@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,74 @@
1
+ import { validateAntigravityImportCredential } from "../google-antigravity";
2
+ import { upsertCredentialByIdentity } from "../store";
3
+ import type { OAuthCredentials } from "../types";
4
+ import {
5
+ ACCOUNT_IMPORT_FORMAT,
6
+ ACCOUNT_IMPORT_MAX_EMAIL_LENGTH,
7
+ ACCOUNT_IMPORT_PROVIDER,
8
+ throwIfAccountImportAborted,
9
+ type AccountImportAdapter,
10
+ type CockpitAccountRecord,
11
+ type ValidatedAntigravityCredential,
12
+ } from "./types";
13
+
14
+ export interface AntigravityImportAdapterDeps {
15
+ validate(refreshToken: string, signal?: AbortSignal): Promise<ValidatedAntigravityCredential>;
16
+ upsert(credential: OAuthCredentials): Promise<"inserted" | "updated">;
17
+ }
18
+
19
+ function safeProviderEmail(value: unknown): string | null {
20
+ if (typeof value !== "string") return null;
21
+ const normalized = value.toLowerCase();
22
+ if (
23
+ normalized.length === 0
24
+ || normalized.length > ACCOUNT_IMPORT_MAX_EMAIL_LENGTH
25
+ || normalized !== normalized.trim()
26
+ || /[\x00-\x20\x7f]/.test(normalized)
27
+ ) return null;
28
+ return normalized;
29
+ }
30
+
31
+ export function createAntigravityAccountImportAdapter(
32
+ deps: AntigravityImportAdapterDeps = {
33
+ validate: validateAntigravityImportCredential,
34
+ upsert: credential => upsertCredentialByIdentity(ACCOUNT_IMPORT_PROVIDER, credential),
35
+ },
36
+ ): AccountImportAdapter {
37
+ return {
38
+ provider: ACCOUNT_IMPORT_PROVIDER,
39
+ format: ACCOUNT_IMPORT_FORMAT,
40
+ async importRecord(record: CockpitAccountRecord, signal?: AbortSignal) {
41
+ throwIfAccountImportAborted(signal);
42
+ let credential: ValidatedAntigravityCredential;
43
+ try {
44
+ credential = await deps.validate(record.refreshToken, signal);
45
+ } catch {
46
+ throwIfAccountImportAborted(signal);
47
+ return { status: "failed", code: "credential_rejected" };
48
+ }
49
+
50
+ throwIfAccountImportAborted(signal);
51
+
52
+ const providerEmail = safeProviderEmail(credential.email);
53
+ if (!providerEmail) return { status: "failed", code: "credential_rejected" };
54
+ if (providerEmail !== record.email) return { status: "failed", code: "identity_mismatch" };
55
+ if (typeof credential.projectId !== "string" || credential.projectId.length === 0) {
56
+ return { status: "failed", code: "missing_project" };
57
+ }
58
+
59
+ throwIfAccountImportAborted(signal);
60
+ try {
61
+ const disposition = await deps.upsert({
62
+ ...credential,
63
+ email: providerEmail,
64
+ source: "credential-file",
65
+ });
66
+ return disposition === "updated"
67
+ ? { status: "updated", code: "updated" }
68
+ : { status: "imported", code: "imported" };
69
+ } catch {
70
+ return { status: "failed", code: "persist_failed" };
71
+ }
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,15 @@
1
+ export { importAccounts } from "./service";
2
+ export {
3
+ ACCOUNT_IMPORT_DEADLINE_MS,
4
+ ACCOUNT_IMPORT_FORMAT,
5
+ ACCOUNT_IMPORT_MAX_BYTES,
6
+ ACCOUNT_IMPORT_MAX_REQUEST_BYTES,
7
+ ACCOUNT_IMPORT_PROVIDER,
8
+ } from "./types";
9
+ export type {
10
+ AccountImportCode,
11
+ AccountImportRecordResult,
12
+ AccountImportRequest,
13
+ AccountImportResult,
14
+ AccountImportStatus,
15
+ } from "./types";
@@ -0,0 +1,83 @@
1
+ import {
2
+ ACCOUNT_IMPORT_MAX_BYTES,
3
+ ACCOUNT_IMPORT_MAX_EMAIL_LENGTH,
4
+ ACCOUNT_IMPORT_MAX_RECORDS,
5
+ ACCOUNT_IMPORT_MAX_REFRESH_TOKEN_BYTES,
6
+ type ParsedCockpitRecord,
7
+ } from "./types";
8
+
9
+ const encoder = new TextEncoder();
10
+ const ALLOWED_RECORD_KEYS = new Set(["email", "refresh_token", "tags", "notes"]);
11
+
12
+ export type CockpitDocumentParseResult =
13
+ | { ok: true; records: ParsedCockpitRecord[] }
14
+ | { ok: false; code: "invalid_document" };
15
+
16
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
18
+ const prototype = Object.getPrototypeOf(value);
19
+ return prototype === Object.prototype || prototype === null;
20
+ }
21
+
22
+ function serializedDocumentSize(document: unknown): number | null {
23
+ try {
24
+ const serialized = JSON.stringify(document);
25
+ return serialized === undefined ? null : encoder.encode(serialized).byteLength;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function validEmail(value: unknown): value is string {
32
+ if (typeof value !== "string" || value.length === 0 || value.length > ACCOUNT_IMPORT_MAX_EMAIL_LENGTH) return false;
33
+ if (value !== value.trim() || /[\x00-\x20\x7f]/.test(value)) return false;
34
+ const at = value.indexOf("@");
35
+ return at > 0 && at === value.lastIndexOf("@") && at < value.length - 1;
36
+ }
37
+
38
+ function validRefreshToken(value: unknown): value is string {
39
+ return typeof value === "string"
40
+ && value.length > 0
41
+ && encoder.encode(value).byteLength <= ACCOUNT_IMPORT_MAX_REFRESH_TOKEN_BYTES
42
+ && !/[\s\x7f]/.test(value);
43
+ }
44
+
45
+ function validMetadata(record: Record<string, unknown>): boolean {
46
+ if (record.tags !== undefined && (
47
+ !Array.isArray(record.tags)
48
+ || record.tags.some(tag => typeof tag !== "string")
49
+ )) return false;
50
+ return record.notes === undefined || typeof record.notes === "string";
51
+ }
52
+
53
+ function parseRecord(value: unknown, index: number): ParsedCockpitRecord {
54
+ if (!isPlainRecord(value)) return { index, code: "invalid_record" };
55
+ if (Object.keys(value).some(key => !ALLOWED_RECORD_KEYS.has(key))) return { index, code: "invalid_record" };
56
+ if (!validEmail(value.email) || !validRefreshToken(value.refresh_token) || !validMetadata(value)) {
57
+ return { index, code: "invalid_record" };
58
+ }
59
+ return {
60
+ index,
61
+ record: {
62
+ email: value.email.toLowerCase(),
63
+ refreshToken: value.refresh_token,
64
+ },
65
+ };
66
+ }
67
+
68
+ export function parseCockpitAccountDocument(document: unknown): CockpitDocumentParseResult {
69
+ const size = serializedDocumentSize(document);
70
+ if (size === null || size > ACCOUNT_IMPORT_MAX_BYTES) return { ok: false, code: "invalid_document" };
71
+ if (!Array.isArray(document) || document.length === 0 || document.length > ACCOUNT_IMPORT_MAX_RECORDS) {
72
+ return { ok: false, code: "invalid_document" };
73
+ }
74
+ const seen = new Set<string>();
75
+ const records = document.map((value, index): ParsedCockpitRecord => {
76
+ const parsed = parseRecord(value, index);
77
+ if ("code" in parsed) return parsed;
78
+ if (seen.has(parsed.record.email)) return { index, code: "invalid_record" };
79
+ seen.add(parsed.record.email);
80
+ return parsed;
81
+ });
82
+ return { ok: true, records };
83
+ }
@@ -0,0 +1,18 @@
1
+ import { createAntigravityAccountImportAdapter } from "./google-antigravity-adapter";
2
+ import {
3
+ ACCOUNT_IMPORT_FORMAT,
4
+ ACCOUNT_IMPORT_PROVIDER,
5
+ type AccountImportAdapter,
6
+ } from "./types";
7
+
8
+ export type AccountImportRegistryResult =
9
+ | { ok: true; adapter: AccountImportAdapter }
10
+ | { ok: false; code: "unsupported_provider" | "unsupported_format" };
11
+
12
+ const antigravityAdapter = createAntigravityAccountImportAdapter();
13
+
14
+ export function resolveAccountImportAdapter(provider: string, format: string): AccountImportRegistryResult {
15
+ if (provider !== ACCOUNT_IMPORT_PROVIDER) return { ok: false, code: "unsupported_provider" };
16
+ if (format !== ACCOUNT_IMPORT_FORMAT) return { ok: false, code: "unsupported_format" };
17
+ return { ok: true, adapter: antigravityAdapter };
18
+ }
@@ -0,0 +1,75 @@
1
+ import { parseCockpitAccountDocument } from "./parser";
2
+ import { resolveAccountImportAdapter, type AccountImportRegistryResult } from "./registry";
3
+ import {
4
+ AccountImportAbortError,
5
+ throwIfAccountImportAborted,
6
+ } from "./types";
7
+ import type {
8
+ AccountImportRequest,
9
+ AccountImportResult,
10
+ AccountImportRecordResult,
11
+ } from "./types";
12
+
13
+ export type AccountImportServiceResult =
14
+ | { ok: true; result: AccountImportResult }
15
+ | {
16
+ ok: false;
17
+ status: 400 | 408;
18
+ code: "unsupported_provider" | "unsupported_format" | "invalid_document" | "import_cancelled";
19
+ /** True when at least one credential was committed before the terminal failure. */
20
+ changed?: boolean;
21
+ };
22
+
23
+ export interface AccountImportServiceDeps {
24
+ resolveAdapter(provider: string, format: string): AccountImportRegistryResult;
25
+ }
26
+
27
+ export async function importAccounts(
28
+ request: AccountImportRequest,
29
+ deps: AccountImportServiceDeps = { resolveAdapter: resolveAccountImportAdapter },
30
+ ): Promise<AccountImportServiceResult> {
31
+ // Provider/format admission intentionally precedes any traversal or serialization of the
32
+ // credential-bearing document.
33
+ const resolved = deps.resolveAdapter(request.provider, request.format);
34
+ if (!resolved.ok) return { ok: false, status: 400, code: resolved.code };
35
+
36
+ const parsed = parseCockpitAccountDocument(request.document);
37
+ if (!parsed.ok) return { ok: false, status: 400, code: parsed.code };
38
+
39
+ const results: AccountImportRecordResult[] = [];
40
+ try {
41
+ for (const item of parsed.records) {
42
+ throwIfAccountImportAborted(request.signal);
43
+ if ("code" in item) {
44
+ results.push({ index: item.index, status: "failed", code: item.code });
45
+ continue;
46
+ }
47
+ const outcome = await resolved.adapter.importRecord(item.record, request.signal);
48
+ results.push({ index: item.index, ...outcome });
49
+ // Record the committed outcome before observing cancellation. The route uses this
50
+ // internal signal to reconcile live stores and caches even when the public response
51
+ // is the terminal 408 DTO.
52
+ throwIfAccountImportAborted(request.signal);
53
+ }
54
+ } catch (error) {
55
+ if (error instanceof AccountImportAbortError) {
56
+ const changed = results.some(result => result.status === "imported" || result.status === "updated");
57
+ return { ok: false, status: 408, code: "import_cancelled", ...(changed ? { changed: true } : {}) };
58
+ }
59
+ throw error;
60
+ }
61
+
62
+ const count = (status: AccountImportRecordResult["status"]) =>
63
+ results.filter(result => result.status === status).length;
64
+ return {
65
+ ok: true,
66
+ result: {
67
+ totalCount: results.length,
68
+ importedCount: count("imported"),
69
+ updatedCount: count("updated"),
70
+ failedCount: count("failed"),
71
+ unsupportedCount: count("unsupported"),
72
+ results,
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,91 @@
1
+ import type { OAuthCredentials } from "../types";
2
+
3
+ export const ACCOUNT_IMPORT_PROVIDER = "google-antigravity" as const;
4
+ export const ACCOUNT_IMPORT_FORMAT = "cockpit-tools" as const;
5
+ export const ACCOUNT_IMPORT_MAX_BYTES = 256 * 1024;
6
+ // The HTTP request also carries the fixed provider/format envelope. Keep that envelope bounded
7
+ // without making a valid document at the 256 KiB contract boundary fail at the API layer.
8
+ export const ACCOUNT_IMPORT_MAX_REQUEST_BYTES = ACCOUNT_IMPORT_MAX_BYTES + 1024;
9
+ // Shared CLI POST timeout and management-route deadline. Individual provider requests keep
10
+ // shorter timeouts; this bounds the full admitted batch (up to ACCOUNT_IMPORT_MAX_RECORDS).
11
+ export const ACCOUNT_IMPORT_DEADLINE_MS = 600_000;
12
+ export const ACCOUNT_IMPORT_MAX_RECORDS = 25;
13
+ export const ACCOUNT_IMPORT_MAX_EMAIL_LENGTH = 254;
14
+ export const ACCOUNT_IMPORT_MAX_REFRESH_TOKEN_BYTES = 16 * 1024;
15
+
16
+ export type AccountImportStatus = "imported" | "updated" | "failed" | "unsupported";
17
+
18
+ export type AccountImportCode =
19
+ | "imported"
20
+ | "updated"
21
+ | "import_cancelled"
22
+ | "unsupported_provider"
23
+ | "unsupported_format"
24
+ | "invalid_document"
25
+ | "invalid_record"
26
+ | "credential_rejected"
27
+ | "identity_mismatch"
28
+ | "missing_project"
29
+ | "persist_failed";
30
+
31
+ export interface AccountImportRecordResult {
32
+ index: number;
33
+ status: AccountImportStatus;
34
+ code: AccountImportCode;
35
+ }
36
+
37
+ export interface AccountImportResult {
38
+ totalCount: number;
39
+ importedCount: number;
40
+ updatedCount: number;
41
+ failedCount: number;
42
+ unsupportedCount: number;
43
+ results: AccountImportRecordResult[];
44
+ }
45
+
46
+ export interface AccountImportRequest {
47
+ provider: string;
48
+ format: string;
49
+ document: unknown;
50
+ signal?: AbortSignal;
51
+ }
52
+
53
+ /** A secret-free terminal signal for a cancelled import request. */
54
+ export class AccountImportAbortError extends Error {
55
+ constructor() {
56
+ super("Account import cancelled");
57
+ this.name = "AccountImportAbortError";
58
+ }
59
+ }
60
+
61
+ export function throwIfAccountImportAborted(signal?: AbortSignal): void {
62
+ if (signal?.aborted) throw new AccountImportAbortError();
63
+ }
64
+
65
+ export interface CockpitAccountRecord {
66
+ email: string;
67
+ refreshToken: string;
68
+ }
69
+
70
+ export type ParsedCockpitRecord =
71
+ | { index: number; record: CockpitAccountRecord }
72
+ | { index: number; code: "invalid_record" };
73
+
74
+ export type AccountImportRecordOutcome =
75
+ | { status: "imported"; code: "imported" }
76
+ | { status: "updated"; code: "updated" }
77
+ | {
78
+ status: "failed";
79
+ code: "credential_rejected" | "identity_mismatch" | "missing_project" | "persist_failed";
80
+ };
81
+
82
+ export interface AccountImportAdapter {
83
+ readonly provider: typeof ACCOUNT_IMPORT_PROVIDER;
84
+ readonly format: typeof ACCOUNT_IMPORT_FORMAT;
85
+ importRecord(record: CockpitAccountRecord, signal?: AbortSignal): Promise<AccountImportRecordOutcome>;
86
+ }
87
+
88
+ export interface ValidatedAntigravityCredential extends OAuthCredentials {
89
+ email?: string;
90
+ projectId?: string;
91
+ }
@@ -81,10 +81,21 @@ function parseTokenResponse(responseBody: string): AnthropicTokenResponse {
81
81
  function credsFrom(data: AnthropicTokenResponse, refreshFallback?: string): OAuthCredentials {
82
82
  const accountUuid = data.account?.uuid;
83
83
  const email = data.account?.email_address;
84
+ // Guard against a missing/non-finite/negative expires_in (malformed upstream
85
+ // response): a NaN expiry would never compare as expired, and a negative
86
+ // duration would stamp an already-past expiry — both block refresh semantics.
87
+ const expiresIn =
88
+ typeof data.expires_in === "number" && Number.isFinite(data.expires_in) && data.expires_in >= 0
89
+ ? data.expires_in
90
+ : 3600;
91
+ // The computed timestamp itself must stay finite: Number.MAX_VALUE passes
92
+ // Number.isFinite but overflows to Infinity once multiplied by 1000.
93
+ const computedExpires = Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
94
+ const expires = Number.isFinite(computedExpires) ? computedExpires : Date.now() + 3600 * 1000 - 5 * 60 * 1000;
84
95
  return {
85
96
  refresh: data.refresh_token || refreshFallback || "",
86
97
  access: data.access_token,
87
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
98
+ expires,
88
99
  accountId: typeof accountUuid === "string" && accountUuid.length > 0 ? accountUuid : undefined,
89
100
  email: typeof email === "string" && email.length > 0 ? email : undefined,
90
101
  };
@@ -5,7 +5,7 @@
5
5
  * Change vs source: the success/error page is an inline HTML constant. opencodex's GUI polls
6
6
  * GET /api/oauth/status, so it does not need OAuth state injected into the callback page.
7
7
  *
8
- * Handles: port allocation (preferred → random fallback), callback server, CSRF state,
8
+ * Handles: port allocation (preferred → random/manual-only fallback), callback server, CSRF state,
9
9
  * manual-input race, 300s timeout. Providers implement generateAuthUrl() + exchangeToken().
10
10
  */
11
11
  import { isAddrInUse } from "../server/ports";
@@ -131,8 +131,14 @@ export abstract class OAuthCallbackFlow {
131
131
  }
132
132
  const redirectUri = `http://${this.callbackHostname}:${this.preferredPort}${this.callbackPath}`;
133
133
  return { servers, redirectUri };
134
- } catch {
134
+ } catch (error) {
135
135
  if (this.redirectUri) {
136
+ // Fixed OAuth redirect URIs cannot move to a random port. Remote dashboards can
137
+ // still complete the same state/PKCE flow by pasting the final redirect URL or
138
+ // authorization code, so do not make a local listener a prerequisite for that path.
139
+ if (this.ctrl.onManualCodeInput && isAddrInUse(error)) {
140
+ return { servers: [], redirectUri: this.redirectUri };
141
+ }
136
142
  throw new Error(
137
143
  `OAuth callback port ${this.preferredPort} unavailable; cannot fall back to a random port when redirectUri is set`,
138
144
  );
@@ -49,10 +49,21 @@ export function extractEmail(idToken?: string, accessToken?: string): string | u
49
49
  function credsFromToken(data: Record<string, unknown>): OAuthCredentials {
50
50
  const idToken = typeof data.id_token === "string" ? data.id_token : undefined;
51
51
  const accessToken = data.access_token as string;
52
+ // ?? only guards null/undefined; NaN or a string expires_in would otherwise
53
+ // produce a NaN expiry that never compares as expired, and a negative duration
54
+ // would stamp an already-past expiry — both block refresh semantics.
55
+ const expiresIn =
56
+ typeof data.expires_in === "number" && Number.isFinite(data.expires_in) && data.expires_in >= 0
57
+ ? data.expires_in
58
+ : 3600;
59
+ // The computed timestamp itself must stay finite: Number.MAX_VALUE passes
60
+ // Number.isFinite but overflows to Infinity once multiplied by 1000.
61
+ const computedExpires = Date.now() + expiresIn * 1000;
62
+ const expires = Number.isFinite(computedExpires) ? computedExpires : Date.now() + 3600 * 1000;
52
63
  return {
53
64
  access: accessToken,
54
65
  refresh: (data.refresh_token as string) ?? "",
55
- expires: Date.now() + ((data.expires_in as number) ?? 3600) * 1000,
66
+ expires,
56
67
  accountId: extractAccountId(idToken, accessToken),
57
68
  email: extractEmail(idToken, accessToken),
58
69
  };
@@ -20,6 +20,7 @@ const CLIENT_SECRET = process.env.GOOGLE_ANTIGRAVITY_CLIENT_SECRET
20
20
  || "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";
21
21
  const AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
22
22
  const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
23
+ const USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v2/userinfo";
23
24
  const PROD_API = "https://cloudcode-pa.googleapis.com";
24
25
  const DAILY_API = "https://daily-cloudcode-pa.googleapis.com";
25
26
  const API_VERSION = "v1internal";
@@ -228,3 +229,29 @@ export async function refreshAntigravityToken(refreshToken: string, signal?: Abo
228
229
  const projectId = await discoverAntigravityProject(creds.access, signal).catch(() => undefined);
229
230
  return projectId ? { ...creds, projectId } : creds;
230
231
  }
232
+
233
+ /**
234
+ * Refresh and derive import identity from Google's pinned userinfo endpoint. Imports may not
235
+ * borrow the email written in a local file: that would let one valid token overwrite another
236
+ * account's slot when a refresh response has no id_token.
237
+ */
238
+ export async function validateAntigravityImportCredential(
239
+ refreshToken: string,
240
+ signal?: AbortSignal,
241
+ ): Promise<OAuthCredentials> {
242
+ const credential = await refreshAntigravityToken(refreshToken, signal);
243
+ const response = await fetch(USERINFO_ENDPOINT, {
244
+ method: "GET",
245
+ headers: { Accept: "application/json", Authorization: `Bearer ${credential.access}` },
246
+ signal: requestSignal(signal),
247
+ });
248
+ if (!response.ok) throw new Error(`Antigravity identity request failed: ${response.status}`);
249
+ const body = (await response.json().catch(() => undefined)) as { email?: unknown; id?: unknown } | undefined;
250
+ if (typeof body?.email !== "string" || body.email.length === 0) {
251
+ throw new Error("Antigravity identity response did not include an email");
252
+ }
253
+ if (typeof body.id !== "string" || body.id.length === 0) {
254
+ throw new Error("Antigravity identity response did not include an account id");
255
+ }
256
+ return { ...credential, accountId: body.id, email: body.email.toLowerCase() };
257
+ }
@@ -3,16 +3,11 @@ import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
3
3
  import { isAccountNeedsReauth } from "../codex/account-runtime-state";
4
4
  import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
5
5
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
6
- import { configuredAdminToken } from "../lib/admin-secrets";
7
6
  import { readRuntimePort } from "../config";
8
- import {
9
- LOCAL_ATTESTATION_CHALLENGE_HEADER,
10
- LOCAL_ATTESTATION_PROOF_HEADER,
11
- createLocalAttestationChallenge,
12
- verifyLocalAttestationProof,
13
- } from "../lib/local-management-attestation";
7
+ import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
14
8
  import { maskAccountId } from "../lib/privacy";
15
- import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
9
+ import { findLiveProxy } from "../server/proxy-liveness";
10
+ import { fetchBoundLocalManagementRead } from "../server/local-management-read-client";
16
11
  import { loadAuthStore, peekAuthStore, peekOAuthRefreshIntent, readOAuthRefreshIntent } from "./store";
17
12
  import type { ProviderAccount } from "./types";
18
13
 
@@ -333,53 +328,22 @@ type LiveProxyCodexHealthResult = {
333
328
  };
334
329
 
335
330
  async function fetchCodexHealthFromLiveProxy(
336
- fetchImpl: typeof fetch = fetch,
331
+ fetchImpl: typeof fetch | undefined = undefined,
337
332
  findLiveProxyImpl: typeof findLiveProxy = findLiveProxy,
338
333
  readRuntimePortImpl: typeof readRuntimePort = readRuntimePort,
339
334
  ): Promise<LiveProxyCodexHealthResult> {
340
335
  const live = await findLiveProxyImpl();
341
336
  if (!live) return { source: "unavailable", entries: null };
342
- // This is a management-plane endpoint. A data-plane service token is intentionally not
343
- // interchangeable with the admin credential even on loopback.
344
- const token = configuredAdminToken();
345
- const headers: Record<string, string> = {};
346
337
  try {
347
- if (token) {
348
- // Public /healthz identity is intentionally forgeable enough for liveness, not
349
- // strong enough to receive a bearer. Prove the listener knows the per-process
350
- // secret stored in the protected runtime record before attaching the admin token.
351
- if (live.source !== "runtime" || live.pid === null) {
352
- return { source: "management-api-unavailable", entries: null };
353
- }
354
- const attestedPid = live.pid;
355
- const runtime = readRuntimePortImpl(attestedPid);
356
- if (!runtime?.attestationSecret || runtime.port !== live.port) {
357
- return { source: "management-api-unavailable", entries: null };
358
- }
359
- const challenge = createLocalAttestationChallenge();
360
- const proofResponse = await fetchImpl(
361
- `http://${probeHostname(live.hostname)}:${live.port}/healthz`,
362
- {
363
- headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
364
- signal: AbortSignal.timeout(4000),
365
- },
366
- );
367
- const proof = proofResponse.headers.get(LOCAL_ATTESTATION_PROOF_HEADER);
368
- if (!proofResponse.ok || !verifyLocalAttestationProof(
369
- runtime.attestationSecret,
370
- challenge,
371
- attestedPid,
372
- live.port,
373
- proof,
374
- )) {
375
- return { source: "management-api-unavailable", entries: null };
376
- }
377
- headers.Authorization = `Bearer ${token}`;
378
- }
379
- const res = await fetchImpl(
380
- `http://${probeHostname(live.hostname)}:${live.port}/api/codex-auth/accounts`,
381
- { headers, signal: AbortSignal.timeout(4000) },
338
+ const read = await fetchBoundLocalManagementRead(
339
+ live,
340
+ LOCAL_MANAGEMENT_READ_PATHS.codexAccounts,
341
+ { fetchImpl, readRuntime: readRuntimePortImpl, timeoutMs: 4_000 },
382
342
  );
343
+ if (read.kind === "unavailable") {
344
+ return { source: "management-api-unavailable", entries: null };
345
+ }
346
+ const res = read.response;
383
347
  if (res.status === 401 || res.status === 403) {
384
348
  return { source: "management-auth-failed", entries: null };
385
349
  }