refactor(org-ops): neutralize BXNet identity from framework core

Move all BXNet-specific defaults out of org-ops-core into the example
CLI binary (org-ops-cli/src/main.rs). The framework is now fork-ready
for any consortium without string-replacing org-specific values.

Changes:
- OrgOpsConfig: neutral defaults (my-org, example.com), added
  infra_namespace, bridge_daemonset, ssh_user fields
- AuthConfig: config_dir_name field replaces hardcoded bxnet-ops
  path; config_dir() now a method using config value
- GitConfig: neutral default (git.example.com)
- auth_commands: DID_BRIDGE_PATH env replaces hardcoded dev path
- lib.rs (connect): namespace, daemonset, SSH user, cert paths all
  read from OrgOpsConfig instead of hardcoded strings
- score_fetcher: neutral corpus entry name
- playbook_commands: neutral Keycloak service name and temp dir
- org-ops-cli/main.rs: explicit BXNet example with "replace these"
  comment, all org-specific values passed via config
- .gitignore: added target/

Zero BXNet references remain in org-ops-core source (verified by grep).

Signed-off-by: Tyler King <tking@guildhouse.dev>
Signed-off-by: Tyler J King <tking727@gmail.com>
This commit is contained in:
Tyler J King 2026-04-15 16:31:19 -04:00
parent b6d9b7fa97
commit c68456d745
8 changed files with 105 additions and 58 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

View file

