@ouro.bot/cli 0.1.0-alpha.816 → 0.1.0-alpha.818

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.
Files changed (50) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +16 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +296 -62
  5. package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
  8. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  9. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  10. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
  11. package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  13. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  14. package/deploy/unraid/sanctuary.xml +2 -1
  15. package/dist/heart/core.js +2 -1
  16. package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
  17. package/dist/heart/daemon/container-spec-auditor.js +16 -9
  18. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  19. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  20. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  21. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  22. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  23. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  24. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  25. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  26. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  27. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  28. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  29. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  30. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  31. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  33. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  36. package/dist/heart/daemon/sense-manager.js +20 -10
  37. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  38. package/dist/repertoire/tools.js +15 -6
  39. package/dist/senses/private-runtime.js +2 -2
  40. package/dist/senses/root-host-approval-port.js +345 -0
  41. package/dist/senses/root-host-approval-runtime.js +393 -0
  42. package/dist/senses/sanctuary-authority-resident.js +102 -0
  43. package/dist/senses/telegram-admission.js +7 -0
  44. package/dist/senses/telegram-attachments.js +5 -1
  45. package/dist/senses/telegram-authority-transport.js +231 -0
  46. package/dist/senses/telegram-client.js +31 -8
  47. package/dist/senses/telegram-entry.js +1 -1
  48. package/dist/senses/telegram.js +168 -28
  49. package/npm-shrinkwrap.json +2 -2
  50. package/package.json +1 -1
@@ -1,6 +1,14 @@
1
1
  Mendelow Cloud Butler operator runbook
2
2
 
3
- The production container is ouro-butler. It runs as UID/GID 10001, publishes no ports, uses host networking only so its loopback-only Unraid GraphQL client can reach 127.0.0.1, mounts the runtime and sanctuary.ouro bundle read-write plus the privileged event spool read-only, and uses restart policy unless-stopped.
3
+ The production container is ouro-butler. It runs as UID/GID 10001, publishes no ports, uses host networking only so its loopback-only Unraid GraphQL client can reach 127.0.0.1, mounts the runtime and sanctuary.ouro bundle read-write plus the privileged event spool read-only, and uses restart policy unless-stopped. The canonical-gateway image appends exactly /run/ouro-authority -> /run/ouro-authority, readonly, rprivate. The pinned alpha.816 predecessor has only the first three mounts. Root token, issuer, package and authority state are never mounted into the resident.
4
+
5
+ Root authority lifecycle
6
+
7
+ The existing DockerMan transaction owns authority-install, authority-activate, authority-retire and authority-restore. A complete exact-image package, including dist and production dependencies, must remain at its package-relative paths; extracting only the old five deployment assets cannot execute this lifecycle. The reviewed root-only inputs are /mnt/user/appdata/ouro-authority/incoming-package, package-manifest.json, request.json and incoming-token. They are not a new resident credential policy. The human rotates the existing bot token directly into root custody; the root owner proves the previous token returns 401 and the fresh token belongs to the same numeric bot before continuing.
8
+
9
+ During installation the resident is stopped, the root gateway is installed and proven, tokenless readiness runs, and authority-activate starts the exact four-mount target. On failure, authority-retire must finish before restoring bundle/config/mounts or starting the exact three-mount predecessor; authority-restore proves that rollback before the outer template journal is removed. An uncertain or interrupted handoff leaves autostart disabled and preserves both journals and root inputs for the same reviewed recovery, never a raw old-template start. Retired keys, policy, history and Jellyfin remain untouched. Re-enabling authority requires a new human token rotation and issuer epoch, not reuse of retired authority.
10
+
11
+ On installed cold boot, the original unless-stopped and DockerMan autostart remain unchanged: /boot/config/custom/ouro-authority/start.sh --boot waits for the array and responsive Docker, then starts the fenced root gateway. Docker may already have started the tokenless resident; it fails closed until the gateway is ready. There is no pre-Docker gate, whole-array gate, resident direct poller or resident token fallback.
4
12
 
5
13
  Bounded Jellyfin stewardship
6
14
 
@@ -39,17 +47,17 @@ Effective-spec audit helper:
39
47
  AUDIT_CONTAINER=$1
40
48
  AUDIT_EXPECTED_IMAGE=$2
41
49
  AUDIT_RUNNER_IMAGE_ID=$3
42
- AUDIT_MOUNT_CONTRACT=${4-canonical}
50
+ AUDIT_MOUNT_CONTRACT=${4-}
43
51
  AUDIT_EXPECTED_IMAGE_REFERENCE=${5-}
44
52
  AUDIT_EXPECTED_ICON=${6-}
45
53
  validate_exact_image_id "$AUDIT_EXPECTED_IMAGE" || return $?
46
54
  validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
47
55
  test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
48
56
  case "$AUDIT_MOUNT_CONTRACT" in
49
- canonical)
57
+ canonical-pre-gateway|canonical-gateway)
50
58
  test -n "$AUDIT_EXPECTED_IMAGE_REFERENCE" || return 1
51
59
  test "$AUDIT_EXPECTED_ICON" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png || return 1
52
- set -- --expected-image-reference "$AUDIT_EXPECTED_IMAGE_REFERENCE" --expected-icon "$AUDIT_EXPECTED_ICON"
60
+ set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" --expected-image-reference "$AUDIT_EXPECTED_IMAGE_REFERENCE" --expected-icon "$AUDIT_EXPECTED_ICON"
53
61
  ;;
54
62
  legacy-alpha742) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
55
63
  *) return 1 ;;
@@ -296,6 +304,41 @@ Effective-spec audit helper:
296
304
  esac
297
305
  docker image inspect "$VALIDATE_IMAGE_ID" >/dev/null || return $?
298
306
  }
