gsh/scripts/bootstrap-jumphost.sh
Tyler J King 63a6c0c520 feat: gsh human mode — interactive governed shell with reedline
Phase 3 / Sprint 2 finish line.

Human mode: reedline REPL with governed prompt.
  [governed] tyler@gsh:~$

Mode detection:
  --exec "cmd"              → machine mode (unchanged)
  --ungoverned --exec "cmd" → ungoverned machine (unchanged)
  (no --exec, TTY attached) → human mode (NEW)
  (no --exec, no TTY)       → error

Command classification per-keystroke (libgsh/classifier.rs):
  Free:       ls, cat, grep, echo, cd, git, ssh, curl — zero overhead
  Governed:   binaries in corpus dir — via org-ops wrapper, CR posted
  Ungoverned: not in corpus but on PATH — warn + execute
  Denied:     corpus manifest but removed — killswitch active

Session lifecycle:
  Start:  validate AC, post SESSION_STARTED CR, print banner
  Active: classify each command, governed ops post lightweight CRs
  End:    print summary (governed/free/denied/ungoverned), post SESSION_ENDED CR

Banner: principal, corpus, session ID, expiry, risk level
Prompt coloring from risk level:
  Baseline/Standard: green [governed]
  Elevated:          yellow [elevated]
  High/Critical:     red [HIGH]

New modules:
  libgsh/classifier.rs — command classification against corpus (4 tests)
  libgsh/session.rs    — session state tracking
  gsh/human.rs         — reedline REPL, prompt, banner, summary

Machine mode: zero changes (regression tested).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:44:34 -04:00

173 lines
5.8 KiB
Bash
Executable file

