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
|
@@ -111,13 +111,25 @@ import {
|
|
|
111
111
|
providerAccountLimit,
|
|
112
112
|
providerAccountBlockedUntil,
|
|
113
113
|
ACCOUNT_PROVIDERS,
|
|
114
|
+
MAX_ACCOUNTS,
|
|
115
|
+
customProviderKeyIds,
|
|
116
|
+
setCustomProviderKeyIds,
|
|
114
117
|
} from '../../runtime/shared/provider-accounts.mjs';
|
|
118
|
+
import { customProviderKeyAccount } from '../../runtime/shared/custom-provider-keys.mjs';
|
|
115
119
|
import { currentProviderAccountId, withProviderAccount } from '../../runtime/shared/provider-auth-binding.mjs';
|
|
116
120
|
import {
|
|
117
121
|
CUSTOM_API_KEY_ENV_RE,
|
|
118
122
|
effectiveCustomModelRows,
|
|
119
123
|
normalizeCustomProviderConfig as normalizeStoredCustomConfig,
|
|
120
124
|
} from '../../runtime/agent/orchestrator/providers/custom-provider-config.mjs';
|
|
125
|
+
import {
|
|
126
|
+
VERTEX_ADC_MODE,
|
|
127
|
+
VERTEX_API_KEY_MODE,
|
|
128
|
+
describeVertexAdc,
|
|
129
|
+
parseVertexServiceAccount,
|
|
130
|
+
probeVertexMetadata,
|
|
131
|
+
} from '../../runtime/agent/orchestrator/providers/vertex-ai-auth.mjs';
|
|
132
|
+
import { vertexEnvLocation } from '../../runtime/agent/orchestrator/providers/vertex-ai-models.mjs';
|
|
121
133
|
|
|
122
134
|
const API_PROVIDERS = Object.freeze([
|
|
123
135
|
Object.freeze({ id: 'openrouter', name: 'OpenRouter', env: 'OPENROUTER_API_KEY', url: 'https://openrouter.ai/keys' }),
|
|
@@ -134,6 +146,7 @@ const API_PROVIDERS = Object.freeze([
|
|
|
134
146
|
url: 'https://console.anthropic.com/settings/keys',
|
|
135
147
|
}),
|
|
136
148
|
Object.freeze({ id: 'gemini', name: 'Gemini API', env: 'GEMINI_API_KEY', url: 'https://aistudio.google.com/apikey' }),
|
|
149
|
+
Object.freeze({ id: 'vertex-ai', name: 'Vertex AI', env: null, url: null }),
|
|
137
150
|
Object.freeze({
|
|
138
151
|
id: 'deepseek',
|
|
139
152
|
name: 'DeepSeek API',
|
|
@@ -374,6 +387,23 @@ function storedSecretDetail(account) {
|
|
|
374
387
|
return isFallbackSecret(account) ? 'stored in private file' : 'stored in keychain';
|
|
375
388
|
}
|
|
376
389
|
|
|
390
|
+
// The Vertex AI gcloud-login row's detail: who is signed in and where, or what to fix.
|
|
391
|
+
function vertexAdcDetail(adc, location) {
|
|
392
|
+
if (!adc) return `gcloud login · ${location}`;
|
|
393
|
+
if (adc.state !== 'ready') return adc.message;
|
|
394
|
+
const account = adc.account || (adc.type === 'metadata' ? 'metadata server' : 'gcloud user login');
|
|
395
|
+
return [account, adc.projectId || 'no project', location].join(' · ');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// The Vertex AI location a row shows and where it comes from: the saved one
|
|
399
|
+
// ('config'), else what the runtime would use unset ('environment' or
|
|
400
|
+
// 'default'), which the connect form offers as a hint without saving it.
|
|
401
|
+
function vertexRowLocation(configured) {
|
|
402
|
+
if (configured.location) return { location: String(configured.location), locationSource: 'config' };
|
|
403
|
+
const env = vertexEnvLocation();
|
|
404
|
+
return env ? { location: env, locationSource: 'environment' } : { location: 'global', locationSource: 'default' };
|
|
405
|
+
}
|
|
406
|
+
|
|
377
407
|
// `keychainIssue`: the setup response's one keyring check (never asked per row).
|
|
378
408
|
function apiProviderSetup(p, providers, checkSecrets, keychainIssue) {
|
|
379
409
|
const configured = providers[p.id] || {};
|
|
@@ -402,6 +432,74 @@ function apiProviderSetup(p, providers, checkSecrets, keychainIssue) {
|
|
|
402
432
|
}
|
|
403
433
|
const rowIssue = authenticated ? null : keychainIssue;
|
|
404
434
|
if (rowIssue) detail = rowIssue.message;
|
|
435
|
+
if (p.id === 'vertex-ai' && configured.authMode === VERTEX_ADC_MODE) {
|
|
436
|
+
// gcloud login: nothing is stored; connected while enabled and the login is readable.
|
|
437
|
+
const { location, locationSource } = vertexRowLocation(configured);
|
|
438
|
+
const adc = checkSecrets ? describeVertexAdc({ projectId: configured.projectId }) : null;
|
|
439
|
+
// Without a login file a metadata server may stand in: probed once, in the
|
|
440
|
+
// background, for the next refresh to report.
|
|
441
|
+
if (adc?.state === 'missing') void probeVertexMetadata();
|
|
442
|
+
const ready = configuredEnabled && (!adc || adc.state === 'ready');
|
|
443
|
+
let adcStatus = configuredEnabled ? 'Not connected' : 'Off';
|
|
444
|
+
if (ready) adcStatus = 'Set';
|
|
445
|
+
return {
|
|
446
|
+
...p,
|
|
447
|
+
group: 'api',
|
|
448
|
+
type: 'api-key',
|
|
449
|
+
authKind: 'adc',
|
|
450
|
+
authMode: VERTEX_ADC_MODE,
|
|
451
|
+
enabled: configuredEnabled,
|
|
452
|
+
authenticated: ready,
|
|
453
|
+
stored: false,
|
|
454
|
+
env: false,
|
|
455
|
+
envName: null,
|
|
456
|
+
status: adcStatus,
|
|
457
|
+
detail: vertexAdcDetail(adc, location),
|
|
458
|
+
location,
|
|
459
|
+
locationSource,
|
|
460
|
+
projectId: adc?.projectId || String(configured.projectId || '') || null,
|
|
461
|
+
...(adc ? { adc } : {}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (p.id === 'vertex-ai') {
|
|
465
|
+
const { location, locationSource } = vertexRowLocation(configured);
|
|
466
|
+
// An express-mode API key, or the service-account key.
|
|
467
|
+
const express = configured.authMode === VERTEX_API_KEY_MODE;
|
|
468
|
+
// The connect form offers the gcloud login too; its status line reads this.
|
|
469
|
+
const adc = checkSecrets && !authenticated ? describeVertexAdc({ projectId: configured.projectId }) : null;
|
|
470
|
+
let projectId = String(configured.projectId || '');
|
|
471
|
+
if (!projectId && authenticated && checkSecrets && !express) {
|
|
472
|
+
try {
|
|
473
|
+
projectId = parseVertexServiceAccount(getAgentApiKey(p.id)).projectId;
|
|
474
|
+
} catch {
|
|
475
|
+
// An unreadable key leaves the project id empty; the status line then shows the location only.
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!rowIssue) {
|
|
479
|
+
if (express) detail = `API key (express mode) · ${location}`;
|
|
480
|
+
else detail = projectId ? `${projectId} · ${location}` : `Location: ${location}`;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
...p,
|
|
484
|
+
...(rowIssue ? { keychainIssue: rowIssue } : {}),
|
|
485
|
+
group: 'api',
|
|
486
|
+
type: 'api-key',
|
|
487
|
+
authKind: express ? 'api-key' : 'service-account',
|
|
488
|
+
authMode: express ? VERTEX_API_KEY_MODE : 'service-account',
|
|
489
|
+
...(adc ? { adc } : {}),
|
|
490
|
+
enabled: configuredEnabled || authenticated,
|
|
491
|
+
authenticated,
|
|
492
|
+
stored,
|
|
493
|
+
env: false,
|
|
494
|
+
envName: null,
|
|
495
|
+
status,
|
|
496
|
+
detail,
|
|
497
|
+
location,
|
|
498
|
+
locationSource,
|
|
499
|
+
projectId: projectId || null,
|
|
500
|
+
...(credentialSource ? { credentialSource } : {}),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
405
503
|
return {
|
|
406
504
|
...p,
|
|
407
505
|
...(rowIssue ? { keychainIssue: rowIssue } : {}),
|
|
@@ -561,8 +659,12 @@ function customProviderRow(id, entry, checkSecrets = true) {
|
|
|
561
659
|
row.auth = keyless ? 'none' : 'key';
|
|
562
660
|
row.discovery = structuredClone(config?.discovery ?? entry.discovery ?? { newModelsHidden: false });
|
|
563
661
|
row.modelOverrides = structuredClone(config?.modelOverrides ?? entry.modelOverrides ?? {});
|
|
662
|
+
if (config?.headers) row.headers = { ...config.headers };
|
|
564
663
|
if (config?.wires) row.wires = [...config.wires];
|
|
565
664
|
if (config?.modelWires) row.modelWires = { ...config.modelWires };
|
|
665
|
+
// Additional API keys by id only; their values never leave the keychain.
|
|
666
|
+
const extraKeyIds = keyless ? [] : customProviderKeyIds(id);
|
|
667
|
+
if (extraKeyIds.length) row.extraApiKeys = extraKeyIds.map((keyId) => ({ id: keyId }));
|
|
566
668
|
// A stored config that does not normalize cannot run (the registry skips
|
|
567
669
|
// it): say so explicitly instead of listing it as connected.
|
|
568
670
|
// Every readiness flag consumers read says "not ready" (`stored` stays the
|
|
@@ -660,6 +762,20 @@ async function modelProxyProviderSetup(options) {
|
|
|
660
762
|
|
|
661
763
|
function apiProviderStatusRow(p, config) {
|
|
662
764
|
const configured = config.providers?.[p.id] || {};
|
|
765
|
+
if (p.id === 'vertex-ai' && configured.authMode === VERTEX_ADC_MODE) {
|
|
766
|
+
const adc = describeVertexAdc({ projectId: configured.projectId });
|
|
767
|
+
return {
|
|
768
|
+
id: p.id,
|
|
769
|
+
type: 'api-key',
|
|
770
|
+
enabled: configured.enabled === true,
|
|
771
|
+
authenticated: configured.enabled === true && adc.state === 'ready',
|
|
772
|
+
stored: false,
|
|
773
|
+
env: false,
|
|
774
|
+
envName: null,
|
|
775
|
+
label: p.name,
|
|
776
|
+
detail: vertexAdcDetail(adc, vertexRowLocation(configured).location),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
663
779
|
const envName = AGENT_PROVIDER_ENV[p.id] || p.env;
|
|
664
780
|
const env = Boolean(envName && process.env[envName]);
|
|
665
781
|
const stored = hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(p.id));
|
|
@@ -720,6 +836,7 @@ export function renderProviderStatus(config = {}) {
|
|
|
720
836
|
if (row.type === 'oauth') source = row.detail || 'oauth';
|
|
721
837
|
else if (row.env) source = `env:${row.envName}`;
|
|
722
838
|
else if (row.stored) source = 'keychain';
|
|
839
|
+
else if (row.detail) source = row.detail;
|
|
723
840
|
const enabled = row.enabled ? 'enabled' : 'disabled';
|
|
724
841
|
return `${row.id.padEnd(width)} ${row.type.padEnd(7)} ${auth.padEnd(8)} ${enabled.padEnd(8)} ${source}`;
|
|
725
842
|
})
|
|
@@ -984,15 +1101,68 @@ export async function updateProviderAccounts(provider, change) {
|
|
|
984
1101
|
return listProviderAccounts(provider);
|
|
985
1102
|
}
|
|
986
1103
|
|
|
987
|
-
export async function saveProviderApiKey(cfgMod, provider, secret) {
|
|
1104
|
+
export async function saveProviderApiKey(cfgMod, provider, secret, options) {
|
|
988
1105
|
requireLocalProviderManagement();
|
|
989
1106
|
const id = String(provider || '').trim();
|
|
990
1107
|
if (!API_PROVIDER_IDS.has(id)) throw new Error(`unknown API-key provider "${id}"`);
|
|
991
1108
|
const value = String(secret || '').trim();
|
|
992
|
-
|
|
1109
|
+
const adc = id === 'vertex-ai' && options?.authMode === VERTEX_ADC_MODE;
|
|
1110
|
+
const express = id === 'vertex-ai' && options?.authMode === VERTEX_API_KEY_MODE;
|
|
1111
|
+
if (!value && !adc) throw new Error(`API key is required for ${id}`);
|
|
1112
|
+
let patch = { enabled: true };
|
|
1113
|
+
if (id === 'vertex-ai') {
|
|
1114
|
+
if (adc && value) throw new Error('Vertex AI gcloud login takes no key');
|
|
1115
|
+
if (express && /[\s{]/.test(value)) throw new Error('Vertex AI express mode takes an API key, not a JSON key file');
|
|
1116
|
+
if (!adc && !express) parseVertexServiceAccount(value);
|
|
1117
|
+
// Only a location the user chose is saved: an empty one is unset (the
|
|
1118
|
+
// runtime then follows the environment's region, then global). None given
|
|
1119
|
+
// at all (the terminal setup) keeps a saved one; express mode never keeps
|
|
1120
|
+
// one, as it serves global only.
|
|
1121
|
+
const location = String(options?.location ?? '')
|
|
1122
|
+
.trim()
|
|
1123
|
+
.toLowerCase();
|
|
1124
|
+
const projectId = String(options?.projectId || '').trim();
|
|
1125
|
+
if (location && !/^[a-z0-9-]{1,63}$/.test(location)) throw new Error('Vertex AI location is invalid');
|
|
1126
|
+
if (express && location && location !== 'global') {
|
|
1127
|
+
throw new Error('Vertex AI express mode serves only the global location');
|
|
1128
|
+
}
|
|
1129
|
+
const locationPatch =
|
|
1130
|
+
location || express || options?.location !== undefined ? { location: location || undefined } : {};
|
|
1131
|
+
if (projectId && (!/^[a-zA-Z0-9._:-]{1,128}$/.test(projectId) || projectId.includes('/'))) {
|
|
1132
|
+
throw new Error('Vertex AI project ID override is invalid');
|
|
1133
|
+
}
|
|
1134
|
+
if (adc) {
|
|
1135
|
+
// gcloud login: nothing secret is stored, and the login must already be usable.
|
|
1136
|
+
let login = describeVertexAdc({ projectId });
|
|
1137
|
+
// No login file: a GCE / Cloud Run / GKE host's metadata server.
|
|
1138
|
+
if (login.state === 'missing' && (await probeVertexMetadata())) login = describeVertexAdc({ projectId });
|
|
1139
|
+
if (login.state !== 'ready') throw new Error(login.message);
|
|
1140
|
+
if (!login.projectId) {
|
|
1141
|
+
throw new Error(
|
|
1142
|
+
'No Google Cloud project found for the gcloud login. Enter a project ID override, or run `gcloud config set project PROJECT_ID`.'
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
// The login replaces a key saved earlier rather than sitting beside it.
|
|
1146
|
+
const account = SECRET_ACCOUNTS.agentApiKey(id);
|
|
1147
|
+
if (hasStoredSecret(account)) await deleteSecretAsync(account);
|
|
1148
|
+
updateConfigProvider(cfgMod, id, {
|
|
1149
|
+
enabled: true,
|
|
1150
|
+
...locationPatch,
|
|
1151
|
+
projectId: projectId || undefined,
|
|
1152
|
+
authMode: VERTEX_ADC_MODE,
|
|
1153
|
+
});
|
|
1154
|
+
return { provider: id, type: 'adc', authenticated: true };
|
|
1155
|
+
}
|
|
1156
|
+
patch = {
|
|
1157
|
+
enabled: true,
|
|
1158
|
+
...locationPatch,
|
|
1159
|
+
projectId: projectId || undefined,
|
|
1160
|
+
authMode: express ? VERTEX_API_KEY_MODE : 'service-account',
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
993
1163
|
await saveSecretAsync(SECRET_ACCOUNTS.agentApiKey(id), value);
|
|
994
|
-
updateConfigProvider(cfgMod, id,
|
|
995
|
-
return { provider: id, type: 'api-key', authenticated: true };
|
|
1164
|
+
updateConfigProvider(cfgMod, id, patch);
|
|
1165
|
+
return { provider: id, type: id === 'vertex-ai' && !express ? 'service-account' : 'api-key', authenticated: true };
|
|
996
1166
|
}
|
|
997
1167
|
|
|
998
1168
|
function sanitizeCustomError(error, secrets = []) {
|
|
@@ -1049,6 +1219,63 @@ function validateCustomTransportFields(input) {
|
|
|
1049
1219
|
if (typeof input.apiKey === 'string' && input.apiKey !== '')
|
|
1050
1220
|
throw new TypeError('custom provider apiKey and apiKeyEnv are mutually exclusive');
|
|
1051
1221
|
}
|
|
1222
|
+
if (input.extraApiKeys !== undefined) validateExtraApiKeys(input.extraApiKeys);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const CUSTOM_KEY_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1226
|
+
|
|
1227
|
+
// Additional API keys in use order, beside the primary key (MAX_ACCOUNTS in
|
|
1228
|
+
// all): `{ id }` keeps a saved key, `{ apiKey }` adds one and `{ id, apiKey }`
|
|
1229
|
+
// replaces a saved key's value; a blank `apiKey` keeps the saved key.
|
|
1230
|
+
function validateExtraApiKeys(rows) {
|
|
1231
|
+
const invalid = () => new TypeError('custom provider extraApiKeys are invalid');
|
|
1232
|
+
if (!Array.isArray(rows) || rows.length >= MAX_ACCOUNTS) throw invalid();
|
|
1233
|
+
const ids = new Set();
|
|
1234
|
+
for (const row of rows) {
|
|
1235
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw invalid();
|
|
1236
|
+
if (Object.keys(row).some((key) => key !== 'id' && key !== 'apiKey')) throw invalid();
|
|
1237
|
+
if (row.id !== undefined && (typeof row.id !== 'string' || !CUSTOM_KEY_ID_RE.test(row.id) || ids.has(row.id)))
|
|
1238
|
+
throw new TypeError('custom provider extraApiKeys id is invalid');
|
|
1239
|
+
if (row.apiKey !== undefined && (typeof row.apiKey !== 'string' || row.apiKey.length > CUSTOM_API_KEY_MAX))
|
|
1240
|
+
throw new TypeError('custom provider extraApiKeys apiKey is invalid');
|
|
1241
|
+
if (row.id === undefined && !row.apiKey?.trim()) throw new TypeError('custom provider additional API key is empty');
|
|
1242
|
+
if (row.id !== undefined) ids.add(row.id);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// The additional-key change a save makes over the `saved` key ids: `rows`
|
|
1247
|
+
// absent keeps them; a typed key is stored under a new id, so a replaced key
|
|
1248
|
+
// starts without the old one's cooldown. A keyless provider keeps none.
|
|
1249
|
+
function extraKeyPlan(saved, rows, keyless) {
|
|
1250
|
+
if (keyless) return { saved, ids: [], added: [], removed: saved };
|
|
1251
|
+
if (rows === undefined) return { saved, ids: saved, added: [], removed: [] };
|
|
1252
|
+
const ids = [];
|
|
1253
|
+
const added = [];
|
|
1254
|
+
for (const row of rows) {
|
|
1255
|
+
if (row.id !== undefined && !saved.includes(row.id)) throw new Error('An additional API key is no longer saved.');
|
|
1256
|
+
const typed = typeof row.apiKey === 'string' ? row.apiKey.trim() : '';
|
|
1257
|
+
if (!typed) {
|
|
1258
|
+
ids.push(row.id);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const keyId = randomUUID();
|
|
1262
|
+
ids.push(keyId);
|
|
1263
|
+
added.push({ keyId, key: typed });
|
|
1264
|
+
}
|
|
1265
|
+
return { saved, ids, added, removed: saved.filter((keyId) => !ids.includes(keyId)) };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const extraKeysChanged = (plan) => plan.ids.join() !== plan.saved.join();
|
|
1269
|
+
|
|
1270
|
+
async function applyExtraKeys(id, plan) {
|
|
1271
|
+
for (const { keyId, key } of plan.added) await saveSecretAsync(customProviderKeyAccount(id, keyId), key);
|
|
1272
|
+
if (extraKeysChanged(plan)) await setCustomProviderKeyIds(id, plan.ids);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// A failed save puts the saved roster back and drops the keys it added.
|
|
1276
|
+
async function revertExtraKeys(id, plan) {
|
|
1277
|
+
if (extraKeysChanged(plan)) await setCustomProviderKeyIds(id, plan.saved);
|
|
1278
|
+
for (const { keyId } of plan.added) await deleteSecretAsync(customProviderKeyAccount(id, keyId));
|
|
1052
1279
|
}
|
|
1053
1280
|
|
|
1054
1281
|
/** Whether the named environment variable is set to a non-empty value. */
|
|
@@ -1064,7 +1291,7 @@ function inputObject(input) {
|
|
|
1064
1291
|
}
|
|
1065
1292
|
|
|
1066
1293
|
// An edit that omits a stored setting keeps it: `auth` for every input shape,
|
|
1067
|
-
// and `discovery` / `modelOverrides` for a v2 input (a legacy `models` input
|
|
1294
|
+
// and `discovery` / `modelOverrides` / `headers` for a v2 input (a legacy `models` input
|
|
1068
1295
|
// states the whole manual model list itself).
|
|
1069
1296
|
function withStoredSettings(input, entry) {
|
|
1070
1297
|
const stored = storedCustomConfig(entry);
|
|
@@ -1079,6 +1306,8 @@ function withStoredSettings(input, entry) {
|
|
|
1079
1306
|
if (out.schemaVersion === stored.schemaVersion) {
|
|
1080
1307
|
if (out.discovery === undefined) out.discovery = stored.discovery;
|
|
1081
1308
|
if (out.modelOverrides === undefined) out.modelOverrides = stored.modelOverrides;
|
|
1309
|
+
// `headers: {}` clears them; an omitted field keeps the stored ones.
|
|
1310
|
+
if (out.headers === undefined && stored.headers) out.headers = stored.headers;
|
|
1082
1311
|
// Served wires and per-model wires have no setup field yet: an edit on
|
|
1083
1312
|
// the same protocol keeps them (another protocol serves other wires).
|
|
1084
1313
|
if ((out.protocol ?? stored.protocol) === stored.protocol) {
|
|
@@ -1095,7 +1324,9 @@ function withStoredSettings(input, entry) {
|
|
|
1095
1324
|
// the config. Synchronous, so a caller can merge and commit one snapshot.
|
|
1096
1325
|
function customInputAt(snapshot, input, normalizeCustomProviderConfig, options) {
|
|
1097
1326
|
const id = input.id === undefined ? null : input.id;
|
|
1098
|
-
|
|
1327
|
+
// Additional keys are secrets, applied by the save itself (extraKeyPlan).
|
|
1328
|
+
const { extraApiKeys: _extraKeys, ...settings } = input;
|
|
1329
|
+
const merged = id ? withStoredSettings(settings, existingCustomEntry(snapshot, id)) : settings;
|
|
1099
1330
|
const config = normalizeCustomProviderConfig(merged, options);
|
|
1100
1331
|
const typed = typeof merged.apiKey === 'string' ? merged.apiKey.trim() : '';
|
|
1101
1332
|
const keyless = config.auth === 'none';
|
|
@@ -1147,9 +1378,16 @@ export async function saveCustomProvider(cfgMod, rawInput) {
|
|
|
1147
1378
|
const commit = async () => {
|
|
1148
1379
|
const account = SECRET_ACCOUNTS.agentApiKey(id);
|
|
1149
1380
|
const previousKey = typed && existingId ? getAgentApiKey(id) : null;
|
|
1381
|
+
// Auth as of this save's turn in the key queue: a no-auth save queued
|
|
1382
|
+
// earlier may have landed since the input was resolved.
|
|
1383
|
+
const keyless = existingId
|
|
1384
|
+
? customInputAt(cfgMod.loadConfig(), rawInput, normalizeCustomProviderConfig, { requireModels: false }).keyless
|
|
1385
|
+
: resolved.keyless;
|
|
1386
|
+
const extra = extraKeyPlan(existingId ? customProviderKeyIds(id) : [], rawInput.extraApiKeys, keyless);
|
|
1150
1387
|
if (typed) await saveSecretAsync(account, typed);
|
|
1151
1388
|
let entry;
|
|
1152
1389
|
try {
|
|
1390
|
+
await applyExtraKeys(id, extra);
|
|
1153
1391
|
// Synchronous from here: omitted settings come from the config this
|
|
1154
1392
|
// save commits over, so an edit that landed during the awaits above (a
|
|
1155
1393
|
// model's visibility, a peer save, a removal) is never reverted.
|
|
@@ -1162,15 +1400,29 @@ export async function saveCustomProvider(cfgMod, rawInput) {
|
|
|
1162
1400
|
{ baseConfig: current }
|
|
1163
1401
|
);
|
|
1164
1402
|
} catch (error) {
|
|
1403
|
+
await revertExtraKeys(id, extra);
|
|
1165
1404
|
if (typed) {
|
|
1166
1405
|
if (previousKey) await saveSecretAsync(account, previousKey);
|
|
1167
1406
|
else await deleteSecretAsync(account);
|
|
1168
1407
|
}
|
|
1169
1408
|
throw error;
|
|
1170
1409
|
}
|
|
1410
|
+
// A removed key is dropped once the save holds; a keychain refusal then
|
|
1411
|
+
// leaves an unused secret behind (reported), never a failed save that did commit.
|
|
1412
|
+
for (const keyId of extra.removed) {
|
|
1413
|
+
await deleteSecretAsync(customProviderKeyAccount(id, keyId)).catch((error) => {
|
|
1414
|
+
process.stderr.write(
|
|
1415
|
+
`[provider-admin] could not delete additional API key ${keyId} of ${id}: ${error?.message || error}\n`
|
|
1416
|
+
);
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1171
1419
|
return customProviderRow(id, entry);
|
|
1172
1420
|
};
|
|
1173
|
-
|
|
1421
|
+
// Every save that may change the keys runs in the provider's key queue: a
|
|
1422
|
+
// typed key, an additional-key list, or a no-auth save (which drops them),
|
|
1423
|
+
// decided from the input alone, never from a roster read outside the queue.
|
|
1424
|
+
const keysChange = typed || rawInput.extraApiKeys !== undefined || (existingId && resolved.keyless);
|
|
1425
|
+
return keysChange ? serializeCustomKeySave(id, commit) : commit();
|
|
1174
1426
|
}
|
|
1175
1427
|
|
|
1176
1428
|
// Replaces only the stored key of an existing custom provider. The provider's
|
|
@@ -1399,6 +1651,9 @@ export async function removeCustomProvider(cfgMod, rawId) {
|
|
|
1399
1651
|
// provider registered so the removal can be retried. A missing secret
|
|
1400
1652
|
// (keyless provider) is not an error.
|
|
1401
1653
|
await deleteSecretAsync(SECRET_ACCOUNTS.agentApiKey(id));
|
|
1654
|
+
const extraKeyIds = customProviderKeyIds(id);
|
|
1655
|
+
for (const keyId of extraKeyIds) await deleteSecretAsync(customProviderKeyAccount(id, keyId));
|
|
1656
|
+
if (extraKeyIds.length) await setCustomProviderKeyIds(id, []);
|
|
1402
1657
|
const current = cfgMod.loadConfig();
|
|
1403
1658
|
const { [id]: _removed, ...providers } = current.providers || {};
|
|
1404
1659
|
cfgMod.saveConfig(withoutProviderReferences({ ...current, providers }, id), { baseConfig: current });
|
|
@@ -1425,13 +1680,29 @@ export async function testCustomProvider(cfgMod, rawInput) {
|
|
|
1425
1680
|
error: { kind: 'empty', message: 'No models were found for this provider' },
|
|
1426
1681
|
};
|
|
1427
1682
|
const { createCustomProvider } = await loadCustomProviderModule();
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1683
|
+
const primary = await createCustomProvider(id || 'custom-test', { ...config, apiKey });
|
|
1684
|
+
// A provider with additional keys tests through the same key pool as a
|
|
1685
|
+
// real request: an explicit quota refusal moves on to the next key.
|
|
1686
|
+
const keyIds = id && !config.apiKeyEnv && config.auth !== 'none' ? customProviderKeyIds(id) : [];
|
|
1687
|
+
let provider = primary;
|
|
1688
|
+
let routingKey;
|
|
1689
|
+
if (keyIds.length) {
|
|
1690
|
+
const [pool, keys] = await Promise.all([
|
|
1691
|
+
import('../../runtime/agent/orchestrator/providers/account-pool.mjs'),
|
|
1692
|
+
import('../../runtime/agent/orchestrator/providers/custom-provider-keys.mjs'),
|
|
1693
|
+
]);
|
|
1694
|
+
routingKey = pool.ROUTING_METADATA_KEY;
|
|
1695
|
+
provider = pool.createCustomKeyPoolProvider(id, primary, (keyId) => keys.customKeyEngine(primary, id, keyId));
|
|
1696
|
+
}
|
|
1697
|
+
const answer = await withTimeout(
|
|
1430
1698
|
(signal) => provider.send([{ role: 'user', content: 'Reply with OK.' }], model, [], { signal, maxTokens: 16 }),
|
|
1431
1699
|
CUSTOM_TEST_TIMEOUT_MS,
|
|
1432
1700
|
'connection test'
|
|
1433
1701
|
);
|
|
1434
|
-
|
|
1702
|
+
// Key numbers count the primary key as 1.
|
|
1703
|
+
const served = routingKey ? answer?.providerMetadata?.[routingKey]?.accountId : undefined;
|
|
1704
|
+
const index = keyIds.indexOf(served);
|
|
1705
|
+
return index < 0 ? { ok: true } : { ok: true, key: { id: served, number: index + 2 } };
|
|
1435
1706
|
} catch (error) {
|
|
1436
1707
|
throw sanitizeCustomError(error, customSecrets(apiKey, config));
|
|
1437
1708
|
}
|
|
@@ -36,6 +36,8 @@ export function createSessionTurnApi(deps) {
|
|
|
36
36
|
agentControl: agent.agentControl,
|
|
37
37
|
taskControl: agent.taskControl,
|
|
38
38
|
stopShellJob: agent.stopShellJob,
|
|
39
|
+
listStoppableShellJobs: agent.listStoppableShellJobs,
|
|
40
|
+
stopShellJobs: agent.stopShellJobs,
|
|
39
41
|
releaseShellJob: agent.releaseShellJob,
|
|
40
42
|
listShellJobs: agent.listShellJobs,
|
|
41
43
|
onNotification: agent.onNotification,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
setBuiltinInstalledInConfig,
|
|
9
9
|
} from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
|
|
10
10
|
import { featureEnvOverride, moduleEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
|
|
11
|
+
import { noteCacheWarmingEnabled } from '../runtime/agent/orchestrator/session/cache-warm.mjs';
|
|
11
12
|
import { normalizeAgentAvatar } from '../runtime/shared/agent-avatar.mjs';
|
|
12
13
|
import { LOCAL_PROVIDER_ID } from '../runtime/local-provider/managed-runtime.mjs';
|
|
13
14
|
import { INSTRUCTION_FILES_MODES, normalizeInstructionFilesMode } from '../runtime/shared/instruction-files-mode.mjs';
|
|
@@ -100,11 +101,13 @@ export function createBuiltinToolSettings(
|
|
|
100
101
|
// default on. The warmer reads the saved value at each
|
|
101
102
|
// warm, so a change applies without a restart.
|
|
102
103
|
getCacheWarmingSettings() {
|
|
103
|
-
return { enabled: getConfig()?.promptCacheWarming
|
|
104
|
+
return { enabled: getConfig()?.promptCacheWarming === true };
|
|
104
105
|
},
|
|
105
106
|
setCacheWarmingEnabled(enabled) {
|
|
106
107
|
if (typeof enabled !== 'boolean') throw new TypeError('cache warming must be on or off (a boolean)');
|
|
107
108
|
saveConfigAndAdopt({ ...getConfig(), promptCacheWarming: enabled });
|
|
109
|
+
// The warmer's synchronous state follows at once (off cancels chains).
|
|
110
|
+
noteCacheWarmingEnabled(enabled);
|
|
108
111
|
return this.getCacheWarmingSettings();
|
|
109
112
|
},
|
|
110
113
|
// The one pixel face every Lead row on the bot page wears, shared by every
|
|
@@ -57,7 +57,7 @@ export function createSystemSettings({
|
|
|
57
57
|
},
|
|
58
58
|
getHeartbeatSettings() {
|
|
59
59
|
return {
|
|
60
|
-
heartbeat: getConfig()?.heartbeat
|
|
60
|
+
heartbeat: getConfig()?.heartbeat === true,
|
|
61
61
|
intervalMinutes: normalizeHeartbeatIntervalMinutes(getConfig()?.heartbeatIntervalMinutes),
|
|
62
62
|
};
|
|
63
63
|
},
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
* prompt-cache warming. Each goes through the runtime method its settings
|
|
4
4
|
* surface uses. Permission and connector writes are made as a remote client's
|
|
5
5
|
* are (`noWiden`): a chat may take authority away, never give an agent more
|
|
6
|
-
* without asking — that stays with the person in the desktop app
|
|
6
|
+
* without asking — that stays with the person: in the desktop app, or on
|
|
7
|
+
* the approval card a connector tool's widening raises
|
|
8
|
+
* (connector-widen-approval.mjs). */
|
|
9
|
+
import { CONNECTOR_POLICY_WIDENS } from '../../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
|
|
7
10
|
import { agentPermissions, setAgentPermission } from '../../runtime/agent/orchestrator/session/human-gate/store.mjs';
|
|
11
|
+
import { askToWidenConnectorTool } from './connector-widen-approval.mjs';
|
|
8
12
|
import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
|
|
9
13
|
|
|
10
14
|
const NO_WIDEN = Object.freeze({ noWiden: true });
|
|
@@ -38,11 +42,17 @@ export const AGENT_SETTINGS_HANDLERS = {
|
|
|
38
42
|
const name = requireText(args.name, 'name');
|
|
39
43
|
return { agent, connectors: await rt.setAgentConnectorEnabled(agent, name, args.enabled, NO_WIDEN) };
|
|
40
44
|
},
|
|
41
|
-
|
|
45
|
+
// A narrowing applies at once; a widening waits on the person's card.
|
|
46
|
+
async set_agent_connector_tool(rt, args, context) {
|
|
42
47
|
const agent = requireText(args.agent, 'agent');
|
|
43
48
|
const name = requireText(args.name, 'name');
|
|
44
49
|
const tool = requireText(args.tool, 'tool');
|
|
45
|
-
|
|
50
|
+
try {
|
|
51
|
+
await rt.setAgentConnectorToolPermission(agent, name, tool, args.permission, NO_WIDEN);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code !== CONNECTOR_POLICY_WIDENS) throw error;
|
|
54
|
+
return askToWidenConnectorTool(rt, { agent, server: name, tool, requested: args.permission }, context);
|
|
55
|
+
}
|
|
46
56
|
return { agent, connector: name, tool, permission: args.permission };
|
|
47
57
|
},
|
|
48
58
|
async set_project_connector(rt, args) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** A bot's request for more authority on one connector tool (setup
|
|
2
|
+
* set_agent_connector_tool toward auto or needs_approval): the model cannot
|
|
3
|
+
* tell the user's words from text it read, so the change waits on an
|
|
4
|
+
* approval card (human-gate/requests.mjs kind `connector`) that only a
|
|
5
|
+
* person answers. The card offers each level above the tool's current one
|
|
6
|
+
* (needs_approval, auto) and Deny; nothing is remembered, so every widening
|
|
7
|
+
* asks again. The picked level is applied to that one tool, re-judged on the
|
|
8
|
+
* stored policy inside its write (mcp/agent-connectors.mjs
|
|
9
|
+
* withApprovedConnectorToolChange): a restriction that landed meanwhile stays. */
|
|
10
|
+
import {
|
|
11
|
+
CONNECTOR_TOOL_ALREADY,
|
|
12
|
+
CONNECTOR_TOOL_NARROWED,
|
|
13
|
+
NO_WIDEN_MESSAGE,
|
|
14
|
+
PERMISSION_RANK,
|
|
15
|
+
agentConnectors,
|
|
16
|
+
connectorGroupId,
|
|
17
|
+
connectorToolPermission,
|
|
18
|
+
withApprovedConnectorToolChange,
|
|
19
|
+
} from '../../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
|
|
20
|
+
import { recordHumanDecision } from '../../runtime/agent/orchestrator/session/human-gate/gate.mjs';
|
|
21
|
+
import { CONNECTOR_WIDEN_LEVELS } from '../../runtime/agent/orchestrator/session/human-gate/requests.mjs';
|
|
22
|
+
import { runAbortable } from '../../runtime/shared/abort-race.mjs';
|
|
23
|
+
|
|
24
|
+
const NO_WIDEN = Object.freeze({ noWiden: true });
|
|
25
|
+
|
|
26
|
+
/** The tool's effective level for `agent` now, as dispatch reads it. */
|
|
27
|
+
function currentLevel(agent, server, tool) {
|
|
28
|
+
return connectorToolPermission(agentConnectors(agent), {
|
|
29
|
+
name: `mcp__${server}__${tool}`,
|
|
30
|
+
mcpServerName: server,
|
|
31
|
+
mcpToolName: tool,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Ask the person on a card, then apply the level they picked.
|
|
36
|
+
* `requestApproval` is the calling turn's approval hook. */
|
|
37
|
+
export async function askToWidenConnectorTool(
|
|
38
|
+
rt,
|
|
39
|
+
{ agent, server, tool, requested },
|
|
40
|
+
{ requestApproval, signal, sessionId, toolCallId } = {}
|
|
41
|
+
) {
|
|
42
|
+
const from = currentLevel(agent, server, tool);
|
|
43
|
+
const receipt = { agent, connector: server, tool, requested };
|
|
44
|
+
if (PERMISSION_RANK[from] >= PERMISSION_RANK[requested]) {
|
|
45
|
+
return { ...receipt, saved: false, permission: from, note: `The tool is already ${from}; nothing changed.` };
|
|
46
|
+
}
|
|
47
|
+
// Nobody can be shown a card here: the person makes it in the desktop app.
|
|
48
|
+
if (typeof requestApproval !== 'function') throw new Error(NO_WIDEN_MESSAGE);
|
|
49
|
+
const choices = CONNECTOR_WIDEN_LEVELS.filter((level) => PERMISSION_RANK[level] > PERMISSION_RANK[from]);
|
|
50
|
+
const answer = await runAbortable(signal, () =>
|
|
51
|
+
requestApproval({
|
|
52
|
+
name: 'setup',
|
|
53
|
+
kind: 'connector',
|
|
54
|
+
agentId: agent,
|
|
55
|
+
// The card names the connector, not one of its accounts.
|
|
56
|
+
connector: { server: connectorGroupId(server), tool, from, requested, choices },
|
|
57
|
+
args: { action: 'set_agent_connector_tool', agent, name: server, tool, permission: requested },
|
|
58
|
+
reason: `Bot "${agent}" asks to change the permission of ${tool} (${connectorGroupId(server)}) from ${from} to ${requested}.`,
|
|
59
|
+
remember: ['once'],
|
|
60
|
+
sessionId,
|
|
61
|
+
toolCallId,
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
const level = answer?.approved === true && choices.includes(answer.scope) ? answer.scope : '';
|
|
65
|
+
// Audited with the answering device; no grant is ever stored (no action kind).
|
|
66
|
+
await recordHumanDecision(
|
|
67
|
+
{ agentId: agent, kind: 'connector', tool: `${server}/${tool}`, target: `${from}>${level || requested}` },
|
|
68
|
+
answer
|
|
69
|
+
);
|
|
70
|
+
// A decision that settles after its turn was cancelled authorizes nothing.
|
|
71
|
+
signal?.throwIfAborted();
|
|
72
|
+
if (!level) {
|
|
73
|
+
const why = answer?.timedOut ? 'The approval card timed out' : 'The user did not approve this change';
|
|
74
|
+
return { ...receipt, saved: false, approved: false, permission: from, note: `${why}; nothing changed.` };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await withApprovedConnectorToolChange({ server, tool, from, to: level }, () =>
|
|
78
|
+
rt.setAgentConnectorToolPermission(agent, server, tool, level, NO_WIDEN)
|
|
79
|
+
);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code !== CONNECTOR_TOOL_NARROWED && error?.code !== CONNECTOR_TOOL_ALREADY) throw error;
|
|
82
|
+
return {
|
|
83
|
+
...receipt,
|
|
84
|
+
saved: false,
|
|
85
|
+
approved: true,
|
|
86
|
+
permission: currentLevel(agent, server, tool),
|
|
87
|
+
note: `The user approved ${level}, but it was not applied: ${error.message}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...receipt,
|
|
92
|
+
approved: true,
|
|
93
|
+
permission: level,
|
|
94
|
+
note: `The user approved on the card; ${tool} is now ${level}.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -364,7 +364,7 @@ async function featureStatus(rt, getConfig) {
|
|
|
364
364
|
computer: { active: builtinFeatureActive(config, 'computer') },
|
|
365
365
|
terminal: { active: builtinFeatureActive(config, 'terminal') },
|
|
366
366
|
heartbeat: {
|
|
367
|
-
enabled: config?.heartbeat
|
|
367
|
+
enabled: config?.heartbeat === true,
|
|
368
368
|
intervalMinutes: normalizeHeartbeatIntervalMinutes(config?.heartbeatIntervalMinutes),
|
|
369
369
|
},
|
|
370
370
|
reactions: { enabled: config?.reactions !== false },
|
|
@@ -909,7 +909,7 @@ export function createSetupToolExecutor({
|
|
|
909
909
|
};
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
async function execute(args, { signal, callerSessionId }) {
|
|
912
|
+
async function execute(args, { signal, callerSessionId, requestApproval = null, toolCallId = null }) {
|
|
913
913
|
signal?.throwIfAborted();
|
|
914
914
|
const requestDesktop = (request) => desktop.request(request, { signal });
|
|
915
915
|
const action = validateSetupInput(args);
|
|
@@ -948,6 +948,10 @@ export function createSetupToolExecutor({
|
|
|
948
948
|
getSession: getCallerSession,
|
|
949
949
|
getConfig: () => getConfig?.() || {},
|
|
950
950
|
executeSessionControl,
|
|
951
|
+
// The calling turn's approval card (a connector tool's widening asks the person).
|
|
952
|
+
requestApproval,
|
|
953
|
+
signal,
|
|
954
|
+
toolCallId,
|
|
951
955
|
});
|
|
952
956
|
}
|
|
953
957
|
|
|
@@ -477,7 +477,8 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
477
477
|
permission: {
|
|
478
478
|
type: 'string',
|
|
479
479
|
enum: [...CONNECTOR_PERMISSIONS],
|
|
480
|
-
description:
|
|
480
|
+
description:
|
|
481
|
+
'set_agent_connector_tool: a narrowing (toward disabled) applies at once; a widening (disabled → needs_approval/auto, needs_approval → auto) waits on an approval card the user answers (needs_approval, auto or deny, whatever was asked) and the result says what was applied.',
|
|
481
482
|
},
|
|
482
483
|
botOrder: {
|
|
483
484
|
type: 'array',
|