Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338import { runner, type SkillContent, type SkillResource, type SkillWorkspace,} from "agents/skills";import type { ToolSet } from "ai";import { skillScriptResult, type SkillScriptResult,} from "../shared/skill-execution";import { validateSkillPath } from "./skill-package";
export const SKILL_SCRIPT_TIMEOUT_MS = 20_000;export const SKILL_SCRIPT_CPU_MS = 1_000;export const SKILL_SCRIPT_SUBREQUESTS = 32;export const SKILL_SCRIPT_MAX_RESULT_BYTES = 49_152;
export interface RunSkillScriptOptions { loader: WorkerLoader; skill: SkillContent; path: string; source: string; resources: SkillResource[]; input: unknown; signal: AbortSignal; workspace?: SkillWorkspace; workspaceAccess: "none" | "read" | "read-write"; tools?: ToolSet;}const failure = ( code: NonNullable<SkillScriptResult["error"]>["code"], message: string,): SkillScriptResult => ({ ok: false, result: null, stdout: "", stderr: "", exitCode: 1, truncated: false, error: { code, message },});
// Native runner source is trusted glue only. The separate user module cannot// reach the executor's lexical __host/__logs or its scratch-output bridge.// Return serialized bounded data so getters/prototypes never cross the RPC.const wrapper = String.raw`export default async function run(input, native) { const stringify = JSON.stringify.bind(JSON); const parse = JSON.parse.bind(JSON); const ownKeys = Reflect.ownKeys.bind(Reflect); const descriptor = Object.getOwnPropertyDescriptor.bind(Object); const prototype = Object.getPrototypeOf.bind(Object); const setPrototype = Object.setPrototypeOf.bind(Object); const hasOwn = Function.call.bind(Object.prototype.hasOwnProperty); const freeze = Object.freeze.bind(Object); const isArray = Array.isArray.bind(Array); const finite = Number.isFinite.bind(Number); const objectPrototype = Object.prototype, arrayPrototype = Array.prototype; const invalid = () => { throw new Error("Invalid or oversized Skill host arguments"); }; const bridgeChecks = []; function watch(owner, key) { const original = descriptor(owner, key); bridgeChecks[bridgeChecks.length] = {owner, key, original, parent:prototype(owner)}; } // The pinned native codec performs a second serialization with these globals. // Refuse host calls after mutation; output-only scripts may still change them. for (const name of ["JSON", "Object", "String", "Uint8Array", "ArrayBuffer"]) watch(globalThis, name); for (const name of ["stringify", "parse"]) watch(JSON, name); watch(Object, "prototype"); watch(Object.prototype, "hasOwnProperty"); watch(Object.prototype.hasOwnProperty, "call"); watch(Function.prototype, "call"); watch(Object.prototype, "toJSON"); watch(Array.prototype, "toJSON"); watch(ArrayBuffer, "isView"); for (const constructor of [Uint8Array, ArrayBuffer]) { watch(constructor, "prototype"); for (let current = constructor; current !== null; current = prototype(current)) watch(current, Symbol.hasInstance); } function bridgeIntact() { for (let i = 0; i < bridgeChecks.length; i++) { const check = bridgeChecks[i], current = descriptor(check.owner, check.key); if (prototype(check.owner) !== check.parent) invalid(); if (!check.original) { if (current !== undefined) invalid(); } else if (!current || !hasOwn(current, "value") || current.value !== check.original.value) invalid(); } } const create = Object.create.bind(Object); const encode = new TextEncoder().encode.bind(new TextEncoder()); const slice = Function.call.bind(String.prototype.slice); const charCode = Function.call.bind(String.prototype.charCodeAt); const split = Function.call.bind(String.prototype.split); const test = Function.call.bind(RegExp.prototype.test); const string = String; const ceil = Math.ceil; let stdout = "", stderr = "", truncated = false; function bytes(value) { return encode(stringify(value)).byteLength; } function clip(value, budget) { if (bytes(value) <= budget) return value; truncated = true; let low = 0, high = value.length; while (low < high) { const middle = ceil((low + high) / 2); if (bytes(slice(value, 0, middle)) <= budget) low = middle; else high = middle - 1; } if (low > 0 && charCode(value, low - 1) >= 0xd800 && charCode(value, low - 1) <= 0xdbff) low--; return slice(value, 0, low); } function write(error, args) { let text = ""; for (let i = 0; i < args.length; i++) { let part; try { part = typeof args[i] === "string" ? args[i] : stringify(args[i]) ?? string(args[i]); } catch { part = "[unserializable]"; } text = clip(text + (i ? " " : "") + part, 8192); } if (error) stderr = clip(stderr + text + "\n", 8192); else stdout = clip(stdout + text + "\n", 8192); } const forbidden = create(null); for (const key of ["__proto__", "prototype", "constructor", "toJSON", "__codemode_binary_v1__"]) forbidden[key] = true; function plain(value, depth, count) { if (++count.nodes > 4096 || depth > 32) invalid(); if (value === null || typeof value === "boolean") return value; if (typeof value === "number") { if (!finite(value)) invalid(); return value; } if (typeof value === "string") { if (value.length > 32768) invalid(); return value; } if (typeof value !== "object") invalid(); const array = isArray(value), parent = prototype(value); if (parent !== null && parent !== (array ? arrayPrototype : objectPrototype)) invalid(); const keys = ownKeys(value); if (keys.length > 4097 || (array && value.length > 4096)) invalid(); const result = array ? setPrototype([], null) : create(null); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (array && key === "length") continue; if (typeof key !== "string" || key.length > 32768 || forbidden[key]) invalid(); if (array && !test(/^(0|[1-9][0-9]*)$/, key)) invalid(); const property = descriptor(value, key); if (!property || !hasOwn(property, "value") || !property.enumerable) invalid(); result[key] = plain(property.value, depth + 1, count); } if (array && result.length !== value.length) invalid(); return result; } function argumentsFor(value) { bridgeIntact(); const clean = plain(value, 0, {nodes:0}); const serialized = stringify(clean); if (encode(serialized).byteLength > 32768) invalid(); const parsed = parse(serialized); bridgeIntact(); return parsed; } function path(value, root = false, glob = false) { if (typeof value !== "string" || value.length > 240) invalid(); if (root && (value === "." || value === "")) return value; const segments = split(value, "/"); for (let i = 0; i < segments.length; i++) if (!test(/^[A-Za-z0-9_*-][A-Za-z0-9._*-]*$/, segments[i])) invalid(); let stars = 0; for (let i = 0; i < value.length; i++) if (value[i] === "*") stars++; if ((!glob && stars) || stars > 1) invalid(); return value; } const workspace = create(null); workspace.readFile = value => native.workspace.readFile(argumentsFor([path(value)])[0]); workspace.listFiles = (value = ".") => native.workspace.listFiles(argumentsFor([path(value, true)])[0]); workspace.glob = value => native.workspace.glob(argumentsFor([path(value, false, true)])[0]); workspace.stat = value => native.workspace.stat(argumentsFor([path(value, true)])[0]); workspace.writeFile = (name, content) => { path(name); if (typeof content !== "string" || content.length > 16384 || encode(content).byteLength > 16384) invalid(); const clean = argumentsFor([{path:name, content}])[0]; return native.workspace.writeFile(clean.path, clean.content); }; const allowed = create(null), tools = create(null); const allowedNames = __SKILL_ALLOWED_TOOLS__; for (let i = 0; i < allowedNames.length; i++) { const name = allowedNames[i]; if (typeof name !== "string" || name.length > 200 || !test(/^[A-Za-z_$][A-Za-z0-9_$]*$/, name) || forbidden[name] || name === "call") invalid(); allowed[name] = true; } const call = (name, value) => { if (typeof name !== "string" || allowed[name] !== true || !value || typeof value !== "object" || isArray(value)) invalid(); const clean = argumentsFor([{name, input:value}])[0]; return native.tools.call(clean.name, clean.input); }; tools.call = call; for (let i = 0; i < allowedNames.length; i++) { const name = allowedNames[i]; tools[name] = value => call(name, value); } freeze(workspace); freeze(tools); const capturedConsole = create(null); capturedConsole.log = capturedConsole.info = capturedConsole.debug = (...args) => write(false, args); capturedConsole.error = capturedConsole.warn = capturedConsole.trace = (...args) => write(true, args); capturedConsole.assert = (condition, ...args) => { if (!condition) write(true, args); }; Object.defineProperty(globalThis, "console", {value: capturedConsole, writable: false, configurable: false}); const finish = (ok, result, code, message) => { const output = create(null); output.ok = ok; output.result = result; output.stdout = stdout; output.stderr = stderr; output.exitCode = ok ? 0 : 1; output.truncated = truncated; if (code) { const error = create(null); error.code = code; error.message = message; output.error = error; } return stringify(output); }; try { const user = await import("skill-entry.js"); if (typeof user.default !== "function") return finish(false, null, "script_failed", "Skill script must export a default function."); const output = create(null); output.writeFile = () => { throw new Error("Script output files are unavailable"); }; const context = create(null); context.skill = native.skill; context.files = native.files; context.workspace = workspace; context.tools = tools; context.output = output; const result = await user.default(input, context); const serialized = stringify(result ?? null); if (serialized === undefined) return finish(false, null, "script_failed", "Skill script returned an unsupported value."); if (encode(serialized).byteLength > 24576) { truncated = true; return finish(false, null, "output_limit", "Skill script result exceeds the output limit."); } // Insert already-serialized user data once; avoid running a user getter or // toJSON method twice when creating the result envelope. return '{"ok":true,"result":' + serialized + ',"stdout":' + stringify(stdout) + ',"stderr":' + stringify(stderr) + ',"exitCode":0,"truncated":' + (truncated ? "true" : "false") + '}'; } catch { return finish(false, null, "script_failed", "Skill script failed. Check its source and granted permissions."); }}`;
export async function runSkillScript( options: RunSkillScriptOptions,): Promise<SkillScriptResult> { if (options.signal.aborted) return failure("cancelled", "Skill script was cancelled."); try { validateSkillPath(options.path, "path"); if (!options.path.startsWith("scripts/")) return failure( "unsupported_runtime", "Choose a JavaScript file under scripts/.", ); if (!/\.(js|mjs)$/.test(options.path)) return failure( "unsupported_runtime", "This installation runs only JavaScript .js and .mjs Skill scripts.", ); if ( new TextEncoder().encode(JSON.stringify(options.input)).byteLength > 32_768 ) return failure("invalid_input", "Skill script input exceeds 32 KiB."); } catch { return failure("invalid_input", "Skill script path or input is invalid."); } const inject = (code: WorkerLoaderWorkerCode): WorkerLoaderWorkerCode => ({ ...code, modules: { ...code.modules, "skill-entry.js": options.source }, env: undefined, globalOutbound: null, limits: { cpuMs: SKILL_SCRIPT_CPU_MS, subRequests: SKILL_SCRIPT_SUBREQUESTS, }, }); const loader: WorkerLoader = { load: (code) => options.loader.load(inject(code)), get: (name, code) => options.loader.get(name, async () => inject(await code())), }; let timeout: ReturnType<typeof setTimeout> | undefined; let abort: () => void = () => {}; const stopped = new Promise<SkillScriptResult>((resolve) => { abort = () => resolve(failure("cancelled", "Skill script was cancelled.")); options.signal.addEventListener("abort", abort, { once: true }); timeout = setTimeout( () => resolve( failure("timeout", "Skill script exceeded its execution deadline."), ), SKILL_SCRIPT_TIMEOUT_MS, ); }); try { options.signal.throwIfAborted(); const execution = runner({ loader, timeout: SKILL_SCRIPT_TIMEOUT_MS, network: false, workspace: options.workspaceAccess, workspaceInstance: options.workspace, tools: options.tools ?? {}, }) .run({ skill: options.skill, path: options.path, source: wrapper.replace( "__SKILL_ALLOWED_TOOLS__", JSON.stringify(Object.keys(options.tools ?? {})), ), input: options.input, resources: options.resources.filter( (resource) => resource.kind !== "script" && resource.path !== "permissions.json", ), }) .then((value) => { if ( typeof value !== "string" || new TextEncoder().encode(value).byteLength > SKILL_SCRIPT_MAX_RESULT_BYTES ) return failure( "output_limit", "Skill script result exceeds the output limit.", ); const parsed = skillScriptResult.safeParse(JSON.parse(value)); return parsed.success ? parsed.data : failure( "script_failed", "Skill script returned an invalid result.", ); }) .catch(() => failure( "script_failed", "Skill script failed. Check its source and granted permissions.", ), ); return await Promise.race([execution, stopped]); } catch { return failure( options.signal.aborted ? "cancelled" : "script_failed", "Skill script could not complete.", ); } finally { if (timeout !== undefined) clearTimeout(timeout); options.signal.removeEventListener("abort", abort); }}