@opengeni/core 0.21.2 → 0.24.1

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 (123) hide show
  1. package/dist/access/index.d.ts +9 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +9 -4
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/chunk-6WKPWE5S.js +2380 -0
  6. package/dist/chunk-6WKPWE5S.js.map +1 -0
  7. package/dist/chunk-JJU6I5XD.js +2713 -0
  8. package/dist/chunk-JJU6I5XD.js.map +1 -0
  9. package/dist/chunk-NYPMQ7NO.js +2270 -0
  10. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  11. package/dist/dependencies.d.ts +26 -6
  12. package/dist/domain/capabilities.d.ts +35 -3
  13. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  14. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  15. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  16. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  17. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  18. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  19. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  20. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  21. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  22. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  23. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  24. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  25. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  26. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  27. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  28. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  29. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  30. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  31. package/dist/domain/memory-slack-publication.d.ts +0 -2
  32. package/dist/domain/packs.d.ts +29 -1
  33. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  34. package/dist/domain/scheduled-tasks.d.ts +27 -3
  35. package/dist/domain/session-tool-policy.d.ts +2 -2
  36. package/dist/domain/sessions.d.ts +44 -10
  37. package/dist/domain/skill-imports.d.ts +32 -0
  38. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  39. package/dist/domain/timeline-annotations.d.ts +5 -0
  40. package/dist/domain/video-generation-capabilities.d.ts +9 -0
  41. package/dist/domain/video-generation.d.ts +47 -0
  42. package/dist/editable-artifact-live/application.d.ts +69 -0
  43. package/dist/editable-artifact-live/errors.d.ts +1 -0
  44. package/dist/editable-artifact-live/index.d.ts +8 -0
  45. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  46. package/dist/editable-artifact-live/ports.d.ts +100 -0
  47. package/dist/editable-artifact-live/server.d.ts +71 -0
  48. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  49. package/dist/editable-artifact-live/types.d.ts +213 -0
  50. package/dist/editable-artifact-live/wire.d.ts +34 -0
  51. package/dist/editable-artifact-live.d.ts +2 -0
  52. package/dist/editable-artifact-live.js +58 -0
  53. package/dist/editable-artifact-live.js.map +1 -0
  54. package/dist/editable-artifacts.d.ts +2 -0
  55. package/dist/editable-artifacts.js +179 -0
  56. package/dist/editable-artifacts.js.map +1 -0
  57. package/dist/index.d.ts +35 -25
  58. package/dist/index.js +3184 -652
  59. package/dist/index.js.map +1 -1
  60. package/dist/managed-session.d.ts +53 -0
  61. package/dist/rigs/index.d.ts +1 -1
  62. package/dist/rigs/provider-images.d.ts +31 -0
  63. package/dist/sandbox/fleet.d.ts +9 -1
  64. package/dist/sandbox/routing.d.ts +1 -0
  65. package/dist/session-authorization.d.ts +10 -1
  66. package/dist/workflow-wake-contract.d.ts +7 -0
  67. package/package.json +18 -10
  68. package/src/access/index.ts +43 -3
  69. package/src/application/new-session-drafts.ts +2 -0
  70. package/src/application/session-commands.ts +217 -116
  71. package/src/dependencies.ts +30 -2
  72. package/src/domain/capabilities.ts +672 -218
  73. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  74. package/src/domain/durable-learning-slack-publication.ts +202 -0
  75. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  76. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  77. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  78. package/src/domain/editable-artifacts/errors.ts +115 -0
  79. package/src/domain/editable-artifacts/hash.ts +131 -0
  80. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  81. package/src/domain/editable-artifacts/index.ts +14 -0
  82. package/src/domain/editable-artifacts/ports.ts +462 -0
  83. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  84. package/src/domain/editable-artifacts/service.ts +2402 -0
  85. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  86. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  87. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  88. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  89. package/src/domain/editable-artifacts/types.ts +835 -0
  90. package/src/domain/insights.ts +136 -33
  91. package/src/domain/memory-slack-delivery.ts +204 -0
  92. package/src/domain/memory-slack-publication.ts +10 -19
  93. package/src/domain/packs.ts +266 -5
  94. package/src/domain/personal-connection-delegations.ts +97 -6
  95. package/src/domain/resources.ts +10 -20
  96. package/src/domain/scheduled-tasks.ts +365 -25
  97. package/src/domain/session-tool-policy.ts +8 -2
  98. package/src/domain/sessions.ts +388 -155
  99. package/src/domain/skill-imports.ts +350 -0
  100. package/src/domain/slack-publication-secret-safety.ts +42 -0
  101. package/src/domain/timeline-annotations.ts +204 -0
  102. package/src/domain/video-generation-capabilities.ts +69 -0
  103. package/src/domain/video-generation.ts +249 -0
  104. package/src/editable-artifact-live/application.ts +182 -0
  105. package/src/editable-artifact-live/errors.ts +4 -0
  106. package/src/editable-artifact-live/index.ts +8 -0
  107. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  108. package/src/editable-artifact-live/ports.ts +139 -0
  109. package/src/editable-artifact-live/server.ts +1707 -0
  110. package/src/editable-artifact-live/ticket.ts +217 -0
  111. package/src/editable-artifact-live/types.ts +286 -0
  112. package/src/editable-artifact-live/wire.ts +95 -0
  113. package/src/editable-artifact-live.ts +2 -0
  114. package/src/editable-artifacts.ts +2 -0
  115. package/src/index.ts +14 -0
  116. package/src/managed-auth-type.ts +4 -3
  117. package/src/managed-session.ts +36 -0
  118. package/src/rigs/index.ts +26 -19
  119. package/src/rigs/provider-images.ts +110 -0
  120. package/src/sandbox/fleet.ts +56 -1
  121. package/src/sandbox/routing.ts +32 -3
  122. package/src/session-authorization.ts +20 -0
  123. package/src/workflow-wake-contract.ts +8 -0
