mixdog 1.2.6 → 1.3.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/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +19 -0
- package/package.json +4 -1
- package/scripts/check-model-recommendations.mjs +89 -0
- package/scripts/gate-local.mjs +7 -0
- package/scripts/lib/isolated-test-env.mjs +16 -0
- package/src/agents/reviewer/AGENT.md +4 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +7 -0
- package/src/defaults/skills/browser-use/SKILL.md +5 -0
- package/src/defaults/skills/memory-management/SKILL.md +4 -1
- package/src/defaults/skills/setup/references/actions.md +11 -4
- package/src/defaults/skills/setup-guide/SKILL.md +175 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- package/src/rules/agent/AGENT.md +4 -0
- package/src/rules/agent-mode/AGENT_MODE.md +24 -0
- package/src/rules/agent-mode/MIXY.md +21 -0
- package/src/rules/agent-mode/ROUTINE.md +6 -0
- package/src/rules/lead/LEAD.md +7 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
- package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
- package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
- package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
- package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
- package/src/runtime/browser-bridge/client.mjs +24 -0
- package/src/runtime/channels/lib/scheduler.mjs +40 -12
- package/src/runtime/channels/lib/schedules-db.mjs +71 -12
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
- package/src/runtime/channels/lib/webhook.mjs +0 -1
- package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
- package/src/runtime/channels/lib/worker-main.mjs +17 -20
- package/src/runtime/computer-bridge/client.mjs +28 -1
- package/src/runtime/connectors/gmail-app-password.mjs +690 -0
- package/src/runtime/connectors/google-mcp.mjs +446 -0
- package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
- package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
- package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
- package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
- package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
- package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
- package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
- package/src/runtime/memory/tool-defs.mjs +5 -0
- package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
- package/src/runtime/shared/agent-avatar.d.mts +64 -0
- package/src/runtime/shared/agent-avatar.mjs +320 -0
- package/src/runtime/shared/agent-route-config.d.mts +3 -0
- package/src/runtime/shared/agent-route-config.mjs +16 -0
- package/src/runtime/shared/atomic-file.mjs +14 -1
- package/src/runtime/shared/config.mjs +8 -1
- package/src/runtime/shared/connector-accounts.mjs +15 -0
- package/src/runtime/shared/connector-catalog.d.mts +52 -0
- package/src/runtime/shared/connector-catalog.mjs +230 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +43 -0
- package/src/runtime/shared/google-oauth-client.mjs +24 -0
- package/src/runtime/shared/held-request-host.d.mts +12 -0
- package/src/runtime/shared/held-request-host.mjs +71 -0
- package/src/runtime/shared/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +28 -0
- package/src/runtime/shared/routine-limits.d.mts +11 -0
- package/src/runtime/shared/routine-limits.mjs +21 -0
- package/src/runtime/shared/routine-trigger.d.mts +10 -0
- package/src/runtime/shared/routine-trigger.mjs +44 -0
- package/src/runtime/shared/schedule-model-ref.mjs +9 -10
- package/src/runtime/shared/slow-span.mjs +4 -12
- package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
- package/src/runtime/shared/transcript-metadata.mjs +10 -0
- package/src/runtime/shared/turn-changes.mjs +0 -8
- package/src/runtime/shared/usage-readout-totals.mjs +56 -0
- package/src/runtime/shared/user-data-guard.mjs +114 -36
- package/src/session-runtime/agent-home-delivery.mjs +459 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +92 -0
- package/src/session-runtime/agent-task-sessions.mjs +200 -0
- package/src/session-runtime/boot/apis.mjs +50 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
- package/src/session-runtime/held-requests.mjs +228 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
- package/src/session-runtime/lifecycle/shared.mjs +6 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
- package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +112 -15
- package/src/session-runtime/model-route/set-route.mjs +14 -3
- package/src/session-runtime/owned-automation-run.mjs +229 -0
- package/src/session-runtime/resource-api.mjs +5 -0
- package/src/session-runtime/resource-mcp-api.mjs +107 -13
- package/src/session-runtime/resource-skills-api.mjs +12 -0
- package/src/session-runtime/room-member-gate.mjs +48 -0
- package/src/session-runtime/routine-home-turn.mjs +156 -0
- package/src/session-runtime/routine-owner.mjs +48 -0
- package/src/session-runtime/schedule-session-run.mjs +30 -64
- package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +93 -24
- package/src/session-runtime/services/memory-access.mjs +37 -0
- package/src/session-runtime/services/provider-admin.mjs +74 -39
- package/src/session-runtime/services/schedule-access.mjs +91 -0
- package/src/session-runtime/session-create/session-options.mjs +34 -7
- package/src/session-runtime/session-hooks.mjs +16 -8
- package/src/session-runtime/setup-tool/executor.mjs +68 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
- package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/skills-api.mjs +16 -1
- package/src/session-runtime/tool-policy-refresh.mjs +238 -29
- package/src/session-runtime/turn/session-ops.mjs +8 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
- package/src/session-runtime/webhook-session-run.mjs +49 -67
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
- package/src/session-runtime/workflow-agents-api.mjs +4 -1
- package/src/standalone/daemon-agent-control.mjs +1 -8
- package/src/standalone/daemon-session-control.mjs +11 -2
- package/src/standalone/daemon.mjs +18 -4
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +164 -86
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +35 -4
- package/src/tui/session/queue-helpers.mjs +2 -1
- package/src/tui/session/session-api/extensions.mjs +4 -0
- package/src/tui/session/session-api/intake/submission.mjs +5 -0
- package/src/tui/session/session-api/integrations.mjs +8 -0
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
- package/src/tui/session/session-api/lifecycle.mjs +17 -1
- package/src/tui/session/session-api/routes.mjs +48 -3
- package/src/tui/session/session-flow/drain.mjs +12 -3
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
- package/src/tui/session/session-flow/steering.mjs +58 -24
- package/src/tui/session/tool-approval.mjs +26 -5
- package/src/tui/session/transcript-intake.mjs +6 -2
- package/src/tui/session/transcript-spill.mjs +5 -1
- package/src/tui/session/tui-steering-persist.mjs +65 -42
- package/src/tui/session/turn-live-call.mjs +56 -0
- package/src/tui/session/turn.mjs +22 -6
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -33,11 +33,17 @@ import {
|
|
|
33
33
|
import { createLazyOutputValidator } from './lazy-output-validator.mjs';
|
|
34
34
|
import { appendMcpStderrTail, createMcpTransport, hasConfiguredMcpAuth } from './client-transport.mjs';
|
|
35
35
|
import { listAllMcpTools, MCP_CONNECT_CANCELLED, runBoundedHandshake } from './client-handshake.mjs';
|
|
36
|
+
import { rememberServerConnector } from './server-connectors.mjs';
|
|
37
|
+
import { mcpOAuthClientFor } from './oauth-client.mjs';
|
|
38
|
+
import { googleMcpService, withGoogleMcp } from '../../../connectors/google-mcp.mjs';
|
|
36
39
|
import {
|
|
37
40
|
clearMcpOAuth,
|
|
41
|
+
commitMcpOAuthSwitch,
|
|
38
42
|
hasStoredMcpOAuthTokens,
|
|
39
43
|
isMcpOAuthReauthenticationRequired,
|
|
40
44
|
MCP_OAUTH_CALLBACK_NOT_PENDING,
|
|
45
|
+
startMcpOAuthSwitch,
|
|
46
|
+
storedMcpOAuthIdentity,
|
|
41
47
|
} from './oauth.mjs';
|
|
42
48
|
export { probeMcpServerAuth } from './oauth-probe.mjs';
|
|
43
49
|
|
|
@@ -67,6 +73,14 @@ const _connectAbortGenerations = new Map();
|
|
|
67
73
|
const _pendingConnects = new Set();
|
|
68
74
|
// scopeId -> root path advertised by that scope's scope-following servers.
|
|
69
75
|
const _scopeRoots = new Map();
|
|
76
|
+
// One stdio process per server config, shared by every scope that connects
|
|
77
|
+
// the same server with the same resolved config: share key -> connection
|
|
78
|
+
// { key, client, transport, liveness, stderrTail, warnings, tools,
|
|
79
|
+
// capabilities, instructions, users: Set<registryKey> }. Every session
|
|
80
|
+
// runtime has its own scope, so per-scope processes multiplied with the
|
|
81
|
+
// number of retained sessions. A scope's close releases only its own use; the
|
|
82
|
+
// process tree is shut down when its last user releases it.
|
|
83
|
+
const _sharedStdio = new Map();
|
|
70
84
|
let mcpSdkPromise = null;
|
|
71
85
|
// Memo for mcpToolHasField(name, field) — keyed by `${scopeId}|${toolName}|${field}`.
|
|
72
86
|
// The lookup (regex parse + servers Map get + tools.find + schema property
|
|
@@ -99,6 +113,24 @@ function scopedServerEntries(scopeId) {
|
|
|
99
113
|
const normalized = normalizeMcpScopeId(scopeId);
|
|
100
114
|
return [...servers.entries()].filter(([, server]) => server?.scopeId === normalized);
|
|
101
115
|
}
|
|
116
|
+
// Share key of a stdio server, or null when its connection must stay private
|
|
117
|
+
// to one scope: remote transports, and servers whose advertised roots follow
|
|
118
|
+
// their scope's cwd.
|
|
119
|
+
function sharedStdioKey(name, cfg) {
|
|
120
|
+
if (cfg?._mixdogRootsFollowScope === true) return null;
|
|
121
|
+
try {
|
|
122
|
+
if (resolveMcpTransportKind(cfg) !== 'stdio') return null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return `${String(name || '')}\u0000${JSON.stringify(cfg)}`;
|
|
127
|
+
}
|
|
128
|
+
// A connection that failed a call is never handed to a later connect; scopes
|
|
129
|
+
// still holding it keep it until they release it themselves.
|
|
130
|
+
function retireSharedConnection(server) {
|
|
131
|
+
const shared = server?.shared;
|
|
132
|
+
if (shared && _sharedStdio.get(shared.key) === shared) _sharedStdio.delete(shared.key);
|
|
133
|
+
}
|
|
102
134
|
function currentConnectAbortGeneration(scopeId) {
|
|
103
135
|
return _connectAbortGenerations.get(normalizeMcpScopeId(scopeId)) || 0;
|
|
104
136
|
}
|
|
@@ -411,6 +443,7 @@ async function reconnectMcpServer(scopeId, serverName, failedServer) {
|
|
|
411
443
|
if (!current || genAtStart !== currentConnectAbortGeneration(scopeId) || current.liveness?.closing) {
|
|
412
444
|
throw new Error(`MCP server "${serverName}" was disconnected`);
|
|
413
445
|
}
|
|
446
|
+
retireSharedConnection(current);
|
|
414
447
|
await replaceServerConnection(scopeId, serverName, current, failedServer.cfg, genAtStart);
|
|
415
448
|
});
|
|
416
449
|
const replacement = servers.get(registryKey);
|
|
@@ -1223,22 +1256,52 @@ export function getMcpServerOAuthState(cfg) {
|
|
|
1223
1256
|
return null;
|
|
1224
1257
|
}
|
|
1225
1258
|
if (!url || hasConfiguredMcpAuth(cfg)) return null;
|
|
1226
|
-
|
|
1259
|
+
const signedIn = hasStoredMcpOAuthTokens(url);
|
|
1260
|
+
if (!signedIn) return { signedIn };
|
|
1261
|
+
// Who it is signed in as (never guessed: empty when the server named nobody)
|
|
1262
|
+
// and whether 계정 바꾸기 can make the provider show its account choice.
|
|
1263
|
+
const who = storedMcpOAuthIdentity(url);
|
|
1264
|
+
return {
|
|
1265
|
+
signedIn,
|
|
1266
|
+
account: typeof who?.account === 'string' ? who.account : '',
|
|
1267
|
+
workspace: typeof who?.workspace === 'string' ? who.workspace : '',
|
|
1268
|
+
forcedAccountChoice: who?.forcedChoice === true,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/** 계정 바꾸기 of an HTTP/SSE server: a browser sign-in beside the stored one
|
|
1272
|
+
* (oauth.mjs startMcpOAuthSwitch); `launch` opens the authorization URL. */
|
|
1273
|
+
export function startMcpServerOAuthSwitch(cfg, launch, serverName) {
|
|
1274
|
+
const url = resolveMcpOAuthUrl(cfg);
|
|
1275
|
+
if (!url) throw new Error(`MCP server "${serverName}" does not use OAuth sign-in`);
|
|
1276
|
+
return startMcpOAuthSwitch({
|
|
1277
|
+
url,
|
|
1278
|
+
launch,
|
|
1279
|
+
serverName,
|
|
1280
|
+
oauthClient: mcpOAuthClientFor(cfg, url),
|
|
1281
|
+
...(googleMcpService(url) ? { fetchImpl: withGoogleMcp() } : {}),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
/** Store a switch's sign-in for the server's URL; resolves its rollback. */
|
|
1285
|
+
export function commitMcpServerOAuthSwitch(cfg, signIn) {
|
|
1286
|
+
return commitMcpOAuthSwitch({ url: resolveMcpOAuthUrl(cfg), signIn });
|
|
1227
1287
|
}
|
|
1228
1288
|
/**
|
|
1229
1289
|
* Log out of an HTTP/SSE server URL: delete the OAuth tokens and client
|
|
1230
1290
|
* registration stored for it, shared by every server configured with that
|
|
1231
|
-
* exact URL, and drop them from live connections.
|
|
1291
|
+
* exact URL, and drop them from live connections. `options.revoke` also
|
|
1292
|
+
* revokes the grant where the authorization server documents how; resolves
|
|
1293
|
+
* `{ revoked }`.
|
|
1232
1294
|
*/
|
|
1233
|
-
export async function clearMcpServerOAuth(cfg) {
|
|
1295
|
+
export async function clearMcpServerOAuth(cfg, options = {}) {
|
|
1234
1296
|
let url;
|
|
1235
1297
|
try {
|
|
1236
1298
|
url = resolveMcpOAuthUrl(cfg);
|
|
1237
1299
|
} catch {
|
|
1238
1300
|
// A config without a valid remote URL never connected, so never logged in.
|
|
1239
|
-
return;
|
|
1301
|
+
return { revoked: false };
|
|
1240
1302
|
}
|
|
1241
|
-
if (url)
|
|
1303
|
+
if (url) return clearMcpOAuth({ url, revoke: options.revoke === true });
|
|
1304
|
+
return { revoked: false };
|
|
1242
1305
|
}
|
|
1243
1306
|
/**
|
|
1244
1307
|
* Close a single server: for stdio transports first shut down the full child
|
|
@@ -1248,6 +1311,13 @@ export async function clearMcpServerOAuth(cfg) {
|
|
|
1248
1311
|
* only kills the direct child and discards the pid we need to walk the tree.
|
|
1249
1312
|
*/
|
|
1250
1313
|
async function _closeServer(server) {
|
|
1314
|
+
// A shared stdio connection closes only when its last scope releases it.
|
|
1315
|
+
const shared = server?.shared;
|
|
1316
|
+
if (shared) {
|
|
1317
|
+
shared.users.delete(server.registryKey);
|
|
1318
|
+
if (shared.users.size > 0) return;
|
|
1319
|
+
retireSharedConnection(server);
|
|
1320
|
+
}
|
|
1251
1321
|
// A deliberate close is not a crash: suppress the unexpected-close report.
|
|
1252
1322
|
if (server?.liveness) server.liveness.closing = true;
|
|
1253
1323
|
const transport = server?.transport;
|
|
@@ -1266,6 +1336,19 @@ async function _closeServer(server) {
|
|
|
1266
1336
|
/* ignore */
|
|
1267
1337
|
}
|
|
1268
1338
|
}
|
|
1339
|
+
// Register `name` in `scopeId` on the live shared connection for `shareKey`,
|
|
1340
|
+
// when there is one. Returns whether it did.
|
|
1341
|
+
function adoptSharedConnection(shareKey, name, cfg, scopeId, genAtStart) {
|
|
1342
|
+
const shared = shareKey ? _sharedStdio.get(shareKey) : null;
|
|
1343
|
+
if (!shared || shared.liveness.alive === false || shared.liveness.closing) return false;
|
|
1344
|
+
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
1345
|
+
const { key: _key, users, ...connection } = shared;
|
|
1346
|
+
users.add(registryKey);
|
|
1347
|
+
rememberServerConnector(name, cfg);
|
|
1348
|
+
servers.set(registryKey, { scopeId, registryKey, name, ...connection, cfg, shared, generation: genAtStart });
|
|
1349
|
+
_invalidateMcpToolFieldMemo();
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1269
1352
|
// Connects whose handshake has not finished yet: disconnectAll() must be able
|
|
1270
1353
|
// to see (and tear down) their transports, because `servers` only lists fully
|
|
1271
1354
|
// handshaken entries. Generation token aborts a connect that outlives a
|
|
@@ -1285,6 +1368,8 @@ async function connectServer(
|
|
|
1285
1368
|
// abort before creating a transport/child at all.
|
|
1286
1369
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
1287
1370
|
}
|
|
1371
|
+
const shareKey = sharedStdioKey(name, cfg);
|
|
1372
|
+
if (adoptSharedConnection(shareKey, name, cfg, scopeId, genAtStart)) return;
|
|
1288
1373
|
const client = createMcpClient(sdk, name, cfg, scopeId);
|
|
1289
1374
|
const { transport, autoDetectAdvert, stderrTail, warnings } = createMcpTransport({
|
|
1290
1375
|
name,
|
|
@@ -1329,26 +1414,42 @@ async function connectServer(
|
|
|
1329
1414
|
}
|
|
1330
1415
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
1331
1416
|
}
|
|
1417
|
+
// Another scope finished connecting the same server meanwhile: use its
|
|
1418
|
+
// process and release this one.
|
|
1419
|
+
if (adoptSharedConnection(shareKey, name, cfg, scopeId, genAtStart)) {
|
|
1420
|
+
await _closeServer({ client, transport, liveness });
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1332
1423
|
const tools = mcpServerTools(name, capabilities, toolsResult.tools, cfg);
|
|
1333
1424
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
1334
|
-
|
|
1335
|
-
scopeId,
|
|
1336
|
-
registryKey,
|
|
1337
|
-
name,
|
|
1425
|
+
const connection = {
|
|
1338
1426
|
client,
|
|
1339
1427
|
transport,
|
|
1340
1428
|
tools,
|
|
1341
|
-
cfg,
|
|
1342
1429
|
instructions,
|
|
1343
1430
|
capabilities,
|
|
1344
1431
|
liveness,
|
|
1345
1432
|
stderrTail,
|
|
1346
1433
|
warnings: warnings || [],
|
|
1434
|
+
};
|
|
1435
|
+
const shared = shareKey ? { ...connection, key: shareKey, users: new Set([registryKey]) } : null;
|
|
1436
|
+
if (shared) _sharedStdio.set(shareKey, shared);
|
|
1437
|
+
rememberServerConnector(name, cfg);
|
|
1438
|
+
servers.set(registryKey, {
|
|
1439
|
+
scopeId,
|
|
1440
|
+
registryKey,
|
|
1441
|
+
name,
|
|
1442
|
+
...connection,
|
|
1443
|
+
cfg,
|
|
1444
|
+
shared,
|
|
1347
1445
|
generation: genAtStart,
|
|
1348
1446
|
});
|
|
1349
1447
|
if (capabilities?.tools?.listChanged && sdk.ToolListChangedNotificationSchema) {
|
|
1448
|
+
// A shared connection refreshes the tools of every scope using it.
|
|
1350
1449
|
client.setNotificationHandler(sdk.ToolListChangedNotificationSchema, () =>
|
|
1351
|
-
|
|
1450
|
+
Promise.all(
|
|
1451
|
+
[...(shared?.users ?? [registryKey])].map((key) => refreshServerTools(key, client, name, cfg, capabilities))
|
|
1452
|
+
)
|
|
1352
1453
|
);
|
|
1353
1454
|
}
|
|
1354
1455
|
_invalidateMcpToolFieldMemo();
|
|
@@ -1517,6 +1618,8 @@ async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
|
|
|
1517
1618
|
}
|
|
1518
1619
|
if (servers.get(registryKey) !== live) return;
|
|
1519
1620
|
live.tools = mcpServerTools(name, capabilities, refreshed.tools, cfg);
|
|
1621
|
+
// A scope adopting the connection later starts from the current listing.
|
|
1622
|
+
if (live.shared) live.shared.tools = live.tools;
|
|
1520
1623
|
_invalidateMcpToolFieldMemo();
|
|
1521
1624
|
} catch (error) {
|
|
1522
1625
|
mcpLog(
|
|
@@ -1570,6 +1673,7 @@ export function _registerMcpServerForTest(scopeId, name, rawTools = [], options
|
|
|
1570
1673
|
generation: currentConnectAbortGeneration(normalizedScopeId),
|
|
1571
1674
|
};
|
|
1572
1675
|
_knownMcpScopes.add(normalizedScopeId);
|
|
1676
|
+
rememberServerConnector(name, server.cfg);
|
|
1573
1677
|
servers.set(registryKey, server);
|
|
1574
1678
|
_invalidateMcpToolFieldMemo();
|
|
1575
1679
|
return server;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The account side of an MCP OAuth sign-in: which documented authorization
|
|
2
|
+
// parameter makes a provider show its account choice, and who a sign-in
|
|
3
|
+
// belongs to, read only from what the authorization server returned.
|
|
4
|
+
//
|
|
5
|
+
// Parameters are sent only where the provider documents them (keyed by the
|
|
6
|
+
// authorization endpoint's host); everywhere else nothing is added:
|
|
7
|
+
// Google prompt=select_account developers.google.com/identity/protocols/oauth2/web-server#creatingclient
|
|
8
|
+
// GitHub prompt=select_account docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
|
|
9
|
+
// Atlassian (Jira, Confluence, Trello) prompt=consent developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/
|
|
10
|
+
// Linear prompt=consent linear.app/developers/oauth-2-0-authentication
|
|
11
|
+
// Dropbox force_reapprove=true www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
|
|
12
|
+
|
|
13
|
+
const SELECT_ACCOUNT = Object.freeze({ prompt: 'select_account' });
|
|
14
|
+
// A normal connect shows the chooser only where it is the documented choice.
|
|
15
|
+
const CONNECT_PARAMS = Object.freeze({
|
|
16
|
+
'accounts.google.com': SELECT_ACCOUNT,
|
|
17
|
+
'github.com': SELECT_ACCOUNT,
|
|
18
|
+
});
|
|
19
|
+
// 계정 바꾸기: the provider's documented way to force the choice (or the grant).
|
|
20
|
+
const SWITCH_PARAMS = Object.freeze({
|
|
21
|
+
...CONNECT_PARAMS,
|
|
22
|
+
'auth.atlassian.com': Object.freeze({ prompt: 'consent' }),
|
|
23
|
+
'linear.app': Object.freeze({ prompt: 'consent' }),
|
|
24
|
+
'mcp.linear.app': Object.freeze({ prompt: 'consent' }),
|
|
25
|
+
'www.dropbox.com': Object.freeze({ force_reapprove: 'true' }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const hostOf = (url) => {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(url).host.toLowerCase();
|
|
31
|
+
} catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** `authorizationUrl` with the account parameters of `mode` (`connect`,
|
|
37
|
+
* `reauth`, `switch`); a repair (`reauth`) adds nothing. A `prompt` the
|
|
38
|
+
* request already carries (the SDK's `consent` for offline access) is kept. */
|
|
39
|
+
export function withAccountParams(authorizationUrl, mode = 'connect') {
|
|
40
|
+
const target = new URL(authorizationUrl);
|
|
41
|
+
const table = mode === 'switch' ? SWITCH_PARAMS : mode === 'connect' ? CONNECT_PARAMS : {};
|
|
42
|
+
const params = table[target.host.toLowerCase()];
|
|
43
|
+
if (!params) return target;
|
|
44
|
+
for (const [key, value] of Object.entries(params)) {
|
|
45
|
+
if (key !== 'prompt') {
|
|
46
|
+
target.searchParams.set(key, value);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const values = new Set((target.searchParams.get('prompt') || '').split(' ').filter(Boolean));
|
|
50
|
+
values.add(value);
|
|
51
|
+
target.searchParams.set('prompt', [...values].join(' '));
|
|
52
|
+
}
|
|
53
|
+
return target;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether 계정 바꾸기 can make the provider of this authorization endpoint
|
|
57
|
+
* show its choice (otherwise the user switches at the provider first). */
|
|
58
|
+
export function canForceAccountChoice(authorizationEndpoint) {
|
|
59
|
+
return Object.hasOwn(SWITCH_PARAMS, hostOf(authorizationEndpoint));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
|
|
63
|
+
|
|
64
|
+
function idTokenClaims(idToken) {
|
|
65
|
+
const payload = typeof idToken === 'string' ? idToken.split('.')[1] : '';
|
|
66
|
+
if (!payload) return {};
|
|
67
|
+
try {
|
|
68
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
69
|
+
return claims && typeof claims === 'object' ? claims : {};
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `{ account, workspace }` from a token endpoint response: the OIDC id_token
|
|
76
|
+
* claims and the identity fields providers return beside the tokens (Notion's
|
|
77
|
+
* workspace and owner, Slack's team, a `user` / `organization` object). Null
|
|
78
|
+
* when the response names nobody: an identity is never guessed. */
|
|
79
|
+
export function identityFromTokenResponse(body) {
|
|
80
|
+
if (!body || typeof body !== 'object') return null;
|
|
81
|
+
const claims = idTokenClaims(body.id_token);
|
|
82
|
+
const user = body.user && typeof body.user === 'object' ? body.user : {};
|
|
83
|
+
const person = body.owner?.user?.person;
|
|
84
|
+
const account =
|
|
85
|
+
text(claims.email) ||
|
|
86
|
+
text(claims.preferred_username) ||
|
|
87
|
+
text(body.email) ||
|
|
88
|
+
text(user.email) ||
|
|
89
|
+
text(user.login) ||
|
|
90
|
+
text(user.username) ||
|
|
91
|
+
text(person?.email) ||
|
|
92
|
+
text(body.owner?.user?.name);
|
|
93
|
+
const workspace =
|
|
94
|
+
text(body.workspace_name) || text(body.team?.name) || text(body.organization?.name) || text(body.org?.name);
|
|
95
|
+
if (!account && !workspace) return null;
|
|
96
|
+
return { account, workspace };
|
|
97
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A pre-registered OAuth client for an MCP server, used as static client
|
|
2
|
+
// information in place of dynamic client registration:
|
|
3
|
+
// - the user's own (custom connector → 고급 설정 → 앱 번호/비밀값): the id
|
|
4
|
+
// in the server config (`oauthClientId`, not a secret), the secret only in
|
|
5
|
+
// the keychain under mcpOAuthClientSecretAccount(url);
|
|
6
|
+
// - else the catalog connector's (connector-catalog.mjs `oauthClient`), with
|
|
7
|
+
// its scopes and the provider's offline-access parameter.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import keychain from '../../../../lib/keychain-cjs.cjs';
|
|
10
|
+
import { connectorById, connectorOfServer } from '../../../shared/connector-catalog.mjs';
|
|
11
|
+
import { GOOGLE_AUTH_PARAMS, googleOAuthClient } from '../../../shared/google-oauth-client.mjs';
|
|
12
|
+
import { normalizeMcpTransportUrl } from './client-config.mjs';
|
|
13
|
+
|
|
14
|
+
const BUILT_IN_CLIENTS = {
|
|
15
|
+
google: () => {
|
|
16
|
+
const client = googleOAuthClient();
|
|
17
|
+
return client && { ...client, authParams: GOOGLE_AUTH_PARAMS };
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The keychain account of the user's client secret for one server URL. */
|
|
22
|
+
export function mcpOAuthClientSecretAccount(url) {
|
|
23
|
+
const key = normalizeMcpTransportUrl(String(url));
|
|
24
|
+
return `mcp-oauth-client.${createHash('sha256').update(key).digest('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Store (or, with '', delete) the user's client secret for server `url`. */
|
|
28
|
+
export async function saveMcpOAuthClientSecret(url, secret, credentialStore = keychain) {
|
|
29
|
+
const account = mcpOAuthClientSecretAccount(url);
|
|
30
|
+
if (secret) await credentialStore.setSecretAsync(account, secret);
|
|
31
|
+
else await credentialStore.deleteSecretAsync(account);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `{ clientId, clientSecret?, secretAccount?, scopes?, authParams? }` for
|
|
36
|
+
* server config `cfg` at `url`, or null to register a client dynamically.
|
|
37
|
+
*/
|
|
38
|
+
export function mcpOAuthClientFor(cfg, url) {
|
|
39
|
+
const backend = connectorById(connectorOfServer(cfg))?.backend;
|
|
40
|
+
const catalog = backend?.kind === 'oauth' ? backend : null;
|
|
41
|
+
const scopes = catalog?.scopes ? [...catalog.scopes] : undefined;
|
|
42
|
+
const own = typeof cfg?.oauthClientId === 'string' ? cfg.oauthClientId.trim() : '';
|
|
43
|
+
if (own) {
|
|
44
|
+
const authParams = catalog?.oauthClient === 'google' ? GOOGLE_AUTH_PARAMS : undefined;
|
|
45
|
+
return { clientId: own, secretAccount: mcpOAuthClientSecretAccount(url), scopes, authParams };
|
|
46
|
+
}
|
|
47
|
+
const builtIn = catalog?.oauthClient ? BUILT_IN_CLIENTS[catalog.oauthClient]?.() : null;
|
|
48
|
+
return builtIn ? { ...builtIn, scopes } : null;
|
|
49
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { withFileLock, writeFileAtomicAsync } from '../../../shared/atomic-file.mjs';
|
|
5
6
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
7
|
+
import { canForceAccountChoice } from './oauth-account.mjs';
|
|
6
8
|
import { reauthenticationRequired } from './oauth-shared.mjs';
|
|
7
9
|
|
|
8
10
|
// Same lease as the provider OAuth refresh (openai-oauth-tokens.mjs): a
|
|
@@ -35,6 +37,31 @@ function readRevisionMarker(account) {
|
|
|
35
37
|
return readFile(credentialFile(account, 'rev'), 'utf8').catch(() => null);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// Who the stored sign-in belongs to, beside the entry (never a credential), so
|
|
41
|
+
// the status lists it without decrypting the keychain:
|
|
42
|
+
// `{ account, workspace, forcedChoice }`; absent when unknown or signed out.
|
|
43
|
+
export function writeIdentityFile(account, creds) {
|
|
44
|
+
const file = credentialFile(account, 'who');
|
|
45
|
+
if (!creds?.tokens) return rm(file, { force: true });
|
|
46
|
+
const identity = creds.identity || {};
|
|
47
|
+
return writeFileAtomicAsync(
|
|
48
|
+
file,
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
account: identity.account || '',
|
|
51
|
+
workspace: identity.workspace || '',
|
|
52
|
+
forcedChoice: canForceAccountChoice(creds.discovery?.authorizationServerMetadata?.authorization_endpoint),
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
export function readIdentityFile(account) {
|
|
57
|
+
try {
|
|
58
|
+
const value = JSON.parse(readFileSync(credentialFile(account, 'who'), 'utf8'));
|
|
59
|
+
return value && typeof value === 'object' ? value : null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
// The stored value as another process may have just written it, not this
|
|
39
66
|
// process's cached copy.
|
|
40
67
|
export function readStoredCredentials(credentialStore, account) {
|
|
@@ -57,6 +84,7 @@ export function createCredentialCache({ url, credentialStore, account }) {
|
|
|
57
84
|
clientInformation: undefined,
|
|
58
85
|
redirectUrl: undefined,
|
|
59
86
|
discovery: undefined,
|
|
87
|
+
identity: undefined,
|
|
60
88
|
observed: undefined,
|
|
61
89
|
loaded: undefined,
|
|
62
90
|
};
|
|
@@ -74,10 +102,25 @@ export function createCredentialCache({ url, credentialStore, account }) {
|
|
|
74
102
|
clientInformation: creds.clientInformation,
|
|
75
103
|
redirectUrl: creds.redirectUrl,
|
|
76
104
|
discovery: creds.discovery,
|
|
105
|
+
identity: creds.identity,
|
|
77
106
|
} = value);
|
|
78
107
|
creds.observed = revisionOf(saved);
|
|
79
108
|
hold();
|
|
80
109
|
};
|
|
110
|
+
// Whether the stored entry is still the one this process last read or wrote:
|
|
111
|
+
// its revision marker says so without a keychain read (on Windows each read
|
|
112
|
+
// is a DPAPI PowerShell round trip of ~250 ms). Every writer changes the
|
|
113
|
+
// marker under the credential lock before the entry, so under that lock an
|
|
114
|
+
// unchanged marker is an unchanged entry.
|
|
115
|
+
const unchanged = async (expected = creds.observed) =>
|
|
116
|
+
expected !== undefined &&
|
|
117
|
+
expected === creds.observed &&
|
|
118
|
+
(await readRevisionMarker(account)) === markerOf(expected, Boolean(creds.tokens));
|
|
119
|
+
// The stored entry as of now, adopted (skipped when unchanged).
|
|
120
|
+
async function adoptStored() {
|
|
121
|
+
if (await unchanged()) return;
|
|
122
|
+
adopt(await readStoredCredentials(credentialStore, account));
|
|
123
|
+
}
|
|
81
124
|
// A failed read (locked keychain) is retried by the next request.
|
|
82
125
|
const ready = () =>
|
|
83
126
|
(creds.loaded ??= credentialStore
|
|
@@ -93,25 +136,31 @@ export function createCredentialCache({ url, credentialStore, account }) {
|
|
|
93
136
|
// the newer entry, or its deletion by a log-out, and changes nothing.
|
|
94
137
|
function update(change, { expected = creds.observed, remove = false } = {}) {
|
|
95
138
|
return withCredentialLock(account, async () => {
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
139
|
+
let exists = creds.observed !== null;
|
|
140
|
+
if (!(await unchanged(expected))) {
|
|
141
|
+
const saved = await readStoredCredentials(credentialStore, account);
|
|
142
|
+
if (revisionOf(saved) !== expected) {
|
|
143
|
+
adopt(saved);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
exists = Boolean(saved);
|
|
100
147
|
}
|
|
101
148
|
change();
|
|
102
149
|
hold();
|
|
103
150
|
if (remove) {
|
|
104
151
|
await writeRevisionMarker(account, null);
|
|
105
|
-
if (
|
|
152
|
+
if (exists) await credentialStore.deleteSecretAsync(account);
|
|
153
|
+
await writeIdentityFile(account, null);
|
|
106
154
|
creds.observed = null;
|
|
107
155
|
} else {
|
|
108
156
|
const revision = randomBytes(16).toString('hex');
|
|
109
157
|
await writeRevisionMarker(account, revision, Boolean(creds.tokens));
|
|
110
|
-
const { tokens, clientInformation, redirectUrl, discovery } = creds;
|
|
158
|
+
const { tokens, clientInformation, redirectUrl, discovery, identity } = creds;
|
|
111
159
|
await credentialStore.setSecretAsync(
|
|
112
160
|
account,
|
|
113
|
-
JSON.stringify({ url, revision, tokens, clientInformation, redirectUrl, discovery })
|
|
161
|
+
JSON.stringify({ url, revision, tokens, clientInformation, redirectUrl, discovery, identity })
|
|
114
162
|
);
|
|
163
|
+
await writeIdentityFile(account, creds);
|
|
115
164
|
creds.observed = revision;
|
|
116
165
|
}
|
|
117
166
|
return true;
|
|
@@ -133,7 +182,7 @@ export function createCredentialCache({ url, credentialStore, account }) {
|
|
|
133
182
|
}
|
|
134
183
|
// Forget the in-memory credentials; the next request re-reads the keychain.
|
|
135
184
|
function clear() {
|
|
136
|
-
creds.tokens = creds.clientInformation = creds.redirectUrl = creds.discovery = undefined;
|
|
185
|
+
creds.tokens = creds.clientInformation = creds.redirectUrl = creds.discovery = creds.identity = undefined;
|
|
137
186
|
creds.loaded = creds.observed = undefined;
|
|
138
187
|
}
|
|
139
188
|
return {
|
|
@@ -141,6 +190,7 @@ export function createCredentialCache({ url, credentialStore, account }) {
|
|
|
141
190
|
held,
|
|
142
191
|
ready,
|
|
143
192
|
adopt,
|
|
193
|
+
adoptStored,
|
|
144
194
|
update,
|
|
145
195
|
observeStoredRevision,
|
|
146
196
|
clear,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { normalizeMcpTransportUrl } from './client-config.mjs';
|
|
4
|
+
import { identityFromTokenResponse } from './oauth-account.mjs';
|
|
4
5
|
import { answerOAuthCallback } from './oauth-callback.mjs';
|
|
5
6
|
import { anySignal, loginFailed, reauthenticationRequired } from './oauth-shared.mjs';
|
|
6
7
|
|
|
@@ -49,12 +50,34 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
|
|
|
49
50
|
flow.browserOpened = false;
|
|
50
51
|
flow.launchBrowser = browser || openBrowser;
|
|
51
52
|
const signal = AbortSignal.any([lifetimeSignal, requestSignal, AbortSignal.timeout(timeoutMs)]);
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
// Who this login's tokens belong to (oauth-account.mjs): the SDK keeps only
|
|
54
|
+
// the token fields, so the token endpoint's answer is read here.
|
|
55
|
+
flow.identity = undefined;
|
|
56
|
+
const safeFetch = async (input, init = {}) => {
|
|
57
|
+
const response = await fetchImpl(normalizeMcpTransportUrl(input), {
|
|
54
58
|
...init,
|
|
55
59
|
redirect: 'error',
|
|
56
60
|
signal: anySignal(signal, init.signal),
|
|
57
61
|
});
|
|
62
|
+
if (response.ok && /(^|&)grant_type=/.test(String(init.body ?? ''))) {
|
|
63
|
+
flow.identity = identityFromTokenResponse(
|
|
64
|
+
await response
|
|
65
|
+
.clone()
|
|
66
|
+
.json()
|
|
67
|
+
.catch(() => null)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return response;
|
|
71
|
+
};
|
|
72
|
+
// The caller's progress hooks (mcp-oauth-login.mjs): the callback arrived,
|
|
73
|
+
// then the tokens are stored.
|
|
74
|
+
const progress = (hook) => {
|
|
75
|
+
try {
|
|
76
|
+
browser?.[hook]?.();
|
|
77
|
+
} catch {
|
|
78
|
+
/* progress reporting never fails the login */
|
|
79
|
+
}
|
|
80
|
+
};
|
|
58
81
|
let abort;
|
|
59
82
|
const callback = new Promise((resolve, reject) => {
|
|
60
83
|
const finish = (code) => (code ? resolve(code) : reject(loginFailed()));
|
|
@@ -72,9 +95,10 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
|
|
|
72
95
|
// refreshed (and rotated) the tokens this request was rejected with:
|
|
73
96
|
// decide on the stored credentials, under the cross-process lock.
|
|
74
97
|
const result = await cache.withLock(async () => {
|
|
75
|
-
|
|
98
|
+
await cache.adoptStored();
|
|
76
99
|
if (creds.tokens && creds.tokens.access_token !== attemptedToken?.access_token) return 'AUTHORIZED';
|
|
77
|
-
|
|
100
|
+
const registered = provider.preRegistered || creds.clientInformation;
|
|
101
|
+
if (!interactive && (!creds.tokens?.refresh_token || !registered || !creds.redirectUrl)) {
|
|
78
102
|
throw reauthenticationRequired();
|
|
79
103
|
}
|
|
80
104
|
flow.state = randomBytes(32).toString('base64url');
|
|
@@ -85,16 +109,17 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
|
|
|
85
109
|
if (result === 'REDIRECT') {
|
|
86
110
|
const code = await callback;
|
|
87
111
|
signal.throwIfAborted();
|
|
112
|
+
progress('onCallback');
|
|
88
113
|
const exchanged = await cache.withLock(async () => {
|
|
89
114
|
// The entry this login registered in is gone only after a log-out
|
|
90
115
|
// since it began, which wins: never recreate it.
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
cache.adopt(saved);
|
|
116
|
+
await cache.adoptStored();
|
|
117
|
+
if (creds.observed === null) throw loginFailed();
|
|
94
118
|
return auth(provider, { ...options, authorizationCode: code });
|
|
95
119
|
});
|
|
96
120
|
if (exchanged !== 'AUTHORIZED') throw loginFailed();
|
|
97
121
|
}
|
|
122
|
+
progress('onAuthorized');
|
|
98
123
|
signal.throwIfAborted();
|
|
99
124
|
} catch (error) {
|
|
100
125
|
// SDK errors may contain authorization server response bodies/secrets.
|