@ouro.bot/cli 0.1.0-alpha.733 → 0.1.0-alpha.734

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 (89) hide show
  1. package/changelog.json +25 -0
  2. package/deploy/unraid/Dockerfile +27 -0
  3. package/deploy/unraid/README.txt +1368 -0
  4. package/deploy/unraid/audit-container-spec.sh +4 -0
  5. package/deploy/unraid/container-runtime.json +1 -0
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
  8. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  9. package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
  10. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
  12. package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
  13. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  14. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  15. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  16. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  17. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  18. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
  19. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
  20. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
  21. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
  22. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
  23. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
  24. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
  25. package/deploy/unraid/sanctuary.xml +25 -0
  26. package/dist/heart/approval-files.js +113 -0
  27. package/dist/heart/approval-store.js +168 -9
  28. package/dist/heart/auth/auth-flow.js +17 -1
  29. package/dist/heart/autonomy-budget.js +44 -1
  30. package/dist/heart/awaiting/await-scheduler.js +6 -2
  31. package/dist/heart/config.js +8 -0
  32. package/dist/heart/core.js +117 -132
  33. package/dist/heart/daemon/cli-exec.js +73 -1
  34. package/dist/heart/daemon/cli-help.js +5 -0
  35. package/dist/heart/daemon/cli-parse.js +6 -4
  36. package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
  37. package/dist/heart/daemon/container-healthcheck.js +94 -0
  38. package/dist/heart/daemon/container-runtime.js +84 -0
  39. package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
  40. package/dist/heart/daemon/container-spec-auditor.js +156 -0
  41. package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
  42. package/dist/heart/daemon/daemon-entry.js +116 -28
  43. package/dist/heart/daemon/daemon-health.js +10 -0
  44. package/dist/heart/daemon/daemon.js +92 -5
  45. package/dist/heart/daemon/runtime-logging.js +6 -0
  46. package/dist/heart/daemon/runtime-mode.js +13 -2
  47. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
  48. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
  49. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  50. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
  51. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  52. package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
  53. package/dist/heart/daemon/sense-manager.js +25 -6
  54. package/dist/heart/daemon/supercronic-supervisor.js +267 -0
  55. package/dist/heart/habits/habit-scheduler.js +7 -3
  56. package/dist/heart/identity.js +13 -3
  57. package/dist/heart/provider-credentials.js +9 -1
  58. package/dist/heart/provider-models.js +8 -0
  59. package/dist/heart/provider-ping.js +5 -2
  60. package/dist/heart/providers/openai-compatible.js +177 -0
  61. package/dist/heart/runtime-credentials.js +217 -8
  62. package/dist/heart/sense-truth.js +2 -0
  63. package/dist/heart/tool-approval.js +1 -0
  64. package/dist/heart/turn-context.js +8 -0
  65. package/dist/mind/prompt.js +9 -0
  66. package/dist/nerves/coverage/source-scanner.js +4 -4
  67. package/dist/nerves/index.js +75 -9
  68. package/dist/nerves/runtime.js +12 -0
  69. package/dist/repertoire/tool-arguments.js +39 -0
  70. package/dist/repertoire/tools-unraid.js +353 -0
  71. package/dist/repertoire/tools.js +19 -1
  72. package/dist/repertoire/unraid-client.js +155 -0
  73. package/dist/repertoire/unraid-restart.js +130 -0
  74. package/dist/senses/private-runtime-worker.js +3 -2
  75. package/dist/senses/sanctuary-grounding.js +118 -0
  76. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  77. package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
  78. package/dist/senses/sanctuary-health-runner.js +146 -0
  79. package/dist/senses/sanctuary-health.js +447 -0
  80. package/dist/senses/sanctuary-interactive-control.js +306 -0
  81. package/dist/senses/sanctuary-runtime.js +206 -0
  82. package/dist/senses/shared-turn.js +11 -3
  83. package/dist/senses/telegram-approval-runtime.js +526 -0
  84. package/dist/senses/telegram-audit-ledger.js +220 -0
  85. package/dist/senses/telegram-client.js +1471 -0
  86. package/dist/senses/telegram-entry.js +80 -0
  87. package/dist/senses/telegram.js +1232 -0
  88. package/npm-shrinkwrap.json +6558 -0
  89. package/package.json +4 -2
