import type { ExecutionErrorCode, FileErrorCode } from "@sema-agent/core"; /** 唯一的 upgrade 路径(§5.1:`GET /v1/device/ws`)。 */ export declare const DEVICE_WS_PATH = "/v1/device/ws"; /** 本 server 铸/认的协议版本。加帧/加字段 = additive 不升版;改语义 = 升版(§5.6)。 */ export declare const DEVICE_PROTOCOL_VERSION = 1; /** 低于此版本 ⇒ `helloReject: protocol_version_unsupported`(响亮拒,不静默降级,[ref])。 */ export declare const DEVICE_MIN_PROTOCOL_VERSION = 1; /** 签名的 domain tag —— 换域即换 tag,防止别处的 Ed25519 签名被拿来当握手用。 */ export declare const DEVICE_HELLO_DOMAIN_TAG = "sema.device.hello.v1"; /** * 传输面硬帽。 * * 🔴 `maxPreAuthFrameBytes` 与 `handshakeDeadlineMs` 是**未鉴权洪泛**的边界(§5.1 pre-auth 资源边界): * 它们必须早于任何 SQL 查询与大对象解析生效 —— 一条 1MiB 的垃圾首帧不该换来一次设备表查询。 */ export declare const DEVICE_WS_LIMITS: { /** 单帧上限(§5.1:文本帧 JSON,超限 = 协议错误)。 */ readonly maxFrameBytes: number; /** 鉴权**之前**允许的帧尺寸(hello 首帧)。 */ readonly maxPreAuthFrameBytes: 4096; /** upgrade 到 helloAck/helloReject 的墙钟上限。 */ readonly handshakeDeadlineMs: 5000; /** `payload` 下行分片的默认片长(单帧帽之下留出 base64 膨胀与帧头余量)。 */ readonly defaultPayloadPartBytes: number; }; /** 设备 → 服务端的帧型全集。 */ export declare const DEVICE_FRAME_TYPES: readonly ["hello", "generationAck", "heartbeat", "result", "chunk", "goodbye"]; export type DeviceFrameType = (typeof DEVICE_FRAME_TYPES)[number]; /** 服务端 → 设备的帧型全集。 */ export declare const DEVICE_SERVER_FRAME_TYPES: readonly ["challenge", "helloAck", "helloReject", "instruction", "payload", "cancel", "resultAck", "chunkAck", "bye", "ping"]; export type DeviceServerFrameType = (typeof DEVICE_SERVER_FRAME_TYPES)[number]; /** 握手拒因(§5.1/§5.2)。`auth_failed` 刻意同时承载「不存在的 deviceId」与「验签失败」——反枚举。 */ export declare const DEVICE_HELLO_REJECT_CODES: readonly ["auth_failed", "revoked", "protocol_version_unsupported", "draining", "already_connected"]; export type DeviceHelloRejectCode = (typeof DEVICE_HELLO_REJECT_CODES)[number]; /** 服务端主动断连的理由(§5.2/§8-R4/§8-R7)。 */ export declare const DEVICE_BYE_REASONS: readonly ["revoked", "draining", "superseded"]; export type DeviceByeReason = (typeof DEVICE_BYE_REASONS)[number]; /** 设备优雅下线的理由(§5.2)。 */ export declare const DEVICE_GOODBYE_REASONS: readonly ["shutdown", "sleep", "user_stop"]; export type DeviceGoodbyeReason = (typeof DEVICE_GOODBYE_REASONS)[number]; /** 上行流片的种类。`exit` = exec 流的**唯一**终止片,直接承载 exitCode(core OutputChunk 契约)。 */ export declare const DEVICE_CHUNK_KINDS: readonly ["stdout", "stderr", "data", "exit"]; export type DeviceChunkKind = (typeof DEVICE_CHUNK_KINDS)[number]; /** cancel 的终局回执状态(§5.3)。 */ export declare const DEVICE_CANCEL_STATES: readonly ["not_started", "killed"]; export type DeviceCancelState = (typeof DEVICE_CANCEL_STATES)[number]; /** * 指令状态闭集(§5.3 task-R2-1)。 * * 🔴 **刻意没有第五个词**:轮2 删掉的那个「已投递」态不可观测(`ws.send()` 成功 ≠ 设备收到),留着 * 必逼实现者在 commit 与它之间造一个窗口,让「窗口内到达的结果」被当成 unexpected 丢掉。两条路径: * `queued → dispatch_committed → terminal` 与 `queued → terminal_never_started`。 */ export declare const DEVICE_INSTRUCTION_STATES: readonly ["queued", "dispatch_committed", "terminal", "terminal_never_started"]; export type DeviceInstructionState = (typeof DEVICE_INSTRUCTION_STATES)[number]; /** 指令 kind ↔ core 的 FileSystem+Shell 面 1:1(§5.1;双端穷举 switch)。 */ export declare const DEVICE_INSTRUCTION_KINDS: readonly ["exec", "execStream", "readTextFile", "readTextLines", "readBinaryFile", "writeFile", "writeFileExclusive", "writeFileGuarded", "appendFile", "fileInfo", "listDir", "canonicalPath", "exists", "readLink", "createDir", "remove", "createTempDir", "createTempFile", "statBatch"]; export type DeviceInstructionKind = (typeof DEVICE_INSTRUCTION_KINDS)[number]; /** 认得就收窄,认不出就 null(**不猜**:未知 kind 恒不下发,fail-closed 方向)。 */ export declare function asInstructionKind(v: string): DeviceInstructionKind | null; /** * 结果帧的错误码闭集 = core 的 `FileErrorCode ∪ ExecutionErrorCode`(§5.1「error code 全闭集」: * 未知值映射为协议错误而非透传 string)。`satisfies` 保证这里没有 core 不认识的词。 */ export declare const DEVICE_OUTCOME_ERROR_CODES: readonly ["aborted", "already_exists", "auth_failed", "callback_error", "invalid", "is_directory", "not_directory", "not_found", "not_supported", "outcome_unknown", "permission_denied", "precondition_failed", "shell_unavailable", "spawn_error", "suspended", "target_unavailable", "timeout", "transport_lost", "unknown"]; export type DeviceOutcomeErrorCode = (typeof DEVICE_OUTCOME_ERROR_CODES)[number]; /** * **编译期**对账的另一半:core 往 `FileErrorCode`/`ExecutionErrorCode` 加一个词而本表没跟 ⇒ `Exclude` * 不再是 `never` ⇒ `return v` 编译红。`satisfies` 只挡「本表多写了 core 不认的词」,这条挡「本表漏了 * core 的词」——两个方向都要,否则提货 core 新版时错误码表会静默变成子集(未覆盖的词落进 * `parseOutcomeErrorCode` 的 null 臂 = 一个合法结局被当协议错误拒掉)。 */ export declare function assertCoreOutcomeErrorCodesAreCovered(v: Exclude): never; /** 帧解析的拒因闭集(进 `device_frame_unexpected` 审计行的 `detail.reason`)。 */ export declare const DEVICE_FRAME_REJECT_REASONS: readonly ["frame_too_large", "not_json", "not_object", "unknown_type", "bad_shape"]; export type DeviceFrameRejectReason = (typeof DEVICE_FRAME_REJECT_REASONS)[number]; /** * 汇聚端在**解析之后**还会拒的理由(世代闸 / pending 表不变量)。与解析拒因合成审计行的闭集词表: * 两段合起来才是「一条设备帧可能被拒的全部理由」,审计消费方按这一张表读。 */ export declare const DEVICE_FRAME_GATE_REJECT_REASONS: readonly ["pre_hello", "generation_mismatch", "unknown_instruction", "instruction_not_committed", "device_mismatch"]; export type DeviceFrameGateRejectReason = (typeof DEVICE_FRAME_GATE_REJECT_REASONS)[number]; /** 审计行 `detail.reason` 的全集。 */ export declare const DEVICE_FRAME_AUDIT_REASONS: readonly ["frame_too_large", "not_json", "not_object", "unknown_type", "bad_shape", "pre_hello", "generation_mismatch", "unknown_instruction", "instruction_not_committed", "device_mismatch"]; export type DeviceFrameAuditReason = DeviceFrameRejectReason | DeviceFrameGateRejectReason; /** 设备文件面的 wire 投影(A-4 的 adapter 负责映射到 core `FileInfo`;本层只冻结形)。 */ export interface WireFileInfo { path: string; kind: "file" | "dir" | "symlink" | "other"; size: number; mtimeMs: number; mode?: number; } /** * 守卫写的前置条件在 wire 上的投影(core `WriteExpectation` 的字段子集,逐字同名)。 * * 🔴 刻意**不**直接把 core 的类型放上 wire:wire 型是本仓铸的、cli 消费的独立契约(§3.7),core 加一 * 个字段不该无声地变成一条设备必须懂的新前置条件 —— 那是静默的语义升版。core 加字段时这里编译不红, * 但 adapter 侧的**穷举投影**会红(见 `remote-env-device.ts` 的 `toWireExpectation`)。 */ export interface WireWriteExpectation { canonicalPath: string; fileId?: string; exclusive?: boolean; noFollow?: boolean; } /** 指令结局(§5.1 `InstructionOutcome`)。 */ export type DeviceInstructionOutcome = { ok: true; kind: "exec"; exitCode: number; truncated?: { stdout?: boolean; stderr?: boolean; }; } | { ok: true; kind: "read"; } | { ok: true; kind: "stat"; info?: WireFileInfo; entries?: WireFileInfo[]; existsValue?: boolean; canonicalPath?: string; linkTarget?: string | null; } /** 写面的**落盘对象收据**(§4.3.1 / core `WriteReceipt`)。`created` 是 additive 扩键(车A-4): * core 的收据形要求它,缺席 = adapter 只能猜「是新建还是覆盖」,而猜出来的收据就是假收据。 */ | { ok: true; kind: "write"; canonicalPath: string; inode?: string; created?: boolean; } | { ok: true; kind: "void"; } | { ok: true; kind: "cancelled"; state: DeviceCancelState; } | { ok: false; errorCode: DeviceOutcomeErrorCode; message: string; }; /** per-kind 精确入参(§5.1 task-R1-8:`InstructionArgs` 按 kind 判别,非 any)。 */ export interface DeviceInstructionArgsByKind { exec: { command: string; }; execStream: { command: string; }; readTextFile: { path: string; }; readTextLines: { path: string; offset?: number; limit?: number; }; readBinaryFile: { path: string; }; /** 内容**不在** args 里:走 `payload` 下行子序(§5.1),args 只带长度与摘要供设备校验。 */ writeFile: { path: string; contentLen: number; sha256: string; }; writeFileExclusive: { path: string; contentLen: number; sha256: string; }; /** `expect` = core `WriteExpectation` 的 wire 投影(车A-4 additive):设备在**同一原子步**里验证它。 */ writeFileGuarded: { path: string; contentLen: number; sha256: string; expect: WireWriteExpectation; }; appendFile: { path: string; contentLen: number; sha256: string; }; fileInfo: { path: string; }; listDir: { path: string; }; canonicalPath: { path: string; }; exists: { path: string; }; readLink: { path: string; }; createDir: { path: string; recursive?: boolean; }; remove: { path: string; recursive?: boolean; }; createTempDir: { prefix?: string; }; createTempFile: { prefix?: string; suffix?: string; }; statBatch: { paths: string[]; }; } export type DeviceChallengeFrame = { t: "challenge"; nonce: string; protocolVersion: number; }; export type DeviceHelloAckFrame = { t: "helloAck"; ok: true; heartbeatIntervalMs: number; maxInflight: number; /** ⚠️ canonical 十进制**字符串**(F18:SQL BIGINT 超 2^53,wire 不声明装不下 SQL 类型的字段)。 */ gen: string; pendingInstructions: string[]; chunkWatermarks: Record; }; export type DeviceHelloRejectFrame = { t: "helloReject"; code: DeviceHelloRejectCode; minProtocolVersion?: number; }; export type DeviceInstructionFrame = { [K in DeviceInstructionKind]: { t: "instruction"; gen: string; instructionId: string; rootSessionId: string; issuedSeq: number; kind: K; args: DeviceInstructionArgsByKind[K]; cwd?: string; shellEnv?: Record; timeoutMs: number; }; }[DeviceInstructionKind]; export type DevicePayloadFrame = { t: "payload"; gen: string; instructionId: string; part: "begin" | "chunk" | "commit"; offset?: number; dataB64?: string; totalLen?: number; sha256?: string; }; export type DeviceServerFrame = DeviceChallengeFrame | DeviceHelloAckFrame | DeviceHelloRejectFrame | DeviceInstructionFrame | DevicePayloadFrame | { t: "cancel"; gen: string; instructionId: string; } | { t: "resultAck"; gen: string; instructionId: string; } | { t: "chunkAck"; gen: string; instructionId: string; upToStreamSeq: number; } | { t: "bye"; gen: string; reason: DeviceByeReason; } | { t: "ping"; }; export type DeviceHelloFrame = { t: "hello"; protocolVersion: number; deviceId: string; epoch: string; signatureB64: string; platform: { os: string; arch: string; executorVersion: string; }; workspaceRoot: string; pathFlavor: string; /** * 🔴 刻意是 `string[]` 而不是 `DeviceInstructionKind[]`:协议是 additive 的(§5.6),一个**更新的** * executor 会声明本 server 还不认识的 kind。收窄在这里拒 = 老 server 认不下新 executor;所以帧里存 * 原样,由汇聚端与自己的闭集取交集 —— 不认识的 kind 恒不下发,方向仍是 fail-closed。 */ supports: string[]; resuming?: { unackedResults: number; }; }; export type DeviceFrame = DeviceHelloFrame | { t: "generationAck"; gen: string; } | { t: "heartbeat"; gen: string; seq: number; inflight: string[]; } | { t: "result"; gen: string; instructionId: string; deviceSeq: number; outcome: DeviceInstructionOutcome; } | { t: "chunk"; gen: string; instructionId: string; streamSeq: number; kind: DeviceChunkKind; dataB64?: string; exitCode?: number; } | { t: "goodbye"; gen: string; reason: DeviceGoodbyeReason; graceMs?: number; }; export type DeviceResultFrame = Extract; export type DeviceChunkFrame = Extract; export type DeviceGoodbyeFrame = Extract; export type DeviceHeartbeatFrame = Extract; export type DeviceFrameParse = { ok: true; frame: DeviceFrame; } | { ok: false; reason: DeviceFrameRejectReason; detail: string; }; /** * **规范** base64(codex 交叉轮 R1-F6,判真)。 * * 🔴 为什么不能只判「是个字符串」再交给 `Buffer.from(v, "base64")`:Node 的解码是**宽松**的 —— * `"!!!!"` 解出一个**空** Buffer 而不抛,`"aG@k="` 悄悄丢掉非法字符照样解出两个字节。于是一条被 * 传输层损坏(或被不可信构造)的流片会以「合法的空/短数据」进入交付路径,水位照常推进、chunkAck * 照常回给设备 —— 设备释放了它手上唯一的正本,调用方拿到的是**静默损坏**的输出。 * 在协议边界判规范形 + 回编码自比,这条路就没了。跨仓同判据:cli 侧编码器必须产规范形。 */ export declare function isCanonicalBase64(v: string): boolean; /** `gen` 的**规范**十进制串。前导零/正号/空白/小数/负数/超安全整数一律 null(见文件头 F18 注)。 */ export declare function parseGeneration(v: string): number | null; export declare function formatGeneration(n: number): string; /** * 设备帧的唯一解析入口。 * * 🔴 顺序是 帽 → JSON → 对象 → 帧型 → 形:每一步都有自己的拒因词,审计行才能回答「它到底哪儿坏了」。 * 没有任何一步会「尽量修一下再放行」——安全轴上的坏形帧只有响亮拒一条路([ref])。 */ export declare function parseDeviceFrameText(text: string, opts?: { maxBytes?: number; }): DeviceFrameParse; /** * 26 位 Crockford base32(ULID)→ 16 字节。非法字符/长度 ⇒ `null`(**不猜**:一个被静默补零的 epoch * 会让两个不同的 executor 进程签出同一份字节)。 */ export declare function decodeUlid16(s: string): Buffer | null; /** * 握手签名的**唯一**字节编码(§5.1 末,跨仓冻结): * `"sema.device.hello.v1" ‖ u32be(len(nonce))‖nonce ‖ u32be(len(deviceId))‖deviceId ‖ epoch16 ‖ u32be(version)`。 */ export declare function buildHelloSignaturePayload(input: { nonce: string; deviceId: string; epoch: string; protocolVersion: number; }): Buffer; /** * 验签判决。`reason` **只进日志**(wire 上永远是同一个 `auth_failed` —— 反枚举),但它必须存在: * 一个「公钥导入失败」和一个「签名对不上」在运维上是两回事,把它们一起折成 `false` 就等于把 * 「这台设备的公钥列存坏了」这条事实永久删掉([ref] 门② SHAPE C:错误要**当数据交出**,不是吞掉)。 */ export type HelloSignatureVerdict = { ok: true; } | { ok: false; reason: string; }; /** * 验签。公钥 wire 形 = **raw 32 字节的 base64**(店里 `devices.pubkey` 存的就是它)。 * * 🔴 **恒不抛**:握手期的输入全部来自未鉴权的对端,一个畸形 base64 把异常抛进 socket 事件回调 = * 未鉴权者可以让进程走进 uncaughtException。所以每条失败路径都收敛成 `{ok:false, reason}` —— 方向 * 是拒(fail-closed),而拒的**原因**原样交给调用方去打日志。 */ export declare function verifyHelloSignature(input: { pubkeyB64: string; signatureB64: string; payload: Buffer; }): HelloSignatureVerdict; /** * 幂等键(§5.1:server 铸 ULID,**不可猜**)。设备按它去重(at-most-once);wire 上刻意不承载 * runId/toolCallId —— 它们在 `ExecutionEnv` 面结构性拿不到(core `ExecutionEnvFactoryContext` * 只有 sessionId/taskId),关联在 server 侧 pending 表里完成。 */ export declare function mintInstructionId(nowMs?: number): string; /** 一次性 challenge nonce(128 位熵,base64url canonical,每 socket 一次)。 */ export declare function mintChallengeNonce(): string; /** executor 进程 epoch 的铸造(测试/自测用;真值由设备侧每次启动自铸)。 */ export declare function mintDeviceEpoch(nowMs?: number): string; //# sourceMappingURL=device-ws-protocol.d.ts.map