@agentconnect.md/daemon 1.53.0-rc.5 → 1.53.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"protocol-CWizQiPF.js","names":["fsp","SANDBOX_TUNNEL_PATHS","z.enum","z.object","z.literal","z.string","z.discriminatedUnion","SANDBOX_TUNNEL_PATH_SOURCE","z.enum","z.object","z.literal","z.string","z.number","z.array","z.unknown","z.boolean","z.discriminatedUnion","z.union"],"sources":["../src/memory/fs.ts","../src/shim/sandbox-paths.ts","../src/shim/tunnel.ts","../src/workspace/git-runner.ts","../src/shim/protocol.ts"],"sourcesContent":["/**\n * `MemoryFs` — the file-system port an agent's managed memory tree is kept behind.\n *\n * Every managed-memory writer and reader (`memory/store.ts`, the memory provider, the dream\n * runner, the CP memory reader) is a DIRECTORY abstraction over this port, so where the tree lives\n * is a placement decision, not a policy one: a local agent's home is `<agent.dir>` on this daemon's\n * disk (`LocalMemoryFs`), a cluster agent's is one root on its sandbox volume reached through the\n * shim (`shim/memory-fs-channel.ts`), and a later home is another implementation. Paths are relative\n * to the root; the root itself is absolute in the coordinates of the filesystem that holds it.\n *\n * SECURITY (local): the daemon is outside the agent's sandbox, so a symlink planted in the writable\n * memory dir must not redirect a read or a write. Every operation canonicalises the parent chain one\n * component at a time, rejects symlink components, and opens the leaf with `O_NOFOLLOW`; writes\n * publish through a random exclusive temp file. The shim executor keeps the same rules against open\n * descriptors where the volume is written by the agent's runtime.\n */\nimport { randomUUID } from 'node:crypto'\nimport { constants, promises as fsp, type Stats } from 'node:fs'\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path'\n\n/** Raised when a memory path escapes its root or resolves through a symlink. Surfaces as `BAD_PAYLOAD`. */\nexport class MemoryPathError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MemoryPathError'\n }\n}\n\n/** Raised when a write exceeds the memory file cap. `BAD_PAYLOAD`. */\nexport class MemoryTooLargeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MemoryTooLargeError'\n }\n}\n\n/** Raised when an `ifMatchMtime` precondition fails (the file changed under the writer). Surfaces as CONFLICT. */\nexport class MemoryConflictError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MemoryConflictError'\n }\n}\n\n/** Raised when a cluster agent's memory home is on a sandbox that is not running — one resolution, no local fallback. */\nexport class MemorySandboxUnavailableError extends Error {\n readonly reason = 'sandbox-unavailable' as const\n constructor(message: string) {\n super(message)\n this.name = 'MemorySandboxUnavailableError'\n }\n}\n\nexport interface MemoryFsFileStat {\n size: number\n /** ISO mtime — the optimistic-concurrency token every memory writer compares. */\n mtime: string\n}\n\nexport interface MemoryFsFile extends MemoryFsFileStat {\n /** The file's text, or its bytes as base64 when the read asked for that encoding. */\n content: string\n}\n\nexport type MemoryFsEncoding = 'utf8' | 'base64'\n\nexport interface MemoryFsEntry {\n name: string\n kind: 'file' | 'dir' | 'other'\n size?: number\n mtime?: string\n}\n\nexport interface MemoryFsWriteOptions {\n /** Non-empty ⇒ the target's current mtime must equal it (a brand-new file never matches). */\n ifMatchMtime?: string\n mode?: number\n}\n\n/** The port. Paths are root-relative; a missing path is data (`null` / `[]` / `false`), never an error. */\nexport interface MemoryFs {\n /** Identity of the tree for the in-process locks and write ledger — equal for every instance over one tree. */\n readonly key: string\n /** Absolute root in the coordinates of the filesystem holding it (an execution cwd is built from it). */\n readonly root: string\n /** The same port re-rooted at a subdirectory (a channel's self-contained memory root). */\n subdir(rel: string): MemoryFs\n readFile(rel: string, encoding?: MemoryFsEncoding): Promise<MemoryFsFile | null>\n /** Atomic replace-or-create; the leaf is never followed and parents are created. */\n writeFile(rel: string, content: string | Uint8Array, options?: MemoryFsWriteOptions): Promise<MemoryFsFileStat>\n readdir(rel: string): Promise<MemoryFsEntry[]>\n mkdir(rel: string): Promise<void>\n /** false when `from` is absent. */\n rename(from: string, to: string): Promise<boolean>\n /** Recursive and forced: absence is fine. */\n rm(rel: string): Promise<void>\n /** Best-effort: set a file's mtime (kept for files a store swap left byte-for-byte unchanged). */\n utimes(rel: string, mtime: string): Promise<void>\n}\n\n/** Split a root-relative path into plain components; `''` is the root itself. */\nexport function memoryRelSegments(rel: string): string[] {\n if (isAbsolute(rel)) throw new MemoryPathError('absolute paths are not allowed')\n const parts = rel.split(/[\\\\/]+/).filter((part) => part !== '' && part !== '.')\n if (parts.some((part) => part === '..' || part.includes('\\0'))) {\n throw new MemoryPathError('path escapes the memory root')\n }\n return parts\n}\n\nfunction isErrno(err: unknown, code: string): boolean {\n return (err as NodeJS.ErrnoException | null)?.code === code\n}\n\n/** Dropped by a concurrent rm: ENOENT, or EPERM while Windows holds the directory in delete-pending. */\nfunction vanished(err: unknown): boolean {\n return isErrno(err, 'ENOENT') || (process.platform === 'win32' && isErrno(err, 'EPERM'))\n}\n\nfunction under(root: string, path: string): boolean {\n const rel = relative(root, path)\n return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))\n}\n\nfunction sameFileVersion(a: Stats, b: Stats): boolean {\n return b.isFile() && a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs\n}\n\n/**\n * A failed containment check is an escape only when the path is still there. Windows resolves a\n * component a concurrent rm already unlinked to a path outside the root, so re-probe before calling\n * a benign race a violation; a dropped component is absence, which is data on the read side.\n */\nasync function rejectEscape(path: string, create: boolean): Promise<null> {\n try {\n await fsp.lstat(path)\n } catch (err) {\n if (!vanished(err) || create) throw err\n return null\n }\n throw new MemoryPathError('path resolves outside the memory root')\n}\n\n/**\n * Canonicalise `parts` under `root` one component at a time, refusing symlink components; with\n * `create` missing components are made along the way. `null` when a component is absent (read side).\n */\nasync function walkContained(root: string, parts: string[], create: boolean): Promise<string | null> {\n let realRoot: string\n try {\n realRoot = await fsp.realpath(root)\n } catch (err) {\n if (!isErrno(err, 'ENOENT')) throw err\n if (!create) return null\n await fsp.mkdir(root, { recursive: true })\n realRoot = await fsp.realpath(root)\n }\n let parent = realRoot\n for (const part of parts) {\n const candidate = join(parent, part)\n let stat: Stats\n try {\n stat = await fsp.lstat(candidate)\n } catch (err) {\n if (!create) {\n if (!vanished(err)) throw err\n return null\n }\n if (!isErrno(err, 'ENOENT')) throw err\n try {\n await fsp.mkdir(candidate)\n } catch (mkdirErr) {\n if (!isErrno(mkdirErr, 'EEXIST')) throw mkdirErr\n }\n stat = await fsp.lstat(candidate)\n }\n if (!stat.isDirectory()) throw new MemoryPathError('memory path contains a symlink or non-directory')\n try {\n parent = await fsp.realpath(candidate)\n } catch (err) {\n // A concurrent rm can drop the component between lstat and realpath; absent stays data on the read side.\n if (!vanished(err) || create) throw err\n return null\n }\n if (!under(realRoot, parent)) return rejectEscape(candidate, create)\n }\n return parent\n}\n\ntype CurrentFile = { existed: false; before: ''; stat?: undefined } | { existed: true; before: string; stat: Stats }\n\n/** Open the leaf without following a symlink; `existed:false` on ENOENT. */\nasync function readCurrentFile(target: string, encoding: MemoryFsEncoding = 'utf8'): Promise<CurrentFile> {\n let handle\n try {\n handle = await fsp.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)\n } catch (err) {\n if (isErrno(err, 'ENOENT')) return { existed: false, before: '' }\n if (isErrno(err, 'ELOOP')) throw new MemoryPathError('memory target is not a regular file')\n throw err\n }\n try {\n const stat = await handle.stat()\n if (!stat.isFile()) throw new MemoryPathError('memory target is not a regular file')\n return { existed: true, before: await handle.readFile(encoding), stat }\n } finally {\n await handle.close()\n }\n}\n\n/** Read one file under `root` (a memory tree or the runtime's own store) without following symlinks; '' when absent. */\nexport async function readContainedMemoryFile(root: string, destination: string): Promise<string> {\n const parts = containedParts(root, destination)\n const parent = await walkContained(root, parts.slice(0, -1), false)\n if (parent === null) return ''\n return (await readCurrentFile(join(parent, parts[parts.length - 1]!))).before\n}\n\n/** `destination` must be a lexical descendant of `root`; returns its components. */\nfunction containedParts(root: string, destination: string): string[] {\n const lexicalRoot = resolve(root)\n const lexicalTarget = resolve(destination)\n if (!under(lexicalRoot, lexicalTarget) || lexicalTarget === lexicalRoot) {\n throw new MemoryPathError('path escapes the memory root')\n }\n return relative(lexicalRoot, lexicalTarget).split(sep).filter(Boolean)\n}\n\n/**\n * Atomically replace one file under `root` without following symlinks. The random `wx` temp defeats\n * pre-planted `<target>.tmp` links; a non-empty `ifMatchMtime` is checked before the temp write and\n * re-verified (dev/ino/size/mtime) right before the rename.\n */\nexport async function atomicWriteContainedMemoryFile(\n root: string,\n destination: string,\n content: string | Uint8Array,\n ifMatchMtime?: string,\n mode?: number\n): Promise<MemoryFsFileStat> {\n const parts = containedParts(root, destination)\n const parent = (await walkContained(root, parts.slice(0, -1), true))!\n const target = join(parent, parts[parts.length - 1]!)\n const current = await readCurrentFile(target)\n if (ifMatchMtime && current.stat?.mtime.toISOString() !== ifMatchMtime) {\n throw new MemoryConflictError('the memory file changed since it was read; reload and retry')\n }\n const temp = join(parent, `.agentconnect-memory-${randomUUID()}.tmp`)\n try {\n if ((await fsp.realpath(parent)) !== parent) await rejectEscape(parent, true)\n await fsp.writeFile(temp, content, { encoding: 'utf8', flag: 'wx', ...(mode === undefined ? {} : { mode }) })\n if (ifMatchMtime && current.stat) {\n let latest: Stats\n try {\n latest = await fsp.lstat(target)\n } catch (err) {\n if (isErrno(err, 'ENOENT')) {\n throw new MemoryConflictError('the memory file changed since it was read; reload and retry')\n }\n throw err\n }\n if (!sameFileVersion(current.stat, latest)) {\n throw new MemoryConflictError('the memory file changed since it was read; reload and retry')\n }\n }\n if ((await fsp.realpath(parent)) !== parent) await rejectEscape(parent, true)\n await publishOverTarget(temp, target)\n } finally {\n await fsp.rm(temp, { force: true }).catch(() => {})\n }\n const stat = await fsp.lstat(target)\n if (!stat.isFile()) throw new MemoryPathError('memory target is not a regular file')\n return { size: stat.size, mtime: stat.mtime.toISOString() }\n}\n\n// Windows cannot rename over a file another handle holds open — a scanner's transient handle on the\n// bytes just written is enough — so EPERM/EACCES/EBUSY here is a race POSIX never has. Bounded retry,\n// as `WorkspaceManager.renameWorkspaceDirectory` does for the same reason on a directory swap.\nasync function publishOverTarget(temp: string, target: string): Promise<void> {\n if (process.platform !== 'win32') return fsp.rename(temp, target)\n const transient = new Set(['EPERM', 'EACCES', 'EBUSY'])\n for (let attempt = 0; ; attempt++) {\n try {\n return await fsp.rename(temp, target)\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (attempt === 9 || code === undefined || !transient.has(code)) throw error\n await new Promise((resolve) => setTimeout(resolve, 10 * (attempt + 1)))\n }\n }\n}\n\n/** The port over this process's own filesystem, contained to `root`. */\nexport class LocalMemoryFs implements MemoryFs {\n readonly root: string\n readonly key: string\n\n constructor(root: string) {\n this.root = resolve(root)\n this.key = this.root\n }\n\n subdir(rel: string): MemoryFs {\n return new LocalMemoryFs(join(this.root, ...memoryRelSegments(rel)))\n }\n\n private leaf(rel: string): { parts: string[]; name: string } {\n const parts = memoryRelSegments(rel)\n if (parts.length === 0) throw new MemoryPathError('a file name is required')\n return { parts: parts.slice(0, -1), name: parts[parts.length - 1]! }\n }\n\n async readFile(rel: string, encoding: MemoryFsEncoding = 'utf8'): Promise<MemoryFsFile | null> {\n const { parts, name } = this.leaf(rel)\n const parent = await walkContained(this.root, parts, false)\n if (parent === null) return null\n const current = await readCurrentFile(join(parent, name), encoding)\n if (!current.existed) return null\n return { content: current.before, size: current.stat.size, mtime: current.stat.mtime.toISOString() }\n }\n\n writeFile(rel: string, content: string | Uint8Array, options: MemoryFsWriteOptions = {}): Promise<MemoryFsFileStat> {\n const parts = memoryRelSegments(rel)\n if (parts.length === 0) throw new MemoryPathError('a file name is required')\n return atomicWriteContainedMemoryFile(\n this.root,\n join(this.root, ...parts),\n content,\n options.ifMatchMtime,\n options.mode\n )\n }\n\n async readdir(rel: string): Promise<MemoryFsEntry[]> {\n const dir = await walkContained(this.root, memoryRelSegments(rel), false)\n if (dir === null) return []\n let dirents\n try {\n dirents = await fsp.readdir(dir, { withFileTypes: true })\n } catch (err) {\n if (isErrno(err, 'ENOENT') || isErrno(err, 'ENOTDIR')) return []\n throw err\n }\n const entries: MemoryFsEntry[] = []\n for (const d of dirents) {\n const kind: MemoryFsEntry['kind'] = d.isDirectory() ? 'dir' : d.isFile() ? 'file' : 'other'\n const entry: MemoryFsEntry = { name: d.name, kind }\n if (kind === 'file') {\n try {\n const st = await fsp.lstat(join(dir, d.name))\n entry.size = st.size\n entry.mtime = st.mtime.toISOString()\n } catch {\n // raced deletion — keep the name-only entry\n }\n }\n entries.push(entry)\n }\n return entries\n }\n\n async mkdir(rel: string): Promise<void> {\n await walkContained(this.root, memoryRelSegments(rel), true)\n }\n\n async rename(from: string, to: string): Promise<boolean> {\n const source = this.leaf(from)\n const sourceParent = await walkContained(this.root, source.parts, false)\n if (sourceParent === null) return false\n const sourcePath = join(sourceParent, source.name)\n try {\n await fsp.lstat(sourcePath)\n } catch (err) {\n if (isErrno(err, 'ENOENT')) return false\n throw err\n }\n const target = this.leaf(to)\n const targetParent = (await walkContained(this.root, target.parts, true))!\n await fsp.rename(sourcePath, join(targetParent, target.name))\n return true\n }\n\n async rm(rel: string): Promise<void> {\n const { parts, name } = this.leaf(rel)\n const parent = await walkContained(this.root, parts, false)\n if (parent === null) return\n await fsp.rm(join(parent, name), { recursive: true, force: true })\n }\n\n async utimes(rel: string, mtime: string): Promise<void> {\n const { parts, name } = this.leaf(rel)\n const parent = await walkContained(this.root, parts, false)\n if (parent === null) return\n const when = new Date(mtime)\n try {\n await fsp.lutimes(join(parent, name), when, when)\n } catch {\n // best-effort: a vanished file keeps whatever mtime it has\n }\n }\n}\n\n/** The sandbox plane as the factory sees it: the port over a bound sandbox volume, or nothing. */\nexport interface SandboxMemoryFsSource {\n memoryFsFor(agentId: string): MemoryFs | undefined\n}\n\n/**\n * The ONE decision about where an agent's managed memory tree lives. With a sandbox plane (every\n * agent of a `--k8s` daemon runs in a pod) it is the port over the agent's sandbox volume, reachable\n * exactly while the pod is bound — no fallback to this member's disk, since a duty move would leave\n * the memory behind; without one, the local port over the agent dir.\n */\nexport function resolveMemoryFs(\n agent: { id: string; dir: string },\n sandbox: SandboxMemoryFsSource | undefined\n): MemoryFs {\n if (!sandbox) return new LocalMemoryFs(agent.dir)\n const fs = sandbox.memoryFsFor(agent.id)\n if (!fs) {\n throw new MemorySandboxUnavailableError(\n `agent \"${agent.id}\" has no running sandbox, so its memory cannot be reached`\n )\n }\n return fs\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh wrapper and nothing else. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","import { z } from 'zod'\nimport { SANDBOX_TUNNEL_PATHS as SANDBOX_TUNNEL_PATH_SOURCE } from './sandbox-paths.js'\n\n/**\n * Unix sockets the daemon exposes into a sandbox. Each is a daemon-side server the runtime\n * expects to find locally: the git-credential helper (which `gh`'s token helper shares) and\n * the MCP bridge. The shim listens on the in-pod path and proxies bytes back over the channel.\n *\n * The set is closed on purpose. A generic \"tunnel any socket\" capability would let a\n * compromised runtime reach whatever the daemon happens to be listening on; naming the\n * servers keeps the grant meaningful.\n */\nexport const TunnelNameSchema = z.enum(['gitcred', 'mcp'])\nexport type TunnelName = z.infer<typeof TunnelNameSchema>\n\n/** Bytes per chunk in either direction. One `shim/request` or `shim/event` carries at most one\n * chunk, so this has to leave room under `MAX_FRAME_BYTES` (256 KiB) after base64 expansion. */\nexport const MAX_TUNNEL_CHUNK_BYTES = 32 * 1024\nconst MAX_TUNNEL_CHUNK_BASE64 = Math.ceil(MAX_TUNNEL_CHUNK_BYTES / 3) * 4\n\n/**\n * Daemon → shim: serve this tunnel on its in-pod path. Idempotent per pod, because the\n * listener belongs to the POD and the channel does not — a credential renewal replaces the\n * socket underneath while every in-pod client keeps its connection.\n *\n * It deliberately does NOT name the path. Both sides already know {@link SANDBOX_TUNNEL_PATHS},\n * and a daemon-supplied path would have to be validated against that map on arrival anyway — so\n * the field would carry no information while widening what a compromised daemon could ask the\n * shim to create.\n */\nexport const TunnelListenSchema = z.object({\n op: z.literal('listen'),\n tunnel: TunnelNameSchema\n})\n\n/**\n * Bytes toward one in-pod connection. The daemon sends these as requests; the shim reports the\n * opposite direction as `shim/event` chunks on the same stream id, which is the only way round:\n * requests flow daemon → shim only, and a tunnel connection is opened by a process inside the\n * pod, so the shim has to announce it.\n */\nexport const TunnelDataSchema = z.object({\n op: z.literal('data'),\n streamId: z.string().uuid(),\n /** base64 because the frame is JSON text; the payload is opaque bytes either way. */\n chunk: z.string().max(MAX_TUNNEL_CHUNK_BASE64)\n})\n\nexport const TunnelCloseSchema = z.object({\n op: z.literal('close'),\n streamId: z.string().uuid(),\n error: z.string().max(200).optional()\n})\n\nexport const TunnelPayloadSchema = z.discriminatedUnion('op', [TunnelListenSchema, TunnelDataSchema, TunnelCloseSchema])\nexport type TunnelPayload = z.infer<typeof TunnelPayloadSchema>\n\n/** What a `listen` reports back, so the daemon logs the path the pod actually serves. */\nexport const TunnelListeningSchema = z.object({ socketPath: z.string().min(1) })\n\n/**\n * Which in-pod path each tunnel is served at. Fixed by the runtime image, not by the daemon's own\n * root, because the daemon's paths mean nothing inside the sandbox.\n *\n * Declared in `sandbox-paths.ts` — the credential helper needs the gitcred path and must not pull\n * this module's zod schemas into its own bundle — and re-exported here, typed against the enum\n * above so a name without a path (or a path without a name) fails to compile.\n */\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<TunnelName, string>> = SANDBOX_TUNNEL_PATH_SOURCE\n","import { execFile } from 'node:child_process'\nimport type { SimpleGit } from 'simple-git'\n\n/**\n * The git operations the daemon actually performs on a workspace — a frozen inventory, not a\n * general-purpose git wrapper.\n *\n * It exists because a cluster-backed agent's workspace lives on the sandbox pod's volume, so\n * the daemon cannot reach it: the orchestration logic stays here and only the *execution*\n * moves. Deriving the interface from what the code already calls (rather than from what git\n * can do) is what keeps that move mechanical — the remote side has a closed list to\n * implement, and re-creating simple-git's surface across a channel is explicitly not the job.\n *\n * Adding a member is a deliberate act: it widens what a half-trusted sandbox will execute.\n */\nexport interface GitRunner {\n /**\n * A runner whose invocations use `env` as their COMPLETE environment, replacing rather than\n * extending the ambient one.\n *\n * Every existing call site threads env per invocation, and replacement is the point: callers\n * build it by sanitizing (`workspaceGitLocalEnv` strips host `GIT_CONFIG_*`, clears protocol\n * allowances, injects config pairs), so merging would quietly undo that sanitization. A\n * caller therefore supplies a whole environment, including identity, not a few overrides.\n *\n * Remotely the environment travels with the request rather than being set on the sandbox, so\n * a runtime cannot read the credential-helper pointers back out of its own env afterwards.\n */\n withEnv(env: Record<string, string>): GitRunner\n /** Run a git subcommand with argv, never a composed shell string. */\n raw(args: string[]): Promise<string>\n clone(repo: string, target: string, options?: string[]): Promise<void>\n /** Pull, returning what the console reports: which files moved and by how much. */\n pull(remote: string, branch: string, options?: string[]): Promise<GitPullSummary>\n status(): Promise<GitStatusSummary>\n /** Commits newest first, bounded by `maxCount`. */\n log(options: { maxCount: number }): Promise<GitLogEntry[]>\n /**\n * Run a read-only subcommand with a HARD ceiling on the bytes it returns, and report\n * whether that ceiling was hit.\n *\n * Distinct from {@link raw} because `raw` accumulates the whole child stdout: one\n * `git diff` on a large change, or a numstat over a tens-of-thousands-of-files dirty\n * tree, is orders of magnitude larger than the wire frame it is being read for. The\n * console's review surface asks for a head slice and nothing more, and it asks on\n * every session page view, so the bound belongs in the contract rather than at each\n * call site — a remote implementation must honour it too, or a sandbox can stream an\n * unbounded reply back across the channel.\n *\n * `overflow: true` means the child was killed at `maxBytes` and `out` is the head\n * slice, which is the answer this seam wants: the caller reports it as truncated.\n */\n readBounded(args: string[], maxBytes: number): Promise<{ out: Buffer; overflow: boolean }>\n}\n\n/**\n * Raised when an invocation never reached git — as opposed to git running and failing.\n *\n * The distinction is load-bearing for every caller that treats a git failure as an ANSWER. The\n * console's `isRepo` preflight is the sharp case: a request the transport dropped is not evidence\n * that the cwd is outside a repository, and reading it as such reports \"not a git checkout\" for a\n * checkout that is there. Only the remote runner raises it — a local child either runs or reports a\n * spawn failure, and there is no channel in between to lose.\n */\nexport class GitTransportError extends Error {\n constructor(\n message: string,\n readonly cause?: unknown\n ) {\n super(message)\n this.name = 'GitTransportError'\n }\n}\n\n/** The pull result the BFF surfaces to the console; nothing here is decorative. */\nexport interface GitPullSummary {\n files: string[]\n insertions: number\n deletions: number\n}\n\n/** A commit as the workspace views consume it — the committer date included, because the\n * console shows when HEAD last moved and an interface without it cannot serve that. */\nexport interface GitLogEntry {\n hash: string\n subject: string\n /** Strict-ISO committer date (`%cI`), empty when the runtime reported none. */\n committedAt: string\n}\n\n/** The status fields the daemon reads; simple-git returns many more. */\nexport interface GitStatusSummary {\n current: string | null\n tracking: string | null\n ahead: number\n behind: number\n files: Array<{ path: string; index: string; working_dir: string }>\n /** Whether the tree has no changes. Taken from simple-git locally rather than re-derived,\n * so the local answer stays authoritative; the remote side derives it and the contract test\n * is what establishes the two agree, including on a conflicted tree. */\n clean: boolean\n}\n\n/** A local diff/log/numstat never touches the network, so it either answers quickly or the\n * checkout is wedged (an index.lock holder, a dead fsmonitor). */\nconst READ_TIMEOUT_MS = 15_000\n\n/** Factory for a runner bound to one working directory. */\nexport type GitRunnerFor = (cwd?: string, abort?: AbortSignal) => GitRunner\n\n/**\n * Today's behaviour: run git in this daemon's own filesystem through simple-git.\n *\n * A thin delegation on purpose — the point of the seam is that the local path keeps its exact\n * semantics (including simple-git's argument handling and its abort-kills-the-child plugin),\n * so a cluster agent and a self-hosted agent differ in where git runs and in nothing else.\n */\nexport class LocalGitRunner implements GitRunner {\n // `cwd` and `env` are carried alongside the handle because `readBounded` spawns its own\n // child and cannot ask simple-git what it was configured with.\n // Both extra parameters are REQUIRED, not optional, because a site that forgets either one\n // fails in a way that reads as a passing test. `cwd`: `readBounded` spawns its own child and\n // cannot ask simple-git where it was pointed, so a missing one runs git in the daemon's own\n // directory (this happened twice while it was optional). `make`: see `withEnv`.\n constructor(\n private readonly git: SimpleGit,\n private readonly cwd: string | undefined,\n private readonly make: (env: Record<string, string>) => SimpleGit,\n private readonly env: Record<string, string> = {}\n ) {}\n\n withEnv(env: Record<string, string>): GitRunner {\n // A derived runner gets its OWN executor, built by `make`. simple-git's `.env()` mutates the\n // ROOT executor and returns the same instance, so sharing a handle across siblings is wrong in\n // three ways that a sequential test cannot see: two derivations leave the last env in place,\n // concurrent calls (`Promise.all`) both run under whichever env was set last, and the BASE\n // inherits a child's env because nothing ever resets it. That is not academic — deriving an\n // identity-carrying runner and a config-audit runner from one base made a commit land as the\n // host's OS user. Independent executors make the env a property of the runner, as the shim's\n // per-request environment already is.\n return new LocalGitRunner(this.make(env), this.cwd, this.make, env)\n }\n\n async raw(args: string[]): Promise<string> {\n return this.git.raw(args)\n }\n\n readBounded(args: string[], maxBytes: number): Promise<{ out: Buffer; overflow: boolean }> {\n // `execFile` rather than the simple-git handle, which is the whole reason this member\n // exists: `maxBuffer` and `timeout` are what bound it, and simple-git exposes neither.\n // The env and cwd come from the same places the handle's do, so the two paths differ in\n // the ceiling and in nothing else.\n return new Promise((resolve, reject) => {\n execFile(\n 'git',\n args,\n {\n ...(this.cwd ? { cwd: this.cwd } : {}),\n env: { ...this.env },\n encoding: 'buffer',\n maxBuffer: maxBytes,\n timeout: READ_TIMEOUT_MS,\n windowsHide: true\n },\n (err, stdout) => {\n if (!err) return resolve({ out: stdout, overflow: false })\n // The ceiling was hit: the child is already dead and `stdout` holds the head slice.\n if ((err as NodeJS.ErrnoException).code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {\n return resolve({ out: stdout, overflow: true })\n }\n reject(err)\n }\n )\n })\n }\n\n async clone(repo: string, target: string, options: string[] = []): Promise<void> {\n await this.git.clone(repo, target, options)\n }\n\n async pull(remote: string, branch: string, options: string[] = []): Promise<GitPullSummary> {\n const result = await this.git.pull(remote, branch, options)\n return {\n files: [...result.files],\n insertions: result.summary.insertions,\n deletions: result.summary.deletions\n }\n }\n\n async status(): Promise<GitStatusSummary> {\n const summary = await this.git.status()\n return {\n current: summary.current ?? null,\n tracking: summary.tracking ?? null,\n ahead: summary.ahead,\n behind: summary.behind,\n files: summary.files.map((file) => ({\n path: file.path,\n index: file.index,\n working_dir: file.working_dir\n })),\n clean: summary.isClean()\n }\n }\n\n async log(options: { maxCount: number }): Promise<GitLogEntry[]> {\n const result = await this.git.log({\n maxCount: options.maxCount,\n format: { hash: '%H', date: '%cI', subject: '%s' }\n })\n return result.all.map((entry) => ({\n hash: entry.hash,\n subject: entry.subject ?? '',\n committedAt: entry.date ?? ''\n }))\n }\n}\n","import { z } from 'zod'\nimport { TunnelNameSchema } from './tunnel.js'\n\n/** WS subprotocol and path the daemon uses when dialing the sandbox shim. */\nexport const SHIM_SUBPROTOCOL = 'agentconnect.shim.v1'\nexport const SHIM_WS_PATH = '/shim/v1'\n\n/** Audience the projected ServiceAccount token is restricted to. A token minted for\n * anything else must not authenticate here, which is what makes the pod's own\n * credential safe to hand over: it is useless anywhere but this endpoint. */\nexport const SHIM_TOKEN_AUDIENCE = 'ac-daemon-callback'\n\n/** Where the pod template projects that token, and where the shim reads it from. */\nexport const SHIM_IDENTITY_TOKEN_PATH = '/var/run/ac-identity/token'\n\n/** Port the in-sandbox shim listens on for daemon dial-in. */\nexport const SHIM_LISTEN_PORT_ENV = 'AC_SHIM_PORT'\nexport const DEFAULT_SHIM_LISTEN_PORT = 8085\n\n/** Root the sandbox permits filesystem work inside — the mounted agent volume. Also non-secret,\n * and fixed by the image rather than chosen per request, since the shim uses it to refuse a\n * cwd that escapes it. */\nexport const SHIM_WORKSPACE_ROOT_ENV = 'AC_SHIM_WORKSPACE_ROOT'\n\n/** The shim's own fallback for that env, and the daemon's assumption for a legacy shim that\n * predates workspace-root reporting — every such image mounted the volume here. */\nexport const DEFAULT_SHIM_WORKSPACE_ROOT = '/agent'\n\n/** `cluster-skills-v2` admits the widened skill manifest; a v1-only shim still gets the narrow one. */\nexport const ShimFeatureSchema = z.enum(['cluster-skills-v1', 'cluster-skills-v2'])\nexport type ShimFeature = z.infer<typeof ShimFeatureSchema>\n\n/** Operations the daemon may ask a bound shim to perform. Every one is authorized\n * individually against the binding's grants — a channel is not a blanket permission.\n * The bodies land in #814 / #815; this is the authorization vocabulary they use. */\nexport const ShimCapabilitySchema = z.enum([\n /** Write daemon-materialized files (secrets, config files) into the sandbox. */\n 'materialize',\n /** Run a command in the sandbox and return a structured result (workspace git). */\n 'exec',\n /** Read a bounded file back out (BFF workspace reads). */\n 'read',\n /** Proxy an in-pod unix socket back to a daemon-side server (gitcred, gh, MCP). */\n 'tunnel',\n /** Run the ACP runtime and relay its stdio as a stream (its own channel: ACP is already\n * a complete protocol, and reinterpreting it here would add a second place to break). */\n 'acp',\n /** Run the merge-when-ready watcher in the pod, so the armed set lives and dies with the\n * sandbox. Its own capability rather than a widening of `exec`: that channel is git-only and\n * enforced in-pod on purpose, and reaching `gh` through it would turn a deliberate boundary\n * into an arbitrary-execution surface. */\n 'automerge',\n /** Install daemon-acquired immutable skills into this pod's workspace. */\n 'skills',\n /** The same channel at the widened manifest limits — all the daemon learns from `cluster-skills-v2`. */\n 'skills-wide',\n /** Report which runtimes this image actually provides, by asking them. The daemon cannot learn\n * this any other way: `--k8s` runs no local runtime, and anything it states from its own\n * configuration is a claim about an image it never opened. */\n 'probe'\n])\nexport type ShimCapability = z.infer<typeof ShimCapabilitySchema>\n\n/** The daemon opens a dial-in channel with the launch it expects to bind. */\nexport const ShimDialHelloSchema = z.object({\n type: z.literal('shim/hello'),\n agentId: z.string().min(1),\n generation: z.number().int().nonnegative()\n})\n\n/** The shim answers the dialer's hello by proving which pod accepted it. */\nexport const ShimIdentitySchema = z.object({\n type: z.literal('shim/identity'),\n /** Projected ServiceAccount token, audience-restricted to {@link SHIM_TOKEN_AUDIENCE}. */\n token: z.string().min(1),\n /** Shim build, for operator diagnosis only — never an authorization input. */\n shimVersion: z.string().max(64).optional(),\n /** This pod's workspace mount; absent on legacy shims means {@link DEFAULT_SHIM_WORKSPACE_ROOT}. */\n workspaceRoot: z.string().min(1).max(4096).optional(),\n /** Versioned optional surfaces supported by this image; absent means a legacy shim. */\n features: z.array(ShimFeatureSchema).max(16).optional()\n})\n\n/** The daemon's answer once the token is verified and mapped to a spawn record. */\nexport const ShimBoundSchema = z.object({\n type: z.literal('shim/bound'),\n /** Short-TTL credential for subsequent frames, bound to this pod and generation. */\n sessionCredential: z.string().min(1),\n /** Seconds until the credential must be re-obtained by re-handshaking. */\n expiresInSeconds: z.number().int().positive(),\n agentId: z.string().min(1),\n /** Monotonic per-agent spawn counter; frames from an older one are refused. */\n generation: z.number().int().nonnegative(),\n grants: z.array(ShimCapabilitySchema)\n})\n\nexport const ShimRejectedSchema = z.object({\n type: z.literal('shim/rejected'),\n /** Coarse, non-probing reason: never leaks which of several checks failed. */\n reason: z.enum(['unauthenticated', 'unknown_pod', 'stale_generation', 'unavailable']),\n message: z.string().max(200)\n})\n\n/** Every post-binding frame carries the credential and the generation it was issued\n * for, so a replayed frame from a previous pod incarnation is refused on arrival. */\nexport const ShimRequestSchema = z.object({\n type: z.literal('shim/request'),\n id: z.string().uuid(),\n sessionCredential: z.string().min(1),\n generation: z.number().int().nonnegative(),\n capability: ShimCapabilitySchema,\n /** Operation payload, shaped per capability by the channels that land later. */\n payload: z.unknown()\n})\n\n// Cancels an in-flight request by id. Carries credential and generation like every post-binding\n// frame, so a replayed cancel from a previous incarnation cannot kill a live request.\nexport const ShimCancelSchema = z.object({\n type: z.literal('shim/cancel'),\n id: z.string().uuid(),\n sessionCredential: z.string().min(1),\n generation: z.number().int().nonnegative(),\n reason: z.string().max(200).optional()\n})\n\nexport const ShimResponseSchema = z.object({\n type: z.literal('shim/response'),\n id: z.string().uuid(),\n ok: z.boolean(),\n payload: z.unknown().optional(),\n error: z.string().max(500).optional()\n})\n\n/** A recurring event on an open stream. Unlike a response, many arrive per request: an ACP\n * runtime emits stdout continuously and exits once, and neither fits one-shot correlation. */\nexport const ShimEventSchema = z.object({\n type: z.literal('shim/event'),\n /** The stream this belongs to: the request id that opened it, or — for a tunnel, whose\n * connections are opened by a process inside the pod — an id the shim mints and announces. */\n streamId: z.string().uuid(),\n event: z.discriminatedUnion('kind', [\n /** A process in the sandbox connected to a tunnel's socket; the daemon dials its own end. */\n z.object({ kind: z.literal('connect'), tunnel: TunnelNameSchema }),\n z.object({ kind: z.literal('chunk'), data: z.string() }),\n z.object({\n kind: z.literal('exit'),\n code: z.number().int().nullable(),\n signal: z.string().nullable(),\n error: z.string().max(500).optional()\n })\n ])\n})\n\nexport const ShimFrameSchema = z.union([\n ShimDialHelloSchema,\n ShimIdentitySchema,\n ShimBoundSchema,\n ShimRejectedSchema,\n ShimRequestSchema,\n ShimCancelSchema,\n ShimResponseSchema,\n ShimEventSchema\n])\n\nexport type ShimBound = z.infer<typeof ShimBoundSchema>\nexport type ShimRejected = z.infer<typeof ShimRejectedSchema>\nexport type ShimRequest = z.infer<typeof ShimRequestSchema>\nexport type ShimCancel = z.infer<typeof ShimCancelSchema>\nexport type ShimResponse = z.infer<typeof ShimResponseSchema>\nexport type ShimEvent = z.infer<typeof ShimEventSchema>\nexport type ShimFrame = z.infer<typeof ShimFrameSchema>\nexport type ShimDialHello = z.infer<typeof ShimDialHelloSchema>\nexport type ShimIdentity = z.infer<typeof ShimIdentitySchema>\n\n/** Parse an inbound frame, returning undefined rather than throwing: a malformed frame\n * from a half-trusted peer is a close-the-connection event, not an exception path. */\nexport function parseShimFrame(text: string): ShimFrame | undefined {\n try {\n const result = ShimFrameSchema.safeParse(JSON.parse(text))\n return result.success ? result.data : undefined\n } catch {\n return undefined\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,kBAAb,cAAqC,MAAM;CACzC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,gCAAb,cAAmD,MAAM;CACvD,SAAkB;CAClB,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;AAkDA,SAAgB,kBAAkB,KAAuB;CACvD,IAAI,WAAW,GAAG,GAAG,MAAM,IAAI,gBAAgB,gCAAgC;CAC/E,MAAM,QAAQ,IAAI,MAAM,QAAQ,CAAC,CAAC,QAAQ,SAAS,SAAS,MAAM,SAAS,GAAG;CAC9E,IAAI,MAAM,MAAM,SAAS,SAAS,QAAQ,KAAK,SAAS,IAAI,CAAC,GAC3D,MAAM,IAAI,gBAAgB,8BAA8B;CAE1D,OAAO;AACT;AAEA,SAAS,QAAQ,KAAc,MAAuB;CACpD,OAAQ,KAAsC,SAAS;AACzD;;AAGA,SAAS,SAAS,KAAuB;CACvC,OAAO,QAAQ,KAAK,QAAQ,KAAM,QAAQ,aAAa,WAAW,QAAQ,KAAK,OAAO;AACxF;AAEA,SAAS,MAAM,MAAc,MAAuB;CAClD,MAAM,MAAM,SAAS,MAAM,IAAI;CAC/B,OAAO,QAAQ,MAAO,QAAQ,QAAQ,CAAC,IAAI,WAAW,KAAK,KAAK,KAAK,CAAC,WAAW,GAAG;AACtF;AAEA,SAAS,gBAAgB,GAAU,GAAmB;CACpD,OAAO,EAAE,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE;AAClG;;;;;;AAOA,eAAe,aAAa,MAAc,QAAgC;CACxE,IAAI;EACF,MAAMA,SAAI,MAAM,IAAI;CACtB,SAAS,KAAK;EACZ,IAAI,CAAC,SAAS,GAAG,KAAK,QAAQ,MAAM;EACpC,OAAO;CACT;CACA,MAAM,IAAI,gBAAgB,uCAAuC;AACnE;;;;;AAMA,eAAe,cAAc,MAAc,OAAiB,QAAyC;CACnG,IAAI;CACJ,IAAI;EACF,WAAW,MAAMA,SAAI,SAAS,IAAI;CACpC,SAAS,KAAK;EACZ,IAAI,CAAC,QAAQ,KAAK,QAAQ,GAAG,MAAM;EACnC,IAAI,CAAC,QAAQ,OAAO;EACpB,MAAMA,SAAI,MAAM,MAAM,EAAE,WAAW,KAAK,CAAC;EACzC,WAAW,MAAMA,SAAI,SAAS,IAAI;CACpC;CACA,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,YAAY,KAAK,QAAQ,IAAI;EACnC,IAAI;EACJ,IAAI;GACF,OAAO,MAAMA,SAAI,MAAM,SAAS;EAClC,SAAS,KAAK;GACZ,IAAI,CAAC,QAAQ;IACX,IAAI,CAAC,SAAS,GAAG,GAAG,MAAM;IAC1B,OAAO;GACT;GACA,IAAI,CAAC,QAAQ,KAAK,QAAQ,GAAG,MAAM;GACnC,IAAI;IACF,MAAMA,SAAI,MAAM,SAAS;GAC3B,SAAS,UAAU;IACjB,IAAI,CAAC,QAAQ,UAAU,QAAQ,GAAG,MAAM;GAC1C;GACA,OAAO,MAAMA,SAAI,MAAM,SAAS;EAClC;EACA,IAAI,CAAC,KAAK,YAAY,GAAG,MAAM,IAAI,gBAAgB,iDAAiD;EACpG,IAAI;GACF,SAAS,MAAMA,SAAI,SAAS,SAAS;EACvC,SAAS,KAAK;GAEZ,IAAI,CAAC,SAAS,GAAG,KAAK,QAAQ,MAAM;GACpC,OAAO;EACT;EACA,IAAI,CAAC,MAAM,UAAU,MAAM,GAAG,OAAO,aAAa,WAAW,MAAM;CACrE;CACA,OAAO;AACT;;AAKA,eAAe,gBAAgB,QAAgB,WAA6B,QAA8B;CACxG,IAAI;CACJ,IAAI;EACF,SAAS,MAAMA,SAAI,KAAK,QAAQ,UAAU,WAAW,UAAU,aAAa,UAAU,UAAU;CAClG,SAAS,KAAK;EACZ,IAAI,QAAQ,KAAK,QAAQ,GAAG,OAAO;GAAE,SAAS;GAAO,QAAQ;EAAG;EAChE,IAAI,QAAQ,KAAK,OAAO,GAAG,MAAM,IAAI,gBAAgB,qCAAqC;EAC1F,MAAM;CACR;CACA,IAAI;EACF,MAAM,OAAO,MAAM,OAAO,KAAK;EAC/B,IAAI,CAAC,KAAK,OAAO,GAAG,MAAM,IAAI,gBAAgB,qCAAqC;EACnF,OAAO;GAAE,SAAS;GAAM,QAAQ,MAAM,OAAO,SAAS,QAAQ;GAAG;EAAK;CACxE,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;AACF;;AAGA,eAAsB,wBAAwB,MAAc,aAAsC;CAChG,MAAM,QAAQ,eAAe,MAAM,WAAW;CAC9C,MAAM,SAAS,MAAM,cAAc,MAAM,MAAM,MAAM,GAAG,EAAE,GAAG,KAAK;CAClE,IAAI,WAAW,MAAM,OAAO;CAC5B,QAAQ,MAAM,gBAAgB,KAAK,QAAQ,MAAM,MAAM,SAAS,EAAG,CAAC,EAAA,CAAG;AACzE;;AAGA,SAAS,eAAe,MAAc,aAA+B;CACnE,MAAM,cAAc,QAAQ,IAAI;CAChC,MAAM,gBAAgB,QAAQ,WAAW;CACzC,IAAI,CAAC,MAAM,aAAa,aAAa,KAAK,kBAAkB,aAC1D,MAAM,IAAI,gBAAgB,8BAA8B;CAE1D,OAAO,SAAS,aAAa,aAAa,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;AACvE;;;;;;AAOA,eAAsB,+BACpB,MACA,aACA,SACA,cACA,MAC2B;CAC3B,MAAM,QAAQ,eAAe,MAAM,WAAW;CAC9C,MAAM,SAAU,MAAM,cAAc,MAAM,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI;CAClE,MAAM,SAAS,KAAK,QAAQ,MAAM,MAAM,SAAS,EAAG;CACpD,MAAM,UAAU,MAAM,gBAAgB,MAAM;CAC5C,IAAI,gBAAgB,QAAQ,MAAM,MAAM,YAAY,MAAM,cACxD,MAAM,IAAI,oBAAoB,6DAA6D;CAE7F,MAAM,OAAO,KAAK,QAAQ,wBAAwB,WAAW,EAAE,KAAK;CACpE,IAAI;EACF,IAAK,MAAMA,SAAI,SAAS,MAAM,MAAO,QAAQ,MAAM,aAAa,QAAQ,IAAI;EAC5E,MAAMA,SAAI,UAAU,MAAM,SAAS;GAAE,UAAU;GAAQ,MAAM;GAAM,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;EAAG,CAAC;EAC5G,IAAI,gBAAgB,QAAQ,MAAM;GAChC,IAAI;GACJ,IAAI;IACF,SAAS,MAAMA,SAAI,MAAM,MAAM;GACjC,SAAS,KAAK;IACZ,IAAI,QAAQ,KAAK,QAAQ,GACvB,MAAM,IAAI,oBAAoB,6DAA6D;IAE7F,MAAM;GACR;GACA,IAAI,CAAC,gBAAgB,QAAQ,MAAM,MAAM,GACvC,MAAM,IAAI,oBAAoB,6DAA6D;EAE/F;EACA,IAAK,MAAMA,SAAI,SAAS,MAAM,MAAO,QAAQ,MAAM,aAAa,QAAQ,IAAI;EAC5E,MAAM,kBAAkB,MAAM,MAAM;CACtC,UAAU;EACR,MAAMA,SAAI,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CACpD;CACA,MAAM,OAAO,MAAMA,SAAI,MAAM,MAAM;CACnC,IAAI,CAAC,KAAK,OAAO,GAAG,MAAM,IAAI,gBAAgB,qCAAqC;CACnF,OAAO;EAAE,MAAM,KAAK;EAAM,OAAO,KAAK,MAAM,YAAY;CAAE;AAC5D;AAKA,eAAe,kBAAkB,MAAc,QAA+B;CAC5E,IAAI,QAAQ,aAAa,SAAS,OAAOA,SAAI,OAAO,MAAM,MAAM;CAChE,MAAM,4BAAY,IAAI,IAAI;EAAC;EAAS;EAAU;CAAO,CAAC;CACtD,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,OAAO,MAAMA,SAAI,OAAO,MAAM,MAAM;CACtC,SAAS,OAAO;EACd,MAAM,OAAQ,MAAgC;EAC9C,IAAI,YAAY,KAAK,SAAS,KAAA,KAAa,CAAC,UAAU,IAAI,IAAI,GAAG,MAAM;EACvE,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,MAAM,UAAU,EAAE,CAAC;CACxE;AAEJ;;AAGA,IAAa,gBAAb,MAAa,cAAkC;CAC7C;CACA;CAEA,YAAY,MAAc;EACxB,KAAK,OAAO,QAAQ,IAAI;EACxB,KAAK,MAAM,KAAK;CAClB;CAEA,OAAO,KAAuB;EAC5B,OAAO,IAAI,cAAc,KAAK,KAAK,MAAM,GAAG,kBAAkB,GAAG,CAAC,CAAC;CACrE;CAEA,KAAa,KAAgD;EAC3D,MAAM,QAAQ,kBAAkB,GAAG;EACnC,IAAI,MAAM,WAAW,GAAG,MAAM,IAAI,gBAAgB,yBAAyB;EAC3E,OAAO;GAAE,OAAO,MAAM,MAAM,GAAG,EAAE;GAAG,MAAM,MAAM,MAAM,SAAS;EAAI;CACrE;CAEA,MAAM,SAAS,KAAa,WAA6B,QAAsC;EAC7F,MAAM,EAAE,OAAO,SAAS,KAAK,KAAK,GAAG;EACrC,MAAM,SAAS,MAAM,cAAc,KAAK,MAAM,OAAO,KAAK;EAC1D,IAAI,WAAW,MAAM,OAAO;EAC5B,MAAM,UAAU,MAAM,gBAAgB,KAAK,QAAQ,IAAI,GAAG,QAAQ;EAClE,IAAI,CAAC,QAAQ,SAAS,OAAO;EAC7B,OAAO;GAAE,SAAS,QAAQ;GAAQ,MAAM,QAAQ,KAAK;GAAM,OAAO,QAAQ,KAAK,MAAM,YAAY;EAAE;CACrG;CAEA,UAAU,KAAa,SAA8B,UAAgC,CAAC,GAA8B;EAClH,MAAM,QAAQ,kBAAkB,GAAG;EACnC,IAAI,MAAM,WAAW,GAAG,MAAM,IAAI,gBAAgB,yBAAyB;EAC3E,OAAO,+BACL,KAAK,MACL,KAAK,KAAK,MAAM,GAAG,KAAK,GACxB,SACA,QAAQ,cACR,QAAQ,IACV;CACF;CAEA,MAAM,QAAQ,KAAuC;EACnD,MAAM,MAAM,MAAM,cAAc,KAAK,MAAM,kBAAkB,GAAG,GAAG,KAAK;EACxE,IAAI,QAAQ,MAAM,OAAO,CAAC;EAC1B,IAAI;EACJ,IAAI;GACF,UAAU,MAAMA,SAAI,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;EAC1D,SAAS,KAAK;GACZ,IAAI,QAAQ,KAAK,QAAQ,KAAK,QAAQ,KAAK,SAAS,GAAG,OAAO,CAAC;GAC/D,MAAM;EACR;EACA,MAAM,UAA2B,CAAC;EAClC,KAAK,MAAM,KAAK,SAAS;GACvB,MAAM,OAA8B,EAAE,YAAY,IAAI,QAAQ,EAAE,OAAO,IAAI,SAAS;GACpF,MAAM,QAAuB;IAAE,MAAM,EAAE;IAAM;GAAK;GAClD,IAAI,SAAS,QACX,IAAI;IACF,MAAM,KAAK,MAAMA,SAAI,MAAM,KAAK,KAAK,EAAE,IAAI,CAAC;IAC5C,MAAM,OAAO,GAAG;IAChB,MAAM,QAAQ,GAAG,MAAM,YAAY;GACrC,QAAQ,CAER;GAEF,QAAQ,KAAK,KAAK;EACpB;EACA,OAAO;CACT;CAEA,MAAM,MAAM,KAA4B;EACtC,MAAM,cAAc,KAAK,MAAM,kBAAkB,GAAG,GAAG,IAAI;CAC7D;CAEA,MAAM,OAAO,MAAc,IAA8B;EACvD,MAAM,SAAS,KAAK,KAAK,IAAI;EAC7B,MAAM,eAAe,MAAM,cAAc,KAAK,MAAM,OAAO,OAAO,KAAK;EACvE,IAAI,iBAAiB,MAAM,OAAO;EAClC,MAAM,aAAa,KAAK,cAAc,OAAO,IAAI;EACjD,IAAI;GACF,MAAMA,SAAI,MAAM,UAAU;EAC5B,SAAS,KAAK;GACZ,IAAI,QAAQ,KAAK,QAAQ,GAAG,OAAO;GACnC,MAAM;EACR;EACA,MAAM,SAAS,KAAK,KAAK,EAAE;EAC3B,MAAM,eAAgB,MAAM,cAAc,KAAK,MAAM,OAAO,OAAO,IAAI;EACvE,MAAMA,SAAI,OAAO,YAAY,KAAK,cAAc,OAAO,IAAI,CAAC;EAC5D,OAAO;CACT;CAEA,MAAM,GAAG,KAA4B;EACnC,MAAM,EAAE,OAAO,SAAS,KAAK,KAAK,GAAG;EACrC,MAAM,SAAS,MAAM,cAAc,KAAK,MAAM,OAAO,KAAK;EAC1D,IAAI,WAAW,MAAM;EACrB,MAAMA,SAAI,GAAG,KAAK,QAAQ,IAAI,GAAG;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACnE;CAEA,MAAM,OAAO,KAAa,OAA8B;EACtD,MAAM,EAAE,OAAO,SAAS,KAAK,KAAK,GAAG;EACrC,MAAM,SAAS,MAAM,cAAc,KAAK,MAAM,OAAO,KAAK;EAC1D,IAAI,WAAW,MAAM;EACrB,MAAM,OAAO,IAAI,KAAK,KAAK;EAC3B,IAAI;GACF,MAAMA,SAAI,QAAQ,KAAK,QAAQ,IAAI,GAAG,MAAM,IAAI;EAClD,QAAQ,CAER;CACF;AACF;;;;;;;AAaA,SAAgB,gBACd,OACA,SACU;CACV,IAAI,CAAC,SAAS,OAAO,IAAI,cAAc,MAAM,GAAG;CAChD,MAAM,KAAK,QAAQ,YAAY,MAAM,EAAE;CACvC,IAAI,CAAC,IACH,MAAM,IAAI,8BACR,UAAU,MAAM,GAAG,0DACrB;CAEF,OAAO;AACT;;;;;;;;;;;;;AC9ZA,MAAa,gCAAgC;;;;AAsB7C,MAAa,yBAAyB;;;;;;;;;;AActC,MAAa,uBAAuB;AAYpC,MAAaC,yBAAoE,OAAO,OAAO;CAC7F,SAAS;CACT,KAAK;AACP,CAAC;;;;;;;;;;;;AClDD,MAAa,mBAAmBC,MAAO,CAAC,WAAW,KAAK,CAAC;;;AAKzD,MAAa,yBAAyB,KAAK;AAC3C,MAAM,0BAA0B,KAAK,KAAK,yBAAyB,CAAC,IAAI;AAoCrCI,mBAAqB,MAAM;CAxB5BH,OAAS;EACzC,IAAIC,QAAU,QAAQ;EACtB,QAAQ;CACV,CAqB+D;CAb/BD,OAAS;EACvC,IAAIC,QAAU,MAAM;EACpB,UAAUC,OAAS,CAAC,CAAC,KAAK;;EAE1B,OAAOA,OAAS,CAAC,CAAC,IAAI,uBAAuB;CAC/C,CAQmF;CANlDF,OAAS;EACxC,IAAIC,QAAU,OAAO;EACrB,UAAUC,OAAS,CAAC,CAAC,KAAK;EAC1B,OAAOA,OAAS,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS;CACtC,CAEqG;AAAiB,CAAC;;AAIvH,MAAa,wBAAwBF,OAAS,EAAE,YAAYE,OAAS,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;;;;;;;AAU/E,MAAa,uBAA6DE;;;;;;;;;;;;ACJ1E,IAAa,oBAAb,cAAuC,MAAM;CAGhC;CAFX,YACE,SACA,OACA;EACA,MAAM,OAAO;EAFJ,KAAA,QAAA;EAGT,KAAK,OAAO;CACd;AACF;;;AAiCA,MAAM,kBAAkB;;;;;;;;AAYxB,IAAa,iBAAb,MAAa,eAAoC;CAQ5B;CACA;CACA;CACA;CAJnB,YACE,KACA,KACA,MACA,MAA+C,CAAC,GAChD;EAJiB,KAAA,MAAA;EACA,KAAA,MAAA;EACA,KAAA,OAAA;EACA,KAAA,MAAA;CAChB;CAEH,QAAQ,KAAwC;EAS9C,OAAO,IAAI,eAAe,KAAK,KAAK,GAAG,GAAG,KAAK,KAAK,KAAK,MAAM,GAAG;CACpE;CAEA,MAAM,IAAI,MAAiC;EACzC,OAAO,KAAK,IAAI,IAAI,IAAI;CAC1B;CAEA,YAAY,MAAgB,UAA+D;EAKzF,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,SACE,OACA,MACA;IACE,GAAI,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;IACpC,KAAK,EAAE,GAAG,KAAK,IAAI;IACnB,UAAU;IACV,WAAW;IACX,SAAS;IACT,aAAa;GACf,IACC,KAAK,WAAW;IACf,IAAI,CAAC,KAAK,OAAO,QAAQ;KAAE,KAAK;KAAQ,UAAU;IAAM,CAAC;IAEzD,IAAK,IAA8B,SAAS,qCAC1C,OAAO,QAAQ;KAAE,KAAK;KAAQ,UAAU;IAAK,CAAC;IAEhD,OAAO,GAAG;GACZ,CACF;EACF,CAAC;CACH;CAEA,MAAM,MAAM,MAAc,QAAgB,UAAoB,CAAC,GAAkB;EAC/E,MAAM,KAAK,IAAI,MAAM,MAAM,QAAQ,OAAO;CAC5C;CAEA,MAAM,KAAK,QAAgB,QAAgB,UAAoB,CAAC,GAA4B;EAC1F,MAAM,SAAS,MAAM,KAAK,IAAI,KAAK,QAAQ,QAAQ,OAAO;EAC1D,OAAO;GACL,OAAO,CAAC,GAAG,OAAO,KAAK;GACvB,YAAY,OAAO,QAAQ;GAC3B,WAAW,OAAO,QAAQ;EAC5B;CACF;CAEA,MAAM,SAAoC;EACxC,MAAM,UAAU,MAAM,KAAK,IAAI,OAAO;EACtC,OAAO;GACL,SAAS,QAAQ,WAAW;GAC5B,UAAU,QAAQ,YAAY;GAC9B,OAAO,QAAQ;GACf,QAAQ,QAAQ;GAChB,OAAO,QAAQ,MAAM,KAAK,UAAU;IAClC,MAAM,KAAK;IACX,OAAO,KAAK;IACZ,aAAa,KAAK;GACpB,EAAE;GACF,OAAO,QAAQ,QAAQ;EACzB;CACF;CAEA,MAAM,IAAI,SAAuD;EAK/D,QAAO,MAJc,KAAK,IAAI,IAAI;GAChC,UAAU,QAAQ;GAClB,QAAQ;IAAE,MAAM;IAAM,MAAM;IAAO,SAAS;GAAK;EACnD,CAAC,EAAA,CACa,IAAI,KAAK,WAAW;GAChC,MAAM,MAAM;GACZ,SAAS,MAAM,WAAW;GAC1B,aAAa,MAAM,QAAQ;EAC7B,EAAE;CACJ;AACF;;;;ACpNA,MAAa,mBAAmB;AAChC,MAAa,eAAe;;;;AAK5B,MAAa,sBAAsB;AAOnC,MAAa,2BAA2B;;;AASxC,MAAa,8BAA8B;;AAG3C,MAAa,oBAAoBC,MAAO,CAAC,qBAAqB,mBAAmB,CAAC;;;;AAMlF,MAAa,uBAAuBA,MAAO;CAEzC;CAEA;CAEA;CAEA;CAGA;CAKA;CAEA;CAEA;CAIA;AACF,CAAC;AA6FD,MAAa,kBAAkBS,MAAQ;CAzFJR,OAAS;EAC1C,MAAMC,QAAU,YAAY;EAC5B,SAASC,OAAS,CAAC,CAAC,IAAI,CAAC;EACzB,YAAYC,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;CAC3C,CAsFE;CAnFgCH,OAAS;EACzC,MAAMC,QAAU,eAAe;;EAE/B,OAAOC,OAAS,CAAC,CAAC,IAAI,CAAC;;EAEvB,aAAaA,OAAS,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS;;EAEzC,eAAeA,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,SAAS;;EAEpD,UAAUE,MAAQ,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS;CACxD,CA0EE;CAvE6BJ,OAAS;EACtC,MAAMC,QAAU,YAAY;;EAE5B,mBAAmBC,OAAS,CAAC,CAAC,IAAI,CAAC;;EAEnC,kBAAkBC,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;EAC5C,SAASD,OAAS,CAAC,CAAC,IAAI,CAAC;;EAEzB,YAAYC,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;EACzC,QAAQC,MAAQ,oBAAoB;CACtC,CA8DE;CA5DgCJ,OAAS;EACzC,MAAMC,QAAU,eAAe;;EAE/B,QAAQF,MAAO;GAAC;GAAmB;GAAe;GAAoB;EAAa,CAAC;EACpF,SAASG,OAAS,CAAC,CAAC,IAAI,GAAG;CAC7B,CAwDE;CApD+BF,OAAS;EACxC,MAAMC,QAAU,cAAc;EAC9B,IAAIC,OAAS,CAAC,CAAC,KAAK;EACpB,mBAAmBA,OAAS,CAAC,CAAC,IAAI,CAAC;EACnC,YAAYC,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;EACzC,YAAY;;EAEZ,SAASE,QAAU;CACrB,CA6CE;CAzC8BL,OAAS;EACvC,MAAMC,QAAU,aAAa;EAC7B,IAAIC,OAAS,CAAC,CAAC,KAAK;EACpB,mBAAmBA,OAAS,CAAC,CAAC,IAAI,CAAC;EACnC,YAAYC,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;EACzC,QAAQD,OAAS,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS;CACvC,CAoCE;CAlCgCF,OAAS;EACzC,MAAMC,QAAU,eAAe;EAC/B,IAAIC,OAAS,CAAC,CAAC,KAAK;EACpB,IAAII,QAAU;EACd,SAASD,QAAU,CAAC,CAAC,SAAS;EAC9B,OAAOH,OAAS,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS;CACtC,CA6BE;CAzB6BF,OAAS;EACtC,MAAMC,QAAU,YAAY;;;EAG5B,UAAUC,OAAS,CAAC,CAAC,KAAK;EAC1B,OAAOK,mBAAqB,QAAQ;GAElCP,OAAS;IAAE,MAAMC,QAAU,SAAS;IAAG,QAAQ;GAAiB,CAAC;GACjED,OAAS;IAAE,MAAMC,QAAU,OAAO;IAAG,MAAMC,OAAS;GAAE,CAAC;GACvDF,OAAS;IACP,MAAMC,QAAU,MAAM;IACtB,MAAME,OAAS,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;IAChC,QAAQD,OAAS,CAAC,CAAC,SAAS;IAC5B,OAAOA,OAAS,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS;GACtC,CAAC;EACH,CAAC;CACH,CAUE;AACF,CAAC;;;AAcD,SAAgB,eAAe,MAAqC;CAClE,IAAI;EACF,MAAM,SAAS,gBAAgB,UAAU,KAAK,MAAM,IAAI,CAAC;EACzD,OAAO,OAAO,UAAU,OAAO,OAAO,KAAA;CACxC,QAAQ;EACN;CACF;AACF"}
1
+ {"version":3,"file":"protocol-CWizQiPF.js","names":["fsp","SANDBOX_TUNNEL_PATHS","z.enum","z.object","z.literal","z.string","z.discriminatedUnion","SANDBOX_TUNNEL_PATH_SOURCE","z.enum","z.object","z.literal","z.string","z.number","z.array","z.unknown","z.boolean","z.discriminatedUnion","z.union"],"sources":["../src/memory/fs.ts","../src/shim/sandbox-paths.ts","../src/shim/tunnel.ts","../src/workspace/git-runner.ts","../src/shim/protocol.ts"],"sourcesContent":["/**\n * `MemoryFs` — the file-system port an agent's managed memory tree is kept behind.\n *\n * Every managed-memory writer and reader (`memory/store.ts`, the memory provider, the dream\n * runner, the CP memory reader) is a DIRECTORY abstraction over this port, so where the tree lives\n * is a placement decision, not a policy one: a local agent's home is `<agent.dir>` on this daemon's\n * disk (`LocalMemoryFs`), a cluster agent's is one root on its sandbox volume reached through the\n * shim (`shim/memory-fs-channel.ts`), and a later home is another implementation. Paths are relative\n * to the root; the root itself is absolute in the coordinates of the filesystem that holds it.\n *\n * SECURITY (local): the daemon is outside the agent's sandbox, so a symlink planted in the writable\n * memory dir must not redirect a read or a write. Every operation canonicalises the parent chain one\n * component at a time, rejects symlink components, and opens the leaf with `O_NOFOLLOW`; writes\n * publish through a random exclusive temp file. The shim executor keeps the same rules against open\n * descriptors where the volume is written by the agent's runtime.\n */\nimport { randomUUID } from 'node:crypto'\nimport { constants, promises as fsp, type Stats } from 'node:fs'\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path'\n\n/** Raised when a memory path escapes its root or resolves through a symlink. Surfaces as `BAD_PAYLOAD`. */\nexport class MemoryPathError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MemoryPathError'\n }\n}\n\n/** Raised when a write exceeds the memory file cap. `BAD_PAYLOAD`. */\nexport class MemoryTooLargeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MemoryTooLargeError'\n }\n}\n\n/** Raised when an `ifMatchMtime` precondition fails (the file changed under the writer). Surfaces as CONFLICT. */\nexport class MemoryConflictError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MemoryConflictError'\n }\n}\n\n/** Raised when a cluster agent's memory home is on a sandbox that is not running — one resolution, no local fallback. */\nexport class MemorySandboxUnavailableError extends Error {\n readonly reason = 'sandbox-unavailable' as const\n constructor(message: string) {\n super(message)\n this.name = 'MemorySandboxUnavailableError'\n }\n}\n\nexport interface MemoryFsFileStat {\n size: number\n /** ISO mtime — the optimistic-concurrency token every memory writer compares. */\n mtime: string\n}\n\nexport interface MemoryFsFile extends MemoryFsFileStat {\n /** The file's text, or its bytes as base64 when the read asked for that encoding. */\n content: string\n}\n\nexport type MemoryFsEncoding = 'utf8' | 'base64'\n\nexport interface MemoryFsEntry {\n name: string\n kind: 'file' | 'dir' | 'other'\n size?: number\n mtime?: string\n}\n\nexport interface MemoryFsWriteOptions {\n /** Non-empty ⇒ the target's current mtime must equal it (a brand-new file never matches). */\n ifMatchMtime?: string\n mode?: number\n}\n\n/** The port. Paths are root-relative; a missing path is data (`null` / `[]` / `false`), never an error. */\nexport interface MemoryFs {\n /** Identity of the tree for the in-process locks and write ledger — equal for every instance over one tree. */\n readonly key: string\n /** Absolute root in the coordinates of the filesystem holding it (an execution cwd is built from it). */\n readonly root: string\n /** The same port re-rooted at a subdirectory (a channel's self-contained memory root). */\n subdir(rel: string): MemoryFs\n readFile(rel: string, encoding?: MemoryFsEncoding): Promise<MemoryFsFile | null>\n /** Atomic replace-or-create; the leaf is never followed and parents are created. */\n writeFile(rel: string, content: string | Uint8Array, options?: MemoryFsWriteOptions): Promise<MemoryFsFileStat>\n readdir(rel: string): Promise<MemoryFsEntry[]>\n mkdir(rel: string): Promise<void>\n /** false when `from` is absent. */\n rename(from: string, to: string): Promise<boolean>\n /** Recursive and forced: absence is fine. */\n rm(rel: string): Promise<void>\n /** Best-effort: set a file's mtime (kept for files a store swap left byte-for-byte unchanged). */\n utimes(rel: string, mtime: string): Promise<void>\n}\n\n/** Split a root-relative path into plain components; `''` is the root itself. */\nexport function memoryRelSegments(rel: string): string[] {\n if (isAbsolute(rel)) throw new MemoryPathError('absolute paths are not allowed')\n const parts = rel.split(/[\\\\/]+/).filter((part) => part !== '' && part !== '.')\n if (parts.some((part) => part === '..' || part.includes('\\0'))) {\n throw new MemoryPathError('path escapes the memory root')\n }\n return parts\n}\n\nfunction isErrno(err: unknown, code: string): boolean {\n return (err as NodeJS.ErrnoException | null)?.code === code\n}\n\n/** Dropped by a concurrent rm: ENOENT, or EPERM while Windows holds the directory in delete-pending. */\nfunction vanished(err: unknown): boolean {\n return isErrno(err, 'ENOENT') || (process.platform === 'win32' && isErrno(err, 'EPERM'))\n}\n\nfunction under(root: string, path: string): boolean {\n const rel = relative(root, path)\n return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))\n}\n\nfunction sameFileVersion(a: Stats, b: Stats): boolean {\n return b.isFile() && a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs\n}\n\n/**\n * A failed containment check is an escape only when the path is still there. Windows resolves a\n * component a concurrent rm already unlinked to a path outside the root, so re-probe before calling\n * a benign race a violation; a dropped component is absence, which is data on the read side.\n */\nasync function rejectEscape(path: string, create: boolean): Promise<null> {\n try {\n await fsp.lstat(path)\n } catch (err) {\n if (!vanished(err) || create) throw err\n return null\n }\n throw new MemoryPathError('path resolves outside the memory root')\n}\n\n/**\n * Canonicalise `parts` under `root` one component at a time, refusing symlink components; with\n * `create` missing components are made along the way. `null` when a component is absent (read side).\n */\nasync function walkContained(root: string, parts: string[], create: boolean): Promise<string | null> {\n let realRoot: string\n try {\n realRoot = await fsp.realpath(root)\n } catch (err) {\n if (!isErrno(err, 'ENOENT')) throw err\n if (!create) return null\n await fsp.mkdir(root, { recursive: true })\n realRoot = await fsp.realpath(root)\n }\n let parent = realRoot\n for (const part of parts) {\n const candidate = join(parent, part)\n let stat: Stats\n try {\n stat = await fsp.lstat(candidate)\n } catch (err) {\n if (!create) {\n if (!vanished(err)) throw err\n return null\n }\n if (!isErrno(err, 'ENOENT')) throw err\n try {\n await fsp.mkdir(candidate)\n } catch (mkdirErr) {\n if (!isErrno(mkdirErr, 'EEXIST')) throw mkdirErr\n }\n stat = await fsp.lstat(candidate)\n }\n if (!stat.isDirectory()) throw new MemoryPathError('memory path contains a symlink or non-directory')\n try {\n parent = await fsp.realpath(candidate)\n } catch (err) {\n // A concurrent rm can drop the component between lstat and realpath; absent stays data on the read side.\n if (!vanished(err) || create) throw err\n return null\n }\n if (!under(realRoot, parent)) return rejectEscape(candidate, create)\n }\n return parent\n}\n\ntype CurrentFile = { existed: false; before: ''; stat?: undefined } | { existed: true; before: string; stat: Stats }\n\n/** Open the leaf without following a symlink; `existed:false` on ENOENT. */\nasync function readCurrentFile(target: string, encoding: MemoryFsEncoding = 'utf8'): Promise<CurrentFile> {\n let handle\n try {\n handle = await fsp.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)\n } catch (err) {\n if (isErrno(err, 'ENOENT')) return { existed: false, before: '' }\n if (isErrno(err, 'ELOOP')) throw new MemoryPathError('memory target is not a regular file')\n throw err\n }\n try {\n const stat = await handle.stat()\n if (!stat.isFile()) throw new MemoryPathError('memory target is not a regular file')\n return { existed: true, before: await handle.readFile(encoding), stat }\n } finally {\n await handle.close()\n }\n}\n\n/** Read one file under `root` (a memory tree or the runtime's own store) without following symlinks; '' when absent. */\nexport async function readContainedMemoryFile(root: string, destination: string): Promise<string> {\n const parts = containedParts(root, destination)\n const parent = await walkContained(root, parts.slice(0, -1), false)\n if (parent === null) return ''\n return (await readCurrentFile(join(parent, parts[parts.length - 1]!))).before\n}\n\n/** `destination` must be a lexical descendant of `root`; returns its components. */\nfunction containedParts(root: string, destination: string): string[] {\n const lexicalRoot = resolve(root)\n const lexicalTarget = resolve(destination)\n if (!under(lexicalRoot, lexicalTarget) || lexicalTarget === lexicalRoot) {\n throw new MemoryPathError('path escapes the memory root')\n }\n return relative(lexicalRoot, lexicalTarget).split(sep).filter(Boolean)\n}\n\n/**\n * Atomically replace one file under `root` without following symlinks. The random `wx` temp defeats\n * pre-planted `<target>.tmp` links; a non-empty `ifMatchMtime` is checked before the temp write and\n * re-verified (dev/ino/size/mtime) right before the rename.\n */\nexport async function atomicWriteContainedMemoryFile(\n root: string,\n destination: string,\n content: string | Uint8Array,\n ifMatchMtime?: string,\n mode?: number\n): Promise<MemoryFsFileStat> {\n const parts = containedParts(root, destination)\n const parent = (await walkContained(root, parts.slice(0, -1), true))!\n const target = join(parent, parts[parts.length - 1]!)\n const current = await readCurrentFile(target)\n if (ifMatchMtime && current.stat?.mtime.toISOString() !== ifMatchMtime) {\n throw new MemoryConflictError('the memory file changed since it was read; reload and retry')\n }\n const temp = join(parent, `.agentconnect-memory-${randomUUID()}.tmp`)\n try {\n if ((await fsp.realpath(parent)) !== parent) await rejectEscape(parent, true)\n await fsp.writeFile(temp, content, { encoding: 'utf8', flag: 'wx', ...(mode === undefined ? {} : { mode }) })\n if (ifMatchMtime && current.stat) {\n let latest: Stats\n try {\n latest = await fsp.lstat(target)\n } catch (err) {\n if (isErrno(err, 'ENOENT')) {\n throw new MemoryConflictError('the memory file changed since it was read; reload and retry')\n }\n throw err\n }\n if (!sameFileVersion(current.stat, latest)) {\n throw new MemoryConflictError('the memory file changed since it was read; reload and retry')\n }\n }\n if ((await fsp.realpath(parent)) !== parent) await rejectEscape(parent, true)\n await publishOverTarget(temp, target)\n } finally {\n await fsp.rm(temp, { force: true }).catch(() => {})\n }\n const stat = await fsp.lstat(target)\n if (!stat.isFile()) throw new MemoryPathError('memory target is not a regular file')\n return { size: stat.size, mtime: stat.mtime.toISOString() }\n}\n\n// Windows cannot rename over a file another handle holds open — a scanner's transient handle on the\n// bytes just written is enough — so EPERM/EACCES/EBUSY here is a race POSIX never has. Bounded retry,\n// as `WorkspaceManager.renameWorkspaceDirectory` does for the same reason on a directory swap.\nasync function publishOverTarget(temp: string, target: string): Promise<void> {\n if (process.platform !== 'win32') return fsp.rename(temp, target)\n const transient = new Set(['EPERM', 'EACCES', 'EBUSY'])\n for (let attempt = 0; ; attempt++) {\n try {\n return await fsp.rename(temp, target)\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (attempt === 9 || code === undefined || !transient.has(code)) throw error\n await new Promise((resolve) => setTimeout(resolve, 10 * (attempt + 1)))\n }\n }\n}\n\n/** The port over this process's own filesystem, contained to `root`. */\nexport class LocalMemoryFs implements MemoryFs {\n readonly root: string\n readonly key: string\n\n constructor(root: string) {\n this.root = resolve(root)\n this.key = this.root\n }\n\n subdir(rel: string): MemoryFs {\n return new LocalMemoryFs(join(this.root, ...memoryRelSegments(rel)))\n }\n\n private leaf(rel: string): { parts: string[]; name: string } {\n const parts = memoryRelSegments(rel)\n if (parts.length === 0) throw new MemoryPathError('a file name is required')\n return { parts: parts.slice(0, -1), name: parts[parts.length - 1]! }\n }\n\n async readFile(rel: string, encoding: MemoryFsEncoding = 'utf8'): Promise<MemoryFsFile | null> {\n const { parts, name } = this.leaf(rel)\n const parent = await walkContained(this.root, parts, false)\n if (parent === null) return null\n const current = await readCurrentFile(join(parent, name), encoding)\n if (!current.existed) return null\n return { content: current.before, size: current.stat.size, mtime: current.stat.mtime.toISOString() }\n }\n\n writeFile(rel: string, content: string | Uint8Array, options: MemoryFsWriteOptions = {}): Promise<MemoryFsFileStat> {\n const parts = memoryRelSegments(rel)\n if (parts.length === 0) throw new MemoryPathError('a file name is required')\n return atomicWriteContainedMemoryFile(\n this.root,\n join(this.root, ...parts),\n content,\n options.ifMatchMtime,\n options.mode\n )\n }\n\n async readdir(rel: string): Promise<MemoryFsEntry[]> {\n const dir = await walkContained(this.root, memoryRelSegments(rel), false)\n if (dir === null) return []\n let dirents\n try {\n dirents = await fsp.readdir(dir, { withFileTypes: true })\n } catch (err) {\n if (isErrno(err, 'ENOENT') || isErrno(err, 'ENOTDIR')) return []\n throw err\n }\n const entries: MemoryFsEntry[] = []\n for (const d of dirents) {\n const kind: MemoryFsEntry['kind'] = d.isDirectory() ? 'dir' : d.isFile() ? 'file' : 'other'\n const entry: MemoryFsEntry = { name: d.name, kind }\n if (kind === 'file') {\n try {\n const st = await fsp.lstat(join(dir, d.name))\n entry.size = st.size\n entry.mtime = st.mtime.toISOString()\n } catch {\n // raced deletion — keep the name-only entry\n }\n }\n entries.push(entry)\n }\n return entries\n }\n\n async mkdir(rel: string): Promise<void> {\n await walkContained(this.root, memoryRelSegments(rel), true)\n }\n\n async rename(from: string, to: string): Promise<boolean> {\n const source = this.leaf(from)\n const sourceParent = await walkContained(this.root, source.parts, false)\n if (sourceParent === null) return false\n const sourcePath = join(sourceParent, source.name)\n try {\n await fsp.lstat(sourcePath)\n } catch (err) {\n if (isErrno(err, 'ENOENT')) return false\n throw err\n }\n const target = this.leaf(to)\n const targetParent = (await walkContained(this.root, target.parts, true))!\n await fsp.rename(sourcePath, join(targetParent, target.name))\n return true\n }\n\n async rm(rel: string): Promise<void> {\n const { parts, name } = this.leaf(rel)\n const parent = await walkContained(this.root, parts, false)\n if (parent === null) return\n await fsp.rm(join(parent, name), { recursive: true, force: true })\n }\n\n async utimes(rel: string, mtime: string): Promise<void> {\n const { parts, name } = this.leaf(rel)\n const parent = await walkContained(this.root, parts, false)\n if (parent === null) return\n const when = new Date(mtime)\n try {\n await fsp.lutimes(join(parent, name), when, when)\n } catch {\n // best-effort: a vanished file keeps whatever mtime it has\n }\n }\n}\n\n/** The sandbox plane as the factory sees it: the port over a bound sandbox volume, or nothing. */\nexport interface SandboxMemoryFsSource {\n memoryFsFor(agentId: string): MemoryFs | undefined\n}\n\n/**\n * The ONE decision about where an agent's managed memory tree lives. With a sandbox plane (every\n * agent of a `--k8s` daemon runs in a pod) it is the port over the agent's sandbox volume, reachable\n * exactly while the pod is bound — no fallback to this member's disk, since a duty move would leave\n * the memory behind; without one, the local port over the agent dir.\n */\nexport function resolveMemoryFs(\n agent: { id: string; dir: string },\n sandbox: SandboxMemoryFsSource | undefined\n): MemoryFs {\n if (!sandbox) return new LocalMemoryFs(agent.dir)\n const fs = sandbox.memoryFsFor(agent.id)\n if (!fs) {\n throw new MemorySandboxUnavailableError(\n `agent \"${agent.id}\" has no running sandbox, so its memory cannot be reached`\n )\n }\n return fs\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh and agent-browser wrappers. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Pod env naming the Chrome the image bakes — agent-browser's only browser-location hook, so an ACP child\n * without it downloads one of its own. Set by the image, projected onto the child by acp-runner. */\nexport const SANDBOX_BROWSER_EXECUTABLE_ENV = 'AGENT_BROWSER_EXECUTABLE_PATH'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","import { z } from 'zod'\nimport { SANDBOX_TUNNEL_PATHS as SANDBOX_TUNNEL_PATH_SOURCE } from './sandbox-paths.js'\n\n/**\n * Unix sockets the daemon exposes into a sandbox. Each is a daemon-side server the runtime\n * expects to find locally: the git-credential helper (which `gh`'s token helper shares) and\n * the MCP bridge. The shim listens on the in-pod path and proxies bytes back over the channel.\n *\n * The set is closed on purpose. A generic \"tunnel any socket\" capability would let a\n * compromised runtime reach whatever the daemon happens to be listening on; naming the\n * servers keeps the grant meaningful.\n */\nexport const TunnelNameSchema = z.enum(['gitcred', 'mcp'])\nexport type TunnelName = z.infer<typeof TunnelNameSchema>\n\n/** Bytes per chunk in either direction. One `shim/request` or `shim/event` carries at most one\n * chunk, so this has to leave room under `MAX_FRAME_BYTES` (256 KiB) after base64 expansion. */\nexport const MAX_TUNNEL_CHUNK_BYTES = 32 * 1024\nconst MAX_TUNNEL_CHUNK_BASE64 = Math.ceil(MAX_TUNNEL_CHUNK_BYTES / 3) * 4\n\n/**\n * Daemon → shim: serve this tunnel on its in-pod path. Idempotent per pod, because the\n * listener belongs to the POD and the channel does not — a credential renewal replaces the\n * socket underneath while every in-pod client keeps its connection.\n *\n * It deliberately does NOT name the path. Both sides already know {@link SANDBOX_TUNNEL_PATHS},\n * and a daemon-supplied path would have to be validated against that map on arrival anyway — so\n * the field would carry no information while widening what a compromised daemon could ask the\n * shim to create.\n */\nexport const TunnelListenSchema = z.object({\n op: z.literal('listen'),\n tunnel: TunnelNameSchema\n})\n\n/**\n * Bytes toward one in-pod connection. The daemon sends these as requests; the shim reports the\n * opposite direction as `shim/event` chunks on the same stream id, which is the only way round:\n * requests flow daemon → shim only, and a tunnel connection is opened by a process inside the\n * pod, so the shim has to announce it.\n */\nexport const TunnelDataSchema = z.object({\n op: z.literal('data'),\n streamId: z.string().uuid(),\n /** base64 because the frame is JSON text; the payload is opaque bytes either way. */\n chunk: z.string().max(MAX_TUNNEL_CHUNK_BASE64)\n})\n\nexport const TunnelCloseSchema = z.object({\n op: z.literal('close'),\n streamId: z.string().uuid(),\n error: z.string().max(200).optional()\n})\n\nexport const TunnelPayloadSchema = z.discriminatedUnion('op', [TunnelListenSchema, TunnelDataSchema, TunnelCloseSchema])\nexport type TunnelPayload = z.infer<typeof TunnelPayloadSchema>\n\n/** What a `listen` reports back, so the daemon logs the path the pod actually serves. */\nexport const TunnelListeningSchema = z.object({ socketPath: z.string().min(1) })\n\n/**\n * Which in-pod path each tunnel is served at. Fixed by the runtime image, not by the daemon's own\n * root, because the daemon's paths mean nothing inside the sandbox.\n *\n * Declared in `sandbox-paths.ts` — the credential helper needs the gitcred path and must not pull\n * this module's zod schemas into its own bundle — and re-exported here, typed against the enum\n * above so a name without a path (or a path without a name) fails to compile.\n */\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<TunnelName, string>> = SANDBOX_TUNNEL_PATH_SOURCE\n","import { execFile } from 'node:child_process'\nimport type { SimpleGit } from 'simple-git'\n\n/**\n * The git operations the daemon actually performs on a workspace — a frozen inventory, not a\n * general-purpose git wrapper.\n *\n * It exists because a cluster-backed agent's workspace lives on the sandbox pod's volume, so\n * the daemon cannot reach it: the orchestration logic stays here and only the *execution*\n * moves. Deriving the interface from what the code already calls (rather than from what git\n * can do) is what keeps that move mechanical — the remote side has a closed list to\n * implement, and re-creating simple-git's surface across a channel is explicitly not the job.\n *\n * Adding a member is a deliberate act: it widens what a half-trusted sandbox will execute.\n */\nexport interface GitRunner {\n /**\n * A runner whose invocations use `env` as their COMPLETE environment, replacing rather than\n * extending the ambient one.\n *\n * Every existing call site threads env per invocation, and replacement is the point: callers\n * build it by sanitizing (`workspaceGitLocalEnv` strips host `GIT_CONFIG_*`, clears protocol\n * allowances, injects config pairs), so merging would quietly undo that sanitization. A\n * caller therefore supplies a whole environment, including identity, not a few overrides.\n *\n * Remotely the environment travels with the request rather than being set on the sandbox, so\n * a runtime cannot read the credential-helper pointers back out of its own env afterwards.\n */\n withEnv(env: Record<string, string>): GitRunner\n /** Run a git subcommand with argv, never a composed shell string. */\n raw(args: string[]): Promise<string>\n clone(repo: string, target: string, options?: string[]): Promise<void>\n /** Pull, returning what the console reports: which files moved and by how much. */\n pull(remote: string, branch: string, options?: string[]): Promise<GitPullSummary>\n status(): Promise<GitStatusSummary>\n /** Commits newest first, bounded by `maxCount`. */\n log(options: { maxCount: number }): Promise<GitLogEntry[]>\n /**\n * Run a read-only subcommand with a HARD ceiling on the bytes it returns, and report\n * whether that ceiling was hit.\n *\n * Distinct from {@link raw} because `raw` accumulates the whole child stdout: one\n * `git diff` on a large change, or a numstat over a tens-of-thousands-of-files dirty\n * tree, is orders of magnitude larger than the wire frame it is being read for. The\n * console's review surface asks for a head slice and nothing more, and it asks on\n * every session page view, so the bound belongs in the contract rather than at each\n * call site — a remote implementation must honour it too, or a sandbox can stream an\n * unbounded reply back across the channel.\n *\n * `overflow: true` means the child was killed at `maxBytes` and `out` is the head\n * slice, which is the answer this seam wants: the caller reports it as truncated.\n */\n readBounded(args: string[], maxBytes: number): Promise<{ out: Buffer; overflow: boolean }>\n}\n\n/**\n * Raised when an invocation never reached git — as opposed to git running and failing.\n *\n * The distinction is load-bearing for every caller that treats a git failure as an ANSWER. The\n * console's `isRepo` preflight is the sharp case: a request the transport dropped is not evidence\n * that the cwd is outside a repository, and reading it as such reports \"not a git checkout\" for a\n * checkout that is there. Only the remote runner raises it — a local child either runs or reports a\n * spawn failure, and there is no channel in between to lose.\n */\nexport class GitTransportError extends Error {\n constructor(\n message: string,\n readonly cause?: unknown\n ) {\n super(message)\n this.name = 'GitTransportError'\n }\n}\n\n/** The pull result the BFF surfaces to the console; nothing here is decorative. */\nexport interface GitPullSummary {\n files: string[]\n insertions: number\n deletions: number\n}\n\n/** A commit as the workspace views consume it — the committer date included, because the\n * console shows when HEAD last moved and an interface without it cannot serve that. */\nexport interface GitLogEntry {\n hash: string\n subject: string\n /** Strict-ISO committer date (`%cI`), empty when the runtime reported none. */\n committedAt: string\n}\n\n/** The status fields the daemon reads; simple-git returns many more. */\nexport interface GitStatusSummary {\n current: string | null\n tracking: string | null\n ahead: number\n behind: number\n files: Array<{ path: string; index: string; working_dir: string }>\n /** Whether the tree has no changes. Taken from simple-git locally rather than re-derived,\n * so the local answer stays authoritative; the remote side derives it and the contract test\n * is what establishes the two agree, including on a conflicted tree. */\n clean: boolean\n}\n\n/** A local diff/log/numstat never touches the network, so it either answers quickly or the\n * checkout is wedged (an index.lock holder, a dead fsmonitor). */\nconst READ_TIMEOUT_MS = 15_000\n\n/** Factory for a runner bound to one working directory. */\nexport type GitRunnerFor = (cwd?: string, abort?: AbortSignal) => GitRunner\n\n/**\n * Today's behaviour: run git in this daemon's own filesystem through simple-git.\n *\n * A thin delegation on purpose — the point of the seam is that the local path keeps its exact\n * semantics (including simple-git's argument handling and its abort-kills-the-child plugin),\n * so a cluster agent and a self-hosted agent differ in where git runs and in nothing else.\n */\nexport class LocalGitRunner implements GitRunner {\n // `cwd` and `env` are carried alongside the handle because `readBounded` spawns its own\n // child and cannot ask simple-git what it was configured with.\n // Both extra parameters are REQUIRED, not optional, because a site that forgets either one\n // fails in a way that reads as a passing test. `cwd`: `readBounded` spawns its own child and\n // cannot ask simple-git where it was pointed, so a missing one runs git in the daemon's own\n // directory (this happened twice while it was optional). `make`: see `withEnv`.\n constructor(\n private readonly git: SimpleGit,\n private readonly cwd: string | undefined,\n private readonly make: (env: Record<string, string>) => SimpleGit,\n private readonly env: Record<string, string> = {}\n ) {}\n\n withEnv(env: Record<string, string>): GitRunner {\n // A derived runner gets its OWN executor, built by `make`. simple-git's `.env()` mutates the\n // ROOT executor and returns the same instance, so sharing a handle across siblings is wrong in\n // three ways that a sequential test cannot see: two derivations leave the last env in place,\n // concurrent calls (`Promise.all`) both run under whichever env was set last, and the BASE\n // inherits a child's env because nothing ever resets it. That is not academic — deriving an\n // identity-carrying runner and a config-audit runner from one base made a commit land as the\n // host's OS user. Independent executors make the env a property of the runner, as the shim's\n // per-request environment already is.\n return new LocalGitRunner(this.make(env), this.cwd, this.make, env)\n }\n\n async raw(args: string[]): Promise<string> {\n return this.git.raw(args)\n }\n\n readBounded(args: string[], maxBytes: number): Promise<{ out: Buffer; overflow: boolean }> {\n // `execFile` rather than the simple-git handle, which is the whole reason this member\n // exists: `maxBuffer` and `timeout` are what bound it, and simple-git exposes neither.\n // The env and cwd come from the same places the handle's do, so the two paths differ in\n // the ceiling and in nothing else.\n return new Promise((resolve, reject) => {\n execFile(\n 'git',\n args,\n {\n ...(this.cwd ? { cwd: this.cwd } : {}),\n env: { ...this.env },\n encoding: 'buffer',\n maxBuffer: maxBytes,\n timeout: READ_TIMEOUT_MS,\n windowsHide: true\n },\n (err, stdout) => {\n if (!err) return resolve({ out: stdout, overflow: false })\n // The ceiling was hit: the child is already dead and `stdout` holds the head slice.\n if ((err as NodeJS.ErrnoException).code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {\n return resolve({ out: stdout, overflow: true })\n }\n reject(err)\n }\n )\n })\n }\n\n async clone(repo: string, target: string, options: string[] = []): Promise<void> {\n await this.git.clone(repo, target, options)\n }\n\n async pull(remote: string, branch: string, options: string[] = []): Promise<GitPullSummary> {\n const result = await this.git.pull(remote, branch, options)\n return {\n files: [...result.files],\n insertions: result.summary.insertions,\n deletions: result.summary.deletions\n }\n }\n\n async status(): Promise<GitStatusSummary> {\n const summary = await this.git.status()\n return {\n current: summary.current ?? null,\n tracking: summary.tracking ?? null,\n ahead: summary.ahead,\n behind: summary.behind,\n files: summary.files.map((file) => ({\n path: file.path,\n index: file.index,\n working_dir: file.working_dir\n })),\n clean: summary.isClean()\n }\n }\n\n async log(options: { maxCount: number }): Promise<GitLogEntry[]> {\n const result = await this.git.log({\n maxCount: options.maxCount,\n format: { hash: '%H', date: '%cI', subject: '%s' }\n })\n return result.all.map((entry) => ({\n hash: entry.hash,\n subject: entry.subject ?? '',\n committedAt: entry.date ?? ''\n }))\n }\n}\n","import { z } from 'zod'\nimport { TunnelNameSchema } from './tunnel.js'\n\n/** WS subprotocol and path the daemon uses when dialing the sandbox shim. */\nexport const SHIM_SUBPROTOCOL = 'agentconnect.shim.v1'\nexport const SHIM_WS_PATH = '/shim/v1'\n\n/** Audience the projected ServiceAccount token is restricted to. A token minted for\n * anything else must not authenticate here, which is what makes the pod's own\n * credential safe to hand over: it is useless anywhere but this endpoint. */\nexport const SHIM_TOKEN_AUDIENCE = 'ac-daemon-callback'\n\n/** Where the pod template projects that token, and where the shim reads it from. */\nexport const SHIM_IDENTITY_TOKEN_PATH = '/var/run/ac-identity/token'\n\n/** Port the in-sandbox shim listens on for daemon dial-in. */\nexport const SHIM_LISTEN_PORT_ENV = 'AC_SHIM_PORT'\nexport const DEFAULT_SHIM_LISTEN_PORT = 8085\n\n/** Root the sandbox permits filesystem work inside — the mounted agent volume. Also non-secret,\n * and fixed by the image rather than chosen per request, since the shim uses it to refuse a\n * cwd that escapes it. */\nexport const SHIM_WORKSPACE_ROOT_ENV = 'AC_SHIM_WORKSPACE_ROOT'\n\n/** The shim's own fallback for that env, and the daemon's assumption for a legacy shim that\n * predates workspace-root reporting — every such image mounted the volume here. */\nexport const DEFAULT_SHIM_WORKSPACE_ROOT = '/agent'\n\n/** `cluster-skills-v2` admits the widened skill manifest; a v1-only shim still gets the narrow one. */\nexport const ShimFeatureSchema = z.enum(['cluster-skills-v1', 'cluster-skills-v2'])\nexport type ShimFeature = z.infer<typeof ShimFeatureSchema>\n\n/** Operations the daemon may ask a bound shim to perform. Every one is authorized\n * individually against the binding's grants — a channel is not a blanket permission.\n * The bodies land in #814 / #815; this is the authorization vocabulary they use. */\nexport const ShimCapabilitySchema = z.enum([\n /** Write daemon-materialized files (secrets, config files) into the sandbox. */\n 'materialize',\n /** Run a command in the sandbox and return a structured result (workspace git). */\n 'exec',\n /** Read a bounded file back out (BFF workspace reads). */\n 'read',\n /** Proxy an in-pod unix socket back to a daemon-side server (gitcred, gh, MCP). */\n 'tunnel',\n /** Run the ACP runtime and relay its stdio as a stream (its own channel: ACP is already\n * a complete protocol, and reinterpreting it here would add a second place to break). */\n 'acp',\n /** Run the merge-when-ready watcher in the pod, so the armed set lives and dies with the\n * sandbox. Its own capability rather than a widening of `exec`: that channel is git-only and\n * enforced in-pod on purpose, and reaching `gh` through it would turn a deliberate boundary\n * into an arbitrary-execution surface. */\n 'automerge',\n /** Install daemon-acquired immutable skills into this pod's workspace. */\n 'skills',\n /** The same channel at the widened manifest limits — all the daemon learns from `cluster-skills-v2`. */\n 'skills-wide',\n /** Report which runtimes this image actually provides, by asking them. The daemon cannot learn\n * this any other way: `--k8s` runs no local runtime, and anything it states from its own\n * configuration is a claim about an image it never opened. */\n 'probe'\n])\nexport type ShimCapability = z.infer<typeof ShimCapabilitySchema>\n\n/** The daemon opens a dial-in channel with the launch it expects to bind. */\nexport const ShimDialHelloSchema = z.object({\n type: z.literal('shim/hello'),\n agentId: z.string().min(1),\n generation: z.number().int().nonnegative()\n})\n\n/** The shim answers the dialer's hello by proving which pod accepted it. */\nexport const ShimIdentitySchema = z.object({\n type: z.literal('shim/identity'),\n /** Projected ServiceAccount token, audience-restricted to {@link SHIM_TOKEN_AUDIENCE}. */\n token: z.string().min(1),\n /** Shim build, for operator diagnosis only — never an authorization input. */\n shimVersion: z.string().max(64).optional(),\n /** This pod's workspace mount; absent on legacy shims means {@link DEFAULT_SHIM_WORKSPACE_ROOT}. */\n workspaceRoot: z.string().min(1).max(4096).optional(),\n /** Versioned optional surfaces supported by this image; absent means a legacy shim. */\n features: z.array(ShimFeatureSchema).max(16).optional()\n})\n\n/** The daemon's answer once the token is verified and mapped to a spawn record. */\nexport const ShimBoundSchema = z.object({\n type: z.literal('shim/bound'),\n /** Short-TTL credential for subsequent frames, bound to this pod and generation. */\n sessionCredential: z.string().min(1),\n /** Seconds until the credential must be re-obtained by re-handshaking. */\n expiresInSeconds: z.number().int().positive(),\n agentId: z.string().min(1),\n /** Monotonic per-agent spawn counter; frames from an older one are refused. */\n generation: z.number().int().nonnegative(),\n grants: z.array(ShimCapabilitySchema)\n})\n\nexport const ShimRejectedSchema = z.object({\n type: z.literal('shim/rejected'),\n /** Coarse, non-probing reason: never leaks which of several checks failed. */\n reason: z.enum(['unauthenticated', 'unknown_pod', 'stale_generation', 'unavailable']),\n message: z.string().max(200)\n})\n\n/** Every post-binding frame carries the credential and the generation it was issued\n * for, so a replayed frame from a previous pod incarnation is refused on arrival. */\nexport const ShimRequestSchema = z.object({\n type: z.literal('shim/request'),\n id: z.string().uuid(),\n sessionCredential: z.string().min(1),\n generation: z.number().int().nonnegative(),\n capability: ShimCapabilitySchema,\n /** Operation payload, shaped per capability by the channels that land later. */\n payload: z.unknown()\n})\n\n// Cancels an in-flight request by id. Carries credential and generation like every post-binding\n// frame, so a replayed cancel from a previous incarnation cannot kill a live request.\nexport const ShimCancelSchema = z.object({\n type: z.literal('shim/cancel'),\n id: z.string().uuid(),\n sessionCredential: z.string().min(1),\n generation: z.number().int().nonnegative(),\n reason: z.string().max(200).optional()\n})\n\nexport const ShimResponseSchema = z.object({\n type: z.literal('shim/response'),\n id: z.string().uuid(),\n ok: z.boolean(),\n payload: z.unknown().optional(),\n error: z.string().max(500).optional()\n})\n\n/** A recurring event on an open stream. Unlike a response, many arrive per request: an ACP\n * runtime emits stdout continuously and exits once, and neither fits one-shot correlation. */\nexport const ShimEventSchema = z.object({\n type: z.literal('shim/event'),\n /** The stream this belongs to: the request id that opened it, or — for a tunnel, whose\n * connections are opened by a process inside the pod — an id the shim mints and announces. */\n streamId: z.string().uuid(),\n event: z.discriminatedUnion('kind', [\n /** A process in the sandbox connected to a tunnel's socket; the daemon dials its own end. */\n z.object({ kind: z.literal('connect'), tunnel: TunnelNameSchema }),\n z.object({ kind: z.literal('chunk'), data: z.string() }),\n z.object({\n kind: z.literal('exit'),\n code: z.number().int().nullable(),\n signal: z.string().nullable(),\n error: z.string().max(500).optional()\n })\n ])\n})\n\nexport const ShimFrameSchema = z.union([\n ShimDialHelloSchema,\n ShimIdentitySchema,\n ShimBoundSchema,\n ShimRejectedSchema,\n ShimRequestSchema,\n ShimCancelSchema,\n ShimResponseSchema,\n ShimEventSchema\n])\n\nexport type ShimBound = z.infer<typeof ShimBoundSchema>\nexport type ShimRejected = z.infer<typeof ShimRejectedSchema>\nexport type ShimRequest = z.infer<typeof ShimRequestSchema>\nexport type ShimCancel = z.infer<typeof ShimCancelSchema>\nexport type ShimResponse = z.infer<typeof ShimResponseSchema>\nexport type ShimEvent = z.infer<typeof ShimEventSchema>\nexport type ShimFrame = z.infer<typeof ShimFrameSchema>\nexport type ShimDialHello = z.infer<typeof ShimDialHelloSchema>\nexport type ShimIdentity = z.infer<typeof ShimIdentitySchema>\n\n/** Parse an inbound frame, returning undefined rather than throwing: a malformed frame\n * from a half-trusted peer is a close-the-connection event, not an exception path. */\nexport function parseShimFrame(text: string): ShimFrame | undefined {\n try {\n const result = ShimFrameSchema.safeParse(JSON.parse(text))\n return result.success ? result.data : undefined\n } catch {\n return undefined\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auto-merge.js","names":[],"sources":["../../src/github/auto-merge/core.ts","../../src/github/auto-merge/loop.ts","../../src/gitcred/env.ts","../../src/gitcred/gh-token-ipc.ts","../../src/shim/sandbox-paths.ts","../../src/shim/auto-merge.ts"],"sourcesContent":["/**\n * The merge-when-ready loop itself — one GitHub read, one readiness verdict, one squash merge.\n *\n * A LEAF module on purpose: node builtins and `fetch`, nothing else. It is bundled into the\n * in-sandbox `auto-merge.js` entry (its own graph, everything inlined) as well as compiled into\n * the daemon, and an import that reached the CP client or the credential cache from here would\n * copy that graph into the half-trusted runtime image. Same reason `gitcred/gh-token-client.ts`\n * keeps its distance.\n *\n * Why this exists at all instead of GitHub's `enablePullRequestAutoMerge`: that mutation refuses\n * every pull request whose `mergeStateStatus` is not BLOCKED — \"clean status\" when the checks\n * passed, \"unstable status\" while they run on a repository with no REQUIRED checks — so on most\n * repositories it can never be armed. The readiness rule below is the one operators actually\n * mean, and it is evaluated against the CURRENT head on every tick: merge-when-ready has to\n * allow the fix commit that turns the checks green.\n */\n\n/** What a tick needs to decide, and nothing more. */\nexport interface PrSnapshot {\n prId: string\n headOid: string\n state: 'OPEN' | 'CLOSED' | 'MERGED'\n isDraft: boolean\n /** GitHub's own mergeability verdict; `UNKNOWN` means it is still computing one. */\n mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN'\n reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | null\n checks: PrCheck[]\n}\n\nexport interface PrCheck {\n name: string\n /** `pending` covers queued/in-progress and a status context with no conclusion yet. */\n outcome: 'success' | 'failure' | 'pending'\n}\n\n/** Either \"merge it\" or the one line that says why not — the answer the console draws. */\nexport type Readiness = { ready: true } | { ready: false; waitingOn: string }\n\nconst MAX_NAMED_CHECKS = 3\n\n/**\n * The readiness rule: open, not a draft, no conflicts, no red or running check, and nobody\n * asking for changes.\n *\n * `REVIEW_REQUIRED` is NOT a blocker and that is deliberate: the operator ticking this box on\n * their own agent's pull request IS the approval, and on a repository with no required reviewers\n * GitHub reports `REVIEW_REQUIRED` forever, which would make the box a control that never fires.\n * `CHANGES_REQUESTED` blocks, because someone actively said no.\n *\n * `UNKNOWN` mergeability waits rather than merging: GitHub computes it asynchronously, and\n * treating \"not computed yet\" as \"no conflicts\" is how a merge-when-ready lands a broken tree.\n */\nexport function readiness(pr: PrSnapshot): Readiness {\n // Both terminal states are answered by `tick` before it gets here, so these two arms are for a\n // DIRECT caller (and for the tests that pin the rule) rather than for the loop.\n if (pr.state === 'MERGED') return { ready: false, waitingOn: 'already merged' }\n if (pr.state === 'CLOSED') return { ready: false, waitingOn: 'the pull request is closed' }\n if (pr.isDraft) return { ready: false, waitingOn: 'the pull request is a draft' }\n if (pr.reviewDecision === 'CHANGES_REQUESTED') return { ready: false, waitingOn: 'changes requested' }\n if (pr.mergeable === 'CONFLICTING') return { ready: false, waitingOn: 'conflicts with the base branch' }\n if (pr.mergeable === 'UNKNOWN') return { ready: false, waitingOn: 'GitHub is still computing mergeability' }\n const failed = pr.checks.filter((check) => check.outcome === 'failure')\n if (failed.length > 0) return { ready: false, waitingOn: `failing checks: ${names(failed)}` }\n const pending = pr.checks.filter((check) => check.outcome === 'pending')\n if (pending.length > 0) return { ready: false, waitingOn: `checks running: ${names(pending)}` }\n return { ready: true }\n}\n\nfunction names(checks: PrCheck[]): string {\n const head = checks.slice(0, MAX_NAMED_CHECKS).map((check) => check.name || 'unnamed')\n return checks.length > head.length ? `${head.join(', ')} +${checks.length - head.length}` : head.join(', ')\n}\n\nconst SNAPSHOT_QUERY = `\nquery AutoMerge($owner:String!,$name:String!,$number:Int!){\n repository(owner:$owner,name:$name){\n pullRequest(number:$number){\n id headRefOid state isDraft mergeable reviewDecision\n commits(last:1){nodes{commit{statusCheckRollup{contexts(first:100){nodes{\n __typename\n ... on CheckRun{name conclusion status}\n ... on StatusContext{context state}\n }}}}}}\n }\n }\n}`\n\nconst MERGE_MUTATION =\n 'mutation($id:ID!,$oid:GitObjectID!){mergePullRequest(input:{pullRequestId:$id,mergeMethod:SQUASH,expectedHeadOid:$oid}){clientMutationId}}'\n\n/** Raised when GitHub answered but refused — as opposed to never being reached. Both keep the\n * watcher armed; only the wording the console shows differs. */\nexport class AutoMergeGithubError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AutoMergeGithubError'\n }\n}\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>\n\nexport interface GithubAccess {\n token: () => Promise<string>\n fetchImpl?: FetchLike\n /** GitHub GraphQL endpoint; overridden only by tests and GHES. */\n endpoint?: string\n}\n\n/** One GraphQL round trip. GraphQL reports refusals inside a 200, so `errors` decides here. */\nasync function graphql<T>(access: GithubAccess, query: string, variables: Record<string, unknown>): Promise<T> {\n return send(access, await access.token(), query, variables)\n}\n\n/** The round trip with the token ALREADY in hand. Split out so a caller that must not await anything\n * between its last abort check and the request can acquire the token first — see `squashMerge`. */\nasync function send<T>(\n access: GithubAccess,\n token: string,\n query: string,\n variables: Record<string, unknown>\n): Promise<T> {\n const fetchImpl = access.fetchImpl ?? ((input: string, init?: RequestInit) => fetch(input, init))\n const res = await fetchImpl(access.endpoint ?? 'https://api.github.com/graphql', {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n accept: 'application/vnd.github+json',\n 'content-type': 'application/json',\n 'user-agent': 'agentconnect-auto-merge'\n },\n body: JSON.stringify({ query, variables })\n })\n if (!res.ok) throw new AutoMergeGithubError(`github answered ${res.status}`)\n const body = (await res.json()) as { data?: T | null; errors?: Array<{ message?: string }> }\n if (body.errors?.length) {\n throw new AutoMergeGithubError(body.errors.map((e) => e.message ?? 'unknown').join('; '))\n }\n if (!body.data) throw new AutoMergeGithubError('github returned no data')\n return body.data\n}\n\ninterface SnapshotAnswer {\n repository: {\n pullRequest: {\n id: string\n headRefOid: string | null\n state: 'OPEN' | 'CLOSED' | 'MERGED'\n isDraft: boolean\n mergeable: string\n reviewDecision: string | null\n commits: {\n nodes: Array<{\n commit: { statusCheckRollup: { contexts: { nodes: Array<Record<string, unknown>> | null } } | null }\n }> | null\n }\n } | null\n } | null\n}\n\nexport async function fetchSnapshot(access: GithubAccess, repoFullName: string, prNumber: number): Promise<PrSnapshot> {\n const [owner, name] = repoFullName.split('/')\n if (!owner || !name) throw new AutoMergeGithubError(`malformed repository name ${repoFullName}`)\n const answer = await graphql<SnapshotAnswer>(access, SNAPSHOT_QUERY, { owner, name, number: prNumber })\n const pr = answer.repository?.pullRequest\n if (!pr) throw new AutoMergeGithubError('pull request not visible to this token')\n const contexts = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes ?? []\n return {\n prId: pr.id,\n headOid: pr.headRefOid ?? '',\n state: pr.state,\n isDraft: pr.isDraft,\n mergeable: pr.mergeable === 'MERGEABLE' || pr.mergeable === 'CONFLICTING' ? pr.mergeable : 'UNKNOWN',\n reviewDecision:\n pr.reviewDecision === 'APPROVED' || pr.reviewDecision === 'CHANGES_REQUESTED'\n ? pr.reviewDecision\n : pr.reviewDecision === 'REVIEW_REQUIRED'\n ? 'REVIEW_REQUIRED'\n : null,\n checks: contexts.map(toCheck)\n }\n}\n\n/** A rollup context is a CheckRun or a StatusContext, and the two spell their outcome differently. */\nfunction toCheck(node: Record<string, unknown>): PrCheck {\n if (node.__typename === 'StatusContext') {\n const state = String(node.state ?? '')\n return {\n name: String(node.context ?? ''),\n outcome: state === 'SUCCESS' ? 'success' : state === 'PENDING' || state === '' ? 'pending' : 'failure'\n }\n }\n const conclusion = String(node.conclusion ?? '')\n const status = String(node.status ?? '')\n // A skipped or neutral run is not a failure, and a cancelled one is: it never reported success.\n const outcome: PrCheck['outcome'] =\n status !== 'COMPLETED' || conclusion === ''\n ? 'pending'\n : conclusion === 'SUCCESS' || conclusion === 'SKIPPED' || conclusion === 'NEUTRAL'\n ? 'success'\n : 'failure'\n return { name: String(node.name ?? ''), outcome }\n}\n\n/**\n * Squash-merge, pinned to the head the readiness verdict was formed against — never to a head\n * the operator saw in the panel minutes ago. A commit landing mid-tick refuses here and the\n * next tick judges the new head on its own merits, which is the whole point of \"when ready\".\n *\n * The token is fetched BEFORE the last abort check, not inside the request: fetching it is itself an\n * await (a pod reads it over the gitcred tunnel), and a disarm landing in that window would otherwise\n * be invisible — the check would have passed already and the POST would go out regardless. Answers\n * `false` when the fence closed instead, so nothing was sent.\n */\nexport async function squashMerge(access: GithubAccess, pr: PrSnapshot, aborted?: () => boolean): Promise<boolean> {\n const token = await access.token()\n // Nothing may be awaited between here and the request; `send` takes the token already resolved.\n if (aborted?.()) return false\n await send(access, token, MERGE_MUTATION, { id: pr.prId, oid: pr.headOid })\n return true\n}\n\n/** What one tick did, for the caller to project as `waitingOn` / `lastError` / `merged`. */\nexport type TickOutcome =\n | { kind: 'merged' }\n /** Terminal for a reason that is not a merge: the pull request was CLOSED. The intent expired with\n * it, and a watcher left polling would merge it if the branch were ever reopened. */\n | { kind: 'closed' }\n /** The fence below closed while this tick was in flight, so the merge was never attempted. */\n | { kind: 'aborted' }\n | { kind: 'waiting'; waitingOn: string }\n | { kind: 'error'; error: string }\n\nexport interface TickOptions {\n /**\n * Checked synchronously in the instant before the merge mutation is sent, with the token already\n * resolved so nothing can be awaited in between.\n *\n * A tick awaits a snapshot and a token before it decides anything, and a disarm arriving inside\n * that window used to be invisible to it: the continuation went on to squash-merge a pull request\n * whose box the operator had already unticked and been told was off. Because the last check is\n * synchronous and immediately precedes the only mutation here, a caller that flips this predicate\n * knows that once it has, no merge can still BEGIN — which is what lets `disarm` answer honestly.\n */\n aborted?: () => boolean\n}\n\n/** One poll: read, judge, and merge if the verdict says so. Never throws — a tick's failure is\n * DATA the watcher keeps armed through, because the usual cure is the next commit. */\nexport async function tick(\n access: GithubAccess,\n repoFullName: string,\n prNumber: number,\n opts: TickOptions = {}\n): Promise<TickOutcome> {\n try {\n if (opts.aborted?.()) return { kind: 'aborted' }\n const pr = await fetchSnapshot(access, repoFullName, prNumber)\n if (pr.state === 'MERGED') return { kind: 'merged' }\n // Closed without merging ends the watch. Keeping it armed would leave a poll running for the life\n // of the pod and, worse, merge the pull request if it were ever reopened.\n if (pr.state === 'CLOSED') return { kind: 'closed' }\n const verdict = readiness(pr)\n if (!verdict.ready) return { kind: 'waiting', waitingOn: verdict.waitingOn }\n // Everything above is a read; below is the irreversible act. The gate is checked here to avoid\n // fetching a token at all for a watch already disarmed, and again inside `squashMerge` with the\n // token in hand — that second one is the check no further await can slip behind.\n if (opts.aborted?.()) return { kind: 'aborted' }\n if (!(await squashMerge(access, pr, opts.aborted))) return { kind: 'aborted' }\n return { kind: 'merged' }\n } catch (err) {\n return { kind: 'error', error: err instanceof Error ? err.message : String(err) }\n }\n}\n\n/** Default poll cadence. One pull request per armed box and a handful of boxes at a time, so a\n * minute is well inside GitHub's budget while still merging promptly after the last check. */\nexport const AUTO_MERGE_POLL_MS = 30_000\n","/**\n * The armed watcher as a running thing: a timer around `tick`, plus the status the console reads.\n *\n * A LEAF module beside `core.ts` for the same reason — it is bundled into the in-sandbox entry.\n * State is in memory and nowhere else: the entry point is a process in the agent's pod (cluster\n * placement) or an object in the daemon (local placement), so losing the pod or restarting the\n * daemon forgets the intent and the box reads back unchecked. That is the designed lifetime, not\n * a limitation — nobody is watching the pull request any more, and the console must not claim\n * otherwise.\n */\nimport { AUTO_MERGE_POLL_MS, tick, type GithubAccess, type TickOutcome } from './core.js'\n\nexport interface AutoMergeStatus {\n waitingOn?: string\n lastError?: string\n merged: boolean\n /** The pull request was closed without merging — terminal, like `merged`, and for the same reason:\n * nothing is watching any more, so the console must not draw an armed box over it. */\n closed?: boolean\n}\n\nexport interface AutoMergeLoopDeps {\n access: GithubAccess\n repoFullName: string\n prNumber: number\n pollMs?: number\n /** Called after every tick, so a host can log it or hand it to its own reader. */\n onStatus?: (status: AutoMergeStatus) => void\n /** Timer seam — a test drives ticks without waiting for a real minute. */\n timers?: {\n setInterval: (fn: () => void, ms: number) => unknown\n clearInterval: (handle: unknown) => void\n }\n}\n\nexport class AutoMergeLoop {\n private handle?: unknown\n private running = false\n /** Bumped by every `stop()`. A tick captures it on entry and refuses to merge once it has moved,\n * which is how a disarm arriving mid-tick fences the mutation instead of racing it. */\n private generation = 0\n private inflight?: Promise<AutoMergeStatus>\n private status: AutoMergeStatus = { merged: false }\n private readonly timers: NonNullable<AutoMergeLoopDeps['timers']>\n\n constructor(private readonly deps: AutoMergeLoopDeps) {\n this.timers = deps.timers ?? {\n setInterval: (fn, ms) => setInterval(fn, ms),\n clearInterval: (handle) => clearInterval(handle as ReturnType<typeof setInterval>)\n }\n }\n\n /** Arm: one immediate tick (an already-green pull request should not wait out a poll), then the\n * cadence. Idempotent — arming an armed loop keeps the one timer it has. */\n start(): void {\n if (this.handle !== undefined) return\n this.handle = this.timers.setInterval(() => void this.run(), this.deps.pollMs ?? AUTO_MERGE_POLL_MS)\n void this.run()\n }\n\n /** Disarm. The generation moves FIRST and unconditionally: a tick already awaiting GitHub reads it\n * before it merges, and bumping it even for an already-stopped loop keeps that fence honest. */\n stop(): void {\n this.generation++\n if (this.handle === undefined) return\n this.timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** Resolves once no tick is in flight. `stop()` guarantees no merge can BEGIN; awaiting this also\n * means none is still in the air, so a caller can answer \"off\" without a merge landing behind it. */\n async settle(): Promise<void> {\n while (this.inflight) await this.inflight.catch(() => undefined)\n }\n\n /** True until the watch ENDS — a merge, or the pull request being closed. The host drops the entry\n * on that falling edge; either way nothing is watching and the box must read unchecked. */\n armed(): boolean {\n return this.handle !== undefined && !this.status.merged && !this.status.closed\n }\n\n current(): AutoMergeStatus {\n return { ...this.status }\n }\n\n /** One tick, guarded against overlap: a slow GitHub must not stack requests behind itself. */\n async run(): Promise<AutoMergeStatus> {\n if (this.running) return this.current()\n this.running = true\n const generation = this.generation\n const attempt = (async () => {\n try {\n this.apply(\n await tick(this.deps.access, this.deps.repoFullName, this.deps.prNumber, {\n aborted: () => this.generation !== generation\n })\n )\n } finally {\n this.running = false\n this.inflight = undefined\n }\n this.deps.onStatus?.(this.current())\n return this.current()\n })()\n this.inflight = attempt\n return attempt\n }\n\n /** A merge is terminal (the timer goes); an error keeps the loop armed, because the usual cure\n * is the next commit and disarming would throw away the operator's intent on one red tick. */\n private apply(outcome: TickOutcome): void {\n if (outcome.kind === 'merged') {\n this.status = { merged: true }\n this.stop()\n return\n }\n if (outcome.kind === 'closed') {\n this.status = { merged: false, closed: true, waitingOn: 'the pull request was closed' }\n this.stop()\n return\n }\n // Disarmed mid-flight: the status this tick would have written describes a watch that no longer\n // exists, so the last one the operator actually saw stands.\n if (outcome.kind === 'aborted') return\n this.status =\n outcome.kind === 'waiting'\n ? { merged: false, waitingOn: outcome.waitingOn }\n : { merged: false, lastError: outcome.error }\n }\n}\n","/**\n * The three environment names the credential channel travels on.\n *\n * A leaf on purpose: the same helper source runs as a daemon CLI subcommand and inside a sandbox\n * pod, and the in-sandbox build asserts that its bundle imports nothing but node builtins. Keeping\n * these here — rather than in `cp/gitcred-server.ts`, which pulls the daemon's credential cache —\n * is what lets one implementation serve both.\n */\n\nexport const GITCRED_CAPABILITY_ENV = 'AC_GITCRED_CAPABILITY'\n/** The agent identity minted TOGETHER with the capability (git-injection\n * gitCredentialEnv). Helpers prefer this pair over the agentId baked into a\n * `.git/config` helper line, which goes stale when an agent is deleted and\n * recreated under the same name over a surviving checkout. */\nexport const GITCRED_AGENT_ENV = 'AC_GITCRED_AGENT'\n/** Where a helper finds the socket, when that is not under this daemon's own root. A helper\n * running in a sandbox pod reaches the daemon through the shim's tunnel instead, and the pod's\n * filesystem has no daemon root to derive a path from. Non-secret: it is a path, and the\n * capability is what authorizes the request that travels over it. */\nexport const GITCRED_SOCKET_ENV = 'AC_GITCRED_SOCKET'\n","// The gitcred socket call itself, split out of `gh-token-client.ts` so a second in-sandbox entry can\n// reach a gh token without also pulling in the gh-argv target resolver and its `git remote` probe.\n// Node builtins only: every consumer of this file is bundled into the runtime image.\nimport { createConnection } from 'node:net'\nimport { GITCRED_CAPABILITY_ENV } from './env.js'\n\nexport interface GitCredIpcReply {\n ok: boolean\n password?: string\n error?: string\n}\n\n/** One newline-delimited-JSON round trip on the gitcred socket. Never rejects: an unreachable\n * daemon is an answer (`ok:false`), and callers report it as data. */\nexport function gitcredIpc(path: string, msg: unknown): Promise<GitCredIpcReply> {\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as GitCredIpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n\n/** A GH_TOKEN-plane token for one repository, or a thrown reason. Fetched per use rather than\n * cached here: these tokens are short-lived, and the daemon/CP side already caches and clamps. */\nexport async function fetchGhToken(\n args: { agentId: string; repoFullName: string; socketPath: string; capability?: string },\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n const res = await gitcredIpc(args.socketPath, {\n op: 'get',\n agentId: args.agentId,\n capability: args.capability ?? env[GITCRED_CAPABILITY_ENV],\n plane: 'gh',\n repoFullName: args.repoFullName\n })\n if (!res.ok || !res.password) {\n throw new Error(\n `no gh credentials for agent ${args.agentId} on ${args.repoFullName}: ${res.error ?? 'unknown error'}`\n )\n }\n return res.password\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh wrapper and nothing else. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","#!/usr/bin/env node\n// The in-sandbox merge-when-ready watcher: one process per armed pull request, spawned by the shim\n// when the daemon arms the box and killed when it disarms. Its own entry for the same reason the\n// credential helper and the gh token fetch are — the image copies ONE file per bundle, so two\n// entries whose graphs are disjoint stay two single files where a shared module would emit a chunk\n// nothing copies.\n//\n// It runs HERE, in the agent's pod, so the watcher's lifetime is the sandbox's: a reclaimed pod\n// takes the intent with it and the console's box reads back unchecked, with nothing persisted\n// anywhere to contradict that. It holds no policy — the daemon decided this agent may merge this\n// repository, and the token it fetches per tick is clamped CP-side.\nimport { AUTO_MERGE_POLL_MS } from '../github/auto-merge/core.js'\nimport { AutoMergeLoop } from '../github/auto-merge/loop.js'\nimport { GITCRED_SOCKET_ENV } from '../gitcred/env.js'\nimport { fetchGhToken } from '../gitcred/gh-token-ipc.js'\nimport { SANDBOX_TUNNEL_PATHS } from './sandbox-paths.js'\n\n/** Poll cadence override, so an operator can tighten it without a new image. */\nexport const AUTO_MERGE_POLL_ENV = 'AC_AUTO_MERGE_POLL_MS'\n\nasync function main(): Promise<number> {\n // `<agentId> <owner/repo> <prNumber>`, positional and in that order — the shim spawns this.\n const [agentId, repoFullName, prRaw] = process.argv.slice(2)\n const prNumber = Number(prRaw)\n if (!agentId || !repoFullName || !Number.isInteger(prNumber) || prNumber <= 0) {\n process.stderr.write('agentconnect: auto-merge expects <agentId> <owner/repo> <prNumber>\\n')\n return 2\n }\n // The tunnel's path unless something names another; a pod has no daemon root to derive one from.\n const socketPath = process.env[GITCRED_SOCKET_ENV]?.trim() || SANDBOX_TUNNEL_PATHS.gitcred\n const pollMs = Number(process.env[AUTO_MERGE_POLL_ENV]) || AUTO_MERGE_POLL_MS\n\n // One line of NDJSON per tick on stdout — the shim reads it back as this watcher's status, so\n // the console can say what the merge is waiting on. Nothing else is ever written to stdout.\n const loop = new AutoMergeLoop({\n access: { token: () => fetchGhToken({ agentId, repoFullName, socketPath }) },\n repoFullName,\n prNumber,\n pollMs,\n onStatus: (status) => {\n // BOTH terminal states exit: merged, and the pull request being closed. Exiting is what tells the\n // shim to drop its entry rather than leaving a process that will never do anything again — and a\n // closed watcher left alive is one that would merge the branch if it were ever reopened. The exit\n // waits for the WRITE to flush: stdout is a pipe here, and exiting first would lose the status.\n const done = status.merged || status.closed === true\n process.stdout.write(JSON.stringify(status) + '\\n', () => {\n if (done) process.exit(0)\n })\n if (done) loop.stop()\n }\n })\n for (const signal of ['SIGTERM', 'SIGINT'] as const) {\n process.on(signal, () => {\n // The same fence the daemon-local watcher uses: `stop()` moves the generation the tick in flight\n // checks before it merges, and settling before exit means the disarm this signal IS cannot be\n // answered while a squash could still begin in here.\n loop.stop()\n void loop.settle().then(() => process.exit(0))\n })\n }\n loop.start()\n await new Promise<void>(() => {})\n return 0\n}\n\nmain().then(\n (code) => process.exit(code),\n (err: unknown) => {\n process.stderr.write(`agentconnect: auto-merge failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":"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"}
1
+ {"version":3,"file":"auto-merge.js","names":[],"sources":["../../src/github/auto-merge/core.ts","../../src/github/auto-merge/loop.ts","../../src/gitcred/env.ts","../../src/gitcred/gh-token-ipc.ts","../../src/shim/sandbox-paths.ts","../../src/shim/auto-merge.ts"],"sourcesContent":["/**\n * The merge-when-ready loop itself — one GitHub read, one readiness verdict, one squash merge.\n *\n * A LEAF module on purpose: node builtins and `fetch`, nothing else. It is bundled into the\n * in-sandbox `auto-merge.js` entry (its own graph, everything inlined) as well as compiled into\n * the daemon, and an import that reached the CP client or the credential cache from here would\n * copy that graph into the half-trusted runtime image. Same reason `gitcred/gh-token-client.ts`\n * keeps its distance.\n *\n * Why this exists at all instead of GitHub's `enablePullRequestAutoMerge`: that mutation refuses\n * every pull request whose `mergeStateStatus` is not BLOCKED — \"clean status\" when the checks\n * passed, \"unstable status\" while they run on a repository with no REQUIRED checks — so on most\n * repositories it can never be armed. The readiness rule below is the one operators actually\n * mean, and it is evaluated against the CURRENT head on every tick: merge-when-ready has to\n * allow the fix commit that turns the checks green.\n */\n\n/** What a tick needs to decide, and nothing more. */\nexport interface PrSnapshot {\n prId: string\n headOid: string\n state: 'OPEN' | 'CLOSED' | 'MERGED'\n isDraft: boolean\n /** GitHub's own mergeability verdict; `UNKNOWN` means it is still computing one. */\n mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN'\n reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | null\n checks: PrCheck[]\n}\n\nexport interface PrCheck {\n name: string\n /** `pending` covers queued/in-progress and a status context with no conclusion yet. */\n outcome: 'success' | 'failure' | 'pending'\n}\n\n/** Either \"merge it\" or the one line that says why not — the answer the console draws. */\nexport type Readiness = { ready: true } | { ready: false; waitingOn: string }\n\nconst MAX_NAMED_CHECKS = 3\n\n/**\n * The readiness rule: open, not a draft, no conflicts, no red or running check, and nobody\n * asking for changes.\n *\n * `REVIEW_REQUIRED` is NOT a blocker and that is deliberate: the operator ticking this box on\n * their own agent's pull request IS the approval, and on a repository with no required reviewers\n * GitHub reports `REVIEW_REQUIRED` forever, which would make the box a control that never fires.\n * `CHANGES_REQUESTED` blocks, because someone actively said no.\n *\n * `UNKNOWN` mergeability waits rather than merging: GitHub computes it asynchronously, and\n * treating \"not computed yet\" as \"no conflicts\" is how a merge-when-ready lands a broken tree.\n */\nexport function readiness(pr: PrSnapshot): Readiness {\n // Both terminal states are answered by `tick` before it gets here, so these two arms are for a\n // DIRECT caller (and for the tests that pin the rule) rather than for the loop.\n if (pr.state === 'MERGED') return { ready: false, waitingOn: 'already merged' }\n if (pr.state === 'CLOSED') return { ready: false, waitingOn: 'the pull request is closed' }\n if (pr.isDraft) return { ready: false, waitingOn: 'the pull request is a draft' }\n if (pr.reviewDecision === 'CHANGES_REQUESTED') return { ready: false, waitingOn: 'changes requested' }\n if (pr.mergeable === 'CONFLICTING') return { ready: false, waitingOn: 'conflicts with the base branch' }\n if (pr.mergeable === 'UNKNOWN') return { ready: false, waitingOn: 'GitHub is still computing mergeability' }\n const failed = pr.checks.filter((check) => check.outcome === 'failure')\n if (failed.length > 0) return { ready: false, waitingOn: `failing checks: ${names(failed)}` }\n const pending = pr.checks.filter((check) => check.outcome === 'pending')\n if (pending.length > 0) return { ready: false, waitingOn: `checks running: ${names(pending)}` }\n return { ready: true }\n}\n\nfunction names(checks: PrCheck[]): string {\n const head = checks.slice(0, MAX_NAMED_CHECKS).map((check) => check.name || 'unnamed')\n return checks.length > head.length ? `${head.join(', ')} +${checks.length - head.length}` : head.join(', ')\n}\n\nconst SNAPSHOT_QUERY = `\nquery AutoMerge($owner:String!,$name:String!,$number:Int!){\n repository(owner:$owner,name:$name){\n pullRequest(number:$number){\n id headRefOid state isDraft mergeable reviewDecision\n commits(last:1){nodes{commit{statusCheckRollup{contexts(first:100){nodes{\n __typename\n ... on CheckRun{name conclusion status}\n ... on StatusContext{context state}\n }}}}}}\n }\n }\n}`\n\nconst MERGE_MUTATION =\n 'mutation($id:ID!,$oid:GitObjectID!){mergePullRequest(input:{pullRequestId:$id,mergeMethod:SQUASH,expectedHeadOid:$oid}){clientMutationId}}'\n\n/** Raised when GitHub answered but refused — as opposed to never being reached. Both keep the\n * watcher armed; only the wording the console shows differs. */\nexport class AutoMergeGithubError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AutoMergeGithubError'\n }\n}\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>\n\nexport interface GithubAccess {\n token: () => Promise<string>\n fetchImpl?: FetchLike\n /** GitHub GraphQL endpoint; overridden only by tests and GHES. */\n endpoint?: string\n}\n\n/** One GraphQL round trip. GraphQL reports refusals inside a 200, so `errors` decides here. */\nasync function graphql<T>(access: GithubAccess, query: string, variables: Record<string, unknown>): Promise<T> {\n return send(access, await access.token(), query, variables)\n}\n\n/** The round trip with the token ALREADY in hand. Split out so a caller that must not await anything\n * between its last abort check and the request can acquire the token first — see `squashMerge`. */\nasync function send<T>(\n access: GithubAccess,\n token: string,\n query: string,\n variables: Record<string, unknown>\n): Promise<T> {\n const fetchImpl = access.fetchImpl ?? ((input: string, init?: RequestInit) => fetch(input, init))\n const res = await fetchImpl(access.endpoint ?? 'https://api.github.com/graphql', {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n accept: 'application/vnd.github+json',\n 'content-type': 'application/json',\n 'user-agent': 'agentconnect-auto-merge'\n },\n body: JSON.stringify({ query, variables })\n })\n if (!res.ok) throw new AutoMergeGithubError(`github answered ${res.status}`)\n const body = (await res.json()) as { data?: T | null; errors?: Array<{ message?: string }> }\n if (body.errors?.length) {\n throw new AutoMergeGithubError(body.errors.map((e) => e.message ?? 'unknown').join('; '))\n }\n if (!body.data) throw new AutoMergeGithubError('github returned no data')\n return body.data\n}\n\ninterface SnapshotAnswer {\n repository: {\n pullRequest: {\n id: string\n headRefOid: string | null\n state: 'OPEN' | 'CLOSED' | 'MERGED'\n isDraft: boolean\n mergeable: string\n reviewDecision: string | null\n commits: {\n nodes: Array<{\n commit: { statusCheckRollup: { contexts: { nodes: Array<Record<string, unknown>> | null } } | null }\n }> | null\n }\n } | null\n } | null\n}\n\nexport async function fetchSnapshot(access: GithubAccess, repoFullName: string, prNumber: number): Promise<PrSnapshot> {\n const [owner, name] = repoFullName.split('/')\n if (!owner || !name) throw new AutoMergeGithubError(`malformed repository name ${repoFullName}`)\n const answer = await graphql<SnapshotAnswer>(access, SNAPSHOT_QUERY, { owner, name, number: prNumber })\n const pr = answer.repository?.pullRequest\n if (!pr) throw new AutoMergeGithubError('pull request not visible to this token')\n const contexts = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes ?? []\n return {\n prId: pr.id,\n headOid: pr.headRefOid ?? '',\n state: pr.state,\n isDraft: pr.isDraft,\n mergeable: pr.mergeable === 'MERGEABLE' || pr.mergeable === 'CONFLICTING' ? pr.mergeable : 'UNKNOWN',\n reviewDecision:\n pr.reviewDecision === 'APPROVED' || pr.reviewDecision === 'CHANGES_REQUESTED'\n ? pr.reviewDecision\n : pr.reviewDecision === 'REVIEW_REQUIRED'\n ? 'REVIEW_REQUIRED'\n : null,\n checks: contexts.map(toCheck)\n }\n}\n\n/** A rollup context is a CheckRun or a StatusContext, and the two spell their outcome differently. */\nfunction toCheck(node: Record<string, unknown>): PrCheck {\n if (node.__typename === 'StatusContext') {\n const state = String(node.state ?? '')\n return {\n name: String(node.context ?? ''),\n outcome: state === 'SUCCESS' ? 'success' : state === 'PENDING' || state === '' ? 'pending' : 'failure'\n }\n }\n const conclusion = String(node.conclusion ?? '')\n const status = String(node.status ?? '')\n // A skipped or neutral run is not a failure, and a cancelled one is: it never reported success.\n const outcome: PrCheck['outcome'] =\n status !== 'COMPLETED' || conclusion === ''\n ? 'pending'\n : conclusion === 'SUCCESS' || conclusion === 'SKIPPED' || conclusion === 'NEUTRAL'\n ? 'success'\n : 'failure'\n return { name: String(node.name ?? ''), outcome }\n}\n\n/**\n * Squash-merge, pinned to the head the readiness verdict was formed against — never to a head\n * the operator saw in the panel minutes ago. A commit landing mid-tick refuses here and the\n * next tick judges the new head on its own merits, which is the whole point of \"when ready\".\n *\n * The token is fetched BEFORE the last abort check, not inside the request: fetching it is itself an\n * await (a pod reads it over the gitcred tunnel), and a disarm landing in that window would otherwise\n * be invisible — the check would have passed already and the POST would go out regardless. Answers\n * `false` when the fence closed instead, so nothing was sent.\n */\nexport async function squashMerge(access: GithubAccess, pr: PrSnapshot, aborted?: () => boolean): Promise<boolean> {\n const token = await access.token()\n // Nothing may be awaited between here and the request; `send` takes the token already resolved.\n if (aborted?.()) return false\n await send(access, token, MERGE_MUTATION, { id: pr.prId, oid: pr.headOid })\n return true\n}\n\n/** What one tick did, for the caller to project as `waitingOn` / `lastError` / `merged`. */\nexport type TickOutcome =\n | { kind: 'merged' }\n /** Terminal for a reason that is not a merge: the pull request was CLOSED. The intent expired with\n * it, and a watcher left polling would merge it if the branch were ever reopened. */\n | { kind: 'closed' }\n /** The fence below closed while this tick was in flight, so the merge was never attempted. */\n | { kind: 'aborted' }\n | { kind: 'waiting'; waitingOn: string }\n | { kind: 'error'; error: string }\n\nexport interface TickOptions {\n /**\n * Checked synchronously in the instant before the merge mutation is sent, with the token already\n * resolved so nothing can be awaited in between.\n *\n * A tick awaits a snapshot and a token before it decides anything, and a disarm arriving inside\n * that window used to be invisible to it: the continuation went on to squash-merge a pull request\n * whose box the operator had already unticked and been told was off. Because the last check is\n * synchronous and immediately precedes the only mutation here, a caller that flips this predicate\n * knows that once it has, no merge can still BEGIN — which is what lets `disarm` answer honestly.\n */\n aborted?: () => boolean\n}\n\n/** One poll: read, judge, and merge if the verdict says so. Never throws — a tick's failure is\n * DATA the watcher keeps armed through, because the usual cure is the next commit. */\nexport async function tick(\n access: GithubAccess,\n repoFullName: string,\n prNumber: number,\n opts: TickOptions = {}\n): Promise<TickOutcome> {\n try {\n if (opts.aborted?.()) return { kind: 'aborted' }\n const pr = await fetchSnapshot(access, repoFullName, prNumber)\n if (pr.state === 'MERGED') return { kind: 'merged' }\n // Closed without merging ends the watch. Keeping it armed would leave a poll running for the life\n // of the pod and, worse, merge the pull request if it were ever reopened.\n if (pr.state === 'CLOSED') return { kind: 'closed' }\n const verdict = readiness(pr)\n if (!verdict.ready) return { kind: 'waiting', waitingOn: verdict.waitingOn }\n // Everything above is a read; below is the irreversible act. The gate is checked here to avoid\n // fetching a token at all for a watch already disarmed, and again inside `squashMerge` with the\n // token in hand — that second one is the check no further await can slip behind.\n if (opts.aborted?.()) return { kind: 'aborted' }\n if (!(await squashMerge(access, pr, opts.aborted))) return { kind: 'aborted' }\n return { kind: 'merged' }\n } catch (err) {\n return { kind: 'error', error: err instanceof Error ? err.message : String(err) }\n }\n}\n\n/** Default poll cadence. One pull request per armed box and a handful of boxes at a time, so a\n * minute is well inside GitHub's budget while still merging promptly after the last check. */\nexport const AUTO_MERGE_POLL_MS = 30_000\n","/**\n * The armed watcher as a running thing: a timer around `tick`, plus the status the console reads.\n *\n * A LEAF module beside `core.ts` for the same reason — it is bundled into the in-sandbox entry.\n * State is in memory and nowhere else: the entry point is a process in the agent's pod (cluster\n * placement) or an object in the daemon (local placement), so losing the pod or restarting the\n * daemon forgets the intent and the box reads back unchecked. That is the designed lifetime, not\n * a limitation — nobody is watching the pull request any more, and the console must not claim\n * otherwise.\n */\nimport { AUTO_MERGE_POLL_MS, tick, type GithubAccess, type TickOutcome } from './core.js'\n\nexport interface AutoMergeStatus {\n waitingOn?: string\n lastError?: string\n merged: boolean\n /** The pull request was closed without merging — terminal, like `merged`, and for the same reason:\n * nothing is watching any more, so the console must not draw an armed box over it. */\n closed?: boolean\n}\n\nexport interface AutoMergeLoopDeps {\n access: GithubAccess\n repoFullName: string\n prNumber: number\n pollMs?: number\n /** Called after every tick, so a host can log it or hand it to its own reader. */\n onStatus?: (status: AutoMergeStatus) => void\n /** Timer seam — a test drives ticks without waiting for a real minute. */\n timers?: {\n setInterval: (fn: () => void, ms: number) => unknown\n clearInterval: (handle: unknown) => void\n }\n}\n\nexport class AutoMergeLoop {\n private handle?: unknown\n private running = false\n /** Bumped by every `stop()`. A tick captures it on entry and refuses to merge once it has moved,\n * which is how a disarm arriving mid-tick fences the mutation instead of racing it. */\n private generation = 0\n private inflight?: Promise<AutoMergeStatus>\n private status: AutoMergeStatus = { merged: false }\n private readonly timers: NonNullable<AutoMergeLoopDeps['timers']>\n\n constructor(private readonly deps: AutoMergeLoopDeps) {\n this.timers = deps.timers ?? {\n setInterval: (fn, ms) => setInterval(fn, ms),\n clearInterval: (handle) => clearInterval(handle as ReturnType<typeof setInterval>)\n }\n }\n\n /** Arm: one immediate tick (an already-green pull request should not wait out a poll), then the\n * cadence. Idempotent — arming an armed loop keeps the one timer it has. */\n start(): void {\n if (this.handle !== undefined) return\n this.handle = this.timers.setInterval(() => void this.run(), this.deps.pollMs ?? AUTO_MERGE_POLL_MS)\n void this.run()\n }\n\n /** Disarm. The generation moves FIRST and unconditionally: a tick already awaiting GitHub reads it\n * before it merges, and bumping it even for an already-stopped loop keeps that fence honest. */\n stop(): void {\n this.generation++\n if (this.handle === undefined) return\n this.timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** Resolves once no tick is in flight. `stop()` guarantees no merge can BEGIN; awaiting this also\n * means none is still in the air, so a caller can answer \"off\" without a merge landing behind it. */\n async settle(): Promise<void> {\n while (this.inflight) await this.inflight.catch(() => undefined)\n }\n\n /** True until the watch ENDS — a merge, or the pull request being closed. The host drops the entry\n * on that falling edge; either way nothing is watching and the box must read unchecked. */\n armed(): boolean {\n return this.handle !== undefined && !this.status.merged && !this.status.closed\n }\n\n current(): AutoMergeStatus {\n return { ...this.status }\n }\n\n /** One tick, guarded against overlap: a slow GitHub must not stack requests behind itself. */\n async run(): Promise<AutoMergeStatus> {\n if (this.running) return this.current()\n this.running = true\n const generation = this.generation\n const attempt = (async () => {\n try {\n this.apply(\n await tick(this.deps.access, this.deps.repoFullName, this.deps.prNumber, {\n aborted: () => this.generation !== generation\n })\n )\n } finally {\n this.running = false\n this.inflight = undefined\n }\n this.deps.onStatus?.(this.current())\n return this.current()\n })()\n this.inflight = attempt\n return attempt\n }\n\n /** A merge is terminal (the timer goes); an error keeps the loop armed, because the usual cure\n * is the next commit and disarming would throw away the operator's intent on one red tick. */\n private apply(outcome: TickOutcome): void {\n if (outcome.kind === 'merged') {\n this.status = { merged: true }\n this.stop()\n return\n }\n if (outcome.kind === 'closed') {\n this.status = { merged: false, closed: true, waitingOn: 'the pull request was closed' }\n this.stop()\n return\n }\n // Disarmed mid-flight: the status this tick would have written describes a watch that no longer\n // exists, so the last one the operator actually saw stands.\n if (outcome.kind === 'aborted') return\n this.status =\n outcome.kind === 'waiting'\n ? { merged: false, waitingOn: outcome.waitingOn }\n : { merged: false, lastError: outcome.error }\n }\n}\n","/**\n * The three environment names the credential channel travels on.\n *\n * A leaf on purpose: the same helper source runs as a daemon CLI subcommand and inside a sandbox\n * pod, and the in-sandbox build asserts that its bundle imports nothing but node builtins. Keeping\n * these here — rather than in `cp/gitcred-server.ts`, which pulls the daemon's credential cache —\n * is what lets one implementation serve both.\n */\n\nexport const GITCRED_CAPABILITY_ENV = 'AC_GITCRED_CAPABILITY'\n/** The agent identity minted TOGETHER with the capability (git-injection\n * gitCredentialEnv). Helpers prefer this pair over the agentId baked into a\n * `.git/config` helper line, which goes stale when an agent is deleted and\n * recreated under the same name over a surviving checkout. */\nexport const GITCRED_AGENT_ENV = 'AC_GITCRED_AGENT'\n/** Where a helper finds the socket, when that is not under this daemon's own root. A helper\n * running in a sandbox pod reaches the daemon through the shim's tunnel instead, and the pod's\n * filesystem has no daemon root to derive a path from. Non-secret: it is a path, and the\n * capability is what authorizes the request that travels over it. */\nexport const GITCRED_SOCKET_ENV = 'AC_GITCRED_SOCKET'\n","// The gitcred socket call itself, split out of `gh-token-client.ts` so a second in-sandbox entry can\n// reach a gh token without also pulling in the gh-argv target resolver and its `git remote` probe.\n// Node builtins only: every consumer of this file is bundled into the runtime image.\nimport { createConnection } from 'node:net'\nimport { GITCRED_CAPABILITY_ENV } from './env.js'\n\nexport interface GitCredIpcReply {\n ok: boolean\n password?: string\n error?: string\n}\n\n/** One newline-delimited-JSON round trip on the gitcred socket. Never rejects: an unreachable\n * daemon is an answer (`ok:false`), and callers report it as data. */\nexport function gitcredIpc(path: string, msg: unknown): Promise<GitCredIpcReply> {\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as GitCredIpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n\n/** A GH_TOKEN-plane token for one repository, or a thrown reason. Fetched per use rather than\n * cached here: these tokens are short-lived, and the daemon/CP side already caches and clamps. */\nexport async function fetchGhToken(\n args: { agentId: string; repoFullName: string; socketPath: string; capability?: string },\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n const res = await gitcredIpc(args.socketPath, {\n op: 'get',\n agentId: args.agentId,\n capability: args.capability ?? env[GITCRED_CAPABILITY_ENV],\n plane: 'gh',\n repoFullName: args.repoFullName\n })\n if (!res.ok || !res.password) {\n throw new Error(\n `no gh credentials for agent ${args.agentId} on ${args.repoFullName}: ${res.error ?? 'unknown error'}`\n )\n }\n return res.password\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh and agent-browser wrappers. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Pod env naming the Chrome the image bakes — agent-browser's only browser-location hook, so an ACP child\n * without it downloads one of its own. Set by the image, projected onto the child by acp-runner. */\nexport const SANDBOX_BROWSER_EXECUTABLE_ENV = 'AGENT_BROWSER_EXECUTABLE_PATH'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","#!/usr/bin/env node\n// The in-sandbox merge-when-ready watcher: one process per armed pull request, spawned by the shim\n// when the daemon arms the box and killed when it disarms. Its own entry for the same reason the\n// credential helper and the gh token fetch are — the image copies ONE file per bundle, so two\n// entries whose graphs are disjoint stay two single files where a shared module would emit a chunk\n// nothing copies.\n//\n// It runs HERE, in the agent's pod, so the watcher's lifetime is the sandbox's: a reclaimed pod\n// takes the intent with it and the console's box reads back unchecked, with nothing persisted\n// anywhere to contradict that. It holds no policy — the daemon decided this agent may merge this\n// repository, and the token it fetches per tick is clamped CP-side.\nimport { AUTO_MERGE_POLL_MS } from '../github/auto-merge/core.js'\nimport { AutoMergeLoop } from '../github/auto-merge/loop.js'\nimport { GITCRED_SOCKET_ENV } from '../gitcred/env.js'\nimport { fetchGhToken } from '../gitcred/gh-token-ipc.js'\nimport { SANDBOX_TUNNEL_PATHS } from './sandbox-paths.js'\n\n/** Poll cadence override, so an operator can tighten it without a new image. */\nexport const AUTO_MERGE_POLL_ENV = 'AC_AUTO_MERGE_POLL_MS'\n\nasync function main(): Promise<number> {\n // `<agentId> <owner/repo> <prNumber>`, positional and in that order — the shim spawns this.\n const [agentId, repoFullName, prRaw] = process.argv.slice(2)\n const prNumber = Number(prRaw)\n if (!agentId || !repoFullName || !Number.isInteger(prNumber) || prNumber <= 0) {\n process.stderr.write('agentconnect: auto-merge expects <agentId> <owner/repo> <prNumber>\\n')\n return 2\n }\n // The tunnel's path unless something names another; a pod has no daemon root to derive one from.\n const socketPath = process.env[GITCRED_SOCKET_ENV]?.trim() || SANDBOX_TUNNEL_PATHS.gitcred\n const pollMs = Number(process.env[AUTO_MERGE_POLL_ENV]) || AUTO_MERGE_POLL_MS\n\n // One line of NDJSON per tick on stdout — the shim reads it back as this watcher's status, so\n // the console can say what the merge is waiting on. Nothing else is ever written to stdout.\n const loop = new AutoMergeLoop({\n access: { token: () => fetchGhToken({ agentId, repoFullName, socketPath }) },\n repoFullName,\n prNumber,\n pollMs,\n onStatus: (status) => {\n // BOTH terminal states exit: merged, and the pull request being closed. Exiting is what tells the\n // shim to drop its entry rather than leaving a process that will never do anything again — and a\n // closed watcher left alive is one that would merge the branch if it were ever reopened. The exit\n // waits for the WRITE to flush: stdout is a pipe here, and exiting first would lose the status.\n const done = status.merged || status.closed === true\n process.stdout.write(JSON.stringify(status) + '\\n', () => {\n if (done) process.exit(0)\n })\n if (done) loop.stop()\n }\n })\n for (const signal of ['SIGTERM', 'SIGINT'] as const) {\n process.on(signal, () => {\n // The same fence the daemon-local watcher uses: `stop()` moves the generation the tick in flight\n // checks before it merges, and settling before exit means the disarm this signal IS cannot be\n // answered while a squash could still begin in here.\n loop.stop()\n void loop.settle().then(() => process.exit(0))\n })\n }\n loop.start()\n await new Promise<void>(() => {})\n return 0\n}\n\nmain().then(\n (code) => process.exit(code),\n (err: unknown) => {\n process.stderr.write(`agentconnect: auto-merge failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":"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"}