@@ -0,0 +1,353 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unraidToolDefinitions = exports.SANCTUARY_SYSTEM_QUERY = exports.SANCTUARY_NOTIFICATIONS_QUERY = exports.SANCTUARY_DISKS_QUERY = exports.SANCTUARY_STORAGE_QUERY = exports.SANCTUARY_CONTAINER_LOGS_QUERY = exports.SANCTUARY_CONTAINERS_QUERY = void 0;
4
+ exports.normalizeDockerStatus = normalizeDockerStatus;
5
+ exports.createUnraidReadTools = createUnraidReadTools;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const runtime_1 = require("../nerves/runtime");
8
+ const unraid_client_1 = require("./unraid-client");
9
+ exports.SANCTUARY_CONTAINERS_QUERY = `query SanctuaryContainers {
10
+ docker { containers(skipCache: true) { id names state status autoStart } }
11
+ }`;
12
+ exports.SANCTUARY_CONTAINER_LOGS_QUERY = `query SanctuaryContainerLogs($id: PrefixedID!, $tail: Int!) {
13
+ docker { logs(id: $id, tail: $tail) { containerId lines { timestamp message } cursor } }
14
+ }`;
15
+ exports.SANCTUARY_STORAGE_QUERY = `query SanctuaryStorage {
16
+ vars { id }
17
+ array { state capacity { kilobytes { used free total } } }
18
+ shares { id name used free size }
19
+ }`;
20
+ exports.SANCTUARY_DISKS_QUERY = `query SanctuaryDisks {
21
+ disks { id name smartStatus temperature }
22
+ array { parityCheckStatus { status date duration errors progress correcting paused running } }
23
+ }`;
24
+ exports.SANCTUARY_NOTIFICATIONS_QUERY = `query SanctuaryNotifications {
25
+ notifications { list(filter: {type: UNREAD, offset: 0, limit: 100}) { id timestamp importance title subject description type } }
26
+ }`;
27
+ exports.SANCTUARY_SYSTEM_QUERY = `query SanctuarySystem {
28
+ vars { id name version }
29
+ info { time os { uptime } versions { core { unraid api } } }
30
+ array { state }
31
+ }`;
32
+ function utf8Bytes(value) { return Buffer.byteLength(value, "utf8"); }
33
+ function identifier(value, label, maxBytes = 128) {
34
+ if (typeof value !== "string" || value.length === 0 || utf8Bytes(value) > maxBytes || value.includes("\uFFFD")) {
35
+ throw new unraid_client_1.UnraidClientError("invalid_response", `${label} is invalid`);
36
+ }
37
+ return value;
38
+ }
39
+ function display(value, maxBytes) {
40
+ if (typeof value !== "string")
41
+ return { value: "", truncated: true };
42
+ const bytes = Buffer.from(value, "utf8");
43
+ if (bytes.length <= maxBytes)
44
+ return { value, truncated: false };
45
+ let prefix = bytes.subarray(0, maxBytes - 3).toString("utf8");
46
+ while (prefix.endsWith("\uFFFD"))
47
+ prefix = prefix.slice(0, -1);
48
+ return { value: `${prefix}...`, truncated: true };
49
+ }
50
+ const DURATION = "(?:Less than a second|About a (?:minute|hour|day|week|month|year)|(?:[1-9][0-9]*) (?:seconds?|minutes?|hours?|days?|weeks?|months?|years?))";
51
+ const RUNNING = new RegExp(`^Up ${DURATION}(?: \\((?:healthy|unhealthy|health: starting)\\))?$`, "u");
52
+ const EXITED = new RegExp(`^Exited \\((0|[1-9][0-9]*)\\) ${DURATION} ago$`, "u");
53
+ const RESTARTING = new RegExp(`^Restarting \\((0|[1-9][0-9]*)\\) ${DURATION} ago$`, "u");
54
+ function exitCode(match) {
55
+ const value = Number(match[1]);
56
+ return Number.isSafeInteger(value) && value >= 0 && value <= 4_294_967_295 ? value : null;
57
+ }
58
+ function normalizeDockerStatus(structuredState, status) {
59
+ if (typeof status !== "string")
60
+ return { state: "unknown", exitCode: null, degraded: true };
61
+ const state = typeof structuredState === "string" ? structuredState.toUpperCase() : "";
62
+ if (state === "RUNNING" && RUNNING.test(status))
63
+ return { state: "running", exitCode: null, degraded: false };
64
+ const exited = status.match(EXITED);
65
+ if (state === "EXITED" && exited) {
66
+ const code = exitCode(exited);
67
+ return code === null ? { state: "unknown", exitCode: null, degraded: true } : { state: "exited", exitCode: code, degraded: false };
68
+ }
69
+ const restarting = status.match(RESTARTING);
70
+ if ((state === "RUNNING" || state === "EXITED") && restarting) {
71
+ const code = exitCode(restarting);
72
+ return code === null ? { state: "unknown", exitCode: null, degraded: true } : { state: "restarting", exitCode: code, degraded: false };
73
+ }
74
+ return { state: "unknown", exitCode: null, degraded: true };
75
+ }
76
+ function numberOrNull(value) {
77
+ const number = typeof value === "bigint" ? Number(value) : typeof value === "string" && /^-?[0-9]+$/u.test(value) ? Number(value) : value;
78
+ return typeof number === "number" && Number.isSafeInteger(number) && number >= 0 ? number : null;
79
+ }
80
+ function percent(used, free) {
81
+ if (used === null || free === null || used + free === 0)
82
+ return null;
83
+ return Number(((used / (used + free)) * 100).toFixed(2));
84
+ }
85
+ function record(value, label) {
86
+ if (!value || typeof value !== "object" || Array.isArray(value))
87
+ throw new unraid_client_1.UnraidClientError("invalid_response", `${label} is invalid`);
88
+ return value;
89
+ }
90
+ function array(value, label) {
91
+ if (!Array.isArray(value))
92
+ throw new unraid_client_1.UnraidClientError("invalid_response", `${label} is invalid`);
93
+ return value;
94
+ }
95
+ function fail(error) {
96
+ const normalized = error instanceof unraid_client_1.UnraidClientError
97
+ ? error
98
+ : new unraid_client_1.UnraidClientError("invalid_response", error instanceof Error ? error.message : String(error));
99
+ return { ok: false, error: { code: normalized.code, message: normalized.message, degraded: true } };
100
+ }
101
+ function containerName(names) {
102
+ const values = array(names, "container names");
103
+ if (values.length !== 1)
104
+ throw new unraid_client_1.UnraidClientError("invalid_response", "container names are ambiguous");
105
+ return identifier(values[0], "container name").replace(/^\//u, "");
106
+ }
107
+ function mapContainers(data) {
108
+ const docker = record(data.docker, "docker response");
109
+ const raw = array(docker.containers, "container list");
110
+ const truncated = raw.length > 200;
111
+ const containers = raw.slice(0, 200).map((entry) => {
112
+ const item = record(entry, "container");
113
+ const id = unraid_client_1.UnraidClient.assertPrefixedId(identifier(item.id, "container id", 256));
114
+ const name = containerName(item.names);
115
+ const shownStatus = display(item.status, 256);
116
+ const normalized = normalizeDockerStatus(item.state, shownStatus.value);
117
+ return {
118
+ id,
119
+ name,
120
+ autostart: typeof item.autoStart === "boolean" ? item.autoStart : false,
121
+ ...normalized,
122
+ degraded: normalized.degraded || shownStatus.truncated || typeof item.autoStart !== "boolean",
123
+ status: shownStatus.value,
124
+ };
125
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
126
+ return { containers, truncated };
127
+ }
128
+ function validTimestamp(value) {
129
+ if (typeof value !== "string" || utf8Bytes(value) > 64 || !/(?:Z|[+-][0-9]{2}:[0-9]{2})$/u.test(value))
130
+ return null;
131
+ const time = Date.parse(value);
132
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
133
+ }
134
+ function liveSourceIdentityDigest(value) {
135
+ const prefixedId = identifier(value, "server identity", 129);
136
+ if (!/^[0-9a-f]{64}:[0-9a-f]{64}$/u.test(prefixedId))
137
+ throw new unraid_client_1.UnraidClientError("invalid_response", "server identity is invalid");
138
+ return (0, node_crypto_1.createHash)("sha256").update(prefixedId).digest("hex");
139
+ }
140
+ function createUnraidReadTools(client) {
141
+ const listContainers = async () => {
142
+ try {
143
+ const data = await client.read(exports.SANCTUARY_CONTAINERS_QUERY, {});
144
+ return { ok: true, data: mapContainers(data) };
145
+ }
146
+ catch (error) {
147
+ return fail(error);
148
+ }
149
+ };
150
+ return {
151
+ listContainers,
152
+ async getContainerLogs(args) {
153
+ try {
154
+ const wanted = identifier(args.container, "container argument");
155
+ if (!Number.isInteger(args.tailLines) || args.tailLines < 1 || args.tailLines > 200)
156
+ throw new unraid_client_1.UnraidClientError("invalid_response", "tailLines must be an integer from 1 to 200");
157
+ const listed = await listContainers();
158
+ if (!listed.ok)
159
+ return listed;
160
+ const matches = listed.data.containers.filter((container) => container.name === wanted);
161
+ if (matches.length === 0)
162
+ return { ok: false, error: { code: "not_found", message: `container '${wanted}' was not found`, degraded: true } };
163
+ if (matches.length !== 1)
164
+ return { ok: false, error: { code: "ambiguous", message: `container '${wanted}' is ambiguous`, degraded: true } };
165
+ const target = matches[0];
166
+ const data = await client.read(exports.SANCTUARY_CONTAINER_LOGS_QUERY, { id: target.id, tail: args.tailLines });
167
+ const logs = record(record(data.docker, "docker response").logs, "container logs");
168
+ if (logs.containerId !== target.id)
169
+ throw new unraid_client_1.UnraidClientError("invalid_response", "container log identity drifted");
170
+ const text = array(logs.lines, "container log lines").map((line) => display(record(line, "log line").message, 1_000_000).value).join("\n");
171
+ const originalBytes = utf8Bytes(text);
172
+ const suffix = Buffer.from(text, "utf8").subarray(Math.max(0, originalBytes - 65_536)).toString("utf8").replace(/^\uFFFD/u, "");
173
+ const returnedBytes = utf8Bytes(suffix);
174
+ return { ok: true, data: { container: { id: target.id, name: target.name }, text: suffix, originalBytes, returnedBytes, truncated: returnedBytes < originalBytes } };
175
+ }
176
+ catch (error) {
177
+ return fail(error);
178
+ }
179
+ },
180
+ async getStorage() {
181
+ try {
182
+ const data = await client.read(exports.SANCTUARY_STORAGE_QUERY, {});
183
+ const sourceIdentityDigest = liveSourceIdentityDigest(record(data.vars, "vars").id);
184
+ const arrayRecord = record(data.array, "array");
185
+ const kb = record(record(arrayRecord.capacity, "array capacity").kilobytes, "array capacity kilobytes");
186
+ const usedKb = numberOrNull(kb.used);
187
+ const freeKb = numberOrNull(kb.free);
188
+ const usedBytes = usedKb === null ? null : usedKb * 1024;
189
+ const freeBytes = freeKb === null ? null : freeKb * 1024;
190
+ const state = display(arrayRecord.state, 128);
191
+ const rawShares = array(data.shares, "shares");
192
+ const truncated = rawShares.length > 256;
193
+ const shares = rawShares.slice(0, 256).map((entry) => {
194
+ const item = record(entry, "share");
195
+ const name = identifier(item.name, "share name");
196
+ const used = numberOrNull(item.used);
197
+ const free = numberOrNull(item.free);
198
+ return { name, usedBytes: used, freeBytes: free, usedPercent: percent(used, free), degraded: used === null || free === null };
199
+ }).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 } };
201
+ }
202
+ catch (error) {
203
+ return fail(error);
204
+ }
205
+ },
206
+ async getDisks() {
207
+ try {
208
+ const data = await client.read(exports.SANCTUARY_DISKS_QUERY, {});
209
+ const rawDisks = array(data.disks, "disks");
210
+ const truncated = rawDisks.length > 64;
211
+ const disks = rawDisks.slice(0, 64).map((entry) => {
212
+ const item = record(entry, "disk");
213
+ const id = identifier(item.id, "disk id");
214
+ const name = identifier(item.name, "disk name");
215
+ const smartRaw = typeof item.smartStatus === "string" ? item.smartStatus.toLowerCase() : "";
216
+ const smart = smartRaw === "passed" ? "passed" : smartRaw === "failed" ? "failed" : "unknown";
217
+ const temperatureC = typeof item.temperature === "number" && Number.isFinite(item.temperature) ? item.temperature : null;
218
+ return { id, name, smart, temperatureC, degraded: smart === "unknown" || temperatureC === null };
219
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
220
+ const parityRaw = record(record(data.array, "array").parityCheckStatus, "parity status");
221
+ const completedAt = validTimestamp(parityRaw.date);
222
+ const errors = numberOrNull(parityRaw.errors);
223
+ const result = errors === 0 && typeof parityRaw.status === "string" && /complete|success/i.test(parityRaw.status) ? "success" : errors !== null && errors > 0 ? "failed" : "unknown";
224
+ const elapsedMs = completedAt ? Date.now() - Date.parse(completedAt) : null;
225
+ const ageHours = elapsedMs !== null && elapsedMs >= 0 ? elapsedMs / 3_600_000 : null;
226
+ return { ok: true, data: { disks, parity: { result, completedAt, ageHours, degraded: result === "unknown" || completedAt === null || ageHours === null }, truncated } };
227
+ }
228
+ catch (error) {
229
+ return fail(error);
230
+ }
231
+ },
232
+ async getNotifications() {
233
+ try {
234
+ const data = await client.read(exports.SANCTUARY_NOTIFICATIONS_QUERY, {});
235
+ const raw = array(record(data.notifications, "notifications").list, "notification list");
236
+ const truncated = raw.length > 100;
237
+ const unacknowledged = raw.slice(0, 100).map((entry) => {
238
+ const item = record(entry, "notification");
239
+ const id = identifier(item.id, "notification id");
240
+ const createdAt = validTimestamp(item.timestamp);
241
+ const importance = typeof item.importance === "string" ? item.importance.toLowerCase() : "";
242
+ const severity = importance === "info" || importance === "warning" || importance === "error" ? importance : "unknown";
243
+ const title = display(item.title, 160);
244
+ const subject = display(item.subject, 250);
245
+ const description = display(item.description, 250);
246
+ const summary = display([subject.value, description.value].filter(Boolean).join("\n"), 500);
247
+ return { id, createdAt, severity, title: title.value, summary: summary.value, degraded: !createdAt || severity === "unknown" || title.truncated || subject.truncated || description.truncated || summary.truncated };
248
+ }).sort((a, b) => {
249
+ if (a.createdAt && b.createdAt)
250
+ return b.createdAt.localeCompare(a.createdAt);
251
+ if (a.createdAt)
252
+ return -1;
253
+ if (b.createdAt)
254
+ return 1;
255
+ return a.id.localeCompare(b.id);
256
+ });
257
+ return { ok: true, data: { unacknowledged, truncated } };
258
+ }
259
+ catch (error) {
260
+ return fail(error);
261
+ }
262
+ },
263
+ async getSystem() {
264
+ try {
265
+ const data = await client.read(exports.SANCTUARY_SYSTEM_QUERY, {});
266
+ const vars = record(data.vars, "vars");
267
+ const info = record(data.info, "info");
268
+ const versions = record(record(info.versions, "versions").core, "core versions");
269
+ const sourceIdentityDigest = liveSourceIdentityDigest(vars.id);
270
+ const name = display(vars.name, 128);
271
+ const unraid = display(versions.unraid ?? vars.version, 128);
272
+ const api = display(versions.api, 128);
273
+ const state = display(record(data.array, "array").state, 128);
274
+ const uptimeSeconds = numberOrNull(record(info.os, "os").uptime);
275
+ return { ok: true, data: { sourceIdentityDigest, serverName: name.value, unraidVersion: unraid.value, apiVersion: api.value, arrayState: state.value, uptimeSeconds, degraded: name.truncated || unraid.truncated || api.truncated || state.truncated || uptimeSeconds === null } };
276
+ }
277
+ catch (error) {
278
+ return fail(error);
279
+ }
280
+ },
281
+ };
282
+ }
283
+ const emptyParameters = { type: "object", properties: {}, additionalProperties: false };
284
+ function missingRuntime() {
285
+ return JSON.stringify({ ok: false, error: { code: "invalid_response", message: "Sanctuary runtime is unavailable", degraded: true } });
286
+ }
287
+ function readDefinition(name, description, method) {
288
+ return {
289
+ tool: { type: "function", function: { name, description, parameters: emptyParameters } },
290
+ handler: async (_args, ctx) => JSON.stringify(ctx?.sanctuary ? await ctx.sanctuary[method]() : JSON.parse(missingRuntime())),
291
+ riskProfile: { mutates: "none", risk: "low" },
292
+ };
293
+ }
294
+ exports.unraidToolDefinitions = [
295
+ readDefinition("unraid_list_containers", "List bounded Sanctuary Docker container status.", "listContainers"),
296
+ {
297
+ tool: {
298
+ type: "function",
299
+ function: {
300
+ name: "unraid_get_container_logs",
301
+ description: "Read the recent bounded log suffix for one exact Sanctuary container name.",
302
+ parameters: {
303
+ type: "object",
304
+ properties: {
305
+ container: { type: "string" },
306
+ tailLines: { type: "integer", minimum: 1, maximum: 200 },
307
+ },
308
+ required: ["container", "tailLines"],
309
+ additionalProperties: false,
310
+ },
311
+ },
312
+ },
313
+ handler: async (args, ctx) => JSON.stringify(ctx?.sanctuary
314
+ ? await ctx.sanctuary.getContainerLogs({ container: String(args.container), tailLines: Number(args.tailLines) })
315
+ : JSON.parse(missingRuntime())),
316
+ riskProfile: { mutates: "none", risk: "low" },
317
+ },
318
+ readDefinition("unraid_get_storage", "Read bounded Sanctuary array and share capacity health.", "getStorage"),
319
+ readDefinition("unraid_get_disks", "Read bounded Sanctuary disk SMART, temperature, and parity health.", "getDisks"),
320
+ readDefinition("unraid_get_notifications", "Read bounded unacknowledged Sanctuary notifications.", "getNotifications"),
321
+ readDefinition("unraid_get_system", "Read bounded Sanctuary system and version health.", "getSystem"),
322
+ {
323
+ tool: {
324
+ type: "function",
325
+ function: {
326
+ name: "unraid_restart_container",
327
+ description: "Propose restarting one exact existing allowlisted Sanctuary container. Execution requires human approval.",
328
+ parameters: {
329
+ type: "object",
330
+ properties: { container: { type: "string" } },
331
+ required: ["container"],
332
+ additionalProperties: false,
333
+ },
334
+ },
335
+ },
336
+ handler: async (args, ctx) => JSON.stringify(ctx?.sanctuary
337
+ ? await ctx.sanctuary.restartContainer({ container: String(args.container) })
338
+ : JSON.parse(missingRuntime())),
339
+ riskProfile: { mutates: "external_side_effect", risk: "high", reason: "restarts one existing allowlisted Docker container" },
340
+ approvalPolicy: () => ({
341
+ kind: "required",
342
+ policyId: "sanctuary.unraid.restart.v1",
343
+ actionClass: "unraid.container.restart",
344
+ requiresSoleCall: true,
345
+ }),
346
+ },
347
+ ];
348
+ (0, runtime_1.emitNervesEvent)({
349
+ component: "repertoire",
350
+ event: "repertoire.unraid_tools_loaded",
351
+ message: "typed Unraid tools loaded",
352
+ meta: { operations: 6 },
353
+ });
@@ -25,6 +25,8 @@ const tools_surface_1 = require("./tools-surface");
25
25
  const mcp_tools_1 = require("./mcp-tools");
26
26
  const tools_voice_1 = require("./tools-voice");
27
27
  const shell_sessions_1 = require("./shell-sessions");
28
+ const tools_unraid_1 = require("./tools-unraid");
29
+ const tools_flow_1 = require("./tools-flow");
28
30
  function safeGetAgentRoot() {
29
31
  try {
30
32
  return (0, identity_1.getAgentRoot)();
@@ -33,6 +35,14 @@ function safeGetAgentRoot() {
33
35
  return undefined;
34
36
  }
35
37
  }
38
+ function isSanctuaryAgent() {
39
+ try {
40
+ return (0, identity_1.getAgentName)() === "sanctuary";
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
36
46
  // Re-export types and constants used by the rest of the codebase
37
47
  var tools_base_2 = require("./tools-base");
38
48
  Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_base_2.tools; } });
@@ -45,7 +55,7 @@ Object.defineProperty(exports, "speakTool", { enumerable: true, get: function ()
45
55
  var tools_surface_2 = require("./tools-surface");
46
56
  Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: function () { return tools_surface_2.surfaceToolDef; } });
47
57
  // All tool definitions in a single registry
48
- const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, tools_surface_1.surfaceToolDefinition];
58
+ const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_surface_1.surfaceToolDefinition];
49
59
  const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
50
60
  // MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
51
61
  // Kept separate from allDefinitions so execTool can find them.
@@ -85,6 +95,14 @@ function filterByCapability(toolList, providerCapabilities) {
85
95
  // When toolPreferences is provided, matching preferences are appended to tool descriptions.
86
96
  // When providerCapabilities is provided, tools with requiredCapability are filtered.
87
97
  function getToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities, mcpManager, _chatModel) {
98
+ if (capabilities?.channel === "telegram" && isSanctuaryAgent()) {
99
+ return [
100
+ ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool),
101
+ tools_flow_1.ponderTool,
102
+ tools_flow_1.settleTool,
103
+ tools_flow_1.speakTool,
104
+ ];
105
+ }
88
106
  const baseTools = baseToolsForCapabilities();
89
107
  const bluebubblesTools = capabilities?.channel === "bluebubbles"
90
108
  ? tools_bluebubbles_1.bluebubblesToolDefinitions.map((d) => d.tool)
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnraidClient = exports.UnraidClientError = void 0;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ class UnraidClientError extends Error {
6
+ code;
7
+ ambiguous;
8
+ constructor(code, message, ambiguous = false, options = {}) {
9
+ super(limitUtf8(message, 240), options.cause === undefined ? undefined : { cause: options.cause });
10
+ this.code = code;
11
+ this.ambiguous = ambiguous;
12
+ this.name = "UnraidClientError";
13
+ }
14
+ }
15
+ exports.UnraidClientError = UnraidClientError;
16
+ function limitUtf8(value, maxBytes) {
17
+ const bytes = Buffer.from(value, "utf8");
18
+ if (bytes.length <= maxBytes)
19
+ return value;
20
+ return bytes.subarray(0, Math.max(0, maxBytes - 3)).toString("utf8").replace(/\uFFFD+$/u, "") + "...";
21
+ }
22
+ function canonicalEndpoint(value) {
23
+ let url;
24
+ try {
25
+ url = new URL(value);
26
+ }
27
+ catch {
28
+ throw new Error("Unraid GraphQL endpoint is invalid");
29
+ }
30
+ if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password || url.search || url.hash) {
31
+ throw new Error("Unraid GraphQL endpoint is invalid");
32
+ }
33
+ return url.toString();
34
+ }
35
+ function statusError(status) {
36
+ if (status === 401)
37
+ return new UnraidClientError("unauthorized", "Unraid GraphQL request was unauthorized");
38
+ if (status === 403)
39
+ return new UnraidClientError("forbidden", "Unraid GraphQL request was forbidden");
40
+ return new UnraidClientError("transport", `Unraid GraphQL request failed with HTTP ${status}`);
41
+ }
42
+ class UnraidClient {
43
+ endpoint;
44
+ apiKey;
45
+ fetchImpl;
46
+ sleep;
47
+ readTimeoutMs;
48
+ mutationTimeoutMs;
49
+ constructor(options) {
50
+ this.endpoint = canonicalEndpoint(options.endpoint);
51
+ this.apiKey = options.apiKey.trim();
52
+ if (!this.apiKey)
53
+ throw new Error("Unraid API key is missing");
54
+ this.fetchImpl = options.fetch ?? fetch;
55
+ this.sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
56
+ this.readTimeoutMs = options.readTimeoutMs ?? 10_000;
57
+ this.mutationTimeoutMs = options.mutationTimeoutMs ?? 15_000;
58
+ }
59
+ static assertPrefixedId(value) {
60
+ if (Buffer.byteLength(value, "utf8") > 256 || !/^[^:]+:[^:]+$/u.test(value)) {
61
+ throw new Error("Unraid identifier must be one bounded prefixed ID");
62
+ }
63
+ return value;
64
+ }
65
+ read(document, variables, signal) {
66
+ return this.execute("read", document, variables, signal);
67
+ }
68
+ mutate(document, variables, signal) {
69
+ return this.execute("mutation", document, variables, signal);
70
+ }
71
+ async execute(kind, document, variables, signal) {
72
+ (0, runtime_1.emitNervesEvent)({
73
+ component: "repertoire",
74
+ event: "repertoire.unraid_request_start",
75
+ message: "Unraid GraphQL request started",
76
+ meta: { kind },
77
+ });
78
+ const delays = [250, 1000];
79
+ const maxAttempts = kind === "read" ? 3 : 1;
80
+ for (let attempt = 0;; attempt += 1) {
81
+ const timeout = AbortSignal.timeout(kind === "read" ? this.readTimeoutMs : this.mutationTimeoutMs);
82
+ const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
83
+ let response;
84
+ try {
85
+ response = await this.fetchImpl(this.endpoint, {
86
+ method: "POST",
87
+ headers: { "x-api-key": this.apiKey, "Content-Type": "application/json" },
88
+ body: JSON.stringify({ query: document, variables }),
89
+ signal: requestSignal,
90
+ });
91
+ }
92
+ catch (error) {
93
+ const timedOut = timeout.aborted && !signal?.aborted;
94
+ if (kind === "read" && !timedOut && !signal?.aborted && attempt < maxAttempts - 1) {
95
+ await this.sleep(delays[attempt]);
96
+ continue;
97
+ }
98
+ const normalized = new UnraidClientError(timedOut ? "timeout" : "transport", timedOut ? "Unraid GraphQL request timed out" : "Unraid GraphQL transport failed", kind === "mutation" && !timedOut, { cause: error });
99
+ this.emitError(kind, normalized);
100
+ throw normalized;
101
+ }
102
+ if (!response.ok) {
103
+ if (kind === "read" && [429, 502, 503, 504].includes(response.status) && attempt < maxAttempts - 1) {
104
+ await this.sleep(delays[attempt]);
105
+ continue;
106
+ }
107
+ const error = statusError(response.status);
108
+ this.emitError(kind, error);
109
+ throw error;
110
+ }
111
+ let envelope;
112
+ try {
113
+ envelope = await response.json();
114
+ }
115
+ catch (cause) {
116
+ const error = new UnraidClientError("invalid_response", "Unraid GraphQL returned invalid JSON", false, { cause });
117
+ this.emitError(kind, error);
118
+ throw error;
119
+ }
120
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
121
+ const error = new UnraidClientError("invalid_response", "Unraid GraphQL response envelope is invalid");
122
+ this.emitError(kind, error);
123
+ throw error;
124
+ }
125
+ const record = envelope;
126
+ if (Array.isArray(record.errors) && record.errors.length > 0) {
127
+ const error = new UnraidClientError("graphql", "Unraid GraphQL operation failed");
128
+ this.emitError(kind, error);
129
+ throw error;
130
+ }
131
+ if (!record.data || typeof record.data !== "object" || Array.isArray(record.data)) {
132
+ const error = new UnraidClientError("invalid_response", "Unraid GraphQL response omitted data");
133
+ this.emitError(kind, error);
134
+ throw error;
135
+ }
136
+ (0, runtime_1.emitNervesEvent)({
137
+ component: "repertoire",
138
+ event: "repertoire.unraid_request_end",
139
+ message: "Unraid GraphQL request completed",
140
+ meta: { kind, attempt: attempt + 1 },
141
+ });
142
+ return record.data;
143
+ }
144
+ }
145
+ emitError(kind, error) {
146
+ (0, runtime_1.emitNervesEvent)({
147
+ level: "error",
148
+ component: "repertoire",
149
+ event: "repertoire.unraid_request_error",
150
+ message: "Unraid GraphQL request failed",
151
+ meta: { kind, code: error.code, ambiguous: error.ambiguous },
152
+ });
153
+ }
154
+ }
155
+ exports.UnraidClient = UnraidClient;