mixdog 1.4.1 → 1.4.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/package.json +3 -2
- package/scripts/build-shipped-history.mjs +115 -33
- package/scripts/gate-local.mjs +172 -61
- package/scripts/lib/gate-scheduler.mjs +138 -32
- package/scripts/lib/gate-timing-reporter.mjs +16 -0
- package/scripts/lib/run-node-tests.mjs +54 -29
- package/scripts/lib/scoped-temp.mjs +1 -0
- package/scripts/lib/test-failure-records.mjs +14 -1
- package/scripts/lib/test-failure-summary.mjs +16 -4
- package/scripts/lib/test-spawn-ledger.mjs +12 -3
- package/scripts/pptx-kit-index.mjs +5 -6
- package/scripts/suite-health.mjs +110 -28
- package/scripts/test.mjs +9 -14
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/explore/AGENT.md +22 -0
- package/src/agents/explore/agent.json +6 -0
- package/src/cli.mjs +7 -0
- package/src/defaults/agents.json +7 -0
- package/src/defaults/shipped-history.json +18 -2
- package/src/defaults/skills/browser-use/SKILL.md +4 -1
- package/src/defaults/skills/code-tidy/SKILL.md +71 -102
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
- package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
- package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
- package/src/defaults/skills/computer-use/SKILL.md +12 -2
- package/src/defaults/skills/goal-management/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +12 -6
- package/src/defaults/skills/setup/references/surfaces.md +2 -2
- package/src/defaults/skills/setup-guide/SKILL.md +5 -2
- package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
- package/src/defaults/skills/skill-creator/SKILL.md +85 -190
- package/src/defaults/skills/skill-creator/references/design.md +70 -162
- package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
- package/src/defaults/skills/test-suite/SKILL.md +194 -0
- package/src/defaults/skills/test-suite/references/case-management.md +51 -0
- package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
- package/src/defaults/skills/test-suite/references/reliability.md +49 -0
- package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
- package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
- package/src/defaults/skills/visualize/SKILL.md +6 -2
- package/src/lib/rules-builder.cjs +6 -30
- package/src/rules/lead/LEAD.md +3 -3
- package/src/rules/shared/20-research.md +3 -6
- package/src/rules/shared/40-editing.md +1 -0
- package/src/rules/shared/60-verification.md +10 -9
- package/src/rules/shared/70-delivery.md +4 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
- package/src/runtime/agent/orchestrator/config.mjs +12 -7
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
- package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
- package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
- package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
- package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
- package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/channels/lib/whisper-port.mjs +19 -10
- package/src/runtime/channels/lib/whisper-server.mjs +10 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
- package/src/runtime/connectors/gmail-app-password.mjs +3 -3
- package/src/runtime/explore/explorer.mjs +93 -0
- package/src/runtime/explore/tool-defs.mjs +35 -0
- package/src/runtime/explore/tool.mjs +123 -0
- package/src/runtime/memory/index.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
- package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +17 -2
- package/src/runtime/shared/atomic-file.mjs +103 -2
- package/src/runtime/shared/background-tasks.mjs +15 -6
- package/src/runtime/shared/connector-accounts.mjs +5 -5
- package/src/runtime/shared/connector-catalog.d.mts +11 -4
- package/src/runtime/shared/connector-catalog.mjs +60 -52
- package/src/runtime/shared/custom-provider-keys.mjs +20 -0
- package/src/runtime/shared/file-has-data.mjs +26 -2
- package/src/runtime/shared/file-lock.mjs +13 -15
- package/src/runtime/shared/git-launch.d.mts +9 -0
- package/src/runtime/shared/git-launch.mjs +213 -0
- package/src/runtime/shared/git-runner-worker.mjs +2 -1
- package/src/runtime/shared/llm/cost.mjs +6 -1
- package/src/runtime/shared/llm/http-agent.mjs +313 -29
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +46 -6
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
- package/src/runtime/shared/llm/system-proxy.mjs +452 -0
- package/src/runtime/shared/provider-accounts.mjs +52 -2
- package/src/runtime/shared/staged-install-worker.mjs +5 -0
- package/src/runtime/shared/staged-update.mjs +47 -2
- package/src/runtime/shared/tool-surface.mjs +110 -36
- package/src/runtime/shared/tool-work-units.mjs +4 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
- package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
- package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
- package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
- package/src/runtime/shared/worktrees/git.mjs +57 -0
- package/src/runtime/shared/worktrees/manager.mjs +205 -137
- package/src/runtime/tidy/install.mjs +95 -15
- package/src/runtime/tidy/languages.mjs +17 -15
- package/src/runtime/tidy/process.mjs +7 -4
- package/src/runtime/tidy/resolve.mjs +19 -16
- package/src/runtime/tidy/runners/biome.mjs +3 -3
- package/src/runtime/tidy/tool.mjs +1 -1
- package/src/runtime/tidy/worktree.mjs +10 -6
- package/src/runtime/visualize-bridge/client.mjs +4 -1
- package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
- package/src/runtime/web-search/index.mjs +1 -1
- package/src/session-runtime/agent-task-sessions.mjs +410 -353
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-status.mjs +103 -103
- package/src/session-runtime/connector-merge-migration.mjs +18 -15
- package/src/session-runtime/conversation-import/discovery.mjs +159 -40
- package/src/session-runtime/conversation-import/service.mjs +9 -3
- package/src/session-runtime/handoff-attachments.mjs +1 -1
- package/src/session-runtime/held-requests.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
- package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
- package/src/session-runtime/mixy-heartbeat.mjs +1 -1
- package/src/session-runtime/provider-auth/login.mjs +2 -2
- package/src/session-runtime/resource-mcp-api.mjs +98 -35
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
- package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
- package/src/session-runtime/resource-mcp-servers.mjs +5 -4
- package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
- package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
- package/src/session-runtime/services/daemon-candidate.mjs +4 -1
- package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
- package/src/session-runtime/services/plugin-admin.mjs +10 -6
- package/src/session-runtime/services/provider-admin.mjs +281 -10
- package/src/session-runtime/session-turn-api.mjs +2 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
- package/src/session-runtime/settings-system-api.mjs +1 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
- package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
- package/src/session-runtime/setup-tool/executor.mjs +6 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
- package/src/session-runtime/turn/agent-ops.mjs +23 -0
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
- package/src/session-runtime/workspace/activity-guards.mjs +176 -0
- package/src/session-runtime/workspace/session-view.mjs +149 -0
- package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
- package/src/session-runtime/workspace/transition-runner.mjs +188 -0
- package/src/session-runtime/workspace/transitions.mjs +108 -1066
- package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
- package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
- package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
- package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
- package/src/standalone/daemon-desktop-runtime.mjs +2 -0
- package/src/standalone/daemon-session-control.mjs +151 -136
- package/src/standalone/daemon.mjs +7 -0
- package/src/standalone/desktop-service-registry.mjs +9 -0
- package/src/standalone/local-session-runtime.mjs +5 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
- package/src/standalone/session-client.mjs +5 -1
- package/src/standalone/session-frame-stream.mjs +83 -11
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/standalone/session-service/agent-tree.mjs +45 -9
- package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
- package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
- package/src/standalone/session-service/stored-reader.mjs +127 -40
- package/src/tui/App.jsx +154 -164
- package/src/tui/app/app-view/floating-panel.jsx +3 -0
- package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
- package/src/tui/app/clipboard.mjs +21 -10
- package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
- package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
- package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
- package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
- package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
- package/src/tui/app/provider-setup-picker.mjs +20 -13
- package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
- package/src/tui/app/slash-dispatch.mjs +1 -1
- package/src/tui/app/use-btw-panel.mjs +29 -0
- package/src/tui/app/use-context-panel-refresh.mjs +31 -0
- package/src/tui/app/use-model-catalog-cache.mjs +13 -10
- package/src/tui/app/use-mouse-input.mjs +11 -0
- package/src/tui/app/use-onboarding-start.mjs +8 -3
- package/src/tui/app/use-panel-transition.mjs +10 -1
- package/src/tui/app/use-pasted-buffers.mjs +28 -20
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
- package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
- package/src/tui/app/use-prompt-queue-history.mjs +3 -2
- package/src/tui/app/use-selection-focus.mjs +11 -2
- package/src/tui/app/use-selection-geometry.mjs +18 -9
- package/src/tui/app/use-selection-paint.mjs +4 -4
- package/src/tui/app/use-selection-stitch.mjs +3 -3
- package/src/tui/app/use-slash-palette.mjs +62 -0
- package/src/tui/app/use-terminal-chrome.mjs +2 -2
- package/src/tui/app/use-transcript-activity.mjs +47 -32
- package/src/tui/app/use-transcript-follow.mjs +12 -4
- package/src/tui/app/use-transcript-scroll.mjs +46 -17
- package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
- package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
- package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
- package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +21 -11
- package/src/tui/app/use-tui-ready.mjs +18 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
- package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
- package/src/tui/components/Picker.jsx +27 -3
- package/src/tui/components/SignInUrl.jsx +68 -0
- package/src/tui/components/TextEntryPanel.jsx +76 -46
- package/src/tui/components/sign-in-url-layout.mjs +24 -0
- package/src/tui/dist/index.mjs +1896 -1341
- package/src/tui/session/goal-turn-state.mjs +6 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +8 -0
- package/src/tui/session/session-api/intake/abort.mjs +2 -1
- package/src/tui/session/session-api/intake.mjs +2 -0
- package/src/tui/session/session-api/integrations.mjs +2 -2
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +2 -2
- package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
- package/src/tui/session/transcript-spill.mjs +92 -4
- package/src/tui/session/transcript-store.mjs +2 -0
- package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
|
@@ -12,13 +12,21 @@ export const CUSTOM_REASONING_LEVELS = CUSTOM_EFFORT_LEVELS;
|
|
|
12
12
|
export const CUSTOM_REASONING_WIRE_FORMATS = Object.freeze(['standard', 'deepseek', 'together', 'vllm']);
|
|
13
13
|
export const CUSTOM_PROVIDER_AUTH_MODES = Object.freeze(['key', 'none']);
|
|
14
14
|
export const CUSTOM_MODEL_OVERRIDE_LIMIT = 500;
|
|
15
|
+
export const CUSTOM_HEADER_LIMIT = 64;
|
|
15
16
|
|
|
16
17
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
17
18
|
const MAX_MODEL_ID = 256;
|
|
18
19
|
const MAX_NAME = 200;
|
|
19
20
|
const MAX_WIRE_VALUE = 64;
|
|
21
|
+
const MAX_HEADER_TEXT = 4096;
|
|
20
22
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point.
|
|
21
23
|
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
24
|
+
// RFC 9110 field-name token, and a field value of visible ASCII, space, tab and
|
|
25
|
+
// obs-text (what a fetch Headers value can carry).
|
|
26
|
+
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
27
|
+
const HEADER_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
28
|
+
// The API key field owns authentication; a header may not carry a credential.
|
|
29
|
+
const AUTH_HEADERS = new Set(['authorization', 'x-api-key', 'api-key']);
|
|
22
30
|
|
|
23
31
|
function fail(message) {
|
|
24
32
|
throw new Error(`[custom-provider] ${message}`);
|
|
@@ -84,6 +92,7 @@ const V2_TOP_LEVEL_KEYS = new Set([
|
|
|
84
92
|
'modelOverrides',
|
|
85
93
|
'wires',
|
|
86
94
|
'modelWires',
|
|
95
|
+
'headers',
|
|
87
96
|
'id',
|
|
88
97
|
'apiKey',
|
|
89
98
|
'apiKeyEnv',
|
|
@@ -295,6 +304,34 @@ function normalizeModelWires(raw, protocol, wires) {
|
|
|
295
304
|
return out;
|
|
296
305
|
}
|
|
297
306
|
|
|
307
|
+
// `headers`: extra HTTP headers sent with every request to the endpoint
|
|
308
|
+
// (inference, model discovery, connection test). Names are case-insensitive
|
|
309
|
+
// tokens; the credential headers belong to the API key field.
|
|
310
|
+
function normalizeHeaders(raw) {
|
|
311
|
+
if (raw === undefined) return {};
|
|
312
|
+
if (!isPlainObject(raw)) fail('headers must be an object of header names to string values');
|
|
313
|
+
const names = Object.keys(raw);
|
|
314
|
+
if (names.length > CUSTOM_HEADER_LIMIT) fail(`at most ${CUSTOM_HEADER_LIMIT} headers are allowed`);
|
|
315
|
+
const seen = new Set();
|
|
316
|
+
const entries = [];
|
|
317
|
+
for (const name of names) {
|
|
318
|
+
if (name.length > MAX_HEADER_TEXT || !HEADER_NAME.test(name))
|
|
319
|
+
fail(`headers has an invalid header name: ${JSON.stringify(name.slice(0, 64))}`);
|
|
320
|
+
const lower = name.toLowerCase();
|
|
321
|
+
if (AUTH_HEADERS.has(lower)) fail(`headers must not set ${name}: authentication belongs to the API key field`);
|
|
322
|
+
if (seen.has(lower)) fail(`headers has duplicate header: ${name}`);
|
|
323
|
+
seen.add(lower);
|
|
324
|
+
if (typeof raw[name] !== 'string') fail(`headers.${name} must be a string`);
|
|
325
|
+
// Validate before trimming, and trim only HTTP whitespace (space, tab), so
|
|
326
|
+
// CR/LF is refused rather than stripped and other characters are kept.
|
|
327
|
+
if (!HEADER_VALUE.test(raw[name])) fail(`headers.${name} has an invalid value`);
|
|
328
|
+
const value = raw[name].replace(/^[ \t]+|[ \t]+$/g, '');
|
|
329
|
+
if (value.length > MAX_HEADER_TEXT) fail(`headers.${name} has an invalid value`);
|
|
330
|
+
entries.push([name, value]);
|
|
331
|
+
}
|
|
332
|
+
return Object.fromEntries(entries);
|
|
333
|
+
}
|
|
334
|
+
|
|
298
335
|
// Legacy (pre-v2) `models: [{ id, name?, contextWindow?, maxOutputTokens? }]`
|
|
299
336
|
// become manual, visible overrides; their presence hides newly discovered ids.
|
|
300
337
|
function migrateLegacy(input) {
|
|
@@ -336,6 +373,7 @@ export function normalizeCustomProviderConfig(input) {
|
|
|
336
373
|
fail('modelOverrides and discovery require schemaVersion 2');
|
|
337
374
|
if (input.wires !== undefined || input.modelWires !== undefined)
|
|
338
375
|
fail('wires and modelWires require schemaVersion 2');
|
|
376
|
+
if (input.headers !== undefined) fail('headers require schemaVersion 2');
|
|
339
377
|
source = migrateLegacy(input);
|
|
340
378
|
} else if (Number.isInteger(version) && version > CUSTOM_PROVIDER_SCHEMA_VERSION)
|
|
341
379
|
fail(`unsupported schemaVersion ${version}; update mixdog to use this provider`);
|
|
@@ -368,6 +406,7 @@ export function normalizeCustomProviderConfig(input) {
|
|
|
368
406
|
const newModelsHidden = optionalBoolean(discovery.newModelsHidden, 'discovery.newModelsHidden') ?? false;
|
|
369
407
|
const wires = normalizeWires(source.wires, protocol);
|
|
370
408
|
const modelWires = normalizeModelWires(source.modelWires, protocol, wires);
|
|
409
|
+
const headers = normalizeHeaders(source.headers);
|
|
371
410
|
return {
|
|
372
411
|
type: 'custom',
|
|
373
412
|
schemaVersion: CUSTOM_PROVIDER_SCHEMA_VERSION,
|
|
@@ -377,6 +416,7 @@ export function normalizeCustomProviderConfig(input) {
|
|
|
377
416
|
enabled: source.enabled !== false,
|
|
378
417
|
auth,
|
|
379
418
|
...(apiKeyEnv && auth === 'key' ? { apiKeyEnv } : {}),
|
|
419
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
380
420
|
discovery: { newModelsHidden },
|
|
381
421
|
modelOverrides: normalizeOverrides(source.modelOverrides, protocol, legacy),
|
|
382
422
|
...(wires ? { wires } : {}),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The engine a custom provider sends with when one of its additional API keys
|
|
2
|
+
// serves (account-pool.mjs `createCustomKeyPoolProvider`). Every key reaches
|
|
3
|
+
// the same endpoint, so the engine is a view of the primary instance: the
|
|
4
|
+
// catalog, discovered models, wire routes and settings stay the primary's,
|
|
5
|
+
// and only the credential and the client built from it are the key's own.
|
|
6
|
+
|
|
7
|
+
import { readCustomProviderKey } from '../../../shared/custom-provider-keys.mjs';
|
|
8
|
+
|
|
9
|
+
function keyView(engine, providerId, keyId, key) {
|
|
10
|
+
const view = Object.create(engine);
|
|
11
|
+
const rekey = (value) => {
|
|
12
|
+
// The key is the only source: an environment variable names the primary key alone.
|
|
13
|
+
view.config = { ...engine.config, apiKey: value, apiKeyEnv: undefined };
|
|
14
|
+
view.apiKey = value;
|
|
15
|
+
view.client = view._createClient(value);
|
|
16
|
+
};
|
|
17
|
+
rekey(key);
|
|
18
|
+
// An auth retry re-reads this key, never the primary's keychain entry.
|
|
19
|
+
view.reloadApiKey = () => {
|
|
20
|
+
const fresh = readCustomProviderKey(providerId, keyId);
|
|
21
|
+
if (fresh) rekey(fresh);
|
|
22
|
+
};
|
|
23
|
+
return view;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `primary` sending with additional key `keyId` of custom provider `providerId`. */
|
|
27
|
+
export function customKeyEngine(primary, providerId, keyId) {
|
|
28
|
+
const key = readCustomProviderKey(providerId, keyId);
|
|
29
|
+
if (!key) throw new Error(`An additional API key of ${providerId} is not readable; enter it again in its settings.`);
|
|
30
|
+
const engine = keyView(primary, providerId, keyId, key);
|
|
31
|
+
// A Messages-wire route sends through the bound Anthropic engine.
|
|
32
|
+
if (primary.messagesEngine) engine.messagesEngine = keyView(primary.messagesEngine, providerId, keyId, key);
|
|
33
|
+
return engine;
|
|
34
|
+
}
|
|
@@ -120,6 +120,7 @@ async function discoverAnthropicModels(provider, signal, timeoutMs = DISCOVERY_T
|
|
|
120
120
|
headers: {
|
|
121
121
|
...(provider._customKeyless ? {} : { 'x-api-key': provider._customKey() }),
|
|
122
122
|
'anthropic-version': ANTHROPIC_VERSION,
|
|
123
|
+
...(provider.config.extraHeaders || {}),
|
|
123
124
|
},
|
|
124
125
|
});
|
|
125
126
|
if (!res.ok) throw Object.assign(new Error(`${provider.name} models ${res.status}`), { status: res.status });
|
|
@@ -153,6 +154,8 @@ export async function createCustomProvider(id, config) {
|
|
|
153
154
|
apiKey: apiKey || undefined,
|
|
154
155
|
...(cfg.apiKeyEnv ? { apiKeyEnv: cfg.apiKeyEnv } : {}),
|
|
155
156
|
...(keyless ? { keyless: true } : {}),
|
|
157
|
+
// User-defined headers ride the engines' existing `extraHeaders` path.
|
|
158
|
+
...(cfg.headers ? { extraHeaders: cfg.headers } : {}),
|
|
156
159
|
...(config.preconnect === false ? { preconnect: false } : {}),
|
|
157
160
|
};
|
|
158
161
|
let provider;
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
decodeJwtPayload,
|
|
15
15
|
expiryFromAccessToken,
|
|
16
16
|
normalizeAccountIdentity,
|
|
17
|
+
OAUTH_NETWORK_HINT,
|
|
18
|
+
oauthFailureReason,
|
|
19
|
+
oauthNetworkCause,
|
|
20
|
+
oauthPublicError,
|
|
17
21
|
} from './lib/oauth-token-utils.mjs';
|
|
18
22
|
|
|
19
23
|
const AUTHORIZE_URL = 'https://app.devin.ai/auth/cli/continue';
|
|
@@ -46,9 +50,11 @@ export const DEVIN_OAUTH_FAILURE_CODES = Object.freeze([
|
|
|
46
50
|
'DEVIN_OAUTH_TOKEN_EXCHANGE_FAILED',
|
|
47
51
|
]);
|
|
48
52
|
|
|
53
|
+
// Built from fixed text, statuses and error codes only, so a failure page may
|
|
54
|
+
// show it; response bodies are never quoted (they may carry the login token).
|
|
49
55
|
function loginFailure(code, text, props = {}) {
|
|
50
56
|
const failureCode = `DEVIN_OAUTH_${code}`;
|
|
51
|
-
return Object.assign(new Error(`${failureCode}: ${text}`), { failureCode }, props);
|
|
57
|
+
return oauthPublicError(Object.assign(new Error(`${failureCode}: ${text}`), { failureCode }, props));
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
function credentialsPath() {
|
|
@@ -175,12 +181,13 @@ export async function exchangeDevinCode(code, verifier, { fetchFn = fetch, signa
|
|
|
175
181
|
});
|
|
176
182
|
} catch (error) {
|
|
177
183
|
// A cancelled login keeps its own abort; any other failure to get an
|
|
178
|
-
// answer (network, timeout) is a coded exchange failure.
|
|
179
|
-
// quoted: only its
|
|
184
|
+
// answer (network, timeout) is a coded exchange failure. Its message is
|
|
185
|
+
// not quoted: only its error code.
|
|
180
186
|
if (signal?.aborted) throw error;
|
|
187
|
+
const cause = oauthNetworkCause(error) || 'network error';
|
|
181
188
|
throw loginFailure(
|
|
182
189
|
'TOKEN_EXCHANGE_FAILED',
|
|
183
|
-
|
|
190
|
+
`Devin token exchange request failed (${cause}). ${OAUTH_NETWORK_HINT}`
|
|
184
191
|
);
|
|
185
192
|
}
|
|
186
193
|
// Response bodies may carry the login token itself, so none is ever quoted
|
|
@@ -341,9 +348,9 @@ async function startDevinOAuthLogin({ openBrowser = true, timeoutMs = LOGIN_TIME
|
|
|
341
348
|
try {
|
|
342
349
|
await acceptCode(code);
|
|
343
350
|
reply(res, 200, oauthSuccessHtml('Devin'));
|
|
344
|
-
} catch {
|
|
345
|
-
// The shared exchange reports its error through waitForCallback.
|
|
346
|
-
reply(res, 500, oauthErrorHtml('Devin could not complete the token exchange.'));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// The shared exchange also reports its error through waitForCallback.
|
|
353
|
+
reply(res, 500, oauthErrorHtml(oauthFailureReason('Devin could not complete the token exchange.', error)));
|
|
347
354
|
}
|
|
348
355
|
};
|
|
349
356
|
|
|
@@ -190,10 +190,11 @@ function connectError(
|
|
|
190
190
|
const finalCode = tooLarge ? 'request_too_large' : mixdogCode;
|
|
191
191
|
const head = `Devin ${operation} ${code || 'error'}${status ? ` (HTTP ${status})` : ''}`;
|
|
192
192
|
const text = [head, detail].filter(Boolean).join(': ') + (detailsSummary ? ` [details: ${detailsSummary}]` : '');
|
|
193
|
+
// A reworded throttle reports its throttle status, not the carrier's.
|
|
194
|
+
const carrierStatus = reworded ? mappedStatus : status || mappedStatus;
|
|
193
195
|
const error = new DevinWireError(text, {
|
|
194
196
|
code: finalCode,
|
|
195
|
-
|
|
196
|
-
status: tooLarge ? 413 : reworded ? mappedStatus : status || mappedStatus,
|
|
197
|
+
status: tooLarge ? 413 : carrierStatus,
|
|
197
198
|
retryable: tooLarge ? false : retryable,
|
|
198
199
|
connectCode: code,
|
|
199
200
|
detail: [detail, detailsSummary].filter(Boolean).join(' '),
|
|
@@ -115,7 +115,7 @@ export function geminiFailureUsage(opts, cachedContent, useModel) {
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
export function resolveGeminiUsage(response, opts, cachedContent, useModel) {
|
|
118
|
+
export function resolveGeminiUsage(response, opts, cachedContent, useModel, provider = 'gemini') {
|
|
119
119
|
const um = response.usageMetadata || null;
|
|
120
120
|
if (!um) return null;
|
|
121
121
|
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
@@ -143,7 +143,7 @@ export function resolveGeminiUsage(response, opts, cachedContent, useModel) {
|
|
|
143
143
|
model: useModel,
|
|
144
144
|
modelDisplay: useModel,
|
|
145
145
|
rawUsage: um,
|
|
146
|
-
provider
|
|
146
|
+
provider,
|
|
147
147
|
});
|
|
148
148
|
return resolvedUsage;
|
|
149
149
|
}
|
|
@@ -589,6 +589,9 @@ export async function consumeGeminiRestStreamResponse(
|
|
|
589
589
|
onChunk = null,
|
|
590
590
|
failureUsage = null,
|
|
591
591
|
firstByteTimeoutMs = GEMINI_FIRST_BYTE_TIMEOUT_MS,
|
|
592
|
+
// A first-byte window longer than the idle window must govern alone until
|
|
593
|
+
// the first payload, or the idle timer would cut the wait short.
|
|
594
|
+
idleAfterFirstChunk = false,
|
|
592
595
|
}
|
|
593
596
|
) {
|
|
594
597
|
const unwrap =
|
|
@@ -649,7 +652,7 @@ export async function consumeGeminiRestStreamResponse(
|
|
|
649
652
|
}
|
|
650
653
|
|
|
651
654
|
try {
|
|
652
|
-
watchdogs.resetIdle();
|
|
655
|
+
if (!idleAfterFirstChunk) watchdogs.resetIdle();
|
|
653
656
|
while (true) {
|
|
654
657
|
let chunk;
|
|
655
658
|
try {
|
|
@@ -6,7 +6,13 @@ import { randomBytes } from 'node:crypto';
|
|
|
6
6
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
7
7
|
import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
|
|
8
8
|
import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
emailFromJwts,
|
|
11
|
+
oauthFailureReason,
|
|
12
|
+
oauthHttpError,
|
|
13
|
+
oauthRequestError,
|
|
14
|
+
oauthResponseJson,
|
|
15
|
+
} from './lib/oauth-token-utils.mjs';
|
|
10
16
|
import {
|
|
11
17
|
CLIENT_ID,
|
|
12
18
|
SCOPE,
|
|
@@ -65,34 +71,43 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code, signal
|
|
|
65
71
|
signal?.throwIfAborted();
|
|
66
72
|
const cleanCode = String(code || '').trim();
|
|
67
73
|
if (!cleanCode) throw new Error('[grok-oauth] authorization code is required');
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
:
|
|
89
|
-
|
|
90
|
-
|
|
74
|
+
let tokenRes;
|
|
75
|
+
try {
|
|
76
|
+
tokenRes = await fetch(discovery.token_endpoint, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
79
|
+
body: new URLSearchParams({
|
|
80
|
+
grant_type: 'authorization_code',
|
|
81
|
+
client_id: CLIENT_ID,
|
|
82
|
+
code: cleanCode,
|
|
83
|
+
code_verifier: pkce.verifier,
|
|
84
|
+
redirect_uri: REDIRECT_URI,
|
|
85
|
+
// xAI re-validates the PKCE challenge at token exchange
|
|
86
|
+
// (not just the verifier), so echo it back. Omitting
|
|
87
|
+
// these makes the exchange fail. Matches the Grok CLI.
|
|
88
|
+
code_challenge: pkce.challenge,
|
|
89
|
+
code_challenge_method: 'S256',
|
|
90
|
+
}),
|
|
91
|
+
// Secret-bearing (authorization code + verifier): refuse
|
|
92
|
+
// redirects so they can't be replayed to an untrusted host.
|
|
93
|
+
redirect: 'error',
|
|
94
|
+
signal: signal
|
|
95
|
+
? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
|
|
96
|
+
: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
97
|
+
dispatcher: getLlmDispatcher(),
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw oauthRequestError('[grok-oauth] token exchange', err);
|
|
101
|
+
}
|
|
91
102
|
if (!tokenRes.ok) {
|
|
92
103
|
const text = await tokenRes.text().catch(() => '');
|
|
93
|
-
throw
|
|
104
|
+
throw oauthHttpError('[grok-oauth] token exchange', tokenRes.status, text, [
|
|
105
|
+
cleanCode,
|
|
106
|
+
pkce.verifier,
|
|
107
|
+
pkce.challenge,
|
|
108
|
+
]);
|
|
94
109
|
}
|
|
95
|
-
const tokens = tokensFromGrant(await tokenRes
|
|
110
|
+
const tokens = tokensFromGrant(await oauthResponseJson(tokenRes, '[grok-oauth] token exchange'), discovery);
|
|
96
111
|
// A cancelled login never stores the account it was exchanging.
|
|
97
112
|
signal?.throwIfAborted();
|
|
98
113
|
saveTokens(tokens);
|
|
@@ -338,14 +353,16 @@ export async function beginOAuthLogin({ openBrowser = true, sleep = defaultSleep
|
|
|
338
353
|
res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
|
|
339
354
|
return;
|
|
340
355
|
}
|
|
356
|
+
// The login settles (closing its server) before this answer is sent, so
|
|
357
|
+
// the connection must not stay open to the closed login's handler.
|
|
341
358
|
try {
|
|
342
359
|
await acceptCode(code);
|
|
343
|
-
res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
|
|
360
|
+
res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
|
|
344
361
|
res.end(oauthSuccessHtml('Grok'));
|
|
345
|
-
} catch {
|
|
346
|
-
// The shared exchange reports its error through waitForCallback.
|
|
347
|
-
res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
|
|
348
|
-
res.end(oauthErrorHtml('Grok could not complete the token exchange.'));
|
|
362
|
+
} catch (err) {
|
|
363
|
+
// The shared exchange also reports its error through waitForCallback.
|
|
364
|
+
res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
|
|
365
|
+
res.end(oauthErrorHtml(oauthFailureReason('Grok could not complete the token exchange.', err)));
|
|
349
366
|
}
|
|
350
367
|
});
|
|
351
368
|
timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
|
|
@@ -40,7 +40,6 @@ const COPY = {
|
|
|
40
40
|
failureTitle: '로그인 실패',
|
|
41
41
|
failureBody: 'Mixdog로 돌아가 다시 시도해 주세요.',
|
|
42
42
|
reason: '사유',
|
|
43
|
-
close: '닫기',
|
|
44
43
|
},
|
|
45
44
|
en: {
|
|
46
45
|
successTitle: 'Sign-in complete',
|
|
@@ -48,7 +47,6 @@ const COPY = {
|
|
|
48
47
|
failureTitle: 'Sign-in failed',
|
|
49
48
|
failureBody: 'Return to Mixdog and try again.',
|
|
50
49
|
reason: 'Reason',
|
|
51
|
-
close: 'Close',
|
|
52
50
|
},
|
|
53
51
|
};
|
|
54
52
|
|
|
@@ -69,7 +67,6 @@ function renderPage({ kind, providerName, reason, lang }) {
|
|
|
69
67
|
const reasonBlock = reason
|
|
70
68
|
? `<div class="reason"><span>${langs.map((code) => COPY[code].reason).join(' / ')}</span>${escapeHtml(reason)}</div>`
|
|
71
69
|
: '';
|
|
72
|
-
const close = langs.map((code) => COPY[code].close).join(' · ');
|
|
73
70
|
const documentTitle = escapeHtml(
|
|
74
71
|
`${langs.map((code) => COPY[code][kind === 'success' ? 'successTitle' : 'failureTitle']).join(' / ')} · Mixdog`
|
|
75
72
|
);
|
|
@@ -130,20 +127,6 @@ function renderPage({ kind, providerName, reason, lang }) {
|
|
|
130
127
|
word-break: break-word;
|
|
131
128
|
}
|
|
132
129
|
.reason span { display: block; margin-bottom: 4px; color: var(--muted); font-family: inherit; font-size: 12px; }
|
|
133
|
-
button {
|
|
134
|
-
margin-top: 24px;
|
|
135
|
-
min-width: 120px;
|
|
136
|
-
height: 40px;
|
|
137
|
-
padding: 0 20px;
|
|
138
|
-
border: 1px solid var(--line);
|
|
139
|
-
border-radius: 10px;
|
|
140
|
-
background: transparent;
|
|
141
|
-
color: var(--fg);
|
|
142
|
-
font: inherit;
|
|
143
|
-
font-size: 14px;
|
|
144
|
-
cursor: pointer;
|
|
145
|
-
}
|
|
146
|
-
button:hover { border-color: var(--muted); }
|
|
147
130
|
</style>
|
|
148
131
|
</head>
|
|
149
132
|
<body>
|
|
@@ -151,7 +134,6 @@ function renderPage({ kind, providerName, reason, lang }) {
|
|
|
151
134
|
<div class="logo">${LOGO_SVG}</div>
|
|
152
135
|
${blocks}
|
|
153
136
|
${reasonBlock}
|
|
154
|
-
<button type="button" onclick="window.close()">${escapeHtml(close)}</button>
|
|
155
137
|
</main>
|
|
156
138
|
</body>
|
|
157
139
|
</html>`;
|
|
@@ -140,14 +140,28 @@ export function isDefinitiveOAuthFailure(text, status = 0) {
|
|
|
140
140
|
return s === 401;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// Fields whose string values are secrets, snake_case or camelCase. `code` is
|
|
144
|
+
// not one: it is also every API's error/status code. The authorization code
|
|
145
|
+
// is redacted by value — exchange paths pass it in `secretValues`.
|
|
146
|
+
const SECRET_FIELD =
|
|
147
|
+
'(?:access_?token|refresh_?token|id_?token|client_?secret|code_?verifier|code_?challenge|device_?code|api_?key|token|key|secret|password|assertion)';
|
|
148
|
+
const SECRET_KEY_RE = new RegExp(`^${SECRET_FIELD}$`, 'i');
|
|
149
|
+
// Fallback for text that is not JSON. `"field": value` — a string (possibly
|
|
150
|
+
// cut off) or a bare non-numeric value in malformed JSON.
|
|
151
|
+
const JSON_SECRET_RE = new RegExp(
|
|
152
|
+
`"(${SECRET_FIELD})"\\s*:\\s*(?:"(?:[^"\\\\]|\\\\.)*"?|(?!-?\\d|true\\b|false\\b|null\\b)[^,}\\]\\s]+)`,
|
|
153
|
+
'gi'
|
|
154
|
+
);
|
|
155
|
+
// Form-encoded or query `field=value`.
|
|
156
|
+
const FORM_SECRET_RE = new RegExp(`(^|[^A-Za-z0-9_])(${SECRET_FIELD})=[^&\\s"']*`, 'gi');
|
|
157
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}(?:\.[A-Za-z0-9_-]*)?/g;
|
|
158
|
+
|
|
159
|
+
// Bearer/JWT/Anthropic tokens and the given secret values, anywhere in `text`.
|
|
160
|
+
function scrubTokenText(text, secretValues) {
|
|
161
|
+
let scrubbed = text
|
|
145
162
|
.replace(/Bearer [A-Za-z0-9._-]+/gi, 'Bearer [REDACTED]')
|
|
146
163
|
.replace(/sk-ant-[A-Za-z0-9._-]+/g, '[REDACTED]')
|
|
147
|
-
.replace(
|
|
148
|
-
/"(accessToken|refreshToken|access_token|refresh_token|code|key)"\s*:\s*"[^"]+"/g,
|
|
149
|
-
(_match, key) => `"${key}":"[REDACTED]"`
|
|
150
|
-
);
|
|
164
|
+
.replace(JWT_RE, '[REDACTED]');
|
|
151
165
|
for (const secret of secretValues) {
|
|
152
166
|
if (typeof secret === 'string' && secret) {
|
|
153
167
|
scrubbed = scrubbed.split(secret).join('[REDACTED]');
|
|
@@ -155,3 +169,121 @@ export function scrubOAuthSecrets(text, secretValues = []) {
|
|
|
155
169
|
}
|
|
156
170
|
return scrubbed;
|
|
157
171
|
}
|
|
172
|
+
|
|
173
|
+
// A JSON object or array is redacted on its parsed form, so escaped key names
|
|
174
|
+
// (`client\u005fsecret`) cannot slip past a pattern. Numbers keep their exact
|
|
175
|
+
// source text; an untouched body is returned as it came. Null when `text` is
|
|
176
|
+
// not a JSON object or array.
|
|
177
|
+
function scrubJsonSecrets(text, secretValues) {
|
|
178
|
+
let parsed;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(text, (_key, value, context) =>
|
|
181
|
+
typeof value === 'number' ? JSON.rawJSON(context.source) : value
|
|
182
|
+
);
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (!parsed || typeof parsed !== 'object' || JSON.isRawJSON(parsed)) return null;
|
|
187
|
+
let changed = false;
|
|
188
|
+
// Each decoded string (keys too) gets the full redaction: it may hold
|
|
189
|
+
// form-encoded fields, a query string or JSON of its own.
|
|
190
|
+
const scrubString = (value) => {
|
|
191
|
+
const scrubbed = scrubOAuthSecrets(value, secretValues);
|
|
192
|
+
if (scrubbed !== value) changed = true;
|
|
193
|
+
return scrubbed;
|
|
194
|
+
};
|
|
195
|
+
const walk = (value) => {
|
|
196
|
+
if (typeof value === 'string') return scrubString(value);
|
|
197
|
+
if (!value || typeof value !== 'object' || JSON.isRawJSON(value)) return value;
|
|
198
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
199
|
+
return Object.fromEntries(
|
|
200
|
+
Object.entries(value).map(([key, inner]) => {
|
|
201
|
+
if (SECRET_KEY_RE.test(key) && typeof inner === 'string') {
|
|
202
|
+
changed = true;
|
|
203
|
+
return [key, '[REDACTED]'];
|
|
204
|
+
}
|
|
205
|
+
return [scrubString(key), walk(inner)];
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
const scrubbed = walk(parsed);
|
|
210
|
+
return changed ? JSON.stringify(scrubbed) : text;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Redact OAuth secrets from a body or message: secret fields (decoded JSON
|
|
214
|
+
* keys, or JSON-like and form-encoded text), bearer/JWT/Anthropic tokens,
|
|
215
|
+
* and the given values. Numeric and status codes are kept. */
|
|
216
|
+
export function scrubOAuthSecrets(text, secretValues = []) {
|
|
217
|
+
const raw = String(text || '');
|
|
218
|
+
const json = scrubJsonSecrets(raw, secretValues);
|
|
219
|
+
if (json !== null) return json;
|
|
220
|
+
return scrubTokenText(
|
|
221
|
+
raw
|
|
222
|
+
.replace(JSON_SECRET_RE, (_match, key) => `"${key}":"[REDACTED]"`)
|
|
223
|
+
.replace(FORM_SECRET_RE, (_match, lead, key) => `${lead}${key}=[REDACTED]`),
|
|
224
|
+
secretValues
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Token-exchange failures a sign-in page may show ---
|
|
229
|
+
|
|
230
|
+
export const OAUTH_NETWORK_HINT = 'Check your proxy/VPN: Mixdog uses the system proxy, or HTTPS_PROXY.';
|
|
231
|
+
const NETWORK_CODE = /^(?:E[A-Z0-9]+|UND_ERR_[A-Z0-9_]+|ERR_[A-Z0-9_]+)$/;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The transport cause of a fetch that got no answer: its error code
|
|
235
|
+
* (ECONNRESET, UND_ERR_CONNECT_TIMEOUT, ENOTFOUND…) or "timed out". Only codes,
|
|
236
|
+
* never messages, which may quote anything. Null for a cancel or a non-network
|
|
237
|
+
* error.
|
|
238
|
+
*/
|
|
239
|
+
export function oauthNetworkCause(error) {
|
|
240
|
+
if (!error || typeof error !== 'object' || error.name === 'AbortError') return null;
|
|
241
|
+
if (error.name === 'TimeoutError') return 'timed out';
|
|
242
|
+
for (let e = error, depth = 0; e && typeof e === 'object' && depth < 5; e = e.cause, depth += 1) {
|
|
243
|
+
if (typeof e.code === 'string' && NETWORK_CODE.test(e.code)) return e.code;
|
|
244
|
+
}
|
|
245
|
+
return error.name === 'TypeError' ? 'network error' : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Errors whose message was built only from codes, statuses and redacted
|
|
249
|
+
// bodies: the only ones a failure page quotes.
|
|
250
|
+
const publicErrors = new WeakSet();
|
|
251
|
+
|
|
252
|
+
/** Mark `error` as safe to show on the sign-in failure page. */
|
|
253
|
+
export function oauthPublicError(error) {
|
|
254
|
+
publicErrors.add(error);
|
|
255
|
+
return error;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A fetch failure as `<operation> request failed (<cause>). <hint>`; other errors unchanged. */
|
|
259
|
+
export function oauthRequestError(operation, error) {
|
|
260
|
+
const cause = oauthNetworkCause(error);
|
|
261
|
+
return cause ? oauthPublicError(new Error(`${operation} request failed (${cause}). ${OAUTH_NETWORK_HINT}`)) : error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A token endpoint's body for an error message: secrets redacted, whitespace folded, shortened. */
|
|
265
|
+
export function oauthBodySummary(text, secretValues = []) {
|
|
266
|
+
return scrubOAuthSecrets(text, secretValues).replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A token endpoint's HTTP failure as `<operation> <status>: <redacted body>`. */
|
|
270
|
+
export function oauthHttpError(operation, status, text, secretValues = []) {
|
|
271
|
+
const summary = oauthBodySummary(text, secretValues);
|
|
272
|
+
return oauthPublicError(new Error(`${operation} ${status}${summary ? `: ${summary}` : ''}`));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The token response's JSON. A body that does not parse is named by its
|
|
276
|
+
* status only: the parser's message quotes the body, tokens included. */
|
|
277
|
+
export async function oauthResponseJson(response, operation) {
|
|
278
|
+
try {
|
|
279
|
+
return await response.json();
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error?.name !== 'SyntaxError') throw oauthRequestError(operation, error);
|
|
282
|
+
throw oauthPublicError(new Error(`${operation} returned a malformed response (HTTP ${response.status})`));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The failure page reason: the summary, plus the error's message only when it is page-safe. */
|
|
287
|
+
export function oauthFailureReason(summary, error) {
|
|
288
|
+
return error && publicErrors.has(error) ? `${summary}\n${error.message}` : summary;
|
|
289
|
+
}
|
|
@@ -195,7 +195,7 @@ export class MimoOAuthProvider {
|
|
|
195
195
|
// publishes nothing. `_listRows` revalidates `seq` and the key before
|
|
196
196
|
// touching shared state, since enrichment runs after this returns.
|
|
197
197
|
let published = true;
|
|
198
|
-
if (items.length) published = publish(items.map(rosterRow));
|
|
198
|
+
if (items.length) published = await publish(items.map(rosterRow));
|
|
199
199
|
else items = seedModelItems();
|
|
200
200
|
return { items, published, seq, apiKey };
|
|
201
201
|
}
|