Commit graph

12 commits

Author SHA256 Message Date
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
88840ae620 feat(libgsh): GSH_* env contract for org-ops-core child processes
Phase 1 of org-ops-core CLI standardization: gsh now exports a
discrete set of governance-context env vars to child processes so
org-ops-core (substrate-level operations library, future move) can
construct a GshContext without re-parsing the GSAP_SESSION_AC blob.

Contract:

  GSH_DID            principal.did (canonical string)
  GSH_ACCORD_HASH    accord_hash
  GSH_SHELL_CLASS    shell_class ("Application" | "System" | ...)
  GSH_POSTURE_LEVEL  posture_level (decimal 1..=5)
  GSH_CAPABILITY_SET capability_set formatted "0x{:08x}"

AC schema (libgsh::ac::AuthorizationContext) gains four optional
fields — accord_hash, shell_class, capability_set, posture_level —
all #[serde(default, skip_serializing_if = "Option::is_none")].
Existing AC producers continue working unchanged; ACs without the
new fields parse cleanly. Serialize is added to the AC structs
to enable round-trip and to let library consumers construct ACs
programmatically.

New module libgsh::governance_env exposes:
- apply(cmd, did, accord_hash, shell_class, posture_level,
       capability_set) — stateless decorator
- apply_from_ac(cmd, &AC) — convenience wrapper over apply

SessionState gains the four governance fields (populated from AC
in from_ac, left None in ungoverned). SessionState::apply_governance_env
threads them onto a child Command at REPL spawn sites.

Spawn sites updated:
- gsh::main::run (governed --exec) — retains the parsed AC and
  calls governance_env::apply_from_ac on the exec Command.
- gsh::human::execute_passthrough — now takes &SessionState;
  applies session governance env (REPL Free/Ungoverned paths).
- gsh::human::execute_governed — applies session governance env
  alongside the existing BASCULE_SESSION_ID / BASCULE_CORPUS_CID.

Legacy GSAP_SESSION_AC / GSAP_SESSION_ID / GSAP_SESSION_SCOPE exports
remain intact — the GSH_* vars are purely additive convenience for
org-ops-core. Session and inline AC modes (which surface only an
ID, not the full struct) export nothing new — same fail-soft
behaviour as before.

Tests added:
- ac::tests::test_governance_fields_round_trip — full payload
  parses and re-serializes losslessly.
- ac::tests::test_governance_fields_absent_back_compat — legacy
  AC parses without governance fields and round-trips without
  emitting them.
- governance_env::tests::apply_all_fields — every GSH_* var set.
- governance_env::tests::apply_partial_only_did — missing fields
  leave the env var unset rather than empty.
- governance_env::tests::apply_from_ac_full — end-to-end AC →
  env var application.
- governance_env::tests::apply_from_legacy_ac_no_governance_fields
  — legacy AC sets only GSH_DID, no other GSH_* vars.

24 tests pass; cargo build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-05-03 08:50:20 -04:00
f810537581 feat(libgsh): Phase 0 — typed Did on AcPrincipal
`AcPrincipal.did: Option<String>` → `Option<guildhouse_did::Did>`.
The AuthorizationContext now carries a W3C-canonical typed DID;
malformed DIDs fail at deserialize time rather than propagating
into the corpus_check / session state.

SessionState.principal stays a String — it can also hold a Unix
username in ungoverned mode, so a typed Did would force
Option<Did> there and complicate the chain. The render at
SessionState::from_ac now goes Did → as_str() instead of cloning
the legacy String. Behaviour at the audit-leaf level is
unchanged when the AC carries a valid `did:web:...` payload.

Phase 0 of DESIGN-DID-INTEGRATION-2026-04-29 §5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-05-01 06:28:19 -04:00
91f027ae61 libgsh: complete scenario coverage for corpus_check execution paths
Adds the ReadFailed scenario (binary path resolves to a directory so
exists() succeeds but read() fails) and a scenarios coverage map at the
top of the test module. The map links each test to the audit fix
scenarios:

- valid CID, content matches: Allowed
- valid CID at admission, tampered content at execution: ContentMismatch
- missing binary where directory exists: Denied (sanity preserved)
- binary present but unreadable: ReadFailed (fail-closed)

Plus the existing sentinels for ungoverned-CID and corpus-not-mounted.

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:18:56 -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
d0b9ca0e6a feat: detect Windows Entra/local principal in WSL2
Session principal resolution chain:
  GSH_PRINCIPAL → BASCULE_DISPLAY_NAME → derive from DID → whoami()
  GSH_DID → BASCULE_USER_DID → whoami()

.gshrc Windows identity detection:
  Entra-joined: whoami /upn → tking@guildhouse.dev → DID
  Domain-joined: USERNAME@USERDNSDOMAIN → DID
  Local: USERNAME only (no DID)

Governed sessions (Bascule) override with authenticated identity.
Non-WSL2 environments fall back silently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:15:05 -04:00
Tyler J King
02bcd58c99 feat: display DEFCON posture in banner + prompt
Reads BASCULE_DEFCON_LEVEL from env. At DEFCON <5:
  Banner: DEFCON level + label (RESTRICTED/CRITICAL/LOCKDOWN) + reason
  Prompt: [restricted] at DEFCON 3, [DEFCON] at ≤2

DEFCON 5 (peacetime): no DEFCON line in banner, normal prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:10:17 -04:00
Tyler J King
231bed5f92 feat: display name in banner + prompt
Banner shows human-readable principal and DID on separate lines:
  Principal: tking@guildhouse.dev
  DID:       did:web:guildhouse.dev/user/tking

Prompt uses short name: [governed] tking@gsh

Reads BASCULE_DISPLAY_NAME env. Fallback: parse DID to name@domain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:18:13 -04:00
Tyler J King
e7bc2ee2b4 fix: align CR format with broker CompleteRequest schema
- Add session_id field to CrEvidence (broker expects it)
- Change merkle_root to Option<String> (null vs empty string)
- Change events to Vec<serde_json::Value> (broker expects list[dict])
- Fixes 422 Unprocessable Entity on CR posting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:11:22 -04:00
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
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