@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.
- package/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- 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
|
+
}
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -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
|
|
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
|
);
|
package/src/oauth/chatgpt.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/oauth/health.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
}
|