@ -1,3 +1,9 @@
//! Example org-ops CLI — BXNet consortium.
//!
//! This binary demonstrates how to build a governed CLI for your
//! consortium using the org-ops-core framework. Fork this file and
//! replace the config values with your org's identity.
use org_ops_core::{
AuthCommands, AuthConfig, GitConfig, GovernedGitCommands, OrgOps, OrgOpsConfig,
PlaybookCommands,
@ -8,6 +14,7 @@ fn main() -> anyhow::Result<()> {
OrgOps::builder()
.with_config(OrgOpsConfig {
// ── Replace these with your consortium's values ──
org_name: "BXNet".into(),
trust_domain: "bxnet.io".into(),
bascule_endpoint: "bascule.bxnet.io:443".into(),
@ -15,8 +22,16 @@ fn main() -> anyhow::Result<()> {
binary_name: "bxnet-ops".into(),
description: "BXNet governed operations CLI".into(),
version: env!("CARGO_PKG_VERSION").into(),
infra_namespace: "guildhouse-infra".into(),
bridge_daemonset: "substrate-bridge".into(),
ssh_user: "tking".into(),
})
.with_commands(AuthCommands::new(AuthConfig::default()))
.with_commands(AuthCommands::new(AuthConfig {
oidc_issuer: "https://auth.bxnet.io/realms/guildhouse".into(),
client_id: "bxnet-ops".into(),
config_dir_name: "bxnet-ops".into(),
..Default::default()
}))
.with_commands(GovernedGitCommands::new(GitConfig {
forgejo_url: "https://git.bxnet.io".into(),
forgejo_token,

View file

@ -14,37 +14,24 @@ pub struct AuthConfig {
pub oidc_issuer: String,
pub client_id: String,
pub did_bridge_port: u16,
/// Directory name under ~/.config/ for credential storage.
pub config_dir_name: String,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
oidc_issuer: "https://auth.bxnet.io/realms/guildhouse".into(),
client_id: "bxnet-ops".into(),
oidc_issuer: "https://auth.example.com/realms/consortium".into(),
client_id: "org-ops".into(),
did_bridge_port: 7777,
config_dir_name: "org-ops".into(),
}
}
}
fn config_dir() -> PathBuf {
fn config_dir_for(name: &str) -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".config").join("bxnet-ops")
}
fn cert_path() -> PathBuf {
config_dir().join("identity.pem")
}
fn key_path() -> PathBuf {
config_dir().join("identity.key")
}
fn did_path() -> PathBuf {
config_dir().join("identity.did")
}
fn expiry_path() -> PathBuf {
config_dir().join("identity.expiry")
PathBuf::from(home).join(".config").join(name)
}
pub struct AuthCommands {
@ -56,8 +43,28 @@ impl AuthCommands {
Self { config }
}
fn ensure_config_dir() {
let dir = config_dir();
fn config_dir(&self) -> PathBuf {
config_dir_for(&self.config.config_dir_name)
}
fn cert_path(&self) -> PathBuf {
self.config_dir().join("identity.pem")
}
fn key_path(&self) -> PathBuf {
self.config_dir().join("identity.key")
}
fn did_path(&self) -> PathBuf {
self.config_dir().join("identity.did")
}
fn expiry_path(&self) -> PathBuf {
self.config_dir().join("identity.expiry")
}
fn ensure_config_dir(&self) {
let dir = self.config_dir();
if !dir.exists() {
fs::create_dir_all(&dir).ok();
#[cfg(unix)]
@ -69,10 +76,9 @@ impl AuthCommands {
}
fn start_did_bridge(&self) -> anyhow::Result<Option<std::process::Child>> {
// Find did_bridge.py
let home = std::env::var("HOME").unwrap_or_default();
// Find did_bridge.py — check DID_BRIDGE_PATH env, then cwd
let bridge_paths = [
format!("{}/projects/substrate-project/guildhouse/services/did-bridge/did_bridge.py", home),
std::env::var("DID_BRIDGE_PATH").unwrap_or_default(),
"did_bridge.py".to_string(),
];
@ -134,7 +140,7 @@ impl AuthCommands {
println!("Authenticating via OIDC...");
println!(" Issuer: {}", self.config.oidc_issuer);
Self::ensure_config_dir();
self.ensure_config_dir();
// Start did-bridge
eprintln!("Starting did-bridge...");
@ -229,21 +235,21 @@ impl AuthCommands {
let (cert, key, did, expires) = result?;
// Store certificate and key (not token)
fs::write(cert_path(), &cert)?;
fs::write(key_path(), &key)?;
fs::write(did_path(), &did)?;
fs::write(expiry_path(), expires.to_string())?;
fs::write(self.cert_path(), &cert)?;
fs::write(self.key_path(), &key)?;
fs::write(self.did_path(), &did)?;
fs::write(self.expiry_path(), expires.to_string())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(key_path(), fs::Permissions::from_mode(0o600)).ok();
fs::set_permissions(self.key_path(), fs::Permissions::from_mode(0o600)).ok();
}
println!();
println!("Authenticated.");
println!(" DID: {}", did);
println!(" Certificate: {}", cert_path().display());
println!(" Certificate: {}", self.cert_path().display());
println!(" Expires: 1 hour");
println!(" Token: zeroized");
@ -251,14 +257,14 @@ impl AuthCommands {
}
fn cmd_status(&self, _ctx: &SessionContext) -> anyhow::Result<()> {
if !cert_path().exists() {
if !self.cert_path().exists() {
println!("Not authenticated.");
println!("Run: guildhouse-ops auth login");
println!("Run: auth login");
return Ok(());
}
let did = fs::read_to_string(did_path()).unwrap_or("unknown".into());
let expires: i64 = fs::read_to_string(expiry_path())
let did = fs::read_to_string(self.did_path()).unwrap_or("unknown".into());
let expires: i64 = fs::read_to_string(self.expiry_path())
.unwrap_or("0".into())
.trim()
.parse()
@ -271,7 +277,7 @@ impl AuthCommands {
if expires > 0 && now > expires {
println!("Session expired.");
println!("Run: guildhouse-ops auth login");
println!("Run: auth login");
return Ok(());
}
@ -284,13 +290,13 @@ impl AuthCommands {
println!("Authenticated");
println!(" DID: {}", did);
println!(" Remaining: {}", remaining);
println!(" Certificate: {}", cert_path().display());
println!(" Certificate: {}", self.cert_path().display());
Ok(())
}
fn cmd_logout(&self, _ctx: &SessionContext) -> anyhow::Result<()> {
for path in &[cert_path(), key_path(), did_path(), expiry_path()] {
for path in &[self.cert_path(), self.key_path(), self.did_path(), self.expiry_path()] {
if path.exists() {
fs::remove_file(path)?;
}

View file

@ -1,5 +1,16 @@
/// Configuration for an org-ops instance.
/// Fork org-ops and set these values for your consortium.
///
/// Set these values for your consortium in the CLI binary's `main()`:
/// ```ignore
/// OrgOps::builder()
/// .with_config(OrgOpsConfig {
/// org_name: "MyOrg".into(),
/// trust_domain: "myorg.example.com".into(),
/// ..Default::default()
/// })
/// .build()
/// .run()
/// ```
#[derive(Debug, Clone)]
pub struct OrgOpsConfig {
pub org_name: String,
@ -9,18 +20,27 @@ pub struct OrgOpsConfig {
pub binary_name: String,
pub description: String,
pub version: String,
/// Kubernetes namespace for port-forward and corpus lookups.
pub infra_namespace: String,
/// DaemonSet name for Bascule port-forward target.
pub bridge_daemonset: String,
/// SSH user for governed shell connections.
pub ssh_user: String,
}
impl Default for OrgOpsConfig {
fn default() -> Self {
Self {
org_name: "BXNet".into(),
trust_domain: "bxnet.io".into(),
bascule_endpoint: "bascule.bxnet.io:443".into(),
chronicle_endpoint: "chronicle.bxnet.io:8080".into(),
binary_name: "bxnet-ops".into(),
description: "BXNet governed operations CLI".into(),
org_name: "my-org".into(),
trust_domain: "example.com".into(),
bascule_endpoint: "localhost:2222".into(),
chronicle_endpoint: "localhost:8090".into(),
binary_name: "org-ops".into(),
description: "Governed operations CLI".into(),
version: env!("CARGO_PKG_VERSION").into(),
infra_namespace: "guildhouse-infra".into(),
bridge_daemonset: "substrate-bridge".into(),
ssh_user: std::env::var("USER").unwrap_or_else(|_| "operator".into()),
}
}
}

View file

@ -28,9 +28,9 @@ pub struct GitConfig {
impl Default for GitConfig {
fn default() -> Self {
Self {
forgejo_url: "https://git.bxnet.io".into(),
forgejo_url: "https://git.example.com".into(),
forgejo_token: None,
chronicle_webhook: "http://localhost:8090/webhook/forgejo".into(),
chronicle_webhook: "http://localhost:8090/webhook/cloudevents".into(),
}
}
}

View file

@ -192,8 +192,8 @@ impl OrgOps {
let pf = std::process::Command::new("kubectl")
.args([
"port-forward",
"-n", "guildhouse-infra",
"daemonset/substrate-bridge",
"-n", &self.config.infra_namespace,
&format!("daemonset/{}", self.config.bridge_daemonset),
"12222:2222",
])
.stdout(std::process::Stdio::null())
@ -219,8 +219,9 @@ impl OrgOps {
// 5. Exec SSH — use certificate if available
let home = std::env::var("HOME").unwrap_or_default();
let cert_file = format!("{}/.config/guildhouse-ops/identity.pem", home);
let key_file = format!("{}/.config/guildhouse-ops/identity.key", home);
let config_name = &self.config.binary_name;
let cert_file = format!("{}/.config/{}/identity.pem", home, config_name);
let key_file = format!("{}/.config/{}/identity.key", home, config_name);
let has_cert = std::path::Path::new(&cert_file).exists()
&& std::path::Path::new(&key_file).exists();
@ -240,7 +241,7 @@ impl OrgOps {
eprintln!(" Tip: run 'auth login' for certificate auth");
}
ssh_args.push(format!("tking@{ssh_host}"));
ssh_args.push(format!("{}@{ssh_host}", self.config.ssh_user));
let status = std::process::Command::new("ssh")
.args(&ssh_args)

View file

@ -152,12 +152,12 @@ impl PlaybookCommands {
let gsap_ac = if let Ok(broker_url) = std::env::var("GSAP_BROKER_URL") {
let token = std::env::var("GSAP_BEARER_TOKEN").unwrap_or_default();
let driver_id = std::env::var("GSAP_DRIVER_ID")
.unwrap_or_else(|_| "keycloak-guildhouse".into());
.unwrap_or_else(|_| "keycloak".into());
let accord = std::env::var("GSAP_ACCORD_TEMPLATE")
.unwrap_or_else(|_| accord_name.clone());
let session_dir = std::env::var("GSAP_SESSION_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir().join("bxnet-gsap"));
.unwrap_or_else(|_| std::env::temp_dir().join("org-ops-gsap"));
let client = GsapClient::new(broker_url, token, session_dir);
let params_json = serde_json::to_string(&extra_vars).unwrap_or_default();

View file

@ -45,7 +45,11 @@ pub fn fetch_score(entry_name: &str) -> WorkloadRiskScore {
}
}
/// Fetch the cluster's best Tier A corpus entry score.
/// Fetch the cluster's default corpus entry score.
///
/// Callers should pass the org's binary name or a configured corpus
/// entry name rather than hardcoding an org-specific value.
pub fn fetch_cluster_score() -> WorkloadRiskScore {
fetch_score("bxnet-ops")
// Default corpus entry — overridden by orgs in their CLI binary.
fetch_score("org-ops")
}