@agentbean/daemon 0.1.33 → 0.1.35
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/dist/adapters/hermes.js +13 -2
- package/dist/agent-instance.js +1 -1
- package/dist/auth-store.js +34 -14
- package/dist/device-daemon.js +21 -18
- package/dist/index.js +62 -22
- package/dist/profile-paths.js +39 -0
- package/dist/sandbox.js +7 -2
- package/dist/scanner.js +8 -6
- package/dist/workspace-manager.js +29 -4
- package/package.json +1 -1
package/dist/adapters/hermes.js
CHANGED
|
@@ -27,11 +27,22 @@ function buildPrompt(input, systemPrompt) {
|
|
|
27
27
|
}
|
|
28
28
|
const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
29
29
|
const BOX_ONLY_RE = /^[\s─━═╭╮╰╯│┃┌┐└┘├┤┬┴┼]+$/;
|
|
30
|
+
function stripEchoedQueryPreamble(lines) {
|
|
31
|
+
const initIdx = lines.findIndex((line) => {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
return trimmed === 'Initializing agent...' || trimmed === 'Initializing agent…';
|
|
34
|
+
});
|
|
35
|
+
if (initIdx < 0)
|
|
36
|
+
return lines;
|
|
37
|
+
if (!lines.slice(0, initIdx).some((line) => line.trim().startsWith('Query:')))
|
|
38
|
+
return lines;
|
|
39
|
+
return lines.slice(initIdx + 1);
|
|
40
|
+
}
|
|
30
41
|
export function extractHermesReply(output) {
|
|
31
|
-
const lines = output
|
|
42
|
+
const lines = stripEchoedQueryPreamble(output
|
|
32
43
|
.replace(ANSI_RE, '')
|
|
33
44
|
.replace(/\r\n?/g, '\n')
|
|
34
|
-
.split('\n');
|
|
45
|
+
.split('\n'));
|
|
35
46
|
let boxStart = -1;
|
|
36
47
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
37
48
|
if (lines[i]?.trim().startsWith('╭')) {
|
package/dist/agent-instance.js
CHANGED
|
@@ -106,7 +106,7 @@ export class AgentInstance {
|
|
|
106
106
|
systemPrompt: this.config.adapter.systemPrompt,
|
|
107
107
|
workspace: projectWorkspace,
|
|
108
108
|
sandboxProfilePath: req.sandboxed && isSandboxAvailable()
|
|
109
|
-
? generateSandboxProfile(this.id, this.config.adapter.command)
|
|
109
|
+
? generateSandboxProfile(this.id, this.config.adapter.command, [run.runDir])
|
|
110
110
|
: undefined,
|
|
111
111
|
env: { ...(this.config.adapter.env ?? {}), ...workspaceEnv(run) },
|
|
112
112
|
}, ctl.signal);
|
package/dist/auth-store.js
CHANGED
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { homedir } from 'node:os';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!existsSync(
|
|
3
|
+
import { agentbeanHome, authFile, ensureProfileRoot, profileIdForNetwork } from './profile-paths.js';
|
|
4
|
+
export function loadAuth(options = {}) {
|
|
5
|
+
const file = authFile(options.profileId);
|
|
6
|
+
if (!existsSync(file))
|
|
8
7
|
return null;
|
|
9
8
|
try {
|
|
10
|
-
return JSON.parse(readFileSync(
|
|
9
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
11
10
|
}
|
|
12
11
|
catch {
|
|
13
12
|
return null;
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
|
-
export function saveAuth(data) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
writeFileSync(
|
|
15
|
+
export function saveAuth(data, options = {}) {
|
|
16
|
+
const profileId = options.profileId ?? null;
|
|
17
|
+
ensureProfileRoot(profileId);
|
|
18
|
+
writeFileSync(authFile(profileId), JSON.stringify(data, null, 2));
|
|
20
19
|
}
|
|
21
|
-
export function clearAuth() {
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
export function clearAuth(options = {}) {
|
|
21
|
+
const file = authFile(options.profileId);
|
|
22
|
+
if (existsSync(file))
|
|
23
|
+
unlinkSync(file);
|
|
24
|
+
}
|
|
25
|
+
export function listAuthProfiles() {
|
|
26
|
+
const profiles = [];
|
|
27
|
+
const teamsDir = join(agentbeanHome(), 'teams');
|
|
28
|
+
if (existsSync(teamsDir)) {
|
|
29
|
+
for (const entry of readdirSync(teamsDir, { withFileTypes: true })) {
|
|
30
|
+
if (!entry.isDirectory())
|
|
31
|
+
continue;
|
|
32
|
+
const auth = loadAuth({ profileId: entry.name });
|
|
33
|
+
if (auth?.networkId)
|
|
34
|
+
profiles.push({ ...auth, profileId: entry.name });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const legacy = loadAuth();
|
|
38
|
+
if (legacy?.networkId) {
|
|
39
|
+
const profileId = profileIdForNetwork(legacy.networkId);
|
|
40
|
+
if (!profiles.some((profile) => profile.profileId === profileId))
|
|
41
|
+
profiles.push({ ...legacy, profileId });
|
|
42
|
+
}
|
|
43
|
+
return profiles;
|
|
24
44
|
}
|
package/dist/device-daemon.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { io } from 'socket.io-client';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { basename, isAbsolute,
|
|
5
|
-
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, isAbsolute, dirname } from 'node:path';
|
|
6
5
|
import { promisify } from 'node:util';
|
|
7
6
|
import { logger } from './log.js';
|
|
8
7
|
import { AgentInstance } from './agent-instance.js';
|
|
9
8
|
import { pickAdapter } from './adapters/factory.js';
|
|
10
9
|
import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, collectSystemInfo } from './scanner.js';
|
|
11
10
|
import { syncWorkspaceArtifacts } from './workspace-sync.js';
|
|
11
|
+
import { scanCacheFile } from './profile-paths.js';
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
function errorMessage(err) {
|
|
14
14
|
if (err instanceof Error && err.message)
|
|
@@ -86,10 +86,6 @@ export function nativeDirectoryPickerCommands(platform = process.platform) {
|
|
|
86
86
|
return [{
|
|
87
87
|
command: 'osascript',
|
|
88
88
|
args: [
|
|
89
|
-
'-e',
|
|
90
|
-
'tell application "Finder" to activate',
|
|
91
|
-
'-e',
|
|
92
|
-
'delay 0.2',
|
|
93
89
|
'-e',
|
|
94
90
|
'POSIX path of (choose folder with prompt "选择项目目录" default location (path to home folder))',
|
|
95
91
|
],
|
|
@@ -140,8 +136,6 @@ export async function selectNativeDirectory(commands = nativeDirectoryPickerComm
|
|
|
140
136
|
}
|
|
141
137
|
throw new Error(lastError ? `directory picker command not available: ${errorMessage(lastError)}` : 'directory picker command not available');
|
|
142
138
|
}
|
|
143
|
-
const CACHE_DIR = join(homedir(), '.agentbean');
|
|
144
|
-
const CACHE_FILE = join(CACHE_DIR, 'scanned-agents.json');
|
|
145
139
|
function isRuntimeEntry(entry) {
|
|
146
140
|
return entry.category === 'executor-hosted' &&
|
|
147
141
|
['codex', 'claude-code', 'kimi-cli', 'Kimi-cli'].includes(entry.adapterKind);
|
|
@@ -164,11 +158,12 @@ function splitLegacyCache(entries) {
|
|
|
164
158
|
}
|
|
165
159
|
return { agents, runtimes };
|
|
166
160
|
}
|
|
167
|
-
function loadCache() {
|
|
161
|
+
function loadCache(profileId) {
|
|
168
162
|
try {
|
|
169
|
-
|
|
163
|
+
const cacheFile = scanCacheFile(profileId);
|
|
164
|
+
if (!existsSync(cacheFile))
|
|
170
165
|
return null;
|
|
171
|
-
const parsed = JSON.parse(readFileSync(
|
|
166
|
+
const parsed = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
172
167
|
if (Array.isArray(parsed))
|
|
173
168
|
return splitLegacyCache(parsed);
|
|
174
169
|
return {
|
|
@@ -180,11 +175,13 @@ function loadCache() {
|
|
|
180
175
|
return null;
|
|
181
176
|
}
|
|
182
177
|
}
|
|
183
|
-
function saveCache(payload) {
|
|
178
|
+
function saveCache(payload, profileId) {
|
|
184
179
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
const cacheFile = scanCacheFile(profileId);
|
|
181
|
+
const cacheDir = dirname(cacheFile);
|
|
182
|
+
if (!existsSync(cacheDir))
|
|
183
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
184
|
+
writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
|
|
188
185
|
}
|
|
189
186
|
catch (err) {
|
|
190
187
|
logger.warn({ err: err?.message }, 'failed to save scan cache');
|
|
@@ -195,6 +192,8 @@ export function createDeviceSocketOptions(input) {
|
|
|
195
192
|
auth: {
|
|
196
193
|
token: input.token,
|
|
197
194
|
deviceId: input.deviceId,
|
|
195
|
+
machineId: input.machineId,
|
|
196
|
+
profileId: input.profileId,
|
|
198
197
|
networkId: input.networkId,
|
|
199
198
|
agents: input.agents,
|
|
200
199
|
systemInfo: input.systemInfo,
|
|
@@ -205,6 +204,8 @@ export function createDeviceSocketOptions(input) {
|
|
|
205
204
|
directoryPicker: true,
|
|
206
205
|
},
|
|
207
206
|
},
|
|
207
|
+
transports: ['websocket', 'polling'],
|
|
208
|
+
rememberUpgrade: true,
|
|
208
209
|
reconnection: true,
|
|
209
210
|
reconnectionDelay: 1_000,
|
|
210
211
|
reconnectionDelayMax: 10_000,
|
|
@@ -296,13 +297,13 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
296
297
|
}
|
|
297
298
|
async function scanAndRegister(sock, useCache) {
|
|
298
299
|
if (useCache) {
|
|
299
|
-
const cached = loadCache();
|
|
300
|
+
const cached = loadCache(cfg.profileId);
|
|
300
301
|
if (cached) {
|
|
301
302
|
logger.info({ count: cached.agents.length + cached.runtimes.length }, 'using cached scan results');
|
|
302
303
|
emitRegister(sock, cached);
|
|
303
304
|
// Background refresh — only emit if results differ
|
|
304
305
|
scanAll().then((fresh) => {
|
|
305
|
-
saveCache(fresh);
|
|
306
|
+
saveCache(fresh, cfg.profileId);
|
|
306
307
|
const cachedKey = JSON.stringify([
|
|
307
308
|
...cached.agents.map((a) => a.command),
|
|
308
309
|
...cached.runtimes.map((rt) => rt.command),
|
|
@@ -324,7 +325,7 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
324
325
|
// Full scan (no cache or cache miss)
|
|
325
326
|
try {
|
|
326
327
|
const scanned = await scanAll();
|
|
327
|
-
saveCache(scanned);
|
|
328
|
+
saveCache(scanned, cfg.profileId);
|
|
328
329
|
emitRegister(sock, scanned);
|
|
329
330
|
}
|
|
330
331
|
catch (err) {
|
|
@@ -337,6 +338,8 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
337
338
|
socket = io(agentUrl, createDeviceSocketOptions({
|
|
338
339
|
token: cfg.server.token,
|
|
339
340
|
deviceId: cfg.deviceId,
|
|
341
|
+
machineId: cfg.machineId,
|
|
342
|
+
profileId: cfg.profileId,
|
|
340
343
|
networkId: cfg.networkId,
|
|
341
344
|
agents: publicAgents,
|
|
342
345
|
systemInfo,
|
package/dist/index.js
CHANGED
|
@@ -7,16 +7,17 @@ import { AgentInstance } from './agent-instance.js';
|
|
|
7
7
|
import { pickAdapter } from './adapters/factory.js';
|
|
8
8
|
import { logger } from './log.js';
|
|
9
9
|
import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, getDeviceId } from './scanner.js';
|
|
10
|
-
import { loadAuth, saveAuth } from './auth-store.js';
|
|
10
|
+
import { listAuthProfiles, loadAuth, saveAuth } from './auth-store.js';
|
|
11
|
+
import { deviceInstanceId, localAgentsDir, profileIdForNetwork } from './profile-paths.js';
|
|
11
12
|
export function discoveredAgentId(name, deviceId) {
|
|
12
13
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
13
14
|
return deviceId ? `scan-${deviceId}-${slug}` : slug;
|
|
14
15
|
}
|
|
15
|
-
async function discoverAgents(deviceId) {
|
|
16
|
+
async function discoverAgents(deviceId, profileId) {
|
|
16
17
|
const [_runtimes, agentos, local] = await Promise.all([
|
|
17
18
|
scanRuntimes(),
|
|
18
19
|
scanAgentOSAgents(),
|
|
19
|
-
scanLocalAgents(),
|
|
20
|
+
scanLocalAgents(profileId ? localAgentsDir(profileId) : undefined),
|
|
20
21
|
]);
|
|
21
22
|
const seen = new Set();
|
|
22
23
|
const results = [];
|
|
@@ -60,6 +61,24 @@ async function startDeviceDaemon(cfg) {
|
|
|
60
61
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
61
62
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
62
63
|
}
|
|
64
|
+
async function buildCliDeviceConfig(input) {
|
|
65
|
+
const machineId = input.machineId ?? await getDeviceId();
|
|
66
|
+
const deviceId = input.explicitDeviceId ?? deviceInstanceId(machineId, input.networkId);
|
|
67
|
+
logger.info({ serverUrl: input.serverUrl, deviceId, machineId, networkId: input.networkId, profileId: input.profileId }, 'CLI mode: auto-discovering agents');
|
|
68
|
+
const agents = await discoverAgents(deviceId, input.profileId);
|
|
69
|
+
if (agents.length === 0) {
|
|
70
|
+
logger.warn('no agents discovered on this machine. Daemon will start with no agents.');
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
deviceId,
|
|
74
|
+
machineId,
|
|
75
|
+
profileId: input.profileId,
|
|
76
|
+
networkId: input.networkId,
|
|
77
|
+
server: { url: input.serverUrl, token: input.token },
|
|
78
|
+
heartbeatIntervalMs: 10_000,
|
|
79
|
+
agents,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
63
82
|
async function runDeviceMode(cfgPath) {
|
|
64
83
|
let cfg;
|
|
65
84
|
let scannedEntries;
|
|
@@ -129,37 +148,62 @@ async function runCliMode() {
|
|
|
129
148
|
'invite': { type: 'string' },
|
|
130
149
|
'device-id': { type: 'string' },
|
|
131
150
|
'network-id': { type: 'string' },
|
|
151
|
+
'profile': { type: 'string' },
|
|
152
|
+
'all-profiles': { type: 'boolean' },
|
|
132
153
|
'help': { type: 'boolean' },
|
|
133
154
|
},
|
|
134
155
|
strict: true,
|
|
135
156
|
});
|
|
136
157
|
if (values.help) {
|
|
137
|
-
console.log(`Usage: agentbean-daemon --server-url <url> --token <token> [--device-id <id>] [--network-id <id>]
|
|
158
|
+
console.log(`Usage: agentbean-daemon --server-url <url> --token <token> [--device-id <id>] [--network-id <id>] [--profile <id>]
|
|
138
159
|
|
|
139
160
|
Options:
|
|
140
161
|
--server-url AgentBean Server URL (required)
|
|
141
162
|
--token Authentication token (required)
|
|
142
163
|
--device-id Device ID (default: auto-detected from hardware)
|
|
143
164
|
--network-id Team ID (default: default)
|
|
165
|
+
--profile Team profile for local auth/cache isolation
|
|
166
|
+
--all-profiles Start one connection for every saved team profile
|
|
144
167
|
`);
|
|
145
168
|
process.exit(0);
|
|
146
169
|
}
|
|
170
|
+
if (values['all-profiles']) {
|
|
171
|
+
const profiles = listAuthProfiles();
|
|
172
|
+
if (profiles.length === 0) {
|
|
173
|
+
console.error('Error: no saved AgentBean team profiles found.');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const machineId = values['device-id'] ?? await getDeviceId();
|
|
177
|
+
const configs = await Promise.all(profiles.map((profile) => buildCliDeviceConfig({
|
|
178
|
+
serverUrl: profile.serverUrl,
|
|
179
|
+
token: profile.token,
|
|
180
|
+
networkId: profile.networkId ?? networkIdFromToken(profile.token) ?? 'default',
|
|
181
|
+
machineId,
|
|
182
|
+
profileId: profile.profileId,
|
|
183
|
+
})));
|
|
184
|
+
await Promise.all(configs.map((cfg) => startDeviceDaemon(cfg)));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
147
187
|
let serverUrl = values['server-url'] ?? process.env.AGENT_BEAN_SERVER_URL;
|
|
148
188
|
let token = values['token'] ?? process.env.AGENT_BEAN_AGENT_TOKEN;
|
|
149
189
|
let savedAuth = null;
|
|
150
190
|
let networkId = values['network-id'] ?? 'default';
|
|
191
|
+
let profileId = values.profile ?? process.env.AGENTBEAN_PROFILE;
|
|
151
192
|
if (values.invite) {
|
|
152
193
|
if (!serverUrl) {
|
|
153
194
|
console.error('Error: --server-url is required with --invite.');
|
|
154
195
|
process.exit(1);
|
|
155
196
|
}
|
|
156
|
-
const
|
|
197
|
+
const machineId = values['device-id'] ?? await getDeviceId();
|
|
198
|
+
const auth = await runInviteMode(serverUrl, values.invite, machineId);
|
|
157
199
|
serverUrl = auth.serverUrl;
|
|
158
200
|
token = auth.token;
|
|
159
201
|
networkId = auth.networkId ?? networkId;
|
|
202
|
+
profileId = profileId ?? profileIdForNetwork(networkId);
|
|
203
|
+
saveAuth(auth, { profileId });
|
|
160
204
|
}
|
|
161
205
|
else if (!token) {
|
|
162
|
-
savedAuth = loadAuth();
|
|
206
|
+
savedAuth = loadAuth({ profileId });
|
|
163
207
|
if (savedAuth) {
|
|
164
208
|
serverUrl = serverUrl ?? savedAuth.serverUrl;
|
|
165
209
|
token = savedAuth.token;
|
|
@@ -182,19 +226,16 @@ Options:
|
|
|
182
226
|
savedNetworkId: savedAuth?.networkId,
|
|
183
227
|
fallbackNetworkId: networkId,
|
|
184
228
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const cfg = {
|
|
192
|
-
deviceId,
|
|
229
|
+
profileId = profileId ?? profileIdForNetwork(networkId);
|
|
230
|
+
const machineId = values['device-id'] ?? await getDeviceId();
|
|
231
|
+
const cfg = await buildCliDeviceConfig({
|
|
232
|
+
serverUrl,
|
|
233
|
+
token,
|
|
193
234
|
networkId,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
};
|
|
235
|
+
machineId,
|
|
236
|
+
explicitDeviceId: values['device-id'] ? values['device-id'] : undefined,
|
|
237
|
+
profileId,
|
|
238
|
+
});
|
|
198
239
|
await startDeviceDaemon(cfg);
|
|
199
240
|
}
|
|
200
241
|
function normalizeBaseUrl(serverUrl) {
|
|
@@ -238,7 +279,7 @@ export function socketErrorMessage(err) {
|
|
|
238
279
|
.map((value) => value.trim());
|
|
239
280
|
return [...new Set(details)].join(': ') || 'unknown socket error';
|
|
240
281
|
}
|
|
241
|
-
async function runInviteMode(serverUrl, inviteCode) {
|
|
282
|
+
async function runInviteMode(serverUrl, inviteCode, deviceId) {
|
|
242
283
|
const { io } = await import('socket.io-client');
|
|
243
284
|
const { execFile } = await import('node:child_process');
|
|
244
285
|
const baseUrl = normalizeBaseUrl(serverUrl);
|
|
@@ -265,7 +306,7 @@ async function runInviteMode(serverUrl, inviteCode) {
|
|
|
265
306
|
clearTimeout(connectTimer);
|
|
266
307
|
logger.info('invite mode: connected, validating invite code');
|
|
267
308
|
console.log('Connected. Validating invite code...');
|
|
268
|
-
socket.emit('auth:invite:validate', { code: inviteCode }, (res) => {
|
|
309
|
+
socket.emit('auth:invite:validate', { code: inviteCode, deviceId }, (res) => {
|
|
269
310
|
if (!res?.ok) {
|
|
270
311
|
fail(new Error(res?.error ?? 'invalid invite code'));
|
|
271
312
|
return;
|
|
@@ -290,7 +331,6 @@ async function runInviteMode(serverUrl, inviteCode) {
|
|
|
290
331
|
userId: payload.userId,
|
|
291
332
|
networkId: payload.networkId,
|
|
292
333
|
};
|
|
293
|
-
saveAuth(auth);
|
|
294
334
|
logger.info({ networkId: auth.networkId }, 'invite mode: token received and saved');
|
|
295
335
|
console.log('Registration complete! Starting daemon...');
|
|
296
336
|
socket.disconnect();
|
|
@@ -300,7 +340,7 @@ async function runInviteMode(serverUrl, inviteCode) {
|
|
|
300
340
|
}
|
|
301
341
|
export async function main() {
|
|
302
342
|
// Check for CLI flags first (npx mode)
|
|
303
|
-
const hasCliFlags = process.argv.some((a) => a === '--server-url' || a === '--token' || a === '--invite' || a === '--help');
|
|
343
|
+
const hasCliFlags = process.argv.some((a) => a === '--server-url' || a === '--token' || a === '--invite' || a === '--profile' || a === '--all-profiles' || a === '--help');
|
|
304
344
|
if (hasCliFlags) {
|
|
305
345
|
await runCliMode();
|
|
306
346
|
return;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export function agentbeanHome() {
|
|
6
|
+
return process.env.AGENTBEAN_HOME?.trim() || join(homedir(), '.agentbean');
|
|
7
|
+
}
|
|
8
|
+
export function profileIdForNetwork(networkId) {
|
|
9
|
+
const source = networkId?.trim() || 'default';
|
|
10
|
+
const slug = source.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
11
|
+
return slug || 'default';
|
|
12
|
+
}
|
|
13
|
+
export function profileRoot(profileId) {
|
|
14
|
+
const explicitDir = process.env.AGENTBEAN_PROFILE_DIR?.trim();
|
|
15
|
+
if (explicitDir && profileId)
|
|
16
|
+
return explicitDir;
|
|
17
|
+
if (!profileId)
|
|
18
|
+
return agentbeanHome();
|
|
19
|
+
return join(agentbeanHome(), 'teams', profileIdForNetwork(profileId));
|
|
20
|
+
}
|
|
21
|
+
export function ensureProfileRoot(profileId) {
|
|
22
|
+
const root = profileRoot(profileId);
|
|
23
|
+
if (!existsSync(root))
|
|
24
|
+
mkdirSync(root, { recursive: true });
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
export function authFile(profileId) {
|
|
28
|
+
return join(profileRoot(profileId), 'auth.json');
|
|
29
|
+
}
|
|
30
|
+
export function scanCacheFile(profileId) {
|
|
31
|
+
return join(profileRoot(profileId), 'scanned-agents.json');
|
|
32
|
+
}
|
|
33
|
+
export function localAgentsDir(profileId) {
|
|
34
|
+
return join(profileRoot(profileId), 'agents');
|
|
35
|
+
}
|
|
36
|
+
export function deviceInstanceId(machineId, networkId) {
|
|
37
|
+
const hash = createHash('sha256').update(`${networkId}:${machineId}`).digest('hex');
|
|
38
|
+
return `dev_${hash.slice(0, 24)}`;
|
|
39
|
+
}
|
package/dist/sandbox.js
CHANGED
|
@@ -21,16 +21,21 @@ export function isSandboxAvailable() {
|
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
export function generateSandboxProfile(agentId, runtimePath) {
|
|
24
|
+
export function generateSandboxProfile(agentId, runtimePath, writableDirs = []) {
|
|
25
25
|
const workspaceDir = getWorkspaceDir(agentId);
|
|
26
26
|
const runtimeDir = runtimePath.includes('/') ? dirname(runtimePath) : '/usr/bin';
|
|
27
27
|
const profilePath = `/tmp/agentbean-sandbox-${agentId}.sb`;
|
|
28
|
+
const extraWritableRules = writableDirs
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((dir) => `(allow file-read* file-write*
|
|
31
|
+
(subpath "${escapeSchemeString(dir)}"))`)
|
|
32
|
+
.join('\n');
|
|
28
33
|
const profile = `(version 1)
|
|
29
34
|
(allow file-read* file-write*
|
|
30
35
|
(subpath "${escapeSchemeString(workspaceDir)}"))
|
|
31
36
|
(allow file-read* file-write*
|
|
32
37
|
(subpath "/tmp"))
|
|
33
|
-
(allow file-read*
|
|
38
|
+
${extraWritableRules ? `${extraWritableRules}\n` : ''}(allow file-read*
|
|
34
39
|
(subpath "${escapeSchemeString(runtimeDir)}"))
|
|
35
40
|
(allow file-read*
|
|
36
41
|
(subpath "/bin")
|
package/dist/scanner.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import { logger } from "./log.js";
|
|
7
|
+
import { agentbeanHome, localAgentsDir } from "./profile-paths.js";
|
|
7
8
|
function readDaemonVersion() {
|
|
8
9
|
try {
|
|
9
10
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
@@ -126,7 +127,7 @@ export function parseOpenClawAgentId(output) {
|
|
|
126
127
|
return null;
|
|
127
128
|
}
|
|
128
129
|
// --- Machine ID (stable per-device identifier) ---
|
|
129
|
-
const MACHINE_ID_FILE = join(
|
|
130
|
+
const MACHINE_ID_FILE = () => join(agentbeanHome(), "device-id");
|
|
130
131
|
function getFirstMacAddress() {
|
|
131
132
|
const ifaces = os.networkInterfaces();
|
|
132
133
|
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
@@ -185,8 +186,9 @@ async function readPlatformMachineId() {
|
|
|
185
186
|
*/
|
|
186
187
|
export async function getDeviceId() {
|
|
187
188
|
// 1. Read cached ID
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
const machineIdFile = MACHINE_ID_FILE();
|
|
190
|
+
if (existsSync(machineIdFile)) {
|
|
191
|
+
const cached = readFileSync(machineIdFile, "utf-8").trim();
|
|
190
192
|
if (cached)
|
|
191
193
|
return cached;
|
|
192
194
|
}
|
|
@@ -221,10 +223,10 @@ export async function getDeviceId() {
|
|
|
221
223
|
}
|
|
222
224
|
// 3. Cache to file
|
|
223
225
|
try {
|
|
224
|
-
const dir =
|
|
226
|
+
const dir = agentbeanHome();
|
|
225
227
|
if (!existsSync(dir))
|
|
226
228
|
mkdirSync(dir, { recursive: true });
|
|
227
|
-
writeFileSync(
|
|
229
|
+
writeFileSync(machineIdFile, deviceId);
|
|
228
230
|
}
|
|
229
231
|
catch {
|
|
230
232
|
// non-fatal
|
|
@@ -308,7 +310,7 @@ export async function scanAgentOSAgents() {
|
|
|
308
310
|
return [hermes, openclaw].filter((a) => a !== null);
|
|
309
311
|
}
|
|
310
312
|
// --- Scan local agent definitions from filesystem ---
|
|
311
|
-
export async function scanLocalAgents(scanDir =
|
|
313
|
+
export async function scanLocalAgents(scanDir = localAgentsDir(process.env.AGENTBEAN_PROFILE)) {
|
|
312
314
|
if (!existsSync(scanDir)) {
|
|
313
315
|
return [];
|
|
314
316
|
}
|
|
@@ -34,6 +34,14 @@ function uniqueDestination(dir, filename) {
|
|
|
34
34
|
}
|
|
35
35
|
return candidate;
|
|
36
36
|
}
|
|
37
|
+
function fileNamePreference(path) {
|
|
38
|
+
const name = basename(path).toLowerCase();
|
|
39
|
+
if (/^ig_[a-f0-9]{32,}\.(png|jpe?g|gif|webp)$/i.test(name))
|
|
40
|
+
return 0;
|
|
41
|
+
if (/^(image|output|generated)[._-]?\d*\.(png|jpe?g|gif|webp)$/i.test(name))
|
|
42
|
+
return 1;
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
37
45
|
function escapeRegExp(value) {
|
|
38
46
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
47
|
}
|
|
@@ -94,12 +102,14 @@ export function workspaceEnv(run) {
|
|
|
94
102
|
}
|
|
95
103
|
export function archiveOutputFiles(run, files) {
|
|
96
104
|
const archived = [];
|
|
97
|
-
const
|
|
105
|
+
const candidates = new Map();
|
|
106
|
+
const hashOrder = [];
|
|
107
|
+
const seenPaths = new Set();
|
|
98
108
|
for (const file of files) {
|
|
99
109
|
const abs = isAbsolute(file) ? file : resolve(file);
|
|
100
|
-
if (
|
|
110
|
+
if (seenPaths.has(abs))
|
|
101
111
|
continue;
|
|
102
|
-
|
|
112
|
+
seenPaths.add(abs);
|
|
103
113
|
let st;
|
|
104
114
|
try {
|
|
105
115
|
st = statSync(abs);
|
|
@@ -109,6 +119,21 @@ export function archiveOutputFiles(run, files) {
|
|
|
109
119
|
catch {
|
|
110
120
|
continue;
|
|
111
121
|
}
|
|
122
|
+
const hash = fileHash(abs);
|
|
123
|
+
const current = candidates.get(hash);
|
|
124
|
+
if (!current) {
|
|
125
|
+
candidates.set(hash, { abs, hash });
|
|
126
|
+
hashOrder.push(hash);
|
|
127
|
+
}
|
|
128
|
+
else if (fileNamePreference(abs) > fileNamePreference(current.abs)) {
|
|
129
|
+
candidates.set(hash, { abs, hash });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const hash of hashOrder) {
|
|
133
|
+
const candidate = candidates.get(hash);
|
|
134
|
+
if (!candidate)
|
|
135
|
+
continue;
|
|
136
|
+
const abs = candidate.abs;
|
|
112
137
|
const alreadyInRun = relative(run.runDir, abs);
|
|
113
138
|
const archivedPath = alreadyInRun && !alreadyInRun.startsWith('..') && !isAbsolute(alreadyInRun)
|
|
114
139
|
? abs
|
|
@@ -121,7 +146,7 @@ export function archiveOutputFiles(run, files) {
|
|
|
121
146
|
archivedPath,
|
|
122
147
|
relativePath: relative(run.agentDir, archivedPath),
|
|
123
148
|
pathKind: 'output',
|
|
124
|
-
sha256:
|
|
149
|
+
sha256: candidate.hash,
|
|
125
150
|
sizeBytes,
|
|
126
151
|
});
|
|
127
152
|
}
|