@@ -0,0 +1,350 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { SkillImportPreview, SkillImportSource } from "@opengeni/contracts";
3
+ import {
4
+ buildPortableSkillArtifact,
5
+ PORTABLE_SKILL_MAX_FILES,
6
+ PORTABLE_SKILL_MAX_TOTAL_BYTES,
7
+ type SkillLibraryFile,
8
+ } from "@opengeni/runtime/skill-library";
9
+ import { HTTPException } from "hono/http-exception";
10
+
11
+ export type GitHubSkillTreeEntry = Readonly<{
12
+ path: string;
13
+ type: "blob" | "tree" | "commit";
14
+ mode: string;
15
+ sha: string;
16
+ size: number | null;
17
+ }>;
18
+
19
+ export type GitHubSkillSourceClient = Readonly<{
20
+ resolveCommit(owner: string, repository: string, ref: string): Promise<string>;
21
+ listTree(
22
+ owner: string,
23
+ repository: string,
24
+ commit: string,
25
+ ): Promise<readonly GitHubSkillTreeEntry[]>;
26
+ readBlob(owner: string, repository: string, sha: string): Promise<Uint8Array>;
27
+ }>;
28
+
29
+ export type ResolvedSkillImport = Readonly<{
30
+ preview: SkillImportPreview;
31
+ files: readonly SkillLibraryFile[];
32
+ }>;
33
+
34
+ type ParsedSkillSource = Readonly<{
35
+ source: SkillImportSource;
36
+ owner: string;
37
+ repository: string;
38
+ ref: string;
39
+ requestedPath: string | null;
40
+ skillSlug: string | null;
41
+ inputUrl: string;
42
+ }>;
43
+
44
+ const githubSegment = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/u;
45
+ const gitCommit = /^[0-9a-f]{40,64}$/u;
46
+ const maxConcurrentBlobReads = 8;
47
+
48
+ export async function resolveSkillImport(
49
+ rawUrl: string,
50
+ client: GitHubSkillSourceClient,
51
+ ): Promise<ResolvedSkillImport> {
52
+ const parsed = parseSkillSource(rawUrl);
53
+ const sourceCommit = await client.resolveCommit(parsed.owner, parsed.repository, parsed.ref);
54
+ if (!gitCommit.test(sourceCommit)) {
55
+ throw new HTTPException(422, { message: "GitHub returned an invalid source commit" });
56
+ }
57
+ const tree = await client.listTree(parsed.owner, parsed.repository, sourceCommit);
58
+ const sourcePath = selectSkillRoot(parsed, tree);
59
+ const entries = skillFilesUnderRoot(tree, sourcePath);
60
+ const declaredBytes = entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0);
61
+ if (entries.length > PORTABLE_SKILL_MAX_FILES) {
62
+ throw new HTTPException(422, {
63
+ message: `Skill contains more than ${PORTABLE_SKILL_MAX_FILES} files`,
64
+ });
65
+ }
66
+ if (declaredBytes > PORTABLE_SKILL_MAX_TOTAL_BYTES) {
67
+ throw new HTTPException(422, {
68
+ message: `Skill exceeds ${PORTABLE_SKILL_MAX_TOTAL_BYTES} bytes`,
69
+ });
70
+ }
71
+ const files = await mapConcurrent(entries, maxConcurrentBlobReads, async (entry) => {
72
+ let content: string;
73
+ try {
74
+ content = new TextDecoder("utf-8", { fatal: true }).decode(
75
+ await client.readBlob(parsed.owner, parsed.repository, entry.sha),
76
+ );
77
+ } catch {
78
+ throw new HTTPException(422, {
79
+ message: `Skill file is not valid UTF-8 text: ${relativeSkillPath(entry.path, sourcePath)}`,
80
+ });
81
+ }
82
+ return { path: relativeSkillPath(entry.path, sourcePath), content };
83
+ });
84
+ let artifact;
85
+ try {
86
+ artifact = buildPortableSkillArtifact(files);
87
+ } catch (error) {
88
+ throw new HTTPException(422, {
89
+ message: error instanceof Error ? error.message : "Skill artifact is invalid",
90
+ });
91
+ }
92
+ const repositoryUrl = `https://github.com/${parsed.owner}/${parsed.repository}`;
93
+ const sourceUrl =
94
+ sourcePath === "."
95
+ ? `${repositoryUrl}/tree/${sourceCommit}`
96
+ : `${repositoryUrl}/tree/${sourceCommit}/${encodeGitHubPath(sourcePath)}`;
97
+ const warnings: string[] = [];
98
+ if (!artifact.files.some((file) => /(^|\/)licen[cs]e(?:\.|$)/iu.test(file.path))) {
99
+ warnings.push("No license file was found inside the selected Skill folder.");
100
+ }
101
+ const executableFiles = artifact.files.filter((file) => /(^|\/)scripts?\//u.test(file.path));
102
+ if (executableFiles.length > 0) {
103
+ warnings.push(
104
+ `${executableFiles.length} script file${executableFiles.length === 1 ? "" : "s"} will be available to the agent only after installation.`,
105
+ );
106
+ }
107
+ return {
108
+ preview: {
109
+ source: parsed.source,
110
+ sourceUrl,
111
+ repositoryUrl,
112
+ owner: parsed.owner,
113
+ repository: parsed.repository,
114
+ sourcePath,
115
+ sourceCommit,
116
+ name: artifact.name,
117
+ description: artifact.description,
118
+ contentSha256: artifact.contentSha256,
119
+ totalBytes: artifact.totalBytes,
120
+ files: artifact.files.map((file) => {
121
+ const bytes = new TextEncoder().encode(file.content);
122
+ return {
123
+ path: file.path,
124
+ byteSize: bytes.byteLength,
125
+ contentSha256: sha256Hex(bytes),
126
+ };
127
+ }),
128
+ warnings,
129
+ installed: false,
130
+ installationVersion: null,
131
+ },
132
+ files: artifact.files,
133
+ };
134
+ }
135
+
136
+ export function portableSkillPluginKey(
137
+ source: Pick<SkillImportPreview, "owner" | "repository" | "sourcePath">,
138
+ ): string {
139
+ return `skill/${source.owner}/${source.repository}/${source.sourcePath}`.toLowerCase();
140
+ }
141
+
142
+ export function portableSkillCapabilityId(
143
+ source: Pick<SkillImportPreview, "name" | "owner" | "repository" | "sourcePath">,
144
+ ): string {
145
+ const identity = portableSkillPluginKey(source);
146
+ const suffix = createHash("sha256").update(identity).digest("hex").slice(0, 12);
147
+ return `skill:${source.name.toLowerCase()}-${suffix}`;
148
+ }
149
+
150
+ export function parseSkillSource(rawUrl: string): ParsedSkillSource {
151
+ let url: URL;
152
+ try {
153
+ url = new URL(rawUrl);
154
+ } catch {
155
+ throw new HTTPException(422, { message: "Enter a valid GitHub or skills.sh URL" });
156
+ }
157
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) {
158
+ throw new HTTPException(422, {
159
+ message: "Skill imports require a credential-free HTTPS URL without a fragment",
160
+ });
161
+ }
162
+ const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
163
+ if (url.hostname === "skills.sh" || url.hostname === "www.skills.sh") {
164
+ if (segments.length !== 3) {
165
+ throw new HTTPException(422, {
166
+ message: "A skills.sh URL must identify one owner, repository, and Skill",
167
+ });
168
+ }
169
+ const [owner, repository, skillSlug] = segments;
170
+ assertGitHubRepository(owner!, repository!);
171
+ if (!skillSlug || !githubSegment.test(skillSlug)) {
172
+ throw new HTTPException(422, { message: "The skills.sh Skill name is invalid" });
173
+ }
174
+ return {
175
+ source: "skills_sh",
176
+ owner: owner!,
177
+ repository: stripGitSuffix(repository!),
178
+ ref: "HEAD",
179
+ requestedPath: null,
180
+ skillSlug,
181
+ inputUrl: url.toString(),
182
+ };
183
+ }
184
+ if (url.hostname !== "github.com" && url.hostname !== "www.github.com") {
185
+ throw new HTTPException(422, {
186
+ message: "Only github.com and skills.sh imports are supported",
187
+ });
188
+ }
189
+ if (segments.length < 2) {
190
+ throw new HTTPException(422, { message: "A GitHub URL must identify a repository" });
191
+ }
192
+ const owner = segments[0]!;
193
+ const repository = stripGitSuffix(segments[1]!);
194
+ assertGitHubRepository(owner, repository);
195
+ if (segments.length === 2) {
196
+ return {
197
+ source: "github",
198
+ owner,
199
+ repository,
200
+ ref: "HEAD",
201
+ requestedPath: null,
202
+ skillSlug: null,
203
+ inputUrl: url.toString(),
204
+ };
205
+ }
206
+ const mode = segments[2];
207
+ if (mode !== "tree" && mode !== "blob") {
208
+ throw new HTTPException(422, {
209
+ message: "Use a GitHub repository, tree, folder, or SKILL.md URL",
210
+ });
211
+ }
212
+ const ref = segments[3];
213
+ if (!ref) throw new HTTPException(422, { message: "The GitHub URL is missing a revision" });
214
+ const pathSegments = segments.slice(4);
215
+ if (pathSegments.length === 0) {
216
+ throw new HTTPException(422, { message: "The GitHub URL is missing a Skill folder" });
217
+ }
218
+ const requestedPath = normalizeGitHubPath(
219
+ mode === "blob" && pathSegments.at(-1)?.toLowerCase() === "skill.md"
220
+ ? pathSegments.slice(0, -1)
221
+ : pathSegments,
222
+ );
223
+ return {
224
+ source: "github",
225
+ owner,
226
+ repository,
227
+ ref,
228
+ requestedPath,
229
+ skillSlug: null,
230
+ inputUrl: url.toString(),
231
+ };
232
+ }
233
+
234
+ function selectSkillRoot(source: ParsedSkillSource, tree: readonly GitHubSkillTreeEntry[]): string {
235
+ const skillFiles = tree
236
+ .filter(
237
+ (entry) =>
238
+ entry.type === "blob" &&
239
+ entry.mode !== "120000" &&
240
+ (entry.path === "SKILL.md" || entry.path.endsWith("/SKILL.md")),
241
+ )
242
+ .map((entry) => entry.path.slice(0, -"/SKILL.md".length) || ".")
243
+ .sort();
244
+ if (source.requestedPath) {
245
+ const root = source.requestedPath;
246
+ if (!skillFiles.includes(root)) {
247
+ throw new HTTPException(422, {
248
+ message: `No top-level SKILL.md was found in ${root}`,
249
+ });
250
+ }
251
+ return root;
252
+ }
253
+ const candidates = source.skillSlug
254
+ ? skillFiles.filter((path) => path.split("/").at(-1) === source.skillSlug)
255
+ : skillFiles;
256
+ if (candidates.length === 0) {
257
+ throw new HTTPException(422, { message: "No Skill folder with SKILL.md was found" });
258
+ }
259
+ if (candidates.length > 1) {
260
+ throw new HTTPException(422, {
261
+ message: "This source contains multiple Skills; paste the exact GitHub folder URL",
262
+ });
263
+ }
264
+ return candidates[0]!;
265
+ }
266
+
267
+ function skillFilesUnderRoot(
268
+ tree: readonly GitHubSkillTreeEntry[],
269
+ root: string,
270
+ ): GitHubSkillTreeEntry[] {
271
+ const prefix = root === "." ? "" : `${root}/`;
272
+ const inside = tree.filter((entry) => entry.path.startsWith(prefix));
273
+ const unsupported = inside.find(
274
+ (entry) => entry.type === "commit" || (entry.type === "blob" && entry.mode === "120000"),
275
+ );
276
+ if (unsupported) {
277
+ throw new HTTPException(422, {
278
+ message: `Skill folders may not contain symbolic links or submodules (${unsupported.path})`,
279
+ });
280
+ }
281
+ const files = inside
282
+ .filter((entry) => entry.type === "blob")
283
+ .sort((left, right) => left.path.localeCompare(right.path));
284
+ if (files.length === 0) {
285
+ throw new HTTPException(422, { message: "The selected Skill folder is empty" });
286
+ }
287
+ return files;
288
+ }
289
+
290
+ function relativeSkillPath(path: string, root: string): string {
291
+ return root === "." ? path : path.slice(root.length + 1);
292
+ }
293
+
294
+ function normalizeGitHubPath(segments: readonly string[]): string {
295
+ if (
296
+ segments.length === 0 ||
297
+ segments.some(
298
+ (segment) =>
299
+ segment.length === 0 ||
300
+ segment === "." ||
301
+ segment === ".." ||
302
+ segment.includes("\\") ||
303
+ /[\u0000-\u001f\u007f]/u.test(segment),
304
+ )
305
+ ) {
306
+ throw new HTTPException(422, { message: "The GitHub Skill path is invalid" });
307
+ }
308
+ return segments.join("/");
309
+ }
310
+
311
+ function encodeGitHubPath(path: string): string {
312
+ return path
313
+ .split("/")
314
+ .map((segment) => encodeURIComponent(segment))
315
+ .join("/");
316
+ }
317
+
318
+ function stripGitSuffix(value: string): string {
319
+ return value.replace(/\.git$/iu, "");
320
+ }
321
+
322
+ function assertGitHubRepository(owner: string, repository: string): void {
323
+ if (!githubSegment.test(owner) || !githubSegment.test(stripGitSuffix(repository))) {
324
+ throw new HTTPException(422, { message: "The GitHub owner or repository name is invalid" });
325
+ }
326
+ }
327
+
328
+ async function mapConcurrent<Input, Output>(
329
+ values: readonly Input[],
330
+ concurrency: number,
331
+ map: (value: Input) => Promise<Output>,
332
+ ): Promise<Output[]> {
333
+ const output = new Array<Output>(values.length);
334
+ let next = 0;
335
+ await Promise.all(
336
+ Array.from({ length: Math.min(concurrency, values.length) }, async () => {
337
+ for (;;) {
338
+ const index = next;
339
+ next += 1;
340
+ if (index >= values.length) return;
341
+ output[index] = await map(values[index]!);
342
+ }
343
+ }),
344
+ );
345
+ return output;
346
+ }
347
+
348
+ function sha256Hex(bytes: Uint8Array): string {
349
+ return createHash("sha256").update(bytes).digest("hex");
350
+ }
@@ -0,0 +1,42 @@
1
+ export const SLACK_PUBLICATION_CREDENTIAL_OMISSION = "[credential omitted]";
2
+
3
+ const CREDENTIAL_PATTERNS = [
4
+ /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*?(?:-----END(?: [A-Z0-9]+)* PRIVATE KEY-----|$)/gi,
5
+ /\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,})/g,
6
+ /\b(?:sk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,})/g,
7
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
8
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}/g,
9
+ /\bAIza[0-9A-Za-z_-]{20,}/g,
10
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
11
+ ] as const;
12
+
13
+ const BEARER_CREDENTIAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi;
14
+ const CREDENTIAL_ASSIGNMENT_PATTERN =
15
+ /(^|[\s,;([])((?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|secret|token))\s*[:=]\s*(?:"[^"]{8,}"|'[^']{8,}'|[^\s,;\])}]{8,})/gi;
16
+ const CREDENTIAL_URL_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+@/gi;
17
+
18
+ /**
19
+ * Deterministic, sink-local protection for text that is about to be persisted
20
+ * as a Slack publication projection or rendered to Slack. It intentionally
21
+ * does not scan or mutate canonical Memory, durable-learning receipts, model
22
+ * context, history, events, or any other internal OpenGeni content path.
23
+ */
24
+ export function sanitizeSlackPublicationText(value: string): string {
25
+ let sanitized = value;
26
+ for (const pattern of CREDENTIAL_PATTERNS) {
27
+ sanitized = sanitized.replace(pattern, SLACK_PUBLICATION_CREDENTIAL_OMISSION);
28
+ }
29
+ sanitized = sanitized.replace(
30
+ BEARER_CREDENTIAL_PATTERN,
31
+ `Bearer ${SLACK_PUBLICATION_CREDENTIAL_OMISSION}`,
32
+ );
33
+ sanitized = sanitized.replace(
34
+ CREDENTIAL_ASSIGNMENT_PATTERN,
35
+ (_match, prefix: string, name: string) =>
36
+ `${prefix}${name}=${SLACK_PUBLICATION_CREDENTIAL_OMISSION}`,
37
+ );
38
+ return sanitized.replace(
39
+ CREDENTIAL_URL_PATTERN,
40
+ (_match, prefix: string) => `${prefix}${SLACK_PUBLICATION_CREDENTIAL_OMISSION}@`,
41
+ );
42
+ }
@@ -0,0 +1,204 @@
1
+ import {
2
+ DraftTimelineAnnotations,
3
+ SubmittedTimelineAnnotations,
4
+ numberTimelineAnnotations,
5
+ type DraftTimelineAnnotation,
6
+ type SessionEvent,
7
+ type SubmittedTimelineAnnotation,
8
+ type TimelineAnnotation,
9
+ } from "@opengeni/contracts";
10
+ import { getSessionEvent, type Database } from "@opengeni/db";
11
+ import { HTTPException } from "hono/http-exception";
12
+
13
+ const SOURCE_CONTEXT_BYTES = 160;
14
+ const ANSI_SEQUENCE = new RegExp("\\u001B\\[[0-?]*[ -/]*[@-~]", "g");
15
+
16
+ type AnnotationValidationOptions = {
17
+ requireNotes: boolean;
18
+ };
19
+
20
+ function invalidAnnotationSource(): never {
21
+ throw new HTTPException(422, { message: "Invalid timeline annotation source" });
22
+ }
23
+
24
+ function safeJson(value: unknown): string {
25
+ try {
26
+ return JSON.stringify(value) ?? "";
27
+ } catch {
28
+ return String(value ?? "");
29
+ }
30
+ }
31
+
32
+ function firstMcpText(value: unknown): string | null {
33
+ if (!Array.isArray(value)) return null;
34
+ for (const part of value) {
35
+ if (
36
+ part !== null &&
37
+ typeof part === "object" &&
38
+ (part as { type?: unknown }).type === "text" &&
39
+ typeof (part as { text?: unknown }).text === "string"
40
+ ) {
41
+ return (part as { text: string }).text;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function normalizedToolOutput(value: unknown, depth = 0): string {
48
+ if (depth > 8 || value === null || value === undefined)
49
+ return value == null ? "" : safeJson(value);
50
+ if (typeof value === "string") return value;
51
+ if (typeof value !== "object" || Array.isArray(value)) return safeJson(value);
52
+ const record = value as Record<string, unknown>;
53
+ if (record.type === "text" && typeof record.text === "string") return record.text;
54
+ const contentText = firstMcpText(record.content);
55
+ if (contentText !== null) return contentText;
56
+ if ("structuredContent" in record) {
57
+ return contentText ?? normalizedToolOutput(record.structuredContent, depth + 1);
58
+ }
59
+ if ("result" in record) return normalizedToolOutput(record.result, depth + 1);
60
+ return safeJson(value);
61
+ }
62
+
63
+ function stripExecBanner(text: string): string {
64
+ const marker = text.indexOf("\nOutput:\n");
65
+ if (marker >= 0) return text.slice(marker + "\nOutput:\n".length);
66
+ return text.startsWith("Output:\n") ? text.slice("Output:\n".length) : text;
67
+ }
68
+
69
+ export function timelineAnnotationSourceText(event: SessionEvent): string | null {
70
+ if (!event.payload || typeof event.payload !== "object" || Array.isArray(event.payload)) {
71
+ return null;
72
+ }
73
+ const payload = event.payload as Record<string, unknown>;
74
+ if (event.type === "user.message" || event.type === "agent.message.completed") {
75
+ return typeof payload.text === "string" ? payload.text : null;
76
+ }
77
+ if (event.type !== "agent.toolCall.output") return null;
78
+ const output = Object.prototype.hasOwnProperty.call(payload, "output")
79
+ ? payload.output
80
+ : payload.preview;
81
+ const text = stripExecBanner(normalizedToolOutput(output)).replace(ANSI_SEQUENCE, "");
82
+ return text.length > 0 ? text : null;
83
+ }
84
+
85
+ function utf8Prefix(value: string, maxBytes: number): string {
86
+ let output = "";
87
+ let bytes = 0;
88
+ for (const character of value) {
89
+ const next = new TextEncoder().encode(character).byteLength;
90
+ if (bytes + next > maxBytes) break;
91
+ output += character;
92
+ bytes += next;
93
+ }
94
+ return output;
95
+ }
96
+
97
+ function utf8Suffix(value: string, maxBytes: number): string {
98
+ let output = "";
99
+ let bytes = 0;
100
+ for (const character of [...value].reverse()) {
101
+ const next = new TextEncoder().encode(character).byteLength;
102
+ if (bytes + next > maxBytes) break;
103
+ output = character + output;
104
+ bytes += next;
105
+ }
106
+ return output;
107
+ }
108
+
109
+ function candidateOffsets(sourceText: string, quote: string): number[] {
110
+ const matches: number[] = [];
111
+ let cursor = 0;
112
+ while (cursor <= sourceText.length - quote.length) {
113
+ const index = sourceText.indexOf(quote, cursor);
114
+ if (index < 0) break;
115
+ matches.push(index);
116
+ cursor = index + 1;
117
+ }
118
+ return matches;
119
+ }
120
+
121
+ function resolveQuoteOffset(sourceText: string, annotation: DraftTimelineAnnotation): number {
122
+ const { source, quote } = annotation;
123
+ if (
124
+ source.endOffset - source.startOffset === quote.length &&
125
+ sourceText.slice(source.startOffset, source.endOffset) === quote
126
+ ) {
127
+ return source.startOffset;
128
+ }
129
+ const matches = candidateOffsets(sourceText, quote);
130
+ if (matches.length === 1) return matches[0]!;
131
+ const contextual = matches.filter((start) => {
132
+ const before = sourceText.slice(Math.max(0, start - source.contextBefore.length), start);
133
+ const after = sourceText.slice(
134
+ start + quote.length,
135
+ start + quote.length + source.contextAfter.length,
136
+ );
137
+ return before === source.contextBefore && after === source.contextAfter;
138
+ });
139
+ if (contextual.length === 1) return contextual[0]!;
140
+ return invalidAnnotationSource();
141
+ }
142
+
143
+ async function validateTimelineAnnotations(
144
+ db: Database,
145
+ workspaceId: string,
146
+ sessionId: string,
147
+ annotations: readonly DraftTimelineAnnotation[],
148
+ options: AnnotationValidationOptions,
149
+ ): Promise<DraftTimelineAnnotation[]> {
150
+ const parsed = options.requireNotes
151
+ ? SubmittedTimelineAnnotations.parse(annotations)
152
+ : DraftTimelineAnnotations.parse(annotations);
153
+ const validated: DraftTimelineAnnotation[] = [];
154
+ for (const annotation of parsed) {
155
+ const event = await getSessionEvent(db, workspaceId, annotation.source.eventId);
156
+ if (
157
+ !event ||
158
+ event.sessionId !== sessionId ||
159
+ event.type !== annotation.source.eventType ||
160
+ event.sequence !== annotation.source.sequence ||
161
+ (event.turnId ?? null) !== annotation.source.turnId
162
+ ) {
163
+ invalidAnnotationSource();
164
+ }
165
+ const sourceText = timelineAnnotationSourceText(event);
166
+ if (sourceText === null) invalidAnnotationSource();
167
+ const startOffset = resolveQuoteOffset(sourceText, annotation);
168
+ const endOffset = startOffset + annotation.quote.length;
169
+ validated.push({
170
+ ...annotation,
171
+ source: {
172
+ ...annotation.source,
173
+ startOffset,
174
+ endOffset,
175
+ contextBefore: utf8Suffix(sourceText.slice(0, startOffset), SOURCE_CONTEXT_BYTES),
176
+ contextAfter: utf8Prefix(sourceText.slice(endOffset), SOURCE_CONTEXT_BYTES),
177
+ },
178
+ });
179
+ }
180
+ return validated;
181
+ }
182
+
183
+ export async function validateDraftTimelineAnnotations(
184
+ db: Database,
185
+ workspaceId: string,
186
+ sessionId: string,
187
+ annotations: readonly DraftTimelineAnnotation[],
188
+ ): Promise<DraftTimelineAnnotation[]> {
189
+ return await validateTimelineAnnotations(db, workspaceId, sessionId, annotations, {
190
+ requireNotes: false,
191
+ });
192
+ }
193
+
194
+ export async function validateSubmittedTimelineAnnotations(
195
+ db: Database,
196
+ workspaceId: string,
197
+ sessionId: string,
198
+ annotations: readonly SubmittedTimelineAnnotation[],
199
+ ): Promise<TimelineAnnotation[]> {
200
+ const validated = await validateTimelineAnnotations(db, workspaceId, sessionId, annotations, {
201
+ requireNotes: true,
202
+ });
203
+ return numberTimelineAnnotations(validated as SubmittedTimelineAnnotation[]);
204
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ SEEDANCE_2_5_MODEL_ID,
3
+ VideoGenerationCapabilities,
4
+ VideoGenerationPolicy,
5
+ type VideoGenerationModelCapability,
6
+ type VideoGenerationPolicy as VideoGenerationPolicyType,
7
+ } from "@opengeni/contracts";
8
+ import { videoGenerationCapabilityRevision } from "./video-generation";
9
+
10
+ /** Reviewed executable catalog. Dynamic workspace policy never mutates tool schemas. */
11
+ export const VIDEO_GENERATION_MODEL_CATALOG: readonly VideoGenerationModelCapability[] =
12
+ Object.freeze([
13
+ Object.freeze({
14
+ modelId: SEEDANCE_2_5_MODEL_ID,
15
+ label: "Seedance 2.5",
16
+ providerLabel: "Vercel AI Gateway",
17
+ sourceModes: Object.freeze([
18
+ "text",
19
+ "first_frame",
20
+ "first_and_last_frames",
21
+ "image_reference",
22
+ "video_reference",
23
+ ]),
24
+ resolutions: Object.freeze(["480p", "720p"]),
25
+ aspectRatios: Object.freeze(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"]),
26
+ duration: Object.freeze({ minSeconds: 4, maxSeconds: 30, stepSeconds: 1 }),
27
+ supportsAudio: true,
28
+ }) as VideoGenerationModelCapability,
29
+ ]);
30
+
31
+ export function defaultVideoGenerationPolicy(): VideoGenerationPolicyType {
32
+ return VideoGenerationPolicy.parse({
33
+ schemaVersion: 1,
34
+ revision: 0,
35
+ fundingSource: "workspace_gateway",
36
+ enabledModelIds: [],
37
+ defaultModelId: null,
38
+ });
39
+ }
40
+
41
+ export function videoGenerationCapabilitiesForPolicy(input: {
42
+ policy: VideoGenerationPolicyType;
43
+ credentialVersion: number;
44
+ }): VideoGenerationCapabilities {
45
+ const policy = VideoGenerationPolicy.parse(input.policy);
46
+ const enabled = new Set(policy.enabledModelIds);
47
+ const models = VIDEO_GENERATION_MODEL_CATALOG.filter((model) => enabled.has(model.modelId));
48
+ if (models.length === 0 || policy.defaultModelId === null) {
49
+ throw new Error("Video generation is disabled for this workspace");
50
+ }
51
+ return VideoGenerationCapabilities.parse({
52
+ schemaVersion: 1,
53
+ capabilityRevision: videoGenerationCapabilityRevision({
54
+ policyRevision: policy.revision,
55
+ fundingSource: policy.fundingSource,
56
+ credentialVersion: input.credentialVersion,
57
+ modelIds: models.map((model) => model.modelId),
58
+ }),
59
+ defaultModelId: policy.defaultModelId,
60
+ models,
61
+ });
62
+ }
63
+
64
+ export function assertKnownVideoGenerationModelIds(modelIds: readonly string[]): void {
65
+ const known = new Set(VIDEO_GENERATION_MODEL_CATALOG.map((model) => model.modelId));
66
+ for (const modelId of modelIds) {
67
+ if (!known.has(modelId)) throw new Error(`Unknown video generation model: ${modelId}`);
68
+ }
69
+ }