mixdog 1.2.6 → 1.3.0

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.
Files changed (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +7 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. 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
- const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
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;
@@ -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
  }
@@ -7,6 +7,7 @@ import { traceCacheBreak } from '../../cache-break-trace.mjs';
7
7
  import { resolveLiveToolCwd } from './tool-exec.mjs';
8
8
  import { notifySkillsApplied, prepareExplicitSkills, recordAppliedSkills } from '../explicit-skills.mjs';
9
9
  import { addUsage, usageDeltaEvent } from './usage.mjs';
10
+ import { isVisibleStreamProgress } from '../../../../shared/stream-progress.mjs';
10
11
  import { prepareProviderRequest } from './request-boundary.mjs';
11
12
  import { applyStepEffort } from './step-auto-effort.mjs';
12
13
  import { projectProviderRequest } from './request-projection.mjs';
@@ -181,6 +182,15 @@ export async function sendProviderRequest(state, round) {
181
182
  tools: round.sendTools,
182
183
  opts,
183
184
  });
185
+ // The first visible token of this request, for its generation time (queue,
186
+ // preflight and time-to-first-token stay out of the speed figures).
187
+ let firstVisibleAt = 0;
188
+ const callerOnStreamDelta = opts.onStreamDelta;
189
+ const observeStreamDelta = (kind, ...rest) => {
190
+ if (!firstVisibleAt && isVisibleStreamProgress(kind)) firstVisibleAt = Date.now();
191
+ return callerOnStreamDelta?.(kind, ...rest);
192
+ };
193
+ opts.onStreamDelta = observeStreamDelta;
184
194
  let result;
185
195
  try {
186
196
  result = await runWithProviderRequestToolsScope(round.requestToolScope, () =>
@@ -208,15 +218,19 @@ export async function sendProviderRequest(state, round) {
208
218
  sideQuestion?.discard();
209
219
  if (!(await advanceAgentRoute(state, error))) throw error;
210
220
  result = { action: 'retry_route' };
221
+ } finally {
222
+ if (opts.onStreamDelta === observeStreamDelta) opts.onStreamDelta = callerOnStreamDelta;
211
223
  }
212
224
  if (result.action === 'proceed') sideQuestion?.settle(result.response);
213
225
  else sideQuestion?.discard();
226
+ const sendEndedAt = Date.now();
214
227
  return {
215
228
  result,
216
229
  providerMessages,
217
230
  prefixGuardCandidate,
218
231
  sendStartedAt,
219
- sendEndedAt: Date.now(),
232
+ sendEndedAt,
233
+ generationMs: firstVisibleAt ? Math.max(0, sendEndedAt - firstVisibleAt) : 0,
220
234
  preSendMs,
221
235
  toolResumeMs,
222
236
  };
@@ -429,6 +443,7 @@ export function settleSendResult(state, round, sent) {
429
443
  usage: reportedUsage,
430
444
  sendTools: round.sendTools,
431
445
  toolCalls: response.toolCalls?.length || 0,
446
+ generationMs: sent.generationMs,
432
447
  })
433
448
  )
434
449
  );
@@ -6,10 +6,16 @@ import { resolvePreToolAskApproval } from '../tool-helpers.mjs';
6
6
  import { runAbortable, throwIfAborted } from '../../../../../shared/abort-race.mjs';
7
7
  import { evaluateExecPolicy } from '../../../tools/bash-policy-scan.mjs';
8
8
  import { beginSessionApprovalWait } from '../../manager/runtime-liveness.mjs';
9
+ import { agentMcpToolPermission } from '../../../mcp/agent-connectors.mjs';
10
+ import { humanGateDecision, recordHumanDecision } from '../../human-gate/gate.mjs';
11
+ import { applyEditedArgs } from '../../human-gate/requests.mjs';
9
12
 
10
13
  const isArgsObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
11
14
 
