Commit graph

10 commits

Author SHA256 Message Date
Tyler J King
5f7f9c0ff7 feat: configurable corpus base dir + Bascule dev config
- corpus_check_with_base(): accepts explicit base directory
- corpus_check(): still defaults to /opt/substrate/corpus
- Improved corpus test with actual Allowed/Denied assertions
- Updated bascule-dev.toml with [gsap] section and shell_command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:46:27 -04:00
Tyler J King
fcc7758249 feat: dev Bascule + dual-cluster connectivity complete
Phase 4b: local dev Bascule for Docker Desktop K8s access.

Dev Bascule:
  Binary: substrate/target/release/bascule (14MB)
  Config: ~/.config/bascule/bascule-dev.toml
    Permissive auth, direct dispatch, localhost:2223
  Keys: ~/.config/bascule/keys/dev_{host,ca}_key
  Startup: scripts/start-dev-bascule.sh

Dual-cluster connectivity verified:
  ssh dev.gsh '!whoami'
    → session created, did:web:guildhouse.dev/user/tyler ✓
  ssh stg.gsh '!whoami'
    → session created, did:web:guildhouse.dev/user/tyler ✓

Topology:
  WSL2 → dev.gsh  (localhost:2223, permissive)
  WSL2 → stg.gsh  (178.104.110.197:30222, Hetzner)
  WSL2 → prod.gsh (178.104.110.197:30222, Hetzner)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:37:52 -04:00
Tyler J King
0adcf12e78 feat: Phase 4 — Bascule dual-cluster connectivity
Hetzner Bascule: already deployed (pod 756dccc486-wwg78, 5d uptime).
  Exposed via NodePort 30222 on all worker nodes.
  SSH responds: russh_0.46.0, session created, DID resolved.

Connectivity verified from WSL2:
  ssh stg.gsh '!whoami'
  → session: 019d4fd5-..., did: did:web:guildhouse.dev/user/tyler
  → tier: ReadOnly, roles: ["operator"]

Config files:
  config/bascule-dev.toml    — permissive auth, localhost:2223
  config/bascule-hetzner.toml — reference for Hetzner NodePort endpoints

bascule-proxy built and installed (~/.local/bin/).
  Config at ~/.config/bascule/config.toml
  Hosts: dev (localhost:2223), stg/prod (178.104.110.197:30222)

SSH config: stg.gsh and prod.gsh aliases configured.

The full chain: WSL2 → SSH → Bascule (Hetzner) → session + DID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:14:51 -04:00
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
Tyler J King
919d8accde refactor: extract libgsh from monolith
Phase 2 of the WSL2 jumphost build.

Workspace: gsh/ (binary) + libgsh/ (library).

libgsh modules:
  ac.rs       — AC validation (R-22 single-use, R-23 corpus match, expiry)
  cr.rs       — CR construction + broker posting + inline AC request
  corpus.rs   — Corpus directory gate (killswitch)
  config.rs   — GshConfig from environment
  registry.rs — Filesystem-based consumed AC registry

gsh/src/main.rs: CLI only (~170 lines).
  Clap args, mode detection, calls libgsh, formats output.

11 unit tests in libgsh:
  ac: valid AC, expired, corpus mismatch, replay, missing context_id
  cr: broker URL formatting
  corpus: ungoverned skip, missing dir, command name extraction
  registry: consume and check
  config: default corpus_cid

Zero behavior change. Same JSON output, same exit codes,
same flags, same env vars, same broker interaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:31:50 -04:00
Tyler J King
af11a797ee feat: per-session AC consumption + corpus gate + exit codes
Phase 1 of the WSL2 jumphost build.

Three execution models:
  1. Pre-issued AC: GSAP_AC='...' gsh --exec "cmd"
     Caller provides AC. gsh validates (R-22/23/24), executes, posts CR.
     For: Bascule, SK plugin, CI/CD.

  2. Inline AC request: GSAP_BROKER_URL=... gsh --exec "cmd"
     Backward compatible fallback.

  3. Ungoverned: gsh --ungoverned --exec "cmd"
     No AC, no CR, no corpus check. Dev mode.

AC validation (validate_pre_issued_ac):
  R-22: Single-use — filesystem registry at ~/.gsh/consumed/{context_id}
  R-23: Corpus match — AC corpus_entry_cid vs GSAP_CORPUS_CID env
  R-24: (parameters_cid field parsed, verification at broker)
  Expiry check — AC expires_at vs now
  Replay detection — consumed context_ids rejected

Corpus directory gate (corpus_check):
  /opt/substrate/corpus/{cid}/{command_name}
  If binary missing from corpus dir → denied (exit 3)
  The live killswitch: remove binary from corpus dir to revoke

Exit codes aligned with DESIGN.md:
  0 = success, 1 = exec failure, 2 = auth failure,
  3 = governance violation, 125 = gsh internal error

JSON output: new fields ac_mode ("pre-issued"|"inline"|"session"|"ungoverned"), corpus_cid

Tested against live fastapi-gsap broker:
  Inline AC: backward compat ✓
  Pre-issued AC from broker: validated + CR posted ✓
  Expired AC: exit 2 ✓
  Replay detection: exit 2 ✓
  Ungoverned mode: no governance overhead ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:07:45 -04:00
Tyler J King
2f9401d3c4 feat: session mode — one AC for N commands
Per-invocation AC is the primitive for single governed ops.
Session mode is for scripts, pipelines, and interactive shells.

Per-invocation (unchanged):
  gsh --exec "cmd"  →  1 AC + 1 CR per command

Session mode (new):
  eval "$(gsh session-start --scope shell:session)"
  gsh --exec "cmd1"  # reuses session AC
  gsh --exec "cmd2"
  eval "$(gsh session-end)"

Detection: GSAP_SESSION_AC in environment.
Subcommands: session-start, session-end, session-status

Known gap: broker currently marks AC consumed after first CR.
Session commands 2+ get 404 on CR. This is a broker-side fix
(needs session AC type). gsh handles it gracefully.

Tested against live fastapi-gsap Spoke on Hetzner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:17:36 -04:00
Tyler J King
eab034f0cc feat: gsh machine mode — first governed shell execution
~200 lines of Rust. Every command: AC → exec → CR → CID.

Usage:
  gsh --exec "echo hello"
  gsh --exec "hcloud server list" --json
  gsh --exec "ansible-playbook site.yml" --dry-run

Flow:
  1. SHA-256 hash the command
  2. POST /governance/authorize/ → AC ID
  3. exec(sh, -c, command) → capture stdout/stderr/exit
  4. POST /governance/complete/ → receipt + Chronicle CID
  5. Print stdout (passthrough) or JSON (structured)
  6. Exit with command's exit code

Environment:
  GSAP_BROKER_URL   http://fastapi-gsap:8000
  GSAP_AGENT_DID    did:web:bxnet.../agent/platform-ops
  GSAP_TOKEN        Bearer token (optional)
  GSAP_CORPUS_CID   sha256:{image_digest} (optional)

Tested against live fastapi-gsap Spoke broker on Hetzner:
  dry-run: AC only ✓
  live exec: stdout passthrough + CID ✓
  JSON mode: ac_id + cr_id + chronicle_cid ✓
  exit code: 42 passed through ✓

The command_hash in the AC request means the broker knows
WHAT will be executed before authorizing. Not just "was
this agent allowed" but "was this exact command authorized."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:01:22 -04:00
Tyler J King
3a2ed1ed42 feat: gsh governed shell — design exploration
DESIGN.md: complete architecture exploration for gsh,
the GCAP governed shell binary.

Two modes:
  Machine: headless JSON I/O, GSAP AC consumption,
    CR posting, exit code governance mapping.
    Auto-detected: no TTY → machine mode.
    What SK plugin and Logic Apps need now.
  Human: interactive, [governed] prompt,
    inline elevation, session-level AC.
    What Sam needs for daily ops.

Architecture: gsh binary + libgsh library.
  common/: AC validation, CR posting, Chronicle env.
  machine/: headless executor.
  human/: reedline shell, prompt, interceptor.

6 open design questions documented.
MVP: machine mode first (~200 lines Rust).

Before building: resolve Q2 (session vs per-command AC)
and Q6 (full shell vs bash wrapper).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:20:27 -04:00
6833d34e68 Initial commit 2026-03-31 03:15:52 +00:00