/* eslint-disable max-classes-per-file -- workspace errors stay next to their contracts. */ import { Effect, Schema } from "effect"; import { CONTEXT_KINDS } from "./project.js"; export type { ContextKind } from "./project.js"; const MeaningfulString = Schema.String.check( Schema.makeFilter((value) => value.trim().length > 0, { expected: "a non-empty string", }) ); const PositiveInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)); const WORKSPACE_PATH = "/workspace"; const CHECKOUT_PATH = "/workspace/repository"; const CONTROL_PATH = "/workspace/control"; const isSafeRelativePath = (value: string): boolean => { if (value.length === 0 || value.startsWith("/") || value.includes("\\")) { return false; } return !value .split("/") .some((segment) => segment === "" || segment === "." || segment === ".."); }; const isValidGitRef = (value: string): boolean => value.length > 0 && !value.startsWith("/") && !value.endsWith("/") && !value.startsWith(".") && !value.endsWith(".") && !value.includes("..") && !value.includes("//") && !value.includes("@{") && ![...value].some((character) => character < " ") && !/[ ~^:?*[\\]/u.test(value); const isPublicGitUrl = (value: string): boolean => { try { const url = new URL(value); return ( (url.protocol === "http:" || url.protocol === "https:") && url.hostname.length > 0 && url.pathname !== "/" && url.username.length === 0 && url.password.length === 0 && url.search.length === 0 && url.hash.length === 0 ); } catch { return false; } }; const shellQuote = (value: string): string => `'${value.replaceAll("'", "'\"'\"'")}'`; const slugIdentity = (value: string): string => { const parts = value .toLowerCase() .split(/[^a-z0-9]+/gu) .filter(Boolean); let result = ""; for (const part of parts) { result = part; } return result.replaceAll(/^-+|-+$/gu, "").slice(-24) || "issue"; }; export const WorkspaceContextFile = Schema.Struct({ content: Schema.String, kind: Schema.Literals([...CONTEXT_KINDS]), path: MeaningfulString, }); export type WorkspaceContextFile = typeof WorkspaceContextFile.Type; export const WorkspaceArtifactFile = Schema.Struct({ content: Schema.String, path: MeaningfulString, }); export type WorkspaceArtifactFile = typeof WorkspaceArtifactFile.Type; export const IssueWorkspaceInput = Schema.Struct({ artifacts: Schema.Array(WorkspaceArtifactFile), branchName: Schema.UndefinedOr(MeaningfulString), checkoutPath: Schema.UndefinedOr(MeaningfulString), contextFiles: Schema.Array(WorkspaceContextFile), defaultBranch: Schema.UndefinedOr(MeaningfulString), issueBody: MeaningfulString, issueId: MeaningfulString, issueNumber: PositiveInteger, issueTitle: MeaningfulString, sourceUrl: Schema.UndefinedOr(MeaningfulString), }); export type IssueWorkspaceInput = typeof IssueWorkspaceInput.Type; export const WorkspaceExecOperation = Schema.Struct({ _tag: Schema.Literal("Exec"), command: MeaningfulString, cwd: MeaningfulString, timeoutMs: PositiveInteger, }); export const WorkspaceMkdirOperation = Schema.Struct({ _tag: Schema.Literal("Mkdir"), path: MeaningfulString, }); export const WorkspaceWriteFileOperation = Schema.Struct({ _tag: Schema.Literal("WriteFile"), content: Schema.String, path: MeaningfulString, }); export const IssueWorkspaceOperation = Schema.Union([ WorkspaceExecOperation, WorkspaceMkdirOperation, WorkspaceWriteFileOperation, ]); export type IssueWorkspaceOperation = typeof IssueWorkspaceOperation.Type; export const IssueWorkspacePlan = Schema.Struct({ artifactsRoot: MeaningfulString, baseBranch: MeaningfulString, branchName: MeaningfulString, checkoutPath: MeaningfulString, contextRoot: MeaningfulString, issuePath: MeaningfulString, operations: Schema.Array(IssueWorkspaceOperation), sourceUrl: MeaningfulString, }); export type IssueWorkspacePlan = typeof IssueWorkspacePlan.Type; export const IssueWorkspaceCommandResult = Schema.Struct({ exitCode: Schema.Int, stderr: Schema.String, stdout: Schema.String, }); export type IssueWorkspaceCommandResult = typeof IssueWorkspaceCommandResult.Type; export const IssueWorkspaceResult = Schema.Struct({ baseBranch: MeaningfulString, branchName: MeaningfulString, checkoutPath: MeaningfulString, headSha: MeaningfulString, sourceUrl: MeaningfulString, }); export type IssueWorkspaceResult = typeof IssueWorkspaceResult.Type; export const WorkspaceValidationReason = Schema.Literals([ "MissingSource", "InvalidInput", "InvalidSourceUrl", "InvalidBranchName", "InvalidPath", "DuplicatePath", ]); export type WorkspaceValidationReason = typeof WorkspaceValidationReason.Type; export class WorkspaceValidationError extends Schema.TaggedErrorClass()( "WorkspaceValidationError", { message: Schema.String, reason: WorkspaceValidationReason, } ) {} export const WorkspaceExecutionReason = Schema.Literals([ "CommandFailed", "InvalidResult", ]); export type WorkspaceExecutionReason = typeof WorkspaceExecutionReason.Type; export class WorkspaceExecutionError extends Schema.TaggedErrorClass()( "WorkspaceExecutionError", { message: Schema.String, reason: WorkspaceExecutionReason, } ) {} export const WorkspaceRunStatus = Schema.Literals([ "ready", "queued", "working", "needs-input", "completed", "failed", ]); export type WorkspaceRunStatus = typeof WorkspaceRunStatus.Type; export class WorkspaceStateError extends Schema.TaggedErrorClass()( "WorkspaceStateError", { from: WorkspaceRunStatus, message: Schema.String, to: WorkspaceRunStatus, } ) {} const validationError = (message: string, reason: WorkspaceValidationReason) => new WorkspaceValidationError({ message, reason }); const requireWorkspaceSource = (sourceUrl: string | undefined) => { if (!sourceUrl || sourceUrl.trim().length === 0) { return Effect.fail( validationError("Project source is not connected", "MissingSource") ); } if (!isPublicGitUrl(sourceUrl)) { return Effect.fail( validationError( "Issue workspace source must be a public http(s) Git URL", "InvalidSourceUrl" ) ); } return Effect.succeed(sourceUrl); }; const validateFiles = ( files: readonly { readonly path: string }[], root: string ): Effect.Effect => Effect.gen(function* validateWorkspaceFiles() { const paths = new Set(); for (const file of files) { if (!isSafeRelativePath(file.path)) { return yield* Effect.fail( validationError( `Invalid workspace file path: ${file.path}`, "InvalidPath" ) ); } const path = `${root}/${file.path}`; if (paths.has(path)) { return yield* Effect.fail( validationError( `Duplicate workspace file path: ${path}`, "DuplicatePath" ) ); } paths.add(path); } }); const makeBranchName = (issueNumber: number, issueId: string): string => `work/issue-${issueNumber}-${slugIdentity(issueId)}`; const makeCheckoutCommand = (input: { readonly baseBranch: string; readonly branchName: string; readonly checkoutPath: string; readonly sourceUrl: string; }): string => { const checkout = shellQuote(input.checkoutPath); const baseBranch = shellQuote(input.baseBranch); const branch = shellQuote(input.branchName); const originBaseBranch = shellQuote(`origin/${input.baseBranch}`); const source = shellQuote(input.sourceUrl); return [ "set -eu", `mkdir -p ${shellQuote(WORKSPACE_PATH)}`, `if [ ! -d ${checkout}/.git ]; then git clone --branch ${baseBranch} --single-branch ${source} ${checkout}; fi`, `if [ "$(git -C ${checkout} branch --show-current)" != ${branch} ]; then git -C ${checkout} show-ref --verify --quiet refs/heads/${branch} && git -C ${checkout} checkout ${branch} || git -C ${checkout} checkout -b ${branch} ${originBaseBranch}; fi`, `printf '%s\\n' "$(git -C ${checkout} branch --show-current)" "$(git -C ${checkout} rev-parse HEAD)"`, ].join(" && "); }; export const makeIssueWorkspacePlan = Effect.fn("ProjectWorkspace.makePlan")( function* makeIssueWorkspacePlan(rawInput: unknown) { const input: IssueWorkspaceInput = yield* Schema.decodeUnknownEffect( IssueWorkspaceInput )(rawInput).pipe( Effect.mapError(() => validationError("Invalid issue workspace input", "InvalidInput") ) ); const sourceUrl = yield* requireWorkspaceSource(input.sourceUrl); const baseBranch = input.defaultBranch?.trim() || "main"; const branchName = input.branchName?.trim() || makeBranchName(input.issueNumber, input.issueId); const checkoutPath = input.checkoutPath?.trim() || CHECKOUT_PATH; if (!isValidGitRef(baseBranch) || !isValidGitRef(branchName)) { return yield* Effect.fail( validationError( "Issue workspace branch name is not a valid Git ref", "InvalidBranchName" ) ); } if ( checkoutPath !== CHECKOUT_PATH || !checkoutPath.startsWith(`${WORKSPACE_PATH}/`) || checkoutPath.includes("..") ) { return yield* Effect.fail( validationError( "Issue workspace checkout path must be /workspace/repository", "InvalidPath" ) ); } const contextRoot = `${CONTROL_PATH}/context`; const artifactsRoot = `${CONTROL_PATH}/artifacts`; yield* validateFiles(input.contextFiles, contextRoot); yield* validateFiles(input.artifacts, artifactsRoot); const operations: IssueWorkspaceOperation[] = [ { _tag: "Mkdir", path: CONTROL_PATH }, { _tag: "Exec", command: makeCheckoutCommand({ baseBranch, branchName, checkoutPath, sourceUrl, }), cwd: WORKSPACE_PATH, timeoutMs: 300_000, }, { _tag: "Mkdir", path: contextRoot }, { _tag: "Mkdir", path: artifactsRoot }, { _tag: "WriteFile", content: `# Issue ${input.issueNumber}: ${input.issueTitle}\n\n${input.issueBody}\n`, path: `${CONTROL_PATH}/issue.md`, }, ...input.contextFiles.map((file) => ({ _tag: "WriteFile" as const, content: file.content, path: `${contextRoot}/${file.path}`, })), ...input.artifacts.map((file) => ({ _tag: "WriteFile" as const, content: file.content, path: `${artifactsRoot}/${file.path}`, })), ]; return { artifactsRoot, baseBranch, branchName, checkoutPath, contextRoot, issuePath: `${CONTROL_PATH}/issue.md`, operations, sourceUrl, }; } ); export const decodeIssueWorkspaceResult = Effect.fn( "ProjectWorkspace.decodeResult" )(function* decodeIssueWorkspaceResult(input: { readonly command: IssueWorkspaceCommandResult; readonly plan: IssueWorkspacePlan; }) { if (input.command.exitCode !== 0) { return yield* Effect.fail( new WorkspaceExecutionError({ message: input.command.stderr || "Issue workspace checkout command failed", reason: "CommandFailed", }) ); } const [branchName, headSha] = input.command.stdout.trim().split(/\s+/u); if ( branchName !== input.plan.branchName || !headSha || !/^[0-9a-f]{7,64}$/u.test(headSha) ) { return yield* Effect.fail( new WorkspaceExecutionError({ message: "Issue workspace checkout command returned invalid branch metadata", reason: "InvalidResult", }) ); } return { baseBranch: input.plan.baseBranch, branchName, checkoutPath: input.plan.checkoutPath, headSha, sourceUrl: input.plan.sourceUrl, }; }); const allowedTransitions: Readonly< Record > = { completed: [], failed: [], "needs-input": ["queued", "working", "failed"], queued: ["working", "completed", "failed"], ready: ["queued", "working", "failed"], working: ["needs-input", "completed", "failed"], }; export const transitionWorkspaceRun = Effect.fn( "ProjectWorkspace.transitionRun" )(function* transitionWorkspaceRun(input: { readonly from: WorkspaceRunStatus; readonly to: WorkspaceRunStatus; }) { if ( input.from === input.to || allowedTransitions[input.from].includes(input.to) ) { return input.to; } return yield* Effect.fail( new WorkspaceStateError({ from: input.from, message: `Cannot transition issue workspace run from ${input.from} to ${input.to}`, to: input.to, }) ); });