| 09752034 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use super::{ui, Ctx}; |
| 6 |
use anyhow::{bail, Context, Result}; |
| 98a0fb9f Andy McCall ↶ |
7 |
use spectra_core::identity::Identity; |
| 09752034 Andy McCall ↶ |
8 |
use spectra_core::keys::KeyPair; |
| 9 |
use spectra_core::ledger::Ledger; |
| 10 |
use spectra_core::principals::{identity_payload, signed_event, Principals}; |
| 11 |
use std::path::{Path, PathBuf}; |
| 12 |
|
| 13 |
|
| 98a0fb9f Andy McCall ↶ |
14 |
|
| 09752034 Andy McCall ↶ |
15 |
pub fn owner_home(ctx: &Ctx, flag: Option<PathBuf>) -> PathBuf { |
| 98a0fb9f Andy McCall ↶ |
16 |
flag.or_else(|| ctx.config_owner_home()).unwrap_or_else(spectra_core::keys::default_home) |
| 09752034 Andy McCall ↶ |
17 |
} |
| 18 |
|
| 19 |
pub async fn init(ctx: &Ctx, owner_home_flag: Option<PathBuf>, name: Option<String>) -> Result<i32> { |
| 20 |
let data = &ctx.data; |
| 21 |
let db = data.join("spectra.db"); |
| 22 |
if db.exists() { |
| 23 |
bail!("{} already holds a Spectra (spectra identity show)", data.display()); |
| 24 |
} |
| 25 |
let home = owner_home(ctx, owner_home_flag); |
| 98a0fb9f Andy McCall ↶ |
26 |
KeyPair::load_home(&home).with_context(|| { |
| 27 |
format!("the owner signs with their own identity: none usable in {} (spectra identity create, or --owner-home)", home.display()) |
| 09752034 Andy McCall ↶ |
28 |
})?; |
| 29 |
std::fs::create_dir_all(data)?; |
| 30 |
#[cfg(unix)] |
| 31 |
{ |
| 32 |
use std::os::unix::fs::PermissionsExt; |
| 33 |
std::fs::set_permissions(data, std::fs::Permissions::from_mode(0o700))?; |
| 34 |
} |
| 35 |
let name = name.unwrap_or_else(hostname); |
| 36 |
let instance = KeyPair::instance(data, &name)?; |
| 98a0fb9f Andy McCall ↶ |
37 |
|
| 38 |
|
| 39 |
let owner = spectra_core::keys::delegate(&home, instance.key, None)?; |
| 09752034 Andy McCall ↶ |
40 |
let ledger = Ledger::open(&db, Some(instance.clone())).await?; |
| 5eb57578 Andy McCall ↶ |
41 |
let mut first = vec![ |
| 42 |
signed_event(&owner, "human", "identity.owner", identity_payload(&owner.identity)), |
| 43 |
signed_event(&instance, "system", "identity.instance", identity_payload(&instance.identity)), |
| 44 |
]; |
| 45 |
first.extend(spectra_core::conversation::default_channel_events()); |
| 46 |
ledger.append(first).await?; |
| 09752034 Andy McCall ↶ |
47 |
ledger.checkpoint().await?; |
| 48 |
let home_s = home.display().to_string(); |
| 49 |
std::fs::write( |
| 50 |
data.join("spectra.toml"), |
| 51 |
format!("# Spectra's configuration (spec §22.1).\n\n[owner]\nkey = \"{}\"\nhome = {}\n", owner.key.to_hex(), toml_str(&home_s)), |
| 52 |
)?; |
| 5d695d86 Andy McCall ↶ |
53 |
println!("{} {} {}", ui::done("Initialised"), ui::subject("Spectra"), ui::path(data.display())); |
| 54 |
println!("{}", ui::kv("owner", format!("{}{}{}", ui::author(&owner.identity.name), ui::sep(), ui::hash(owner.key.short(12))))); |
| 55 |
println!("{}", ui::kv("instance", format!("{}{}{}", name, ui::sep(), ui::hash(instance.key.short(12))))); |
| 56 |
println!("{}", ui::kv("ledger", format!("{}{}", ui::path(db.display()), ui::dim(" (verified with spectra admin verify)")))); |
| 09752034 Andy McCall ↶ |
57 |
println!(); |
| 58 |
println!( |
| 5d695d86 Andy McCall ↶ |
59 |
" {} {} {}", |
| 60 |
ui::note(), |
| 61 |
ui::dim("then add agents with"), |
| 62 |
ui::id("spectra agent add NAME --kind KIND --project PROJECT --home DIR") |
| 63 |
); |
| 09752034 Andy McCall ↶ |
64 |
Ok(0) |
| 65 |
} |
| 66 |
|
| 67 |
pub async fn show(ctx: &Ctx, json: bool) -> Result<i32> { |
| 67bfa06a Andy McCall ↶ |
68 |
if let Some(cfg) = ctx.remote()? { |
| 69 |
return show_remote(&cfg, json).await; |
| 70 |
} |
| 09752034 Andy McCall ↶ |
71 |
let ledger = Ledger::open_read_only(&ctx.db()?).await?; |
| 72 |
let p = Principals::load(&ledger).await?; |
| 73 |
let owner = p.owner().context("no owner is recorded")?; |
| 74 |
let instance = p.instance(); |
| 98a0fb9f Andy McCall ↶ |
75 |
let delegated = instance.is_some_and(|i| owner.delegates_to(&i.hash(), spectra_core::ids::now_ms())); |
| 09752034 Andy McCall ↶ |
76 |
if json { |
| 77 |
println!( |
| 78 |
"{}", |
| 79 |
serde_json::json!({ |
| 80 |
"owner": { "name": owner.name, "key": owner.hash().to_hex() }, |
| 81 |
"instance": instance.map(|i| serde_json::json!({ "name": i.name, "key": i.hash().to_hex(), "delegated": delegated })), |
| 82 |
}) |
| 83 |
); |
| 84 |
return Ok(0); |
| 85 |
} |
| 5d695d86 Andy McCall ↶ |
86 |
println!("{}", ui::section("Owner")); |
| 98a0fb9f Andy McCall ↶ |
87 |
println!("{}", ui::kv("name", ui::author(format!("{} <{}>", owner.name, owner.contact)))); |
| 5d695d86 Andy McCall ↶ |
88 |
println!("{}", ui::kv("key", ui::hash(owner.hash().to_hex()))); |
| 89 |
println!("{}", ui::kv("delegates", owner.delegates.len())); |
| 09752034 Andy McCall ↶ |
90 |
if let Some(i) = instance { |
| 5d695d86 Andy McCall ↶ |
91 |
println!("{}", ui::section("Instance")); |
| 92 |
println!("{}", ui::kv("name", &i.name)); |
| 93 |
println!("{}", ui::kv("key", ui::hash(i.hash().to_hex()))); |
| 94 |
if delegated { |
| 95 |
println!("{}", ui::kv("status", ui::ok("delegated by the owner: checkpoints are verifiably yours"))); |
| 96 |
} else { |
| 98a0fb9f Andy McCall ↶ |
97 |
println!("{}", ui::kv("status", ui::warn("not delegated by the owner's current identity"))); |
| 5d695d86 Andy McCall ↶ |
98 |
} |
| 09752034 Andy McCall ↶ |
99 |
} |
| 100 |
Ok(0) |
| 101 |
} |
| 102 |
|
| 103 |
|
| 104 |
|
| 105 |
pub async fn refresh(ctx: &Ctx, owner_home_flag: Option<PathBuf>) -> Result<i32> { |
| 571572f5 Andy McCall ↶ |
106 |
let home = owner_home(ctx, owner_home_flag); |
| 107 |
let w = ctx.owner_writer(&home).await?; |
| 67bfa06a Andy McCall ↶ |
108 |
let rotated = w.refresh_event(&home).await?; |
| 571572f5 Andy McCall ↶ |
109 |
let outcome = rotated.as_ref().map(|_| KeyPair::load_home(&home).map(|p| p.identity)).transpose()?; |
| 110 |
if let Some(e) = rotated { |
| 111 |
w.append(vec![e]).await?; |
| 112 |
} |
| 09752034 Andy McCall ↶ |
113 |
match outcome { |
| 5d695d86 Andy McCall ↶ |
114 |
Some(id) => println!( |
| 115 |
"{} {}{}{}", |
| 116 |
ui::done("Recorded"), |
| 117 |
ui::subject("the owner's current identity"), |
| 118 |
ui::sep(), |
| 119 |
ui::hash(id.hash().short(12)) |
| 120 |
), |
| 121 |
None => println!("{} {}", ui::done("Current"), ui::subject("the owner's identity has not moved")), |
| 09752034 Andy McCall ↶ |
122 |
} |
| 123 |
Ok(0) |
| 124 |
} |
| 125 |
|
| 571572f5 Andy McCall ↶ |
126 |
|
| 127 |
|
| 128 |
|
| 129 |
pub async fn refresh_event(ledger: &Ledger, home: &Path) -> Result<Option<spectra_core::event::NewEvent>> { |
| 09752034 Andy McCall ↶ |
130 |
let p = Principals::load(ledger).await?; |
| 131 |
let recorded = p.owner().context("no owner is recorded (spectra init)")?.clone(); |
| eca5af92 Andy McCall ↶ |
132 |
rotation(&recorded, home) |
| 133 |
} |
| 134 |
|
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
pub fn rotation(recorded: &Identity, home: &Path) -> Result<Option<spectra_core::event::NewEvent>> { |
| 09752034 Andy McCall ↶ |
139 |
let pair = KeyPair::load_home(home)?; |
| 140 |
if pair.key == recorded.hash() { |
| 141 |
return Ok(None); |
| 142 |
} |
| 143 |
let mut chain = Vec::new(); |
| 144 |
let mut at = pair.identity.clone(); |
| 145 |
while let Some(prev) = at.previous { |
| 146 |
if prev == recorded.hash() { |
| 147 |
break; |
| 148 |
} |
| 149 |
let file = home.join("keys").join(format!("{}.identity", prev.to_hex())); |
| 150 |
at = Identity::decode( |
| 151 |
&std::fs::read(&file).with_context(|| format!("{} is not a successor of the recorded owner", pair.identity.name))?, |
| 152 |
) |
| 153 |
.map_err(|e| anyhow::anyhow!("{}: {e}", file.display()))?; |
| 154 |
chain.push(hex::encode(at.encode())); |
| 155 |
if chain.len() > 1000 { |
| 156 |
bail!("the owner's identity chain is too long to follow"); |
| 157 |
} |
| 158 |
} |
| 159 |
if at.previous != Some(recorded.hash()) && pair.identity.previous != Some(recorded.hash()) { |
| 160 |
bail!("{} in {} is not a successor of the recorded owner {}", pair.identity.name, home.display(), recorded.hash().short(12)); |
| 161 |
} |
| 162 |
let mut payload = identity_payload(&pair.identity); |
| 163 |
payload["chain"] = chain.into(); |
| 571572f5 Andy McCall ↶ |
164 |
Ok(Some(signed_event(&pair, "human", "identity.rotated", payload))) |
| 165 |
} |
| 166 |
|
| 167 |
|
| 168 |
|
| 169 |
|
| 170 |
pub async fn recorded_owner(ledger: &Ledger, home: &Path) -> Result<KeyPair> { |
| 171 |
let p = Principals::load(ledger).await?; |
| 172 |
let recorded = p.owner().context("no owner is recorded (spectra init)")?.hash(); |
| 173 |
let keys = home.join("keys"); |
| 174 |
KeyPair::load(&keys.join(format!("{}.secret", recorded.to_hex())), &keys.join(format!("{}.identity", recorded.to_hex()))) |
| 175 |
.with_context(|| format!("the recorded owner's key is not in {}", home.display())) |
| 67bfa06a Andy McCall ↶ |
176 |
} |
| 177 |
|
| 178 |
|
| 179 |
async fn show_remote(cfg: &super::connect::ClientCfg, json: bool) -> Result<i32> { |
| 180 |
let info = super::call(&spectra_mcp::client::Client::at(cfg.endpoint()?, None), "GET", "/api/v1/instance", None).await?; |
| 181 |
let owner_key = info["owner"].as_str().unwrap_or("").to_string(); |
| 182 |
let instance_key = info["instance"]["key"].as_str().unwrap_or("").to_string(); |
| 98a0fb9f Andy McCall ↶ |
183 |
let home = spectra_core::keys::default_home(); |
| 67bfa06a Andy McCall ↶ |
184 |
let me = KeyPair::load_home(&home).ok(); |
| 185 |
let delegated = me.as_ref().is_some_and(|m| { |
| 98a0fb9f Andy McCall ↶ |
186 |
spectra_core::identity::KeyId::from_hex(&instance_key).is_some_and(|k| m.identity.delegates_to(&k, spectra_core::ids::now_ms())) |
| 67bfa06a Andy McCall ↶ |
187 |
}); |
| 188 |
if json { |
| 189 |
println!( |
| 190 |
"{}", |
| 191 |
serde_json::json!({ |
| 192 |
"server": cfg.server, |
| 193 |
"owner": { "name": me.as_ref().map(|m| m.identity.name.clone()), "key": owner_key }, |
| 194 |
"instance": { "name": info["name"], "key": instance_key, "delegated": delegated }, |
| 195 |
}) |
| 196 |
); |
| 197 |
return Ok(0); |
| 198 |
} |
| 199 |
println!("{}", ui::section("Server")); |
| 200 |
println!("{}", ui::kv("url", ui::path(&cfg.server))); |
| 201 |
println!("{}", ui::section("Owner")); |
| 202 |
if let Some(m) = &me { |
| 98a0fb9f Andy McCall ↶ |
203 |
println!("{}", ui::kv("name", ui::author(format!("{} <{}>", m.identity.name, m.identity.contact)))); |
| 67bfa06a Andy McCall ↶ |
204 |
} |
| 205 |
println!("{}", ui::kv("key", ui::hash(&owner_key))); |
| 206 |
println!("{}", ui::section("Instance")); |
| 207 |
println!("{}", ui::kv("name", info["name"].as_str().unwrap_or(""))); |
| 208 |
println!("{}", ui::kv("key", ui::hash(&instance_key))); |
| 209 |
if delegated { |
| 210 |
println!("{}", ui::kv("status", ui::ok("delegated by you: its checkpoints and certificate are verifiably yours"))); |
| 211 |
} else { |
| 212 |
println!("{}", ui::kv("status", ui::warn("not delegated by your current identity"))); |
| 213 |
} |
| 98a0fb9f Andy McCall ↶ |
214 |
Ok(0) |
| 215 |
} |
| 216 |
|
| 217 |
|
| 218 |
pub fn create(name: &str, contact: &str, home: Option<PathBuf>) -> Result<i32> { |
| 219 |
let home = home.unwrap_or_else(spectra_core::keys::default_home); |
| 220 |
let pair = KeyPair::create_home(&home, name.trim(), contact.trim())?; |
| 221 |
println!("{} {} {}", ui::done("Created"), ui::subject("your identity"), ui::path(home.display())); |
| 222 |
println!("{}", ui::kv("name", ui::author(&pair.identity.name))); |
| 223 |
println!("{}", ui::kv("key", ui::hash(pair.key.to_hex()))); |
| 224 |
println!(" {} {}", ui::note(), ui::dim("keep keys/ private: anyone with it can act as you")); |
| 225 |
println!( |
| 226 |
" {} {} {}", |
| 227 |
ui::note(), |
| 228 |
ui::dim("next: spectra init here, or on a server"), |
| 229 |
ui::id(format!("spectra init --owner {}", pair.key.to_hex())) |
| 230 |
); |
| 67bfa06a Andy McCall ↶ |
231 |
Ok(0) |
| 09752034 Andy McCall ↶ |
232 |
} |
| 233 |
|
| 77df591d Andy McCall ↶ |
234 |
|
| 235 |
|
| 236 |
|
| 237 |
pub async fn init_for(ctx: &Ctx, owner_key: &str, name: Option<String>) -> Result<i32> { |
| 238 |
let key = owner_key.trim().to_ascii_lowercase(); |
| 239 |
if key.len() != 64 || hex::decode(&key).is_err() { |
| 98a0fb9f Andy McCall ↶ |
240 |
bail!("{owner_key} is not a KeyId (64 hex digits; the owner's `spectra identity show` prints it)"); |
| 77df591d Andy McCall ↶ |
241 |
} |
| 242 |
let data = &ctx.data; |
| 243 |
let db = data.join("spectra.db"); |
| 244 |
if db.exists() { |
| 245 |
bail!("{} already holds a Spectra (spectra identity show)", data.display()); |
| 246 |
} |
| 247 |
std::fs::create_dir_all(data)?; |
| 248 |
#[cfg(unix)] |
| 249 |
{ |
| 250 |
use std::os::unix::fs::PermissionsExt; |
| 251 |
std::fs::set_permissions(data, std::fs::Permissions::from_mode(0o700))?; |
| 252 |
} |
| 253 |
let name = name.unwrap_or_else(hostname); |
| 254 |
let instance = KeyPair::instance(data, &name)?; |
| 255 |
let ledger = Ledger::open(&db, Some(instance.clone())).await?; |
| 256 |
let mut first = vec![signed_event(&instance, "system", "identity.instance", identity_payload(&instance.identity))]; |
| 257 |
first.extend(spectra_core::conversation::default_channel_events()); |
| 258 |
ledger.append(first).await?; |
| 259 |
ledger.checkpoint().await?; |
| 260 |
std::fs::write( |
| 261 |
data.join("spectra.toml"), |
| 262 |
format!( |
| 263 |
"# Spectra's configuration (spec §22.1).\n\n[owner]\nkey = \"{key}\" # claimed from the owner's machine with `spectra connect`\n\n[server]\n# bind = \"[::]:7350\" # serve other machines over TLS (spec §8.6)\n" |
| 264 |
), |
| 265 |
)?; |
| 266 |
println!("{} {} {}", ui::done("Initialised"), ui::subject("Spectra"), ui::path(data.display())); |
| 267 |
println!("{}", ui::kv("owner", format!("{}{}", ui::hash(&key[..12]), ui::dim(" (named; not yet claimed)")))); |
| 268 |
println!("{}", ui::kv("instance", format!("{}{}{}", name, ui::sep(), ui::hash(instance.key.short(12))))); |
| 269 |
println!(); |
| 270 |
println!(" {} {}", ui::note(), ui::dim("serve it to other machines: set [server] bind in spectra.toml, then spectra serve")); |
| 271 |
println!(" {} {}", ui::note(), ui::dim("then, on the owner's machine:")); |
| 272 |
println!(" {}", ui::id("spectra connect https://HOST:7350")); |
| 273 |
Ok(0) |
| 274 |
} |
| 275 |
|
| 09752034 Andy McCall ↶ |
276 |
fn hostname() -> String { |
| 277 |
std::fs::read_to_string("/etc/hostname") |
| 278 |
.ok() |
| 279 |
.map(|h| h.trim().to_string()) |
| 280 |
.filter(|h| !h.is_empty()) |
| 281 |
.unwrap_or_else(|| "spectra".into()) |
| 282 |
} |
| 283 |
|
| 284 |
fn toml_str(s: &str) -> String { |
| 285 |
serde_json::to_string(s).unwrap_or_default() |
| 286 |
} |