Skip to main content

sim_lib_agent_runner_process/
effects.rs

1use crate::{ModelRequest, ModelResponse, ProcessRunner};
2use sim_kernel::{
3    CapabilityName, Cx, Datum, DatumStore, Effect, Expr, Ref, Result, Symbol, core_any_ref, effect,
4    value_from_ref,
5};
6
7/// Returns the capability gating subprocess execution by a [`ProcessRunner`].
8///
9/// Spawning `/bin/sh -c <command>` is an arbitrary host-exec operation, so the
10/// runner demands this capability before reaching the shell, mirroring the way
11/// sibling crates gate their host effects with an explicit `cx.require(...)`.
12pub fn host_process_capability() -> CapabilityName {
13    CapabilityName::new("exec")
14}
15
16pub(super) fn resolve_process_effect<F>(
17    runner: &ProcessRunner,
18    cx: &mut Cx,
19    request: ModelRequest,
20    perform: F,
21) -> Result<ModelResponse>
22where
23    F: FnOnce(&ProcessRunner, ModelRequest) -> Result<ModelResponse>,
24{
25    let required_capability = process_effect_capability(cx);
26    let effect = process_effect(runner, cx, &request, required_capability)?;
27    let result = effect::resolve_effect(cx, effect, |cx, _effect| {
28        let response = perform(runner, request)?;
29        response_ref(cx, response)
30    })?;
31    response_from_ref(cx, &result)
32}
33
34fn process_effect(
35    runner: &ProcessRunner,
36    cx: &mut Cx,
37    request: &ModelRequest,
38    required_capability: CapabilityName,
39) -> Result<Effect> {
40    let input = Datum::Node {
41        tag: Symbol::qualified("agent", "ProcessRunnerInput"),
42        fields: vec![
43            (Symbol::new("runner"), Datum::Symbol(runner.runner.clone())),
44            (Symbol::new("model"), Datum::String(runner.model.clone())),
45            (
46                Symbol::new("request"),
47                Datum::try_from(Expr::from(request.clone()))?,
48            ),
49        ],
50    };
51    let input = Ref::Content(cx.datum_store_mut().intern(input)?);
52    Effect::new(
53        host_process_effect_kind(),
54        Ref::Symbol(runner.runner.clone()),
55        input,
56        core_any_ref(),
57        effect::effect_resume_op_key(),
58        effect::effect_abort_op_key(),
59    )
60    .with_replay_key(Some(Ref::Symbol(Symbol::qualified(
61        "agent",
62        "process-runner-v1",
63    ))))
64    .map(|effect| effect.requiring(required_capability))
65}
66
67fn host_process_effect_kind() -> Symbol {
68    Symbol::qualified("effect", "host-process")
69}
70
71fn process_effect_capability(cx: &Cx) -> CapabilityName {
72    granted_capability_or_alias(cx, host_process_capability(), exec_aliases())
73        .unwrap_or_else(host_process_capability)
74}
75
76fn exec_aliases() -> &'static [&'static str] {
77    &["host.process"]
78}
79
80fn granted_capability_or_alias(
81    cx: &Cx,
82    canonical: CapabilityName,
83    aliases: &'static [&'static str],
84) -> Option<CapabilityName> {
85    if cx.capabilities().contains(&canonical) {
86        return Some(canonical);
87    }
88    aliases
89        .iter()
90        .copied()
91        .map(CapabilityName::new)
92        .find(|alias| cx.capabilities().contains(alias))
93}
94
95fn response_ref(cx: &mut Cx, response: ModelResponse) -> Result<Ref> {
96    Ok(Ref::Content(
97        cx.datum_store_mut()
98            .intern(Datum::try_from(Expr::from(response))?)?,
99    ))
100}
101
102fn response_from_ref(cx: &mut Cx, reference: &Ref) -> Result<ModelResponse> {
103    ModelResponse::try_from(value_from_ref(cx, reference)?.object().as_expr(cx)?)
104}