@tokensrc/codex 1.13.4 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +79 -3
  2. package/dist/client.d.ts +29 -1
  3. package/dist/client.js +221 -2
  4. package/dist/client.js.map +1 -1
  5. package/dist/command-directory.d.ts +1 -0
  6. package/dist/command-directory.js +3 -2
  7. package/dist/command-directory.js.map +1 -1
  8. package/dist/commands.d.ts +8 -2
  9. package/dist/commands.js +567 -40
  10. package/dist/commands.js.map +1 -1
  11. package/dist/console-status.js +1 -1
  12. package/dist/console-status.js.map +1 -1
  13. package/dist/credential-supervisor.d.ts +17 -0
  14. package/dist/credential-supervisor.js +84 -0
  15. package/dist/credential-supervisor.js.map +1 -0
  16. package/dist/credentials.d.ts +75 -3
  17. package/dist/credentials.js +357 -12
  18. package/dist/credentials.js.map +1 -1
  19. package/dist/enrollment-envelope.d.ts +18 -0
  20. package/dist/enrollment-envelope.js +112 -0
  21. package/dist/enrollment-envelope.js.map +1 -0
  22. package/dist/enrollment-flow.d.ts +27 -0
  23. package/dist/enrollment-flow.js +224 -0
  24. package/dist/enrollment-flow.js.map +1 -0
  25. package/dist/enrollment-login.d.ts +26 -0
  26. package/dist/enrollment-login.js +104 -0
  27. package/dist/enrollment-login.js.map +1 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +17 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/managed-credential-v3-envelope.d.ts +60 -0
  32. package/dist/managed-credential-v3-envelope.js +234 -0
  33. package/dist/managed-credential-v3-envelope.js.map +1 -0
  34. package/dist/managed-credential-v3-flow.d.ts +45 -0
  35. package/dist/managed-credential-v3-flow.js +499 -0
  36. package/dist/managed-credential-v3-flow.js.map +1 -0
  37. package/dist/managed-credential-v3-journal.d.ts +192 -0
  38. package/dist/managed-credential-v3-journal.js +153 -0
  39. package/dist/managed-credential-v3-journal.js.map +1 -0
  40. package/dist/managed-credential-v3.d.ts +117 -0
  41. package/dist/managed-credential-v3.js +423 -0
  42. package/dist/managed-credential-v3.js.map +1 -0
  43. package/dist/migration-login-batch.d.ts +66 -0
  44. package/dist/migration-login-batch.js +202 -0
  45. package/dist/migration-login-batch.js.map +1 -0
  46. package/dist/private-file.d.ts +10 -0
  47. package/dist/private-file.js +77 -2
  48. package/dist/private-file.js.map +1 -1
  49. package/dist/profile.d.ts +10 -0
  50. package/dist/profile.js +55 -2
  51. package/dist/profile.js.map +1 -1
  52. package/dist/protocol.d.ts +1070 -0
  53. package/dist/protocol.js +560 -0
  54. package/dist/protocol.js.map +1 -1
  55. package/dist/retirement-flow.d.ts +38 -0
  56. package/dist/retirement-flow.js +78 -0
  57. package/dist/retirement-flow.js.map +1 -0
  58. package/dist/runtime.d.ts +4 -1
  59. package/dist/runtime.js +29 -4
  60. package/dist/runtime.js.map +1 -1
  61. package/dist/selection.d.ts +10 -1
  62. package/dist/selection.js +38 -2
  63. package/dist/selection.js.map +1 -1
  64. package/dist/short-lived-delivery-flow.d.ts +41 -0
  65. package/dist/short-lived-delivery-flow.js +141 -0
  66. package/dist/short-lived-delivery-flow.js.map +1 -0
  67. package/dist/short-lived-delivery-journal.d.ts +34 -0
  68. package/dist/short-lived-delivery-journal.js +137 -0
  69. package/dist/short-lived-delivery-journal.js.map +1 -0
  70. package/dist/short-lived-delivery.d.ts +26 -0
  71. package/dist/short-lived-delivery.js +139 -0
  72. package/dist/short-lived-delivery.js.map +1 -0
  73. package/dist/tui.js +23 -0
  74. package/dist/tui.js.map +1 -1
  75. package/dist/types.d.ts +6 -2
  76. package/package.json +2 -2
package/dist/commands.js CHANGED
@@ -1,17 +1,37 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { hostname } from "node:os";
3
+ import { join } from "node:path";
3
4
  import { getStoragePath, loadAccounts, setStoragePath, setStoragePathDirect } from "codex-multi-auth/storage";
4
5
  import { normalizeCodexServiceUrl } from "./client.js";
5
6
  import { CodexServiceError } from "./errors.js";
6
- import { backupCodexFileCredentials, codexFileCredentialsMatchAccount, inspectLocalCodexCredentialStatus, resetManagedCodexFileCredentials, restoreCodexFileCredentials, writeCodexChatgptFileCredentials } from "./credentials.js";
7
+ import { backupCodexFileCredentials, codexFileCredentialsMatchAccount, discardCodexFileCredentialBackup, inspectLocalCodexCredentialStatus, readManagedCodexLineageState, resetManagedCodexFileCredentials, restoreCodexFileCredentials, writeCodexChatgptFileCredentials } from "./credentials.js";
7
8
  import { CODEX_DISTRIBUTION_SCHEMA_VERSION } from "./protocol.js";
8
9
  import { formatCodexAccountDisplayIdentity, resolveCodexAdminAccount, resolveCodexAccountDisplayName } from "./account-display.js";
9
10
  import { loadCodexQuotaUploads } from "./quota-cache.js";
10
11
  import { cleanupLegacyCodexProvider } from "./legacy-cleanup.js";
11
- import { clearCodexSelection, loadCodexSelection, saveCodexSelection } from "./selection.js";
12
+ import { executeCodexEnrollmentLogin } from "./enrollment-flow.js";
13
+ import { executeCodexMigrationLoginBatch } from "./migration-login-batch.js";
14
+ import { executeCodexShortLivedCredentialDelivery } from "./short-lived-delivery-flow.js";
15
+ import { executeManagedCodexV3Login, executeManagedCodexV3Upload, reportManagedCodexV3Credentials } from "./managed-credential-v3-flow.js";
16
+ import { readManagedCodexV3State } from "./managed-credential-v3.js";
17
+ import { profileManagedCredentialsV3Enabled } from "./profile.js";
18
+ import { executeCodexAssignmentRetirementLocalDelete } from "./retirement-flow.js";
19
+ import { clearCodexSelection, loadCodexSelection, rememberCodexSelectionProviderAccount, saveCodexSelection } from "./selection.js";
12
20
  import { installCodexShortcuts, removeCodexShortcuts } from "./shortcuts.js";
