Wires the AuditPipeline's flush() path to QM's QuartermasterNotary
gRPC service. Previously flush() only updated local notarized=true
flags; now it batches pending leaf hashes into a CreateAnchorRequest
and persists the returned anchor_id + leaf_index back on each event
row.
Lazy-retry semantics match guildhouse-spire-plugins pkg/governance
(F.1): the gRPC channel is established on first successful flush and
cached in Arc<Mutex<Option<QuartermasterNotaryClient<Channel>>>>. If
QM is unreachable, bascule logs a warning, re-queues the leaves into
the pending buffer, and retries on the next flush interval. Local
audit rows are still written with notarized=true; only anchor_id
stays NULL until an anchor successfully lands. This is the same
pattern that unblocks the bascule-deploys-before-QM ordering problem
without crashing bascule.
Schema: bascule.audit_events already had anchor_id uuid + leaf_index
integer columns (migrations.rs, pre-existing). This commit populates
them for the first time.
Config:
- New `cluster_id` field on BasculeConfig, sourced from
BASCULE_CLUSTER_ID env. Empty string disables QM submission (local
storage only). In F.4, bascule gets the UUID from QM's clusters
table (generated at QM genesis).
- Existing `qm_endpoint` field now actually used (was scaffolded in
pre-F.4 code but never read).
Backwards-compat:
- submit(&self, event: &AuditEvent, notarize: bool) signature preserved.
- should_notarize(classification, fidelity) public fn preserved.
- Internal leaf_data hashing simplified to an event-field digest
(event_id + session_id + operator + command + classification +
exec_result + timestamp); bypasses serde_json_canonicalizer
dependency that the prior version required. Verify path still
works against QM's merkle tree because QM hashes whatever bytes
bascule submits — QM doesn't re-compute; it trusts the leaf
payload bascule submitted is the leaf.
Signed-off-by: Tyler J King <tking@guildhouse.dev>
Introduce ShellClass (Application | System) as a session-scoped
classification derived from PostureLevel at ceremony grant time.
- ShellClass::Application: default, software operations only
- ShellClass::System: host operations, requires Normal (5) posture
- derive_shell_class(): pure function, configurable threshold
- satisfies(): hierarchical check (System satisfies Application)
- No mid-session upgrade by design (immutable in SessionScope)
Added shell_class and posture_level_at_establishment to SessionScope
with #[serde(default)] for backward-compatible deserialization.
Signed-off-by: Tyler King <tking@guildhouse.dev>
Signed-off-by: Tyler J King <tking727@gmail.com>
Replace hardcoded posture return in AttestationHandler (Shellstream
namespace 0x0005) with PostureReader that reads the posture-current
ConfigMap written by the substrate-operator's posture evaluator.
Data pipeline is now end-to-end:
Keylime verifier -> posture evaluator -> ConfigMap -> bascule-agent
Behavior:
- posture_source='config': reads posture-current ConfigMap, maps
level to PostureLevel, caches with configurable TTL (default 30s)
- posture_source='static' or dev_mode: returns configured static
level and wire value (replaces hardcoded string for clarity)
- Graceful fallback: missing ConfigMap -> PostureLevel::Lockdown
(fail-closed) + warning log
New dependencies: kube, k8s-openapi, governance-types (via path).
Does NOT add keylime-client — reads ConfigMap JSON directly.
Signed-off-by: Tyler King <tking@guildhouse.dev>
Signed-off-by: Tyler J King <tking727@gmail.com>