@opengeni/db 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-PSX56ZTL.js → chunk-T2U4H4Z2.js} +31 -1
- package/dist/chunk-T2U4H4Z2.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +293 -15
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +91 -9
- package/dist/{schema-fwrPBw5T.d.ts → schema-DuRsrmzD.d.ts} +269 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -1
- package/drizzle/0035_session_mcp_servers.sql +50 -0
- package/drizzle/0036_modal_lease_orphan_reaper.sql +92 -0
- package/drizzle/0037_session_instructions.sql +18 -0
- package/package.json +3 -3
- package/src/event-payload-sanitizer.ts +58 -1
- package/src/index.ts +392 -21
- package/src/schema.ts +29 -0
- package/dist/chunk-PSX56ZTL.js.map +0 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- 0036_modal_lease_orphan_reaper.sql
|
|
2
|
+
-- Modal lease leak hardening:
|
|
3
|
+
-- 1. An expired warming lease with a persisted instance_id is now surfaced as
|
|
4
|
+
-- immediately drainable instead of being reset cold and losing the only DB
|
|
5
|
+
-- pointer to the provider sandbox.
|
|
6
|
+
-- 2. The worker's provider-side Modal orphan sweep gets a SECURITY DEFINER
|
|
7
|
+
-- live-lease attribution read so it can compare Modal tags against current
|
|
8
|
+
-- lease rows across workspaces without disabling FORCE RLS.
|
|
9
|
+
|
|
10
|
+
CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
|
|
11
|
+
p_viewer_holder_ttl_ms bigint,
|
|
12
|
+
p_idle_grace_ms bigint
|
|
13
|
+
)
|
|
14
|
+
RETURNS TABLE (workspace_id uuid, sandbox_group_id uuid, instance_id text, lease_epoch integer)
|
|
15
|
+
LANGUAGE plpgsql
|
|
16
|
+
SECURITY DEFINER
|
|
17
|
+
AS $$
|
|
18
|
+
BEGIN
|
|
19
|
+
DELETE FROM sandbox_lease_holders h
|
|
20
|
+
WHERE h.kind = 'viewer'
|
|
21
|
+
AND h.last_heartbeat_at < now() - make_interval(secs => p_viewer_holder_ttl_ms / 1000.0);
|
|
22
|
+
|
|
23
|
+
UPDATE sandbox_leases L SET
|
|
24
|
+
refcount = c.total,
|
|
25
|
+
turn_holders = c.turns,
|
|
26
|
+
viewer_holders = c.viewers,
|
|
27
|
+
liveness = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
|
|
28
|
+
THEN 'draining' ELSE L.liveness END,
|
|
29
|
+
expires_at = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
|
|
30
|
+
THEN now() + make_interval(secs => p_idle_grace_ms / 1000.0)
|
|
31
|
+
ELSE L.expires_at END,
|
|
32
|
+
updated_at = now()
|
|
33
|
+
FROM (
|
|
34
|
+
SELECT L2.id,
|
|
35
|
+
(SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id)::int AS total,
|
|
36
|
+
(SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'turn')::int AS turns,
|
|
37
|
+
(SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'viewer')::int AS viewers
|
|
38
|
+
FROM sandbox_leases L2
|
|
39
|
+
) c
|
|
40
|
+
WHERE L.id = c.id;
|
|
41
|
+
|
|
42
|
+
-- Warming died before provider create returned: no instance was ever tracked.
|
|
43
|
+
UPDATE sandbox_leases AS L SET
|
|
44
|
+
liveness = 'cold', instance_id = NULL,
|
|
45
|
+
resume_backend_id = NULL, resume_state = NULL,
|
|
46
|
+
data_plane_url = NULL, terminal_data_plane_url = NULL, updated_at = now()
|
|
47
|
+
WHERE L.liveness = 'warming' AND L.expires_at < now() AND L.instance_id IS NULL;
|
|
48
|
+
|
|
49
|
+
-- Warming died after provider create returned: keep the instance_id and hand it
|
|
50
|
+
-- to the normal provider termination path in this same sweep.
|
|
51
|
+
UPDATE sandbox_leases AS L SET
|
|
52
|
+
liveness = 'draining',
|
|
53
|
+
refcount = 0,
|
|
54
|
+
turn_holders = 0,
|
|
55
|
+
viewer_holders = 0,
|
|
56
|
+
data_plane_url = NULL,
|
|
57
|
+
terminal_data_plane_url = NULL,
|
|
58
|
+
expires_at = now() - interval '1 millisecond',
|
|
59
|
+
updated_at = now()
|
|
60
|
+
WHERE L.liveness = 'warming' AND L.expires_at < now() AND L.instance_id IS NOT NULL;
|
|
61
|
+
|
|
62
|
+
RETURN QUERY
|
|
63
|
+
SELECT L.workspace_id, L.sandbox_group_id, L.instance_id, L.lease_epoch
|
|
64
|
+
FROM sandbox_leases L
|
|
65
|
+
WHERE L.liveness = 'draining' AND L.expires_at < now() AND L.refcount = 0;
|
|
66
|
+
END;
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
CREATE OR REPLACE FUNCTION opengeni_private.list_live_modal_sandbox_leases()
|
|
70
|
+
RETURNS TABLE (
|
|
71
|
+
lease_id uuid,
|
|
72
|
+
workspace_id uuid,
|
|
73
|
+
sandbox_group_id uuid,
|
|
74
|
+
instance_id text,
|
|
75
|
+
liveness text
|
|
76
|
+
)
|
|
77
|
+
LANGUAGE sql
|
|
78
|
+
SECURITY DEFINER
|
|
79
|
+
AS $$
|
|
80
|
+
SELECT L.id, L.workspace_id, L.sandbox_group_id, L.instance_id, L.liveness
|
|
81
|
+
FROM sandbox_leases L
|
|
82
|
+
WHERE L.liveness IN ('warming', 'warm', 'draining')
|
|
83
|
+
AND (L.backend = 'modal' OR L.resume_backend_id = 'modal');
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
DO $$
|
|
87
|
+
BEGIN
|
|
88
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
89
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.reap_sandbox_leases(bigint, bigint) TO opengeni_app;
|
|
90
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.list_live_modal_sandbox_leases() TO opengeni_app;
|
|
91
|
+
END IF;
|
|
92
|
+
END $$;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Per-session agent persona/system instructions.
|
|
2
|
+
--
|
|
3
|
+
-- An optional per-agent-type prompt supplied by an embedding host at session
|
|
4
|
+
-- creation. It rides the SAME system-level instructions channel the
|
|
5
|
+
-- per-workspace agent_instructions rides, composed AFTER the workspace persona
|
|
6
|
+
-- so it refines it for this one session — never as a user-visible timeline
|
|
7
|
+
-- event. NULL means the session carried none, so every existing row keeps its
|
|
8
|
+
-- historical, byte-identical composed instructions after this migration without
|
|
9
|
+
-- a backfill. Org-visible metadata, not a secret.
|
|
10
|
+
ALTER TABLE "sessions"
|
|
11
|
+
ADD COLUMN IF NOT EXISTS "instructions" text;
|
|
12
|
+
|
|
13
|
+
DO $$
|
|
14
|
+
BEGIN
|
|
15
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
16
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
|
|
17
|
+
END IF;
|
|
18
|
+
END $$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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": {
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@opengeni/codex": "^0.2.1",
|
|
55
|
-
"@opengeni/config": "^0.2.
|
|
56
|
-
"@opengeni/contracts": "^0.
|
|
55
|
+
"@opengeni/config": "^0.2.4",
|
|
56
|
+
"@opengeni/contracts": "^0.6.0",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"postgres": "^3.4.7"
|
|
59
59
|
},
|
|
@@ -81,9 +81,66 @@ export function sanitizeEventPayload<T>(payload: T): T {
|
|
|
81
81
|
}
|
|
82
82
|
if (payload && typeof payload === "object") {
|
|
83
83
|
const entries = Object.entries(payload as Record<string, unknown>).map(
|
|
84
|
-
([key, value]) => [sanitizeEventString(key),
|
|
84
|
+
([key, value]) => [sanitizeEventString(key), sanitizeSensitiveEventField(key, value)] as const,
|
|
85
85
|
);
|
|
86
86
|
return Object.fromEntries(entries) as unknown as T;
|
|
87
87
|
}
|
|
88
88
|
return payload;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
function sanitizeSensitiveEventField(key: string, value: unknown): unknown {
|
|
92
|
+
if (key === "mcpServers") {
|
|
93
|
+
return sanitizeSessionMcpServerList(value);
|
|
94
|
+
}
|
|
95
|
+
if (key === "mcpCredentialUpdates") {
|
|
96
|
+
return sanitizeMcpCredentialUpdateList(value);
|
|
97
|
+
}
|
|
98
|
+
return sanitizeEventPayload(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sanitizeSessionMcpServerList(value: unknown): unknown {
|
|
102
|
+
if (!Array.isArray(value)) {
|
|
103
|
+
return sanitizeEventPayload(value);
|
|
104
|
+
}
|
|
105
|
+
return value.map((item) => {
|
|
106
|
+
if (!isPlainObject(item)) {
|
|
107
|
+
return sanitizeEventPayload(item);
|
|
108
|
+
}
|
|
109
|
+
const { headers, headersEncrypted, ...rest } = item;
|
|
110
|
+
const cleaned = sanitizeEventPayload(rest) as Record<string, unknown>;
|
|
111
|
+
const headerNames = safeHeaderNames(headers) ?? safeHeaderNames(headersEncrypted);
|
|
112
|
+
if (headerNames) {
|
|
113
|
+
cleaned.headerNames = headerNames;
|
|
114
|
+
}
|
|
115
|
+
return cleaned;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sanitizeMcpCredentialUpdateList(value: unknown): unknown {
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
return sanitizeEventPayload(value);
|
|
122
|
+
}
|
|
123
|
+
return value.map((item) => {
|
|
124
|
+
if (!isPlainObject(item)) {
|
|
125
|
+
return sanitizeEventPayload(item);
|
|
126
|
+
}
|
|
127
|
+
const { headers, headersEncrypted, ...rest } = item;
|
|
128
|
+
const cleaned = sanitizeEventPayload(rest) as Record<string, unknown>;
|
|
129
|
+
const headerNames = safeHeaderNames(headers) ?? safeHeaderNames(headersEncrypted);
|
|
130
|
+
if (headerNames) {
|
|
131
|
+
cleaned.headerNames = headerNames;
|
|
132
|
+
}
|
|
133
|
+
return cleaned;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function safeHeaderNames(value: unknown): string[] | null {
|
|
138
|
+
if (!isPlainObject(value)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return Object.keys(value).map(sanitizeEventString).sort();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
145
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
146
|
+
}
|