import { randomUUID } from "node:crypto"; import { stableJsonDigest } from "../shared/launch-contract.ts"; import { createWorkflowResourcePermit, type WorkflowResourceAuthority, type WorkflowResourcePermit, } from "../shared/workflow-child-permit.ts"; import type { WorkflowResourceProvenanceV1 } from "../shared/types.ts"; const RESOURCE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; const MAX_ARGS_BYTES = 16 * 1024; const MAX_STRING_BYTES = 16 * 1024; export interface ResolvedWorkflowResource { script: string; permit: WorkflowResourcePermit; provenance: WorkflowResourceProvenanceV1; } export type WorkflowResourceResolution = | { ok: true; resource: ResolvedWorkflowResource } | { ok: false; error: string }; type WorkflowResourceDefinition = { name: string; version: number; resolve: (args: Record) => { script: string; authority: WorkflowResourceAuthority } | { error: string }; }; function isPlainRecord(value: unknown): value is Record { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function jsonByteLength(value: unknown): number { try { const encoded = JSON.stringify(value); if (encoded === undefined) throw new Error("must contain JSON data"); return Buffer.byteLength(encoded, "utf8"); } catch (error) { throw new Error(`must contain plain JSON data: ${error instanceof Error ? error.message : String(error)}`); } } function validatePlainJson(value: unknown, path: string, depth = 0): void { if (depth > 8) throw new Error(`${path} is too deeply nested.`); if (value === null || typeof value === "boolean") return; if (typeof value === "string") { if (!value.trim()) throw new Error(`${path} must not be empty.`); if (Buffer.byteLength(value, "utf8") > MAX_STRING_BYTES) throw new Error(`${path} exceeds ${MAX_STRING_BYTES} bytes.`); return; } if (typeof value === "number") { if (!Number.isFinite(value)) throw new Error(`${path} must be finite.`); return; } if (Array.isArray(value)) { if (value.length > 64) throw new Error(`${path} contains too many items.`); for (const [index, entry] of value.entries()) validatePlainJson(entry, `${path}[${index}]`, depth + 1); return; } if (!isPlainRecord(value)) throw new Error(`${path} must contain plain JSON data.`); if (Object.keys(value).length > 16) throw new Error(`${path} contains too many fields.`); for (const [key, entry] of Object.entries(value)) { if (!key.trim()) throw new Error(`${path} contains an empty field name.`); validatePlainJson(entry, `${path}.${key}`, depth + 1); } } function normalizeArgs(value: unknown): { args?: Record; error?: string } { if (value === undefined) return { args: {} }; if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." }; try { validatePlainJson(value, "workflow args"); if (jsonByteLength(value) > MAX_ARGS_BYTES) return { error: `workflow args exceed ${MAX_ARGS_BYTES} bytes.` }; return { args: { ...value } }; } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } } function resolveRunCi(args: Record): { script: string; authority: WorkflowResourceAuthority } | { error: string } { const allowed = new Set(["command", "timeoutMs"]); const unsupported = Object.keys(args).filter((key) => !allowed.has(key)); if (unsupported.length > 0) return { error: `workflow 'run-ci' args contain unsupported fields: ${unsupported.join(", ")}.` }; const command = args.command === undefined ? "npm test" : args.command; if (command !== "npm test" && command !== "npm run typecheck") return { error: "workflow 'run-ci' args.command must be 'npm test' or 'npm run typecheck'." }; const timeoutMs = args.timeoutMs === undefined ? 120_000 : args.timeoutMs; if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86_400_000) return { error: "workflow 'run-ci' args.timeoutMs must be an integer from 1 to 86400000." }; const params = { kind: "command", command, timeoutMs, role: "ci" }; return { script: `return await runs.host("ci", ${JSON.stringify(params)});`, authority: { host: { keys: ["ci"], commands: [command] } }, }; } function resolveReview(args: Record): { script: string; authority: WorkflowResourceAuthority } | { error: string } { const unsupported = Object.keys(args).filter((key) => key !== "task"); if (unsupported.length > 0) return { error: `workflow 'review' args contain unsupported fields: ${unsupported.join(", ")}.` }; const task = args.task; if (typeof task !== "string" || !task.trim()) return { error: "workflow 'review' requires a non-empty string args.task." }; return { script: `return (await runs.run("review", { agent: "reviewer", task: ${JSON.stringify(task.trim())} })).output;`, authority: {}, }; } const WORKFLOW_RESOURCES: readonly WorkflowResourceDefinition[] = [ { name: "review", version: 1, resolve: resolveReview }, { name: "run-ci", version: 1, resolve: resolveRunCi }, ]; function findWorkflowResource(name: string): WorkflowResourceDefinition | undefined { return WORKFLOW_RESOURCES.find((resource) => resource.name === name); } function listWorkflowResourceNames(): string[] { return WORKFLOW_RESOURCES.map((resource) => resource.name); } /** Resolve only extension-owned resources so policy can distinguish them from raw scripts; caller-provided script text is never consulted. */ export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown): WorkflowResourceResolution { if (typeof nameValue !== "string" || !nameValue.trim()) return { ok: false, error: "workflow must be a non-empty resource name." }; const name = nameValue.trim(); if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." }; const resource = findWorkflowResource(name); if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` }; const normalizedArgs = normalizeArgs(argsValue); if (normalizedArgs.error) return { ok: false, error: normalizedArgs.error }; const resolved = resource.resolve(normalizedArgs.args!); if ("error" in resolved) return { ok: false, error: resolved.error }; const resourceId = randomUUID(); const permit = createWorkflowResourcePermit({ resourceName: resource.name, resourceVersion: resource.version, resourceId, scriptDigest: stableJsonDigest(resolved.script), authority: resolved.authority, }); const provenance: WorkflowResourceProvenanceV1 = Object.freeze({ kind: "workflow", name: resource.name, version: resource.version, invocation: "named", expansion: "resolved", id: resourceId, }); return { ok: true, resource: { script: resolved.script, permit, provenance } }; }