@ouro.bot/cli 0.1.0-alpha.816 → 0.1.0-alpha.817

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 (49) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +10 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +280 -60
  5. package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
  8. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  9. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  10. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
  11. package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  13. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  14. package/deploy/unraid/sanctuary.xml +2 -1
  15. package/dist/heart/core.js +2 -1
  16. package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
  17. package/dist/heart/daemon/container-spec-auditor.js +16 -9
  18. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  19. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  20. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  21. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  22. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  23. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  24. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  25. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  26. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  27. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  28. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  29. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  30. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  31. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  33. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  36. package/dist/heart/daemon/sense-manager.js +20 -10
  37. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  38. package/dist/repertoire/tools.js +15 -6
  39. package/dist/senses/root-host-approval-port.js +345 -0
  40. package/dist/senses/root-host-approval-runtime.js +393 -0
  41. package/dist/senses/sanctuary-authority-resident.js +102 -0
  42. package/dist/senses/telegram-admission.js +7 -0
  43. package/dist/senses/telegram-attachments.js +5 -1
  44. package/dist/senses/telegram-authority-transport.js +231 -0
  45. package/dist/senses/telegram-client.js +31 -8
  46. package/dist/senses/telegram-entry.js +1 -1
  47. package/dist/senses/telegram.js +168 -28
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSanctuaryTelegramAuthorityTransport = createSanctuaryTelegramAuthorityTransport;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const runtime_1 = require("../nerves/runtime");
6
+ const sanctuary_authority_codec_1 = require("../heart/daemon/sanctuary-authority-codec");
7
+ const root_host_approval_port_1 = require("./root-host-approval-port");
8
+ function isObject(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function validPollBody(body) {
12
+ return Object.keys(body).sort().join(",") === "allowed_updates,offset,timeout"
13
+ && Number.isSafeInteger(body.offset)
14
+ && body.offset >= 0
15
+ && body.timeout === 50
16
+ && Array.isArray(body.allowed_updates)
17
+ && body.allowed_updates.length === 2
18
+ && body.allowed_updates[0] === "message"
19
+ && body.allowed_updates[1] === "callback_query";
20
+ }
21
+ function rawUpdateDigest(update) {
22
+ return `tgu_${(0, node_crypto_1.createHash)("sha256")
23
+ .update(`ouroboros.telegram.update.v1\0${JSON.stringify(update)}`, "utf8")
24
+ .digest("base64url")}`;
25
+ }
26
+ function exactKeys(value, keys) {
27
+ const actual = Object.keys(value).sort();
28
+ const expected = [...keys].sort();
29
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
30
+ }
31
+ function canonicalTime(value) {
32
+ if (typeof value !== "string")
33
+ return false;
34
+ const instant = new Date(value);
35
+ return !Number.isNaN(instant.getTime()) && instant.toISOString() === value;
36
+ }
37
+ function verifyObservation(artifact, update, verification) {
38
+ let payload;
39
+ try {
40
+ payload = (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
41
+ artifact,
42
+ expectedDomain: "ouro.sanctuary.telegram-observation.v1",
43
+ expectedKeyId: verification.expectedKeyId,
44
+ publicKey: verification.publicKey,
45
+ });
46
+ }
47
+ catch (error) {
48
+ throw new Error("Sanctuary Telegram authority poll response is invalid", { cause: error });
49
+ }
50
+ if (!isObject(payload)
51
+ || !exactKeys(payload, [
52
+ "targetHost", "botId", "updateId", "updateClass", "userId", "chatId", "ownerEligible",
53
+ "messageId", "callbackQueryId", "rawUpdateDigest", "observedAt", "settlement", "nonce", "publicKeyDigest",
54
+ ...(Object.hasOwn(payload, "deliveryUpdateDigest") ? ["deliveryUpdateDigest"] : []),
55
+ ])
56
+ || payload.targetHost !== verification.expectedTargetHost
57
+ || payload.botId !== verification.expectedBotId
58
+ || payload.publicKeyDigest !== verification.expectedPublicKeyDigest
59
+ || payload.updateId !== update.update_id
60
+ || !["message", "callback"].includes(String(payload.updateClass))
61
+ || typeof payload.userId !== "string"
62
+ || typeof payload.chatId !== "string"
63
+ || typeof payload.ownerEligible !== "boolean"
64
+ || (payload.messageId !== null && typeof payload.messageId !== "string")
65
+ || (payload.callbackQueryId !== null && typeof payload.callbackQueryId !== "string")
66
+ || typeof payload.rawUpdateDigest !== "string"
67
+ || !/^tgu_[A-Za-z0-9_-]{43}$/u.test(payload.rawUpdateDigest)
68
+ || (Object.hasOwn(payload, "deliveryUpdateDigest") && typeof payload.deliveryUpdateDigest !== "string")
69
+ || (payload.deliveryUpdateDigest ?? payload.rawUpdateDigest) !== rawUpdateDigest(update)
70
+ || !canonicalTime(payload.observedAt)
71
+ || payload.settlement !== "pending"
72
+ || typeof payload.nonce !== "string"
73
+ || !/^[A-Za-z0-9_-]{43}$/u.test(payload.nonce)) {
74
+ throw new Error("Sanctuary Telegram authority observation payload is invalid");
75
+ }
76
+ const callback = update.callback_query;
77
+ const message = update.message;
78
+ if ((!callback && !message) || (callback && message) || (callback && (!callback.message || !callback.from)) || (message && !message.from)) {
79
+ throw new Error("Sanctuary Telegram authority observation update is unsupported");
80
+ }
81
+ const updateClass = callback ? "callback" : "message";
82
+ const userId = String(callback ? callback.from.id : message.from.id);
83
+ const chatId = String(callback ? callback.message.chat.id : message.chat.id);
84
+ const messageId = String(callback ? callback.message.message_id : message.message_id);
85
+ const callbackQueryId = callback ? callback.id : null;
86
+ if (payload.updateClass !== updateClass
87
+ || payload.userId !== userId
88
+ || payload.chatId !== chatId
89
+ || payload.messageId !== messageId
90
+ || payload.callbackQueryId !== callbackQueryId
91
+ || payload.ownerEligible !== (userId === verification.expectedOwnerUserId && chatId === verification.expectedOwnerChatId)) {
92
+ throw new Error("Sanctuary Telegram authority observation coordinates changed");
93
+ }
94
+ return Object.freeze({
95
+ schemaVersion: 1,
96
+ observationDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(artifact.domain, payload),
97
+ targetHost: payload.targetHost,
98
+ botId: payload.botId,
99
+ updateId: payload.updateId,
100
+ updateClass: payload.updateClass,
101
+ userId: payload.userId,
102
+ chatId: payload.chatId,
103
+ ownerEligible: payload.ownerEligible,
104
+ messageId: payload.messageId,
105
+ callbackQueryId: payload.callbackQueryId,
106
+ rawUpdateDigest: payload.rawUpdateDigest,
107
+ ...(payload.deliveryUpdateDigest ? { deliveryUpdateDigest: payload.deliveryUpdateDigest } : {}),
108
+ observedAt: payload.observedAt,
109
+ keyId: artifact.keyId,
110
+ publicKeyDigest: payload.publicKeyDigest,
111
+ });
112
+ }
113
+ function createSanctuaryTelegramAuthorityTransport(client, verification) {
114
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_authority_transport_created", message: "Tokenless Sanctuary Telegram authority transport created" });
115
+ verification = Object.freeze({ ...verification });
116
+ const observations = new Map();
117
+ const metadata = new Map();
118
+ const callbacks = new Map();
119
+ let currentObservation = null;
120
+ let stopped = false;
121
+ const api = {
122
+ async request(method, body, signal) {
123
+ if (signal?.aborted)
124
+ throw signal.reason;
125
+ if (stopped)
126
+ throw new Error("Sanctuary Telegram authority transport is stopped");
127
+ if (method !== "getUpdates") {
128
+ return await client.request("telegram.request", {
129
+ method,
130
+ body,
131
+ ...(currentObservation ? {
132
+ observation: {
133
+ updateId: currentObservation.payload.updateId,
134
+ observationDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(currentObservation.domain, currentObservation.payload),
135
+ },
136
+ } : {}),
137
+ });
138
+ }
139
+ if (!validPollBody(body))
140
+ throw new Error("Sanctuary Telegram authority poll request is invalid");
141
+ const result = await client.request("telegram.poll", {});
142
+ if (result === null)
143
+ return [];
144
+ if (!isObject(result)
145
+ || !isObject(result.observation)
146
+ || !isObject(result.observation.payload)
147
+ || !isObject(result.update)
148
+ || !Number.isSafeInteger(result.update.update_id)
149
+ || result.observation.payload.updateId !== result.update.update_id) {
150
+ throw new Error("Sanctuary Telegram authority poll response is invalid");
151
+ }
152
+ const update = result.update;
153
+ const observation = result.observation;
154
+ const verifiedMetadata = verifyObservation(observation, update, verification);
155
+ const existing = observations.get(update.update_id);
156
+ if (existing
157
+ && (0, sanctuary_authority_codec_1.authorityArtifactDigest)(existing.domain, existing.payload)
158
+ !== (0, sanctuary_authority_codec_1.authorityArtifactDigest)(observation.domain, observation.payload)) {
159
+ throw new Error("Sanctuary Telegram authority observation changed during redelivery");
160
+ }
161
+ observations.set(update.update_id, observation);
162
+ metadata.set(update.update_id, verifiedMetadata);
163
+ callbacks.set(update.update_id, result.hostCallback);
164
+ currentObservation = observation;
165
+ return [update];
166
+ },
167
+ stop() {
168
+ if (stopped)
169
+ return;
170
+ stopped = true;
171
+ client.close();
172
+ },
173
+ };
174
+ const transport = {
175
+ api,
176
+ async downloadFile(filePath) {
177
+ const result = await client.request("telegram.file", { filePath });
178
+ if (!isObject(result)
179
+ || typeof result.bodyBase64 !== "string"
180
+ || (result.contentType !== undefined && typeof result.contentType !== "string")) {
181
+ throw new Error("Sanctuary Telegram authority file response is invalid");
182
+ }
183
+ const body = Buffer.from(result.bodyBase64, "base64");
184
+ if (body.toString("base64") !== result.bodyBase64 || body.length > 20_000_000) {
185
+ throw new Error("Sanctuary Telegram authority file response is invalid");
186
+ }
187
+ return new Response(body, {
188
+ headers: result.contentType ? { "content-type": result.contentType } : undefined,
189
+ });
190
+ },
191
+ async admitChat(input) {
192
+ await client.request("telegram.chat.admit", input);
193
+ },
194
+ async revokeChat(input) {
195
+ await client.request("telegram.chat.revoke", input);
196
+ },
197
+ metadataForUpdate(update) {
198
+ const value = metadata.get(update.update_id);
199
+ if (!value)
200
+ return null;
201
+ if ((value.deliveryUpdateDigest ?? value.rawUpdateDigest) !== rawUpdateDigest(update)) {
202
+ throw new Error("Sanctuary Telegram authority update changed after verification");
203
+ }
204
+ return value;
205
+ },
206
+ async settleTransport(update, outcome) {
207
+ const observation = observations.get(update.update_id);
208
+ if (!observation)
209
+ throw new Error("Sanctuary Telegram authority observation is unavailable for settlement");
210
+ await client.request("telegram.settle", {
211
+ updateId: update.update_id,
212
+ observationDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(observation.domain, observation.payload),
213
+ outcome,
214
+ });
215
+ observations.delete(update.update_id);
216
+ metadata.delete(update.update_id);
217
+ callbacks.delete(update.update_id);
218
+ if (currentObservation?.payload.updateId === update.update_id)
219
+ currentObservation = null;
220
+ },
221
+ };
222
+ transport.hostApproval = (0, root_host_approval_port_1.createRootHostApprovalPort)(client, verification, {
223
+ current: () => currentObservation ? metadata.get(currentObservation.payload.updateId) : null,
224
+ lookup: (update) => {
225
+ const value = transport.metadataForUpdate(update);
226
+ return value ? { metadata: value, hostCallback: callbacks.get(update.update_id) } : null;
227
+ },
228
+ stopped: () => stopped,
229
+ });
230
+ return transport;
231
+ }
@@ -507,7 +507,7 @@ function createTelegramLongPoll(options) {
507
507
  const rawAttachmentCount = (message) => [
508
508
  message.document, message.audio, message.video, message.voice, message.animation, message.sticker,
509
509
  ].filter(Boolean).length + (message.photo?.length ? 1 : 0);
510
- const authorizedMessage = (update) => {
510
+ const authorizedMessage = (update, authority) => {
511
511
  const message = update.message;
512
512
  const userId = message?.from ? String(message.from.id) : "";
513
513
  const chatId = message ? String(message.chat.id) : "";
@@ -532,9 +532,10 @@ function createTelegramLongPoll(options) {
532
532
  ...(Number.isSafeInteger(message.reply_to_message?.message_id) && message.reply_to_message.message_id > 0
533
533
  ? { replyToMessageId: String(message.reply_to_message.message_id) }
534
534
  : {}),
535
+ ...(authority ? { authority } : {}),
535
536
  };
536
537
  };
537
- const unknownMessage = (update) => {
538
+ const unknownMessage = (update, authority) => {
538
539
  const message = update.message;
539
540
  if (!options.onUnknownMessage || !options.botId || !message?.from || message.chat.type !== "private")
540
541
  return null;
@@ -557,6 +558,7 @@ function createTelegramLongPoll(options) {
557
558
  hasAttachments: attachmentCount > 0,
558
559
  attachments,
559
560
  ...(attachmentCount > attachments.length ? { attachmentNotices: ["attachment unavailable: Telegram media metadata was incomplete"] } : {}),
561
+ ...(authority ? { authority } : {}),
560
562
  };
561
563
  };
562
564
  const authorizedCallback = (update) => {
@@ -566,17 +568,18 @@ function createTelegramLongPoll(options) {
566
568
  && String(callback.message.chat.id) === options.expectedChatId);
567
569
  };
568
570
  const dispatch = async (update) => {
571
+ const authority = options.transportMetadata?.(update) ?? undefined;
569
572
  const handled = !update.callback_query || authorizedCallback(update)
570
- ? await options.onUpdate?.(update) ?? false
573
+ ? await options.onUpdate?.(update, authority) ?? false
571
574
  : false;
572
575
  if (handled)
573
576
  return;
574
- const message = authorizedMessage(update);
577
+ const message = authorizedMessage(update, authority);
575
578
  if (message) {
576
579
  await options.onMessage(message);
577
580
  return;
578
581
  }
579
- const stranger = unknownMessage(update);
582
+ const stranger = unknownMessage(update, authority);
580
583
  if (stranger) {
581
584
  await options.onUnknownMessage(stranger);
582
585
  return;
@@ -619,15 +622,18 @@ function createTelegramLongPoll(options) {
619
622
  if (!Array.isArray(updates))
620
623
  throw new Error("Telegram getUpdates result must be an array");
621
624
  for (const update of updates) {
622
- if (!update || !Number.isSafeInteger(update.update_id) || update.update_id < nextUpdateId)
625
+ if (!update || !Number.isSafeInteger(update.update_id) || (update.update_id < nextUpdateId && !options.settleTransport))
623
626
  continue;
624
627
  options.onBeforeDispatch?.();
625
- const next = update.update_id + 1;
628
+ const next = Math.max(nextUpdateId, update.update_id + 1);
626
629
  const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update) || unknownMessage(update));
627
630
  const newlyCaptured = requiresDurableDispatch ? (options.inboxStore?.capture(update) ?? true) : true;
628
631
  if (newlyCaptured) {
629
632
  if (requiresDurableDispatch && options.inboxStore && !options.inboxStore.claim(update)) {
630
633
  options.onDispatchSettled?.();
634
+ const indeterminate = options.inboxStore.loadIndeterminate()
635
+ .some((receipt) => sameReceipt(receipt, updateReceipt(update)));
636
+ await options.settleTransport?.(update, indeterminate ? "indeterminate" : "completed");
631
637
  options.offsetStore.save(next);
632
638
  nextUpdateId = next;
633
639
  options.inboxStore.commit?.(update);
@@ -651,11 +657,28 @@ function createTelegramLongPoll(options) {
651
657
  throw new AggregateError([dispatchError, auditError], "Telegram dispatch and acceptance audit verification failed");
652
658
  throw auditError;
653
659
  }
660
+ try {
661
+ if (options.settleTransport) {
662
+ await options.settleTransport?.(update, dispatchError === undefined ? "completed" : "indeterminate");
663
+ }
664
+ }
665
+ catch (settlementError) {
666
+ if (dispatchError !== undefined)
667
+ throw new AggregateError([dispatchError, settlementError], "Telegram dispatch and transport settlement failed");
668
+ throw settlementError;
669
+ }
654
670
  if (dispatchError !== undefined)
655
671
  throw dispatchError;
656
672
  }
657
- else
673
+ else {
658
674
  options.onDispatchSettled?.();
675
+ if (options.settleTransport) {
676
+ // Only an existing inbox can report that this update was already captured.
677
+ const indeterminate = options.inboxStore.loadIndeterminate()
678
+ .some((receipt) => sameReceipt(receipt, updateReceipt(update)));
679
+ await options.settleTransport(update, indeterminate ? "indeterminate" : "completed");
680
+ }
681
+ }
659
682
  options.offsetStore.save(next);
660
683
  nextUpdateId = next;
661
684
  if (requiresDurableDispatch)
@@ -61,7 +61,7 @@ Promise.all([
61
61
  }
62
62
  const machine = loadOrCreateMachineIdentity();
63
63
  await refreshMachineRuntimeCredentialConfig(agentName, machine.machineId, { preserveCachedOnFailure: true }).catch(() => undefined);
64
- const app = await startTelegramSenseApp(agentName);
64
+ const app = await startTelegramSenseApp(agentName, true);
65
65
  let stopping;
66
66
  const stop = () => { stopping ??= app.stop(); };
67
67
  process.once("SIGTERM", stop);