@opengeni/db 0.7.3 → 0.10.7

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.
Files changed (70) hide show
  1. package/dist/{chunk-YFQ7SGE4.js → chunk-BMFDXFPA.js} +23 -1
  2. package/dist/chunk-BMFDXFPA.js.map +1 -0
  3. package/dist/chunk-KW526IJA.js +127 -0
  4. package/dist/chunk-KW526IJA.js.map +1 -0
  5. package/dist/{chunk-B22X3IEZ.js → chunk-P6PKXY5W.js} +604 -32
  6. package/dist/chunk-P6PKXY5W.js.map +1 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +7408 -2276
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.d.ts +29 -4
  11. package/dist/migrate.js +3 -1
  12. package/dist/provision-roles.d.ts +1122 -73
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/{schema-BN5mB9xZ.d.ts → schema-CqkzrBRS.d.ts} +6967 -4457
  15. package/dist/schema.d.ts +1 -1
  16. package/dist/schema.js +19 -1
  17. package/drizzle/0053_codex_credential_leases.sql +2 -2
  18. package/drizzle/0057_durable_queue_control.sql +1 -1
  19. package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
  20. package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
  21. package/drizzle/0063_session_control_mega_foundation.sql +1 -1
  22. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +15 -0
  23. package/drizzle/0065_codex_subscription_overview.sql +168 -0
  24. package/drizzle/0065_session_attempt_quiescence.sql +26 -0
  25. package/drizzle/0065_session_tool_policy.sql +38 -0
  26. package/drizzle/0066_session_interruption_attempt_lookup.sql +4 -0
  27. package/drizzle/0067_session_event_payload_bounds.sql +209 -0
  28. package/drizzle/0068_workspace_control_event_bounds.sql +134 -0
  29. package/drizzle/0069_session_event_history_backfill.sql +32 -0
  30. package/drizzle/0070_session_event_type_sequence_lookup.sql +4 -0
  31. package/drizzle/0071_session_event_monitoring_tail.sql +10 -0
  32. package/drizzle/0072_sessions_workspace_created_id_idx.sql +4 -0
  33. package/drizzle/0073_sessions_workspace_updated_id_idx.sql +4 -0
  34. package/drizzle/0074_session_activity_revisions.sql +72 -0
  35. package/drizzle/0075_sessions_workspace_activity_revision_idx.sql +4 -0
  36. package/drizzle/0076_session_workflow_wake_acl.sql +13 -0
  37. package/drizzle/0077_session_attempt_latest_lookup.sql +4 -0
  38. package/drizzle/0094_quarantine_credential_bearing_catalog_urls.sql +51 -0
  39. package/drizzle/0095_github_existing_installations.sql +84 -0
  40. package/drizzle/0096_session_turn_initiators.sql +97 -0
  41. package/drizzle/0097_host_export_outbox.sql +1220 -0
  42. package/drizzle/0098_usage_events_workspace_session_idx.sql +4 -0
  43. package/drizzle/0099_session_human_input_attempt_owner_index.sql +6 -0
  44. package/drizzle/0100_session_human_input_requests.sql +89 -0
  45. package/drizzle/0101_session_mcp_connection_refs.sql +30 -0
  46. package/drizzle/0102_session_command_receipt_service_actor.sql +16 -0
  47. package/drizzle/0103_host_export_root_session.sql +166 -0
  48. package/drizzle/0104_host_export_root_session_backfill.sql +27 -0
  49. package/drizzle/0105_session_turn_instructions.sql +9 -0
  50. package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
  51. package/drizzle/0107_host_export_lineage_contract.sql +381 -0
  52. package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
  53. package/package.json +5 -4
  54. package/src/codex-token-resolver.ts +175 -14
  55. package/src/connection-token-resolver.ts +428 -119
  56. package/src/event-payload-sanitizer.ts +88 -20
  57. package/src/index.ts +7208 -1273
  58. package/src/memory-domain.ts +1 -1
  59. package/src/migrate.ts +86 -23
  60. package/src/persistence-errors.ts +252 -0
  61. package/src/provision-roles.ts +42 -0
  62. package/src/schema.ts +658 -34
  63. package/src/session-control.ts +520 -38
  64. package/src/session-queue-commands.ts +385 -61
  65. package/src/session-tool-call-settlement.ts +58 -7
  66. package/src/turn-initiator.ts +155 -0
  67. package/dist/chunk-7LDU7F5P.js +0 -80
  68. package/dist/chunk-7LDU7F5P.js.map +0 -1
  69. package/dist/chunk-B22X3IEZ.js.map +0 -1
  70. package/dist/chunk-YFQ7SGE4.js.map +0 -1
