@ouro.bot/cli 0.1.0-alpha.732 → 0.1.0-alpha.734

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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/changelog.json +34 -0
  3. package/deploy/unraid/Dockerfile +27 -0
  4. package/deploy/unraid/README.txt +1368 -0
  5. package/deploy/unraid/audit-container-spec.sh +4 -0
  6. package/deploy/unraid/container-runtime.json +1 -0
  7. package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
  8. package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
  9. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  10. package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
  11. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  12. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
  13. package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
  14. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  15. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  16. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  17. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  18. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  19. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
  20. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
  21. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
  22. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
  23. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
  24. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
  25. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
  26. package/deploy/unraid/sanctuary.xml +25 -0
  27. package/dist/heart/approval-files.js +113 -0
  28. package/dist/heart/approval-store.js +168 -9
  29. package/dist/heart/auth/auth-flow.js +17 -1
  30. package/dist/heart/autonomy-budget.js +44 -1
  31. package/dist/heart/awaiting/await-scheduler.js +6 -2
  32. package/dist/heart/config.js +8 -0
  33. package/dist/heart/core.js +117 -132
  34. package/dist/heart/daemon/cli-exec.js +74 -2
  35. package/dist/heart/daemon/cli-help.js +6 -1
  36. package/dist/heart/daemon/cli-parse.js +6 -4
  37. package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
  38. package/dist/heart/daemon/container-healthcheck.js +94 -0
  39. package/dist/heart/daemon/container-runtime.js +84 -0
  40. package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
  41. package/dist/heart/daemon/container-spec-auditor.js +156 -0
  42. package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
  43. package/dist/heart/daemon/daemon-entry.js +116 -28
  44. package/dist/heart/daemon/daemon-health.js +10 -0
  45. package/dist/heart/daemon/daemon.js +92 -5
  46. package/dist/heart/daemon/doctor.js +56 -7
  47. package/dist/heart/daemon/runtime-logging.js +6 -0
  48. package/dist/heart/daemon/runtime-mode.js +13 -2
  49. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
  50. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
  51. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  52. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
  53. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  54. package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
  55. package/dist/heart/daemon/sense-manager.js +25 -6
  56. package/dist/heart/daemon/supercronic-supervisor.js +267 -0
  57. package/dist/heart/habits/habit-scheduler.js +7 -3
  58. package/dist/heart/identity.js +13 -3
  59. package/dist/heart/provider-credentials.js +9 -1
  60. package/dist/heart/provider-models.js +8 -0
  61. package/dist/heart/provider-ping.js +5 -2
  62. package/dist/heart/providers/openai-compatible.js +177 -0
  63. package/dist/heart/runtime-credentials.js +217 -8
  64. package/dist/heart/sense-truth.js +2 -0
  65. package/dist/heart/tool-approval.js +1 -0
  66. package/dist/heart/turn-context.js +8 -0
  67. package/dist/mailroom/blob-store.js +97 -19
  68. package/dist/mailroom/cache-sync-cli.js +5 -1
  69. package/dist/mailroom/core.js +23 -8
  70. package/dist/mailroom/hosted-cache-sync.js +179 -44
  71. package/dist/mailroom/search-cache.js +109 -0
  72. package/dist/mind/prompt.js +9 -0
  73. package/dist/nerves/coverage/source-scanner.js +4 -4
  74. package/dist/nerves/index.js +75 -9
  75. package/dist/nerves/runtime.js +12 -0
  76. package/dist/repertoire/tool-arguments.js +39 -0
  77. package/dist/repertoire/tools-unraid.js +353 -0
  78. package/dist/repertoire/tools.js +19 -1
  79. package/dist/repertoire/unraid-client.js +155 -0
  80. package/dist/repertoire/unraid-restart.js +130 -0
  81. package/dist/senses/private-runtime-worker.js +3 -2
  82. package/dist/senses/sanctuary-grounding.js +118 -0
  83. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  84. package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
  85. package/dist/senses/sanctuary-health-runner.js +146 -0
  86. package/dist/senses/sanctuary-health.js +447 -0
  87. package/dist/senses/sanctuary-interactive-control.js +306 -0
  88. package/dist/senses/sanctuary-runtime.js +206 -0
  89. package/dist/senses/shared-turn.js +11 -3
  90. package/dist/senses/telegram-approval-runtime.js +526 -0
  91. package/dist/senses/telegram-audit-ledger.js +220 -0
  92. package/dist/senses/telegram-client.js +1471 -0
  93. package/dist/senses/telegram-entry.js +80 -0
  94. package/dist/senses/telegram.js +1232 -0
  95. package/npm-shrinkwrap.json +6558 -0
  96. package/package.json +4 -2
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_PROVIDER_ITERATIONS = void 0;
4
+ exports.validateToolCallBatchAtProductionBoundary = validateToolCallBatchAtProductionBoundary;
3
5
  exports.createProviderRegistry = createProviderRegistry;
4
6
  exports.resetProviderRuntime = resetProviderRuntime;
