@lobu/cli 6.1.1 → 7.1.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/dist/commands/_lib/apply/apply-cmd.d.ts +36 -0
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +696 -40
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +285 -0
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +469 -28
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +187 -3
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +879 -88
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +72 -3
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +473 -84
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/apply/prompt.d.ts +6 -0
- package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
- package/dist/commands/_lib/apply/prompt.js +16 -0
- package/dist/commands/_lib/apply/prompt.js.map +1 -1
- package/dist/commands/_lib/apply/render.d.ts +9 -0
- package/dist/commands/_lib/apply/render.d.ts.map +1 -1
- package/dist/commands/_lib/apply/render.js +80 -3
- package/dist/commands/_lib/apply/render.js.map +1 -1
- package/dist/commands/_lib/connector-loader.d.ts +3 -0
- package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
- package/dist/commands/_lib/connector-loader.js +129 -0
- package/dist/commands/_lib/connector-loader.js.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.js +351 -0
- package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
- package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
- package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/export/export-cmd.js +329 -0
- package/dist/commands/_lib/export/export-cmd.js.map +1 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +11 -14
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +28 -7
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/connector.d.ts +3 -0
- package/dist/commands/connector.d.ts.map +1 -0
- package/dist/commands/connector.js +5 -0
- package/dist/commands/connector.js.map +1 -0
- package/dist/commands/dev.d.ts +23 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +273 -8
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +28 -18
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +29 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -16
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +15 -144
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/schema.d.ts +28 -1
- package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
- package/dist/commands/memory/_lib/schema.js +120 -4
- package/dist/commands/memory/_lib/schema.js.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.js +41 -18
- package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
- package/dist/commands/org.d.ts +4 -0
- package/dist/commands/org.d.ts.map +1 -1
- package/dist/commands/org.js +10 -0
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/token.d.ts +9 -0
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +54 -3
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +4 -13
- package/dist/commands/validate.js.map +1 -1
- package/dist/config/loader.js +2 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/connectors/README.md +2 -3
- package/dist/connectors/apple_health.ts +138 -0
- package/dist/connectors/apple_photos.ts +178 -0
- package/dist/connectors/apple_screen_time.ts +82 -0
- package/dist/connectors/browser/evaluate.ts +120 -0
- package/dist/connectors/browser/fill_form.ts +107 -0
- package/dist/connectors/browser/page_text.ts +108 -0
- package/dist/connectors/browser-scraper-utils.ts +111 -3
- package/dist/connectors/capterra.ts +5 -1
- package/dist/connectors/chrome_tabs.ts +74 -0
- package/dist/connectors/g2.ts +5 -1
- package/dist/connectors/github.ts +16 -38
- package/dist/connectors/glassdoor.ts +5 -1
- package/dist/connectors/google_calendar.ts +28 -6
- package/dist/connectors/google_gmail.ts +6 -3
- package/dist/connectors/google_play.ts +32 -5
- package/dist/connectors/hackernews.ts +37 -2
- package/dist/connectors/index.ts +14 -1
- package/dist/connectors/linkedin.ts +32 -9
- package/dist/connectors/local_directory.ts +91 -0
- package/dist/connectors/reddit.ts +1 -0
- package/dist/connectors/revolut.ts +569 -0
- package/dist/connectors/rss.ts +33 -8
- package/dist/connectors/trustpilot.ts +36 -21
- package/dist/connectors/website.ts +8 -69
- package/dist/connectors/whatsapp.ts +21 -22
- package/dist/connectors/whatsapp_local.ts +125 -0
- package/dist/connectors/x.ts +17 -7
- package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
- package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
- package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
- package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
- package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
- package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
- package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
- package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
- package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
- package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
- package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
- package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
- package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
- package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
- package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
- package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
- package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
- package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
- package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
- package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
- package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
- package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
- package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
- package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
- package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
- package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
- package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
- package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
- package/dist/eval/client.d.ts.map +1 -1
- package/dist/eval/client.js +11 -0
- package/dist/eval/client.js.map +1 -1
- package/dist/eval/grader.js +2 -1
- package/dist/eval/grader.js.map +1 -1
- package/dist/eval/types.d.ts +2 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -114
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +9 -0
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/internal/context.js +41 -6
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts +5 -0
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +75 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/gateway-url.d.ts +14 -0
- package/dist/internal/gateway-url.d.ts.map +1 -1
- package/dist/internal/gateway-url.js +19 -0
- package/dist/internal/gateway-url.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/local-env.d.ts.map +1 -1
- package/dist/internal/local-env.js +9 -2
- package/dist/internal/local-env.js.map +1 -1
- package/dist/server.bundle.mjs +42251 -36931
- package/dist/start-local.bundle.mjs +16437 -9882
- package/dist/templates/TESTING.md.tmpl +9 -9
- package/package.json +8 -6
- package/dist/connectors/google_photos.ts +0 -776
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Add a stable `slug` identity to `connections` so `lobu apply` can diff
|
|
4
|
+
-- connections by an immutable key instead of the mutable `display_name`.
|
|
5
|
+
--
|
|
6
|
+
-- Mirrors the existing `auth_profiles.slug` design: text slug, unique per org
|
|
7
|
+
-- among live rows (partial index on `deleted_at IS NULL`), generated from the
|
|
8
|
+
-- display name when not supplied explicitly.
|
|
9
|
+
--
|
|
10
|
+
-- Backfill MUST produce exactly what `ensureUniqueConnectionSlug` /
|
|
11
|
+
-- `slugifyConnectionName` in packages/server/src/utils/connections.ts would
|
|
12
|
+
-- generate (that file is the source of truth):
|
|
13
|
+
-- 1. base = slugify(display_name); if empty, slugify(connector_key); if still
|
|
14
|
+
-- empty, the literal 'connection'. slugify = lowercase, every run of
|
|
15
|
+
-- non-alphanumerics -> '-', trim leading/trailing '-'.
|
|
16
|
+
-- 2. Collisions per (organization_id) among live (`deleted_at IS NULL`) rows
|
|
17
|
+
-- are resolved with a deterministic numeric suffix loop: base, base-2,
|
|
18
|
+
-- base-3, ... assigned in ascending id order. Soft-deleted rows do not
|
|
19
|
+
-- participate in the unique index, so they keep their base slug freely.
|
|
20
|
+
|
|
21
|
+
ALTER TABLE public.connections
|
|
22
|
+
ADD COLUMN IF NOT EXISTS slug text;
|
|
23
|
+
|
|
24
|
+
-- slugify(display_name) -> slugify(connector_key) -> 'connection'
|
|
25
|
+
WITH base AS (
|
|
26
|
+
SELECT
|
|
27
|
+
c.id,
|
|
28
|
+
coalesce(
|
|
29
|
+
NULLIF(
|
|
30
|
+
regexp_replace(
|
|
31
|
+
regexp_replace(lower(coalesce(c.display_name, '')), '[^a-z0-9]+', '-', 'g'),
|
|
32
|
+
'(^-+|-+$)', '', 'g'
|
|
33
|
+
),
|
|
34
|
+
''
|
|
35
|
+
),
|
|
36
|
+
NULLIF(
|
|
37
|
+
regexp_replace(
|
|
38
|
+
regexp_replace(lower(coalesce(c.connector_key, '')), '[^a-z0-9]+', '-', 'g'),
|
|
39
|
+
'(^-+|-+$)', '', 'g'
|
|
40
|
+
),
|
|
41
|
+
''
|
|
42
|
+
),
|
|
43
|
+
'connection'
|
|
44
|
+
) AS base_slug
|
|
45
|
+
FROM public.connections c
|
|
46
|
+
)
|
|
47
|
+
UPDATE public.connections c
|
|
48
|
+
SET slug = b.base_slug
|
|
49
|
+
FROM base b
|
|
50
|
+
WHERE b.id = c.id
|
|
51
|
+
AND c.slug IS NULL;
|
|
52
|
+
|
|
53
|
+
-- Resolve collisions to base / base-2 / base-3 / ... in ascending id order.
|
|
54
|
+
-- Loops until no live (deleted_at IS NULL) duplicates remain — a re-assigned
|
|
55
|
+
-- `base-N` could itself collide with another row whose base slug is already
|
|
56
|
+
-- `base-N`, so a single pass is not enough.
|
|
57
|
+
--
|
|
58
|
+
-- This produces a deterministic, collision-free assignment with the same
|
|
59
|
+
-- semantics as the runtime (slugified connector_key fallback, numeric `-N`
|
|
60
|
+
-- suffixing). It is NOT guaranteed to be byte-identical to what
|
|
61
|
+
-- `ensureUniqueConnectionSlug` would pick for pathological mixed-name sets
|
|
62
|
+
-- (the runtime resolves in row-creation order against live DB state, which
|
|
63
|
+
-- pure SQL can't replay) — `packages/server/src/utils/connections.ts` is the
|
|
64
|
+
-- source of truth for new rows.
|
|
65
|
+
DO $$
|
|
66
|
+
DECLARE
|
|
67
|
+
v_changed integer;
|
|
68
|
+
BEGIN
|
|
69
|
+
LOOP
|
|
70
|
+
WITH ranked AS (
|
|
71
|
+
SELECT
|
|
72
|
+
id,
|
|
73
|
+
organization_id,
|
|
74
|
+
slug,
|
|
75
|
+
-- strip any suffix we may have appended on a prior pass so the
|
|
76
|
+
-- base groups stay stable across iterations
|
|
77
|
+
regexp_replace(slug, '-[0-9]+$', '') AS base_slug,
|
|
78
|
+
row_number() OVER (
|
|
79
|
+
PARTITION BY organization_id, regexp_replace(slug, '-[0-9]+$', '')
|
|
80
|
+
ORDER BY id
|
|
81
|
+
) AS rn
|
|
82
|
+
FROM public.connections
|
|
83
|
+
WHERE deleted_at IS NULL
|
|
84
|
+
),
|
|
85
|
+
target AS (
|
|
86
|
+
SELECT
|
|
87
|
+
id,
|
|
88
|
+
CASE WHEN rn = 1 THEN base_slug ELSE base_slug || '-' || rn::text END AS desired_slug
|
|
89
|
+
FROM ranked
|
|
90
|
+
)
|
|
91
|
+
UPDATE public.connections c
|
|
92
|
+
SET slug = t.desired_slug
|
|
93
|
+
FROM target t
|
|
94
|
+
WHERE t.id = c.id
|
|
95
|
+
AND c.slug IS DISTINCT FROM t.desired_slug;
|
|
96
|
+
|
|
97
|
+
GET DIAGNOSTICS v_changed = ROW_COUNT;
|
|
98
|
+
EXIT WHEN v_changed = 0;
|
|
99
|
+
END LOOP;
|
|
100
|
+
END $$;
|
|
101
|
+
|
|
102
|
+
-- Guard: there must be no live-slug duplicate per org before the unique index.
|
|
103
|
+
DO $$
|
|
104
|
+
DECLARE
|
|
105
|
+
v_dups integer;
|
|
106
|
+
BEGIN
|
|
107
|
+
SELECT count(*) INTO v_dups
|
|
108
|
+
FROM (
|
|
109
|
+
SELECT organization_id, slug
|
|
110
|
+
FROM public.connections
|
|
111
|
+
WHERE deleted_at IS NULL
|
|
112
|
+
GROUP BY organization_id, slug
|
|
113
|
+
HAVING count(*) > 1
|
|
114
|
+
) d;
|
|
115
|
+
IF v_dups > 0 THEN
|
|
116
|
+
RAISE EXCEPTION 'connections.slug backfill left % duplicate (organization_id, slug) group(s) among live rows', v_dups;
|
|
117
|
+
END IF;
|
|
118
|
+
END $$;
|
|
119
|
+
|
|
120
|
+
ALTER TABLE public.connections
|
|
121
|
+
ALTER COLUMN slug SET NOT NULL;
|
|
122
|
+
|
|
123
|
+
CREATE UNIQUE INDEX IF NOT EXISTS connections_org_slug_unique
|
|
124
|
+
ON public.connections (organization_id, slug)
|
|
125
|
+
WHERE deleted_at IS NULL;
|
|
126
|
+
|
|
127
|
+
-- migrate:down
|
|
128
|
+
|
|
129
|
+
DROP INDEX IF EXISTS public.connections_org_slug_unique;
|
|
130
|
+
ALTER TABLE public.connections
|
|
131
|
+
DROP COLUMN IF EXISTS slug;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Maps a chat-platform user (Slack `U…`, …) to a Lobu account. Recorded as a
|
|
4
|
+
-- side effect of `/lobu link <code>` — the code is minted by an authenticated
|
|
5
|
+
-- `lobu run`, so `oauth_states.payload.createdBy` is the Lobu user. Once a user
|
|
6
|
+
-- is linked here, they can re-bind any chat to an agent they can manage via
|
|
7
|
+
-- `/lobu link <agentId>` without minting a fresh code.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS public.chat_user_identities (
|
|
10
|
+
platform text NOT NULL,
|
|
11
|
+
team_id text NOT NULL DEFAULT '', -- workspace id; '' for platforms without one
|
|
12
|
+
platform_user_id text NOT NULL,
|
|
13
|
+
lobu_user_id text NOT NULL REFERENCES public."user"(id) ON DELETE CASCADE,
|
|
14
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
15
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
16
|
+
PRIMARY KEY (platform, team_id, platform_user_id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS chat_user_identities_lobu_user_idx
|
|
20
|
+
ON public.chat_user_identities (lobu_user_id);
|
|
21
|
+
|
|
22
|
+
-- migrate:down
|
|
23
|
+
|
|
24
|
+
DROP TABLE IF EXISTS public.chat_user_identities;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Let an auth_profile of kind 'browser_session' live on a specific device worker
|
|
4
|
+
-- instead of holding cookies in auth_data. When device_worker_id is set, cookies
|
|
5
|
+
-- live on disk inside the Mac app's managed --user-data-dir at user_data_dir;
|
|
6
|
+
-- the server never sees them. Cloud/fleet path (device_worker_id NULL,
|
|
7
|
+
-- auth_data populated) is unchanged.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE public.auth_profiles
|
|
10
|
+
ADD COLUMN IF NOT EXISTS device_worker_id uuid,
|
|
11
|
+
ADD COLUMN IF NOT EXISTS browser_kind text,
|
|
12
|
+
ADD COLUMN IF NOT EXISTS user_data_dir text;
|
|
13
|
+
|
|
14
|
+
DO $$
|
|
15
|
+
BEGIN
|
|
16
|
+
IF NOT EXISTS (
|
|
17
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_device_worker_id_fkey'
|
|
18
|
+
) THEN
|
|
19
|
+
ALTER TABLE public.auth_profiles
|
|
20
|
+
ADD CONSTRAINT auth_profiles_device_worker_id_fkey
|
|
21
|
+
FOREIGN KEY (device_worker_id)
|
|
22
|
+
REFERENCES public.device_workers (id)
|
|
23
|
+
ON DELETE CASCADE;
|
|
24
|
+
END IF;
|
|
25
|
+
END$$;
|
|
26
|
+
|
|
27
|
+
DO $$
|
|
28
|
+
BEGIN
|
|
29
|
+
IF NOT EXISTS (
|
|
30
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_browser_kind_check'
|
|
31
|
+
) THEN
|
|
32
|
+
ALTER TABLE public.auth_profiles
|
|
33
|
+
ADD CONSTRAINT auth_profiles_browser_kind_check
|
|
34
|
+
CHECK (browser_kind IS NULL OR browser_kind = ANY (ARRAY['chrome','brave','arc','edge']));
|
|
35
|
+
END IF;
|
|
36
|
+
END$$;
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS auth_profiles_device_worker_idx
|
|
39
|
+
ON public.auth_profiles (device_worker_id)
|
|
40
|
+
WHERE device_worker_id IS NOT NULL;
|
|
41
|
+
|
|
42
|
+
-- migrate:down
|
|
43
|
+
|
|
44
|
+
DROP INDEX IF EXISTS public.auth_profiles_device_worker_idx;
|
|
45
|
+
ALTER TABLE public.auth_profiles
|
|
46
|
+
DROP CONSTRAINT IF EXISTS auth_profiles_browser_kind_check,
|
|
47
|
+
DROP CONSTRAINT IF EXISTS auth_profiles_device_worker_id_fkey,
|
|
48
|
+
DROP COLUMN IF EXISTS user_data_dir,
|
|
49
|
+
DROP COLUMN IF EXISTS browser_kind,
|
|
50
|
+
DROP COLUMN IF EXISTS device_worker_id;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Add `cdp_url` to auth_profiles. For a device-bound `browser_session`
|
|
4
|
+
-- profile, exactly one of {user_data_dir, cdp_url} should be set:
|
|
5
|
+
-- user_data_dir → managed Chrome with isolated cookies (default)
|
|
6
|
+
-- cdp_url → attach to a running Chrome via remote-debugging-port
|
|
7
|
+
-- The application enforces this invariant; we don't add a CHECK constraint
|
|
8
|
+
-- because the OR-on-NULL semantics are awkward to express and the column
|
|
9
|
+
-- is harmless when both are NULL (legacy fleet path with cookies in
|
|
10
|
+
-- auth_data jsonb).
|
|
11
|
+
|
|
12
|
+
ALTER TABLE public.auth_profiles
|
|
13
|
+
ADD COLUMN IF NOT EXISTS cdp_url text;
|
|
14
|
+
|
|
15
|
+
-- A device-bound browser_session profile MUST set exactly one of
|
|
16
|
+
-- (user_data_dir, cdp_url). Other profile kinds — and non-device-bound
|
|
17
|
+
-- browser_session profiles (cookies in auth_data, fleet-served) — are
|
|
18
|
+
-- exempt. Enforcing this at the DB stops a buggy admin tool or a bad
|
|
19
|
+
-- merge from setting both and then having the connector silently prefer
|
|
20
|
+
-- whichever code path it sees first.
|
|
21
|
+
DO $$
|
|
22
|
+
BEGIN
|
|
23
|
+
IF NOT EXISTS (
|
|
24
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_device_browser_path_xor'
|
|
25
|
+
) THEN
|
|
26
|
+
ALTER TABLE public.auth_profiles
|
|
27
|
+
ADD CONSTRAINT auth_profiles_device_browser_path_xor
|
|
28
|
+
CHECK (
|
|
29
|
+
device_worker_id IS NULL
|
|
30
|
+
OR profile_kind <> 'browser_session'
|
|
31
|
+
OR (
|
|
32
|
+
(user_data_dir IS NOT NULL AND cdp_url IS NULL)
|
|
33
|
+
OR (user_data_dir IS NULL AND cdp_url IS NOT NULL)
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
END IF;
|
|
37
|
+
END$$;
|
|
38
|
+
|
|
39
|
+
-- migrate:down
|
|
40
|
+
|
|
41
|
+
ALTER TABLE public.auth_profiles
|
|
42
|
+
DROP CONSTRAINT IF EXISTS auth_profiles_device_browser_path_xor,
|
|
43
|
+
DROP COLUMN IF EXISTS cdp_url;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Unify notifications with events.
|
|
4
|
+
--
|
|
5
|
+
-- A notification was a (org, user, title, body, type, resource_url) row in
|
|
6
|
+
-- its own table with per-user `is_read`. Conceptually it's an event with a
|
|
7
|
+
-- particular kind + per-user delivery / read-state. This migration turns
|
|
8
|
+
-- every notification into:
|
|
9
|
+
-- 1. an event with semantic_type='notification' (org-wide visibility in
|
|
10
|
+
-- the events stream — searchable, addressable, links into knowledge);
|
|
11
|
+
-- 2. a notification_targets row (event_id, user_id, delivered_at, read_at)
|
|
12
|
+
-- so the inbox still scopes to the targeted user.
|
|
13
|
+
--
|
|
14
|
+
-- After this, "send to admins" inserts one event + N targets; "mark read"
|
|
15
|
+
-- updates a target row; "unread count" counts target rows without read_at.
|
|
16
|
+
-- Search across events naturally includes notifications, but a user's
|
|
17
|
+
-- inbox is still private to them.
|
|
18
|
+
|
|
19
|
+
CREATE TABLE public.notification_targets (
|
|
20
|
+
event_id bigint NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
|
|
21
|
+
user_id text NOT NULL,
|
|
22
|
+
delivered_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
23
|
+
read_at timestamp with time zone,
|
|
24
|
+
PRIMARY KEY (event_id, user_id)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Fast inbox lookups: list a user's unread notifications, newest first.
|
|
28
|
+
CREATE INDEX idx_notification_targets_user_unread
|
|
29
|
+
ON public.notification_targets (user_id, delivered_at DESC)
|
|
30
|
+
WHERE read_at IS NULL;
|
|
31
|
+
|
|
32
|
+
-- All of a user's notifications, newest first (for read-list pagination).
|
|
33
|
+
CREATE INDEX idx_notification_targets_user_all
|
|
34
|
+
ON public.notification_targets (user_id, delivered_at DESC);
|
|
35
|
+
|
|
36
|
+
-- Backfill existing notifications. We keep 1:1 row mapping (one event per
|
|
37
|
+
-- legacy notification) for safety — at scale the right model is "one event,
|
|
38
|
+
-- many targets" but the old schema didn't capture that and we can't
|
|
39
|
+
-- retroactively coalesce without an oracle.
|
|
40
|
+
WITH legacy AS (
|
|
41
|
+
SELECT id, organization_id, user_id, type, title, body,
|
|
42
|
+
resource_type, resource_id, resource_url, is_read, created_at
|
|
43
|
+
FROM public.notifications
|
|
44
|
+
ORDER BY id ASC
|
|
45
|
+
),
|
|
46
|
+
inserted AS (
|
|
47
|
+
INSERT INTO public.events
|
|
48
|
+
(organization_id, title, payload_text, payload_type, semantic_type,
|
|
49
|
+
occurred_at, created_at, metadata, origin_id)
|
|
50
|
+
SELECT
|
|
51
|
+
l.organization_id,
|
|
52
|
+
l.title,
|
|
53
|
+
l.body,
|
|
54
|
+
'text',
|
|
55
|
+
'notification',
|
|
56
|
+
l.created_at,
|
|
57
|
+
l.created_at,
|
|
58
|
+
jsonb_build_object(
|
|
59
|
+
'notification_type', l.type,
|
|
60
|
+
'resource_type', l.resource_type,
|
|
61
|
+
'resource_id', l.resource_id,
|
|
62
|
+
'resource_url', l.resource_url,
|
|
63
|
+
'legacy_notification_id', l.id
|
|
64
|
+
),
|
|
65
|
+
'notification:legacy:' || l.id::text
|
|
66
|
+
FROM legacy l
|
|
67
|
+
RETURNING id AS event_id, (metadata->>'legacy_notification_id')::bigint AS legacy_id
|
|
68
|
+
)
|
|
69
|
+
INSERT INTO public.notification_targets (event_id, user_id, delivered_at, read_at)
|
|
70
|
+
SELECT
|
|
71
|
+
i.event_id,
|
|
72
|
+
l.user_id,
|
|
73
|
+
l.created_at,
|
|
74
|
+
CASE WHEN l.is_read THEN l.created_at ELSE NULL END
|
|
75
|
+
FROM inserted i
|
|
76
|
+
JOIN public.notifications l ON l.id = i.legacy_id;
|
|
77
|
+
|
|
78
|
+
-- Drop the legacy table. All readers/writers go through the new service.
|
|
79
|
+
DROP TABLE public.notifications;
|
|
80
|
+
|
|
81
|
+
-- migrate:down
|
|
82
|
+
|
|
83
|
+
-- One-way migration. Recovery is from backup; events created here stay
|
|
84
|
+
-- (deleting them would also wipe their notification_targets via CASCADE).
|
|
85
|
+
-- If you really need to roll back: re-create the table, copy notifications
|
|
86
|
+
-- back out of events + notification_targets, drop the event rows.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- User-driven scheduled jobs.
|
|
4
|
+
--
|
|
5
|
+
-- Why a separate table:
|
|
6
|
+
-- * `runs` already holds *fired* / *pending-to-fire* rows via
|
|
7
|
+
-- scheduler.spawn(). Each scheduled_jobs row is the *definition* of a
|
|
8
|
+
-- recurring (or one-shot) schedule — its source of truth.
|
|
9
|
+
-- * The ticker (`scheduled-jobs-tick`) scans this table on cron, spawns
|
|
10
|
+
-- a runs row per firing via TaskScheduler.spawn, and advances
|
|
11
|
+
-- next_run_at from `cron`. If the tick or a firing fails, the next
|
|
12
|
+
-- tick re-reads the same row (next_run_at didn't move forward) and
|
|
13
|
+
-- retries. Self-healing.
|
|
14
|
+
-- * Attribution lives here: who scheduled it (user or agent), what run
|
|
15
|
+
-- was the trigger, what event was the trigger. Lets "why did the
|
|
16
|
+
-- system act?" become a single JOIN.
|
|
17
|
+
-- * Cascade-on-delete: when an agent is deleted, all its schedules
|
|
18
|
+
-- evaporate via the FK — no orphan wake-ups firing into the void.
|
|
19
|
+
|
|
20
|
+
CREATE TABLE public.scheduled_jobs (
|
|
21
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
22
|
+
organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE,
|
|
23
|
+
|
|
24
|
+
-- What fires
|
|
25
|
+
action_type text NOT NULL, -- 'send_notification' | 'wake_agent' | ...
|
|
26
|
+
action_args jsonb NOT NULL, -- handler payload
|
|
27
|
+
cron text, -- null = one-shot; cron string = recurring
|
|
28
|
+
next_run_at timestamp with time zone NOT NULL,
|
|
29
|
+
last_fired_at timestamp with time zone,
|
|
30
|
+
last_fired_run_id bigint, -- the runs.id from the most recent firing
|
|
31
|
+
paused boolean NOT NULL DEFAULT false,
|
|
32
|
+
|
|
33
|
+
description text NOT NULL, -- human summary for the UI / audit
|
|
34
|
+
|
|
35
|
+
-- Attribution
|
|
36
|
+
created_by_user text, -- user that scheduled it (null when agent did)
|
|
37
|
+
created_by_agent text, -- agent that scheduled it (null when user did)
|
|
38
|
+
source_run_id bigint, -- runs.id that originated the scheduling, if any
|
|
39
|
+
source_event_id bigint, -- events.id that originated, if any
|
|
40
|
+
source_thread_id text, -- chat-thread context, if any
|
|
41
|
+
|
|
42
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
43
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
44
|
+
|
|
45
|
+
CONSTRAINT scheduled_jobs_attribution_check CHECK (
|
|
46
|
+
created_by_user IS NOT NULL OR created_by_agent IS NOT NULL
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- Cascade: dropping an agent kills its scheduled jobs (so an agent's
|
|
51
|
+
-- wake-ups don't outlive the agent itself). Conditional so the migration
|
|
52
|
+
-- works on installs where the agents table doesn't exist yet (very
|
|
53
|
+
-- old) — every row already has organization_id which is the harder constraint.
|
|
54
|
+
DO $$
|
|
55
|
+
BEGIN
|
|
56
|
+
IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'agents' AND relkind = 'r') THEN
|
|
57
|
+
ALTER TABLE public.scheduled_jobs
|
|
58
|
+
ADD CONSTRAINT scheduled_jobs_agent_fkey
|
|
59
|
+
FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE;
|
|
60
|
+
END IF;
|
|
61
|
+
END$$;
|
|
62
|
+
|
|
63
|
+
DO $$
|
|
64
|
+
BEGIN
|
|
65
|
+
IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'runs' AND relkind = 'r') THEN
|
|
66
|
+
ALTER TABLE public.scheduled_jobs
|
|
67
|
+
ADD CONSTRAINT scheduled_jobs_source_run_fkey
|
|
68
|
+
FOREIGN KEY (source_run_id) REFERENCES public.runs(id) ON DELETE SET NULL;
|
|
69
|
+
END IF;
|
|
70
|
+
END$$;
|
|
71
|
+
|
|
72
|
+
DO $$
|
|
73
|
+
BEGIN
|
|
74
|
+
IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'events' AND relkind = 'r') THEN
|
|
75
|
+
ALTER TABLE public.scheduled_jobs
|
|
76
|
+
ADD CONSTRAINT scheduled_jobs_source_event_fkey
|
|
77
|
+
FOREIGN KEY (source_event_id) REFERENCES public.events(id) ON DELETE SET NULL;
|
|
78
|
+
END IF;
|
|
79
|
+
END$$;
|
|
80
|
+
|
|
81
|
+
-- Index: the ticker's hot read.
|
|
82
|
+
CREATE INDEX idx_scheduled_jobs_due
|
|
83
|
+
ON public.scheduled_jobs (next_run_at)
|
|
84
|
+
WHERE NOT paused;
|
|
85
|
+
|
|
86
|
+
-- Index: list per-agent / per-user.
|
|
87
|
+
CREATE INDEX idx_scheduled_jobs_org_agent
|
|
88
|
+
ON public.scheduled_jobs (organization_id, created_by_agent)
|
|
89
|
+
WHERE created_by_agent IS NOT NULL;
|
|
90
|
+
|
|
91
|
+
CREATE INDEX idx_scheduled_jobs_org_user
|
|
92
|
+
ON public.scheduled_jobs (organization_id, created_by_user)
|
|
93
|
+
WHERE created_by_user IS NOT NULL;
|
|
94
|
+
|
|
95
|
+
-- migrate:down
|
|
96
|
+
|
|
97
|
+
DROP TABLE IF EXISTS public.scheduled_jobs;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Drop the NOT NULL on auth_profiles.connector_key so browser_session
|
|
4
|
+
-- profiles can be device-bound resources without a connector binding.
|
|
5
|
+
--
|
|
6
|
+
-- A browser_session profile is physically (device, browser_kind, user_data_dir
|
|
7
|
+
-- XOR cdp_url) — the connector_key was always a hint, not a gate. One CDP
|
|
8
|
+
-- attach on a Mac already has cookies for every site the user is logged into;
|
|
9
|
+
-- forcing one row per connector against the same cdp_url was bookkeeping for
|
|
10
|
+
-- the DB's benefit, not the user's. Connection resolution falls back to
|
|
11
|
+
-- "browser_session on the connection's device_worker_id" when no exact
|
|
12
|
+
-- connector match exists.
|
|
13
|
+
--
|
|
14
|
+
-- Other profile kinds (env, oauth_app, oauth_account, interactive) remain
|
|
15
|
+
-- per-connector; the new check constraint keeps them honest.
|
|
16
|
+
|
|
17
|
+
ALTER TABLE public.auth_profiles
|
|
18
|
+
ALTER COLUMN connector_key DROP NOT NULL;
|
|
19
|
+
|
|
20
|
+
DO $$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF NOT EXISTS (
|
|
23
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_connector_key_required'
|
|
24
|
+
) THEN
|
|
25
|
+
ALTER TABLE public.auth_profiles
|
|
26
|
+
ADD CONSTRAINT auth_profiles_connector_key_required
|
|
27
|
+
CHECK (
|
|
28
|
+
connector_key IS NOT NULL
|
|
29
|
+
OR profile_kind = 'browser_session'
|
|
30
|
+
);
|
|
31
|
+
END IF;
|
|
32
|
+
END$$;
|
|
33
|
+
|
|
34
|
+
-- migrate:down
|
|
35
|
+
|
|
36
|
+
ALTER TABLE public.auth_profiles
|
|
37
|
+
DROP CONSTRAINT IF EXISTS auth_profiles_connector_key_required;
|
|
38
|
+
|
|
39
|
+
-- Restoring NOT NULL would fail if any browser_session rows now have
|
|
40
|
+
-- connector_key = NULL. Backfill with a placeholder before running this.
|
|
41
|
+
ALTER TABLE public.auth_profiles
|
|
42
|
+
ALTER COLUMN connector_key SET NOT NULL;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
|
|
3
|
+
-- Collapse `connection.config.auto_approve_actions` (string[]) and
|
|
4
|
+
-- `connection.config.require_approval_actions` (string[]) into a single
|
|
5
|
+
-- `action_modes` (Record<string, 'disabled' | 'approval' | 'auto'>) map.
|
|
6
|
+
--
|
|
7
|
+
-- The old two-array model couldn't express "agent must not call this op
|
|
8
|
+
-- at all" — every action the connector defined was always reachable, the
|
|
9
|
+
-- arrays only flipped approval prompts. The new map adds 'disabled' as the
|
|
10
|
+
-- third state and gives every op an explicit user-chosen mode.
|
|
11
|
+
--
|
|
12
|
+
-- Backfill rule, per row, for every op listed in either array:
|
|
13
|
+
-- op in auto_approve_actions → action_modes[op] = 'auto'
|
|
14
|
+
-- op in require_approval_actions → action_modes[op] = 'approval'
|
|
15
|
+
-- When an op appears in both, 'approval' wins (it's the stricter signal:
|
|
16
|
+
-- the user explicitly opted in to seeing an approval prompt).
|
|
17
|
+
--
|
|
18
|
+
-- Ops the user never touched are not stored in action_modes; the server
|
|
19
|
+
-- falls back to the connector's per-op `requires_approval` default at read
|
|
20
|
+
-- time, which preserves today's "all on" behavior.
|
|
21
|
+
--
|
|
22
|
+
-- We drop the two old keys in the same statement so the new state is the
|
|
23
|
+
-- only state on disk after migration.
|
|
24
|
+
|
|
25
|
+
UPDATE public.connections
|
|
26
|
+
SET config = (
|
|
27
|
+
COALESCE(config, '{}'::jsonb)
|
|
28
|
+
- 'auto_approve_actions'
|
|
29
|
+
- 'require_approval_actions'
|
|
30
|
+
)
|
|
31
|
+
|| jsonb_build_object(
|
|
32
|
+
'action_modes',
|
|
33
|
+
COALESCE(
|
|
34
|
+
(
|
|
35
|
+
-- 'approval' wins over 'auto' when an op appears in both arrays
|
|
36
|
+
-- (MIN('approval', 'auto') = 'approval' lexicographically).
|
|
37
|
+
SELECT jsonb_object_agg(op_key, mode)
|
|
38
|
+
FROM (
|
|
39
|
+
SELECT op_key, MIN(mode) AS mode
|
|
40
|
+
FROM (
|
|
41
|
+
SELECT op_key, 'approval'::text AS mode
|
|
42
|
+
FROM jsonb_array_elements_text(
|
|
43
|
+
CASE
|
|
44
|
+
WHEN jsonb_typeof(config->'require_approval_actions') = 'array'
|
|
45
|
+
THEN config->'require_approval_actions'
|
|
46
|
+
ELSE '[]'::jsonb
|
|
47
|
+
END
|
|
48
|
+
) AS op_key
|
|
49
|
+
UNION ALL
|
|
50
|
+
SELECT op_key, 'auto'::text AS mode
|
|
51
|
+
FROM jsonb_array_elements_text(
|
|
52
|
+
CASE
|
|
53
|
+
WHEN jsonb_typeof(config->'auto_approve_actions') = 'array'
|
|
54
|
+
THEN config->'auto_approve_actions'
|
|
55
|
+
ELSE '[]'::jsonb
|
|
56
|
+
END
|
|
57
|
+
) AS op_key
|
|
58
|
+
) all_modes
|
|
59
|
+
GROUP BY op_key
|
|
60
|
+
) collapsed
|
|
61
|
+
),
|
|
62
|
+
'{}'::jsonb
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
WHERE config IS NOT NULL
|
|
66
|
+
AND (
|
|
67
|
+
config ? 'auto_approve_actions'
|
|
68
|
+
OR config ? 'require_approval_actions'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- migrate:down
|
|
72
|
+
|
|
73
|
+
-- Reverse the collapse: split action_modes back into the two arrays.
|
|
74
|
+
-- 'auto' → auto_approve_actions
|
|
75
|
+
-- 'approval' → require_approval_actions
|
|
76
|
+
-- 'disabled' has no pre-refactor equivalent and is silently dropped on
|
|
77
|
+
-- downgrade — the agent will see the op again as if no override existed.
|
|
78
|
+
UPDATE public.connections
|
|
79
|
+
SET config = (
|
|
80
|
+
COALESCE(config, '{}'::jsonb) - 'action_modes'
|
|
81
|
+
)
|
|
82
|
+
|| jsonb_build_object(
|
|
83
|
+
'auto_approve_actions',
|
|
84
|
+
COALESCE(
|
|
85
|
+
(
|
|
86
|
+
SELECT jsonb_agg(key)
|
|
87
|
+
FROM jsonb_each_text(config->'action_modes')
|
|
88
|
+
WHERE value = 'auto'
|
|
89
|
+
),
|
|
90
|
+
'[]'::jsonb
|
|
91
|
+
),
|
|
92
|
+
'require_approval_actions',
|
|
93
|
+
COALESCE(
|
|
94
|
+
(
|
|
95
|
+
SELECT jsonb_agg(key)
|
|
96
|
+
FROM jsonb_each_text(config->'action_modes')
|
|
97
|
+
WHERE value = 'approval'
|
|
98
|
+
),
|
|
99
|
+
'[]'::jsonb
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
WHERE config IS NOT NULL
|
|
103
|
+
AND jsonb_typeof(config->'action_modes') = 'object';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
-- Relax the device-binding XOR for browser_session profiles to allow
|
|
3
|
+
-- mirror mode, where neither user_data_dir nor cdp_url is set on the
|
|
4
|
+
-- row (the source profile dir lives in auth_data.source_profile_dir).
|
|
5
|
+
-- Keep the mutual exclusion of the two columns so they can't be set
|
|
6
|
+
-- together; application validation enforces "exactly one of mirror /
|
|
7
|
+
-- cdp / legacy" per row.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE auth_profiles
|
|
10
|
+
DROP CONSTRAINT IF EXISTS auth_profiles_device_browser_path_xor;
|
|
11
|
+
|
|
12
|
+
ALTER TABLE auth_profiles
|
|
13
|
+
ADD CONSTRAINT auth_profiles_device_browser_path_mutex
|
|
14
|
+
CHECK (
|
|
15
|
+
device_worker_id IS NULL
|
|
16
|
+
OR profile_kind <> 'browser_session'
|
|
17
|
+
OR user_data_dir IS NULL
|
|
18
|
+
OR cdp_url IS NULL
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- migrate:down
|
|
22
|
+
ALTER TABLE auth_profiles
|
|
23
|
+
DROP CONSTRAINT IF EXISTS auth_profiles_device_browser_path_mutex;
|
|
24
|
+
|
|
25
|
+
ALTER TABLE auth_profiles
|
|
26
|
+
ADD CONSTRAINT auth_profiles_device_browser_path_xor
|
|
27
|
+
CHECK (
|
|
28
|
+
device_worker_id IS NULL
|
|
29
|
+
OR profile_kind <> 'browser_session'
|
|
30
|
+
OR ((user_data_dir IS NOT NULL) AND (cdp_url IS NULL))
|
|
31
|
+
OR ((user_data_dir IS NULL) AND (cdp_url IS NOT NULL))
|
|
32
|
+
);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- migrate:up
|
|
2
|
+
-- Phase A of moving `agents` from a globally-unique `id` PK to a per-org
|
|
3
|
+
-- composite PK `(organization_id, id)`. The application has always treated
|
|
4
|
+
-- agents as org-scoped (every read/delete/list filters by organization_id),
|
|
5
|
+
-- so the global PK is a latent footgun: two orgs cannot share an agent ID,
|
|
6
|
+
-- and a stale agent in one org silently blocks another org from using the
|
|
7
|
+
-- same name.
|
|
8
|
+
--
|
|
9
|
+
-- This phase is INTENTIONALLY NON-BREAKING. It only:
|
|
10
|
+
-- 1. Adds an `organization_id` column to each FK-holding child table
|
|
11
|
+
-- (NULLABLE — backfilled here, set NOT NULL in a later phase once the
|
|
12
|
+
-- app-code refactor lands so every INSERT writes the value).
|
|
13
|
+
-- 2. Backfills `organization_id` from agents (no orphan rows in prod).
|
|
14
|
+
-- 3. Adds a parallel UNIQUE constraint on `agents (organization_id, id)`
|
|
15
|
+
-- so the schema is ready for the eventual PK swap.
|
|
16
|
+
-- 4. Adds composite indexes on each child table so the upcoming
|
|
17
|
+
-- org-scoped query patterns are fast from day one.
|
|
18
|
+
--
|
|
19
|
+
-- The single-column PK on agents and the single-column FKs on child tables
|
|
20
|
+
-- stay in place. App code keeps working unmodified. The PK swap and FK
|
|
21
|
+
-- composite migration ship in a separate PR after the storage interfaces
|
|
22
|
+
-- are plumbed with `organization_id`.
|
|
23
|
+
|
|
24
|
+
-- ── 1. Add organization_id columns (nullable for now).
|
|
25
|
+
ALTER TABLE agent_grants ADD COLUMN organization_id text;
|
|
26
|
+
ALTER TABLE agent_connections ADD COLUMN organization_id text;
|
|
27
|
+
ALTER TABLE agent_users ADD COLUMN organization_id text;
|
|
28
|
+
ALTER TABLE agent_channel_bindings ADD COLUMN organization_id text;
|
|
29
|
+
ALTER TABLE grants ADD COLUMN organization_id text;
|
|
30
|
+
|
|
31
|
+
-- ── 2. Backfill from agents.
|
|
32
|
+
UPDATE agent_grants SET organization_id = a.organization_id FROM agents a WHERE agent_grants.agent_id = a.id;
|
|
33
|
+
UPDATE agent_connections SET organization_id = a.organization_id FROM agents a WHERE agent_connections.agent_id = a.id;
|
|
34
|
+
UPDATE agent_users SET organization_id = a.organization_id FROM agents a WHERE agent_users.agent_id = a.id;
|
|
35
|
+
UPDATE agent_channel_bindings SET organization_id = a.organization_id FROM agents a WHERE agent_channel_bindings.agent_id = a.id;
|
|
36
|
+
UPDATE grants SET organization_id = a.organization_id FROM agents a WHERE grants.agent_id = a.id;
|
|
37
|
+
|
|
38
|
+
-- ── 3. Parallel UNIQUE on agents (organization_id, id). The single-column
|
|
39
|
+
-- PK on (id) stays — this is purely additive and signals to readers
|
|
40
|
+
-- that org-scoped uniqueness is the eventual model. The PK swap in a
|
|
41
|
+
-- later migration will drop this UNIQUE and reuse the index for the
|
|
42
|
+
-- new composite PK.
|
|
43
|
+
ALTER TABLE agents
|
|
44
|
+
ADD CONSTRAINT agents_organization_id_id_key UNIQUE (organization_id, id);
|
|
45
|
+
|
|
46
|
+
-- ── 4. Composite indexes on child tables for upcoming org-scoped queries.
|
|
47
|
+
CREATE INDEX agent_grants_org_agent_idx ON agent_grants (organization_id, agent_id);
|
|
48
|
+
CREATE INDEX agent_connections_org_agent_idx ON agent_connections (organization_id, agent_id);
|
|
49
|
+
CREATE INDEX agent_users_org_agent_idx ON agent_users (organization_id, agent_id);
|
|
50
|
+
CREATE INDEX agent_channel_bindings_org_agent_idx ON agent_channel_bindings (organization_id, agent_id);
|
|
51
|
+
CREATE INDEX grants_org_agent_idx ON grants (organization_id, agent_id);
|
|
52
|
+
|
|
53
|
+
-- migrate:down
|
|
54
|
+
DROP INDEX IF EXISTS grants_org_agent_idx;
|
|
55
|
+
DROP INDEX IF EXISTS agent_channel_bindings_org_agent_idx;
|
|
56
|
+
DROP INDEX IF EXISTS agent_users_org_agent_idx;
|
|
57
|
+
DROP INDEX IF EXISTS agent_connections_org_agent_idx;
|
|
58
|
+
DROP INDEX IF EXISTS agent_grants_org_agent_idx;
|
|
59
|
+
|
|
60
|
+
ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_organization_id_id_key;
|
|
61
|
+
|
|
62
|
+
ALTER TABLE grants DROP COLUMN IF EXISTS organization_id;
|
|
63
|
+
ALTER TABLE agent_channel_bindings DROP COLUMN IF EXISTS organization_id;
|
|
64
|
+
ALTER TABLE agent_users DROP COLUMN IF EXISTS organization_id;
|
|
65
|
+
ALTER TABLE agent_connections DROP COLUMN IF EXISTS organization_id;
|
|
66
|
+
ALTER TABLE agent_grants DROP COLUMN IF EXISTS organization_id;
|