Compare commits
10 commits
0adcf12e78
...
d0b9ca0e6a
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
d0b9ca0e6a | ||
|
|
b363d1da3b | ||
|
|
02bcd58c99 | ||
|
|
3c4042ce8e | ||
|
|
231bed5f92 | ||
|
|
ff16b5642e | ||
|
|
740fcdb3b5 | ||
|
|
e7bc2ee2b4 | ||
|
|
5f7f9c0ff7 | ||
|
|
fcc7758249 |
10 changed files with 543 additions and 22 deletions
|
|
@ -6,8 +6,15 @@ 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
Normal file
1
dist/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.tar
|
||||
|
|
@ -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, "session_started");
|
||||
let _ = post_cr(&client, base, &session.ac_id, "completed");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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_ended");
|
||||
let _ = post_cr(&client, base, &session.ac_id, "session_end");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +150,8 @@ 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.principal, "║".bright_blue());
|
||||
println!("{} Principal: {:<44}{}", "║".bright_blue(), session.display_name, "║".bright_blue());
|
||||
println!("{} DID: {:<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(),
|
||||
|
|
@ -161,6 +162,25 @@ 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!();
|
||||
}
|
||||
|
|
@ -192,17 +212,24 @@ fn print_summary(session: &SessionState) {
|
|||
}
|
||||
|
||||
fn build_prompt(session: &SessionState) -> DefaultPrompt {
|
||||
let risk_indicator = match session.risk_level.as_str() {
|
||||
// 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 user = session.principal.split('@').next().unwrap_or(&session.principal);
|
||||
let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name);
|
||||
|
||||
DefaultPrompt::new(
|
||||
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, user)),
|
||||
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, short_name)),
|
||||
DefaultPromptSegment::Empty,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,24 @@ 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("/opt/substrate/corpus").join(corpus_cid);
|
||||
let corpus_dir = Path::new(base_dir).join(corpus_cid);
|
||||
if !corpus_dir.exists() {
|
||||
return CorpusCheckResult::NotMounted;
|
||||
}
|
||||
|
|
@ -73,10 +82,14 @@ mod tests {
|
|||
std::fs::create_dir_all(&corpus_dir).unwrap();
|
||||
std::fs::write(corpus_dir.join("kubectl"), "").unwrap();
|
||||
|
||||
// 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");
|
||||
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 { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ pub struct CrRequest {
|
|||
|
||||
#[derive(Serialize)]
|
||||
pub struct CrEvidence {
|
||||
pub events: Vec<String>,
|
||||
pub merkle_root: String,
|
||||
pub session_id: Option<String>,
|
||||
pub events: Vec<serde_json::Value>,
|
||||
pub merkle_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -77,8 +78,9 @@ 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: String::new(),
|
||||
merkle_root: None,
|
||||
},
|
||||
behavioral_attestation: CrAttestation {
|
||||
status: "unavailable".into(),
|
||||
|
|
|
|||
|
|
@ -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, CorpusCheckResult};
|
||||
pub use corpus::{corpus_check, corpus_check_with_base, CorpusCheckResult, DEFAULT_CORPUS_BASE};
|
||||
pub use cr::{post_cr, CrResult};
|
||||
pub use registry::ConsumedRegistry;
|
||||
pub use session::SessionState;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ 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,
|
||||
|
|
@ -35,11 +38,22 @@ 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,
|
||||
risk_level: "standard".to_string(), // TODO: read from AC when broker embeds it
|
||||
display_name,
|
||||
risk_level: "standard".to_string(),
|
||||
defcon_level,
|
||||
defcon_reason,
|
||||
started_at: chrono::Utc::now(),
|
||||
expires_at,
|
||||
governed_count: 0,
|
||||
|
|
@ -51,11 +65,27 @@ 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: whoami(),
|
||||
principal,
|
||||
display_name,
|
||||
risk_level: "ungoverned".to_string(),
|
||||
defcon_level,
|
||||
defcon_reason,
|
||||
started_at: chrono::Utc::now(),
|
||||
expires_at: None,
|
||||
governed_count: 0,
|
||||
|
|
@ -82,3 +112,18 @@ 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()
|
||||
}
|
||||
|
|
|
|||
187
scripts/build-substrate-wsl2.sh
Executable file
187
scripts/build-substrate-wsl2.sh
Executable file
|
|
@ -0,0 +1,187 @@
|
|||
#!/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"
|
||||
205
scripts/build-wsl2-image.sh
Executable file
205
scripts/build-wsl2-image.sh
Executable file
|
|
@ -0,0 +1,205 @@
|
|||
#!/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
|
||||
34
scripts/start-dev-bascule.sh
Executable file
34
scripts/start-dev-bascule.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/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