@ouro.bot/cli 0.1.0-alpha.742 → 0.1.0-alpha.743

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 (71) hide show
  1. package/changelog.json +26 -0
  2. package/deploy/unraid/Dockerfile +7 -2
  3. package/deploy/unraid/README.txt +704 -64
  4. package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
  5. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  6. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  7. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  8. package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
  9. package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
  10. package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
  13. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
  14. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
  15. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
  16. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
  17. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
  18. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
  19. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  20. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
  21. package/deploy/unraid/sanctuary.xml +13 -10
  22. package/dist/arc/cares.js +171 -37
  23. package/dist/arc/obligations.js +38 -0
  24. package/dist/heart/awaiting/await-alert.js +4 -2
  25. package/dist/heart/awaiting/await-expiry.js +39 -4
  26. package/dist/heart/awaiting/await-parser.js +8 -0
  27. package/dist/heart/core.js +37 -7
  28. package/dist/heart/cross-chat-delivery.js +1 -0
  29. package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
  30. package/dist/heart/daemon/container-spec-auditor.js +38 -11
  31. package/dist/heart/daemon/daemon-entry.js +19 -5
  32. package/dist/heart/daemon/daemon.js +154 -22
  33. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +126 -42
  34. package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
  35. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
  36. package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
  37. package/dist/heart/external-events/router.js +1025 -43
  38. package/dist/heart/session-events.js +23 -5
  39. package/dist/heart/steward-policy.js +302 -0
  40. package/dist/heart/tool-approval.js +3 -1
  41. package/dist/heart/turn-context.js +1 -0
  42. package/dist/mind/context.js +8 -3
  43. package/dist/mind/pending.js +25 -0
  44. package/dist/mind/prompt.js +49 -1
  45. package/dist/repertoire/guardrails.js +2 -1
  46. package/dist/repertoire/relationship-authorization.js +194 -0
  47. package/dist/repertoire/tools-awaiting.js +229 -26
  48. package/dist/repertoire/tools-base.js +15 -0
  49. package/dist/repertoire/tools-continuity.js +259 -4
  50. package/dist/repertoire/tools-notes.js +29 -3
  51. package/dist/repertoire/tools-session.js +219 -4
  52. package/dist/repertoire/tools-steward-policy.js +88 -0
  53. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  54. package/dist/repertoire/tools-unraid.js +113 -6
  55. package/dist/repertoire/tools.js +104 -9
  56. package/dist/repertoire/unraid-restart.js +86 -4
  57. package/dist/senses/private-runtime-worker.js +36 -3
  58. package/dist/senses/private-runtime.js +310 -63
  59. package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
  60. package/dist/senses/sanctuary-health-runner.js +47 -103
  61. package/dist/senses/sanctuary-health.js +53 -105
  62. package/dist/senses/sanctuary-runtime.js +36 -2
  63. package/dist/senses/sanctuary-sab.js +80 -0
  64. package/dist/senses/shared-turn.js +64 -9
  65. package/dist/senses/telegram-admission.js +684 -0
  66. package/dist/senses/telegram-approval-runtime.js +46 -6
  67. package/dist/senses/telegram-client.js +212 -66
  68. package/dist/senses/telegram-effect-adapter.js +787 -0
  69. package/dist/senses/telegram.js +728 -48
  70. package/npm-shrinkwrap.json +6 -6
  71. package/package.json +2 -2
