@opengeni/db 3.6.0 → 3.7.4-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/automations.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/{chunk-UGDVSQ6U.js → chunk-3ZMN4EXC.js} +2 -2
- package/dist/{chunk-744DFC65.js → chunk-6FIVOJ3V.js} +96 -6
- package/dist/chunk-6FIVOJ3V.js.map +1 -0
- package/dist/{chunk-UJ2EW3QI.js → chunk-DXJ2WY6W.js} +427 -85
- package/dist/chunk-DXJ2WY6W.js.map +1 -0
- package/dist/{chunk-CDMMSUWJ.js → chunk-JP6IYGYI.js} +3 -3
- package/dist/{chunk-Q5YUGQVK.js → chunk-JS3AWDKZ.js} +3 -3
- package/dist/{chunk-G3732M3T.js → chunk-LHKB62GC.js} +2 -2
- package/dist/{chunk-2NT476HD.js → chunk-QL4AHBYD.js} +2 -2
- package/dist/{chunk-IW6O6G7R.js → chunk-VDXHLVDE.js} +2 -2
- package/dist/{chunk-WW7AJ5IC.js → chunk-ZTSJPFDH.js} +5 -7
- package/dist/chunk-ZTSJPFDH.js.map +1 -0
- package/dist/codex-token-resolver.d.ts +12 -1
- package/dist/company-profile-agent-admin.d.ts +4 -2
- package/dist/company-profile-agent-policy.d.ts +20 -0
- package/dist/company-profile-schema.d.ts +573 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +487 -20
- package/dist/index.js +9489 -6041
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-human-provisioning.d.ts +3 -3
- package/dist/memory-domain.d.ts +5 -3
- package/dist/model-catalog.d.ts +153 -0
- package/dist/pr-review.d.ts +9 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +7 -7
- package/dist/schema.d.ts +1069 -6
- package/dist/schema.js +17 -1
- package/dist/session-background-commands.js +3 -3
- package/dist/session-control.d.ts +75 -5
- package/dist/session-queue-commands.d.ts +19 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/user-resource-authority.d.ts +8 -0
- package/dist/video-generation.js +3 -3
- package/dist/xai-subscription.d.ts +8 -0
- package/drizzle/0369_local_human_personal_authority.sql +350 -0
- package/drizzle/0370_workspace_member_management_scope.sql +98 -0
- package/drizzle/0371_workspace_member_candidate_inventory.sql +128 -0
- package/drizzle/0372_ordered_variable_set_runtime_authority.sql +287 -0
- package/drizzle/0373_sandbox_shared_preparation.sql +59 -0
- package/drizzle/0374_session_event_cursors.sql +315 -0
- package/drizzle/0375_session_recovery_observability.sql +73 -0
- package/drizzle/0376_organization_workspace_inventory.sql +88 -0
- package/drizzle/0377_scoped_rig_health_projection.sql +80 -0
- package/drizzle/0378_scoped_rig_health_audit_timestamp.sql +77 -0
- package/drizzle/0379_session_event_raw_lane_activation.sql +518 -0
- package/drizzle/0380_autonomous_company_profile_agent_policy.sql +829 -0
- package/drizzle/0381_organization_codex_subscription_inheritance.sql +720 -0
- package/drizzle/0382_organization_api_key_provenance.sql +91 -0
- package/drizzle/0383_codex_cooldown_reconciliation.sql +51 -0
- package/drizzle/0384_codex_cooldown_revision_guard_privileges.sql +6 -0
- package/drizzle/0385_connected_machine_legacy_tilde_workdirs.sql +25 -0
- package/drizzle/0386_local_organization_administration.sql +157 -0
- package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
- package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
- package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
- package/package.json +6 -6
- package/src/automations.ts +9 -1
- package/src/codex-token-resolver.ts +26 -1
- package/src/company-brain-governed-writes.ts +30 -1
- package/src/company-profile-agent-admin.ts +68 -1
- package/src/company-profile-agent-policy.ts +126 -0
- package/src/company-profile-schema.ts +115 -1
- package/src/database.ts +3 -5
- package/src/index.ts +4473 -570
- package/src/insights.ts +21 -7
- package/src/managed-human-provisioning.ts +3 -3
- package/src/memory-domain.ts +9 -4
- package/src/model-catalog.ts +682 -0
- package/src/organization-membership-lifecycle.ts +0 -1
- package/src/pr-review.ts +19 -0
- package/src/provision-roles.ts +83 -4
- package/src/runtime-posture.ts +26 -0
- package/src/schema.ts +259 -10
- package/src/session-control.ts +347 -59
- package/src/session-queue-commands.ts +303 -122
- package/src/session-tool-call-settlement.ts +34 -7
- package/src/user-resource-authority.ts +56 -0
- package/src/xai-subscription.ts +66 -14
- package/dist/chunk-744DFC65.js.map +0 -1
- package/dist/chunk-UJ2EW3QI.js.map +0 -1
- package/dist/chunk-WW7AJ5IC.js.map +0 -1
- /package/dist/{chunk-UGDVSQ6U.js.map → chunk-3ZMN4EXC.js.map} +0 -0
- /package/dist/{chunk-CDMMSUWJ.js.map → chunk-JP6IYGYI.js.map} +0 -0
- /package/dist/{chunk-Q5YUGQVK.js.map → chunk-JS3AWDKZ.js.map} +0 -0
- /package/dist/{chunk-G3732M3T.js.map → chunk-LHKB62GC.js.map} +0 -0
- /package/dist/{chunk-2NT476HD.js.map → chunk-QL4AHBYD.js.map} +0 -0
- /package/dist/{chunk-IW6O6G7R.js.map → chunk-VDXHLVDE.js.map} +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Migration 0389: deployment-owned model catalog authority, provider-scoped
|
|
3
|
+
-- workspace Vercel AI Gateway/OpenRouter slugs, and indexed pre-catalog prompt
|
|
4
|
+
-- receipt replay.
|
|
5
|
+
-- This changes the exact runtime-posture table/grant contract. Stop every API,
|
|
6
|
+
-- control worker, and turn worker before applying it, and never restart a
|
|
7
|
+
-- pre-0389 image after commit.
|
|
8
|
+
|
|
9
|
+
SET LOCAL lock_timeout = '5s';
|
|
10
|
+
SET LOCAL statement_timeout = '5min';
|
|
11
|
+
|
|
12
|
+
DO $model_catalog_runtime_drain_before$
|
|
13
|
+
DECLARE
|
|
14
|
+
configured_roles_text text := nullif(
|
|
15
|
+
current_setting('opengeni.migration_application_roles', true), ''
|
|
16
|
+
);
|
|
17
|
+
configured_roles jsonb;
|
|
18
|
+
BEGIN
|
|
19
|
+
IF configured_roles_text IS NULL THEN
|
|
20
|
+
RAISE EXCEPTION
|
|
21
|
+
'0389 model catalog activation requires an explicit application database role list'
|
|
22
|
+
USING ERRCODE = '55000';
|
|
23
|
+
END IF;
|
|
24
|
+
BEGIN
|
|
25
|
+
configured_roles := configured_roles_text::jsonb;
|
|
26
|
+
EXCEPTION WHEN OTHERS THEN
|
|
27
|
+
RAISE EXCEPTION
|
|
28
|
+
'0389 model catalog activation received a malformed application database role list'
|
|
29
|
+
USING ERRCODE = '55000';
|
|
30
|
+
END;
|
|
31
|
+
IF jsonb_typeof(configured_roles) <> 'array'
|
|
32
|
+
OR jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
|
|
33
|
+
OR EXISTS (
|
|
34
|
+
SELECT 1 FROM jsonb_array_elements(configured_roles) AS roles(value)
|
|
35
|
+
WHERE jsonb_typeof(value) <> 'string'
|
|
36
|
+
OR btrim(value #>> '{}') = ''
|
|
37
|
+
OR octet_length(value #>> '{}') > 63
|
|
38
|
+
)
|
|
39
|
+
OR (
|
|
40
|
+
SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
|
|
41
|
+
) <> (
|
|
42
|
+
SELECT count(DISTINCT value)
|
|
43
|
+
FROM jsonb_array_elements_text(configured_roles) AS roles(value)
|
|
44
|
+
)
|
|
45
|
+
THEN
|
|
46
|
+
RAISE EXCEPTION
|
|
47
|
+
'0389 model catalog activation received an invalid application database role list'
|
|
48
|
+
USING ERRCODE = '55000';
|
|
49
|
+
END IF;
|
|
50
|
+
IF EXISTS (
|
|
51
|
+
SELECT 1 FROM pg_stat_activity activity
|
|
52
|
+
JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
|
|
53
|
+
ON roles.role_name = activity.usename
|
|
54
|
+
WHERE activity.datname = current_database()
|
|
55
|
+
AND activity.pid <> pg_backend_pid()
|
|
56
|
+
)
|
|
57
|
+
THEN
|
|
58
|
+
RAISE EXCEPTION
|
|
59
|
+
'0389 model catalog activation requires all configured OpenGeni application database sessions to be stopped'
|
|
60
|
+
USING ERRCODE = '55000';
|
|
61
|
+
END IF;
|
|
62
|
+
END
|
|
63
|
+
$model_catalog_runtime_drain_before$;
|
|
64
|
+
|
|
65
|
+
-- Exactly one deployment-global, secret-free catalog document. Ordinary API
|
|
66
|
+
-- and worker roles may only read it; the operator upsert command uses a
|
|
67
|
+
-- migration/admin credential. No seed or boot-time rewrite is intentional.
|
|
68
|
+
CREATE TABLE deployment_model_catalog (
|
|
69
|
+
singleton boolean PRIMARY KEY DEFAULT true,
|
|
70
|
+
document jsonb NOT NULL,
|
|
71
|
+
version bigint NOT NULL DEFAULT 1,
|
|
72
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
73
|
+
CONSTRAINT deployment_model_catalog_singleton_chk CHECK (singleton),
|
|
74
|
+
CONSTRAINT deployment_model_catalog_document_chk CHECK (jsonb_typeof(document) = 'object'),
|
|
75
|
+
CONSTRAINT deployment_model_catalog_version_chk CHECK (version > 0)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
COMMENT ON TABLE deployment_model_catalog IS
|
|
79
|
+
'Deployment-global, secret-free model catalog singleton. Runtime is SELECT-only; operators update it explicitly.';
|
|
80
|
+
|
|
81
|
+
CREATE TABLE workspace_gateway_custom_models (
|
|
82
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
83
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
84
|
+
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
85
|
+
provider_kind text NOT NULL,
|
|
86
|
+
upstream_model_id text NOT NULL,
|
|
87
|
+
label text,
|
|
88
|
+
version integer NOT NULL DEFAULT 1,
|
|
89
|
+
create_operation_id uuid NOT NULL,
|
|
90
|
+
create_request_hash text NOT NULL,
|
|
91
|
+
delete_operation_id uuid,
|
|
92
|
+
delete_request_hash text,
|
|
93
|
+
created_by_subject_id text NOT NULL,
|
|
94
|
+
retired_at timestamptz,
|
|
95
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
96
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
97
|
+
CONSTRAINT workspace_gateway_custom_models_workspace_account_fk
|
|
98
|
+
FOREIGN KEY (workspace_id, account_id)
|
|
99
|
+
REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
|
|
100
|
+
CONSTRAINT workspace_gateway_custom_models_provider_kind_chk CHECK (
|
|
101
|
+
provider_kind IN ('vercel_gateway', 'openrouter')
|
|
102
|
+
),
|
|
103
|
+
CONSTRAINT workspace_gateway_custom_models_upstream_chk CHECK (
|
|
104
|
+
octet_length(upstream_model_id) BETWEEN 1 AND 238
|
|
105
|
+
AND upstream_model_id ~ '^[!-~]+$'
|
|
106
|
+
AND upstream_model_id !~ '[|]'
|
|
107
|
+
),
|
|
108
|
+
CONSTRAINT workspace_gateway_custom_models_label_chk CHECK (
|
|
109
|
+
label IS NULL OR (
|
|
110
|
+
octet_length(label) BETWEEN 1 AND 128
|
|
111
|
+
AND label !~ '[\r\n|]'
|
|
112
|
+
)
|
|
113
|
+
),
|
|
114
|
+
CONSTRAINT workspace_gateway_custom_models_actor_chk CHECK (
|
|
115
|
+
octet_length(created_by_subject_id) BETWEEN 1 AND 1024
|
|
116
|
+
),
|
|
117
|
+
CONSTRAINT workspace_gateway_custom_models_version_chk CHECK (version > 0),
|
|
118
|
+
CONSTRAINT workspace_gateway_custom_models_create_hash_chk CHECK (
|
|
119
|
+
create_request_hash ~ '^[a-f0-9]{64}$'
|
|
120
|
+
),
|
|
121
|
+
CONSTRAINT workspace_gateway_custom_models_delete_receipt_chk CHECK (
|
|
122
|
+
(delete_operation_id IS NULL AND delete_request_hash IS NULL)
|
|
123
|
+
OR (
|
|
124
|
+
delete_operation_id IS NOT NULL
|
|
125
|
+
AND delete_request_hash ~ '^[a-f0-9]{64}$'
|
|
126
|
+
AND retired_at IS NOT NULL
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
CREATE UNIQUE INDEX workspace_gateway_custom_models_workspace_upstream_uq
|
|
132
|
+
ON workspace_gateway_custom_models (workspace_id, provider_kind, upstream_model_id)
|
|
133
|
+
WHERE retired_at IS NULL;
|
|
134
|
+
|
|
135
|
+
CREATE UNIQUE INDEX workspace_gateway_custom_models_create_operation_uq
|
|
136
|
+
ON workspace_gateway_custom_models (workspace_id, provider_kind, create_operation_id);
|
|
137
|
+
|
|
138
|
+
CREATE UNIQUE INDEX workspace_gateway_custom_models_delete_operation_uq
|
|
139
|
+
ON workspace_gateway_custom_models (workspace_id, provider_kind, delete_operation_id)
|
|
140
|
+
WHERE delete_operation_id IS NOT NULL;
|
|
141
|
+
|
|
142
|
+
-- New Send/Steer receipts persist one parsed boundary-request fingerprint and
|
|
143
|
+
-- replay it before mutable model-catalog resolution. Legacy receipt identity
|
|
144
|
+
-- remains unchanged; this partial index keeps the actor/key compatibility
|
|
145
|
+
-- probe bounded even when the prior action or target session differs.
|
|
146
|
+
CREATE INDEX session_command_receipts_prompt_actor_operation_idx
|
|
147
|
+
ON session_command_receipts (
|
|
148
|
+
workspace_id,
|
|
149
|
+
actor_type,
|
|
150
|
+
actor_subject_id,
|
|
151
|
+
actor_attempt_id,
|
|
152
|
+
operation_key
|
|
153
|
+
)
|
|
154
|
+
WHERE action IN ('prompt.send', 'prompt.steer');
|
|
155
|
+
|
|
156
|
+
ALTER TABLE workspace_gateway_custom_models ENABLE ROW LEVEL SECURITY;
|
|
157
|
+
ALTER TABLE workspace_gateway_custom_models FORCE ROW LEVEL SECURITY;
|
|
158
|
+
CREATE POLICY workspace_isolation ON workspace_gateway_custom_models
|
|
159
|
+
USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
|
|
160
|
+
WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
|
|
161
|
+
|
|
162
|
+
-- Migration 0040 registers owner default privileges for the historical
|
|
163
|
+
-- opengeni_app role. Rotated-role deployments must not inherit that stale
|
|
164
|
+
-- grant on these new tables, and host defaults may name other obsolete
|
|
165
|
+
-- application roles. Strip every explicit non-owner table grantee. The
|
|
166
|
+
-- migration_application_roles list above is drain detection only; the
|
|
167
|
+
-- post-migration role provisioner grants only the deployment's current target
|
|
168
|
+
-- runtime role.
|
|
169
|
+
DO $model_catalog_table_acl_reset$
|
|
170
|
+
DECLARE
|
|
171
|
+
data_schema text := pg_catalog.current_schema();
|
|
172
|
+
relation_name text;
|
|
173
|
+
role_name text;
|
|
174
|
+
BEGIN
|
|
175
|
+
FOR relation_name IN
|
|
176
|
+
SELECT relation.value
|
|
177
|
+
FROM pg_catalog.unnest(
|
|
178
|
+
ARRAY[
|
|
179
|
+
'deployment_model_catalog',
|
|
180
|
+
'workspace_gateway_custom_models'
|
|
181
|
+
]::text[]
|
|
182
|
+
) AS relation(value)
|
|
183
|
+
LOOP
|
|
184
|
+
EXECUTE pg_catalog.format(
|
|
185
|
+
'REVOKE ALL ON TABLE %I.%I FROM PUBLIC',
|
|
186
|
+
data_schema,
|
|
187
|
+
relation_name
|
|
188
|
+
);
|
|
189
|
+
FOR role_name IN
|
|
190
|
+
SELECT grantee_role.rolname
|
|
191
|
+
FROM pg_catalog.pg_class relation
|
|
192
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(
|
|
193
|
+
coalesce(
|
|
194
|
+
relation.relacl,
|
|
195
|
+
pg_catalog.acldefault('r', relation.relowner)
|
|
196
|
+
)
|
|
197
|
+
) privilege
|
|
198
|
+
INNER JOIN pg_catalog.pg_roles grantee_role
|
|
199
|
+
ON grantee_role.oid = privilege.grantee
|
|
200
|
+
WHERE relation.oid = pg_catalog.to_regclass(
|
|
201
|
+
pg_catalog.format('%I.%I', data_schema, relation_name)
|
|
202
|
+
)
|
|
203
|
+
AND privilege.grantee <> 0
|
|
204
|
+
AND privilege.grantee <> relation.relowner
|
|
205
|
+
GROUP BY grantee_role.rolname
|
|
206
|
+
ORDER BY grantee_role.rolname COLLATE "C"
|
|
207
|
+
LOOP
|
|
208
|
+
EXECUTE pg_catalog.format(
|
|
209
|
+
'REVOKE ALL ON TABLE %I.%I FROM %I',
|
|
210
|
+
data_schema,
|
|
211
|
+
relation_name,
|
|
212
|
+
role_name
|
|
213
|
+
);
|
|
214
|
+
END LOOP;
|
|
215
|
+
END LOOP;
|
|
216
|
+
END
|
|
217
|
+
$model_catalog_table_acl_reset$;
|
|
218
|
+
|
|
219
|
+
COMMENT ON TABLE workspace_gateway_custom_models IS
|
|
220
|
+
'Workspace-owned Vercel AI Gateway and OpenRouter upstream slugs. Retired rows remain execution evidence; capabilities and billing are never stored here.';
|
|
221
|
+
|
|
222
|
+
DO $model_catalog_runtime_drain_after$
|
|
223
|
+
DECLARE
|
|
224
|
+
configured_roles jsonb := current_setting(
|
|
225
|
+
'opengeni.migration_application_roles', false
|
|
226
|
+
)::jsonb;
|
|
227
|
+
BEGIN
|
|
228
|
+
IF EXISTS (
|
|
229
|
+
SELECT 1 FROM pg_stat_activity activity
|
|
230
|
+
JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
|
|
231
|
+
ON roles.role_name = activity.usename
|
|
232
|
+
WHERE activity.datname = current_database()
|
|
233
|
+
AND activity.pid <> pg_backend_pid()
|
|
234
|
+
)
|
|
235
|
+
THEN
|
|
236
|
+
RAISE EXCEPTION
|
|
237
|
+
'0389 model catalog activation observed a configured OpenGeni application database session after schema installation'
|
|
238
|
+
USING ERRCODE = '55000';
|
|
239
|
+
END IF;
|
|
240
|
+
END
|
|
241
|
+
$model_catalog_runtime_drain_after$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.4-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": {
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@opengeni/codemode": "^0.4.
|
|
82
|
-
"@opengeni/codex": "^0.2.
|
|
83
|
-
"@opengeni/config": "^0.22.
|
|
84
|
-
"@opengeni/contracts": "^2.
|
|
85
|
-
"@opengeni/network": "^0.2.3",
|
|
81
|
+
"@opengeni/codemode": "^0.4.22-canary.1",
|
|
82
|
+
"@opengeni/codex": "^0.2.20-canary.1",
|
|
83
|
+
"@opengeni/config": "^0.22.5-canary.1",
|
|
84
|
+
"@opengeni/contracts": "^2.9.2-canary.1",
|
|
85
|
+
"@opengeni/network": "^0.2.3-canary.6",
|
|
86
86
|
"drizzle-orm": "^0.45.2",
|
|
87
87
|
"postgres": "^3.4.7"
|
|
88
88
|
},
|
package/src/automations.ts
CHANGED
|
@@ -223,6 +223,8 @@ export async function createAutomationTrigger(
|
|
|
223
223
|
createdBySubjectId: string;
|
|
224
224
|
request: CreateAutomationTriggerRequest;
|
|
225
225
|
adapterId: string;
|
|
226
|
+
/** Trusted database-only admission seam. Throwing rolls creation back. */
|
|
227
|
+
beforeCreateCommit?: (tx: Database) => Promise<void>;
|
|
226
228
|
},
|
|
227
229
|
): Promise<AutomationTrigger> {
|
|
228
230
|
return await withWorkspaceRls(
|
|
@@ -289,6 +291,7 @@ export async function createAutomationTrigger(
|
|
|
289
291
|
throw new Error("Automation Pack trigger must use its frozen manifest template");
|
|
290
292
|
}
|
|
291
293
|
}
|
|
294
|
+
await input.beforeCreateCommit?.(tx);
|
|
292
295
|
const [head] = await tx
|
|
293
296
|
.insert(schema.automationTriggers)
|
|
294
297
|
.values({
|
|
@@ -438,6 +441,8 @@ export async function updateAutomationTrigger(
|
|
|
438
441
|
triggerId: string;
|
|
439
442
|
subjectId: string;
|
|
440
443
|
request: UpdateAutomationTriggerRequest;
|
|
444
|
+
/** Trusted database-only admission seam. Throwing rolls the revision back. */
|
|
445
|
+
beforeUpdateCommit?: (tx: Database) => Promise<void>;
|
|
441
446
|
},
|
|
442
447
|
): Promise<AutomationTrigger | null> {
|
|
443
448
|
return await withWorkspaceRls(
|
|
@@ -478,6 +483,7 @@ export async function updateAutomationTrigger(
|
|
|
478
483
|
"Pack-owned automation triggers must be managed through their Pack setup API",
|
|
479
484
|
);
|
|
480
485
|
}
|
|
486
|
+
await input.beforeUpdateCommit?.(tx);
|
|
481
487
|
const revisionNumber = existing.head.currentRevision + 1;
|
|
482
488
|
const [head] = await tx
|
|
483
489
|
.update(schema.automationTriggers)
|
|
@@ -696,7 +702,9 @@ export async function claimAutomationRun(
|
|
|
696
702
|
.for("update")
|
|
697
703
|
.limit(1);
|
|
698
704
|
if (!row) return null;
|
|
699
|
-
if (row.status === "dispatched" || row.status === "skipped"
|
|
705
|
+
if (row.status === "dispatched" || row.status === "skipped" || row.status === "failed") {
|
|
706
|
+
return mapRunExecution(row);
|
|
707
|
+
}
|
|
700
708
|
const [updated] = await tx
|
|
701
709
|
.update(schema.automationRuns)
|
|
702
710
|
.set({
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
CODEX_REFRESH_FALLBACK_MS,
|
|
25
25
|
CODEX_REFRESH_WINDOW_MS,
|
|
26
26
|
CodexReloginRequired,
|
|
27
|
+
codexUsageConfirmsQuotaAvailable,
|
|
27
28
|
type CodexTokenSnapshot,
|
|
28
29
|
type CodexUsagePayload,
|
|
29
30
|
type CodexFetch,
|
|
@@ -43,6 +44,8 @@ export type CodexCredentialTokens = {
|
|
|
43
44
|
idToken: string;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
export type CodexCredentialCooldownKind = "quota" | "rate_limit";
|
|
48
|
+
|
|
46
49
|
export type CodexCredentialForRun = {
|
|
47
50
|
id: string;
|
|
48
51
|
version: number;
|
|
@@ -56,6 +59,9 @@ export type CodexCredentialForRun = {
|
|
|
56
59
|
lastRefreshAt: Date | null;
|
|
57
60
|
status: string;
|
|
58
61
|
lastError: string | null;
|
|
62
|
+
exhaustedUntil: Date | null;
|
|
63
|
+
exhaustedKind: CodexCredentialCooldownKind | null;
|
|
64
|
+
exhaustedRevision: number;
|
|
59
65
|
};
|
|
60
66
|
|
|
61
67
|
export type CodexAccountUsageSnapshot = {
|
|
@@ -66,6 +72,12 @@ export type CodexAccountUsageSnapshot = {
|
|
|
66
72
|
checkedAt?: Date;
|
|
67
73
|
resetCreditAvailableCount?: number | null;
|
|
68
74
|
resetCreditsCheckedAt?: Date | null;
|
|
75
|
+
/**
|
|
76
|
+
* Clear only a quota cooldown with this exact revision. Set solely after a
|
|
77
|
+
* live provider response proves allowance is open; a concurrent refusal
|
|
78
|
+
* advances the revision and makes this observation stale.
|
|
79
|
+
*/
|
|
80
|
+
clearQuotaCooldownRevision?: number;
|
|
69
81
|
};
|
|
70
82
|
|
|
71
83
|
type CodexCredentialRefreshInput = {
|
|
@@ -375,7 +387,8 @@ function errorUsagePayload(reason?: "needs_relogin"): CodexUsagePayload {
|
|
|
375
387
|
* account's expired JWT from 401-ing the usage read.
|
|
376
388
|
* 2. fetch GET /wham/usage with that bearer.
|
|
377
389
|
* 3. normalize (§3) into the P2/P3 contract.
|
|
378
|
-
* 4. on any windows present, write the
|
|
390
|
+
* 4. on any windows present, write the usage cache and conditionally reconcile
|
|
391
|
+
* the exact older typed quota cooldown observed before provider I/O.
|
|
379
392
|
*
|
|
380
393
|
* A refresh that stamps needs_relogin returns { status:"error", reason } and never
|
|
381
394
|
* hits the provider; a transient refresh error returns a plain error payload.
|
|
@@ -396,6 +409,13 @@ export async function fetchCodexUsageForAccount(
|
|
|
396
409
|
return errorUsagePayload(error instanceof CodexReloginRequired ? "needs_relogin" : undefined);
|
|
397
410
|
}
|
|
398
411
|
|
|
412
|
+
// Snapshot cooldown authority immediately before provider I/O. The usage
|
|
413
|
+
// write may clear only this exact revision; any concurrent refusal advances
|
|
414
|
+
// it and wins. A metadata-read failure must not sink the usage response.
|
|
415
|
+
const observedCredential = await deps
|
|
416
|
+
.loadCredential(db, settings, workspaceId, credentialId)
|
|
417
|
+
.catch(() => null);
|
|
418
|
+
|
|
399
419
|
let normalized: CodexUsagePayload;
|
|
400
420
|
try {
|
|
401
421
|
const usage = await fetchCodexUsage(
|
|
@@ -438,6 +458,11 @@ export async function fetchCodexUsageForAccount(
|
|
|
438
458
|
checkedAt,
|
|
439
459
|
}
|
|
440
460
|
: {}),
|
|
461
|
+
...(observedCredential?.exhaustedUntil &&
|
|
462
|
+
observedCredential.exhaustedKind === "quota" &&
|
|
463
|
+
codexUsageConfirmsQuotaAvailable(normalized)
|
|
464
|
+
? { clearQuotaCooldownRevision: observedCredential.exhaustedRevision }
|
|
465
|
+
: {}),
|
|
441
466
|
...(normalized.rateLimitResetCredits
|
|
442
467
|
? {
|
|
443
468
|
resetCreditAvailableCount: normalized.rateLimitResetCredits.availableCount,
|
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
16
16
|
import type { Database } from "./database";
|
|
17
17
|
import { rawRows, setSubjectRlsContext, withRlsContext } from "./database";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
PreferenceRegistryStableKeyConflictError,
|
|
20
|
+
sanitizePreferenceDescriptorText,
|
|
21
|
+
} from "./preference-registry";
|
|
22
|
+
import { nestedPostgresSqlState, safeDatabaseErrorFacts } from "./persistence-errors";
|
|
19
23
|
import {
|
|
20
24
|
appendKnowledgeClaim,
|
|
21
25
|
appendKnowledgeClaimReview,
|
|
@@ -847,6 +851,26 @@ function isInstructionPolicyProposal(
|
|
|
847
851
|
);
|
|
848
852
|
}
|
|
849
853
|
|
|
854
|
+
function isPreferenceProposal(
|
|
855
|
+
request: CompanyBrainGovernedWriteRequestType,
|
|
856
|
+
): request is PreferenceProposalRequest {
|
|
857
|
+
return request.kind === "propose_preference" || request.kind === "promote_task_note_preference";
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const PREFERENCE_PROPOSAL_STABLE_KEY_CONFLICT_DIAGNOSTIC =
|
|
861
|
+
"workspace preference key is bound to another proposal";
|
|
862
|
+
|
|
863
|
+
function isPreferenceProposalStableKeyConflict(error: unknown): boolean {
|
|
864
|
+
if (nestedPostgresSqlState(error) !== "23505") return false;
|
|
865
|
+
const constraint = safeDatabaseErrorFacts(error).constraint ?? "";
|
|
866
|
+
if (constraint.startsWith("preference_registry_preferences_")) return true;
|
|
867
|
+
for (let current = error, depth = 0; current instanceof Error && depth < 8; depth += 1) {
|
|
868
|
+
if (current.message.includes(PREFERENCE_PROPOSAL_STABLE_KEY_CONFLICT_DIAGNOSTIC)) return true;
|
|
869
|
+
current = current.cause;
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
|
|
850
874
|
async function materializeWaysOfWorkingProposal(
|
|
851
875
|
db: Database,
|
|
852
876
|
input: {
|
|
@@ -1008,6 +1032,11 @@ export async function writeCompanyBrainGovernedProposal(
|
|
|
1008
1032
|
) {
|
|
1009
1033
|
throw error;
|
|
1010
1034
|
}
|
|
1035
|
+
if (isPreferenceProposal(request) && isPreferenceProposalStableKeyConflict(error)) {
|
|
1036
|
+
throw new PreferenceRegistryStableKeyConflictError(
|
|
1037
|
+
"A preference with this stable key already exists for the workspace",
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1011
1040
|
if (error instanceof ScopedKnowledgeAuthorityError) {
|
|
1012
1041
|
throw new CompanyBrainGovernedWriteAuthorityError(error.message);
|
|
1013
1042
|
}
|
|
@@ -28,6 +28,7 @@ export class CompanyProfileAgentAdminError extends Error {
|
|
|
28
28
|
constructor(
|
|
29
29
|
readonly code:
|
|
30
30
|
| "authority_unavailable"
|
|
31
|
+
| "policy_disabled"
|
|
31
32
|
| "confirmation_unavailable"
|
|
32
33
|
| "profile_conflict"
|
|
33
34
|
| "operation_reused"
|
|
@@ -62,6 +63,13 @@ function translate(error: unknown, stage: "propose" | "confirm"): never {
|
|
|
62
63
|
{ cause: error },
|
|
63
64
|
);
|
|
64
65
|
}
|
|
66
|
+
if (state === "P1852") {
|
|
67
|
+
throw new CompanyProfileAgentAdminError(
|
|
68
|
+
"policy_disabled",
|
|
69
|
+
"Agent-authored company-profile changes are disabled by organization policy.",
|
|
70
|
+
{ cause: error },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
65
73
|
if (state === "40001") {
|
|
66
74
|
throw new CompanyProfileAgentAdminError(
|
|
67
75
|
"profile_conflict",
|
|
@@ -86,6 +94,18 @@ function translate(error: unknown, stage: "propose" | "confirm"): never {
|
|
|
86
94
|
throw error;
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
/** Deterministic UUID-shaped id for the activation stage of one proposal operation. */
|
|
98
|
+
export function derivedCompanyProfileAutomaticActivationOperationId(operationId: string): string {
|
|
99
|
+
const bytes = createHash("sha256")
|
|
100
|
+
.update(`company-profile-agent-admin:v1:${operationId}:automatic-activation`, "utf8")
|
|
101
|
+
.digest()
|
|
102
|
+
.subarray(0, 16);
|
|
103
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
|
|
104
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
105
|
+
const hex = bytes.toString("hex");
|
|
106
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
async function withAgentAttemptContext<T>(
|
|
90
110
|
db: Database,
|
|
91
111
|
attempt: CompanyProfileAgentAttemptType,
|
|
@@ -119,10 +139,13 @@ export async function proposeCompanyProfileForAgent(
|
|
|
119
139
|
receipt_id: string;
|
|
120
140
|
revision_id: string;
|
|
121
141
|
human_input: unknown;
|
|
142
|
+
policy_mode: "suggest" | "automatic";
|
|
143
|
+
automatic_activation_receipt_id: string | null;
|
|
144
|
+
activation_event_id: string | null;
|
|
122
145
|
replayed: boolean;
|
|
123
146
|
}>(
|
|
124
147
|
scoped,
|
|
125
|
-
sql`select * from
|
|
148
|
+
sql`select * from propose_company_profile_for_attempt_v2(
|
|
126
149
|
${attempt.accountId}::uuid,
|
|
127
150
|
${attempt.workspaceId}::uuid,
|
|
128
151
|
${attempt.sessionId}::uuid,
|
|
@@ -130,6 +153,7 @@ export async function proposeCompanyProfileForAgent(
|
|
|
130
153
|
${attempt.attemptId}::uuid,
|
|
131
154
|
${attempt.executionGeneration},
|
|
132
155
|
${request.operationId}::uuid,
|
|
156
|
+
${derivedCompanyProfileAutomaticActivationOperationId(request.operationId)}::uuid,
|
|
133
157
|
${contentJson},
|
|
134
158
|
${contentHash},
|
|
135
159
|
${request.reason}
|
|
@@ -138,6 +162,48 @@ export async function proposeCompanyProfileForAgent(
|
|
|
138
162
|
if (!row) throw new Error("Company-profile proposal returned no receipt");
|
|
139
163
|
return row;
|
|
140
164
|
});
|
|
165
|
+
if (
|
|
166
|
+
result.policy_mode === "automatic" &&
|
|
167
|
+
result.automatic_activation_receipt_id &&
|
|
168
|
+
result.activation_event_id
|
|
169
|
+
) {
|
|
170
|
+
const [inventory, event] = await Promise.all([
|
|
171
|
+
listCompanyProfile(db, {
|
|
172
|
+
accountId: attempt.accountId,
|
|
173
|
+
workspaceId: attempt.workspaceId,
|
|
174
|
+
limit: 1,
|
|
175
|
+
}),
|
|
176
|
+
getCompanyProfileActivationEvent(db, {
|
|
177
|
+
accountId: attempt.accountId,
|
|
178
|
+
workspaceId: attempt.workspaceId,
|
|
179
|
+
eventId: result.activation_event_id,
|
|
180
|
+
}),
|
|
181
|
+
]);
|
|
182
|
+
if (!event?.newRevision) {
|
|
183
|
+
throw new Error("Company-profile automatic activation event is unavailable");
|
|
184
|
+
}
|
|
185
|
+
const revision = await getCompanyProfileRevision(db, {
|
|
186
|
+
accountId: attempt.accountId,
|
|
187
|
+
workspaceId: attempt.workspaceId,
|
|
188
|
+
revisionId: event.newRevision.id,
|
|
189
|
+
});
|
|
190
|
+
return CompanyProfileAgentProposalReceipt.parse({
|
|
191
|
+
status: "activated",
|
|
192
|
+
operationId: request.operationId,
|
|
193
|
+
proposalReceiptId: result.receipt_id,
|
|
194
|
+
automaticActivationReceiptId: result.automatic_activation_receipt_id,
|
|
195
|
+
policyMode: "automatic",
|
|
196
|
+
mutation: {
|
|
197
|
+
revision,
|
|
198
|
+
head: inventory.current?.revisionId === revision.id ? inventory.current : null,
|
|
199
|
+
event,
|
|
200
|
+
},
|
|
201
|
+
replayed: result.replayed,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (result.policy_mode !== "suggest") {
|
|
205
|
+
throw new Error("Company-profile proposal returned an incomplete automatic activation");
|
|
206
|
+
}
|
|
141
207
|
const revision = await getCompanyProfileRevision(db, {
|
|
142
208
|
accountId: attempt.accountId,
|
|
143
209
|
workspaceId: attempt.workspaceId,
|
|
@@ -148,6 +214,7 @@ export async function proposeCompanyProfileForAgent(
|
|
|
148
214
|
operationId: request.operationId,
|
|
149
215
|
proposalReceiptId: result.receipt_id,
|
|
150
216
|
revision,
|
|
217
|
+
policyMode: "suggest",
|
|
151
218
|
humanInput: CompanyProfileAgentHumanInputPrompt.parse(result.human_input),
|
|
152
219
|
confirmWith: "company_profile_confirm",
|
|
153
220
|
replayed: result.replayed,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CompanyProfileAgentPolicy,
|
|
3
|
+
CompanyProfileAgentPolicyMode,
|
|
4
|
+
type CompanyProfileAgentPolicy as CompanyProfileAgentPolicyType,
|
|
5
|
+
type CompanyProfileAgentPolicyMode as CompanyProfileAgentPolicyModeType,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import { sql } from "drizzle-orm";
|
|
8
|
+
import { rawRows, withRlsContext, withSessionRlsActorContext, type Database } from "./database";
|
|
9
|
+
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
10
|
+
|
|
11
|
+
export class CompanyProfileAgentPolicyError extends Error {
|
|
12
|
+
readonly name = "CompanyProfileAgentPolicyError";
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
readonly code: "authority_unavailable" | "policy_conflict" | "operation_reused" | "invalid",
|
|
16
|
+
message: string,
|
|
17
|
+
options?: ErrorOptions,
|
|
18
|
+
) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function translate(error: unknown): never {
|
|
24
|
+
const state = nestedPostgresSqlState(error);
|
|
25
|
+
if (state === "42501") {
|
|
26
|
+
throw new CompanyProfileAgentPolicyError(
|
|
27
|
+
"authority_unavailable",
|
|
28
|
+
"An active organization owner is required to manage company-profile agent autonomy.",
|
|
29
|
+
{ cause: error },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (state === "40001") {
|
|
33
|
+
throw new CompanyProfileAgentPolicyError(
|
|
34
|
+
"policy_conflict",
|
|
35
|
+
"The company-profile agent policy changed in another request. Reload and try again.",
|
|
36
|
+
{ cause: error },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (state === "23505" || state === "P1851") {
|
|
40
|
+
throw new CompanyProfileAgentPolicyError(
|
|
41
|
+
"operation_reused",
|
|
42
|
+
"The company-profile agent policy operation id was already used for different input.",
|
|
43
|
+
{ cause: error },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (state === "22023" || state === "23514" || state === "55000") {
|
|
47
|
+
throw new CompanyProfileAgentPolicyError(
|
|
48
|
+
"invalid",
|
|
49
|
+
"The company-profile agent policy request is invalid.",
|
|
50
|
+
{ cause: error },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function withHumanPolicyContext<T>(
|
|
57
|
+
db: Database,
|
|
58
|
+
input: { accountId: string; workspaceId: string; actorSubjectId: string },
|
|
59
|
+
fn: (scoped: Database) => Promise<T>,
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
return await withSessionRlsActorContext({ subjectId: input.actorSubjectId }, async () =>
|
|
62
|
+
withRlsContext(
|
|
63
|
+
db,
|
|
64
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
65
|
+
async (scoped) => {
|
|
66
|
+
await scoped.execute(
|
|
67
|
+
sql`select set_config('opengeni.principal_kind', 'human_session', true)`,
|
|
68
|
+
);
|
|
69
|
+
return await fn(scoped);
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getCompanyProfileAgentPolicy(
|
|
76
|
+
db: Database,
|
|
77
|
+
input: { accountId: string; workspaceId: string; actorSubjectId: string },
|
|
78
|
+
): Promise<CompanyProfileAgentPolicyType> {
|
|
79
|
+
try {
|
|
80
|
+
return await withHumanPolicyContext(db, input, async (scoped) => {
|
|
81
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
82
|
+
scoped,
|
|
83
|
+
sql`select get_company_profile_agent_policy(
|
|
84
|
+
${input.accountId}::uuid,
|
|
85
|
+
${input.workspaceId}::uuid,
|
|
86
|
+
${input.actorSubjectId}
|
|
87
|
+
) as result`,
|
|
88
|
+
);
|
|
89
|
+
return CompanyProfileAgentPolicy.parse(row?.result);
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
translate(error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function updateCompanyProfileAgentPolicy(
|
|
97
|
+
db: Database,
|
|
98
|
+
input: {
|
|
99
|
+
accountId: string;
|
|
100
|
+
workspaceId: string;
|
|
101
|
+
actorSubjectId: string;
|
|
102
|
+
mode: CompanyProfileAgentPolicyModeType;
|
|
103
|
+
expectedVersion: number;
|
|
104
|
+
operationId: string;
|
|
105
|
+
},
|
|
106
|
+
): Promise<CompanyProfileAgentPolicyType> {
|
|
107
|
+
const mode = CompanyProfileAgentPolicyMode.parse(input.mode);
|
|
108
|
+
try {
|
|
109
|
+
return await withHumanPolicyContext(db, input, async (scoped) => {
|
|
110
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
111
|
+
scoped,
|
|
112
|
+
sql`select update_company_profile_agent_policy(
|
|
113
|
+
${input.accountId}::uuid,
|
|
114
|
+
${input.workspaceId}::uuid,
|
|
115
|
+
${input.actorSubjectId},
|
|
116
|
+
${mode},
|
|
117
|
+
${input.expectedVersion}::bigint,
|
|
118
|
+
${input.operationId}::uuid
|
|
119
|
+
) as result`,
|
|
120
|
+
);
|
|
121
|
+
return CompanyProfileAgentPolicy.parse(row?.result);
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
translate(error);
|
|
125
|
+
}
|
|
126
|
+
}
|