AkurAI Build
Menu

popagent

public

Latest change c5300493b2cdef9ff6d10de92b8a15067eac3e8b - Permit autonomous skill instruction reads by AkurAI Build

import { createHash } from "node:crypto";
import { execFile, spawn } from "node:child_process";
import { access, mkdir, rm, symlink } from "node:fs/promises";
import { isAbsolute, join, posix, relative, resolve } from "node:path";
import { agentSettings, type AgentSettingsStore } from "./agent-settings";
import { agentWorkspaces, type AgentWorkspaceStore } from "./agent-workspaces";
import type { AgentSettings, AgentTask, AgentTaskSource } from "./api-types";
import { autonomySettings, type AutonomySettingsStore } from "./autonomy-settings";
import { agentWorkspaceRuntime } from "./workspace";
import { appLogger } from "./observability";

const SELF_UPDATE_BRANCH_PREFIX = "autonomous/self-update/";
const BUILD_MAINTENANCE_BRANCH_PREFIX = "autonomous/build-maintenance/";
const ZERO_OBJECT_ID = "0".repeat(40);
export const SELF_UPDATE_REVIEW_DIFF_MAX_BYTES = 64_000;
const SELF_UPDATE_COMMAND_OUTPUT_MAX_BYTES = 16_000;

const PROTECTED_PATHS: Record<string, true> = {
  ".agents": true,
  ".akurai.yml": true,
  ".circleci": true,
  ".env": true,
  ".github": true,
  ".gitlab-ci.yml": true,
  ".gitattributes": true,
  ".gitmodules": true,
  ".gitignore": true,
  ".env.example": true,
  "bun.lock": true,
  "bunfig.toml": true,
  "deploy.sh": true,
  "docker-compose.yml": true,
  "package.json": true,
  "src/agent-context.ts": true,
  "src/agent-execution.ts": true,
  "src/agent-role-seed.sql": true,
  "src/agent-runtime-settings.sql": true,
  "src/agent-runtime-settings.ts": true,
  "src/agent-settings.ts": true,
  "src/agent.ts": true,
  "src/api-types.ts": true,
  "src/agent-skills.ts": true,
  "src/autonomy-settings.ts": true,
  "src/auth.ts": true,
  "src/browser-policy.ts": true,
  "src/browser-recordings.ts": true,
  "src/browser-sessions.ts": true,
  "src/browser-settings.sql": true,
  "src/browser-settings.ts": true,
  "src/browser-profiles.ts": true,
  "src/evolution-runtime.ts": true,
  "src/evolution-store.ts": true,
  "src/evolution.sql": true,
  "src/hook-lifecycle.ts": true,
  "src/hooks.ts": true,
  "src/models.ts": true,
  "src/secret-bindings.sql": true,
  "src/secrets.ts": true,
  "src/self-update-policy.ts": true,
  "src/self-update-scheduler.ts": true,
  "src/self-update-workflow.ts": true,
  "src/server.ts": true,
  "src/sessions.ts": true,
  "src/tasks.ts": true,
  "src/tools/akurai-build.ts": true,
  "src/tools/bifrost.ts": true,
  "src/tools/secrets.ts": true,
  "src/tools/memory.ts": true,
  "src/workspace.ts": true,
  "tsconfig.json": true,
};
const PROTECTED_PREFIXES = [
  ".agents/",
  ".circleci/",
  ".git/",
  ".github/",
  ".gitlab/",
  "systemd/",
  "src/agent-",
  "src/browser-",
  "src/evolution-",
  "src/settings-",
  "src/tools/",
  "src/server/",
  "src/deploy/",
];
const CREDENTIAL_BASENAMES: Record<string, true> = {
  ".netrc": true,
  ".npmrc": true,
  ".pypirc": true,
  "auth.json": true,
  credentials: true,
  "credentials.json": true,
  id_dsa: true,
  id_ed25519: true,
  id_rsa: true,
  "service-account.json": true,
};

export type GitSnapshot = {
  head: string;
  branch: string;
  remotes: string;
};
export type SelfUpdateRun = {
  taskId: string;
  workspaceId: string;
  repositoryPath: string;
  path: string;
  branch: string;
  snapshot: GitSnapshot;
  cloneRemotes: string;
  delegations: Set<string>;
  reviewedDiffDigest?: string;
  deploymentEnabled: boolean;
  source?: Extract<AgentTaskSource, "self-update" | "build-maintenance">;
};

