@runuai/host 0.9.14 → 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/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 +3051 -200
- package/lib/preview-sidecar.ts +68 -14
- 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 +956 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +459 -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/README.md
CHANGED
|
@@ -6,7 +6,7 @@ that runs parallel AI coding tasks in ephemeral Docker containers on a machine
|
|
|
6
6
|
your machine to the Uai cloud and runs the task containers locally. It also
|
|
7
7
|
serves a small read-only monitor UI on `127.0.0.1` (ADR-028).
|
|
8
8
|
|
|
9
|
-
One process does
|
|
9
|
+
One process does three things:
|
|
10
10
|
|
|
11
11
|
- **Cloud bridge** — a host-initiated *outbound* WSS to
|
|
12
12
|
`wss://app.runuai.com/host` carrying every command/tunnel. The cloud never
|
|
@@ -14,6 +14,9 @@ One process does two things:
|
|
|
14
14
|
- **Local UI** — an HTTP server on `127.0.0.1:5876` (next free port if taken):
|
|
15
15
|
connection badge, service info, active tasks, recent events. Localhost only,
|
|
16
16
|
no auth, read-only — mutations come from the cloud, not here.
|
|
17
|
+
- **MCP gateway** — a credential-injecting HTTP proxy for task containers,
|
|
18
|
+
bound to `127.0.0.1:5877` by default. Listener failures retry with capped
|
|
19
|
+
backoff and are advertised to the cloud as bounded health state.
|
|
17
20
|
|
|
18
21
|
## Requirements
|
|
19
22
|
|
|
@@ -40,9 +43,10 @@ uai-host install # per-user service: launchd (macOS) / systemd --user (Linu
|
|
|
40
43
|
uai-host start
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
The host generates its **own** bridge credential locally
|
|
44
|
-
only
|
|
45
|
-
|
|
46
|
+
The host generates its **own** bridge credential locally. The cloud persists
|
|
47
|
+
only its hash; the raw credential transits authenticated TLS for bridge auth
|
|
48
|
+
and existing-host attachment but is not stored there (ADR-015). Once started
|
|
49
|
+
it appears in your org and can run tasks. Data + config live under `~/.uai`
|
|
46
50
|
(`~/.uai/.env.local`, `~/.uai/data/`); from a repo checkout they stay under the
|
|
47
51
|
repo instead. The monitor UI is at `http://127.0.0.1:5876` (exact port in
|
|
48
52
|
`uai-host status`).
|
|
@@ -51,7 +55,11 @@ repo instead. The monitor UI is at `http://127.0.0.1:5876` (exact port in
|
|
|
51
55
|
|
|
52
56
|
```
|
|
53
57
|
setup --cloud <wss-url> --enroll <token>
|
|
54
|
-
claim this machine
|
|
58
|
+
claim this machine, or enroll its existing identity in
|
|
59
|
+
the token's workspace
|
|
60
|
+
enroll <token> [--cloud <wss-url>]
|
|
61
|
+
first-time setup, or attach this machine to another
|
|
62
|
+
workspace; uses the stored cloud URL when available
|
|
55
63
|
install [--dry-run] install as a per-user service for this OS
|
|
56
64
|
uninstall remove the service
|
|
57
65
|
start | stop control the installed service
|
|
@@ -63,6 +71,9 @@ pair <token> store a host token directly (manual fallback for setup)
|
|
|
63
71
|
open open the local UI in your browser
|
|
64
72
|
```
|
|
65
73
|
|
|
74
|
+
`enroll` is available in host version 0.9.15 and newer. Its compatible cloud
|
|
75
|
+
endpoint is intentionally deployed before that host release.
|
|
76
|
+
|
|
66
77
|
`--dry-run` on `install` (and start/stop/…) prints the unit file + load
|
|
67
78
|
commands without touching launchd/systemd.
|
|
68
79
|
|
|
@@ -72,6 +83,12 @@ Runs identically with no display: install via the same flow, skip `uai-host
|
|
|
72
83
|
open`, and use `uai-host status` for everything the UI shows. The UI stays
|
|
73
84
|
reachable over an SSH port-forward to `127.0.0.1:5876` if you want it.
|
|
74
85
|
|
|
86
|
+
At boot, task and channel starts wait for the initial agent-CLI maintenance
|
|
87
|
+
pass to settle before any in-container writer or runner executes. The cloud
|
|
88
|
+
host page shows this short preparation state. It also shows MCP gateway
|
|
89
|
+
listener failures while the host retries them; gateway health is independent
|
|
90
|
+
of ordinary task admission.
|
|
91
|
+
|
|
75
92
|
## Security
|
|
76
93
|
|
|
77
94
|
Tasks run in ephemeral containers and **do not** mount your live AI session
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CREATE INDEX `uai_host_events_task_id_idx` ON `uai_host_events` (`task_id`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE `host_settings` (
|
|
2
|
+
`id` text PRIMARY KEY DEFAULT 'host' NOT NULL,
|
|
3
|
+
`retain_terminal_history` integer DEFAULT true NOT NULL,
|
|
4
|
+
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
5
|
+
CONSTRAINT "host_settings_singleton" CHECK (`host_settings`.`id` = 'host'),
|
|
6
|
+
CONSTRAINT "host_settings_retention_boolean" CHECK (`host_settings`.`retain_terminal_history` IN (0, 1))
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
INSERT INTO `host_settings` (`id`, `retain_terminal_history`) VALUES ('host', true);
|
|
@@ -99,6 +99,27 @@
|
|
|
99
99
|
"when": 1786045000000,
|
|
100
100
|
"tag": "0013_github_credential_generations",
|
|
101
101
|
"breakpoints": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"idx": 14,
|
|
105
|
+
"version": "6",
|
|
106
|
+
"when": 1786406400000,
|
|
107
|
+
"tag": "0014_host_inventory_event_index",
|
|
108
|
+
"breakpoints": true
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"idx": 15,
|
|
112
|
+
"version": "6",
|
|
113
|
+
"when": 1786406401000,
|
|
114
|
+
"tag": "0015_host_settings",
|
|
115
|
+
"breakpoints": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"idx": 16,
|
|
119
|
+
"version": "6",
|
|
120
|
+
"when": 1786406402000,
|
|
121
|
+
"tag": "0016_task_environment",
|
|
122
|
+
"breakpoints": true
|
|
102
123
|
}
|
|
103
124
|
]
|
|
104
125
|
}
|
package/db/schema.ts
CHANGED
|
@@ -9,49 +9,99 @@
|
|
|
9
9
|
import { sql } from "drizzle-orm";
|
|
10
10
|
import {
|
|
11
11
|
blob,
|
|
12
|
+
check,
|
|
13
|
+
index,
|
|
12
14
|
integer,
|
|
13
15
|
primaryKey,
|
|
14
16
|
sqliteTable,
|
|
15
17
|
text,
|
|
16
18
|
} from "drizzle-orm/sqlite-core";
|
|
17
19
|
|
|
18
|
-
export const hostTasks = sqliteTable(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.default(
|
|
38
|
-
})
|
|
20
|
+
export const hostTasks = sqliteTable(
|
|
21
|
+
"uai_host_tasks",
|
|
22
|
+
{
|
|
23
|
+
taskId: text("task_id").primaryKey(),
|
|
24
|
+
codeServerPort: integer("code_server_port"),
|
|
25
|
+
previewPorts: text("preview_ports").notNull().default("[]"),
|
|
26
|
+
composeProject: text("compose_project"),
|
|
27
|
+
worktreePath: text("worktree_path"),
|
|
28
|
+
// ADR-101 Phase C. Null rows predate the provider seam and are inferred as
|
|
29
|
+
// Docker Compose only when their legacy runtime identity is complete.
|
|
30
|
+
environmentProvider: text("environment_provider"),
|
|
31
|
+
environmentLocator: text("environment_locator"),
|
|
32
|
+
statusMirror: text("status_mirror"),
|
|
33
|
+
// Owner + project slugs — received in TaskLaunchInput at task-up and
|
|
34
|
+
// persisted here so the local UI (ADR-028) can show "who/what" without a
|
|
35
|
+
// cloud round-trip. The cloud stays the audit authority.
|
|
36
|
+
ownerUserId: text("owner_user_id"),
|
|
37
|
+
ownerEmail: text("owner_email"),
|
|
38
|
+
ownerName: text("owner_name"),
|
|
39
|
+
projectSlugs: text("project_slugs").notNull().default("[]"),
|
|
40
|
+
lockedAt: integer("locked_at", { mode: "number" }),
|
|
41
|
+
startedAt: integer("started_at", { mode: "number" }),
|
|
42
|
+
endedAt: integer("ended_at", { mode: "number" }),
|
|
43
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
44
|
+
.notNull()
|
|
45
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
46
|
+
},
|
|
47
|
+
(table) => ({
|
|
48
|
+
environmentPair: check(
|
|
49
|
+
"uai_host_tasks_environment_pair",
|
|
50
|
+
sql`(${table.environmentProvider} IS NULL) = (${table.environmentLocator} IS NULL)`,
|
|
51
|
+
),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
39
54
|
|
|
40
55
|
export type HostTask = typeof hostTasks.$inferSelect;
|
|
41
56
|
export type NewHostTask = typeof hostTasks.$inferInsert;
|
|
42
57
|
|
|
58
|
+
// Singleton host-local settings row (ADR-100). Runtime switches that must be
|
|
59
|
+
// visible before SQLite opens remain in `.env.local`; retention policy belongs
|
|
60
|
+
// here because it governs owner-facing local history only. Correctness
|
|
61
|
+
// tombstones and orphan-GC evidence are never controlled by this setting.
|
|
62
|
+
export const hostSettings = sqliteTable(
|
|
63
|
+
"host_settings",
|
|
64
|
+
{
|
|
65
|
+
id: text("id").primaryKey().default("host"),
|
|
66
|
+
retainTerminalHistory: integer("retain_terminal_history", {
|
|
67
|
+
mode: "boolean",
|
|
68
|
+
})
|
|
69
|
+
.notNull()
|
|
70
|
+
.default(true),
|
|
71
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
72
|
+
.notNull()
|
|
73
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
74
|
+
},
|
|
75
|
+
(table) => ({
|
|
76
|
+
singleton: check("host_settings_singleton", sql`${table.id} = 'host'`),
|
|
77
|
+
retentionBoolean: check(
|
|
78
|
+
"host_settings_retention_boolean",
|
|
79
|
+
sql`${table.retainTerminalHistory} IN (0, 1)`,
|
|
80
|
+
),
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export type HostSettings = typeof hostSettings.$inferSelect;
|
|
85
|
+
export type NewHostSettings = typeof hostSettings.$inferInsert;
|
|
86
|
+
|
|
43
87
|
// Host-side task lifecycle event log (ADR-028). HostEvent (the agent-output
|
|
44
88
|
// stream) is forwarded to the cloud, not stored; this table is the local
|
|
45
89
|
// `/api/events` feed — appended on task.created/started/ended (+ ship).
|
|
46
|
-
export const hostEvents = sqliteTable(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.notNull()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
export const hostEvents = sqliteTable(
|
|
91
|
+
"uai_host_events",
|
|
92
|
+
{
|
|
93
|
+
id: text("id").primaryKey(), // ulid
|
|
94
|
+
taskId: text("task_id").notNull(),
|
|
95
|
+
kind: text("kind").notNull(), // task.created | task.started | task.ended | task.ship
|
|
96
|
+
ts: integer("ts", { mode: "number" })
|
|
97
|
+
.notNull()
|
|
98
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
99
|
+
detail: text("detail"),
|
|
100
|
+
},
|
|
101
|
+
(t) => ({
|
|
102
|
+
taskIdIdx: index("uai_host_events_task_id_idx").on(t.taskId),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
55
105
|
|
|
56
106
|
export type HostEventRow = typeof hostEvents.$inferSelect;
|
|
57
107
|
export type NewHostEventRow = typeof hostEvents.$inferInsert;
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
# Design points:
|
|
11
11
|
# - Debian bookworm-slim base, non-root `node` user (home /home/node).
|
|
12
12
|
# - asdf at /opt/asdf with plugins for nodejs/python/golang/ruby/rust.
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Before Compose starts a task, a host-controlled maintenance container
|
|
14
|
+
# materializes its declared versions into the host-wide /opt/asdf-data
|
|
15
|
+
# volume. uai-init only installs task-local dependencies afterward.
|
|
15
16
|
# - A default ~/.tool-versions (asdf global fallback) pins a working Node +
|
|
16
17
|
# Python so the agent CLIs and a 0-project scratchpad always have a runtime;
|
|
17
18
|
# a project's own .tool-versions takes precedence (asdf dir walk).
|
|
@@ -65,6 +66,7 @@ RUN apt-get update \
|
|
|
65
66
|
tk-dev \
|
|
66
67
|
tmux \
|
|
67
68
|
unzip \
|
|
69
|
+
util-linux \
|
|
68
70
|
xz-utils \
|
|
69
71
|
zlib1g-dev \
|
|
70
72
|
zsh \
|
|
@@ -121,18 +123,33 @@ RUN groupadd --gid 1000 node \
|
|
|
121
123
|
|
|
122
124
|
ENV ASDF_DIR=/opt/asdf
|
|
123
125
|
ENV ASDF_DATA_DIR=/opt/asdf-data
|
|
126
|
+
ENV ASDF_CONFIG_FILE=/dev/null
|
|
127
|
+
ENV ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions
|
|
128
|
+
ENV COREPACK_HOME=/opt/asdf-data/corepack
|
|
129
|
+
ENV COREPACK_DEFAULT_TO_LATEST=0
|
|
130
|
+
ENV COREPACK_ENABLE_AUTO_PIN=0
|
|
131
|
+
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
|
132
|
+
ENV COREPACK_ENV_FILE=0
|
|
133
|
+
ENV COREPACK_ENABLE_PROJECT_SPEC=1
|
|
134
|
+
ENV COREPACK_ENABLE_STRICT=1
|
|
135
|
+
ENV COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0
|
|
136
|
+
ENV COREPACK_INTEGRITY_CHECK=1
|
|
137
|
+
ENV COREPACK_ENABLE_NETWORK=0
|
|
138
|
+
ENV COREPACK_NPM_REGISTRY=https://registry.npmjs.org
|
|
124
139
|
# Leave the tool-versions filename at asdf's default (`.tool-versions`) so asdf
|
|
125
140
|
# walks UP from the cwd and a project's /workspace/<slug>/.tool-versions wins.
|
|
126
141
|
# The baked default lives at the node user's home (~/.tool-versions) as asdf's
|
|
127
142
|
# global fallback for tasks with no project file (agent CLIs, scratchpad).
|
|
128
|
-
#
|
|
129
|
-
#
|
|
143
|
+
# Keep ASDF_DEFAULT_TOOL_VERSIONS_FILENAME as the basename above, never an
|
|
144
|
+
# absolute path: an absolute value bypasses project files entirely (it made
|
|
130
145
|
# asdf read only /etc/.tool-versions, ignoring per-project pins).
|
|
131
146
|
|
|
132
147
|
RUN git clone --depth 1 --branch "$ASDF_VERSION" https://github.com/asdf-vm/asdf.git "$ASDF_DIR" \
|
|
133
148
|
&& mkdir -p "$ASDF_DATA_DIR" \
|
|
149
|
+
&& touch "$ASDF_DATA_DIR/.uai-maintenance.lock" \
|
|
134
150
|
&& chown -R node:node "$ASDF_DIR" "$ASDF_DATA_DIR" \
|
|
135
|
-
&& chmod -R g+rwX "$ASDF_DATA_DIR"
|
|
151
|
+
&& chmod -R g+rwX "$ASDF_DATA_DIR" \
|
|
152
|
+
&& chmod 0664 "$ASDF_DATA_DIR/.uai-maintenance.lock"
|
|
136
153
|
|
|
137
154
|
# Make asdf available on PATH + shell init for BOTH root and node, for login
|
|
138
155
|
# and non-login shells (uai-init runs as node via a non-interactive exec).
|
|
@@ -172,8 +189,8 @@ RUN su node -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/oh
|
|
|
172
189
|
# 4. asdf plugins + the root default runtimes.
|
|
173
190
|
#
|
|
174
191
|
# Plugins are added for all common runtimes; only the root defaults are
|
|
175
|
-
# *installed* at build time.
|
|
176
|
-
#
|
|
192
|
+
# *installed* at build time. A pre-Compose maintenance container installs
|
|
193
|
+
# project versions into the shared /opt/asdf-data volume.
|
|
177
194
|
#
|
|
178
195
|
# Run as node so the install lands in /opt/asdf-data owned by node.
|
|
179
196
|
# ---------------------------------------------------------------------------
|
|
@@ -183,14 +200,17 @@ RUN su node -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/oh
|
|
|
183
200
|
# scratchpad tasks). A project file takes precedence via asdf's dir walk. The
|
|
184
201
|
# `tool-versions` file in this directory is the AUTHORITATIVE source.
|
|
185
202
|
COPY tool-versions /home/node/.tool-versions
|
|
186
|
-
|
|
203
|
+
COPY --chown=root:root container/corepack-version /usr/local/share/uai/corepack-version
|
|
204
|
+
COPY --chown=root:root container/uai-materialize-runtimes /usr/local/bin/uai-materialize-runtimes
|
|
205
|
+
RUN chmod 0644 /home/node/.tool-versions /usr/local/share/uai/corepack-version \
|
|
206
|
+
&& chmod 0755 /usr/local/bin/uai-materialize-runtimes \
|
|
187
207
|
&& chown node:node /home/node/.tool-versions
|
|
188
208
|
|
|
189
209
|
USER node
|
|
190
210
|
|
|
191
211
|
# Add plugins for all common runtimes, then install exactly the versions the
|
|
192
212
|
# default ~/.tool-versions pins (read straight from the file so there is one
|
|
193
|
-
# source of truth). Other versions install
|
|
213
|
+
# source of truth). Other versions install in the pre-Compose materializer.
|
|
194
214
|
RUN bash -lc '\
|
|
195
215
|
set -e; \
|
|
196
216
|
. /opt/asdf/asdf.sh; \
|
|
@@ -214,7 +234,13 @@ RUN bash -lc '\
|
|
|
214
234
|
RUN bash -lc '\
|
|
215
235
|
set -e; \
|
|
216
236
|
. /opt/asdf/asdf.sh; \
|
|
217
|
-
|
|
237
|
+
COREPACK_VERSION="$(sed -n "1p" /usr/local/share/uai/corepack-version)"; \
|
|
238
|
+
printf "%s" "$COREPACK_VERSION" | grep -Eq "^[0-9]+\\.[0-9]+\\.[0-9]+$"; \
|
|
239
|
+
npm install -g @anthropic-ai/claude-code @openai/codex "corepack@$COREPACK_VERSION" \
|
|
240
|
+
--registry=https://registry.npmjs.org/ \
|
|
241
|
+
--userconfig=/dev/null \
|
|
242
|
+
--globalconfig=/dev/uai-empty-npmrc; \
|
|
243
|
+
corepack enable; \
|
|
218
244
|
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method standalone --prefix /home/node/.local; \
|
|
219
245
|
asdf reshim nodejs; \
|
|
220
246
|
'
|
|
@@ -13,10 +13,12 @@ See [`docs/runtime.md`](../../../docs/runtime.md) for the authoritative spec.
|
|
|
13
13
|
uid/gid 1000). The container runs as `node`; uai-init and the agent CLIs
|
|
14
14
|
expect `/home/node`.
|
|
15
15
|
- **asdf** at `/opt/asdf`, data dir `/opt/asdf-data` (the host-wide
|
|
16
|
-
`uai-asdf-data` volume mounts there). Plugins
|
|
17
|
-
`python`, `golang`, `ruby`, `rust`. Only the root
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
`uai-asdf-data` volume mounts there read-only in task apps). Plugins
|
|
17
|
+
pre-added for `nodejs`, `python`, `golang`, `ruby`, `rust`. Only the root
|
|
18
|
+
defaults are installed at build time; after worktree resolution, a
|
|
19
|
+
host-controlled pre-start maintenance container is the bounded, locked
|
|
20
|
+
read-write user that installs and proves other versions before the task app
|
|
21
|
+
exists.
|
|
20
22
|
- **Default `~/.tool-versions`** (asdf's global fallback, at `/home/node`)
|
|
21
23
|
pinning a default Node + Python. The `tool-versions` file in this directory is
|
|
22
24
|
the authoritative source for the numbers; the `Dockerfile` `COPY`s it to
|
|
@@ -37,7 +39,18 @@ See [`docs/runtime.md`](../../../docs/runtime.md) for the authoritative spec.
|
|
|
37
39
|
- **code-server** (the Editor pane), **gh**, **git**, **curl**,
|
|
38
40
|
**ca-certificates**, **jq**, **ripgrep**, **tmux**, **direnv**, **uv**,
|
|
39
41
|
plus `build-essential` and the `-dev` libraries asdf's build plugins need.
|
|
40
|
-
- **`uai-
|
|
42
|
+
- **`uai-materialize-runtimes`** baked at
|
|
43
|
+
`/usr/local/bin/uai-materialize-runtimes`; it is the credential-free,
|
|
44
|
+
workspace-read-only shared-cache writer used before Compose starts a task.
|
|
45
|
+
The host supplies the selected project slugs at the fixed, read-only
|
|
46
|
+
`/run/uai/runtime-projects` allowlist; only those immediate Git worktrees are
|
|
47
|
+
inspected, so workspace control/shared directories cannot become projects.
|
|
48
|
+
- **`uai-init`** baked at `/usr/local/bin/uai-init`; it installs task-local
|
|
49
|
+
dependencies and starts code-server, but does not mutate asdf runtimes.
|
|
50
|
+
- **Task-private npm globals** under `/home/node/.local/share/uai-npm`, with
|
|
51
|
+
cache/logs under `/home/node/.cache/uai-npm`. The shared asdf shims and
|
|
52
|
+
system controls remain ahead of this prefix's `bin` on `PATH`, and
|
|
53
|
+
`ASDF_SKIP_RESHIM=1` prevents npm's asdf hook from writing shared shims.
|
|
41
54
|
- `WORKDIR /workspace`; `CMD ["sleep","infinity"]`.
|
|
42
55
|
|
|
43
56
|
Codex credentials are **not** baked in or mounted — task-up `docker cp`s them
|
|
@@ -50,7 +63,8 @@ is bind-mounted read-only by the generated compose.
|
|
|
50
63
|
| --- | --- |
|
|
51
64
|
| Standard image tag | `uai-standard:dev` |
|
|
52
65
|
| Per-task derived image (only with `extra`) | `uai-task-<taskId>` |
|
|
53
|
-
| Shared asdf data volume | `uai-asdf-data
|
|
66
|
+
| Shared asdf data volume | app: `uai-asdf-data:/opt/asdf-data:ro`; maintenance: `:rw` |
|
|
67
|
+
| Task npm prefix / cache | `/home/node/.local/share/uai-npm` / `/home/node/.cache/uai-npm` |
|
|
54
68
|
| Compose service name | `app` (container `task-<taskId>-app-1`) |
|
|
55
69
|
| In-container workspace | `/workspace` |
|
|
56
70
|
| code-server port | `8080` (inside the container) |
|
|
@@ -77,37 +91,61 @@ steps below are for local image iteration only.
|
|
|
77
91
|
# 1. Build.
|
|
78
92
|
docker build -t uai-standard:dev host-agent/images/standard
|
|
79
93
|
|
|
80
|
-
# 2. Create the shared asdf data volume + a throwaway
|
|
94
|
+
# 2. Create the shared asdf data volume + a throwaway Git worktree and the
|
|
95
|
+
# host-controlled selected-project allowlist.
|
|
81
96
|
docker volume create uai-asdf-data
|
|
82
|
-
mkdir -p /tmp/uai-smoke/workspace/demo
|
|
97
|
+
mkdir -p /tmp/uai-smoke/workspace/demo/.git
|
|
83
98
|
printf 'nodejs 22.11.0\n' > /tmp/uai-smoke/workspace/demo/.tool-versions
|
|
84
|
-
printf '{"name":"demo","packageManager":"
|
|
99
|
+
printf '{"name":"demo","packageManager":"pnpm@10.15.0"}\n' \
|
|
85
100
|
> /tmp/uai-smoke/workspace/demo/package.json
|
|
101
|
+
printf 'demo\n' > /tmp/uai-smoke/runtime-projects
|
|
86
102
|
|
|
87
|
-
# 3.
|
|
103
|
+
# 3. Materialize before the app exists, the way task-up does.
|
|
104
|
+
docker run --rm --user node --workdir /workspace \
|
|
105
|
+
-v /tmp/uai-smoke/workspace:/workspace:ro \
|
|
106
|
+
-v /tmp/uai-smoke/runtime-projects:/run/uai/runtime-projects:ro \
|
|
107
|
+
-v uai-asdf-data:/opt/asdf-data:rw \
|
|
108
|
+
uai-standard:dev /usr/bin/timeout --signal=TERM --kill-after=10s 600s \
|
|
109
|
+
/usr/local/bin/uai-materialize-runtimes
|
|
110
|
+
|
|
111
|
+
# 4. Run the container the way compose would (service `app`, workspace bind,
|
|
88
112
|
# asdf cache volume, code-server port published on loopback).
|
|
89
113
|
docker run -d --name uai-smoke \
|
|
90
114
|
-v /tmp/uai-smoke/workspace:/workspace \
|
|
91
|
-
-v uai-
|
|
115
|
+
-v /tmp/uai-smoke/runtime-projects:/run/uai/runtime-projects:ro \
|
|
116
|
+
-v uai-asdf-data:/opt/asdf-data:ro \
|
|
117
|
+
-e ASDF_SKIP_RESHIM=1 \
|
|
118
|
+
-e NPM_CONFIG_PREFIX=/home/node/.local/share/uai-npm \
|
|
119
|
+
-e npm_config_prefix=/home/node/.local/share/uai-npm \
|
|
120
|
+
-e NPM_CONFIG_CACHE=/home/node/.cache/uai-npm \
|
|
121
|
+
-e npm_config_cache=/home/node/.cache/uai-npm \
|
|
122
|
+
-e NPM_CONFIG_LOGS_DIR=/home/node/.cache/uai-npm/_logs \
|
|
123
|
+
-e npm_config_logs_dir=/home/node/.cache/uai-npm/_logs \
|
|
124
|
+
-e NPM_CONFIG_UMASK=0002 -e npm_config_umask=0002 \
|
|
125
|
+
-e 'PATH=/opt/asdf-data/shims:/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/share/uai-npm/bin' \
|
|
92
126
|
-p 127.0.0.1::8080 \
|
|
93
127
|
uai-standard:dev
|
|
94
128
|
|
|
95
|
-
#
|
|
129
|
+
# 5. Prove isolation and sanity-check the baked toolchain (bare exec — no
|
|
130
|
+
# profile sourced).
|
|
131
|
+
docker exec uai-smoke sh -c \
|
|
132
|
+
'! touch /opt/asdf-data/.uai-app-write-probe 2>/dev/null && test "$(npm config get prefix)" = /home/node/.local/share/uai-npm'
|
|
96
133
|
docker exec uai-smoke bash -lc 'asdf --version && node -v && python --version'
|
|
97
134
|
docker exec uai-smoke claude --version
|
|
98
135
|
docker exec uai-smoke codex --version
|
|
99
136
|
|
|
100
|
-
#
|
|
137
|
+
# 6. Run uai-init: task-local deps + code-server on 8080.
|
|
101
138
|
docker exec uai-smoke /usr/local/bin/uai-init
|
|
102
139
|
docker exec uai-smoke bash -lc 'curl -sf http://127.0.0.1:8080/healthz || \
|
|
103
140
|
curl -sf -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/'
|
|
104
141
|
|
|
105
|
-
#
|
|
142
|
+
# 7. Tear down.
|
|
106
143
|
docker rm -f uai-smoke
|
|
107
144
|
```
|
|
108
145
|
|
|
109
|
-
Expected: step
|
|
110
|
-
|
|
146
|
+
Expected: step 5 proves the app cannot write the shared cache and npm resolves
|
|
147
|
+
the private prefix, then prints the asdf version and root-default Node +
|
|
148
|
+
Python; `claude` / `codex` resolve; step 6 launches code-server bound to
|
|
111
149
|
`0.0.0.0:8080` and the HTTP probe returns a 2xx/3xx.
|
|
112
150
|
|
|
113
151
|
## Notes / gotchas
|
|
@@ -118,5 +156,12 @@ Expected: step 4 prints the asdf version and the root-default Node + Python;
|
|
|
118
156
|
- **First-use asdf cost.** The first task needing a given runtime version pays
|
|
119
157
|
the download/build time; results persist on `uai-asdf-data` for every later
|
|
120
158
|
task on the host.
|
|
121
|
-
-
|
|
122
|
-
|
|
159
|
+
- **Rolling recovery is preserve-and-recreate.** Before shared maintenance, an
|
|
160
|
+
upgraded host stops legacy writable/non-current apps and leaves their
|
|
161
|
+
worktrees intact. Docker mount modes are immutable, so only an exact current
|
|
162
|
+
read-only app can recover in place; explicit Resume recreates anything else.
|
|
163
|
+
- **Ordinary `uai-init` failures remain best-effort.** A failing task-local
|
|
164
|
+
dependency install logs and is skipped so one bad repo never blocks editor
|
|
165
|
+
startup. Runtime materialization is separate and fail-closed before the app
|
|
166
|
+
starts; a workspace runtime-authority violation exits 78 so the host can
|
|
167
|
+
quarantine the container instead of treating it as ready.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.33.0
|