@runuai/host 0.9.14 → 0.9.42
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/README.md +22 -5
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +33 -2
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3060 -218
- package/lib/preview-sidecar.ts +57 -13
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1405 -107
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +766 -42
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
package/src/protocol.ts
CHANGED
|
@@ -29,6 +29,31 @@ export type HostCommandResult<T> =
|
|
|
29
29
|
code: HostErrorCode;
|
|
30
30
|
message: string;
|
|
31
31
|
retryable?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Narrow rolling-safe reason for a retryable refusal. Old cloud readers
|
|
34
|
+
* ignore this additive field; new readers use it only to bridge the
|
|
35
|
+
* short supervisor restart after a managed-runtime activation. Generic
|
|
36
|
+
* `retryable` failures (for example, no Docker runtime) remain fail-fast.
|
|
37
|
+
*/
|
|
38
|
+
retryReason?: "managed_restart" | "runtime_setup";
|
|
39
|
+
/**
|
|
40
|
+
* Who decided this failed.
|
|
41
|
+
*
|
|
42
|
+
* `"cloud"` means the command never reached a host at all — the bridge
|
|
43
|
+
* had no socket, the socket belongs to another instance, or a relay/ack
|
|
44
|
+
* deadline expired. Absent means the HOST answered.
|
|
45
|
+
*
|
|
46
|
+
* It is a separate field rather than a distinct error code on purpose.
|
|
47
|
+
* `host_unavailable` is emitted from both sides — the host uses it for
|
|
48
|
+
* its own probes (Docker not answering, Git state unverifiable) — and a
|
|
49
|
+
* caller that must tell those apart, like the ADR-084 failure streak,
|
|
50
|
+
* cannot do it from the code. Splitting the CODE instead would put a new
|
|
51
|
+
* wire value in front of instances that predate it: during a rolling
|
|
52
|
+
* deploy an old cloud receives a relayed reply from a new socket owner,
|
|
53
|
+
* finds no case for it in `hostStatus`, and renders an error as HTTP 200.
|
|
54
|
+
* An unknown FIELD is ignored by old readers; an unknown CODE is not.
|
|
55
|
+
*/
|
|
56
|
+
origin?: "cloud";
|
|
32
57
|
};
|
|
33
58
|
|
|
34
59
|
/** Capability ids shared by host advertisement and fail-closed cloud gates. */
|
|
@@ -46,6 +71,41 @@ export const GITHUB_CREDENTIAL_GENERATION_PROTOCOL_FEATURE =
|
|
|
46
71
|
// a disconnect no longer loses events (they replay), so it keeps its
|
|
47
72
|
// in-flight turn buffers across the gap.
|
|
48
73
|
export const EVENT_REPLAY_PROTOCOL_FEATURE = "event-replay-v1";
|
|
74
|
+
/** ADR-100 L2: the cloud can request a bounded, positive-evidence inventory
|
|
75
|
+
* of task-owned state from this authenticated host generation. */
|
|
76
|
+
export const HOST_TASK_INVENTORY_PROTOCOL_FEATURE = "host-task-inventory-v1";
|
|
77
|
+
/** ADR-100 L3: a host advertising this feature publishes its one-way
|
|
78
|
+
* boot-maintenance readiness latch and rejects task/container work until the
|
|
79
|
+
* initial standard-image and shared-CLI reconciliation has settled. */
|
|
80
|
+
export const HOST_MAINTENANCE_READINESS_PROTOCOL_FEATURE =
|
|
81
|
+
"host-maintenance-readiness-v1";
|
|
82
|
+
/** ADR-100 L3: bounded health for the host-local MCP gateway listener. */
|
|
83
|
+
export const MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE = "mcp-gateway-health-v1";
|
|
84
|
+
/** ADR-100 remote host operations. Each family is independently advertised so
|
|
85
|
+
* a rolling cloud can expose only controls that the connected host supports. */
|
|
86
|
+
export const HOST_LOGS_PROTOCOL_FEATURE = "host-logs-v1";
|
|
87
|
+
export const HOST_ENGINE_LOGIN_PROTOCOL_FEATURE = "host-engine-login-v1";
|
|
88
|
+
export const HOST_CONFIG_PROTOCOL_FEATURE = "host-config-v1";
|
|
89
|
+
export const MAX_HOST_OP_ID_CHARS = 128;
|
|
90
|
+
export const MAX_HOST_LOG_LINES = 1_000;
|
|
91
|
+
export const MAX_HOST_LOG_LINE_BYTES = 4 * 1_024;
|
|
92
|
+
export const MAX_HOST_LOG_CHUNK_LINES = 100;
|
|
93
|
+
export const MAX_HOST_LOG_CHUNK_BYTES = 64 * 1_024;
|
|
94
|
+
export const MAX_ENGINE_LOGIN_TEXT_CHARS = 4_096;
|
|
95
|
+
export const MAX_ENGINE_LOGIN_QUERY_CHARS = 4_096;
|
|
96
|
+
export const MAX_ENGINE_LOGIN_URL_CHARS = 2_048;
|
|
97
|
+
export const MAX_REMOTE_HOST_MESSAGE_BYTES = 512;
|
|
98
|
+
export const MAX_HOST_TASK_INVENTORY_PAGE_SIZE = 256;
|
|
99
|
+
export const MAX_HOST_TASK_INVENTORY_SCAN_ID_CHARS = 128;
|
|
100
|
+
export const MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS = 128;
|
|
101
|
+
/**
|
|
102
|
+
* A redeem response carrying this exact header proves the cloud checks a spent
|
|
103
|
+
* token before expiry. That lets a new CLI distinguish a definitive 404/410
|
|
104
|
+
* from an old/rolled-back handler whose 410 could follow a committed response
|
|
105
|
+
* that was lost in transit.
|
|
106
|
+
*/
|
|
107
|
+
export const ENROLLMENT_REPLAY_PROTOCOL_HEADER = "x-uai-enrollment-replay";
|
|
108
|
+
export const ENROLLMENT_REPLAY_PROTOCOL_VERSION = "same-host-v1";
|
|
49
109
|
export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
|
|
50
110
|
export const MAX_AGENT_ID_CHARS = 128;
|
|
51
111
|
export const MAX_SECRETARY_DISPATCH_RECIPIENTS = 16;
|
|
@@ -68,6 +128,20 @@ export interface CommandContext {
|
|
|
68
128
|
* dead resolver per retry.
|
|
69
129
|
*/
|
|
70
130
|
timeoutMs?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Cloud-internal routing override for a durable taskDown. Lifecycle cleanup
|
|
133
|
+
* may outlive the task row whose host assignment normally routes commands.
|
|
134
|
+
* This field is context metadata: it is never serialized onto the host
|
|
135
|
+
* command frame, and no host/request payload may populate it.
|
|
136
|
+
*/
|
|
137
|
+
targetHostId?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Cloud-internal credential-generation fence paired with targetHostId.
|
|
140
|
+
* Durable cleanup snapshots the registry verifier before source deletion;
|
|
141
|
+
* the bridge must route only to a socket authenticated with this exact hash.
|
|
142
|
+
* Like targetHostId, this is relay metadata and never reaches the host wire.
|
|
143
|
+
*/
|
|
144
|
+
expectedHostTokenHash?: string;
|
|
71
145
|
}
|
|
72
146
|
|
|
73
147
|
export type PermissionDecision = { kind: "accept" } | { kind: "decline" };
|
|
@@ -100,6 +174,557 @@ export interface TaskAgent {
|
|
|
100
174
|
permissions?: string[];
|
|
101
175
|
}
|
|
102
176
|
|
|
177
|
+
export type ContainerRuntimePreference = "auto" | "docker" | "apple-container";
|
|
178
|
+
|
|
179
|
+
/** ADR-101 runtime state. The wire shape is deliberately impossible to
|
|
180
|
+
* misinterpret: transitional state has no verdict, ready names the selected
|
|
181
|
+
* provider, and a failed verdict always carries bounded operator guidance. */
|
|
182
|
+
export type ContainerRuntimeCapability =
|
|
183
|
+
| {
|
|
184
|
+
status: "checking";
|
|
185
|
+
preference: ContainerRuntimePreference;
|
|
186
|
+
provider?: never;
|
|
187
|
+
message?: never;
|
|
188
|
+
action?: never;
|
|
189
|
+
}
|
|
190
|
+
| {
|
|
191
|
+
status: "ready";
|
|
192
|
+
preference: "auto" | "docker";
|
|
193
|
+
provider: "docker";
|
|
194
|
+
message?: never;
|
|
195
|
+
action?: never;
|
|
196
|
+
}
|
|
197
|
+
| {
|
|
198
|
+
status: "ready";
|
|
199
|
+
preference: "auto" | "apple-container";
|
|
200
|
+
provider: "apple-container";
|
|
201
|
+
message?: never;
|
|
202
|
+
action?: never;
|
|
203
|
+
}
|
|
204
|
+
| {
|
|
205
|
+
status: "no-runtime";
|
|
206
|
+
preference: ContainerRuntimePreference;
|
|
207
|
+
provider?: never;
|
|
208
|
+
message: string;
|
|
209
|
+
action: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** ADR-100: bounded operator-visible state for the host-local MCP gateway.
|
|
213
|
+
* Raw bind errors are deliberately not part of the capability frame. */
|
|
214
|
+
export type McpGatewayCapability =
|
|
215
|
+
| { status: "starting"; port: number }
|
|
216
|
+
| { status: "ready"; port: number }
|
|
217
|
+
| {
|
|
218
|
+
status: "down";
|
|
219
|
+
port: number;
|
|
220
|
+
reason: "port_in_use" | "bind_failed";
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** ADR-100 operation ids are deliberately URL/DOM-safe and bounded before
|
|
224
|
+
* they are admitted to any per-connection operation map. */
|
|
225
|
+
export type HostOpId = string;
|
|
226
|
+
|
|
227
|
+
export interface HostLogsStartFrame {
|
|
228
|
+
kind: "host.logs.start";
|
|
229
|
+
opId: HostOpId;
|
|
230
|
+
lines: number;
|
|
231
|
+
follow: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface HostLogsStopFrame {
|
|
235
|
+
kind: "host.logs.stop";
|
|
236
|
+
opId: HostOpId;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface HostLogsChunkFrame {
|
|
240
|
+
kind: "host.logs.chunk";
|
|
241
|
+
opId: HostOpId;
|
|
242
|
+
lines: string[];
|
|
243
|
+
/** Lines discarded by the host's bounded drop-oldest buffer. Omitted at 0. */
|
|
244
|
+
dropped?: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type HostLogsEndReason = "complete" | "stopped" | "error";
|
|
248
|
+
|
|
249
|
+
export type HostLogsEndFrame =
|
|
250
|
+
| {
|
|
251
|
+
kind: "host.logs.end";
|
|
252
|
+
opId: HostOpId;
|
|
253
|
+
reason: "complete" | "stopped";
|
|
254
|
+
message?: never;
|
|
255
|
+
}
|
|
256
|
+
| {
|
|
257
|
+
kind: "host.logs.end";
|
|
258
|
+
opId: HostOpId;
|
|
259
|
+
reason: "error";
|
|
260
|
+
message: string;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export type EngineLoginKind = "claude" | "codex";
|
|
264
|
+
export type EngineLoginPhase =
|
|
265
|
+
| "starting"
|
|
266
|
+
| "authorize"
|
|
267
|
+
| "awaiting_input"
|
|
268
|
+
| "awaiting_callback"
|
|
269
|
+
| "succeeded"
|
|
270
|
+
| "failed"
|
|
271
|
+
| "cancelled"
|
|
272
|
+
| "timed_out";
|
|
273
|
+
export type EngineLoginErrorCode =
|
|
274
|
+
| "unavailable"
|
|
275
|
+
| "start_failed"
|
|
276
|
+
| "invalid_input"
|
|
277
|
+
| "invalid_callback"
|
|
278
|
+
| "persist_failed"
|
|
279
|
+
| "timeout";
|
|
280
|
+
|
|
281
|
+
export interface EngineLoginStartFrame {
|
|
282
|
+
kind: "engine.login.start";
|
|
283
|
+
opId: HostOpId;
|
|
284
|
+
engine: EngineLoginKind;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The only generic one-time response channel. Long-lived tokens must never
|
|
288
|
+
* be represented by this protocol and are persisted entirely on the host. */
|
|
289
|
+
export interface EngineLoginInputFrame {
|
|
290
|
+
kind: "engine.login.input";
|
|
291
|
+
opId: HostOpId;
|
|
292
|
+
text: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The pasted failed-localhost callback query used by Codex's login flow. */
|
|
296
|
+
export interface EngineLoginCallbackFrame {
|
|
297
|
+
kind: "engine.login.callback";
|
|
298
|
+
opId: HostOpId;
|
|
299
|
+
query: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface EngineLoginStopFrame {
|
|
303
|
+
kind: "engine.login.stop";
|
|
304
|
+
opId: HostOpId;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface EngineLoginEventBase {
|
|
308
|
+
kind: "engine.login.event";
|
|
309
|
+
opId: HostOpId;
|
|
310
|
+
engine: EngineLoginKind;
|
|
311
|
+
message?: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Secret-blind host→cloud login progress. OAuth codes and callback queries
|
|
315
|
+
* have no field in this direction; `errorCode` is a closed machine reason. */
|
|
316
|
+
export type EngineLoginEventFrame =
|
|
317
|
+
| (EngineLoginEventBase & {
|
|
318
|
+
phase: "authorize";
|
|
319
|
+
url: string;
|
|
320
|
+
errorCode?: never;
|
|
321
|
+
})
|
|
322
|
+
| (EngineLoginEventBase & {
|
|
323
|
+
phase: "failed" | "timed_out";
|
|
324
|
+
url?: never;
|
|
325
|
+
errorCode?: EngineLoginErrorCode;
|
|
326
|
+
})
|
|
327
|
+
| (EngineLoginEventBase & {
|
|
328
|
+
phase:
|
|
329
|
+
| "starting"
|
|
330
|
+
| "awaiting_input"
|
|
331
|
+
| "awaiting_callback"
|
|
332
|
+
| "succeeded"
|
|
333
|
+
| "cancelled";
|
|
334
|
+
url?: never;
|
|
335
|
+
errorCode?: never;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
/** Rolling-safe, secret-free engine state. v1 intentionally makes no claim
|
|
339
|
+
* about expiry; that requires positive adapter evidence in a future version. */
|
|
340
|
+
export interface EngineLoginCapability {
|
|
341
|
+
engine: EngineLoginKind;
|
|
342
|
+
status: "configured" | "missing";
|
|
343
|
+
loginSupported: true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The exact local host settings ADR-100 permits over the bridge. */
|
|
347
|
+
export interface HostConfigState {
|
|
348
|
+
agentCliAutoupdate: boolean;
|
|
349
|
+
telemetryEnabled: boolean;
|
|
350
|
+
retainTerminalHistory: boolean;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export type HostConfigPatch = Partial<HostConfigState>;
|
|
354
|
+
|
|
355
|
+
export interface HostConfigGetFrame {
|
|
356
|
+
kind: "host.config.get";
|
|
357
|
+
opId: HostOpId;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface HostConfigSetFrame {
|
|
361
|
+
kind: "host.config.set";
|
|
362
|
+
opId: HostOpId;
|
|
363
|
+
patch: HostConfigPatch;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export type HostConfigErrorCode = "invalid_patch" | "persistence_failed";
|
|
367
|
+
|
|
368
|
+
export type HostConfigAckFrame =
|
|
369
|
+
| {
|
|
370
|
+
kind: "host.config.ack";
|
|
371
|
+
opId: HostOpId;
|
|
372
|
+
ok: true;
|
|
373
|
+
config: HostConfigState;
|
|
374
|
+
restartRequired: boolean;
|
|
375
|
+
}
|
|
376
|
+
| {
|
|
377
|
+
kind: "host.config.ack";
|
|
378
|
+
opId: HostOpId;
|
|
379
|
+
ok: false;
|
|
380
|
+
errorCode: HostConfigErrorCode;
|
|
381
|
+
message: string;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
type WireObject = Record<string, unknown>;
|
|
385
|
+
const utf8Encoder = new TextEncoder();
|
|
386
|
+
|
|
387
|
+
function utf8ByteLength(value: string): number {
|
|
388
|
+
return utf8Encoder.encode(value).byteLength;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function exactWireObject(
|
|
392
|
+
value: unknown,
|
|
393
|
+
required: readonly string[],
|
|
394
|
+
optional: readonly string[] = [],
|
|
395
|
+
): WireObject | null {
|
|
396
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
397
|
+
const object = value as WireObject;
|
|
398
|
+
const keys = Object.keys(object);
|
|
399
|
+
const allowed = new Set([...required, ...optional]);
|
|
400
|
+
if (
|
|
401
|
+
required.some((key) => !Object.hasOwn(object, key)) ||
|
|
402
|
+
keys.some((key) => !allowed.has(key))
|
|
403
|
+
) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
return object;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isBoundedMessage(value: unknown): value is string {
|
|
410
|
+
return (
|
|
411
|
+
typeof value === "string" &&
|
|
412
|
+
utf8ByteLength(value) <= MAX_REMOTE_HOST_MESSAGE_BYTES
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isEngineLoginKind(value: unknown): value is EngineLoginKind {
|
|
417
|
+
return value === "claude" || value === "codex";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isEngineLoginErrorCode(value: unknown): value is EngineLoginErrorCode {
|
|
421
|
+
return (
|
|
422
|
+
value === "unavailable" ||
|
|
423
|
+
value === "start_failed" ||
|
|
424
|
+
value === "invalid_input" ||
|
|
425
|
+
value === "invalid_callback" ||
|
|
426
|
+
value === "persist_failed" ||
|
|
427
|
+
value === "timeout"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function isHttpsUrl(value: unknown): value is string {
|
|
432
|
+
if (
|
|
433
|
+
typeof value !== "string" ||
|
|
434
|
+
value.length === 0 ||
|
|
435
|
+
value.length > MAX_ENGINE_LOGIN_URL_CHARS ||
|
|
436
|
+
value.trim() !== value
|
|
437
|
+
) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
const url = new URL(value);
|
|
442
|
+
return url.protocol === "https:" && !url.username && !url.password;
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function isHostOpId(value: unknown): value is HostOpId {
|
|
449
|
+
return (
|
|
450
|
+
typeof value === "string" &&
|
|
451
|
+
value.length <= MAX_HOST_OP_ID_CHARS &&
|
|
452
|
+
/^[A-Za-z0-9_-]+$/.test(value)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function isHostLogsStartFrame(
|
|
457
|
+
value: unknown,
|
|
458
|
+
): value is HostLogsStartFrame {
|
|
459
|
+
const frame = exactWireObject(value, ["kind", "opId", "lines", "follow"]);
|
|
460
|
+
return Boolean(
|
|
461
|
+
frame &&
|
|
462
|
+
frame.kind === "host.logs.start" &&
|
|
463
|
+
isHostOpId(frame.opId) &&
|
|
464
|
+
typeof frame.lines === "number" &&
|
|
465
|
+
Number.isSafeInteger(frame.lines) &&
|
|
466
|
+
frame.lines >= 1 &&
|
|
467
|
+
frame.lines <= MAX_HOST_LOG_LINES &&
|
|
468
|
+
typeof frame.follow === "boolean",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function isHostLogsStopFrame(
|
|
473
|
+
value: unknown,
|
|
474
|
+
): value is HostLogsStopFrame {
|
|
475
|
+
const frame = exactWireObject(value, ["kind", "opId"]);
|
|
476
|
+
return Boolean(
|
|
477
|
+
frame && frame.kind === "host.logs.stop" && isHostOpId(frame.opId),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function isHostLogsChunkFrame(
|
|
482
|
+
value: unknown,
|
|
483
|
+
): value is HostLogsChunkFrame {
|
|
484
|
+
const frame = exactWireObject(value, ["kind", "opId", "lines"], ["dropped"]);
|
|
485
|
+
if (
|
|
486
|
+
!frame ||
|
|
487
|
+
frame.kind !== "host.logs.chunk" ||
|
|
488
|
+
!isHostOpId(frame.opId) ||
|
|
489
|
+
!Array.isArray(frame.lines) ||
|
|
490
|
+
frame.lines.length < 1 ||
|
|
491
|
+
frame.lines.length > MAX_HOST_LOG_CHUNK_LINES ||
|
|
492
|
+
frame.lines.some(
|
|
493
|
+
(line) =>
|
|
494
|
+
typeof line !== "string" ||
|
|
495
|
+
utf8ByteLength(line) > MAX_HOST_LOG_LINE_BYTES,
|
|
496
|
+
) ||
|
|
497
|
+
frame.lines.reduce(
|
|
498
|
+
(bytes, line) => bytes + utf8ByteLength(line as string),
|
|
499
|
+
0,
|
|
500
|
+
) > MAX_HOST_LOG_CHUNK_BYTES
|
|
501
|
+
) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return (
|
|
505
|
+
!Object.hasOwn(frame, "dropped") ||
|
|
506
|
+
(typeof frame.dropped === "number" &&
|
|
507
|
+
Number.isSafeInteger(frame.dropped) &&
|
|
508
|
+
frame.dropped >= 0)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function isHostLogsEndFrame(value: unknown): value is HostLogsEndFrame {
|
|
513
|
+
const frame = exactWireObject(value, ["kind", "opId", "reason"], ["message"]);
|
|
514
|
+
if (
|
|
515
|
+
!frame ||
|
|
516
|
+
frame.kind !== "host.logs.end" ||
|
|
517
|
+
!isHostOpId(frame.opId)
|
|
518
|
+
) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
if (frame.reason === "error") {
|
|
522
|
+
return Object.hasOwn(frame, "message") && isBoundedMessage(frame.message);
|
|
523
|
+
}
|
|
524
|
+
return (
|
|
525
|
+
(frame.reason === "complete" || frame.reason === "stopped") &&
|
|
526
|
+
!Object.hasOwn(frame, "message")
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function isEngineLoginStartFrame(
|
|
531
|
+
value: unknown,
|
|
532
|
+
): value is EngineLoginStartFrame {
|
|
533
|
+
const frame = exactWireObject(value, ["kind", "opId", "engine"]);
|
|
534
|
+
return Boolean(
|
|
535
|
+
frame &&
|
|
536
|
+
frame.kind === "engine.login.start" &&
|
|
537
|
+
isHostOpId(frame.opId) &&
|
|
538
|
+
isEngineLoginKind(frame.engine),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function isEngineLoginInputFrame(
|
|
543
|
+
value: unknown,
|
|
544
|
+
): value is EngineLoginInputFrame {
|
|
545
|
+
const frame = exactWireObject(value, ["kind", "opId", "text"]);
|
|
546
|
+
return Boolean(
|
|
547
|
+
frame &&
|
|
548
|
+
frame.kind === "engine.login.input" &&
|
|
549
|
+
isHostOpId(frame.opId) &&
|
|
550
|
+
typeof frame.text === "string" &&
|
|
551
|
+
frame.text.length >= 1 &&
|
|
552
|
+
frame.text.length <= MAX_ENGINE_LOGIN_TEXT_CHARS,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function isEngineLoginCallbackFrame(
|
|
557
|
+
value: unknown,
|
|
558
|
+
): value is EngineLoginCallbackFrame {
|
|
559
|
+
const frame = exactWireObject(value, ["kind", "opId", "query"]);
|
|
560
|
+
return Boolean(
|
|
561
|
+
frame &&
|
|
562
|
+
frame.kind === "engine.login.callback" &&
|
|
563
|
+
isHostOpId(frame.opId) &&
|
|
564
|
+
typeof frame.query === "string" &&
|
|
565
|
+
frame.query.length >= 1 &&
|
|
566
|
+
frame.query.length <= MAX_ENGINE_LOGIN_QUERY_CHARS,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function isEngineLoginStopFrame(
|
|
571
|
+
value: unknown,
|
|
572
|
+
): value is EngineLoginStopFrame {
|
|
573
|
+
const frame = exactWireObject(value, ["kind", "opId"]);
|
|
574
|
+
return Boolean(
|
|
575
|
+
frame && frame.kind === "engine.login.stop" && isHostOpId(frame.opId),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function isEngineLoginEventFrame(
|
|
580
|
+
value: unknown,
|
|
581
|
+
): value is EngineLoginEventFrame {
|
|
582
|
+
const frame = exactWireObject(
|
|
583
|
+
value,
|
|
584
|
+
["kind", "opId", "engine", "phase"],
|
|
585
|
+
["url", "errorCode", "message"],
|
|
586
|
+
);
|
|
587
|
+
if (
|
|
588
|
+
!frame ||
|
|
589
|
+
frame.kind !== "engine.login.event" ||
|
|
590
|
+
!isHostOpId(frame.opId) ||
|
|
591
|
+
!isEngineLoginKind(frame.engine) ||
|
|
592
|
+
(Object.hasOwn(frame, "message") && !isBoundedMessage(frame.message))
|
|
593
|
+
) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
if (frame.phase === "authorize") {
|
|
597
|
+
return (
|
|
598
|
+
isHttpsUrl(frame.url) &&
|
|
599
|
+
!Object.hasOwn(frame, "errorCode")
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (Object.hasOwn(frame, "url")) return false;
|
|
603
|
+
if (frame.phase === "failed" || frame.phase === "timed_out") {
|
|
604
|
+
return (
|
|
605
|
+
!Object.hasOwn(frame, "errorCode") ||
|
|
606
|
+
isEngineLoginErrorCode(frame.errorCode)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return (
|
|
610
|
+
(frame.phase === "starting" ||
|
|
611
|
+
frame.phase === "awaiting_input" ||
|
|
612
|
+
frame.phase === "awaiting_callback" ||
|
|
613
|
+
frame.phase === "succeeded" ||
|
|
614
|
+
frame.phase === "cancelled") &&
|
|
615
|
+
!Object.hasOwn(frame, "errorCode")
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function isEngineLoginCapability(
|
|
620
|
+
value: unknown,
|
|
621
|
+
): value is EngineLoginCapability {
|
|
622
|
+
const capability = exactWireObject(value, [
|
|
623
|
+
"engine",
|
|
624
|
+
"status",
|
|
625
|
+
"loginSupported",
|
|
626
|
+
]);
|
|
627
|
+
return Boolean(
|
|
628
|
+
capability &&
|
|
629
|
+
isEngineLoginKind(capability.engine) &&
|
|
630
|
+
(capability.status === "configured" || capability.status === "missing") &&
|
|
631
|
+
capability.loginSupported === true,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function isHostConfigState(value: unknown): value is HostConfigState {
|
|
636
|
+
const config = exactWireObject(value, [
|
|
637
|
+
"agentCliAutoupdate",
|
|
638
|
+
"telemetryEnabled",
|
|
639
|
+
"retainTerminalHistory",
|
|
640
|
+
]);
|
|
641
|
+
return Boolean(
|
|
642
|
+
config &&
|
|
643
|
+
typeof config.agentCliAutoupdate === "boolean" &&
|
|
644
|
+
typeof config.telemetryEnabled === "boolean" &&
|
|
645
|
+
typeof config.retainTerminalHistory === "boolean",
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function isHostConfigPatch(value: unknown): value is HostConfigPatch {
|
|
650
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
651
|
+
const patch = value as WireObject;
|
|
652
|
+
const keys = Object.keys(patch);
|
|
653
|
+
const allowed = new Set([
|
|
654
|
+
"agentCliAutoupdate",
|
|
655
|
+
"telemetryEnabled",
|
|
656
|
+
"retainTerminalHistory",
|
|
657
|
+
]);
|
|
658
|
+
return (
|
|
659
|
+
keys.length >= 1 &&
|
|
660
|
+
keys.every(
|
|
661
|
+
(key) => allowed.has(key) && typeof patch[key] === "boolean",
|
|
662
|
+
)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function isHostConfigGetFrame(
|
|
667
|
+
value: unknown,
|
|
668
|
+
): value is HostConfigGetFrame {
|
|
669
|
+
const frame = exactWireObject(value, ["kind", "opId"]);
|
|
670
|
+
return Boolean(
|
|
671
|
+
frame && frame.kind === "host.config.get" && isHostOpId(frame.opId),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function isHostConfigSetFrame(
|
|
676
|
+
value: unknown,
|
|
677
|
+
): value is HostConfigSetFrame {
|
|
678
|
+
const frame = exactWireObject(value, ["kind", "opId", "patch"]);
|
|
679
|
+
return Boolean(
|
|
680
|
+
frame &&
|
|
681
|
+
frame.kind === "host.config.set" &&
|
|
682
|
+
isHostOpId(frame.opId) &&
|
|
683
|
+
isHostConfigPatch(frame.patch),
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function isHostConfigAckFrame(
|
|
688
|
+
value: unknown,
|
|
689
|
+
): value is HostConfigAckFrame {
|
|
690
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
691
|
+
const candidate = value as WireObject;
|
|
692
|
+
if (candidate.ok === true) {
|
|
693
|
+
const frame = exactWireObject(value, [
|
|
694
|
+
"kind",
|
|
695
|
+
"opId",
|
|
696
|
+
"ok",
|
|
697
|
+
"config",
|
|
698
|
+
"restartRequired",
|
|
699
|
+
]);
|
|
700
|
+
return Boolean(
|
|
701
|
+
frame &&
|
|
702
|
+
frame.kind === "host.config.ack" &&
|
|
703
|
+
isHostOpId(frame.opId) &&
|
|
704
|
+
isHostConfigState(frame.config) &&
|
|
705
|
+
typeof frame.restartRequired === "boolean",
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (candidate.ok === false) {
|
|
709
|
+
const frame = exactWireObject(value, [
|
|
710
|
+
"kind",
|
|
711
|
+
"opId",
|
|
712
|
+
"ok",
|
|
713
|
+
"errorCode",
|
|
714
|
+
"message",
|
|
715
|
+
]);
|
|
716
|
+
return Boolean(
|
|
717
|
+
frame &&
|
|
718
|
+
frame.kind === "host.config.ack" &&
|
|
719
|
+
isHostOpId(frame.opId) &&
|
|
720
|
+
(frame.errorCode === "invalid_patch" ||
|
|
721
|
+
frame.errorCode === "persistence_failed") &&
|
|
722
|
+
isBoundedMessage(frame.message),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
|
|
103
728
|
/**
|
|
104
729
|
* Host capability advertisement (ADR-021). Sent as a `host.capabilities`
|
|
105
730
|
* frame after auth-success and re-sent when the host's adapter registry
|
|
@@ -131,6 +756,16 @@ export interface HostCapabilities {
|
|
|
131
756
|
kind: string;
|
|
132
757
|
availableVersions: string[];
|
|
133
758
|
}>;
|
|
759
|
+
/** ADR-101: the machine-level container backend. Optional for rolling
|
|
760
|
+
* compatibility with hosts predating runtime detection. */
|
|
761
|
+
containerRuntime?: ContainerRuntimeCapability;
|
|
762
|
+
/** ADR-100: one-way boot latch. It becomes true after the initial shared
|
|
763
|
+
* image/CLI maintenance pass settles, including a best-effort failure. */
|
|
764
|
+
maintenanceReady?: boolean;
|
|
765
|
+
/** ADR-100: health of the independently-retrying host MCP gateway. */
|
|
766
|
+
mcpGateway?: McpGatewayCapability;
|
|
767
|
+
/** ADR-100: feature-gated, secret-free remote login availability. */
|
|
768
|
+
engineLogins?: EngineLoginCapability[];
|
|
134
769
|
// ADR-033: cloud user ids that currently have a GitHub token ON THIS HOST —
|
|
135
770
|
// the per-host gh-connected state shown on the host detail page. Re-advertised
|
|
136
771
|
// whenever a token is added/removed. Optional: older hosts omit it.
|
|
@@ -190,6 +825,9 @@ export interface TaskUpResult {
|
|
|
190
825
|
export interface TaskDownResult {
|
|
191
826
|
status?: string;
|
|
192
827
|
alreadyGone?: boolean;
|
|
828
|
+
/** Present only after an ADR-100 orphan-GC command has proved runtime,
|
|
829
|
+
* filesystem, credential/ACL, and host-SQLite task state absent. */
|
|
830
|
+
localStatePruned?: true;
|
|
193
831
|
[key: string]: unknown;
|
|
194
832
|
}
|
|
195
833
|
|
|
@@ -278,6 +916,28 @@ export interface TaskDownInput {
|
|
|
278
916
|
taskId: string;
|
|
279
917
|
task: TaskCommandTask;
|
|
280
918
|
projects: TaskCommandProject[];
|
|
919
|
+
/** Optional and additive for rolling compatibility. Ordinary taskDown keeps
|
|
920
|
+
* terminal host history; orphan GC deletes task-owned local metadata only
|
|
921
|
+
* after the destructive teardown has been positively verified. */
|
|
922
|
+
cleanup?: {
|
|
923
|
+
kind: "orphan-gc";
|
|
924
|
+
pruneLocalState: true;
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export type HostTaskInventorySource =
|
|
929
|
+
| "db"
|
|
930
|
+
| "session"
|
|
931
|
+
| "event"
|
|
932
|
+
| "workspace"
|
|
933
|
+
| "compose"
|
|
934
|
+
| "network"
|
|
935
|
+
| "volume"
|
|
936
|
+
| "preview";
|
|
937
|
+
|
|
938
|
+
export interface HostTaskInventoryEntry {
|
|
939
|
+
taskId: string;
|
|
940
|
+
sources: HostTaskInventorySource[];
|
|
281
941
|
}
|
|
282
942
|
|
|
283
943
|
export interface TaskDiffInput {
|
|
@@ -609,6 +1269,24 @@ export type CloudToHost =
|
|
|
609
1269
|
// probe (headerValue, clientSecret) are write-only; nothing secret ever
|
|
610
1270
|
// rides an ack. `opId` correlates request↔ack.
|
|
611
1271
|
| { kind: "mcp.op"; opId: string; op: McpOp }
|
|
1272
|
+
// ADR-100 remote host operations. Each family is capability-gated by the
|
|
1273
|
+
// cloud during rolling deploys and parsed with the exact guards above.
|
|
1274
|
+
| HostLogsStartFrame
|
|
1275
|
+
| HostLogsStopFrame
|
|
1276
|
+
| EngineLoginStartFrame
|
|
1277
|
+
| EngineLoginInputFrame
|
|
1278
|
+
| EngineLoginCallbackFrame
|
|
1279
|
+
| EngineLoginStopFrame
|
|
1280
|
+
| HostConfigGetFrame
|
|
1281
|
+
| HostConfigSetFrame
|
|
1282
|
+
| {
|
|
1283
|
+
kind: "host.inventory.request";
|
|
1284
|
+
scanId: string;
|
|
1285
|
+
/** Null starts a new immutable snapshot. Otherwise this is the previous
|
|
1286
|
+
* page's opaque/safe task-id cursor. */
|
|
1287
|
+
cursor: string | null;
|
|
1288
|
+
limit: number;
|
|
1289
|
+
}
|
|
612
1290
|
// ADR-103: sent once right after auth to a host that supplied a bootId.
|
|
613
1291
|
// `afterSeq` is the cloud's replay watermark for this boot: the host
|
|
614
1292
|
// replays every outbox entry with seq > afterSeq. `null` means the cloud
|
|
@@ -650,6 +1328,16 @@ export type HostToCloud =
|
|
|
650
1328
|
// for pre-103 hosts (which also never send seq'd events).
|
|
651
1329
|
| { kind: "auth"; token: string; hostId: string; bootId?: string }
|
|
652
1330
|
| { kind: "host.capabilities"; capabilities: HostCapabilities }
|
|
1331
|
+
| {
|
|
1332
|
+
kind: "host.inventory.page";
|
|
1333
|
+
scanId: string;
|
|
1334
|
+
cursor: string | null;
|
|
1335
|
+
entries: HostTaskInventoryEntry[];
|
|
1336
|
+
nextCursor: string | null;
|
|
1337
|
+
/** False means one or more Docker label queries failed. Positive
|
|
1338
|
+
* evidence from successful sources remains valid and is still sent. */
|
|
1339
|
+
runtimeEnumerated: boolean;
|
|
1340
|
+
}
|
|
653
1341
|
| {
|
|
654
1342
|
kind: "result";
|
|
655
1343
|
commandId: string;
|
|
@@ -733,7 +1421,13 @@ export type HostToCloud =
|
|
|
733
1421
|
authorizeUrl?: string;
|
|
734
1422
|
scopes?: string[];
|
|
735
1423
|
}
|
|
736
|
-
| { kind: "mcp.ack"; opId: string; ok: false; error: string }
|
|
1424
|
+
| { kind: "mcp.ack"; opId: string; ok: false; error: string }
|
|
1425
|
+
// ADR-100 remote operation output remains outside the durable task-event
|
|
1426
|
+
// outbox: it belongs to one authenticated bridge connection and opId.
|
|
1427
|
+
| HostLogsChunkFrame
|
|
1428
|
+
| HostLogsEndFrame
|
|
1429
|
+
| EngineLoginEventFrame
|
|
1430
|
+
| HostConfigAckFrame;
|
|
737
1431
|
|
|
738
1432
|
export type HostEvent =
|
|
739
1433
|
| {
|