@opengeni/contracts 0.40.0 → 0.50.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 (148) hide show
  1. package/dist/atlassian.d.ts +278 -0
  2. package/dist/atlassian.js +57 -0
  3. package/dist/atlassian.js.map +1 -0
  4. package/dist/canonical-human-identities.d.ts +157 -0
  5. package/dist/canonical-human-identities.js +23 -0
  6. package/dist/canonical-human-identities.js.map +1 -0
  7. package/dist/{chunk-7JZMI22I.js → chunk-5UDNP7AO.js} +34 -2
  8. package/dist/chunk-5UDNP7AO.js.map +1 -0
  9. package/dist/chunk-BVGIYW7D.js +11 -0
  10. package/dist/chunk-BVGIYW7D.js.map +1 -0
  11. package/dist/chunk-CADSX6UN.js +1145 -0
  12. package/dist/chunk-CADSX6UN.js.map +1 -0
  13. package/dist/chunk-CM6BMECR.js +24 -0
  14. package/dist/chunk-CM6BMECR.js.map +1 -0
  15. package/dist/chunk-D2HKF2EF.js +1460 -0
  16. package/dist/chunk-D2HKF2EF.js.map +1 -0
  17. package/dist/chunk-JBCIY7QD.js +194 -0
  18. package/dist/chunk-JBCIY7QD.js.map +1 -0
  19. package/dist/chunk-JUFK3T4Q.js +80 -0
  20. package/dist/chunk-JUFK3T4Q.js.map +1 -0
  21. package/dist/chunk-LSB7GRLG.js +779 -0
  22. package/dist/chunk-LSB7GRLG.js.map +1 -0
  23. package/dist/chunk-LW7OH6CS.js +382 -0
  24. package/dist/chunk-LW7OH6CS.js.map +1 -0
  25. package/dist/chunk-M5ZGRVIQ.js +719 -0
  26. package/dist/chunk-M5ZGRVIQ.js.map +1 -0
  27. package/dist/chunk-MYBZRDTY.js +340 -0
  28. package/dist/chunk-MYBZRDTY.js.map +1 -0
  29. package/dist/chunk-Q6A7XT2N.js +16734 -0
  30. package/dist/chunk-Q6A7XT2N.js.map +1 -0
  31. package/dist/chunk-RIY7GYQS.js +1204 -0
  32. package/dist/chunk-RIY7GYQS.js.map +1 -0
  33. package/dist/chunk-TZ6YEYN4.js +386 -0
  34. package/dist/chunk-TZ6YEYN4.js.map +1 -0
  35. package/dist/chunk-Y3MLSLB4.js +392 -0
  36. package/dist/chunk-Y3MLSLB4.js.map +1 -0
  37. package/dist/chunk-YRKRZ6OF.js +134 -0
  38. package/dist/chunk-YRKRZ6OF.js.map +1 -0
  39. package/dist/codex-provider-account-authority.d.ts +23 -0
  40. package/dist/codex-provider-account-authority.js +23 -0
  41. package/dist/codex-provider-account-authority.js.map +1 -0
  42. package/dist/company-profile.d.ts +629 -0
  43. package/dist/document-artifact-commands.d.ts +282 -0
  44. package/dist/document-artifact-commands.js +54 -0
  45. package/dist/document-artifact-commands.js.map +1 -0
  46. package/dist/document-artifact-query.d.ts +2 -0
  47. package/dist/document-artifact-query.js +54 -0
  48. package/dist/document-artifact-query.js.map +1 -0
  49. package/dist/editable-artifact-binary.d.ts +56 -0
  50. package/dist/editable-artifact-causal-frontier.d.ts +12 -0
  51. package/dist/editable-artifact-causal-frontier.js +18 -0
  52. package/dist/editable-artifact-causal-frontier.js.map +1 -0
  53. package/dist/editable-artifact-codec-registry.d.ts +48 -0
  54. package/dist/editable-artifact-codec-registry.js +20 -0
  55. package/dist/editable-artifact-codec-registry.js.map +1 -0
  56. package/dist/editable-artifact-committed-transaction.d.ts +35 -0
  57. package/dist/editable-artifact-committed-transaction.js +19 -0
  58. package/dist/editable-artifact-committed-transaction.js.map +1 -0
  59. package/dist/editable-artifact-live.d.ts +276 -0
  60. package/dist/editable-artifact-live.js +907 -0
  61. package/dist/editable-artifact-live.js.map +1 -0
  62. package/dist/editable-artifact-serialized-commit.d.ts +53 -0
  63. package/dist/editable-artifact-serialized-commit.js +26 -0
  64. package/dist/editable-artifact-serialized-commit.js.map +1 -0
  65. package/dist/editable-artifact-versions.d.ts +6 -0
  66. package/dist/editable-artifact-versions.js +11 -0
  67. package/dist/editable-artifact-versions.js.map +1 -0
  68. package/dist/editable-artifacts.d.ts +46 -0
  69. package/dist/editable-artifacts.js +236 -0
  70. package/dist/editable-artifacts.js.map +1 -0
  71. package/dist/event-preview.d.ts +1 -1
  72. package/dist/google-drive.d.ts +114 -2
  73. package/dist/google-drive.js +72 -14
  74. package/dist/google-drive.js.map +1 -1
  75. package/dist/image-generation.d.ts +70 -0
  76. package/dist/index.d.ts +5661 -166
  77. package/dist/index.js +1438 -21
  78. package/dist/interaction.d.ts +8290 -0
  79. package/dist/knowledge.d.ts +543 -0
  80. package/dist/memory-slack-delivery.d.ts +397 -0
  81. package/dist/presentation-artifact-commands.d.ts +349 -0
  82. package/dist/presentation-artifact-commands.js +66 -0
  83. package/dist/presentation-artifact-commands.js.map +1 -0
  84. package/dist/presentation-artifact-query.d.ts +2 -0
  85. package/dist/presentation-artifact-query.js +66 -0
  86. package/dist/presentation-artifact-query.js.map +1 -0
  87. package/dist/retained-output.d.ts +24 -0
  88. package/dist/sandbox-snapshots.d.ts +9 -0
  89. package/dist/scoped-knowledge.d.ts +14 -13
  90. package/dist/slack-bot-scopes.d.ts +4 -0
  91. package/dist/slack-bot-scopes.js +3 -1
  92. package/dist/slack-task-policy.d.ts +294 -0
  93. package/dist/spreadsheet-artifact-commands.d.ts +101 -0
  94. package/dist/spreadsheet-artifact-commands.js +27 -0
  95. package/dist/spreadsheet-artifact-commands.js.map +1 -0
  96. package/dist/spreadsheet-artifact-date.d.ts +4 -0
  97. package/dist/spreadsheet-artifact-query.d.ts +120 -0
  98. package/dist/spreadsheet-artifact-query.js +48 -0
  99. package/dist/spreadsheet-artifact-query.js.map +1 -0
  100. package/dist/task-notes.d.ts +128 -0
  101. package/dist/tool-catalog.d.ts +576 -0
  102. package/dist/video-generation.d.ts +580 -0
  103. package/dist/video-generation.js +59 -0
  104. package/dist/video-generation.js.map +1 -0
  105. package/dist/workspace-learning-policy.d.ts +208 -0
  106. package/dist/xai-provider-account-authority.d.ts +23 -0
  107. package/dist/xai-provider-account-authority.js +9 -0
  108. package/dist/xai-provider-account-authority.js.map +1 -0
  109. package/package.json +73 -1
  110. package/src/artifacts.ts +1 -1
  111. package/src/atlassian.ts +160 -0
  112. package/src/canonical-human-identities.ts +93 -0
  113. package/src/codex-provider-account-authority.ts +37 -0
  114. package/src/company-profile.ts +306 -0
  115. package/src/document-artifact-commands.ts +1709 -0
  116. package/src/document-artifact-query.ts +2 -0
  117. package/src/editable-artifact-binary.ts +396 -0
  118. package/src/editable-artifact-causal-frontier.ts +154 -0
  119. package/src/editable-artifact-codec-registry.ts +261 -0
  120. package/src/editable-artifact-committed-transaction.ts +478 -0
  121. package/src/editable-artifact-live.ts +1379 -0
  122. package/src/editable-artifact-serialized-commit.ts +494 -0
  123. package/src/editable-artifact-versions.ts +8 -0
  124. package/src/editable-artifacts.ts +473 -0
  125. package/src/google-drive.ts +68 -15
  126. package/src/image-generation.ts +106 -0
  127. package/src/index.ts +2920 -326
  128. package/src/interaction.ts +3173 -0
  129. package/src/knowledge.ts +207 -0
  130. package/src/memory-slack-delivery.ts +183 -0
  131. package/src/presentation-artifact-commands.ts +2035 -0
  132. package/src/presentation-artifact-query.ts +2 -0
  133. package/src/retained-output.ts +149 -2
  134. package/src/sandbox-snapshots.ts +28 -1
  135. package/src/scoped-knowledge.ts +18 -4
  136. package/src/slack-bot-scopes.ts +34 -1
  137. package/src/slack-task-policy.ts +215 -0
  138. package/src/spreadsheet-artifact-commands.ts +999 -0
  139. package/src/spreadsheet-artifact-date.ts +21 -0
  140. package/src/spreadsheet-artifact-query.ts +1411 -0
  141. package/src/task-notes.ts +92 -0
  142. package/src/tool-catalog.ts +334 -0
  143. package/src/video-generation.ts +431 -0
  144. package/src/workspace-learning-policy.ts +233 -0
  145. package/src/xai-provider-account-authority.ts +37 -0
  146. package/dist/chunk-5HR2GPSC.js +0 -11031
  147. package/dist/chunk-5HR2GPSC.js.map +0 -1
  148. package/dist/chunk-7JZMI22I.js.map +0 -1
