@ouro.bot/cli 0.1.0-alpha.815 → 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.
- package/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +18 -0
- package/deploy/unraid/Dockerfile +6 -0
- package/deploy/unraid/README.txt +305 -365
- package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
- package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
- package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/approval-store.js +11 -1
- package/dist/heart/core.js +2 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +9 -8
- package/dist/heart/daemon/container-spec-auditor.js +17 -24
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
- package/dist/heart/daemon/sense-manager.js +20 -10
- package/dist/heart/external-events/router.js +31 -15
- package/dist/heart/steward-policy.js +374 -58
- package/dist/heart/tool-approval.js +8 -1
- package/dist/repertoire/relationship-authorization.js +128 -0
- package/dist/repertoire/tools-base.js +9 -13
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools-steward-policy.js +34 -10
- package/dist/repertoire/tools-unraid.js +79 -32
- package/dist/repertoire/tools.js +37 -25
- package/dist/repertoire/unraid-restart.js +179 -52
- package/dist/senses/private-runtime.js +27 -12
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -0
- package/dist/senses/sanctuary-health-runner.js +0 -1
- package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
- package/dist/senses/sanctuary-runtime.js +2 -0
- package/dist/senses/telegram-admission.js +7 -0
- package/dist/senses/telegram-approval-runtime.js +130 -12
- package/dist/senses/telegram-attachments.js +5 -1
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +31 -8
- package/dist/senses/telegram-entry.js +1 -1
- package/dist/senses/telegram.js +174 -28
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -52,9 +52,11 @@ const sanctuary_acceptance_marker_1 = require("../heart/daemon/sanctuary-accepta
|
|
|
52
52
|
const telegram_1 = require("./telegram");
|
|
53
53
|
const context_1 = require("../mind/context");
|
|
54
54
|
const session_transaction_1 = require("../mind/session-transaction");
|
|
55
|
+
const relationship_authorization_1 = require("../repertoire/relationship-authorization");
|
|
55
56
|
const tools_1 = require("../repertoire/tools");
|
|
56
57
|
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
57
58
|
const runtime_1 = require("../nerves/runtime");
|
|
59
|
+
const shared_turn_1 = require("./shared-turn");
|
|
58
60
|
const telegram_client_1 = require("./telegram-client");
|
|
59
61
|
function telegramApprovalCommitBarrierHooks(effectBarrier) {
|
|
60
62
|
return {
|
|
@@ -190,11 +192,11 @@ function createTelegramApprovalRuntime(options) {
|
|
|
190
192
|
const resolveTool = options.dependencies?.resolveTool ?? tools_1.resolveToolDefinition;
|
|
191
193
|
const agentRoot = options.dependencies?.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
|
|
192
194
|
const owner = Object.freeze({ agentName: options.agentName, agentRoot });
|
|
193
|
-
const currentOptions = async (record) => {
|
|
195
|
+
const currentOptions = async (record, phase) => {
|
|
194
196
|
try {
|
|
195
197
|
if (!options.resolveLiveToolContext)
|
|
196
198
|
throw new Error("current owner authority producer is unavailable");
|
|
197
|
-
|
|
199
|
+
let context = await options.resolveLiveToolContext(record);
|
|
198
200
|
const relationship = context.relationshipAuthorization;
|
|
199
201
|
if (context.agentName !== owner.agentName || context.agentRoot !== owner.agentRoot
|
|
200
202
|
|| relationship?.profileId !== "sanctuary-owner" || relationship.actor?.trustLevel !== "family"
|
|
@@ -203,6 +205,26 @@ function createTelegramApprovalRuntime(options) {
|
|
|
203
205
|
|| context.currentSession.key !== record.sessionKey || context.currentSession.sessionPath !== record.sessionPath) {
|
|
204
206
|
throw new Error("current approval owner coordinates are not exact");
|
|
205
207
|
}
|
|
208
|
+
if (record.toolName === "unraid_restart_container") {
|
|
209
|
+
const restartOwner = await resolveRestartOwnerContext(record, context);
|
|
210
|
+
if (restartOwner.allowed) {
|
|
211
|
+
context = restartOwner.context;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const { reason } = restartOwner;
|
|
215
|
+
(0, runtime_1.emitNervesEvent)({
|
|
216
|
+
level: "warn", component: "senses", event: "senses.telegram_approval_owner_denied",
|
|
217
|
+
message: "restart approval owner revalidation failed",
|
|
218
|
+
meta: { approvalId: record.approvalId, reason, phase },
|
|
219
|
+
});
|
|
220
|
+
if (phase === "decision")
|
|
221
|
+
return null;
|
|
222
|
+
context = {
|
|
223
|
+
...context,
|
|
224
|
+
relationshipAuthorization: { authorizedContextScopes: [], advertisedToolNames: [], authorizeTool: () => ({ allowed: false, reason }) },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
206
228
|
const runtime = await (options.dependencies?.getProviderRuntime ?? core_1.getProviderRuntime)("human", owner);
|
|
207
229
|
const mcpManager = await (options.dependencies?.getSharedMcpManager ?? mcp_manager_1.getSharedMcpManager)(owner) ?? undefined;
|
|
208
230
|
const selectCurrentTools = () => (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)("telegram"), context.context?.friend.toolPreferences, context.context, runtime.capabilities, mcpManager, runtime.model, context);
|
|
@@ -220,9 +242,10 @@ function createTelegramApprovalRuntime(options) {
|
|
|
220
242
|
return null;
|
|
221
243
|
}
|
|
222
244
|
};
|
|
223
|
-
const invocationContext = (current, definition) => ({
|
|
245
|
+
const invocationContext = (current, definition, restartApproval) => ({
|
|
224
246
|
...current.toolContext,
|
|
225
247
|
toolSelection: Object.freeze({ ordinary: Object.freeze([definition]), engine: Object.freeze([]) }),
|
|
248
|
+
...(restartApproval ? { restartApproval } : {}),
|
|
226
249
|
});
|
|
227
250
|
const stateRoot = path.join(agentRoot, "state", "approvals");
|
|
228
251
|
const store = (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite"), now: () => new Date(now()) });
|
|
@@ -230,6 +253,44 @@ function createTelegramApprovalRuntime(options) {
|
|
|
230
253
|
const tokens = new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json"));
|
|
231
254
|
const pendingStore = new telegram_client_1.FileTelegramPendingApprovalStore(path.join(stateRoot, "telegram-pending.json"));
|
|
232
255
|
let transport;
|
|
256
|
+
const ownerSessionMatches = (binding, sessionPath) => {
|
|
257
|
+
if (binding.sessionKey !== `telegram:${options.subject}`
|
|
258
|
+
|| sessionPath !== (0, shared_turn_1.getSenseSessionPath)(options.agentName, binding.friendId, "telegram", binding.sessionKey, agentRoot))
|
|
259
|
+
return false;
|
|
260
|
+
const envelope = (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
|
|
261
|
+
const latestUser = envelope && (0, session_events_1.selectEffectiveSessionEvents)(envelope.events).findLast((event) => event.role === "user");
|
|
262
|
+
return latestUser?.id === binding.sessionEventId && latestUser.relations.references.includes(binding.requestId);
|
|
263
|
+
};
|
|
264
|
+
const resolveRestartOwnerContext = async (record, currentContext) => {
|
|
265
|
+
const resolve = options.resolveOwnerRelationship;
|
|
266
|
+
const binding = record.ownerBinding && Object.freeze({ ...record.ownerBinding });
|
|
267
|
+
if (!binding || !resolve || (options.toolContext.agentRoot ?? agentRoot) !== agentRoot || record.sessionKey !== binding.sessionKey) {
|
|
268
|
+
return { allowed: false, reason: "restart approval owner binding is unavailable" };
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
if (!ownerSessionMatches(binding, record.sessionPath))
|
|
272
|
+
return { allowed: false, reason: "restart approval owner session changed" };
|
|
273
|
+
const relationship = await resolve(binding);
|
|
274
|
+
const context = {
|
|
275
|
+
...currentContext,
|
|
276
|
+
relationshipAuthorization: {
|
|
277
|
+
...relationship,
|
|
278
|
+
requestId: binding.requestId,
|
|
279
|
+
authorizeTool: async (name, args) => {
|
|
280
|
+
const current = await resolve(binding);
|
|
281
|
+
if (!ownerSessionMatches(binding, record.sessionPath))
|
|
282
|
+
return { allowed: false, reason: "restart approval owner session changed" };
|
|
283
|
+
return current.authorizeTool(name, args);
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const authorization = await (0, relationship_authorization_1.authorizeRestartApprovalRequester)(context, record.arguments, binding);
|
|
288
|
+
return authorization.allowed ? { allowed: true, context } : authorization;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return { allowed: false, reason: "restart approval owner authorization is unavailable" };
|
|
292
|
+
}
|
|
293
|
+
};
|
|
233
294
|
const commitAcceptanceEvidence = options.dependencies?.commitAcceptanceEvidence ?? (async (event, meta) => {
|
|
234
295
|
if (event === "telegram.callback_settled") {
|
|
235
296
|
await (0, runtime_1.emitNervesEventDurable)({
|
|
@@ -256,6 +317,25 @@ function createTelegramApprovalRuntime(options) {
|
|
|
256
317
|
if (request.toolCall.type !== "function")
|
|
257
318
|
throw new Error("approval requires a function tool call");
|
|
258
319
|
effectBarrier();
|
|
320
|
+
let ownerBinding;
|
|
321
|
+
if (request.toolCall.function.name === "unraid_restart_container") {
|
|
322
|
+
const live = request.liveToolContext;
|
|
323
|
+
const authorization = await (0, relationship_authorization_1.authorizeRoutineActionRequester)(live, request.arguments);
|
|
324
|
+
if (!authorization.allowed || authorization.requester.kind !== "owner"
|
|
325
|
+
|| live?.agentRoot !== agentRoot || live.currentSession?.sessionPath !== context.sessionPath) {
|
|
326
|
+
throw new Error("restart approval requires a current owner request");
|
|
327
|
+
}
|
|
328
|
+
const requester = authorization.requester;
|
|
329
|
+
ownerBinding = {
|
|
330
|
+
friendId: requester.friendId, requestId: requester.requestId, sessionEventId: requester.sessionEventId,
|
|
331
|
+
sessionKey: requester.origin.key, profileVersion: authorization.profileVersion,
|
|
332
|
+
};
|
|
333
|
+
if (!ownerSessionMatches(ownerBinding, context.sessionPath))
|
|
334
|
+
throw new Error("restart approval owner session changed");
|
|
335
|
+
const policy = await (0, tools_1.approvalPolicyForInvocation)("unraid_restart_container", request.arguments, live);
|
|
336
|
+
if (policy.kind !== "required")
|
|
337
|
+
throw new Error("restart approval no longer has owner fallback");
|
|
338
|
+
}
|
|
259
339
|
const scenarioHandleDigest = acceptanceMarker()?.scenarioHandleDigest;
|
|
260
340
|
const committed = (0, tool_approval_1.commitApprovalProposal)({
|
|
261
341
|
approvalStore: store,
|
|
@@ -279,6 +359,7 @@ function createTelegramApprovalRuntime(options) {
|
|
|
279
359
|
transportChatId: options.subject,
|
|
280
360
|
expiresAt: new Date(now() + 300_000).toISOString(),
|
|
281
361
|
frozenAssistantMessage: request.frozenAssistantMessage,
|
|
362
|
+
...(ownerBinding ? { ownerBinding } : {}),
|
|
282
363
|
...(scenarioHandleDigest ? { scenarioHandleDigest } : {}),
|
|
283
364
|
},
|
|
284
365
|
preCallMessages: request.preCallMessages,
|
|
@@ -369,7 +450,7 @@ function createTelegramApprovalRuntime(options) {
|
|
|
369
450
|
completeContinuation: () => { effectBarrier(); store.completeContinuation({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
|
|
370
451
|
runAgent: provider,
|
|
371
452
|
revalidate: async () => {
|
|
372
|
-
const current = await currentOptions(record);
|
|
453
|
+
const current = await currentOptions(record, "continuation");
|
|
373
454
|
continuationAuthorized = current !== null;
|
|
374
455
|
return current ? { ...current, ...approvalContinuationRunAgentOptions(current.toolContext, continuationCoordinator) } : null;
|
|
375
456
|
},
|
|
@@ -457,6 +538,7 @@ function createTelegramApprovalRuntime(options) {
|
|
|
457
538
|
}
|
|
458
539
|
else if (existing.state === "proposed") {
|
|
459
540
|
const ownerId = `telegram-decision-${(0, node_crypto_1.randomUUID)()}`;
|
|
541
|
+
let restartApproval;
|
|
460
542
|
let definition;
|
|
461
543
|
record = await (0, session_transaction_1.withSessionTurnLease)(existing.sessionPath, async (lease) => (0, tool_approval_1.executeApprovalDecision)({
|
|
462
544
|
approvalStore: store,
|
|
@@ -470,31 +552,67 @@ function createTelegramApprovalRuntime(options) {
|
|
|
470
552
|
sessionKey: existing.sessionKey,
|
|
471
553
|
},
|
|
472
554
|
ownerId,
|
|
473
|
-
currentSessionRevision: (0, session_transaction_1.readSessionTransaction)(existing.sessionPath, lease).revision,
|
|
555
|
+
currentSessionRevision: () => (0, session_transaction_1.readSessionTransaction)(existing.sessionPath, lease).revision,
|
|
474
556
|
resolveTool: async (name) => {
|
|
475
|
-
const current = await currentOptions(existing);
|
|
557
|
+
const current = await currentOptions(existing, "decision");
|
|
476
558
|
definition = current ? resolveTool(name, current.toolContext.toolSelection) : undefined;
|
|
477
559
|
return definition;
|
|
478
560
|
},
|
|
479
561
|
resolveApprovalPolicy: async (name, args) => {
|
|
480
|
-
const current = await currentOptions(existing);
|
|
562
|
+
const current = await currentOptions(existing, "decision");
|
|
481
563
|
return current ? (0, tools_1.approvalPolicyForInvocation)(name, args, current.toolContext) : { kind: "not_required" };
|
|
482
564
|
},
|
|
483
|
-
liveGuard: async () =>
|
|
565
|
+
liveGuard: async (context) => {
|
|
566
|
+
if (existing.toolName !== "unraid_restart_container")
|
|
567
|
+
return { ok: true };
|
|
568
|
+
const current = await currentOptions(existing, "decision");
|
|
569
|
+
if (!current || !existing.ownerBinding)
|
|
570
|
+
return { ok: false, reason: "restart approval owner binding is unavailable" };
|
|
571
|
+
const restartToolContext = current.toolContext;
|
|
572
|
+
const authorization = await (0, relationship_authorization_1.authorizeRestartApprovalRequester)(restartToolContext, context.arguments, existing.ownerBinding);
|
|
573
|
+
if (!authorization.allowed)
|
|
574
|
+
return { ok: false, reason: authorization.reason };
|
|
575
|
+
try {
|
|
576
|
+
const listed = await restartToolContext.sanctuary?.listContainers();
|
|
577
|
+
if (!listed || typeof listed !== "object" || !("ok" in listed) || listed.ok !== true
|
|
578
|
+
|| !("data" in listed) || !listed.data || typeof listed.data !== "object"
|
|
579
|
+
|| !("truncated" in listed.data) || listed.data.truncated !== false
|
|
580
|
+
|| !("containers" in listed.data) || !Array.isArray(listed.data.containers)) {
|
|
581
|
+
return { ok: false, reason: "restart approval inventory is invalid" };
|
|
582
|
+
}
|
|
583
|
+
const matches = listed.data.containers.filter((target) => target?.name === context.arguments.container);
|
|
584
|
+
const target = matches[0];
|
|
585
|
+
if (matches.length !== 1 || typeof target?.id !== "string" || !target.id.trim() || target.id !== target.id.trim() || target.degraded !== false) {
|
|
586
|
+
return { ok: false, reason: "restart approval target is not exact" };
|
|
587
|
+
}
|
|
588
|
+
restartApproval = Object.freeze({
|
|
589
|
+
approvalId: existing.approvalId, agentRoot, sessionPath: existing.sessionPath,
|
|
590
|
+
ownerBinding: Object.freeze({ ...existing.ownerBinding }),
|
|
591
|
+
argumentDigest: existing.argumentDigest,
|
|
592
|
+
target: Object.freeze({ id: target.id, name: String(context.arguments.container) }),
|
|
593
|
+
});
|
|
594
|
+
return { ok: true };
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return { ok: false, reason: "restart approval target is unavailable" };
|
|
598
|
+
}
|
|
599
|
+
},
|
|
484
600
|
liveRisk: async () => ({ ok: true }),
|
|
485
601
|
preflight: async (context) => {
|
|
486
|
-
const current = await currentOptions(existing);
|
|
602
|
+
const current = await currentOptions(existing, "decision");
|
|
487
603
|
if (!current)
|
|
488
604
|
return { ok: false, reason: "current tool authority is unavailable" };
|
|
489
|
-
const result = await (0, tools_1.preflightToolCall)(context.record.toolName, context.arguments, invocationContext(current, context.definition));
|
|
605
|
+
const result = await (0, tools_1.preflightToolCall)(context.record.toolName, context.arguments, invocationContext(current, context.definition, restartApproval));
|
|
490
606
|
return result.kind === "ready" ? { ok: true } : { ok: false, reason: result.text };
|
|
491
607
|
},
|
|
492
608
|
hooks: telegramApprovalDecisionBarrierHooks(effectBarrier),
|
|
493
609
|
execute: async (name, args) => {
|
|
494
|
-
|
|
610
|
+
if (name === "unraid_restart_container" && !restartApproval)
|
|
611
|
+
return { kind: "rejected_before_handler", text: "restart approval was not revalidated" };
|
|
612
|
+
const current = await currentOptions(existing, "decision");
|
|
495
613
|
if (!current || !definition)
|
|
496
614
|
return { kind: "rejected_before_handler", text: "current approved tool authority is unavailable" };
|
|
497
|
-
const approvedToolContext = invocationContext(current, definition);
|
|
615
|
+
const approvedToolContext = invocationContext(current, definition, restartApproval);
|
|
498
616
|
const execute = () => executeApprovedTelegramTool(name, args, (toolName, toolArgs) => (0, tools_1.executeTool)(toolName, toolArgs, approvedToolContext, options.dependencies?.executeTool), decisionScenarioDigest, existing.approvalId, effectBarrier);
|
|
499
617
|
return decisionScenarioDigest
|
|
500
618
|
? (0, sanctuary_acceptance_marker_1.runWithSanctuaryAcceptanceApproval)({ approvalId: existing.approvalId, argumentDigest: existing.argumentDigest }, execute)
|
|
@@ -150,7 +150,11 @@ async function ingestTelegramAttachments(input) {
|
|
|
150
150
|
throw new Error("invalid remote path");
|
|
151
151
|
if (remote.file_size !== undefined && (!Number.isSafeInteger(remote.file_size) || remote.file_size < 0 || remote.file_size > exports.MAX_TELEGRAM_ATTACHMENT_BYTES))
|
|
152
152
|
throw new Error("advertised size exceeds limit");
|
|
153
|
-
const response =
|
|
153
|
+
const response = input.downloadFile
|
|
154
|
+
? await input.downloadFile(remote.file_path)
|
|
155
|
+
: input.botToken
|
|
156
|
+
? await fetchImpl(`https://api.telegram.org/file/bot${input.botToken}/${remote.file_path}`, { signal: AbortSignal.timeout(30_000) })
|
|
157
|
+
: (() => { throw new Error("attachment transport is unavailable"); })();
|
|
154
158
|
if (!response.ok)
|
|
155
159
|
throw new Error(`download failed with HTTP ${response.status}`);
|
|
156
160
|
const buffer = await readBounded(response, exports.MAX_TELEGRAM_ATTACHMENT_BYTES);
|
|
@@ -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);
|