@ouro.bot/cli 0.1.0-alpha.814 → 0.1.0-alpha.816
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 +15 -0
- package/deploy/unraid/README.txt +25 -305
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/approval-store.js +11 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +3 -3
- package/dist/heart/daemon/container-spec-auditor.js +3 -17
- 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-steward-policy.js +34 -10
- package/dist/repertoire/tools-unraid.js +79 -32
- package/dist/repertoire/tools.js +22 -19
- package/dist/repertoire/unraid-restart.js +179 -52
- package/dist/senses/private-runtime.js +27 -12
- package/dist/senses/sanctuary-health-runner.js +0 -1
- package/dist/senses/sanctuary-interactive-control.js +160 -56
- package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
- package/dist/senses/sanctuary-runtime.js +2 -0
- package/dist/senses/telegram-approval-runtime.js +130 -12
- package/dist/senses/telegram.js +30 -21
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -229,70 +229,174 @@ function createSanctuaryInteractiveControl(options) {
|
|
|
229
229
|
const socketPath = path.join(options.agentRoot, "state", "acceptance", "telegram-control.sock");
|
|
230
230
|
const runRequest = options.runRequest ?? (async (operation) => operation());
|
|
231
231
|
let server;
|
|
232
|
+
let endpoint;
|
|
233
|
+
let startPromise;
|
|
234
|
+
let stopPromise;
|
|
232
235
|
let updateId = 2_100_000_000;
|
|
236
|
+
const readEndpoint = () => (0, node_fs_1.lstatSync)(socketPath, { bigint: true, throwIfNoEntry: false });
|
|
237
|
+
// Socket mtime, unlike ctime, survives permission-only changes.
|
|
238
|
+
const matches = (observed, current) => current !== undefined && current.isSocket() && current.dev === observed.dev && current.ino === observed.ino
|
|
239
|
+
&& current.birthtimeNs === observed.birthtimeNs && current.mtimeNs === observed.mtimeNs;
|
|
240
|
+
const stopListener = async () => {
|
|
241
|
+
const active = server;
|
|
242
|
+
if (!active)
|
|
243
|
+
return;
|
|
244
|
+
if (active.listening) {
|
|
245
|
+
const current = readEndpoint();
|
|
246
|
+
// Native Server.close() also unlinks its pathname, so ownership must precede it.
|
|
247
|
+
if (current && (!endpoint || !matches(endpoint, current)))
|
|
248
|
+
throw new Error("Sanctuary interactive control socket ownership was replaced");
|
|
249
|
+
await new Promise((resolve, reject) => active.close((error) => error ? reject(error) : resolve()));
|
|
250
|
+
}
|
|
251
|
+
server = undefined;
|
|
252
|
+
endpoint = undefined;
|
|
253
|
+
};
|
|
233
254
|
return {
|
|
234
255
|
socketPath,
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
return;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
start() {
|
|
257
|
+
if (startPromise)
|
|
258
|
+
return startPromise;
|
|
259
|
+
const previousStop = stopPromise;
|
|
260
|
+
stopPromise = undefined;
|
|
261
|
+
const starting = (async () => {
|
|
262
|
+
if (previousStop)
|
|
263
|
+
await previousStop;
|
|
264
|
+
if (server) {
|
|
265
|
+
if (endpoint && matches(endpoint, readEndpoint()))
|
|
266
|
+
return;
|
|
267
|
+
await stopListener();
|
|
268
|
+
}
|
|
269
|
+
(0, node_fs_1.mkdirSync)(path.dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
270
|
+
const previous = readEndpoint();
|
|
271
|
+
if (previous) {
|
|
272
|
+
if (!previous.isSocket())
|
|
273
|
+
throw new Error("Sanctuary interactive control endpoint is not a socket");
|
|
274
|
+
await new Promise((resolve, reject) => {
|
|
275
|
+
const socket = (0, node_net_1.createConnection)(socketPath);
|
|
276
|
+
const finish = (error) => { socket.destroy(); if (error)
|
|
277
|
+
reject(error);
|
|
278
|
+
else
|
|
279
|
+
resolve(); };
|
|
280
|
+
socket.setTimeout(1_000, () => finish(new Error("Sanctuary interactive control ownership probe timed out")));
|
|
281
|
+
socket.once("connect", () => finish(new Error("Sanctuary interactive control endpoint has another live listener")));
|
|
282
|
+
socket.once("error", (error) => finish(error.code === "ECONNREFUSED" ? undefined : error));
|
|
283
|
+
});
|
|
284
|
+
const current = readEndpoint();
|
|
285
|
+
if (!matches(previous, current) || current.ctimeNs !== previous.ctimeNs)
|
|
286
|
+
throw new Error("Sanctuary interactive control endpoint was replaced during the ownership probe");
|
|
287
|
+
(0, node_fs_1.unlinkSync)(socketPath);
|
|
288
|
+
}
|
|
289
|
+
server = (0, node_net_1.createServer)({ allowHalfOpen: true }, (connection) => {
|
|
290
|
+
let raw = "";
|
|
291
|
+
connection.setEncoding("utf8");
|
|
292
|
+
connection.on("error", () => undefined);
|
|
293
|
+
connection.on("data", (chunk) => { raw += chunk; if (Buffer.byteLength(raw) > MAX_CONTROL_REQUEST)
|
|
294
|
+
connection.destroy(); });
|
|
295
|
+
connection.on("end", () => {
|
|
296
|
+
void runRequest(async () => {
|
|
297
|
+
try {
|
|
298
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_interactive_control_request", message: "Sanctuary interactive control request received", meta: { bytes: Buffer.byteLength(raw) } });
|
|
299
|
+
const parsed = object(JSON.parse(raw), "interactive control request");
|
|
300
|
+
if (parsed.operation === "interactive_runtime_ready") {
|
|
301
|
+
exactKeys(parsed, ["operation", "label", "scenarioHandleDigest"], "interactive readiness request");
|
|
302
|
+
if (parsed.label !== "unit-16m-restart-continuation" || typeof parsed.scenarioHandleDigest !== "string" || !SHA256.test(parsed.scenarioHandleDigest))
|
|
303
|
+
throw new Error("interactive readiness binding is invalid");
|
|
304
|
+
connection.end(`${JSON.stringify({ ok: true, result: { ready: true } })}\n`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const result = await executeSanctuaryInteractiveEngine(parsed, {
|
|
308
|
+
agentRoot: options.agentRoot,
|
|
309
|
+
readApprovals: (digest) => (0, approval_store_1.readApprovalsByScenarioHandleDigest)(path.join(options.agentRoot, "state", "approvals", "approvals.sqlite"), digest),
|
|
310
|
+
readPending: () => new telegram_client_1.FileTelegramPendingApprovalStore(path.join(options.agentRoot, "state", "approvals", "telegram-pending.json")).load(),
|
|
311
|
+
createSession: async () => ({
|
|
312
|
+
handle: ({ callbackData, queryId, messageId }) => runRequest(() => options.transport.handleUpdate({
|
|
313
|
+
update_id: updateId++,
|
|
314
|
+
callback_query: {
|
|
315
|
+
id: queryId,
|
|
316
|
+
from: { id: Number(options.authorizedUserId) },
|
|
317
|
+
data: callbackData,
|
|
318
|
+
message: {
|
|
319
|
+
message_id: Number(messageId),
|
|
320
|
+
chat: { id: Number(options.authorizedChatId) },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
})),
|
|
324
|
+
pendingApprovalIds: () => options.transport.listPendingDeliveries().map(({ approvalId }) => approvalId),
|
|
325
|
+
close: () => undefined,
|
|
326
|
+
}),
|
|
327
|
+
proveIndeterminateRecovery: (approval, digest) => proveSanctuaryAttemptedRecoveryWithoutRetry(options.agentRoot, digest, approval),
|
|
328
|
+
writeCredentialObserved: () => /credential|api[_-]?key|token|secret/iu.test(raw),
|
|
329
|
+
});
|
|
330
|
+
connection.end(`${JSON.stringify({ ok: true, result })}\n`);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
connection.end(`${JSON.stringify({ ok: false, error: "interactive runtime operation failed" })}\n`);
|
|
334
|
+
}
|
|
335
|
+
}).catch(() => { if (!connection.destroyed)
|
|
336
|
+
connection.end(`${JSON.stringify({ ok: false, error: "interactive runtime operation failed" })}\n`); });
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
const active = server;
|
|
340
|
+
try {
|
|
341
|
+
await new Promise((resolve, reject) => {
|
|
342
|
+
const onError = (error) => { active.off("listening", onListening); reject(error); };
|
|
343
|
+
const onListening = () => {
|
|
344
|
+
active.off("error", onError);
|
|
345
|
+
try {
|
|
346
|
+
if (!endpoint || !matches(endpoint, readEndpoint()))
|
|
347
|
+
throw new Error("Sanctuary interactive control socket ownership was replaced during startup");
|
|
348
|
+
resolve();
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
reject(error);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
active.once("error", onError);
|
|
355
|
+
active.once("listening", onListening);
|
|
249
356
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return;
|
|
357
|
+
active.listen(socketPath);
|
|
358
|
+
if (active.listening) {
|
|
359
|
+
endpoint = readEndpoint();
|
|
360
|
+
if (!endpoint?.isSocket())
|
|
361
|
+
throw new Error("Sanctuary interactive control startup ownership is unproven");
|
|
362
|
+
(0, node_fs_1.chmodSync)(socketPath, 0o600);
|
|
363
|
+
endpoint = readEndpoint();
|
|
258
364
|
}
|
|
259
|
-
const result = await executeSanctuaryInteractiveEngine(parsed, {
|
|
260
|
-
agentRoot: options.agentRoot,
|
|
261
|
-
readApprovals: (digest) => (0, approval_store_1.readApprovalsByScenarioHandleDigest)(path.join(options.agentRoot, "state", "approvals", "approvals.sqlite"), digest),
|
|
262
|
-
readPending: () => new telegram_client_1.FileTelegramPendingApprovalStore(path.join(options.agentRoot, "state", "approvals", "telegram-pending.json")).load(),
|
|
263
|
-
createSession: async () => ({
|
|
264
|
-
handle: ({ callbackData, queryId, messageId }) => runRequest(() => options.transport.handleUpdate({
|
|
265
|
-
update_id: updateId++,
|
|
266
|
-
callback_query: {
|
|
267
|
-
id: queryId,
|
|
268
|
-
from: { id: Number(options.authorizedUserId) },
|
|
269
|
-
data: callbackData,
|
|
270
|
-
message: {
|
|
271
|
-
message_id: Number(messageId),
|
|
272
|
-
chat: { id: Number(options.authorizedChatId) },
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
})),
|
|
276
|
-
pendingApprovalIds: () => options.transport.listPendingDeliveries().map(({ approvalId }) => approvalId),
|
|
277
|
-
close: () => undefined,
|
|
278
|
-
}),
|
|
279
|
-
proveIndeterminateRecovery: (approval, digest) => proveSanctuaryAttemptedRecoveryWithoutRetry(options.agentRoot, digest, approval),
|
|
280
|
-
writeCredentialObserved: () => /credential|api[_-]?key|token|secret/iu.test(raw),
|
|
281
|
-
});
|
|
282
|
-
connection.end(`${JSON.stringify({ ok: true, result })}\n`);
|
|
283
365
|
}
|
|
284
|
-
catch {
|
|
285
|
-
|
|
366
|
+
catch (error) {
|
|
367
|
+
active.off("error", onError);
|
|
368
|
+
onError(error);
|
|
286
369
|
}
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
try {
|
|
374
|
+
await stopListener();
|
|
375
|
+
}
|
|
376
|
+
catch (cleanupError) {
|
|
377
|
+
throw new AggregateError([error, cleanupError], "Sanctuary interactive control startup and ownership cleanup failed");
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
})().finally(() => { if (startPromise === starting)
|
|
382
|
+
startPromise = undefined; });
|
|
383
|
+
startPromise = starting;
|
|
384
|
+
return starting;
|
|
385
|
+
},
|
|
386
|
+
stop() {
|
|
387
|
+
if (stopPromise)
|
|
388
|
+
return stopPromise;
|
|
389
|
+
const starting = startPromise;
|
|
390
|
+
startPromise = undefined;
|
|
391
|
+
const stopping = (async () => {
|
|
392
|
+
if (starting)
|
|
393
|
+
await starting;
|
|
394
|
+
await stopListener();
|
|
395
|
+
})().finally(() => { if (stopPromise === stopping)
|
|
396
|
+
stopPromise = undefined; });
|
|
397
|
+
stopPromise = stopping;
|
|
398
|
+
return stopping;
|
|
292
399
|
},
|
|
293
|
-
async stop() { const active = server; server = undefined; if (active)
|
|
294
|
-
await new Promise((resolve) => active.close(() => resolve())); if ((0, node_fs_1.existsSync)(socketPath))
|
|
295
|
-
(0, node_fs_1.unlinkSync)(socketPath); },
|
|
296
400
|
};
|
|
297
401
|
}
|
|
298
402
|
async function sanctuaryInteractiveControlReady(socketPath, timeoutMs = 1_000) {
|
|
@@ -132,7 +132,10 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
|
|
|
132
132
|
const mentionsMedia = /\b(?:film|films|movie|movies|shows|tv|jellyfin|watch|shelf|stock|catalog|library|lib)\b/u.test(normalized);
|
|
133
133
|
const asksCatalog = /\b(?:have|got|stock|catalog|library|lib|shelf|jellyfin|favorite|favourite|recommend|suggest|pick|watch|add|see|show|list|browse|access)\b/u.test(normalized);
|
|
134
134
|
const requestedTitleQuery = requestedTitle(normalized);
|
|
135
|
-
|
|
135
|
+
const operationalOnly = /\b(?:restart|reboot|start|stop|stopped|stopping|running|down|investigate|redeploy|deploy|crash|crashed)\b/u.test(normalized)
|
|
136
|
+
&& !requestedTitleQuery
|
|
137
|
+
&& !/\b(?:film|films|movie|movies|shows|tv|watch|shelf|stock|catalog|library|lib|have|got|favorite|favourite|recommend|suggest|pick|add|see|show|list|browse|access|find|search|count|contents)\b/u.test(normalized);
|
|
138
|
+
if (operationalOnly || (!mentionsMedia && !requestedTitleQuery) || !asksCatalog)
|
|
136
139
|
return undefined;
|
|
137
140
|
const kind = requestKind(normalized, requestedTitleQuery);
|
|
138
141
|
const asksForAddition = /\b(?:add|missing from|must get)\b/u.test(normalized);
|
|
@@ -274,6 +274,8 @@ function createSanctuaryToolContext(agentName) {
|
|
|
274
274
|
acceptanceApproval: sanctuary_acceptance_marker_1.readSanctuaryAcceptanceApproval,
|
|
275
275
|
reserveRoutineAction: (input) => (0, steward_policy_1.consumeRoutineActionGrant)(agentRoot, input),
|
|
276
276
|
transitionRoutineAction: (input) => (0, steward_policy_1.transitionRoutineActionReceipt)(agentRoot, input),
|
|
277
|
+
withRoutineActionAttempt: (reservation, validate, attempt) => (0, steward_policy_1.withRoutineActionAttempt)(agentRoot, reservation, validate, attempt),
|
|
278
|
+
withApprovalPolicyLease: (operation) => (0, steward_policy_1.withStewardPolicyLease)(agentRoot, operation),
|
|
277
279
|
});
|
|
278
280
|
return {
|
|
279
281
|
agentRoot,
|
|
@@ -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)
|
package/dist/senses/telegram.js
CHANGED
|
@@ -913,6 +913,12 @@ function createTelegramSenseApp(options) {
|
|
|
913
913
|
subject,
|
|
914
914
|
identityKey,
|
|
915
915
|
toolContext: toolContext ?? {},
|
|
916
|
+
...(options.resolveRelationshipAuthorization ? {
|
|
917
|
+
resolveOwnerRelationship: (binding) => options.resolveRelationshipAuthorization({
|
|
918
|
+
friendId: binding.friendId, requestId: binding.requestId, sessionEventId: binding.sessionEventId, sessionKey: binding.sessionKey,
|
|
919
|
+
botId: botId, userId: authorizedUserId, chatId: authorizedChatId,
|
|
920
|
+
}),
|
|
921
|
+
} : {}),
|
|
916
922
|
resolveLiveToolContext: async (record) => {
|
|
917
923
|
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
918
924
|
if (record.transport !== "telegram" || record.requesterId !== subject
|
|
@@ -1669,6 +1675,26 @@ function createTelegramSenseApp(options) {
|
|
|
1669
1675
|
catch (error) {
|
|
1670
1676
|
releaseAcceptanceAudit(error);
|
|
1671
1677
|
}
|
|
1678
|
+
const pendingCleanup = new Set([
|
|
1679
|
+
() => poll.stop(),
|
|
1680
|
+
async () => { await runPromise?.catch(() => undefined); },
|
|
1681
|
+
async () => { await Promise.all([...approvalReconciliationsInFlight]); },
|
|
1682
|
+
async () => { await interactiveControl?.stop(); },
|
|
1683
|
+
() => api.stop(),
|
|
1684
|
+
() => approvalRuntime?.close(),
|
|
1685
|
+
() => effectJournal?.close(),
|
|
1686
|
+
() => admissionStore?.close(),
|
|
1687
|
+
() => (0, tools_awaiting_1.resetAwaitToolDeps)(),
|
|
1688
|
+
() => runWithAcceptanceAuditOwner(() => {
|
|
1689
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1690
|
+
component: "senses",
|
|
1691
|
+
event: "senses.telegram_poll_end",
|
|
1692
|
+
message: "Telegram long poll stopped",
|
|
1693
|
+
meta: { agentName: options.agentName, subject },
|
|
1694
|
+
});
|
|
1695
|
+
}),
|
|
1696
|
+
retireAcceptanceAudit,
|
|
1697
|
+
]);
|
|
1672
1698
|
return {
|
|
1673
1699
|
run(signal) {
|
|
1674
1700
|
if (runPromise)
|
|
@@ -1762,37 +1788,20 @@ function createTelegramSenseApp(options) {
|
|
|
1762
1788
|
return stopPromise;
|
|
1763
1789
|
stopPromise = (async () => {
|
|
1764
1790
|
const errors = [];
|
|
1765
|
-
const
|
|
1791
|
+
for (const operation of pendingCleanup) {
|
|
1766
1792
|
try {
|
|
1767
1793
|
await operation();
|
|
1794
|
+
pendingCleanup.delete(operation);
|
|
1768
1795
|
}
|
|
1769
1796
|
catch (error) {
|
|
1770
1797
|
errors.push(error);
|
|
1771
1798
|
}
|
|
1772
|
-
}
|
|
1773
|
-
await attempt(() => poll.stop());
|
|
1774
|
-
await attempt(async () => { await runPromise?.catch(() => undefined); });
|
|
1775
|
-
await attempt(async () => { await Promise.all([...approvalReconciliationsInFlight]); });
|
|
1776
|
-
await attempt(async () => { await interactiveControl?.stop(); });
|
|
1777
|
-
await attempt(() => api.stop());
|
|
1778
|
-
await attempt(() => approvalRuntime?.close());
|
|
1779
|
-
await attempt(() => effectJournal?.close());
|
|
1780
|
-
await attempt(() => admissionStore?.close());
|
|
1781
|
-
await attempt(() => (0, tools_awaiting_1.resetAwaitToolDeps)());
|
|
1782
|
-
await attempt(() => runWithAcceptanceAuditOwner(() => {
|
|
1783
|
-
(0, runtime_1.emitNervesEvent)({
|
|
1784
|
-
component: "senses",
|
|
1785
|
-
event: "senses.telegram_poll_end",
|
|
1786
|
-
message: "Telegram long poll stopped",
|
|
1787
|
-
meta: { agentName: options.agentName, subject },
|
|
1788
|
-
});
|
|
1789
|
-
}));
|
|
1790
|
-
await attempt(retireAcceptanceAudit);
|
|
1799
|
+
}
|
|
1791
1800
|
if (errors.length === 1)
|
|
1792
1801
|
throw errors[0];
|
|
1793
1802
|
if (errors.length > 1)
|
|
1794
1803
|
throw new AggregateError(errors, "Telegram sense cleanup failed");
|
|
1795
|
-
})();
|
|
1804
|
+
})().catch((error) => { stopPromise = undefined; throw error; });
|
|
1796
1805
|
return stopPromise;
|
|
1797
1806
|
},
|
|
1798
1807
|
};
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.816",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.816",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|