@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": [
|
package/deploy/unraid/Dockerfile
CHANGED
|
@@ -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
|
package/deploy/unraid/README.txt
CHANGED
|
@@ -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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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 "$
|
|
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"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
347
|
-
&& CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}'
|
|
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}}'
|
|
350
|
-
&& docker rename
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
docker
|
|
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
|
-
""
|
|
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"]
|
|
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=$
|
|
580
|
-
--mount "type=bind,src=$
|
|
581
|
-
--entrypoint /bin/sh "$
|
|
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 -
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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 "$
|
|
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.
|
|
810
|
-
|
|
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
|
|
@@ -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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
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",
|