@@ -0,0 +1,134 @@
1
+ -- deployment-mode: rolling
2
+ -- tracking-64: workspace-control rows are compact invalidations, not evidence blobs.
3
+ -- Preserve exact source byte counts while bounding the two free-form fields so
4
+ -- old binaries and direct writers cannot create a durable replay poison row.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ ALTER TABLE workspace_control_events
10
+ ADD COLUMN reason_original_bytes integer,
11
+ ADD COLUMN actor_original_bytes integer;
12
+
13
+ CREATE OR REPLACE FUNCTION opengeni_private.truncate_workspace_control_text(
14
+ source_text text,
15
+ max_bytes integer
16
+ )
17
+ RETURNS text
18
+ LANGUAGE plpgsql
19
+ IMMUTABLE
20
+ STRICT
21
+ SET search_path = pg_catalog
22
+ AS $function$
23
+ DECLARE
24
+ marker constant text := '…[truncated]';
25
+ prefix_budget integer := greatest(0, max_bytes - octet_length(marker));
26
+ candidate text;
27
+ excess integer;
28
+ BEGIN
29
+ IF octet_length(source_text) <= max_bytes THEN
30
+ RETURN source_text;
31
+ END IF;
32
+
33
+ -- left() is character-safe. Starting at at most prefix_budget characters
34
+ -- caps allocation even for a multi-megabyte legacy value; each pass removes
35
+ -- enough characters for the worst-case four-byte UTF-8 encoding.
36
+ candidate := left(source_text, prefix_budget);
37
+ WHILE octet_length(candidate) > prefix_budget LOOP
38
+ excess := octet_length(candidate) - prefix_budget;
39
+ candidate := left(candidate, greatest(0, char_length(candidate) - ceil(excess / 4.0)::integer));
40
+ END LOOP;
41
+ RETURN candidate || marker;
42
+ END;
43
+ $function$;
44
+
45
+ CREATE OR REPLACE FUNCTION opengeni_private.bound_workspace_control_event()
46
+ RETURNS trigger
47
+ LANGUAGE plpgsql
48
+ SET search_path = pg_catalog
49
+ AS $function$
50
+ DECLARE
51
+ source_bytes integer;
52
+ BEGIN
53
+ IF NEW.reason IS NULL THEN
54
+ NEW.reason_original_bytes := NULL;
55
+ ELSE
56
+ source_bytes := octet_length(NEW.reason);
57
+ IF TG_OP = 'INSERT' THEN
58
+ NEW.reason_original_bytes := greatest(source_bytes, coalesce(NEW.reason_original_bytes, 0));
59
+ ELSIF NEW.reason IS DISTINCT FROM OLD.reason THEN
60
+ -- An update from an old binary carries the old metadata column even
61
+ -- though it knows only `reason`. A genuinely changed raw value starts a
62
+ -- new byte fact instead of inheriting the previous value's larger count.
63
+ NEW.reason_original_bytes := source_bytes;
64
+ ELSE
65
+ NEW.reason_original_bytes := greatest(
66
+ source_bytes,
67
+ coalesce(NEW.reason_original_bytes, OLD.reason_original_bytes, 0)
68
+ );
69
+ END IF;
70
+ NEW.reason := opengeni_private.truncate_workspace_control_text(NEW.reason, 8192);
71
+ END IF;
72
+
73
+ source_bytes := octet_length(NEW.actor);
74
+ IF TG_OP = 'INSERT' THEN
75
+ NEW.actor_original_bytes := greatest(source_bytes, coalesce(NEW.actor_original_bytes, 0));
76
+ ELSIF NEW.actor IS DISTINCT FROM OLD.actor THEN
77
+ NEW.actor_original_bytes := source_bytes;
78
+ ELSE
79
+ NEW.actor_original_bytes := greatest(
80
+ source_bytes,
81
+ coalesce(NEW.actor_original_bytes, OLD.actor_original_bytes, 0)
82
+ );
83
+ END IF;
84
+ NEW.actor := opengeni_private.truncate_workspace_control_text(NEW.actor, 1024);
85
+ RETURN NEW;
86
+ END;
87
+ $function$;
88
+
89
+ CREATE TRIGGER workspace_control_events_bound_fields
90
+ BEFORE INSERT OR UPDATE OF reason, actor, reason_original_bytes, actor_original_bytes
91
+ ON workspace_control_events
92
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.bound_workspace_control_event();
93
+
94
+ -- Rewrite only historical rows that violate the new durable caps. Already
95
+ -- bounded rows are safe to serve as-is and retain null metadata as the explicit
96
+ -- rolling-upgrade legacy shape; avoiding a blanket UPDATE keeps installation
97
+ -- work proportional to poison rows instead of table cardinality. The original
98
+ -- counts are supplied in the same statement so truncation never erases truth.
99
+ UPDATE workspace_control_events
100
+ SET
101
+ reason_original_bytes = CASE WHEN reason IS NULL THEN NULL ELSE octet_length(reason) END,
102
+ actor_original_bytes = octet_length(actor),
103
+ reason = reason,
104
+ actor = actor
105
+ WHERE octet_length(reason) > 8192
106
+ OR octet_length(actor) > 1024;
107
+
108
+ ALTER TABLE workspace_control_events
109
+ ADD CONSTRAINT workspace_control_events_reason_bytes_check
110
+ CHECK (reason IS NULL OR octet_length(reason) <= 8192) NOT VALID,
111
+ ADD CONSTRAINT workspace_control_events_actor_bytes_check
112
+ CHECK (octet_length(actor) <= 1024) NOT VALID,
113
+ ADD CONSTRAINT workspace_control_events_original_bytes_check
114
+ CHECK (
115
+ (reason IS NULL AND reason_original_bytes IS NULL)
116
+ OR (
117
+ reason IS NOT NULL
118
+ AND (
119
+ reason_original_bytes IS NULL
120
+ OR reason_original_bytes >= octet_length(reason)
121
+ )
122
+ )
123
+ ) NOT VALID,
124
+ ADD CONSTRAINT workspace_control_events_actor_original_bytes_check
125
+ CHECK (
126
+ actor_original_bytes IS NULL
127
+ OR actor_original_bytes >= octet_length(actor)
128
+ ) NOT VALID;
129
+
130
+ ALTER TABLE workspace_control_events
131
+ VALIDATE CONSTRAINT workspace_control_events_reason_bytes_check,
132
+ VALIDATE CONSTRAINT workspace_control_events_actor_bytes_check,
133
+ VALIDATE CONSTRAINT workspace_control_events_original_bytes_check,
134
+ VALIDATE CONSTRAINT workspace_control_events_actor_original_bytes_check;
@@ -0,0 +1,32 @@
1
+ -- deployment-mode: rolling
2
+ -- tracking-64: finish the 0067 expand/contract sequence. The trigger has bounded
3
+ -- every new/updated row since 0067; now rewrite only legacy violations through
4
+ -- that trigger and validate the durable storage invariant.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ UPDATE session_events
10
+ SET
11
+ payload = payload,
12
+ type = type,
13
+ client_event_id = client_event_id,
14
+ producer_id = producer_id,
15
+ turn_association = turn_association,
16
+ duplicate_reason = duplicate_reason
17
+ WHERE octet_length(payload::text) > 65536
18
+ OR octet_length(type) > 256
19
+ OR position(E'\n' in type) > 0
20
+ OR position(E'\r' in type) > 0
21
+ OR (client_event_id IS NOT NULL AND octet_length(client_event_id) > 1024)
22
+ OR (producer_id IS NOT NULL AND octet_length(producer_id) > 1024)
23
+ OR (turn_association IS NOT NULL AND octet_length(turn_association) > 64)
24
+ OR (duplicate_reason IS NOT NULL AND octet_length(duplicate_reason) > 4096);
25
+
26
+ ALTER TABLE session_events
27
+ VALIDATE CONSTRAINT session_events_payload_bytes_check,
28
+ VALIDATE CONSTRAINT session_events_type_bytes_check,
29
+ VALIDATE CONSTRAINT session_events_client_event_id_bytes_check,
30
+ VALIDATE CONSTRAINT session_events_producer_id_bytes_check,
31
+ VALIDATE CONSTRAINT session_events_turn_association_bytes_check,
32
+ VALIDATE CONSTRAINT session_events_duplicate_reason_bytes_check;
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_events_workspace_session_type_sequence_idx"
4
+ ON "session_events" ("workspace_id", "session_id", "type", "sequence" DESC);
@@ -0,0 +1,10 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_events_workspace_session_monitoring_tail_idx"
4
+ ON "session_events" ("workspace_id", "session_id", "sequence" DESC)
5
+ WHERE "type" NOT IN (
6
+ 'agent.message.delta',
7
+ 'agent.reasoning.delta',
8
+ 'sandbox.command.output.delta',
9
+ 'terminal.pty.output.delta'
10
+ );
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "sessions_workspace_created_id_idx"
4
+ ON "sessions" ("workspace_id", "created_at" DESC, "id" DESC);
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "sessions_workspace_updated_id_idx"
4
+ ON "sessions" ("workspace_id", "updated_at" DESC, "id" DESC);
@@ -0,0 +1,72 @@
1
+ -- deployment-mode: rolling
2
+ -- tracking-65: transactional workspace revisions make updated-order discovery and
3
+ -- its next incremental scan one gap-free handoff independent of application
4
+ -- clocks. Existing sessions remain revision zero until their next activity.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ CREATE TABLE "workspace_session_activity_revisions" (
10
+ "workspace_id" uuid PRIMARY KEY,
11
+ "account_id" uuid NOT NULL,
12
+ "revision" bigint NOT NULL DEFAULT 0,
13
+ CONSTRAINT "workspace_session_activity_revisions_workspace_account_fk"
14
+ FOREIGN KEY ("workspace_id", "account_id")
15
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
16
+ CONSTRAINT "workspace_session_activity_revisions_revision_check"
17
+ CHECK ("revision" >= 0)
18
+ );
19
+
20
+ ALTER TABLE "workspace_session_activity_revisions" ENABLE ROW LEVEL SECURITY;
21
+ ALTER TABLE "workspace_session_activity_revisions" FORCE ROW LEVEL SECURITY;
22
+ CREATE POLICY workspace_isolation ON "workspace_session_activity_revisions"
23
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
24
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
25
+
26
+ -- PostgreSQL installs this constant default without rewriting existing rows.
27
+ -- Revision zero is an intentional legacy bucket ordered by (updated_at, id).
28
+ ALTER TABLE "sessions"
29
+ ADD COLUMN "activity_revision" bigint NOT NULL DEFAULT 0;
30
+
31
+ CREATE OR REPLACE FUNCTION opengeni_private.assign_session_activity_revision()
32
+ RETURNS trigger
33
+ LANGUAGE plpgsql
34
+ SET search_path = pg_catalog
35
+ AS $function$
36
+ DECLARE
37
+ next_revision bigint;
38
+ BEGIN
39
+ EXECUTE format(
40
+ 'INSERT INTO %I.workspace_session_activity_revisions AS counter '
41
+ || '(workspace_id, account_id, revision) VALUES ($1, $2, 1) '
42
+ || 'ON CONFLICT (workspace_id) DO UPDATE SET '
43
+ || 'account_id = excluded.account_id, revision = counter.revision + 1 '
44
+ || 'RETURNING revision',
45
+ TG_TABLE_SCHEMA
46
+ )
47
+ INTO next_revision
48
+ USING NEW.workspace_id, NEW.account_id;
49
+
50
+ NEW.activity_revision := next_revision;
51
+ RETURN NEW;
52
+ END;
53
+ $function$;
54
+
55
+ CREATE TRIGGER sessions_assign_activity_revision
56
+ BEFORE INSERT OR UPDATE OF updated_at, activity_revision
57
+ ON "sessions"
58
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.assign_session_activity_revision();
59
+
60
+ DO $grants$
61
+ DECLARE target_schema text := current_schema();
62
+ BEGIN
63
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
64
+ EXECUTE format(
65
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.workspace_session_activity_revisions TO opengeni_app',
66
+ target_schema
67
+ );
68
+ END IF;
69
+ END $grants$;
70
+
71
+ RESET statement_timeout;
72
+ RESET lock_timeout;
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "sessions_workspace_activity_revision_idx"
4
+ ON "sessions" ("workspace_id", "activity_revision" DESC, "updated_at" DESC, "id" DESC);
@@ -0,0 +1,13 @@
1
+ -- deployment-mode: rolling
2
+ -- Migration 0063 recreates this SECURITY DEFINER function and revokes its
3
+ -- default PUBLIC authority. Restore only the runtime dispatcher's exact grant;
4
+ -- production Helm runs migrations without a separate provision-roles pass.
5
+ REVOKE ALL ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer) FROM PUBLIC;
6
+
7
+ DO $$
8
+ BEGIN
9
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
10
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer)
11
+ TO opengeni_app;
12
+ END IF;
13
+ END $$;
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_turn_attempts_latest_session_idx"
4
+ ON "session_turn_attempts" ("workspace_id", "session_id", "started_at" DESC, "id" DESC);
@@ -0,0 +1,51 @@
1
+ -- deployment-mode: rolling
2
+ -- Catalog endpoints are public data. Remove opaque query/fragment/userinfo/path URLs
3
+ -- imported before the catalog hygiene gate and prevent their continued use.
4
+ DELETE FROM "capability_installations" AS "installation"
5
+ WHERE EXISTS (
6
+ SELECT 1
7
+ FROM "capability_catalog_items" AS "registry_item"
8
+ WHERE
9
+ "registry_item"."id" = "installation"."capability_id"
10
+ AND "registry_item"."workspace_id" IS NULL
11
+ AND "registry_item"."source" = 'registry'
12
+ AND "registry_item"."metadata" ->> 'registry' = 'integrations.sh'
13
+ AND (
14
+ "registry_item"."endpoint_url" LIKE '%?%'
15
+ OR "registry_item"."endpoint_url" LIKE '%#%'
16
+ OR "registry_item"."endpoint_url" ~* '^https?://[^/?#]*@'
17
+ OR "registry_item"."endpoint_url" ~* '^https?://[^/?#]+/(?:[^/?#]*/)*[^/?#]{24,}(?:/|$)'
18
+ OR "registry_item"."mcp_url" LIKE '%?%'
19
+ OR "registry_item"."mcp_url" LIKE '%#%'
20
+ OR "registry_item"."mcp_url" ~* '^https?://[^/?#]*@'
21
+ OR "registry_item"."mcp_url" ~* '^https?://[^/?#]+/(?:[^/?#]*/)*[^/?#]{24,}(?:/|$)'
22
+ )
23
+ )
24
+ AND NOT EXISTS (
25
+ SELECT 1
26
+ FROM "capability_catalog_items" AS "workspace_item"
27
+ WHERE
28
+ "workspace_item"."workspace_id" = "installation"."workspace_id"
29
+ AND "workspace_item"."id" = "installation"."capability_id"
30
+ );
31
+
32
+ DELETE FROM "capability_catalog_items"
33
+ WHERE
34
+ "source" = 'registry'
35
+ AND "metadata" ->> 'registry' = 'integrations.sh'
36
+ AND (
37
+ "endpoint_url" LIKE '%?%'
38
+ OR "endpoint_url" LIKE '%#%'
39
+ OR "endpoint_url" ~* '^https?://[^/?#]*@'
40
+ OR "endpoint_url" ~* '^https?://[^/?#]+/(?:[^/?#]*/)*[^/?#]{24,}(?:/|$)'
41
+ OR "mcp_url" LIKE '%?%'
42
+ OR "mcp_url" LIKE '%#%'
43
+ OR "mcp_url" ~* '^https?://[^/?#]*@'
44
+ OR "mcp_url" ~* '^https?://[^/?#]+/(?:[^/?#]*/)*[^/?#]{24,}(?:/|$)'
45
+ );
46
+
47
+ -- Earlier import diagnostics may contain the same URLs. Counts remain in their
48
+ -- dedicated columns; the unstructured diagnostic payload is not authoritative.
49
+ UPDATE "import_batches"
50
+ SET "details" = '{}'::jsonb, "updated_at" = NOW()
51
+ WHERE "source" = 'integrations.sh';
@@ -0,0 +1,84 @@
1
+ -- deployment-mode: rolling
2
+
3
+ ALTER TABLE "github_installations"
4
+ ADD COLUMN IF NOT EXISTS "repository_scope" text NOT NULL DEFAULT 'all';
5
+ ALTER TABLE "github_installations"
6
+ ADD COLUMN IF NOT EXISTS "linked_by_subject_id" text;
7
+
8
+ DO $$
9
+ BEGIN
10
+ IF NOT EXISTS (
11
+ SELECT 1 FROM pg_constraint
12
+ WHERE conname = 'github_installations_repository_scope_check'
13
+ AND conrelid = 'github_installations'::regclass
14
+ ) THEN
15
+ ALTER TABLE "github_installations"
16
+ ADD CONSTRAINT "github_installations_repository_scope_check"
17
+ CHECK ("repository_scope" IN ('all', 'selected'));
18
+ END IF;
19
+ END $$;
20
+
21
+ CREATE TABLE IF NOT EXISTS "github_installation_repositories" (
22
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
23
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
24
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
25
+ "installation_id" integer NOT NULL,
26
+ "repository_id" bigint NOT NULL,
27
+ "created_at" timestamptz NOT NULL DEFAULT now()
28
+ );
29
+
30
+ DO $$
31
+ BEGIN
32
+ IF NOT EXISTS (
33
+ SELECT 1 FROM pg_constraint
34
+ WHERE conname = 'github_installation_repositories_workspace_account_fk'
35
+ AND conrelid = 'github_installation_repositories'::regclass
36
+ ) THEN
37
+ ALTER TABLE "github_installation_repositories"
38
+ ADD CONSTRAINT "github_installation_repositories_workspace_account_fk"
39
+ FOREIGN KEY ("workspace_id", "account_id")
40
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE;
41
+ END IF;
42
+ IF NOT EXISTS (
43
+ SELECT 1 FROM pg_constraint
44
+ WHERE conname = 'github_installation_repositories_installation_fk'
45
+ AND conrelid = 'github_installation_repositories'::regclass
46
+ ) THEN
47
+ ALTER TABLE "github_installation_repositories"
48
+ ADD CONSTRAINT "github_installation_repositories_installation_fk"
49
+ FOREIGN KEY ("workspace_id", "installation_id")
50
+ REFERENCES "github_installations"("workspace_id", "installation_id") ON DELETE CASCADE;
51
+ END IF;
52
+ END $$;
53
+
54
+ CREATE UNIQUE INDEX IF NOT EXISTS "github_install_repo_workspace_installation_repo_idx"
55
+ ON "github_installation_repositories" ("workspace_id", "installation_id", "repository_id");
56
+ CREATE INDEX IF NOT EXISTS "github_install_repo_workspace_installation_idx"
57
+ ON "github_installation_repositories" ("workspace_id", "installation_id");
58
+
59
+ ALTER TABLE "github_installation_repositories" ENABLE ROW LEVEL SECURITY;
60
+ ALTER TABLE "github_installation_repositories" FORCE ROW LEVEL SECURITY;
61
+ DO $$
62
+ BEGIN
63
+ IF EXISTS (
64
+ SELECT 1 FROM pg_policies
65
+ WHERE schemaname = current_schema()
66
+ AND tablename = 'github_installation_repositories'
67
+ AND policyname = 'workspace_isolation'
68
+ ) THEN
69
+ DROP POLICY workspace_isolation ON "github_installation_repositories";
70
+ END IF;
71
+ END $$;
72
+ CREATE POLICY workspace_isolation ON "github_installation_repositories"
73
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
74
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
75
+
76
+ DO $$
77
+ BEGIN
78
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
79
+ EXECUTE format(
80
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO opengeni_app',
81
+ current_schema()
82
+ );
83
+ END IF;
84
+ END $$;
@@ -0,0 +1,97 @@
1
+ -- deployment-mode: rolling
2
+ -- Existing rows and old rolling-deploy writers receive an explicit service
3
+ -- sentinel. Host credential resolvers must reject that sentinel; it is never
4
+ -- inferred back to a session creator.
5
+
6
+ ALTER TABLE "sessions"
7
+ ADD COLUMN IF NOT EXISTS "created_by_kind" text NOT NULL DEFAULT 'service',
8
+ ADD COLUMN IF NOT EXISTS "created_by_subject_id" text NOT NULL DEFAULT 'unattributed-legacy',
9
+ ADD COLUMN IF NOT EXISTS "created_by_context" jsonb NOT NULL DEFAULT '{"backfill":true}'::jsonb;
10
+
11
+ ALTER TABLE "session_turns"
12
+ ADD COLUMN IF NOT EXISTS "initiator_kind" text NOT NULL DEFAULT 'service',
13
+ ADD COLUMN IF NOT EXISTS "initiator_subject_id" text NOT NULL DEFAULT 'unattributed-legacy',
14
+ ADD COLUMN IF NOT EXISTS "initiator_context" jsonb NOT NULL DEFAULT '{"backfill":true}'::jsonb;
15
+
16
+ DO $$
17
+ BEGIN
18
+ IF NOT EXISTS (
19
+ SELECT 1 FROM pg_constraint
20
+ WHERE conname = 'sessions_created_by_kind_check'
21
+ AND conrelid = 'sessions'::regclass
22
+ ) THEN
23
+ ALTER TABLE "sessions"
24
+ ADD CONSTRAINT "sessions_created_by_kind_check"
25
+ CHECK ("created_by_kind" IN ('subject', 'service')) NOT VALID;
26
+ END IF;
27
+ IF NOT EXISTS (
28
+ SELECT 1 FROM pg_constraint
29
+ WHERE conname = 'session_turns_initiator_kind_check'
30
+ AND conrelid = 'session_turns'::regclass
31
+ ) THEN
32
+ ALTER TABLE "session_turns"
33
+ ADD CONSTRAINT "session_turns_initiator_kind_check"
34
+ CHECK ("initiator_kind" IN ('subject', 'service')) NOT VALID;
35
+ END IF;
36
+ END $$;
37
+
38
+ ALTER TABLE "sessions" VALIDATE CONSTRAINT "sessions_created_by_kind_check";
39
+ ALTER TABLE "session_turns" VALIDATE CONSTRAINT "session_turns_initiator_kind_check";
40
+
41
+ CREATE OR REPLACE FUNCTION opengeni_private.prevent_session_creator_mutation()
42
+ RETURNS trigger
43
+ LANGUAGE plpgsql
44
+ AS $$
45
+ BEGIN
46
+ IF NEW.created_by_kind IS DISTINCT FROM OLD.created_by_kind
47
+ OR NEW.created_by_subject_id IS DISTINCT FROM OLD.created_by_subject_id
48
+ OR NEW.created_by_context IS DISTINCT FROM OLD.created_by_context
49
+ THEN
50
+ RAISE EXCEPTION 'session creator is immutable' USING ERRCODE = '23514';
51
+ END IF;
52
+ RETURN NEW;
53
+ END;
54
+ $$;
55
+
56
+ CREATE OR REPLACE FUNCTION opengeni_private.prevent_session_turn_initiator_mutation()
57
+ RETURNS trigger
58
+ LANGUAGE plpgsql
59
+ AS $$
60
+ BEGIN
61
+ IF NEW.initiator_kind IS DISTINCT FROM OLD.initiator_kind
62
+ OR NEW.initiator_subject_id IS DISTINCT FROM OLD.initiator_subject_id
63
+ OR NEW.initiator_context IS DISTINCT FROM OLD.initiator_context
64
+ THEN
65
+ RAISE EXCEPTION 'session turn initiator is immutable' USING ERRCODE = '23514';
66
+ END IF;
67
+ RETURN NEW;
68
+ END;
69
+ $$;
70
+
71
+ DO $$
72
+ BEGIN
73
+ IF NOT EXISTS (
74
+ SELECT 1 FROM pg_trigger
75
+ WHERE tgname = 'sessions_creator_immutable'
76
+ AND tgrelid = 'sessions'::regclass
77
+ AND NOT tgisinternal
78
+ ) THEN
79
+ CREATE TRIGGER sessions_creator_immutable
80
+ BEFORE UPDATE OF created_by_kind, created_by_subject_id, created_by_context
81
+ ON "sessions"
82
+ FOR EACH ROW
83
+ EXECUTE FUNCTION opengeni_private.prevent_session_creator_mutation();
84
+ END IF;
85
+ IF NOT EXISTS (
86
+ SELECT 1 FROM pg_trigger
87
+ WHERE tgname = 'session_turns_initiator_immutable'
88
+ AND tgrelid = 'session_turns'::regclass
89
+ AND NOT tgisinternal
90
+ ) THEN
91
+ CREATE TRIGGER session_turns_initiator_immutable
92
+ BEFORE UPDATE OF initiator_kind, initiator_subject_id, initiator_context
93
+ ON "session_turns"
94
+ FOR EACH ROW
95
+ EXECUTE FUNCTION opengeni_private.prevent_session_turn_initiator_mutation();
96
+ END IF;
97
+ END $$;