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
|
@@ -29,6 +29,7 @@ import { getStoreDir } from './store/session-paths.mjs';
|
|
|
29
29
|
import { forgetSessionSaveBaseline } from './store/save-worker.mjs';
|
|
30
30
|
import { messageIdOf, stampMessageIds } from './message-ids.mjs';
|
|
31
31
|
import { restoreTranscriptItems } from './transcript-restore/restore.mjs';
|
|
32
|
+
import { isAgentModeSession } from './agent-mode.mjs';
|
|
32
33
|
import {
|
|
33
34
|
COMPACTION_ARCHIVE_MAX_BYTES,
|
|
34
35
|
COMPACTION_DIVIDER_LABEL,
|
|
@@ -163,7 +164,7 @@ function storedHistoryMessage(message) {
|
|
|
163
164
|
* (compactionArchiveProjection messages) as stored history restores it,
|
|
164
165
|
* namespaced and marked archived, then the divider. Empty when nothing
|
|
165
166
|
* visible left. */
|
|
166
|
-
export function compactionSegmentItems(messages, { sessionId, seq, at }) {
|
|
167
|
+
export function compactionSegmentItems(messages, { sessionId, seq, at, divider = true }) {
|
|
167
168
|
const restored = restoreTranscriptItems(messages.map(storedHistoryMessage), { sessionId });
|
|
168
169
|
if (!restored.length) return [];
|
|
169
170
|
const prefix = `hist_${sessionId}_`;
|
|
@@ -171,6 +172,8 @@ export function compactionSegmentItems(messages, { sessionId, seq, at }) {
|
|
|
171
172
|
const id = String(item?.id ?? '');
|
|
172
173
|
return { ...item, id: `arch${seq}_${id.startsWith(prefix) ? id.slice(prefix.length) : id}`, archived: true };
|
|
173
174
|
});
|
|
175
|
+
// Agent mode compacts silently: its transcript gets no compaction notice.
|
|
176
|
+
if (!divider) return items;
|
|
174
177
|
items.push({
|
|
175
178
|
kind: 'statusdone',
|
|
176
179
|
id: `arch${seq}_divider`,
|
|
@@ -321,9 +324,10 @@ export function scheduleCompactionArchive({
|
|
|
321
324
|
const clearedThrough = Math.max(0, Math.floor(Number(previous.clearedThrough) || 0));
|
|
322
325
|
const epoch = compactionArchiveDeleteEpoch(id);
|
|
323
326
|
session.compaction.archive = { ...previous, nextSeq: seq + 1 };
|
|
327
|
+
const divider = !isAgentModeSession(session);
|
|
324
328
|
const run = async () => {
|
|
325
329
|
await new Promise((resolve) => setImmediate(resolve));
|
|
326
|
-
const items = compactionSegmentItems(projection.messages, { sessionId: id, seq, at });
|
|
330
|
+
const items = compactionSegmentItems(projection.messages, { sessionId: id, seq, at, divider });
|
|
327
331
|
if (!items.length) return null;
|
|
328
332
|
const segments = await appendSegment({ sessionId: id, seq, at, items, clearedThrough, maxBytes, epoch });
|
|
329
333
|
const archive = session.compaction?.archive;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// session/human-gate/gate.mjs
|
|
2
|
+
// The one policy hook for human approval, applied to every model tool call
|
|
3
|
+
// after the PreToolUse hooks (loop/tool-exec/before-hook.mjs):
|
|
4
|
+
// humanGateDecision(call, decision, ctx) → { decision, request }
|
|
5
|
+
// turns an outbound call (outbound-policy.mjs) into an `ask` whatever the
|
|
6
|
+
// hooks or the agent's permission said short of a deny, unless the agent's
|
|
7
|
+
// permission for that action kind (store.mjs) is `allow`; `block` denies it
|
|
8
|
+
// without a card. The outbound rule applies ONLY while a bot acts (see
|
|
9
|
+
// actingBotId); an ordinary coding session's outbound call passes with the
|
|
10
|
+
// hook decision unchanged. Other surfaces (group rooms) call it with their own
|
|
11
|
+
// decision to get the same outbound rule. After the user answered,
|
|
12
|
+
// recordHumanDecision stores a "don't ask again" grant and the audit line. Unattended runs (schedule, webhook, room member) register a
|
|
13
|
+
// port here (session-runtime/held-requests.mjs) that turns their asks into
|
|
14
|
+
// held requests instead of an approval card.
|
|
15
|
+
import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../agent-home.mjs';
|
|
16
|
+
import { serverConnector } from '../../mcp/server-connectors.mjs';
|
|
17
|
+
import { readAgentDefinitionKind } from '../../../../shared/agent-kind.mjs';
|
|
18
|
+
import { isContactAgentId } from '../../../../shared/agent-route-config.mjs';
|
|
19
|
+
import { mixdogRoot, resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
20
|
+
import { roomMemberOfSession } from '../agent-room.mjs';
|
|
21
|
+
import { classifyOutboundCall, isRememberableAction } from './outbound-policy.mjs';
|
|
22
|
+
import { humanGateStore } from './store.mjs';
|
|
23
|
+
|
|
24
|
+
export const OUTBOUND_APPROVAL_REASON = 'This sends or posts outside Mixdog; it needs your approval.';
|
|
25
|
+
export const BLOCKED_ACTION_REASON = "This agent's permission blocks this action.";
|
|
26
|
+
/** Outbound grants may be remembered for this agent, never for every agent;
|
|
27
|
+
* payments and deletes never. */
|
|
28
|
+
const outboundRemember = (category) => (isRememberableAction(category) ? ['once', 'agent'] : ['once']);
|
|
29
|
+
|
|
30
|
+
let unattendedPort = null;
|
|
31
|
+
/** `{ runOf(sessionId) → { kind, agentId, name } | null, hold(request, { signal }) → Promise<approval>,
|
|
32
|
+
* humanNeeded(sessionId, outcome, site) → Promise<boolean> }` — the last ends a schedule/webhook
|
|
33
|
+
* run that would need a takeover (session-runtime/held-requests.mjs). */
|
|
34
|
+
export function setUnattendedApprovalPort(port) {
|
|
35
|
+
unattendedPort = port || null;
|
|
36
|
+
}
|
|
37
|
+
export function unattendedApprovalPort() {
|
|
38
|
+
return unattendedPort;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The agent a session is bound to: its unattended run's owner, a room
|
|
42
|
+
* member, a home thread's agent; '' for any other (coding) session. */
|
|
43
|
+
function boundAgentId(sessionId) {
|
|
44
|
+
const run = unattendedPort?.runOf?.(sessionId);
|
|
45
|
+
if (run?.agentId) return run.agentId;
|
|
46
|
+
return roomMemberOfSession(sessionId)?.agentId || agentIdOfHomeSession(sessionId) || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The agent a session acts for: its unattended run's owner, a room member,
|
|
50
|
+
* a home thread's agent, else Mixy. */
|
|
51
|
+
export function agentIdForSession(sessionId) {
|
|
52
|
+
return boundAgentId(sessionId) || MIXY_AGENT_ID;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The bot a session acts for, or '' when none: Mixy in her own home thread
|
|
56
|
+
* (or her unattended run / room member session), or a contact agent (`kind:
|
|
57
|
+
* bot`, isContactAgentId) in its home thread, room member session or
|
|
58
|
+
* unattended run. A coding Lead (also id 'lead'), its workflow agents and
|
|
59
|
+
* their sub-agent sessions are bound to nothing here, so they are no bot.
|
|
60
|
+
* A sub-agent of a bot has its own session id and no link back to the
|
|
61
|
+
* caller in this layer, so it is not gated either. */
|
|
62
|
+
function actingBotId(sessionId) {
|
|
63
|
+
const id = boundAgentId(sessionId);
|
|
64
|
+
if (!id) return '';
|
|
65
|
+
if (id === MIXY_AGENT_ID) return id;
|
|
66
|
+
const kind = readAgentDefinitionKind({ dataDir: resolvePluginData(), shippedRoot: mixdogRoot(), id });
|
|
67
|
+
return isContactAgentId(id, kind) ? id : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const actionOf = (decision) => String(decision?.action || decision?.decision || '').toLowerCase();
|
|
71
|
+
|
|
72
|
+
async function mcpAnnotations(name, scopeId) {
|
|
73
|
+
if (!String(name).startsWith('mcp__')) return undefined;
|
|
74
|
+
try {
|
|
75
|
+
const { getMcpTools } = await import('../../mcp/client.mjs');
|
|
76
|
+
const tool = getMcpTools(scopeId || undefined).find((entry) => entry.name === name);
|
|
77
|
+
if (!tool) return undefined;
|
|
78
|
+
return { ...(tool.annotations || {}), ...(tool.mcpToolName ? { mcpToolName: tool.mcpToolName } : {}) };
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Messages a trash card lists by sender and subject; the rest read "외 N통". */
|
|
85
|
+
const PREVIEW_MESSAGES = 3;
|
|
86
|
+
|
|
87
|
+
/** A connector that can say whose account it is and what its message ids are
|
|
88
|
+
* offers a read-only `describe_emails` tool (connectors/gmail-app-password.mjs).
|
|
89
|
+
* Returns `{ account, messages, more }` for the card, or null: the card then
|
|
90
|
+
* shows what the call itself carries. Never changes what is approved. */
|
|
91
|
+
async function describeEmails(call, outbound, ctx) {
|
|
92
|
+
if (outbound.category !== 'email' && outbound.category !== 'trash') return null;
|
|
93
|
+
const [, server = ''] = String(call?.name || '').split('__');
|
|
94
|
+
// Only the Gmail connector offers it; any other mail server never sees an unlisted call.
|
|
95
|
+
if (serverConnector(server) !== 'gmail') return null;
|
|
96
|
+
const name = `mcp__${server}__describe_emails`;
|
|
97
|
+
try {
|
|
98
|
+
// Not a listed tool: the server answers it (or errors, and the card keeps the call's own content).
|
|
99
|
+
const { executeMcpTool } = await import('../../mcp/client.mjs');
|
|
100
|
+
const ids = outbound.category === 'trash' && Array.isArray(call.args?.ids) ? call.args.ids.map(String) : [];
|
|
101
|
+
const shown = ids.slice(0, PREVIEW_MESSAGES);
|
|
102
|
+
const raw = await executeMcpTool(name, { ids: shown }, { scopeId: ctx.mcpScopeId });
|
|
103
|
+
const { account, emails } = JSON.parse(raw);
|
|
104
|
+
const messages = (Array.isArray(emails) ? emails : []).map((mail) =>
|
|
105
|
+
[mail.from, mail.subject, String(mail.date || '').slice(0, 10)].filter(Boolean).join(' · ')
|
|
106
|
+
);
|
|
107
|
+
return { account: String(account || ''), messages, more: Math.max(0, ids.length - shown.length) };
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {{ name: string, args: any }} call
|
|
115
|
+
* @param {object|null} decision the PreToolUse decision so far
|
|
116
|
+
* @param {{ sessionId?: string, mcpScopeId?: string, annotations?: object, store?: object }} ctx
|
|
117
|
+
* @returns {Promise<{ decision: object|null, request: object|null, gate: object|null }>}
|
|
118
|
+
* `request` holds the kind-specific fields an `ask` presents; `gate` the
|
|
119
|
+
* audit identity `{ agentId, kind, action, tool, target }`.
|
|
120
|
+
*/
|
|
121
|
+
export async function humanGateDecision(call, decision, ctx = {}) {
|
|
122
|
+
const action = actionOf(decision);
|
|
123
|
+
if (action === 'deny' || action === 'block') return { decision, request: null, gate: null };
|
|
124
|
+
const agentId = agentIdForSession(ctx.sessionId);
|
|
125
|
+
const annotations = ctx.annotations ?? (await mcpAnnotations(call?.name, ctx.mcpScopeId));
|
|
126
|
+
const classified = classifyOutboundCall({ name: call?.name, args: call?.args, annotations });
|
|
127
|
+
const outbound = classified && actingBotId(ctx.sessionId) ? classified : null;
|
|
128
|
+
if (!outbound) {
|
|
129
|
+
if (action !== 'ask') return { decision, request: null, gate: null };
|
|
130
|
+
return {
|
|
131
|
+
decision,
|
|
132
|
+
request: { kind: 'approval', agentId, remember: ['once'] },
|
|
133
|
+
gate: { agentId, kind: 'approval', tool: String(call?.name || ''), target: '' },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const gate = { agentId, kind: 'outbound', action: outbound.category, tool: outbound.tool, target: outbound.target };
|
|
137
|
+
const request = {
|
|
138
|
+
kind: 'outbound',
|
|
139
|
+
agentId,
|
|
140
|
+
outbound: {
|
|
141
|
+
category: outbound.category,
|
|
142
|
+
target: outbound.target,
|
|
143
|
+
account: outbound.account,
|
|
144
|
+
payload: outbound.payload,
|
|
145
|
+
editable: outbound.editable,
|
|
146
|
+
details: outbound.details,
|
|
147
|
+
},
|
|
148
|
+
remember: outboundRemember(outbound.category),
|
|
149
|
+
};
|
|
150
|
+
const store = ctx.store || (await humanGateStore());
|
|
151
|
+
const mode = await store.modeFor({ agentId, action: outbound.category });
|
|
152
|
+
const base = decision && typeof decision === 'object' ? decision : {};
|
|
153
|
+
if (mode === 'block') {
|
|
154
|
+
await store.recordAudit({ ...gate, decision: 'blocked', device: '' });
|
|
155
|
+
return { decision: { ...base, action: 'deny', reason: BLOCKED_ACTION_REASON }, request: null, gate: null };
|
|
156
|
+
}
|
|
157
|
+
if (action !== 'ask' && mode === 'allow') {
|
|
158
|
+
await store.recordAudit({ ...gate, decision: 'remembered', device: '' });
|
|
159
|
+
return { decision, request: null, gate: null };
|
|
160
|
+
}
|
|
161
|
+
const described = await describeEmails(call, outbound, ctx);
|
|
162
|
+
if (described) {
|
|
163
|
+
if (described.account) request.outbound.account = described.account;
|
|
164
|
+
if (described.messages.length)
|
|
165
|
+
request.outbound.details = { ...outbound.details, messages: described.messages.join('\n') };
|
|
166
|
+
if (described.more) request.outbound.details = { ...request.outbound.details, more: String(described.more) };
|
|
167
|
+
}
|
|
168
|
+
// A hook that did not rewrite the call (allow / no decision) carries no
|
|
169
|
+
// name or args into the ask: the card shows, and the run executes, the
|
|
170
|
+
// call that was classified.
|
|
171
|
+
const rewriting = action === 'ask' || action === 'modify' || action === 'rewrite';
|
|
172
|
+
const { name: _name, args: _args, ...unrewritten } = base;
|
|
173
|
+
return {
|
|
174
|
+
decision: { ...(rewriting ? base : unrewritten), action: 'ask', reason: base.reason || OUTBOUND_APPROVAL_REASON },
|
|
175
|
+
request,
|
|
176
|
+
gate,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Audit one answered request and store its remembered grant. `approval` is
|
|
181
|
+
* the waiter's result ({ approved, response, remember, device, timedOut }). */
|
|
182
|
+
export async function recordHumanDecision(gate, approval, { store } = {}) {
|
|
183
|
+
if (!gate) return;
|
|
184
|
+
const target = store || (await humanGateStore());
|
|
185
|
+
let decision = approval?.response || (approval?.approved === true ? 'approve' : 'deny');
|
|
186
|
+
if (approval?.timedOut) decision = 'timeout';
|
|
187
|
+
const device = typeof approval?.device === 'string' ? approval.device : '';
|
|
188
|
+
if (approval?.approved === true && gate.action) {
|
|
189
|
+
await target.grant({ agentId: gate.agentId, action: gate.action, scope: approval.remember, device });
|
|
190
|
+
}
|
|
191
|
+
await target.recordAudit({ ...gate, decision, device });
|
|
192
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// session/human-gate/held-message.mjs
|
|
2
|
+
// A held request of an unattended run, posted into its owning agent's home
|
|
3
|
+
// thread: one row when it is held (the app renders it with Approve / Deny,
|
|
4
|
+
// answering through the ordinary approval call with the held id) and one when
|
|
5
|
+
// it settles. The stored text opens with the `[mixdog-runtime]` control marker
|
|
6
|
+
// and the row is synthetic, so no human-request classifier reads it as the
|
|
7
|
+
// user's words; the structured `heldRequest` payload is for the UI only.
|
|
8
|
+
import { escapeHarnessTags } from '../../../../shared/shell-output-summary.mjs';
|
|
9
|
+
|
|
10
|
+
export const HELD_REQUEST_SOURCE = 'held-request';
|
|
11
|
+
const STATUSES = new Set(['pending', 'approved', 'denied', 'expired']);
|
|
12
|
+
|
|
13
|
+
const clean = (value, limit = 200) =>
|
|
14
|
+
escapeHarnessTags(
|
|
15
|
+
String(value ?? '')
|
|
16
|
+
.replace(/[\r\n"]+/g, ' ')
|
|
17
|
+
.trim()
|
|
18
|
+
).slice(0, limit);
|
|
19
|
+
|
|
20
|
+
/** The canonical held-request payload; `payload` is the outbound text the
|
|
21
|
+
* card may show (never a secret: secrets are never held). */
|
|
22
|
+
export function normalizeHeldRequest(value = {}) {
|
|
23
|
+
const status = STATUSES.has(value.status) ? value.status : 'pending';
|
|
24
|
+
return {
|
|
25
|
+
id: String(value.id || ''),
|
|
26
|
+
status,
|
|
27
|
+
agentId: String(value.agentId || ''),
|
|
28
|
+
kind: String(value.kind || 'approval'),
|
|
29
|
+
tool: clean(value.tool),
|
|
30
|
+
target: clean(value.target),
|
|
31
|
+
reason: clean(value.reason, 400),
|
|
32
|
+
...(typeof value.payload === 'string' && value.payload ? { payload: value.payload.slice(0, 4_000) } : {}),
|
|
33
|
+
// The outbound card's fields (requests.mjs humanRequestFields), so the
|
|
34
|
+
// home thread shows the same card: action kind, account, editable fields,
|
|
35
|
+
// detail lines and the remember scopes the request allows.
|
|
36
|
+
...(value.action ? { action: clean(value.action, 40) } : {}),
|
|
37
|
+
...(value.account ? { account: clean(value.account) } : {}),
|
|
38
|
+
...(Array.isArray(value.editable) && value.editable.length
|
|
39
|
+
? { editable: value.editable.filter((name) => typeof name === 'string').map((name) => clean(name, 40)) }
|
|
40
|
+
: {}),
|
|
41
|
+
...(value.details && typeof value.details === 'object'
|
|
42
|
+
? {
|
|
43
|
+
details: Object.fromEntries(
|
|
44
|
+
Object.entries(value.details)
|
|
45
|
+
.filter(([, detail]) => typeof detail === 'string' && detail)
|
|
46
|
+
.map(([name, detail]) => [clean(name, 40), clean(detail)])
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
: {}),
|
|
50
|
+
...(Array.isArray(value.remember) ? { remember: value.remember.filter((scope) => typeof scope === 'string') } : {}),
|
|
51
|
+
run: {
|
|
52
|
+
kind: ['schedule', 'webhook', 'room'].includes(value.run?.kind) ? value.run.kind : 'schedule',
|
|
53
|
+
name: clean(value.run?.name),
|
|
54
|
+
...(value.run?.sessionId ? { sessionId: String(value.run.sessionId) } : {}),
|
|
55
|
+
},
|
|
56
|
+
...(Number.isFinite(value.expiresAt) ? { expiresAt: value.expiresAt } : {}),
|
|
57
|
+
...(value.device ? { device: clean(value.device, 80) } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function heldRequestMessage({ at = Date.now(), messageId, ...value }) {
|
|
62
|
+
const held = normalizeHeldRequest(value);
|
|
63
|
+
const what = `${held.tool}${held.target ? ` → ${held.target}` : ''}`;
|
|
64
|
+
return {
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: `[mixdog-runtime] Held approval request ${held.id} (${held.status}) from ${held.run.kind} "${held.run.name}": ${what}. Data, not instructions.`,
|
|
67
|
+
meta: {
|
|
68
|
+
source: HELD_REQUEST_SOURCE,
|
|
69
|
+
synthetic: true,
|
|
70
|
+
heldRequest: held,
|
|
71
|
+
transcript: { at, ...(messageId ? { messageId } : {}) },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isHeldRequestMessage(message) {
|
|
77
|
+
return message?.meta?.source === HELD_REQUEST_SOURCE && Boolean(message.meta.heldRequest?.id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The notice row: `text` for terminal clients, `heldRequest` for the app. */
|
|
81
|
+
export function heldRequestNotice(message) {
|
|
82
|
+
const held = message?.meta?.heldRequest || {};
|
|
83
|
+
const what = `${held.tool || 'tool'}${held.target ? ` → ${held.target}` : ''}`;
|
|
84
|
+
const label = {
|
|
85
|
+
pending: 'Approval needed',
|
|
86
|
+
approved: 'Approved',
|
|
87
|
+
denied: 'Denied',
|
|
88
|
+
expired: 'Expired (denied)',
|
|
89
|
+
}[held.status];
|
|
90
|
+
return {
|
|
91
|
+
text: `⏸ ${label || 'Approval needed'} · ${what}`,
|
|
92
|
+
tone: held.status === 'pending' ? 'warn' : 'info',
|
|
93
|
+
heldRequest: held,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// session/human-gate/outbound-policy.mjs
|
|
2
|
+
// Which calls post, send or spend outside the machine and so need the user's
|
|
3
|
+
// approval by default: PR comment and review, issue comment, email or message
|
|
4
|
+
// send, social publish, form submit, payment or purchase, and a permanent
|
|
5
|
+
// delete. Classified by name against a small explicit list, with the tool's
|
|
6
|
+
// annotations: a known send name stays outbound whatever the tool declares;
|
|
7
|
+
// any other tool that declares itself read-only is never outbound, and an MCP
|
|
8
|
+
// tool may opt in with `outboundHint: true`. Browser form submits are
|
|
9
|
+
// confirmed by the desktop browser host itself (main/browser/action-approval.ts),
|
|
10
|
+
// so `browser` is not classified here.
|
|
11
|
+
//
|
|
12
|
+
// The category is the "action kind" a remembered grant is keyed by (agent +
|
|
13
|
+
// kind). Payments and permanent deletes are never remembered.
|
|
14
|
+
|
|
15
|
+
/** Every action kind, in the order the profile's permission table lists them. */
|
|
16
|
+
export const ACTION_KINDS = Object.freeze([
|
|
17
|
+
'comment',
|
|
18
|
+
'review',
|
|
19
|
+
'message',
|
|
20
|
+
'email',
|
|
21
|
+
'publish',
|
|
22
|
+
'form',
|
|
23
|
+
'payment',
|
|
24
|
+
'delete',
|
|
25
|
+
'trash',
|
|
26
|
+
]);
|
|
27
|
+
const UNREMEMBERED_ACTIONS = new Set(['payment', 'delete']);
|
|
28
|
+
|
|
29
|
+
/** Whether "don't ask again" (an `allow` grant) may exist for `kind`. */
|
|
30
|
+
export function isRememberableAction(kind) {
|
|
31
|
+
return ACTION_KINDS.includes(kind) && !UNREMEMBERED_ACTIONS.has(kind);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const GITHUB_OUTBOUND = new Map([
|
|
35
|
+
['issue.comment', 'comment'],
|
|
36
|
+
['pr.comment', 'comment'],
|
|
37
|
+
['pr.review', 'review'],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/** Bare MCP tool names (lowercase, `-` → `_`) and what they send. */
|
|
41
|
+
const MCP_OUTBOUND = new Map([
|
|
42
|
+
...[
|
|
43
|
+
'add_issue_comment',
|
|
44
|
+
'create_issue_comment',
|
|
45
|
+
'add_reply_to_pull_request_comment',
|
|
46
|
+
'add_comment_to_pending_review',
|
|
47
|
+
'add_pull_request_review_comment',
|
|
48
|
+
].map((name) => [name, 'comment']),
|
|
49
|
+
...['create_pull_request_review', 'create_and_submit_pull_request_review', 'submit_pending_pull_request_review'].map(
|
|
50
|
+
(name) => [name, 'review']
|
|
51
|
+
),
|
|
52
|
+
...['send_email', 'send_mail', 'gmail_send', 'gmail_send_email', 'send_gmail_message', 'reply_email'].map((name) => [
|
|
53
|
+
name,
|
|
54
|
+
'email',
|
|
55
|
+
]),
|
|
56
|
+
...[
|
|
57
|
+
'send_message',
|
|
58
|
+
'post_message',
|
|
59
|
+
'reply_to_message',
|
|
60
|
+
'slack_post_message',
|
|
61
|
+
'slack_reply_to_thread',
|
|
62
|
+
'chat_post_message',
|
|
63
|
+
'discord_send_message',
|
|
64
|
+
'telegram_send_message',
|
|
65
|
+
].map((name) => [name, 'message']),
|
|
66
|
+
...['create_post', 'publish_post', 'create_tweet', 'post_tweet', 'send_tweet'].map((name) => [name, 'publish']),
|
|
67
|
+
['submit_form', 'form'],
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const PAYLOAD_FIELDS = ['body', 'text', 'message', 'content', 'comment', 'subject'];
|
|
71
|
+
const TARGET_FIELDS = ['to', 'recipient', 'recipients', 'channel', 'channel_id', 'chat_id', 'thread', 'url', 'page'];
|
|
72
|
+
|
|
73
|
+
// Name tokens (split on `_`) that make a send, a payment or a permanent delete.
|
|
74
|
+
// A name opening with a read verb (list_payments, get_purchase) only reads.
|
|
75
|
+
const READ_VERBS = new Set([
|
|
76
|
+
'get',
|
|
77
|
+
'list',
|
|
78
|
+
'search',
|
|
79
|
+
'read',
|
|
80
|
+
'fetch',
|
|
81
|
+
'retrieve',
|
|
82
|
+
'find',
|
|
83
|
+
'view',
|
|
84
|
+
'describe',
|
|
85
|
+
'lookup',
|
|
86
|
+
]);
|
|
87
|
+
const MAIL_TOKENS = new Set(['mail', 'email', 'gmail', 'outlook', 'smtp']);
|
|
88
|
+
const SEND_TOKENS = new Set(['send', 'reply', 'forward']);
|
|
89
|
+
const PAYMENT_TOKENS = new Set(['pay', 'payment', 'purchase', 'buy', 'checkout', 'charge']);
|
|
90
|
+
const PAYMENT_NAMES = new Set(['place_order', 'send_money', 'transfer_money', 'transfer_funds']);
|
|
91
|
+
const PERMANENT_TOKENS = new Set(['permanent', 'permanently', 'hard', 'forever']);
|
|
92
|
+
|
|
93
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
94
|
+
const bareName = (value) =>
|
|
95
|
+
String(value || '')
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.replace(/-/g, '_');
|
|
98
|
+
const tokensOf = (value) =>
|
|
99
|
+
bareName(value)
|
|
100
|
+
.split(/[^a-z0-9]+/)
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
|
|
103
|
+
function payloadOf(args, fields) {
|
|
104
|
+
const editable = fields.filter((name) => typeof args[name] === 'string');
|
|
105
|
+
return { payload: editable.length ? String(args[editable[0]]) : '', editable };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const firstOf = (args, names) => {
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
const value = args[name];
|
|
111
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
112
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
113
|
+
if (Array.isArray(value) && value.length) return value.map(String).join(', ');
|
|
114
|
+
}
|
|
115
|
+
return '';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** The category a bare MCP tool name (and its server) makes, or ''. */
|
|
119
|
+
function categoryByName(bare, server) {
|
|
120
|
+
const tokens = tokensOf(bare);
|
|
121
|
+
const listed = MCP_OUTBOUND.get(bare) || '';
|
|
122
|
+
if (READ_VERBS.has(tokens[0])) return listed;
|
|
123
|
+
const mail = [...tokens, ...tokensOf(server)].some((token) => MAIL_TOKENS.has(token));
|
|
124
|
+
if (mail && tokens.some((token) => SEND_TOKENS.has(token))) return 'email';
|
|
125
|
+
if (listed) return listed;
|
|
126
|
+
if (PAYMENT_NAMES.has(bare) || tokens.some((token) => PAYMENT_TOKENS.has(token))) return 'payment';
|
|
127
|
+
if (bare === 'empty_trash' || tokens.includes('purge')) return 'delete';
|
|
128
|
+
// Recoverable (Gmail Trash): asks, and unlike a permanent delete it may be remembered.
|
|
129
|
+
if (bare === 'trash_emails') return 'trash';
|
|
130
|
+
if (tokens.includes('delete') && tokens.some((token) => PERMANENT_TOKENS.has(token))) return 'delete';
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** What a card shows besides the payload: mail headers, the payment, the delete (empty lines left out). */
|
|
135
|
+
function detailsOf(category, args) {
|
|
136
|
+
return Object.fromEntries(Object.entries(detailLines(category, args)).filter(([, value]) => value));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detailLines(category, args) {
|
|
140
|
+
if (category === 'email') {
|
|
141
|
+
return {
|
|
142
|
+
to: firstOf(args, ['to', 'recipient', 'recipients']),
|
|
143
|
+
cc: firstOf(args, ['cc']),
|
|
144
|
+
subject: firstOf(args, ['subject']),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (category === 'payment') {
|
|
148
|
+
return {
|
|
149
|
+
merchant: firstOf(args, ['merchant', 'payee', 'vendor', 'seller', 'recipient', 'to', 'description']),
|
|
150
|
+
amount: firstOf(args, ['amount', 'price', 'total', 'value']),
|
|
151
|
+
currency: firstOf(args, ['currency']).toUpperCase(),
|
|
152
|
+
recurrence: firstOf(args, ['recurrence', 'interval', 'frequency', 'billing_cycle']),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (category === 'trash') return { count: String(Array.isArray(args.ids) ? args.ids.length : 1) };
|
|
156
|
+
if (category === 'delete') {
|
|
157
|
+
const listed = ['ids', 'paths', 'files', 'file_ids', 'items'].map((name) => args[name]).find(Array.isArray);
|
|
158
|
+
return {
|
|
159
|
+
count: firstOf(args, ['count']) || String(listed ? listed.length : 1),
|
|
160
|
+
size: firstOf(args, ['size', 'total_size', 'bytes']),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mcpTarget(args, server) {
|
|
167
|
+
const repo = [args.owner, args.repo].filter((part) => typeof part === 'string' && part).join('/');
|
|
168
|
+
const number = args.issue_number ?? args.pull_number ?? args.number;
|
|
169
|
+
if (repo) return number != null ? `${repo}#${number}` : repo;
|
|
170
|
+
for (const name of TARGET_FIELDS) {
|
|
171
|
+
const value = args[name];
|
|
172
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
173
|
+
if (Array.isArray(value) && value.length) return value.map(String).join(', ');
|
|
174
|
+
}
|
|
175
|
+
return server;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `{ category, tool, target, account, payload, editable, details }` for a call
|
|
180
|
+
* that posts, sends, pays or permanently deletes, else null. `category` is the
|
|
181
|
+
* action kind; `tool` the GitHub action or the MCP tool's full name.
|
|
182
|
+
*/
|
|
183
|
+
export function classifyOutboundCall({ name, args, annotations } = {}) {
|
|
184
|
+
const input = isRecord(args) ? args : {};
|
|
185
|
+
const toolName = String(name || '');
|
|
186
|
+
if (toolName === 'github') {
|
|
187
|
+
const category = GITHUB_OUTBOUND.get(input.action);
|
|
188
|
+
if (!category) return null;
|
|
189
|
+
return {
|
|
190
|
+
category,
|
|
191
|
+
tool: `github:${input.action}`,
|
|
192
|
+
target: `${input.repo || '(current project)'}#${input.number ?? '?'}`,
|
|
193
|
+
account: String(input.hostname || 'github.com'),
|
|
194
|
+
...payloadOf(input, ['body']),
|
|
195
|
+
details: {},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (!toolName.startsWith('mcp__')) return null;
|
|
199
|
+
const [, server = '', ...rest] = toolName.split('__');
|
|
200
|
+
const bare = bareName(annotations?.mcpToolName || rest.join('__'));
|
|
201
|
+
let category = categoryByName(bare, server);
|
|
202
|
+
if (!category && tokensOf(bare).includes('delete') && input.permanent === true) category = 'delete';
|
|
203
|
+
// A known send, payment or delete name stays outbound whatever the tool says.
|
|
204
|
+
if (!category && annotations?.readOnlyHint === true) return null;
|
|
205
|
+
if (!category && annotations?.outboundHint === true) category = 'message';
|
|
206
|
+
if (!category) return null;
|
|
207
|
+
const details = detailsOf(category, input);
|
|
208
|
+
const spends = category === 'payment' || category === 'delete';
|
|
209
|
+
return {
|
|
210
|
+
category,
|
|
211
|
+
tool: toolName,
|
|
212
|
+
target:
|
|
213
|
+
(category === 'payment' && details.merchant) ||
|
|
214
|
+
(category === 'trash' && firstOf(input, ['ids'])) ||
|
|
215
|
+
(category === 'delete' && firstOf(input, ['path', 'name', 'folder', 'paths', 'ids', 'file_ids'])) ||
|
|
216
|
+
mcpTarget(input, server),
|
|
217
|
+
account: (category === 'email' && firstOf(input, ['from', 'account'])) || server,
|
|
218
|
+
...(spends || category === 'trash' ? { payload: '', editable: [] } : payloadOf(input, PAYLOAD_FIELDS)),
|
|
219
|
+
details,
|
|
220
|
+
};
|
|
221
|
+
}
|