@@ -0,0 +1,2 @@
1
+ /** Presentation query/projection ABI lives beside commands so shared scene codecs cannot drift. */
2
+ export * from "./presentation-artifact-commands";
@@ -15,6 +15,10 @@ export const COMPUTER_SCREENSHOT_MAX_PIXELS = 67_108_864;
15
15
  export const COMPUTER_SCREENSHOT_RETENTION_MS = 30 * 24 * 60 * 60 * 1_000;
16
16
  /** Hard workspace reservation across pending + ready screenshot artifacts. */
17
17
  export const COMPUTER_SCREENSHOT_WORKSPACE_QUOTA_BYTES = 5 * 1024 * 1024 * 1024;
18
+ /** Generated images are permanent workspace files, never inline history blobs. */
19
+ export const GENERATED_IMAGE_MAX_BYTES = 64 * 1024 * 1024;
20
+ /** Generated videos are permanent workspace files and are always streamed. */
21
+ export const GENERATED_VIDEO_MAX_BYTES = 512 * 1024 * 1024;
18
22
 
19
23
  const encoder = new TextEncoder();
20
24
  const LOWERCASE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -31,6 +35,8 @@ export const RetainedOutputKind = z.enum([
31
35
  "internal_update",
32
36
  "event_media",
33
37
  "computer_screenshot",
38
+ "generated_image",
39
+ "generated_video",
34
40
  "file",
35
41
  ]);
