@opengeni/db 0.9.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.
- package/dist/{chunk-4LG5NBTC.js → chunk-P6PKXY5W.js} +93 -1
- package/dist/chunk-P6PKXY5W.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1332 -178
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +406 -32
- package/dist/{schema-CdPGTHlD.d.ts → schema-CqkzrBRS.d.ts} +513 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -1
- package/drizzle/0053_codex_credential_leases.sql +2 -2
- package/drizzle/0057_durable_queue_control.sql +1 -1
- package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
- package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
- package/drizzle/0063_session_control_mega_foundation.sql +1 -1
- package/drizzle/0064_rotation_strategy_sharded_backfill.sql +1 -1
- package/drizzle/0065_codex_subscription_overview.sql +168 -0
- package/drizzle/0065_session_tool_policy.sql +38 -0
- package/drizzle/0067_session_event_payload_bounds.sql +2 -2
- package/drizzle/0068_workspace_control_event_bounds.sql +2 -2
- package/drizzle/0069_session_event_history_backfill.sql +2 -2
- package/drizzle/0074_session_activity_revisions.sql +2 -2
- package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
- package/drizzle/0107_host_export_lineage_contract.sql +381 -0
- package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
- package/package.json +5 -4
- package/src/codex-token-resolver.ts +175 -14
- package/src/connection-token-resolver.ts +143 -120
- package/src/event-payload-sanitizer.ts +32 -2
- package/src/index.ts +1888 -205
- package/src/schema.ts +107 -1
- package/src/session-control.ts +2 -0
- package/src/session-queue-commands.ts +94 -21
- package/dist/chunk-4LG5NBTC.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- deployment-mode: rolling
|
|
2
|
-
--
|
|
2
|
+
-- tracking-36: the strategy picker is gone — rotation-enabled always behaves as
|
|
3
3
|
-- sticky-sharded (worker-side effectiveRotationStrategy normalizes every read).
|
|
4
4
|
-- Backfill stored legacy values so the column tells the truth going forward.
|
|
5
5
|
-- The column itself is KEPT (old-binary rollback safety): an old worker reading
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Codex quota: trustworthy Codex subscription overview, allocator OCC/audit, and
|
|
3
|
+
-- owning-human reset-credit redemption.
|
|
4
|
+
--
|
|
5
|
+
-- Old API binaries do not write connected_by_subject_id and do not fence
|
|
6
|
+
-- disconnect/reconnect against unresolved provider attempts. Drain every old
|
|
7
|
+
-- API replica before applying this migration, then start only this revision;
|
|
8
|
+
-- mixed-version writers would invalidate the owning-human redemption boundary.
|
|
9
|
+
--
|
|
10
|
+
-- tracking-21 exclusively owns allocator_enabled in migration 0053. This migration
|
|
11
|
+
-- only adds the independent OCC/audit fields, provider summary cache, connecting
|
|
12
|
+
-- human attribution, and the durable ambiguity-safe redemption state machine.
|
|
13
|
+
|
|
14
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
15
|
+
ADD COLUMN IF NOT EXISTS "allocator_version" integer NOT NULL DEFAULT 1,
|
|
16
|
+
ADD COLUMN IF NOT EXISTS "allocator_updated_by_subject_id" text,
|
|
17
|
+
ADD COLUMN IF NOT EXISTS "allocator_updated_at" timestamptz,
|
|
18
|
+
ADD COLUMN IF NOT EXISTS "reset_credit_available_count" integer,
|
|
19
|
+
ADD COLUMN IF NOT EXISTS "reset_credits_checked_at" timestamptz,
|
|
20
|
+
ADD COLUMN IF NOT EXISTS "connected_by_subject_id" text;
|
|
21
|
+
|
|
22
|
+
DO $$
|
|
23
|
+
BEGIN
|
|
24
|
+
IF NOT EXISTS (
|
|
25
|
+
SELECT 1 FROM pg_constraint
|
|
26
|
+
WHERE conname = 'codex_subscription_credentials_allocator_version_check'
|
|
27
|
+
AND conrelid = 'codex_subscription_credentials'::regclass
|
|
28
|
+
) THEN
|
|
29
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
30
|
+
ADD CONSTRAINT "codex_subscription_credentials_allocator_version_check"
|
|
31
|
+
CHECK (allocator_version > 0) NOT VALID;
|
|
32
|
+
END IF;
|
|
33
|
+
IF NOT EXISTS (
|
|
34
|
+
SELECT 1 FROM pg_constraint
|
|
35
|
+
WHERE conname = 'codex_subscription_credentials_reset_count_check'
|
|
36
|
+
AND conrelid = 'codex_subscription_credentials'::regclass
|
|
37
|
+
) THEN
|
|
38
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
39
|
+
ADD CONSTRAINT "codex_subscription_credentials_reset_count_check"
|
|
40
|
+
CHECK (reset_credit_available_count IS NULL OR reset_credit_available_count >= 0) NOT VALID;
|
|
41
|
+
END IF;
|
|
42
|
+
IF NOT EXISTS (
|
|
43
|
+
SELECT 1 FROM pg_constraint
|
|
44
|
+
WHERE conname = 'codex_subscription_credentials_human_owner_check'
|
|
45
|
+
AND conrelid = 'codex_subscription_credentials'::regclass
|
|
46
|
+
) THEN
|
|
47
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
48
|
+
ADD CONSTRAINT "codex_subscription_credentials_human_owner_check"
|
|
49
|
+
CHECK (connected_by_subject_id IS NULL OR connected_by_subject_id LIKE 'user:_%') NOT VALID;
|
|
50
|
+
END IF;
|
|
51
|
+
END $$;
|
|
52
|
+
|
|
53
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
54
|
+
VALIDATE CONSTRAINT "codex_subscription_credentials_allocator_version_check";
|
|
55
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
56
|
+
VALIDATE CONSTRAINT "codex_subscription_credentials_reset_count_check";
|
|
57
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
58
|
+
VALIDATE CONSTRAINT "codex_subscription_credentials_human_owner_check";
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS "codex_reset_redemption_attempts" (
|
|
61
|
+
-- Browser-generated logical id. Reusing it is what prevents one ambiguous
|
|
62
|
+
-- click/reload/retry from becoming a second logical redemption.
|
|
63
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
64
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
65
|
+
"workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
|
|
66
|
+
"credential_id" uuid NOT NULL,
|
|
67
|
+
"subject_id" text NOT NULL,
|
|
68
|
+
-- SHA-256 of the Better Auth session id. The raw session id and cookies never
|
|
69
|
+
-- enter product tables, logs, audit metadata, or browser responses.
|
|
70
|
+
"browser_session_hash" text NOT NULL,
|
|
71
|
+
-- Required for an ambiguity retry. It is intentionally absent from audit and
|
|
72
|
+
-- event metadata; provider opaque ids are not observability dimensions.
|
|
73
|
+
"credit_id" text NOT NULL,
|
|
74
|
+
-- Generated once by the server and reused for every upstream retry.
|
|
75
|
+
"upstream_idempotency_key" uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
76
|
+
"status" text NOT NULL DEFAULT 'processing',
|
|
77
|
+
"outcome" text,
|
|
78
|
+
"claim_holder_id" uuid,
|
|
79
|
+
"claim_expires_at" timestamptz,
|
|
80
|
+
"confirmation_expires_at" timestamptz NOT NULL,
|
|
81
|
+
"provider_started_at" timestamptz,
|
|
82
|
+
"completed_at" timestamptz,
|
|
83
|
+
"last_failure_kind" text,
|
|
84
|
+
"retry_count" integer NOT NULL DEFAULT 0,
|
|
85
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
86
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
87
|
+
CONSTRAINT "codex_reset_redemption_workspace_account_fk"
|
|
88
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
89
|
+
REFERENCES "workspaces"("id", "account_id")
|
|
90
|
+
ON DELETE CASCADE,
|
|
91
|
+
-- Deliberately no credential FK: a disconnect must never cascade-delete the
|
|
92
|
+
-- only durable upstream redeem_request_id after provider work may have begun.
|
|
93
|
+
-- Accessors validate and lock the workspace credential before every send;
|
|
94
|
+
-- completed history may outlive a later safe credential disconnect.
|
|
95
|
+
CONSTRAINT "codex_reset_redemption_status_check"
|
|
96
|
+
CHECK (status IN ('processing', 'provider_started', 'completed')),
|
|
97
|
+
CONSTRAINT "codex_reset_redemption_outcome_check"
|
|
98
|
+
CHECK (outcome IS NULL OR outcome IN ('reset', 'nothingToReset', 'noCredit', 'alreadyRedeemed')),
|
|
99
|
+
CONSTRAINT "codex_reset_redemption_completed_check"
|
|
100
|
+
CHECK ((status = 'completed') = (outcome IS NOT NULL AND completed_at IS NOT NULL)),
|
|
101
|
+
CONSTRAINT "codex_reset_redemption_retry_count_check"
|
|
102
|
+
CHECK (retry_count >= 0),
|
|
103
|
+
CONSTRAINT "codex_reset_redemption_human_subject_check"
|
|
104
|
+
CHECK (subject_id LIKE 'user:_%')
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
-- Keep this additive/idempotent for forward recovery from a previously completed
|
|
108
|
+
-- equivalent schema state.
|
|
109
|
+
DO $$
|
|
110
|
+
BEGIN
|
|
111
|
+
IF NOT EXISTS (
|
|
112
|
+
SELECT 1 FROM pg_constraint
|
|
113
|
+
WHERE conname = 'codex_reset_redemption_human_subject_check'
|
|
114
|
+
AND conrelid = 'codex_reset_redemption_attempts'::regclass
|
|
115
|
+
) THEN
|
|
116
|
+
ALTER TABLE "codex_reset_redemption_attempts"
|
|
117
|
+
ADD CONSTRAINT "codex_reset_redemption_human_subject_check"
|
|
118
|
+
CHECK (subject_id LIKE 'user:_%') NOT VALID;
|
|
119
|
+
END IF;
|
|
120
|
+
END $$;
|
|
121
|
+
ALTER TABLE "codex_reset_redemption_attempts"
|
|
122
|
+
VALIDATE CONSTRAINT "codex_reset_redemption_human_subject_check";
|
|
123
|
+
|
|
124
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "codex_reset_redemption_upstream_key_idx"
|
|
125
|
+
ON "codex_reset_redemption_attempts" ("upstream_idempotency_key");
|
|
126
|
+
-- A provider credit can have only one active or successfully consumed logical
|
|
127
|
+
-- attempt. Provider outcomes nothingToReset/noCredit are explicitly non-success
|
|
128
|
+
-- outcomes and must not permanently prevent a later, newly confirmed attempt.
|
|
129
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "codex_reset_redemption_credential_credit_idx"
|
|
130
|
+
ON "codex_reset_redemption_attempts" ("workspace_id", "credential_id", "credit_id")
|
|
131
|
+
WHERE status <> 'completed' OR outcome IN ('reset', 'alreadyRedeemed');
|
|
132
|
+
CREATE INDEX IF NOT EXISTS "codex_reset_redemption_workspace_credential_idx"
|
|
133
|
+
ON "codex_reset_redemption_attempts" ("workspace_id", "credential_id", "created_at" DESC);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS "codex_reset_redemption_claim_expiry_idx"
|
|
135
|
+
ON "codex_reset_redemption_attempts" ("claim_expires_at")
|
|
136
|
+
WHERE status <> 'completed';
|
|
137
|
+
|
|
138
|
+
ALTER TABLE "codex_reset_redemption_attempts" ENABLE ROW LEVEL SECURITY;
|
|
139
|
+
ALTER TABLE "codex_reset_redemption_attempts" FORCE ROW LEVEL SECURITY;
|
|
140
|
+
DO $$
|
|
141
|
+
BEGIN
|
|
142
|
+
IF EXISTS (
|
|
143
|
+
SELECT 1 FROM pg_policies
|
|
144
|
+
WHERE schemaname = current_schema()
|
|
145
|
+
AND tablename = 'codex_reset_redemption_attempts'
|
|
146
|
+
AND policyname = 'workspace_isolation'
|
|
147
|
+
) THEN
|
|
148
|
+
DROP POLICY workspace_isolation ON "codex_reset_redemption_attempts";
|
|
149
|
+
END IF;
|
|
150
|
+
END $$;
|
|
151
|
+
|
|
152
|
+
CREATE POLICY workspace_isolation ON "codex_reset_redemption_attempts"
|
|
153
|
+
USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
|
|
154
|
+
WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
|
|
155
|
+
|
|
156
|
+
DO $$
|
|
157
|
+
DECLARE
|
|
158
|
+
target_schema text := current_schema();
|
|
159
|
+
app_role text := 'opengeni_app';
|
|
160
|
+
BEGIN
|
|
161
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = app_role) THEN
|
|
162
|
+
EXECUTE format(
|
|
163
|
+
'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO %I',
|
|
164
|
+
target_schema,
|
|
165
|
+
app_role
|
|
166
|
+
);
|
|
167
|
+
END IF;
|
|
168
|
+
END $$;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Persist the origin of a session's tool allow-list. NULL is a
|
|
3
|
+
-- deliberate legacy marker for rows created before policy provenance existed.
|
|
4
|
+
ALTER TABLE "sessions"
|
|
5
|
+
ADD COLUMN IF NOT EXISTS "tool_policy" jsonb;
|
|
6
|
+
|
|
7
|
+
ALTER TABLE "sessions"
|
|
8
|
+
DROP CONSTRAINT IF EXISTS "sessions_tool_policy_shape_check";
|
|
9
|
+
|
|
10
|
+
ALTER TABLE "sessions"
|
|
11
|
+
ADD CONSTRAINT "sessions_tool_policy_shape_check"
|
|
12
|
+
CHECK (
|
|
13
|
+
"tool_policy" IS NULL
|
|
14
|
+
OR (
|
|
15
|
+
jsonb_typeof("tool_policy") = 'object'
|
|
16
|
+
AND "tool_policy" ? 'mode'
|
|
17
|
+
AND ("tool_policy" ->> 'mode') IN ('workspace_default', 'explicit', 'inherited', 'legacy')
|
|
18
|
+
AND "tool_policy" ? 'inheritedFromSessionId'
|
|
19
|
+
AND (
|
|
20
|
+
("tool_policy" ->> 'inheritedFromSessionId') IS NULL
|
|
21
|
+
OR ("tool_policy" ->> 'inheritedFromSessionId') ~ '^[0-9a-fA-F-]{36}$'
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
) NOT VALID;
|
|
25
|
+
|
|
26
|
+
-- Follow-up turns also need provenance: an omitted tools key inherits the
|
|
27
|
+
-- session policy, while an explicit array replaces it for that turn (subject
|
|
28
|
+
-- to the session subset fence). Existing turns used merge semantics and their
|
|
29
|
+
-- selected refs are already materialized on sessions.tools, so false preserves
|
|
30
|
+
-- their effective allow-list during the rolling upgrade.
|
|
31
|
+
ALTER TABLE "session_turns"
|
|
32
|
+
ADD COLUMN IF NOT EXISTS "tools_provided" boolean NOT NULL DEFAULT false;
|
|
33
|
+
|
|
34
|
+
-- Human composer drafts need the same provenance across reloads. Without this,
|
|
35
|
+
-- [] cannot distinguish "inherit the session policy" from an intentional
|
|
36
|
+
-- per-turn empty selection.
|
|
37
|
+
ALTER TABLE "composer_drafts"
|
|
38
|
+
ADD COLUMN IF NOT EXISTS "tools_provided" boolean NOT NULL DEFAULT false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- deployment-mode: rolling
|
|
2
|
-
--
|
|
2
|
+
-- tracking-64: session_events is the lossy human/audit projection, not a blob store.
|
|
3
3
|
-- Install the BEFORE trigger before the NOT VALID check so an older pod in the
|
|
4
4
|
-- same rolling deployment is normalized instead of having an oversized write
|
|
5
5
|
-- rejected. Historical oversized rows stay readable and can be defensively
|
|
@@ -206,4 +206,4 @@ ALTER TABLE session_events
|
|
|
206
206
|
NOT VALID,
|
|
207
207
|
ADD CONSTRAINT session_events_duplicate_reason_bytes_check
|
|
208
208
|
CHECK (duplicate_reason IS NULL OR octet_length(duplicate_reason) <= 4096)
|
|
209
|
-
NOT VALID;
|
|
209
|
+
NOT VALID;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- deployment-mode: rolling
|
|
2
|
-
--
|
|
2
|
+
-- tracking-64: workspace-control rows are compact invalidations, not evidence blobs.
|
|
3
3
|
-- Preserve exact source byte counts while bounding the two free-form fields so
|
|
4
4
|
-- old binaries and direct writers cannot create a durable replay poison row.
|
|
5
5
|
|
|
@@ -131,4 +131,4 @@ ALTER TABLE workspace_control_events
|
|
|
131
131
|
VALIDATE CONSTRAINT workspace_control_events_reason_bytes_check,
|
|
132
132
|
VALIDATE CONSTRAINT workspace_control_events_actor_bytes_check,
|
|
133
133
|
VALIDATE CONSTRAINT workspace_control_events_original_bytes_check,
|
|
134
|
-
VALIDATE CONSTRAINT workspace_control_events_actor_original_bytes_check;
|
|
134
|
+
VALIDATE CONSTRAINT workspace_control_events_actor_original_bytes_check;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- deployment-mode: rolling
|
|
2
|
-
--
|
|
2
|
+
-- tracking-64: finish the 0067 expand/contract sequence. The trigger has bounded
|
|
3
3
|
-- every new/updated row since 0067; now rewrite only legacy violations through
|
|
4
4
|
-- that trigger and validate the durable storage invariant.
|
|
5
5
|
|
|
@@ -29,4 +29,4 @@ ALTER TABLE session_events
|
|
|
29
29
|
VALIDATE CONSTRAINT session_events_client_event_id_bytes_check,
|
|
30
30
|
VALIDATE CONSTRAINT session_events_producer_id_bytes_check,
|
|
31
31
|
VALIDATE CONSTRAINT session_events_turn_association_bytes_check,
|
|
32
|
-
VALIDATE CONSTRAINT session_events_duplicate_reason_bytes_check;
|
|
32
|
+
VALIDATE CONSTRAINT session_events_duplicate_reason_bytes_check;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- deployment-mode: rolling
|
|
2
|
-
--
|
|
2
|
+
-- tracking-65: transactional workspace revisions make updated-order discovery and
|
|
3
3
|
-- its next incremental scan one gap-free handoff independent of application
|
|
4
4
|
-- clocks. Existing sessions remain revision zero until their next activity.
|
|
5
5
|
|
|
@@ -69,4 +69,4 @@ BEGIN
|
|
|
69
69
|
END $grants$;
|
|
70
70
|
|
|
71
71
|
RESET statement_timeout;
|
|
72
|
-
RESET lock_timeout;
|
|
72
|
+
RESET lock_timeout;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Workers are drained before this migration so no executable attempt can cross
|
|
3
|
+
-- the boundary without an immutable approval-policy snapshot. Closed attempts
|
|
4
|
+
-- are historical evidence only and receive an inert empty snapshot.
|
|
5
|
+
SET lock_timeout = '5s';
|
|
6
|
+
SET statement_timeout = '5min';
|
|
7
|
+
|
|
8
|
+
DO $$
|
|
9
|
+
BEGIN
|
|
10
|
+
IF EXISTS (
|
|
11
|
+
SELECT 1
|
|
12
|
+
FROM "session_turn_attempts"
|
|
13
|
+
WHERE "state" <> 'closed'
|
|
14
|
+
LIMIT 1
|
|
15
|
+
) THEN
|
|
16
|
+
RAISE EXCEPTION
|
|
17
|
+
'live session turn attempts must be drained before the MCP approval-policy cutover';
|
|
18
|
+
END IF;
|
|
19
|
+
END
|
|
20
|
+
$$;
|
|
21
|
+
|
|
22
|
+
ALTER TABLE "session_turn_attempts"
|
|
23
|
+
ADD COLUMN "mcp_approval_policies" jsonb NOT NULL DEFAULT '{}'::jsonb;
|
|
24
|
+
|
|
25
|
+
-- PostgreSQL installs a constant default without rewriting the historical
|
|
26
|
+
-- table. Remove the default immediately: every new attempt must provide its
|
|
27
|
+
-- claim-time snapshot explicitly.
|
|
28
|
+
ALTER TABLE "session_turn_attempts"
|
|
29
|
+
ALTER COLUMN "mcp_approval_policies" DROP DEFAULT;
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Forward-only repair for published 0097/0103/0104 histories. Immutable root
|
|
3
|
+
-- capture already exists after 0103; this migration linearizes first-consumer
|
|
4
|
+
-- enablement with deferred producers and validates the session/root invariant.
|
|
5
|
+
-- It never derives or rewrites historical lineage. Rows that may have been
|
|
6
|
+
-- populated by the old 0104 backfill require explicit evidence-backed
|
|
7
|
+
-- disposition before retry.
|
|
8
|
+
|
|
9
|
+
SET LOCAL lock_timeout = '5s';
|
|
10
|
+
SET LOCAL statement_timeout = '5min';
|
|
11
|
+
|
|
12
|
+
-- Replacing these SECURITY DEFINER functions preserves their identity, owner,
|
|
13
|
+
-- and existing exporter ACL while making the config-row lock the commit-order
|
|
14
|
+
-- boundary between first-consumer registration and deferred source capture.
|
|
15
|
+
DO $migration$
|
|
16
|
+
DECLARE target_schema text := current_schema();
|
|
17
|
+
BEGIN
|
|
18
|
+
EXECUTE format($create$
|
|
19
|
+
CREATE OR REPLACE FUNCTION opengeni_private.enqueue_host_session_event_export()
|
|
20
|
+
RETURNS trigger
|
|
21
|
+
LANGUAGE plpgsql
|
|
22
|
+
SECURITY DEFINER
|
|
23
|
+
SET search_path = pg_catalog
|
|
24
|
+
AS $function$
|
|
25
|
+
DECLARE
|
|
26
|
+
v_enabled boolean;
|
|
27
|
+
v_initiator jsonb;
|
|
28
|
+
v_context jsonb := '{}'::jsonb;
|
|
29
|
+
v_origin text;
|
|
30
|
+
v_payload_bytes integer;
|
|
31
|
+
BEGIN
|
|
32
|
+
IF NEW.type IN (
|
|
33
|
+
'agent.message.delta', 'agent.reasoning.delta',
|
|
34
|
+
'sandbox.command.output.delta', 'terminal.pty.output.delta'
|
|
35
|
+
) THEN
|
|
36
|
+
RETURN NEW;
|
|
37
|
+
END IF;
|
|
38
|
+
|
|
39
|
+
SELECT c.session_events_enabled INTO v_enabled
|
|
40
|
+
FROM %1$I.host_export_config c WHERE c.id = 1
|
|
41
|
+
FOR SHARE;
|
|
42
|
+
IF coalesce(v_enabled, false) = false THEN
|
|
43
|
+
RETURN NEW;
|
|
44
|
+
END IF;
|
|
45
|
+
|
|
46
|
+
IF NEW.turn_id IS NOT NULL THEN
|
|
47
|
+
SELECT
|
|
48
|
+
CASE WHEN octet_length(t.initiator_subject_id) <= 1024 THEN
|
|
49
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
50
|
+
'kind', t.initiator_kind,
|
|
51
|
+
'subjectId', t.initiator_subject_id,
|
|
52
|
+
'label', CASE
|
|
53
|
+
WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
|
|
54
|
+
THEN left(t.initiator_context ->> 'label', 256)
|
|
55
|
+
ELSE NULL
|
|
56
|
+
END
|
|
57
|
+
))
|
|
58
|
+
ELSE NULL END,
|
|
59
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
60
|
+
'label', CASE
|
|
61
|
+
WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
|
|
62
|
+
THEN left(t.initiator_context ->> 'label', 256)
|
|
63
|
+
ELSE NULL
|
|
64
|
+
END,
|
|
65
|
+
'backfill', CASE
|
|
66
|
+
WHEN jsonb_typeof(t.initiator_context -> 'backfill') = 'boolean'
|
|
67
|
+
THEN t.initiator_context -> 'backfill'
|
|
68
|
+
ELSE NULL
|
|
69
|
+
END,
|
|
70
|
+
'attributionOmitted', CASE
|
|
71
|
+
WHEN octet_length(t.initiator_subject_id) > 1024 THEN 'subject_id_too_large'
|
|
72
|
+
ELSE NULL
|
|
73
|
+
END
|
|
74
|
+
)),
|
|
75
|
+
t.source
|
|
76
|
+
INTO v_initiator, v_context, v_origin
|
|
77
|
+
FROM %1$I.session_turns t
|
|
78
|
+
WHERE t.workspace_id = NEW.workspace_id AND t.id = NEW.turn_id;
|
|
79
|
+
ELSIF NEW.type = 'session.created' THEN
|
|
80
|
+
SELECT
|
|
81
|
+
CASE WHEN octet_length(s.created_by_subject_id) <= 1024 THEN
|
|
82
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
83
|
+
'kind', s.created_by_kind,
|
|
84
|
+
'subjectId', s.created_by_subject_id,
|
|
85
|
+
'label', CASE
|
|
86
|
+
WHEN jsonb_typeof(s.created_by_context -> 'label') = 'string'
|
|
87
|
+
THEN left(s.created_by_context ->> 'label', 256)
|
|
88
|
+
ELSE NULL
|
|
89
|
+
END
|
|
90
|
+
))
|
|
91
|
+
ELSE NULL END,
|
|
92
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
93
|
+
'label', CASE
|
|
94
|
+
WHEN jsonb_typeof(s.created_by_context -> 'label') = 'string'
|
|
95
|
+
THEN left(s.created_by_context ->> 'label', 256)
|
|
96
|
+
ELSE NULL
|
|
97
|
+
END,
|
|
98
|
+
'backfill', CASE
|
|
99
|
+
WHEN jsonb_typeof(s.created_by_context -> 'backfill') = 'boolean'
|
|
100
|
+
THEN s.created_by_context -> 'backfill'
|
|
101
|
+
ELSE NULL
|
|
102
|
+
END,
|
|
103
|
+
'attributionOmitted', CASE
|
|
104
|
+
WHEN octet_length(s.created_by_subject_id) > 1024 THEN 'subject_id_too_large'
|
|
105
|
+
ELSE NULL
|
|
106
|
+
END
|
|
107
|
+
)),
|
|
108
|
+
NULL
|
|
109
|
+
INTO v_initiator, v_context, v_origin
|
|
110
|
+
FROM %1$I.sessions s
|
|
111
|
+
WHERE s.workspace_id = NEW.workspace_id AND s.id = NEW.session_id;
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
v_payload_bytes := octet_length(NEW.payload::text)
|
|
115
|
+
+ octet_length(NEW.type)
|
|
116
|
+
+ coalesce(octet_length(NEW.client_event_id), 0)
|
|
117
|
+
+ coalesce(octet_length(NEW.turn_association), 0)
|
|
118
|
+
+ coalesce(octet_length(NEW.duplicate_reason), 0)
|
|
119
|
+
+ octet_length(coalesce(v_initiator, 'null'::jsonb)::text)
|
|
120
|
+
+ octet_length(v_context::text)
|
|
121
|
+
+ 768;
|
|
122
|
+
INSERT INTO %1$I.host_export_outbox (
|
|
123
|
+
export_kind, source_id, account_id, workspace_id, session_id,
|
|
124
|
+
turn_id, turn_generation, turn_attempt_id, session_sequence,
|
|
125
|
+
client_event_id, turn_association, duplicate_of_event_id, duplicate_reason,
|
|
126
|
+
event_type, idempotency_key, initiator, initiator_context, origin,
|
|
127
|
+
payload, envelope_bytes, occurred_at, source_recorded_at, enqueued_at
|
|
128
|
+
) VALUES (
|
|
129
|
+
'session_event', NEW.id, NEW.account_id, NEW.workspace_id, NEW.session_id,
|
|
130
|
+
NEW.turn_id, NEW.turn_generation, NEW.turn_attempt_id, NEW.sequence,
|
|
131
|
+
NEW.client_event_id, NEW.turn_association, NEW.duplicate_of_event_id,
|
|
132
|
+
NEW.duplicate_reason,
|
|
133
|
+
NEW.type, 'session_event:' || NEW.id::text, v_initiator,
|
|
134
|
+
coalesce(v_context, '{}'::jsonb), v_origin, NEW.payload,
|
|
135
|
+
greatest(1, v_payload_bytes), NEW.occurred_at,
|
|
136
|
+
NEW.created_at, clock_timestamp()
|
|
137
|
+
)
|
|
138
|
+
ON CONFLICT (export_kind, source_id) DO NOTHING;
|
|
139
|
+
RETURN NEW;
|
|
140
|
+
END $function$;
|
|
141
|
+
$create$, target_schema);
|
|
142
|
+
|
|
143
|
+
EXECUTE format($create$
|
|
144
|
+
CREATE OR REPLACE FUNCTION opengeni_private.enqueue_host_usage_event_export()
|
|
145
|
+
RETURNS trigger
|
|
146
|
+
LANGUAGE plpgsql
|
|
147
|
+
SECURITY DEFINER
|
|
148
|
+
SET search_path = pg_catalog
|
|
149
|
+
AS $function$
|
|
150
|
+
DECLARE
|
|
151
|
+
v_enabled boolean;
|
|
152
|
+
v_initiator jsonb := CASE
|
|
153
|
+
WHEN NEW.initiator_kind IS NOT NULL THEN jsonb_strip_nulls(jsonb_build_object(
|
|
154
|
+
'kind', NEW.initiator_kind,
|
|
155
|
+
'subjectId', NEW.initiator_subject_id,
|
|
156
|
+
'label', CASE
|
|
157
|
+
WHEN jsonb_typeof(NEW.initiator_context -> 'label') = 'string'
|
|
158
|
+
THEN left(NEW.initiator_context ->> 'label', 256)
|
|
159
|
+
ELSE NULL
|
|
160
|
+
END
|
|
161
|
+
))
|
|
162
|
+
ELSE NULL
|
|
163
|
+
END;
|
|
164
|
+
v_context jsonb := jsonb_strip_nulls(jsonb_build_object(
|
|
165
|
+
'label', CASE
|
|
166
|
+
WHEN jsonb_typeof(NEW.initiator_context -> 'label') = 'string'
|
|
167
|
+
THEN left(NEW.initiator_context ->> 'label', 256)
|
|
168
|
+
ELSE NULL
|
|
169
|
+
END,
|
|
170
|
+
'backfill', CASE
|
|
171
|
+
WHEN jsonb_typeof(NEW.initiator_context -> 'backfill') = 'boolean'
|
|
172
|
+
THEN NEW.initiator_context -> 'backfill'
|
|
173
|
+
ELSE NULL
|
|
174
|
+
END
|
|
175
|
+
));
|
|
176
|
+
v_origin text := NEW.origin;
|
|
177
|
+
v_payload jsonb;
|
|
178
|
+
v_payload_bytes integer;
|
|
179
|
+
BEGIN
|
|
180
|
+
SELECT c.usage_events_enabled INTO v_enabled
|
|
181
|
+
FROM %1$I.host_export_config c WHERE c.id = 1
|
|
182
|
+
FOR SHARE;
|
|
183
|
+
IF coalesce(v_enabled, false) = false THEN
|
|
184
|
+
RETURN NEW;
|
|
185
|
+
END IF;
|
|
186
|
+
|
|
187
|
+
-- Export-specific bounds apply only while the optional host stream is
|
|
188
|
+
-- enabled. Standalone/custom usage writers retain their historical
|
|
189
|
+
-- behavior; an embedded writer that cannot be represented fails its
|
|
190
|
+
-- source transaction visibly instead of committing a poison outbox row.
|
|
191
|
+
IF octet_length(NEW.event_type) NOT BETWEEN 1 AND 256
|
|
192
|
+
OR octet_length(NEW.unit) NOT BETWEEN 1 AND 128
|
|
193
|
+
OR (NEW.subject_id IS NOT NULL AND octet_length(NEW.subject_id) > 1024)
|
|
194
|
+
OR (NEW.source_resource_type IS NOT NULL
|
|
195
|
+
AND octet_length(NEW.source_resource_type) > 256)
|
|
196
|
+
OR (NEW.source_resource_id IS NOT NULL
|
|
197
|
+
AND octet_length(NEW.source_resource_id) > 2048)
|
|
198
|
+
OR octet_length(NEW.idempotency_key) NOT BETWEEN 1 AND 2048
|
|
199
|
+
OR (NEW.billing_provider_event_id IS NOT NULL
|
|
200
|
+
AND octet_length(NEW.billing_provider_event_id) > 2048) THEN
|
|
201
|
+
RAISE EXCEPTION 'usage event exceeds the enabled host-export wire bounds'
|
|
202
|
+
USING ERRCODE = '22001';
|
|
203
|
+
END IF;
|
|
204
|
+
|
|
205
|
+
IF NEW.turn_id IS NOT NULL THEN
|
|
206
|
+
SELECT
|
|
207
|
+
CASE WHEN octet_length(t.initiator_subject_id) <= 1024 THEN
|
|
208
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
209
|
+
'kind', t.initiator_kind,
|
|
210
|
+
'subjectId', t.initiator_subject_id,
|
|
211
|
+
'label', CASE
|
|
212
|
+
WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
|
|
213
|
+
THEN left(t.initiator_context ->> 'label', 256)
|
|
214
|
+
ELSE NULL
|
|
215
|
+
END
|
|
216
|
+
))
|
|
217
|
+
ELSE NULL END,
|
|
218
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
219
|
+
'label', CASE
|
|
220
|
+
WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
|
|
221
|
+
THEN left(t.initiator_context ->> 'label', 256)
|
|
222
|
+
ELSE NULL
|
|
223
|
+
END,
|
|
224
|
+
'backfill', CASE
|
|
225
|
+
WHEN jsonb_typeof(t.initiator_context -> 'backfill') = 'boolean'
|
|
226
|
+
THEN t.initiator_context -> 'backfill'
|
|
227
|
+
ELSE NULL
|
|
228
|
+
END,
|
|
229
|
+
'attributionOmitted', CASE
|
|
230
|
+
WHEN octet_length(t.initiator_subject_id) > 1024 THEN 'subject_id_too_large'
|
|
231
|
+
ELSE NULL
|
|
232
|
+
END
|
|
233
|
+
)),
|
|
234
|
+
t.source
|
|
235
|
+
INTO v_initiator, v_context, v_origin
|
|
236
|
+
FROM %1$I.session_turns t
|
|
237
|
+
WHERE t.workspace_id = NEW.workspace_id AND t.id = NEW.turn_id;
|
|
238
|
+
ELSIF v_initiator IS NULL AND NEW.subject_id IS NOT NULL
|
|
239
|
+
AND octet_length(NEW.subject_id) <= 1024 THEN
|
|
240
|
+
v_initiator := jsonb_build_object('kind', 'subject', 'subjectId', NEW.subject_id);
|
|
241
|
+
END IF;
|
|
242
|
+
|
|
243
|
+
v_payload := jsonb_build_object(
|
|
244
|
+
'id', NEW.id,
|
|
245
|
+
'workspaceId', NEW.workspace_id,
|
|
246
|
+
'accountId', NEW.account_id,
|
|
247
|
+
'subjectId', NEW.subject_id,
|
|
248
|
+
'eventType', NEW.event_type,
|
|
249
|
+
'quantity', NEW.quantity,
|
|
250
|
+
'unit', NEW.unit,
|
|
251
|
+
'sourceResourceType', NEW.source_resource_type,
|
|
252
|
+
'sourceResourceId', NEW.source_resource_id,
|
|
253
|
+
'idempotencyKey', NEW.idempotency_key,
|
|
254
|
+
'occurredAt', NEW.occurred_at,
|
|
255
|
+
'recordedAt', NEW.recorded_at,
|
|
256
|
+
'exportedToBillingAt', NEW.exported_to_billing_at,
|
|
257
|
+
'billingProviderEventId', NEW.billing_provider_event_id
|
|
258
|
+
);
|
|
259
|
+
v_payload_bytes := octet_length(v_payload::text)
|
|
260
|
+
+ octet_length(coalesce(v_initiator, 'null'::jsonb)::text)
|
|
261
|
+
+ octet_length(v_context::text)
|
|
262
|
+
+ 768;
|
|
263
|
+
INSERT INTO %1$I.host_export_outbox (
|
|
264
|
+
export_kind, source_id, account_id, workspace_id, session_id,
|
|
265
|
+
turn_id, turn_attempt_id, event_type, idempotency_key, initiator,
|
|
266
|
+
initiator_context, origin, payload, envelope_bytes, occurred_at,
|
|
267
|
+
source_recorded_at, enqueued_at
|
|
268
|
+
) VALUES (
|
|
269
|
+
'usage_event', NEW.id, NEW.account_id, NEW.workspace_id, NEW.session_id,
|
|
270
|
+
NEW.turn_id, NEW.turn_attempt_id, NEW.event_type, NEW.idempotency_key,
|
|
271
|
+
v_initiator, coalesce(v_context, '{}'::jsonb), v_origin, v_payload,
|
|
272
|
+
greatest(1, v_payload_bytes), NEW.occurred_at,
|
|
273
|
+
NEW.recorded_at, clock_timestamp()
|
|
274
|
+
)
|
|
275
|
+
ON CONFLICT (export_kind, source_id) DO NOTHING;
|
|
276
|
+
RETURN NEW;
|
|
277
|
+
END $function$;
|
|
278
|
+
$create$, target_schema);
|
|
279
|
+
END $migration$;
|
|
280
|
+
|
|
281
|
+
REVOKE ALL ON FUNCTION
|
|
282
|
+
opengeni_private.enqueue_host_session_event_export() FROM PUBLIC;
|
|
283
|
+
REVOKE ALL ON FUNCTION
|
|
284
|
+
opengeni_private.enqueue_host_usage_event_export() FROM PUBLIC;
|
|
285
|
+
|
|
286
|
+
-- The old 0104 backfill resolved lineage from then-current session ancestry.
|
|
287
|
+
-- A session-scoped row enqueued before 0103's durable ledger boundary therefore
|
|
288
|
+
-- lacks immutable-capture provenance even when its root is non-null. Reject the
|
|
289
|
+
-- candidate without touching data. The single-row probe is bounded by the
|
|
290
|
+
-- migration statement timeout; operators must disposition suspect history
|
|
291
|
+
-- separately and rerun the same migration.
|
|
292
|
+
DO $migration$
|
|
293
|
+
DECLARE
|
|
294
|
+
capture_applied_at timestamptz;
|
|
295
|
+
capture_ledger_rows integer;
|
|
296
|
+
suspect_source_id uuid;
|
|
297
|
+
BEGIN
|
|
298
|
+
SELECT min(m.applied_at), count(*)::integer
|
|
299
|
+
INTO capture_applied_at, capture_ledger_rows
|
|
300
|
+
FROM "schema_migrations" m
|
|
301
|
+
WHERE m.name = '0103_host_export_root_session.sql';
|
|
302
|
+
|
|
303
|
+
IF capture_ledger_rows <> 1 OR capture_applied_at IS NULL THEN
|
|
304
|
+
RAISE EXCEPTION '0107 requires the exact 0103 schema-migration ledger boundary'
|
|
305
|
+
USING ERRCODE = '55000',
|
|
306
|
+
HINT = 'Apply migrations through immutable 0104 with the canonical runner before retrying 0107.';
|
|
307
|
+
END IF;
|
|
308
|
+
|
|
309
|
+
SELECT o.source_id
|
|
310
|
+
INTO suspect_source_id
|
|
311
|
+
FROM "host_export_outbox" o
|
|
312
|
+
WHERE o.session_id IS NOT NULL
|
|
313
|
+
AND (
|
|
314
|
+
o.root_session_id IS NULL
|
|
315
|
+
OR o.enqueued_at < capture_applied_at
|
|
316
|
+
)
|
|
317
|
+
ORDER BY o.enqueued_at, o.source_id
|
|
318
|
+
LIMIT 1;
|
|
319
|
+
|
|
320
|
+
IF suspect_source_id IS NOT NULL THEN
|
|
321
|
+
RAISE EXCEPTION '0107 found host-export lineage without immutable-capture provenance'
|
|
322
|
+
USING ERRCODE = '23514',
|
|
323
|
+
HINT = 'Do not derive lineage from current sessions; use an evidence-backed maintenance disposition, then retry 0107.';
|
|
324
|
+
END IF;
|
|
325
|
+
END $migration$;
|
|
326
|
+
|
|
327
|
+
DO $migration$
|
|
328
|
+
DECLARE
|
|
329
|
+
existing_expression text;
|
|
330
|
+
normalized_expression text;
|
|
331
|
+
BEGIN
|
|
332
|
+
SELECT pg_get_expr(c.conbin, c.conrelid, true)
|
|
333
|
+
INTO existing_expression
|
|
334
|
+
FROM pg_catalog.pg_constraint c
|
|
335
|
+
WHERE c.conname = 'host_export_outbox_root_session_check'
|
|
336
|
+
AND c.conrelid = 'host_export_outbox'::regclass
|
|
337
|
+
AND c.contype = 'c'
|
|
338
|
+
AND NOT c.connoinherit;
|
|
339
|
+
|
|
340
|
+
IF existing_expression IS NULL THEN
|
|
341
|
+
IF EXISTS (
|
|
342
|
+
SELECT 1
|
|
343
|
+
FROM pg_catalog.pg_constraint c
|
|
344
|
+
WHERE c.conname = 'host_export_outbox_root_session_check'
|
|
345
|
+
AND c.conrelid = 'host_export_outbox'::regclass
|
|
346
|
+
) THEN
|
|
347
|
+
RAISE EXCEPTION 'host_export_outbox_root_session_check has an incompatible constraint type'
|
|
348
|
+
USING ERRCODE = '55000';
|
|
349
|
+
END IF;
|
|
350
|
+
|
|
351
|
+
ALTER TABLE "host_export_outbox"
|
|
352
|
+
ADD CONSTRAINT "host_export_outbox_root_session_check"
|
|
353
|
+
CHECK ("session_id" IS NULL OR "root_session_id" IS NOT NULL)
|
|
354
|
+
NOT VALID;
|
|
355
|
+
ELSE
|
|
356
|
+
normalized_expression := lower(regexp_replace(
|
|
357
|
+
existing_expression,
|
|
358
|
+
'["()[:space:]]',
|
|
359
|
+
'',
|
|
360
|
+
'g'
|
|
361
|
+
));
|
|
362
|
+
IF normalized_expression <> 'session_idisnullorroot_session_idisnotnull' THEN
|
|
363
|
+
RAISE EXCEPTION 'host_export_outbox_root_session_check has an incompatible expression'
|
|
364
|
+
USING ERRCODE = '55000';
|
|
365
|
+
END IF;
|
|
366
|
+
END IF;
|
|
367
|
+
END $migration$;
|
|
368
|
+
|
|
369
|
+
-- VALIDATE takes SHARE UPDATE EXCLUSIVE, which permits ordinary reads/writes.
|
|
370
|
+
-- Both lock acquisition and the read-only table scan are source-bounded above;
|
|
371
|
+
-- timeout is a retryable operator disposition, never permission to rewrite data.
|
|
372
|
+
DO $migration$
|
|
373
|
+
BEGIN
|
|
374
|
+
ALTER TABLE "host_export_outbox"
|
|
375
|
+
VALIDATE CONSTRAINT "host_export_outbox_root_session_check";
|
|
376
|
+
EXCEPTION
|
|
377
|
+
WHEN check_violation THEN
|
|
378
|
+
RAISE EXCEPTION '0107 cannot validate host-export lineage without historical provenance'
|
|
379
|
+
USING ERRCODE = '23514',
|
|
380
|
+
HINT = 'Do not derive lineage from current sessions; use an evidence-backed maintenance disposition, then retry 0107.';
|
|
381
|
+
END $migration$;
|