13
21
  import { isCodexAccountAvailable, renderCodexAccountDashboard, selectCodexAccount } from "./tui.js";
22
+ import { readCodexShortLivedDeliveryJournal } from "./short-lived-delivery-journal.js";
23
+ import { superviseCodexShortLivedCredentials } from "./credential-supervisor.js";
14
24
  export const NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE = "当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。";
25
+ // The Server refreshes a user-bound lineage only inside its final 60-second
26
+ // credential safety window. Asking for a new delivery earlier would merely
27
+ // re-encrypt the same access/ID tokens and must not be treated as a renewal.
28
+ const SERVER_MANAGED_SHORT_LIVED_REFRESH_WINDOW_MS = 60_000;
29
+ const SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED = "schema_v2_legacy_management_disabled";
30
+ export function effectiveCodexCredentialRefreshBefore(requestedMs, serverManagedShortLived) {
31
+ return serverManagedShortLived
32
+ ? Math.min(requestedMs, SERVER_MANAGED_SHORT_LIVED_REFRESH_WINDOW_MS)
33
+ : requestedMs;
34
+ }
15
35
  async function json(command, deps) {
16
36
  if (command.optsWithGlobals().json)
17
37
  return true;
@@ -30,6 +50,27 @@ function success(value) {
30
50
  function importFailures(credentials) {
31
51
  return credentials.filter(({ status }) => status === "REJECTED" || status === "UNKNOWN");
32
52
  }
53
+ function schemaV2LifecycleEnabled(config) {
54
+ const capabilities = config.profile?.userBoundRefreshTokens;
55
+ return capabilities?.delivery === true || capabilities?.migrationPlans === true;
56
+ }
57
+ function managedCredentialsV3Enabled(config) {
58
+ return profileManagedCredentialsV3Enabled(config.profile);
59
+ }
60
+ function codexConfigPathForEnv(env) {
61
+ const codexHome = env.CODEX_HOME?.trim();
62
+ return codexHome ? join(codexHome, "config.toml") : undefined;
63
+ }
64
+ function assertLegacyManagementEnabled(config) {
65
+ if (!schemaV2LifecycleEnabled(config))
66
+ return;
67
+ throw new CodexServiceError("Schema-v2 user-bound credentials are enabled; legacy refresh-token pool management is disabled.", SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED, 409);
68
+ }
69
+ function assignmentCredentialStatus(assignment) {
70
+ if (assignment.deliveryEligible)
71
+ return "AVAILABLE";
72
+ return ["NEEDS_REAUTH", "LOST", "DIVERGED", "COMPROMISED"].includes(assignment.lineageStatus ?? "") ? "NEEDS_REAUTH" : "TEMPORARILY_UNAVAILABLE";
73
+ }
33
74
  function serviceConnection(options = {}) {
34
75
  return {
35
76
  ...(options.server ? { serverUrl: options.server } : {}),
@@ -64,15 +105,62 @@ function formatAdminAccount(account) {
64
105
  })} (${account.id})`;
65
106
  }
66
107
  async function loadAccountData(deps, options = {}) {
67
- const response = await deps.clientFor(serviceConnection(options)).listAccounts();
68
108
  const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
109
+ if (managedCredentialsV3Enabled(deps.config)) {
110
+ const v3Assignments = await deps.clientFor(serviceConnection(options)).listV3CredentialAssignments();
111
+ const accounts = v3Assignments.items.map((assignment) => ({
112
+ id: assignment.managedAccountId,
113
+ accountId: assignment.assignmentId,
114
+ label: assignment.label,
115
+ isDefault: true,
116
+ accessExpiresAt: Date.parse(assignment.credentialExpiresAt),
117
+ credentialStatus: assignment.lineageStatus === "NEEDS_REAUTH"
118
+ ? "NEEDS_REAUTH"
119
+ : assignment.authorityMode === "CLIENT_MANAGED" ? "AVAILABLE" : "TEMPORARILY_UNAVAILABLE",
120
+ lineageStatus: assignment.lineageStatus === "STALE" ? "SYNC_LAGGING" : assignment.lineageStatus,
121
+ quota: null
122
+ }));
123
+ return {
124
+ response: { schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION, accounts,
125
+ defaultAccountId: accounts[0]?.id ?? null },
126
+ selectedAccountId: previous?.account.managedAccountId,
127
+ v3Assignments
128
+ };
129
+ }
130
+ if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
131
+ const assignments = await deps.clientFor(serviceConnection(options)).listCredentialAssignments();
132
+ const accounts = assignments.items.map((assignment) => ({
133
+ id: assignment.managedAccountId,
134
+ accountId: assignment.assignmentId,
135
+ label: assignment.label,
136
+ isDefault: previous?.account.assignmentId === assignment.assignmentId,
137
+ ...(assignment.credentialExpiresAt
138
+ ? { accessExpiresAt: Date.parse(assignment.credentialExpiresAt) }
139
+ : {}),
140
+ credentialStatus: assignmentCredentialStatus(assignment),
141
+ lineageStatus: assignment.lineageStatus ?? undefined,
142
+ quota: null
143
+ }));
144
+ return {
145
+ response: {
146
+ schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION,
147
+ accounts,
148
+ defaultAccountId: previous?.account.managedAccountId ?? null
149
+ },
150
+ selectedAccountId: previous?.account.managedAccountId,
151
+ assignments
152
+ };
153
+ }
154
+ const response = await deps.clientFor(serviceConnection(options)).listAccounts();
69
155
  return {
70
156
  response,
71
157
  selectedAccountId: previous?.account.id ?? response.defaultAccountId ?? undefined
72
158
  };
73
159
  }
74
160
  async function chooseAccount(deps, requested, options = {}) {
75
- const { response, selectedAccountId } = await loadAccountData(deps, options);
161
+ const data = await loadAccountData(deps, options);
162
+ const { response, selectedAccountId } = data;
163
+ const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
76
164
  const cli = deps.config.branding.cliName === "numa"
77
165
  ? "numa codex use"
78
166
  : `${deps.config.branding.cliName} use`;
@@ -88,39 +176,240 @@ async function chooseAccount(deps, requested, options = {}) {
88
176
  label: account.label,
89
177
  providerAccountId: account.accountId
90
178
  });
179
+ const assignment = data.assignments?.items.find((item) => item.managedAccountId === account.id && item.assignmentId === account.accountId);
180
+ const retainedProviderAccountId = assignment
181
+ && previous?.account.assignmentId === assignment.assignmentId
182
+ ? previous.account.providerAccountId
183
+ : undefined;
184
+ const v3Assignment = data.v3Assignments?.items.find((item) => item.managedAccountId === account.id && item.assignmentId === account.accountId);
91
185
  const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
92
186
  serviceUrl: options.server ?? deps.config.connection.serverUrl,
93
187
  allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
94
- account: { id: account.id, accountId: account.accountId, label: accountLabel }
188
+ account: v3Assignment ? {
189
+ id: v3Assignment.managedAccountId,
190
+ accountId: v3Assignment.providerAccountId,
191
+ label: accountLabel,
192
+ distributionMode: "CLIENT_MANAGED_V3",
193
+ assignmentId: v3Assignment.assignmentId,
194
+ managedAccountId: v3Assignment.managedAccountId,
195
+ providerAccountId: v3Assignment.providerAccountId
196
+ } : assignment ? {
197
+ id: assignment.managedAccountId,
198
+ label: accountLabel,
199
+ distributionMode: "SERVER_MANAGED_SHORT_LIVED",
200
+ assignmentId: assignment.assignmentId,
201
+ managedAccountId: assignment.managedAccountId,
202
+ ...(retainedProviderAccountId ? {
203
+ accountId: retainedProviderAccountId,
204
+ providerAccountId: retainedProviderAccountId
205
+ } : {})
206
+ } : {
207
+ id: account.id,
208
+ accountId: account.accountId,
209
+ label: accountLabel,
210
+ distributionMode: "SCHEMA_V1"
211
+ }
95
212
  });
96
213
  return { account, selection };
97
214
  }
98
- async function installCredentials(deps, accountId, service) {
99
- const response = await deps.clientFor(service).getLoginCredentials(accountId);
100
- await cleanupLegacyCodexProvider();
101
- await backupCodexFileCredentials();
215
+ async function installCredentials(deps, selection, service) {
216
+ const client = deps.clientFor(service);
217
+ await cleanupLegacyCodexProvider(codexConfigPathForEnv(deps.env));
218
+ if (managedCredentialsV3Enabled(deps.config)) {
219
+ const state = await readManagedCodexV3State(deps.env);
220
+ if (!state || state.phase !== "INSTALLED" || state.authorityMode !== "CLIENT_MANAGED"
221
+ || selection.account.distributionMode !== "CLIENT_MANAGED_V3"
222
+ || selection.account.assignmentId !== state.assignmentId
223
+ || selection.account.managedAccountId !== state.managedAccountId
224
+ || selection.account.providerAccountId !== state.providerAccountId) {
225
+ throw new CodexServiceError("The selected v3 assignment does not match acknowledged local credentials; run `numa codex login` or `numa codex upload`.", "lineage_version_conflict", 409);
226
+ }
227
+ const assignments = await client.listV3CredentialAssignments();
228
+ const current = assignments.items[0];
229
+ if (!current || current.assignmentId !== state.assignmentId || current.lineageId !== state.lineageId
230
+ || current.authorityMode !== "CLIENT_MANAGED") {
231
+ throw new CodexServiceError("The v3 credential assignment is no longer active.", "codex_assignment_not_found", 404);
232
+ }
233
+ return { expiresAt: Date.parse(state.credentialExpiresAt), selection };
234
+ }
235
+ if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
236
+ await discardCodexFileCredentialBackup(deps.env);
237
+ if (selection.account.distributionMode !== "SERVER_MANAGED_SHORT_LIVED"
238
+ || !selection.account.assignmentId || !selection.account.managedAccountId) {
239
+ throw new CodexServiceError("The saved Codex selection is not a schema-v2 credential assignment.", "codex_assignment_not_found", 404);
240
+ }
241
+ const assignments = await client.listCredentialAssignments();
242
+ const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
243
+ && item.managedAccountId === selection.account.managedAccountId);
244
+ if (!assignment) {
245
+ throw new CodexServiceError("The selected schema-v2 credential assignment is no longer active.", "codex_assignment_not_found", 404);
246
+ }
247
+ const state = await readManagedCodexLineageState(deps.env);
248
+ const journal = await readCodexShortLivedDeliveryJournal(deps.env);
249
+ const recoverable = journal?.tenantId === deps.config.connection.tenantId
250
+ && journal.assignmentId === assignment.assignmentId
251
+ || state?.assignmentId === assignment.assignmentId
252
+ && (state.tenantId === deps.config.connection.tenantId || state.tenantId === null)
253
+ && (state.phase === "INSTALLING"
254
+ || state.phase === "INSTALLED_PENDING_ACK" && Date.parse(state.ackExpiresAt) > Date.now());
255
+ if (!assignment.deliveryEligible && !recoverable) {
256
+ throw new CodexServiceError("The selected schema-v2 credential assignment is not ready for delivery.", "short_lived_credential_lineage_not_ready", 409);
257
+ }
258
+ const delivered = await executeCodexShortLivedCredentialDelivery({
259
+ client,
260
+ tenantId: deps.config.connection.tenantId,
261
+ assignmentId: assignment.assignmentId,
262
+ allowNewDelivery: assignment.deliveryEligible,
263
+ env: deps.env
264
+ });
265
+ const updated = await rememberCodexSelectionProviderAccount(deps.config.persistence.configDirectory, assignment.assignmentId, delivered.providerAccountId);
266
+ return { expiresAt: Date.parse(delivered.credentialExpiresAt), selection: updated };
267
+ }
268
+ await backupCodexFileCredentials(deps.env);
269
+ const response = await client.getLoginCredentials(selection.account.id);
102
270
  await writeCodexChatgptFileCredentials({
103
271
  accessToken: response.accessToken,
104
272
  idToken: response.idToken,
105
273
  accountId: response.account.accountId
106
- });
107
- return { expiresAt: response.expiresAt };
274
+ }, Date.now(), deps.env);
275
+ return { expiresAt: response.expiresAt, selection };
108
276
  }
109
277
  async function selectedCredentials(deps, requested, options = {}) {
278
+ if (managedCredentialsV3Enabled(deps.config)) {
279
+ const state = await readManagedCodexV3State(deps.env);
280
+ if (!state || state.phase !== "INSTALLED" || state.authorityMode !== "CLIENT_MANAGED") {
281
+ throw new CodexServiceError("No acknowledged v3 Codex credential is installed; run `numa codex login` or `numa codex upload`.", "managed_codex_v3_login_required", 409);
282
+ }
283
+ if (requested && ![
284
+ state.assignmentId,
285
+ state.managedAccountId,
286
+ state.providerAccountId,
287
+ state.lineageId
288
+ ].includes(requested)) {
289
+ throw new CodexServiceError("The requested account is not the active v3 lineage.", "codex_account_not_found", 404);
290
+ }
291
+ const assignments = await deps.clientFor(serviceConnection(options)).listV3CredentialAssignments();
292
+ const assignment = assignments.items[0];
293
+ if (!assignment || assignment.assignmentId !== state.assignmentId
294
+ || assignment.managedAccountId !== state.managedAccountId
295
+ || assignment.providerAccountId !== state.providerAccountId
296
+ || assignment.lineageId !== state.lineageId
297
+ || assignment.authorityMode !== "CLIENT_MANAGED") {
298
+ throw new CodexServiceError("The local v3 lineage no longer matches the server assignment.", "lineage_version_conflict", 409);
299
+ }
300
+ const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
301
+ serviceUrl: options.server ?? deps.config.connection.serverUrl,
302
+ allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
303
+ account: {
304
+ id: state.managedAccountId,
305
+ accountId: state.providerAccountId,
306
+ label: assignment.label,
307
+ distributionMode: "CLIENT_MANAGED_V3",
308
+ assignmentId: state.assignmentId,
309
+ managedAccountId: state.managedAccountId,
310
+ providerAccountId: state.providerAccountId
311
+ }
312
+ });
313
+ return { selection, expiresAt: Date.parse(state.credentialExpiresAt) };
314
+ }
110
315
  let selection = await loadCodexSelection(deps.config.persistence.configDirectory);
111
316
  const currentServiceUrl = options.server ?? deps.config.connection.serverUrl;
112
317
  const currentAllowsInsecureHttp = Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp);
113
318
  const serviceChanged = selection !== undefined && !sameServiceUrl(selection.serviceUrl, selection.allowInsecurePoc, currentServiceUrl, currentAllowsInsecureHttp);
114
- if (!selection || serviceChanged || options.server || requested && selection.account.id !== requested) {
319
+ const requestedMatchesSelection = selection !== undefined && requested !== undefined
320
+ && [
321
+ selection.account.id,
322
+ selection.account.accountId,
323
+ selection.account.assignmentId,
324
+ selection.account.managedAccountId,
325
+ selection.account.providerAccountId,
326
+ selection.account.label
327
+ ].includes(requested);
328
+ if (!selection || serviceChanged || requested && !requestedMatchesSelection) {
115
329
  selection = (await chooseAccount(deps, requested, options)).selection;
116
330
  }
117
- if (!(await codexFileCredentialsMatchAccount(selection.account.accountId))) {
118
- await installCredentials(deps, selection.account.id, {
331
+ if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
332
+ if (selection.account.distributionMode !== "SERVER_MANAGED_SHORT_LIVED") {
333
+ selection = (await chooseAccount(deps, requested, options)).selection;
334
+ }
335
+ const assignments = await deps.clientFor({
336
+ serverUrl: selection.serviceUrl,
337
+ allowInsecureHttp: selection.allowInsecurePoc
338
+ }).listCredentialAssignments();
339
+ const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
340
+ && item.managedAccountId === selection.account.managedAccountId);
341
+ if (!assignment) {
342
+ throw new CodexServiceError("The selected schema-v2 credential assignment is no longer active.", "codex_assignment_not_found", 404);
343
+ }
344
+ const state = await readManagedCodexLineageState(deps.env);
345
+ const journal = await readCodexShortLivedDeliveryJournal(deps.env);
346
+ const stateMatches = state !== undefined
347
+ && (state.tenantId === deps.config.connection.tenantId || state.tenantId === null)
348
+ && state.assignmentId === assignment.assignmentId
349
+ && state.managedAccountId === assignment.managedAccountId;
350
+ const local = await inspectLocalCodexCredentialStatus(stateMatches ? state.providerAccountId : selection.account.providerAccountId, deps.env);
351
+ const pending = journal?.tenantId === deps.config.connection.tenantId
352
+ && journal.assignmentId === assignment.assignmentId
353
+ || stateMatches && state.phase !== "INSTALLED";
354
+ if (!assignment.deliveryEligible || !stateMatches || !local.usable
355
+ || local.matchesExpectedAccount !== true || pending) {
356
+ return installCredentials(deps, selection, {
357
+ serverUrl: selection.serviceUrl,
358
+ allowInsecureHttp: selection.allowInsecurePoc
359
+ });
360
+ }
361
+ const matchingState = state;
362
+ if (selection.account.providerAccountId !== matchingState.providerAccountId) {
363
+ selection = await rememberCodexSelectionProviderAccount(deps.config.persistence.configDirectory, assignment.assignmentId, matchingState.providerAccountId);
364
+ }
365
+ return { selection, expiresAt: local.expiresAt ?? Date.parse(matchingState.credentialExpiresAt) };
366
+ }
367
+ if (!selection.account.accountId) {
368
+ throw new CodexServiceError("The saved Codex selection has no provider account.", "invalid_configuration");
369
+ }
370
+ if (!(await codexFileCredentialsMatchAccount(selection.account.accountId, deps.env))) {
371
+ return installCredentials(deps, selection, {
119
372
  serverUrl: selection.serviceUrl,
120
373
  allowInsecureHttp: selection.allowInsecurePoc
121
374
  });
122
375
  }
123
- return selection;
376
+ const local = await inspectLocalCodexCredentialStatus(selection.account.accountId, deps.env);
377
+ return { selection, expiresAt: local.expiresAt };
378
+ }
379
+ async function launchWithSelectedCredentials(deps, selected, launch) {
380
+ if (managedCredentialsV3Enabled(deps.config)) {
381
+ try {
382
+ return await launch();
383
+ }
384
+ finally {
385
+ const report = await reportManagedCodexV3Credentials({
386
+ client: deps.clientFor({
387
+ serverUrl: selected.selection.serviceUrl,
388
+ allowInsecureHttp: selected.selection.allowInsecurePoc
389
+ }),
390
+ tenantId: deps.config.connection.tenantId,
391
+ env: deps.env
392
+ });
393
+ if (!report.reported && report.warningCode) {
394
+ deps.errorOutput.write(`Codex credential observation deferred (${report.warningCode}); local credentials were not marked invalid.\n`);
395
+ }
396
+ }
397
+ }
398
+ if (deps.config.profile?.userBoundRefreshTokens?.delivery !== true)
399
+ return launch();
400
+ let current = selected;
401
+ return superviseCodexShortLivedCredentials({
402
+ initialExpiresAt: current.expiresAt,
403
+ launch: (signal) => launch(signal),
404
+ renew: async () => {
405
+ const installed = await installCredentials(deps, current.selection, {
406
+ serverUrl: current.selection.serviceUrl,
407
+ allowInsecureHttp: current.selection.allowInsecurePoc
408
+ });
409
+ current = installed;
410
+ return { expiresAt: installed.expiresAt };
411
+ }
412
+ });
124
413
  }
125
414
  export function registerCodexCommands(program, deps, options = {}) {
126
415
  const codex = options.nested === false
@@ -147,21 +436,67 @@ export function registerCodexCommands(program, deps, options = {}) {
147
436
  else
148
437
  deps.output.write("Logged out.\n");
149
438
  });
439
+ if (!includeAuthCommands && managedCredentialsV3Enabled(deps.config))
440
+ serviceOptions(codex.command("login")
441
+ .description("Run an isolated official Codex login and install client-managed credentials"))
442
+ .option("--device-auth", "use the official Codex device authorization flow")
443
+ .action(async (opts, command) => {
444
+ if (!managedCredentialsV3Enabled(deps.config)) {
445
+ throw new CodexServiceError("Managed Codex credential v3 is not enabled by the verified platform profile.", "managed_codex_v3_not_enabled", 409);
446
+ }
447
+ const result = await executeManagedCodexV3Login({
448
+ client: deps.clientFor(serviceConnection(opts)),
449
+ tenantId: deps.config.connection.tenantId,
450
+ env: deps.env,
451
+ deviceAuth: opts.deviceAuth
452
+ });
453
+ const assignments = await deps.clientFor(serviceConnection(opts)).listV3CredentialAssignments();
454
+ const assignment = assignments.items.find((item) => item.assignmentId === result.assignmentId);
455
+ await saveCodexSelection(deps.config.persistence.configDirectory, {
456
+ serviceUrl: opts.server ?? deps.config.connection.serverUrl,
457
+ allowInsecurePoc: Boolean(opts.insecurePoc || opts.insecureHttp || deps.config.connection.allowInsecureHttp),
458
+ account: {
459
+ id: result.managedAccountId,
460
+ accountId: result.providerAccountId,
461
+ label: assignment?.label ?? "Codex account",
462
+ distributionMode: "CLIENT_MANAGED_V3",
463
+ assignmentId: result.assignmentId,
464
+ managedAccountId: result.managedAccountId,
465
+ providerAccountId: result.providerAccountId
466
+ }
467
+ });
468
+ const output = {
469
+ enrollmentId: result.enrollmentId,
470
+ assignmentId: result.assignmentId,
471
+ managedAccountId: result.managedAccountId,
472
+ providerAccountId: result.providerAccountId,
473
+ lineageId: result.lineageId,
474
+ generationVersion: result.generationVersion,
475
+ authorityMode: result.authorityMode,
476
+ credentialExpiresAt: result.credentialExpiresAt
477
+ };
478
+ if (await json(command, deps))
479
+ print(deps.output, success(output));
480
+ else
481
+ deps.output.write(`Codex login installed and acknowledged for ${result.providerAccountId}; generation=${result.generationVersion}.\n`);
482
+ });
150
483
  serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
151
484
  .action(async (opts, command) => {
152
- const { response: result, selectedAccountId } = await loadAccountData(deps, opts);
485
+ const { response: result, selectedAccountId, assignments, v3Assignments } = await loadAccountData(deps, opts);
153
486
  const hasAvailableAccount = result.accounts.some(isCodexAccountAvailable);
487
+ const shouldRequestAccount = result.accounts.length === 0
488
+ || !hasAvailableAccount && assignments === undefined && v3Assignments === undefined;
154
489
  if (await json(command, deps))
155
490
  print(deps.output, success({
156
491
  ...result,
157
- ...(hasAvailableAccount ? {} : { guidance: { code: "chatgpt_account_request_required", ticket_template: "IT11", message: NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE } })
492
+ ...(shouldRequestAccount ? { guidance: { code: "chatgpt_account_request_required", ticket_template: "IT11", message: NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE } } : {})
158
493
  }));
159
494
  else if (result.accounts.length === 0) {
160
495
  deps.output.write(`【需要申请 ChatGPT 账号】\n${NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE}\n`);
161
496
  }
162
497
  else {
163
498
  deps.output.write(renderCodexAccountDashboard(result.accounts, selectedAccountId));
164
- if (!hasAvailableAccount) {
499
+ if (shouldRequestAccount) {
165
500
  deps.output.write(`【需要申请 ChatGPT 账号】\n${NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE}\n`);
166
501
  }
167
502
  }
@@ -169,7 +504,7 @@ export function registerCodexCommands(program, deps, options = {}) {
169
504
  serviceOptions(codex.command("use [account]").description("Select an account and update the official Codex auth.json"))
170
505
  .action(async (account, opts, command) => {
171
506
  const result = await chooseAccount(deps, account, opts);
172
- await installCredentials(deps, result.account.id, serviceConnection(opts));
507
+ await installCredentials(deps, result.selection, serviceConnection(opts));
173
508
  if (await json(command, deps))
174
509
  print(deps.output, success({ account: result.account, auth_mode: "chatgpt" }));
175
510
  else
@@ -183,8 +518,8 @@ export function registerCodexCommands(program, deps, options = {}) {
183
518
  if (opts.selectAccount)
184
519
  await clearCodexSelection(deps.config.persistence.configDirectory);
185
520
  const selected = await selectedCredentials(deps, opts.account, opts);
186
- deps.errorOutput.write(`Using ChatGPT account: ${selected.account.label} (${selected.account.id})\n`);
187
- process.exitCode = await deps.run(args);
521
+ deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
522
+ process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
188
523
  });
189
524
  serviceOptions(codex.command("app [workspace]").description("Validate and launch the official Codex desktop app"))
190
525
  .option("-a, --account <id>", "account id")
@@ -195,8 +530,8 @@ export function registerCodexCommands(program, deps, options = {}) {
195
530
  if (opts.selectAccount)
196
531
  await clearCodexSelection(deps.config.persistence.configDirectory);
197
532
  const selected = await selectedCredentials(deps, opts.account, opts);
198
- deps.errorOutput.write(`Using ChatGPT account: ${selected.account.label} (${selected.account.id})\n`);
199
- process.exitCode = await deps.app(workspace);
533
+ deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
534
+ process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.app(workspace, { signal }));
200
535
  });
201
536
  codex.command("status").description("Show selection, desktop app, and official auth.json status")
202
537
  .option("--remote", "include remote accounts")
@@ -247,8 +582,10 @@ export function registerCodexCommands(program, deps, options = {}) {
247
582
  codex.command("reset").description("Reset selection and managed official credentials")
248
583
  .action(async (_options, command) => {
249
584
  const selection = await loadCodexSelection(deps.config.persistence.configDirectory);
250
- const cleanup = await cleanupLegacyCodexProvider();
251
- const auth = await resetManagedCodexFileCredentials(selection?.account.accountId);
585
+ const cleanup = await cleanupLegacyCodexProvider(codexConfigPathForEnv(deps.env));
586
+ if (schemaV2LifecycleEnabled(deps.config))
587
+ await discardCodexFileCredentialBackup(deps.env);
588
+ const auth = await resetManagedCodexFileCredentials(selection?.account.providerAccountId ?? selection?.account.accountId, deps.env);
252
589
  await clearCodexSelection(deps.config.persistence.configDirectory);
253
590
  if (await json(command, deps))
254
591
  print(deps.output, success({ reset: true, auth_action: auth.action, legacy_provider_removed: cleanup.changed }));
@@ -264,7 +601,8 @@ export function registerCodexCommands(program, deps, options = {}) {
264
601
  .option("--replace-login", "confirm replacement")
265
602
  .action(async (account, opts, command) => {
266
603
  if (opts.restore) {
267
- const result = await restoreCodexFileCredentials();
604
+ assertLegacyManagementEnabled(deps.config);
605
+ const result = await restoreCodexFileCredentials(deps.env);
268
606
  if (await json(command, deps))
269
607
  print(deps.output, success({ action: result.action }));
270
608
  else
@@ -275,56 +613,124 @@ export function registerCodexCommands(program, deps, options = {}) {
275
613
  throw new Error("auth-agent replaces the current login; add --replace-login to confirm.");
276
614
  if (opts.credentialStore !== "file")
277
615
  throw new Error("ChatGPT auth.json 同步目前仅支持 --credential-store file。");
278
- const selection = (await chooseAccount(deps, account, opts)).selection;
616
+ if (managedCredentialsV3Enabled(deps.config)) {
617
+ const interval = Number(opts.interval) * 1_000;
618
+ if (!Number.isFinite(interval) || interval <= 0)
619
+ throw new Error("--interval must be positive.");
620
+ do {
621
+ const result = await reportManagedCodexV3Credentials({
622
+ client: deps.clientFor(serviceConnection(opts)),
623
+ tenantId: deps.config.connection.tenantId,
624
+ env: deps.env
625
+ });
626
+ const event = {
627
+ level: result.reported ? "INFO" : "WARN",
628
+ event: result.reported ? "credentials.observed" : "credentials.observation_deferred",
629
+ at: new Date().toISOString(),
630
+ ...(result.generationVersion ? { generationVersion: result.generationVersion } : {}),
631
+ ...(result.warningCode ? { warningCode: result.warningCode } : {})
632
+ };
633
+ if (await json(command, deps))
634
+ print(deps.output, success(event));
635
+ else
636
+ deps.output.write(`${event.at} ${event.level} ${event.event}${result.warningCode ? ` (${result.warningCode})` : ""}\n`);
637
+ if (opts.once)
638
+ break;
639
+ await new Promise((resolve) => setTimeout(resolve, interval));
640
+ } while (true);
641
+ return;
642
+ }
643
+ let selected = await selectedCredentials(deps, account, opts);
279
644
  const interval = Number(opts.interval) * 1_000;
280
- const refreshBefore = Number(opts.refreshBefore) * 1_000;
645
+ const requestedRefreshBefore = Number(opts.refreshBefore) * 1_000;
281
646
  if (!Number.isFinite(interval) || interval <= 0)
282
647
  throw new Error("--interval must be positive.");
283
- if (!Number.isFinite(refreshBefore) || refreshBefore <= 0)
648
+ if (!Number.isFinite(requestedRefreshBefore) || requestedRefreshBefore <= 0) {
284
649
  throw new Error("--refresh-before must be positive.");
285
- const local = await inspectLocalCodexCredentialStatus(selection.account.accountId);
286
- let expiresAt = local.state === "installed" && local.matchesExpectedAccount === true
287
- ? local.expiresAt
288
- : undefined;
650
+ }
651
+ const refreshBefore = effectiveCodexCredentialRefreshBefore(requestedRefreshBefore, deps.config.profile?.userBoundRefreshTokens?.delivery === true);
652
+ let expiresAt = selected.expiresAt;
289
653
  do {
290
654
  const timestamp = Date.now();
291
655
  if (expiresAt === undefined || expiresAt <= timestamp + refreshBefore) {
292
- expiresAt = (await installCredentials(deps, selection.account.id, serviceConnection(opts))).expiresAt;
656
+ selected = await installCredentials(deps, selected.selection, serviceConnection(opts));
657
+ expiresAt = selected.expiresAt;
293
658
  const event = {
294
659
  level: "INFO",
295
660
  event: "credentials.installed",
296
661
  at: new Date(timestamp).toISOString(),
297
- account: selection.account.id,
662
+ account: selected.selection.account.id,
298
663
  expiresAt
299
664
  };
300
665
  if (await json(command, deps))
301
666
  print(deps.output, success(event));
302
667
  else
303
- deps.output.write(`${event.at} INFO credentials.installed ${selection.account.label}\n`);
668
+ deps.output.write(`${event.at} INFO credentials.installed ${selected.selection.account.label}\n`);
304
669
  }
305
670
  else {
306
671
  const event = {
307
672
  level: "INFO",
308
673
  event: "credentials.current",
309
674
  at: new Date(timestamp).toISOString(),
310
- account: selection.account.id,
675
+ account: selected.selection.account.id,
311
676
  expiresAt,
312
677
  refreshAt: expiresAt - refreshBefore
313
678
  };
314
679
  if (await json(command, deps))
315
680
  print(deps.output, success(event));
316
681
  else
317
- deps.output.write(`${event.at} INFO credentials.current ${selection.account.label}; refresh not due\n`);
682
+ deps.output.write(`${event.at} INFO credentials.current ${selected.selection.account.label}; refresh not due\n`);
318
683
  }
319
684
  if (opts.once)
320
685
  break;
321
686
  await new Promise((resolve) => setTimeout(resolve, interval));
322
687
  } while (true);
323
688
  });
324
- serviceOptions(codex.command("upload").description("Upload the local codex-multi-auth account pool"))
689
+ serviceOptions(codex.command("upload").description("Enroll official Codex credentials (v3) or upload the legacy local pool"))
325
690
  .option("--storage <path>", "account storage path")
326
691
  .option("--source <name>", "upload source")
327
692
  .action(async (opts, command) => {
693
+ if (managedCredentialsV3Enabled(deps.config)) {
694
+ if (opts.storage || opts.source) {
695
+ throw new CodexServiceError("Managed credential v3 upload reads only the official CODEX_HOME/auth.json.", "invalid_configuration");
696
+ }
697
+ const result = await executeManagedCodexV3Upload({
698
+ client: deps.clientFor(serviceConnection(opts)),
699
+ tenantId: deps.config.connection.tenantId,
700
+ env: deps.env
701
+ });
702
+ const assignments = await deps.clientFor(serviceConnection(opts)).listV3CredentialAssignments();
703
+ const assignment = assignments.items.find((item) => item.assignmentId === result.assignmentId);
704
+ await saveCodexSelection(deps.config.persistence.configDirectory, {
705
+ serviceUrl: opts.server ?? deps.config.connection.serverUrl,
706
+ allowInsecurePoc: Boolean(opts.insecurePoc || opts.insecureHttp || deps.config.connection.allowInsecureHttp),
707
+ account: {
708
+ id: result.managedAccountId,
709
+ accountId: result.providerAccountId,
710
+ label: assignment?.label ?? "Codex account",
711
+ distributionMode: "CLIENT_MANAGED_V3",
712
+ assignmentId: result.assignmentId,
713
+ managedAccountId: result.managedAccountId,
714
+ providerAccountId: result.providerAccountId
715
+ }
716
+ });
717
+ const output = {
718
+ enrollmentId: result.enrollmentId,
719
+ assignmentId: result.assignmentId,
720
+ managedAccountId: result.managedAccountId,
721
+ providerAccountId: result.providerAccountId,
722
+ lineageId: result.lineageId,
723
+ generationVersion: result.generationVersion,
724
+ authorityMode: result.authorityMode,
725
+ credentialExpiresAt: result.credentialExpiresAt
726
+ };
727
+ if (await json(command, deps))
728
+ print(deps.output, success(output));
729
+ else
730
+ deps.output.write(`Official Codex credentials enrolled, installed, and acknowledged; generation=${result.generationVersion}.\n`);
731
+ return;
732
+ }
733
+ assertLegacyManagementEnabled(deps.config);
328
734
  if (opts.storage)
329
735
  setStoragePathDirect(opts.storage);
330
736
  else
@@ -382,6 +788,124 @@ export function registerCodexCommands(program, deps, options = {}) {
382
788
  if (failures.length > 0)
383
789
  process.exitCode = 1;
384
790
  });
791
+ const enrollment = codex.command("enrollment")
792
+ .description("Execute administrator-created one-shot Codex login intents");
793
+ serviceOptions(enrollment.command("status <intent>").description("Show redacted enrollment intent status"))
794
+ .action(async (intent, opts, command) => {
795
+ const result = await deps.clientFor(serviceConnection(opts)).getEnrollmentIntent(intent);
796
+ if (await json(command, deps)) {
797
+ print(deps.output, { ok: true, ...result });
798
+ return;
799
+ }
800
+ deps.output.write(`${result.intent.id} ${result.intent.type} ${result.intent.status}`
801
+ + ` revision=${result.intent.revision}\n`);
802
+ deps.output.write(`expires=${result.intent.expiresAt}`
803
+ + `${result.intent.assignmentId ? ` assignment=${result.intent.assignmentId}` : ""}`
804
+ + `${result.intent.managedAccountId ? ` account=${result.intent.managedAccountId}` : ""}\n`);
805
+ if (!result.capabilities.candidateUpload) {
806
+ deps.output.write("安全候选上传尚未启用;当前任务只能查询或取消,不会启动 OpenAI 登录。\n");
807
+ }
808
+ });
809
+ serviceOptions(enrollment.command("login <intent>")
810
+ .description("Run one isolated official Codex login for an enrollment intent"))
811
+ .option("--device-auth", "use the official Codex device authorization flow")
812
+ .action(async (intent, _opts, command) => {
813
+ const result = await executeCodexEnrollmentLogin({
814
+ client: deps.clientFor(serviceConnection(_opts)),
815
+ tenantId: deps.config.connection.tenantId,
816
+ intentId: intent,
817
+ deviceAuth: _opts.deviceAuth
818
+ });
819
+ if (await json(command, deps))
820
+ print(deps.output, { ok: true, ...result });
821
+ else {
822
+ deps.output.write(`${result.intent.id} 登录候选已处理;状态=${result.intent.status}`
823
+ + `${result.result.managedAccountId ? `,账号=${result.result.managedAccountId}` : ""}`
824
+ + `${result.result.lineageId ? `,lineage=${result.result.lineageId}` : ""}`
825
+ + "。\n");
826
+ if (result.result.outcome === "TEMPORARILY_UNAVAILABLE") {
827
+ deps.output.write("同一进程内的安全重试已耗尽;请取消并新建任务,不能用另一次登录覆盖已保留的候选。\n");
828
+ }
829
+ }
830
+ });
831
+ serviceOptions(enrollment.command("login-batch <planId>")
832
+ .description("Sequentially execute isolated logins from a migration plan queue")
833
+ .option("--device-auth", "use the official Codex device authorization flow")
834
+ .option("--dry-run", "show the redacted sequential plan without starting login")
835
+ .option("--page-size <count>", "migration queue page size", "100"))
836
+ .action(async (planId, opts, command) => {
837
+ if (deps.config.profile?.userBoundRefreshTokens?.migrationPlans !== true) {
838
+ throw new CodexServiceError("The selected tenant has not enabled schema-v2 migration login plans.", "capability_not_enabled", 409);
839
+ }
840
+ const pageSize = Number(opts.pageSize);
841
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
842
+ throw new CodexServiceError("--page-size must be an integer from 1 to 100.", "invalid_configuration");
843
+ }
844
+ const batch = await executeCodexMigrationLoginBatch({
845
+ client: deps.clientFor(serviceConnection(opts)),
846
+ tenantId: deps.config.connection.tenantId,
847
+ planId,
848
+ deviceAuth: opts.deviceAuth,
849
+ dryRun: opts.dryRun,
850
+ pageSize
851
+ });
852
+ const failed = batch.counts.failed > 0
853
+ || ["TARGET_UNCERTAIN", "AUTHENTICATION_ERROR", "SERVICE_ERROR", "LOGIN_ERROR"]
854
+ .includes(batch.stopReason);
855
+ if (await json(command, deps)) {
856
+ print(deps.output, { ok: !failed, ...batch });
857
+ }
858
+ else {
859
+ deps.output.write(`migration=${batch.plan.id} status=${batch.plan.status} revision=${batch.plan.revision}`
860
+ + ` stop=${batch.stopReason}${batch.dryRun ? " dry-run" : ""}\n`);
861
+ deps.output.write(`capacity: verifiedMinimum=${batch.capacity.verifiedMinimum}, verifiedLimit=unknown,`
862
+ + ` sequentialRequired=${batch.capacity.sequentialRequired}; verifiedMinimum 不是上限。\n`);
863
+ for (const item of batch.items) {
864
+ deps.output.write(`${item.assignmentId} ${item.action}/${item.status}`
865
+ + `${item.intentId ? ` intent=${item.intentId}` : ""}`
866
+ + `${item.outcome ? ` outcome=${item.outcome}` : ""}`
867
+ + `${item.skipReason ? ` skip=${item.skipReason}` : ""}`
868
+ + `${item.safeErrorCode ? ` error=${item.safeErrorCode}` : ""}\n`);
869
+ }
870
+ deps.output.write(`summary: attempted=${batch.counts.attempted}, completed=${batch.counts.completed},`
871
+ + ` failed=${batch.counts.failed}, skipped=${batch.counts.skipped}.\n`);
872
+ }
873
+ if (failed)
874
+ process.exitCode = 1;
875
+ });
876
+ serviceOptions(enrollment.command("cancel <intent>").description("Cancel an enrollment intent"))
877
+ .option("--idempotency-key <key>", "reuse a prior cancellation idempotency key")
878
+ .action(async (intent, opts, command) => {
879
+ const client = deps.clientFor(serviceConnection(opts));
880
+ const current = await client.getEnrollmentIntent(intent);
881
+ const result = await client.cancelEnrollmentIntent(intent, {
882
+ idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(),
883
+ revision: current.intent.revision
884
+ });
885
+ if (await json(command, deps))
886
+ print(deps.output, { ok: true, ...result });
887
+ else
888
+ deps.output.write(`${result.intent.id} 已取消;状态=${result.intent.status}。\n`);
889
+ });
890
+ const retirement = codex.command("retirement")
891
+ .description("Remove an exact locally installed lineage for a Server retirement request");
892
+ serviceOptions(retirement.command("acknowledge <retirement>")
893
+ .description("Delete the matching managed short-lived credentials and append local evidence")
894
+ .option("--idempotency-key <key>", "reuse a prior local deletion acknowledgement key"))
895
+ .action(async (retirementId, opts, command) => {
896
+ const result = await executeCodexAssignmentRetirementLocalDelete({
897
+ client: deps.clientFor(serviceConnection(opts)),
898
+ retirementId,
899
+ env: deps.env,
900
+ ...(opts.idempotencyKey?.trim() ? { idempotencyKey: opts.idempotencyKey.trim() } : {})
901
+ });
902
+ if (await json(command, deps))
903
+ print(deps.output, { ok: true, ...result });
904
+ else
905
+ deps.output.write(`本机匹配的短效凭据已删除并记录证据;retirement=${result.retirement.id}`
906
+ + `,revision=${result.retirement.revision},状态=${result.retirement.status}。\n`
907
+ + "该证据不代表其他终端已清理;最终退役仍等待全部已披露短效凭据自然过期。\n");
908
+ });
385
909
  const admin = codex.command("admin")
386
910
  .description("Administer server-managed Codex accounts and assignments");
387
911
  const adminAccounts = admin.command("accounts")
@@ -425,6 +949,7 @@ export function registerCodexCommands(program, deps, options = {}) {
425
949
  serviceOptions(adminAccounts.command("refresh <account>")
426
950
  .description("Request one server-side refresh-token exchange"))
427
951
  .action(async (account, opts, command) => {
952
+ assertLegacyManagementEnabled(deps.config);
428
953
  const client = deps.clientFor(serviceConnection(opts));
429
954
  const resolved = await resolveAdminAccount(client, account);
430
955
  const result = await client.refreshAdminAccount(resolved.id);
@@ -468,6 +993,7 @@ export function registerCodexCommands(program, deps, options = {}) {
468
993
  .description("Bind a ChatGPT/Codex account to a Keycloak user"))
469
994
  .option("--default", "make this the user's default Codex account")
470
995
  .action(async (account, subject, opts, command) => {
996
+ assertLegacyManagementEnabled(deps.config);
471
997
  const client = deps.clientFor(serviceConnection(opts));
472
998
  const resolved = await resolveAdminAccount(client, account);
473
999
  const result = await client.bindAdminAssignment({
@@ -484,6 +1010,7 @@ export function registerCodexCommands(program, deps, options = {}) {
484
1010
  serviceOptions(adminAssignments.command("unbind <assignment>")
485
1011
  .description("Remove a ChatGPT/Codex account assignment"))
486
1012
  .action(async (assignment, opts, command) => {
1013
+ assertLegacyManagementEnabled(deps.config);
487
1014
  const client = deps.clientFor(serviceConnection(opts));
488
1015
  const assignmentList = await client.listAdminAssignments();
489
1016
  const target = assignmentList.assignments.find((item) => item.id === assignment);