@runuai/host 0.9.13 → 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.
Files changed (97) hide show
  1. package/README.md +22 -5
  2. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  3. package/db/migrations/0015_host_settings.sql +9 -0
  4. package/db/migrations/0016_task_environment.sql +2 -0
  5. package/db/migrations/meta/_journal.json +21 -0
  6. package/db/schema.ts +80 -30
  7. package/images/standard/Dockerfile +36 -10
  8. package/images/standard/README.md +63 -18
  9. package/images/standard/container/corepack-version +1 -0
  10. package/images/standard/container/uai-init +308 -38
  11. package/images/standard/container/uai-materialize-runtimes +1527 -0
  12. package/lib/agent-cli.ts +69 -4
  13. package/lib/agent.ts +46 -7
  14. package/lib/agents/claude.ts +13 -8
  15. package/lib/agents/codex.ts +11 -6
  16. package/lib/agents/cursor.ts +39 -29
  17. package/lib/agents/durable-proc.ts +20 -27
  18. package/lib/agents/factory.ts +9 -25
  19. package/lib/agents/grok.ts +43 -30
  20. package/lib/agents/kimi.ts +44 -29
  21. package/lib/agents/opencode.ts +43 -31
  22. package/lib/agents/proc.ts +149 -114
  23. package/lib/agents/transport.ts +62 -50
  24. package/lib/agents/types.ts +6 -4
  25. package/lib/apple-runtime-recycle.ts +236 -0
  26. package/lib/apple-uninstall-teardown.ts +224 -0
  27. package/lib/browser-testing.ts +233 -93
  28. package/lib/codex-auth.ts +40 -6
  29. package/lib/command-db.ts +20 -0
  30. package/lib/container-runtime.ts +1338 -0
  31. package/lib/db.ts +1 -0
  32. package/lib/docker-exec.ts +87 -5
  33. package/lib/engine-accounts.ts +68 -5
  34. package/lib/engine-login.ts +1952 -0
  35. package/lib/enrollment-state.ts +251 -0
  36. package/lib/env-file.ts +155 -0
  37. package/lib/env.ts +4 -0
  38. package/lib/git-diff.ts +98 -32
  39. package/lib/git-identity.ts +199 -87
  40. package/lib/github-tokens.ts +202 -91
  41. package/lib/host-cloud-url.ts +62 -0
  42. package/lib/host-config.ts +279 -0
  43. package/lib/host-logs.ts +962 -0
  44. package/lib/keyed-promise-tail.ts +23 -0
  45. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  46. package/lib/managed-activation-watcher.ts +72 -0
  47. package/lib/managed-install-owner-watcher.ts +55 -0
  48. package/lib/managed-operation-drain.ts +49 -0
  49. package/lib/managed-runtime.ts +3644 -0
  50. package/lib/managed-update-scheduler.ts +125 -0
  51. package/lib/mcp-gateway.ts +450 -23
  52. package/lib/orchestrator.ts +3070 -223
  53. package/lib/preview-sidecar.ts +57 -13
  54. package/lib/release-manifest.ts +708 -0
  55. package/lib/release-trust.ts +28 -0
  56. package/lib/runtime-activation-tail.ts +232 -0
  57. package/lib/runtime-archive.ts +1086 -0
  58. package/lib/runtime-authority.ts +79 -0
  59. package/lib/runtime-guard.ts +36 -0
  60. package/lib/runtime-provider-state.ts +169 -0
  61. package/lib/runtime-state.ts +232 -12
  62. package/lib/skills.ts +24 -3
  63. package/lib/ssh.ts +18 -0
  64. package/lib/standard-image.ts +1104 -141
  65. package/lib/stopped-task-status-queue.ts +44 -0
  66. package/lib/task-container-cli.ts +269 -0
  67. package/lib/task-diff.ts +66 -46
  68. package/lib/task-environment/apple-container.ts +757 -0
  69. package/lib/task-environment/docker.ts +945 -0
  70. package/lib/task-environment/index.ts +364 -0
  71. package/lib/task-environment/legacy-adoption.ts +443 -0
  72. package/lib/task-environment/registry.ts +58 -0
  73. package/lib/task-environment/types.ts +408 -0
  74. package/lib/task-identity.ts +19 -0
  75. package/lib/task-inventory.ts +585 -0
  76. package/lib/tunnel-registry.ts +135 -19
  77. package/lib/tunnel-runtime.ts +235 -0
  78. package/package.json +1 -1
  79. package/scripts/agent/_common.sh +123 -3
  80. package/scripts/agent/task-down.sh +146 -38
  81. package/scripts/agent/task-status.sh +19 -3
  82. package/scripts/agent/task-up.sh +1463 -109
  83. package/scripts/install/darwin.ts +848 -50
  84. package/scripts/install/linux.ts +838 -35
  85. package/scripts/install/types.ts +43 -0
  86. package/scripts/install/util.ts +215 -8
  87. package/scripts/install/win.ts +12 -0
  88. package/src/apple-tunnel-route.ts +104 -0
  89. package/src/cli.ts +1464 -72
  90. package/src/event-outbox.ts +83 -4
  91. package/src/index.ts +871 -50
  92. package/src/main.ts +1398 -255
  93. package/src/paths.ts +17 -1
  94. package/src/protocol.ts +695 -1
  95. package/src/runtime-bootstrap.ts +165 -0
  96. package/src/ui/server.ts +46 -10
  97. package/src/ui/types.ts +37 -0
