@runuai/host 0.9.144 → 0.9.146
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/agents/codex.ts +1 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar.ts +12 -1
- package/package.json +1 -1
- package/scripts/agent/task-up.sh +12 -9
- package/src/index.ts +11 -0
- package/src/main.ts +32 -3
- package/src/protocol.ts +12 -2
package/lib/agents/codex.ts
CHANGED
|
@@ -55,6 +55,7 @@ import type {
|
|
|
55
55
|
// Sourced from that picker; UPDATE WHEN MODELS CHANGE. Order = display order
|
|
56
56
|
// in the cloud picker. Legacy models remain reachable via config.toml.
|
|
57
57
|
const CODEX_MODELS = [
|
|
58
|
+
"gpt-6-astra",
|
|
58
59
|
"gpt-5.6-sol",
|
|
59
60
|
"gpt-5.6-terra",
|
|
60
61
|
"gpt-5.6-luna",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The warm-pool claim's host half (docs/machine-warm-pool.md): the cloud's
|
|
3
|
+
* `sidecarAssign` names the task this pool machine now serves. The task id
|
|
4
|
+
* is appended to the sidecar unit's EnvironmentFile and the process exits
|
|
5
|
+
* with ASSIGN_RESTART_EXIT_CODE — systemd (`Restart=on-failure`) brings the
|
|
6
|
+
* unit back up in plain sidecar mode, the tested runner-restart path.
|
|
7
|
+
* Rewriting a root-owned file and restarting is deliberately boring: no
|
|
8
|
+
* runtime re-wiring of boot-time gates, no half-pool half-task states.
|
|
9
|
+
*/
|
|
10
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
import { sidecarPoolMode } from "./sidecar";
|
|
13
|
+
|
|
14
|
+
/** Anything but 0 and 3: 0 would not restart the unit, 3 is the lease-denied
|
|
15
|
+
* latch (`RestartPreventExitStatus=3`). */
|
|
16
|
+
export const ASSIGN_RESTART_EXIT_CODE = 64;
|
|
17
|
+
const EXIT_DELAY_MS = 500;
|
|
18
|
+
|
|
19
|
+
export function sidecarEnvFile(): string {
|
|
20
|
+
return process.env.UAI_SIDECAR_ENV_FILE ?? "/etc/uai/sidecar.env";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function applySidecarAssign(
|
|
24
|
+
taskId: string,
|
|
25
|
+
deps: {
|
|
26
|
+
envFile?: string;
|
|
27
|
+
exit?: (code: number) => void;
|
|
28
|
+
append?: (file: string, text: string) => void;
|
|
29
|
+
read?: (file: string) => string;
|
|
30
|
+
} = {},
|
|
31
|
+
): void {
|
|
32
|
+
if (!sidecarPoolMode()) {
|
|
33
|
+
throw new Error("not a pool machine (already assigned, or not a sidecar)");
|
|
34
|
+
}
|
|
35
|
+
if (!/^[a-z0-9]{1,32}$/i.test(taskId)) throw new Error("invalid task id");
|
|
36
|
+
const file = deps.envFile ?? sidecarEnvFile();
|
|
37
|
+
const read = deps.read ?? ((f: string) => readFileSync(f, "utf8"));
|
|
38
|
+
const current = read(file);
|
|
39
|
+
if (/^UAI_SIDECAR_TASK=/m.test(current)) {
|
|
40
|
+
throw new Error("this machine already carries a task assignment");
|
|
41
|
+
}
|
|
42
|
+
const append = deps.append ?? ((f: string, text: string) => appendFileSync(f, text));
|
|
43
|
+
append(file, `UAI_SIDECAR_TASK=${taskId}\n`);
|
|
44
|
+
// Ack first, then die: the delay lets the command reply flush to the
|
|
45
|
+
// bridge before the socket goes with the process.
|
|
46
|
+
const exit = deps.exit ?? ((code: number) => process.exit(code));
|
|
47
|
+
const timer = setTimeout(() => exit(ASSIGN_RESTART_EXIT_CODE), EXIT_DELAY_MS);
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
}
|
package/lib/sidecar.ts
CHANGED
|
@@ -13,8 +13,19 @@ export function sidecarTaskId(): string | null {
|
|
|
13
13
|
return value && /^[a-z0-9]{1,32}$/i.test(value) ? value : null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Warm-pool mode (docs/machine-warm-pool.md): a sidecar with no task yet.
|
|
18
|
+
* Same machine gates as sidecar mode, no lease (nothing to scope one to);
|
|
19
|
+
* the process waits for `sidecarAssign` to write `UAI_SIDECAR_TASK` into
|
|
20
|
+
* the unit env and restart it into plain sidecar mode. Once a task id is
|
|
21
|
+
* present, pool mode is over — even before the restart lands.
|
|
22
|
+
*/
|
|
23
|
+
export function sidecarPoolMode(): boolean {
|
|
24
|
+
return process.env.UAI_SIDECAR_POOL === "1" && sidecarTaskId() === null;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export function sidecarMode(): boolean {
|
|
17
|
-
return sidecarTaskId() !== null;
|
|
28
|
+
return sidecarTaskId() !== null || sidecarPoolMode();
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
package/package.json
CHANGED
package/scripts/agent/task-up.sh
CHANGED
|
@@ -91,11 +91,9 @@ project_env_json=$(printf '%s' "$project_env_json" | jq -c '.')
|
|
|
91
91
|
|
|
92
92
|
task_dir="$UAI_WORKSPACE_ROOT/tasks/$task_id"
|
|
93
93
|
task_workspace="$task_dir/workspace"
|
|
94
|
+
# The HOST control tree. Deliberately not bind-mounted: the container sees only
|
|
95
|
+
# $task_workspace. Anything an agent must reach belongs under $task_workspace/.uai.
|
|
94
96
|
task_uai_dir="$task_dir/.uai"
|
|
95
|
-
# Agents are told to save browser screenshots and other artifacts under
|
|
96
|
-
# /workspace/.uai/attachments/, but nothing created the directory — every
|
|
97
|
-
# first browser_take_screenshot hit ENOENT (found by an agent in the first
|
|
98
|
-
# apple-container task, 2026-08-24). Created with the .uai dir below.
|
|
99
97
|
projects_root="$UAI_WORKSPACE_ROOT/projects"
|
|
100
98
|
compose_project=$(compose_project_for_task "$task_id")
|
|
101
99
|
app_container=$(app_container_for_task "$task_id")
|
|
@@ -880,14 +878,19 @@ shared_owner_user=$(jq -r '.[0].owner_user_id // ""' <<<"$task_json")
|
|
|
880
878
|
shared_owner_org=$(printf '%s' "$shared_owner_org" | tr ':' '_')
|
|
881
879
|
shared_owner_user=$(printf '%s' "$shared_owner_user" | tr ':' '_')
|
|
882
880
|
shared_root="$UAI_WORKSPACE_ROOT/shared"
|
|
881
|
+
# The workspace's own .uai is the only one the container can read (/workspace/.uai):
|
|
882
|
+
# the CLI, the chat log and the attachments agents are told to write screenshots
|
|
883
|
+
# into all live here. Create it unconditionally — it used to be made only when
|
|
884
|
+
# shared files were on, and `attachments` was made in the unmounted control tree,
|
|
885
|
+
# so every first browser_take_screenshot hit ENOENT (first seen 2026-08-24).
|
|
886
|
+
reject_unsafe_host_directory "$task_workspace/.uai" \
|
|
887
|
+
"the task workspace marker directory"
|
|
888
|
+
mkdir -p "$task_workspace/.uai/attachments"
|
|
883
889
|
if [ "$shared_files_mode" != "off" ]; then
|
|
884
890
|
[ -n "$shared_owner_org" ] && mkdir -p "$shared_root/orgs/$shared_owner_org"
|
|
885
891
|
[ -n "$shared_owner_user" ] && mkdir -p "$shared_root/users/$shared_owner_user"
|
|
886
892
|
# Marker for the preamble: only containers that actually carry the mounts
|
|
887
893
|
# get the "## Shared files" briefing (pre-feature containers don't).
|
|
888
|
-
reject_unsafe_host_directory "$task_workspace/.uai" \
|
|
889
|
-
"the task workspace marker directory"
|
|
890
|
-
mkdir -p "$task_workspace/.uai"
|
|
891
894
|
: > "$task_workspace/.uai/files-mounted"
|
|
892
895
|
fi
|
|
893
896
|
|
|
@@ -1328,7 +1331,7 @@ done < <(jq -c '.[]' <<<"$projects_json")
|
|
|
1328
1331
|
# uid-501 macOS host. The sanitizer above repairs its rewritten control files
|
|
1329
1332
|
# on every later resume.
|
|
1330
1333
|
step "WORKTREE_FAILED" "share task files with container"
|
|
1331
|
-
mkdir -p "$task_uai_dir"
|
|
1334
|
+
mkdir -p "$task_uai_dir"
|
|
1332
1335
|
# The shared group may be a broad host group (for example macOS `staff`). Keep
|
|
1333
1336
|
# its writable leaves behind host-only parents; Docker bind-mounts the exact
|
|
1334
1337
|
# workspace/repository roots, so node does not need host-path traversal here.
|
|
@@ -1376,7 +1379,7 @@ fi
|
|
|
1376
1379
|
|
|
1377
1380
|
step "RENDER_FAILED" "generate compose"
|
|
1378
1381
|
|
|
1379
|
-
mkdir -p "$task_uai_dir"
|
|
1382
|
+
mkdir -p "$task_uai_dir"
|
|
1380
1383
|
|
|
1381
1384
|
STANDARD_CONTAINER_DIR="${UAI_STANDARD_CONTAINER_ASSETS_DIR:-$SCRIPT_DIR/../../images/standard/container}"
|
|
1382
1385
|
if ! STANDARD_CONTAINER_DIR=$(cd "$STANDARD_CONTAINER_DIR" && pwd -P); then
|
package/src/index.ts
CHANGED
|
@@ -1459,6 +1459,17 @@ export const hostCommands: HostCommands = {
|
|
|
1459
1459
|
}
|
|
1460
1460
|
},
|
|
1461
1461
|
|
|
1462
|
+
async sidecarAssign(ctx, input) {
|
|
1463
|
+
logCommand(ctx, "sidecarAssign", input.taskId);
|
|
1464
|
+
try {
|
|
1465
|
+
const { applySidecarAssign } = await import("../lib/sidecar-assign");
|
|
1466
|
+
applySidecarAssign(input.taskId);
|
|
1467
|
+
return ok(undefined);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
return failFromUnknown(err);
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
|
|
1462
1473
|
async channelInterrupt(ctx, taskId, agentId) {
|
|
1463
1474
|
logCommand(ctx, "channelInterrupt", taskId, agentId);
|
|
1464
1475
|
try {
|
package/src/main.ts
CHANGED
|
@@ -257,7 +257,7 @@ import {
|
|
|
257
257
|
} from "./protocol";
|
|
258
258
|
import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
|
|
259
259
|
import { LeaseManager } from "../lib/lease";
|
|
260
|
-
import { markSidecarReady, sidecarMode, sidecarTaskId } from "../lib/sidecar";
|
|
260
|
+
import { markSidecarReady, sidecarMode, sidecarPoolMode, sidecarTaskId } from "../lib/sidecar";
|
|
261
261
|
import {
|
|
262
262
|
purgeDefaultEngineSlots,
|
|
263
263
|
SIGN_IN_BOX_TASK_REFUSAL,
|
|
@@ -638,7 +638,9 @@ if (signInBoxMode()) {
|
|
|
638
638
|
* reconciles and the S3 file mounts run. A denial means the task is no
|
|
639
639
|
* longer ours to run — exit 3 and let the cloud's sweep reclaim the machine.
|
|
640
640
|
*/
|
|
641
|
-
|
|
641
|
+
// A POOL-mode sidecar requests no lease: it has no task to scope one to,
|
|
642
|
+
// and the cloud would refuse it anyway (docs/machine-warm-pool.md).
|
|
643
|
+
const leaseManager: LeaseManager | null = sidecarTaskId()
|
|
642
644
|
? new LeaseManager({
|
|
643
645
|
taskId: sidecarTaskId()!,
|
|
644
646
|
send: (frame) => {
|
|
@@ -931,7 +933,11 @@ function buildCapabilities(): HostCapabilities {
|
|
|
931
933
|
},
|
|
932
934
|
}
|
|
933
935
|
: {}),
|
|
934
|
-
...(sidecarTaskId()
|
|
936
|
+
...(sidecarTaskId()
|
|
937
|
+
? { sidecar: { taskId: sidecarTaskId()! } }
|
|
938
|
+
: sidecarPoolMode()
|
|
939
|
+
? { sidecar: { pool: true } }
|
|
940
|
+
: {}),
|
|
935
941
|
maintenanceReady: areAgentClisReady(),
|
|
936
942
|
mcpGateway: mcpGateway.state(),
|
|
937
943
|
engineLogins: engineLoginManager.capabilities(),
|
|
@@ -2777,6 +2783,8 @@ async function dispatchCommand(
|
|
|
2777
2783
|
);
|
|
2778
2784
|
case "filesOp":
|
|
2779
2785
|
return hostCommands.filesOp(ctx, expectFilesOpInput(args, 0));
|
|
2786
|
+
case "sidecarAssign":
|
|
2787
|
+
return hostCommands.sidecarAssign(ctx, expectSidecarAssignInput(args, 0));
|
|
2780
2788
|
case "channelInterrupt":
|
|
2781
2789
|
return hostCommands.channelInterrupt(
|
|
2782
2790
|
ctx,
|
|
@@ -2910,6 +2918,26 @@ function expectEngineAccountRemoveInput(
|
|
|
2910
2918
|
};
|
|
2911
2919
|
}
|
|
2912
2920
|
|
|
2921
|
+
function expectSidecarAssignInput(
|
|
2922
|
+
args: unknown[],
|
|
2923
|
+
index: number,
|
|
2924
|
+
): { hostId: string; taskId: string } {
|
|
2925
|
+
const value = args[index];
|
|
2926
|
+
const hostId = (value as { hostId?: unknown } | null)?.hostId;
|
|
2927
|
+
const taskId = (value as { taskId?: unknown } | null)?.taskId;
|
|
2928
|
+
if (
|
|
2929
|
+
!value ||
|
|
2930
|
+
typeof value !== "object" ||
|
|
2931
|
+
typeof hostId !== "string" ||
|
|
2932
|
+
hostId.length === 0 ||
|
|
2933
|
+
typeof taskId !== "string" ||
|
|
2934
|
+
!/^[a-z0-9]{1,32}$/i.test(taskId)
|
|
2935
|
+
) {
|
|
2936
|
+
throw new Error(`invalid command args: expected sidecar assignment at ${index}`);
|
|
2937
|
+
}
|
|
2938
|
+
return { hostId, taskId };
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2913
2941
|
function expectNumberArg(args: unknown[], index: number): number {
|
|
2914
2942
|
const value = args[index];
|
|
2915
2943
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -3505,6 +3533,7 @@ const HOST_COMMAND_NAMES = {
|
|
|
3505
3533
|
attachmentWrite: true,
|
|
3506
3534
|
attachmentRead: true,
|
|
3507
3535
|
filesOp: true,
|
|
3536
|
+
sidecarAssign: true,
|
|
3508
3537
|
channelInterrupt: true,
|
|
3509
3538
|
appendTranscript: true,
|
|
3510
3539
|
previewEnsure: true,
|
package/src/protocol.ts
CHANGED
|
@@ -1075,8 +1075,10 @@ export interface HostCapabilities {
|
|
|
1075
1075
|
* provider/region on the host page and, eventually, per-task backing
|
|
1076
1076
|
* choice keys off it. */
|
|
1077
1077
|
/** ADR-133 sidecar mode: this host IS the task's machine — exactly one
|
|
1078
|
-
* task, no container runtime, tunnels on loopback.
|
|
1079
|
-
|
|
1078
|
+
* task, no container runtime, tunnels on loopback. A warm-pool machine
|
|
1079
|
+
* (docs/machine-warm-pool.md) advertises `pool` instead of a task until
|
|
1080
|
+
* `sidecarAssign` restarts it into task mode. */
|
|
1081
|
+
sidecar?: { taskId?: string; pool?: boolean };
|
|
1080
1082
|
machineTasks?: {
|
|
1081
1083
|
provider: string;
|
|
1082
1084
|
region?: string;
|
|
@@ -1550,6 +1552,14 @@ export interface HostCommands {
|
|
|
1550
1552
|
ctx: CommandContext,
|
|
1551
1553
|
input: FilesOpInput,
|
|
1552
1554
|
): Promise<HostCommandResult<FilesOpValue>>;
|
|
1555
|
+
/** Warm pool claim (docs/machine-warm-pool.md): tell a POOL-mode sidecar
|
|
1556
|
+
* which task it now serves. The sidecar writes `UAI_SIDECAR_TASK` into
|
|
1557
|
+
* its unit env, acks, and restarts into plain sidecar mode; a runner not
|
|
1558
|
+
* in pool mode refuses. */
|
|
1559
|
+
sidecarAssign(
|
|
1560
|
+
ctx: CommandContext,
|
|
1561
|
+
input: { hostId: string; taskId: string },
|
|
1562
|
+
): Promise<HostCommandResult<void>>;
|
|
1553
1563
|
channelInterrupt(
|
|
1554
1564
|
ctx: CommandContext,
|
|
1555
1565
|
taskId: string,
|