@opengeni/api-router 4.0.0 → 4.1.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/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  createOpenGeniSlackBotInteractionClient,
8
8
  startSlackInteractionPump,
9
9
  startTemporalScheduleCleanupPump
10
- } from "./chunk-E7TZFJ65.js";
10
+ } from "./chunk-24C5LPNE.js";
11
11
 
12
12
  // src/index.ts
13
13
  import { scheduledTaskKnowledgeSource } from "@opengeni/contracts";
@@ -46,7 +46,7 @@ export declare function requireGitHubLensConnect(deps: ApiRouteDeps): Promise<{
46
46
  eager?: boolean | undefined;
47
47
  optional?: boolean | undefined;
48
48
  }[];
49
- firstPartyMcpTools: ("artifacts_archive" | "artifacts_create" | "artifacts_get_source" | "artifacts_list" | "artifacts_prepare_upload" | "artifacts_publish" | "artifacts_restore" | "artifacts_rollback" | "atlassian_get" | "atlassian_search" | "atlassian_sources_list" | "browser_act" | "browser_auth" | "browser_clipboard" | "browser_debug" | "browser_identity" | "browser_lifecycle" | "browser_observe" | "browser_open" | "browser_publish" | "browser_tabs" | "capability_authorization_request" | "capability_catalog_search" | "command_read" | "command_wait" | "company_profile_confirm" | "company_profile_propose" | "computer_act" | "computer_clipboard" | "computer_lifecycle" | "computer_observe" | "computer_open" | "computer_targets" | "connected_machine_remove" | "editable_artifact_apply" | "editable_artifact_create" | "editable_artifact_export" | "editable_artifact_export_status" | "editable_artifact_get" | "editable_artifact_import" | "editable_artifact_inspect" | "editable_artifact_list" | "environment_list" | "environment_set_variable" | "fiken_bank_accounts_list" | "fiken_companies_list" | "fiken_contact_create" | "fiken_contacts_list" | "fiken_invoice_draft_create" | "fiken_invoice_get" | "fiken_invoices_list" | "fiken_products_list" | "fiken_purchases_list" | "fiken_sales_list" | "github_connect_link" | "github_repositories_list" | "goal_complete" | "goal_pause" | "goal_progress" | "goal_resume" | "goal_set" | "goal_update" | "instruction_policy_get" | "instruction_policy_propose" | "instruction_policy_save" | "interaction_discover" | "interaction_request_human" | "knowledge_archive" | "knowledge_browse" | "knowledge_correct" | "knowledge_get" | "knowledge_prepare_save" | "knowledge_propose" | "knowledge_retain_file" | "knowledge_retain_message" | "knowledge_save" | "knowledge_search" | "memory_correct" | "memory_save" | "memory_search" | "preference_propose" | "preference_registry_get" | "preference_registry_summary" | "project_create" | "project_delete" | "project_get" | "project_list" | "project_reorder" | "project_update" | "reddit_accounts_list" | "reddit_mentions_live" | "reddit_post_reply" | "reddit_posts_sync" | "reddit_search_live" | "reddit_thread_fetch" | "remember" | "remember_confirm" | "rig_get" | "rig_list" | "rig_promote" | "rig_propose_change" | "rig_verify" | "run_on" | "sandbox_attach" | "sandbox_file_publish" | "sandbox_provision" | "sandbox_swap" | "sandboxes_list" | "scheduled_task_runs_list" | "scheduled_tasks_create" | "scheduled_tasks_delete" | "scheduled_tasks_get" | "scheduled_tasks_list" | "scheduled_tasks_pause" | "scheduled_tasks_resume" | "scheduled_tasks_trigger" | "scheduled_tasks_update" | "session_create" | "session_events" | "session_get" | "session_human_input_respond" | "session_pause" | "session_resume" | "session_send_message" | "session_set_project" | "session_steer" | "session_wait" | "sessions_list" | "set_other_session_title" | "set_session_title" | "slack_bot_channel_history" | "slack_bot_delete_message" | "slack_bot_file_content" | "slack_bot_file_info" | "slack_bot_list_channels" | "slack_bot_list_files" | "slack_bot_list_users" | "slack_bot_post_message" | "slack_bot_search" | "slack_bot_thread_replies" | "social_connections_list" | "social_daily_analysis_context" | "social_mentions_live" | "social_post_reply" | "social_posts_recent" | "social_posts_sync" | "social_search_live" | "social_thread_fetch" | "task_note_archive" | "task_note_promote_instruction_policy" | "task_note_promote_knowledge" | "task_note_promote_preference" | "task_note_replace" | "task_note_save" | "task_notes_list" | "variable_set_get_variable" | "variable_set_list" | "variable_set_set_variable" | "wait_for_input" | "work_claim_release" | "work_claim_upsert" | "x_accounts_list" | "x_mentions_live" | "x_post_reply" | "x_posts_sync" | "x_search_live" | "x_thread_fetch")[];
49
+ firstPartyMcpTools: ("artifacts_archive" | "artifacts_create" | "artifacts_get_source" | "artifacts_list" | "artifacts_prepare_upload" | "artifacts_publish" | "artifacts_restore" | "artifacts_rollback" | "atlassian_get" | "atlassian_search" | "atlassian_sources_list" | "browser_act" | "browser_auth" | "browser_clipboard" | "browser_debug" | "browser_identity" | "browser_lifecycle" | "browser_observe" | "browser_open" | "browser_publish" | "browser_tabs" | "capability_authorization_request" | "capability_catalog_search" | "command_read" | "command_wait" | "company_profile_confirm" | "company_profile_propose" | "computer_act" | "computer_clipboard" | "computer_lifecycle" | "computer_observe" | "computer_open" | "computer_targets" | "connected_machine_enroll_token" | "connected_machine_remove" | "editable_artifact_apply" | "editable_artifact_create" | "editable_artifact_export" | "editable_artifact_export_status" | "editable_artifact_get" | "editable_artifact_import" | "editable_artifact_inspect" | "editable_artifact_list" | "environment_list" | "environment_set_variable" | "fiken_bank_accounts_list" | "fiken_companies_list" | "fiken_contact_create" | "fiken_contacts_list" | "fiken_invoice_draft_create" | "fiken_invoice_get" | "fiken_invoices_list" | "fiken_products_list" | "fiken_purchases_list" | "fiken_sales_list" | "github_connect_link" | "github_repositories_list" | "goal_complete" | "goal_pause" | "goal_progress" | "goal_resume" | "goal_set" | "goal_update" | "instruction_policy_get" | "instruction_policy_propose" | "instruction_policy_save" | "interaction_discover" | "interaction_request_human" | "knowledge_archive" | "knowledge_browse" | "knowledge_correct" | "knowledge_get" | "knowledge_prepare_save" | "knowledge_propose" | "knowledge_retain_file" | "knowledge_retain_message" | "knowledge_save" | "knowledge_search" | "memory_correct" | "memory_save" | "memory_search" | "preference_propose" | "preference_registry_get" | "preference_registry_summary" | "project_create" | "project_delete" | "project_get" | "project_list" | "project_reorder" | "project_update" | "reddit_accounts_list" | "reddit_mentions_live" | "reddit_post_reply" | "reddit_posts_sync" | "reddit_search_live" | "reddit_thread_fetch" | "remember" | "remember_confirm" | "rig_get" | "rig_list" | "rig_promote" | "rig_propose_change" | "rig_verify" | "run_on" | "sandbox_attach" | "sandbox_file_publish" | "sandbox_provision" | "sandbox_swap" | "sandboxes_list" | "scheduled_task_runs_list" | "scheduled_tasks_create" | "scheduled_tasks_delete" | "scheduled_tasks_get" | "scheduled_tasks_list" | "scheduled_tasks_pause" | "scheduled_tasks_resume" | "scheduled_tasks_trigger" | "scheduled_tasks_update" | "session_create" | "session_events" | "session_get" | "session_human_input_respond" | "session_pause" | "session_resume" | "session_send_message" | "session_set_project" | "session_steer" | "session_wait" | "sessions_list" | "set_other_session_title" | "set_session_title" | "slack_bot_channel_history" | "slack_bot_delete_message" | "slack_bot_file_content" | "slack_bot_file_info" | "slack_bot_list_channels" | "slack_bot_list_files" | "slack_bot_list_users" | "slack_bot_post_message" | "slack_bot_search" | "slack_bot_thread_replies" | "social_connections_list" | "social_daily_analysis_context" | "social_mentions_live" | "social_post_reply" | "social_posts_recent" | "social_posts_sync" | "social_search_live" | "social_thread_fetch" | "task_note_archive" | "task_note_promote_instruction_policy" | "task_note_promote_knowledge" | "task_note_promote_preference" | "task_note_replace" | "task_note_save" | "task_notes_list" | "variable_set_get_variable" | "variable_set_list" | "variable_set_set_variable" | "wait_for_input" | "work_claim_release" | "work_claim_upsert" | "x_accounts_list" | "x_mentions_live" | "x_post_reply" | "x_posts_sync" | "x_search_live" | "x_thread_fetch")[];
50
50
  firstPartyMcpPermissions: ("account:admin" | "account:read" | "api_keys:manage" | "artifacts:publish" | "artifacts:read" | "billing:manage" | "billing:read" | "capabilities:manage" | "codemode:call" | "connections:read" | "connections:write" | "documents:manage" | "documents:search" | "enrollments:manage" | "enrollments:read" | "environments:manage" | "environments:use" | "files:read" | "files:upload" | "files:write" | "github:manage" | "github:use" | "goals:manage" | "mcp_servers:attach" | "members:manage" | "rigs:manage" | "rigs:use" | "scheduled_tasks:manage" | "scheduled_tasks:run" | "secrets:list" | "secrets:read" | "secrets:write" | "sessions:control" | "sessions:create" | "sessions:read" | "stream:acknowledge" | "stream:control" | "stream:view" | "terminal:attach" | "variable-sets:attach" | "variable-sets:list" | "variable-sets:manage" | "variable-sets:read" | "variable-sets:use" | "variable-sets:write" | "workspace:admin" | "workspace:create" | "workspace:read")[];