36
42
  export type RetainedOutputKind = z.infer<typeof RetainedOutputKind>;
@@ -116,14 +122,72 @@ export const RetainedArtifactReferenceSchema = z
116
122
  });
117
123
  }
118
124
 
119
- if (value.kind === "computer_screenshot") {
125
+ if (
126
+ value.kind === "computer_screenshot" ||
127
+ value.kind === "generated_image" ||
128
+ value.kind === "generated_video"
129
+ ) {
120
130
  if (!value.dimensions) {
121
131
  ctx.addIssue({
122
132
  code: "custom",
123
133
  path: ["dimensions"],
124
- message: "computer screenshot receipts require exact dimensions",
134
+ message: `${value.kind} receipts require exact dimensions`,
135
+ });
136
+ } else if (
137
+ value.dimensions.width * value.dimensions.height >
138
+ COMPUTER_SCREENSHOT_MAX_PIXELS
139
+ ) {
140
+ ctx.addIssue({
141
+ code: "custom",
142
+ path: ["dimensions"],
143
+ message: `${value.kind} dimensions exceed the pixel limit`,
144
+ });
145
+ }
146
+ }
147
+
148
+ if (value.kind === "generated_image") {
149
+ if (value.retention.policy !== "workspace_file" || !workspaceMatch || sessionMatch) {
150
+ ctx.addIssue({
151
+ code: "custom",
152
+ path: ["retention"],
153
+ message: "generated images require permanent workspace-file retrieval",
154
+ });
155
+ }
156
+ if (
157
+ !["image/png", "image/jpeg", "image/webp"].includes(value.contentType) ||
158
+ value.originalBytes <= 0 ||
159
+ value.originalBytes > GENERATED_IMAGE_MAX_BYTES
160
+ ) {
161
+ ctx.addIssue({
162
+ code: "custom",
163
+ path: ["contentType"],
164
+ message: "generated image media metadata is invalid",
125
165
  });
126
166
  }
167
+ }
168
+
169
+ if (value.kind === "generated_video") {
170
+ if (value.retention.policy !== "workspace_file" || !workspaceMatch || sessionMatch) {
171
+ ctx.addIssue({
172
+ code: "custom",
173
+ path: ["retention"],
174
+ message: "generated videos require permanent workspace-file retrieval",
175
+ });
176
+ }
177
+ if (
178
+ value.contentType !== "video/mp4" ||
179
+ value.originalBytes <= 0 ||
180
+ value.originalBytes > GENERATED_VIDEO_MAX_BYTES
181
+ ) {
182
+ ctx.addIssue({
183
+ code: "custom",
184
+ path: ["contentType"],
185
+ message: "generated video media metadata is invalid",
186
+ });
187
+ }
188
+ }
189
+
190
+ if (value.kind === "computer_screenshot") {
127
191
  if (value.retention.policy !== "session_screenshot" || !sessionMatch) {
128
192
  ctx.addIssue({
129
193
  code: "custom",
@@ -190,6 +254,16 @@ export type RetainedScreenshotArtifactInput = RetainedArtifactFileInput & {
190
254
  expiresAt: string;
191
255
  };
192
256
 
257
+ export type RetainedGeneratedImageArtifactInput = RetainedArtifactFileInput & {
258
+ width: number;
259
+ height: number;
260
+ };
261
+
262
+ export type RetainedGeneratedVideoArtifactInput = RetainedArtifactFileInput & {
263
+ width: number;
264
+ height: number;
265
+ };
266
+
193
267
  /**
194
268
  * Convert a ready, integrity-addressed workspace file into the only available
195
269
  * retained-output receipt shape. Invalid, pending, or checksum-less files fail
@@ -252,6 +326,79 @@ export function retainedScreenshotReferenceFromFile(
252
326
  return parsed.success ? parsed.data : null;
253
327
  }
254
328
 
329
+ /** Build the permanent workspace receipt for one validated generated image. */
330
+ export function retainedGeneratedImageReferenceFromFile(
331
+ file: RetainedGeneratedImageArtifactInput,
332
+ ): RetainedArtifactReference | null {
333
+ if (
334
+ file.status !== "ready" ||
335
+ !file.sha256 ||
336
+ file.sizeBytes <= 0 ||
337
+ file.sizeBytes > GENERATED_IMAGE_MAX_BYTES
338
+ ) {
339
+ return null;
340
+ }
341
+ const value = {
342
+ available: true as const,
343
+ artifactId: file.id,
344
+ kind: "generated_image" as const,
345
+ contentType: canonicalRetainedContentType(file.contentType),
346
+ originalBytes: file.sizeBytes,
347
+ sha256: file.sha256,
348
+ retainedAt: file.updatedAt,
349
+ dimensions: { width: file.width, height: file.height },
350
+ retention: {
351
+ policy: "workspace_file" as const,
352
+ expiresAt: null,
353
+ },
354
+ retrieval: {
355
+ method: "GET" as const,
356
+ path: `/v1/workspaces/${file.workspaceId}/artifacts/${file.id}/content`,
357
+ acceptRanges: "bytes" as const,
358
+ maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
359
+ },
360
+ };
361
+ const parsed = RetainedArtifactReferenceSchema.safeParse(value);
362
+ return parsed.success ? parsed.data : null;
363
+ }
364
+
365
+ /** Build the permanent workspace receipt for one validated generated video. */
366
+ export function retainedGeneratedVideoReferenceFromFile(
367
+ file: RetainedGeneratedVideoArtifactInput,
368
+ ): RetainedArtifactReference | null {
369
+ if (
370
+ file.status !== "ready" ||
371
+ file.contentType !== "video/mp4" ||
372
+ !file.sha256 ||
373
+ file.sizeBytes <= 0 ||
374
+ file.sizeBytes > GENERATED_VIDEO_MAX_BYTES
375
+ ) {
376
+ return null;
377
+ }
378
+ const value = {
379
+ available: true as const,
380
+ artifactId: file.id,
381
+ kind: "generated_video" as const,
382
+ contentType: "video/mp4" as const,
383
+ originalBytes: file.sizeBytes,
384
+ sha256: file.sha256,
385
+ retainedAt: file.updatedAt,
386
+ dimensions: { width: file.width, height: file.height },
387
+ retention: {
388
+ policy: "workspace_file" as const,
389
+ expiresAt: null,
390
+ },
391
+ retrieval: {
392
+ method: "GET" as const,
393
+ path: `/v1/workspaces/${file.workspaceId}/artifacts/${file.id}/content`,
394
+ acceptRanges: "bytes" as const,
395
+ maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
396
+ },
397
+ };
398
+ const parsed = RetainedArtifactReferenceSchema.safeParse(value);
399
+ return parsed.success ? parsed.data : null;
400
+ }
401
+
255
402
  function canonicalRetainedContentType(value: string): string {
256
403
  const mediaType = value.split(";", 1)[0]?.trim().toLowerCase() ?? "";
257
404
  return CANONICAL_MEDIA_TYPE.test(mediaType) ? mediaType : "application/octet-stream";
@@ -6,6 +6,14 @@ export type WorkspaceTreeFingerprint = {
6
6
  entryCount: number;
7
7
  fileCount: number;
8
8
  totalFileBytes: number;
9
+ /**
10
+ * Absent means the original deterministic GNU-tar projection used by remote
11
+ * sandboxes. Host-backed Local/Docker sessions use the Agents SDK's portable
12
+ * archive projection, which intentionally contains directories and regular
13
+ * files only. Keeping the discriminator in the descriptor makes restores
14
+ * stable when a Docker image changes and preserves old descriptor behavior.
15
+ */
16
+ projection?: "sdk_local_archive_v1";
9
17
  };
10
18
 
11
19
  export type TarWorkspaceArchiveDescriptor = {
@@ -114,7 +122,8 @@ export function parseWorkspaceArchiveDescriptor(value: unknown): WorkspaceArchiv
114
122
  !SHA256.test(workspace.sha256) ||
115
123
  !nonnegativeInteger(workspace.entryCount) ||
116
124
  !nonnegativeInteger(workspace.fileCount) ||
117
- !nonnegativeInteger(workspace.totalFileBytes)
125
+ !nonnegativeInteger(workspace.totalFileBytes) ||
126
+ (workspace.projection !== undefined && workspace.projection !== "sdk_local_archive_v1")
118
127
  ) {
119
128
  return null;
120
129
  }
@@ -181,6 +190,24 @@ export function decodeNativeSnapshotRef(bytes: Uint8Array): NativeSnapshotRef |
181
190
  return null;
182
191
  }
183
192
 
193
+ export function encodeNativeSnapshotRef(ref: NativeSnapshotRef): Uint8Array {
194
+ const prefix = NATIVE_SNAPSHOT_PREFIXES.find((entry) => entry.provider === ref.provider)?.prefix;
195
+ if (!prefix || typeof ref.snapshotId !== "string" || ref.snapshotId.length === 0) {
196
+ throw new TypeError("Native snapshot reference requires a supported provider and snapshot id");
197
+ }
198
+ if (ref.workspacePersistence !== undefined && typeof ref.workspacePersistence !== "string") {
199
+ throw new TypeError("Native snapshot workspace persistence must be a string");
200
+ }
201
+ return new TextEncoder().encode(
202
+ `${prefix}${JSON.stringify({
203
+ snapshot_id: ref.snapshotId,
204
+ ...(ref.workspacePersistence === undefined
205
+ ? {}
206
+ : { workspace_persistence: ref.workspacePersistence }),
207
+ })}`,
208
+ );
209
+ }
210
+
184
211
  export function backendForNativeSnapshotProvider(provider: NativeSnapshotProvider): string {
185
212
  return provider === "modal_snapshot_directory" || provider === "modal_snapshot_filesystem"
186
213
  ? "modal"
@@ -4,10 +4,24 @@ import { z } from "zod";
4
4
  export const ScopedKnowledgeScopeKind = z.enum(["organization", "workspace", "personal"]);
5
5
  export type ScopedKnowledgeScopeKind = z.infer<typeof ScopedKnowledgeScopeKind>;
6
6
 
7
- export type ScopedKnowledgeScope =
8
- | { kind: "organization"; workspaceId: null; subjectId: null }
9
- | { kind: "workspace"; workspaceId: string; subjectId: null }
10
- | { kind: "personal"; workspaceId: string | null; subjectId: string };
7
+ export const ScopedKnowledgeScope = z.discriminatedUnion("kind", [
8
+ z.object({
9
+ kind: z.literal("organization"),
10
+ workspaceId: z.null(),
11
+ subjectId: z.null(),
12
+ }),
13
+ z.object({
14
+ kind: z.literal("workspace"),
15
+ workspaceId: z.string().uuid(),
16
+ subjectId: z.null(),
17
+ }),
18
+ z.object({
19
+ kind: z.literal("personal"),
20
+ workspaceId: z.string().uuid().nullable(),
21
+ subjectId: z.string().min(1).max(1024),
22
+ }),
23
+ ]);
24
+ export type ScopedKnowledgeScope = z.infer<typeof ScopedKnowledgeScope>;
11
25
 
12
26
  export const ScopedKnowledgeActorKind = z.enum(["human", "service"]);
13
27
  export type ScopedKnowledgeActorKind = z.infer<typeof ScopedKnowledgeActorKind>;
@@ -41,6 +41,35 @@ export const OPENGENI_SLACK_BOT_EVENTS = [
41
41
  "reaction_added",
42
42
  ] as const;
43
43
 
44
+ /** Full user-token scope set for every tool currently exposed by Slack's hosted MCP server. */
45
+ export const OPENGENI_SLACK_MCP_USER_SCOPES = [
46
+ "search:read.public",
47
+ "search:read.private",
48
+ "search:read.mpim",
49
+ "search:read.im",
50
+ "search:read.files",
51
+ "files:read",
52
+ "emoji:read",
53
+ "search:read.users",
54
+ "chat:write",
55
+ "channels:history",
56
+ "groups:history",
57
+ "mpim:history",
58
+ "im:history",
59
+ "channels:write",
60
+ "groups:write",
61
+ "im:write",
62
+ "mpim:write",
63
+ "reactions:write",
64
+ "canvases:read",
65
+ "canvases:write",
66
+ "users:read",
67
+ "users:read.email",
68
+ "channels:read",
69
+ "groups:read",
70
+ "mpim:read",
71
+ ] as const;
72
+
44
73
  export const OPENGENI_MANAGED_PUBLIC_BASE_URL = "https://app.opengeni.ai" as const;
45
74
 
46
75
  function normalizedSlackManifestBaseUrl(publicBaseUrl: string): string {
@@ -83,7 +112,10 @@ export function buildOpenGeniSlackBotManifest(publicBaseUrl: string) {
83
112
  `${baseUrl}/v1/integrations/oauth/callback`,
84
113
  `${baseUrl}/v1/integrations/slack/callback`,
85
114
  ],
86
- scopes: { bot: [...OPENGENI_SLACK_BOT_REQUESTED_SCOPES] },
115
+ scopes: {
116
+ bot: [...OPENGENI_SLACK_BOT_REQUESTED_SCOPES],
117
+ user: [...OPENGENI_SLACK_MCP_USER_SCOPES],
118
+ },
87
119
  },
88
120
  settings: {
89
121
  event_subscriptions: {
@@ -94,6 +126,7 @@ export function buildOpenGeniSlackBotManifest(publicBaseUrl: string) {
94
126
  is_enabled: true,
95
127
  request_url: `${baseUrl}/v1/integrations/slack/interactions`,
96
128
  },
129
+ is_mcp_enabled: true,
97
130
  org_deploy_enabled: false,
98
131
  socket_mode_enabled: false,
99
132
  token_rotation_enabled: false,
@@ -0,0 +1,215 @@
1
+ import { z } from "zod";
2
+
3
+ export const SLACK_TASK_POLICY_ID_MAX_CHARS = 128;
4
+ export const SLACK_TASK_POLICY_REASON_MAX_CHARS = 4_096;
5
+ export const SLACK_TASK_POLICY_MAX_IDS = 256;
6
+
7
+ const SlackOpaqueId = z.string().min(1).max(SLACK_TASK_POLICY_ID_MAX_CHARS);
8
+
9
+ export const SlackSharedConversationMode = z.enum(["deny", "private_handoff"]);
10
+ export type SlackSharedConversationMode = z.infer<typeof SlackSharedConversationMode>;
11
+
12
+ export const SlackResultPublicationMode = z.enum(["never", "approval_required", "allow"]);
13
+ export type SlackResultPublicationMode = z.infer<typeof SlackResultPublicationMode>;
14
+
15
+ function uniqueSortedIds(values: string[]): string[] {
16
+ return [...new Set(values)].sort();
17
+ }
18
+
19
+ export const SlackTaskPolicyContent = z
20
+ .object({
21
+ allowedTeamIds: z
22
+ .array(SlackOpaqueId)
23
+ .max(SLACK_TASK_POLICY_MAX_IDS)
24
+ .transform(uniqueSortedIds),
25
+ allowedConversationIds: z
26
+ .array(SlackOpaqueId)
27
+ .max(SLACK_TASK_POLICY_MAX_IDS)
28
+ .transform(uniqueSortedIds),
29
+ allowGuestInitiators: z.boolean(),
30
+ allowExternalInitiators: z.boolean(),
31
+ allowMpim: z.boolean(),
32
+ sharedConversationMode: SlackSharedConversationMode,
33
+ resultPublicationMode: SlackResultPublicationMode,
34
+ })
35
+ .strict();
36
+ export type SlackTaskPolicyContent = z.infer<typeof SlackTaskPolicyContent>;
37
+
38
+ export const DEFAULT_SLACK_TASK_POLICY: SlackTaskPolicyContent = Object.freeze({
39
+ allowedTeamIds: [],
40
+ allowedConversationIds: [],
41
+ allowGuestInitiators: false,
42
+ allowExternalInitiators: false,
43
+ allowMpim: false,
44
+ sharedConversationMode: "deny",
45
+ resultPublicationMode: "never",
46
+ });
47
+
48
+ export function canonicalizeSlackTaskPolicy(value: SlackTaskPolicyContent): SlackTaskPolicyContent {
49
+ const parsed = SlackTaskPolicyContent.parse(value);
50
+ return {
51
+ allowedTeamIds: parsed.allowedTeamIds,
52
+ allowedConversationIds: parsed.allowedConversationIds,
53
+ allowGuestInitiators: parsed.allowGuestInitiators,
54
+ allowExternalInitiators: parsed.allowExternalInitiators,
55
+ allowMpim: parsed.allowMpim,
56
+ sharedConversationMode: parsed.sharedConversationMode,
57
+ resultPublicationMode: parsed.resultPublicationMode,
58
+ };
59
+ }
60
+
61
+ export const SlackTaskPolicyRevisionIdentity = z.object({
62
+ id: z.string().uuid(),
63
+ revision: z.number().int().positive(),
64
+ policyHash: z.string().regex(/^[0-9a-f]{64}$/),
65
+ });
66
+ export type SlackTaskPolicyRevisionIdentity = z.infer<typeof SlackTaskPolicyRevisionIdentity>;
67
+
68
+ export const SlackTaskPolicyRevision = SlackTaskPolicyRevisionIdentity.extend({
69
+ operationId: z.string().uuid(),
70
+ accountId: z.string().uuid(),
71
+ workspaceId: z.string().uuid(),
72
+ policy: SlackTaskPolicyContent,
73
+ supersedesRevisionId: z.string().uuid().nullable(),
74
+ createdBySubjectId: z.string().min(1).max(1_024),
75
+ createdAt: z.string().datetime(),
76
+ });
77
+ export type SlackTaskPolicyRevision = z.infer<typeof SlackTaskPolicyRevision>;
78
+
79
+ export const SlackTaskPolicyHead = z.object({
80
+ accountId: z.string().uuid(),
81
+ workspaceId: z.string().uuid(),
82
+ revisionId: z.string().uuid(),
83
+ revision: z.number().int().positive(),
84
+ policyHash: z.string().regex(/^[0-9a-f]{64}$/),
85
+ activationVersion: z.number().int().positive(),
86
+ activatedAt: z.string().datetime(),
87
+ });
88
+ export type SlackTaskPolicyHead = z.infer<typeof SlackTaskPolicyHead>;
89
+
90
+ export const SlackTaskPolicyActivationEvent = z.object({
91
+ id: z.string().uuid(),
92
+ operationId: z.string().uuid(),
93
+ accountId: z.string().uuid(),
94
+ workspaceId: z.string().uuid(),
95
+ activationVersion: z.number().int().positive(),
96
+ oldRevision: SlackTaskPolicyRevisionIdentity.nullable(),
97
+ newRevision: SlackTaskPolicyRevisionIdentity,
98
+ actorSubjectId: z.string().min(1).max(1_024),
99
+ reason: z.string().min(1).max(SLACK_TASK_POLICY_REASON_MAX_CHARS),
100
+ createdAt: z.string().datetime(),
101
+ });
102
+ export type SlackTaskPolicyActivationEvent = z.infer<typeof SlackTaskPolicyActivationEvent>;
103
+
104
+ export const SlackTaskPolicyListResponse = z.object({
105
+ current: SlackTaskPolicyHead.nullable(),
106
+ activeRevision: SlackTaskPolicyRevision.nullable(),
107
+ revisions: z.array(SlackTaskPolicyRevision),
108
+ activationEvents: z.array(SlackTaskPolicyActivationEvent),
109
+ });
110
+ export type SlackTaskPolicyListResponse = z.infer<typeof SlackTaskPolicyListResponse>;
111
+
112
+ export const UpdateSlackTaskPolicyRequest = z.object({
113
+ operationId: z.string().uuid().optional(),
114
+ policy: SlackTaskPolicyContent,
115
+ expectedCurrentRevisionId: z.string().uuid().nullable(),
116
+ expectedActivationVersion: z.number().int().nonnegative(),
117
+ reason: z.string().trim().min(1).max(SLACK_TASK_POLICY_REASON_MAX_CHARS),
118
+ });
119
+ export type UpdateSlackTaskPolicyRequest = z.infer<typeof UpdateSlackTaskPolicyRequest>;
120
+
121
+ export const SlackTaskPolicyMutationResponse = z.object({
122
+ revision: SlackTaskPolicyRevision,
123
+ head: SlackTaskPolicyHead,
124
+ event: SlackTaskPolicyActivationEvent,
125
+ });
126
+ export type SlackTaskPolicyMutationResponse = z.infer<typeof SlackTaskPolicyMutationResponse>;
127
+
128
+ export type SlackTaskPolicyConversationFacts = Readonly<{
129
+ installationTeamId: string;
130
+ conversationId: string;
131
+ contextTeamId: string | null;
132
+ connectedTeamIds: readonly string[] | null;
133
+ sharedTeamIds: readonly string[] | null;
134
+ isShared: boolean;
135
+ isExternallyShared: boolean;
136
+ isOrgShared: boolean;
137
+ isPendingExternallyShared: boolean;
138
+ isMpim: boolean;
139
+ }>;
140
+
141
+ export type SlackTaskPolicyInitiatorFacts = Readonly<{
142
+ teamId: string | null;
143
+ isGuest: boolean | null;
144
+ isExternal: boolean | null;
145
+ }>;
146
+
147
+ export type SlackTaskPolicyDecision = Readonly<{
148
+ disposition: "ordinary" | "deny" | "private_handoff";
149
+ publication: SlackResultPublicationMode;
150
+ reason:
151
+ | "ordinary_conversation"
152
+ | "policy_missing"
153
+ | "conversation_not_allowed"
154
+ | "team_not_allowed"
155
+ | "ambiguous_shared_facts"
156
+ | "mpim_not_allowed"
157
+ | "guest_not_allowed"
158
+ | "external_not_allowed"
159
+ | "allowed";
160
+ }>;
161
+
162
+ export function evaluateSlackTaskPolicy(input: {
163
+ policy: SlackTaskPolicyContent | null;
164
+ conversation: SlackTaskPolicyConversationFacts;
165
+ initiator: SlackTaskPolicyInitiatorFacts;
166
+ }): SlackTaskPolicyDecision {
167
+ const { conversation, initiator } = input;
168
+ const governed =
169
+ conversation.isShared ||
170
+ conversation.isExternallyShared ||
171
+ conversation.isOrgShared ||
172
+ conversation.isPendingExternallyShared ||
173
+ conversation.isMpim;
174
+ if (!governed) {
175
+ return { disposition: "ordinary", publication: "allow", reason: "ordinary_conversation" };
176
+ }
177
+ if (!input.policy) return { disposition: "deny", publication: "never", reason: "policy_missing" };
178
+ const policy = canonicalizeSlackTaskPolicy(input.policy);
179
+ if (!policy.allowedConversationIds.includes(conversation.conversationId)) {
180
+ return { disposition: "deny", publication: "never", reason: "conversation_not_allowed" };
181
+ }
182
+ if (conversation.isMpim && !policy.allowMpim) {
183
+ return { disposition: "deny", publication: "never", reason: "mpim_not_allowed" };
184
+ }
185
+ if (initiator.isGuest === null || initiator.isExternal === null || initiator.teamId === null) {
186
+ return { disposition: "deny", publication: "never", reason: "ambiguous_shared_facts" };
187
+ }
188
+ if (initiator.isGuest && !policy.allowGuestInitiators) {
189
+ return { disposition: "deny", publication: "never", reason: "guest_not_allowed" };
190
+ }
191
+ if (initiator.isExternal && !policy.allowExternalInitiators) {
192
+ return { disposition: "deny", publication: "never", reason: "external_not_allowed" };
193
+ }
194
+ const teams = [
195
+ conversation.installationTeamId,
196
+ conversation.contextTeamId,
197
+ initiator.teamId,
198
+ ...(conversation.connectedTeamIds ?? []),
199
+ ...(conversation.sharedTeamIds ?? []),
200
+ ];
201
+ if (conversation.connectedTeamIds === null || conversation.sharedTeamIds === null) {
202
+ return { disposition: "deny", publication: "never", reason: "ambiguous_shared_facts" };
203
+ }
204
+ if (teams.some((teamId) => teamId === null || !policy.allowedTeamIds.includes(teamId))) {
205
+ return { disposition: "deny", publication: "never", reason: "team_not_allowed" };
206
+ }
207
+ if (policy.sharedConversationMode === "deny") {
208
+ return { disposition: "deny", publication: "never", reason: "allowed" };
209
+ }
210
+ return {
211
+ disposition: "private_handoff",
212
+ publication: policy.resultPublicationMode,
213
+ reason: "allowed",
214
+ };
215
+ }