Compare commits

...

3 Commits

Author SHA1 Message Date
Richard Lee
9f3986e6b4 Checkpoint before e2e test 2026-04-06 15:08:56 -07:00
Richard Lee
24ce07949e Checkpoint before e2e test 2026-04-04 16:50:19 -07:00
Richard Lee
e6d2da4716 Initial Clippy 2026-04-04 13:20:29 -07:00
40 changed files with 4080 additions and 1 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1978,6 +1978,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-apply-patch",
"codex-arg0",
"codex-backend-client",
"codex-cloud-requirements",
"codex-core",
"codex-feedback",

View File

@@ -147,6 +147,115 @@
],
"type": "object"
},
"CodexAvatarAdminAwardGrantParams": {
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"awardedBy": {
"type": [
"string",
"null"
]
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"avatarId",
"awardId",
"sourceType"
],
"type": "object"
},
"CodexAvatarAdminCapabilitiesReadParams": {
"type": "object"
},
"CodexAvatarAdminProofDropGrantParams": {
"properties": {
"accountUserId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"awardId",
"sourceType"
],
"type": "object"
},
"CodexAvatarEquipParams": {
"properties": {
"avatarId": {
"type": "string"
}
},
"required": [
"avatarId"
],
"type": "object"
},
"CodexAvatarInventoryReadParams": {
"type": "object"
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
@@ -4538,6 +4647,126 @@
"title": "Account/rateLimits/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/inventory/read"
],
"title": "Avatar/inventory/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarInventoryReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/inventory/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/equip"
],
"title": "Avatar/equipRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarEquipParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/equipRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/admin/award"
],
"title": "Avatar/admin/awardRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarAdminAwardGrantParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/awardRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/admin/proof-drop"
],
"title": "Avatar/admin/proofDropRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarAdminProofDropGrantParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/proofDropRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/admin/capabilities/read"
],
"title": "Avatar/admin/capabilities/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarAdminCapabilitiesReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/capabilities/readRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -0,0 +1,54 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": "integer"
},
"awardedBy": {
"type": [
"string",
"null"
]
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"avatarId",
"awardId",
"awardedAt",
"sourceType"
],
"title": "CodexAvatarAward",
"type": "object"
}

View File

@@ -1319,6 +1319,126 @@
"title": "Account/rateLimits/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"avatar/inventory/read"
],
"title": "Avatar/inventory/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/CodexAvatarInventoryReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/inventory/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"avatar/equip"
],
"title": "Avatar/equipRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/CodexAvatarEquipParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/equipRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"avatar/admin/award"
],
"title": "Avatar/admin/awardRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/CodexAvatarAdminAwardGrantParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/awardRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"avatar/admin/proof-drop"
],
"title": "Avatar/admin/proofDropRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/CodexAvatarAdminProofDropGrantParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/proofDropRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"avatar/admin/capabilities/read"
],
"title": "Avatar/admin/capabilities/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/CodexAvatarAdminCapabilitiesReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/capabilities/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -1637,6 +1757,60 @@
],
"title": "ClientRequest"
},
"CodexAvatarAward": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": "integer"
},
"awardedBy": {
"type": [
"string",
"null"
]
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"avatarId",
"awardId",
"awardedAt",
"sourceType"
],
"title": "CodexAvatarAward",
"type": "object"
},
"CommandExecutionApprovalDecision": {
"oneOf": [
{
@@ -5553,6 +5727,426 @@
],
"type": "string"
},
"CodexAvatarAdminAwardGrantParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"awardedBy": {
"type": [
"string",
"null"
]
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"avatarId",
"awardId",
"sourceType"
],
"title": "CodexAvatarAdminAwardGrantParams",
"type": "object"
},
"CodexAvatarAdminCapabilitiesReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodexAvatarAdminCapabilitiesReadParams",
"type": "object"
},
"CodexAvatarAdminCapabilitiesReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"canGrantAwards": {
"type": "boolean"
},
"canGrantProofDropBoxes": {
"type": "boolean"
}
},
"required": [
"canGrantAwards",
"canGrantProofDropBoxes"
],
"title": "CodexAvatarAdminCapabilitiesReadResponse",
"type": "object"
},
"CodexAvatarAdminProofDropGrantParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"awardId",
"sourceType"
],
"title": "CodexAvatarAdminProofDropGrantParams",
"type": "object"
},
"CodexAvatarBoxOddsBucket": {
"properties": {
"bucketId": {
"type": "string"
},
"label": {
"type": "string"
},
"probabilityPercent": {
"format": "int64",
"type": "integer"
}
},
"required": [
"bucketId",
"label",
"probabilityPercent"
],
"type": "object"
},
"CodexAvatarBoxRules": {
"properties": {
"guaranteedNewThreshold": {
"format": "int64",
"type": "integer"
},
"legendaryPityThreshold": {
"format": "int64",
"type": "integer"
},
"odds": {
"items": {
"$ref": "#/definitions/v2/CodexAvatarBoxOddsBucket"
},
"type": "array"
},
"oddsTableVersion": {
"type": "string"
},
"rareOrBetterPityThreshold": {
"format": "int64",
"type": "integer"
},
"rulesetVersion": {
"type": "string"
}
},
"required": [
"guaranteedNewThreshold",
"legendaryPityThreshold",
"odds",
"oddsTableVersion",
"rareOrBetterPityThreshold",
"rulesetVersion"
],
"type": "object"
},
"CodexAvatarDefinition": {
"properties": {
"accentClassName": {
"type": "string"
},
"assetRef": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"collectionDescription": {
"type": "string"
},
"collectionName": {
"type": "string"
},
"description": {
"type": "string"
},
"displayName": {
"type": "string"
},
"isProgressVisible": {
"type": "boolean"
},
"lore": {
"type": "string"
},
"rarity": {
"$ref": "#/definitions/v2/CodexAvatarRarity"
},
"silhouetteGlowClassName": {
"type": "string"
},
"sortOrder": {
"format": "int64",
"type": "integer"
},
"status": {
"$ref": "#/definitions/v2/CodexAvatarStatus"
}
},
"required": [
"accentClassName",
"assetRef",
"avatarId",
"collectionDescription",
"collectionName",
"description",
"displayName",
"isProgressVisible",
"lore",
"rarity",
"silhouetteGlowClassName",
"sortOrder",
"status"
],
"type": "object"
},
"CodexAvatarEquipParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"avatarId": {
"type": "string"
}
},
"required": [
"avatarId"
],
"title": "CodexAvatarEquipParams",
"type": "object"
},
"CodexAvatarInventoryReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodexAvatarInventoryReadParams",
"type": "object"
},
"CodexAvatarInventoryReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarDefinitions": {
"items": {
"$ref": "#/definitions/v2/CodexAvatarDefinition"
},
"type": "array"
},
"boxRules": {
"$ref": "#/definitions/v2/CodexAvatarBoxRules"
},
"equippedAvatarId": {
"type": "string"
},
"ownedAvatars": {
"items": {
"$ref": "#/definitions/v2/CodexAvatarOwnership"
},
"type": "array"
},
"pendingRevealAwards": {
"items": {
"$ref": "#/definitions/v2/CodexAvatarRevealAward"
},
"type": "array"
},
"pityState": {
"$ref": "#/definitions/v2/CodexAvatarPityState"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"accountUserId",
"avatarDefinitions",
"boxRules",
"equippedAvatarId",
"ownedAvatars",
"pendingRevealAwards",
"pityState",
"updatedAt"
],
"title": "CodexAvatarInventoryReadResponse",
"type": "object"
},
"CodexAvatarOwnership": {
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"sourceSummary": {
"type": [
"string",
"null"
]
}
},
"required": [
"accountUserId",
"avatarId"
],
"type": "object"
},
"CodexAvatarPityState": {
"properties": {
"guaranteedNewAvailable": {
"type": "boolean"
},
"nonNewOutcomeStreak": {
"format": "int64",
"type": "integer"
},
"rollsSinceLegendary": {
"format": "int64",
"type": "integer"
},
"rollsSinceRareOrBetter": {
"format": "int64",
"type": "integer"
}
},
"required": [
"guaranteedNewAvailable",
"nonNewOutcomeStreak",
"rollsSinceLegendary",
"rollsSinceRareOrBetter"
],
"type": "object"
},
"CodexAvatarRarity": {
"enum": [
"common",
"rare",
"epic",
"legendary"
],
"type": "string"
},
"CodexAvatarRevealAward": {
"properties": {
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": "integer"
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"outcomeAvatarId": {
"type": [
"string",
"null"
]
},
"outcomeKind": {
"type": "string"
},
"pityStateAfter": {
"$ref": "#/definitions/v2/CodexAvatarPityState"
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"awardId",
"awardedAt",
"outcomeKind",
"pityStateAfter",
"sourceType"
],
"type": "object"
},
"CodexAvatarStatus": {
"enum": [
"active",
"hidden",
"retired"
],
"type": "string"
},
"CodexErrorInfo": {
"description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.",
"oneOf": [

View File

@@ -1894,6 +1894,126 @@
"title": "Account/rateLimits/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/inventory/read"
],
"title": "Avatar/inventory/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarInventoryReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/inventory/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/equip"
],
"title": "Avatar/equipRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarEquipParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/equipRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/admin/award"
],
"title": "Avatar/admin/awardRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarAdminAwardGrantParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/awardRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/admin/proof-drop"
],
"title": "Avatar/admin/proofDropRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarAdminProofDropGrantParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/proofDropRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"avatar/admin/capabilities/read"
],
"title": "Avatar/admin/capabilities/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CodexAvatarAdminCapabilitiesReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Avatar/admin/capabilities/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -2212,6 +2332,426 @@
],
"title": "ClientRequest"
},
"CodexAvatarAdminAwardGrantParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"awardedBy": {
"type": [
"string",
"null"
]
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"avatarId",
"awardId",
"sourceType"
],
"title": "CodexAvatarAdminAwardGrantParams",
"type": "object"
},
"CodexAvatarAdminCapabilitiesReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodexAvatarAdminCapabilitiesReadParams",
"type": "object"
},
"CodexAvatarAdminCapabilitiesReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"canGrantAwards": {
"type": "boolean"
},
"canGrantProofDropBoxes": {
"type": "boolean"
}
},
"required": [
"canGrantAwards",
"canGrantProofDropBoxes"
],
"title": "CodexAvatarAdminCapabilitiesReadResponse",
"type": "object"
},
"CodexAvatarAdminProofDropGrantParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"awardId",
"sourceType"
],
"title": "CodexAvatarAdminProofDropGrantParams",
"type": "object"
},
"CodexAvatarBoxOddsBucket": {
"properties": {
"bucketId": {
"type": "string"
},
"label": {
"type": "string"
},
"probabilityPercent": {
"format": "int64",
"type": "integer"
}
},
"required": [
"bucketId",
"label",
"probabilityPercent"
],
"type": "object"
},
"CodexAvatarBoxRules": {
"properties": {
"guaranteedNewThreshold": {
"format": "int64",
"type": "integer"
},
"legendaryPityThreshold": {
"format": "int64",
"type": "integer"
},
"odds": {
"items": {
"$ref": "#/definitions/CodexAvatarBoxOddsBucket"
},
"type": "array"
},
"oddsTableVersion": {
"type": "string"
},
"rareOrBetterPityThreshold": {
"format": "int64",
"type": "integer"
},
"rulesetVersion": {
"type": "string"
}
},
"required": [
"guaranteedNewThreshold",
"legendaryPityThreshold",
"odds",
"oddsTableVersion",
"rareOrBetterPityThreshold",
"rulesetVersion"
],
"type": "object"
},
"CodexAvatarDefinition": {
"properties": {
"accentClassName": {
"type": "string"
},
"assetRef": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"collectionDescription": {
"type": "string"
},
"collectionName": {
"type": "string"
},
"description": {
"type": "string"
},
"displayName": {
"type": "string"
},
"isProgressVisible": {
"type": "boolean"
},
"lore": {
"type": "string"
},
"rarity": {
"$ref": "#/definitions/CodexAvatarRarity"
},
"silhouetteGlowClassName": {
"type": "string"
},
"sortOrder": {
"format": "int64",
"type": "integer"
},
"status": {
"$ref": "#/definitions/CodexAvatarStatus"
}
},
"required": [
"accentClassName",
"assetRef",
"avatarId",
"collectionDescription",
"collectionName",
"description",
"displayName",
"isProgressVisible",
"lore",
"rarity",
"silhouetteGlowClassName",
"sortOrder",
"status"
],
"type": "object"
},
"CodexAvatarEquipParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"avatarId": {
"type": "string"
}
},
"required": [
"avatarId"
],
"title": "CodexAvatarEquipParams",
"type": "object"
},
"CodexAvatarInventoryReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodexAvatarInventoryReadParams",
"type": "object"
},
"CodexAvatarInventoryReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarDefinitions": {
"items": {
"$ref": "#/definitions/CodexAvatarDefinition"
},
"type": "array"
},
"boxRules": {
"$ref": "#/definitions/CodexAvatarBoxRules"
},
"equippedAvatarId": {
"type": "string"
},
"ownedAvatars": {
"items": {
"$ref": "#/definitions/CodexAvatarOwnership"
},
"type": "array"
},
"pendingRevealAwards": {
"items": {
"$ref": "#/definitions/CodexAvatarRevealAward"
},
"type": "array"
},
"pityState": {
"$ref": "#/definitions/CodexAvatarPityState"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"accountUserId",
"avatarDefinitions",
"boxRules",
"equippedAvatarId",
"ownedAvatars",
"pendingRevealAwards",
"pityState",
"updatedAt"
],
"title": "CodexAvatarInventoryReadResponse",
"type": "object"
},
"CodexAvatarOwnership": {
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"sourceSummary": {
"type": [
"string",
"null"
]
}
},
"required": [
"accountUserId",
"avatarId"
],
"type": "object"
},
"CodexAvatarPityState": {
"properties": {
"guaranteedNewAvailable": {
"type": "boolean"
},
"nonNewOutcomeStreak": {
"format": "int64",
"type": "integer"
},
"rollsSinceLegendary": {
"format": "int64",
"type": "integer"
},
"rollsSinceRareOrBetter": {
"format": "int64",
"type": "integer"
}
},
"required": [
"guaranteedNewAvailable",
"nonNewOutcomeStreak",
"rollsSinceLegendary",
"rollsSinceRareOrBetter"
],
"type": "object"
},
"CodexAvatarRarity": {
"enum": [
"common",
"rare",
"epic",
"legendary"
],
"type": "string"
},
"CodexAvatarRevealAward": {
"properties": {
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": "integer"
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"outcomeAvatarId": {
"type": [
"string",
"null"
]
},
"outcomeKind": {
"type": "string"
},
"pityStateAfter": {
"$ref": "#/definitions/CodexAvatarPityState"
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"awardId",
"awardedAt",
"outcomeKind",
"pityStateAfter",
"sourceType"
],
"type": "object"
},
"CodexAvatarStatus": {
"enum": [
"active",
"hidden",
"retired"
],
"type": "string"
},
"CodexErrorInfo": {
"description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.",
"oneOf": [

View File

@@ -0,0 +1,56 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"awardedBy": {
"type": [
"string",
"null"
]
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"accountUserId",
"avatarId",
"awardId",
"sourceType"
],
"title": "CodexAvatarAdminAwardGrantParams",
"type": "object"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodexAvatarAdminCapabilitiesReadParams",
"type": "object"
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"canGrantAwards": {
"type": "boolean"
},
"canGrantProofDropBoxes": {
"type": "boolean"
}
},
"required": [
"canGrantAwards",
"canGrantProofDropBoxes"
],
"title": "CodexAvatarAdminCapabilitiesReadResponse",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"avatarId": {
"type": "string"
}
},
"required": [
"avatarId"
],
"title": "CodexAvatarEquipParams",
"type": "object"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodexAvatarInventoryReadParams",
"type": "object"
}

