import type { ExtensionDiagnosticInput, ExtensionDiagnosticSink, } from "../shared/extension-diagnostics"; import { emitExtensionDiagnostic } from "./extension-diagnostics"; import { createFetchTools, type FetchResult, } from "@cloudflare/think/tools/fetch"; import * as Effect from "effect/Effect"; import { z } from "zod"; import { bodyJson } from "../server/http"; import type { SkillReview } from "../shared/skill-review"; import { skillOrigin, type SkillOrigin } from "../shared/skills"; import { runAgent } from "./agent-io"; import { InstalledSkills, MAX_SKILL_OBJECT_BYTES } from "./installed-skills"; import { OperationFailure } from "./operation-result"; import { SkillPackageError, validateSkillPackage, type ValidatedSkillPackage, } from "./skill-package"; import { publicWebUrl } from "./web-source"; import { downloadGitHubSkill, githubSkillFolder, GitHubSkillError, } from "./github-skill"; const REVIEW_LIFETIME_MS = 5 * 60_000; const fetchPackage = createFetchTools({ allowlist: ["https://**"], maxBytes: MAX_SKILL_OBJECT_BYTES, maxModelChars: MAX_SKILL_OBJECT_BYTES, timeoutMs: 15_000, response: "text", spillToWorkspace: false, followRedirects: "none", modelHeaderAllowlist: [], defaultAccept: "application/json", }).fetch_url; class ReviewFailure extends Error { constructor( readonly code: string, message: string, readonly status = 400, ) { super(message); } } function diagnosticFailure( error: unknown, ): ExtensionDiagnosticInput["failure"] { if (error instanceof SkillPackageError) return "validation"; if (error instanceof OperationFailure) return "unavailable"; if (!(error instanceof ReviewFailure)) return "storage"; switch (error.code) { case "timeout": return "timeout"; case "download_failed": return "connection"; case "storage_unavailable": return "storage"; case "unavailable": case "review_expired": return "unavailable"; default: return "validation"; } } const unreadable = () => new ReviewFailure( "invalid_package", "Choose a valid format-1 JSON Skill package of at most 13 MiB.", ); const expired = () => new ReviewFailure( "review_expired", "This review expired or was replaced. Review the package again.", 409, ); export class SkillInstaller { private pending?: { review: SkillReview; package: ValidatedSkillPackage; timer: ReturnType; release: () => void; }; private generation = 0; constructor( private readonly store: InstalledSkills | undefined, private readonly reservedNames: () => ReadonlySet, private readonly lifetime: { keepAlive: () => Promise<() => void>; now?: () => number; }, private readonly diagnostics?: ExtensionDiagnosticSink, ) {} private now() { return this.lifetime.now?.() ?? Date.now(); } private clearReview() { const pending = this.pending; this.pending = undefined; if (pending) { clearTimeout(pending.timer); pending.release(); } } private async review( value: unknown, origin: SkillOrigin, generation: number, signal: AbortSignal, ) { if (generation !== this.generation) throw expired(); const skill = validateSkillPackage(value, origin, this.reservedNames()); const previous = this.store!.list().find( (item) => item.name === skill.name, ); const { files: _files, entry, ...metadata } = skill; const review: SkillReview = { ...metadata, reviewId: crypto.randomUUID(), expiresAt: new Date(this.now() + REVIEW_LIFETIME_MS).toISOString(), instructions: entry.body, license: entry.license, compatibility: entry.compatibility, metadata: entry.metadata, allowedTools: entry.allowedTools ?? null, resources: (entry.resources ?? []).map((resource) => ({ path: resource.path, kind: resource.kind, size: resource.size!, })), replaces: previous ? { version: previous.version, revision: previous.revision } : null, }; const release = await this.lifetime.keepAlive().catch(() => { throw new ReviewFailure( "unavailable", "Could not keep the Skill review available. Try again.", 503, ); }); try { signal.throwIfAborted(); const remaining = Date.parse(review.expiresAt) - this.now(); if (generation !== this.generation || remaining <= 0) throw expired(); // The timer prevents hibernation; native Agent alarm heartbeats prevent // idle eviction. Both end with this one bounded, memory-only review. const timer = setTimeout(() => { if (this.pending?.review.reviewId === review.reviewId) this.clearReview(); }, remaining); this.pending = { review, package: skill, timer, release }; return review; } catch (error) { release(); throw error; } } private fromUrl(value: unknown) { return Effect.gen(function* () { const requested = yield* Effect.try({ try: () => { const { url } = z .strictObject({ url: z.url().max(2048) }) .parse(value); skillOrigin.parse({ kind: "url", url }); return publicWebUrl(url).href; }, catch: () => new ReviewFailure( "invalid_url", "Use a public GitHub folder or direct HTTPS package URL without embedded credentials.", ), }); const folder = yield* Effect.try({ try: () => githubSkillFolder(requested), catch: (error) => new ReviewFailure( "invalid_url", error instanceof GitHubSkillError ? error.message : "Use a valid public GitHub folder URL.", ), }); if (folder) return yield* downloadGitHubSkill(folder).pipe( Effect.mapError( (error) => new ReviewFailure(error.code, error.message, error.status), ), ); const result = yield* Effect.tryPromise({ try: (signal) => Promise.resolve( fetchPackage.execute!( { url: requested }, { toolCallId: "skill-package-review", messages: [], context: {}, abortSignal: signal, }, ), ) as Promise, catch: () => new ReviewFailure( "download_failed", "Could not download the Skill package. Try again.", 502, ), }); if (!result.ok || result.truncated || !result.body) return yield* Effect.fail( new ReviewFailure( "download_failed", "Could not download a complete package. Use a direct HTTPS URL without redirects, at most 13 MiB.", 502, ), ); const downloaded = yield* Effect.try({ try: (): unknown => JSON.parse(result.body!), catch: unreadable, }); return { value: downloaded, origin: { kind: "url" as const, url: requested }, }; }); } request(request: Request): Promise { const path = new URL(request.url).pathname; const startedAt = Date.now(); let provenance: Pick< ExtensionDiagnosticInput, "source" | "version" | "fingerprint" > = {}; let reported = false; const report = ( status: ExtensionDiagnosticInput["status"], failure?: ExtensionDiagnosticInput["failure"], ) => { if ( reported || request.method !== "POST" || ![ "/api/skills/install", "/api/skills/review-upload", "/api/skills/review-url", ].includes(path) ) return; reported = true; emitExtensionDiagnostic(this.diagnostics, { operation: path === "/api/skills/install" ? "skill-install" : "skill-validation", startedAt, status, ...provenance, ...(failure ? { failure } : {}), }); }; const remember = ( skill: Pick, ) => { provenance = { source: { kind: "skill", id: `installed:${skill.name}` }, version: skill.version, fingerprint: skill.fingerprint, }; }; const response = (value: unknown, status = 200) => Response.json(value, { status, headers: { "Cache-Control": "no-store" }, }); return runAgent( Effect.gen({ self: this }, function* () { if (!this.store) return yield* Effect.fail( new ReviewFailure( "storage_unavailable", "Skill storage is unavailable for this installation.", 503, ), ); if (request.method !== "POST") return response( { error: { code: "method_not_allowed", path: "", message: "Use POST for Skill review and installation.", }, }, 405, ); if (path === "/api/skills/install") { const input = yield* bodyJson( new Response(request.body, { headers: request.headers }), 1024, unreadable(), ); const { reviewId } = yield* Effect.try({ try: () => z.strictObject({ reviewId: z.uuid() }).parse(input), catch: unreadable, }); const pending = this.pending; if (pending?.review.reviewId === reviewId) remember(pending.review); if (pending && Date.parse(pending.review.expiresAt) <= this.now()) this.clearReview(); if ( !pending || this.pending !== pending || pending.review.reviewId !== reviewId ) return yield* Effect.fail(expired()); this.clearReview(); this.generation++; const installed = yield* this.store.install( { formatVersion: 1, files: pending.package.files }, pending.package.origin, pending.review.replaces?.revision ?? null, ); report("completed"); return response(installed, 201); } if ( path !== "/api/skills/review-upload" && path !== "/api/skills/review-url" ) return response( { error: { code: "not_found", path: "", message: "Skill route not found.", }, }, 404, ); const generation = ++this.generation; this.clearReview(); const value = yield* bodyJson( new Response(request.body, { headers: request.headers }), path.endsWith("review-url") ? 4096 : MAX_SKILL_OBJECT_BYTES, unreadable(), ); const prepared = path.endsWith("review-url") ? yield* this.fromUrl(value) : { value, origin: yield* Effect.try({ try: () => skillOrigin.parse({ kind: "upload", filename: decodeURIComponent( request.headers.get("X-Filename") ?? "package.skill.json", ), }), catch: () => new ReviewFailure( "invalid_filename", "Use an uploaded filename without directories or control characters.", ), }), }; const review = yield* Effect.tryPromise({ try: (signal) => this.review(prepared.value, prepared.origin, generation, signal), catch: (error) => error instanceof SkillPackageError || error instanceof ReviewFailure ? error : unreadable(), }); remember(review); report("completed"); return response(review); }).pipe( Effect.timeoutOrElse({ duration: 25_000, orElse: () => Effect.fail( new ReviewFailure( "timeout", "Skill operation timed out. Reload Skills before trying again.", 504, ), ), }), Effect.catch((error) => { report("error", diagnosticFailure(error)); const known = error instanceof ReviewFailure || error instanceof SkillPackageError || error instanceof OperationFailure; return Effect.succeed( response( { error: { code: known ? error.code : "storage_failed", path: error instanceof SkillPackageError ? error.path : "", message: known ? error.message : "Skill storage could not complete the operation. Reload Skills and try again.", }, }, error instanceof ReviewFailure ? error.status : error instanceof OperationFailure ? 409 : error instanceof SkillPackageError ? 400 : 502, ), ); }), Effect.catchDefect(() => { report("error", "unknown"); return Effect.succeed( response( { error: { code: "unavailable", path: "", message: "Skill operation unavailable. Try again.", }, }, 500, ), ); }), Effect.ensuring( Effect.sync(() => { if (!reported) report( request.signal.aborted ? "aborted" : "error", request.signal.aborted ? "cancelled" : "unknown", ); }), ), ), request.signal, ); } }