12
- async function askApproval(call, { callerSessionId, executeOpts, toolApprovalHook, decision }) {
15
+ async function askApproval(
16
+ call,
17
+ { callerSessionId, executeOpts, toolApprovalHook, decision, request = null, gate = null }
18
+ ) {
13
19
  const askReason = String(decision?.reason || 'approval requested by hook').trim();
14
20
  // While the user decides, the session is waiting on them, not working.
15
21
  const releaseApprovalWait = beginSessionApprovalWait(callerSessionId);
@@ -24,17 +30,45 @@ async function askApproval(call, { callerSessionId, executeOpts, toolApprovalHoo
24
30
  toolCallId: executeOpts.toolCallId || null,
25
31
  askReason,
26
32
  toolApprovalHook,
33
+ request,
27
34
  })
28
35
  );
29
36
  } finally {
30
37
  releaseApprovalWait();
31
38
  }
32
- if (askOutcome.denial) return { denial: askOutcome.denial };
33
39
  const approval = askOutcome.approval;
40
+ await recordHumanDecision(gate, approval);
41
+ if (askOutcome.denial) return { denial: askOutcome.denial };
34
42
  if (approval && typeof approval === 'object' && isArgsObject(approval.args)) call.args = approval.args;
43
+ // The card's edited text replaces the model's before the call runs.
44
+ if (request?.kind === 'outbound' && approval?.editedArgs) {
45
+ call.args = applyEditedArgs(call.args, approval.editedArgs, request.outbound?.editable);
46
+ }
47
+ // The user approved this very call on the richer outbound card.
48
+ if (request?.kind === 'outbound') call.outboundApproved = true;
35
49
  return null;
36
50
  }
37
51
 
52
+ export const CONNECTOR_APPROVAL_REASON = 'This connector tool is set to ask before it runs for this agent.';
53
+
54
+ /**
55
+ * A connector tool the running agent set to `needs_approval` asks through the
56
+ * ordinary approval hook, whatever the PreToolUse hooks decided. Run after
57
+ * them, on the final call: a renamed call is judged under its final name.
58
+ * A call the outbound card already got approved is not asked a second time.
59
+ * @returns {Promise<{ denial: string } | null>} the denial, or null to run
60
+ */
61
+ export function applyConnectorApproval(call, { agentId, callerSessionId, executeOpts, toolApprovalHook }) {
62
+ if (call.outboundApproved) return null;
63
+ if (agentMcpToolPermission(agentId, call.name) !== 'needs_approval') return null;
64
+ return askApproval(call, {
65
+ callerSessionId,
66
+ executeOpts,
67
+ toolApprovalHook,
68
+ decision: { reason: CONNECTOR_APPROVAL_REASON },
69
+ });
70
+ }
71
+
38
72
  /**
39
73
  * Elevation pass for a shell call (or a terminal `run`) whose exec policy
40
74
  * decision is 'ask' (elevated launch). Runs after the final call is known.
@@ -62,22 +96,40 @@ export async function applyBeforeToolHook({
62
96
  executeOpts,
63
97
  beforeToolHook,
64
98
  toolApprovalHook,
99
+ mcpScopeId = null,
65
100
  }) {
66
101
  const call = { name, args, cwd };
67
102
  // ask_user is itself the user prompt; it never goes through the ordinary approval gate.
68
- if (!beforeToolHook || name === 'ask_user') return call;
103
+ if (name === 'ask_user') return call;
69
104
  try {
70
- const decision = await runAbortable(executeOpts.signal, () =>
71
- beforeToolHook(
72
- {
73
- name,
74
- args,
75
- cwd,
76
- sessionId: callerSessionId,
77
- toolCallId: executeOpts.toolCallId || null,
78
- },
79
- { signal: executeOpts.signal }
80
- )
105
+ const hookDecision = beforeToolHook
106
+ ? await runAbortable(executeOpts.signal, () =>
107
+ beforeToolHook(
108
+ {
109
+ name,
110
+ args,
111
+ cwd,
112
+ sessionId: callerSessionId,
113
+ toolCallId: executeOpts.toolCallId || null,
114
+ },
115
+ { signal: executeOpts.signal }
116
+ )
117
+ )
118
+ : null;
119
+ // Outbound sends ask the user whatever the hooks allowed (human-gate/gate.mjs).
120
+ // Judged as the call will run: only a rewriting decision changes name/args.
121
+ const hookAction = String(hookDecision?.action || hookDecision?.decision || '').toLowerCase();
122
+ const rewritten = hookAction === 'ask' || hookAction === 'modify' || hookAction === 'rewrite';
123
+ const { decision, request, gate } = await humanGateDecision(
124
+ {
125
+ name:
126
+ rewritten && typeof hookDecision?.name === 'string' && hookDecision.name.trim()
127
+ ? hookDecision.name.trim()
128
+ : name,
129
+ args: rewritten && isArgsObject(hookDecision?.args) ? hookDecision.args : args,
130
+ },
131
+ hookDecision,
132
+ { sessionId: callerSessionId, mcpScopeId }
81
133
  );
82
134
  const action = String(decision?.action || decision?.decision || '').toLowerCase();
83
135
  if (action === 'deny' || action === 'block') {
@@ -89,7 +141,14 @@ export async function applyBeforeToolHook({
89
141
  if (typeof decision?.name === 'string' && decision.name.trim()) call.name = decision.name.trim();
90
142
  }
91
143
  if (action === 'ask') {
92
- const denied = await askApproval(call, { callerSessionId, executeOpts, toolApprovalHook, decision });
144
+ const denied = await askApproval(call, {
145
+ callerSessionId,
146
+ executeOpts,
147
+ toolApprovalHook,
148
+ decision,
149
+ request,
150
+ gate,
151
+ });
93
152
  if (denied) return denied;
94
153
  }
95
154
  } catch (error) {
@@ -14,7 +14,7 @@ import { runWithToolExecutionOwner } from '../../../../shared/tool-execution-own
14
14
  import { runWithLocalSearchTelemetry } from '../../tools/builtin/local-search-telemetry.mjs';
15
15
  import { throwIfAborted } from '../../../../shared/abort-race.mjs';
16
16
  import { prepareToolCall } from './tool-exec/call-prep.mjs';
17
- import { applyBeforeToolHook, applyShellElevationGate } from './tool-exec/before-hook.mjs';
17
+ import { applyBeforeToolHook, applyConnectorApproval, applyShellElevationGate } from './tool-exec/before-hook.mjs';
18
18
  import { dispatchToolCall } from './tool-exec/dispatch.mjs';
19
19
  import { applyAfterToolHook } from './tool-exec/after-hook.mjs';
20
20
  import { runReadOnlyIoWithDeadline } from './tool-exec/read-only-deadline.mjs';
@@ -50,8 +50,16 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
50
50
  executeOpts,
51
51
  beforeToolHook: hookFrom(executeOpts, sessionRef, 'beforeToolHook'),
52
52
  toolApprovalHook,
53
+ mcpScopeId: sessionRef?.mcpScopeId || null,
53
54
  });
54
55
  if (hooked.denial) return hooked.denial;
56
+ const connectorDenied = await applyConnectorApproval(hooked, {
57
+ agentId: sessionRef?.agent,
58
+ callerSessionId,
59
+ executeOpts,
60
+ toolApprovalHook,
61
+ });
62
+ if (connectorDenied) return connectorDenied.denial;
55
63
  // A decision can settle after its turn was cancelled. It must never
56
64
  // authorize a new mutation, even if the hook ignores the abort signal.
57
65
  throwIfAborted(executeOpts.signal);
@@ -27,12 +27,13 @@ function invalidateForToolSurfaceMigration(session) {
27
27
  }
28
28
 
29
29
  /**
30
- * After a surface re-apply outside a request (resume, provider switch)
31
- * migrated the exposure: the first send of the next turn carries the tag.
30
+ * After a surface re-apply outside a request (resume, provider switch, a
31
+ * workflow switch: `intent`) changed the request prefix: the first send of the
32
+ * next turn carries the tag.
32
33
  */
33
- export function markToolSurfaceMigrated(session) {
34
+ export function markToolSurfaceMigrated(session, intent = TOOL_SURFACE_MIGRATION_INTENT) {
34
35
  invalidateForToolSurfaceMigration(session);
35
- session.pendingCacheBreakIntent = TOOL_SURFACE_MIGRATION_INTENT;
36
+ session.pendingCacheBreakIntent = intent;
36
37
  }
37
38
 
38
39
  /** Provider switch: rebuild the surface for `provider`, migrating bridge ↔ native. */