xaq-cli 0.0.0 → 0.1.0-nightly.72
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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/admin-5gThRwgj.js +162 -0
- package/dist/admin-5gThRwgj.js.map +1 -0
- package/dist/admin-Btrm8mCd.js +218 -0
- package/dist/admin-Btrm8mCd.js.map +1 -0
- package/dist/admin-CWofnT_Q.js +2 -0
- package/dist/api-BFpElwqK.js +89 -0
- package/dist/api-BFpElwqK.js.map +1 -0
- package/dist/audit-BB7jK9Z5.js +2 -0
- package/dist/audit-BVYknW2i.js +79 -0
- package/dist/audit-BVYknW2i.js.map +1 -0
- package/dist/auth-CXmBdMqw.js +2 -0
- package/dist/auth-D-GmwNv-.js +550 -0
- package/dist/auth-D-GmwNv-.js.map +1 -0
- package/dist/brand--ZB8jTn1.js +32 -0
- package/dist/brand--ZB8jTn1.js.map +1 -0
- package/dist/bus-Cz0flS37.js +12 -0
- package/dist/bus-Cz0flS37.js.map +1 -0
- package/dist/bus-OKIGut72.js +2 -0
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-BNVLjYXA.js +167 -0
- package/dist/commands-BNVLjYXA.js.map +1 -0
- package/dist/commands-CdA6-WE7.js +112 -0
- package/dist/commands-CdA6-WE7.js.map +1 -0
- package/dist/config-C9XmYZWO.js +94 -0
- package/dist/config-C9XmYZWO.js.map +1 -0
- package/dist/config-CNVi0v5t.js +2 -0
- package/dist/connect-TtJ9n0Rw.js +362 -0
- package/dist/connect-TtJ9n0Rw.js.map +1 -0
- package/dist/content-C1lKMxFS.js +1170 -0
- package/dist/content-C1lKMxFS.js.map +1 -0
- package/dist/content-worker.js +28 -0
- package/dist/content-worker.js.map +1 -0
- package/dist/crypto-fv2fj3S8.js +75 -0
- package/dist/crypto-fv2fj3S8.js.map +1 -0
- package/dist/db-BEUEUPyP.js +561 -0
- package/dist/db-BEUEUPyP.js.map +1 -0
- package/dist/db-Q5BQuwga.js +2 -0
- package/dist/errors-FbMS3QBo.js +39 -0
- package/dist/errors-FbMS3QBo.js.map +1 -0
- package/dist/filter-Ck6OMxvu.js +158 -0
- package/dist/filter-Ck6OMxvu.js.map +1 -0
- package/dist/hermes-CKyzt1mH.js +3 -0
- package/dist/hermes-DHnDTYJp.js +1023 -0
- package/dist/hermes-DHnDTYJp.js.map +1 -0
- package/dist/import-DsN5NZT3.js +910 -0
- package/dist/import-DsN5NZT3.js.map +1 -0
- package/dist/instance-CwWbFbJ8.js +2 -0
- package/dist/instance-Dj9TpJB2.js +195 -0
- package/dist/instance-Dj9TpJB2.js.map +1 -0
- package/dist/listen-B9iO7HgQ.js +1568 -0
- package/dist/listen-B9iO7HgQ.js.map +1 -0
- package/dist/listen-CXVKG9XD.js +2 -0
- package/dist/manage-Bo6COB4p.js +477 -0
- package/dist/manage-Bo6COB4p.js.map +1 -0
- package/dist/manage-DoXqvjnU.js +2 -0
- package/dist/mcpheader-B8c5YO1B.js +2 -0
- package/dist/mcpheader-C9H4vudu.js +185 -0
- package/dist/mcpheader-C9H4vudu.js.map +1 -0
- package/dist/mcpsetup-BnHudwpq.js +2 -0
- package/dist/mcpsetup-DXXo2qjf.js +1053 -0
- package/dist/mcpsetup-DXXo2qjf.js.map +1 -0
- package/dist/node-check-CjTk6VNL.js +24 -0
- package/dist/node-check-CjTk6VNL.js.map +1 -0
- package/dist/paths-d-C2kdjo.js +2 -0
- package/dist/paths-yzOKD6ow.js +46 -0
- package/dist/paths-yzOKD6ow.js.map +1 -0
- package/dist/profile-CcMZ4hn7.js +2 -0
- package/dist/profile-NmMKAczz.js +222 -0
- package/dist/profile-NmMKAczz.js.map +1 -0
- package/dist/prompt-CKMQx_MJ.js +114 -0
- package/dist/prompt-CKMQx_MJ.js.map +1 -0
- package/dist/recipes-C7TIDlRj.js +3 -0
- package/dist/recipes-DG_-doGq.js +314 -0
- package/dist/recipes-DG_-doGq.js.map +1 -0
- package/dist/send-Dk-t8ia-.js +108 -0
- package/dist/send-Dk-t8ia-.js.map +1 -0
- package/dist/server-CDqDp1q3.js +20640 -0
- package/dist/server-CDqDp1q3.js.map +1 -0
- package/dist/service-BVn3soZZ.js +766 -0
- package/dist/service-BVn3soZZ.js.map +1 -0
- package/dist/service-BaWGPmSd.js +2 -0
- package/dist/service-C7jfo-Ex.js +457 -0
- package/dist/service-C7jfo-Ex.js.map +1 -0
- package/dist/service-DxDyKDQ-.js +2 -0
- package/dist/settings-B5qSHxRM.js +134 -0
- package/dist/settings-B5qSHxRM.js.map +1 -0
- package/dist/settings-DQvZaMN1.js +2 -0
- package/dist/setup-token-CwuSzfLK.js +48 -0
- package/dist/setup-token-CwuSzfLK.js.map +1 -0
- package/dist/skill/SKILL.md +445 -0
- package/dist/skill-1UQj3qgW.js +69 -0
- package/dist/skill-1UQj3qgW.js.map +1 -0
- package/dist/skill-pelq18JQ.js +2 -0
- package/dist/ssrf-BlVpGZ7C.js +181 -0
- package/dist/ssrf-BlVpGZ7C.js.map +1 -0
- package/dist/types-BhJDbSzE.js +135 -0
- package/dist/types-BhJDbSzE.js.map +1 -0
- package/dist/ui-B8Sd4Y7Y.js +81 -0
- package/dist/ui-B8Sd4Y7Y.js.map +1 -0
- package/dist/urls-Dp8pBGMU.js +2 -0
- package/dist/urls-U_LfYV61.js +27 -0
- package/dist/urls-U_LfYV61.js.map +1 -0
- package/dist/version-BxH7OuWD.js +20 -0
- package/dist/version-BxH7OuWD.js.map +1 -0
- package/dist/web/assets/_agentId-DonOCOlC.js +1 -0
- package/dist/web/assets/_app-B7t5lEia.js +1 -0
- package/dist/web/assets/_app-DMTGqYpM.js +65 -0
- package/dist/web/assets/_connectionId-DR5vFXOg.js +1 -0
- package/dist/web/assets/_webhookId-LfUlsULp.js +1 -0
- package/dist/web/assets/account-status-CEszs13C.js +1 -0
- package/dist/web/assets/accounts-5m7aiXuZ.js +1 -0
- package/dist/web/assets/accounts-list-YfJzThZa.js +1 -0
- package/dist/web/assets/activity-DOIe7SAg.js +1 -0
- package/dist/web/assets/add-connection-C8JR-13R.js +1 -0
- package/dist/web/assets/agent-create-form-CPxnPYH4.js +1 -0
- package/dist/web/assets/agents-B2Qj5KxT.js +1 -0
- package/dist/web/assets/approvals-BGaVaoFy.js +1 -0
- package/dist/web/assets/approve._id-BLrQnzPM.js +1 -0
- package/dist/web/assets/arrow-left-DZVAXoKj.js +1 -0
- package/dist/web/assets/arrow-right-C2zcqRr2.js +1 -0
- package/dist/web/assets/attention-gO9Nw5uW.js +1 -0
- package/dist/web/assets/audit-BPXXre8S.js +1 -0
- package/dist/web/assets/audit-bits-CGyIprzK.js +1 -0
- package/dist/web/assets/bell-ring-ig3zn1iS.js +1 -0
- package/dist/web/assets/bot-Zk76q_39.js +1 -0
- package/dist/web/assets/calendar-notice-DsCfq3VJ.js +1 -0
- package/dist/web/assets/calendar-plus-Cycfkk0y.js +1 -0
- package/dist/web/assets/calendar-setup-Pn61Wj1n.js +2 -0
- package/dist/web/assets/card-GSO5o-Ug.js +1 -0
- package/dist/web/assets/check-check-Ds1bPIGv.js +1 -0
- package/dist/web/assets/checkbox-B937U-Fd.js +1 -0
- package/dist/web/assets/chevron-right-Cqh4mDk1.js +1 -0
- package/dist/web/assets/chips-input-2xWDofmE.js +1 -0
- package/dist/web/assets/choice-cards-CgAIqBGX.js +1 -0
- package/dist/web/assets/circle-x-DcU-0zVh.js +1 -0
- package/dist/web/assets/command-khhto70y.js +1 -0
- package/dist/web/assets/common-BPZV8TFb.js +1 -0
- package/dist/web/assets/connect-agent-guide-CJdw31p2.js +1 -0
- package/dist/web/assets/connect-host-lr2c3xWh.js +6 -0
- package/dist/web/assets/connection-row-DYx_5o0c.js +1 -0
- package/dist/web/assets/connections-C4NOFb5R.js +1 -0
- package/dist/web/assets/data-table-kW1pkRB5.js +1 -0
- package/dist/web/assets/empty-DzTDioBP.js +1 -0
- package/dist/web/assets/events-C9zJnPKK.js +1 -0
- package/dist/web/assets/eye-B9gT1QSq.js +1 -0
- package/dist/web/assets/eye-off-Diktqz_g.js +1 -0
- package/dist/web/assets/field-C6ejurju.js +1 -0
- package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/web/assets/get-started-C38bq1gZ.js +2 -0
- package/dist/web/assets/hidden-chars-Dr9TxfvF.js +1 -0
- package/dist/web/assets/index-DGzo3kGh.js +11 -0
- package/dist/web/assets/index-zKOCQLGX.css +2 -0
- package/dist/web/assets/input-CWwJgozn.js +1 -0
- package/dist/web/assets/input-group-HG8OXcJ4.js +1 -0
- package/dist/web/assets/label-multi-select-sH8mllw8.js +1 -0
- package/dist/web/assets/labels-B5UqVwFo.js +1 -0
- package/dist/web/assets/laptop-BxJ4Nbj7.js +1 -0
- package/dist/web/assets/last-call-D-O5O94z.js +1 -0
- package/dist/web/assets/listener-handled-HNCuVrQ7.js +1 -0
- package/dist/web/assets/login-DaeKclcf.js +1 -0
- package/dist/web/assets/mail-C5fm2pTC.js +1 -0
- package/dist/web/assets/mcp-call-UoOTPFyh.js +1 -0
- package/dist/web/assets/mcp-call-card-Cr-OEDX2.js +3 -0
- package/dist/web/assets/message-square-quote-se8Bf3RP.js +1 -0
- package/dist/web/assets/mutations-CLPqJ6cj.js +1 -0
- package/dist/web/assets/network-igNw0VVx.js +1 -0
- package/dist/web/assets/pairing-XjuaVr2u.js +1 -0
- package/dist/web/assets/pairing-notifier-KPzOEZSO.js +1 -0
- package/dist/web/assets/paperclip-znS4iTrg.js +1 -0
- package/dist/web/assets/password-input-DV3aqcNf.js +1 -0
- package/dist/web/assets/pause-o-dK3khK.js +1 -0
- package/dist/web/assets/pencil-DkPFAhEq.js +1 -0
- package/dist/web/assets/play-DvCHND93.js +1 -0
- package/dist/web/assets/plug-CXAniEiR.js +1 -0
- package/dist/web/assets/plug-zap-BwR-l5wb.js +1 -0
- package/dist/web/assets/plus-B_TUslu3.js +1 -0
- package/dist/web/assets/preload-helper-Cc_aKyUb.js +1 -0
- package/dist/web/assets/public-url-rF2Xjyel.js +1 -0
- package/dist/web/assets/queries-Cv3nXx0U.js +42 -0
- package/dist/web/assets/refresh-cw-Ch5FNrby.js +1 -0
- package/dist/web/assets/revealed-keys-C_9NtJPY.js +1 -0
- package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/web/assets/rotate-ccw-B-mzPDk0.js +1 -0
- package/dist/web/assets/search-CTVKGomS.js +1 -0
- package/dist/web/assets/select-Bb806bpq.js +1 -0
- package/dist/web/assets/send-DuMVuYs1.js +1 -0
- package/dist/web/assets/settings-BWauhfFf.js +1 -0
- package/dist/web/assets/setup-B7bNKQOp.js +1 -0
- package/dist/web/assets/sheet-CYPwpUsH.js +1 -0
- package/dist/web/assets/shield-alert-CSYB-ThV.js +1 -0
- package/dist/web/assets/shield-check-BulpsYqK.js +1 -0
- package/dist/web/assets/sparkles-CUTiob4K.js +1 -0
- package/dist/web/assets/stamp-BNXUGNa3.js +1 -0
- package/dist/web/assets/step-url-51aMNak-.js +1 -0
- package/dist/web/assets/stream-Du7fY5_Y.js +1 -0
- package/dist/web/assets/tag-I5_-1jFL.js +1 -0
- package/dist/web/assets/terminal-BvxDFTJc.js +1 -0
- package/dist/web/assets/test-email-button-DYi7V_6X.js +1 -0
- package/dist/web/assets/toggle-group-Bdo1gmQ1.js +1 -0
- package/dist/web/assets/trash-Ci0aICEl.js +1 -0
- package/dist/web/assets/use-delivery-speed-BBh7W2q6.js +1 -0
- package/dist/web/assets/use-label-names-BXicesnP.js +1 -0
- package/dist/web/assets/use-sign-out-CPrY_oVO.js +1 -0
- package/dist/web/assets/useInfiniteQuery-DJLIM8yJ.js +1 -0
- package/dist/web/assets/useQuery-MrTwwNy5.js +1 -0
- package/dist/web/assets/useRouterState-D4OBK8l5.js +1 -0
- package/dist/web/assets/webhook-DjeeZiNC.js +1 -0
- package/dist/web/assets/webhook-bits-jw-y2D3u.js +1 -0
- package/dist/web/assets/webhook-sheet-B8h0skDe.js +34 -0
- package/dist/web/assets/webhooks-Cfsdpoyd.js +1 -0
- package/dist/web/assets/x-MF2TZ9aG.js +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +27 -0
- package/package.json +98 -1
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { chmodSync } from "node:fs";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
//#region server/db/schema.ts
|
|
5
|
+
const MIGRATIONS = [
|
|
6
|
+
`
|
|
7
|
+
CREATE TABLE settings (
|
|
8
|
+
key TEXT PRIMARY KEY,
|
|
9
|
+
value TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE TABLE admin_sessions (
|
|
13
|
+
token_hash TEXT PRIMARY KEY,
|
|
14
|
+
created_at TEXT NOT NULL,
|
|
15
|
+
expires_at TEXT NOT NULL,
|
|
16
|
+
ip TEXT,
|
|
17
|
+
user_agent TEXT
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE accounts (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
email TEXT NOT NULL UNIQUE,
|
|
23
|
+
display_name TEXT,
|
|
24
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
25
|
+
last_error TEXT,
|
|
26
|
+
refresh_token_enc TEXT NOT NULL,
|
|
27
|
+
access_token_enc TEXT,
|
|
28
|
+
access_expires_at TEXT,
|
|
29
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
30
|
+
history_id TEXT,
|
|
31
|
+
watch_expires_at TEXT,
|
|
32
|
+
last_sync_at TEXT,
|
|
33
|
+
created_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- Pending OAuth flows (state → PKCE verifier). Short-lived.
|
|
37
|
+
CREATE TABLE oauth_states (
|
|
38
|
+
state TEXT PRIMARY KEY,
|
|
39
|
+
code_verifier TEXT NOT NULL,
|
|
40
|
+
redirect_uri TEXT NOT NULL,
|
|
41
|
+
reauth_account_id TEXT,
|
|
42
|
+
created_at TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE agents (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
name TEXT NOT NULL,
|
|
48
|
+
description TEXT,
|
|
49
|
+
key_prefix TEXT NOT NULL,
|
|
50
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
51
|
+
disabled INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
last_used_at TEXT,
|
|
53
|
+
created_at TEXT NOT NULL
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE grants (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
59
|
+
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
60
|
+
capabilities TEXT NOT NULL, -- JSON Capability[]
|
|
61
|
+
label_scope TEXT, -- JSON string[] | NULL (= whole mailbox)
|
|
62
|
+
send_mode TEXT NOT NULL DEFAULT 'draft_only',
|
|
63
|
+
allow_to TEXT NOT NULL DEFAULT '[]', -- JSON string[]
|
|
64
|
+
reply_only INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
max_sends_per_hour INTEGER,
|
|
66
|
+
max_sends_per_day INTEGER,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
UNIQUE (agent_id, account_id)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- Agent event-feed cursor for the MCP pull tools (list_events / wait_for_events / ack_events).
|
|
72
|
+
CREATE TABLE agent_cursors (
|
|
73
|
+
agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
|
|
74
|
+
acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
updated_at TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE events (
|
|
79
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
id TEXT NOT NULL UNIQUE,
|
|
81
|
+
type TEXT NOT NULL,
|
|
82
|
+
account_id TEXT NOT NULL,
|
|
83
|
+
gmail_message_id TEXT,
|
|
84
|
+
gmail_thread_id TEXT,
|
|
85
|
+
label_ids TEXT NOT NULL DEFAULT '[]', -- JSON: message labels at event time (used for scope + filters)
|
|
86
|
+
message TEXT, -- JSON MessageSummary | NULL
|
|
87
|
+
changed_label_ids TEXT, -- JSON string[] for label.added/removed
|
|
88
|
+
detail TEXT, -- JSON
|
|
89
|
+
dedupe_key TEXT NOT NULL UNIQUE, -- e.g. "acc_x:msgid:message.received" — Pub/Sub is at-least-once
|
|
90
|
+
created_at TEXT NOT NULL
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX events_account_seq ON events(account_id, seq);
|
|
93
|
+
CREATE INDEX events_thread ON events(account_id, gmail_thread_id);
|
|
94
|
+
CREATE INDEX events_created ON events(created_at);
|
|
95
|
+
|
|
96
|
+
-- Threads the account has sent into; drives thread.reply_received.
|
|
97
|
+
CREATE TABLE sent_threads (
|
|
98
|
+
account_id TEXT NOT NULL,
|
|
99
|
+
gmail_thread_id TEXT NOT NULL,
|
|
100
|
+
last_sent_at TEXT NOT NULL,
|
|
101
|
+
PRIMARY KEY (account_id, gmail_thread_id)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE TABLE webhooks (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
107
|
+
name TEXT NOT NULL,
|
|
108
|
+
url TEXT NOT NULL,
|
|
109
|
+
secret_enc TEXT NOT NULL,
|
|
110
|
+
secret_prefix TEXT NOT NULL,
|
|
111
|
+
event_types TEXT NOT NULL, -- JSON EventType[]
|
|
112
|
+
filter TEXT NOT NULL DEFAULT '{}', -- JSON WebhookFilter
|
|
113
|
+
include_body INTEGER NOT NULL DEFAULT 0,
|
|
114
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
115
|
+
created_at TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE TABLE deliveries (
|
|
119
|
+
id TEXT PRIMARY KEY,
|
|
120
|
+
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
121
|
+
event_seq INTEGER NOT NULL,
|
|
122
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
123
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
124
|
+
next_attempt_at TEXT,
|
|
125
|
+
last_status_code INTEGER,
|
|
126
|
+
last_error TEXT,
|
|
127
|
+
last_response TEXT,
|
|
128
|
+
duration_ms INTEGER,
|
|
129
|
+
created_at TEXT NOT NULL,
|
|
130
|
+
updated_at TEXT NOT NULL,
|
|
131
|
+
UNIQUE (webhook_id, event_seq)
|
|
132
|
+
);
|
|
133
|
+
CREATE INDEX deliveries_due ON deliveries(status, next_attempt_at);
|
|
134
|
+
CREATE INDEX deliveries_webhook ON deliveries(webhook_id, created_at);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE approvals (
|
|
137
|
+
id TEXT PRIMARY KEY,
|
|
138
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
139
|
+
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
140
|
+
kind TEXT NOT NULL,
|
|
141
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
142
|
+
draft_id TEXT NOT NULL,
|
|
143
|
+
payload TEXT NOT NULL, -- JSON {to,cc,bcc,subject,body,threadId,attachments,reason}
|
|
144
|
+
decided_at TEXT,
|
|
145
|
+
decision_note TEXT,
|
|
146
|
+
sent_message_id TEXT,
|
|
147
|
+
error TEXT,
|
|
148
|
+
created_at TEXT NOT NULL,
|
|
149
|
+
expires_at TEXT NOT NULL
|
|
150
|
+
);
|
|
151
|
+
CREATE INDEX approvals_status ON approvals(status, created_at);
|
|
152
|
+
|
|
153
|
+
CREATE TABLE audit_log (
|
|
154
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
|
+
ts TEXT NOT NULL,
|
|
156
|
+
actor_type TEXT NOT NULL,
|
|
157
|
+
actor_id TEXT,
|
|
158
|
+
action TEXT NOT NULL,
|
|
159
|
+
account_id TEXT,
|
|
160
|
+
target TEXT,
|
|
161
|
+
outcome TEXT NOT NULL,
|
|
162
|
+
reason TEXT,
|
|
163
|
+
duration_ms INTEGER,
|
|
164
|
+
detail TEXT
|
|
165
|
+
);
|
|
166
|
+
CREATE INDEX audit_ts ON audit_log(ts);
|
|
167
|
+
CREATE INDEX audit_actor ON audit_log(actor_id, ts);
|
|
168
|
+
`,
|
|
169
|
+
`
|
|
170
|
+
CREATE TABLE pairings (
|
|
171
|
+
id TEXT PRIMARY KEY,
|
|
172
|
+
device_code_hash TEXT NOT NULL UNIQUE, -- sha256 of the CLI's polling secret
|
|
173
|
+
user_code TEXT NOT NULL, -- shown in the terminal and the console, e.g. "K7QM-2XPD"
|
|
174
|
+
name TEXT NOT NULL,
|
|
175
|
+
description TEXT,
|
|
176
|
+
requested TEXT NOT NULL DEFAULT '{}', -- JSON
|
|
177
|
+
client TEXT NOT NULL DEFAULT '{}', -- JSON
|
|
178
|
+
ip TEXT,
|
|
179
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
180
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
181
|
+
api_key_enc TEXT, -- set on approval, wiped once delivered to the CLI
|
|
182
|
+
last_poll_at TEXT,
|
|
183
|
+
decided_at TEXT,
|
|
184
|
+
decision_note TEXT,
|
|
185
|
+
delivered_at TEXT,
|
|
186
|
+
created_at TEXT NOT NULL,
|
|
187
|
+
expires_at TEXT NOT NULL
|
|
188
|
+
);
|
|
189
|
+
CREATE INDEX pairings_status ON pairings(status, created_at);
|
|
190
|
+
CREATE INDEX pairings_user_code ON pairings(user_code);
|
|
191
|
+
|
|
192
|
+
-- Server-side cursor per agentinbox-listen consumer (MCP ack_events keeps using agent_cursors).
|
|
193
|
+
CREATE TABLE listener_cursors (
|
|
194
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
195
|
+
consumer TEXT NOT NULL,
|
|
196
|
+
acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
197
|
+
client TEXT NOT NULL DEFAULT '{}',
|
|
198
|
+
connected INTEGER NOT NULL DEFAULT 0,
|
|
199
|
+
last_seen_at TEXT,
|
|
200
|
+
updated_at TEXT NOT NULL,
|
|
201
|
+
PRIMARY KEY (agent_id, consumer)
|
|
202
|
+
);
|
|
203
|
+
`,
|
|
204
|
+
`
|
|
205
|
+
-- When Google said the refresh token expires (refresh_token_expires_in); NULL = long-lived.
|
|
206
|
+
ALTER TABLE accounts ADD COLUMN refresh_expires_at TEXT;
|
|
207
|
+
-- Why it expires: 'testing' (initial lifetime ~7 days: External app in Testing) or 'time_based'
|
|
208
|
+
-- (any other lifetime, e.g. time-based access the user chose on the consent screen). NULL = unknown.
|
|
209
|
+
ALTER TABLE accounts ADD COLUMN refresh_expiry_kind TEXT;
|
|
210
|
+
-- id_token "hd" claim: Workspace domain, '' = personal (non-Workspace) Google account, NULL = not known (connected before this migration).
|
|
211
|
+
ALTER TABLE accounts ADD COLUMN workspace_domain TEXT;
|
|
212
|
+
`,
|
|
213
|
+
`
|
|
214
|
+
-- Seq the consumer's cursor started at when it was created (the head, or 0 for start=beginning):
|
|
215
|
+
-- mail at or below it predates the listener (what \`listen --backfill\` replays). NULL = created
|
|
216
|
+
-- before this migration, or by an ack (unknown).
|
|
217
|
+
ALTER TABLE listener_cursors ADD COLUMN start_seq INTEGER;
|
|
218
|
+
`,
|
|
219
|
+
`
|
|
220
|
+
-- Last authenticated request to /mcp; throttled like last_used_at (first write immediate).
|
|
221
|
+
ALTER TABLE agents ADD COLUMN last_mcp_at TEXT;
|
|
222
|
+
-- 'paired' (approved from \`agentinbox connect\`: the key lives in a profile on that machine) or
|
|
223
|
+
-- 'key' (created in the console, key copied by hand).
|
|
224
|
+
ALTER TABLE agents ADD COLUMN origin TEXT NOT NULL DEFAULT 'key';
|
|
225
|
+
-- Backfill: a kept approved pairing, or a listener (\`listen\` runs on a pairing's profile), means paired.
|
|
226
|
+
UPDATE agents SET origin = 'paired'
|
|
227
|
+
WHERE id IN (SELECT agent_id FROM pairings WHERE status = 'approved' AND agent_id IS NOT NULL)
|
|
228
|
+
OR id IN (SELECT agent_id FROM listener_cursors);
|
|
229
|
+
`,
|
|
230
|
+
`
|
|
231
|
+
-- SetupState.verified now needs a successful tool call in the audit log (pruned after the retention
|
|
232
|
+
-- period) or a listener ack, and is remembered in setup.verifiedAt once seen. Installs where an agent
|
|
233
|
+
-- already reached /mcp counted as verified before: remember that, so the setup guide doesn't come back.
|
|
234
|
+
INSERT OR IGNORE INTO settings (key, value)
|
|
235
|
+
SELECT 'setup.verifiedAt', json_quote(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
236
|
+
WHERE EXISTS (SELECT 1 FROM agents WHERE last_mcp_at IS NOT NULL);
|
|
237
|
+
`,
|
|
238
|
+
`
|
|
239
|
+
-- Grants: calendar.read / calendar.write live in grants.capabilities; existing grants have neither (mail-only).
|
|
240
|
+
ALTER TABLE grants ADD COLUMN calendar_scope TEXT; -- JSON string[] of calendar ids | NULL (= every calendar)
|
|
241
|
+
ALTER TABLE grants ADD COLUMN calendar_write_mode TEXT NOT NULL DEFAULT 'approval'; -- 'direct' | 'approval'
|
|
242
|
+
|
|
243
|
+
-- Accounts: granted scopes are already in accounts.scopes (JSON, since migration 1).
|
|
244
|
+
-- NULL = derive from scopes (missing calendar scopes → not_granted, else active); 'api_disabled' | 'error'
|
|
245
|
+
-- when a probe or sync said so (cleared by the next success).
|
|
246
|
+
ALTER TABLE accounts ADD COLUMN calendar_status TEXT;
|
|
247
|
+
ALTER TABLE accounts ADD COLUMN calendar_error TEXT;
|
|
248
|
+
ALTER TABLE accounts ADD COLUMN calendar_synced_at TEXT;
|
|
249
|
+
|
|
250
|
+
-- Events: calendar.* events carry a CalendarEventSummary instead of a message.
|
|
251
|
+
ALTER TABLE events ADD COLUMN calendar_id TEXT; -- for calendar scopes and filters
|
|
252
|
+
ALTER TABLE events ADD COLUMN calendar_event TEXT; -- JSON CalendarEventSummary | NULL
|
|
253
|
+
|
|
254
|
+
-- Incremental sync cursor (and optional push channel) per synced calendar.
|
|
255
|
+
CREATE TABLE calendar_sync (
|
|
256
|
+
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
257
|
+
calendar_id TEXT NOT NULL,
|
|
258
|
+
name TEXT,
|
|
259
|
+
access_role TEXT,
|
|
260
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
261
|
+
sync_token TEXT, -- events.list nextSyncToken; NULL = needs a (baseline) full sync
|
|
262
|
+
baseline_at TEXT, -- when the last full sync finished: events created before it aren't "new"
|
|
263
|
+
last_sync_at TEXT,
|
|
264
|
+
last_error TEXT,
|
|
265
|
+
channel_id TEXT, -- events.watch channel (push), NULL when polling only
|
|
266
|
+
channel_resource_id TEXT,
|
|
267
|
+
channel_token_hash TEXT, -- sha256 of the X-Goog-Channel-Token we chose
|
|
268
|
+
channel_expires_at TEXT,
|
|
269
|
+
channel_verified_at TEXT, -- Google's "sync" notification reached us (the address is reachable)
|
|
270
|
+
updated_at TEXT NOT NULL,
|
|
271
|
+
PRIMARY KEY (account_id, calendar_id)
|
|
272
|
+
);
|
|
273
|
+
CREATE UNIQUE INDEX calendar_sync_channel ON calendar_sync(channel_id) WHERE channel_id IS NOT NULL;
|
|
274
|
+
|
|
275
|
+
-- Last seen version of events on synced calendars, to tell created / updated (what changed) / cancelled
|
|
276
|
+
-- apart and to replay a 410 full resync as changes. Upcoming and recurring events only (pruned after end).
|
|
277
|
+
CREATE TABLE calendar_event_state (
|
|
278
|
+
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
279
|
+
calendar_id TEXT NOT NULL,
|
|
280
|
+
event_id TEXT NOT NULL,
|
|
281
|
+
fingerprint TEXT NOT NULL, -- digest of the material fields (CalendarEventChange)
|
|
282
|
+
summary TEXT NOT NULL, -- JSON CalendarEventSummary as last seen
|
|
283
|
+
description_hash TEXT,
|
|
284
|
+
ends_at TEXT, -- NULL for recurring series (kept while the series lives)
|
|
285
|
+
updated_at TEXT NOT NULL,
|
|
286
|
+
PRIMARY KEY (account_id, calendar_id, event_id)
|
|
287
|
+
);
|
|
288
|
+
CREATE INDEX calendar_event_state_ends ON calendar_event_state(ends_at);
|
|
289
|
+
|
|
290
|
+
-- Calendar approvals reuse the approvals table: kind 'calendar.*', draft_id '' (no Gmail draft),
|
|
291
|
+
-- payload JSON {calendar: CalendarApprovalDetail, reason, …}, sent_message_id = the Google event id once applied.
|
|
292
|
+
|
|
293
|
+
-- Existing installs (an account was connected before this upgrade) add Calendar explicitly with the
|
|
294
|
+
-- console's "Add Google Calendar" flow; fresh installs get it by default (no row = enabled).
|
|
295
|
+
INSERT OR IGNORE INTO settings (key, value)
|
|
296
|
+
SELECT 'calendar.setup', '{"enabled":false}'
|
|
297
|
+
WHERE EXISTS (SELECT 1 FROM accounts);
|
|
298
|
+
`,
|
|
299
|
+
`
|
|
300
|
+
-- One upstream with its credential. Gmail/Calendar stay in accounts (M1 doesn't move them).
|
|
301
|
+
CREATE TABLE connections (
|
|
302
|
+
id TEXT PRIMARY KEY, -- con_…
|
|
303
|
+
owner_id TEXT, -- NULL = the install's single owner (real multi-user is M4)
|
|
304
|
+
-- remote_http in M1. native_whoop (M2) and stdio (M4) are reserved, and native_google too (Gmail/Calendar
|
|
305
|
+
-- as a connection kind, docs "Code impact"): CHECKs can't be widened without rebuilding a table that
|
|
306
|
+
-- other tables reference.
|
|
307
|
+
kind TEXT NOT NULL CHECK (kind IN ('remote_http', 'native_whoop', 'stdio', 'native_google')),
|
|
308
|
+
-- The preset id (notion, whoop, …) or NULL for a custom server. Deliberately no CHECK: presets are validated
|
|
309
|
+
-- in code against the registry, so adding one never needs a rebuild of this table.
|
|
310
|
+
preset TEXT,
|
|
311
|
+
-- Operator-chosen; tools are exposed as <slug>__<tool>. No "_" in a slug, so the split is unambiguous.
|
|
312
|
+
slug TEXT NOT NULL UNIQUE CHECK (length(slug) BETWEEN 1 AND 24 AND slug NOT GLOB '*[^a-z0-9-]*'),
|
|
313
|
+
name TEXT NOT NULL,
|
|
314
|
+
url TEXT, -- the MCP endpoint; required for remote_http
|
|
315
|
+
auth_type TEXT NOT NULL DEFAULT 'none' CHECK (auth_type IN ('none', 'header', 'oauth')),
|
|
316
|
+
-- What agents may do by default (Wynn's model): full | ask (risky tools wait for approval) | read.
|
|
317
|
+
level_default TEXT NOT NULL DEFAULT 'ask' CHECK (level_default IN ('full', 'ask', 'read')),
|
|
318
|
+
-- Admin-only private-target allow: the exact scheme://host:port the admin entered, or NULL (off).
|
|
319
|
+
-- Editing the URL clears it.
|
|
320
|
+
-- (No path, query, userinfo, blanks, backslash, percent-encoding, brackets or control characters; ends in
|
|
321
|
+
-- the port.) The code canonicalises it, and the guard itself validates host and ranges.
|
|
322
|
+
private_target TEXT CHECK (private_target IS NULL OR (
|
|
323
|
+
(private_target GLOB 'http://?*:[0-9]*' OR private_target GLOB 'https://?*:[0-9]*')
|
|
324
|
+
AND private_target NOT GLOB '*://*[/?#@ ]*' AND private_target NOT GLOB '*[^0-9]'
|
|
325
|
+
AND private_target NOT GLOB '*[\\%[]*'
|
|
326
|
+
AND private_target NOT GLOB '*[' || char(1) || '-' || char(31) || char(127) || ']*'
|
|
327
|
+
)),
|
|
328
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'needs_reauth', 'error', 'disabled')),
|
|
329
|
+
status_reason TEXT,
|
|
330
|
+
-- When the credential stops working without a human (no refresh grant, or a hard cap such as Notion's
|
|
331
|
+
-- 180 days): drives the "expires in N days" warnings. NULL = not known to expire.
|
|
332
|
+
expires_at TEXT,
|
|
333
|
+
-- Negotiated protocol era and transport, cached (see the SDK ADR), and when it was last negotiated: the
|
|
334
|
+
-- cache expires from protocol_checked_at (the expiry is the code's).
|
|
335
|
+
protocol TEXT,
|
|
336
|
+
protocol_checked_at TEXT,
|
|
337
|
+
tools_cached_at TEXT, -- last successful tools/list
|
|
338
|
+
instructions_hash TEXT, -- sha256 of the server's instructions, pinned separately from tools
|
|
339
|
+
events_cursor TEXT, -- native presets: where the event poll resumes (e.g. WHOOP without a webhook URL)
|
|
340
|
+
created_at TEXT NOT NULL,
|
|
341
|
+
updated_at TEXT NOT NULL,
|
|
342
|
+
CHECK (kind <> 'remote_http' OR url IS NOT NULL)
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
-- The connection's secrets, one row per connection. *_enc columns are AES-GCM ciphertext (server/crypto.ts)
|
|
346
|
+
-- and are never logged or sent to the console or agents.
|
|
347
|
+
CREATE TABLE connection_credentials (
|
|
348
|
+
connection_id TEXT PRIMARY KEY REFERENCES connections(id) ON DELETE CASCADE,
|
|
349
|
+
-- auth_type 'header': the static header injected on every request (e.g. Authorization: Bearer …).
|
|
350
|
+
header_name_enc TEXT,
|
|
351
|
+
header_value_enc TEXT,
|
|
352
|
+
-- auth_type 'oauth'. The issuer is the authorization server's exact issuer (checked on every flow). The
|
|
353
|
+
-- client is the registration (pre-registered, metadata document or DCR): JSON {client_id, client_secret?, …}.
|
|
354
|
+
oauth_issuer_enc TEXT,
|
|
355
|
+
oauth_client_enc TEXT,
|
|
356
|
+
access_token_enc TEXT,
|
|
357
|
+
refresh_token_enc TEXT, -- rotated with a compare-and-swap, saved before it's used
|
|
358
|
+
expires_at TEXT, -- access token expiry
|
|
359
|
+
scope TEXT, -- granted scopes, space-separated (the union after insufficient_scope)
|
|
360
|
+
-- An authorization in flight (cleared when it completes): the PKCE verifier and the discovery result
|
|
361
|
+
-- (JSON: resource metadata, authorization server metadata, resource, redirect URI).
|
|
362
|
+
pkce_verifier_enc TEXT,
|
|
363
|
+
discovery_enc TEXT,
|
|
364
|
+
created_at TEXT NOT NULL,
|
|
365
|
+
updated_at TEXT NOT NULL
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
-- Each upstream tool as pinned. Agents only ever see the pinned definition. A changed hash or a tool that
|
|
369
|
+
-- wasn't there at connect is held until the owner accepts the candidate.
|
|
370
|
+
CREATE TABLE upstream_tools (
|
|
371
|
+
connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
|
|
372
|
+
name TEXT NOT NULL, -- the upstream's own name
|
|
373
|
+
-- <slug>__<name> as listed to agents: [A-Za-z0-9_-], at most 64 (clients prepend mcp__<server>__).
|
|
374
|
+
exposed_name TEXT NOT NULL UNIQUE CHECK (length(exposed_name) BETWEEN 1 AND 64 AND exposed_name NOT GLOB '*[^A-Za-z0-9_-]*'),
|
|
375
|
+
-- JSON {title, description, inputSchema, outputSchema, annotations}; NULL only for a held new tool.
|
|
376
|
+
definition TEXT,
|
|
377
|
+
pin_hash TEXT, -- sha256(JCS({name, title, description, inputSchema, outputSchema, annotations}))
|
|
378
|
+
-- Risk classes (risky = irreversible; read = listed at the read-only level) and where they came from.
|
|
379
|
+
risky INTEGER NOT NULL DEFAULT 0 CHECK (risky IN (0, 1)),
|
|
380
|
+
read INTEGER NOT NULL DEFAULT 0 CHECK (read IN (0, 1)),
|
|
381
|
+
class_source TEXT NOT NULL CHECK (class_source IN ('preset', 'annotation', 'heuristic')),
|
|
382
|
+
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'held')),
|
|
383
|
+
held_reason TEXT CHECK (held_reason IS NULL OR held_reason IN ('changed', 'new')),
|
|
384
|
+
candidate_definition TEXT, -- JSON, same shape as definition: what accepting would pin
|
|
385
|
+
candidate_hash TEXT,
|
|
386
|
+
first_seen_at TEXT NOT NULL,
|
|
387
|
+
updated_at TEXT NOT NULL,
|
|
388
|
+
PRIMARY KEY (connection_id, name),
|
|
389
|
+
CHECK (risky = 0 OR read = 0),
|
|
390
|
+
CHECK ((definition IS NULL) = (pin_hash IS NULL)),
|
|
391
|
+
CHECK ((candidate_definition IS NULL) = (candidate_hash IS NULL)),
|
|
392
|
+
-- IS, not =: a NULL comparison would make the CHECK pass.
|
|
393
|
+
CHECK (
|
|
394
|
+
(state = 'active' AND held_reason IS NULL AND candidate_hash IS NULL AND pin_hash IS NOT NULL)
|
|
395
|
+
OR (state = 'held' AND held_reason IS 'changed' AND candidate_hash IS NOT NULL AND pin_hash IS NOT NULL)
|
|
396
|
+
OR (state = 'held' AND held_reason IS 'new' AND candidate_hash IS NOT NULL AND pin_hash IS NULL)
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
-- Per agent × connection access. No row = no access. There are no per-tool grant modes.
|
|
401
|
+
CREATE TABLE connection_grants (
|
|
402
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
403
|
+
connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
|
|
404
|
+
level TEXT NOT NULL CHECK (level IN ('full', 'ask', 'read')),
|
|
405
|
+
created_at TEXT NOT NULL,
|
|
406
|
+
updated_at TEXT NOT NULL,
|
|
407
|
+
PRIMARY KEY (agent_id, connection_id)
|
|
408
|
+
);
|
|
409
|
+
CREATE INDEX connection_grants_connection ON connection_grants(connection_id);
|
|
410
|
+
|
|
411
|
+
-- "Always allow this": a matching call by this agent never asks again. Optionally only for one target:
|
|
412
|
+
-- an argument (JSON Pointer into the arguments) and its canonical JSON value.
|
|
413
|
+
CREATE TABLE approval_rules (
|
|
414
|
+
id TEXT PRIMARY KEY, -- rul_…
|
|
415
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
416
|
+
connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
|
|
417
|
+
tool TEXT NOT NULL, -- upstream tool name (upstream_tools.name), stable across slug edits
|
|
418
|
+
target_path TEXT,
|
|
419
|
+
target_value TEXT,
|
|
420
|
+
created_at TEXT NOT NULL,
|
|
421
|
+
CHECK ((target_path IS NULL) = (target_value IS NULL))
|
|
422
|
+
);
|
|
423
|
+
CREATE UNIQUE INDEX approval_rules_match ON approval_rules(agent_id, connection_id, tool, COALESCE(target_path, ''), COALESCE(target_value, ''));
|
|
424
|
+
CREATE INDEX approval_rules_connection ON approval_rules(connection_id);
|
|
425
|
+
|
|
426
|
+
-- Approvals, rebuilt: SQLite can't drop NOT NULL. account_id and draft_id become nullable (an mcp.call
|
|
427
|
+
-- has neither), connection_id and tool_call are added. Same ids, rowids (list order), indexes and
|
|
428
|
+
-- cascades; nothing references approvals, so dropping it cascades nowhere.
|
|
429
|
+
-- tool_call (mcp.call only): JSON {tool, exposedName, args (the exact canonical bytes, as a string),
|
|
430
|
+
-- argsSha256, pinHash, result (once executed)}.
|
|
431
|
+
CREATE TABLE approvals_new (
|
|
432
|
+
id TEXT PRIMARY KEY,
|
|
433
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
434
|
+
account_id TEXT REFERENCES accounts(id) ON DELETE CASCADE,
|
|
435
|
+
connection_id TEXT REFERENCES connections(id) ON DELETE CASCADE,
|
|
436
|
+
kind TEXT NOT NULL,
|
|
437
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
438
|
+
draft_id TEXT,
|
|
439
|
+
payload TEXT NOT NULL, -- JSON {to,cc,bcc,subject,body,threadId,attachments,reason,…}
|
|
440
|
+
tool_call TEXT,
|
|
441
|
+
decided_at TEXT,
|
|
442
|
+
decision_note TEXT,
|
|
443
|
+
sent_message_id TEXT,
|
|
444
|
+
error TEXT,
|
|
445
|
+
created_at TEXT NOT NULL,
|
|
446
|
+
expires_at TEXT NOT NULL,
|
|
447
|
+
-- Mail and calendar kinds keep exactly the invariant they had (an account and a draft id, '' for
|
|
448
|
+
-- calendar); only mcp.call goes through a connection.
|
|
449
|
+
CHECK (
|
|
450
|
+
(kind = 'mcp.call' AND account_id IS NULL AND draft_id IS NULL AND connection_id IS NOT NULL AND tool_call IS NOT NULL)
|
|
451
|
+
OR (kind <> 'mcp.call' AND account_id IS NOT NULL AND draft_id IS NOT NULL AND connection_id IS NULL AND tool_call IS NULL)
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
INSERT INTO approvals_new (rowid, id, agent_id, account_id, kind, status, draft_id, payload, decided_at, decision_note, sent_message_id, error, created_at, expires_at)
|
|
455
|
+
SELECT rowid, id, agent_id, account_id, kind, status, draft_id, payload, decided_at, decision_note, sent_message_id, error, created_at, expires_at
|
|
456
|
+
FROM approvals ORDER BY rowid;
|
|
457
|
+
DROP TABLE approvals;
|
|
458
|
+
ALTER TABLE approvals_new RENAME TO approvals;
|
|
459
|
+
CREATE INDEX approvals_status ON approvals(status, created_at);
|
|
460
|
+
CREATE INDEX approvals_connection ON approvals(connection_id);
|
|
461
|
+
|
|
462
|
+
-- Events from connections (connection.auth_failed, connection.tool_changed, approval.requested, whoop.*) carry
|
|
463
|
+
-- their connection here. account_id stays NOT NULL: connection events store '' there. Additive, no rebuild.
|
|
464
|
+
ALTER TABLE events ADD COLUMN connection_id TEXT;
|
|
465
|
+
CREATE INDEX events_connection_seq ON events(connection_id, seq) WHERE connection_id IS NOT NULL;
|
|
466
|
+
`,
|
|
467
|
+
`
|
|
468
|
+
-- When the stored access token was issued: proactive refresh runs at expires_at - max(60 s, 10 % of the lifetime).
|
|
469
|
+
ALTER TABLE connection_credentials ADD COLUMN issued_at TEXT;
|
|
470
|
+
-- An upstream connection's authorization in flight (/oauth/connections/callback): state -> connection. NULL for
|
|
471
|
+
-- Google's flow. code_verifier is '' for these rows: the verifier is kept encrypted, keyed by state, in
|
|
472
|
+
-- connection_credentials.pkce_verifier_enc.
|
|
473
|
+
ALTER TABLE oauth_states ADD COLUMN connection_id TEXT REFERENCES connections(id) ON DELETE CASCADE;
|
|
474
|
+
`,
|
|
475
|
+
`
|
|
476
|
+
-- 1: every agent gets this connection at level_default, including agents created later (console, pairing).
|
|
477
|
+
-- NULL: only the agents picked (connection_grants). Per-agent levels still live in connection_grants.
|
|
478
|
+
ALTER TABLE connections ADD COLUMN all_agents INTEGER CHECK (all_agents IS NULL OR all_agents = 1);
|
|
479
|
+
`
|
|
480
|
+
];
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region server/db/index.ts
|
|
483
|
+
/** Thin synchronous wrapper over node:sqlite. All repos go through this. */
|
|
484
|
+
var Db = class {
|
|
485
|
+
raw;
|
|
486
|
+
depth = 0;
|
|
487
|
+
constructor(path) {
|
|
488
|
+
this.raw = new DatabaseSync(path);
|
|
489
|
+
this.raw.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
|
|
490
|
+
this.migrate();
|
|
491
|
+
if (path !== ":memory:") for (const file of [
|
|
492
|
+
path,
|
|
493
|
+
`${path}-wal`,
|
|
494
|
+
`${path}-shm`
|
|
495
|
+
]) try {
|
|
496
|
+
chmodSync(file, 384);
|
|
497
|
+
} catch {}
|
|
498
|
+
}
|
|
499
|
+
get(sql, ...params) {
|
|
500
|
+
return this.raw.prepare(sql).get(...params);
|
|
501
|
+
}
|
|
502
|
+
all(sql, ...params) {
|
|
503
|
+
return this.raw.prepare(sql).all(...params);
|
|
504
|
+
}
|
|
505
|
+
run(sql, ...params) {
|
|
506
|
+
const r = this.raw.prepare(sql).run(...params);
|
|
507
|
+
return {
|
|
508
|
+
changes: Number(r.changes),
|
|
509
|
+
lastInsertRowid: Number(r.lastInsertRowid)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Nested calls join the outer transaction (savepoints). */
|
|
513
|
+
tx(fn) {
|
|
514
|
+
const sp = `sp${this.depth++}`;
|
|
515
|
+
this.raw.exec(`SAVEPOINT ${sp}`);
|
|
516
|
+
try {
|
|
517
|
+
const out = fn();
|
|
518
|
+
this.raw.exec(`RELEASE ${sp}`);
|
|
519
|
+
return out;
|
|
520
|
+
} catch (err) {
|
|
521
|
+
this.raw.exec(`ROLLBACK TO ${sp}; RELEASE ${sp}`);
|
|
522
|
+
throw err;
|
|
523
|
+
} finally {
|
|
524
|
+
this.depth--;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
close() {
|
|
528
|
+
this.raw.close();
|
|
529
|
+
}
|
|
530
|
+
migrate() {
|
|
531
|
+
this.raw.exec("CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)");
|
|
532
|
+
const current = this.get("SELECT MAX(version) AS v FROM _migrations")?.v ?? 0;
|
|
533
|
+
MIGRATIONS.forEach((sql, i) => {
|
|
534
|
+
const version = i + 1;
|
|
535
|
+
if (version <= current) return;
|
|
536
|
+
this.tx(() => {
|
|
537
|
+
this.raw.exec(sql);
|
|
538
|
+
this.run("INSERT INTO _migrations (version, applied_at) VALUES (?, ?)", version, (/* @__PURE__ */ new Date()).toISOString());
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const json = {
|
|
544
|
+
parse(v, fallback) {
|
|
545
|
+
if (typeof v !== "string" || v === "") return fallback;
|
|
546
|
+
try {
|
|
547
|
+
return JSON.parse(v);
|
|
548
|
+
} catch {
|
|
549
|
+
return fallback;
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
str(v) {
|
|
553
|
+
return JSON.stringify(v);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const bool = (v) => v === 1 || v === 1n || v === true;
|
|
557
|
+
const now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
558
|
+
//#endregion
|
|
559
|
+
export { now as i, bool as n, json as r, Db as t };
|
|
560
|
+
|
|
561
|
+
//# sourceMappingURL=db-BEUEUPyP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-BEUEUPyP.js","names":[],"sources":["../server/db/schema.ts","../server/db/index.ts"],"sourcesContent":["// Ordered migrations. Append only — never edit a shipped migration.\n// Conventions: ids are prefixed TEXT, timestamps are ISO-8601 TEXT (UTC), JSON is TEXT,\n// booleans are INTEGER 0/1, secrets are AES-GCM ciphertext TEXT (see server/crypto.ts).\n\nexport const MIGRATIONS: string[] = [\n /* 1: initial schema */ `\n CREATE TABLE settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n CREATE TABLE admin_sessions (\n token_hash TEXT PRIMARY KEY,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n ip TEXT,\n user_agent TEXT\n );\n\n CREATE TABLE accounts (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n display_name TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n last_error TEXT,\n refresh_token_enc TEXT NOT NULL,\n access_token_enc TEXT,\n access_expires_at TEXT,\n scopes TEXT NOT NULL DEFAULT '[]',\n history_id TEXT,\n watch_expires_at TEXT,\n last_sync_at TEXT,\n created_at TEXT NOT NULL\n );\n\n -- Pending OAuth flows (state → PKCE verifier). Short-lived.\n CREATE TABLE oauth_states (\n state TEXT PRIMARY KEY,\n code_verifier TEXT NOT NULL,\n redirect_uri TEXT NOT NULL,\n reauth_account_id TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE agents (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT,\n key_prefix TEXT NOT NULL,\n key_hash TEXT NOT NULL UNIQUE,\n disabled INTEGER NOT NULL DEFAULT 0,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE grants (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n capabilities TEXT NOT NULL, -- JSON Capability[]\n label_scope TEXT, -- JSON string[] | NULL (= whole mailbox)\n send_mode TEXT NOT NULL DEFAULT 'draft_only',\n allow_to TEXT NOT NULL DEFAULT '[]', -- JSON string[]\n reply_only INTEGER NOT NULL DEFAULT 0,\n max_sends_per_hour INTEGER,\n max_sends_per_day INTEGER,\n created_at TEXT NOT NULL,\n UNIQUE (agent_id, account_id)\n );\n\n -- Agent event-feed cursor for the MCP pull tools (list_events / wait_for_events / ack_events).\n CREATE TABLE agent_cursors (\n agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,\n acked_seq INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE events (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n id TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL,\n account_id TEXT NOT NULL,\n gmail_message_id TEXT,\n gmail_thread_id TEXT,\n label_ids TEXT NOT NULL DEFAULT '[]', -- JSON: message labels at event time (used for scope + filters)\n message TEXT, -- JSON MessageSummary | NULL\n changed_label_ids TEXT, -- JSON string[] for label.added/removed\n detail TEXT, -- JSON\n dedupe_key TEXT NOT NULL UNIQUE, -- e.g. \"acc_x:msgid:message.received\" — Pub/Sub is at-least-once\n created_at TEXT NOT NULL\n );\n CREATE INDEX events_account_seq ON events(account_id, seq);\n CREATE INDEX events_thread ON events(account_id, gmail_thread_id);\n CREATE INDEX events_created ON events(created_at);\n\n -- Threads the account has sent into; drives thread.reply_received.\n CREATE TABLE sent_threads (\n account_id TEXT NOT NULL,\n gmail_thread_id TEXT NOT NULL,\n last_sent_at TEXT NOT NULL,\n PRIMARY KEY (account_id, gmail_thread_id)\n );\n\n CREATE TABLE webhooks (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n url TEXT NOT NULL,\n secret_enc TEXT NOT NULL,\n secret_prefix TEXT NOT NULL,\n event_types TEXT NOT NULL, -- JSON EventType[]\n filter TEXT NOT NULL DEFAULT '{}', -- JSON WebhookFilter\n include_body INTEGER NOT NULL DEFAULT 0,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE deliveries (\n id TEXT PRIMARY KEY,\n webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,\n event_seq INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER NOT NULL DEFAULT 0,\n next_attempt_at TEXT,\n last_status_code INTEGER,\n last_error TEXT,\n last_response TEXT,\n duration_ms INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE (webhook_id, event_seq)\n );\n CREATE INDEX deliveries_due ON deliveries(status, next_attempt_at);\n CREATE INDEX deliveries_webhook ON deliveries(webhook_id, created_at);\n\n CREATE TABLE approvals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n draft_id TEXT NOT NULL,\n payload TEXT NOT NULL, -- JSON {to,cc,bcc,subject,body,threadId,attachments,reason}\n decided_at TEXT,\n decision_note TEXT,\n sent_message_id TEXT,\n error TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX approvals_status ON approvals(status, created_at);\n\n CREATE TABLE audit_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n actor_type TEXT NOT NULL,\n actor_id TEXT,\n action TEXT NOT NULL,\n account_id TEXT,\n target TEXT,\n outcome TEXT NOT NULL,\n reason TEXT,\n duration_ms INTEGER,\n detail TEXT\n );\n CREATE INDEX audit_ts ON audit_log(ts);\n CREATE INDEX audit_actor ON audit_log(actor_id, ts);\n `,\n\n /* 2: device-flow pairing + listener cursors */ `\n CREATE TABLE pairings (\n id TEXT PRIMARY KEY,\n device_code_hash TEXT NOT NULL UNIQUE, -- sha256 of the CLI's polling secret\n user_code TEXT NOT NULL, -- shown in the terminal and the console, e.g. \"K7QM-2XPD\"\n name TEXT NOT NULL,\n description TEXT,\n requested TEXT NOT NULL DEFAULT '{}', -- JSON\n client TEXT NOT NULL DEFAULT '{}', -- JSON\n ip TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n api_key_enc TEXT, -- set on approval, wiped once delivered to the CLI\n last_poll_at TEXT,\n decided_at TEXT,\n decision_note TEXT,\n delivered_at TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX pairings_status ON pairings(status, created_at);\n CREATE INDEX pairings_user_code ON pairings(user_code);\n\n -- Server-side cursor per agentinbox-listen consumer (MCP ack_events keeps using agent_cursors).\n CREATE TABLE listener_cursors (\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n consumer TEXT NOT NULL,\n acked_seq INTEGER NOT NULL DEFAULT 0,\n client TEXT NOT NULL DEFAULT '{}',\n connected INTEGER NOT NULL DEFAULT 0,\n last_seen_at TEXT,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (agent_id, consumer)\n );\n `,\n /* 3: onboarding diagnostics on accounts */ `\n -- When Google said the refresh token expires (refresh_token_expires_in); NULL = long-lived.\n ALTER TABLE accounts ADD COLUMN refresh_expires_at TEXT;\n -- Why it expires: 'testing' (initial lifetime ~7 days: External app in Testing) or 'time_based'\n -- (any other lifetime, e.g. time-based access the user chose on the consent screen). NULL = unknown.\n ALTER TABLE accounts ADD COLUMN refresh_expiry_kind TEXT;\n -- id_token \"hd\" claim: Workspace domain, '' = personal (non-Workspace) Google account, NULL = not known (connected before this migration).\n ALTER TABLE accounts ADD COLUMN workspace_domain TEXT;\n `,\n /* 4: where each listener consumer started */ `\n -- Seq the consumer's cursor started at when it was created (the head, or 0 for start=beginning):\n -- mail at or below it predates the listener (what \\`listen --backfill\\` replays). NULL = created\n -- before this migration, or by an ack (unknown).\n ALTER TABLE listener_cursors ADD COLUMN start_seq INTEGER;\n `,\n /* 5: MCP use apart from listen; how each agent was added */ `\n -- Last authenticated request to /mcp; throttled like last_used_at (first write immediate).\n ALTER TABLE agents ADD COLUMN last_mcp_at TEXT;\n -- 'paired' (approved from \\`agentinbox connect\\`: the key lives in a profile on that machine) or\n -- 'key' (created in the console, key copied by hand).\n ALTER TABLE agents ADD COLUMN origin TEXT NOT NULL DEFAULT 'key';\n -- Backfill: a kept approved pairing, or a listener (\\`listen\\` runs on a pairing's profile), means paired.\n UPDATE agents SET origin = 'paired'\n WHERE id IN (SELECT agent_id FROM pairings WHERE status = 'approved' AND agent_id IS NOT NULL)\n OR id IN (SELECT agent_id FROM listener_cursors);\n `,\n /* 6: keep existing installs verified */ `\n -- SetupState.verified now needs a successful tool call in the audit log (pruned after the retention\n -- period) or a listener ack, and is remembered in setup.verifiedAt once seen. Installs where an agent\n -- already reached /mcp counted as verified before: remember that, so the setup guide doesn't come back.\n INSERT OR IGNORE INTO settings (key, value)\n SELECT 'setup.verifiedAt', json_quote(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n WHERE EXISTS (SELECT 1 FROM agents WHERE last_mcp_at IS NOT NULL);\n `,\n /* 7: Google Calendar (docs/calendar.md). Additive only: mail keeps working untouched. */ `\n -- Grants: calendar.read / calendar.write live in grants.capabilities; existing grants have neither (mail-only).\n ALTER TABLE grants ADD COLUMN calendar_scope TEXT; -- JSON string[] of calendar ids | NULL (= every calendar)\n ALTER TABLE grants ADD COLUMN calendar_write_mode TEXT NOT NULL DEFAULT 'approval'; -- 'direct' | 'approval'\n\n -- Accounts: granted scopes are already in accounts.scopes (JSON, since migration 1).\n -- NULL = derive from scopes (missing calendar scopes → not_granted, else active); 'api_disabled' | 'error'\n -- when a probe or sync said so (cleared by the next success).\n ALTER TABLE accounts ADD COLUMN calendar_status TEXT;\n ALTER TABLE accounts ADD COLUMN calendar_error TEXT;\n ALTER TABLE accounts ADD COLUMN calendar_synced_at TEXT;\n\n -- Events: calendar.* events carry a CalendarEventSummary instead of a message.\n ALTER TABLE events ADD COLUMN calendar_id TEXT; -- for calendar scopes and filters\n ALTER TABLE events ADD COLUMN calendar_event TEXT; -- JSON CalendarEventSummary | NULL\n\n -- Incremental sync cursor (and optional push channel) per synced calendar.\n CREATE TABLE calendar_sync (\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n calendar_id TEXT NOT NULL,\n name TEXT,\n access_role TEXT,\n is_primary INTEGER NOT NULL DEFAULT 0,\n sync_token TEXT, -- events.list nextSyncToken; NULL = needs a (baseline) full sync\n baseline_at TEXT, -- when the last full sync finished: events created before it aren't \"new\"\n last_sync_at TEXT,\n last_error TEXT,\n channel_id TEXT, -- events.watch channel (push), NULL when polling only\n channel_resource_id TEXT,\n channel_token_hash TEXT, -- sha256 of the X-Goog-Channel-Token we chose\n channel_expires_at TEXT,\n channel_verified_at TEXT, -- Google's \"sync\" notification reached us (the address is reachable)\n updated_at TEXT NOT NULL,\n PRIMARY KEY (account_id, calendar_id)\n );\n CREATE UNIQUE INDEX calendar_sync_channel ON calendar_sync(channel_id) WHERE channel_id IS NOT NULL;\n\n -- Last seen version of events on synced calendars, to tell created / updated (what changed) / cancelled\n -- apart and to replay a 410 full resync as changes. Upcoming and recurring events only (pruned after end).\n CREATE TABLE calendar_event_state (\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n calendar_id TEXT NOT NULL,\n event_id TEXT NOT NULL,\n fingerprint TEXT NOT NULL, -- digest of the material fields (CalendarEventChange)\n summary TEXT NOT NULL, -- JSON CalendarEventSummary as last seen\n description_hash TEXT,\n ends_at TEXT, -- NULL for recurring series (kept while the series lives)\n updated_at TEXT NOT NULL,\n PRIMARY KEY (account_id, calendar_id, event_id)\n );\n CREATE INDEX calendar_event_state_ends ON calendar_event_state(ends_at);\n\n -- Calendar approvals reuse the approvals table: kind 'calendar.*', draft_id '' (no Gmail draft),\n -- payload JSON {calendar: CalendarApprovalDetail, reason, …}, sent_message_id = the Google event id once applied.\n\n -- Existing installs (an account was connected before this upgrade) add Calendar explicitly with the\n -- console's \"Add Google Calendar\" flow; fresh installs get it by default (no row = enabled).\n INSERT OR IGNORE INTO settings (key, value)\n SELECT 'calendar.setup', '{\"enabled\":false}'\n WHERE EXISTS (SELECT 1 FROM accounts);\n `,\n /* 8: MCP broker (docs/MCP-BROKER.md, M1 PR 2). New tables, events.connection_id and the approvals rebuild; nothing reads them yet. */ `\n -- One upstream with its credential. Gmail/Calendar stay in accounts (M1 doesn't move them).\n CREATE TABLE connections (\n id TEXT PRIMARY KEY, -- con_…\n owner_id TEXT, -- NULL = the install's single owner (real multi-user is M4)\n -- remote_http in M1. native_whoop (M2) and stdio (M4) are reserved, and native_google too (Gmail/Calendar\n -- as a connection kind, docs \"Code impact\"): CHECKs can't be widened without rebuilding a table that\n -- other tables reference.\n kind TEXT NOT NULL CHECK (kind IN ('remote_http', 'native_whoop', 'stdio', 'native_google')),\n -- The preset id (notion, whoop, …) or NULL for a custom server. Deliberately no CHECK: presets are validated\n -- in code against the registry, so adding one never needs a rebuild of this table.\n preset TEXT,\n -- Operator-chosen; tools are exposed as <slug>__<tool>. No \"_\" in a slug, so the split is unambiguous.\n slug TEXT NOT NULL UNIQUE CHECK (length(slug) BETWEEN 1 AND 24 AND slug NOT GLOB '*[^a-z0-9-]*'),\n name TEXT NOT NULL,\n url TEXT, -- the MCP endpoint; required for remote_http\n auth_type TEXT NOT NULL DEFAULT 'none' CHECK (auth_type IN ('none', 'header', 'oauth')),\n -- What agents may do by default (Wynn's model): full | ask (risky tools wait for approval) | read.\n level_default TEXT NOT NULL DEFAULT 'ask' CHECK (level_default IN ('full', 'ask', 'read')),\n -- Admin-only private-target allow: the exact scheme://host:port the admin entered, or NULL (off).\n -- Editing the URL clears it.\n -- (No path, query, userinfo, blanks, backslash, percent-encoding, brackets or control characters; ends in\n -- the port.) The code canonicalises it, and the guard itself validates host and ranges.\n private_target TEXT CHECK (private_target IS NULL OR (\n (private_target GLOB 'http://?*:[0-9]*' OR private_target GLOB 'https://?*:[0-9]*')\n AND private_target NOT GLOB '*://*[/?#@ ]*' AND private_target NOT GLOB '*[^0-9]'\n AND private_target NOT GLOB '*[\\\\%[]*'\n AND private_target NOT GLOB '*[' || char(1) || '-' || char(31) || char(127) || ']*'\n )),\n status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'needs_reauth', 'error', 'disabled')),\n status_reason TEXT,\n -- When the credential stops working without a human (no refresh grant, or a hard cap such as Notion's\n -- 180 days): drives the \"expires in N days\" warnings. NULL = not known to expire.\n expires_at TEXT,\n -- Negotiated protocol era and transport, cached (see the SDK ADR), and when it was last negotiated: the\n -- cache expires from protocol_checked_at (the expiry is the code's).\n protocol TEXT,\n protocol_checked_at TEXT,\n tools_cached_at TEXT, -- last successful tools/list\n instructions_hash TEXT, -- sha256 of the server's instructions, pinned separately from tools\n events_cursor TEXT, -- native presets: where the event poll resumes (e.g. WHOOP without a webhook URL)\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n CHECK (kind <> 'remote_http' OR url IS NOT NULL)\n );\n\n -- The connection's secrets, one row per connection. *_enc columns are AES-GCM ciphertext (server/crypto.ts)\n -- and are never logged or sent to the console or agents.\n CREATE TABLE connection_credentials (\n connection_id TEXT PRIMARY KEY REFERENCES connections(id) ON DELETE CASCADE,\n -- auth_type 'header': the static header injected on every request (e.g. Authorization: Bearer …).\n header_name_enc TEXT,\n header_value_enc TEXT,\n -- auth_type 'oauth'. The issuer is the authorization server's exact issuer (checked on every flow). The\n -- client is the registration (pre-registered, metadata document or DCR): JSON {client_id, client_secret?, …}.\n oauth_issuer_enc TEXT,\n oauth_client_enc TEXT,\n access_token_enc TEXT,\n refresh_token_enc TEXT, -- rotated with a compare-and-swap, saved before it's used\n expires_at TEXT, -- access token expiry\n scope TEXT, -- granted scopes, space-separated (the union after insufficient_scope)\n -- An authorization in flight (cleared when it completes): the PKCE verifier and the discovery result\n -- (JSON: resource metadata, authorization server metadata, resource, redirect URI).\n pkce_verifier_enc TEXT,\n discovery_enc TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n -- Each upstream tool as pinned. Agents only ever see the pinned definition. A changed hash or a tool that\n -- wasn't there at connect is held until the owner accepts the candidate.\n CREATE TABLE upstream_tools (\n connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,\n name TEXT NOT NULL, -- the upstream's own name\n -- <slug>__<name> as listed to agents: [A-Za-z0-9_-], at most 64 (clients prepend mcp__<server>__).\n exposed_name TEXT NOT NULL UNIQUE CHECK (length(exposed_name) BETWEEN 1 AND 64 AND exposed_name NOT GLOB '*[^A-Za-z0-9_-]*'),\n -- JSON {title, description, inputSchema, outputSchema, annotations}; NULL only for a held new tool.\n definition TEXT,\n pin_hash TEXT, -- sha256(JCS({name, title, description, inputSchema, outputSchema, annotations}))\n -- Risk classes (risky = irreversible; read = listed at the read-only level) and where they came from.\n risky INTEGER NOT NULL DEFAULT 0 CHECK (risky IN (0, 1)),\n read INTEGER NOT NULL DEFAULT 0 CHECK (read IN (0, 1)),\n class_source TEXT NOT NULL CHECK (class_source IN ('preset', 'annotation', 'heuristic')),\n state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'held')),\n held_reason TEXT CHECK (held_reason IS NULL OR held_reason IN ('changed', 'new')),\n candidate_definition TEXT, -- JSON, same shape as definition: what accepting would pin\n candidate_hash TEXT,\n first_seen_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (connection_id, name),\n CHECK (risky = 0 OR read = 0),\n CHECK ((definition IS NULL) = (pin_hash IS NULL)),\n CHECK ((candidate_definition IS NULL) = (candidate_hash IS NULL)),\n -- IS, not =: a NULL comparison would make the CHECK pass.\n CHECK (\n (state = 'active' AND held_reason IS NULL AND candidate_hash IS NULL AND pin_hash IS NOT NULL)\n OR (state = 'held' AND held_reason IS 'changed' AND candidate_hash IS NOT NULL AND pin_hash IS NOT NULL)\n OR (state = 'held' AND held_reason IS 'new' AND candidate_hash IS NOT NULL AND pin_hash IS NULL)\n )\n );\n\n -- Per agent × connection access. No row = no access. There are no per-tool grant modes.\n CREATE TABLE connection_grants (\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,\n level TEXT NOT NULL CHECK (level IN ('full', 'ask', 'read')),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (agent_id, connection_id)\n );\n CREATE INDEX connection_grants_connection ON connection_grants(connection_id);\n\n -- \"Always allow this\": a matching call by this agent never asks again. Optionally only for one target:\n -- an argument (JSON Pointer into the arguments) and its canonical JSON value.\n CREATE TABLE approval_rules (\n id TEXT PRIMARY KEY, -- rul_…\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,\n tool TEXT NOT NULL, -- upstream tool name (upstream_tools.name), stable across slug edits\n target_path TEXT,\n target_value TEXT,\n created_at TEXT NOT NULL,\n CHECK ((target_path IS NULL) = (target_value IS NULL))\n );\n CREATE UNIQUE INDEX approval_rules_match ON approval_rules(agent_id, connection_id, tool, COALESCE(target_path, ''), COALESCE(target_value, ''));\n CREATE INDEX approval_rules_connection ON approval_rules(connection_id);\n\n -- Approvals, rebuilt: SQLite can't drop NOT NULL. account_id and draft_id become nullable (an mcp.call\n -- has neither), connection_id and tool_call are added. Same ids, rowids (list order), indexes and\n -- cascades; nothing references approvals, so dropping it cascades nowhere.\n -- tool_call (mcp.call only): JSON {tool, exposedName, args (the exact canonical bytes, as a string),\n -- argsSha256, pinHash, result (once executed)}.\n CREATE TABLE approvals_new (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT REFERENCES accounts(id) ON DELETE CASCADE,\n connection_id TEXT REFERENCES connections(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n draft_id TEXT,\n payload TEXT NOT NULL, -- JSON {to,cc,bcc,subject,body,threadId,attachments,reason,…}\n tool_call TEXT,\n decided_at TEXT,\n decision_note TEXT,\n sent_message_id TEXT,\n error TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n -- Mail and calendar kinds keep exactly the invariant they had (an account and a draft id, '' for\n -- calendar); only mcp.call goes through a connection.\n CHECK (\n (kind = 'mcp.call' AND account_id IS NULL AND draft_id IS NULL AND connection_id IS NOT NULL AND tool_call IS NOT NULL)\n OR (kind <> 'mcp.call' AND account_id IS NOT NULL AND draft_id IS NOT NULL AND connection_id IS NULL AND tool_call IS NULL)\n )\n );\n INSERT INTO approvals_new (rowid, id, agent_id, account_id, kind, status, draft_id, payload, decided_at, decision_note, sent_message_id, error, created_at, expires_at)\n SELECT rowid, id, agent_id, account_id, kind, status, draft_id, payload, decided_at, decision_note, sent_message_id, error, created_at, expires_at\n FROM approvals ORDER BY rowid;\n DROP TABLE approvals;\n ALTER TABLE approvals_new RENAME TO approvals;\n CREATE INDEX approvals_status ON approvals(status, created_at);\n CREATE INDEX approvals_connection ON approvals(connection_id);\n\n -- Events from connections (connection.auth_failed, connection.tool_changed, approval.requested, whoop.*) carry\n -- their connection here. account_id stays NOT NULL: connection events store '' there. Additive, no rebuild.\n ALTER TABLE events ADD COLUMN connection_id TEXT;\n CREATE INDEX events_connection_seq ON events(connection_id, seq) WHERE connection_id IS NOT NULL;\n `,\n /* 9: broker upstream OAuth (M1 PRs 3/4). Additive columns only. */ `\n -- When the stored access token was issued: proactive refresh runs at expires_at - max(60 s, 10 % of the lifetime).\n ALTER TABLE connection_credentials ADD COLUMN issued_at TEXT;\n -- An upstream connection's authorization in flight (/oauth/connections/callback): state -> connection. NULL for\n -- Google's flow. code_verifier is '' for these rows: the verifier is kept encrypted, keyed by state, in\n -- connection_credentials.pkce_verifier_enc.\n ALTER TABLE oauth_states ADD COLUMN connection_id TEXT REFERENCES connections(id) ON DELETE CASCADE;\n `,\n /* 10: broker \"All agents\" (new-user walk). Additive. */ `\n -- 1: every agent gets this connection at level_default, including agents created later (console, pairing).\n -- NULL: only the agents picked (connection_grants). Per-agent levels still live in connection_grants.\n ALTER TABLE connections ADD COLUMN all_agents INTEGER CHECK (all_agents IS NULL OR all_agents = 1);\n `,\n];\n","import { chmodSync } from \"node:fs\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { MIGRATIONS } from \"./schema.ts\";\n\nexport type Param = SQLInputValue;\nexport type Row = Record<string, unknown>;\n\n/** Thin synchronous wrapper over node:sqlite. All repos go through this. */\nexport class Db {\n readonly raw: DatabaseSync;\n private depth = 0;\n\n constructor(path: string) {\n this.raw = new DatabaseSync(path);\n this.raw.exec(\"PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;\");\n this.migrate();\n // The database holds (encrypted) tokens and the audit log: owner-only, whatever the umask.\n if (path !== \":memory:\") {\n for (const file of [path, `${path}-wal`, `${path}-shm`]) {\n try {\n chmodSync(file, 0o600);\n } catch {\n // Not created (yet) or not ours to change.\n }\n }\n }\n }\n\n get<T = Row>(sql: string, ...params: Param[]): T | undefined {\n return this.raw.prepare(sql).get(...params) as T | undefined;\n }\n\n all<T = Row>(sql: string, ...params: Param[]): T[] {\n return this.raw.prepare(sql).all(...params) as T[];\n }\n\n run(sql: string, ...params: Param[]): { changes: number; lastInsertRowid: number } {\n const r = this.raw.prepare(sql).run(...params);\n return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };\n }\n\n /** Nested calls join the outer transaction (savepoints). */\n tx<T>(fn: () => T): T {\n const sp = `sp${this.depth++}`;\n this.raw.exec(`SAVEPOINT ${sp}`);\n try {\n const out = fn();\n this.raw.exec(`RELEASE ${sp}`);\n return out;\n } catch (err) {\n this.raw.exec(`ROLLBACK TO ${sp}; RELEASE ${sp}`);\n throw err;\n } finally {\n this.depth--;\n }\n }\n\n close(): void {\n this.raw.close();\n }\n\n private migrate(): void {\n this.raw.exec(\"CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)\");\n const row = this.get<{ v: number | null }>(\"SELECT MAX(version) AS v FROM _migrations\");\n const current = row?.v ?? 0;\n MIGRATIONS.forEach((sql, i) => {\n const version = i + 1;\n if (version <= current) return;\n this.tx(() => {\n this.raw.exec(sql);\n this.run(\"INSERT INTO _migrations (version, applied_at) VALUES (?, ?)\", version, new Date().toISOString());\n });\n });\n }\n}\n\nexport const json = {\n parse<T>(v: unknown, fallback: T): T {\n if (typeof v !== \"string\" || v === \"\") return fallback;\n try {\n return JSON.parse(v) as T;\n } catch {\n return fallback;\n }\n },\n str(v: unknown): string {\n return JSON.stringify(v);\n },\n};\n\nexport const bool = (v: unknown): boolean => v === 1 || v === 1n || v === true;\nexport const now = (): string => new Date().toISOString();\n"],"mappings":";;;;AAIA,MAAa,aAAuB;CACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoKwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCJ;;;;;;;;;CASE;;;;;;CAMe;;;;;;;;;;;CAWpB;;;;;;;;CAQiD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6D6C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwKnE;;;;;;;;CAQX;;;;;AAK3D;;;;ACxdA,IAAa,KAAb,MAAgB;CACd;CACA,QAAgB;CAEhB,YAAY,MAAc;EACxB,KAAK,MAAM,IAAI,aAAa,IAAI;EAChC,KAAK,IAAI,KAAK,kFAAkF;EAChG,KAAK,QAAQ;EAEb,IAAI,SAAS,YACX,KAAK,MAAM,QAAQ;GAAC;GAAM,GAAG,KAAK;GAAO,GAAG,KAAK;EAAK,GACpD,IAAI;GACF,UAAU,MAAM,GAAK;EACvB,QAAQ,CAER;CAGN;CAEA,IAAa,KAAa,GAAG,QAAgC;EAC3D,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;CAC5C;CAEA,IAAa,KAAa,GAAG,QAAsB;EACjD,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;CAC5C;CAEA,IAAI,KAAa,GAAG,QAA+D;EACjF,MAAM,IAAI,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;EAC7C,OAAO;GAAE,SAAS,OAAO,EAAE,OAAO;GAAG,iBAAiB,OAAO,EAAE,eAAe;EAAE;CAClF;;CAGA,GAAM,IAAgB;EACpB,MAAM,KAAK,KAAK,KAAK;EACrB,KAAK,IAAI,KAAK,aAAa,IAAI;EAC/B,IAAI;GACF,MAAM,MAAM,GAAG;GACf,KAAK,IAAI,KAAK,WAAW,IAAI;GAC7B,OAAO;EACT,SAAS,KAAK;GACZ,KAAK,IAAI,KAAK,eAAe,GAAG,YAAY,IAAI;GAChD,MAAM;EACR,UAAU;GACR,KAAK;EACP;CACF;CAEA,QAAc;EACZ,KAAK,IAAI,MAAM;CACjB;CAEA,UAAwB;EACtB,KAAK,IAAI,KAAK,gGAAgG;EAE9G,MAAM,UADM,KAAK,IAA0B,2CACzB,CAAC,EAAE,KAAK;EAC1B,WAAW,SAAS,KAAK,MAAM;GAC7B,MAAM,UAAU,IAAI;GACpB,IAAI,WAAW,SAAS;GACxB,KAAK,SAAS;IACZ,KAAK,IAAI,KAAK,GAAG;IACjB,KAAK,IAAI,+DAA+D,0BAAS,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;GAC3G,CAAC;EACH,CAAC;CACH;AACF;AAEA,MAAa,OAAO;CAClB,MAAS,GAAY,UAAgB;EACnC,IAAI,OAAO,MAAM,YAAY,MAAM,IAAI,OAAO;EAC9C,IAAI;GACF,OAAO,KAAK,MAAM,CAAC;EACrB,QAAQ;GACN,OAAO;EACT;CACF;CACA,IAAI,GAAoB;EACtB,OAAO,KAAK,UAAU,CAAC;CACzB;AACF;AAEA,MAAa,QAAQ,MAAwB,MAAM,KAAK,MAAM,MAAM,MAAM;AAC1E,MAAa,6BAAoB,IAAI,KAAK,EAAA,CAAE,YAAY"}
|