@opengeni/db 0.27.11 → 0.28.9
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-JYQPJ6Y5.js → chunk-P6CUHXQZ.js} +2967 -2272
- package/dist/chunk-P6CUHXQZ.js.map +1 -0
- package/dist/{chunk-RPHPNVWW.js → chunk-VHBFHMPC.js} +15 -1
- package/dist/chunk-VHBFHMPC.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/index.d.ts +272 -26
- package/dist/index.js +7798 -5008
- package/dist/index.js.map +1 -1
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +1420 -271
- package/dist/schema.js +25 -1
- package/dist/session-realtime-terminal.d.ts +7 -0
- package/dist/transcription-recordings-schema.d.ts +1264 -0
- package/dist/transcription-recordings.d.ts +228 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0170_resumable_transcription_recordings.sql +440 -0
- package/drizzle/0175_resumable_transcription_provider_deadline.sql +24 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/package.json +4 -4
- package/src/database.ts +7 -1
- package/src/environment-crypto.ts +22 -9
- package/src/index.ts +4368 -1678
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/persistence-errors.ts +29 -34
- package/src/runtime-posture.ts +14 -0
- package/src/schema.ts +264 -42
- package/src/session-control.ts +125 -76
- package/src/session-queue-commands.ts +325 -234
- package/src/session-realtime-context.ts +18 -5
- package/src/session-realtime-ledger.ts +29 -7
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +3 -2
- package/src/session-tool-call-settlement.ts +29 -15
- package/src/transcription-recordings-schema.ts +253 -0
- package/src/transcription-recordings.ts +1538 -0
- package/dist/chunk-JYQPJ6Y5.js.map +0 -1
- package/dist/chunk-RPHPNVWW.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Existing managed workspace administrators receive explicit new permissions.
|
|
3
|
+
-- API keys and frozen session grants are deliberately untouched: an old
|
|
4
|
+
-- workspace:admin wildcard must never acquire plaintext secret reads.
|
|
5
|
+
UPDATE "workspace_memberships"
|
|
6
|
+
SET "permissions" =
|
|
7
|
+
"permissions"
|
|
8
|
+
|| CASE WHEN "permissions" ? 'variable-sets:list' THEN '[]'::jsonb ELSE '["variable-sets:list"]'::jsonb END
|
|
9
|
+
|| CASE WHEN "permissions" ? 'variable-sets:read' THEN '[]'::jsonb ELSE '["variable-sets:read"]'::jsonb END
|
|
10
|
+
|| CASE WHEN "permissions" ? 'variable-sets:write' THEN '[]'::jsonb ELSE '["variable-sets:write"]'::jsonb END
|
|
11
|
+
|| CASE WHEN "permissions" ? 'secrets:list' THEN '[]'::jsonb ELSE '["secrets:list"]'::jsonb END
|
|
12
|
+
|| CASE WHEN "permissions" ? 'secrets:read' THEN '[]'::jsonb ELSE '["secrets:read"]'::jsonb END
|
|
13
|
+
|| CASE WHEN "permissions" ? 'secrets:write' THEN '[]'::jsonb ELSE '["secrets:write"]'::jsonb END
|
|
14
|
+
WHERE "permissions" ? 'workspace:admin';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- A private human-DM shortcut is initially keyed to a source conversation the
|
|
3
|
+
-- workspace bot cannot join. Delivery becomes eligible only after the durable
|
|
4
|
+
-- acknowledgement has rekeyed the route to the invoking user's bot-DM thread.
|
|
5
|
+
|
|
6
|
+
SET lock_timeout = '5s';
|
|
7
|
+
SET statement_timeout = '10min';
|
|
8
|
+
|
|
9
|
+
DO $migration$
|
|
10
|
+
DECLARE data_schema text := current_schema();
|
|
11
|
+
BEGIN
|
|
12
|
+
EXECUTE format($ddl$
|
|
13
|
+
CREATE OR REPLACE FUNCTION opengeni_private.claim_slack_interaction_delivery(
|
|
14
|
+
p_holder uuid,
|
|
15
|
+
p_lease_ms integer
|
|
16
|
+
)
|
|
17
|
+
RETURNS SETOF %1$I.slack_interactions
|
|
18
|
+
LANGUAGE plpgsql
|
|
19
|
+
SECURITY DEFINER
|
|
20
|
+
SET search_path = pg_catalog
|
|
21
|
+
AS $function$
|
|
22
|
+
BEGIN
|
|
23
|
+
IF p_lease_ms < 1000 OR p_lease_ms > 300000 THEN
|
|
24
|
+
RAISE EXCEPTION 'invalid Slack delivery claim lease';
|
|
25
|
+
END IF;
|
|
26
|
+
RETURN QUERY
|
|
27
|
+
WITH candidate AS (
|
|
28
|
+
SELECT I.id
|
|
29
|
+
FROM %1$I.slack_interactions I
|
|
30
|
+
WHERE I.session_id IS NOT NULL
|
|
31
|
+
AND I.terminal_delivery_state = 'open'
|
|
32
|
+
AND (I.delivery_retry_at IS NULL OR I.delivery_retry_at <= now())
|
|
33
|
+
AND (I.delivery_claim_holder_id IS NULL OR I.delivery_claim_expires_at <= now())
|
|
34
|
+
AND NOT (
|
|
35
|
+
I.visibility = 'private'
|
|
36
|
+
AND I.triggering_provider_event_id LIKE 'shortcut:%%'
|
|
37
|
+
AND I.ack_slack_message_ts IS NULL
|
|
38
|
+
)
|
|
39
|
+
AND EXISTS (
|
|
40
|
+
SELECT 1
|
|
41
|
+
FROM %1$I.session_events E
|
|
42
|
+
WHERE E.workspace_id = I.workspace_id
|
|
43
|
+
AND E.session_id = I.session_id
|
|
44
|
+
AND E.sequence > I.last_delivered_session_event_sequence
|
|
45
|
+
AND E.type IN (
|
|
46
|
+
'agent.message.completed',
|
|
47
|
+
'session.humanInput.requested',
|
|
48
|
+
'turn.completed',
|
|
49
|
+
'turn.failed',
|
|
50
|
+
'turn.cancelled',
|
|
51
|
+
'session.status.changed'
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
ORDER BY I.delivery_retry_at NULLS FIRST, I.updated_at, I.id
|
|
55
|
+
FOR UPDATE SKIP LOCKED
|
|
56
|
+
LIMIT 1
|
|
57
|
+
)
|
|
58
|
+
UPDATE %1$I.slack_interactions I
|
|
59
|
+
SET delivery_claim_holder_id = p_holder,
|
|
60
|
+
delivery_claim_expires_at = now() + make_interval(secs => p_lease_ms::double precision / 1000),
|
|
61
|
+
delivery_attempt_count = I.delivery_attempt_count + 1,
|
|
62
|
+
updated_at = now()
|
|
63
|
+
FROM candidate C
|
|
64
|
+
WHERE I.id = C.id
|
|
65
|
+
RETURNING I.*;
|
|
66
|
+
END
|
|
67
|
+
$function$
|
|
68
|
+
$ddl$, data_schema);
|
|
69
|
+
END
|
|
70
|
+
$migration$;
|
|
71
|
+
|
|
72
|
+
REVOKE ALL ON FUNCTION opengeni_private.claim_slack_interaction_delivery(uuid, integer)
|
|
73
|
+
FROM PUBLIC;
|
|
74
|
+
|
|
75
|
+
DO $grants$
|
|
76
|
+
BEGIN
|
|
77
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
78
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.claim_slack_interaction_delivery(uuid, integer)
|
|
79
|
+
TO opengeni_app;
|
|
80
|
+
END IF;
|
|
81
|
+
END
|
|
82
|
+
$grants$;
|
|
83
|
+
|
|
84
|
+
RESET statement_timeout;
|
|
85
|
+
RESET lock_timeout;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Preserve retained screenshot cleanup/accounting authority across parent
|
|
3
|
+
-- deletion, make quota release explicit and idempotent, and fence provider
|
|
4
|
+
-- deletion with one durable maintenance claim.
|
|
5
|
+
|
|
6
|
+
ALTER TABLE "retained_screenshot_artifacts"
|
|
7
|
+
ALTER COLUMN "session_id" DROP NOT NULL,
|
|
8
|
+
ALTER COLUMN "turn_id" DROP NOT NULL,
|
|
9
|
+
ALTER COLUMN "attempt_id" DROP NOT NULL,
|
|
10
|
+
ADD COLUMN "quota_state" text NOT NULL DEFAULT 'reserved',
|
|
11
|
+
ADD COLUMN "maintenance_claim_id" uuid,
|
|
12
|
+
ADD COLUMN "maintenance_claimed_at" timestamptz;
|
|
13
|
+
|
|
14
|
+
ALTER TABLE "retained_screenshot_artifacts"
|
|
15
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_workspace_session_fk",
|
|
16
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_workspace_turn_fk",
|
|
17
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_workspace_attempt_fk",
|
|
18
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_workspace_file_fk",
|
|
19
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_status_chk",
|
|
20
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_ready_shape_chk",
|
|
21
|
+
DROP CONSTRAINT "retained_screenshot_artifacts_terminal_cleanup_chk";
|
|
22
|
+
|
|
23
|
+
-- Claims created by migration 0140 carried no owner token. Return them to an
|
|
24
|
+
-- unclaimed state before the ownership constraint is installed.
|
|
25
|
+
UPDATE "retained_screenshot_artifacts"
|
|
26
|
+
SET
|
|
27
|
+
"status" = CASE
|
|
28
|
+
WHEN "status" = 'reconciling' THEN 'pending'
|
|
29
|
+
WHEN "status" = 'cleanup_pending' THEN 'cleanup_queued'
|
|
30
|
+
ELSE "status"
|
|
31
|
+
END,
|
|
32
|
+
"quota_state" = CASE
|
|
33
|
+
WHEN "status" IN ('failed', 'expired', 'deleted') THEN 'released'
|
|
34
|
+
WHEN "status" = 'ready' OR "ready_at" IS NOT NULL THEN 'ready'
|
|
35
|
+
ELSE 'reserved'
|
|
36
|
+
END,
|
|
37
|
+
"cleanup_reason" = CASE
|
|
38
|
+
WHEN "status" = 'cleanup_pending' THEN coalesce("cleanup_reason", 'orphaned')
|
|
39
|
+
WHEN "status" IN ('failed', 'expired', 'deleted')
|
|
40
|
+
THEN coalesce("cleanup_reason", "status")
|
|
41
|
+
ELSE "cleanup_reason"
|
|
42
|
+
END,
|
|
43
|
+
"maintenance_claim_id" = NULL,
|
|
44
|
+
"maintenance_claimed_at" = NULL;
|
|
45
|
+
|
|
46
|
+
ALTER TABLE "retained_screenshot_artifacts"
|
|
47
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_session_fk"
|
|
48
|
+
FOREIGN KEY ("session_id") REFERENCES "sessions"("id") ON DELETE SET NULL,
|
|
49
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_turn_fk"
|
|
50
|
+
FOREIGN KEY ("turn_id") REFERENCES "session_turns"("id") ON DELETE SET NULL,
|
|
51
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_attempt_fk"
|
|
52
|
+
FOREIGN KEY ("attempt_id") REFERENCES "session_turn_attempts"("id") ON DELETE SET NULL,
|
|
53
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_workspace_file_fk"
|
|
54
|
+
FOREIGN KEY ("workspace_id", "artifact_id")
|
|
55
|
+
REFERENCES "files"("workspace_id", "id") ON DELETE RESTRICT,
|
|
56
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_status_chk"
|
|
57
|
+
CHECK (
|
|
58
|
+
"status" IN (
|
|
59
|
+
'pending', 'reconciling', 'ready', 'cleanup_queued',
|
|
60
|
+
'cleanup_pending', 'failed', 'expired', 'deleted'
|
|
61
|
+
)
|
|
62
|
+
),
|
|
63
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_quota_state_chk"
|
|
64
|
+
CHECK ("quota_state" IN ('reserved', 'ready', 'released')),
|
|
65
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_status_quota_chk"
|
|
66
|
+
CHECK (
|
|
67
|
+
("status" IN ('pending', 'reconciling') AND "quota_state" = 'reserved')
|
|
68
|
+
OR ("status" = 'ready' AND "quota_state" = 'ready')
|
|
69
|
+
OR (
|
|
70
|
+
"status" IN ('cleanup_queued', 'cleanup_pending')
|
|
71
|
+
AND "quota_state" IN ('reserved', 'ready')
|
|
72
|
+
)
|
|
73
|
+
OR (
|
|
74
|
+
"status" IN ('failed', 'expired', 'deleted')
|
|
75
|
+
AND "quota_state" = 'released'
|
|
76
|
+
)
|
|
77
|
+
),
|
|
78
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_ready_shape_chk"
|
|
79
|
+
CHECK (
|
|
80
|
+
("status" = 'ready' AND "ready_at" IS NOT NULL)
|
|
81
|
+
OR ("status" IN ('pending', 'reconciling') AND "ready_at" IS NULL)
|
|
82
|
+
OR "status" IN ('cleanup_queued', 'cleanup_pending', 'failed', 'expired', 'deleted')
|
|
83
|
+
),
|
|
84
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_cleanup_reason_chk"
|
|
85
|
+
CHECK (
|
|
86
|
+
("status" IN ('pending', 'reconciling', 'ready') AND "cleanup_reason" IS NULL)
|
|
87
|
+
OR (
|
|
88
|
+
"status" IN ('cleanup_queued', 'cleanup_pending', 'failed', 'expired', 'deleted')
|
|
89
|
+
AND "cleanup_reason" IS NOT NULL
|
|
90
|
+
)
|
|
91
|
+
),
|
|
92
|
+
ADD CONSTRAINT "retained_screenshot_artifacts_claim_shape_chk"
|
|
93
|
+
CHECK (
|
|
94
|
+
(
|
|
95
|
+
"status" IN ('reconciling', 'cleanup_pending')
|
|
96
|
+
AND "maintenance_claim_id" IS NOT NULL
|
|
97
|
+
AND "maintenance_claimed_at" IS NOT NULL
|
|
98
|
+
)
|
|
99
|
+
OR (
|
|
100
|
+
"status" NOT IN ('reconciling', 'cleanup_pending')
|
|
101
|
+
AND "maintenance_claim_id" IS NULL
|
|
102
|
+
AND "maintenance_claimed_at" IS NULL
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
DROP INDEX "retained_screenshot_artifacts_pending_reconcile_idx";
|
|
107
|
+
CREATE INDEX "retained_screenshot_artifacts_pending_reconcile_idx"
|
|
108
|
+
ON "retained_screenshot_artifacts" ("updated_at", "artifact_id")
|
|
109
|
+
WHERE "status" IN ('pending', 'reconciling', 'cleanup_queued', 'cleanup_pending');
|
|
110
|
+
|
|
111
|
+
-- Make the aggregate counters a projection of explicit artifact quota state.
|
|
112
|
+
-- This also clears quota leaked by an old parent/file cascade whose lifecycle
|
|
113
|
+
-- row no longer exists.
|
|
114
|
+
INSERT INTO "workspace_screenshot_quotas" ("workspace_id", "account_id")
|
|
115
|
+
SELECT DISTINCT "workspace_id", "account_id"
|
|
116
|
+
FROM "retained_screenshot_artifacts"
|
|
117
|
+
ON CONFLICT ("workspace_id") DO NOTHING;
|
|
118
|
+
|
|
119
|
+
UPDATE "workspace_screenshot_quotas" Q
|
|
120
|
+
SET
|
|
121
|
+
"reserved_bytes" = COALESCE((
|
|
122
|
+
SELECT sum(A."size_bytes")
|
|
123
|
+
FROM "retained_screenshot_artifacts" A
|
|
124
|
+
WHERE A."workspace_id" = Q."workspace_id" AND A."quota_state" = 'reserved'
|
|
125
|
+
), 0),
|
|
126
|
+
"ready_bytes" = COALESCE((
|
|
127
|
+
SELECT sum(A."size_bytes")
|
|
128
|
+
FROM "retained_screenshot_artifacts" A
|
|
129
|
+
WHERE A."workspace_id" = Q."workspace_id" AND A."quota_state" = 'ready'
|
|
130
|
+
), 0),
|
|
131
|
+
"updated_at" = clock_timestamp();
|
|
132
|
+
|
|
133
|
+
CREATE OR REPLACE FUNCTION opengeni_private.queue_detached_retained_screenshot()
|
|
134
|
+
RETURNS trigger
|
|
135
|
+
LANGUAGE plpgsql
|
|
136
|
+
AS $$
|
|
137
|
+
DECLARE
|
|
138
|
+
v_reason text;
|
|
139
|
+
BEGIN
|
|
140
|
+
IF NOT (
|
|
141
|
+
(OLD.session_id IS NOT NULL AND NEW.session_id IS NULL)
|
|
142
|
+
OR (OLD.turn_id IS NOT NULL AND NEW.turn_id IS NULL)
|
|
143
|
+
OR (OLD.attempt_id IS NOT NULL AND NEW.attempt_id IS NULL)
|
|
144
|
+
) THEN
|
|
145
|
+
RETURN NEW;
|
|
146
|
+
END IF;
|
|
147
|
+
|
|
148
|
+
IF NEW.status IN ('failed', 'expired', 'deleted') THEN
|
|
149
|
+
RETURN NEW;
|
|
150
|
+
END IF;
|
|
151
|
+
|
|
152
|
+
v_reason := CASE
|
|
153
|
+
WHEN NEW.session_id IS NULL THEN 'session_deleted'
|
|
154
|
+
WHEN NEW.turn_id IS NULL THEN 'turn_deleted'
|
|
155
|
+
ELSE 'attempt_deleted'
|
|
156
|
+
END;
|
|
157
|
+
NEW.cleanup_reason := v_reason;
|
|
158
|
+
NEW.updated_at := clock_timestamp();
|
|
159
|
+
|
|
160
|
+
IF NEW.status IN ('pending', 'reconciling', 'ready', 'cleanup_queued') THEN
|
|
161
|
+
NEW.status := 'cleanup_queued';
|
|
162
|
+
NEW.maintenance_claim_id := NULL;
|
|
163
|
+
NEW.maintenance_claimed_at := NULL;
|
|
164
|
+
END IF;
|
|
165
|
+
RETURN NEW;
|
|
166
|
+
END;
|
|
167
|
+
$$;
|
|
168
|
+
|
|
169
|
+
DROP TRIGGER IF EXISTS "retained_screenshot_detachment_queue_trg"
|
|
170
|
+
ON "retained_screenshot_artifacts";
|
|
171
|
+
CREATE TRIGGER "retained_screenshot_detachment_queue_trg"
|
|
172
|
+
BEFORE UPDATE OF "session_id", "turn_id", "attempt_id"
|
|
173
|
+
ON "retained_screenshot_artifacts"
|
|
174
|
+
FOR EACH ROW
|
|
175
|
+
EXECUTE FUNCTION opengeni_private.queue_detached_retained_screenshot();
|
|
176
|
+
|
|
177
|
+
DROP FUNCTION opengeni_private.claim_retained_screenshot_maintenance(bigint, bigint, integer);
|
|
178
|
+
|
|
179
|
+
DO $migration$
|
|
180
|
+
DECLARE target_schema text := current_schema();
|
|
181
|
+
BEGIN
|
|
182
|
+
EXECUTE format($create$
|
|
183
|
+
CREATE FUNCTION opengeni_private.claim_retained_screenshot_maintenance(
|
|
184
|
+
p_pending_grace_ms bigint,
|
|
185
|
+
p_claim_timeout_ms bigint,
|
|
186
|
+
p_limit integer
|
|
187
|
+
)
|
|
188
|
+
RETURNS TABLE (
|
|
189
|
+
action text,
|
|
190
|
+
claim_id uuid,
|
|
191
|
+
artifact_id uuid,
|
|
192
|
+
account_id uuid,
|
|
193
|
+
workspace_id uuid,
|
|
194
|
+
session_id uuid,
|
|
195
|
+
object_key text,
|
|
196
|
+
media_type text,
|
|
197
|
+
size_bytes bigint,
|
|
198
|
+
sha256 text,
|
|
199
|
+
width integer,
|
|
200
|
+
height integer,
|
|
201
|
+
retention_expires_at timestamptz,
|
|
202
|
+
cleanup_reason text
|
|
203
|
+
)
|
|
204
|
+
LANGUAGE sql
|
|
205
|
+
SECURITY DEFINER
|
|
206
|
+
SET search_path = pg_catalog
|
|
207
|
+
AS $function$
|
|
208
|
+
WITH candidates AS (
|
|
209
|
+
SELECT
|
|
210
|
+
A.artifact_id,
|
|
211
|
+
gen_random_uuid() AS claim_id,
|
|
212
|
+
CASE
|
|
213
|
+
WHEN A.status IN ('pending', 'reconciling') THEN 'reconcile'
|
|
214
|
+
ELSE 'delete'
|
|
215
|
+
END AS action,
|
|
216
|
+
CASE
|
|
217
|
+
WHEN A.status = 'ready' THEN 'expired'
|
|
218
|
+
WHEN A.status IN ('cleanup_queued', 'cleanup_pending')
|
|
219
|
+
THEN coalesce(A.cleanup_reason, 'orphaned')
|
|
220
|
+
ELSE NULL
|
|
221
|
+
END AS cleanup_reason
|
|
222
|
+
FROM %1$I.retained_screenshot_artifacts A
|
|
223
|
+
WHERE
|
|
224
|
+
(A.status = 'ready' AND A.retention_expires_at <= clock_timestamp())
|
|
225
|
+
OR A.status = 'cleanup_queued'
|
|
226
|
+
OR (
|
|
227
|
+
A.status = 'pending'
|
|
228
|
+
AND A.updated_at <= clock_timestamp() - (
|
|
229
|
+
greatest(p_pending_grace_ms, 0)::double precision * interval '1 millisecond'
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
OR (
|
|
233
|
+
A.status IN ('reconciling', 'cleanup_pending')
|
|
234
|
+
AND A.maintenance_claimed_at <= clock_timestamp() - (
|
|
235
|
+
greatest(p_claim_timeout_ms, 0)::double precision * interval '1 millisecond'
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
ORDER BY A.retention_expires_at, A.artifact_id
|
|
239
|
+
LIMIT least(greatest(p_limit, 0), 1000)
|
|
240
|
+
FOR UPDATE SKIP LOCKED
|
|
241
|
+
), claimed AS (
|
|
242
|
+
UPDATE %1$I.retained_screenshot_artifacts A
|
|
243
|
+
SET
|
|
244
|
+
status = CASE WHEN C.action = 'reconcile' THEN 'reconciling' ELSE 'cleanup_pending' END,
|
|
245
|
+
cleanup_reason = C.cleanup_reason,
|
|
246
|
+
maintenance_claim_id = C.claim_id,
|
|
247
|
+
maintenance_claimed_at = clock_timestamp(),
|
|
248
|
+
updated_at = clock_timestamp()
|
|
249
|
+
FROM candidates C
|
|
250
|
+
WHERE A.artifact_id = C.artifact_id
|
|
251
|
+
RETURNING A.*, C.action, C.claim_id
|
|
252
|
+
)
|
|
253
|
+
SELECT C.action, C.claim_id, C.artifact_id, C.account_id, C.workspace_id, C.session_id,
|
|
254
|
+
F.object_key, C.media_type, C.size_bytes, C.sha256, C.width, C.height,
|
|
255
|
+
C.retention_expires_at, C.cleanup_reason
|
|
256
|
+
FROM claimed C
|
|
257
|
+
JOIN %1$I.files F
|
|
258
|
+
ON F.workspace_id = C.workspace_id
|
|
259
|
+
AND F.id = C.artifact_id;
|
|
260
|
+
$function$;
|
|
261
|
+
$create$, target_schema);
|
|
262
|
+
END $migration$;
|
|
263
|
+
|
|
264
|
+
REVOKE ALL ON FUNCTION opengeni_private.claim_retained_screenshot_maintenance(bigint, bigint, integer)
|
|
265
|
+
FROM PUBLIC;
|
|
266
|
+
|
|
267
|
+
DO $$
|
|
268
|
+
BEGIN
|
|
269
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
270
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.claim_retained_screenshot_maintenance(bigint, bigint, integer)
|
|
271
|
+
TO opengeni_app;
|
|
272
|
+
END IF;
|
|
273
|
+
END $$;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
|
|
3
|
+
-- Connected-machine removal is a control-plane operation. The enrollment row
|
|
4
|
+
-- remains durable evidence; this receipt table makes retries deterministic and
|
|
5
|
+
-- preserves blocked dependency explanations without storing credentials.
|
|
6
|
+
CREATE TABLE "machine_removal_operations" (
|
|
7
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
9
|
+
"workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
|
|
10
|
+
"enrollment_id" uuid NOT NULL REFERENCES "enrollments"("id") ON DELETE RESTRICT,
|
|
11
|
+
"operation_key" text NOT NULL,
|
|
12
|
+
"request_fingerprint" text NOT NULL,
|
|
13
|
+
"outcome" text NOT NULL,
|
|
14
|
+
"result" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
15
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
16
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
17
|
+
CONSTRAINT "machine_removal_operations_request_fingerprint_chk"
|
|
18
|
+
CHECK ("request_fingerprint" ~ '^[0-9a-f]{64}$'),
|
|
19
|
+
CONSTRAINT "machine_removal_operations_operation_key_chk"
|
|
20
|
+
CHECK (length(btrim("operation_key")) BETWEEN 1 AND 200
|
|
21
|
+
AND "operation_key" = btrim("operation_key")),
|
|
22
|
+
CONSTRAINT "machine_removal_operations_outcome_chk"
|
|
23
|
+
CHECK ("outcome" IN ('removed', 'already_removed', 'blocked')),
|
|
24
|
+
CONSTRAINT "machine_removal_operations_workspace_account_fk"
|
|
25
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
26
|
+
REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE UNIQUE INDEX "machine_removal_operations_workspace_operation_uq"
|
|
30
|
+
ON "machine_removal_operations" ("workspace_id", "operation_key");
|
|
31
|
+
CREATE UNIQUE INDEX "machine_removal_operations_workspace_id_uq"
|
|
32
|
+
ON "machine_removal_operations" ("workspace_id", "id");
|
|
33
|
+
CREATE INDEX "machine_removal_operations_enrollment_created_idx"
|
|
34
|
+
ON "machine_removal_operations" ("workspace_id", "enrollment_id", "created_at");
|
|
35
|
+
|
|
36
|
+
ALTER TABLE "machine_removal_operations" ENABLE ROW LEVEL SECURITY;
|
|
37
|
+
ALTER TABLE "machine_removal_operations" FORCE ROW LEVEL SECURITY;
|
|
38
|
+
CREATE POLICY workspace_isolation ON "machine_removal_operations"
|
|
39
|
+
USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
|
|
40
|
+
WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
|
|
41
|
+
|
|
42
|
+
DO $grants$
|
|
43
|
+
DECLARE target_schema text := current_schema();
|
|
44
|
+
BEGIN
|
|
45
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
46
|
+
EXECUTE format(
|
|
47
|
+
'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.machine_removal_operations TO opengeni_app',
|
|
48
|
+
target_schema
|
|
49
|
+
);
|
|
50
|
+
END IF;
|
|
51
|
+
END
|
|
52
|
+
$grants$;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Migration 0172 is already deployed and immutable. Keep its database-owned
|
|
3
|
+
-- fallback default aligned with the connected-machine removal tool through a
|
|
4
|
+
-- forward migration; application session creation still writes its exact
|
|
5
|
+
-- frozen tool selection explicitly.
|
|
6
|
+
|
|
7
|
+
ALTER TABLE "sessions"
|
|
8
|
+
ALTER COLUMN "first_party_mcp_tools"
|
|
9
|
+
SET DEFAULT '[
|
|
10
|
+
"set_session_title",
|
|
11
|
+
"goal_set",
|
|
12
|
+
"goal_update",
|
|
13
|
+
"goal_complete",
|
|
14
|
+
"goal_pause",
|
|
15
|
+
"memory_search",
|
|
16
|
+
"memory_save",
|
|
17
|
+
"memory_correct",
|
|
18
|
+
"preference_registry_summary",
|
|
19
|
+
"preference_registry_get",
|
|
20
|
+
"sandboxes_list",
|
|
21
|
+
"sandbox_attach",
|
|
22
|
+
"sandbox_swap",
|
|
23
|
+
"run_on",
|
|
24
|
+
"sandbox_provision",
|
|
25
|
+
"connected_machine_remove",
|
|
26
|
+
"rig_list",
|
|
27
|
+
"rig_get",
|
|
28
|
+
"rig_propose_change",
|
|
29
|
+
"rig_verify",
|
|
30
|
+
"rig_promote",
|
|
31
|
+
"sessions_list",
|
|
32
|
+
"session_get",
|
|
33
|
+
"session_events",
|
|
34
|
+
"session_create",
|
|
35
|
+
"session_send_message",
|
|
36
|
+
"session_pause",
|
|
37
|
+
"session_resume",
|
|
38
|
+
"session_steer",
|
|
39
|
+
"set_other_session_title",
|
|
40
|
+
"variable_set_list",
|
|
41
|
+
"environment_list",
|
|
42
|
+
"variable_set_set_variable",
|
|
43
|
+
"environment_set_variable",
|
|
44
|
+
"github_connect_link",
|
|
45
|
+
"github_repositories_list",
|
|
46
|
+
"social_connections_list",
|
|
47
|
+
"social_posts_recent",
|
|
48
|
+
"social_daily_analysis_context",
|
|
49
|
+
"social_search_live",
|
|
50
|
+
"social_mentions_live",
|
|
51
|
+
"social_thread_fetch",
|
|
52
|
+
"social_posts_sync",
|
|
53
|
+
"social_post_reply",
|
|
54
|
+
"scheduled_tasks_list",
|
|
55
|
+
"scheduled_tasks_get",
|
|
56
|
+
"scheduled_tasks_create",
|
|
57
|
+
"scheduled_tasks_update",
|
|
58
|
+
"scheduled_tasks_pause",
|
|
59
|
+
"scheduled_tasks_resume",
|
|
60
|
+
"scheduled_tasks_trigger",
|
|
61
|
+
"scheduled_tasks_delete",
|
|
62
|
+
"scheduled_task_runs_list",
|
|
63
|
+
"slack_bot_list_channels",
|
|
64
|
+
"slack_bot_channel_history",
|
|
65
|
+
"slack_bot_thread_replies",
|
|
66
|
+
"slack_bot_list_users",
|
|
67
|
+
"slack_bot_list_files",
|
|
68
|
+
"slack_bot_file_info",
|
|
69
|
+
"slack_bot_file_content",
|
|
70
|
+
"slack_bot_post_message",
|
|
71
|
+
"slack_bot_delete_message",
|
|
72
|
+
"artifacts_list",
|
|
73
|
+
"artifacts_get_source",
|
|
74
|
+
"artifacts_create",
|
|
75
|
+
"artifacts_publish",
|
|
76
|
+
"artifacts_rollback"
|
|
77
|
+
]'::jsonb;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.9",
|
|
4
4
|
"description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@opengeni/codex": "^0.2.
|
|
54
|
-
"@opengeni/config": "^0.
|
|
55
|
-
"@opengeni/contracts": "^0.
|
|
53
|
+
"@opengeni/codex": "^0.2.13",
|
|
54
|
+
"@opengeni/config": "^0.12.1",
|
|
55
|
+
"@opengeni/contracts": "^0.40.0",
|
|
56
56
|
"@opengeni/network": "^0.2.0",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"postgres": "^3.4.7"
|
package/src/database.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
runIdempotentPersistenceTransaction,
|
|
8
8
|
type IdempotentPersistenceTransactionOptions,
|
|
9
9
|
} from "./persistence-errors";
|
|
10
|
+
import { LOSSLESS_CONTENT_WRITER_APPLICATION_NAME } from "./lossless-json";
|
|
10
11
|
import * as schema from "./schema";
|
|
11
12
|
|
|
12
13
|
// §7.7 driver widening (Step I). `Database` is the structural, cross-driver
|
|
@@ -164,7 +165,7 @@ export function createDb(databaseUrl: string, options: CreateDbOptions = {}): Db
|
|
|
164
165
|
// postgres-js IGNORES a URL `?search_path=`. Unset searchPath → omit it so the
|
|
165
166
|
// server default (`public`) is unchanged for standalone.
|
|
166
167
|
connection: {
|
|
167
|
-
application_name:
|
|
168
|
+
application_name: LOSSLESS_CONTENT_WRITER_APPLICATION_NAME,
|
|
168
169
|
...(options.searchPath ? { search_path: options.searchPath } : {}),
|
|
169
170
|
...(options.isolationLevel ? { default_transaction_isolation: options.isolationLevel } : {}),
|
|
170
171
|
},
|
|
@@ -210,6 +211,11 @@ export async function setRlsContext(db: Database, context: RlsContext): Promise<
|
|
|
210
211
|
await db.execute(
|
|
211
212
|
sql`select set_config('opengeni.workspace_id', ${context.workspaceId ?? ""}, true)`,
|
|
212
213
|
);
|
|
214
|
+
// Transaction-local writer identity covers supported injected/embedded
|
|
215
|
+
// database handles whose connection-level application_name is host-owned.
|
|
216
|
+
// Old OpenGeni binaries do not set this GUC, so migration-installed update
|
|
217
|
+
// fences can distinguish their partial writes without inspecting content.
|
|
218
|
+
await db.execute(sql`select set_config('opengeni.lossless_content_writer', '1', true)`);
|
|
213
219
|
await db.execute(sql`select set_config('opengeni.sandbox_recovery_protocol_v2', '1', true)`);
|
|
214
220
|
}
|
|
215
221
|
|
|
@@ -1,35 +1,44 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const CURRENT_VERSION_PREFIX = "v2";
|
|
4
|
+
const LEGACY_VERSION_PREFIX = "v1";
|
|
4
5
|
const IV_BYTES = 12;
|
|
5
6
|
const GCM_TAG_BYTES = 16;
|
|
6
7
|
const KEY_BYTES = 32;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Encrypts one workspace environment variable value with AES-256-GCM under an
|
|
10
|
-
* operator key held outside Postgres.
|
|
11
|
+
* operator key held outside Postgres. v2 encodes the JavaScript string as
|
|
12
|
+
* UTF-16LE before encryption so every code unit (including NUL and lone
|
|
13
|
+
* surrogates) round-trips exactly. Output format:
|
|
14
|
+
* `v2:<b64 iv>:<b64 ciphertext||tag>`.
|
|
11
15
|
*/
|
|
12
16
|
export function encryptEnvironmentValue(key: Uint8Array, plaintext: string): string {
|
|
13
17
|
assertKey(key);
|
|
14
18
|
const iv = randomBytes(IV_BYTES);
|
|
15
19
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
16
20
|
const ciphertext = Buffer.concat([
|
|
17
|
-
cipher.update(plaintext, "
|
|
21
|
+
cipher.update(Buffer.from(plaintext, "utf16le")),
|
|
18
22
|
cipher.final(),
|
|
19
23
|
cipher.getAuthTag(),
|
|
20
24
|
]);
|
|
21
|
-
return `${
|
|
25
|
+
return `${CURRENT_VERSION_PREFIX}:${iv.toString("base64")}:${ciphertext.toString("base64")}`;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
|
-
* Decrypts
|
|
26
|
-
* ciphertext: unknown versions throw
|
|
27
|
-
* auth-tag mismatches throw
|
|
29
|
+
* Decrypts current lossless v2 values and historical UTF-8 v1 values. Error
|
|
30
|
+
* messages never echo plaintext or ciphertext: unknown versions throw
|
|
31
|
+
* "unsupported environment value format", auth-tag mismatches throw
|
|
32
|
+
* "environment value decryption failed".
|
|
28
33
|
*/
|
|
29
34
|
export function decryptEnvironmentValue(key: Uint8Array, stored: string): string {
|
|
30
35
|
assertKey(key);
|
|
31
36
|
const parts = stored.split(":");
|
|
32
|
-
|
|
37
|
+
const version = parts[0];
|
|
38
|
+
if (
|
|
39
|
+
parts.length !== 3 ||
|
|
40
|
+
(version !== CURRENT_VERSION_PREFIX && version !== LEGACY_VERSION_PREFIX)
|
|
41
|
+
) {
|
|
33
42
|
throw new Error("unsupported environment value format");
|
|
34
43
|
}
|
|
35
44
|
const iv = Buffer.from(parts[1]!, "base64");
|
|
@@ -42,7 +51,11 @@ export function decryptEnvironmentValue(key: Uint8Array, stored: string): string
|
|
|
42
51
|
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
43
52
|
decipher.setAuthTag(tag);
|
|
44
53
|
try {
|
|
45
|
-
|
|
54
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
55
|
+
if (version === CURRENT_VERSION_PREFIX && plaintext.length % 2 !== 0) {
|
|
56
|
+
throw new Error("invalid lossless environment value length");
|
|
57
|
+
}
|
|
58
|
+
return plaintext.toString(version === CURRENT_VERSION_PREFIX ? "utf16le" : "utf8");
|
|
46
59
|
} catch {
|
|
47
60
|
throw new Error("environment value decryption failed");
|
|
48
61
|
}
|