@opengeni/db 4.4.0-canary.2 → 5.0.0-canary.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/artifact-catalog.d.ts +41 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/{chunk-M4PKBJRS.js → chunk-6T2GGYWE.js} +4 -4
- package/dist/{chunk-2KOLL33E.js → chunk-7HMK3B45.js} +2 -2
- package/dist/{chunk-WFU6PS54.js → chunk-DPONPYJD.js} +2 -2
- package/dist/{chunk-6IJD3XXF.js → chunk-ERKVSPDX.js} +3 -3
- package/dist/{chunk-5UUWZDAV.js → chunk-F73UKJY6.js} +31 -1
- package/dist/{chunk-5UUWZDAV.js.map → chunk-F73UKJY6.js.map} +1 -1
- package/dist/{chunk-6V3HDEMR.js → chunk-HQDVI2DB.js} +2 -2
- package/dist/{chunk-UOXWVP3T.js → chunk-MWMXWHJD.js} +2 -2
- package/dist/{chunk-CHBLBEU6.js → chunk-T6BWIGWJ.js} +2 -2
- package/dist/{chunk-XJP7KJ6U.js → chunk-T6YLHJLS.js} +47 -3
- package/dist/chunk-T6YLHJLS.js.map +1 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1893 -1812
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-operations.js +2 -2
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +2 -2
- package/dist/runtime-posture.d.ts +2 -0
- package/dist/schema.d.ts +93 -0
- package/dist/schema.js +3 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +4 -4
- package/dist/session-event-slices.js +2 -2
- package/dist/session-mcp-credential-rotation.js +4 -4
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +4 -4
- package/drizzle/0465_sandbox_file_publications.sql +142 -0
- package/drizzle/0466_agent_instruction_activation_preservation.sql +190 -0
- package/drizzle/0467_host_resolver_full_organization_keys.sql +34 -0
- package/package.json +6 -6
- package/src/artifact-catalog.ts +121 -0
- package/src/host-mcp-resolvers.ts +1 -1
- package/src/index.ts +1 -0
- package/src/provision-roles.ts +6 -0
- package/src/runtime-posture.ts +54 -1
- package/src/schema.ts +30 -0
- package/dist/chunk-XJP7KJ6U.js.map +0 -1
- /package/dist/{chunk-M4PKBJRS.js.map → chunk-6T2GGYWE.js.map} +0 -0
- /package/dist/{chunk-2KOLL33E.js.map → chunk-7HMK3B45.js.map} +0 -0
- /package/dist/{chunk-WFU6PS54.js.map → chunk-DPONPYJD.js.map} +0 -0
- /package/dist/{chunk-6IJD3XXF.js.map → chunk-ERKVSPDX.js.map} +0 -0
- /package/dist/{chunk-6V3HDEMR.js.map → chunk-HQDVI2DB.js.map} +0 -0
- /package/dist/{chunk-UOXWVP3T.js.map → chunk-MWMXWHJD.js.map} +0 -0
- /package/dist/{chunk-CHBLBEU6.js.map → chunk-T6BWIGWJ.js.map} +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Agent instruction writes are additive or localized. Older application
|
|
3
|
+
-- instances may still omit editMode while this migration rolls out, but their
|
|
4
|
+
-- complete-content request is accepted only when it contains the active
|
|
5
|
+
-- instruction byte-for-byte. Existing unsafe pending revisions and explicit
|
|
6
|
+
-- replacement revisions are fenced again at the central activation ledger.
|
|
7
|
+
|
|
8
|
+
ALTER FUNCTION agent_instruction_apply(uuid,uuid,jsonb,jsonb)
|
|
9
|
+
RENAME TO agent_instruction_apply_0462_unsafe;
|
|
10
|
+
|
|
11
|
+
CREATE FUNCTION agent_instruction_apply(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
12
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
13
|
+
DECLARE
|
|
14
|
+
operation text:=p_request->>'operation';
|
|
15
|
+
edit_mode text;
|
|
16
|
+
current_state jsonb;
|
|
17
|
+
current_content text;
|
|
18
|
+
requested_content text;
|
|
19
|
+
old_text text;
|
|
20
|
+
new_text text;
|
|
21
|
+
expected_content text;
|
|
22
|
+
first_position integer;
|
|
23
|
+
second_position integer;
|
|
24
|
+
preservation jsonb;
|
|
25
|
+
effective_actor jsonb:=p_actor;
|
|
26
|
+
BEGIN
|
|
27
|
+
IF operation='save' THEN
|
|
28
|
+
-- Preserve the exact fingerprint of operations first executed by either
|
|
29
|
+
-- side of this rolling boundary. New receipts carry the derived marker in
|
|
30
|
+
-- their stored actor; older receipts intentionally do not.
|
|
31
|
+
IF p_request->>'operationId' IS NOT NULL THEN
|
|
32
|
+
SELECT op.actor->'_instructionPreservation' INTO preservation
|
|
33
|
+
FROM agent_instruction_operations op
|
|
34
|
+
WHERE op.account_id=p_account
|
|
35
|
+
AND op.operation_id=(p_request->>'operationId')::uuid;
|
|
36
|
+
IF FOUND THEN
|
|
37
|
+
IF preservation IS NOT NULL THEN
|
|
38
|
+
effective_actor:=p_actor||jsonb_build_object(
|
|
39
|
+
'_instructionPreservation',preservation);
|
|
40
|
+
END IF;
|
|
41
|
+
RETURN agent_instruction_apply_0462_unsafe(
|
|
42
|
+
p_account,p_workspace,effective_actor,p_request);
|
|
43
|
+
END IF;
|
|
44
|
+
END IF;
|
|
45
|
+
current_state:=agent_instruction_apply_0462_unsafe(
|
|
46
|
+
p_account,p_workspace,p_actor,
|
|
47
|
+
jsonb_build_object('operation','get','target',p_request->'target'));
|
|
48
|
+
current_content:=current_state->>'content';
|
|
49
|
+
IF p_request ? 'editMode' THEN
|
|
50
|
+
edit_mode:=p_request->>'editMode';
|
|
51
|
+
IF edit_mode IS NULL OR edit_mode NOT IN ('append','edit') THEN
|
|
52
|
+
RAISE EXCEPTION 'Agents cannot replace the complete workspace instruction'
|
|
53
|
+
USING ERRCODE='22023';
|
|
54
|
+
END IF;
|
|
55
|
+
IF edit_mode='append' THEN
|
|
56
|
+
requested_content:=p_request->>'content';
|
|
57
|
+
expected_content:=CASE
|
|
58
|
+
WHEN current_content IS NULL THEN requested_content
|
|
59
|
+
WHEN right(current_content,2)=E'\n\n' THEN current_content||requested_content
|
|
60
|
+
WHEN right(current_content,1)=E'\n' THEN current_content||E'\n'||requested_content
|
|
61
|
+
ELSE current_content||E'\n\n'||requested_content
|
|
62
|
+
END;
|
|
63
|
+
ELSE
|
|
64
|
+
old_text:=p_request->>'oldText';
|
|
65
|
+
new_text:=p_request->>'newText';
|
|
66
|
+
IF current_content IS NOT NULL AND old_text IS NOT DISTINCT FROM current_content THEN
|
|
67
|
+
RAISE EXCEPTION 'Exact instruction edits must be localized; the complete instruction cannot be replaced'
|
|
68
|
+
USING ERRCODE='22023';
|
|
69
|
+
END IF;
|
|
70
|
+
first_position:=strpos(current_content,old_text);
|
|
71
|
+
IF first_position=0 THEN
|
|
72
|
+
RAISE EXCEPTION 'Exact instruction edit anchor was not found' USING ERRCODE='22023';
|
|
73
|
+
END IF;
|
|
74
|
+
second_position:=strpos(substring(current_content FROM first_position+1),old_text);
|
|
75
|
+
IF second_position>0 THEN
|
|
76
|
+
RAISE EXCEPTION 'Exact instruction edit anchor is ambiguous' USING ERRCODE='22023';
|
|
77
|
+
END IF;
|
|
78
|
+
expected_content:=overlay(current_content PLACING new_text FROM first_position FOR length(old_text));
|
|
79
|
+
END IF;
|
|
80
|
+
ELSE
|
|
81
|
+
-- A pre-0462 caller already constructed a complete proposed document.
|
|
82
|
+
-- Preserve rolling availability for additive requests without allowing
|
|
83
|
+
-- that compatibility path to discard any active byte.
|
|
84
|
+
requested_content:=p_request->>'content';
|
|
85
|
+
IF current_content IS NOT NULL
|
|
86
|
+
AND (requested_content IS NULL OR strpos(requested_content,current_content)=0) THEN
|
|
87
|
+
RAISE EXCEPTION 'Legacy instruction replacement would discard active workspace instructions'
|
|
88
|
+
USING ERRCODE='22023';
|
|
89
|
+
END IF;
|
|
90
|
+
edit_mode:='legacy_safe';
|
|
91
|
+
expected_content:=requested_content;
|
|
92
|
+
END IF;
|
|
93
|
+
preservation:=jsonb_strip_nulls(jsonb_build_object(
|
|
94
|
+
'saveOperationId',p_request->>'operationId',
|
|
95
|
+
'baselineContentHash',CASE WHEN current_content IS NULL THEN NULL
|
|
96
|
+
ELSE encode(sha256(convert_to(current_content,'UTF8')),'hex') END,
|
|
97
|
+
'resultContentHash',CASE WHEN expected_content IS NULL THEN NULL
|
|
98
|
+
ELSE encode(sha256(convert_to(expected_content,'UTF8')),'hex') END,
|
|
99
|
+
'editMode',edit_mode));
|
|
100
|
+
effective_actor:=p_actor||jsonb_build_object(
|
|
101
|
+
'_instructionPreservation',preservation);
|
|
102
|
+
END IF;
|
|
103
|
+
RETURN agent_instruction_apply_0462_unsafe(
|
|
104
|
+
p_account,p_workspace,effective_actor,p_request);
|
|
105
|
+
END $$;
|
|
106
|
+
|
|
107
|
+
CREATE FUNCTION agent_instruction_activation_preserves_baseline()
|
|
108
|
+
RETURNS trigger LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
109
|
+
DECLARE
|
|
110
|
+
proposed workspace_instruction_policy_revisions%ROWTYPE;
|
|
111
|
+
baseline_content text;
|
|
112
|
+
baseline_hash text;
|
|
113
|
+
proposed_hash text;
|
|
114
|
+
preservation jsonb;
|
|
115
|
+
BEGIN
|
|
116
|
+
IF NEW.type<>'activate' OR NEW.old_revision_id IS NULL THEN RETURN NEW; END IF;
|
|
117
|
+
SELECT * INTO proposed FROM workspace_instruction_policy_revisions r
|
|
118
|
+
WHERE r.account_id=NEW.account_id AND r.workspace_id=NEW.workspace_id
|
|
119
|
+
AND r.id=NEW.new_revision_id;
|
|
120
|
+
IF NOT FOUND OR proposed.provenance_source<>'agent_learning' THEN RETURN NEW; END IF;
|
|
121
|
+
SELECT r.content INTO baseline_content FROM workspace_instruction_policy_revisions r
|
|
122
|
+
WHERE r.account_id=NEW.account_id AND r.workspace_id=NEW.workspace_id
|
|
123
|
+
AND r.id=NEW.old_revision_id;
|
|
124
|
+
IF NOT FOUND THEN
|
|
125
|
+
RAISE EXCEPTION 'The active instruction baseline is unavailable'
|
|
126
|
+
USING ERRCODE='22023';
|
|
127
|
+
END IF;
|
|
128
|
+
-- Compatibility revisions that retain the complete baseline are safe even
|
|
129
|
+
-- when they predate the wrapper-derived preservation metadata below.
|
|
130
|
+
IF baseline_content IS NOT NULL AND strpos(proposed.content,baseline_content)>0 THEN
|
|
131
|
+
RETURN NEW;
|
|
132
|
+
END IF;
|
|
133
|
+
baseline_hash:=encode(sha256(convert_to(baseline_content,'UTF8')),'hex');
|
|
134
|
+
proposed_hash:=encode(sha256(convert_to(proposed.content,'UTF8')),'hex');
|
|
135
|
+
preservation:=proposed.agent_learning_context#>'{actor,_instructionPreservation}';
|
|
136
|
+
IF preservation->>'saveOperationId'=proposed.operation_id::text
|
|
137
|
+
AND preservation->>'baselineContentHash'=baseline_hash
|
|
138
|
+
AND preservation->>'resultContentHash'=proposed_hash
|
|
139
|
+
AND preservation->>'editMode' IN ('append','edit','legacy_safe') THEN
|
|
140
|
+
RETURN NEW;
|
|
141
|
+
END IF;
|
|
142
|
+
RAISE EXCEPTION 'Unsafe whole-instruction agent replacement cannot be activated; reject it and create an append or localized edit'
|
|
143
|
+
USING ERRCODE='22023';
|
|
144
|
+
END $$;
|
|
145
|
+
|
|
146
|
+
DROP TRIGGER IF EXISTS agent_instruction_activation_preservation
|
|
147
|
+
ON workspace_instruction_policy_activation_events;
|
|
148
|
+
CREATE TRIGGER agent_instruction_activation_preservation
|
|
149
|
+
BEFORE INSERT ON workspace_instruction_policy_activation_events
|
|
150
|
+
FOR EACH ROW EXECUTE FUNCTION agent_instruction_activation_preserves_baseline();
|
|
151
|
+
|
|
152
|
+
DO $secure$
|
|
153
|
+
DECLARE grantee text; runtime_role text;
|
|
154
|
+
BEGIN
|
|
155
|
+
EXECUTE format(
|
|
156
|
+
'ALTER FUNCTION %I.agent_instruction_apply_0462_unsafe(uuid,uuid,jsonb,jsonb) SET search_path = %I, pg_catalog, pg_temp',
|
|
157
|
+
current_schema(),current_schema());
|
|
158
|
+
EXECUTE format(
|
|
159
|
+
'ALTER FUNCTION %I.agent_instruction_apply(uuid,uuid,jsonb,jsonb) SET search_path = %I, pg_catalog, pg_temp',
|
|
160
|
+
current_schema(),current_schema());
|
|
161
|
+
EXECUTE format(
|
|
162
|
+
'ALTER FUNCTION %I.agent_instruction_activation_preserves_baseline() SET search_path = %I, pg_catalog, pg_temp',
|
|
163
|
+
current_schema(),current_schema());
|
|
164
|
+
REVOKE ALL ON FUNCTION agent_instruction_apply_0462_unsafe(uuid,uuid,jsonb,jsonb) FROM PUBLIC;
|
|
165
|
+
REVOKE ALL ON FUNCTION agent_instruction_apply(uuid,uuid,jsonb,jsonb) FROM PUBLIC;
|
|
166
|
+
REVOKE ALL ON FUNCTION agent_instruction_activation_preserves_baseline() FROM PUBLIC;
|
|
167
|
+
FOR grantee IN SELECT DISTINCT role.rolname FROM pg_proc proc,
|
|
168
|
+
LATERAL aclexplode(coalesce(proc.proacl,acldefault('f',proc.proowner))) acl
|
|
169
|
+
JOIN pg_roles role ON role.oid=acl.grantee
|
|
170
|
+
WHERE proc.pronamespace=current_schema()::regnamespace
|
|
171
|
+
AND proc.proname='agent_instruction_apply_0462_unsafe'
|
|
172
|
+
AND acl.grantee<>proc.proowner
|
|
173
|
+
LOOP
|
|
174
|
+
EXECUTE format(
|
|
175
|
+
'REVOKE ALL ON FUNCTION %I.agent_instruction_apply_0462_unsafe(uuid,uuid,jsonb,jsonb) FROM %I',
|
|
176
|
+
current_schema(),grantee);
|
|
177
|
+
END LOOP;
|
|
178
|
+
FOR runtime_role IN SELECT jsonb_array_elements_text(
|
|
179
|
+
current_setting('opengeni.migration_application_roles')::jsonb)
|
|
180
|
+
LOOP
|
|
181
|
+
IF runtime_role<>current_user AND EXISTS(SELECT 1 FROM pg_roles WHERE rolname=runtime_role) THEN
|
|
182
|
+
EXECUTE format(
|
|
183
|
+
'REVOKE ALL ON FUNCTION %I.agent_instruction_apply_0462_unsafe(uuid,uuid,jsonb,jsonb) FROM %I',
|
|
184
|
+
current_schema(),runtime_role);
|
|
185
|
+
EXECUTE format(
|
|
186
|
+
'GRANT EXECUTE ON FUNCTION %I.agent_instruction_apply(uuid,uuid,jsonb,jsonb) TO %I',
|
|
187
|
+
current_schema(),runtime_role);
|
|
188
|
+
END IF;
|
|
189
|
+
END LOOP;
|
|
190
|
+
END $secure$;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Align the existing resolver write fence with public full organization keys.
|
|
3
|
+
-- No grants, ownership, row policies, or stored credentials change.
|
|
4
|
+
CREATE OR REPLACE FUNCTION opengeni_private.guard_host_mcp_resolver_write() RETURNS trigger
|
|
5
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
|
|
6
|
+
DECLARE permissions_value jsonb;
|
|
7
|
+
BEGIN
|
|
8
|
+
SELECT credential.permissions INTO permissions_value FROM api_keys credential
|
|
9
|
+
WHERE credential.account_id = NEW.account_id
|
|
10
|
+
AND 'api_key:' || credential.id::text = opengeni_private.current_subject_id()
|
|
11
|
+
AND credential.workspace_id IS NULL AND credential.credential_kind = 'organization'
|
|
12
|
+
AND credential.revoked_at IS NULL
|
|
13
|
+
AND (credential.expires_at IS NULL OR credential.expires_at > clock_timestamp())
|
|
14
|
+
FOR SHARE;
|
|
15
|
+
IF NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
16
|
+
OR NOT coalesce(permissions_value ? 'workspace:admin', false) THEN
|
|
17
|
+
RAISE EXCEPTION 'organization service administration required' USING ERRCODE = '42501';
|
|
18
|
+
END IF;
|
|
19
|
+
IF TG_TABLE_NAME = 'host_mcp_resolvers' THEN
|
|
20
|
+
IF TG_OP = 'UPDATE' THEN
|
|
21
|
+
IF ROW(NEW.id, NEW.account_id, NEW.external_source, NEW.created_at)
|
|
22
|
+
IS DISTINCT FROM ROW(OLD.id, OLD.account_id, OLD.external_source, OLD.created_at)
|
|
23
|
+
OR NEW.generation <> OLD.generation + 1 THEN
|
|
24
|
+
RAISE EXCEPTION 'resolver identity or generation conflict' USING ERRCODE = '40001';
|
|
25
|
+
END IF;
|
|
26
|
+
ELSIF NEW.generation <> 1 THEN
|
|
27
|
+
RAISE EXCEPTION 'initial resolver generation invalid' USING ERRCODE = '22023';
|
|
28
|
+
END IF;
|
|
29
|
+
ELSIF NEW.actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id() THEN
|
|
30
|
+
RAISE EXCEPTION 'resolver operation actor invalid' USING ERRCODE = '42501';
|
|
31
|
+
END IF;
|
|
32
|
+
RETURN NEW;
|
|
33
|
+
END
|
|
34
|
+
$body$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-canary.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": {
|
|
@@ -102,11 +102,11 @@
|
|
|
102
102
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@opengeni/codemode": "^0.5.
|
|
106
|
-
"@opengeni/codex": "^0.2.
|
|
107
|
-
"@opengeni/config": "^1.2.
|
|
108
|
-
"@opengeni/contracts": "^
|
|
109
|
-
"@opengeni/network": "^0.3.1-canary.
|
|
105
|
+
"@opengeni/codemode": "^0.5.8-canary.0",
|
|
106
|
+
"@opengeni/codex": "^0.2.23-canary.0",
|
|
107
|
+
"@opengeni/config": "^1.2.1-canary.0",
|
|
108
|
+
"@opengeni/contracts": "^4.0.0-canary.0",
|
|
109
|
+
"@opengeni/network": "^0.3.1-canary.15",
|
|
110
110
|
"drizzle-orm": "^0.45.2",
|
|
111
111
|
"postgres": "^3.4.7"
|
|
112
112
|
},
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ArtifactCatalogKind, ArtifactCatalogListQuery } from "@opengeni/contracts";
|
|
2
|
+
import { sql, type SQL } from "drizzle-orm";
|
|
3
|
+
import { withRlsContext, type Database } from "./database";
|
|
4
|
+
|
|
5
|
+
export type ArtifactCatalogPosition = { key: string; kind: ArtifactCatalogKind; id: string };
|
|
6
|
+
/** Internal candidates are NOT an authorized public projection. */
|
|
7
|
+
export type ArtifactCatalogCandidate = {
|
|
8
|
+
id: string;
|
|
9
|
+
kind: ArtifactCatalogKind;
|
|
10
|
+
/** Internal adapter identity; never exposed as public provenance. */
|
|
11
|
+
origin: "site" | "editable_artifact" | "generated_image" | "sandbox_file";
|
|
12
|
+
title: string;
|
|
13
|
+
status: "active" | "archived";
|
|
14
|
+
created_at: Date | string;
|
|
15
|
+
updated_at: Date | string;
|
|
16
|
+
source_session_id: string | null;
|
|
17
|
+
version_id: string | null;
|
|
18
|
+
sort_key: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Called only after immutable bytes have been retained by explicit publication. */
|
|
22
|
+
export async function recordSandboxFilePublication(
|
|
23
|
+
db: Database,
|
|
24
|
+
input: { accountId: string; workspaceId: string; fileId: string; sourceSessionId: string },
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
await withRlsContext(db, input, async (tx) => {
|
|
27
|
+
await tx.execute(
|
|
28
|
+
sql`select opengeni_private.record_sandbox_file_publication(${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.fileId}::uuid, ${input.sourceSessionId}::uuid)`,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Bounded keyset projection over existing domains; no universal content store.
|
|
34
|
+
* The caller must still perform editable application and file access checks.
|
|
35
|
+
*/
|
|
36
|
+
export async function listArtifactCatalogCandidates(
|
|
37
|
+
db: Database,
|
|
38
|
+
scope: { accountId: string; workspaceId: string },
|
|
39
|
+
input: {
|
|
40
|
+
query: ArtifactCatalogListQuery;
|
|
41
|
+
kinds: readonly ArtifactCatalogKind[];
|
|
42
|
+
snapshotAt: string;
|
|
43
|
+
after?: ArtifactCatalogPosition;
|
|
44
|
+
limit: number;
|
|
45
|
+
},
|
|
46
|
+
): Promise<ArtifactCatalogCandidate[]> {
|
|
47
|
+
if (!input.kinds.length) return [];
|
|
48
|
+
if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 201)
|
|
49
|
+
throw new Error("Invalid catalog candidate limit");
|
|
50
|
+
const { query } = input;
|
|
51
|
+
const source = query.sourceSessionId;
|
|
52
|
+
const branches: SQL[] = [];
|
|
53
|
+
if (input.kinds.includes("site"))
|
|
54
|
+
branches.push(sql`
|
|
55
|
+
SELECT a.id::text, 'site'::text AS kind, a.title, a.status, a.created_at, a.updated_at,
|
|
56
|
+
${source ? sql`${source}::uuid` : sql`v.source_session_id`} AS source_session_id,
|
|
57
|
+
a.current_version_id::text AS version_id, 'site'::text AS origin
|
|
58
|
+
FROM workspace_artifacts a LEFT JOIN workspace_artifact_versions v
|
|
59
|
+
ON v.workspace_id = a.workspace_id AND v.artifact_id = a.id AND v.id = a.current_version_id
|
|
60
|
+
WHERE a.account_id = ${scope.accountId}::uuid AND a.workspace_id = ${scope.workspaceId}::uuid
|
|
61
|
+
${source ? sql`AND EXISTS (SELECT 1 FROM workspace_artifact_versions sv WHERE sv.workspace_id = a.workspace_id AND sv.artifact_id = a.id AND sv.source_session_id = ${source}::uuid)` : sql``}`);
|
|
62
|
+
const editableKinds = input.kinds.filter((kind) =>
|
|
63
|
+
["document", "spreadsheet", "presentation"].includes(kind),
|
|
64
|
+
);
|
|
65
|
+
if (editableKinds.length)
|
|
66
|
+
branches.push(sql`
|
|
67
|
+
SELECT a.id, a.modality AS kind, a.title, a.lifecycle_state AS status, a.created_at, a.updated_at,
|
|
68
|
+
${source ? sql`${source}::uuid` : sql`NULL::uuid`} AS source_session_id, NULL::text AS version_id, 'editable_artifact'::text AS origin
|
|
69
|
+
FROM editable_artifacts a WHERE a.account_id = ${scope.accountId}::uuid AND a.workspace_id = ${scope.workspaceId}::uuid
|
|
70
|
+
AND a.modality IN (${sql.join(
|
|
71
|
+
editableKinds.map((kind) => sql`${kind}`),
|
|
72
|
+
sql`, `,
|
|
73
|
+
)})
|
|
74
|
+
${source ? sql`AND EXISTS (SELECT 1 FROM editable_artifact_session_links l WHERE l.account_id = a.account_id AND l.workspace_id = a.workspace_id AND l.artifact_id = a.id AND l.session_id = ${source}::uuid)` : sql``}`);
|
|
75
|
+
if (input.kinds.includes("image"))
|
|
76
|
+
branches.push(sql`
|
|
77
|
+
SELECT f.id::text, 'image'::text AS kind, f.filename AS title, 'active'::text AS status,
|
|
78
|
+
g.created_at, g.updated_at, g.session_id AS source_session_id, NULL::text AS version_id, 'generated_image'::text AS origin
|
|
79
|
+
FROM generated_image_artifacts g JOIN files f ON f.account_id = g.account_id AND f.workspace_id = g.workspace_id AND f.id = g.artifact_id
|
|
80
|
+
WHERE g.account_id = ${scope.accountId}::uuid AND g.workspace_id = ${scope.workspaceId}::uuid AND g.status = 'ready' AND f.status = 'ready'
|
|
81
|
+
${source ? sql`AND g.session_id = ${source}::uuid` : sql``}`);
|
|
82
|
+
const publishedKinds = input.kinds.filter((kind) => kind === "file" || kind === "image");
|
|
83
|
+
if (publishedKinds.length)
|
|
84
|
+
branches.push(sql`
|
|
85
|
+
SELECT p.file_id::text AS id, p.kind, p.title, 'active'::text AS status,
|
|
86
|
+
p.published_at AS created_at, p.published_at AS updated_at, p.source_session_id, NULL::text AS version_id, 'sandbox_file'::text AS origin
|
|
87
|
+
FROM opengeni_private.list_sandbox_file_publications(${scope.accountId}::uuid, ${scope.workspaceId}::uuid,
|
|
88
|
+
${JSON.stringify({ sourceSessionId: source, q: query.q, kinds: publishedKinds, sort: query.sort, snapshotAt: input.snapshotAt, after: input.after, limit: input.limit })}::jsonb) p`);
|
|
89
|
+
if (!branches.length) return [];
|
|
90
|
+
const sort =
|
|
91
|
+
query.sort === "title"
|
|
92
|
+
? sql`lower(title)`
|
|
93
|
+
: query.sort === "newest"
|
|
94
|
+
? sql`to_char(created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US')`
|
|
95
|
+
: sql`to_char(updated_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US')`;
|
|
96
|
+
const descending = query.sort !== "title";
|
|
97
|
+
const position = input.after;
|
|
98
|
+
const keyset = !position
|
|
99
|
+
? sql`true`
|
|
100
|
+
: sql`(
|
|
101
|
+
sort_key COLLATE "C" ${descending ? sql`<` : sql`>`} ${position.key} COLLATE "C"
|
|
102
|
+
OR (sort_key = ${position.key} AND (kind COLLATE "C", id COLLATE "C") > (${position.kind}, ${position.id})))`;
|
|
103
|
+
return withRlsContext(
|
|
104
|
+
db,
|
|
105
|
+
scope,
|
|
106
|
+
async (tx) => {
|
|
107
|
+
const result = await tx.execute(sql`
|
|
108
|
+
WITH candidates AS (${sql.join(branches, sql` UNION ALL `)}),
|
|
109
|
+
ordered AS (SELECT *, ${sort} AS sort_key FROM candidates
|
|
110
|
+
WHERE status = ${query.status} AND created_at <= ${input.snapshotAt}::timestamptz
|
|
111
|
+
${query.q ? sql`AND strpos(lower(title), lower(${query.q})) > 0` : sql``})
|
|
112
|
+
SELECT * FROM ordered WHERE ${keyset}
|
|
113
|
+
ORDER BY sort_key COLLATE "C" ${descending ? sql`DESC` : sql`ASC`}, kind COLLATE "C", id COLLATE "C"
|
|
114
|
+
LIMIT ${input.limit}`);
|
|
115
|
+
return (
|
|
116
|
+
Array.isArray(result) ? result : (result as { rows: ArtifactCatalogCandidate[] }).rows
|
|
117
|
+
) as ArtifactCatalogCandidate[];
|
|
118
|
+
},
|
|
119
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -58,7 +58,7 @@ async function administered<T>(
|
|
|
58
58
|
sql`select id from api_keys where account_id = ${actor.accountId}::uuid
|
|
59
59
|
and 'api_key:' || id::text = ${actor.subjectId} and workspace_id is null and credential_kind = 'organization'
|
|
60
60
|
and revoked_at is null and (expires_at is null or expires_at > clock_timestamp())
|
|
61
|
-
and permissions ? '
|
|
61
|
+
and permissions ? 'workspace:admin' for share`,
|
|
62
62
|
);
|
|
63
63
|
if (!key) throw new HostMcpResolverError(403, "organization service administration required");
|
|
64
64
|
await tx.execute(
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./artifact-catalog";
|
|
1
2
|
import { grantWorkspaceAccess } from "./workspace-membership-access";
|
|
2
3
|
export { grantWorkspaceAccess, listWorkspaceMembers } from "./workspace-membership-access";
|
|
3
4
|
import { codexSelectionDiagnostics } from "./codex-selection-diagnostics";
|
package/src/provision-roles.ts
CHANGED
|
@@ -2089,6 +2089,12 @@ BEGIN
|
|
|
2089
2089
|
EXECUTE format('GRANT USAGE ON SCHEMA opengeni_private TO %I', ${literal(role)});
|
|
2090
2090
|
EXECUTE format('REVOKE CREATE ON SCHEMA opengeni_private FROM %I', ${literal(role)});
|
|
2091
2091
|
EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_private TO %I', ${literal(role)});
|
|
2092
|
+
IF to_regclass('opengeni_private.sandbox_file_publications') IS NOT NULL THEN
|
|
2093
|
+
EXECUTE format('REVOKE ALL ON TABLE opengeni_private.sandbox_file_publications FROM %I', ${literal(role)});
|
|
2094
|
+
REVOKE ALL ON TABLE opengeni_private.sandbox_file_publications FROM PUBLIC;
|
|
2095
|
+
REVOKE ALL ON FUNCTION opengeni_private.record_sandbox_file_publication(uuid,uuid,uuid,uuid) FROM PUBLIC;
|
|
2096
|
+
REVOKE ALL ON FUNCTION opengeni_private.list_sandbox_file_publications(uuid,uuid,jsonb) FROM PUBLIC;
|
|
2097
|
+
END IF;
|
|
2092
2098
|
FOREACH routine_signature IN ARRAY ARRAY[
|
|
2093
2099
|
'guard_mcp_operation_immutable()',
|
|
2094
2100
|
'mcp_operation_command_scoped(jsonb,text,jsonb)',
|
package/src/runtime-posture.ts
CHANGED
|
@@ -48,6 +48,11 @@ const AUTOMATIC_SESSION_TITLE_FANOUT_MIGRATION_ROUTINE =
|
|
|
48
48
|
const AUTOMATIC_SESSION_TITLE_POLICY_TRIGGER_ROUTINE =
|
|
49
49
|
"enforce_automatic_session_title_policy_v1()";
|
|
50
50
|
const AUTOMATIC_SESSION_TITLE_FANOUT_OUTBOX_TABLE = "automatic_session_title_fanout_outbox_v1";
|
|
51
|
+
export const SANDBOX_FILE_PUBLICATION_RUNTIME_ROUTINES = [
|
|
52
|
+
"record_sandbox_file_publication(uuid, uuid, uuid, uuid)",
|
|
53
|
+
"list_sandbox_file_publications(uuid, uuid, jsonb)",
|
|
54
|
+
] as const;
|
|
55
|
+
const SANDBOX_FILE_PUBLICATIONS_TABLE = "sandbox_file_publications";
|
|
51
56
|
const AUTOMATIC_SESSION_TITLE_QUARANTINE_FENCE_ROUTINE =
|
|
52
57
|
"acquire_automatic_session_title_quarantine_fences_v1(integer)";
|
|
53
58
|
|
|
@@ -1630,6 +1635,7 @@ export type RuntimeRoutinePosture = {
|
|
|
1630
1635
|
execute: boolean;
|
|
1631
1636
|
publicExecute?: boolean;
|
|
1632
1637
|
securityDefiner: boolean;
|
|
1638
|
+
configuration?: string[] | null;
|
|
1633
1639
|
};
|
|
1634
1640
|
|
|
1635
1641
|
export type RuntimeTargetRoutinePosture = RuntimeRoutinePosture & {
|
|
@@ -1952,6 +1958,7 @@ export async function inspectRuntimeDatabasePosture(
|
|
|
1952
1958
|
${DOCUMENT_MIGRATION_CAPABILITY_TABLE},
|
|
1953
1959
|
${SCOPED_COMPUTE_CAPABILITY_TABLE},
|
|
1954
1960
|
${CONNECTION_TENANCY_BACKFILL_CAPABILITY_TABLE},
|
|
1961
|
+
${SANDBOX_FILE_PUBLICATIONS_TABLE},
|
|
1955
1962
|
${AUTOMATIC_SESSION_TITLE_FANOUT_OUTBOX_TABLE}
|
|
1956
1963
|
)
|
|
1957
1964
|
`),
|
|
@@ -2018,6 +2025,7 @@ export async function inspectRuntimeDatabasePosture(
|
|
|
2018
2025
|
can_execute: boolean;
|
|
2019
2026
|
public_execute: boolean;
|
|
2020
2027
|
security_definer: boolean;
|
|
2028
|
+
configuration: string[] | null;
|
|
2021
2029
|
}>(
|
|
2022
2030
|
await tx.execute(sql`
|
|
2023
2031
|
select
|
|
@@ -2029,7 +2037,8 @@ export async function inspectRuntimeDatabasePosture(
|
|
|
2029
2037
|
from aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) acl
|
|
2030
2038
|
where acl.grantee = 0 and acl.privilege_type = 'EXECUTE'
|
|
2031
2039
|
) as public_execute,
|
|
2032
|
-
p.prosecdef as security_definer
|
|
2040
|
+
p.prosecdef as security_definer,
|
|
2041
|
+
p.proconfig as configuration
|
|
2033
2042
|
from pg_proc p
|
|
2034
2043
|
join pg_namespace n on n.oid = p.pronamespace
|
|
2035
2044
|
where n.nspname = 'opengeni_private'
|
|
@@ -2042,6 +2051,7 @@ export async function inspectRuntimeDatabasePosture(
|
|
|
2042
2051
|
execute: row.can_execute,
|
|
2043
2052
|
publicExecute: row.public_execute,
|
|
2044
2053
|
securityDefiner: row.security_definer,
|
|
2054
|
+
configuration: row.configuration,
|
|
2045
2055
|
}));
|
|
2046
2056
|
|
|
2047
2057
|
return {
|
|
@@ -3509,6 +3519,49 @@ export function evaluateRuntimeDatabasePosture(
|
|
|
3509
3519
|
}
|
|
3510
3520
|
}
|
|
3511
3521
|
|
|
3522
|
+
const publicationTables = posture.privateTables.filter(
|
|
3523
|
+
(table) => table.name === SANDBOX_FILE_PUBLICATIONS_TABLE,
|
|
3524
|
+
);
|
|
3525
|
+
if (publicationTables.length !== 1) {
|
|
3526
|
+
if (!options.protectedTables)
|
|
3527
|
+
violations.push("sandbox file publication private relation is missing or ambiguous");
|
|
3528
|
+
} else {
|
|
3529
|
+
const table = publicationTables[0]!;
|
|
3530
|
+
if (!table.rlsEnabled || !table.rlsForced || !table.rlsActive || (table.policyCount ?? 0) < 2) {
|
|
3531
|
+
violations.push("sandbox file publication relation lacks active FORCE-RLS file isolation");
|
|
3532
|
+
}
|
|
3533
|
+
if (
|
|
3534
|
+
table.select ||
|
|
3535
|
+
table.insert ||
|
|
3536
|
+
table.update ||
|
|
3537
|
+
table.delete ||
|
|
3538
|
+
table.owner === expectedRole
|
|
3539
|
+
) {
|
|
3540
|
+
violations.push("runtime role has forbidden direct sandbox file publication authority");
|
|
3541
|
+
}
|
|
3542
|
+
const filesOwner = tableByName.get("files")?.owner;
|
|
3543
|
+
if (filesOwner && table.owner !== filesOwner)
|
|
3544
|
+
violations.push("sandbox file publication owner does not match file authority");
|
|
3545
|
+
for (const name of SANDBOX_FILE_PUBLICATION_RUNTIME_ROUTINES) {
|
|
3546
|
+
const routines = posture.privateRoutines.filter((routine) => routine.name === name);
|
|
3547
|
+
const quotedSchema = `"${targetSchema.replaceAll('"', '""')}"`;
|
|
3548
|
+
const searchPaths = new Set([
|
|
3549
|
+
`search_path=pg_catalog, ${quotedSchema}, pg_temp`,
|
|
3550
|
+
`search_path=pg_catalog, ${/^[a-z_][a-z0-9_]*$/.test(targetSchema) ? targetSchema : quotedSchema}, pg_temp`,
|
|
3551
|
+
]);
|
|
3552
|
+
if (
|
|
3553
|
+
routines.length !== 1 ||
|
|
3554
|
+
!routines[0]!.execute ||
|
|
3555
|
+
routines[0]!.publicExecute ||
|
|
3556
|
+
!routines[0]!.securityDefiner ||
|
|
3557
|
+
routines[0]!.owner !== table.owner ||
|
|
3558
|
+
!routines[0]!.configuration?.some((configuration) => searchPaths.has(configuration))
|
|
3559
|
+
) {
|
|
3560
|
+
violations.push(`sandbox file publication capability ${name} is missing or unsafe`);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3512
3565
|
for (const routine of posture.privateRoutines) {
|
|
3513
3566
|
if (routine.owner === expectedRole) {
|
|
3514
3567
|
violations.push(`runtime role owns private routine ${routine.name}`);
|
package/src/schema.ts
CHANGED
|
@@ -5512,6 +5512,36 @@ export const files = pgTable(
|
|
|
5512
5512
|
}),
|
|
5513
5513
|
);
|
|
5514
5514
|
|
|
5515
|
+
/** Immutable publication receipt; content and ownership remain in files. */
|
|
5516
|
+
export const sandboxFilePublications = opengeniPrivateSchema.table(
|
|
5517
|
+
"sandbox_file_publications",
|
|
5518
|
+
{
|
|
5519
|
+
accountId: uuid("account_id").notNull(),
|
|
5520
|
+
workspaceId: uuid("workspace_id").notNull(),
|
|
5521
|
+
fileId: uuid("file_id").notNull(),
|
|
5522
|
+
sourceSessionId: uuid("source_session_id"),
|
|
5523
|
+
publishedAt: timestamp("published_at", { withTimezone: true }).notNull().defaultNow(),
|
|
5524
|
+
},
|
|
5525
|
+
(table) => ({
|
|
5526
|
+
pk: primaryKey({
|
|
5527
|
+
name: "sandbox_file_publications_pk",
|
|
5528
|
+
columns: [table.accountId, table.workspaceId, table.fileId],
|
|
5529
|
+
}),
|
|
5530
|
+
file: foreignKey({
|
|
5531
|
+
name: "sandbox_file_publications_file_fk",
|
|
5532
|
+
columns: [table.accountId, table.workspaceId, table.fileId],
|
|
5533
|
+
foreignColumns: [files.accountId, files.workspaceId, files.id],
|
|
5534
|
+
}).onDelete("cascade"),
|
|
5535
|
+
// SQL owns the composite session FK with column-specific SET NULL.
|
|
5536
|
+
session: index("sandbox_file_publications_session_idx").on(
|
|
5537
|
+
table.workspaceId,
|
|
5538
|
+
table.sourceSessionId,
|
|
5539
|
+
table.publishedAt,
|
|
5540
|
+
table.fileId,
|
|
5541
|
+
),
|
|
5542
|
+
}),
|
|
5543
|
+
);
|
|
5544
|
+
|
|
5515
5545
|
export const fileUploads = pgTable(
|
|
5516
5546
|
"file_uploads",
|
|
5517
5547
|
{
|