@ouro.bot/cli 0.1.0-alpha.835 → 0.1.0-alpha.837

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,18 @@
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.837",
6
+ "changes": [
7
+ "Sanctuary: the root authority keeps a permanent empty child cgroup (ouro-keep), so Unraid's cgroup2-unraid can no longer remove the authority cgroup (D-026). The host keeper now lets the reaper run and pauses it only while the keep child is missing; every reader of the authority cgroup ignores the keep child."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.836",
12
+ "changes": [
13
+ "release:bump appends new change lines to an existing top changelog entry instead of silently replacing them (D-042); --replace keeps the old rewrite behaviour."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.835",
6
18
  "changes": [
@@ -428,12 +428,13 @@ const GATEWAY_SUPERVISOR = `${AUTHORITY_CUSTOM}/gateway-supervisor.sh`
428
428
  const GATEWAY_WATCHDOG = `${AUTHORITY_CUSTOM}/gateway-keeper-watchdog.sh`
429
429
  const UNRAID_STOP_HOOK = "/boot/config/stop"
430
430
  const GATEWAY_SUPERVISOR_SH = String.raw`#!/bin/sh
431
- # Ouro authority gateway keeper for Unraid (D-026 workaround).
432
- # The .830 root-authority gateway needs /sys/fs/cgroup/ouro-authority to persist while it
433
- # runs, but Unraid's cgroup2-unraid reaps empty cgroups and the gateway never populates it,
434
- # so the reaper deletes the cgroup and kills the gateway. A FIRM (not per-loop-racing) reaper
435
- # pause is required for the gateway to reach readiness. This keeper:
436
- # - firmly pauses the reaper and keeps it paused,
431
+ # Ouro authority gateway keeper for Unraid.
432
+ # The root-authority gateway needs /sys/fs/cgroup/ouro-authority to persist, and Unraid's
433
+ # cgroup2-unraid removes every top-level cgroup that reports populated 0. The kernel will not
434
+ # remove a cgroup that has a child cgroup, so the authority keeps an empty ouro-keep child
435
+ # (D-026). This keeper:
436
+ # - creates that keep child and lets the reaper run; it pauses the reaper only while the
437
+ # keep child is missing (a fresh boot, or an authority older than the keep child),
437
438
  # - keeps the gateway process alive,
438
439
  # - reconnects the resident (docker restart) if it is unhealthy while the gateway is ready
439
440
  # (handles reboot ordering, where Docker autostarts the resident before the gateway).
@@ -443,15 +444,17 @@ R=/mnt/user/appdata/ouro-authority
443
444
  GW=$R/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js
444
445
  CFG=$R/active.json
445
446
  LOG=/var/log/ouro-gateway.log
446
- pause_reaper() {
447
+ reaper_policy() {
447
448
  P=$(cat /run/cgroup2-unraid.pid 2>/dev/null) || return 0
448
449
  [ -n "$P" ] || return 0
450
+ if [ -d "$CG/ouro-keep" ]; then kill -CONT "$P" 2>/dev/null; return 0; fi
449
451
  S=$(ps -o stat= -p "$P" 2>/dev/null | tr -d ' ')
450
452
  case "$S" in T*) : ;; *) kill -STOP "$P" 2>/dev/null ;; esac
451
453
  }
452
454
  ensure_cg() {
453
455
  [ -d "$CG" ] || { mkdir -m 700 "$CG" 2>/dev/null; chown 0:0 "$CG" 2>/dev/null
454
456
  for c in cpu memory pids; do echo "+$c" > "$CG/cgroup.subtree_control" 2>/dev/null; done; }
457
+ [ -d "$CG/ouro-keep" ] || mkdir -m 700 "$CG/ouro-keep" 2>/dev/null
455
458
  }
456
459
  ensure_sock() {
457
460
  # Docker autostarts the resident before us and auto-creates its missing bind
@@ -475,12 +478,12 @@ start_gw() {
475
478
  }
476
479
  # An in-place upgrade pauses us with a flag; a stale flag (> 30 min) is ignored.
477
480
  maintenance() { [ -n "$(find /run/ouro-authority-maintenance -mmin -30 2>/dev/null)" ]; }
478
- pause_reaper; ensure_cg; ensure_sock
481
+ ensure_cg; reaper_policy; ensure_sock
479
482
  last_res=0
480
483
  while true; do
481
484
  if maintenance; then sleep 15; continue; fi
482
- pause_reaper
483
485
  ensure_cg
486
+ reaper_policy
484
487
  ensure_sock
485
488
  [ -n "$(gw_pid)" ] || { start_gw; sleep 8; }
486
489
  rd=$(jq -rc .status "$R"/epochs/*/readiness.json 2>/dev/null)
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.835",
2
+ "runtimeVersion": "0.1.0-alpha.837",
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.835</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.837</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SANCTUARY_CGROUP_KEEP = void 0;
37
+ exports.sanctuaryCgroupChildren = sanctuaryCgroupChildren;
36
38
  exports.verifySanctuaryAuthorityInstallation = verifySanctuaryAuthorityInstallation;
37
39
  const node_crypto_1 = require("node:crypto");
38
40
  const fs = __importStar(require("node:fs"));
@@ -70,6 +72,16 @@ function readOwned(filePath, uid, gid, mode) {
70
72
  fs.closeSync(descriptor);
71
73
  }
72
74
  }
75
+ /**
76
+ * Unraid's cgroup2-unraid removes every top-level cgroup that reports `populated 0`,
77
+ * and the kernel refuses to remove a cgroup that has child cgroups. So the authority
78
+ * keeps one permanent empty child, and every reader of its cgroup ignores it (D-026).
79
+ */
80
+ exports.SANCTUARY_CGROUP_KEEP = "ouro-keep";
81
+ /** The authority cgroup's execution children: directories other than the keep child. */
82
+ function sanctuaryCgroupChildren(cgroupRoot) {
83
+ return fs.readdirSync(cgroupRoot).filter((name) => name !== exports.SANCTUARY_CGROUP_KEEP && fs.lstatSync(path.join(cgroupRoot, name)).isDirectory());
84
+ }
73
85
  function verifySanctuaryAuthorityInstallation(input) {
74
86
  const { expectedUid: uid, expectedGid: gid } = input;
75
87
  for (const root of [input.packageRoot, input.stateRoot, input.stagingRoot, input.cgroupRoot])
@@ -151,7 +151,7 @@ class SanctuaryAuthorityRootLifecycle {
151
151
  throw new Error("Resolve pending execution state before re-pinning");
152
152
  }
153
153
  }
154
- if (fs.readdirSync(this.#p(CGROUP)).some(name => fs.lstatSync(this.#p(`${CGROUP}/${name}`)).isDirectory()))
154
+ if ((0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
155
155
  throw new Error("Resolve pending cgroups before re-pinning");
156
156
  this.#verifyPrimitive("/usr/bin/prlimit", prlimitDigest);
157
157
  this.#verifyPrimitive("/usr/bin/setsid", setsidDigest);
@@ -294,9 +294,26 @@ class SanctuaryAuthorityRootLifecycle {
294
294
  throw new Error("Sanctuary installed boot lifecycle changed");
295
295
  return true;
296
296
  }
297
+ /** Create the authority cgroup with its keep child. The reaper can remove a freshly
298
+ * made (still empty) cgroup before the child exists, so retry that race a few times. */
299
+ #cgroup() {
300
+ const keep = this.#p(`${CGROUP}/${sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP}`);
301
+ for (let attempt = 1; !fs.existsSync(keep); attempt += 1) {
302
+ try {
303
+ this.#directory(CGROUP);
304
+ fs.mkdirSync(keep, { mode: 0o700 });
305
+ }
306
+ catch (error) {
307
+ if (error.code !== "ENOENT" || attempt >= 5)
308
+ throw error;
309
+ }
310
+ }
311
+ this.#directory(CGROUP);
312
+ }
297
313
  #runtimeDirectories() {
298
- for (const directory of [this.#epochRoot(), STAGING, CGROUP])
314
+ for (const directory of [this.#epochRoot(), STAGING])
299
315
  this.#directory(directory);
316
+ this.#cgroup();
300
317
  this.#directory(SOCKET, 0o750, this.#socketGid);
301
318
  // Add only the required controllers; never replace another owner's controls.
302
319
  for (const root of ["/sys/fs/cgroup", CGROUP]) {
@@ -546,7 +563,7 @@ class SanctuaryAuthorityRootLifecycle {
546
563
  const epoch = this.#epoch();
547
564
  if (proof.schemaVersion !== 1 || proof.keyId !== epoch.epochId || proof.publicKeyDigest !== epoch.publicKeyDigest || proof.quiescent !== true
548
565
  || !Number.isSafeInteger(proof.cursor) || proof.cursor < epoch.predecessorCursor
549
- || fs.readdirSync(this.#p(CGROUP)).some((entry) => fs.lstatSync(this.#p(`${CGROUP}/${entry}`)).isDirectory()))
566
+ || (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
550
567
  throw new Error("Sanctuary gateway retirement cleanup is unproven");
551
568
  return true;
552
569
  }
@@ -854,6 +871,7 @@ class SanctuaryAuthorityRootLifecycle {
854
871
  }
855
872
  /** Start the gateway, then the resident, and prove both. */
856
873
  async #startUpgraded() {
874
+ this.#runtimeDirectories();
857
875
  await this.#startGateway();
858
876
  this.#docker(["start", "ouro-butler"]);
859
877
  await this.#wait(async () => this.#target().State.Running && this.#target().State.Health?.Status === "healthy" && await this.#ready(), 300_000);
@@ -39,6 +39,7 @@ const node_child_process_1 = require("node:child_process");
39
39
  const fs = __importStar(require("node:fs"));
40
40
  const path = __importStar(require("node:path"));
41
41
  const runtime_1 = require("../../nerves/runtime");
42
+ const sanctuary_authority_installation_1 = require("./sanctuary-authority-installation");
42
43
  const SCHEMA_VERSION = 1;
43
44
  const DIGEST = /^sha256:[a-f0-9]{64}$/u;
44
45
  const PERMIT_ID = /^permit-[A-Za-z0-9_-]{43}$/u;
@@ -183,10 +184,8 @@ class DetachedSanctuaryHostSupervisor {
183
184
  throw new Error("Sanctuary host supervisor state is unmatched");
184
185
  }
185
186
  }
186
- for (const permitId of fs.readdirSync(this.#options.cgroupRoot)) {
187
+ for (const permitId of (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#options.cgroupRoot)) {
187
188
  const cgroupPath = path.join(this.#options.cgroupRoot, permitId);
188
- if (!fs.lstatSync(cgroupPath).isDirectory())
189
- continue;
190
189
  if (!PERMIT_ID.test(permitId))
191
190
  throw new Error("Sanctuary host cgroup entry is invalid");
192
191
  if (!known.has(permitId) && !await this.#reconcileCgroup(cgroupPath)) {
@@ -346,7 +346,7 @@ async function startSanctuaryTelegramAuthority(options) {
346
346
  hostAuthority.retireRegistrations(ledger);
347
347
  await reconcileExecutions();
348
348
  if (hostAuthority.retireRegistrations(ledger).length !== 0
349
- || fs.readdirSync(config.hostCgroupRoot).some((entry) => fs.lstatSync(path.join(config.hostCgroupRoot, entry)).isDirectory()))
349
+ || (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(config.hostCgroupRoot).length !== 0)
350
350
  throw new Error("Sanctuary authority retirement cleanup is unproven");
351
351
  writePrivateJson(path.join(config.epochRoot, "retirement.json"), {
352
352
  schemaVersion: 1, keyId: config.keyId, publicKeyDigest: config.publicKeyDigest,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.835",
3
+ "version": "0.1.0-alpha.837",
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.835",
9
+ "version": "0.1.0-alpha.837",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.835",
3
+ "version": "0.1.0-alpha.837",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },