@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
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.sessionToolDefinitions = void 0;
|
|
37
|
+
exports.readButlerOperationalVisibility = readButlerOperationalVisibility;
|
|
38
|
+
exports.formatButlerOperationalVisibility = formatButlerOperationalVisibility;
|
|
37
39
|
exports.renderInnerProgressStatus = renderInnerProgressStatus;
|
|
38
40
|
const fs = __importStar(require("fs"));
|
|
39
41
|
const path = __importStar(require("path"));
|
|
@@ -55,6 +57,15 @@ const habit_session_summary_1 = require("../heart/habits/habit-session-summary")
|
|
|
55
57
|
const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
|
|
56
58
|
const mail_import_discovery_1 = require("../heart/mail-import-discovery");
|
|
57
59
|
const outbound_1 = require("../senses/voice/outbound");
|
|
60
|
+
const router_1 = require("../heart/external-events/router");
|
|
61
|
+
const steward_policy_1 = require("../heart/steward-policy");
|
|
62
|
+
const await_parser_1 = require("../heart/awaiting/await-parser");
|
|
63
|
+
const daemon_health_1 = require("../heart/daemon/daemon-health");
|
|
64
|
+
const turn_context_1 = require("../heart/turn-context");
|
|
65
|
+
const telegram_effect_adapter_1 = require("../senses/telegram-effect-adapter");
|
|
66
|
+
const telegram_admission_1 = require("../senses/telegram-admission");
|
|
67
|
+
const sanctuary_health_1 = require("../senses/sanctuary-health");
|
|
68
|
+
const run_ledger_1 = require("../heart/run-ledger");
|
|
58
69
|
const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
|
|
59
70
|
const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
|
|
60
71
|
const VALID_HABIT_SUMMARY_WHICH = new Set(["latest", "previous", "latest-success", "latest-failure"]);
|
|
@@ -380,6 +391,202 @@ async function buildToolActiveWorkFrame(ctx) {
|
|
|
380
391
|
targetCandidates: [],
|
|
381
392
|
});
|
|
382
393
|
}
|
|
394
|
+
function boundedStatusText(value, max = 180) {
|
|
395
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
396
|
+
return normalized.length <= max ? normalized : `${normalized.slice(0, max - 3)}...`;
|
|
397
|
+
}
|
|
398
|
+
function readButlerOperationalVisibility(agentRoot = (0, identity_1.getAgentRoot)(), agentName = (0, identity_1.getAgentName)()) {
|
|
399
|
+
const sourceErrors = {};
|
|
400
|
+
const read = (source, operation, fallback) => {
|
|
401
|
+
try {
|
|
402
|
+
return operation();
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
sourceErrors[source] = boundedStatusText(error instanceof Error ? error.message : String(error)) || "unknown read failure";
|
|
406
|
+
return fallback;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const awaitsDir = path.join(agentRoot, "awaiting");
|
|
410
|
+
const awaits = read("awaits", () => fs.existsSync(awaitsDir) ? fs.readdirSync(awaitsDir, { withFileTypes: true })
|
|
411
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
412
|
+
.map((entry) => {
|
|
413
|
+
const filePath = path.join(awaitsDir, entry.name);
|
|
414
|
+
return (0, await_parser_1.parseAwaitFile)(fs.readFileSync(filePath, "utf8"), filePath);
|
|
415
|
+
})
|
|
416
|
+
.filter((entry) => entry.status === "pending") : [], []);
|
|
417
|
+
const effectsRoot = path.join(agentRoot, "state", "telegram", "effects");
|
|
418
|
+
const telegramEffects = read("telegram_effects", () => {
|
|
419
|
+
if (!fs.existsSync(effectsRoot))
|
|
420
|
+
throw new Error("Telegram effect journal has not been created yet");
|
|
421
|
+
const journal = new telegram_effect_adapter_1.FileTelegramEffectJournal(effectsRoot);
|
|
422
|
+
try {
|
|
423
|
+
return journal.list().map((artifact) => ({
|
|
424
|
+
id: artifact.id,
|
|
425
|
+
authorClass: artifact.authorClass,
|
|
426
|
+
targetKind: artifact.target.kind,
|
|
427
|
+
state: artifact.parts.some((part) => part.state === "indeterminate") ? "indeterminate"
|
|
428
|
+
: artifact.parts.some((part) => part.state === "attempting") ? "attempting"
|
|
429
|
+
: artifact.parts.every((part) => part.state === "session_recorded") ? "session_recorded"
|
|
430
|
+
: artifact.parts.some((part) => part.state === "accepted") ? "accepted"
|
|
431
|
+
: "prepared",
|
|
432
|
+
acceptedAt: artifact.parts.flatMap((part) => part.acceptedAt ? [part.acceptedAt] : []).sort().at(-1) ?? null,
|
|
433
|
+
sessionRecordedAt: artifact.parts.flatMap((part) => part.sessionRecordedAt ? [part.sessionRecordedAt] : []).sort().at(-1) ?? null,
|
|
434
|
+
messageIds: artifact.parts.flatMap((part) => part.messageId ? [part.messageId] : []),
|
|
435
|
+
sessionEventIds: artifact.parts.flatMap((part) => part.sessionEventId ? [part.sessionEventId] : []),
|
|
436
|
+
updatedAt: artifact.updatedAt,
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
journal.close();
|
|
441
|
+
}
|
|
442
|
+
}, []);
|
|
443
|
+
const admissionsRoot = path.join(agentRoot, "state", "senses", "telegram", "admissions");
|
|
444
|
+
const telegramAdmissions = read("telegram_admissions", () => {
|
|
445
|
+
if (!fs.existsSync(admissionsRoot))
|
|
446
|
+
throw new Error("Telegram admission store has not been created yet");
|
|
447
|
+
const store = new telegram_admission_1.FileTelegramAdmissionStore(admissionsRoot);
|
|
448
|
+
try {
|
|
449
|
+
return store.list().map((record) => ({ id: record.id, status: record.status, displayCode: record.displayCode, createdAt: record.createdAt, expiresAt: record.expiresAt }));
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
store.close();
|
|
453
|
+
}
|
|
454
|
+
}, []);
|
|
455
|
+
return {
|
|
456
|
+
agentName,
|
|
457
|
+
externalEvents: read("external_events", () => (0, router_1.listExternalEventStatus)((0, router_1.getExternalEventRoot)()).filter((event) => event.agent === agentName), []),
|
|
458
|
+
stewardPolicy: read("steward_policy", () => (0, steward_policy_1.readStewardPolicy)(agentRoot), { schemaVersion: 1, version: 0, desiredStates: {}, routineActionGrants: {}, updatedAt: null }),
|
|
459
|
+
awaits,
|
|
460
|
+
telegramEffects,
|
|
461
|
+
telegramAdmissions,
|
|
462
|
+
daemonHealth: read("daemon_health", () => {
|
|
463
|
+
const health = (0, daemon_health_1.readHealth)((0, daemon_health_1.getDefaultHealthPath)());
|
|
464
|
+
if (!health)
|
|
465
|
+
throw new Error("daemon health is missing or invalid");
|
|
466
|
+
return health;
|
|
467
|
+
}, null),
|
|
468
|
+
healthState: read("health_state", () => (0, sanctuary_health_1.readSanctuaryHealthState)(path.join(agentRoot, "state", "health", "sanctuary-health.json")), undefined),
|
|
469
|
+
routineActions: read("routine_actions", () => (0, steward_policy_1.readRoutineActionReceipts)(agentRoot), []),
|
|
470
|
+
providerLanes: read("provider", () => {
|
|
471
|
+
const config = (0, identity_1.loadAgentConfig)();
|
|
472
|
+
if (!config.humanFacing || !config.agentFacing)
|
|
473
|
+
throw new Error("provider lanes are unavailable");
|
|
474
|
+
return { outward: config.humanFacing, inner: config.agentFacing };
|
|
475
|
+
}, null),
|
|
476
|
+
lastAutomatedEventRun: read("run_ledger", () => (0, run_ledger_1.readRunLedger)(agentRoot)
|
|
477
|
+
.filter((row) => row.sourceKind === "private-runtime" && row.senseOrHabit === "external-event" && row.lifecycle === "completed")
|
|
478
|
+
.sort((left, right) => (right.endedAt ?? right.recordedAt).localeCompare(left.endedAt ?? left.recordedAt))[0] ?? null, null),
|
|
479
|
+
senseStatusLines: read("senses", () => (0, turn_context_1.readSenseStatusLines)(), []),
|
|
480
|
+
sourceErrors,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function formatButlerOperationalVisibility(status, options = {}) {
|
|
484
|
+
const section = options.section ?? "all";
|
|
485
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
486
|
+
const limit = Math.min(20, Math.max(1, options.limit ?? 10));
|
|
487
|
+
const show = (wanted) => section === "all" || section === wanted;
|
|
488
|
+
const page = (rows) => rows.slice(offset, offset + limit);
|
|
489
|
+
const more = (label, total) => total > offset + limit ? ` - ${total - offset - limit} more ${label}; call query_active_work with section=${label}, offset=${offset + limit}, limit=${limit}` : null;
|
|
490
|
+
const daemon = status.daemonHealth;
|
|
491
|
+
const agentHealth = daemon?.agents[status.agentName];
|
|
492
|
+
const lines = ["## butler operational visibility"];
|
|
493
|
+
const error = (source) => status.sourceErrors[source] ? `unavailable (${status.sourceErrors[source]})` : null;
|
|
494
|
+
if (show("health")) {
|
|
495
|
+
lines.push(`- daemon: ${error("daemon_health") ?? daemon?.status ?? "unavailable"}${daemon ? `; mode ${daemon.mode}; pid ${daemon.pid}` : ""}`);
|
|
496
|
+
lines.push(`- butler: ${agentHealth ? `${agentHealth.status}; pid ${agentHealth.pid ?? "none"}; crashes ${agentHealth.crashes}` : "unavailable"}`);
|
|
497
|
+
lines.push("- senses:");
|
|
498
|
+
lines.push(...(error("senses") ? [` - ${error("senses")}`] : status.senseStatusLines.length > 0 ? status.senseStatusLines.map((line) => ` ${line}`) : [" - unavailable"]));
|
|
499
|
+
const health = status.healthState;
|
|
500
|
+
lines.push(`- health sweeps: ${error("health_state") ?? health?.sweepReceipts.length ?? 0}; delivered ${health?.deliveredReceipts.length ?? 0}; indeterminate ${health?.indeterminateDeliveries.length ?? 0}; outbox ${health?.outbox ? `${health.outbox.status}:${health.outbox.id}` : "clear"}; open incidents ${health ? Object.keys(health.incidents).length : 0}; updated ${health?.updatedAt ?? "unavailable"}`);
|
|
501
|
+
const healthRows = [
|
|
502
|
+
...(health?.sweepReceipts ?? []).map((sweep) => ({ at: sweep.completedAt, text: `sweep ${sweep.sweepId}: opened ${sweep.opened}; recovered ${sweep.recovered}; completed ${sweep.completedAt}${sweep.deliveryId ? `; delivery ${sweep.deliveryId}` : ""}` })),
|
|
503
|
+
...(health?.deliveredReceipts ?? []).map((delivery) => ({ at: delivery.deliveredAt, text: `delivered ${delivery.deliveryId}: ${delivery.kind}; messages ${delivery.messageIds.join(",")}; at ${delivery.deliveredAt}` })),
|
|
504
|
+
...(health?.indeterminateDeliveries ?? []).map((delivery) => ({ at: delivery.createdAt, text: `indeterminate ${delivery.id}: ${delivery.kind}; ${delivery.status}; created ${delivery.createdAt}` })),
|
|
505
|
+
].sort((a, b) => b.at.localeCompare(a.at));
|
|
506
|
+
for (const row of page(healthRows))
|
|
507
|
+
lines.push(` - ${row.text}`);
|
|
508
|
+
const overflow = more("health", healthRows.length);
|
|
509
|
+
if (overflow)
|
|
510
|
+
lines.push(overflow);
|
|
511
|
+
}
|
|
512
|
+
if (show("provider"))
|
|
513
|
+
lines.push(`- provider lanes: ${error("provider") ?? (status.providerLanes ? `outward ${status.providerLanes.outward.provider}/${status.providerLanes.outward.model}; inner ${status.providerLanes.inner.provider}/${status.providerLanes.inner.model}` : "unavailable")}`);
|
|
514
|
+
if (show("policy")) {
|
|
515
|
+
lines.push(`- steward policy: ${error("steward_policy") ?? `version ${status.stewardPolicy.version}; ${Object.keys(status.stewardPolicy.desiredStates).length} desired states; ${Object.keys(status.stewardPolicy.routineActionGrants).length} routine grants`}`);
|
|
516
|
+
const entries = [...Object.entries(status.stewardPolicy.desiredStates).map(([key, value]) => ({ key, kind: "desired", value })), ...Object.entries(status.stewardPolicy.routineActionGrants).map(([key, value]) => ({ key, kind: "grant", value }))];
|
|
517
|
+
for (const entry of page(entries)) {
|
|
518
|
+
const expiry = entry.value.expiresAt;
|
|
519
|
+
const active = !expiry || Date.parse(expiry) > Date.now();
|
|
520
|
+
lines.push(entry.kind === "desired" ? ` - ${entry.key} = ${boundedStatusText(entry.value.value)}; ${active ? "active" : "expired"}; ${entry.value.provenance}; source ${boundedStatusText(entry.value.source)}; v${entry.value.version}${expiry ? `; expires ${expiry}` : ""}` : ` - ${entry.key}: ${entry.value.action} on ${entry.value.targets.join(", ")}; ${active ? "active" : "expired"}; max ${entry.value.maxCount}/${entry.value.windowMs}ms; ${entry.value.verificationRequired ? "verification required" : "no verification"}; v${entry.value.version}${expiry ? `; expires ${expiry}` : ""}`);
|
|
521
|
+
}
|
|
522
|
+
const overflow = more("policy", entries.length);
|
|
523
|
+
if (overflow)
|
|
524
|
+
lines.push(overflow);
|
|
525
|
+
}
|
|
526
|
+
if (show("actions")) {
|
|
527
|
+
const actions = [...(status.routineActions ?? [])].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
528
|
+
lines.push(`- routine action receipts: ${error("routine_actions") ?? actions.length}`);
|
|
529
|
+
for (const action of page(actions)) {
|
|
530
|
+
const verification = action.state === "verified" ? `verified ${action.verifiedAfterState ?? "state unavailable"}` : action.state === "failed" ? "failed" : action.state === "indeterminate" ? "indeterminate; manual inspection required" : action.state === "recovered_no_effect" ? "no effect occurred" : "pending";
|
|
531
|
+
lines.push(` - ${action.id}: ${action.action} ${action.target}; ${action.state}; updated ${action.updatedAt}; verification ${verification}`);
|
|
532
|
+
}
|
|
533
|
+
const overflow = more("actions", actions.length);
|
|
534
|
+
if (overflow)
|
|
535
|
+
lines.push(overflow);
|
|
536
|
+
}
|
|
537
|
+
if (show("awaits")) {
|
|
538
|
+
lines.push(`- pending awaits: ${status.awaits.length}`);
|
|
539
|
+
if (error("awaits"))
|
|
540
|
+
lines.push(` - ${error("awaits")}`);
|
|
541
|
+
for (const pending of page(status.awaits))
|
|
542
|
+
lines.push(` - ${pending.name}: ${boundedStatusText(pending.condition ?? (pending.body || "condition unavailable"))}${pending.wake_at ? `; wake ${pending.wake_at}` : ""}`);
|
|
543
|
+
const overflow = more("awaits", status.awaits.length);
|
|
544
|
+
if (overflow)
|
|
545
|
+
lines.push(overflow);
|
|
546
|
+
}
|
|
547
|
+
if (show("telegram")) {
|
|
548
|
+
const acceptedTimes = status.telegramEffects.flatMap((effect) => effect.acceptedAt ? [effect.acceptedAt] : []).sort().reverse();
|
|
549
|
+
const recordedTimes = status.telegramEffects.flatMap((effect) => effect.sessionRecordedAt ? [effect.sessionRecordedAt] : []).sort().reverse();
|
|
550
|
+
const legacyAccepted = status.telegramEffects.some((effect) => !effect.acceptedAt && (effect.state === "accepted" || effect.state === "session_recorded"));
|
|
551
|
+
const legacyRecorded = status.telegramEffects.some((effect) => !effect.sessionRecordedAt && effect.state === "session_recorded");
|
|
552
|
+
lines.push(`- latest Telegram accepted: ${acceptedTimes[0] ?? (legacyAccepted ? "timestamp unavailable (legacy artifacts present)" : "none")}; latest session recorded: ${recordedTimes[0] ?? (legacyRecorded ? "timestamp unavailable (legacy artifacts present)" : "none")}`);
|
|
553
|
+
lines.push(`- Telegram effects: ${error("telegram_effects") ?? status.telegramEffects.length}`);
|
|
554
|
+
lines.push(`- Telegram admissions: ${error("telegram_admissions") ?? status.telegramAdmissions.length}`);
|
|
555
|
+
const telegramRows = [
|
|
556
|
+
...status.telegramEffects.map((effect) => ({ at: effect.updatedAt, effect })),
|
|
557
|
+
...status.telegramAdmissions.map((admission) => ({ at: new Date(admission.createdAt).toISOString(), admission })),
|
|
558
|
+
].sort((a, b) => b.at.localeCompare(a.at));
|
|
559
|
+
for (const row of page(telegramRows)) {
|
|
560
|
+
if ("admission" in row) {
|
|
561
|
+
const admission = row.admission;
|
|
562
|
+
lines.push(` - admission ${admission.id}: ${admission.status}; code ${admission.displayCode}; expires ${new Date(admission.expiresAt).toISOString()}`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const effect = row.effect;
|
|
566
|
+
const accepted = effect.acceptedAt ?? ((effect.state === "accepted" || effect.state === "session_recorded") ? "timestamp unavailable (legacy)" : "not accepted");
|
|
567
|
+
const recorded = effect.sessionRecordedAt ?? (effect.state === "session_recorded" ? "timestamp unavailable (legacy)" : "not recorded");
|
|
568
|
+
lines.push(` - ${effect.id}: ${effect.state}; ${effect.authorClass}; ${effect.targetKind}; accepted ${accepted}; session recorded ${recorded}; message ids ${effect.messageIds?.join(",") || "none"}; session events ${effect.sessionEventIds?.join(",") || "none"}; updated ${effect.updatedAt}`);
|
|
569
|
+
}
|
|
570
|
+
const overflow = more("telegram", telegramRows.length);
|
|
571
|
+
if (overflow)
|
|
572
|
+
lines.push(overflow);
|
|
573
|
+
}
|
|
574
|
+
if (show("events")) {
|
|
575
|
+
const events = [...status.externalEvents].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
576
|
+
const completedRun = status.lastAutomatedEventRun;
|
|
577
|
+
lines.push(`- external issues: ${events.length}; last successful automated event turn ${error("run_ledger") ?? (completedRun ? `${completedRun.endedAt ?? completedRun.recordedAt}; run ${completedRun.runId}; target ${completedRun.targetHash}` : "none")}`);
|
|
578
|
+
if (error("external_events"))
|
|
579
|
+
lines.push(` - ${error("external_events")}`);
|
|
580
|
+
for (const event of page(events)) {
|
|
581
|
+
const policy = event.stewardPolicy?.kind === "current" ? `${event.stewardPolicy.key}@${event.stewardPolicy.version}` : "none";
|
|
582
|
+
lines.push(` - ${event.source}/${event.eventType}/${event.eventId}: ${event.executionState}; disposition ${event.classification ?? "pending"}/${event.decision ?? "pending"}; reason ${boundedStatusText(event.reason ?? event.lastError ?? "not yet recorded")}; silence/wake ${event.nextWake ? JSON.stringify(event.nextWake) : "pending"}; steward ${policy}${event.awaitId ? `; await ${event.awaitId}` : ""}${event.careId ? `; care ${event.careId}` : ""}`);
|
|
583
|
+
}
|
|
584
|
+
const overflow = more("events", events.length);
|
|
585
|
+
if (overflow)
|
|
586
|
+
lines.push(overflow);
|
|
587
|
+
}
|
|
588
|
+
return lines.join("\n");
|
|
589
|
+
}
|
|
383
590
|
function findDelegatingBridgeId(ctx) {
|
|
384
591
|
const currentSession = ctx?.currentSession;
|
|
385
592
|
if (!currentSession)
|
|
@@ -422,16 +629,24 @@ exports.sessionToolDefinitions = [
|
|
|
422
629
|
type: "function",
|
|
423
630
|
function: {
|
|
424
631
|
name: "query_active_work",
|
|
425
|
-
description: "read the current live world-state across visible sessions, coding lanes, private-runtime work, and return obligations.
|
|
632
|
+
description: "read the current live world-state across visible sessions, coding lanes, private-runtime work, and return obligations. Sanctuary operational details support bounded section/offset/limit paging.",
|
|
426
633
|
parameters: {
|
|
427
634
|
type: "object",
|
|
428
|
-
properties: {
|
|
635
|
+
properties: {
|
|
636
|
+
section: { type: "string", enum: ["all", "health", "policy", "awaits", "telegram", "events", "actions", "provider"], description: "Sanctuary detail section; defaults to all" },
|
|
637
|
+
offset: { type: "string", description: "zero-based detail offset; defaults to 0" },
|
|
638
|
+
limit: { type: "string", description: "detail rows per section, 1-20; defaults to 10" },
|
|
639
|
+
},
|
|
429
640
|
},
|
|
430
641
|
},
|
|
431
642
|
},
|
|
432
|
-
handler: async (
|
|
643
|
+
handler: async (args, ctx) => {
|
|
433
644
|
const frame = await buildToolActiveWorkFrame(ctx);
|
|
434
|
-
|
|
645
|
+
const activeWork = `this is my current top-level live world-state.\nanswer whole-self status questions from this before drilling into individual sessions.\n\n${(0, active_work_1.formatActiveWorkFrame)(frame)}`;
|
|
646
|
+
const numeric = (value, fallback, min, max) => value === undefined ? fallback : /^\d+$/u.test(value) ? Math.min(max, Math.max(min, Number(value))) : fallback;
|
|
647
|
+
return (0, identity_1.getAgentName)() === "sanctuary"
|
|
648
|
+
? `${activeWork}\n\n${formatButlerOperationalVisibility(readButlerOperationalVisibility(), { section: args.section, offset: numeric(args.offset, 0, 0, 10_000), limit: numeric(args.limit, 10, 1, 20) })}`
|
|
649
|
+
: activeWork;
|
|
435
650
|
},
|
|
436
651
|
},
|
|
437
652
|
{
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stewardPolicyToolDefinition = void 0;
|
|
4
|
+
const steward_policy_1 = require("../heart/steward-policy");
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
function arrayArgument(raw, label) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
throw new Error(`${label} is required`);
|
|
9
|
+
const value = JSON.parse(raw);
|
|
10
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string"))
|
|
11
|
+
throw new Error(`${label} must be a JSON string array`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
exports.stewardPolicyToolDefinition = {
|
|
15
|
+
tool: {
|
|
16
|
+
type: "function",
|
|
17
|
+
function: {
|
|
18
|
+
name: "steward_policy_manage",
|
|
19
|
+
description: "Read or update the household steward's typed desired-state and routine-action policy. Updates require the current authenticated family request and exact policy version.",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
action: { type: "string", enum: ["read", "set_desired_state", "grant_routine_action"] },
|
|
24
|
+
expectedVersion: { type: "integer", minimum: 0 },
|
|
25
|
+
key: { type: "string" },
|
|
26
|
+
value: { type: "string" },
|
|
27
|
+
provenance: { type: "string", enum: ["stated", "observed", "default", "installed_explicit_policy"] },
|
|
28
|
+
source: { type: "string" },
|
|
29
|
+
routineAction: { type: "string" },
|
|
30
|
+
targetsJson: { type: "string", description: "JSON array of exact target names" },
|
|
31
|
+
maxCount: { type: "integer", minimum: 1 },
|
|
32
|
+
windowMs: { type: "integer", minimum: 1 },
|
|
33
|
+
verificationRequired: { type: "boolean" },
|
|
34
|
+
exclusionsJson: { type: "string", description: "JSON array of exact excluded target names" },
|
|
35
|
+
expiresAt: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
required: ["action"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
handler: (args, ctx) => {
|
|
43
|
+
if (!ctx?.agentRoot)
|
|
44
|
+
throw new Error("steward policy runtime is unavailable");
|
|
45
|
+
if (!ctx.relationshipAuthorization)
|
|
46
|
+
throw new Error("steward policy access requires authenticated relationship authority");
|
|
47
|
+
if (args.action === "read") {
|
|
48
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "read steward policy", meta: { action: "read" } });
|
|
49
|
+
return JSON.stringify((0, steward_policy_1.readStewardPolicy)(ctx.agentRoot));
|
|
50
|
+
}
|
|
51
|
+
const actor = ctx.relationshipAuthorization?.actor;
|
|
52
|
+
if (!actor)
|
|
53
|
+
throw new Error("steward policy mutation requires authenticated relationship authority");
|
|
54
|
+
const expectedVersion = Number(args.expectedVersion);
|
|
55
|
+
if (!Number.isInteger(expectedVersion) || expectedVersion < 0)
|
|
56
|
+
throw new Error("expectedVersion must be a nonnegative integer");
|
|
57
|
+
let mutation;
|
|
58
|
+
if (args.action === "set_desired_state") {
|
|
59
|
+
if (args.provenance !== "stated" && args.provenance !== "observed" && args.provenance !== "default")
|
|
60
|
+
throw new Error("desired state provenance is invalid");
|
|
61
|
+
mutation = { kind: "set_desired_state", key: args.key ?? "", value: args.value ?? "", provenance: args.provenance, source: args.source ?? "", ...(args.expiresAt ? { expiresAt: args.expiresAt } : {}) };
|
|
62
|
+
}
|
|
63
|
+
else if (args.action === "grant_routine_action") {
|
|
64
|
+
if (args.provenance !== "stated" && args.provenance !== "installed_explicit_policy")
|
|
65
|
+
throw new Error("routine action provenance is invalid");
|
|
66
|
+
mutation = {
|
|
67
|
+
kind: "grant_routine_action",
|
|
68
|
+
key: args.key ?? "",
|
|
69
|
+
action: args.routineAction ?? "",
|
|
70
|
+
targets: arrayArgument(args.targetsJson, "targetsJson"),
|
|
71
|
+
maxCount: Number(args.maxCount),
|
|
72
|
+
windowMs: Number(args.windowMs),
|
|
73
|
+
verificationRequired: args.verificationRequired === "true",
|
|
74
|
+
exclusions: arrayArgument(args.exclusionsJson, "exclusionsJson"),
|
|
75
|
+
provenance: args.provenance,
|
|
76
|
+
...(args.expiresAt ? { expiresAt: args.expiresAt } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
throw new Error("steward policy action is invalid");
|
|
81
|
+
const result = (0, steward_policy_1.updateStewardPolicy)(ctx.agentRoot, { expectedVersion, actor, mutation });
|
|
82
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "updated steward policy", meta: { action: args.action } });
|
|
83
|
+
return JSON.stringify(result);
|
|
84
|
+
},
|
|
85
|
+
riskProfile: (args) => args.action === "read"
|
|
86
|
+
? { mutates: "none", risk: "low" }
|
|
87
|
+
: { mutates: "durable_state_write", risk: "high", reason: "updates typed household steward policy under authenticated family authority" },
|
|
88
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.telegramContactToolDefinition = void 0;
|
|
4
|
+
const runtime_1 = require("../nerves/runtime");
|
|
5
|
+
exports.telegramContactToolDefinition = {
|
|
6
|
+
tool: { type: "function", function: {
|
|
7
|
+
name: "telegram_contact_manage",
|
|
8
|
+
description: "List household Telegram contacts, revoke one exact approved contact, or release one exact blocked contact request. Owner only; quarantined message text is never returned.",
|
|
9
|
+
parameters: { type: "object", properties: {
|
|
10
|
+
action: { type: "string", enum: ["list", "revoke", "unblock"] },
|
|
11
|
+
friendId: { type: "string", description: "Exact Friend ID from list; required for revoke." },
|
|
12
|
+
admissionId: { type: "string", description: "Exact blocked admission ID from list; required for unblock." },
|
|
13
|
+
}, required: ["action"], additionalProperties: false },
|
|
14
|
+
} },
|
|
15
|
+
handler: async (args, ctx) => {
|
|
16
|
+
const actor = ctx?.relationshipAuthorization?.actor;
|
|
17
|
+
const manager = ctx?.telegramContactManager;
|
|
18
|
+
if (!actor || actor.trustLevel !== "family" || !manager)
|
|
19
|
+
return JSON.stringify({ ok: false, error: "Telegram contact management requires the live owner relationship" });
|
|
20
|
+
const action = String(args.action ?? "");
|
|
21
|
+
if (action === "list")
|
|
22
|
+
return JSON.stringify({ ok: true, ...(await manager.list({ actorFriendId: actor.friendId })) });
|
|
23
|
+
if (action === "revoke" && typeof args.friendId === "string" && args.friendId.trim())
|
|
24
|
+
return JSON.stringify({ ok: true, ...(await manager.revoke({ actorFriendId: actor.friendId, friendId: args.friendId })) });
|
|
25
|
+
if (action === "unblock" && typeof args.admissionId === "string" && args.admissionId.trim())
|
|
26
|
+
return JSON.stringify({ ok: true, ...(await manager.unblock({ actorFriendId: actor.friendId, admissionId: args.admissionId })) });
|
|
27
|
+
return JSON.stringify({ ok: false, error: "Telegram contact management arguments are invalid" });
|
|
28
|
+
},
|
|
29
|
+
riskProfile: (args) => String(args.action) === "list" ? { mutates: "none", risk: "low" } : { mutates: "durable_state_write", risk: "high", reason: "changes one exact household Telegram admission" },
|
|
30
|
+
approvalPolicy: () => ({ kind: "not_required" }),
|
|
31
|
+
};
|
|
32
|
+
(0, runtime_1.emitNervesEvent)({
|
|
33
|
+
component: "repertoire",
|
|
34
|
+
event: "repertoire.telegram_contact_tool_loaded",
|
|
35
|
+
message: "Telegram contact management tool loaded",
|
|
36
|
+
meta: { operations: 3 },
|
|
37
|
+
});
|
|
@@ -6,6 +6,9 @@ exports.createUnraidReadTools = createUnraidReadTools;
|
|
|
6
6
|
const node_crypto_1 = require("node:crypto");
|
|
7
7
|
const runtime_1 = require("../nerves/runtime");
|
|
8
8
|
const unraid_client_1 = require("./unraid-client");
|
|
9
|
+
const tools_base_1 = require("./tools-base");
|
|
10
|
+
const steward_policy_1 = require("../heart/steward-policy");
|
|
11
|
+
const obligations_1 = require("../arc/obligations");
|
|
9
12
|
exports.SANCTUARY_CONTAINERS_QUERY = `query SanctuaryContainers {
|
|
10
13
|
docker { containers(skipCache: true) { id names state status autoStart } }
|
|
11
14
|
}`;
|
|
@@ -16,6 +19,7 @@ exports.SANCTUARY_STORAGE_QUERY = `query SanctuaryStorage {
|
|
|
16
19
|
vars { id }
|
|
17
20
|
array { state capacity { kilobytes { used free total } } }
|
|
18
21
|
shares { id name used free size }
|
|
22
|
+
docker { containers(skipCache: true) { id names state status autoStart } }
|
|
19
23
|
}`;
|
|
20
24
|
exports.SANCTUARY_DISKS_QUERY = `query SanctuaryDisks {
|
|
21
25
|
disks { id name smartStatus temperature }
|
|
@@ -197,7 +201,30 @@ function createUnraidReadTools(client) {
|
|
|
197
201
|
const free = numberOrNull(item.free);
|
|
198
202
|
return { name, usedBytes: used, freeBytes: free, usedPercent: percent(used, free), degraded: used === null || free === null };
|
|
199
203
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
200
|
-
|
|
204
|
+
const largestCandidates = shares
|
|
205
|
+
.filter((share) => share.usedBytes !== null && share.usedBytes > 0)
|
|
206
|
+
.sort((a, b) => Number(b.usedBytes) - Number(a.usedBytes) || a.name.localeCompare(b.name))
|
|
207
|
+
.slice(0, 10)
|
|
208
|
+
.map((share) => ({ kind: "share", name: share.name, usedBytes: share.usedBytes }));
|
|
209
|
+
let unmanic = { state: "unknown", degraded: true };
|
|
210
|
+
if (data.docker && typeof data.docker === "object" && !Array.isArray(data.docker)) {
|
|
211
|
+
const matches = mapContainers(data).containers.filter((container) => container.name === "unmanic");
|
|
212
|
+
if (matches.length === 1)
|
|
213
|
+
unmanic = { state: matches[0].state, degraded: matches[0].degraded };
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, data: {
|
|
216
|
+
sourceIdentityDigest,
|
|
217
|
+
array: { state: state.value, usedBytes, freeBytes, usedPercent: percent(usedBytes, freeBytes), degraded: state.truncated || usedBytes === null || freeBytes === null },
|
|
218
|
+
shares,
|
|
219
|
+
largestCandidates,
|
|
220
|
+
optimization: {
|
|
221
|
+
unmanic,
|
|
222
|
+
estimatedReclaimableBytes: null,
|
|
223
|
+
estimateConfidence: "unavailable",
|
|
224
|
+
reason: "Share usage is real, but the read-only Unraid API does not expose bounded file-level codec evidence; no savings estimate is claimed.",
|
|
225
|
+
},
|
|
226
|
+
truncated,
|
|
227
|
+
} };
|
|
201
228
|
}
|
|
202
229
|
catch (error) {
|
|
203
230
|
return fail(error);
|
|
@@ -284,6 +311,52 @@ const emptyParameters = { type: "object", properties: {}, additionalProperties:
|
|
|
284
311
|
function missingRuntime() {
|
|
285
312
|
return JSON.stringify({ ok: false, error: { code: "invalid_response", message: "Sanctuary runtime is unavailable", degraded: true } });
|
|
286
313
|
}
|
|
314
|
+
function householdRepairObligation(ctx, target) {
|
|
315
|
+
const requester = (0, tools_base_1.routineActionRequester)(ctx);
|
|
316
|
+
if (requester?.kind !== "household_request")
|
|
317
|
+
return null;
|
|
318
|
+
if (!ctx.agentRoot)
|
|
319
|
+
throw new Error("household repair request tracking is unavailable");
|
|
320
|
+
const existing = (0, obligations_1.findPendingObligationForRequest)(ctx.agentRoot, { requestId: requester.requestId, owedTo: requester.origin });
|
|
321
|
+
if (existing)
|
|
322
|
+
return existing;
|
|
323
|
+
return (0, obligations_1.createObligation)(ctx.agentRoot, {
|
|
324
|
+
origin: requester.origin,
|
|
325
|
+
owedTo: requester.origin,
|
|
326
|
+
requestId: requester.requestId,
|
|
327
|
+
sourceProvenance: { kind: "human_request", source: requester.origin.channel, ref: `request:${requester.requestId}` },
|
|
328
|
+
content: `Restore Sanctuary service ${target} and report the outcome`,
|
|
329
|
+
currentSurface: { kind: "runtime", label: `Sanctuary container ${target}` },
|
|
330
|
+
nextAction: `Restart ${target} under the Butler's standing grant and verify recovery`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async function runTrackedRestart(ctx, target, routine) {
|
|
334
|
+
const obligation = householdRepairObligation(ctx, target);
|
|
335
|
+
if (obligation && ctx.agentRoot)
|
|
336
|
+
(0, obligations_1.advanceObligation)(ctx.agentRoot, obligation.id, { status: "updating_runtime", latestNote: `Starting the requested restart of ${target}` });
|
|
337
|
+
try {
|
|
338
|
+
const result = await ctx.sanctuary.restartContainer({ container: target }, routine ? { routine } : undefined);
|
|
339
|
+
if (obligation && ctx.agentRoot) {
|
|
340
|
+
const succeeded = !!result && typeof result === "object" && !Array.isArray(result) && result.ok === true;
|
|
341
|
+
if (succeeded)
|
|
342
|
+
(0, obligations_1.advanceObligation)(ctx.agentRoot, obligation.id, {
|
|
343
|
+
status: "updating_runtime",
|
|
344
|
+
latestNote: `The requested restart of ${target} verified recovery`,
|
|
345
|
+
nextAction: "Report the verified outcome to the exact requester",
|
|
346
|
+
});
|
|
347
|
+
else
|
|
348
|
+
(0, obligations_1.advanceObligation)(ctx.agentRoot, obligation.id, { status: "investigating", latestNote: `The requested restart of ${target} did not verify recovery`, nextAction: `Diagnose ${target} and report back to the requester` });
|
|
349
|
+
if (succeeded)
|
|
350
|
+
(0, obligations_1.markObligationReturnReady)(ctx.agentRoot, obligation.id, `unraid-restart:${target}:verified`);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (obligation && ctx.agentRoot)
|
|
356
|
+
(0, obligations_1.advanceObligation)(ctx.agentRoot, obligation.id, { status: "investigating", latestNote: `The requested restart of ${target} failed`, nextAction: `Diagnose ${target} and report back to the requester` });
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
287
360
|
function readDefinition(name, description, method) {
|
|
288
361
|
return {
|
|
289
362
|
tool: { type: "function", function: { name, description, parameters: emptyParameters } },
|
|
@@ -319,12 +392,20 @@ exports.unraidToolDefinitions = [
|
|
|
319
392
|
readDefinition("unraid_get_disks", "Read bounded Sanctuary disk SMART, temperature, and parity health.", "getDisks"),
|
|
320
393
|
readDefinition("unraid_get_notifications", "Read bounded unacknowledged Sanctuary notifications.", "getNotifications"),
|
|
321
394
|
readDefinition("unraid_get_system", "Read bounded Sanctuary system and version health.", "getSystem"),
|
|
395
|
+
readDefinition("unraid_check_services", "Freshly check the fixed public Sanctuary service endpoints and return bounded status with an observation timestamp.", "checkServices"),
|
|
396
|
+
readDefinition("sanctuary_get_download_queue", "Read the bounded live household download queue state, including whether the spend guard has paused it.", "getDownloadQueue"),
|
|
397
|
+
{
|
|
398
|
+
tool: { type: "function", function: { name: "sanctuary_resume_download_queue", description: "Resume the household download queue after Ari confirms the provider is ready, then independently verify paused=false. This can spend prepaid download credit.", parameters: emptyParameters } },
|
|
399
|
+
handler: async (_args, ctx) => JSON.stringify(ctx?.sanctuary ? await ctx.sanctuary.resumeDownloadQueue() : JSON.parse(missingRuntime())),
|
|
400
|
+
riskProfile: { mutates: "external_side_effect", risk: "high", reason: "resumes downloads that can spend prepaid provider credit" },
|
|
401
|
+
approvalPolicy: () => ({ kind: "required", policyId: "sanctuary.downloads.resume.v1", actionClass: "sanctuary.downloads.resume", requiresSoleCall: true }),
|
|
402
|
+
},
|
|
322
403
|
{
|
|
323
404
|
tool: {
|
|
324
405
|
type: "function",
|
|
325
406
|
function: {
|
|
326
407
|
name: "unraid_restart_container",
|
|
327
|
-
description: "
|
|
408
|
+
description: "Restart one exact existing allowlisted Sanctuary container when standing policy allows; otherwise request approval.",
|
|
328
409
|
parameters: {
|
|
329
410
|
type: "object",
|
|
330
411
|
properties: { container: { type: "string" } },
|
|
@@ -333,9 +414,35 @@ exports.unraidToolDefinitions = [
|
|
|
333
414
|
},
|
|
334
415
|
},
|
|
335
416
|
},
|
|
336
|
-
handler: async (args, ctx) =>
|
|
337
|
-
|
|
338
|
-
|
|
417
|
+
handler: async (args, ctx) => {
|
|
418
|
+
if (!ctx?.sanctuary)
|
|
419
|
+
return missingRuntime();
|
|
420
|
+
const target = String(args.container);
|
|
421
|
+
let routine;
|
|
422
|
+
if (ctx.routineActionSelection) {
|
|
423
|
+
if (!ctx.agentRoot || !(0, tools_base_1.routineActionRequester)(ctx))
|
|
424
|
+
return JSON.stringify({ ok: false, error: { code: "approval_required", message: "routine action authorization is unavailable", degraded: true } });
|
|
425
|
+
const { key, expectedPolicyVersion } = ctx.routineActionSelection;
|
|
426
|
+
if (ctx.routineActionSelection.target !== target)
|
|
427
|
+
return JSON.stringify({ ok: false, error: { code: "approval_required", message: "routine action arguments changed", degraded: true } });
|
|
428
|
+
const decision = (0, steward_policy_1.inspectRoutineActionGrant)(ctx.agentRoot, { key, action: "unraid.container.restart", target, expectedPolicyVersion });
|
|
429
|
+
if (!decision.allowed)
|
|
430
|
+
return JSON.stringify({ ok: false, error: { code: "approval_required", message: decision.reason, degraded: true } });
|
|
431
|
+
routine = {
|
|
432
|
+
key,
|
|
433
|
+
expectedPolicyVersion: decision.policyVersion,
|
|
434
|
+
reauthorize: async () => {
|
|
435
|
+
const authorization = await ctx.relationshipAuthorization.authorizeTool("unraid_restart_container", { container: target });
|
|
436
|
+
if (!authorization.allowed)
|
|
437
|
+
return authorization;
|
|
438
|
+
if (!Number.isInteger(authorization.profileVersion) || Number(authorization.profileVersion) < 1)
|
|
439
|
+
return { allowed: false, reason: "relationship capability profile is not versioned" };
|
|
440
|
+
return { allowed: true, receiptId: authorization.receiptId, profileVersion: Number(authorization.profileVersion) };
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return JSON.stringify(await runTrackedRestart(ctx, target, routine));
|
|
445
|
+
},
|
|
339
446
|
riskProfile: { mutates: "external_side_effect", risk: "high", reason: "restarts one existing allowlisted Docker container" },
|
|
340
447
|
approvalPolicy: () => ({
|
|
341
448
|
kind: "required",
|
|
@@ -349,5 +456,5 @@ exports.unraidToolDefinitions = [
|
|
|
349
456
|
component: "repertoire",
|
|
350
457
|
event: "repertoire.unraid_tools_loaded",
|
|
351
458
|
message: "typed Unraid tools loaded",
|
|
352
|
-
meta: { operations:
|
|
459
|
+
meta: { operations: 9 },
|
|
353
460
|
});
|