@ouro.bot/cli 0.1.0-alpha.733 → 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 (89) hide show
  1. package/changelog.json +25 -0
  2. package/deploy/unraid/Dockerfile +27 -0
  3. package/deploy/unraid/README.txt +1368 -0
  4. package/deploy/unraid/audit-container-spec.sh +4 -0
  5. package/deploy/unraid/container-runtime.json +1 -0
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
  8. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  9. package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
  10. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
  12. package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
  13. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  14. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  15. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  16. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  17. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  18. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
  19. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
  20. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
  21. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
  22. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
  23. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
  24. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
  25. package/deploy/unraid/sanctuary.xml +25 -0
  26. package/dist/heart/approval-files.js +113 -0
  27. package/dist/heart/approval-store.js +168 -9
  28. package/dist/heart/auth/auth-flow.js +17 -1
  29. package/dist/heart/autonomy-budget.js +44 -1
  30. package/dist/heart/awaiting/await-scheduler.js +6 -2
  31. package/dist/heart/config.js +8 -0
  32. package/dist/heart/core.js +117 -132
  33. package/dist/heart/daemon/cli-exec.js +73 -1
  34. package/dist/heart/daemon/cli-help.js +5 -0
  35. package/dist/heart/daemon/cli-parse.js +6 -4
  36. package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
  37. package/dist/heart/daemon/container-healthcheck.js +94 -0
  38. package/dist/heart/daemon/container-runtime.js +84 -0
  39. package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
  40. package/dist/heart/daemon/container-spec-auditor.js +156 -0
  41. package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
  42. package/dist/heart/daemon/daemon-entry.js +116 -28
  43. package/dist/heart/daemon/daemon-health.js +10 -0
  44. package/dist/heart/daemon/daemon.js +92 -5
  45. package/dist/heart/daemon/runtime-logging.js +6 -0
  46. package/dist/heart/daemon/runtime-mode.js +13 -2
  47. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
  48. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
  49. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  50. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
  51. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  52. package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
  53. package/dist/heart/daemon/sense-manager.js +25 -6
  54. package/dist/heart/daemon/supercronic-supervisor.js +267 -0
  55. package/dist/heart/habits/habit-scheduler.js +7 -3
  56. package/dist/heart/identity.js +13 -3
  57. package/dist/heart/provider-credentials.js +9 -1
  58. package/dist/heart/provider-models.js +8 -0
  59. package/dist/heart/provider-ping.js +5 -2
  60. package/dist/heart/providers/openai-compatible.js +177 -0
  61. package/dist/heart/runtime-credentials.js +217 -8
  62. package/dist/heart/sense-truth.js +2 -0
  63. package/dist/heart/tool-approval.js +1 -0
  64. package/dist/heart/turn-context.js +8 -0
  65. package/dist/mind/prompt.js +9 -0
  66. package/dist/nerves/coverage/source-scanner.js +4 -4
  67. package/dist/nerves/index.js +75 -9
  68. package/dist/nerves/runtime.js +12 -0
  69. package/dist/repertoire/tool-arguments.js +39 -0
  70. package/dist/repertoire/tools-unraid.js +353 -0
  71. package/dist/repertoire/tools.js +19 -1
  72. package/dist/repertoire/unraid-client.js +155 -0
  73. package/dist/repertoire/unraid-restart.js +130 -0
  74. package/dist/senses/private-runtime-worker.js +3 -2
  75. package/dist/senses/sanctuary-grounding.js +118 -0
  76. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  77. package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
  78. package/dist/senses/sanctuary-health-runner.js +146 -0
  79. package/dist/senses/sanctuary-health.js +447 -0
  80. package/dist/senses/sanctuary-interactive-control.js +306 -0
  81. package/dist/senses/sanctuary-runtime.js +206 -0
  82. package/dist/senses/shared-turn.js +11 -3
  83. package/dist/senses/telegram-approval-runtime.js +526 -0
  84. package/dist/senses/telegram-audit-ledger.js +220 -0
  85. package/dist/senses/telegram-client.js +1471 -0
  86. package/dist/senses/telegram-entry.js +80 -0
  87. package/dist/senses/telegram.js +1232 -0
  88. package/npm-shrinkwrap.json +6558 -0
  89. package/package.json +4 -2
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.failFastContainerCredentialBootstrapStartup = failFastContainerCredentialBootstrapStartup;
4
+ exports.startDaemonAfterContainerCredentialBootstrap = startDaemonAfterContainerCredentialBootstrap;
5
+ const runtime_1 = require("../../nerves/runtime");
6
+ const daemon_tombstone_1 = require("./daemon-tombstone");
7
+ const REDACTED_BOOTSTRAP_STARTUP_ERROR = "container credential bootstrap rejected; recoverable claim retained for reconciliation";
8
+ function failFastContainerCredentialBootstrapStartup(input) {
9
+ const error = new Error(REDACTED_BOOTSTRAP_STARTUP_ERROR);
10
+ try {
11
+ (0, daemon_tombstone_1.writeDaemonTombstone)("startupFailure", error);
12
+ }
13
+ catch {
14
+ // Exit remains mandatory even if best-effort tombstone reporting fails.
15
+ }
16
+ try {
17
+ (0, runtime_1.emitNervesEvent)({
18
+ level: "error",
19
+ component: "daemon",
20
+ event: "daemon.entry_error",
21
+ message: "daemon entrypoint failed before server startup",
22
+ meta: { error: REDACTED_BOOTSTRAP_STARTUP_ERROR },
23
+ });
24
+ }
25
+ catch {
26
+ // Exit remains mandatory even if best-effort event reporting fails.
27
+ }
28
+ input.exit(1);
29
+ }
30
+ async function startDaemonAfterContainerCredentialBootstrap(input) {
31
+ try {
32
+ await input.loadBootstrap();
33
+ }
34
+ catch {
35
+ input.markStartupFailure();
36
+ failFastContainerCredentialBootstrapStartup({ exit: input.exit });
37
+ return false;
38
+ }
39
+ await input.startDaemon();
40
+ return true;
41
+ }
@@ -57,6 +57,8 @@ const await_scheduler_1 = require("../awaiting/await-scheduler");
57
57
  const await_expiry_1 = require("../awaiting/await-expiry");
