Compare commits
No commits in common. "d0b9ca0e6ab7cc1360616df3d748b302565c0c9f9814a856b5d1e6cf60c55fd5" and "0adcf12e7867bb9ddf10f9c80af9dec95da462f6d671d271616fd96bf7272626" have entirely different histories.
d0b9ca0e6a
...
0adcf12e78
10 changed files with 22 additions and 543 deletions
|
|
@ -6,15 +6,8 @@ ca_key_path = "/dev/null"
|
||||||
host_key_path = "/dev/null"
|
host_key_path = "/dev/null"
|
||||||
dispatch_mode = "direct"
|
dispatch_mode = "direct"
|
||||||
auth_mode = "permissive"
|
auth_mode = "permissive"
|
||||||
shell_command = "/home/tking/.local/bin/gsh"
|
|
||||||
|
|
||||||
[elevation]
|
[elevation]
|
||||||
operator_ttl_secs = 3600
|
operator_ttl_secs = 3600
|
||||||
admin_ttl_secs = 1800
|
admin_ttl_secs = 1800
|
||||||
emergency_ttl_secs = 900
|
emergency_ttl_secs = 900
|
||||||
|
|
||||||
[gsap]
|
|
||||||
# broker_url = "http://localhost:8091"
|
|
||||||
# token = ""
|
|
||||||
default_corpus_cid = "sha256:dev-jumphost"
|
|
||||||
default_accord_template = "shell-exec"
|
|
||||||
|
|
|
||||||
1
dist/.gitignore
vendored
1
dist/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
*.tar
|
|
||||||
|
|
@ -30,7 +30,7 @@ pub fn run_human_mode(
|
||||||
// Post SESSION_STARTED CR if broker available:
|
// Post SESSION_STARTED CR if broker available:
|
||||||
if let Some(ref base) = broker_url {
|
if let Some(ref base) = broker_url {
|
||||||
if let Ok(client) = build_client(token) {
|
if let Ok(client) = build_client(token) {
|
||||||
let _ = post_cr(&client, base, &session.ac_id, "completed");
|
let _ = post_cr(&client, base, &session.ac_id, "session_started");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ pub fn run_human_mode(
|
||||||
// Post SESSION_ENDED CR:
|
// Post SESSION_ENDED CR:
|
||||||
if let Some(ref base) = broker_url {
|
if let Some(ref base) = broker_url {
|
||||||
if let Ok(client) = build_client(token) {
|
if let Ok(client) = build_client(token) {
|
||||||
let _ = post_cr(&client, base, &session.ac_id, "session_end");
|
let _ = post_cr(&client, base, &session.ac_id, "session_ended");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,8 +150,7 @@ fn print_banner(session: &SessionState) {
|
||||||
println!();
|
println!();
|
||||||
println!("{}", "╔══════════════════════════════════════════════════════════╗".bright_blue());
|
println!("{}", "╔══════════════════════════════════════════════════════════╗".bright_blue());
|
||||||
println!("{} Guildhouse Governed Shell v0.1.0{}", "║".bright_blue(), " ".repeat(24).to_string() + &"║".bright_blue().to_string());
|
println!("{} Guildhouse Governed Shell v0.1.0{}", "║".bright_blue(), " ".repeat(24).to_string() + &"║".bright_blue().to_string());
|
||||||
println!("{} Principal: {:<44}{}", "║".bright_blue(), session.display_name, "║".bright_blue());
|
println!("{} Principal: {:<44}{}", "║".bright_blue(), session.principal, "║".bright_blue());
|
||||||
println!("{} DID: {:<44}{}", "║".bright_blue(), session.principal, "║".bright_blue());
|
|
||||||
println!("{} Corpus: {:<44}{}", "║".bright_blue(), corpus_short, "║".bright_blue());
|
println!("{} Corpus: {:<44}{}", "║".bright_blue(), corpus_short, "║".bright_blue());
|
||||||
println!("{} Session: {:<44}{}", "║".bright_blue(), format!("{} (expires {})", &session.ac_id[..8.min(session.ac_id.len())], expiry), "║".bright_blue());
|
println!("{} Session: {:<44}{}", "║".bright_blue(), format!("{} (expires {})", &session.ac_id[..8.min(session.ac_id.len())], expiry), "║".bright_blue());
|
||||||
println!("{} Risk: {:<44}{}", "║".bright_blue(),
|
println!("{} Risk: {:<44}{}", "║".bright_blue(),
|
||||||
|
|
@ -162,25 +161,6 @@ fn print_banner(session: &SessionState) {
|
||||||
_ => session.risk_level.clone(),
|
_ => session.risk_level.clone(),
|
||||||
},
|
},
|
||||||
"║".bright_blue());
|
"║".bright_blue());
|
||||||
|
|
||||||
// DEFCON line — only shown when not peacetime
|
|
||||||
if session.defcon_level < 5 {
|
|
||||||
let defcon_label = match session.defcon_level {
|
|
||||||
1 => "LOCKDOWN".red().to_string(),
|
|
||||||
2 => "CRITICAL".red().to_string(),
|
|
||||||
3 => "RESTRICTED".yellow().to_string(),
|
|
||||||
4 => "ELEVATED".yellow().to_string(),
|
|
||||||
_ => "PEACETIME".green().to_string(),
|
|
||||||
};
|
|
||||||
println!("{} DEFCON: {:<44}{}", "║".bright_blue(),
|
|
||||||
format!("{} — {}", session.defcon_level, defcon_label),
|
|
||||||
"║".bright_blue());
|
|
||||||
if let Some(ref reason) = session.defcon_reason {
|
|
||||||
let truncated = if reason.len() > 42 { format!("{}...", &reason[..39]) } else { reason.clone() };
|
|
||||||
println!("{} Reason: {:<44}{}", "║".bright_blue(), truncated, "║".bright_blue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{}", "╚══════════════════════════════════════════════════════════╝".bright_blue());
|
println!("{}", "╚══════════════════════════════════════════════════════════╝".bright_blue());
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
@ -212,24 +192,17 @@ fn print_summary(session: &SessionState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_prompt(session: &SessionState) -> DefaultPrompt {
|
fn build_prompt(session: &SessionState) -> DefaultPrompt {
|
||||||
// DEFCON overrides the risk indicator when elevated
|
let risk_indicator = match session.risk_level.as_str() {
|
||||||
let risk_indicator = if session.defcon_level <= 2 {
|
|
||||||
"[DEFCON]"
|
|
||||||
} else if session.defcon_level == 3 {
|
|
||||||
"[restricted]"
|
|
||||||
} else {
|
|
||||||
match session.risk_level.as_str() {
|
|
||||||
"baseline" | "standard" | "ungoverned" => "[governed]",
|
"baseline" | "standard" | "ungoverned" => "[governed]",
|
||||||
"elevated" => "[elevated]",
|
"elevated" => "[elevated]",
|
||||||
"high" | "critical" => "[HIGH]",
|
"high" | "critical" => "[HIGH]",
|
||||||
_ => "[governed]",
|
_ => "[governed]",
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name);
|
let user = session.principal.split('@').next().unwrap_or(&session.principal);
|
||||||
|
|
||||||
DefaultPrompt::new(
|
DefaultPrompt::new(
|
||||||
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, short_name)),
|
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, user)),
|
||||||
DefaultPromptSegment::Empty,
|
DefaultPromptSegment::Empty,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,15 @@ pub enum CorpusCheckResult {
|
||||||
Denied { command: String, corpus_cid: String },
|
Denied { command: String, corpus_cid: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default corpus base directory.
|
|
||||||
pub const DEFAULT_CORPUS_BASE: &str = "/opt/substrate/corpus";
|
|
||||||
|
|
||||||
/// Check if a command is authorized in the corpus directory.
|
/// Check if a command is authorized in the corpus directory.
|
||||||
///
|
///
|
||||||
/// `base_dir` overrides the default /opt/substrate/corpus (set via GSH_CORPUS_DIR env).
|
|
||||||
/// Returns Ok(result) always. Caller decides whether to block on Denied.
|
/// Returns Ok(result) always. Caller decides whether to block on Denied.
|
||||||
pub fn corpus_check(corpus_cid: &str, command: &str) -> CorpusCheckResult {
|
pub fn corpus_check(corpus_cid: &str, command: &str) -> CorpusCheckResult {
|
||||||
corpus_check_with_base(corpus_cid, command, DEFAULT_CORPUS_BASE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// corpus_check with an explicit base directory.
|
|
||||||
pub fn corpus_check_with_base(corpus_cid: &str, command: &str, base_dir: &str) -> CorpusCheckResult {
|
|
||||||
if corpus_cid == "sha256:ungoverned" {
|
if corpus_cid == "sha256:ungoverned" {
|
||||||
return CorpusCheckResult::Ungoverned;
|
return CorpusCheckResult::Ungoverned;
|
||||||
}
|
}
|
||||||
|
|
||||||
let corpus_dir = Path::new(base_dir).join(corpus_cid);
|
let corpus_dir = Path::new("/opt/substrate/corpus").join(corpus_cid);
|
||||||
if !corpus_dir.exists() {
|
if !corpus_dir.exists() {
|
||||||
return CorpusCheckResult::NotMounted;
|
return CorpusCheckResult::NotMounted;
|
||||||
}
|
}
|
||||||
|
|
@ -82,14 +73,10 @@ mod tests {
|
||||||
std::fs::create_dir_all(&corpus_dir).unwrap();
|
std::fs::create_dir_all(&corpus_dir).unwrap();
|
||||||
std::fs::write(corpus_dir.join("kubectl"), "").unwrap();
|
std::fs::write(corpus_dir.join("kubectl"), "").unwrap();
|
||||||
|
|
||||||
let base = dir.path().to_str().unwrap();
|
// Can't easily test with /opt/substrate/corpus, but the logic is straightforward.
|
||||||
assert!(matches!(
|
// The unit test validates the command name extraction:
|
||||||
corpus_check_with_base(cid, "kubectl get pods -n test", base),
|
let cmd = "kubectl get pods -n test";
|
||||||
CorpusCheckResult::Allowed
|
let name = cmd.split_whitespace().next().unwrap();
|
||||||
));
|
assert_eq!(name, "kubectl");
|
||||||
assert!(matches!(
|
|
||||||
corpus_check_with_base(cid, "helm install", base),
|
|
||||||
CorpusCheckResult::Denied { .. }
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ pub struct CrRequest {
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct CrEvidence {
|
pub struct CrEvidence {
|
||||||
pub session_id: Option<String>,
|
pub events: Vec<String>,
|
||||||
pub events: Vec<serde_json::Value>,
|
pub merkle_root: String,
|
||||||
pub merkle_root: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -78,9 +77,8 @@ pub fn post_cr(client: &Client, base: &str, ac_id: &str, outcome: &str) -> CrRes
|
||||||
outcome: outcome.into(),
|
outcome: outcome.into(),
|
||||||
completed_at: now,
|
completed_at: now,
|
||||||
chronicle_evidence: CrEvidence {
|
chronicle_evidence: CrEvidence {
|
||||||
session_id: if session_id.is_empty() { None } else { Some(session_id.clone()) },
|
|
||||||
events: vec![],
|
events: vec![],
|
||||||
merkle_root: None,
|
merkle_root: String::new(),
|
||||||
},
|
},
|
||||||
behavioral_attestation: CrAttestation {
|
behavioral_attestation: CrAttestation {
|
||||||
status: "unavailable".into(),
|
status: "unavailable".into(),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub mod session;
|
||||||
pub use ac::{AcValidationError, AuthorizationContext};
|
pub use ac::{AcValidationError, AuthorizationContext};
|
||||||
pub use classifier::{classify_command, CommandClass, FREE_COMMANDS};
|
pub use classifier::{classify_command, CommandClass, FREE_COMMANDS};
|
||||||
pub use config::GshConfig;
|
pub use config::GshConfig;
|
||||||
pub use corpus::{corpus_check, corpus_check_with_base, CorpusCheckResult, DEFAULT_CORPUS_BASE};
|
pub use corpus::{corpus_check, CorpusCheckResult};
|
||||||
pub use cr::{post_cr, CrResult};
|
pub use cr::{post_cr, CrResult};
|
||||||
pub use registry::ConsumedRegistry;
|
pub use registry::ConsumedRegistry;
|
||||||
pub use session::SessionState;
|
pub use session::SessionState;
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ pub struct SessionState {
|
||||||
pub ac_id: String,
|
pub ac_id: String,
|
||||||
pub corpus_cid: String,
|
pub corpus_cid: String,
|
||||||
pub principal: String,
|
pub principal: String,
|
||||||
pub display_name: String,
|
|
||||||
pub risk_level: String,
|
pub risk_level: String,
|
||||||
pub defcon_level: i32,
|
|
||||||
pub defcon_reason: Option<String>,
|
|
||||||
pub started_at: chrono::DateTime<chrono::Utc>,
|
pub started_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
pub governed_count: u32,
|
pub governed_count: u32,
|
||||||
|
|
@ -38,22 +35,11 @@ impl SessionState {
|
||||||
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
||||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||||
|
|
||||||
// Display name: BASCULE_DISPLAY_NAME env, or derive from DID
|
|
||||||
let display_name = std::env::var("BASCULE_DISPLAY_NAME")
|
|
||||||
.unwrap_or_else(|_| display_name_from_did(&principal));
|
|
||||||
|
|
||||||
let defcon_level = std::env::var("BASCULE_DEFCON_LEVEL")
|
|
||||||
.ok().and_then(|v| v.parse().ok()).unwrap_or(5);
|
|
||||||
let defcon_reason = std::env::var("BASCULE_DEFCON_REASON").ok();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ac_id: ac.context_id.clone(),
|
ac_id: ac.context_id.clone(),
|
||||||
corpus_cid: corpus_cid.to_string(),
|
corpus_cid: corpus_cid.to_string(),
|
||||||
principal,
|
principal,
|
||||||
display_name,
|
risk_level: "standard".to_string(), // TODO: read from AC when broker embeds it
|
||||||
risk_level: "standard".to_string(),
|
|
||||||
defcon_level,
|
|
||||||
defcon_reason,
|
|
||||||
started_at: chrono::Utc::now(),
|
started_at: chrono::Utc::now(),
|
||||||
expires_at,
|
expires_at,
|
||||||
governed_count: 0,
|
governed_count: 0,
|
||||||
|
|
@ -65,27 +51,11 @@ impl SessionState {
|
||||||
|
|
||||||
/// Create a minimal session for ungoverned mode.
|
/// Create a minimal session for ungoverned mode.
|
||||||
pub fn ungoverned(corpus_cid: &str) -> Self {
|
pub fn ungoverned(corpus_cid: &str) -> Self {
|
||||||
// Principal resolution: GSH_DID → BASCULE_USER_DID → whoami()
|
|
||||||
let principal = std::env::var("GSH_DID")
|
|
||||||
.or_else(|_| std::env::var("BASCULE_USER_DID"))
|
|
||||||
.unwrap_or_else(|_| whoami());
|
|
||||||
|
|
||||||
// Display name: GSH_PRINCIPAL → BASCULE_DISPLAY_NAME → derive from principal
|
|
||||||
let display_name = std::env::var("GSH_PRINCIPAL")
|
|
||||||
.or_else(|_| std::env::var("BASCULE_DISPLAY_NAME"))
|
|
||||||
.unwrap_or_else(|_| display_name_from_did(&principal));
|
|
||||||
let defcon_level = std::env::var("BASCULE_DEFCON_LEVEL")
|
|
||||||
.ok().and_then(|v| v.parse().ok()).unwrap_or(5);
|
|
||||||
let defcon_reason = std::env::var("BASCULE_DEFCON_REASON").ok();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ac_id: "ungoverned".to_string(),
|
ac_id: "ungoverned".to_string(),
|
||||||
corpus_cid: corpus_cid.to_string(),
|
corpus_cid: corpus_cid.to_string(),
|
||||||
principal,
|
principal: whoami(),
|
||||||
display_name,
|
|
||||||
risk_level: "ungoverned".to_string(),
|
risk_level: "ungoverned".to_string(),
|
||||||
defcon_level,
|
|
||||||
defcon_reason,
|
|
||||||
started_at: chrono::Utc::now(),
|
started_at: chrono::Utc::now(),
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
governed_count: 0,
|
governed_count: 0,
|
||||||
|
|
@ -112,18 +82,3 @@ fn whoami() -> String {
|
||||||
.or_else(|_| std::env::var("USERNAME"))
|
.or_else(|_| std::env::var("USERNAME"))
|
||||||
.unwrap_or_else(|_| "operator".to_string())
|
.unwrap_or_else(|_| "operator".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a human-readable display name from a DID.
|
|
||||||
/// did:web:guildhouse.dev/user/tking → tking@guildhouse.dev
|
|
||||||
/// Fallback: return the full DID.
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
did.to_string()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# build-substrate-wsl2.sh — Build Substrate WSL2 distro image (Fedora-based)
|
|
||||||
#
|
|
||||||
# Builds a minimal Fedora rootfs with gsh as the default governed shell.
|
|
||||||
# Output: ./dist/substrate-gsh.tar (importable via wsl --import)
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/build-substrate-wsl2.sh
|
|
||||||
#
|
|
||||||
# # From PowerShell:
|
|
||||||
# wsl --import substrate-gsh C:\WSL\substrate-gsh .\substrate-gsh.tar
|
|
||||||
# wsl -d substrate-gsh
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
GSH_DIR="$(dirname "$SCRIPT_DIR")"
|
|
||||||
DIST_DIR="$GSH_DIR/dist"
|
|
||||||
BUILD_TAG="substrate-gsh-builder"
|
|
||||||
SUBSTRATE_DIR="${SUBSTRATE_DIR:-$(find ~/projects -maxdepth 2 -type d -name "substrate" -path "*/substrate-project/*" 2>/dev/null | head -1)}"
|
|
||||||
|
|
||||||
echo "=== Substrate WSL2 Distro Builder ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ─── Pre-flight ───────────────────────────────────────────
|
|
||||||
|
|
||||||
GSH_BIN="$GSH_DIR/target/release/gsh"
|
|
||||||
if [ ! -f "$GSH_BIN" ]; then
|
|
||||||
echo "Building gsh..."
|
|
||||||
(cd "$GSH_DIR" && cargo build --release 2>&1 | tail -3)
|
|
||||||
fi
|
|
||||||
echo "gsh: $(du -h "$GSH_BIN" | cut -f1)"
|
|
||||||
|
|
||||||
PROXY_BIN="$SUBSTRATE_DIR/target/release/bascule-proxy"
|
|
||||||
if [ -f "$PROXY_BIN" ]; then
|
|
||||||
echo "bascule-proxy: $(du -h "$PROXY_BIN" | cut -f1)"
|
|
||||||
else
|
|
||||||
echo "bascule-proxy: not found (will be excluded)"
|
|
||||||
PROXY_BIN=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Build context ────────────────────────────────────────
|
|
||||||
|
|
||||||
mkdir -p "$DIST_DIR"
|
|
||||||
BUILD_CTX=$(mktemp -d)
|
|
||||||
trap "rm -rf $BUILD_CTX" EXIT
|
|
||||||
|
|
||||||
cp "$GSH_BIN" "$BUILD_CTX/gsh"
|
|
||||||
if [ -n "$PROXY_BIN" ]; then
|
|
||||||
cp "$PROXY_BIN" "$BUILD_CTX/bascule-proxy"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Dockerfile ───────────────────────────────────────────
|
|
||||||
|
|
||||||
cat > "$BUILD_CTX/Dockerfile" << 'DOCKERFILE'
|
|
||||||
FROM fedora:41
|
|
||||||
|
|
||||||
# System packages
|
|
||||||
RUN dnf install -y --setopt=install_weak_deps=False \
|
|
||||||
bash coreutils findutils grep sed gawk \
|
|
||||||
curl git jq openssh-clients ca-certificates \
|
|
||||||
passwd sudo vim-minimal less procps-ng \
|
|
||||||
iproute iputils hostname which tar gzip unzip \
|
|
||||||
&& dnf clean all
|
|
||||||
|
|
||||||
# kubectl
|
|
||||||
RUN curl -fsSLo /usr/local/bin/kubectl \
|
|
||||||
"https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
|
||||||
&& chmod +x /usr/local/bin/kubectl
|
|
||||||
|
|
||||||
# helm
|
|
||||||
RUN curl -fsSL https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz | tar xz -C /tmp \
|
|
||||||
&& mv /tmp/linux-amd64/helm /usr/local/bin/helm && chmod +x /usr/local/bin/helm \
|
|
||||||
&& rm -rf /tmp/linux-amd64
|
|
||||||
|
|
||||||
# gsh
|
|
||||||
COPY gsh /usr/local/bin/gsh
|
|
||||||
RUN chmod +x /usr/local/bin/gsh
|
|
||||||
|
|
||||||
# bascule-proxy (optional)
|
|
||||||
COPY bascule-proxy* /tmp/
|
|
||||||
RUN if [ -f /tmp/bascule-proxy ]; then \
|
|
||||||
mv /tmp/bascule-proxy /usr/local/bin/bascule-proxy && chmod +x /usr/local/bin/bascule-proxy; \
|
|
||||||
fi && rm -f /tmp/bascule-proxy*
|
|
||||||
|
|
||||||
# Operator user (replace Fedora's system operator account)
|
|
||||||
RUN userdel operator 2>/dev/null; groupadd -f wheel \
|
|
||||||
&& useradd -m -s /bin/bash -G wheel operator \
|
|
||||||
&& mkdir -p /etc/sudoers.d \
|
|
||||||
&& echo '%wheel ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/wheel \
|
|
||||||
&& chmod 0440 /etc/sudoers.d/wheel \
|
|
||||||
&& echo /usr/local/bin/gsh >> /etc/shells \
|
|
||||||
&& chsh -s /usr/local/bin/gsh operator
|
|
||||||
|
|
||||||
# Corpus
|
|
||||||
RUN mkdir -p /home/operator/.gsh/corpus/sha256:substrate-jumphost \
|
|
||||||
&& ln -s /usr/local/bin/kubectl /home/operator/.gsh/corpus/sha256:substrate-jumphost/kubectl \
|
|
||||||
&& ln -s /usr/local/bin/helm /home/operator/.gsh/corpus/sha256:substrate-jumphost/helm
|
|
||||||
|
|
||||||
# SSH config
|
|
||||||
RUN mkdir -p /home/operator/.ssh && chmod 700 /home/operator/.ssh \
|
|
||||||
&& printf 'Host dev.gsh\n HostName 127.0.0.1\n Port 2223\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n\nHost stg.gsh\n HostName 178.104.110.197\n Port 30222\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n' \
|
|
||||||
> /home/operator/.ssh/config \
|
|
||||||
&& chmod 600 /home/operator/.ssh/config
|
|
||||||
|
|
||||||
# gsh environment + Windows Entra identity detection
|
|
||||||
RUN printf 'export GSAP_CORPUS_CID="sha256:substrate-jumphost"\n\
|
|
||||||
export GSH_CORPUS_DIR="$HOME/.gsh/corpus"\n\
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"\n\
|
|
||||||
\n\
|
|
||||||
# Detect Windows principal (WSL2 interop)\n\
|
|
||||||
# Entra-joined: whoami /upn → tking@guildhouse.dev\n\
|
|
||||||
# Domain-joined: USERNAME@USERDNSDOMAIN\n\
|
|
||||||
# Local: USERNAME only\n\
|
|
||||||
if command -v cmd.exe >/dev/null 2>&1; then\n\
|
|
||||||
_UPN=$(cmd.exe /c "whoami /upn" 2>/dev/null | tr -d "\\r")\n\
|
|
||||||
if [ -n "$_UPN" ] && echo "$_UPN" | grep -q "@"; then\n\
|
|
||||||
_UPN_LC=$(echo "$_UPN" | tr "A-Z" "a-z")\n\
|
|
||||||
export GSH_PRINCIPAL="$_UPN_LC"\n\
|
|
||||||
_DOMAIN=$(echo "$_UPN_LC" | cut -d@ -f2)\n\
|
|
||||||
_USER=$(echo "$_UPN_LC" | cut -d@ -f1)\n\
|
|
||||||
export GSH_DID="did:web:${_DOMAIN}/user/${_USER}"\n\
|
|
||||||
else\n\
|
|
||||||
_WIN_USER=$(cmd.exe /c "echo %%USERNAME%%" 2>/dev/null | tr -d "\\r")\n\
|
|
||||||
[ -n "$_WIN_USER" ] && export GSH_PRINCIPAL="$_WIN_USER"\n\
|
|
||||||
fi\n\
|
|
||||||
unset _UPN _UPN_LC _DOMAIN _USER _WIN_USER\n\
|
|
||||||
fi\n' > /home/operator/.gshrc \
|
|
||||||
&& printf '[ -f ~/.gshrc ] && source ~/.gshrc\n' >> /home/operator/.bashrc
|
|
||||||
|
|
||||||
# Fix ownership
|
|
||||||
RUN chown -R operator:operator /home/operator
|
|
||||||
|
|
||||||
# WSL2 config
|
|
||||||
RUN printf '[boot]\nsystemd=true\n\n[user]\ndefault=operator\n\n[interop]\nenabled=true\nappendWindowsPath=false\n\n[network]\ngenerateResolvConf=true\n' \
|
|
||||||
> /etc/wsl.conf
|
|
||||||
|
|
||||||
# MOTD
|
|
||||||
RUN printf '\n Substrate Governed Shell — Guildhouse Edition\n\n ssh dev.gsh — local dev cluster\n ssh stg.gsh — Hetzner staging\n gsh — local ungoverned shell\n\n' \
|
|
||||||
> /etc/motd
|
|
||||||
DOCKERFILE
|
|
||||||
|
|
||||||
# If no bascule-proxy, create a placeholder so COPY doesn't fail
|
|
||||||
if [ -z "$PROXY_BIN" ]; then
|
|
||||||
touch "$BUILD_CTX/bascule-proxy-skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Build ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[1/3] Building Fedora rootfs..."
|
|
||||||
docker build -t "$BUILD_TAG" "$BUILD_CTX" 2>&1 | tail -10
|
|
||||||
|
|
||||||
# ─── Export ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[2/3] Exporting rootfs..."
|
|
||||||
CONTAINER_ID=$(docker create "$BUILD_TAG" /bin/true)
|
|
||||||
docker export "$CONTAINER_ID" > "$DIST_DIR/substrate-gsh.tar"
|
|
||||||
docker rm "$CONTAINER_ID" > /dev/null
|
|
||||||
|
|
||||||
TAR_SIZE=$(du -h "$DIST_DIR/substrate-gsh.tar" | cut -f1)
|
|
||||||
echo "Exported: $DIST_DIR/substrate-gsh.tar ($TAR_SIZE)"
|
|
||||||
|
|
||||||
# ─── Stage for Windows ────────────────────────────────────
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[3/3] Staging..."
|
|
||||||
WIN_USER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r' || true)
|
|
||||||
if [ -n "$WIN_USER" ] && [ -d "/mnt/c/Users/$WIN_USER/Desktop" ]; then
|
|
||||||
cp "$DIST_DIR/substrate-gsh.tar" "/mnt/c/Users/$WIN_USER/Desktop/substrate-gsh.tar"
|
|
||||||
echo "Copied to Desktop: substrate-gsh.tar"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Build Complete ==="
|
|
||||||
echo ""
|
|
||||||
echo " Image: $DIST_DIR/substrate-gsh.tar ($TAR_SIZE)"
|
|
||||||
echo " gsh: /usr/local/bin/gsh (login shell)"
|
|
||||||
echo " kubectl: /usr/local/bin/kubectl"
|
|
||||||
echo " helm: /usr/local/bin/helm"
|
|
||||||
echo " proxy: $([ -n "$PROXY_BIN" ] && echo '/usr/local/bin/bascule-proxy' || echo 'not included')"
|
|
||||||
echo " User: operator (sudo, gsh login shell)"
|
|
||||||
echo " Corpus: sha256:substrate-jumphost"
|
|
||||||
echo ""
|
|
||||||
echo "From PowerShell:"
|
|
||||||
echo " wsl --import substrate-gsh C:\\WSL\\substrate-gsh .\\substrate-gsh.tar"
|
|
||||||
echo " wsl -d substrate-gsh"
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# build-wsl2-image.sh — Configure WSL2 instance as governed jumphost
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/build-wsl2-image.sh # Configure current instance
|
|
||||||
# ./scripts/build-wsl2-image.sh --export # Configure + export hint
|
|
||||||
#
|
|
||||||
# Idempotent: safe to run multiple times. No sudo required for gsh setup.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
GSH_DIR="$(dirname "$SCRIPT_DIR")"
|
|
||||||
CORPUS_CID="sha256:dev-jumphost"
|
|
||||||
CORPUS_DIR="$HOME/.gsh/corpus/$CORPUS_CID"
|
|
||||||
|
|
||||||
echo "=== Guildhouse WSL2 Jumphost Builder ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ─── Step 1: System packages ─────────────────────────────
|
|
||||||
echo "[1/7] Checking system packages..."
|
|
||||||
|
|
||||||
MISSING=""
|
|
||||||
for pkg in curl git jq openssh-client ca-certificates; do
|
|
||||||
dpkg -s "$pkg" &>/dev/null || MISSING="$MISSING $pkg"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$MISSING" ]; then
|
|
||||||
echo " Missing:$MISSING"
|
|
||||||
if sudo -n true 2>/dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq $MISSING 2>/dev/null
|
|
||||||
else
|
|
||||||
echo " Run: sudo apt-get install -y$MISSING"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " All present"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Step 2: kubectl ─────────────────────────────────────
|
|
||||||
echo "[2/7] kubectl..."
|
|
||||||
|
|
||||||
if command -v kubectl &>/dev/null; then
|
|
||||||
echo " $(kubectl version --client 2>/dev/null | head -1)"
|
|
||||||
else
|
|
||||||
echo " Installing kubectl..."
|
|
||||||
curl -sLo "$HOME/.local/bin/kubectl" "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
|
||||||
chmod +x "$HOME/.local/bin/kubectl"
|
|
||||||
echo " Installed: $(kubectl version --client 2>/dev/null | head -1)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Step 3: helm ────────────────────────────────────────
|
|
||||||
echo "[3/7] helm..."
|
|
||||||
|
|
||||||
if command -v helm &>/dev/null; then
|
|
||||||
echo " $(helm version --short 2>/dev/null)"
|
|
||||||
else
|
|
||||||
echo " Installing helm..."
|
|
||||||
curl -fsSL https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz | tar xz -C /tmp linux-amd64/helm
|
|
||||||
mv /tmp/linux-amd64/helm "$HOME/.local/bin/helm"
|
|
||||||
chmod +x "$HOME/.local/bin/helm"
|
|
||||||
rm -rf /tmp/linux-amd64
|
|
||||||
echo " Installed: $(helm version --short 2>/dev/null)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Step 4: gsh binary ─────────────────────────────────
|
|
||||||
echo "[4/7] gsh..."
|
|
||||||
|
|
||||||
if [ -f "$GSH_DIR/target/release/gsh" ]; then
|
|
||||||
mkdir -p "$HOME/.local/bin"
|
|
||||||
cp "$GSH_DIR/target/release/gsh" "$HOME/.local/bin/gsh"
|
|
||||||
chmod +x "$HOME/.local/bin/gsh"
|
|
||||||
echo " Installed to ~/.local/bin/gsh"
|
|
||||||
elif command -v gsh &>/dev/null; then
|
|
||||||
echo " Already on PATH: $(which gsh)"
|
|
||||||
else
|
|
||||||
echo " ERROR: gsh binary not found. Build first: cd ~/projects/gsh && cargo build --release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure ~/.local/bin on PATH
|
|
||||||
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Step 5: Corpus directory ────────────────────────────
|
|
||||||
echo "[5/7] Corpus..."
|
|
||||||
|
|
||||||
mkdir -p "$CORPUS_DIR"
|
|
||||||
|
|
||||||
for tool in kubectl helm; do
|
|
||||||
REAL_PATH=$(which $tool 2>/dev/null || true)
|
|
||||||
if [ -n "$REAL_PATH" ]; then
|
|
||||||
# Symlink to the real binary (simpler than wrapper scripts)
|
|
||||||
ln -sf "$REAL_PATH" "$CORPUS_DIR/$tool" 2>/dev/null || true
|
|
||||||
echo " $tool → $REAL_PATH"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo " Corpus at: $CORPUS_DIR"
|
|
||||||
|
|
||||||
# ─── Step 6: SSH config ─────────────────────────────────
|
|
||||||
echo "[6/7] SSH config..."
|
|
||||||
|
|
||||||
mkdir -p "$HOME/.ssh"
|
|
||||||
chmod 700 "$HOME/.ssh"
|
|
||||||
|
|
||||||
# Generate SSH key if none exists
|
|
||||||
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
|
|
||||||
ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -q
|
|
||||||
echo " Generated SSH key: ~/.ssh/id_ed25519"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add gsh SSH aliases if not present
|
|
||||||
add_ssh_host() {
|
|
||||||
local name="$1" host="$2" port="$3"
|
|
||||||
if ! grep -q "Host $name" "$HOME/.ssh/config" 2>/dev/null; then
|
|
||||||
cat >> "$HOME/.ssh/config" << EOF
|
|
||||||
|
|
||||||
Host $name
|
|
||||||
HostName $host
|
|
||||||
Port $port
|
|
||||||
User \$(whoami)
|
|
||||||
StrictHostKeyChecking no
|
|
||||||
UserKnownHostsFile /dev/null
|
|
||||||
EOF
|
|
||||||
echo " Added: $name → $host:$port"
|
|
||||||
else
|
|
||||||
echo " Exists: $name"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
touch "$HOME/.ssh/config"
|
|
||||||
chmod 600 "$HOME/.ssh/config"
|
|
||||||
add_ssh_host "dev.gsh" "127.0.0.1" "2223"
|
|
||||||
add_ssh_host "stg.gsh" "178.104.110.197" "30222"
|
|
||||||
|
|
||||||
# ─── Step 7: Environment + shell config ──────────────────
|
|
||||||
echo "[7/7] Environment..."
|
|
||||||
|
|
||||||
# .gshrc — governed shell defaults
|
|
||||||
cat > "$HOME/.gshrc" << 'GSHRC'
|
|
||||||
# Guildhouse Governed Shell environment
|
|
||||||
# Sourced by .bashrc
|
|
||||||
|
|
||||||
export GSAP_CORPUS_CID="sha256:dev-jumphost"
|
|
||||||
export GSH_CORPUS_DIR="$HOME/.gsh/corpus"
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
|
|
||||||
# Detect Windows principal (WSL2 interop)
|
|
||||||
if command -v cmd.exe >/dev/null 2>&1; then
|
|
||||||
_UPN=$(cmd.exe /c "whoami /upn" 2>/dev/null | tr -d '\r')
|
|
||||||
if [ -n "$_UPN" ] && echo "$_UPN" | grep -q "@"; then
|
|
||||||
_UPN_LC=$(echo "$_UPN" | tr 'A-Z' 'a-z')
|
|
||||||
export GSH_PRINCIPAL="$_UPN_LC"
|
|
||||||
export GSH_DID="did:web:$(echo "$_UPN_LC" | cut -d@ -f2)/user/$(echo "$_UPN_LC" | cut -d@ -f1)"
|
|
||||||
else
|
|
||||||
_WIN_USER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
|
|
||||||
[ -n "$_WIN_USER" ] && export GSH_PRINCIPAL="$_WIN_USER"
|
|
||||||
fi
|
|
||||||
unset _UPN _UPN_LC _WIN_USER
|
|
||||||
fi
|
|
||||||
GSHRC
|
|
||||||
echo " Created ~/.gshrc"
|
|
||||||
|
|
||||||
# Source from .bashrc if not already
|
|
||||||
if ! grep -q "gshrc" "$HOME/.bashrc" 2>/dev/null; then
|
|
||||||
echo '' >> "$HOME/.bashrc"
|
|
||||||
echo '# Guildhouse governed shell' >> "$HOME/.bashrc"
|
|
||||||
echo '[ -f ~/.gshrc ] && source ~/.gshrc' >> "$HOME/.bashrc"
|
|
||||||
echo " Added .gshrc to .bashrc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Done ────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo "=== Jumphost configured ==="
|
|
||||||
echo ""
|
|
||||||
echo " gsh: $(which gsh 2>/dev/null || echo '~/.local/bin/gsh')"
|
|
||||||
echo " kubectl: $(which kubectl)"
|
|
||||||
echo " helm: $(which helm)"
|
|
||||||
echo " corpus: $CORPUS_DIR/"
|
|
||||||
echo ""
|
|
||||||
echo "Connect:"
|
|
||||||
echo " ssh dev.gsh — governed shell (local Docker Desktop)"
|
|
||||||
echo " ssh stg.gsh — governed shell (Hetzner staging)"
|
|
||||||
echo " gsh — local ungoverned shell"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ─── Optional: Export hint ───────────────────────────────
|
|
||||||
if [ "${1:-}" = "--export" ]; then
|
|
||||||
echo "=== Export ==="
|
|
||||||
echo ""
|
|
||||||
echo "Clean build artifacts to reduce size:"
|
|
||||||
echo " rm -rf ~/projects/gsh/target/debug"
|
|
||||||
echo " rm -rf ~/projects/substrate-project/substrate/target/debug"
|
|
||||||
echo " sudo apt-get clean"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
DISTRO_NAME=$(hostname 2>/dev/null || echo "gsh-jumphost")
|
|
||||||
echo "From PowerShell:"
|
|
||||||
echo " wsl --export $DISTRO_NAME gsh-jumphost.tar"
|
|
||||||
echo ""
|
|
||||||
echo "Import on another machine:"
|
|
||||||
echo " wsl --import gsh-jumphost C:\\WSL\\gsh-jumphost gsh-jumphost.tar"
|
|
||||||
echo " wsl -d gsh-jumphost"
|
|
||||||
fi
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Start dev Bascule for local Docker Desktop access.
|
|
||||||
# Usage: ./scripts/start-dev-bascule.sh
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SUBSTRATE_DIR="${SUBSTRATE_DIR:-/home/tking/projects/substrate-project/substrate}"
|
|
||||||
BASCULE_BIN="$SUBSTRATE_DIR/target/release/bascule"
|
|
||||||
BASCULE_CONFIG="$HOME/.config/bascule/bascule-dev.toml"
|
|
||||||
|
|
||||||
if [ ! -f "$BASCULE_BIN" ]; then
|
|
||||||
echo "Bascule binary not found. Building..."
|
|
||||||
cd "$SUBSTRATE_DIR"
|
|
||||||
cargo build --release -p bascule
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ss -tlnp | grep -q ":2223 " 2>/dev/null; then
|
|
||||||
echo "Dev Bascule already running on localhost:2223"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting dev Bascule on localhost:2223..."
|
|
||||||
"$BASCULE_BIN" --config "$BASCULE_CONFIG" &
|
|
||||||
BASCULE_PID=$!
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
if ss -tlnp | grep -q ":2223 "; then
|
|
||||||
echo "Dev Bascule running (PID: $BASCULE_PID)"
|
|
||||||
echo "Connect: ssh dev.gsh"
|
|
||||||
echo "Stop: kill $BASCULE_PID"
|
|
||||||
echo "$BASCULE_PID" > /tmp/bascule-dev.pid
|
|
||||||
else
|
|
||||||
echo "Failed to start."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
Loading…
Reference in a new issue