@opengeni/db 1.0.1 → 3.2.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/attached-browser-devices.d.ts +7 -0
  2. package/dist/automations.d.ts +154 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-integrations.d.ts +13 -0
  5. package/dist/child-lifecycle-notices.d.ts +68 -0
  6. package/dist/{chunk-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
  7. package/dist/chunk-2EH4WRHL.js.map +1 -0
  8. package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
  9. package/dist/chunk-6HMM2R5T.js +390 -0
  10. package/dist/chunk-6HMM2R5T.js.map +1 -0
  11. package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
  12. package/dist/chunk-AZHW7R5V.js.map +1 -0
  13. package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
  14. package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
  15. package/dist/chunk-KRLPEMLH.js.map +1 -0
  16. package/dist/chunk-NAB6FKFN.js +516 -0
  17. package/dist/chunk-NAB6FKFN.js.map +1 -0
  18. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  19. package/dist/chunk-SIBYBA2D.js.map +1 -0
  20. package/dist/chunk-UZNXG44K.js +4508 -0
  21. package/dist/chunk-UZNXG44K.js.map +1 -0
  22. package/dist/codemode-operations.d.ts +1 -6
  23. package/dist/company-brain-context-selection-schema.d.ts +705 -0
  24. package/dist/company-brain-context-selection.d.ts +94 -0
  25. package/dist/company-brain-governed-writes.d.ts +24 -0
  26. package/dist/company-brain.d.ts +55 -0
  27. package/dist/company-profile-agent-admin.d.ts +15 -0
  28. package/dist/company-profile-schema.d.ts +619 -2
  29. package/dist/company-profile.d.ts +9 -4
  30. package/dist/connection-authority.d.ts +95 -0
  31. package/dist/connection-scopes.d.ts +8 -0
  32. package/dist/connection-tenancy-backfill.d.ts +85 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +23 -4
  35. package/dist/editable-artifact-durable-export.js +2 -2
  36. package/dist/editable-artifacts.d.ts +2 -2
  37. package/dist/editable-artifacts.js +3 -3
  38. package/dist/governed-learning-activation-schema.d.ts +984 -0
  39. package/dist/governed-learning-activation.d.ts +98 -0
  40. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  41. package/dist/governed-learning-evaluator.d.ts +28 -0
  42. package/dist/index.d.ts +1606 -81
  43. package/dist/index.js +21898 -8147
  44. package/dist/index.js.map +1 -1
  45. package/dist/insights.d.ts +18 -0
  46. package/dist/integration-bindings.d.ts +1 -0
  47. package/dist/integration-facets.d.ts +30 -0
  48. package/dist/interaction-placement-loss.d.ts +22 -0
  49. package/dist/managed-human-provisioning.d.ts +25 -0
  50. package/dist/memory-domain.d.ts +3 -3
  51. package/dist/migrate.d.ts +11 -1
  52. package/dist/migrate.js +1 -1
  53. package/dist/new-session-drafts.d.ts +43 -5
  54. package/dist/organization-membership-backfill.d.ts +174 -0
  55. package/dist/organization-membership-lifecycle.d.ts +162 -0
  56. package/dist/pr-review.d.ts +107 -0
  57. package/dist/preference-registry-schema.d.ts +2 -0
  58. package/dist/preference-registry.d.ts +2 -0
  59. package/dist/provision-roles.js +1 -1
  60. package/dist/runtime-posture.d.ts +29 -8
  61. package/dist/schema.d.ts +24914 -13703
  62. package/dist/schema.js +99 -1
  63. package/dist/scoped-knowledge-schema.d.ts +52 -1
  64. package/dist/session-background-commands.d.ts +125 -0
  65. package/dist/session-background-commands.js +34 -0
  66. package/dist/session-background-commands.js.map +1 -0
  67. package/dist/session-control.d.ts +156 -3
  68. package/dist/session-goal-pacing.d.ts +68 -0
  69. package/dist/session-queue-commands.d.ts +43 -4
  70. package/dist/session-realtime-ledger.d.ts +2 -0
  71. package/dist/session-tenancy.d.ts +71 -1
  72. package/dist/session-tenancy.js +25 -3
  73. package/dist/session-tool-call-settlement.d.ts +9 -4
  74. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  75. package/dist/slack-routing.d.ts +268 -0
  76. package/dist/slack-task-policy-schema.d.ts +34 -0
  77. package/dist/slack-task-policy.d.ts +8 -0
  78. package/dist/slack-user-link-access.d.ts +28 -0
  79. package/dist/task-notes-schema.d.ts +594 -0
  80. package/dist/task-notes.d.ts +39 -0
  81. package/dist/turn-initiator.d.ts +2 -0
  82. package/dist/user-resource-authority.d.ts +109 -0
  83. package/dist/video-generation.js +3 -3
  84. package/dist/workspace-authority.d.ts +81 -0
  85. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  86. package/dist/workspace-instruction-policies.d.ts +62 -0
  87. package/dist/workspace-learning-policy.d.ts +1 -0
  88. package/dist/workspace-membership-permissions.d.ts +2 -0
  89. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  90. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  91. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  92. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  93. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  94. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  95. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  96. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  97. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  98. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  99. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  100. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  101. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  102. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  103. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  104. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  105. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  106. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  107. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  108. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  109. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  110. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  111. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  112. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  113. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  114. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  115. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  116. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  117. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  118. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  119. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  120. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  121. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  122. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  123. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  124. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  125. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  126. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  127. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  128. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  129. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  130. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  131. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  132. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  133. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  134. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  135. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  136. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  137. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  138. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  139. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  140. package/drizzle/0294_preference_activation_authority.sql +237 -0
  141. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  142. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  143. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  144. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  145. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  146. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  147. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  148. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  149. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  150. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  151. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  152. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  153. package/drizzle/0307_session_attention_state.sql +51 -0
  154. package/drizzle/0308_session_archives.sql +35 -0
  155. package/drizzle/0309_channel_project_pins.sql +6 -0
  156. package/drizzle/0310_channel_project_order.sql +27 -0
  157. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  158. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  159. package/drizzle/0313_private_child_session_authority.sql +365 -0
  160. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  161. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  162. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  163. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  164. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  165. package/drizzle/0319_event_triggered_automations.sql +254 -0
  166. package/drizzle/0320_pr_review_pack.sql +113 -0
  167. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  168. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  169. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  170. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  171. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  172. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  173. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  174. package/drizzle/0328_session_background_commands.sql +590 -0
  175. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  176. package/drizzle/0330_api_key_descriptions.sql +20 -0
  177. package/drizzle/0331_managed_organization_creation.sql +163 -0
  178. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  179. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  180. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  181. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  182. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  183. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  184. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  185. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  186. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  187. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  188. package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
  189. package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
  190. package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
  191. package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
  192. package/drizzle/0346_document_migration_audit_surface.sql +454 -0
  193. package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -0
  194. package/package.json +10 -6
  195. package/src/attached-browser-devices.ts +233 -5
  196. package/src/automations.ts +911 -0
  197. package/src/browser-sessions.ts +10 -0
  198. package/src/capability-integrations.ts +175 -1
  199. package/src/child-lifecycle-notices.ts +241 -0
  200. package/src/codemode-operations.ts +1 -35
  201. package/src/company-brain-context-selection-schema.ts +100 -0
  202. package/src/company-brain-context-selection.ts +359 -0
  203. package/src/company-brain-governed-writes.ts +1170 -0
  204. package/src/company-brain.ts +309 -0
  205. package/src/company-profile-agent-admin.ts +225 -0
  206. package/src/company-profile-schema.ts +121 -1
  207. package/src/company-profile.ts +21 -0
  208. package/src/computer-sessions.ts +10 -1
  209. package/src/connection-authority.ts +290 -0
  210. package/src/connection-scopes.ts +20 -0
  211. package/src/connection-tenancy-backfill.ts +167 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +87 -2
  214. package/src/editable-artifacts-schema.ts +10 -5
  215. package/src/editable-artifacts.ts +21 -11
  216. package/src/governed-learning-activation-schema.ts +121 -0
  217. package/src/governed-learning-activation.ts +588 -0
  218. package/src/governed-learning-evaluator-schema.ts +72 -0
  219. package/src/governed-learning-evaluator.ts +217 -0
  220. package/src/index.ts +11676 -1550
  221. package/src/insights.ts +107 -0
  222. package/src/integration-bindings.ts +88 -6
  223. package/src/integration-facets.ts +287 -84
  224. package/src/interaction-placement-loss.ts +234 -0
  225. package/src/managed-human-provisioning.ts +116 -0
  226. package/src/memory-domain.ts +27 -32
  227. package/src/migrate.ts +77 -1
  228. package/src/new-session-drafts.ts +229 -35
  229. package/src/organization-membership-backfill.ts +735 -0
  230. package/src/organization-membership-lifecycle.ts +995 -0
  231. package/src/pr-review.ts +890 -0
  232. package/src/preference-registry.ts +31 -1
  233. package/src/provision-roles.ts +905 -6
  234. package/src/runtime-posture-cli.ts +2 -0
  235. package/src/runtime-posture.ts +1317 -72
  236. package/src/schema.ts +3316 -428
  237. package/src/scoped-knowledge-schema.ts +16 -8
  238. package/src/scoped-knowledge.ts +1 -1
  239. package/src/session-background-commands.ts +759 -0
  240. package/src/session-control.ts +640 -74
  241. package/src/session-goal-pacing.ts +126 -0
  242. package/src/session-queue-commands.ts +441 -85
  243. package/src/session-realtime-context.ts +4 -6
  244. package/src/session-realtime-ledger.ts +28 -14
  245. package/src/session-realtime-mirror.ts +1 -1
  246. package/src/session-tenancy.ts +410 -49
  247. package/src/session-tool-call-settlement.ts +38 -29
  248. package/src/slack-routing-personal-workspace.ts +67 -0
  249. package/src/slack-routing.ts +867 -0
  250. package/src/slack-task-policy-schema.ts +5 -0
  251. package/src/slack-task-policy.ts +18 -0
  252. package/src/slack-user-link-access.ts +138 -11
  253. package/src/task-notes-schema.ts +74 -0
  254. package/src/task-notes.ts +124 -2
  255. package/src/turn-initiator.ts +8 -0
  256. package/src/user-resource-authority.ts +377 -0
  257. package/src/workspace-authority.ts +160 -0
  258. package/src/workspace-instruction-policies-schema.ts +53 -1
  259. package/src/workspace-instruction-policies.ts +528 -77
  260. package/src/workspace-learning-policy.ts +21 -5
  261. package/src/workspace-membership-permissions.ts +9 -0
  262. package/dist/chunk-4SR4HKKA.js.map +0 -1
  263. package/dist/chunk-6ZYKCRFO.js.map +0 -1
  264. package/dist/chunk-NXAFLCQX.js +0 -2468
  265. package/dist/chunk-NXAFLCQX.js.map +0 -1
  266. package/dist/chunk-SK7YYHBI.js.map +0 -1
  267. package/dist/chunk-WD5ROSLL.js +0 -117
  268. package/dist/chunk-WD5ROSLL.js.map +0 -1
  269. package/dist/chunk-ZWREYEWA.js.map +0 -1
  270. /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
  271. /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
@@ -114,6 +114,11 @@ export const slackSharedTaskOrigins = pgTable(
114
114
  sourceThreadTs: text("source_thread_ts").notNull(),
115
115
  initiatingSlackUserId: text("initiating_slack_user_id").notNull(),
116
116
  policyRevisionId: uuid("policy_revision_id").notNull(),
117
+ // The Slack task policy is a home fact and the interaction is a routed one,
118
+ // so the frozen revision carries its own tenancy pair. Null on rows written
119
+ // before Slack workspace routing, where the two were always equal.
120
+ policyAccountId: uuid("policy_account_id"),
121
+ policyWorkspaceId: uuid("policy_workspace_id"),
117
122
  policyHash: text("policy_hash").notNull(),
118
123
  policyActivationVersion: bigint("policy_activation_version", { mode: "number" }).notNull(),
119
124
  publicationMode: text("publication_mode")
@@ -30,6 +30,14 @@ export type SlackSharedTaskOrigin = {
30
30
  sourceThreadTs: string;
31
31
  initiatingSlackUserId: string;
32
32
  policyRevisionId: string;
33
+ /**
34
+ * Home tenancy of the frozen policy revision. The row's own
35
+ * `accountId`/`workspaceId` are the routed task's, which is a different
36
+ * workspace once Slack workspace routing is on. Null on rows written before
37
+ * routing existed, where the two were always equal.
38
+ */
39
+ policyAccountId: string | null;
40
+ policyWorkspaceId: string | null;
33
41
  policyHash: string;
34
42
  policyActivationVersion: number;
35
43
  publicationMode: "never" | "approval_required" | "allow";
@@ -310,6 +318,16 @@ export async function saveSlackSharedTaskOrigin(
310
318
  .limit(1);
311
319
  if (!existing) throw new Error("Slack shared-task origin conflict could not be resolved");
312
320
  for (const [key, value] of Object.entries(input)) {
321
+ // A row written before Slack workspace routing carries no policy tenancy
322
+ // and implied its own, because the two could not differ then. A replay
323
+ // from a new image supplies it, which is not a change of evidence and
324
+ // must not be reported as one.
325
+ if (
326
+ (key === "policyAccountId" || key === "policyWorkspaceId") &&
327
+ existing[key as keyof OriginRow] === null
328
+ ) {
329
+ continue;
330
+ }
313
331
  if (existing[key as keyof OriginRow] !== value) {
314
332
  throw new SlackTaskPolicyInvalidOperationError(
315
333
  "Slack shared-task origin identity conflicted with immutable evidence",
@@ -6,11 +6,13 @@ import {
6
6
  type SlackUserLinkAccessRequest,
7
7
  type SlackUserLinkAccessRequestStatus,
8
8
  } from "@opengeni/contracts";
9
- import { and, asc, eq, gt, inArray, ne } from "drizzle-orm";
9
+ import { and, asc, eq, gt, inArray, ne, sql } from "drizzle-orm";
10
10
 
11
11
  import { type Database, withWorkspaceRls } from "./database";
12
+ import { namedSubjectPersonalWorkspaceId } from "./slack-routing-personal-workspace";
12
13
  import { withLosslessContentWriteVersion } from "./lossless-json";
13
14
  import * as schema from "./schema";
15
+ import { normalizeWorkspaceMembershipPermissions } from "./workspace-membership-permissions";
14
16
 
15
17
  type RequestRow = typeof schema.slackUserLinkAccessRequests.$inferSelect;
16
18
  type OperationKind = "request" | "cancel" | "approve" | "deny";
@@ -51,9 +53,45 @@ export type SlackUserLinkAccessMutationInput = {
51
53
  idempotencyKey: string;
52
54
  };
53
55
 
56
+ /**
57
+ * The Slack team one access request was raised from.
58
+ *
59
+ * The public request shape deliberately omits it, but an approver has to
60
+ * resolve the installation binding to know where the identity link belongs, and
61
+ * for a routed request that is not the workspace being granted.
62
+ */
63
+ export async function slackUserLinkAccessRequestTeam(
64
+ db: Database,
65
+ input: { workspaceId: string; requestId: string },
66
+ ): Promise<{ slackTeamId: string; connectionId: string } | null> {
67
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
68
+ const [row] = await scopedDb
69
+ .select({
70
+ slackTeamId: schema.slackUserLinkAccessRequests.slackTeamId,
71
+ connectionId: schema.slackUserLinkAccessRequests.connectionId,
72
+ })
73
+ .from(schema.slackUserLinkAccessRequests)
74
+ .where(
75
+ and(
76
+ eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
77
+ eq(schema.slackUserLinkAccessRequests.id, input.requestId),
78
+ ),
79
+ )
80
+ .limit(1);
81
+ return row ?? null;
82
+ });
83
+ }
84
+
85
+ /**
86
+ * The tenancy an identity link belongs to. See `withSlackIdentityHomeScope`.
87
+ */
88
+ export type SlackIdentityHome = { accountId: string; workspaceId: string };
89
+
54
90
  export type ApproveSlackUserLinkAccessInput = SlackUserLinkAccessMutationInput & {
55
91
  role?: string;
56
92
  permissions: Permission[];
93
+ /** The installation's tenancy, when it differs from the routed request's. */
94
+ identityHome?: SlackIdentityHome;
57
95
  };
58
96
 
59
97
  const activeStatuses: SlackUserLinkAccessRequestStatus[] = ["prepared", "pending"];
@@ -154,7 +192,13 @@ export async function prepareSlackUserLinkAccessRequest(
154
192
 
155
193
  export async function getSlackUserLinkAccessRequestForSubject(
156
194
  db: Database,
157
- input: { workspaceId: string; requestId: string; subjectId: string },
195
+ input: {
196
+ workspaceId: string;
197
+ requestId: string;
198
+ subjectId: string;
199
+ /** The installation's tenancy, when it differs from the routed request's. */
200
+ identityHome?: SlackIdentityHome;
201
+ },
158
202
  now = new Date(),
159
203
  ): Promise<SlackUserLinkAccessRequest | null> {
160
204
  return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
@@ -318,7 +362,10 @@ export async function approveSlackUserLinkAccessRequest(
318
362
  });
319
363
  const membership = await membershipAllowsSlackLink(database, row);
320
364
  if (!membership) throw new Error("Slack access approval did not create the required grant");
321
- await upsertSlackIdentityLink(database, row, input.actorSubjectId, now);
365
+ await withSlackIdentityHomeScope(database, row, input.identityHome, async () => {
366
+ await assertSlackIdentityAvailable(database, row);
367
+ await upsertSlackIdentityLink(database, row, input.actorSubjectId, now, input.identityHome);
368
+ });
322
369
  const [updated] = await database
323
370
  .update(schema.slackUserLinkAccessRequests)
324
371
  .set({
@@ -362,7 +409,13 @@ export async function approveSlackUserLinkAccessRequest(
362
409
  */
363
410
  export async function completeSlackUserLinkAccessIfGranted(
364
411
  db: Database,
365
- input: { workspaceId: string; requestId: string; subjectId: string },
412
+ input: {
413
+ workspaceId: string;
414
+ requestId: string;
415
+ subjectId: string;
416
+ /** The installation's tenancy, when it differs from the routed request's. */
417
+ identityHome?: SlackIdentityHome;
418
+ },
366
419
  now = new Date(),
367
420
  ): Promise<SlackUserLinkAccessRequest | null> {
368
421
  return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
@@ -388,8 +441,10 @@ export async function completeSlackUserLinkAccessIfGranted(
388
441
  if (!(await membershipAllowsSlackLink(database, row))) {
389
442
  return mapRequest(row);
390
443
  }
391
- await assertSlackIdentityAvailable(database, row);
392
- await upsertSlackIdentityLink(database, row, input.subjectId, now);
444
+ await withSlackIdentityHomeScope(database, row, input.identityHome, async () => {
445
+ await assertSlackIdentityAvailable(database, row);
446
+ await upsertSlackIdentityLink(database, row, input.subjectId, now, input.identityHome);
447
+ });
393
448
  const [updated] = await tx
394
449
  .update(schema.slackUserLinkAccessRequests)
395
450
  .set({
@@ -563,7 +618,21 @@ async function membershipAllowsSlackLink(database: Database, row: RequestRow) {
563
618
  ),
564
619
  )
565
620
  .limit(1);
566
- const permissions = membership?.permissions ?? [];
621
+ if (workspaceMembershipPermissionsAllowSlackLink(membership?.permissions)) return true;
622
+ // A managed human's own personal workspace deliberately has no
623
+ // `workspace_memberships` row, so a bare membership join denies its owner.
624
+ // Once a Slack direct message can land there, that would tell people to
625
+ // request access to their own workspace. The pointer is DERIVED from an
626
+ // active organization membership; it is never accepted from the request row.
627
+ const personalWorkspaceId = await namedSubjectPersonalWorkspaceId(database, {
628
+ accountId: row.accountId,
629
+ subjectId: row.subjectId,
630
+ });
631
+ return personalWorkspaceId === row.workspaceId;
632
+ }
633
+
634
+ export function workspaceMembershipPermissionsAllowSlackLink(value: unknown): boolean {
635
+ const permissions = normalizeWorkspaceMembershipPermissions(value);
567
636
  return permissions.includes("sessions:create") || permissions.includes("workspace:admin");
568
637
  }
569
638
 
@@ -584,17 +653,75 @@ async function assertSlackIdentityAvailable(database: Database, row: RequestRow)
584
653
  }
585
654
  }
586
655
 
656
+ /**
657
+ * The tenancy an identity link belongs to.
658
+ *
659
+ * `slack_bot_user_links` is unique on `(connection_id, slack_user_id)` globally
660
+ * but RLS-visible only under ONE workspace, and the Slack pump reads it under
661
+ * the installation's. A request for a ROUTED workspace runs under that routed
662
+ * scope, so writing the link there would file it where the pump cannot see it,
663
+ * and reading it there is blind to the row that already exists at home. Both the
664
+ * availability check and the write therefore take the installation's tenancy
665
+ * explicitly, and fall back to the request row's when a caller has none, which
666
+ * is exactly the pre-routing behaviour.
667
+ */
668
+ function identityHomeFor(row: RequestRow, home: SlackIdentityHome | undefined): SlackIdentityHome {
669
+ return home ?? { accountId: row.accountId, workspaceId: row.workspaceId };
670
+ }
671
+
672
+ /**
673
+ * Run the identity-link statements under the installation's own tenancy.
674
+ *
675
+ * The surrounding transaction is scoped to the request's workspace, which for a
676
+ * routed request is NOT where the link lives, and it must stay one transaction:
677
+ * an approval that granted membership without linking the identity, or the
678
+ * reverse, would be worse than either. So the tenant GUCs are swapped for
679
+ * exactly these statements and restored, rather than opening a second
680
+ * transaction. Restoring is not optional: everything after this call still
681
+ * belongs to the request's workspace.
682
+ */
683
+ async function withSlackIdentityHomeScope<T>(
684
+ database: Database,
685
+ row: RequestRow,
686
+ home: SlackIdentityHome | undefined,
687
+ fn: () => Promise<T>,
688
+ ): Promise<T> {
689
+ const identity = identityHomeFor(row, home);
690
+ if (identity.accountId === row.accountId && identity.workspaceId === row.workspaceId) {
691
+ return await fn();
692
+ }
693
+ await database.execute(
694
+ sql`select set_config('opengeni.account_id', ${identity.accountId}, true),
695
+ set_config('opengeni.workspace_id', ${identity.workspaceId}, true)`,
696
+ );
697
+ try {
698
+ return await fn();
699
+ } finally {
700
+ // A failed statement can leave the transaction aborted and unwinding to
701
+ // rollback, so preserve the original diagnostic rather than replacing it
702
+ // with the restore's own error.
703
+ await database
704
+ .execute(
705
+ sql`select set_config('opengeni.account_id', ${row.accountId}, true),
706
+ set_config('opengeni.workspace_id', ${row.workspaceId}, true)`,
707
+ )
708
+ .catch(() => undefined);
709
+ }
710
+ }
711
+
587
712
  async function upsertSlackIdentityLink(
588
713
  database: Database,
589
714
  row: RequestRow,
590
715
  actorSubjectId: string,
591
716
  now: Date,
717
+ home?: SlackIdentityHome,
592
718
  ) {
719
+ const identity = identityHomeFor(row, home);
593
720
  const [created] = await database
594
721
  .insert(schema.slackBotUserLinks)
595
722
  .values({
596
- accountId: row.accountId,
597
- workspaceId: row.workspaceId,
723
+ accountId: identity.accountId,
724
+ workspaceId: identity.workspaceId,
598
725
  connectionId: row.connectionId,
599
726
  slackTeamId: row.slackTeamId,
600
727
  slackUserId: row.slackUserId,
@@ -624,8 +751,8 @@ async function upsertSlackIdentityLink(
624
751
  await database
625
752
  .update(schema.slackBotUserLinks)
626
753
  .set({
627
- accountId: row.accountId,
628
- workspaceId: row.workspaceId,
754
+ accountId: identity.accountId,
755
+ workspaceId: identity.workspaceId,
629
756
  slackTeamId: row.slackTeamId,
630
757
  linkedBySubjectId: actorSubjectId,
631
758
  updatedAt: now,
@@ -133,3 +133,77 @@ export const taskNoteWriteCapabilities = pgTable(
133
133
  ),
134
134
  }),
135
135
  );
136
+
137
+ // Transaction-local, owner-only admission for the single Task-note evidence
138
+ // insert materialized by a governed promotion. Migration 0260 owns its RLS,
139
+ // one-shot trigger consumption, and runtime-role revocation.
140
+ export const taskNoteKnowledgePromotionCapabilities = pgTable(
141
+ "task_note_knowledge_promotion_capabilities",
142
+ {
143
+ backendPid: integer("backend_pid").notNull(),
144
+ transactionId: xid8("transaction_id").notNull(),
145
+ capabilityId: uuid("capability_id").notNull(),
146
+ accountId: uuid("account_id").notNull(),
147
+ workspaceId: uuid("workspace_id").notNull(),
148
+ noteId: uuid("note_id").notNull(),
149
+ rootSessionId: uuid("root_session_id").notNull(),
150
+ noteVersion: integer("note_version").notNull(),
151
+ noteTextHash: text("note_text_hash").notNull(),
152
+ evidenceOperationId: text("evidence_operation_id").notNull(),
153
+ claimOperationId: text("claim_operation_id").notNull(),
154
+ actorSubjectId: text("actor_subject_id").notNull(),
155
+ initiatingHumanSubjectId: text("initiating_human_subject_id").notNull(),
156
+ learningPolicySnapshotId: uuid("learning_policy_snapshot_id").notNull(),
157
+ },
158
+ (table) => ({
159
+ identity: uniqueIndex("task_note_knowledge_promotion_capabilities_identity_uq").on(
160
+ table.backendPid,
161
+ table.transactionId,
162
+ table.capabilityId,
163
+ ),
164
+ }),
165
+ );
166
+
167
+ // Immutable, content-free lineage for one atomic archive + replacement create.
168
+ // Migration 0260 owns exact constraints, FORCE RLS, and lifecycle-only writes.
169
+ export const taskNoteReplacementReceipts = pgTable(
170
+ "task_note_replacement_receipts",
171
+ {
172
+ id: uuid("id").primaryKey().defaultRandom(),
173
+ accountId: uuid("account_id").notNull(),
174
+ workspaceId: uuid("workspace_id").notNull(),
175
+ rootSessionId: uuid("root_session_id").notNull(),
176
+ operationId: uuid("operation_id").notNull(),
177
+ inputHash: text("input_hash").notNull(),
178
+ replacedNoteId: uuid("replaced_note_id").notNull(),
179
+ replacedNoteVersion: integer("replaced_note_version").notNull(),
180
+ replacementNoteId: uuid("replacement_note_id").notNull(),
181
+ replacementNoteVersion: integer("replacement_note_version").notNull(),
182
+ archiveOperationId: uuid("archive_operation_id").notNull(),
183
+ createOperationId: uuid("create_operation_id").notNull(),
184
+ actorKind: text("actor_kind").notNull(),
185
+ actorSubjectId: text("actor_subject_id").notNull(),
186
+ initiatingHumanSubjectId: text("initiating_human_subject_id"),
187
+ actorSessionId: uuid("actor_session_id").notNull(),
188
+ actorTurnId: uuid("actor_turn_id").notNull(),
189
+ actorAttemptId: uuid("actor_attempt_id").notNull(),
190
+ actorExecutionGeneration: integer("actor_execution_generation").notNull(),
191
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
192
+ },
193
+ (table) => ({
194
+ workspaceOperation: uniqueIndex("task_note_replacement_receipts_workspace_operation_uq").on(
195
+ table.workspaceId,
196
+ table.operationId,
197
+ ),
198
+ workspaceReplacement: uniqueIndex("task_note_replacement_receipts_workspace_replacement_uq").on(
199
+ table.workspaceId,
200
+ table.replacementNoteId,
201
+ ),
202
+ rootTimeline: index("task_note_replacement_receipts_root_timeline_idx").on(
203
+ table.workspaceId,
204
+ table.rootSessionId,
205
+ table.createdAt,
206
+ table.id,
207
+ ),
208
+ }),
209
+ );
package/src/task-notes.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import {
2
2
  TASK_NOTE_LIST_DEFAULT_LIMIT,
3
3
  TASK_NOTE_LIST_MAX_LIMIT,
4
+ TASK_NOTE_MAX_LIFETIME_DAYS,
4
5
  TaskNote,
5
6
  TaskNoteKind,
6
7
  TaskNoteListResponse,
7
8
  TaskNoteMutationResult,
8
9
  TaskNoteReason,
10
+ TaskNoteReplacementResult,
9
11
  TaskNoteText,
10
12
  type TaskNoteKind as TaskNoteKindType,
11
13
  } from "@opengeni/contracts";
14
+ import { createHash } from "node:crypto";
12
15
  import { sql } from "drizzle-orm";
13
16
  import type { Database } from "./database";
14
17
  import { rawRows, withRlsContext } from "./database";
@@ -36,6 +39,16 @@ export type ArchiveTaskNoteInput = TaskNoteAttemptClaims & {
36
39
  reason: string;
37
40
  };
38
41
 
42
+ export type ReplaceTaskNoteInput = TaskNoteAttemptClaims & {
43
+ operationId: string;
44
+ replacedNoteId: string;
45
+ expectedReplacedVersion: number;
46
+ replacementKind: TaskNoteKindType;
47
+ replacementText: string;
48
+ replacementExpiresInDays: number;
49
+ reason: string;
50
+ };
51
+
39
52
  export type ListTaskNotesInput = TaskNoteAttemptClaims & {
40
53
  includeArchived?: boolean;
41
54
  limit?: number;
@@ -58,6 +71,27 @@ type TaskNoteRow = {
58
71
  replayed?: boolean;
59
72
  };
60
73
 
74
+ type TaskNoteReplacementRow = {
75
+ operation_id: string;
76
+ input_hash: string;
77
+ replaced_note_id: string;
78
+ replaced_note_version: number;
79
+ replacement_note_id: string;
80
+ root_session_id: string;
81
+ replacement_kind: string;
82
+ replacement_text: string;
83
+ replacement_status: string;
84
+ replacement_version: number;
85
+ replacement_expires_at: Date | string;
86
+ replacement_created_at: Date | string;
87
+ replacement_updated_at: Date | string;
88
+ replacement_archived_at: Date | string | null;
89
+ replacement_actor_kind: string;
90
+ replacement_source_session_id: string;
91
+ replacement_source_turn_id: string;
92
+ replayed: boolean;
93
+ };
94
+
61
95
  function iso(value: Date | string): string {
62
96
  return (value instanceof Date ? value : new Date(value)).toISOString();
63
97
  }
@@ -88,6 +122,20 @@ function assertAttemptClaims(claims: TaskNoteAttemptClaims): void {
88
122
  }
89
123
  }
90
124
 
125
+ function derivedTaskNoteOperationId(operationId: string, label: "archive" | "create"): string {
126
+ const bytes = Buffer.from(
127
+ createHash("sha256")
128
+ .update(`task-note-replacement:v1:${operationId}:${label}`, "utf8")
129
+ .digest("hex")
130
+ .slice(0, 32),
131
+ "hex",
132
+ );
133
+ bytes[6] = (bytes[6]! & 0x0f) | 0x50;
134
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
135
+ const hex = bytes.toString("hex");
136
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
137
+ }
138
+
91
139
  export async function createTaskNote(db: Database, input: CreateTaskNoteInput) {
92
140
  assertAttemptClaims(input);
93
141
  TaskNoteKind.parse(input.kind);
@@ -95,9 +143,9 @@ export async function createTaskNote(db: Database, input: CreateTaskNoteInput) {
95
143
  if (
96
144
  !Number.isSafeInteger(input.expiresInDays) ||
97
145
  input.expiresInDays < 1 ||
98
- input.expiresInDays > 30
146
+ input.expiresInDays > TASK_NOTE_MAX_LIFETIME_DAYS
99
147
  ) {
100
- throw new Error("Task note expiry must be 1-30 whole days");
148
+ throw new Error(`Task note expiry must be 1-${TASK_NOTE_MAX_LIFETIME_DAYS} whole days`);
101
149
  }
102
150
  const rows = await withRlsContext(
103
151
  db,
@@ -150,6 +198,80 @@ export async function archiveTaskNote(db: Database, input: ArchiveTaskNoteInput)
150
198
  return TaskNoteMutationResult.parse({ note: noteFromRow(rows[0]!), replayed: rows[0]!.replayed });
151
199
  }
152
200
 
201
+ /**
202
+ * Atomically archive one exact active note and create its immutable replacement.
203
+ * Exact retries return the original linked receipt; changed input or authority fails.
204
+ */
205
+ export async function replaceTaskNote(db: Database, input: ReplaceTaskNoteInput) {
206
+ assertAttemptClaims(input);
207
+ TaskNoteKind.parse(input.replacementKind);
208
+ TaskNoteText.parse(input.replacementText);
209
+ TaskNoteReason.parse(input.reason);
210
+ if (
211
+ input.expectedReplacedVersion !== 1 ||
212
+ !Number.isSafeInteger(input.replacementExpiresInDays) ||
213
+ input.replacementExpiresInDays < 1 ||
214
+ input.replacementExpiresInDays > TASK_NOTE_MAX_LIFETIME_DAYS
215
+ ) {
216
+ throw new Error(
217
+ `Task-note replacement requires version 1 and a 1-${TASK_NOTE_MAX_LIFETIME_DAYS} day expiry`,
218
+ );
219
+ }
220
+ const archiveOperationId = derivedTaskNoteOperationId(input.operationId, "archive");
221
+ const createOperationId = derivedTaskNoteOperationId(input.operationId, "create");
222
+ const rows = await withRlsContext(
223
+ db,
224
+ { accountId: input.accountId, workspaceId: input.workspaceId },
225
+ async (tx) =>
226
+ await rawRows<TaskNoteReplacementRow>(
227
+ tx,
228
+ sql`SELECT * FROM replace_task_note_for_attempt(
229
+ ${input.accountId}::uuid,
230
+ ${input.workspaceId}::uuid,
231
+ ${input.sessionId}::uuid,
232
+ ${input.turnId}::uuid,
233
+ ${input.attemptId}::uuid,
234
+ ${input.executionGeneration}::integer,
235
+ ${input.operationId}::uuid,
236
+ ${archiveOperationId}::uuid,
237
+ ${createOperationId}::uuid,
238
+ ${input.replacedNoteId}::uuid,
239
+ ${input.expectedReplacedVersion}::integer,
240
+ ${input.replacementKind}::text,
241
+ ${input.replacementText}::text,
242
+ ${input.replacementExpiresInDays}::integer,
243
+ ${input.reason}::text
244
+ )`,
245
+ ),
246
+ );
247
+ const row = rows[0];
248
+ if (!row || rows.length !== 1) throw new Error("Task-note replacement returned no receipt");
249
+ return TaskNoteReplacementResult.parse({
250
+ operationId: row.operation_id,
251
+ inputHash: row.input_hash,
252
+ replaces: {
253
+ noteId: row.replaced_note_id,
254
+ archivedVersion: row.replaced_note_version,
255
+ },
256
+ replacement: noteFromRow({
257
+ note_id: row.replacement_note_id,
258
+ root_session_id: row.root_session_id,
259
+ kind: row.replacement_kind,
260
+ note_text: row.replacement_text,
261
+ status: row.replacement_status,
262
+ version: row.replacement_version,
263
+ expires_at: row.replacement_expires_at,
264
+ created_at: row.replacement_created_at,
265
+ updated_at: row.replacement_updated_at,
266
+ archived_at: row.replacement_archived_at,
267
+ actor_kind: row.replacement_actor_kind,
268
+ source_session_id: row.replacement_source_session_id,
269
+ source_turn_id: row.replacement_source_turn_id,
270
+ }),
271
+ replayed: row.replayed,
272
+ });
273
+ }
274
+
153
275
  export async function listTaskNotes(db: Database, input: ListTaskNotesInput) {
154
276
  assertAttemptClaims(input);
155
277
  const limit = input.limit ?? TASK_NOTE_LIST_DEFAULT_LIMIT;
@@ -16,6 +16,8 @@ export const UNATTRIBUTED_LEGACY_INITIATOR: TurnInitiator = {
16
16
  export type FrozenTurnInitiator = {
17
17
  initiator: TurnInitiator;
18
18
  context: TurnInitiatorContext;
19
+ /** Exact causal human frozen only after the caller/attempt authority is validated. */
20
+ initiatingHumanSubjectId?: string | null;
19
21
  };
20
22
 
21
23
  const MAX_AGENT_PROVENANCE_HOPS = 32;
@@ -100,6 +102,7 @@ export async function frozenInitiatorForCommandActor(
100
102
  ...(actor.subjectLabel ? { label: actor.subjectLabel } : {}),
101
103
  },
102
104
  context: { ...(actor.context ?? {}) },
105
+ initiatingHumanSubjectId: null,
103
106
  };
104
107
  }
105
108
  if (actor.type !== "agent_attempt") {
@@ -110,6 +113,7 @@ export async function frozenInitiatorForCommandActor(
110
113
  ...(subjectLabel ? { label: subjectLabel } : {}),
111
114
  },
112
115
  context: {},
116
+ initiatingHumanSubjectId: actor.subjectId,
113
117
  };
114
118
  }
115
119
 
@@ -118,6 +122,7 @@ export async function frozenInitiatorForCommandActor(
118
122
  initiatorKind: schema.sessionTurns.initiatorKind,
119
123
  initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
120
124
  initiatorContext: schema.sessionTurns.initiatorContext,
125
+ initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
121
126
  })
122
127
  .from(schema.sessionTurns)
123
128
  .where(
@@ -151,5 +156,8 @@ export async function frozenInitiatorForCommandActor(
151
156
  via: clipped,
152
157
  ...(hops.length > clipped.length ? { viaTruncated: true } : {}),
153
158
  },
159
+ initiatingHumanSubjectId:
160
+ turn.initiatingHumanSubjectId ??
161
+ (turn.initiatorKind === "subject" ? turn.initiatorSubjectId : null),
154
162
  };
155
163
  }