Commit graph

19 commits

Author SHA256 Message Date
31b76f2817 chore(workspace): fix cross-workspace path deps + document naming violation
Some checks failed
CI / dco (push) Has been cancelled
CI / build (push) Has been cancelled
Three path deps in this workspace's Cargo.toml (substrate-proto,
hfl-types, substrate-hfl) pointed at `../substrate-project/substrate/
crates/...`, which resolves correctly only when bascule-oss sits as a
sibling to a substrate-project-named directory at the parent level.
That's not the current layout: bascule-oss is now nested INSIDE the
substrate-project umbrella, as a working tree alongside substrate/
and other Guildhouse repos. The paths don't resolve in the current
nested layout; bascule-oss has been locally unbuildable since the
reorg (existing Cargo.lock survived only because nobody ran
`cargo update` or `cargo generate-lockfile` to force re-resolution).

Fix: update the three paths to `../substrate/crates/...`, matching
the sibling-checkout layout. Comment updated to name the current
convention explicitly rather than referencing an old side-by-side
CI claim that no longer matches CI's actual layout either (see
below on CI state).

What this fix does NOT change

- **The architectural rule in CLAUDE.md still stands** —
  "zero substrate/chronicle/gsap dependencies." This fix is a path
  correction, not a policy change.
- **The rule is already broken in both spirit and letter.** bascule-
  core depends on substrate-proto unconditionally; substrate-proto
  depends on substrate-common; both names are substrate-prefixed.
  The CI contamination check at .github/workflows/ci.yml:81-88
  greps Cargo.lock for "substrate|chronicle|gsap|hfl|metakernel"
  and fails on any match — current Cargo.lock contains substrate-
  common, substrate-hfl, substrate-proto, hfl-types, so CI would
  fail on a fresh run regardless of this patch.
- **This fix is a LOCAL buildability unblock only.** It makes
  `cargo check -p bascule-core` pass in the current layout. It does
  NOT fix CI, which is independently broken for the reasons above.

CI state

The bascule-oss CI workflow uses `actions/checkout@v4` with no
coordinating checkout of substrate-project or its substrate/
subrepo. Regardless of whether bascule-oss's paths use
`../substrate/...` or `../substrate-project/substrate/...`, CI
currently has nothing to resolve them against. And the
contamination check would flag the resulting Cargo.lock even if
the paths did resolve. CI is already red (or will be, on next
trigger).

Re-enabling CI against bascule-oss requires separate coordination
work: either adding a substrate-project checkout step to the
workflow, or renaming substrate-proto (see below), or amending
the contamination check to allow specific neutral-protocol crates
by name. That work is out of scope for this patch.

Architectural open item: substrate-proto naming violation

Documented in the new NOTES-SUBSTRATE-PROTO-NAMING-VIOLATION.md.
Summary: substrate-proto's CONTENT is a legitimate neutral
protocol contract (proto-generated SAT types bascule-core needs
to compose session attestations). The NAMING is substrate-
prefixed, which both violates CLAUDE.md's string-match rule and
creates an optical problem for CNCF sandbox positioning —
sandbox committees read dependency names more than content when
evaluating whether a project is a generic open-source substrate
or a specific company's product with open-source framing.

Proposed resolution (post-v1): rename substrate-proto → sat-proto
(or attestation-proto), move the consumed subset of substrate-
common's public surface out of substrate-common into the neutrally-
named crate, update bascule-core and any other consumers, regenerate
Cargo.lock, confirm the contamination check passes.

This is NOT v1-blocking. This IS CNCF-sandbox-positioning work —
worth doing in coordination with sandbox application prep so the
naming story and the institutional positioning story land
together.

Why we're shipping this narrower fix now

Phase 5.5c-c of the substrate foundation work (the adapter crate
bridging bascule-core session lifecycle to bascule-revocation-
ingress SessionLookup) requires depending on bascule-core as a
cross-workspace path dep. Without this path fix, substrate-side
crates can't pull bascule-core locally, blocking 5.5c-c
implementation. The path fix costs one line per dep and
introduces no new coupling; the deeper rename is several-repo
coordination that would delay 5.5c-c unnecessarily.