58
58
  const os_cron_deps_1 = require("./os-cron-deps");
59
59
  const os_cron_1 = require("./os-cron");
60
+ const container_runtime_1 = require("./container-runtime");
61
+ const supercronic_supervisor_1 = require("./supercronic-supervisor");
60
62
  const daemon_tombstone_1 = require("./daemon-tombstone");
61
63
  const agent_config_check_1 = require("./agent-config-check");
62
64
  const pulse_1 = require("./pulse");
@@ -70,7 +72,14 @@ const context_loss_sentinel_1 = require("../context-loss-sentinel");
70
72
  const provider_credentials_1 = require("../provider-credentials");
71
73
  const runtime_credentials_1 = require("../runtime-credentials");
72
74
  const machine_identity_1 = require("../machine-identity");
75
+ const container_credential_bootstrap_1 = require("./container-credential-bootstrap");
76
+ const daemon_bootstrap_startup_1 = require("./daemon-bootstrap-startup");
73
77
  const auth_flow_1 = require("../auth/auth-flow");
78
+ const sanctuary_health_runner_1 = require("../../senses/sanctuary-health-runner");
79
+ const sanctuary_acceptance_marker_1 = require("./sanctuary-acceptance-marker");
80
+ const sanctuary_scheduler_liveness_1 = require("./sanctuary-scheduler-liveness");
81
+ const sanctuary_scheduler_origin_1 = require("./sanctuary-scheduler-origin");
82
+ const telegram_1 = require("../../senses/telegram");
74
83
  function parseSocketPath(argv) {
75
84
  const socketIndex = argv.indexOf("--socket");
76
85
  if (socketIndex >= 0) {
@@ -244,6 +253,17 @@ const taskScheduler = new task_scheduler_1.TaskDrivenScheduler({
244
253
  });
245
254
  const habitSchedulers = [];
246
255
  const awaitSchedulers = [];
256
+ const containerRuntimePolicy = (0, container_runtime_1.readContainerRuntimePolicy)();
257
+ const supercronicSupervisor = containerRuntimePolicy?.scheduler === "supercronic"
258
+ ? new supercronic_supervisor_1.SupercronicSupervisor({
259
+ binaryPath: "/usr/local/bin/supercronic",
260
+ crontabPath: path.join((0, identity_1.getAgentBundlesRoot)(), "..", ".ouro-cli", "scheduler", "sanctuary.crontab"),
261
+ onFatal: (error) => {
262
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.supercronic_state", message: "Supercronic restart budget exhausted", meta: { error: error.message } });
263
+ process.exit(1);
264
+ },
265
+ })
266
+ : null;
247
267
  function habitCronLabelOwner(agent) {
248
268
  const agentPrefix = `bot.ouro.${agent}.`;
249
269
  const awaitPrefix = `${agentPrefix}await.`;
@@ -334,21 +354,25 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
334
354
  catch { /* recovery is best-effort */ }
335
355
  },
336
356
  });
