@ouro.bot/cli 0.1.0-alpha.815 → 0.1.0-alpha.817
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/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +18 -0
- package/deploy/unraid/Dockerfile +6 -0
- package/deploy/unraid/README.txt +305 -365
- package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
- package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
- package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/approval-store.js +11 -1
- package/dist/heart/core.js +2 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +9 -8
- package/dist/heart/daemon/container-spec-auditor.js +17 -24
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
- package/dist/heart/daemon/sense-manager.js +20 -10
- package/dist/heart/external-events/router.js +31 -15
- package/dist/heart/steward-policy.js +374 -58
- package/dist/heart/tool-approval.js +8 -1
- package/dist/repertoire/relationship-authorization.js +128 -0
- package/dist/repertoire/tools-base.js +9 -13
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools-steward-policy.js +34 -10
- package/dist/repertoire/tools-unraid.js +79 -32
- package/dist/repertoire/tools.js +37 -25
- package/dist/repertoire/unraid-restart.js +179 -52
- package/dist/senses/private-runtime.js +27 -12
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -0
- package/dist/senses/sanctuary-health-runner.js +0 -1
- package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
- package/dist/senses/sanctuary-runtime.js +2 -0
- package/dist/senses/telegram-admission.js +7 -0
- package/dist/senses/telegram-approval-runtime.js +130 -12
- package/dist/senses/telegram-attachments.js +5 -1
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +31 -8
- package/dist/senses/telegram-entry.js +1 -1
- package/dist/senses/telegram.js +174 -28
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/deploy/unraid/README.txt
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
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.
|
|
12
|
+
|
|
13
|
+
Bounded Jellyfin stewardship
|
|
14
|
+
|
|
15
|
+
Do not start a weak predecessor after policy exists. Once live A-006 authority has been provisioned, repair by forward-fixing and installing another policy-aware image through the existing exact-image update procedure. Its transaction mechanics are unchanged; a stopped older image is not permission to resume weaker policy enforcement.
|
|
16
|
+
|
|
17
|
+
Packaged desired states and grants remain empty. Build and installation create no authority and do not restart Jellyfin. Package migration preserves the live policy, audit and action-receipt bytes, including on a repeated migration.
|
|
18
|
+
|
|
19
|
+
A standing restart requires the intersection of an applied owner-stated positive desired state, an applied owner-stated grant and a fresh authorized request or exact current health event. Values on, always_on and expected_on permit either request-driven or event-driven evaluation; on_demand permits current owner or household requests only. Active off, disabled, paused, intentionally_off or intentionally_paused states deny mutation and make an unchanged expected health condition quiet. Missing, expired, inferred, unaudited, conflicted or mismatched positive state cannot authorize a standing restart. Installation provenance is not owner-stated authority.
|
|
20
|
+
|
|
21
|
+
Provision only after the policy-aware image is healthy and its package and running-image identities agree. Ari must supply a fresh authenticated owner Telegram message after installation. This runbook, old messages, agent-written setup text and installation records are not that authorization. The approved setup terms are container:jellyfin=on with no desired-state expiry; grant key unraid.restart:jellyfin, action unraid.container.restart, target jellyfin, 2 attempts per 1800000 ms, verificationRequired=true, exclusions=[], provenance=stated, and grant expiry exactly 365 days after authorization.
|
|
22
|
+
|
|
23
|
+
Use the existing steward_policy_manage actions sequentially: set_desired_state -> read -> grant_routine_action -> read. Carry the current readback version into each write. The first write must be applied and read back before the grant; desired state alone cannot authorize action. The final read must show both exact keys and versions, the stated provenance, rate, verification requirement, exclusions and calculated expiry. The two writes must have two linked applied v2 rows in state/policy/policy-audit.ndjson. Do not edit policy files directly or fabricate the authorizing event.
|
|
24
|
+
|
|
25
|
+
Use steward_policy_manage read for current policy and state/policy/action-receipts.ndjson for reservation and outcome evidence. Durable reservations count toward the limit, not only successful restarts. A reservation or attempt is not proof of a successful effect: retain definite no-effect, failed, indeterminate and verified-success outcomes without replaying an uncertain attempt. Fresh relationship, request, policy, grant, reservation and exact target checks still apply immediately before mutation.
|
|
26
|
+
|
|
27
|
+
When an owner request has only a missing or expired grant and no active negative desired state, the existing one-time owner approval path remains available. A conflicting negative state requires clarification or a policy update, not an approval workaround. Household and event denials never create a self-approval card.
|
|
28
|
+
|
|
29
|
+
Never manufacture a Jellyfin outage to demonstrate the grant. Record a natural event or ordinary request if one occurs; otherwise report organic operation as unobserved, separately from installed and provisioned state.
|
|
4
30
|
|
|
5
31
|
Start/stop:
|
|
6
32
|
docker start ouro-butler
|
|
@@ -21,19 +47,19 @@ Effective-spec audit helper:
|
|
|
21
47
|
AUDIT_CONTAINER=$1
|
|
22
48
|
AUDIT_EXPECTED_IMAGE=$2
|
|
23
49
|
AUDIT_RUNNER_IMAGE_ID=$3
|
|
24
|
-
AUDIT_MOUNT_CONTRACT=${4-
|
|
50
|
+
AUDIT_MOUNT_CONTRACT=${4-}
|
|
25
51
|
AUDIT_EXPECTED_IMAGE_REFERENCE=${5-}
|
|
26
52
|
AUDIT_EXPECTED_ICON=${6-}
|
|
27
53
|
validate_exact_image_id "$AUDIT_EXPECTED_IMAGE" || return $?
|
|
28
54
|
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
29
55
|
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
30
56
|
case "$AUDIT_MOUNT_CONTRACT" in
|
|
31
|
-
canonical)
|
|
57
|
+
canonical-pre-gateway|canonical-gateway)
|
|
32
58
|
test -n "$AUDIT_EXPECTED_IMAGE_REFERENCE" || return 1
|
|
33
59
|
test "$AUDIT_EXPECTED_ICON" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png || return 1
|
|
34
|
-
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"
|
|
35
61
|
;;
|
|
36
|
-
legacy-alpha742
|
|
62
|
+
legacy-alpha742) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
|
|
37
63
|
*) return 1 ;;
|
|
38
64
|
esac
|
|
39
65
|
INSPECT_DIR=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/inspect.XXXXXX) || return $?
|
|
@@ -278,6 +304,41 @@ Effective-spec audit helper:
|
|
|
278
304
|
esac
|
|
279
305
|
docker image inspect "$VALIDATE_IMAGE_ID" >/dev/null || return $?
|
|
280
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
|
+
}
|
|
281
342
|
assert_only_running_butler() {
|
|
282
343
|
(
|
|
283
344
|
EXPECTED_RUNNING_BUTLER=$1
|
|
@@ -362,7 +423,7 @@ Effective-spec audit helper:
|
|
|
362
423
|
validate_exact_image_id "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
363
424
|
test "$(docker inspect --format '{{.Image}}' "$PIN_SOURCE_CONTAINER")" = "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
364
425
|
case "$PIN_SOURCE_CONTAINER $PIN_SOURCE_IMAGE_ID" in
|
|
365
|
-
"ouro-butler sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d"
|
|
426
|
+
"ouro-butler sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d") return 0 ;;
|
|
366
427
|
"ouro-butler "*) ;;
|
|
367
428
|
*) return 1 ;;
|
|
368
429
|
esac
|
|
@@ -381,24 +442,16 @@ Effective-spec audit helper:
|
|
|
381
442
|
assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
382
443
|
audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" legacy-alpha742
|
|
383
444
|
}
|
|
384
|
-
assert_prepackage_alpha797_source() {
|
|
385
|
-
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
386
|
-
AUDIT_RUNNER_IMAGE_ID=$2
|
|
387
|
-
SOURCE_CONTAINER=$3
|
|
388
|
-
assert_sanctuary_update_source_pin "$SOURCE_CONTAINER" "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
389
|
-
audit_effective "$SOURCE_CONTAINER" "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" prepackage-alpha797
|
|
390
|
-
}
|
|
391
445
|
assert_update_source() {
|
|
392
446
|
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
393
447
|
AUDIT_RUNNER_IMAGE_ID=$2
|
|
394
448
|
if test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d; then
|
|
395
449
|
assert_legacy_alpha742_source "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
396
|
-
elif test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d; then
|
|
397
|
-
assert_prepackage_alpha797_source "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" ouro-butler
|
|
398
450
|
else
|
|
399
451
|
assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
400
452
|
EXPECTED_SOURCE_IMAGE_REFERENCE=$(docker inspect --format '{{.Config.Image}}' ouro-butler) || return $?
|
|
401
|
-
|
|
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
|
|
402
455
|
fi
|
|
403
456
|
}
|
|
404
457
|
validate_sanctuary_roots() {
|
|
@@ -858,11 +911,12 @@ Effective-spec audit helper:
|
|
|
858
911
|
audit_registered_dockerman_template() {
|
|
859
912
|
REGISTERED_TEMPLATE_RUNNER_IMAGE_ID=$1
|
|
860
913
|
REGISTERED_TEMPLATE_VERSION_IMAGE=$2
|
|
914
|
+
REGISTERED_TEMPLATE_MOUNT_CONTRACT=$3
|
|
861
915
|
REGISTERED_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
862
916
|
validate_exact_image_id "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" || return $?
|
|
863
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 $?
|
|
864
918
|
test -f "$REGISTERED_TEMPLATE_PATH" && test ! -L "$REGISTERED_TEMPLATE_PATH" || return 1
|
|
865
|
-
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 $?
|
|
866
920
|
}
|
|
867
921
|
verify_known_good_rollback_artifact() {
|
|
868
922
|
EXPECTED_KNOWN_GOOD_IMAGE_ID=$1
|
|
@@ -921,6 +975,12 @@ Effective-spec audit helper:
|
|
|
921
975
|
printf '%s\n' "$FINAL_PROOF_PATH"
|
|
922
976
|
}
|
|
923
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
|
|
924
984
|
RECOVERY_START_STATE=$(docker inspect --format '{{.State.Running}}' ouro-butler) || return $?
|
|
925
985
|
case "$RECOVERY_START_STATE" in
|
|
926
986
|
true)
|
|
@@ -973,6 +1033,7 @@ Effective-spec audit helper:
|
|
|
973
1033
|
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" commit || return $?
|
|
974
1034
|
return 0
|
|
975
1035
|
fi
|
|
1036
|
+
retire_sanctuary_authority_if_pending || return $?
|
|
976
1037
|
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
977
1038
|
RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
978
1039
|
test "$RECOVERY_CURRENT_ROLLBACK_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
@@ -1111,6 +1172,10 @@ Effective-spec audit helper:
|
|
|
1111
1172
|
TEMPLATE_RECOVERY_EVIDENCE=$EVENT_ASSET_STAGE/template-recovery-evidence.json
|
|
1112
1173
|
TEMPLATE_RECOVERY_INSPECTION=$EVENT_ASSET_STAGE/template-recovery-inspection.json
|
|
1113
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
|
|
1114
1179
|
if test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = rollback; then
|
|
1115
1180
|
recover_pending_sanctuary_bundle_migration "$IMAGE_ID" || return $?
|
|
1116
1181
|
TEMPLATE_RECOVERY_POST_BUNDLE_STATUS=$(read_sanctuary_bundle_transaction_status "$IMAGE_ID") || return $?
|
|
@@ -1134,7 +1199,7 @@ Effective-spec audit helper:
|
|
|
1134
1199
|
test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1135
1200
|
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
1136
1201
|
assert_only_running_butler ouro-butler || return $?
|
|
1137
|
-
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1202
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1138
1203
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1139
1204
|
chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1140
1205
|
write_dockerman_recovery_evidence committing target-exact-committing "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
@@ -1145,30 +1210,7 @@ Effective-spec audit helper:
|
|
|
1145
1210
|
verify_butler_autostart "1 0 0 0" || return $?
|
|
1146
1211
|
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_PRODUCTION_PRESENT" = false; then
|
|
1147
1212
|
test "$TEMPLATE_RECOVERY_STATE" = rollback || return 1
|
|
1148
|
-
|
|
1149
|
-
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
1150
|
-
! docker container inspect ouro-butler-legacy-evidence >/dev/null 2>&1 || return 1
|
|
1151
|
-
TEMPLATE_RECOVERY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
1152
|
-
test "$TEMPLATE_RECOVERY_STAGING_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1153
|
-
assert_prepackage_alpha797_source "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" ouro-butler-staging || return $?
|
|
1154
|
-
TEMPLATE_RECOVERY_STAGING_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-staging) || return $?
|
|
1155
|
-
case "$TEMPLATE_RECOVERY_STAGING_RUNNING" in
|
|
1156
|
-
true)
|
|
1157
|
-
assert_only_running_butler ouro-butler-staging || return $?
|
|
1158
|
-
;;
|
|
1159
|
-
false)
|
|
1160
|
-
assert_only_running_butler - || return $?
|
|
1161
|
-
docker start ouro-butler-staging || return $?
|
|
1162
|
-
assert_only_running_butler ouro-butler-staging || return $?
|
|
1163
|
-
;;
|
|
1164
|
-
*) return 1 ;;
|
|
1165
|
-
esac
|
|
1166
|
-
set_butler_autostart staging || return $?
|
|
1167
|
-
write_dockerman_recovery_evidence absent adoption-source-exact - "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1168
|
-
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"restore-prior-template"}' || return 1
|
|
1169
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1170
|
-
return 0
|
|
1171
|
-
fi
|
|
1213
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
1172
1214
|
assert_only_running_butler - || return $?
|
|
1173
1215
|
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1174
1216
|
TEMPLATE_RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
@@ -1209,7 +1251,7 @@ Effective-spec audit helper:
|
|
|
1209
1251
|
TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1210
1252
|
test "$TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1211
1253
|
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return 1
|
|
1212
|
-
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1254
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1213
1255
|
start_only_butler_for_recovery || return $?
|
|
1214
1256
|
wait_butler_ready ouro-butler || return $?
|
|
1215
1257
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
@@ -1219,7 +1261,7 @@ Effective-spec audit helper:
|
|
|
1219
1261
|
enable_butler_autostart || return $?
|
|
1220
1262
|
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null || return $?
|
|
1221
1263
|
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = committing && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"; then
|
|
1222
|
-
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1264
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1223
1265
|
wait_butler_ready ouro-butler || return $?
|
|
1224
1266
|
verify_butler_autostart "1 0 0 0" || return $?
|
|
1225
1267
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
@@ -1278,34 +1320,10 @@ Effective-spec audit helper:
|
|
|
1278
1320
|
PREPARE_WRONG_FILE_MODE=$(find "$PREPARE_RUNTIME_ROOT" "$PREPARE_AGENT_ROOT" -type f ! -perm 0600 -print -quit) || return $?
|
|
1279
1321
|
test -z "$PREPARE_WRONG_FILE_MODE" || return $?
|
|
1280
1322
|
}
|
|
1281
|
-
validate_sanctuary_legacy_staging() {
|
|
1282
|
-
EXPECTED_LEGACY_CONTAINER_ID=${1-}
|
|
1283
|
-
EXPECTED_LEGACY_IMAGE_ID=${2-}
|
|
1284
|
-
LEGACY_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1285
|
-
LEGACY_NAME_COUNTS=$(printf '%s\n' "$LEGACY_CONTAINER_NAMES" | awk '
|
|
1286
|
-
/butler/ { butlers++ }
|
|
1287
|
-
$0 == "ouro-butler-staging" { staging++ }
|
|
1288
|
-
END { printf "%d %d", butlers + 0, staging + 0 }
|
|
1289
|
-
') || return $?
|
|
1290
|
-
test "$LEGACY_NAME_COUNTS" = "1 1" || return $?
|
|
1291
|
-
assert_only_running_butler ouro-butler-staging || return $?
|
|
1292
|
-
LEGACY_STAGING_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-staging) || return $?
|
|
1293
|
-
test "$LEGACY_STAGING_RUNNING" = true || return $?
|
|
1294
|
-
LEGACY_STAGING_CONTAINER_ID=$(docker inspect --format '{{.Id}}' ouro-butler-staging) || return $?
|
|
1295
|
-
case "$LEGACY_STAGING_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
|
|
1296
|
-
test "${#LEGACY_STAGING_CONTAINER_ID}" -eq 64 || return 1
|
|
1297
|
-
LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
1298
|
-
validate_exact_image_id "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
1299
|
-
if test -n "$EXPECTED_LEGACY_CONTAINER_ID"; then
|
|
1300
|
-
test "$LEGACY_STAGING_CONTAINER_ID" = "$EXPECTED_LEGACY_CONTAINER_ID" || return 1
|
|
1301
|
-
test "$LEGACY_STAGING_IMAGE_ID" = "$EXPECTED_LEGACY_IMAGE_ID" || return 1
|
|
1302
|
-
fi
|
|
1303
|
-
}
|
|
1304
1323
|
classify_sanctuary_update_source() {
|
|
1305
1324
|
UPDATE_ENTRY_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1306
1325
|
if printf '%s\n' "$UPDATE_ENTRY_CONTAINER_NAMES" | grep -Fxq ouro-butler-staging; then
|
|
1307
|
-
|
|
1308
|
-
assert_sanctuary_update_source_pin ouro-butler-staging "$LEGACY_STAGING_IMAGE_ID"
|
|
1326
|
+
return 1
|
|
1309
1327
|
else
|
|
1310
1328
|
UPDATE_ENTRY_SOURCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
1311
1329
|
assert_update_topology "$UPDATE_ENTRY_SOURCE_IMAGE_ID" || return $?
|
|
@@ -1321,238 +1339,6 @@ Effective-spec audit helper:
|
|
|
1321
1339
|
classify_sanctuary_update_source
|
|
1322
1340
|
fi
|
|
1323
1341
|
}
|
|
1324
|
-
prepare_sanctuary_legacy_adoption() {
|
|
1325
|
-
IMAGE_ID=$1
|
|
1326
|
-
validate_exact_image_id "$IMAGE_ID" || return $?
|
|
1327
|
-
validate_sanctuary_legacy_staging || return $?
|
|
1328
|
-
PREPARED_LEGACY_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
|
|
1329
|
-
PREPARED_LEGACY_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
|
|
1330
|
-
assert_prepackage_alpha797_source "$PREPARED_LEGACY_IMAGE_ID" "$IMAGE_ID" ouro-butler-staging || return $?
|
|
1331
|
-
prepare_canonical_sanctuary_roots "$IMAGE_ID" || return $?
|
|
1332
|
-
bootstrap_sanctuary_vault "$IMAGE_ID" \
|
|
1333
|
-
/mnt/user/appdata/ouro-butler/runtime/container-credentials.json \
|
|
1334
|
-
sanctuary-unraid sanctuary || return $?
|
|
1335
|
-
provision_sanctuary_sab_credential "$IMAGE_ID" || return $?
|
|
1336
|
-
verify_sanctuary_sab_readiness "$IMAGE_ID" || return $?
|
|
1337
|
-
validate_sanctuary_legacy_staging "$PREPARED_LEGACY_CONTAINER_ID" "$PREPARED_LEGACY_IMAGE_ID" || return $?
|
|
1338
|
-
LEGACY_STAGING_CONTAINER_ID=$PREPARED_LEGACY_CONTAINER_ID
|
|
1339
|
-
LEGACY_STAGING_IMAGE_ID=$PREPARED_LEGACY_IMAGE_ID
|
|
1340
|
-
}
|
|
1341
|
-
capture_sanctuary_legacy_evidence() {
|
|
1342
|
-
CAPTURED_LEGACY_CONTAINER_ID=$1
|
|
1343
|
-
CAPTURED_LEGACY_IMAGE_ID=$2
|
|
1344
|
-
LEGACY_EVIDENCE_ROOT=$3
|
|
1345
|
-
case "$CAPTURED_LEGACY_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
|
|
1346
|
-
test "${#CAPTURED_LEGACY_CONTAINER_ID}" -eq 64 || return 1
|
|
1347
|
-
validate_exact_image_id "$CAPTURED_LEGACY_IMAGE_ID" || return $?
|
|
1348
|
-
case "$LEGACY_EVIDENCE_ROOT" in /*) ;; *) return 1 ;; esac
|
|
1349
|
-
if test -e "$LEGACY_EVIDENCE_ROOT" || test -L "$LEGACY_EVIDENCE_ROOT"; then
|
|
1350
|
-
/usr/local/bin/node -e '
|
|
1351
|
-
const fs = require("node:fs");
|
|
1352
|
-
const root = fs.lstatSync(process.argv[1]);
|
|
1353
|
-
if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o777) !== 0o700) process.exit(1);
|
|
1354
|
-
if (root.uid !== process.geteuid() || root.gid !== process.getegid()) process.exit(1);
|
|
1355
|
-
' "$LEGACY_EVIDENCE_ROOT" || return $?
|
|
1356
|
-
else
|
|
1357
|
-
install -d -m 0700 -o 0 -g 0 "$LEGACY_EVIDENCE_ROOT" || return $?
|
|
1358
|
-
fi
|
|
1359
|
-
LEGACY_EVIDENCE_DIR="$LEGACY_EVIDENCE_ROOT/${CAPTURED_LEGACY_IMAGE_ID#sha256:}"
|
|
1360
|
-
if test -e "$LEGACY_EVIDENCE_DIR" || test -L "$LEGACY_EVIDENCE_DIR"; then
|
|
1361
|
-
test -d "$LEGACY_EVIDENCE_DIR" && test ! -L "$LEGACY_EVIDENCE_DIR" || return 1
|
|
1362
|
-
else
|
|
1363
|
-
mkdir -m 0700 "$LEGACY_EVIDENCE_DIR" || return $?
|
|
1364
|
-
fi
|
|
1365
|
-
/usr/local/bin/node -e '
|
|
1366
|
-
const fs = require("node:fs");
|
|
1367
|
-
const [rootPath, directoryPath] = process.argv.slice(1);
|
|
1368
|
-
const root = fs.lstatSync(rootPath);
|
|
1369
|
-
const directory = fs.lstatSync(directoryPath);
|
|
1370
|
-
const privateDirectory = value => value.isDirectory() && !value.isSymbolicLink() && (value.mode & 0o777) === 0o700;
|
|
1371
|
-
if (!privateDirectory(root) || !privateDirectory(directory)) process.exit(1);
|
|
1372
|
-
if (root.uid !== process.geteuid() || root.gid !== process.getegid()) process.exit(1);
|
|
1373
|
-
if (root.uid !== directory.uid || root.gid !== directory.gid) process.exit(1);
|
|
1374
|
-
const allowed = new Set(["container.json", "image.json"]);
|
|
1375
|
-
if (fs.readdirSync(directoryPath).some(name => !allowed.has(name))) process.exit(1);
|
|
1376
|
-
' "$LEGACY_EVIDENCE_ROOT" "$LEGACY_EVIDENCE_DIR" || return $?
|
|
1377
|
-
validate_legacy_evidence_entry() {
|
|
1378
|
-
VALIDATED_EVIDENCE_KIND=$1
|
|
1379
|
-
VALIDATED_EVIDENCE_PATH=$2
|
|
1380
|
-
/usr/local/bin/node -e '
|
|
1381
|
-
const fs = require("node:fs");
|
|
1382
|
-
const [kind, filePath, rootPath, containerId, imageId] = process.argv.slice(1);
|
|
1383
|
-
const root = fs.lstatSync(rootPath);
|
|
1384
|
-
const entry = fs.lstatSync(filePath);
|
|
1385
|
-
if (!entry.isFile() || entry.isSymbolicLink() || entry.uid !== root.uid || entry.gid !== root.gid) process.exit(1);
|
|
1386
|
-
const mode = entry.mode & 0o777;
|
|
1387
|
-
if (mode !== 0o600 && mode !== 0o644) process.exit(1);
|
|
1388
|
-
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1389
|
-
if (!Array.isArray(value) || value.length !== 1 || !value[0] || typeof value[0] !== "object") process.exit(1);
|
|
1390
|
-
if (kind === "container" && (value[0].Id !== containerId || value[0].Image !== imageId)) process.exit(1);
|
|
1391
|
-
if (kind === "image" && value[0].Id !== imageId) process.exit(1);
|
|
1392
|
-
' "$VALIDATED_EVIDENCE_KIND" "$VALIDATED_EVIDENCE_PATH" "$LEGACY_EVIDENCE_ROOT" "$CAPTURED_LEGACY_CONTAINER_ID" "$CAPTURED_LEGACY_IMAGE_ID"
|
|
1393
|
-
}
|
|
1394
|
-
capture_missing_legacy_evidence_entry() {
|
|
1395
|
-
MISSING_EVIDENCE_KIND=$1
|
|
1396
|
-
MISSING_EVIDENCE_PATH=$2
|
|
1397
|
-
LEGACY_EVIDENCE_TMP=$(mktemp "$LEGACY_EVIDENCE_ROOT/.capture.XXXXXXXX") || return $?
|
|
1398
|
-
if test "$MISSING_EVIDENCE_KIND" = container; then
|
|
1399
|
-
docker container inspect "$CAPTURED_LEGACY_CONTAINER_ID" >"$LEGACY_EVIDENCE_TMP" || {
|
|
1400
|
-
CAPTURE_STATUS=$?
|
|
1401
|
-
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
1402
|
-
return "$CAPTURE_STATUS"
|
|
1403
|
-
}
|
|
1404
|
-
else
|
|
1405
|
-
docker image inspect "$CAPTURED_LEGACY_IMAGE_ID" >"$LEGACY_EVIDENCE_TMP" || {
|
|
1406
|
-
CAPTURE_STATUS=$?
|
|
1407
|
-
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
1408
|
-
return "$CAPTURE_STATUS"
|
|
1409
|
-
}
|
|
1410
|
-
fi
|
|
1411
|
-
chmod 0600 "$LEGACY_EVIDENCE_TMP" || {
|
|
1412
|
-
CAPTURE_STATUS=$?
|
|
1413
|
-
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
1414
|
-
return "$CAPTURE_STATUS"
|
|
1415
|
-
}
|
|
1416
|
-
validate_legacy_evidence_entry "$MISSING_EVIDENCE_KIND" "$LEGACY_EVIDENCE_TMP" || {
|
|
1417
|
-
CAPTURE_STATUS=$?
|
|
1418
|
-
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
1419
|
-
return "$CAPTURE_STATUS"
|
|
1420
|
-
}
|
|
1421
|
-
sync -f "$LEGACY_EVIDENCE_TMP" || {
|
|
1422
|
-
CAPTURE_STATUS=$?
|
|
1423
|
-
rm -f "$LEGACY_EVIDENCE_TMP"
|
|
1424
|
-
return "$CAPTURE_STATUS"
|
|
1425
|
-
}
|
|
1426
|
-
if ln "$LEGACY_EVIDENCE_TMP" "$MISSING_EVIDENCE_PATH"; then
|
|
1427
|
-
rm -f "$LEGACY_EVIDENCE_TMP" || return $?
|
|
1428
|
-
else
|
|
1429
|
-
rm -f "$LEGACY_EVIDENCE_TMP" || return $?
|
|
1430
|
-
validate_legacy_evidence_entry "$MISSING_EVIDENCE_KIND" "$MISSING_EVIDENCE_PATH" || return $?
|
|
1431
|
-
fi
|
|
1432
|
-
}
|
|
1433
|
-
for LEGACY_EVIDENCE_KIND in container image; do
|
|
1434
|
-
LEGACY_EVIDENCE_PATH="$LEGACY_EVIDENCE_DIR/$LEGACY_EVIDENCE_KIND.json"
|
|
1435
|
-
if test -e "$LEGACY_EVIDENCE_PATH" || test -L "$LEGACY_EVIDENCE_PATH"; then
|
|
1436
|
-
validate_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
|
|
1437
|
-
else
|
|
1438
|
-
capture_missing_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
|
|
1439
|
-
fi
|
|
1440
|
-
chmod 0600 "$LEGACY_EVIDENCE_PATH" || return $?
|
|
1441
|
-
validate_legacy_evidence_entry "$LEGACY_EVIDENCE_KIND" "$LEGACY_EVIDENCE_PATH" || return $?
|
|
1442
|
-
done
|
|
1443
|
-
sync -f "$LEGACY_EVIDENCE_DIR" || return $?
|
|
1444
|
-
}
|
|
1445
|
-
install_from_legacy_staging() {
|
|
1446
|
-
prepare_sanctuary_legacy_adoption "$IMAGE_ID" || return $?
|
|
1447
|
-
ADOPTION_PREPARED_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
|
|
1448
|
-
ADOPTION_PREPARED_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
|
|
1449
|
-
verify_sanctuary_provider_readiness "$IMAGE_ID" || return $?
|
|
1450
|
-
ADOPTION_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1451
|
-
ADOPTION_NAME_COUNTS=$(printf '%s\n' "$ADOPTION_CONTAINER_NAMES" | awk '
|
|
1452
|
-
/butler/ { butlers++ }
|
|
1453
|
-
$0 == "ouro-butler-staging" { staging++ }
|
|
1454
|
-
END { printf "%d %d", butlers + 0, staging + 0 }
|
|
1455
|
-
') || return $?
|
|
1456
|
-
test "$ADOPTION_NAME_COUNTS" = "1 1" || return $?
|
|
1457
|
-
assert_only_running_butler ouro-butler-staging || return $?
|
|
1458
|
-
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-staging)" = true || return $?
|
|
1459
|
-
test "$(docker inspect --format '{{.Id}}' ouro-butler-staging)" = "$ADOPTION_PREPARED_CONTAINER_ID" || return $?
|
|
1460
|
-
test "$(docker inspect --format '{{.Image}}' ouro-butler-staging)" = "$ADOPTION_PREPARED_IMAGE_ID" || return $?
|
|
1461
|
-
LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
|
|
1462
|
-
LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
|
|
1463
|
-
LEGACY_EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/legacy-evidence
|
|
1464
|
-
capture_sanctuary_legacy_evidence "$LEGACY_STAGING_CONTAINER_ID" "$LEGACY_STAGING_IMAGE_ID" "$LEGACY_EVIDENCE_ROOT" || return $?
|
|
1465
|
-
validate_sanctuary_legacy_staging "$ADOPTION_PREPARED_CONTAINER_ID" "$ADOPTION_PREPARED_IMAGE_ID" || return $?
|
|
1466
|
-
LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
|
|
1467
|
-
LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
|
|
1468
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" prepare --source-template "$STAGED_TEMPLATE" --version-tag "$VERSION_IMAGE" --manifest-digest "$MANIFEST_DIGEST" --rollback-image-id "$LEGACY_STAGING_IMAGE_ID" --target-image-id "$IMAGE_ID" >/dev/null || return $?
|
|
1469
|
-
if disable_butler_autostart; then
|
|
1470
|
-
:
|
|
1471
|
-
else
|
|
1472
|
-
ADOPTION_AUTOSTART_STATUS=$?
|
|
1473
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1474
|
-
return "$ADOPTION_AUTOSTART_STATUS"
|
|
1475
|
-
fi
|
|
1476
|
-
if docker stop "$LEGACY_STAGING_CONTAINER_ID" \
|
|
1477
|
-
&& CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' "$LEGACY_STAGING_CONTAINER_ID") \
|
|
1478
|
-
&& test "$CURRENT_LEGACY_STAGING_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
|
|
1479
|
-
&& test "$(docker inspect --format '{{.State.Running}}' "$LEGACY_STAGING_CONTAINER_ID")" = false \
|
|
1480
|
-
&& docker rename "$LEGACY_STAGING_CONTAINER_ID" ouro-butler-legacy-evidence \
|
|
1481
|
-
&& CURRENT_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) \
|
|
1482
|
-
&& test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
|
|
1483
|
-
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false \
|
|
1484
|
-
&& assert_only_running_butler - \
|
|
1485
|
-
&& test "$(inspect_registry_manifest_digest "$VERSION_IMAGE")" = "$MANIFEST_DIGEST" \
|
|
1486
|
-
&& test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
|
|
1487
|
-
&& docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
1488
|
-
--label net.unraid.docker.managed=dockerman \
|
|
1489
|
-
--label "net.unraid.docker.icon=$TEMPLATE_ICON" \
|
|
1490
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1491
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1492
|
-
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1493
|
-
"$VERSION_IMAGE" \
|
|
1494
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" \
|
|
1495
|
-
&& assert_only_running_butler - \
|
|
1496
|
-
&& docker start ouro-butler \
|
|
1497
|
-
&& assert_only_running_butler ouro-butler \
|
|
1498
|
-
&& test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" \
|
|
1499
|
-
&& wait_butler_ready ouro-butler \
|
|
1500
|
-
&& enable_butler_autostart \
|
|
1501
|
-
&& /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null; then
|
|
1502
|
-
ADOPTION_FINAL_ROOT="$EVENT_ASSET_STAGE/adoption-final-proof"
|
|
1503
|
-
ADOPTION_FINAL_PROOF=$(write_dockerman_final_proof "$IMAGE_ID" "$VERSION_IMAGE" "$TEMPLATE_ICON" "$ADOPTION_FINAL_ROOT") || return $?
|
|
1504
|
-
verify_known_good_rollback_artifact "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
1505
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" commit --proof "$ADOPTION_FINAL_PROOF" >/dev/null || return $?
|
|
1506
|
-
return 0
|
|
1507
|
-
else
|
|
1508
|
-
ADOPTION_STATUS=$?
|
|
1509
|
-
fi
|
|
1510
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null || return $?
|
|
1511
|
-
ADOPTION_RECOVERY_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1512
|
-
case "
|
|
1513
|
-
$ADOPTION_RECOVERY_NAMES
|
|
1514
|
-
" in
|
|
1515
|
-
*"
|
|
1516
|
-
ouro-butler
|
|
1517
|
-
"*)
|
|
1518
|
-
ADOPTION_PARTIAL_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
1519
|
-
test "$ADOPTION_PARTIAL_IMAGE_ID" = "$IMAGE_ID" || return $?
|
|
1520
|
-
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
1521
|
-
docker rm --force ouro-butler || return $?
|
|
1522
|
-
;;
|
|
1523
|
-
esac
|
|
1524
|
-
ADOPTION_RECOVERY_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1525
|
-
case "
|
|
1526
|
-
$ADOPTION_RECOVERY_NAMES
|
|
1527
|
-
" in
|
|
1528
|
-
*"
|
|
1529
|
-
ouro-butler-staging
|
|
1530
|
-
"*)
|
|
1531
|
-
CURRENT_LEGACY_STAGING_CONTAINER_ID=$(docker inspect --format '{{.Id}}' ouro-butler-staging) || return $?
|
|
1532
|
-
case "$CURRENT_LEGACY_STAGING_CONTAINER_ID" in *[!0-9a-f]*|'') return 1 ;; esac
|
|
1533
|
-
test "${#CURRENT_LEGACY_STAGING_CONTAINER_ID}" -eq 64 || return 1
|
|
1534
|
-
CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
1535
|
-
case "$CURRENT_LEGACY_STAGING_IMAGE_ID" in
|
|
1536
|
-
"$IMAGE_ID")
|
|
1537
|
-
docker stop ouro-butler-staging >/dev/null 2>&1 || true
|
|
1538
|
-
docker rm --force ouro-butler-staging || return $?
|
|
1539
|
-
;;
|
|
1540
|
-
"$LEGACY_STAGING_IMAGE_ID")
|
|
1541
|
-
test "$CURRENT_LEGACY_STAGING_CONTAINER_ID" = "$LEGACY_STAGING_CONTAINER_ID" || return 1
|
|
1542
|
-
docker stop "$LEGACY_STAGING_CONTAINER_ID" >/dev/null 2>&1 || return $?
|
|
1543
|
-
test "$(docker inspect --format '{{.State.Running}}' "$LEGACY_STAGING_CONTAINER_ID")" = false || return $?
|
|
1544
|
-
docker rename "$LEGACY_STAGING_CONTAINER_ID" ouro-butler-legacy-evidence || return $?
|
|
1545
|
-
;;
|
|
1546
|
-
*) return 1 ;;
|
|
1547
|
-
esac
|
|
1548
|
-
;;
|
|
1549
|
-
esac
|
|
1550
|
-
CURRENT_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1551
|
-
test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
1552
|
-
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return $?
|
|
1553
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1554
|
-
return "$ADOPTION_STATUS"
|
|
1555
|
-
}
|
|
1556
1342
|
validate_sanctuary_legacy_import_marker() {
|
|
1557
1343
|
IMPORT_MARKER=$1
|
|
1558
1344
|
IMPORT_SOURCE=$2
|
|
@@ -1880,6 +1666,12 @@ NODE
|
|
|
1880
1666
|
verify_sanctuary_telegram_readiness() {
|
|
1881
1667
|
TELEGRAM_READINESS_IMAGE_ID=$1
|
|
1882
1668
|
validate_exact_image_id "$TELEGRAM_READINESS_IMAGE_ID" || return $?
|
|
1669
|
+
TELEGRAM_READINESS_CONTRACT=$(sanctuary_image_mount_contract "$TELEGRAM_READINESS_IMAGE_ID") || return $?
|
|
1670
|
+
case "$TELEGRAM_READINESS_CONTRACT" in
|
|
1671
|
+
canonical-gateway) set -- --mount "type=bind,src=/run/ouro-authority,dst=/run/ouro-authority,readonly" ;;
|
|
1672
|
+
canonical-pre-gateway) set -- ;;
|
|
1673
|
+
*) return 1 ;;
|
|
1674
|
+
esac
|
|
1883
1675
|
TELEGRAM_READINESS_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1884
1676
|
TELEGRAM_READINESS_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1885
1677
|
! docker container inspect ouro-butler-telegram-readiness >/dev/null 2>&1 || return 1
|
|
@@ -1888,10 +1680,142 @@ NODE
|
|
|
1888
1680
|
--mount "type=bind,src=$TELEGRAM_READINESS_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1889
1681
|
--mount "type=bind,src=$TELEGRAM_READINESS_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro,readonly" \
|
|
1890
1682
|
--entrypoint /opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh \
|
|
1891
|
-
"$TELEGRAM_READINESS_IMAGE_ID" telegram-readiness >/dev/null || return $?
|
|
1683
|
+
"$@" "$TELEGRAM_READINESS_IMAGE_ID" telegram-readiness >/dev/null || return $?
|
|
1892
1684
|
! docker container inspect ouro-butler-telegram-readiness >/dev/null 2>&1 || return 1
|
|
1893
1685
|
normalize_sanctuary_private_permissions "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" "$TELEGRAM_READINESS_IMAGE_ID" || return $?
|
|
1894
|
-
|
|
1686
|
+
if test "$TELEGRAM_READINESS_CONTRACT" = canonical-gateway; then
|
|
1687
|
+
validate_sanctuary_roots "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" || return $?
|
|
1688
|
+
else
|
|
1689
|
+
validate_sanctuary_roots "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" live-precutover || return $?
|
|
1690
|
+
fi
|
|
1691
|
+
}
|
|
1692
|
+
read_sanctuary_authority_state() {
|
|
1693
|
+
(
|
|
1694
|
+
test -n "${STAGED_DOCKERMAN_TRANSACTION-}" || return 1
|
|
1695
|
+
AUTHORITY_STATUS=$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" status) || return $?
|
|
1696
|
+
printf '%s' "$AUTHORITY_STATUS" | /usr/local/bin/node -e '
|
|
1697
|
+
let input = "";
|
|
1698
|
+
process.stdin.setEncoding("utf8");
|
|
1699
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1700
|
+
process.stdin.on("end", () => {
|
|
1701
|
+
const record = JSON.parse(input);
|
|
1702
|
+
if (record === null || (record && !Object.hasOwn(record, "authority"))) process.stdout.write("none");
|
|
1703
|
+
else if (["installing", "active", "retiring", "retired"].includes(record?.authority?.state)) process.stdout.write(record.authority.state);
|
|
1704
|
+
else process.exit(1);
|
|
1705
|
+
});
|
|
1706
|
+
' || return $?
|
|
1707
|
+
)
|
|
1708
|
+
}
|
|
1709
|
+
retire_sanctuary_authority_if_pending() {
|
|
1710
|
+
(
|
|
1711
|
+
AUTHORITY_STATE=$(read_sanctuary_authority_state) || return $?
|
|
1712
|
+
test "$AUTHORITY_STATE" != none || return 0
|
|
1713
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-retire >/dev/null || return $?
|
|
1714
|
+
)
|
|
1715
|
+
}
|
|
1716
|
+
receive_sanctuary_authority_token() {
|
|
1717
|
+
(
|
|
1718
|
+
test "$(id -u)" = 0 && test -t 0 || return 1
|
|
1719
|
+
test "$(stat -c '%u:%g:%a' /mnt/user/appdata/ouro-authority)" = 0:0:700 || return 1
|
|
1720
|
+
test ! -e /mnt/user/appdata/ouro-authority/incoming-token && test ! -L /mnt/user/appdata/ouro-authority/incoming-token || return 1
|
|
1721
|
+
TOKEN_TERMINAL_MODE=$(stty -g) || return $?
|
|
1722
|
+
trap 'stty "$TOKEN_TERMINAL_MODE"' EXIT
|
|
1723
|
+
trap 'exit 129' HUP
|
|
1724
|
+
trap 'exit 130' INT
|
|
1725
|
+
trap 'exit 143' TERM
|
|
1726
|
+
printf '%s\n' 'Human: paste the freshly rotated same-bot token, press Enter, then Ctrl-D. Input is hidden.' >&2
|
|
1727
|
+
stty -echo || return $?
|
|
1728
|
+
umask 077
|
|
1729
|
+
set -C
|
|
1730
|
+
dd status=none > /mnt/user/appdata/ouro-authority/incoming-token || return $?
|
|
1731
|
+
test -s /mnt/user/appdata/ouro-authority/incoming-token || return 1
|
|
1732
|
+
sync -f /mnt/user/appdata/ouro-authority/incoming-token || return $?
|
|
1733
|
+
)
|
|
1734
|
+
}
|
|
1735
|
+
prepare_sanctuary_authority_inputs() {
|
|
1736
|
+
(
|
|
1737
|
+
AUTHORITY_SOURCE=$1
|
|
1738
|
+
test "$(id -u)" = 0 || return 1
|
|
1739
|
+
/usr/local/bin/node - "$AUTHORITY_SOURCE" "$AUTHORITY_EPOCH_ID" "$AUTHORITY_BOT_ID" "$AUTHORITY_OWNER_ID" <<'NODE'
|
|
1740
|
+
const fs = require("node:fs");
|
|
1741
|
+
const path = require("node:path");
|
|
1742
|
+
const crypto = require("node:crypto");
|
|
1743
|
+
const [source, epochId, botId, ownerId] = process.argv.slice(2);
|
|
1744
|
+
const root = "/mnt/user/appdata/ouro-authority";
|
|
1745
|
+
const incoming = `${root}/incoming-package`;
|
|
1746
|
+
const sha = bytes => `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
1747
|
+
const directory = target => {
|
|
1748
|
+
if (!fs.existsSync(target)) fs.mkdirSync(target, { mode: 0o700 });
|
|
1749
|
+
const stat = fs.lstatSync(target);
|
|
1750
|
+
if (!stat.isDirectory() || fs.realpathSync(target) !== target || stat.uid !== 0 || stat.gid !== 0 || (stat.mode & 0o7777) !== 0o700) throw new Error("unsafe authority input directory");
|
|
1751
|
+
};
|
|
1752
|
+
const owned = (target, mode) => {
|
|
1753
|
+
const stat = fs.lstatSync(target);
|
|
1754
|
+
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");
|
|
1755
|
+
return fs.readFileSync(target);
|
|
1756
|
+
};
|
|
1757
|
+
const persist = (target, bytes, mode) => {
|
|
1758
|
+
if (fs.existsSync(target)) {
|
|
1759
|
+
if (!owned(target, mode).equals(bytes)) throw new Error("authority input conflicts with retained recovery bytes");
|
|
1760
|
+
} else {
|
|
1761
|
+
const fd = fs.openSync(target, "wx", mode);
|
|
1762
|
+
try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
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");
|
|
1766
|
+
directory(root);
|
|
1767
|
+
directory(incoming);
|
|
1768
|
+
const sourceRoot = fs.realpathSync(source);
|
|
1769
|
+
const files = {};
|
|
1770
|
+
const walk = (relative, ancestors = []) => {
|
|
1771
|
+
const input = path.join(sourceRoot, relative);
|
|
1772
|
+
const resolved = fs.realpathSync(input);
|
|
1773
|
+
if (resolved !== sourceRoot && !resolved.startsWith(`${sourceRoot}/`)) throw new Error("package link escapes exact image payload");
|
|
1774
|
+
if (ancestors.includes(resolved)) throw new Error("package link cycle");
|
|
1775
|
+
const stat = fs.statSync(input);
|
|
1776
|
+
if (stat.isDirectory()) {
|
|
1777
|
+
directory(path.join(incoming, relative));
|
|
1778
|
+
for (const name of fs.readdirSync(input).sort()) {
|
|
1779
|
+
if (!/^[A-Za-z0-9_@.-]+$/.test(name) || name === "." || name === "..") throw new Error("unsupported package path");
|
|
1780
|
+
walk(path.join(relative, name), [...ancestors, resolved]);
|
|
1781
|
+
}
|
|
1782
|
+
} else {
|
|
1783
|
+
if (!stat.isFile()) throw new Error("non-file package payload");
|
|
1784
|
+
const bytes = fs.readFileSync(input);
|
|
1785
|
+
const mode = stat.mode & 0o111 ? 0o700 : 0o600;
|
|
1786
|
+
persist(path.join(incoming, relative), bytes, mode);
|
|
1787
|
+
files[relative] = { digest: sha(bytes), mode };
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
walk("");
|
|
1791
|
+
const actual = [];
|
|
1792
|
+
const inventory = relative => {
|
|
1793
|
+
for (const entry of fs.readdirSync(path.join(incoming, relative), { withFileTypes: true })) {
|
|
1794
|
+
const name = path.join(relative, entry.name);
|
|
1795
|
+
if (entry.isDirectory()) inventory(name); else actual.push(name);
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
inventory("");
|
|
1799
|
+
if (JSON.stringify(actual.sort()) !== JSON.stringify(Object.keys(files).sort())) throw new Error("unexpected incoming package residue");
|
|
1800
|
+
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"]) {
|
|
1801
|
+
if (!files[required]) throw new Error(`missing authority package asset: ${required}`);
|
|
1802
|
+
}
|
|
1803
|
+
if (!Object.keys(files).some(name => name.startsWith("node_modules/"))) throw new Error("production dependencies are absent");
|
|
1804
|
+
const manifest = Buffer.from(`${JSON.stringify({ schemaVersion: 1, files })}\n`);
|
|
1805
|
+
if (manifest.byteLength > 8388608) throw new Error("complete package manifest exceeds the root lifecycle 8388608-byte limit; stop before token rotation");
|
|
1806
|
+
persist(`${root}/package-manifest.json`, manifest, 0o600);
|
|
1807
|
+
const primitive = file => {
|
|
1808
|
+
const stat = fs.statSync(file);
|
|
1809
|
+
if (!stat.isFile() || stat.uid !== 0 || stat.gid !== 0 || (stat.mode & 0o022) || !(stat.mode & 0o111)) throw new Error("unsafe host primitive");
|
|
1810
|
+
return sha(fs.readFileSync(file));
|
|
1811
|
+
};
|
|
1812
|
+
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") };
|
|
1813
|
+
persist(`${root}/request.json`, Buffer.from(`${JSON.stringify(request)}\n`), 0o600);
|
|
1814
|
+
if (fs.existsSync(`${root}/incoming-token`)) owned(`${root}/incoming-token`, 0o600);
|
|
1815
|
+
const fd = fs.openSync(root, "r");
|
|
1816
|
+
try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
1817
|
+
NODE
|
|
1818
|
+
)
|
|
1895
1819
|
}
|
|
1896
1820
|
run_sanctuary_docker() {
|
|
1897
1821
|
/usr/bin/timeout -s KILL 20 /usr/bin/docker "$@"
|
|
@@ -2085,7 +2009,7 @@ NODE
|
|
|
2085
2009
|
return 1
|
|
2086
2010
|
}
|
|
2087
2011
|
Update:
|
|
2088
|
-
Before downloading or staging anything, classify the live source with the existing topology gates unless the fixed root-owned DockerMan transaction journal signals that an interrupted update needs the exact packaged recovery tools.
|
|
2012
|
+
Before downloading or staging anything, classify the live production source with the existing topology gates unless the fixed root-owned DockerMan transaction journal signals that an interrupted update needs the exact packaged recovery tools. Staging-only adoption is retired. A recovery journal admits only image acquisition and private staging until the staged transaction implementation validates the full journal and restores a classifiable source:
|
|
2089
2013
|
admit_sanctuary_update_entry
|
|
2090
2014
|
Set the released package version, its reviewed release-manifest digest, and its reviewed platform-local image ID. The canonical package-version tag is immutable release identity, while the digest and local ID independently prove what the registry and this host resolved:
|
|
2091
2015
|
PACKAGE_VERSION=<released-version>
|
|
@@ -2099,22 +2023,26 @@ Update:
|
|
|
2099
2023
|
test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID"
|
|
2100
2024
|
AUDIT_RUNNER_IMAGE_ID=$IMAGE_ID
|
|
2101
2025
|
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID"
|
|
2026
|
+
TARGET_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$IMAGE_ID")
|
|
2027
|
+
test "$TARGET_MOUNT_CONTRACT" = canonical-gateway
|
|
2102
2028
|
TEMPLATE_ICON=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
|
|
2103
2029
|
DOCKERMAN_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
2104
2030
|
DOCKERMAN_TEMPLATE_JOURNAL=/boot/config/custom/ouro-butler/docker-man-template-transaction.json
|
|
2105
2031
|
test "$DOCKERMAN_TEMPLATE_PATH" = /boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
2106
2032
|
test "$DOCKERMAN_TEMPLATE_JOURNAL" = /boot/config/custom/ouro-butler/docker-man-template-transaction.json
|
|
2107
|
-
Before stopping, renaming, or creating any Butler container, extract the
|
|
2033
|
+
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.
|
|
2108
2034
|
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:
|
|
2109
2035
|
EVENT_ASSET_STAGE=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/ouro-events.XXXXXX)
|
|
2110
2036
|
chmod 0700 "$EVENT_ASSET_STAGE"
|
|
2111
2037
|
EVENT_SCRIPT_STAGE="$EVENT_ASSET_STAGE/ouro-events"
|
|
2112
2038
|
mkdir "$EVENT_SCRIPT_STAGE"
|
|
2039
|
+
STAGED_PACKAGE_ROOT="$EVENT_ASSET_STAGE/package"
|
|
2040
|
+
mkdir "$STAGED_PACKAGE_ROOT"
|
|
2113
2041
|
STAGED_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.xml"
|
|
2114
2042
|
STAGED_EXACT_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.exact-image.xml"
|
|
2115
2043
|
STAGED_RUNTIME_POLICY="$EVENT_ASSET_STAGE/container-runtime.json"
|
|
2116
|
-
STAGED_DOCKERMAN_TRANSACTION="$
|
|
2117
|
-
STAGED_DOCKERMAN_XML_VALIDATOR="$
|
|
2044
|
+
STAGED_DOCKERMAN_TRANSACTION="$STAGED_PACKAGE_ROOT/deploy/unraid/docker-man-template-transaction.mjs"
|
|
2045
|
+
STAGED_DOCKERMAN_XML_VALIDATOR="$STAGED_PACKAGE_ROOT/deploy/unraid/docker-man-template-xml.cjs"
|
|
2118
2046
|
EVENT_ASSET_CONTAINER=
|
|
2119
2047
|
cleanup_event_asset_stage() {
|
|
2120
2048
|
if test -n "$EVENT_ASSET_CONTAINER"; then
|
|
@@ -2125,14 +2053,13 @@ Update:
|
|
|
2125
2053
|
trap cleanup_event_asset_stage EXIT
|
|
2126
2054
|
EVENT_ASSET_CONTAINER=$(docker create --pull=never --network none --read-only --entrypoint /bin/false "$IMAGE_ID")
|
|
2127
2055
|
test "$(docker inspect --format '{{.Image}}' "$EVENT_ASSET_CONTAINER")" = "$IMAGE_ID"
|
|
2056
|
+
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/." "$STAGED_PACKAGE_ROOT/"
|
|
2128
2057
|
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/ouro-events/." "$EVENT_SCRIPT_STAGE/"
|
|
2129
2058
|
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/sanctuary.xml" "$STAGED_TEMPLATE"
|
|
2130
2059
|
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/container-runtime.json" "$STAGED_RUNTIME_POLICY"
|
|
2131
|
-
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/docker-man-template-transaction.mjs" "$STAGED_DOCKERMAN_TRANSACTION"
|
|
2132
|
-
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/docker-man-template-xml.cjs" "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
2133
2060
|
docker rm "$EVENT_ASSET_CONTAINER"
|
|
2134
2061
|
EVENT_ASSET_CONTAINER=
|
|
2135
|
-
EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json
|
|
2062
|
+
EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json ouro-events package sanctuary.xml)
|
|
2136
2063
|
ACTUAL_RELEASE_ASSETS=$(find "$EVENT_ASSET_STAGE" -mindepth 1 -maxdepth 1 -exec basename {} \; | LC_ALL=C sort)
|
|
2137
2064
|
test "$ACTUAL_RELEASE_ASSETS" = "$EXPECTED_RELEASE_ASSETS"
|
|
2138
2065
|
test -d "$EVENT_SCRIPT_STAGE" && test ! -L "$EVENT_SCRIPT_STAGE"
|
|
@@ -2142,6 +2069,8 @@ Update:
|
|
|
2142
2069
|
test -f "$STAGED_DOCKERMAN_XML_VALIDATOR" && test ! -L "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
2143
2070
|
chown 0:0 "$STAGED_TEMPLATE" "$STAGED_RUNTIME_POLICY" "$STAGED_DOCKERMAN_TRANSACTION" "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
2144
2071
|
chmod 0600 "$STAGED_TEMPLATE" "$STAGED_RUNTIME_POLICY" "$STAGED_DOCKERMAN_TRANSACTION" "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
2072
|
+
chown -R 0:0 "$STAGED_PACKAGE_ROOT"
|
|
2073
|
+
chmod 0700 "$STAGED_PACKAGE_ROOT"
|
|
2145
2074
|
EXPECTED_EVENT_ASSETS=$(printf '%s\n' bootstrap-spool.sh emit-event.mjs emit-usenet-event.sh install-usenet-guard.sh usenet-health.sh)
|
|
2146
2075
|
ACTUAL_EVENT_ASSETS=$(find "$EVENT_SCRIPT_STAGE" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | LC_ALL=C sort)
|
|
2147
2076
|
test "$ACTUAL_EVENT_ASSETS" = "$EXPECTED_EVENT_ASSETS"
|
|
@@ -2180,7 +2109,7 @@ Update:
|
|
|
2180
2109
|
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
2181
2110
|
--mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
|
|
2182
2111
|
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
2183
|
-
"$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE"
|
|
2112
|
+
"$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE" --mount-contract "$TARGET_MOUNT_CONTRACT"
|
|
2184
2113
|
/usr/local/bin/node -e '
|
|
2185
2114
|
const fs = require("node:fs");
|
|
2186
2115
|
const [sourcePath, destinationPath, imageId] = process.argv.slice(1);
|
|
@@ -2200,7 +2129,7 @@ Update:
|
|
|
2200
2129
|
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
2201
2130
|
--mount "type=bind,src=$STAGED_EXACT_TEMPLATE,dst=/audit/sanctuary.exact-image.xml,readonly" \
|
|
2202
2131
|
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
2203
|
-
"$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID"
|
|
2132
|
+
"$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID" --mount-contract "$TARGET_MOUNT_CONTRACT"
|
|
2204
2133
|
recover_dockerman_template_transaction
|
|
2205
2134
|
recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
|
|
2206
2135
|
classify_sanctuary_update_source
|
|
@@ -2209,24 +2138,22 @@ Update:
|
|
|
2209
2138
|
verify_installed_usenet_guard
|
|
2210
2139
|
If extraction or transactional installation fails, abort before changing any live Butler container. The installer restores the previous assets, go file, and cron; production stays running.
|
|
2211
2140
|
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.
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
That function never applies the package-managed target contract to the older alpha.797 container or restarts it. It first proves the pinned alpha.797 image and exact allowed legacy shape, creates the missing canonical roots from the reviewed target image, checks required files and permissions, and finishes vault plus provider preparation. Any failure leaves the old Butler running with autostart untouched.
|
|
2221
|
-
After preparation succeeds, it records the exact old container and image, installs the original version-tagged template under the crash journal, disables Butler autostart, stops and rechecks the old container, and renames it to stopped ouro-butler-legacy-evidence. The journal also binds Jellyfin's exact container ID, image ID, state, and restart count under its digest. It then creates one canonical production container from the reviewed version tag with --pull=never, audits it before start, proves it is the only running healthy Butler, enables only production autostart, proves the stopped rollback plus DockerMan and Community Apps recognition, and commits the template journal only while Jellyfin remains unchanged.
|
|
2222
|
-
If activation fails, it removes only a partial target container, preserves the stopped legacy evidence, restores the prior template, leaves autostart disabled, and returns the original failure.
|
|
2223
|
-
For normal updates, preflight accepts only the pinned alpha.742 two-mount source, the pinned pre-package-managed alpha.797 source, or an already package-managed canonical source before any autostart or live-container change. The two pinned exceptions may validate an old source, but can never authorize creation of a new target.
|
|
2224
|
-
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:
|
|
2141
|
+
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.
|
|
2142
|
+
The DockerMan transaction binds Jellyfin's container ID, image ID, state, and restart count. Update, rollback and recovery must preserve that checkpoint.
|
|
2143
|
+
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:
|
|
2144
|
+
AUTHORITY_EPOCH_ID=<new-reviewed-epoch-id>
|
|
2145
|
+
AUTHORITY_BOT_ID=8541786263
|
|
2146
|
+
AUTHORITY_OWNER_ID=<reviewed-private-owner-user-and-chat-id>
|
|
2147
|
+
install -d -m 0700 -o 0 -g 0 /mnt/user/appdata/ouro-authority
|
|
2148
|
+
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:
|
|
2225
2149
|
/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
|
|
2226
2150
|
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
2227
2151
|
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
|
|
2228
2152
|
ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
2229
2153
|
validate_exact_image_id "$ROLLBACK_IMAGE_ID"
|
|
2154
|
+
ROLLBACK_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$ROLLBACK_IMAGE_ID")
|
|
2155
|
+
test "$ROLLBACK_IMAGE_ID" = sha256:589b7cf8f96d139ee9fd86204a183126aadcbd3fbfb406063709be303397b1b2
|
|
2156
|
+
test "$ROLLBACK_MOUNT_CONTRACT" = canonical-pre-gateway
|
|
2230
2157
|
if assert_update_topology "$ROLLBACK_IMAGE_ID"; then
|
|
2231
2158
|
:
|
|
2232
2159
|
else
|
|
@@ -2234,17 +2161,20 @@ Update:
|
|
|
2234
2161
|
(exit "$UPDATE_PREFLIGHT_STATUS")
|
|
2235
2162
|
fi
|
|
2236
2163
|
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
2237
|
-
/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
|
|
2238
2164
|
if provision_sanctuary_sab_credential "$IMAGE_ID" \
|
|
2239
2165
|
&& verify_sanctuary_sab_readiness "$IMAGE_ID" \
|
|
2240
|
-
&& verify_sanctuary_telegram_readiness "$
|
|
2166
|
+
&& verify_sanctuary_telegram_readiness "$ROLLBACK_IMAGE_ID"; then
|
|
2241
2167
|
:
|
|
2242
2168
|
else
|
|
2243
2169
|
PRECUTOVER_READINESS_STATUS=$?
|
|
2244
|
-
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
2245
2170
|
(exit "$PRECUTOVER_READINESS_STATUS")
|
|
2246
2171
|
fi
|
|
2247
|
-
|
|
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:
|
|
2248
2178
|
if disable_butler_autostart; then
|
|
2249
2179
|
:
|
|
2250
2180
|
else
|
|
@@ -2252,6 +2182,23 @@ Update:
|
|
|
2252
2182
|
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
2253
2183
|
(exit "$AUTOSTART_DISABLE_STATUS")
|
|
2254
2184
|
fi
|
|
2185
|
+
Only after pinned old-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:
|
|
2186
|
+
receive_sanctuary_authority_token
|
|
2187
|
+
test "$(stat -c '%u:%g:%a' /mnt/user/appdata/ouro-authority/incoming-token)" = 0:0:600
|
|
2188
|
+
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:
|
|
2189
|
+
if /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-install >/dev/null \
|
|
2190
|
+
&& verify_sanctuary_telegram_readiness "$IMAGE_ID"; then
|
|
2191
|
+
:
|
|
2192
|
+
else
|
|
2193
|
+
AUTHORITY_INSTALL_STATUS=$?
|
|
2194
|
+
retire_sanctuary_authority_if_pending
|
|
2195
|
+
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
2196
|
+
start_only_butler_for_recovery
|
|
2197
|
+
wait_butler_ready ouro-butler
|
|
2198
|
+
enable_butler_autostart
|
|
2199
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
2200
|
+
(exit "$AUTHORITY_INSTALL_STATUS")
|
|
2201
|
+
fi
|
|
2255
2202
|
Define the stale-rollback cleanup used by the preparation guard:
|
|
2256
2203
|
remove_stopped_rollback_if_present() {
|
|
2257
2204
|
EXPECTED_STALE_ROLLBACK_IMAGE_ID=$1
|
|
@@ -2288,6 +2235,7 @@ ouro-butler-rollback
|
|
|
2288
2235
|
PRODUCTION_PREPARATION_CLEANUP_STATUS=0
|
|
2289
2236
|
PRODUCTION_PREPARATION_RECOVERY_SAFE=true
|
|
2290
2237
|
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null
|
|
2238
|
+
retire_sanctuary_authority_if_pending
|
|
2291
2239
|
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
2292
2240
|
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
2293
2241
|
docker stop ouro-butler-rollback >/dev/null 2>&1 || true
|
|
@@ -2328,22 +2276,16 @@ ouro-butler-rollback
|
|
|
2328
2276
|
fi
|
|
2329
2277
|
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.
|
|
2330
2278
|
If neither exact container can be found, the failure returns with Butler autostart disabled.
|
|
2331
|
-
Do not start a target-image daemon between the production rename and final production activation. The exact-image static audit
|
|
2279
|
+
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.
|
|
2332
2280
|
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.
|
|
2333
2281
|
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.
|
|
2334
2282
|
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:
|
|
2335
2283
|
if test "$(inspect_registry_manifest_digest "$VERSION_IMAGE")" = "$MANIFEST_DIGEST" \
|
|
2336
2284
|
&& test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
|
|
2337
|
-
&&
|
|
2338
|
-
|
|
2339
|
-
--label "net.unraid.docker.icon=$TEMPLATE_ICON" \
|
|
2340
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
2341
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
2342
|
-
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
2343
|
-
"$VERSION_IMAGE" \
|
|
2344
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" \
|
|
2285
|
+
&& create_sanctuary_container "$IMAGE_ID" "$VERSION_IMAGE" "$TARGET_MOUNT_CONTRACT" \
|
|
2286
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" \
|
|
2345
2287
|
&& assert_only_running_butler - \
|
|
2346
|
-
&&
|
|
2288
|
+
&& /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-activate >/dev/null \
|
|
2347
2289
|
&& assert_only_running_butler ouro-butler \
|
|
2348
2290
|
&& test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" \
|
|
2349
2291
|
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false \
|
|
@@ -2355,6 +2297,7 @@ ouro-butler-rollback
|
|
|
2355
2297
|
PRODUCTION_ACTIVATION_STATUS=$?
|
|
2356
2298
|
PRODUCTION_ACTIVATION_CLEANUP_STATUS=0
|
|
2357
2299
|
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null
|
|
2300
|
+
retire_sanctuary_authority_if_pending
|
|
2358
2301
|
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
2359
2302
|
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
2360
2303
|
PARTIAL_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
@@ -2388,10 +2331,21 @@ ouro-butler-rollback
|
|
|
2388
2331
|
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" commit --proof "$FINAL_PROOF_PATH" >/dev/null
|
|
2389
2332
|
cleanup_event_asset_stage
|
|
2390
2333
|
trap - EXIT
|
|
2391
|
-
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.
|
|
2334
|
+
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.
|
|
2392
2335
|
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.
|
|
2393
2336
|
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.
|
|
2394
2337
|
|
|
2338
|
+
Execution-only primitive refresh:
|
|
2339
|
+
|
|
2340
|
+
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.
|
|
2341
|
+
|
|
2342
|
+
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.
|
|
2343
|
+
|
|
2344
|
+
/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
|
|
2345
|
+
/usr/local/bin/node /mnt/user/appdata/ouro-authority/package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js boot
|
|
2346
|
+
|
|
2347
|
+
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.
|
|
2348
|
+
|
|
2395
2349
|
Backup:
|
|
2396
2350
|
Set BACKUP_ROOT to a new absolute snapshot path on the destination filesystem.
|
|
2397
2351
|
Set AUDIT_RUNNER_IMAGE_TAG to the reviewed new image containing the
|
|
@@ -2611,7 +2565,12 @@ Restore:
|
|
|
2611
2565
|
process.stdout.write(expected);
|
|
2612
2566
|
' "$BACKUP_ROOT/provenance/container-inspect.json" "$BACKUP_ROOT/provenance/package-version")
|
|
2613
2567
|
test "$(docker image inspect --format '{{.Id}}' "$RESTORE_VERSION_IMAGE")" = "$IMAGE_ID"
|
|
2614
|
-
|
|
2568
|
+
RESTORE_MOUNT_CONTRACT=$(sanctuary_image_mount_contract "$IMAGE_ID")
|
|
2569
|
+
audit_registered_dockerman_template "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_VERSION_IMAGE" "$RESTORE_MOUNT_CONTRACT"
|
|
2570
|
+
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:
|
|
2571
|
+
test ! -e /mnt/user/appdata/ouro-authority/active.json && test ! -L /mnt/user/appdata/ouro-authority/active.json
|
|
2572
|
+
test ! -e /boot/config/custom/ouro-butler/docker-man-template-transaction.json && test ! -L /boot/config/custom/ouro-butler/docker-man-template-transaction.json
|
|
2573
|
+
test "$RESTORE_MOUNT_CONTRACT" = canonical-pre-gateway
|
|
2615
2574
|
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.
|
|
2616
2575
|
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.
|
|
2617
2576
|
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:
|
|
@@ -2668,14 +2627,8 @@ Restore:
|
|
|
2668
2627
|
&& /bin/bash "$HOST_RESTORE_INSTALLER" --restore-root "$BACKUP_ROOT/host" \
|
|
2669
2628
|
&& rm -f "$HOST_RESTORE_INSTALLER" \
|
|
2670
2629
|
&& docker image inspect "$IMAGE_ID" >/dev/null \
|
|
2671
|
-
&&
|
|
2672
|
-
|
|
2673
|
-
--label "net.unraid.docker.icon=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png" \
|
|
2674
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
2675
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
2676
|
-
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
2677
|
-
"$RESTORE_VERSION_IMAGE" \
|
|
2678
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png \
|
|
2630
|
+
&& create_sanctuary_container "$IMAGE_ID" "$RESTORE_VERSION_IMAGE" "$RESTORE_MOUNT_CONTRACT" \
|
|
2631
|
+
&& 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 \
|
|
2679
2632
|
&& assert_only_running_butler - \
|
|
2680
2633
|
&& docker start ouro-butler \
|
|
2681
2634
|
&& assert_only_running_butler ouro-butler \
|
|
@@ -2826,13 +2779,8 @@ Packaged Unit 16 acceptance execution:
|
|
|
2826
2779
|
execute it in the order below. Before every execution the launcher regenerates
|
|
2827
2780
|
the config from the packaged fixed contract and requires byte-for-byte equality.
|
|
2828
2781
|
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.
|
|
2829
|
-
The cursor snapshot is deliberately materialized and executed twice around the
|
|
2830
|
-
|
|
2831
|
-
`runtime/config` and keeps the bot token inside the consuming harness process.
|
|
2832
|
-
It never reads the retired container credential file or carries the token in a
|
|
2833
|
-
descriptor, argument, environment variable, shell variable, config, evidence,
|
|
2834
|
-
or output. Callback injection alone maps its reviewed saved callback-update
|
|
2835
|
-
JSON from host fd 3 through Docker stdin to in-container fd 3.
|
|
2782
|
+
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.
|
|
2783
|
+
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.
|
|
2836
2784
|
Stage the reviewed callback JSON at the fixed path below in the root-owned
|
|
2837
2785
|
tmpfs inbox, then use this single fail-closed helper. It opens the input once,
|
|
2838
2786
|
validates the opened descriptor and its original path refer to the same
|
|
@@ -2954,15 +2902,7 @@ Packaged Unit 16 acceptance execution:
|
|
|
2954
2902
|
Telegram runtime fields and both configured provider records were freshly read.
|
|
2955
2903
|
Scenario handles remain private to the scenario adapter. The main one-shot
|
|
2956
2904
|
never receives the Docker socket, Unraid key directory, or a host-root mount.
|
|
2957
|
-
Telegram bootstrap
|
|
2958
|
-
poller quiescence guard: it verifies the exact healthy staging container,
|
|
2959
|
-
assumes recovery responsibility before attempting the stop, stops it with a
|
|
2960
|
-
30-second grace bound, proves it is stopped, and mounts a
|
|
2961
|
-
root-owned typed zero-poller fact. Its exit/signal trap restarts that same exact
|
|
2962
|
-
container and waits up to 120 seconds for healthy recovery on both success and
|
|
2963
|
-
failure. It never reads or changes Unraid autostart configuration. Every command
|
|
2964
|
-
also receives a freshly generated, redacted typed container-inspect snapshot;
|
|
2965
|
-
raw container environment or credential values are never captured.
|
|
2905
|
+
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.
|
|
2966
2906
|
Callback injection requires two stable zero observations from a durable callback playback journal under
|
|
2967
2907
|
`state/approvals`; the journal is keyed only by the full callback-coordinate
|
|
2968
2908
|
digest and stores no raw update, callback data, user, chat, message, or query ID.
|