Fix-what's-blocking, document-what's-underlying, keep moving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
2026-04-20 18:14:28 -04:00
984a37e0cb chore: silence bascule-shell config dead-code warnings
Some checks failed
CI / dco (push) Has been cancelled
CI / build (push) Has been cancelled
The TOML schema for ~/.config/bascule/shell.toml carries
`servers = [{alias, hostname, port}]` entries that
bascule-shell deserializes but doesn't read at runtime. The
shell-side server chooser uses ssh host aliases (dev.gsh /
stg.gsh / prod.gsh) instead.

Marking the fields `#[allow(dead_code)]` with a comment
preserves the TOML wire format (so users with existing config
files don't get a parse error) without leaving the compiler
warning.

Verification:
  $ cargo build --workspace
    | grep -c "warning:"
  0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
2026-04-08 13:49:32 -04:00
33f6bf729a feat(hfl): bascule-core SAT compose routes through HFL when available
Post-M6 enhancement: when /dev/substrate-hfl is loaded and the
bascule-core `hfl` feature is enabled, the new
compose_via_hfl_or_local() entry point hands a serialized
SessionClaim to the kernel's attestation::SAT_BUNDLE function and
uses the kernel-composed SatBundle (proto-encoded) in place of the
locally-composed M1 bundle. Kernel composition has access to TPM
state, governance state, and platform-claim producers Bascule can't
reach from userspace.

Without the `hfl` feature: M1 path unchanged.
With the `hfl` feature but no kernel module: graceful fallback to
the M1 local compose path. Per ADR D9, the SAT chain stays alive
regardless of HFL availability.

bascule-core::hfl_sat (NEW, behind --features hfl):
  - compose_via_hfl_or_local(inputs) tries the kernel path first.
    On any failure (device missing, ioctl error, decode error)
    it logs at debug and returns the local M1 compose result.
  - try_compose_via_hfl() encodes the SessionClaim with prost,
    dispatches via HflClient::dispatch(0x0005, 1, claim_bytes,
    [0u8;32], current_epoch), and decodes the result as a
    proto SatBundle.
  - 2 unit tests cover the device-absent fallback path (+ structure
    equivalence with the M1 local compose).

Cargo.toml:
  - Workspace deps: hfl-types + substrate-hfl as path deps to the
    substrate workspace (cross-workspace, CI mounts both checkouts
    side by side).
  - bascule-core gains a `hfl` feature gating hfl-types +
    substrate-hfl + prost (the last for SessionClaim::encode_to_vec
    on the substrate-proto-generated types).

Tested (Docker rust:1.88-bookworm):
  cargo build  -p bascule-core                       clean
  cargo test   -p bascule-core --lib sat              7/7  (M1 regression)
  cargo build  -p bascule-core --features hfl        clean
  cargo test   -p bascule-core --features hfl --lib  26/26
    +2 hfl_sat tests on top of the existing bascule-core suite

Branched off main (post-merge of the M1..M5 stack).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
2026-04-08 10:41:09 -04:00
2520525ec6 feat(m5): bascule-shell prefers SPIFFE SVID URI as principal
Adds bascule_shell::identity::detect_spiffe_svid which reads a
SPIFFE SVID URI from /var/run/spire/svid-uri (override via
SPIFFE_SVID_PATH). When present it wins over Entra/AZ-CLI/
Kerberos/cached-OIDC/system-user, becoming the SAT session_leaf
actor field that QM's M5 SpiffeSvidEvaluator validates against
the cluster allowlist.

Why a file read instead of the SPIFFE Workload API: bascule-shell
ships independently from QM and the standard SPIRE k8s sidecar
writes the URI as /var/run/spire/svid-uri alongside svid.pem.
The file path is hermetic for tests and matches the deploy model.
If a future iteration needs continuous SVID URI rotation, switch
to a notify watcher or pull spiffe::workload_api.

Trust domain is parsed and surfaced as Identity.domain so the
banner / dashboard can show "spiffe://gh.dev" affiliation.

bascule_shell::main::set_env: auth_method == "spiffe" maps to
BASCULE_ROLES = "operator" by default. SPIRE-attested workloads
are explicitly cluster-issued so they get operator role until
per-workload provisioning lands. The existing precedence
(caller-set BASCULE_ROLES wins) is unchanged.

Bascule mTLS *channel* construction (Bascule -> QM gRPC
renegotiation) is intentionally NOT wired in this commit.
Per ADR D9 hot path is local; the renegotiation client is
deferred to M6 alongside the Rekor signing client because they
share the rustls dep tree.

Tested (Docker rust:1.88-bookworm):
  cargo build  -p bascule-shell -p bascule-core   clean
  cargo test   -p bascule-core --lib sat          7/7 (M1 regression)

Stacked on feat/m3-defcon-env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
2026-04-07 21:05:05 -04:00
df5a2a6f88 feat(m3): bascule-shell exports DEFCON env vars from posture-current.json
bascule-shell loads /opt/substrate/posture/current.json
(BASCULE_POSTURE_FILE override) at session start and exports:

  BASCULE_DEFCON_LEVEL          numeric global level (1..5)
  BASCULE_POSTURE_LEVEL         alias (already shipped in M1)
  BASCULE_CAPABILITY_CEILING    CAP_NONE..CAP_GOVERN
  BASCULE_CEREMONY_REQUIRED     "0" / "1"
  BASCULE_MAX_SESSION_TTL       minutes, omitted when 0

Fail-soft: missing/malformed file degrades to peacetime defaults so
the shell exec path stays alive on misconfigured hosts.

The new posture.rs module is a tiny inline snapshot loader (60 LOC,
serde_json on top of an already-present dep) — bascule-oss does not
pull libgsh as a dep, so the JSON wire format produced by
substrate-operator is the contract. gsh and bascule-shell share that
contract, not Rust types.

Stacked on feat/m2-roles-export.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
2026-04-07 19:02:34 -04:00
56529626f6 feat(m2): bascule-shell exports BASCULE_ROLES for gsh's role check
bascule-shell::set_env now populates BASCULE_ROLES so gsh's
M2 role-aware classifier has something to match against.

Precedence:
  1. Caller-set BASCULE_ROLES wins (env var preserved as-is).
  2. Otherwise derive a default from auth_method:
       oidc-entra | oidc-cached | kerberos -> operator
       ssh-key                              -> apprentice
       _                                    -> apprentice

The auth-method fallback is intentionally minimal — bascule-oss
Identity has no real roles field, and proper role provisioning
(Entra group claims, SPIFFE workload roles) lands in M5. This
default at least populates the env var so M2's role-deny path
is exercised end-to-end on existing dev shells instead of
silently empty.

Stacked on feat/m1-session-sat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
2026-04-07 17:52:50 -04:00
999c78ef4c feat(m1): bascule-shell composes a real SAT anchored on session_leaf
Replaces the opaque BASCULE_ATTESTATION_HASH (a SHA over a
"pcrN:val;ima:hash;" evidence string) with a real proto-canonical
SatBundle composed from the operator's identity + local platform
attestation, anchored on the L4 SessionClaim.

bascule-core::sat (NEW): pure composer module.
- build_session_claim(SessionInputs) -> SessionClaim builds the
  L4 leaf from {principal, auth_method, actor_type,
  identity_verified, platform_attested, software_verified,
  nonce_seed}, computes posture per SAT-SPEC-0002 §7, and
  populates the L1/L2/L3 binding fields with zero-padded
  placeholders until upstream producers exist.
- compose_local(SessionClaim) -> ComposedSat assembles the proto
  SatBundle via SatBundleBuilder. Hot path stays local per ADR D9
  (zero network); QM's gRPC ComposeSat is the warm-path surface.
- 7 unit tests cover layer/actor wiring, posture math at each
  evidence level, deterministic nonce, sat_hash uniqueness across
  principal changes.

bascule-shell: composes the SAT in main() right before execvp
of the inner shell — that's the OSS equivalent of an "Authenticated
-> ShellActive" transition (the OSS Bascule has no russh state
machine; it's a CLI wrapper). Exports the new env var surface:

  BASCULE_SAT_HASH            hex of proto sat_hash (canonical)
  BASCULE_SESSION_CLAIM_HASH  hex of L4 leaf hash
  BASCULE_SESSION_ID          UUID from SessionClaim
  BASCULE_POSTURE_LEVEL       SAT-SPEC-0002 §7 posture

  BASCULE_ATTESTATION_HASH    retained as compat alias (gsh /
                              dashboard consumers); now points at
                              the proto sat_hash, not the old
                              evidence-string SHA.

Cross-workspace path dep: substrate-proto via
../substrate-project/substrate/crates/substrate-proto. CI mounts
~/projects as one volume so the path resolves. Switching to a git
dep is post-MVP.

Note: russh-keys pulls `home` which requires Rust 1.88; CI bumps
the docker image accordingly. No code change.

Tested:
  cargo build -p bascule-core -p bascule-shell             clean
  cargo test  -p bascule-core --lib sat                    7/7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
2026-04-07 14:38:20 -04:00
6eb2de5dc0 docs: update all documentation for management API + dashboard
Updated 9 files to reflect:
  Management API (axum, port 9090) — embedded in bascule-server
  Dioxus dashboard components (WASM web target)
  6 crates in workspace (was 4)

README.md:
  Added Management API + Dashboard features section
  Added dashboard row to comparison table

docs/architecture.md:
  Updated diagram showing dual-listener architecture
  Added Management API section explaining Arc<SessionStore> sharing
  Updated crate table (6 crates)

docs/configuration.md:
  Added [dashboard] config section reference

docs/observability.md:
  Added Management API monitoring section with curl examples

docs/quickstart.md:
  Added Management API quick start section

docs/comparison.md:
  Added dashboard and TPM attestation rows

CLAUDE.md + CONTRIBUTING.md:
  Updated crate lists and feature flags

config/bascule.example.toml:
  Added [dashboard] section

All 17 README links verified valid. Build clean.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-05 17:17:18 -04:00
72fa8cee92 feat: embedded management API (axum, port 9090)
Same binary, same process, two listeners:
  Port 2222: SSH proxy (russh)
  Port 9090: Management API (axum)

API endpoints:
  GET /api/sessions         — active sessions
  GET /api/sessions/history — recent history (last 500)
  GET /api/stats            — aggregate analytics
  GET /api/health           — server health + version
  GET /api/info             — server capabilities

Session tracking:
  Arc<SessionStore> shared between SSH handler and API
  In-memory: active sessions + 500-session history ring buffer
  Tracks: auth breakdown, peak concurrent, TPM attested %

Feature flag:
  --features dashboard (default on) — includes axum + tower-http
  --no-default-features — SSH-only, no HTTP dependency

Config:
  [dashboard] section: enabled, listen address

All smoke tests pass. 0 substrate deps.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-05 15:09:26 -04:00
04dd74d15f feat: Dioxus dashboard — session analytics + WASM web target
New crates:
  bascule-dashboard — shared Dioxus component library
    SessionTable: live active sessions with auth/backend/TPM status
    StatsCards: active count, 24h total, TPM attested %, failed auth
    StatusBar: connection health indicator
    types.rs: DashboardSession, DashboardStats, HealthResponse

  bascule-dashboard-web — WASM web target (Dioxus 0.6 + web features)
    Compiles to wasm32-unknown-unknown
    Dark-first CSS (light mode via prefers-color-scheme)
    Monospace data display, clean stat cards

  bascule-core/store.rs — in-memory session store
    SessionStore with active sessions + aggregate stats
    Updated via SessionHandler hooks

Both dashboard library and web WASM target compile clean.
Server and shell builds unaffected. Zero substrate deps.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-05 14:10:01 -04:00
4aa7e9d816 docs: DCO, NOTICE, and governance framework
DCO (Developer Certificate of Origin):
  Standard DCO 1.1 (Linux kernel, CNCF, Kubernetes standard)
  Contributors retain copyright — no rights assignment

NOTICE:
  Copyright attribution (Guildhouse LLC)
  Contributors retain copyright, own their implementations
  SessionHandler/AuthProvider as public API boundary
  Tribal jurisdiction for voluntary dispute resolution

GOVERNANCE.md:
  Project governance model and decision making
  IP framework: Guildhouse brand vs contributor code vs shared Apache 2.0
  SessionHandler trait IS the product boundary
  Tribal dispute resolution: voluntary, technically informed
  Tribal partnership mission

CI:
  DCO sign-off check on pull requests
  Existing commits on main exempt

README + CONTRIBUTING:
  Governance section, DCO instructions, corporate guidance

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-05 11:13:20 -04:00
2fa92f8635 docs: comprehensive documentation + developer experience polish
New files:
  CONTRIBUTING.md — dev setup, code style, PR process
  CLAUDE.md — workspace context for Claude Code
  Makefile — build, test, lint, fmt, docker, helm-lint, dev, ci
  .editorconfig — consistent formatting
  rustfmt.toml — Rust formatting config
  docs/kubernetes.md — Helm install, values, architecture
  docs/bascule-shell.md — client shell install, config, TPM
  charts/bascule/README.md — Helm quick start

Updated:
  README.md — accurate feature matrix, clear shipped vs planned
  config/bascule.example.toml — full reference (72 lines, all fields)

All 15 README links verified valid.
Helm lint clean. Build passes. 0 substrate deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:53:08 -04:00
9dc5cb9eee feat: Kubernetes native integration — Helm chart + K8s/SPIFFE config
Helm chart (charts/bascule/):
  Deployment with shell sidecar container (shared jumphost model)
  Service (LoadBalancer/NodePort/ClusterIP)
  ConfigMap with auto-generated config.toml
  RBAC (Role + RoleBinding for pods/exec)
  NetworkPolicy (restrict shell egress, allow DNS + K8s API)
  ServiceAccount with create flag
  Configurable shell image (k8s-ops, net-ops, dev, minimal)
  Helm lint passes clean

K8s backend config (bascule-core):
  [k8s] section: enabled, namespace, pod_name, shell_container, shell
  Auto-detection via POD_NAME/POD_NAMESPACE env vars (downward API)
  Backend priority: K8s > proxy > container > local PTY
  K8s exec implementation deferred to --features k8s (kube crate)

SPIFFE/SPIRE auth config:
  [auth.spiffe] section: trust_domain, trust_bundle_path, workload_api_socket
  JWT-SVID token-as-password authentication pattern
  Implementation deferred to bascule-auth-spiffe crate

Zero substrate dependencies. Default build unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:23:09 -04:00
043b9b9bdc feat: bascule-shell — identity-aware shell with TPM attestation
New crate: bascule-shell (471 lines, 1.8MB binary)
  Login shell that detects identity + platform attestation at startup.
  Wraps bash/zsh/fish — operator works normally, identity travels with them.

Identity detection (priority order):
  1. Entra via WSL2 interop
  2. Azure CLI
  3. Kerberos TGT
  4. Cached OIDC token
  5. System user (fallback)

Platform attestation:
  TPM 2.0 PCR values via tpm2_pcrread (PCRs 0,1,2,7,10,14)
  IMA measurement log hash + count
  Keylime agent state
  Entra device compliance (WSL2 only)
  Composite SHA-256 hash over all evidence

Shell features:
  Banner with identity + attestation summary
  BASCULE_* env vars injected into inner shell
  --info mode for dry-run display
  --json mode for machine-readable output
  --exec mode for single-command execution
  Configurable via ~/.config/bascule/shell.toml

Tested on Fedora with real TPM 2.0:
  6 PCRs successfully read from hardware
  All env vars propagated to inner shell
  1.8MB binary, 0 substrate deps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:47:46 -04:00
e7fc9fa5e1 feat: structured logging, tracing spans, comprehensive documentation
Observability:
  Structured JSON logging via BASCULE_LOG_FORMAT=json
  Tracing spans on auth (method, principal, peer)
  Tracing spans on session lifecycle (id, principal, backend, source_ip)
  Tracing spans on exec requests (session_id, command)
  Config: [telemetry] and [metrics] sections (OTel export planned)

Documentation (8 files, 489 lines):
  docs/quickstart.md — three-path getting started
  docs/configuration.md — full config reference with examples
  docs/authentication.md — all auth modes with setup guides
  docs/architecture.md — backends, traits, extension model, security
  docs/observability.md — logging, tracing, metrics
  docs/comparison.md — vs Teleport, Boundary, StrongDM
  images/README.md — curated image catalog
  README.md — features, comparison, quickstart, extension example

1557 lines Rust, 489 lines docs, 0 substrate deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:45:03 -04:00
8d789524e8 feat: container backend — ephemeral right-sized shell containers
Third session backend: per-session ephemeral containers.
SSH session → container spawns → operator works → disconnect → destroyed.

Container runtime abstraction:
  Docker, Podman, Nerdctl via CLI execution (auto-detect)
  No libdocker dependency — any OCI-compliant runtime

Container config ([container] section):
  image, pull_policy, mounts, env, memory/cpu limits
  ephemeral (destroy on exit), hardened (drop caps)
  read_only_rootfs, network mode, user override

Handler: SessionBackend enum now has three variants:
  Local(PtyBridge) — spawn local shell
  Proxy(UpstreamSession) — forward to remote SSH host
  Container(ContainerSession) — spawn ephemeral container
  Priority: proxy > container > local PTY

Curated base images (images/):
  minimal — bash, coreutils, curl, jq, ssh (~50MB)
  k8s-ops — + kubectl, helm (~120MB)
  net-ops — + nmap, dig, traceroute, tcpdump (~90MB)
  dev — + git, make, gcc, python3 (~250MB)

The container IS the access boundary:
if it's not in the image, the operator can't run it.

SessionHandler hooks fire in all three modes.
6.5MB binary, 0 substrate deps, 1197 lines bascule-core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:23:39 -04:00
2212f7f870 feat: remote SSH proxy mode
Bascule now supports two session modes:
  Local — spawns a PTY on this machine (default, existing)
  Proxy — forwards the session to a target SSH host (NEW)

Proxy mode:
  SSH client ←→ bascule (auth + hooks) ←→ target SSH host
  Authenticates client via configured auth provider
  Connects to upstream SSH host via russh client
  Bridges I/O between client and upstream channels
  PTY, shell, and exec requests forwarded to target
  Exit status propagated back to client

Config:
  [proxy]
  target_host = "192.168.1.100"
  target_port = 22
  target_user = "deploy"           # optional, defaults to principal
  target_key_path = "/etc/bascule/target_key"
  accept_target_host_key = false   # dev only

SessionHandler hooks fire in both modes:
  on_session_start, on_exec, on_session_end
  Custom handlers can enforce policy regardless of mode

New file: proxy.rs (152 lines)
  UpstreamHandler — minimal russh client handler
  connect_upstream — connects + authenticates to target
  bridge_upstream_to_client — bidirectional I/O bridge

Binary: 6.3MB, zero substrate deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:01:08 -04:00
02142f7be4 feat: Entra Agent ID auth provider + governance leak cleanup
New crate: bascule-auth-agent-id
  Microsoft Entra Agent ID authentication for AI agents
  Validates OAuth tokens against Entra JWKS (60min cache)
  Extracts agent metadata: type, blueprint, sponsor, scopes
  Detects on-behalf-of (delegated) agents
  Token-as-password pattern for SSH auth

Cleanup:
  Removed all governance-specific references from comments
  SessionHandler trait is the only extension point
  Zero substrate/chronicle/gsap dependencies
  Config example uses neutral terminology

Config:
  [auth.agent_id] section for Entra configuration
  tenant_id, audiences, multi_tenant fields

3 crates: bascule-core, bascule-server, bascule-auth-agent-id
938 lines total, 5.6MB binary, 0 substrate deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:35:32 -04:00
bfa26cfd15 feat: Bascule — identity-aware SSH proxy
Open-source SSH proxy with pluggable authentication and
extensible session handling. Zero external governance dependencies.

Core (bascule-core):
  russh 0.46 SSH server with PTY bridge (portable-pty)
  Pluggable auth: AuthProvider trait (SSH keys, accept-all dev mode)
  SessionHandler trait for extending behavior (audit, governance)
  TOML configuration, ephemeral Ed25519 host key generation

Binary (bascule-server):
  Single binary, 5.6MB release build
  CLI with --config flag
  Default: accept-all auth on port 2222

Extension points:
  AuthProvider — implement for OIDC, certificates, custom auth
  SessionHandler — implement for audit, governance, recording
  DefaultHandler — passthrough (ships with open-source version)

Zero substrate/chronicle/gsap/hfl dependencies.
Apache 2.0 License.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:25:33 -04:00