Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions db/migrations/20260516120000_agents_per_org_pk_swap.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
-- migrate:up
-- Phase C: swap `agents` PK from globally-unique `id` to per-org composite
-- `(organization_id, id)`. The application has always treated agents as
-- org-scoped (every read/list filters by organization_id) but the global PK
-- silently blocked two orgs from sharing an agent ID — for example, a stale
-- `food-ordering` in one org would prevent `food-ordering` in another.
--
-- Phase A (20260515120000) added the org column + composite indexes on the
-- 5 FK-holding child tables and backfilled values from agents. Phase B (the
-- application-code refactor in this PR) plumbs `organization_id` through every
-- INSERT/UPDATE/DELETE/SELECT touching these tables. This migration is the
-- final structural swap: it drops the single-column PK + FKs, adds the
-- composite PK + FKs, and widens the per-(agent,kind,pattern) uniques on
-- agent_users / agent_grants / grants with organization_id.

-- ── 0. Set NOT NULL on the columns Phase A backfilled.
-- Backfill defensively in case any rows snuck in NULL (e.g. embedded PGlite
-- installs that bypassed the dbmate runner during a partial state).
UPDATE public.agent_grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
UPDATE public.agent_connections c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
UPDATE public.agent_users c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
UPDATE public.agent_channel_bindings c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
UPDATE public.grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;

-- Drop any orphan rows (agent_id with no matching agents row). Backfill
-- can't recover these.
DELETE FROM public.agent_grants WHERE organization_id IS NULL;
DELETE FROM public.agent_connections WHERE organization_id IS NULL;
DELETE FROM public.agent_users WHERE organization_id IS NULL;
DELETE FROM public.agent_channel_bindings WHERE organization_id IS NULL;
DELETE FROM public.grants WHERE organization_id IS NULL;

ALTER TABLE public.agent_grants ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE public.agent_connections ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE public.agent_users ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE public.grants ALTER COLUMN organization_id SET NOT NULL;

-- ── 1. Drop the 6 single-column FKs into agents(id).
ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_fkey;
ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_agent_id_fkey;
ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_agent_id_fkey;
ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_agent_id_fkey;
ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_agent_id_fkey;
ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_agent_fkey;

-- ── 2. Drop the unique/PK constraints on child tables that scope to bare agent_id.
ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_pattern_key;
ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey;
ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey;

-- ── 3. Swap the PK on agents from (id) to (organization_id, id).
ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey;
ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id);

-- ── 4. Re-add per-org-scoped uniques on the child tables.
ALTER TABLE public.agent_grants
ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern);
ALTER TABLE public.agent_users
ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id);
ALTER TABLE public.grants
ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern);

-- ── 5. Re-add composite FKs (organization_id, agent_id) → agents(organization_id, id).
ALTER TABLE public.agent_grants
ADD CONSTRAINT agent_grants_org_agent_fkey
FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
ALTER TABLE public.agent_connections
ADD CONSTRAINT agent_connections_org_agent_fkey
FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
ALTER TABLE public.agent_users
ADD CONSTRAINT agent_users_org_agent_fkey
FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
ALTER TABLE public.agent_channel_bindings
ADD CONSTRAINT agent_channel_bindings_org_agent_fkey
FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
ALTER TABLE public.grants
ADD CONSTRAINT grants_org_agent_fkey
FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
ALTER TABLE public.scheduled_jobs
ADD CONSTRAINT scheduled_jobs_org_agent_fkey
FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

-- migrate:down
-- Reverse the swap. NOTE: this WILL FAIL if two orgs ended up sharing an
-- agent ID after this migration shipped (the previous PK on (id) requires
-- global uniqueness). That's by design — this migration's whole purpose is
-- to allow per-org agent IDs that the old PK forbids.

ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_org_agent_fkey;
ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_org_agent_fkey;
ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_org_agent_fkey;
ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_org_agent_fkey;
ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_org_agent_fkey;
ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_fkey;

ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey;
ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey;
ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_pattern_key;

ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey;
ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (id);

ALTER TABLE public.agent_grants ADD CONSTRAINT agent_grants_agent_id_pattern_key UNIQUE (agent_id, pattern);
ALTER TABLE public.agent_users ADD CONSTRAINT agent_users_pkey PRIMARY KEY (agent_id, platform, user_id);
ALTER TABLE public.grants ADD CONSTRAINT grants_pkey PRIMARY KEY (agent_id, kind, pattern);

ALTER TABLE public.agent_grants
ADD CONSTRAINT agent_grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ALTER TABLE public.agent_connections
ADD CONSTRAINT agent_connections_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ALTER TABLE public.agent_users
ADD CONSTRAINT agent_users_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ALTER TABLE public.agent_channel_bindings
ADD CONSTRAINT agent_channel_bindings_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ALTER TABLE public.grants
ADD CONSTRAINT grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ALTER TABLE public.scheduled_jobs
ADD CONSTRAINT scheduled_jobs_agent_fkey FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE;

ALTER TABLE public.grants ALTER COLUMN organization_id DROP NOT NULL;
ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id DROP NOT NULL;
ALTER TABLE public.agent_users ALTER COLUMN organization_id DROP NOT NULL;
ALTER TABLE public.agent_connections ALTER COLUMN organization_id DROP NOT NULL;
ALTER TABLE public.agent_grants ALTER COLUMN organization_id DROP NOT NULL;
47 changes: 24 additions & 23 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ CREATE TABLE public.agent_channel_bindings (
channel_id text NOT NULL,
team_id text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
organization_id text
organization_id text NOT NULL
);

