import { ExecutionError, FileError, RemoteExecutionError, type ExecutionEnv, type ExecutionEnvFactory, type ExecStreamOptions, type FileInfo, type OutputChunk, type RemoteConnectConfig, type RemoteExecutionEnv, type Result, type SessionToken, type SnapshotId, type VmLifecycleOptions, type WorkspaceHandle, type WriteExpectation, type WriteReceipt } from "@sema-agent/core"; import { type DeviceOwner, type DeviceRejectCode, type DeviceRow, type DeviceStore } from "../device-store.js"; import type { DeviceEnrollment } from "../device-enrollment.js"; import type { DeviceWsHub } from "../device-ws-hub.js"; import type { DeviceInstructionKind, DeviceOutcomeErrorCode } from "../device-ws-protocol.js"; /** `WorkspaceHandle.provider` 与 `REMOTE_EXEC` 词表的那一个词,单点。 */ export declare const DEVICE_PROVIDER: "device"; /** core 没从包根导出 `ExecutionEnvExecOptions` —— 从接口上取,与六条兄弟腿逐字同姿势 * (`remote-env-host.ts` 的 `type ExecOpts = Parameters[1]`)。 */ type ExecOpts = Parameters[1]; /** * 设备**结局帧**的错误码 → core `FileErrorCode`。 * * `Record` 是**穷举**的:协议的结局码闭集加词而此处不加行 = 编译红 * ([ref] 词表纪律;不是运行时 switch 的默认臂)。 * * 🔴 三条不许折错的线: * ① `outcome_unknown` **原样过境**——core 把它排除在 `RETRYABLE_REMOTE_ERROR_CODES` 之外正是为了 * 「可能已经执行过」这一格;折成 `timeout`/`transport_lost` 会让上层按可重试处理,重驱已提交副作用。 * ② `target_unavailable`(never-started 的诚实词)在 `FileErrorCode` 上**没有对位词** —— 落 * `transport_lost`:它的契约是「连接故事已知、重连后可重试」,而 never-started 的重试确实是安全的。 * 诚实残余:file 面因此**分不出**「从未开始」与「传输中断」,两者都落在同一个可重试桶里(方向对、 * 粒度粗)。要分得开需要 core 在 `FileErrorCode` 上补 never-started 词 —— 已作 O12 的后续请托登记。 * ③ exec 专属词(`shell_unavailable`/`spawn_error`/`callback_error`/`suspended`/`auth_failed`)出现在 * file 面上是**设备端协议违约**(它把一个 exec 结局贴到一次文件操作上)⇒ 落 `unknown`,不猜。 */ export declare const DEVICE_OUTCOME_TO_FILE_ERROR: Readonly>; /** * 设备**结局帧**的错误码 → core `ExecutionErrorCode`。穷举同上。 * * 🔴 file 专属词(`already_exists`/`is_directory`/`not_directory`/`not_found`/`permission_denied`/ * `precondition_failed`/`invalid`)出现在 exec 面同样是协议违约 ⇒ `unknown`,不猜。 * `not_supported` 在 `ExecutionErrorCode` 上无对位词 —— 落 `shell_unavailable`(它的契约就是「命令 * **从未启动**,因为可执行面解析不了」,与「设备没有这条指令面」同侧;never-started 语义保住)。 */ export declare const DEVICE_OUTCOME_TO_EXEC_ERROR: Readonly>; type RejectCode = DeviceRejectCode; /** * 汇聚端的 **typed 拒**(投递门**之前**,§5.3 投递边界:设备从未可能见过这条指令)→ core 错误码。 * * 🔴 全部落在**可安全重试**的那一侧,这是投递边界给的硬事实,不是乐观估计。exec 面用 O12 的 * `target_unavailable`(「目标在 commit 前拒了,命令从未启动」的诚实词);file 面无对位词,同上落 * `transport_lost`。 * 例外 `identity_mismatch` / `revoked` / `not_found`:那是**永久**拒(换设备/吊销/不是你的),重试 * 只会同样失败 —— exec 面落 `auth_failed`(NEVER retry 的那一格),file 面无对位词落 `permission_denied` * (「这条路你走不通,而且不是暂时的」是它最近的语义)。 * `draining`([ref] 借用位)与 busy 同侧(commit 前拒,从未启动)—— 判别靠 rejectText 把 `[draining]` * 整句带进文案:重试该去**替换副本**,不是稍后打同一个。 */ export declare const DEVICE_REJECT_TO_EXEC_ERROR: Readonly>; export declare const DEVICE_REJECT_TO_FILE_ERROR: Readonly>; export interface DeviceEnvLogger { warn(msg: string, fields?: Record): void; info?(msg: string, fields?: Record): void; } export interface DeviceEnvFactoryDeps { /** S-213④ / core [ref]:设备上执行用户的 home(`ExecutionEnv.homeDir` 座)。由 `boot/execution-env.ts` * 从唯一属主 `executionLaneHomeDir` 递进;缺席 ⇒ 本 env 声明「没有 home」,`~/` 规则判 unreadable。 */ homeDir?: string; hub: DeviceWsHub; store: DeviceStore; enrollment: DeviceEnrollment; /** 指令级默认超时(ms;`DEVICE_EXEC_TIMEOUT_MS`,§4.7)。core 的 per-command `timeout` 可收紧它。 */ execTimeoutMs?: number; /** §4.3.3 蜂道:`deviceCwdHonored` 闸下由 resolve-spec 注册的 per-session cwd(**单写者**)。 */ perSessionCwd?: ReadonlyMap; perSessionShellEnv?: ReadonlyMap>; logger?: DeviceEnvLogger; } interface DeviceEnvConfig { hub: DeviceWsHub; enrollment: DeviceEnrollment; rootSessionId: string; deviceId: string; owner: DeviceOwner; cwd: string; /** S-213④ / core [ref] —— 员工设备上执行用户的 home(`ExecutionEnv.homeDir` 座)。缺席 ⇒ 本 env 声明 * 「我没有 home」,`~/` 形权限规则对本腿判 unreadable(fail-closed)。值经 `boot/execution-env.ts` * 从唯一属主 `executionLaneHomeDir` 递进;设备是运行期连进来的,boot 期只有运维显式声明才有值。 */ homeDir?: string; shellEnv?: Record; execTimeoutMs: number; /** 设备当前连接声明的指令面(`undefined` = 铸 env 时设备不在线 ⇒ 可选面一律缺席)。 */ supports: ReadonlySet | undefined; logger?: DeviceEnvLogger; } export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv { private readonly cfg; /** core `ExecutionEnv.homeDir`([ref]):设备上执行用户的 home。见 `DeviceEnvConfig.homeDir`。 */ readonly homeDir?: string | undefined; /** * 🔴 `suspendable: false` 在 core 契约里**同时断言**「工作区在目标上外部持久」(remote-env.ts 的 * load-bearing 不变量)—— 设备工作区 = 员工机磁盘,真持久 ✅,park-only 降级腿因此合法可用。 * 谁要把 executor 的工作区改成临时目录,先来改这一行:那一刻这个 `false` 就变成谎话,park 赎回会 * 落到一个空工作区上而链路上没有任何一层会报错。 */ /** * 🔴 **S-354 / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath` * 就是**命令真正跑的那块文件系统**的权威答案(设备 WS 协议的**必答** `canonicalPath` op)。声明它 = 告诉引擎:整链答不出来是**解析器出了事** * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。 * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。 * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的 * 布尔字面量零影响(三键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由 * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。 * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。 */ readonly capabilities: { isolation: boolean; suspendable: boolean; canonicalPathAuthoritative: boolean; }; readonly externalContentTarget = true; /** [ref]:设备路径不在 worker 宿主文件系统上 —— host-path 回执消费方据此拒绝把它当宿主路径呈现。 */ readonly hostLocalPaths = false; cwd: string; /** 可选面(presence-typed,见文件头「铁律」段):构造期按设备声明装配,缺席就是缺席。 */ readLink?: (path: string, abortSignal?: AbortSignal) => Promise>; writeFileExclusive?: (path: string, content: string | Uint8Array, abortSignal?: AbortSignal) => Promise>; writeFileGuarded?: (path: string, content: string | Uint8Array, expect: WriteExpectation, abortSignal?: AbortSignal) => Promise>; private connected; /** 在飞的那一次准入(单飞记忆)——见 {@link admitShared}。settle 即清,**不缓存败诺**。 */ private connectPromise?; /** 拆除是**终态**(六条兄弟腿同纪律:destroy 之后 connect/exec/读面一律 fail-closed,不复活)。 */ private destroyed; constructor(cfg: DeviceEnvConfig); /** * 铸造时装配的**保护型**可选面(guarded / exclusive 写)。`connect()` 拿它与设备**此刻**的声明对拍。 * * 🔴 为什么只对拍这两条而不是全部 supports(codex R1-F2 的修法边界):这两条是 core 契约里 * 「缺席 ⇒ 调用方按 advisory 降级并自担 TOCTOU」的那一类 —— 它们的缺席是**放宽**方向。其余面 * (exec/read/…)缺席时调用会得到一条响亮的 `not_supported`,不构成静默降级。 */ private guardFaceSnapshot; /** * 设备**此刻**多出来的保护面(相对本 env 铸造时的快照)。非空 = 漂移。 * * 🔴 **为什么这条判据不能只守 `connect()`**(codex R2-F3,判真):`connect()` 一辈子只跑一两次,而 * 设备的 WS 可以**透明重连** —— 断开、以同一 deviceId 连回来,`this.connected` 全程不变,后续 * `dispatch` 直接用汇聚端的**当前**连接。于是「弱能力连接上铸的 env + 设备连回时带上了 guarded 面」 * 这一形跳过了 connect 期的闸:core 仍看不到 guarded 方法、仍走非原子写降级,而没有任何一层再问一次。 * 所以判据要落在**每一次非原子写之前**(那正是被降级的那条路),connect 期的检查是它的早期快门。 */ private guardFaceDrift; /** * 铸造时**缺席**的保护面 —— 交给 hub 在投递门上原子复核(codex R3-F1)。 * * adapter 侧的 {@link guardFaceDrift} 是**早期快门**:它与真正的下发之间隔着 `admitSession` 的一次 * SQL 往返,设备可以在那个窗口里以更强的能力连回来。投递门是本进程里唯一同时持有「最终选中的连接」 * 与「不可回退的 commit」的地方,所以权威判据在那儿;这里只负责把「我是在没有它们的前提下铸的」这句 * 话原样送过去。 */ private absentGuardFaces; private driftError; private resolve; absolutePath(p: string): Promise>; joinPath(parts: string[]): Promise>; workspaceHandle(): WorkspaceHandle; /** * 不建连接:做**准入 + 取活连接**(文件头「唯一范式差」)。**显式**入口 —— 首次调用任一面时 * {@link ensureConnected} 会自动走同一条路(单飞共享),所以这里重复调用是幂等的。 * * 🔴 **绝不换设备**:准入解出的 deviceId 与本 env 铸造时钉住的那一台不符 ⇒ typed * `device.identity_mismatch`(lane 是身份不是偏好,§4.5 步 2)。这一格正是 park 赎回期换绑/吊销的 * 落点:静默改投等于把一次会话的全部工作区假设搬到另一台机器上。 */ connect(config?: RemoteConnectConfig): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; /** * 准入的**单飞**入口:在飞的那一次由全部等待者共用(显式 `connect()` 与任意面的懒连接共享同一次), * settle 后记忆即清 —— 成功由 `connected` 位挡住重入,失败让下一次调用重新试(**不缓存败诺**, * 与 k8s/ssh/e2b 三腿逐字同纪律)。 */ private admitShared; /** * 懒连接:**首次调用任一面**(exec / execStream / 文件面)时自动走一次准入。 * * 🔴 为什么这条必须在 adapter 里(而不是「谁用谁先 connect」):task/run 车道**没有任何人**会替 * adapter 调 `connect()` —— 工厂铸出 env 就直接用它的 exec/文件面(本仓亲核:`src/` 里对执行环境的 * `.connect()` 只有 bench 与生命周期面自己)。所以一条「设了未连闸、又不自连」的腿,它的每一次工具 * 调用都被自家的闸挡回,而设备侧一条指令帧都收不到。e2b(首用建 VM)/ k8s(`connectPromise ??=`)/ * ssh(single-flight join)/ local-docker 四腿早就是懒连接;本腿从此同形,`connect()` 的语义 * (准入 + 取活连接 + 保护面对拍)一个字不动,只是触发时机与兄弟一致。 * * 返回 `undefined` = 可以往下走;否则是**三币种**的失败行,由调用面翻成自己那种货币。 */ private ensureConnected; /** `connect()` 的**本体**(准入 + 身份等值 + 保护面对拍)。失败以三币种形返回,见 {@link DeviceAdmissionFailure}。 */ private admit; /** 控制面重连(core 在 transport 重挂时调)——语义与 connect 逐字同一条:重新准入 + 身份等值。 */ reconnect(_sessionToken: SessionToken): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; 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; }>; private unsupported; /** * 解除本 env 与设备的关联。**不动设备上的工作区文件**(与 adb 腿「destroy 只断开不重置手机」同纪律 * —— 那是员工自己的盘,§11-O6 才是「要不要清理」这个问题的属主)。幂等,永不抛。 */ destroy(): Promise; cleanup(): Promise; /** * 终态 fail-closed + **懒连接**门(exec 面)。未连 ⇒ 先走一次准入,失败按 exec 货币报**准入自己的** * 拒因(不再吐一句「call connect() first」—— 这条车道上没有任何人能照做)。 */ private shellGate; /** 终态 fail-closed + **懒连接**门(文件面)。语义与 {@link shellGate} 逐字同一条,只换货币。 */ private fsGate; private timeoutMsFor; /** * 投递漏斗。**终态复核就在这里**,不在门上(codex 对抗复审 r1 [high],判真后收)。 * * 🔴 为什么门上那一次不够:门是 `async`,所以调用方从门返回到这里之间**让出过执行权** —— 一次 * `destroy()` 可以整个跑完再让调用方恢复,于是门读到的「未拆除」在投递那一刻已经过期(懒连接把门 * 变成异步的那一刻就开了这个窗;`connected` 快路径也一样让出)。本函数的函数体到 `hub.dispatch(input)` * 之间**没有任何 await**,所以它是投递前最后一个同步点 —— 闸要放在真正不可回退的那一格上。 * 三个面各自把这一态翻成自己的货币(穷举 switch,加臂 = 编译红)。 */ private dispatch; /** * 三态结果 → 文件面的 `Result`。 * * **穷举 switch,无 default 臂**:`DeviceDispatchResult` 加臂而此处不加 = 编译红。三态各自的 * 「重试安全性」在 hub 的类型注里逐字定义,本函数只做翻译,不重新判断。 */ private fileOutcome; /** 结局 kind 与请求面不符 = 设备协议违约:响亮拒,**不猜**(否则一个没发生的操作会被读成成功)。 */ private wrongOutcome; exec(command: string, options?: ExecOpts): Promise>; /** * 流式 exec。契约:按序 yield `stdout`/`stderr`,末尾**恰好一个** `exit` 片;失败在 exit 之前 throw * `RemoteExecutionError`(core `OutputChunk` 契约)。 * * 🔴 「恰好一个 exit」不是本层的信任,是**汇聚端已判过**的事实(hub 的 `expectsStreamTerminator` * 对声明了流的调用强制唯一终止片,缺了就把整条指令判失败而不是静默尾截断)。本层只负责不把它弄丢。 */ execStream(command: string, options?: ExecStreamOptions): AsyncIterable; exists(p: string, abortSignal?: AbortSignal): Promise>; canonicalPath(p: string, abortSignal?: AbortSignal): Promise>; fileInfo(p: string, abortSignal?: AbortSignal): Promise>; private doReadLink; readBinaryFile(p: string, abortSignal?: AbortSignal): Promise>; readTextFile(p: string, abortSignal?: AbortSignal): Promise>; readTextLines(p: string, options?: { maxLines?: number; abortSignal?: AbortSignal; }): Promise>; listDir(p: string, abortSignal?: AbortSignal): Promise>; writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise>; appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise>; private doWrite; /** * O13 守卫写:前置条件与写入在设备上的**同一原子步**里发生,回一份落盘对象收据。 * * 🔴 这是 §4.3.1「把裁决绑定到真实落盘对象」的**唯一**承载。它成立的边界要说清:关掉的是「调用方 * 铸前置条件」与「写入」之间那个窗;更早的策略裁决(例如等过人类审批的写门判决)不在这条收据里 —— * core 的方法注对此已逐字声明,本腿不越界宣称。 */ private doGuardedWrite; private warnMissingCreated; 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>; } /** * device lane 的 `ExecutionEnvFactory`(`boot/execution-env.ts` 的第七臂)。 * * 🔴 **异步工厂,且在铸 env 之前就把绑定读出来** —— 三个理由,每个都不能省: * ① `cwd` 是 `FileSystem` 的**同步属性**:缺省值 = 设备 hello 声明的 `workspaceRoot`,那在库里; * ② 可选面的 presence(`writeFileGuarded`…)必须在构造期定(core 降级律,见文件头); * ③ 「这个 placement 根本没绑设备」是**装配/准入错误**,不是一次运行期业务拒绝 —— 响亮抛,让 spawn * 失败(core 对 `isolation:"worktree"` 的同款姿势:宁可 throw,绝不静默给一个不对的 env)。 * 对比:「绑了但现在不在线」是运行期事实 ⇒ 构造成功,`connect()` 回 typed `device.offline`。 */ export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory; /** * §4.3.3 的 cwd 优先序 —— 与 host 腿的 `effectiveHostWorkspace` **同源**: * ① 本 session 显式注册的 cwd(`deviceCwdHonored` 闸下由 resolve-spec 写); * ② `ctx.parentCwd`(委派子代:自己没注册过 cwd,继承父的工作目录 —— 少了它子代会丢工作目录); * ③ 设备 hello 声明并落库的 `workspaceRoot`(缺 cwd 时的解析基准,有明确 wire 契约,不是「executor * 默认根」这种空话)。 */ export declare function effectiveDeviceCwd(sessionCwd: string | undefined, parentCwd: string | undefined, row: Pick): string; export {}; //# sourceMappingURL=remote-env-device.d.ts.map