Skip to main content

ikigai_script/
endpoints.rs

1//! The resources, as endpoints.
2//!
3//! Only two of them hold state — the script (its head and versions) and a run — and they
4//! hold it through the host's [`Backend`]. Everything else is composed: `…:result` and
5//! `…:runs` read `…:compiled` through the kernel and hand the program to the language's
6//! evaluator as a sub-request; `urn:script:eval` saves a draft by sinking to the script
7//! like any other writer.
8
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::sync::{Arc, Mutex};
11
12use async_trait::async_trait;
13use ikigai_core::{
14    ActionSpec, ArgRef, ArgSpec, Description, Endpoint, EndpointSpace, Error, Exact, Invocation,
15    Iri, ReprType, Representation, Request, Resolution, Result, Scope, Space, SpaceEntry, Topology,
16    UriTemplate, Verb,
17};
18use serde::{Deserialize, Serialize};
19
20use crate::authority::{
21    self, cap, cap_run, principal_of, Act, Ceiling, CeilingPolicy, CAP_ANY, CAP_DELETE, CAP_LISP,
22    CAP_READ, CAP_READ_PUBLIC, CAP_RUN, CAP_RUN_PUBLIC, CAP_WRITE,
23};
24use crate::backend::Backend;
25use crate::model::{
26    self, Event, Head, Language, Outcome, Run, State, Version, MAX_RECORDED_RESULT, SCHEMA,
27};
28use crate::name::{self, CATALOG_IRI, EVAL_IRI};
29use crate::plan;
30use crate::sparql::{self, Analysis, Form, Parameter, SparqlDoor, Value, CAP_READ_GRAPH};
31
32const PLAIN: &str = "text/plain";
33const JSON: &str = "application/json";
34const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
35const XSD_BOOLEAN: &str = "http://www.w3.org/2001/XMLSchema#boolean";
36const XSD_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#integer";
37const XSD_POSITIVE_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#positiveInteger";
38const TURTLE: &str = "text/turtle";
39/// The faces of a read: the source (or a summary) for people, the record for machines.
40const READ_FACES: [&str; 2] = [PLAIN, JSON];
41/// The faces of the catalog and a run record: those, and the graph (see [`crate::graph`]).
42const RECORD_FACES: [&str; 3] = [PLAIN, JSON, TURTLE];
43
44/// What a host decides when it mounts scripts: where they live and the ceiling it allows each
45/// one. Built with [`SpaceConfig::new`] and handed to [`space`].
46///
47/// Who a request comes from is not configured here: it is the principal the request's
48/// capability names (`urn:cap:principal:<iri>`), which the host's door mints with
49/// `Capability::with_principal` (see [`authority::principal_of`]).
50///
51/// There is no default backend and no default ceiling, on purpose: both are the host's
52/// decision, and a library that guessed either would be guessing about authority.
53#[derive(Clone)]
54#[non_exhaustive]
55pub struct SpaceConfig {
56    backend: Arc<dyn Backend>,
57    ceiling: CeilingPolicy,
58    sparql: Option<SparqlDoor>,
59    on_change: Option<ChangeHook>,
60    /// Each script's contract (a published SPARQL script's or plan's own endpoints, the
61    /// template's, or the one a script that is not published wears), built once per head
62    /// and dropped by every write or retire through this crate.
63    described: Arc<Mutex<HashMap<String, Contract>>>,
64}
65
66/// What a host runs when a script's head changes through this crate: see
67/// [`SpaceConfig::on_change`].
68pub type ChangeHook = Arc<dyn Fn(&str) + Send + Sync>;
69
70impl SpaceConfig {
71    /// Scripts kept in `backend`, each run under the ceiling `ceiling` answers for it.
72    pub fn new(backend: Arc<dyn Backend>, ceiling: CeilingPolicy) -> SpaceConfig {
73        SpaceConfig {
74            backend,
75            ceiling,
76            sparql: None,
77            on_change: None,
78            described: Arc::new(Mutex::new(HashMap::new())),
79        }
80    }
81
82    /// Accept SPARQL scripts, evaluated through `door` (the store's graph-scoped doors).
83    /// Without it, publishing `language=sparql` is refused: which store a query runs
84    /// against is the host's decision, as the evaluator a Lisp script reaches is.
85    pub fn sparql(mut self, door: SparqlDoor) -> SpaceConfig {
86        self.sparql = Some(door);
87        self
88    }
89
90    /// Run `hook` with the script's name after every publish or retire through this crate.
91    ///
92    /// ★ **Wire it to `kernel.cut(ikigai_core::BINDINGS_THREAD)`.** A published SPARQL
93    /// script is its own entry in the catalog and the action manifold (`…:result` and
94    /// `…:runs` with ITS parameters as arguments), and the kernel caches every description
95    /// and Meta answer under `urn:kernel:bindings`, which it cannot know a publish changed.
96    /// The library holds no kernel and an endpoint may not cut that thread without
97    /// `urn:cap:kernel:cut`, so the host, which holds both, makes the cut. Unwired, a new
98    /// query is runnable at once but the catalog, MCP and the engine's argument routing
99    /// describe the old set until something else cuts the thread.
100    pub fn on_change(mut self, hook: ChangeHook) -> SpaceConfig {
101        self.on_change = Some(hook);
102        self
103    }
104
105    /// A script's head changed through this crate: forget its contract, tell the host.
106    fn changed(&self, name: &str) {
107        self.described
108            .lock()
109            .expect("the described-script memo")
110            .remove(name);
111        if let Some(hook) = &self.on_change {
112            hook(name);
113        }
114    }
115}
116
117/// Bind every script resource.
118///
119/// ⚠ **Bind order is resolution order** (`EndpointSpace` takes the first grammar that
120/// matches), and `urn:script:{name}`'s template captures the rest of an IRI, so the two
121/// exact names go first and the bare script last. A name cannot contain `:`, so
122/// `urn:script:x:compiled` can only be the compiled form of `x`.
123pub fn space(config: SpaceConfig) -> ScriptSpace {
124    let shared = Arc::new(config);
125    let template = |t: &str| UriTemplate::parse(t).expect("a constant template");
126    let inner = EndpointSpace::new()
127        .bind(Exact::new(EVAL_IRI), EvalEndpoint)
128        .bind(
129            Exact::new(CATALOG_IRI),
130            CatalogEndpoint {
131                shared: Arc::clone(&shared),
132            },
133        )
134        .bind(
135            template("urn:script:{name}:version:{digest}"),
136            VersionEndpoint {
137                shared: Arc::clone(&shared),
138            },
139        )
140        .bind(
141            template("urn:script:{name}:run:{id}"),
142            RunEndpoint {
143                shared: Arc::clone(&shared),
144            },
145        )
146        .bind(
147            template("urn:script:{name}:compiled"),
148            CompiledEndpoint {
149                shared: Arc::clone(&shared),
150            },
151        )
152        .bind(
153            template("urn:script:{name}:result"),
154            ResultEndpoint {
155                shared: Arc::clone(&shared),
156            },
157        )
158        .bind(
159            template("urn:script:{name}:runs"),
160            RunsEndpoint {
161                shared: Arc::clone(&shared),
162            },
163        )
164        .bind(
165            template("urn:script:{name}"),
166            ScriptEndpoint {
167                shared: Arc::clone(&shared),
168            },
169        );
170    ScriptSpace {
171        inner,
172        lisp: LispDoors::new(&shared),
173        shared,
174    }
175}
176
177// ---------------------------------------------------------------------------------------
178// Gates
179// ---------------------------------------------------------------------------------------
180
181fn holds(inv: &Invocation<'_>, scope: &str) -> bool {
182    inv.capability.allows(scope)
183}
184
185/// Whether the caller may `act` on `name`: its exact grant or a namespace grant, and no
186/// exclusion naming it ([`authority::holds`]).
187fn may(inv: &Invocation<'_>, act: Act, name: &str) -> bool {
188    authority::holds(inv.capability, act, name)
189}
190
191/// Whether the caller reaches a PUBLIC script through `public` (a `…:public` grant), unless
192/// an exclusion it holds names the script.
193fn may_public(inv: &Invocation<'_>, act: Act, public: &str, name: &str) -> bool {
194    holds(inv, public) && !authority::excluded(inv.capability, act, name)
195}
196
197/// How a refusal says what a script grant can name.
198const GRANT_FORMS: &str = "A script grant names one script (`urn:cap:script:{act}:{name}`) or a \
199     namespace of them (`urn:cap:script:{act}:{namespace}-*`)";
200
201/// Public and published: what the `…:public` grants reach.
202fn is_public(head: &Head) -> bool {
203    head.public && head.state == State::Published
204}
205
206/// The head of `name`, if the caller may read it.
207///
208/// ★ No existence oracle: a caller without the script's own read grant is told `Denied`
209/// whether the script exists or not, unless it is public and they hold
210/// [`CAP_READ_PUBLIC`]. Only a caller who may read it learns that it is absent.
211fn readable_head(inv: &Invocation<'_>, shared: &SpaceConfig, name: &str) -> Result<Option<Head>> {
212    gated_head(inv, shared, name, &[(Act::Read, CAP_READ_PUBLIC)])
213}
214
215/// The head of `name` if the caller may do one of `acts` to it ([`may`]), or the script is
216/// public and they hold that act's public grant. Otherwise `Denied`, naming both.
217fn gated_head(
218    inv: &Invocation<'_>,
219    shared: &SpaceConfig,
220    name: &str,
221    acts: &[(Act, &str)],
222) -> Result<Option<Head>> {
223    if acts.iter().any(|(act, _)| may(inv, *act, name)) {
224        return shared.backend.head(name);
225    }
226    if let Ok(Some(head)) = shared.backend.head(name) {
227        if is_public(&head)
228            && acts
229                .iter()
230                .any(|(act, public)| may_public(inv, *act, public, name))
231        {
232            return Ok(Some(head));
233        }
234    }
235    Err(Error::Denied(format!(
236        "this capability holds none of {} — nor, for a public script, {}. {GRANT_FORMS}, and \
237         an exclusion (`…:-{{name}}`) takes one back out",
238        acts.iter()
239            .map(|(act, _)| cap(*act, name))
240            .collect::<Vec<_>>()
241            .join(", "),
242        acts.iter()
243            .map(|(_, public)| *public)
244            .collect::<Vec<_>>()
245            .join(", ")
246    )))
247}
248
249fn require(inv: &Invocation<'_>, act: Act, name: &str, doing: &str) -> Result<()> {
250    if may(inv, act, name) {
251        Ok(())
252    } else {
253        Err(Error::Denied(format!(
254            "{doing} needs `{}`, which this capability does not hold. {GRANT_FORMS}, and an \
255             exclusion (`…:-{{name}}`) takes one back out",
256            cap(act, name)
257        )))
258    }
259}
260
261fn found(head: Option<Head>, name: &str) -> Result<Head> {
262    head.ok_or_else(|| Error::NotFound(format!("no script is published at urn:script:{name}")))
263}
264
265// ---------------------------------------------------------------------------------------
266// Drafts: a version never published is its author's alone
267// ---------------------------------------------------------------------------------------
268
269/// Who may see one version of a script, beyond the read (or run) grant that gates it.
270///
271/// ★ **A draft is visible only to its author until it is published.** The author is the
272/// principal recorded when the draft was written, the principal the writer's CAPABILITY
273/// named ([`authority::principal_of`]), and the reader is its author when their capability
274/// `acts_as` that principal. Never an argument, so no caller can name itself the author.
275/// This only ever NARROWS what a grant reaches: seeing a draft needs the grant AND
276/// authorship, so it can give no one more than their capability.
277///
278/// Every answer is CACHEABLE, whatever the sight (ledger #1077). It is a function of the
279/// capability and the script's state alone: the kernel keys its cache on the capability,
280/// which carries the principal, so alice's cached draft is never bob's answer even when the
281/// rest of their capabilities are the same; and every answer hangs from the script's
282/// thread, which a publish or retire cuts. Before 0.2.0 the principal came from a host
283/// stamper the cache key could not see, and these answers could not be cached.
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285enum Sight {
286    /// Published content, the caller's own draft, or a root caller.
287    Visible,
288    /// A version never published, and the caller is not its author: absent, told exactly
289    /// as a name nobody wrote is (`NotFound`, `false`), so its existence does not leak.
290    Hidden,
291}
292
293/// Whether `digest` was ever published under this head: the head published at it, or any
294/// change in its history did. Once published, always: history only grows.
295fn ever_published(head: &Head, digest: &str) -> bool {
296    (head.version == digest && head.state == State::Published)
297        || head
298            .history
299            .iter()
300            .any(|event| event.action == "publish" && event.version == digest)
301}
302
303/// What the caller may see of version `digest` of `head`'s script.
304///
305/// Root sees everything: it is the host's own authority, not a principal's, and holds the
306/// backend the draft is stored in (`acts_as` is true for root, and root also sees a draft
307/// whose author is nobody's identity).
308fn sight(inv: &Invocation<'_>, head: &Head, digest: &str) -> Sight {
309    if ever_published(head, digest) || inv.capability.is_root() {
310        return Sight::Visible;
311    }
312    let wrote_it = |author: &str| authority::is_identity(author) && inv.capability.acts_as(author);
313    let mine = (head.version == digest && wrote_it(&head.publisher))
314        || head
315            .history
316            .iter()
317            .any(|e| e.action == "draft" && e.version == digest && wrote_it(&e.principal));
318    if mine {
319        Sight::Visible
320    } else {
321        Sight::Hidden
322    }
323}
324
325/// `found`'s NotFound for a name the caller may read but cannot see: the same words as for
326/// a name nobody wrote, plus, for a capability naming no principal (which says nothing
327/// about any one name), why that caller can see no draft at all.
328fn absent(inv: &Invocation<'_>, name: &str) -> Error {
329    let mut message = format!("no script is published at urn:script:{name}");
330    if inv.capability.principal().is_none() && !inv.capability.is_root() {
331        message.push_str(
332            " (this request's capability names no principal (`urn:cap:principal:…`, minted \
333             by the host's door), so it cannot be shown to be a draft's author: a draft is \
334             visible to its author and root alone until it is published)",
335        );
336    }
337    Error::NotFound(message)
338}
339
340/// The head of `name`, from a gate's answer, if the caller may also SEE the version it
341/// points at.
342fn seen_head(inv: &Invocation<'_>, name: &str, gated: Option<Head>) -> Result<Head> {
343    let Some(head) = gated else {
344        return Err(absent(inv, name));
345    };
346    match sight(inv, &head, &head.version) {
347        Sight::Hidden => Err(absent(inv, name)),
348        Sight::Visible => Ok(head),
349    }
350}
351
352fn head_version(shared: &SpaceConfig, head: &Head) -> Result<Version> {
353    shared
354        .backend
355        .version(&head.name, &head.version)?
356        .ok_or_else(|| {
357            Error::Endpoint(format!(
358                "urn:script:{} points at version {}, which its backend does not hold",
359                head.name, head.version
360            ))
361        })
362}
363
364// ---------------------------------------------------------------------------------------
365// Arguments and faces
366// ---------------------------------------------------------------------------------------
367
368/// An optional inline argument; present-but-not-UTF-8 is an error, absent is `None`.
369fn optional<'a>(inv: &'a Invocation<'_>, name: &str) -> Result<Option<&'a str>> {
370    match inv.inline_str(name) {
371        Ok(value) => Ok(Some(value)),
372        Err(Error::MissingArgument(_)) => Ok(None),
373        Err(other) => Err(other),
374    }
375}
376
377fn parse_bool(name: &str, text: &str) -> Result<bool> {
378    match text.trim() {
379        "true" => Ok(true),
380        "false" => Ok(false),
381        other => Err(Error::InvalidArgument {
382            name: name.to_string(),
383            detail: format!("`{other}` is not `true` or `false`"),
384        }),
385    }
386}
387
388/// Which face was asked for: [`READ_FACES`]. One this resource cannot serve is refused,
389/// never substituted.
390fn wanted_face(inv: &Invocation<'_>) -> Result<&'static str> {
391    face_of(inv, &READ_FACES)
392}
393
394/// Which of `faces` was asked for (the first when none was).
395fn face_of(inv: &Invocation<'_>, faces: &[&'static str]) -> Result<&'static str> {
396    match optional(inv, "as")? {
397        None => Ok(faces[0]),
398        Some(asked) => {
399            let bare = asked.split(';').next().unwrap_or(asked).trim();
400            faces
401                .iter()
402                .find(|face| **face == bare)
403                .copied()
404                .ok_or_else(|| Error::InvalidArgument {
405                    name: "as".to_string(),
406                    detail: format!(
407                        "`{asked}` is not a face this resource serves; one of {}",
408                        faces.join(", ")
409                    ),
410                })
411        }
412    }
413}
414
415fn plain(text: impl Into<String>) -> Representation {
416    Representation::new(
417        ReprType::new(PLAIN).with_param("charset", "utf-8"),
418        text.into().into_bytes(),
419    )
420}
421
422fn json<T: Serialize>(value: &T) -> Result<Representation> {
423    let mut bytes = serde_json::to_vec_pretty(value)
424        .map_err(|e| Error::Endpoint(format!("could not serialize a record: {e}")))?;
425    bytes.push(b'\n');
426    Ok(Representation::new(ReprType::new(JSON), bytes))
427}
428
429fn iri(text: &str) -> Result<Iri> {
430    Iri::parse(text).map_err(|e| Error::Endpoint(format!("`{text}` is not an IRI: {e}")))
431}
432
433fn inline(text: &str) -> ArgRef {
434    ArgRef::Inline(text.as_bytes().to_vec())
435}
436
437fn name_arg() -> ArgSpec {
438    ArgSpec::new("name")
439        .summary(
440            "The script's name: one segment of lowercase letters, digits, `-` and `_` \
441             (`eval`, `catalog`, `public`, `outcome` and `principal` are reserved).",
442        )
443        .class(XSD_STRING)
444        .binding()
445}
446
447fn as_arg() -> ArgSpec {
448    ArgSpec::new("as")
449        .summary(
450            "The face: `text/plain` (for people) or `application/json` (the versioned \
451             record, `\"schema\": 1`). Any other is refused, never substituted.",
452        )
453        .class(XSD_STRING)
454        .one_of(READ_FACES)
455        .default_value(PLAIN)
456        .optional()
457}
458
459/// `as` for a record with a graph face.
460fn record_as_arg() -> ArgSpec {
461    ArgSpec::new("as")
462        .summary(
463            "The face: `text/plain` (for people), `application/json` (the versioned record, \
464             `\"schema\": 1`) or `text/turtle` (the graph: PROV-O and the shared \
465             vocabulary, no blank nodes). Any other is refused, never substituted.",
466        )
467        .class(XSD_STRING)
468        .one_of(RECORD_FACES)
469        .default_value(PLAIN)
470        .optional()
471}
472
473fn turtle(graph: crate::graph::Graph) -> Result<Representation> {
474    let bytes = graph
475        .turtle()
476        .map_err(|e| Error::Endpoint(format!("could not write the graph face: {e}")))?;
477    Ok(Representation::new(
478        ReprType::new(TURTLE).with_param("charset", "utf-8"),
479        bytes,
480    ))
481}
482
483fn data_arg(arg: &str, summary: &str) -> ArgSpec {
484    ArgSpec::new(arg)
485        .summary(summary)
486        .class(XSD_STRING)
487        .optional()
488}
489
490// ---------------------------------------------------------------------------------------
491// urn:script:{name} — the script (the head), and publishing
492// ---------------------------------------------------------------------------------------
493
494/// The script's JSON face: its head, flattened, with the version it points at.
495#[derive(Serialize)]
496struct ScriptDocument<'a> {
497    iri: String,
498    #[serde(flatten)]
499    head: &'a Head,
500    #[serde(rename = "versionIri")]
501    version_iri: String,
502    language: Language,
503    requires: &'a BTreeSet<String>,
504    #[serde(skip_serializing_if = "Option::is_none")]
505    budget: Option<u64>,
506    source: &'a str,
507}
508
509struct ScriptEndpoint {
510    shared: Arc<SpaceConfig>,
511}
512
513#[async_trait]
514impl Endpoint for ScriptEndpoint {
515    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
516        let name = name::from_bindings(inv)?;
517        let shared = &self.shared;
518        match inv.request.verb {
519            Verb::Exists => {
520                let exists = match readable_head(inv, shared, &name)? {
521                    None => false,
522                    Some(head) => sight(inv, &head, &head.version) == Sight::Visible,
523                };
524                Ok(plain(if exists { "true\n" } else { "false\n" }).cacheable())
525            }
526            Verb::Source => {
527                let want = wanted_face(inv)?;
528                let head = seen_head(inv, &name, readable_head(inv, shared, &name)?)?;
529                let version = head_version(shared, &head)?;
530                let repr = if want == JSON {
531                    json(&ScriptDocument {
532                        iri: name::script_iri(&name),
533                        head: &head,
534                        version_iri: name::version_iri(&name, &head.version),
535                        language: version.language,
536                        requires: &version.requires,
537                        budget: version.budget,
538                        source: &version.source,
539                    })?
540                } else {
541                    plain(version.source)
542                };
543                Ok(repr.cacheable())
544            }
545            Verb::Sink => publish(inv, shared, &name).await,
546            Verb::Delete => retire(inv, shared, &name),
547            other => Err(unsupported("script", other)),
548        }
549    }
550
551    fn name(&self) -> &str {
552        "script"
553    }
554
555    fn describe(&self) -> Description {
556        Description::new("script")
557            .title("A script")
558            .summary(
559                "A script as a resource: fetch its source without running it, publish or \
560                 replace it (every write is a new content-addressed version), or retire it.",
561            )
562            .verb(Verb::Meta)
563            .action(
564                ActionSpec::new(Verb::Source)
565                    .summary(
566                        "The script, WITHOUT running it: its source (`text/plain`), or its head \
567                         record (`application/json`): state, public flag, version, what it \
568                         declares and what its publisher held. Needs \
569                         `urn:cap:script:read:{name}`, or `urn:cap:script:read:public` for a \
570                         public published script.",
571                    )
572                    .input(name_arg())
573                    .input(as_arg())
574                    .output(PLAIN)
575                    .output(JSON)
576                    .requires(CAP_READ),
577            )
578            .action(
579                ActionSpec::new(Verb::Exists)
580                    .summary("Whether the script exists (to a caller who may read it).")
581                    .input(name_arg())
582                    .output(PLAIN)
583                    .requires(CAP_READ),
584            )
585            .action(
586                ActionSpec::new(Verb::Sink)
587                    .summary(
588                        "Publish or replace the script; answers the new version's IRI. The \
589                         declared `requires` (plus the language's own capability) must all \
590                         be held by the publisher, or the publish is refused: a script never \
591                         runs with more than its publisher held. Needs \
592                         `urn:cap:script:write:{name}`.",
593                    )
594                    .input(name_arg())
595                    .input(
596                        ArgSpec::new("content")
597                            .summary("The source text.")
598                            .class(XSD_STRING),
599                    )
600                    .input(
601                        ArgSpec::new("language")
602                            .summary(
603                                "The script's language: `lisp`, `sparql` (a query or update, \
604                                 when the host mounts a SPARQL door), or `plan` (an \
605                                 ik:Process graph, when the host binds the plan doors).",
606                            )
607                            .class(XSD_STRING)
608                            .one_of(Language::ALL.map(Language::as_str))
609                            .default_value("lisp")
610                            .optional(),
611                    )
612                    .input(
613                        ArgSpec::new("requires")
614                            .summary(
615                                "Whitespace-separated `urn:cap:` grants the script needs to \
616                                 run. Lisp: the language's capability is added; exact grants \
617                                 only, no wildcards, no exclusions. SPARQL: DERIVED from the \
618                                 graphs the text names; a plan: DERIVED from its steps' \
619                                 contracts. For both, omit it: one that says anything else \
620                                 is refused, naming the derived set.",
621                            )
622                            .class(XSD_STRING)
623                            .optional(),
624                    )
625                    .input(
626                        ArgSpec::new("budget")
627                            .summary(
628                                "The store time budget the script's runs may have, in \
629                                 milliseconds (at most 120000). Refused unless the publisher \
630                                 holds `urn:cap:store:budget:<m>` with m at least this (root \
631                                 holds 120000). A run gets the least of this, what the \
632                                 publisher held, the host's ceiling and the runner's own \
633                                 budget grant; omitted, every run gets the store's base. Any \
634                                 language.",
635                            )
636                            .class(XSD_POSITIVE_INTEGER)
637                            .optional(),
638                    )
639                    .input(
640                        ArgSpec::new("public")
641                            .summary(
642                                "Whether holders of `urn:cap:script:run:public` (a host's \
643                                 anonymous principal) may run it and holders of \
644                                 `urn:cap:script:read:public` read it.",
645                            )
646                            .class(XSD_BOOLEAN)
647                            .one_of(["true", "false"])
648                            .default_value("false")
649                            .optional(),
650                    )
651                    .input(
652                        ArgSpec::new("state")
653                            .summary(
654                                "`published` (runnable) or `draft` (saved, not runnable, \
655                                 and visible only to its author until published). To \
656                                 retire, Delete.",
657                            )
658                            .class(XSD_STRING)
659                            .one_of(["published", "draft"])
660                            .default_value("published")
661                            .optional(),
662                    )
663                    .input(
664                        ArgSpec::new("if-version")
665                            .summary(
666                                "Write only if the head is still this version (`none`: only \
667                                 if the script does not exist yet); otherwise `Conflict`.",
668                            )
669                            .class(XSD_STRING)
670                            .optional(),
671                    )
672                    .output(PLAIN)
673                    .requires(CAP_WRITE),
674            )
675            .action(
676                ActionSpec::new(Verb::Delete)
677                    .summary(
678                        "Retire the script: it stays readable and its versions fetchable, but \
679                         it never runs again until republished. Needs \
680                         `urn:cap:script:delete:{name}`.",
681                    )
682                    .input(name_arg())
683                    .output(PLAIN)
684                    .requires(CAP_DELETE),
685            )
686    }
687}
688
689async fn publish(inv: &Invocation<'_>, shared: &SpaceConfig, name: &str) -> Result<Representation> {
690    require(inv, Act::Write, name, "publishing a script")?;
691    let source = inv.inline_str("content")?;
692    let language = optional(inv, "language")?
693        .map(Language::parse)
694        .transpose()?
695        .unwrap_or(Language::Lisp);
696    let requires = match language {
697        Language::Lisp => {
698            let mut requires = optional(inv, "requires")?
699                .map(authority::parse_requires)
700                .transpose()?
701                .unwrap_or_default();
702            requires.insert(CAP_LISP.to_string());
703            requires
704        }
705        Language::Sparql => derived_requires(inv, shared, source)?,
706        Language::Plan => {
707            // Validated by the host's validator first, so a malformed plan is refused
708            // naming the shape it broke; then read for what this crate decides itself;
709            // then its authority derived from its steps' contracts.
710            plan::check_bound(source)?;
711            plan::validate(inv, source).await?;
712            plan::analyze(source)?;
713            plan::derive(inv, source, optional(inv, "requires")?).await?
714        }
715    };
716    let budget = optional(inv, "budget")?
717        .map(authority::parse_budget)
718        .transpose()?;
719    let public = optional(inv, "public")?
720        .map(|v| parse_bool("public", v))
721        .transpose()?
722        .unwrap_or(false);
723    let state = match optional(inv, "state")?.map(str::trim) {
724        None | Some("published") => State::Published,
725        Some("draft") => State::Draft,
726        Some(other) => {
727            return Err(Error::InvalidArgument {
728                name: "state".to_string(),
729                detail: format!(
730                    "`{other}` is not `published` or `draft`; to retire a script, Delete it"
731                ),
732            })
733        }
734    };
735    // ★ No elevation: refused here, before anything is written, if the publisher does not
736    // hold every scope the script declares.
737    let grant = authority::grant_at_publish(inv.capability, &requires)?;
738    // …and the same for store time (ledger #1384): a declared budget is refused unless the
739    // publisher holds at least that much, and what they held is kept with the head.
740    let granted_budget = budget
741        .map(|millis| authority::budget_at_publish(inv.capability, millis))
742        .transpose()?;
743
744    let version = Version {
745        language,
746        requires,
747        budget,
748        source: source.to_string(),
749    };
750    let digest = version.digest();
751    let current = shared.backend.head(name)?;
752    if let Some(expected) = optional(inv, "if-version")? {
753        let at = current.as_ref().map_or("none", |h| h.version.as_str());
754        if at != expected.trim() {
755            return Err(Error::Conflict(format!(
756                "urn:script:{name} is at {at}, not {}; read it again and retry",
757                expected.trim()
758            )));
759        }
760    }
761    shared.backend.put_version(name, &version)?;
762
763    let principal = principal_of(inv.capability);
764    let at = inv.now().map(|t| t.as_millis());
765    let mut history = current
766        .as_ref()
767        .map(|h| h.history.clone())
768        .unwrap_or_default();
769    history.push(Event {
770        action: if state == State::Draft {
771            "draft"
772        } else {
773            "publish"
774        }
775        .to_string(),
776        version: digest.clone(),
777        state,
778        public,
779        principal: principal.clone(),
780        at,
781    });
782    let head = Head {
783        schema: SCHEMA,
784        name: name.to_string(),
785        version: digest.clone(),
786        state,
787        public,
788        granted: grant.granted,
789        exclusions: grant.exclusions,
790        granted_budget,
791        publisher: principal,
792        updated: at,
793        history,
794    };
795    shared.backend.swap_head(name, current.as_ref(), &head)?;
796    shared.changed(name);
797    Ok(plain(format!("{}\n", name::version_iri(name, &digest))))
798}
799
800/// A SPARQL script's authority: DERIVED from the graphs its text names, never declared.
801/// A `requires=` is accepted only when it says exactly the same thing, and refused naming
802/// the derived set when it does not, so an author can neither under- nor over-declare.
803fn derived_requires(
804    inv: &Invocation<'_>,
805    shared: &SpaceConfig,
806    source: &str,
807) -> Result<BTreeSet<String>> {
808    let door = door(shared).map_err(|_| Error::InvalidArgument {
809        name: "language".to_string(),
810        detail: "this host takes no SPARQL scripts: it mounts no SPARQL door".to_string(),
811    })?;
812    let derived = sparql::analyze(source, door)?.requires();
813    if let Some(said) = optional(inv, "requires")? {
814        let said: BTreeSet<String> = said.split_whitespace().map(str::to_string).collect();
815        if said != derived {
816            return Err(Error::InvalidArgument {
817                name: "requires".to_string(),
818                detail: format!(
819                    "a SPARQL script's authority is derived from the graphs its text names, \
820                     not declared, and this one derives {}; the `requires` given says {}. \
821                     Omit `requires`",
822                    listed(&derived),
823                    listed(&said)
824                ),
825            });
826        }
827    }
828    Ok(derived)
829}
830
831fn listed(scopes: &BTreeSet<String>) -> String {
832    if scopes.is_empty() {
833        "nothing".to_string()
834    } else {
835        scopes.iter().cloned().collect::<Vec<_>>().join(" ")
836    }
837}
838
839fn door(shared: &SpaceConfig) -> Result<&SparqlDoor> {
840    shared.sparql.as_ref().ok_or_else(|| {
841        Error::Endpoint(
842            "this host mounts no SPARQL door (`SpaceConfig::sparql`), so it cannot run a \
843             SPARQL script"
844                .to_string(),
845        )
846    })
847}
848
849fn retire(inv: &Invocation<'_>, shared: &SpaceConfig, name: &str) -> Result<Representation> {
850    require(inv, Act::Delete, name, "retiring a script")?;
851    let current = found(shared.backend.head(name)?, name)?;
852    let iri = name::script_iri(name);
853    if current.state == State::Retired {
854        return Ok(plain(format!("{iri} was already retired\n")));
855    }
856    let at = inv.now().map(|t| t.as_millis());
857    let mut head = current.clone();
858    head.state = State::Retired;
859    head.updated = at;
860    head.history.push(Event {
861        action: "retire".to_string(),
862        version: current.version.clone(),
863        state: State::Retired,
864        public: current.public,
865        principal: principal_of(inv.capability),
866        at,
867    });
868    shared.backend.swap_head(name, Some(&current), &head)?;
869    shared.changed(name);
870    Ok(plain(format!("retired {iri}\n")))
871}
872
873// ---------------------------------------------------------------------------------------
874// urn:script:{name}:version:{digest}
875// ---------------------------------------------------------------------------------------
876
877struct VersionEndpoint {
878    shared: Arc<SpaceConfig>,
879}
880
881#[async_trait]
882impl Endpoint for VersionEndpoint {
883    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
884        let name = name::from_bindings(inv)?;
885        let digest = inv
886            .bindings
887            .get("digest")
888            .ok_or_else(|| Error::Endpoint("no `digest` captured".to_string()))?
889            .to_string();
890        // ★ Hung from the SCRIPT's thread, which every publish and retire cuts, and named
891        // FIRST, so every answer below carries it: a success (ledger #1076) and a NotFound
892        // alike (ledger #1079). Everything this answer says is the script's state, not the
893        // version's: whether that content has been written under the name (a version named
894        // by its content can be asked for before anyone publishes it), whether the caller may
895        // see it (a draft is its author's until it is published), and whether the caller may
896        // read the name at all (a public script's version stops being readable through the
897        // public grant when it is retired). The kernel hangs an answer from its OWN name,
898        // which no write here ever targets, so without this a cached `false`, or a cached
899        // fallback over a NotFound, outlived the publish that ended the absence. Caching the
900        // negative is kept, not dropped: it is a backend read per ask, and an uncacheable
901        // `Exists` would make every composite over it uncacheable. The kernel carries the
902        // thread on a miss only (NotFound, Unresolved); a refusal stays never-cached.
903        inv.depends_on(name::script_iri(&name));
904        let head = readable_head(inv, &self.shared, &name)?;
905        let version = self.shared.backend.version(&name, &digest);
906        // A version that exists is seen as its head's history says: one never published is
907        // its author's alone. One with no head at all (a write that lost its race) was never
908        // published either.
909        let seen = match (&version, &head) {
910            (Ok(Some(_)), Some(head)) => sight(inv, head, &digest),
911            (Ok(Some(_)), None) if !inv.capability.is_root() => Sight::Hidden,
912            _ => Sight::Visible,
913        };
914        let no_such = || Error::NotFound(format!("urn:script:{name} has no version {digest}"));
915        match inv.request.verb {
916            Verb::Exists => {
917                let exists = match version {
918                    Ok(v) => v.is_some() && seen != Sight::Hidden,
919                    Err(Error::InvalidArgument { .. }) => false,
920                    Err(other) => return Err(other),
921                };
922                Ok(plain(if exists { "true\n" } else { "false\n" }).cacheable())
923            }
924            Verb::Source => {
925                let want = wanted_face(inv)?;
926                let version = version?.ok_or_else(no_such)?;
927                if seen == Sight::Hidden {
928                    return Err(no_such());
929                }
930                let repr = if want == JSON {
931                    json(&version.document(&name))?
932                } else {
933                    plain(version.source)
934                };
935                Ok(repr.cacheable())
936            }
937            other => Err(unsupported("script-version", other)),
938        }
939    }
940
941    fn name(&self) -> &str {
942        "script-version"
943    }
944
945    fn describe(&self) -> Description {
946        let digest = || {
947            ArgSpec::new("digest")
948                .summary("The version's digest: `sha256:` and 64 hex digits over its content.")
949                .class(XSD_STRING)
950                .binding()
951        };
952        Description::new("script-version")
953            .title("One version of a script")
954            .summary(
955                "An immutable version, named by the sha256 of its content (language, declared \
956                 capability, source). Republishing moves the head; every version stays \
957                 fetchable here.",
958            )
959            .verb(Verb::Meta)
960            .action(
961                ActionSpec::new(Verb::Source)
962                    .summary(
963                        "The version's source (`text/plain`) or its record (`application/json`).",
964                    )
965                    .input(name_arg())
966                    .input(digest())
967                    .input(as_arg())
968                    .output(PLAIN)
969                    .output(JSON)
970                    .requires(CAP_READ),
971            )
972            .action(
973                ActionSpec::new(Verb::Exists)
974                    .summary("Whether the script has this version.")
975                    .input(name_arg())
976                    .input(digest())
977                    .output(PLAIN)
978                    .requires(CAP_READ),
979            )
980    }
981}
982
983// ---------------------------------------------------------------------------------------
984// urn:script:{name}:compiled — the prepared form
985// ---------------------------------------------------------------------------------------
986
987/// The compiled form: everything a run needs, in one cacheable resource hung from the
988/// script's thread. For Lisp the "compilation" is preparation — the evaluator offers no
989/// separate compile step to cache — so this is the program and the authority it runs
990/// under, bound to the version they came from. For SPARQL it adds what the parsed text
991/// says: its form, its parameters and the graphs it names ([`Analysis`]).
992#[derive(Clone, Debug, Serialize, Deserialize)]
993pub struct Prepared {
994    /// [`SCHEMA`].
995    pub schema: u32,
996    /// The script.
997    pub name: String,
998    /// `urn:script:{name}:compiled`.
999    pub iri: String,
1000    /// The version prepared.
1001    pub version: String,
1002    /// Its language.
1003    pub language: Language,
1004    /// The door a run issues to: `urn:lisp:eval`, or the host's SPARQL door for this form.
1005    pub evaluator: String,
1006    /// The program.
1007    pub program: String,
1008    /// What it declares (for SPARQL: what its text derives).
1009    pub requires: BTreeSet<String>,
1010    /// What its publisher held of that.
1011    pub granted: BTreeSet<String>,
1012    /// The publisher's exclusions.
1013    pub exclusions: BTreeSet<String>,
1014    /// The store time budget it declares, in milliseconds.
1015    #[serde(default, skip_serializing_if = "Option::is_none")]
1016    pub budget: Option<u64>,
1017    /// The store time budget its publisher held when they published it.
1018    #[serde(default, skip_serializing_if = "Option::is_none")]
1019    pub granted_budget: Option<u64>,
1020    /// Whether it is public.
1021    pub public: bool,
1022    /// Its state.
1023    pub state: State,
1024    /// For a SPARQL script: its form, parameters and graphs, read from the parsed text.
1025    #[serde(default, skip_serializing_if = "Option::is_none")]
1026    pub sparql: Option<Analysis>,
1027    /// For a plan: its process, its steps' verbs (read or write) and its parameters.
1028    #[serde(default, skip_serializing_if = "Option::is_none")]
1029    pub plan: Option<plan::Analysis>,
1030}
1031
1032struct CompiledEndpoint {
1033    shared: Arc<SpaceConfig>,
1034}
1035
1036#[async_trait]
1037impl Endpoint for CompiledEndpoint {
1038    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
1039        if inv.request.verb != Verb::Source {
1040            return Err(unsupported("script-compiled", inv.request.verb));
1041        }
1042        let name = name::from_bindings(inv)?;
1043        // ★ Hung from the SCRIPT's thread, which every publish and retire cuts (the kernel
1044        // cuts the thread named after a Sink's or Delete's target), and named FIRST so a
1045        // NotFound carries it as well as a success (ledger #1079): without it a cached
1046        // compiled form would outlive the version it was prepared from, and a cached fallback
1047        // over "nothing is published here" (or "this draft is not yours") would outlive the
1048        // publish that ended it. A run as a read reaches this form by sub-request, so its
1049        // answers, and its misses, inherit the thread.
1050        inv.depends_on(name::script_iri(&name));
1051        // Readable by a reader or a runner: a run reads its program here. A draft's program
1052        // is its author's alone, so a run of someone else's draft finds nothing to run.
1053        let gated = gated_head(
1054            inv,
1055            &self.shared,
1056            &name,
1057            &[(Act::Read, CAP_READ_PUBLIC), (Act::Run, CAP_RUN_PUBLIC)],
1058        )?;
1059        let head = seen_head(inv, &name, gated)?;
1060        let version = head_version(&self.shared, &head)?;
1061        let (evaluator, analysis, plan_analysis) = match version.language {
1062            Language::Lisp => (LISP_EVAL.to_string(), None, None),
1063            Language::Sparql => {
1064                let door = door(&self.shared)?;
1065                let analysis = sparql::analyze(&version.source, door)?;
1066                (door.query_iri(analysis.form), Some(analysis), None)
1067            }
1068            Language::Plan => (
1069                plan::EVAL.to_string(),
1070                None,
1071                Some(plan::analyze(&version.source)?),
1072            ),
1073        };
1074        let prepared = Prepared {
1075            schema: SCHEMA,
1076            iri: name::part_iri(&name, "compiled"),
1077            version: head.version.clone(),
1078            language: version.language,
1079            evaluator,
1080            program: version.source,
1081            requires: version.requires,
1082            granted: head.granted,
1083            exclusions: head.exclusions,
1084            budget: version.budget,
1085            granted_budget: head.granted_budget,
1086            public: head.public,
1087            state: head.state,
1088            sparql: analysis,
1089            plan: plan_analysis,
1090            name: name.clone(),
1091        };
1092        Ok(json(&prepared)?.cacheable())
1093    }
1094
1095    fn name(&self) -> &str {
1096        "script-compiled"
1097    }
1098
1099    fn describe(&self) -> Description {
1100        Description::new("script-compiled")
1101            .title("A script's compiled form")
1102            .summary(
1103                "The head version prepared for its evaluator, with the authority it runs \
1104                 under (and, for SPARQL, its form, parameters and graphs; for a plan, its \
1105                 steps' verbs and its parameters): cached, and cut \
1106                 whenever the script is republished or retired. Readable by a holder of the \
1107                 script's read OR run grant (a run reads its program here).",
1108            )
1109            .verb(Verb::Meta)
1110            .action(
1111                ActionSpec::new(Verb::Source)
1112                    .summary("The prepared form, as JSON.")
1113                    .input(name_arg())
1114                    .output(JSON)
1115                    .requires(CAP_ANY),
1116            )
1117    }
1118}
1119
1120// ---------------------------------------------------------------------------------------
1121// Running: …:result (a read) and …:runs (a write)
1122// ---------------------------------------------------------------------------------------
1123
1124const LISP_EVAL: &str = "urn:lisp:eval";
1125
1126/// Read the compiled form through the kernel, check the run gate and the state, and
1127/// compute what the run may keep of the runner's capability (and under which ceiling).
1128async fn prepare_run(
1129    inv: &Invocation<'_>,
1130    shared: &SpaceConfig,
1131    name: &str,
1132) -> Result<(Prepared, BTreeSet<String>, Ceiling)> {
1133    // Composed, not reached into: the run is a reader of its own compiled form, so the
1134    // compiled form's thread (and the script's, through it) lands on the result.
1135    let compiled = inv
1136        .issue(Request::new(
1137            Verb::Source,
1138            iri(&name::part_iri(name, "compiled"))?,
1139        ))
1140        .await?;
1141    let prepared: Prepared = serde_json::from_slice(&compiled.bytes).map_err(|e| {
1142        Error::Endpoint(format!(
1143            "urn:script:{name}:compiled did not answer a prepared form: {e}"
1144        ))
1145    })?;
1146    if prepared.name != name {
1147        return Err(Error::Endpoint(format!(
1148            "urn:script:{name}:compiled answered for `{}`",
1149            prepared.name
1150        )));
1151    }
1152    let public = prepared.public && prepared.state == State::Published;
1153    let may_run =
1154        may(inv, Act::Run, name) || (public && may_public(inv, Act::Run, CAP_RUN_PUBLIC, name));
1155    let run_grant = || {
1156        format!(
1157            "`{}` or a namespace grant covering it{}",
1158            cap_run(name),
1159            if prepared.public {
1160                format!(" (or `{CAP_RUN_PUBLIC}`, since it is public)")
1161            } else {
1162                " — it is not public".to_string()
1163            }
1164        )
1165    };
1166    // ★ The STATE first (ledger #1261): whoever reached the compiled form may read or run
1167    // this script and can see its head version (a draft's author, or root), so telling them
1168    // it is a draft or retired discloses nothing, and it is the reason the run cannot happen
1169    // whatever grant they hold. The run grant it will also need is said after it.
1170    let also = || {
1171        if may_run {
1172            String::new()
1173        } else {
1174            format!("; running it will also need {}", run_grant())
1175        }
1176    };
1177    match prepared.state {
1178        State::Published => {}
1179        State::Draft => {
1180            return Err(Error::Conflict(format!(
1181                "urn:script:{name} is a draft; publish it (`state=published`) to run it{}{}",
1182                ad_hoc(prepared.language),
1183                also()
1184            )))
1185        }
1186        State::Retired => {
1187            return Err(Error::Conflict(format!(
1188                "urn:script:{name} is retired; publish it again to run it{}",
1189                also()
1190            )))
1191        }
1192    }
1193    if !may_run {
1194        return Err(Error::Denied(format!(
1195            "running urn:script:{name} needs {}. {GRANT_FORMS}, and an exclusion \
1196             (`…:-{{name}}`) takes one back out",
1197            run_grant()
1198        )));
1199    }
1200    let ceiling = (shared.ceiling)(name);
1201    let keep = authority::effective(
1202        &prepared.requires,
1203        &prepared.granted,
1204        &prepared.exclusions,
1205        &ceiling,
1206    );
1207    Ok((prepared, keep, ceiling))
1208}
1209
1210/// Where a draft's code can run without publishing it, said only for a language that door
1211/// takes: `urn:script:eval` runs Lisp and refuses SPARQL and plans.
1212fn ad_hoc(language: Language) -> String {
1213    match language {
1214        Language::Lisp => format!(", or try the code at {EVAL_IRI}"),
1215        Language::Plan => format!(
1216            ", or run the plan itself at {} (`in` = the plan), under your own capability",
1217            plan::EVAL
1218        ),
1219        Language::Sparql => String::new(),
1220    }
1221}
1222
1223/// The Lisp evaluator sub-request: `in` is the program, `data` what it reads with `(input)`.
1224fn lisp_request(program: &str, data: Option<&str>) -> Result<Request> {
1225    let mut request = Request::new(Verb::Source, iri(LISP_EVAL)?).with_arg("in", inline(program));
1226    if let Some(data) = data {
1227        request = request.with_arg("data", inline(data));
1228    }
1229    Ok(request)
1230}
1231
1232/// `run_args`: the description of the run's input, shared by both run doors.
1233const DATA_SUMMARY: &str = "Optional data the script reads with `(input)` — data, never code.";
1234
1235/// Which door a run came through.
1236#[derive(Clone, Copy, PartialEq, Eq)]
1237enum Door {
1238    /// `…:result`: a read, answered with the script's value.
1239    Result,
1240    /// `…:runs`: a write, recorded.
1241    Runs,
1242}
1243
1244/// What a run issues, and exactly the scopes it issues it under (before the runner's own
1245/// capability is attenuated to them).
1246struct Planned {
1247    request: Request,
1248    keep: BTreeSet<String>,
1249    /// What a refusal of this run should also say: why the run held only family markers
1250    /// (ledger #1220). Set only for a plan run that keeps one.
1251    note: Option<String>,
1252}
1253
1254/// A run's refusal, with the run's [`Planned::note`] said after it when there is one.
1255fn explain(note: Option<&str>, error: Error) -> Error {
1256    match (note, error) {
1257        (Some(note), Error::Denied(message)) => Error::Denied(format!("{message}. {note}")),
1258        (_, error) => error,
1259    }
1260}
1261
1262/// The run's sub-request, for whichever language the script is in.
1263async fn prepare_request(
1264    inv: &Invocation<'_>,
1265    shared: &SpaceConfig,
1266    prepared: &Prepared,
1267    keep: BTreeSet<String>,
1268    ceiling: &Ceiling,
1269    through: Door,
1270) -> Result<Planned> {
1271    let planned = match prepared.language {
1272        Language::Lisp => {
1273            let data = optional(
1274                inv,
1275                match through {
1276                    Door::Result => "data",
1277                    Door::Runs => "content",
1278                },
1279            )?;
1280            Planned {
1281                request: lisp_request(&prepared.program, data)?,
1282                keep,
1283                note: None,
1284            }
1285        }
1286        Language::Sparql => plan_sparql(inv, shared, prepared, keep, ceiling, through).await?,
1287        Language::Plan => plan_eval_request(inv, prepared, keep, ceiling, through)?,
1288    };
1289    Ok(with_budget(inv, prepared, ceiling, planned))
1290}
1291
1292/// Give a run the store time its script declared, bounded like the rest of its authority
1293/// (ledger #1384): never above what its publisher held at publish, the host's ceiling or the
1294/// runner's own budget grant ([`authority::effective_budget`]). A script declaring nothing
1295/// carries no budget grant, whatever its runner holds, so its runs get the store's base.
1296///
1297/// What the run can CARRY is bounded by attenuation, which keeps grants by exact name:
1298/// - a SPARQL run's one store request also says `budget=<bound>`, which the store's doors
1299///   read as a lowering, so the run carries a grant at or above the bound
1300///   ([`authority::budget_grant_lowered`]) and is answered at exactly the least of the four;
1301/// - a Lisp script's or a plan's store requests are its own, and nothing on their way can
1302///   lower them, so the run carries `urn:cap:store:budget:<effective>` when its runner can
1303///   be narrowed to that grant (root, or a runner holding it exactly), and otherwise the
1304///   largest budget grant its runner holds below it: less, never more
1305///   ([`authority::budget_grant`]).
1306fn with_budget(
1307    inv: &Invocation<'_>,
1308    prepared: &Prepared,
1309    ceiling: &Ceiling,
1310    mut planned: Planned,
1311) -> Planned {
1312    let Some(bound) = authority::budget_bound(prepared.budget, prepared.granted_budget, ceiling)
1313    else {
1314        return planned;
1315    };
1316    let runner = inv.capability;
1317    let grant = match prepared.language {
1318        Language::Sparql => {
1319            planned.request = planned
1320                .request
1321                .with_arg("budget", inline(&bound.to_string()));
1322            authority::budget_grant_lowered(runner, bound)
1323        }
1324        Language::Lisp | Language::Plan => {
1325            authority::effective_budget(prepared.budget, prepared.granted_budget, ceiling, runner)
1326                .and_then(|millis| authority::budget_grant(runner, millis))
1327        }
1328    };
1329    planned.keep.extend(grant);
1330    planned
1331}
1332
1333/// A plan run: refuse a write through the read door, take the plan's parameters, turn a
1334/// root publisher's derived families into exact grants, and address `urn:plan:eval`.
1335///
1336/// The plan's steps then run as `urn:plan:eval`'s sub-requests under exactly the narrowed
1337/// capability, and each meets its own target's floor there: a step the run may not take is
1338/// refused by the kernel at that step, typed, and arrives here unchanged.
1339fn plan_eval_request(
1340    inv: &Invocation<'_>,
1341    prepared: &Prepared,
1342    keep: BTreeSet<String>,
1343    ceiling: &Ceiling,
1344    through: Door,
1345) -> Result<Planned> {
1346    let name = &prepared.name;
1347    let analysis = prepared.plan.as_ref().ok_or_else(|| {
1348        Error::Endpoint(format!(
1349            "urn:script:{name}:compiled carries no plan analysis"
1350        ))
1351    })?;
1352    if through == Door::Result && analysis.mutates() {
1353        let writers: Vec<String> = analysis
1354            .steps
1355            .iter()
1356            .filter(|s| s.mutates())
1357            .map(|s| format!("<{}> ({} <{}>)", s.iri, s.verb, s.resolves))
1358            .collect();
1359        return Err(Error::InvalidArgument {
1360            name: "name".to_string(),
1361            detail: format!(
1362                "urn:script:{name} is a plan that writes ({}), so it is not a read: run it with \
1363                 a Sink to urn:script:{name}:runs",
1364                writers.join(", ")
1365            ),
1366        });
1367    }
1368    let declared: Vec<(&str, bool)> = analysis
1369        .parameters
1370        .iter()
1371        .map(|p| (p.name.as_str(), p.must_be_given()))
1372        .collect();
1373    let values = given_parameters(inv, name, &declared, through)?;
1374    let face = optional(inv, "as")?;
1375    let keep = plan::expand_families(keep, inv.capability, ceiling);
1376    // Only a ROOT run is left holding a marker nobody named. A scoped run keeps a family only
1377    // when the runner itself holds it (a namespace grant, ledger #1222): that is the runner's
1378    // own grant, and the note's "neither the runner nor the ceiling names a member" would be
1379    // false of it.
1380    let markers = if inv.capability.is_root() {
1381        plan::markers(&keep)
1382    } else {
1383        Vec::new()
1384    };
1385    let note = (!markers.is_empty()).then(|| {
1386        format!(
1387            "urn:script:{name} ran holding {} only as a family: a family held is not a grant, \
1388             so a step whose module checks an exact token under it is refused. Neither the \
1389             runner nor the host's ceiling for this script names a member (a step's contract \
1390             declares the family, never the member, and root holds no list to pick one from). \
1391             Run it under a capability that holds the exact grants, or have the host's \
1392             ceiling for urn:script:{name} name them",
1393            markers
1394                .iter()
1395                .map(|m| format!("`{m}`"))
1396                .collect::<Vec<_>>()
1397                .join(", ")
1398        )
1399    });
1400    Ok(Planned {
1401        request: plan::eval_request(&prepared.program, &values, face),
1402        keep,
1403        note,
1404    })
1405}
1406
1407/// A SPARQL run: bind the parameters into the parsed query, work out its dataset and the
1408/// exact store grants it needs, and address the host's store door for its form.
1409///
1410/// ★ **The dataset is never wider than the authority.** The graphs the text fixes need
1411/// their grants in the run's capability (the store refuses the whole read, naming the
1412/// grant, when one is missing). A graph only the run decides (`GRAPH ?g`, or a parameter
1413/// naming a graph) is admitted only under the derived family, and only when the
1414/// publisher held it (or was root), the host's ceiling allows it AND the runner holds it:
1415/// that set is "the caller's readable union", computed here per run and never more.
1416async fn plan_sparql(
1417    inv: &Invocation<'_>,
1418    shared: &SpaceConfig,
1419    prepared: &Prepared,
1420    mut keep: BTreeSet<String>,
1421    ceiling: &Ceiling,
1422    through: Door,
1423) -> Result<Planned> {
1424    let name = &prepared.name;
1425    let door = door(shared)?;
1426    let analysis = prepared.sparql.as_ref().ok_or_else(|| {
1427        Error::Endpoint(format!(
1428            "urn:script:{name}:compiled carries no SPARQL analysis"
1429        ))
1430    })?;
1431    if through == Door::Result && !analysis.form.is_read() {
1432        return Err(Error::InvalidArgument {
1433            name: "name".to_string(),
1434            detail: format!(
1435                "urn:script:{name} is a SPARQL UPDATE, which is a write: run it with a Sink to \
1436                 urn:script:{name}:runs"
1437            ),
1438        });
1439    }
1440    let values = parameter_values(inv, name, &analysis.parameters, through)?;
1441    let face = sparql_face(inv, analysis.form)?;
1442    let bound = sparql::bind(&prepared.program, door, &values)?;
1443
1444    let runner = inv.capability;
1445    // The root publisher's marker: expanded below into exact grants, never attenuated as
1446    // it stands (attenuation keeps grants by exact name).
1447    let root_marker = keep.remove(CAP_READ_GRAPH);
1448    let family = prepared.requires.contains(CAP_READ_GRAPH);
1449    let by_family = |scope: &str, keep: &BTreeSet<String>| {
1450        family
1451            && runner.allows(scope)
1452            && if root_marker {
1453                ceiling.allows(scope)
1454            } else {
1455                keep.contains(scope)
1456            }
1457    };
1458
1459    if bound.form == Form::Update {
1460        // An update's one graph is fixed by its text (a parameter cannot name it), so its
1461        // grants are exactly the derived ones, already in `keep`.
1462        let graph = bound.graphs.iter().next().cloned().ok_or_else(|| {
1463            Error::Endpoint(format!("urn:script:{name} names no graph to update"))
1464        })?;
1465        for scope in &prepared.requires {
1466            may_use(name, &graph, scope, &keep, runner)?;
1467        }
1468        let request = Request::new(Verb::Sink, iri(&door.query_iri(Form::Update))?)
1469            .with_arg("content", inline(&bound.text))
1470            .with_arg("graph", inline(&graph));
1471        return Ok(Planned {
1472            request,
1473            keep,
1474            note: None,
1475        });
1476    }
1477
1478    let mut extra = BTreeSet::new();
1479    for graph in &bound.graphs {
1480        let scope = sparql::cap_read_graph(graph);
1481        if !keep.contains(&scope) && by_family(&scope, &keep) {
1482            extra.insert(scope);
1483        }
1484    }
1485    let mut dataset = bound.graphs.clone();
1486    if bound.any_graph {
1487        if !family {
1488            return Err(Error::Endpoint(format!(
1489                "urn:script:{name} reads a variable graph but its record derives no \
1490                 `{CAP_READ_GRAPH}`; republish it"
1491            )));
1492        }
1493        let under = |scopes: &mut dyn Iterator<Item = &String>| -> BTreeSet<String> {
1494            scopes
1495                .filter_map(|s| s.strip_prefix(sparql::READ_GRAPH_PREFIX))
1496                .filter(|g| !g.starts_with('-') && !g.ends_with('*'))
1497                .map(str::to_string)
1498                .collect()
1499        };
1500        let candidates = if !root_marker {
1501            under(&mut keep.iter())
1502        } else if let Some(held) = runner.scopes() {
1503            under(&mut held.iter())
1504        } else {
1505            // Root ran a script a root published: the readable union is every graph the
1506            // store holds, listed by the store itself under the broad read the runner holds.
1507            let listing = inv
1508                .issue_attenuated(
1509                    Request::new(Verb::Source, iri(&door.graphs_iri())?),
1510                    [sparql::CAP_STORE_READ.to_string()],
1511                )
1512                .await?;
1513            String::from_utf8_lossy(&listing.bytes)
1514                .lines()
1515                .map(str::trim)
1516                .filter(|l| !l.is_empty())
1517                .map(str::to_string)
1518                .collect()
1519        };
1520        for graph in candidates {
1521            let scope = sparql::cap_read_graph(&graph);
1522            if keep.contains(&scope) && runner.allows(&scope) || by_family(&scope, &keep) {
1523                extra.insert(scope);
1524                dataset.insert(graph);
1525            }
1526        }
1527    }
1528    if dataset.is_empty() {
1529        return Err(Error::Denied(format!(
1530            "urn:script:{name} reads the caller's readable graphs, and this run may read none: \
1531             it needs `{}` grants the publisher held, the host allows and the runner holds",
1532            sparql::READ_GRAPH_PREFIX
1533        )));
1534    }
1535    keep.extend(extra);
1536    for graph in &dataset {
1537        may_use(name, graph, &sparql::cap_read_graph(graph), &keep, runner)?;
1538    }
1539    let graphs = dataset.into_iter().collect::<Vec<_>>().join(" ");
1540    let request = Request::new(Verb::Source, iri(&door.query_iri(bound.form))?)
1541        .with_arg("query", inline(&bound.text))
1542        .with_arg("graph", inline(&graphs))
1543        .with_arg("as", inline(face));
1544    Ok(Planned {
1545        request,
1546        keep,
1547        note: None,
1548    })
1549}
1550
1551/// Refused HERE, naming the exact grant and who withheld it, rather than by the store's
1552/// door: the kernel's floor there sees only the family a run holds, so its refusal would
1553/// name `urn:cap:store:read:graph:*` and not the graph.
1554fn may_use(
1555    name: &str,
1556    graph: &str,
1557    scope: &str,
1558    keep: &BTreeSet<String>,
1559    runner: &ikigai_core::Capability,
1560) -> Result<()> {
1561    if !runner.allows(scope) {
1562        return Err(Error::Denied(format!(
1563            "running urn:script:{name} reads or writes <{graph}>, which needs `{scope}`, and the \
1564             runner does not hold it"
1565        )));
1566    }
1567    if !keep.contains(scope) {
1568        return Err(Error::Denied(format!(
1569            "running urn:script:{name} reads or writes <{graph}>, which needs `{scope}`, and \
1570             this script may not have it: its publisher did not hold it, or the host's ceiling \
1571             for it does not allow it"
1572        )));
1573    }
1574    Ok(())
1575}
1576
1577/// The face a SPARQL run answers in: `as=`, one its form can serve, or the form's default.
1578/// Refused, never substituted.
1579fn sparql_face(inv: &Invocation<'_>, form: Form) -> Result<&'static str> {
1580    let faces = form.faces();
1581    match optional(inv, "as")? {
1582        None => Ok(faces[0]),
1583        Some(asked) => {
1584            let bare = asked.split(';').next().unwrap_or(asked).trim();
1585            faces
1586                .iter()
1587                .find(|f| **f == bare)
1588                .copied()
1589                .ok_or_else(|| Error::InvalidArgument {
1590                    name: "as".to_string(),
1591                    detail: format!(
1592                        "`{asked}` is not a face a SPARQL {} answers in; one of {}",
1593                        form.as_str().to_uppercase(),
1594                        faces.join(", ")
1595                    ),
1596                })
1597        }
1598    }
1599}
1600
1601/// The run's parameter values, each checked against its declared type.
1602///
1603/// What was given is [`given_parameters`]'s; a parameter not given takes its default, and
1604/// one with neither stays unbound.
1605fn parameter_values(
1606    inv: &Invocation<'_>,
1607    name: &str,
1608    parameters: &[Parameter],
1609    through: Door,
1610) -> Result<BTreeMap<String, Value>> {
1611    let declared: Vec<(&str, bool)> = parameters
1612        .iter()
1613        .map(|p| (p.name.as_str(), p.required && p.default.is_none()))
1614        .collect();
1615    let mut given = given_parameters(inv, name, &declared, through)?;
1616    let mut values = BTreeMap::new();
1617    for parameter in parameters {
1618        if let Some(lexical) = given
1619            .remove(&parameter.name)
1620            .or_else(|| parameter.default.clone())
1621        {
1622            values.insert(parameter.name.clone(), parameter.value(&lexical)?);
1623        }
1624    }
1625    Ok(values)
1626}
1627
1628/// The argument names a TRANSPORT owns, which a run never reads as a script's parameters
1629/// (ledger #1173).
1630///
1631/// `ikigai-web` stamps a write with its provenance, `received`, `client` and `principal`,
1632/// read off the connection, and with the body's `content-type`; `ikigai-quic` stamps
1633/// `principal` on every verb. Each is the door's statement about the request, never the
1634/// caller's, and none is an input to a script: who a request is from reaches a run in its
1635/// CAPABILITY (`Capability::with_principal`, read with [`authority::principal_of`]), which
1636/// is what every run and publish records.
1637///
1638/// So the doors of this crate ignore these names wherever a script's parameters are read
1639/// (`…:result` and `…:runs`, for a query and a plan), instead of refusing them as arguments
1640/// the script does not declare — which made every run for effects through the HTTP door a
1641/// `400` — and no script may declare a parameter by one of them (refused at publish; see
1642/// [`sparql::RESERVED_PARAMETERS`] and [`plan::RESERVED_PARAMETERS`]). A value a CALLER
1643/// supplies under one of them (where a door passes it through) is ignored the same way, so
1644/// it can neither reach a script nor stand in for the principal.
1645///
1646/// ```
1647/// use ikigai_script::TRANSPORT_ARGUMENTS;
1648/// for owned in TRANSPORT_ARGUMENTS {
1649///     assert!(ikigai_script::sparql::RESERVED_PARAMETERS.contains(&owned), "{owned}");
1650///     assert!(ikigai_script::plan::RESERVED_PARAMETERS.contains(&owned), "{owned}");
1651/// }
1652/// ```
1653pub const TRANSPORT_ARGUMENTS: [&str; 4] = ["received", "client", "principal", "content-type"];
1654
1655/// The parameters a run was GIVEN, as text, for a script that declares `declared` (each
1656/// parameter's name, and whether a run must give it).
1657///
1658/// Named arguments, or (through `…:runs`) a JSON object piped as `content`. A missing
1659/// parameter a run must give is `MissingArgument`; a parameter given both ways and an
1660/// argument the script does not declare are `InvalidArgument`: a binding the script does not
1661/// mention is refused, never ignored, so a filter you thought was applied can never silently
1662/// not be. Refused here, before a run is recorded: nothing ran.
1663///
1664/// The one exception is [`TRANSPORT_ARGUMENTS`]: a door's own stamps, ignored, never bound.
1665/// `declared` never holds one: the parameters are read again from the source each time the
1666/// compiled form is prepared, and that read refuses a reserved name, so a script stored
1667/// before they were reserved that declares one fails there, naming it, and never runs with
1668/// the parameter silently unbound.
1669fn given_parameters(
1670    inv: &Invocation<'_>,
1671    name: &str,
1672    declared: &[(&str, bool)],
1673    through: Door,
1674) -> Result<BTreeMap<String, String>> {
1675    let listed = || {
1676        if declared.is_empty() {
1677            "it declares none".to_string()
1678        } else {
1679            format!(
1680                "its parameters are {}",
1681                declared
1682                    .iter()
1683                    .map(|(p, _)| *p)
1684                    .collect::<Vec<_>>()
1685                    .join(", ")
1686            )
1687        }
1688    };
1689    for arg in inv.request.args.keys() {
1690        let known = arg == "as"
1691            || TRANSPORT_ARGUMENTS.contains(&arg.as_str())
1692            || (through == Door::Runs && arg == "content")
1693            || declared.iter().any(|(p, _)| p == arg);
1694        if !known {
1695            return Err(Error::InvalidArgument {
1696                name: arg.clone(),
1697                detail: format!("is not a parameter of urn:script:{name}; {}", listed()),
1698            });
1699        }
1700    }
1701    let mut piped: BTreeMap<String, String> = BTreeMap::new();
1702    if through == Door::Runs {
1703        // An empty body is no parameters: a bare `sink …:runs` sends one.
1704        if let Some(content) = optional(inv, "content")?.filter(|c| !c.trim().is_empty()) {
1705            let object: serde_json::Map<String, serde_json::Value> = serde_json::from_str(content)
1706                .map_err(|e| Error::InvalidArgument {
1707                    name: "content".to_string(),
1708                    detail: format!(
1709                        "a script's piped content is its parameters as one JSON object ({e})"
1710                    ),
1711                })?;
1712            for (key, value) in object {
1713                let lexical = match value {
1714                    serde_json::Value::String(s) => s,
1715                    serde_json::Value::Number(n) => n.to_string(),
1716                    serde_json::Value::Bool(b) => b.to_string(),
1717                    other => {
1718                        return Err(Error::InvalidArgument {
1719                            name: "content".to_string(),
1720                            detail: format!(
1721                                "`{key}` is {other}; a parameter's value is a string, a number \
1722                                 or a boolean"
1723                            ),
1724                        })
1725                    }
1726                };
1727                piped.insert(key, lexical);
1728            }
1729        }
1730    }
1731    let mut given = BTreeMap::new();
1732    for (parameter, must) in declared {
1733        let named = optional(inv, parameter)?;
1734        let lexical = match (named, piped.remove(*parameter)) {
1735            (Some(_), Some(_)) => {
1736                return Err(Error::InvalidArgument {
1737                    name: parameter.to_string(),
1738                    detail: "is given both by name and in the piped content".to_string(),
1739                })
1740            }
1741            (Some(value), None) => Some(value.to_string()),
1742            (None, Some(value)) => Some(value),
1743            (None, None) => None,
1744        };
1745        match lexical {
1746            Some(lexical) => {
1747                given.insert(parameter.to_string(), lexical);
1748            }
1749            None if *must => return Err(Error::MissingArgument(parameter.to_string())),
1750            None => {}
1751        }
1752    }
1753    if let Some(stray) = piped.keys().next() {
1754        return Err(Error::InvalidArgument {
1755            name: "content".to_string(),
1756            detail: format!(
1757                "`{stray}` is not a parameter of urn:script:{name}; {}",
1758                listed()
1759            ),
1760        });
1761    }
1762    Ok(given)
1763}
1764
1765struct ResultEndpoint {
1766    shared: Arc<SpaceConfig>,
1767}
1768
1769#[async_trait]
1770impl Endpoint for ResultEndpoint {
1771    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
1772        if inv.request.verb != Verb::Source {
1773            return Err(unsupported("script-result", inv.request.verb));
1774        }
1775        let name = name::from_bindings(inv)?;
1776        let (prepared, keep, ceiling) = prepare_run(inv, &self.shared, &name).await?;
1777        let planned =
1778            prepare_request(inv, &self.shared, &prepared, keep, &ceiling, Door::Result).await?;
1779        // ★ The ONLY authority a run gets: the runner's own capability, narrowed. There is
1780        // no form that widens, so a script cannot reach past its runner whatever it says.
1781        let Planned {
1782            request,
1783            keep,
1784            note,
1785        } = planned;
1786        let answer = inv
1787            .issue_attenuated(request, keep)
1788            .await
1789            .map_err(|e| explain(note.as_deref(), e))?;
1790        // Cacheable as far as this endpoint is concerned; the kernel folds in the
1791        // evaluator's expiry (a Lisp program is uncacheable unless it opts in with
1792        // `(cacheable …)`; a query is as cacheable as the store's answer, which hangs from
1793        // the store's write threads) and the compiled form's threads. The authority thread
1794        // is the host's to cut when it changes this script's ceiling.
1795        Ok(Representation::new(answer.repr_type, answer.bytes)
1796            .cacheable()
1797            .depends_on(name::authority_thread(&name)))
1798    }
1799
1800    fn name(&self) -> &str {
1801        "script-result"
1802    }
1803
1804    fn describe(&self) -> Description {
1805        Description::new("script-result")
1806            .title("A script's result")
1807            .summary(
1808                "Run the script as a READ and answer its value. It runs under the runner's \
1809                 capability narrowed to what the script declares, its publisher held and \
1810                 the host allows it — never more. Cacheable exactly as far as the script's \
1811                 own sub-requests are (a Lisp program opts in with `(cacheable …)`), and \
1812                 recomputed when the script is republished. No run record: a cached answer \
1813                 runs nothing; record a run with `…:runs`. A published SPARQL script or \
1814                 plan is its own entry, with its parameters as arguments; a published Lisp \
1815                 script's door also requires `urn:cap:lisp`. A run of a script that is not \
1816                 published is refused for its state (to a caller who may see it) or as \
1817                 absent.",
1818            )
1819            .verb(Verb::Meta)
1820            .action(
1821                ActionSpec::new(Verb::Source)
1822                    .summary("The script's answer.")
1823                    .input(name_arg())
1824                    .input(data_arg("data", DATA_SUMMARY))
1825                    .output(PLAIN)
1826                    .requires(CAP_RUN),
1827            )
1828    }
1829}
1830
1831struct RunsEndpoint {
1832    shared: Arc<SpaceConfig>,
1833}
1834
1835#[async_trait]
1836impl Endpoint for RunsEndpoint {
1837    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
1838        if inv.request.verb != Verb::Sink {
1839            return Err(unsupported("script-runs", inv.request.verb));
1840        }
1841        let name = name::from_bindings(inv)?;
1842        let shared = &self.shared;
1843        let (prepared, keep, ceiling) = prepare_run(inv, shared, &name).await?;
1844        // A caller's mistake (a missing or ill-typed parameter) is refused here, before a
1845        // run is recorded: nothing ran.
1846        let planned = prepare_request(inv, shared, &prepared, keep, &ceiling, Door::Runs).await?;
1847        let runs_as = inv.capability.attenuate(planned.keep.iter().cloned());
1848        let mut run = Run {
1849            schema: SCHEMA,
1850            name: name.clone(),
1851            id: 0,
1852            version: prepared.version.clone(),
1853            principal: principal_of(inv.capability),
1854            capability: runs_as.scopes().cloned().unwrap_or_default(),
1855            started: inv.now().map(|t| t.as_millis()),
1856            ended: None,
1857            outcome: Outcome::Running,
1858            result: None,
1859            truncated: false,
1860            trace_span: inv.trace_span(),
1861        };
1862        run.id = shared.backend.start_run(&name, &run)?;
1863        let Planned {
1864            request,
1865            keep,
1866            note,
1867        } = planned;
1868        let answer = inv
1869            .issue_attenuated(request, keep)
1870            .await
1871            .map_err(|e| explain(note.as_deref(), e));
1872        run.ended = inv.now().map(|t| t.as_millis());
1873        match answer {
1874            Ok(repr) => {
1875                let (text, truncated) = model::truncate(
1876                    String::from_utf8_lossy(&repr.bytes).into_owned(),
1877                    MAX_RECORDED_RESULT,
1878                );
1879                run.outcome = Outcome::Ok;
1880                run.result = Some(text);
1881                run.truncated = truncated;
1882                shared.backend.finish_run(&name, &run)?;
1883                Ok(plain(format!("{}\n", run.iri())))
1884            }
1885            Err(error) => {
1886                run.outcome = Outcome::Failed {
1887                    kind: kind(&error).to_string(),
1888                    message: error.to_string(),
1889                };
1890                shared.backend.finish_run(&name, &run)?;
1891                Err(annotate(error, &run.iri()))
1892            }
1893        }
1894    }
1895
1896    fn name(&self) -> &str {
1897        "script-runs"
1898    }
1899
1900    fn describe(&self) -> Description {
1901        Description::new("script-runs")
1902            .title("A script's runs")
1903            .summary(
1904                "Run the script for its EFFECTS and record the run: who (the principal \
1905                 the capability names), which version, under exactly what capability, when, and the \
1906                 outcome. Answers the run's IRI; a run that fails is recorded too, and its \
1907                 error names the record. A published SPARQL script or plan is its own \
1908                 entry, with its parameters as arguments; a published Lisp script's door also \
1909                 requires `urn:cap:lisp`. A script that is not published is refused before \
1910                 any run is recorded.",
1911            )
1912            .verb(Verb::Meta)
1913            .action(
1914                ActionSpec::new(Verb::Sink)
1915                    .summary("Run the script and record the run.")
1916                    .input(name_arg())
1917                    .input(data_arg("content", DATA_SUMMARY))
1918                    .output(PLAIN)
1919                    .requires(CAP_RUN),
1920            )
1921    }
1922}
1923
1924// ---------------------------------------------------------------------------------------
1925// A published SPARQL script's own contract, and the space that offers it
1926// ---------------------------------------------------------------------------------------
1927
1928/// A SPARQL script's or a plan's own endpoints, `…:result` and `…:runs`, each the
1929/// template's endpoint behind the script's OWN description.
1930#[derive(Clone)]
1931struct PerScript {
1932    result: Arc<dyn Endpoint>,
1933    runs: Arc<dyn Endpoint>,
1934    /// Whether its `…:result` is a read it offers (a query, not an update).
1935    reads: bool,
1936    /// Whether it is published, and so listed in the catalog. A draft of content that was
1937    /// published before, or a retired script, still answers under its own contract (its run
1938    /// is refused for its state, not for a language capability it never needed), but is not
1939    /// offered. A draft never published has no contract of its own (see `build`).
1940    listed: bool,
1941}
1942
1943/// Which contract a script's `…:result` and `…:runs` answer under.
1944#[derive(Clone)]
1945enum Contract {
1946    /// A published SPARQL script's or a plan's own (and a draft or a retired script whose
1947    /// head version was published once): its parameters, and the scopes its text derives.
1948    Own(PerScript),
1949    /// [`LispDoors`]': a published Lisp script, whose run needs `urn:cap:lisp`.
1950    Lisp,
1951    /// The template's, which asks no language grant: a script that is not published (a
1952    /// draft, a retired Lisp script) and a name nobody wrote.
1953    Template,
1954}
1955
1956/// The doors a PUBLISHED Lisp script runs through: the template's, declaring the language
1957/// grant its evaluator needs (`urn:cap:lisp`) beside the template's own (ledger #1261).
1958///
1959/// ★ **Why the template does not declare it.** The kernel's floor checks a contract before
1960/// the endpoint is entered, and the template is also the contract of every draft (whatever
1961/// its language) and of every name nobody wrote. Declared there, a run of a SPARQL draft was
1962/// refused because its author's capability did not grant Lisp, and only root or a Lisp
1963/// holder ever heard the real reason, that it is a draft. Without it, the run says that to
1964/// a caller who may see the draft, and that nothing is there to anyone else.
1965///
1966/// ⚠ **The template stays ONE contract for all of those, on purpose.** Meta is answered from
1967/// the description to anyone who can reach the door, so a draft's contract must say nothing
1968/// a name nobody wrote does not (ledger #952): not its language, not its parameters, not that
1969/// it exists. What it does tell apart is a PUBLISHED Lisp script, which wears this one; that a
1970/// script is published is already public for SPARQL and plans, each its own catalog entry.
1971/// And the kernel describes the template's catalog entry by resolving it at a probe name
1972/// (which nobody wrote), so this one can never be the template's: the template's entry
1973/// would describe nothing, and the Lisp door would leave the catalog and the manifold.
1974///
1975/// Nothing runs under the template but a refusal: a published script never wears it, and a
1976/// script published between resolution and the run is run with the caller's own capability
1977/// attenuated to what it requires, so a Lisp script's evaluator still refuses a runner
1978/// without `urn:cap:lisp`.
1979#[derive(Clone)]
1980struct LispDoors {
1981    result: Arc<dyn Endpoint>,
1982    runs: Arc<dyn Endpoint>,
1983}
1984
1985impl LispDoors {
1986    /// Built once per space: the kernel memoizes floors by the endpoint's allocation.
1987    fn new(shared: &Arc<SpaceConfig>) -> LispDoors {
1988        let door = |inner: Arc<dyn Endpoint>| -> Arc<dyn Endpoint> {
1989            let description = needs_lisp(inner.describe());
1990            Arc::new(Described {
1991                id: description.id.clone(),
1992                description,
1993                inner,
1994            })
1995        };
1996        LispDoors {
1997            result: door(Arc::new(ResultEndpoint {
1998                shared: Arc::clone(shared),
1999            })),
2000            runs: door(Arc::new(RunsEndpoint {
2001                shared: Arc::clone(shared),
2002            })),
2003        }
2004    }
2005}
2006
2007/// `template`, with the language grant a Lisp script's run needs on every action. The id is
2008/// the template's: it is the same door, with one more scope declared.
2009fn needs_lisp(mut template: Description) -> Description {
2010    for action in &mut template.actions {
2011        action.requires.push(CAP_LISP.to_string());
2012    }
2013    template
2014}
2015
2016/// The template's endpoint, behind one script's contract.
2017///
2018/// ★ **Why a script is its own entry.** A description is the contract the engine routes
2019/// arguments by, selection matches on, MCP projects and the Emacs aliases are generated
2020/// from, and the kernel answers it per ENDPOINT, never per IRI. A query's or a plan's
2021/// parameters are real arguments only if they are in a description, so each gets one, and
2022/// so does a plan with no parameters: its contract declares what its steps derive, and the
2023/// template declares none of that.
2024/// It is built once per head (the kernel memoizes floors by this allocation) and dropped
2025/// by the next publish or retire.
2026struct Described {
2027    inner: Arc<dyn Endpoint>,
2028    id: String,
2029    description: Description,
2030}
2031
2032#[async_trait]
2033impl Endpoint for Described {
2034    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
2035        self.inner.invoke(inv).await
2036    }
2037
2038    fn name(&self) -> &str {
2039        &self.id
2040    }
2041
2042    fn describe(&self) -> Description {
2043        self.description.clone()
2044    }
2045}
2046
2047/// The script resources as a space: [`space`]'s bindings, plus each published SPARQL
2048/// script and plan as its own `…:result` and `…:runs` entry, carrying its parameters.
2049pub struct ScriptSpace {
2050    inner: EndpointSpace,
2051    lisp: LispDoors,
2052    shared: Arc<SpaceConfig>,
2053}
2054
2055impl ScriptSpace {
2056    /// The contract the script's run doors answer under: its own when it is a SPARQL
2057    /// script this host can run or a plan, [`LispDoors`]' when it is a published Lisp
2058    /// script, and the template's otherwise.
2059    ///
2060    /// Finding that out costs one head and version read per name, remembered until the next
2061    /// write.
2062    fn contract(&self, name: &str) -> Contract {
2063        let shared = &self.shared;
2064        if let Some(known) = shared
2065            .described
2066            .lock()
2067            .expect("the described-script memo")
2068            .get(name)
2069        {
2070            return known.clone();
2071        }
2072        let Ok(Some(head)) = shared.backend.head(name) else {
2073            // ⚠ Remember only scripts that exist: a name nobody wrote is a miss every time,
2074            // never an entry, so resolving arbitrary names cannot grow this map.
2075            return Contract::Template;
2076        };
2077        let built = self.build(name, &head);
2078        shared
2079            .described
2080            .lock()
2081            .expect("the described-script memo")
2082            .insert(name.to_string(), built.clone());
2083        built
2084    }
2085
2086    fn build(&self, name: &str, head: &Head) -> Contract {
2087        // ★ A draft never published wears the template's contract. Meta is answered from
2088        // the description to anyone who can reach the door (the kernel's, before any
2089        // endpoint runs, so it cannot ask who is calling), and a script's own contract is
2090        // made of its text: its parameters, its leading comment, the graphs it names. Its own
2091        // entry would publish all three, and that the draft exists. Its run is still refused
2092        // (for its state, to its author; as absent, to everyone else).
2093        if !ever_published(head, &head.version) {
2094            return Contract::Template;
2095        }
2096        // A head that points at a version its backend cannot read keeps the stricter floor;
2097        // the run says what is wrong.
2098        let Ok(Some(version)) = self.shared.backend.version(name, &head.version) else {
2099            return Contract::Lisp;
2100        };
2101        let own = match version.language {
2102            Language::Lisp if head.state == State::Published => return Contract::Lisp,
2103            Language::Lisp => None,
2104            Language::Sparql => self.build_sparql(name, head, &version),
2105            Language::Plan => self.build_plan(name, head, &version),
2106        };
2107        // A query this host cannot analyze (or run: it mounts no SPARQL door) needs no
2108        // language grant either; its run says why it cannot run.
2109        own.map_or(Contract::Template, Contract::Own)
2110    }
2111
2112    /// A per-script endpoint: the template's, behind `description`.
2113    fn described(
2114        &self,
2115        inner: Arc<dyn Endpoint>,
2116        id: String,
2117        mut description: Description,
2118        head: &Head,
2119    ) -> Arc<dyn Endpoint> {
2120        description.summary.push_str(&run_gate_note(head));
2121        Arc::new(Described {
2122            inner,
2123            id,
2124            description,
2125        }) as Arc<dyn Endpoint>
2126    }
2127
2128    fn result_endpoint(&self) -> Arc<dyn Endpoint> {
2129        Arc::new(ResultEndpoint {
2130            shared: Arc::clone(&self.shared),
2131        })
2132    }
2133
2134    fn runs_endpoint(&self) -> Arc<dyn Endpoint> {
2135        Arc::new(RunsEndpoint {
2136            shared: Arc::clone(&self.shared),
2137        })
2138    }
2139
2140    fn build_sparql(&self, name: &str, head: &Head, version: &Version) -> Option<PerScript> {
2141        let door = self.shared.sparql.as_ref()?;
2142        let analysis = sparql::analyze(&version.source, door).ok()?;
2143        let about = about(&version.source);
2144        let inputs: Vec<ArgSpec> = analysis
2145            .parameters
2146            .iter()
2147            .map(|p| {
2148                let mut arg = ArgSpec::new(p.name.clone())
2149                    .summary(
2150                        p.summary
2151                            .clone()
2152                            .unwrap_or_else(|| format!("The value of ?{} in the query.", p.name)),
2153                    )
2154                    .class(p.kind.iri().to_string());
2155                if let Some(default) = &p.default {
2156                    arg = arg.default_value(default.clone()).optional();
2157                } else if !p.required {
2158                    arg = arg.optional();
2159                }
2160                arg
2161            })
2162            .collect();
2163        let params = |spec: ActionSpec| contracted(spec, &inputs, name, head, version);
2164        let form = analysis.form;
2165        let faces = form.faces();
2166        let as_arg = || {
2167            ArgSpec::new("as")
2168                .summary(format!(
2169                    "The face: one of {}. Any other is refused, never substituted.",
2170                    faces.join(", ")
2171                ))
2172                .class(XSD_STRING)
2173                .one_of(faces.iter().copied())
2174                .default_value(faces[0])
2175                .optional()
2176        };
2177        let id = format!("script-{name}-result");
2178        let description = if form.is_read() {
2179            let mut spec = params(ActionSpec::new(Verb::Source))
2180                .summary(format!("Run the query {name} and answer its result."))
2181                .input(as_arg());
2182            for face in faces {
2183                spec = spec.output(*face);
2184            }
2185            Description::new(id.clone())
2186                .title(format!("{name}: a SPARQL {}", form.as_str().to_uppercase()))
2187                .summary(format!(
2188                    "{about}Runs as a READ under the runner's capability narrowed to the \
2189                     graphs the query names; cached as the store's answer is, and \
2190                     recomputed after a write to the store or a republish."
2191                ))
2192                .verb(Verb::Meta)
2193                .action(spec)
2194        } else {
2195            // An update is never a read: no action is offered here, and a Source that
2196            // arrives anyway is refused by the run, naming `…:runs`.
2197            Description::new(id.clone())
2198                .title(format!("{name}: a SPARQL UPDATE (run it at …:runs)"))
2199                .summary(format!(
2200                    "{about}A write: run it with a Sink to urn:script:{name}:runs."
2201                ))
2202                .verb(Verb::Meta)
2203        };
2204        let result = self.described(self.result_endpoint(), id, description, head);
2205        let id = format!("script-{name}-runs");
2206        let mut spec = params(ActionSpec::new(Verb::Sink))
2207            .summary(format!(
2208                "Run {name} for its effects and record the run; answers the run's IRI."
2209            ))
2210            .input(piped_parameters_arg())
2211            .output(PLAIN);
2212        if form.is_read() {
2213            spec = spec.input(as_arg());
2214        }
2215        let runs = self.described(
2216            self.runs_endpoint(),
2217            id.clone(),
2218            Description::new(id)
2219                .title(format!("{name}: runs"))
2220                .summary(format!(
2221                    "{about}Each run is recorded: who, which version, under exactly what \
2222                     capability, when, and the outcome."
2223                ))
2224                .verb(Verb::Meta)
2225                .action(spec),
2226            head,
2227        );
2228        Some(PerScript {
2229            result,
2230            runs,
2231            reads: form.is_read(),
2232            listed: head.state == State::Published,
2233        })
2234    }
2235
2236    /// A plan's own contract: its declared parameters as arguments, and the capability
2237    /// derived from its steps as what the floor checks.
2238    fn build_plan(&self, name: &str, head: &Head, version: &Version) -> Option<PerScript> {
2239        let analysis = plan::analyze(&version.source).ok()?;
2240        let about = about(&version.source);
2241        let inputs: Vec<ArgSpec> = analysis
2242            .parameters
2243            .iter()
2244            .map(|p| {
2245                let mut arg = ArgSpec::new(p.name.clone())
2246                    .summary(
2247                        p.summary
2248                            .clone()
2249                            .unwrap_or_else(|| format!("The plan's parameter `{}`.", p.name)),
2250                    )
2251                    .class(p.class.clone().unwrap_or_else(|| XSD_STRING.to_string()));
2252                if let Some(default) = &p.default {
2253                    arg = arg.default_value(default.clone()).optional();
2254                } else if !p.required {
2255                    arg = arg.optional();
2256                }
2257                arg
2258            })
2259            .collect();
2260        let params = |spec: ActionSpec| contracted(spec, &inputs, name, head, version);
2261        // A plan's answer is its result step's own representation, so no list of faces can
2262        // be declared in advance: `as` is passed to the evaluator, which transrepts or
2263        // refuses.
2264        let as_arg = || {
2265            ArgSpec::new("as")
2266                .summary(
2267                    "The face of the plan's result: when its result step served another \
2268                     media type it is transrepted, and refused when nothing converts it.",
2269                )
2270                .class(XSD_STRING)
2271                .optional()
2272        };
2273        let reads = !analysis.mutates();
2274        let id = format!("script-{name}-result");
2275        let description = if reads {
2276            Description::new(id.clone())
2277                .title(format!("{name}: a plan"))
2278                .summary(format!(
2279                    "{about}Runs as a READ (every step reads) under the runner's capability \
2280                     narrowed to what the plan's steps require; as cacheable as its least \
2281                     cacheable step, and recomputed after a republish."
2282                ))
2283                .verb(Verb::Meta)
2284                .action(
2285                    params(ActionSpec::new(Verb::Source))
2286                        .summary(format!("Run the plan {name} and answer its result."))
2287                        .input(as_arg())
2288                        .output(PLAIN),
2289                )
2290        } else {
2291            // A plan with a Sink or Delete step is never a read: no action is offered here,
2292            // and a Source that arrives anyway is refused by the run, naming `…:runs`.
2293            Description::new(id.clone())
2294                .title(format!("{name}: a plan that writes (run it at …:runs)"))
2295                .summary(format!(
2296                    "{about}A write: run it with a Sink to urn:script:{name}:runs."
2297                ))
2298                .verb(Verb::Meta)
2299        };
2300        let result = self.described(self.result_endpoint(), id, description, head);
2301        let id = format!("script-{name}-runs");
2302        let runs = self.described(
2303            self.runs_endpoint(),
2304            id.clone(),
2305            Description::new(id)
2306                .title(format!("{name}: runs"))
2307                .summary(format!(
2308                    "{about}Each run is recorded: who, which version, under exactly what \
2309                     capability, when, and the outcome."
2310                ))
2311                .verb(Verb::Meta)
2312                .action(
2313                    params(ActionSpec::new(Verb::Sink))
2314                        .summary(format!(
2315                            "Run the plan {name} and record the run; answers the run's IRI."
2316                        ))
2317                        .input(piped_parameters_arg())
2318                        .input(as_arg())
2319                        .output(PLAIN),
2320                ),
2321            head,
2322        );
2323        Some(PerScript {
2324            result,
2325            runs,
2326            reads,
2327            listed: head.state == State::Published,
2328        })
2329    }
2330}
2331
2332/// What a script's own contract says about its run gate when the kernel's floor cannot say
2333/// it (ledger #1261): for a script in a namespace, that the floor is the namespace's family
2334/// and why. A public script's floor is every run grant's family, and a name with no `-`
2335/// declares its exact grant, which the floor's refusal names as it stands.
2336///
2337/// ★ The floor's refusal is the KERNEL's ("capability does not grant `…:run:team-*`"), and
2338/// the endpoint is never entered to add to it, so this is said where a refused caller can
2339/// read it: the contract Meta answers.
2340fn run_gate_note(head: &Head) -> String {
2341    let floor = authority::run_floor(&head.name);
2342    if head.public || floor == cap_run(&head.name) {
2343        return String::new();
2344    }
2345    format!(
2346        " Running it needs `{}` or a namespace grant covering it. A name with a `-` is in a \
2347         namespace, and a contract cannot depend on who asks, so the kernel's floor checks the \
2348         family of its top-level namespace, `{floor}`: any grant in it passes there, and the \
2349         exact rule is checked when the run starts.",
2350        cap_run(&head.name)
2351    )
2352}
2353
2354/// `spec` with a script's own `inputs`, the run gate and every scope its version requires.
2355///
2356/// The run gate, as the kernel can check it before the endpoint is entered: the script's own
2357/// grant, or the family of its top-level namespace when it is in one, so a namespace grant
2358/// is not refused at the floor ([`authority::run_floor`]); for a public one, the family of
2359/// every run grant (its own grant OR the public one, which `requires` cannot say any other
2360/// way: it is all-of). The exact rule is checked inside, by [`prepare_run`].
2361fn contracted(
2362    mut spec: ActionSpec,
2363    inputs: &[ArgSpec],
2364    name: &str,
2365    head: &Head,
2366    version: &Version,
2367) -> ActionSpec {
2368    for input in inputs {
2369        spec = spec.input(input.clone());
2370    }
2371    spec = spec.requires(if head.public {
2372        CAP_RUN.to_string()
2373    } else {
2374        authority::run_floor(name)
2375    });
2376    for scope in &version.requires {
2377        spec = spec.requires(scope.clone());
2378    }
2379    spec
2380}
2381
2382/// `…:runs`' piped body, for a script with parameters.
2383fn piped_parameters_arg() -> ArgSpec {
2384    ArgSpec::new("content")
2385        .summary(
2386            "The parameters as one JSON object, for a pipe (a parameter given both here and \
2387             by name is refused).",
2388        )
2389        .class(XSD_STRING)
2390        .optional()
2391}
2392
2393/// A script's own words for the catalog: its leading comment lines that declare nothing.
2394fn about(source: &str) -> String {
2395    let mut lines = Vec::new();
2396    for line in source.lines() {
2397        let trimmed = line.trim();
2398        let Some(body) = trimmed.strip_prefix('#') else {
2399            if trimmed.is_empty() {
2400                continue;
2401            }
2402            break;
2403        };
2404        let body = body.trim();
2405        if !body.is_empty() && !body.starts_with("@param") {
2406            lines.push(body);
2407        }
2408    }
2409    if lines.is_empty() {
2410        String::new()
2411    } else {
2412        format!("{} ", lines.join(" "))
2413    }
2414}
2415
2416impl Space for ScriptSpace {
2417    fn resolve(&self, request: &Request, scope: &Scope) -> Resolution {
2418        let resolution = self.inner.resolve(request, scope);
2419        let (name, part) = match &resolution {
2420            Resolution::Hit(hit) => {
2421                let part = match hit.endpoint.name() {
2422                    "script-result" => Door::Result,
2423                    "script-runs" => Door::Runs,
2424                    _ => return resolution,
2425                };
2426                match hit.bindings.get("name") {
2427                    Some(name) if name::validate(name).is_ok() => (name.to_string(), part),
2428                    _ => return resolution,
2429                }
2430            }
2431            Resolution::Miss => return resolution,
2432        };
2433        let (result, runs) = match self.contract(&name) {
2434            Contract::Own(per) => (per.result, per.runs),
2435            Contract::Template => return resolution,
2436            Contract::Lisp => (Arc::clone(&self.lisp.result), Arc::clone(&self.lisp.runs)),
2437        };
2438        resolution.map_endpoint(|_| match part {
2439            Door::Result => result,
2440            Door::Runs => runs,
2441        })
2442    }
2443
2444    fn entries(&self) -> Option<Vec<SpaceEntry>> {
2445        let mut entries = self.inner.entries().unwrap_or_default();
2446        for name in self.shared.backend.names().unwrap_or_default() {
2447            let Contract::Own(per) = self.contract(&name) else {
2448                continue;
2449            };
2450            if !per.listed {
2451                continue;
2452            }
2453            if per.reads {
2454                entries.push(SpaceEntry::new(
2455                    name::part_iri(&name, "result"),
2456                    per.result.name(),
2457                ));
2458            }
2459            entries.push(SpaceEntry::new(
2460                name::part_iri(&name, "runs"),
2461                per.runs.name(),
2462            ));
2463        }
2464        Some(entries)
2465    }
2466
2467    fn id(&self) -> Option<Iri> {
2468        self.inner.id()
2469    }
2470
2471    fn topology(&self) -> Topology {
2472        self.inner.topology()
2473    }
2474}
2475
2476/// An error's kind, as a run record spells it.
2477fn kind(error: &Error) -> &'static str {
2478    match error {
2479        Error::Unresolved(_) => "unresolved",
2480        Error::MissingArgument(_) => "missing-argument",
2481        Error::InvalidArgument { .. } => "invalid-argument",
2482        Error::Endpoint(_) => "endpoint",
2483        Error::Denied(_) => "denied",
2484        Error::NotFound(_) => "not-found",
2485        Error::Conflict(_) => "conflict",
2486        Error::Timeout(_) => "timeout",
2487        Error::Unavailable(_) => "unavailable",
2488        _ => "other",
2489    }
2490}
2491
2492/// The same error, of the same type, naming the run record it was written to.
2493fn annotate(error: Error, run: &str) -> Error {
2494    let note = |message: String| format!("{message} (run recorded at {run})");
2495    match error {
2496        Error::Endpoint(m) => Error::Endpoint(note(m)),
2497        Error::Denied(m) => Error::Denied(note(m)),
2498        Error::NotFound(m) => Error::NotFound(note(m)),
2499        Error::Conflict(m) => Error::Conflict(note(m)),
2500        Error::Timeout(m) => Error::Timeout(note(m)),
2501        Error::Unavailable(m) => Error::Unavailable(note(m)),
2502        Error::InvalidArgument { name, detail } => Error::InvalidArgument {
2503            name,
2504            detail: note(detail),
2505        },
2506        other => other,
2507    }
2508}
2509
2510// ---------------------------------------------------------------------------------------
2511// urn:script:{name}:run:{id}
2512// ---------------------------------------------------------------------------------------
2513
2514struct RunEndpoint {
2515    shared: Arc<SpaceConfig>,
2516}
2517
2518#[async_trait]
2519impl Endpoint for RunEndpoint {
2520    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
2521        let name = name::from_bindings(inv)?;
2522        // ★ Hung from the script's RUNS thread, which every run cuts (the kernel cuts the
2523        // thread named after a Sink's target, `urn:script:{name}:runs`), and named FIRST so a
2524        // NotFound carries it as well as a success (ledger #1079's shape): whether run `{id}`
2525        // exists is the state a run writes, never anything written through this record's own
2526        // name, so without it a cached fallback over "no run {id}" outlived the run that
2527        // recorded it. A FAILED run is recorded too, and cuts it as well: the kernel cuts a
2528        // Sink's target whenever the endpoint ran (ikigai-core 0.1.95; pinned in
2529        // `tests/lifecycle.rs`).
2530        inv.depends_on(name::part_iri(&name, "runs"));
2531        // A run record says who ran what: readable by the script's readers and its named
2532        // runners, never through the public grants.
2533        if !may(inv, Act::Read, &name) && !may(inv, Act::Run, &name) {
2534            return Err(Error::Denied(format!(
2535                "a run record of urn:script:{name} needs `{}` or `{}`. {GRANT_FORMS}",
2536                cap(Act::Read, &name),
2537                cap(Act::Run, &name)
2538            )));
2539        }
2540        let id_text = inv
2541            .bindings
2542            .get("id")
2543            .ok_or_else(|| Error::Endpoint("no `id` captured".to_string()))?;
2544        let id = id_text.parse::<u64>().map_err(|_| Error::InvalidArgument {
2545            name: "id".to_string(),
2546            detail: format!("`{id_text}` is not a run number"),
2547        });
2548        let run = match id {
2549            Ok(id) => self.shared.backend.run(&name, id)?,
2550            Err(_) if inv.request.verb == Verb::Exists => None,
2551            Err(e) => return Err(e),
2552        };
2553        match inv.request.verb {
2554            Verb::Exists => Ok(plain(if run.is_some() { "true\n" } else { "false\n" })),
2555            Verb::Source => {
2556                let want = face_of(inv, &RECORD_FACES)?;
2557                let run = run.ok_or_else(|| {
2558                    Error::NotFound(format!("urn:script:{name} has no run {id_text}"))
2559                })?;
2560                let finished = run.outcome != Outcome::Running;
2561                let repr = match want {
2562                    JSON => json(&run)?,
2563                    TURTLE => {
2564                        let mut graph = crate::graph::Graph::default();
2565                        graph.record(&run);
2566                        turtle(graph)?
2567                    }
2568                    _ => plain(run.render()),
2569                };
2570                // A finished run never changes again; one still running will.
2571                Ok(if finished { repr.cacheable() } else { repr })
2572            }
2573            other => Err(unsupported("script-run", other)),
2574        }
2575    }
2576
2577    fn name(&self) -> &str {
2578        "script-run"
2579    }
2580
2581    fn describe(&self) -> Description {
2582        let id = || {
2583            ArgSpec::new("id")
2584                .summary("The run's number, unique per script.")
2585                .class(XSD_INTEGER)
2586                .binding()
2587        };
2588        Description::new("script-run")
2589            .title("One run of a script")
2590            .summary(
2591                "A recorded run: principal, version, capability, start and end, outcome, the \
2592                 result (up to 64 KiB) and the host's trace span when it traced. Needs the \
2593                 script's read or run grant.",
2594            )
2595            .verb(Verb::Meta)
2596            .action(
2597                ActionSpec::new(Verb::Source)
2598                    .summary("The run record, as text, JSON, or a PROV-O graph (Turtle).")
2599                    .input(name_arg())
2600                    .input(id())
2601                    .input(record_as_arg())
2602                    .output(PLAIN)
2603                    .output(JSON)
2604                    .output(TURTLE)
2605                    .requires(CAP_ANY),
2606            )
2607            .action(
2608                ActionSpec::new(Verb::Exists)
2609                    .summary("Whether the run exists.")
2610                    .input(name_arg())
2611                    .input(id())
2612                    .output(PLAIN)
2613                    .requires(CAP_ANY),
2614            )
2615    }
2616}
2617
2618// ---------------------------------------------------------------------------------------
2619// urn:script:eval — the paste box
2620// ---------------------------------------------------------------------------------------
2621
2622struct EvalEndpoint;
2623
2624#[async_trait]
2625impl Endpoint for EvalEndpoint {
2626    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
2627        if inv.request.verb != Verb::Sink {
2628            return Err(unsupported("script-eval", inv.request.verb));
2629        }
2630        let code = inv.inline_str("content")?;
2631        let language_text = optional(inv, "language")?;
2632        let language = language_text
2633            .map(Language::parse)
2634            .transpose()?
2635            .unwrap_or(Language::Lisp);
2636        if language == Language::Sparql {
2637            return Err(Error::InvalidArgument {
2638                name: "language".to_string(),
2639                detail: "SPARQL through urn:script:eval is the SPARQL Protocol face (ledger \
2640                         #955), not in this version: publish the query as a script and read \
2641                         its `…:result`"
2642                    .to_string(),
2643            });
2644        }
2645        if language == Language::Plan {
2646            // Not a second door onto the same thing: the host's evaluator already runs a
2647            // supplied plan under the caller's own capability, which is all this door adds.
2648            return Err(Error::InvalidArgument {
2649                name: "language".to_string(),
2650                detail: format!(
2651                    "a plan supplied ad hoc runs at {} (`in` = the plan), under the caller's \
2652                     own capability; to keep it, publish it as a script",
2653                    plan::EVAL
2654                ),
2655            });
2656        }
2657        if let Some(save) = optional(inv, "save")? {
2658            let save = save.trim();
2659            name::validate(save)?;
2660            // Saved the way any writer saves: a Sink to the script, under the caller's own
2661            // capability (so it needs `urn:cap:script:write:{save}`), which cuts the script's
2662            // thread like every other write.
2663            let mut request = Request::new(Verb::Sink, iri(&name::script_iri(save))?)
2664                .with_arg("content", inline(code))
2665                .with_arg("language", inline(language.as_str()))
2666                .with_arg("state", inline("draft"));
2667            if let Some(requires) = optional(inv, "requires")? {
2668                request = request.with_arg("requires", inline(requires));
2669            }
2670            inv.issue(request).await?;
2671        }
2672        // ★ The caller's OWN capability, unchanged: `issue`, not `issue_attenuated`, and
2673        // nothing that could add to it exists.
2674        let answer = inv
2675            .issue(lisp_request(code, optional(inv, "data")?)?)
2676            .await?;
2677        Ok(Representation::new(answer.repr_type, answer.bytes))
2678    }
2679
2680    fn name(&self) -> &str {
2681        "script-eval"
2682    }
2683
2684    fn describe(&self) -> Description {
2685        Description::new("script-eval")
2686            .title("Run code you supply")
2687            .summary(
2688                "Run typed or pasted code as an anonymous script under the caller's OWN \
2689                 capability and nothing more; answers its value. `save=` keeps it as a draft \
2690                 of that script (which needs that script's write grant).",
2691            )
2692            .verb(Verb::Meta)
2693            .action(
2694                ActionSpec::new(Verb::Sink)
2695                    .summary("Run the code.")
2696                    .input(
2697                        ArgSpec::new("content")
2698                            .summary("The code (piped).")
2699                            .class(XSD_STRING),
2700                    )
2701                    .input(
2702                        ArgSpec::new("language")
2703                            .summary(
2704                                "The code's language: `lisp`, the one this door runs, so the \
2705                                 one its `requires` (`urn:cap:lisp`) is for. A plan runs at \
2706                                 urn:plan:eval, which declares no language capability; SPARQL \
2707                                 here is the Protocol face, not in this version.",
2708                            )
2709                            .class(XSD_STRING)
2710                            // ★ Only what this door runs (ledger #1174): offering `sparql` and
2711                            // `plan` here advertised values it always refuses, under a floor
2712                            // (`urn:cap:lisp`) that only Lisp needs.
2713                            .one_of([Language::Lisp.as_str()])
2714                            .default_value("lisp")
2715                            .optional(),
2716                    )
2717                    .input(data_arg("data", DATA_SUMMARY))
2718                    .input(
2719                        ArgSpec::new("save")
2720                            .summary(
2721                                "A script name: also keep the code as a draft at \
2722                                 `urn:script:{save}`. Needs `urn:cap:script:write:{save}`.",
2723                            )
2724                            .class(XSD_STRING)
2725                            .optional(),
2726                    )
2727                    .input(
2728                        ArgSpec::new("requires")
2729                            .summary("With `save=`: the draft's declared capability.")
2730                            .class(XSD_STRING)
2731                            .optional(),
2732                    )
2733                    .output(PLAIN)
2734                    .requires(CAP_LISP),
2735            )
2736    }
2737}
2738
2739// ---------------------------------------------------------------------------------------
2740// urn:script:catalog
2741// ---------------------------------------------------------------------------------------
2742
2743/// One row of the catalog.
2744#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2745pub struct CatalogEntry {
2746    /// The script.
2747    pub name: String,
2748    /// `urn:script:{name}`.
2749    pub iri: String,
2750    /// Its state, when its head could be read.
2751    pub state: Option<State>,
2752    /// Whether it is public.
2753    pub public: Option<bool>,
2754    /// Its head version.
2755    pub version: Option<String>,
2756    /// Its latest run.
2757    #[serde(rename = "lastRun")]
2758    pub last_run: Option<LastRun>,
2759    /// Why its head could not be read, when it could not.
2760    pub error: Option<String>,
2761}
2762
2763/// A catalog row's latest run.
2764#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2765pub struct LastRun {
2766    /// `urn:script:{name}:run:{id}`.
2767    pub iri: String,
2768    /// `running`, `ok` or `failed`.
2769    pub status: String,
2770    /// When it ended.
2771    pub ended: Option<u64>,
2772}
2773
2774/// The catalog's JSON face.
2775#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2776pub struct Catalog {
2777    /// [`SCHEMA`].
2778    pub schema: u32,
2779    /// Every script the caller may read, by name.
2780    pub scripts: Vec<CatalogEntry>,
2781}
2782
2783struct CatalogEndpoint {
2784    shared: Arc<SpaceConfig>,
2785}
2786
2787#[async_trait]
2788impl Endpoint for CatalogEndpoint {
2789    async fn invoke(&self, inv: &Invocation<'_>) -> Result<Representation> {
2790        if inv.request.verb != Verb::Source {
2791            return Err(unsupported("script-catalog", inv.request.verb));
2792        }
2793        let want = face_of(inv, &RECORD_FACES)?;
2794        let backend = &self.shared.backend;
2795        let mut scripts = Vec::new();
2796        // Each row's last run in full, for the graph face (a catalog row keeps less).
2797        let mut last_runs = Vec::new();
2798        for name in backend.names()? {
2799            let own = may(inv, Act::Read, &name);
2800            let mut entry = CatalogEntry {
2801                iri: name::script_iri(&name),
2802                name: name.clone(),
2803                state: None,
2804                public: None,
2805                version: None,
2806                last_run: None,
2807                error: None,
2808            };
2809            let mut last = None;
2810            match backend.head(&name) {
2811                // A broken head is shown to whoever may read the script, so the dashboard
2812                // says what is wrong instead of losing the row (or the whole list).
2813                Err(e) if own => entry.error = Some(e.to_string()),
2814                // Someone else's draft is not listed: to this caller it does not exist.
2815                Ok(Some(head)) if sight(inv, &head, &head.version) == Sight::Hidden => continue,
2816                Ok(Some(head))
2817                    if own
2818                        || (is_public(&head)
2819                            && may_public(inv, Act::Read, CAP_READ_PUBLIC, &name)) =>
2820                {
2821                    entry.state = Some(head.state);
2822                    entry.public = Some(head.public);
2823                    entry.version = Some(head.version);
2824                    last = backend.last_run(&name).ok().flatten();
2825                    entry.last_run = last.as_ref().map(|run| LastRun {
2826                        iri: run.iri(),
2827                        status: match run.outcome {
2828                            Outcome::Running => "running",
2829                            Outcome::Ok => "ok",
2830                            Outcome::Failed { .. } => "failed",
2831                        }
2832                        .to_string(),
2833                        ended: run.ended,
2834                    });
2835                }
2836                _ => continue,
2837            }
2838            last_runs.push(last);
2839            scripts.push(entry);
2840        }
2841        if want == TURTLE {
2842            let mut graph = crate::graph::Graph::default();
2843            for (entry, last) in scripts.iter().zip(&last_runs) {
2844                graph.script(&entry.name, entry.version.as_deref(), last.as_ref());
2845            }
2846            return turtle(graph);
2847        }
2848        if want == JSON {
2849            // Live: a run that fails is recorded without a write through any name the
2850            // catalog could hang from, so this list is never cached.
2851            return json(&Catalog {
2852                schema: SCHEMA,
2853                scripts,
2854            });
2855        }
2856        let mut text = String::new();
2857        for entry in &scripts {
2858            match (&entry.error, entry.state) {
2859                (Some(error), _) => text.push_str(&format!("{}  BROKEN: {error}\n", entry.iri)),
2860                (None, Some(state)) => {
2861                    let version = entry.version.as_deref().unwrap_or("");
2862                    text.push_str(&format!(
2863                        "{}  {}{}  {}",
2864                        entry.iri,
2865                        state.as_str(),
2866                        if entry.public == Some(true) {
2867                            " public"
2868                        } else {
2869                            ""
2870                        },
2871                        &version[..version.len().min(19)],
2872                    ));
2873                    if let Some(run) = &entry.last_run {
2874                        text.push_str(&format!(
2875                            "  last run {} {} {}",
2876                            run.iri,
2877                            run.status,
2878                            model::when(run.ended)
2879                        ));
2880                    }
2881                    text.push('\n');
2882                }
2883                _ => {}
2884            }
2885        }
2886        if scripts.is_empty() {
2887            text.push_str("no scripts this capability may read\n");
2888        }
2889        Ok(plain(text))
2890    }
2891
2892    fn name(&self) -> &str {
2893        "script-catalog"
2894    }
2895
2896    fn describe(&self) -> Description {
2897        Description::new("script-catalog")
2898            .title("Scripts")
2899            .summary(
2900                "Every script the caller may read: state, public flag, head version and last \
2901                 run. Live (never cached). A host that wants `urn:host:scripts` aliases it \
2902                 here.",
2903            )
2904            .verb(Verb::Meta)
2905            .action(
2906                ActionSpec::new(Verb::Source)
2907                    .summary("The catalog, as text, JSON, or a graph (Turtle).")
2908                    .input(record_as_arg())
2909                    .output(PLAIN)
2910                    .output(JSON)
2911                    .output(TURTLE)
2912                    .requires(CAP_READ),
2913            )
2914    }
2915}
2916
2917fn unsupported(id: &str, verb: Verb) -> Error {
2918    Error::Endpoint(format!("{id} does not answer {verb:?}"))
2919}