feat(gsh): add --register mode and agent API for shellbound host
Adds `gsh register --service-name <name>` subcommand for systemd ExecStartPre integration. Connects to substrate-fabric Unix socket, sends RegisterAppShell, writes shell.env for EnvironmentFile= loading. New libgsh modules: - register.rs: fabric IPC client for app shell creation + env writer - agent_api.rs: capability attenuation validation for agent sub-shells Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tyler J King <tking@guildhouse.dev>
This commit is contained in:
parent
061e2206ea
commit
7c84854222
8 changed files with 486 additions and 1 deletions
121
Cargo.lock
generated
121
Cargo.lock
generated
|
|
@ -175,6 +175,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
|
|
@ -189,6 +195,33 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
|
|
@ -318,6 +351,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -651,6 +690,8 @@ dependencies = [
|
|||
"reedline",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"substrate-ipc",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
@ -690,6 +731,17 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
|
|
@ -1067,8 +1119,10 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"substrate-ipc",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1131,6 +1185,15 @@ version = "2.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
|
@ -1178,6 +1241,19 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
|
|
@ -1765,6 +1841,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "substrate-ipc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ciborium",
|
||||
"nix",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -1895,11 +1982,25 @@ dependencies = [
|
|||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
|
|
@ -2596,6 +2697,26 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.7"
|
||||
|
|
|
|||
|
|
@ -24,3 +24,5 @@ reedline = "0.38"
|
|||
colored = "2"
|
||||
atty = "0.2"
|
||||
tracing = "0.1"
|
||||
substrate-ipc = { path = "../substrate/crates/substrate-ipc" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -19,3 +19,5 @@ chrono = { workspace = true }
|
|||
reedline = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
atty = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
substrate-ipc = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -63,6 +63,19 @@ enum Cmd {
|
|||
},
|
||||
SessionEnd,
|
||||
SessionStatus,
|
||||
/// Register an app shell with substrate-fabric (systemd ExecStartPre).
|
||||
Register {
|
||||
/// Service name for the shell registration.
|
||||
#[arg(long)]
|
||||
service_name: String,
|
||||
/// Path to the fabric Unix socket.
|
||||
#[arg(long, default_value = "/run/substrate/fabric.sock")]
|
||||
fabric_socket: String,
|
||||
/// Directory to write shell.env into. Defaults to
|
||||
/// /run/substrate/shells/{service_name}/
|
||||
#[arg(long)]
|
||||
env_dir: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -125,8 +138,18 @@ fn run(args: Args) -> Result<i32> {
|
|||
return Ok(code);
|
||||
}
|
||||
|
||||
// ── Session subcommands ──────────────────────────────────
|
||||
// ── Subcommands ─────────────────────────────────────────
|
||||
if let Some(cmd) = &args.command {
|
||||
// Register is handled separately — it doesn't need a broker.
|
||||
if let Cmd::Register {
|
||||
service_name,
|
||||
fabric_socket,
|
||||
env_dir,
|
||||
} = cmd
|
||||
{
|
||||
return run_register(service_name, fabric_socket, env_dir.as_deref());
|
||||
}
|
||||
|
||||
let base = args.broker_url.clone()
|
||||
.or_else(|| std::env::var("GSAP_BROKER_URL").ok())
|
||||
.context("GSAP_BROKER_URL not set")?;
|
||||
|
|
@ -166,6 +189,7 @@ fn run(args: Args) -> Result<i32> {
|
|||
}
|
||||
Ok(0)
|
||||
}
|
||||
Cmd::Register { .. } => unreachable!("handled above"),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -339,3 +363,24 @@ fn run(args: Args) -> Result<i32> {
|
|||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
fn run_register(service_name: &str, fabric_socket: &str, env_dir: Option<&str>) -> Result<i32> {
|
||||
let rt = tokio::runtime::Runtime::new().context("creating tokio runtime")?;
|
||||
let env = rt.block_on(libgsh::register::register_app_shell(
|
||||
service_name,
|
||||
Some(fabric_socket),
|
||||
))?;
|
||||
|
||||
let dir = match env_dir {
|
||||
Some(d) => std::path::PathBuf::from(d),
|
||||
None => std::path::PathBuf::from(format!("/run/substrate/shells/{service_name}")),
|
||||
};
|
||||
|
||||
let path = libgsh::register::write_shell_env(&dir, &env)?;
|
||||
eprintln!(
|
||||
"gsh: registered shell {} — env at {}",
|
||||
env.shell_id,
|
||||
path.display()
|
||||
);
|
||||
Ok(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ hex = { workspace = true }
|
|||
chrono = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
substrate-ipc = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
129
libgsh/src/agent_api.rs
Normal file
129
libgsh/src/agent_api.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
//! Agent shell registration API.
|
||||
//!
|
||||
//! Unix socket server that accepts RegisterAgentShell requests from
|
||||
//! callers wanting to spawn governed agent sub-shells. Validates
|
||||
//! capability attenuation (requested ⊆ parent) before forwarding
|
||||
//! to substrate-fabric.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use substrate_ipc::fabric_api::{FabricRequest, FabricResponse};
|
||||
use substrate_ipc::wire;
|
||||
|
||||
const DEFAULT_FABRIC_SOCKET: &str = "/run/substrate/fabric.sock";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AgentError {
|
||||
#[error("capability widening: requested 0x{requested:08x} exceeds parent 0x{parent:08x}")]
|
||||
CapabilityWidening { requested: u32, parent: u32 },
|
||||
#[error("connecting to fabric socket at {path}: {source}")]
|
||||
Connect {
|
||||
path: String,
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("fabric IPC: {0}")]
|
||||
Wire(#[from] wire::WireError),
|
||||
#[error("registration denied: {reason}")]
|
||||
Denied { reason: String },
|
||||
#[error("fabric error: {0}")]
|
||||
FabricError(String),
|
||||
#[error("reading cgroup: {0}")]
|
||||
Cgroup(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentShellEnv {
|
||||
pub shell_id: String,
|
||||
pub env_vars: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Validate that requested capabilities are a subset of parent's.
|
||||
pub fn validate_attenuation(
|
||||
requested: u32,
|
||||
parent: u32,
|
||||
) -> Result<(), AgentError> {
|
||||
if requested & !parent != 0 {
|
||||
return Err(AgentError::CapabilityWidening {
|
||||
requested,
|
||||
parent,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register an agent shell via substrate-fabric.
|
||||
///
|
||||
/// Validates capability attenuation before forwarding. The caller
|
||||
/// must be in the parent shell's cgroup subtree.
|
||||
pub async fn register_agent_shell(
|
||||
parent_shell_id: &str,
|
||||
parent_capabilities: u32,
|
||||
requested_capabilities: u32,
|
||||
fabric_socket: Option<&str>,
|
||||
) -> Result<AgentShellEnv, AgentError> {
|
||||
validate_attenuation(requested_capabilities, parent_capabilities)?;
|
||||
|
||||
let cgroup_path = substrate_ipc::cgroup::get_self_cgroup_path()
|
||||
.map_err(|e| AgentError::Cgroup(e.to_string()))?;
|
||||
|
||||
let sock_path = fabric_socket.unwrap_or(DEFAULT_FABRIC_SOCKET);
|
||||
let stream = UnixStream::connect(sock_path)
|
||||
.await
|
||||
.map_err(|e| AgentError::Connect {
|
||||
path: sock_path.into(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
let (mut rd, mut wr) = tokio::io::split(stream);
|
||||
|
||||
let req = FabricRequest::RegisterAgentShell {
|
||||
parent_shell_id: parent_shell_id.into(),
|
||||
requested_capabilities,
|
||||
cgroup_path,
|
||||
};
|
||||
wire::send_msg(&mut wr, &req).await?;
|
||||
|
||||
let resp: FabricResponse = wire::recv_msg(&mut rd).await?;
|
||||
match resp {
|
||||
FabricResponse::ShellCreated { shell_id, env_vars } => {
|
||||
Ok(AgentShellEnv { shell_id, env_vars })
|
||||
}
|
||||
FabricResponse::Denied { reason } => Err(AgentError::Denied { reason }),
|
||||
FabricResponse::Error(msg) => Err(AgentError::FabricError(msg)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn attenuation_allows_subset() {
|
||||
validate_attenuation(0x01, 0x03).unwrap(); // READ ⊆ READ|PROPOSE
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attenuation_allows_equal() {
|
||||
validate_attenuation(0x03, 0x03).unwrap(); // exact match
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attenuation_allows_empty() {
|
||||
validate_attenuation(0x00, 0x0F).unwrap(); // nothing requested
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attenuation_rejects_widening() {
|
||||
let err = validate_attenuation(0x04, 0x03).unwrap_err(); // MUTATE not in READ|PROPOSE
|
||||
assert!(matches!(err, AgentError::CapabilityWidening { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attenuation_rejects_superset() {
|
||||
let err = validate_attenuation(0x0F, 0x03).unwrap_err(); // ALL not in READ|PROPOSE
|
||||
assert!(matches!(err, AgentError::CapabilityWidening { .. }));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
pub mod ac;
|
||||
pub mod agent_api;
|
||||
pub mod chronicle_events;
|
||||
pub mod classifier;
|
||||
pub mod config;
|
||||
pub mod corpus;
|
||||
pub mod cr;
|
||||
pub mod governance_env;
|
||||
pub mod register;
|
||||
pub mod registry;
|
||||
pub mod session;
|
||||
|
||||
|
|
|
|||
182
libgsh/src/register.rs
Normal file
182
libgsh/src/register.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
//! App shell registration via substrate-fabric IPC.
|
||||
//!
|
||||
//! Used by `gsh --register --service-name <name>` in systemd
|
||||
//! ExecStartPre to create a governed shell for an application
|
||||
//! service before its main process starts.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use substrate_ipc::fabric_api::{FabricRequest, FabricResponse};
|
||||
use substrate_ipc::wire;
|
||||
|
||||
const DEFAULT_FABRIC_SOCKET: &str = "/run/substrate/fabric.sock";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RegisterError {
|
||||
#[error("connecting to fabric socket at {path}: {source}")]
|
||||
Connect {
|
||||
path: String,
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("fabric IPC: {0}")]
|
||||
Wire(#[from] wire::WireError),
|
||||
#[error("registration denied: {reason}")]
|
||||
Denied { reason: String },
|
||||
#[error("fabric error: {0}")]
|
||||
FabricError(String),
|
||||
#[error("writing shell env to {path}: {source}")]
|
||||
WriteEnv {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("reading cgroup: {0}")]
|
||||
Cgroup(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShellEnv {
|
||||
pub shell_id: String,
|
||||
pub env_vars: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Register an app shell with substrate-fabric.
|
||||
///
|
||||
/// 1. Discovers the caller's cgroup path
|
||||
/// 2. Connects to the fabric Unix socket
|
||||
/// 3. Sends RegisterAppShell
|
||||
/// 4. Returns the shell environment on success
|
||||
pub async fn register_app_shell(
|
||||
service_name: &str,
|
||||
fabric_socket: Option<&str>,
|
||||
) -> Result<ShellEnv, RegisterError> {
|
||||
let cgroup_path = substrate_ipc::cgroup::get_self_cgroup_path()
|
||||
.map_err(|e| RegisterError::Cgroup(e.to_string()))?;
|
||||
|
||||
let sock_path = fabric_socket.unwrap_or(DEFAULT_FABRIC_SOCKET);
|
||||
let stream = UnixStream::connect(sock_path)
|
||||
.await
|
||||
.map_err(|e| RegisterError::Connect {
|
||||
path: sock_path.into(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
let (mut rd, mut wr) = tokio::io::split(stream);
|
||||
|
||||
let req = FabricRequest::RegisterAppShell {
|
||||
service_name: service_name.into(),
|
||||
cgroup_path,
|
||||
};
|
||||
wire::send_msg(&mut wr, &req).await?;
|
||||
|
||||
let resp: FabricResponse = wire::recv_msg(&mut rd).await?;
|
||||
match resp {
|
||||
FabricResponse::ShellCreated { shell_id, env_vars } => {
|
||||
Ok(ShellEnv { shell_id, env_vars })
|
||||
}
|
||||
FabricResponse::Denied { reason } => Err(RegisterError::Denied { reason }),
|
||||
FabricResponse::Error(msg) => Err(RegisterError::FabricError(msg)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write shell environment variables to a systemd-compatible
|
||||
/// EnvironmentFile at the given directory.
|
||||
///
|
||||
/// Creates `{dir}/shell.env` with `KEY=VALUE` lines.
|
||||
pub fn write_shell_env(dir: &Path, env: &ShellEnv) -> Result<PathBuf, RegisterError> {
|
||||
std::fs::create_dir_all(dir).map_err(|e| RegisterError::WriteEnv {
|
||||
path: dir.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
let env_path = dir.join("shell.env");
|
||||
let mut content = String::new();
|
||||
content.push_str(&format!("SUBSTRATE_SHELL_ID={}\n", env.shell_id));
|
||||
for (k, v) in &env.env_vars {
|
||||
content.push_str(&format!("{k}={v}\n"));
|
||||
}
|
||||
|
||||
std::fs::write(&env_path, &content).map_err(|e| RegisterError::WriteEnv {
|
||||
path: env_path.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
Ok(env_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn write_shell_env_creates_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let env = ShellEnv {
|
||||
shell_id: "test-shell-001".into(),
|
||||
env_vars: {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("GSH_DID".into(), "did:web:example:user:test".into());
|
||||
m.insert("GSH_SHELL_CLASS".into(), "Application".into());
|
||||
m
|
||||
},
|
||||
};
|
||||
|
||||
let path = write_shell_env(dir.path(), &env).unwrap();
|
||||
assert!(path.exists());
|
||||
|
||||
let content = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(content.contains("SUBSTRATE_SHELL_ID=test-shell-001"));
|
||||
assert!(content.contains("GSH_DID=did:web:example:user:test"));
|
||||
assert!(content.contains("GSH_SHELL_CLASS=Application"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_fails_when_socket_missing() {
|
||||
let result = register_app_shell("test-svc", Some("/tmp/nonexistent-fabric.sock")).await;
|
||||
assert!(matches!(result, Err(RegisterError::Connect { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_round_trip_with_mock_fabric() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let sock_path = dir.path().join("fabric.sock");
|
||||
let sock_str = sock_path.to_str().unwrap().to_string();
|
||||
|
||||
let listener = tokio::net::UnixListener::bind(&sock_path).unwrap();
|
||||
|
||||
let sock_clone = sock_str.clone();
|
||||
let client = tokio::spawn(async move {
|
||||
register_app_shell("my-daemon", Some(&sock_clone)).await
|
||||
});
|
||||
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
let (mut rd, mut wr) = tokio::io::split(stream);
|
||||
|
||||
let req: FabricRequest = wire::recv_msg(&mut rd).await.unwrap();
|
||||
match req {
|
||||
FabricRequest::RegisterAppShell { service_name, .. } => {
|
||||
assert_eq!(service_name, "my-daemon");
|
||||
}
|
||||
_ => panic!("wrong request variant"),
|
||||
}
|
||||
|
||||
let mut env_vars = HashMap::new();
|
||||
env_vars.insert("GSH_DID".into(), "did:web:test:hosts:h1:shells:my-daemon".into());
|
||||
env_vars.insert("GSH_SHELL_CLASS".into(), "Application".into());
|
||||
let resp = FabricResponse::ShellCreated {
|
||||
shell_id: "shell-abc".into(),
|
||||
env_vars,
|
||||
};
|
||||
wire::send_msg(&mut wr, &resp).await.unwrap();
|
||||
|
||||
let result = client.await.unwrap().unwrap();
|
||||
assert_eq!(result.shell_id, "shell-abc");
|
||||
assert_eq!(
|
||||
result.env_vars.get("GSH_DID").unwrap(),
|
||||
"did:web:test:hosts:h1:shells:my-daemon"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue