Commit graph

8 commits

Author SHA256 Message Date
a97e9569d6 feat(gsh): ShellTier T0-T6 + LMDB session enrichment + GSH_SHELL_TIER
ShellTier enum (T0-T6) with tree hierarchy, satisfies(), from_shell_class()
backward compat mapping. Exported as GSH_SHELL_TIER alongside GSH_SHELL_CLASS.

SessionState carries shell_tier derived from AC shell_tier field, GSH_SHELL_TIER
env, or shell_class mapping. Prompt shows tier: [governed] T2:tking@gsh.

Optional LMDB enrichment (behind `lmdb` feature flag): reads earned credentials
and identity class from substrate-identity-store, displays in banner.

16 shell_tier tests, 3 LMDB enrichment tests, 3 governance_env tests.
66 tests without lmdb, 69 with --features lmdb.

Signed-off-by: Tyler J King <tking@guildhouse.dev>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-05-30 11:46:41 -04:00
d0b674f6cd feat(libgsh): DID-sourced capability intersection via IntersectionInput
IntersectionInput replaces separate accord lookups with a DID-document-
sourced three-way intersection: effective = posix_bounding & accord_allowed
& delegation_remaining. denied_to_allowed_mask() converts denied capability
names to a bitmask for the intersection.

Includes workspace-level cleanup and gsh binary refactoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-05-26 09:56:20 -04:00
7c84854222 feat(gsh): add --register mode and agent API for shellbound host
Adds `gsh register --service-name <name>` subcommand for systemd
ExecStartPre integration. Connects to substrate-fabric Unix socket,
sends RegisterAppShell, writes shell.env for EnvironmentFile= loading.

New libgsh modules:
- register.rs: fabric IPC client for app shell creation + env writer
- agent_api.rs: capability attenuation validation for agent sub-shells

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-05-15 05:47:29 -04:00
13b393a7f1 libgsh: verify corpus binary content before allowing execution
corpus_check() previously returned Allowed as soon as it found a file by
name in the corpus directory keyed by CID. The CID acted as a directory
label, not a content commitment. An attacker with write access to the
corpus directory could plant a malicious binary under a legitimate CID
and it would execute with that CID's authorization.

This change hashes the binary at the resolved path and compares to the
CID its directory is named for. Mismatches return a new ContentMismatch
variant; unreadable binaries return ReadFailed. Both are execution-denied
states — main.rs handles each explicitly with exit code 3 (previously
used only for Denied).

Both error classes emit Chronicle-shaped structured tracing events
(target: "chronicle") with stable event_type constants from
libgsh::chronicle_events. The field shape matches what substrate-chronicle's
post-io_uring emission API is expected to require; migration to direct
Chronicle emission becomes a mechanical translation once that API
stabilizes.

The tamper signal is that the binary and its directory name disagree.
This closes the execution-path half of the CID-content verification
audit fix — admission (corpus-operator) rejects CID forgery before the
enforcement ConfigMap is written; execution (libgsh) rejects any tamper
that landed after admission. Defense in depth across both layers.

Kernel-layer CID verification (the third layer, where eBPF LSM hooks
authorize by binary name via FNV-1a hash of comm) is explicit backlog,
deferred to Bifrost where in-kernel hashing or a ring-buffer userspace
verifier can be evaluated properly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-04-25 03:02:37 -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
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