@ouro.bot/cli 0.1.0-alpha.741 → 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.
Files changed (81) hide show
  1. package/changelog.json +42 -0
  2. package/deploy/unraid/Dockerfile +7 -2
  3. package/deploy/unraid/README.txt +717 -106
  4. package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
  5. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  6. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  7. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  8. package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
  9. package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
  10. package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
  12. package/deploy/unraid/sanctuary.ouro/agent.json +2 -2
  13. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
  14. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
  15. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +3 -10
  16. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
  17. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
  18. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
  19. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
  20. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
  21. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  22. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
  23. package/deploy/unraid/sanctuary.xml +12 -9
  24. package/dist/arc/cares.js +171 -37
  25. package/dist/arc/obligations.js +38 -0
  26. package/dist/heart/awaiting/await-alert.js +4 -2
  27. package/dist/heart/awaiting/await-expiry.js +39 -4
  28. package/dist/heart/awaiting/await-parser.js +8 -0
  29. package/dist/heart/context-loss-gauntlet.js +31 -2
  30. package/dist/heart/context-loss-sentinel.js +2 -0
  31. package/dist/heart/core.js +37 -7
  32. package/dist/heart/cross-chat-delivery.js +1 -0
  33. package/dist/heart/daemon/container-healthcheck.js +3 -1
  34. package/dist/heart/daemon/container-runtime.js +13 -0
  35. package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
  36. package/dist/heart/daemon/container-spec-auditor.js +38 -11
  37. package/dist/heart/daemon/daemon-entry.js +19 -5
  38. package/dist/heart/daemon/daemon.js +154 -22
  39. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +151 -87
  40. package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
  41. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
  42. package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
  43. package/dist/heart/daemon/sanctuary-scheduler-origin.js +11 -5
  44. package/dist/heart/daemon/supercronic-supervisor.js +16 -1
  45. package/dist/heart/external-events/router.js +1025 -43
  46. package/dist/heart/provider-models.js +10 -3
  47. package/dist/heart/session-events.js +23 -5
  48. package/dist/heart/steward-policy.js +302 -0
  49. package/dist/heart/tool-approval.js +3 -1
  50. package/dist/heart/turn-context.js +1 -0
  51. package/dist/mind/context.js +8 -3
  52. package/dist/mind/pending.js +25 -0
  53. package/dist/mind/prompt.js +49 -1
  54. package/dist/repertoire/guardrails.js +2 -1
  55. package/dist/repertoire/relationship-authorization.js +194 -0
  56. package/dist/repertoire/tools-awaiting.js +229 -26
  57. package/dist/repertoire/tools-base.js +15 -0
  58. package/dist/repertoire/tools-continuity.js +259 -4
  59. package/dist/repertoire/tools-notes.js +29 -3
  60. package/dist/repertoire/tools-session.js +219 -4
  61. package/dist/repertoire/tools-steward-policy.js +88 -0
  62. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  63. package/dist/repertoire/tools-unraid.js +113 -6
  64. package/dist/repertoire/tools.js +104 -9
  65. package/dist/repertoire/unraid-restart.js +86 -4
  66. package/dist/senses/private-runtime-worker.js +36 -3
  67. package/dist/senses/private-runtime.js +310 -63
  68. package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
  69. package/dist/senses/sanctuary-health-runner.js +51 -96
  70. package/dist/senses/sanctuary-health.js +54 -93
  71. package/dist/senses/sanctuary-runtime.js +36 -2
  72. package/dist/senses/sanctuary-sab.js +80 -0
  73. package/dist/senses/shared-turn.js +64 -9
  74. package/dist/senses/telegram-admission.js +684 -0
  75. package/dist/senses/telegram-approval-runtime.js +46 -6
  76. package/dist/senses/telegram-client.js +236 -72
  77. package/dist/senses/telegram-effect-adapter.js +787 -0
  78. package/dist/senses/telegram-entry.js +4 -1
  79. package/dist/senses/telegram.js +728 -48
  80. package/npm-shrinkwrap.json +6 -6
  81. 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. use this instead of piecing status together from separate session and coding tools.",
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 (_args, ctx) => {
643
+ handler: async (args, ctx) => {
433
644
  const frame = await buildToolActiveWorkFrame(ctx);
434
- return `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)}`;
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
- return { ok: true, data: { sourceIdentityDigest, array: { state: state.value, usedBytes, freeBytes, usedPercent: percent(usedBytes, freeBytes), degraded: state.truncated || usedBytes === null || freeBytes === null }, shares, truncated } };
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: "Propose restarting one exact existing allowlisted Sanctuary container. Execution requires human approval.",
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) => JSON.stringify(ctx?.sanctuary
337
- ? await ctx.sanctuary.restartContainer({ container: String(args.container) })
338
- : JSON.parse(missingRuntime())),
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: 6 },
459
+ meta: { operations: 9 },
353
460
  });