5
7
  exports.getModel = getModel;
@@ -24,6 +26,20 @@ const friends_1 = require("@ouro.bot/friends");
24
26
  const tools_2 = require("../repertoire/tools");
25
27
  const streaming_1 = require("./streaming");
26
28
  const runtime_1 = require("../nerves/runtime");
29
+ function validateToolCallBatchAtProductionBoundary(calls, activeTools) {
30
+ const callIdCounts = new Map();
31
+ for (const call of calls)
32
+ callIdCounts.set(call.id, (callIdCounts.get(call.id) ?? 0) + 1);
33
+ return calls.map((call) => {
34
+ if (callIdCounts.get(call.id) !== 1)
35
+ return { call, error: "duplicate tool call id" };
36
+ const advertised = activeTools.find((tool) => tool.function.name === call.name);
37
+ if (!advertised || !advertised.function.parameters || typeof advertised.function.parameters !== "object")
38
+ return { call, error: "tool was not advertised with a valid argument schema" };
39
+ const validated = (0, tool_arguments_1.validateAdvertisedToolArguments)(call.arguments, advertised.function.parameters);
40
+ return validated.ok ? { call, advertised, validated: validated.value } : { call, error: validated.reason };
41
+ });
42
+ }
27
43
  const context_1 = require("../mind/context");
28
44
  const prompt_budget_1 = require("../mind/prompt-budget");
29
45
  const prompt_1 = require("../mind/prompt");
@@ -34,6 +50,7 @@ const azure_1 = require("./providers/azure");
34
50
  const minimax_1 = require("./providers/minimax");
35
51
  const openai_codex_1 = require("./providers/openai-codex");
36
52
  const github_copilot_1 = require("./providers/github-copilot");
53
+ const openai_compatible_1 = require("./providers/openai-compatible");
37
54
  const orientation_frame_1 = require("./orientation-frame");
38
55
  const identity_2 = require("./identity");
39
56
  const socket_client_1 = require("./daemon/socket-client");
@@ -109,6 +126,9 @@ function createProviderRegistry() {
109
126
  return (0, openai_codex_1.createOpenAICodexProviderRuntime)(model, providerConfig);
110
127
  case "github-copilot":
111
128
  return (0, github_copilot_1.createGithubCopilotProviderRuntime)(model, providerConfig);
129
+ case "openai-compatible":
130
+ case "openai-compatible-gemini":
131
+ return (0, openai_compatible_1.createOpenAICompatibleProviderRuntime)(provider, model, providerConfig);
112
132
  }
113
133
  },
114
134
  };
@@ -198,6 +218,8 @@ function getProviderDisplayLabel(facing = "human") {
198
218
  "openai-codex": () => `openai codex (${model})`,
199
219
  /* v8 ignore next -- branch: tested via display label unit test @preserve */
200
220
  "github-copilot": () => `github copilot (${model})`,
221
+ "openai-compatible": () => `z.ai openai-compatible (${model})`,
222
+ "openai-compatible-gemini": () => `gemini openai-compatible (${model})`,
201
223
  };
202
224
  return providerLabelBuilders[provider]();
203
225
  }
