feat(approval): per-launch UUID session_id + scrub legacy bearer leaks#2952
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (2)
📝 WalkthroughWalkthroughRemoves ChangesSession ID Privacy Hardening
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
PendingApproval and ApprovalAuditEntry no longer carry session_id; DomainEvent::ApprovalRequested no longer publishes it. Session-id provenance is internal to ApprovalGate; downstream consumers (frontend, audit log readers) never used it. The session_id column is retained in SQLite via insert_pending's new explicit parameter so the store still indexes by it for internal correlation/purge, but the value is no longer surfaced on the public types or domain event.
…ssion_id session_id no longer derives from the RPC bearer. The bearer remains the authentication credential; session_id is now strictly a per-launch correlation token published in audit events and written to approval.db. This removes the on-disk and audit-log exposure of any operator-supplied OPENHUMAN_CORE_TOKEN value via the approval surface.
…rsion migration One-shot migration runs on schema init: any pre-existing pending_approvals.session_id values are overwritten with the literal "pre-migration-redacted" (exposed as PRE_MIGRATION_SESSION_ID). The session_id column is retained for downgrade safety but no longer carries credential material. PRAGMA user_version is bumped to 1 after the scrub so the rewrite runs exactly once per DB even across many process launches. Test covers both the first-open scrub and the idempotent no-op on re-open.
Debug-build assertion in ApprovalGate::new pins session_id to the session-<uuid> shape minted by bootstrap_core_runtime — any other input (e.g. a raw RPC bearer wired up by a future refactor) trips the assertion immediately. Three tests guarantee that PendingApproval Debug + serialize, ApprovalAuditEntry Debug, and DomainEvent::ApprovalRequested Debug never expose a session_id field, preventing accidental re-introduction of credential leaks through the audit surface.
ApprovalGate::new's debug-build assertion requires session_id to begin with the per-launch UUID prefix. Update the e2e fixture to mirror the production shape so the assertion does not trip during the auto-approved external-effect tool-loop test.
296070c to
5acfc58
Compare
Summary
session_idfrom public approval surface types (PendingApproval,ApprovalAuditEntry) and from theDomainEvent::ApprovalRequestedbus payload.session_idunconditionally as a per-launch UUID (session-<uuid>) insidebootstrap_core_runtime— no longer derived from the JSON-RPC bearer.PRAGMA user_version=1SQLite migration that scrubs legacypending_approvals.session_idrows to a redacted sentinel. Column retained for downgrade safety.Problem
The approval gate persisted the JSON-RPC bearer token verbatim as the
session_idfield on every persisted approval row, on every publishedApprovalRequesteddomain event, and through any audit-log Debug surface. When the bearer was set as a stable value (the documented power-user / remote-core / docker-cloud path), this turned the on-diskapproval.dbSQLite file, anytracing/ panic backtrace that touched the event, and any subscriber that Debug-printed aPendingApprovalinto a plaintext bearer-credential leak.The pre-existing log-line
<redacted>masking atsrc/core/jsonrpc.rsonly suppressed the console surface — the credential still flowed into SQLite + the event bus + the gate's in-memory state.Solution
Commit 1 —
refactor(approval): drop session_id from public approval typesRemove the
session_id: Stringfield fromPendingApproval(src/openhuman/approval/types.rs) andApprovalAuditEntry. Removesession_idfromDomainEvent::ApprovalRequested(src/core/event_bus/events.rs). Cascade compile-error fixes through callers; the field was never read by any frontend (verified via grep onapp/src/components/chat/ApprovalRequestCard.tsx+ redux store consumers).Commit 2 —
feat(core/jsonrpc): use unconditional per-launch UUID for approval session_idReplace the
match crate::core::auth::get_rpc_token()branch at the gate-installation site withlet session_id = format!("session-{}", uuid::Uuid::new_v4()). The bearer remains the authentication credential;session_idis now strictly a per-launch correlation token. The old<redacted>label machinery is removed (no longer applicable — the value is always a UUID and always safe to log).Commit 3 —
feat(approval/store): scrub legacy session_id rows via PRAGMA user_version migrationOne-shot migration runs inside the schema-init path on first open.
PRAGMA user_versiongates the rewrite so the scrub runs exactly once per database even across many process launches. Existing rows are overwritten with the literalpre-migration-redacted(exported asPRE_MIGRATION_SESSION_ID). The column itself is retained so a downgrade does not lose the row entirely.Commit 4 —
test(approval): regression guards on session_id removalApprovalGate::newcarries a#[cfg(debug_assertions)] debug_assert!that the suppliedsession_idstarts with thesession-prefix. Three regression tests guarantee that:PendingApprovalDebug + serde JSON serialize never carry asession_idfield.ApprovalAuditEntryDebug never carries asession_idfield.DomainEvent::ApprovalRequestedDebug never carries asession_idfield.Any future refactor that re-introduces a raw-bearer wiring trips the debug-build assertion immediately; any refactor that re-introduces the field name (via serde rename, manual Debug impl, or struct re-addition) fails the regression tests.
Submission Checklist
approval::store::tests,approval::types::tests,approval::gate::tests,core::event_bus::events::tests.Closes #NNN— tracked privately; cross-referenced from the security-advisory thread instead of a public issue.Impact
UPDATE pending_approvals SET session_id = 'pre-migration-redacted' WHERE session_id != 'pre-migration-redacted'followed byPRAGMA user_version = 1. Audit history for in-flight prompts loses per-launch grouping (rows reflect the redacted sentinel); decided/expired rows are unaffected operationally.PendingApproval,ApprovalAuditEntry, andDomainEvent::ApprovalRequestedlose theirsession_idfield. Frontend not affected (field never consumed byApprovalRequestCardor the chat-runtime slice). Any third-party subscriber on the event bus that readsession_idwould not compile against this branch —#[non_exhaustive]already shieldedDomainEventfrom breaking-pattern-match changes, but a positional struct destructure would need updating.RPC_TOKENOnceLockfor any audit/correlation purpose.session_idcolumn is retained on disk; an older binary still reads the column shape, just sees the redacted sentinel for any pre-upgrade rows.Related
ApprovalGate::session_id()accessor (gate.rs:466) currently has zero non-test callers — candidate for removal once the security-advisory window closes.Summary by CodeRabbit