Replace local 263-line ShellTier enum + 16 tests with a single
re-export from substrate-session. All generic ShellTier behavior
(satisfies, parent, from_shell_class, from_numeric) lives in the
canonical crate. 51 gsh tests passing.
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>
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>
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>
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>
~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>