Skip to main content

submilli_engine/stdlib/
shared.rs

1//! Helpers shared across stdlib libraries.
2
3use crate::runtime::approvals::{self, ApprovalRequest, Unasked};
4use crate::runtime::decision::{
5    CallOrigin, CallSite, CallTicket, EntryPath, PackageLine, SourceLine,
6};
7use crate::runtime::fs::{ContainError, ContentPath, LinkPath, resolve_content, resolve_link};
8use crate::runtime::fuel;
9use crate::runtime::host::{
10    permission_denied, permission_denied_invariant, permission_denied_read_only,
11    quota_exceeded_error, write_boxed_number_struct,
12};
13use crate::runtime::metrics::MetricsSink;
14use crate::runtime::security::{AuditDecision, CheckOutcome, SecurityCheck};
15use crate::runtime::vfs::{Access, Placement};
16use crate::runtime::{QuotaExceeded, StoreData};
17
18/// The package whose code is executing, read off the innermost wasm frame's owning module.
19///
20/// This answers "whose code is running", which is the question a capability check needs —
21/// not "whose export did we last enter", which is what a shadow stack records. The two
22/// diverge whenever a package invokes caller-supplied code: a vtable-dispatched `toJson`, a
23/// closure passed across a package boundary.
24///
25/// Fails closed on both ways the principal can come back unknown, and neither may fall back
26/// to a permissive default:
27///
28/// - **No frames.** No wasm is executing, so this is an embedder-driven call with no guest
29///   principal to speak of.
30/// - **Frames, but the innermost module has no name.** A module this compiler produced always
31///   carries one. One that does not is not a principal we can name.
32///
33/// Attribution visits the innermost module directly, independently of the
34/// diagnostic backtrace setting, without capturing or symbolizing outer frames.
35pub(crate) fn running_package(
36    store: &impl wasmtime::AsContext<Data = StoreData>,
37) -> Result<String, PrincipalError> {
38    let mut principal = None;
39    wasmtime::WasmBacktrace::visit_modules(store, |module| {
40        principal = Some(match module.name() {
41            Some(name) => owned_principal(name).map_err(PrincipalError::Internal),
42            None => Err(PrincipalError::Unknown(UnknownPrincipal {
43                label: "<unnamed module>",
44                reason: "the running module declares no package name, so its caller cannot be identified",
45            })),
46        });
47        std::ops::ControlFlow::Break(())
48    }).map_err(|error| PrincipalError::Internal(crate::runtime::host::fatal_host_error(error)))?;
49    principal.unwrap_or_else(|| {
50        Err(PrincipalError::Unknown(UnknownPrincipal {
51            label: "<no wasm frame>",
52            reason: "no wasm frame is executing, so the call has no caller to attribute it to",
53        }))
54    })
55}
56
57pub(crate) fn owned_principal(name: &str) -> wasmtime::Result<String> {
58    let mut result = String::new();
59    result
60        .try_reserve_exact(name.len())
61        .map_err(crate::runtime::host::fatal_host_error)?;
62    result.push_str(name);
63    Ok(result)
64}
65
66pub(crate) enum PrincipalError {
67    Unknown(UnknownPrincipal),
68    Internal(wasmtime::Error),
69}
70impl PrincipalError {
71    pub fn into_denial(self, capability: &str) -> wasmtime::Error {
72        match self {
73            Self::Unknown(unknown) => {
74                permission_denied_invariant(unknown.label, capability, unknown.reason)
75            }
76            Self::Internal(error) => error,
77        }
78    }
79
80    pub fn label_or_error(self) -> wasmtime::Result<String> {
81        match self {
82            Self::Unknown(unknown) => owned_principal(unknown.label),
83            Self::Internal(error) => Err(error),
84        }
85    }
86}
87
88/// An unknown principal label cannot collide with an actual package name.
89pub(crate) struct UnknownPrincipal {
90    pub label: &'static str,
91    pub reason: &'static str,
92}
93
94/// Call at the top of each gated host fn body — after argument parsing,
95/// before any I/O. Attributes the call to whoever's code is running, via
96/// [`running_package`]. A denial surfaces to the guest as a catchable
97/// `PermissionDeniedError`.
98///
99/// A capability the catalog marks `main_denial` is refused to `main` here,
100/// ahead of the policy. `submilli:security#check` does not route through this
101/// function, so the invariant does not extend to it; that path attributes to
102/// the caller and would deny anyway.
103pub fn check_security(
104    store: impl wasmtime::AsContextMut<Data = StoreData>,
105    capability: &str,
106    context: serde_json::Value,
107) -> wasmtime::Result<()> {
108    check_security_call(store, capability, context).map(|_| ())
109}
110
111/// [`check_security`], returning the recorder's ticket for the call when one is installed,
112/// for host functions whose later decisions (redirect hops) belong to the same call.
113///
114/// It cannot wait, so a call the policy asks about is refused as a policy denial. A gate
115/// that can wait for the approver uses [`check_and_approve_call`].
116pub(crate) fn check_security_call(
117    mut store: impl wasmtime::AsContextMut<Data = StoreData>,
118    capability: &str,
119    context: serde_json::Value,
120) -> wasmtime::Result<Option<CallTicket>> {
121    let (caller, ticket) = enter_gate(&mut store, capability, &context)?;
122    let data = store.as_context().data();
123    authorize_capability(
124        &caller,
125        data.security_check.as_ref(),
126        data.metrics.as_ref(),
127        capability,
128        &context,
129        data.vfs.cwd(),
130        CallSite::new(ticket, EntryPath::GatedOp),
131    )?;
132    Ok(ticket)
133}
134
135/// [`check_security_call`] for a gate that can wait: a call the policy asks about waits
136/// for its approver's decision, as an effect of its own ahead of the operation's.
137pub(crate) async fn check_and_approve_call(
138    caller: &mut wasmtime::Caller<'_, StoreData>,
139    capability: &str,
140    context: serde_json::Value,
141) -> wasmtime::Result<Option<CallTicket>> {
142    let (principal, ticket) = enter_gate(&mut *caller, capability, &context)?;
143    let site = CallSite::new(ticket, EntryPath::GatedOp);
144    let checked = {
145        let data = caller.data();
146        authorize_or_ask(
147            &principal,
148            data.security_check.as_ref(),
149            capability,
150            &context,
151            data.vfs.cwd(),
152            site,
153        )?
154    };
155    if let Checked::Ask(request) = checked {
156        approvals::ask(caller, request, site).await?;
157    }
158    Ok(ticket)
159}
160
161/// The check a listing makes for one candidate, the check its use would make,
162/// without waiting. A candidate the policy asks about is kept, and not audited, when
163/// this run can ask its approver, since using it asks. Otherwise it is refused as a
164/// policy denial, as its use would be.
165pub(crate) fn check_candidate(
166    mut store: impl wasmtime::AsContextMut<Data = StoreData>,
167    capability: &str,
168    context: serde_json::Value,
169) -> wasmtime::Result<()> {
170    let (caller, ticket) = enter_gate(&mut store, capability, &context)?;
171    let site = CallSite::new(ticket, EntryPath::GatedOp);
172    let data = store.as_context().data();
173    let security_check = data.security_check.as_ref();
174    match authorize_or_ask(
175        &caller,
176        security_check,
177        capability,
178        &context,
179        data.vfs.cwd(),
180        site,
181    )? {
182        Checked::Allowed => Ok(()),
183        Checked::Ask(_) if data.approval_provider.is_some() => Ok(()),
184        // Filtered out, not refused: no call was made, so nothing is reported.
185        Checked::Ask(request) => {
186            let denial = approvals::deny_unasked(&request, security_check, site);
187            Err(permission_denied(
188                request.caller,
189                request.capability,
190                denial,
191            ))
192        }
193    }
194}
195
196/// The gate's entry: its flat charge, the caller's attribution, and the recorder's call.
197fn enter_gate(
198    mut store: impl wasmtime::AsContextMut<Data = StoreData>,
199    capability: &str,
200    context: &serde_json::Value,
201) -> wasmtime::Result<(String, Option<CallTicket>)> {
202    // Direct caller attribution and the policy check have one flat gate charge.
203    fuel::charge_host_fuel(&mut store, fuel::GATE)?;
204    let caller = running_package(&store).map_err(|error| {
205        if let PrincipalError::Unknown(ref unknown) = error {
206            audit_entry_denial(
207                &store,
208                unknown.label,
209                capability,
210                context,
211                "invariant",
212                unknown.reason,
213            );
214        }
215        error.into_denial(capability)
216    })?;
217    // Uncharged on purpose; see `DecisionLogConfig::max_line_capture_frames`.
218    let ticket = begin_recorded_call(&store, &caller, capability);
219    Ok((caller, ticket))
220}
221
222/// Begins the recorder's call for a host function at its entry, with where in the
223/// submitted program, and in a package, it came from. `None` when no recorder is installed.
224pub(crate) fn begin_recorded_call(
225    store: &impl wasmtime::AsContext<Data = StoreData>,
226    caller: &str,
227    capability: &str,
228) -> Option<CallTicket> {
229    let recorder = store.as_context().data().security_check.recorder()?;
230    Some(recorder.begin_call(caller, capability, call_origin(store)))
231}
232
233/// Where the running call came from: the line in the submitted program, and the line
234/// inside a package when the program reached the call through one. Each is `None` when
235/// its lookup fails.
236///
237/// Walks a full backtrace, which costs more than [`running_package`]'s module visit, so
238/// it runs only while a recorder is installed and still wants lines, uncharged to guest
239/// fuel (see `DecisionLogConfig::max_line_capture_frames`). The program's line is its own
240/// call into the package; the package's line is the innermost frame of package code.
241pub(crate) fn call_origin(store: &impl wasmtime::AsContext<Data = StoreData>) -> CallOrigin {
242    let Some(recorder) = store.as_context().data().security_check.recorder() else {
243        return CallOrigin::default();
244    };
245    if !recorder.wants_line() {
246        return CallOrigin::default();
247    }
248    let backtrace = wasmtime::WasmBacktrace::force_capture(store);
249    recorder.note_line_capture(backtrace.frames().len());
250    let frames = backtrace.frames();
251    let program = frames
252        .iter()
253        .position(|frame| frame.module().name() == Some(crate::mangle::USER_PACKAGE));
254    let line = program.and_then(|at| {
255        let symbol = frames[at].symbols().first()?;
256        Some(SourceLine {
257            line: symbol.line()?,
258            column: symbol.column(),
259        })
260    });
261    // Frames inside the program's call: a package's frames carry its name on their path.
262    let package = frames[..program.unwrap_or(frames.len())]
263        .iter()
264        .find_map(package_line);
265    CallOrigin { line, package }
266}
267
268/// The package line a frame is at, when it is package code with a known line.
269fn package_line(frame: &wasmtime::FrameInfo) -> Option<PackageLine> {
270    let package = frame.module().name()?;
271    let symbol = frame.symbols().first()?;
272    let module = symbol.file()?.strip_prefix(package)?.strip_prefix('/')?;
273    Some(PackageLine {
274        package: package.to_owned(),
275        module: module.to_owned(),
276        function: symbol.name().map(ToString::to_string),
277        line: symbol.line()?,
278        column: symbol.column(),
279    })
280}
281
282/// Marks the latest recorded decision as a denial this host function swallowed to filter a
283/// listing. A no-op without a recorder.
284pub(crate) fn mark_filtered(store: &impl wasmtime::AsContext<Data = StoreData>) {
285    if let Some(recorder) = store.as_context().data().security_check.recorder() {
286        recorder.mark_last_filtered();
287    }
288}
289
290/// [`check_security`] for a caller resolved earlier, such as the principal of a
291/// request whose redirect hops are checked after the host fn has suspended. A call
292/// the policy asks about is refused, and reported to `metrics`: this check cannot wait.
293pub(crate) fn authorize_capability(
294    caller: &str,
295    security_check: &dyn SecurityCheck,
296    metrics: &dyn MetricsSink,
297    capability: &str,
298    context: &serde_json::Value,
299    cwd: &str,
300    site: CallSite,
301) -> wasmtime::Result<()> {
302    match authorize_or_ask(caller, security_check, capability, context, cwd, site)? {
303        Checked::Allowed => Ok(()),
304        Checked::Ask(request) => Err(approvals::refuse(
305            request,
306            Unasked::CannotWait,
307            security_check,
308            metrics,
309            site,
310        )),
311    }
312}
313
314/// [`authorize_capability`], handing back a call the policy asks about instead of
315/// refusing it.
316fn authorize_or_ask(
317    caller: &str,
318    security_check: &dyn SecurityCheck,
319    capability: &str,
320    context: &serde_json::Value,
321    cwd: &str,
322    site: CallSite,
323) -> wasmtime::Result<Checked> {
324    // The ordering is the invariant. This must precede both the delegation
325    // below and any work the host fn does after we return — a reorder that
326    // consults the policy first makes the refusal conditional on a policy
327    // existing, and the default check allows everything.
328    if caller == crate::mangle::USER_PACKAGE
329        && let Some(reason) =
330            crate::stdlib::capabilities::find_any(capability).and_then(|entry| entry.main_denial)
331    {
332        audit_denial_at(
333            security_check,
334            caller,
335            capability,
336            context,
337            "invariant",
338            reason,
339            site,
340        );
341        return Err(permission_denied_invariant(caller, capability, reason));
342    }
343    check_or_ask(security_check, caller, capability, context, cwd, site)
344        .map_err(|reason| permission_denied(caller, capability, reason))
345}
346
347/// A policy check's answer short of a denial.
348pub(crate) enum Checked {
349    Allowed,
350    /// The policy asks an approver; nothing is audited until they decide.
351    Ask(ApprovalRequest),
352}
353
354/// Asks the policy whether `caller` may use `capability`, and audits its answer with the
355/// recorder's explanation. A denial returns the policy's reason. It cannot wait, so a
356/// call the policy asks about is refused, and reported to `metrics`.
357pub(crate) fn check_and_audit(
358    security_check: &dyn SecurityCheck,
359    metrics: &dyn MetricsSink,
360    caller: &str,
361    capability: &str,
362    context: &serde_json::Value,
363    cwd: &str,
364    site: CallSite,
365) -> Result<(), String> {
366    match check_or_ask(security_check, caller, capability, context, cwd, site)? {
367        Checked::Allowed => Ok(()),
368        Checked::Ask(request) => {
369            request.report_unasked(Unasked::CannotWait, None, metrics);
370            Err(approvals::deny_unasked(&request, security_check, site))
371        }
372    }
373}
374
375/// [`check_and_audit`], handing back a call the policy asks about. Its request carries
376/// the context as the policy tested it.
377pub(crate) fn check_or_ask(
378    security_check: &dyn SecurityCheck,
379    caller: &str,
380    capability: &str,
381    context: &serde_json::Value,
382    cwd: &str,
383    site: CallSite,
384) -> Result<Checked, String> {
385    let audit_context = security_check.audit_context(capability, context, cwd);
386    let (allowed, rule, reason) =
387        match security_check.check_with_cwd(caller, capability, context, cwd) {
388            CheckOutcome::Allow { rule } => (true, rule, None),
389            CheckOutcome::Deny { reason, rule } => (false, rule, Some(reason)),
390            CheckOutcome::Ask { approver, rule } => {
391                return Ok(Checked::Ask(ApprovalRequest {
392                    approver,
393                    caller: caller.to_owned(),
394                    capability: capability.to_owned(),
395                    context: audit_context.into_owned(),
396                    rule,
397                }));
398            }
399        };
400    // The explanation is for the recorder alone; enforcement never reads it.
401    let explanation = security_check
402        .recorder()
403        .and_then(|_| security_check.explain(caller, capability, context, cwd));
404    security_check.audit(
405        AuditDecision::new(
406            caller,
407            capability,
408            audit_context.as_ref(),
409            allowed,
410            "policy",
411            rule,
412            reason.as_deref(),
413        )
414        .with_explanation(explanation.as_ref())
415        .with_site(site),
416    );
417    match reason {
418        None => Ok(Checked::Allowed),
419        Some(reason) => Err(reason),
420    }
421}
422
423pub(crate) fn audit_denial_at(
424    security: &dyn SecurityCheck,
425    caller: &str,
426    capability: &str,
427    context: &serde_json::Value,
428    source: &str,
429    reason: &str,
430    site: CallSite,
431) {
432    security.audit(
433        AuditDecision::new(
434            caller,
435            capability,
436            context,
437            false,
438            source,
439            None,
440            Some(reason),
441        )
442        .with_site(site),
443    );
444}
445
446/// Audits a refusal at the entry of a host call as a call of its own, with the program
447/// line that led to it. For a refusal after the call's gate, see [`audit_denial_in`].
448pub(crate) fn audit_entry_denial(
449    store: &impl wasmtime::AsContext<Data = StoreData>,
450    caller: &str,
451    capability: &str,
452    context: &serde_json::Value,
453    source: &str,
454    reason: &str,
455) {
456    let security = store.as_context().data().security_check.as_ref();
457    let ticket = security
458        .recorder()
459        .map(|recorder| recorder.begin_call(caller, capability, call_origin(store)));
460    audit_denial_at(
461        security,
462        caller,
463        capability,
464        context,
465        source,
466        reason,
467        CallSite::new(ticket, EntryPath::GatedOp),
468    );
469}
470
471/// Audits a further refusal inside the host call `ticket` names, such as a read-only or
472/// quota refusal after the policy allowed it. Without a ticket the refusal is its own call.
473pub(crate) fn audit_denial_in(
474    store: &impl wasmtime::AsContext<Data = StoreData>,
475    ticket: Option<CallTicket>,
476    caller: &str,
477    capability: &str,
478    context: &serde_json::Value,
479    source: &str,
480    reason: &str,
481) {
482    audit_denial_at(
483        store.as_context().data().security_check.as_ref(),
484        caller,
485        capability,
486        context,
487        source,
488        reason,
489        CallSite::new(ticket, EntryPath::GatedOp),
490    );
491}
492
493/// Resolve a guest path for an operation that reaches its contents — every component,
494/// including the last, traversed under containment.
495pub fn resolve_content_or_trap(
496    data: &StoreData,
497    guest_path: &str,
498    op: &str,
499) -> wasmtime::Result<ContentPath> {
500    resolve_content(&data.vfs, data.vfs.cwd(), guest_path)
501        .map_err(|err| contain_trap(op, guest_path, &err))
502}
503
504/// Resolve a guest path for an operation that acts on the link itself — the final
505/// component is never followed.
506pub fn resolve_link_or_trap(
507    data: &StoreData,
508    guest_path: &str,
509    op: &str,
510) -> wasmtime::Result<LinkPath> {
511    resolve_link(&data.vfs, data.vfs.cwd(), guest_path)
512        .map_err(|err| contain_trap(op, guest_path, &err))
513}
514
515/// Refuse a write into a volume mounted read-only, attributed to the running
516/// package. Call after the capability check and resolution, before any other
517/// work, so the policy sees every attempt and a refused call changes nothing.
518/// `ticket` is the host call's gate, which the refusal continues.
519pub(crate) fn require_writable(
520    store: impl wasmtime::AsContext<Data = StoreData>,
521    ticket: Option<CallTicket>,
522    placement: &Placement,
523    capability: &str,
524    guest_path: &str,
525) -> wasmtime::Result<()> {
526    if placement.access() == Access::ReadWrite {
527        return Ok(());
528    }
529    let caller = running_package(&store).map_err(|error| {
530        if let PrincipalError::Unknown(ref unknown) = error {
531            audit_denial_in(
532                &store,
533                ticket,
534                unknown.label,
535                capability,
536                &serde_json::json!({ "path": guest_path }),
537                "invariant",
538                unknown.reason,
539            );
540        }
541        error.into_denial(capability)
542    })?;
543    audit_denial_in(
544        &store,
545        ticket,
546        &caller,
547        capability,
548        &serde_json::json!({ "path": guest_path }),
549        "read_only",
550        "the destination volume is read-only",
551    );
552    Err(read_only_denial(
553        &caller,
554        capability,
555        guest_path,
556        placement.mount_point(),
557    ))
558}
559
560fn read_only_denial(
561    caller: &str,
562    capability: &str,
563    guest_path: &str,
564    mount: &str,
565) -> wasmtime::Error {
566    permission_denied_read_only(
567        caller,
568        capability,
569        format!("{guest_path} is in the volume mounted read-only at {mount}"),
570    )
571}
572
573/// The single translation from a containment failure to a guest-visible trap. Every
574/// escape reaches the guest as the same diagnostic regardless of which operation hit it,
575/// so an LLM reading one recognises the rest.
576///
577/// A read-only refusal that got past [`require_writable`] still surfaces as a
578/// `PermissionDeniedError`, attributed to `<vfs>` since the caller is not at hand.
579pub fn contain_trap(op: &str, guest_path: &str, err: &ContainError) -> wasmtime::Error {
580    if let ContainError::ReadOnly(mount) = err {
581        return read_only_denial("<vfs>", op, guest_path, mount);
582    }
583    wasmtime::Error::msg(format!("{op} {guest_path}: {err}"))
584}
585
586/// Distinguish a missing parent from an escaping one at the point a write first touches
587/// the filesystem.
588///
589/// Both surface as a failure to create, and keeping them apart is what an LLM needs: one
590/// is fixable with `mkdir`, the other can never succeed. The `parent.is_dir()` pre-check
591/// this replaced conflated them the other way — it reported every escape as a missing
592/// parent, costing a turn on a `mkdir` that could never work.
593pub fn write_target_trap(op: &str, guest_path: &str, err: &ContainError) -> wasmtime::Error {
594    match err {
595        ContainError::Io(e) if e.kind() == std::io::ErrorKind::NotFound => {
596            wasmtime::Error::msg(format!(
597                "{op} {guest_path}: parent directory does not exist \
598                 (create it with mkdir(path, {{ recursive: true }}))"
599            ))
600        }
601        _ => contain_trap(op, guest_path, err),
602    }
603}
604
605/// A write refused because it would pass the VFS's size limit, as a `QuotaExceededError`
606/// the program can catch.
607pub(crate) fn quota_refusal(
608    op: &str,
609    guest_path: &str,
610    exceeded: QuotaExceeded,
611) -> wasmtime::Error {
612    quota_exceeded_error(format!("{op} {guest_path}: {exceeded}"))
613}
614
615// --- shared by the model-listing libraries (`submilli:llm`, `submilli:embedding`) ---
616
617/// Characters a `Model.description` may carry into a guest model's
618/// model-selection reasoning.
619///
620/// The bound is not the whole defense — [`sanitize_description`] strips control
621/// characters and collapses the text to one line first, because a length bound
622/// alone does not stop an injection short enough to fit. The bound is what stops
623/// a long one, and what keeps a listing's size independent of how much prose an
624/// operator wrote.
625pub const MAX_DESCRIPTION_CHARS: usize = 280;
626
627/// Whether a per-candidate check's answer removes the candidate (`Ok(false)`),
628/// keeps it (`Ok(true)`), or must propagate (`Err`).
629///
630/// Only the policy's own answer filters. An invariant denial means the check
631/// could not be made at all — the caller could not be named, or a runtime rule
632/// refused ahead of the policy — and swallowing it would turn a runtime refusal
633/// into a silently short listing that reads as "the operator configured fewer
634/// models."
635pub(crate) fn filters_candidate(checked: wasmtime::Result<()>) -> wasmtime::Result<bool> {
636    let Err(err) = checked else {
637        return Ok(true);
638    };
639    match err.downcast_ref::<crate::runtime::host::PermissionDenied>() {
640        Some(denial) if denial.is_policy() => Ok(false),
641        _ => Err(err),
642    }
643}
644
645/// The candidates of a listing the caller may use. `gate` runs the capability
646/// check one candidate's use would; a policy denial leaves the candidate out and
647/// marks the recorded decision filtered, and any other refusal propagates, the
648/// [`filters_candidate`] rule.
649pub(crate) fn permitted_candidates<T>(
650    caller: &mut wasmtime::Caller<'_, StoreData>,
651    candidates: Vec<T>,
652    name_of: impl Fn(&T) -> &str,
653    mut gate: impl FnMut(&mut wasmtime::Caller<'_, StoreData>, &str) -> wasmtime::Result<()>,
654) -> wasmtime::Result<Vec<T>> {
655    let mut permitted = Vec::with_capacity(candidates.len());
656    for candidate in candidates {
657        if filters_candidate(gate(caller, name_of(&candidate)))? {
658            permitted.push(candidate);
659        } else {
660            mark_filtered(&*caller);
661        }
662    }
663    Ok(permitted)
664}
665
666/// At most the first 200 bytes of a program-supplied value, for an error that
667/// echoes it: a refused value may be refused for its size.
668pub(crate) fn truncated(value: &str) -> std::borrow::Cow<'_, str> {
669    const MAX_ECHO_BYTES: usize = 200;
670    if value.len() <= MAX_ECHO_BYTES {
671        return std::borrow::Cow::Borrowed(value);
672    }
673    let end = value.floor_char_boundary(MAX_ECHO_BYTES);
674    std::borrow::Cow::Owned(format!("{}…", value.get(..end).unwrap_or_default()))
675}
676
677/// Reduce an operator-authored `description` to inert single-line data.
678///
679/// This is sanitization, not merely a bound. The text flows verbatim into a
680/// guest model's model-selection reasoning, so an injection that redirects which
681/// model a program calls fits comfortably inside any length limit — a bound
682/// alone stops only the long ones. Control characters and line breaks are what
683/// let injected text present itself as a new instruction block, so they are
684/// removed first; the bound then stops the rest.
685pub(crate) fn sanitize_description(description: &str) -> Option<String> {
686    let flattened: String = description
687        .chars()
688        .map(|c| if c.is_control() { ' ' } else { c })
689        .collect();
690    let collapsed = flattened.split_whitespace().collect::<Vec<_>>().join(" ");
691    if collapsed.is_empty() {
692        return None;
693    }
694    Some(collapsed.chars().take(MAX_DESCRIPTION_CHARS).collect())
695}
696
697/// Charge for the check and establish caller attribution even for an empty
698/// catalog, without asking policy about a model that does not exist.
699/// `count_field` names the filter context's count (`prompt_count`, `input_count`).
700pub(crate) fn preflight_models(
701    caller: &mut wasmtime::Caller<'_, StoreData>,
702    capability: &str,
703    count_field: &str,
704) -> wasmtime::Result<()> {
705    preflight_listing(
706        caller,
707        capability,
708        &serde_json::json!({ "model": "", count_field: 0 }),
709    )
710}
711
712/// [`preflight_models`] for any listing filtered per candidate: `context` is what
713/// an invariant denial is audited with, the context of an empty candidate.
714pub(crate) fn preflight_listing(
715    caller: &mut wasmtime::Caller<'_, StoreData>,
716    capability: &str,
717    context: &serde_json::Value,
718) -> wasmtime::Result<()> {
719    fuel::charge_host_fuel(&mut *caller, fuel::GATE)?;
720    running_package(caller).map(|_| ()).map_err(|error| {
721        if let PrincipalError::Unknown(ref unknown) = error {
722            audit_entry_denial(
723                &*caller,
724                unknown.label,
725                capability,
726                context,
727                "invariant",
728                unknown.reason,
729            );
730        }
731        error.into_denial(capability)
732    })
733}
734
735/// Audit a token-budget refusal against the running package, inside the host call
736/// `ticket` names. `Err` is the denial to throw instead when the package cannot
737/// be named.
738pub(crate) fn audit_quota_denial(
739    caller: &wasmtime::Caller<'_, StoreData>,
740    ticket: Option<CallTicket>,
741    capability: &str,
742    model: &str,
743    reason: &str,
744) -> Result<(), wasmtime::Error> {
745    let who = running_package(caller).or_else(PrincipalError::label_or_error)?;
746    audit_denial_in(
747        caller,
748        ticket,
749        &who,
750        capability,
751        &serde_json::json!({ "model": model }),
752        "quota",
753        reason,
754    );
755    Ok(())
756}
757
758/// A boxed number, or `undefined` when absent.
759pub(crate) fn optional_number(
760    caller: &mut wasmtime::Caller<'_, StoreData>,
761    number: Option<f64>,
762) -> wasmtime::Result<wasmtime::Val> {
763    match number {
764        Some(number) => Ok(wasmtime::Val::AnyRef(Some(
765            write_boxed_number_struct(caller, number)?.to_anyref(),
766        ))),
767        None => crate::runtime::prelude::undefined::value(caller),
768    }
769}