@agentshouse/kit 0.1.0-alpha.36 → 0.1.0-alpha.38

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.
@@ -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
- "${DOCKER_RUN[@]:0:2}" -i "${DOCKER_RUN[@]:2}" login "$@"
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
- "${DOCKER_RUN[@]:0:2}" ${PUBLISHED[@]+"${PUBLISHED[@]}"} "${DOCKER_RUN[@]:2}" login "$@" --host-browser | while IFS= read -r line; do
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 [[ "$RUNTIME" == 'Docker Desktop' ]]; then
230
- establish_docker_desktop "$CHIP"
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
- establish_docker_engine
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
- case "$DOCKER_PLATFORM" in
237
- linux/x86_64|linux/amd64) DOCKER_ARCH=amd64 ;;
238
- linux/aarch64|linux/arm64) DOCKER_ARCH=arm64 ;;
239
- *) refuse "unsupported $RUNTIME platform $DOCKER_PLATFORM" ;;
240
- esac
241
- [[ "$DOCKER_ARCH" == "$ARCH" ]] || refuse "host linux/$ARCH does not match $RUNTIME linux/$DOCKER_ARCH"
242
- if ((FORWARDING)); then
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,60 @@ 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":"intermittent","execution_authority":"allow","migrators":[],"instances":[],"clients":[],"host":null,"skills":%s}\n' "$SKILLS" > "$KIT_HOME/kit.json"
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
- if [[ "$RUNTIME" == 'Docker Desktop' && "$HOUSE" == https://* ]]; then
283
- NETWORK=()
284
- LOGIN_PORT=$((49152 + RANDOM % 16384))
285
- PUBLISHED=(--publish "127.0.0.1:$LOGIN_PORT:$LOGIN_PORT" --env "HOUSE_KIT_LOGIN_PORT=$LOGIN_PORT")
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 docker container inspect "$NAME" >/dev/null 2>&1; then
290
- INSTALLED_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$NAME")
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
- PLACED=()
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
- if [[ "$INSTALLED_IMAGE" != "$IMAGE" ]]; then
328
- docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
329
- if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
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
- docker start "$NAME" >/dev/null
343
- printf 'House Kit restarted for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
560
+ resume_kit
344
561
  fi
345
562
  exit 0
346
563
  fi
347
564
 
348
- docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
349
-
350
- if [[ -f "$KIT_HOME/credential" ]]; then
351
- "${DOCKER_RUN[@]}" credential check
565
+ if ((NATIVE)); then
566
+ establish_native_packages
567
+ if [[ "$(installed_image)" != "$IMAGE" ]]; then
568
+ stage_native_kit
569
+ place_native_kit
570
+ fi
352
571
  else
353
- LOGIN=()
354
- if ((HOUSE_SELECTED)); then LOGIN+=(--house "$HOUSE"); fi
355
- if ((MANUAL)); then LOGIN+=(--manual); fi
356
- login_kit ${LOGIN[@]+"${LOGIN[@]}"}
572
+ docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
357
573
  fi
358
-
359
- "${DOCKER_RUN[@]}" apply
360
- start_resident
361
- printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
574
+ connect_kit
@@ -364,7 +364,7 @@ function Connect-Kit {
364
364
  $entry = @('--entrypoint', 'house')
365
365
  }
366
366
  $terminal = @()
367
- if (-not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected) { $terminal = @('-t') }
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/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.36",
3
+ "version": "0.1.0-alpha.38",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/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.36/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/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.36/connect-windows.ps1)))",
17
+ "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/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.36/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-windows.ps1"
21
21
  },
