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,446 @@
|
|
|
1
|
+
// Mixdog's built-in Google connectors (Gmail, Google Calendar, Google Drive):
|
|
2
|
+
// a small MCP server answered in-process at a reserved address
|
|
3
|
+
// (https://<service>.mixdog.invalid/mcp, RFC 2606: never resolvable, so no
|
|
4
|
+
// request or token can leave for it). It is an ordinary remote MCP server to
|
|
5
|
+
// the client, so the existing MCP OAuth sign-in applies unchanged: the
|
|
6
|
+
// protected-resource metadata names Google as the authorization server, the
|
|
7
|
+
// sign-in runs with Mixdog's pre-registered Desktop client (PKCE S256, state,
|
|
8
|
+
// loopback redirect, access_type=offline; mcp/oauth-client.mjs), the tokens
|
|
9
|
+
// live in the keychain, and each MCP request carries the access token, which
|
|
10
|
+
// this server spends on the Gmail / Calendar / Drive REST APIs.
|
|
11
|
+
|
|
12
|
+
import { accountInstructions } from '../shared/connector-accounts.mjs';
|
|
13
|
+
|
|
14
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
15
|
+
const ORIGIN_SUFFIX = '.mixdog.invalid';
|
|
16
|
+
const GOOGLE_ISSUER = 'https://accounts.google.com';
|
|
17
|
+
const READ_CAP = 100_000;
|
|
18
|
+
|
|
19
|
+
const json = (status, body, headers = {}) =>
|
|
20
|
+
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } });
|
|
21
|
+
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
22
|
+
const textResult = (value) => ({ content: [{ type: 'text', text: value }] });
|
|
23
|
+
const errorResult = (message) => ({ content: [{ type: 'text', text: message }], isError: true });
|
|
24
|
+
|
|
25
|
+
class GoogleUnauthorized extends Error {}
|
|
26
|
+
class ToolInputError extends Error {}
|
|
27
|
+
|
|
28
|
+
function requireText(args, key) {
|
|
29
|
+
const value = text(args?.[key]);
|
|
30
|
+
if (!value) throw new ToolInputError(`${key} is required`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One Google REST call with the user's access token. */
|
|
35
|
+
async function google({ token, fetchImpl }, url, { method = 'GET', body, headers = {}, raw = false } = {}) {
|
|
36
|
+
const response = await fetchImpl(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: `Bearer ${token}`,
|
|
40
|
+
...(body && !headers['content-type'] ? { 'content-type': 'application/json' } : {}),
|
|
41
|
+
...headers,
|
|
42
|
+
},
|
|
43
|
+
body: typeof body === 'string' || body == null ? body : JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
if (response.status === 401) throw new GoogleUnauthorized();
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const detail = await response.json().catch(() => null);
|
|
48
|
+
throw new Error(detail?.error?.message || `Google answered ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
if (raw) return response;
|
|
51
|
+
return response.status === 204 ? null : response.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Gmail (gmail.send) ──────────────────────────────────────────────────────
|
|
55
|
+
const NO_HEADER_BREAKS = /[\r\n]/;
|
|
56
|
+
const encodedWord = (value) => `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
|
|
57
|
+
|
|
58
|
+
function mimeMessage({ to, cc, bcc, subject, body }) {
|
|
59
|
+
for (const [key, value] of Object.entries({ to, cc, bcc, subject })) {
|
|
60
|
+
if (value && NO_HEADER_BREAKS.test(value)) throw new ToolInputError(`${key} must be one line`);
|
|
61
|
+
}
|
|
62
|
+
const lines = [
|
|
63
|
+
`To: ${to}`,
|
|
64
|
+
cc ? `Cc: ${cc}` : '',
|
|
65
|
+
bcc ? `Bcc: ${bcc}` : '',
|
|
66
|
+
`Subject: ${encodedWord(subject)}`,
|
|
67
|
+
'MIME-Version: 1.0',
|
|
68
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
69
|
+
'Content-Transfer-Encoding: base64',
|
|
70
|
+
].filter(Boolean);
|
|
71
|
+
return `${lines.join('\r\n')}\r\n\r\n${Buffer.from(body, 'utf8').toString('base64')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const GMAIL_TOOLS = [
|
|
75
|
+
{
|
|
76
|
+
name: 'send_email',
|
|
77
|
+
description: 'Send a plain-text email from the signed-in Gmail account.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
to: { type: 'string', description: 'Recipients, comma-separated.' },
|
|
82
|
+
subject: { type: 'string' },
|
|
83
|
+
body: { type: 'string' },
|
|
84
|
+
cc: { type: 'string' },
|
|
85
|
+
bcc: { type: 'string' },
|
|
86
|
+
},
|
|
87
|
+
required: ['to', 'subject', 'body'],
|
|
88
|
+
},
|
|
89
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
90
|
+
async run(token, args) {
|
|
91
|
+
const raw = Buffer.from(
|
|
92
|
+
mimeMessage({
|
|
93
|
+
to: requireText(args, 'to'),
|
|
94
|
+
cc: text(args.cc),
|
|
95
|
+
bcc: text(args.bcc),
|
|
96
|
+
subject: text(args.subject),
|
|
97
|
+
body: String(args.body ?? ''),
|
|
98
|
+
})
|
|
99
|
+
).toString('base64url');
|
|
100
|
+
const sent = await google(token, 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: { raw },
|
|
103
|
+
});
|
|
104
|
+
return `Sent (message ${sent.id}).`;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// ── Google Calendar (calendar.events) ───────────────────────────────────────
|
|
110
|
+
const EVENTS = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
|
|
111
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
112
|
+
|
|
113
|
+
function when(value, timeZone) {
|
|
114
|
+
const at = text(value);
|
|
115
|
+
if (!at) return undefined;
|
|
116
|
+
if (DATE_ONLY.test(at)) return { date: at };
|
|
117
|
+
return { dateTime: at, ...(timeZone ? { timeZone } : {}) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function eventBody(args) {
|
|
121
|
+
const timeZone = text(args.timeZone);
|
|
122
|
+
const body = {
|
|
123
|
+
summary: text(args.summary) || undefined,
|
|
124
|
+
description: typeof args.description === 'string' ? args.description : undefined,
|
|
125
|
+
location: text(args.location) || undefined,
|
|
126
|
+
start: when(args.start, timeZone),
|
|
127
|
+
end: when(args.end, timeZone),
|
|
128
|
+
attendees: Array.isArray(args.attendees)
|
|
129
|
+
? args.attendees.map((email) => ({ email: text(email) })).filter((row) => row.email)
|
|
130
|
+
: undefined,
|
|
131
|
+
};
|
|
132
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const eventLine = (event) =>
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
id: event.id,
|
|
138
|
+
summary: event.summary,
|
|
139
|
+
start: event.start?.dateTime || event.start?.date,
|
|
140
|
+
end: event.end?.dateTime || event.end?.date,
|
|
141
|
+
location: event.location,
|
|
142
|
+
attendees: event.attendees?.map((row) => row.email),
|
|
143
|
+
link: event.htmlLink,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const EVENT_FIELDS = {
|
|
147
|
+
summary: { type: 'string' },
|
|
148
|
+
start: { type: 'string', description: 'RFC 3339 date-time, or YYYY-MM-DD for all day.' },
|
|
149
|
+
end: { type: 'string', description: 'RFC 3339 date-time, or YYYY-MM-DD (exclusive) for all day.' },
|
|
150
|
+
timeZone: { type: 'string', description: 'IANA zone, e.g. Asia/Seoul.' },
|
|
151
|
+
description: { type: 'string' },
|
|
152
|
+
location: { type: 'string' },
|
|
153
|
+
attendees: { type: 'array', items: { type: 'string' }, description: 'Guest emails.' },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const CALENDAR_TOOLS = [
|
|
157
|
+
{
|
|
158
|
+
name: 'list_events',
|
|
159
|
+
description: 'List or search events on the primary calendar.',
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
timeMin: { type: 'string', description: 'RFC 3339; default now.' },
|
|
164
|
+
timeMax: { type: 'string', description: 'RFC 3339.' },
|
|
165
|
+
query: { type: 'string', description: 'Free-text search.' },
|
|
166
|
+
maxResults: { type: 'number', description: '1-100, default 25.' },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
170
|
+
async run(token, args) {
|
|
171
|
+
const params = new URLSearchParams({
|
|
172
|
+
singleEvents: 'true',
|
|
173
|
+
orderBy: 'startTime',
|
|
174
|
+
timeMin: text(args.timeMin) || new Date().toISOString(),
|
|
175
|
+
maxResults: String(Math.min(100, Math.max(1, Number(args.maxResults) || 25))),
|
|
176
|
+
});
|
|
177
|
+
if (text(args.timeMax)) params.set('timeMax', text(args.timeMax));
|
|
178
|
+
if (text(args.query)) params.set('q', text(args.query));
|
|
179
|
+
const list = await google(token, `${EVENTS}?${params}`);
|
|
180
|
+
const items = list.items || [];
|
|
181
|
+
return items.length ? items.map(eventLine).join('\n') : 'No events.';
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'create_event',
|
|
186
|
+
description: 'Create an event on the primary calendar.',
|
|
187
|
+
inputSchema: { type: 'object', properties: EVENT_FIELDS, required: ['summary', 'start', 'end'] },
|
|
188
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
189
|
+
async run(token, args) {
|
|
190
|
+
requireText(args, 'summary');
|
|
191
|
+
requireText(args, 'start');
|
|
192
|
+
requireText(args, 'end');
|
|
193
|
+
return eventLine(await google(token, EVENTS, { method: 'POST', body: eventBody(args) }));
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'update_event',
|
|
198
|
+
description: 'Change fields of an event on the primary calendar.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: { eventId: { type: 'string' }, ...EVENT_FIELDS },
|
|
202
|
+
required: ['eventId'],
|
|
203
|
+
},
|
|
204
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
205
|
+
async run(token, args) {
|
|
206
|
+
const id = encodeURIComponent(requireText(args, 'eventId'));
|
|
207
|
+
return eventLine(await google(token, `${EVENTS}/${id}`, { method: 'PATCH', body: eventBody(args) }));
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'delete_event',
|
|
212
|
+
description: 'Delete an event from the primary calendar.',
|
|
213
|
+
inputSchema: { type: 'object', properties: { eventId: { type: 'string' } }, required: ['eventId'] },
|
|
214
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
215
|
+
async run(token, args) {
|
|
216
|
+
const id = encodeURIComponent(requireText(args, 'eventId'));
|
|
217
|
+
await google(token, `${EVENTS}/${id}`, { method: 'DELETE' });
|
|
218
|
+
return 'Deleted.';
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// ── Google Drive (drive.file: files this app created or the user opened with it)
|
|
224
|
+
const DRIVE = 'https://www.googleapis.com/drive/v3/files';
|
|
225
|
+
const FILE_FIELDS = 'id,name,mimeType,modifiedTime,webViewLink';
|
|
226
|
+
const EXPORT_AS = {
|
|
227
|
+
'application/vnd.google-apps.document': 'text/plain',
|
|
228
|
+
'application/vnd.google-apps.spreadsheet': 'text/csv',
|
|
229
|
+
'application/vnd.google-apps.presentation': 'text/plain',
|
|
230
|
+
};
|
|
231
|
+
const TEXT_LIKE = /^text\/|^application\/(?:json|xml|javascript|x-yaml|csv)/;
|
|
232
|
+
const quoteQuery = (value) => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
233
|
+
|
|
234
|
+
const DRIVE_TOOLS = [
|
|
235
|
+
{
|
|
236
|
+
name: 'search_files',
|
|
237
|
+
description: 'List or search Drive files Mixdog can see (ones it created or you opened with it).',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
query: { type: 'string', description: 'Words in the file name.' },
|
|
242
|
+
pageSize: { type: 'number', description: '1-100, default 25.' },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
246
|
+
async run(token, args) {
|
|
247
|
+
const terms = ['trashed = false'];
|
|
248
|
+
if (text(args.query)) terms.push(`name contains '${quoteQuery(text(args.query))}'`);
|
|
249
|
+
const params = new URLSearchParams({
|
|
250
|
+
q: terms.join(' and '),
|
|
251
|
+
fields: `files(${FILE_FIELDS})`,
|
|
252
|
+
orderBy: 'modifiedTime desc',
|
|
253
|
+
pageSize: String(Math.min(100, Math.max(1, Number(args.pageSize) || 25))),
|
|
254
|
+
});
|
|
255
|
+
const list = await google(token, `${DRIVE}?${params}`);
|
|
256
|
+
const files = list.files || [];
|
|
257
|
+
return files.length ? files.map((file) => JSON.stringify(file)).join('\n') : 'No files.';
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'read_file',
|
|
262
|
+
description: 'Read a Drive file as text (Docs, Sheets as CSV, Slides, text files).',
|
|
263
|
+
inputSchema: { type: 'object', properties: { fileId: { type: 'string' } }, required: ['fileId'] },
|
|
264
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
265
|
+
async run(token, args) {
|
|
266
|
+
const id = encodeURIComponent(requireText(args, 'fileId'));
|
|
267
|
+
const meta = await google(token, `${DRIVE}/${id}?fields=${FILE_FIELDS}`);
|
|
268
|
+
const exportAs = EXPORT_AS[meta.mimeType];
|
|
269
|
+
let url = '';
|
|
270
|
+
if (exportAs) url = `${DRIVE}/${id}/export?mimeType=${encodeURIComponent(exportAs)}`;
|
|
271
|
+
else if (TEXT_LIKE.test(meta.mimeType || '')) url = `${DRIVE}/${id}?alt=media`;
|
|
272
|
+
if (!url) return `${JSON.stringify(meta)}\n(Not a text file; open it at the link.)`;
|
|
273
|
+
const body = await (await google(token, url, { raw: true })).text();
|
|
274
|
+
const clipped = body.length > READ_CAP ? `${body.slice(0, READ_CAP)}\n[truncated]` : body;
|
|
275
|
+
return `${JSON.stringify(meta)}\n\n${clipped}`;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'create_file',
|
|
280
|
+
description: 'Create a text file (or a Google Doc) in Drive.',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
name: { type: 'string' },
|
|
285
|
+
content: { type: 'string' },
|
|
286
|
+
mimeType: { type: 'string', description: 'Default text/plain.' },
|
|
287
|
+
asGoogleDoc: { type: 'boolean', description: 'Convert to a Google Doc.' },
|
|
288
|
+
folderId: { type: 'string' },
|
|
289
|
+
},
|
|
290
|
+
required: ['name', 'content'],
|
|
291
|
+
},
|
|
292
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
293
|
+
async run(token, args) {
|
|
294
|
+
const metadata = {
|
|
295
|
+
name: requireText(args, 'name'),
|
|
296
|
+
...(args.asGoogleDoc === true ? { mimeType: 'application/vnd.google-apps.document' } : {}),
|
|
297
|
+
...(text(args.folderId) ? { parents: [text(args.folderId)] } : {}),
|
|
298
|
+
};
|
|
299
|
+
const boundary = `mixdog-${Date.now().toString(36)}`;
|
|
300
|
+
const body = [
|
|
301
|
+
`--${boundary}`,
|
|
302
|
+
'Content-Type: application/json; charset=UTF-8',
|
|
303
|
+
'',
|
|
304
|
+
JSON.stringify(metadata),
|
|
305
|
+
`--${boundary}`,
|
|
306
|
+
`Content-Type: ${text(args.mimeType) || 'text/plain'}; charset=UTF-8`,
|
|
307
|
+
'',
|
|
308
|
+
String(args.content ?? ''),
|
|
309
|
+
`--${boundary}--`,
|
|
310
|
+
'',
|
|
311
|
+
].join('\r\n');
|
|
312
|
+
const file = await google(
|
|
313
|
+
token,
|
|
314
|
+
`https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=${FILE_FIELDS}`,
|
|
315
|
+
{
|
|
316
|
+
method: 'POST',
|
|
317
|
+
body,
|
|
318
|
+
headers: { 'content-type': `multipart/related; boundary=${boundary}` },
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
return JSON.stringify(file);
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const SERVICES = {
|
|
327
|
+
gmail: { name: 'Gmail', tools: GMAIL_TOOLS },
|
|
328
|
+
calendar: { name: 'Google Calendar', tools: CALENDAR_TOOLS },
|
|
329
|
+
drive: { name: 'Google Drive', tools: DRIVE_TOOLS },
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** The reserved MCP address of built-in Google service `service`. */
|
|
333
|
+
export function googleMcpUrl(service) {
|
|
334
|
+
return `https://${service}${ORIGIN_SUFFIX}/mcp`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The built-in service a URL addresses, or null. */
|
|
338
|
+
export function googleMcpService(input) {
|
|
339
|
+
let url;
|
|
340
|
+
try {
|
|
341
|
+
url = new URL(String(input));
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (url.protocol !== 'https:' || !url.hostname.endsWith(ORIGIN_SUFFIX)) return null;
|
|
346
|
+
return Object.hasOwn(SERVICES, url.hostname.slice(0, -ORIGIN_SUFFIX.length)) ? url : null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const bearerOf = (headers) => /^Bearer\s+(\S+)$/i.exec(headers.get('authorization') || '')?.[1] || '';
|
|
350
|
+
|
|
351
|
+
/** `{ ok, email }`: whether Google still accepts `token` (an expired or
|
|
352
|
+
* revoked one is not), and the account it was issued for ('' when unnamed). */
|
|
353
|
+
async function tokenInfo({ token, fetchImpl }) {
|
|
354
|
+
const response = await fetchImpl(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`);
|
|
355
|
+
if (!response.ok) return { ok: false, email: '' };
|
|
356
|
+
const info = await response.json().catch(() => null);
|
|
357
|
+
return { ok: true, email: text(info?.email) };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function rpc(service, token, message) {
|
|
361
|
+
const { id, method, params } = message || {};
|
|
362
|
+
const reply = (result) => ({ jsonrpc: '2.0', id, result });
|
|
363
|
+
if (method === 'initialize') {
|
|
364
|
+
const info = await tokenInfo(token);
|
|
365
|
+
if (!info.ok) throw new GoogleUnauthorized();
|
|
366
|
+
return reply({
|
|
367
|
+
protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
|
|
368
|
+
capabilities: { tools: { listChanged: false } },
|
|
369
|
+
serverInfo: { name: `mixdog-${service.key}`, version: '1.0.0' },
|
|
370
|
+
instructions: accountInstructions(service.name, info.email),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (method === 'ping') return reply({});
|
|
374
|
+
if (method === 'tools/list') {
|
|
375
|
+
// Several accounts of one service are separate servers: each tool says whose it is.
|
|
376
|
+
const { email } = await tokenInfo(token).catch(() => ({ email: '' }));
|
|
377
|
+
return reply({
|
|
378
|
+
tools: service.tools.map(({ run, ...tool }) => ({
|
|
379
|
+
...tool,
|
|
380
|
+
...(email ? { description: `${tool.description} (${service.name} account: ${email})` } : {}),
|
|
381
|
+
})),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (method === 'tools/call') {
|
|
385
|
+
const tool = service.tools.find((candidate) => candidate.name === params?.name);
|
|
386
|
+
if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
|
|
387
|
+
try {
|
|
388
|
+
return reply(textResult(await tool.run(token, params?.arguments || {})));
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof GoogleUnauthorized) throw error;
|
|
391
|
+
return reply(errorResult(error?.message || String(error)));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Answers a request to a built-in Google service like a remote MCP server. */
|
|
398
|
+
async function answer(url, init, fetchImpl) {
|
|
399
|
+
const key = url.hostname.slice(0, -ORIGIN_SUFFIX.length);
|
|
400
|
+
const service = { key, ...SERVICES[key] };
|
|
401
|
+
const resource = googleMcpUrl(key);
|
|
402
|
+
const metadataUrl = `https://${url.hostname}/.well-known/oauth-protected-resource/mcp`;
|
|
403
|
+
if (url.pathname.startsWith('/.well-known/oauth-protected-resource')) {
|
|
404
|
+
return json(200, { resource, authorization_servers: [GOOGLE_ISSUER], bearer_methods_supported: ['header'] });
|
|
405
|
+
}
|
|
406
|
+
if (url.pathname !== '/mcp') return json(404, { error: 'not_found' });
|
|
407
|
+
const method = String(init?.method || 'GET').toUpperCase();
|
|
408
|
+
if (method !== 'POST') return new Response(null, { status: 405, headers: { allow: 'POST' } });
|
|
409
|
+
const unauthorized = () =>
|
|
410
|
+
json(401, { error: 'invalid_token' }, { 'www-authenticate': `Bearer resource_metadata="${metadataUrl}"` });
|
|
411
|
+
const bearer = bearerOf(new Headers(init?.headers));
|
|
412
|
+
if (!bearer) return unauthorized();
|
|
413
|
+
const token = { token: bearer, fetchImpl };
|
|
414
|
+
let message;
|
|
415
|
+
try {
|
|
416
|
+
message = JSON.parse(typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body));
|
|
417
|
+
} catch {
|
|
418
|
+
return json(400, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
419
|
+
}
|
|
420
|
+
const batch = Array.isArray(message) ? message : [message];
|
|
421
|
+
try {
|
|
422
|
+
const replies = (
|
|
423
|
+
await Promise.all(batch.map((one) => (one?.id === undefined ? null : rpc(service, token, one))))
|
|
424
|
+
).filter(Boolean);
|
|
425
|
+
if (!replies.length) return new Response(null, { status: 202 });
|
|
426
|
+
return json(200, Array.isArray(message) ? replies : replies[0]);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error instanceof GoogleUnauthorized) return unauthorized();
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** A `fetch` that answers the built-in Google services in-process and sends
|
|
434
|
+
* every other request (Google's sign-in and token endpoints) to `next`. */
|
|
435
|
+
export function withGoogleMcp(next = fetch) {
|
|
436
|
+
return (input, init) => {
|
|
437
|
+
const url = googleMcpService(input instanceof Request ? input.url : input);
|
|
438
|
+
return url
|
|
439
|
+
? answer(
|
|
440
|
+
url,
|
|
441
|
+
input instanceof Request ? { method: input.method, headers: input.headers, body: init?.body } : init,
|
|
442
|
+
next
|
|
443
|
+
)
|
|
444
|
+
: next(input, init);
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Who may read and write which core-memory pool. Pools: common memory
|
|
2
|
+
// (project_id and agent_id NULL), a project's (project_id), and an agent's own
|
|
3
|
+
// (agent_id, project_id NULL).
|
|
4
|
+
//
|
|
5
|
+
// The caller arrives as `memory_caller` `{ kind, agentId }`, resolved by the
|
|
6
|
+
// session runtime from the calling session
|
|
7
|
+
// (session-runtime/services/memory-access.mjs); the model never supplies it. A
|
|
8
|
+
// call without one is the host acting for the user (desktop panels, TUI,
|
|
9
|
+
// admin routes).
|
|
10
|
+
// - common/project memory: every caller reads it; only a conversation with
|
|
11
|
+
// the user (Mixy's home, a Lead session, an agent's home thread) or the
|
|
12
|
+
// host changes it;
|
|
13
|
+
// - an agent's memory: that agent alone reads and writes it, from its home
|
|
14
|
+
// thread or its own unattended runs; Mixy and the host may also list and
|
|
15
|
+
// delete it. An unattended run (schedule, webhook, room member, delegated
|
|
16
|
+
// agent) therefore writes nowhere else, so external content cannot plant
|
|
17
|
+
// standing instructions in a pool another session reads.
|
|
18
|
+
// A deleted agent's pool is retained, unreferenced: its rows stay until the
|
|
19
|
+
// user deletes them, and an agent created later under the same id sees them.
|
|
20
|
+
import { agentHomeSessionId } from '../../agent/orchestrator/session/agent-home.mjs';
|
|
21
|
+
|
|
22
|
+
// 'host' names the absent caller explicitly; claiming it grants nothing a call
|
|
23
|
+
// without `memory_caller` would not.
|
|
24
|
+
const CALLER_KINDS = new Set(['host', 'human', 'agent', 'room-member', 'headless']);
|
|
25
|
+
const INTERACTIVE_KINDS = new Set(['host', 'human', 'agent']);
|
|
26
|
+
|
|
27
|
+
export const HOST_MEMORY_CALLER = Object.freeze({ kind: 'host', agentId: '', interactive: true });
|
|
28
|
+
|
|
29
|
+
/** `value` as a normalized agent id, or ''. */
|
|
30
|
+
export function validAgentId(value) {
|
|
31
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
32
|
+
return id && agentHomeSessionId(id) ? id : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The caller `{ kind, agentId, interactive }` of a `memory_caller` value; an
|
|
36
|
+
* unrecognized one is an unattended run without an agent (fail closed). */
|
|
37
|
+
export function normalizeMemoryCaller(value) {
|
|
38
|
+
if (value == null || value.kind === 'host') return HOST_MEMORY_CALLER;
|
|
39
|
+
const kind = CALLER_KINDS.has(value?.kind) ? value.kind : 'headless';
|
|
40
|
+
const agentId = validAgentId(value?.agentId);
|
|
41
|
+
if (kind === 'agent' && !agentId) return { kind: 'headless', agentId: '', interactive: false };
|
|
42
|
+
return { kind, agentId, interactive: INTERACTIVE_KINDS.has(kind) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mayManageAgents = (caller) => caller.kind === 'host' || caller.kind === 'human';
|
|
46
|
+
|
|
47
|
+
export function corePoolLabel({ projectId = null, agentId = null } = {}) {
|
|
48
|
+
if (agentId) return `agent=${agentId}`;
|
|
49
|
+
return `project=${projectId ?? 'COMMON'}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Throws unless `caller` may read agent `agentId`'s pool (none: common/project). */
|
|
53
|
+
export function assertCoreRead(caller, agentId) {
|
|
54
|
+
if (!agentId || caller.agentId === agentId || mayManageAgents(caller)) return;
|
|
55
|
+
throw new Error(`the memory of agent "${agentId}" is readable only by that agent`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Throws unless `caller` may `op` ('add' | 'edit' | 'delete') an entry of `pool`. */
|
|
59
|
+
export function assertCoreWrite(caller, op, pool = {}) {
|
|
60
|
+
const agentId = pool.agentId || null;
|
|
61
|
+
if (agentId) {
|
|
62
|
+
if (caller.agentId === agentId || (op === 'delete' && mayManageAgents(caller))) return;
|
|
63
|
+
throw new Error(`memory ${op} refused: the memory of agent "${agentId}" is written only by that agent`);
|
|
64
|
+
}
|
|
65
|
+
if (caller.interactive) return;
|
|
66
|
+
throw new Error(
|
|
67
|
+
`memory ${op} refused: ${pool.projectId ? `project memory "${pool.projectId}"` : 'common memory'} changes only in a conversation with the user; an unattended run writes only its own agent memory (agent_id "self")`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -44,6 +44,7 @@ function normalizeCuratedEntry(row) {
|
|
|
44
44
|
indexRevision: row?.indexRevision ?? row?.index_revision ?? null,
|
|
45
45
|
summary,
|
|
46
46
|
projectId: normalizeProjectId(row?.projectId ?? row?.project_id),
|
|
47
|
+
agentId: normalizeProjectId(row?.agentId ?? row?.agent_id),
|
|
47
48
|
updatedAt: finiteNumber(row?.updatedAt ?? row?.updated_at),
|
|
48
49
|
};
|
|
49
50
|
}
|
|
@@ -115,7 +116,7 @@ export async function refreshCoreMemoryFile(db, dataDir) {
|
|
|
115
116
|
const revision = reserveRevision(dataDir);
|
|
116
117
|
const directory = await syncCoreMemoryIndexes(db);
|
|
117
118
|
const curatedResult = await db.query(`
|
|
118
|
-
SELECT id, summary, project_id, updated_at
|
|
119
|
+
SELECT id, summary, project_id, agent_id, updated_at
|
|
119
120
|
FROM core_entries
|
|
120
121
|
WHERE status IS NULL OR status = 'active'
|
|
121
122
|
ORDER BY project_id NULLS FIRST, id ASC
|
|
@@ -129,16 +130,23 @@ export async function refreshCoreMemoryFile(db, dataDir) {
|
|
|
129
130
|
);
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
/** Common, then the `cwd` project's, then agent `agentId`'s own entries (none
|
|
134
|
+
* without an agent); the caller's byte budget cuts from the end. */
|
|
135
|
+
export function readSessionCoreMemoryPayload(dataDir, cwd, agentId = '') {
|
|
133
136
|
const file = readCoreMemoryFile(dataDir);
|
|
134
137
|
if (!file) return null;
|
|
135
138
|
const projectId = resolveProjectScope(cwd);
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
141
|
-
}
|
|
139
|
+
const agent = normalizeProjectId(agentId);
|
|
140
|
+
const rank = (entry) => {
|
|
141
|
+
if (entry.agentId !== null) return agent !== null && entry.agentId === agent ? 2 : -1;
|
|
142
|
+
if (entry.projectId === null) return 0;
|
|
143
|
+
return entry.projectId === projectId ? 1 : -1;
|
|
144
|
+
};
|
|
145
|
+
const curated = file.curated
|
|
146
|
+
.map((entry) => ({ entry, rank: rank(entry) }))
|
|
147
|
+
.filter(({ rank: value }) => value >= 0)
|
|
148
|
+
.sort((a, b) => a.rank - b.rank || a.entry.id - b.entry.id)
|
|
149
|
+
.map(({ entry }) => entry);
|
|
142
150
|
return {
|
|
143
151
|
projectId,
|
|
144
152
|
revision: file.revision,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { corePoolLabel } from './core-memory-access.mjs';
|
|
2
3
|
|
|
3
4
|
const PREFIX = 'memory.core.index.';
|
|
4
|
-
|
|
5
|
+
// A project scope keys as before (`null` for common, `"slug"`); an agent's own
|
|
6
|
+
// pool as `{"agent":"id"}`.
|
|
7
|
+
const scopeKey = (scope, agentId = null) =>
|
|
8
|
+
agentId ? JSON.stringify({ agent: agentId }) : JSON.stringify(scope ?? null);
|
|
5
9
|
|
|
6
10
|
// Stable database keys remain internal. This persisted directory is the
|
|
7
11
|
// public, per-scope address space, including its stale-request fence.
|
|
@@ -10,7 +14,7 @@ export async function syncCoreMemoryIndexes(db) {
|
|
|
10
14
|
await tx.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [PREFIX]);
|
|
11
15
|
const records = (
|
|
12
16
|
await tx.query(`
|
|
13
|
-
SELECT id, project_id FROM core_entries
|
|
17
|
+
SELECT id, project_id, agent_id FROM core_entries
|
|
14
18
|
WHERE status IS NULL OR status = 'active' ORDER BY id ASC
|
|
15
19
|
`)
|
|
16
20
|
).rows;
|
|
@@ -24,7 +28,7 @@ export async function syncCoreMemoryIndexes(db) {
|
|
|
24
28
|
const groups = new Map();
|
|
25
29
|
for (const row of records) {
|
|
26
30
|
if (!Number.isSafeInteger(Number(row.id)) || Number(row.id) <= 0) throw new Error('invalid internal memory key');
|
|
27
|
-
const key = scopeKey(row.project_id);
|
|
31
|
+
const key = scopeKey(row.project_id, row.agent_id);
|
|
28
32
|
if (!groups.has(key)) groups.set(key, []);
|
|
29
33
|
groups.get(key).push(Number(row.id));
|
|
30
34
|
}
|
|
@@ -55,7 +59,7 @@ export async function syncCoreMemoryIndexes(db) {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
export function indexedCoreRecord(row, directory) {
|
|
58
|
-
const state = directory.get(scopeKey(row.project_id ?? row.projectId));
|
|
62
|
+
const state = directory.get(scopeKey(row.project_id ?? row.projectId, row.agent_id ?? row.agentId));
|
|
59
63
|
const index = state?.recordIds.indexOf(Number(row.id)) ?? -1;
|
|
60
64
|
return {
|
|
61
65
|
...row,
|
|
@@ -65,23 +69,25 @@ export function indexedCoreRecord(row, directory) {
|
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
/** `agentId` addresses that agent's own pool instead of the project `scope`. */
|
|
73
|
+
export async function resolveCoreMemoryIndex(db, scope, index, revision, agentId = null) {
|
|
74
|
+
if (scope === '*' && !agentId) throw new Error('memory writes require one explicit project scope');
|
|
70
75
|
if (!Number.isSafeInteger(Number(index)) || Number(index) <= 0)
|
|
71
76
|
throw new Error('memory index must be a positive integer');
|
|
72
77
|
if (typeof revision !== 'string' || !revision)
|
|
73
78
|
throw new Error('index_revision required; list memories before editing or deleting');
|
|
74
79
|
const directory = await syncCoreMemoryIndexes(db);
|
|
75
|
-
const state = directory.get(scopeKey(scope));
|
|
80
|
+
const state = directory.get(scopeKey(scope, agentId));
|
|
76
81
|
if (!state || state.revision !== revision)
|
|
77
82
|
throw new Error('memory indices changed; list memories again before editing or deleting');
|
|
78
83
|
const recordId = state.recordIds[Number(index) - 1];
|
|
79
|
-
if (!recordId) throw new Error(`no memory index=${index} in
|
|
84
|
+
if (!recordId) throw new Error(`no memory index=${index} in ${corePoolLabel({ projectId: scope, agentId })}`);
|
|
80
85
|
return recordId;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
export async function publicCoreMemoryIdentity(db, row) {
|
|
84
89
|
const indexed = indexedCoreRecord(row, await syncCoreMemoryIndexes(db));
|
|
85
90
|
if (!indexed.id) throw new Error('memory changed before its index could be returned; list memories again');
|
|
86
|
-
|
|
91
|
+
const pool = corePoolLabel({ projectId: row.project_id, agentId: row.agent_id });
|
|
92
|
+
return `${pool} id=${indexed.id} index_revision=${indexed.index_revision}`;
|
|
87
93
|
}
|