use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use wasmtime::Engine;
use crate::audit::Transport;
use crate::consent::CurrentConsentSink;
use crate::info::ComponentInfo;
use crate::resolve::ComponentRef;
use crate::{ComponentHandle, Metadata};
pub struct ComponentRuntime {
engine: Engine,
}
#[derive(Default)]
pub struct RuntimeConfig {
pub grants: act_policy::grant::GrantPolicy,
pub metadata: Metadata,
pub max_memory: Option<usize>,
pub audit: AuditOptions,
pub credentials: Option<CredentialsSource>,
}
#[derive(Default)]
pub struct AuditOptions {
pub transport: Transport,
pub record_args: bool,
}
pub struct CredentialsSource {
pub backend: Option<String>,
pub refresher: Option<Arc<dyn crate::credentials::CredentialRefresher>>,
}
pub struct ConsentConfig {
pub prompter: Arc<dyn act_policy::consent::ConsentPrompter>,
pub has_prompt_channel: bool,
pub sink: Arc<CurrentConsentSink>,
pub cache: Arc<act_policy::consent::DecisionCache>,
}
impl ConsentConfig {
pub fn deny() -> Self {
Self {
prompter: Arc::new(act_policy::consent::DenyPrompter),
has_prompt_channel: false,
sink: Arc::new(CurrentConsentSink::new()),
cache: Arc::new(act_policy::consent::DecisionCache::new()),
}
}
}
pub struct RunningComponent {
info: ComponentInfo,
handle: ComponentHandle,
has_sessions: bool,
path: PathBuf,
}
impl ComponentRuntime {
pub fn new() -> Result<Self> {
Ok(Self {
engine: crate::create_engine()?,
})
}
pub fn engine(&self) -> &Engine {
&self.engine
}
pub async fn load(
&self,
component: &ComponentRef,
config: &RuntimeConfig,
consent: ConsentConfig,
) -> Result<RunningComponent> {
let path = crate::resolve::resolve(component, false).await?;
let wasm_bytes = std::fs::read(&path).context("reading component file")?;
let info = crate::read_component_info(&wasm_bytes)?;
let fs_mode = config
.grants
.resolve(act_types::constants::CAP_FILESYSTEM)
.mode;
let mounts = crate::fs_policy::resolve_mounts(&info.std.capabilities, fs_mode);
crate::fs_policy::create_mount_dirs(&mounts).context("creating mount directories")?;
let preopens = crate::fs_policy::derive_preopens(&mounts);
tracing::debug!(
name = %info.std.name,
version = %info.std.version,
path = %path.display(),
"Loading component"
);
let (wasm, digest) = crate::load_component(&self.engine, &path)?;
let linker = crate::create_linker(&self.engine)?;
let audit = crate::AuditContext {
component_ref: component.to_string(),
digest,
transport: config.audit.transport,
has_prompt_channel: consent.has_prompt_channel,
record_args: config.audit.record_args,
};
let credentials = match &config.credentials {
Some(source) => credential_host(
component,
source.backend.as_deref(),
source.refresher.clone(),
)?,
None => None,
};
let (instance, session_provider, store) = crate::instantiate_component(
&self.engine,
&wasm,
&linker,
&preopens,
&config.grants,
&info,
config.max_memory,
consent.prompter,
consent.cache,
credentials,
&audit,
)
.await?;
let has_sessions = session_provider.is_some();
let handle =
crate::spawn_component_actor(instance, session_provider, store, consent.sink, audit);
tracing::debug!(name = %info.std.name, version = %info.std.version, "Component ready");
Ok(RunningComponent {
info,
handle,
has_sessions,
path,
})
}
}
impl RunningComponent {
pub fn info(&self) -> &ComponentInfo {
&self.info
}
pub fn has_sessions(&self) -> bool {
self.has_sessions
}
pub fn path(&self) -> &std::path::Path {
&self.path
}
pub fn handle(&self) -> &ComponentHandle {
&self.handle
}
}
fn credential_host(
component: &ComponentRef,
backend: Option<&str>,
refresher: Option<Arc<dyn crate::credentials::CredentialRefresher>>,
) -> Result<Option<Arc<crate::credentials::CredentialHost>>> {
let component_ref = crate::resolve::profile_key(component);
let Some(choice) = crate::credentials::resolve_backend(backend)? else {
return Ok(None);
};
let root = crate::credentials::backend_root(&choice).to_path_buf();
let store = act_credentials::backend::select(choice, &root)
.with_context(|| format!("opening credential store at {}", root.display()))?;
let host = crate::credentials::CredentialHost::new(Arc::from(store), component_ref);
Ok(Some(Arc::new(match refresher {
Some(r) => host.with_refresher(r),
None => host,
})))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_runtime_reads_the_profile_the_writer_wrote() {
let dir = tempfile::tempdir().unwrap();
let backend = format!("file:{}", dir.path().display());
let cwd = std::env::current_dir().unwrap();
for spelling in ["./x.wasm", "x.wasm"] {
let component: ComponentRef = spelling.parse().unwrap();
let host = credential_host(&component, Some(&backend), None)
.unwrap()
.expect("an explicitly named backend always yields a host");
assert_eq!(
host.component(),
cwd.join("x.wasm").display().to_string(),
"{spelling} must reach the same profile as every other spelling"
);
}
}
}