@agentshouse/kit 0.1.0-alpha.37 → 0.1.0-alpha.39
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 +2 -2
- package/bin/connect-linux.sh +292 -78
- package/bin/connect-windows.ps1 +1 -1
- package/bin/kit.js +18 -2
- package/bootstrap.json +5 -5
- package/docs/linux.md +31 -0
- package/docs/windows.md +1 -2
- package/package.json +1 -1
- package/src/configuration.js +1 -2
- package/src/contract.js +6 -2
- package/src/workspace.js +194 -48
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ The Skill set Kit installs is Matt Pocock's
|
|
|
13
13
|
Linux (`amd64`, `arm64`) or macOS (Apple silicon, Intel):
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
16
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Windows 11 (`amd64`), in PowerShell:
|
|
20
20
|
|
|
21
21
|
```powershell
|
|
22
|
-
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
22
|
+
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The command sets up Docker if it is missing, creates `~/.house-kit` (Kit home)
|
package/bin/connect-linux.sh
CHANGED
|
@@ -21,6 +21,15 @@ RUNTIME='Docker Engine'
|
|
|
21
21
|
OPENER=xdg-open
|
|
22
22
|
NETWORK=(--network host)
|
|
23
23
|
PUBLISHED=()
|
|
24
|
+
AVAILABILITY=intermittent
|
|
25
|
+
NATIVE=0
|
|
26
|
+
NATIVE_RELEASE=26.04
|
|
27
|
+
NATIVE_ARCHITECTURE=amd64
|
|
28
|
+
NATIVE_PACKAGES=(git tmux)
|
|
29
|
+
NATIVE_PREFIX=/opt/house-kit
|
|
30
|
+
NATIVE_SERVICE=house-kit.service
|
|
31
|
+
NATIVE_UNIT=/etc/systemd/system/house-kit.service
|
|
32
|
+
NATIVE_WORKSPACE=/agents/house
|
|
24
33
|
|
|
25
34
|
refuse() {
|
|
26
35
|
printf 'kit_bootstrap_refused: %s\n' "$1" >&2
|
|
@@ -32,13 +41,25 @@ bind_mount() {
|
|
|
32
41
|
printf 'type=bind,"src=%s",dst=%s' "$source" "$2"
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
native_kit() {
|
|
45
|
+
PATH="$NATIVE_PREFIX/bin:$PATH" HOUSE_KIT_HOME="$KIT_HOME" "$NATIVE_PREFIX/bin/node" "$NATIVE_PREFIX/bin/$1" "${@:2}"
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
login_kit() {
|
|
36
49
|
if [[ " $* " == *' --manual '* ]]; then
|
|
37
|
-
|
|
50
|
+
if ((NATIVE)); then
|
|
51
|
+
native_kit kit login "$@"
|
|
52
|
+
else
|
|
53
|
+
"${DOCKER_RUN[@]:0:2}" -i "${DOCKER_RUN[@]:2}" login "$@"
|
|
54
|
+
fi
|
|
38
55
|
return
|
|
39
56
|
fi
|
|
40
57
|
command -v "$OPENER" >/dev/null || refuse 'no host browser opener; rerun with --manual'
|
|
41
|
-
|
|
58
|
+
if ((NATIVE)); then
|
|
59
|
+
native_kit kit login "$@" --host-browser
|
|
60
|
+
else
|
|
61
|
+
"${DOCKER_RUN[@]:0:2}" ${PUBLISHED[@]+"${PUBLISHED[@]}"} "${DOCKER_RUN[@]:2}" login "$@" --host-browser
|
|
62
|
+
fi | while IFS= read -r line; do
|
|
42
63
|
if [[ "$line" == 'Open '* ]]; then
|
|
43
64
|
printf 'Opening House Login in the host browser.\n'
|
|
44
65
|
"$OPENER" "${line#Open }" >/dev/null 2>&1 || refuse 'host browser did not open; rerun with --manual'
|
|
@@ -169,6 +190,204 @@ establish_docker_engine() {
|
|
|
169
190
|
docker_answers || refuse "Docker Engine does not answer: $(docker_cause)"
|
|
170
191
|
}
|
|
171
192
|
|
|
193
|
+
check_native_host() {
|
|
194
|
+
local distribution='' release='' described=macOS
|
|
195
|
+
if [[ "$RUNTIME" == 'Docker Engine' ]]; then
|
|
196
|
+
described=Linux
|
|
197
|
+
if [[ -r /etc/os-release ]]; then
|
|
198
|
+
distribution=$(. /etc/os-release && printf '%s' "${ID:-}")
|
|
199
|
+
release=$(. /etc/os-release && printf '%s' "${VERSION_ID:-}")
|
|
200
|
+
described=$(. /etc/os-release && printf '%s' "${PRETTY_NAME:-$described}")
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
[[ "$distribution" == ubuntu && "$release" == "$NATIVE_RELEASE" && "$ARCH" == "$NATIVE_ARCHITECTURE" ]] ||
|
|
204
|
+
refuse "--linux runs only on Ubuntu $NATIVE_RELEASE LTS on $NATIVE_ARCHITECTURE, not $described on $ARCH"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
establish_native_packages() {
|
|
209
|
+
local present
|
|
210
|
+
present=$(installed_packages "${NATIVE_PACKAGES[@]}" | wc -l)
|
|
211
|
+
((present == ${#NATIVE_PACKAGES[@]})) && return
|
|
212
|
+
printf 'Installing git and tmux from Ubuntu'\''s apt repository; sudo may ask for your password.\n'
|
|
213
|
+
elevated apt-get update -qq >/dev/null || refuse 'apt could not refresh its package lists'
|
|
214
|
+
elevated apt-get install -y -qq "${NATIVE_PACKAGES[@]}" >/dev/null || refuse 'apt could not install git and tmux'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
stage_native_kit() {
|
|
218
|
+
local copy
|
|
219
|
+
NATIVE_STAGE=$(mktemp -d) || refuse 'no temporary directory for the Kit package'
|
|
220
|
+
trap 'rm -rf "$NATIVE_STAGE"' EXIT
|
|
221
|
+
docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
|
|
222
|
+
copy=$(docker create --platform "linux/$ARCH" "$IMAGE") || refuse 'the Kit image could not be opened'
|
|
223
|
+
mkdir "$NATIVE_STAGE/bin" "$NATIVE_STAGE/lib"
|
|
224
|
+
if ! docker cp --quiet "$copy:/usr/local/bin/node" "$NATIVE_STAGE/bin/node" ||
|
|
225
|
+
! docker cp --quiet "$copy:/usr/local/lib/node_modules" "$NATIVE_STAGE/lib/node_modules"; then
|
|
226
|
+
docker rm "$copy" >/dev/null 2>&1 || true
|
|
227
|
+
refuse 'the Kit package could not be copied from its image'
|
|
228
|
+
fi
|
|
229
|
+
docker rm "$copy" >/dev/null
|
|
230
|
+
ln -s ../lib/node_modules/@agentshouse/kit/bin/kit.js "$NATIVE_STAGE/bin/kit"
|
|
231
|
+
ln -s ../lib/node_modules/@agentshouse/kit/src/session-bin/house "$NATIVE_STAGE/bin/house"
|
|
232
|
+
ln -s ../lib/node_modules/npm/bin/npm-cli.js "$NATIVE_STAGE/bin/npm"
|
|
233
|
+
ln -s ../lib/node_modules/npm/bin/npx-cli.js "$NATIVE_STAGE/bin/npx"
|
|
234
|
+
printf '%s\n' "$IMAGE" > "$NATIVE_STAGE/image"
|
|
235
|
+
chmod 0755 "$NATIVE_STAGE" "$NATIVE_STAGE/bin" "$NATIVE_STAGE/lib"
|
|
236
|
+
chmod 0644 "$NATIVE_STAGE/image"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
place_native_kit() {
|
|
240
|
+
printf 'Installing House Kit in %s; sudo may ask for your password.\n' "$NATIVE_PREFIX"
|
|
241
|
+
elevated rm -rf "$NATIVE_PREFIX" && elevated mv "$NATIVE_STAGE" "$NATIVE_PREFIX" && elevated chown -R root:root "$NATIVE_PREFIX" ||
|
|
242
|
+
refuse "House Kit could not be installed in $NATIVE_PREFIX"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
start_native_service() {
|
|
246
|
+
printf '[Unit]\nDescription=House Kit\nWants=network-online.target\nAfter=network-online.target docker.service\n\n[Service]\nUser=%s\nWorkingDirectory=%s\nEnvironment=PATH=%s/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nExecStart=%s/bin/node %s/bin/kit resident\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\n' \
|
|
247
|
+
"$(id -un)" "$NATIVE_WORKSPACE" "$NATIVE_PREFIX" "$NATIVE_PREFIX" "$NATIVE_PREFIX" | elevated tee "$NATIVE_UNIT" >/dev/null &&
|
|
248
|
+
elevated chmod 0644 "$NATIVE_UNIT" || refuse "$NATIVE_UNIT could not be written"
|
|
249
|
+
elevated systemctl daemon-reload || refuse 'systemd could not read the House Kit service'
|
|
250
|
+
elevated systemctl --quiet enable --now "$NATIVE_SERVICE" || refuse 'the House Kit service could not be started'
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
run_kit() {
|
|
254
|
+
if ((NATIVE)); then
|
|
255
|
+
native_kit kit "$@"
|
|
256
|
+
else
|
|
257
|
+
"${DOCKER_RUN[@]}" "$@"
|
|
258
|
+
fi
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
kit_running() {
|
|
262
|
+
if ((NATIVE)); then
|
|
263
|
+
systemctl is-active --quiet "$NATIVE_SERVICE"
|
|
264
|
+
else
|
|
265
|
+
[[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]
|
|
266
|
+
fi
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
kit_installed() {
|
|
270
|
+
if ((NATIVE)); then
|
|
271
|
+
[[ -f "$NATIVE_UNIT" ]]
|
|
272
|
+
else
|
|
273
|
+
docker container inspect "$NAME" >/dev/null 2>&1
|
|
274
|
+
fi
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
installed_image() {
|
|
278
|
+
if ((NATIVE == 0)); then
|
|
279
|
+
docker inspect --format '{{.Config.Image}}' "$NAME"
|
|
280
|
+
elif [[ -f "$NATIVE_PREFIX/image" ]]; then
|
|
281
|
+
cat "$NATIVE_PREFIX/image"
|
|
282
|
+
fi
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
check_installation() {
|
|
286
|
+
if ((NATIVE)); then
|
|
287
|
+
grep -qx "User=$(id -un)" "$NATIVE_UNIT" || refuse 'House Kit runs natively on this host for another account'
|
|
288
|
+
return
|
|
289
|
+
fi
|
|
290
|
+
[[ "$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$NAME")" == unless-stopped ]] || refuse 'the installed Kit has a different lifecycle'
|
|
291
|
+
[[ "$(docker inspect --format '{{len .Mounts}}' "$NAME")" == 2 ]] || refuse 'the installed Kit has unexpected mounts'
|
|
292
|
+
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$KIT_HOME" ]] || refuse 'the installed Kit uses another Kit home'
|
|
293
|
+
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed Kit home is not a writable bind mount'
|
|
294
|
+
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$WORKSPACE" ]] || refuse 'the installed Kit uses another workspace root'
|
|
295
|
+
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed workspace is not a writable bind mount'
|
|
296
|
+
[[ "$(docker inspect --format '{{index .Config.Labels "agentshouse.house"}}' "$NAME")" == "$HOUSE" ]] || refuse 'the installed Kit uses another House origin'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
forward_kit() {
|
|
300
|
+
if ((NATIVE)); then
|
|
301
|
+
native_kit "$FORWARDED" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
302
|
+
return
|
|
303
|
+
fi
|
|
304
|
+
local placed=() entry=() terminal=() here
|
|
305
|
+
if [[ "$FORWARDED" == house ]]; then
|
|
306
|
+
here=$(pwd -P)
|
|
307
|
+
[[ "$here" == "$WORKSPACE" || "$here" == "$WORKSPACE"/* ]] || refuse "the current directory is outside the workspace $WORKSPACE"
|
|
308
|
+
placed=(--workdir "/agents/house${here#"$WORKSPACE"}")
|
|
309
|
+
entry=(--entrypoint house)
|
|
310
|
+
fi
|
|
311
|
+
if [[ -t 0 && -t 1 ]]; then terminal=(-t); fi
|
|
312
|
+
if [[ -t 0 && ! -t 1 ]]; then
|
|
313
|
+
ECHOED=$(stty -g)
|
|
314
|
+
trap 'stty "$ECHOED"' EXIT
|
|
315
|
+
stty -echo
|
|
316
|
+
fi
|
|
317
|
+
if kit_running; then
|
|
318
|
+
docker exec -i ${terminal[@]+"${terminal[@]}"} ${placed[@]+"${placed[@]}"} "$NAME" "$FORWARDED" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
319
|
+
else
|
|
320
|
+
"${DOCKER_RUN[@]:0:2}" -i ${terminal[@]+"${terminal[@]}"} ${placed[@]+"${placed[@]}"} ${entry[@]+"${entry[@]}"} "${DOCKER_RUN[@]:2}" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
321
|
+
fi
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
start_kit() {
|
|
325
|
+
if ((NATIVE)); then
|
|
326
|
+
start_native_service
|
|
327
|
+
else
|
|
328
|
+
start_resident
|
|
329
|
+
fi
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
update_kit() {
|
|
333
|
+
local stopped=0
|
|
334
|
+
if ((NATIVE)); then
|
|
335
|
+
stage_native_kit
|
|
336
|
+
else
|
|
337
|
+
docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
|
|
338
|
+
fi
|
|
339
|
+
if ! kit_running; then
|
|
340
|
+
stopped=1
|
|
341
|
+
elif ((NATIVE)); then
|
|
342
|
+
native_kit kit resident drain
|
|
343
|
+
else
|
|
344
|
+
docker exec "$NAME" kit resident drain
|
|
345
|
+
fi
|
|
346
|
+
if ((NATIVE)); then
|
|
347
|
+
elevated systemctl stop "$NATIVE_SERVICE" || refuse 'the House Kit service could not be stopped'
|
|
348
|
+
place_native_kit
|
|
349
|
+
else
|
|
350
|
+
docker rm -f "$NAME" >/dev/null
|
|
351
|
+
fi
|
|
352
|
+
rm -f "$KIT_HOME/state/update-drain"
|
|
353
|
+
((stopped == 0)) || run_kit credential check
|
|
354
|
+
start_kit
|
|
355
|
+
printf 'House Kit updated for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
resume_kit() {
|
|
359
|
+
run_kit credential check
|
|
360
|
+
if kit_running; then
|
|
361
|
+
printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
362
|
+
return
|
|
363
|
+
fi
|
|
364
|
+
if ((NATIVE)); then
|
|
365
|
+
elevated systemctl start "$NATIVE_SERVICE" || refuse 'the House Kit service could not be started'
|
|
366
|
+
else
|
|
367
|
+
docker start "$NAME" >/dev/null
|
|
368
|
+
fi
|
|
369
|
+
printf 'House Kit restarted for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
connect_kit() {
|
|
373
|
+
if [[ -f "$KIT_HOME/credential" ]]; then
|
|
374
|
+
run_kit credential check
|
|
375
|
+
else
|
|
376
|
+
local login=()
|
|
377
|
+
if ((HOUSE_SELECTED)); then login+=(--house "$HOUSE"); fi
|
|
378
|
+
if ((MANUAL)); then login+=(--manual); fi
|
|
379
|
+
login_kit ${login[@]+"${login[@]}"}
|
|
380
|
+
fi
|
|
381
|
+
run_kit apply
|
|
382
|
+
start_kit
|
|
383
|
+
printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if [[ "${1:-}" == --linux && ( "${2:-}" == kit || "${2:-}" == house ) ]]; then
|
|
387
|
+
NATIVE=1
|
|
388
|
+
shift
|
|
389
|
+
fi
|
|
390
|
+
|
|
172
391
|
if [[ "${1:-}" == kit || "${1:-}" == house ]]; then
|
|
173
392
|
FORWARDING=1
|
|
174
393
|
FORWARDED="$1"
|
|
@@ -199,9 +418,20 @@ while (($#)); do
|
|
|
199
418
|
SKILLS=false
|
|
200
419
|
shift
|
|
201
420
|
;;
|
|
421
|
+
--linux)
|
|
422
|
+
NATIVE=1
|
|
423
|
+
shift
|
|
424
|
+
;;
|
|
202
425
|
*) refuse "unknown argument $1" ;;
|
|
203
426
|
esac
|
|
204
427
|
done
|
|
428
|
+
if ((NATIVE)); then
|
|
429
|
+
((WORKSPACE_SELECTED == 0)) || refuse "--linux keeps its workspace at $NATIVE_WORKSPACE; --workspace is for a container"
|
|
430
|
+
WORKSPACE=$NATIVE_WORKSPACE
|
|
431
|
+
AVAILABILITY=continuous
|
|
432
|
+
elif [[ -e "$NATIVE_UNIT" ]]; then
|
|
433
|
+
refuse 'House Kit runs natively on this host; rerun this bootstrap with --linux'
|
|
434
|
+
fi
|
|
205
435
|
|
|
206
436
|
[[ "$IMAGE" =~ ^ghcr\.io/agentshouse/kit@sha256:[0-9a-f]{64}$ ]] || refuse 'this bootstrap has not been published with an immutable image digest'
|
|
207
437
|
case "$(uname -s)" in
|
|
@@ -225,21 +455,36 @@ case "$(uname -s)" in
|
|
|
225
455
|
;;
|
|
226
456
|
*) refuse "unsupported host $(uname -s); this bootstrap is for Linux and macOS" ;;
|
|
227
457
|
esac
|
|
458
|
+
((NATIVE == 0)) || check_native_host
|
|
228
459
|
[[ "$WORKSPACE" == /* ]] || refuse 'workspace root must be an absolute path'
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
DOCKER_SYSTEM=$(docker info --format '{{.OperatingSystem}}' 2>/dev/null) || refuse "Docker Desktop does not answer: $(docker_cause)"
|
|
232
|
-
[[ "$DOCKER_SYSTEM" == 'Docker Desktop' ]] || refuse "Docker answers from $DOCKER_SYSTEM, not Docker Desktop; stop that engine or switch Docker to Docker Desktop, then rerun this bootstrap"
|
|
460
|
+
if ((NATIVE && FORWARDING)); then
|
|
461
|
+
[[ -f "$NATIVE_UNIT" ]] || refuse 'House Kit is not installed; run this bootstrap with --linux first'
|
|
233
462
|
else
|
|
234
|
-
|
|
463
|
+
if [[ "$RUNTIME" == 'Docker Desktop' ]]; then
|
|
464
|
+
establish_docker_desktop "$CHIP"
|
|
465
|
+
DOCKER_SYSTEM=$(docker info --format '{{.OperatingSystem}}' 2>/dev/null) || refuse "Docker Desktop does not answer: $(docker_cause)"
|
|
466
|
+
[[ "$DOCKER_SYSTEM" == 'Docker Desktop' ]] || refuse "Docker answers from $DOCKER_SYSTEM, not Docker Desktop; stop that engine or switch Docker to Docker Desktop, then rerun this bootstrap"
|
|
467
|
+
else
|
|
468
|
+
establish_docker_engine
|
|
469
|
+
fi
|
|
470
|
+
case "$DOCKER_PLATFORM" in
|
|
471
|
+
linux/x86_64|linux/amd64) DOCKER_ARCH=amd64 ;;
|
|
472
|
+
linux/aarch64|linux/arm64) DOCKER_ARCH=arm64 ;;
|
|
473
|
+
*) refuse "unsupported $RUNTIME platform $DOCKER_PLATFORM" ;;
|
|
474
|
+
esac
|
|
475
|
+
[[ "$DOCKER_ARCH" == "$ARCH" ]] || refuse "host linux/$ARCH does not match $RUNTIME linux/$DOCKER_ARCH"
|
|
235
476
|
fi
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
[[ "$
|
|
242
|
-
|
|
477
|
+
if ((NATIVE)); then
|
|
478
|
+
if ((FORWARDING == 0)) && docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
479
|
+
refuse "House Kit runs in the container $NAME on this computer; remove that container, then rerun this bootstrap with --linux"
|
|
480
|
+
fi
|
|
481
|
+
[[ ! -L "${NATIVE_WORKSPACE%/*}" ]] || refuse "${NATIVE_WORKSPACE%/*} is a symbolic link"
|
|
482
|
+
if [[ ! -e "$NATIVE_WORKSPACE" && ! -L "$NATIVE_WORKSPACE" ]]; then
|
|
483
|
+
printf 'Creating %s for the Agents on this host; sudo may ask for your password.\n' "$NATIVE_WORKSPACE"
|
|
484
|
+
{ [[ -d "${NATIVE_WORKSPACE%/*}" ]] || elevated install -d -m 0755 "${NATIVE_WORKSPACE%/*}"; } &&
|
|
485
|
+
elevated install -d -m 0700 -o "$(id -u)" -g "$(id -g)" "$NATIVE_WORKSPACE" || refuse "$NATIVE_WORKSPACE could not be created"
|
|
486
|
+
fi
|
|
487
|
+
elif ((FORWARDING)); then
|
|
243
488
|
docker container inspect "$NAME" >/dev/null 2>&1 || refuse 'House Kit is not installed; run this bootstrap without arguments first'
|
|
244
489
|
WORKSPACE=$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Source}}{{end}}{{end}}' "$NAME")
|
|
245
490
|
fi
|
|
@@ -270,92 +515,61 @@ fi
|
|
|
270
515
|
[[ "$HOUSE" == https://* || "$HOUSE" == http://127.0.0.1:* || "$HOUSE" == http://localhost:* ]] || refuse 'House origin must use HTTPS or local loopback HTTP'
|
|
271
516
|
if ((FORWARDING == 0)); then
|
|
272
517
|
if [[ ! -f "$KIT_HOME/kit.json" ]]; then
|
|
273
|
-
printf '{"availability":"
|
|
518
|
+
printf '{"availability":"%s","execution_authority":"allow","migrators":[],"instances":[],"clients":[],"host":null,"skills":%s}\n' "$AVAILABILITY" "$SKILLS" > "$KIT_HOME/kit.json"
|
|
274
519
|
elif grep -Eq '"skills"[[:space:]]*:' "$KIT_HOME/kit.json"; then
|
|
275
520
|
sed -E -i.old "s/(\"skills\"[[:space:]]*:[[:space:]]*)(true|false)/\\1$SKILLS/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
276
521
|
else
|
|
277
522
|
sed -E -i.old "1s/^([[:space:]]*)\\{/\\1{\"skills\":$SKILLS,/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
278
523
|
fi
|
|
524
|
+
if ((NATIVE)); then
|
|
525
|
+
sed -E -i.old "s/(\"availability\"[[:space:]]*:[[:space:]]*)\"[^\"]*\"/\\1\"$AVAILABILITY\"/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
526
|
+
fi
|
|
279
527
|
rm -f "$KIT_HOME/kit.json.old"
|
|
280
528
|
grep -Eq "\"skills\"[[:space:]]*:[[:space:]]*$SKILLS" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
529
|
+
((NATIVE == 0)) || grep -Eq "\"availability\"[[:space:]]*:[[:space:]]*\"$AVAILABILITY\"" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
281
530
|
fi
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
531
|
+
|
|
532
|
+
if ((NATIVE)); then
|
|
533
|
+
RERUN='with --linux'
|
|
534
|
+
else
|
|
535
|
+
RERUN='without arguments'
|
|
536
|
+
if [[ "$RUNTIME" == 'Docker Desktop' && "$HOUSE" == https://* ]]; then
|
|
537
|
+
NETWORK=()
|
|
538
|
+
LOGIN_PORT=$((49152 + RANDOM % 16384))
|
|
539
|
+
PUBLISHED=(--publish "127.0.0.1:$LOGIN_PORT:$LOGIN_PORT" --env "HOUSE_KIT_LOGIN_PORT=$LOGIN_PORT")
|
|
540
|
+
fi
|
|
541
|
+
DOCKER_RUN=(docker run --rm ${NETWORK[@]+"${NETWORK[@]}"} --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
|
|
286
542
|
fi
|
|
287
|
-
DOCKER_RUN=(docker run --rm ${NETWORK[@]+"${NETWORK[@]}"} --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
|
|
288
543
|
|
|
289
|
-
if
|
|
290
|
-
|
|
291
|
-
((FORWARDING == 0)) || [[ "$INSTALLED_IMAGE" == "$IMAGE" ]] || refuse 'an installed Kit uses a different image; rerun this bootstrap without arguments to update it'
|
|
292
|
-
[[ "$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$NAME")" == unless-stopped ]] || refuse 'the installed Kit has a different lifecycle'
|
|
293
|
-
[[ "$(docker inspect --format '{{len .Mounts}}' "$NAME")" == 2 ]] || refuse 'the installed Kit has unexpected mounts'
|
|
294
|
-
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$KIT_HOME" ]] || refuse 'the installed Kit uses another Kit home'
|
|
295
|
-
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed Kit home is not a writable bind mount'
|
|
296
|
-
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$WORKSPACE" ]] || refuse 'the installed Kit uses another workspace root'
|
|
297
|
-
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed workspace is not a writable bind mount'
|
|
298
|
-
[[ "$(docker inspect --format '{{index .Config.Labels "agentshouse.house"}}' "$NAME")" == "$HOUSE" ]] || refuse 'the installed Kit uses another House origin'
|
|
544
|
+
if kit_installed; then
|
|
545
|
+
check_installation
|
|
299
546
|
[[ -f "$KIT_HOME/credential" && -f "$KIT_HOME/environment" && -f "$KIT_HOME/house" ]] || refuse 'the installed Kit has no enrolled authority'
|
|
300
547
|
if ((FORWARDING)); then
|
|
548
|
+
[[ "$(installed_image)" == "$IMAGE" ]] || refuse "an installed Kit uses a different image; rerun this bootstrap $RERUN to update it"
|
|
301
549
|
if [[ "$FORWARDED" == kit && "${FORWARD[0]:-}" == login ]]; then
|
|
302
550
|
login_kit ${FORWARD[@]+"${FORWARD[@]:1}"}
|
|
303
551
|
exit 0
|
|
304
552
|
fi
|
|
305
|
-
|
|
306
|
-
ENTRY=()
|
|
307
|
-
if [[ "$FORWARDED" == house ]]; then
|
|
308
|
-
HERE=$(pwd -P)
|
|
309
|
-
[[ "$HERE" == "$WORKSPACE" || "$HERE" == "$WORKSPACE"/* ]] || refuse "the current directory is outside the workspace $WORKSPACE"
|
|
310
|
-
PLACED=(--workdir "/agents/house${HERE#"$WORKSPACE"}")
|
|
311
|
-
ENTRY=(--entrypoint house)
|
|
312
|
-
fi
|
|
313
|
-
TERMINAL=()
|
|
314
|
-
if [[ -t 0 && -t 1 ]]; then TERMINAL=(-t); fi
|
|
315
|
-
if [[ -t 0 && ! -t 1 ]]; then
|
|
316
|
-
ECHOED=$(stty -g)
|
|
317
|
-
trap 'stty "$ECHOED"' EXIT
|
|
318
|
-
stty -echo
|
|
319
|
-
fi
|
|
320
|
-
if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
|
|
321
|
-
docker exec -i ${TERMINAL[@]+"${TERMINAL[@]}"} ${PLACED[@]+"${PLACED[@]}"} "$NAME" "$FORWARDED" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
322
|
-
else
|
|
323
|
-
"${DOCKER_RUN[@]:0:2}" -i ${TERMINAL[@]+"${TERMINAL[@]}"} ${PLACED[@]+"${PLACED[@]}"} ${ENTRY[@]+"${ENTRY[@]}"} "${DOCKER_RUN[@]:2}" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
324
|
-
fi
|
|
553
|
+
forward_kit
|
|
325
554
|
exit
|
|
326
555
|
fi
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
docker exec "$NAME" kit resident drain
|
|
331
|
-
fi
|
|
332
|
-
docker rm -f "$NAME" >/dev/null
|
|
333
|
-
rm -f "$KIT_HOME/state/update-drain"
|
|
334
|
-
start_resident
|
|
335
|
-
printf 'House Kit updated for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
336
|
-
exit 0
|
|
337
|
-
fi
|
|
338
|
-
"${DOCKER_RUN[@]}" credential check
|
|
339
|
-
if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
|
|
340
|
-
printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
556
|
+
((NATIVE == 0)) || establish_native_packages
|
|
557
|
+
if [[ "$(installed_image)" != "$IMAGE" ]]; then
|
|
558
|
+
update_kit
|
|
341
559
|
else
|
|
342
|
-
|
|
343
|
-
printf 'House Kit restarted for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
560
|
+
resume_kit
|
|
344
561
|
fi
|
|
562
|
+
((NATIVE == 0)) || native_kit kit workspace update
|
|
345
563
|
exit 0
|
|
346
564
|
fi
|
|
347
565
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if [[
|
|
351
|
-
|
|
566
|
+
if ((NATIVE)); then
|
|
567
|
+
establish_native_packages
|
|
568
|
+
if [[ "$(installed_image)" != "$IMAGE" ]]; then
|
|
569
|
+
stage_native_kit
|
|
570
|
+
place_native_kit
|
|
571
|
+
fi
|
|
352
572
|
else
|
|
353
|
-
|
|
354
|
-
if ((HOUSE_SELECTED)); then LOGIN+=(--house "$HOUSE"); fi
|
|
355
|
-
if ((MANUAL)); then LOGIN+=(--manual); fi
|
|
356
|
-
login_kit ${LOGIN[@]+"${LOGIN[@]}"}
|
|
573
|
+
docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
|
|
357
574
|
fi
|
|
358
|
-
|
|
359
|
-
"${DOCKER_RUN[@]}" apply
|
|
360
|
-
start_resident
|
|
361
|
-
printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
575
|
+
connect_kit
|
package/bin/connect-windows.ps1
CHANGED
|
@@ -364,7 +364,7 @@ function Connect-Kit {
|
|
|
364
364
|
$entry = @('--entrypoint', 'house')
|
|
365
365
|
}
|
|
366
366
|
$terminal = @()
|
|
367
|
-
if (-not [Console]::IsInputRedirected
|
|
367
|
+
if (-not [Console]::IsInputRedirected) { $terminal = @('-t') }
|
|
368
368
|
if ((Read-Inspect '{{.State.Running}}') -ceq 'true') {
|
|
369
369
|
& docker exec -i @terminal @placed $Name $Forwarded @Forward
|
|
370
370
|
} else {
|
package/bin/kit.js
CHANGED
|
@@ -13,7 +13,7 @@ import { runResident, runWorkers } from "../src/worker.js";
|
|
|
13
13
|
import { openSession, releaseControl, requestDrain } from "../src/house.js";
|
|
14
14
|
import { reportHost } from "../src/host.js";
|
|
15
15
|
import { signInAgent, updateAgents } from "../src/agents.js";
|
|
16
|
-
import { serveWorkspaces } from "../src/workspace.js";
|
|
16
|
+
import { serveWorkspaces, updateWorkspaces } from "../src/workspace.js";
|
|
17
17
|
import { captureRepository } from "../src/repository.js";
|
|
18
18
|
import { bootstrap, pull, rebootstrap, submit } from "../src/sync.js";
|
|
19
19
|
import { formatMount, formatUnmount, mountPackage, unmountSubtree } from "../src/mount.js";
|
|
@@ -35,7 +35,7 @@ const LOGIN_USAGE =
|
|
|
35
35
|
const HOST_USAGE = "usage: kit host report [--config <path>]";
|
|
36
36
|
const AGENTS_USAGE =
|
|
37
37
|
"usage: kit agents update [--config <path>] | kit agents sign-in <kind> [--config <path>]";
|
|
38
|
-
const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>]";
|
|
38
|
+
const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>] | kit workspace update";
|
|
39
39
|
const SOURCE_USAGE =
|
|
40
40
|
"usage: kit source <capture|status> <account|instance> [service] [--config <path>]\n" +
|
|
41
41
|
" kit source <reset|retry-files> <instance> [service] [--config <path>]\n" +
|
|
@@ -895,6 +895,21 @@ async function runWorkspaceHost(args, directory) {
|
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
897
|
|
|
898
|
+
async function runWorkspaceUpdate(args, directory) {
|
|
899
|
+
declared(args, [], 0, { exact: true });
|
|
900
|
+
const answered = await updateWorkspaces({
|
|
901
|
+
house: await storedHouse(directory),
|
|
902
|
+
configuration: join(directory, "kit.json"),
|
|
903
|
+
});
|
|
904
|
+
for (const { workspace, running } of answered.updated) {
|
|
905
|
+
write(running ? `Updated workspace ${workspace}.` : `Updated workspace ${workspace}; it stays stopped.`);
|
|
906
|
+
}
|
|
907
|
+
for (const { workspace, cause } of answered.unchanged) {
|
|
908
|
+
write(`Workspace ${workspace} is unchanged: ${cause}`);
|
|
909
|
+
}
|
|
910
|
+
if (answered.unchanged.length > 0) process.exitCode = 1;
|
|
911
|
+
}
|
|
912
|
+
|
|
898
913
|
async function runTelegramConnect(args, directory) {
|
|
899
914
|
declared(args, [], 0, { exact: true });
|
|
900
915
|
const house = await storedHouse(directory);
|
|
@@ -1027,6 +1042,7 @@ async function main() {
|
|
|
1027
1042
|
if (command === "workspace") {
|
|
1028
1043
|
const [action, ...rest] = args;
|
|
1029
1044
|
if (action === "host") return runWorkspaceHost(rest, directory);
|
|
1045
|
+
if (action === "update") return runWorkspaceUpdate(rest, directory);
|
|
1030
1046
|
throw new Error(WORKSPACE_USAGE);
|
|
1031
1047
|
}
|
|
1032
1048
|
if (command === "telegram") {
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.39",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"windows": {
|
|
17
|
-
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
17
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))",
|
|
18
18
|
"asset": {
|
|
19
19
|
"name": "connect-windows.ps1",
|
|
20
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1"
|
|
21
21
|
},
|
|
22
22
|
"workspace": {
|
|
23
23
|
"argument": "--workspace",
|
package/docs/linux.md
CHANGED
|
@@ -44,3 +44,34 @@ bash <(curl -fsSL …/connect-linux.sh) house git push --owner
|
|
|
44
44
|
|
|
45
45
|
`--house http://127.0.0.1:<port>` or `--house http://localhost:<port>` runs Kit
|
|
46
46
|
on the host network so it can reach a House served on this computer.
|
|
47
|
+
|
|
48
|
+
## An Always-on server that hosts containers
|
|
49
|
+
|
|
50
|
+
An Always-on Ubuntu server that will run persistent container Environments
|
|
51
|
+
connects natively instead of in a container. Run the command with `--linux`:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
bash <(curl -fsSL …/connect-linux.sh) --linux
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It needs Ubuntu 26.04 LTS on `amd64`; any other host is refused. Every other
|
|
58
|
+
computer takes the ordinary command. It sets up:
|
|
59
|
+
|
|
60
|
+
- Docker Engine, as above, and `git` and `tmux` from Ubuntu's apt repository.
|
|
61
|
+
- `/opt/house-kit`: House Kit and the Node.js it runs on, copied from the
|
|
62
|
+
release's image and owned by root.
|
|
63
|
+
- `~/.house-kit`: Kit home, readable only by you.
|
|
64
|
+
- `/agents/house`: your Agents' workspace, readable only by you.
|
|
65
|
+
- `house-kit.service`: the systemd service that runs Kit as you and starts it
|
|
66
|
+
with the server.
|
|
67
|
+
|
|
68
|
+
The server connects as a continuous Environment. Put `--linux` before `kit` or
|
|
69
|
+
`house` to run a Kit command, as in
|
|
70
|
+
`bash <(curl -fsSL …/connect-linux.sh) --linux kit apply`, and run a newer
|
|
71
|
+
release's command with `--linux` to update it, as above.
|
|
72
|
+
|
|
73
|
+
The same run updates the container Environments the server hosts to that
|
|
74
|
+
release. A running one waits, and is named, until its own Agent sessions end;
|
|
75
|
+
run the command again then. A stopped one is updated and stays stopped. Each
|
|
76
|
+
keeps its files, sign-ins and CPU, memory and disk limits; only what was
|
|
77
|
+
installed outside its workspace and Kit home is not kept.
|
package/docs/windows.md
CHANGED
|
@@ -30,8 +30,7 @@ refused.
|
|
|
30
30
|
|
|
31
31
|
- Append `kit` and a command to run it, as on [Linux](linux.md). Each run
|
|
32
32
|
leaves its exit status in `$LASTEXITCODE`.
|
|
33
|
-
-
|
|
34
|
-
output on the terminal: with output redirected, what you type is echoed.
|
|
33
|
+
- A command whose output is redirected also writes its prompts and errors there.
|
|
35
34
|
- A House on this computer needs host networking enabled in Docker Desktop's
|
|
36
35
|
settings.
|
|
37
36
|
- This bootstrap has not yet been run on a real Windows computer.
|
package/package.json
CHANGED
package/src/configuration.js
CHANGED
|
@@ -160,12 +160,11 @@ function maximaOf(value, declaration) {
|
|
|
160
160
|
|
|
161
161
|
function hostOf(value, declaration) {
|
|
162
162
|
if (value === null) return null;
|
|
163
|
-
const host = closed(value, ["workspace", "maxima"
|
|
163
|
+
const host = closed(value, ["workspace", "maxima"], declaration);
|
|
164
164
|
if (host.maxima === undefined) refuse(`${declaration}.maxima`);
|
|
165
165
|
return {
|
|
166
166
|
workspace: absolutePath(host.workspace, `${declaration}.workspace`),
|
|
167
167
|
maxima: maximaOf(host.maxima, `${declaration}.maxima`),
|
|
168
|
-
image: host.image === undefined || host.image === null ? null : text(host.image, `${declaration}.image`, 255),
|
|
169
168
|
};
|
|
170
169
|
}
|
|
171
170
|
|
package/src/contract.js
CHANGED
|
@@ -276,16 +276,20 @@ export const KIT_WORKSPACES_ACQUIRE_PATH = "/kit/workspaces/acquire";
|
|
|
276
276
|
export const WORKSPACE_ACQUIRE_MAX = 10;
|
|
277
277
|
export const WORKSPACE_POLL_MS = 2_000;
|
|
278
278
|
export const WORKSPACE_LABEL = "house-kit.workspace";
|
|
279
|
-
export const
|
|
280
|
-
export const WORKSPACE_KIT_HOME = "/workspace/.house-kit";
|
|
279
|
+
export const WORKSPACE_KIT_HOME = "/kit-home";
|
|
281
280
|
export const WORKSPACE_DISK_IMAGE = "disk.img";
|
|
282
281
|
export const WORKSPACE_DISK_MOUNT = "mnt";
|
|
282
|
+
export const WORKSPACE_DISK_KIT_HOME = "kit-home";
|
|
283
|
+
export const WORKSPACE_DISK_HOUSE = "house";
|
|
284
|
+
export const WORKSPACE_SELECTED_IMAGE = "/opt/house-kit/image";
|
|
285
|
+
export const WORKSPACE_RELEASE_IMAGE = /^ghcr\.io\/agentshouse\/kit@sha256:[0-9a-f]{64}$/;
|
|
283
286
|
export const WORKSPACE_MOUNT_OPTIONS = "loop,nosuid,nodev";
|
|
284
287
|
export const HOST_MOUNT_INFO = "/proc/self/mountinfo";
|
|
285
288
|
export const WORKSPACE_SYSTEM_RESERVE_BYTES = 512 * 1024 * 1024;
|
|
286
289
|
export const WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
|
|
287
290
|
export const WORKSPACE_READY_ATTEMPTS = 60;
|
|
288
291
|
export const WORKSPACE_READY_POLL_MS = 1_000;
|
|
292
|
+
export const WORKSPACE_LOCK_POLL_MS = 1_000;
|
|
289
293
|
export const WORKSPACE_STOP_TIMEOUT_SECONDS = 10;
|
|
290
294
|
export const WORKSPACE_OPERATION_WITHDRAWN = "hosted_environment_operation_not_found";
|
|
291
295
|
export const CGROUP_CONTROLLERS = "/sys/fs/cgroup/cgroup.controllers";
|
package/src/workspace.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { access, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { createServer } from "node:net";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { readConfiguration } from "./configuration.js";
|
|
4
5
|
import {
|
|
5
6
|
CGROUP_CONTROLLERS,
|
|
6
7
|
HOST_MOUNT_INFO,
|
|
8
|
+
LAUNCH_DIRECTORY_BASE,
|
|
7
9
|
LOOP_CONTROL,
|
|
8
10
|
WORKSPACE_ACQUIRE_MAX,
|
|
9
11
|
WORKSPACE_COMMAND_TIMEOUT_MS,
|
|
12
|
+
WORKSPACE_DISK_HOUSE,
|
|
10
13
|
WORKSPACE_DISK_IMAGE,
|
|
14
|
+
WORKSPACE_DISK_KIT_HOME,
|
|
11
15
|
WORKSPACE_DISK_MOUNT,
|
|
12
16
|
WORKSPACE_KIT_HOME,
|
|
13
17
|
WORKSPACE_MOUNT_OPTIONS,
|
|
14
18
|
WORKSPACE_LABEL,
|
|
15
|
-
|
|
19
|
+
WORKSPACE_LOCK_POLL_MS,
|
|
16
20
|
WORKSPACE_OPERATION_WITHDRAWN,
|
|
17
21
|
WORKSPACE_POLL_MS,
|
|
18
22
|
WORKSPACE_READY_ATTEMPTS,
|
|
19
23
|
WORKSPACE_READY_POLL_MS,
|
|
24
|
+
WORKSPACE_RELEASE_IMAGE,
|
|
25
|
+
WORKSPACE_SELECTED_IMAGE,
|
|
20
26
|
WORKSPACE_STOP_TIMEOUT_SECONDS,
|
|
21
27
|
WORKSPACE_SYSTEM_RESERVE_BYTES,
|
|
22
28
|
} from "./contract.js";
|
|
@@ -27,10 +33,13 @@ import {
|
|
|
27
33
|
openSession,
|
|
28
34
|
releaseControl,
|
|
29
35
|
reportWorkspaceOutcome,
|
|
36
|
+
updateDrain,
|
|
30
37
|
} from "./house.js";
|
|
31
38
|
import { houseFailed, KitRefusal } from "./refusal.js";
|
|
32
39
|
|
|
33
40
|
const REQUIRED_CONTROLLERS = ["cpu", "memory"];
|
|
41
|
+
const PROVISIONED_DISK = "/disk";
|
|
42
|
+
const STOPPED_STATES = ["created", "exited"];
|
|
34
43
|
const MEMINFO = "/proc/meminfo";
|
|
35
44
|
const MEM_AVAILABLE = /^MemAvailable:\s+(\d+)\s+kB$/m;
|
|
36
45
|
|
|
@@ -106,30 +115,35 @@ export function mountedPaths(document) {
|
|
|
106
115
|
);
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
export function workspaceCreateArguments(identity,
|
|
118
|
+
export function workspaceCreateArguments(identity, workspace, image, house, root) {
|
|
119
|
+
const disk = workspaceMountPath(root, identity);
|
|
110
120
|
return [
|
|
111
121
|
"create",
|
|
112
122
|
"--name",
|
|
113
123
|
identity,
|
|
114
124
|
"--label",
|
|
115
|
-
`${WORKSPACE_LABEL}=${
|
|
125
|
+
`${WORKSPACE_LABEL}=${workspace.environment}`,
|
|
116
126
|
"--cpus",
|
|
117
|
-
(
|
|
127
|
+
(workspace.maxima.cpu_millicores / 1000).toFixed(3),
|
|
118
128
|
"--memory",
|
|
119
|
-
`${
|
|
129
|
+
`${workspace.maxima.memory_bytes}b`,
|
|
120
130
|
"--memory-swap",
|
|
121
|
-
`${
|
|
131
|
+
`${workspace.maxima.memory_bytes}b`,
|
|
122
132
|
"--restart",
|
|
123
133
|
"no",
|
|
134
|
+
...(new URL(house).protocol === "https:" ? [] : ["--network", "host"]),
|
|
124
135
|
"--mount",
|
|
125
|
-
`type=bind,source=${
|
|
136
|
+
`type=bind,source=${disk}/${WORKSPACE_DISK_KIT_HOME},target=${WORKSPACE_KIT_HOME}`,
|
|
137
|
+
"--mount",
|
|
138
|
+
`type=bind,source=${disk}/${WORKSPACE_DISK_HOUSE},target=${LAUNCH_DIRECTORY_BASE}`,
|
|
139
|
+
"--env",
|
|
140
|
+
`HOME=${WORKSPACE_KIT_HOME}`,
|
|
126
141
|
"--env",
|
|
127
142
|
`HOUSE_KIT_HOME=${WORKSPACE_KIT_HOME}`,
|
|
128
143
|
"--env",
|
|
129
|
-
|
|
130
|
-
"--workdir",
|
|
131
|
-
WORKSPACE_MOUNT,
|
|
144
|
+
"HOUSE_KIT_CONTAINER=1",
|
|
132
145
|
image,
|
|
146
|
+
"resident",
|
|
133
147
|
];
|
|
134
148
|
}
|
|
135
149
|
|
|
@@ -198,7 +212,7 @@ export async function workspacePrerequisites(host, image) {
|
|
|
198
212
|
if (formatter.status !== 0) {
|
|
199
213
|
return {
|
|
200
214
|
supported: false,
|
|
201
|
-
refusal: `the
|
|
215
|
+
refusal: `the selected Kit image ${image} carries no mkfs.ext4, so no volume ceiling can be created`,
|
|
202
216
|
};
|
|
203
217
|
}
|
|
204
218
|
return { supported: true, runtime: `docker ${version.stdout.trim()}` };
|
|
@@ -215,34 +229,45 @@ function bootstrapConfiguration() {
|
|
|
215
229
|
});
|
|
216
230
|
}
|
|
217
231
|
|
|
218
|
-
function
|
|
232
|
+
function diskArguments(root, identity, image, script) {
|
|
219
233
|
return [
|
|
220
234
|
"run",
|
|
221
235
|
"--rm",
|
|
222
236
|
"--interactive",
|
|
223
237
|
"--mount",
|
|
224
|
-
`type=bind,source=${workspaceMountPath(root, identity)},target=${
|
|
238
|
+
`type=bind,source=${workspaceMountPath(root, identity)},target=${PROVISIONED_DISK}`,
|
|
225
239
|
"--entrypoint",
|
|
226
240
|
"sh",
|
|
227
241
|
image,
|
|
228
242
|
"-c",
|
|
229
|
-
|
|
243
|
+
script,
|
|
230
244
|
];
|
|
231
245
|
}
|
|
232
246
|
|
|
233
|
-
async function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
247
|
+
async function onDisk(host, settings, identity, script, stdin = "") {
|
|
248
|
+
const written = await docker(host, diskArguments(settings.root, identity, settings.image, script), stdin);
|
|
249
|
+
if (written.status !== 0) {
|
|
250
|
+
throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function provisionWorkspaceState(host, settings, identity, operation) {
|
|
255
|
+
const home = `${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`;
|
|
256
|
+
const house = `${PROVISIONED_DISK}/${WORKSPACE_DISK_HOUSE}`;
|
|
257
|
+
const files = [
|
|
258
|
+
...(operation.authority === undefined ? [] : [["credential", operation.authority.credential]]),
|
|
259
|
+
["kit.json", bootstrapConfiguration()],
|
|
260
|
+
["house", settings.house],
|
|
261
|
+
["environment", operation.environment],
|
|
262
|
+
];
|
|
263
|
+
for (const [name, content] of files) {
|
|
264
|
+
await onDisk(
|
|
239
265
|
host,
|
|
240
|
-
|
|
241
|
-
|
|
266
|
+
settings,
|
|
267
|
+
identity,
|
|
268
|
+
`set -e; mkdir -p ${home} ${house}; chmod 700 ${home} ${house}; cat > ${home}/${name}; chmod 600 ${home}/${name}`,
|
|
269
|
+
`${content}\n`,
|
|
242
270
|
);
|
|
243
|
-
if (written.status !== 0) {
|
|
244
|
-
throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
|
|
245
|
-
}
|
|
246
271
|
}
|
|
247
272
|
}
|
|
248
273
|
|
|
@@ -289,9 +314,7 @@ export async function createWorkspace(host, settings, operation) {
|
|
|
289
314
|
}
|
|
290
315
|
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
291
316
|
if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
|
|
292
|
-
|
|
293
|
-
await provisionWorkspaceState(host, settings.root, identity, settings.image, operation.authority);
|
|
294
|
-
}
|
|
317
|
+
await provisionWorkspaceState(host, settings, identity, operation);
|
|
295
318
|
const created = await docker(
|
|
296
319
|
host,
|
|
297
320
|
workspaceCreateArguments(identity, operation, settings.image, settings.house, settings.root),
|
|
@@ -312,9 +335,42 @@ async function workspacePresent(host, identity) {
|
|
|
312
335
|
return inspected.status === 0 ? inspected.stdout.trim() : null;
|
|
313
336
|
}
|
|
314
337
|
|
|
338
|
+
async function heldWorkspace(host, identity) {
|
|
339
|
+
const inspected = await docker(host, [
|
|
340
|
+
"container",
|
|
341
|
+
"inspect",
|
|
342
|
+
"--format",
|
|
343
|
+
`{{.State.Status}} {{.Config.Image}} {{index .Config.Labels "${WORKSPACE_LABEL}"}} {{.HostConfig.NanoCpus}} {{.HostConfig.Memory}}`,
|
|
344
|
+
identity,
|
|
345
|
+
]);
|
|
346
|
+
if (inspected.status !== 0) return null;
|
|
347
|
+
const [state, image, environment, nanoCpus, memory] = inspected.stdout.trim().split(/\s+/);
|
|
348
|
+
return {
|
|
349
|
+
state,
|
|
350
|
+
image,
|
|
351
|
+
environment,
|
|
352
|
+
maxima: { cpu_millicores: Number(nanoCpus) / 1_000_000, memory_bytes: Number(memory) },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function replaceWorkspace(host, settings, identity, held) {
|
|
357
|
+
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
358
|
+
if (mountRefusal !== null) return mountRefusal;
|
|
359
|
+
await onDisk(host, settings, identity, `rm -f ${updateDrain(`${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`)}`);
|
|
360
|
+
const removed = await docker(host, ["rm", "--force", identity]);
|
|
361
|
+
if (removed.status !== 0) return `the workspace was not replaced: ${removed.stderr.trim()}`;
|
|
362
|
+
const created = await docker(
|
|
363
|
+
host,
|
|
364
|
+
workspaceCreateArguments(identity, held, settings.image, settings.house, settings.root),
|
|
365
|
+
);
|
|
366
|
+
if (created.status !== 0) return `the workspace was not recreated: ${created.stderr.trim()}`;
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
315
370
|
export async function startWorkspace(host, settings, operation) {
|
|
316
371
|
const identity = operation.workspace ?? workspaceIdentity(operation);
|
|
317
|
-
|
|
372
|
+
const held = await heldWorkspace(host, identity);
|
|
373
|
+
if (held === null) return { outcome: "absent" };
|
|
318
374
|
const available = memoryAvailable(await host.read(MEMINFO));
|
|
319
375
|
if (available - operation.maxima.memory_bytes < WORKSPACE_SYSTEM_RESERVE_BYTES) {
|
|
320
376
|
return {
|
|
@@ -324,6 +380,10 @@ export async function startWorkspace(host, settings, operation) {
|
|
|
324
380
|
}
|
|
325
381
|
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
326
382
|
if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
|
|
383
|
+
if (STOPPED_STATES.includes(held.state) && held.image !== settings.image) {
|
|
384
|
+
const refusal = await replaceWorkspace(host, settings, identity, held);
|
|
385
|
+
if (refusal !== null) return { outcome: "failed", refusal };
|
|
386
|
+
}
|
|
327
387
|
const started = await docker(host, ["start", identity]);
|
|
328
388
|
if (started.status !== 0) {
|
|
329
389
|
return { outcome: "failed", refusal: `the workspace did not start: ${started.stderr.trim()}` };
|
|
@@ -425,37 +485,123 @@ export async function reconcileWorkspaces(host, authorized) {
|
|
|
425
485
|
);
|
|
426
486
|
const stopped = [];
|
|
427
487
|
for (const managed of await managedWorkspaces(host)) {
|
|
428
|
-
if (managed.state !== "running") continue;
|
|
429
488
|
if (expected.get(managed.environment) === "running") continue;
|
|
430
|
-
const settled = await
|
|
431
|
-
"
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
489
|
+
const settled = await exclusively(host, managed.name, async () => {
|
|
490
|
+
if ((await workspacePresent(host, managed.name)) !== "running") return false;
|
|
491
|
+
const stopping = await docker(host, [
|
|
492
|
+
"stop",
|
|
493
|
+
"--timeout",
|
|
494
|
+
String(WORKSPACE_STOP_TIMEOUT_SECONDS),
|
|
495
|
+
managed.name,
|
|
496
|
+
]);
|
|
497
|
+
return stopping.status === 0;
|
|
498
|
+
});
|
|
499
|
+
if (settled) stopped.push(managed.name);
|
|
437
500
|
}
|
|
438
501
|
return stopped;
|
|
439
502
|
}
|
|
440
503
|
|
|
504
|
+
function workspaceLock(identity) {
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const lock = createServer();
|
|
507
|
+
lock.once("error", reject);
|
|
508
|
+
lock.listen({ path: `\0${WORKSPACE_LABEL}/${identity}` }, () => resolve(lock));
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function exclusively(host, identity, act) {
|
|
513
|
+
let lock = null;
|
|
514
|
+
while (lock === null) {
|
|
515
|
+
try {
|
|
516
|
+
lock = await workspaceLock(identity);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (error.code !== "EADDRINUSE") throw error;
|
|
519
|
+
await host.wait(WORKSPACE_LOCK_POLL_MS);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
return await act();
|
|
524
|
+
} finally {
|
|
525
|
+
await new Promise((resolve) => lock.close(resolve));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
441
529
|
export async function performOperation(host, settings, operation) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
530
|
+
const selected = operation.kind === "stop" ? settings : { ...settings, image: await selectedImage(host) };
|
|
531
|
+
return exclusively(host, operation.workspace ?? workspaceIdentity(operation), () => {
|
|
532
|
+
if (operation.kind === "stop") return stopWorkspace(host, operation);
|
|
533
|
+
if (operation.kind === "create") return createWorkspace(host, selected, operation);
|
|
534
|
+
if (operation.kind === "start") return startWorkspace(host, selected, operation);
|
|
535
|
+
return deleteWorkspace(host, selected, operation);
|
|
536
|
+
});
|
|
446
537
|
}
|
|
447
538
|
|
|
448
|
-
|
|
539
|
+
async function selectedImage(host) {
|
|
540
|
+
let image;
|
|
541
|
+
try {
|
|
542
|
+
image = (await host.read(WORKSPACE_SELECTED_IMAGE)).trim();
|
|
543
|
+
} catch {
|
|
544
|
+
image = null;
|
|
545
|
+
}
|
|
546
|
+
if (image === null || !WORKSPACE_RELEASE_IMAGE.test(image)) {
|
|
547
|
+
throw new KitRefusal("kit_workspace_host_unconfigured", WORKSPACE_SELECTED_IMAGE);
|
|
548
|
+
}
|
|
549
|
+
return image;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function hostSettings(options) {
|
|
449
553
|
const configuration = await readConfiguration(options.configuration);
|
|
450
|
-
if (configuration.host === null
|
|
554
|
+
if (configuration.host === null) {
|
|
451
555
|
throw new KitRefusal("kit_workspace_host_unconfigured", options.configuration);
|
|
452
556
|
}
|
|
557
|
+
return { root: configuration.host.workspace, house: options.house };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function updateWorkspace(host, settings, name) {
|
|
561
|
+
try {
|
|
562
|
+
const held = await heldWorkspace(host, name);
|
|
563
|
+
if (held === null || held.image === settings.image) return null;
|
|
564
|
+
const running = !STOPPED_STATES.includes(held.state);
|
|
565
|
+
if (running) {
|
|
566
|
+
const drained = await docker(host, ["exec", name, "kit", "resident", "drain"]);
|
|
567
|
+
if (drained.status !== 0) {
|
|
568
|
+
return { workspace: name, cause: drained.stdout.trim() || drained.stderr.trim() };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const refusal = await replaceWorkspace(host, settings, name, held);
|
|
572
|
+
if (refusal !== null) return { workspace: name, cause: refusal };
|
|
573
|
+
if (running) {
|
|
574
|
+
const started = await docker(host, ["start", name]);
|
|
575
|
+
if (started.status !== 0) {
|
|
576
|
+
return { workspace: name, cause: `the workspace did not start: ${started.stderr.trim()}` };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { workspace: name, running };
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (!(error instanceof KitRefusal)) throw error;
|
|
582
|
+
return { workspace: name, cause: error.message };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export async function updateWorkspaces(options, dependencies = {}) {
|
|
453
587
|
const host = adapter(dependencies);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
};
|
|
588
|
+
const updated = [];
|
|
589
|
+
const unchanged = [];
|
|
590
|
+
const managed = await managedWorkspaces(host);
|
|
591
|
+
if (managed.length === 0) return { updated, unchanged };
|
|
592
|
+
const settings = { ...(await hostSettings(options)), image: await selectedImage(host) };
|
|
593
|
+
for (const { name } of managed) {
|
|
594
|
+
const answer = await exclusively(host, name, () => updateWorkspace(host, settings, name));
|
|
595
|
+
if (answer === null) continue;
|
|
596
|
+
if (answer.cause === undefined) updated.push(answer);
|
|
597
|
+
else unchanged.push(answer);
|
|
598
|
+
}
|
|
599
|
+
return { updated, unchanged };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export async function serveWorkspaces(options, dependencies = {}) {
|
|
603
|
+
const host = adapter(dependencies);
|
|
604
|
+
const settings = await hostSettings(options);
|
|
459
605
|
const session = await openSession(options, dependencies);
|
|
460
606
|
const settled = [];
|
|
461
607
|
const withdrawn = [];
|