type RoleRepository = Pick<AgentSettingsStore, "get">;
type WorkspaceRepository = Pick<AgentWorkspaceStore, "resolveRepository">;
type AutonomyRepository = Pick<AutonomySettingsStore, "get">;

export class SelfUpdatePolicyError extends Error {
  override readonly name = "SelfUpdatePolicyError";
}

function protectedPath(path: string): boolean {
  const slashPath = path.replaceAll("\\", "/");
  if (isAbsolute(path) || slashPath.startsWith("/")) return true;
  const normalized = posix.normalize(slashPath).replace(/^\.\//, "");
  if (normalized === ".." || normalized.startsWith("../")) return true;
  const lower = normalized.toLowerCase();
  const base = posix.basename(lower);
  return lower === ".git"
    || lower.startsWith(".env.")
    || (base.startsWith("agents") && base.endsWith(".md"))
    || Boolean(PROTECTED_PATHS[lower])
    || PROTECTED_PREFIXES.some((prefix) => lower.startsWith(prefix))
    || /(^|\/)(?:\.aws|\.ssh|credentials?|secrets?|tokens?)(?:[./_-]|$)/.test(lower)
    || /\.(?:kdbx|key|p12|pfx|pem)$/.test(lower)
    || Boolean(CREDENTIAL_BASENAMES[base]);
}

export function assertSelfUpdatePathAllowed(path: string): void {
  if (protectedPath(path)) {
    throw new SelfUpdatePolicyError("Self-update containment denied a protected path");
  }
}

export function normalizeSelfUpdateToolInput(input: unknown, workspacePath: string): void {
  if (!input || typeof input !== "object" || Array.isArray(input)) return;
  const values = input as Record<string, unknown>;
  for (const key of ["path", "cwd"] as const) {
    const value = values[key];
    if (typeof value !== "string" || !isAbsolute(value)) continue;
    const contained = relative(workspacePath, value);
    if (contained === ".." || contained.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(contained)) {
      throw new SelfUpdatePolicyError("Self-update containment denied a protected path");
    }
    values[key] = contained || ".";
  }
}

function assertGitCommand(tokens: string[]): void {
  const operation = tokens[1];
  const args = tokens.slice(2);
  if (operation === "status") {
    if (args.every((arg) => ["--short", "--porcelain", "--porcelain=v1", "--branch"].includes(arg))) return;
  } else if (operation === "diff") {
    if (args.every((arg) => ["--stat", "--name-only", "--cached", "--check", "--no-ext-diff"].includes(arg))) return;
  } else if (operation === "log") {
    if (args.every((arg) => arg === "--oneline" || arg === "--decorate" || /^-n\d+$/.test(arg) || /^--max-count=\d+$/.test(arg))) return;
  } else if (operation === "branch") {
    if (args.every((arg) => arg === "--show-current" || arg === "--list")) return;
  } else if (operation === "rev-parse") {
    if (args.length === 1 && ["HEAD", "--show-toplevel"].includes(args[0]!)) return;
    if (args.length === 2 && args[0] === "--abbrev-ref" && args[1] === "HEAD") return;
  } else if (operation === "add") {
    const paths = args.filter((arg) => arg !== "-A" && arg !== "--all" && arg !== "--");
    if (paths.some((path) => path.startsWith("-"))) {
      throw new SelfUpdatePolicyError("Self-update containment denied a Git add option");
    }
    if (paths.length || args.includes("-A") || args.includes("--all")) {
      for (const path of paths) assertSelfUpdatePathAllowed(path);
      return;
    }
  } else if (operation === "commit") {
    const forbidden = args.some((arg) => ["--amend", "-a", "--all", "--reuse-message", "-C", "-c"].includes(arg));
    const messageIndex = args.indexOf("-m");
    if (!forbidden
      && args.includes("--no-verify")
      && args.includes("--no-gpg-sign")
      && messageIndex >= 0
      && Boolean(args[messageIndex + 1])) return;
  }
  throw new SelfUpdatePolicyError("Self-update containment denied a Git operation");
}

function assertVerificationCommand(tokens: string[]): void {
  const [executable, operation, target] = tokens;
  if (executable === "bun" && operation === "test") return;
  if (executable === "bunx" && ["biome", "eslint", "prettier", "svelte-check", "tsc", "vite"].includes(operation ?? "")) return;
  if (["npm", "pnpm", "yarn"].includes(executable ?? "")) {
    const script = operation === "run" ? target : operation;
    if (["build", "check", "lint", "test", "typecheck"].includes(script ?? "")) return;
  }
  if (executable === "cargo" && ["check", "clippy", "fmt", "test"].includes(operation ?? "")) return;
  if (executable === "go" && ["build", "test", "vet"].includes(operation ?? "")) return;
  if (executable === "python" && operation === "-m" && ["mypy", "pytest", "ruff"].includes(target ?? "")) return;
  if (["mypy", "pytest", "ruff"].includes(executable ?? "")) return;
  if (executable === "deno" && ["check", "fmt", "lint", "test"].includes(operation ?? "")) return;
  if (executable === "dotnet" && ["build", "test"].includes(operation ?? "")) return;
  if (["gradle", "mvn"].includes(executable ?? "") && ["build", "check", "test", "verify"].includes(operation ?? "")) return;
  if (executable === "make" && ["build", "check", "lint", "test"].includes(operation ?? "")) return;
  if (executable === "cmake" && operation === "--build") return;
  if (executable === "ninja") return;
  throw new SelfUpdatePolicyError("Self-update containment denied a command");
}

function assertSelfUpdateCommand(command: string): void {
  if (!command.trim() || /[\n\r;&|<>`$\\"']/.test(command)) {
    throw new SelfUpdatePolicyError("Self-update containment requires one literal command");
  }
  const lower = command.toLowerCase();
  if (/(^|\s)(?:\.env(?:\.|\s|$)|\.git(?:\/|\s|$)|credential|secret|deploy|publish|push|remote|systemctl|docker|ssh|curl|wget|gh)(?:\s|\/|$)/.test(lower)) {
    throw new SelfUpdatePolicyError("Self-update containment denied a protected operation");
  }
  const tokens = command.trim().split(/\s+/);
  if (tokens[0] === "git") assertGitCommand(tokens);
  else assertVerificationCommand(tokens);
}

function allowedInstructionRead(toolName: string, path: string): boolean {
  if (toolName !== "mastra_workspace_read_file") return false;
  const normalized = posix.normalize(path.replaceAll("\\", "/")).replace(/^\.\//, "");
  return /^agents(?:_[a-z0-9_-]+)?\.md$/i.test(posix.basename(normalized))
    || /^\.agents\/skills\/[^/]+\/skill\.md$/i.test(normalized);
}

export function assertSelfUpdateToolCall(toolName: string, input: unknown): void {
  const lowerName = toolName.toLowerCase();
  if (toolName === "retainMemory"
    || toolName === "editMemory"
    || toolName === "useBrowserSecret"
    || toolName === "bifrostNavigator"
    || /browser|playwright|navigate|screenshot/.test(lowerName)) {
    throw new SelfUpdatePolicyError("Self-update containment denied a browser or credential tool");
  }
  if (!input || typeof input !== "object" || Array.isArray(input)) return;
  const values = input as Record<string, unknown>;
  if (typeof values.path === "string" && !allowedInstructionRead(toolName, values.path)) {
    assertSelfUpdatePathAllowed(values.path);
  }
  if (typeof values.cwd === "string" && values.cwd !== ".") assertSelfUpdatePathAllowed(values.cwd);
  if (toolName === "mastra_workspace_list_files" && values.showHidden === true) {
    throw new SelfUpdatePolicyError("Self-update containment denied hidden-file listing");
  }
  if (toolName === "mastra_workspace_grep" && values.includeHidden === true) {
    throw new SelfUpdatePolicyError("Self-update containment denied hidden-file search");
  }
  if (toolName === "mastra_workspace_execute_command") {
    if (typeof values.command !== "string") {
      throw new SelfUpdatePolicyError("Self-update command is missing");
    }
    assertSelfUpdateCommand(values.command);
  }
}

type ProcessOptions = {
  cwd?: string;
  signal?: AbortSignal;
  timeout?: number;
  maxBytes?: number;
  allowedExitCodes?: number[];
  env?: NodeJS.ProcessEnv;
};
type ProcessResult = {
  stdout: Buffer;
  stderr: Buffer;
  code: number | null;
};

function runProcess(command: string, args: string[], options: ProcessOptions = {}): Promise<ProcessResult> {
  options.signal?.throwIfAborted();
  const maxBytes = options.maxBytes ?? SELF_UPDATE_REVIEW_DIFF_MAX_BYTES;
  const { promise, resolve: resolveOutput, reject } = Promise.withResolvers<ProcessResult>();
  const child = spawn(command, args, {
    cwd: options.cwd,
    env: {
      PATH: process.env.PATH,
      HOME: "/nonexistent",
      GIT_CONFIG_NOSYSTEM: "1",
      GIT_TERMINAL_PROMPT: "0",
      GIT_OPTIONAL_LOCKS: "0",
      ...options.env,
    },
    stdio: ["ignore", "pipe", "pipe"],
  });
  const stdout: Buffer[] = [];
  const stderr: Buffer[] = [];
  let stdoutBytes = 0;
  let stderrBytes = 0;
  let exceeded = false;
  let aborted = false;
  let timer: ReturnType<typeof setTimeout> | undefined;
  const append = (target: Buffer[], chunk: Buffer, current: number) => {
    const next = current + chunk.byteLength;
    if (next > maxBytes) {
      exceeded = true;
      child.kill("SIGKILL");
      return current;
    }
    target.push(chunk);
    return next;
  };
  const abort = () => {
    aborted = true;
    child.kill("SIGTERM");
  };
  options.signal?.addEventListener("abort", abort, { once: true });
  if (options.timeout !== undefined) {
    timer = setTimeout(() => child.kill("SIGKILL"), options.timeout);
  }
  child.stdout.on("data", (chunk: Buffer | string) => {
    stdoutBytes = append(stdout, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), stdoutBytes);
  });
  child.stderr.on("data", (chunk: Buffer | string) => {
    stderrBytes = append(stderr, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), stderrBytes);
  });
  child.once("error", reject);
  child.once("close", (code) => {
    clearTimeout(timer);
    options.signal?.removeEventListener("abort", abort);
    if (exceeded) {
      reject(new SelfUpdatePolicyError("Self-update command output exceeded its byte bound"));
    } else if (aborted || options.signal?.aborted) {
      reject(options.signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
    } else if (!options.allowedExitCodes?.includes(code ?? -1) && code !== 0) {
      reject(new SelfUpdatePolicyError("Self-update contained command failed"));
    } else {
      resolveOutput({ stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), code });
    }
  });
  return promise;
}

function processOutput(command: string, args: string[], cwd?: string): Promise<Buffer> {
  return runProcess(command, args, { cwd }).then(({ stdout }) => stdout);
}

const SAFE_GIT_CONFIG = [
  "-c", "core.hooksPath=/dev/null",
  "-c", "credential.helper=",
  "-c", "core.pager=cat",
  "-c", "diff.external=",
];

function git(
  path: string,
  args: string[],
  options: Omit<ProcessOptions, "cwd"> = {},
): Promise<Buffer> {
  return runProcess("git", [...SAFE_GIT_CONFIG, "-C", path, ...args], {
    ...options,
    maxBytes: options.maxBytes ?? SELF_UPDATE_REVIEW_DIFF_MAX_BYTES,
  }).then(({ stdout }) => stdout);
}


async function gitSucceeds(path: string, args: string[]): Promise<boolean> {
  try {
    await git(path, args);
    return true;
  } catch {
    return false;
  }
}

function digest(value: Buffer): string {
  return createHash("sha256").update(value).digest("hex");
}
export type SelfUpdateInspection = {
  changedPaths: string[];
  diffCheck: string;
  diffEvidence: string;
  diffDigest: string;
  clean: boolean;
};

async function reviewDiff(path: string): Promise<Buffer> {
  const indexPath = join(path, ".git", `popagent-review-${crypto.randomUUID()}.index`);
  const env = { GIT_INDEX_FILE: indexPath };
  try {
    await git(path, ["read-tree", "HEAD"], { env });
    await git(path, ["add", "--all"], { env });
    return await git(path, [
      "diff",
      "--cached",
      "--binary",
      "--no-ext-diff",
      "--no-renames",
    ], { env, maxBytes: SELF_UPDATE_REVIEW_DIFF_MAX_BYTES });
  } finally {
    await rm(indexPath, { force: true }).catch(() => undefined);
  }
}

async function snapshot(path: string): Promise<GitSnapshot> {
  const [head, branch, status, remotes] = await Promise.all([
    git(path, ["rev-parse", "HEAD"]),
    git(path, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
    git(path, ["status", "--porcelain=v1", "--untracked-files=all"]),
    git(path, ["remote", "-v"]),
  ]);
  if (status.length) throw new SelfUpdatePolicyError("Self-update requires a clean registered Git workspace");
  return {
    head: head.toString("utf8").trim(),
    branch: branch.toString("utf8").trim(),
    remotes: digest(remotes),
  };
}

function assertEligibleRoles(
  supervisor: AgentSettings | undefined,
  implementer: AgentSettings | undefined,
  reviewer: AgentSettings | undefined,
): void {
  if (supervisor?.workspaceAccess !== "read-write"
    || implementer?.workspaceAccess !== "read-write"
    || !implementer.delegationEnabled
    || reviewer?.workspaceAccess === "none"
    || !reviewer?.delegationEnabled) {
    throw new SelfUpdatePolicyError("Self-update agent roles are not eligible");
  }
}
function assertBuildMaintenanceRoles(
  maintainer: AgentSettings | undefined,
  reviewer: AgentSettings | undefined,
): void {
  if (maintainer?.workspaceAccess !== "read-write"
    || !maintainer.delegationEnabled
    || reviewer?.workspaceAccess === "none"
    || !reviewer?.delegationEnabled) {
    throw new SelfUpdatePolicyError("Build maintenance agent roles are not eligible");
  }
}

export class SelfUpdateRuntimePolicy {
  private readonly activeTasks = new Set<string>();

  constructor(
    private readonly workspaces: WorkspaceRepository = agentWorkspaces,
    private readonly roles: RoleRepository = agentSettings,
    private readonly settings: AutonomyRepository = autonomySettings,
  ) {}

  async begin(task: AgentTask): Promise<SelfUpdateRun> {
    if (task.source !== "self-update" && task.source !== "build-maintenance") {
      throw new SelfUpdatePolicyError("Autonomous task source is invalid");
    }
    const source = task.source;
    if (this.activeTasks.has(task.id)) throw new SelfUpdatePolicyError("Autonomous task is already executing");
    this.activeTasks.add(task.id);
    const root = resolve(
      process.env.POPAGENT_DATA_DIR ?? join(process.cwd(), "data"),
      source === "build-maintenance" ? "build-maintenance" : "self-update",
    );
    const path = join(root, createHash("sha256").update(task.id).digest("hex"));
    try {
      const [currentSettings, supervisor, implementer, buildMaintainer, reviewer, repository] = await Promise.all([
        this.settings.get(),
        this.roles.get("orchistrator"),
        this.roles.get("implementer"),
        this.roles.get("build-maintainer"),
        this.roles.get("reviewer"),
        this.workspaces.resolveRepository(task.workspaceId),
      ]);
      if (!currentSettings.enabled
        || (source === "self-update" && !currentSettings.selfUpdateEnabled)
        || (source === "build-maintenance" && !currentSettings.idleImprovementEnabled)) {
        throw new SelfUpdatePolicyError(
          source === "build-maintenance" ? "Build maintenance is disabled" : "Self-update is disabled",
        );
      }
      if (source === "build-maintenance"
        && repository.workspace.name.trim().toLowerCase() !== "akurai-build"
        && !/(?:^|[\\/])akurai-build$/.test(repository.workspace.repositoryPath.trim().toLowerCase())) {
        throw new SelfUpdatePolicyError("Build maintenance requires the AkurAI-Build workspace");
      }
      if (source === "build-maintenance") assertBuildMaintenanceRoles(buildMaintainer, reviewer);
      else assertEligibleRoles(supervisor, implementer, reviewer);
      const captured = await snapshot(repository.path);
      const prefix = source === "build-maintenance"
        ? BUILD_MAINTENANCE_BRANCH_PREFIX
        : SELF_UPDATE_BRANCH_PREFIX;
      const branch = `${prefix}${task.id.replace(/[^a-zA-Z0-9._-]/g, "-")}-${crypto.randomUUID().slice(0, 8)}`;
      await mkdir(root, { recursive: true, mode: 0o700 });
      await rm(path, { recursive: true, force: true });
      await processOutput("git", [
        ...SAFE_GIT_CONFIG,
        "clone",
        "--no-hardlinks",
        "--no-checkout",
        "--",
        repository.path,
        path,
      ]);
      await git(path, ["checkout", "--no-track", "-b", branch, captured.head]);
      await git(path, ["config", "user.name", source === "build-maintenance" ? "Popagent Build Maintenance" : "Popagent Self Update"]);
      await git(path, ["config", "user.email", source === "build-maintenance" ? "build-maintenance@localhost" : "self-update@localhost"]);
      await git(path, ["config", "commit.gpgsign", "false"]);
      const dependencyPath = join(repository.path, "node_modules");
      if (await gitSucceeds(path, ["check-ignore", "--quiet", "node_modules"])) {
        try {
          await access(dependencyPath);
          await symlink(dependencyPath, join(path, "node_modules"), "dir");
        } catch {
          // Verification remains available for dependency-free repositories.
        }
      }
      await git(path, ["config", "core.hooksPath", "/dev/null"]);
      return {
        taskId: task.id,
        workspaceId: task.workspaceId,
        repositoryPath: repository.path,
        path,
        branch,
        snapshot: captured,
        cloneRemotes: digest(await git(path, ["remote", "-v"])),
        delegations: new Set<string>(),
        deploymentEnabled: source === "self-update" && currentSettings.idleDeploymentEnabled,
        source,
      };
    } catch (error) {
      await rm(path, { recursive: true, force: true }).catch(() => undefined);
      this.activeTasks.delete(task.id);
      throw error;
    }
  }

  observeDelegation(run: SelfUpdateRun, agentId: string, success: boolean, diffDigest?: string): void {
    const required = run.source === "build-maintenance" ? "build-maintainer" : "implementer";
    if (success && agentId === required) {
      run.delegations.add(agentId);
    }
    if (success && agentId === "reviewer" && diffDigest === run.reviewedDiffDigest) {
      run.delegations.add(agentId);
    }
  }

  async inspect(run: SelfUpdateRun): Promise<SelfUpdateInspection> {
    const [status, diffCheck, diff] = await Promise.all([
      git(run.path, ["status", "--porcelain=v1", "--untracked-files=all"]),
      git(run.path, ["diff", "--check"]),
      reviewDiff(run.path),
    ]);
    const changedPaths = status
      .toString("utf8")
      .split("\n")
      .map((line) => line.slice(3).trim())
      .filter(Boolean)
      .flatMap((line) => line.includes(" -> ") ? line.split(" -> ") : [line]);
    for (const changedPath of changedPaths) assertSelfUpdatePathAllowed(changedPath);
    if (diff.byteLength > SELF_UPDATE_REVIEW_DIFF_MAX_BYTES) {
      throw new SelfUpdatePolicyError("Self-update diff exceeds the review byte bound");
    }
    return {
      changedPaths,
      diffCheck: diffCheck.toString("utf8").trim().slice(0, 4_000),
      diffEvidence: diff.toString("utf8"),
      diffDigest: digest(diff),
      clean: changedPaths.length === 0,
    };
  }

  async verify(run: SelfUpdateRun, command: string, signal?: AbortSignal): Promise<string> {
    assertSelfUpdateCommand(command);
    signal?.throwIfAborted();
    const tokens = command.trim().split(/\s+/);
    if (tokens[0] === "git") {
      return (await git(run.path, tokens.slice(1), {
        signal,
        maxBytes: SELF_UPDATE_COMMAND_OUTPUT_MAX_BYTES,
      })).toString("utf8").trim().slice(0, 4_000);
    }
    const workspace = await agentWorkspaceRuntime.resolveAutonomous(
      run.workspaceId,
      run.path,
      false,
      run.source ?? "self-update",
    );
    const result = await workspace.sandbox?.executeCommand?.(tokens[0]!, tokens.slice(1), {
      timeout: 30_000,
      abortSignal: signal,
      maxRetainedBytes: SELF_UPDATE_COMMAND_OUTPUT_MAX_BYTES,
      env: {
        HOME: "/nonexistent",
        NODE_ENV: "test",
      },
    });
    if (!result) throw new SelfUpdatePolicyError("Self-update verification sandbox is unavailable");
    if (!result.success) throw new SelfUpdatePolicyError("Self-update verification command failed");
    const stdout = Buffer.from(result.stdout, "utf8").subarray(0, SELF_UPDATE_COMMAND_OUTPUT_MAX_BYTES).toString("utf8");
    const stderr = Buffer.from(result.stderr, "utf8").subarray(0, SELF_UPDATE_COMMAND_OUTPUT_MAX_BYTES).toString("utf8");
    return `${stdout}${stderr ? `\nstderr:\n${stderr}` : ""}`.trim().slice(0, 4_000);
  }

  async commit(run: SelfUpdateRun, expectedDiffDigest: string): Promise<string> {
    const inspection = await this.inspect(run);
    if (inspection.clean) throw new SelfUpdatePolicyError("Self-update commit requires a source change");
    if (inspection.diffDigest !== expectedDiffDigest) {
      throw new SelfUpdatePolicyError("Self-update diff changed after review");
    }
    await git(run.path, ["add", "--all"]);
    const beforeCommit = await this.inspect(run);
    if (beforeCommit.diffDigest !== expectedDiffDigest) {
      throw new SelfUpdatePolicyError("Self-update diff changed before commit");
    }
    await git(run.path, [
      "commit",
      "--no-verify",
      "--no-gpg-sign",
      "-m",
      "Autonomous self-update",
    ]);
    return (await git(run.path, ["rev-parse", "HEAD"])).toString("utf8").trim();
  }

  async accept(run: SelfUpdateRun, signal?: AbortSignal): Promise<void> {
    let importedHead: string | undefined;
    try {
      const source = run.source ?? "self-update";
      const label = source === "build-maintenance" ? "Build maintenance" : "Self-update";
      const [currentSettings, supervisor, implementer, buildMaintainer, reviewer, repository] = await Promise.all([
        this.settings.get(),
        this.roles.get("orchistrator"),
        this.roles.get("implementer"),
        this.roles.get("build-maintainer"),
        this.roles.get("reviewer"),
        this.workspaces.resolveRepository(run.workspaceId),
      ]);
      if (!currentSettings.enabled
        || (source === "self-update" && !currentSettings.selfUpdateEnabled)
        || (source === "build-maintenance" && !currentSettings.idleImprovementEnabled)) {
        throw new SelfUpdatePolicyError(
          source === "build-maintenance" ? "Build maintenance was disabled before completion" : "Self-update was disabled before completion",
        );
      }
      if (source === "build-maintenance") assertBuildMaintenanceRoles(buildMaintainer, reviewer);
      else assertEligibleRoles(supervisor, implementer, reviewer);
      if (resolve(repository.path) !== resolve(run.repositoryPath)) {
        throw new SelfUpdatePolicyError(
          source === "build-maintenance"
            ? "Build maintenance workspace registration changed during execution"
            : "Self-update workspace registration changed during execution",
        );
      }
      const [current, cloneStatus, cloneBranch, cloneHead, cloneRemotes] = await Promise.all([
        snapshot(run.repositoryPath),
        git(run.path, ["status", "--porcelain=v1", "--untracked-files=all"]),
        git(run.path, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
        git(run.path, ["rev-parse", "HEAD"]),
        git(run.path, ["remote", "-v"]),
      ]);
      if (current.head !== run.snapshot.head
        || current.branch !== run.snapshot.branch
        || current.remotes !== run.snapshot.remotes) {
        throw new SelfUpdatePolicyError(
          source === "build-maintenance"
            ? "Registered workspace changed during build maintenance"
            : "Registered workspace changed during self-update",
        );
      }
      if (cloneStatus.length
        || cloneBranch.toString("utf8").trim() !== run.branch
        || digest(cloneRemotes) !== run.cloneRemotes) {
        throw new SelfUpdatePolicyError(
          source === "build-maintenance"
            ? "Build maintenance checkout failed completion containment"
            : "Self-update checkout failed completion containment",
        );
      }
      const finalHead = cloneHead.toString("utf8").trim();
      if (finalHead === run.snapshot.head) return;
      const requiredDelegation = source === "build-maintenance" ? "build-maintainer" : "implementer";
      if (!run.reviewedDiffDigest
        || !run.delegations.has(requiredDelegation)
        || !run.delegations.has("reviewer")) {
        throw new SelfUpdatePolicyError(
          source === "build-maintenance"
            ? "Build maintenance requires successful Build Maintainer and reviewer delegation"
            : "Self-update requires successful implementer and reviewer delegation",
        );
      }
      if (await gitSucceeds(run.repositoryPath, ["cat-file", "-e", `${finalHead}^{commit}`])) {
        throw new SelfUpdatePolicyError(`${label} result is not a new local commit`);
      }
      if (!(await gitSucceeds(run.path, ["merge-base", "--is-ancestor", run.snapshot.head, finalHead]))) {
        throw new SelfUpdatePolicyError(`${label} commit does not descend from the captured HEAD`);
      }
      const changed = (await git(run.path, [
        "diff",
        "--no-renames",
        "--name-only",
        "-z",
        `${run.snapshot.head}..${finalHead}`,
      ]))
        .toString("utf8")
        .split("\0")
        .filter(Boolean);
      if (!changed.length) throw new SelfUpdatePolicyError(`${label} commit has no source change`);
      for (const path of changed) assertSelfUpdatePathAllowed(path);
      const committedDiff = await git(run.path, [
        "diff",
        "--binary",
        "--no-ext-diff",
        "--no-renames",
        `${run.snapshot.head}..${finalHead}`,
      ], { maxBytes: SELF_UPDATE_REVIEW_DIFF_MAX_BYTES });
      if (digest(committedDiff) !== run.reviewedDiffDigest) {
        throw new SelfUpdatePolicyError(`${label} diff changed after review`);
      }
      if (await gitSucceeds(run.repositoryPath, ["show-ref", "--verify", "--quiet", `refs/heads/${run.branch}`])) {
        throw new SelfUpdatePolicyError(`${label} branch already exists`);
      }
      await git(run.repositoryPath, ["fetch", "--no-tags", "--no-write-fetch-head", run.path, finalHead]);
      signal?.throwIfAborted();
      const importedDiff = await git(run.path, [
        "diff",
        "--binary",
        "--no-ext-diff",
        "--no-renames",
        `${run.snapshot.head}..${finalHead}`,
      ], { maxBytes: SELF_UPDATE_REVIEW_DIFF_MAX_BYTES });
      if (digest(importedDiff) !== run.reviewedDiffDigest) {
        throw new SelfUpdatePolicyError(`${label} diff changed before import`);
      }
      await git(run.repositoryPath, ["update-ref", `refs/heads/${run.branch}`, finalHead, ZERO_OBJECT_ID]);
      importedHead = finalHead;
      signal?.throwIfAborted();
      if (run.deploymentEnabled) {
        try {
          await processOutput(join(run.repositoryPath, "deploy.sh"), ["publish"], run.repositoryPath);
        } catch (error) {
          await git(run.repositoryPath, ["reset", "--hard", run.snapshot.head]);
          throw error;
        }
        execFile(join(run.repositoryPath, "deploy.sh"), ["deploy"], {
          cwd: run.repositoryPath,
          windowsHide: true,
        }, (error) => {
          if (error) appLogger().error("self_update.deploy_failed", { taskId: run.taskId, error: error.message });
        }).unref();
      }
    } catch (error) {
      if (importedHead) {
        await git(run.repositoryPath, ["update-ref", "-d", `refs/heads/${run.branch}`, importedHead]).catch(() => undefined);
      }
      throw error;
    } finally {
      await this.release(run);
    }
  }

  async reject(run: SelfUpdateRun): Promise<void> {
    await this.release(run);
  }

  private async release(run: SelfUpdateRun): Promise<void> {
    await rm(run.path, { recursive: true, force: true });
    this.activeTasks.delete(run.taskId);
  }
}

export const selfUpdateRuntimePolicy = new SelfUpdateRuntimePolicy();