@@ -247,6 +269,7 @@ function createHabitCallbackBuffer(callbacks) {
247
269
  function createFinalOnlyTextBuffer(callbacks) {
248
270
  return createCallbackBuffer(callbacks, "text-only");
249
271
  }
272
+ exports.MAX_PROVIDER_ITERATIONS = 8;
250
273
  function continuationCallbacks(callbacks, text) {
251
274
  const noop = () => undefined;
252
275
  return {
@@ -356,18 +379,6 @@ function hasFreshPendingWork(options) {
356
379
  && message.content.trim().length > 0);
357
380
  }
358
381
  const HABIT_CONTROL_TOOLS = new Set(["rest", "ponder", "observe"]);
359
- function habitToolArgs(call) {
360
- try {
361
- const parsed = JSON.parse(call.arguments);
362
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
363
- return { ok: false, reason: `habit tool '${call.name}' arguments must be a JSON object` };
364
- }
365
- return { ok: true, args: parsed };
366
- }
367
- catch {
368
- return { ok: false, reason: `habit tool '${call.name}' has malformed JSON arguments` };
369
- }
370
- }
371
382
  function highRiskExternalMutation(profile) {
372
383
  if (profile.risk !== "high")
373
384
  return null;
@@ -383,8 +394,8 @@ function recordBlockedHabitSurfaceAttempts(habitSession, toolCalls, reason) {
383
394
  for (const call of toolCalls) {
384
395
  if (call.name !== "send_message" && call.name !== "surface")
385
396
  continue;
386
- const parsed = habitToolArgs(call);
387
- const args = parsed.ok ? parsed.args : {};
397
+ // The canonical pre-batch schema gate guarantees object arguments here.
398
+ const args = JSON.parse(call.arguments);
388
399
  habitSession.recordSurfaceAttempt({
389
400
  recipient: String(args.friendId ?? args.delegationId ?? "unknown"),
390
401
  channel: String(args.channel ?? call.name),
@@ -395,10 +406,7 @@ function recordBlockedHabitSurfaceAttempts(habitSession, toolCalls, reason) {
395
406
  });
396
407
  }
397
408
  }
398
- async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigins, activeToolNames, noSend) {
399
- if (noSend && toolCalls.some((call) => call.name === "ponder")) {
400
- return "habit tool 'ponder' cannot create continuations under immutable no-send authority";
401
- }
409
+ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigins) {
402
410
  if (!habitSession)
403
411
  return null;
404
412
  const granted = new Set(habitSession.toolPolicy.grantedTools);
@@ -406,16 +414,13 @@ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigi
406
414
  for (const call of toolCalls) {
407
415
  if (denied.has(call.name))
408
416
  return `habit tool '${call.name}' is denied by this habit session`;
409
- if (!activeToolNames.has(call.name))
410
- return `habit tool '${call.name}' was not advertised to this model turn`;
411
417
  if (HABIT_CONTROL_TOOLS.has(call.name))
412
418
  continue;
413
419
  if (!granted.has(call.name))
414
420
  return `habit tool '${call.name}' was not granted to this habit session`;
415
- const parsed = habitToolArgs(call);
416
- if (!parsed.ok)
417
- return parsed.reason;
418
- const riskProfile = (0, tools_1.riskProfileForToolName)(call.name, parsed.args);
421
+ // The canonical pre-batch schema gate guarantees object arguments here.
422
+ const args = JSON.parse(call.arguments);
423
+ const riskProfile = (0, tools_1.riskProfileForToolName)(call.name, args);
419
424
  if (!riskProfile)
420
425
  return `habit tool '${call.name}' does not have a known executable risk profile`;
421
426
  const externalMutation = highRiskExternalMutation(riskProfile);
@@ -427,7 +432,7 @@ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigi
427
432
  agentRoot: (0, identity_2.getAgentRoot)(),
428
433
  envelope: habitSession.permissionEnvelope,
429
434
  toolName: call.name,
430
- args: parsed.args,
435
+ args,
431
436
  friendStore: habitSession.friendStore,
432
437
  delegatedOrigins,
433
438
  });
@@ -441,7 +446,7 @@ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigi
441
446
  * BlueBubbles is final-only: its adapter owns one accepted visibility boundary.
442
447
  * The private runtime has `ponder`; MCP returns synchronously; mail is batch. */
443
448
  function isChatStyleChannel(channel) {
444
- return channel === "cli" || channel === "teams" || channel === "voice";
449
+ return (0, friends_1.getChannelCapabilities)(channel).chatStyle;
445
450
  }
446
451
  function bindCurrentIngressEvidenceLocator(toolList, evidence) {
447
452
  if (!evidence
@@ -493,13 +498,8 @@ function parseSettlePayload(argumentsText) {
493
498
  return { answer, intent };
494
499
  }
495
500
  function parsePonderPayload(argumentsText) {
496
- try {
497
- const parsed = JSON.parse(argumentsText);
498
- return parsed && typeof parsed === "object" ? parsed : {};
499
- }
500
- catch {
501
- return {};
502
- }
501
+ // The canonical pre-batch schema gate guarantees a valid object payload.
502
+ return JSON.parse(argumentsText);
503
503
  }
504
504
  function parseSuccessCriteria(raw) {
505
505
  if (typeof raw !== "string")
@@ -885,7 +885,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
885
885
  generatedMessages.push(...structuredClone(next));
886
886
  };
887
887
  const facing = (0, friends_1.channelToFacing)(channel);
888
- let providerRuntime = await getProviderRuntime(facing);
888
+ let providerRuntime = options?.providerRuntimeOverride ?? await getProviderRuntime(facing);
889
889
  const provider = providerRuntime.id;
890
890
  const toolChoiceRequired = options?.toolChoiceRequired ?? true;
891
891
  const traceId = options?.traceId;
@@ -1039,6 +1039,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1039
1039
  // a ponder packet created the return obligation in this turn.
1040
1040
  let noToolCallRetries = 0;
1041
1041
  const NO_TOOL_CALL_MAX_RETRIES = 2;
1042
+ let providerIterations = 0;
1042
1043
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
1043
1044
  const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
1044
1045
  const finishTerminalProviderError = (error, classification) => {
@@ -1126,7 +1127,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1126
1127
  const filteredBaseTools = isPrivateRuntimeChannel
1127
1128
  ? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
1128
1129
  : baseTools;
1129
- const activeTools = [
1130
+ const ordinaryActiveTools = [
1130
1131
  ...filteredBaseTools,
1131
1132
  ...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
1132
1133
  ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
@@ -1135,6 +1136,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1135
1136
  ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
1136
1137
  ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
1137
1138
  ];
1139
+ const activeTools = options?.toolProfile === "sanctuary-health-private"
1140
+ ? (() => {
1141
+ const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
1142
+ if (channel !== "inner" || sendTools.length !== 1 || baseTools.length !== 1) {
1143
+ throw new Error("sanctuary-health-private requires inner channel with exactly one canonical send_message definition");
1144
+ }
1145
+ return [sendTools[0], tools_1.restTool];
1146
+ })()
1147
+ : ordinaryActiveTools;
1138
1148
  const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
1139
1149
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
1140
1150
  if (steeringFollowUps.length > 0) {
@@ -1278,6 +1288,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1278
1288
  continue;
1279
1289
  }
1280
1290
  const result = attempt.value;
1291
+ providerIterations += 1;
1292
+ if (providerIterations === exports.MAX_PROVIDER_ITERATIONS && result.toolCalls.length > 0) {
1293
+ throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} before tool execution`);
1294
+ }
1281
1295
  const streamCallbackBuffer = turnCallbackBufferRef.current;
1282
1296
  turnCallbackBufferRef.current = null;
1283
1297
  if (result.settleFinalization && !result.settleFinalization.ok) {
@@ -1454,7 +1468,54 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1454
1468
  else {
1455
1469
  // Reset the retry counter on any successful tool call.
1456
1470
  noToolCallRetries = 0;
1457
- const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins, activeToolNames, augmentedToolContext?.noSend === true);
1471
+ const preCallMessages = structuredClone(messages.filter((message) => message.role !== "system"));
1472
+ const validatedCalls = validateToolCallBatchAtProductionBoundary(result.toolCalls, activeTools);
1473
+ const invalidCall = validatedCalls.find((entry) => "error" in entry);
1474
+ if (invalidCall) {
1475
+ await streamCallbackBuffer?.flush();
1476
+ pushGenerated(msg);
1477
+ const unadvertisedCall = validatedCalls.find((entry) => !activeToolNames.has(entry.call.name));
1478
+ for (const entry of validatedCalls) {
1479
+ const detail = "error" in entry ? entry.error : "another call in this batch had invalid arguments";
1480
+ const rejection = unadvertisedCall
1481
+ ? `rejected: ${entry.call.name} was not advertised for this channel; no handler was executed.`
1482
+ : `invalid tool arguments: ${detail}`;
1483
+ pushGenerated({ role: "tool", tool_call_id: entry.call.id, content: rejection });
1484
+ providerRuntime.appendToolOutput(entry.call.id, rejection);
1485
+ options?.toolBoundaryObserver?.({
1486
+ name: entry.call.name,
1487
+ reason: activeToolNames.has(entry.call.name) ? "invalid_arguments" : "profile_excluded",
1488
+ globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(entry.call.name)?.handler === "function",
1489
+ invoked: false,
1490
+ sideEffect: false,
1491
+ });
1492
+ }
1493
+ if (unadvertisedCall) {
1494
+ (0, runtime_1.emitNervesEvent)({
1495
+ level: "warn",
1496
+ component: "engine",
1497
+ event: "engine.unadvertised_tool_blocked",
1498
+ message: "blocked an unadvertised tool call before batch execution",
1499
+ meta: { channel: String(channel), toolName: unadvertisedCall.call.name },
1500
+ });
1501
+ }
1502
+ else {
1503
+ (0, runtime_1.emitNervesEvent)({
1504
+ level: "warn",
1505
+ component: "engine",
1506
+ event: "engine.tool_arguments_rejected",
1507
+ message: "tool batch rejected before execution because arguments were invalid",
1508
+ meta: { toolCallId: invalidCall.call.id, toolName: invalidCall.call.name },
1509
+ });
1510
+ }
1511
+ continue;
1512
+ }
1513
+ const validCalls = validatedCalls;
1514
+ const validatedCallArguments = new Map(validCalls.map((entry) => [
1515
+ entry.call,
1516
+ entry.validated.arguments,
1517
+ ]));
1518
+ const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins);
1458
1519
  if (habitBlockReason) {
1459
1520
  streamCallbackBuffer?.discard();
1460
1521
  recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
@@ -1480,16 +1541,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1480
1541
  ? (0, tools_1.resolveToolDefinition)(soleTerminalCall.name)?.terminalProjection
1481
1542
  : undefined;
1482
1543
  if (soleTerminalCall && soleTerminalProjection?.mode === "verbatim") {
1483
- let terminalArgs = {};
1484
- try {
1485
- const parsed = JSON.parse(soleTerminalCall.arguments);
1486
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1487
- terminalArgs = parsed;
1488
- }
1489
- }
1490
- catch {
1491
- // The registered handler owns exact argument validation.
1492
- }
1544
+ const terminalArgs = validatedCallArguments.get(soleTerminalCall);
1493
1545
  if (soleTerminalProjection.clearBufferedText
1494
1546
  || callbacks.settleOutputMode === "final_only") {
1495
1547
  streamCallbackBuffer?.discard();
@@ -1529,13 +1581,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1529
1581
  }
1530
1582
  // Check for settle sole call: intercept before tool execution
1531
1583
  if (isSoleSettle) {
1532
- /* v8 ignore next -- defensive: JSON.parse catch for malformed settle args @preserve */
1533
- const settleArgs = (() => { try {
1534
- return JSON.parse(result.toolCalls[0].arguments);
1535
- }
1536
- catch {
1537
- return {};
1538
- } })();
1584
+ const settleArgs = validatedCallArguments.get(result.toolCalls[0]);
1539
1585
  callbacks.onToolStart("settle", settleArgs);
1540
1586
  // Private-runtime attention queue gate: reject settle if items remain
1541
1587
  const attentionQueue = augmentedToolContext?.delegatedOrigins;
@@ -1627,13 +1673,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1627
1673
  const isSoleObserve = result.toolCalls.length === 1 && result.toolCalls[0].name === "observe";
1628
1674
  if (isSoleObserve) {
1629
1675
  streamCallbackBuffer?.discard();
1630
- /* v8 ignore next -- defensive: JSON.parse catch for malformed observe args @preserve */
1631
- const observeArgs = (() => { try {
1632
- return JSON.parse(result.toolCalls[0].arguments);
1633
- }
1634
- catch {
1635
- return {};
1636
- } })();
1676
+ const observeArgs = validatedCallArguments.get(result.toolCalls[0]);
1637
1677
  let reason;
1638
1678
  if (typeof observeArgs?.reason === "string")
1639
1679
  reason = observeArgs.reason;
@@ -1657,12 +1697,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1657
1697
  const isSoleRest = result.toolCalls.length === 1 && result.toolCalls[0].name === "rest";
1658
1698
  if (isSoleRest) {
1659
1699
  streamCallbackBuffer?.discard();
1660
- const restArgs = (() => { try {
1661
- return JSON.parse(result.toolCalls[0].arguments);
1662
- }
1663
- catch {
1664
- return {};
1665
- } })();
1700
+ const restArgs = validatedCallArguments.get(result.toolCalls[0]);
1666
1701
  callbacks.onToolStart("rest", restArgs);
1667
1702
  // Attention queue gate: reject rest if items remain
1668
1703
  const attentionQueue = augmentedToolContext?.delegatedOrigins;
@@ -1705,42 +1740,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1705
1740
  done = true;
1706
1741
  continue;
1707
1742
  }
1708
- const preCallMessages = structuredClone(messages.filter((message) => message.role !== "system"));
1709
- const validatedCalls = options?.approvalCoordinator
1710
- ? result.toolCalls.map((call) => {
1711
- const advertised = activeTools.find((tool) => tool.function.name === call.name);
1712
- if (!advertised || !advertised.function.parameters || typeof advertised.function.parameters !== "object") {
1713
- return { call, error: "tool was not advertised with a valid argument schema" };
1714
- }
1715
- const validated = (0, tool_arguments_1.validateAdvertisedToolArguments)(call.arguments, advertised.function.parameters);
1716
- return validated.ok
1717
- ? { call, advertised, validated: validated.value }
1718
- : { call, error: validated.reason };
1719
- })
1720
- : null;
1721
- const invalidApprovalCall = validatedCalls?.find((entry) => "error" in entry);
1722
- if (invalidApprovalCall) {
1723
- await streamCallbackBuffer?.flush();
1724
- pushGenerated(msg);
1725
- for (const entry of validatedCalls) {
1726
- const detail = "error" in entry ? entry.error : "another call in this batch had invalid arguments";
1727
- const rejection = `invalid tool arguments: ${detail}`;
1728
- pushGenerated({ role: "tool", tool_call_id: entry.call.id, content: rejection });
1729
- providerRuntime.appendToolOutput(entry.call.id, rejection);
1730
- }
1731
- (0, runtime_1.emitNervesEvent)({
1732
- level: "warn",
1733
- component: "engine",
1734
- event: "engine.tool_arguments_rejected",
1735
- message: "tool batch rejected before execution because arguments were invalid",
1736
- meta: { toolCallId: invalidApprovalCall.call.id, toolName: invalidApprovalCall.call.name },
1737
- });
1738
- continue;
1739
- }
1740
- const approvalCalls = validatedCalls?.map((entry) => ({
1743
+ const approvalCalls = options?.approvalCoordinator ? validCalls.map((entry) => ({
1741
1744
  ...entry,
1742
1745
  policy: (0, tools_1.approvalPolicyForToolName)(entry.call.name, entry.validated.arguments),
1743
- })) ?? [];
1746
+ })) : [];
1744
1747
  const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
1745
1748
  if (protectedCall && result.toolCalls.length !== 1) {
1746
1749
  streamCallbackBuffer?.discard();
@@ -1816,21 +1819,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1816
1819
  for (const tc of result.toolCalls) {
1817
1820
  if (signal?.aborted)
1818
1821
  break;
1819
- if (tc.name === "speak" && !activeToolNames.has("speak")) {
1820
- const rejection = "rejected: speak was not advertised for this channel; no outward message was sent.";
1821
- callbacks.onToolStart(tc.name, {});
1822
- callbacks.onToolEnd(tc.name, "", false);
1823
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
1824
- providerRuntime.appendToolOutput(tc.id, rejection);
1825
- (0, runtime_1.emitNervesEvent)({
1826
- level: "warn",
1827
- component: "engine",
1828
- event: "engine.unadvertised_speak_blocked",
1829
- message: "blocked an unadvertised speak tool call",
1830
- meta: { channel: String(channel) },
1831
- });
1832
- continue;
1833
- }
1834
1822
  // Reject sole-call tools when mixed with other tool calls
1835
1823
  const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
1836
1824
  const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
@@ -1842,13 +1830,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1842
1830
  providerRuntime.appendToolOutput(tc.id, soleCallRejection);
1843
1831
  continue;
1844
1832
  }
1845
- let args = {};
1846
- try {
1847
- args = JSON.parse(tc.arguments);
1848
- }
1849
- catch {
1850
- /* ignore */
1851
- }
1833
+ const args = validatedCallArguments.get(tc);
1852
1834
  if (tc.name === "send_message" && args.friendId === "self") {
1853
1835
  const latestUserText = latestUserMessageText(messages);
1854
1836
  if (!isPrivateRuntimeChannel && looksLikePrivateReturnRequest(latestUserText)) {
@@ -1863,12 +1845,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1863
1845
  sawSendMessageSelf = true;
1864
1846
  }
1865
1847
  if (tc.name === "speak") {
1866
- let speakArgs = {};
1867
- try {
1868
- speakArgs = JSON.parse(tc.arguments);
1869
- }
1870
- catch { /* malformed */ }
1871
- const speakMessage = typeof speakArgs.message === "string" ? speakArgs.message : "";
1848
+ // The canonical pre-batch schema gate guarantees a required string.
1849
+ const speakArgs = JSON.parse(tc.arguments);
1850
+ const speakMessage = speakArgs.message;
1872
1851
  const argSummary = (0, tools_1.summarizeArgs)("speak", { message: speakMessage });
1873
1852
  callbacks.onToolStart("speak", { message: speakMessage });
1874
1853
  if (speakMessage.trim().length === 0) {
@@ -2024,7 +2003,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2024
2003
  }
2025
2004
  }
2026
2005
  }
2027
- else if (action === "revise") {
2006
+ else {
2028
2007
  const packetId = typeof parsedArgs.packet_id === "string" ? parsedArgs.packet_id.trim() : "";
2029
2008
  const kind = parsedArgs.kind;
2030
2009
  const objective = typeof parsedArgs.objective === "string" ? parsedArgs.objective.trim() : "";
@@ -2045,9 +2024,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2045
2024
  : null;
2046
2025
  resultAction = "revised";
2047
2026
  }
2048
- else {
2049
- throw new Error("ponder requires action=create or revise.");
2050
- }
2051
2027
  if (returnObligationId) {
2052
2028
  for (const token of extractPrivateReturnHeldTokens(privateReturnSourceRequest)) {
2053
2029
  privateReturnHeldTokens.add(token);
@@ -2110,6 +2086,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2110
2086
  success = false;
2111
2087
  augmentedToolContext?.habitSession?.recordError?.(toolResult);
2112
2088
  }
2089
+ const resolvedRiskProfile = (0, tools_1.resolveToolDefinition)(tc.name)?.riskProfile;
2090
+ const toolRiskProfile = typeof resolvedRiskProfile === "function" ? resolvedRiskProfile(args) : resolvedRiskProfile;
2091
+ options?.toolBoundaryObserver?.({
2092
+ name: tc.name,
2093
+ reason: "dispatched",
2094
+ globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
2095
+ invoked: true,
2096
+ sideEffect: success && toolRiskProfile?.mutates !== "none",
2097
+ });
2113
2098
  toolResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
2114
2099
  (0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
2115
2100
  callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, toolResult, success), success);
@@ -93,6 +93,7 @@ const background_operations_1 = require("../background-operations");
93
93
  const mail_import_discovery_1 = require("../mail-import-discovery");
94
94
  const session_activity_1 = require("../session-activity");
95
95
  const target_resolution_1 = require("../target-resolution");
96
+ const sanctuary_scheduler_origin_1 = require("./sanctuary-scheduler-origin");
96
97
  const cli_parse_1 = require("./cli-parse");
97
98
  const cli_parse_2 = require("./cli-parse");
98
99
  const cli_help_1 = require("./cli-help");
@@ -1488,6 +1489,8 @@ function toDaemonCommand(command) {
1488
1489
  noSend: true,
1489
1490
  };
1490
1491
  }
1492
+ if (command.kind === "habit.poke")
1493
+ return (0, sanctuary_scheduler_origin_1.createSanctuarySchedulerFireCommand)(command) ?? command;
1491
1494
  return command;
1492
1495
  }
1493
1496
  function habitProbeJsonResponse(response) {
@@ -2217,7 +2220,7 @@ const DEFAULT_RUNTIME_APPLY_TIMEOUT_MS = 15_000;
2217
2220
  const DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS = 500;
2218
2221
  const DEFAULT_DAEMON_STATUS_TIMEOUT_MS = 4_000;
2219
2222
  const DEFAULT_AGENT_RESTART_TIMEOUT_MS = 8_000;
2220
- const CONNECT_PROVIDER_CHOICES = ["openai-codex", "anthropic", "minimax", "azure", "github-copilot"];
2223
+ const CONNECT_PROVIDER_CHOICES = ["openai-compatible", "openai-compatible-gemini", "openai-codex", "anthropic", "minimax", "azure", "github-copilot"];
2221
2224
  function hasRuntimeConfigValue(config, key) {
2222
2225
  return readRuntimeConfigString(config, key) !== null;
2223
2226
  }
@@ -2813,6 +2816,7 @@ function enableAgentSense(agent, sense, deps) {
2813
2816
  mail: senses.mail ?? { enabled: false },
2814
2817
  voice: senses.voice ?? { enabled: false },
2815
2818
  a2a: senses.a2a ?? { enabled: false },
2819
+ telegram: senses.telegram ?? { enabled: false },
2816
2820
  [sense]: { ...existing, enabled: true },
2817
2821
  };
2818
2822
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
@@ -4997,6 +5001,71 @@ async function executeConnectA2A(agent, deps) {
4997
5001
  deps.writeStdout(message);
4998
5002
  return message;
4999
5003
  }
5004
+ function parseCanonicalTelegramId(value, label) {
5005
+ const trimmed = value.trim();
5006
+ if (!/^[1-9][0-9]*$/u.test(trimmed)) {
5007
+ throw new Error(`${label} must be a canonical positive decimal string`);
5008
+ }
5009
+ return trimmed;
5010
+ }
5011
+ async function executeConnectTelegram(agent, deps) {
5012
+ if (agent === "SerpentGuide") {
5013
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect Telegram on the hatchling agent instead.");
5014
+ }
5015
+ const promptInput = requirePromptInput(deps, "Telegram setup");
5016
+ const promptSecret = requirePromptSecret(deps, "Telegram bot token entry");
5017
+ const botToken = (await promptSecret("Telegram bot token: ")).trim();
5018
+ if (!/^[0-9]+:[A-Za-z0-9_-]+$/u.test(botToken))
5019
+ throw new Error("Telegram bot token has an invalid format");
5020
+ const authorizedUserId = parseCanonicalTelegramId(await promptInput("Authorized Telegram user ID: "), "Telegram authorized user ID");
5021
+ const authorizedChatId = parseCanonicalTelegramId(await promptInput("Authorized private Telegram chat ID: "), "Telegram authorized chat ID");
5022
+ const progress = createHumanCommandProgress(deps, "connect telegram");
5023
+ let stored;
5024
+ try {
5025
+ stored = await runCommandProgressPhase(progress, "saving Telegram setup", async () => {
5026
+ progress.updateDetail("checking existing runtime config");
5027
+ const current = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
5028
+ if (!current.ok && current.reason !== "missing") {
5029
+ throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
5030
+ }
5031
+ progress.updateDetail("storing private Telegram coordinates in the agent vault");
5032
+ return (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(agent, {
5033
+ ...(current.ok ? current.config : {}),
5034
+ telegramBotToken: botToken,
5035
+ telegramAuthorizedUserId: authorizedUserId,
5036
+ telegramAuthorizedChatId: authorizedChatId,
5037
+ }, providerCliNow(deps));
5038
+ }, () => "private Telegram coordinates stored");
5039
+ progress.updateDetail("enabling Telegram in agent.json");
5040
+ enableAgentSense(agent, "telegram", deps);
5041
+ }
5042
+ finally {
5043
+ progress.end();
5044
+ }
5045
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
5046
+ return writeCapabilityOutcome(deps, {
5047
+ subtitle: `${agent}'s private Telegram sense is configured.`,
5048
+ summary: `Telegram is ready for ${agent}.`,
5049
+ whatChanged: [
5050
+ "Capability: private Telegram sense",
5051
+ `Stored: ${stored.itemPath}`,
5052
+ "agent.json: senses.telegram.enabled = true",
5053
+ "token and numeric IDs were not printed",
5054
+ ...(syncSummary ? [syncSummary] : []),
5055
+ ],
5056
+ nextMoves: ["Run ouro up so the daemon starts the Telegram long poll."],
5057
+ fallbackLines: [
5058
+ `Telegram connected for ${agent}`,
5059
+ "capability: private Telegram sense",
5060
+ `stored: ${stored.itemPath}`,
5061
+ "agent.json: senses.telegram.enabled = true",
5062
+ "token and numeric IDs were not printed",
5063
+ "",
5064
+ "Next: run `ouro up` so the daemon starts the Telegram long poll.",
5065
+ ...(syncSummary ? [syncSummary] : []),
5066
+ ],
5067
+ });
5068
+ }
5000
5069
  async function executeConnectWorkbench(agent, deps) {
5001
5070
  const { workbenchMcpCommand, workbenchHasStaleBundleEntry } = readConnectBaySenseFlags(agent, deps);
5002
5071
  const changed = removeWorkbenchMcpRegistration(agent, deps);
@@ -5045,6 +5114,8 @@ async function executeConnect(command, deps) {
5045
5114
  return executeConnectVoice(command.agent, deps);
5046
5115
  if (command.target === "a2a")
5047
5116
  return executeConnectA2A(command.agent, deps);
5117
+ if (command.target === "telegram")
5118
+ return executeConnectTelegram(command.agent, deps);
5048
5119
  if (command.target === "workbench")
5049
5120
  return executeConnectWorkbench(command.agent, deps);
5050
5121
  const progress = createHumanCommandProgress(deps, "connect");
@@ -5417,6 +5488,7 @@ async function executeAuthRun(command, deps) {
5417
5488
  agentName: command.agent,
5418
5489
  provider,
5419
5490
  promptInput: deps.promptInput,
5491
+ promptSecret: deps.promptSecret,
5420
5492
  onProgress: (message) => progress.updateDetail(message),
5421
5493
  });
5422
5494
  }
@@ -6406,7 +6478,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6406
6478
  }
6407
6479
  if (command.kind === "mail.sync-cache") {
6408
6480
  const { runHostedMailCacheSync } = await Promise.resolve().then(() => __importStar(require("../../mailroom/cache-sync-cli")));
6409
- const text = await runHostedMailCacheSync(command.agent);
6481
+ const text = await runHostedMailCacheSync(command.agent, deps.writeStdout);
6410
6482
  deps.writeStdout(text);
6411
6483
  return text;
6412
6484
  }
@@ -445,6 +445,11 @@ const SUBCOMMAND_HELP = {
445
445
  usage: "ouro connect a2a [--agent <name>]",
446
446
  example: "ouro connect a2a --agent <agent>",
447
447
  },
448
+ "connect telegram": {
449
+ description: "Connect a private Telegram bot and enable the Telegram sense",
450
+ usage: "ouro connect telegram [--agent <name>]",
451
+ example: "ouro connect telegram --agent <agent>",
452
+ },
448
453
  "connect workbench": {
449
454
  description: "Verify native Ouro Workbench runtime injection and clean stale bundle entries",
450
455
  usage: "ouro connect workbench [--agent <name>]",
@@ -541,7 +546,7 @@ const SUBCOMMAND_HELP = {
541
546
  example: "ouro mail backfill-indexes --agent <agent>",
542
547
  },
543
548
  "mail sync-cache": {
544
- description: "Converge the local decrypted search cache against complete hosted Mailroom authority in the foreground.",
549
+ description: "Converge the local decrypted search cache against stable hosted Mailroom authority in up to three passes, using 20 bounded body-read workers, foreground settlement progress, a 30-second heartbeat, and durable missing-key receipts.",
545
550
  usage: "ouro mail sync-cache [--agent <name>]",
546
551
  example: "ouro mail sync-cache --agent <agent>",
547
552
  },
@@ -86,7 +86,7 @@ function extractLaneFlag(args) {
86
86
  return { lane: value, rest };
87
87
  }
88
88
  function isAgentProvider(value) {
89
- return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
89
+ return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot" || value === "openai-compatible" || value === "openai-compatible-gemini";
90
90
  }
91
91
  function usage() {
92
92
  return [
@@ -103,7 +103,7 @@ function usage() {
103
103
  " ouro -v|--version",
104
104
  " ouro auth [--agent <name>] [--provider <provider>]",
105
105
  " ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
106
- " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
106
+ " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
107
107
  " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
108
108
  " ouro mail backfill-indexes [--agent <name>] [--foreground]",
109
109
  " ouro auth verify [--agent <name>] [--provider <provider>]",
@@ -1035,9 +1035,11 @@ function normalizeConnectTarget(value) {
1035
1035
  return "voice";
1036
1036
  if (value === "a2a" || value === "agent2agent" || value === "agent-to-agent")
1037
1037
  return "a2a";
1038
+ if (value === "telegram")
1039
+ return "telegram";
1038
1040
  if (value === "workbench" || value === "ouro-workbench" || value === "terminal-workbench")
1039
1041
  return "workbench";
1040
- throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>]");
1042
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>]");
1041
1043
  }
1042
1044
  function extractMailSourceFlags(args, usageText) {
1043
1045
  const rest = [];
@@ -1090,7 +1092,7 @@ function extractMailSourceFlags(args, usageText) {
1090
1092
  };
1091
1093
  }
1092
1094
  function parseConnectCommand(args) {
1093
- const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
1095
+ const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
1094
1096
  const { agent, rest: afterAgent } = extractAgentFlag(args);
1095
1097
  const mailFlags = extractMailSourceFlags(afterAgent, usageText);
1096
1098
  if (mailFlags.rest.length > 1)