@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
@@ -39,7 +39,9 @@ exports.readRuntimeCredentialConfig = readRuntimeCredentialConfig;
39
39
  exports.readMachineRuntimeCredentialConfig = readMachineRuntimeCredentialConfig;
40
40
  exports.cacheRuntimeCredentialConfig = cacheRuntimeCredentialConfig;
41
41
  exports.cacheMachineRuntimeCredentialConfig = cacheMachineRuntimeCredentialConfig;
42
+ exports.isRuntimeCredentialBootstrapMessage = isRuntimeCredentialBootstrapMessage;
42
43
  exports.applyRuntimeCredentialBootstrapMessage = applyRuntimeCredentialBootstrapMessage;
44
+ exports.persistRuntimeCredentialBootstrapMessage = persistRuntimeCredentialBootstrapMessage;
43
45
  exports.waitForRuntimeCredentialBootstrap = waitForRuntimeCredentialBootstrap;
44
46
  exports.refreshRuntimeCredentialConfig = refreshRuntimeCredentialConfig;
45
47
  exports.refreshMachineRuntimeCredentialConfig = refreshMachineRuntimeCredentialConfig;
@@ -68,25 +70,54 @@ function isCredentialRecord(value) {
68
70
  return false;
69
71
  return Object.values(value).every(isCredentialValue);
70
72
  }
