import { spawn } from "node:child_process"; import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core"; /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */ type ExecOpts = Parameters[1]; /** Construction config for {@link RemoteLocalDockerExecutionEnv}. */ export interface LocalDockerEnvConfig { /** * Base container image to run the task in. **REQUIRED — no default** (domestic-images iron rule: the registry * is the operator's choice, never a hardcoded `docker.io` origin). e.g. the docker.io/claybobby pool or an * in-network `node:20` mirror (SWR deprecated). */ image: string; /** * S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的 * 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。 * 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask), * 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。 * 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来; * adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。 */ homeDir?: string; /** Workspace root INSIDE the container; relative paths resolve against it. Default {@link DEFAULT_MOUNT_PATH}. */ mountPath?: string; /** Path to the docker binary (or a docker-compatible CLI). Default "docker" (must be on PATH in the worker). */ dockerPath?: string; /** * DOCKER_HOST override for the docker CLI (e.g. a non-default socket). Passed as `-H ` on EVERY docker * invocation (not the process env) so it can't leak / drift. Absent = the daemon DOCKER_HOST points at. * * Pointing this OFF-BOX (`tcp://host:2376`, `ssh://user@host`) is exactly the DUAL-MODE-DESIGN §5 * **`remote-docker`** provider — see {@link remoteDockerExecutionEnvFactory} (remote-env-host.ts), which is * this adapter with the endpoint required. Nothing else about the class is host-local: the workspace is * `mkdir`'d INSIDE the container (never a bind-mount of a host path) and bytes move by `docker cp` / * `docker exec cat`, which stream over the daemon API. */ dockerHost?: string; /** * Provider NAME reported on the {@link WorkspaceHandle}. Default `"local-docker"`; the `remote-docker` arm * passes `"remote-docker"` so a handle never mislabels which lane it is (the CLASS is shared — `remote-docker` * IS this adapter aimed at a remote daemon). Cosmetic/observability only: it selects no behavior. */ providerLabel?: string; /** Container memory limit (docker `--memory`, e.g. "2g"). Absent = no limit. */ memory?: string; /** Container CPU limit (docker `--cpus`, e.g. 1.5). Absent = no limit. */ cpus?: number; /** Max processes in the container (docker `--pids-limit`) — a fork-bomb backstop on the shared host kernel. * Default 512 (generous for builds; raise for heavy parallelism). Set 0/negative to disable (not advised). */ pidsLimit?: number; /** Drop ALL Linux capabilities (docker `--cap-drop ALL`) — hardens this code sandbox, but BREAKS workloads * that need caps (some installs/builds). Default OFF (the container boundary + no-new-privileges + pids-limit * are always on). Turn ON for a genuinely-untrusted lane; operators re-add caps as needed. */ dropAllCaps?: boolean; /** Container network mode (docker `--network`). Absent = docker default (bridge, isolated net-ns). 🔴 `"host"` * SHARES the host network namespace — it removes the net-ns isolation dimension and exposes host-local * services (DBs/admin ports/metadata) to agent-run code. It is NOT a true isolation boundary; use only when * the lane is operator-trusted and needs host networking. */ network?: "none" | "bridge" | "host"; /** * Out-of-band env injected into EVERY command via `docker exec -e` (secrets etc.). The control plane resolves * secret env-NAMEs from the worker's own `process.env` and passes resolved values here — they never enter the * model prompt or the tool command string. Merged UNDER per-command `options.env` (per-call wins). */ env?: Record; /** Per-command wall-clock bound (ms) when the caller gives none. Default {@link DEFAULT_COMMAND_TIMEOUT_MS}. */ commandTimeoutMs?: number; /** Wall-clock bound (ms) for a docker control op (run/rm/inspect/cp). Default {@link DEFAULT_CONTROL_TIMEOUT_MS}. */ controlTimeoutMs?: number; /** Stable id mixed into the container NAME (e.g. the sessionId). A random suffix is always appended so two * envs sharing an id never collide. Default = a random id. */ id?: string; } /** Inject `spawn` for tests (a fake docker CLI). Defaults to node's child_process spawn. */ export interface LocalDockerEnvDeps { spawn?: typeof spawn; } export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv { /** DUAL-MODE-DESIGN §5: a container is a real isolation boundary; v1 has no managed memory snapshot. */ readonly capabilities: { isolation: boolean; suspendable: boolean; }; /** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */ cwd: string; /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */ readonly homeDir?: string | undefined; private readonly cfg; private readonly spawnFn; /** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */ private readonly containerName; /** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */ private readonly providerName; /** Set once the container is up + the workspace dir created. */ private containerId?; private handle?; /** Terminal once {@link destroy} ran — guards exec/fs from running against a removed container. */ private destroyed; /** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */ private connecting?; constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps); private resolve; workspaceHandle(): WorkspaceHandle; connect(config?: RemoteConnectConfig): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; private doConnect; suspendVM(_options?: VmLifecycleOptions): Promise<{ ok: true; value: SnapshotId; } | { ok: false; error: RemoteExecutionError; }>; resumeVM(_snapshotId: SnapshotId, _options?: VmLifecycleOptions): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; postResumeInit(): Promise<{ ok: true; value: void; } | { ok: false; error: RemoteExecutionError; }>; /** * Liveness re-check (NOT a snapshot resume): if the container is still running → idempotent success; if it's * gone (crashed / externally removed) → `connect_failed` (no managed snapshot to re-attach to). `sessionToken` * is the container name (== our handle id); a mismatch with this env's container fails closed. */ reconnect(sessionToken: SessionToken): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; /** `docker rm -f` the container (full teardown). Idempotent, best-effort, never throws (cleanup contract). */ destroy(): Promise; cleanup(): Promise; private forceRemove; exec(command: string, options?: ExecOpts): Promise>; execStream(command: string, options?: ExecStreamOptions): AsyncIterable; absolutePath(p: string): Promise>; joinPath(parts: string[]): Promise>; readBinaryFile(p: string, abortSignal?: AbortSignal): Promise>; readTextFile(p: string, abortSignal?: AbortSignal): Promise>; readTextLines(p: string, options?: { maxLines?: number; abortSignal?: AbortSignal; }): Promise>; writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise>; appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise>; private posixFsInst?; private get posixFs(); fileInfo(p: string, abortSignal?: AbortSignal): Promise>; listDir(p: string, abortSignal?: AbortSignal): Promise>; readLink(p: string, abortSignal?: AbortSignal): Promise>; /** S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。一次往返里的一个 `readlink` * 循环(上限 64、只锚不折、遇环即止),脚本与读法的单一属主在 `remote-shell.ts`,四条 shell 腿共用同一份。 * 缺席读法用不上(本腿在场);`ok:false` 由 core 读作「这一次没有跳」,永不是拒绝。 */ canonicalChain(p: string, abortSignal?: AbortSignal): Promise>; canonicalPath(p: string, abortSignal?: AbortSignal): Promise>; exists(p: string, abortSignal?: AbortSignal): Promise>; createDir(p: string, options?: { recursive?: boolean; abortSignal?: AbortSignal; }): Promise>; remove(p: string, options?: { recursive?: boolean; force?: boolean; abortSignal?: AbortSignal; }): Promise>; createTempDir(prefix?: string, abortSignal?: AbortSignal): Promise>; createTempFile(options?: { prefix?: string; suffix?: string; abortSignal?: AbortSignal; }): Promise>; /** Prepend `-H ` to a docker argv when configured (per-invocation, not the process env). */ private withHostFlag; /** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`). * argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`. */ private execArgv; /** Like {@link execArgv} but for an internal FileSystem helper command (uses the workspace cwd + adapter env). */ private execRaw; /** Spawn the docker binary with argv; collect stdout/stderr (+ raw bytes when binary), bound by timeout/abort. */ private docker; /** Lazy connect for an exec path → ExecutionError on failure (fail closed, never throws). */ /** S1([ref]):容器内**用户命令**的 shell。缺省 /bin/sh;首次连接后探测一次 `command -v bash`, * 有则切 bash(TB 实测 dash 三病:$'\t' 静默错字节/[[ ]] 127/进程替换)。镜像无 bash(alpine 裸底) * ⇒ 保 sh(修前字节形,诚实降级);探测自身失败同。容器**创建**引导命令(mkdir && tail)不走 * 本 shell——无 bash 语法且探测尚不可能。 */ private containerShell; private containerShellProbed; private probeContainerShell; private ensureConnectedExec; /** Lazy connect for a FileSystem path → FileError on failure (fail closed, never throws). */ private fsReady; /** Map an ExecutionError from a docker CONTROL op (run/inspect) onto a RemoteExecutionError code. */ private dockerToRemoteError; } /** * `ExecutionEnvFactory` for the TOC `local-docker` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment * makes its agent run each task in a per-task container on the worker's OWN docker daemon (isolation:true, * suspendable:false). Returns the env UNCONNECTED (lazy — the container starts on first fs/exec use); the Runner * owns the lifetime and calls `destroy()` (= `docker rm -f`) on task end. Folds the per-task `ctx.taskId`/ * `ctx.sessionId` into the container name so two concurrent tasks never share a container. */ export declare function localDockerExecutionEnvFactory(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory; export {}; //# sourceMappingURL=remote-env-local-docker.d.ts.map