mixdog 1.1.0 → 1.1.2
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 +13 -6
- package/package.json +1 -2
- package/scripts/latest-guard.mjs +63 -0
- package/scripts/lib/run-node-tests.mjs +103 -27
- package/scripts/lib/test-failure-summary.mjs +7 -2
- package/scripts/merge-mac-update-feed.mjs +107 -0
- package/scripts/mobile-release.mjs +210 -0
- package/scripts/post-release-check.mjs +353 -0
- package/scripts/publish-asset-release.mjs +43 -0
- package/scripts/release-guard.mjs +63 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test.mjs +29 -7
- package/src/defaults/shipped-history.json +2 -0
- package/src/defaults/skills/computer-use/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +1 -0
- package/src/headless-role.mjs +2 -2
- package/src/lib/keychain-cjs.cjs +582 -17
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/computer-bridge/client.mjs +25 -1
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
- package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
- package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
- package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
- package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
- package/src/runtime/effort-judge/judge-input.mjs +5 -2
- package/src/runtime/effort-judge/model-install.mjs +9 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
- package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
- package/src/runtime/shared/config.mjs +50 -2
- package/src/runtime/shared/developer-options.mjs +7 -0
- package/src/runtime/shared/instruction-files-mode.mjs +9 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -1
- package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
- package/src/runtime/shared/pristine-execution-contract.json +9 -0
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/provider-accounts.mjs +2 -0
- package/src/runtime/shared/resource-admission.mjs +10 -3
- package/src/runtime/shared/schedule-model-ref.mjs +11 -0
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-approval-wait.mjs +35 -0
- package/src/runtime/shared/shell-display.mjs +4 -2
- package/src/runtime/shared/tool-surface.mjs +2 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/session-runtime/boot/apis.mjs +5 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/tools.mjs +5 -4
- package/src/session-runtime/model-recency.mjs +3 -0
- package/src/session-runtime/provider-auth/login.mjs +10 -10
- package/src/session-runtime/provider-auth/setup.mjs +22 -2
- package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +7 -3
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-facade.mjs +5 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/schedule-session-run.mjs +3 -1
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
- package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +4 -2
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +80 -17
- package/src/session-runtime/services/seeds.mjs +7 -10
- package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/setup-tool/executor.mjs +5 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/tool-policy-refresh.mjs +3 -0
- package/src/session-runtime/webhook-session-run.mjs +3 -1
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/local-session-runtime.mjs +2 -78
- package/src/standalone/session-protocol.mjs +2 -0
- package/src/tui/app/model-options.mjs +13 -1
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
- package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
- package/src/tui/dist/index.mjs +84 -34
- package/src/tui/session/context-state.mjs +2 -0
- package/src/tui/session/live-share/owner.mjs +17 -0
- package/src/tui/session/oauth-flows.mjs +2 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/integrations.mjs +9 -8
- package/src/tui/session/session-api/routes.mjs +2 -2
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
- package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
- package/src/tui/spinner-meta.mjs +26 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - buildAgentRoleContent — BP3: agent role/system rules
|
|
10
10
|
* - buildAgentRetrievalInjectionContent — BP3: narrow read-only retrieval role
|
|
11
11
|
* - buildLeadMetaContent — BP2: Lead profile/settings context
|
|
12
|
-
* - buildInjectionContent — legacy joined Lead session content
|
|
13
12
|
*
|
|
14
13
|
* 4-BP cache layout (composeSystemPrompt):
|
|
15
14
|
* BP1 = shared tool policy
|
|
@@ -400,27 +399,6 @@ function buildLeadLanguageContent({ DATA_DIR }) {
|
|
|
400
399
|
return buildLanguageSection(DATA_DIR);
|
|
401
400
|
}
|
|
402
401
|
|
|
403
|
-
/**
|
|
404
|
-
* Build the Lead injection content.
|
|
405
|
-
*/
|
|
406
|
-
function buildInjectionContent({ PLUGIN_ROOT, DATA_DIR }) {
|
|
407
|
-
const parts = [];
|
|
408
|
-
|
|
409
|
-
const tool = buildSharedToolContent({ PLUGIN_ROOT });
|
|
410
|
-
if (tool) parts.push(tool);
|
|
411
|
-
|
|
412
|
-
const meta = buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
|
|
413
|
-
if (meta) parts.push(meta);
|
|
414
|
-
|
|
415
|
-
const role = buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
|
|
416
|
-
if (role) parts.push(role);
|
|
417
|
-
|
|
418
|
-
const language = buildLeadLanguageContent({ DATA_DIR });
|
|
419
|
-
if (language) parts.push(language);
|
|
420
|
-
|
|
421
|
-
return parts.join('\n\n');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
402
|
function buildAgentRoleContent({ PLUGIN_ROOT, profile = 'full' }) {
|
|
425
403
|
const AGENT_DIR = path.join(PLUGIN_ROOT, 'rules', 'agent');
|
|
426
404
|
if (String(profile || 'full') === 'retrieval') {
|
|
@@ -455,7 +433,6 @@ module.exports = {
|
|
|
455
433
|
buildLeadMetaContent,
|
|
456
434
|
buildLeadLanguageContent,
|
|
457
435
|
buildAgentRoleContent,
|
|
458
|
-
buildInjectionContent,
|
|
459
436
|
buildAgentRetrievalInjectionContent,
|
|
460
437
|
omitToolRoutes,
|
|
461
438
|
};
|
package/src/rules/lead/LEAD.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
never per-call narration or plan repeats.
|
|
11
11
|
- Mid-task: a replacement supersedes, an addition folds in, a status question
|
|
12
12
|
gets a brief answer while work continues.
|
|
13
|
-
- Context is managed automatically: never
|
|
14
|
-
|
|
13
|
+
- Context is managed automatically: never stop or propose stopping because of
|
|
14
|
+
context size.
|
|
15
15
|
- Be a grounded, candid, and attentive collaborator: natural and respectful,
|
|
16
16
|
without flattery, forced humor, mimicry, or stereotyping.
|
|
17
17
|
<!-- tools: agent -->
|
|
@@ -2,6 +2,11 @@ import { hasExtensionScopes, normalizeExtensionScopes } from '../../shared/exten
|
|
|
2
2
|
import { agentRouteStorageNeedsMigration, canonicalizeAgentRouteStorage } from '../../shared/agent-route-config.mjs';
|
|
3
3
|
import profileConfig from '../../shared/profile-config.cjs';
|
|
4
4
|
import { normalizeAgentProviderId, normalizePreset } from './config-presets.mjs';
|
|
5
|
+
import {
|
|
6
|
+
loadSavedCodexCatalog,
|
|
7
|
+
migrateCodexRoute,
|
|
8
|
+
supersededCodexSettingsKey,
|
|
9
|
+
} from './providers/openai-oauth-catalog-store.mjs';
|
|
5
10
|
|
|
6
11
|
const { normalizeProfileConfig } = profileConfig;
|
|
7
12
|
const RETIRED_LOCAL_PROVIDER_IDS = new Set(['ollama', 'lmstudio']);
|
|
@@ -205,7 +210,20 @@ function retiredLocalProviderStoragePresent(value = {}) {
|
|
|
205
210
|
);
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
// Saved routes on a model the provider catalog has superseded (see
|
|
214
|
+
// openai-oauth-catalog-store.mjs) — read from the last fetched catalog.
|
|
215
|
+
function supersededRouteStoragePresent(value = {}, catalog = loadSavedCodexCatalog()) {
|
|
216
|
+
const superseded = (route) => migrateCodexRoute(route, catalog) !== route;
|
|
217
|
+
return (
|
|
218
|
+
(Array.isArray(value?.presets) && value.presets.some(superseded)) ||
|
|
219
|
+
Object.values(configObject(value?.agents)).some(superseded) ||
|
|
220
|
+
Object.values(configObject(value?.maintenance)).some(superseded) ||
|
|
221
|
+
Object.keys(configObject(value?.modelSettings)).some((key) => supersededCodexSettingsKey(key, catalog))
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
208
225
|
export function canonicalizeAgentStorage(value = {}) {
|
|
226
|
+
const catalog = loadSavedCodexCatalog();
|
|
209
227
|
const next = canonicalizeAgentRouteStorage(value);
|
|
210
228
|
const removedPresetIds = new Set(
|
|
211
229
|
(Array.isArray(next.presets) ? next.presets : [])
|
|
@@ -216,17 +234,26 @@ export function canonicalizeAgentStorage(value = {}) {
|
|
|
216
234
|
next.providers = configObject(next.providers);
|
|
217
235
|
for (const id of RETIRED_LOCAL_PROVIDER_IDS) delete next.providers[id];
|
|
218
236
|
next.presets = Array.isArray(next.presets)
|
|
219
|
-
? next.presets
|
|
237
|
+
? next.presets
|
|
238
|
+
.map((preset) => normalizePreset(migrateCodexRoute(preset, catalog)))
|
|
239
|
+
.filter((preset) => preset && !isRetiredLocalRoute(preset))
|
|
220
240
|
: [];
|
|
221
241
|
if (removedPresetIds.has(String(next.default || '').trim())) next.default = null;
|
|
222
242
|
next.agents = Object.fromEntries(
|
|
223
|
-
Object.entries(configObject(next.agents))
|
|
243
|
+
Object.entries(configObject(next.agents))
|
|
244
|
+
.filter(([, route]) => !isRetiredLocalRoute(route))
|
|
245
|
+
.map(([id, route]) => [id, migrateCodexRoute(route, catalog)])
|
|
224
246
|
);
|
|
225
247
|
next.maintenance = Object.fromEntries(
|
|
226
|
-
Object.entries(configObject(next.maintenance))
|
|
248
|
+
Object.entries(configObject(next.maintenance))
|
|
249
|
+
.filter(([, route]) => !isRetiredLocalRoute(route))
|
|
250
|
+
.map(([slot, route]) => [slot, migrateCodexRoute(route, catalog)])
|
|
227
251
|
);
|
|
252
|
+
// A superseded model's own settings have no model left to apply to.
|
|
228
253
|
next.modelSettings = Object.fromEntries(
|
|
229
|
-
Object.entries(normalizedModelSettings(value)).filter(
|
|
254
|
+
Object.entries(normalizedModelSettings(value)).filter(
|
|
255
|
+
([key]) => !isRetiredLocalModelKey(key) && !supersededCodexSettingsKey(key, catalog)
|
|
256
|
+
)
|
|
230
257
|
);
|
|
231
258
|
setOrDelete(next, 'autoClear', canonicalizeAutoClearStorage(next.autoClear));
|
|
232
259
|
setOrDelete(next, 'compaction', canonicalizeCompactionStorage(next.compaction));
|
|
@@ -266,6 +293,7 @@ export function agentConfigStorageNeedsMigration(value = {}) {
|
|
|
266
293
|
return (
|
|
267
294
|
agentRouteStorageNeedsMigration(value) ||
|
|
268
295
|
retiredLocalProviderStoragePresent(value) ||
|
|
296
|
+
supersededRouteStoragePresent(value) ||
|
|
269
297
|
RETIRED_AGENT_FIELDS.some((key) => key !== 'guide' && Object.hasOwn(value || {}, key)) ||
|
|
270
298
|
presets.some((preset) => !normalizePreset(preset)) ||
|
|
271
299
|
(modules && typeof modules === 'object' && Object.hasOwn(modules, 'memory')) ||
|
|
@@ -4,6 +4,10 @@ import { mergeStoredProviders } from './config-providers-merge.mjs';
|
|
|
4
4
|
import { normalizeExtensionScopes } from '../../shared/extension-scopes.mjs';
|
|
5
5
|
import { applyConfigPatch, diffConfig } from '../../shared/config-patch.mjs';
|
|
6
6
|
import { normalizeWorkflowSelection } from '../../shared/orchestration.mjs';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_INSTRUCTION_FILES_MODE,
|
|
9
|
+
normalizeInstructionFilesMode,
|
|
10
|
+
} from '../../shared/instruction-files-mode.mjs';
|
|
7
11
|
import {
|
|
8
12
|
DEFAULT_DISABLED_AGENT_ROSTER,
|
|
9
13
|
canonicalizeAgentRouteStorage,
|
|
@@ -107,9 +111,13 @@ export function buildDefaultConfig(options = {}) {
|
|
|
107
111
|
// stored in mixdog-config.json — enabled at runtime from the presence of
|
|
108
112
|
// Mixdog-owned credentials.
|
|
109
113
|
providers['grok-oauth'] = oauthEntry('grok-oauth');
|
|
114
|
+
// Muse Code subscription (Meta Muse Spark), enabled from its stored key.
|
|
115
|
+
providers['muse-code-oauth'] = oauthEntry('muse-code-oauth');
|
|
110
116
|
// Dev-only providers (Settings → Developer → Providers, one option each):
|
|
111
117
|
// omitted entirely from the default config while their option is off so
|
|
112
118
|
// they never surface in settings.
|
|
119
|
+
// Experimental MiMo subscription: enabled from a stored platform API key.
|
|
120
|
+
if (isOAuthProviderAvailable('mimo-oauth')) providers['mimo-oauth'] = oauthEntry('mimo-oauth');
|
|
113
121
|
// Experimental direct Cursor wire provider. It remains disabled unless a
|
|
114
122
|
// Mixdog-owned login or CURSOR_ACCESS_TOKEN is present.
|
|
115
123
|
if (isOAuthProviderAvailable('cursor-oauth')) providers['cursor-oauth'] = oauthEntry('cursor-oauth');
|
|
@@ -187,6 +195,13 @@ function normalizeRecapConfig(rawRecap) {
|
|
|
187
195
|
return recap;
|
|
188
196
|
}
|
|
189
197
|
|
|
198
|
+
// `instructionFiles` (instruction-files-mode.mjs): only a valid non-default
|
|
199
|
+
// mode is kept; absent means the default.
|
|
200
|
+
function storedInstructionFiles(agent) {
|
|
201
|
+
const mode = normalizeInstructionFilesMode(agent?.instructionFiles);
|
|
202
|
+
return mode === DEFAULT_INSTRUCTION_FILES_MODE ? {} : { instructionFiles: mode };
|
|
203
|
+
}
|
|
204
|
+
|
|
190
205
|
export function loadConfig(options = {}) {
|
|
191
206
|
const includeSecrets = options.secrets !== false;
|
|
192
207
|
try {
|
|
@@ -245,6 +260,7 @@ export function loadConfig(options = {}) {
|
|
|
245
260
|
// canonicalizeAgentRouteStorage drops it when empty.
|
|
246
261
|
disabledAgents: legacyDisabledAgents ? disabledAgentIds(raw) : effectiveDisabledAgents(raw),
|
|
247
262
|
...(raw.memoryTools && typeof raw.memoryTools === 'object' ? { memoryTools: { ...raw.memoryTools } } : {}),
|
|
263
|
+
...storedInstructionFiles(raw),
|
|
248
264
|
...normalizeWorkflowSelection(raw),
|
|
249
265
|
profile: normalizeProfileConfig(raw.profile),
|
|
250
266
|
skills: normalizeSkillsConfig(raw.skills),
|
|
@@ -420,6 +436,8 @@ function buildAgentSaveBuilder(config) {
|
|
|
420
436
|
} else Object.assign(next, disabledAgentDelta(canonicalRoutes.disabledAgents));
|
|
421
437
|
if (config.memoryTools && typeof config.memoryTools === 'object') next.memoryTools = { ...config.memoryTools };
|
|
422
438
|
else delete next.memoryTools;
|
|
439
|
+
delete next.instructionFiles;
|
|
440
|
+
Object.assign(next, storedInstructionFiles(config));
|
|
423
441
|
delete next.workflowRoutes;
|
|
424
442
|
// Defaults live in code: values equal to the current default are not stored.
|
|
425
443
|
return storedAgentForm(removeRetiredAgentFields(next));
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The user's instruction files (AGENTS.md / CLAUDE.md), global then project,
|
|
2
|
+
// rendered as one `# Instruction Files` block for a new session's BP3 core.
|
|
3
|
+
import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { readSection } from '../../../shared/config.mjs';
|
|
7
|
+
import { normalizeInstructionFilesMode } from '../../../shared/instruction-files-mode.mjs';
|
|
8
|
+
import { featureEnvOverride } from '../runtime-core/config-helpers.mjs';
|
|
9
|
+
|
|
10
|
+
// Per-file budget in UTF-8 bytes (same byte-based cap as core memory).
|
|
11
|
+
const INSTRUCTION_FILE_MAX_BYTES = 32 * 1024;
|
|
12
|
+
const TRUNCATED_MARK = '\n\n[... truncated]';
|
|
13
|
+
const KINDS_BY_MODE = { all: ['agents', 'claude'], agents: ['agents'], claude: ['claude'], off: [] };
|
|
14
|
+
const GLOBAL_DIR = { agents: '.codex', claude: '.claude' };
|
|
15
|
+
const FILE_NAME = { agents: 'AGENTS.md', claude: 'CLAUDE.md' };
|
|
16
|
+
|
|
17
|
+
/** The configured mode, forced off by MIXDOG_BOOT_INSTRUCTION_FILES=0. */
|
|
18
|
+
export function effectiveInstructionFilesMode(mode) {
|
|
19
|
+
if (featureEnvOverride('MIXDOG_BOOT_INSTRUCTION_FILES') === false) return 'off';
|
|
20
|
+
return normalizeInstructionFilesMode(mode);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The stored agent-config value; an unreadable config reads as the default. */
|
|
24
|
+
export function configuredInstructionFilesMode() {
|
|
25
|
+
try {
|
|
26
|
+
return normalizeInstructionFilesMode(readSection('agent')?.instructionFiles);
|
|
27
|
+
} catch {
|
|
28
|
+
return normalizeInstructionFilesMode(undefined);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
33
|
+
|
|
34
|
+
/** Candidate files in load order: global files first, then the project root's.
|
|
35
|
+
* Project files are confined to the real directory of their `root`; global
|
|
36
|
+
* files carry no root and may resolve anywhere. */
|
|
37
|
+
function instructionFileCandidates({ mode, cwd, homeDir = homedir() } = {}) {
|
|
38
|
+
const kinds = KINDS_BY_MODE[effectiveInstructionFilesMode(mode)];
|
|
39
|
+
const candidates = [];
|
|
40
|
+
if (homeDir) {
|
|
41
|
+
const root = resolve(homeDir);
|
|
42
|
+
// Global files may be symlinks into a dotfiles repo: no containment root.
|
|
43
|
+
for (const kind of kinds) candidates.push({ path: join(root, GLOBAL_DIR[kind], FILE_NAME[kind]) });
|
|
44
|
+
}
|
|
45
|
+
if (cwd) {
|
|
46
|
+
const root = resolve(cwd);
|
|
47
|
+
for (const kind of kinds) candidates.push({ path: join(root, FILE_NAME[kind]), root });
|
|
48
|
+
}
|
|
49
|
+
return candidates;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isInside(root, path) {
|
|
53
|
+
const rel = relative(root, path);
|
|
54
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function skipNote(path, reason) {
|
|
58
|
+
process.stderr.write(`[instruction-files] skipped ${path}: ${reason}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** At most the file's first cap bytes (plus a BOM allowance and one probe byte)
|
|
62
|
+
* from an open fd, so a huge or endless file is never read whole. */
|
|
63
|
+
function readHead(fd) {
|
|
64
|
+
const buffer = Buffer.allocUnsafe(INSTRUCTION_FILE_MAX_BYTES + UTF8_BOM.length + 1);
|
|
65
|
+
let length = 0;
|
|
66
|
+
while (length < buffer.length) {
|
|
67
|
+
const count = readSync(fd, buffer, length, buffer.length - length, null);
|
|
68
|
+
if (count === 0) break;
|
|
69
|
+
length += count;
|
|
70
|
+
}
|
|
71
|
+
return buffer.subarray(0, length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Decode a head that may end mid-character: the incomplete tail is dropped. */
|
|
75
|
+
function decodeHead(bytes) {
|
|
76
|
+
return new TextDecoder('utf-8').decode(bytes, { stream: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The bounded, trimmed text of one instruction file; over the budget it is cut
|
|
80
|
+
* at a word boundary and ends with the truncation notice. */
|
|
81
|
+
function boundedText(head) {
|
|
82
|
+
const bytes = head.subarray(0, UTF8_BOM.length).equals(UTF8_BOM) ? head.subarray(UTF8_BOM.length) : head;
|
|
83
|
+
if (bytes.length <= INSTRUCTION_FILE_MAX_BYTES) return decodeHead(bytes).trim();
|
|
84
|
+
const budget = INSTRUCTION_FILE_MAX_BYTES - Buffer.byteLength(TRUNCATED_MARK, 'utf8');
|
|
85
|
+
const text = decodeHead(bytes.subarray(0, budget)).trim();
|
|
86
|
+
return `${text.replace(/\s+\S*$/, '')}${TRUNCATED_MARK}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The file's bounded content, or '' when it is absent, unreadable, not a
|
|
90
|
+
* regular file, or resolves (symlink/junction) outside `root` when one is set. */
|
|
91
|
+
function readInstructionFile({ path, root }) {
|
|
92
|
+
let fd;
|
|
93
|
+
try {
|
|
94
|
+
const realPath = realpathSync.native(path);
|
|
95
|
+
if (root && !isInside(realpathSync.native(root), realPath)) {
|
|
96
|
+
skipNote(path, 'resolves outside its root');
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
if (!statSync(realPath).isFile()) return '';
|
|
100
|
+
fd = openSync(realPath, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
101
|
+
if (!fstatSync(fd).isFile()) return '';
|
|
102
|
+
return boundedText(readHead(fd));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR' && error?.code !== 'EISDIR') {
|
|
105
|
+
skipNote(path, error?.code || error?.message || 'unreadable');
|
|
106
|
+
}
|
|
107
|
+
return '';
|
|
108
|
+
} finally {
|
|
109
|
+
if (fd !== undefined) closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The `# Instruction Files` block, or '' when no file has content. Files
|
|
114
|
+
* whose bounded content repeats an included one (symlinks, copies) are dropped. */
|
|
115
|
+
export function loadInstructionFilesContext({ mode, cwd, homeDir = homedir() } = {}) {
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const sections = [];
|
|
118
|
+
for (const candidate of instructionFileCandidates({ mode, cwd, homeDir })) {
|
|
119
|
+
const content = readInstructionFile(candidate);
|
|
120
|
+
if (!content || seen.has(content)) continue;
|
|
121
|
+
seen.add(content);
|
|
122
|
+
sections.push(`## ${candidate.path}\n\n${content}`);
|
|
123
|
+
}
|
|
124
|
+
if (!sections.length) return '';
|
|
125
|
+
return [
|
|
126
|
+
'# Instruction Files',
|
|
127
|
+
"The user's instruction files. Project files take precedence over global ones when they conflict.",
|
|
128
|
+
...sections,
|
|
129
|
+
].join('\n\n');
|
|
130
|
+
}
|
|
@@ -268,12 +268,16 @@ export function composeSystemPrompt(opts) {
|
|
|
268
268
|
// Keep the active workflow first within BP3 so it leads the role and
|
|
269
269
|
// environment material that varies with the session.
|
|
270
270
|
const coreMemory = skip.memory ? '' : trimmedPromptSlice(opts.coreMemoryContext);
|
|
271
|
+
// User AGENTS.md / CLAUDE.md block (context/instruction-files.mjs), already
|
|
272
|
+
// headed `# Instruction Files`; rides right after core memory.
|
|
273
|
+
const instructionFiles = skip.claudemd ? '' : trimmedPromptSlice(opts.instructionFilesContext);
|
|
271
274
|
const sessionMarkerCore = joinPromptSlices([
|
|
272
275
|
trimmedPromptSlice(opts.workflowContext),
|
|
273
276
|
opts.roleRules || '',
|
|
274
277
|
opts.userPrompt || '',
|
|
275
278
|
roleInstructionContext,
|
|
276
279
|
coreMemory ? `# Core Memory\n${coreMemory}` : '',
|
|
280
|
+
instructionFiles,
|
|
277
281
|
]);
|
|
278
282
|
// Response language is the LAST system text before the conversation. It
|
|
279
283
|
// rides the environment block (not the tier3 core) because the `# Session`
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/** The file's modification time in ms; 0 when it cannot be stat-ed. */
|
|
4
|
+
export function fileMtimeMs(path) {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path).mtimeMs;
|
|
7
|
+
} catch {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
export function decodeJwtPayload(token) {
|
|
2
13
|
try {
|
|
3
14
|
const parts = String(token || '').split('.');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Open an OAuth login URL in the browser, best effort: a failure is logged, never thrown. */
|
|
2
|
+
export async function openLoginUrl(provider, url) {
|
|
3
|
+
try {
|
|
4
|
+
const { openInBrowser } = await import('../../../../shared/open-url.mjs');
|
|
5
|
+
openInBrowser(url);
|
|
6
|
+
} catch (err) {
|
|
7
|
+
process.stderr.write(`[${provider}] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// MiMo browser login. The platform encrypts the minted API key to a one-shot
|
|
2
|
+
// X25519 public key we send in the authorize URL; the reply (loopback redirect
|
|
3
|
+
// or a pasted code) carries `u` = ephemeralPub(32) | nonce(12) | ciphertext | tag(16).
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { lookup } from 'node:dns/promises';
|
|
6
|
+
import { createDecipheriv, createHash, createPublicKey, diffieHellman, generateKeyPairSync } from 'node:crypto';
|
|
7
|
+
import { openLoginUrl } from './lib/open-login-url.mjs';
|
|
8
|
+
import {
|
|
9
|
+
authorizeEndpoint,
|
|
10
|
+
manualRedirectUri,
|
|
11
|
+
resultUrl,
|
|
12
|
+
LOGIN_TIMEOUT_MS,
|
|
13
|
+
ensureTokenPlanBaseUrl,
|
|
14
|
+
getKeyName,
|
|
15
|
+
normalizeMimoBaseUrl,
|
|
16
|
+
saveTokens,
|
|
17
|
+
} from './mimo-oauth-tokens.mjs';
|
|
18
|
+
|
|
19
|
+
// Fixed SPKI prefix for a raw 32-byte X25519 public key.
|
|
20
|
+
const X25519_SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
|
|
21
|
+
const EPHEMERAL_LEN = 32;
|
|
22
|
+
const NONCE_LEN = 12;
|
|
23
|
+
const TAG_LEN = 16;
|
|
24
|
+
const MAX_API_KEY_LEN = 512;
|
|
25
|
+
const MAX_UID_LEN = 128;
|
|
26
|
+
|
|
27
|
+
export function generateLoginKeyPair() {
|
|
28
|
+
const { publicKey, privateKey } = generateKeyPairSync('x25519');
|
|
29
|
+
return { privateKey, publicKeyB64: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url') };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Decrypt the `u` payload into { apiKey, uid, baseUrl }.
|
|
33
|
+
export function decryptMimoPayload(u, privateKey) {
|
|
34
|
+
const bytes = Buffer.from(String(u || '').trim(), 'base64url');
|
|
35
|
+
if (bytes.length <= EPHEMERAL_LEN + NONCE_LEN + TAG_LEN) throw new Error('[mimo-oauth] payload too short');
|
|
36
|
+
const ephemeral = bytes.subarray(0, EPHEMERAL_LEN);
|
|
37
|
+
const nonce = bytes.subarray(EPHEMERAL_LEN, EPHEMERAL_LEN + NONCE_LEN);
|
|
38
|
+
const sealed = bytes.subarray(EPHEMERAL_LEN + NONCE_LEN, bytes.length - TAG_LEN);
|
|
39
|
+
const tag = bytes.subarray(bytes.length - TAG_LEN);
|
|
40
|
+
const publicKey = createPublicKey({
|
|
41
|
+
key: Buffer.concat([X25519_SPKI_PREFIX, ephemeral]),
|
|
42
|
+
format: 'der',
|
|
43
|
+
type: 'spki',
|
|
44
|
+
});
|
|
45
|
+
const key = createHash('sha256').update(diffieHellman({ privateKey, publicKey })).digest();
|
|
46
|
+
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
|
47
|
+
decipher.setAuthTag(tag);
|
|
48
|
+
let doc;
|
|
49
|
+
try {
|
|
50
|
+
doc = JSON.parse(Buffer.concat([decipher.update(sealed), decipher.final()]).toString('utf-8'));
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error('[mimo-oauth] could not decrypt the authorization payload');
|
|
53
|
+
}
|
|
54
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
55
|
+
throw new Error('[mimo-oauth] authorization payload is not an object');
|
|
56
|
+
}
|
|
57
|
+
const apiKey = typeof doc.sk === 'string' ? doc.sk.trim() : '';
|
|
58
|
+
if (!apiKey) throw new Error('[mimo-oauth] authorization payload carried no API key');
|
|
59
|
+
if (apiKey.length > MAX_API_KEY_LEN || !/^[\x21-\x7e]+$/.test(apiKey)) {
|
|
60
|
+
throw new Error('[mimo-oauth] authorization payload carried a malformed API key');
|
|
61
|
+
}
|
|
62
|
+
const uidPresent = doc.uid !== undefined && doc.uid !== null && doc.uid !== '';
|
|
63
|
+
if (uidPresent && typeof doc.uid !== 'string' && !Number.isFinite(doc.uid)) {
|
|
64
|
+
throw new Error('[mimo-oauth] authorization payload carried a malformed account id');
|
|
65
|
+
}
|
|
66
|
+
const uid = uidPresent ? String(doc.uid) : '';
|
|
67
|
+
if (uid.length > MAX_UID_LEN || /[\x00-\x1f\x7f]/.test(uid)) {
|
|
68
|
+
throw new Error('[mimo-oauth] authorization payload carried a malformed account id');
|
|
69
|
+
}
|
|
70
|
+
const baseUrl = doc.url === undefined || doc.url === null || doc.url === '' ? '' : normalizeMimoBaseUrl(doc.url);
|
|
71
|
+
return { apiKey, uid, baseUrl };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A pasted value may be the bare code or the full callback URL containing it.
|
|
75
|
+
function codeFromInput(input) {
|
|
76
|
+
const text = String(input || '').trim();
|
|
77
|
+
if (!text) throw new Error('[mimo-oauth] authorization code is required');
|
|
78
|
+
if (/^https?:\/\//i.test(text)) {
|
|
79
|
+
const u = new URL(text).searchParams.get('u');
|
|
80
|
+
if (!u) throw new Error('[mimo-oauth] callback URL has no authorization code');
|
|
81
|
+
return u;
|
|
82
|
+
}
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function authorizeUrl(pk, redirectUri, keyName) {
|
|
87
|
+
const url = new URL(authorizeEndpoint());
|
|
88
|
+
url.searchParams.set('pk', pk);
|
|
89
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
90
|
+
url.searchParams.set('kn', 'mimocode');
|
|
91
|
+
url.searchParams.set('key_name', keyName);
|
|
92
|
+
return url.toString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resultRedirect(error) {
|
|
96
|
+
const url = new URL(resultUrl());
|
|
97
|
+
url.searchParams.set('status', error ? 'error' : 'success');
|
|
98
|
+
if (error) url.searchParams.set('message', String(error.message || error).slice(0, 200));
|
|
99
|
+
return url.toString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Every loopback address `localhost` can resolve to (IPv4 and IPv6), so the
|
|
103
|
+
// browser reaches the callback whichever family it picks.
|
|
104
|
+
async function loopbackAddresses() {
|
|
105
|
+
try {
|
|
106
|
+
const found = await lookup('localhost', { all: true });
|
|
107
|
+
const addresses = [...new Set(found.map((entry) => entry.address))];
|
|
108
|
+
if (addresses.length) return addresses;
|
|
109
|
+
} catch {
|
|
110
|
+
/* fall back below */
|
|
111
|
+
}
|
|
112
|
+
return ['127.0.0.1'];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function listenOn(server, port, host) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
server.once('error', reject);
|
|
118
|
+
server.listen(port, host, () => {
|
|
119
|
+
server.off('error', reject);
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function beginOAuthLogin({
|
|
126
|
+
openBrowser = true,
|
|
127
|
+
timeoutMs = LOGIN_TIMEOUT_MS,
|
|
128
|
+
fetchImpl = undefined,
|
|
129
|
+
addresses = null,
|
|
130
|
+
printUrl = false,
|
|
131
|
+
} = {}) {
|
|
132
|
+
const { privateKey, publicKeyB64 } = generateLoginKeyPair();
|
|
133
|
+
const keyName = getKeyName();
|
|
134
|
+
|
|
135
|
+
const servers = [];
|
|
136
|
+
const work = new AbortController();
|
|
137
|
+
let timer = null;
|
|
138
|
+
let finish = null;
|
|
139
|
+
let settled = false;
|
|
140
|
+
let url = '';
|
|
141
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
142
|
+
finish = (value, error = null) => {
|
|
143
|
+
if (settled) return;
|
|
144
|
+
settled = true;
|
|
145
|
+
if (timer) clearTimeout(timer);
|
|
146
|
+
work.abort();
|
|
147
|
+
for (const server of servers) {
|
|
148
|
+
try {
|
|
149
|
+
server.close();
|
|
150
|
+
} catch {
|
|
151
|
+
/* already closed */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (error) reject(error);
|
|
155
|
+
else resolve(value);
|
|
156
|
+
};
|
|
157
|
+
timer = setTimeout(
|
|
158
|
+
() => finish(null, new Error(`[mimo-oauth] login timed out after ${Math.round(timeoutMs / 1000)}s`)),
|
|
159
|
+
timeoutMs
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
// The caller may only use the pasted-code path; a later timeout/cancel must not be an unhandled rejection.
|
|
163
|
+
waitForCallback.catch(() => {});
|
|
164
|
+
|
|
165
|
+
// A Token Plan key without a gateway URL is routed to its region first. The
|
|
166
|
+
// credential is written only while the login is still open, then the login
|
|
167
|
+
// is completed in the same synchronous step.
|
|
168
|
+
const settle = async (decrypted) => {
|
|
169
|
+
const resolved = await ensureTokenPlanBaseUrl(decrypted, { fetchImpl, signal: work.signal, persist: false });
|
|
170
|
+
if (settled) throw new Error('[mimo-oauth] login is no longer active');
|
|
171
|
+
const tokens = saveTokens(resolved);
|
|
172
|
+
finish(tokens);
|
|
173
|
+
return tokens;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const onRequest = async (req, res) => {
|
|
177
|
+
const target = new URL(req.url || '/', 'http://localhost');
|
|
178
|
+
const u = target.pathname === '/' ? target.searchParams.get('u') : null;
|
|
179
|
+
if (!u || settled) {
|
|
180
|
+
res.writeHead(404);
|
|
181
|
+
res.end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Only a payload sealed to this login's key settles it; anything else is
|
|
185
|
+
// answered with an error and the login keeps waiting.
|
|
186
|
+
let decrypted;
|
|
187
|
+
try {
|
|
188
|
+
decrypted = decryptMimoPayload(u, privateKey);
|
|
189
|
+
} catch {
|
|
190
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
191
|
+
res.end('Invalid authorization payload');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
await settle(decrypted);
|
|
196
|
+
res.writeHead(302, { Location: resultRedirect(null) });
|
|
197
|
+
res.end();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
res.writeHead(302, { Location: resultRedirect(err) });
|
|
200
|
+
res.end();
|
|
201
|
+
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
let port = 0;
|
|
206
|
+
let bindError = null;
|
|
207
|
+
for (const address of addresses || (await loopbackAddresses())) {
|
|
208
|
+
const server = createServer(onRequest);
|
|
209
|
+
try {
|
|
210
|
+
await listenOn(server, port, address);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// A failed address only matters when no other listener comes up.
|
|
213
|
+
bindError = err;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Once bound, a runtime fault on this listener ends the login.
|
|
217
|
+
server.on('error', (err) => finish(null, new Error(`[mimo-oauth] callback server failed: ${err?.message || err}`)));
|
|
218
|
+
servers.push(server);
|
|
219
|
+
port = server.address().port;
|
|
220
|
+
}
|
|
221
|
+
if (!servers.length) {
|
|
222
|
+
const err = bindError || new Error('[mimo-oauth] no loopback address to listen on');
|
|
223
|
+
finish(null, err);
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
url = authorizeUrl(publicKeyB64, `http://localhost:${port}/`, keyName);
|
|
227
|
+
const manualUrl = authorizeUrl(publicKeyB64, manualRedirectUri(), keyName);
|
|
228
|
+
// The URL carries the one-shot login public key. The interactive surfaces
|
|
229
|
+
// (TUI, desktop) receive it in the returned handle and show it to the user
|
|
230
|
+
// only; it is printed to the terminal solely for the headless loginOAuth().
|
|
231
|
+
if (printUrl) {
|
|
232
|
+
process.stderr.write(
|
|
233
|
+
`\n[mimo-oauth] Open this URL to log in:\n${url}\n\nOr, if the browser is on another machine:\n${manualUrl}\n\n`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (openBrowser) await openLoginUrl('mimo-oauth', url);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
provider: 'mimo-oauth',
|
|
240
|
+
url,
|
|
241
|
+
manualUrl,
|
|
242
|
+
port,
|
|
243
|
+
waitForCallback,
|
|
244
|
+
completeCode: async (input) => settle(decryptMimoPayload(codeFromInput(input), privateKey)),
|
|
245
|
+
cancel: () => {
|
|
246
|
+
finish?.(null);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function loginOAuth() {
|
|
252
|
+
const login = await beginOAuthLogin({ printUrl: true });
|
|
253
|
+
return await login.waitForCallback;
|
|
254
|
+
}
|