73
+ function isPresentCredentialValue(value) {
74
+ if (typeof value === "string")
75
+ return value.trim().length > 0;
76
+ return typeof value === "number" && Number.isFinite(value) && value !== 0;
77
+ }
78
+ function hasExactKeys(value, keys) {
79
+ return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
80
+ }
81
+ function isCanonicalTimestamp(value) {
82
+ if (typeof value !== "string")
83
+ return false;
84
+ try {
85
+ return new Date(value).toISOString() === value;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
71
91
  function isProviderCredentialRecord(value) {
72
92
  if (!isRecord(value))
73
93
  return false;
74
- if (typeof value.provider !== "string")
94
+ if (!hasExactKeys(value, ["provider", "revision", "updatedAt", "credentials", "config", "provenance"]))
95
+ return false;
96
+ if (typeof value.provider !== "string" || !Object.prototype.hasOwnProperty.call(identity_1.PROVIDER_CREDENTIALS, value.provider))
97
+ return false;
98
+ if (typeof value.revision !== "string" || !/^vault_[0-9a-f]{16}$/u.test(value.revision))
75
99
  return false;
76
- if (typeof value.revision !== "string" || value.revision.trim().length === 0)
100
+ if (!isCanonicalTimestamp(value.updatedAt))
77
101
  return false;
78
- if (typeof value.updatedAt !== "string" || value.updatedAt.trim().length === 0)
102
+ const credentials = value.credentials;
103
+ const config = value.config;
104
+ if (!isCredentialRecord(credentials))
79
105
  return false;
80
- if (!isCredentialRecord(value.credentials))
106
+ if (!isCredentialRecord(config))
81
107
  return false;
82
- if (!isCredentialRecord(value.config))
108
+ const provider = value.provider;
109
+ const canonicalCredentials = (0, provider_credentials_1.splitProviderCredentialFields)(provider, credentials).credentials;
110
+ const canonicalConfig = (0, provider_credentials_1.splitProviderCredentialFields)(provider, config).config;
111
+ if (!identity_1.PROVIDER_CREDENTIALS[provider].required.every((field) => (isPresentCredentialValue(canonicalCredentials[field]) || isPresentCredentialValue(canonicalConfig[field]))))
83
112
  return false;
84
113
  const provenance = value.provenance;
85
114
  if (!isRecord(provenance))
86
115
  return false;
116
+ if (!hasExactKeys(provenance, ["source", "updatedAt"]))
117
+ return false;
87
118
  if (provenance.source !== "auth-flow" && provenance.source !== "manual")
88
119
  return false;
89
- if (typeof provenance.updatedAt !== "string" || provenance.updatedAt.trim().length === 0)
120
+ if (!isCanonicalTimestamp(provenance.updatedAt) || provenance.updatedAt !== value.updatedAt)
90
121
  return false;
91
122
  return true;
92
123
  }
@@ -98,6 +129,55 @@ function stableJson(value) {
98
129
  }
99
130
  return JSON.stringify(value);
100
131
  }
132
+ function reconcileBootstrapFields(base, bootstrap, prefix) {
133
+ const conflicts = [];
134
+ const merge = (current, incoming, pathPrefix) => {
135
+ const next = { ...current };
136
+ for (const [key, incomingValue] of Object.entries(incoming)) {
137
+ const fieldPath = `${pathPrefix}.${key}`;
138
+ if (!Object.prototype.hasOwnProperty.call(current, key)) {
139
+ next[key] = incomingValue;
140
+ continue;
141
+ }
142
+ const currentValue = current[key];
143
+ if (isRecord(currentValue) && isRecord(incomingValue)) {
144
+ next[key] = merge(currentValue, incomingValue, fieldPath);
145
+ }
146
+ else if (stableJson(currentValue) !== stableJson(incomingValue)) {
147
+ conflicts.push(fieldPath);
148
+ }
149
+ }
150
+ return next;
151
+ };
152
+ const merged = merge(base, bootstrap, prefix);
153
+ if (conflicts.length > 0) {
154
+ throw new Error(`container credential bootstrap conflicts with existing fields: ${conflicts.sort().join(", ")}`);
155
+ }
156
+ return merged;
157
+ }
158
+ function assertBootstrapReadback(actual, expected, prefix) {
159
+ const failures = [];
160
+ const inspect = (current, intended, pathPrefix) => {
161
+ for (const [key, intendedValue] of Object.entries(intended)) {
162
+ const fieldPath = `${pathPrefix}.${key}`;
163
+ if (!Object.prototype.hasOwnProperty.call(current, key)) {
164
+ failures.push(fieldPath);
165
+ continue;
166
+ }
167
+ const currentValue = current[key];
168
+ if (isRecord(currentValue) && isRecord(intendedValue)) {
169
+ inspect(currentValue, intendedValue, fieldPath);
170
+ }
171
+ else if (stableJson(currentValue) !== stableJson(intendedValue)) {
172
+ failures.push(fieldPath);
173
+ }
174
+ }
175
+ };
176
+ inspect(actual, expected, prefix);
177
+ if (failures.length > 0) {
178
+ throw new Error(`container credential bootstrap readback did not contain intended fields: ${failures.sort().join(", ")}`);
179
+ }
180
+ }
101
181
  function runtimeConfigVaultPath(agentName, itemName = exports.RUNTIME_CONFIG_ITEM_NAME) {
102
182
  return `vault:${agentName}:${itemName}`;
103
183
  }
@@ -198,20 +278,41 @@ function isRuntimeCredentialBootstrapMessage(value) {
198
278
  if (!value || typeof value !== "object" || Array.isArray(value))
199
279
  return false;
200
280
  const record = value;
281
+ if (!Object.keys(record).every((key) => [
282
+ "type",
283
+ "agentName",
284
+ "runtimeConfig",
285
+ "machineRuntimeConfig",
286
+ "machineId",
287
+ "providerCredentialRecords",
288
+ ].includes(key)))
289
+ return false;
201
290
  if (record.type !== "ouro.runtimeCredentialBootstrap")
202
291
  return false;
203
292
  if (typeof record.agentName !== "string" || record.agentName.trim().length === 0)
204
293
  return false;
205
- if (record.runtimeConfig !== undefined && !isRecord(record.runtimeConfig))
294
+ if (record.runtimeConfig !== undefined
295
+ && (!isRecord(record.runtimeConfig) || Object.keys(record.runtimeConfig).length === 0))
206
296
  return false;
207
- if (record.machineRuntimeConfig !== undefined && !isRecord(record.machineRuntimeConfig))
297
+ if (record.machineRuntimeConfig !== undefined
298
+ && (!isRecord(record.machineRuntimeConfig) || Object.keys(record.machineRuntimeConfig).length === 0))
208
299
  return false;
209
300
  if (record.machineId !== undefined && (typeof record.machineId !== "string" || record.machineId.trim().length === 0))
210
301
  return false;
211
302
  if (record.providerCredentialRecords !== undefined
212
303
  && (!Array.isArray(record.providerCredentialRecords)
304
+ || record.providerCredentialRecords.length === 0
213
305
  || !record.providerCredentialRecords.every(isProviderCredentialRecord)))
214
306
  return false;
307
+ if (Array.isArray(record.providerCredentialRecords)) {
308
+ const providers = record.providerCredentialRecords.map((providerRecord) => providerRecord.provider);
309
+ if (new Set(providers).size !== providers.length)
310
+ return false;
311
+ }
312
+ if (record.runtimeConfig === undefined
313
+ && record.machineRuntimeConfig === undefined
314
+ && (!Array.isArray(record.providerCredentialRecords) || record.providerCredentialRecords.length === 0))
315
+ return false;
215
316
  return true;
216
317
  }
217
318
  function applyRuntimeCredentialBootstrapMessage(message) {
@@ -241,6 +342,114 @@ function applyRuntimeCredentialBootstrapMessage(message) {
241
342
  });
242
343
  return true;
243
344
  }