@@ -0,0 +1,165 @@
1
+ /** ADR-106 boot wiring kept side-effect free and directly testable. */
2
+
3
+ import {
4
+ appleContainerRuntimeBinding,
5
+ containerRuntimeMachineIdentity,
6
+ rejectContainerRuntimeSelection,
7
+ reprobeContainerRuntimeMachineIdentity,
8
+ setAppleContainerRuntime,
9
+ setAppleContainerSelectionGuard,
10
+ type AppleRuntimeBinding,
11
+ } from "../lib/container-runtime";
12
+ import {
13
+ resolveBundledRuntimeFromEnvironment,
14
+ type ResolvedBundledRuntime,
15
+ } from "../lib/managed-runtime";
16
+ import {
17
+ adoptLegacyTaskEnvironments,
18
+ hostTaskEnvironmentsAllowMachineSelection,
19
+ hostTaskEnvironmentsMatchMachine,
20
+ setAppleContainerBinding,
21
+ type HostMachineIdentity,
22
+ type LegacyTaskEnvironmentAdoptionVerdict,
23
+ type TaskEnvironmentBackendVerdict,
24
+ } from "../lib/task-environment";
25
+
26
+ export interface RuntimeBootstrapDeps {
27
+ readonly resolveRuntime?: () => Promise<ResolvedBundledRuntime | null>;
28
+ readonly registerRuntime?: (binding: AppleRuntimeBinding | null) => void;
29
+ readonly registerSelectionGuard?: (
30
+ guard: ((endpoint: string) => boolean | Promise<boolean>) | null,
31
+ ) => void;
32
+ readonly allowMachineSelection?: (machine: {
33
+ backend: "apple-container";
34
+ endpoint: string;
35
+ }) => TaskEnvironmentBackendVerdict;
36
+ }
37
+
38
+ /**
39
+ * Register the bundled Apple container toolchain from the active signed
40
+ * generation, if one exists (ADR-106). Registration is fail-closed: seams are
41
+ * cleared first so a thrown validation cannot leave a stale binding, and the
42
+ * selection guard keeps `auto` off the bundled runtime while durable task
43
+ * rows still belong to another backend. An unmanaged checkout, Linux archive,
44
+ * or rolling v1 generation registers nothing; there is deliberately no PATH
45
+ * or inferred-layout fallback.
46
+ */
47
+ export async function configureBundledContainerRuntime(
48
+ deps: RuntimeBootstrapDeps = {},
49
+ ): Promise<ResolvedBundledRuntime | null> {
50
+ const registerRuntime = deps.registerRuntime ?? setAppleContainerRuntime;
51
+ const registerGuard =
52
+ deps.registerSelectionGuard ?? setAppleContainerSelectionGuard;
53
+ registerRuntime(null);
54
+ registerGuard(null);
55
+
56
+ const bundled = await (
57
+ deps.resolveRuntime ?? resolveBundledRuntimeFromEnvironment
58
+ )();
59
+ if (bundled === null) return null;
60
+
61
+ const allowSelection =
62
+ deps.allowMachineSelection ?? hostTaskEnvironmentsAllowMachineSelection;
63
+ registerGuard(
64
+ (endpoint) =>
65
+ allowSelection({ backend: "apple-container", endpoint }).ok,
66
+ );
67
+ registerRuntime({
68
+ containerCliPath: bundled.containerCliPath,
69
+ kernelPath: bundled.kernelPath,
70
+ });
71
+ return bundled;
72
+ }
73
+
74
+ export interface RuntimeCompatibilityDeps {
75
+ readonly machineIdentity?: () => HostMachineIdentity | null;
76
+ readonly matchMachine?: (
77
+ machine: HostMachineIdentity,
78
+ ) => TaskEnvironmentBackendVerdict;
79
+ readonly adoptLegacy?: (
80
+ machine: HostMachineIdentity,
81
+ ) => Promise<LegacyTaskEnvironmentAdoptionVerdict>;
82
+ readonly reprobeRuntime?: () => Promise<HostMachineIdentity | null>;
83
+ readonly rejectSelection?: (message: string, action?: string) => void;
84
+ }
85
+
86
+ /**
87
+ * Final database-backed fence before maintenance/recovery touches a selected
88
+ * daemon. A missing exact endpoint and every malformed/newer/mismatched task
89
+ * locator are quarantines, never retry hints against the current machine.
90
+ */
91
+ export async function selectedRuntimeMatchesPersistedTaskEnvironments(
92
+ deps: RuntimeCompatibilityDeps = {},
93
+ ): Promise<boolean> {
94
+ const reject = deps.rejectSelection ?? rejectContainerRuntimeSelection;
95
+ const machine = (
96
+ deps.machineIdentity ?? containerRuntimeMachineIdentity
97
+ )();
98
+ if (machine === null) {
99
+ reject(
100
+ "The selected container runtime has no durable local machine identity.",
101
+ );
102
+ return false;
103
+ }
104
+ const reprobe =
105
+ deps.reprobeRuntime ?? reprobeContainerRuntimeMachineIdentity;
106
+ const liveMachine = await reprobe();
107
+ if (
108
+ liveMachine === null ||
109
+ liveMachine.backend !== machine.backend ||
110
+ liveMachine.endpoint !== machine.endpoint ||
111
+ liveMachine.engineId !== machine.engineId
112
+ ) {
113
+ reject(
114
+ "The selected container runtime daemon identity changed during validation.",
115
+ );
116
+ return false;
117
+ }
118
+ // Legacy compose rows can only ever be adopted by a Docker daemon; any
119
+ // other active backend makes the candidate check inside adoption fail
120
+ // closed with its repair guidance, so the machine is narrowed lazily.
121
+ // Bind (or unbind) the Apple provider to the proven identity before any
122
+ // maintenance can reconstruct a task environment. The binding carries the
123
+ // exact machine the fence just re-probed, so provider reconstruction and
124
+ // this admission decision can never disagree about identity.
125
+ if (deps.machineIdentity === undefined) {
126
+ const binding = appleContainerRuntimeBinding();
127
+ if (machine.backend === "apple-container" && binding !== null) {
128
+ const appleMachine = {
129
+ backend: "apple-container" as const,
130
+ endpoint: machine.endpoint,
131
+ engineId: machine.engineId,
132
+ };
133
+ setAppleContainerBinding({
134
+ containerCliPath: () => binding.containerCliPath,
135
+ machineIdentity: () => appleMachine,
136
+ });
137
+ } else {
138
+ setAppleContainerBinding(null);
139
+ }
140
+ }
141
+
142
+ const adoption = await (
143
+ deps.adoptLegacy ??
144
+ ((selected: HostMachineIdentity) =>
145
+ adoptLegacyTaskEnvironments(selected, {
146
+ reprobeMachine: reprobe,
147
+ }))
148
+ )(machine);
149
+ if (!adoption.ok) {
150
+ reject(
151
+ "Uai Host could not safely adopt persisted legacy task resources" +
152
+ (adoption.detail ? `: ${adoption.detail}` : "."),
153
+ );
154
+ return false;
155
+ }
156
+ const verdict = (
157
+ deps.matchMachine ?? hostTaskEnvironmentsMatchMachine
158
+ )(machine);
159
+ if (verdict.ok) return true;
160
+ reject(
161
+ "The selected container runtime does not match persisted task environments" +
162
+ (verdict.detail ? `: ${verdict.detail}` : "."),
163
+ );
164
+ return false;
165
+ }
package/src/ui/server.ts CHANGED
@@ -29,6 +29,7 @@ import { getOrchestrator } from "../../lib/orchestrator";
29
29
  import { parsePreviewPortRuntimes } from "../../lib/preview-ports";