View File

@@ -0,0 +1,286 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"CodexAvatarBoxOddsBucket": {
"properties": {
"bucketId": {
"type": "string"
},
"label": {
"type": "string"
},
"probabilityPercent": {
"format": "int64",
"type": "integer"
}
},
"required": [
"bucketId",
"label",
"probabilityPercent"
],
"type": "object"
},
"CodexAvatarBoxRules": {
"properties": {
"guaranteedNewThreshold": {
"format": "int64",
"type": "integer"
},
"legendaryPityThreshold": {
"format": "int64",
"type": "integer"
},
"odds": {
"items": {
"$ref": "#/definitions/CodexAvatarBoxOddsBucket"
},
"type": "array"
},
"oddsTableVersion": {
"type": "string"
},
"rareOrBetterPityThreshold": {
"format": "int64",
"type": "integer"
},
"rulesetVersion": {
"type": "string"
}
},
"required": [
"guaranteedNewThreshold",
"legendaryPityThreshold",
"odds",
"oddsTableVersion",
"rareOrBetterPityThreshold",
"rulesetVersion"
],
"type": "object"
},
"CodexAvatarDefinition": {
"properties": {
"accentClassName": {
"type": "string"
},
"assetRef": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"collectionDescription": {
"type": "string"
},
"collectionName": {
"type": "string"
},
"description": {
"type": "string"
},
"displayName": {
"type": "string"
},
"isProgressVisible": {
"type": "boolean"
},
"lore": {
"type": "string"
},
"rarity": {
"$ref": "#/definitions/CodexAvatarRarity"
},
"silhouetteGlowClassName": {
"type": "string"
},
"sortOrder": {
"format": "int64",
"type": "integer"
},
"status": {
"$ref": "#/definitions/CodexAvatarStatus"
}
},
"required": [
"accentClassName",
"assetRef",
"avatarId",
"collectionDescription",
"collectionName",
"description",
"displayName",
"isProgressVisible",
"lore",
"rarity",
"silhouetteGlowClassName",
"sortOrder",
"status"
],
"type": "object"
},
"CodexAvatarOwnership": {
"properties": {
"accountUserId": {
"type": "string"
},
"avatarId": {
"type": "string"
},
"sourceSummary": {
"type": [
"string",
"null"
]
}
},
"required": [
"accountUserId",
"avatarId"
],
"type": "object"
},
"CodexAvatarPityState": {
"properties": {
"guaranteedNewAvailable": {
"type": "boolean"
},
"nonNewOutcomeStreak": {
"format": "int64",
"type": "integer"
},
"rollsSinceLegendary": {
"format": "int64",
"type": "integer"
},
"rollsSinceRareOrBetter": {
"format": "int64",
"type": "integer"
}
},
"required": [
"guaranteedNewAvailable",
"nonNewOutcomeStreak",
"rollsSinceLegendary",
"rollsSinceRareOrBetter"
],
"type": "object"
},
"CodexAvatarRarity": {
"enum": [
"common",
"rare",
"epic",
"legendary"
],
"type": "string"
},
"CodexAvatarRevealAward": {
"properties": {
"awardId": {
"type": "string"
},
"awardedAt": {
"format": "int64",
"type": "integer"
},
"metadataJson": {
"type": [
"string",
"null"
]
},
"outcomeAvatarId": {
"type": [
"string",
"null"
]
},
"outcomeKind": {
"type": "string"
},
"pityStateAfter": {
"$ref": "#/definitions/CodexAvatarPityState"
},
"sourceRef": {
"type": [
"string",
"null"
]
},
"sourceSummary": {
"type": [
"string",
"null"
]
},
"sourceType": {
"type": "string"
}
},
"required": [
"awardId",
"awardedAt",
"outcomeKind",
"pityStateAfter",
"sourceType"
],
"type": "object"
},
"CodexAvatarStatus": {
"enum": [
"active",
"hidden",
"retired"
],
"type": "string"
}
},
"properties": {
"accountUserId": {
"type": "string"
},
"avatarDefinitions": {
"items": {
"$ref": "#/definitions/CodexAvatarDefinition"
},
"type": "array"
},
"boxRules": {
"$ref": "#/definitions/CodexAvatarBoxRules"
},
"equippedAvatarId": {
"type": "string"
},
"ownedAvatars": {
"items": {
"$ref": "#/definitions/CodexAvatarOwnership"
},
"type": "array"
},
"pendingRevealAwards": {
"items": {
"$ref": "#/definitions/CodexAvatarRevealAward"
},
"type": "array"
},
"pityState": {
"$ref": "#/definitions/CodexAvatarPityState"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"accountUserId",
"avatarDefinitions",
"boxRules",
"equippedAvatarId",
"ownedAvatars",
"pendingRevealAwards",
"pityState",
"updatedAt"
],
"title": "CodexAvatarInventoryReadResponse",
"type": "object"
}

File diff suppressed because one or more lines are too long

View 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 CodexAvatarAdminAwardGrantParams = { accountUserId: string, awardId: string, avatarId: string, sourceType: string, sourceRef?: string | null, awardedAt?: bigint | null, awardedBy?: string | null, metadataJson?: string | null, sourceSummary?: string | null, };

View 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 CodexAvatarAdminCapabilitiesReadParams = Record<string, never>;

View 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 CodexAvatarAdminCapabilitiesReadResponse = { canGrantAwards: boolean, canGrantProofDropBoxes: boolean, };

View 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 CodexAvatarAward = { awardId: string, accountUserId: string, avatarId: string, sourceType: string, sourceRef: string | null, awardedAt: bigint, awardedBy: string | null, metadataJson: string | null, sourceSummary: string | null, };

View 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 { CodexAvatarRarity } from "./CodexAvatarRarity";
import type { CodexAvatarStatus } from "./CodexAvatarStatus";
export type CodexAvatarDefinition = { avatarId: string, displayName: string, description: string, rarity: CodexAvatarRarity, assetRef: string, status: CodexAvatarStatus, sortOrder: bigint, collectionName: string, collectionDescription: string, lore: string, accentClassName: string, silhouetteGlowClassName: string, isProgressVisible: boolean, };

View 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 CodexAvatarEquipParams = { avatarId: string, };

View 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 CodexAvatarInventoryReadParams = Record<string, never>;

View File

