@opengeni/core 3.0.1 → 3.1.2

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 (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -0,0 +1,130 @@
1
+ import type { AccessGrant, WorkspaceArtifactVisibility } from "@opengeni/contracts";
2
+ import {
3
+ getVisibleSessionTenancyVisibility,
4
+ scheduledTaskViewerSubject,
5
+ type Database,
6
+ type WorkspaceArtifactViewer,
7
+ } from "@opengeni/db";
8
+ import { HTTPException } from "hono/http-exception";
9
+ import type { PrivacySettings } from "../application/organization-privacy";
10
+ import { mayStartOnlyMe } from "./scheduled-task-audience";
11
+ import { creationInitiatorForGrant } from "./sessions";
12
+
13
+ /**
14
+ * A Site has an audience, like a chat and a schedule: the whole workspace, or
15
+ * only its owner ("Only me"). Everyone with workspace access sees workspace
16
+ * Sites. An Only me Site is visible to its owner alone; admins cannot see it,
17
+ * exactly like an Only me chat. Only the owner changes the audience.
18
+ *
19
+ * Every surface that reads or changes Sites (HTTP, agent tools, the artifact
20
+ * library, Site tools and Site conversations) resolves its viewer here and
21
+ * passes it to the database layer, which filters by it.
22
+ */
23
+
24
+ export const SITE_PRIVATE_OWNER_MESSAGE = "Only a person can have an Only me Site.";
25
+ export const SITE_PRIVATE_DISABLED_MESSAGE = "Only me is turned off for this organization.";
26
+ export const SITE_AUDIENCE_HUMAN_MESSAGE =
27
+ "Only the Site's owner can change who can see it, from Opengeni.";
28
+
29
+ /**
30
+ * The person a request reads Sites as: the human caller, or the human an exact
31
+ * agent attempt acts for. Keys, services and runs without a person see
32
+ * workspace Sites only.
33
+ */
34
+ export async function siteViewerForGrant(
35
+ db: Database,
36
+ grant: AccessGrant,
37
+ ): Promise<WorkspaceArtifactViewer> {
38
+ const creator = creationInitiatorForGrant(grant);
39
+ const subjectId = await scheduledTaskViewerSubject(db, {
40
+ workspaceId: grant.workspaceId,
41
+ ...(creator.initiator ? { createdBy: creator.initiator } : {}),
42
+ ...(creator.context ? { createdByContext: creator.context } : {}),
43
+ createdByActor: creator.actor ?? null,
44
+ });
45
+ return { subjectId };
46
+ }
47
+
48
+ function stored(visibility: WorkspaceArtifactVisibility): "user_private" | "workspace_shared" {
49
+ return visibility === "private" ? "user_private" : "workspace_shared";
50
+ }
51
+
52
+ async function requireOnlyMeAllowed(input: {
53
+ db: Database;
54
+ settings: PrivacySettings;
55
+ workspaceId: string;
56
+ ownerSubjectId: string | null;
57
+ }): Promise<string> {
58
+ if (!input.ownerSubjectId) throw new HTTPException(422, { message: SITE_PRIVATE_OWNER_MESSAGE });
59
+ if (!(await mayStartOnlyMe(input.db, input.settings, input.workspaceId, input.ownerSubjectId)))
60
+ throw new HTTPException(403, { message: SITE_PRIVATE_DISABLED_MESSAGE });
61
+ return input.ownerSubjectId;
62
+ }
63
+
64
+ /**
65
+ * The audience and owner to store for a new Site. An explicit choice wins;
66
+ * Only me needs a person who may start Only me work here. Without one, a Site
67
+ * published from a chat starts with that chat's audience, so a Site made in an
68
+ * Only me chat stays Only me until its owner shares it. Anything else starts
69
+ * shared with the workspace, as every Site did before audiences existed.
70
+ */
71
+ export async function resolveNewSiteAudience(input: {
72
+ db: Database;
73
+ settings: PrivacySettings;
74
+ grant: AccessGrant;
75
+ viewer: WorkspaceArtifactViewer;
76
+ requested: WorkspaceArtifactVisibility | undefined;
77
+ sourceSessionId: string | null;
78
+ }): Promise<{
79
+ visibility: "user_private" | "workspace_shared";
80
+ ownerSubjectId: string | null;
81
+ }> {
82
+ const ownerSubjectId = input.viewer.subjectId;
83
+ if (input.requested === "private") {
84
+ await requireOnlyMeAllowed({
85
+ db: input.db,
86
+ settings: input.settings,
87
+ workspaceId: input.grant.workspaceId,
88
+ ownerSubjectId,
89
+ });
90
+ return { visibility: "user_private", ownerSubjectId };
91
+ }
92
+ if (input.requested === "workspace" || input.sourceSessionId === null || !ownerSubjectId)
93
+ return { visibility: "workspace_shared", ownerSubjectId };
94
+ // The chat already is Only me, so this narrows nothing and needs no fresh
95
+ // permission, like an existing Only me item when Only me is later turned off.
96
+ const chat = await getVisibleSessionTenancyVisibility(
97
+ input.db,
98
+ input.grant.workspaceId,
99
+ input.sourceSessionId,
100
+ );
101
+ return {
102
+ visibility: chat === "user_private" ? "user_private" : "workspace_shared",
103
+ ownerSubjectId,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * The stored audience for an owner's change. Sharing with the workspace is
109
+ * always allowed; limiting to Only me needs Only me to be allowed for them.
110
+ * Whether the caller owns the Site is checked atomically by the database.
111
+ */
112
+ export async function resolveSiteAudienceChange(input: {
113
+ db: Database;
114
+ settings: PrivacySettings;
115
+ grant: AccessGrant;
116
+ viewer: WorkspaceArtifactViewer;
117
+ requested: WorkspaceArtifactVisibility;
118
+ }): Promise<"user_private" | "workspace_shared"> {
119
+ // Like a chat's audience, this is a person's own decision, not an agent's.
120
+ if (input.grant.principalKind === "agent_attempt")
121
+ throw new HTTPException(403, { message: SITE_AUDIENCE_HUMAN_MESSAGE });
122
+ if (input.requested === "private")
123
+ await requireOnlyMeAllowed({
124
+ db: input.db,
125
+ settings: input.settings,
126
+ workspaceId: input.grant.workspaceId,
127
+ ownerSubjectId: input.viewer.subjectId,
128
+ });
129
+ return stored(input.requested);
130
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  SkillInstallInput,
4
4
  SkillRevisionInput,
5
5
  SkillSaveInput,
6
+ SkillRemoveInput,
6
7
  } from "@opengeni/contracts";
7
8
  import { validateSkillTextFiles } from "@opengeni/contracts";
8
9
  import { buildPortableSkillArtifact } from "@opengeni/runtime/skill-library";
@@ -30,6 +31,14 @@ export function skillBundleHash(files: readonly SkillFile[]): string {
30
31
  }
31
32
 
32
33
  export const listSkills = listSkillRecords;
34
+ export async function removeSkill(db: Database, input: SkillRemoveInput) {
35
+ const { accountId, workspaceId, actor, ...request } = input;
36
+ return applySkillLifecycle(
37
+ db,
38
+ { accountId, workspaceId, actor },
39
+ { ...request, operation: "remove" },
40
+ );
41
+ }
33
42
  export async function readSkill(
34
43
  db: Database,
35
44
  context: SkillReadContext,
@@ -3,19 +3,26 @@ import {
3
3
  OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
4
4
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
5
5
  OpenGeniSlackBotConnectionMetadata,
6
+ SCHEDULED_SLACK_BOT_POSTING_TOOLS,
6
7
  areOpenGeniSlackBotScopesAccepted,
7
8
  type AccessGrant,
8
9
  type ConnectionMetadata,
10
+ type FirstPartyMcpToolName,
9
11
  type OpenGeniSlackBotConnectionMetadata as OpenGeniSlackBotMetadata,
12
+ type ScheduledTask,
13
+ type ScheduledTaskAgentConfig,
10
14
  type Session,
11
15
  } from "@opengeni/contracts";
12
16
  import {
13
17
  getConnectionMetadata,
18
+ availableSlackBotConnectionMetadata,
19
+ requireWorkspace,
14
20
  type ConnectionMetadataWithVerification,
15
21
  type Database,
16
22
  } from "@opengeni/db";
17
23
  import { HTTPException } from "hono/http-exception";
18
- import { requirePermission } from "../access";
24
+ import { requirePermission, type AccessGrantAuthorization } from "../access";
25
+ import { grantHasAgentAttemptAuthority } from "../session-authorization";
19
26
 
20
27
  export function openGeniSlackBotMetadata(
21
28
  metadata: Record<string, unknown>,
@@ -69,15 +76,22 @@ export async function requireOpenGeniSlackBotConnection(
69
76
  workspaceId: string,
70
77
  connectionId: string,
71
78
  ): Promise<ConnectionMetadata> {
72
- const connection = await getConnectionMetadata(db, workspaceId, connectionId, null);
79
+ const local = await getConnectionMetadata(db, workspaceId, connectionId, null);
80
+ const workspace = await requireWorkspace(db, workspaceId);
81
+ const connection =
82
+ local ??
83
+ (
84
+ await availableSlackBotConnectionMetadata(db, { accountId: workspace.accountId, workspaceId })
85
+ ).find((candidate) => candidate.id === connectionId) ??
86
+ null;
73
87
  if (!connection || !isOpenGeniSlackBotConnection(connection)) {
74
88
  throw new HTTPException(422, {
75
- message: "slackBotConnectionId must reference an OpenGeni Slack bot connection",
89
+ message: "slackBotConnectionId must reference an Opengeni Slack bot connection",
76
90
  });
77
91
  }
78
92
  if (connection.status !== "active") {
79
93
  throw new HTTPException(422, {
80
- message: `OpenGeni Slack bot connection is not active (${connection.status})`,
94
+ message: `Opengeni Slack bot connection is not active (${connection.status})`,
81
95
  });
82
96
  }
83
97
  return connection;
@@ -126,3 +140,126 @@ export function isTrustedScheduledSlackBotSession(
126
140
  function zUuid(value: string): boolean {
127
141
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
128
142
  }
143
+
144
+ /**
145
+ * Proves, with the bot's own token, that it may post in the chosen channel:
146
+ * the bot is a member and the channel is active and not shared with another
147
+ * organization. Supplied only by the HTTP routes a person uses to edit tasks.
148
+ */
149
+ export type ScheduledTaskSlackChannelVerifier = (input: {
150
+ connectionId: string;
151
+ channelId: string;
152
+ }) => Promise<void>;
153
+
154
+ /**
155
+ * Whether this request is a signed-in person acting for themselves: never an
156
+ * agent attempt, a service, an API key, or a request carrying service
157
+ * provenance. The same rule gates Agent learning settings.
158
+ */
159
+ export function isAuthenticatedPersonAuthorization(
160
+ authorization: AccessGrantAuthorization | undefined,
161
+ ): boolean {
162
+ if (!authorization) return false;
163
+ const { grant } = authorization;
164
+ return (
165
+ grant.principalKind === "human_session" &&
166
+ authorization.contextIntegrity &&
167
+ authorization.authenticatedSubjectId === grant.subjectId &&
168
+ !grant.serviceInitiator &&
169
+ !grant.serviceInitiatorContext &&
170
+ grant.metadata?.attemptId === undefined &&
171
+ grant.metadata?.turnId === undefined &&
172
+ grant.metadata?.executionGeneration === undefined &&
173
+ !grantHasAgentAttemptAuthority(grant)
174
+ );
175
+ }
176
+
177
+ /**
178
+ * A scheduled task may post as the Opengeni workspace bot only to the one
179
+ * Slack channel a person chose on the task. Choosing or changing that channel
180
+ * therefore needs a signed-in person with `connections:write`, and the bot's
181
+ * membership is verified at that moment. Keeping or clearing the existing
182
+ * channel needs neither, so an agent editing other task fields cannot redirect
183
+ * posts but also does not have to re-prove the person's choice. Clearing is the
184
+ * safe direction and is never refused, even for a task that continues one chat.
185
+ */
186
+ export async function validateScheduledTaskSlackChannel(input: {
187
+ grant: AccessGrant;
188
+ authorization?: AccessGrantAuthorization | undefined;
189
+ previous: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId"> | null;
190
+ next: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">;
191
+ runMode: ScheduledTask["runMode"];
192
+ /**
193
+ * Null unless the task already created the reusable chat its runs continue.
194
+ * Otherwise reports whether that chat was created with the posting tools,
195
+ * which are fixed at creation.
196
+ */
197
+ reusableSessionCanPost: (() => Promise<boolean>) | null;
198
+ verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
199
+ }): Promise<void> {
200
+ const previousChannel = input.previous?.slackBotChannelId ?? null;
201
+ const nextChannel = input.next.slackBotChannelId ?? null;
202
+ // Clearing takes effect at once: the tools re-read the task and refuse.
203
+ if (!nextChannel) return;
204
+ if (!input.next.slackBotConnectionId) {
205
+ throw new HTTPException(422, {
206
+ message: "slackBotChannelId requires slackBotConnectionId",
207
+ });
208
+ }
209
+ if (input.runMode === "existing_session") {
210
+ throw new HTTPException(422, {
211
+ message: "Slack channel posting needs a task that starts its own chats",
212
+ });
213
+ }
214
+ if (
215
+ previousChannel === nextChannel &&
216
+ (input.previous?.slackBotConnectionId ?? null) === input.next.slackBotConnectionId
217
+ ) {
218
+ return;
219
+ }
220
+ if (!isAuthenticatedPersonAuthorization(input.authorization)) {
221
+ throw new HTTPException(403, {
222
+ message: "Only a person can choose the Slack channel a scheduled task posts to",
223
+ });
224
+ }
225
+ requirePermission(input.grant, "connections:write");
226
+ // A chat created without the posting tools keeps that tool set, so choosing
227
+ // a channel for it would silently post nothing.
228
+ if (input.reusableSessionCanPost && !(await input.reusableSessionCanPost())) {
229
+ throw new HTTPException(409, {
230
+ message:
231
+ "this task continues a chat that was created without Slack posting; recreate the task to post from it",
232
+ });
233
+ }
234
+ if (!input.verifySlackChannel) {
235
+ throw new HTTPException(422, {
236
+ message: "Choose the Slack channel in the schedule editor",
237
+ });
238
+ }
239
+ await input.verifySlackChannel({
240
+ connectionId: input.next.slackBotConnectionId,
241
+ channelId: nextChannel,
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Add the bot posting tools to a generated scheduled run when, and only when,
247
+ * a person chose the task's Slack channel. The deployment ceiling still
248
+ * applies: an operator who disallowed these tools keeps them off.
249
+ */
250
+ export function withScheduledSlackBotPostingTools(
251
+ tools: readonly FirstPartyMcpToolName[],
252
+ agentConfig: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">,
253
+ allowedTools: readonly FirstPartyMcpToolName[],
254
+ ): FirstPartyMcpToolName[] {
255
+ const posting = new Set<FirstPartyMcpToolName>(SCHEDULED_SLACK_BOT_POSTING_TOOLS);
256
+ // Ordinary-chat defaults include these tools, but an unattended occurrence
257
+ // must have its own person-chosen destination before receiving them.
258
+ const next = tools.filter((tool) => !posting.has(tool));
259
+ if (!agentConfig.slackBotConnectionId || !agentConfig.slackBotChannelId) return next;
260
+ const allowed = new Set(allowedTools);
261
+ for (const tool of SCHEDULED_SLACK_BOT_POSTING_TOOLS) {
262
+ if (allowed.has(tool) && !next.includes(tool)) next.push(tool);
263
+ }
264
+ return next;
265
+ }
@@ -19,7 +19,7 @@ const CREDENTIAL_URL_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+@/gi
19
19
  * Deterministic, sink-local protection for text that is about to be persisted
20
20
  * as a Slack publication projection or rendered to Slack. It intentionally
21
21
  * does not scan or mutate canonical Memory, durable-learning receipts, model
22
- * context, history, events, or any other internal OpenGeni content path.
22
+ * context, history, events, or any other internal Opengeni content path.
23
23
  */
24
24
  export function sanitizeSlackPublicationText(value: string): string {
25
25
  let sanitized = value;
@@ -0,0 +1,19 @@
1
+ /** Vaults and secret references: the only import point for the vault domain. */
2
+ export { buildMissingSecretAskCard } from "@opengeni/contracts";
3
+ export { isVaultError, VaultError, vaultErrorFromDatabase } from "@opengeni/db";
4
+ export {
5
+ toVaultItem,
6
+ toVaultItemSummary,
7
+ toVaultSummary,
8
+ vaultCanManage,
9
+ vaultCanWrite,
10
+ vaultItemReferences,
11
+ type VaultCallerAuthority,
12
+ } from "./mapping";
13
+ export {
14
+ chooseSecretCandidate,
15
+ createSecretResolver,
16
+ type SecretResolverOptions,
17
+ } from "./secret-resolver";
18
+ export type { VaultRequestActor, VaultServiceDeps } from "./types";
19
+ export { createVaultService, vaultActorIsApiKey, type VaultService } from "./vault-service";
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Vault rows (encrypted, as the 0739 routines return them) to the public
3
+ * contract shapes. Only text-kind field values are decrypted for display;
4
+ * every other kind stays server-side until an audited reveal or a resolve.
5
+ */
6
+ import {
7
+ formatSecretReference,
8
+ secretNameKey,
9
+ type VaultItem,
10
+ type VaultItemField,
11
+ type VaultItemReferences,
12
+ type VaultItemSummary,
13
+ type VaultSummary,
14
+ } from "@opengeni/contracts";
15
+ import {
16
+ decryptEnvironmentValue,
17
+ type VaultFieldRow,
18
+ type VaultItemRow,
19
+ type VaultRow,
20
+ } from "@opengeni/db";
21
+
22
+ /** What the caller may do beyond reading; decided by the route's grant. */
23
+ export type VaultCallerAuthority = {
24
+ /** `secrets:write` */
25
+ write: boolean;
26
+ /** `account:admin` on the organization */
27
+ accountAdmin: boolean;
28
+ };
29
+
30
+ export function vaultCanWrite(row: VaultRow, authority: VaultCallerAuthority): boolean {
31
+ // Read-write connected vaults are edited through their provider (W5, OP-3).
32
+ if (row.mode !== "read_write" || !authority.write) return false;
33
+ if (row.ownerKind === "user") return row.ownedByCaller;
34
+ return true;
35
+ }
36
+
37
+ export function vaultCanManage(row: VaultRow, authority: VaultCallerAuthority): boolean {
38
+ switch (row.ownerKind) {
39
+ case "task":
40
+ return false;
41
+ case "organization":
42
+ return authority.accountAdmin;
43
+ case "user":
44
+ return row.ownedByCaller && authority.write;
45
+ case "workspace":
46
+ return authority.write;
47
+ }
48
+ }
49
+
50
+ export function toVaultSummary(row: VaultRow, authority: VaultCallerAuthority): VaultSummary {
51
+ return {
52
+ id: row.id,
53
+ workspaceId: row.workspaceId,
54
+ ownerKind: row.ownerKind,
55
+ ownerLabel: row.ownerLabel ?? "",
56
+ rootSessionId: row.rootSessionId,
57
+ name: row.name,
58
+ description: row.description,
59
+ source: row.source,
60
+ mode: row.mode,
61
+ syncStatus: row.syncStatus,
62
+ syncErrorCode: row.syncErrorCode,
63
+ syncErrorMessage: row.syncErrorMessage,
64
+ lastSyncedAt: row.lastSyncedAt,
65
+ rateLimitedUntil: row.rateLimitedUntil,
66
+ providerConnectionId: row.providerConnectionId,
67
+ availability: row.availability,
68
+ isDefault: row.isDefault,
69
+ itemCount: Number(row.itemCount),
70
+ canWrite: vaultCanWrite(row, authority),
71
+ canManage: vaultCanManage(row, authority),
72
+ generation: Number(row.generation),
73
+ createdAt: row.createdAt,
74
+ updatedAt: row.updatedAt,
75
+ };
76
+ }
77
+
78
+ function decryptOrNull(key: Uint8Array | null, stored: string | null): string | null {
79
+ if (stored === null || key === null) return null;
80
+ try {
81
+ return decryptEnvironmentValue(key, stored);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /** A reference string, falling back to the ID form when a name cannot be referenced. */
88
+ function referenceFor(
89
+ item: Pick<VaultItemRow, "id" | "name" | "vaultName">,
90
+ field: string | undefined,
91
+ otp = false,
92
+ ): string {
93
+ const tail = { ...(field !== undefined ? { field } : {}), otp };
94
+ try {
95
+ return formatSecretReference({ vault: item.vaultName, item: item.name, ...tail });
96
+ } catch {
97
+ return formatSecretReference({ itemId: item.id, ...tail });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Ready references for an item. Every reference names the vault and the field
103
+ * (`{{secret:Vault/Item/field}}`), so it cannot become ambiguous when another
104
+ * reachable vault gains an item with the same name.
105
+ */
106
+ export function vaultItemReferences(
107
+ item: Pick<VaultItemRow, "id" | "name" | "vaultName" | "fields" | "primaryFieldId">,
108
+ ): VaultItemReferences {
109
+ const byField: Record<string, string> = {};
110
+ for (const field of item.fields) byField[field.name] = referenceFor(item, field.name);
111
+ const primary = item.fields.find((field) => field.id === item.primaryFieldId);
112
+ const totp = item.fields.find((field) => field.kind === "totp");
113
+ return {
114
+ primary: primary ? byField[primary.name]! : null,
115
+ otp: totp ? referenceFor(item, totp.name, true) : null,
116
+ byField,
117
+ };
118
+ }
119
+
120
+ function usernameOf(row: VaultItemRow, key: Uint8Array | null): string | null {
121
+ const field = row.fields.find(
122
+ (candidate) => candidate.kind === "text" && secretNameKey(candidate.name) === "username",
123
+ );
124
+ return field ? decryptOrNull(key, field.valueEncrypted) : null;
125
+ }
126
+
127
+ export function toVaultItemSummary(row: VaultItemRow, key: Uint8Array | null): VaultItemSummary {
128
+ return {
129
+ id: row.id,
130
+ vaultId: row.vaultId,
131
+ type: row.type,
132
+ name: row.name,
133
+ websites: row.websites ?? [],
134
+ username: usernameOf(row, key),
135
+ hasTotp: row.fields.some((field) => field.kind === "totp"),
136
+ hasPasskey: row.type === "passkey",
137
+ expiresAt: row.expiresAt,
138
+ archivedAt: row.archivedAt,
139
+ lastUsedAt: row.lastUsedAt,
140
+ updatedAt: row.updatedAt,
141
+ references: vaultItemReferences(row),
142
+ };
143
+ }
144
+
145
+ function toVaultItemField(
146
+ item: VaultItemRow,
147
+ field: VaultFieldRow,
148
+ key: Uint8Array | null,
149
+ ): VaultItemField {
150
+ return {
151
+ id: field.id,
152
+ name: field.name,
153
+ kind: field.kind,
154
+ position: field.position,
155
+ currentVersion: field.currentVersion,
156
+ value: field.kind === "text" ? decryptOrNull(key, field.valueEncrypted) : null,
157
+ fileName: field.fileName,
158
+ fileSize: field.fileSize,
159
+ reference: referenceFor(item, field.name),
160
+ updatedAt: field.updatedAt,
161
+ };
162
+ }
163
+
164
+ export function toVaultItem(row: VaultItemRow, key: Uint8Array | null): VaultItem {
165
+ const textField = (name: string) => {
166
+ const field = row.fields.find((candidate) => secretNameKey(candidate.name) === name);
167
+ return field ? decryptOrNull(key, field.valueEncrypted) : null;
168
+ };
169
+ const credentialId = row.type === "passkey" ? (textField("credentialid") ?? "") : "";
170
+ return {
171
+ ...toVaultItemSummary(row, key),
172
+ fields: row.fields.map((field) => toVaultItemField(row, field, key)),
173
+ passkey:
174
+ row.type === "passkey"
175
+ ? {
176
+ rpId: textField("rpid") ?? "",
177
+ userName: textField("username"),
178
+ credentialIdSuffix: credentialId.slice(-8),
179
+ signCount: Number(row.passkeySignCount ?? 0),
180
+ leasedUntil: row.passkeyLeasedUntil,
181
+ lastUsedAt: row.lastUsedAt,
182
+ }
183
+ : null,
184
+ aliases: row.aliases ?? [],
185
+ primaryFieldId: row.primaryFieldId,
186
+ createdBySubjectId: row.createdBySubjectId,
187
+ createdAt: row.createdAt,
188
+ generation: Number(row.generation),
189
+ };
190
+ }