mixdog 1.2.0 → 1.2.1
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 +7 -5
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +13 -3
- package/src/defaults/skills/visualize/SKILL.md +12 -9
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config.mjs +10 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
- package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +25 -4
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/model-catalog.mjs +0 -12
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +124 -20
- package/src/runtime/shared/provider-api-key.mjs +12 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +55 -0
- package/src/session-runtime/boot/config.mjs +5 -2
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +29 -18
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/mcp-glue/connect.mjs +57 -10
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +9 -2
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +20 -5
- package/src/session-runtime/resource-mcp-api.mjs +101 -77
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/daemon-stored-session-views.mjs +11 -3
- package/src/standalone/daemon.mjs +41 -4
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +507 -171
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/settings.mjs +7 -0
- package/src/tui/session/session-flow/drain.mjs +14 -4
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn.mjs +17 -0
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
accountScopedSessionId,
|
|
4
4
|
readProviderAccountPool,
|
|
5
5
|
chooseProviderAccount,
|
|
6
|
-
|
|
6
|
+
selectProviderAccountIf,
|
|
7
7
|
blockProviderAccount,
|
|
8
8
|
providerAccountExhausted,
|
|
9
|
+
providerAccountExhaustedForModelOnly,
|
|
10
|
+
providerAccountPolicy,
|
|
9
11
|
providerAccountsResetAt,
|
|
10
12
|
} from '../../../shared/provider-accounts.mjs';
|
|
11
13
|
import { withProviderAccount, hasExplicitProviderAuthBinding } from '../../../shared/provider-auth-binding.mjs';
|
|
@@ -116,13 +118,82 @@ function boundAccountSend(providerName, row, messages, options) {
|
|
|
116
118
|
return { opts, history, emitted: () => emitted };
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
// Commit only a successful fallback, without overwriting a newer selection
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
// Commit only a successful fallback, without overwriting a newer selection:
|
|
122
|
+
// the selection is compared under the write's lock, not before it. A fallback
|
|
123
|
+
// forced by a model-scoped limit ("7D Fable" full, or a block attributed to
|
|
124
|
+
// one family) serves this request only: the selected account still serves
|
|
125
|
+
// every other model, so it stays the provider's account of record.
|
|
126
|
+
async function commitFallbackAccount(providerName, pool, row, model, refusalScoped, policy) {
|
|
127
|
+
if (row.id === pool.selectedId || refusalScoped) return null;
|
|
128
|
+
if (accountExhaustedForModelOnly(providerName, pool.selectedId, model)) return null;
|
|
129
|
+
return selectProviderAccountIf(providerName, pool.selectedId, row.id, policy);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The account each session runs on under the headroom policy. Every answer
|
|
133
|
+
// records its provider and account in the assistant message's
|
|
134
|
+
// `providerMetadata` (and in provider state when the adapter keeps one), so
|
|
135
|
+
// the binding survives a restart and eviction from this map, which only
|
|
136
|
+
// spares the history walk. Bounded, oldest binding first out.
|
|
137
|
+
const SESSION_ACCOUNT_LIMIT = 1000;
|
|
138
|
+
const ROUTING_METADATA_KEY = 'accountRouting';
|
|
139
|
+
const sessionAccounts = new Map();
|
|
140
|
+
|
|
141
|
+
// Forget every in-memory binding, as a restart does (tests).
|
|
142
|
+
export function resetSessionAccountBindings() {
|
|
143
|
+
sessionAccounts.clear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rememberSessionAccount(key, id) {
|
|
147
|
+
sessionAccounts.delete(key);
|
|
148
|
+
sessionAccounts.set(key, id);
|
|
149
|
+
if (sessionAccounts.size > SESSION_ACCOUNT_LIMIT) sessionAccounts.delete(sessionAccounts.keys().next().value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function accountExhaustedForModelOnly(providerName, id, model) {
|
|
153
|
+
const pool = readProviderAccountPool(providerName);
|
|
154
|
+
const row = pool.accounts.find((entry) => entry.id === id);
|
|
155
|
+
return (
|
|
156
|
+
Boolean(row) &&
|
|
157
|
+
providerAccountExhaustedForModelOnly(row, Date.now(), model, { useCredits: pool.useCredits === true })
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Only a binding recorded for THIS provider counts: a session that switched
|
|
162
|
+
// providers carries another roster's account ids (every roster has a `default`).
|
|
163
|
+
// The session's binding is read before the serving account, which differs
|
|
164
|
+
// when a model-scoped limit lent this one answer to another account.
|
|
165
|
+
function sessionAccountId(providerName, key, messages, options) {
|
|
166
|
+
if (key && sessionAccounts.has(key)) return sessionAccounts.get(key);
|
|
167
|
+
const state = options.providerState;
|
|
168
|
+
if (state?.providerAccountId && state.providerAccountProvider === providerName) {
|
|
169
|
+
return state.providerSessionAccountId || state.providerAccountId;
|
|
170
|
+
}
|
|
171
|
+
for (let index = (messages?.length || 0) - 1; index >= 0; index -= 1) {
|
|
172
|
+
const routing = messages[index]?.providerMetadata?.[ROUTING_METADATA_KEY];
|
|
173
|
+
if (routing?.accountId && routing.provider === providerName) return routing.sessionAccountId || routing.accountId;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Stamp the serving account on the answer, independent of transport state,
|
|
179
|
+
// with the account the session stays bound to (`bindingId`).
|
|
180
|
+
function recordServingAccount(providerName, result, id, bindingId) {
|
|
181
|
+
if (!result || typeof result !== 'object') return;
|
|
182
|
+
if (result.providerReplay) result.providerReplay = { ...result.providerReplay, accountId: id };
|
|
183
|
+
if (result.providerState) {
|
|
184
|
+
result.providerState = {
|
|
185
|
+
...result.providerState,
|
|
186
|
+
providerAccountId: id,
|
|
187
|
+
providerSessionAccountId: bindingId,
|
|
188
|
+
providerAccountProvider: providerName,
|
|
189
|
+
};
|
|
125
190
|
}
|
|
191
|
+
const metadata =
|
|
192
|
+
result.providerMetadata && typeof result.providerMetadata === 'object' ? result.providerMetadata : {};
|
|
193
|
+
result.providerMetadata = {
|
|
194
|
+
...metadata,
|
|
195
|
+
[ROUTING_METADATA_KEY]: { provider: providerName, accountId: id, sessionAccountId: bindingId },
|
|
196
|
+
};
|
|
126
197
|
}
|
|
127
198
|
|
|
128
199
|
// Whether the failed account's quota window is exhausted, plus the server's
|
|
@@ -158,12 +229,18 @@ function poolSend(providerName, account) {
|
|
|
158
229
|
let lastError;
|
|
159
230
|
let probed = false;
|
|
160
231
|
const evidenced = new Set();
|
|
232
|
+
const sessionKey = options.sessionId ? `${providerName}:${options.sessionId}` : null;
|
|
233
|
+
const boundId = sessionAccountId(providerName, sessionKey, messages, options);
|
|
234
|
+
// Whether this send already left an account, and whether the latest quota
|
|
235
|
+
// refusal held only for this model's family.
|
|
236
|
+
let switched = false;
|
|
237
|
+
let refusalScoped = false;
|
|
161
238
|
// Mark `error` only when every account in `ids` (default: the roster) is
|
|
162
239
|
// exhausted with quota evidence: observed in this send, or a full window.
|
|
163
240
|
const markExhausted = (error, ids) => {
|
|
164
241
|
const pool = readProviderAccountPool(providerName);
|
|
165
|
-
//
|
|
166
|
-
const scope = ids || (pool
|
|
242
|
+
// Manual policy: the eligible set is the selected account alone.
|
|
243
|
+
const scope = ids || (providerAccountPolicy(pool) === 'manual' ? [pool.selectedId] : null);
|
|
167
244
|
const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
|
|
168
245
|
const opts = { useCredits: pool.useCredits === true };
|
|
169
246
|
const now = Date.now();
|
|
@@ -194,7 +271,8 @@ function poolSend(providerName, account) {
|
|
|
194
271
|
}
|
|
195
272
|
return account('default').send(messages, model, tools, options);
|
|
196
273
|
}
|
|
197
|
-
const
|
|
274
|
+
const policy = providerAccountPolicy(pool);
|
|
275
|
+
const row = chooseProviderAccount(pool, attempted, Date.now(), model, { sessionAccountId: boundId });
|
|
198
276
|
if (!row) {
|
|
199
277
|
// Once per send: a refusal built on a stale meter must not outlive the
|
|
200
278
|
// meter. Re-measuring can only widen the roster, so it is tried before
|
|
@@ -209,9 +287,18 @@ function poolSend(providerName, account) {
|
|
|
209
287
|
const bound = boundAccountSend(providerName, row, messages, options);
|
|
210
288
|
try {
|
|
211
289
|
const result = await account(row.id).send(bound.history, model, tools, bound.opts);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
290
|
+
// Headroom policy: a session's first pick, or its move off its account
|
|
291
|
+
// on a limit, becomes the selection; continuing on its account does
|
|
292
|
+
// not, so concurrent sessions never flip the selection back and forth.
|
|
293
|
+
// A move forced by a model-scoped limit serves this request only.
|
|
294
|
+
const leftBound = Boolean(boundId) && row.id !== boundId;
|
|
295
|
+
const scoped = switched
|
|
296
|
+
? refusalScoped
|
|
297
|
+
: policy === 'headroom' && leftBound && accountExhaustedForModelOnly(providerName, boundId, model);
|
|
298
|
+
recordServingAccount(providerName, result, row.id, scoped && boundId ? boundId : row.id);
|
|
299
|
+
if (sessionKey && policy === 'headroom' && !scoped) rememberSessionAccount(sessionKey, row.id);
|
|
300
|
+
const commit = policy !== 'headroom' || !boundId || leftBound;
|
|
301
|
+
if (commit && (await commitFallbackAccount(providerName, pool, row, model, scoped, policy))) {
|
|
215
302
|
// Publish only a committed selection, never a speculative retry or
|
|
216
303
|
// one superseded by a manual switch. The existing progress channel
|
|
217
304
|
// carries this across runtime shards to the attached UI.
|
|
@@ -230,7 +317,7 @@ function poolSend(providerName, account) {
|
|
|
230
317
|
if (options.signal?.aborted) throw error;
|
|
231
318
|
if (error?.reauthRequired === true) {
|
|
232
319
|
if (
|
|
233
|
-
|
|
320
|
+
policy === 'manual' ||
|
|
234
321
|
bound.emitted() ||
|
|
235
322
|
error.unsafeToRetry ||
|
|
236
323
|
error.liveTextEmitted ||
|
|
@@ -238,13 +325,15 @@ function poolSend(providerName, account) {
|
|
|
238
325
|
)
|
|
239
326
|
throw error;
|
|
240
327
|
lastError = error;
|
|
328
|
+
switched = true;
|
|
329
|
+
refusalScoped = false;
|
|
241
330
|
options.onStageChange?.('reconnecting', { message: 'Sign-in expired — switching to the next account' });
|
|
242
331
|
continue;
|
|
243
332
|
}
|
|
244
333
|
const { exhausted, delay, evidence } = await accountQuotaExhausted(providerName, row, model, error, account);
|
|
245
334
|
if (options.signal?.aborted || !exhausted) throw error;
|
|
246
335
|
if (evidence) evidenced.add(row.id);
|
|
247
|
-
blockProviderAccount(
|
|
336
|
+
await blockProviderAccount(
|
|
248
337
|
providerName,
|
|
249
338
|
row.id,
|
|
250
339
|
Date.now() + (delay > 0 ? delay : 5 * 60_000),
|
|
@@ -253,9 +342,11 @@ function poolSend(providerName, account) {
|
|
|
253
342
|
{ quota: evidence === true }
|
|
254
343
|
);
|
|
255
344
|
if (bound.emitted() || error.unsafeToRetry || error.liveTextEmitted || error.emittedToolCall) throw error;
|
|
256
|
-
//
|
|
257
|
-
if (
|
|
345
|
+
// Manual policy: the eligible set is the selected account alone.
|
|
346
|
+
if (policy === 'manual') throw markExhausted(error, [row.id]);
|
|
258
347
|
lastError = error;
|
|
348
|
+
switched = true;
|
|
349
|
+
refusalScoped = accountExhaustedForModelOnly(providerName, row.id, model);
|
|
259
350
|
options.onStageChange?.('reconnecting', { message: 'Quota exhausted — switching to the next account' });
|
|
260
351
|
}
|
|
261
352
|
}
|
|
@@ -10,7 +10,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
|
|
|
10
10
|
// source release just to advance the user-agent version.
|
|
11
11
|
// Claude Opus 5.5 requires 2.1.280 or newer. The release workflow raises this
|
|
12
12
|
// to the published CLI version (scripts/sync-client-version-floors.mjs).
|
|
13
|
-
export const DEFAULT_CLI_VERSION = '2.1.
|
|
13
|
+
export const DEFAULT_CLI_VERSION = '2.1.299';
|
|
14
14
|
export const CLAUDE_CLI_NPM_PACKAGE = '@anthropic-ai/claude-code';
|
|
15
15
|
|
|
16
16
|
const CACHE_SCHEMA_VERSION = 1;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reloadProviderApiKey, resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
|
|
2
2
|
import { sanitizeToolPairs } from '../session/context-utils.mjs';
|
|
3
3
|
import { AnthropicFallbackTriggeredError } from './retry-classifier.mjs';
|
|
4
4
|
import { readStreamOutcome } from './lib/stream-outcome.mjs';
|
|
@@ -42,9 +42,19 @@ export class AnthropicProvider {
|
|
|
42
42
|
constructor(config) {
|
|
43
43
|
this.config = config || {};
|
|
44
44
|
this.name = this.config.name || 'anthropic';
|
|
45
|
-
this.apiKey =
|
|
45
|
+
this.apiKey = resolveProviderApiKey(this.name, this.config) || null;
|
|
46
46
|
this.client = this._createClient(this.apiKey);
|
|
47
47
|
}
|
|
48
|
+
// Client for the currently resolved key; rebuilt when the key changed since
|
|
49
|
+
// it was built (e.g. a keyless-constructed instance).
|
|
50
|
+
_currentClient() {
|
|
51
|
+
const key = resolveProviderApiKey(this.name, this.config) || null;
|
|
52
|
+
if (key && key !== this.apiKey) {
|
|
53
|
+
this.apiKey = key;
|
|
54
|
+
this.client = this._createClient(key);
|
|
55
|
+
}
|
|
56
|
+
return this.client;
|
|
57
|
+
}
|
|
48
58
|
// Tool-search is a request capability, not an account capability. Keep it
|
|
49
59
|
// off the client defaults and add it only on turns that actually serialize
|
|
50
60
|
// defer_loading tools.
|
|
@@ -55,8 +65,10 @@ export class AnthropicProvider {
|
|
|
55
65
|
const custom = isCustomProviderName(this.name);
|
|
56
66
|
const keyless = custom && this.config.keyless === true;
|
|
57
67
|
const betaHeaders = this.config.disableBetaHeaders || custom ? null : buildAnthropicBetaHeaders();
|
|
68
|
+
let credentials = { apiKey };
|
|
69
|
+
if (custom) credentials = { apiKey: keyless ? '' : apiKey || '', authToken: null };
|
|
58
70
|
return new (loadAnthropic())({
|
|
59
|
-
...
|
|
71
|
+
...credentials,
|
|
60
72
|
...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
|
|
61
73
|
defaultHeaders: {
|
|
62
74
|
...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}),
|
|
@@ -68,10 +80,7 @@ export class AnthropicProvider {
|
|
|
68
80
|
}
|
|
69
81
|
reloadApiKey() {
|
|
70
82
|
try {
|
|
71
|
-
const newKey =
|
|
72
|
-
getAgentApiKey(this.name) ||
|
|
73
|
-
this.config.apiKey ||
|
|
74
|
-
(this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
|
|
83
|
+
const newKey = reloadProviderApiKey(this.name, this.config);
|
|
75
84
|
if (newKey) {
|
|
76
85
|
this.config = { ...(this.config || {}), apiKey: newKey };
|
|
77
86
|
this.apiKey = newKey;
|
|
@@ -160,7 +169,7 @@ export class AnthropicProvider {
|
|
|
160
169
|
// The SDK calls and their initial-response retry policy
|
|
161
170
|
// (anthropic-api-transport.mjs).
|
|
162
171
|
const transport = createAnthropicApiTransport({
|
|
163
|
-
client: this.
|
|
172
|
+
client: this._currentClient(),
|
|
164
173
|
label: this.name,
|
|
165
174
|
opts,
|
|
166
175
|
useModel,
|
|
@@ -296,8 +305,7 @@ export class AnthropicProvider {
|
|
|
296
305
|
}
|
|
297
306
|
}
|
|
298
307
|
async listModels() {
|
|
299
|
-
const apiKey =
|
|
300
|
-
this.apiKey || this.config?.apiKey || (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
|
|
308
|
+
const apiKey = resolveProviderApiKey(this.name, this.config);
|
|
301
309
|
if (!apiKey) return MODELS;
|
|
302
310
|
try {
|
|
303
311
|
const base = String(this.config?.baseURL || 'https://api.anthropic.com').replace(/\/+$/, '');
|
|
@@ -329,6 +337,6 @@ export class AnthropicProvider {
|
|
|
329
337
|
async isAvailable() {
|
|
330
338
|
// Availability probes must not spend tokens or depend on a live
|
|
331
339
|
// network. Dispatch owns authentication validation and 401 reload.
|
|
332
|
-
return !!(this.
|
|
340
|
+
return !!resolveProviderApiKey(this.name, this.config);
|
|
333
341
|
}
|
|
334
342
|
}
|
|
@@ -89,6 +89,15 @@ export function resolveAutoEffort({ base, options, probs }) {
|
|
|
89
89
|
const ladder = autoEffortLadder(options);
|
|
90
90
|
const at = ladder.indexOf(String(base ?? '').toLowerCase());
|
|
91
91
|
if (at < 0) return null;
|
|
92
|
+
return resolveOnLadder(ladder, at, probs);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when some judged outcome could apply: the base lies on the model's ladder. */
|
|
96
|
+
export function autoEffortApplicable({ base, options }) {
|
|
97
|
+
return autoEffortLadder(options).includes(String(base ?? '').toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveOnLadder(ladder, at, probs) {
|
|
92
101
|
const { step, level, confidence } = judgedStep(probs);
|
|
93
102
|
const effort = ladder[Math.max(0, Math.min(ladder.length - 1, at + step))];
|
|
94
103
|
return { base: ladder[at], effort, step, level, confidence: Number(confidence.toFixed(3)) };
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// Command Code browser sign-in. The Studio CLI auth page mints a Provider API
|
|
2
|
+
// key and POSTs it (with our one-shot state) as JSON to a loopback callback.
|
|
3
|
+
// A pasted key or pasted callback JSON completes the same login by hand. The
|
|
4
|
+
// first sign-in on a machine adopts an existing Command Code CLI login instead.
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { openLoginUrl } from './lib/open-login-url.mjs';
|
|
11
|
+
import { readProviderAccountPool } from '../../../shared/provider-accounts.mjs';
|
|
12
|
+
import { withProviderAccount } from '../../../shared/provider-auth-binding.mjs';
|
|
13
|
+
import {
|
|
14
|
+
LOGIN_TIMEOUT_MS,
|
|
15
|
+
PROVIDER_ID,
|
|
16
|
+
STUDIO_ORIGIN,
|
|
17
|
+
fetchCommandCodeIdentity,
|
|
18
|
+
isWellFormedApiKey,
|
|
19
|
+
loadTokens,
|
|
20
|
+
saveTokens,
|
|
21
|
+
} from './command-code-oauth-tokens.mjs';
|
|
22
|
+
|
|
23
|
+
export const CALLBACK_PORT = 5959;
|
|
24
|
+
const CALLBACK_PATH = '/callback';
|
|
25
|
+
const MAX_CALLBACK_BYTES = 16 * 1024;
|
|
26
|
+
const CALLBACK_FIELDS = Object.freeze(['apiKey', 'userId', 'userName', 'keyName']);
|
|
27
|
+
|
|
28
|
+
const CORS_HEADERS = Object.freeze({
|
|
29
|
+
'Access-Control-Allow-Origin': STUDIO_ORIGIN,
|
|
30
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
31
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
32
|
+
// The Studio page is public and the callback is loopback: Chromium asks first.
|
|
33
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
34
|
+
Vary: 'Origin',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const defaultLocalAuthPath = () => join(homedir(), '.commandcode', 'auth.json');
|
|
38
|
+
|
|
39
|
+
/** Validate the Studio callback body against this login's state. */
|
|
40
|
+
export function parseCallbackPayload(value, expectedState) {
|
|
41
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
42
|
+
throw new Error('[command-code-oauth] callback body must be an object');
|
|
43
|
+
}
|
|
44
|
+
if (typeof value.state !== 'string' || !expectedState || value.state !== expectedState) {
|
|
45
|
+
throw new Error('[command-code-oauth] callback state mismatch');
|
|
46
|
+
}
|
|
47
|
+
for (const field of CALLBACK_FIELDS) {
|
|
48
|
+
if (typeof value[field] !== 'string' || !value[field].trim()) {
|
|
49
|
+
throw new Error(`[command-code-oauth] callback is missing ${field}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const apiKey = value.apiKey.trim();
|
|
53
|
+
if (!isWellFormedApiKey(apiKey)) throw new Error('[command-code-oauth] callback carried a malformed API key');
|
|
54
|
+
return { apiKey, userId: value.userId.trim(), userName: value.userName.trim(), keyName: value.keyName.trim() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A paste is either the callback JSON (state-checked) or a bare API key. */
|
|
58
|
+
export function parsePastedInput(input, expectedState) {
|
|
59
|
+
const pasted = String(input ?? '').trim();
|
|
60
|
+
if (!pasted) throw new Error('[command-code-oauth] API key is required');
|
|
61
|
+
if (pasted.startsWith('{')) {
|
|
62
|
+
let doc;
|
|
63
|
+
try {
|
|
64
|
+
doc = JSON.parse(pasted);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error('[command-code-oauth] pasted callback is not valid JSON');
|
|
67
|
+
}
|
|
68
|
+
return parseCallbackPayload(doc, expectedState);
|
|
69
|
+
}
|
|
70
|
+
if (!isWellFormedApiKey(pasted)) throw new Error('[command-code-oauth] pasted API key is malformed');
|
|
71
|
+
return { apiKey: pasted, userId: '', userName: '', keyName: '' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// No account of this provider is connected yet: no roster entry and no
|
|
75
|
+
// credential in the default slot. Adding a second account or reconnecting an
|
|
76
|
+
// existing one must pick a fresh browser identity, never the local CLI login.
|
|
77
|
+
function isFirstSignIn() {
|
|
78
|
+
try {
|
|
79
|
+
if (readProviderAccountPool(PROVIDER_ID).accounts.length) return false;
|
|
80
|
+
return !withProviderAccount(PROVIDER_ID, 'default', () => loadTokens());
|
|
81
|
+
} catch {
|
|
82
|
+
// An unreadable roster cannot prove this is the first account.
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readLocalAuth(path) {
|
|
88
|
+
try {
|
|
89
|
+
const doc = JSON.parse(readFileSync(path, 'utf-8'));
|
|
90
|
+
const apiKey = typeof doc?.apiKey === 'string' ? doc.apiKey.trim() : '';
|
|
91
|
+
const userId = typeof doc?.userId === 'string' ? doc.userId.trim() : '';
|
|
92
|
+
return apiKey && userId && isWellFormedApiKey(apiKey) ? { apiKey, userId } : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listenOn(server, port, host) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
server.once('error', reject);
|
|
101
|
+
server.listen(port, host, () => {
|
|
102
|
+
server.off('error', reject);
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readBody(req) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let size = 0;
|
|
112
|
+
req.on('data', (chunk) => {
|
|
113
|
+
size += chunk.length;
|
|
114
|
+
if (size > MAX_CALLBACK_BYTES) {
|
|
115
|
+
reject(new Error('[command-code-oauth] callback body too large'));
|
|
116
|
+
req.destroy();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
});
|
|
121
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
122
|
+
req.on('error', reject);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reply(res, status, body) {
|
|
127
|
+
res.writeHead(status, { ...CORS_HEADERS, ...(body ? { 'Content-Type': 'application/json' } : {}) });
|
|
128
|
+
res.end(body ? JSON.stringify(body) : undefined);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function authorizeUrl(port, state) {
|
|
132
|
+
const url = new URL('/studio/auth/cli', STUDIO_ORIGIN);
|
|
133
|
+
url.searchParams.set('callback', `http://127.0.0.1:${port}${CALLBACK_PATH}`);
|
|
134
|
+
url.searchParams.set('state', state);
|
|
135
|
+
return url.toString();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function beginOAuthLogin({
|
|
139
|
+
openBrowser = true,
|
|
140
|
+
timeoutMs = LOGIN_TIMEOUT_MS,
|
|
141
|
+
fetchImpl = fetch,
|
|
142
|
+
port: preferredPort = CALLBACK_PORT,
|
|
143
|
+
importLocal = isFirstSignIn(),
|
|
144
|
+
localAuthPath = defaultLocalAuthPath(),
|
|
145
|
+
printUrl = false,
|
|
146
|
+
} = {}) {
|
|
147
|
+
// Validate and store a key under its whoami identity.
|
|
148
|
+
const store = async (candidate, signal) => {
|
|
149
|
+
const identity = await fetchCommandCodeIdentity(candidate.apiKey, { fetchImpl, signal });
|
|
150
|
+
return { apiKey: candidate.apiKey, userId: identity.userId, userName: identity.userName || candidate.userName };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (importLocal) {
|
|
154
|
+
const local = readLocalAuth(localAuthPath);
|
|
155
|
+
if (local) {
|
|
156
|
+
let tokens = null;
|
|
157
|
+
try {
|
|
158
|
+
tokens = saveTokens(await store(local));
|
|
159
|
+
} catch {
|
|
160
|
+
/* a stale or rejected CLI login falls through to the browser */
|
|
161
|
+
}
|
|
162
|
+
if (tokens) {
|
|
163
|
+
return {
|
|
164
|
+
provider: PROVIDER_ID,
|
|
165
|
+
url: '',
|
|
166
|
+
manualUrl: null,
|
|
167
|
+
imported: true,
|
|
168
|
+
waitForCallback: Promise.resolve(tokens),
|
|
169
|
+
cancel: () => {},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const state = randomBytes(32).toString('base64url');
|
|
176
|
+
const servers = [];
|
|
177
|
+
// Open client connections, and the ones still owed a reply by a request
|
|
178
|
+
// that is completing the login.
|
|
179
|
+
const sockets = new Set();
|
|
180
|
+
const replying = new Set();
|
|
181
|
+
const work = new AbortController();
|
|
182
|
+
let timer = null;
|
|
183
|
+
let finish = null;
|
|
184
|
+
let settled = false;
|
|
185
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
186
|
+
finish = (value, error = null) => {
|
|
187
|
+
if (settled) return;
|
|
188
|
+
settled = true;
|
|
189
|
+
if (timer) clearTimeout(timer);
|
|
190
|
+
work.abort();
|
|
191
|
+
for (const server of servers) {
|
|
192
|
+
try {
|
|
193
|
+
server.close();
|
|
194
|
+
} catch {
|
|
195
|
+
/* already closed */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// close() only stops accepting: drop every connection (a stalled
|
|
199
|
+
// partial POST included) except those about to get their reply.
|
|
200
|
+
for (const socket of sockets) if (!replying.has(socket)) socket.destroy();
|
|
201
|
+
if (error) reject(error);
|
|
202
|
+
else resolve(value);
|
|
203
|
+
};
|
|
204
|
+
timer = setTimeout(
|
|
205
|
+
() => finish(null, new Error(`[command-code-oauth] login timed out after ${Math.round(timeoutMs / 1000)}s`)),
|
|
206
|
+
timeoutMs
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
// A caller may only use the paste path; a later timeout/cancel must not be an unhandled rejection.
|
|
210
|
+
waitForCallback.catch(() => {});
|
|
211
|
+
|
|
212
|
+
// The credential is written only while the login is still open, and the
|
|
213
|
+
// login completes in the same synchronous step.
|
|
214
|
+
const settle = async (candidate) => {
|
|
215
|
+
const tokens = await store(candidate, work.signal);
|
|
216
|
+
if (settled) throw new Error('[command-code-oauth] login is no longer active');
|
|
217
|
+
const saved = saveTokens(tokens);
|
|
218
|
+
finish(saved);
|
|
219
|
+
return saved;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleRequest = async (req, res) => {
|
|
223
|
+
const origin = req.headers.origin;
|
|
224
|
+
if (origin && origin !== STUDIO_ORIGIN) return reply(res, 403, { success: false, error: 'Forbidden origin' });
|
|
225
|
+
if (req.method === 'OPTIONS') return reply(res, 204, null);
|
|
226
|
+
let pathname;
|
|
227
|
+
try {
|
|
228
|
+
pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
229
|
+
} catch {
|
|
230
|
+
return reply(res, 400, { success: false, error: 'Bad request' });
|
|
231
|
+
}
|
|
232
|
+
if (pathname !== CALLBACK_PATH || settled) {
|
|
233
|
+
return reply(res, 404, { success: false, error: 'Not found' });
|
|
234
|
+
}
|
|
235
|
+
if (req.method !== 'POST') return reply(res, 405, { success: false, error: 'Method not allowed' });
|
|
236
|
+
// Anything that is not this login's well-formed callback is refused and
|
|
237
|
+
// the login keeps waiting.
|
|
238
|
+
let doc;
|
|
239
|
+
try {
|
|
240
|
+
doc = JSON.parse(await readBody(req));
|
|
241
|
+
} catch {
|
|
242
|
+
return reply(res, 400, { success: false, error: 'Invalid JSON body' });
|
|
243
|
+
}
|
|
244
|
+
let candidate;
|
|
245
|
+
try {
|
|
246
|
+
candidate = parseCallbackPayload(doc, state);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return reply(res, 400, { success: false, error: String(err.message).replace(/^\[command-code-oauth\]\s*/, '') });
|
|
249
|
+
}
|
|
250
|
+
replying.add(req.socket);
|
|
251
|
+
try {
|
|
252
|
+
await settle(candidate);
|
|
253
|
+
reply(res, 200, { success: true });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
reply(res, 400, { success: false, error: 'API key validation failed' });
|
|
256
|
+
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// No request can fail outside this handler; once the login is over, a
|
|
261
|
+
// connection closes as soon as its reply is flushed.
|
|
262
|
+
const onRequest = (req, res) => {
|
|
263
|
+
res.on('finish', () => {
|
|
264
|
+
replying.delete(req.socket);
|
|
265
|
+
if (settled) req.socket.destroy();
|
|
266
|
+
});
|
|
267
|
+
handleRequest(req, res).catch(() => {
|
|
268
|
+
replying.delete(req.socket);
|
|
269
|
+
if (res.headersSent) res.destroy();
|
|
270
|
+
else reply(res, 400, { success: false, error: 'Bad request' });
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
const trackSockets = (server) => {
|
|
274
|
+
server.on('connection', (socket) => {
|
|
275
|
+
if (settled) {
|
|
276
|
+
socket.destroy();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
sockets.add(socket);
|
|
280
|
+
socket.on('close', () => {
|
|
281
|
+
sockets.delete(socket);
|
|
282
|
+
replying.delete(socket);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
return server;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// The advertised callback is 127.0.0.1; ::1 is bound on the same port when
|
|
289
|
+
// possible because `localhost` may resolve to it first.
|
|
290
|
+
let primary = trackSockets(createServer(onRequest));
|
|
291
|
+
try {
|
|
292
|
+
try {
|
|
293
|
+
await listenOn(primary, preferredPort, '127.0.0.1');
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// The fixed port is taken (another login, another app): any free port.
|
|
296
|
+
if (!preferredPort) throw err;
|
|
297
|
+
primary = trackSockets(createServer(onRequest));
|
|
298
|
+
await listenOn(primary, 0, '127.0.0.1');
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
finish(null, err);
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
const port = primary.address().port;
|
|
305
|
+
servers.push(primary);
|
|
306
|
+
const secondary = trackSockets(createServer(onRequest));
|
|
307
|
+
try {
|
|
308
|
+
await listenOn(secondary, port, '::1');
|
|
309
|
+
servers.push(secondary);
|
|
310
|
+
} catch {
|
|
311
|
+
/* no IPv6 loopback: the IPv4 listener is enough */
|
|
312
|
+
}
|
|
313
|
+
for (const server of servers) {
|
|
314
|
+
server.on('error', (err) =>
|
|
315
|
+
finish(null, new Error(`[command-code-oauth] callback server failed: ${err?.message || err}`))
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const url = authorizeUrl(port, state);
|
|
320
|
+
if (printUrl) process.stderr.write(`\n[command-code-oauth] Open this URL to sign in:\n${url}\n\n`);
|
|
321
|
+
if (openBrowser) await openLoginUrl(PROVIDER_ID, url);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
provider: PROVIDER_ID,
|
|
325
|
+
url,
|
|
326
|
+
manualUrl: null,
|
|
327
|
+
port,
|
|
328
|
+
waitForCallback,
|
|
329
|
+
completeCode: async (input) => settle(parsePastedInput(input, state)),
|
|
330
|
+
cancel: () => finish(null),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function loginOAuth() {
|
|
335
|
+
const login = await beginOAuthLogin({ printUrl: true });
|
|
336
|
+
return await login.waitForCallback;
|
|
337
|
+
}
|