307
+ sanctuary_image_mount_contract() {
308
+ (
309
+ CONTRACT_IMAGE_ID=$1
310
+ validate_exact_image_id "$CONTRACT_IMAGE_ID" || return $?
311
+ CONTRACT_LABEL=$(docker image inspect --format '{{with .Config.Labels}}{{index . "bot.ouro.sanctuary.mount-contract"}}{{end}}' "$CONTRACT_IMAGE_ID") || return $?
312
+ case "$CONTRACT_IMAGE_ID:$CONTRACT_LABEL" in
313
+ sha256:589b7cf8f96d139ee9fd86204a183126aadcbd3fbfb406063709be303397b1b2:) printf '%s\n' canonical-pre-gateway ;;
314
+ sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d:) printf '%s\n' legacy-alpha742 ;;
315
+ sha256:589b7cf8f96d139ee9fd86204a183126aadcbd3fbfb406063709be303397b1b2:*|sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d:*) return 1 ;;
316
+ *:canonical-gateway) printf '%s\n' canonical-gateway ;;
317
+ *) printf '%s\n' 'Unreviewed image mount contract; refusing version-only classification.' >&2; return 1 ;;
318
+ esac
319
+ )
320
+ }
321
+ create_sanctuary_container() {
322
+ (
323
+ CREATE_IMAGE_ID=$1
324
+ CREATE_IMAGE_REFERENCE=$2
325
+ CREATE_MOUNT_CONTRACT=$3
326
+ test "$(sanctuary_image_mount_contract "$CREATE_IMAGE_ID")" = "$CREATE_MOUNT_CONTRACT" || return 1
327
+ test "$(docker image inspect --format '{{.Id}}' "$CREATE_IMAGE_REFERENCE")" = "$CREATE_IMAGE_ID" || return 1
328
+ case "$CREATE_MOUNT_CONTRACT" in
329
+ canonical-gateway) set -- --mount "type=bind,src=/run/ouro-authority,dst=/run/ouro-authority,readonly" ;;
330
+ canonical-pre-gateway) set -- ;;
331
+ *) return 1 ;;
332
+ esac
333
+ docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
334
+ --label net.unraid.docker.managed=dockerman \
335
+ --label "net.unraid.docker.icon=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png" \
336
+ --mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
337
+ --mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
338
+ --mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
339
+ "$@" "$CREATE_IMAGE_REFERENCE" || return $?
340
+ )
341
+ }
299
342
  assert_only_running_butler() {
300
343
  (
301
344
  EXPECTED_RUNNING_BUTLER=$1
@@ -407,7 +450,8 @@ Effective-spec audit helper:
407
450
  else
408
451
  assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
409
452
  EXPECTED_SOURCE_IMAGE_REFERENCE=$(docker inspect --format '{{.Config.Image}}' ouro-butler) || return $?
410
- audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$EXPECTED_SOURCE_IMAGE_REFERENCE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
453
+ EXPECTED_SOURCE_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$EXPECTED_SOURCE_IMAGE_ID") || return $?
454
+ audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$EXPECTED_SOURCE_MOUNT_CONTRACT" "$EXPECTED_SOURCE_IMAGE_REFERENCE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
411
455
  fi
412
456
  }
413
457
  validate_sanctuary_roots() {
@@ -867,11 +911,12 @@ Effective-spec audit helper:
867
911
  audit_registered_dockerman_template() {
868
912
  REGISTERED_TEMPLATE_RUNNER_IMAGE_ID=$1
869
913
  REGISTERED_TEMPLATE_VERSION_IMAGE=$2
914
+ REGISTERED_TEMPLATE_MOUNT_CONTRACT=$3
870
915
  REGISTERED_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
871
916
  validate_exact_image_id "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" || return $?
872
917
  printf '%s\n' "$REGISTERED_TEMPLATE_VERSION_IMAGE" | grep -Eq '^ghcr\.io/ourostack/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || return $?
873
918
  test -f "$REGISTERED_TEMPLATE_PATH" && test ! -L "$REGISTERED_TEMPLATE_PATH" || return 1
874
- docker run --rm --pull=never --network=none --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh --mount "type=bind,src=$REGISTERED_TEMPLATE_PATH,dst=/audit/sanctuary.xml,readonly" "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /opt/ouro/deploy/unraid/container-runtime.json --expected-image-reference "$REGISTERED_TEMPLATE_VERSION_IMAGE" || return $?
919
+ docker run --rm --pull=never --network=none --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh --mount "type=bind,src=$REGISTERED_TEMPLATE_PATH,dst=/audit/sanctuary.xml,readonly" "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /opt/ouro/deploy/unraid/container-runtime.json --expected-image-reference "$REGISTERED_TEMPLATE_VERSION_IMAGE" --mount-contract "$REGISTERED_TEMPLATE_MOUNT_CONTRACT" || return $?
875
920
  }
876
921
  verify_known_good_rollback_artifact() {
877
922
  EXPECTED_KNOWN_GOOD_IMAGE_ID=$1
@@ -930,6 +975,12 @@ Effective-spec audit helper:
930
975
  printf '%s\n' "$FINAL_PROOF_PATH"
931
976
  }
932
977
  start_only_butler_for_recovery() {
978
+ RECOVERY_AUTHORITY_STATE=$(read_sanctuary_authority_state) || return $?
979
+ if test "$RECOVERY_AUTHORITY_STATE" != none; then
980
+ /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-restore >/dev/null || return $?
981
+ assert_only_running_butler ouro-butler || return $?
982
+ return 0
983
+ fi
933
984
  RECOVERY_START_STATE=$(docker inspect --format '{{.State.Running}}' ouro-butler) || return $?
934
985
  case "$RECOVERY_START_STATE" in
935
986
  true)
@@ -982,6 +1033,7 @@ Effective-spec audit helper:
982
1033
  migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" commit || return $?
983
1034
  return 0
984
1035
  fi
1036
+ retire_sanctuary_authority_if_pending || return $?
985
1037
  if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
986
1038
  RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
987
1039
  test "$RECOVERY_CURRENT_ROLLBACK_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
@@ -1120,6 +1172,10 @@ Effective-spec audit helper:
1120
1172
  TEMPLATE_RECOVERY_EVIDENCE=$EVENT_ASSET_STAGE/template-recovery-evidence.json
1121
1173
  TEMPLATE_RECOVERY_INSPECTION=$EVENT_ASSET_STAGE/template-recovery-inspection.json
1122
1174
  TEMPLATE_RECOVERY_FINAL_ROOT=$EVENT_ASSET_STAGE/template-recovery-final
1175
+ if test "$TEMPLATE_RECOVERY_STATE" = rollback; then
1176
+ disable_butler_autostart || return $?
1177
+ retire_sanctuary_authority_if_pending || return $?
1178
+ fi
1123
1179
  if test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = rollback; then
1124
1180
  recover_pending_sanctuary_bundle_migration "$IMAGE_ID" || return $?
1125
1181
  TEMPLATE_RECOVERY_POST_BUNDLE_STATUS=$(read_sanctuary_bundle_transaction_status "$IMAGE_ID") || return $?
@@ -1143,7 +1199,7 @@ Effective-spec audit helper:
1143
1199
  test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
1144
1200
  test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
1145
1201
  assert_only_running_butler ouro-butler || return $?
1146
- audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1202
+ audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1147
1203
  migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
1148
1204
  chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
1149
1205
  write_dockerman_recovery_evidence committing target-exact-committing "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
@@ -1195,7 +1251,7 @@ Effective-spec audit helper:
1195
1251
  TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
1196
1252
  test "$TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
1197
1253
  test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return 1
1198
- audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1254
+ audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1199
1255
  start_only_butler_for_recovery || return $?
1200
1256
  wait_butler_ready ouro-butler || return $?
1201
1257
  migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
@@ -1205,7 +1261,7 @@ Effective-spec audit helper:
1205
1261
  enable_butler_autostart || return $?
1206
1262
  /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null || return $?
1207
1263
  elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = committing && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"; then
1208
- audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1264
+ audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1209
1265
  wait_butler_ready ouro-butler || return $?
1210
1266
  verify_butler_autostart "1 0 0 0" || return $?
1211
1267
  migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
@@ -1610,6 +1666,17 @@ NODE
1610
1666
  verify_sanctuary_telegram_readiness() {
1611
1667
  TELEGRAM_READINESS_IMAGE_ID=$1
1612
1668
  validate_exact_image_id "$TELEGRAM_READINESS_IMAGE_ID" || return $?
1669
+ TELEGRAM_READINESS_CONTRACT=$(sanctuary_image_mount_contract "$TELEGRAM_READINESS_IMAGE_ID") || return $?
1670
+ TELEGRAM_READINESS_CONTEXT=${2-}
1671
+ case "$TELEGRAM_READINESS_CONTRACT" in
1672
+ canonical-gateway) TELEGRAM_READINESS_CONTEXT=${TELEGRAM_READINESS_CONTEXT:-strict}; set -- --mount "type=bind,src=/run/ouro-authority,dst=/run/ouro-authority,readonly" ;;
1673
+ canonical-pre-gateway) TELEGRAM_READINESS_CONTEXT=${TELEGRAM_READINESS_CONTEXT:-live-precutover}; set -- ;;
1674
+ *) return 1 ;;
1675
+ esac
1676
+ case "$TELEGRAM_READINESS_CONTRACT:$TELEGRAM_READINESS_CONTEXT" in
1677
+ canonical-gateway:strict|canonical-pre-gateway:strict|canonical-pre-gateway:live-precutover) ;;
1678
+ *) return 1 ;;
1679
+ esac
1613
1680
  TELEGRAM_READINESS_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