@@ -66,10 +66,14 @@ function emptyAwait(stem, body) {
66
66
  alert: null,
67
67
  mode: "full",
68
68
  max_age: null,
69
+ wake_at: null,
69
70
  status: "pending",
70
71
  created_at: null,
71
72
  filed_from: null,
72
73
  filed_for_friend_id: null,
74
+ filed_from_key: null,
75
+ request_id: null,
76
+ obligation_id: null,
73
77
  body,
74
78
  resolved_at: null,
75
79
  resolution_observation: null,
@@ -102,10 +106,14 @@ function parseAwaitFile(content, filePath) {
102
106
  alert: nonEmptyString(frontmatter.alert),
103
107
  mode,
104
108
  max_age: nonEmptyString(frontmatter.max_age),
109
+ wake_at: nonEmptyString(frontmatter.wake_at),
105
110
  status,
106
111
  created_at: nonEmptyString(frontmatter.created_at),
107
112
  filed_from: nonEmptyString(frontmatter.filed_from),
108
113
  filed_for_friend_id: nonEmptyString(frontmatter.filed_for_friend_id),
114
+ filed_from_key: nonEmptyString(frontmatter.filed_from_key),
115
+ request_id: nonEmptyString(frontmatter.request_id),
116
+ obligation_id: nonEmptyString(frontmatter.obligation_id),
109
117
  body,
110
118
  resolved_at: nonEmptyString(frontmatter.resolved_at),
111
119
  resolution_observation: nonEmptyString(frontmatter.resolution_observation),
@@ -962,6 +962,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
962
962
  try {
963
963
  const buildSystemOptions = {
964
964
  ...options,
965
+ relationshipContextScopes: options?.toolContext?.relationshipAuthorization?.authorizedContextScopes,
966
+ relationshipProfileId: options?.toolContext?.relationshipAuthorization?.profileId,
967
+ relationshipToolNames: options?.toolContext?.relationshipAuthorization?.advertisedToolNames,
965
968
  orientationFrame: turnOrientationFrame,
966
969
  resumePriorWork: options?.resumePriorWork === true,
967
970
  providerCapabilities: providerRuntime.capabilities,
@@ -1083,7 +1086,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1083
1086
  catch { /* unsupported */ }
1084
1087
  const toolPreferences = currentContext?.friend?.toolPreferences;
1085
1088
  const unboundBaseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, friends_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities, options?.mcpManager, providerRuntime.model);
1086
- const baseTools = bindCurrentIngressEvidenceLocator(unboundBaseTools, options?.toolContext?.currentIngressEvidence);
1089
+ const relationshipToolNames = options?.toolContext?.relationshipAuthorization?.advertisedToolNames;
1090
+ const relationshipScopedTools = relationshipToolNames
1091
+ ? unboundBaseTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
1092
+ : unboundBaseTools;
1093
+ const baseTools = bindCurrentIngressEvidenceLocator(relationshipScopedTools, options?.toolContext?.currentIngressEvidence);
1087
1094
  // Augment tool context with reasoning effort controls from provider
1088
1095
  const baseToolContext = options?.toolContext
1089
1096
  ?? (turnOrientationFrame ? { signin: async () => undefined, orientationFrame: turnOrientationFrame } : undefined);
@@ -1127,7 +1134,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1127
1134
  const filteredBaseTools = isPrivateRuntimeChannel
1128
1135
  ? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
1129
1136
  : baseTools;
1130
- const ordinaryActiveTools = [
1137
+ const unscopedOrdinaryActiveTools = [
1131
1138
  ...filteredBaseTools,
1132
1139
  ...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
1133
1140
  ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
@@ -1136,6 +1143,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1136
1143
  ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
1137
1144
  ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
1138
1145
  ];
1146
+ const ordinaryActiveTools = relationshipToolNames
1147
+ ? unscopedOrdinaryActiveTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
1148
+ : unscopedOrdinaryActiveTools;
1139
1149
  const activeTools = options?.toolProfile === "sanctuary-health-private"
1140
1150
  ? (() => {
1141
1151
  const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
@@ -1740,10 +1750,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1740
1750
  done = true;
1741
1751
  continue;
1742
1752
  }
1743
- const approvalCalls = options?.approvalCoordinator ? validCalls.map((entry) => ({
1744
- ...entry,
1745
- policy: (0, tools_1.approvalPolicyForToolName)(entry.call.name, entry.validated.arguments),
1746
- })) : [];
1753
+ const approvalCalls = await Promise.all(validCalls.map(async (entry) => {
1754
+ const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
1755
+ return {
1756
+ ...entry,
1757
+ ...classification,
1758
+ };
1759
+ }));
1747
1760
  const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
1748
1761
  if (protectedCall && result.toolCalls.length !== 1) {
1749
1762
  streamCallbackBuffer?.discard();
@@ -1763,6 +1776,21 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1763
1776
  continue;
1764
1777
  }
1765
1778
  if (protectedCall && protectedCall.policy.kind === "required") {
1779
+ if (!options?.approvalCoordinator) {
1780
+ streamCallbackBuffer?.discard();
1781
+ pushGenerated(msg);
1782
+ const rejection = "rejected: this protected tool requires approval, but the approval coordinator is unavailable; the handler was not invoked.";
1783
+ pushGenerated({ role: "tool", tool_call_id: protectedCall.call.id, content: rejection });
1784
+ providerRuntime.appendToolOutput(protectedCall.call.id, rejection);
1785
+ (0, runtime_1.emitNervesEvent)({
1786
+ level: "warn",
1787
+ component: "engine",
1788
+ event: "engine.approval_coordinator_unavailable",
1789
+ message: "protected tool failed closed before handler execution",
1790
+ meta: { toolName: protectedCall.call.name, toolCallId: protectedCall.call.id },
1791
+ });
1792
+ continue;
1793
+ }
1766
1794
  streamCallbackBuffer?.discard();
1767
1795
  pushGenerated(msg);
1768
1796
  const toolDigest = (0, tool_arguments_1.digestJson)({
@@ -2078,7 +2106,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2078
2106
  let success;
2079
2107
  try {
2080
2108
  const execToolFn = options?.execTool ?? tools_1.execTool;
2081
- toolResult = await execToolFn(tc.name, args, augmentedToolContext);
2109
+ const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
2110
+ const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
2111
+ toolResult = await execToolFn(tc.name, args, executionToolContext);
2082
2112
  success = true;
2083
2113
  }
2084
2114
  catch (e) {
@@ -10,6 +10,7 @@ function buildPendingEnvelope(request, agentName, now) {
10
10
  channel: request.channel,
11
11
  key: request.key,
12
12
  content: request.content,
13
+ ...(request.requestId ? { requestId: request.requestId } : {}),
13
14
  timestamp: now,
14
15
  };
15
16
  }
@@ -71,8 +71,11 @@ function runContainerSpecAuditorCli(args, deps = {}) {
71
71
  const write = deps.write ?? ((text) => process.stdout.write(text));
72
72
  const staged = parseModeArguments(args, ["--template", "--runtime-policy", "--expected-image"]);
73
73
  const effective = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image"]);
74
- if (!staged && !effective) {
75
- write(JSON.stringify({ ok: false, error: "usage: staged --template <path> --runtime-policy <path> --expected-image <id>; effective --inspect <path> --image-inspect <path> --expected-image <id>" }) + "\n");
74
+ const legacyCandidate = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image", "--mount-contract"]);
75
+ const legacyEffective = legacyCandidate?.["--mount-contract"] === "legacy-alpha742" ? legacyCandidate : null;
76
+ const selectedEffective = effective ?? legacyEffective;
77
+ if (!staged && !selectedEffective) {
78
+ write(JSON.stringify({ ok: false, error: "usage: staged --template <path> --runtime-policy <path> --expected-image <id>; effective --inspect <path> --image-inspect <path> --expected-image <id> [--mount-contract legacy-alpha742]" }) + "\n");
76
79
  (0, runtime_1.emitNervesEvent)({
77
80
  level: "error",
78
81
  component: "daemon",
@@ -82,12 +85,12 @@ function runContainerSpecAuditorCli(args, deps = {}) {
82
85
  });
83
86
  return 2;
84
87
  }
85
- if (effective) {
88
+ if (selectedEffective) {
86
89
  let containerText;
87
90
  let imageText;
88
91
  try {
89
- containerText = readFile(effective["--inspect"]);
90
- imageText = readFile(effective["--image-inspect"]);
92
+ containerText = readFile(selectedEffective["--inspect"]);
93
+ imageText = readFile(selectedEffective["--image-inspect"]);
91
94
  }
92
95
  catch (error) {
93
96
  write(JSON.stringify({ ok: false, error: "effective audit inputs are unreadable" }) + "\n");
@@ -118,10 +121,11 @@ function runContainerSpecAuditorCli(args, deps = {}) {
118
121
  return 2;
119
122
  }
120
123
  const result = (0, container_spec_auditor_1.auditSanctuaryContainerSpec)(containerInspect, {
121
- expectedImage: effective["--expected-image"],
124
+ expectedImage: selectedEffective["--expected-image"],
122
125
  expectedEnvironment,
126
+ mountContract: legacyEffective ? "legacy-alpha742" : "canonical",
123
127
  });
124
- if (imageInspect.Id !== effective["--expected-image"]) {
128
+ if (imageInspect.Id !== selectedEffective["--expected-image"]) {
125
129
  result.ok = false;
126
130
  result.violations.unshift("reviewed image inspect identity does not match the expected local image ID");
127
131
  }
@@ -6,11 +6,18 @@ const runtime_1 = require("../../nerves/runtime");
6
6
  const EXPECTED_BINDS = [
7
7
  "/mnt/user/appdata/ouro-butler/runtime/.ouro-cli:/home/ouro/.ouro-cli:rw",
8
8
  "/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro:/home/ouro/AgentBundles/sanctuary.ouro:rw",
9
+ "/boot/config/custom/ouro-events/spool:/run/ouro-events:ro",
10
+ "/mnt/user/appdata/sabnzbd/sabnzbd.ini:/run/sanctuary/sabnzbd.ini:ro",
9
11
  ];
10
12
  const EXPECTED_MOUNTS = [
11
- ["/mnt/user/appdata/ouro-butler/runtime/.ouro-cli", "/home/ouro/.ouro-cli"],
12
- ["/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro", "/home/ouro/AgentBundles/sanctuary.ouro"],
13
+ ["/mnt/user/appdata/ouro-butler/runtime/.ouro-cli", "/home/ouro/.ouro-cli", true],
14
+ ["/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro", "/home/ouro/AgentBundles/sanctuary.ouro", true],
15
+ ["/boot/config/custom/ouro-events/spool", "/run/ouro-events", false],
16
+ ["/mnt/user/appdata/sabnzbd/sabnzbd.ini", "/run/sanctuary/sabnzbd.ini", false],
13
17
  ];
18
+ const LEGACY_ALPHA742_IMAGE = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
19
+ const LEGACY_ALPHA742_BINDS = EXPECTED_BINDS.slice(0, 2);
20
+ const LEGACY_ALPHA742_MOUNTS = EXPECTED_MOUNTS.slice(0, 2);
14
21
  const EXPECTED_EXTRA_PARAMS = "--restart=unless-stopped --user=10001:10001";
15
22
  function record(value) {
16
23
  return value && typeof value === "object" && !Array.isArray(value)
@@ -29,12 +36,18 @@ function auditSanctuaryContainerSpec(value, options) {
29
36
  const root = record(value);
30
37
  const config = record(root?.Config);
31
38
  const host = record(root?.HostConfig);
39
+ const network = record(root?.NetworkSettings);
32
40
  if (!root || !config || !host) {
33
41
  violations.push("inspect payload must contain object Config and HostConfig records");
34
42
  }
35
43
  else {
36
44
  if (!/^sha256:[a-f0-9]{64}$/u.test(options.expectedImage))
37
45
  violations.push("expected image must be an exact local Docker image ID");
46
+ const mountContract = options.mountContract ?? "canonical";
47
+ if (mountContract === "legacy-alpha742" && options.expectedImage !== LEGACY_ALPHA742_IMAGE)
48
+ violations.push("legacy mount exception requires the pinned alpha.742 image ID");
49
+ const expectedBinds = mountContract === "legacy-alpha742" ? LEGACY_ALPHA742_BINDS : EXPECTED_BINDS;
50
+ const expectedMounts = mountContract === "legacy-alpha742" ? LEGACY_ALPHA742_MOUNTS : EXPECTED_MOUNTS;
38
51
  if (config.Image !== options.expectedImage)
39
52
  violations.push("image does not match the reviewed exact local Docker image ID");
40
53
  if (root.Path !== "node")
@@ -54,8 +67,16 @@ function auditSanctuaryContainerSpec(value, options) {
54
67
  violations.push("container must expose no ports");
55
68
  if (host.NetworkMode !== "host")
56
69
  violations.push("network mode must be host");
70
+ if (host.PidMode !== "")
71
+ violations.push("PID namespace must be private");
72
+ if (host.IpcMode !== "private")
73
+ violations.push("IPC namespace must be private");
57
74
  if (host.Privileged !== false)
58
75
  violations.push("container must not be privileged");
76
+ if (host.ReadonlyRootfs !== false)
77
+ violations.push("root filesystem mode must match the reviewed runtime");
78
+ if (!(host.SecurityOpt === null || (Array.isArray(host.SecurityOpt) && host.SecurityOpt.length === 0)))
79
+ violations.push("container must set no security options");
59
80
  const restart = record(host.RestartPolicy);
60
81
  if (restart?.Name !== "unless-stopped" || restart.MaximumRetryCount !== 0)
61
82
  violations.push("restart policy must be unless-stopped");
@@ -65,19 +86,25 @@ function auditSanctuaryContainerSpec(value, options) {
65
86
  violations.push("container must have no devices");
66
87
  if (!(host.CapAdd === null || (Array.isArray(host.CapAdd) && host.CapAdd.length === 0)))
67
88
  violations.push("container must add no capabilities");
89
+ if (!(host.CapDrop === null || (Array.isArray(host.CapDrop) && host.CapDrop.length === 0)))
90
+ violations.push("container must drop no capabilities");
91
+ if (host.PublishAllPorts !== false)
92
+ violations.push("container must not publish all exposed ports");
93
+ if (!isEmptyRecord(network?.Ports))
94
+ violations.push("effective network ports must be empty");
68
95
  const binds = stringArray(host.Binds);
69
- if (!binds || JSON.stringify([...binds].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort()))
70
- violations.push("bind set must equal the canonical two writable roots");
96
+ if (!binds || JSON.stringify([...binds].sort()) !== JSON.stringify([...expectedBinds].sort()))
97
+ violations.push("bind set does not match the selected Sanctuary mount contract");
71
98
  const mounts = Array.isArray(root.Mounts) ? root.Mounts : [];
72
99
  const normalizedMounts = mounts.map((mount) => {
73
100
  const item = record(mount);
74
- return item && item.Type === "bind" && item.RW === true && typeof item.Source === "string" && typeof item.Destination === "string"
75
- ? [item.Source, item.Destination]
101
+ return item && item.Type === "bind" && typeof item.RW === "boolean" && typeof item.Source === "string" && typeof item.Destination === "string"
102
+ ? [item.Source, item.Destination, item.RW]
76
103
  : null;
77
104
  });
78
105
  if (normalizedMounts.some((mount) => mount === null)
79
- || JSON.stringify(normalizedMounts.sort()) !== JSON.stringify(EXPECTED_MOUNTS.map((mount) => [...mount]).sort())) {
80
- violations.push("effective mounts must equal the canonical two writable bind mounts");
106
+ || JSON.stringify(normalizedMounts.sort()) !== JSON.stringify(expectedMounts.map((mount) => [...mount]).sort())) {
107
+ violations.push("effective mounts do not match the selected Sanctuary mount contract");
81
108
  }
82
109
  }
83
110
  const result = { ok: violations.length === 0, violations };
@@ -130,10 +157,10 @@ function auditSanctuaryStagedFiles(input) {
130
157
  const mode = match[1].match(/\bMode="([^"]+)"/u)?.[1];
131
158
  return type === "Path" && target && mode ? `${match[2]}:${target}:${mode}` : "invalid";
132
159
  });
133
- if (configOpenCount !== 2
134
- || configEntries.length !== 2
160
+ if (configOpenCount !== 4
161
+ || configEntries.length !== 4
135
162
  || JSON.stringify([...pathConfigs].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort())) {
136
- violations.push("template Config entries must equal the canonical two path binds");
163
+ violations.push("template Config entries must equal the canonical four path binds");
137
164
  }
138
165
  const postArgsOpenCount = [...input.templateXml.matchAll(/<PostArgs\b/gu)].length;
139
166
  const postArgs = [...input.templateXml.matchAll(/<PostArgs(?:(?:\s*\/>)|>([^<]*)<\/PostArgs>)/gu)];
@@ -412,12 +412,20 @@ const daemon = new daemon_1.OuroDaemon({
412
412
  const before = schedulerScenario ? (0, sanctuary_scheduler_liveness_1.readSanctuaryHealthCursor)(agentRoot) : null;
413
413
  let providerInvocationCount = 0;
414
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; },
415
+ const result = await (0, sanctuary_health_runner_1.runSanctuaryHealthHabit)(agent, {
416
+ submitEvidence: async (input) => {
417
+ const response = await daemon.handleCommand({ kind: "external.event.submit", ...input });
418
+ if (!response.ok)
419
+ throw new Error(response.error ?? "Sanctuary health evidence submission failed");
420
+ return response.data.event;
419
421
  },
420
- } : {});
422
+ ...(schedulerScenario ? {
423
+ acceptanceMetrics: {
424
+ onPrivateTurnStart: () => { privateTurnCount += 1; },
425
+ onProviderInvocation: () => { providerInvocationCount += 1; },
426
+ },
427
+ } : {}),
428
+ });
421
429
  if (schedulerScenario) {
422
430
  if (!supercronicSupervisor || !before || !occurrenceId || !schedulerOrigin)
423
431
  throw new Error("Sanctuary scheduler liveness supervisor provenance is unavailable");
@@ -834,6 +842,12 @@ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap
834
842
  awaitName,
835
843
  deliveryDeps: {
836
844
  agentName: agent,
845
+ deliverers: {
846
+ telegram: async (request) => {
847
+ const { sendTelegramAwaitFollowUp } = await Promise.resolve().then(() => __importStar(require("../../senses/telegram")));
848
+ return sendTelegramAwaitFollowUp(agent, request);
849
+ },
850
+ },
837
851
  queuePending: () => {
838
852
  // Best-effort: queue private-runtime wake so the agent processes the alert path.
839
853
  (0, socket_client_1.sendDaemonCommand)(socketPath, (0, await_private_wake_1.buildAwaitPrivateWakeCommand)({
@@ -615,10 +615,14 @@ class OuroDaemon {
615
615
  mailboxServer = null;
616
616
  socketIdentity = null;
617
617
  senseAutostartTimer = null;
618
+ externalEventReconcileTimer = null;
619
+ externalEventReconcileRunning = false;
618
620
  mailboxServerFactory;
619
621
  privateRuntimePolicyDeps;
620
622
  onStopCommandComplete;
621
623
  externalEventRoot;
624
+ privilegedEventSpoolRoot;
625
+ privilegedEventScanner;
622
626
  rsvpHabitRunner;
623
627
  nativeHabitRunner;
624
628
  nativeHabitMatch;
@@ -640,6 +644,8 @@ class OuroDaemon {
640
644
  this.privateRuntimePolicyDeps = options.privateRuntimePolicyDeps ?? {};
641
645
  this.onStopCommandComplete = options.onStopCommandComplete ?? null;
642
646
  this.externalEventRoot = options.externalEventRoot ?? null;
647
+ this.privilegedEventSpoolRoot = options.privilegedEventSpoolRoot ?? "/run/ouro-events";
648
+ this.privilegedEventScanner = options.privilegedEventScanner ?? router_1.scanPrivilegedEventSpool;
643
649
  this.rsvpHabitRunner = options.rsvpHabitRunner;
644
650
  this.nativeHabitRunner = options.nativeHabitRunner;
645
651
  this.nativeHabitMatch = options.nativeHabitMatch;
@@ -694,6 +700,7 @@ class OuroDaemon {
694
700
  agentName,
695
701
  agentRoot: path.join(this.bundlesRoot, `${agentName}.ouro`),
696
702
  })));
703
+ const externalEvents = (0, router_1.listExternalEventStatus)(this.externalEventRoot ?? (0, router_1.getExternalEventRoot)());
697
704
  const mailboxUrl = this.mailboxServer?.origin ?? "http://127.0.0.1:0";
698
705
  return {
699
706
  overview: {
@@ -714,6 +721,7 @@ class OuroDaemon {
714
721
  sync,
715
722
  agents,
716
723
  ...(providers.length > 0 ? { providers } : {}),
724
+ ...(externalEvents.length > 0 ? { externalEvents } : {}),
717
725
  };
718
726
  }
719
727
  overlaySchedulerHealth(healthChecks) {
@@ -801,6 +809,11 @@ class OuroDaemon {
801
809
  // (daemon manages multiple agents; agent identity must be set before loading MCP config)
802
810
  await this.orphanStartupDrain(this.socketPath);
803
811
  await this.openCommandSocket();
812
+ await this.reconcileExternalEvents();
813
+ this.externalEventReconcileTimer = setInterval(() => {
814
+ void this.reconcileExternalEvents();
815
+ }, 1_000);
816
+ this.externalEventReconcileTimer.unref?.();
804
817
  this.triggerAutoStartAgents();
805
818
  this.triggerAutoStartSensesWhenAgentsSettled();
806
819
  // Write all managed PIDs to disk so the next daemon can clean up
@@ -1105,6 +1118,10 @@ class OuroDaemon {
1105
1118
  clearTimeout(this.senseAutostartTimer);
1106
1119
  this.senseAutostartTimer = null;
1107
1120
  }
1121
+ if (this.externalEventReconcileTimer) {
1122
+ clearInterval(this.externalEventReconcileTimer);
1123
+ this.externalEventReconcileTimer = null;
1124
+ }
1108
1125
  const workerStopTasks = [Promise.resolve().then(() => this.processManager.stopAll())];
1109
1126
  if (this.senseManager) {
1110
1127
  workerStopTasks.push(Promise.resolve().then(() => this.senseManager.stopAll()));
@@ -1279,14 +1296,14 @@ class OuroDaemon {
1279
1296
  ],
1280
1297
  };
1281
1298
  }
1282
- buildExternalEventPrivateWakeCommand(command, receiptId) {
1299
+ buildExternalEventPrivateWakeCommand(command, receiptId, generation, attemptCount) {
1283
1300
  return {
1284
1301
  kind: "private.wake",
1285
1302
  agent: command.agent,
1286
1303
  reason: `external event ${command.source}/${command.eventType}`,
1287
1304
  triggerSource: "external-event",
1288
1305
  budgetClass: "interactive",
1289
- idempotencyKey: `external-event:${command.agent}:${command.source}:${command.eventId}`,
1306
+ idempotencyKey: `external-event:${command.agent}:${command.source}:${command.eventId}:generation:${generation}:attempt:${attemptCount}`,
1290
1307
  originRefs: [
1291
1308
  { kind: "external-event", id: command.eventId, source: command.source, eventType: command.eventType },
1292
1309
  { kind: "queue-receipt", id: receiptId },
@@ -1456,7 +1473,7 @@ class OuroDaemon {
1456
1473
  data: result,
1457
1474
  };
1458
1475
  }
1459
- buildPrivateRuntimeWorkerWakeMessage(command, decision) {
1476
+ buildPrivateRuntimeWorkerWakeMessage(command, decision, externalEvent) {
1460
1477
  const awaitName = (0, await_private_wake_1.awaitNameFromPrivateWakeCommand)(command);
1461
1478
  if (awaitName) {
1462
1479
  return {
@@ -1483,13 +1500,13 @@ class OuroDaemon {
1483
1500
  privateTurnDecision: decision,
1484
1501
  };
1485
1502
  }
1486
- return { type: "message", privateTurnDecision: decision };
1503
+ return { type: "message", privateTurnDecision: decision, ...(externalEvent ? { externalEvent } : {}) };
1487
1504
  }
1488
1505
  queueExternalEventForPrivateRuntime(record) {
1489
1506
  const pendingDir = path.join(this.bundlesRoot, `${record.agent}.ouro`, "state", "pending", pending_1.PRIVATE_RUNTIME_PENDING.friendId, pending_1.PRIVATE_RUNTIME_PENDING.channel, pending_1.PRIVATE_RUNTIME_PENDING.key);
1490
1507
  const originKey = `${record.source}:${record.eventId}`;
1491
1508
  const parsedReceivedAt = Date.parse(record.receivedAt);
1492
- (0, pending_1.queuePendingMessage)(pendingDir, {
1509
+ (0, pending_1.queuePendingMessageOnce)(pendingDir, {
1493
1510
  from: "ouro-external-event",
1494
1511
  friendId: "ouro-external-event",
1495
1512
  channel: "external-event",
@@ -1503,6 +1520,7 @@ class OuroDaemon {
1503
1520
  },
1504
1521
  obligationStatus: "pending",
1505
1522
  mode: "relay",
1523
+ packetId: `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount}`,
1506
1524
  });
1507
1525
  (0, runtime_1.emitNervesEvent)({
1508
1526
  component: "daemon",
@@ -1517,7 +1535,124 @@ class OuroDaemon {
1517
1535
  },
1518
1536
  });
1519
1537
  }
1520
- async handlePrivateRuntimeWake(command, beforeDispatch) {
1538
+ externalEventRootPath() {
1539
+ return this.externalEventRoot ?? (0, router_1.getExternalEventRoot)();
1540
+ }
1541
+ externalEventLease(record) {
1542
+ /* v8 ignore next -- dispatcher calls this only with the record returned by claimExternalEvent @preserve */
1543
+ if (!record.claimOwner)
1544
+ throw new Error("External event dispatch requires a claimed receipt");
1545
+ return {
1546
+ schemaVersion: 1,
1547
+ recordPath: record.recordPath,
1548
+ agent: record.agent,
1549
+ source: record.source,
1550
+ eventId: record.eventId,
1551
+ generation: record.generation,
1552
+ observationRevision: record.observationRevision,
1553
+ claimOwner: record.claimOwner,
1554
+ };
1555
+ }
1556
+ async dispatchExternalEvents(records) {
1557
+ const claimed = [];
1558
+ try {
1559
+ for (const record of records) {
1560
+ const owner = `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount + 1}`;
1561
+ claimed.push((0, router_1.claimExternalEvent)(record.recordPath, { owner, expectedVersion: record.version, expectedGeneration: record.generation }));
1562
+ }
1563
+ const primary = claimed[0];
1564
+ const [primaryLease, ...relatedEvents] = claimed.map((record) => this.externalEventLease(record));
1565
+ const lease = { ...primaryLease, ...(relatedEvents.length > 0 ? { relatedEvents } : {}) };
1566
+ const receipt = {
1567
+ id: `external-event:${primary.agent}:${primary.source}:${primary.eventId}:generation:${primary.generation}:attempt:${primary.attemptCount}`,
1568
+ queuedAt: primary.updatedAt,
1569
+ };
1570
+ const wake = await this.handlePrivateRuntimeWake(this.buildExternalEventPrivateWakeCommand({
1571
+ kind: "external.event.submit",
1572
+ agent: primary.agent,
1573
+ source: primary.source,
1574
+ eventType: primary.eventType,
1575
+ eventId: primary.eventId,
1576
+ }, receipt.id, primary.generation, primary.attemptCount), () => { for (const record of claimed)
1577
+ this.queueExternalEventForPrivateRuntime(record); }, lease);
1578
+ const denied = !wake.ok || wake.data?.decision?.executable === false;
1579
+ if (denied)
1580
+ throw new Error(wake.error ?? wake.message ?? /* v8 ignore next -- daemon wake responses always carry an error or message @preserve */ "external-event private turn was denied");
1581
+ return { events: claimed, event: primary, receipt, wake };
1582
+ }
1583
+ catch (error) {
1584
+ for (const record of claimed) {
1585
+ const latest = (0, router_1.readExternalEventRecord)(record.recordPath);
1586
+ if (latest.executionState === "running" && latest.claimOwner === record.claimOwner) {
1587
+ (0, router_1.failExternalEventAttempt)(latest.recordPath, {
1588
+ owner: record.claimOwner, expectedVersion: latest.version, expectedGeneration: latest.generation,
1589
+ error: error instanceof Error ? error.message : String(error),
1590
+ });
1591
+ }
1592
+ }
1593
+ throw error;
1594
+ }
1595
+ }
1596
+ dispatchExternalEvent(record) {
1597
+ return this.dispatchExternalEvents([record]);
1598
+ }
1599
+ async reconcileExternalEvents() {
1600
+ /* v8 ignore next -- re-entrancy guard is exercised only by overlapping real timer callbacks @preserve */
1601
+ if (this.externalEventReconcileRunning)
1602
+ return;
1603
+ this.externalEventReconcileRunning = true;
1604
+ try {
1605
+ const now = new Date().toISOString();
1606
+ if (fs.existsSync(this.privilegedEventSpoolRoot)) {
1607
+ this.privilegedEventScanner({ spoolRoot: this.privilegedEventSpoolRoot, eventRoot: this.externalEventRootPath() });
1608
+ }
1609
+ const statuses = (0, router_1.listExternalEventStatus)(this.externalEventRootPath());
1610
+ const dueRecords = [];
1611
+ for (const status of statuses) {
1612
+ if (status.corrupt)
1613
+ continue;
1614
+ let record = (0, router_1.readExternalEventRecord)(status.recordPath);
1615
+ if (record.dispatchEnabled === false)
1616
+ continue;
1617
+ if (record.executionState === "running" && record.claimExpiresAt && Date.parse(record.claimExpiresAt) <= Date.parse(now)) {
1618
+ record = (0, router_1.reconcileExternalEvent)(record.recordPath);
1619
+ }
1620
+ const due = record.executionState === "received"
1621
+ || record.executionState === "queued"
1622
+ || (record.executionState === "retry_wait" && record.nextAttemptAt !== null && Date.parse(record.nextAttemptAt) <= Date.parse(now));
1623
+ if (due)
1624
+ dueRecords.push(record);
1625
+ }
1626
+ const batches = new Map();
1627
+ for (const record of dueRecords) {
1628
+ const key = `${record.agent}\0${record.source}`;
1629
+ const batch = batches.get(key) ?? [];
1630
+ batch.push(record);
1631
+ batches.set(key, batch);
1632
+ }
1633
+ for (const records of batches.values()) {
1634
+ for (let offset = 0; offset < records.length; offset += 32) {
1635
+ const batch = records.slice(offset, offset + 32);
1636
+ try {
1637
+ await this.dispatchExternalEvents(batch);
1638
+ }
1639
+ catch (error) {
1640
+ (0, runtime_1.emitNervesEvent)({
1641
+ level: "warn",
1642
+ component: "daemon",
1643
+ event: "daemon.external_event_dispatch_error",
1644
+ message: "external event dispatch failed",
1645
+ meta: { recordPath: batch[0].recordPath, batchSize: batch.length, error: error instanceof Error ? error.message : /* v8 ignore next -- dispatch failures are Error objects @preserve */ String(error) },
1646
+ });
1647
+ }
1648
+ }
1649
+ }
1650
+ }
1651
+ finally {
1652
+ this.externalEventReconcileRunning = false;
1653
+ }
1654
+ }
1655
+ async handlePrivateRuntimeWake(command, beforeDispatch, externalEvent) {
1521
1656
  if (!this.hasManagedPrivateRuntime(command.agent)) {
1522
1657
  return {
1523
1658
  ok: false,
@@ -1537,7 +1672,7 @@ class OuroDaemon {
1537
1672
  }
1538
1673
  await beforeDispatch?.(decision);
1539
1674
  await this.processManager.startAgent(command.agent);
1540
- this.processManager.sendToAgent?.(command.agent, this.buildPrivateRuntimeWorkerWakeMessage(command, decision));
1675
+ this.processManager.sendToAgent?.(command.agent, this.buildPrivateRuntimeWorkerWakeMessage(command, decision, externalEvent));
1541
1676
  return {
1542
1677
  ok: true,
1543
1678
  message: `woke private runtime for ${command.agent}`,
@@ -1766,27 +1901,24 @@ class OuroDaemon {
1766
1901
  evidence: command.evidence,
1767
1902
  payloadPath: command.payloadPath,
1768
1903
  priority: command.priority,
1904
+ observationRevision: command.observationRevision,
1905
+ transition: command.transition,
1769
1906
  }, {
1770
1907
  ...(this.externalEventRoot ? { root: this.externalEventRoot } : {}),
1908
+ dispatchEnabled: command.wake !== false,
1771
1909
  });
1772
- const receipt = await this.router.send({
1773
- from: "ouro-external-event",
1774
- to: command.agent,
1775
- content: (0, router_1.buildExternalEventMessage)(record),
1776
- priority: record.priority,
1777
- sessionId: command.sessionId,
1778
- taskRef: command.taskRef ?? `${record.source}:${record.eventId}`,
1779
- });
1780
- let wake = null;
1781
- if (command.wake !== false) {
1782
- wake = await this.handlePrivateRuntimeWake(this.buildExternalEventPrivateWakeCommand(command, receipt.id), () => this.queueExternalEventForPrivateRuntime(record));
1910
+ if (!record.shouldWake || command.wake === false) {
1911
+ return {
1912
+ ok: true,
1913
+ message: `updated external event ${record.source}/${record.eventId} without wake`,
1914
+ data: { event: record, receipt: null, wake: null },
1915
+ };
1783
1916
  }
1917
+ const dispatched = await this.dispatchExternalEvent(record);
1784
1918
  return {
1785
1919
  ok: true,
1786
- message: command.wake === false
1787
- ? `queued external event ${record.source}/${record.eventId} as ${receipt.id}`
1788
- : `queued external event ${record.source}/${record.eventId} as ${receipt.id}; ${wake?.message ?? "wake skipped"}`,
1789
- data: { event: record, receipt, wake },
1920
+ message: `queued external event ${record.source}/${record.eventId} as ${dispatched.receipt.id}; ${dispatched.wake.message ?? "wake skipped"}`,
1921
+ data: dispatched,
1790
1922
  };
1791
1923
  }
1792
1924
  case "private.decisions": {