Merge pull request #4359 from catoncat/agent/fix-agent-identity-import-expiry

fix(openai): make Agent Identity usable end to end
This commit is contained in:
Wesley Liddick
2026-07-15 20:55:05 +08:00
committed by GitHub
12 changed files with 424 additions and 62 deletions
@@ -3117,6 +3117,7 @@
:show-session-token-option="false"
:show-access-token-option="false"
:show-codex-session-import-option="form.platform === 'openai'"
:show-agent-identity-option="form.platform === 'openai'"
:show-codex-pat-option="form.platform === 'openai'"
:show-sso-option="form.platform === 'grok'"
:show-manual-option="true"
@@ -5445,6 +5446,30 @@ const formatCodexImportMessages = (messages?: CodexSessionImportMessage[]) => {
.join('\n')
}
const isAgentIdentityImportContent = (content: string) => {
const isAgentIdentityValue = (value: unknown): boolean => {
if (Array.isArray(value)) return value.length > 0 && value.every(isAgentIdentityValue)
if (!value || typeof value !== 'object') return false
const record = value as Record<string, unknown>
const authMode = record.auth_mode ?? record.authMode
const agentIdentity = record.agent_identity ?? record.agentIdentity
return (typeof authMode === 'string' && authMode.toLowerCase() === 'agentidentity')
|| (!!agentIdentity && typeof agentIdentity === 'object')
}
try {
return isAgentIdentityValue(JSON.parse(content))
} catch {
const lines = content.split('\n').map((line) => line.trim()).filter(Boolean)
if (lines.length === 0) return false
try {
return lines.every((line) => isAgentIdentityValue(JSON.parse(line)))
} catch {
return false
}
}
}
const handleOpenAIImportCodexSession = async (content: string) => {
const oauthClient = openaiOAuth
const trimmed = content.trim()
@@ -5452,6 +5477,10 @@ const handleOpenAIImportCodexSession = async (content: string) => {
oauthClient.error.value = t('admin.accounts.oauth.openai.codexSessionEmpty')
return
}
if (oauthFlowRef.value?.inputMethod === 'agent_identity' && !isAgentIdentityImportContent(trimmed)) {
oauthClient.error.value = t('admin.accounts.oauth.openai.agentIdentityInvalid')
return
}
const credentialExtras = buildOpenAICodexImportCredentialExtras()
if (credentialExtras === null) {
@@ -103,6 +103,17 @@
t('admin.accounts.oauth.openai.codexSessionAuth')
}}</span>
</label>
<label v-if="showAgentIdentityOption" class="flex cursor-pointer items-center gap-2">
<input
v-model="inputMethod"
type="radio"
value="agent_identity"
class="text-blue-600 focus:ring-blue-500"
/>
<span class="text-sm text-blue-900 dark:text-blue-200">{{
t('admin.accounts.oauth.openai.agentIdentityAuth')
}}</span>
</label>
<label v-if="showCodexPatOption" class="flex cursor-pointer items-center gap-2">
<input
v-model="inputMethod"
@@ -277,12 +288,12 @@
</div>
<!-- Codex auth.json / session credential batch import -->
<div v-if="inputMethod === 'codex_session'" class="space-y-4">
<div v-if="inputMethod === 'codex_session' || inputMethod === 'agent_identity'" class="space-y-4">
<div
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t('admin.accounts.oauth.openai.codexSessionDesc') }}
{{ t(isAgentIdentityInput ? 'admin.accounts.oauth.openai.agentIdentityDesc' : 'admin.accounts.oauth.openai.codexSessionDesc') }}
</p>
<div class="mb-4">
@@ -290,7 +301,7 @@
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<Icon name="key" size="sm" class="text-blue-500" />
{{ t('admin.accounts.oauth.openai.codexSessionInputLabel') }}
{{ t(isAgentIdentityInput ? 'admin.accounts.oauth.openai.agentIdentityInputLabel' : 'admin.accounts.oauth.openai.codexSessionInputLabel') }}
<span
v-if="parsedCodexSessionCount > 1"
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
@@ -302,11 +313,11 @@
v-model="codexSessionInput"
rows="8"
class="input w-full resize-y font-mono text-sm"
:placeholder="t('admin.accounts.oauth.openai.codexSessionPlaceholder')"
:placeholder="t(isAgentIdentityInput ? 'admin.accounts.oauth.openai.agentIdentityPlaceholder' : 'admin.accounts.oauth.openai.codexSessionPlaceholder')"
spellcheck="false"
></textarea>
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
{{ t('admin.accounts.oauth.openai.codexSessionHint') }}
{{ t(isAgentIdentityInput ? 'admin.accounts.oauth.openai.agentIdentityHint' : 'admin.accounts.oauth.openai.codexSessionHint') }}
</p>
</div>
@@ -822,6 +833,7 @@ interface Props {
showSessionTokenOption?: boolean
showAccessTokenOption?: boolean
showCodexSessionImportOption?: boolean
showAgentIdentityOption?: boolean
showCodexPatOption?: boolean
showSsoOption?: boolean
showManualOption?: boolean
@@ -845,6 +857,7 @@ const props = withDefaults(defineProps<Props>(), {
showSessionTokenOption: false,
showAccessTokenOption: false,
showCodexSessionImportOption: false,
showAgentIdentityOption: false,
showCodexPatOption: false,
showSsoOption: false,
showManualOption: true,
@@ -901,6 +914,7 @@ const oauthImportantNotice = computed(() => {
// Local state
const inputMethod = ref<AuthInputMethod>(props.initialInputMethod)
const isAgentIdentityInput = computed(() => inputMethod.value === 'agent_identity')
const authCodeInput = ref('')
const sessionKeyInput = ref('')
const refreshTokenInput = ref('')
@@ -921,6 +935,7 @@ const methodOptionCount = computed(() => [
props.showSessionTokenOption,
props.showAccessTokenOption,
props.showCodexSessionImportOption,
props.showAgentIdentityOption,
props.showCodexPatOption,
props.showSsoOption
].filter(Boolean).length)
@@ -64,6 +64,14 @@ const BaseDialogStub = defineComponent({
const OAuthAuthorizationFlowStub = defineComponent({
name: 'OAuthAuthorizationFlow',
props: {
showManualOption: Boolean,
showCodexSessionImportOption: Boolean,
showAgentIdentityOption: Boolean,
showCodexPatOption: Boolean,
initialInputMethod: String,
},
data: () => ({ inputMethod: 'manual' }),
emits: ['import-codex-session', 'import-codex-pat'],
template: `
<div>
@@ -147,6 +155,34 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('exposes Agent Identity in the OpenAI authorization methods', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
await wrapper.get('form#create-account-form input[type="text"]').setValue('OpenAI account')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
const flow = wrapper.getComponent(OAuthAuthorizationFlowStub)
expect(flow.props('showManualOption')).toBe(true)
expect(flow.props('showCodexSessionImportOption')).toBe(true)
expect(flow.props('showAgentIdentityOption')).toBe(true)
expect(flow.props('showCodexPatOption')).toBe(true)
expect(flow.props('initialInputMethod')).toBe('manual')
})
it.each([
['camelCase', { authMode: 'agentIdentity', agentIdentity: { agentRuntimeId: 'runtime' } }],
['nested identity without auth_mode', { agent_identity: { agent_runtime_id: 'runtime' } }],
])('accepts backend-compatible %s Agent Identity imports', async (_name, content) => {
const wrapper = await openCodexImportStep()
const flow = wrapper.getComponent(OAuthAuthorizationFlowStub)
flow.vm.inputMethod = 'agent_identity'
flow.vm.$emit('import-codex-session', JSON.stringify(content))
await flushPromises()
expect(importCodexSessionMock).toHaveBeenCalledTimes(1)
})
it('sends true explicitly when OpenAI long-context billing is enabled', async () => {
await submitApiKeyAccount('openai', true)