@runuai/host 0.9.140 → 0.9.142
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/package.json +1 -1
- package/scripts/agent/task-up.sh +37 -1
- package/src/main.ts +13 -3
- package/src/ui/server.ts +19 -0
package/package.json
CHANGED
package/scripts/agent/task-up.sh
CHANGED
|
@@ -141,6 +141,42 @@ reject_workspace_runtime_shadow() {
|
|
|
141
141
|
fi
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
# The first check of a task-up used to be the refusal above. A fresh task
|
|
145
|
+
# never trips it (the workspace is empty), but a RESUMED one can: the
|
|
146
|
+
# workspace persists across an idle park, agents' shells start in /workspace,
|
|
147
|
+
# and `asdf local nodejs 22` from there writes exactly this file — after
|
|
148
|
+
# which the task could never wake again without someone deleting the file on
|
|
149
|
+
# the host (client runner, 2026-09-14). The entry is the task's own
|
|
150
|
+
# container-writable content, so move it aside instead: a rename is
|
|
151
|
+
# metadata-only (never opens a FIFO/device/socket), the runtime proof is
|
|
152
|
+
# unchanged because nothing is left at the root, and the content survives
|
|
153
|
+
# under .uai/refused/ where the agent can re-pin it in a project. The
|
|
154
|
+
# post-materialization recheck below keeps refusing — by then the root must
|
|
155
|
+
# be empty by construction, so an entry there is a bug, not a user file.
|
|
156
|
+
quarantine_workspace_runtime_shadow() {
|
|
157
|
+
local path="$task_workspace/.tool-versions" aside_dir aside
|
|
158
|
+
if [ -L "$path" ] || [ -e "$path" ]; then
|
|
159
|
+
aside_dir="$task_workspace/.uai/refused"
|
|
160
|
+
reject_unsafe_host_directory "$task_workspace/.uai" "the task control directory"
|
|
161
|
+
reject_unsafe_host_directory "$aside_dir" "the refused-files directory"
|
|
162
|
+
mkdir -p "$aside_dir"
|
|
163
|
+
aside="$aside_dir/tool-versions.$(date +%s)"
|
|
164
|
+
if ! mv -f "$path" "$aside" 2>/dev/null || [ -L "$path" ] || [ -e "$path" ]; then
|
|
165
|
+
reject_workspace_runtime_shadow
|
|
166
|
+
fi
|
|
167
|
+
cat > "$aside_dir/README.txt" <<'NOTE'
|
|
168
|
+
Uai moved a /workspace/.tool-versions aside before starting this task.
|
|
169
|
+
|
|
170
|
+
A .tool-versions at the workspace root is outside the runtime proof and would
|
|
171
|
+
override the runtimes proved for every project in the task, so the host refuses
|
|
172
|
+
to start a container while one exists. The file is kept here, untouched, under
|
|
173
|
+
its original content. Pin runtimes in a project's own .tool-versions instead
|
|
174
|
+
(/workspace/<project>/.tool-versions), which the materializer proves.
|
|
175
|
+
NOTE
|
|
176
|
+
log "moved a workspace-root .tool-versions aside to ${aside#"$task_workspace"/} (it would override the runtimes proved for this task; pin per project instead)"
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
144
180
|
mapped_host_port() {
|
|
145
181
|
local container="$1" container_port="$2"
|
|
146
182
|
docker port "$container" "$container_port" 2>/dev/null \
|
|
@@ -555,7 +591,7 @@ else
|
|
|
555
591
|
fi
|
|
556
592
|
reject_unsafe_host_directory "$task_workspace" "the task workspace"
|
|
557
593
|
mkdir -p "$task_workspace"
|
|
558
|
-
|
|
594
|
+
quarantine_workspace_runtime_shadow
|
|
559
595
|
|
|
560
596
|
# Git resolves symlinked ancestors when it writes a linked worktree's absolute
|
|
561
597
|
# `.git` pointer. Keep the configured (lexical) task paths as Uai's stable host
|
package/src/main.ts
CHANGED
|
@@ -813,8 +813,13 @@ void connect();
|
|
|
813
813
|
// Local browser UI (ADR-028) — same single process, alongside the WSS client.
|
|
814
814
|
// Best-effort: a UI bind failure must not take the host service down.
|
|
815
815
|
// A task machine has no operator sitting at it (ADR-133): no local UI, no
|
|
816
|
-
// Mac vnode watch.
|
|
817
|
-
|
|
816
|
+
// Mac vnode watch. The sign-in box (ADR-136) has no operator either, but it
|
|
817
|
+
// keeps a monitor-only status listener: `uai-host status` reads it, and so
|
|
818
|
+
// does the managed installer's post-start check — without it the box read
|
|
819
|
+
// as "service not running" and the first managed install rolled itself back
|
|
820
|
+
// (live 2026-09-14), leaving the box on an npm install that no auto-update
|
|
821
|
+
// reaches.
|
|
822
|
+
if (!sidecarMode()) void startLocalUi();
|
|
818
823
|
// ADR-118 follow-up: vnode-pressure observability (macOS/apple-container
|
|
819
824
|
// hosts). Logs only on band transitions; the sensor caches its sysctl read.
|
|
820
825
|
if (!sidecarMode() && !signInBoxMode()) {
|
|
@@ -834,6 +839,7 @@ async function startLocalUi(): Promise<void> {
|
|
|
834
839
|
cloudUrl: bridgeUrl,
|
|
835
840
|
hostId,
|
|
836
841
|
logPath: serviceLogPath(),
|
|
842
|
+
monitorOnly: signInBoxMode(),
|
|
837
843
|
taskMemory: async (composeProject) =>
|
|
838
844
|
containerRuntimeProblem()
|
|
839
845
|
? null
|
|
@@ -865,7 +871,11 @@ async function startLocalUi(): Promise<void> {
|
|
|
865
871
|
}
|
|
866
872
|
},
|
|
867
873
|
});
|
|
868
|
-
console.log(
|
|
874
|
+
console.log(
|
|
875
|
+
signInBoxMode()
|
|
876
|
+
? `[host-agent] local status endpoint on http://127.0.0.1:${handle.port} (monitor only)`
|
|
877
|
+
: `[host-agent] local UI on http://127.0.0.1:${handle.port}`,
|
|
878
|
+
);
|
|
869
879
|
} catch (err) {
|
|
870
880
|
console.warn(
|
|
871
881
|
`[host-agent] local UI failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
package/src/ui/server.ts
CHANGED
|
@@ -69,6 +69,15 @@ export interface UiServerOptions {
|
|
|
69
69
|
uiDir: string;
|
|
70
70
|
/** File to persist the chosen port to (`$UAI_DATA_DIR/host.port`). */
|
|
71
71
|
portFilePath: string;
|
|
72
|
+
/**
|
|
73
|
+
* ADR-136 sign-in box: answer only the read-only status trio
|
|
74
|
+
* (`/api/status`, `/api/cloud`, `/api/tasks`) so `uai-host status` — and
|
|
75
|
+
* the managed installer's post-start check, which parses it — can see the
|
|
76
|
+
* box come up and connect. Every write and every other read (engines,
|
|
77
|
+
* users, events, the static UI) is 404: the box holds no credential at
|
|
78
|
+
* rest and runs no tasks, and nobody sits at it.
|
|
79
|
+
*/
|
|
80
|
+
monitorOnly?: boolean;
|
|
72
81
|
/** First port to try (5876 or UAI_HOST_UI_PORT). 0 lets the OS pick (tests). */
|
|
73
82
|
startPort: number;
|
|
74
83
|
// Facts for /api/status:
|
|
@@ -110,6 +119,13 @@ export interface UiServerHandle {
|
|
|
110
119
|
close: () => Promise<void>;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
/** The GET routes a `monitorOnly` server still answers (see UiServerOptions). */
|
|
123
|
+
const MONITOR_ONLY_ROUTES: ReadonlySet<string> = new Set([
|
|
124
|
+
"/api/status",
|
|
125
|
+
"/api/cloud",
|
|
126
|
+
"/api/tasks",
|
|
127
|
+
]);
|
|
128
|
+
|
|
113
129
|
/** Start the local UI server, binding to 127.0.0.1 with port fallback. */
|
|
114
130
|
export async function startUiServer(
|
|
115
131
|
opts: UiServerOptions,
|
|
@@ -170,6 +186,9 @@ async function handle(
|
|
|
170
186
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
171
187
|
const method = req.method ?? "GET";
|
|
172
188
|
try {
|
|
189
|
+
if (opts.monitorOnly && !MONITOR_ONLY_ROUTES.has(path)) {
|
|
190
|
+
return sendError(res, 404, `no such endpoint: ${path}`);
|
|
191
|
+
}
|
|
173
192
|
// POST — engine connect/disconnect (the only writes; localhost-bound, so
|
|
174
193
|
// the loopback binding is the perimeter, ADR-028).
|
|
175
194
|
if (method === "POST") {
|