@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.
- package/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18812 -6833
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- 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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
+
}
|