@opengeni/core 0.28.1 → 1.0.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 (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18812 -6833
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import type { SkillImportPreview, SkillImportSource } from "@opengeni/contracts";
3
3
  import {
4
4
  buildPortableSkillArtifact,
5
+ parsePortableSkillFrontmatter,
6
+ PORTABLE_SKILL_MAX_FILE_BYTES,
5
7
  PORTABLE_SKILL_MAX_FILES,
6
8
  PORTABLE_SKILL_MAX_TOTAL_BYTES,
7
9
  type SkillLibraryFile,
@@ -17,6 +19,11 @@ export type GitHubSkillTreeEntry = Readonly<{
17
19
  }>;
18
20
 
19
21
  export type GitHubSkillSourceClient = Readonly<{
22
+ downloadSnapshot?(
23
+ owner: string,
24
+ repository: string,
25
+ slug: string,
26
+ ): Promise<readonly SkillLibraryFile[]>;
20
27
  resolveCommit(owner: string, repository: string, ref: string): Promise<string>;
21
28
  listTree(
22
29
  owner: string,
@@ -50,12 +57,27 @@ export async function resolveSkillImport(
50
57
  client: GitHubSkillSourceClient,
51
58
  ): Promise<ResolvedSkillImport> {
52
59
  const parsed = parseSkillSource(rawUrl);
60
+ if (parsed.source === "skills_sh" && client.downloadSnapshot) {
61
+ const files = await client.downloadSnapshot(parsed.owner, parsed.repository, parsed.skillSlug!);
62
+ return buildResolvedImport(parsed, files, parsed.skillSlug!, null);
63
+ }
53
64
  const sourceCommit = await client.resolveCommit(parsed.owner, parsed.repository, parsed.ref);
54
65
  if (!gitCommit.test(sourceCommit)) {
55
66
  throw new HTTPException(422, { message: "GitHub returned an invalid source commit" });
56
67
  }
57
68
  const tree = await client.listTree(parsed.owner, parsed.repository, sourceCommit);
58
- const sourcePath = selectSkillRoot(parsed, tree);
69
+ // Metadata discovery and the selected artifact share immutable blobs. This
70
+ // avoids reading SKILL.md twice without caching mutable refs or remote URLs.
71
+ const blobs = new Map<string, Promise<Uint8Array>>();
72
+ const readBlob = (sha: string) => {
73
+ let pending = blobs.get(sha);
74
+ if (!pending) {
75
+ pending = client.readBlob(parsed.owner, parsed.repository, sha);
76
+ blobs.set(sha, pending);
77
+ }
78
+ return pending;
79
+ };
80
+ const sourcePath = await selectSkillRoot(parsed, tree, readBlob, sourceCommit);
59
81
  const entries = skillFilesUnderRoot(tree, sourcePath);
60
82
  const declaredBytes = entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0);
61
83
  if (entries.length > PORTABLE_SKILL_MAX_FILES) {
@@ -69,11 +91,11 @@ export async function resolveSkillImport(
69
91
  });
70
92
  }
71
93
  const files = await mapConcurrent(entries, maxConcurrentBlobReads, async (entry) => {
94
+ // Keep provider/network failures distinct from UTF-8 validation failures.
95
+ const bytes = await readBlob(entry.sha);
72
96
  let content: string;
73
97
  try {
74
- content = new TextDecoder("utf-8", { fatal: true }).decode(
75
- await client.readBlob(parsed.owner, parsed.repository, entry.sha),
76
- );
98
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
77
99
  } catch {
78
100
  throw new HTTPException(422, {
79
101
  message: `Skill file is not valid UTF-8 text: ${relativeSkillPath(entry.path, sourcePath)}`,
@@ -81,6 +103,15 @@ export async function resolveSkillImport(
81
103
  }
82
104
  return { path: relativeSkillPath(entry.path, sourcePath), content };
83
105
  });
106
+ return buildResolvedImport(parsed, files, sourcePath, sourceCommit);
107
+ }
108
+
109
+ function buildResolvedImport(
110
+ parsed: ParsedSkillSource,
111
+ files: readonly SkillLibraryFile[],
112
+ sourcePath: string,
113
+ commit: string | null,
114
+ ): ResolvedSkillImport {
84
115
  let artifact;
85
116
  try {
86
117
  artifact = buildPortableSkillArtifact(files);
@@ -90,8 +121,25 @@ export async function resolveSkillImport(
90
121
  });
91
122
  }
92
123
  const repositoryUrl = `https://github.com/${parsed.owner}/${parsed.repository}`;
93
- const sourceUrl =
94
- sourcePath === "."
124
+ // The existing revision field holds our content digest for registry snapshots;
125
+ // snapshots do not provide a Git commit or a repository-relative folder path.
126
+ const sourceCommit = commit ?? artifact.contentSha256;
127
+ if (
128
+ !commit &&
129
+ artifact.name
130
+ .toLowerCase()
131
+ .replace(/[\s_]+/g, "-")
132
+ .replace(/[^a-z0-9-]/g, "")
133
+ .replace(/-+/g, "-")
134
+ .replace(/^-|-$/g, "") !== parsed.skillSlug!.toLowerCase()
135
+ ) {
136
+ throw new HTTPException(422, {
137
+ message: "The downloaded skill does not match the requested name",
138
+ });
139
+ }
140
+ const sourceUrl = !commit
141
+ ? `https://skills.sh/${parsed.owner}/${parsed.repository}/${parsed.skillSlug}`
142
+ : sourcePath === "."
95
143
  ? `${repositoryUrl}/tree/${sourceCommit}`
96
144
  : `${repositoryUrl}/tree/${sourceCommit}/${encodeGitHubPath(sourcePath)}`;
97
145
  const warnings: string[] = [];
@@ -106,6 +154,7 @@ export async function resolveSkillImport(
106
154
  }
107
155
  return {
108
156
  preview: {
157
+ markdown: artifact.files.find((file) => file.path === "SKILL.md")?.content,
109
158
  source: parsed.source,
110
159
  sourceUrl,
111
160
  repositoryUrl,
@@ -159,7 +208,12 @@ export function parseSkillSource(rawUrl: string): ParsedSkillSource {
159
208
  message: "Skill imports require a credential-free HTTPS URL without a fragment",
160
209
  });
161
210
  }
162
- const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
211
+ let segments: string[];
212
+ try {
213
+ segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
214
+ } catch {
215
+ throw new HTTPException(422, { message: "The Skill URL contains invalid encoding" });
216
+ }
163
217
  if (url.hostname === "skills.sh" || url.hostname === "www.skills.sh") {
164
218
  if (segments.length !== 3) {
165
219
  throw new HTTPException(422, {
@@ -212,14 +266,14 @@ export function parseSkillSource(rawUrl: string): ParsedSkillSource {
212
266
  const ref = segments[3];
213
267
  if (!ref) throw new HTTPException(422, { message: "The GitHub URL is missing a revision" });
214
268
  const pathSegments = segments.slice(4);
215
- if (pathSegments.length === 0) {
269
+ if (pathSegments.length === 0 && mode === "blob") {
216
270
  throw new HTTPException(422, { message: "The GitHub URL is missing a Skill folder" });
217
271
  }
218
- const requestedPath = normalizeGitHubPath(
272
+ const folderSegments =
219
273
  mode === "blob" && pathSegments.at(-1)?.toLowerCase() === "skill.md"
220
274
  ? pathSegments.slice(0, -1)
221
- : pathSegments,
222
- );
275
+ : pathSegments;
276
+ const requestedPath = folderSegments.length === 0 ? "." : normalizeGitHubPath(folderSegments);
223
277
  return {
224
278
  source: "github",
225
279
  owner,
@@ -231,7 +285,12 @@ export function parseSkillSource(rawUrl: string): ParsedSkillSource {
231
285
  };
232
286
  }
233
287
 
234
- function selectSkillRoot(source: ParsedSkillSource, tree: readonly GitHubSkillTreeEntry[]): string {
288
+ async function selectSkillRoot(
289
+ source: ParsedSkillSource,
290
+ tree: readonly GitHubSkillTreeEntry[],
291
+ readBlob: (sha: string) => Promise<Uint8Array>,
292
+ sourceCommit: string,
293
+ ): Promise<string> {
235
294
  const skillFiles = tree
236
295
  .filter(
237
296
  (entry) =>
@@ -239,7 +298,7 @@ function selectSkillRoot(source: ParsedSkillSource, tree: readonly GitHubSkillTr
239
298
  entry.mode !== "120000" &&
240
299
  (entry.path === "SKILL.md" || entry.path.endsWith("/SKILL.md")),
241
300
  )
242
- .map((entry) => entry.path.slice(0, -"/SKILL.md".length) || ".")
301
+ .map((entry) => (entry.path === "SKILL.md" ? "." : entry.path.slice(0, -"/SKILL.md".length)))
243
302
  .sort();
244
303
  if (source.requestedPath) {
245
304
  const root = source.requestedPath;
@@ -250,9 +309,61 @@ function selectSkillRoot(source: ParsedSkillSource, tree: readonly GitHubSkillTr
250
309
  }
251
310
  return root;
252
311
  }
253
- const candidates = source.skillSlug
254
- ? skillFiles.filter((path) => path.split("/").at(-1) === source.skillSlug)
255
- : skillFiles;
312
+ let candidates = skillFiles;
313
+ if (source.skillSlug) {
314
+ // skills.sh identifies frontmatter names, not necessarily folder basenames
315
+ // (e.g. vercel-react-best-practices lives in skills/react-best-practices).
316
+ // Bound discovery as well as the eventual artifact; exact folder URLs avoid
317
+ // this scan for very large repositories or duplicate names.
318
+ if (skillFiles.length > PORTABLE_SKILL_MAX_FILES) {
319
+ throw new HTTPException(422, {
320
+ message: "Too many Skill candidates; paste the exact GitHub folder URL",
321
+ });
322
+ }
323
+ let metadataBytes = 0;
324
+ const entriesByPath = new Map(tree.map((entry) => [entry.path, entry]));
325
+ const matches = await mapConcurrent(skillFiles, maxConcurrentBlobReads, async (root) => {
326
+ const entry = entriesByPath.get(root === "." ? "SKILL.md" : `${root}/SKILL.md`)!;
327
+ if ((entry.size ?? 0) > PORTABLE_SKILL_MAX_FILE_BYTES) {
328
+ throw new HTTPException(422, {
329
+ message: "Skill metadata is too large; paste the exact GitHub folder URL",
330
+ });
331
+ }
332
+ const bytes = await readBlob(entry.sha);
333
+ metadataBytes += bytes.byteLength;
334
+ if (
335
+ bytes.byteLength > PORTABLE_SKILL_MAX_FILE_BYTES ||
336
+ metadataBytes > PORTABLE_SKILL_MAX_TOTAL_BYTES
337
+ ) {
338
+ throw new HTTPException(422, {
339
+ message: "Skill metadata is too large; paste the exact GitHub folder URL",
340
+ });
341
+ }
342
+ let markdown: string;
343
+ try {
344
+ markdown = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
345
+ } catch {
346
+ throw new HTTPException(422, {
347
+ message: "Skill metadata is not valid UTF-8; paste the exact GitHub folder URL",
348
+ });
349
+ }
350
+ return (
351
+ parsePortableSkillFrontmatter(markdown).name?.toLowerCase() ===
352
+ source.skillSlug!.toLowerCase()
353
+ );
354
+ });
355
+ candidates = skillFiles.filter((_, index) => matches[index]);
356
+ if (candidates.length === 0) {
357
+ // A folder basename is not a skills.sh identity. Never silently import a
358
+ // different frontmatter name from a stale directory-shaped link.
359
+ const folders = skillFiles.filter((path) => path.split("/").at(-1) === source.skillSlug);
360
+ const exactPath =
361
+ folders.length === 1 ? encodeGitHubPath(folders[0]!) : "<exact-skill-folder-path>";
362
+ throw new HTTPException(422, {
363
+ message: `No Skill frontmatter name matches skills.sh slug "${source.skillSlug}"; the link may be stale. Check the current Skill name, or explicitly select the intended Skill using its exact GitHub folder URL: https://github.com/${source.owner}/${source.repository}/tree/${sourceCommit}/${exactPath}`,
364
+ });
365
+ }
366
+ }
256
367
  if (candidates.length === 0) {
257
368
  throw new HTTPException(422, { message: "No Skill folder with SKILL.md was found" });
258
369
  }
@@ -332,13 +443,22 @@ async function mapConcurrent<Input, Output>(
332
443
  ): Promise<Output[]> {
333
444
  const output = new Array<Output>(values.length);
334
445
  let next = 0;
446
+ let failed = false;
335
447
  await Promise.all(
336
448
  Array.from({ length: Math.min(concurrency, values.length) }, async () => {
337
449
  for (;;) {
450
+ if (failed) return;
338
451
  const index = next;
339
452
  next += 1;
340
453
  if (index >= values.length) return;
341
- output[index] = await map(values[index]!);
454
+ try {
455
+ output[index] = await map(values[index]!);
456
+ } catch (error) {
457
+ // Let existing reads settle, but do not launch further provider calls
458
+ // after any peer detects a failed request or an exceeded byte budget.
459
+ failed = true;
460
+ throw error;
461
+ }
342
462
  }
343
463
  }),
344
464
  );
@@ -0,0 +1,221 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { pinnedFetch, readResponseJsonBounded } from "@opengeni/network";
3
+
4
+ export type PublicSkillSearchInput = Readonly<{
5
+ query: string;
6
+ limit?: number;
7
+ owner?: string;
8
+ }>;
9
+
10
+ export type PublicSkillSearchResult = Readonly<{
11
+ provider: "skills_sh";
12
+ query: string;
13
+ items: readonly Readonly<{
14
+ id: string;
15
+ name: string;
16
+ source: string;
17
+ skillId: string;
18
+ url: string;
19
+ installs: number;
20
+ }>[];
21
+ nextCursor: null;
22
+ }>;
23
+
24
+ export type PublicSkillSearchClient = Readonly<{
25
+ search(input: PublicSkillSearchInput): Promise<PublicSkillSearchResult>;
26
+ }>;
27
+
28
+ export type PublicSkillSearchErrorCode =
29
+ | "invalid_query"
30
+ | "rate_limited"
31
+ | "timeout"
32
+ | "unavailable"
33
+ | "invalid_response";
34
+
35
+ export class PublicSkillSearchError extends Error {
36
+ constructor(
37
+ readonly code: PublicSkillSearchErrorCode,
38
+ message: string,
39
+ readonly retryAfterSeconds: number | null = null,
40
+ ) {
41
+ super(message);
42
+ this.name = "PublicSkillSearchError";
43
+ }
44
+ }
45
+
46
+ // Undocumented, unauthenticated compatibility endpoint used by the official CLI.
47
+ // Verified 2026-09-08 against vercel-labs/skills commit
48
+ // 1682051d48c34f5eb135e6475c1a965dce05e820, src/find.ts. This is NOT the documented
49
+ // /api/v1/skills/search API (which requires Vercel OIDC). Keep this contract behind
50
+ // this adapter: no promised pagination/quota, no CLI dependency or credentials.
51
+ const endpoint = "https://skills.sh/api/search";
52
+ const timeoutMs = 10_000;
53
+ const maxResponseBytes = 256 * 1024;
54
+ const maxResults = 20;
55
+ const ownerPattern = /^[a-z0-9](?:[a-z0-9-]{0,38})$/iu;
56
+ const segmentPattern = /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/iu;
57
+
58
+ export function createPublicSkillSearchClient(
59
+ settings: Settings,
60
+ fetcher: typeof pinnedFetch = pinnedFetch,
61
+ ): PublicSkillSearchClient {
62
+ return {
63
+ async search(input) {
64
+ const query = input.query.trim();
65
+ const limit = input.limit ?? maxResults;
66
+ const owner = input.owner?.trim().toLowerCase();
67
+ if (
68
+ query.length < 2 ||
69
+ query.length > 200 ||
70
+ /[\u0000-\u001f\u007f]/u.test(query) ||
71
+ !Number.isSafeInteger(limit) ||
72
+ limit < 1 ||
73
+ limit > maxResults ||
74
+ (owner !== undefined && !ownerPattern.test(owner))
75
+ ) {
76
+ throw new PublicSkillSearchError("invalid_query", "Invalid public Skill search parameters");
77
+ }
78
+ const url = new URL(endpoint);
79
+ url.searchParams.set("q", query);
80
+ url.searchParams.set("limit", String(limit));
81
+ if (owner) url.searchParams.set("owner", owner);
82
+ const controller = new AbortController();
83
+ let timer: ReturnType<typeof setTimeout> | undefined;
84
+ const deadline = new Promise<never>((_, reject) => {
85
+ timer = setTimeout(() => {
86
+ controller.abort();
87
+ reject(new PublicSkillSearchError("timeout", "Public Skill search timed out"));
88
+ }, timeoutMs);
89
+ });
90
+ try {
91
+ return await Promise.race([
92
+ deadline,
93
+ (async () => {
94
+ const response = await fetcher(
95
+ url,
96
+ {
97
+ method: "GET",
98
+ headers: { accept: "application/json" },
99
+ credentials: "omit",
100
+ redirect: "manual",
101
+ signal: controller.signal,
102
+ },
103
+ settings,
104
+ { label: "Public Skill search", requireHttpsOutsideLocalTest: true },
105
+ );
106
+ if (controller.signal.aborted || !response.ok) {
107
+ void response.body?.cancel().catch(() => undefined);
108
+ if (controller.signal.aborted) {
109
+ throw new PublicSkillSearchError("timeout", "Public Skill search timed out");
110
+ }
111
+ if (response.status === 429) {
112
+ throw new PublicSkillSearchError(
113
+ "rate_limited",
114
+ "Public Skill search is rate limited",
115
+ parseRetryAfter(response.headers.get("retry-after")),
116
+ );
117
+ }
118
+ throw new PublicSkillSearchError("unavailable", "Public Skill search is unavailable");
119
+ }
120
+ let payload: unknown;
121
+ try {
122
+ payload = await readResponseJsonBounded(
123
+ response,
124
+ maxResponseBytes,
125
+ "Public Skill search",
126
+ {
127
+ signal: controller.signal,
128
+ },
129
+ );
130
+ } catch {
131
+ throw new PublicSkillSearchError(
132
+ controller.signal.aborted ? "timeout" : "invalid_response",
133
+ controller.signal.aborted
134
+ ? "Public Skill search timed out"
135
+ : "Public Skill search returned an invalid response",
136
+ );
137
+ }
138
+ return normalizeResponse(payload, query, limit, owner);
139
+ })(),
140
+ ]);
141
+ } catch (error) {
142
+ if (error instanceof PublicSkillSearchError) throw error;
143
+ throw new PublicSkillSearchError(
144
+ controller.signal.aborted ? "timeout" : "unavailable",
145
+ controller.signal.aborted
146
+ ? "Public Skill search timed out"
147
+ : "Public Skill search is unavailable",
148
+ );
149
+ } finally {
150
+ clearTimeout(timer);
151
+ }
152
+ },
153
+ };
154
+ }
155
+
156
+ function normalizeResponse(
157
+ payload: unknown,
158
+ query: string,
159
+ limit: number,
160
+ owner: string | undefined,
161
+ ): PublicSkillSearchResult {
162
+ const invalid = () =>
163
+ new PublicSkillSearchError(
164
+ "invalid_response",
165
+ "Public Skill search returned an invalid response",
166
+ );
167
+ if (!isRecord(payload) || !Array.isArray(payload.skills) || payload.skills.length > maxResults) {
168
+ throw invalid();
169
+ }
170
+ const seen = new Set<string>();
171
+ const items: PublicSkillSearchResult["items"][number][] = [];
172
+ for (const skill of payload.skills) {
173
+ if (!isRecord(skill) || typeof skill.id !== "string" || typeof skill.source !== "string")
174
+ throw invalid();
175
+ const parts = skill.id.split("/");
176
+ // Website-hosted skills cannot use our GitHub import flow. Their presence
177
+ // must not discard the supported results in the same response.
178
+ if (parts.length === 2 && skill.source === parts[0]) continue;
179
+ if (
180
+ parts.length !== 3 ||
181
+ !ownerPattern.test(parts[0]!) ||
182
+ !parts.every((part) => segmentPattern.test(part)) ||
183
+ skill.source !== parts.slice(0, 2).join("/") ||
184
+ (skill.skillId !== undefined && skill.skillId !== parts[2]) ||
185
+ typeof skill.name !== "string" ||
186
+ !skill.name.trim() ||
187
+ skill.name.length > 200 ||
188
+ /[\u0000-\u001f\u007f]/u.test(skill.name) ||
189
+ typeof skill.installs !== "number" ||
190
+ !Number.isSafeInteger(skill.installs) ||
191
+ skill.installs < 0 ||
192
+ (owner !== undefined && parts[0]!.toLowerCase() !== owner)
193
+ )
194
+ throw invalid();
195
+ if (seen.has(skill.id)) continue;
196
+ seen.add(skill.id);
197
+ items.push({
198
+ id: skill.id,
199
+ name: skill.name.trim(),
200
+ source: skill.source,
201
+ skillId: parts[2]!,
202
+ url: `https://skills.sh/${parts.map(encodeURIComponent).join("/")}`,
203
+ installs: skill.installs,
204
+ });
205
+ }
206
+ return { provider: "skills_sh", query, items: items.slice(0, limit), nextCursor: null };
207
+ }
208
+
209
+ function isRecord(value: unknown): value is Record<string, unknown> {
210
+ return typeof value === "object" && value !== null && !Array.isArray(value);
211
+ }
212
+
213
+ function parseRetryAfter(value: string | null): number | null {
214
+ if (value === null) return null;
215
+ if (/^\d+$/u.test(value)) {
216
+ const seconds = Number(value);
217
+ return Number.isSafeInteger(seconds) ? seconds : null;
218
+ }
219
+ const date = Date.parse(value);
220
+ return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1000)) : null;
221
+ }
@@ -0,0 +1,104 @@
1
+ import type {
2
+ SkillFile,
3
+ SkillInstallInput,
4
+ SkillRevisionInput,
5
+ SkillSaveInput,
6
+ SkillRemoveInput,
7
+ } from "@opengeni/contracts";
8
+ import { validateSkillTextFiles } from "@opengeni/contracts";
9
+ import { buildPortableSkillArtifact } from "@opengeni/runtime/skill-library";
10
+ import {
11
+ applySkillLifecycle,
12
+ listSkillRecords,
13
+ skillFilesContentHash,
14
+ type Database,
15
+ type SkillReadContext,
16
+ } from "@opengeni/db";
17
+ export { replayPortableSkillInstall } from "@opengeni/db";
18
+
19
+ /** Canonical text-only folder; rejects invalid Unicode before PostgreSQL conversion. */
20
+ export function validateSkillFiles(files: readonly SkillFile[]): SkillFile[] {
21
+ validateSkillTextFiles(files);
22
+ const result = files.map(({ path, content }) => ({ path, content }));
23
+ if (!result.some((file) => file.path === "SKILL.md" && file.content.trim()))
24
+ throw new Error("Skill requires nonempty SKILL.md");
25
+ return result.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
26
+ }
27
+
28
+ /** Separate from the historical SKILL.md hash, which must never be rewritten. */
29
+ export function skillBundleHash(files: readonly SkillFile[]): string {
30
+ return skillFilesContentHash(validateSkillFiles(files));
31
+ }
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
+ }
42
+ export async function readSkill(
43
+ db: Database,
44
+ context: SkillReadContext,
45
+ skillId: string,
46
+ revisionId?: string,
47
+ ) {
48
+ return (
49
+ (
50
+ await listSkillRecords(db, context, {
51
+ skillId,
52
+ ...(revisionId ? { revisionId } : {}),
53
+ limit: 1,
54
+ })
55
+ )[0] ?? null
56
+ );
57
+ }
58
+ export async function saveSkill(db: Database, input: SkillSaveInput) {
59
+ const { accountId, workspaceId, actor, ...request } = input;
60
+ const artifact = buildPortableSkillArtifact(validateSkillFiles(input.files));
61
+ return applySkillLifecycle(
62
+ db,
63
+ { accountId, workspaceId, actor },
64
+ {
65
+ ...request,
66
+ operation: "save",
67
+ files: artifact.files,
68
+ title: artifact.name,
69
+ description: artifact.description,
70
+ },
71
+ );
72
+ }
73
+ export async function installSkill(db: Database, input: SkillInstallInput) {
74
+ const { accountId, workspaceId, actor, ...request } = input;
75
+ return applySkillLifecycle(
76
+ db,
77
+ { accountId, workspaceId, actor },
78
+ { ...request, operation: "install" },
79
+ );
80
+ }
81
+ export async function approveSkill(db: Database, input: SkillRevisionInput) {
82
+ const { accountId, workspaceId, actor, ...request } = input;
83
+ return applySkillLifecycle(
84
+ db,
85
+ { accountId, workspaceId, actor },
86
+ { ...request, operation: "approve" },
87
+ );
88
+ }
89
+ export async function rejectSkill(db: Database, input: SkillRevisionInput) {
90
+ const { accountId, workspaceId, actor, ...request } = input;
91
+ return applySkillLifecycle(
92
+ db,
93
+ { accountId, workspaceId, actor },
94
+ { ...request, operation: "reject" },
95
+ );
96
+ }
97
+ export async function restoreSkill(db: Database, input: SkillRevisionInput) {
98
+ const { accountId, workspaceId, actor, ...request } = input;
99
+ return applySkillLifecycle(
100
+ db,
101
+ { accountId, workspaceId, actor },
102
+ { ...request, operation: "restore" },
103
+ );
104
+ }