Uh oh!
There was an error while loading. Please reload this page.
feat(auth): multi-workspace credential set and session management - #522
feat(auth): multi-workspace credential set and session management#522khaliqgant wants to merge 2 commits into
Conversation
- Rename CredentialCache → WorkspaceCredential (type alias preserved) - Add workspace_alias field for human-friendly workspace selectors - New CredentialSet: manages multiple workspace memberships with default - New AuthSessionSet: runtime session container with default lookup - Flexible deserialization: supports legacy single-credential, array, and new credential set formats - Selector lookup by workspace_id or alias (case-insensitive) - Auto-normalize: empty API keys filtered, single-membership auto-default
| if let Ok(set) = serde_json::from_value::<CredentialSet>(value.clone()) { | ||
| return Ok(Self::normalize(set)); | ||
| } |
There was a problem hiding this comment.
🔴 CredentialSet deserialization always succeeds first, making legacy/WorkspaceSource fallback branches dead code
CredentialSet derives Default and both its fields (memberships, default_workspace_id) carry #[serde(default)] (src/auth.rs:29-39). Because serde ignores unknown fields by default, serde_json::from_value::<CredentialSet>(value) will succeed for any JSON object — including a legacy WorkspaceCredential object or a WorkspaceSource object — producing an empty CredentialSet (zero memberships, no default). This means the first branch at line 69 always matches for object inputs, so the legacy WorkspaceCredential branch (line 73), the Vec<WorkspaceCredential> branch (line 77), and the WorkspaceSource branch (line 81) are never reached for JSON objects. A legacy credential cache file like {"workspace_id":"ws1","agent_id":"a1","api_key":"rk_live_x",...} will be silently parsed into an empty CredentialSet with no memberships, losing all credential data.
Example of the silent data loss
Given legacy JSON:
{"workspace_id":"ws1","agent_id":"a1","api_key":"rk_live_x","updated_at":"2025-01-01T00:00:00Z"}from_value hits line 69, deserializes it as CredentialSet{memberships: [], default_workspace_id: None}, normalizes it (still empty), and returns — the WorkspaceCredential branch at line 73 is never tried.
Prompt for agents
In src/auth.rs, the CredentialSet::from_value method at line 68-99 tries to deserialize as CredentialSet first (line 69), but because CredentialSet has #[serde(default)] on all fields and no #[serde(deny_unknown_fields)], any JSON object successfully deserializes as an empty CredentialSet. This makes all subsequent fallback branches (WorkspaceCredential, Vec<WorkspaceCredential>, WorkspaceSource) unreachable for JSON objects.
Fix options:
1. Add #[serde(deny_unknown_fields)] to CredentialSet so that objects with unrecognized keys (like workspace_id, agent_id, etc.) fail to deserialize as CredentialSet.
2. Alternatively, after successfully deserializing as CredentialSet at line 69, check whether the result is meaningful (e.g. set.memberships.is_empty() && set.default_workspace_id.is_none()) and only return it if it has actual data — otherwise fall through to the legacy branches.
3. Or reorder the branches so that more specific types (WorkspaceCredential, which has required fields) are tried before the permissive CredentialSet.
Was this helpful? React with 👍 or 👎 to provide feedback.
khaliqgant
commented
Mar 10, 2026
Closing — all auth changes (WorkspaceCredential, CredentialSet, AuthSessionSet) are already included in PR #519 (multi-workspace runtime impl). This PR is redundant. |
Summary
Adds the auth-layer foundation for multi-workspace support. This is the credential/session management piece that complements the runtime impl in #519 and the spec in #517.
Changes
WorkspaceCredential— renamed fromCredentialCache(type alias preserved for backward compat)workspace_aliasfield — human-friendly workspace selectors (e.g."personal","work")CredentialSet— manages N workspace memberships with a default, flexible deserialization (supports legacy single-credential, array, and new set format)AuthSessionSet— runtime session container with default session lookupworkspace_idoralias(case-insensitive)Related