mixdog 1.2.6 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +71 -9
- package/package.json +4 -1
- package/scripts/check-model-recommendations.mjs +89 -0
- package/scripts/gate-local.mjs +7 -0
- package/scripts/lib/isolated-test-env.mjs +16 -0
- package/src/agents/reviewer/AGENT.md +4 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/browser-use/SKILL.md +5 -0
- package/src/defaults/skills/memory-management/SKILL.md +4 -1
- package/src/defaults/skills/setup/references/actions.md +14 -4
- package/src/defaults/skills/setup-guide/SKILL.md +178 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent/AGENT.md +4 -0
- package/src/rules/agent-mode/AGENT_MODE.md +24 -0
- package/src/rules/agent-mode/MIXY.md +28 -0
- package/src/rules/agent-mode/ROUTINE.md +6 -0
- package/src/rules/lead/LEAD.md +7 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
- package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
- package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
- package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/browser-bridge/client.mjs +24 -0
- package/src/runtime/channels/lib/scheduler.mjs +40 -12
- package/src/runtime/channels/lib/schedules-db.mjs +71 -12
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
- package/src/runtime/channels/lib/webhook.mjs +0 -1
- package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
- package/src/runtime/channels/lib/worker-main.mjs +17 -20
- package/src/runtime/computer-bridge/client.mjs +28 -1
- package/src/runtime/connectors/gmail-app-password.mjs +690 -0
- package/src/runtime/connectors/google-mcp.mjs +446 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
- package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
- package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
- package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
- package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
- package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
- package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
- package/src/runtime/memory/tool-defs.mjs +5 -0
- package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
- package/src/runtime/shared/agent-avatar.d.mts +64 -0
- package/src/runtime/shared/agent-avatar.mjs +320 -0
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +4 -0
- package/src/runtime/shared/agent-route-config.mjs +30 -0
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/atomic-file.mjs +14 -1
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/config.mjs +8 -1
- package/src/runtime/shared/connector-accounts.mjs +15 -0
- package/src/runtime/shared/connector-catalog.d.mts +51 -0
- package/src/runtime/shared/connector-catalog.mjs +228 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +45 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/google-oauth-client.mjs +24 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/held-request-host.d.mts +12 -0
- package/src/runtime/shared/held-request-host.mjs +71 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +33 -0
- package/src/runtime/shared/routine-limits.d.mts +11 -0
- package/src/runtime/shared/routine-limits.mjs +21 -0
- package/src/runtime/shared/routine-trigger.d.mts +10 -0
- package/src/runtime/shared/routine-trigger.mjs +44 -0
- package/src/runtime/shared/schedule-model-ref.mjs +9 -10
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/slow-span.mjs +4 -12
- package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
- package/src/runtime/shared/transcript-metadata.mjs +10 -0
- package/src/runtime/shared/turn-changes.mjs +0 -8
- package/src/runtime/shared/usage-readout-totals.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +119 -38
- package/src/session-runtime/agent-home-delivery.mjs +826 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +133 -0
- package/src/session-runtime/agent-task-sessions.mjs +460 -0
- package/src/session-runtime/boot/apis.mjs +63 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/held-requests.mjs +228 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
- package/src/session-runtime/lifecycle/shared.mjs +6 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
- package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +112 -15
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +35 -4
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/owned-automation-run.mjs +229 -0
- package/src/session-runtime/resource-api.mjs +5 -0
- package/src/session-runtime/resource-mcp-api.mjs +107 -13
- package/src/session-runtime/resource-skills-api.mjs +12 -0
- package/src/session-runtime/room-member-gate.mjs +48 -0
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/routine-home-turn.mjs +156 -0
- package/src/session-runtime/routine-owner.mjs +48 -0
- package/src/session-runtime/schedule-session-run.mjs +30 -64
- package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/channel-admin.mjs +93 -24
- package/src/session-runtime/services/memory-access.mjs +37 -0
- package/src/session-runtime/services/provider-admin.mjs +74 -39
- package/src/session-runtime/services/schedule-access.mjs +91 -0
- package/src/session-runtime/session-create/session-options.mjs +34 -7
- package/src/session-runtime/session-hooks.mjs +16 -8
- package/src/session-runtime/setup-tool/executor.mjs +79 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
- package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/skills-api.mjs +16 -1
- package/src/session-runtime/tool-policy-refresh.mjs +249 -29
- package/src/session-runtime/turn/session-ops.mjs +8 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
- package/src/session-runtime/webhook-session-run.mjs +49 -67
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
- package/src/session-runtime/workflow-agents-api.mjs +4 -1
- package/src/standalone/daemon-agent-control.mjs +1 -8
- package/src/standalone/daemon-session-control.mjs +21 -4
- package/src/standalone/daemon.mjs +31 -5
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +234 -95
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +34 -4
- package/src/tui/session/queue-helpers.mjs +2 -1
- package/src/tui/session/session-api/extensions.mjs +4 -0
- package/src/tui/session/session-api/intake/submission.mjs +5 -0
- package/src/tui/session/session-api/integrations.mjs +8 -0
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
- package/src/tui/session/session-api/lifecycle.mjs +17 -1
- package/src/tui/session/session-api/routes.mjs +61 -5
- package/src/tui/session/session-flow/drain.mjs +12 -3
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
- package/src/tui/session/session-flow/steering.mjs +58 -24
- package/src/tui/session/tool-approval.mjs +26 -5
- package/src/tui/session/transcript-intake.mjs +8 -2
- package/src/tui/session/transcript-spill.mjs +5 -1
- package/src/tui/session/tui-steering-persist.mjs +65 -42
- package/src/tui/session/turn-live-call.mjs +64 -0
- package/src/tui/session/turn.mjs +34 -8
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// session/human-gate/requests.mjs
|
|
2
|
+
// The data model of a human-in-the-loop prompt. A request has one `kind`:
|
|
3
|
+
// approval — allow or deny one tool call;
|
|
4
|
+
// outbound — a call that posts, sends, pays or deletes outside (its action
|
|
5
|
+
// kind, target, account, the exact payload text, which fields
|
|
6
|
+
// the card may edit, and the mail/payment/delete detail lines);
|
|
7
|
+
// secret — the user types a value into a target field; the request names
|
|
8
|
+
// the target and the field label, never a value;
|
|
9
|
+
// takeover — the user does a step themselves and says "done";
|
|
10
|
+
// permission — the user picks one of the offered scopes.
|
|
11
|
+
// A response is one of HUMAN_RESPONSES plus a `remember` scope; a policy lists
|
|
12
|
+
// the scopes it allows and can leave out `always`. Nothing here ever carries a
|
|
13
|
+
// secret value: a decision that names one is stripped to its verdict.
|
|
14
|
+
|
|
15
|
+
export const HUMAN_REQUEST_KINDS = Object.freeze(['approval', 'outbound', 'secret', 'takeover', 'permission']);
|
|
16
|
+
export const HUMAN_RESPONSES = Object.freeze(['approve', 'deny', 'edit-and-approve', 'provide-secret', 'skip', 'done']);
|
|
17
|
+
export const REMEMBER_SCOPES = Object.freeze(['once', 'agent', 'always']);
|
|
18
|
+
export const SECRET_REQUEST_TIMEOUT_MS = 120_000;
|
|
19
|
+
|
|
20
|
+
const RESPONSES_BY_KIND = Object.freeze({
|
|
21
|
+
approval: ['approve', 'deny'],
|
|
22
|
+
outbound: ['approve', 'edit-and-approve', 'deny'],
|
|
23
|
+
secret: ['provide-secret', 'skip'],
|
|
24
|
+
takeover: ['done', 'deny'],
|
|
25
|
+
permission: ['approve', 'deny'],
|
|
26
|
+
});
|
|
27
|
+
const GRANTING_RESPONSES = new Set(['approve', 'edit-and-approve', 'provide-secret', 'done']);
|
|
28
|
+
const TEXT_LIMIT = 200;
|
|
29
|
+
/** An outbound card's extra lines: mail headers, the payment, the delete, the
|
|
30
|
+
* trash card's message list (gate.mjs describeEmails) and its remainder. */
|
|
31
|
+
const DETAIL_FIELDS = [
|
|
32
|
+
'to',
|
|
33
|
+
'cc',
|
|
34
|
+
'subject',
|
|
35
|
+
'merchant',
|
|
36
|
+
'amount',
|
|
37
|
+
'currency',
|
|
38
|
+
'recurrence',
|
|
39
|
+
'count',
|
|
40
|
+
'size',
|
|
41
|
+
'messages',
|
|
42
|
+
'more',
|
|
43
|
+
];
|
|
44
|
+
const DETAIL_LIMITS = { messages: 1_000 };
|
|
45
|
+
const PAYLOAD_LIMIT = 40_000;
|
|
46
|
+
|
|
47
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
48
|
+
const text = (value, limit = TEXT_LIMIT) => (typeof value === 'string' ? value.trim().slice(0, limit) : '');
|
|
49
|
+
|
|
50
|
+
/** The human kind of a request; anything else (ask_user's 'question', a
|
|
51
|
+
* hook's bare ask) is an ordinary approval for responses. */
|
|
52
|
+
export function humanRequestKind(request) {
|
|
53
|
+
return HUMAN_REQUEST_KINDS.includes(request?.kind) ? request.kind : 'approval';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A login takeover's sign-in page: https, or http on loopback only (a local
|
|
57
|
+
* site); never embedded credentials. `{ url, origin }`, or null. */
|
|
58
|
+
export function loginPageUrl(value) {
|
|
59
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 2048) return null;
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = new URL(value.trim());
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
67
|
+
const loopback =
|
|
68
|
+
host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
69
|
+
if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback)) return null;
|
|
70
|
+
if (parsed.username || parsed.password) return null;
|
|
71
|
+
return { url: parsed.href, origin: parsed.origin };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rememberScopes(value) {
|
|
75
|
+
const listed = Array.isArray(value) ? value.filter((scope) => REMEMBER_SCOPES.includes(scope)) : [];
|
|
76
|
+
return ['once', ...new Set(listed.filter((scope) => scope !== 'once'))];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The kind-specific fields of a request, whitelisted. Spread onto the
|
|
80
|
+
* presented request; `args` is replaced for a secret so no value rides it. */
|
|
81
|
+
export function humanRequestFields(input = {}) {
|
|
82
|
+
const kind = input?.kind;
|
|
83
|
+
if (!HUMAN_REQUEST_KINDS.includes(kind)) return {};
|
|
84
|
+
const fields = { remember: rememberScopes(input.remember) };
|
|
85
|
+
// The agent the request acts for (its name on the card and the remembered grant).
|
|
86
|
+
if (text(input.agentId)) fields.agentId = text(input.agentId);
|
|
87
|
+
if (kind === 'outbound' && isRecord(input.outbound)) {
|
|
88
|
+
const outbound = input.outbound;
|
|
89
|
+
const details = isRecord(outbound.details) ? outbound.details : {};
|
|
90
|
+
fields.outbound = {
|
|
91
|
+
category: text(outbound.category),
|
|
92
|
+
target: text(outbound.target),
|
|
93
|
+
account: text(outbound.account),
|
|
94
|
+
payload: typeof outbound.payload === 'string' ? outbound.payload.slice(0, PAYLOAD_LIMIT) : '',
|
|
95
|
+
editable: Array.isArray(outbound.editable) ? outbound.editable.filter((name) => typeof name === 'string') : [],
|
|
96
|
+
details: Object.fromEntries(
|
|
97
|
+
DETAIL_FIELDS.filter((name) => text(details[name])).map((name) => [
|
|
98
|
+
name,
|
|
99
|
+
text(details[name], DETAIL_LIMITS[name]),
|
|
100
|
+
])
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (kind === 'secret') {
|
|
105
|
+
const secret = isRecord(input.secret) ? input.secret : {};
|
|
106
|
+
fields.secret = {
|
|
107
|
+
requestId: text(secret.requestId),
|
|
108
|
+
surface: secret.surface === 'computer' ? 'computer' : 'browser',
|
|
109
|
+
target: text(secret.target),
|
|
110
|
+
field: text(secret.field),
|
|
111
|
+
};
|
|
112
|
+
fields.args = { surface: fields.secret.surface, target: fields.secret.target, field: fields.secret.field };
|
|
113
|
+
fields.remember = ['once'];
|
|
114
|
+
}
|
|
115
|
+
if (kind === 'takeover') {
|
|
116
|
+
const takeover = isRecord(input.takeover) ? input.takeover : {};
|
|
117
|
+
const surface = takeover.surface === 'computer' ? 'computer' : 'browser';
|
|
118
|
+
// The window it is about (ask-user-tool-defs.mjs handoffIdentity): a
|
|
119
|
+
// browser page ID, or a computer window id and app, so the card can bring
|
|
120
|
+
// exactly that window forward on the user's click.
|
|
121
|
+
const token = (value, limit) => (/^[\w.:#@-]+$/.test(text(value, limit)) ? text(value, limit) : '');
|
|
122
|
+
const identity =
|
|
123
|
+
surface === 'browser'
|
|
124
|
+
? { ...(token(takeover.tab, 64) ? { tab: token(takeover.tab, 64) } : {}) }
|
|
125
|
+
: {
|
|
126
|
+
...(token(takeover.windowId, 128) ? { windowId: token(takeover.windowId, 128) } : {}),
|
|
127
|
+
...(text(takeover.app, 120) ? { app: text(takeover.app, 120) } : {}),
|
|
128
|
+
};
|
|
129
|
+
// A login takeover (purpose 'login') opens the desktop's sign-in window on
|
|
130
|
+
// its validated page instead of handing over the side panel.
|
|
131
|
+
const login = surface === 'browser' && takeover.purpose === 'login' ? loginPageUrl(takeover.url) : null;
|
|
132
|
+
fields.takeover = {
|
|
133
|
+
surface,
|
|
134
|
+
instructions: text(takeover.instructions, 2_000),
|
|
135
|
+
...(text(takeover.target) ? { target: text(takeover.target) } : {}),
|
|
136
|
+
...identity,
|
|
137
|
+
...(login ? { purpose: 'login', url: login.url, origin: login.origin } : {}),
|
|
138
|
+
};
|
|
139
|
+
fields.remember = ['once'];
|
|
140
|
+
}
|
|
141
|
+
if (kind === 'permission') {
|
|
142
|
+
const scopes = Array.isArray(input.permission?.scopes) ? input.permission.scopes : [];
|
|
143
|
+
fields.permission = {
|
|
144
|
+
scopes: scopes
|
|
145
|
+
.filter((scope) => isRecord(scope) && text(scope.id))
|
|
146
|
+
.map((scope) => ({ id: text(scope.id), label: text(scope.label) || text(scope.id) })),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return fields;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Only `editable` string fields of `editedArgs`; {} when nothing applies. */
|
|
153
|
+
function editableArgs(editedArgs, editable) {
|
|
154
|
+
if (!isRecord(editedArgs)) return {};
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const name of editable || []) {
|
|
157
|
+
if (typeof editedArgs[name] === 'string') out[name] = editedArgs[name].slice(0, PAYLOAD_LIMIT);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The canonical response to `request` from a decision: `approved` (the card's
|
|
164
|
+
* polarity) plus optional `{ response, editedArgs, remember, scope }`. An
|
|
165
|
+
* unknown or contradicting response falls back to the polarity's default;
|
|
166
|
+
* edits survive only on an outbound request and only for its editable fields;
|
|
167
|
+
* `remember` only when the request allows it and the call was granted.
|
|
168
|
+
*/
|
|
169
|
+
export function normalizeHumanResponse(request, approved, extra = {}) {
|
|
170
|
+
const kind = humanRequestKind(request);
|
|
171
|
+
const allowed = RESPONSES_BY_KIND[kind];
|
|
172
|
+
const polarity = approved === true;
|
|
173
|
+
const fallback = polarity ? allowed[0] : allowed[allowed.length - 1];
|
|
174
|
+
const asked = isRecord(extra) && allowed.includes(extra.response) ? extra.response : '';
|
|
175
|
+
let response = asked && GRANTING_RESPONSES.has(asked) === polarity ? asked : fallback;
|
|
176
|
+
const result = { approved: polarity, response };
|
|
177
|
+
if (response === 'edit-and-approve') {
|
|
178
|
+
const edits = editableArgs(extra.editedArgs, request?.outbound?.editable);
|
|
179
|
+
if (Object.keys(edits).length) result.editedArgs = edits;
|
|
180
|
+
else response = result.response = 'approve';
|
|
181
|
+
}
|
|
182
|
+
const scopes = Array.isArray(request?.remember) ? request.remember : ['once'];
|
|
183
|
+
result.remember = polarity && scopes.includes(extra?.remember) ? extra.remember : 'once';
|
|
184
|
+
if (kind === 'permission' && polarity) {
|
|
185
|
+
const ids = (request?.permission?.scopes || []).map((scope) => scope.id);
|
|
186
|
+
if (ids.includes(extra?.scope)) result.scope = extra.scope;
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `args` with the card's edited fields in place of the model's. */
|
|
192
|
+
export function applyEditedArgs(args, editedArgs, editable) {
|
|
193
|
+
const edits = editableArgs(editedArgs, editable);
|
|
194
|
+
return Object.keys(edits).length ? { ...(isRecord(args) ? args : {}), ...edits } : args;
|
|
195
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// session/human-gate/store.mjs
|
|
2
|
+
// Per-agent permissions and the decision audit, both under the plugin data dir.
|
|
3
|
+
// A grant is keyed by agent + action kind (outbound-policy.mjs ACTION_KINDS)
|
|
4
|
+
// and says `allow` (never ask: "다시 묻지 않기") or `block` (deny without a
|
|
5
|
+
// card); no grant means ask. `agent` scope names the agent, `always` stands for
|
|
6
|
+
// every agent ('*'), and an agent's own grant wins over an every-agent one;
|
|
7
|
+
// `once` is never stored. Payments and deletes are never allowed. Grants are
|
|
8
|
+
// listed and revoked by id. The audit is one JSONL line per decision with
|
|
9
|
+
// agent, kind, action, tool, target, decision, deciding device and time —
|
|
10
|
+
// never a value, an argument or a payload — read back newest first.
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { parseJsonText, updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
|
|
15
|
+
import { ACTION_KINDS, isRememberableAction } from './outbound-policy.mjs';
|
|
16
|
+
|
|
17
|
+
const ANY_AGENT = '*';
|
|
18
|
+
const FIELD_LIMIT = 200;
|
|
19
|
+
const AUDIT_KEYS = ['agentId', 'kind', 'action', 'tool', 'target', 'decision', 'device'];
|
|
20
|
+
export const PERMISSION_MODES = Object.freeze(['ask', 'allow', 'block']);
|
|
21
|
+
|
|
22
|
+
const field = (value) => String(value ?? '').slice(0, FIELD_LIMIT);
|
|
23
|
+
const grantId = (agentId, action) =>
|
|
24
|
+
`grant_${createHash('sha256').update(`${agentId}\n${action}`).digest('hex').slice(0, 24)}`;
|
|
25
|
+
const isGrant = (row) => ACTION_KINDS.includes(row?.action) && (row.mode === 'allow' || row.mode === 'block');
|
|
26
|
+
|
|
27
|
+
export function createHumanGateStore(directory) {
|
|
28
|
+
const grantsFile = join(directory, 'grants.json');
|
|
29
|
+
const auditFile = join(directory, 'audit.jsonl');
|
|
30
|
+
const grantsOf = (doc) => (Array.isArray(doc?.grants) ? doc.grants.filter(isGrant) : []);
|
|
31
|
+
async function readGrants() {
|
|
32
|
+
try {
|
|
33
|
+
return grantsOf(parseJsonText(await readFile(grantsFile, 'utf8')));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code === 'ENOENT') return [];
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function updateGrants(mutate) {
|
|
40
|
+
await mkdir(directory, { recursive: true });
|
|
41
|
+
let result;
|
|
42
|
+
await updateJsonAtomic(grantsFile, (doc) => {
|
|
43
|
+
const out = mutate(grantsOf(doc));
|
|
44
|
+
if (!out) return undefined;
|
|
45
|
+
result = out.result;
|
|
46
|
+
return { grants: out.grants };
|
|
47
|
+
});
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/** Set `agentId`'s mode for `action`; `ask` removes its grant. Returns the
|
|
51
|
+
* grant, or null for `ask` and for an `allow` that may not exist. */
|
|
52
|
+
async function setMode({ agentId, action, mode, device = '' }) {
|
|
53
|
+
if (!ACTION_KINDS.includes(action) || !PERMISSION_MODES.includes(mode)) return null;
|
|
54
|
+
if (mode === 'allow' && !isRememberableAction(action)) return null;
|
|
55
|
+
const owner = field(agentId);
|
|
56
|
+
const id = grantId(owner, action);
|
|
57
|
+
const grant = mode === 'ask' ? null : { id, agentId: owner, action, mode, device: field(device), at: Date.now() };
|
|
58
|
+
await updateGrants((grants) => ({
|
|
59
|
+
grants: [...grants.filter((row) => row.id !== id), ...(grant ? [grant] : [])],
|
|
60
|
+
result: grant,
|
|
61
|
+
}));
|
|
62
|
+
return grant;
|
|
63
|
+
}
|
|
64
|
+
/** `allow`, `block` or `ask` for `agentId` + `action`; the agent's own grant wins. */
|
|
65
|
+
async function modeFor({ agentId, action }) {
|
|
66
|
+
const grants = (await readGrants()).filter((grant) => grant.action === action);
|
|
67
|
+
const own =
|
|
68
|
+
grants.find((grant) => grant.agentId === agentId) || grants.find((grant) => grant.agentId === ANY_AGENT);
|
|
69
|
+
if (own?.mode === 'allow' && !isRememberableAction(action)) return 'ask';
|
|
70
|
+
return own?.mode || 'ask';
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
setMode,
|
|
74
|
+
modeFor,
|
|
75
|
+
/** Stores an `allow` for `agent`/`always` scope; `once` stores nothing. Returns the grant or null. */
|
|
76
|
+
async grant({ agentId, action, scope, device = '' }) {
|
|
77
|
+
if (scope !== 'agent' && scope !== 'always') return null;
|
|
78
|
+
return setMode({ agentId: scope === 'always' ? ANY_AGENT : agentId, action, mode: 'allow', device });
|
|
79
|
+
},
|
|
80
|
+
/** Every action kind with `agentId`'s mode, for the profile's permission table. */
|
|
81
|
+
async permissions(agentId) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const action of ACTION_KINDS) {
|
|
84
|
+
out.push({ action, mode: await modeFor({ agentId, action }), rememberable: isRememberableAction(action) });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
},
|
|
88
|
+
/** Grants that apply to `agentId` (its own and every-agent ones); all when omitted. */
|
|
89
|
+
async listGrants({ agentId } = {}) {
|
|
90
|
+
const grants = await readGrants();
|
|
91
|
+
return agentId ? grants.filter((grant) => grant.agentId === agentId || grant.agentId === ANY_AGENT) : grants;
|
|
92
|
+
},
|
|
93
|
+
async revokeGrant(id) {
|
|
94
|
+
return (
|
|
95
|
+
(await updateGrants((grants) => {
|
|
96
|
+
const rest = grants.filter((grant) => grant.id !== id);
|
|
97
|
+
return rest.length === grants.length ? null : { grants: rest, result: true };
|
|
98
|
+
})) === true
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
async recordAudit(entry) {
|
|
102
|
+
const line = { at: Number.isFinite(entry?.at) ? entry.at : Date.now() };
|
|
103
|
+
for (const key of AUDIT_KEYS) line[key] = field(entry?.[key]);
|
|
104
|
+
await mkdir(dirname(auditFile), { recursive: true });
|
|
105
|
+
await appendFile(auditFile, `${JSON.stringify(line)}\n`, 'utf8');
|
|
106
|
+
return line;
|
|
107
|
+
},
|
|
108
|
+
/** Newest first; `agentId` filters to one agent. */
|
|
109
|
+
async readAudit({ agentId = '', limit = 100 } = {}) {
|
|
110
|
+
let raw = '';
|
|
111
|
+
try {
|
|
112
|
+
raw = await readFile(auditFile, 'utf8');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code === 'ENOENT') return [];
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const rows = [];
|
|
118
|
+
for (const line of raw.split('\n').reverse()) {
|
|
119
|
+
if (!line) continue;
|
|
120
|
+
try {
|
|
121
|
+
const row = JSON.parse(line);
|
|
122
|
+
if (!agentId || row.agentId === agentId) rows.push(row);
|
|
123
|
+
} catch {}
|
|
124
|
+
if (rows.length >= limit) break;
|
|
125
|
+
}
|
|
126
|
+
return rows;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let processStore = null;
|
|
132
|
+
/** The process store under the plugin data dir; built on first use. */
|
|
133
|
+
export async function humanGateStore() {
|
|
134
|
+
if (!processStore) {
|
|
135
|
+
const { getPluginData } = await import('../../config.mjs');
|
|
136
|
+
processStore = createHumanGateStore(join(getPluginData(), 'human-gate'));
|
|
137
|
+
}
|
|
138
|
+
return processStore;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function requiredAgentId(agentId) {
|
|
142
|
+
if (typeof agentId !== 'string' || !agentId.trim() || agentId.length > FIELD_LIMIT || agentId === ANY_AGENT) {
|
|
143
|
+
throw new Error('agent id is invalid');
|
|
144
|
+
}
|
|
145
|
+
return agentId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The profile's "권한" table for `agentId` (capability getAgentPermissions). */
|
|
149
|
+
export async function agentPermissions(agentId) {
|
|
150
|
+
return (await humanGateStore()).permissions(requiredAgentId(agentId));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Set one row of that table (capability setAgentPermission); returns the table. */
|
|
154
|
+
export async function setAgentPermission(agentId, action, mode) {
|
|
155
|
+
const agent = requiredAgentId(agentId);
|
|
156
|
+
if (!ACTION_KINDS.includes(action)) throw new Error(`action must be one of ${ACTION_KINDS.join(', ')}`);
|
|
157
|
+
if (!PERMISSION_MODES.includes(mode)) throw new Error(`mode must be one of ${PERMISSION_MODES.join(', ')}`);
|
|
158
|
+
if (mode === 'allow' && !isRememberableAction(action)) throw new Error(`${action} always asks; it cannot be allowed`);
|
|
159
|
+
const store = await humanGateStore();
|
|
160
|
+
await store.setMode({ agentId: agent, action, mode });
|
|
161
|
+
return store.permissions(agent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Replace the process store (tests); null restores the default. */
|
|
165
|
+
export function setHumanGateStore(store) {
|
|
166
|
+
processStore = store || null;
|
|
167
|
+
}
|
|
@@ -17,6 +17,7 @@ import { HANDOFF_TIMEOUT_MAX_MS } from '../compact/constants.mjs';
|
|
|
17
17
|
import { envFlag, envPositiveInt } from '../../../../shared/env.mjs';
|
|
18
18
|
import { positiveInt } from '../../../../shared/numbers.mjs';
|
|
19
19
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
20
|
+
import { isAgentModeSession } from '../agent-mode.mjs';
|
|
20
21
|
import { providerInputExcludesCache, providerNativePdf } from '../../providers/registry.mjs';
|
|
21
22
|
import { contentMediaBytes, contentMediaCount } from '../../providers/media-normalization.mjs';
|
|
22
23
|
import { splitProtectedContext } from '../compact/messages.mjs';
|
|
@@ -133,7 +134,8 @@ function effectiveContextWindowPercent(sessionRef, cfg) {
|
|
|
133
134
|
export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
134
135
|
if (!sessionRef) return null;
|
|
135
136
|
const cfg = sessionRef.compaction || {};
|
|
136
|
-
|
|
137
|
+
// Agent mode always compacts on its own: the user is never asked to.
|
|
138
|
+
const auto = (cfg.auto !== false || isAgentModeSession(sessionRef)) && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
137
139
|
if (!auto) return { auto: false };
|
|
138
140
|
const { contextWindow, boundaryTokens } = compactBoundaryFor(sessionRef, cfg);
|
|
139
141
|
if (!boundaryTokens) return null;
|
|
@@ -8,6 +8,7 @@ import { resolveLiveToolCwd } from './tool-exec.mjs';
|
|
|
8
8
|
import { createSteeringDrain } from './steering.mjs';
|
|
9
9
|
import { createNoToolTurnResolver } from './no-tool-turn.mjs';
|
|
10
10
|
import { classifyTerminationReason } from './termination.mjs';
|
|
11
|
+
import { routeUsageSegments } from './usage.mjs';
|
|
11
12
|
|
|
12
13
|
// Consecutive identical-AND-failing tool calls (same name+args, error result)
|
|
13
14
|
// tolerated across iterations before the loop refuses to re-execute and steers
|
|
@@ -153,5 +154,13 @@ export function finishLoop(state) {
|
|
|
153
154
|
providerStateUpdated: state.providerStateUpdated,
|
|
154
155
|
terminationReason: classifyTerminationReason(response, { sessionAgent: state.sessionAgent }),
|
|
155
156
|
providerContinuations: state.noToolTurn.providerContinuations,
|
|
157
|
+
...(state.routeUsageHops?.length
|
|
158
|
+
? {
|
|
159
|
+
usageSegments: routeUsageSegments(state.routeUsageHops, state.lastUsage, {
|
|
160
|
+
provider: state.sessionRef?.provider,
|
|
161
|
+
model: state.sessionRef?.model,
|
|
162
|
+
}),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
156
165
|
};
|
|
157
166
|
}
|
|
@@ -16,6 +16,7 @@ import { isOffReadonlySurface } from './deferred-call-through.mjs';
|
|
|
16
16
|
import { BRIDGE_CALL_TOOL_NAME, bridgeErrorText, isToolBridgeEnabled } from '../../runtime-core/tool-bridge.mjs';
|
|
17
17
|
import { sessionAllowlistExcludes, sessionDisallows } from '../../runtime-core/deferred-tool-availability.mjs';
|
|
18
18
|
import { providerToolExclusionReason } from './provider-tool-exclusions.mjs';
|
|
19
|
+
import { agentMcpToolPermission, projectMcpToolDisabled } from '../../mcp/agent-connectors.mjs';
|
|
19
20
|
|
|
20
21
|
const WORKER_DENIED_TOOLS = new Set(['agent', 'ask_user', 'session']);
|
|
21
22
|
const BRIDGE_DISCOVERY_TOOL_NAMES = new Set(['load_tool']);
|
|
@@ -122,6 +123,15 @@ export function preDispatchDenyForSession(sessionRef, call) {
|
|
|
122
123
|
if (sessionDisallows(sessionRef, schemaName)) {
|
|
123
124
|
return `Error: tool "${name}" is not available in this session.`;
|
|
124
125
|
}
|
|
126
|
+
// An agent's connectors: a tool of a server it does not mount, or one set
|
|
127
|
+
// to `disabled`, stays refused though the process-wide registry knows it.
|
|
128
|
+
if (agentMcpToolPermission(sessionRef?.agent, schemaName) === 'disabled') {
|
|
129
|
+
return `Error: tool "${name}" is not available to this agent.`;
|
|
130
|
+
}
|
|
131
|
+
// The project the session runs in may switch a connector off as well.
|
|
132
|
+
if (projectMcpToolDisabled(sessionRef, schemaName)) {
|
|
133
|
+
return `Error: tool "${name}" is switched off in this project.`;
|
|
134
|
+
}
|
|
125
135
|
if (isOffReadonlySurface(sessionRef, schemaName)) {
|
|
126
136
|
return `Error: tool "${name}" is not available in this read-only session.`;
|
|
127
137
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { routeFallbackVerdict } from '../../../../shared/model-route-fallback.mjs';
|
|
2
|
+
import { providerErrorKind } from '../../providers/provider-error-kind.mjs';
|
|
3
|
+
import { displayModelName } from '../../../../shared/model-display.mjs';
|
|
4
|
+
import { presentErrorText } from '../../../../shared/err-text.mjs';
|
|
2
5
|
import { getProvider, initProviders } from '../../providers/registry.mjs';
|
|
3
6
|
import { loadConfig } from '../../config.mjs';
|
|
4
7
|
import { ensureProviderEnabled } from '../../runtime-core/config-helpers.mjs';
|
|
@@ -7,19 +10,107 @@ import { pinSessionWireRoute } from '../../providers/wire-route.mjs';
|
|
|
7
10
|
import { buildRequestSendOptions } from '../manager/ask-turn-loop-options.mjs';
|
|
8
11
|
import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
|
|
9
12
|
import { resolveSessionContextMeta } from '../manager/context-meta.mjs';
|
|
13
|
+
import { stripProviderReplayMessages } from '../manager/provider-replay.mjs';
|
|
14
|
+
|
|
15
|
+
// The short reason a notice gives for leaving a model, by provider kind.
|
|
16
|
+
const HOP_REASONS = {
|
|
17
|
+
quota_exhausted: 'usage limit reached',
|
|
18
|
+
entitlement: 'not included in the plan',
|
|
19
|
+
billing: 'out of credits',
|
|
20
|
+
rate_limited: 'rate-limited',
|
|
21
|
+
overloaded: 'unavailable',
|
|
22
|
+
transient: 'unavailable',
|
|
23
|
+
service_tier_unavailable: 'unavailable',
|
|
24
|
+
auth: 'sign-in failed',
|
|
25
|
+
permission: 'access denied',
|
|
26
|
+
invalid_request: 'model not available',
|
|
27
|
+
context_overflow: 'context too long',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const routeLabel = (route) => displayModelName(route.model, route.provider) || `${route.provider}/${route.model}`;
|
|
31
|
+
|
|
32
|
+
/** The muted transcript line for a hop: `<from> <reason> — switched to <to>.` */
|
|
33
|
+
export function routeFallbackNoticeText(from, to, error) {
|
|
34
|
+
const reason = HOP_REASONS[providerErrorKind(error).kind] || 'unavailable';
|
|
35
|
+
return `${routeLabel(from)} ${reason} — switched to ${routeLabel(to)}.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function routeFailure(route, error) {
|
|
39
|
+
return {
|
|
40
|
+
provider: route.provider,
|
|
41
|
+
model: route.model,
|
|
42
|
+
kind: providerErrorKind(error).kind,
|
|
43
|
+
error: presentErrorText(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The chain ended after at least one hop: the final error keeps its own kind
|
|
48
|
+
// and names every model that was tried and why each failed. `skipped` is the
|
|
49
|
+
// next route when the chain stopped before it (a context overflow whose next
|
|
50
|
+
// window is no larger), so the text says why instead of "every model failed".
|
|
51
|
+
function annotateEndedChain(session, error, skipped = null) {
|
|
52
|
+
const tried = session?.routeFallbackFailures;
|
|
53
|
+
if (!tried?.length || !error || typeof error !== 'object' || error.routeFallbackAttempts) return;
|
|
54
|
+
const attempts = [...tried, routeFailure(session, error)];
|
|
55
|
+
try {
|
|
56
|
+
error.routeFallbackAttempts = attempts;
|
|
57
|
+
const lines = attempts.map((attempt) => `${routeLabel(attempt)}: ${attempt.error}`).join('; ');
|
|
58
|
+
const head = skipped
|
|
59
|
+
? `Fallback chain stopped before ${routeLabel(skipped)} (its context window is not larger) — tried`
|
|
60
|
+
: 'Every model in the fallback chain failed —';
|
|
61
|
+
error.message = `${error.message}\n${head} ${lines}`;
|
|
62
|
+
if (skipped) error.routeFallbackStoppedBefore = { provider: skipped.provider, model: skipped.model };
|
|
63
|
+
} catch {
|
|
64
|
+
/* frozen errors keep their own message */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A context overflow moves only to a model whose window is larger.
|
|
69
|
+
async function nextContextGrows(state, next) {
|
|
70
|
+
const config = ensureProviderEnabled(loadConfig(), next.provider);
|
|
71
|
+
await initProviders(config.providers, { signal: state.signal });
|
|
72
|
+
state.throwIfAborted();
|
|
73
|
+
const provider = getProvider(next.provider);
|
|
74
|
+
if (!provider) return false;
|
|
75
|
+
const current = resolveSessionContextMeta(state.provider, state.sessionRef.model, state.sessionRef).rawContextWindow;
|
|
76
|
+
const window = resolveSessionContextMeta(provider, next.model, next).rawContextWindow;
|
|
77
|
+
return Number(window) > Number(current || state.sessionRef.selectedContextWindow || 0);
|
|
78
|
+
}
|
|
10
79
|
|
|
11
80
|
export async function advanceAgentRoute(state, error) {
|
|
12
81
|
const session = state.sessionRef;
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
82
|
+
if (!session?.agent || !session.routeFallbacks?.length) return false;
|
|
83
|
+
const index = session.routeFallbackIndex || 0;
|
|
84
|
+
const next = session.routeFallbacks[index];
|
|
85
|
+
// Only THIS request must have produced nothing (its stream outcome says so):
|
|
86
|
+
// earlier requests of the turn may have run tools. Their calls and results
|
|
87
|
+
// are already in the live turn, which the next model continues from, so no
|
|
88
|
+
// completed tool runs again.
|
|
89
|
+
const verdict = routeFallbackVerdict(error, { signal: state.signal });
|
|
90
|
+
const hops = verdict === 'hop' || (verdict === 'context' && next && (await nextContextGrows(state, next)));
|
|
91
|
+
if (!next || !hops) {
|
|
92
|
+
if (verdict !== 'stay') annotateEndedChain(session, error, next || null);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
17
95
|
state.throwIfAborted();
|
|
18
96
|
const config = ensureProviderEnabled(loadConfig(), next.provider);
|
|
19
97
|
await initProviders(config.providers, { signal: state.signal });
|
|
20
98
|
state.throwIfAborted();
|
|
21
99
|
const provider = getProvider(next.provider);
|
|
22
100
|
if (!provider) throw new Error(`Provider "${next.provider}" not available`);
|
|
101
|
+
const failed = routeFailure(session, error);
|
|
102
|
+
// The left model's share of the turn ends here: the running total at the
|
|
103
|
+
// switch bounds it, so each model's tokens are booked at its own rates.
|
|
104
|
+
state.routeUsageHops = [
|
|
105
|
+
...(state.routeUsageHops || []),
|
|
106
|
+
{ provider: session.provider, model: session.model, usage: state.lastUsage ? { ...state.lastUsage } : null },
|
|
107
|
+
];
|
|
108
|
+
const notice = routeFallbackNoticeText(session, next, error);
|
|
109
|
+
// Saved with the new route: a crash before the live turn is checkpointed
|
|
110
|
+
// again recovers that turn without the previous provider's native state.
|
|
111
|
+
if (session.activeTurnCheckpoint && typeof session.activeTurnCheckpoint === 'object') {
|
|
112
|
+
session.activeTurnCheckpoint = { ...session.activeTurnCheckpoint, routeChanged: true };
|
|
113
|
+
}
|
|
23
114
|
const oldOptions = buildRequestSendOptions({
|
|
24
115
|
sessionId: state.sessionId,
|
|
25
116
|
session,
|
|
@@ -41,9 +132,12 @@ export async function advanceAgentRoute(state, error) {
|
|
|
41
132
|
);
|
|
42
133
|
if (!updated) throw new Error('agent session closed during model fallback');
|
|
43
134
|
updated.routeFallbackIndex = index + 1;
|
|
135
|
+
updated.routeFallbackFailures = [...(session.routeFallbackFailures || []), failed];
|
|
44
136
|
await pinSessionWireRoute(updated, provider);
|
|
45
137
|
state.throwIfAborted();
|
|
46
|
-
|
|
138
|
+
// The live turn (prompt, assistant tool calls, tool results) carries on;
|
|
139
|
+
// only the previous provider's native replay state is dropped from it.
|
|
140
|
+
state.messages.splice(0, state.messages.length, ...stripProviderReplayMessages(state.messages));
|
|
47
141
|
updated.liveTurnMessages = state.messages;
|
|
48
142
|
state.sessionRef = updated;
|
|
49
143
|
state.provider = provider;
|
|
@@ -67,6 +161,14 @@ export async function advanceAgentRoute(state, error) {
|
|
|
67
161
|
signal: state.signal,
|
|
68
162
|
})
|
|
69
163
|
);
|
|
164
|
+
try {
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
`[route-fallback] ${failed.provider}/${failed.model} (${failed.kind}) -> ${updated.provider}/${updated.model}\n`
|
|
167
|
+
);
|
|
168
|
+
state.opts.onStageChange?.('notice', { tone: 'info', message: notice });
|
|
169
|
+
} catch {
|
|
170
|
+
/* display-only */
|
|
171
|
+
}
|
|
70
172
|
state.opts.onStageChange?.('connecting', {
|
|
71
173
|
provider: updated.provider,
|
|
72
174
|
model: updated.model,
|
|
@@ -181,6 +181,10 @@ export async function sendProviderRequest(state, round) {
|
|
|
181
181
|
tools: round.sendTools,
|
|
182
182
|
opts,
|
|
183
183
|
});
|
|
184
|
+
// The request's time for the speed figures, the wait for the first token
|
|
185
|
+
// included: the span the usage ledger recorded as `duration_ms` (stamped on
|
|
186
|
+
// the response); wall time only for a response no ledger row timed.
|
|
187
|
+
const requestStartedAt = Date.now();
|
|
184
188
|
let result;
|
|
185
189
|
try {
|
|
186
190
|
result = await runWithProviderRequestToolsScope(round.requestToolScope, () =>
|
|
@@ -211,12 +215,16 @@ export async function sendProviderRequest(state, round) {
|
|
|
211
215
|
}
|
|
212
216
|
if (result.action === 'proceed') sideQuestion?.settle(result.response);
|
|
213
217
|
else sideQuestion?.discard();
|
|
218
|
+
const sendEndedAt = Date.now();
|
|
214
219
|
return {
|
|
215
220
|
result,
|
|
216
221
|
providerMessages,
|
|
217
222
|
prefixGuardCandidate,
|
|
218
223
|
sendStartedAt,
|
|
219
|
-
sendEndedAt
|
|
224
|
+
sendEndedAt,
|
|
225
|
+
requestMs: Number.isFinite(result.response?.usageDurationMs)
|
|
226
|
+
? Math.max(0, result.response.usageDurationMs)
|
|
227
|
+
: Math.max(0, sendEndedAt - requestStartedAt),
|
|
220
228
|
preSendMs,
|
|
221
229
|
toolResumeMs,
|
|
222
230
|
};
|
|
@@ -429,6 +437,7 @@ export function settleSendResult(state, round, sent) {
|
|
|
429
437
|
usage: reportedUsage,
|
|
430
438
|
sendTools: round.sendTools,
|
|
431
439
|
toolCalls: response.toolCalls?.length || 0,
|
|
440
|
+
requestMs: sent.requestMs,
|
|
432
441
|
})
|
|
433
442
|
)
|
|
434
443
|
);
|