1614
1681
  TELEGRAM_READINESS_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
1615
1682
  ! docker container inspect ouro-butler-telegram-readiness >/dev/null 2>&1 || return 1
@@ -1618,10 +1685,138 @@ NODE
1618
1685
  --mount "type=bind,src=$TELEGRAM_READINESS_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
1619
1686
  --mount "type=bind,src=$TELEGRAM_READINESS_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro,readonly" \
1620
1687
  --entrypoint /opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh \
1621
- "$TELEGRAM_READINESS_IMAGE_ID" telegram-readiness >/dev/null || return $?
1688
+ "$@" "$TELEGRAM_READINESS_IMAGE_ID" telegram-readiness >/dev/null || return $?
1622
1689
  ! docker container inspect ouro-butler-telegram-readiness >/dev/null 2>&1 || return 1
1623
1690
  normalize_sanctuary_private_permissions "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" "$TELEGRAM_READINESS_IMAGE_ID" || return $?
1624
- validate_sanctuary_roots "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" live-precutover || return $?
1691
+ validate_sanctuary_roots "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" "$TELEGRAM_READINESS_CONTEXT" || return $?
1692
+ }
1693
+ read_sanctuary_authority_state() {
1694
+ (
1695
+ test -n "${STAGED_DOCKERMAN_TRANSACTION-}" || return 1
1696
+ AUTHORITY_STATUS=$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" status) || return $?
1697
+ printf '%s' "$AUTHORITY_STATUS" | /usr/local/bin/node -e '
1698
+ let input = "";
1699
+ process.stdin.setEncoding("utf8");
1700
+ process.stdin.on("data", chunk => { input += chunk; });
1701
+ process.stdin.on("end", () => {
1702
+ const record = JSON.parse(input);
1703
+ if (record === null || (record && !Object.hasOwn(record, "authority"))) process.stdout.write("none");
1704
+ else if (["installing", "active", "retiring", "retired"].includes(record?.authority?.state)) process.stdout.write(record.authority.state);
1705
+ else process.exit(1);
1706
+ });
1707
+ ' || return $?
1708
+ )
1709
+ }
1710
+ retire_sanctuary_authority_if_pending() {
1711
+ (
1712
+ AUTHORITY_STATE=$(read_sanctuary_authority_state) || return $?
1713
+ test "$AUTHORITY_STATE" != none || return 0
1714
+ /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-retire >/dev/null || return $?
1715
+ )
1716
+ }
1717
+ receive_sanctuary_authority_token() {
1718
+ (
1719
+ test "$(id -u)" = 0 && test -t 0 || return 1
1720
+ test "$(stat -c '%u:%g:%a' /mnt/user/appdata/ouro-authority)" = 0:0:700 || return 1
1721
+ test ! -e /mnt/user/appdata/ouro-authority/incoming-token && test ! -L /mnt/user/appdata/ouro-authority/incoming-token || return 1
1722
+ TOKEN_TERMINAL_MODE=$(stty -g) || return $?
1723
+ trap 'stty "$TOKEN_TERMINAL_MODE"' EXIT
1724
+ trap 'exit 129' HUP
1725
+ trap 'exit 130' INT
1726
+ trap 'exit 143' TERM
1727
+ printf '%s\n' 'Human: paste the freshly rotated same-bot token, press Enter, then Ctrl-D. Input is hidden.' >&2
1728
+ stty -echo || return $?
1729
+ umask 077
1730
+ set -C
1731
+ dd status=none > /mnt/user/appdata/ouro-authority/incoming-token || return $?
1732
+ test -s /mnt/user/appdata/ouro-authority/incoming-token || return 1
1733
+ sync -f /mnt/user/appdata/ouro-authority/incoming-token || return $?
1734
+ )
1735
+ }
1736
+ prepare_sanctuary_authority_inputs() {
1737
+ (
1738
+ AUTHORITY_SOURCE=$1
1739
+ test "$(id -u)" = 0 || return 1
1740
+ /usr/local/bin/node - "$AUTHORITY_SOURCE" "$AUTHORITY_EPOCH_ID" "$AUTHORITY_BOT_ID" "$AUTHORITY_OWNER_ID" <<'NODE'
1741
+ const fs = require("node:fs");
1742
+ const path = require("node:path");
1743
+ const crypto = require("node:crypto");
1744
+ const [source, epochId, botId, ownerId] = process.argv.slice(2);
1745
+ const root = "/mnt/user/appdata/ouro-authority";
1746
+ const incoming = `${root}/incoming-package`;
1747
+ const sha = bytes => `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
1748
+ const directory = target => {
1749
+ if (!fs.existsSync(target)) fs.mkdirSync(target, { mode: 0o700 });
1750
+ const stat = fs.lstatSync(target);
1751
+ if (!stat.isDirectory() || fs.realpathSync(target) !== target || stat.uid !== 0 || stat.gid !== 0 || (stat.mode & 0o7777) !== 0o700) throw new Error("unsafe authority input directory");
1752
+ };
1753
+ const owned = (target, mode) => {
1754
+ const stat = fs.lstatSync(target);
1755
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== 0 || stat.gid !== 0 || (stat.mode & 0o7777) !== mode) throw new Error("unsafe authority input file");
1756
+ return fs.readFileSync(target);
1757
+ };
1758
+ const persist = (target, bytes, mode) => {
1759
+ if (fs.existsSync(target)) {
1760
+ if (!owned(target, mode).equals(bytes)) throw new Error("authority input conflicts with retained recovery bytes");
1761
+ } else {
1762
+ const fd = fs.openSync(target, "wx", mode);
1763
+ try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
1764
+ }
1765
+ };
1766
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(epochId) || !/^[1-9][0-9]*$/.test(botId) || !/^[1-9][0-9]*$/.test(ownerId)) throw new Error("invalid reviewed identity");
1767
+ directory(root);
1768
+ directory(incoming);
1769
+ const sourceRoot = fs.realpathSync(source);
1770
+ const files = {};
1771
+ const walk = (relative, ancestors = []) => {
1772
+ const input = path.join(sourceRoot, relative);
1773
+ const resolved = fs.realpathSync(input);
1774
+ if (resolved !== sourceRoot && !resolved.startsWith(`${sourceRoot}/`)) throw new Error("package link escapes exact image payload");
1775
+ if (ancestors.includes(resolved)) throw new Error("package link cycle");
1776
+ const stat = fs.statSync(input);
1777
+ if (stat.isDirectory()) {
1778
+ directory(path.join(incoming, relative));
1779
+ for (const name of fs.readdirSync(input).sort()) {
1780
+ if (!/^[A-Za-z0-9_@.-]+$/.test(name) || name === "." || name === "..") throw new Error("unsupported package path");
1781
+ walk(path.join(relative, name), [...ancestors, resolved]);
1782
+ }
1783
+ } else {
1784
+ if (!stat.isFile()) throw new Error("non-file package payload");
1785
+ const bytes = fs.readFileSync(input);
1786
+ const mode = stat.mode & 0o111 ? 0o700 : 0o600;
1787
+ persist(path.join(incoming, relative), bytes, mode);
1788
+ files[relative] = { digest: sha(bytes), mode };
1789
+ }
1790
+ };
1791
+ walk("");
1792
+ const actual = [];
1793
+ const inventory = relative => {
1794
+ for (const entry of fs.readdirSync(path.join(incoming, relative), { withFileTypes: true })) {
1795
+ const name = path.join(relative, entry.name);
1796
+ if (entry.isDirectory()) inventory(name); else actual.push(name);
1797
+ }
1798
+ };
1799
+ inventory("");
1800
+ if (JSON.stringify(actual.sort()) !== JSON.stringify(Object.keys(files).sort())) throw new Error("unexpected incoming package residue");
1801
+ for (const required of ["package.json", "npm-shrinkwrap.json", "dist/heart/daemon/sanctuary-authority-root-lifecycle.js", "dist/heart/daemon/sanctuary-telegram-authority-entry.js", "dist/heart/daemon/sanctuary-host-supervisor-entry.js", "deploy/unraid/sanctuary-host-launcher.sh", "deploy/unraid/sanctuary-authority-service.sh"]) {
1802
+ if (!files[required]) throw new Error(`missing authority package asset: ${required}`);
1803
+ }
1804
+ if (!Object.keys(files).some(name => name.startsWith("node_modules/"))) throw new Error("production dependencies are absent");
1805
+ const manifest = Buffer.from(`${JSON.stringify({ schemaVersion: 1, files })}\n`);
1806
+ if (manifest.byteLength > 8388608) throw new Error("complete package manifest exceeds the root lifecycle 8388608-byte limit; stop before token rotation");
1807
+ persist(`${root}/package-manifest.json`, manifest, 0o600);
1808
+ const primitive = file => {
1809
+ const stat = fs.statSync(file);
1810
+ if (!stat.isFile() || stat.uid !== 0 || stat.gid !== 0 || (stat.mode & 0o022) || !(stat.mode & 0o111)) throw new Error("unsafe host primitive");
1811
+ return sha(fs.readFileSync(file));
1812
+ };
1813
+ const request = { schemaVersion: 1, epochId, botId, ownerUserId: ownerId, ownerChatId: ownerId, packageDigest: sha(manifest), nodeDigest: primitive("/usr/local/bin/node"), prlimitDigest: primitive("/usr/bin/prlimit"), setsidDigest: primitive("/usr/bin/setsid"), shellDigest: primitive("/bin/sh") };
1814
+ persist(`${root}/request.json`, Buffer.from(`${JSON.stringify(request)}\n`), 0o600);
1815
+ if (fs.existsSync(`${root}/incoming-token`)) owned(`${root}/incoming-token`, 0o600);
1816
+ const fd = fs.openSync(root, "r");
1817
+ try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
1818
+ NODE
1819
+ )
1625
1820
  }
1626
1821
  run_sanctuary_docker() {
1627
1822
  /usr/bin/timeout -s KILL 20 /usr/bin/docker "$@"
@@ -1829,22 +2024,26 @@ Update:
1829
2024
  test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID"
1830
2025
  AUDIT_RUNNER_IMAGE_ID=$IMAGE_ID
1831
2026
  validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID"
2027
+ TARGET_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$IMAGE_ID")
2028
+ test "$TARGET_MOUNT_CONTRACT" = canonical-gateway
1832
2029
  TEMPLATE_ICON=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
1833
2030
  DOCKERMAN_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
1834
2031
  DOCKERMAN_TEMPLATE_JOURNAL=/boot/config/custom/ouro-butler/docker-man-template-transaction.json
1835
2032
  test "$DOCKERMAN_TEMPLATE_PATH" = /boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
1836
2033
  test "$DOCKERMAN_TEMPLATE_JOURNAL" = /boot/config/custom/ouro-butler/docker-man-template-transaction.json
1837
- Before stopping, renaming, or creating any Butler container, extract the packaged event, template, runtime-policy, and DockerMan transaction assets from that exact image ID. Do not copy these files from a checkout or another image.
2034
+ Before stopping, renaming, or creating any Butler container, extract the complete /opt/ouro package from that exact image ID, including production node_modules and package-relative dist. Do not copy these files from a checkout or another image. Use the package version extracted from the selected immutable image; version alone is not the mount contract. Only the reviewed new image's bot.ouro.sanctuary.mount-contract=canonical-gateway label admits the fourth mount. The immutable source alpha.816 ID above remains canonical-pre-gateway and must never be retagged as the gateway release.
1838
2035
  Stage the event assets, audit the original version-tagged template, create a separate temporary copy for exact local-image-ID auditing, and keep the private stage until the outer transaction commits:
1839
2036
  EVENT_ASSET_STAGE=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/ouro-events.XXXXXX)
1840
2037
  chmod 0700 "$EVENT_ASSET_STAGE"
1841
2038
  EVENT_SCRIPT_STAGE="$EVENT_ASSET_STAGE/ouro-events"
1842
2039
  mkdir "$EVENT_SCRIPT_STAGE"
2040
+ STAGED_PACKAGE_ROOT="$EVENT_ASSET_STAGE/package"
2041
+ mkdir "$STAGED_PACKAGE_ROOT"
1843
2042
  STAGED_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.xml"
1844
2043
  STAGED_EXACT_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.exact-image.xml"
1845
2044
  STAGED_RUNTIME_POLICY="$EVENT_ASSET_STAGE/container-runtime.json"
1846
- STAGED_DOCKERMAN_TRANSACTION="$EVENT_ASSET_STAGE/docker-man-template-transaction.mjs"
1847
- STAGED_DOCKERMAN_XML_VALIDATOR="$EVENT_ASSET_STAGE/docker-man-template-xml.cjs"
2045
+ STAGED_DOCKERMAN_TRANSACTION="$STAGED_PACKAGE_ROOT/deploy/unraid/docker-man-template-transaction.mjs"
2046
+ STAGED_DOCKERMAN_XML_VALIDATOR="$STAGED_PACKAGE_ROOT/deploy/unraid/docker-man-template-xml.cjs"
1848
2047
  EVENT_ASSET_CONTAINER=
1849
2048
  cleanup_event_asset_stage() {
1850
2049
  if test -n "$EVENT_ASSET_CONTAINER"; then
@@ -1855,14 +2054,13 @@ Update:
1855
2054
  trap cleanup_event_asset_stage EXIT
1856
2055
  EVENT_ASSET_CONTAINER=$(docker create --pull=never --network none --read-only --entrypoint /bin/false "$IMAGE_ID")
1857
2056
  test "$(docker inspect --format '{{.Image}}' "$EVENT_ASSET_CONTAINER")" = "$IMAGE_ID"
2057
+ docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/." "$STAGED_PACKAGE_ROOT/"
1858
2058
  docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/ouro-events/." "$EVENT_SCRIPT_STAGE/"
1859
2059
  docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/sanctuary.xml" "$STAGED_TEMPLATE"
1860
2060
  docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/container-runtime.json" "$STAGED_RUNTIME_POLICY"
1861
- docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/docker-man-template-transaction.mjs" "$STAGED_DOCKERMAN_TRANSACTION"
1862
- docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/docker-man-template-xml.cjs" "$STAGED_DOCKERMAN_XML_VALIDATOR"
1863
2061
  docker rm "$EVENT_ASSET_CONTAINER"
1864
2062
  EVENT_ASSET_CONTAINER=
1865
- EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json docker-man-template-transaction.mjs docker-man-template-xml.cjs ouro-events sanctuary.xml)
2063
+ EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json ouro-events package sanctuary.xml)
1866
2064
  ACTUAL_RELEASE_ASSETS=$(find "$EVENT_ASSET_STAGE" -mindepth 1 -maxdepth 1 -exec basename {} \; | LC_ALL=C sort)
1867
2065
  test "$ACTUAL_RELEASE_ASSETS" = "$EXPECTED_RELEASE_ASSETS"
1868
2066
  test -d "$EVENT_SCRIPT_STAGE" && test ! -L "$EVENT_SCRIPT_STAGE"
@@ -1872,6 +2070,8 @@ Update:
1872
2070
  test -f "$STAGED_DOCKERMAN_XML_VALIDATOR" && test ! -L "$STAGED_DOCKERMAN_XML_VALIDATOR"
1873
2071
  chown 0:0 "$STAGED_TEMPLATE" "$STAGED_RUNTIME_POLICY" "$STAGED_DOCKERMAN_TRANSACTION" "$STAGED_DOCKERMAN_XML_VALIDATOR"
1874
2072
  chmod 0600 "$STAGED_TEMPLATE" "$STAGED_RUNTIME_POLICY" "$STAGED_DOCKERMAN_TRANSACTION" "$STAGED_DOCKERMAN_XML_VALIDATOR"
2073
+ chown -R 0:0 "$STAGED_PACKAGE_ROOT"
2074
+ chmod 0700 "$STAGED_PACKAGE_ROOT"
1875
2075
  EXPECTED_EVENT_ASSETS=$(printf '%s\n' bootstrap-spool.sh emit-event.mjs emit-usenet-event.sh install-usenet-guard.sh usenet-health.sh)
1876
2076
  ACTUAL_EVENT_ASSETS=$(find "$EVENT_SCRIPT_STAGE" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | LC_ALL=C sort)
1877
2077
  test "$ACTUAL_EVENT_ASSETS" = "$EXPECTED_EVENT_ASSETS"
@@ -1910,7 +2110,7 @@ Update:
1910
2110
  --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
1911
2111
  --mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
1912
2112
  --mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
1913
- "$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE"
2113
+ "$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE" --mount-contract "$TARGET_MOUNT_CONTRACT"
1914
2114
  /usr/local/bin/node -e '
1915
2115
  const fs = require("node:fs");
1916
2116
  const [sourcePath, destinationPath, imageId] = process.argv.slice(1);
@@ -1930,7 +2130,7 @@ Update:
1930
2130
  --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
1931
2131
  --mount "type=bind,src=$STAGED_EXACT_TEMPLATE,dst=/audit/sanctuary.exact-image.xml,readonly" \
1932
2132
  --mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
1933
- "$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID"
2133
+ "$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID" --mount-contract "$TARGET_MOUNT_CONTRACT"
1934
2134
  recover_dockerman_template_transaction
1935
2135
  recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
1936
2136
  classify_sanctuary_update_source
@@ -1941,12 +2141,20 @@ Update:
1941
2141
  If boot activation or verification fails after that transaction commits, leave production untouched, repair or rerun this exact-image installation, and stop. Container and DockerMan rollback begin only after the later production preflight succeeds.
1942
2142
  For normal updates, preflight accepts only the pinned alpha.742 two-mount source or an already package-managed canonical source before any autostart or live-container change. The pinned exception may validate an old source, but can never authorize creation of a new target. Provider authentication and standalone credential-readiness helpers are not normal-update prechecks; never place credentials in arguments, shell variables or history.
1943
2143
  The DockerMan transaction binds Jellyfin's container ID, image ID, state, and restart count. Update, rollback and recovery must preserve that checkpoint.
1944
- Production must be the only running Butler poller; staging must be absent; rollback may be absent or one stopped container with the exact production image. A stopped legacy-evidence container is preserved. Disable every Butler name in Unraid's array-autostart file and verify that result before stopping production. First resolve and validate the exact image ID of the known-good production container while it is still running, so a lookup failure cannot strand a renamed container:
2144
+ For this authority migration, only the exact pinned alpha.816 predecessor may become rollback. The legacy-alpha742 digest remains supported for historical source audits, not a policy-weak authority rollback. Preserve one exact stopped alpha.816 predecessor, Jellyfin and every policy/history byte. Staging must be absent and production must be the only running resident. Capture the non-secret reviewed bot/owner/epoch inputs; OWNER_ID is the already authenticated private Telegram user/chat, never inferred from resident-written prose:
2145
+ AUTHORITY_EPOCH_ID=<new-reviewed-epoch-id>
2146
+ AUTHORITY_BOT_ID=8541786263
2147
+ AUTHORITY_OWNER_ID=<reviewed-private-owner-user-and-chat-id>
2148
+ install -d -m 0700 -o 0 -g 0 /mnt/user/appdata/ouro-authority
2149
+ Disable every Butler name in Unraid's array-autostart file before authority-install freezes the resident. First resolve and validate the exact known-good image while it is still running:
1945
2150
  /bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
1946
2151
  test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
1947
2152
  test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
1948
2153
  ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
1949
2154
  validate_exact_image_id "$ROLLBACK_IMAGE_ID"
2155
+ ROLLBACK_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$ROLLBACK_IMAGE_ID")
2156
+ test "$ROLLBACK_IMAGE_ID" = sha256:589b7cf8f96d139ee9fd86204a183126aadcbd3fbfb406063709be303397b1b2
2157
+ test "$ROLLBACK_MOUNT_CONTRACT" = canonical-pre-gateway
1950
2158
  if assert_update_topology "$ROLLBACK_IMAGE_ID"; then
1951
2159
  :
1952
2160
  else
@@ -1954,17 +2162,19 @@ Update:
1954
2162
  (exit "$UPDATE_PREFLIGHT_STATUS")
1955
2163
  fi
1956
2164
  assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
1957
- /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" prepare --source-template "$STAGED_TEMPLATE" --version-tag "$VERSION_IMAGE" --manifest-digest "$MANIFEST_DIGEST" --rollback-image-id "$ROLLBACK_IMAGE_ID" --target-image-id "$IMAGE_ID" >/dev/null
1958
- if provision_sanctuary_sab_credential "$IMAGE_ID" \
1959
- && verify_sanctuary_sab_readiness "$IMAGE_ID" \
1960
- && verify_sanctuary_telegram_readiness "$IMAGE_ID"; then
2165
+ if ( provision_sanctuary_sab_credential "$IMAGE_ID" ) \
2166
+ && verify_sanctuary_sab_readiness "$IMAGE_ID"; then
1961
2167
  :
1962
2168
  else
1963
2169
  PRECUTOVER_READINESS_STATUS=$?
1964
- /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
1965
2170
  (exit "$PRECUTOVER_READINESS_STATUS")
1966
2171
  fi
1967
- Guard the atomic autostart disable separately. If it fails, production has not been touched and the captured status is propagated:
2172
+ Prepare the complete package and request before human token revocation. The input helper's manifest-size check must pass the root lifecycle reader's 8388608-byte manifest limit; ordinary private records retain their 1048576-byte limit. Do not prune dependencies or split the inventory to bypass either bound. If this check refuses the full payload, leave the existing token, autostart and resident unchanged:
2173
+ prepare_sanctuary_authority_inputs "$STAGED_PACKAGE_ROOT"
2174
+ The input helper copies every regular package file and materializes only package-internal dependency symlinks as independent files, hashes the complete inventory with exact modes, and writes mode-0600 manifest/request files. The request binds schemaVersion, epochId, botId, ownerUserId, ownerChatId, packageDigest and the actual root node/prlimit/setsid/shell hashes. Review those hashes against the approved package and host primitives before prepare. Repeated preparation accepts only identical retained inputs; unknown extra files, external links or changed bytes fail closed. No network token probe or resident start occurs in preparation.
2175
+ Prepare the durable transaction and disable autostart before the human rotates any token:
2176
+ /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" prepare --source-template "$STAGED_TEMPLATE" --version-tag "$VERSION_IMAGE" --manifest-digest "$MANIFEST_DIGEST" --rollback-image-id "$ROLLBACK_IMAGE_ID" --target-image-id "$IMAGE_ID" >/dev/null
2177
+ Guard the atomic autostart disable separately. If it fails, the original token remains valid and production has not been touched; restore the template and propagate the captured status:
1968
2178
  if disable_butler_autostart; then
1969
2179
  :
1970
2180
  else
@@ -1972,6 +2182,37 @@ Update:
1972
2182
  /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
1973
2183
  (exit "$AUTOSTART_DISABLE_STATUS")
1974
2184
  fi
2185
+ Stop the exact predecessor before its final Telegram and filesystem readiness check. Otherwise a concurrent private-runtime receipt can appear between permission normalization and strict validation. The full package and durable transaction are already prepared, and autostart is disabled. Keep the stopped-resident mode strict: no live control socket or mode exception is allowed. Before token rotation, any failure restores the same policy-aware predecessor and propagates the failure; an interrupted stop remains recoverable from the prepared transaction:
2186
+ if docker stop --time 30 ouro-butler \
2187
+ && assert_only_running_butler - \
2188
+ && verify_sanctuary_telegram_readiness "$ROLLBACK_IMAGE_ID" strict; then
2189
+ :
2190
+ else
2191
+ PRECUTOVER_QUIESCENCE_STATUS=$?
2192
+ assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
2193
+ start_only_butler_for_recovery
2194
+ wait_butler_ready ouro-butler
2195
+ enable_butler_autostart
2196
+ /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
2197
+ (exit "$PRECUTOVER_QUIESCENCE_STATUS")
2198
+ fi
2199
+ Only after pinned stopped-image readiness, full-package preparation, durable transaction preparation and autostart disabling pass, the human rotates MendelowCloudButlerBot's token for that same numeric bot and uses the hidden root-terminal input helper. Do not echo the token, put it in argv/environment/history, copy it into the resident, or replace retained incoming-token during recovery. A failed or interrupted paste must be reconciled by the human before retry; retain the journal and disabled autostart, and do not invent a token:
2200
+ receive_sanctuary_authority_token
2201
+ test "$(stat -c '%u:%g:%a' /mnt/user/appdata/ouro-authority/incoming-token)" = 0:0:600
2202
+ Gateway installation and its tokenless readiness belong inside the stopped-resident transaction, never in the old live precheck. A failure attempts retirement and exact old-image restoration; failure of either keeps autostart disabled and the journals/root inputs intact:
2203
+ if /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-install >/dev/null \
2204
+ && verify_sanctuary_telegram_readiness "$IMAGE_ID"; then
2205
+ :
2206
+ else
2207
+ AUTHORITY_INSTALL_STATUS=$?
2208
+ retire_sanctuary_authority_if_pending
2209
+ assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
2210
+ start_only_butler_for_recovery
2211
+ wait_butler_ready ouro-butler
2212
+ enable_butler_autostart
2213
+ /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
2214
+ (exit "$AUTHORITY_INSTALL_STATUS")
2215
+ fi
1975
2216
  Define the stale-rollback cleanup used by the preparation guard:
1976
2217
  remove_stopped_rollback_if_present() {
1977
2218
  EXPECTED_STALE_ROLLBACK_IMAGE_ID=$1
@@ -2008,6 +2249,7 @@ ouro-butler-rollback
2008
2249
  PRODUCTION_PREPARATION_CLEANUP_STATUS=0
2009
2250
  PRODUCTION_PREPARATION_RECOVERY_SAFE=true
2010
2251
  /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null
2252
+ retire_sanctuary_authority_if_pending
2011
2253
  if docker container inspect ouro-butler >/dev/null 2>&1; then
2012
2254
  if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
2013
2255
  docker stop ouro-butler-rollback >/dev/null 2>&1 || true
@@ -2048,22 +2290,16 @@ ouro-butler-rollback
2048
2290
  fi
2049
2291
  Preparation failure therefore either restores the still-named exact production after attempting to remove any stale rollback, or renames the exact stopped rollback back. Both safe paths revalidate and start the old container, wait within the fixed bound, restore production-only autostart atomically, and return the original failure. If stale rollback removal fails, restore production only when the leftover is proven stopped; otherwise leave both containers untouched, roll back the journals, and return the cleanup failure for repair.
2050
2292
  If neither exact container can be found, the failure returns with Butler autostart disabled.
2051
- Do not start a target-image daemon between the production rename and final production activation. The exact-image static audit, download-queue readiness, and vault-backed Telegram identity check have already passed before autostart or live-container changes.
2293
+ Do not start a target-image daemon between the production rename and final production activation. The exact-image static audit and download-queue readiness have passed. The old image's pinned pre-gateway readiness ran before cutover; the new image's socket-mounted tokenless readiness ran only after authority-install proved the root gateway.
2052
2294
  Provider and complete daemon readiness are exercised only by the transactional production activation below. Its failure arm restores and revalidates the exact prior production, so a disposable daemon cannot reconcile or claim live external-event state before cutover.
2053
2295
  Create and activate production from the same exact image ID and exact authority in one explicit conditional so `set -eu` cannot exit before rollback. Only a successful create, effective audit, start, stopped-rollback assertion, and bounded readiness wait may enable production autostart.
2054
2296
  On failure, capture the activation status, remove only a partially created new production container, restore and audit the stopped rollback against its exact old image ID, prove it ready, restore production-only autostart atomically, and return the original failure. If the partial target cannot be removed, preserve its matching target bundle and retained migration receipt, roll back the DockerMan journal without attempting the colliding rename, then return the cleanup failure:
2055
2297
  if test "$(inspect_registry_manifest_digest "$VERSION_IMAGE")" = "$MANIFEST_DIGEST" \
2056
2298
  && test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
2057
- && docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
2058
- --label net.unraid.docker.managed=dockerman \
2059
- --label "net.unraid.docker.icon=$TEMPLATE_ICON" \
2060
- --mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
2061
- --mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
2062
- --mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
2063
- "$VERSION_IMAGE" \
2064
- && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" \
2299
+ && create_sanctuary_container "$IMAGE_ID" "$VERSION_IMAGE" "$TARGET_MOUNT_CONTRACT" \
2300
+ && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" \
2065
2301
  && assert_only_running_butler - \
2066
- && docker start ouro-butler \
2302
+ && /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-activate >/dev/null \
2067
2303
  && assert_only_running_butler ouro-butler \
2068
2304
  && test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" \
2069
2305
  && test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false \
@@ -2075,6 +2311,7 @@ ouro-butler-rollback
2075
2311
  PRODUCTION_ACTIVATION_STATUS=$?
2076
2312
  PRODUCTION_ACTIVATION_CLEANUP_STATUS=0
2077
2313
  /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null
2314
+ retire_sanctuary_authority_if_pending
2078
2315
  if docker container inspect ouro-butler >/dev/null 2>&1; then
2079
2316
  docker stop ouro-butler >/dev/null 2>&1 || true
2080
2317
  PARTIAL_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
@@ -2108,10 +2345,21 @@ ouro-butler-rollback
2108
2345
  /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" commit --proof "$FINAL_PROOF_PATH" >/dev/null
2109
2346
  cleanup_event_asset_stage
2110
2347
  trap - EXIT
2111
- Keep ouro-butler-rollback stopped until the new production container is proven or the explicit rollback arm restores it. Never create production from a mutable tag, a bare tag, or a bare local image ID.
2348
+ Keep ouro-butler-rollback stopped until the new production container is proven or the explicit rollback arm restores it. The rollback arm first retires root registrations and executions, stops the gateway, ends the epoch and restores the current valid token/cursor. Only then does it restore the prior bundle and rename the untouched three-mount alpha.816 container. start_only_butler_for_recovery invokes authority-restore while the authority journal exists; it cannot bypass retirement with docker start. Never create production from a mutable tag, a bare tag, or a bare local image ID.
2112
2349
  Docker tab Update, Force Update, Edit/Apply, Update All, and CA Action Centre updates remain visible but are unsupported because stock recreation deletes reviewed rollback evidence. Visibility, start, stop, and autostart remain supported; use only this reviewed version-tag transaction for updates.
2113
2350
  Community Apps determines installed state from the DockerMan template plus the live container name and image. The helper proves that same relationship without calling the endpoint that refreshes Community Apps' UI cache; the later live UI smoke confirms what Ari sees.
2114
2351
 
2352
+ Execution-only primitive refresh:
2353
+
2354
+ A reviewed Unraid update to prlimit or setsid makes new host execution unavailable, but the installed gateway can still boot for Telegram and retire for rollback. Package, Node and shell pins remain mandatory. Gateway launch uses Node's native detached process support; it does not execute an unreviewed setsid binary.
2355
+
2356
+ Root maintainer: stop the verified gateway process using its exact root-owned lock/PID identity, then confirm there is no pending DockerMan transaction or unresolved execution, supervisor or cgroup state. Review the current prlimit and setsid file hashes and ownership before supplying their full SHA-256 digests to the package-owned command below. The command verifies those exact bytes and metadata; it never adopts current hashes implicitly. It changes no token, issuer, owner, cursor, policy or grant. An interrupted two-file publication remains fail-closed and can be completed by repeating the same reviewed command.
2357
+
2358
+ /usr/local/bin/node /mnt/user/appdata/ouro-authority/package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js repin-execution sha256:REVIEWED_PRLIMIT_SHA256 sha256:REVIEWED_SETSID_SHA256
2359
+ /usr/local/bin/node /mnt/user/appdata/ouro-authority/package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js boot
2360
+
2361
+ Replace each REVIEWED_*_SHA256 placeholder with its reviewed 64 hexadecimal digits. After restart, verify the signed host.status health result before proposing a new host command. Changed package, Node or shell pins require repair of that separate boundary, not this execution-only refresh.
2362
+
2115
2363
  Backup:
2116
2364
  Set BACKUP_ROOT to a new absolute snapshot path on the destination filesystem.
2117
2365
  Set AUDIT_RUNNER_IMAGE_TAG to the reviewed new image containing the
@@ -2331,7 +2579,12 @@ Restore:
2331
2579
  process.stdout.write(expected);
2332
2580
  ' "$BACKUP_ROOT/provenance/container-inspect.json" "$BACKUP_ROOT/provenance/package-version")
2333
2581
  test "$(docker image inspect --format '{{.Id}}' "$RESTORE_VERSION_IMAGE")" = "$IMAGE_ID"
2334
- audit_registered_dockerman_template "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_VERSION_IMAGE"
2582
+ RESTORE_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$IMAGE_ID")
2583
+ audit_registered_dockerman_template "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_VERSION_IMAGE" "$RESTORE_MOUNT_CONTRACT"
2584
+ This data-snapshot Restore is not authority rollback. It must not overwrite a current gateway cursor, reintroduce a revoked resident token or remove a gateway mount behind the root owner. Refuse before mutation if a root epoch/config or deployment journal exists; use the retained Update transaction's authority-retire/authority-restore recovery instead. After a committed authority installation, obtain a reviewed root-aware restore transaction rather than applying this historical data snapshot:
2585
+ test ! -e /mnt/user/appdata/ouro-authority/active.json && test ! -L /mnt/user/appdata/ouro-authority/active.json
2586
+ test ! -e /boot/config/custom/ouro-butler/docker-man-template-transaction.json && test ! -L /boot/config/custom/ouro-butler/docker-man-template-transaction.json
2587
+ test "$RESTORE_MOUNT_CONTRACT" = canonical-pre-gateway
2335
2588
  Before changing autostart, durable roots, or live containers, run the nounset-safe input, backup-root, image, and topology preflight. It requires a nonempty canonical absolute BACKUP_ROOT other than /, both required directories, an exact local sha256 image ID, canonical production as the only running Butler, no staging or rollback, and at most one exact stopped legacy-evidence container. It also audits the live source container with the reviewed runner.
2336
2589
  Restore never rewrites DockerMan registration outside the reviewed template transaction. The preflight therefore stops unless the persistent template already names the exact snapshot version, and the post-start check proves DockerMan and Community Apps still recognize it.
2337
2590
  If the template names another version, first run the same reviewed version-tag update transaction for the snapshot version, then rerun Restore. A mismatch stops before autostart, durable-root, or production-container changes:
@@ -2388,14 +2641,8 @@ Restore:
2388
2641
  && /bin/bash "$HOST_RESTORE_INSTALLER" --restore-root "$BACKUP_ROOT/host" \
2389
2642
  && rm -f "$HOST_RESTORE_INSTALLER" \
2390
2643
  && docker image inspect "$IMAGE_ID" >/dev/null \
2391
- && docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
2392
- --label net.unraid.docker.managed=dockerman \
2393
- --label "net.unraid.docker.icon=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png" \
2394
- --mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
2395
- --mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
2396
- --mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
2397
- "$RESTORE_VERSION_IMAGE" \
2398
- && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png \
2644
+ && create_sanctuary_container "$IMAGE_ID" "$RESTORE_VERSION_IMAGE" "$RESTORE_MOUNT_CONTRACT" \
2645
+ && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_MOUNT_CONTRACT" "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png \
2399
2646
  && assert_only_running_butler - \
2400
2647
  && docker start ouro-butler \
2401
2648
  && assert_only_running_butler ouro-butler \
@@ -2546,13 +2793,8 @@ Packaged Unit 16 acceptance execution:
2546
2793
  execute it in the order below. Before every execution the launcher regenerates
2547
2794
  the config from the packaged fixed contract and requires byte-for-byte equality.
2548
2795
  Unit 16d-2 stops at the pre-model quarantine boundary: use a genuinely distinct private Telegram sender, confirm the fixed acknowledgement and owner admission card, and do not approve the contact during this scenario. The production-identical allow-to-one-turn continuation is covered by the Telegram admission integration suite when a second live account is unavailable. Unit 16h is acceptance-only: it exercises the delivery path against isolated state, restores exact health and cron bytes, and does not activate a production daily digest.
2549
- The cursor snapshot is deliberately materialized and executed twice around the
2550
- live scenario. Telegram bootstrap refreshes the canonical agent vault
2551
- `runtime/config` and keeps the bot token inside the consuming harness process.
2552
- It never reads the retired container credential file or carries the token in a
2553
- descriptor, argument, environment variable, shell variable, config, evidence,
2554
- or output. Callback injection alone maps its reviewed saved callback-update
2555
- JSON from host fd 3 through Docker stdin to in-container fd 3.
2796
+ The cursor snapshot is deliberately materialized and executed twice around the live scenario. Every telegram-offset proof uses the fresh signed telegram.cursor.snapshot logical progress digest, excluding snapshot time/signature rather than hashing a fixed resident offset file. Stale, absent or frozen resident offsets cannot satisfy cursor evidence. Telegram bootstrap confirms the already pinned root owner through gateway poll/settle; it performs no token acquisition, vault credential write or direct getUpdates. Its one-shot has network=none and a readonly runtime mount. Callback injection alone maps reviewed saved callback-update JSON from host fd 3 through Docker stdin to in-container fd 3.
2797
+ Allow the 90-second config materializer budget and the existing 65-second gateway socket wait; do not replace timeout failure with resident token readiness. Unit16b is explicitly retired as a production containment assertion and remains runtime-vault-readiness. Unit16e is the exact four-mount production snapshot with readOnlyRoot=false; mountsExact remains strict.
2556
2798
  Stage the reviewed callback JSON at the fixed path below in the root-owned
2557
2799
  tmpfs inbox, then use this single fail-closed helper. It opens the input once,
2558
2800
  validates the opened descriptor and its original path refer to the same
@@ -2674,15 +2916,7 @@ Packaged Unit 16 acceptance execution:
2674
2916
  Telegram runtime fields and both configured provider records were freshly read.
2675
2917
  Scenario handles remain private to the scenario adapter. The main one-shot
2676
2918
  never receives the Docker socket, Unraid key directory, or a host-root mount.
2677
- Telegram bootstrap additionally brackets its one-shot with a host-controlled
2678
- poller quiescence guard: it verifies the exact healthy staging container,
2679
- assumes recovery responsibility before attempting the stop, stops it with a
2680
- 30-second grace bound, proves it is stopped, and mounts a
2681
- root-owned typed zero-poller fact. Its exit/signal trap restarts that same exact
2682
- container and waits up to 120 seconds for healthy recovery on both success and
2683
- failure. It never reads or changes Unraid autostart configuration. Every command
2684
- also receives a freshly generated, redacted typed container-inspect snapshot;
2685
- raw container environment or credential values are never captured.
2919
+ Telegram bootstrap brackets its one-shot with a host-controlled quiescence guard: it verifies the exact healthy profile-selected resident, assumes recovery responsibility before stopping it with a 30-second grace bound, and proves it is stopped while one live root gateway owns polling. The broker proves the exact root lock/command/start identity and revoked predecessor token epoch; it does not accept a zero-poller file or a resident offset as authority. Every separately launched gateway consumer mounts only /run/ouro-authority readonly. The exit/signal trap restarts the same resident and waits up to 120 seconds for healthy recovery on success or failure without changing Unraid autostart. Every command receives a fresh redacted typed container snapshot, never raw environment or credential values.
2686
2920
  Callback injection requires two stable zero observations from a durable callback playback journal under
2687
2921
  `state/approvals`; the journal is keyed only by the full callback-coordinate
2688
2922
  digest and stores no raw update, callback data, user, chat, message, or query ID.