22
22
  "workspace": {
23
23
  "argument": "--workspace",
@@ -0,0 +1,104 @@
1
+ # Configuration
2
+
3
+ `kit.json` in Kit home is the one configuration file. Every section below is
4
+ required; a file that omits one is refused.
5
+
6
+ ```json
7
+ {
8
+ "availability": "continuous",
9
+ "execution_authority": "allow",
10
+ "migrators": [],
11
+ "instances": [
12
+ {
13
+ "id": "acme",
14
+ "preset": "custom",
15
+ "services": [
16
+ {
17
+ "service": "notes",
18
+ "source_ref": "src_acme_notes",
19
+ "rooms": ["work-room"],
20
+ "backfill": { "days": 30 },
21
+ "connector": {
22
+ "command": ["/usr/local/bin/acme-export", "--ndjson"],
23
+ "timeout_ms": 120000,
24
+ "environment": { "ACME_REGION": "eu" },
25
+ "secrets": {
26
+ "ACME_TOKEN": { "environment": "ACME_TOKEN_SOURCE" },
27
+ "ACME_KEY": { "file": "/run/secrets/acme-key" }
28
+ }
29
+ }
30
+ }
31
+ ]
32
+ }
33
+ ],
34
+ "clients": [
35
+ {
36
+ "kind": "claude-code",
37
+ "settings": "/home/ada/.claude/settings.json"
38
+ }
39
+ ],
40
+ "host": null,
41
+ "skills": true
42
+ }
43
+ ```
44
+
45
+ - `availability`: `"continuous"` for a computer that stays online,
46
+ `"intermittent"` for a laptop.
47
+ - `execution_authority`: `"allow"` or `"refuse"`, this computer's answer to
48
+ every permission request an agent makes.
49
+ - `instances`: your own sources, each with a `custom` connector (below).
50
+ - `clients`: agent clients outside Kit whose settings get House's session-start
51
+ hook. `claude-code` is the one supported kind; declare `[]` for none.
52
+ - `migrators`: `[]` unless you run a migrator.
53
+ - `host`: `null` unless this computer reports its capacity to House.
54
+ - `skills`: `true` accepts the Skill set, `false` declines it. The connect
55
+ command sets it.
56
+
57
+ Agents and agent CLIs are chosen in House, not here. Send your changes to
58
+ House with:
59
+
60
+ ```sh
61
+ kit apply
62
+ ```
63
+
64
+ ## Secrets
65
+
66
+ Never write a secret into `kit.json`. Under `secrets`, name an environment
67
+ variable (`{ "environment": "NAME" }`) or a file (`{ "file": "/path" }`); Kit
68
+ passes its value to the connector only.
69
+
70
+ ## Custom connectors
71
+
72
+ A connector is your own command. Kit runs it without a shell, with only `PATH`,
73
+ `environment` and the resolved secrets, and never gives it a House credential.
74
+ It must finish within `timeout_ms` (at most 120000).
75
+
76
+ Kit writes one request line to its standard input:
77
+
78
+ ```json
79
+ { "contract": "2026-08-22", "instance": "acme", "cursor": null, "limit": 100 }
80
+ ```
81
+
82
+ The command prints NDJSON: `item` lines, then at most one `cursor` line.
83
+ `cursor` in the next request is the last one it printed.
84
+
85
+ ```
86
+ {"type":"item","namespace":"acme/notes","native_object_id":"note-91","native_version":"7","observed_at":"2026-08-20T09:15:00.000Z","body":"the note","author":"ada","url":"https://acme.example/notes/91","attachments":[{"url":"https://acme.example/files/1","name":"plan.pdf"}]}
87
+ {"type":"cursor","cursor":"page-2"}
88
+ ```
89
+
90
+ `namespace`, `native_object_id`, `observed_at` and `body` are required. An
91
+ attachment is a `url`, or a local `file` with its exact `path`, `bytes`,
92
+ `sha256` and optional `media_type`. A malformed line, a non-zero exit or a
93
+ timeout rejects the whole run and nothing is delivered.
94
+
95
+ `backfill` bounds the first capture: `{ "days": 30 }`, `{ "count": 500 }` or
96
+ `{ "all": true }`.
97
+
98
+ ```sh
99
+ kit source capture acme # capture every service of an instance now
100
+ kit source capture acme notes # one service
101
+ kit source status acme # health, cursor, gaps and unsaved files
102
+ kit source retry-files acme # save files a capture could not save
103
+ kit source reset acme notes # start again from the backfill boundary
104
+ ```
package/docs/google.md ADDED
@@ -0,0 +1,33 @@
1
+ # Google sources
2
+
3
+ You add Gmail, Calendar and Drive sources in the House App: an account name you
4
+ choose, the service, the Rooms it goes to, and how much history the first
5
+ capture takes. Nothing is configured on this computer.
6
+
7
+ Each service captures, read-only:
8
+
9
+ - Gmail: every message except Spam and Trash.
10
+ - Calendar: the account's primary calendar.
11
+ - Drive: the files the account owns.
12
+
13
+ ## Signing in
14
+
15
+ Kit ships no Google OAuth client. Each account signs in with your own:
16
+
17
+ 1. In your Google Cloud project, create an OAuth client of type **Desktop app**
18
+ and enable the API of each service you use.
19
+ 2. When House asks, enter the client id and secret on its page.
20
+ 3. Approve Google's consent. Google then sends the browser to a
21
+ `http://127.0.0.1:<port>/` address that does not load; copy that whole
22
+ address and paste it into the House page.
23
+
24
+ Kit then captures each signed-in source every 15 minutes.
25
+
26
+ ```sh
27
+ kit source capture work # capture every source of the account now
28
+ kit source capture work gmail # one service
29
+ kit source status work
30
+ ```
31
+
32
+ If `kit source status` reports `api_disabled`, enable the API it names in your
33
+ Google Cloud project.