@ouro.bot/cli 0.1.0-alpha.839 → 0.1.0-alpha.840
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 +7 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +10 -3
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +41 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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.840",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Sanctuary upgrade rollback robustness, from the 837 rehearsal on the real host: gateway readiness gets a 15-minute budget (verifying every pinned package file with a cold Unraid page cache takes minutes, and a 120-second budget failed a real rollback after it had restored every record), and a rollback marks its journal so an upgrade can never resume forward from a half-rolled-back journal; only upgrade-rollback continues it.",
|
|
8
|
+
"Sanctuary: the cgroup keep child is used only while the installed package understands it. An older gateway (before 837) refuses an unknown child cgroup, so a rollback to it now removes the keep child and holds Unraid's reaper first, and the host keeper does the same. Found when a rollback from 837 to 830 left the 830 gateway exiting on the keep child; the Butler was recovered by hand in about 17 minutes."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
{
|
|
5
12
|
"version": "0.1.0-alpha.839",
|
|
6
13
|
"changes": [
|
|
@@ -446,17 +446,23 @@ R=/mnt/user/appdata/ouro-authority
|
|
|
446
446
|
GW=$R/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js
|
|
447
447
|
CFG=$R/active.json
|
|
448
448
|
LOG=/var/log/ouro-gateway.log
|
|
449
|
+
# Only an authority from 0.1.0-alpha.837 on understands the keep child; an older gateway
|
|
450
|
+
# refuses to start beside an unknown cgroup (a rollback to 830 hit exactly that). So the
|
|
451
|
+
# keep child exists only while the installed package supports it; otherwise it is removed
|
|
452
|
+
# and the reaper stays paused, the pre-837 behaviour.
|
|
453
|
+
keep_supported() { grep -q ouro-keep "$R/package/dist/heart/daemon/sanctuary-authority-installation.js" 2>/dev/null; }
|
|
449
454
|
reaper_policy() {
|
|
450
455
|
P=$(cat /run/cgroup2-unraid.pid 2>/dev/null) || return 0
|
|
451
456
|
[ -n "$P" ] || return 0
|
|
452
|
-
if [ -d "$CG/ouro-keep" ]; then kill -CONT "$P" 2>/dev/null; return 0; fi
|
|
457
|
+
if keep_supported && [ -d "$CG/ouro-keep" ]; then kill -CONT "$P" 2>/dev/null; return 0; fi
|
|
453
458
|
S=$(ps -o stat= -p "$P" 2>/dev/null | tr -d ' ')
|
|
454
459
|
case "$S" in T*) : ;; *) kill -STOP "$P" 2>/dev/null ;; esac
|
|
455
460
|
}
|
|
456
461
|
ensure_cg() {
|
|
457
462
|
[ -d "$CG" ] || { mkdir -m 700 "$CG" 2>/dev/null; chown 0:0 "$CG" 2>/dev/null
|
|
458
463
|
for c in cpu memory pids; do echo "+$c" > "$CG/cgroup.subtree_control" 2>/dev/null; done; }
|
|
459
|
-
[ -d "$CG/ouro-keep" ] || mkdir -m 700 "$CG/ouro-keep" 2>/dev/null
|
|
464
|
+
if keep_supported; then [ -d "$CG/ouro-keep" ] || mkdir -m 700 "$CG/ouro-keep" 2>/dev/null
|
|
465
|
+
else [ -d "$CG/ouro-keep" ] && rmdir "$CG/ouro-keep" 2>/dev/null; fi
|
|
460
466
|
}
|
|
461
467
|
ensure_sock() {
|
|
462
468
|
# Docker autostarts the resident before us and auto-creates its missing bind
|
|
@@ -480,10 +486,11 @@ start_gw() {
|
|
|
480
486
|
}
|
|
481
487
|
# An in-place upgrade pauses us with a flag; a stale flag (> 30 min) is ignored.
|
|
482
488
|
maintenance() { [ -n "$(find /run/ouro-authority-maintenance -mmin -30 2>/dev/null)" ]; }
|
|
483
|
-
|
|
489
|
+
reaper_policy; ensure_cg; ensure_sock
|
|
484
490
|
last_res=0
|
|
485
491
|
while true; do
|
|
486
492
|
if maintenance; then sleep 15; continue; fi
|
|
493
|
+
reaper_policy
|
|
487
494
|
ensure_cg
|
|
488
495
|
reaper_policy
|
|
489
496
|
ensure_sock
|
|
@@ -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.840</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -59,6 +59,10 @@ const CGROUP = "/sys/fs/cgroup/ouro-authority";
|
|
|
59
59
|
const BOOT = "/boot/config/custom/ouro-authority/start.sh";
|
|
60
60
|
const BOOT_LINE = `/bin/sh ${BOOT} --boot & # ouro-authority`;
|
|
61
61
|
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
62
|
+
// The gateway verifies every pinned package file before it reports ready. With a cold
|
|
63
|
+
// page cache on Unraid's array that takes minutes, so readiness gets a generous budget
|
|
64
|
+
// (a 120 s budget failed a real rollback on 2026-09-24).
|
|
65
|
+
const GATEWAY_READY_TIMEOUT_MS = 900_000;
|
|
62
66
|
const TEMPLATE_JOURNAL = "/boot/config/custom/ouro-butler/docker-man-template-transaction.json";
|
|
63
67
|
// In-place upgrade of an installed authority: same epoch (token, issuer, cursor,
|
|
64
68
|
// gateway state), new reviewed package and resident image. Every root record it
|
|
@@ -298,6 +302,15 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
298
302
|
* made (still empty) cgroup before the child exists, so retry that race a few times. */
|
|
299
303
|
#cgroup() {
|
|
300
304
|
const keep = this.#p(`${CGROUP}/${sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP}`);
|
|
305
|
+
if (!this.#keepSupported()) {
|
|
306
|
+
// A package from before the keep child refuses an unknown child cgroup and relies on
|
|
307
|
+
// the host keeper holding Unraid's reaper paused (a rollback to 830 hit exactly that).
|
|
308
|
+
this.#freezeUnraidReaper();
|
|
309
|
+
if (fs.existsSync(keep))
|
|
310
|
+
fs.rmdirSync(keep);
|
|
311
|
+
this.#directory(CGROUP);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
301
314
|
for (let attempt = 1; !fs.existsSync(keep); attempt += 1) {
|
|
302
315
|
try {
|
|
303
316
|
this.#directory(CGROUP);
|
|
@@ -310,6 +323,23 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
310
323
|
}
|
|
311
324
|
this.#directory(CGROUP);
|
|
312
325
|
}
|
|
326
|
+
/** Whether the installed package's gateway understands the keep child (0.1.0-alpha.837+). */
|
|
327
|
+
#keepSupported() {
|
|
328
|
+
const installation = this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-authority-installation.js`);
|
|
329
|
+
return fs.existsSync(installation) && fs.readFileSync(installation, "utf8").includes(sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP);
|
|
330
|
+
}
|
|
331
|
+
#freezeUnraidReaper() {
|
|
332
|
+
const pidFile = this.#p("/run/cgroup2-unraid.pid");
|
|
333
|
+
if (!fs.existsSync(pidFile))
|
|
334
|
+
return;
|
|
335
|
+
const pid = Number(fs.readFileSync(pidFile, "utf8").trim());
|
|
336
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
337
|
+
return;
|
|
338
|
+
try {
|
|
339
|
+
process.kill(pid, "SIGSTOP");
|
|
340
|
+
}
|
|
341
|
+
catch { /* not running: nothing to hold */ }
|
|
342
|
+
}
|
|
313
343
|
#runtimeDirectories() {
|
|
314
344
|
for (const directory of [this.#epochRoot(), STAGING])
|
|
315
345
|
this.#directory(directory);
|
|
@@ -506,10 +536,10 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
506
536
|
if (failed)
|
|
507
537
|
throw new Error("Sanctuary gateway launch failed");
|
|
508
538
|
return this.#ready();
|
|
509
|
-
});
|
|
539
|
+
}, GATEWAY_READY_TIMEOUT_MS);
|
|
510
540
|
}
|
|
511
541
|
else
|
|
512
|
-
await this.#wait(() => this.#ready());
|
|
542
|
+
await this.#wait(() => this.#ready(), GATEWAY_READY_TIMEOUT_MS);
|
|
513
543
|
}
|
|
514
544
|
#target() {
|
|
515
545
|
const target = this.#containers().find((container) => container.Name === "/ouro-butler");
|
|
@@ -682,6 +712,10 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
682
712
|
if (fs.existsSync(this.#p(TEMPLATE_JOURNAL)))
|
|
683
713
|
throw new Error("Finish the pending installation before upgrading");
|
|
684
714
|
let journal = fs.existsSync(this.#p(UPGRADE)) ? this.#upgradeJournal() : this.#beginUpgrade(input);
|
|
715
|
+
// A rollback that stopped partway has already restored some records; resuming
|
|
716
|
+
// forward from its step list would act on a predecessor that the list calls switched.
|
|
717
|
+
if (journal.rollingBack)
|
|
718
|
+
throw new Error("A Sanctuary upgrade rollback is pending; run upgrade-rollback to finish it");
|
|
685
719
|
if (journal.to.imageId !== input.targetImageId || journal.to.imageReference !== input.imageReference)
|
|
686
720
|
throw new Error("A different Sanctuary upgrade is pending; roll it back first");
|
|
687
721
|
const target = () => new _a({ targetImageId: journal.to.imageId, rollbackImageId: journal.from.imageId }, this.#options);
|
|
@@ -714,6 +748,10 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
714
748
|
if (!fs.existsSync(this.#p(UPGRADE)))
|
|
715
749
|
return false;
|
|
716
750
|
let journal = this.#upgradeJournal();
|
|
751
|
+
if (!journal.rollingBack) {
|
|
752
|
+
journal = { ...journal, rollingBack: true };
|
|
753
|
+
this.#write(UPGRADE, JSON.stringify(journal));
|
|
754
|
+
}
|
|
717
755
|
await this.#halt();
|
|
718
756
|
if (journal.completed.includes("switch")) {
|
|
719
757
|
// A migration interrupted before the journal recorded it may or may not be
|
|
@@ -755,6 +793,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
755
793
|
const sides = [journal?.from, journal?.to];
|
|
756
794
|
if (!journal || journal.schemaVersion !== 1 || journal.epochId !== this.#request.epochId || !Array.isArray(journal.completed)
|
|
757
795
|
|| !journal.completed.every((step) => exports.SANCTUARY_UPGRADE_STEPS.includes(step))
|
|
796
|
+
|| (journal.rollingBack !== undefined && journal.rollingBack !== true)
|
|
758
797
|
|| !sides.every((side) => side && DIGEST.test(side.imageId) && DIGEST.test(side.packageDigest) && IMAGE_REFERENCE.test(side.imageReference))
|
|
759
798
|
|| !DIGEST.test(journal.from.rollbackImageId))
|
|
760
799
|
throw new Error("Sanctuary upgrade journal is invalid");
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.840",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.840",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|