Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions shell/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ name = "shell"
path = "src/main.rs"

[dependencies]
iii-sdk = "=0.19.1-next.1"
iii-observability = "=0.19.1-next.1"
iii-sdk = "=0.20.0"
iii-helpers = "=0.20.0"
schemars = { version = "0.8", features = ["uuid1"] }
libc = "0.2"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "process", "time", "io-util", "fs"] }
Expand Down
34 changes: 20 additions & 14 deletions shell/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use std::sync::Arc;
use std::time::Duration;

use iii_sdk::{IIIError, RegisterFunction, RegisterTriggerInput, TriggerRequest, III};
use iii_sdk::errors::Error;
use iii_sdk::protocol::{RegisterTriggerInput, TriggerRequest};
use iii_sdk::{IIIClient, RegisterFunction};
use serde_json::{json, Value};
use tokio::sync::{Mutex, RwLock};

Expand All @@ -32,7 +34,7 @@ pub struct ShellRuntime {
#[derive(Clone)]
pub struct AppState {
pub runtime: Arc<RwLock<ShellRuntime>>,
pub iii: III,
pub iii: IIIClient,
/// Serializes hot-reloads: held across the authoritative fetch + build + swap
/// so an older event's slow build can never clobber a newer applied config.
pub reload_lock: Arc<Mutex<()>>,
Expand Down Expand Up @@ -102,7 +104,7 @@ pub fn prepare_config(cfg: &ShellConfig) -> Result<Arc<ShellConfig>, String> {
}

/// Build the live runtime: validate the config, then build the host fs backend.
pub fn build_runtime(cfg: &ShellConfig, iii: &III) -> Result<ShellRuntime, String> {
pub fn build_runtime(cfg: &ShellConfig, iii: &IIIClient) -> Result<ShellRuntime, String> {
let config = prepare_config(cfg)?;
if config.fs.host_root.is_none() {
tracing::warn!(
Expand Down Expand Up @@ -137,7 +139,7 @@ pub fn build_runtime(cfg: &ShellConfig, iii: &III) -> Result<ShellRuntime, Strin
/// cannot seed `ShellConfig::default()`: it is intentionally unjailed/invalid,
/// so the built-in seed is `ShellConfig::seed_default()`, a bootable permissive
/// dev default.
pub async fn register_config(iii: &III, seed: Option<&ShellConfig>) -> Result<(), String> {
pub async fn register_config(iii: &IIIClient, seed: Option<&ShellConfig>) -> Result<(), String> {
let mut payload = json!({
"id": CONFIG_ID,
"name": "Shell",
Expand Down Expand Up @@ -170,7 +172,7 @@ pub async fn register_config(iii: &III, seed: Option<&ShellConfig>) -> Result<()

/// Seed the built-in default only when nothing is stored yet — never overwrite
/// an operator's persisted value.
async fn should_seed_default_value(iii: &III) -> Result<bool, String> {
async fn should_seed_default_value(iii: &IIIClient) -> Result<bool, String> {
match try_get_config_value(iii).await? {
None => Ok(true),
Some(value) if value.is_null() => Ok(true),
Expand All @@ -179,7 +181,7 @@ async fn should_seed_default_value(iii: &III) -> Result<bool, String> {
}

/// Read the live `shell` configuration (env-expanded by the configuration worker).
pub async fn fetch_config(iii: &III) -> Result<ShellConfig, String> {
pub async fn fetch_config(iii: &IIIClient) -> Result<ShellConfig, String> {
let value = get_config_value(iii).await?;
if value.is_null() {
// Null means register_config did not seed (its seed_default failed
Expand All @@ -199,18 +201,18 @@ pub async fn fetch_config(iii: &III) -> Result<ShellConfig, String> {
ShellConfig::from_json(&value)
}

async fn get_config_value(iii: &III) -> Result<Value, String> {
async fn get_config_value(iii: &IIIClient) -> Result<Value, String> {
try_get_config_value(iii)
.await?
.ok_or_else(|| format!("configuration `{CONFIG_ID}` not found"))
}

async fn try_get_config_value(iii: &III) -> Result<Option<Value>, String> {
async fn try_get_config_value(iii: &IIIClient) -> Result<Option<Value>, String> {
match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await {
Ok(resp) => Ok(resp.get("value").cloned()),
// `trigger_with_retry` flattens the structured `IIIError` to its
// `trigger_with_retry` flattens the structured `Error` to its
// Display string, so we substring-match the recovered message rather
// than branch on `IIIError::Remote { code }`. The engine's missing-entry
// than branch on `Error::Remote { code }`. The engine's missing-entry
// codes vary in case (`function_not_found`, `STATEMENT_NOT_FOUND`,
// `NOT_FOUND`), so uppercase before matching to catch them all. A
// false negative is non-fatal — it just propagates the raw retry error
Expand Down Expand Up @@ -243,15 +245,15 @@ async fn apply_config(state: &AppState, cfg: ShellConfig) -> Result<(), String>
}

/// Register the internal config-change handler and bind a `configuration` trigger.
pub fn register_config_trigger(iii: &III, state: AppState) -> Result<(), IIIError> {
pub fn register_config_trigger(iii: &IIIClient, state: AppState) -> Result<(), Error> {
let st = state.clone();
iii.register_function(
CONFIG_FN_ID,
RegisterFunction::new_async(move |_payload: Value| {
let st = st.clone();
async move {
on_config_change(&st).await.map_err(IIIError::from)?;
Ok::<Value, IIIError>(json!({ "ok": true }))
on_config_change(&st).await.map_err(Error::from)?;
Ok::<Value, Error>(json!({ "ok": true }))
}
})
.description("Internal: reload the security policy + fs backend on configuration change."),
Expand Down Expand Up @@ -363,7 +365,11 @@ where
}
}

async fn trigger_with_retry(iii: &III, function_id: &str, payload: Value) -> Result<Value, String> {
async fn trigger_with_retry(
iii: &IIIClient,
function_id: &str,
payload: Value,
) -> Result<Value, String> {
let mut last_err = String::new();
for attempt in 1..=CONFIG_RETRIES {
match iii
Expand Down
20 changes: 10 additions & 10 deletions shell/src/exec/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ impl ExecError {
/// (OOM only); `expect` so future shape changes fail loudly rather
/// than producing malformed JSON.
///
/// The handler-return path lifts `ExecError` to `IIIError::Remote` directly
/// (see `From<ExecError> for IIIError` below), so it no longer stringifies.
/// The handler-return path lifts `ExecError` to `Error::Remote` directly
/// (see `From<ExecError> for Error` below), so it no longer stringifies.
/// `to_json` is kept as the canonical `{code,message}` serialization
/// (round-trip coverage in tests) and for any caller that needs the wire
/// shape as a `String`.
Expand All @@ -35,13 +35,13 @@ impl ExecError {
}

/// Carry the S-code to the wire as the top-level `code`. The engine SDK maps
/// `IIIError::Remote { code, message, .. }` to the wire `ErrorBody` verbatim,
/// so an agent can branch on `error.code` (e.g. "S211"). Any other `IIIError`
/// `Error::Remote { code, message, .. }` to the wire `ErrorBody` verbatim,
/// so an agent can branch on `error.code` (e.g. "S211"). Any other `Error`
/// variant collapses to `code: "invocation_failed"` with the real code buried
/// in the message — which is exactly what we are escaping here.
impl From<ExecError> for iii_sdk::IIIError {
impl From<ExecError> for iii_sdk::errors::Error {
fn from(err: ExecError) -> Self {
iii_sdk::IIIError::Remote {
iii_sdk::errors::Error::Remote {
code: err.code.to_string(),
message: err.message,
stacktrace: None,
Expand All @@ -67,15 +67,15 @@ mod tests {
assert_ne!(ExecError::new("S210", "x"), ExecError::new("S211", "x"),);
}

/// The wire contract: `ExecError` lifts to `IIIError::Remote { code, .. }`
/// The wire contract: `ExecError` lifts to `Error::Remote { code, .. }`
/// so the S-code reaches the wire `code` verbatim. Any other variant (e.g.
/// Handler) would collapse to `code: "invocation_failed"` — pin against that
/// regression so an agent can keep branching on `error.code`.
#[test]
fn converts_to_iii_remote_carrying_the_s_code() {
let err: iii_sdk::IIIError = ExecError::new("S216", "host exec: boom").into();
let err: iii_sdk::errors::Error = ExecError::new("S216", "host exec: boom").into();
match err {
iii_sdk::IIIError::Remote {
iii_sdk::errors::Error::Remote {
code,
message,
stacktrace,
Expand All @@ -84,7 +84,7 @@ mod tests {
assert_eq!(message, "host exec: boom");
assert!(stacktrace.is_none());
}
other => panic!("expected IIIError::Remote, got {other:?}"),
other => panic!("expected Error::Remote, got {other:?}"),
}
}
}
12 changes: 6 additions & 6 deletions shell/src/exec/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,16 @@ fn is_engine_timeout(err: &ExecError) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use iii_sdk::IIIError;
use iii_sdk::errors::Error;
use serde_json::Value;
use std::sync::Mutex;

/// Stub forwarder using `Mutex<Option<...>>` to handle the
/// non-Clone `IIIError` shape. Same pattern as
/// non-Clone `Error` shape. Same pattern as
/// `tests/sandbox_dispatch.rs::StubFwd`.
struct StubFwd {
captured: Mutex<Option<(String, Value)>>,
next: Mutex<Option<Result<Value, IIIError>>>,
next: Mutex<Option<Result<Value, Error>>>,
}

impl StubFwd {
Expand All @@ -162,13 +162,13 @@ mod tests {
fn handler_err(json_msg: &'static str) -> Arc<Self> {
Arc::new(Self {
captured: Mutex::new(None),
next: Mutex::new(Some(Err(IIIError::Handler(json_msg.to_string())))),
next: Mutex::new(Some(Err(Error::Handler(json_msg.to_string())))),
})
}
fn remote_err(code: &str, message: &str) -> Arc<Self> {
Arc::new(Self {
captured: Mutex::new(None),
next: Mutex::new(Some(Err(IIIError::Remote {
next: Mutex::new(Some(Err(Error::Remote {
code: code.into(),
message: message.into(),
stacktrace: None,
Expand All @@ -179,7 +179,7 @@ mod tests {

#[async_trait]
impl TriggerFwd for StubFwd {
async fn trigger(&self, fid: &str, payload: Value) -> Result<Value, IIIError> {
async fn trigger(&self, fid: &str, payload: Value) -> Result<Value, Error> {
*self.captured.lock().unwrap() = Some((fid.into(), payload));
self.next
.lock()
Expand Down
8 changes: 4 additions & 4 deletions shell/src/exec_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ use crate::triggers::IiiTriggerFwd;
pub fn pick_exec_backend(
target: Target,
cfg: Arc<ShellConfig>,
iii: iii_sdk::III,
iii: iii_sdk::IIIClient,
) -> Arc<dyn ExecBackend> {
match target {
Target::Host => Arc::new(HostExecBackend::new(cfg)),
Target::Sandbox { sandbox_id } => sandbox_for(sandbox_id, iii, cfg.sandbox.enabled),
}
}

fn sandbox_for(id: Uuid, iii: iii_sdk::III, enabled: bool) -> Arc<dyn ExecBackend> {
fn sandbox_for(id: Uuid, iii: iii_sdk::IIIClient, enabled: bool) -> Arc<dyn ExecBackend> {
Arc::new(SandboxExecBackend::new(
Arc::new(IiiTriggerFwd::new(iii)),
enabled,
Expand All @@ -32,6 +32,6 @@ fn sandbox_for(id: Uuid, iii: iii_sdk::III, enabled: bool) -> Arc<dyn ExecBacken
}

// No unit tests in this module: `pick_exec_backend`'s match arms are trivial
// constructors that would require a real `iii_sdk::III` to exercise. The
// constructors that would require a real `iii_sdk::IIIClient` to exercise. The
// ExecError -> wire-code conversion is covered by
// `exec::error::tests` (the `From<ExecError> for IIIError` Remote lift).
// `exec::error::tests` (the `From<ExecError> for Error` Remote lift).
20 changes: 10 additions & 10 deletions shell/src/fs/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ impl FsError {
/// effectively infallible (OOM only); `expect` so future changes that
/// break the invariant fail loudly instead of producing malformed JSON.
///
/// The handler-return path lifts `FsError` to `IIIError::Remote` directly
/// (see `From<FsError> for IIIError` below), so it no longer stringifies.
/// The handler-return path lifts `FsError` to `Error::Remote` directly
/// (see `From<FsError> for Error` below), so it no longer stringifies.
/// `to_json` is kept as the canonical `{code,message}` serialization
/// (round-trip coverage in tests) and for any caller that needs the wire
/// shape as a `String`.
Expand All @@ -49,13 +49,13 @@ impl FsError {
}

/// Carry the S2xx code to the wire as the top-level `code`. The engine SDK
/// maps `IIIError::Remote { code, message, .. }` to the wire `ErrorBody`
/// maps `Error::Remote { code, message, .. }` to the wire `ErrorBody`
/// verbatim, so an agent can branch on `error.code` (e.g. "S211"). Any other
/// `IIIError` variant collapses to `code: "invocation_failed"` with the real
/// `Error` variant collapses to `code: "invocation_failed"` with the real
/// code buried in the message — which is exactly what we are escaping here.
impl From<FsError> for iii_sdk::IIIError {
impl From<FsError> for iii_sdk::errors::Error {
fn from(err: FsError) -> Self {
iii_sdk::IIIError::Remote {
iii_sdk::errors::Error::Remote {
code: err.code.to_string(),
message: err.message,
stacktrace: None,
Expand Down Expand Up @@ -118,15 +118,15 @@ mod tests {
assert!(j.contains("\"message\":\"nope\""));
}

/// The wire contract: `FsError` lifts to `IIIError::Remote { code, .. }` so
/// The wire contract: `FsError` lifts to `Error::Remote { code, .. }` so
/// the S-code reaches the wire `code` verbatim. Any other variant (e.g.
/// Handler) would collapse to `code: "invocation_failed"` — pin against that
/// regression so an agent can keep branching on `error.code`.
#[test]
fn converts_to_iii_remote_carrying_the_s_code() {
let err: iii_sdk::IIIError = FsError::new("S215", "denied").into();
let err: iii_sdk::errors::Error = FsError::new("S215", "denied").into();
match err {
iii_sdk::IIIError::Remote {
iii_sdk::errors::Error::Remote {
code,
message,
stacktrace,
Expand All @@ -135,7 +135,7 @@ mod tests {
assert_eq!(message, "denied");
assert!(stacktrace.is_none());
}
other => panic!("expected IIIError::Remote, got {other:?}"),
other => panic!("expected Error::Remote, got {other:?}"),
}
}
}
Loading
Loading