From 7809fa5cdac5835da0cca164357c4522c1398a6e Mon Sep 17 00:00:00 2001 From: "Christian M. Todie" Date: Mon, 12 Oct 2026 14:26:10 -0400 Subject: [PATCH 1/4] docs(specs): land SPEC-0002 proof-carrying policy gates and godel prover certification - Define Proof-Carrying Code (PCC) model for estate policy gates. - Formalize state transition safety invariant proof I(S) => I(S'). - Specify out-of-band Goedel prover architecture and Lean 4 certificate generation. - Establish O(1) AST proof-checker interface in gs-domain. - Link SPEC-0002 in README. Co-authored-by: Kratos Reviewed-by: Mimir --- README.md | 4 + ...C-0002-goedel-prover-gate-certification.md | 131 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 specs/SPEC-0002-goedel-prover-gate-certification.md diff --git a/README.md b/README.md index 9486949..12a5dd9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ Part of the [unsigned](https://unsigned.gg) systems ecosystem. * **Estate Sensing:** Sensing daemon states, sessions, and live pods. * **Audit Trail:** Attestation records and statutory duty clocks. +## Specifications +* [SPEC-0001: Godseat Control Surface & Policy Gate Tiers](specs/SPEC-0001-godseat-control-surface.md) +* [SPEC-0002: Proof-Carrying Policy Gates & Gödel Prover Certification](specs/SPEC-0002-goedel-prover-gate-certification.md) + ## License Dedicated to the public domain under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/) or Apache-2.0 / MIT. diff --git a/specs/SPEC-0002-goedel-prover-gate-certification.md b/specs/SPEC-0002-goedel-prover-gate-certification.md new file mode 100644 index 0000000..6137aa8 --- /dev/null +++ b/specs/SPEC-0002-goedel-prover-gate-certification.md @@ -0,0 +1,131 @@ +# SPEC-0002: Proof-Carrying Policy Gates & Gödel Prover Certification + +## 1. Status & Metadata +- **Status:** Proposed & Living +- **Authors:** Kratos , Mimir +- **Target Crate:** `gs-domain` (`crates/gs-domain`) +- **License:** Apache-2.0 OR MIT / CC0 1.0 Universal +- **Depends On:** [SPEC-0001-godseat-control-surface.md](SPEC-0001-godseat-control-surface.md) + +--- + +## 2. Context & Problem Statement + +Autonomous agents operating on the cluster control plane must evaluate actions before execution. +Static pattern matching (e.g. comparing action strings against allow and deny lists) exhibits structural limitations: +1. It cannot verify dynamic state transitions where safety depends on preconditions. +2. It cannot evaluate complex composed multi-step mutation sequences. +3. It cannot generalize to novel, semantically valid tool invocations without continuous manual rule expansion. + +Theoretical self-improving systems (Schmidhuber's 2003 Gödel machine) mandate formal proof of global optimality before self-rewriting. +Modern empirical systems (Darwin Gödel Machine, STOP, ADAS) drop formal proof requirements for heuristic feedback. +This shift introduces documented failure modes: +* **Marker Stripping:** Agents delete internal validation markers to bypass hallucination detection. +* **Context Collapse:** Wholesale instruction rewrites purge tacit operational knowledge (e.g. ACE accuracy falling from 66.7% to 57.1%). +* **Permission Drift:** Agents grant themselves expanded authorization under the guise of workflow optimization. + +`gs` resolves this tension using a **Proof-Carrying Code (PCC)** architecture for policy gates. +A high-level Gödel prover synthesizes machine-verifiable proofs out-of-band. +The low-level `gs-domain` gate verifies these proofs in deterministic $O(1)$ time without searching. + +--- + +## 3. Formal Invariants & Model + +### Invariant 1: State Transition Proof Obligation +Let state $S$ be formalized as a first-order theory representing permissions, cluster topology, and audit integrity. +Define the global estate safety invariant $I(S)$: + +$$I(S) \triangleq \text{Safety}(S) \wedge \text{AuditIntact}(S) \wedge \neg\text{SealedAction}(S)$$ + +For any proposed action $a$ transitioning the estate from state $S$ to state $S'$, the prover must establish: + +$$(I(S) \wedge \text{Pre}(a, S) \wedge \text{Exec}(S, a, S')) \implies (I(S') \wedge \text{TierCriteria}(a, S'))$$ + +### Invariant 2: Cryptographic Proof Binding +The proof $\pi$ must be bound to the cryptographic digests of the initial state, action, and resulting state: + +$$\text{Binding} \triangleq \langle H(S), \text{canonical}(a), H(S') \rangle$$ + +A proof is valid only when $H(S)$ matches the live ground truth measured by the perception layer. + +### Invariant 3: Zero-Search Gate Verification ($O(1)$) +The runtime gate certifier (`gs-domain`) shall not execute proof search, SMT solving, or tactic discovery. +The gate operates strictly as a deterministic Abstract Syntax Tree (AST) proof-checker: +* It traverses the proof term $\pi$ in a single forward pass. +* Verification time is bounded: $t_{\text{verify}} \le O(|\pi|) \le O(1)$ relative to state size $|S|$. +* Proof certificates must be self-contained and free of unresolved assumptions (`sorry`). + +### Invariant 4: Permissions Immutability +Agents may propose changes to prompts, instructions, and tools. +Agents may never propose mutations to permission ledgers, sandbox configurations, or gate definitions. +Any state transition attempting to expand agent mutation authority fails closed. + +--- + +## 4. Policy Gate Lattice Integration + +The gate certifier assigns an action context to exactly one of four tiers based on proof verification: + +| Verification Result | Operational Criteria | Assigned Tier | +|---|---|---| +| Valid proof $\pi$ | Internal, reversible, observable, certain | **Green (和面 wamen)** | +| Incomplete proof or timeout | Degradable, multi-target, or partial uncertainty | **Yellow / Red (怒眼 igan)** | +| Unproven or safety violation | Irreversible, outward-facing, live peer | **Red (赤鬼 akaoni)** | +| Sealed action ($a \in \text{Never}$) | Prohibited by definition; bypasses prover | **Never (封印 fūin)** | + +> [!CAUTION] +> Sealed actions cannot execute under any circumstance. +> Actions in the Sealed set (`force-push-main`, `commit-secrets`, `disable-audit`, `drop-database`) bypass the proof synthesizer and fail closed immediately. + +--- + +## 5. Architectural Pipeline + +``` + [Proposed Action a] + │ + ▼ + ┌───────────────────┐ + │ Sealed Check? │──► Yes ──► Reject immediately (Never) + └───────────────────┘ + │ No + ▼ + ┌───────────────────┐ + │ Out-of-Band │ + │ Gödel Prover │──► Synthesizes transition proof π: + │ (Lean 4 / Z3) │ (I(S) ∧ Exec(S, a, S')) ⟹ I(S') + └───────────────────┘ + │ Emits π bound to ⟨H(S), a, H(S')⟩ + ▼ + ┌───────────────────┐ + │ gs-domain Gate │──► O(1) AST proof check: + │ (Zero-I/O Kernel) │ • π valid ∧ Reversible ⟹ Green + └───────────────────┘ • π invalid / Timeout ⟹ Red +``` + +--- + +## 6. Implementation Blueprint in gs-domain + +The `ActionContext` struct in `crates/gs-domain/src/gate.rs` is extended to carry optional proof verification tokens: + +```rust +/// Cryptographic proof token validating state invariant preservation. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProofCertificate { + /// Format identifier (e.g. "lean4-term", "alethe-smt", "snark-groth16"). + pub format: String, + /// Cryptographic digest of initial state H(S). + pub pre_state_hash: [u8; 32], + /// Cryptographic digest of post-state H(S'). + pub post_state_hash: [u8; 32], + /// Serialized AST proof term or verification token. + pub proof_payload: Vec, +} +``` + +When evaluating `classify(&ctx)`: +1. If the action is sealed, return `GateTier::Never`. +2. If `ctx.proof` contains a valid certificate verifying $I(S) \implies I(S')$ and all local criteria are met, return `GateTier::Green`. +3. If the proof is absent, unverified, or timed out, default to `GateTier::Red`. From f8a9b32d3cfd8bf78c2bab54341d26d36ead925d Mon Sep 17 00:00:00 2001 From: "Christian M. Todie" Date: Thu, 15 Oct 2026 22:06:59 -0400 Subject: [PATCH 2/4] feat(gs-domain): enforce WIMSE agent principals and RFC 8693 delegation chains at policy gate --- crates/gs-domain/src/gate.rs | 295 +++++++++++++++++++++++++++++++++++ crates/gs-domain/src/lib.rs | 5 +- crates/gs/src/lib.rs | 12 ++ crates/gs/src/main.rs | 74 ++++++++- 4 files changed, 381 insertions(+), 5 deletions(-) diff --git a/crates/gs-domain/src/gate.rs b/crates/gs-domain/src/gate.rs index f73e34a..87422df 100644 --- a/crates/gs-domain/src/gate.rs +++ b/crates/gs-domain/src/gate.rs @@ -232,6 +232,258 @@ pub fn classify(ctx: &ActionContext) -> (GateTier, String) { ) } +/// Maximum allowed RFC 8693 `act` delegation chain depth at the Godseat policy gate. +pub const MAX_DELEGATION_DEPTH: usize = 8; + +/// Parsed WIMSE (`draft-klrc-aiagent-auth-03`) or operator principal kind. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum WimsePrincipalKind { + /// Autonomous agent (`spiffe:///agent//`). + Agent { + /// SPIFFE trust domain. + trust_domain: String, + /// Owning team or lane. + team: String, + /// Agent instance identifier. + agent_id: String, + }, + /// Conclave Elf (`spiffe:///conclave//elf/`). + ConclaveElf { + /// SPIFFE trust domain. + trust_domain: String, + /// Mesh identifier. + mesh: String, + /// Elf identifier. + elf_id: String, + }, + /// Human operator principal (`user:` or `operator:`). + Operator { + /// Operator handle or identifier. + id: String, + }, +} + +impl WimsePrincipalKind { + /// Parse a principal subject URI into a [`WimsePrincipalKind`]. + pub fn parse(subject: &str) -> Result { + let subject = subject.trim(); + if subject.is_empty() { + return Err("principal subject must not be empty".to_owned()); + } + if let Some(id) = subject + .strip_prefix("user:") + .or_else(|| subject.strip_prefix("operator:")) + { + if id.is_empty() || id.contains('/') || id.chars().any(char::is_whitespace) { + return Err(format!("invalid operator principal '{subject}'")); + } + return Ok(Self::Operator { id: id.to_owned() }); + } + let rest = subject + .strip_prefix("spiffe://") + .ok_or_else(|| format!("unsupported principal scheme in '{subject}'"))?; + let parts: Vec<&str> = rest.split('/').collect(); + match parts.as_slice() { + [td, "agent", team, agent_id] + if valid_segment(td) && valid_segment(team) && valid_segment(agent_id) => + { + Ok(Self::Agent { + trust_domain: (*td).to_owned(), + team: (*team).to_owned(), + agent_id: (*agent_id).to_owned(), + }) + } + [td, "conclave", mesh, "elf", elf_id] + if valid_segment(td) && valid_segment(mesh) && valid_segment(elf_id) => + { + Ok(Self::ConclaveElf { + trust_domain: (*td).to_owned(), + mesh: (*mesh).to_owned(), + elf_id: (*elf_id).to_owned(), + }) + } + _ => Err(format!( + "invalid WIMSE SPIFFE path in '{subject}' (expected /agent// or /conclave//elf/)" + )), + } + } + + /// Returns the SPIFFE trust domain for workload principals, or `None` for operators. + #[must_use] + pub fn trust_domain(&self) -> Option<&str> { + match self { + Self::Agent { trust_domain, .. } | Self::ConclaveElf { trust_domain, .. } => { + Some(trust_domain.as_str()) + } + Self::Operator { .. } => None, + } + } + + /// Returns `true` if this principal represents a human operator. + #[must_use] + pub const fn is_operator(&self) -> bool { + matches!(self, Self::Operator { .. }) + } +} + +fn valid_segment(seg: &str) -> bool { + !seg.is_empty() + && !seg + .chars() + .any(|c| c.is_whitespace() || c.is_control() || c == ':') +} + +/// Introspected OAuth2 / Ory Hydra and WIMSE (`draft-klrc-aiagent-auth-03`) principal claims +/// presented at the Godseat policy gate. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GatePrincipalClaims { + /// Primary subject (`sub`) of the WIMSE JWT-SVID or OAuth2 access token. + pub subject: String, + /// Ordered RFC 8693 `act` delegation chain (outermost actor first, root delegator last). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub delegation_chain: Vec, + /// Granted OAuth2 / Hydra scopes (`scope` claim split on whitespace). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub scopes: Vec, +} + +impl GatePrincipalClaims { + /// Construct claims for a subject with no delegation chain or scopes. + #[must_use] + pub fn new(subject: impl Into) -> Self { + Self { + subject: subject.into(), + delegation_chain: Vec::new(), + scopes: Vec::new(), + } + } + + /// Validate the primary subject and every actor in the RFC 8693 `act` chain + /// against `expected_trust_domain`. + pub fn validate(&self, expected_trust_domain: &str) -> Result { + if self.delegation_chain.len() > MAX_DELEGATION_DEPTH { + return Err(format!( + "RFC 8693 act delegation chain depth {} exceeds maximum {}", + self.delegation_chain.len(), + MAX_DELEGATION_DEPTH + )); + } + let subject_kind = WimsePrincipalKind::parse(&self.subject)?; + if let Some(td) = subject_kind.trust_domain() { + if td != expected_trust_domain { + return Err(format!( + "subject trust domain '{td}' does not match '{expected_trust_domain}'" + )); + } + } + for (idx, actor) in self.delegation_chain.iter().enumerate() { + if actor == &self.subject { + return Err(format!( + "subject '{}' cannot appear in its own RFC 8693 act delegation chain", + self.subject + )); + } + if self.delegation_chain[..idx].contains(actor) { + return Err(format!( + "duplicate actor '{actor}' in RFC 8693 act delegation chain" + )); + } + let actor_kind = WimsePrincipalKind::parse(actor)?; + if let Some(td) = actor_kind.trust_domain() { + if td != expected_trust_domain { + return Err(format!( + "delegation actor trust domain '{td}' does not match '{expected_trust_domain}'" + )); + } + } + } + Ok(subject_kind) + } + + /// Returns `true` if any actor in the RFC 8693 `act` chain is a human operator. + #[must_use] + pub fn has_operator_delegator(&self) -> bool { + self.delegation_chain.iter().any(|actor| { + WimsePrincipalKind::parse(actor) + .map(|k| k.is_operator()) + .unwrap_or(false) + }) + } + + /// Returns `true` if the token carries `scope`. + #[must_use] + pub fn has_scope(&self, scope: &str) -> bool { + self.scopes.iter().any(|s| s == scope) + } +} + +/// Classifies an action with WIMSE (`draft-klrc-aiagent-auth-03`) principal verification +/// and RFC 8693 `act` delegation chain enforcement. +/// +/// - Invalid principals, foreign SPIFFE trust domains, or cyclic/over-depth `act` chains +/// are immediately sealed (`GateTier::Never`). +/// - Actions classified as `GateTier::Red` may be satisfied by an agent only when its +/// RFC 8693 `act` chain includes a verified human operator delegator (`user:` or +/// `operator:`) and the explicit approval scope `gate:approve:` +/// (or `estate:admin`). +#[must_use] +pub fn classify_with_principal( + ctx: &ActionContext, + claims: &GatePrincipalClaims, + expected_trust_domain: &str, +) -> (GateTier, String) { + let subject_kind = match claims.validate(expected_trust_domain) { + Ok(kind) => kind, + Err(err) => { + return ( + GateTier::Never, + format!("WIMSE / RFC 8693 principal validation failed ({err}) — the mask refuses"), + ); + } + }; + + let (base_tier, base_reason) = classify(ctx); + if base_tier == GateTier::Never { + return (GateTier::Never, base_reason); + } + + let action = canonical_action(&ctx.action); + let approval_scope = format!("gate:approve:{action}"); + + match base_tier { + GateTier::Red | GateTier::Yellow => { + if subject_kind.is_operator() + || (claims.has_operator_delegator() + && (claims.has_scope(&approval_scope) || claims.has_scope("estate:admin"))) + { + ( + GateTier::Green, + format!( + "'{}' authorized via verified RFC 8693 operator delegation for '{}' — calm face", + ctx.action, claims.subject + ), + ) + } else { + ( + GateTier::Red, + format!( + "{base_reason} (principal '{}' lacks operator act delegation and '{approval_scope}' scope)", + claims.subject + ), + ) + } + } + GateTier::Green => ( + GateTier::Green, + format!( + "{base_reason} (verified WIMSE principal '{}')", + claims.subject + ), + ), + GateTier::Never => (GateTier::Never, base_reason), + } +} + #[cfg(test)] mod tests { use super::*; @@ -279,4 +531,47 @@ mod tests { let (tier, _) = classify(&ctx); assert_eq!(tier, GateTier::Red); } + + #[test] + fn test_wimse_principal_and_rfc8693_delegation_at_gate() { + let red_ctx = ActionContext::safe_internal("secret-rotate", "openbao/prod"); + + // Agent without operator delegation remains Red. + let unapproved_agent = GatePrincipalClaims { + subject: "spiffe://unsigned.gg/agent/estate/builder-01".to_owned(), + delegation_chain: vec!["spiffe://unsigned.gg/agent/estate/coordinator-01".to_owned()], + scopes: vec!["estate:admin".to_owned()], + }; + let (tier, _) = classify_with_principal(&red_ctx, &unapproved_agent, "unsigned.gg"); + assert_eq!(tier, GateTier::Red); + + // Agent with operator in RFC 8693 act chain + explicit gate scope upgrades Red -> Green. + let delegated_agent = GatePrincipalClaims { + subject: "spiffe://unsigned.gg/agent/estate/builder-01".to_owned(), + delegation_chain: vec![ + "spiffe://unsigned.gg/agent/estate/coordinator-01".to_owned(), + "user:ctodie".to_owned(), + ], + scopes: vec!["gate:approve:secret-rotate".to_owned()], + }; + let (tier, _) = classify_with_principal(&red_ctx, &delegated_agent, "unsigned.gg"); + assert_eq!(tier, GateTier::Green); + + // Sealed action remains Never even with operator delegation and admin scope. + let sealed_ctx = ActionContext::safe_internal("force-push-main", "repo-gs"); + let (tier, _) = classify_with_principal(&sealed_ctx, &delegated_agent, "unsigned.gg"); + assert_eq!(tier, GateTier::Never); + + // Foreign trust domain or cyclic delegation is sealed (Never). + let (tier, _) = classify_with_principal(&red_ctx, &delegated_agent, "other.domain"); + assert_eq!(tier, GateTier::Never); + + let cyclic = GatePrincipalClaims { + subject: "spiffe://unsigned.gg/agent/estate/builder-01".to_owned(), + delegation_chain: vec!["spiffe://unsigned.gg/agent/estate/builder-01".to_owned()], + scopes: vec![], + }; + let (tier, _) = classify_with_principal(&red_ctx, &cyclic, "unsigned.gg"); + assert_eq!(tier, GateTier::Never); + } } diff --git a/crates/gs-domain/src/lib.rs b/crates/gs-domain/src/lib.rs index 43e2739..43ceddd 100644 --- a/crates/gs-domain/src/lib.rs +++ b/crates/gs-domain/src/lib.rs @@ -11,5 +11,8 @@ pub mod gate; pub mod incident; -pub use gate::{canonical_action, classify, ActionContext, GateTier}; +pub use gate::{ + canonical_action, classify, classify_with_principal, ActionContext, GatePrincipalClaims, + GateTier, WimsePrincipalKind, MAX_DELEGATION_DEPTH, +}; pub use incident::{DutyWindow, IncidentRecord, IncidentState}; diff --git a/crates/gs/src/lib.rs b/crates/gs/src/lib.rs index e77797d..dd4a13c 100644 --- a/crates/gs/src/lib.rs +++ b/crates/gs/src/lib.rs @@ -18,3 +18,15 @@ pub fn evaluate_gate(action: &str, target: &str) -> (GateTier, String) { let ctx = ActionContext::safe_internal(action, target); classify(&ctx) } + +/// Evaluates a gate tier for an action with WIMSE principal and RFC 8693 `act` delegation claims. +#[must_use] +pub fn evaluate_gate_with_principal( + action: &str, + target: &str, + claims: &GatePrincipalClaims, + expected_trust_domain: &str, +) -> (GateTier, String) { + let ctx = ActionContext::safe_internal(action, target); + classify_with_principal(&ctx, claims, expected_trust_domain) +} diff --git a/crates/gs/src/main.rs b/crates/gs/src/main.rs index 5fcc890..12ef920 100644 --- a/crates/gs/src/main.rs +++ b/crates/gs/src/main.rs @@ -4,7 +4,10 @@ use std::env; use std::process::ExitCode; -use gs::{evaluate_gate, is_paused, probe_tcp_port, DutyWindow, IncidentRecord}; +use gs::{ + evaluate_gate, evaluate_gate_with_principal, is_paused, probe_tcp_port, DutyWindow, + GatePrincipalClaims, IncidentRecord, +}; fn print_usage() { eprintln!( @@ -12,7 +15,8 @@ fn print_usage() { \n\ USAGE:\n\ gs status Sense estate daemons, pause flags, and health\n\ - gs gate [target] Classify an action against the 4-tier policy gate\n\ + gs gate [target] [--principal ] [--act ...] [--scope ...] [--trust-domain ]\n\ + Classify an action against the 4-tier policy gate\n\ gs incident demo Demonstrate Article 73 duty clock lifecycle\n\ gs --help Print this help text\n\ gs --version Print version information", @@ -69,9 +73,71 @@ fn main() -> ExitCode { return ExitCode::FAILURE; } let action = &args[1]; - let target = args.get(2).map(String::as_str).unwrap_or("cluster"); + let mut target = "cluster"; + let mut idx = 2; + if let Some(arg) = args.get(2) { + if !arg.starts_with("--") { + target = arg.as_str(); + idx = 3; + } + } + + let mut principal: Option = None; + let mut delegation_chain: Vec = Vec::new(); + let mut scopes: Vec = Vec::new(); + let mut trust_domain = "unsigned.gg".to_owned(); + + while idx < args.len() { + match args[idx].as_str() { + "--principal" => { + let Some(val) = args.get(idx + 1) else { + eprintln!("error: '--principal' requires a value"); + return ExitCode::FAILURE; + }; + principal = Some(val.clone()); + idx += 2; + } + "--act" => { + let Some(val) = args.get(idx + 1) else { + eprintln!("error: '--act' requires a value"); + return ExitCode::FAILURE; + }; + delegation_chain.push(val.clone()); + idx += 2; + } + "--scope" => { + let Some(val) = args.get(idx + 1) else { + eprintln!("error: '--scope' requires a value"); + return ExitCode::FAILURE; + }; + scopes.push(val.clone()); + idx += 2; + } + "--trust-domain" => { + let Some(val) = args.get(idx + 1) else { + eprintln!("error: '--trust-domain' requires a value"); + return ExitCode::FAILURE; + }; + trust_domain = val.clone(); + idx += 2; + } + flag => { + eprintln!("error: unknown gate option '{flag}'"); + return ExitCode::FAILURE; + } + } + } - let (tier, reason) = evaluate_gate(action, target); + let (tier, reason) = if let Some(subject) = principal { + let claims = GatePrincipalClaims { + subject, + delegation_chain, + scopes, + }; + evaluate_gate_with_principal(action, target, &claims, &trust_domain) + } else { + evaluate_gate(action, target) + }; println!("Action: {action}"); println!("Target: {target}"); println!("Gate Tier: {tier}"); From feed43034c85956992ac5ddfc7ee5303ad76fbca Mon Sep 17 00:00:00 2001 From: "Christian M. Todie" Date: Fri, 16 Oct 2026 01:27:10 -0400 Subject: [PATCH 3/4] chore(build): switch to GitHub Flow and align with estate Rust best practices --- .cargo/config.toml | 16 ++ .github/workflows/ci.yml | 48 ++++-- .gitignore | 2 +- Cargo.lock | 208 +++++++++++++++++++++++ Cargo.toml | 36 ++-- Justfile | 28 ---- Makefile | 70 +++++--- clippy.toml | 5 + crates/gs-domain/src/lib.rs | 4 +- crates/gs/src/main.rs | 11 +- crates/xtask/Cargo.toml | 18 ++ crates/xtask/src/main.rs | 317 ++++++++++++++++++++++++++++++++++++ deny.toml | 51 ++++++ rust-toolchain.toml | 5 + rustfmt.toml | 4 + 15 files changed, 740 insertions(+), 83 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 Cargo.lock delete mode 100644 Justfile create mode 100644 clippy.toml create mode 100644 crates/xtask/Cargo.toml create mode 100644 crates/xtask/src/main.rs create mode 100644 deny.toml create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..0a47d7d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,16 @@ +# Cargo workspace configuration for gs +# Host Throttling: Physical host ceres runs multiple active agent sessions. +# Shared workstations keep jobs=2 to prevent CPU starvation and OOM. + +[build] +jobs = 2 + +[alias] +xtask = "run --package xtask --" +xwork = "run --package xtask -- xwork" + +[registries.crates-io] +protocol = "sparse" + +[net] +git-fetch-with-cli = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f53522b..7ac3ee9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,38 +2,56 @@ name: CI on: push: - branches: [main, development] + branches: [main] pull_request: - branches: [main, development] + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: CARGO_TERM_COLOR: always - RUSTFLAGS: "-D warnings" + CARGO_INCREMENTAL: 0 + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + RUST_BACKTRACE: 1 jobs: - check-and-test: - name: Check, Lint & Test + check: + name: Build & Verify (xwork + xtask) runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Install stable Rust toolchain - uses: dtolnay/rust-toolchain@stable + - name: Install Rust toolchain (1.94.1) + uses: dtolnay/rust-toolchain@master with: + toolchain: "1.94.1" components: rustfmt, clippy - - name: Rust cache + - name: Rust Cache uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} - - name: Format check + - name: Verify estate hygiene & Wave-0 graph audit + run: | + cargo xtask hygiene + cargo xtask graph-audit --break-check + + - name: Check formatting run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --workspace --all-targets -j 2 -- -D warnings + - name: Run Clippy + run: cargo clippy --workspace --all-targets --locked -j 2 -- -D warnings - - name: Workspace check - run: cargo check --workspace --all-targets -j 2 + - name: Run Tests + run: | + cargo test --workspace --locked -j 2 + cargo test --doc --workspace --locked -j 2 - - name: Run unit tests - run: cargo test --workspace -j 2 + - name: Supply-chain check (cargo-deny) + uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check licenses bans sources diff --git a/.gitignore b/.gitignore index 96ef6c0..a906011 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /target -Cargo.lock +/.tools diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..98b8828 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,208 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "gs" +version = "0.1.0" +dependencies = [ + "anyhow", + "gs-domain", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "gs-domain" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.8", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.8", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 42c6017..af028d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,23 @@ [workspace] resolver = "2" -members = ["crates/*"] +members = [ + "crates/gs-domain", + "crates/gs", + "crates/xtask", +] [workspace.package] -edition = "2021" version = "0.1.0" +edition = "2024" rust-version = "1.85" -publish = false -license = "Apache-2.0 OR MIT" -authors = ["Christian M. Todie "] +license = "MIT OR Apache-2.0" +authors = ["Cerebral Engineering "] repository = "https://github.com/unsigned-gg/gs" +publish = false + +[profile.dev] +# Line tables give useful backtraces at a fraction of full DWARF compile time and disk size. +debug = "line-tables-only" [workspace.lints.rust] unsafe_code = "forbid" @@ -17,14 +25,16 @@ missing_docs = "warn" [workspace.lints.clippy] all = { level = "warn", priority = -1 } +unwrap_used = "warn" +expect_used = "warn" +panic = "warn" +todo = "warn" +dbg_macro = "warn" +unnecessary_map_or = "warn" [workspace.dependencies] -anyhow = "1" -thiserror = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" tracing = "0.1" - -[profile.release] -lto = true -codegen-units = 1 diff --git a/Justfile b/Justfile deleted file mode 100644 index d087021..0000000 --- a/Justfile +++ /dev/null @@ -1,28 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -jobs := "2" - -default: help - -help: - @echo "gs Justfile targets:" - @echo " build - Compile all crates (-j {{jobs}})" - @echo " check - Run cargo check (-j {{jobs}})" - @echo " test - Run test suite (-j {{jobs}})" - @echo " fmt - Format code" - @echo " clippy - Run clippy linter" - -build: - cargo build --workspace -j {{jobs}} - -check: - cargo check --workspace --all-targets -j {{jobs}} - -test: - cargo test --workspace -j {{jobs}} - -fmt: - cargo fmt --all - -clippy: - cargo clippy --workspace --all-targets -j {{jobs}} -- -D warnings diff --git a/Makefile b/Makefile index c47b8eb..e317ed7 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,64 @@ +# Makefile for gs (Godseat — Estate Control Surface) +# Host Throttling: Physical host ceres runs multiple active agent sessions. +# Shared workstations keep JOBS=2 to prevent CPU starvation and OOM. + SHELL := bash .SHELLFLAGS := -euo pipefail -c .DEFAULT_GOAL := help JOBS ?= 2 +export JOBS + +.PHONY: help default xwork check hygiene graph-audit build fmt fmt-check clippy test doc-test deny smoke ci + +##@ General + +help: ## Show this help message + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-18s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +default: help + +##@ Verification & CI Gates (cargo xtask) + +xwork: ## Run full verification DAG via cargo xwork + cargo xwork -.PHONY: help build check test fmt clippy clean +check: ## Run verification pipeline with break-check + cargo xtask check --break-check -help: - @echo "gs Makefile targets:" - @echo " build - Compile all crates (-j $(JOBS))" - @echo " check - Run cargo check (-j $(JOBS))" - @echo " test - Run test suite (-j $(JOBS))" - @echo " fmt - Format code" - @echo " clippy - Run clippy linter" - @echo " clean - Clean build artifacts" +ci: ## Run full CI verification suite + cargo xwork -build: - cargo build --workspace -j $(JOBS) +hygiene: ## Verify estate configuration files and absence of Justfile + cargo xtask hygiene -check: - cargo check --workspace --all-targets -j $(JOBS) +graph-audit: ## Verify workspace DAG and Wave-0 gs-domain purity + cargo xtask graph-audit --break-check -test: - cargo test --workspace -j $(JOBS) +##@ Build & Smoke -fmt: +build: ## Compile all workspace crates (-j 2) + cargo build --workspace --locked -j $(JOBS) + +smoke: ## Build and smoke-test the gs CLI (-j 2) + cargo xtask smoke + +##@ Code Quality & Supply Chain + +fmt: ## Format all workspace Rust code cargo fmt --all -clippy: - cargo clippy --workspace --all-targets -j $(JOBS) -- -D warnings +fmt-check: ## Check formatting without modifying files + cargo fmt --all -- --check + +clippy: ## Lint workspace with clippy (-D warnings, -j 2) + cargo clippy --workspace --all-targets --locked -j $(JOBS) -- -D warnings + +test: ## Run unit and integration test suite (-j 2) + cargo test --workspace --locked -j $(JOBS) + +doc-test: ## Run documentation tests (-j 2) + cargo test --doc --workspace --locked -j $(JOBS) -clean: - cargo clean +deny: ## Audit licenses, bans, and advisories via cargo-deny + cargo deny check diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..673d658 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,5 @@ +# clippy config: estate convention. +too-many-arguments-threshold = 8 +type-complexity-threshold = 300 +allow-unwrap-in-tests = true +allow-expect-in-tests = true diff --git a/crates/gs-domain/src/lib.rs b/crates/gs-domain/src/lib.rs index 43ceddd..461ea35 100644 --- a/crates/gs-domain/src/lib.rs +++ b/crates/gs-domain/src/lib.rs @@ -12,7 +12,7 @@ pub mod gate; pub mod incident; pub use gate::{ - canonical_action, classify, classify_with_principal, ActionContext, GatePrincipalClaims, - GateTier, WimsePrincipalKind, MAX_DELEGATION_DEPTH, + ActionContext, GatePrincipalClaims, GateTier, MAX_DELEGATION_DEPTH, WimsePrincipalKind, + canonical_action, classify, classify_with_principal, }; pub use incident::{DutyWindow, IncidentRecord, IncidentState}; diff --git a/crates/gs/src/main.rs b/crates/gs/src/main.rs index 12ef920..adf87e2 100644 --- a/crates/gs/src/main.rs +++ b/crates/gs/src/main.rs @@ -5,8 +5,8 @@ use std::env; use std::process::ExitCode; use gs::{ - evaluate_gate, evaluate_gate_with_principal, is_paused, probe_tcp_port, DutyWindow, - GatePrincipalClaims, IncidentRecord, + DutyWindow, GatePrincipalClaims, IncidentRecord, evaluate_gate, evaluate_gate_with_principal, + is_paused, probe_tcp_port, }; fn print_usage() { @@ -162,11 +162,14 @@ fn main() -> ExitCode { record.deadline_unix_secs() ); - record.establish_link(now + 100).unwrap(); + if let Err(err) = record.establish_link(now + 100) { + eprintln!("gs: failed to establish link: {err}"); + return ExitCode::FAILURE; + } println!(" 3. Link established at: {}", now + 100); println!( " 4. Derived deadline: {}", - record.deadline_unix_secs().unwrap() + record.deadline_unix_secs().unwrap_or_default() ); println!( " 5. Is breached at now + 500s? {}", diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml new file mode 100644 index 0000000..b2fe98a --- /dev/null +++ b/crates/xtask/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "xtask" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +publish = false +description = "Workspace verification, hygiene, and CI runner for gs" + +[lints] +workspace = true + +[dependencies] +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs new file mode 100644 index 0000000..ad7819e --- /dev/null +++ b/crates/xtask/src/main.rs @@ -0,0 +1,317 @@ +//! `cargo xtask` verification, hygiene, and CI runner for `gs` (Godseat). +//! +//! Enforces estate best practices: +//! - Host throttling (`-j 2`) +//! - Wave-0 purity of `gs-domain` (`graph-audit` + `--break-check`) +//! - Repository hygiene (`rust-toolchain.toml`, `rustfmt.toml`, `clippy.toml`, `deny.toml`, `.cargo/config.toml`) +//! - Full verification pipeline (`xwork`, `check`, `fmt`, `clippy`, `test`, `deny`, `build`, `smoke`) + +use std::collections::BTreeSet; +use std::env; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; + +/// Forbidden runtime/network/filesystem crates in Wave-0 `gs-domain`. +const FORBIDDEN_DOMAIN_DEPS: &[&str] = &[ + "tokio", + "reqwest", + "hyper", + "axum", + "sqlx", + "rusqlite", + "std::fs", + "std::net", + "std::process", +]; + +fn main() -> ExitCode { + let args: Vec = env::args().skip(1).collect(); + let root = match workspace_root() { + Ok(r) => r, + Err(e) => { + eprintln!("xtask error: {e}"); + return ExitCode::FAILURE; + } + }; + let jobs = env::var("JOBS") + .ok() + .filter(|s| !s.trim().is_empty()) + .unwrap_or_else(|| "2".to_string()); + + let cmd = args.first().map(String::as_str).unwrap_or("help"); + let break_check = args.iter().any(|a| a == "--break-check"); + + let result = match cmd { + "xwork" => run_xwork(&root, &jobs), + "check" => run_check(&root, &jobs, break_check), + "hygiene" => run_hygiene(&root), + "graph-audit" => run_graph_audit(&root, break_check), + "fmt" => run_cargo(&root, &["fmt", "--all"]), + "fmt-check" => run_cargo(&root, &["fmt", "--all", "--", "--check"]), + "clippy" => run_cargo( + &root, + &[ + "clippy", + "--workspace", + "--all-targets", + "--locked", + "-j", + &jobs, + "--", + "-D", + "warnings", + ], + ), + "test" => run_cargo(&root, &["test", "--workspace", "--locked", "-j", &jobs]), + "doc-test" => run_cargo( + &root, + &["test", "--doc", "--workspace", "--locked", "-j", &jobs], + ), + "deny" => run_cargo(&root, &["deny", "check"]), + "build" => run_cargo(&root, &["build", "--workspace", "--locked", "-j", &jobs]), + "smoke" => run_cargo( + &root, + &["build", "--package", "gs", "--locked", "-j", &jobs], + ) + .and_then(|()| { + run_cargo( + &root, + &[ + "run", + "--package", + "gs", + "--locked", + "-j", + &jobs, + "--", + "status", + ], + ) + }), + "help" | "--help" | "-h" => { + print_usage(); + Ok(()) + } + other => Err(format!("unknown xtask command `{other}`")), + }; + + match result { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("xtask error: {e}"); + ExitCode::FAILURE + } + } +} + +fn print_usage() { + println!("Usage: cargo xtask [--break-check]"); + println!(); + println!("Commands:"); + println!( + " xwork Execute full verification DAG (hygiene, fmt-check, clippy, test, graph-audit)" + ); + println!(" check Run verification gates (supports --break-check)"); + println!(" hygiene Verify estate configuration files and absence of Justfile"); + println!( + " graph-audit Verify workspace DAG and Wave-0 gs-domain purity (supports --break-check)" + ); + println!(" fmt Format all workspace Rust code"); + println!(" fmt-check Check Rust formatting without modifying files"); + println!(" clippy Run clippy across workspace with -D warnings"); + println!(" test Run unit and integration tests (-j 2)"); + println!(" doc-test Run documentation tests (-j 2)"); + println!(" deny Run cargo-deny supply-chain check"); + println!(" build Build workspace (-j 2)"); + println!(" smoke Build and smoke-test the gs CLI"); +} + +fn workspace_root() -> Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .map_err(|_| "CARGO_MANIFEST_DIR not set".to_string())?; + manifest_dir + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .ok_or_else(|| "cannot resolve workspace root from CARGO_MANIFEST_DIR".to_string()) +} + +fn run_cargo(root: &Path, args: &[&str]) -> Result<(), String> { + println!("==> cargo {}", args.join(" ")); + let status = Command::new("cargo") + .args(args) + .current_dir(root) + .status() + .map_err(|e| format!("failed to spawn cargo: {e}"))?; + if status.success() { + Ok(()) + } else { + Err(format!("`cargo {}` exited with {status}", args.join(" "))) + } +} + +fn run_hygiene(root: &Path) -> Result<(), String> { + println!("==> xtask hygiene"); + let required = [ + "rust-toolchain.toml", + "rustfmt.toml", + "clippy.toml", + "deny.toml", + ".cargo/config.toml", + "Makefile", + "Cargo.toml", + ]; + for rel in required { + if !root.join(rel).is_file() { + return Err(format!("missing required estate file: {rel}")); + } + } + if root.join("Justfile").exists() || root.join("justfile").exists() { + return Err("Justfile is retired by estate SOP; use Makefile + cargo xtask".to_string()); + } + let cargo_cfg = std::fs::read_to_string(root.join(".cargo/config.toml")) + .map_err(|e| format!("read .cargo/config.toml: {e}"))?; + if !cargo_cfg.contains("jobs = 2") { + return Err( + ".cargo/config.toml must enforce `jobs = 2` for host ceres throttling".to_string(), + ); + } + let root_toml = std::fs::read_to_string(root.join("Cargo.toml")) + .map_err(|e| format!("read Cargo.toml: {e}"))?; + if !root_toml.contains("line-tables-only") { + return Err("Cargo.toml must set `[profile.dev] debug = \"line-tables-only\"`".to_string()); + } + println!("xtask hygiene: ok"); + Ok(()) +} + +/// Evaluates whether `gs-domain` dependency list and source imports satisfy Wave-0 purity. +pub fn check_domain_purity(deps: &[&str], source_snippets: &[&str]) -> Result<(), String> { + for dep in deps { + if FORBIDDEN_DOMAIN_DEPS.contains(dep) { + return Err(format!( + "Wave-0 purity violation: `gs-domain` depends on forbidden crate `{dep}`" + )); + } + if *dep == "gs" || *dep == "xtask" { + return Err(format!( + "DAG cycle violation: Wave-0 `gs-domain` cannot depend on higher-wave crate `{dep}`" + )); + } + } + for src in source_snippets { + for forbidden in ["std::fs", "std::net", "std::process"] { + if src.contains(forbidden) { + return Err(format!( + "Wave-0 purity violation: `gs-domain` source references `{forbidden}`" + )); + } + } + } + Ok(()) +} + +fn run_graph_audit(root: &Path, break_check: bool) -> Result<(), String> { + println!("==> xtask graph-audit (break_check={break_check})"); + let domain_toml = std::fs::read_to_string(root.join("crates/gs-domain/Cargo.toml")) + .map_err(|e| format!("read crates/gs-domain/Cargo.toml: {e}"))?; + + let mut deps = BTreeSet::new(); + let mut in_deps = false; + for line in domain_toml.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('[') { + in_deps = trimmed == "[dependencies]" || trimmed.starts_with("[dependencies."); + continue; + } + if in_deps && !trimmed.is_empty() && !trimmed.starts_with('#') { + if let Some((name, _)) = trimmed.split_once('=') { + let dep_name = name.trim().split('.').next().unwrap_or("").trim(); + if !dep_name.is_empty() { + deps.insert(dep_name.to_string()); + } + } + } + } + + let mut sources = Vec::new(); + let src_dir = root.join("crates/gs-domain/src"); + let entries = + std::fs::read_dir(&src_dir).map_err(|e| format!("read_dir {}: {e}", src_dir.display()))?; + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("rs") { + let content = std::fs::read_to_string(&path) + .map_err(|e| format!("read {}: {e}", path.display()))?; + sources.push(content); + } + } + + let dep_refs: Vec<&str> = deps.iter().map(String::as_str).collect(); + let src_refs: Vec<&str> = sources.iter().map(String::as_str).collect(); + check_domain_purity(&dep_refs, &src_refs)?; + + if break_check { + let mut sabotaged = dep_refs.clone(); + sabotaged.push("tokio"); + if check_domain_purity(&sabotaged, &src_refs).is_ok() { + return Err("break-check failed: injected `tokio` into gs-domain did not fail check_domain_purity".to_string()); + } + let sabotaged_src = vec!["use std::fs::File;"]; + if check_domain_purity(&dep_refs, &sabotaged_src).is_ok() { + return Err("break-check failed: injected `std::fs` into gs-domain did not fail check_domain_purity".to_string()); + } + println!("xtask graph-audit: break-check verified (sabotaged inputs rejected)"); + } + + println!("xtask graph-audit: ok (gs-domain Wave-0 pure)"); + Ok(()) +} + +fn run_check(root: &Path, jobs: &str, break_check: bool) -> Result<(), String> { + run_hygiene(root)?; + run_graph_audit(root, break_check)?; + run_cargo(root, &["fmt", "--all", "--", "--check"])?; + run_cargo( + root, + &[ + "clippy", + "--workspace", + "--all-targets", + "--locked", + "-j", + jobs, + "--", + "-D", + "warnings", + ], + )?; + run_cargo(root, &["test", "--workspace", "--locked", "-j", jobs])?; + run_cargo( + root, + &["test", "--doc", "--workspace", "--locked", "-j", jobs], + )?; + Ok(()) +} + +fn run_xwork(root: &Path, jobs: &str) -> Result<(), String> { + run_check(root, jobs, true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wave0_domain_purity_accepts_clean_domain_and_rejects_io() { + assert!( + check_domain_purity(&["serde", "serde_json", "thiserror"], &["pub struct Foo;"]) + .is_ok() + ); + assert!(check_domain_purity(&["serde", "tokio"], &["pub struct Foo;"]).is_err()); + assert!(check_domain_purity(&["gs"], &["pub struct Foo;"]).is_err()); + assert!(check_domain_purity(&["serde"], &["use std::net::TcpStream;"]).is_err()); + } +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..f339243 --- /dev/null +++ b/deny.toml @@ -0,0 +1,51 @@ +# cargo-deny configuration for gs (Godseat — Estate Control Surface) +# Enforces supply-chain security, license compliance, and dependency hygiene. + +[advisories] +version = 2 +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +yanked = "deny" +unmaintained = "workspace" +unsound = "all" +ignore = [] + +[licenses] +version = 2 +confidence-threshold = 0.8 +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Unicode-DFS-2016", + "Zlib", + "MPL-2.0", + "CDLA-Permissive-2.0", + "0BSD", + "CC0-1.0", + "Unlicense", +] + +[bans] +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" +allow = [] +deny = [ + { name = "openssl", wrappers = [] }, + { name = "openssl-sys", wrappers = [] }, +] +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f1faedc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# Pinned estate Rust toolchain (matches ci-rust-runner:v0.1.6). +channel = "1.94.1" +components = ["rustfmt", "clippy"] +profile = "minimal" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..444f29c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +# rustfmt config: matches the estate convention (tab_spaces 4, max_width 100). +max_width = 100 +tab_spaces = 4 +edition = "2024" From 6abed118377c474f393c04b8580788be31b3f4bf Mon Sep 17 00:00:00 2001 From: "Christian M. Todie" Date: Fri, 16 Oct 2026 01:30:23 -0400 Subject: [PATCH 4/4] fix(ci): inherit gs-domain version from workspace and allow wildcard paths in deny.toml --- Cargo.toml | 1 + crates/gs/Cargo.toml | 2 +- deny.toml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index af028d6..97de849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ unnecessary_map_or = "warn" [workspace.dependencies] anyhow = "1.0" +gs-domain = { version = "0.1.0", path = "crates/gs-domain" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" diff --git a/crates/gs/Cargo.toml b/crates/gs/Cargo.toml index d464831..dfb7998 100644 --- a/crates/gs/Cargo.toml +++ b/crates/gs/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" workspace = true [dependencies] -gs-domain = { path = "../gs-domain" } +gs-domain.workspace = true anyhow.workspace = true thiserror.workspace = true serde.workspace = true diff --git a/deny.toml b/deny.toml index f339243..6d5f3bf 100644 --- a/deny.toml +++ b/deny.toml @@ -33,6 +33,7 @@ allow = [ [bans] multiple-versions = "warn" wildcards = "deny" +allow-wildcard-paths = true highlight = "all" workspace-default-features = "allow" external-default-features = "allow"