use std::sync::Mutex;
use ikigai_core::{
ActionSpec, ArgSpec, Description, FnEndpoint, Invocation, ReprType, Representation, Verb,
};
use crate::XSD_STRING;
pub const CAP_HOST_POSTURE: &str = "urn:cap:host:posture";
#[derive(Clone, Debug, Default)]
pub struct Posture {
pub door: String,
pub mounts: MountPosture,
pub clients: Vec<TrustedIdentity>,
pub surface: Option<String>,
pub authority: Option<String>,
pub reloads: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MountPosture {
Declined,
Composed(Vec<ComposedMount>),
}
impl Default for MountPosture {
fn default() -> Self {
MountPosture::Composed(Vec::new())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ComposedMount {
pub mode: &'static str,
pub prefix: String,
pub target: String,
pub cert_dir: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustedIdentity {
pub label: String,
pub fingerprint: String,
pub path: String,
}
static POSTURE: Mutex<Option<Posture>> = Mutex::new(None);
pub fn set_posture(posture: Posture) {
*POSTURE.lock().expect("posture lock") = Some(posture);
}
pub fn posture() -> Option<Posture> {
POSTURE.lock().expect("posture lock").clone()
}
pub const MOUNTS_DECLINED: &str =
"declined (--no-config-mounts) — the config home's `mount` lines are not read";
fn mount_line(mount: &ComposedMount) -> String {
let certs = match &mount.cert_dir {
Some(dir) => format!(" [certs {dir}]"),
None => String::new(),
};
format!(
"mount {} {} -> {}{certs}",
mount.mode, mount.prefix, mount.target
)
}
pub fn mount_lines(mounts: &MountPosture) -> Vec<String> {
match mounts {
MountPosture::Declined => vec![format!("mount {MOUNTS_DECLINED}")],
MountPosture::Composed(mounts) if mounts.is_empty() => {
vec!["mount none composed — no `mount` lines in the config home".to_string()]
}
MountPosture::Composed(mounts) => mounts.iter().map(mount_line).collect(),
}
}
pub fn client_lines(clients: &[TrustedIdentity]) -> Vec<String> {
let width = clients.iter().map(|c| c.label.len()).max().unwrap_or(0);
clients
.iter()
.map(|client| {
format!(
"client {label:width$} {fingerprint} {path}",
label = client.label,
fingerprint = client.fingerprint,
path = client.path
)
})
.collect()
}
const KEY: usize = 9;
const FRESHNESS_NOTE: &str = concat!(
" as of STARTUP — the door, the surface, the authority, the mounts (composed\n",
" wholesale) and the set of certificates that may connect at all were\n",
" decided once, when this process started, and cannot change while it\n",
" runs. Nothing here is a re-read of the config home: it is what THIS\n",
" PROCESS composed.\n",
);
fn posture_text(posture: Option<&Posture>) -> String {
let Some(posture) = posture else {
return UNRECORDED_TEXT.to_string();
};
let mut out = String::from("ikigai host posture\n");
let mut row = |key: &str, value: &str| {
out.push_str(&format!(" {key:KEY$} {value}\n"));
};
row("door", &posture.door);
if let Some(surface) = &posture.surface {
row("surface", surface);
}
if let Some(authority) = &posture.authority {
row("authority", authority);
}
for line in mount_lines(&posture.mounts) {
out.push_str(&format!(" {line}\n"));
}
for line in client_lines(&posture.clients) {
out.push_str(&format!(" {line}\n"));
}
if posture.clients.is_empty() {
out.push_str(" client none — this door does not authenticate clients by certificate\n");
}
out.push_str(FRESHNESS_NOTE);
if posture.reloads.is_empty() {
out.push_str(concat!(
" reloads nothing — this door re-reads no file while it runs, so the\n",
" line above holds whole.\n",
));
}
for reload in &posture.reloads {
out.push_str(&format!(" {:KEY$} {reload}\n", "reloads"));
}
out
}
const UNRECORDED_TEXT: &str = "\
ikigai host posture
unrecorded — the code that composed this kernel recorded no posture, so this process
cannot say what it composed. This is NOT `nothing was composed`: a door that composes no
mounts still records that. Reachable only from a kernel built outside the CLI's doors
(an embedding host, or a test).
";
fn literal(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
for c in text.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out
}
fn posture_turtle(posture: Option<&Posture>) -> String {
let mut out = String::from(
"@prefix ik: <https://ikigai-rs.dev/ns#> .\n\
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\n",
);
let Some(posture) = posture else {
out.push_str(
"<urn:host:posture> a ik:Posture ;\n \
rdfs:comment \"unrecorded: the code that composed this kernel recorded no \
posture. NOT the same as composing nothing.\" .\n",
);
return out;
};
out.push_str(&format!(
"<urn:host:posture> a ik:Posture ;\n \
ik:door \"{}\" ;\n \
ik:asOf \"startup\" ;\n \
rdfs:comment \"what THIS PROCESS composed when it started — not a re-read of the \
config home. Anything the door re-reads while running is named by ik:reloads.\"",
literal(&posture.door),
));
if let Some(surface) = &posture.surface {
out.push_str(&format!(" ;\n ik:surface \"{}\"", literal(surface)));
}
if let Some(authority) = &posture.authority {
out.push_str(&format!(" ;\n ik:authority \"{}\"", literal(authority)));
}
for reload in &posture.reloads {
out.push_str(&format!(" ;\n ik:reloads \"{}\"", literal(reload)));
}
let composed = match &posture.mounts {
MountPosture::Declined => {
out.push_str(" ;\n ik:mountPosture \"declined\"");
&[][..]
}
MountPosture::Composed(mounts) if mounts.is_empty() => {
out.push_str(" ;\n ik:mountPosture \"none\"");
&[][..]
}
MountPosture::Composed(mounts) => {
out.push_str(" ;\n ik:mountPosture \"composed\"");
mounts
}
};
for (n, _) in composed.iter().enumerate() {
out.push_str(&format!(
" ;\n ik:mount <urn:ikigai:host:posture:mount:{n}>"
));
}
for (n, _) in posture.clients.iter().enumerate() {
out.push_str(&format!(
" ;\n ik:trustedClient <urn:ikigai:host:posture:client:{n}>"
));
}
out.push_str(" .\n\n");
for (n, mount) in composed.iter().enumerate() {
out.push_str(&format!(
"<urn:ikigai:host:posture:mount:{n}> a ik:Mount ;\n \
ik:mountMode \"{}\" ;\n \
ik:mountPrefix \"{}\" ;\n \
ik:mountTarget \"{}\"",
literal(mount.mode),
literal(&mount.prefix),
literal(&mount.target),
));
if let Some(dir) = &mount.cert_dir {
out.push_str(&format!(" ;\n ik:certDir \"{}\"", literal(dir)));
}
out.push_str(" .\n\n");
}
for (n, client) in posture.clients.iter().enumerate() {
out.push_str(&format!(
"<urn:ikigai:host:posture:client:{n}> a ik:TrustedClient ;\n \
rdfs:label \"{}\" ;\n \
ik:fingerprint \"{}\" ;\n \
ik:path \"{}\" .\n\n",
literal(&client.label),
literal(&client.fingerprint),
literal(&client.path),
));
}
out
}
pub(crate) fn host_posture() -> FnEndpoint {
FnEndpoint::new("host-posture", |inv: &Invocation<'_>| {
if !inv.capability.allows(CAP_HOST_POSTURE) {
return Err(ikigai_core::Error::Denied(format!(
"reading this host's posture requires `{CAP_HOST_POSTURE}` — it reports \
filesystem and socket paths, which describe the machine"
)));
}
let turtle = inv
.inline_str("as")
.map(|v| v.contains("turtle"))
.unwrap_or(false);
let recorded = posture();
let (body, repr) = if turtle {
(posture_turtle(recorded.as_ref()), "text/turtle")
} else {
(posture_text(recorded.as_ref()), "text/plain")
};
Ok(Representation::new(ReprType::new(repr), body.into_bytes()))
})
.with_description(
Description::new("host-posture")
.title("Host posture")
.summary(
"what this process composed at startup: its mounts, the client certificates \
it trusts, its served surface and the authority a caller resolves under",
)
.verb(Verb::Source)
.action(
ActionSpec::new(Verb::Source)
.summary(
"the startup-composed configuration of THIS process — never a \
re-read of the config home",
)
.input(
ArgSpec::new("as")
.optional()
.class(XSD_STRING)
.one_of(["text/plain", "text/turtle"])
.default_value("text/plain")
.summary("the representation to return (default text/plain)"),
)
.output("text/plain")
.output("text/turtle")
.requires(CAP_HOST_POSTURE),
),
)
}
#[cfg(test)]
mod tests {
use super::*;
use ikigai_core::{Capability, Iri, Request};
fn a_mount(mode: &'static str, prefix: &str, target: &str) -> ComposedMount {
ComposedMount {
mode,
prefix: prefix.to_string(),
target: target.to_string(),
cert_dir: None,
}
}
fn a_posture() -> Posture {
Posture {
door: "quic://0.0.0.0:4433".to_string(),
mounts: MountPosture::Composed(vec![
a_mount("prefer", "urn:iki:store:", "/tmp/gonk.sock"),
ComposedMount {
cert_dir: Some("/tmp/quic-bug".to_string()),
..a_mount("alias", "urn:cal:", "quic://bug.local:4433")
},
]),
clients: vec![
TrustedIdentity {
label: "base".to_string(),
fingerprint: "bbaea49f3556e06f".to_string(),
path: "/tmp/quic/client.crt".to_string(),
},
TrustedIdentity {
label: "plasma".to_string(),
fingerprint: "afae4ee811856ef9".to_string(),
path: "/tmp/quic/clients/plasma.crt".to_string(),
},
],
surface: Some("host + fs".to_string()),
authority: Some("per-client workspaces".to_string()),
reloads: Vec::new(),
}
}
#[test]
fn the_three_mount_postures_read_differently() {
assert_eq!(
mount_lines(&MountPosture::Composed(Vec::new())),
vec!["mount none composed — no `mount` lines in the config home"]
);
assert_eq!(
mount_lines(&MountPosture::Declined),
vec![format!("mount {MOUNTS_DECLINED}")]
);
assert_eq!(
mount_lines(&MountPosture::Composed(vec![a_mount(
"prefer",
"urn:x:",
"/tmp/x.sock"
)])),
vec!["mount prefer urn:x: -> /tmp/x.sock"]
);
}
#[test]
fn a_mounts_own_cert_dir_is_on_its_line() {
let lines = mount_lines(&a_posture().mounts);
assert_eq!(
lines[1],
"mount alias urn:cal: -> quic://bug.local:4433 [certs /tmp/quic-bug]"
);
}
#[test]
fn the_text_face_names_the_items_and_dates_them() {
let text = posture_text(Some(&a_posture()));
assert!(text.contains("door quic://0.0.0.0:4433"), "{text}");
assert!(
text.contains("mount prefer urn:iki:store: -> /tmp/gonk.sock"),
"{text}"
);
assert!(
text.contains("client base bbaea49f3556e06f /tmp/quic/client.crt"),
"{text}"
);
assert!(text.contains("client plasma afae4ee811856ef9"), "{text}");
assert!(text.contains("as of STARTUP"), "{text}");
assert!(
text.contains("re-read of the config home"),
"the report must say it is the PROCESS's composition, not the file's: {text}"
);
}
#[test]
fn an_unrecorded_posture_refuses_to_guess() {
let text = posture_text(None);
assert!(text.contains("unrecorded"), "{text}");
assert!(
!text.contains("none composed"),
"an unrecorded posture must not read as an empty topology: {text}"
);
let turtle = posture_turtle(None);
assert!(turtle.contains("unrecorded"), "{turtle}");
assert!(!turtle.contains("ik:mountPosture"), "{turtle}");
}
#[test]
fn a_live_fact_is_named_rather_than_covered_by_the_startup_line() {
let frozen = posture_text(Some(&a_posture()));
assert!(frozen.contains("reloads nothing"), "{frozen}");
let live = posture_text(Some(&Posture {
reloads: vec!["clients.json — per connection".to_string()],
..a_posture()
}));
assert!(
live.contains("reloads clients.json — per connection"),
"{live}"
);
assert!(
!live.contains("reloads nothing"),
"a door with a live fact must not also claim it re-reads nothing: {live}"
);
let turtle = posture_turtle(Some(&Posture {
reloads: vec!["clients.json — per connection".to_string()],
..a_posture()
}));
assert!(
turtle.contains("ik:reloads \"clients.json — per connection\""),
"{turtle}"
);
}
#[test]
fn a_door_without_client_certs_says_so() {
let text = posture_text(Some(&Posture {
clients: Vec::new(),
..a_posture()
}));
assert!(
text.contains("client none — this door does not authenticate"),
"{text}"
);
}
#[test]
fn the_graph_face_is_skolemized_turtle() {
let turtle = posture_turtle(Some(&a_posture()));
assert!(
turtle.contains("<urn:host:posture> a ik:Posture"),
"{turtle}"
);
assert!(
turtle.contains("<urn:ikigai:host:posture:mount:0> a ik:Mount"),
"{turtle}"
);
assert!(
turtle.contains("<urn:ikigai:host:posture:client:1> a ik:TrustedClient"),
"{turtle}"
);
assert!(turtle.contains("ik:asOf \"startup\""), "{turtle}");
assert!(!turtle.contains("_:"), "no blank nodes: {turtle}");
assert!(turtle.contains("ik:certDir \"/tmp/quic-bug\""), "{turtle}");
}
#[test]
fn the_graph_face_distinguishes_declined_from_empty() {
let declined = posture_turtle(Some(&Posture {
mounts: MountPosture::Declined,
..a_posture()
}));
assert!(
declined.contains("ik:mountPosture \"declined\""),
"{declined}"
);
assert!(!declined.contains("ik:mount <"), "{declined}");
let empty = posture_turtle(Some(&Posture {
mounts: MountPosture::Composed(Vec::new()),
..a_posture()
}));
assert!(empty.contains("ik:mountPosture \"none\""), "{empty}");
}
#[test]
fn operator_supplied_strings_are_escaped_into_the_graph() {
let turtle = posture_turtle(Some(&Posture {
mounts: MountPosture::Composed(vec![a_mount(
"prefer",
"urn:x:",
"/tmp/he said \"hi\"\\here",
)]),
..a_posture()
}));
assert!(
turtle.contains("ik:mountTarget \"/tmp/he said \\\"hi\\\"\\\\here\""),
"{turtle}"
);
}
fn posture_kernel() -> ikigai_core::Kernel {
ikigai_core::Kernel::new(std::sync::Arc::new(
crate::EndpointSpace::new().bind(crate::Exact::new("urn:host:posture"), host_posture()),
))
}
fn read(kernel: &ikigai_core::Kernel, cap: Capability) -> ikigai_core::Result<Representation> {
futures::executor::block_on(kernel.issue(
Request::new(
Verb::Source,
Iri::parse("urn:host:posture".to_string()).expect("iri"),
),
&cap,
))
}
#[test]
fn the_public_capability_is_refused_and_inspect_is_not_enough() {
set_posture(a_posture());
let kernel = posture_kernel();
let denied = read(&kernel, Capability::scoped(Vec::<String>::new()))
.expect_err("the public capability must be refused");
assert!(
matches!(denied, ikigai_core::Error::Denied(_)),
"a public read must be Denied, not something else: {denied:?}"
);
read(&kernel, Capability::scoped(["urn:cap:kernel:inspect"]))
.expect_err("the manifold grant must not carry the disk layout");
let allowed = read(&kernel, Capability::scoped([CAP_HOST_POSTURE]))
.expect("the declared scope must be sufficient");
assert!(String::from_utf8_lossy(&allowed.bytes).contains("ikigai host posture"));
}
#[test]
fn the_posture_representation_is_not_cacheable() {
set_posture(a_posture());
let repr = read(&posture_kernel(), Capability::scoped([CAP_HOST_POSTURE]))
.expect("resolves under its declared scope");
assert_eq!(
repr.expiry,
ikigai_core::Expiry::Always,
"a process-state fact must expire immediately"
);
}
}