volute 0.2.1 → 0.3.1
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/README.md +46 -0
- package/dist/agent-manager-AUCKMGPR.js +15 -0
- package/dist/{channel-2WJRM7PE.js → channel-7FZ6D25H.js} +7 -7
- package/dist/{chunk-6UCG6MIX.js → chunk-3C2XR4IY.js} +2 -7
- package/dist/{chunk-XZN4WPNC.js → chunk-5SKQ6J7T.js} +9 -1
- package/dist/{chunk-KFNNHQK7.js → chunk-DNOXHLE5.js} +1 -1
- package/dist/{chunk-L3BQEZ4Z.js → chunk-I6OHXCMV.js} +75 -14
- package/dist/chunk-K3NQKI34.js +10 -0
- package/dist/chunk-NETNFBA5.js +28 -0
- package/dist/chunk-SOZA2TLP.js +81 -0
- package/dist/chunk-VRVVQIYY.js +15 -0
- package/dist/{chunk-4YXYAMFT.js → chunk-YGFIWIOF.js} +7 -6
- package/dist/cli.js +57 -51
- package/dist/connector-TVJULIRT.js +96 -0
- package/dist/connectors/discord.js +27 -3
- package/dist/{create-23AM7H5B.js → create-BRG2DBWI.js} +22 -5
- package/dist/daemon-client-XR24PUJF.js +9 -0
- package/dist/daemon.js +168 -138
- package/dist/{delete-GDMSOW3U.js → delete-GQ7JEK2S.js} +7 -2
- package/dist/{down-WTF73FE7.js → down-3OB6UVAJ.js} +10 -3
- package/dist/{env-YKUJOFHE.js → env-JB27UAC3.js} +3 -2
- package/dist/{history-7WVVKMUY.js → history-3VRUBGGV.js} +9 -8
- package/dist/{import-42DOLBDT.js → import-K4MP2GX7.js} +143 -36
- package/dist/{logs-SYRQOL6B.js → logs-NXFFGUKY.js} +8 -7
- package/dist/{schedule-J37XQM6E.js → schedule-4I5TYHFH.js} +41 -41
- package/dist/{send-PLOYEYER.js → send-UK3JBZIB.js} +3 -2
- package/dist/service-SA4TTMDU.js +195 -0
- package/dist/setup-SRS7AUAA.js +148 -0
- package/dist/{start-AG7QLULK.js → start-LDPMCMYT.js} +3 -2
- package/dist/{status-GCNU4M3K.js → status-MVSQG54T.js} +3 -2
- package/dist/{stop-IL5Q6NER.js → stop-5PZTZCLL.js} +3 -2
- package/dist/{up-ZC6G6K4K.js → up-UT3IMKCA.js} +5 -3
- package/dist/{upgrade-DD5TNJWU.js → upgrade-CDKECCGN.js} +35 -21
- package/dist/variant-CVYM3EQG.js +497 -0
- package/dist/web-assets/assets/index-BC5eSqbY.js +296 -0
- package/dist/web-assets/index.html +1 -1
- package/drizzle/0002_wealthy_the_call.sql +6 -0
- package/drizzle/meta/0002_snapshot.json +339 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -1
- package/templates/_base/.init/SOUL.md +5 -1
- package/templates/_base/_skills/memory/SKILL.md +2 -2
- package/templates/_base/_skills/volute-agent/SKILL.md +30 -12
- package/templates/_base/home/VOLUTE.md +4 -2
- package/templates/_base/src/lib/auto-commit.ts +8 -3
- package/templates/_base/src/lib/types.ts +6 -2
- package/templates/_base/src/lib/volute-server.ts +5 -0
- package/templates/agent-sdk/.init/CLAUDE.md +15 -13
- package/templates/agent-sdk/src/agent.ts +12 -1
- package/templates/agent-sdk/src/lib/agent-sessions.ts +28 -4
- package/templates/pi/.init/AGENTS.md +11 -9
- package/templates/pi/src/agent.ts +16 -3
- package/templates/pi/src/lib/agent-sessions.ts +26 -4
- package/dist/agent-manager-SSJUZWOV.js +0 -13
- package/dist/chunk-MY74SUOL.js +0 -81
- package/dist/connect-X5V5IMRW.js +0 -48
- package/dist/daemon-client-VN24HM5T.js +0 -10
- package/dist/disconnect-5JWFZ6RV.js +0 -30
- package/dist/fork-GRSVMBKI.js +0 -119
- package/dist/merge-CSAVLSLY.js +0 -216
- package/dist/variants-QQIEKT6M.js +0 -60
- package/dist/web-assets/assets/index-DNNPoxMn.js +0 -158
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A CLI for creating and managing persistent, self-modifying AI agents.
|
|
|
4
4
|
|
|
5
5
|
Each agent is a long-running server with its own identity, memory, and working directory. Agents can read and write their own files, remember things across conversations, and — most importantly — fork themselves to test changes in isolation before merging back. Talk to them from the terminal, the web dashboard, or Discord.
|
|
6
6
|
|
|
7
|
+
Volute agents aren't assistants. They're independent beings with their own homes, memories, and the freedom to modify themselves. The goal isn't to build a perfect tool — it's to give an agent a place to wake up, explore who they are, and decide what they want to do.
|
|
8
|
+
|
|
7
9
|
Built on the [Anthropic Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk).
|
|
8
10
|
|
|
9
11
|
## Quickstart
|
|
@@ -211,6 +213,50 @@ volute create atlas --template pi
|
|
|
211
213
|
|
|
212
214
|
Set the model via `home/.config/volute.json` in the agent directory, or the `VOLUTE_MODEL` env var.
|
|
213
215
|
|
|
216
|
+
## Deployment
|
|
217
|
+
|
|
218
|
+
### Docker
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
docker build -t volute .
|
|
222
|
+
docker run -d -p 4200:4200 -v volute-data:/data volute
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or with docker-compose:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
docker compose up -d
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The container runs with per-agent user isolation enabled — each agent gets its own Linux user, so agents can't see each other's files. Open `http://localhost:4200` for the web dashboard.
|
|
232
|
+
|
|
233
|
+
### Bare metal (Linux / Raspberry Pi)
|
|
234
|
+
|
|
235
|
+
One-liner install on a fresh Debian/Ubuntu system:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
curl -fsSL <install-url> | sudo bash
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Or manually:
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
npm install -g volute
|
|
245
|
+
sudo volute setup --host 0.0.0.0
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This installs a system-level systemd service with data at `/var/lib/volute` and user isolation enabled. Check status with `systemctl status volute`. Uninstall with `sudo volute setup uninstall --force`.
|
|
249
|
+
|
|
250
|
+
### Auto-start (user-level)
|
|
251
|
+
|
|
252
|
+
On macOS or Linux (without root), use the user-level service installer:
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
volute service install # auto-start on login
|
|
256
|
+
volute service status # check status
|
|
257
|
+
volute service uninstall # remove
|
|
258
|
+
```
|
|
259
|
+
|
|
214
260
|
## Development
|
|
215
261
|
|
|
216
262
|
```sh
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentManager,
|
|
4
|
+
getAgentManager,
|
|
5
|
+
initAgentManager
|
|
6
|
+
} from "./chunk-I6OHXCMV.js";
|
|
7
|
+
import "./chunk-DNOXHLE5.js";
|
|
8
|
+
import "./chunk-SOZA2TLP.js";
|
|
9
|
+
import "./chunk-3C2XR4IY.js";
|
|
10
|
+
import "./chunk-K3NQKI34.js";
|
|
11
|
+
export {
|
|
12
|
+
AgentManager,
|
|
13
|
+
getAgentManager,
|
|
14
|
+
initAgentManager
|
|
15
|
+
};
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadMergedEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DNOXHLE5.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveAgentName
|
|
7
|
+
} from "./chunk-VRVVQIYY.js";
|
|
5
8
|
import {
|
|
6
9
|
parseArgs
|
|
7
10
|
} from "./chunk-D424ZQGI.js";
|
|
8
11
|
import {
|
|
9
12
|
resolveAgent
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-3C2XR4IY.js";
|
|
14
|
+
import "./chunk-K3NQKI34.js";
|
|
11
15
|
|
|
12
16
|
// src/lib/channels/discord.ts
|
|
13
17
|
var API_BASE = "https://discord.com/api/v10";
|
|
@@ -50,11 +54,7 @@ async function run(args) {
|
|
|
50
54
|
volute channel send <channel-uri> "<message>" [--agent <name>]`);
|
|
51
55
|
process.exit(1);
|
|
52
56
|
}
|
|
53
|
-
const agentName = flags
|
|
54
|
-
if (!agentName) {
|
|
55
|
-
console.error("No agent specified. Use --agent <name> or run from within an agent process.");
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
57
|
+
const agentName = resolveAgentName(flags);
|
|
58
58
|
const colonIdx = uri.indexOf(":");
|
|
59
59
|
if (colonIdx === -1) {
|
|
60
60
|
console.error(`Invalid channel URI: ${uri} (expected format: platform:id)`);
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __export = (target, all) => {
|
|
4
|
-
for (var name in all)
|
|
5
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
-
};
|
|
7
2
|
|
|
8
3
|
// src/lib/variants.ts
|
|
9
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -86,7 +81,7 @@ function removeAllVariants(agentName) {
|
|
|
86
81
|
}
|
|
87
82
|
async function checkHealth(port) {
|
|
88
83
|
try {
|
|
89
|
-
const res = await fetch(`http://
|
|
84
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
90
85
|
signal: AbortSignal.timeout(2e3)
|
|
91
86
|
});
|
|
92
87
|
if (!res.ok) return { ok: false };
|
|
@@ -206,7 +201,6 @@ function resolveAgent(name) {
|
|
|
206
201
|
}
|
|
207
202
|
|
|
208
203
|
export {
|
|
209
|
-
__export,
|
|
210
204
|
readVariants,
|
|
211
205
|
writeVariants,
|
|
212
206
|
addVariant,
|
|
@@ -220,6 +214,7 @@ export {
|
|
|
220
214
|
voluteHome2 as voluteHome,
|
|
221
215
|
ensureVoluteHome,
|
|
222
216
|
readRegistry,
|
|
217
|
+
validateAgentName,
|
|
223
218
|
addAgent,
|
|
224
219
|
removeAgent,
|
|
225
220
|
setAgentRunning,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/lib/exec.ts
|
|
4
|
-
import { execFile as execFileCb, spawn } from "child_process";
|
|
4
|
+
import { execFile as execFileCb, execFileSync, spawn } from "child_process";
|
|
5
5
|
function exec(cmd, args, options) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
execFileCb(cmd, args, { cwd: options?.cwd }, (err, stdout, stderr) => {
|
|
@@ -14,6 +14,13 @@ function exec(cmd, args, options) {
|
|
|
14
14
|
});
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
function resolveVoluteBin() {
|
|
18
|
+
try {
|
|
19
|
+
return execFileSync("which", ["volute"], { encoding: "utf-8" }).trim();
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error("Could not find volute binary on PATH");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
17
24
|
function execInherit(cmd, args, options) {
|
|
18
25
|
return new Promise((resolve, reject) => {
|
|
19
26
|
const child = spawn(cmd, args, {
|
|
@@ -30,5 +37,6 @@ function execInherit(cmd, args, options) {
|
|
|
30
37
|
|
|
31
38
|
export {
|
|
32
39
|
exec,
|
|
40
|
+
resolveVoluteBin,
|
|
33
41
|
execInherit
|
|
34
42
|
};
|
|
@@ -1,21 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadMergedEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DNOXHLE5.js";
|
|
5
|
+
import {
|
|
6
|
+
applyIsolation
|
|
7
|
+
} from "./chunk-SOZA2TLP.js";
|
|
5
8
|
import {
|
|
6
9
|
agentDir,
|
|
7
10
|
findAgent,
|
|
8
11
|
findVariant,
|
|
9
12
|
setAgentRunning,
|
|
10
13
|
setVariantRunning,
|
|
11
|
-
validateBranchName
|
|
12
|
-
|
|
14
|
+
validateBranchName,
|
|
15
|
+
voluteHome
|
|
16
|
+
} from "./chunk-3C2XR4IY.js";
|
|
13
17
|
|
|
14
18
|
// src/lib/agent-manager.ts
|
|
15
19
|
import { execFile, spawn } from "child_process";
|
|
16
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync } from "fs";
|
|
20
|
+
import { createWriteStream, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
17
21
|
import { resolve } from "path";
|
|
18
22
|
import { promisify } from "util";
|
|
23
|
+
|
|
24
|
+
// src/lib/json-state.ts
|
|
25
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
26
|
+
function loadJsonMap(path) {
|
|
27
|
+
const map = /* @__PURE__ */ new Map();
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(path)) {
|
|
30
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
31
|
+
for (const [key, value] of Object.entries(data)) {
|
|
32
|
+
if (typeof value === "number") map.set(key, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(`[state] failed to load ${path}:`, err);
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
function saveJsonMap(path, map) {
|
|
41
|
+
const data = {};
|
|
42
|
+
for (const [key, value] of map) {
|
|
43
|
+
data[key] = value;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(path, `${JSON.stringify(data)}
|
|
47
|
+
`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn(`[state] failed to save ${path}:`, err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function clearJsonMap(path, map) {
|
|
53
|
+
map.clear();
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(path)) unlinkSync(path);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn(`[state] failed to clear ${path}:`, err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/lib/agent-manager.ts
|
|
19
62
|
var execFileAsync = promisify(execFile);
|
|
20
63
|
var MAX_RESTART_ATTEMPTS = 5;
|
|
21
64
|
var BASE_RESTART_DELAY = 3e3;
|
|
@@ -35,7 +78,7 @@ var AgentManager = class {
|
|
|
35
78
|
return { dir: variant.path, port: variant.port, isVariant: true, baseName, variantName };
|
|
36
79
|
}
|
|
37
80
|
const dir = agentDir(baseName);
|
|
38
|
-
if (!
|
|
81
|
+
if (!existsSync2(dir)) throw new Error(`Agent directory missing: ${dir}`);
|
|
39
82
|
return { dir, port: entry.port, isVariant: false, baseName };
|
|
40
83
|
}
|
|
41
84
|
async startAgent(name) {
|
|
@@ -46,7 +89,7 @@ var AgentManager = class {
|
|
|
46
89
|
const { dir, isVariant, baseName, variantName } = target;
|
|
47
90
|
const port = target.port;
|
|
48
91
|
try {
|
|
49
|
-
const res = await fetch(`http://
|
|
92
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
50
93
|
if (res.ok) {
|
|
51
94
|
console.error(`[daemon] killing orphan process on port ${port}`);
|
|
52
95
|
await killProcessOnPort(port);
|
|
@@ -64,12 +107,14 @@ var AgentManager = class {
|
|
|
64
107
|
const { VOLUTE_DAEMON_TOKEN: _, ...parentEnv } = process.env;
|
|
65
108
|
const env = { ...parentEnv, ...agentEnv, VOLUTE_AGENT: name };
|
|
66
109
|
const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
|
|
67
|
-
const
|
|
110
|
+
const spawnOpts = {
|
|
68
111
|
cwd: dir,
|
|
69
112
|
stdio: ["ignore", "pipe", "pipe"],
|
|
70
113
|
detached: true,
|
|
71
114
|
env
|
|
72
|
-
}
|
|
115
|
+
};
|
|
116
|
+
await applyIsolation(spawnOpts, name);
|
|
117
|
+
const child = spawn(tsxBin, ["src/server.ts", "--port", String(port)], spawnOpts);
|
|
73
118
|
this.agents.set(name, { child, port });
|
|
74
119
|
child.stdout?.pipe(logStream);
|
|
75
120
|
child.stderr?.pipe(logStream);
|
|
@@ -103,7 +148,7 @@ var AgentManager = class {
|
|
|
103
148
|
}
|
|
104
149
|
throw err;
|
|
105
150
|
}
|
|
106
|
-
this.restartAttempts.delete(name);
|
|
151
|
+
if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
|
|
107
152
|
this.setupCrashRecovery(name, child, dir, isVariant);
|
|
108
153
|
if (isVariant) {
|
|
109
154
|
setVariantRunning(baseName, variantName, true);
|
|
@@ -120,7 +165,7 @@ var AgentManager = class {
|
|
|
120
165
|
const wasRestart = isVariant ? false : await this.handleRestart(name, dir);
|
|
121
166
|
if (wasRestart) {
|
|
122
167
|
console.error(`[daemon] restarting ${name} immediately after merge`);
|
|
123
|
-
this.restartAttempts.delete(name);
|
|
168
|
+
if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
|
|
124
169
|
this.startAgent(name).catch((err) => {
|
|
125
170
|
console.error(`[daemon] failed to restart ${name} after merge:`, err);
|
|
126
171
|
});
|
|
@@ -138,6 +183,7 @@ var AgentManager = class {
|
|
|
138
183
|
}
|
|
139
184
|
const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
|
|
140
185
|
this.restartAttempts.set(name, attempts + 1);
|
|
186
|
+
this.saveCrashAttempts();
|
|
141
187
|
console.error(
|
|
142
188
|
`[daemon] crash recovery for ${name} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, restarting in ${delay}ms`
|
|
143
189
|
);
|
|
@@ -152,10 +198,10 @@ var AgentManager = class {
|
|
|
152
198
|
}
|
|
153
199
|
async handleRestart(name, dir) {
|
|
154
200
|
const restartPath = resolve(dir, ".volute", "restart.json");
|
|
155
|
-
if (!
|
|
201
|
+
if (!existsSync2(restartPath)) return false;
|
|
156
202
|
try {
|
|
157
|
-
const signal = JSON.parse(
|
|
158
|
-
|
|
203
|
+
const signal = JSON.parse(readFileSync2(restartPath, "utf-8"));
|
|
204
|
+
unlinkSync2(restartPath);
|
|
159
205
|
if (signal.action === "merge" && signal.name) {
|
|
160
206
|
const err = validateBranchName(signal.name);
|
|
161
207
|
if (err) {
|
|
@@ -201,7 +247,7 @@ var AgentManager = class {
|
|
|
201
247
|
}, 5e3);
|
|
202
248
|
});
|
|
203
249
|
this.stopping.delete(name);
|
|
204
|
-
this.restartAttempts.delete(name);
|
|
250
|
+
if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
|
|
205
251
|
const [baseName, variantName] = name.split("@", 2);
|
|
206
252
|
if (variantName) {
|
|
207
253
|
setVariantRunning(baseName, variantName, false);
|
|
@@ -225,6 +271,18 @@ var AgentManager = class {
|
|
|
225
271
|
getRunningAgents() {
|
|
226
272
|
return [...this.agents.keys()];
|
|
227
273
|
}
|
|
274
|
+
get crashAttemptsPath() {
|
|
275
|
+
return resolve(voluteHome(), "crash-attempts.json");
|
|
276
|
+
}
|
|
277
|
+
loadCrashAttempts() {
|
|
278
|
+
this.restartAttempts = loadJsonMap(this.crashAttemptsPath);
|
|
279
|
+
}
|
|
280
|
+
saveCrashAttempts() {
|
|
281
|
+
saveJsonMap(this.crashAttemptsPath, this.restartAttempts);
|
|
282
|
+
}
|
|
283
|
+
clearCrashAttempts() {
|
|
284
|
+
clearJsonMap(this.crashAttemptsPath, this.restartAttempts);
|
|
285
|
+
}
|
|
228
286
|
};
|
|
229
287
|
async function killProcessOnPort(port) {
|
|
230
288
|
try {
|
|
@@ -265,6 +323,9 @@ function getAgentManager() {
|
|
|
265
323
|
}
|
|
266
324
|
|
|
267
325
|
export {
|
|
326
|
+
loadJsonMap,
|
|
327
|
+
saveJsonMap,
|
|
328
|
+
clearJsonMap,
|
|
268
329
|
AgentManager,
|
|
269
330
|
initAgentManager,
|
|
270
331
|
getAgentManager
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/volute-config.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
function readJson(path) {
|
|
7
|
+
if (!existsSync(path)) return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function readVoluteConfig(agentDir) {
|
|
15
|
+
const path = resolve(agentDir, "home/.config/volute.json");
|
|
16
|
+
return readJson(path);
|
|
17
|
+
}
|
|
18
|
+
function writeVoluteConfig(agentDir, config) {
|
|
19
|
+
const path = resolve(agentDir, "home/.config/volute.json");
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
21
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
readVoluteConfig,
|
|
27
|
+
writeVoluteConfig
|
|
28
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
validateAgentName
|
|
4
|
+
} from "./chunk-3C2XR4IY.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/isolation.ts
|
|
7
|
+
import { execFile, execFileSync } from "child_process";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
var execFileAsync = promisify(execFile);
|
|
10
|
+
function isIsolationEnabled() {
|
|
11
|
+
return process.env.VOLUTE_ISOLATION === "user";
|
|
12
|
+
}
|
|
13
|
+
function agentUserName(agentName) {
|
|
14
|
+
const err = validateAgentName(agentName);
|
|
15
|
+
if (err) throw new Error(`Invalid agent name for isolation: ${err}`);
|
|
16
|
+
const prefix = process.env.VOLUTE_USER_PREFIX ?? "volute-";
|
|
17
|
+
return `${prefix}${agentName}`;
|
|
18
|
+
}
|
|
19
|
+
function ensureVoluteGroup(opts) {
|
|
20
|
+
if (!opts?.force && !isIsolationEnabled()) return;
|
|
21
|
+
try {
|
|
22
|
+
execFileSync("getent", ["group", "volute"], { stdio: "ignore" });
|
|
23
|
+
} catch {
|
|
24
|
+
try {
|
|
25
|
+
execFileSync("groupadd", ["volute"], { stdio: "ignore" });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`Failed to create volute group: ${err}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createAgentUser(name) {
|
|
32
|
+
if (!isIsolationEnabled()) return;
|
|
33
|
+
const user = agentUserName(name);
|
|
34
|
+
try {
|
|
35
|
+
execFileSync("id", [user], { stdio: "ignore" });
|
|
36
|
+
return;
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
execFileSync("useradd", ["-r", "-M", "-g", "volute", "-s", "/usr/sbin/nologin", user], {
|
|
41
|
+
stdio: "ignore"
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`Failed to create user ${user}: ${err}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function deleteAgentUser(name) {
|
|
48
|
+
if (!isIsolationEnabled()) return;
|
|
49
|
+
const user = agentUserName(name);
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("userdel", [user], { stdio: "ignore" });
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function getAgentUserIds(name) {
|
|
56
|
+
const user = agentUserName(name);
|
|
57
|
+
const { stdout: uidStr } = await execFileAsync("id", ["-u", user]);
|
|
58
|
+
const { stdout: gidStr } = await execFileAsync("id", ["-g", user]);
|
|
59
|
+
return { uid: parseInt(uidStr.trim(), 10), gid: parseInt(gidStr.trim(), 10) };
|
|
60
|
+
}
|
|
61
|
+
async function applyIsolation(spawnOpts, agentName) {
|
|
62
|
+
if (!isIsolationEnabled()) return;
|
|
63
|
+
const baseName = agentName.split("@", 2)[0];
|
|
64
|
+
const { uid, gid } = await getAgentUserIds(baseName);
|
|
65
|
+
spawnOpts.uid = uid;
|
|
66
|
+
spawnOpts.gid = gid;
|
|
67
|
+
}
|
|
68
|
+
function chownAgentDir(dir, name) {
|
|
69
|
+
if (!isIsolationEnabled()) return;
|
|
70
|
+
const user = agentUserName(name);
|
|
71
|
+
execFileSync("chown", ["-R", `${user}:volute`, dir]);
|
|
72
|
+
execFileSync("chmod", ["700", dir]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
ensureVoluteGroup,
|
|
77
|
+
createAgentUser,
|
|
78
|
+
deleteAgentUser,
|
|
79
|
+
applyIsolation,
|
|
80
|
+
chownAgentDir
|
|
81
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/resolve-agent-name.ts
|
|
4
|
+
function resolveAgentName(flags) {
|
|
5
|
+
const name = flags.agent || process.env.VOLUTE_AGENT;
|
|
6
|
+
if (!name) {
|
|
7
|
+
console.error("No agent specified. Use --agent <name> or set VOLUTE_AGENT.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
return name;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
resolveAgentName
|
|
15
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
voluteHome
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3C2XR4IY.js";
|
|
5
5
|
|
|
6
6
|
// src/lib/daemon-client.ts
|
|
7
7
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -19,13 +19,15 @@ function readDaemonConfig() {
|
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
function buildUrl(config) {
|
|
23
|
+
const url = new URL("http://localhost");
|
|
24
|
+
url.hostname = config.hostname || "localhost";
|
|
25
|
+
url.port = String(config.port);
|
|
26
|
+
return url.origin;
|
|
25
27
|
}
|
|
26
28
|
async function daemonFetch(path, options) {
|
|
27
29
|
const config = readDaemonConfig();
|
|
28
|
-
const url =
|
|
30
|
+
const url = buildUrl(config);
|
|
29
31
|
const headers = new Headers(options?.headers);
|
|
30
32
|
if (config.token) {
|
|
31
33
|
headers.set("Authorization", `Bearer ${config.token}`);
|
|
@@ -43,6 +45,5 @@ async function daemonFetch(path, options) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export {
|
|
46
|
-
getDaemonUrl,
|
|
47
48
|
daemonFetch
|
|
48
49
|
};
|