51
51
  model: string | null;
52
52
  reasoningEffort: "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh" | null;
@@ -39,7 +39,7 @@ export type OAuthCallbackResult = {
39
39
  type AuthorizationServerMetadata = McpAuthorizationServerMetadata;
40
40
  export declare const OAUTH_START_DEADLINE_MS = 15000;
41
41
  export declare const OAUTH_CALLBACK_DEADLINE_MS = 30000;
42
- export type OAuthStartStage = "connection_lookup" | "mcp_challenge" | "protected_resource_metadata" | "authorization_server_metadata" | "client_registration";
42
+ export type OAuthStartStage = "connection_lookup" | "mcp_challenge" | "protected_resource_metadata" | "authorization_server_metadata" | "client_registration" | "state_persist";
43
43
  /** Policy is the outermost lock. Receipt replay still runs normal authorization,
44
44
  * but the DB calls authorizeAcquisition only for uncommitted progress. Provider
45
45
  * requests must happen after this transaction, never inside either wrapper. */
@@ -813,4 +813,12 @@ export declare function createSlackFilesListCursor(input: {
813
813
  page: number;
814
814
  }, context: SlackFilesCursorContext): string;
815
815
  export declare function nextSlackFilesListPage(payload: SlackPayload, requested: SlackFilesListPage, returnedFileCount: number): number | null;
816
+ /**
817
+ * Slack action IDs are unique within a block. Keep the logical card untouched
818
+ * for the durable request digest, and partition repeated actions only on the
819
+ * provider wire. In particular, a legacy invalid_blocks receipt can retry with
820
+ * its original operation ID and digest; completed/uncertain posts keep their
821
+ * existing replay/reconciliation path. Values and action IDs remain exact.
822
+ */
823
+ export declare function slackMessageWireBlocks(blocks: SlackMessageBlock[] | undefined): SlackMessageBlock[] | undefined;
816
824
  export {};
@@ -19,7 +19,7 @@ export declare const SLACK_SESSION_INSTRUCTIONS: string;
19
19
  * selection at session creation while keeping Slack mutations out of the model
20
20
  * surface; interaction delivery remains owned by the durable delivery pump.
21
21
  */
22
- export declare const SLACK_TASK_FIRST_PARTY_MCP_TOOLS: ("artifacts_archive" | "artifacts_create" | "artifacts_get_source" | "artifacts_list" | "artifacts_prepare_upload" | "artifacts_publish" | "artifacts_restore" | "artifacts_rollback" | "atlassian_get" | "atlassian_search" | "atlassian_sources_list" | "browser_act" | "browser_auth" | "browser_clipboard" | "browser_debug" | "browser_identity" | "browser_lifecycle" | "browser_observe" | "browser_open" | "browser_publish" | "browser_tabs" | "capability_authorization_request" | "capability_catalog_search" | "command_read" | "command_wait" | "company_profile_confirm" | "company_profile_propose" | "computer_act" | "computer_clipboard" | "computer_lifecycle" | "computer_observe" | "computer_open" | "computer_targets" | "connected_machine_remove" | "editable_artifact_apply" | "editable_artifact_create" | "editable_artifact_export" | "editable_artifact_export_status" | "editable_artifact_get" | "editable_artifact_import" | "editable_artifact_inspect" | "editable_artifact_list" | "environment_list" | "environment_set_variable" | "fiken_bank_accounts_list" | "fiken_companies_list" | "fiken_contact_create" | "fiken_contacts_list" | "fiken_invoice_draft_create" | "fiken_invoice_get" | "fiken_invoices_list" | "fiken_products_list" | "fiken_purchases_list" | "fiken_sales_list" | "github_connect_link" | "github_repositories_list" | "goal_complete" | "goal_pause" | "goal_progress" | "goal_resume" | "goal_set" | "goal_update" | "instruction_policy_get" | "instruction_policy_propose" | "instruction_policy_save" | "interaction_discover" | "interaction_request_human" | "knowledge_archive" | "knowledge_browse" | "knowledge_correct" | "knowledge_get" | "knowledge_prepare_save" | "knowledge_propose" | "knowledge_retain_file" | "knowledge_retain_message" | "knowledge_save" | "knowledge_search" | "memory_correct" | "memory_save" | "memory_search" | "preference_propose" | "preference_registry_get" | "preference_registry_summary" | "project_create" | "project_delete" | "project_get" | "project_list" | "project_reorder" | "project_update" | "reddit_accounts_list" | "reddit_mentions_live" | "reddit_post_reply" | "reddit_posts_sync" | "reddit_search_live" | "reddit_thread_fetch" | "remember" | "remember_confirm" | "rig_get" | "rig_list" | "rig_promote" | "rig_propose_change" | "rig_verify" | "run_on" | "sandbox_attach" | "sandbox_file_publish" | "sandbox_provision" | "sandbox_swap" | "sandboxes_list" | "scheduled_task_runs_list" | "scheduled_tasks_create" | "scheduled_tasks_delete" | "scheduled_tasks_get" | "scheduled_tasks_list" | "scheduled_tasks_pause" | "scheduled_tasks_resume" | "scheduled_tasks_trigger" | "scheduled_tasks_update" | "session_create" | "session_events" | "session_get" | "session_human_input_respond" | "session_pause" | "session_resume" | "session_send_message" | "session_set_project" | "session_steer" | "session_wait" | "sessions_list" | "set_other_session_title" | "set_session_title" | "slack_bot_channel_history" | "slack_bot_delete_message" | "slack_bot_file_content" | "slack_bot_file_info" | "slack_bot_list_channels" | "slack_bot_list_files" | "slack_bot_list_users" | "slack_bot_post_message" | "slack_bot_search" | "slack_bot_thread_replies" | "social_connections_list" | "social_daily_analysis_context" | "social_mentions_live" | "social_post_reply" | "social_posts_recent" | "social_posts_sync" | "social_search_live" | "social_thread_fetch" | "task_note_archive" | "task_note_promote_instruction_policy" | "task_note_promote_knowledge" | "task_note_promote_preference" | "task_note_replace" | "task_note_save" | "task_notes_list" | "variable_set_get_variable" | "variable_set_list" | "variable_set_set_variable" | "wait_for_input" | "work_claim_release" | "work_claim_upsert" | "x_accounts_list" | "x_mentions_live" | "x_post_reply" | "x_posts_sync" | "x_search_live" | "x_thread_fetch")[];
22
+ export declare const SLACK_TASK_FIRST_PARTY_MCP_TOOLS: ("artifacts_archive" | "artifacts_create" | "artifacts_get_source" | "artifacts_list" | "artifacts_prepare_upload" | "artifacts_publish" | "artifacts_restore" | "artifacts_rollback" | "atlassian_get" | "atlassian_search" | "atlassian_sources_list" | "browser_act" | "browser_auth" | "browser_clipboard" | "browser_debug" | "browser_identity" | "browser_lifecycle" | "browser_observe" | "browser_open" | "browser_publish" | "browser_tabs" | "capability_authorization_request" | "capability_catalog_search" | "command_read" | "command_wait" | "company_profile_confirm" | "company_profile_propose" | "computer_act" | "computer_clipboard" | "computer_lifecycle" | "computer_observe" | "computer_open" | "computer_targets" | "connected_machine_enroll_token" | "connected_machine_remove" | "editable_artifact_apply" | "editable_artifact_create" | "editable_artifact_export" | "editable_artifact_export_status" | "editable_artifact_get" | "editable_artifact_import" | "editable_artifact_inspect" | "editable_artifact_list" | "environment_list" | "environment_set_variable" | "fiken_bank_accounts_list" | "fiken_companies_list" | "fiken_contact_create" | "fiken_contacts_list" | "fiken_invoice_draft_create" | "fiken_invoice_get" | "fiken_invoices_list" | "fiken_products_list" | "fiken_purchases_list" | "fiken_sales_list" | "github_connect_link" | "github_repositories_list" | "goal_complete" | "goal_pause" | "goal_progress" | "goal_resume" | "goal_set" | "goal_update" | "instruction_policy_get" | "instruction_policy_propose" | "instruction_policy_save" | "interaction_discover" | "interaction_request_human" | "knowledge_archive" | "knowledge_browse" | "knowledge_correct" | "knowledge_get" | "knowledge_prepare_save" | "knowledge_propose" | "knowledge_retain_file" | "knowledge_retain_message" | "knowledge_save" | "knowledge_search" | "memory_correct" | "memory_save" | "memory_search" | "preference_propose" | "preference_registry_get" | "preference_registry_summary" | "project_create" | "project_delete" | "project_get" | "project_list" | "project_reorder" | "project_update" | "reddit_accounts_list" | "reddit_mentions_live" | "reddit_post_reply" | "reddit_posts_sync" | "reddit_search_live" | "reddit_thread_fetch" | "remember" | "remember_confirm" | "rig_get" | "rig_list" | "rig_promote" | "rig_propose_change" | "rig_verify" | "run_on" | "sandbox_attach" | "sandbox_file_publish" | "sandbox_provision" | "sandbox_swap" | "sandboxes_list" | "scheduled_task_runs_list" | "scheduled_tasks_create" | "scheduled_tasks_delete" | "scheduled_tasks_get" | "scheduled_tasks_list" | "scheduled_tasks_pause" | "scheduled_tasks_resume" | "scheduled_tasks_trigger" | "scheduled_tasks_update" | "session_create" | "session_events" | "session_get" | "session_human_input_respond" | "session_pause" | "session_resume" | "session_send_message" | "session_set_project" | "session_steer" | "session_wait" | "sessions_list" | "set_other_session_title" | "set_session_title" | "slack_bot_channel_history" | "slack_bot_delete_message" | "slack_bot_file_content" | "slack_bot_file_info" | "slack_bot_list_channels" | "slack_bot_list_files" | "slack_bot_list_users" | "slack_bot_post_message" | "slack_bot_search" | "slack_bot_thread_replies" | "social_connections_list" | "social_daily_analysis_context" | "social_mentions_live" | "social_post_reply" | "social_posts_recent" | "social_posts_sync" | "social_search_live" | "social_thread_fetch" | "task_note_archive" | "task_note_promote_instruction_policy" | "task_note_promote_knowledge" | "task_note_promote_preference" | "task_note_replace" | "task_note_save" | "task_notes_list" | "variable_set_get_variable" | "variable_set_list" | "variable_set_set_variable" | "wait_for_input" | "work_claim_release" | "work_claim_upsert" | "x_accounts_list" | "x_mentions_live" | "x_post_reply" | "x_posts_sync" | "x_search_live" | "x_thread_fetch")[];
23
23
  export type NormalizedSlackInteraction = {
24
24
  providerEventId: string;
25
25
  providerMessageId: string;
@@ -0,0 +1,9 @@
1
+ /** Select only whole items that actually survived the model-facing envelope.
2
+ * A cursor is a position, never proof that preceding content was consumed. */
3
+ export declare function completeChildReadSequences(page: {
4
+ view: string;
5
+ sourceExact: boolean;
6
+ events: readonly ({
7
+ sequence: number;
8
+ } & Record<string, unknown>)[];
9
+ }): number[];
@@ -193,6 +193,9 @@ export declare const FIRST_PARTY_TOOL_AUTHORIZATION: {
193
193
  connected_machine_remove: {
194
194
  allOf: "enrollments:manage"[];
195
195
  };
196
+ connected_machine_enroll_token: {
197
+ allOf: "enrollments:manage"[];
198
+ };
196
199
  rig_list: {
197
200
  allOf: "rigs:use"[];
198
201
  };
@@ -69,6 +69,8 @@ export type SessionWaitTarget = {
69
69
  afterSequence: number;
70
70
  };
71
71
  export type SessionWaitEventSummary = {
72
+ /** True only when every semantic result field survived all projections. */
73
+ contentComplete?: boolean;
72
74
  id: string;
73
75
  sequence: number;
74
76
  type: SessionEventType;
@@ -1,3 +1,4 @@
1
+ import { type Settings } from "@opengeni/config";
1
2
  import { type CodexAccountStatus } from "@opengeni/db";
2
3
  export declare function codexAccountJson(row: CodexAccountStatus, options?: {
3
4
  appsCredentialId?: string | null;
@@ -44,7 +45,7 @@ export declare function codexWorkerReadiness(input: {
44
45
  poolReady: boolean;
45
46
  workerRoutable: boolean;
46
47
  };
47
- export declare function codexModelsForPicker(): Array<{
48
+ export declare function codexModelsForPicker(settings?: Settings): Array<{
48
49
  id: string;
49
50
  label: string;
50
51
  provider: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,22 +44,22 @@
44
44
  "@llamaindex/liteparse": "2.14.2",
45
45
  "@modelcontextprotocol/sdk": "^1.29.0",
46
46
  "@opengeni/agent-proto": "^0.6.0",
47
- "@opengeni/artifact-tool": "^0.3.30",
47
+ "@opengeni/artifact-tool": "^0.3.31",
48
48
  "@opengeni/capabilities": "^0.3.4",
49
- "@opengeni/codemode": "^0.6.0",
50
- "@opengeni/codex": "^0.2.24",
51
- "@opengeni/config": "^2.0.0",
52
- "@opengeni/contracts": "^5.0.0",
53
- "@opengeni/core": "^4.0.0",
54
- "@opengeni/db": "^6.0.0",
55
- "@opengeni/documents": "^0.8.31",
56
- "@opengeni/events": "^0.4.29",
57
- "@opengeni/github": "^0.7.14",
49
+ "@opengeni/codemode": "^0.6.1",
50
+ "@opengeni/codex": "^0.2.25",
51
+ "@opengeni/config": "^2.1.0",
52
+ "@opengeni/contracts": "^5.1.0",
53
+ "@opengeni/core": "^4.0.1",
54
+ "@opengeni/db": "^6.0.1",
55
+ "@opengeni/documents": "^0.8.32",
56
+ "@opengeni/events": "^0.4.30",
57
+ "@opengeni/github": "^0.7.15",
58
58
  "@opengeni/network": "^0.3.1",
59
- "@opengeni/observability": "^0.8.30",
60
- "@opengeni/runtime": "^4.0.0",
61
- "@opengeni/storage": "^0.2.131",
62
- "@opengeni/tool-gateway": "^0.1.11",
59
+ "@opengeni/observability": "^0.8.31",
60
+ "@opengeni/runtime": "^4.0.1",
61
+ "@opengeni/storage": "^0.2.132",
62
+ "@opengeni/tool-gateway": "^0.1.12",
63
63
  "@opengeni/xai-subscription": "^0.1.4",
64
64
  "@temporalio/client": "^1.17.0",
65
65
  "better-auth": "^1.6.14",
@@ -29,9 +29,11 @@ import {
29
29
  getGlobalCatalogOAuthProfile,
30
30
  listConnectionsMetadata,
31
31
  loadIntegrationOAuthClient,
32
+ loadIntegrationOAuthPendingState,
32
33
  normalizeBearerScheme,
33
34
  replaceIntegrationOAuthClientIfCurrent,
34
35
  storeIntegrationOAuthClient,
36
+ storeIntegrationOAuthPendingState,
35
37
  updateConnection,
36
38
  withDatabaseStatementTimeout,
37
39
  type Database,
@@ -55,7 +57,7 @@ import {
55
57
  type McpProtectedResourceMetadata,
56
58
  } from "@opengeni/network";
57
59
  import { Buffer } from "node:buffer";
58
- import { createHash, randomBytes } from "node:crypto";
60
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
59
61
  import { HTTPException } from "hono/http-exception";
60
62
  import {
61
63
  assertConnectionOwnershipAllowedForPrincipal,
@@ -196,7 +198,8 @@ export type OAuthStartStage =
196
198
  | "mcp_challenge"
197
199
  | "protected_resource_metadata"
198
200
  | "authorization_server_metadata"
199
- | "client_registration";
201
+ | "client_registration"
202
+ | "state_persist";
200
203
 
201
204
  class OAuthStartStageError extends Error {
202
205
  constructor(
@@ -524,7 +527,7 @@ async function startMcpOAuthWithinDeadline(
524
527
  ),
525
528
  );
526
529
  const key = requireEnvironmentEncryption(settings);
527
- const state = createSignedState(requireIntegrationsStateSecret(settings), {
530
+ const fullState = createSignedState(requireIntegrationsStateSecret(settings), {
528
531
  ...(context.integrationKey ? { integrationKey: context.integrationKey } : {}),
529
532
  ...(context.connectAttemptId ? { connectAttemptId: context.connectAttemptId } : {}),
530
533
  accountId: context.accountId,
@@ -574,6 +577,29 @@ async function startMcpOAuthWithinDeadline(
574
577
  returnPath,
575
578
  ...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
576
579
  });
580
+ const expiresAt = new Date(Date.now() + oauthStateTtlMs);
581
+ let state = fullState;
582
+ if (settings.integrationsOauthShortStateEnabled) {
583
+ const stateId = randomUUID();
584
+ state = createSignedState(requireIntegrationsStateSecret(settings), {
585
+ kind: "mcp_oauth_reference",
586
+ id: stateId,
587
+ accountId: context.accountId,
588
+ workspaceId: context.workspaceId,
589
+ });
590
+ if (state.length > 512) {
591
+ throw new Error("MCP OAuth reference state exceeded its provider-safe length");
592
+ }
593
+ await deadline.run("state_persist", async () =>
594
+ storeIntegrationOAuthPendingState(db, {
595
+ id: stateId,
596
+ accountId: context.accountId,
597
+ workspaceId: context.workspaceId,
598
+ stateEncrypted: encryptEnvironmentValue(key, fullState),
599
+ expiresAt,
600
+ }),
601
+ );
602
+ }
577
603
  const authorizationUrl = buildAuthorizationUrl({
578
604
  endpoint: discovery.as.authorizationEndpoint,
579
605
  settings,
@@ -589,7 +615,7 @@ async function startMcpOAuthWithinDeadline(
589
615
  return OAuthStartResponse.parse({
590
616
  state,
591
617
  authorizationUrl,
592
- expiresAt: new Date(Date.now() + oauthStateTtlMs).toISOString(),
618
+ expiresAt: expiresAt.toISOString(),
593
619
  });
594
620
  }
595
621
 
@@ -701,7 +727,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
701
727
  };
702
728
  }
703
729
  try {
704
- state = readOAuthState(input.state, settings);
730
+ state = await resolveMcpOAuthState(db, settings, input.state, deadline);
705
731
  integrationKey = state.integrationKey ?? "custom:mcp";
706
732
  if (state.connectAttemptId) {
707
733
  const stored = await getConnectAttempt(db, state, state.connectAttemptId);
@@ -1718,6 +1744,41 @@ export function buildAuthorizationUrl(input: {
1718
1744
  return url.toString();
1719
1745
  }
1720
1746
 
1747
+ async function resolveMcpOAuthState(
1748
+ db: Database,
1749
+ settings: Settings,
1750
+ state: string,
1751
+ deadline: OAuthCallbackDeadline,
1752
+ ): Promise<OAuthStatePayload> {
1753
+ const reference = readSignedState(state, requireIntegrationsStateSecret(settings)) as Record<
1754
+ string,
1755
+ unknown
1756
+ > | null;
1757
+ if (reference?.kind !== "mcp_oauth_reference") {
1758
+ // In-flight grants from the previous release remain valid for their
1759
+ // normal ten-minute lifetime during a rolling deployment.
1760
+ return readOAuthState(state, settings);
1761
+ }
1762
+ const accountId = requiredString(reference.accountId, "state.accountId");
1763
+ const workspaceId = requiredString(reference.workspaceId, "state.workspaceId");
1764
+ const id = requiredString(reference.id, "state.id");
1765
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) {
1766
+ throw new HTTPException(400, { message: "invalid OAuth state reference" });
1767
+ }
1768
+ const encrypted = await runCallbackDatabaseStage(deadline, "state_verify", db, (scopedDb) =>
1769
+ loadIntegrationOAuthPendingState(scopedDb, { accountId, workspaceId, id }),
1770
+ );
1771
+ if (!encrypted) {
1772
+ throw new HTTPException(400, { message: "invalid or expired OAuth state" });
1773
+ }
1774
+ const fullState = decryptEnvironmentValue(requireEnvironmentEncryption(settings), encrypted);
1775
+ const resolved = readOAuthState(fullState, settings);
1776
+ if (resolved.accountId !== accountId || resolved.workspaceId !== workspaceId) {
1777
+ throw new HTTPException(400, { message: "OAuth state reference mismatch" });
1778
+ }
1779
+ return resolved;
1780
+ }
1781
+
1721
1782
  function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1722
1783
  const payload = readSignedState(state, requireIntegrationsStateSecret(settings)) as Record<
1723
1784
  string,
@@ -2170,6 +2231,8 @@ function oauthStartStageLabel(stage: OAuthStartStage): string {
2170
2231
  return "authorization-server discovery";
2171
2232
  case "client_registration":
2172
2233
  return "OAuth client registration";
2234
+ case "state_persist":
2235
+ return "OAuth state persistence";
2173
2236
  }
2174
2237
  }
2175
2238
 
@@ -1,4 +1,4 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
3
3
  import {
4
4
  OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
@@ -1051,6 +1051,7 @@ export class OpenGeniSlackBotClient {
1051
1051
  const targetKind = input.userId ? "user" : "channel";
1052
1052
  const targetId = input.userId ?? input.channelId!;
1053
1053
  const blocks = validateSlackMessageBlocks(input.blocks);
1054
+ const wireBlocks = slackMessageWireBlocks(blocks);
1054
1055
  const requestDigest = this.postRequestDigest({
1055
1056
  operationId: input.operationId,
1056
1057
  targetKind,
@@ -1132,7 +1133,7 @@ export class OpenGeniSlackBotClient {
1132
1133
  const posted = await this.call(headers, "chat.postMessage", {
1133
1134
  channel: channelId,
1134
1135
  text: input.text,
1135
- ...(blocks ? { blocks: JSON.stringify(blocks) } : {}),
1136
+ ...(wireBlocks ? { blocks: JSON.stringify(wireBlocks) } : {}),
1136
1137
  client_msg_id: input.operationId,
1137
1138
  unfurl_links: "false",
1138
1139
  unfurl_media: "false",
@@ -1197,6 +1198,7 @@ export class OpenGeniSlackBotClient {
1197
1198
  let providerCallStarted = false;
1198
1199
  const blocks = validateSlackMessageBlocks(input.blocks);
1199
1200
  try {
1201
+ const wireBlocks = slackMessageWireBlocks(blocks);
1200
1202
  const headers = await this.headersFor(operation);
1201
1203
  await this.requireMemberChannel(headers, input.channelId);
1202
1204
  const requestDigest = this.updateRequestDigest({ ...input, ...(blocks ? { blocks } : {}) });
@@ -1235,7 +1237,7 @@ export class OpenGeniSlackBotClient {
1235
1237
  channel: input.channelId,
1236
1238
  ts: input.timestamp,
1237
1239
  text: input.text,
1238
- ...(blocks ? { blocks: JSON.stringify(blocks) } : {}),
1240
+ ...(wireBlocks ? { blocks: JSON.stringify(wireBlocks) } : {}),
1239
1241
  });
1240
1242
  const slackChannelId = requiredSlackString(updated.channel, "channel");
1241
1243
  const slackMessageTimestamp = requiredSlackString(updated.ts, "ts");
@@ -3000,6 +3002,33 @@ export function nextSlackFilesListPage(
3000
3002
  return nextPage;
3001
3003
  }
3002
3004
 
3005
+ /**
3006
+ * Slack action IDs are unique within a block. Keep the logical card untouched
3007
+ * for the durable request digest, and partition repeated actions only on the
3008
+ * provider wire. In particular, a legacy invalid_blocks receipt can retry with
3009
+ * its original operation ID and digest; completed/uncertain posts keep their
3010
+ * existing replay/reconciliation path. Values and action IDs remain exact.
3011
+ */
3012
+ export function slackMessageWireBlocks(
3013
+ blocks: SlackMessageBlock[] | undefined,
3014
+ ): SlackMessageBlock[] | undefined {
3015
+ if (!blocks) return undefined;
3016
+ const wire = blocks.flatMap((block, index): SlackMessageBlock[] => {
3017
+ if (
3018
+ block.type !== "actions" ||
3019
+ new Set(block.elements.map((e) => e.action_id)).size === block.elements.length
3020
+ )
3021
+ return [block];
3022
+ const id = createHash("sha256").update(`${index}:${block.block_id}`).digest("hex").slice(0, 24);
3023
+ return block.elements.map((element, position) => ({
3024
+ type: "actions",
3025
+ block_id: `opengeni_wire_${id}_${position}`,
3026
+ elements: [element],
3027
+ }));
3028
+ });
3029
+ return validateSlackMessageBlocks(wire);
3030
+ }
3031
+
3003
3032
  function validateSlackMessageBlocks(
3004
3033
  blocks: SlackMessageBlock[] | undefined,
3005
3034
  ): SlackMessageBlock[] | undefined {
@@ -3,6 +3,7 @@ import {
3
3
  approvalIdentifier,
4
4
  ApproveSlackUserLinkAccessRequest,
5
5
  DEFAULT_FIRST_PARTY_MCP_TOOLS,
6
+ resolveWorkspaceSessionToolDefaults,
6
7
  hasOpenGeniSlackReactionScope,
7
8
  ListSlackUserLinkAccessRequestsResponse,
8
9
  PrepareSlackUserLinkAccessRequest,
@@ -52,6 +53,7 @@ import {
52
53
  getOrCreateSlackInteraction,
53
54
  getLatestSessionModelForSubject,
54
55
  getSession,
56
+ getSessionByCreateIdempotencyKey,
55
57
  getSessionEvent,
56
58
  getSessionHumanInputRequest,
57
59
  childRequiresActionResolutionExists,
@@ -119,6 +121,7 @@ import {
119
121
  acceptSessionUserMessage,
120
122
  controlHumanSessionWorkstream,
121
123
  createSessionForRequest,
124
+ getActorNewSessionDefaults,
122
125
  hasPermission,
123
126
  requireAccessContext,
124
127
  requireAccessGrant,
@@ -2165,6 +2168,16 @@ async function processSlackInboxEntry(deps: ApiRouteDeps, entry: SlackInteractio
2165
2168
  );
2166
2169
  let session: Awaited<ReturnType<typeof createSessionForRequest>>;
2167
2170
  try {
2171
+ const defaults = await slackNewSessionDefaults(
2172
+ deps,
2173
+ grant,
2174
+ interaction,
2175
+ `slack:${entry.connectionId}:${entry.providerEventId}`,
2176
+ );
2177
+ const workspace = await getWorkspace(deps.db, interaction.workspaceId);
2178
+ const defaultTools =
2179
+ defaults.firstPartyMcpTools ??
2180
+ resolveWorkspaceSessionToolDefaults(workspace?.settings)?.firstPartyMcpTools;
2168
2181
  const prepared = slackInvocationPreparedMessage(
2169
2182
  preparedEntry,
2170
2183
  preparedAttachments,
@@ -2175,13 +2188,17 @@ async function processSlackInboxEntry(deps: ApiRouteDeps, entry: SlackInteractio
2175
2188
  grant,
2176
2189
  interaction.workspaceId,
2177
2190
  {
2191
+ ...defaults,
2178
2192
  requestedSessionId: interaction.sessionReservationId,
2179
2193
  initialMessage: prepared.entry.text,
2180
2194
  ...(prepared.modelContext ? { modelContext: prepared.modelContext } : {}),
2181
2195
  instructions: SLACK_SESSION_INSTRUCTIONS,
2182
- firstPartyMcpTools: slackTaskFirstPartyMcpTools(deps.settings),
2183
- resources: preparedAttachments.resources,
2184
- ...(preferredModel ? { model: preferredModel } : {}),
2196
+ firstPartyMcpTools:
2197
+ defaultTools !== undefined
2198
+ ? allowedFirstPartyMcpToolsForSession(deps.settings, defaultTools)
2199
+ : slackTaskFirstPartyMcpTools(deps.settings),
2200
+ resources: [...defaults.resources, ...preparedAttachments.resources],
2201
+ ...(!defaults.model && preferredModel ? { model: preferredModel } : {}),
2185
2202
  idempotencyKey: `slack:${entry.connectionId}:${entry.providerEventId}`,
2186
2203
  clientEventId: `slack:${entry.providerEventId}`,
2187
2204
  },
@@ -2764,19 +2781,25 @@ async function processSlackReactionInboxEntry(
2764
2781
  const preparedEntry = slackReactionPreparedEntry(entry, context, preparedTask);
2765
2782
  let session: Awaited<ReturnType<typeof createSessionForRequest>>;
2766
2783
  try {
2784
+ const defaults = await slackNewSessionDefaults(
2785
+ deps,
2786
+ grant,
2787
+ interaction,
2788
+ `slack-interaction:${interaction.id}`,
2789
+ );
2767
2790
  session = await createSessionForRequest(
2768
2791
  await withCatalogSettings(deps, grant),
2769
2792
  grant,
2770
2793
  interaction.workspaceId,
2771
2794
  {
2795
+ ...defaults,
2772
2796
  requestedSessionId: interaction.sessionReservationId,
2773
2797
  initialMessage: preparedEntry.text,
2774
2798
  instructions: SLACK_SESSION_INSTRUCTIONS,
2775
- // The exact reacted message and bounded containing thread are already in
2776
- // the prompt; do not expose general Slack history tools for this trigger.
2777
- firstPartyMcpTools: resolveFirstPartyMcpToolPolicy(deps.settings).default,
2778
- resources: preparedTask.resources,
2779
- ...(preferredModel ? { model: preferredModel } : {}),
2799
+ // Reaction context stays bounded; ordinary tools follow the same saved
2800
+ // selection/workspace defaults as the website, without adding Slack tools.
2801
+ resources: [...defaults.resources, ...preparedTask.resources],
2802
+ ...(!defaults.model && preferredModel ? { model: preferredModel } : {}),
2780
2803
  // Every reaction entry converging on this route must use the same create
2781
2804
  // key. This closes the same-owner multi-event race while the owner check
2782
2805
  // above prevents a different subject from winning creation authority.
@@ -2854,6 +2877,54 @@ function slackReactionPreparedEntry(
2854
2877
  };
2855
2878
  }
2856
2879
 
2880
+ // A create can commit its reserved shell before initial-event acceptance. Keep
2881
+ // that shell's selections on retry instead of reading a subsequently edited draft.
2882
+ async function slackNewSessionDefaults(
2883
+ deps: ApiRouteDeps,
2884
+ grant: AccessGrant,
2885
+ interaction: { workspaceId: string; sessionReservationId: string; owningSubjectId: string },
2886
+ createKey: string,
2887
+ ): Promise<Awaited<ReturnType<typeof getActorNewSessionDefaults>>> {
2888
+ const pending = await getSessionByCreateIdempotencyKey(
2889
+ deps.db,
2890
+ interaction.workspaceId,
2891
+ createKey,
2892
+ );
2893
+ if (!pending) return getActorNewSessionDefaults(deps, grant, interaction.workspaceId);
2894
+ if (
2895
+ pending.id !== interaction.sessionReservationId ||
2896
+ interaction.owningSubjectId !== grant.subjectId ||
2897
+ pending.createdBy.kind !== "subject" ||
2898
+ pending.createdBy.subjectId !== grant.subjectId
2899
+ ) {
2900
+ throw new SlackInteractionPermanentError("slack_session_create_identity_mismatch");
2901
+ }
2902
+ return {
2903
+ model: pending.model,
2904
+ reasoningEffort: pending.reasoningEffort,
2905
+ latencyMode: pending.latencyMode,
2906
+ resources: pending.resources.filter((resource) => resource.kind === "repository"),
2907
+ ...(pending.toolPolicy.mode === "explicit"
2908
+ ? { tools: pending.tools }
2909
+ : {
2910
+ excludedMcpServerIds: pending.toolPolicy.excludedMcpServerIds,
2911
+ }),
2912
+ firstPartyMcpTools: pending.firstPartyMcpTools,
2913
+ ...(pending.firstPartyMcpPermissions
2914
+ ? { firstPartyMcpPermissions: pending.firstPartyMcpPermissions }
2915
+ : {}),
2916
+ variableSetIds: pending.variableSetIds,
2917
+ ...(pending.rigId ? { rigId: pending.rigId } : {}),
2918
+ sandboxBackend: pending.sandboxBackend,
2919
+ ...(pending.sandboxBackend === "selfhosted" && pending.activeSandboxId
2920
+ ? {
2921
+ targetSandboxId: pending.activeSandboxId,
2922
+ ...(pending.workingDir ? { workingDir: pending.workingDir } : {}),
2923
+ }
2924
+ : {}),
2925
+ };
2926
+ }
2927
+
2857
2928
  async function remountSlackReactionTaskForSession(
2858
2929
  deps: ApiRouteDeps,
2859
2930
  workspaceId: string,
@@ -0,0 +1,19 @@
1
+ /** Select only whole items that actually survived the model-facing envelope.
2
+ * A cursor is a position, never proof that preceding content was consumed. */
3
+ export function completeChildReadSequences(page: {
4
+ view: string;
5
+ sourceExact: boolean;
6
+ events: readonly ({ sequence: number } & Record<string, unknown>)[];
7
+ }): number[] {
8
+ if (!page.sourceExact || !["conversation", "results"].includes(page.view)) return [];
9
+ return page.events
10
+ .filter((event) => {
11
+ if (event.sourceOmitted) return false;
12
+ if (event.fragment === undefined) return true;
13
+ if (!event.fragment || typeof event.fragment !== "object") return false;
14
+ const fragment = event.fragment as Record<string, unknown>;
15
+ // A final fragment cannot prove earlier fragments were ever read.
16
+ return fragment.offset === 0 && fragment.complete === true;
17
+ })
18
+ .map((event) => event.sequence);
19
+ }
@@ -90,6 +90,7 @@ export const FIRST_PARTY_TOOL_AUTHORIZATION = {
90
90
  run_on: { sessionRequired: true, allOf: ["sessions:control"] },
91
91
  sandbox_provision: { sessionRequired: true, allOf: ["sessions:control"] },
92
92
  connected_machine_remove: { allOf: ["enrollments:manage"] },
93
+ connected_machine_enroll_token: { allOf: ["enrollments:manage"] },
93
94
  rig_list: { allOf: ["rigs:use"] },
94
95
  rig_get: { allOf: ["rigs:use"] },
95
96
  rig_propose_change: { allOf: ["rigs:use"] },