@ouro.bot/cli 0.1.0-alpha.814 → 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 CHANGED
@@ -1,6 +1,21 @@
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
+ },
12
+ {
13
+ "version": "0.1.0-alpha.815",
14
+ "changes": [
15
+ "Protect Sanctuary's resident control socket from transient Telegram app disposal, and safely retry interrupted startup and cleanup without stealing another listener.",
16
+ "Preserve the order of overlapping startup and shutdown requests, and check socket modification time without mistaking permission changes for replacement."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.814",
6
21
  "changes": [
@@ -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|prepackage-alpha797) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
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"|"ouro-butler sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d"|"ouro-butler-staging sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d") return 0 ;;
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
- if docker container inspect ouro-butler-staging >/dev/null 2>&1; then
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
- validate_sanctuary_legacy_staging || return $?
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. Initial alpha.797 adoption must prove the exact staging name and image pin; normal updates must prove the existing production topology. A recovery journal admits only image acquisition and private staging until the staged transaction implementation validates the full journal and restores a classifiable source:
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
- Initial install/adoption is a separate terminal path for the verified older layout: no production or rollback, exactly one running (possibly unhealthy) ouro-butler-staging, and no legacy-evidence container. After the helpers above are loaded and IMAGE_ID is resolved, run this exact sequence.
2213
- MiniMax credentials come from the byte-verified legacy bootstrap envelope. Never place credentials in arguments, shell variables, or history.
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,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.814",
2
+ "runtimeVersion": "0.1.0-alpha.816",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -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.814</Repository>
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" || sourceContract === "prepackage-alpha797" ? sourceCandidate : null;
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 <legacy-alpha742|prepackage-alpha797>" }) + "\n");
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" || sourceContract === "prepackage-alpha797" ? sourceContract : "canonical",
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 === "prepackage-alpha797" && root.Name !== `/${EXPECTED_NAME}` && root.Name !== "/ouro-butler-staging")
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
- try {
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 renewExternalEventClaim(recordPath, input) {
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
- const record = readExternalEventRecord(recordPath);
1180
- if (record.executionState !== "running" || record.claimOwner !== input.owner)
1181
- throw new Error("External event claim owner mismatch");
1182
- if (record.generation !== input.expectedGeneration)
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) {