@@ -0,0 +1,10 @@
// 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 { CodexAvatarBoxRules } from "./CodexAvatarBoxRules";
import type { CodexAvatarDefinition } from "./CodexAvatarDefinition";
import type { CodexAvatarOwnership } from "./CodexAvatarOwnership";
import type { CodexAvatarPityState } from "./CodexAvatarPityState";
import type { CodexAvatarRevealAward } from "./CodexAvatarRevealAward";
export type CodexAvatarInventoryReadResponse = { accountUserId: string, avatarDefinitions: Array<CodexAvatarDefinition>, ownedAvatars: Array<CodexAvatarOwnership>, equippedAvatarId: string, boxRules: CodexAvatarBoxRules, pityState: CodexAvatarPityState, pendingRevealAwards: Array<CodexAvatarRevealAward>, updatedAt: bigint, };

View 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 CodexAvatarOwnership = { accountUserId: string, avatarId: string, sourceSummary: string | null, };

View 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 CodexAvatarRarity = "common" | "rare" | "epic" | "legendary";

View 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 CodexAvatarStatus = "active" | "hidden" | "retired";

View File

@@ -31,6 +31,22 @@ export type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus";
export type { ChatgptAuthTokensRefreshParams } from "./ChatgptAuthTokensRefreshParams";
export type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason";
export type { ChatgptAuthTokensRefreshResponse } from "./ChatgptAuthTokensRefreshResponse";
export type { CodexAvatarAdminAwardGrantParams } from "./CodexAvatarAdminAwardGrantParams";
export type { CodexAvatarAdminCapabilitiesReadParams } from "./CodexAvatarAdminCapabilitiesReadParams";
export type { CodexAvatarAdminCapabilitiesReadResponse } from "./CodexAvatarAdminCapabilitiesReadResponse";
export type { CodexAvatarAdminProofDropGrantParams } from "./CodexAvatarAdminProofDropGrantParams";
export type { CodexAvatarAward } from "./CodexAvatarAward";
export type { CodexAvatarBoxOddsBucket } from "./CodexAvatarBoxOddsBucket";
export type { CodexAvatarBoxRules } from "./CodexAvatarBoxRules";
export type { CodexAvatarDefinition } from "./CodexAvatarDefinition";
export type { CodexAvatarEquipParams } from "./CodexAvatarEquipParams";
export type { CodexAvatarInventoryReadParams } from "./CodexAvatarInventoryReadParams";
export type { CodexAvatarInventoryReadResponse } from "./CodexAvatarInventoryReadResponse";
export type { CodexAvatarOwnership } from "./CodexAvatarOwnership";
export type { CodexAvatarPityState } from "./CodexAvatarPityState";
export type { CodexAvatarRarity } from "./CodexAvatarRarity";
export type { CodexAvatarRevealAward } from "./CodexAvatarRevealAward";
export type { CodexAvatarStatus } from "./CodexAvatarStatus";
export type { CodexErrorInfo } from "./CodexErrorInfo";
export type { CollabAgentState } from "./CollabAgentState";
export type { CollabAgentStatus } from "./CollabAgentStatus";

View File

