mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Option to Notify Workspace Owner When Usage Limit is Reached (#16969)
## Summary - Replace the manual `/notify-owner` flow with an inline confirmation prompt when a usage-based workspace member hits a credits-depleted limit. - Fetch the current workspace role from the live ChatGPT `accounts/check/v4-2023-04-27` endpoint so owner/member behavior matches the desktop and web clients. - Keep owner, member, and spend-cap messaging distinct so we only offer the owner nudge when the workspace is actually out of credits. ## What Changed - `backend-client` - Added a typed fetch for the current account role from `accounts/check`. - Mapped backend role values into a Rust workspace-role enum. - `app-server` and protocol - Added `workspaceRole` to `account/read` and `account/updated`. - Derived `isWorkspaceOwner` from the live role, with a fallback to the cached token claim when the role fetch is unavailable. - `tui` - Removed the explicit `/notify-owner` slash command. - When a member is blocked because the workspace is out of credits, the error now prompts: - `Your workspace is out of credits. Request more from your workspace owner? [y/N]` - Choosing `y` sends the existing owner-notification request. - Choosing `n`, pressing `Esc`, or accepting the default selection dismisses the prompt without sending anything. - Selection popups now honor explicit item shortcuts, which is how the `y` / `n` interaction is wired. ## Reviewer Notes - The main behavior change is scoped to usage-based workspace members whose workspace credits are depleted. - Spend-cap reached should not show the owner-notification prompt. - Owners and admins should continue to see `/usage` guidance instead of the member prompt. - The live role fetch is best-effort; if it fails, we fall back to the existing token-derived ownership signal. ## Testing - Manual verification - Workspace owner does not see the member prompt. - Workspace member with depleted credits sees the confirmation prompt and can send the nudge with `y`. - Workspace member with spend cap reached does not see the owner-notification prompt. ### Workspace member out of usage https://github.com/user-attachments/assets/341ac396-eff4-4a7f-bf0c-60660becbea1 ### Workspace owner <img width="1728" height="1086" alt="Screenshot 2026-04-09 at 11 48 22 AM" src="https://github.com/user-attachments/assets/06262a45-e3fc-4cc4-8326-1cbedad46ed6" />
This commit is contained in:
1 parent
36712d8546
commit
9f2a585153
82 files changed
+3233
-60
No files matched your search
@@ -128,6 +128,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
chatgpt_plan_type: Some(InternalPlanType::Known(InternalKnownPlan::Pro)),
|
||||
chatgpt_user_id: Some("user-12345".to_string()),
|
||||
chatgpt_account_id: None,
|
||||
is_org_owner: None,
|
||||
raw_jwt: fake_jwt,
|
||||
},
|
||||
access_token: "test-access-token".to_string(),
|
||||
|
||||
@@ -302,6 +302,14 @@ impl CodexAuth {
|
||||
.and_then(|t| t.id_token.chatgpt_user_id)
|
||||
}
|
||||
|
||||
/// Returns `Some(true)` when the current ChatGPT token marks the user as a
|
||||
/// workspace owner, `Some(false)` for non-owners, and `None` when auth is
|
||||
/// unavailable or the token omits the claim.
|
||||
pub fn is_workspace_owner(&self) -> Option<bool> {
|
||||
self.get_current_token_data()
|
||||
.and_then(|t| t.id_token.is_org_owner)
|
||||
}
|
||||
|
||||
/// Account-facing plan classification derived from the current token.
|
||||
/// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…)
|
||||
/// mapped from the ID token's internal plan value. Prefer this when you
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct IdTokenInfo {
|
||||
pub chatgpt_user_id: Option<String>,
|
||||
/// Organization/workspace identifier associated with the token, if present.
|
||||
pub chatgpt_account_id: Option<String>,
|
||||
/// Whether the current user owns the workspace, when present in the token.
|
||||
pub is_org_owner: Option<bool>,
|
||||
pub raw_jwt: String,
|
||||
}
|
||||
|
||||
@@ -88,6 +90,8 @@ struct AuthClaims {
|
||||
user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
chatgpt_account_id: Option<String>,
|
||||
#[serde(default)]
|
||||
is_org_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -139,6 +143,7 @@ pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoErr
|
||||
chatgpt_plan_type: auth.chatgpt_plan_type,
|
||||
chatgpt_user_id: auth.chatgpt_user_id.or(auth.user_id),
|
||||
chatgpt_account_id: auth.chatgpt_account_id,
|
||||
is_org_owner: auth.is_org_owner,
|
||||
}),
|
||||
None => Ok(IdTokenInfo {
|
||||
email,
|
||||
@@ -146,6 +151,7 @@ pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoErr
|
||||
chatgpt_plan_type: None,
|
||||
chatgpt_user_id: None,
|
||||
chatgpt_account_id: None,
|
||||
is_org_owner: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,21 @@ fn id_token_info_parses_usage_based_business_plans() {
|
||||
assert_eq!(enterprise_cbp.is_workspace_account(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_parses_workspace_owner_flag() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({
|
||||
"email": "owner@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_plan_type": "self_serve_business_usage_based",
|
||||
"is_org_owner": true
|
||||
}
|
||||
}));
|
||||
|
||||
let info = parse_chatgpt_jwt_claims(&fake_jwt).expect("should parse");
|
||||
assert_eq!(info.email.as_deref(), Some("owner@example.com"));
|
||||
assert_eq!(info.is_org_owner, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_handles_missing_fields() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" }));
|
||||
|
||||
Reference in new issue
Block a user