@runuai/host 0.9.42 → 0.9.43
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/lib/orchestrator.ts
CHANGED
|
@@ -111,7 +111,10 @@ import {
|
|
|
111
111
|
type ResolvedEngineAccount,
|
|
112
112
|
} from "./engine-accounts";
|
|
113
113
|
import { clearTaskGatewayAcl, setupMcpTaskConfig } from "./mcp-gateway";
|
|
114
|
-
import {
|
|
114
|
+
import {
|
|
115
|
+
PREVIEW_SIDECAR_TASK_LABEL,
|
|
116
|
+
stopPreviewSidecars,
|
|
117
|
+
} from "./preview-sidecar";
|
|
115
118
|
import {
|
|
116
119
|
agentClisReady,
|
|
117
120
|
standardImageCorepackVersionPath,
|
|
@@ -4110,8 +4113,12 @@ async function dockerStart(containerName: string): Promise<DockerStartResult> {
|
|
|
4110
4113
|
return "failed";
|
|
4111
4114
|
}
|
|
4112
4115
|
|
|
4116
|
+
// The fourth column is the preview-sidecar marker: a sidecar can wear the
|
|
4117
|
+
// task's compose labels, and a row that is neither app-shaped nor a known
|
|
4118
|
+
// sidecar reads as AMBIGUOUS here — which fenced maintenance on a healthy
|
|
4119
|
+
// host (live 2026-08-18).
|
|
4113
4120
|
const RUNTIME_WRITER_LIST_FORMAT =
|
|
4114
|
-
|
|
4121
|
+
`{{json .Names}}\t{{.Label "com.docker.compose.project"}}\t{{.Label "com.docker.compose.service"}}\t{{.Label "${PREVIEW_SIDECAR_TASK_LABEL}"}}`;
|
|
4115
4122
|
const RUNTIME_WRITER_INSPECT_FORMAT = [
|
|
4116
4123
|
"{{json .Id}}",
|
|
4117
4124
|
"{{json .Name}}",
|
|
@@ -4233,10 +4240,12 @@ async function listRuntimeWriterCandidates(): Promise<
|
|
|
4233
4240
|
for (const rawLine of listed.stdout.split(/\r?\n/)) {
|
|
4234
4241
|
if (!rawLine) continue;
|
|
4235
4242
|
const fields = rawLine.split("\t");
|
|
4236
|
-
if (fields.length !==
|
|
4243
|
+
if (fields.length !== 4) {
|
|
4237
4244
|
ambiguous = true;
|
|
4238
4245
|
continue;
|
|
4239
4246
|
}
|
|
4247
|
+
// A preview sidecar is a known non-writer, not an ambiguity.
|
|
4248
|
+
if (fields[3] !== "") continue;
|
|
4240
4249
|
let name: unknown;
|
|
4241
4250
|
try {
|
|
4242
4251
|
name = JSON.parse(fields[0]!);
|
package/lib/preview-sidecar.ts
CHANGED
|
@@ -21,7 +21,17 @@ import { inspectTunnelContainer } from "./tunnel-runtime";
|
|
|
21
21
|
|
|
22
22
|
/** Port the proxy listens on inside the sidecar (published to a random host port). */
|
|
23
23
|
const INNER_PORT = 9000;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Exported for every "which container is the task's APP" scan: sidecars can
|
|
26
|
+
* legitimately wear the task's compose project/service labels (teardown
|
|
27
|
+
* finds them by project), so any container carrying THIS label must be
|
|
28
|
+
* excluded before counting app containers. Live 2026-08-18: one browser
|
|
29
|
+
* sidecar with compose labels made legacy adoption count two app containers
|
|
30
|
+
* and fail the whole runtime selection closed — every task on the host read
|
|
31
|
+
* as "still starting".
|
|
32
|
+
*/
|
|
33
|
+
export const PREVIEW_SIDECAR_TASK_LABEL = "com.runuai.preview-sidecar.task";
|
|
34
|
+
const LABEL = PREVIEW_SIDECAR_TASK_LABEL;
|
|
25
35
|
const TARGET_LABEL = "com.runuai.preview-sidecar.target";
|
|
26
36
|
|
|
27
37
|
/** (taskId,name,containerPort) -> published 127.0.0.1 host port of a live sidecar. */
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../agent";
|
|
17
17
|
import { dockerCli, type DockerResult } from "../docker-exec";
|
|
18
18
|
import { taskDir } from "../env";
|
|
19
|
+
import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
|
|
19
20
|
import { assertSafeHostTaskId } from "../task-identity";
|
|
20
21
|
import {
|
|
21
22
|
assertTaskEnvironmentProcessRequest,
|
|
@@ -868,8 +869,12 @@ function taskStatusToEnvironmentStatus(
|
|
|
868
869
|
};
|
|
869
870
|
}
|
|
870
871
|
|
|
872
|
+
// The fourth column is the preview-sidecar marker: a sidecar wearing the
|
|
873
|
+
// task's compose labels must read as "not the app", not as malformed
|
|
874
|
+
// identity data — the latter turned a healthy running task's status into
|
|
875
|
+
// "unknown" (live 2026-08-18).
|
|
871
876
|
const DOCKER_STATUS_FORMAT =
|
|
872
|
-
|
|
877
|
+
`{{.Names}}\t{{.Label "com.docker.compose.project"}}\t{{.State}}\t{{.Label "${PREVIEW_SIDECAR_TASK_LABEL}"}}`;
|
|
873
878
|
|
|
874
879
|
async function inspectDockerTaskEnvironmentStatus(
|
|
875
880
|
docker: typeof dockerCli,
|
|
@@ -897,14 +902,20 @@ async function inspectDockerTaskEnvironmentStatus(
|
|
|
897
902
|
|
|
898
903
|
let state: string | undefined;
|
|
899
904
|
for (const raw of result.stdout.split(/\r?\n/)) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
905
|
+
// No trim before splitting: the app row's LAST column (the sidecar
|
|
906
|
+
// marker) is empty, and trimming would eat its tab and shift the shape.
|
|
907
|
+
if (!raw.trim()) continue;
|
|
908
|
+
const [name, project, candidateState, sidecar, ...extra] = raw.split("\t");
|
|
909
|
+
// A preview sidecar wearing the task's compose labels is not the app
|
|
910
|
+
// container; skipping it keeps a healthy task from reading as unknown.
|
|
911
|
+
if (sidecar !== undefined && sidecar !== "") continue;
|
|
903
912
|
if (
|
|
904
913
|
state !== undefined ||
|
|
905
914
|
name !== environment.containerName ||
|
|
906
915
|
project !== environment.composeProject ||
|
|
907
916
|
!candidateState ||
|
|
917
|
+
!candidateState.trim() ||
|
|
918
|
+
sidecar === undefined ||
|
|
908
919
|
extra.length > 0
|
|
909
920
|
) {
|
|
910
921
|
return {
|
|
@@ -5,6 +5,7 @@ import { eq } from "drizzle-orm";
|
|
|
5
5
|
import { dockerCli, type DockerResult } from "../docker-exec";
|
|
6
6
|
import { getDb, schema, type HostTask } from "../db";
|
|
7
7
|
import { parsePreviewPortRuntimes } from "../preview-ports";
|
|
8
|
+
import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
|
|
8
9
|
import { assertSafeHostTaskId } from "../task-identity";
|
|
9
10
|
import {
|
|
10
11
|
APPLE_TASK_ENVIRONMENT_PROVIDER,
|
|
@@ -26,7 +27,11 @@ const INSPECT_OUTPUT_LIMIT = 128 * 1024;
|
|
|
26
27
|
const INVENTORY_OUTPUT_LIMIT = 128 * 1024;
|
|
27
28
|
const LEGACY_INSPECT_FORMAT =
|
|
28
29
|
'{{json .Id}}\t{{json .Name}}\t{{json (index .Config.Labels "com.docker.compose.project")}}\t{{json (index .Config.Labels "com.docker.compose.service")}}\t{{json .State.Status}}\t{{json .Mounts}}';
|
|
29
|
-
|
|
30
|
+
// The third column is the preview-sidecar marker: a sidecar can wear the
|
|
31
|
+
// task's compose project/service labels (teardown finds it by project), so
|
|
32
|
+
// the app-container census must see and skip it — counting one as an app
|
|
33
|
+
// container failed the WHOLE runtime adoption closed (live 2026-08-18).
|
|
34
|
+
const LEGACY_INVENTORY_FORMAT = `{{.ID}}\t{{.Names}}\t{{.Label "${PREVIEW_SIDECAR_TASK_LABEL}"}}`;
|
|
30
35
|
|
|
31
36
|
type LegacyTaskRow = Pick<
|
|
32
37
|
HostTask,
|
|
@@ -317,27 +322,38 @@ async function proveNoConflictingAppContainer(
|
|
|
317
322
|
) {
|
|
318
323
|
throw new Error(`task ${candidate.row.taskId} app inventory failed`);
|
|
319
324
|
}
|
|
325
|
+
if (result.stdout !== "" && !result.stdout.endsWith("\n")) {
|
|
326
|
+
throw new Error(`task ${candidate.row.taskId} app inventory failed`);
|
|
327
|
+
}
|
|
328
|
+
const appRows: string[][] = [];
|
|
329
|
+
for (const line of result.stdout === ""
|
|
330
|
+
? []
|
|
331
|
+
: result.stdout.slice(0, -1).split("\n")) {
|
|
332
|
+
const fields = line.split("\t");
|
|
333
|
+
if (fields.length !== 3) {
|
|
334
|
+
throw new Error(`task ${candidate.row.taskId} app inventory failed`);
|
|
335
|
+
}
|
|
336
|
+
// A preview sidecar wearing the task's compose labels is not an app
|
|
337
|
+
// container; counting one here failed the whole runtime adoption closed
|
|
338
|
+
// (live 2026-08-18, six labeled browser sidecars on the Studio).
|
|
339
|
+
if (fields[2] !== "") continue;
|
|
340
|
+
appRows.push(fields);
|
|
341
|
+
}
|
|
320
342
|
if (expected === null) {
|
|
321
343
|
// The exact container is proven absent; adoption is sound only if no
|
|
322
|
-
// same-project replacement exists either
|
|
323
|
-
|
|
324
|
-
if (result.stdout !== "") {
|
|
344
|
+
// same-project replacement exists either.
|
|
345
|
+
if (appRows.length !== 0) {
|
|
325
346
|
throw new Error(
|
|
326
347
|
`task ${candidate.row.taskId} has conflicting app containers`,
|
|
327
348
|
);
|
|
328
349
|
}
|
|
329
350
|
return;
|
|
330
351
|
}
|
|
331
|
-
if (
|
|
332
|
-
throw new Error(`task ${candidate.row.taskId} app inventory failed`);
|
|
333
|
-
}
|
|
334
|
-
const lines = result.stdout.slice(0, -1).split("\n");
|
|
335
|
-
if (lines.length !== 1) {
|
|
352
|
+
if (appRows.length !== 1) {
|
|
336
353
|
throw new Error(`task ${candidate.row.taskId} has conflicting app containers`);
|
|
337
354
|
}
|
|
338
|
-
const fields =
|
|
355
|
+
const fields = appRows[0]!;
|
|
339
356
|
if (
|
|
340
|
-
fields.length !== 2 ||
|
|
341
357
|
fields[0] !== expected.containerId ||
|
|
342
358
|
fields[1] !== expected.containerName
|
|
343
359
|
) {
|
package/package.json
CHANGED