345
+ async function persistRuntimeCredentialBootstrapMessage(message, options) {
346
+ if (!isRuntimeCredentialBootstrapMessage(message))
347
+ return false;
348
+ const agentName = message.agentName.trim();
349
+ const now = options.now ?? new Date();
350
+ if (message.machineId !== undefined && message.machineId.trim() !== options.machineId.trim()) {
351
+ throw new Error("container credential bootstrap machineId does not match this machine");
352
+ }
353
+ let runtimePlan = null;
354
+ let machinePlan = null;
355
+ let expectedRuntime = null;
356
+ let expectedMachine = null;
357
+ const providerPlans = [];
358
+ const expectedProviders = new Map();
359
+ if (message.runtimeConfig) {
360
+ const current = await refreshRuntimeCredentialConfig(agentName);
361
+ if (!current.ok && current.reason !== "missing") {
362
+ throw new Error(`cannot reconcile runtimeConfig at ${current.itemPath}: ${current.error}`);
363
+ }
364
+ const base = current.ok ? current.config : {};
365
+ const merged = reconcileBootstrapFields(base, message.runtimeConfig, "runtimeConfig");
366
+ expectedRuntime = merged;
367
+ if (stableJson(merged) !== stableJson(base))
368
+ runtimePlan = merged;
369
+ }
370
+ if (message.machineRuntimeConfig) {
371
+ const machineId = message.machineId ?? options.machineId;
372
+ const current = await refreshMachineRuntimeCredentialConfig(agentName, machineId);
373
+ if (!current.ok && current.reason !== "missing") {
374
+ throw new Error(`cannot reconcile machineRuntimeConfig at ${current.itemPath}: ${current.error}`);
375
+ }
376
+ const base = current.ok ? current.config : {};
377
+ const merged = reconcileBootstrapFields(base, message.machineRuntimeConfig, "machineRuntimeConfig");
378
+ expectedMachine = merged;
379
+ if (stableJson(merged) !== stableJson(base))
380
+ machinePlan = merged;
381
+ }
382
+ for (const record of message.providerCredentialRecords ?? []) {
383
+ const current = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, { providers: [record.provider], skipCache: true });
384
+ if (!current.ok && current.reason !== "missing") {
385
+ throw new Error(`cannot reconcile providerCredentialRecords.${record.provider} at ${current.poolPath}: ${current.error}`);
386
+ }
387
+ const existing = current.ok ? current.pool.providers[record.provider] : undefined;
388
+ const credentials = reconcileBootstrapFields(existing?.credentials ?? {}, record.credentials, `providerCredentialRecords.${record.provider}.credentials`);
389
+ const config = reconcileBootstrapFields(existing?.config ?? {}, record.config, `providerCredentialRecords.${record.provider}.config`);
390
+ expectedProviders.set(record.provider, { credentials, config });
391
+ if (!existing || stableJson(credentials) !== stableJson(existing.credentials) || stableJson(config) !== stableJson(existing.config)) {
392
+ providerPlans.push({
393
+ provider: record.provider,
394
+ credentials,
395
+ config,
396
+ provenanceSource: existing?.provenance.source ?? record.provenance.source,
397
+ });
398
+ }
399
+ }
400
+ if (runtimePlan)
401
+ await upsertRuntimeCredentialConfig(agentName, runtimePlan, now);
402
+ if (machinePlan) {
403
+ await upsertMachineRuntimeCredentialConfig(agentName, message.machineId ?? options.machineId, machinePlan, now);
404
+ }
405
+ for (const plan of providerPlans) {
406
+ await (0, provider_credentials_1.upsertProviderCredential)({
407
+ agentName,
408
+ provider: plan.provider,
409
+ credentials: plan.credentials,
410
+ config: plan.config,
411
+ provenance: { source: plan.provenanceSource },
412
+ now,
413
+ });
414
+ }
415
+ if (message.runtimeConfig) {
416
+ const refreshed = await refreshRuntimeCredentialConfig(agentName);
417
+ if (!refreshed.ok)
418
+ throw new Error(`cannot read back runtimeConfig at ${refreshed.itemPath}: ${refreshed.error}`);
419
+ assertBootstrapReadback(refreshed.config, expectedRuntime, "runtimeConfig");
420
+ }
421
+ if (message.machineRuntimeConfig) {
422
+ const refreshed = await refreshMachineRuntimeCredentialConfig(agentName, message.machineId ?? options.machineId);
423
+ if (!refreshed.ok)
424
+ throw new Error(`cannot read back machineRuntimeConfig at ${refreshed.itemPath}: ${refreshed.error}`);
425
+ assertBootstrapReadback(refreshed.config, expectedMachine, "machineRuntimeConfig");
426
+ }
427
+ if ((message.providerCredentialRecords?.length ?? 0) > 0) {
428
+ const refreshed = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName);
429
+ if (!refreshed.ok)
430
+ throw new Error(`cannot read back providerCredentialRecords at ${refreshed.poolPath}: ${refreshed.error}`);
431
+ for (const [provider, expected] of expectedProviders) {
432
+ const record = refreshed.pool.providers[provider];
433
+ if (!record) {
434
+ throw new Error(`container credential bootstrap readback did not contain intended fields: providerCredentialRecords.${provider}`);
435
+ }
436
+ assertBootstrapReadback(record.credentials, expected.credentials, `providerCredentialRecords.${provider}.credentials`);
437
+ assertBootstrapReadback(record.config, expected.config, `providerCredentialRecords.${provider}.config`);
438
+ }
439
+ }
440
+ (0, runtime_1.emitNervesEvent)({
441
+ component: "config/identity",
442
+ event: "config.runtime_credentials_bootstrap_persisted",
443
+ message: "persisted container credential bootstrap in the agent vault",
444
+ meta: {
445
+ agentName,
446
+ runtimeConfig: !!message.runtimeConfig,
447
+ machineRuntimeConfig: !!message.machineRuntimeConfig,
448
+ providerCredentialRecords: message.providerCredentialRecords?.length ?? 0,
449
+ },
450
+ });
451
+ return true;
452
+ }
244
453
  function waitForRuntimeCredentialBootstrap(agentName, options = {}) {
245
454
  const timeoutMs = options.timeoutMs ?? 1_500;
246
455
  return new Promise((resolve) => {
@@ -10,6 +10,7 @@ const SENSES = [
10
10
  { sense: "mail", label: "Mail", daemonManaged: true },
11
11
  { sense: "voice", label: "Voice", daemonManaged: true },
12
12
  { sense: "a2a", label: "A2A", daemonManaged: true },
13
+ { sense: "telegram", label: "Telegram", daemonManaged: true },
13
14
  { sense: "workbench", label: "Workbench", daemonManaged: false },
14
15
  ];
15
16
  function configuredSenses(senses) {
@@ -22,6 +23,7 @@ function configuredSenses(senses) {
22
23
  mail: configured.mail ?? { ...identity_1.DEFAULT_AGENT_SENSES.mail },
23
24
  voice: configured.voice ?? { ...identity_1.DEFAULT_AGENT_SENSES.voice },
24
25
  a2a: configured.a2a ?? { ...identity_1.DEFAULT_AGENT_SENSES.a2a },
26
+ telegram: configured.telegram ?? { ...identity_1.DEFAULT_AGENT_SENSES.telegram },
25
27
  workbench: configured.workbench ?? { ...identity_1.DEFAULT_AGENT_SENSES.workbench },
26
28
  };
27
29
  }
@@ -297,6 +297,7 @@ async function executeApprovalDecision(options) {
297
297
  catch (error) {
298
298
  if (!(error instanceof ApprovalExecutionFailedError))
299
299
  throw error;
300
+ await options.hooks?.afterHandler?.();
300
301
  return options.approvalStore.complete({
301
302
  approvalId: attempted.approvalId,
302
303
  ownerId: attempted.ownerId,
@@ -164,6 +164,7 @@ function readSenseStatusLines() {
164
164
  mail: configuredSenses.mail ?? { enabled: false },
165
165
  voice: configuredSenses.voice ?? { enabled: false },
166
166
  a2a: configuredSenses.a2a ?? { enabled: false },
167
+ telegram: configuredSenses.telegram ?? { enabled: false },
167
168
  workbench: configuredSenses.workbench ?? { enabled: false },
168
169
  };
169
170
  const payload = (0, config_1.loadConfig)();
@@ -210,6 +211,9 @@ function readSenseStatusLines() {
210
211
  ? openAIRealtimeVoiceReady
211
212
  : cascadeVoiceReady,
212
213
  a2a: true,
214
+ telegram: hasTextField(runtimePayload, "telegramBotToken")
215
+ && hasTextField(runtimePayload, "telegramAuthorizedUserId")
216
+ && hasTextField(runtimePayload, "telegramAuthorizedChatId"),
213
217
  workbench: false,
214
218
  };
215
219
  const rows = [
@@ -234,6 +238,10 @@ function readSenseStatusLines() {
234
238
  label: "A2A",
235
239
  status: senses.a2a.enabled ? "ready" : "disabled",
236
240
  },
241
+ {
242
+ label: "Telegram",
243
+ status: !senses.telegram.enabled ? "disabled" : configured.telegram ? "ready" : "needs_config",
244
+ },
237
245
  {
238
246
  label: "Workbench",
239
247
  // Workbench can be injected at runtime (the Workbench app spawns
@@ -348,6 +348,7 @@ const PROCESS_TYPE_LABELS = {
348
348
  voice: "voice handler",
349
349
  a2a: "a2a handler",
350
350
  mcp: "mcp bridge",
351
+ telegram: "telegram handler",
351
352
  };
352
353
  function processTypeLabel(channel) {
353
354
  return PROCESS_TYPE_LABELS[channel];
@@ -468,6 +469,7 @@ function localSenseStatusLines() {
468
469
  mail: configuredSenses.mail ?? { enabled: false },
469
470
  voice: configuredSenses.voice ?? { enabled: false },
470
471
  a2a: configuredSenses.a2a ?? { enabled: false },
472
+ telegram: configuredSenses.telegram ?? { enabled: false },
471
473
  workbench: configuredSenses.workbench ?? { enabled: false },
472
474
  };
473
475
  const payload = (0, config_1.loadConfig)();
@@ -493,6 +495,9 @@ function localSenseStatusLines() {
493
495
  && hasTextField(voice, "whisperCliPath")
494
496
  && hasTextField(voice, "whisperModelPath"),
495
497
  a2a: true,
498
+ telegram: hasTextField(runtimePayload, "telegramBotToken")
499
+ && hasTextField(runtimePayload, "telegramAuthorizedUserId")
500
+ && hasTextField(runtimePayload, "telegramAuthorizedChatId"),
496
501
  workbench: false,
497
502
  };
498
503
  const rows = [
@@ -517,6 +522,10 @@ function localSenseStatusLines() {
517
522
  label: "A2A",
518
523
  status: senses.a2a.enabled ? "ready" : "disabled",
519
524
  },
525
+ {
526
+ label: "Telegram",
527
+ status: !senses.telegram.enabled ? "disabled" : configured.telegram ? "ready" : "needs_config",
528
+ },
520
529
  {
521
530
  label: "Workbench",
522
531
  // Workbench can be injected at runtime (the Workbench app spawns
@@ -1,22 +1,22 @@
1
1
  "use strict";
2
2
  /**
3
- * Static source scanner for emitNervesEvent calls.
3
+ * Static source scanner for emitNervesEvent and emitNervesEventDurable calls.
4
4
  *
5
5
  * Extracts component:event keys from production source files by
6
- * regex-matching emitNervesEvent({ component: "...", event: "..." })
6
+ * regex-matching either static emitter with literal component/event fields.
7
7
  * calls. Only accepts static string literals (single or double quotes).
8
8
  * Template literals and variable references are rejected.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.scanSourceForNervesKeys = scanSourceForNervesKeys;
12
- const EMIT_CALL_RE = /emitNervesEvent\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
12
+ const EMIT_CALL_RE = /emitNervesEvent(?:Durable)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
13
13
  function extractStringLiteral(block, field) {
14
14
  const re = new RegExp(`${field}\\s*:\\s*(['"])((?:(?!\\1).)+)\\1`);
15
15
  const match = re.exec(block);
16
16
  return match ? match[2] : null;
17
17
  }
18
18
  /**
19
- * Scan a source file's content for emitNervesEvent calls and extract
19
+ * Scan a source file's content for synchronous or durable Nerves calls and extract
20
20
  * component:event keys. Only static string literals are accepted.
21
21
  */
22
22
  function scanSourceForNervesKeys(source) {
@@ -61,6 +61,24 @@ const LEVEL_PRIORITY = {
61
61
  warn: 30,
62
62
  error: 40,
63
63
  };
64
+ function fsyncFileAndDirectory(filePath) {
65
+ if ((0, fs_1.existsSync)(filePath)) {
66
+ const fileDescriptor = (0, fs_1.openSync)(filePath, "r");
67
+ try {
68
+ (0, fs_1.fsyncSync)(fileDescriptor);
69
+ }
70
+ finally {
71
+ (0, fs_1.closeSync)(fileDescriptor);
72
+ }
73
+ }
74
+ const directoryDescriptor = (0, fs_1.openSync)((0, path_1.dirname)(filePath), "r");
75
+ try {
76
+ (0, fs_1.fsyncSync)(directoryDescriptor);
77
+ }
78
+ finally {
79
+ (0, fs_1.closeSync)(directoryDescriptor);
80
+ }
81
+ }
64
82
  const GLOBAL_SINKS_KEY = Symbol.for("ouroboros.nerves.global-sinks");
65
83
  function resolveGlobalSinks() {
66
84
  const scope = globalThis;
@@ -248,6 +266,7 @@ function rotateIfNeeded(filePath, optionsOrMaxSize) {
248
266
  if ((0, fs_1.existsSync)(plain1)) {
249
267
  (0, fs_1.unlinkSync)(plain1);
250
268
  }
269
+ fsyncFileAndDirectory(filePath);
251
270
  (0, fs_1.renameSync)(filePath, plain1);
252
271
  // Step 3: gzip (or keep plain) the renamed file into the .1 generation.
253
272
  if (compress) {
@@ -259,8 +278,10 @@ function rotateIfNeeded(filePath, optionsOrMaxSize) {
259
278
  const buf = (0, fs_1.readFileSync)(plain1);
260
279
  const compressed = zlib.gzipSync(buf);
261
280
  (0, fs_1.writeFileSync)(gz1, compressed);
281
+ fsyncFileAndDirectory(gz1);
262
282
  (0, fs_1.unlinkSync)(plain1);
263
283
  }
284
+ fsyncFileAndDirectory(compress ? generationGzPath(base, ext, 1) : plain1);
264
285
  completed = true;
265
286
  (0, runtime_1.emitNervesEvent)({
266
287
  component: "nerves",
@@ -298,6 +319,33 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
298
319
  const queue = [];
299
320
  let flushing = false;
300
321
  let bytesSinceCheck = 0;
322
+ let enqueued = 0;
323
+ let completed = 0;
324
+ let appendFailure = null;
325
+ const barriers = [];
326
+ function settleBarriers() {
327
+ const ready = barriers.filter((barrier) => barrier.target <= completed);
328
+ if (ready.length === 0)
329
+ return;
330
+ for (const barrier of ready)
331
+ barriers.splice(barriers.indexOf(barrier), 1);
332
+ if (appendFailure) {
333
+ for (const barrier of ready)
334
+ barrier.reject(appendFailure);
335
+ return;
336
+ }
337
+ try {
338
+ fsyncFileAndDirectory(filePath);
339
+ for (const barrier of ready)
340
+ barrier.resolve();
341
+ }
342
+ catch (error) {
343
+ const failure = error instanceof Error ? error : new Error(String(error));
344
+ appendFailure = failure;
345
+ for (const barrier of ready)
346
+ barrier.reject(failure);
347
+ }
348
+ }
301
349
  function flush() {
302
350
  if (flushing || queue.length === 0)
303
351
  return;
@@ -310,31 +358,42 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
310
358
  try {
311
359
  rotateIfNeeded(filePath, options);
312
360
  }
313
- catch {
314
- // Rotation errors are surfaced via nerves events; never block writes.
361
+ catch (rotationError) {
362
+ appendFailure ??= rotationError instanceof Error ? rotationError : new Error(String(rotationError));
315
363
  }
316
364
  }
317
- (0, fs_1.appendFile)(filePath, line, "utf8", () => {
365
+ (0, fs_1.appendFile)(filePath, line, "utf8", (error) => {
366
+ completed += 1;
367
+ if (error)
368
+ appendFailure ??= error;
318
369
  if (checkAfterAppend) {
319
370
  try {
371
+ fsyncFileAndDirectory(filePath);
320
372
  rotateIfNeeded(filePath, options);
321
373
  }
322
- catch {
323
- // Rotation errors are surfaced via nerves events; never block writes.
374
+ catch (rotationError) {
375
+ appendFailure ??= rotationError instanceof Error ? rotationError : new Error(String(rotationError));
324
376
  }
325
377
  }
378
+ settleBarriers();
326
379
  flushing = false;
327
380
  flush();
328
381
  });
329
382
  }
330
- return (entry) => {
383
+ const sink = ((entry) => {
331
384
  const line = verbose
332
385
  ? `${JSON.stringify(entry)}\n`
333
386
  : `${(0, redact_1.redactString)(JSON.stringify((0, redact_1.redactLogEntry)(entry)))}\n`;
334
387
  bytesSinceCheck += line.length;
388
+ enqueued += 1;
335
389
  queue.push(line);
336
390
  flush();
337
- };
391
+ });
392
+ sink.barrier = () => new Promise((resolve, reject) => {
393
+ barriers.push({ target: enqueued, resolve, reject });
394
+ settleBarriers();
395
+ });
396
+ return sink;
338
397
  }
339
398
  function registerGlobalLogSink(sink) {
340
399
  globalSinks.add(sink);
@@ -356,9 +415,10 @@ function createLogger(options = {}) {
356
415
  const configuredLevel = options.level ?? "info";
357
416
  const sinks = options.sinks ?? [createStderrSink()];
358
417
  const sink = createFanoutSink(sinks);
418
+ const durableSinks = sinks.filter((candidate) => "barrier" in candidate && typeof candidate.barrier === "function");
359
419
  const now = options.now ?? (() => new Date());
360
- function emit(level, entry) {
361
- if (!shouldEmit(configuredLevel, level)) {
420
+ function emit(level, entry, force = false) {
421
+ if (!force && !shouldEmit(configuredLevel, level)) {
362
422
  return;
363
423
  }
364
424
  const payload = {
@@ -378,5 +438,11 @@ function createLogger(options = {}) {
378
438
  info: (entry) => emit("info", entry),
379
439
  warn: (entry) => emit("warn", entry),
380
440
  error: (entry) => emit("error", entry),
441
+ durabilityBarrier: async () => {
442
+ if (durableSinks.length === 0)
443
+ throw new Error("No durable Nerves sink is configured");
444
+ await Promise.all(durableSinks.map((durableSink) => durableSink.barrier()));
445
+ },
446
+ emitDurable: (level, entry) => emit(level, entry, true),
381
447
  };
382
448
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setRuntimeLogger = setRuntimeLogger;
4
4
  exports.emitNervesEvent = emitNervesEvent;
5
+ exports.emitNervesEventDurable = emitNervesEventDurable;
5
6
  const index_1 = require("./index");
6
7
  let runtimeLogger = null;
7
8
  function getRuntimeLogger() {
@@ -40,3 +41,14 @@ function emitNervesEvent(event) {
40
41
  logger.info(payload);
41
42
  }
42
43
  }
44
+ async function emitNervesEventDurable(event) {
45
+ const logger = getRuntimeLogger();
46
+ logger.emitDurable(event.level ?? "info", {
47
+ event: event.event,
48
+ trace_id: (0, index_1.ensureTraceId)(event.trace_id),
49
+ component: event.component,
50
+ message: event.message,
51
+ meta: event.meta ?? {},
52
+ });
53
+ await logger.durabilityBarrier();
54
+ }
@@ -16,6 +16,34 @@ const ajv = new ajv_1.default({
16
16
  useDefaults: false,
17
17
  });
18
18
  const validators = new WeakMap();
19
+ function unsupportedSchemaFeature(value, seen = new WeakSet()) {
20
+ if (value === null)
21
+ return "null schema values are unsupported";
22
+ if (typeof value !== "object")
23
+ return undefined;
24
+ if (seen.has(value))
25
+ return "cyclic schemas are unsupported";
26
+ seen.add(value);
27
+ if (Array.isArray(value)) {
28
+ for (const entry of value) {
29
+ const reason = unsupportedSchemaFeature(entry, seen);
30
+ if (reason)
31
+ return reason;
32
+ }
33
+ return undefined;
34
+ }
35
+ for (const [key, entry] of Object.entries(value)) {
36
+ if (key === "patternProperties")
37
+ return "patternProperties is unsupported";
38
+ if (key === "type" && (entry === "null" || (Array.isArray(entry) && entry.includes("null")))) {
39
+ return "null schema types are unsupported";
40
+ }
41
+ const reason = unsupportedSchemaFeature(entry, seen);
42
+ if (reason)
43
+ return reason;
44
+ }
45
+ return undefined;
46
+ }
19
47
  function canonicalize(value) {
20
48
  if (value === null || typeof value !== "object")
21
49
  return JSON.stringify(value);
@@ -33,6 +61,17 @@ function digestJson(value) {
33
61
  return digest(canonicalize(value));
34
62
  }
35
63
  function validateAdvertisedToolArguments(rawArguments, schema) {
64
+ const unsupported = unsupportedSchemaFeature(schema);
65
+ if (unsupported) {
66
+ (0, runtime_1.emitNervesEvent)({
67
+ level: "error",
68
+ component: "repertoire",
69
+ event: "repertoire.tool_schema_unsupported",
70
+ message: "advertised tool schema used an unsupported feature",
71
+ meta: { reason: unsupported },
72
+ });
73
+ return { ok: false, reason: `advertised schema is unsupported: ${unsupported}` };
74
+ }
36
75
  let parsed;
37
76
  try {
38
77
  parsed = JSON.parse(rawArguments);