@ouro.bot/cli 0.1.0-alpha.843 → 0.1.0-alpha.845

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,19 @@
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.845",
6
+ "changes": [
7
+ "Senses: a turn that errors now tells the person so instead of going silent (naming the step limit when that is the cause). This was the mechanism behind the Butler's answer-then-greet sequence (D-004): a question hit the provider step limit (D-007), nothing was sent, and each prod became its own turn. The engine still refuses to answer without evidence; the private channel stays silent."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.844",
12
+ "changes": [
13
+ "Sanctuary authority start is faster and no longer blind (D-036): package verification walks the tree once without following symlinks and then reads each file with O_NOFOLLOW, instead of resolving every file's real path (on Unraid that was two-thirds of each pass: about 15 s instead of about 47 s per pass, and a gateway start runs two), and the gateway's own output now goes to a root-only, size-capped gateway.log in the authority directory instead of nowhere.",
14
+ "Sanctuary: the agent's own view of its senses reports the tokenless root-authority Telegram as ready (it said needs_config while Telegram worked), and a token found in the resident's config as needs_config, matching the daemon's rule (D-047)."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.843",
6
19
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.843",
2
+ "runtimeVersion": "0.1.0-alpha.845",
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.843</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.845</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -58,8 +58,12 @@ function directory(filePath, uid, gid, mode) {
58
58
  throw new Error("Sanctuary authority installation directory metadata changed");
59
59
  }
60
60
  }
61
- function readOwned(filePath, uid, gid, mode) {
62
- canonical(filePath);
61
+ /** `trustedParent`: the caller already walked every parent directory without following
62
+ * symlinks, so only the leaf needs guarding, and O_NOFOLLOW does that. Resolving each
63
+ * package file's real path cost two-thirds of every verification on Unraid (D-036). */
64
+ function readOwned(filePath, uid, gid, mode, trustedParent = false) {
65
+ if (!trustedParent)
66
+ canonical(filePath);
63
67
  const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
64
68
  try {
65
69
  const stat = fs.fstatSync(descriptor);
@@ -102,8 +106,6 @@ function verifySanctuaryAuthorityInstallation(input) {
102
106
  || typeof pin.digest !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(pin.digest) || ![0o600, 0o644, 0o700, 0o755].includes(pin.mode)) {
103
107
  throw new Error("Sanctuary authority package file pin is invalid");
104
108
  }
105
- if (digest(readOwned(path.join(input.packageRoot, relative), uid, gid, pin.mode)) !== pin.digest)
106
- throw new Error("Sanctuary authority package file digest changed");
107
109
  }
108
110
  const actual = [];
109
111
  const walk = (relative) => {
@@ -125,6 +127,13 @@ function verifySanctuaryAuthorityInstallation(input) {
125
127
  walk("");
126
128
  if (JSON.stringify(actual.sort()) !== JSON.stringify(expected))
127
129
  throw new Error("Sanctuary authority package inventory changed");
130
+ // The walk above reached every file through real (not symlinked) directories, so a
131
+ // file read only needs to refuse a symlinked leaf.
132
+ for (const relative of expected) {
133
+ const pin = manifest.files[relative];
134
+ if (digest(readOwned(path.join(input.packageRoot, relative), uid, gid, pin.mode, true)) !== pin.digest)
135
+ throw new Error("Sanctuary authority package file digest changed");
136
+ }
128
137
  const controllers = ["cpu", "memory", "pids"];
129
138
  const readControl = (name) => fs.readFileSync(path.join(input.cgroupRoot, name), "utf8").trim();
130
139
  for (const name of ["cgroup.controllers", "cgroup.subtree_control"]) {
@@ -73,6 +73,7 @@ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
73
73
  // page cache on Unraid's array that takes minutes, so readiness gets a generous budget
74
74
  // (a 120 s budget failed a real rollback on 2026-09-24).
75
75
  const GATEWAY_READY_TIMEOUT_MS = 900_000;
76
+ const GATEWAY_LOG_MAX_BYTES = 5 * 1024 * 1024;
76
77
  const TEMPLATE_JOURNAL = "/boot/config/custom/ouro-butler/docker-man-template-transaction.json";
77
78
  // In-place upgrade of an installed authority: same epoch (token, issuer, cursor,
78
79
  // gateway state), new reviewed package and resident image. Every root record it
@@ -536,9 +537,16 @@ class SanctuaryAuthorityRootLifecycle {
536
537
  throw new Error("Sanctuary root token custody is not exclusive");
537
538
  if (this.#pid() === null) {
538
539
  this.#remove(`${this.#epochRoot()}/readiness.json`);
539
- const child = (0, node_child_process_1.spawn)(this.#p("/usr/local/bin/node"), [this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js`), "--config", this.#p(`${ROOT}/active.json`)], {
540
- cwd: "/", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, detached: true, stdio: "ignore",
541
- });
540
+ const log = this.#gatewayLog();
541
+ let child;
542
+ try {
543
+ child = (0, node_child_process_1.spawn)(this.#p("/usr/local/bin/node"), [this.#p(`${ROOT}/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js`), "--config", this.#p(`${ROOT}/active.json`)], {
544
+ cwd: "/", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, detached: true, stdio: ["ignore", log, log],
545
+ });
546
+ }
547
+ finally {
548
+ fs.closeSync(log);
549
+ }
542
550
  child.unref();
543
551
  let failed = false;
544
552
  child.once("error", () => { failed = true; });
@@ -551,6 +559,17 @@ class SanctuaryAuthorityRootLifecycle {
551
559
  else
552
560
  await this.#wait(() => this.#ready(), GATEWAY_READY_TIMEOUT_MS);
553
561
  }
562
+ /** The gateway's own output, kept root-only beside the token it guards and capped at two
563
+ * generations. It used to go nowhere, so a gateway that exited at startup said nothing
564
+ * (D-036: finding "host cgroup entry is invalid" took a hand-run gateway). */
565
+ #gatewayLog() {
566
+ const file = this.#p(`${ROOT}/gateway.log`);
567
+ if (fs.existsSync(file) && fs.statSync(file).size > GATEWAY_LOG_MAX_BYTES)
568
+ fs.renameSync(file, `${file}.1`);
569
+ const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW, 0o600);
570
+ fs.fchmodSync(fd, 0o600);
571
+ return fd;
572
+ }
554
573
  #target() {
555
574
  const target = this.#containers().find((container) => container.Name === "/ouro-butler");
556
575
  if (!target || target.Image !== this.#transaction.targetImageId || target.Config.User !== "10001:10001"
@@ -212,9 +212,14 @@ function readSenseStatusLines() {
212
212
  ? openAIRealtimeVoiceReady
213
213
  : cascadeVoiceReady,
214
214
  a2a: true,
215
- telegram: hasTextField(runtimePayload, "telegramBotToken")
216
- && hasTextField(runtimePayload, "telegramAuthorizedUserId")
217
- && hasTextField(runtimePayload, "telegramAuthorizedChatId"),
215
+ // Sanctuary's resident is deliberately tokenless: the root authority gateway holds the
216
+ // token, so a token in the resident's config would be the misconfiguration. Mirror the
217
+ // daemon's rule (sense-manager) instead of reporting a working sense as needs_config (D-047).
218
+ telegram: agentName === "sanctuary"
219
+ ? runtimeConfig.ok && machineRuntimeConfig.ok && !Object.hasOwn(runtimePayload, "telegramBotToken") && !Object.hasOwn(machinePayload, "telegramBotToken")
220
+ : hasTextField(runtimePayload, "telegramBotToken")
221
+ && hasTextField(runtimePayload, "telegramAuthorizedUserId")
222
+ && hasTextField(runtimePayload, "telegramAuthorizedChatId"),
218
223
  workbench: false,
219
224
  };
220
225
  const rows = [
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.stripThinkBlocks = stripThinkBlocks;
43
43
  exports.extractOutwardSenseDeliveryText = extractOutwardSenseDeliveryText;
44
44
  exports.getSenseSessionPath = getSenseSessionPath;
45
+ exports.erroredTurnReply = erroredTurnReply;
45
46
  exports.runSenseTurn = runSenseTurn;
46
47
  const os = __importStar(require("os"));
47
48
  const path = __importStar(require("path"));
@@ -333,6 +334,17 @@ function getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot
333
334
  * Caller provides channel, session key, friend, and message;
334
335
  * this function handles all pipeline wiring.
335
336
  */
337
+ /**
338
+ * What an outward sense says when a turn errors and provider failover has nothing to say.
339
+ * Such turns used to end in silence: the owner saw no reply, prodded, and later got
340
+ * the late answer followed by a greeting to a prod (D-004); the step-limit stop was the
341
+ * observed cause (D-007). The engine still refuses to answer without evidence.
342
+ */
343
+ function erroredTurnReply(errorMessage) {
344
+ return /iteration limit exhausted/u.test(errorMessage ?? "")
345
+ ? "I couldn't finish that one: I ran out of steps before I had an answer I could stand behind. Ask me again, or ask something narrower, and I'll take another run at it."
346
+ : "I couldn't finish that one: something went wrong before I had an answer. Ask me again and I'll retry.";
347
+ }
336
348
  async function runSenseTurn(options) {
337
349
  return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
338
350
  const owner = Object.freeze({ agentName: options.agentName, agentRoot: (0, identity_1.getAgentRoot)(options.agentName) });
@@ -453,6 +465,7 @@ async function runSenseTurnExclusive(options, owner) {
453
465
  // once the turn completes.
454
466
  let committedResponseText = "";
455
467
  let pendingResponseText = "";
468
+ let terminalErrorMessage;
456
469
  let terminalDeliveryKind = "text";
457
470
  const deliveries = [];
458
471
  const deliveryFailures = [];
@@ -535,6 +548,8 @@ async function runSenseTurnExclusive(options, owner) {
535
548
  emitFrontendEvent({ type: "tool_completed", data: { name, summary: _summary, success } });
536
549
  },
537
550
  onError: (error, severity) => {
551
+ if (severity === "terminal")
552
+ terminalErrorMessage = error.message;
538
553
  emitFrontendEvent({ type: "error", data: { message: error.message, severity } });
539
554
  },
540
555
  onClearText: () => {
@@ -661,7 +676,9 @@ async function runSenseTurnExclusive(options, owner) {
661
676
  const eventView = currentTurnEventView(terminalEvents, existingEventIds, userMessage, options.precommittedIngress, options.ingressRelations);
662
677
  let finalDeliveryKind = terminalDeliveryKind;
663
678
  const acceptedTerminalOutcome = turnResult.turnOutcome === "settled" || turnResult.turnOutcome === "blocked";
664
- const failoverText = turnResult.turnOutcome === "errored" ? turnResult.failoverMessage?.trim() : undefined;
679
+ const failoverText = turnResult.turnOutcome === "errored"
680
+ ? turnResult.failoverMessage?.trim() || (channel === "inner" ? undefined : erroredTurnReply(terminalErrorMessage))
681
+ : undefined;
665
682
  const expectsOutwardResponse = acceptedTerminalOutcome || turnResult.turnOutcome === "command" || Boolean(failoverText);
666
683
  let finalCausalCoordinate;
667
684
  if (acceptedTerminalOutcome) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.843",
3
+ "version": "0.1.0-alpha.845",
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.843",
9
+ "version": "0.1.0-alpha.845",
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.843",
3
+ "version": "0.1.0-alpha.845",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },