@ouro.bot/cli 0.1.0-alpha.861 → 0.1.0-alpha.862

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,15 @@
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.862",
6
+ "changes": [
7
+ "Sanctuary tool context is now attached to every private-runtime turn of the sanctuary agent (await, habit, instinct, external-event), so await turns can observe the download queue.",
8
+ "The missing-Sanctuary-context result now uses code tool_context_missing and says it is a harness wiring gap, not a runtime outage.",
9
+ "restart_runtime now refuses to restart the daemon from autonomous turns (await, habit, external-event, instinct, scheduler) and enforces a persisted 6-hour cooldown, started only after the daemon accepts a restart.",
10
+ "resolve_await now constrains verdict to \"yes\" or \"no\", says that still pending is \"no\", and returns an explicit error naming the bad value and what to send."
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.861",
6
15
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.861",
2
+ "runtimeVersion": "0.1.0-alpha.862",
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.861</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.862</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -165,7 +165,7 @@ async function defaultTurnRunner(input) {
165
165
  // every Unraid read answered "Sanctuary runtime is unavailable"). The relationship
166
166
  // profile still decides which of those tools the peer may call.
167
167
  toolContext: {
168
- ...(input.agentName === "sanctuary" ? (0, sanctuary_runtime_1.createSanctuaryToolContext)(input.agentName) : {}),
168
+ ...((0, sanctuary_runtime_1.isSanctuaryAgent)(input.agentName) ? (0, sanctuary_runtime_1.createSanctuaryToolContext)(input.agentName) : {}),
169
169
  ...(input.relationshipAuthorization ? { relationshipAuthorization: input.relationshipAuthorization } : {}),
170
170
  ...(input.delegatedCommand ? { delegatedCommand: input.delegatedCommand } : {}),
171
171
  },
@@ -243,7 +243,7 @@ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName
243
243
  return JSON.stringify({ error: `await "${name}" is not pending (status: ${existing.status})` });
244
244
  }
245
245
  if (verdict !== "yes" && verdict !== "no") {
246
- return JSON.stringify({ error: "verdict must be 'yes' or 'no'" });
246
+ return JSON.stringify({ error: `verdict must be exactly "yes" or "no" (got ${JSON.stringify(verdict)}); use "no" with an observation while still waiting` });
247
247
  }
248
248
  if (!observation || !observation.trim()) {
249
249
  return JSON.stringify({ error: "observation is required" });
@@ -521,12 +521,12 @@ exports.awaitingToolDefinitions = [
521
521
  type: "function",
522
522
  function: {
523
523
  name: "resolve_await",
524
- description: "Resolve a pending await with a verdict. verdict='yes' archives and fires the alert. verdict='no' records the observation and continues polling.",
524
+ description: "Resolve a pending await with a verdict. verdict='yes' archives and fires the alert. verdict='no' records the observation and continues polling; still pending is always 'no'.",
525
525
  parameters: {
526
526
  type: "object",
527
527
  properties: {
528
528
  name: { type: "string", description: "Await name (filename stem)." },
529
- verdict: { type: "string", description: "'yes' if the condition is met, 'no' otherwise." },
529
+ verdict: { type: "string", enum: ["yes", "no"], description: "Exactly 'yes' if the condition is met. Exactly 'no' for anything else, including still pending, not yet, or unable to tell: send 'no' with an observation and polling continues." },
530
530
  observation: { type: "string", description: "One-line summary of what I saw this tick." },
531
531
  },
532
532
  required: ["name", "verdict", "observation"],
@@ -1,14 +1,99 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runtimeToolDefinitions = void 0;
36
+ exports.runtimeToolDefinitions = exports.RESTART_COOLDOWN_MS = void 0;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const node_crypto_1 = require("node:crypto");
4
40
  const identity_1 = require("../heart/identity");
5
41
  const runtime_1 = require("../nerves/runtime");
6
42
  const socket_client_1 = require("../heart/daemon/socket-client");
7
- async function restartRuntime(args, agentName) {
43
+ exports.RESTART_COOLDOWN_MS = 6 * 60 * 60 * 1000;
44
+ function restartCooldownPath(agentName) {
45
+ return path.join((0, identity_1.getAgentStateRoot)(agentName), "daemon", "restart-runtime-cooldown.json");
46
+ }
47
+ function readLastRestartMs(agentName) {
48
+ try {
49
+ const parsed = JSON.parse(fs.readFileSync(restartCooldownPath(agentName), "utf8"));
50
+ return typeof parsed.requestedAtMs === "number" ? parsed.requestedAtMs : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ // The cooldown is stored per agent, but the restart it limits is daemon-wide.
57
+ function recordRestart(agentName, nowMs) {
58
+ try {
59
+ const file = restartCooldownPath(agentName);
60
+ fs.mkdirSync(path.dirname(file), { recursive: true });
61
+ const temporary = `${file}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
62
+ fs.writeFileSync(temporary, JSON.stringify({ requestedAtMs: nowMs }), "utf8");
63
+ fs.renameSync(temporary, file);
64
+ }
65
+ catch {
66
+ // A failed cooldown write must not block an otherwise allowed restart.
67
+ }
68
+ }
69
+ async function restartRuntime(args, agentName, ctx) {
8
70
  if (typeof args.reason !== "string" || args.reason.trim().length === 0) {
9
71
  return JSON.stringify({ error: "reason is required (one-line audit string)" });
10
72
  }
11
73
  const reason = args.reason.trim();
74
+ if (ctx?.autonomousTurnKind) {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "repertoire",
77
+ event: "repertoire.runtime_restart_refused",
78
+ message: "restart_runtime refused in an autonomous turn",
79
+ meta: { agent: agentName, turnKind: ctx.autonomousTurnKind, reason },
80
+ });
81
+ return JSON.stringify({
82
+ refused: true,
83
+ code: "autonomous_turn_restart_refused",
84
+ error: `restart_runtime is not allowed from an autonomous ${ctx.autonomousTurnKind} turn: restarting the daemon kills in-flight work for every agent and habit. A tool error or missing capability is not evidence of a dead runtime. Record or report the problem (note it, surface it to the owner) instead of restarting.`,
85
+ });
86
+ }
87
+ const lastMs = readLastRestartMs(agentName);
88
+ const nowMs = Date.now();
89
+ if (lastMs !== null && lastMs <= nowMs && nowMs - lastMs < exports.RESTART_COOLDOWN_MS) {
90
+ return JSON.stringify({
91
+ refused: true,
92
+ code: "restart_cooldown",
93
+ error: `a restart was already requested ${Math.round((nowMs - lastMs) / 60000)} minutes ago; agent-requested restarts are limited to one per 6 hours. Record or report the problem instead.`,
94
+ retryAfterMs: exports.RESTART_COOLDOWN_MS - (nowMs - lastMs),
95
+ });
96
+ }
12
97
  (0, runtime_1.emitNervesEvent)({
13
98
  component: "repertoire",
14
99
  event: "repertoire.runtime_restart_requested",
@@ -21,6 +106,9 @@ async function restartRuntime(args, agentName) {
21
106
  reason,
22
107
  requestedBy: agentName,
23
108
  });
109
+ // Only a daemon that accepted the command starts the cooldown; a failed attempt must not lock the owner out.
110
+ if (response.ok)
111
+ recordRestart(agentName, nowMs);
24
112
  return JSON.stringify({
25
113
  requested: true,
26
114
  reason,
@@ -110,9 +198,9 @@ exports.runtimeToolDefinitions = [
110
198
  },
111
199
  },
112
200
  },
113
- handler: async (args) => {
201
+ handler: async (args, ctx) => {
114
202
  const agentName = (0, identity_1.getAgentName)();
115
- return restartRuntime({ reason: args.reason }, agentName);
203
+ return restartRuntime({ reason: args.reason }, agentName, ctx);
116
204
  },
117
205
  riskProfile: { mutates: "external_side_effect", risk: "high", reason: "restarts the hosting runtime" },
118
206
  },
@@ -336,7 +336,7 @@ function createUnraidReadTools(client) {
336
336
  }
337
337
  const emptyParameters = { type: "object", properties: {}, additionalProperties: false };
338
338
  function missingRuntime() {
339
- return JSON.stringify({ ok: false, error: { code: "invalid_response", message: "Sanctuary runtime is unavailable", degraded: true } });
339
+ return JSON.stringify({ ok: false, error: { code: "tool_context_missing", message: "Sanctuary tools are not wired into this turn; this is a harness wiring gap, not a runtime outage — do not restart; report it.", degraded: true } });
340
340
  }
341
341
  function householdRepairObligation(selection, target) {
342
342
  if (selection?.kind !== "standing" || selection.requester.kind !== "household_request")
@@ -46,6 +46,7 @@ exports.enrichDelegatedFromWithBridge = enrichDelegatedFromWithBridge;
46
46
  exports.routeDelegatedCompletion = routeDelegatedCompletion;
47
47
  exports.buildParseErrorNudge = buildParseErrorNudge;
48
48
  exports.buildHeldReturnWakeMessage = buildHeldReturnWakeMessage;
49
+ exports.sanctuaryToolContextFor = sanctuaryToolContextFor;
49
50
  exports.runPrivateRuntimeTurn = runPrivateRuntimeTurn;
50
51
  const fs = __importStar(require("fs"));
51
52
  const path = __importStar(require("path"));
@@ -968,6 +969,28 @@ function reduceHabitSessionToNoSend(habitSession) {
968
969
  },
969
970
  };
970
971
  }
972
+ /** Sanctuary tool context for a private turn: always for the sanctuary agent, and for sanctuary external events.
973
+ * Sanctuary external events keep the strict behavior (a missing config fails the turn); other turns degrade to
974
+ * no context so a config gap cannot stop the agent from waking at all. */
975
+ function sanctuaryToolContextFor(agentName, externalEventSource) {
976
+ const sanctuaryEvent = externalEventSource === "sanctuary-health" || externalEventSource === "sanctuary-usenet";
977
+ if (sanctuaryEvent)
978
+ return (0, sanctuary_runtime_1.createSanctuaryToolContext)(agentName);
979
+ if (!(0, sanctuary_runtime_1.isSanctuaryAgent)(agentName))
980
+ return {};
981
+ try {
982
+ return (0, sanctuary_runtime_1.createSanctuaryToolContext)(agentName);
983
+ }
984
+ catch (error) {
985
+ (0, runtime_1.emitNervesEvent)({
986
+ component: "senses",
987
+ event: "senses.sanctuary_tool_context_unavailable",
988
+ message: "sanctuary tool context could not be built for a private turn",
989
+ meta: { agent: agentName, reason: String(error) },
990
+ });
991
+ return {};
992
+ }
993
+ }
971
994
  async function runPrivateRuntimeTurn(options) {
972
995
  const now = options?.now ?? (() => new Date());
973
996
  const reason = options?.reason ?? "instinct";
@@ -1476,6 +1499,12 @@ async function runPrivateRuntimeTurn(options) {
1476
1499
  toolContext: {
1477
1500
  signin: async () => undefined,
1478
1501
  delegatedOrigins: attentionQueue,
1502
+ // Every private-runtime turn is autonomous; restart_runtime refuses to act on one.
1503
+ autonomousTurnKind: options?.externalEvent ? "external-event"
1504
+ : reason === "await" || reason === "habit" || reason === "instinct" ? reason : "scheduler",
1505
+ // The Butler's Sanctuary read tools need the typed runtime on every private turn
1506
+ // (await, habit, instinct, external-event), not only on sanctuary external events.
1507
+ ...(sanctuaryToolContextFor(agentName, options?.externalEvent?.source)),
1479
1508
  ...(relationshipAwait ? {
1480
1509
  friendStore: relationshipAwait.store,
1481
1510
  context: relationshipAwait.context,
@@ -1489,7 +1518,6 @@ async function runPrivateRuntimeTurn(options) {
1489
1518
  relationshipAuthorization: externalEventRelationship.relationshipAuthorization,
1490
1519
  externalEventAuthority: externalEventRelationship.externalEventAuthority,
1491
1520
  externalEventEffects: externalEventRelationship.externalEventEffects,
1492
- ...(["sanctuary-health", "sanctuary-usenet"].includes(options.externalEvent.source) ? (0, sanctuary_runtime_1.createSanctuaryToolContext)(agentName) : {}),
1493
1521
  } : {}),
1494
1522
  ...(options?.noSend ? { noSend: true } : {}),
1495
1523
  ...(effectiveHabitSession ? { habitSession: effectiveHabitSession } : {}),
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isSanctuaryAgent = isSanctuaryAgent;
36
37
  exports.runWithSanctuaryToolReceiptCollection = runWithSanctuaryToolReceiptCollection;
37
38
  exports.loadSanctuarySabApiKey = loadSanctuarySabApiKey;
38
39
  exports.ensureSanctuarySourceRuntimeReady = ensureSanctuarySourceRuntimeReady;
@@ -60,6 +61,10 @@ const bundle_manifest_1 = require("../mind/bundle-manifest");
60
61
  const sanctuaryToolReceipts = new node_async_hooks_1.AsyncLocalStorage();
61
62
  const sanctuaryToolGroundings = new node_async_hooks_1.AsyncLocalStorage();
62
63
  const acceptanceLedgerTails = new Map();
64
+ /** The Butler agent whose private and A2A turns carry the Sanctuary tool context. */
65
+ function isSanctuaryAgent(agentName) {
66
+ return agentName === "sanctuary";
67
+ }
63
68
  function isSanctuaryExternalEventSource(source) {
64
69
  return source === "sanctuary-health" || source === "sanctuary-usenet";
65
70
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.861",
3
+ "version": "0.1.0-alpha.862",
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.861",
9
+ "version": "0.1.0-alpha.862",
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.861",
3
+ "version": "0.1.0-alpha.862",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },