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.
- package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +19 -0
- 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 +7 -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 +11 -4
- package/src/defaults/skills/setup-guide/SKILL.md +175 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- 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 +21 -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 +182 -13
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -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-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- 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/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 +12 -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 +10 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -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 +162 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -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 +165 -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/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -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 +7 -0
- 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/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 +27 -5
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -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/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 +24 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -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 +44 -1
- 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/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/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-route-config.d.mts +3 -0
- package/src/runtime/shared/agent-route-config.mjs +16 -0
- package/src/runtime/shared/atomic-file.mjs +14 -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 +52 -0
- package/src/runtime/shared/connector-catalog.mjs +230 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +43 -0
- package/src/runtime/shared/google-oauth-client.mjs +24 -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/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +28 -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/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 +56 -0
- package/src/runtime/shared/user-data-guard.mjs +114 -36
- package/src/session-runtime/agent-home-delivery.mjs +459 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +92 -0
- package/src/session-runtime/agent-task-sessions.mjs +200 -0
- package/src/session-runtime/boot/apis.mjs +50 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- 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/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 +31 -2
- 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/set-route.mjs +14 -3
- 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/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 +10 -0
- 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/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 +68 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
- package/src/session-runtime/setup-tool/settings-contract.mjs +8 -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 +238 -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 +3 -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 +556 -22
- 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 +11 -2
- package/src/standalone/daemon.mjs +18 -4
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +164 -86
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +35 -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 +48 -3
- 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 +6 -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 +56 -0
- package/src/tui/session/turn.mjs +22 -6
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// session/human-gate/requests.mjs
|
|
2
|
+
// The data model of a human-in-the-loop prompt. A request has one `kind`:
|
|
3
|
+
// approval — allow or deny one tool call;
|
|
4
|
+
// outbound — a call that posts, sends, pays or deletes outside (its action
|
|
5
|
+
// kind, target, account, the exact payload text, which fields
|
|
6
|
+
// the card may edit, and the mail/payment/delete detail lines);
|
|
7
|
+
// secret — the user types a value into a target field; the request names
|
|
8
|
+
// the target and the field label, never a value;
|
|
9
|
+
// takeover — the user does a step themselves and says "done";
|
|
10
|
+
// permission — the user picks one of the offered scopes.
|
|
11
|
+
// A response is one of HUMAN_RESPONSES plus a `remember` scope; a policy lists
|
|
12
|
+
// the scopes it allows and can leave out `always`. Nothing here ever carries a
|
|
13
|
+
// secret value: a decision that names one is stripped to its verdict.
|
|
14
|
+
|
|
15
|
+
export const HUMAN_REQUEST_KINDS = Object.freeze(['approval', 'outbound', 'secret', 'takeover', 'permission']);
|
|
16
|
+
export const HUMAN_RESPONSES = Object.freeze(['approve', 'deny', 'edit-and-approve', 'provide-secret', 'skip', 'done']);
|
|
17
|
+
export const REMEMBER_SCOPES = Object.freeze(['once', 'agent', 'always']);
|
|
18
|
+
export const SECRET_REQUEST_TIMEOUT_MS = 120_000;
|
|
19
|
+
|
|
20
|
+
const RESPONSES_BY_KIND = Object.freeze({
|
|
21
|
+
approval: ['approve', 'deny'],
|
|
22
|
+
outbound: ['approve', 'edit-and-approve', 'deny'],
|
|
23
|
+
secret: ['provide-secret', 'skip'],
|
|
24
|
+
takeover: ['done', 'deny'],
|
|
25
|
+
permission: ['approve', 'deny'],
|
|
26
|
+
});
|
|
27
|
+
const GRANTING_RESPONSES = new Set(['approve', 'edit-and-approve', 'provide-secret', 'done']);
|
|
28
|
+
const TEXT_LIMIT = 200;
|
|
29
|
+
/** An outbound card's extra lines: mail headers, the payment, the delete, the
|
|
30
|
+
* trash card's message list (gate.mjs describeEmails) and its remainder. */
|
|
31
|
+
const DETAIL_FIELDS = [
|
|
32
|
+
'to',
|
|
33
|
+
'cc',
|
|
34
|
+
'subject',
|
|
35
|
+
'merchant',
|
|
36
|
+
'amount',
|
|
37
|
+
'currency',
|
|
38
|
+
'recurrence',
|
|
39
|
+
'count',
|
|
40
|
+
'size',
|
|
41
|
+
'messages',
|
|
42
|
+
'more',
|
|
43
|
+
];
|
|
44
|
+
const DETAIL_LIMITS = { messages: 1_000 };
|
|
45
|
+
const PAYLOAD_LIMIT = 40_000;
|
|
46
|
+
|
|
47
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
48
|
+
const text = (value, limit = TEXT_LIMIT) => (typeof value === 'string' ? value.trim().slice(0, limit) : '');
|
|
49
|
+
|
|
50
|
+
/** The human kind of a request; anything else (ask_user's 'question', a
|
|
51
|
+
* hook's bare ask) is an ordinary approval for responses. */
|
|
52
|
+
export function humanRequestKind(request) {
|
|
53
|
+
return HUMAN_REQUEST_KINDS.includes(request?.kind) ? request.kind : 'approval';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A login takeover's sign-in page: https, or http on loopback only (a local
|
|
57
|
+
* site); never embedded credentials. `{ url, origin }`, or null. */
|
|
58
|
+
export function loginPageUrl(value) {
|
|
59
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 2048) return null;
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = new URL(value.trim());
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
67
|
+
const loopback =
|
|
68
|
+
host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
69
|
+
if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback)) return null;
|
|
70
|
+
if (parsed.username || parsed.password) return null;
|
|
71
|
+
return { url: parsed.href, origin: parsed.origin };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rememberScopes(value) {
|
|
75
|
+
const listed = Array.isArray(value) ? value.filter((scope) => REMEMBER_SCOPES.includes(scope)) : [];
|
|
76
|
+
return ['once', ...new Set(listed.filter((scope) => scope !== 'once'))];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The kind-specific fields of a request, whitelisted. Spread onto the
|
|
80
|
+
* presented request; `args` is replaced for a secret so no value rides it. */
|
|
81
|
+
export function humanRequestFields(input = {}) {
|
|
82
|
+
const kind = input?.kind;
|
|
83
|
+
if (!HUMAN_REQUEST_KINDS.includes(kind)) return {};
|
|
84
|
+
const fields = { remember: rememberScopes(input.remember) };
|
|
85
|
+
// The agent the request acts for (its name on the card and the remembered grant).
|
|
86
|
+
if (text(input.agentId)) fields.agentId = text(input.agentId);
|
|
87
|
+
if (kind === 'outbound' && isRecord(input.outbound)) {
|
|
88
|
+
const outbound = input.outbound;
|
|
89
|
+
const details = isRecord(outbound.details) ? outbound.details : {};
|
|
90
|
+
fields.outbound = {
|
|
91
|
+
category: text(outbound.category),
|
|
92
|
+
target: text(outbound.target),
|
|
93
|
+
account: text(outbound.account),
|
|
94
|
+
payload: typeof outbound.payload === 'string' ? outbound.payload.slice(0, PAYLOAD_LIMIT) : '',
|
|
95
|
+
editable: Array.isArray(outbound.editable) ? outbound.editable.filter((name) => typeof name === 'string') : [],
|
|
96
|
+
details: Object.fromEntries(
|
|
97
|
+
DETAIL_FIELDS.filter((name) => text(details[name])).map((name) => [
|
|
98
|
+
name,
|
|
99
|
+
text(details[name], DETAIL_LIMITS[name]),
|
|
100
|
+
])
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (kind === 'secret') {
|
|
105
|
+
const secret = isRecord(input.secret) ? input.secret : {};
|
|
106
|
+
fields.secret = {
|
|
107
|
+
requestId: text(secret.requestId),
|
|
108
|
+
surface: secret.surface === 'computer' ? 'computer' : 'browser',
|
|
109
|
+
target: text(secret.target),
|
|
110
|
+
field: text(secret.field),
|
|
111
|
+
};
|
|
112
|
+
fields.args = { surface: fields.secret.surface, target: fields.secret.target, field: fields.secret.field };
|
|
113
|
+
fields.remember = ['once'];
|
|
114
|
+
}
|
|
115
|
+
if (kind === 'takeover') {
|
|
116
|
+
const takeover = isRecord(input.takeover) ? input.takeover : {};
|
|
117
|
+
const surface = takeover.surface === 'computer' ? 'computer' : 'browser';
|
|
118
|
+
// The window it is about (ask-user-tool-defs.mjs handoffIdentity): a
|
|
119
|
+
// browser page ID, or a computer window id and app, so the card can bring
|
|
120
|
+
// exactly that window forward on the user's click.
|
|
121
|
+
const token = (value, limit) => (/^[\w.:#@-]+$/.test(text(value, limit)) ? text(value, limit) : '');
|
|
122
|
+
const identity =
|
|
123
|
+
surface === 'browser'
|
|
124
|
+
? { ...(token(takeover.tab, 64) ? { tab: token(takeover.tab, 64) } : {}) }
|
|
125
|
+
: {
|
|
126
|
+
...(token(takeover.windowId, 128) ? { windowId: token(takeover.windowId, 128) } : {}),
|
|
127
|
+
...(text(takeover.app, 120) ? { app: text(takeover.app, 120) } : {}),
|
|
128
|
+
};
|
|
129
|
+
// A login takeover (purpose 'login') opens the desktop's sign-in window on
|
|
130
|
+
// its validated page instead of handing over the side panel.
|
|
131
|
+
const login = surface === 'browser' && takeover.purpose === 'login' ? loginPageUrl(takeover.url) : null;
|
|
132
|
+
fields.takeover = {
|
|
133
|
+
surface,
|
|
134
|
+
instructions: text(takeover.instructions, 2_000),
|
|
135
|
+
...(text(takeover.target) ? { target: text(takeover.target) } : {}),
|
|
136
|
+
...identity,
|
|
137
|
+
...(login ? { purpose: 'login', url: login.url, origin: login.origin } : {}),
|
|
138
|
+
};
|
|
139
|
+
fields.remember = ['once'];
|
|
140
|
+
}
|
|
141
|
+
if (kind === 'permission') {
|
|
142
|
+
const scopes = Array.isArray(input.permission?.scopes) ? input.permission.scopes : [];
|
|
143
|
+
fields.permission = {
|
|
144
|
+
scopes: scopes
|
|
145
|
+
.filter((scope) => isRecord(scope) && text(scope.id))
|
|
146
|
+
.map((scope) => ({ id: text(scope.id), label: text(scope.label) || text(scope.id) })),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return fields;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Only `editable` string fields of `editedArgs`; {} when nothing applies. */
|
|
153
|
+
function editableArgs(editedArgs, editable) {
|
|
154
|
+
if (!isRecord(editedArgs)) return {};
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const name of editable || []) {
|
|
157
|
+
if (typeof editedArgs[name] === 'string') out[name] = editedArgs[name].slice(0, PAYLOAD_LIMIT);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The canonical response to `request` from a decision: `approved` (the card's
|
|
164
|
+
* polarity) plus optional `{ response, editedArgs, remember, scope }`. An
|
|
165
|
+
* unknown or contradicting response falls back to the polarity's default;
|
|
166
|
+
* edits survive only on an outbound request and only for its editable fields;
|
|
167
|
+
* `remember` only when the request allows it and the call was granted.
|
|
168
|
+
*/
|
|
169
|
+
export function normalizeHumanResponse(request, approved, extra = {}) {
|
|
170
|
+
const kind = humanRequestKind(request);
|
|
171
|
+
const allowed = RESPONSES_BY_KIND[kind];
|
|
172
|
+
const polarity = approved === true;
|
|
173
|
+
const fallback = polarity ? allowed[0] : allowed[allowed.length - 1];
|
|
174
|
+
const asked = isRecord(extra) && allowed.includes(extra.response) ? extra.response : '';
|
|
175
|
+
let response = asked && GRANTING_RESPONSES.has(asked) === polarity ? asked : fallback;
|
|
176
|
+
const result = { approved: polarity, response };
|
|
177
|
+
if (response === 'edit-and-approve') {
|
|
178
|
+
const edits = editableArgs(extra.editedArgs, request?.outbound?.editable);
|
|
179
|
+
if (Object.keys(edits).length) result.editedArgs = edits;
|
|
180
|
+
else response = result.response = 'approve';
|
|
181
|
+
}
|
|
182
|
+
const scopes = Array.isArray(request?.remember) ? request.remember : ['once'];
|
|
183
|
+
result.remember = polarity && scopes.includes(extra?.remember) ? extra.remember : 'once';
|
|
184
|
+
if (kind === 'permission' && polarity) {
|
|
185
|
+
const ids = (request?.permission?.scopes || []).map((scope) => scope.id);
|
|
186
|
+
if (ids.includes(extra?.scope)) result.scope = extra.scope;
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `args` with the card's edited fields in place of the model's. */
|
|
192
|
+
export function applyEditedArgs(args, editedArgs, editable) {
|
|
193
|
+
const edits = editableArgs(editedArgs, editable);
|
|
194
|
+
return Object.keys(edits).length ? { ...(isRecord(args) ? args : {}), ...edits } : args;
|
|
195
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// session/human-gate/store.mjs
|
|
2
|
+
// Per-agent permissions and the decision audit, both under the plugin data dir.
|
|
3
|
+
// A grant is keyed by agent + action kind (outbound-policy.mjs ACTION_KINDS)
|
|
4
|
+
// and says `allow` (never ask: "다시 묻지 않기") or `block` (deny without a
|
|
5
|
+
// card); no grant means ask. `agent` scope names the agent, `always` stands for
|
|
6
|
+
// every agent ('*'), and an agent's own grant wins over an every-agent one;
|
|
7
|
+
// `once` is never stored. Payments and deletes are never allowed. Grants are
|
|
8
|
+
// listed and revoked by id. The audit is one JSONL line per decision with
|
|
9
|
+
// agent, kind, action, tool, target, decision, deciding device and time —
|
|
10
|
+
// never a value, an argument or a payload — read back newest first.
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { parseJsonText, updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
|
|
15
|
+
import { ACTION_KINDS, isRememberableAction } from './outbound-policy.mjs';
|
|
16
|
+
|
|
17
|
+
const ANY_AGENT = '*';
|
|
18
|
+
const FIELD_LIMIT = 200;
|
|
19
|
+
const AUDIT_KEYS = ['agentId', 'kind', 'action', 'tool', 'target', 'decision', 'device'];
|
|
20
|
+
export const PERMISSION_MODES = Object.freeze(['ask', 'allow', 'block']);
|
|
21
|
+
|
|
22
|
+
const field = (value) => String(value ?? '').slice(0, FIELD_LIMIT);
|
|
23
|
+
const grantId = (agentId, action) =>
|
|
24
|
+
`grant_${createHash('sha256').update(`${agentId}\n${action}`).digest('hex').slice(0, 24)}`;
|
|
25
|
+
const isGrant = (row) => ACTION_KINDS.includes(row?.action) && (row.mode === 'allow' || row.mode === 'block');
|
|
26
|
+
|
|
27
|
+
export function createHumanGateStore(directory) {
|
|
28
|
+
const grantsFile = join(directory, 'grants.json');
|
|
29
|
+
const auditFile = join(directory, 'audit.jsonl');
|
|
30
|
+
const grantsOf = (doc) => (Array.isArray(doc?.grants) ? doc.grants.filter(isGrant) : []);
|
|
31
|
+
async function readGrants() {
|
|
32
|
+
try {
|
|
33
|
+
return grantsOf(parseJsonText(await readFile(grantsFile, 'utf8')));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code === 'ENOENT') return [];
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function updateGrants(mutate) {
|
|
40
|
+
await mkdir(directory, { recursive: true });
|
|
41
|
+
let result;
|
|
42
|
+
await updateJsonAtomic(grantsFile, (doc) => {
|
|
43
|
+
const out = mutate(grantsOf(doc));
|
|
44
|
+
if (!out) return undefined;
|
|
45
|
+
result = out.result;
|
|
46
|
+
return { grants: out.grants };
|
|
47
|
+
});
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/** Set `agentId`'s mode for `action`; `ask` removes its grant. Returns the
|
|
51
|
+
* grant, or null for `ask` and for an `allow` that may not exist. */
|
|
52
|
+
async function setMode({ agentId, action, mode, device = '' }) {
|
|
53
|
+
if (!ACTION_KINDS.includes(action) || !PERMISSION_MODES.includes(mode)) return null;
|
|
54
|
+
if (mode === 'allow' && !isRememberableAction(action)) return null;
|
|
55
|
+
const owner = field(agentId);
|
|
56
|
+
const id = grantId(owner, action);
|
|
57
|
+
const grant = mode === 'ask' ? null : { id, agentId: owner, action, mode, device: field(device), at: Date.now() };
|
|
58
|
+
await updateGrants((grants) => ({
|
|
59
|
+
grants: [...grants.filter((row) => row.id !== id), ...(grant ? [grant] : [])],
|
|
60
|
+
result: grant,
|
|
61
|
+
}));
|
|
62
|
+
return grant;
|
|
63
|
+
}
|
|
64
|
+
/** `allow`, `block` or `ask` for `agentId` + `action`; the agent's own grant wins. */
|
|
65
|
+
async function modeFor({ agentId, action }) {
|
|
66
|
+
const grants = (await readGrants()).filter((grant) => grant.action === action);
|
|
67
|
+
const own =
|
|
68
|
+
grants.find((grant) => grant.agentId === agentId) || grants.find((grant) => grant.agentId === ANY_AGENT);
|
|
69
|
+
if (own?.mode === 'allow' && !isRememberableAction(action)) return 'ask';
|
|
70
|
+
return own?.mode || 'ask';
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
setMode,
|
|
74
|
+
modeFor,
|
|
75
|
+
/** Stores an `allow` for `agent`/`always` scope; `once` stores nothing. Returns the grant or null. */
|
|
76
|
+
async grant({ agentId, action, scope, device = '' }) {
|
|
77
|
+
if (scope !== 'agent' && scope !== 'always') return null;
|
|
78
|
+
return setMode({ agentId: scope === 'always' ? ANY_AGENT : agentId, action, mode: 'allow', device });
|
|
79
|
+
},
|
|
80
|
+
/** Every action kind with `agentId`'s mode, for the profile's permission table. */
|
|
81
|
+
async permissions(agentId) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const action of ACTION_KINDS) {
|
|
84
|
+
out.push({ action, mode: await modeFor({ agentId, action }), rememberable: isRememberableAction(action) });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
},
|
|
88
|
+
/** Grants that apply to `agentId` (its own and every-agent ones); all when omitted. */
|
|
89
|
+
async listGrants({ agentId } = {}) {
|
|
90
|
+
const grants = await readGrants();
|
|
91
|
+
return agentId ? grants.filter((grant) => grant.agentId === agentId || grant.agentId === ANY_AGENT) : grants;
|
|
92
|
+
},
|
|
93
|
+
async revokeGrant(id) {
|
|
94
|
+
return (
|
|
95
|
+
(await updateGrants((grants) => {
|
|
96
|
+
const rest = grants.filter((grant) => grant.id !== id);
|
|
97
|
+
return rest.length === grants.length ? null : { grants: rest, result: true };
|
|
98
|
+
})) === true
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
async recordAudit(entry) {
|
|
102
|
+
const line = { at: Number.isFinite(entry?.at) ? entry.at : Date.now() };
|
|
103
|
+
for (const key of AUDIT_KEYS) line[key] = field(entry?.[key]);
|
|
104
|
+
await mkdir(dirname(auditFile), { recursive: true });
|
|
105
|
+
await appendFile(auditFile, `${JSON.stringify(line)}\n`, 'utf8');
|
|
106
|
+
return line;
|
|
107
|
+
},
|
|
108
|
+
/** Newest first; `agentId` filters to one agent. */
|
|
109
|
+
async readAudit({ agentId = '', limit = 100 } = {}) {
|
|
110
|
+
let raw = '';
|
|
111
|
+
try {
|
|
112
|
+
raw = await readFile(auditFile, 'utf8');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code === 'ENOENT') return [];
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const rows = [];
|
|
118
|
+
for (const line of raw.split('\n').reverse()) {
|
|
119
|
+
if (!line) continue;
|
|
120
|
+
try {
|
|
121
|
+
const row = JSON.parse(line);
|
|
122
|
+
if (!agentId || row.agentId === agentId) rows.push(row);
|
|
123
|
+
} catch {}
|
|
124
|
+
if (rows.length >= limit) break;
|
|
125
|
+
}
|
|
126
|
+
return rows;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let processStore = null;
|
|
132
|
+
/** The process store under the plugin data dir; built on first use. */
|
|
133
|
+
export async function humanGateStore() {
|
|
134
|
+
if (!processStore) {
|
|
135
|
+
const { getPluginData } = await import('../../config.mjs');
|
|
136
|
+
processStore = createHumanGateStore(join(getPluginData(), 'human-gate'));
|
|
137
|
+
}
|
|
138
|
+
return processStore;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function requiredAgentId(agentId) {
|
|
142
|
+
if (typeof agentId !== 'string' || !agentId.trim() || agentId.length > FIELD_LIMIT || agentId === ANY_AGENT) {
|
|
143
|
+
throw new Error('agent id is invalid');
|
|
144
|
+
}
|
|
145
|
+
return agentId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The profile's "권한" table for `agentId` (capability getAgentPermissions). */
|
|
149
|
+
export async function agentPermissions(agentId) {
|
|
150
|
+
return (await humanGateStore()).permissions(requiredAgentId(agentId));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Set one row of that table (capability setAgentPermission); returns the table. */
|
|
154
|
+
export async function setAgentPermission(agentId, action, mode) {
|
|
155
|
+
const agent = requiredAgentId(agentId);
|
|
156
|
+
if (!ACTION_KINDS.includes(action)) throw new Error(`action must be one of ${ACTION_KINDS.join(', ')}`);
|
|
157
|
+
if (!PERMISSION_MODES.includes(mode)) throw new Error(`mode must be one of ${PERMISSION_MODES.join(', ')}`);
|
|
158
|
+
if (mode === 'allow' && !isRememberableAction(action)) throw new Error(`${action} always asks; it cannot be allowed`);
|
|
159
|
+
const store = await humanGateStore();
|
|
160
|
+
await store.setMode({ agentId: agent, action, mode });
|
|
161
|
+
return store.permissions(agent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Replace the process store (tests); null restores the default. */
|
|
165
|
+
export function setHumanGateStore(store) {
|
|
166
|
+
processStore = store || null;
|
|
167
|
+
}
|
|
@@ -17,6 +17,7 @@ import { HANDOFF_TIMEOUT_MAX_MS } from '../compact/constants.mjs';
|
|
|
17
17
|
import { envFlag, envPositiveInt } from '../../../../shared/env.mjs';
|
|
18
18
|
import { positiveInt } from '../../../../shared/numbers.mjs';
|
|
19
19
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
20
|
+
import { isAgentModeSession } from '../agent-mode.mjs';
|
|
20
21
|
import { providerInputExcludesCache, providerNativePdf } from '../../providers/registry.mjs';
|
|
21
22
|
import { contentMediaBytes, contentMediaCount } from '../../providers/media-normalization.mjs';
|
|
22
23
|
import { splitProtectedContext } from '../compact/messages.mjs';
|
|
@@ -133,7 +134,8 @@ function effectiveContextWindowPercent(sessionRef, cfg) {
|
|
|
133
134
|
export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
134
135
|
if (!sessionRef) return null;
|
|
135
136
|
const cfg = sessionRef.compaction || {};
|
|
136
|
-
|
|
137
|
+
// Agent mode always compacts on its own: the user is never asked to.
|
|
138
|
+
const auto = (cfg.auto !== false || isAgentModeSession(sessionRef)) && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
137
139
|
if (!auto) return { auto: false };
|
|
138
140
|
const { contextWindow, boundaryTokens } = compactBoundaryFor(sessionRef, cfg);
|
|
139
141
|
if (!boundaryTokens) return null;
|
|
@@ -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
|
|
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(
|
|
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 (
|
|
103
|
+
if (name === 'ask_user') return call;
|
|
69
104
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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, {
|
|
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
|
-
*
|
|
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 =
|
|
36
|
+
session.pendingCacheBreakIntent = intent;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** Provider switch: rebuild the surface for `provider`, migrating bridge ↔ native. */
|