@iislee/opencodex 2.31.0 → 2.35.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 +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface PackageTreeObservation {
|
|
4
|
+
readonly device: bigint;
|
|
5
|
+
readonly inode: bigint;
|
|
6
|
+
readonly contentTimeNs: bigint;
|
|
7
|
+
readonly size: bigint;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PackageTreeIntegrityStatus =
|
|
11
|
+
| { readonly ok: true }
|
|
12
|
+
| { readonly ok: false; readonly reason: "package_tree_replaced" | "package_tree_unreadable" };
|
|
13
|
+
|
|
14
|
+
export interface PackageTreeIntegrityGuard {
|
|
15
|
+
status(): PackageTreeIntegrityStatus;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ObservePackageTree = () => PackageTreeObservation | null;
|
|
19
|
+
type PackageTreeRuntimeInstall = "bun" | "npm" | "source";
|
|
20
|
+
|
|
21
|
+
const packageManifestUrl = new URL("../../package.json", import.meta.url);
|
|
22
|
+
|
|
23
|
+
function observePackageManifest(): PackageTreeObservation | null {
|
|
24
|
+
try {
|
|
25
|
+
const stat = statSync(packageManifestUrl, { bigint: true });
|
|
26
|
+
return {
|
|
27
|
+
device: stat.dev,
|
|
28
|
+
inode: stat.ino,
|
|
29
|
+
// mtimeNs, NOT ctimeNs. An inode-change time moves for METADATA writes that
|
|
30
|
+
// replace nothing: chmod, chown, touch, an editor normalizing permissions, a
|
|
31
|
+
// backup tool restoring modes. Each of those left device, inode and size
|
|
32
|
+
// identical, so the comparison below called the manifest "replaced" and every
|
|
33
|
+
// /v1/* request answered 503 until the process was restarted. Measured on
|
|
34
|
+
// macOS: chmod alone moved ctimeNs and left mtimeNs untouched.
|
|
35
|
+
//
|
|
36
|
+
// mtimeNs still catches every real replacement. An in-place rewrite of the
|
|
37
|
+
// same byte length moves mtimeNs while inode and size hold; an atomic
|
|
38
|
+
// install (write-then-rename, which is what a package manager does) changes
|
|
39
|
+
// the inode as well. Both were measured before this change was made.
|
|
40
|
+
contentTimeNs: stat.mtimeNs,
|
|
41
|
+
size: stat.size,
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sameObservation(left: PackageTreeObservation, right: PackageTreeObservation): boolean {
|
|
49
|
+
return left.device === right.device
|
|
50
|
+
&& left.inode === right.inode
|
|
51
|
+
&& left.contentTimeNs === right.contentTimeNs
|
|
52
|
+
&& left.size === right.size;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How long an `ok` observation is reused before the manifest is stat'd again.
|
|
57
|
+
*
|
|
58
|
+
* `status()` runs on `/healthz`, `/readyz` and every `/v1/*` request, so an unthrottled guard
|
|
59
|
+
* adds a filesystem syscall to the proxy's hot path to detect an event that happens at most
|
|
60
|
+
* once per install. A replaced tree is not time-critical either: the process is already
|
|
61
|
+
* serving broken imports, and one more second of that is not worse than a syscall per turn
|
|
62
|
+
* forever.
|
|
63
|
+
*
|
|
64
|
+
* A NEGATIVE result is never cached — once the tree looks wrong, every later request re-checks,
|
|
65
|
+
* so a repaired install recovers on its own instead of staying refused for a window.
|
|
66
|
+
*/
|
|
67
|
+
const PACKAGE_TREE_RECHECK_MS = 1_000;
|
|
68
|
+
|
|
69
|
+
export function createPackageTreeIntegrityGuard(
|
|
70
|
+
observe: ObservePackageTree = observePackageManifest,
|
|
71
|
+
now: () => number = Date.now,
|
|
72
|
+
): PackageTreeIntegrityGuard {
|
|
73
|
+
const boot = observe();
|
|
74
|
+
let lastOkAt: number | null = null;
|
|
75
|
+
return {
|
|
76
|
+
status(): PackageTreeIntegrityStatus {
|
|
77
|
+
const at = now();
|
|
78
|
+
if (lastOkAt !== null && at - lastOkAt < PACKAGE_TREE_RECHECK_MS) return { ok: true };
|
|
79
|
+
const current = observe();
|
|
80
|
+
if (boot === null || current === null) return { ok: false, reason: "package_tree_unreadable" };
|
|
81
|
+
if (!sameObservation(boot, current)) return { ok: false, reason: "package_tree_replaced" };
|
|
82
|
+
lastOkAt = at;
|
|
83
|
+
return { ok: true };
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Installed packages fail closed when their manifest is replaced under a live process.
|
|
90
|
+
* A source checkout is different: editing package.json is ordinary development work, and
|
|
91
|
+
* Bun keeps serving the module snapshot already loaded by this process until the operator
|
|
92
|
+
* chooses to restart. Fencing every request there makes running dev directly impossible.
|
|
93
|
+
*/
|
|
94
|
+
export function createRuntimePackageTreeIntegrityGuard(
|
|
95
|
+
installer: PackageTreeRuntimeInstall,
|
|
96
|
+
observe: ObservePackageTree = observePackageManifest,
|
|
97
|
+
now: () => number = Date.now,
|
|
98
|
+
): PackageTreeIntegrityGuard {
|
|
99
|
+
if (installer === "source") return { status: () => ({ ok: true }) };
|
|
100
|
+
return createPackageTreeIntegrityGuard(observe, now);
|
|
101
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { loadConfig
|
|
2
|
+
import { loadConfig } from "../config";
|
|
3
|
+
import { readRuntimePort } from "../config/process-state";
|
|
3
4
|
import { configuredAdminToken } from "./admin-secrets";
|
|
4
5
|
|
|
5
6
|
export function isProcessAlive(pid: number): boolean {
|
|
@@ -23,3 +23,18 @@ export function loadServiceTokenFromFile(env: Record<string, string | undefined>
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contents of the installed service token file. The launch wrapper always re-exports
|
|
29
|
+
* this file as OPENCODEX_API_AUTH_TOKEN, so doctor and start must inspect it even
|
|
30
|
+
* when the calling shell has no data-plane env var.
|
|
31
|
+
* Returns the token or null — never throws, never logs the value.
|
|
32
|
+
*/
|
|
33
|
+
export function readInstalledServiceToken(): string | null {
|
|
34
|
+
try {
|
|
35
|
+
const token = readFileSync(serviceApiTokenFilePath(), "utf8").trim();
|
|
36
|
+
return token || null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -59,6 +59,35 @@ function declaresString(schema: SchemaNode): boolean {
|
|
|
59
59
|
return Array.isArray(type) && type.includes("string");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Issue #2316: Codex advertises `multi_agent_v1__wait_agent`'s `timeout_ms` as a JSON
|
|
63
|
+
// Schema `number`, but its Rust runtime deserializes the field as `u64` and rejects
|
|
64
|
+
// `120000.0` with "invalid type: floating point `120000.0`, expected u64" before the
|
|
65
|
+
// tool ever runs. The schema lookup is not the problem — the error text comes from
|
|
66
|
+
// Codex's own deserializer, so the call reached it — the problem is that `number`
|
|
67
|
+
// alone never authorizes the integral-float repair below.
|
|
68
|
+
//
|
|
69
|
+
// This allowlist is deliberately one field wide. It names only what has a live
|
|
70
|
+
// reproduction against a real `u64`, because the repair is only unambiguous for a
|
|
71
|
+
// field that cannot legitimately hold a fraction. Generic names (`start`, `end`,
|
|
72
|
+
// `priority`, `port`) would silently rewrite a third-party tool's fractional value,
|
|
73
|
+
// so a new entry needs its own evidence, not a plausible-sounding name.
|
|
74
|
+
//
|
|
75
|
+
// Cursor's sibling `yield_time_ms` (src/adapters/cursor/tool-definitions.ts) is also
|
|
76
|
+
// declared `number`; it is NOT included here because no rejection has been captured
|
|
77
|
+
// against it. It gets its own change when it gets its own reproduction.
|
|
78
|
+
const U64_NUMBER_FIELDS = new Set(["timeout_ms"]);
|
|
79
|
+
|
|
80
|
+
// Issue #2443 / #2451: Codex Desktop's bare `wait` tool has the same schema/runtime
|
|
81
|
+
// split for `yield_time_ms` and `max_tokens`. Scope these names to that bare tool so a
|
|
82
|
+
// third-party or namespaced tool can still use fractional values legitimately.
|
|
83
|
+
// #2448 allowlisted the hyphenated `yield-time_ms`; live Grok 4.6 calls and the
|
|
84
|
+
// advertised wait schema both use the underscore form, so that name is the one
|
|
85
|
+
// with a captured u64 rejection. Cursor still uses the same underscore name on a
|
|
86
|
+
// namespaced tool; the wait-only map keeps that path byte-identical.
|
|
87
|
+
const U64_NUMBER_FIELDS_BY_TOOL = new Map<string, ReadonlySet<string>>([
|
|
88
|
+
["wait", new Set(["yield_time_ms", "max_tokens"])],
|
|
89
|
+
]);
|
|
90
|
+
|
|
62
91
|
/** True when the node accepts a JSON number (`integer` or `number`), so a numeric
|
|
63
92
|
* value is already schema-valid and must not be rewritten into a string. */
|
|
64
93
|
function declaresNumeric(schema: SchemaNode): boolean {
|
|
@@ -118,7 +147,14 @@ interface CoerceResult {
|
|
|
118
147
|
changed: boolean;
|
|
119
148
|
}
|
|
120
149
|
|
|
121
|
-
function coerceValue(
|
|
150
|
+
function coerceValue(
|
|
151
|
+
value: unknown,
|
|
152
|
+
schema: SchemaNode | undefined,
|
|
153
|
+
root: SchemaNode,
|
|
154
|
+
depth: number,
|
|
155
|
+
toolName?: string,
|
|
156
|
+
propertyName?: string,
|
|
157
|
+
): CoerceResult {
|
|
122
158
|
// A hostile or deeply nested schema must not blow the stack.
|
|
123
159
|
if (depth > 64) return { value, changed: false };
|
|
124
160
|
const resolved = schema ? resolveRef(schema, root, new Set()) : undefined;
|
|
@@ -126,7 +162,19 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
126
162
|
if (typeof value === "number") {
|
|
127
163
|
if (!resolved) return { value, changed: false };
|
|
128
164
|
const branches = compositionBranches(resolved);
|
|
129
|
-
|
|
165
|
+
// #2316: a known Codex-native u64 field counts as integer-declared even when the
|
|
166
|
+
// advertised schema says `number`, but only when the field really is numeric —
|
|
167
|
+
// an allowlisted name over a string-typed field is a disagreement, not a repair.
|
|
168
|
+
const nativeU64Field = propertyName !== undefined
|
|
169
|
+
&& (
|
|
170
|
+
U64_NUMBER_FIELDS.has(propertyName)
|
|
171
|
+
|| U64_NUMBER_FIELDS_BY_TOOL.get(toolName ?? "")?.has(propertyName) === true
|
|
172
|
+
);
|
|
173
|
+
const nativeU64Declared = nativeU64Field
|
|
174
|
+
&& (declaresNumeric(resolved) || branches.some(declaresNumeric));
|
|
175
|
+
const integerDeclared = declaresInteger(resolved)
|
|
176
|
+
|| branches.some(declaresInteger)
|
|
177
|
+
|| nativeU64Declared;
|
|
130
178
|
if (!integerDeclared && safelyIntegral(value)) {
|
|
131
179
|
// Issue #1938: a bare integer in a string-only field has exactly one faithful
|
|
132
180
|
// string reading. A field that also accepts a numeric type keeps the number.
|
|
@@ -148,7 +196,9 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
148
196
|
const itemSchema = resolved ? asSchema(resolved.items) : undefined;
|
|
149
197
|
let changed = false;
|
|
150
198
|
const next = value.map(entry => {
|
|
151
|
-
|
|
199
|
+
// Array items have no property name of their own; passing the array's key would
|
|
200
|
+
// let `timeout_ms: [1.5]` inherit the allowlist. Items are judged by schema only.
|
|
201
|
+
const result = coerceValue(entry, itemSchema, root, depth + 1, toolName);
|
|
152
202
|
if (result.changed) changed = true;
|
|
153
203
|
return result.value;
|
|
154
204
|
});
|
|
@@ -164,7 +214,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
164
214
|
const next: Record<string, unknown> = {};
|
|
165
215
|
for (const [key, entry] of Object.entries(object)) {
|
|
166
216
|
const childSchema = asSchema(properties?.[key]) ?? additional;
|
|
167
|
-
const result = coerceValue(entry, childSchema, root, depth + 1);
|
|
217
|
+
const result = coerceValue(entry, childSchema, root, depth + 1, toolName, key);
|
|
168
218
|
if (result.changed) changed = true;
|
|
169
219
|
next[key] = result.value;
|
|
170
220
|
}
|
|
@@ -182,6 +232,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
182
232
|
export function coerceIntegerToolArguments(
|
|
183
233
|
args: string,
|
|
184
234
|
parameters: Record<string, unknown> | undefined,
|
|
235
|
+
toolName?: string,
|
|
185
236
|
): string {
|
|
186
237
|
if (!parameters || !args) return args;
|
|
187
238
|
// Cheap reject: a payload with no digit cannot need either repair (integral-float
|
|
@@ -196,7 +247,7 @@ export function coerceIntegerToolArguments(
|
|
|
196
247
|
return args;
|
|
197
248
|
}
|
|
198
249
|
const root = parameters as SchemaNode;
|
|
199
|
-
const result = coerceValue(parsed, root, root, 0);
|
|
250
|
+
const result = coerceValue(parsed, root, root, 0, toolName);
|
|
200
251
|
if (!result.changed) return args;
|
|
201
252
|
return JSON.stringify(result.value);
|
|
202
253
|
}
|
|
@@ -276,10 +276,30 @@ export function parseCallbackInput(input: string): { kind: "url" | "query" | "ra
|
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
278
|
const url = new URL(value);
|
|
279
|
+
// Also read the fragment. No provider configured here returns one — every
|
|
280
|
+
// OAuthCallbackFlow asks for response_type=code without response_mode, so
|
|
281
|
+
// the parameters land in the query — but a full URL whose parameters sit in
|
|
282
|
+
// the hash parses as a valid URL with no code, and reading only the query
|
|
283
|
+
// rejects it as "no authorization code found in input". This is the one
|
|
284
|
+
// place a fragment-returning provider would land, and the raw branch below
|
|
285
|
+
// already understands `code#state`.
|
|
286
|
+
//
|
|
287
|
+
// The query wins as a WHOLE when it carries the response, and the two
|
|
288
|
+
// fields are never mixed across collections. Reading `code` and `state`
|
|
289
|
+
// independently would accept `?state=<expected>#code=<other>` — one
|
|
290
|
+
// response assembled from two sources — which is exactly the confusion a
|
|
291
|
+
// state check exists to prevent. An authorization response arrives in one
|
|
292
|
+
// place; treat it that way.
|
|
293
|
+
//
|
|
294
|
+
// Only `code` and `state` are read — never a token. This repo does not
|
|
295
|
+
// implement the implicit grant and a paste field must not become the place
|
|
296
|
+
// it appears.
|
|
297
|
+
const fragment = new URLSearchParams(url.hash.replace(/^#/, ""));
|
|
298
|
+
const source = url.searchParams.has("code") ? url.searchParams : fragment;
|
|
279
299
|
return {
|
|
280
300
|
kind: "url",
|
|
281
|
-
code:
|
|
282
|
-
state:
|
|
301
|
+
code: source.get("code") ?? undefined,
|
|
302
|
+
state: source.get("state") ?? undefined,
|
|
283
303
|
};
|
|
284
304
|
} catch {
|
|
285
305
|
// Not a URL - check for query string format
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cline OAuth — automatic session import from local Cline CLI / VS Code extension
|
|
3
|
+
* and background WorkOS token refresh.
|
|
4
|
+
*
|
|
5
|
+
* Security: tokens are read securely from local config files and refreshed directly
|
|
6
|
+
* against WorkOS API (api.workos.com). Tokens and credentials are never logged.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { clineConfigPath } from "../clients/config-export";
|
|
10
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
11
|
+
|
|
12
|
+
export const WORKOS_CLIENT_ID = "client_01K3A541FN8TA3EPPHTD2325AR";
|
|
13
|
+
export const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
|
|
14
|
+
const WORKOS_TOKEN_PREFIX = "workos:";
|
|
15
|
+
|
|
16
|
+
export interface ClineLoginOptions {
|
|
17
|
+
importLocal?: "fallback" | "off";
|
|
18
|
+
forceLogin?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ClineProvidersJson {
|
|
22
|
+
providers?: {
|
|
23
|
+
cline?: {
|
|
24
|
+
settings?: {
|
|
25
|
+
provider?: string;
|
|
26
|
+
auth?: {
|
|
27
|
+
accessToken?: unknown;
|
|
28
|
+
refreshToken?: unknown;
|
|
29
|
+
expiresAt?: unknown;
|
|
30
|
+
accountId?: unknown;
|
|
31
|
+
metadata?: {
|
|
32
|
+
userInfo?: {
|
|
33
|
+
email?: unknown;
|
|
34
|
+
name?: unknown;
|
|
35
|
+
subject?: unknown;
|
|
36
|
+
clineUserId?: unknown;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface WorkOSAuthResponse {
|
|
46
|
+
access_token?: unknown;
|
|
47
|
+
refresh_token?: unknown;
|
|
48
|
+
user?: {
|
|
49
|
+
id?: unknown;
|
|
50
|
+
external_id?: unknown;
|
|
51
|
+
email?: unknown;
|
|
52
|
+
first_name?: unknown;
|
|
53
|
+
last_name?: unknown;
|
|
54
|
+
};
|
|
55
|
+
error?: unknown;
|
|
56
|
+
error_description?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function decodeJwtExpiry(token: string): number {
|
|
60
|
+
try {
|
|
61
|
+
const rawJwt = token.startsWith(WORKOS_TOKEN_PREFIX) ? token.slice(WORKOS_TOKEN_PREFIX.length) : token;
|
|
62
|
+
const parts = rawJwt.split(".");
|
|
63
|
+
if (parts.length === 3 && parts[1]) {
|
|
64
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8")) as { exp?: unknown };
|
|
65
|
+
if (typeof payload.exp === "number" && Number.isFinite(payload.exp)) {
|
|
66
|
+
return payload.exp * 1000;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// fallback below
|
|
71
|
+
}
|
|
72
|
+
return Date.now() + 3600_000;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatClineAccessToken(token: string): string {
|
|
76
|
+
return token.startsWith(WORKOS_TOKEN_PREFIX) ? token : `${WORKOS_TOKEN_PREFIX}${token}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function detectLocalClineToken(): OAuthCredentials | null {
|
|
80
|
+
const configPath = clineConfigPath();
|
|
81
|
+
if (!existsSync(configPath)) return null;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8")) as ClineProvidersJson;
|
|
85
|
+
const auth = raw.providers?.cline?.settings?.auth;
|
|
86
|
+
if (!auth || typeof auth.accessToken !== "string" || typeof auth.refreshToken !== "string") {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const accessToken = auth.accessToken.trim();
|
|
91
|
+
const refreshToken = auth.refreshToken.trim();
|
|
92
|
+
if (!accessToken || !refreshToken) return null;
|
|
93
|
+
|
|
94
|
+
const expiresAt = typeof auth.expiresAt === "number" && Number.isFinite(auth.expiresAt)
|
|
95
|
+
? auth.expiresAt
|
|
96
|
+
: decodeJwtExpiry(accessToken);
|
|
97
|
+
|
|
98
|
+
const userInfo = auth.metadata?.userInfo;
|
|
99
|
+
const accountId = typeof auth.accountId === "string" && auth.accountId.length > 0
|
|
100
|
+
? auth.accountId
|
|
101
|
+
: typeof userInfo?.clineUserId === "string" && userInfo.clineUserId.length > 0
|
|
102
|
+
? userInfo.clineUserId
|
|
103
|
+
: typeof userInfo?.subject === "string" && userInfo.subject.length > 0
|
|
104
|
+
? userInfo.subject
|
|
105
|
+
: undefined;
|
|
106
|
+
|
|
107
|
+
const email = typeof userInfo?.email === "string" && userInfo.email.length > 0
|
|
108
|
+
? userInfo.email
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
access: formatClineAccessToken(accessToken),
|
|
113
|
+
refresh: refreshToken,
|
|
114
|
+
expires: expiresAt,
|
|
115
|
+
...(accountId ? { accountId } : {}),
|
|
116
|
+
...(email ? { email } : {}),
|
|
117
|
+
source: "local-cli",
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function refreshClineToken(
|
|
125
|
+
refreshToken: string,
|
|
126
|
+
signal?: AbortSignal,
|
|
127
|
+
credential?: OAuthCredentials,
|
|
128
|
+
): Promise<OAuthCredentials> {
|
|
129
|
+
if (signal?.aborted) {
|
|
130
|
+
throw signal.reason ?? new DOMException("Cline token refresh aborted", "AbortError");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const response = await fetch(WORKOS_AUTH_URL, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
Accept: "application/json",
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
client_id: WORKOS_CLIENT_ID,
|
|
141
|
+
grant_type: "refresh_token",
|
|
142
|
+
refresh_token: refreshToken,
|
|
143
|
+
}),
|
|
144
|
+
signal,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const body = await response.text().catch(() => "");
|
|
149
|
+
throw new Error(`Cline WorkOS token refresh failed (HTTP ${response.status}): ${body || response.statusText}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const data = (await response.json()) as WorkOSAuthResponse;
|
|
153
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
154
|
+
throw new Error(`Cline token refresh returned no access_token: ${JSON.stringify(data)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const nextRefreshToken = typeof data.refresh_token === "string" && data.refresh_token
|
|
158
|
+
? data.refresh_token
|
|
159
|
+
: refreshToken;
|
|
160
|
+
|
|
161
|
+
const access = formatClineAccessToken(data.access_token);
|
|
162
|
+
const expires = decodeJwtExpiry(data.access_token);
|
|
163
|
+
|
|
164
|
+
const accountId = typeof data.user?.external_id === "string" && data.user.external_id
|
|
165
|
+
? data.user.external_id
|
|
166
|
+
: typeof data.user?.id === "string" && data.user.id
|
|
167
|
+
? data.user.id
|
|
168
|
+
: credential?.accountId;
|
|
169
|
+
|
|
170
|
+
const email = typeof data.user?.email === "string" && data.user.email
|
|
171
|
+
? data.user.email
|
|
172
|
+
: credential?.email;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
access,
|
|
176
|
+
refresh: nextRefreshToken,
|
|
177
|
+
expires,
|
|
178
|
+
...(accountId ? { accountId } : {}),
|
|
179
|
+
...(email ? { email } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function loginCline(
|
|
184
|
+
ctrl?: OAuthController,
|
|
185
|
+
options: ClineLoginOptions = {},
|
|
186
|
+
): Promise<OAuthCredentials> {
|
|
187
|
+
if (options.importLocal !== "off") {
|
|
188
|
+
ctrl?.onProgress?.("Checking local Cline CLI and VS Code installation for active session...");
|
|
189
|
+
const local = detectLocalClineToken();
|
|
190
|
+
if (local) {
|
|
191
|
+
if (local.expires <= Date.now() + 60_000) {
|
|
192
|
+
ctrl?.onProgress?.("Local Cline session token expired; refreshing via WorkOS...");
|
|
193
|
+
return await refreshClineToken(local.refresh, ctrl?.signal, local);
|
|
194
|
+
}
|
|
195
|
+
ctrl?.onProgress?.("Imported active Cline session successfully.");
|
|
196
|
+
return local;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error(
|
|
201
|
+
"No local Cline session found. Please log in to Cline via the VS Code extension or Cline CLI (`cline`), then re-run `ocx login cline` to import your account.",
|
|
202
|
+
);
|
|
203
|
+
}
|