@sema-agent/server 7.49.0 → 7.51.0
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.
- package/USAGE.md +39 -0
- package/dist/boot/device-lane.d.ts +79 -0
- package/dist/boot/device-lane.js +63 -0
- package/dist/boot/engine-lease.d.ts +95 -0
- package/dist/boot/engine-lease.js +56 -0
- package/dist/boot/execution-env.d.ts +48 -2
- package/dist/boot/execution-env.js +57 -5
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +12 -9
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +4 -1
- package/dist/boot/shutdown.d.ts +10 -0
- package/dist/boot/shutdown.js +19 -1
- package/dist/boot/stores.js +9 -0
- package/dist/config-center/types.d.ts +10 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +18 -0
- package/dist/config-types.d.ts +32 -1
- package/dist/config.js +69 -10
- package/dist/device-enrollment.d.ts +125 -0
- package/dist/device-enrollment.js +156 -0
- package/dist/device-store.d.ts +385 -0
- package/dist/device-store.js +407 -0
- package/dist/device-ws-hub.d.ts +182 -0
- package/dist/device-ws-hub.js +1012 -0
- package/dist/device-ws-protocol.d.ts +429 -0
- package/dist/device-ws-protocol.js +464 -0
- package/dist/env-facts.d.ts +4 -1
- package/dist/env-facts.js +1 -0
- package/dist/execution-lane-caps.d.ts +152 -0
- package/dist/execution-lane-caps.js +166 -0
- package/dist/fleet/subagent-tail-bus.d.ts +1 -1
- package/dist/fleet/subagent-tail-bus.js +10 -0
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +7 -2
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/tasks.js +12 -6
- package/dist/http/send.d.ts +14 -0
- package/dist/http/send.js +5 -0
- package/dist/http/server.d.ts +19 -0
- package/dist/http/server.js +21 -2
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/leader/wire.js +4 -2
- package/dist/main.js +10 -3
- package/dist/orchestration/hardened-vm-runner.d.ts +7 -0
- package/dist/orchestration/hardened-vm-runner.js +11 -1
- package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
- package/dist/plugins/device-store-sql.d.ts +130 -0
- package/dist/plugins/device-store-sql.js +574 -0
- package/dist/plugins/remote-env-device.d.ts +271 -0
- package/dist/plugins/remote-env-device.js +727 -0
- package/dist/plugins/remote-scratchpad.js +1 -1
- package/dist/plugins/store-backend.d.ts +9 -0
- package/dist/plugins/store-backend.js +3 -0
- package/dist/plugins/usage-window-store-sql.d.ts +2 -2
- package/dist/plugins/usage-window-store-sql.js +17 -6
- package/dist/task-cwd.d.ts +25 -0
- package/dist/task-cwd.js +3 -0
- package/dist/task-settings.js +3 -1
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { createPublicKey, randomBytes, randomInt, verify as edVerify } from "node:crypto";
|
|
2
|
+
export const DEVICE_WS_PATH = "/v1/device/ws";
|
|
3
|
+
export const DEVICE_PROTOCOL_VERSION = 1;
|
|
4
|
+
export const DEVICE_MIN_PROTOCOL_VERSION = 1;
|
|
5
|
+
export const DEVICE_HELLO_DOMAIN_TAG = "sema.device.hello.v1";
|
|
6
|
+
export const DEVICE_WS_LIMITS = {
|
|
7
|
+
maxFrameBytes: 1024 * 1024,
|
|
8
|
+
maxPreAuthFrameBytes: 4096,
|
|
9
|
+
handshakeDeadlineMs: 5000,
|
|
10
|
+
defaultPayloadPartBytes: 256 * 1024,
|
|
11
|
+
};
|
|
12
|
+
export const DEVICE_FRAME_TYPES = ["hello", "generationAck", "heartbeat", "result", "chunk", "goodbye"];
|
|
13
|
+
export const DEVICE_SERVER_FRAME_TYPES = ["challenge", "helloAck", "helloReject", "instruction", "payload", "cancel", "resultAck", "chunkAck", "bye", "ping"];
|
|
14
|
+
export const DEVICE_HELLO_REJECT_CODES = ["auth_failed", "revoked", "protocol_version_unsupported", "draining", "already_connected"];
|
|
15
|
+
export const DEVICE_BYE_REASONS = ["revoked", "draining", "superseded"];
|
|
16
|
+
export const DEVICE_GOODBYE_REASONS = ["shutdown", "sleep", "user_stop"];
|
|
17
|
+
export const DEVICE_CHUNK_KINDS = ["stdout", "stderr", "data", "exit"];
|
|
18
|
+
export const DEVICE_CANCEL_STATES = ["not_started", "killed"];
|
|
19
|
+
export const DEVICE_INSTRUCTION_STATES = ["queued", "dispatch_committed", "terminal", "terminal_never_started"];
|
|
20
|
+
export const DEVICE_INSTRUCTION_KINDS = [
|
|
21
|
+
"exec",
|
|
22
|
+
"execStream",
|
|
23
|
+
"readTextFile",
|
|
24
|
+
"readTextLines",
|
|
25
|
+
"readBinaryFile",
|
|
26
|
+
"writeFile",
|
|
27
|
+
"writeFileExclusive",
|
|
28
|
+
"writeFileGuarded",
|
|
29
|
+
"appendFile",
|
|
30
|
+
"fileInfo",
|
|
31
|
+
"listDir",
|
|
32
|
+
"canonicalPath",
|
|
33
|
+
"exists",
|
|
34
|
+
"readLink",
|
|
35
|
+
"createDir",
|
|
36
|
+
"remove",
|
|
37
|
+
"createTempDir",
|
|
38
|
+
"createTempFile",
|
|
39
|
+
"statBatch",
|
|
40
|
+
];
|
|
41
|
+
const INSTRUCTION_KIND_SET = new Set(DEVICE_INSTRUCTION_KINDS);
|
|
42
|
+
export function asInstructionKind(v) {
|
|
43
|
+
return INSTRUCTION_KIND_SET.has(v) ? v : null;
|
|
44
|
+
}
|
|
45
|
+
export const DEVICE_OUTCOME_ERROR_CODES = [
|
|
46
|
+
"aborted",
|
|
47
|
+
"already_exists",
|
|
48
|
+
"auth_failed",
|
|
49
|
+
"callback_error",
|
|
50
|
+
"invalid",
|
|
51
|
+
"is_directory",
|
|
52
|
+
"not_directory",
|
|
53
|
+
"not_found",
|
|
54
|
+
"not_supported",
|
|
55
|
+
"outcome_unknown",
|
|
56
|
+
"permission_denied",
|
|
57
|
+
"precondition_failed",
|
|
58
|
+
"shell_unavailable",
|
|
59
|
+
"spawn_error",
|
|
60
|
+
"suspended",
|
|
61
|
+
"target_unavailable",
|
|
62
|
+
"timeout",
|
|
63
|
+
"transport_lost",
|
|
64
|
+
"unknown",
|
|
65
|
+
];
|
|
66
|
+
const OUTCOME_ERROR_CODE_SET = new Set(DEVICE_OUTCOME_ERROR_CODES);
|
|
67
|
+
export function assertCoreOutcomeErrorCodesAreCovered(v) {
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
export const DEVICE_FRAME_REJECT_REASONS = ["frame_too_large", "not_json", "not_object", "unknown_type", "bad_shape"];
|
|
71
|
+
export const DEVICE_FRAME_GATE_REJECT_REASONS = ["pre_hello", "generation_mismatch", "unknown_instruction", "instruction_not_committed", "device_mismatch"];
|
|
72
|
+
export const DEVICE_FRAME_AUDIT_REASONS = [...DEVICE_FRAME_REJECT_REASONS, ...DEVICE_FRAME_GATE_REJECT_REASONS];
|
|
73
|
+
function reject(reason, detail) {
|
|
74
|
+
return { ok: false, reason, detail };
|
|
75
|
+
}
|
|
76
|
+
function isRecord(v) {
|
|
77
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
78
|
+
}
|
|
79
|
+
function str(v) {
|
|
80
|
+
return typeof v === "string" ? v : null;
|
|
81
|
+
}
|
|
82
|
+
function int(v) {
|
|
83
|
+
return typeof v === "number" && Number.isSafeInteger(v) ? v : null;
|
|
84
|
+
}
|
|
85
|
+
function nonNegInt(v) {
|
|
86
|
+
const n = int(v);
|
|
87
|
+
return n !== null && n >= 0 ? n : null;
|
|
88
|
+
}
|
|
89
|
+
const BASE64_CANONICAL_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
90
|
+
export function isCanonicalBase64(v) {
|
|
91
|
+
if (!BASE64_CANONICAL_RE.test(v))
|
|
92
|
+
return false;
|
|
93
|
+
return Buffer.from(v, "base64").toString("base64") === v;
|
|
94
|
+
}
|
|
95
|
+
export function parseGeneration(v) {
|
|
96
|
+
if (!/^(0|[1-9][0-9]*)$/.test(v))
|
|
97
|
+
return null;
|
|
98
|
+
const n = Number(v);
|
|
99
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
100
|
+
}
|
|
101
|
+
export function formatGeneration(n) {
|
|
102
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
103
|
+
throw new Error(`device-ws: generation must be a non-negative safe integer (got ${n})`);
|
|
104
|
+
return String(n);
|
|
105
|
+
}
|
|
106
|
+
function parseOutcomeErrorCode(v) {
|
|
107
|
+
const s = str(v);
|
|
108
|
+
return s !== null && OUTCOME_ERROR_CODE_SET.has(s) ? s : null;
|
|
109
|
+
}
|
|
110
|
+
function parseWireFileInfo(v) {
|
|
111
|
+
if (!isRecord(v))
|
|
112
|
+
return null;
|
|
113
|
+
const path = str(v.path);
|
|
114
|
+
const kind = str(v.kind);
|
|
115
|
+
const size = int(v.size);
|
|
116
|
+
const mtimeMs = int(v.mtimeMs);
|
|
117
|
+
if (path === null || size === null || mtimeMs === null)
|
|
118
|
+
return null;
|
|
119
|
+
if (kind !== "file" && kind !== "dir" && kind !== "symlink" && kind !== "other")
|
|
120
|
+
return null;
|
|
121
|
+
let mode;
|
|
122
|
+
if (v.mode !== undefined) {
|
|
123
|
+
const m = int(v.mode);
|
|
124
|
+
if (m === null)
|
|
125
|
+
return null;
|
|
126
|
+
mode = m;
|
|
127
|
+
}
|
|
128
|
+
return { path, kind, size, mtimeMs, ...(mode === undefined ? {} : { mode }) };
|
|
129
|
+
}
|
|
130
|
+
function parseOutcome(v) {
|
|
131
|
+
if (!isRecord(v))
|
|
132
|
+
return null;
|
|
133
|
+
if (v.ok === false) {
|
|
134
|
+
const errorCode = parseOutcomeErrorCode(v.errorCode);
|
|
135
|
+
const message = str(v.message);
|
|
136
|
+
if (errorCode === null || message === null)
|
|
137
|
+
return null;
|
|
138
|
+
return { ok: false, errorCode, message };
|
|
139
|
+
}
|
|
140
|
+
if (v.ok !== true)
|
|
141
|
+
return null;
|
|
142
|
+
const kind = str(v.kind);
|
|
143
|
+
switch (kind) {
|
|
144
|
+
case "exec": {
|
|
145
|
+
const exitCode = int(v.exitCode);
|
|
146
|
+
if (exitCode === null)
|
|
147
|
+
return null;
|
|
148
|
+
if (v.truncated === undefined)
|
|
149
|
+
return { ok: true, kind: "exec", exitCode };
|
|
150
|
+
if (!isRecord(v.truncated))
|
|
151
|
+
return null;
|
|
152
|
+
const stdout = v.truncated.stdout;
|
|
153
|
+
const stderr = v.truncated.stderr;
|
|
154
|
+
if (stdout !== undefined && typeof stdout !== "boolean")
|
|
155
|
+
return null;
|
|
156
|
+
if (stderr !== undefined && typeof stderr !== "boolean")
|
|
157
|
+
return null;
|
|
158
|
+
return { ok: true, kind: "exec", exitCode, truncated: { ...(stdout === undefined ? {} : { stdout }), ...(stderr === undefined ? {} : { stderr }) } };
|
|
159
|
+
}
|
|
160
|
+
case "read":
|
|
161
|
+
return { ok: true, kind: "read" };
|
|
162
|
+
case "stat": {
|
|
163
|
+
const out = { ok: true, kind: "stat" };
|
|
164
|
+
if (v.info !== undefined) {
|
|
165
|
+
const info = parseWireFileInfo(v.info);
|
|
166
|
+
if (info === null)
|
|
167
|
+
return null;
|
|
168
|
+
out.info = info;
|
|
169
|
+
}
|
|
170
|
+
if (v.entries !== undefined) {
|
|
171
|
+
if (!Array.isArray(v.entries))
|
|
172
|
+
return null;
|
|
173
|
+
const entries = [];
|
|
174
|
+
for (const e of v.entries) {
|
|
175
|
+
const parsed = parseWireFileInfo(e);
|
|
176
|
+
if (parsed === null)
|
|
177
|
+
return null;
|
|
178
|
+
entries.push(parsed);
|
|
179
|
+
}
|
|
180
|
+
out.entries = entries;
|
|
181
|
+
}
|
|
182
|
+
if (v.existsValue !== undefined) {
|
|
183
|
+
if (typeof v.existsValue !== "boolean")
|
|
184
|
+
return null;
|
|
185
|
+
out.existsValue = v.existsValue;
|
|
186
|
+
}
|
|
187
|
+
if (v.canonicalPath !== undefined) {
|
|
188
|
+
const cp = str(v.canonicalPath);
|
|
189
|
+
if (cp === null)
|
|
190
|
+
return null;
|
|
191
|
+
out.canonicalPath = cp;
|
|
192
|
+
}
|
|
193
|
+
if (v.linkTarget !== undefined) {
|
|
194
|
+
if (v.linkTarget !== null && typeof v.linkTarget !== "string")
|
|
195
|
+
return null;
|
|
196
|
+
out.linkTarget = v.linkTarget;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
case "write": {
|
|
201
|
+
const canonicalPath = str(v.canonicalPath);
|
|
202
|
+
if (canonicalPath === null)
|
|
203
|
+
return null;
|
|
204
|
+
let inode;
|
|
205
|
+
if (v.inode !== undefined) {
|
|
206
|
+
const s = str(v.inode);
|
|
207
|
+
if (s === null)
|
|
208
|
+
return null;
|
|
209
|
+
inode = s;
|
|
210
|
+
}
|
|
211
|
+
if (v.created !== undefined && typeof v.created !== "boolean")
|
|
212
|
+
return null;
|
|
213
|
+
const created = v.created;
|
|
214
|
+
return { ok: true, kind: "write", canonicalPath, ...(inode === undefined ? {} : { inode }), ...(created === undefined ? {} : { created }) };
|
|
215
|
+
}
|
|
216
|
+
case "void":
|
|
217
|
+
return { ok: true, kind: "void" };
|
|
218
|
+
case "cancelled": {
|
|
219
|
+
const state = str(v.state);
|
|
220
|
+
if (state !== "not_started" && state !== "killed")
|
|
221
|
+
return null;
|
|
222
|
+
return { ok: true, kind: "cancelled", state };
|
|
223
|
+
}
|
|
224
|
+
default:
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export function parseDeviceFrameText(text, opts) {
|
|
229
|
+
const cap = opts?.maxBytes ?? DEVICE_WS_LIMITS.maxFrameBytes;
|
|
230
|
+
const size = Buffer.byteLength(text, "utf8");
|
|
231
|
+
if (size > cap)
|
|
232
|
+
return reject("frame_too_large", `frame is ${size} bytes (cap ${cap})`);
|
|
233
|
+
let raw;
|
|
234
|
+
try {
|
|
235
|
+
raw = JSON.parse(text);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
return reject("not_json", err instanceof Error ? err.message : String(err));
|
|
239
|
+
}
|
|
240
|
+
if (!isRecord(raw))
|
|
241
|
+
return reject("not_object", "top-level frame must be a JSON object");
|
|
242
|
+
const t = str(raw.t);
|
|
243
|
+
if (t === null)
|
|
244
|
+
return reject("unknown_type", "frame has no string `t`");
|
|
245
|
+
const type = DEVICE_FRAME_TYPES.find((x) => x === t);
|
|
246
|
+
if (type === undefined)
|
|
247
|
+
return reject("unknown_type", `no device→server frame type ${JSON.stringify(t)}`);
|
|
248
|
+
switch (type) {
|
|
249
|
+
case "hello": {
|
|
250
|
+
const protocolVersion = int(raw.protocolVersion);
|
|
251
|
+
const deviceId = str(raw.deviceId);
|
|
252
|
+
const epoch = str(raw.epoch);
|
|
253
|
+
const signatureB64 = str(raw.signatureB64);
|
|
254
|
+
const workspaceRoot = str(raw.workspaceRoot);
|
|
255
|
+
const pathFlavor = str(raw.pathFlavor);
|
|
256
|
+
if (protocolVersion === null || deviceId === null || deviceId === "" || epoch === null || signatureB64 === null || workspaceRoot === null || pathFlavor === null) {
|
|
257
|
+
return reject("bad_shape", "hello is missing a required scalar field");
|
|
258
|
+
}
|
|
259
|
+
if (!isRecord(raw.platform))
|
|
260
|
+
return reject("bad_shape", "hello.platform must be an object");
|
|
261
|
+
const os = str(raw.platform.os);
|
|
262
|
+
const arch = str(raw.platform.arch);
|
|
263
|
+
const executorVersion = str(raw.platform.executorVersion);
|
|
264
|
+
if (os === null || arch === null || executorVersion === null)
|
|
265
|
+
return reject("bad_shape", "hello.platform is missing a field");
|
|
266
|
+
if (!Array.isArray(raw.supports) || raw.supports.some((s) => typeof s !== "string"))
|
|
267
|
+
return reject("bad_shape", "hello.supports must be a string array");
|
|
268
|
+
let resuming;
|
|
269
|
+
if (raw.resuming !== undefined) {
|
|
270
|
+
if (!isRecord(raw.resuming))
|
|
271
|
+
return reject("bad_shape", "hello.resuming must be an object");
|
|
272
|
+
const unacked = nonNegInt(raw.resuming.unackedResults);
|
|
273
|
+
if (unacked === null)
|
|
274
|
+
return reject("bad_shape", "hello.resuming.unackedResults must be a non-negative integer");
|
|
275
|
+
resuming = { unackedResults: unacked };
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
frame: {
|
|
280
|
+
t: "hello",
|
|
281
|
+
protocolVersion,
|
|
282
|
+
deviceId,
|
|
283
|
+
epoch,
|
|
284
|
+
signatureB64,
|
|
285
|
+
platform: { os, arch, executorVersion },
|
|
286
|
+
workspaceRoot,
|
|
287
|
+
pathFlavor,
|
|
288
|
+
supports: raw.supports.filter((s) => typeof s === "string"),
|
|
289
|
+
...(resuming === undefined ? {} : { resuming }),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case "generationAck": {
|
|
294
|
+
const gen = str(raw.gen);
|
|
295
|
+
if (gen === null || parseGeneration(gen) === null)
|
|
296
|
+
return reject("bad_shape", "generationAck.gen must be a canonical decimal string");
|
|
297
|
+
return { ok: true, frame: { t: "generationAck", gen } };
|
|
298
|
+
}
|
|
299
|
+
case "heartbeat": {
|
|
300
|
+
const gen = str(raw.gen);
|
|
301
|
+
const seq = nonNegInt(raw.seq);
|
|
302
|
+
if (gen === null || parseGeneration(gen) === null || seq === null)
|
|
303
|
+
return reject("bad_shape", "heartbeat requires gen + non-negative integer seq");
|
|
304
|
+
if (!Array.isArray(raw.inflight) || raw.inflight.some((s) => typeof s !== "string"))
|
|
305
|
+
return reject("bad_shape", "heartbeat.inflight must be a string array");
|
|
306
|
+
return { ok: true, frame: { t: "heartbeat", gen, seq, inflight: raw.inflight.filter((s) => typeof s === "string") } };
|
|
307
|
+
}
|
|
308
|
+
case "result": {
|
|
309
|
+
const gen = str(raw.gen);
|
|
310
|
+
const instructionId = str(raw.instructionId);
|
|
311
|
+
const deviceSeq = nonNegInt(raw.deviceSeq);
|
|
312
|
+
if (gen === null || parseGeneration(gen) === null || instructionId === null || instructionId === "" || deviceSeq === null) {
|
|
313
|
+
return reject("bad_shape", "result requires gen + instructionId + non-negative integer deviceSeq");
|
|
314
|
+
}
|
|
315
|
+
const outcome = parseOutcome(raw.outcome);
|
|
316
|
+
if (outcome === null)
|
|
317
|
+
return reject("bad_shape", "result.outcome is not a recognised outcome shape");
|
|
318
|
+
return { ok: true, frame: { t: "result", gen, instructionId, deviceSeq, outcome } };
|
|
319
|
+
}
|
|
320
|
+
case "chunk": {
|
|
321
|
+
const gen = str(raw.gen);
|
|
322
|
+
const instructionId = str(raw.instructionId);
|
|
323
|
+
const streamSeq = nonNegInt(raw.streamSeq);
|
|
324
|
+
const kindRaw = str(raw.kind);
|
|
325
|
+
const kind = DEVICE_CHUNK_KINDS.find((k) => k === kindRaw);
|
|
326
|
+
if (gen === null || parseGeneration(gen) === null || instructionId === null || instructionId === "" || streamSeq === null || kind === undefined) {
|
|
327
|
+
return reject("bad_shape", "chunk requires gen + instructionId + streamSeq + a known kind");
|
|
328
|
+
}
|
|
329
|
+
let dataB64;
|
|
330
|
+
if (raw.dataB64 !== undefined) {
|
|
331
|
+
const d = str(raw.dataB64);
|
|
332
|
+
if (d === null)
|
|
333
|
+
return reject("bad_shape", "chunk.dataB64 must be a string");
|
|
334
|
+
if (!isCanonicalBase64(d))
|
|
335
|
+
return reject("bad_shape", "chunk.dataB64 is not canonical base64 (a lax decode would silently corrupt the stream)");
|
|
336
|
+
dataB64 = d;
|
|
337
|
+
}
|
|
338
|
+
let exitCode;
|
|
339
|
+
if (raw.exitCode !== undefined) {
|
|
340
|
+
const e = int(raw.exitCode);
|
|
341
|
+
if (e === null)
|
|
342
|
+
return reject("bad_shape", "chunk.exitCode must be an integer");
|
|
343
|
+
exitCode = e;
|
|
344
|
+
}
|
|
345
|
+
switch (kind) {
|
|
346
|
+
case "exit":
|
|
347
|
+
if (exitCode === undefined)
|
|
348
|
+
return reject("bad_shape", "an exit chunk must carry exitCode (core OutputChunk contract)");
|
|
349
|
+
if (dataB64 !== undefined)
|
|
350
|
+
return reject("bad_shape", "an exit chunk must not carry dataB64");
|
|
351
|
+
break;
|
|
352
|
+
case "stdout":
|
|
353
|
+
case "stderr":
|
|
354
|
+
case "data":
|
|
355
|
+
if (dataB64 === undefined)
|
|
356
|
+
return reject("bad_shape", `a ${kind} chunk must carry dataB64`);
|
|
357
|
+
if (exitCode !== undefined)
|
|
358
|
+
return reject("bad_shape", `a ${kind} chunk must not carry exitCode (only the single exit chunk terminates a stream)`);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
return { ok: true, frame: { t: "chunk", gen, instructionId, streamSeq, kind, ...(dataB64 === undefined ? {} : { dataB64 }), ...(exitCode === undefined ? {} : { exitCode }) } };
|
|
362
|
+
}
|
|
363
|
+
case "goodbye": {
|
|
364
|
+
const gen = str(raw.gen);
|
|
365
|
+
const reasonRaw = str(raw.reason);
|
|
366
|
+
const reason = DEVICE_GOODBYE_REASONS.find((r) => r === reasonRaw);
|
|
367
|
+
if (gen === null || parseGeneration(gen) === null || reason === undefined)
|
|
368
|
+
return reject("bad_shape", "goodbye requires gen + a known reason");
|
|
369
|
+
let graceMs;
|
|
370
|
+
if (raw.graceMs !== undefined) {
|
|
371
|
+
const g = nonNegInt(raw.graceMs);
|
|
372
|
+
if (g === null)
|
|
373
|
+
return reject("bad_shape", "goodbye.graceMs must be a non-negative integer");
|
|
374
|
+
graceMs = g;
|
|
375
|
+
}
|
|
376
|
+
return { ok: true, frame: { t: "goodbye", gen, reason, ...(graceMs === undefined ? {} : { graceMs }) } };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
381
|
+
const CROCKFORD_INDEX = new Map([...CROCKFORD].map((c, i) => [c, i]));
|
|
382
|
+
export function decodeUlid16(s) {
|
|
383
|
+
if (s.length !== 26)
|
|
384
|
+
return null;
|
|
385
|
+
const first = CROCKFORD_INDEX.get(s[0]);
|
|
386
|
+
if (first === undefined || first > 7)
|
|
387
|
+
return null;
|
|
388
|
+
const out = Buffer.alloc(16);
|
|
389
|
+
let acc = 0n;
|
|
390
|
+
for (const ch of s) {
|
|
391
|
+
const v = CROCKFORD_INDEX.get(ch);
|
|
392
|
+
if (v === undefined)
|
|
393
|
+
return null;
|
|
394
|
+
acc = (acc << 5n) | BigInt(v);
|
|
395
|
+
}
|
|
396
|
+
for (let i = 15; i >= 0; i--) {
|
|
397
|
+
out[i] = Number(acc & 0xffn);
|
|
398
|
+
acc >>= 8n;
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
function lengthPrefixed(v) {
|
|
403
|
+
const body = Buffer.from(v, "utf8");
|
|
404
|
+
const head = Buffer.alloc(4);
|
|
405
|
+
head.writeUInt32BE(body.length, 0);
|
|
406
|
+
return Buffer.concat([head, body]);
|
|
407
|
+
}
|
|
408
|
+
export function buildHelloSignaturePayload(input) {
|
|
409
|
+
const epoch16 = decodeUlid16(input.epoch);
|
|
410
|
+
if (epoch16 === null)
|
|
411
|
+
throw new Error(`device-ws: epoch must be a 26-char Crockford ULID (got ${JSON.stringify(input.epoch)})`);
|
|
412
|
+
if (!Number.isSafeInteger(input.protocolVersion) || input.protocolVersion < 0 || input.protocolVersion > 0xffffffff) {
|
|
413
|
+
throw new Error(`device-ws: protocolVersion out of u32 range (got ${input.protocolVersion})`);
|
|
414
|
+
}
|
|
415
|
+
const version = Buffer.alloc(4);
|
|
416
|
+
version.writeUInt32BE(input.protocolVersion, 0);
|
|
417
|
+
return Buffer.concat([Buffer.from(DEVICE_HELLO_DOMAIN_TAG, "utf8"), lengthPrefixed(input.nonce), lengthPrefixed(input.deviceId), epoch16, version]);
|
|
418
|
+
}
|
|
419
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
420
|
+
export function verifyHelloSignature(input) {
|
|
421
|
+
const raw = Buffer.from(input.pubkeyB64, "base64");
|
|
422
|
+
if (raw.length !== 32)
|
|
423
|
+
return { ok: false, reason: `stored pubkey decoded to ${raw.length} bytes (an Ed25519 raw key is 32)` };
|
|
424
|
+
const sig = Buffer.from(input.signatureB64, "base64");
|
|
425
|
+
if (sig.length !== 64)
|
|
426
|
+
return { ok: false, reason: `signature decoded to ${sig.length} bytes (Ed25519 signatures are 64)` };
|
|
427
|
+
let key;
|
|
428
|
+
try {
|
|
429
|
+
key = createPublicKey({ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]), format: "der", type: "spki" });
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
return { ok: false, reason: `stored pubkey could not be imported: ${err instanceof Error ? err.message : String(err)}` };
|
|
433
|
+
}
|
|
434
|
+
let verified;
|
|
435
|
+
try {
|
|
436
|
+
verified = edVerify(null, input.payload, key, sig);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
return { ok: false, reason: `signature verification threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
440
|
+
}
|
|
441
|
+
return verified ? { ok: true } : { ok: false, reason: "signature did not verify against the stored public key" };
|
|
442
|
+
}
|
|
443
|
+
function mintUlid(nowMs) {
|
|
444
|
+
let ts = Math.max(0, Math.floor(nowMs));
|
|
445
|
+
const time = [];
|
|
446
|
+
for (let i = 0; i < 10; i++) {
|
|
447
|
+
time.unshift(CROCKFORD[ts % 32]);
|
|
448
|
+
ts = Math.floor(ts / 32);
|
|
449
|
+
}
|
|
450
|
+
let rand = "";
|
|
451
|
+
for (let i = 0; i < 16; i++)
|
|
452
|
+
rand += CROCKFORD[randomInt(32)];
|
|
453
|
+
return `${time.join("")}${rand}`;
|
|
454
|
+
}
|
|
455
|
+
export function mintInstructionId(nowMs = Date.now()) {
|
|
456
|
+
return `ins_${mintUlid(nowMs)}`;
|
|
457
|
+
}
|
|
458
|
+
export function mintChallengeNonce() {
|
|
459
|
+
return randomBytes(16).toString("base64url");
|
|
460
|
+
}
|
|
461
|
+
export function mintDeviceEpoch(nowMs = Date.now()) {
|
|
462
|
+
return mintUlid(nowMs);
|
|
463
|
+
}
|
|
464
|
+
//# sourceMappingURL=device-ws-protocol.js.map
|
package/dist/env-facts.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface SandboxEnvFacts {
|
|
|
19
19
|
* workspace 文件已从快照恢复——「scratch lost」≠「工作丢了」)。
|
|
20
20
|
* - ssh/adb:park-only,远端 host/device 持续存在 ⇒ scratch preserved;processes 不声明(前台随
|
|
21
21
|
* 连接断,daemon 存活——不知道的不说)。
|
|
22
|
+
* - device(v2 §4.2):同 ssh/adb 那一档 —— 工作区是员工机磁盘,park-only 赎回后文件还在
|
|
23
|
+
* (core 的 `suspendable:false` 恰恰断言「工作区在目标上外部持久」);processes 不声明:
|
|
24
|
+
* executor 是员工机上的独立进程,断连/合盖后前台进程存亡我们不知道,不知道的不说。
|
|
22
25
|
* - host:同机 ⇒ scratch preserved;processes 不声明(worker 进程可能换代)。
|
|
23
26
|
* - local-docker:容器跨 leg 存续不保证(reconnect 或有或无)⇒ 整域不声明。 */
|
|
24
27
|
resumeFacts?: {
|
|
@@ -42,7 +45,7 @@ export declare function buildEnvFacts(opts: {
|
|
|
42
45
|
* codex 复审 medium:k8s 必须按**有效快照能力**分叉,不能只看车道名——s3Snapshot 未配=park-only,
|
|
43
46
|
* resume 是全新 pod 且**工作区不恢复**,「文件已从快照恢复」那句会让模型信赖不存在的文件(比缺席
|
|
44
47
|
* 更糟的谎)。 */
|
|
45
|
-
export declare function resumeFactsForLane(provider: "e2b" | "k8s" | "ssh" | "adb" | "local-docker" | "host" | undefined, caps?: {
|
|
48
|
+
export declare function resumeFactsForLane(provider: "e2b" | "k8s" | "ssh" | "adb" | "local-docker" | "host" | "device" | undefined, caps?: {
|
|
46
49
|
k8sSnapshot?: boolean;
|
|
47
50
|
}): SandboxEnvFacts["resumeFacts"] | undefined;
|
|
48
51
|
/** Path-safe session segment: the session id can be CALLER-SUPPLIED (body.sessionId), so it must not be able to
|
package/dist/env-facts.js
CHANGED
|
@@ -33,6 +33,7 @@ export function resumeFactsForLane(provider, caps) {
|
|
|
33
33
|
: { processes: "lost", scratch: "lost", note: "resume provisions a fresh pod — the previous workspace is NOT restored" };
|
|
34
34
|
case "ssh":
|
|
35
35
|
case "adb":
|
|
36
|
+
case "device":
|
|
36
37
|
return { scratch: "preserved" };
|
|
37
38
|
case "host":
|
|
38
39
|
case undefined:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 执行车道**闭集**与 per-lane 能力表(design device-executor-lane-v2 §4.1 收敛件,车A-1)。
|
|
3
|
+
*
|
|
4
|
+
* ## 病根:lane 词表是开放字符串上的十几处独立判别式
|
|
5
|
+
*
|
|
6
|
+
* `config.remoteExec?.provider` 的静态型是 `string | undefined` 的判别联合,但消费面从来不是一处
|
|
7
|
+
* switch —— 而是散在十几个文件里的**独立等式/集合/否定**:`cwdHonored` 比 `=== "host"`、
|
|
8
|
+
* `isRemoteScratchpadLane` 查一个 `Set`、`isSandboxPathAdjudicationLane` 取 `!== "host"` 的否定、
|
|
9
|
+
* `sandboxSendLaneEnabled` 是一串 if。于是「lane 词表加一个词」的病形不是编译错,是**每一处的默认
|
|
10
|
+
* 答案静默错**:新词不在 Set 里 ⇒ 静默 false;不等于 `"host"` ⇒ 静默 true。没有一处会报,每一处都
|
|
11
|
+
* 给出一个**没有人表过态**的答案(DISEASE-SHAPES V1/V2;#157 执行车道轴的同族病)。
|
|
12
|
+
*
|
|
13
|
+
* ## 本模块 = 那张表的单源(三件)
|
|
14
|
+
*
|
|
15
|
+
* ① {@link ExecutionLane}:闭集联合。含 `in-process` —— `REMOTE_EXEC` 未设的进程内 stub 形**是**一条
|
|
16
|
+
* 真车道(`boot/resolve-spec.ts` 的 `?? "in-process"`、`config.ts` 的落点记账都已在用这个词),
|
|
17
|
+
* 把它排除在闭集外,普查就少一列。
|
|
18
|
+
* ② {@link executionLaneCaps}:**穷举 switch**(先例 = `capabilities/hands-lane.ts` 的
|
|
19
|
+
* `pickHandsRunner`:无 `default` 臂 ⇒ 联合加词而此处不加臂 = 编译错,而不是运行期落一个默认行)。
|
|
20
|
+
* ③ {@link ExecutionLaneCaps} 的每一位都点名它**今天的判别式坐标**,由对账测试
|
|
21
|
+
* `test/execution-lane-caps.test.ts` 逐位跑真判别式与本表对拍。
|
|
22
|
+
*
|
|
23
|
+
* ## 🔴 本模块是表,不是执法者
|
|
24
|
+
*
|
|
25
|
+
* 消费点今天仍各自判别(把判别式改成读本表 = 分批的后续车)。所以本表的全部价值押在对账测试上:
|
|
26
|
+
* 表与判别式一旦分叉,门红。**唯一**允许的分叉是 {@link PLANNED_LANE_DIVERGENCES} 里逐条登记的
|
|
27
|
+
* 「表是目标态、判别式待某车补」;登记项之外的分叉是漂移,登记项被补齐而不销行同样是红(双向)。
|
|
28
|
+
*
|
|
29
|
+
* ## 能力位的口径
|
|
30
|
+
*
|
|
31
|
+
* 每一位回答的是**车道结构上能不能**,不是「这个部署开没开」:旋钮(`LSP_ENABLED` /
|
|
32
|
+
* `SCHEDULER_ENABLED` / k8s 的 `s3Snapshot`)由消费点在表之上再合取。租户面是唯一的例外 —— 它与
|
|
33
|
+
* 车道语义绑死(host 的单用户前提),所以进了 {@link LaneCapability} 的第三个词而不是靠调用方记住。
|
|
34
|
+
*
|
|
35
|
+
* 依赖纪律:本文件是**叶**(只 type-import config-types),不引 config.ts —— config.ts 反过来引它取
|
|
36
|
+
* {@link RemoteExecProvider}(#157 必需 env 表按同一联合键控)。
|
|
37
|
+
*/
|
|
38
|
+
import type { ServiceConfigFlat } from "./config-types.js";
|
|
39
|
+
/** 一位能力的三态。`single-user-only` = 车道支持,但只在单用户部署(`requirePrincipal !== true`)开 ——
|
|
40
|
+
* 多租户下这一位必须关(host 家族的 cwd/scheduler/backgroundShell/LSP 都是这一档,理由同源:
|
|
41
|
+
* 一个租户的输入不许把 agent 指向 worker 宿主上另一个租户/运维的文件与进程)。 */
|
|
42
|
+
export type LaneCapability = "supported" | "unsupported" | "single-user-only";
|
|
43
|
+
/** `REMOTE_EXEC` 判别联合里的 provider 词全集 —— **派生自真配置型**,不是手抄一份。
|
|
44
|
+
* 手抄的那份挡不住本模块要挡的病:给 `ServiceConfigFlat["remoteExec"]` 加一条臂(`remote-docker` 就是
|
|
45
|
+
* config-types.ts 里写着的下一条)可以完全不碰本文件而编译通过,穷举 switch 于是只对**自己的**副本穷举,
|
|
46
|
+
* 能力表与普查数组一起静默漏行(codex R1-F2)。派生之后:配置加臂 ⇒ 本文件的 switch/词表/夹具同时编译红。 */
|
|
47
|
+
export type RemoteExecProvider = NonNullable<ServiceConfigFlat["remoteExec"]>["provider"];
|
|
48
|
+
/** 闭集:执行车道全集 = provider 词 + `in-process`(`REMOTE_EXEC` 未设的进程内 stub 形,配置面无 provider
|
|
49
|
+
* 可派生 —— 它是「没有 remoteExec」这个状态的名字)。加词 ⇒ {@link executionLaneCaps} 的穷举 switch 与
|
|
50
|
+
* {@link EXECUTION_LANE_WORDS} 两处都编译红,这正是要的(「加词必须逐位表态」)。 */
|
|
51
|
+
export type ExecutionLane = "in-process" | RemoteExecProvider;
|
|
52
|
+
/**
|
|
53
|
+
* per-lane 能力位。每一位的注里点名**今天真正做判决的那处代码**——对账测试按这些坐标跑真函数,
|
|
54
|
+
* 所以坐标漂了(重命名/搬家/改语义)门会红,注释不会烂在这里。
|
|
55
|
+
*/
|
|
56
|
+
export interface ExecutionLaneCaps {
|
|
57
|
+
/** 调用方送来的 `cwd` 是否被尊重。判别式:`task-cwd.ts` 的 `cwdHonored`。 */
|
|
58
|
+
readonly callerCwd: LaneCapability;
|
|
59
|
+
/** core 的后台 shell 面(`run_in_background`/`BashOutput`/`KillShell`)。判别式:各 adapter 的
|
|
60
|
+
* `backgroundCapabilities.supported`(`plugins/remote-env-{host,e2b,k8s}.ts`);host 腿的闸在
|
|
61
|
+
* `boot/execution-env.ts` 的 `hostCfg.backgroundShell`。 */
|
|
62
|
+
readonly backgroundShell: LaneCapability;
|
|
63
|
+
/** 自唤醒 scheduler(`CronCreate`/…)。判别式:`boot/execution-env.ts` host 臂的 `hostScheduler`
|
|
64
|
+
* 与 `http/routes/capabilities.ts` 的 `scheduler` 位(两处同判据)。 */
|
|
65
|
+
readonly scheduler: LaneCapability;
|
|
66
|
+
/** LSP。判别式:`boot/execution-env.ts` 的 `lspManager` 两臂(沙箱桥 e2b/k8s;host 本地 NodeLspManager)。 */
|
|
67
|
+
readonly lsp: LaneCapability;
|
|
68
|
+
/** SendUserFile 的**沙箱直传腿**(host/in-process 的本机 fs 源是另一条腿,不是本位)。
|
|
69
|
+
* 判别式:`capabilities/sandbox-file-send.ts` 的 `sandboxSendLaneEnabled`。 */
|
|
70
|
+
readonly sandboxSendUserFile: LaneCapability;
|
|
71
|
+
/** OS 级隔离边界。判别式:`boot/execution-env.ts` 的 `isolated`(经 `remote_exec_enabled` 日志可观测)。 */
|
|
72
|
+
readonly isolation: LaneCapability;
|
|
73
|
+
/** 可挂起(k8s 以配了 `s3Snapshot` 为前提 —— 「结构上能不能」的口径)。判别式:同上的 `suspendable`。 */
|
|
74
|
+
readonly suspendable: LaneCapability;
|
|
75
|
+
/** `/tmp/scratchpad/<sessionId>` 远端约定。判别式:`plugins/remote-scratchpad.ts` 的 `isRemoteScratchpadLane`。 */
|
|
76
|
+
readonly remoteScratchpad: LaneCapability;
|
|
77
|
+
/** 写门走 #165 的 `DeferredSandboxPathEnv` 代理裁决(而不是直接拿 worker 本机 fs 裁)。
|
|
78
|
+
* 判别式:`boot/deferred-sandbox-path-env.ts` 的 `isSandboxPathAdjudicationLane`。 */
|
|
79
|
+
readonly sandboxPathAdjudication: LaneCapability;
|
|
80
|
+
/** git-worktree 隔离。判别式:`boot/execution-env.ts` 的 host 臂 vs `worktree_isolation_unsupported_lane` warn。 */
|
|
81
|
+
readonly worktreeIsolation: LaneCapability;
|
|
82
|
+
/** 记忆持久面的**自动**表态(部署可用 `MEMORY_PERSISTENCE_CAPABLE` 显式覆盖,那是旋钮不是车道位)。
|
|
83
|
+
* 判别式:`boot/memory-boundary.ts` 的 `effectiveMemoryPersistenceCapable`。 */
|
|
84
|
+
readonly memoryPersistenceCapable: LaneCapability;
|
|
85
|
+
/** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)。判别式:`boot/resolve-spec.ts` 的
|
|
86
|
+
* `!isSandboxPathAdjudicationLane(...)` 与 `boot/stores.ts` 的 `lane === undefined || lane === "host"`
|
|
87
|
+
* ——**两处各写了一份**,对账测试同时咬住两份(它们分叉过就是记忆边界判错盘)。 */
|
|
88
|
+
readonly hostFilePlane: LaneCapability;
|
|
89
|
+
/** core 的 `isRemoteExecutionEnv` 对本车道的判决(鸭子类型;判别位 = `config.remoteExec` 在不在场,
|
|
90
|
+
* **含 host**)。5.26.0 起它决定 `# Memory` 写指令撤不撤。 */
|
|
91
|
+
readonly coreRemoteExecutionEnv: LaneCapability;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* per-lane 能力表。**穷举 switch,禁 default 臂**(hands-lane.ts:39-46 先例)。
|
|
95
|
+
*
|
|
96
|
+
* device 行的表态来自 v2 §4.1/§4.3:
|
|
97
|
+
* · `callerCwd: supported` —— 员工在自己设备的项目目录里干活是这条车道的**存在理由**;host 闸防的
|
|
98
|
+
* 「跨租户宿主路径穿越」在此结构性不存在(路径落在发起者自己的设备上,归属已由 deviceId↔principal
|
|
99
|
+
* 绑定验证),故不要求单用户。今天 `cwdHonored` 还答 false ⇒ 登记在 {@link PLANNED_LANE_DIVERGENCES}。
|
|
100
|
+
* · `sandboxPathAdjudication: supported` —— 自动判真且**这是深思后的正确默认**:写门经 #165 代理把四读
|
|
101
|
+
* 原语转发到设备取真文件系统事实,云盘永不参与设备写目标的裁决。
|
|
102
|
+
* · `remoteScratchpad: supported` —— executor 平台闭集 = darwin|linux,`/tmp/scratchpad/<sessionId>`
|
|
103
|
+
* 的惰性 mkdir 约定原样适用;不加词则 scratchpad envFact 对模型整块缺席。
|
|
104
|
+
* · scheduler / backgroundShell / lsp / sandboxSendUserFile / worktreeIsolation = v1 **诚实关闭**
|
|
105
|
+
* (不是遗漏:设备侧长驻进程、LSP sidecar、直传腿都是 follow-on,声明了就是谎)。
|
|
106
|
+
* · `isolation`/`suspendable` = false/false —— 真设备,不隔离不可快照(与 ssh/adb 同宣告);
|
|
107
|
+
* `suspendable:false` 在 core 契约里同时断言「工作区在目标上外部持久」,员工机磁盘满足,park-only
|
|
108
|
+
* 赎回腿因此合法。
|
|
109
|
+
* · `memoryPersistenceCapable: unsupported` —— 设备的手够不着云 SQL 记忆店,默认撤 `# Memory` 写指令
|
|
110
|
+
* 恰是诚实形(§4.3.4)。
|
|
111
|
+
*/
|
|
112
|
+
export declare function executionLaneCaps(lane: ExecutionLane): ExecutionLaneCaps;
|
|
113
|
+
/** 一个开放字符串是不是闭集里的词。判据 = 词表键在不在,故与联合天然同源。 */
|
|
114
|
+
export declare function isExecutionLane(word: string): word is ExecutionLane;
|
|
115
|
+
/** 车道普查全集(顺序 = 词表声明序;对账测试按它逐 lane 跑真判别式)。 */
|
|
116
|
+
export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
|
|
117
|
+
/**
|
|
118
|
+
* `config.remoteExec?.provider` → 车道词。
|
|
119
|
+
*
|
|
120
|
+
* · `undefined`(REMOTE_EXEC 未设)⇒ `"in-process"`,那是真车道不是缺席;
|
|
121
|
+
* · 认不出的词 ⇒ `undefined`,**调用方必须 fail-closed**(禁静默按某个默认车道处理)。生产链上这一臂
|
|
122
|
+
* 不可达:`loadConfig` 的 #157 拒启门先咬未知词;它存在是为了消费点自卫时有个响亮出口。
|
|
123
|
+
*/
|
|
124
|
+
export declare function executionLaneOf(provider: string | undefined): ExecutionLane | undefined;
|
|
125
|
+
/** 把一位能力在**具体部署**上落成布尔:租户面是唯一入参(旋钮由消费点自己再合取)。
|
|
126
|
+
* 穷举 switch,无 default 臂 —— {@link LaneCapability} 加词而此处不加臂 = 编译红。 */
|
|
127
|
+
export declare function laneCapabilityHolds(cap: LaneCapability, deployment: {
|
|
128
|
+
requirePrincipal?: boolean;
|
|
129
|
+
}): boolean;
|
|
130
|
+
/** 一条**已登记**的分叉:表写的是目标态,今天的判别式还答别的。 */
|
|
131
|
+
export interface PlannedLaneDivergence {
|
|
132
|
+
readonly lane: ExecutionLane;
|
|
133
|
+
readonly capability: keyof ExecutionLaneCaps;
|
|
134
|
+
/** 今天那处判别式给出的答案(对账测试拿它对拍 —— 分叉被补齐了这一行就红,逼着销行)。 */
|
|
135
|
+
readonly today: LaneCapability;
|
|
136
|
+
/** 谁来补(设计稿里的车号)。 */
|
|
137
|
+
readonly owner: string;
|
|
138
|
+
readonly why: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 分叉登记簿 —— **闭集**。对账测试双向咬:表外分叉 = 漂移(红),表内分叉被补齐而不销行 = 陈账(红)。
|
|
142
|
+
*
|
|
143
|
+
* 🟢 **空表 = 今天的真实状态**(车A-4,2026-08-28 销行)。唯一那条(device 的 `callerCwd`)已按登记的
|
|
144
|
+
* 条件补齐:`deviceCwdHonored`(`task-cwd.ts`)+ 写侧闸(`boot/resolve-spec.ts` 的同一处单写者)+ 读侧
|
|
145
|
+
* 消费(`plugins/remote-env-device.ts` 的 `effectiveDeviceCwd`)**同批**落地,于是能力表写的 `supported`
|
|
146
|
+
* 与判别式给的答案一致,分叉自然消失 —— 对账测试的双向咬(表外分叉 = 红 / 表内陈账 = 红)因此对本表
|
|
147
|
+
* 只剩「保持空」这一条要求。
|
|
148
|
+
*
|
|
149
|
+
* 加行的门槛不变:一条分叉必须写清 today / owner / why 三件,并在补齐的**同一批**里销掉。
|
|
150
|
+
*/
|
|
151
|
+
export declare const PLANNED_LANE_DIVERGENCES: readonly PlannedLaneDivergence[];
|
|
152
|
+
//# sourceMappingURL=execution-lane-caps.d.ts.map
|