@ouro.bot/cli 0.1.0-alpha.815 → 0.1.0-alpha.816
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +8 -0
- package/deploy/unraid/README.txt +25 -305
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/approval-store.js +11 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +3 -3
- package/dist/heart/daemon/container-spec-auditor.js +3 -17
- 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-steward-policy.js +34 -10
- package/dist/repertoire/tools-unraid.js +79 -32
- package/dist/repertoire/tools.js +22 -19
- package/dist/repertoire/unraid-restart.js +179 -52
- package/dist/senses/private-runtime.js +27 -12
- 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-approval-runtime.js +130 -12
- package/dist/senses/telegram.js +6 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.816",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Support bounded Sanctuary service restarts only when a fresh request or health event intersects with explicit owner-stated desired state and restart grants.",
|
|
8
|
+
"Record applied policy changes, recheck authority and exact service identity before each restart, enforce durable restart limits, and preserve honest action and verification outcomes through approvals and event recovery.",
|
|
9
|
+
"Document fresh post-install owner provisioning and retire the fulfilled alpha.797 source-adoption path while preserving current package-managed update, recovery and empty packaged authority."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
4
12
|
{
|
|
5
13
|
"version": "0.1.0-alpha.815",
|
|
6
14
|
"changes": [
|
package/deploy/unraid/README.txt
CHANGED
|
@@ -2,6 +2,24 @@ Mendelow Cloud Butler operator runbook
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
+
Bounded Jellyfin stewardship
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
5
23
|
Start/stop:
|
|
6
24
|
docker start ouro-butler
|
|
7
25
|
docker stop ouro-butler
|
|
@@ -33,7 +51,7 @@ Effective-spec audit helper:
|
|
|
33
51
|
test "$AUDIT_EXPECTED_ICON" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png || return 1
|
|
34
52
|
set -- --expected-image-reference "$AUDIT_EXPECTED_IMAGE_REFERENCE" --expected-icon "$AUDIT_EXPECTED_ICON"
|
|
35
53
|
;;
|
|
36
|
-
legacy-alpha742
|
|
54
|
+
legacy-alpha742) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
|
|
37
55
|
*) return 1 ;;
|
|
38
56
|
esac
|
|
39
57
|
INSPECT_DIR=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/inspect.XXXXXX) || return $?
|
|
@@ -362,7 +380,7 @@ Effective-spec audit helper:
|
|
|
362
380
|
validate_exact_image_id "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
363
381
|
test "$(docker inspect --format '{{.Image}}' "$PIN_SOURCE_CONTAINER")" = "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
364
382
|
case "$PIN_SOURCE_CONTAINER $PIN_SOURCE_IMAGE_ID" in
|
|
365
|
-
"ouro-butler sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d"
|
|
383
|
+
"ouro-butler sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d") return 0 ;;
|
|
366
384
|
"ouro-butler "*) ;;
|
|
367
385
|
*) return 1 ;;
|
|
368
386
|
esac
|
|
@@ -381,20 +399,11 @@ Effective-spec audit helper:
|
|
|
381
399
|
assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
382
400
|
audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" legacy-alpha742
|
|
383
401
|
}
|
|
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
402
|
assert_update_source() {
|
|
392
403
|
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
393
404
|
AUDIT_RUNNER_IMAGE_ID=$2
|
|
394
405
|
if test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d; then
|
|
395
406
|
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
407
|
else
|
|
399
408
|
assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
400
409
|
EXPECTED_SOURCE_IMAGE_REFERENCE=$(docker inspect --format '{{.Config.Image}}' ouro-butler) || return $?
|
|
@@ -1145,30 +1154,7 @@ Effective-spec audit helper:
|
|
|
1145
1154
|
verify_butler_autostart "1 0 0 0" || return $?
|
|
1146
1155
|
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_PRODUCTION_PRESENT" = false; then
|
|
1147
1156
|
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
|
|
1157
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
1172
1158
|
assert_only_running_butler - || return $?
|
|
1173
1159
|
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1174
1160
|
TEMPLATE_RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
@@ -1278,34 +1264,10 @@ Effective-spec audit helper:
|
|
|
1278
1264
|
PREPARE_WRONG_FILE_MODE=$(find "$PREPARE_RUNTIME_ROOT" "$PREPARE_AGENT_ROOT" -type f ! -perm 0600 -print -quit) || return $?
|
|
1279
1265
|
test -z "$PREPARE_WRONG_FILE_MODE" || return $?
|
|
1280
1266
|
}
|
|
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
1267
|
classify_sanctuary_update_source() {
|
|
1305
1268
|
UPDATE_ENTRY_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1306
1269
|
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"
|
|
1270
|
+
return 1
|
|
1309
1271
|
else
|
|
1310
1272
|
UPDATE_ENTRY_SOURCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
1311
1273
|
assert_update_topology "$UPDATE_ENTRY_SOURCE_IMAGE_ID" || return $?
|
|
@@ -1321,238 +1283,6 @@ Effective-spec audit helper:
|
|
|
1321
1283
|
classify_sanctuary_update_source
|
|
1322
1284
|
fi
|
|
1323
1285
|
}
|
|
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
1286
|
validate_sanctuary_legacy_import_marker() {
|
|
1557
1287
|
IMPORT_MARKER=$1
|
|
1558
1288
|
IMPORT_SOURCE=$2
|
|
@@ -2085,7 +1815,7 @@ NODE
|
|
|
2085
1815
|
return 1
|
|
2086
1816
|
}
|
|
2087
1817
|
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.
|
|
1818
|
+
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
1819
|
admit_sanctuary_update_entry
|
|
2090
1820
|
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
1821
|
PACKAGE_VERSION=<released-version>
|
|
@@ -2209,18 +1939,8 @@ Update:
|
|
|
2209
1939
|
verify_installed_usenet_guard
|
|
2210
1940
|
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
1941
|
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
|
-
These provider-readiness commands are adoption-only; do not use them as a normal-update precheck against an active canonical production daemon.
|
|
2215
|
-
Sanctuary legacy adoption commands:
|
|
2216
|
-
prepare_sanctuary_legacy_adoption "$IMAGE_ID"
|
|
2217
|
-
verify_sanctuary_provider_readiness "$IMAGE_ID"
|
|
2218
|
-
install_from_legacy_staging
|
|
2219
|
-
These commands are one terminal path; after install succeeds, stop and do not continue into the normal update below. The final install is noninteractive: it reruns resumable preparation plus fresh readiness, but never authentication.
|
|
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.
|
|
1942
|
+
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
|
+
The DockerMan transaction binds Jellyfin's container ID, image ID, state, and restart count. Update, rollback and recovery must preserve that checkpoint.
|
|
2224
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:
|
|
2225
1945
|
/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
|
|
2226
1946
|
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.816</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -126,9 +126,14 @@ function canonicalApprovalArguments(value) {
|
|
|
126
126
|
}
|
|
127
127
|
function hasExactKeys(value) {
|
|
128
128
|
const actual = Object.keys(value).sort();
|
|
129
|
-
const expected = [...RECORD_KEYS].sort();
|
|
129
|
+
const expected = [...RECORD_KEYS, ...("ownerBinding" in value ? ["ownerBinding"] : [])].sort();
|
|
130
130
|
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
131
131
|
}
|
|
132
|
+
function validOwnerBinding(value) {
|
|
133
|
+
return isObject(value) && Object.keys(value).length === 5
|
|
134
|
+
&& ["friendId", "requestId", "sessionEventId", "sessionKey"].every((key) => isNonEmpty(value[key]) && value[key] === value[key].trim())
|
|
135
|
+
&& typeof value.profileVersion === "number" && Number.isSafeInteger(value.profileVersion) && value.profileVersion > 0;
|
|
136
|
+
}
|
|
132
137
|
function validStateShape(record) {
|
|
133
138
|
const hasOwner = isNonEmpty(record.ownerId);
|
|
134
139
|
const hasAttempt = isTimestamp(record.attemptedAt);
|
|
@@ -196,6 +201,8 @@ function validStateShape(record) {
|
|
|
196
201
|
function parseApprovalRecord(value) {
|
|
197
202
|
if (!isObject(value) || !hasExactKeys(value))
|
|
198
203
|
fail("corrupt_record");
|
|
204
|
+
if ("ownerBinding" in value && !validOwnerBinding(value.ownerBinding))
|
|
205
|
+
fail("corrupt_record");
|
|
199
206
|
const record = value;
|
|
200
207
|
if (!UUID.test(record.approvalId))
|
|
201
208
|
fail("corrupt_record");
|
|
@@ -241,6 +248,8 @@ function assertHash(value, code) {
|
|
|
241
248
|
fail(code);
|
|
242
249
|
}
|
|
243
250
|
function assertPrepareInput(input) {
|
|
251
|
+
if (input.ownerBinding !== undefined && !validOwnerBinding(input.ownerBinding))
|
|
252
|
+
fail("invalid_proposal");
|
|
244
253
|
for (const value of [input.toolCallId, input.toolName, input.policyId, input.sessionKey, input.sessionPath,
|
|
245
254
|
input.requesterId, input.transport, input.transportUserId, input.transportChatId]) {
|
|
246
255
|
if (!isNonEmpty(value))
|
|
@@ -615,6 +624,7 @@ function openApprovalStore(options) {
|
|
|
615
624
|
result: null,
|
|
616
625
|
reason: null,
|
|
617
626
|
frozenAssistantMessage: structuredClone(input.frozenAssistantMessage),
|
|
627
|
+
...(input.ownerBinding ? { ownerBinding: structuredClone(input.ownerBinding) } : {}),
|
|
618
628
|
};
|
|
619
629
|
if (input.scenarioHandleDigest !== undefined)
|
|
620
630
|
assertHash(input.scenarioHandleDigest, "invalid_proposal");
|
|
@@ -76,10 +76,10 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
76
76
|
const effective = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image", "--expected-image-reference", "--expected-icon"]);
|
|
77
77
|
const sourceCandidate = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image", "--mount-contract"]);
|
|
78
78
|
const sourceContract = sourceCandidate?.["--mount-contract"];
|
|
79
|
-
const sourceEffective = sourceContract === "legacy-alpha742"
|
|
79
|
+
const sourceEffective = sourceContract === "legacy-alpha742" ? sourceCandidate : null;
|
|
80
80
|
const selectedEffective = effective ?? sourceEffective;
|
|
81
81
|
if (!staged && !persistent && !selectedEffective) {
|
|
82
|
-
write(JSON.stringify({ ok: false, error: "usage: staged --template <path> --runtime-policy <path> --expected-image <id>; persistent --persistent-template <path> --runtime-policy <path> --expected-image-reference <tag>; effective --inspect <path> --image-inspect <path> --expected-image <id> --expected-image-reference <tag> --expected-icon <url>; source compatibility effective --inspect <path> --image-inspect <path> --expected-image <id> --mount-contract
|
|
82
|
+
write(JSON.stringify({ ok: false, error: "usage: staged --template <path> --runtime-policy <path> --expected-image <id>; persistent --persistent-template <path> --runtime-policy <path> --expected-image-reference <tag>; effective --inspect <path> --image-inspect <path> --expected-image <id> --expected-image-reference <tag> --expected-icon <url>; source compatibility effective --inspect <path> --image-inspect <path> --expected-image <id> --mount-contract legacy-alpha742" }) + "\n");
|
|
83
83
|
(0, runtime_1.emitNervesEvent)({
|
|
84
84
|
level: "error",
|
|
85
85
|
component: "daemon",
|
|
@@ -129,7 +129,7 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
129
129
|
expectedEnvironment,
|
|
130
130
|
expectedImageReference: selectedEffective["--expected-image-reference"],
|
|
131
131
|
expectedIcon: selectedEffective["--expected-icon"],
|
|
132
|
-
mountContract: sourceContract === "legacy-alpha742"
|
|
132
|
+
mountContract: sourceContract === "legacy-alpha742" ? sourceContract : "canonical",
|
|
133
133
|
});
|
|
134
134
|
if (imageInspect.Id !== selectedEffective["--expected-image"]) {
|
|
135
135
|
result.ok = false;
|
|
@@ -16,7 +16,6 @@ const EXPECTED_MOUNTS = [
|
|
|
16
16
|
["/boot/config/custom/ouro-events/spool", "/run/ouro-events", false, "rprivate"],
|
|
17
17
|
];
|
|
18
18
|
const LEGACY_ALPHA742_IMAGE = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
|
|
19
|
-
const PREPACKAGE_ALPHA797_IMAGE = "sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d";
|
|
20
19
|
const LEGACY_ALPHA742_MOUNTS = EXPECTED_MOUNTS.slice(0, 2);
|
|
21
20
|
const EXPECTED_EXTRA_PARAMS = "--restart=unless-stopped --user=10001:10001";
|
|
22
21
|
const EXPECTED_NAME = "ouro-butler";
|
|
@@ -53,10 +52,10 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
53
52
|
if (!EXACT_IMAGE.test(options.expectedImage))
|
|
54
53
|
violations.push("expected image must be an exact local Docker image ID");
|
|
55
54
|
const mountContract = options.mountContract ?? "canonical";
|
|
55
|
+
if (mountContract !== "canonical" && mountContract !== "legacy-alpha742")
|
|
56
|
+
violations.push("unsupported mount contract");
|
|
56
57
|
if (mountContract === "legacy-alpha742" && options.expectedImage !== LEGACY_ALPHA742_IMAGE)
|
|
57
58
|
violations.push("legacy mount exception requires the pinned alpha.742 image ID");
|
|
58
|
-
if (mountContract === "prepackage-alpha797" && options.expectedImage !== PREPACKAGE_ALPHA797_IMAGE)
|
|
59
|
-
violations.push("pre-package-managed source exception requires the pinned alpha.797 image ID");
|
|
60
59
|
const expectedMounts = mountContract === "legacy-alpha742" ? LEGACY_ALPHA742_MOUNTS : EXPECTED_MOUNTS;
|
|
61
60
|
const expectedArgs = mountContract === "canonical" ? PACKAGE_DAEMON_ARGS : LEGACY_DAEMON_ARGS;
|
|
62
61
|
const expectedEntrypoint = mountContract === "canonical" ? PACKAGE_ENTRYPOINT : LEGACY_ENTRYPOINT;
|
|
@@ -106,20 +105,7 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
106
105
|
violations.push("effective network ports must be empty");
|
|
107
106
|
if (mountContract === "canonical" && root.Name !== `/${EXPECTED_NAME}`)
|
|
108
107
|
violations.push("container name must be /ouro-butler");
|
|
109
|
-
if (mountContract === "
|
|
110
|
-
violations.push("pre-package-managed source name must be /ouro-butler or /ouro-butler-staging");
|
|
111
|
-
if (mountContract === "prepackage-alpha797") {
|
|
112
|
-
if (config.Image !== PREPACKAGE_ALPHA797_IMAGE)
|
|
113
|
-
violations.push("pre-package-managed source configured image must equal the pinned alpha.797 image ID");
|
|
114
|
-
const labels = record(config.Labels);
|
|
115
|
-
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.managed"))
|
|
116
|
-
violations.push("pre-package-managed source must not carry a DockerMan managed label");
|
|
117
|
-
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.icon"))
|
|
118
|
-
violations.push("pre-package-managed source must not carry a DockerMan icon label");
|
|
119
|
-
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.webui"))
|
|
120
|
-
violations.push("pre-package-managed source must not carry a DockerMan WebUI label");
|
|
121
|
-
}
|
|
122
|
-
else if (mountContract === "canonical") {
|
|
108
|
+
if (mountContract === "canonical") {
|
|
123
109
|
if (!options.expectedImageReference || !VERSION_REFERENCE.test(options.expectedImageReference))
|
|
124
110
|
violations.push("expected image reference must be the canonical package-version tag");
|
|
125
111
|
if (config.Image !== options.expectedImageReference)
|
|
@@ -290,10 +290,7 @@ function withRecordLock(recordPath, operation) {
|
|
|
290
290
|
}
|
|
291
291
|
};
|
|
292
292
|
acquire();
|
|
293
|
-
|
|
294
|
-
return operation();
|
|
295
|
-
}
|
|
296
|
-
finally {
|
|
293
|
+
const release = () => {
|
|
297
294
|
try {
|
|
298
295
|
/* v8 ignore else -- concurrency fence: a replaced owner must not release its successor's lock @preserve */
|
|
299
296
|
if (fs.readFileSync(ownerPath, "utf8") === owner) {
|
|
@@ -304,6 +301,17 @@ function withRecordLock(recordPath, operation) {
|
|
|
304
301
|
catch {
|
|
305
302
|
// A stale owner must never remove a successor's lock.
|
|
306
303
|
}
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
const result = operation();
|
|
307
|
+
if (result instanceof Promise)
|
|
308
|
+
return result.finally(release);
|
|
309
|
+
release();
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
release();
|
|
314
|
+
throw error;
|
|
307
315
|
}
|
|
308
316
|
}
|
|
309
317
|
function isRecord(value) {
|
|
@@ -1174,18 +1182,26 @@ function claimExternalEvent(recordPath, input) {
|
|
|
1174
1182
|
}, now);
|
|
1175
1183
|
});
|
|
1176
1184
|
}
|
|
1177
|
-
function
|
|
1185
|
+
function renewClaimLocked(recordPath, input, requireCurrent) {
|
|
1186
|
+
const record = readExternalEventRecord(recordPath);
|
|
1187
|
+
if (record.executionState !== "running" || record.claimOwner !== input.owner)
|
|
1188
|
+
throw new Error("External event claim owner mismatch");
|
|
1189
|
+
if (record.generation !== input.expectedGeneration)
|
|
1190
|
+
throw new Error("External event generation mismatch");
|
|
1191
|
+
const leaseMs = input.leaseMs ?? 30_000;
|
|
1192
|
+
if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0)
|
|
1193
|
+
throw new Error("External event claim renewal is invalid");
|
|
1194
|
+
const now = input.now?.() ?? new Date().toISOString();
|
|
1195
|
+
if (requireCurrent && !(Date.parse(record.claimExpiresAt ?? "") > Date.parse(now)))
|
|
1196
|
+
throw new Error("External event claim expired");
|
|
1197
|
+
return commitMutation(recordPath, { ...record, claimExpiresAt: new Date(Date.parse(now) + leaseMs).toISOString() }, now);
|
|
1198
|
+
}
|
|
1199
|
+
function renewExternalEventClaim(recordPath, input, operation) {
|
|
1178
1200
|
return withRecordLock(recordPath, () => {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
throw new Error("External event generation mismatch");
|
|
1184
|
-
const leaseMs = input.leaseMs ?? 30_000;
|
|
1185
|
-
if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0)
|
|
1186
|
-
throw new Error("External event claim renewal is invalid");
|
|
1187
|
-
const now = input.now?.() ?? new Date().toISOString();
|
|
1188
|
-
return commitMutation(recordPath, { ...record, claimExpiresAt: new Date(Date.parse(now) + leaseMs).toISOString() }, now);
|
|
1201
|
+
if (!operation)
|
|
1202
|
+
return renewClaimLocked(recordPath, input, false);
|
|
1203
|
+
const signal = AbortSignal.timeout(25_000);
|
|
1204
|
+
return operation(renewClaimLocked(recordPath, input, true), signal);
|
|
1189
1205
|
});
|
|
1190
1206
|
}
|
|
1191
1207
|
function assertClaim(record, input) {
|