@ouro.bot/cli 0.1.0-alpha.734 → 0.1.0-alpha.736

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/changelog.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.736",
6
+ "changes": [
7
+ "Fix the packaged Unraid image build to honor the repository peer dependency install contract.",
8
+ "Keep release lock metadata aligned and gate publication on building the packed Unraid image."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.735",
13
+ "changes": [
14
+ "Repair Mendelow Cloud Butler for Sanctuary's Unraid legacy adoption with an explicit source-to-target machine identity migration that validates the complete credential envelope before applying only in-memory canonical clones while preserving the original source for recovery and the shipped durable approvals contract.",
15
+ "Split adoption into resumable preparation, hidden-input provider authentication, strict live GLM/Gemini readiness, and a noninteractive final install that revalidates the exact legacy container and image immediately before mutation.",
16
+ "Make legacy evidence capture restart-safe with exact container/image-bound validation, private atomic no-overwrite publication, deterministic partial-snapshot reuse, and fail-closed rejection of conflicting or unsafe evidence.",
17
+ "Supersede 0.1.0-alpha.734 before deployment; it must not be built, imported, or installed on Sanctuary."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.734",
6
22
  "changes": [
@@ -12,7 +12,7 @@ RUN apt-get update \
12
12
 
13
13
  WORKDIR /opt/ouro
14
14
  COPY package.json npm-shrinkwrap.json ./
15
- RUN npm ci --omit=dev --workspaces=false --no-audit --no-fund
15
+ RUN npm ci --omit=dev --legacy-peer-deps --workspaces=false --no-audit --no-fund
16
16
  COPY dist ./dist
17
17
  COPY assets ./assets
18
18
  COPY skills ./skills
@@ -314,40 +314,176 @@ Effective-spec audit helper:
314
314
  PREPARE_WRONG_FILE_MODE=$(find "$PREPARE_RUNTIME_ROOT" "$PREPARE_AGENT_ROOT" -type f ! -perm 0600 -print -quit) || return $?
315
315
  test -z "$PREPARE_WRONG_FILE_MODE" || return $?
316
316
  }
317
- install_from_legacy_staging() {
318
- ADOPTION_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
319
- ADOPTION_NAME_COUNTS=$(printf '%s\n' "$ADOPTION_CONTAINER_NAMES" | awk '
320
- $0 == "ouro-butler" { production++ }
317
+ validate_sanctuary_legacy_staging() {
318
+ EXPECTED_LEGACY_CONTAINER_ID=${1-}
319
+ EXPECTED_LEGACY_IMAGE_ID=${2-}
320
+ LEGACY_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
321
+ LEGACY_NAME_COUNTS=$(printf '%s\n' "$LEGACY_CONTAINER_NAMES" | awk '
322
+ /butler/ { butlers++ }
321
323
  $0 == "ouro-butler-staging" { staging++ }
322
- $0 == "ouro-butler-rollback" { rollback++ }
323
- $0 == "ouro-butler-legacy-evidence" { legacy++ }
324
- END {
325
- printf "%d %d %d %d", production + 0, staging + 0, rollback + 0, legacy + 0
326
- }
324
+ END { printf "%d %d", butlers + 0, staging + 0 }
327
325
  ') || return $?
328
- test "$ADOPTION_NAME_COUNTS" = "0 1 0 0" || return $?
326
+ test "$LEGACY_NAME_COUNTS" = "1 1" || return $?
329
327
  assert_only_running_butler ouro-butler-staging || return $?
328
+ LEGACY_STAGING_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-staging) || return $?
329
+ test "$LEGACY_STAGING_RUNNING" = true || return $?
330
+ LEGACY_STAGING_CONTAINER_ID=$(docker inspect --format '{{.Id}}' ouro-butler-staging) || return $?
331
+ case "$LEGACY_STAGING_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
332
+ test "${#LEGACY_STAGING_CONTAINER_ID}" -eq 64 || return 1
330
333
  LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
331
334
  validate_exact_image_id "$LEGACY_STAGING_IMAGE_ID" || return $?
335
+ if test -n "$EXPECTED_LEGACY_CONTAINER_ID"; then
336
+ test "$LEGACY_STAGING_CONTAINER_ID" = "$EXPECTED_LEGACY_CONTAINER_ID" || return 1
337
+ test "$LEGACY_STAGING_IMAGE_ID" = "$EXPECTED_LEGACY_IMAGE_ID" || return 1
338
+ fi
339
+ }
340
+ prepare_sanctuary_legacy_adoption() {
341
+ IMAGE_ID=$1
332
342
  validate_exact_image_id "$IMAGE_ID" || return $?
343
+ validate_sanctuary_legacy_staging || return $?
344
+ PREPARED_LEGACY_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
345
+ PREPARED_LEGACY_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
333
346
  prepare_canonical_sanctuary_roots "$IMAGE_ID" || return $?
334
- bootstrap_sanctuary_vault "$IMAGE_ID" /mnt/user/appdata/ouro-butler/runtime/container-credentials.json || return $?
335
- LEGACY_EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/legacy-evidence
336
- LEGACY_EVIDENCE_DIR="$LEGACY_EVIDENCE_ROOT/${LEGACY_STAGING_IMAGE_ID#sha256:}"
337
- install -d -m 0700 -o 0 -g 0 "$LEGACY_EVIDENCE_ROOT" || return $?
338
- mkdir -m 0700 "$LEGACY_EVIDENCE_DIR" || return $?
339
- docker container inspect ouro-butler-staging >"$LEGACY_EVIDENCE_DIR/container.json" || return $?
340
- docker image inspect "$LEGACY_STAGING_IMAGE_ID" >"$LEGACY_EVIDENCE_DIR/image.json" || return $?
341
- chmod 0600 "$LEGACY_EVIDENCE_DIR/container.json" "$LEGACY_EVIDENCE_DIR/image.json" || return $?
342
- sync -f "$LEGACY_EVIDENCE_DIR/container.json" || return $?
343
- sync -f "$LEGACY_EVIDENCE_DIR/image.json" || return $?
347
+ bootstrap_sanctuary_vault "$IMAGE_ID" \
348
+ /mnt/user/appdata/ouro-butler/runtime/container-credentials.json \
349
+ sanctuary-unraid sanctuary || return $?
350
+ validate_sanctuary_legacy_staging "$PREPARED_LEGACY_CONTAINER_ID" "$PREPARED_LEGACY_IMAGE_ID" || return $?
351
+ LEGACY_STAGING_CONTAINER_ID=$PREPARED_LEGACY_CONTAINER_ID
352
+ LEGACY_STAGING_IMAGE_ID=$PREPARED_LEGACY_IMAGE_ID
353
+ }
354
+ capture_sanctuary_legacy_evidence() {
355
+ CAPTURED_LEGACY_CONTAINER_ID=$1
356
+ CAPTURED_LEGACY_IMAGE_ID=$2
357
+ LEGACY_EVIDENCE_ROOT=$3
358
+ case "$CAPTURED_LEGACY_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
359
+ test "${#CAPTURED_LEGACY_CONTAINER_ID}" -eq 64 || return 1
360
+ validate_exact_image_id "$CAPTURED_LEGACY_IMAGE_ID" || return $?
361
+ case "$LEGACY_EVIDENCE_ROOT" in /*) ;; *) return 1 ;; esac
362
+ if test -e "$LEGACY_EVIDENCE_ROOT" || test -L "$LEGACY_EVIDENCE_ROOT"; then
363
+ /usr/local/bin/node -e '
364
+ const fs = require("node:fs");
365
+ const root = fs.lstatSync(process.argv[1]);
366
+ if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o777) !== 0o700) process.exit(1);
367
+ if (root.uid !== process.geteuid() || root.gid !== process.getegid()) process.exit(1);
368
+ ' "$LEGACY_EVIDENCE_ROOT" || return $?
369
+ else
370
+ install -d -m 0700 -o 0 -g 0 "$LEGACY_EVIDENCE_ROOT" || return $?
371
+ fi
372
+ LEGACY_EVIDENCE_DIR="$LEGACY_EVIDENCE_ROOT/${CAPTURED_LEGACY_IMAGE_ID#sha256:}"
373
+ if test -e "$LEGACY_EVIDENCE_DIR" || test -L "$LEGACY_EVIDENCE_DIR"; then
374
+ test -d "$LEGACY_EVIDENCE_DIR" && test ! -L "$LEGACY_EVIDENCE_DIR" || return 1
375
+ else
376
+ mkdir -m 0700 "$LEGACY_EVIDENCE_DIR" || return $?
377
+ fi
378
+ /usr/local/bin/node -e '
379
+ const fs = require("node:fs");
380
+ const [rootPath, directoryPath] = process.argv.slice(1);
381
+ const root = fs.lstatSync(rootPath);
382
+ const directory = fs.lstatSync(directoryPath);
383
+ const privateDirectory = value => value.isDirectory() && !value.isSymbolicLink() && (value.mode & 0o777) === 0o700;
384
+ if (!privateDirectory(root) || !privateDirectory(directory)) process.exit(1);
385
+ if (root.uid !== process.geteuid() || root.gid !== process.getegid()) process.exit(1);
386
+ if (root.uid !== directory.uid || root.gid !== directory.gid) process.exit(1);
387
+ const allowed = new Set(["container.json", "image.json"]);
388
+ if (fs.readdirSync(directoryPath).some(name => !allowed.has(name))) process.exit(1);
389
+ ' "$LEGACY_EVIDENCE_ROOT" "$LEGACY_EVIDENCE_DIR" || return $?
390
+ validate_legacy_evidence_entry() {
391
+ VALIDATED_EVIDENCE_KIND=$1
392
+ VALIDATED_EVIDENCE_PATH=$2
393
+ /usr/local/bin/node -e '
394
+ const fs = require("node:fs");
395
+ const [kind, filePath, rootPath, containerId, imageId] = process.argv.slice(1);
396
+ const root = fs.lstatSync(rootPath);
397
+ const entry = fs.lstatSync(filePath);
398
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.uid !== root.uid || entry.gid !== root.gid) process.exit(1);
399
+ const mode = entry.mode & 0o777;
400
+ if (mode !== 0o600 && mode !== 0o644) process.exit(1);
401
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
402
+ if (!Array.isArray(value) || value.length !== 1 || !value[0] || typeof value[0] !== "object") process.exit(1);
403
+ if (kind === "container" && (value[0].Id !== containerId || value[0].Image !== imageId)) process.exit(1);
404
+ if (kind === "image" && value[0].Id !== imageId) process.exit(1);
405
+ ' "$VALIDATED_EVIDENCE_KIND" "$VALIDATED_EVIDENCE_PATH" "$LEGACY_EVIDENCE_ROOT" "$CAPTURED_LEGACY_CONTAINER_ID" "$CAPTURED_LEGACY_IMAGE_ID"
406
+ }
407
+ capture_missing_legacy_evidence_entry() {
408
+ MISSING_EVIDENCE_KIND=$1
409
+ MISSING_EVIDENCE_PATH=$2
410
+ LEGACY_EVIDENCE_TMP=$(mktemp "$LEGACY_EVIDENCE_ROOT/.capture.XXXXXXXX") || return $?
411
+ if test "$MISSING_EVIDENCE_KIND" = container; then
412
+ docker container inspect "$CAPTURED_LEGACY_CONTAINER_ID" >"$LEGACY_EVIDENCE_TMP" || {
413
+ CAPTURE_STATUS=$?
414
+ rm -f "$LEGACY_EVIDENCE_TMP"
415
+ return "$CAPTURE_STATUS"
416
+ }
417
+ else
418
+ docker image inspect "$CAPTURED_LEGACY_IMAGE_ID" >"$LEGACY_EVIDENCE_TMP" || {
419
+ CAPTURE_STATUS=$?
420
+ rm -f "$LEGACY_EVIDENCE_TMP"
421
+ return "$CAPTURE_STATUS"
422
+ }
423
+ fi
424
+ chmod 0600 "$LEGACY_EVIDENCE_TMP" || {
425
+ CAPTURE_STATUS=$?
426
+ rm -f "$LEGACY_EVIDENCE_TMP"
427
+ return "$CAPTURE_STATUS"
428
+ }
429
+ validate_legacy_evidence_entry "$MISSING_EVIDENCE_KIND" "$LEGACY_EVIDENCE_TMP" || {
430
+ CAPTURE_STATUS=$?
431
+ rm -f "$LEGACY_EVIDENCE_TMP"
432
+ return "$CAPTURE_STATUS"
433
+ }
434
+ sync -f "$LEGACY_EVIDENCE_TMP" || {
435
+ CAPTURE_STATUS=$?
436
+ rm -f "$LEGACY_EVIDENCE_TMP"
437
+ return "$CAPTURE_STATUS"
438
+ }
439
+ if ln "$LEGACY_EVIDENCE_TMP" "$MISSING_EVIDENCE_PATH"; then
440
+ rm -f "$LEGACY_EVIDENCE_TMP" || return $?
441
+ else
442
+ rm -f "$LEGACY_EVIDENCE_TMP" || return $?
443
+ validate_legacy_evidence_entry "$MISSING_EVIDENCE_KIND" "$MISSING_EVIDENCE_PATH" || return $?
444
+ fi
445
+ }
446
+ for LEGACY_EVIDENCE_KIND in container image; do
447
+ LEGACY_EVIDENCE_PATH="$LEGACY_EVIDENCE_DIR/$LEGACY_EVIDENCE_KIND.json"
448
+ if test -e "$LEGACY_EVIDENCE_PATH" || test -L "$LEGACY_EVIDENCE_PATH"; then
449
+ validate_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
450
+ else
451
+ capture_missing_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
452
+ fi
453
+ chmod 0600 "$LEGACY_EVIDENCE_PATH" || return $?
454
+ validate_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
455
+ done
344
456
  sync -f "$LEGACY_EVIDENCE_DIR" || return $?
457
+ }
458
+ install_from_legacy_staging() {
459
+ prepare_sanctuary_legacy_adoption "$IMAGE_ID" || return $?
460
+ ADOPTION_PREPARED_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
461
+ ADOPTION_PREPARED_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
462
+ verify_sanctuary_provider_readiness "$IMAGE_ID" || return $?
463
+ ADOPTION_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
464
+ ADOPTION_NAME_COUNTS=$(printf '%s\n' "$ADOPTION_CONTAINER_NAMES" | awk '
465
+ /butler/ { butlers++ }
466
+ $0 == "ouro-butler-staging" { staging++ }
467
+ END { printf "%d %d", butlers + 0, staging + 0 }
468
+ ') || return $?
469
+ test "$ADOPTION_NAME_COUNTS" = "1 1" || return $?
470
+ assert_only_running_butler ouro-butler-staging || return $?
471
+ test "$(docker inspect --format '{{.State.Running}}' ouro-butler-staging)" = true || return $?
472
+ test "$(docker inspect --format '{{.Id}}' ouro-butler-staging)" = "$ADOPTION_PREPARED_CONTAINER_ID" || return $?
473
+ test "$(docker inspect --format '{{.Image}}' ouro-butler-staging)" = "$ADOPTION_PREPARED_IMAGE_ID" || return $?
474
+ LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
475
+ LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
476
+ LEGACY_EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/legacy-evidence
477
+ capture_sanctuary_legacy_evidence "$LEGACY_STAGING_CONTAINER_ID" "$LEGACY_STAGING_IMAGE_ID" "$LEGACY_EVIDENCE_ROOT" || return $?
478
+ validate_sanctuary_legacy_staging "$ADOPTION_PREPARED_CONTAINER_ID" "$ADOPTION_PREPARED_IMAGE_ID" || return $?
479
+ LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
480
+ LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
345
481
  disable_butler_autostart || return $?
346
- if docker stop ouro-butler-staging \
347
- && CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) \
482
+ if docker stop "$LEGACY_STAGING_CONTAINER_ID" \
483
+ && CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' "$LEGACY_STAGING_CONTAINER_ID") \
348
484
  && test "$CURRENT_LEGACY_STAGING_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
349
- && test "$(docker inspect --format '{{.State.Running}}' ouro-butler-staging)" = false \
350
- && docker rename ouro-butler-staging ouro-butler-legacy-evidence \
485
+ && test "$(docker inspect --format '{{.State.Running}}' "$LEGACY_STAGING_CONTAINER_ID")" = false \
486
+ && docker rename "$LEGACY_STAGING_CONTAINER_ID" ouro-butler-legacy-evidence \
351
487
  && CURRENT_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) \
352
488
  && test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
353
489
  && test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false \
@@ -395,6 +531,9 @@ $ADOPTION_RECOVERY_NAMES
395
531
  *"
396
532
  ouro-butler-staging
397
533
  "*)
534
+ CURRENT_LEGACY_STAGING_CONTAINER_ID=$(docker inspect --format '{{.Id}}' ouro-butler-staging) || return $?
535
+ case "$CURRENT_LEGACY_STAGING_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
536
+ test "${#CURRENT_LEGACY_STAGING_CONTAINER_ID}" -eq 64 || return 1
398
537
  CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
399
538
  case "$CURRENT_LEGACY_STAGING_IMAGE_ID" in
400
539
  "$IMAGE_ID")
@@ -402,9 +541,10 @@ ouro-butler-staging
402
541
  docker rm --force ouro-butler-staging || return $?
403
542
  ;;
404
543
  "$LEGACY_STAGING_IMAGE_ID")
405
- docker stop ouro-butler-staging >/dev/null 2>&1 || return $?
406
- test "$(docker inspect --format '{{.State.Running}}' ouro-butler-staging)" = false || return $?
407
- docker rename ouro-butler-staging ouro-butler-legacy-evidence || return $?
544
+ test "$CURRENT_LEGACY_STAGING_CONTAINER_ID" = "$LEGACY_STAGING_CONTAINER_ID" || return 1
545
+ docker stop "$LEGACY_STAGING_CONTAINER_ID" >/dev/null 2>&1 || return $?
546
+ test "$(docker inspect --format '{{.State.Running}}' "$LEGACY_STAGING_CONTAINER_ID")" = false || return $?
547
+ docker rename "$LEGACY_STAGING_CONTAINER_ID" ouro-butler-legacy-evidence || return $?
408
548
  ;;
409
549
  *) return 1 ;;
410
550
  esac
@@ -453,9 +593,18 @@ ouro-butler-staging
453
593
  bootstrap_sanctuary_vault() {
454
594
  BOOTSTRAP_IMAGE_ID=$1
455
595
  BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE=${2-}
596
+ BOOTSTRAP_SOURCE_MACHINE_ID=${3-}
597
+ BOOTSTRAP_TARGET_MACHINE_ID=${4-}
456
598
  validate_exact_image_id "$BOOTSTRAP_IMAGE_ID" || return $?
457
599
  case "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" in
458
- ""|/mnt/user/appdata/ouro-butler/runtime/container-credentials.json) ;;
600
+ "")
601
+ test -z "$BOOTSTRAP_SOURCE_MACHINE_ID" || return 1
602
+ test -z "$BOOTSTRAP_TARGET_MACHINE_ID" || return 1
603
+ ;;
604
+ /mnt/user/appdata/ouro-butler/runtime/container-credentials.json)
605
+ test "$BOOTSTRAP_SOURCE_MACHINE_ID" = sanctuary-unraid || return 1
606
+ test "$BOOTSTRAP_TARGET_MACHINE_ID" = sanctuary || return 1
607
+ ;;
459
608
  *) return 1 ;;
460
609
  esac
461
610
  BOOTSTRAP_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
@@ -560,11 +709,16 @@ local unlock: available
560
709
  --mount "type=bind,src=$BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
561
710
  --entrypoint node "$BOOTSTRAP_IMAGE_ID" -e '
562
711
  const { loadContainerCredentialBootstrap } = require("/opt/ouro/dist/heart/daemon/container-credential-bootstrap.js")
563
- loadContainerCredentialBootstrap(["sanctuary"]).then(
712
+ loadContainerCredentialBootstrap(["sanctuary"], {
713
+ machineIdMigration: {
714
+ sourceMachineId: process.argv[1],
715
+ targetMachineId: process.argv[2],
716
+ },
717
+ }).then(
564
718
  () => process.exit(0),
565
719
  () => { process.stderr.write("credential bootstrap failed\n"); process.exit(1) },
566
720
  )
567
- ' || return $?
721
+ ' "$BOOTSTRAP_SOURCE_MACHINE_ID" "$BOOTSTRAP_TARGET_MACHINE_ID" || return $?
568
722
  ! docker container inspect ouro-butler-credential-bootstrap >/dev/null 2>&1 || return 1
569
723
  test ! -e "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
570
724
  test ! -e "$BOOTSTRAP_CREDENTIAL_CLAIM" || return $?
@@ -575,35 +729,107 @@ local unlock: available
575
729
  test ! -L "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE" || return $?
576
730
  test "$(stat -c '%u:%g %a' "$BOOTSTRAP_LEGACY_CREDENTIAL_SOURCE")" = "$BOOTSTRAP_LEGACY_METADATA" || return $?
577
731
  fi
732
+ validate_sanctuary_roots "$BOOTSTRAP_RUNTIME_ROOT" "$BOOTSTRAP_AGENT_ROOT" || return $?
733
+ }
734
+ authenticate_sanctuary_provider() {
735
+ AUTH_IMAGE_ID=$1
736
+ AUTH_PROVIDER=${2-}
737
+ validate_exact_image_id "$AUTH_IMAGE_ID" || return $?
738
+ case "$AUTH_PROVIDER" in
739
+ openai-compatible) AUTH_CONTAINER=ouro-butler-provider-auth-glm ;;
740
+ openai-compatible-gemini) AUTH_CONTAINER=ouro-butler-provider-auth-gemini ;;
741
+ *) return 1 ;;
742
+ esac
743
+ AUTH_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
744
+ AUTH_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
745
+ ! docker container inspect "$AUTH_CONTAINER" >/dev/null 2>&1 || return 1
746
+ docker run --rm -it --pull=never --network host --name "$AUTH_CONTAINER" --user 10001:10001 \
747
+ --mount "type=bind,src=$AUTH_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
748
+ --mount "type=bind,src=$AUTH_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
749
+ --entrypoint node "$AUTH_IMAGE_ID" /opt/ouro/dist/heart/daemon/ouro-entry.js \
750
+ auth --agent sanctuary --provider "$AUTH_PROVIDER" || return $?
751
+ ! docker container inspect "$AUTH_CONTAINER" >/dev/null 2>&1 || return 1
752
+ }
753
+ verify_sanctuary_provider_readiness() {
754
+ READINESS_IMAGE_ID=$1
755
+ validate_exact_image_id "$READINESS_IMAGE_ID" || return $?
756
+ READINESS_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
757
+ READINESS_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
758
+ ! docker container inspect ouro-butler-provider-readiness >/dev/null 2>&1 || return 1
578
759
  docker run --rm --pull=never --network host --name ouro-butler-provider-readiness --user 10001:10001 \
579
- --mount "type=bind,src=$BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
580
- --mount "type=bind,src=$BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
581
- --entrypoint /bin/sh "$BOOTSTRAP_IMAGE_ID" -ceu '
760
+ --mount "type=bind,src=$READINESS_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
761
+ --mount "type=bind,src=$READINESS_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
762
+ --entrypoint /bin/sh "$READINESS_IMAGE_ID" -ceu '
582
763
  umask 077
583
- READINESS_STARTED_AT=$(node -e "process.stdout.write(new Date().toISOString())")
584
764
  node /opt/ouro/dist/heart/daemon/ouro-entry.js check --agent sanctuary --lane outward
585
765
  node /opt/ouro/dist/heart/daemon/ouro-entry.js check --agent sanctuary --lane inner
586
- node -e "
587
- const fs = require(\"node:fs\");
588
- const agentPath = process.argv[2];
589
- const readinessPath = process.argv[3];
590
- const agent = JSON.parse(fs.readFileSync(agentPath, \"utf8\"));
591
- const readiness = JSON.parse(fs.readFileSync(readinessPath, \"utf8\"));
592
- if (readiness.schemaVersion !== 1 || !readiness.lanes || typeof readiness.lanes !== \"object\") process.exit(1);
593
- for (const [lane, facing] of [[\"outward\", \"humanFacing\"], [\"inner\", \"agentFacing\"]]) {
594
- const entry = readiness.lanes[lane];
595
- const binding = agent[facing];
596
- if (!entry || !binding || entry.agentName !== \"sanctuary\" || entry.lane !== lane) process.exit(1);
597
- if (entry.status !== \"ready\" || entry.provider !== binding.provider || entry.model !== binding.model) process.exit(1);
598
- if (!Number.isFinite(Date.parse(entry.checkedAt)) || Date.parse(entry.checkedAt) < Date.parse(process.argv[1])) process.exit(1);
599
- if (typeof entry.credentialRevision !== \"string\" || entry.credentialRevision.length === 0) process.exit(1);
600
- }
601
- " "$READINESS_STARTED_AT" \
602
- /home/ouro/AgentBundles/sanctuary.ouro/agent.json \
603
- /home/ouro/AgentBundles/sanctuary.ouro/state/providers/readiness.json
766
+ node - <<'"'"'NODE'"'"'
767
+ (async () => {
768
+ try {
769
+ const fs = require("node:fs");
770
+ const root = "/home/ouro/AgentBundles/sanctuary.ouro";
771
+ const agent = JSON.parse(fs.readFileSync(`${root}/agent.json`, "utf8"));
772
+ const policy = JSON.parse(fs.readFileSync(`${root}/provider-readiness.json`, "utf8"));
773
+ const expectedPolicy = { version: 1, selectionPolicy: "explicit-same-lane-only" };
774
+ const expected = new Map([
775
+ ["openai-compatible", { provider: "openai-compatible", model: "glm-5.2", vaultItem: "providers/openai-compatible", baseUrl: "https://api.z.ai/api/paas/v4/" }],
776
+ ["openai-compatible-gemini", { provider: "openai-compatible-gemini", model: "gemini-3.6-flash", vaultItem: "providers/openai-compatible-gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/" }],
777
+ ]);
778
+ const exactLane = value => value && value.provider === "openai-compatible" && value.model === "glm-5.2";
779
+ if (!exactLane(agent.humanFacing) || !exactLane(agent.agentFacing)
780
+ || policy.version !== expectedPolicy.version || policy.selectionPolicy !== expectedPolicy.selectionPolicy
781
+ || !Array.isArray(policy.providers) || policy.providers.length !== expected.size) throw new Error();
782
+ for (const entry of policy.providers) {
783
+ const wanted = expected.get(entry.provider);
784
+ if (!wanted || entry.model !== wanted.model || entry.vaultItem !== wanted.vaultItem || entry.baseUrl !== wanted.baseUrl) throw new Error();
785
+ expected.delete(entry.provider);
786
+ }
787
+ if (expected.size !== 0) throw new Error();
788
+ const { refreshProviderCredentialPool } = require("/opt/ouro/dist/heart/provider-credentials.js");
789
+ const { pingProvider } = require("/opt/ouro/dist/heart/provider-ping.js");
790
+ const refreshed = await refreshProviderCredentialPool("sanctuary", {
791
+ providers: ["openai-compatible", "openai-compatible-gemini"],
792
+ skipCache: true,
793
+ });
794
+ if (!refreshed.ok) throw new Error();
795
+ const glm = refreshed.pool.providers["openai-compatible"];
796
+ const gemini = refreshed.pool.providers["openai-compatible-gemini"];
797
+ const exactRecord = (record, provider, baseUrl) => record && record.provider === provider
798
+ && typeof record.revision === "string" && record.revision.length > 0
799
+ && typeof record.credentials?.apiKey === "string" && record.credentials.apiKey.trim().length > 0
800
+ && record.config?.baseUrl === baseUrl;
801
+ if (!exactRecord(glm, "openai-compatible", "https://api.z.ai/api/paas/v4/")
802
+ || !exactRecord(gemini, "openai-compatible-gemini", "https://generativelanguage.googleapis.com/v1beta/openai/")
803
+ || glm.credentials.apiKey === gemini.credentials.apiKey
804
+ || new Set([glm.revision, gemini.revision]).size !== 2) throw new Error();
805
+ const probes = [
806
+ { provider: "openai-compatible", model: "glm-5.2", credentialRevision: glm.revision, record: glm },
807
+ { provider: "openai-compatible", model: "glm-5.2", credentialRevision: glm.revision, record: glm },
808
+ { provider: "openai-compatible-gemini", model: "gemini-3.6-flash", credentialRevision: gemini.revision, record: gemini },
809
+ ];
810
+ const results = await Promise.all(probes.map(({ provider, model, record }) => pingProvider(
811
+ provider,
812
+ { ...record.credentials, ...record.config },
813
+ { model, timeoutMs: 10000, attemptPolicy: { baseDelayMs: 0 } },
814
+ )));
815
+ for (let index = 0; index < results.length; index += 1) {
816
+ const result = results[index];
817
+ const probe = probes[index];
818
+ if (!result.ok || !Array.isArray(result.attempts) || result.attempts.length < 1 || result.attempts.length > 3
819
+ || !result.attempts.every(attempt => attempt.provider === probe.provider && attempt.model === probe.model
820
+ && attempt.operation === "ping" && typeof attempt.ok === "boolean")
821
+ || result.attempts.at(-1)?.ok !== true) throw new Error();
822
+ }
823
+ process.stdout.write("Sanctuary provider readiness verified.\n");
824
+ } catch {
825
+ process.stderr.write("Sanctuary provider readiness verification failed.\n");
826
+ process.exitCode = 1;
827
+ }
828
+ })();
829
+ NODE
604
830
  ' || return $?
605
831
  ! docker container inspect ouro-butler-provider-readiness >/dev/null 2>&1 || return 1
606
- validate_sanctuary_roots "$BOOTSTRAP_RUNTIME_ROOT" "$BOOTSTRAP_AGENT_ROOT" || return $?
832
+ validate_sanctuary_roots "$READINESS_RUNTIME_ROOT" "$READINESS_AGENT_ROOT" || return $?
607
833
  }
608
834
  run_sanctuary_docker() {
609
835
  /usr/bin/timeout -s KILL 20 /usr/bin/docker "$@"
@@ -806,9 +1032,20 @@ Update:
806
1032
  docker image inspect "$IMAGE_ID" >/dev/null
807
1033
  Initial install/adoption is a separate terminal path for the verified live
808
1034
  legacy state: no production or rollback, exactly one running (possibly
809
- unhealthy) ouro-butler-staging, and no legacy-evidence container. Run this and
810
- stop; do not continue into the normal update below:
1035
+ unhealthy) ouro-butler-staging, and no legacy-evidence container. After the
1036
+ helper definitions above are loaded and IMAGE_ID is resolved, run this exact
1037
+ sequence. The two authentication commands prompt through the runtime's hidden
1038
+ terminal input; do not place credentials in arguments, shell variables, or
1039
+ history.
1040
+ Sanctuary legacy adoption commands:
1041
+ prepare_sanctuary_legacy_adoption "$IMAGE_ID"
1042
+ authenticate_sanctuary_provider "$IMAGE_ID" openai-compatible
1043
+ authenticate_sanctuary_provider "$IMAGE_ID" openai-compatible-gemini
1044
+ verify_sanctuary_provider_readiness "$IMAGE_ID"
811
1045
  install_from_legacy_staging
1046
+ These commands are one terminal path; after install succeeds, stop and do not
1047
+ continue into the normal update below. The final install is noninteractive:
1048
+ it reruns resumable preparation plus fresh readiness, but never authentication.
812
1049
  That function never applies the canonical auditor to the known-noncanonical
813
1050
  legacy container or restarts it. It verifies and pins the legacy image,
814
1051
  provisions the absent canonical roots from the exact target image's packaged
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.734",
2
+ "runtimeVersion": "0.1.0-alpha.735",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-20T00:00:00.000Z"
5
5
  }
@@ -144,16 +144,39 @@ async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
144
144
  loaded.add(message.agentName);
145
145
  }
146
146
  const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId;
147
- for (const message of envelope.credentials) {
148
- if (isRecord(message) && typeof message.machineId === "string" && message.machineId.trim() !== machineId) {
149
- throw new Error("container credential bootstrap machineId does not match this machine");
147
+ const migration = options.machineIdMigration;
148
+ let messages = envelope.credentials;
149
+ if (migration) {
150
+ const { sourceMachineId, targetMachineId } = migration;
151
+ if (!sourceMachineId
152
+ || !targetMachineId
153
+ || sourceMachineId !== sourceMachineId.trim()
154
+ || targetMachineId !== targetMachineId.trim()
155
+ || sourceMachineId === targetMachineId) {
156
+ throw new Error("container credential bootstrap machineId migration is invalid");
157
+ }
158
+ if (machineId !== targetMachineId) {
159
+ throw new Error("container credential bootstrap machineId migration target does not match this machine");
160
+ }
161
+ for (const message of messages) {
162
+ if (!isRecord(message) || message.machineId !== sourceMachineId) {
163
+ throw new Error("container credential bootstrap machineId migration source does not match");
164
+ }
165
+ }
166
+ messages = messages.map((message) => ({ ...message, machineId: targetMachineId }));
167
+ }
168
+ else {
169
+ for (const message of messages) {
170
+ if (isRecord(message) && typeof message.machineId === "string" && message.machineId !== machineId) {
171
+ throw new Error("container credential bootstrap machineId does not match this machine");
172
+ }
150
173
  }
151
174
  }
152
175
  const persist = options.persist ?? ((message) => (0, runtime_credentials_1.persistRuntimeCredentialBootstrapMessage)(message, {
153
176
  machineId,
154
177
  }));
155
178
  try {
156
- for (const message of envelope.credentials) {
179
+ for (const message of messages) {
157
180
  if (!await persist(message))
158
181
  throw new Error("invalid bootstrap message");
159
182
  }
@@ -169,7 +192,7 @@ async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
169
192
  throw new Error("container credential bootstrap persistence failed; recoverable claim retained for reconciliation");
170
193
  }
171
194
  if (options.apply) {
172
- for (const message of envelope.credentials) {
195
+ for (const message of messages) {
173
196
  if (!options.apply(message))
174
197
  throw new Error("container credential bootstrap message is invalid");
175
198
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.734",
3
+ "version": "0.1.0-alpha.736",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.734",
9
+ "version": "0.1.0-alpha.736",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.734",
3
+ "version": "0.1.0-alpha.736",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },