@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/lib/browser-testing.ts
CHANGED
|
@@ -43,8 +43,18 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { createHash } from "node:crypto";
|
|
45
45
|
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
taskAppContainerName,
|
|
48
|
+
taskContainerBackend,
|
|
49
|
+
taskContainerCli,
|
|
50
|
+
} from "./task-container-cli";
|
|
47
51
|
import { MCP_CONFIG_LOCK_PATH } from "./mcp-config-lock";
|
|
52
|
+
import {
|
|
53
|
+
RUNTIME_AUTHORITY_ENV,
|
|
54
|
+
runtimeAuthorityDockerExecArgs,
|
|
55
|
+
} from "./runtime-authority";
|
|
56
|
+
import { STANDARD_DEFAULT_NODE_VERSION } from "./standard-image";
|
|
57
|
+
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
48
58
|
|
|
49
59
|
const SERVER_DISPLAY = ":99";
|
|
50
60
|
/**
|
|
@@ -935,17 +945,23 @@ const VIEWER_STEPS: string[] = [
|
|
|
935
945
|
// Xvfb — one-shot; the X lockfile is the truth (also set when an agent
|
|
936
946
|
// started the display itself). If it dies the lock clears and the next
|
|
937
947
|
// session ensure relaunches it.
|
|
938
|
-
`
|
|
948
|
+
`[ -x /usr/bin/Xvfb ] || exit 0; [ -e ${X_LOCK} ] || /usr/bin/nohup /usr/bin/${XVFB_CMD} >>/tmp/uai-xvfb.log 2>&1 &`,
|
|
939
949
|
// x11vnc under a restart loop (it exits whenever the X server isn't up
|
|
940
950
|
// yet). Pidfile-guarded.
|
|
941
|
-
`
|
|
951
|
+
`[ -x /usr/bin/x11vnc ] || exit 0; [ -f /tmp/uai-x11vnc.pid ] && /usr/bin/kill -0 "$(/usr/bin/cat /tmp/uai-x11vnc.pid)" 2>/dev/null && exit 0; /usr/bin/nohup /bin/sh -c 'echo $$ > /tmp/uai-x11vnc.pid; while true; do /usr/bin/x11vnc -display ${SERVER_DISPLAY} -forever -shared -nopw -quiet >>/tmp/uai-x11vnc.log 2>&1; /bin/sleep 2; done' >/dev/null 2>&1 &`,
|
|
942
952
|
// websockify/noVNC under the same pattern.
|
|
943
|
-
`
|
|
953
|
+
`[ -x /usr/bin/websockify ] || exit 0; [ -f /tmp/uai-novnc.pid ] && /usr/bin/kill -0 "$(/usr/bin/cat /tmp/uai-novnc.pid)" 2>/dev/null && exit 0; /usr/bin/nohup /bin/sh -c 'echo $$ > /tmp/uai-novnc.pid; while true; do /usr/bin/websockify --web=/usr/share/novnc 0.0.0.0:6080 localhost:5900 >>/tmp/uai-websockify.log 2>&1; /bin/sleep 2; done' >/dev/null 2>&1 &`,
|
|
944
954
|
];
|
|
945
955
|
|
|
946
|
-
const
|
|
956
|
+
const DEFAULT_NODE_BIN =
|
|
957
|
+
`/opt/asdf-data/installs/nodejs/${STANDARD_DEFAULT_NODE_VERSION}/bin`;
|
|
958
|
+
const ROOT_RUNTIME_PATH =
|
|
959
|
+
`${DEFAULT_NODE_BIN}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`;
|
|
960
|
+
const APT_DEPS_CMD =
|
|
961
|
+
`PATH=${ROOT_RUNTIME_PATH} ${DEFAULT_NODE_BIN}/npx ` +
|
|
962
|
+
"-y playwright@latest install-deps chromium";
|
|
947
963
|
const APT_VIEWER_CMD =
|
|
948
|
-
"apt-get install -y --no-install-recommends xvfb x11vnc novnc websockify";
|
|
964
|
+
"/usr/bin/apt-get install -y --no-install-recommends xvfb x11vnc novnc websockify";
|
|
949
965
|
|
|
950
966
|
function aptGuardPaths(): { done: string; lock: string } {
|
|
951
967
|
const digest = createHash("sha256")
|
|
@@ -978,10 +994,19 @@ export async function setupBrowserTesting(
|
|
|
978
994
|
containerName: string,
|
|
979
995
|
hasCodex: boolean,
|
|
980
996
|
codexHomes: readonly string[] = hasCodex ? [DEFAULT_CODEX_HOME] : [],
|
|
997
|
+
environment?: Pick<TaskEnvironmentHandle, "exec" | "launchDetachedSession">,
|
|
981
998
|
): Promise<BrowserSetup> {
|
|
982
999
|
let ready = false;
|
|
983
1000
|
let configured = false;
|
|
984
1001
|
let changed = false;
|
|
1002
|
+
// The no-handle fallbacks below exec by container name. The passed name is
|
|
1003
|
+
// the Docker Compose replica shape; on the Apple backend the container has
|
|
1004
|
+
// the provider's fixed name and the `docker` binary does not exist (live
|
|
1005
|
+
// 2026-08-18: every fallback died with spawn docker ENOENT, which is why
|
|
1006
|
+
// sessions never saw their browser MCP tools). Normalize both here.
|
|
1007
|
+
const container = taskContainerBackend().apple
|
|
1008
|
+
? taskAppContainerName(taskId)
|
|
1009
|
+
: containerName;
|
|
985
1010
|
try {
|
|
986
1011
|
const selectedCodexHomes = hasCodex
|
|
987
1012
|
? [...new Set(codexHomes.length > 0 ? codexHomes : [DEFAULT_CODEX_HOME])]
|
|
@@ -1001,9 +1026,27 @@ export async function setupBrowserTesting(
|
|
|
1001
1026
|
// ONE exec per daemon: the single-line nohup shape is the only one that
|
|
1002
1027
|
// reliably survives `docker exec -d`.
|
|
1003
1028
|
for (const step of VIEWER_STEPS) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1029
|
+
if (environment) {
|
|
1030
|
+
await environment.launchDetachedSession({
|
|
1031
|
+
argv: ["sh", "-c", step],
|
|
1032
|
+
env: RUNTIME_AUTHORITY_ENV,
|
|
1033
|
+
maxOutputBytes: 64 * 1024,
|
|
1034
|
+
launchTimeoutMs: 10_000,
|
|
1035
|
+
});
|
|
1036
|
+
} else {
|
|
1037
|
+
await taskContainerCli(
|
|
1038
|
+
[
|
|
1039
|
+
"exec",
|
|
1040
|
+
"-d",
|
|
1041
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
1042
|
+
container,
|
|
1043
|
+
"sh",
|
|
1044
|
+
"-c",
|
|
1045
|
+
step,
|
|
1046
|
+
],
|
|
1047
|
+
{ timeoutMs: 10_000 },
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1007
1050
|
}
|
|
1008
1051
|
|
|
1009
1052
|
// Browser readiness — AWAITED, and deliberately OUTSIDE the marker guard.
|
|
@@ -1019,23 +1062,68 @@ export async function setupBrowserTesting(
|
|
|
1019
1062
|
// from outside: the volume is host-wide, so another container's Playwright
|
|
1020
1063
|
// can evict our build mid-task. Re-running it is how that heals — and it
|
|
1021
1064
|
// costs ~0.5s once the build is present.
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1065
|
+
const installed = environment
|
|
1066
|
+
? await (async () => {
|
|
1067
|
+
await environment.exec({
|
|
1068
|
+
argv: ["/usr/bin/chown", "node:node", "/opt/pw-browsers"],
|
|
1069
|
+
user: "root",
|
|
1070
|
+
env: RUNTIME_AUTHORITY_ENV,
|
|
1071
|
+
timeoutMs: 5_000,
|
|
1072
|
+
maxOutputBytes: 64 * 1024,
|
|
1073
|
+
});
|
|
1074
|
+
return environment.exec({
|
|
1075
|
+
argv: ["sh", "-lc", PREWARM_CMD],
|
|
1076
|
+
cwd: "/workspace",
|
|
1077
|
+
env: RUNTIME_AUTHORITY_ENV,
|
|
1078
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
1079
|
+
maxOutputBytes: 256 * 1024,
|
|
1080
|
+
});
|
|
1081
|
+
})()
|
|
1082
|
+
: await taskContainerCli(
|
|
1083
|
+
[
|
|
1084
|
+
"exec",
|
|
1085
|
+
"-u",
|
|
1086
|
+
"root",
|
|
1087
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
1088
|
+
container,
|
|
1089
|
+
"/usr/bin/chown",
|
|
1090
|
+
"node:node",
|
|
1091
|
+
"/opt/pw-browsers",
|
|
1092
|
+
],
|
|
1093
|
+
{ timeoutMs: 5_000 },
|
|
1094
|
+
).then(async (chown) => {
|
|
1095
|
+
if (chown.status !== 0) return chown;
|
|
1096
|
+
return taskContainerCli(
|
|
1097
|
+
[
|
|
1098
|
+
"exec",
|
|
1099
|
+
"-w",
|
|
1100
|
+
"/workspace",
|
|
1101
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
1102
|
+
container,
|
|
1103
|
+
"sh",
|
|
1104
|
+
"-lc",
|
|
1105
|
+
PREWARM_CMD,
|
|
1106
|
+
],
|
|
1107
|
+
{ timeoutMs: INSTALL_TIMEOUT_MS },
|
|
1108
|
+
);
|
|
1109
|
+
});
|
|
1110
|
+
const installedStatus =
|
|
1111
|
+
"exitCode" in installed ? installed.exitCode : installed.status;
|
|
1112
|
+
const installedStderr =
|
|
1113
|
+
"stderr" in installed && installed.stderr instanceof Uint8Array
|
|
1114
|
+
? Buffer.from(installed.stderr).toString("utf8")
|
|
1115
|
+
: typeof installed.stderr === "string"
|
|
1116
|
+
? installed.stderr
|
|
1117
|
+
: "";
|
|
1118
|
+
ready = installedStatus === 0;
|
|
1031
1119
|
if (!ready) {
|
|
1032
1120
|
// Agents still spawn — a task without a browser beats no task — but the
|
|
1033
1121
|
// caller retries on this, and it is logged loudly, because the symptom
|
|
1034
1122
|
// downstream (a browser that won't launch) reads as anything but an
|
|
1035
1123
|
// install that quietly failed up here.
|
|
1036
1124
|
console.warn(
|
|
1037
|
-
`[browser] task ${taskId}: browser install failed (status ${
|
|
1038
|
-
`see ${INSTALL_LOG} in the container. ${
|
|
1125
|
+
`[browser] task ${taskId}: browser install failed (status ${installedStatus}); ` +
|
|
1126
|
+
`see ${INSTALL_LOG} in the container. ${installedStderr.slice(0, 200)}`,
|
|
1039
1127
|
);
|
|
1040
1128
|
}
|
|
1041
1129
|
|
|
@@ -1048,55 +1136,80 @@ export async function setupBrowserTesting(
|
|
|
1048
1136
|
// survives those copies, so a gated setup could skip the rewrite and
|
|
1049
1137
|
// report ready while Codex had no browser at all. The migration is cheap
|
|
1050
1138
|
// and byte-idempotent, so the honest fix is to re-assert.
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1139
|
+
const writeTimeoutMs =
|
|
1140
|
+
(10 + Math.max(45, 15 + selectedCodexHomes.length * 12) + 5 + 10) *
|
|
1141
|
+
1_000;
|
|
1142
|
+
const writeEnv = {
|
|
1143
|
+
...RUNTIME_AUTHORITY_ENV,
|
|
1144
|
+
UAI_BROWSER_DEF: JSON.stringify(SERVER_DEF),
|
|
1145
|
+
UAI_MCP_PATH: MCP_CONFIG_PATH,
|
|
1146
|
+
UAI_CLAUDE_SETTINGS_PATH: CLAUDE_SETTINGS_PATH,
|
|
1147
|
+
UAI_CLAUDE_SETTINGS_DEF: CLAUDE_SETTINGS_JSON,
|
|
1148
|
+
...(selectedCodexHomes.length > 0
|
|
1149
|
+
? { UAI_CODEX_HOMES: JSON.stringify(selectedCodexHomes) }
|
|
1150
|
+
: {}),
|
|
1151
|
+
};
|
|
1152
|
+
const writeArgv = [
|
|
1153
|
+
"flock",
|
|
1154
|
+
"-w",
|
|
1155
|
+
"10",
|
|
1156
|
+
MCP_CONFIG_LOCK_PATH,
|
|
1157
|
+
"timeout",
|
|
1158
|
+
"--kill-after=5",
|
|
1159
|
+
String(Math.max(45, 15 + selectedCodexHomes.length * 12)),
|
|
1160
|
+
"node",
|
|
1161
|
+
"-e",
|
|
1162
|
+
MIGRATE_JS,
|
|
1163
|
+
] as [string, ...string[]];
|
|
1164
|
+
const wrote = environment
|
|
1165
|
+
? await environment.exec({
|
|
1166
|
+
argv: writeArgv,
|
|
1167
|
+
cwd: "/workspace",
|
|
1168
|
+
env: writeEnv,
|
|
1169
|
+
timeoutMs: writeTimeoutMs,
|
|
1170
|
+
maxOutputBytes: 256 * 1024,
|
|
1171
|
+
})
|
|
1172
|
+
: await taskContainerCli(
|
|
1173
|
+
[
|
|
1174
|
+
"exec",
|
|
1175
|
+
"-w",
|
|
1176
|
+
"/workspace",
|
|
1177
|
+
"-e",
|
|
1178
|
+
`UAI_BROWSER_DEF=${JSON.stringify(SERVER_DEF)}`,
|
|
1179
|
+
"-e",
|
|
1180
|
+
`UAI_MCP_PATH=${MCP_CONFIG_PATH}`,
|
|
1181
|
+
"-e",
|
|
1182
|
+
`UAI_CLAUDE_SETTINGS_PATH=${CLAUDE_SETTINGS_PATH}`,
|
|
1183
|
+
"-e",
|
|
1184
|
+
`UAI_CLAUDE_SETTINGS_DEF=${CLAUDE_SETTINGS_JSON}`,
|
|
1185
|
+
...(selectedCodexHomes.length > 0
|
|
1186
|
+
? ["-e", `UAI_CODEX_HOMES=${JSON.stringify(selectedCodexHomes)}`]
|
|
1187
|
+
: []),
|
|
1188
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
1189
|
+
container,
|
|
1190
|
+
...writeArgv,
|
|
1191
|
+
],
|
|
1192
|
+
{ timeoutMs: writeTimeoutMs },
|
|
1193
|
+
);
|
|
1194
|
+
const wroteStatus = "exitCode" in wrote ? wrote.exitCode : wrote.status;
|
|
1195
|
+
const wroteStdout =
|
|
1196
|
+
"stdout" in wrote && wrote.stdout instanceof Uint8Array
|
|
1197
|
+
? Buffer.from(wrote.stdout).toString("utf8")
|
|
1198
|
+
: typeof wrote.stdout === "string"
|
|
1199
|
+
? wrote.stdout
|
|
1200
|
+
: "";
|
|
1201
|
+
const wroteStderr =
|
|
1202
|
+
"stderr" in wrote && wrote.stderr instanceof Uint8Array
|
|
1203
|
+
? Buffer.from(wrote.stderr).toString("utf8")
|
|
1204
|
+
: typeof wrote.stderr === "string"
|
|
1205
|
+
? wrote.stderr
|
|
1206
|
+
: "";
|
|
1207
|
+
if (wroteStatus !== 0) {
|
|
1095
1208
|
console.warn(
|
|
1096
|
-
`[browser] task ${taskId}: MCP config write failed: ${
|
|
1209
|
+
`[browser] task ${taskId}: MCP config write failed: ${wroteStderr.slice(0, 300)}`,
|
|
1097
1210
|
);
|
|
1098
1211
|
} else {
|
|
1099
|
-
const resultLine =
|
|
1212
|
+
const resultLine = wroteStdout
|
|
1100
1213
|
.split(/\r?\n/)
|
|
1101
1214
|
.find((line) => line.startsWith(CONFIG_RESULT_TOKEN));
|
|
1102
1215
|
if (!resultLine) {
|
|
@@ -1147,43 +1260,70 @@ export async function setupBrowserTesting(
|
|
|
1147
1260
|
APT_VIEWER_CMD,
|
|
1148
1261
|
// Close lock fd 9 before starting long-lived viewer grandchildren, or
|
|
1149
1262
|
// they inherit it and keep the apt lock forever after this worker exits.
|
|
1150
|
-
|
|
1151
|
-
"chown -R node:node /home/node/.npm 2>/dev/null || true",
|
|
1152
|
-
|
|
1263
|
+
`/usr/bin/su -s /bin/sh node -c ${shellQuote(viewerSetup)} 9>&-`,
|
|
1264
|
+
"/usr/bin/chown -R node:node /home/node/.npm 2>/dev/null || true",
|
|
1265
|
+
`/usr/bin/touch ${aptGuard.done}`,
|
|
1153
1266
|
].join("; ");
|
|
1154
1267
|
const guardedAptStart = [
|
|
1155
1268
|
`[ -f ${aptGuard.done} ] && exit 0`,
|
|
1156
1269
|
`exec 9>${aptGuard.lock}`,
|
|
1157
|
-
"flock -n 9 || exit 0",
|
|
1270
|
+
"/usr/bin/flock -n 9 || exit 0",
|
|
1158
1271
|
// A different worker may have completed between the first check and
|
|
1159
1272
|
// this lock acquisition.
|
|
1160
1273
|
`[ -f ${aptGuard.done} ] && exit 0`,
|
|
1161
|
-
|
|
1274
|
+
`/usr/bin/nohup /bin/sh -c ${shellQuote(aptWorker)} 9>&9 </dev/null ` +
|
|
1162
1275
|
">/tmp/uai-pw-deps.log 2>&1 &",
|
|
1163
1276
|
].join("; ");
|
|
1164
1277
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
"-
|
|
1175
|
-
"
|
|
1176
|
-
"
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1278
|
+
const rootWorkerEnv = {
|
|
1279
|
+
...RUNTIME_AUTHORITY_ENV,
|
|
1280
|
+
NPM_CONFIG_CACHE: "/tmp/uai-root-npm-cache",
|
|
1281
|
+
npm_config_cache: "/tmp/uai-root-npm-cache",
|
|
1282
|
+
NPM_CONFIG_LOGS_DIR: "/tmp/uai-root-npm-cache/_logs",
|
|
1283
|
+
npm_config_logs_dir: "/tmp/uai-root-npm-cache/_logs",
|
|
1284
|
+
};
|
|
1285
|
+
if (environment) {
|
|
1286
|
+
await environment.exec({
|
|
1287
|
+
argv: ["/bin/sh", "-c", guardedAptStart],
|
|
1288
|
+
user: "root",
|
|
1289
|
+
cwd: "/workspace",
|
|
1290
|
+
env: rootWorkerEnv,
|
|
1291
|
+
timeoutMs: 10_000,
|
|
1292
|
+
maxOutputBytes: 64 * 1024,
|
|
1293
|
+
});
|
|
1294
|
+
} else {
|
|
1295
|
+
await taskContainerCli(
|
|
1296
|
+
[
|
|
1297
|
+
"exec",
|
|
1298
|
+
"-u",
|
|
1299
|
+
"root",
|
|
1300
|
+
// HOME=/home/node is for asdf version resolution ONLY — without the
|
|
1301
|
+
// cache redirect below, root's npx writes ROOT-OWNED entries into
|
|
1302
|
+
// node's ~/.npm and breaks the node user's own npx (which launches
|
|
1303
|
+
// the MCP server). Found live 2026-07-08.
|
|
1304
|
+
"-w",
|
|
1305
|
+
"/workspace",
|
|
1306
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
1307
|
+
// These root-worker exceptions must follow the common authority args:
|
|
1308
|
+
// Docker/npm use the final value when a spelling is repeated. Keep
|
|
1309
|
+
// both cases aligned so npm's case-insensitive config lookup cannot
|
|
1310
|
+
// fall back to node's private cache or logs directory.
|
|
1311
|
+
"-e",
|
|
1312
|
+
"NPM_CONFIG_CACHE=/tmp/uai-root-npm-cache",
|
|
1313
|
+
"-e",
|
|
1314
|
+
"npm_config_cache=/tmp/uai-root-npm-cache",
|
|
1315
|
+
"-e",
|
|
1316
|
+
"NPM_CONFIG_LOGS_DIR=/tmp/uai-root-npm-cache/_logs",
|
|
1317
|
+
"-e",
|
|
1318
|
+
"npm_config_logs_dir=/tmp/uai-root-npm-cache/_logs",
|
|
1319
|
+
container,
|
|
1320
|
+
"/bin/sh",
|
|
1321
|
+
"-c",
|
|
1322
|
+
guardedAptStart,
|
|
1323
|
+
],
|
|
1324
|
+
{ timeoutMs: 10_000 },
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1187
1327
|
console.log(
|
|
1188
1328
|
`[browser] task ${taskId}: Playwright MCP wired (browser ${ready ? "ready" : "NOT ready"}; apt deps backgrounded)`,
|
|
1189
1329
|
);
|
package/lib/codex-auth.ts
CHANGED
|
@@ -42,6 +42,12 @@ import { isNull } from "drizzle-orm";
|
|
|
42
42
|
|
|
43
43
|
import { getDb, schema } from "./db";
|
|
44
44
|
import { dockerCli } from "./docker-exec";
|
|
45
|
+
import {
|
|
46
|
+
listRunningTaskAppContainerNames,
|
|
47
|
+
taskAppContainerName,
|
|
48
|
+
taskContainerCli,
|
|
49
|
+
} from "./task-container-cli";
|
|
50
|
+
import { runtimeAuthorityDockerExecArgs } from "./runtime-authority";
|
|
45
51
|
|
|
46
52
|
/** The exact set task-up.sh copies into `/home/node/.codex`. */
|
|
47
53
|
const CODEX_ITEMS = [
|
|
@@ -72,7 +78,7 @@ function ownerCodexDir(): string {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const defaultExec: NonNullable<CodexDeps["exec"]> = async (args) => {
|
|
75
|
-
const res = await
|
|
81
|
+
const res = await taskContainerCli(args, { timeoutMs: EXEC_TIMEOUT_MS });
|
|
76
82
|
return { status: res.status, stdout: res.stdout, stderr: res.stderr };
|
|
77
83
|
};
|
|
78
84
|
|
|
@@ -82,7 +88,7 @@ function defaultActiveTaskContainers(): string[] {
|
|
|
82
88
|
.from(schema.hostTasks)
|
|
83
89
|
.where(isNull(schema.hostTasks.endedAt))
|
|
84
90
|
.all()
|
|
85
|
-
.map((r) =>
|
|
91
|
+
.map((r) => taskAppContainerName(r.taskId));
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/**
|
|
@@ -93,6 +99,13 @@ function defaultActiveTaskContainers(): string[] {
|
|
|
93
99
|
async function dockerRunningNames(
|
|
94
100
|
exec: NonNullable<CodexDeps["exec"]>,
|
|
95
101
|
): Promise<Set<string> | null> {
|
|
102
|
+
// The list verb differs per backend (docker ps vs apple container list);
|
|
103
|
+
// the shared helper owns that translation. The injected exec seam is kept
|
|
104
|
+
// for tests, which script docker-shaped ps answers.
|
|
105
|
+
if (exec === defaultExec) {
|
|
106
|
+
const names = await listRunningTaskAppContainerNames();
|
|
107
|
+
return names === null ? null : new Set(names);
|
|
108
|
+
}
|
|
96
109
|
const res = await exec(["ps", "--filter", "status=running", "--format", "{{.Names}}"]);
|
|
97
110
|
if (res.status !== 0) return null;
|
|
98
111
|
return new Set(
|
|
@@ -129,7 +142,16 @@ async function copyCodexInto(
|
|
|
129
142
|
}
|
|
130
143
|
};
|
|
131
144
|
await must(
|
|
132
|
-
[
|
|
145
|
+
[
|
|
146
|
+
"exec",
|
|
147
|
+
"-u",
|
|
148
|
+
"root",
|
|
149
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
150
|
+
container,
|
|
151
|
+
"/bin/mkdir",
|
|
152
|
+
"-p",
|
|
153
|
+
"/home/node/.codex",
|
|
154
|
+
],
|
|
133
155
|
"mkdir /home/node/.codex",
|
|
134
156
|
);
|
|
135
157
|
for (const item of items) {
|
|
@@ -138,7 +160,17 @@ async function copyCodexInto(
|
|
|
138
160
|
await must(["cp", src, `${container}:/home/node/.codex/`], `docker cp ${item}`);
|
|
139
161
|
}
|
|
140
162
|
await must(
|
|
141
|
-
[
|
|
163
|
+
[
|
|
164
|
+
"exec",
|
|
165
|
+
"-u",
|
|
166
|
+
"root",
|
|
167
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
168
|
+
container,
|
|
169
|
+
"/bin/chown",
|
|
170
|
+
"-R",
|
|
171
|
+
"node:node",
|
|
172
|
+
"/home/node/.codex",
|
|
173
|
+
],
|
|
142
174
|
"chown -R node:node /home/node/.codex",
|
|
143
175
|
);
|
|
144
176
|
}
|
|
@@ -220,7 +252,7 @@ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
220
252
|
* missing `~/.codex` dir is a no-op — the host-start reinject already handles
|
|
221
253
|
* a first login that flips Codex available. UAI_CODEX_REINJECT=0 disables it.
|
|
222
254
|
*/
|
|
223
|
-
export function watchCodexAuth(): void {
|
|
255
|
+
export function watchCodexAuth(canReinject: () => boolean = () => true): void {
|
|
224
256
|
if (watcher || process.env.UAI_CODEX_REINJECT === "0") return;
|
|
225
257
|
const dir = ownerCodexDir();
|
|
226
258
|
if (!existsSync(dir)) return;
|
|
@@ -229,7 +261,9 @@ export function watchCodexAuth(): void {
|
|
|
229
261
|
// filename is null on some platforms — then we can't tell, so proceed.
|
|
230
262
|
if (filename && filename !== "auth.json") return;
|
|
231
263
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
232
|
-
debounceTimer = setTimeout(() =>
|
|
264
|
+
debounceTimer = setTimeout(() => {
|
|
265
|
+
if (canReinject()) void reinjectCodexRunningTasks();
|
|
266
|
+
}, 2_000);
|
|
233
267
|
debounceTimer.unref?.();
|
|
234
268
|
});
|
|
235
269
|
} catch (err) {
|
package/lib/command-db.ts
CHANGED
|
@@ -37,6 +37,26 @@ export function createTaskDownCommandDb(
|
|
|
37
37
|
return dbPath;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Environment-provider teardown reconstructs from a durable task locator, not
|
|
42
|
+
* from the cloud command payload that originally provisioned it. The teardown
|
|
43
|
+
* script needs only the exact task identity/runtime row; it deliberately
|
|
44
|
+
* derives every destructive Docker/filesystem target from that identity.
|
|
45
|
+
*/
|
|
46
|
+
export function createTaskEnvironmentDownCommandDb(
|
|
47
|
+
taskId: string,
|
|
48
|
+
runtime: HostTask | null,
|
|
49
|
+
): string {
|
|
50
|
+
const dbPath = newCommandDbPath(taskId);
|
|
51
|
+
const db = openCommandDb(dbPath);
|
|
52
|
+
try {
|
|
53
|
+
insertTask(db, runtimeTask(taskId, runtime));
|
|
54
|
+
} finally {
|
|
55
|
+
db.close();
|
|
56
|
+
}
|
|
57
|
+
return dbPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
export function createTaskStatusCommandDb(
|
|
41
61
|
taskId: string,
|
|
42
62
|
runtime: HostTask | null,
|