arisa 5.1.2 → 5.1.8
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/AGENTS.md +12 -4
- package/ARISA-MASTER-SLAVE-SPEC.md +844 -0
- package/README.md +25 -0
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +55 -12
- package/src/core/config/config-defaults.js +3 -1
- package/src/core/tools/daemon-processes.js +48 -6
- package/src/core/tools/daemon-runtime.js +370 -138
- package/src/core/tools/ipc-client.js +3 -0
- package/src/core/tools/official-tool-catalog.js +32 -0
- package/src/core/tools/official-tool-installer.js +183 -0
- package/src/core/tools/tool-registry.js +203 -18
- package/src/core/tools/tool-resource-note-store.js +78 -0
- package/src/index.js +25 -2
- package/src/official-tools.lock.json +40 -0
- package/src/runtime/arisa-capabilities.js +43 -3
- package/src/runtime/create-app.js +9 -0
- package/src/runtime/create-headless-app.js +77 -0
- package/src/runtime/doctor.js +27 -2
- package/src/runtime/headless-tool-executor.js +45 -0
- package/src/runtime/paths.js +16 -4
- package/src/runtime/secure-request-file.js +21 -0
- package/src/runtime/slave-bootstrap-url.js +51 -0
- package/src/runtime/slave-cli.js +267 -0
- package/src/runtime/slave-service.js +225 -0
- package/src/runtime/tool-usage-report.js +11 -3
- package/src/transport/telegram/bot.js +37 -7
- package/test/capabilities-security.test.js +29 -0
- package/test/daemon-catalog-conformance.test.js +3 -1
- package/test/daemon-runtime.test.js +58 -2
- package/test/official-tool-installer.test.js +107 -0
- package/test/paths.test.js +6 -12
- package/test/slave-cli.test.js +282 -0
- package/test/telegram-text-artifact.test.js +24 -1
- package/test/tool-capability-search.test.js +55 -0
- package/test/tool-registry-run.test.js +70 -1
- package/test/tool-resource-note.test.js +50 -0
- package/test/tool-usage.test.js +10 -5
- package/test-fixtures/fake-daemon.js +12 -1
package/README.md
CHANGED
|
@@ -103,6 +103,31 @@ Per chat (`~/.arisa/chats/<chatId>/`):
|
|
|
103
103
|
|
|
104
104
|
Managed daemons become ready only after their tool-defined health operation succeeds through the normal command queue. Arisa records heartbeats, successful jobs, errors, and standard lifecycle states, then retries recovery or recreates an unhealthy process with its persisted scope and startup context.
|
|
105
105
|
|
|
106
|
+
Daemon tools may opt into the `arisa-daemon-v1` local protocol for immediate
|
|
107
|
+
multiplexed jobs and incremental NDJSON events over a capability-protected local
|
|
108
|
+
socket. The runtime persists request, accepted and terminal records so a restart
|
|
109
|
+
can recover queued work and will not silently repeat an accepted effect. Legacy
|
|
110
|
+
daemon tools continue to use the existing request-file contract.
|
|
111
|
+
|
|
112
|
+
### Arisa Master and Slave
|
|
113
|
+
|
|
114
|
+
The official `master-slave` daemon tool lets one normal Arisa installation act
|
|
115
|
+
as Master for deterministic headless Slave hosts. Master keeps Telegram and Pi;
|
|
116
|
+
Slave runs only the IPC host, daemon supervisor and installed tools. Connections
|
|
117
|
+
are authenticated, encrypted, initiated by Slave and restricted by per-Slave
|
|
118
|
+
roots and capability grants.
|
|
119
|
+
|
|
120
|
+
Linux with systemd is the first supported Slave target. Bootstrap uses a
|
|
121
|
+
single-use URL issued by Master:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm i -g arisa && arisa slave tcp://198.51.100.12:4719/arisa_secret_v1_<secret>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The URL is sensitive and may remain in shell history. `arisa slave status`,
|
|
128
|
+
`log`, `tools`, `start`, `stop`, `restart` and `unpair` operate the isolated
|
|
129
|
+
headless service without starting Telegram or Pi Agent.
|
|
130
|
+
|
|
106
131
|
Pi authentication can use either:
|
|
107
132
|
- an API key entered during bootstrap
|
|
108
133
|
- or Pi's existing OAuth login when supported, such as `openai-codex`
|
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@ import { createSystemShellTool } from "./system-shell-tool.js";
|
|
|
11
11
|
import { clampModelThinkingLevel } from "./pi-runtime.js";
|
|
12
12
|
import { clampModelSpeed, createModelSpeedController } from "./model-speed.js";
|
|
13
13
|
import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
|
|
14
|
+
import { searchOfficialToolCatalog } from "../tools/official-tool-catalog.js";
|
|
15
|
+
import { ToolResourceNoteStore } from "../tools/tool-resource-note-store.js";
|
|
14
16
|
|
|
15
17
|
const piValidationTimeoutMs = 60_000;
|
|
16
18
|
const arisaToolNames = [
|
|
@@ -18,6 +20,7 @@ const arisaToolNames = [
|
|
|
18
20
|
"tool_help",
|
|
19
21
|
"tool_skills",
|
|
20
22
|
"set_tool_config",
|
|
23
|
+
"set_tool_resource_note",
|
|
21
24
|
"run_tool",
|
|
22
25
|
"list_scheduled_tasks",
|
|
23
26
|
"cancel_scheduled_task",
|
|
@@ -221,6 +224,7 @@ export class AgentManager {
|
|
|
221
224
|
this.toolRegistry = toolRegistry;
|
|
222
225
|
this.taskStore = taskStore;
|
|
223
226
|
this.logger = logger;
|
|
227
|
+
this.resourceNotes = new ToolResourceNoteStore();
|
|
224
228
|
this.sessions = new Map();
|
|
225
229
|
this.pendingNewSessions = new Set();
|
|
226
230
|
this.pendingSessionHandoffs = new Map();
|
|
@@ -505,7 +509,12 @@ export class AgentManager {
|
|
|
505
509
|
await this.toolRegistry.load();
|
|
506
510
|
this.logger?.log("agent", `run_tool ${name}`);
|
|
507
511
|
const chatArtifactStore = this.artifactStore.forChat(chatId);
|
|
508
|
-
const
|
|
512
|
+
const resourceId = String(request?.resourceId || "").trim();
|
|
513
|
+
const resourceNote = resourceId
|
|
514
|
+
? await this.resourceNotes.get(chatId, name, resourceId)
|
|
515
|
+
: "";
|
|
516
|
+
const enrichedRequest = resourceNote ? { ...request, resourceId, resourceNote } : request;
|
|
517
|
+
const result = await this.toolRegistry.run({ name, request: enrichedRequest, chatId });
|
|
509
518
|
|
|
510
519
|
if (result.output?.text) {
|
|
511
520
|
const outArtifact = await chatArtifactStore.createText({
|
|
@@ -551,9 +560,9 @@ export class AgentManager {
|
|
|
551
560
|
defineTool({
|
|
552
561
|
name: "list_tools",
|
|
553
562
|
label: "List tools",
|
|
554
|
-
description: "List Arisa
|
|
555
|
-
parameters: Type.Object({}),
|
|
556
|
-
execute: async () => {
|
|
563
|
+
description: "List Arisa tools, or search installed tool metadata by capability with automatic official-catalog fallback.",
|
|
564
|
+
parameters: Type.Object({ query: Type.Optional(Type.String()) }),
|
|
565
|
+
execute: async (_id, params) => {
|
|
557
566
|
await this.toolRegistry.load();
|
|
558
567
|
const coreTools = getCoreCodingTools({
|
|
559
568
|
tools: policy.tools,
|
|
@@ -567,17 +576,35 @@ export class AgentManager {
|
|
|
567
576
|
shell: policy.shell.shellPath || (process.platform === "win32" ? "powershell" : "sh"),
|
|
568
577
|
enabled: !(policy.excludeTools || []).includes("system_shell")
|
|
569
578
|
}];
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
579
|
+
const query = params.query?.trim() || "";
|
|
580
|
+
let catalogFallback = null;
|
|
581
|
+
const cliTools = query
|
|
582
|
+
? this.toolRegistry.search(query).map((tool) => ({
|
|
583
|
+
...tool,
|
|
584
|
+
source: "arisa-modular",
|
|
585
|
+
invocation: "run_tool"
|
|
586
|
+
}))
|
|
587
|
+
: (await this.toolRegistry.listWithRuntime(chatId)).map((tool) => ({
|
|
588
|
+
...tool,
|
|
589
|
+
source: "arisa-modular",
|
|
590
|
+
invocation: "run_tool"
|
|
591
|
+
}));
|
|
592
|
+
if (query && cliTools.length === 0) {
|
|
593
|
+
try {
|
|
594
|
+
catalogFallback = await searchOfficialToolCatalog(query);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
catalogFallback = { unavailable: true, error: error?.message || String(error), matches: [] };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
575
599
|
const result = {
|
|
600
|
+
query: query || null,
|
|
576
601
|
workspaceDir: policy.workspaceDir,
|
|
577
|
-
coreTools,
|
|
578
|
-
nativeTools,
|
|
602
|
+
coreTools: query ? [] : coreTools,
|
|
603
|
+
nativeTools: query ? [] : nativeTools,
|
|
579
604
|
cliTools,
|
|
580
|
-
|
|
605
|
+
officialCatalogMatches: Array.isArray(catalogFallback) ? catalogFallback : catalogFallback?.matches || [],
|
|
606
|
+
catalogFallback: catalogFallback && !Array.isArray(catalogFallback) ? catalogFallback : null,
|
|
607
|
+
tools: query ? cliTools : [...coreTools.filter((tool) => tool.enabled), ...nativeTools.filter((tool) => tool.enabled), ...cliTools]
|
|
581
608
|
};
|
|
582
609
|
return {
|
|
583
610
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -619,6 +646,20 @@ export class AgentManager {
|
|
|
619
646
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
620
647
|
}
|
|
621
648
|
}),
|
|
649
|
+
defineTool({
|
|
650
|
+
name: "set_tool_resource_note",
|
|
651
|
+
label: "Set tool resource note",
|
|
652
|
+
description: "Set or clear a deterministic chat-scoped note of up to 200 characters for one tool resource.",
|
|
653
|
+
parameters: Type.Object({
|
|
654
|
+
name: Type.String(),
|
|
655
|
+
resourceId: Type.String(),
|
|
656
|
+
note: Type.String()
|
|
657
|
+
}),
|
|
658
|
+
execute: async (_id, params) => {
|
|
659
|
+
const result = await this.resourceNotes.set(chatId, params.name, params.resourceId, params.note);
|
|
660
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
661
|
+
}
|
|
662
|
+
}),
|
|
622
663
|
defineTool({
|
|
623
664
|
name: "run_tool",
|
|
624
665
|
label: "Run tool",
|
|
@@ -627,6 +668,7 @@ export class AgentManager {
|
|
|
627
668
|
name: Type.String(),
|
|
628
669
|
artifactId: Type.Optional(Type.String()),
|
|
629
670
|
text: Type.Optional(Type.String()),
|
|
671
|
+
resourceId: Type.Optional(Type.String()),
|
|
630
672
|
args: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
631
673
|
deliver: Type.Optional(Type.Boolean())
|
|
632
674
|
}),
|
|
@@ -643,6 +685,7 @@ export class AgentManager {
|
|
|
643
685
|
request: {
|
|
644
686
|
artifact,
|
|
645
687
|
text: params.text,
|
|
688
|
+
resourceId: params.resourceId,
|
|
646
689
|
args: params.args || {}
|
|
647
690
|
},
|
|
648
691
|
chatId
|
|
@@ -11,7 +11,9 @@ export const daemonConfigDefaults = Object.freeze({
|
|
|
11
11
|
restartBackoffMaxMs: 60_000,
|
|
12
12
|
startupTimeoutMs: 120_000,
|
|
13
13
|
stopTimeoutMs: 3_000,
|
|
14
|
-
queuePollIntervalMs: 250
|
|
14
|
+
queuePollIntervalMs: 250,
|
|
15
|
+
streamBufferBytes: 1_048_576,
|
|
16
|
+
ipcFrameBytes: 1_048_576
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
export const telegramConfigDefaults = Object.freeze({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { closeSync, openSync } from "node:fs";
|
|
4
|
-
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
chatsDir,
|
|
@@ -35,6 +35,7 @@ function daemonIdentity(toolNameOrOptions, scope) {
|
|
|
35
35
|
export function daemonPaths(toolNameOrOptions, scope) {
|
|
36
36
|
const identity = daemonIdentity(toolNameOrOptions, scope);
|
|
37
37
|
const root = getDaemonInstanceDir(identity.toolName, identity.scope);
|
|
38
|
+
const localSocket = path.join(root, "daemon.sock");
|
|
38
39
|
return {
|
|
39
40
|
...identity,
|
|
40
41
|
instanceId: getDaemonInstanceId(identity.scope),
|
|
@@ -44,7 +45,13 @@ export function daemonPaths(toolNameOrOptions, scope) {
|
|
|
44
45
|
metaFile: path.join(root, "daemon.meta.json"),
|
|
45
46
|
statusFile: path.join(root, "status.json"),
|
|
46
47
|
logFile: path.join(root, "daemon.log"),
|
|
47
|
-
startLockFile: path.join(root, "daemon.start.lock")
|
|
48
|
+
startLockFile: path.join(root, "daemon.start.lock"),
|
|
49
|
+
socketFile: process.platform === "win32"
|
|
50
|
+
? `\\\\.\\pipe\\arisa-daemon-${crypto.createHash("sha256").update(root).digest("hex").slice(0, 16)}`
|
|
51
|
+
: Buffer.byteLength(localSocket) <= 96
|
|
52
|
+
? localSocket
|
|
53
|
+
: path.join("/tmp", `arisa-daemon-${crypto.createHash("sha256").update(root).digest("hex").slice(0, 24)}.sock`),
|
|
54
|
+
capabilityFile: path.join(root, "daemon.capability")
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -63,6 +70,25 @@ export async function writeJson(file, value) {
|
|
|
63
70
|
await rename(temporary, file);
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
export async function ensureDaemonCapability(pathsOrIdentity) {
|
|
74
|
+
const paths = pathsOrIdentity.capabilityFile ? pathsOrIdentity : daemonPaths(pathsOrIdentity);
|
|
75
|
+
await mkdir(paths.root, { recursive: true });
|
|
76
|
+
try {
|
|
77
|
+
const existing = (await readFile(paths.capabilityFile, "utf8")).trim();
|
|
78
|
+
if (existing) return existing;
|
|
79
|
+
} catch {}
|
|
80
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
81
|
+
try {
|
|
82
|
+
await writeFile(paths.capabilityFile, `${token}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
83
|
+
return token;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error?.code !== "EEXIST") throw error;
|
|
86
|
+
const existing = (await readFile(paths.capabilityFile, "utf8")).trim();
|
|
87
|
+
if (!existing) throw new Error(`Invalid daemon capability file for ${paths.toolName}`);
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
export function isProcessAlive(pid) {
|
|
67
93
|
if (!pid) return false;
|
|
68
94
|
try {
|
|
@@ -232,6 +258,8 @@ export async function unregisterManagedDaemon(toolNameOrOptions, { scope } = {})
|
|
|
232
258
|
rm(paths.pidFile, { force: true }),
|
|
233
259
|
rm(paths.statusFile, { force: true }),
|
|
234
260
|
rm(paths.startLockFile, { force: true }),
|
|
261
|
+
rm(paths.capabilityFile, { force: true }),
|
|
262
|
+
process.platform === "win32" ? Promise.resolve() : rm(paths.socketFile, { force: true }),
|
|
235
263
|
rm(paths.commandsDir, { recursive: true, force: true })
|
|
236
264
|
]);
|
|
237
265
|
return { toolName: paths.toolName, scope: paths.scope, instanceId: paths.instanceId };
|
|
@@ -311,8 +339,9 @@ export async function startManagedDaemon({
|
|
|
311
339
|
return current.pid;
|
|
312
340
|
}
|
|
313
341
|
await rm(paths.pidFile, { force: true });
|
|
314
|
-
await rm(paths.commandsDir, { recursive: true, force: true });
|
|
315
342
|
await mkdir(paths.commandsDir, { recursive: true });
|
|
343
|
+
const capabilityToken = await ensureDaemonCapability(paths);
|
|
344
|
+
if (process.platform !== "win32") await chmod(paths.capabilityFile, 0o600);
|
|
316
345
|
if (beforeStart) await beforeStart();
|
|
317
346
|
|
|
318
347
|
const startedAt = new Date().toISOString();
|
|
@@ -344,7 +373,8 @@ export async function startManagedDaemon({
|
|
|
344
373
|
env: {
|
|
345
374
|
...process.env,
|
|
346
375
|
ARISA_DAEMON_META_FILE: paths.metaFile,
|
|
347
|
-
ARISA_DAEMON_INSTANCE_ID: paths.instanceId
|
|
376
|
+
ARISA_DAEMON_INSTANCE_ID: paths.instanceId,
|
|
377
|
+
ARISA_DAEMON_CAPABILITY: capabilityToken
|
|
348
378
|
}
|
|
349
379
|
});
|
|
350
380
|
await new Promise((resolve, reject) => {
|
|
@@ -411,7 +441,13 @@ async function listGlobalDaemonRecords() {
|
|
|
411
441
|
const records = [];
|
|
412
442
|
for (const entry of entries) {
|
|
413
443
|
if (!entry.isDirectory()) continue;
|
|
414
|
-
|
|
444
|
+
let paths;
|
|
445
|
+
try {
|
|
446
|
+
paths = daemonPaths(entry.name);
|
|
447
|
+
} catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const meta = await readJson(paths.metaFile, null);
|
|
415
451
|
if (meta?.toolName && meta?.entryPath) records.push(meta);
|
|
416
452
|
}
|
|
417
453
|
return records;
|
|
@@ -437,7 +473,13 @@ async function listChatDaemonRecords() {
|
|
|
437
473
|
const toolEntries = await readdir(toolsRoot, { withFileTypes: true }).catch(() => []);
|
|
438
474
|
for (const toolEntry of toolEntries) {
|
|
439
475
|
if (!toolEntry.isDirectory()) continue;
|
|
440
|
-
|
|
476
|
+
let paths;
|
|
477
|
+
try {
|
|
478
|
+
paths = daemonPaths({ toolName: toolEntry.name, scope });
|
|
479
|
+
} catch {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const meta = await readJson(paths.metaFile, null);
|
|
441
483
|
if (meta?.toolName && meta?.entryPath) records.push(meta);
|
|
442
484
|
}
|
|
443
485
|
}
|