@opengeni/db 5.0.1 → 6.0.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/automations.d.ts +0 -2
- package/dist/canonical-human-identities.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-components.d.ts +5 -13
- package/dist/capability-integrations.d.ts +23 -8
- package/dist/capability-reconciliation.d.ts +5 -0
- package/dist/capability-reconciliation.js +21 -0
- package/dist/capability-reconciliation.js.map +1 -0
- package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
- package/dist/chunk-4UJCXBR2.js.map +1 -0
- package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
- package/dist/chunk-AJA2ZIUW.js +219 -0
- package/dist/chunk-AJA2ZIUW.js.map +1 -0
- package/dist/chunk-CDAT6EKW.js +247 -0
- package/dist/chunk-CDAT6EKW.js.map +1 -0
- package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
- package/dist/chunk-CJI6G254.js.map +1 -0
- package/dist/chunk-G32NVMRJ.js +276 -0
- package/dist/chunk-G32NVMRJ.js.map +1 -0
- package/dist/chunk-GABXCG3N.js +5811 -0
- package/dist/chunk-GABXCG3N.js.map +1 -0
- package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
- package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
- package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
- package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
- package/dist/chunk-MN5PZMEK.js.map +1 -0
- package/dist/chunk-PJI4MUZ7.js +131 -0
- package/dist/chunk-PJI4MUZ7.js.map +1 -0
- package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
- package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
- package/dist/chunk-RL2MUXM3.js.map +1 -0
- package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
- package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
- package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
- package/dist/chunk-XIXVVBW2.js.map +1 -0
- package/dist/chunk-ZPJ6V2VG.js +156 -0
- package/dist/chunk-ZPJ6V2VG.js.map +1 -0
- package/dist/codex-capacity-recovery.d.ts +12 -0
- package/dist/codex-failure-eligibility.d.ts +9 -0
- package/dist/company-profile-schema.d.ts +4 -2
- package/dist/connect-attempts.d.ts +15 -0
- package/dist/connect-attempts.js +25 -0
- package/dist/connect-attempts.js.map +1 -0
- package/dist/connection-authority.d.ts +7 -63
- package/dist/connection-refresh-lock.d.ts +4 -0
- package/dist/connection-token-resolver.d.ts +8 -43
- package/dist/database-timing.d.ts +10 -0
- package/dist/database.d.ts +4 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +9 -0
- package/dist/editable-artifacts.js +7 -5
- package/dist/index.d.ts +447 -78
- package/dist/index.js +12705 -12872
- package/dist/index.js.map +1 -1
- package/dist/insights-usage-bundle.d.ts +3 -0
- package/dist/integration-bindings.d.ts +1 -1
- package/dist/integration-facets.d.ts +18 -1
- package/dist/knowledge-entries-schema.d.ts +2 -0
- package/dist/knowledge-entries.d.ts +1 -0
- package/dist/knowledge-indexing.d.ts +1 -0
- package/dist/live-session-attempt.d.ts +35 -137
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-account-bindings.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +11 -0
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.js +1 -1
- package/dist/organization-integration-policy.d.ts +51 -0
- package/dist/organization-integration-policy.js +18 -0
- package/dist/organization-integration-policy.js.map +1 -0
- package/dist/organization-usage.d.ts +20 -0
- package/dist/plugin-packages.d.ts +9 -12
- package/dist/pr-review.d.ts +0 -11
- package/dist/preference-registry-schema.d.ts +17 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.d.ts +124 -6
- package/dist/retained-provider-commands.js +29 -97
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/retired-session-create-metadata.d.ts +4 -0
- package/dist/runtime-posture.d.ts +7 -5
- package/dist/schema.d.ts +876 -656
- package/dist/schema.js +5 -7
- package/dist/session-admission-block.d.ts +32 -0
- package/dist/session-attempt-writers.d.ts +4 -0
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -126
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-control.d.ts +5 -1
- package/dist/session-event-slices.d.ts +10 -0
- package/dist/session-event-slices.js +8 -222
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-file-attachments.d.ts +28 -0
- package/dist/session-goal-reports.d.ts +43 -0
- package/dist/session-mcp-approval.d.ts +4 -0
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-message-search.d.ts +22 -0
- package/dist/session-queue-commands.d.ts +2 -6
- package/dist/session-retry.d.ts +36 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/skill-source-release-impact.d.ts +18 -0
- package/dist/skill-source-release.d.ts +9 -0
- package/dist/skills.d.ts +7 -1
- package/dist/user-resource-authority.d.ts +0 -8
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
- package/drizzle/0469_knowledge_source_discovery.sql +85 -0
- package/drizzle/0470_organization_integration_policy.sql +151 -0
- package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
- package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
- package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
- package/drizzle/0474_goal_report_requirements.sql +156 -0
- package/drizzle/0475_session_event_history_statistics.sql +15 -0
- package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
- package/drizzle/0477_managed_sign_in_methods.sql +303 -0
- package/drizzle/0478_sender_owned_connections.sql +2772 -0
- package/drizzle/0479_connection_create_idempotency.sql +16 -0
- package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
- package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
- package/drizzle/0482_remove_packs.sql +417 -0
- package/drizzle/0483_preclaim_admission_block.sql +85 -0
- package/drizzle/0484_insights_usage_projection.sql +211 -0
- package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
- package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
- package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
- package/drizzle/0488_permanent_skill_removal.sql +207 -0
- package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
- package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
- package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
- package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
- package/drizzle/0493_stopping_command_error_containment.sql +58 -0
- package/drizzle/0494_mcp_account_bindings.sql +334 -0
- package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
- package/drizzle/0496_supervised_command_settlement.sql +386 -0
- package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
- package/drizzle/0498_enrollment_membership_fence.sql +87 -0
- package/drizzle/0499_session_attachment_access.sql +224 -0
- package/drizzle/0500_session_execution_epoch.sql +37 -0
- package/drizzle/0501_session_sharing_execution.sql +591 -0
- package/drizzle/0502_active_message_boundary_forks.sql +30 -0
- package/package.json +17 -5
- package/src/automations.ts +17 -73
- package/src/canonical-human-identities.ts +17 -5
- package/src/capability-components.ts +7 -24
- package/src/capability-integrations.ts +301 -21
- package/src/capability-reconciliation.ts +19 -0
- package/src/codex-capacity-recovery.ts +47 -0
- package/src/codex-failure-eligibility.ts +93 -0
- package/src/connect-attempts.ts +49 -18
- package/src/connection-authority.ts +12 -158
- package/src/connection-refresh-lock.ts +22 -0
- package/src/connection-token-resolver.ts +58 -530
- package/src/database-timing.ts +43 -0
- package/src/database.ts +61 -26
- package/src/editable-artifacts.ts +1 -1
- package/src/index.ts +3578 -1273
- package/src/insights-model-bundle.ts +19 -7
- package/src/insights-usage-bundle.ts +16 -19
- package/src/integration-bindings.ts +2 -7
- package/src/integration-facets.ts +73 -6
- package/src/knowledge-entries.ts +8 -1
- package/src/live-session-attempt.ts +0 -22
- package/src/mcp-account-bindings.ts +12 -0
- package/src/mcp-oauth.ts +26 -0
- package/src/organization-integration-policy.ts +235 -0
- package/src/organization-usage.ts +83 -0
- package/src/plugin-packages.ts +226 -42
- package/src/pr-review.ts +7 -69
- package/src/preference-registry-schema.ts +1 -0
- package/src/provision-roles.ts +50 -37
- package/src/retained-provider-commands.ts +286 -9
- package/src/retired-session-create-metadata.ts +16 -0
- package/src/runtime-posture.ts +217 -13
- package/src/schema.ts +104 -120
- package/src/session-admission-block.ts +29 -0
- package/src/session-attempt-writers.ts +48 -0
- package/src/session-control.ts +21 -5
- package/src/session-event-slices.ts +76 -26
- package/src/session-file-attachments.ts +66 -0
- package/src/session-goal-reports.ts +255 -0
- package/src/session-mcp-approval.ts +30 -0
- package/src/session-message-search.ts +348 -0
- package/src/session-queue-commands.ts +60 -36
- package/src/session-retry.ts +319 -0
- package/src/skill-config-migration.ts +11 -2
- package/src/skill-source-release-impact.ts +33 -0
- package/src/skill-source-release.ts +67 -26
- package/src/skills.ts +44 -6
- package/src/user-resource-authority.ts +0 -56
- package/dist/chunk-6T2GGYWE.js.map +0 -1
- package/dist/chunk-7HMK3B45.js.map +0 -1
- package/dist/chunk-DPONPYJD.js.map +0 -1
- package/dist/chunk-F73UKJY6.js.map +0 -1
- package/dist/chunk-T6YLHJLS.js.map +0 -1
- package/dist/host-mcp-binding-match.d.ts +0 -5
- package/dist/host-mcp-bindings.d.ts +0 -79
- package/dist/host-mcp-resolvers.d.ts +0 -47
- package/dist/host-mcp-task-authority.d.ts +0 -43
- package/dist/host-selection-identity.d.ts +0 -6
- package/dist/pack-components.d.ts +0 -83
- package/dist/pack-installations.d.ts +0 -86
- package/src/host-mcp-binding-match.ts +0 -34
- package/src/host-mcp-bindings.ts +0 -777
- package/src/host-mcp-resolvers.ts +0 -266
- package/src/host-mcp-task-authority.ts +0 -384
- package/src/host-selection-identity.ts +0 -29
- package/src/pack-components.ts +0 -980
- package/src/pack-installations.ts +0 -950
- /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
- /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
- /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Foundation only: new binaries must opt acquisition writers into the guard.
|
|
3
|
+
-- The runtime table/grant contract changes; old processes must be drained.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
DO $drain$
|
|
7
|
+
DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
|
|
8
|
+
BEGIN
|
|
9
|
+
IF roles IS NULL OR jsonb_typeof(roles) <> 'array' OR jsonb_array_length(roles) NOT BETWEEN 1 AND 16 THEN
|
|
10
|
+
RAISE EXCEPTION 'integration policy migration requires application database roles' USING ERRCODE = '55000';
|
|
11
|
+
END IF;
|
|
12
|
+
IF EXISTS (SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
|
|
13
|
+
OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63)
|
|
14
|
+
OR EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
|
|
15
|
+
WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
|
|
16
|
+
RAISE EXCEPTION 'integration policy migration requires valid stopped application roles' USING ERRCODE = '55000';
|
|
17
|
+
END IF;
|
|
18
|
+
END $drain$;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE organization_integration_policies (
|
|
21
|
+
account_id uuid PRIMARY KEY REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
22
|
+
mode text NOT NULL CHECK (mode IN ('unrestricted', 'restricted')),
|
|
23
|
+
allowed_integration_keys jsonb NOT NULL CHECK (jsonb_typeof(allowed_integration_keys) = 'array'),
|
|
24
|
+
revision bigint NOT NULL CHECK (revision BETWEEN 1 AND 9007199254740991)
|
|
25
|
+
);
|
|
26
|
+
CREATE TABLE organization_integration_policy_operations (
|
|
27
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
28
|
+
operation_id uuid NOT NULL,
|
|
29
|
+
actor_subject_id text NOT NULL,
|
|
30
|
+
request jsonb NOT NULL,
|
|
31
|
+
result jsonb NOT NULL,
|
|
32
|
+
PRIMARY KEY (account_id, operation_id)
|
|
33
|
+
);
|
|
34
|
+
ALTER TABLE organization_integration_policies ENABLE ROW LEVEL SECURITY;
|
|
35
|
+
ALTER TABLE organization_integration_policies FORCE ROW LEVEL SECURITY;
|
|
36
|
+
ALTER TABLE organization_integration_policy_operations ENABLE ROW LEVEL SECURITY;
|
|
37
|
+
ALTER TABLE organization_integration_policy_operations FORCE ROW LEVEL SECURITY;
|
|
38
|
+
REVOKE ALL ON organization_integration_policies, organization_integration_policy_operations FROM PUBLIC;
|
|
39
|
+
CREATE POLICY organization_integration_policy_account ON organization_integration_policies
|
|
40
|
+
USING (account_id = opengeni_private.current_account_id())
|
|
41
|
+
WITH CHECK (account_id = opengeni_private.current_account_id());
|
|
42
|
+
CREATE POLICY organization_integration_policy_operation_account ON organization_integration_policy_operations
|
|
43
|
+
USING (account_id = opengeni_private.current_account_id())
|
|
44
|
+
WITH CHECK (account_id = opengeni_private.current_account_id());
|
|
45
|
+
|
|
46
|
+
-- This is a live fence, not authentication. The application must verify canonical
|
|
47
|
+
-- administrator provenance before selecting the subject, including on replay.
|
|
48
|
+
CREATE FUNCTION opengeni_private.assert_organization_integration_policy_administrator(
|
|
49
|
+
p_account uuid, p_actor text
|
|
50
|
+
) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
51
|
+
DECLARE
|
|
52
|
+
previous_marker text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
53
|
+
permissions_value jsonb; member_id uuid;
|
|
54
|
+
BEGIN
|
|
55
|
+
IF p_account IS NULL OR p_actor IS NULL
|
|
56
|
+
OR p_account IS DISTINCT FROM opengeni_private.current_account_id()
|
|
57
|
+
OR p_actor IS DISTINCT FROM opengeni_private.current_subject_id() THEN
|
|
58
|
+
RAISE EXCEPTION 'integration policy scope invalid' USING ERRCODE = '42501';
|
|
59
|
+
END IF;
|
|
60
|
+
IF p_actor LIKE 'api_key:%' THEN
|
|
61
|
+
SELECT permissions INTO permissions_value FROM api_keys
|
|
62
|
+
WHERE account_id = p_account AND 'api_key:' || id::text = p_actor
|
|
63
|
+
AND credential_kind = 'organization' AND workspace_id IS NULL AND revoked_at IS NULL
|
|
64
|
+
AND (expires_at IS NULL OR expires_at > clock_timestamp()) FOR SHARE;
|
|
65
|
+
IF NOT coalesce(permissions_value ? 'workspace:admin', false) THEN
|
|
66
|
+
RAISE EXCEPTION 'organization administrator required' USING ERRCODE = '42501';
|
|
67
|
+
END IF;
|
|
68
|
+
ELSE
|
|
69
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'organization_private_session_settings', true);
|
|
70
|
+
SELECT id INTO member_id FROM organization_memberships
|
|
71
|
+
WHERE account_id = p_account AND subject_id = p_actor AND status = 'active' AND role IN ('owner','admin') FOR SHARE;
|
|
72
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker, ''), true);
|
|
73
|
+
IF member_id IS NULL THEN RAISE EXCEPTION 'organization administrator required' USING ERRCODE = '42501'; END IF;
|
|
74
|
+
END IF;
|
|
75
|
+
END $body$;
|
|
76
|
+
REVOKE ALL ON FUNCTION opengeni_private.assert_organization_integration_policy_administrator(uuid,text) FROM PUBLIC;
|
|
77
|
+
|
|
78
|
+
CREATE FUNCTION opengeni_private.update_organization_integration_policy(
|
|
79
|
+
p_account uuid, p_actor text, p_request jsonb
|
|
80
|
+
) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
81
|
+
DECLARE
|
|
82
|
+
prior organization_integration_policy_operations%ROWTYPE;
|
|
83
|
+
current_revision bigint; expected_revision bigint; operation_value uuid; result_value jsonb;
|
|
84
|
+
policy_lock bigint := hashtextextended('organization-integration-policy:' || p_account::text, 0);
|
|
85
|
+
BEGIN
|
|
86
|
+
IF p_account IS NULL OR p_actor IS NULL
|
|
87
|
+
OR p_account IS DISTINCT FROM opengeni_private.current_account_id()
|
|
88
|
+
OR p_actor IS DISTINCT FROM opengeni_private.current_subject_id() THEN
|
|
89
|
+
RAISE EXCEPTION 'integration policy scope invalid' USING ERRCODE = '42501';
|
|
90
|
+
END IF;
|
|
91
|
+
IF jsonb_typeof(p_request) IS DISTINCT FROM 'object'
|
|
92
|
+
OR p_request ->> 'mode' IS NULL OR p_request ->> 'mode' NOT IN ('unrestricted', 'restricted')
|
|
93
|
+
OR jsonb_typeof(p_request -> 'allowedIntegrationKeys') IS DISTINCT FROM 'array'
|
|
94
|
+
OR jsonb_typeof(p_request -> 'expectedRevision') IS DISTINCT FROM 'number'
|
|
95
|
+
OR coalesce(p_request ->> 'expectedRevision', '') !~ '^[0-9]+$'
|
|
96
|
+
OR jsonb_typeof(p_request -> 'operationId') IS DISTINCT FROM 'string'
|
|
97
|
+
OR p_request - ARRAY['mode','allowedIntegrationKeys','expectedRevision','operationId'] <> '{}'::jsonb THEN
|
|
98
|
+
RAISE EXCEPTION 'integration policy request invalid' USING ERRCODE = '22023';
|
|
99
|
+
END IF;
|
|
100
|
+
expected_revision := (p_request ->> 'expectedRevision')::bigint;
|
|
101
|
+
operation_value := (p_request ->> 'operationId')::uuid;
|
|
102
|
+
IF expected_revision NOT BETWEEN 0 AND 9007199254740990
|
|
103
|
+
OR jsonb_array_length(p_request -> 'allowedIntegrationKeys') > 10000
|
|
104
|
+
OR EXISTS (SELECT 1 FROM jsonb_array_elements(p_request -> 'allowedIntegrationKeys') k
|
|
105
|
+
WHERE jsonb_typeof(k) <> 'string' OR length(k #>> '{}') NOT BETWEEN 1 AND 200
|
|
106
|
+
OR k #>> '{}' !~ '^[a-z0-9][a-z0-9._:-]*$')
|
|
107
|
+
OR (SELECT count(*) <> count(DISTINCT k) FROM jsonb_array_elements(p_request -> 'allowedIntegrationKeys') k) THEN
|
|
108
|
+
RAISE EXCEPTION 'integration policy request invalid' USING ERRCODE = '22023';
|
|
109
|
+
END IF;
|
|
110
|
+
-- Never upgrade a shared acquisition fence: two upgrading acquisitions deadlock.
|
|
111
|
+
-- This also detects a shared fence retained by a released nested savepoint.
|
|
112
|
+
IF EXISTS (SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid()
|
|
113
|
+
AND mode = 'ShareLock' AND granted AND objsubid = 1
|
|
114
|
+
AND classid = ((policy_lock >> 32) & 4294967295)::oid
|
|
115
|
+
AND objid = (policy_lock & 4294967295)::oid) THEN
|
|
116
|
+
RAISE EXCEPTION 'policy mutation cannot upgrade an acquisition fence' USING ERRCODE = '55000';
|
|
117
|
+
END IF;
|
|
118
|
+
-- Policy BEFORE membership: acquisition callbacks such as personal OAuth take
|
|
119
|
+
-- membership themselves. The inverse order cycles with their shared policy lock.
|
|
120
|
+
-- No workspace row is required or locked by this organization-only operation.
|
|
121
|
+
PERFORM pg_advisory_xact_lock(policy_lock);
|
|
122
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:' || p_account::text, 0));
|
|
123
|
+
PERFORM opengeni_private.assert_organization_integration_policy_administrator(p_account, p_actor);
|
|
124
|
+
SELECT * INTO prior FROM organization_integration_policy_operations
|
|
125
|
+
WHERE account_id = p_account AND operation_id = operation_value;
|
|
126
|
+
IF FOUND THEN
|
|
127
|
+
IF prior.actor_subject_id <> p_actor OR prior.request <> p_request THEN
|
|
128
|
+
RAISE EXCEPTION 'integration policy operation reused' USING ERRCODE = '23505';
|
|
129
|
+
END IF;
|
|
130
|
+
RETURN prior.result;
|
|
131
|
+
END IF;
|
|
132
|
+
SELECT revision INTO current_revision FROM organization_integration_policies WHERE account_id = p_account;
|
|
133
|
+
IF coalesce(current_revision, 0) <> expected_revision THEN
|
|
134
|
+
RAISE EXCEPTION 'integration policy revision conflict' USING ERRCODE = '40001';
|
|
135
|
+
END IF;
|
|
136
|
+
result_value := jsonb_build_object('mode', p_request -> 'mode', 'allowedIntegrationKeys', p_request -> 'allowedIntegrationKeys', 'revision', expected_revision + 1);
|
|
137
|
+
INSERT INTO organization_integration_policies (account_id, mode, allowed_integration_keys, revision)
|
|
138
|
+
VALUES (p_account, p_request ->> 'mode', p_request -> 'allowedIntegrationKeys', expected_revision + 1)
|
|
139
|
+
ON CONFLICT (account_id) DO UPDATE SET mode = excluded.mode,
|
|
140
|
+
allowed_integration_keys = excluded.allowed_integration_keys, revision = excluded.revision;
|
|
141
|
+
INSERT INTO organization_integration_policy_operations (account_id, operation_id, actor_subject_id, request, result)
|
|
142
|
+
VALUES (p_account, operation_value, p_actor, p_request, result_value);
|
|
143
|
+
RETURN result_value;
|
|
144
|
+
END $body$;
|
|
145
|
+
REVOKE ALL ON FUNCTION opengeni_private.update_organization_integration_policy(uuid,text,jsonb) FROM PUBLIC;
|
|
146
|
+
-- Explicit pg_temp LAST prevents temporary relation shadowing in the definer.
|
|
147
|
+
DO $safe_path$
|
|
148
|
+
BEGIN
|
|
149
|
+
EXECUTE format('ALTER FUNCTION opengeni_private.update_organization_integration_policy(uuid,text,jsonb) SET search_path = pg_catalog, %I, pg_temp', current_schema());
|
|
150
|
+
EXECUTE format('ALTER FUNCTION opengeni_private.assert_organization_integration_policy_administrator(uuid,text) SET search_path = pg_catalog, %I, pg_temp', current_schema());
|
|
151
|
+
END $safe_path$;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Exact account-scoped reporting, retaining FORCE RLS and actual actor scope.
|
|
3
|
+
-- Only this aggregate function may mint the short-lived owner capability. It
|
|
4
|
+
-- never returns raw facts and does not change any write/tenant/session policy.
|
|
5
|
+
SET LOCAL lock_timeout = '5s';
|
|
6
|
+
SET LOCAL statement_timeout = '10min';
|
|
7
|
+
|
|
8
|
+
CREATE TABLE opengeni_private.organization_usage_read_capabilities (
|
|
9
|
+
backend_pid integer NOT NULL,
|
|
10
|
+
transaction_id xid8 NOT NULL,
|
|
11
|
+
account_id uuid NOT NULL,
|
|
12
|
+
subject_id text,
|
|
13
|
+
initiating_human_subject_id text,
|
|
14
|
+
PRIMARY KEY (backend_pid, transaction_id)
|
|
15
|
+
);
|
|
16
|
+
REVOKE ALL ON TABLE opengeni_private.organization_usage_read_capabilities FROM PUBLIC;
|
|
17
|
+
|
|
18
|
+
-- Strip hostile owner defaults, not merely the nominal application role.
|
|
19
|
+
DO $table_acl$
|
|
20
|
+
DECLARE role_name text;
|
|
21
|
+
BEGIN
|
|
22
|
+
FOR role_name IN
|
|
23
|
+
SELECT DISTINCT role_row.rolname
|
|
24
|
+
FROM pg_catalog.pg_class relation
|
|
25
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(coalesce(relation.relacl,
|
|
26
|
+
pg_catalog.acldefault('r', relation.relowner))) privilege
|
|
27
|
+
JOIN pg_catalog.pg_roles role_row ON role_row.oid = privilege.grantee
|
|
28
|
+
WHERE relation.oid = 'opengeni_private.organization_usage_read_capabilities'::regclass
|
|
29
|
+
AND privilege.grantee <> relation.relowner
|
|
30
|
+
LOOP
|
|
31
|
+
EXECUTE format('REVOKE ALL ON TABLE opengeni_private.organization_usage_read_capabilities FROM %I', role_name);
|
|
32
|
+
END LOOP;
|
|
33
|
+
END
|
|
34
|
+
$table_acl$;
|
|
35
|
+
|
|
36
|
+
DO $functions$
|
|
37
|
+
DECLARE data_schema text := pg_catalog.current_schema();
|
|
38
|
+
BEGIN
|
|
39
|
+
EXECUTE format($ddl$
|
|
40
|
+
CREATE FUNCTION %1$I.organization_usage_policy_capability_active(p_actor text)
|
|
41
|
+
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER
|
|
42
|
+
SET search_path = pg_catalog
|
|
43
|
+
AS $fn$
|
|
44
|
+
SELECT p_actor = pg_catalog.pg_get_userbyid((
|
|
45
|
+
SELECT relowner FROM pg_catalog.pg_class
|
|
46
|
+
WHERE oid = %2$L::pg_catalog.regclass
|
|
47
|
+
)) AND nullif(pg_catalog.current_setting('opengeni.workspace_id', true), '') IS NULL
|
|
48
|
+
AND EXISTS (
|
|
49
|
+
SELECT 1 FROM opengeni_private.organization_usage_read_capabilities capability
|
|
50
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
51
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
52
|
+
AND capability.account_id::text IS NOT DISTINCT FROM nullif(pg_catalog.current_setting('opengeni.account_id', true), '')
|
|
53
|
+
AND capability.subject_id IS NOT DISTINCT FROM nullif(pg_catalog.current_setting('opengeni.subject_id', true), '')
|
|
54
|
+
AND capability.initiating_human_subject_id IS NOT DISTINCT FROM nullif(pg_catalog.current_setting('opengeni.initiating_human_subject_id', true), '')
|
|
55
|
+
)
|
|
56
|
+
$fn$;
|
|
57
|
+
REVOKE ALL ON FUNCTION %1$I.organization_usage_policy_capability_active(text) FROM PUBLIC;
|
|
58
|
+
-- RLS plans check function ACLs before short circuiting. This reveals only
|
|
59
|
+
-- a same-backend boolean; callers cannot insert its capability row.
|
|
60
|
+
GRANT EXECUTE ON FUNCTION %1$I.organization_usage_policy_capability_active(text) TO PUBLIC;
|
|
61
|
+
|
|
62
|
+
CREATE FUNCTION opengeni_private.organization_usage_summary(
|
|
63
|
+
p_account_id uuid, p_since timestamptz, p_until timestamptz,
|
|
64
|
+
p_granularity text, p_after_workspace_id uuid, p_include_period boolean
|
|
65
|
+
) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER
|
|
66
|
+
SET search_path = pg_catalog, %1$I, opengeni_private, pg_temp
|
|
67
|
+
SET plan_cache_mode = force_custom_plan
|
|
68
|
+
AS $fn$
|
|
69
|
+
DECLARE
|
|
70
|
+
context_account_id uuid;
|
|
71
|
+
context_subject_id text;
|
|
72
|
+
context_human_id text;
|
|
73
|
+
page_ids uuid[];
|
|
74
|
+
next_cursor uuid;
|
|
75
|
+
response jsonb;
|
|
76
|
+
BEGIN
|
|
77
|
+
PERFORM opengeni_private.session_variable_set_attachments_protocol_v1_active();
|
|
78
|
+
BEGIN
|
|
79
|
+
context_account_id := nullif(current_setting('opengeni.account_id', true), '')::uuid;
|
|
80
|
+
EXCEPTION WHEN invalid_text_representation THEN
|
|
81
|
+
RAISE EXCEPTION 'Organization usage account context is malformed' USING ERRCODE = '42501';
|
|
82
|
+
END;
|
|
83
|
+
context_subject_id := nullif(current_setting('opengeni.subject_id', true), '');
|
|
84
|
+
context_human_id := nullif(current_setting('opengeni.initiating_human_subject_id', true), '');
|
|
85
|
+
IF context_account_id IS NULL OR context_account_id IS DISTINCT FROM p_account_id
|
|
86
|
+
OR nullif(current_setting('opengeni.workspace_id', true), '') IS NOT NULL
|
|
87
|
+
THEN
|
|
88
|
+
RAISE EXCEPTION 'Organization usage requires the exact account-only context' USING ERRCODE = '42501';
|
|
89
|
+
END IF;
|
|
90
|
+
IF p_since IS NULL OR p_until IS NULL OR NOT isfinite(p_since) OR NOT isfinite(p_until)
|
|
91
|
+
OR p_until < p_since OR p_until - p_since > interval '366 days'
|
|
92
|
+
OR p_granularity IS NULL OR p_granularity NOT IN ('hour', 'day')
|
|
93
|
+
OR (p_granularity = 'hour' AND p_until - p_since > interval '1 day')
|
|
94
|
+
OR p_include_period IS NULL
|
|
95
|
+
THEN
|
|
96
|
+
RAISE EXCEPTION 'Organization usage window or granularity is invalid' USING ERRCODE = '22023';
|
|
97
|
+
END IF;
|
|
98
|
+
-- Match listSharedWorkspacesForAccount: canonical membership pointers
|
|
99
|
+
-- exclude EVERY Personal workspace before lookahead/cursor selection.
|
|
100
|
+
-- Names, kind guesses and caller-owned Personal exceptions are forbidden.
|
|
101
|
+
-- Period totals remain actor-visible accounting across the account;
|
|
102
|
+
-- this shared-only inventory deliberately need not sum to those totals.
|
|
103
|
+
SELECT coalesce(array_agg(id ORDER BY id), '{}'::uuid[]) INTO page_ids
|
|
104
|
+
FROM (SELECT id FROM %1$I.workspaces
|
|
105
|
+
WHERE account_id = context_account_id
|
|
106
|
+
AND id IN (SELECT workspace_id FROM %1$I.list_organization_workspace_ids(context_account_id))
|
|
107
|
+
AND (p_after_workspace_id IS NULL OR id > p_after_workspace_id)
|
|
108
|
+
ORDER BY id LIMIT 51) page;
|
|
109
|
+
IF cardinality(page_ids) > 50 THEN
|
|
110
|
+
next_cursor := page_ids[50];
|
|
111
|
+
page_ids := page_ids[1:50];
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
INSERT INTO opengeni_private.organization_usage_read_capabilities
|
|
115
|
+
(backend_pid, transaction_id, account_id, subject_id, initiating_human_subject_id)
|
|
116
|
+
VALUES (pg_backend_pid(), pg_current_xact_id(), context_account_id, context_subject_id, context_human_id);
|
|
117
|
+
BEGIN
|
|
118
|
+
WITH visible_sessions AS MATERIALIZED (
|
|
119
|
+
-- Sessions retain ordinary FORCE RLS. Private actor checks occur
|
|
120
|
+
-- once per session here, never once per fact in the aggregate scan.
|
|
121
|
+
SELECT id, account_id, workspace_id FROM %1$I.sessions session_row
|
|
122
|
+
WHERE account_id = context_account_id
|
|
123
|
+
AND (p_include_period OR workspace_id = ANY(page_ids))
|
|
124
|
+
AND (context_subject_id IS NULL OR visibility = 'workspace_shared'
|
|
125
|
+
OR %1$I.session_private_actor_visible(account_id, workspace_id,
|
|
126
|
+
owner_organization_membership_id, owner_subject_id))
|
|
127
|
+
), visible AS MATERIALIZED (
|
|
128
|
+
SELECT usage_row.workspace_id, usage_row.event_type, usage_row.unit, usage_row.quantity,
|
|
129
|
+
CASE WHEN p_include_period THEN to_char(date_trunc(p_granularity, usage_row.occurred_at AT TIME ZONE 'UTC'),
|
|
130
|
+
CASE WHEN p_granularity = 'hour' THEN 'YYYY-MM-DD"T"HH24:00' ELSE 'YYYY-MM-DD' END) END AS bucket
|
|
131
|
+
FROM %1$I.usage_events usage_row
|
|
132
|
+
LEFT JOIN visible_sessions session_row
|
|
133
|
+
ON session_row.id = usage_row.session_id
|
|
134
|
+
AND session_row.account_id = usage_row.account_id
|
|
135
|
+
AND session_row.workspace_id = usage_row.workspace_id
|
|
136
|
+
WHERE usage_row.account_id = context_account_id
|
|
137
|
+
AND usage_row.occurred_at >= p_since AND usage_row.occurred_at < p_until
|
|
138
|
+
AND (p_include_period OR usage_row.workspace_id = ANY(page_ids))
|
|
139
|
+
AND (usage_row.session_id IS NULL OR session_row.id IS NOT NULL)
|
|
140
|
+
), aggregates AS MATERIALIZED (
|
|
141
|
+
SELECT workspace_id, bucket, grouping(workspace_id) AS all_workspaces,
|
|
142
|
+
grouping(bucket) AS all_buckets,
|
|
143
|
+
jsonb_build_object('eventType', event_type, 'unit', unit,
|
|
144
|
+
'quantity', sum(quantity)::text, 'eventCount', count(*)::text) AS total
|
|
145
|
+
FROM visible GROUP BY GROUPING SETS
|
|
146
|
+
((event_type, unit), (bucket, event_type, unit), (workspace_id, event_type, unit))
|
|
147
|
+
), bucket_rows AS (
|
|
148
|
+
SELECT bucket, jsonb_agg(total ORDER BY total->>'eventType', total->>'unit') AS totals
|
|
149
|
+
FROM aggregates WHERE all_buckets = 0 AND p_include_period GROUP BY bucket
|
|
150
|
+
), workspace_rows AS (
|
|
151
|
+
SELECT workspace_id, jsonb_agg(total ORDER BY total->>'eventType', total->>'unit') AS totals
|
|
152
|
+
FROM aggregates WHERE all_workspaces = 0 AND workspace_id = ANY(page_ids) GROUP BY workspace_id
|
|
153
|
+
)
|
|
154
|
+
SELECT jsonb_build_object(
|
|
155
|
+
'totals', CASE WHEN p_include_period THEN coalesce((SELECT jsonb_agg(total ORDER BY total->>'eventType', total->>'unit')
|
|
156
|
+
FROM aggregates WHERE all_workspaces = 1 AND all_buckets = 1), '[]'::jsonb) ELSE NULL END,
|
|
157
|
+
'buckets', CASE WHEN p_include_period THEN coalesce((SELECT jsonb_agg(jsonb_build_object('bucket', bucket, 'totals', totals) ORDER BY bucket)
|
|
158
|
+
FROM bucket_rows), '[]'::jsonb) ELSE NULL END,
|
|
159
|
+
'workspaces', coalesce((SELECT jsonb_agg(jsonb_build_object('workspaceId', w.id, 'name', w.name,
|
|
160
|
+
'totals', coalesce(r.totals, '[]'::jsonb)) ORDER BY w.id)
|
|
161
|
+
FROM %1$I.workspaces w LEFT JOIN workspace_rows r ON r.workspace_id = w.id
|
|
162
|
+
WHERE w.account_id = context_account_id AND w.id = ANY(page_ids)), '[]'::jsonb),
|
|
163
|
+
'nextWorkspaceCursor', next_cursor
|
|
164
|
+
) INTO response;
|
|
165
|
+
|
|
166
|
+
DELETE FROM opengeni_private.organization_usage_read_capabilities
|
|
167
|
+
WHERE backend_pid = pg_backend_pid() AND transaction_id = pg_current_xact_id_if_assigned();
|
|
168
|
+
RETURN response;
|
|
169
|
+
EXCEPTION WHEN OTHERS THEN
|
|
170
|
+
DELETE FROM opengeni_private.organization_usage_read_capabilities
|
|
171
|
+
WHERE backend_pid = pg_backend_pid() AND transaction_id = pg_current_xact_id_if_assigned();
|
|
172
|
+
RAISE;
|
|
173
|
+
END;
|
|
174
|
+
END
|
|
175
|
+
$fn$;
|
|
176
|
+
REVOKE ALL ON FUNCTION opengeni_private.organization_usage_summary(uuid,timestamptz,timestamptz,text,uuid,boolean) FROM PUBLIC;
|
|
177
|
+
$ddl$, data_schema, format('%I.usage_events', data_schema));
|
|
178
|
+
END
|
|
179
|
+
$functions$;
|
|
180
|
+
|
|
181
|
+
DO $policy$
|
|
182
|
+
DECLARE data_schema text := current_schema(); installed record; expected_expression text;
|
|
183
|
+
BEGIN
|
|
184
|
+
SELECT p.polpermissive, p.polcmd, pg_get_expr(p.polqual, p.polrelid) AS expression INTO installed
|
|
185
|
+
FROM pg_policy p WHERE p.polrelid = format('%I.usage_events', data_schema)::regclass
|
|
186
|
+
AND p.polname = 'session_visibility_isolation';
|
|
187
|
+
IF NOT FOUND OR installed.polpermissive OR installed.polcmd <> 'r'
|
|
188
|
+
OR installed.expression IS NULL
|
|
189
|
+
THEN
|
|
190
|
+
RAISE EXCEPTION 'Organization usage refuses unknown fact visibility policy' USING ERRCODE = '55000';
|
|
191
|
+
END IF;
|
|
192
|
+
-- Compare parsed policy trees, not substring heuristics. This temporary,
|
|
193
|
+
-- equally restrictive policy is dropped before the atomic migration ends.
|
|
194
|
+
-- A future visibility change must be reconciled explicitly rather than
|
|
195
|
+
-- silently replaced by a capability for a different authorization rule.
|
|
196
|
+
EXECUTE format('CREATE POLICY organization_usage_expected_visibility ON %1$I.usage_events AS RESTRICTIVE FOR SELECT USING (
|
|
197
|
+
CASE WHEN %1$I.insights_fact_read_policy_capability_active(current_user,
|
|
198
|
+
pg_catalog.pg_get_userbyid((SELECT relation.relowner FROM pg_catalog.pg_class relation WHERE relation.oid = %2$L::pg_catalog.regclass)),
|
|
199
|
+
''usage_events'') THEN true ELSE %1$I.session_reference_visible(account_id, workspace_id, session_id) END)',
|
|
200
|
+
data_schema, format('%I.usage_events', data_schema));
|
|
201
|
+
SELECT pg_get_expr(p.polqual, p.polrelid) INTO expected_expression FROM pg_policy p
|
|
202
|
+
WHERE p.polrelid = format('%I.usage_events', data_schema)::regclass
|
|
203
|
+
AND p.polname = 'organization_usage_expected_visibility';
|
|
204
|
+
EXECUTE format('DROP POLICY organization_usage_expected_visibility ON %I.usage_events', data_schema);
|
|
205
|
+
IF installed.expression IS DISTINCT FROM expected_expression THEN
|
|
206
|
+
RAISE EXCEPTION 'Organization usage refuses changed fact visibility semantics' USING ERRCODE = '55000';
|
|
207
|
+
END IF;
|
|
208
|
+
-- The cheap owner-capability initPlan comes first; normal callers retain the
|
|
209
|
+
-- complete existing expression. Only SELECT changes; all writes stay intact.
|
|
210
|
+
EXECUTE format('ALTER POLICY session_visibility_isolation ON %I.usage_events USING (CASE WHEN (SELECT %I.organization_usage_policy_capability_active(current_user)) THEN true ELSE (%s) END)',
|
|
211
|
+
data_schema, data_schema, installed.expression);
|
|
212
|
+
END
|
|
213
|
+
$policy$;
|
|
214
|
+
|
|
215
|
+
DO $function_acl$
|
|
216
|
+
DECLARE role_name text; data_schema text := current_schema();
|
|
217
|
+
BEGIN
|
|
218
|
+
FOR role_name IN
|
|
219
|
+
SELECT DISTINCT r.rolname FROM pg_proc p
|
|
220
|
+
CROSS JOIN LATERAL aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) privilege
|
|
221
|
+
JOIN pg_roles r ON r.oid = privilege.grantee
|
|
222
|
+
WHERE p.oid = 'opengeni_private.organization_usage_summary(uuid,timestamptz,timestamptz,text,uuid,boolean)'::regprocedure
|
|
223
|
+
AND privilege.grantee <> p.proowner
|
|
224
|
+
LOOP
|
|
225
|
+
EXECUTE format('REVOKE ALL ON FUNCTION opengeni_private.organization_usage_summary(uuid,timestamptz,timestamptz,text,uuid,boolean) FROM %I', role_name);
|
|
226
|
+
END LOOP;
|
|
227
|
+
FOR role_name IN
|
|
228
|
+
SELECT r.rolname FROM jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) configured(value)
|
|
229
|
+
JOIN pg_roles r ON r.rolname = configured.value
|
|
230
|
+
WHERE has_table_privilege(r.rolname, format('%I.usage_events', data_schema), 'SELECT')
|
|
231
|
+
AND has_table_privilege(r.rolname, format('%I.sessions', data_schema), 'SELECT')
|
|
232
|
+
LOOP
|
|
233
|
+
EXECUTE format('GRANT USAGE ON SCHEMA opengeni_private TO %I', role_name);
|
|
234
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION opengeni_private.organization_usage_summary(uuid,timestamptz,timestamptz,text,uuid,boolean) TO %I', role_name);
|
|
235
|
+
END LOOP;
|
|
236
|
+
END
|
|
237
|
+
$function_acl$;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Rolling-compatible storage fences. Existing non-report goals are unchanged.
|
|
3
|
+
-- No caller boolean can waive a persisted requirement, including old binaries.
|
|
4
|
+
-- Trust boundary: authenticated agent/API callers can supply receipt IDs, never
|
|
5
|
+
-- receipt bodies. Trusted application code records proof after native query and
|
|
6
|
+
-- decode. The runtime SQL role can INSERT these rows and is inside that trust
|
|
7
|
+
-- boundary; these guards are retention/integrity checks, not an attestation
|
|
8
|
+
-- against compromised application-role SQL or database administrators.
|
|
9
|
+
CREATE FUNCTION session_goal_report_requirements_before_write()
|
|
10
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
11
|
+
DECLARE requirements jsonb := coalesce(NEW.metadata -> 'reportRequirementsV1', '[]'::jsonb);
|
|
12
|
+
DECLARE previous_requirements jsonb;
|
|
13
|
+
DECLARE deliveries jsonb;
|
|
14
|
+
DECLARE requirement jsonb;
|
|
15
|
+
DECLARE delivery jsonb;
|
|
16
|
+
DECLARE artifact record;
|
|
17
|
+
BEGIN
|
|
18
|
+
IF jsonb_typeof(requirements) <> 'array' OR jsonb_array_length(requirements) > 16 THEN
|
|
19
|
+
RAISE EXCEPTION 'Invalid goal report requirements' USING ERRCODE = '23514';
|
|
20
|
+
END IF;
|
|
21
|
+
FOR requirement IN SELECT value FROM jsonb_array_elements(requirements) LOOP
|
|
22
|
+
IF jsonb_typeof(requirement) <> 'object'
|
|
23
|
+
OR (SELECT count(*) FROM jsonb_object_keys(requirement)) <> 2
|
|
24
|
+
OR jsonb_typeof(requirement -> 'id') IS DISTINCT FROM 'string'
|
|
25
|
+
OR (requirement ->> 'id') !~ '^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$'
|
|
26
|
+
OR jsonb_typeof(requirement -> 'title') IS DISTINCT FROM 'string'
|
|
27
|
+
OR octet_length(requirement ->> 'title') NOT BETWEEN 1 AND 512
|
|
28
|
+
OR btrim(requirement ->> 'title') = ''
|
|
29
|
+
THEN RAISE EXCEPTION 'Invalid goal report requirement' USING ERRCODE = '23514'; END IF;
|
|
30
|
+
END LOOP;
|
|
31
|
+
IF (SELECT count(DISTINCT value ->> 'id') FROM jsonb_array_elements(requirements)) <> jsonb_array_length(requirements) THEN
|
|
32
|
+
RAISE EXCEPTION 'Duplicate goal report requirement IDs' USING ERRCODE = '23514';
|
|
33
|
+
END IF;
|
|
34
|
+
IF TG_OP = 'UPDATE' AND NOT (OLD.status = 'completed' AND NEW.status = 'active') THEN
|
|
35
|
+
previous_requirements := coalesce(OLD.metadata -> 'reportRequirementsV1', '[]'::jsonb);
|
|
36
|
+
IF NOT requirements @> previous_requirements THEN
|
|
37
|
+
RAISE EXCEPTION 'Goal report requirements are append-only until completed-goal replacement' USING ERRCODE = '23514';
|
|
38
|
+
END IF;
|
|
39
|
+
IF OLD.status = 'completed' AND (requirements <> previous_requirements OR NEW.metadata -> 'reportDeliveriesV1' IS DISTINCT FROM OLD.metadata -> 'reportDeliveriesV1') THEN
|
|
40
|
+
RAISE EXCEPTION 'Completed report requirements and deliveries are immutable' USING ERRCODE = '23514';
|
|
41
|
+
END IF;
|
|
42
|
+
END IF;
|
|
43
|
+
IF TG_OP = 'UPDATE' AND OLD.status = 'completed' AND NEW.status = 'active' THEN
|
|
44
|
+
-- Old rolling writers retain metadata on replacement. They must never
|
|
45
|
+
-- reuse a previous goal's verified delivery to complete the new goal.
|
|
46
|
+
NEW.metadata := jsonb_set(NEW.metadata, '{reportDeliveriesV1}', '[]'::jsonb);
|
|
47
|
+
END IF;
|
|
48
|
+
IF NEW.status = 'completed' AND (TG_OP = 'INSERT' OR OLD.status <> 'completed') AND jsonb_array_length(requirements) > 0 THEN
|
|
49
|
+
deliveries := coalesce(NEW.metadata -> 'reportDeliveriesV1', '[]'::jsonb);
|
|
50
|
+
IF jsonb_typeof(deliveries) <> 'array' OR jsonb_array_length(deliveries) <> jsonb_array_length(requirements)
|
|
51
|
+
OR (SELECT count(DISTINCT value ->> 'requirementId') FROM jsonb_array_elements(deliveries)) <> jsonb_array_length(requirements)
|
|
52
|
+
THEN RAISE EXCEPTION 'Missing report deliveries' USING ERRCODE = '23514'; END IF;
|
|
53
|
+
-- Same no-wait lock policy as the application: artifact writers already
|
|
54
|
+
-- lock the aggregate before checking their actor's session authority.
|
|
55
|
+
FOR delivery IN SELECT value FROM jsonb_array_elements(deliveries) ORDER BY value ->> 'artifactId' LOOP
|
|
56
|
+
IF NOT EXISTS (SELECT 1 FROM jsonb_array_elements(requirements) r WHERE r ->> 'id' = delivery ->> 'requirementId') THEN
|
|
57
|
+
RAISE EXCEPTION 'Unknown report requirement' USING ERRCODE = '23514';
|
|
58
|
+
END IF;
|
|
59
|
+
SELECT head_sequence, state_hash, authorization_revision INTO artifact FROM editable_artifacts
|
|
60
|
+
WHERE account_id = NEW.account_id AND workspace_id = NEW.workspace_id
|
|
61
|
+
AND id = delivery ->> 'artifactId' AND modality = 'document' AND lifecycle_state = 'active'
|
|
62
|
+
FOR SHARE NOWAIT;
|
|
63
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Report document unavailable' USING ERRCODE = '23514'; END IF;
|
|
64
|
+
IF NOT EXISTS (
|
|
65
|
+
SELECT 1 FROM session_command_receipts receipt
|
|
66
|
+
WHERE receipt.id::text = delivery ->> 'inspectionReceiptId'
|
|
67
|
+
AND receipt.account_id = NEW.account_id AND receipt.workspace_id = NEW.workspace_id
|
|
68
|
+
AND receipt.target_session_id = NEW.session_id
|
|
69
|
+
AND receipt.actor_type = 'agent_attempt' AND receipt.action = 'artifact.document.inspect'
|
|
70
|
+
AND receipt.result ->> 'version' = 'native-document-inspection.v1'
|
|
71
|
+
AND receipt.result ->> 'queryKind' = 'body'
|
|
72
|
+
AND receipt.result ->> 'artifactId' = delivery ->> 'artifactId'
|
|
73
|
+
AND receipt.result ->> 'headSequence' = artifact.head_sequence::text
|
|
74
|
+
AND receipt.result ->> 'stateHash' = artifact.state_hash
|
|
75
|
+
AND receipt.result ->> 'authorizationRevision' = artifact.authorization_revision::text
|
|
76
|
+
) THEN RAISE EXCEPTION 'Missing or stale document inspection proof' USING ERRCODE = '23514'; END IF;
|
|
77
|
+
END LOOP;
|
|
78
|
+
END IF;
|
|
79
|
+
RETURN NEW;
|
|
80
|
+
END;
|
|
81
|
+
$$;
|
|
82
|
+
|
|
83
|
+
CREATE TRIGGER session_goal_report_requirements_before_write_trigger
|
|
84
|
+
BEFORE INSERT OR UPDATE OF metadata, status ON session_goals
|
|
85
|
+
FOR EACH ROW EXECUTE FUNCTION session_goal_report_requirements_before_write();
|
|
86
|
+
|
|
87
|
+
-- Runs after the existing snapshot freeze trigger, without rewriting old turns.
|
|
88
|
+
CREATE FUNCTION session_goal_freeze_report_requirements()
|
|
89
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
90
|
+
DECLARE requirements jsonb;
|
|
91
|
+
BEGIN
|
|
92
|
+
IF NEW.goal_snapshot ->> 'state' <> 'none' THEN
|
|
93
|
+
SELECT coalesce(metadata -> 'reportRequirementsV1', '[]'::jsonb) INTO requirements
|
|
94
|
+
FROM session_goals WHERE workspace_id = NEW.workspace_id AND session_id = NEW.session_id;
|
|
95
|
+
NEW.goal_snapshot := NEW.goal_snapshot || jsonb_build_object('reportRequirements', coalesce(requirements, '[]'::jsonb));
|
|
96
|
+
END IF;
|
|
97
|
+
RETURN NEW;
|
|
98
|
+
END;
|
|
99
|
+
$$;
|
|
100
|
+
|
|
101
|
+
CREATE TRIGGER zz_session_goal_freeze_report_requirements_trigger
|
|
102
|
+
BEFORE INSERT ON session_turns
|
|
103
|
+
FOR EACH ROW EXECUTE FUNCTION session_goal_freeze_report_requirements();
|
|
104
|
+
|
|
105
|
+
CREATE FUNCTION session_document_inspection_receipt_immutable()
|
|
106
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
107
|
+
BEGIN
|
|
108
|
+
IF (OLD.action = 'artifact.document.inspect' OR NEW.action = 'artifact.document.inspect') AND NEW IS DISTINCT FROM OLD THEN
|
|
109
|
+
RAISE EXCEPTION 'Native document inspection receipts are immutable' USING ERRCODE = '23514';
|
|
110
|
+
END IF;
|
|
111
|
+
RETURN NEW;
|
|
112
|
+
END;
|
|
113
|
+
$$;
|
|
114
|
+
|
|
115
|
+
CREATE TRIGGER session_document_inspection_receipt_immutable_trigger
|
|
116
|
+
BEFORE UPDATE ON session_command_receipts
|
|
117
|
+
FOR EACH ROW EXECUTE FUNCTION session_document_inspection_receipt_immutable();
|
|
118
|
+
|
|
119
|
+
CREATE FUNCTION session_document_inspection_receipt_retained()
|
|
120
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
121
|
+
BEGIN
|
|
122
|
+
-- Keep proof while its session exists. FK retention cascades remove the
|
|
123
|
+
-- session first and must remain able to delete its receipts. Checking parent
|
|
124
|
+
-- existence also prevents using an unrelated nested trigger to bypass this.
|
|
125
|
+
IF OLD.action = 'artifact.document.inspect' AND EXISTS (
|
|
126
|
+
SELECT 1 FROM sessions WHERE account_id = OLD.account_id
|
|
127
|
+
AND workspace_id = OLD.workspace_id AND id = OLD.target_session_id
|
|
128
|
+
) THEN
|
|
129
|
+
RAISE EXCEPTION 'Native document inspection receipts are retained with their session' USING ERRCODE = '23514';
|
|
130
|
+
END IF;
|
|
131
|
+
RETURN OLD;
|
|
132
|
+
END;
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
CREATE TRIGGER session_document_inspection_receipt_retained_trigger
|
|
136
|
+
BEFORE DELETE ON session_command_receipts
|
|
137
|
+
FOR EACH ROW EXECUTE FUNCTION session_document_inspection_receipt_retained();
|
|
138
|
+
|
|
139
|
+
CREATE FUNCTION session_goal_report_requirements_before_delete()
|
|
140
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
141
|
+
BEGIN
|
|
142
|
+
-- Parent session/workspace deletion remains cancellation, not completion.
|
|
143
|
+
-- Ordinary goal clearing must carry the exact trusted API cancellation stamp.
|
|
144
|
+
IF pg_trigger_depth() = 1 AND OLD.status <> 'completed'
|
|
145
|
+
AND jsonb_array_length(coalesce(OLD.metadata -> 'reportRequirementsV1', '[]'::jsonb)) > 0
|
|
146
|
+
AND current_setting('opengeni.goal_report_clear_id', true) IS DISTINCT FROM OLD.id::text
|
|
147
|
+
THEN
|
|
148
|
+
RAISE EXCEPTION 'Pending report requirements require explicit API cancellation' USING ERRCODE = '23514';
|
|
149
|
+
END IF;
|
|
150
|
+
RETURN OLD;
|
|
151
|
+
END;
|
|
152
|
+
$$;
|
|
153
|
+
|
|
154
|
+
CREATE TRIGGER session_goal_report_requirements_before_delete_trigger
|
|
155
|
+
BEFORE DELETE ON session_goals
|
|
156
|
+
FOR EACH ROW EXECUTE FUNCTION session_goal_report_requirements_before_delete();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Session identity determines workspace and account. Independent column
|
|
3
|
+
-- estimates otherwise multiply the same scope selectivity several times.
|
|
4
|
+
-- No index, payload, pagination boundary, grant or planner setting changes.
|
|
5
|
+
SET LOCAL lock_timeout = '5s';
|
|
6
|
+
|
|
7
|
+
CREATE STATISTICS session_events_scope_statistics (dependencies, mcv)
|
|
8
|
+
ON account_id, workspace_id, session_id FROM session_events;
|
|
9
|
+
ALTER STATISTICS session_events_scope_statistics SET STATISTICS 1000;
|
|
10
|
+
|
|
11
|
+
-- Populate existing installations immediately; autovacuum ANALYZE maintains
|
|
12
|
+
-- this statistics object afterward. Only scope and cursor columns are sampled.
|
|
13
|
+
-- The policy DDL is deliberately in 0476's separate transaction so its lock
|
|
14
|
+
-- cannot be retained across this scan.
|
|
15
|
+
ANALYZE session_events (account_id, workspace_id, session_id, sequence);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Statistics were populated in 0475's separate transaction, so this policy
|
|
3
|
+
-- DDL transaction does not retain its table lock across the ANALYZE scan.
|
|
4
|
+
-- Keep the migration-owner capability exactly as introduced by 0353. In
|
|
5
|
+
-- particular, ownership is resolved dynamically, not captured as a policy role.
|
|
6
|
+
-- Express the owner-only branch as one conditional: treating its owner and
|
|
7
|
+
-- capability terms as independent selectivity estimates can badly undercount
|
|
8
|
+
-- ordinary scoped reads and favor scanning/sorting an entire session history.
|
|
9
|
+
SET LOCAL lock_timeout = '5s';
|
|
10
|
+
|
|
11
|
+
ALTER POLICY session_events_automatic_title_quarantine_v1 ON session_events
|
|
12
|
+
USING (
|
|
13
|
+
CASE WHEN current_user = (
|
|
14
|
+
SELECT pg_catalog.pg_get_userbyid(relation.relowner)
|
|
15
|
+
FROM pg_catalog.pg_class relation
|
|
16
|
+
WHERE relation.oid = 'session_events'::regclass
|
|
17
|
+
) THEN pg_catalog.current_setting(
|
|
18
|
+
'opengeni.automatic_session_title_quarantine_v1', true
|
|
19
|
+
) = '1' ELSE false END
|
|
20
|
+
)
|
|
21
|
+
WITH CHECK (
|
|
22
|
+
CASE WHEN current_user = (
|
|
23
|
+
SELECT pg_catalog.pg_get_userbyid(relation.relowner)
|
|
24
|
+
FROM pg_catalog.pg_class relation
|
|
25
|
+
WHERE relation.oid = 'session_events'::regclass
|
|
26
|
+
) THEN pg_catalog.current_setting(
|
|
27
|
+
'opengeni.automatic_session_title_quarantine_v1', true
|
|
28
|
+
) = '1' ELSE false END
|
|
29
|
+
);
|