diff --git a/Cargo.lock b/Cargo.lock index 5cc561d..a857127 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,17 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -90,12 +101,34 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.11.0" @@ -212,6 +245,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + [[package]] name = "core-foundation" version = "0.9.4" @@ -283,6 +328,69 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -325,6 +433,32 @@ dependencies = [ "syn", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -373,6 +507,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -514,6 +654,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "guildhouse-did" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "ed25519-dalek", + "hex", + "multibase", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "url", +] + [[package]] name = "h2" version = "0.4.13" @@ -904,6 +1061,7 @@ version = "0.1.0" dependencies = [ "chrono", "dirs", + "guildhouse-did", "hex", "reqwest", "serde", @@ -956,6 +1114,17 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.8.0" @@ -980,6 +1149,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -1112,6 +1293,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1161,6 +1352,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1257,6 +1457,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1475,6 +1684,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.12" @@ -1497,6 +1715,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/README.md b/README.md index 953acbb..bf26ff0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # gsh -gsh — the GCAP governed shell. Human and machine modes. Chronicle-attributed execution. \ No newline at end of file +gsh — the GCAP governed shell. Human and machine modes. Chronicle-attributed execution. + +**Status (2026-04-28):** Active development. Design is mature ([DESIGN.md](DESIGN.md)). The architectural anchor is the shell type system (per [DESIGN-SHELL-ARCHITECTURE-2026-04-28.md](../DESIGN-SHELL-ARCHITECTURE-2026-04-28.md)); gsh is the canonical consumer of the type system, built on libgsh. \ No newline at end of file diff --git a/libgsh/Cargo.toml b/libgsh/Cargo.toml index 37a793f..ec215c2 100644 --- a/libgsh/Cargo.toml +++ b/libgsh/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true description = "Governed shell library — AC validation, CR building, corpus gate" [dependencies] +guildhouse-did = { path = "../../guildhouse-did" } serde = { workspace = true } serde_json = { workspace = true } reqwest = { workspace = true } diff --git a/libgsh/src/ac.rs b/libgsh/src/ac.rs index c214289..f27e1f8 100644 --- a/libgsh/src/ac.rs +++ b/libgsh/src/ac.rs @@ -1,5 +1,6 @@ //! Authorization Context validation (R-22, R-23, R-24). +use guildhouse_did::Did; use serde::Deserialize; use thiserror::Error; @@ -32,7 +33,7 @@ pub struct AcOperation { #[derive(Deserialize, Debug, Clone, Default)] pub struct AcPrincipal { #[serde(default)] - pub did: Option, + pub did: Option, #[serde(default)] pub display_name: Option, } diff --git a/libgsh/src/config.rs b/libgsh/src/config.rs index aafe9f2..7cc9b69 100644 --- a/libgsh/src/config.rs +++ b/libgsh/src/config.rs @@ -1,13 +1,15 @@ //! Configuration from environment variables. +use guildhouse_did::Did; + /// Consolidated gsh configuration from env vars. pub struct GshConfig { /// Pre-issued AC JSON string. pub ac: Option, /// GSAP broker URL. pub broker_url: Option, - /// Agent DID. - pub agent_did: Option, + /// Agent DID. None when `GSAP_AGENT_DID` is unset or doesn't parse as a DID. + pub agent_did: Option, /// Bearer auth token. pub token: Option, /// Corpus CID this session is authorized for. @@ -28,7 +30,9 @@ impl GshConfig { Self { ac: std::env::var("GSAP_AC").ok(), broker_url: std::env::var("GSAP_BROKER_URL").ok(), - agent_did: std::env::var("GSAP_AGENT_DID").ok(), + agent_did: std::env::var("GSAP_AGENT_DID") + .ok() + .and_then(|s| Did::parse(&s).ok()), token: std::env::var("GSAP_TOKEN").ok(), corpus_cid: std::env::var("GSAP_CORPUS_CID") .unwrap_or_else(|_| "sha256:ungoverned".into()), diff --git a/libgsh/src/cr.rs b/libgsh/src/cr.rs index fd6f2db..91e26ad 100644 --- a/libgsh/src/cr.rs +++ b/libgsh/src/cr.rs @@ -70,6 +70,13 @@ pub fn broker_url(base: &str, path: &str) -> String { pub fn post_cr(client: &Client, base: &str, ac_id: &str, outcome: &str) -> CrResult { let now = chrono::Utc::now().to_rfc3339(); let session_id = std::env::var("CHRONICLE_SESSION_ID").unwrap_or_default(); + // Phase 0 D1: FFC DID sourced from env (FFC_DID), not hardcoded. + // W3C colon form. Empty on error → null in payload. + let ffc_did = std::env::var("FFC_DID") + .ok() + .filter(|s| !s.is_empty()) + .and_then(|s| guildhouse_did::Did::parse(&s).ok()) + .map(|d| d.as_str().to_owned()); match client .post(broker_url(base, "governance/complete/")) @@ -85,7 +92,7 @@ pub fn post_cr(client: &Client, base: &str, ac_id: &str, outcome: &str) -> CrRes behavioral_attestation: CrAttestation { status: "unavailable".into(), }, - ffc: serde_json::json!({"did": "did:web:guildhouse.dev", "chronicle_session_id": session_id}), + ffc: serde_json::json!({"did": ffc_did, "chronicle_session_id": session_id}), signature: serde_json::json!({"value": "gsh"}), }) .send() diff --git a/libgsh/src/session.rs b/libgsh/src/session.rs index 7927556..da7515a 100644 --- a/libgsh/src/session.rs +++ b/libgsh/src/session.rs @@ -21,10 +21,14 @@ pub struct SessionState { impl SessionState { pub fn from_ac(ac: &AuthorizationContext, corpus_cid: &str) -> Self { + // Phase 0: AcPrincipal.did is now Did-typed. Render to canonical + // string for SessionState.principal (which stays String — it can + // also hold a Unix username in ungoverned mode, so a typed Did + // would force Option here and complicate the chain). let principal = ac .principal .as_ref() - .and_then(|p| p.did.clone()) + .and_then(|p| p.did.as_ref().map(|d| d.as_str().to_owned())) .or_else(|| { ac.principal .as_ref() @@ -114,16 +118,30 @@ fn whoami() -> String { } /// Derive a human-readable display name from a DID. -/// did:web:guildhouse.dev/user/tking → tking@guildhouse.dev -/// Fallback: return the full DID. +/// +/// Phase 0 D1: prefers the W3C-canonical colon form +/// (`did:web:host:user:tking → tking@host`). The legacy slash form +/// (`did:web:host/user/tking`) is also recognized as a transition +/// affordance so any stale stored DIDs render sensibly. Falls through +/// to the input string when no `did:web:` prefix is present. fn display_name_from_did(did: &str) -> String { - if let Some(rest) = did.strip_prefix("did:web:") { - let parts: Vec<&str> = rest.splitn(2, '/').collect(); - if parts.len() == 2 { - let domain = parts[0]; - let name = parts[1].rsplit('/').next().unwrap_or(parts[1]); - return format!("{}@{}", name, domain); + let Some(rest) = did.strip_prefix("did:web:") else { + return did.to_string(); + }; + + // Try colon form first: `host:seg1:...:segN` → name=segN, domain=host. + if let Some((domain, path)) = rest.split_once(':') { + if !path.is_empty() { + let name = path.rsplit(':').next().unwrap_or(path); + return format!("{name}@{domain}"); } } + + // Legacy slash form: `host/user/tking`. + if let Some((domain, path)) = rest.split_once('/') { + let name = path.rsplit('/').next().unwrap_or(path); + return format!("{name}@{domain}"); + } + did.to_string() }