@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.
- package/changelog.json +26 -0
- package/deploy/unraid/Dockerfile +7 -2
- package/deploy/unraid/README.txt +704 -64
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
- package/deploy/unraid/sanctuary.xml +13 -10
- package/dist/arc/cares.js +171 -37
- package/dist/arc/obligations.js +38 -0
- package/dist/heart/awaiting/await-alert.js +4 -2
- package/dist/heart/awaiting/await-expiry.js +39 -4
- package/dist/heart/awaiting/await-parser.js +8 -0
- package/dist/heart/core.js +37 -7
- package/dist/heart/cross-chat-delivery.js +1 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
- package/dist/heart/daemon/container-spec-auditor.js +38 -11
- package/dist/heart/daemon/daemon-entry.js +19 -5
- package/dist/heart/daemon/daemon.js +154 -22
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +126 -42
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
- package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
- package/dist/heart/external-events/router.js +1025 -43
- package/dist/heart/session-events.js +23 -5
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/tool-approval.js +3 -1
- package/dist/heart/turn-context.js +1 -0
- package/dist/mind/context.js +8 -3
- package/dist/mind/pending.js +25 -0
- package/dist/mind/prompt.js +49 -1
- package/dist/repertoire/guardrails.js +2 -1
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/tools-awaiting.js +229 -26
- package/dist/repertoire/tools-base.js +15 -0
- package/dist/repertoire/tools-continuity.js +259 -4
- package/dist/repertoire/tools-notes.js +29 -3
- package/dist/repertoire/tools-session.js +219 -4
- package/dist/repertoire/tools-steward-policy.js +88 -0
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-unraid.js +113 -6
- package/dist/repertoire/tools.js +104 -9
- package/dist/repertoire/unraid-restart.js +86 -4
- package/dist/senses/private-runtime-worker.js +36 -3
- package/dist/senses/private-runtime.js +310 -63
- package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
- package/dist/senses/sanctuary-health-runner.js +47 -103
- package/dist/senses/sanctuary-health.js +53 -105
- package/dist/senses/sanctuary-runtime.js +36 -2
- package/dist/senses/sanctuary-sab.js +80 -0
- package/dist/senses/shared-turn.js +64 -9
- package/dist/senses/telegram-admission.js +684 -0
- package/dist/senses/telegram-approval-runtime.js +46 -6
- package/dist/senses/telegram-client.js +212 -66
- package/dist/senses/telegram-effect-adapter.js +787 -0
- package/dist/senses/telegram.js +728 -48
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.telegramApprovalCommitBarrierHooks = telegramApprovalCommitBarrierHooks;
|
|
37
37
|
exports.telegramApprovalDecisionBarrierHooks = telegramApprovalDecisionBarrierHooks;
|
|
38
38
|
exports.approvalContinuationRunAgentOptions = approvalContinuationRunAgentOptions;
|
|
39
|
+
exports.formatTelegramApprovalPrompt = formatTelegramApprovalPrompt;
|
|
39
40
|
exports.executeApprovedTelegramTool = executeApprovedTelegramTool;
|
|
40
41
|
exports.createTelegramApprovalRuntime = createTelegramApprovalRuntime;
|
|
41
42
|
const path = __importStar(require("node:path"));
|
|
@@ -45,6 +46,7 @@ const approval_store_1 = require("../heart/approval-store");
|
|
|
45
46
|
const tool_approval_1 = require("../heart/tool-approval");
|
|
46
47
|
const core_1 = require("../heart/core");
|
|
47
48
|
const identity_1 = require("../heart/identity");
|
|
49
|
+
const session_events_1 = require("../heart/session-events");
|
|
48
50
|
const sanctuary_acceptance_marker_1 = require("../heart/daemon/sanctuary-acceptance-marker");
|
|
49
51
|
const telegram_1 = require("./telegram");
|
|
50
52
|
const context_1 = require("../mind/context");
|
|
@@ -69,6 +71,14 @@ function telegramApprovalDecisionBarrierHooks(effectBarrier) {
|
|
|
69
71
|
function approvalContinuationRunAgentOptions(toolContext, approvalCoordinator) {
|
|
70
72
|
return { toolContext: toolContext, approvalCoordinator };
|
|
71
73
|
}
|
|
74
|
+
function formatTelegramApprovalPrompt(toolName, args) {
|
|
75
|
+
if (toolName === "sanctuary_resume_download_queue")
|
|
76
|
+
return "Resume household downloads? This can spend prepaid download credit. I’ll verify the queue actually resumed.";
|
|
77
|
+
const container = args.container;
|
|
78
|
+
if (toolName === "unraid_restart_container" && typeof container === "string" && container.length > 0 && container.length <= 128 && !container.includes("\n"))
|
|
79
|
+
return `Restart ${container}?`;
|
|
80
|
+
return `Approve ${toolName} with exact arguments ${JSON.stringify(args)}?`;
|
|
81
|
+
}
|
|
72
82
|
async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDigest, approvalId, effectBarrier = () => undefined) {
|
|
73
83
|
if (name === "unraid_restart_container")
|
|
74
84
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -80,6 +90,21 @@ async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDi
|
|
|
80
90
|
try {
|
|
81
91
|
effectBarrier();
|
|
82
92
|
const result = await execute(name, args);
|
|
93
|
+
if (name === "sanctuary_resume_download_queue") {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(result);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new tool_approval_1.ApprovalExecutionFailedError("approved download resume returned an invalid result");
|
|
100
|
+
}
|
|
101
|
+
const data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
102
|
+
const outcome = data?.data && typeof data.data === "object" && !Array.isArray(data.data) ? data.data : null;
|
|
103
|
+
const after = outcome?.after && typeof outcome.after === "object" && !Array.isArray(outcome.after) ? outcome.after : null;
|
|
104
|
+
if (data?.ok !== true || outcome?.verified !== true || after?.paused !== false)
|
|
105
|
+
throw new tool_approval_1.ApprovalExecutionFailedError("approved download resume was not independently verified");
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
83
108
|
if (name !== "unraid_restart_container")
|
|
84
109
|
return result;
|
|
85
110
|
let parsed;
|
|
@@ -152,7 +177,9 @@ function createTelegramApprovalRuntime(options) {
|
|
|
152
177
|
const provider = options.dependencies?.runProvider ?? core_1.runAgent;
|
|
153
178
|
const resolveTool = options.dependencies?.resolveTool ?? tools_1.resolveToolDefinition;
|
|
154
179
|
const executeTool = options.dependencies?.executeTool ?? tools_1.execTool;
|
|
155
|
-
const
|
|
180
|
+
const agentRoot = options.dependencies?.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
|
|
181
|
+
const liveToolContext = { ...options.toolContext, agentRoot: options.toolContext.agentRoot ?? agentRoot };
|
|
182
|
+
const stateRoot = path.join(agentRoot, "state", "approvals");
|
|
156
183
|
const store = (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite"), now: () => new Date(now()) });
|
|
157
184
|
const checkpoints = new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json"));
|
|
158
185
|
const tokens = new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json"));
|
|
@@ -212,7 +239,7 @@ function createTelegramApprovalRuntime(options) {
|
|
|
212
239
|
preCallMessages: request.preCallMessages,
|
|
213
240
|
hooks: telegramApprovalCommitBarrierHooks(effectBarrier),
|
|
214
241
|
});
|
|
215
|
-
const prompt =
|
|
242
|
+
const prompt = formatTelegramApprovalPrompt(request.toolCall.function.name, request.arguments);
|
|
216
243
|
effectBarrier();
|
|
217
244
|
const actionDigest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify({ toolName: committed.record.toolName, argumentDigest: committed.record.argumentDigest })).digest("hex");
|
|
218
245
|
const targetDigest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify({ container: committed.record.arguments.container })).digest("hex");
|
|
@@ -269,6 +296,7 @@ function createTelegramApprovalRuntime(options) {
|
|
|
269
296
|
return { accepted: false, terminalText: "⚠️ Approval checkpoint is unavailable" };
|
|
270
297
|
const continuationOwnerId = `telegram-continuation-${(0, node_crypto_1.randomUUID)()}`;
|
|
271
298
|
let continuationEpoch = 0;
|
|
299
|
+
let continuationCausalEventId;
|
|
272
300
|
const continuationCoordinator = {
|
|
273
301
|
propose: (request) => coordinator({
|
|
274
302
|
sessionPath: record.sessionPath,
|
|
@@ -293,11 +321,18 @@ function createTelegramApprovalRuntime(options) {
|
|
|
293
321
|
markContinuationAttempted: () => { effectBarrier(); store.markContinuationAttempted({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
|
|
294
322
|
completeContinuation: () => { effectBarrier(); store.completeContinuation({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
|
|
295
323
|
runAgent: provider,
|
|
296
|
-
runAgentOptions: approvalContinuationRunAgentOptions(
|
|
297
|
-
persist: (messages, result) => {
|
|
324
|
+
runAgentOptions: approvalContinuationRunAgentOptions(liveToolContext, continuationCoordinator),
|
|
325
|
+
persist: (messages, result) => {
|
|
326
|
+
effectBarrier();
|
|
327
|
+
const existingEventIds = new Set((0, session_events_1.loadSessionEnvelopeFile)(record.sessionPath)?.events.map((event) => event.id) ?? []);
|
|
328
|
+
const events = (0, context_1.saveSession)(record.sessionPath, messages, result?.usage, undefined, lease);
|
|
329
|
+
continuationCausalEventId = result
|
|
330
|
+
? [...events].reverse().find((event) => event.role === "assistant" && !existingEventIds.has(event.id))?.id
|
|
331
|
+
: undefined;
|
|
332
|
+
},
|
|
298
333
|
deliver: async (text) => {
|
|
299
334
|
effectBarrier();
|
|
300
|
-
const messageIds = await (0,
|
|
335
|
+
const messageIds = await options.effects.sendText({ idempotencyKey: `approval:${record.approvalId}:continuation:${(0, node_crypto_1.createHash)("sha256").update(text).digest("hex")}`, chatId: options.authorizedChatId, text, authorClass: "butler", ...(continuationCausalEventId ? { causalEventId: continuationCausalEventId } : {}) });
|
|
301
336
|
effectBarrier();
|
|
302
337
|
if (acceptanceBinding) {
|
|
303
338
|
const unsigned = {
|
|
@@ -322,6 +357,7 @@ function createTelegramApprovalRuntime(options) {
|
|
|
322
357
|
};
|
|
323
358
|
transport = (0, telegram_client_1.createTelegramApprovalTransport)({
|
|
324
359
|
api: options.api,
|
|
360
|
+
effects: options.effects,
|
|
325
361
|
expectedUserId: options.authorizedUserId,
|
|
326
362
|
expectedChatId: options.authorizedChatId,
|
|
327
363
|
pendingStore,
|
|
@@ -375,11 +411,15 @@ function createTelegramApprovalRuntime(options) {
|
|
|
375
411
|
ownerId,
|
|
376
412
|
currentSessionRevision: (0, session_transaction_1.readSessionTransaction)(existing.sessionPath, lease).revision,
|
|
377
413
|
resolveTool,
|
|
414
|
+
resolveApprovalPolicy: (name, args) => (0, tools_1.approvalPolicyForInvocation)(name, args, liveToolContext),
|
|
378
415
|
liveGuard: async () => ({ ok: true }),
|
|
379
416
|
liveRisk: async () => ({ ok: true }),
|
|
380
417
|
hooks: telegramApprovalDecisionBarrierHooks(effectBarrier),
|
|
381
418
|
execute: (name, args) => {
|
|
382
|
-
const
|
|
419
|
+
const approvedToolContext = name === "unraid_restart_container"
|
|
420
|
+
? { ...options.toolContext, relationshipAuthorization: undefined }
|
|
421
|
+
: options.toolContext;
|
|
422
|
+
const execute = () => executeApprovedTelegramTool(name, args, (toolName, toolArgs) => executeTool(toolName, toolArgs, approvedToolContext), decisionScenarioDigest, existing.approvalId, effectBarrier);
|
|
383
423
|
return decisionScenarioDigest
|
|
384
424
|
? (0, sanctuary_acceptance_marker_1.runWithSanctuaryAcceptanceApproval)({ approvalId: existing.approvalId, argumentDigest: existing.argumentDigest }, execute)
|
|
385
425
|
: execute();
|
|
@@ -84,6 +84,11 @@ const TELEGRAM_UPDATE_DIGEST_DOMAIN = "ouroboros.telegram.update.v1";
|
|
|
84
84
|
const TELEGRAM_UPDATE_SEQUENCE_DOMAIN = "ouroboros.telegram.update-sequence.v1";
|
|
85
85
|
const TELEGRAM_UPDATE_DIGEST = /^tgu_[A-Za-z0-9_-]{43}$/u;
|
|
86
86
|
const TELEGRAM_UPDATE_SEQUENCE_DIGEST = /^tgs_[A-Za-z0-9_-]{43}$/u;
|
|
87
|
+
const TELEGRAM_ADMISSION_ID = /^[a-f0-9]{20}$/u;
|
|
88
|
+
const TELEGRAM_SESSION_EVENT_ID = /^evt-[0-9]{6,}$/u;
|
|
89
|
+
function boundedTelegramWorkText(value, maxLength) {
|
|
90
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
91
|
+
}
|
|
87
92
|
function updateReceipt(update) {
|
|
88
93
|
return {
|
|
89
94
|
digest: `tgu_${(0, node_crypto_1.createHash)("sha256").update(`${TELEGRAM_UPDATE_DIGEST_DOMAIN}\0${JSON.stringify(update)}`, "utf8").digest("base64url")}`,
|
|
@@ -147,6 +152,47 @@ class FileTelegramUpdateInboxStore {
|
|
|
147
152
|
&& Number.isSafeInteger(record.quarantinedAt)
|
|
148
153
|
&& record.quarantinedAt >= 0
|
|
149
154
|
&& typeof record.warningAcknowledged === "boolean";
|
|
155
|
+
const validAdmitted = (record) => Boolean(record)
|
|
156
|
+
&& typeof record === "object"
|
|
157
|
+
&& !Array.isArray(record)
|
|
158
|
+
&& Object.keys(record).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
|
|
159
|
+
&& TELEGRAM_ADMISSION_ID.test(record.admissionId)
|
|
160
|
+
&& boundedTelegramWorkText(record.friendId, 512)
|
|
161
|
+
&& boundedTelegramWorkText(record.sessionKey, 1_024)
|
|
162
|
+
&& TELEGRAM_SESSION_EVENT_ID.test(record.eventId)
|
|
163
|
+
&& record.reference === `telegram-admission:${record.admissionId}`;
|
|
164
|
+
const validIndeterminateAdmitted = (record) => {
|
|
165
|
+
if (!record || typeof record !== "object" || Array.isArray(record)
|
|
166
|
+
|| Object.keys(record).sort().join(",") !== "admissionId,eventId,friendId,quarantinedAt,reference,sessionKey")
|
|
167
|
+
return false;
|
|
168
|
+
const { quarantinedAt, ...work } = record;
|
|
169
|
+
return Number.isSafeInteger(quarantinedAt) && quarantinedAt >= 0 && validAdmitted(work);
|
|
170
|
+
};
|
|
171
|
+
if (value.version === 5) {
|
|
172
|
+
if (Object.keys(value).sort().join(",") !== "admittedDispatching,admittedIndeterminate,admittedPending,admittedSettled,dispatching,indeterminate,pending,settled,version"
|
|
173
|
+
|| !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate) || !Array.isArray(value.settled)
|
|
174
|
+
|| !Array.isArray(value.admittedPending) || !Array.isArray(value.admittedDispatching)
|
|
175
|
+
|| !Array.isArray(value.admittedSettled) || !Array.isArray(value.admittedIndeterminate)
|
|
176
|
+
|| !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt) || !value.settled.every(validReceipt)
|
|
177
|
+
|| !value.indeterminate.every(validIndeterminate)
|
|
178
|
+
|| !value.admittedPending.every(validAdmitted) || !value.admittedDispatching.every(validAdmitted)
|
|
179
|
+
|| !value.admittedSettled.every(validAdmitted) || !value.admittedIndeterminate.every(validIndeterminateAdmitted)) {
|
|
180
|
+
throw new Error("invalid bounded inbox shape");
|
|
181
|
+
}
|
|
182
|
+
const state = structuredClone(value);
|
|
183
|
+
const timestamp = this.timestamp();
|
|
184
|
+
const prunedUpdates = this.prune(state.indeterminate, timestamp);
|
|
185
|
+
const prunedAdmitted = state.admittedIndeterminate
|
|
186
|
+
.filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
|
|
187
|
+
.sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
|
|
188
|
+
.slice(-this.maxIndeterminateReceipts);
|
|
189
|
+
if (prunedUpdates.length !== state.indeterminate.length || prunedAdmitted.length !== state.admittedIndeterminate.length) {
|
|
190
|
+
state.indeterminate = prunedUpdates;
|
|
191
|
+
state.admittedIndeterminate = prunedAdmitted;
|
|
192
|
+
this.write(state);
|
|
193
|
+
}
|
|
194
|
+
return state;
|
|
195
|
+
}
|
|
150
196
|
if (value.version === 4) {
|
|
151
197
|
if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,settled,version"
|
|
152
198
|
|| !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate)
|
|
@@ -155,12 +201,17 @@ class FileTelegramUpdateInboxStore {
|
|
|
155
201
|
|| !value.settled.every(validReceipt)
|
|
156
202
|
|| !value.indeterminate.every(validIndeterminate))
|
|
157
203
|
throw new Error("invalid bounded inbox shape");
|
|
158
|
-
const state =
|
|
204
|
+
const state = {
|
|
205
|
+
...structuredClone(value),
|
|
206
|
+
version: 5,
|
|
207
|
+
admittedPending: [],
|
|
208
|
+
admittedDispatching: [],
|
|
209
|
+
admittedSettled: [],
|
|
210
|
+
admittedIndeterminate: [],
|
|
211
|
+
};
|
|
159
212
|
const pruned = this.prune(state.indeterminate, this.timestamp());
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.write(state);
|
|
163
|
-
}
|
|
213
|
+
state.indeterminate = pruned;
|
|
214
|
+
this.write(state);
|
|
164
215
|
return state;
|
|
165
216
|
}
|
|
166
217
|
if (value.version === 3) {
|
|
@@ -171,11 +222,12 @@ class FileTelegramUpdateInboxStore {
|
|
|
171
222
|
throw new Error("invalid bounded inbox shape");
|
|
172
223
|
const timestamp = this.timestamp();
|
|
173
224
|
const migrated = {
|
|
174
|
-
version:
|
|
225
|
+
version: 5,
|
|
175
226
|
pending: structuredClone(value.pending),
|
|
176
227
|
dispatching: structuredClone(value.dispatching),
|
|
177
228
|
settled: [],
|
|
178
229
|
indeterminate: this.prune(structuredClone(value.indeterminate), timestamp),
|
|
230
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
179
231
|
};
|
|
180
232
|
this.write(migrated);
|
|
181
233
|
return migrated;
|
|
@@ -190,7 +242,7 @@ class FileTelegramUpdateInboxStore {
|
|
|
190
242
|
throw new Error("invalid opaque inbox receipt");
|
|
191
243
|
const timestamp = this.timestamp();
|
|
192
244
|
const migrated = {
|
|
193
|
-
version:
|
|
245
|
+
version: 5,
|
|
194
246
|
pending: structuredClone(value.pending),
|
|
195
247
|
dispatching: structuredClone(value.dispatching),
|
|
196
248
|
settled: [],
|
|
@@ -199,6 +251,7 @@ class FileTelegramUpdateInboxStore {
|
|
|
199
251
|
quarantinedAt: timestamp,
|
|
200
252
|
warningAcknowledged: false,
|
|
201
253
|
})), timestamp),
|
|
254
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
202
255
|
};
|
|
203
256
|
this.write(migrated);
|
|
204
257
|
return migrated;
|
|
@@ -217,7 +270,7 @@ class FileTelegramUpdateInboxStore {
|
|
|
217
270
|
throw new Error("invalid legacy inbox record");
|
|
218
271
|
const timestamp = this.timestamp();
|
|
219
272
|
const migrated = {
|
|
220
|
-
version:
|
|
273
|
+
version: 5,
|
|
221
274
|
pending: [],
|
|
222
275
|
dispatching: [],
|
|
223
276
|
settled: [],
|
|
@@ -226,13 +279,17 @@ class FileTelegramUpdateInboxStore {
|
|
|
226
279
|
quarantinedAt: timestamp,
|
|
227
280
|
warningAcknowledged: false,
|
|
228
281
|
})), timestamp),
|
|
282
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
229
283
|
};
|
|
230
284
|
this.write(migrated);
|
|
231
285
|
return migrated;
|
|
232
286
|
}
|
|
233
287
|
catch (error) {
|
|
234
288
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
235
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
version: 5, pending: [], dispatching: [], settled: [], indeterminate: [],
|
|
291
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
292
|
+
};
|
|
236
293
|
throw new Error("Telegram update inbox state is corrupt", { cause: error });
|
|
237
294
|
}
|
|
238
295
|
}
|
|
@@ -322,6 +379,94 @@ class FileTelegramUpdateInboxStore {
|
|
|
322
379
|
state.settled = settled;
|
|
323
380
|
this.write(state);
|
|
324
381
|
}
|
|
382
|
+
captureAdmittedWork(work) {
|
|
383
|
+
if (!this.validAdmittedWork(work))
|
|
384
|
+
throw new Error("Telegram admitted work is invalid");
|
|
385
|
+
const state = this.read();
|
|
386
|
+
const existing = [
|
|
387
|
+
...state.admittedPending,
|
|
388
|
+
...state.admittedDispatching,
|
|
389
|
+
...state.admittedSettled,
|
|
390
|
+
...state.admittedIndeterminate,
|
|
391
|
+
].find((candidate) => candidate.admissionId === work.admissionId);
|
|
392
|
+
if (existing) {
|
|
393
|
+
const { quarantinedAt: _quarantinedAt, ...canonical } = existing;
|
|
394
|
+
if (JSON.stringify(canonical) !== JSON.stringify(work))
|
|
395
|
+
throw new Error("Telegram admitted work has a conflicting receipt");
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
state.admittedPending.push(structuredClone(work));
|
|
399
|
+
this.write(state);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
loadPendingAdmittedWork() {
|
|
403
|
+
return structuredClone(this.read().admittedPending);
|
|
404
|
+
}
|
|
405
|
+
claimAdmittedWork(admissionId) {
|
|
406
|
+
if (!TELEGRAM_ADMISSION_ID.test(admissionId))
|
|
407
|
+
throw new Error("Telegram admission id is invalid");
|
|
408
|
+
const state = this.read();
|
|
409
|
+
if (state.admittedDispatching.some((candidate) => candidate.admissionId === admissionId)
|
|
410
|
+
|| state.admittedSettled.some((candidate) => candidate.admissionId === admissionId)
|
|
411
|
+
|| state.admittedIndeterminate.some((candidate) => candidate.admissionId === admissionId))
|
|
412
|
+
return null;
|
|
413
|
+
const index = state.admittedPending.findIndex((candidate) => candidate.admissionId === admissionId);
|
|
414
|
+
if (index < 0)
|
|
415
|
+
return null;
|
|
416
|
+
const [work] = state.admittedPending.splice(index, 1);
|
|
417
|
+
state.admittedDispatching.push(work);
|
|
418
|
+
this.write(state);
|
|
419
|
+
return structuredClone(work);
|
|
420
|
+
}
|
|
421
|
+
completeAdmittedWork(admissionId) {
|
|
422
|
+
if (!TELEGRAM_ADMISSION_ID.test(admissionId))
|
|
423
|
+
throw new Error("Telegram admission id is invalid");
|
|
424
|
+
const state = this.read();
|
|
425
|
+
const index = state.admittedDispatching.findIndex((candidate) => candidate.admissionId === admissionId);
|
|
426
|
+
if (index < 0)
|
|
427
|
+
return;
|
|
428
|
+
const [work] = state.admittedDispatching.splice(index, 1);
|
|
429
|
+
state.admittedSettled.push(work);
|
|
430
|
+
state.admittedSettled = state.admittedSettled.slice(-this.maxIndeterminateReceipts);
|
|
431
|
+
this.write(state);
|
|
432
|
+
}
|
|
433
|
+
quarantineStrandedAdmittedWork() {
|
|
434
|
+
const state = this.read();
|
|
435
|
+
if (state.admittedDispatching.length === 0)
|
|
436
|
+
return [];
|
|
437
|
+
const timestamp = this.timestamp();
|
|
438
|
+
const stranded = state.admittedDispatching.map((work) => ({ ...work, quarantinedAt: timestamp }));
|
|
439
|
+
state.admittedDispatching = [];
|
|
440
|
+
state.admittedIndeterminate = [...state.admittedIndeterminate, ...stranded]
|
|
441
|
+
.filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
|
|
442
|
+
.sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
|
|
443
|
+
.slice(-this.maxIndeterminateReceipts);
|
|
444
|
+
this.write(state);
|
|
445
|
+
return stranded.map(({ quarantinedAt: _quarantinedAt, ...work }) => work);
|
|
446
|
+
}
|
|
447
|
+
admittedWorkState(admissionId) {
|
|
448
|
+
if (!TELEGRAM_ADMISSION_ID.test(admissionId))
|
|
449
|
+
throw new Error("Telegram admission id is invalid");
|
|
450
|
+
const state = this.read();
|
|
451
|
+
if (state.admittedPending.some((work) => work.admissionId === admissionId))
|
|
452
|
+
return "pending";
|
|
453
|
+
if (state.admittedDispatching.some((work) => work.admissionId === admissionId))
|
|
454
|
+
return "dispatching";
|
|
455
|
+
if (state.admittedSettled.some((work) => work.admissionId === admissionId))
|
|
456
|
+
return "settled";
|
|
457
|
+
if (state.admittedIndeterminate.some((work) => work.admissionId === admissionId))
|
|
458
|
+
return "indeterminate";
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
validAdmittedWork(work) {
|
|
462
|
+
return Boolean(work) && typeof work === "object" && !Array.isArray(work)
|
|
463
|
+
&& Object.keys(work).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
|
|
464
|
+
&& TELEGRAM_ADMISSION_ID.test(work.admissionId)
|
|
465
|
+
&& boundedTelegramWorkText(work.friendId, 512)
|
|
466
|
+
&& boundedTelegramWorkText(work.sessionKey, 1_024)
|
|
467
|
+
&& TELEGRAM_SESSION_EVENT_ID.test(work.eventId)
|
|
468
|
+
&& work.reference === `telegram-admission:${work.admissionId}`;
|
|
469
|
+
}
|
|
325
470
|
}
|
|
326
471
|
exports.FileTelegramUpdateInboxStore = FileTelegramUpdateInboxStore;
|
|
327
472
|
function createTelegramLongPoll(options) {
|
|
@@ -340,7 +485,38 @@ function createTelegramLongPoll(options) {
|
|
|
340
485
|
const chatId = message ? String(message.chat.id) : "";
|
|
341
486
|
if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId || typeof message.text !== "string")
|
|
342
487
|
return null;
|
|
343
|
-
return {
|
|
488
|
+
return {
|
|
489
|
+
updateId: update.update_id,
|
|
490
|
+
messageId: String(message.message_id),
|
|
491
|
+
userId,
|
|
492
|
+
chatId,
|
|
493
|
+
text: message.text,
|
|
494
|
+
...(Number.isSafeInteger(message.reply_to_message?.message_id) && message.reply_to_message.message_id > 0
|
|
495
|
+
? { replyToMessageId: String(message.reply_to_message.message_id) }
|
|
496
|
+
: {}),
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
const unknownMessage = (update) => {
|
|
500
|
+
const message = update.message;
|
|
501
|
+
if (!options.onUnknownMessage || !options.botId || !message?.from || message.chat.type !== "private")
|
|
502
|
+
return null;
|
|
503
|
+
const userId = String(message.from.id);
|
|
504
|
+
const chatId = String(message.chat.id);
|
|
505
|
+
if (userId === options.expectedUserId && chatId === options.expectedChatId)
|
|
506
|
+
return null;
|
|
507
|
+
const displayLabel = [message.from.first_name, message.from.last_name].filter(Boolean).join(" ")
|
|
508
|
+
|| (message.from.username ? `@${message.from.username}` : `Telegram user ${userId}`);
|
|
509
|
+
return {
|
|
510
|
+
updateId: update.update_id,
|
|
511
|
+
messageId: message.message_id,
|
|
512
|
+
botId: options.botId,
|
|
513
|
+
userId,
|
|
514
|
+
chatId,
|
|
515
|
+
text: message.text ?? message.caption ?? "",
|
|
516
|
+
displayLabel,
|
|
517
|
+
hasAttachments: Boolean(message.document || message.photo?.length || message.audio || message.video
|
|
518
|
+
|| message.voice || message.animation || message.sticker),
|
|
519
|
+
};
|
|
344
520
|
};
|
|
345
521
|
const authorizedCallback = (update) => {
|
|
346
522
|
const callback = update.callback_query;
|
|
@@ -349,7 +525,9 @@ function createTelegramLongPoll(options) {
|
|
|
349
525
|
&& String(callback.message.chat.id) === options.expectedChatId);
|
|
350
526
|
};
|
|
351
527
|
const dispatch = async (update) => {
|
|
352
|
-
const handled =
|
|
528
|
+
const handled = !update.callback_query || authorizedCallback(update)
|
|
529
|
+
? await options.onUpdate?.(update) ?? false
|
|
530
|
+
: false;
|
|
353
531
|
if (handled)
|
|
354
532
|
return;
|
|
355
533
|
const message = authorizedMessage(update);
|
|
@@ -357,6 +535,11 @@ function createTelegramLongPoll(options) {
|
|
|
357
535
|
await options.onMessage(message);
|
|
358
536
|
return;
|
|
359
537
|
}
|
|
538
|
+
const stranger = unknownMessage(update);
|
|
539
|
+
if (stranger) {
|
|
540
|
+
await options.onUnknownMessage(stranger);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
360
543
|
const distinctAccount = Boolean(update.message?.from && String(update.message.from.id) !== options.expectedUserId);
|
|
361
544
|
(0, runtime_1.emitNervesEvent)({
|
|
362
545
|
component: "senses",
|
|
@@ -399,7 +582,7 @@ function createTelegramLongPoll(options) {
|
|
|
399
582
|
continue;
|
|
400
583
|
options.onBeforeDispatch?.();
|
|
401
584
|
const next = update.update_id + 1;
|
|
402
|
-
const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update));
|
|
585
|
+
const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update) || unknownMessage(update));
|
|
403
586
|
const newlyCaptured = requiresDurableDispatch ? (options.inboxStore?.capture(update) ?? true) : true;
|
|
404
587
|
if (newlyCaptured) {
|
|
405
588
|
if (requiresDurableDispatch && options.inboxStore && !options.inboxStore.claim(update)) {
|
|
@@ -833,31 +1016,14 @@ function createTelegramApprovalTransport(options) {
|
|
|
833
1016
|
if (pending.messageId === null)
|
|
834
1017
|
return now();
|
|
835
1018
|
const terminalEditStartedAt = now();
|
|
836
|
-
const base = {
|
|
837
|
-
chat_id: options.expectedChatId,
|
|
838
|
-
message_id: Number(pending.messageId),
|
|
839
|
-
reply_markup: { inline_keyboard: [] },
|
|
840
|
-
};
|
|
841
|
-
const editSignal = AbortSignal.timeout(exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS);
|
|
842
1019
|
try {
|
|
843
1020
|
effectBarrier();
|
|
844
|
-
await options.
|
|
1021
|
+
await options.effects.edit({ idempotencyKey: `approval:${pending.approvalId}:edit:${(0, node_crypto_1.createHash)("sha256").update(terminalText).digest("hex")}`, chatId: options.expectedChatId, messageId: Number(pending.messageId), text: terminalText });
|
|
845
1022
|
}
|
|
846
1023
|
catch (error) {
|
|
847
1024
|
const alreadyTerminal = error instanceof TelegramApiError && error.status === 400 && /message is not modified/i.test(error.message);
|
|
848
|
-
if (!alreadyTerminal)
|
|
849
|
-
|
|
850
|
-
throw error;
|
|
851
|
-
try {
|
|
852
|
-
effectBarrier();
|
|
853
|
-
await options.api.request("editMessageText", { ...base, text: terminalText }, editSignal);
|
|
854
|
-
}
|
|
855
|
-
catch (fallbackError) {
|
|
856
|
-
if (!(fallbackError instanceof TelegramApiError) || fallbackError.status !== 400 || !/message is not modified/i.test(fallbackError.message)) {
|
|
857
|
-
throw fallbackError;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
1025
|
+
if (!alreadyTerminal)
|
|
1026
|
+
throw error;
|
|
861
1027
|
}
|
|
862
1028
|
const terminalizedAt = now();
|
|
863
1029
|
if (pending.acceptanceBinding)
|
|
@@ -876,11 +1042,11 @@ function createTelegramApprovalTransport(options) {
|
|
|
876
1042
|
const acknowledge = async (callbackQueryId, invalid) => {
|
|
877
1043
|
try {
|
|
878
1044
|
effectBarrier();
|
|
879
|
-
await options.
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
1045
|
+
await options.effects.acknowledge({
|
|
1046
|
+
idempotencyKey: `approval-callback:${(0, node_crypto_1.createHash)("sha256").update(callbackQueryId).digest("hex")}`,
|
|
1047
|
+
callbackQueryId,
|
|
1048
|
+
...(invalid ? { text: "This approval is no longer valid.", showAlert: true } : {}),
|
|
1049
|
+
});
|
|
884
1050
|
return "acknowledged";
|
|
885
1051
|
}
|
|
886
1052
|
catch (error) {
|
|
@@ -1027,16 +1193,6 @@ function createTelegramApprovalTransport(options) {
|
|
|
1027
1193
|
if (pendingByCallback.has(approveCallbackData) || pendingByCallback.has(denyCallbackData)) {
|
|
1028
1194
|
throw new Error("Telegram approval callback handle collision");
|
|
1029
1195
|
}
|
|
1030
|
-
const replyMarkup = { inline_keyboard: [[
|
|
1031
|
-
{ text: "Approve", callback_data: approveCallbackData },
|
|
1032
|
-
{ text: "Deny", callback_data: denyCallbackData },
|
|
1033
|
-
]] };
|
|
1034
|
-
const htmlBody = {
|
|
1035
|
-
chat_id: options.expectedChatId,
|
|
1036
|
-
text: escapeTelegramHtml(input.prompt),
|
|
1037
|
-
parse_mode: "HTML",
|
|
1038
|
-
reply_markup: replyMarkup,
|
|
1039
|
-
};
|
|
1040
1196
|
const pending = {
|
|
1041
1197
|
approvalId: input.approvalId,
|
|
1042
1198
|
decisionToken: input.decisionToken,
|
|
@@ -1065,26 +1221,16 @@ function createTelegramApprovalTransport(options) {
|
|
|
1065
1221
|
throw error;
|
|
1066
1222
|
}
|
|
1067
1223
|
try {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
effectBarrier();
|
|
1077
|
-
result = await options.api.request("sendMessage", {
|
|
1078
|
-
chat_id: options.expectedChatId,
|
|
1079
|
-
text: input.prompt,
|
|
1080
|
-
reply_markup: replyMarkup,
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
if (!result || typeof result !== "object" || Array.isArray(result) || !("message_id" in result)
|
|
1084
|
-
|| typeof result.message_id !== "number" || !Number.isSafeInteger(result.message_id) || result.message_id <= 0) {
|
|
1224
|
+
effectBarrier();
|
|
1225
|
+
const messageId = await options.effects.sendCard({
|
|
1226
|
+
idempotencyKey: `approval:${input.approvalId}:prompt`,
|
|
1227
|
+
chatId: options.expectedChatId,
|
|
1228
|
+
text: input.prompt,
|
|
1229
|
+
buttons: [[{ text: "Approve", callbackData: approveCallbackData }, { text: "Deny", callbackData: denyCallbackData }]],
|
|
1230
|
+
});
|
|
1231
|
+
if (!Number.isSafeInteger(messageId) || messageId <= 0)
|
|
1085
1232
|
throw new Error("Telegram sendMessage response did not include a canonical message_id");
|
|
1086
|
-
|
|
1087
|
-
pending.messageId = String(result.message_id);
|
|
1233
|
+
pending.messageId = String(messageId);
|
|
1088
1234
|
pending.deliveryState = "bound";
|
|
1089
1235
|
if (input.acceptanceBinding) {
|
|
1090
1236
|
const boundAt = now();
|