30
30
  import { getCloudState } from "../../lib/cloud-state";
31
31
  import { dockerCli } from "../../lib/docker-exec";
32
+ import type { ContainerRuntimeState } from "../../lib/container-runtime";
32
33
  import {
33
34
  connectEngine,
34
35
  disconnectEngine,
@@ -47,6 +48,7 @@ import {
47
48
  import { ensureStandardImage } from "../../lib/standard-image";
48
49
  import {
49
50
  CloudResponse,
51
+ ContainerRuntimeResponse,
50
52
  EngineOpResponse,
51
53
  EnginesResponse,
52
54
  EventsResponse,
@@ -58,6 +60,7 @@ import {
58
60
 
59
61
  const HOST = "127.0.0.1";
60
62
  const MAX_PORT_ATTEMPTS = 10;
63
+ export const RUNTIME_RECHECK_CONTROL_HEADER = "runtime-recheck-v1";
61
64
 
62
65
  export interface UiServerOptions {
63
66
  /** Host runtime DB (read-only here). */
@@ -93,6 +96,12 @@ export interface UiServerOptions {
93
96
  * endpoint never spawns a login shell (which is bounded, but real, I/O).
94
97
  */
95
98
  engineSeams?: Partial<EngineSeams>;
99
+ /** ADR-101 local control/read surface. Optional for embedding/tests. */
100
+ runtimeState?: () => ContainerRuntimeState;
101
+ recheckRuntime?: () => Promise<ContainerRuntimeState>;
102
+ /** False while the selected backend is unavailable or still recovering.
103
+ * Credential edits remain usable, but must not kick Docker maintenance. */
104
+ containersOperational?: () => boolean;
96
105
  }
97
106
 
98
107
  export interface UiServerHandle {
@@ -179,6 +188,26 @@ async function handle(
179
188
  return await handleEngineAccountRemove(req, res, opts);
180
189
  case "/api/tasks/stop":
181
190
  return await handleTaskStop(req, res, opts);
191
+ case "/api/runtime/recheck": {
192
+ // Loopback is not a browser CSRF boundary. Requiring a non-simple
193
+ // header is deliberate even though its value is not a secret:
194
+ // hostile pages cannot attach it without a CORS preflight, and this
195
+ // server grants no CORS permission. The local CLI is the caller.
196
+ if (
197
+ req.headers["x-uai-local-control"] !==
198
+ RUNTIME_RECHECK_CONTROL_HEADER
199
+ ) {
200
+ return sendError(res, 403, "local control header required");
201
+ }
202
+ if (!opts.recheckRuntime) {
203
+ return sendError(res, 501, "runtime recheck is unavailable");
204
+ }
205
+ return sendJson(
206
+ res,
207
+ ContainerRuntimeResponse,
208
+ await opts.recheckRuntime(),
209
+ );
210
+ }
182
211
  }
183
212
  return sendError(res, 404, `no such endpoint: ${path}`);
184
213
  }
@@ -196,6 +225,7 @@ async function handle(
196
225
  hostName: hostname(),
197
226
  hostId: opts.hostId,
198
227
  agentMode: agentMode(),
228
+ containerRuntime: opts.runtimeState?.(),
199
229
  });
200
230
  case "/api/cloud":
201
231
  return sendJson(res, CloudResponse, getCloudState());
@@ -320,14 +350,16 @@ async function handleEngineConnect(
320
350
  // Re-advertise so the cloud picker offers the engine; rebuild the image so
321
351
  // an optional engine's CLI (kimi/grok/cursor) is installed. Best-effort.
322
352
  opts.readvertise?.();
323
- void ensureStandardImage();
324
- // A re-login only reaches a RUNNING task once its agents re-spawn — the
325
- // credential is injected at spawn (Claude's token via `docker exec -e`).
326
- // Refresh running agents of this kind so a reconnect self-heals tasks stuck
327
- // on a revoked/expired token, instead of needing a manual Stop/Resume.
328
- void getOrchestrator()
329
- .refreshEngineAccountAgents(kind)
330
- .catch(() => {});
353
+ if (opts.containersOperational?.() !== false) {
354
+ void ensureStandardImage();
355
+ // A re-login only reaches a RUNNING task once its agents re-spawn — the
356
+ // credential is injected at spawn (Claude's token via `docker exec -e`).
357
+ // Refresh running agents of this kind so a reconnect self-heals tasks
358
+ // stuck on a revoked/expired token, instead of needing Stop/Resume.
359
+ void getOrchestrator()
360
+ .refreshEngineAccountAgents(kind)
361
+ .catch(() => {});
362
+ }
331
363
  }
332
364
  emit({ done: true, ok: result.ok, message: result.message });
333
365
  res.end();
@@ -370,7 +402,9 @@ async function handleEngineAccountAdd(
370
402
  const result = addEngineAccount(kind, label, { apiKey, token });
371
403
  if (result.ok) {
372
404
  opts.readvertise?.();
373
- void ensureStandardImage();
405
+ if (opts.containersOperational?.() !== false) {
406
+ void ensureStandardImage();
407
+ }
374
408
  }
375
409
  return sendJson(res, EngineOpResponse, {
376
410
  ok: result.ok,
@@ -424,7 +458,9 @@ async function handleEngineAccountConnect(
424
458
  }
425
459
  if (result.ok) {
426
460
  opts.readvertise?.();
427
- void ensureStandardImage();
461
+ if (opts.containersOperational?.() !== false) {
462
+ void ensureStandardImage();
463
+ }
428
464
  }
429
465
  emit({ done: true, ok: result.ok, message: result.message });
430
466
  res.end();
package/src/ui/types.ts CHANGED
@@ -8,6 +8,42 @@
8
8
 
9
9
  import { z } from "zod";
10
10
 
11
+ const RuntimePreference = z.enum(["auto", "docker", "apple-container"]);
12
+ export const ContainerRuntimeResponse = z.union([
13
+ z
14
+ .object({
15
+ status: z.literal("checking"),
16
+ preference: RuntimePreference,
17
+ detail: z.string().trim().min(1).max(300).optional(),
18
+ })
19
+ .strict(),
20
+ z.union([
21
+ z
22
+ .object({
23
+ status: z.literal("ready"),
24
+ preference: z.enum(["auto", "docker"]),
25
+ provider: z.literal("docker"),
26
+ })
27
+ .strict(),
28
+ z
29
+ .object({
30
+ status: z.literal("ready"),
31
+ preference: z.enum(["auto", "apple-container"]),
32
+ provider: z.literal("apple-container"),
33
+ })
34
+ .strict(),
35
+ ]),
36
+ z
37
+ .object({
38
+ status: z.literal("no-runtime"),
39
+ preference: RuntimePreference,
40
+ message: z.string().trim().min(1).max(1_000),
41
+ action: z.string().trim().min(1).max(200),
42
+ })
43
+ .strict(),
44
+ ]);
45
+ export type ContainerRuntimeResponse = z.infer<typeof ContainerRuntimeResponse>;
46
+
11
47
  // GET /api/status — process + config facts.
12
48
  export const StatusResponse = z.object({
13
49
  pid: z.number().int(),
@@ -22,6 +58,7 @@ export const StatusResponse = z.object({
22
58
  // such field — `npm i -g` swaps the CLI before `uai-host restart` swaps the
23
59
  // service, so that window is normal, not exotic.
24
60
  agentMode: z.enum(["real", "mock"]).optional(),
61
+ containerRuntime: ContainerRuntimeResponse.optional(),
25
62
  });
26
63
  export type StatusResponse = z.infer<typeof StatusResponse>;
27
64