mirror of
https://github.com/openai/codex.git
synced 2026-06-02 03:11:59 +00:00
Compare commits
10 Commits
pr24982
...
dev/dhruvg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e39598b663 | ||
|
|
bc2dd9faf5 | ||
|
|
9f70baed4a | ||
|
|
08abda2726 | ||
|
|
fd36ea04e2 | ||
|
|
62190604cf | ||
|
|
c4c1505bbf | ||
|
|
409afc2046 | ||
|
|
5ae17fb80f | ||
|
|
b7bce9beb7 |
@@ -5,6 +5,48 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AccountSessionsAddParams": {
|
||||
"properties": {
|
||||
"switchToAddedAccount": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsListParams": {
|
||||
"properties": {
|
||||
"refreshWorkspaceMetadata": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsLogoutParams": {
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsSwitchParams": {
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"sessionId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeCreditType": {
|
||||
"enum": [
|
||||
"credits",
|
||||
@@ -5875,6 +5917,102 @@
|
||||
"title": "Account/login/cancelRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/add"
|
||||
],
|
||||
"title": "AccountSession/addRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsAddParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/addRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/list"
|
||||
],
|
||||
"title": "AccountSession/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/logout"
|
||||
],
|
||||
"title": "AccountSession/logoutRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsLogoutParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/logoutRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/switch"
|
||||
],
|
||||
"title": "AccountSession/switchRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsSwitchParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/switchRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -1787,6 +1787,102 @@
|
||||
"title": "Account/login/cancelRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/add"
|
||||
],
|
||||
"title": "AccountSession/addRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/AccountSessionsAddParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/addRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/list"
|
||||
],
|
||||
"title": "AccountSession/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/AccountSessionsListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/logout"
|
||||
],
|
||||
"title": "AccountSession/logoutRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/AccountSessionsLogoutParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/logoutRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/switch"
|
||||
],
|
||||
"title": "AccountSession/switchRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/AccountSessionsSwitchParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/switchRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -5731,6 +5827,193 @@
|
||||
"title": "AccountRateLimitsUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSession": {
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastUsedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"plan": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"selectedWorkspaceAccountId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"workspaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AccountSessionWorkspace"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isActive",
|
||||
"lastUsedAt",
|
||||
"sessionId",
|
||||
"workspaces"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionWorkspace": {
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"kind": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AccountSessionWorkspaceKind"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/AccountSessionWorkspaceStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionWorkspaceKind": {
|
||||
"enum": [
|
||||
"personal",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AccountSessionWorkspaceStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"disabled",
|
||||
"deactivated"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AccountSessionsAddParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"switchToAddedAccount": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "AccountSessionsAddParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"refreshWorkspaceMetadata": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "AccountSessionsListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsLogoutParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId"
|
||||
],
|
||||
"title": "AccountSessionsLogoutParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"activeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AccountSession"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessions"
|
||||
],
|
||||
"title": "AccountSessionsResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsSwitchParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"sessionId"
|
||||
],
|
||||
"title": "AccountSessionsSwitchParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -103,6 +103,193 @@
|
||||
"title": "AccountRateLimitsUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSession": {
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastUsedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"plan": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"selectedWorkspaceAccountId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"workspaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AccountSessionWorkspace"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isActive",
|
||||
"lastUsedAt",
|
||||
"sessionId",
|
||||
"workspaces"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionWorkspace": {
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"kind": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AccountSessionWorkspaceKind"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/AccountSessionWorkspaceStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionWorkspaceKind": {
|
||||
"enum": [
|
||||
"personal",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AccountSessionWorkspaceStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"disabled",
|
||||
"deactivated"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AccountSessionsAddParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"switchToAddedAccount": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "AccountSessionsAddParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"refreshWorkspaceMetadata": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "AccountSessionsListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsLogoutParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId"
|
||||
],
|
||||
"title": "AccountSessionsLogoutParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"activeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AccountSession"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessions"
|
||||
],
|
||||
"title": "AccountSessionsResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionsSwitchParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"sessionId"
|
||||
],
|
||||
"title": "AccountSessionsSwitchParams",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -2535,6 +2722,102 @@
|
||||
"title": "Account/login/cancelRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/add"
|
||||
],
|
||||
"title": "AccountSession/addRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsAddParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/addRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/list"
|
||||
],
|
||||
"title": "AccountSession/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/logout"
|
||||
],
|
||||
"title": "AccountSession/logoutRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsLogoutParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/logoutRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"accountSession/switch"
|
||||
],
|
||||
"title": "AccountSession/switchRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AccountSessionsSwitchParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "AccountSession/switchRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
10
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsAddParams.json
generated
Normal file
10
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsAddParams.json
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"switchToAddedAccount": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "AccountSessionsAddParams",
|
||||
"type": "object"
|
||||
}
|
||||
10
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsListParams.json
generated
Normal file
10
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsListParams.json
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"refreshWorkspaceMetadata": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "AccountSessionsListParams",
|
||||
"type": "object"
|
||||
}
|
||||
13
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsLogoutParams.json
generated
Normal file
13
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsLogoutParams.json
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId"
|
||||
],
|
||||
"title": "AccountSessionsLogoutParams",
|
||||
"type": "object"
|
||||
}
|
||||
139
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsResponse.json
generated
Normal file
139
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsResponse.json
generated
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AccountSession": {
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastUsedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"plan": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"selectedWorkspaceAccountId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"workspaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AccountSessionWorkspace"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isActive",
|
||||
"lastUsedAt",
|
||||
"sessionId",
|
||||
"workspaces"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionWorkspace": {
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"kind": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AccountSessionWorkspaceKind"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/AccountSessionWorkspaceStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountSessionWorkspaceKind": {
|
||||
"enum": [
|
||||
"personal",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AccountSessionWorkspaceStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"disabled",
|
||||
"deactivated"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"activeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AccountSession"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessions"
|
||||
],
|
||||
"title": "AccountSessionsResponse",
|
||||
"type": "object"
|
||||
}
|
||||
17
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsSwitchParams.json
generated
Normal file
17
codex-rs/app-server-protocol/schema/json/v2/AccountSessionsSwitchParams.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountId",
|
||||
"sessionId"
|
||||
],
|
||||
"title": "AccountSessionsSwitchParams",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
6
codex-rs/app-server-protocol/schema/typescript/v2/AccountSession.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/AccountSession.ts
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AccountSessionWorkspace } from "./AccountSessionWorkspace";
|
||||
|
||||
export type AccountSession = { sessionId: string, email: string | null, userId: string | null, displayName: string | null, imageUrl: string | null, plan: string | null, lastUsedAt: bigint, isActive: boolean, selectedWorkspaceAccountId: string | null, workspaces: Array<AccountSessionWorkspace>, };
|
||||
7
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionWorkspace.ts
generated
Normal file
7
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionWorkspace.ts
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AccountSessionWorkspaceKind } from "./AccountSessionWorkspaceKind";
|
||||
import type { AccountSessionWorkspaceStatus } from "./AccountSessionWorkspaceStatus";
|
||||
|
||||
export type AccountSessionWorkspace = { accountId: string, name: string | null, imageUrl: string | null, kind: AccountSessionWorkspaceKind | null, status: AccountSessionWorkspaceStatus, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionWorkspaceKind.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionWorkspaceKind.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountSessionWorkspaceKind = "personal" | "workspace";
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionWorkspaceStatus.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionWorkspaceStatus.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountSessionWorkspaceStatus = "active" | "disabled" | "deactivated";
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsAddParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsAddParams.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountSessionsAddParams = { switchToAddedAccount?: boolean, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsListParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsListParams.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountSessionsListParams = { refreshWorkspaceMetadata?: boolean, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsLogoutParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsLogoutParams.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountSessionsLogoutParams = { sessionId: string, };
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsResponse.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsResponse.ts
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AccountSession } from "./AccountSession";
|
||||
|
||||
export type AccountSessionsResponse = { activeSessionId: string | null, sessions: Array<AccountSession>, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsSwitchParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountSessionsSwitchParams.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountSessionsSwitchParams = { sessionId: string, accountId: string, };
|
||||
@@ -3,6 +3,15 @@
|
||||
export type { Account } from "./Account";
|
||||
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountSession } from "./AccountSession";
|
||||
export type { AccountSessionWorkspace } from "./AccountSessionWorkspace";
|
||||
export type { AccountSessionWorkspaceKind } from "./AccountSessionWorkspaceKind";
|
||||
export type { AccountSessionWorkspaceStatus } from "./AccountSessionWorkspaceStatus";
|
||||
export type { AccountSessionsAddParams } from "./AccountSessionsAddParams";
|
||||
export type { AccountSessionsListParams } from "./AccountSessionsListParams";
|
||||
export type { AccountSessionsLogoutParams } from "./AccountSessionsLogoutParams";
|
||||
export type { AccountSessionsResponse } from "./AccountSessionsResponse";
|
||||
export type { AccountSessionsSwitchParams } from "./AccountSessionsSwitchParams";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
|
||||
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
|
||||
|
||||
@@ -919,6 +919,33 @@ client_request_definitions! {
|
||||
response: v2::CancelLoginAccountResponse,
|
||||
},
|
||||
|
||||
AccountSessionsAdd => "accountSession/add" {
|
||||
params: v2::AccountSessionsAddParams,
|
||||
serialization: global("account-auth"),
|
||||
response: v2::AccountSessionsResponse,
|
||||
},
|
||||
|
||||
AccountSessionsList => "accountSession/list" {
|
||||
params: v2::AccountSessionsListParams,
|
||||
serialization: global("account-auth"),
|
||||
manual_payload_conversion: manual,
|
||||
response: v2::AccountSessionsResponse,
|
||||
},
|
||||
|
||||
AccountSessionsLogout => "accountSession/logout" {
|
||||
params: v2::AccountSessionsLogoutParams,
|
||||
serialization: global("account-auth"),
|
||||
manual_payload_conversion: manual,
|
||||
response: v2::AccountSessionsResponse,
|
||||
},
|
||||
|
||||
AccountSessionsSwitch => "accountSession/switch" {
|
||||
params: v2::AccountSessionsSwitchParams,
|
||||
serialization: global("account-auth"),
|
||||
manual_payload_conversion: manual,
|
||||
response: v2::AccountSessionsResponse,
|
||||
},
|
||||
|
||||
LogoutAccount => "account/logout" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: global("account-auth"),
|
||||
|
||||
@@ -137,6 +137,89 @@ pub struct CancelLoginAccountResponse {
|
||||
pub status: CancelLoginAccountStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSessionsAddParams {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub switch_to_added_account: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSessionsListParams {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub refresh_workspace_metadata: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSessionsLogoutParams {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSessionsSwitchParams {
|
||||
pub session_id: String,
|
||||
pub account_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSessionsResponse {
|
||||
pub active_session_id: Option<String>,
|
||||
pub sessions: Vec<AccountSession>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSession {
|
||||
pub session_id: String,
|
||||
pub email: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub image_url: Option<String>,
|
||||
pub plan: Option<String>,
|
||||
pub last_used_at: i64,
|
||||
pub is_active: bool,
|
||||
pub selected_workspace_account_id: Option<String>,
|
||||
pub workspaces: Vec<AccountSessionWorkspace>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountSessionWorkspace {
|
||||
pub account_id: String,
|
||||
pub name: Option<String>,
|
||||
pub image_url: Option<String>,
|
||||
pub kind: Option<AccountSessionWorkspaceKind>,
|
||||
pub status: AccountSessionWorkspaceStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AccountSessionWorkspaceKind {
|
||||
Personal,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AccountSessionWorkspaceStatus {
|
||||
Active,
|
||||
Disabled,
|
||||
Deactivated,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -1757,6 +1757,10 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
|
||||
- `account/login/start` — begin login (`apiKey`, `chatgpt`, `chatgptDeviceCode`).
|
||||
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
|
||||
- `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`.
|
||||
- `accountSession/add` — save the current managed ChatGPT auth session.
|
||||
- `accountSession/list` — list saved managed ChatGPT auth sessions and optionally refresh their workspace metadata.
|
||||
- `accountSession/switch` — make a saved ChatGPT auth session and workspace current.
|
||||
- `accountSession/logout` — revoke and remove one saved ChatGPT auth session.
|
||||
- `account/logout` — sign out; triggers `account/updated`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
|
||||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||||
@@ -1842,19 +1846,30 @@ Field notes:
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": false, "error": "…" } }
|
||||
```
|
||||
|
||||
### 6) Logout
|
||||
### 6) Save and switch ChatGPT account sessions
|
||||
|
||||
Desktop clients can save the current managed ChatGPT auth payload after login, list saved sessions, and switch the active workspace. Switching a workspace exchanges the saved access token for a newly signed workspace-scoped token without opening a browser. These methods return the full saved-session snapshot because the number of interactive account sessions is intentionally small.
|
||||
|
||||
```json
|
||||
{ "method": "account/logout", "id": 6 }
|
||||
{ "id": 6, "result": {} }
|
||||
{ "method": "accountSession/add", "id": 6, "params": { "switchToAddedAccount": true } }
|
||||
{ "method": "accountSession/list", "id": 7, "params": { "refreshWorkspaceMetadata": true } }
|
||||
{ "method": "accountSession/switch", "id": 8, "params": { "sessionId": "<uuid>", "accountId": "<workspace-id>" } }
|
||||
{ "method": "accountSession/logout", "id": 9, "params": { "sessionId": "<uuid>" } }
|
||||
```
|
||||
|
||||
### 7) Logout
|
||||
|
||||
```json
|
||||
{ "method": "account/logout", "id": 10 }
|
||||
{ "id": 10, "result": {} }
|
||||
{ "method": "account/updated", "params": { "authMode": null, "planType": null } }
|
||||
```
|
||||
|
||||
### 7) Rate limits (ChatGPT)
|
||||
### 8) Rate limits (ChatGPT)
|
||||
|
||||
```json
|
||||
{ "method": "account/rateLimits/read", "id": 7 }
|
||||
{ "id": 7, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null, "rateLimitReachedType": null } } }
|
||||
{ "method": "account/rateLimits/read", "id": 11 }
|
||||
{ "id": 11, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null, "rateLimitReachedType": null } } }
|
||||
{ "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } }
|
||||
```
|
||||
|
||||
|
||||
581
codex-rs/app-server/src/account_sessions.rs
Normal file
581
codex-rs/app-server/src/account_sessions.rs
Normal file
@@ -0,0 +1,581 @@
|
||||
use base64::Engine;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::AccountSession;
|
||||
use codex_app_server_protocol::AccountSessionWorkspace;
|
||||
use codex_app_server_protocol::AccountSessionWorkspaceKind;
|
||||
use codex_app_server_protocol::AccountSessionWorkspaceStatus;
|
||||
use codex_app_server_protocol::AccountSessionsResponse;
|
||||
use codex_backend_client::AccountEntry;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthDotJson;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::delete_account_session_auth;
|
||||
use codex_login::load_account_session_auth;
|
||||
use codex_login::load_auth_dot_json;
|
||||
use codex_login::logout;
|
||||
use codex_login::revoke_account_session_auth;
|
||||
use codex_login::save_account_session_auth;
|
||||
use codex_login::save_auth;
|
||||
use codex_login::token_data::parse_chatgpt_jwt_claims;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
const ACCOUNT_SESSIONS_FILE: &str = "account-sessions.json";
|
||||
|
||||
pub(crate) struct AccountSessionsStore<'a> {
|
||||
codex_home: &'a Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StoredAccountSessions {
|
||||
active_session_id: Option<String>,
|
||||
sessions: Vec<StoredAccountSession>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StoredAccountSession {
|
||||
session_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
auth_json: Option<AuthDotJson>,
|
||||
email: Option<String>,
|
||||
user_id: Option<String>,
|
||||
display_name: Option<String>,
|
||||
image_url: Option<String>,
|
||||
plan: Option<String>,
|
||||
last_used_at: i64,
|
||||
selected_workspace_account_id: Option<String>,
|
||||
workspaces: Vec<AccountSessionWorkspace>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
struct AccessTokenClaims {
|
||||
#[serde(rename = "https://api.openai.com/profile", default)]
|
||||
profile: AccessTokenProfileClaims,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
struct AccessTokenProfileClaims {
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
picture: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> AccountSessionsStore<'a> {
|
||||
pub(crate) fn new(
|
||||
codex_home: &'a Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn add(
|
||||
&self,
|
||||
switch_to_added_account: bool,
|
||||
) -> std::io::Result<AccountSessionsResponse> {
|
||||
let mut stored = self.load()?;
|
||||
let mut auth_json = load_auth_dot_json(self.codex_home, self.auth_credentials_store_mode)?
|
||||
.ok_or_else(|| std::io::Error::other("No active ChatGPT auth session to add"))?;
|
||||
let mut session = Self::session_from_auth_json(&auth_json)
|
||||
.ok_or_else(|| std::io::Error::other("No active ChatGPT auth session to add"))?;
|
||||
self.refresh_workspace_metadata(&mut session, &mut auth_json)
|
||||
.await;
|
||||
|
||||
let existing_index = stored.sessions.iter().position(|saved| {
|
||||
session
|
||||
.email
|
||||
.as_ref()
|
||||
.is_some_and(|email| saved.email.as_ref() == Some(email))
|
||||
|| session
|
||||
.user_id
|
||||
.as_ref()
|
||||
.is_some_and(|user_id| saved.user_id.as_ref() == Some(user_id))
|
||||
});
|
||||
if let Some(index) = existing_index {
|
||||
session
|
||||
.session_id
|
||||
.clone_from(&stored.sessions[index].session_id);
|
||||
}
|
||||
let added_session_id = session.session_id.clone();
|
||||
self.save_session_auth(&added_session_id, &auth_json)?;
|
||||
if let Some(index) = existing_index {
|
||||
stored.sessions[index] = session;
|
||||
} else {
|
||||
stored.sessions.push(session);
|
||||
}
|
||||
|
||||
if switch_to_added_account || stored.active_session_id.is_none() {
|
||||
stored.active_session_id = Some(added_session_id);
|
||||
save_auth(
|
||||
self.codex_home,
|
||||
&auth_json,
|
||||
self.auth_credentials_store_mode,
|
||||
)?;
|
||||
} else if let Some(active_session_id) = stored.active_session_id.as_deref() {
|
||||
self.save_active_session_auth(active_session_id)?;
|
||||
}
|
||||
|
||||
self.save(&stored)?;
|
||||
Ok(Self::response(stored))
|
||||
}
|
||||
|
||||
pub(crate) async fn list(
|
||||
&self,
|
||||
refresh_workspace_metadata: bool,
|
||||
) -> std::io::Result<AccountSessionsResponse> {
|
||||
self.sync_active_auth()?;
|
||||
let mut stored = self.load()?;
|
||||
if refresh_workspace_metadata {
|
||||
for session in &mut stored.sessions {
|
||||
let Some(mut auth_json) = self.load_session_auth(&session.session_id)? else {
|
||||
continue;
|
||||
};
|
||||
self.refresh_workspace_metadata(session, &mut auth_json)
|
||||
.await;
|
||||
self.save_session_auth(&session.session_id, &auth_json)?;
|
||||
}
|
||||
self.save(&stored)?;
|
||||
}
|
||||
Ok(Self::response(stored))
|
||||
}
|
||||
|
||||
pub(crate) async fn logout(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> std::io::Result<AccountSessionsResponse> {
|
||||
self.sync_active_auth()?;
|
||||
let mut stored = self.load()?;
|
||||
let index = stored
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| session.session_id == session_id)
|
||||
.ok_or_else(|| std::io::Error::other("Saved ChatGPT account session not found"))?;
|
||||
let removed = stored.sessions.remove(index);
|
||||
if let Err(err) = revoke_account_session_auth(
|
||||
self.codex_home,
|
||||
&removed.session_id,
|
||||
self.auth_credentials_store_mode,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to revoke saved account session during logout: {err}");
|
||||
}
|
||||
delete_account_session_auth(
|
||||
self.codex_home,
|
||||
&removed.session_id,
|
||||
self.auth_credentials_store_mode,
|
||||
)?;
|
||||
|
||||
if stored.active_session_id.as_deref() == Some(session_id) {
|
||||
let newest = stored
|
||||
.sessions
|
||||
.iter()
|
||||
.max_by_key(|session| session.last_used_at);
|
||||
stored.active_session_id = newest.map(|session| session.session_id.clone());
|
||||
match newest {
|
||||
Some(session) => self.save_active_session_auth(&session.session_id)?,
|
||||
None => {
|
||||
logout(self.codex_home, self.auth_credentials_store_mode)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.save(&stored)?;
|
||||
Ok(Self::response(stored))
|
||||
}
|
||||
|
||||
pub(crate) async fn switch(
|
||||
&self,
|
||||
session_id: &str,
|
||||
account_id: &str,
|
||||
) -> std::io::Result<AccountSessionsResponse> {
|
||||
self.sync_active_auth()?;
|
||||
let mut stored = self.load()?;
|
||||
let index = stored
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| session.session_id == session_id)
|
||||
.ok_or_else(|| std::io::Error::other("Saved ChatGPT account session not found"))?;
|
||||
let mut auth_json = self
|
||||
.load_session_auth(&stored.sessions[index].session_id)?
|
||||
.ok_or_else(|| std::io::Error::other("Saved ChatGPT account session has no tokens"))?;
|
||||
let auth = CodexAuth::from_account_session_auth_dot_json(
|
||||
self.codex_home,
|
||||
&stored.sessions[index].session_id,
|
||||
auth_json.clone(),
|
||||
self.auth_credentials_store_mode,
|
||||
Some(self.chatgpt_base_url),
|
||||
)
|
||||
.await?;
|
||||
let client = BackendClient::from_auth(self.chatgpt_base_url, &auth)
|
||||
.map_err(std::io::Error::other)?;
|
||||
// Changing only tokens.account_id would update the request header while leaving the
|
||||
// bearer token scoped to the previous workspace. Exchange it first so backend routing
|
||||
// uses the selected workspace from the newly signed token claims.
|
||||
let replacement = client
|
||||
.switch_workspace_token(account_id)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
// The exchange does not return a replacement ID token. Refresh the workspace-specific
|
||||
// plan from the new access token and keep the saved ID token intact.
|
||||
let plan = parse_chatgpt_jwt_claims(&replacement.access_token)
|
||||
.ok()
|
||||
.and_then(|claims| claims.get_chatgpt_plan_type_raw());
|
||||
let tokens = auth_json
|
||||
.tokens
|
||||
.as_mut()
|
||||
.ok_or_else(|| std::io::Error::other("Saved ChatGPT account session has no tokens"))?;
|
||||
tokens.access_token = replacement.access_token;
|
||||
// Refresh-token rotation is optional, so keep the saved token when none is returned.
|
||||
if let Some(refresh_token) = replacement.refresh_token {
|
||||
tokens.refresh_token = refresh_token;
|
||||
}
|
||||
tokens.account_id = Some(account_id.to_string());
|
||||
auth_json.last_refresh = Some(Utc::now());
|
||||
let session = &mut stored.sessions[index];
|
||||
if let Some(plan) = plan {
|
||||
session.plan = Some(plan);
|
||||
}
|
||||
session.selected_workspace_account_id = Some(account_id.to_string());
|
||||
session.last_used_at = Utc::now().timestamp();
|
||||
stored.active_session_id = Some(session_id.to_string());
|
||||
self.save_session_auth(session_id, &auth_json)?;
|
||||
save_auth(
|
||||
self.codex_home,
|
||||
&auth_json,
|
||||
self.auth_credentials_store_mode,
|
||||
)?;
|
||||
self.save(&stored)?;
|
||||
Ok(Self::response(stored))
|
||||
}
|
||||
|
||||
pub(crate) fn sync_active_auth(&self) -> std::io::Result<()> {
|
||||
let mut stored = self.load()?;
|
||||
if self.migrate_legacy_auth(&mut stored)? {
|
||||
self.save(&stored)?;
|
||||
}
|
||||
let Some(active_session_id) = stored.active_session_id.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(auth_json) =
|
||||
load_auth_dot_json(self.codex_home, self.auth_credentials_store_mode)?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(session) = stored
|
||||
.sessions
|
||||
.iter_mut()
|
||||
.find(|session| &session.session_id == active_session_id)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if !Self::same_identity(session, &auth_json) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let saved_auth_json = self.load_session_auth(&session.session_id)?;
|
||||
let selected_workspace_account_id = auth_json
|
||||
.tokens
|
||||
.as_ref()
|
||||
.and_then(|tokens| tokens.account_id.clone());
|
||||
if saved_auth_json.as_ref() == Some(&auth_json)
|
||||
&& (selected_workspace_account_id.is_none()
|
||||
|| session.selected_workspace_account_id == selected_workspace_account_id)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.save_session_auth(&session.session_id, &auth_json)?;
|
||||
if selected_workspace_account_id.is_some() {
|
||||
session.selected_workspace_account_id = selected_workspace_account_id;
|
||||
}
|
||||
self.save(&stored)
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_all_and_clear(&self) -> std::io::Result<()> {
|
||||
let Some(mut stored) = self.read()? else {
|
||||
return self.clear();
|
||||
};
|
||||
self.migrate_legacy_auth(&mut stored)?;
|
||||
for session in stored.sessions {
|
||||
if let Err(err) = revoke_account_session_auth(
|
||||
self.codex_home,
|
||||
&session.session_id,
|
||||
self.auth_credentials_store_mode,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to revoke saved account session during logout: {err}");
|
||||
}
|
||||
delete_account_session_auth(
|
||||
self.codex_home,
|
||||
&session.session_id,
|
||||
self.auth_credentials_store_mode,
|
||||
)?;
|
||||
}
|
||||
self.clear()
|
||||
}
|
||||
|
||||
fn load(&self) -> std::io::Result<StoredAccountSessions> {
|
||||
match self.read()? {
|
||||
Some(mut stored) => {
|
||||
if self.migrate_legacy_auth(&mut stored)? {
|
||||
self.save(&stored)?;
|
||||
}
|
||||
Ok(stored)
|
||||
}
|
||||
None => {
|
||||
let auth_json =
|
||||
load_auth_dot_json(self.codex_home, self.auth_credentials_store_mode)?;
|
||||
let Some(auth_json) = auth_json else {
|
||||
return Ok(StoredAccountSessions::default());
|
||||
};
|
||||
let Some(session) = Self::session_from_auth_json(&auth_json) else {
|
||||
return Ok(StoredAccountSessions::default());
|
||||
};
|
||||
self.save_session_auth(&session.session_id, &auth_json)?;
|
||||
let stored = StoredAccountSessions {
|
||||
active_session_id: Some(session.session_id.clone()),
|
||||
sessions: vec![session],
|
||||
};
|
||||
self.save(&stored)?;
|
||||
Ok(stored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_legacy_auth(&self, stored: &mut StoredAccountSessions) -> std::io::Result<bool> {
|
||||
let mut migrated = false;
|
||||
for session in &mut stored.sessions {
|
||||
if let Some(auth_json) = session.auth_json.take() {
|
||||
self.save_session_auth(&session.session_id, &auth_json)?;
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
Ok(migrated)
|
||||
}
|
||||
|
||||
fn load_session_auth(&self, session_id: &str) -> std::io::Result<Option<AuthDotJson>> {
|
||||
load_account_session_auth(
|
||||
self.codex_home,
|
||||
session_id,
|
||||
self.auth_credentials_store_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn save_session_auth(&self, session_id: &str, auth_json: &AuthDotJson) -> std::io::Result<()> {
|
||||
save_account_session_auth(
|
||||
self.codex_home,
|
||||
session_id,
|
||||
auth_json,
|
||||
self.auth_credentials_store_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn save_active_session_auth(&self, session_id: &str) -> std::io::Result<()> {
|
||||
let auth_json = self
|
||||
.load_session_auth(session_id)?
|
||||
.ok_or_else(|| std::io::Error::other("Saved ChatGPT account session has no tokens"))?;
|
||||
save_auth(
|
||||
self.codex_home,
|
||||
&auth_json,
|
||||
self.auth_credentials_store_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn clear(&self) -> std::io::Result<()> {
|
||||
match std::fs::remove_file(self.path()) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> std::io::Result<Option<StoredAccountSessions>> {
|
||||
match std::fs::read_to_string(self.path()) {
|
||||
Ok(payload) => serde_json::from_str(&payload)
|
||||
.map(Some)
|
||||
.map_err(std::io::Error::other),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, sessions: &StoredAccountSessions) -> std::io::Result<()> {
|
||||
let path = self.path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut options = OpenOptions::new();
|
||||
options.truncate(true).write(true).create(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
options.mode(0o600);
|
||||
}
|
||||
let mut file = options.open(path)?;
|
||||
file.write_all(serde_json::to_string_pretty(sessions)?.as_bytes())?;
|
||||
file.flush()
|
||||
}
|
||||
|
||||
async fn refresh_workspace_metadata(
|
||||
&self,
|
||||
session: &mut StoredAccountSession,
|
||||
auth_json: &mut AuthDotJson,
|
||||
) {
|
||||
let Ok(auth) = CodexAuth::from_account_session_auth_dot_json(
|
||||
self.codex_home,
|
||||
&session.session_id,
|
||||
auth_json.clone(),
|
||||
self.auth_credentials_store_mode,
|
||||
Some(self.chatgpt_base_url),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok(client) = BackendClient::from_auth(self.chatgpt_base_url, &auth) else {
|
||||
return;
|
||||
};
|
||||
let Ok(accounts) = client.get_accounts_check().await else {
|
||||
return;
|
||||
};
|
||||
session.selected_workspace_account_id = session
|
||||
.selected_workspace_account_id
|
||||
.clone()
|
||||
.or(accounts.default_account_id)
|
||||
.or_else(|| accounts.account_ordering.first().cloned());
|
||||
if let Some(account_id) = session.selected_workspace_account_id.as_ref()
|
||||
&& let Some(tokens) = auth_json.tokens.as_mut()
|
||||
{
|
||||
tokens.account_id = Some(account_id.clone());
|
||||
}
|
||||
session.workspaces = accounts
|
||||
.accounts
|
||||
.into_iter()
|
||||
.map(Self::workspace_from_account)
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn session_from_auth_json(auth_json: &AuthDotJson) -> Option<StoredAccountSession> {
|
||||
let tokens = auth_json.tokens.as_ref()?;
|
||||
let claims = Self::access_token_claims(&tokens.access_token);
|
||||
let selected_workspace_account_id = tokens
|
||||
.account_id
|
||||
.clone()
|
||||
.or_else(|| tokens.id_token.chatgpt_account_id.clone());
|
||||
let workspaces = selected_workspace_account_id
|
||||
.as_ref()
|
||||
.map(|account_id| {
|
||||
vec![AccountSessionWorkspace {
|
||||
account_id: account_id.clone(),
|
||||
name: None,
|
||||
image_url: None,
|
||||
kind: None,
|
||||
status: AccountSessionWorkspaceStatus::Active,
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(StoredAccountSession {
|
||||
session_id: Uuid::now_v7().to_string(),
|
||||
auth_json: None,
|
||||
email: tokens.id_token.email.clone(),
|
||||
user_id: tokens.id_token.chatgpt_user_id.clone(),
|
||||
display_name: claims.profile.name,
|
||||
image_url: claims.profile.picture.or(claims.profile.image),
|
||||
plan: tokens.id_token.get_chatgpt_plan_type_raw(),
|
||||
last_used_at: Utc::now().timestamp(),
|
||||
selected_workspace_account_id,
|
||||
workspaces,
|
||||
})
|
||||
}
|
||||
|
||||
fn access_token_claims(access_token: &str) -> AccessTokenClaims {
|
||||
let Some(payload) = access_token.split('.').nth(1) else {
|
||||
return AccessTokenClaims::default();
|
||||
};
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(payload)
|
||||
.ok()
|
||||
.and_then(|payload| serde_json::from_slice(&payload).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn same_identity(session: &StoredAccountSession, auth_json: &AuthDotJson) -> bool {
|
||||
let Some(tokens) = auth_json.tokens.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
session
|
||||
.email
|
||||
.as_ref()
|
||||
.zip(tokens.id_token.email.as_ref())
|
||||
.is_some_and(|(saved, active)| saved == active)
|
||||
|| session
|
||||
.user_id
|
||||
.as_ref()
|
||||
.zip(tokens.id_token.chatgpt_user_id.as_ref())
|
||||
.is_some_and(|(saved, active)| saved == active)
|
||||
}
|
||||
|
||||
fn workspace_from_account(account: AccountEntry) -> AccountSessionWorkspace {
|
||||
let kind = match account.structure.as_str() {
|
||||
"personal" => Some(AccountSessionWorkspaceKind::Personal),
|
||||
"workspace" => Some(AccountSessionWorkspaceKind::Workspace),
|
||||
_ => None,
|
||||
};
|
||||
AccountSessionWorkspace {
|
||||
account_id: account.id,
|
||||
name: account.name,
|
||||
image_url: account.profile_picture_url,
|
||||
kind,
|
||||
status: AccountSessionWorkspaceStatus::Active,
|
||||
}
|
||||
}
|
||||
|
||||
fn response(stored: StoredAccountSessions) -> AccountSessionsResponse {
|
||||
let active_session_id = stored.active_session_id;
|
||||
let mut sessions = stored
|
||||
.sessions
|
||||
.into_iter()
|
||||
.map(|session| AccountSession {
|
||||
is_active: Some(&session.session_id) == active_session_id.as_ref(),
|
||||
session_id: session.session_id,
|
||||
email: session.email,
|
||||
user_id: session.user_id,
|
||||
display_name: session.display_name,
|
||||
image_url: session.image_url,
|
||||
plan: session.plan,
|
||||
last_used_at: session.last_used_at,
|
||||
selected_workspace_account_id: session.selected_workspace_account_id,
|
||||
workspaces: session.workspaces,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sessions.sort_by_key(|session| std::cmp::Reverse(session.last_used_at));
|
||||
AccountSessionsResponse {
|
||||
active_session_id,
|
||||
sessions,
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
self.codex_home.join(ACCOUNT_SESSIONS_FILE)
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::registry::Registry;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
mod account_sessions;
|
||||
mod analytics_utils;
|
||||
mod app_server_tracing;
|
||||
mod attestation;
|
||||
|
||||
@@ -1271,6 +1271,18 @@ impl MessageProcessor {
|
||||
ClientRequest::CancelLoginAccount { params, .. } => {
|
||||
self.account_processor.cancel_login_account(params).await
|
||||
}
|
||||
ClientRequest::AccountSessionsAdd { params, .. } => {
|
||||
self.account_processor.add_account_session(params).await
|
||||
}
|
||||
ClientRequest::AccountSessionsList { params, .. } => {
|
||||
self.account_processor.list_account_sessions(params).await
|
||||
}
|
||||
ClientRequest::AccountSessionsLogout { params, .. } => {
|
||||
self.account_processor.logout_account_session(params).await
|
||||
}
|
||||
ClientRequest::AccountSessionsSwitch { params, .. } => {
|
||||
self.account_processor.switch_account_session(params).await
|
||||
}
|
||||
ClientRequest::GetAccount { params, .. } => {
|
||||
self.account_processor.get_account(params).await
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::account_sessions::AccountSessionsStore;
|
||||
use crate::bespoke_event_handling::apply_bespoke_event_handling;
|
||||
use crate::bespoke_event_handling::maybe_emit_hook_prompt_item_completed;
|
||||
use crate::command_exec::CommandExecManager;
|
||||
@@ -22,6 +23,10 @@ use codex_analytics::InputError;
|
||||
use codex_analytics::TurnSteerRequestError;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::AccountLoginCompletedNotification;
|
||||
use codex_app_server_protocol::AccountSessionsAddParams;
|
||||
use codex_app_server_protocol::AccountSessionsListParams;
|
||||
use codex_app_server_protocol::AccountSessionsLogoutParams;
|
||||
use codex_app_server_protocol::AccountSessionsSwitchParams;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
|
||||
@@ -105,6 +105,56 @@ impl AccountRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn add_account_session(
|
||||
&self,
|
||||
params: AccountSessionsAddParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let response = self
|
||||
.account_sessions_store()
|
||||
.add(params.switch_to_added_account)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to add account session: {err}")))?;
|
||||
self.sync_auth_after_account_session_change().await;
|
||||
Ok(Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_account_sessions(
|
||||
&self,
|
||||
params: AccountSessionsListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.account_sessions_store()
|
||||
.list(params.refresh_workspace_metadata)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
.map_err(|err| internal_error(format!("failed to list account sessions: {err}")))
|
||||
}
|
||||
|
||||
pub(crate) async fn logout_account_session(
|
||||
&self,
|
||||
params: AccountSessionsLogoutParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let response = self
|
||||
.account_sessions_store()
|
||||
.logout(¶ms.session_id)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to log out account session: {err}")))?;
|
||||
self.sync_auth_after_account_session_change().await;
|
||||
Ok(Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn switch_account_session(
|
||||
&self,
|
||||
params: AccountSessionsSwitchParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let response = self
|
||||
.account_sessions_store()
|
||||
.switch(¶ms.session_id, ¶ms.account_id)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to switch account session: {err}")))?;
|
||||
self.sync_auth_after_account_session_change().await;
|
||||
Ok(Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_account(
|
||||
&self,
|
||||
params: GetAccountParams,
|
||||
@@ -159,6 +209,42 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn account_sessions_store(&self) -> AccountSessionsStore<'_> {
|
||||
AccountSessionsStore::new(
|
||||
&self.config.codex_home,
|
||||
self.config.cli_auth_credentials_store_mode,
|
||||
&self.config.chatgpt_base_url,
|
||||
)
|
||||
}
|
||||
|
||||
fn sync_active_account_session(&self) -> Result<(), JSONRPCErrorError> {
|
||||
self.account_sessions_store()
|
||||
.sync_active_auth()
|
||||
.map_err(|err| internal_error(format!("failed to sync active account session: {err}")))
|
||||
}
|
||||
|
||||
async fn sync_auth_after_account_session_change(&self) {
|
||||
self.auth_manager.reload().await;
|
||||
self.config_manager.replace_cloud_requirements_loader(
|
||||
self.auth_manager.clone(),
|
||||
self.config.chatgpt_base_url.clone(),
|
||||
);
|
||||
self.config_manager
|
||||
.sync_default_client_residency_requirement()
|
||||
.await;
|
||||
Self::maybe_refresh_remote_installed_plugins_cache_for_current_config(
|
||||
&self.config_manager,
|
||||
&self.thread_manager,
|
||||
self.auth_manager.auth_cached(),
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn maybe_refresh_remote_installed_plugins_cache_for_current_config(
|
||||
config_manager: &ConfigManager,
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
@@ -265,6 +351,8 @@ impl AccountRequestProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
self.sync_active_account_session()?;
|
||||
|
||||
// Cancel any active login attempt.
|
||||
{
|
||||
let mut guard = self.active_login.lock().await;
|
||||
@@ -317,9 +405,12 @@ impl AccountRequestProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
self.sync_active_account_session()?;
|
||||
|
||||
let opts = LoginServerOptions {
|
||||
open_browser: false,
|
||||
codex_streamlined_login,
|
||||
revoke_previous_auth: false,
|
||||
..LoginServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
CLIENT_ID.to_string(),
|
||||
@@ -562,6 +653,8 @@ impl AccountRequestProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
self.sync_active_account_session()?;
|
||||
|
||||
// Cancel any active login attempt to avoid persisting managed auth state.
|
||||
{
|
||||
let mut guard = self.active_login.lock().await;
|
||||
@@ -676,6 +769,12 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
self.sync_active_account_session()?;
|
||||
self.account_sessions_store()
|
||||
.revoke_all_and_clear()
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to clear account sessions: {err}")))?;
|
||||
|
||||
match self.auth_manager.logout_with_revoke().await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::types::AccountsCheckResponse;
|
||||
use crate::types::CodeTaskDetailsResponse;
|
||||
use crate::types::ConfigFileResponse;
|
||||
use crate::types::PaginatedListTaskListItem;
|
||||
use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind;
|
||||
use crate::types::RateLimitStatusPayload;
|
||||
use crate::types::SwitchWorkspaceTokenResponse;
|
||||
use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use anyhow::Result;
|
||||
use codex_api::SharedAuthProvider;
|
||||
@@ -95,6 +97,11 @@ struct SendAddCreditsNudgeEmailRequest {
|
||||
credit_type: AddCreditsNudgeCreditType,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SwitchWorkspaceTokenRequest<'a> {
|
||||
workspace_id: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PathStyle {
|
||||
/// /api/codex/…
|
||||
@@ -301,6 +308,31 @@ impl Client {
|
||||
Ok(Self::rate_limit_snapshots_from_payload(payload))
|
||||
}
|
||||
|
||||
pub async fn get_accounts_check(&self) -> Result<AccountsCheckResponse> {
|
||||
let url = match self.path_style {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/accounts/check", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/accounts/check", self.base_url),
|
||||
};
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
self.decode_json(&url, &ct, &body)
|
||||
}
|
||||
|
||||
pub async fn switch_workspace_token(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
) -> Result<SwitchWorkspaceTokenResponse> {
|
||||
let url = format!("{}/accounts/switch-workspace-token", self.base_url);
|
||||
let req = self
|
||||
.http
|
||||
.post(&url)
|
||||
.headers(self.headers())
|
||||
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
|
||||
.json(&SwitchWorkspaceTokenRequest { workspace_id });
|
||||
let (body, ct) = self.exec_request(req, "POST", &url).await?;
|
||||
self.decode_json(&url, &ct, &body)
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
&self,
|
||||
credit_type: AddCreditsNudgeCreditType,
|
||||
|
||||
@@ -4,9 +4,12 @@ pub(crate) mod types;
|
||||
pub use client::AddCreditsNudgeCreditType;
|
||||
pub use client::Client;
|
||||
pub use client::RequestError;
|
||||
pub use types::AccountEntry;
|
||||
pub use types::AccountsCheckResponse;
|
||||
pub use types::CodeTaskDetailsResponse;
|
||||
pub use types::CodeTaskDetailsResponseExt;
|
||||
pub use types::ConfigFileResponse;
|
||||
pub use types::PaginatedListTaskListItem;
|
||||
pub use types::SwitchWorkspaceTokenResponse;
|
||||
pub use types::TaskListItem;
|
||||
pub use types::TurnAttemptsSiblingTurnsResponse;
|
||||
|
||||
@@ -13,6 +13,100 @@ use serde::de::Deserializer;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AccountsCheckResponse {
|
||||
pub accounts: Vec<AccountEntry>,
|
||||
pub account_ordering: Vec<String>,
|
||||
pub default_account_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AccountEntry {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub profile_picture_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub structure: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SwitchWorkspaceTokenResponse {
|
||||
pub access_token: String,
|
||||
#[serde(default)]
|
||||
pub refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawAccountsCheckResponse {
|
||||
#[serde(default)]
|
||||
accounts: RawAccounts,
|
||||
#[serde(default)]
|
||||
account_ordering: Vec<String>,
|
||||
#[serde(default)]
|
||||
default_account_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawAccounts {
|
||||
List(Vec<AccountEntry>),
|
||||
Map(HashMap<String, ChatGptAccountEntry>),
|
||||
}
|
||||
|
||||
impl Default for RawAccounts {
|
||||
fn default() -> Self {
|
||||
Self::List(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChatGptAccountEntry {
|
||||
account: ChatGptAccountInfo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChatGptAccountInfo {
|
||||
account_id: Option<String>,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
profile_picture_url: Option<String>,
|
||||
#[serde(default)]
|
||||
structure: String,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AccountsCheckResponse {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw = RawAccountsCheckResponse::deserialize(deserializer)?;
|
||||
let accounts = match raw.accounts {
|
||||
RawAccounts::List(accounts) => accounts,
|
||||
RawAccounts::Map(mut accounts) => raw
|
||||
.account_ordering
|
||||
.iter()
|
||||
.filter_map(|account_id| {
|
||||
let account = accounts.remove(account_id)?.account;
|
||||
Some(AccountEntry {
|
||||
id: account.account_id?,
|
||||
name: account.name,
|
||||
profile_picture_url: account.profile_picture_url,
|
||||
structure: account.structure,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
Ok(Self {
|
||||
accounts,
|
||||
account_ordering: raw.account_ordering,
|
||||
default_account_id: raw.default_account_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand-rolled models for the Cloud Tasks task-details response.
|
||||
/// The generated OpenAPI models are pretty bad. This is a half-step
|
||||
/// towards hand-rolling them.
|
||||
|
||||
@@ -30,6 +30,7 @@ pub use crate::auth::agent_identity::AgentIdentityAuth;
|
||||
pub use crate::auth::storage::AgentIdentityAuthRecord;
|
||||
pub use crate::auth::storage::AuthDotJson;
|
||||
use crate::auth::storage::AuthStorageBackend;
|
||||
use crate::auth::storage::create_account_session_auth_storage;
|
||||
use crate::auth::storage::create_auth_storage;
|
||||
use crate::auth::util::try_parse_error_message;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
@@ -240,6 +241,31 @@ impl CodexAuth {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_account_session_auth_dot_json(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
auth_dot_json: AuthDotJson,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
) -> std::io::Result<Self> {
|
||||
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
|
||||
let mut auth = Self::from_auth_dot_json(
|
||||
codex_home,
|
||||
auth_dot_json,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await?;
|
||||
if let Self::Chatgpt(chatgpt_auth) = &mut auth {
|
||||
chatgpt_auth.storage = create_account_session_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
session_id,
|
||||
storage_mode,
|
||||
)?;
|
||||
}
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
pub async fn from_auth_storage(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
@@ -607,6 +633,59 @@ pub fn load_auth_dot_json(
|
||||
storage.load()
|
||||
}
|
||||
|
||||
/// Persist a saved account session without replacing the active auth payload.
|
||||
pub fn save_account_session_auth(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
auth: &AuthDotJson,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<()> {
|
||||
let storage = create_account_session_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
session_id,
|
||||
auth_credentials_store_mode,
|
||||
)?;
|
||||
storage.save(auth)
|
||||
}
|
||||
|
||||
/// Load a saved account session without changing the active auth payload.
|
||||
pub fn load_account_session_auth(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<Option<AuthDotJson>> {
|
||||
let storage = create_account_session_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
session_id,
|
||||
auth_credentials_store_mode,
|
||||
)?;
|
||||
storage.load()
|
||||
}
|
||||
|
||||
/// Revoke a saved account session without changing the active auth payload.
|
||||
pub async fn revoke_account_session_auth(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<()> {
|
||||
let auth = load_account_session_auth(codex_home, session_id, auth_credentials_store_mode)?;
|
||||
revoke_auth_tokens(auth.as_ref()).await
|
||||
}
|
||||
|
||||
/// Delete a saved account session without changing the active auth payload.
|
||||
pub fn delete_account_session_auth(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<bool> {
|
||||
let storage = create_account_session_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
session_id,
|
||||
auth_credentials_store_mode,
|
||||
)?;
|
||||
storage.delete()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AuthConfig {
|
||||
pub codex_home: PathBuf,
|
||||
|
||||
@@ -28,6 +28,10 @@ use codex_keyring_store::KeyringStore;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
mod account_session;
|
||||
|
||||
pub(super) use account_session::create_account_session_auth_storage;
|
||||
|
||||
/// Expected structure for $CODEX_HOME/auth.json.
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct AuthDotJson {
|
||||
@@ -225,7 +229,6 @@ impl AuthStorageBackend for KeyringAuthStorage {
|
||||
|
||||
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
|
||||
let key = compute_store_key(&self.codex_home)?;
|
||||
// Simpler error mapping per style: prefer method reference over closure
|
||||
let serialized = serde_json::to_string(auth).map_err(std::io::Error::other)?;
|
||||
self.save_to_keyring(&key, &serialized)?;
|
||||
if let Err(err) = delete_file_if_exists(&self.codex_home) {
|
||||
|
||||
285
codex-rs/login/src/auth/storage/account_session.rs
Normal file
285
codex-rs/login/src/auth/storage/account_session.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
use super::AuthDotJson;
|
||||
use super::AuthStorageBackend;
|
||||
use super::EPHEMERAL_AUTH_STORE;
|
||||
use super::KEYRING_SERVICE;
|
||||
use super::compute_store_key;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_keyring_store::DefaultKeyringStore;
|
||||
use codex_keyring_store::KeyringStore;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
|
||||
pub(in crate::auth) fn create_account_session_auth_storage(
|
||||
codex_home: PathBuf,
|
||||
session_id: &str,
|
||||
mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<Arc<dyn AuthStorageBackend>> {
|
||||
validate_session_id(session_id)?;
|
||||
let keyring_store: Arc<dyn KeyringStore> = Arc::new(DefaultKeyringStore);
|
||||
Ok(match mode {
|
||||
AuthCredentialsStoreMode::File => Arc::new(FileAuthStorage::new(&codex_home, session_id)),
|
||||
AuthCredentialsStoreMode::Keyring => Arc::new(KeyringAuthStorage::new(
|
||||
&codex_home,
|
||||
session_id,
|
||||
keyring_store,
|
||||
)?),
|
||||
AuthCredentialsStoreMode::Auto => Arc::new(AutoAuthStorage::new(
|
||||
&codex_home,
|
||||
session_id,
|
||||
keyring_store,
|
||||
)?),
|
||||
AuthCredentialsStoreMode::Ephemeral => {
|
||||
Arc::new(EphemeralAuthStorage::new(&codex_home, session_id)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_session_id(session_id: &str) -> std::io::Result<()> {
|
||||
let mut components = Path::new(session_id).components();
|
||||
if matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"account session ID must be a single path component",
|
||||
))
|
||||
}
|
||||
|
||||
fn auth_file(codex_home: &Path, session_id: &str) -> PathBuf {
|
||||
codex_home
|
||||
.join("account-sessions")
|
||||
.join(format!("{session_id}.json"))
|
||||
}
|
||||
|
||||
fn store_key(codex_home: &Path, session_id: &str) -> std::io::Result<String> {
|
||||
Ok(format!(
|
||||
"{}|account-session|{session_id}",
|
||||
compute_store_key(codex_home)?
|
||||
))
|
||||
}
|
||||
|
||||
fn delete_file_if_exists(path: &Path) -> std::io::Result<bool> {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FileAuthStorage {
|
||||
auth_file: PathBuf,
|
||||
}
|
||||
|
||||
impl FileAuthStorage {
|
||||
fn new(codex_home: &Path, session_id: &str) -> Self {
|
||||
Self {
|
||||
auth_file: auth_file(codex_home, session_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthStorageBackend for FileAuthStorage {
|
||||
fn load(&self) -> std::io::Result<Option<AuthDotJson>> {
|
||||
let mut file = match File::open(&self.auth_file) {
|
||||
Ok(file) => file,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
serde_json::from_str(&contents)
|
||||
.map(Some)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
|
||||
if let Some(parent) = self.auth_file.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut options = OpenOptions::new();
|
||||
options.truncate(true).write(true).create(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
options.mode(0o600);
|
||||
}
|
||||
let mut file = options.open(&self.auth_file)?;
|
||||
file.write_all(serde_json::to_string_pretty(auth)?.as_bytes())?;
|
||||
file.flush()
|
||||
}
|
||||
|
||||
fn delete(&self) -> std::io::Result<bool> {
|
||||
delete_file_if_exists(&self.auth_file)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct KeyringAuthStorage {
|
||||
auth_file: PathBuf,
|
||||
store_key: String,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
}
|
||||
|
||||
impl KeyringAuthStorage {
|
||||
fn new(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
auth_file: auth_file(codex_home, session_id),
|
||||
store_key: store_key(codex_home, session_id)?,
|
||||
keyring_store,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthStorageBackend for KeyringAuthStorage {
|
||||
fn load(&self) -> std::io::Result<Option<AuthDotJson>> {
|
||||
match self.keyring_store.load(KEYRING_SERVICE, &self.store_key) {
|
||||
Ok(Some(serialized)) => serde_json::from_str(&serialized).map(Some).map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"failed to deserialize CLI auth from keyring: {err}"
|
||||
))
|
||||
}),
|
||||
Ok(None) => Ok(None),
|
||||
Err(error) => Err(std::io::Error::other(format!(
|
||||
"failed to load CLI auth from keyring: {}",
|
||||
error.message()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
|
||||
let serialized = serde_json::to_string(auth).map_err(std::io::Error::other)?;
|
||||
match self
|
||||
.keyring_store
|
||||
.save(KEYRING_SERVICE, &self.store_key, &serialized)
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
let message = format!(
|
||||
"failed to write OAuth tokens to keyring: {}",
|
||||
error.message()
|
||||
);
|
||||
warn!("{message}");
|
||||
return Err(std::io::Error::other(message));
|
||||
}
|
||||
}
|
||||
if let Err(err) = delete_file_if_exists(&self.auth_file) {
|
||||
warn!("failed to remove CLI auth fallback file: {err}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete(&self) -> std::io::Result<bool> {
|
||||
let keyring_removed = self
|
||||
.keyring_store
|
||||
.delete(KEYRING_SERVICE, &self.store_key)
|
||||
.map_err(|err| {
|
||||
std::io::Error::other(format!("failed to delete auth from keyring: {err}"))
|
||||
});
|
||||
let file_removed = delete_file_if_exists(&self.auth_file);
|
||||
Ok(keyring_removed? || file_removed?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AutoAuthStorage {
|
||||
keyring_storage: Arc<KeyringAuthStorage>,
|
||||
file_storage: Arc<FileAuthStorage>,
|
||||
}
|
||||
|
||||
impl AutoAuthStorage {
|
||||
fn new(
|
||||
codex_home: &Path,
|
||||
session_id: &str,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
keyring_storage: Arc::new(KeyringAuthStorage::new(
|
||||
codex_home,
|
||||
session_id,
|
||||
keyring_store,
|
||||
)?),
|
||||
file_storage: Arc::new(FileAuthStorage::new(codex_home, session_id)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthStorageBackend for AutoAuthStorage {
|
||||
fn load(&self) -> std::io::Result<Option<AuthDotJson>> {
|
||||
match self.keyring_storage.load() {
|
||||
Ok(Some(auth)) => Ok(Some(auth)),
|
||||
Ok(None) => self.file_storage.load(),
|
||||
Err(err) => {
|
||||
warn!("failed to load CLI auth from keyring, falling back to file storage: {err}");
|
||||
self.file_storage.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
|
||||
match self.keyring_storage.save(auth) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => {
|
||||
warn!("failed to save auth to keyring, falling back to file storage: {err}");
|
||||
self.file_storage.save(auth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self) -> std::io::Result<bool> {
|
||||
self.keyring_storage.delete()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct EphemeralAuthStorage {
|
||||
store_key: String,
|
||||
}
|
||||
|
||||
impl EphemeralAuthStorage {
|
||||
fn new(codex_home: &Path, session_id: &str) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
store_key: store_key(codex_home, session_id)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_store<F, T>(&self, action: F) -> std::io::Result<T>
|
||||
where
|
||||
F: FnOnce(&mut HashMap<String, AuthDotJson>, String) -> std::io::Result<T>,
|
||||
{
|
||||
let mut store = EPHEMERAL_AUTH_STORE
|
||||
.lock()
|
||||
.map_err(|_| std::io::Error::other("failed to lock ephemeral auth storage"))?;
|
||||
action(&mut store, self.store_key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthStorageBackend for EphemeralAuthStorage {
|
||||
fn load(&self) -> std::io::Result<Option<AuthDotJson>> {
|
||||
self.with_store(|store, key| Ok(store.get(&key).cloned()))
|
||||
}
|
||||
|
||||
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
|
||||
self.with_store(|store, key| {
|
||||
store.insert(key, auth.clone());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn delete(&self) -> std::io::Result<bool> {
|
||||
self.with_store(|store, key| Ok(store.remove(&key).is_some()))
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,7 @@ pub async fn complete_device_code_login(
|
||||
tokens.access_token,
|
||||
tokens.refresh_token,
|
||||
opts.cli_auth_credentials_store_mode,
|
||||
opts.revoke_previous_auth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ pub use auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
pub use auth::RefreshTokenError;
|
||||
pub use auth::UnauthorizedRecovery;
|
||||
pub use auth::default_client;
|
||||
pub use auth::delete_account_session_auth;
|
||||
pub use auth::enforce_login_restrictions;
|
||||
pub use auth::load_account_session_auth;
|
||||
pub use auth::load_auth_dot_json;
|
||||
pub use auth::login_with_access_token;
|
||||
pub use auth::login_with_api_key;
|
||||
@@ -44,6 +46,8 @@ pub use auth::logout;
|
||||
pub use auth::logout_with_revoke;
|
||||
pub use auth::read_codex_access_token_from_env;
|
||||
pub use auth::read_openai_api_key_from_env;
|
||||
pub use auth::revoke_account_session_auth;
|
||||
pub use auth::save_account_session_auth;
|
||||
pub use auth::save_auth;
|
||||
pub use auth_env_telemetry::AuthEnvTelemetry;
|
||||
pub use auth_env_telemetry::collect_auth_env_telemetry;
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct ServerOptions {
|
||||
pub force_state: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
pub codex_streamlined_login: bool,
|
||||
pub revoke_previous_auth: bool,
|
||||
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ impl ServerOptions {
|
||||
force_state: None,
|
||||
forced_chatgpt_workspace_id,
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
cli_auth_credentials_store_mode,
|
||||
}
|
||||
}
|
||||
@@ -362,6 +364,7 @@ async fn process_request(
|
||||
tokens.access_token.clone(),
|
||||
tokens.refresh_token.clone(),
|
||||
opts.cli_auth_credentials_store_mode,
|
||||
opts.revoke_previous_auth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -793,6 +796,7 @@ pub(crate) async fn persist_tokens_async(
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
revoke_previous_auth: bool,
|
||||
) -> io::Result<()> {
|
||||
// Reuse existing synchronous logic but run it off the async runtime.
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
@@ -829,7 +833,8 @@ pub(crate) async fn persist_tokens_async(
|
||||
.await
|
||||
.map_err(|e| io::Error::other(format!("persist task failed: {e}")))??;
|
||||
|
||||
if should_revoke_auth_tokens(previous_auth.as_ref(), &auth)
|
||||
if revoke_previous_auth
|
||||
&& should_revoke_auth_tokens(previous_auth.as_ref(), &auth)
|
||||
&& let Err(err) = revoke_auth_tokens(previous_auth.as_ref()).await
|
||||
{
|
||||
warn!("failed to revoke superseded auth tokens after login: {err}");
|
||||
@@ -1227,6 +1232,7 @@ mod tests {
|
||||
"new-access".to_string(),
|
||||
"new-refresh".to_string(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*revoke_previous_auth*/ true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1287,6 +1293,7 @@ mod tests {
|
||||
"new-access".to_string(),
|
||||
"shared-refresh".to_string(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*revoke_previous_auth*/ true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
force_state: Some(state),
|
||||
forced_chatgpt_workspace_id: Some(vec![chatgpt_account_id.to_string()]),
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -190,6 +191,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
|
||||
force_state: Some(state),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -233,6 +235,7 @@ async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result<
|
||||
WORKSPACE_ID_SECOND_ALLOWED.to_string(),
|
||||
]),
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let auth_url = Url::parse(&server.auth_url)?;
|
||||
@@ -271,6 +274,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -331,6 +335,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -399,6 +404,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -544,6 +550,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
force_state: Some("cancel_state".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
|
||||
let first_server = run_login_server(first_opts)?;
|
||||
@@ -565,6 +572,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
force_state: Some("cancel_state_2".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
revoke_previous_auth: true,
|
||||
};
|
||||
|
||||
let second_server = run_login_server(second_opts)?;
|
||||
|
||||
Reference in New Issue
Block a user