@@ -118,6 +118,7 @@ pub fn generate_ts_with_options(
ServerRequest::export_all_to(out_dir)?;
export_server_responses(out_dir)?;
ServerNotification::export_all_to(out_dir)?;
crate::protocol::v2::CodexAvatarAward::export_all_to(out_dir)?;
if !options.experimental_api {
filter_experimental_ts(out_dir)?;
@@ -206,6 +207,12 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -
|d| write_json_schema_with_return::<crate::ServerRequest>(d, "ServerRequest"),
|d| write_json_schema_with_return::<crate::ClientNotification>(d, "ClientNotification"),
|d| write_json_schema_with_return::<crate::ServerNotification>(d, "ServerNotification"),
|d| {
write_json_schema_with_return::<crate::protocol::v2::CodexAvatarAward>(
d,
"CodexAvatarAward",
)
},
];
let mut schemas: Vec<GeneratedSchema> = Vec::new();

View File

@@ -485,6 +485,31 @@ client_request_definitions! {
response: v2::GetAccountRateLimitsResponse,
},
AvatarInventoryRead => "avatar/inventory/read" {
params: v2::CodexAvatarInventoryReadParams,
response: v2::CodexAvatarInventoryReadResponse,
},
AvatarEquip => "avatar/equip" {
params: v2::CodexAvatarEquipParams,
response: v2::CodexAvatarInventoryReadResponse,
},
AvatarAdminAward => "avatar/admin/award" {
params: v2::CodexAvatarAdminAwardGrantParams,
response: v2::CodexAvatarInventoryReadResponse,
},
AvatarAdminProofDrop => "avatar/admin/proof-drop" {
params: v2::CodexAvatarAdminProofDropGrantParams,
response: v2::CodexAvatarInventoryReadResponse,
},
AvatarAdminCapabilitiesRead => "avatar/admin/capabilities/read" {
params: v2::CodexAvatarAdminCapabilitiesReadParams,
response: v2::CodexAvatarAdminCapabilitiesReadResponse,
},
FeedbackUpload => "feedback/upload" {
params: v2::FeedbackUploadParams,
response: v2::FeedbackUploadResponse,
@@ -1517,6 +1542,129 @@ mod tests {
Ok(())
}
#[test]
fn serialize_avatar_inventory_read() -> Result<()> {
let request = ClientRequest::AvatarInventoryRead {
request_id: RequestId::Integer(7),
params: v2::CodexAvatarInventoryReadParams::default(),
};
assert_eq!(
json!({
"method": "avatar/inventory/read",
"id": 7,
"params": {}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_avatar_equip() -> Result<()> {
let request = ClientRequest::AvatarEquip {
request_id: RequestId::Integer(8),
params: v2::CodexAvatarEquipParams {
avatar_id: "clippy".to_string(),
},
};
assert_eq!(
json!({
"method": "avatar/equip",
"id": 8,
"params": {
"avatarId": "clippy"
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_avatar_admin_award() -> Result<()> {
let request = ClientRequest::AvatarAdminAward {
request_id: RequestId::Integer(9),
params: v2::CodexAvatarAdminAwardGrantParams {
account_user_id: "target-user-123".to_string(),
award_id: "manual-grant-1".to_string(),
avatar_id: "prism".to_string(),
source_type: "manual-admin-grant".to_string(),
source_ref: Some("support-ticket-1".to_string()),
awarded_at: Some(123),
awarded_by: Some("admin-user".to_string()),
metadata_json: Some("{\"reason\":\"support\"}".to_string()),
source_summary: Some("Manual support grant".to_string()),
},
};
assert_eq!(
json!({
"method": "avatar/admin/award",
"id": 9,
"params": {
"accountUserId": "target-user-123",
"awardId": "manual-grant-1",
"avatarId": "prism",
"sourceType": "manual-admin-grant",
"sourceRef": "support-ticket-1",
"awardedAt": 123,
"awardedBy": "admin-user",
"metadataJson": "{\"reason\":\"support\"}",
"sourceSummary": "Manual support grant"
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_avatar_admin_proof_drop() -> Result<()> {
let request = ClientRequest::AvatarAdminProofDrop {
request_id: RequestId::Integer(10),
params: v2::CodexAvatarAdminProofDropGrantParams {
account_user_id: "target-user-123".to_string(),
award_id: "proof-drop-1".to_string(),
source_type: "proof-drop-box".to_string(),
source_ref: Some("support-ticket-1".to_string()),
awarded_at: Some(123),
source_summary: Some("Manual proof-drop box".to_string()),
},
};
assert_eq!(
json!({
"method": "avatar/admin/proof-drop",
"id": 10,
"params": {
"accountUserId": "target-user-123",
"awardId": "proof-drop-1",
"sourceType": "proof-drop-box",
"sourceRef": "support-ticket-1",
"awardedAt": 123,
"sourceSummary": "Manual proof-drop box"
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_avatar_admin_capabilities_read() -> Result<()> {
let request = ClientRequest::AvatarAdminCapabilitiesRead {
request_id: RequestId::Integer(11),
params: v2::CodexAvatarAdminCapabilitiesReadParams::default(),
};
assert_eq!(
json!({
"method": "avatar/admin/capabilities/read",
"id": 11,
"params": {}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn account_serializes_fields_in_camel_case() -> Result<()> {
let api_key = v2::Account::ApiKey {};

View File

@@ -1737,6 +1737,188 @@ pub struct GetAccountResponse {
pub requires_openai_auth: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum CodexAvatarStatus {
Active,
Hidden,
Retired,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum CodexAvatarRarity {
Common,
Rare,
Epic,
Legendary,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarDefinition {
pub avatar_id: String,
pub display_name: String,
pub description: String,
pub rarity: CodexAvatarRarity,
pub asset_ref: String,
pub status: CodexAvatarStatus,
pub sort_order: i64,
pub collection_name: String,
pub collection_description: String,
pub lore: String,
pub accent_class_name: String,
pub silhouette_glow_class_name: String,
pub is_progress_visible: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarOwnership {
pub account_user_id: String,
pub avatar_id: String,
pub source_summary: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarAward {
pub award_id: String,
pub account_user_id: String,
pub avatar_id: String,
pub source_type: String,
pub source_ref: Option<String>,
pub awarded_at: i64,
pub awarded_by: Option<String>,
pub metadata_json: Option<String>,
pub source_summary: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarInventoryReadParams {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarEquipParams {
pub avatar_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarAdminAwardGrantParams {
pub account_user_id: String,
pub award_id: String,
pub avatar_id: String,
pub source_type: String,
#[ts(optional = nullable)]
pub source_ref: Option<String>,
#[ts(optional = nullable)]
pub awarded_at: Option<i64>,
#[ts(optional = nullable)]
pub awarded_by: Option<String>,
#[ts(optional = nullable)]
pub metadata_json: Option<String>,
#[ts(optional = nullable)]
pub source_summary: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarAdminProofDropGrantParams {
pub account_user_id: String,
pub award_id: String,
pub source_type: String,
#[ts(optional = nullable)]
pub source_ref: Option<String>,
#[ts(optional = nullable)]
pub awarded_at: Option<i64>,
#[ts(optional = nullable)]
pub source_summary: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarAdminCapabilitiesReadParams {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarAdminCapabilitiesReadResponse {
pub can_grant_awards: bool,
pub can_grant_proof_drop_boxes: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarBoxOddsBucket {
pub bucket_id: String,
pub label: String,
pub probability_percent: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarPityState {
pub rolls_since_rare_or_better: i64,
pub rolls_since_legendary: i64,
pub non_new_outcome_streak: i64,
pub guaranteed_new_available: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarBoxRules {
pub ruleset_version: String,
pub odds_table_version: String,
pub rare_or_better_pity_threshold: i64,
pub legendary_pity_threshold: i64,
pub guaranteed_new_threshold: i64,
pub odds: Vec<CodexAvatarBoxOddsBucket>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarRevealAward {
pub award_id: String,
pub awarded_at: i64,
pub source_type: String,
pub source_ref: Option<String>,
pub source_summary: Option<String>,
pub outcome_kind: String,
pub outcome_avatar_id: Option<String>,
pub metadata_json: Option<String>,
pub pity_state_after: CodexAvatarPityState,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CodexAvatarInventoryReadResponse {
pub account_user_id: String,
pub avatar_definitions: Vec<CodexAvatarDefinition>,
pub owned_avatars: Vec<CodexAvatarOwnership>,
pub equipped_avatar_id: String,
pub box_rules: CodexAvatarBoxRules,
pub pity_state: CodexAvatarPityState,
pub pending_reveal_awards: Vec<CodexAvatarRevealAward>,
pub updated_at: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View File

@@ -7,6 +7,7 @@ use crate::export::filter_experimental_ts_tree;
use crate::export::generate_index_ts_tree;
use crate::protocol::common::visit_client_response_types;
use crate::protocol::common::visit_server_response_types;
use crate::protocol::v2::CodexAvatarAward;
use anyhow::Context;
use anyhow::Result;
use serde_json::Map;
@@ -65,6 +66,7 @@ pub fn generate_typescript_schema_fixture_subtree_for_tests() -> Result<BTreeMap
visit_server_response_types(visitor);
})?;
collect_typescript_fixture_file::<ServerNotification>(&mut files, &mut seen)?;
collect_typescript_fixture_file::<CodexAvatarAward>(&mut files, &mut seen)?;
filter_experimental_ts_tree(&mut files)?;
generate_index_ts_tree(&mut files);

View File

@@ -0,0 +1,284 @@
use crate::error_code::INTERNAL_ERROR_CODE;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use codex_app_server_protocol::CodexAvatarAdminAwardGrantParams;
use codex_app_server_protocol::CodexAvatarAdminCapabilitiesReadResponse;
use codex_app_server_protocol::CodexAvatarAdminProofDropGrantParams;
use codex_app_server_protocol::CodexAvatarBoxOddsBucket;
use codex_app_server_protocol::CodexAvatarBoxRules;
use codex_app_server_protocol::CodexAvatarDefinition;
use codex_app_server_protocol::CodexAvatarEquipParams;
use codex_app_server_protocol::CodexAvatarInventoryReadResponse;
use codex_app_server_protocol::CodexAvatarOwnership;
use codex_app_server_protocol::CodexAvatarPityState;
use codex_app_server_protocol::CodexAvatarRarity;
use codex_app_server_protocol::CodexAvatarRevealAward;
use codex_app_server_protocol::CodexAvatarStatus;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_backend_client::Client as BackendClient;
use codex_backend_client::CodexAvatarAdminAwardGrantRequest as BackendAvatarAdminAwardGrantRequest;
use codex_backend_client::CodexAvatarAdminCapabilitiesResponse as BackendAvatarAdminCapabilitiesResponse;
use codex_backend_client::CodexAvatarAdminProofDropGrantRequest as BackendAvatarAdminProofDropGrantRequest;
use codex_backend_client::CodexAvatarBoxOddsBucket as BackendAvatarBoxOddsBucket;
use codex_backend_client::CodexAvatarBoxRules as BackendAvatarBoxRules;
use codex_backend_client::CodexAvatarDefinition as BackendAvatarDefinition;
use codex_backend_client::CodexAvatarInventoryResponse as BackendAvatarInventoryResponse;
use codex_backend_client::CodexAvatarOwnership as BackendAvatarOwnership;
use codex_backend_client::CodexAvatarPityState as BackendAvatarPityState;
use codex_backend_client::CodexAvatarRarity as BackendAvatarRarity;
use codex_backend_client::CodexAvatarRevealAward as BackendAvatarRevealAward;
use codex_backend_client::CodexAvatarStatus as BackendAvatarStatus;
use codex_backend_client::RequestError;
use codex_core::AuthManager;
use serde_json::Value;
pub(crate) async fn read_avatar_inventory(
auth_manager: &AuthManager,
chatgpt_base_url: &str,
) -> Result<CodexAvatarInventoryReadResponse, JSONRPCErrorError> {
let client = avatar_backend_client(auth_manager, chatgpt_base_url).await?;
let response = client
.get_avatar_inventory()
.await
.map_err(|err| backend_avatar_error("read avatar inventory", err))?;
Ok(map_avatar_inventory_response(response))
}
pub(crate) async fn equip_avatar(
auth_manager: &AuthManager,
chatgpt_base_url: &str,
params: CodexAvatarEquipParams,
) -> Result<CodexAvatarInventoryReadResponse, JSONRPCErrorError> {
let client = avatar_backend_client(auth_manager, chatgpt_base_url).await?;
let response = client
.equip_avatar(params.avatar_id)
.await
.map_err(|err| backend_avatar_error("equip avatar", err))?;
Ok(map_avatar_inventory_response(response))
}
pub(crate) async fn grant_admin_avatar_award(
auth_manager: &AuthManager,
chatgpt_base_url: &str,
params: CodexAvatarAdminAwardGrantParams,
) -> Result<CodexAvatarInventoryReadResponse, JSONRPCErrorError> {
let client = avatar_backend_client(auth_manager, chatgpt_base_url).await?;
let response = client
.grant_admin_avatar_award(BackendAvatarAdminAwardGrantRequest {
account_user_id: params.account_user_id,
award_id: params.award_id,
avatar_id: params.avatar_id,
source_type: params.source_type,
source_ref: params.source_ref,
awarded_at: params.awarded_at,
awarded_by: params.awarded_by,
metadata_json: params.metadata_json,
source_summary: params.source_summary,
})
.await
.map_err(|err| backend_avatar_error("grant avatar award", err))?;
Ok(map_avatar_inventory_response(response))
}
pub(crate) async fn grant_admin_avatar_proof_drop(
auth_manager: &AuthManager,
chatgpt_base_url: &str,
params: CodexAvatarAdminProofDropGrantParams,
) -> Result<CodexAvatarInventoryReadResponse, JSONRPCErrorError> {
let client = avatar_backend_client(auth_manager, chatgpt_base_url).await?;
let response = client
.grant_admin_avatar_proof_drop(BackendAvatarAdminProofDropGrantRequest {
account_user_id: params.account_user_id,
award_id: params.award_id,
source_type: params.source_type,
source_ref: params.source_ref,
awarded_at: params.awarded_at,
source_summary: params.source_summary,
})
.await
.map_err(|err| backend_avatar_error("grant proof-drop box", err))?;
Ok(map_avatar_inventory_response(response))
}
pub(crate) async fn read_avatar_admin_capabilities(
auth_manager: &AuthManager,
chatgpt_base_url: &str,
) -> Result<CodexAvatarAdminCapabilitiesReadResponse, JSONRPCErrorError> {
let client = avatar_backend_client(auth_manager, chatgpt_base_url).await?;
let response = client
.get_avatar_admin_capabilities()
.await
.map_err(|err| backend_avatar_error("read avatar admin capabilities", err))?;
Ok(map_avatar_admin_capabilities_response(response))
}
async fn avatar_backend_client(
auth_manager: &AuthManager,
chatgpt_base_url: &str,
) -> Result<BackendClient, JSONRPCErrorError> {
let Some(auth) = auth_manager.auth().await else {
return Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: "codex account authentication required to manage avatars".to_string(),
data: None,
});
};
if !auth.is_chatgpt_auth() {
return Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: "chatgpt authentication required to manage avatars".to_string(),
data: None,
});
}
BackendClient::from_auth(chatgpt_base_url.to_string(), &auth).map_err(|err| JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to construct backend client: {err}"),
data: None,
})
}
fn backend_avatar_error(action: &str, err: RequestError) -> JSONRPCErrorError {
match &err {
RequestError::UnexpectedStatus { status, body, .. }
if status.as_u16() == 400 || status.as_u16() == 403 =>
{
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: avatar_error_detail(body)
.unwrap_or_else(|| format!("failed to {action}: {err}")),
data: None,
}
}
_ => JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to {action}: {err}"),
data: None,
},
}
}
fn avatar_error_detail(body: &str) -> Option<String> {
let value: Value = serde_json::from_str(body).ok()?;
value
.get("detail")
.and_then(Value::as_str)
.map(str::to_string)
}
fn map_avatar_inventory_response(
response: BackendAvatarInventoryResponse,
) -> CodexAvatarInventoryReadResponse {
CodexAvatarInventoryReadResponse {
account_user_id: response.account_user_id,
avatar_definitions: response
.avatar_definitions
.into_iter()
.map(map_avatar_definition)
.collect(),
owned_avatars: response
.owned_avatars
.into_iter()
.map(map_avatar_ownership)
.collect(),
equipped_avatar_id: response.equipped_avatar_id,
box_rules: map_avatar_box_rules(response.box_rules),
pity_state: map_avatar_pity_state(response.pity_state),
pending_reveal_awards: response
.pending_reveal_awards
.into_iter()
.map(map_avatar_reveal_award)
.collect(),
updated_at: response.updated_at,
}
}
fn map_avatar_admin_capabilities_response(
response: BackendAvatarAdminCapabilitiesResponse,
) -> CodexAvatarAdminCapabilitiesReadResponse {
CodexAvatarAdminCapabilitiesReadResponse {
can_grant_awards: response.can_grant_awards,
can_grant_proof_drop_boxes: response.can_grant_proof_drop_boxes,
}
}
fn map_avatar_box_rules(rules: BackendAvatarBoxRules) -> CodexAvatarBoxRules {
CodexAvatarBoxRules {
ruleset_version: rules.ruleset_version,
odds_table_version: rules.odds_table_version,
rare_or_better_pity_threshold: rules.rare_or_better_pity_threshold,
legendary_pity_threshold: rules.legendary_pity_threshold,
guaranteed_new_threshold: rules.guaranteed_new_threshold,
odds: rules
.odds
.into_iter()
.map(map_avatar_box_odds_bucket)
.collect(),
}
}
fn map_avatar_box_odds_bucket(bucket: BackendAvatarBoxOddsBucket) -> CodexAvatarBoxOddsBucket {
CodexAvatarBoxOddsBucket {
bucket_id: bucket.bucket_id,
label: bucket.label,
probability_percent: bucket.probability_percent,
}
}
fn map_avatar_pity_state(pity_state: BackendAvatarPityState) -> CodexAvatarPityState {
CodexAvatarPityState {
rolls_since_rare_or_better: pity_state.rolls_since_rare_or_better,
rolls_since_legendary: pity_state.rolls_since_legendary,
non_new_outcome_streak: pity_state.non_new_outcome_streak,
guaranteed_new_available: pity_state.guaranteed_new_available,
}
}
fn map_avatar_reveal_award(award: BackendAvatarRevealAward) -> CodexAvatarRevealAward {
CodexAvatarRevealAward {
award_id: award.award_id,
awarded_at: award.awarded_at,
source_type: award.source_type,
source_ref: award.source_ref,
source_summary: award.source_summary,
outcome_kind: award.outcome_kind,
outcome_avatar_id: award.outcome_avatar_id,
metadata_json: award.metadata_json,
pity_state_after: map_avatar_pity_state(award.pity_state_after),
}
}
fn map_avatar_definition(definition: BackendAvatarDefinition) -> CodexAvatarDefinition {
CodexAvatarDefinition {
avatar_id: definition.avatar_id,
display_name: definition.display_name,
description: definition.description,
rarity: match definition.rarity {
BackendAvatarRarity::Common => CodexAvatarRarity::Common,
BackendAvatarRarity::Rare => CodexAvatarRarity::Rare,
BackendAvatarRarity::Epic => CodexAvatarRarity::Epic,
BackendAvatarRarity::Legendary => CodexAvatarRarity::Legendary,
},
asset_ref: definition.asset_ref,
status: match definition.status {
BackendAvatarStatus::Active => CodexAvatarStatus::Active,
BackendAvatarStatus::Hidden => CodexAvatarStatus::Hidden,
BackendAvatarStatus::Retired => CodexAvatarStatus::Retired,
},
sort_order: definition.sort_order,
collection_name: definition.collection_name,
collection_description: definition.collection_description,
lore: definition.lore,
accent_class_name: definition.accent_class_name,
silhouette_glow_class_name: definition.silhouette_glow_class_name,
is_progress_visible: definition.is_progress_visible,
}
}
fn map_avatar_ownership(ownership: BackendAvatarOwnership) -> CodexAvatarOwnership {
CodexAvatarOwnership {
account_user_id: ownership.account_user_id,
avatar_id: ownership.avatar_id,
source_summary: ownership.source_summary,
}
}

View File

@@ -1,3 +1,4 @@
use crate::avatar_rpc;
use crate::bespoke_event_handling::apply_bespoke_event_handling;
use crate::command_exec::CommandExecManager;
use crate::command_exec::StartCommandExecParams;
@@ -33,6 +34,9 @@ use codex_app_server_protocol::CancelLoginAccountResponse;
use codex_app_server_protocol::CancelLoginAccountStatus;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ClientResponse;
use codex_app_server_protocol::CodexAvatarAdminAwardGrantParams;
use codex_app_server_protocol::CodexAvatarAdminProofDropGrantParams;
use codex_app_server_protocol::CodexAvatarEquipParams;
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
use codex_app_server_protocol::CollaborationModeListParams;
use codex_app_server_protocol::CollaborationModeListResponse;
@@ -904,6 +908,32 @@ impl CodexMessageProcessor {
self.get_account(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::AvatarInventoryRead {
request_id,
params: _,
} => {
self.avatar_inventory_read(to_connection_request_id(request_id))
.await;
}
ClientRequest::AvatarEquip { request_id, params } => {
self.avatar_equip(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::AvatarAdminAward { request_id, params } => {
self.avatar_admin_award(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::AvatarAdminProofDrop { request_id, params } => {
self.avatar_admin_proof_drop(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::AvatarAdminCapabilitiesRead {
request_id,
params: _,
} => {
self.avatar_admin_capabilities_read(to_connection_request_id(request_id))
.await;
}
ClientRequest::GitDiffToRemote { request_id, params } => {
self.git_diff_to_origin(to_connection_request_id(request_id), params.cwd)
.await;
@@ -1686,6 +1716,90 @@ impl CodexMessageProcessor {
}
}
async fn avatar_inventory_read(&self, request_id: ConnectionRequestId) {
match avatar_rpc::read_avatar_inventory(&self.auth_manager, &self.config.chatgpt_base_url)
.await
{
Ok(response) => {
self.outgoing.send_response(request_id, response).await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
}
}
async fn avatar_equip(&self, request_id: ConnectionRequestId, params: CodexAvatarEquipParams) {
match avatar_rpc::equip_avatar(&self.auth_manager, &self.config.chatgpt_base_url, params)
.await
{
Ok(response) => {
self.outgoing.send_response(request_id, response).await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
}
}
async fn avatar_admin_award(
&self,
request_id: ConnectionRequestId,
params: CodexAvatarAdminAwardGrantParams,
) {
match avatar_rpc::grant_admin_avatar_award(
&self.auth_manager,
&self.config.chatgpt_base_url,
params,
)
.await
{
Ok(response) => {
self.outgoing.send_response(request_id, response).await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
}
}
async fn avatar_admin_proof_drop(
&self,
request_id: ConnectionRequestId,
params: CodexAvatarAdminProofDropGrantParams,
) {
match avatar_rpc::grant_admin_avatar_proof_drop(
&self.auth_manager,
&self.config.chatgpt_base_url,
params,
)
.await
{
Ok(response) => {
self.outgoing.send_response(request_id, response).await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
}
}
async fn avatar_admin_capabilities_read(&self, request_id: ConnectionRequestId) {
match avatar_rpc::read_avatar_admin_capabilities(
&self.auth_manager,
&self.config.chatgpt_base_url,
)
.await
{
Ok(response) => {
self.outgoing.send_response(request_id, response).await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
}
}
async fn fetch_account_rate_limits(
&self,
) -> Result<

View File

@@ -61,6 +61,7 @@ use tracing_subscriber::registry::Registry;
use tracing_subscriber::util::SubscriberInitExt;
mod app_server_tracing;
mod avatar_rpc;
mod bespoke_event_handling;
mod codex_message_processor;
mod command_exec;

View File

@@ -15,6 +15,9 @@ use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientNotification;
use codex_app_server_protocol::CodexAvatarAdminAwardGrantParams;
use codex_app_server_protocol::CodexAvatarAdminProofDropGrantParams;
use codex_app_server_protocol::CodexAvatarEquipParams;
use codex_app_server_protocol::CollaborationModeListParams;
use codex_app_server_protocol::CommandExecParams;
use codex_app_server_protocol::CommandExecResizeParams;
@@ -296,6 +299,46 @@ impl McpProcess {
self.send_request("account/read", params).await
}
/// Send an `avatar/inventory/read` JSON-RPC request.
pub async fn send_avatar_inventory_read_request(&mut self) -> anyhow::Result<i64> {
let params = Some(serde_json::json!({}));
self.send_request("avatar/inventory/read", params).await
}
/// Send an `avatar/equip` JSON-RPC request.
pub async fn send_avatar_equip_request(
&mut self,
params: CodexAvatarEquipParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("avatar/equip", params).await
}
/// Send an `avatar/admin/award` JSON-RPC request.
pub async fn send_avatar_admin_award_request(
&mut self,
params: CodexAvatarAdminAwardGrantParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("avatar/admin/award", params).await
}
/// Send an `avatar/admin/proof-drop` JSON-RPC request.
pub async fn send_avatar_admin_proof_drop_request(
&mut self,
params: CodexAvatarAdminProofDropGrantParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("avatar/admin/proof-drop", params).await
}
/// Send an `avatar/admin/capabilities/read` JSON-RPC request.
pub async fn send_avatar_admin_capabilities_read_request(&mut self) -> anyhow::Result<i64> {
let params = Some(serde_json::json!({}));
self.send_request("avatar/admin/capabilities/read", params)
.await
}
/// Send an `account/login/start` JSON-RPC request with ChatGPT auth tokens.
pub async fn send_chatgpt_auth_tokens_login_request(
&mut self,

View File

@@ -0,0 +1,686 @@
use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::CodexAvatarAdminAwardGrantParams;
use codex_app_server_protocol::CodexAvatarAdminCapabilitiesReadResponse;
use codex_app_server_protocol::CodexAvatarAdminProofDropGrantParams;
use codex_app_server_protocol::CodexAvatarBoxOddsBucket;
use codex_app_server_protocol::CodexAvatarBoxRules;
use codex_app_server_protocol::CodexAvatarDefinition;
use codex_app_server_protocol::CodexAvatarEquipParams;
use codex_app_server_protocol::CodexAvatarInventoryReadResponse;
use codex_app_server_protocol::CodexAvatarOwnership;
use codex_app_server_protocol::CodexAvatarPityState;
use codex_app_server_protocol::CodexAvatarRarity;
use codex_app_server_protocol::CodexAvatarRevealAward;
use codex_app_server_protocol::CodexAvatarStatus;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::RequestId;
use codex_core::auth::AuthCredentialsStoreMode;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::body_json;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
#[tokio::test]
async fn avatar_inventory_read_requires_auth() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp.send_avatar_inventory_read_request().await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.id, RequestId::Integer(request_id));
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(
error.error.message,
"codex account authentication required to manage avatars"
);
Ok(())
}
#[tokio::test]
async fn avatar_inventory_read_requires_chatgpt_auth() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let login_request_id = mcp
.send_login_account_api_key_request("sk-test-key")
.await?;
let login_response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(login_request_id)),
)
.await??;
let login: LoginAccountResponse = to_response(login_response)?;
assert_eq!(login, LoginAccountResponse::ApiKey {});
let request_id = mcp.send_avatar_inventory_read_request().await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.id, RequestId::Integer(request_id));
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(
error.error.message,
"chatgpt authentication required to manage avatars"
);
Ok(())
}
#[tokio::test]
async fn avatar_inventory_read_returns_snapshot() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("GET"))
.and(path("/api/codex/avatars/inventory"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(200).set_body_json(snapshot_json("prism")))
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp.send_avatar_inventory_read_request().await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: CodexAvatarInventoryReadResponse = to_response(response)?;
assert_eq!(received, expected_snapshot("prism"));
Ok(())
}
#[tokio::test]
async fn avatar_equip_forwards_backend_validation_error() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("POST"))
.and(path("/api/codex/avatars/equip"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
"detail": "cannot equip unowned avatar prism",
})))
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_avatar_equip_request(CodexAvatarEquipParams {
avatar_id: "prism".to_string(),
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.id, RequestId::Integer(request_id));
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(error.error.message, "cannot equip unowned avatar prism");
Ok(())
}
#[tokio::test]
async fn avatar_equip_returns_snapshot_with_clippy_fallback() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("POST"))
.and(path("/api/codex/avatars/equip"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(200).set_body_json(snapshot_json("clippy")))
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_avatar_equip_request(CodexAvatarEquipParams {
avatar_id: "sunset".to_string(),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: CodexAvatarInventoryReadResponse = to_response(response)?;
assert_eq!(received, expected_snapshot("clippy"));
Ok(())
}
#[tokio::test]
async fn avatar_admin_award_forwards_request_and_returns_target_user_snapshot() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("POST"))
.and(path("/api/codex/avatars/admin/awards"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.and(body_json(json!({
"accountUserId": "target-user-456",
"awardId": "manual-grant-1",
"avatarId": "prism",
"sourceType": "manual-admin-grant",
"sourceRef": "support-ticket-1",
"awardedAt": 123,
"awardedBy": "admin-user",
"metadataJson": "{\"reason\":\"support\"}",
"sourceSummary": "Manual support grant"
})))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(snapshot_json_for_user("prism", "target-user-456")),
)
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_avatar_admin_award_request(CodexAvatarAdminAwardGrantParams {
account_user_id: "target-user-456".to_string(),
award_id: "manual-grant-1".to_string(),
avatar_id: "prism".to_string(),
source_type: "manual-admin-grant".to_string(),
source_ref: Some("support-ticket-1".to_string()),
awarded_at: Some(123),
awarded_by: Some("admin-user".to_string()),
metadata_json: Some("{\"reason\":\"support\"}".to_string()),
source_summary: Some("Manual support grant".to_string()),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: CodexAvatarInventoryReadResponse = to_response(response)?;
assert_eq!(
received,
expected_snapshot_for_user("prism", "target-user-456")
);
Ok(())
}
#[tokio::test]
async fn avatar_admin_proof_drop_forwards_request_and_returns_reveal_snapshot() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("POST"))
.and(path("/api/codex/avatars/admin/proof-drop"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.and(body_json(json!({
"accountUserId": "target-user-456",
"awardId": "proof-drop-1",
"sourceType": "proof-drop-box",
"sourceRef": "support-ticket-1",
"awardedAt": 123,
"sourceSummary": "Manual proof-drop box"
})))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(snapshot_json_with_reveal("prism", "target-user-456")),
)
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_avatar_admin_proof_drop_request(CodexAvatarAdminProofDropGrantParams {
account_user_id: "target-user-456".to_string(),
award_id: "proof-drop-1".to_string(),
source_type: "proof-drop-box".to_string(),
source_ref: Some("support-ticket-1".to_string()),
awarded_at: Some(123),
source_summary: Some("Manual proof-drop box".to_string()),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: CodexAvatarInventoryReadResponse = to_response(response)?;
assert_eq!(
received,
expected_snapshot_with_reveal("prism", "target-user-456")
);
Ok(())
}
#[tokio::test]
async fn avatar_admin_award_forwards_backend_permission_error() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("POST"))
.and(path("/api/codex/avatars/admin/awards"))
.respond_with(ResponseTemplate::new(403).set_body_json(json!({
"detail": "Not a Codex admin",
})))
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_avatar_admin_award_request(CodexAvatarAdminAwardGrantParams {
account_user_id: "target-user-456".to_string(),
award_id: "manual-grant-1".to_string(),
avatar_id: "prism".to_string(),
source_type: "manual-admin-grant".to_string(),
source_ref: None,
awarded_at: None,
awarded_by: None,
metadata_json: None,
source_summary: None,
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.id, RequestId::Integer(request_id));
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(error.error.message, "Not a Codex admin");
Ok(())
}
#[tokio::test]
async fn avatar_admin_capabilities_read_returns_backend_capability_flags() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let server = MockServer::start().await;
write_chatgpt_base_url(codex_home.path(), &server.uri())?;
Mock::given(method("GET"))
.and(path("/api/codex/avatars/admin/capabilities"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"canGrantAwards": true,
"canGrantProofDropBoxes": true,
})))
.mount(&server)
.await;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp.send_avatar_admin_capabilities_read_request().await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: CodexAvatarAdminCapabilitiesReadResponse = to_response(response)?;
assert_eq!(
received,
CodexAvatarAdminCapabilitiesReadResponse {
can_grant_awards: true,
can_grant_proof_drop_boxes: true,
}
);
Ok(())
}
fn write_chatgpt_base_url(codex_home: &Path, base_url: &str) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!("chatgpt_base_url = \"{base_url}\"\n"),
)
}
fn snapshot_json(equipped_avatar_id: &str) -> Value {
snapshot_json_for_user(equipped_avatar_id, "account-123")
}
fn snapshot_json_for_user(equipped_avatar_id: &str, account_user_id: &str) -> Value {
json!({
"accountUserId": account_user_id,
"avatarDefinitions": [
{
"avatarId": "clippy",
"displayName": "Clippy",
"description": "The default Codex paperclip avatar",
"rarity": "common",
"assetRef": "builtin:clippy",
"status": "active",
"sortOrder": 0,
"collectionName": "Desktop Set",
"collectionDescription": "Classic office companions.",
"lore": "Helpful and persistent.",
"accentClassName": "bg-slate-100 text-slate-950",
"silhouetteGlowClassName": "bg-slate-300/45",
"isProgressVisible": true,
},
{
"avatarId": "prism",
"displayName": "Prism",
"description": "A shiny earnable avatar",
"rarity": "rare",
"assetRef": "builtin:prism",
"status": "hidden",
"sortOrder": 10,
"collectionName": "Prism Set",
"collectionDescription": "Light-bent mascots.",
"lore": "Glows softly.",
"accentClassName": "bg-violet-100 text-violet-950",
"silhouetteGlowClassName": "bg-violet-300/45",
"isProgressVisible": true,
}
],
"ownedAvatars": [
{
"accountUserId": account_user_id,
"avatarId": "clippy",
"sourceSummary": "Default avatar",
},
{
"accountUserId": account_user_id,
"avatarId": "prism",
"sourceSummary": "Quest reward",
}
],
"equippedAvatarId": equipped_avatar_id,
"boxRules": box_rules_json(),
"pityState": pity_state_json(),
"pendingRevealAwards": [],
"updatedAt": 500,
})
}
fn snapshot_json_with_reveal(equipped_avatar_id: &str, account_user_id: &str) -> Value {
let mut snapshot = snapshot_json_for_user(equipped_avatar_id, account_user_id);
snapshot["pendingRevealAwards"] = json!([reveal_award_json()]);
snapshot
}
fn box_rules_json() -> Value {
json!({
"rulesetVersion": "signal-seasons-v1",
"oddsTableVersion": "signal-seasons-v1",
"rareOrBetterPityThreshold": 10,
"legendaryPityThreshold": 40,
"guaranteedNewThreshold": 6,
"odds": [
{
"bucketId": "common",
"label": "Common",
"probabilityPercent": 63,
},
{
"bucketId": "rare",
"label": "Rare",
"probabilityPercent": 26,
},
{
"bucketId": "legendary",
"label": "Legendary",
"probabilityPercent": 1,
},
{
"bucketId": "no_drop",
"label": "No drop",
"probabilityPercent": 10,
},
],
})
}
fn pity_state_json() -> Value {
json!({
"rollsSinceRareOrBetter": 1,
"rollsSinceLegendary": 8,
"nonNewOutcomeStreak": 0,
"guaranteedNewAvailable": true,
})
}
fn reveal_award_json() -> Value {
json!({
"awardId": "proof-drop-1",
"awardedAt": 123,
"sourceType": "proof-drop-box",
"sourceRef": "support-ticket-1",
"sourceSummary": "Manual proof-drop box",
"outcomeKind": "unlock",
"outcomeAvatarId": "prism",
"metadataJson": "{\"revealable\":true}",
"pityStateAfter": pity_state_json(),
})
}
fn expected_snapshot(equipped_avatar_id: &str) -> CodexAvatarInventoryReadResponse {
expected_snapshot_for_user(equipped_avatar_id, "account-123")
}
fn expected_snapshot_with_reveal(
equipped_avatar_id: &str,
account_user_id: &str,
) -> CodexAvatarInventoryReadResponse {
let mut snapshot = expected_snapshot_for_user(equipped_avatar_id, account_user_id);
snapshot.pending_reveal_awards = vec![expected_reveal_award()];
snapshot
}
fn expected_snapshot_for_user(
equipped_avatar_id: &str,
account_user_id: &str,
) -> CodexAvatarInventoryReadResponse {
CodexAvatarInventoryReadResponse {
account_user_id: account_user_id.to_string(),
avatar_definitions: vec![
CodexAvatarDefinition {
avatar_id: "clippy".to_string(),
display_name: "Clippy".to_string(),
description: "The default Codex paperclip avatar".to_string(),
rarity: CodexAvatarRarity::Common,
asset_ref: "builtin:clippy".to_string(),
status: CodexAvatarStatus::Active,
sort_order: 0,
collection_name: "Desktop Set".to_string(),
collection_description: "Classic office companions.".to_string(),
lore: "Helpful and persistent.".to_string(),
accent_class_name: "bg-slate-100 text-slate-950".to_string(),
silhouette_glow_class_name: "bg-slate-300/45".to_string(),
is_progress_visible: true,
},
CodexAvatarDefinition {
avatar_id: "prism".to_string(),
display_name: "Prism".to_string(),
description: "A shiny earnable avatar".to_string(),
rarity: CodexAvatarRarity::Rare,
asset_ref: "builtin:prism".to_string(),
status: CodexAvatarStatus::Hidden,
sort_order: 10,
collection_name: "Prism Set".to_string(),
collection_description: "Light-bent mascots.".to_string(),
lore: "Glows softly.".to_string(),
accent_class_name: "bg-violet-100 text-violet-950".to_string(),
silhouette_glow_class_name: "bg-violet-300/45".to_string(),
is_progress_visible: true,
},
],
owned_avatars: vec![
CodexAvatarOwnership {
account_user_id: account_user_id.to_string(),
avatar_id: "clippy".to_string(),
source_summary: Some("Default avatar".to_string()),
},
CodexAvatarOwnership {
account_user_id: account_user_id.to_string(),
avatar_id: "prism".to_string(),
source_summary: Some("Quest reward".to_string()),
},
],
equipped_avatar_id: equipped_avatar_id.to_string(),
box_rules: expected_box_rules(),
pity_state: expected_pity_state(),
pending_reveal_awards: Vec::new(),
updated_at: 500,
}
}
fn expected_box_rules() -> CodexAvatarBoxRules {
CodexAvatarBoxRules {
ruleset_version: "signal-seasons-v1".to_string(),
odds_table_version: "signal-seasons-v1".to_string(),
rare_or_better_pity_threshold: 10,
legendary_pity_threshold: 40,
guaranteed_new_threshold: 6,
odds: vec![
CodexAvatarBoxOddsBucket {
bucket_id: "common".to_string(),
label: "Common".to_string(),
probability_percent: 63,
},
CodexAvatarBoxOddsBucket {
bucket_id: "rare".to_string(),
label: "Rare".to_string(),
probability_percent: 26,
},
CodexAvatarBoxOddsBucket {
bucket_id: "legendary".to_string(),
label: "Legendary".to_string(),
probability_percent: 1,
},
CodexAvatarBoxOddsBucket {
bucket_id: "no_drop".to_string(),
label: "No drop".to_string(),
probability_percent: 10,
},
],
}
}
fn expected_pity_state() -> CodexAvatarPityState {
CodexAvatarPityState {
rolls_since_rare_or_better: 1,
rolls_since_legendary: 8,
non_new_outcome_streak: 0,
guaranteed_new_available: true,
}
}
fn expected_reveal_award() -> CodexAvatarRevealAward {
CodexAvatarRevealAward {
award_id: "proof-drop-1".to_string(),
awarded_at: 123,
source_type: "proof-drop-box".to_string(),
source_ref: Some("support-ticket-1".to_string()),
source_summary: Some("Manual proof-drop box".to_string()),
outcome_kind: "unlock".to_string(),
outcome_avatar_id: Some("prism".to_string()),
metadata_json: Some("{\"revealable\":true}".to_string()),
pity_state_after: expected_pity_state(),
}
}

View File

@@ -1,6 +1,7 @@
mod account;
mod analytics;
mod app_list;
mod avatar;
mod collaboration_mode_list;
#[cfg(unix)]
mod command_exec;

View File

@@ -1,8 +1,14 @@
use crate::types::CodeTaskDetailsResponse;
use crate::types::CodexAvatarAdminAwardGrantRequest;
use crate::types::CodexAvatarAdminCapabilitiesResponse;
use crate::types::CodexAvatarAdminProofDropGrantRequest;
use crate::types::CodexAvatarEquipRequest;
use crate::types::CodexAvatarInventoryResponse;
use crate::types::ConfigFileResponse;
use crate::types::PaginatedListTaskListItem;
use crate::types::RateLimitStatusPayload;
use crate::types::TurnAttemptsSiblingTurnsResponse;
use crate::types::WorkspaceOutOfCreditsNotificationResponse;
use anyhow::Result;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_core::auth::CodexAuth;
@@ -265,6 +271,97 @@ impl Client {
Ok(Self::rate_limit_snapshots_from_payload(payload))
}
pub async fn get_avatar_inventory(
&self,
) -> std::result::Result<CodexAvatarInventoryResponse, RequestError> {
let url = match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/avatars/inventory", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/avatars/inventory", self.base_url),
};
let req = self.http.get(&url).headers(self.headers());
let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?;
self.decode_json::<CodexAvatarInventoryResponse>(&url, &ct, &body)
.map_err(RequestError::from)
}
pub async fn equip_avatar(
&self,
avatar_id: String,
) -> std::result::Result<CodexAvatarInventoryResponse, RequestError> {
let url = match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/avatars/equip", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/avatars/equip", self.base_url),
};
let req = self
.http
.post(&url)
.headers(self.headers())
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
.json(&CodexAvatarEquipRequest { avatar_id });
let (body, ct) = self.exec_request_detailed(req, "POST", &url).await?;
self.decode_json::<CodexAvatarInventoryResponse>(&url, &ct, &body)
.map_err(RequestError::from)
}
pub async fn grant_admin_avatar_award(
&self,
request_body: CodexAvatarAdminAwardGrantRequest,
) -> std::result::Result<CodexAvatarInventoryResponse, RequestError> {
let url = match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/avatars/admin/awards", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/avatars/admin/awards", self.base_url),
};
let req = self
.http
.post(&url)
.headers(self.headers())
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
.json(&request_body);
let (body, ct) = self.exec_request_detailed(req, "POST", &url).await?;
self.decode_json::<CodexAvatarInventoryResponse>(&url, &ct, &body)
.map_err(RequestError::from)
}
pub async fn grant_admin_avatar_proof_drop(
&self,
request_body: CodexAvatarAdminProofDropGrantRequest,
) -> std::result::Result<CodexAvatarInventoryResponse, RequestError> {
let url = match self.path_style {
PathStyle::CodexApi => {
format!("{}/api/codex/avatars/admin/proof-drop", self.base_url)
}
PathStyle::ChatGptApi => {
format!("{}/wham/avatars/admin/proof-drop", self.base_url)
}
};
let req = self
.http
.post(&url)
.headers(self.headers())
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
.json(&request_body);
let (body, ct) = self.exec_request_detailed(req, "POST", &url).await?;
self.decode_json::<CodexAvatarInventoryResponse>(&url, &ct, &body)
.map_err(RequestError::from)
}
pub async fn get_avatar_admin_capabilities(
&self,
) -> std::result::Result<CodexAvatarAdminCapabilitiesResponse, RequestError> {
let url = match self.path_style {
PathStyle::CodexApi => {
format!("{}/api/codex/avatars/admin/capabilities", self.base_url)
}
PathStyle::ChatGptApi => {
format!("{}/wham/avatars/admin/capabilities", self.base_url)
}
};
let req = self.http.get(&url).headers(self.headers());
let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?;
self.decode_json::<CodexAvatarAdminCapabilitiesResponse>(&url, &ct, &body)
.map_err(RequestError::from)
}
pub async fn list_tasks(
&self,
limit: Option<i32>,
@@ -357,6 +454,16 @@ impl Client {
.map_err(RequestError::from)
}
pub async fn post_workspace_out_of_credits_notification(
&self,
) -> std::result::Result<WorkspaceOutOfCreditsNotificationResponse, RequestError> {
let url = self.workspace_out_of_credits_notification_url()?;
let req = self.http.post(&url).headers(self.headers());
let (body, ct) = self.exec_request_detailed(req, "POST", &url).await?;
self.decode_json::<WorkspaceOutOfCreditsNotificationResponse>(&url, &ct, &body)
.map_err(RequestError::from)
}
/// Create a new task (user turn) by POSTing to the appropriate backend path
/// based on `path_style`. Returns the created task id.
pub async fn create_task(&self, request_body: serde_json::Value) -> Result<String> {
@@ -498,6 +605,20 @@ impl Client {
let seconds_i64 = i64::from(seconds);
Some((seconds_i64 + 59) / 60)
}
fn workspace_out_of_credits_notification_url(
&self,
) -> std::result::Result<String, RequestError> {
match self.path_style {
PathStyle::ChatGptApi => Ok(format!(
"{}/credits/workspace_out_of_credits_notifications",
self.base_url
)),
PathStyle::CodexApi => Err(RequestError::Other(anyhow::anyhow!(
"workspace out-of-credits notifications are only supported for ChatGPT backend-api"
))),
}
}
}
#[cfg(test)]
@@ -649,4 +770,51 @@ mod tests {
.unwrap_or_else(|| snapshots[0].clone());
assert_eq!(preferred.limit_id.as_deref(), Some("codex"));
}
#[test]
fn workspace_notification_url_uses_chatgpt_credits_path() {
let client = Client::new("https://chatgpt.com").expect("client");
let url = client
.workspace_out_of_credits_notification_url()
.expect("chatgpt backend-api url");
assert_eq!(
url,
"https://chatgpt.com/backend-api/credits/workspace_out_of_credits_notifications"
);
}
#[test]
fn workspace_notification_url_rejects_codex_api_paths() {
let client = Client::new("https://api.openai.com").expect("client");
let err = client
.workspace_out_of_credits_notification_url()
.expect_err("codex api path should be rejected");
assert_eq!(
err.to_string(),
"workspace out-of-credits notifications are only supported for ChatGPT backend-api"
);
}
#[test]
fn workspace_notification_response_decodes() {
let client = Client::new("https://chatgpt.com").expect("client");
let response: WorkspaceOutOfCreditsNotificationResponse = client
.decode_json(
"https://chatgpt.com/backend-api/credits/workspace_out_of_credits_notifications",
"application/json",
r#"{"status":"cooldown_active"}"#,
)
.expect("response should decode");
assert_eq!(
response,
WorkspaceOutOfCreditsNotificationResponse {
status: crate::types::WorkspaceOutOfCreditsNotificationStatus::CooldownActive,
}
);
}
}

View File

@@ -5,7 +5,22 @@ pub use client::Client;
pub use client::RequestError;
pub use types::CodeTaskDetailsResponse;
pub use types::CodeTaskDetailsResponseExt;
pub use types::CodexAvatarAdminAwardGrantRequest;
pub use types::CodexAvatarAdminCapabilitiesResponse;
pub use types::CodexAvatarAdminProofDropGrantRequest;
pub use types::CodexAvatarBoxOddsBucket;
pub use types::CodexAvatarBoxRules;
pub use types::CodexAvatarDefinition;
pub use types::CodexAvatarEquipRequest;
pub use types::CodexAvatarInventoryResponse;
pub use types::CodexAvatarOwnership;
pub use types::CodexAvatarPityState;
pub use types::CodexAvatarRarity;
pub use types::CodexAvatarRevealAward;
pub use types::CodexAvatarStatus;
pub use types::ConfigFileResponse;
pub use types::PaginatedListTaskListItem;
pub use types::TaskListItem;
pub use types::TurnAttemptsSiblingTurnsResponse;
pub use types::WorkspaceOutOfCreditsNotificationResponse;
pub use types::WorkspaceOutOfCreditsNotificationStatus;

View File

@@ -9,10 +9,159 @@ pub use codex_backend_openapi_models::models::RateLimitWindowSnapshot;
pub use codex_backend_openapi_models::models::TaskListItem;
use serde::Deserialize;
use serde::Serialize;
use serde::de::Deserializer;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceOutOfCreditsNotificationStatus {
Sent,
CooldownActive,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct WorkspaceOutOfCreditsNotificationResponse {
pub status: WorkspaceOutOfCreditsNotificationStatus,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum CodexAvatarStatus {
Active,
Hidden,
Retired,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum CodexAvatarRarity {
Common,
Rare,
Epic,
Legendary,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarDefinition {
pub avatar_id: String,
pub display_name: String,
pub description: String,
pub rarity: CodexAvatarRarity,
pub asset_ref: String,
pub status: CodexAvatarStatus,
pub sort_order: i64,
pub collection_name: String,
pub collection_description: String,
pub lore: String,
pub accent_class_name: String,
pub silhouette_glow_class_name: String,
pub is_progress_visible: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarOwnership {
pub account_user_id: String,
pub avatar_id: String,
pub source_summary: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarEquipRequest {
pub avatar_id: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarAdminAwardGrantRequest {
pub account_user_id: String,
pub award_id: String,
pub avatar_id: String,
pub source_type: String,
pub source_ref: Option<String>,
pub awarded_at: Option<i64>,
pub awarded_by: Option<String>,
pub metadata_json: Option<String>,
pub source_summary: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarAdminProofDropGrantRequest {
pub account_user_id: String,
pub award_id: String,
pub source_type: String,
pub source_ref: Option<String>,
pub awarded_at: Option<i64>,
pub source_summary: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarBoxOddsBucket {
pub bucket_id: String,
pub label: String,
pub probability_percent: i64,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarPityState {
pub rolls_since_rare_or_better: i64,
pub rolls_since_legendary: i64,
pub non_new_outcome_streak: i64,
pub guaranteed_new_available: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarBoxRules {
pub ruleset_version: String,
pub odds_table_version: String,
pub rare_or_better_pity_threshold: i64,
pub legendary_pity_threshold: i64,
pub guaranteed_new_threshold: i64,
pub odds: Vec<CodexAvatarBoxOddsBucket>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarRevealAward {
pub award_id: String,
pub awarded_at: i64,
pub source_type: String,
pub source_ref: Option<String>,
pub source_summary: Option<String>,
pub outcome_kind: String,
pub outcome_avatar_id: Option<String>,
pub metadata_json: Option<String>,
pub pity_state_after: CodexAvatarPityState,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarInventoryResponse {
pub account_user_id: String,
pub avatar_definitions: Vec<CodexAvatarDefinition>,
pub owned_avatars: Vec<CodexAvatarOwnership>,
pub equipped_avatar_id: String,
pub box_rules: CodexAvatarBoxRules,
pub pity_state: CodexAvatarPityState,
pub pending_reveal_awards: Vec<CodexAvatarRevealAward>,
pub updated_at: i64,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodexAvatarAdminCapabilitiesResponse {
pub can_grant_awards: bool,
pub can_grant_proof_drop_boxes: bool,
}
/// 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.

View File

@@ -26,6 +26,7 @@ clap = { workspace = true, features = ["derive"] }
codex-arg0 = { workspace = true }
codex-app-server-client = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-backend-client = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-feedback = { workspace = true }

View File

@@ -9,6 +9,7 @@ mod event_processor;
mod event_processor_with_human_output;
pub mod event_processor_with_jsonl_output;
pub mod exec_events;
mod workspace_out_of_credits;
pub use cli::Cli;
pub use cli::Command;
@@ -730,6 +731,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
// Track whether a fatal error was reported by the server so we can
// exit with a non-zero status for automation-friendly signaling.
let mut error_seen = false;
let mut workspace_out_of_credits_candidate = false;
let mut interrupt_channel_open = true;
let primary_thread_id_for_requests = primary_thread_id.to_string();
loop {
@@ -768,6 +770,14 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
handle_server_request(&client, request, &mut error_seen).await;
}
InProcessServerEvent::ServerNotification(mut notification) => {
if workspace_out_of_credits::notification_is_usage_limit_failure(
&notification,
&primary_thread_id_for_requests,
&task_id,
) {
workspace_out_of_credits_candidate = true;
}
if let ServerNotification::Error(payload) = &notification {
if payload.thread_id == primary_thread_id_for_requests
&& payload.turn_id == task_id
@@ -824,6 +834,15 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
warn!("in-process app-server shutdown failed: {err}");
}
event_processor.print_final_output();
if let Err(err) = workspace_out_of_credits::maybe_prompt_for_workspace_out_of_credits(
&config,
workspace_out_of_credits_candidate && error_seen,
!json_mode,
)
.await
{
warn!("workspace out-of-credits prompt failed: {err}");
}
if error_seen {
std::process::exit(1);
}

View File

@@ -0,0 +1,376 @@
use codex_app_server_protocol::CodexErrorInfo;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::TurnError;
use codex_backend_client::Client as BackendClient;
use codex_backend_client::WorkspaceOutOfCreditsNotificationStatus;
use codex_core::AuthManager;
use codex_core::CodexAuth;
use codex_core::config::Config;
use codex_protocol::account::PlanType;
use codex_protocol::protocol::RateLimitSnapshot;
use std::io;
use std::io::IsTerminal;
const REQUEST_PROMPT: &str =
"Workspace out of credits. Request more from your workspace owner? [y/N]";
const REQUEST_SENT_MESSAGE: &str = "Request sent!";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PromptDecisionInput {
candidate_usage_limit_failure: bool,
human_output: bool,
stdin_is_tty: bool,
stderr_is_tty: bool,
auth_supports_request: bool,
}
pub(crate) fn notification_is_usage_limit_failure(
notification: &ServerNotification,
thread_id: &str,
turn_id: &str,
) -> bool {
match notification {
ServerNotification::Error(payload) => {
payload.thread_id == thread_id
&& payload.turn_id == turn_id
&& !payload.will_retry
&& turn_error_is_usage_limit(&payload.error)
}
ServerNotification::TurnCompleted(payload) => {
payload.thread_id == thread_id
&& payload.turn.id == turn_id
&& payload.turn.status == codex_app_server_protocol::TurnStatus::Failed
&& payload
.turn
.error
.as_ref()
.is_some_and(turn_error_is_usage_limit)
}
_ => false,
}
}
pub(crate) async fn maybe_prompt_for_workspace_out_of_credits(
config: &Config,
candidate_usage_limit_failure: bool,
human_output: bool,
) -> anyhow::Result<()> {
let stdin_is_tty = io::stdin().is_terminal();
let stderr_is_tty = io::stderr().is_terminal();
let initial_decision = PromptDecisionInput {
candidate_usage_limit_failure,
human_output,
stdin_is_tty,
stderr_is_tty,
auth_supports_request: false,
};
if !terminal_prompt_prerequisites_met(initial_decision) {
return Ok(());
}
let auth_manager = AuthManager::shared(
config.codex_home.clone(),
/*enable_codex_api_key_env*/ true,
config.cli_auth_credentials_store_mode,
);
let auth = auth_manager.auth().await;
let auth_supports_request = auth_supports_workspace_request(auth.as_ref());
let decision = PromptDecisionInput {
auth_supports_request,
..initial_decision
};
if !decision.auth_supports_request {
return Ok(());
}
let Some(auth) = auth else {
return Ok(());
};
let client = BackendClient::from_auth(config.chatgpt_base_url.clone(), &auth)?;
let rate_limits = client.get_rate_limits().await?;
if !should_offer_workspace_out_of_credits_prompt(decision, Some(&rate_limits)) {
return Ok(());
}
if !prompt_for_workspace_request()? {
return Ok(());
}
let response = client.post_workspace_out_of_credits_notification().await?;
match response.status {
WorkspaceOutOfCreditsNotificationStatus::Sent
| WorkspaceOutOfCreditsNotificationStatus::CooldownActive => {
eprintln!("{REQUEST_SENT_MESSAGE}");
}
}
Ok(())
}
fn turn_error_is_usage_limit(error: &TurnError) -> bool {
matches!(
error.codex_error_info,
Some(CodexErrorInfo::UsageLimitExceeded)
)
}
fn terminal_prompt_prerequisites_met(input: PromptDecisionInput) -> bool {
input.candidate_usage_limit_failure
&& input.human_output
&& input.stdin_is_tty
&& input.stderr_is_tty
}
fn auth_supports_workspace_request(auth: Option<&CodexAuth>) -> bool {
auth.is_some_and(|auth| auth.is_chatgpt_auth() && auth.get_account_id().is_some())
}
fn should_offer_workspace_out_of_credits_prompt(
input: PromptDecisionInput,
snapshot: Option<&RateLimitSnapshot>,
) -> bool {
terminal_prompt_prerequisites_met(input)
&& input.auth_supports_request
&& snapshot.is_some_and(rate_limits_confirm_workspace_out_of_credits)
}
fn rate_limits_confirm_workspace_out_of_credits(snapshot: &RateLimitSnapshot) -> bool {
if snapshot.plan_type != Some(PlanType::SelfServeBusinessUsageBased) {
return false;
}
let Some(credits) = snapshot.credits.as_ref() else {
return false;
};
if credits.unlimited {
return false;
}
credits
.balance
.as_deref()
.and_then(parse_credit_balance)
.is_some_and(|balance| balance <= 0.0)
}
fn parse_credit_balance(balance: &str) -> Option<f64> {
balance.trim().parse::<f64>().ok()
}
fn prompt_for_workspace_request() -> io::Result<bool> {
eprintln!("{REQUEST_PROMPT}");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(parse_affirmative_response(&input))
}
fn parse_affirmative_response(input: &str) -> bool {
let answer = input.trim();
answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes")
}
#[cfg(test)]
mod tests {
use super::*;
use codex_app_server_protocol::ErrorNotification;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnStatus;
use codex_protocol::protocol::CreditsSnapshot;
use pretty_assertions::assert_eq;
fn zero_credit_snapshot() -> RateLimitSnapshot {
RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: None,
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: false,
unlimited: false,
balance: Some("0".to_string()),
}),
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}
}
fn usage_limit_turn_error() -> TurnError {
TurnError {
message: "You've hit your usage limit.".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
additional_details: None,
}
}
#[test]
fn error_notification_marks_usage_limit_failure() {
let notification = ServerNotification::Error(ErrorNotification {
error: usage_limit_turn_error(),
will_retry: false,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
});
assert!(notification_is_usage_limit_failure(
&notification,
"thread-1",
"turn-1"
));
}
#[test]
fn unrelated_notification_does_not_mark_usage_limit_failure() {
let notification = ServerNotification::Error(ErrorNotification {
error: TurnError {
message: "network failed".to_string(),
codex_error_info: Some(CodexErrorInfo::ResponseStreamDisconnected {
http_status_code: Some(503),
}),
additional_details: None,
},
will_retry: false,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
});
assert!(!notification_is_usage_limit_failure(
&notification,
"thread-1",
"turn-1"
));
}
#[test]
fn failed_turn_marks_usage_limit_failure() {
let notification = ServerNotification::TurnCompleted(TurnCompletedNotification {
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
items: Vec::new(),
status: TurnStatus::Failed,
error: Some(usage_limit_turn_error()),
},
});
assert!(notification_is_usage_limit_failure(
&notification,
"thread-1",
"turn-1"
));
}
#[test]
fn prompt_decision_requires_ttys_and_human_output() {
let snapshot = zero_credit_snapshot();
let input = PromptDecisionInput {
candidate_usage_limit_failure: true,
human_output: false,
stdin_is_tty: true,
stderr_is_tty: true,
auth_supports_request: true,
};
assert!(!should_offer_workspace_out_of_credits_prompt(
input,
Some(&snapshot)
));
}
#[test]
fn prompt_decision_requires_supported_auth() {
let snapshot = zero_credit_snapshot();
let input = PromptDecisionInput {
candidate_usage_limit_failure: true,
human_output: true,
stdin_is_tty: true,
stderr_is_tty: true,
auth_supports_request: false,
};
assert!(!should_offer_workspace_out_of_credits_prompt(
input,
Some(&snapshot)
));
}
#[test]
fn workspace_out_of_credits_confirmation_requires_zero_balance() {
assert!(rate_limits_confirm_workspace_out_of_credits(
&zero_credit_snapshot()
));
}
#[test]
fn workspace_out_of_credits_confirmation_rejects_missing_balance() {
let mut snapshot = zero_credit_snapshot();
snapshot.credits.as_mut().expect("credits").balance = None;
assert!(!rate_limits_confirm_workspace_out_of_credits(&snapshot));
}
#[test]
fn workspace_out_of_credits_confirmation_rejects_positive_balance() {
let mut snapshot = zero_credit_snapshot();
snapshot.credits.as_mut().expect("credits").balance = Some("12.5".to_string());
assert!(!rate_limits_confirm_workspace_out_of_credits(&snapshot));
}
#[test]
fn workspace_out_of_credits_confirmation_rejects_wrong_plan() {
let mut snapshot = zero_credit_snapshot();
snapshot.plan_type = Some(PlanType::Team);
assert!(!rate_limits_confirm_workspace_out_of_credits(&snapshot));
}
#[test]
fn workspace_out_of_credits_confirmation_rejects_unlimited_credits() {
let mut snapshot = zero_credit_snapshot();
snapshot.credits = Some(CreditsSnapshot {
has_credits: true,
unlimited: true,
balance: Some("0".to_string()),
});
assert!(!rate_limits_confirm_workspace_out_of_credits(&snapshot));
}
#[test]
fn chatgpt_auth_with_account_id_is_supported() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
assert!(auth_supports_workspace_request(Some(&auth)));
}
#[test]
fn api_key_auth_is_not_supported() {
let auth = CodexAuth::from_api_key("test-key");
assert!(!auth_supports_workspace_request(Some(&auth)));
}
#[test]
fn affirmative_response_accepts_y_and_yes() {
assert!(parse_affirmative_response("y"));
assert!(parse_affirmative_response("YES"));
assert!(!parse_affirmative_response(""));
assert!(!parse_affirmative_response("n"));
}
#[test]
fn prompt_decision_accepts_usage_limit_zero_credit_case() {
let input = PromptDecisionInput {
candidate_usage_limit_failure: true,
human_output: true,
stdin_is_tty: true,
stderr_is_tty: true,
auth_supports_request: true,
};
assert_eq!(
should_offer_workspace_out_of_credits_prompt(input, Some(&zero_credit_snapshot())),
true
);
}
}