@opengeni/db 0.27.8 → 0.27.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-M6EOXYHK.js → chunk-JYQPJ6Y5.js} +59 -50
- package/dist/chunk-JYQPJ6Y5.js.map +1 -0
- package/dist/{chunk-IHPCI4GV.js → chunk-RPHPNVWW.js} +3 -1
- package/dist/chunk-RPHPNVWW.js.map +1 -0
- package/dist/codex-token-resolver.d.ts +50 -8
- package/dist/connection-token-resolver.d.ts +45 -7
- package/dist/database.d.ts +137 -0
- package/dist/index.d.ts +83 -184
- package/dist/index.js +2983 -2691
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +1 -1
- package/dist/memory-governance.d.ts +1 -1
- package/dist/new-session-drafts.d.ts +1 -1
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +155 -96
- package/dist/schema.js +3 -1
- package/dist/scoped-knowledge.d.ts +1 -1
- package/dist/session-control.d.ts +1 -1
- package/dist/session-queue-commands.d.ts +1 -1
- package/dist/session-realtime-context.d.ts +1 -1
- package/dist/session-realtime-ledger.d.ts +1 -1
- package/dist/session-realtime-mirror.d.ts +1 -1
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +1 -1
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/turn-initiator.d.ts +1 -1
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/drizzle/0171_social_connection_subject_ownership.sql +53 -0
- package/drizzle/0172_retire_model_visible_github_token.sql +86 -0
- package/drizzle/0173_codex_auth_boundaries.sql +107 -0
- package/drizzle/0174_session_wake_live_interruption.sql +80 -0
- package/package.json +4 -4
- package/src/codex-token-resolver.ts +102 -49
- package/src/connection-token-resolver.ts +67 -26
- package/src/database.ts +393 -0
- package/src/index.ts +747 -645
- package/src/insights.ts +2 -2
- package/src/memory-governance.ts +2 -2
- package/src/new-session-drafts.ts +1 -1
- package/src/preference-registry.ts +2 -2
- package/src/provision-roles.ts +1 -1
- package/src/runtime-posture.ts +3 -1
- package/src/schema.ts +70 -50
- package/src/scoped-knowledge.ts +2 -2
- package/src/session-control.ts +1 -1
- package/src/session-queue-commands.ts +1 -1
- package/src/session-realtime-context.ts +1 -1
- package/src/session-realtime-ledger.ts +1 -1
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-realtime-state.ts +1 -1
- package/src/session-realtime-terminal.ts +1 -1
- package/src/session-realtime.ts +1 -1
- package/src/session-tool-call-settlement.ts +1 -1
- package/src/turn-initiator.ts +1 -1
- package/src/workspace-artifacts.ts +2 -2
- package/src/workspace-instruction-policies.ts +2 -2
- package/dist/chunk-IHPCI4GV.js.map +0 -1
- package/dist/chunk-M6EOXYHK.js.map +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Extend first-party social credentials with the same workspace/personal ownership boundary as MCP connections.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE social_connections
|
|
5
|
+
ADD COLUMN subject_id text;
|
|
6
|
+
|
|
7
|
+
DROP INDEX social_connections_workspace_provider_handle_idx;
|
|
8
|
+
CREATE UNIQUE INDEX social_connections_workspace_provider_handle_idx
|
|
9
|
+
ON social_connections (workspace_id, provider, account_handle)
|
|
10
|
+
WHERE subject_id IS NULL;
|
|
11
|
+
CREATE UNIQUE INDEX social_connections_subject_provider_handle_idx
|
|
12
|
+
ON social_connections (workspace_id, subject_id, provider)
|
|
13
|
+
WHERE subject_id IS NOT NULL;
|
|
14
|
+
CREATE INDEX social_connections_subject_provider_status_idx
|
|
15
|
+
ON social_connections (workspace_id, subject_id, provider, status);
|
|
16
|
+
|
|
17
|
+
DROP POLICY workspace_isolation ON social_connections;
|
|
18
|
+
CREATE POLICY workspace_isolation ON social_connections
|
|
19
|
+
USING (
|
|
20
|
+
opengeni_private.workspace_rls_visible(account_id, workspace_id)
|
|
21
|
+
AND (
|
|
22
|
+
subject_id IS NULL
|
|
23
|
+
OR subject_id = nullif(current_setting('opengeni.subject_id', true), '')
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
WITH CHECK (
|
|
27
|
+
opengeni_private.workspace_rls_visible(account_id, workspace_id)
|
|
28
|
+
AND (
|
|
29
|
+
subject_id IS NULL
|
|
30
|
+
OR subject_id = nullif(current_setting('opengeni.subject_id', true), '')
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
DROP POLICY workspace_isolation ON social_posts;
|
|
35
|
+
CREATE POLICY workspace_isolation ON social_posts
|
|
36
|
+
USING (
|
|
37
|
+
opengeni_private.workspace_rls_visible(account_id, workspace_id)
|
|
38
|
+
AND EXISTS (
|
|
39
|
+
SELECT 1
|
|
40
|
+
FROM social_connections connection
|
|
41
|
+
WHERE connection.id = social_posts.connection_id
|
|
42
|
+
AND connection.workspace_id = social_posts.workspace_id
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
WITH CHECK (
|
|
46
|
+
opengeni_private.workspace_rls_visible(account_id, workspace_id)
|
|
47
|
+
AND EXISTS (
|
|
48
|
+
SELECT 1
|
|
49
|
+
FROM social_connections connection
|
|
50
|
+
WHERE connection.id = social_posts.connection_id
|
|
51
|
+
AND connection.workspace_id = social_posts.workspace_id
|
|
52
|
+
)
|
|
53
|
+
);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- GitHub App installation credentials are host-owned run material. Retire the
|
|
3
|
+
-- model-visible github_token MCP tool from every durable session selection and
|
|
4
|
+
-- prevent an old writer from reintroducing it after the coordinated cutover.
|
|
5
|
+
|
|
6
|
+
UPDATE "sessions"
|
|
7
|
+
SET "first_party_mcp_tools" = "first_party_mcp_tools" - 'github_token'
|
|
8
|
+
WHERE "first_party_mcp_tools" ? 'github_token';
|
|
9
|
+
|
|
10
|
+
ALTER TABLE "sessions"
|
|
11
|
+
ALTER COLUMN "first_party_mcp_tools"
|
|
12
|
+
SET DEFAULT '[
|
|
13
|
+
"set_session_title",
|
|
14
|
+
"goal_set",
|
|
15
|
+
"goal_update",
|
|
16
|
+
"goal_complete",
|
|
17
|
+
"goal_pause",
|
|
18
|
+
"memory_search",
|
|
19
|
+
"memory_save",
|
|
20
|
+
"memory_correct",
|
|
21
|
+
"preference_registry_summary",
|
|
22
|
+
"preference_registry_get",
|
|
23
|
+
"sandboxes_list",
|
|
24
|
+
"sandbox_attach",
|
|
25
|
+
"sandbox_swap",
|
|
26
|
+
"run_on",
|
|
27
|
+
"sandbox_provision",
|
|
28
|
+
"rig_list",
|
|
29
|
+
"rig_get",
|
|
30
|
+
"rig_propose_change",
|
|
31
|
+
"rig_verify",
|
|
32
|
+
"rig_promote",
|
|
33
|
+
"sessions_list",
|
|
34
|
+
"session_get",
|
|
35
|
+
"session_events",
|
|
36
|
+
"session_create",
|
|
37
|
+
"session_send_message",
|
|
38
|
+
"session_pause",
|
|
39
|
+
"session_resume",
|
|
40
|
+
"session_steer",
|
|
41
|
+
"set_other_session_title",
|
|
42
|
+
"variable_set_list",
|
|
43
|
+
"environment_list",
|
|
44
|
+
"variable_set_set_variable",
|
|
45
|
+
"environment_set_variable",
|
|
46
|
+
"github_connect_link",
|
|
47
|
+
"github_repositories_list",
|
|
48
|
+
"social_connections_list",
|
|
49
|
+
"social_posts_recent",
|
|
50
|
+
"social_daily_analysis_context",
|
|
51
|
+
"social_search_live",
|
|
52
|
+
"social_mentions_live",
|
|
53
|
+
"social_thread_fetch",
|
|
54
|
+
"social_posts_sync",
|
|
55
|
+
"social_post_reply",
|
|
56
|
+
"scheduled_tasks_list",
|
|
57
|
+
"scheduled_tasks_get",
|
|
58
|
+
"scheduled_tasks_create",
|
|
59
|
+
"scheduled_tasks_update",
|
|
60
|
+
"scheduled_tasks_pause",
|
|
61
|
+
"scheduled_tasks_resume",
|
|
62
|
+
"scheduled_tasks_trigger",
|
|
63
|
+
"scheduled_tasks_delete",
|
|
64
|
+
"scheduled_task_runs_list",
|
|
65
|
+
"slack_bot_list_channels",
|
|
66
|
+
"slack_bot_channel_history",
|
|
67
|
+
"slack_bot_thread_replies",
|
|
68
|
+
"slack_bot_list_users",
|
|
69
|
+
"slack_bot_list_files",
|
|
70
|
+
"slack_bot_file_info",
|
|
71
|
+
"slack_bot_file_content",
|
|
72
|
+
"slack_bot_post_message",
|
|
73
|
+
"slack_bot_delete_message",
|
|
74
|
+
"artifacts_list",
|
|
75
|
+
"artifacts_get_source",
|
|
76
|
+
"artifacts_create",
|
|
77
|
+
"artifacts_publish",
|
|
78
|
+
"artifacts_rollback"
|
|
79
|
+
]'::jsonb;
|
|
80
|
+
|
|
81
|
+
ALTER TABLE "sessions"
|
|
82
|
+
DROP CONSTRAINT IF EXISTS "sessions_first_party_mcp_tools_no_model_credentials_chk";
|
|
83
|
+
|
|
84
|
+
ALTER TABLE "sessions"
|
|
85
|
+
ADD CONSTRAINT "sessions_first_party_mcp_tools_no_model_credentials_chk"
|
|
86
|
+
CHECK (NOT ("first_party_mcp_tools" @> '["github_token"]'::jsonb));
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
|
|
3
|
+
SET LOCAL lock_timeout = '5s';
|
|
4
|
+
SET LOCAL statement_timeout = '10min';
|
|
5
|
+
|
|
6
|
+
-- Reject mixed-version writers before locking, then repeat after the locks to
|
|
7
|
+
-- close the connect-before-lock race. The deployment must keep API and workers
|
|
8
|
+
-- stopped until the new application version is active.
|
|
9
|
+
DO $codex_boundaries_writer_drain_before_lock$
|
|
10
|
+
BEGIN
|
|
11
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
|
|
12
|
+
AND EXISTS (
|
|
13
|
+
SELECT 1
|
|
14
|
+
FROM pg_stat_activity
|
|
15
|
+
WHERE datname = current_database()
|
|
16
|
+
AND usename = 'opengeni_app'
|
|
17
|
+
AND pid <> pg_backend_pid()
|
|
18
|
+
)
|
|
19
|
+
THEN
|
|
20
|
+
RAISE EXCEPTION
|
|
21
|
+
'Codex authentication boundaries activation requires all opengeni_app sessions to be stopped'
|
|
22
|
+
USING ERRCODE = '55000';
|
|
23
|
+
END IF;
|
|
24
|
+
END
|
|
25
|
+
$codex_boundaries_writer_drain_before_lock$;
|
|
26
|
+
|
|
27
|
+
LOCK TABLE "codex_subscription_credentials" IN ACCESS EXCLUSIVE MODE;
|
|
28
|
+
LOCK TABLE "session_history_items" IN ACCESS EXCLUSIVE MODE;
|
|
29
|
+
LOCK TABLE "agent_run_states" IN ACCESS EXCLUSIVE MODE;
|
|
30
|
+
|
|
31
|
+
DO $codex_boundaries_writer_drain_after_lock$
|
|
32
|
+
BEGIN
|
|
33
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
|
|
34
|
+
AND EXISTS (
|
|
35
|
+
SELECT 1
|
|
36
|
+
FROM pg_stat_activity
|
|
37
|
+
WHERE datname = current_database()
|
|
38
|
+
AND usename = 'opengeni_app'
|
|
39
|
+
AND pid <> pg_backend_pid()
|
|
40
|
+
)
|
|
41
|
+
THEN
|
|
42
|
+
RAISE EXCEPTION
|
|
43
|
+
'Codex authentication boundaries activation requires all opengeni_app sessions to be stopped'
|
|
44
|
+
USING ERRCODE = '55000';
|
|
45
|
+
END IF;
|
|
46
|
+
END
|
|
47
|
+
$codex_boundaries_writer_drain_after_lock$;
|
|
48
|
+
|
|
49
|
+
-- One optional workspace credential for ChatGPT connected Apps. This pointer is
|
|
50
|
+
-- intentionally separate from inference rotation and starts unset.
|
|
51
|
+
CREATE UNIQUE INDEX "codex_subscription_credentials_workspace_account_id_idx"
|
|
52
|
+
ON "codex_subscription_credentials" ("workspace_id", "account_id", "id");
|
|
53
|
+
|
|
54
|
+
CREATE TABLE "codex_apps_settings" (
|
|
55
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
56
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
57
|
+
"workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
|
|
58
|
+
"credential_id" uuid,
|
|
59
|
+
"version" integer NOT NULL DEFAULT 1,
|
|
60
|
+
"designated_at" timestamptz,
|
|
61
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
62
|
+
CONSTRAINT "codex_apps_settings_workspace_account_fk"
|
|
63
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
64
|
+
REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
|
|
65
|
+
CONSTRAINT "codex_apps_settings_credential_scope_fk"
|
|
66
|
+
FOREIGN KEY ("workspace_id", "account_id", "credential_id")
|
|
67
|
+
REFERENCES "codex_subscription_credentials"("workspace_id", "account_id", "id")
|
|
68
|
+
ON DELETE CASCADE,
|
|
69
|
+
CONSTRAINT "codex_apps_settings_designation_shape_chk" CHECK (
|
|
70
|
+
"version" > 0 AND (
|
|
71
|
+
("credential_id" IS NULL AND "designated_at" IS NULL)
|
|
72
|
+
OR
|
|
73
|
+
("credential_id" IS NOT NULL AND "designated_at" IS NOT NULL)
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE UNIQUE INDEX "codex_apps_settings_workspace_idx"
|
|
79
|
+
ON "codex_apps_settings" ("workspace_id");
|
|
80
|
+
|
|
81
|
+
ALTER TABLE "codex_apps_settings" ENABLE ROW LEVEL SECURITY;
|
|
82
|
+
ALTER TABLE "codex_apps_settings" FORCE ROW LEVEL SECURITY;
|
|
83
|
+
CREATE POLICY workspace_isolation ON "codex_apps_settings"
|
|
84
|
+
USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
|
|
85
|
+
WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
|
|
86
|
+
|
|
87
|
+
-- Remove the false credential/history relation and the obsolete
|
|
88
|
+
-- connector-aware inference cache. Provider rejection receipts remain.
|
|
89
|
+
ALTER TABLE "session_history_items"
|
|
90
|
+
DROP COLUMN "producer_codex_credential_id";
|
|
91
|
+
ALTER TABLE "agent_run_states"
|
|
92
|
+
DROP COLUMN "frozen_codex_credential_id";
|
|
93
|
+
ALTER TABLE "codex_subscription_credentials"
|
|
94
|
+
DROP COLUMN "connector_namespaces",
|
|
95
|
+
DROP COLUMN "connectors_checked_at";
|
|
96
|
+
|
|
97
|
+
DO $$
|
|
98
|
+
DECLARE
|
|
99
|
+
target_schema text := current_schema();
|
|
100
|
+
BEGIN
|
|
101
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
102
|
+
EXECUTE format(
|
|
103
|
+
'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.codex_apps_settings TO opengeni_app',
|
|
104
|
+
target_schema
|
|
105
|
+
);
|
|
106
|
+
END IF;
|
|
107
|
+
END $$;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Historical, fully quiesced interruptions are audit evidence, not live control work.
|
|
3
|
+
|
|
4
|
+
SET lock_timeout = '5s';
|
|
5
|
+
SET statement_timeout = '10min';
|
|
6
|
+
|
|
7
|
+
DROP FUNCTION opengeni_private.claim_session_workflow_wakes(integer);
|
|
8
|
+
|
|
9
|
+
DO $migration$
|
|
10
|
+
DECLARE target_schema text := current_schema();
|
|
11
|
+
BEGIN
|
|
12
|
+
EXECUTE format($create$
|
|
13
|
+
CREATE FUNCTION opengeni_private.claim_session_workflow_wakes(p_limit integer)
|
|
14
|
+
RETURNS TABLE (
|
|
15
|
+
account_id uuid,
|
|
16
|
+
workspace_id uuid,
|
|
17
|
+
session_id uuid,
|
|
18
|
+
temporal_workflow_id text,
|
|
19
|
+
wake_revision bigint,
|
|
20
|
+
interruption_requested boolean
|
|
21
|
+
)
|
|
22
|
+
LANGUAGE plpgsql
|
|
23
|
+
SECURITY DEFINER
|
|
24
|
+
SET search_path = pg_catalog
|
|
25
|
+
AS $function$
|
|
26
|
+
BEGIN
|
|
27
|
+
RETURN QUERY
|
|
28
|
+
WITH due AS (
|
|
29
|
+
SELECT o.session_id
|
|
30
|
+
FROM %1$I.session_workflow_wake_outbox o
|
|
31
|
+
WHERE o.wake_revision > o.delivered_revision
|
|
32
|
+
AND o.next_attempt_at <= now()
|
|
33
|
+
ORDER BY o.next_attempt_at, o.updated_at, o.session_id
|
|
34
|
+
FOR UPDATE SKIP LOCKED
|
|
35
|
+
LIMIT greatest(1, least(coalesce(p_limit, 100), 1000))
|
|
36
|
+
)
|
|
37
|
+
UPDATE %1$I.session_workflow_wake_outbox o
|
|
38
|
+
SET attempts = o.attempts + 1,
|
|
39
|
+
next_attempt_at = now() + make_interval(
|
|
40
|
+
secs => least(300, greatest(1, power(2, least(o.attempts, 8))::integer))
|
|
41
|
+
),
|
|
42
|
+
updated_at = now()
|
|
43
|
+
FROM due
|
|
44
|
+
WHERE o.session_id = due.session_id
|
|
45
|
+
RETURNING o.account_id, o.workspace_id, o.session_id,
|
|
46
|
+
o.temporal_workflow_id, o.wake_revision,
|
|
47
|
+
o.control_revision > o.delivered_revision
|
|
48
|
+
OR EXISTS (
|
|
49
|
+
SELECT 1
|
|
50
|
+
FROM %1$I.session_attempt_interruptions interruption
|
|
51
|
+
JOIN %1$I.session_turn_attempts attempt
|
|
52
|
+
ON attempt.workspace_id = interruption.workspace_id
|
|
53
|
+
AND attempt.session_id = interruption.session_id
|
|
54
|
+
AND attempt.id = interruption.attempt_id
|
|
55
|
+
WHERE interruption.workspace_id = o.workspace_id
|
|
56
|
+
AND interruption.session_id = o.session_id
|
|
57
|
+
AND (
|
|
58
|
+
interruption.state IN ('pending', 'delivered', 'acknowledged')
|
|
59
|
+
OR (
|
|
60
|
+
interruption.state IN ('settled', 'rejected_stale')
|
|
61
|
+
AND attempt.quiesced_at IS NULL
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
) AS interruption_requested;
|
|
65
|
+
END $function$;
|
|
66
|
+
$create$, target_schema);
|
|
67
|
+
END $migration$;
|
|
68
|
+
|
|
69
|
+
REVOKE ALL ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer) FROM PUBLIC;
|
|
70
|
+
|
|
71
|
+
DO $$
|
|
72
|
+
BEGIN
|
|
73
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
74
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer)
|
|
75
|
+
TO opengeni_app;
|
|
76
|
+
END IF;
|
|
77
|
+
END $$;
|
|
78
|
+
|
|
79
|
+
RESET statement_timeout;
|
|
80
|
+
RESET lock_timeout;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.11",
|
|
4
4
|
"description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@opengeni/codex": "^0.2.
|
|
54
|
-
"@opengeni/config": "^0.10.
|
|
55
|
-
"@opengeni/contracts": "^0.38.
|
|
53
|
+
"@opengeni/codex": "^0.2.11",
|
|
54
|
+
"@opengeni/config": "^0.10.14",
|
|
55
|
+
"@opengeni/contracts": "^0.38.3",
|
|
56
56
|
"@opengeni/network": "^0.2.0",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"postgres": "^3.4.7"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hoisted here from apps/worker/src/activities/codex-auth.ts so BOTH the worker
|
|
4
4
|
// (turn-time bearer for the streamed run) AND the api (the /wham/usage quota-bar
|
|
5
5
|
// reads) drive ONE resolver — no duplicated refresh/CAS/single-flight logic. The
|
|
6
|
-
// worker re-exports buildCodexTokenResolver from
|
|
6
|
+
// worker re-exports buildCodexTokenResolver from @opengeni/db for back-compat, so
|
|
7
7
|
// the agent-turn.ts call site is unchanged.
|
|
8
8
|
//
|
|
9
9
|
// Why @opengeni/db is the right home: the resolver only orchestrates accessors
|
|
@@ -35,15 +35,52 @@ import {
|
|
|
35
35
|
refreshCodexToken,
|
|
36
36
|
} from "@opengeni/codex";
|
|
37
37
|
import { encryptEnvironmentValue } from "./environment-crypto";
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
import type { Database } from "./database";
|
|
39
|
+
|
|
40
|
+
export type CodexCredentialTokens = {
|
|
41
|
+
accessToken: string;
|
|
42
|
+
refreshToken: string;
|
|
43
|
+
idToken: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type CodexCredentialForRun = {
|
|
47
|
+
id: string;
|
|
48
|
+
version: number;
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
tokens: CodexCredentialTokens;
|
|
51
|
+
chatgptAccountId: string | null;
|
|
52
|
+
scopes: string | null;
|
|
53
|
+
planType: string | null;
|
|
54
|
+
isFedramp: boolean;
|
|
55
|
+
expiresAt: Date | null;
|
|
56
|
+
lastRefreshAt: Date | null;
|
|
57
|
+
status: string;
|
|
58
|
+
lastError: string | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type CodexAccountUsageSnapshot = {
|
|
62
|
+
primaryUsedPercent?: number | null;
|
|
63
|
+
primaryResetAt?: Date | null;
|
|
64
|
+
secondaryUsedPercent?: number | null;
|
|
65
|
+
secondaryResetAt?: Date | null;
|
|
66
|
+
checkedAt?: Date;
|
|
67
|
+
resetCreditAvailableCount?: number | null;
|
|
68
|
+
resetCreditsCheckedAt?: Date | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type CodexCredentialRefreshInput = {
|
|
72
|
+
id: string;
|
|
73
|
+
version: number;
|
|
74
|
+
workspaceId: string;
|
|
75
|
+
credentialEncrypted: string;
|
|
76
|
+
expiresAt: Date | null;
|
|
77
|
+
lastRefreshAt: Date;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type CodexCredentialStatusTarget = {
|
|
81
|
+
id: string;
|
|
82
|
+
version: number;
|
|
83
|
+
};
|
|
47
84
|
|
|
48
85
|
// Single-flight per CREDENTIAL INSTANCE (row id + version), process-module scope.
|
|
49
86
|
// Keying by the loaded credential's id+version — NOT by workspaceId alone (P1-b) —
|
|
@@ -142,25 +179,37 @@ export async function withCodexTokenDeadline<T>(
|
|
|
142
179
|
|
|
143
180
|
// Dependencies are injectable so the lifecycle logic (single-flight, staleness,
|
|
144
181
|
// needs_relogin transition) is unit-testable without a database. Production uses
|
|
145
|
-
// the real db + codex functions
|
|
182
|
+
// the root composition wrapper supplies the real db + codex functions.
|
|
146
183
|
export type CodexAuthDeps = {
|
|
147
|
-
loadCredential:
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
loadCredential: (
|
|
185
|
+
db: Database,
|
|
186
|
+
settings: Settings,
|
|
187
|
+
workspaceId: string,
|
|
188
|
+
credentialId: string,
|
|
189
|
+
) => Promise<CodexCredentialForRun | null>;
|
|
190
|
+
recordRefresh: (db: Database, input: CodexCredentialRefreshInput) => Promise<boolean>;
|
|
191
|
+
setStatus: (
|
|
192
|
+
db: Database,
|
|
193
|
+
workspaceId: string,
|
|
194
|
+
status: "active" | "needs_relogin" | "error",
|
|
195
|
+
lastError: string | null,
|
|
196
|
+
target: CodexCredentialStatusTarget,
|
|
197
|
+
) => Promise<boolean>;
|
|
150
198
|
refresh: typeof refreshCodexToken;
|
|
151
199
|
encrypt: typeof encryptEnvironmentValue;
|
|
152
200
|
keyBytes: typeof environmentsEncryptionKeyBytes;
|
|
153
|
-
withRefreshLock:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
withRefreshLock: <T>(
|
|
202
|
+
db: Database,
|
|
203
|
+
workspaceId: string,
|
|
204
|
+
credentialId: string,
|
|
205
|
+
fn: (lockedDb: Database) => Promise<T>,
|
|
206
|
+
) => Promise<T>;
|
|
207
|
+
recordUsage?: (
|
|
208
|
+
db: Database,
|
|
209
|
+
workspaceId: string,
|
|
210
|
+
credentialId: string,
|
|
211
|
+
snapshot: CodexAccountUsageSnapshot,
|
|
212
|
+
) => Promise<boolean>;
|
|
164
213
|
};
|
|
165
214
|
|
|
166
215
|
export function buildCodexTokenResolver(
|
|
@@ -173,7 +222,7 @@ export function buildCodexTokenResolver(
|
|
|
173
222
|
// 0 rows against the now-inactive row — so a refresh racing a switch can never
|
|
174
223
|
// clobber the newly-active account. The single-flight map needs zero change.
|
|
175
224
|
credentialId: string,
|
|
176
|
-
deps: CodexAuthDeps
|
|
225
|
+
deps: CodexAuthDeps,
|
|
177
226
|
): { getToken: () => Promise<CodexTokenSnapshot>; refresh: () => Promise<CodexTokenSnapshot> } {
|
|
178
227
|
const snapshot = (cred: CodexCredentialForRun): CodexTokenSnapshot => ({
|
|
179
228
|
accessToken: cred.tokens.accessToken,
|
|
@@ -336,9 +385,10 @@ export async function fetchCodexUsageForAccount(
|
|
|
336
385
|
settings: Settings,
|
|
337
386
|
workspaceId: string,
|
|
338
387
|
credentialId: string,
|
|
388
|
+
deps: CodexAuthDeps,
|
|
339
389
|
fetchImpl: CodexFetch = fetch,
|
|
340
390
|
): Promise<CodexUsagePayload> {
|
|
341
|
-
const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId);
|
|
391
|
+
const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId, deps);
|
|
342
392
|
let token: CodexTokenSnapshot;
|
|
343
393
|
try {
|
|
344
394
|
token = await resolver.getToken();
|
|
@@ -373,27 +423,29 @@ export async function fetchCodexUsageForAccount(
|
|
|
373
423
|
// cached without erasing or falsely refreshing the last valid quota truth.
|
|
374
424
|
// Cache-write is best-effort: a disconnect under us (false) or a transient
|
|
375
425
|
// write error must NOT sink the freshly-read result we are about to return.
|
|
376
|
-
await
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
426
|
+
await deps
|
|
427
|
+
.recordUsage?.(db, workspaceId, credentialId, {
|
|
428
|
+
...(parsedQuota
|
|
429
|
+
? {
|
|
430
|
+
primaryUsedPercent: normalized.fiveHour?.percent ?? null,
|
|
431
|
+
primaryResetAt: normalized.fiveHour?.resetAt
|
|
432
|
+
? new Date(normalized.fiveHour.resetAt)
|
|
433
|
+
: null,
|
|
434
|
+
secondaryUsedPercent: normalized.weekly?.percent ?? null,
|
|
435
|
+
secondaryResetAt: normalized.weekly?.resetAt
|
|
436
|
+
? new Date(normalized.weekly.resetAt)
|
|
437
|
+
: null,
|
|
438
|
+
checkedAt,
|
|
439
|
+
}
|
|
440
|
+
: {}),
|
|
441
|
+
...(normalized.rateLimitResetCredits
|
|
442
|
+
? {
|
|
443
|
+
resetCreditAvailableCount: normalized.rateLimitResetCredits.availableCount,
|
|
444
|
+
resetCreditsCheckedAt: checkedAt,
|
|
445
|
+
}
|
|
446
|
+
: {}),
|
|
447
|
+
})
|
|
448
|
+
.catch(() => undefined);
|
|
397
449
|
}
|
|
398
450
|
|
|
399
451
|
return normalized;
|
|
@@ -418,9 +470,10 @@ export async function fetchCodexRateLimitResetCreditsForAccount(
|
|
|
418
470
|
settings: Settings,
|
|
419
471
|
workspaceId: string,
|
|
420
472
|
credentialId: string,
|
|
473
|
+
deps: CodexAuthDeps,
|
|
421
474
|
fetchImpl: CodexFetch = fetch,
|
|
422
475
|
): Promise<CodexRateLimitResetCreditsAccountResult> {
|
|
423
|
-
const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId);
|
|
476
|
+
const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId, deps);
|
|
424
477
|
let token: CodexTokenSnapshot;
|
|
425
478
|
try {
|
|
426
479
|
token = await resolver.getToken();
|