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>
173 lines
5.8 KiB
Bash
Executable file
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."
|