Compare commits

..

No commits in common. "d0b9ca0e6ab7cc1360616df3d748b302565c0c9f9814a856b5d1e6cf60c55fd5" and "0adcf12e7867bb9ddf10f9c80af9dec95da462f6d671d271616fd96bf7272626" have entirely different histories.

10 changed files with 22 additions and 543 deletions

View file

@ -6,15 +6,8 @@ ca_key_path = "/dev/null"
host_key_path = "/dev/null"
dispatch_mode = "direct"
auth_mode = "permissive"
shell_command = "/home/tking/.local/bin/gsh"
[elevation]
operator_ttl_secs = 3600
admin_ttl_secs = 1800
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
View file

@ -1 +0,0 @@
*.tar

View file

@ -30,7 +30,7 @@ pub fn run_human_mode(
// Post SESSION_STARTED CR if broker available:
if let Some(ref base) = broker_url {
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:
if let Some(ref base) = broker_url {
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!("{}", "╔══════════════════════════════════════════════════════════╗".bright_blue());
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!("{} DID: {:<44}{}", "".bright_blue(), session.principal, "".bright_blue());
println!("{} Principal: {:<44}{}", "".bright_blue(), session.principal, "".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!("{} Risk: {:<44}{}", "".bright_blue(),
@ -162,25 +161,6 @@ fn print_banner(session: &SessionState) {
_ => session.risk_level.clone(),
},
"".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!();
}
@ -212,24 +192,17 @@ fn print_summary(session: &SessionState) {
}
fn build_prompt(session: &SessionState) -> DefaultPrompt {
// DEFCON overrides the risk indicator when elevated
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]",
"elevated" => "[elevated]",
"high" | "critical" => "[HIGH]",
_ => "[governed]",
}
let risk_indicator = match session.risk_level.as_str() {
"baseline" | "standard" | "ungoverned" => "[governed]",
"elevated" => "[elevated]",
"high" | "critical" => "[HIGH]",
_ => "[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(
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, short_name)),
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, user)),
DefaultPromptSegment::Empty,
)
}

View file

@ -15,24 +15,15 @@ pub enum CorpusCheckResult {
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.
///
/// `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.
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" {
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() {
return CorpusCheckResult::NotMounted;
}
@ -82,14 +73,10 @@ mod tests {
std::fs::create_dir_all(&corpus_dir).unwrap();
std::fs::write(corpus_dir.join("kubectl"), "").unwrap();
let base = dir.path().to_str().unwrap();
assert!(matches!(
corpus_check_with_base(cid, "kubectl get pods -n test", base),
CorpusCheckResult::Allowed
));
assert!(matches!(
corpus_check_with_base(cid, "helm install", base),
CorpusCheckResult::Denied { .. }
));
// Can't easily test with /opt/substrate/corpus, but the logic is straightforward.
// The unit test validates the command name extraction:
let cmd = "kubectl get pods -n test";
let name = cmd.split_whitespace().next().unwrap();
assert_eq!(name, "kubectl");
}
}

View file

@ -17,9 +17,8 @@ pub struct CrRequest {
#[derive(Serialize)]
pub struct CrEvidence {
pub session_id: Option<String>,
pub events: Vec<serde_json::Value>,
pub merkle_root: Option<String>,
pub events: Vec<String>,
pub merkle_root: String,
}
#[derive(Serialize)]
@ -78,9 +77,8 @@ pub fn post_cr(client: &Client, base: &str, ac_id: &str, outcome: &str) -> CrRes
outcome: outcome.into(),
completed_at: now,
chronicle_evidence: CrEvidence {
session_id: if session_id.is_empty() { None } else { Some(session_id.clone()) },
events: vec![],
merkle_root: None,
merkle_root: String::new(),
},
behavioral_attestation: CrAttestation {
status: "unavailable".into(),

View file

@ -9,7 +9,7 @@ pub mod session;
pub use ac::{AcValidationError, AuthorizationContext};
pub use classifier::{classify_command, CommandClass, FREE_COMMANDS};
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 registry::ConsumedRegistry;
pub use session::SessionState;

View file

@ -7,10 +7,7 @@ pub struct SessionState {
pub ac_id: String,
pub corpus_cid: String,
pub principal: String,
pub display_name: String,
pub risk_level: String,
pub defcon_level: i32,
pub defcon_reason: Option<String>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub governed_count: u32,
@ -38,22 +35,11 @@ impl SessionState {
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.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 {
ac_id: ac.context_id.clone(),
corpus_cid: corpus_cid.to_string(),
principal,
display_name,
risk_level: "standard".to_string(),
defcon_level,
defcon_reason,
risk_level: "standard".to_string(), // TODO: read from AC when broker embeds it
started_at: chrono::Utc::now(),
expires_at,
governed_count: 0,
@ -65,27 +51,11 @@ impl SessionState {
/// Create a minimal session for ungoverned mode.
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 {
ac_id: "ungoverned".to_string(),
corpus_cid: corpus_cid.to_string(),
principal,
display_name,
principal: whoami(),
risk_level: "ungoverned".to_string(),
defcon_level,
defcon_reason,
started_at: chrono::Utc::now(),
expires_at: None,
governed_count: 0,
@ -112,18 +82,3 @@ fn whoami() -> String {
.or_else(|_| std::env::var("USERNAME"))
.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()
}

View file

@ -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"

View file

@ -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

View file

@ -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