arisa 5.1.11 → 5.1.13
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/ARISA-MASTER-SLAVE-SPEC.md +7 -4
- package/package.json +8 -7
- package/src/official-tools.lock.json +5 -5
- package/src/runtime/create-app.js +4 -2
- package/src/runtime/update-manager.js +43 -0
- package/src/transport/telegram/bot.js +34 -13
- package/src/transport/telegram/update-command.js +122 -0
- package/test/context-and-task-bounds.test.js +38 -0
- package/test/official-tool-installer.test.js +11 -0
- package/test/update-command.test.js +139 -0
- package/test/update-manager.test.js +57 -1
|
@@ -76,7 +76,9 @@ el protocolo, la CLI y la documentación.
|
|
|
76
76
|
- Linux con systemd es la primera plataforma operativa soportada. Master y Slave
|
|
77
77
|
pueden coexistir en un host mediante homes, sockets, PID y logs separados.
|
|
78
78
|
- El usuario dedicado es la opción recomendada. El usuario actual está
|
|
79
|
-
permitido y root requiere
|
|
79
|
+
permitido y root requiere una elección explícita durante la instalación.
|
|
80
|
+
Una Slave elegida como root queda por defecto en modo host completo con todas
|
|
81
|
+
las capacidades v1; una política guardada posteriormente reemplaza ese default.
|
|
80
82
|
- Los jobs dirigidos a Slaves offline se rechazan en v1.
|
|
81
83
|
- La instalación remota acepta sólo tools del catálogo verificado y requiere
|
|
82
84
|
confirmación. Nunca se instala código silenciosamente como root.
|
|
@@ -565,9 +567,10 @@ Ejecutar Arisa Slave como:
|
|
|
565
567
|
```
|
|
566
568
|
|
|
567
569
|
Se admite elegir `root`. El perfil resultante y todos los informes de Master
|
|
568
|
-
deben mostrar que Slave tiene autoridad root.
|
|
569
|
-
|
|
570
|
-
|
|
570
|
+
deben mostrar que Slave tiene autoridad root. Elegir root instala por defecto un
|
|
571
|
+
perfil de host completo con raíz `/` y todas las capacidades v1, incluida
|
|
572
|
+
`process.exec`. Una política persistida después puede volver a restringir las
|
|
573
|
+
raíces y operaciones en la capa de aplicación.
|
|
571
574
|
|
|
572
575
|
Las restricciones de aplicación reducen el uso accidental, pero dejan de ser
|
|
573
576
|
una frontera de seguridad si el propio proceso de Slave resulta comprometido:
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.13",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"arisa": "bin/arisa.js"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"bootstrap": "node src/index.js --bootstrap",
|
|
13
|
+
"test": "node --test"
|
|
14
|
+
},
|
|
10
15
|
"keywords": [
|
|
11
16
|
"telegram",
|
|
12
17
|
"pi-agent",
|
|
@@ -39,15 +44,11 @@
|
|
|
39
44
|
"url": "https://github.com/clasen/Arisa/issues"
|
|
40
45
|
},
|
|
41
46
|
"homepage": "https://arisa.sh",
|
|
47
|
+
"packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215",
|
|
42
48
|
"dependencies": {
|
|
43
49
|
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
44
50
|
"@sinclair/typebox": "^0.34.41",
|
|
45
51
|
"grammy": "^1.42.0",
|
|
46
52
|
"typebox": "1.1.38"
|
|
47
|
-
},
|
|
48
|
-
"scripts": {
|
|
49
|
-
"start": "node src/index.js",
|
|
50
|
-
"bootstrap": "node src/index.js --bootstrap",
|
|
51
|
-
"test": "node --test"
|
|
52
53
|
}
|
|
53
|
-
}
|
|
54
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"repository": "https://github.com/clasen/Arisa.git",
|
|
4
|
-
"commit": "
|
|
4
|
+
"commit": "8438bffbc656de7f85e9a98fe7d8804b3c0224ea",
|
|
5
5
|
"tools": {
|
|
6
6
|
"master-slave": {
|
|
7
7
|
"files": {
|
|
8
|
-
"README.md": "
|
|
8
|
+
"README.md": "8533804fda909c2f7bb0d05d40e2d6be9ef9126215bde70cd0ac4dda4afe9c55",
|
|
9
9
|
"batch-runner.js": "be3939efe9c5800d3078c15a094f53db1094ef6e809d7826db7c47e3bec0c31b",
|
|
10
10
|
"chat-state-store.js": "50de39aa33432733bb688eb01968e314a32b169324a66d0f0089b2a19ed9c880",
|
|
11
11
|
"config.js": "ff1c1cb6701ae1da7a6eb1519ba96a416e48daa73c2155a6f93cf5d64f65cef8",
|
|
12
|
-
"index.js": "
|
|
12
|
+
"index.js": "8586303b3d1bf937932159adbe9b69c1031e6a393300e7d3aca93910ddfab082",
|
|
13
13
|
"lib/bootstrap-url.js": "f05925cc0f0dff0882f94f9908c6f2c80aa8632cd51f63389381dcbb0f73afb3",
|
|
14
14
|
"lib/encrypted-frames.js": "47e1b11fafdbbcdfd86a0993a47a774e4f37d6386edeaf8ec1c8511b109728cf",
|
|
15
15
|
"lib/handshake-crypto.js": "9728b117aad7de53f5f0e255c0fd865ee1a19633368730a7634334d374094578",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"master-domain.js": "10b7dfb43eb9939eb5baec54f742076cc73a2add6589a0ff57c51bc79c236faf",
|
|
21
21
|
"network-session.js": "db2d81a7ef47af29236d963b98e21414a11fef68e8b20895126eaad3f696ba3d",
|
|
22
22
|
"package.json": "8e9177238ba878dd4b51e32e0ed58a39c5789186c22b19ad7a43ab6caf9dc469",
|
|
23
|
-
"remote-runtime.js": "
|
|
23
|
+
"remote-runtime.js": "55dd7e577a822be17c2f337798a2d7f44ad0c5b6d68c46490e058282135c4658",
|
|
24
24
|
"slave-operations.js": "fec2e8addfff98f080284f24f722c1516124e5f6d12d8c03bb3c9cc0c475d1d6",
|
|
25
25
|
"state-store.js": "c56849a3f1b9c990128276e17963034a7836abacedc44f2e500d5732ebf9e451",
|
|
26
26
|
"test/batch-runner.test.js": "f14c6fddff5d6240008eef388df6a153e82490e534d06419d7bb267e4f4693a7",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test/network-session.test.js": "2e459346cc9364be705bc5f54691245044588b2cd03ae77e2e870dee6dfbcca6",
|
|
32
32
|
"test/pairing-secret-store.test.js": "c51257365fd0b641c24768d710cdff2d1c5f8816bf90f66c2c01a226904cb43f",
|
|
33
33
|
"test/profile-catalog.test.js": "d8e297bee413d53ec13f2e1779c13f2e019cb8304976d567eab0e9f1ca36637c",
|
|
34
|
-
"test/remote-runtime.test.js": "
|
|
34
|
+
"test/remote-runtime.test.js": "1161bd653dc47b9b8219bf848d0d395ce390f2bb7b58ea41e3e4270b1d504798",
|
|
35
35
|
"test/slave-operations.test.js": "a84526ad7554c7d03192a68655b4637bec18750559a5c3436f7471e912ca74cc",
|
|
36
36
|
"tool.manifest.json": "abf24c67b1d9ef0331ded59e04dcc092772419e74691c0d753ab51b40286d6f6"
|
|
37
37
|
}
|
|
@@ -11,7 +11,7 @@ import { createIpcServer } from "./ipc/ipc-server.js";
|
|
|
11
11
|
import { getAgentConfig } from "../core/agent/model-selection.js";
|
|
12
12
|
import { normalizeModelSpeed } from "../core/agent/model-speed.js";
|
|
13
13
|
import { runDoctor } from "./doctor.js";
|
|
14
|
-
import { checkForUpdates,
|
|
14
|
+
import { checkForUpdates, installCoreUpdate, updateOfficialTools } from "./update-manager.js";
|
|
15
15
|
|
|
16
16
|
function normalizeString(value) {
|
|
17
17
|
const text = String(value ?? "").trim();
|
|
@@ -146,7 +146,9 @@ export async function createApp({ logger, runtimeOverrides, requestRestart } = {
|
|
|
146
146
|
},
|
|
147
147
|
logger
|
|
148
148
|
}),
|
|
149
|
-
checkUpdates: async (chatId) =>
|
|
149
|
+
checkUpdates: async (chatId) => checkForUpdates({ chatId, toolRegistry }),
|
|
150
|
+
updateCore: async (targetVersion) => installCoreUpdate({ targetVersion }),
|
|
151
|
+
updateTools: async (chatId) => updateOfficialTools({ chatId, toolRegistry }),
|
|
150
152
|
requestRestart,
|
|
151
153
|
logger
|
|
152
154
|
});
|
|
@@ -58,6 +58,32 @@ async function fetchLatestVersion() {
|
|
|
58
58
|
return version;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export async function installCoreUpdate({ targetVersion }, {
|
|
62
|
+
readVersion = readCurrentVersion,
|
|
63
|
+
fetchVersion = fetchLatestVersion,
|
|
64
|
+
execute = runCommand
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (!parseSemver(targetVersion)) throw new Error("Invalid Arisa update version");
|
|
67
|
+
const [currentVersion, latestVersion] = await Promise.all([readVersion(), fetchVersion()]);
|
|
68
|
+
if (targetVersion !== latestVersion) {
|
|
69
|
+
throw new Error(`Arisa ${latestVersion} is now the latest version. Run /update again.`);
|
|
70
|
+
}
|
|
71
|
+
if (compareVersions(currentVersion, latestVersion) !== -1) {
|
|
72
|
+
return { updated: false, currentVersion, latestVersion };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await execute("npm", ["install", "--global", `arisa@${latestVersion}`]);
|
|
76
|
+
const installedVersion = await readVersion();
|
|
77
|
+
if (installedVersion !== latestVersion) {
|
|
78
|
+
throw new Error(`npm completed, but Arisa is still ${installedVersion} instead of ${latestVersion}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
updated: true,
|
|
82
|
+
previousVersion: currentVersion,
|
|
83
|
+
currentVersion: installedVersion
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
async function cloneCatalog(scratchRoot) {
|
|
62
88
|
const repoDir = path.join(scratchRoot, "repo");
|
|
63
89
|
await runCommand("git", ["clone", "--depth", "1", "--branch", defaultBranch, "--", defaultRepoUrl, repoDir], { cwd: scratchRoot, timeoutMs: 180_000 });
|
|
@@ -156,6 +182,23 @@ export async function checkForUpdates({ chatId, toolRegistry }) {
|
|
|
156
182
|
};
|
|
157
183
|
}
|
|
158
184
|
|
|
185
|
+
export async function updateOfficialTools({ chatId, toolRegistry }) {
|
|
186
|
+
try {
|
|
187
|
+
const result = await toolRegistry.run({
|
|
188
|
+
name: "official-tool-sync",
|
|
189
|
+
chatId,
|
|
190
|
+
request: { args: { action: "update-safe" } }
|
|
191
|
+
});
|
|
192
|
+
const sync = parseToolSyncOutput(result);
|
|
193
|
+
return {
|
|
194
|
+
updated: (sync.updates || []).filter((item) => item.action === "updated").map((item) => item.name),
|
|
195
|
+
skipped: (sync.skipped || []).map(({ name, status }) => ({ name, status }))
|
|
196
|
+
};
|
|
197
|
+
} finally {
|
|
198
|
+
await toolRegistry.load();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
function shortToolStatus(status) {
|
|
160
203
|
return ({
|
|
161
204
|
"locally-modified": "local",
|
|
@@ -16,6 +16,8 @@ import { ConversationHistoryStore } from "../../core/conversation/conversation-h
|
|
|
16
16
|
import { formatDoctorReport } from "../../runtime/doctor.js";
|
|
17
17
|
import { formatToolUsageReport } from "../../runtime/tool-usage-report.js";
|
|
18
18
|
import { ToolResourceNoteStore } from "../../core/tools/tool-resource-note-store.js";
|
|
19
|
+
import { formatUpdateReport } from "../../runtime/update-manager.js";
|
|
20
|
+
import { buildUpdatePicker, createTelegramUpdateCallbackHandler } from "./update-command.js";
|
|
19
21
|
|
|
20
22
|
const slowPromptNoticeMs = 300_000;
|
|
21
23
|
|
|
@@ -23,7 +25,7 @@ export const telegramCommands = Object.freeze([
|
|
|
23
25
|
{ command: "new", description: "New chat context" },
|
|
24
26
|
{ command: "restart", description: "Restart Arisa" },
|
|
25
27
|
{ command: "doctor", description: "Check runtime health" },
|
|
26
|
-
{ command: "update", description: "Check
|
|
28
|
+
{ command: "update", description: "Check and apply updates" },
|
|
27
29
|
{ command: "tools", description: "Tool usage counts" },
|
|
28
30
|
{ command: "model", description: "Choose chat model" },
|
|
29
31
|
{ command: "effort", description: "Choose reasoning effort" },
|
|
@@ -352,21 +354,23 @@ function buildStartupMessage(chatMeta = {}) {
|
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
export async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
355
|
-
|
|
357
|
+
const assistantMessages = [];
|
|
358
|
+
let assistantMessage = "";
|
|
356
359
|
let assistantErrorMessage = "";
|
|
357
|
-
let shouldSeparateAssistantMessage = false;
|
|
358
360
|
let slowPromptTimer = null;
|
|
361
|
+
const finishAssistantMessage = () => {
|
|
362
|
+
if (assistantMessage && !isSilentReply(assistantMessage)) {
|
|
363
|
+
assistantMessages.push(assistantMessage);
|
|
364
|
+
}
|
|
365
|
+
assistantMessage = "";
|
|
366
|
+
};
|
|
359
367
|
const unsubscribe = session.subscribe((event) => {
|
|
360
368
|
if (event.arisaPromptScoped === false) return;
|
|
361
369
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
362
|
-
|
|
370
|
+
finishAssistantMessage();
|
|
363
371
|
}
|
|
364
372
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
365
|
-
|
|
366
|
-
text += "\n\n";
|
|
367
|
-
shouldSeparateAssistantMessage = false;
|
|
368
|
-
}
|
|
369
|
-
text += event.assistantMessageEvent.delta;
|
|
373
|
+
assistantMessage += event.assistantMessageEvent.delta;
|
|
370
374
|
}
|
|
371
375
|
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
372
376
|
if (event.message.stopReason === "error") {
|
|
@@ -375,6 +379,7 @@ export async function collectText(session, prompt, { logger, chatId, onSlowPromp
|
|
|
375
379
|
// Auto-compaction and retry can emit a transient error before a successful continuation.
|
|
376
380
|
assistantErrorMessage = "";
|
|
377
381
|
}
|
|
382
|
+
finishAssistantMessage();
|
|
378
383
|
}
|
|
379
384
|
const logMessage = sessionEventLogMessage(event);
|
|
380
385
|
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
@@ -400,7 +405,8 @@ export async function collectText(session, prompt, { logger, chatId, onSlowPromp
|
|
|
400
405
|
throw new Error(assistantErrorMessage);
|
|
401
406
|
}
|
|
402
407
|
|
|
403
|
-
|
|
408
|
+
finishAssistantMessage();
|
|
409
|
+
return assistantMessages.join("\n\n").trim();
|
|
404
410
|
}
|
|
405
411
|
|
|
406
412
|
export function isSilentReply(text) {
|
|
@@ -568,7 +574,7 @@ export async function closeModelPicker(ctx, { messageText, callbackText }) {
|
|
|
568
574
|
await ctx.answerCallbackQuery({ text: callbackText });
|
|
569
575
|
}
|
|
570
576
|
|
|
571
|
-
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, checkUpdates, requestRestart, logger }) {
|
|
577
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, checkUpdates, updateCore, updateTools, requestRestart, logger }) {
|
|
572
578
|
const resourceNotes = new ToolResourceNoteStore();
|
|
573
579
|
const bot = new Bot(config.telegram.token);
|
|
574
580
|
const perChatState = createChatStateStore();
|
|
@@ -588,6 +594,18 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
588
594
|
requestRestart,
|
|
589
595
|
logger
|
|
590
596
|
});
|
|
597
|
+
const handleUpdateCallback = createTelegramUpdateCallbackHandler({
|
|
598
|
+
authorize: (ctx) => authorizeChat({
|
|
599
|
+
config,
|
|
600
|
+
chatId: ctx.chat.id,
|
|
601
|
+
saveConfig,
|
|
602
|
+
chatMeta: getIncomingChatMeta(ctx)
|
|
603
|
+
}),
|
|
604
|
+
updateCore,
|
|
605
|
+
updateTools,
|
|
606
|
+
requestRestart,
|
|
607
|
+
logger
|
|
608
|
+
});
|
|
591
609
|
|
|
592
610
|
function chatKey(chatId) {
|
|
593
611
|
return String(chatId);
|
|
@@ -1272,11 +1290,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1272
1290
|
if (!auth.ok) return;
|
|
1273
1291
|
const pending = await ctx.reply(renderTelegramHtml("```text\nChecking Arisa and official tool updates…\n```"), { parse_mode: "HTML" });
|
|
1274
1292
|
try {
|
|
1293
|
+
const report = await checkUpdates(ctx.chat.id);
|
|
1294
|
+
const picker = buildUpdatePicker(report);
|
|
1275
1295
|
await ctx.api.editMessageText(
|
|
1276
1296
|
ctx.chat.id,
|
|
1277
1297
|
pending.message_id,
|
|
1278
|
-
renderTelegramHtml(
|
|
1279
|
-
{ parse_mode: "HTML" }
|
|
1298
|
+
renderTelegramHtml(formatUpdateReport(report)),
|
|
1299
|
+
{ parse_mode: "HTML", reply_markup: picker.replyMarkup }
|
|
1280
1300
|
);
|
|
1281
1301
|
} catch (error) {
|
|
1282
1302
|
logger?.error("update", `update check failed: ${getErrorMessage(error)}`);
|
|
@@ -1342,6 +1362,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1342
1362
|
});
|
|
1343
1363
|
|
|
1344
1364
|
bot.on("callback_query:data", async (ctx, next) => {
|
|
1365
|
+
if (await handleUpdateCallback(ctx)) return;
|
|
1345
1366
|
const modelAction = parseModelPickerAction(ctx.callbackQuery.data);
|
|
1346
1367
|
const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
|
|
1347
1368
|
const speedAction = modelAction || effortAction ? null : parseSpeedPickerAction(ctx.callbackQuery.data);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const callbackPrefix = "arisa-update:";
|
|
2
|
+
|
|
3
|
+
function callbackMessage(ctx) {
|
|
4
|
+
return {
|
|
5
|
+
chatId: ctx.chat.id,
|
|
6
|
+
messageId: ctx.callbackQuery.message.message_id
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function editCallbackMessage(ctx, text, extra) {
|
|
11
|
+
const { chatId, messageId } = callbackMessage(ctx);
|
|
12
|
+
return extra
|
|
13
|
+
? ctx.api.editMessageText(chatId, messageId, text, extra)
|
|
14
|
+
: ctx.api.editMessageText(chatId, messageId, text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildUpdatePicker(report) {
|
|
18
|
+
const rows = [];
|
|
19
|
+
if (report.core.updateAvailable) {
|
|
20
|
+
rows.push([{
|
|
21
|
+
text: `Update Arisa → ${report.core.latestVersion}`,
|
|
22
|
+
callback_data: `${callbackPrefix}core:${report.core.latestVersion}`
|
|
23
|
+
}]);
|
|
24
|
+
}
|
|
25
|
+
if (report.tools.updateable.length) {
|
|
26
|
+
rows.push([{
|
|
27
|
+
text: `Update safe tools (${report.tools.updateable.length})`,
|
|
28
|
+
callback_data: `${callbackPrefix}tools`
|
|
29
|
+
}]);
|
|
30
|
+
}
|
|
31
|
+
rows.push(rows.length
|
|
32
|
+
? [{ text: "Not now", callback_data: `${callbackPrefix}close` }]
|
|
33
|
+
: [{ text: "Up to date", callback_data: `${callbackPrefix}noop` }]);
|
|
34
|
+
return { replyMarkup: { inline_keyboard: rows } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseUpdateAction(data) {
|
|
38
|
+
if (data === `${callbackPrefix}tools`) return { type: "tools" };
|
|
39
|
+
if (data === `${callbackPrefix}close`) return { type: "close" };
|
|
40
|
+
if (data === `${callbackPrefix}noop`) return { type: "noop" };
|
|
41
|
+
const core = String(data || "").match(/^arisa-update:core:(\d+\.\d+\.\d+)$/);
|
|
42
|
+
return core ? { type: "core", targetVersion: core[1] } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatOfficialToolUpdateResult(result) {
|
|
46
|
+
const lines = result.updated.length
|
|
47
|
+
? ["Updated official tools:", ...result.updated.map((name) => `- ${name}`)]
|
|
48
|
+
: ["Official tools were already up to date."];
|
|
49
|
+
if (result.skipped.length) {
|
|
50
|
+
lines.push("", "Not changed (needs review):");
|
|
51
|
+
for (const item of result.skipped) lines.push(`- ${item.name} [${item.status}]`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createTelegramUpdateCallbackHandler({ authorize, updateCore, updateTools, requestRestart, logger }) {
|
|
57
|
+
let updateInProgress = false;
|
|
58
|
+
|
|
59
|
+
return async function handleTelegramUpdateCallback(ctx) {
|
|
60
|
+
const action = parseUpdateAction(ctx.callbackQuery.data);
|
|
61
|
+
if (!action) return false;
|
|
62
|
+
|
|
63
|
+
const auth = await authorize(ctx);
|
|
64
|
+
if (!auth.ok) {
|
|
65
|
+
await ctx.answerCallbackQuery({ text: "This chat is not authorized.", show_alert: true });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (action.type === "noop") {
|
|
69
|
+
await ctx.answerCallbackQuery({ text: "Arisa and official tools are up to date." });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (action.type === "close") {
|
|
73
|
+
await ctx.answerCallbackQuery();
|
|
74
|
+
await editCallbackMessage(ctx, "Update cancelled.");
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (updateInProgress) {
|
|
78
|
+
await ctx.answerCallbackQuery({ text: "An update is already in progress.", show_alert: true });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateInProgress = true;
|
|
83
|
+
const coreUpdate = action.type === "core";
|
|
84
|
+
try {
|
|
85
|
+
await ctx.answerCallbackQuery({ text: coreUpdate ? "Updating Arisa…" : "Updating safe tools…" });
|
|
86
|
+
if (coreUpdate) {
|
|
87
|
+
await editCallbackMessage(ctx, `Updating Arisa to ${action.targetVersion}…`);
|
|
88
|
+
const result = await updateCore(action.targetVersion);
|
|
89
|
+
if (!result.updated) {
|
|
90
|
+
await editCallbackMessage(ctx, `Arisa ${result.currentVersion} is already up to date.`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
await editCallbackMessage(
|
|
94
|
+
ctx,
|
|
95
|
+
`Arisa updated from ${result.previousVersion} to ${result.currentVersion}. Restarting…`
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
await requestRestart();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
logger?.error("update", `restart after update failed: ${message}`);
|
|
102
|
+
await editCallbackMessage(
|
|
103
|
+
ctx,
|
|
104
|
+
`Arisa updated to ${result.currentVersion}, but restart failed: ${message}\nRun /restart to activate it.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await editCallbackMessage(ctx, "Updating safe official tools…");
|
|
111
|
+
await editCallbackMessage(ctx, formatOfficialToolUpdateResult(await updateTools(ctx.chat.id)));
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
logger?.error("update", `update action failed: ${message}`);
|
|
116
|
+
await editCallbackMessage(ctx, `Arisa update failed: ${message}`).catch(() => {});
|
|
117
|
+
return true;
|
|
118
|
+
} finally {
|
|
119
|
+
updateInProgress = false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -59,6 +59,44 @@ test("collectText preserves the final assistant error", async () => {
|
|
|
59
59
|
await assert.rejects(() => collectText(session, "hello"), /terminal failure/);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
test("collectText drops a standalone silent event reply before a steered response", async () => {
|
|
63
|
+
const session = createSession([
|
|
64
|
+
{ type: "message_start", message: { role: "assistant" } },
|
|
65
|
+
{
|
|
66
|
+
type: "message_update",
|
|
67
|
+
message: { role: "assistant" },
|
|
68
|
+
assistantMessageEvent: { type: "text_delta", delta: "NO_REPLY" }
|
|
69
|
+
},
|
|
70
|
+
{ type: "message_end", message: { role: "assistant", stopReason: "stop" } },
|
|
71
|
+
{ type: "message_start", message: { role: "assistant" } },
|
|
72
|
+
{
|
|
73
|
+
type: "message_update",
|
|
74
|
+
message: { role: "assistant" },
|
|
75
|
+
assistantMessageEvent: { type: "text_delta", delta: "The useful Telegram response." }
|
|
76
|
+
},
|
|
77
|
+
{ type: "message_end", message: { role: "assistant", stopReason: "stop" } }
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
assert.equal(await collectText(session, "event followed by a Telegram steer"), "The useful Telegram response.");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("collectText preserves useful text in the same message as a silent marker", async () => {
|
|
84
|
+
const session = createSession([
|
|
85
|
+
{ type: "message_start", message: { role: "assistant" } },
|
|
86
|
+
{
|
|
87
|
+
type: "message_update",
|
|
88
|
+
message: { role: "assistant" },
|
|
89
|
+
assistantMessageEvent: { type: "text_delta", delta: "NO_REPLY\n\nThis text belongs to the same response." }
|
|
90
|
+
},
|
|
91
|
+
{ type: "message_end", message: { role: "assistant", stopReason: "stop" } }
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
assert.equal(
|
|
95
|
+
await collectText(session, "one response that mentions the marker"),
|
|
96
|
+
"NO_REPLY\n\nThis text belongs to the same response."
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
62
100
|
test("recognizes standalone silent reply markers", () => {
|
|
63
101
|
assert.equal(isSilentReply("NO_REPLY"), true);
|
|
64
102
|
assert.equal(isSilentReply("No reply needed."), true);
|
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import test from "node:test";
|
|
7
8
|
import {
|
|
8
9
|
installBundledOfficialTool,
|
|
@@ -59,6 +60,16 @@ test("verifies the exact file set and digests", async (t) => {
|
|
|
59
60
|
await assert.rejects(() => verifyOfficialToolTree(source, files), /unexpected=extra.js/);
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
test("bundled master-slave lock matches the catalog source", async () => {
|
|
64
|
+
const lock = JSON.parse(await readFile(new URL("../src/official-tools.lock.json", import.meta.url), "utf8"));
|
|
65
|
+
const source = fileURLToPath(new URL("../../tools/master-slave/", import.meta.url));
|
|
66
|
+
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
await verifyOfficialToolTree(source, lock.tools["master-slave"].files),
|
|
69
|
+
{ files: Object.keys(lock.tools["master-slave"].files).length }
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
62
73
|
test("rejects symbolic links before deployment", async (t) => {
|
|
63
74
|
const { source, files } = await fixture(t);
|
|
64
75
|
await symlink(path.join(source, "index.js"), path.join(source, "link.js"));
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildUpdatePicker,
|
|
5
|
+
createTelegramUpdateCallbackHandler,
|
|
6
|
+
parseUpdateAction
|
|
7
|
+
} from "../src/transport/telegram/update-command.js";
|
|
8
|
+
|
|
9
|
+
function report(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
core: { currentVersion: "5.0.2", latestVersion: "5.1.0", updateAvailable: true },
|
|
12
|
+
bootstrapInstalled: [],
|
|
13
|
+
tools: {
|
|
14
|
+
installedOfficial: 2,
|
|
15
|
+
official: [],
|
|
16
|
+
nonOfficial: [],
|
|
17
|
+
counts: {},
|
|
18
|
+
updateable: ["context-vault", "gmail-workspace"],
|
|
19
|
+
blocked: []
|
|
20
|
+
},
|
|
21
|
+
...overrides
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("offers only available update actions", () => {
|
|
26
|
+
assert.deepEqual(buildUpdatePicker(report()).replyMarkup.inline_keyboard, [
|
|
27
|
+
[{ text: "Update Arisa → 5.1.0", callback_data: "arisa-update:core:5.1.0" }],
|
|
28
|
+
[{ text: "Update safe tools (2)", callback_data: "arisa-update:tools" }],
|
|
29
|
+
[{ text: "Not now", callback_data: "arisa-update:close" }]
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(buildUpdatePicker(report({
|
|
33
|
+
core: { currentVersion: "5.1.0", latestVersion: "5.1.0", updateAvailable: false },
|
|
34
|
+
tools: { ...report().tools, updateable: [] }
|
|
35
|
+
})).replyMarkup.inline_keyboard, [
|
|
36
|
+
[{ text: "Up to date", callback_data: "arisa-update:noop" }]
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("parses only valid update callback data", () => {
|
|
41
|
+
assert.deepEqual(parseUpdateAction("arisa-update:core:5.1.0"), { type: "core", targetVersion: "5.1.0" });
|
|
42
|
+
assert.deepEqual(parseUpdateAction("arisa-update:tools"), { type: "tools" });
|
|
43
|
+
assert.deepEqual(parseUpdateAction("arisa-update:close"), { type: "close" });
|
|
44
|
+
assert.deepEqual(parseUpdateAction("arisa-update:noop"), { type: "noop" });
|
|
45
|
+
assert.equal(parseUpdateAction("arisa-update:core:latest"), null);
|
|
46
|
+
assert.equal(parseUpdateAction("model:1"), null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("applies a confirmed core update and then requests restart", async () => {
|
|
50
|
+
const calls = [];
|
|
51
|
+
const handler = createTelegramUpdateCallbackHandler({
|
|
52
|
+
authorize: async () => ({ ok: true }),
|
|
53
|
+
updateCore: async (targetVersion) => {
|
|
54
|
+
calls.push(["updateCore", targetVersion]);
|
|
55
|
+
return { updated: true, previousVersion: "5.0.2", currentVersion: targetVersion };
|
|
56
|
+
},
|
|
57
|
+
updateTools: async () => assert.fail("must not update tools"),
|
|
58
|
+
requestRestart: async () => { calls.push("restart"); }
|
|
59
|
+
});
|
|
60
|
+
const ctx = {
|
|
61
|
+
chat: { id: 42 },
|
|
62
|
+
callbackQuery: { data: "arisa-update:core:5.1.0", message: { message_id: 7 } },
|
|
63
|
+
answerCallbackQuery: async (payload) => { calls.push(["answer", payload]); },
|
|
64
|
+
api: {
|
|
65
|
+
editMessageText: async (...args) => { calls.push(["edit", ...args]); }
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
assert.equal(await handler(ctx), true);
|
|
70
|
+
assert.deepEqual(calls, [
|
|
71
|
+
["answer", { text: "Updating Arisa…" }],
|
|
72
|
+
["edit", 42, 7, "Updating Arisa to 5.1.0…"],
|
|
73
|
+
["updateCore", "5.1.0"],
|
|
74
|
+
["edit", 42, 7, "Arisa updated from 5.0.2 to 5.1.0. Restarting…"],
|
|
75
|
+
"restart"
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("uses official-tool-sync for the confirmed safe tool action", async () => {
|
|
80
|
+
const calls = [];
|
|
81
|
+
const handler = createTelegramUpdateCallbackHandler({
|
|
82
|
+
authorize: async () => ({ ok: true }),
|
|
83
|
+
updateCore: async () => assert.fail("must not update core"),
|
|
84
|
+
updateTools: async (chatId) => {
|
|
85
|
+
calls.push(["updateTools", chatId]);
|
|
86
|
+
return {
|
|
87
|
+
updated: ["context-vault"],
|
|
88
|
+
skipped: [{ name: "customized", status: "locally-modified" }]
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
requestRestart: async () => assert.fail("tool updates do not restart Arisa")
|
|
92
|
+
});
|
|
93
|
+
const ctx = {
|
|
94
|
+
chat: { id: 42 },
|
|
95
|
+
callbackQuery: { data: "arisa-update:tools", message: { message_id: 7 } },
|
|
96
|
+
answerCallbackQuery: async (payload) => { calls.push(["answer", payload]); },
|
|
97
|
+
api: {
|
|
98
|
+
editMessageText: async (...args) => { calls.push(["edit", ...args]); }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
assert.equal(await handler(ctx), true);
|
|
103
|
+
assert.deepEqual(calls, [
|
|
104
|
+
["answer", { text: "Updating safe tools…" }],
|
|
105
|
+
["edit", 42, 7, "Updating safe official tools…"],
|
|
106
|
+
["updateTools", 42],
|
|
107
|
+
["edit", 42, 7, [
|
|
108
|
+
"Updated official tools:",
|
|
109
|
+
"- context-vault",
|
|
110
|
+
"",
|
|
111
|
+
"Not changed (needs review):",
|
|
112
|
+
"- customized [locally-modified]"
|
|
113
|
+
].join("\n")]
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("reports restart failure separately after a successful core update", async () => {
|
|
118
|
+
const edits = [];
|
|
119
|
+
const handler = createTelegramUpdateCallbackHandler({
|
|
120
|
+
authorize: async () => ({ ok: true }),
|
|
121
|
+
updateCore: async () => ({ updated: true, previousVersion: "5.0.2", currentVersion: "5.1.0" }),
|
|
122
|
+
updateTools: async () => assert.fail("must not update tools"),
|
|
123
|
+
requestRestart: async () => { throw new Error("not running as a service"); }
|
|
124
|
+
});
|
|
125
|
+
const ctx = {
|
|
126
|
+
chat: { id: 42 },
|
|
127
|
+
callbackQuery: { data: "arisa-update:core:5.1.0", message: { message_id: 7 } },
|
|
128
|
+
answerCallbackQuery: async () => {},
|
|
129
|
+
api: {
|
|
130
|
+
editMessageText: async (_chatId, _messageId, text) => { edits.push(text); }
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
assert.equal(await handler(ctx), true);
|
|
135
|
+
assert.equal(edits.at(-1), [
|
|
136
|
+
"Arisa updated to 5.1.0, but restart failed: not running as a service",
|
|
137
|
+
"Run /restart to activate it."
|
|
138
|
+
].join("\n"));
|
|
139
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { compareVersions, formatUpdateReport } from "../src/runtime/update-manager.js";
|
|
3
|
+
import { compareVersions, formatUpdateReport, installCoreUpdate, updateOfficialTools } from "../src/runtime/update-manager.js";
|
|
4
4
|
|
|
5
5
|
test("compares semantic versions", () => {
|
|
6
6
|
assert.equal(compareVersions("5.0.2", "5.0.3"), -1);
|
|
@@ -85,3 +85,59 @@ test("shortens long review status labels", () => {
|
|
|
85
85
|
assert.match(report, /campaign-draft-runner\n \[untracked\]/);
|
|
86
86
|
assert.ok(report.split("\n").every((line) => [...line].length <= 35));
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test("installs only the currently published core update and verifies it", async () => {
|
|
90
|
+
const calls = [];
|
|
91
|
+
const versions = ["5.0.2", "5.1.0"];
|
|
92
|
+
const result = await installCoreUpdate({ targetVersion: "5.1.0" }, {
|
|
93
|
+
readVersion: async () => versions.shift(),
|
|
94
|
+
fetchVersion: async () => "5.1.0",
|
|
95
|
+
execute: async (...args) => { calls.push(args); }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(calls, [["npm", ["install", "--global", "arisa@5.1.0"]]]);
|
|
99
|
+
assert.deepEqual(result, {
|
|
100
|
+
updated: true,
|
|
101
|
+
previousVersion: "5.0.2",
|
|
102
|
+
currentVersion: "5.1.0"
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("refuses a stale core update button", async () => {
|
|
107
|
+
await assert.rejects(
|
|
108
|
+
installCoreUpdate({ targetVersion: "5.1.0" }, {
|
|
109
|
+
readVersion: async () => "5.0.2",
|
|
110
|
+
fetchVersion: async () => "5.1.1",
|
|
111
|
+
execute: async () => assert.fail("must not install a stale target")
|
|
112
|
+
}),
|
|
113
|
+
/Run \/update again/
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("updates only safe official tools and reloads the registry", async () => {
|
|
118
|
+
const calls = [];
|
|
119
|
+
const toolRegistry = {
|
|
120
|
+
async run(request) {
|
|
121
|
+
calls.push(request);
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
output: {
|
|
125
|
+
json: {
|
|
126
|
+
updates: [{ name: "context-vault", action: "updated" }],
|
|
127
|
+
skipped: [{ name: "customized", status: "locally-modified" }]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async load() { calls.push("load"); }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(await updateOfficialTools({ chatId: 42, toolRegistry }), {
|
|
136
|
+
updated: ["context-vault"],
|
|
137
|
+
skipped: [{ name: "customized", status: "locally-modified" }]
|
|
138
|
+
});
|
|
139
|
+
assert.deepEqual(calls, [
|
|
140
|
+
{ name: "official-tool-sync", chatId: 42, request: { args: { action: "update-safe" } } },
|
|
141
|
+
"load"
|
|
142
|
+
]);
|
|
143
|
+
});
|