337
- let entryRuntimeStopping = false;
357
+ let entryRuntimeStopPromise = null;
338
358
  let stopCommandExitScheduled = false;
339
359
  function stopEntryRuntime() {
340
- if (entryRuntimeStopping)
341
- return;
342
- entryRuntimeStopping = true;
343
- for (const s of habitSchedulers) {
344
- s.stopWatching();
345
- s.stop();
346
- }
347
- for (const s of awaitSchedulers) {
348
- s.stopWatching();
349
- s.stop();
350
- }
351
- healthMonitor.stopPeriodicChecks();
360
+ if (entryRuntimeStopPromise)
361
+ return entryRuntimeStopPromise;
362
+ entryRuntimeStopPromise = (async () => {
363
+ stopHealthHeartbeat();
364
+ for (const s of habitSchedulers) {
365
+ s.stopWatching();
366
+ s.stop();
367
+ }
368
+ for (const s of awaitSchedulers) {
369
+ s.stopWatching();
370
+ s.stop();
371
+ }
372
+ await supercronicSupervisor?.stop();
373
+ healthMonitor.stopPeriodicChecks();
374
+ })();
375
+ return entryRuntimeStopPromise;
352
376
  }
353
377
  function scheduleCleanProcessExitAfterStopCommand() {
354
378
  if (stopCommandExitScheduled)
@@ -367,9 +391,56 @@ const daemon = new daemon_1.OuroDaemon({
367
391
  scheduler,
368
392
  healthMonitor,
369
393
  router,
394
+ schedulerFireVerifier: (command) => {
395
+ if (!supercronicSupervisor)
396
+ throw new Error("Supercronic scheduler is unavailable");
397
+ const marker = (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)("sanctuary");
398
+ return (0, sanctuary_scheduler_origin_1.verifySanctuarySchedulerFireCommand)(command, {
399
+ childPid: supercronicSupervisor.authenticatedSnapshot("habit:sanctuary").childPid,
400
+ identityKey: (0, telegram_1.readOrCreateTelegramIdentityKey)(path.join((0, identity_1.getAgentBundlesRoot)(), "sanctuary.ouro")),
401
+ scenarioHandleDigest: marker?.label === "unit-16f-cron-fingerprint" ? marker.scenarioHandleDigest : null, now: () => new Date(),
402
+ readFile: (target) => fs.readFileSync(target, "utf8"), readLink: (target) => fs.readlinkSync(target),
403
+ });
404
+ },
405
+ schedulerFireConsumer: (origin) => (0, sanctuary_scheduler_liveness_1.consumeSanctuarySchedulerFire)(path.join((0, identity_1.getAgentBundlesRoot)(), "sanctuary.ouro"), origin),
406
+ nativeHabitRunner: async ({ agent, habitName, trigger, occurrenceId, runnerId, schedulerOrigin }) => {
407
+ if (agent !== "sanctuary" || habitName !== "sanctuary-health")
408
+ return null;
409
+ const marker = (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)(agent);
410
+ const schedulerScenario = marker?.label === "unit-16f-cron-fingerprint" ? marker : null;
411
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agent}.ouro`);
412
+ const before = schedulerScenario ? (0, sanctuary_scheduler_liveness_1.readSanctuaryHealthCursor)(agentRoot) : null;
413
+ let providerInvocationCount = 0;
414
+ let privateTurnCount = 0;
415
+ const result = await (0, sanctuary_health_runner_1.runSanctuaryHealthHabit)(agent, schedulerScenario ? {
416
+ acceptanceMetrics: {
417
+ onPrivateTurnStart: () => { privateTurnCount += 1; },
418
+ onProviderInvocation: () => { providerInvocationCount += 1; },
419
+ },
420
+ } : {});
421
+ if (schedulerScenario) {
422
+ if (!supercronicSupervisor || !before || !occurrenceId || !schedulerOrigin)
423
+ throw new Error("Sanctuary scheduler liveness supervisor provenance is unavailable");
424
+ (0, sanctuary_scheduler_liveness_1.recordSanctuarySchedulerLivenessReceipt)({
425
+ agentRoot,
426
+ trigger,
427
+ occurrenceId,
428
+ runnerId,
429
+ scenario: schedulerScenario,
430
+ supervisor: supercronicSupervisor.authenticatedSnapshot("habit:sanctuary"),
431
+ before,
432
+ providerInvocationCount,
433
+ privateTurnCount,
434
+ schedulerOrigin,
435
+ identityKey: (0, telegram_1.readOrCreateTelegramIdentityKey)(agentRoot),
436
+ });
437
+ }
438
+ return result;
439
+ },
440
+ nativeHabitMatch: (agent, habitName) => agent === "sanctuary" && habitName === "sanctuary-health",
370
441
  mode,
371
- onStopCommandComplete: () => {
372
- stopEntryRuntime();
442
+ onStopCommandComplete: async () => {
443
+ await stopEntryRuntime();
373
444
  scheduleCleanProcessExitAfterStopCommand();
374
445
  },
375
446
  });
@@ -495,6 +566,7 @@ function emitAwaitSetupError(agent, error) {
495
566
  const healthWriter = new daemon_health_1.DaemonHealthWriter((0, daemon_health_1.getDefaultHealthPath)());
496
567
  const healthSink = (0, daemon_health_1.createHealthNervesSink)(healthWriter, buildDaemonHealthState);
497
568
  (0, index_1.registerGlobalLogSink)(healthSink);
569
+ const stopHealthHeartbeat = (0, daemon_health_1.startDaemonHealthHeartbeat)(healthWriter, buildDaemonHealthState);
498
570
  /* v8 ignore stop */
499
571
  function writeStopCommandHealthState() {
500
572
  try {
@@ -611,10 +683,20 @@ function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
611
683
  /* v8 ignore stop */
612
684
  }
613
685
  /* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
614
- void daemon.start().then(async () => {
686
+ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap)({
687
+ loadBootstrap: () => (0, container_credential_bootstrap_1.loadContainerCredentialBootstrap)(managedAgents),
688
+ startDaemon: () => daemon.start(),
689
+ markStartupFailure: () => { _tombstoneWritten = true; },
690
+ exit: (code) => process.exit(code),
691
+ }).then(async (started) => {
692
+ if (!started)
693
+ return;
694
+ supercronicSupervisor?.start();
615
695
  const providerPreload = startProviderCredentialPoolPreload();
616
696
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
617
- const ouroPath = (0, os_cron_deps_1.resolveOuroBinaryPath)();
697
+ const ouroPath = supercronicSupervisor
698
+ ? "/usr/local/bin/node /opt/ouro/dist/heart/daemon/ouro-entry.js"
699
+ : (0, os_cron_deps_1.resolveOuroBinaryPath)();
618
700
  const osCronDeps = (0, os_cron_deps_1.createRealOsCronDeps)();
619
701
  for (const agent of managedAgents) {
620
702
  scheduleStartupSentinelAfterProviderPreload(agent, providerPreload);
@@ -624,13 +706,15 @@ void daemon.start().then(async () => {
624
706
  try {
625
707
  // Migrate old tasks/habits/ to habits/ at bundle root
626
708
  (0, habit_migration_1.migrateHabitsFromTaskSystem)(bundleRoot);
627
- const osCronManager = new os_cron_1.LaunchdCronManager(osCronDeps, { ownsLabel: habitCronLabelOwner(agent) });
709
+ const osCronManager = supercronicSupervisor
710
+ ? supercronicSupervisor.namespace(`habit:${agent}`)
711
+ : new os_cron_1.LaunchdCronManager(osCronDeps, { ownsLabel: habitCronLabelOwner(agent) });
628
712
  const scheduler = new habit_scheduler_1.HabitScheduler({
629
713
  agent,
630
714
  habitsDir,
631
715
  osCronManager,
632
716
  onHabitFire: (habitName, trigger, context) => {
633
- if ((0, habit_policy_1.isRsvpHabitName)(habitName)) {
717
+ if ((0, habit_policy_1.isRsvpHabitName)(habitName) || (agent === "sanctuary" && habitName === "sanctuary-health")) {
634
718
  (0, socket_client_1.sendDaemonCommand)(socketPath, {
635
719
  kind: "habit.poke",
636
720
  agent,
@@ -676,7 +760,9 @@ void daemon.start().then(async () => {
676
760
  ouroPath,
677
761
  watch: (dir, cb) => fs.watch(dir, cb),
678
762
  },
679
- execForVerify: verifyOsCron,
763
+ execForVerify: supercronicSupervisor ? () => supercronicSupervisor.verificationOutput() : verifyOsCron,
764
+ verifyJobs: supercronicSupervisor ? (jobs) => supercronicSupervisor.verifyNamespace(`habit:${agent}`, jobs) : undefined,
765
+ platform: supercronicSupervisor ? "linux" : undefined,
680
766
  });
681
767
  try {
682
768
  scheduler.start();
@@ -719,7 +805,9 @@ void daemon.start().then(async () => {
719
805
  const awaitsDir = path.join(bundleRoot, "awaiting");
720
806
  const awaitDegradedComponent = `awaits:${agent}`;
721
807
  try {
722
- const awaitOsCronManager = new os_cron_1.LaunchdCronManager(osCronDeps, { ownsLabel: awaitCronLabelOwner(agent) });
808
+ const awaitOsCronManager = supercronicSupervisor
809
+ ? supercronicSupervisor.namespace(`await:${agent}`)
810
+ : new os_cron_1.LaunchdCronManager(osCronDeps, { ownsLabel: awaitCronLabelOwner(agent) });
723
811
  const awaitScheduler = new await_scheduler_1.AwaitScheduler({
724
812
  agent,
725
813
  awaitsDir,
@@ -782,7 +870,9 @@ void daemon.start().then(async () => {
782
870
  ouroPath,
783
871
  watch: (dir, cb) => fs.watch(dir, cb),
784
872
  },
785
- execForVerify: verifyOsCron,
873
+ execForVerify: supercronicSupervisor ? () => supercronicSupervisor.verificationOutput() : verifyOsCron,
874
+ verifyJobs: supercronicSupervisor ? (jobs) => supercronicSupervisor.verifyNamespace(`await:${agent}`, jobs) : undefined,
875
+ platform: supercronicSupervisor ? "linux" : undefined,
786
876
  });
787
877
  try {
788
878
  awaitScheduler.start();
@@ -853,10 +943,9 @@ process.on("SIGINT", () => {
853
943
  // tombstone is strictly better than silence.
854
944
  _tombstoneWritten = true;
855
945
  (0, daemon_tombstone_1.writeDaemonTombstone)("sigint", new Error("daemon received SIGINT"));
856
- stopEntryRuntime();
857
- const forcedExit = setTimeout(() => process.exit(1), 5_000);
946
+ const forcedExit = setTimeout(() => process.exit(1), 12_000);
858
947
  forcedExit.unref();
859
- void daemon.stop().then(() => {
948
+ void stopEntryRuntime().then(() => daemon.stop()).then(() => {
860
949
  clearTimeout(forcedExit);
861
950
  process.exit(0);
862
951
  }, () => {
@@ -867,10 +956,9 @@ process.on("SIGINT", () => {
867
956
  process.on("SIGTERM", () => {
868
957
  _tombstoneWritten = true;
869
958
  (0, daemon_tombstone_1.writeDaemonTombstone)("sigterm", new Error("daemon received SIGTERM"));
870
- stopEntryRuntime();
871
- const forcedExit = setTimeout(() => process.exit(1), 5_000);
959
+ const forcedExit = setTimeout(() => process.exit(1), 12_000);
872
960
  forcedExit.unref();
873
- void daemon.stop().then(() => {
961
+ void stopEntryRuntime().then(() => daemon.stop()).then(() => {
874
962
  clearTimeout(forcedExit);
875
963
  process.exit(0);
876
964
  }, () => {
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.HEALTH_TRACKED_EVENTS = exports.DaemonHealthWriter = void 0;
37
37
  exports.isRollupStatus = isRollupStatus;
38
38
  exports.isDaemonStatus = isDaemonStatus;
39
+ exports.startDaemonHealthHeartbeat = startDaemonHealthHeartbeat;
39
40
  exports.getDefaultHealthPath = getDefaultHealthPath;
40
41
  exports.createHealthNervesSink = createHealthNervesSink;
41
42
  exports.readHealth = readHealth;
@@ -100,6 +101,15 @@ class DaemonHealthWriter {
100
101
  }
101
102
  }
102
103
  exports.DaemonHealthWriter = DaemonHealthWriter;
104
+ function startDaemonHealthHeartbeat(writer, getState, intervalMs = 30_000) {
105
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0)
106
+ throw new Error("health heartbeat interval must be positive");
107
+ const write = () => writer.writeHealth(getState());
108
+ write();
109
+ const timer = setInterval(write, intervalMs);
110
+ timer.unref?.();
111
+ return () => clearInterval(timer);
112
+ }
103
113
  function getDefaultHealthPath() {
104
114
  return path.join(os.homedir(), ".ouro-cli", "daemon-health.json");
105
115
  }
@@ -49,6 +49,7 @@ const fs = __importStar(require("fs"));
49
49
  const net = __importStar(require("net"));
50
50
  const os = __importStar(require("os"));
51
51
  const path = __importStar(require("path"));
52
+ const node_crypto_1 = require("node:crypto");
52
53
  const identity_1 = require("../identity");
53
54
  const agent_discovery_1 = require("./agent-discovery");
54
55
  const runtime_1 = require("../../nerves/runtime");
@@ -78,6 +79,8 @@ const habit_parser_1 = require("../habits/habit-parser");
78
79
  const habit_runtime_state_1 = require("../habits/habit-runtime-state");
79
80
  const router_1 = require("../external-events/router");
80
81
  const habit_policy_1 = require("../../rsvp/habit-policy");
82
+ const container_runtime_1 = require("./container-runtime");
83
+ const habit_session_1 = require("../habits/habit-session");
81
84
  const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
82
85
  /**
83
86
  * Defense-in-depth: detect if we're running under vitest. The pidfile lives
@@ -127,6 +130,7 @@ function parseOrphanPidsFromPs(psOutput, selfPid) {
127
130
  && !line.includes("mail-entry.js")
128
131
  && !line.includes("teams-entry.js")
129
132
  && !line.includes("a2a-entry.js")
133
+ && !line.includes("telegram-entry.js")
130
134
  && !line.includes("voice-entry.js"))
131
135
  continue;
132
136
  // Parse `<pid> <ppid> <command...>`. ps pads these with leading spaces.
@@ -616,6 +620,12 @@ class OuroDaemon {
616
620
  onStopCommandComplete;
617
621
  externalEventRoot;
618
622
  rsvpHabitRunner;
623
+ nativeHabitRunner;
624
+ nativeHabitMatch;
625
+ nativeHabitClaims = new Set();
626
+ authenticatedSchedulerPokes = new WeakMap();
627
+ schedulerFireVerifier;
628
+ schedulerFireConsumer;
619
629
  orphanStartupDrain;
620
630
  constructor(options) {
621
631
  this.socketPath = options.socketPath;
@@ -631,6 +641,10 @@ class OuroDaemon {
631
641
  this.onStopCommandComplete = options.onStopCommandComplete ?? null;
632
642
  this.externalEventRoot = options.externalEventRoot ?? null;
633
643
  this.rsvpHabitRunner = options.rsvpHabitRunner;
644
+ this.nativeHabitRunner = options.nativeHabitRunner;
645
+ this.nativeHabitMatch = options.nativeHabitMatch;
646
+ this.schedulerFireVerifier = options.schedulerFireVerifier;
647
+ this.schedulerFireConsumer = options.schedulerFireConsumer;
634
648
  this.orphanStartupDrain = options.orphanStartupDrain ?? drainOrphanProcessesBeforeStartup;
635
649
  }
636
650
  /* v8 ignore start -- default mailbox server wiring: production-only path, tests inject mailboxServerFactory stub instead. startMailboxHttpServer itself has full coverage in mailbox-http.test.ts @preserve */
@@ -752,19 +766,21 @@ class OuroDaemon {
752
766
  }
753
767
  }
754
768
  async startInner() {
769
+ const containerPolicy = (0, container_runtime_1.readContainerRuntimePolicy)();
755
770
  // Register update hooks and apply pending updates before starting agents
756
771
  (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
757
772
  (0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
758
773
  const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
759
- await (0, update_hooks_1.applyPendingUpdates)(this.bundlesRoot, currentVersion);
774
+ if (containerPolicy?.updates !== "disabled")
775
+ await (0, update_hooks_1.applyPendingUpdates)(this.bundlesRoot, currentVersion);
760
776
  // Start periodic update checker (polls npm registry every 30 minutes)
761
777
  // Skip in dev mode — dev builds should not auto-update from npm
762
- if (this.mode === "dev") {
778
+ if (this.mode === "dev" || containerPolicy?.updates === "disabled") {
763
779
  (0, runtime_1.emitNervesEvent)({
764
780
  component: "daemon",
765
781
  event: "daemon.update_checker_skip",
766
782
  message: "skipping update checker in dev mode",
767
- meta: { reason: "dev mode" },
783
+ meta: { reason: this.mode === "dev" ? "dev mode" : "container policy" },
768
784
  });
769
785
  }
770
786
  else {
@@ -1538,7 +1554,7 @@ class OuroDaemon {
1538
1554
  await this.stop();
1539
1555
  }
1540
1556
  finally {
1541
- this.onStopCommandComplete?.();
1557
+ await this.onStopCommandComplete?.();
1542
1558
  }
1543
1559
  return { ok: true, message: "daemon stopped" };
1544
1560
  case "daemon.restart": {
@@ -1559,7 +1575,7 @@ class OuroDaemon {
1559
1575
  await this.stop();
1560
1576
  }
1561
1577
  finally {
1562
- this.onStopCommandComplete?.();
1578
+ await this.onStopCommandComplete?.();
1563
1579
  }
1564
1580
  return {
1565
1581
  ok: true,
@@ -1872,6 +1888,26 @@ class OuroDaemon {
1872
1888
  data: receipt,
1873
1889
  };
1874
1890
  }
1891
+ case "habit.scheduler-fire": {
1892
+ if (!this.schedulerFireVerifier || !this.schedulerFireConsumer)
1893
+ return { ok: false, error: "authenticated scheduler fire is unavailable" };
1894
+ let origin;
1895
+ try {
1896
+ origin = this.schedulerFireVerifier(command);
1897
+ this.schedulerFireConsumer(origin);
1898
+ }
1899
+ catch (error) {
1900
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1901
+ }
1902
+ const poke = { kind: "habit.poke", agent: command.agent, habitName: command.habitName, trigger: "cron", occurrenceId: origin.occurrenceId };
1903
+ this.authenticatedSchedulerPokes.set(poke, origin);
1904
+ try {
1905
+ return await this.handleCommand(poke);
1906
+ }
1907
+ finally {
1908
+ this.authenticatedSchedulerPokes.delete(poke);
1909
+ }
1910
+ }
1875
1911
  case "habit.poke": {
1876
1912
  if (!isValidHabitPokeCommand(command)) {
1877
1913
  return {
@@ -1883,6 +1919,10 @@ class OuroDaemon {
1883
1919
  if (!(0, flight_recorder_1.isHabitRunTrigger)(trigger)) {
1884
1920
  return { ok: false, error: `invalid habit trigger: ${String(trigger)}` };
1885
1921
  }
1922
+ const schedulerOrigin = this.authenticatedSchedulerPokes.get(command);
1923
+ if (command.agent === "sanctuary" && command.habitName === "sanctuary-health" && trigger === "cron" && !schedulerOrigin) {
1924
+ return { ok: false, error: "Sanctuary cron habit requires authenticated Supercronic provenance" };
1925
+ }
1886
1926
  if (!(0, habit_policy_1.isRsvpHabitName)(command.habitName) && !this.hasManagedPrivateRuntime(command.agent)) {
1887
1927
  return {
1888
1928
  ok: false,
@@ -1915,6 +1955,53 @@ class OuroDaemon {
1915
1955
  trigger,
1916
1956
  resolution,
1917
1957
  });
1958
+ if (this.nativeHabitRunner && this.nativeHabitMatch?.(command.agent, command.habitName)) {
1959
+ const occurrenceId = resolution.occurrenceId ?? `${trigger}:${command.agent}:${command.habitName}`;
1960
+ const claimKey = `${command.agent}:${command.habitName}`;
1961
+ if (this.nativeHabitClaims.has(claimKey)) {
1962
+ return { ok: true, message: `skipped overlapping native habit occurrence ${occurrenceId}` };
1963
+ }
1964
+ this.nativeHabitClaims.add(claimKey);
1965
+ const startedAt = new Date().toISOString();
1966
+ const runnerId = (0, node_crypto_1.randomUUID)();
1967
+ let nativeResult;
1968
+ try {
1969
+ nativeResult = await this.nativeHabitRunner({
1970
+ agent: command.agent,
1971
+ habitName: command.habitName,
1972
+ trigger,
1973
+ occurrenceId,
1974
+ runnerId,
1975
+ ...(schedulerOrigin ? { schedulerOrigin } : {}),
1976
+ }) ?? { ok: false, error: "native habit runner declined a matched habit" };
1977
+ }
1978
+ catch (error) {
1979
+ nativeResult = { ok: false, error: error instanceof Error ? error.message : String(error) };
1980
+ }
1981
+ const endedAt = new Date().toISOString();
1982
+ let completion;
1983
+ try {
1984
+ completion = (0, habit_session_1.completeHabitRun)({
1985
+ agentRoot: path.join(this.bundlesRoot, `${command.agent}.ouro`),
1986
+ habit: resolution.habit,
1987
+ runId: runnerId,
1988
+ trigger,
1989
+ startedAt,
1990
+ endedAt,
1991
+ operationId: occurrenceId,
1992
+ permissionEnvelope: { schemaVersion: 1, canMessageOutward: true, returnRoutes: [], deniedTools: [], warnings: [] },
1993
+ toolPolicy: { requestedTools: null, grantedTools: [], deniedTools: [], outwardMessagingAllowed: true },
1994
+ ...(nativeResult.ok ? {} : { errors: [nativeResult.error ?? nativeResult.message ?? "native habit failed"] }),
1995
+ });
1996
+ }
1997
+ finally {
1998
+ this.nativeHabitClaims.delete(claimKey);
1999
+ }
2000
+ if (!completion.receiptWritten || !completion.runtimeStateRecorded) {
2001
+ return { ok: false, error: "native habit completed but its durable receipt or cursor could not be recorded", data: nativeResult };
2002
+ }
2003
+ return nativeResult;
2004
+ }
1918
2005
  if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
1919
2006
  return this.runNativeRsvpHabit(command, trigger, resolution.occurrenceId);
1920
2007
  }
@@ -52,6 +52,12 @@ function defaultLoggingForProcess(processName) {
52
52
  sinks: ["ndjson"],
53
53
  };
54
54
  }
55
+ if (processName === "telegram") {
56
+ return {
57
+ level: "info",
58
+ sinks: ["terminal", "ndjson"],
59
+ };
60
+ }
55
61
  if (processName === "bluebubbles" || processName === "mail" || processName === "voice" || processName === "a2a") {
56
62
  return {
57
63
  level: "warn",
@@ -50,11 +50,22 @@ function detectRuntimeMode(rootPath, deps = {}) {
50
50
  });
51
51
  return "production";
52
52
  }
53
+ const isWorktree = rootPath.includes(".claude/worktrees/");
54
+ const isGitRepo = checkExists(path.join(rootPath, ".git"));
55
+ if (!isWorktree && !isGitRepo && checkExists(path.join(rootPath, "container-runtime.json"))) {
56
+ (0, runtime_1.emitNervesEvent)({
57
+ component: "daemon",
58
+ event: "daemon.runtime_mode_detected",
59
+ message: "detected runtime mode",
60
+ meta: { rootPath, mode: "production", reason: "container-runtime" },
61
+ });
62
+ return "production";
63
+ }
53
64
  // 2-4. Everything else is dev: worktrees, git repos, unknown paths
54
65
  // (conservative default: assume dev unless proven production)
55
- const reason = rootPath.includes(".claude/worktrees/")
66
+ const reason = isWorktree
56
67
  ? "worktree"
57
- : checkExists(path.join(rootPath, ".git"))
68
+ : isGitRepo
58
69
  ? "git-repo"
59
70
  : "unknown";
60
71
  (0, runtime_1.emitNervesEvent)({