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>
Broker now supports session-scoped ACs that stay active across
multiple CRs. Session start posts 'completed' CR, session end
posts 'session_end' CR which consumes the AC.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session start/end CRs used invalid outcome values (session_started,
session_ended) not in broker's Outcome enum, causing 422. Also, broker
consumes AC on first CR, blocking subsequent per-command CRs.
Skipped session lifecycle CRs until session-scoped AC model is
implemented. Per-command CRs still post on governed command completion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
~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>