--
Expand All @@ -132,7 +132,7 @@ CREATE TABLE public.agent_connections (
error_message text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
organization_id text,
organization_id text NOT NULL,
CONSTRAINT agent_connections_status_check CHECK ((status = ANY (ARRAY['active'::text, 'stopped'::text, 'error'::text])))
);

Expand All @@ -147,7 +147,7 @@ CREATE TABLE public.agent_grants (
expires_at timestamp with time zone,
granted_at timestamp with time zone DEFAULT now() NOT NULL,
denied boolean DEFAULT false,
organization_id text
organization_id text NOT NULL
);

--
Expand Down Expand Up @@ -191,7 +191,7 @@ CREATE TABLE public.agent_users (
platform text NOT NULL,
user_id text NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
organization_id text
organization_id text NOT NULL
);

--
Expand Down Expand Up @@ -1109,7 +1109,7 @@ CREATE TABLE public.grants (
expires_at timestamp with time zone,
granted_at timestamp with time zone DEFAULT now() NOT NULL,
denied boolean DEFAULT false NOT NULL,
organization_id text
organization_id text NOT NULL
);

--
Expand Down Expand Up @@ -2214,11 +2214,11 @@ ALTER TABLE ONLY public.agent_connections
ADD CONSTRAINT agent_connections_pkey PRIMARY KEY (id);

--
-- Name: agent_grants agent_grants_agent_id_pattern_key; Type: CONSTRAINT; Schema: public; Owner: -
-- Name: agent_grants agent_grants_org_agent_pattern_key; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.agent_grants
ADD CONSTRAINT agent_grants_agent_id_pattern_key UNIQUE (agent_id, pattern);
ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern);

--
-- Name: agent_grants agent_grants_pkey; Type: CONSTRAINT; Schema: public; Owner: -
Expand All @@ -2239,14 +2239,14 @@ ALTER TABLE ONLY public.agent_secrets
--

ALTER TABLE ONLY public.agent_users
ADD CONSTRAINT agent_users_pkey PRIMARY KEY (agent_id, platform, user_id);
ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id);

--
-- Name: agents agents_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.agents
ADD CONSTRAINT agents_pkey PRIMARY KEY (id);
ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id);

--
-- Name: auth_profiles auth_profiles_org_id_unique; Type: CONSTRAINT; Schema: public; Owner: -
Expand Down Expand Up @@ -2421,7 +2421,7 @@ ALTER TABLE ONLY public.feeds
--

ALTER TABLE ONLY public.grants
ADD CONSTRAINT grants_pkey PRIMARY KEY (agent_id, kind, pattern);
ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern);

--
-- Name: watcher_versions insight_template_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
Expand Down Expand Up @@ -4104,32 +4104,32 @@ ALTER TABLE ONLY public.account
ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE;

--
-- Name: agent_channel_bindings agent_channel_bindings_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- Name: agent_channel_bindings agent_channel_bindings_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.agent_channel_bindings
ADD CONSTRAINT agent_channel_bindings_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ADD CONSTRAINT agent_channel_bindings_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

--
-- Name: agent_connections agent_connections_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- Name: agent_connections agent_connections_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.agent_connections
ADD CONSTRAINT agent_connections_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ADD CONSTRAINT agent_connections_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

--
-- Name: agent_grants agent_grants_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- Name: agent_grants agent_grants_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.agent_grants
ADD CONSTRAINT agent_grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ADD CONSTRAINT agent_grants_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

--
-- Name: agent_users agent_users_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- Name: agent_users agent_users_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.agent_users
ADD CONSTRAINT agent_users_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ADD CONSTRAINT agent_users_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

--
-- Name: agents agents_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
Expand Down Expand Up @@ -4545,11 +4545,11 @@ ALTER TABLE ONLY public.event_classifiers
ADD CONSTRAINT fk_event_classifiers_insight FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE SET NULL;

--
-- Name: grants grants_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- Name: grants grants_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.grants
ADD CONSTRAINT grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
ADD CONSTRAINT grants_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

--
-- Name: watcher_versions insight_template_versions_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
Expand Down Expand Up @@ -4811,11 +4811,11 @@ ALTER TABLE ONLY public.runs
ADD CONSTRAINT runs_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE SET NULL;

--
-- Name: scheduled_jobs scheduled_jobs_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- Name: scheduled_jobs scheduled_jobs_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.scheduled_jobs
ADD CONSTRAINT scheduled_jobs_agent_fkey FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE;
ADD CONSTRAINT scheduled_jobs_org_agent_fkey FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;

--
-- Name: scheduled_jobs scheduled_jobs_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
Expand Down Expand Up @@ -4990,4 +4990,5 @@ INSERT INTO public.schema_migrations (version) VALUES
('20260514160000'),
('20260515120000'),
('20260515150000'),
('20260515160000');
('20260515160000'),
('20260516120000');
1 change: 1 addition & 0 deletions examples/office-bot/lobu.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fail closed and deny with a reason.
"""

[memory]
organization_id = "UdNAH1bb3csC842vhOgxAHVcfX4tYU5A"
enabled = true
org = "lobu-team"
name = "Lobu Team"
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/agent-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ export interface StoredConnection {
id: string;
platform: string;
agentId?: string;
/**
* Organization id this connection belongs to. Optional in the type for
* back-compat with in-memory tests, but required at the storage layer
* (`agent_connections.organization_id` is NOT NULL post-Phase-C).
*/
organizationId?: string;
config: Record<string, any>;
settings: ConnectionSettings;
metadata: Record<string, any>;
Expand Down
Loading
Loading