@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913
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/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
package/src/images/loop.ts
CHANGED
|
@@ -24,7 +24,9 @@ import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
|
24
24
|
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
25
25
|
import { rateLimitRetryDelayMs } from "../providers/key-failover";
|
|
26
26
|
import {
|
|
27
|
+
createTranslatorBudget,
|
|
27
28
|
isTranslatorBudgetExceededError,
|
|
29
|
+
TRANSLATOR_MAX_CALL_ARGUMENT_BYTES,
|
|
28
30
|
TRANSLATOR_MAX_TURN_BYTES,
|
|
29
31
|
TranslatorBudgetExceededError,
|
|
30
32
|
} from "../lib/translator-budget";
|
|
@@ -101,6 +103,57 @@ interface ImageCall {
|
|
|
101
103
|
providerMetadata?: OcxProviderOpaqueToolCallMetadata;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/** Independent retention owner: adapter leases and final SSE buffers have separate lifetimes. */
|
|
107
|
+
function createIterationEventBudget() {
|
|
108
|
+
const budget = createTranslatorBudget();
|
|
109
|
+
let firstEvent = true;
|
|
110
|
+
let argumentBytes: number | undefined;
|
|
111
|
+
let trailingHighSurrogate = false;
|
|
112
|
+
return {
|
|
113
|
+
retain(event: AdapterEvent, coalescedIntoTail = false): void {
|
|
114
|
+
// Heartbeats are never retained or passed to scanEventsForImageCall.
|
|
115
|
+
if (event.type === "heartbeat") return;
|
|
116
|
+
if (event.type === "tool_call_start") {
|
|
117
|
+
argumentBytes = 0;
|
|
118
|
+
trailingHighSurrogate = false;
|
|
119
|
+
} else if (event.type === "tool_call_delta" && argumentBytes !== undefined) {
|
|
120
|
+
const chunk = event.arguments;
|
|
121
|
+
argumentBytes += Buffer.byteLength(chunk);
|
|
122
|
+
// A surrogate pair may straddle adapter deltas; count the concatenated UTF-8 string.
|
|
123
|
+
if (trailingHighSurrogate && /^[\uDC00-\uDFFF]/.test(chunk)) argumentBytes -= 2;
|
|
124
|
+
if (chunk.length > 0) trailingHighSurrogate = /[\uD800-\uDBFF]$/.test(chunk);
|
|
125
|
+
if (argumentBytes > TRANSLATOR_MAX_CALL_ARGUMENT_BYTES) {
|
|
126
|
+
throw new TranslatorBudgetExceededError("tool_args", TRANSLATOR_MAX_CALL_ARGUMENT_BYTES);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
argumentBytes = undefined;
|
|
130
|
+
trailingHighSurrogate = false;
|
|
131
|
+
}
|
|
132
|
+
// A delta the queue merged into its buffered tail leaves one object behind, not two,
|
|
133
|
+
// so it costs the appended payload rather than another envelope. Charging the whole
|
|
134
|
+
// event here would bill over 32 MiB for the ~1 MiB that a million one-character text
|
|
135
|
+
// deltas actually retain, and abort a turn far below the documented limit.
|
|
136
|
+
const appended = coalescedIntoTail
|
|
137
|
+
? event.type === "text_delta" ? event.text : event.type === "thinking_delta" ? event.thinking : undefined
|
|
138
|
+
: undefined;
|
|
139
|
+
if (appended !== undefined) {
|
|
140
|
+
// JSON escaping is per character, so the tail grows by the quoted form minus its
|
|
141
|
+
// quotes. A surrogate pair split across two deltas is the one over-count, by eight
|
|
142
|
+
// bytes, which bounds memory conservatively and never under-charges.
|
|
143
|
+
budget.chargeRetained(Buffer.byteLength(JSON.stringify(appended)) - 2, {
|
|
144
|
+
kind: "retained_collectors",
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
budget.chargeRetained(Buffer.byteLength(JSON.stringify(event)) + (firstEvent ? 2 : 1), {
|
|
149
|
+
kind: "retained_collectors",
|
|
150
|
+
});
|
|
151
|
+
firstEvent = false;
|
|
152
|
+
},
|
|
153
|
+
dispose: () => budget.dispose(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
/**
|
|
105
158
|
* Split an iteration's adapter events into (a) the image-generation tool calls to intercept and
|
|
106
159
|
* (b) the events to pass through to Codex. An image tool-call's own start/delta/end events are
|
|
@@ -370,6 +423,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
370
423
|
interface IterationResponse {
|
|
371
424
|
response: Response;
|
|
372
425
|
responseAdapter: ProviderAdapter;
|
|
426
|
+
/** runTurn events are already bounded; consume them without replaying or charging twice. */
|
|
427
|
+
collectedEvents?: AdapterEvent[];
|
|
373
428
|
}
|
|
374
429
|
type IterationSplit = ReturnType<typeof scanEventsForImageCall>;
|
|
375
430
|
|
|
@@ -397,29 +452,31 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
397
452
|
|
|
398
453
|
// runTurn adapters (Cursor) own all upstream communication via an emit callback. They don't
|
|
399
454
|
// expose buildRequest/fetchResponse/parseStream to the bridge, so collect their events through
|
|
400
|
-
// an AdapterEventQueue and
|
|
455
|
+
// an AdapterEventQueue and pass the bounded collection directly to the common scanner.
|
|
401
456
|
if (adapter.runTurn) {
|
|
402
457
|
await deps.waitForRequestSlot?.(signal);
|
|
403
458
|
const queue = createAdapterEventQueue({
|
|
404
459
|
onBacklogExceeded: () => internalAbort.abort("runTurn backlog exceeded"),
|
|
405
460
|
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
461
|
+
const iterationBudget = createIterationEventBudget();
|
|
462
|
+
let accepting = true;
|
|
463
|
+
let collectionError: unknown;
|
|
464
|
+
const closeOnAbort = (): void => { accepting = false; queue.close(); };
|
|
465
|
+
signal.addEventListener("abort", closeOnAbort, { once: true });
|
|
466
|
+
const emit = (event: AdapterEvent): void => {
|
|
467
|
+
if (!accepting || signal.aborted) return;
|
|
468
|
+
try {
|
|
469
|
+
// Check at emission, before even a synchronous producer can fill the queue. push
|
|
470
|
+
// reports whether it merged this delta into the buffered tail, which is what the
|
|
471
|
+
// iteration actually retains once the consumer drains it.
|
|
472
|
+
iterationBudget.retain(event, queue.push(event));
|
|
473
|
+
} catch (error) {
|
|
474
|
+
collectionError = error;
|
|
475
|
+
closeOnAbort();
|
|
476
|
+
internalAbort.abort(error);
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
423
480
|
|
|
424
481
|
// Bound collect with a real *idle* deadline that resets on each emitted event.
|
|
425
482
|
// A fixed wall-clock race would abort legitimate long Cursor turns that keep
|
|
@@ -439,14 +496,34 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
439
496
|
});
|
|
440
497
|
const events: AdapterEvent[] = [];
|
|
441
498
|
try {
|
|
499
|
+
// Attempt telemetry must fire at dispatch time, not after collection.
|
|
500
|
+
deps.onAttemptSend?.();
|
|
501
|
+
void adapter.runTurn(iterParsed, {
|
|
502
|
+
headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
|
|
503
|
+
abortSignal: signal,
|
|
504
|
+
translatorBudget,
|
|
505
|
+
}, emit).then(closeOnAbort).catch(err => {
|
|
506
|
+
if (accepting) {
|
|
507
|
+
collectionError = err;
|
|
508
|
+
if (isTranslatorBudgetExceededError(err)) internalAbort.abort(err);
|
|
509
|
+
}
|
|
510
|
+
closeOnAbort();
|
|
511
|
+
});
|
|
442
512
|
idle.reset();
|
|
443
513
|
for await (const event of queue.stream()) {
|
|
444
514
|
if (timedOut) break;
|
|
445
515
|
idle.reset();
|
|
446
|
-
events.push(event);
|
|
516
|
+
if (event.type !== "heartbeat") events.push(event);
|
|
447
517
|
}
|
|
448
518
|
} finally {
|
|
519
|
+
accepting = false;
|
|
449
520
|
idle.cancel();
|
|
521
|
+
signal.removeEventListener("abort", closeOnAbort);
|
|
522
|
+
iterationBudget.dispose();
|
|
523
|
+
}
|
|
524
|
+
if (collectionError) {
|
|
525
|
+
if (isTranslatorBudgetExceededError(collectionError)) throw collectionError;
|
|
526
|
+
throw new LoopError(502, collectionError instanceof Error ? collectionError.message : String(collectionError));
|
|
450
527
|
}
|
|
451
528
|
if (timedOut) {
|
|
452
529
|
throw new LoopError(504, `runTurn inactivity timeout after ${stallTimeoutMs}ms during image-bridge`);
|
|
@@ -466,14 +543,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
466
543
|
}
|
|
467
544
|
throw new LoopError(502, errorEvent.message);
|
|
468
545
|
}
|
|
546
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
|
|
469
547
|
|
|
470
|
-
|
|
471
|
-
...adapter,
|
|
472
|
-
async *parseStream() {
|
|
473
|
-
for (const e of events) yield e;
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
return { response: new Response(new Uint8Array(0), { status: 200 }), responseAdapter: wrappedAdapter };
|
|
548
|
+
return { response: new Response(new Uint8Array(0), { status: 200 }), responseAdapter: adapter, collectedEvents: events };
|
|
477
549
|
}
|
|
478
550
|
|
|
479
551
|
let headerDeadline = clearableDeadline(connectTimeoutMs, signal);
|
|
@@ -643,23 +715,34 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
643
715
|
// Consume and validate one successful response body. Only invisible heartbeat events escape while
|
|
644
716
|
// semantic output remains buffered for safe scanning.
|
|
645
717
|
const consumeIterationEvents = async function* (prepared: IterationResponse): AsyncGenerator<AdapterEvent, IterationSplit> {
|
|
646
|
-
const events: AdapterEvent[] = [];
|
|
718
|
+
const events: AdapterEvent[] = prepared.collectedEvents ?? [];
|
|
719
|
+
const iterationBudget = prepared.collectedEvents ? undefined : createIterationEventBudget();
|
|
647
720
|
try {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
721
|
+
if (iterationBudget) {
|
|
722
|
+
const parse = prepared.responseAdapter.parseStream.bind(prepared.responseAdapter);
|
|
723
|
+
for await (const event of parseStreamWithProgress(prepared.response, parse, {
|
|
724
|
+
signal,
|
|
725
|
+
inactivityTimeoutMs: stallTimeoutMs,
|
|
726
|
+
translatorBudget,
|
|
727
|
+
})) {
|
|
728
|
+
if (event.type === "heartbeat") yield event;
|
|
729
|
+
else {
|
|
730
|
+
iterationBudget.retain(event);
|
|
731
|
+
events.push(event);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
656
734
|
}
|
|
657
735
|
} catch (error) {
|
|
658
|
-
if (isTranslatorBudgetExceededError(error))
|
|
736
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
737
|
+
internalAbort.abort(error);
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
659
740
|
if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
|
|
660
741
|
if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
|
|
661
742
|
if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
|
|
662
743
|
throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
744
|
+
} finally {
|
|
745
|
+
iterationBudget?.dispose();
|
|
663
746
|
}
|
|
664
747
|
|
|
665
748
|
const terminalIndexes = events.flatMap((event, index) =>
|
package/src/lib/admission.ts
CHANGED
|
@@ -58,20 +58,26 @@ export function createAdmissionGate(name: string, limit: number): {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function retainedUtf8Bytes(value: string): number {
|
|
61
|
-
|
|
61
|
+
// Keep TextEncoder's runtime coercion for legacy callers outside the string-typed contract.
|
|
62
|
+
// Template coercion rejects Symbols; String(value) would silently accept them.
|
|
63
|
+
return Buffer.byteLength(typeof value === "string" ? value : value === undefined ? "" : `${value}`, "utf8");
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function utf8Prefix(value: string, maxBytes: number): string {
|
|
65
67
|
if (maxBytes <= 0) return "";
|
|
66
68
|
let bytes = 0;
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
const
|
|
69
|
+
let end = 0;
|
|
70
|
+
while (end < value.length) {
|
|
71
|
+
const code = value.charCodeAt(end);
|
|
72
|
+
const next = value.charCodeAt(end + 1);
|
|
73
|
+
const pair = code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff;
|
|
74
|
+
// An unpaired surrogate encodes as a three-byte replacement, like TextEncoder.
|
|
75
|
+
const size = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : pair ? 4 : 3;
|
|
70
76
|
if (bytes + size > maxBytes) break;
|
|
71
|
-
result += character;
|
|
72
77
|
bytes += size;
|
|
78
|
+
end += pair ? 2 : 1;
|
|
73
79
|
}
|
|
74
|
-
return
|
|
80
|
+
return value.slice(0, end);
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
export function truncateRetainedUtf8(value: string, maxBytes: number): string {
|
package/src/lib/redact.ts
CHANGED
|
@@ -252,6 +252,13 @@ const SECRET_VALUE_PATTERNS: Array<[RegExp, string]> = [
|
|
|
252
252
|
[/((?:"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$3`],
|
|
253
253
|
// Raw JSON "token" field values (Copilot token exchange bodies echo the credential here).
|
|
254
254
|
[/(("token"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$4`],
|
|
255
|
+
// Cognition/Devin session keys, and the bare JWTs several providers hand out.
|
|
256
|
+
// A Connect EOS trailer can quote the request that carried the key, and the
|
|
257
|
+
// rules above only fire on a label — `Bearer`, `api_key=`, `"token":` — which
|
|
258
|
+
// a quoted proto field does not have. `eyJ` is the base64url of `{"`, so the
|
|
259
|
+
// JWT rule needs a real three-segment shape and does not match ordinary prose.
|
|
260
|
+
[/\bdevin-session-token\$[A-Za-z0-9._~+/=-]{8,}/g, REDACTED_SECRET],
|
|
261
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g, REDACTED_SECRET],
|
|
255
262
|
[/\b(arn:aws:[A-Za-z0-9_-]+:[A-Za-z0-9-]*:\d{12}:[A-Za-z0-9_/:+=,.@-]+)\b/g, REDACTED_SECRET],
|
|
256
263
|
];
|
|
257
264
|
|
|
@@ -112,14 +112,15 @@ export function retainTranslatedEvent<T extends object>(
|
|
|
112
112
|
*/
|
|
113
113
|
export function retainTranslatedEventBatch<T extends object>(events: T[], budget: TranslatorBudget): void {
|
|
114
114
|
if (events.length === 0) return;
|
|
115
|
-
|
|
116
|
-
const
|
|
115
|
+
// Preserve atomic batch admission without retaining serialized strings or joining a second copy.
|
|
116
|
+
const eventBytes = events.map(event => Buffer.byteLength(JSON.stringify(event)));
|
|
117
|
+
const totalBytes = eventBytes.reduce((total, bytes) => total + bytes, events.length + 1);
|
|
117
118
|
budget.chargeRetained(totalBytes, { kind: "retained_collectors" });
|
|
118
119
|
for (let index = 0; index < events.length; index++) {
|
|
119
120
|
const delimiterBytes = index === events.length - 1 ? 2 : 1;
|
|
120
121
|
retainedEventOwnership.set(events[index]!, {
|
|
121
122
|
budget,
|
|
122
|
-
bytes:
|
|
123
|
+
bytes: eventBytes[index]! + delimiterBytes,
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
}
|
|
@@ -397,7 +397,7 @@ export function formatWindowsSchtasksError(error: unknown, args: string[]): stri
|
|
|
397
397
|
const guidance = [
|
|
398
398
|
"Windows access denied while running Task Scheduler.",
|
|
399
399
|
`Command: schtasks ${argsText}`,
|
|
400
|
-
"
|
|
400
|
+
"The OpenCodex task definition is scoped to the installing account and normally registers without elevation, so this denial is not fixed by approving a UAC prompt. Check `ocx service status`: if an existing `opencodex-proxy` task belongs to a different account, remove it from that account and retry `ocx service install`.",
|
|
401
401
|
].join(" ");
|
|
402
402
|
if (operation === "create" && ownedCreateAccessDenied) {
|
|
403
403
|
return `${guidance}\n${WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER}`;
|
|
@@ -23,6 +23,19 @@ export const DEVICE_VERIFICATION_URL = "https://auth.openai.com/codex/device";
|
|
|
23
23
|
|
|
24
24
|
/** The grant's own lifetime. Polling past this only produces a worse error message. */
|
|
25
25
|
const DEVICE_FLOW_TTL_MS = 15 * 60 * 1000;
|
|
26
|
+
/**
|
|
27
|
+
* Per-fetch deadline for every device-flow HTTP call (#3898). Until this
|
|
28
|
+
* existed the only bounds were the 15-minute grant TTL and the caller's
|
|
29
|
+
* abort, so one stuck TCP connection could hold the login slot for the whole
|
|
30
|
+
* grant. A FRESH timeout per fetch attempt is required — a single timeout
|
|
31
|
+
* shared across the poll loop would kill the 15-minute grant.
|
|
32
|
+
*/
|
|
33
|
+
export const DEVICE_FETCH_TIMEOUT_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
function deviceFetchSignal(signal?: AbortSignal): AbortSignal {
|
|
36
|
+
const timeout = AbortSignal.timeout(DEVICE_FETCH_TIMEOUT_MS);
|
|
37
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
38
|
+
}
|
|
26
39
|
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
27
40
|
const MIN_POLL_INTERVAL_MS = 1_000;
|
|
28
41
|
/**
|
|
@@ -86,7 +99,7 @@ async function requestUserCode(signal?: AbortSignal): Promise<DeviceUserCode> {
|
|
|
86
99
|
method: "POST",
|
|
87
100
|
headers: { "Content-Type": "application/json" },
|
|
88
101
|
body: JSON.stringify({ client_id: CHATGPT_CLIENT_ID }),
|
|
89
|
-
signal,
|
|
102
|
+
signal: deviceFetchSignal(signal),
|
|
90
103
|
});
|
|
91
104
|
if (!response.ok) throw deviceError("request", response.status);
|
|
92
105
|
const payload = (await response.json()) as Record<string, unknown>;
|
|
@@ -123,7 +136,7 @@ async function pollForGrant(
|
|
|
123
136
|
method: "POST",
|
|
124
137
|
headers: { "Content-Type": "application/json" },
|
|
125
138
|
body: JSON.stringify({ device_auth_id: device.deviceAuthId, user_code: device.userCode }),
|
|
126
|
-
signal,
|
|
139
|
+
signal: deviceFetchSignal(signal),
|
|
127
140
|
});
|
|
128
141
|
if (response.status === 403 || response.status === 404) {
|
|
129
142
|
// Cap the wait at the time actually left. Sleeping a full interval past
|
|
@@ -149,7 +162,7 @@ async function pollForGrant(
|
|
|
149
162
|
throw new Error("ChatGPT device authorization expired");
|
|
150
163
|
}
|
|
151
164
|
|
|
152
|
-
async function
|
|
165
|
+
async function exchangeGrantRaw(grant: DeviceGrant, signal?: AbortSignal): Promise<Record<string, unknown>> {
|
|
153
166
|
const response = await fetch(CHATGPT_TOKEN_URL, {
|
|
154
167
|
method: "POST",
|
|
155
168
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -160,10 +173,54 @@ async function exchangeGrant(grant: DeviceGrant, signal?: AbortSignal): Promise<
|
|
|
160
173
|
code_verifier: grant.codeVerifier,
|
|
161
174
|
redirect_uri: DEVICE_REDIRECT_URI,
|
|
162
175
|
}).toString(),
|
|
163
|
-
signal,
|
|
176
|
+
signal: deviceFetchSignal(signal),
|
|
164
177
|
});
|
|
165
178
|
if (!response.ok) throw deviceError("token exchange", response.status);
|
|
166
|
-
return
|
|
179
|
+
return (await response.json()) as Record<string, unknown>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function exchangeGrant(grant: DeviceGrant, signal?: AbortSignal): Promise<OAuthCredentials> {
|
|
183
|
+
return credsFromToken(await exchangeGrantRaw(grant, signal));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The native-main reauth result (#3898): the projected credential PLUS the
|
|
188
|
+
* id_token the pool projection deliberately drops. The id_token is the
|
|
189
|
+
* identity document the native auth.json requires
|
|
190
|
+
* (native-profile-store.ts), and it never leaves this process — it is
|
|
191
|
+
* written to the native main slot by the caller, never serialized into a
|
|
192
|
+
* DTO, log, or error.
|
|
193
|
+
*/
|
|
194
|
+
export interface NativeDeviceLogin {
|
|
195
|
+
credential: OAuthCredentials;
|
|
196
|
+
idToken: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function exchangeGrantNative(grant: DeviceGrant, signal?: AbortSignal): Promise<NativeDeviceLogin> {
|
|
200
|
+
const payload = await exchangeGrantRaw(grant, signal);
|
|
201
|
+
const credential = credsFromToken(payload);
|
|
202
|
+
const idToken = nonEmptyString(payload.id_token);
|
|
203
|
+
if (!idToken) throw new Error("ChatGPT device token response missing id_token");
|
|
204
|
+
if (!credential.refresh) throw new Error("ChatGPT device token response missing refresh token");
|
|
205
|
+
if (!credential.accountId) throw new Error("ChatGPT device token response missing account identity");
|
|
206
|
+
return { credential, idToken };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Device flow for the native __main__ slot. Same grant as the pool flow, but
|
|
211
|
+
* nothing is persisted here and no OAuth store is touched: the caller
|
|
212
|
+
* (main-device-reauth service) owns the fenced commit into CODEX_HOME
|
|
213
|
+
* auth.json.
|
|
214
|
+
*/
|
|
215
|
+
export async function loginChatGPTNativeDevice(ctrl: OAuthController): Promise<NativeDeviceLogin> {
|
|
216
|
+
const device = await requestUserCode(ctrl.signal);
|
|
217
|
+
ctrl.onAuth?.({
|
|
218
|
+
url: DEVICE_VERIFICATION_URL,
|
|
219
|
+
instructions: `Enter code: ${device.userCode}`,
|
|
220
|
+
deviceCode: device.userCode,
|
|
221
|
+
});
|
|
222
|
+
const grant = await pollForGrant(device, ctrl.signal);
|
|
223
|
+
return exchangeGrantNative(grant, ctrl.signal);
|
|
167
224
|
}
|
|
168
225
|
|
|
169
226
|
/**
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin CLI credential-file import.
|
|
3
|
+
*
|
|
4
|
+
* The installed CLI writes `credentials.toml` after `devin auth login`, and the
|
|
5
|
+
* `windsurf_api_key` in it is an ordinary `devin-session-token$<JWT>` — the same
|
|
6
|
+
* shape RegisterUser returns for the browser sign-in, and the same one the
|
|
7
|
+
* cloud-direct client already speaks. Measured against a signed-in CLI: it mints
|
|
8
|
+
* a user_jwt, opens the full model catalog, and streams chat.
|
|
9
|
+
*
|
|
10
|
+
* This module is only the file-reading half of that. The login decision —
|
|
11
|
+
* adopt the CLI session when present, fall back to the Auth0 browser flow when
|
|
12
|
+
* the file is absent — lives in `loginDevin` in `../devin.ts`, because the
|
|
13
|
+
* merged `devin` provider owns both paths (`devin-cli` survives only as a
|
|
14
|
+
* deprecated alias for unmigrated configs).
|
|
15
|
+
*
|
|
16
|
+
* The file also carries `devin_webapp_host` and `devin_api_url`, which belong to
|
|
17
|
+
* the Devin *session* product (`cog_` keys, agent VMs) rather than to model
|
|
18
|
+
* inference. Neither is read here.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { posix, win32 } from "node:path";
|
|
23
|
+
|
|
24
|
+
/** Absolute-path override, for a CLI installed somewhere this resolver does not model. */
|
|
25
|
+
export const DEVIN_CLI_CREDENTIALS_ENV = "OPENCODEX_DEVIN_CLI_CREDENTIALS";
|
|
26
|
+
|
|
27
|
+
export interface DevinCliLoginDeps {
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
exists?: (path: string) => boolean;
|
|
31
|
+
read?: (path: string) => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where the CLI keeps its own credential.
|
|
36
|
+
*
|
|
37
|
+
* Measured on a live install: `$XDG_DATA_HOME/devin/credentials.toml`, i.e.
|
|
38
|
+
* `~/.local/share/devin/...`, which `devin auth status` prints. Note this is the
|
|
39
|
+
* DATA dir, not the config dir — an earlier draft guessed `~/.config` and was
|
|
40
|
+
* wrong. The Windows branch mirrors the CLI's own installer.
|
|
41
|
+
*/
|
|
42
|
+
export function devinCliCredentialsPath(
|
|
43
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
+
platform: NodeJS.Platform = process.platform,
|
|
45
|
+
): string {
|
|
46
|
+
const override = env[DEVIN_CLI_CREDENTIALS_ENV]?.trim();
|
|
47
|
+
// Absolute only. A relative override would resolve against whatever directory
|
|
48
|
+
// the proxy happens to be running in, which is not a location a user can mean.
|
|
49
|
+
if (override && (override.startsWith("/") || /^[A-Za-z]:[\\/]/.test(override))) return override;
|
|
50
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
51
|
+
if (platform === "win32") {
|
|
52
|
+
// `??` treats an empty APPDATA as set, and join("", "devin", …) is a path
|
|
53
|
+
// relative to whatever directory the proxy was started in — so a file planted
|
|
54
|
+
// there would import as the operator's own CLI session. An empty or
|
|
55
|
+
// whitespace-only value is an absent value.
|
|
56
|
+
const appData = env.APPDATA?.trim() || paths.join(homedir(), "AppData", "Roaming");
|
|
57
|
+
return paths.join(appData, "devin", "credentials.toml");
|
|
58
|
+
}
|
|
59
|
+
const dataHome = env.XDG_DATA_HOME?.trim() || paths.join(homedir(), ".local", "share");
|
|
60
|
+
return paths.join(dataHome, "devin", "credentials.toml");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DevinCliCredentialFile {
|
|
64
|
+
apiKey: string;
|
|
65
|
+
apiServerUrl: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Upper bound on the credential file we are willing to parse.
|
|
70
|
+
*
|
|
71
|
+
* The measured file is four short lines. Reading an arbitrarily large file into
|
|
72
|
+
* a string and running two global-ish regexes over it is work we never need to
|
|
73
|
+
* do, and a file this size is not the CLI's.
|
|
74
|
+
*/
|
|
75
|
+
const DEVIN_CLI_CREDENTIALS_MAX_BYTES = 64 * 1024;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Why the import has no credential, for the caller that owns the response.
|
|
79
|
+
*
|
|
80
|
+
* `missing` and `unreadable` used to collapse into the same `undefined`, so a
|
|
81
|
+
* permission error on an existing file was reported as "not signed in" and sent
|
|
82
|
+
* the operator to `devin auth login`, which does not fix it. The distinction
|
|
83
|
+
* matters even more now that `missing` is the browser-fallback branch: only an
|
|
84
|
+
* absent file may fall through.
|
|
85
|
+
*/
|
|
86
|
+
export type DevinCliCredentialOutcome =
|
|
87
|
+
| { kind: "ok"; file: DevinCliCredentialFile }
|
|
88
|
+
| { kind: "missing" }
|
|
89
|
+
| { kind: "unreadable" }
|
|
90
|
+
| { kind: "incomplete" };
|
|
91
|
+
|
|
92
|
+
export function readDevinCliCredentialOutcome(deps: DevinCliLoginDeps = {}): DevinCliCredentialOutcome {
|
|
93
|
+
const path = devinCliCredentialsPath(deps.env, deps.platform);
|
|
94
|
+
const exists = deps.exists ?? existsSync;
|
|
95
|
+
if (!exists(path)) return { kind: "missing" };
|
|
96
|
+
let raw: string;
|
|
97
|
+
try {
|
|
98
|
+
raw = (deps.read ?? ((p: string) => readFileSync(p, "utf8")))(path);
|
|
99
|
+
} catch {
|
|
100
|
+
// Nothing from the error is repeated: it carries the path, and an EACCES
|
|
101
|
+
// message is not worth the risk of echoing anything read off disk.
|
|
102
|
+
return { kind: "unreadable" };
|
|
103
|
+
}
|
|
104
|
+
if (raw.length > DEVIN_CLI_CREDENTIALS_MAX_BYTES) return { kind: "unreadable" };
|
|
105
|
+
const apiKey = raw.match(/^\s*windsurf_api_key\s*=\s*"([^"]+)"/m)?.[1]?.trim();
|
|
106
|
+
const apiServerUrl = raw.match(/^\s*api_server_url\s*=\s*"([^"]+)"/m)?.[1]?.trim();
|
|
107
|
+
if (!apiKey || !apiServerUrl) return { kind: "incomplete" };
|
|
108
|
+
return { kind: "ok", file: { apiKey, apiServerUrl } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read the two keys that matter, and nothing else.
|
|
113
|
+
*
|
|
114
|
+
* The measured file is four flat `key = "value"` lines: no tables, no comments,
|
|
115
|
+
* no single quotes. A line matcher is therefore enough and a TOML dependency is
|
|
116
|
+
* not, and the quoted form is required rather than optional — an unquoted
|
|
117
|
+
* matcher would pass its own fixtures and miss the real file.
|
|
118
|
+
*
|
|
119
|
+
* Returns undefined rather than throwing so the caller owns the outcome.
|
|
120
|
+
* Nothing here ever puts the file's contents into a thrown value.
|
|
121
|
+
*/
|
|
122
|
+
export function readDevinCliCredentialFile(deps: DevinCliLoginDeps = {}): DevinCliCredentialFile | undefined {
|
|
123
|
+
const outcome = readDevinCliCredentialOutcome(deps);
|
|
124
|
+
return outcome.kind === "ok" ? outcome.file : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** True when a signed-in CLI credential is readable. Used for status, never for auth. */
|
|
128
|
+
export function devinCliSignedIn(deps: DevinCliLoginDeps = {}): boolean {
|
|
129
|
+
return readDevinCliCredentialFile(deps) !== undefined;
|
|
130
|
+
}
|
package/src/oauth/devin.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Devin / Cognition OAuth.
|
|
3
3
|
*
|
|
4
|
-
* Login
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Login is import-first: a signed-in Devin CLI has already completed PKCE, so
|
|
5
|
+
* its credentials.toml session is adopted without opening a browser (the kiro
|
|
6
|
+
* shape). Only when no CLI credential exists does login fall back to the Auth0
|
|
7
|
+
* browser sign-in flow (windsurf.com/windsurf/signin with
|
|
8
|
+
* redirect_uri=show-auth-token), which exchanges the pasted Firebase ID token
|
|
9
|
+
* via Cognition's RegisterUser for a long-lived API key. The browser path is
|
|
10
|
+
* kept because it is the only login route for a user without the CLI.
|
|
11
|
+
*
|
|
12
|
+
* `devin-cli` used to be a second provider id owning the import half; it is
|
|
13
|
+
* now a deprecated alias for `devin` (see DEPRECATED_OAUTH_PROVIDER_ALIASES in
|
|
14
|
+
* ./index), and this module is the single login/refresh owner for both.
|
|
7
15
|
*/
|
|
8
16
|
import { randomUUID } from "node:crypto";
|
|
9
17
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
10
18
|
import { DEFAULT_REGION, type WindsurfRegion } from "./devin/types";
|
|
11
19
|
import { registerUser } from "./devin/register-user";
|
|
12
20
|
import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiBaseUrl, validateDevinApiBaseUrl } from "./devin/api-base";
|
|
21
|
+
import { readDevinCliCredentialOutcome } from "./devin/cli-import";
|
|
13
22
|
import { getCredential } from "./store";
|
|
14
23
|
|
|
15
24
|
export { DEVIN_DEFAULT_API_SERVER } from "./devin/api-base";
|
|
@@ -25,10 +34,13 @@ export { DEVIN_DEFAULT_API_SERVER } from "./devin/api-base";
|
|
|
25
34
|
*/
|
|
26
35
|
export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "devin"): string {
|
|
27
36
|
return (
|
|
28
|
-
// Provider-scoped
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
37
|
+
// Provider-scoped, keyed by the configured provider id verbatim. `devin-cli`
|
|
38
|
+
// is a deprecated alias for `devin`, but an unmigrated config row still owns
|
|
39
|
+
// its old credential slot until the startup migration rekeys the row and the
|
|
40
|
+
// slot together — normalizing the id here would read the wrong slot for that
|
|
41
|
+
// window. An EU or FedStart tenant is recorded on the credential rather than
|
|
42
|
+
// in the registry, so a fixed "devin" slot would send the key to the wrong
|
|
43
|
+
// host either way.
|
|
32
44
|
validateDevinApiBaseUrl(getCredential(providerId)?.apiBaseUrl) ??
|
|
33
45
|
validateDevinApiBaseUrl(configuredBaseUrl) ??
|
|
34
46
|
DEVIN_DEFAULT_API_SERVER
|
|
@@ -153,7 +165,50 @@ async function loginDevinBrowser(ctrl: OAuthController, region: WindsurfRegion):
|
|
|
153
165
|
return credentials;
|
|
154
166
|
}
|
|
155
167
|
|
|
156
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Import-first login for the merged `devin` provider.
|
|
170
|
+
*
|
|
171
|
+
* A signed-in CLI already completed PKCE, so its session is adopted directly
|
|
172
|
+
* and no browser opens. Only `missing` falls through to the browser flow:
|
|
173
|
+
* `unreadable` and `incomplete` describe a file that exists but is broken,
|
|
174
|
+
* and a browser sign-in would not repair it, so they throw rather than
|
|
175
|
+
* silently routing around the real problem. `forceLogin` skips the import
|
|
176
|
+
* outright — reauth and add-account must be able to reach a different account
|
|
177
|
+
* than the CLI's, and the management routes already set it for both.
|
|
178
|
+
*/
|
|
179
|
+
export async function loginDevin(
|
|
180
|
+
ctrl: OAuthController,
|
|
181
|
+
opts?: { forceLogin?: boolean },
|
|
182
|
+
): Promise<OAuthCredentials> {
|
|
183
|
+
if (opts?.forceLogin !== true) {
|
|
184
|
+
const outcome = readDevinCliCredentialOutcome();
|
|
185
|
+
// No branch names path contents or parsed values. A Connect error can echo
|
|
186
|
+
// a request, and redactSecretString does not recognise a bare JWT or a
|
|
187
|
+
// devin-session-token — the same caution register-user.ts applies to error
|
|
188
|
+
// bodies applies to anything thrown here.
|
|
189
|
+
if (outcome.kind === "unreadable") {
|
|
190
|
+
// The file is there and we could not read it, so `devin auth login` is
|
|
191
|
+
// the wrong instruction: it would succeed and change nothing.
|
|
192
|
+
throw new Error(
|
|
193
|
+
"Found a Devin CLI credential file but could not read it. Check its permissions and size, then try again.",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (outcome.kind === "incomplete") {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"The Devin CLI credential file is missing its session key or API server URL. Run `devin auth login` again to rewrite it.",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (outcome.kind === "ok") {
|
|
202
|
+
// The host comes off disk and then receives the key, so it passes the
|
|
203
|
+
// same allowlist as the RegisterUser host. An unallowlisted value falls
|
|
204
|
+
// back to the default rather than becoming an exfiltration target.
|
|
205
|
+
const apiBaseUrl = resolveDevinApiBaseUrl(outcome.file.apiServerUrl);
|
|
206
|
+
ctrl.onProgress?.("Imported the signed-in Devin CLI session.");
|
|
207
|
+
return credentialsFromApiKey(outcome.file.apiKey, apiBaseUrl, "local-cli");
|
|
208
|
+
}
|
|
209
|
+
// "missing": no CLI session to adopt. The browser flow below is the only
|
|
210
|
+
// login path left for a user without the CLI installed.
|
|
211
|
+
}
|
|
157
212
|
return loginDevinBrowser(ctrl, DEFAULT_REGION);
|
|
158
213
|
}
|
|
159
214
|
|