Skip to main content

capsule_runtime/
types.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::PathBuf,
4    sync::Arc,
5};
6
7use murmur_artifact::{
8    ArtifactImplementation, ArtifactRuntime, ContextConfig, HookConfig, InferenceConfig,
9    LifecycleConfig, LifecycleOverride, Registry,
10};
11use wasmtime::{component::Component, Engine};
12
13use crate::{
14    bindings::host::murmur::tool::run::ToolResult,
15    hooks::ShellDispatchInfo,
16    limits::{EpochTicker, ExecutionLimits},
17};
18
19pub(crate) struct DispatchOutcome {
20    pub result: ToolResult,
21    pub shell: Option<ShellDispatchInfo>,
22    /// True when the dispatch was served by reading a skill.md file rather than running
23    /// a binary or WASM component. Used by the agent loop to write a `skill_call` trace
24    /// event instead of a `tool_call` event.
25    pub is_skill: bool,
26}
27
28impl DispatchOutcome {
29    pub(crate) fn tool(result: ToolResult) -> Self {
30        Self {
31            result,
32            shell: None,
33            is_skill: false,
34        }
35    }
36
37    pub(crate) fn skill(result: ToolResult) -> Self {
38        Self {
39            result,
40            shell: None,
41            is_skill: true,
42        }
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ArtifactRequest {
48    pub name: String,
49    pub version: String,
50    pub runtime: ArtifactRuntime,
51    /// Optional local source path (skill artifacts only). When set, the runtime resolves the
52    /// skill's `skill.md` directly from this path and skips registry resolution entirely.
53    /// Relative paths resolve against `StageRequest::manifest_dir`.
54    pub source: Option<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct LockExpectation {
59    pub name: String,
60    pub resolved_version: String,
61    pub sha256_wasm: String,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ResolvedLockArtifact {
66    pub name: String,
67    pub resolved_version: String,
68    pub sha256_wasm: String,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Default)]
72pub struct CapabilityPolicy {
73    pub network_allow: Vec<String>,
74    pub filesystem_scope: Option<String>,
75    pub shell_allow: Vec<String>,
76    pub spawn_allow: Vec<String>,
77    pub shell_strip_env: Vec<String>,
78    pub shell_baseline_env: Vec<String>,
79    /// Host env var names a WASM guest may observe, from `capabilities.env.allow`. Empty by
80    /// default: a guest sees only the runtime's own `MURMUR_*` injection unless the manifest
81    /// names a variable here. `shell_strip_env` and the credential patterns still apply on
82    /// top, so a credential-shaped name declared here is stripped anyway.
83    pub env_allow: Vec<String>,
84    /// Execution limits applied to every guest store in the session. Always fully
85    /// resolved — `capability_policy_from_runtime_manifest` substitutes a default for
86    /// each field `capabilities.limits` omits, so there is no "unset" state here and a
87    /// silent manifest yields [`ExecutionLimits::default`] rather than no limits.
88    pub limits: ExecutionLimits,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct InstalledArtifactSummary {
93    pub name: String,
94    pub version: String,
95    pub runtime: ArtifactRuntime,
96    pub implementation: Option<ArtifactImplementation>,
97}
98
99#[derive(Clone)]
100pub(crate) struct StagedHookArtifact {
101    pub name: String,
102    pub version: String,
103    pub component: Component,
104    pub config: HookConfig,
105}
106
107#[derive(Debug, Clone)]
108pub struct StageRequest {
109    pub manifest_dir: PathBuf,
110    pub capsule_name: String,
111    pub capsule_version: String,
112    pub capsule_component_bytes: Vec<u8>,
113    pub artifacts: Vec<ArtifactRequest>,
114    pub allowlisted_tools: HashSet<String>,
115    pub lock_expectations: Option<Vec<LockExpectation>>,
116    pub capability_policy: CapabilityPolicy,
117    pub inference: Option<InferenceConfig>,
118    pub context: Option<ContextConfig>,
119    /// OTLP/HTTP endpoint for span export; None = no external OTel emission.
120    pub otel_endpoint: Option<String>,
121    /// JSON-serialized EvalConfig injected into hook WASI env as MURMUR_EVAL_CONFIG.
122    pub eval_config_json: Option<String>,
123    /// Dataset case identifier injected as MURMUR_CASE_ID when running a dataset.
124    pub case_id: Option<String>,
125    /// Dataset identifier injected as MURMUR_DATASET_ID when running a dataset.
126    pub dataset_id: Option<String>,
127    /// Lifecycle config from the manifest (None = use LifecycleConfig::default()).
128    pub lifecycle: Option<LifecycleConfig>,
129    /// Optional override applied on top of lifecycle (e.g. from CLI flags or mur-roost).
130    pub lifecycle_override: Option<LifecycleOverride>,
131    /// Trace config from the manifest (None = defaults apply: input captured, output not).
132    pub trace: Option<murmur_artifact::TraceConfig>,
133    /// Optional user project directory to mount as the capsule's accessible workspace.
134    /// When set, session artifacts go into `<workdir>/.murmur/<session_id>/`.
135    /// When None, uses the default temp dir (existing behavior).
136    pub workdir: Option<std::path::PathBuf>,
137    /// Address to bind the HTTP server on. Defaults to "127.0.0.1" for local runs.
138    /// Use "0.0.0.0" when deploying to a VM so the capsule is reachable externally.
139    pub bind_addr: String,
140    /// Internal port from `network.internal_port` in the manifest. When `Some`, the runtime
141    /// binds strictly to this port and errors if it is already in use. When `None`, the OS
142    /// assigns a port.
143    pub internal_port: Option<u16>,
144    /// Job ID assigned by mur-roost for this capsule instance. Injected as MURMUR_JOB_ID so
145    /// spawned child capsules can set `spawned_by` when calling POST /spawn on mur-roost.
146    /// None when not launched via mur-roost (direct CLI, tests).
147    pub job_id: Option<String>,
148}
149
150pub struct StagedSession {
151    pub session_id: String,
152    pub workdir: PathBuf,
153    /// User project directory (or session dir if no --workdir). WASM tools see this at ".".
154    pub accessible_workdir: std::path::PathBuf,
155    pub(crate) manifest_dir: PathBuf,
156    pub(crate) capsule_name: String,
157    pub(crate) capsule_version: String,
158    pub(crate) capsule_url: String,
159    pub resolved_lock_artifacts: Vec<ResolvedLockArtifact>,
160    pub(crate) installed_artifacts: Vec<InstalledArtifactSummary>,
161    pub(crate) inference: Option<InferenceConfig>,
162    pub(crate) context: Option<ContextConfig>,
163    pub(crate) engine: Engine,
164    /// `None` for manifest-only agent capsules; `Some` for script capsules with a WASM component.
165    pub(crate) capsule_component: Option<Component>,
166    pub(crate) tool_components: HashMap<String, Component>,
167    pub(crate) hook_components: Vec<StagedHookArtifact>,
168    pub(crate) allowlisted_tools: HashSet<String>,
169    pub(crate) capability_policy: CapabilityPolicy,
170    pub(crate) otel_endpoint: Option<String>,
171    pub(crate) eval_config_json: Option<String>,
172    pub(crate) case_id: Option<String>,
173    pub(crate) dataset_id: Option<String>,
174    /// Resolved lifecycle config (manifest + override already applied).
175    pub(crate) lifecycle: LifecycleConfig,
176    /// Whether to capture tool output in trace events (resolved from manifest trace config).
177    pub(crate) trace_include_tool_output: bool,
178    /// Address the HTTP server is bound to (copied from StageRequest::bind_addr).
179    pub(crate) bind_addr: String,
180    /// Internal port from the manifest (copied from StageRequest::internal_port).
181    pub(crate) internal_port: Option<u16>,
182    /// Job ID from mur-roost (copied from StageRequest::job_id).
183    pub(crate) job_id: Option<String>,
184    /// Registry used to resolve this session's artifacts, retained so `manage.pull()` can
185    /// resolve additional artifacts at runtime after staging has completed.
186    pub(crate) registry: Arc<dyn Registry>,
187    /// Keeps this session's epoch ticker running: without it no epoch deadline set on any
188    /// store built from `engine` can ever fire. Held here (rather than detached) so ticking
189    /// stops when the session's `StagedSession` drops. `launch_session` moves the other
190    /// fields out one by one, which leaves this one in place until the session returns.
191    pub(crate) _epoch_ticker: EpochTicker,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct LaunchResult {
196    pub session_id: String,
197    pub workdir: PathBuf,
198}
199
200pub fn capability_policy_from_runtime_manifest(
201    manifest: &murmur_artifact::RuntimeManifest,
202) -> CapabilityPolicy {
203    let caps = manifest.capabilities.as_ref();
204    let network_allow = caps
205        .and_then(|c| c.network.as_ref())
206        .map(|network| network.allow.clone())
207        .unwrap_or_default();
208
209    let filesystem_scope = caps
210        .and_then(|c| c.filesystem.as_ref())
211        .and_then(|filesystem| filesystem.scope.clone());
212
213    let shell_allow = caps
214        .and_then(|c| c.shell.as_ref())
215        .map(|shell| shell.allow.clone())
216        .unwrap_or_default();
217    let shell_strip_env = caps
218        .and_then(|c| c.shell.as_ref())
219        .and_then(|shell| shell.strip_env.clone())
220        .unwrap_or_default();
221    let shell_baseline_env = caps
222        .and_then(|c| c.shell.as_ref())
223        .and_then(|shell| shell.baseline_env.clone())
224        .unwrap_or_default();
225
226    let spawn_allow = caps
227        .and_then(|c| c.spawn.as_ref())
228        .map(|spawn| spawn.allow.clone())
229        .unwrap_or_default();
230
231    let env_allow = caps
232        .and_then(|c| c.env.as_ref())
233        .map(|env| env.allow.clone())
234        .unwrap_or_default();
235
236    let limits = ExecutionLimits::resolve(caps.and_then(|c| c.limits.as_ref()));
237
238    CapabilityPolicy {
239        network_allow,
240        filesystem_scope,
241        shell_allow,
242        spawn_allow,
243        shell_strip_env,
244        shell_baseline_env,
245        env_allow,
246        limits,
247    }
248}