#!/bin/bash
# bootstrap-jumphost.sh
# Configure this WSL2 instance as a governed jumphost.
# Run once. Idempotent.
set -euo pipefail
SUBSTRATE_ROOT="/opt/substrate"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSH_ROOT="$(dirname "$SCRIPT_DIR")"
GSH_BIN="$GSH_ROOT/target/release/gsh"
DEV_CORPUS_CID="sha256:dev-jumphost"
echo "=== Governed Jumphost Bootstrap ==="
echo "Substrate root: $SUBSTRATE_ROOT"
echo "gsh binary: $GSH_BIN"
echo ""
# ── Directory structure ───────────────────────────────────────
echo "Creating directory structure..."
sudo mkdir -p "$SUBSTRATE_ROOT/corpus"
sudo mkdir -p "$SUBSTRATE_ROOT/org-ops/bin"
sudo mkdir -p "$SUBSTRATE_ROOT/org-ops/lib"
sudo mkdir -p "$SUBSTRATE_ROOT/profiles"
sudo chown -R "$(whoami)" "$SUBSTRATE_ROOT"
# ── Shared governance library ─────────────────────────────────
cat > "$SUBSTRATE_ROOT/org-ops/lib/governance.sh" << 'GOVLIB'
# governance.sh — shared functions for org-ops wrappers
# Sourced by every governed wrapper.
_gov_params_cid() {
echo -n "$*" | sha256sum | awk '{print "sha256:"$1}'
}
_gov_log() {
local binary="$1" exit_code="$2" params_cid="$3"
local session_id="${BASCULE_SESSION_ID:-ungoverned}"
if [ "$session_id" != "ungoverned" ]; then
>&2 printf '{"governance":"executed","binary":"%s","params_cid":"%s","session_id":"%s","exit_code":%d}\n' \
"$binary" "$params_cid" "$session_id" "$exit_code"
fi
}
GOVLIB
echo " governance.sh installed"
# ── Org-ops wrappers ─────────────────────────────────────────
write_wrapper() {
local name="$1" real_path="$2"
local wrapper="$SUBSTRATE_ROOT/org-ops/bin/${name}-governed"
cat > "$wrapper" << WRAPPER
#!/bin/bash
# ${name}-governed — governance wrapper for $name
# Generated by bootstrap-jumphost.sh
set -euo pipefail
source "$SUBSTRATE_ROOT/org-ops/lib/governance.sh"
PARAMS_CID=\$(_gov_params_cid "\$@")
$real_path "\$@"
EXIT_CODE=\$?
_gov_log "$name" "\$EXIT_CODE" "\$PARAMS_CID"
exit \$EXIT_CODE
WRAPPER
chmod +x "$wrapper"
echo " ${name}-governed → $real_path"
}
echo "Installing org-ops wrappers..."
write_wrapper "kubectl" "/usr/local/bin/kubectl"
write_wrapper "helm" "/usr/local/bin/helm"
write_wrapper "hcloud" "/tmp/hcloud"
write_wrapper "ansible-playbook" "/usr/bin/ansible-playbook"
# ── Dev corpus ────────────────────────────────────────────────
echo "Creating dev corpus ($DEV_CORPUS_CID)..."
mkdir -p "$SUBSTRATE_ROOT/corpus/$DEV_CORPUS_CID"
for tool in kubectl helm hcloud ansible-playbook; do
ln -sf "$SUBSTRATE_ROOT/org-ops/bin/${tool}-governed" \
"$SUBSTRATE_ROOT/corpus/$DEV_CORPUS_CID/$tool"
done
echo " Binaries linked into corpus"
# ── Capability profile ────────────────────────────────────────
cat > "$SUBSTRATE_ROOT/profiles/$DEV_CORPUS_CID.json" << 'PROFILE'
{
"corpus_id": "dev-jumphost",
"corpus_entry_cid": "sha256:dev-jumphost",
"classification": "development",
"risk_level": "elevated",
"binaries": [
{
"name": "kubectl",
"layer_classifications": ["applications"],
"access_capabilities": ["read", "write"],
"wrapped_by": "org-ops/kubectl-governed",
"real_path": "/usr/local/bin/kubectl"
},
{
"name": "helm",
"layer_classifications": ["applications"],
"access_capabilities": ["read", "write"],
"wrapped_by": "org-ops/helm-governed",
"real_path": "/usr/local/bin/helm"
},
{
"name": "hcloud",
"layer_classifications": ["systems"],
"access_capabilities": ["read", "write"],
"wrapped_by": "org-ops/hcloud-governed",
"real_path": "/tmp/hcloud"
},
{
"name": "ansible-playbook",
"layer_classifications": ["systems", "applications", "network"],
"access_capabilities": ["read", "write"],
"wrapped_by": "org-ops/ansible-governed",
"real_path": "/usr/bin/ansible-playbook"
}
]
}
PROFILE
echo " Capability profile written"
# ── Install gsh ───────────────────────────────────────────────
if [ -f "$GSH_BIN" ]; then
sudo cp "$GSH_BIN" /usr/local/bin/gsh
echo " gsh installed to /usr/local/bin/gsh"
else
echo " WARNING: gsh binary not found at $GSH_BIN"
echo " Run: cd $GSH_ROOT && cargo build --release"
fi
# ── Environment setup ─────────────────────────────────────────
GSH_ENV_FILE="$HOME/.gsh-env"
cat > "$GSH_ENV_FILE" << ENV
# Governed shell environment — sourced by .bashrc
export GSAP_CORPUS_CID="$DEV_CORPUS_CID"
export GSAP_BROKER_URL="\${GSAP_BROKER_URL:-}"
export PATH="/opt/substrate/corpus/$DEV_CORPUS_CID:\$PATH"
ENV
# Add to .bashrc if not already there:
if ! grep -q "gsh-env" "$HOME/.bashrc" 2>/dev/null; then
echo "" >> "$HOME/.bashrc"
echo "# Governed shell environment" >> "$HOME/.bashrc"
echo "[ -f $GSH_ENV_FILE ] && source $GSH_ENV_FILE" >> "$HOME/.bashrc"
echo " .bashrc updated to source gsh-env"
else
echo " .bashrc already sources gsh-env"
fi
echo ""
echo "=== Bootstrap Complete ==="
echo ""
echo "Corpus: $DEV_CORPUS_CID"
echo "Profile: $SUBSTRATE_ROOT/profiles/$DEV_CORPUS_CID.json"
echo ""
echo "Usage:"
echo " source ~/.gsh-env"
echo " gsh --ungoverned --exec 'kubectl get nodes'"
echo " gsh --exec 'kubectl --context docker-desktop get nodes'"
echo ""
echo "For governed mode, set GSAP_BROKER_URL and GSAP_TOKEN."