@agentbean/daemon 0.1.9 → 0.1.10
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/claude-code.js +1 -0
- package/dist/adapters/codex.js +1 -1
- package/dist/adapters/hermes.js +1 -0
- package/dist/adapters/openclaw.js +1 -0
- package/dist/agent-instance.js +38 -8
- package/dist/connection.js +33 -6
- package/dist/device-daemon.js +21 -1
- package/dist/workspace-manager.js +134 -0
- package/dist/workspace-sync.js +69 -0
- package/package.json +1 -1
package/dist/adapters/codex.js
CHANGED
package/dist/adapters/hermes.js
CHANGED
|
@@ -72,6 +72,7 @@ export class HermesAdapter {
|
|
|
72
72
|
const child = spawn(this.opts.command, buildArgs(runtimeArgs(this.opts.args), prompt), {
|
|
73
73
|
cwd,
|
|
74
74
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
env: { ...process.env, ...(input.env ?? {}) },
|
|
75
76
|
});
|
|
76
77
|
const stdoutChunks = [];
|
|
77
78
|
const stderrChunks = [];
|
|
@@ -32,6 +32,7 @@ export class OpenClawAdapter {
|
|
|
32
32
|
const child = spawn(this.opts.command, buildArgs(this.opts.args ?? [], prompt), {
|
|
33
33
|
cwd,
|
|
34
34
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
env: { ...process.env, ...(input.env ?? {}) },
|
|
35
36
|
});
|
|
36
37
|
const stdoutChunks = [];
|
|
37
38
|
const stderrChunks = [];
|
package/dist/agent-instance.js
CHANGED
|
@@ -2,6 +2,7 @@ import { logger } from './log.js';
|
|
|
2
2
|
import { uploadArtifact } from './uploader.js';
|
|
3
3
|
import { postProcess } from './post-process.js';
|
|
4
4
|
import { generateSandboxProfile, getWorkspaceDir, isSandboxAvailable } from './sandbox.js';
|
|
5
|
+
import { archiveOutputFiles, beginAgentWorkspaceRun, finishAgentWorkspaceRun, workspaceEnv, } from './workspace-manager.js';
|
|
5
6
|
function errorMessage(err) {
|
|
6
7
|
if (err instanceof Error && err.message)
|
|
7
8
|
return err.message;
|
|
@@ -41,37 +42,65 @@ export class AgentInstance {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
async handleDispatch(opts) {
|
|
44
|
-
const { socket, req, serverUrl, token, networkId } = opts;
|
|
45
|
+
const { socket, req, serverUrl, token, networkId, deviceId } = opts;
|
|
45
46
|
const ctl = new AbortController();
|
|
46
47
|
const dispatchStart = Date.now();
|
|
48
|
+
const teamId = req.teamId ?? req.networkId ?? networkId;
|
|
49
|
+
const projectWorkspace = req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace;
|
|
50
|
+
const run = beginAgentWorkspaceRun({
|
|
51
|
+
teamId,
|
|
52
|
+
teamName: req.teamName,
|
|
53
|
+
agentId: this.id,
|
|
54
|
+
agentName: this.name,
|
|
55
|
+
runId: req.requestId,
|
|
56
|
+
prompt: req.prompt,
|
|
57
|
+
projectDir: projectWorkspace,
|
|
58
|
+
});
|
|
59
|
+
let archivedFiles = [];
|
|
47
60
|
try {
|
|
48
61
|
const rawBody = await this.adapter.ask({
|
|
49
62
|
prompt: req.prompt,
|
|
50
63
|
history: req.history ?? [],
|
|
51
64
|
systemPrompt: this.config.adapter.systemPrompt,
|
|
52
|
-
workspace:
|
|
65
|
+
workspace: projectWorkspace,
|
|
53
66
|
sandboxProfilePath: req.sandboxed && isSandboxAvailable()
|
|
54
67
|
? generateSandboxProfile(this.id, this.config.adapter.command)
|
|
55
68
|
: undefined,
|
|
69
|
+
env: workspaceEnv(run),
|
|
56
70
|
}, ctl.signal);
|
|
57
|
-
const processed = await postProcess(rawBody,
|
|
71
|
+
const processed = await postProcess(rawBody, projectWorkspace, this.adapter.kind, dispatchStart, {
|
|
72
|
+
outputDirs: [run.outputDir, run.intermediateDir],
|
|
73
|
+
});
|
|
74
|
+
archivedFiles = archiveOutputFiles(run, processed.outputFiles);
|
|
75
|
+
finishAgentWorkspaceRun(run, { replyText: processed.replyText, files: archivedFiles, status: 'completed' });
|
|
58
76
|
const artifactIds = [];
|
|
59
|
-
if (
|
|
60
|
-
for (const
|
|
77
|
+
if (archivedFiles.length > 0) {
|
|
78
|
+
for (const file of archivedFiles) {
|
|
61
79
|
try {
|
|
62
80
|
const result = await uploadArtifact({
|
|
63
81
|
serverUrl,
|
|
64
82
|
token,
|
|
65
|
-
networkId,
|
|
66
|
-
filePath,
|
|
83
|
+
networkId: teamId,
|
|
84
|
+
filePath: file.archivedPath,
|
|
67
85
|
channelId: req.channelId,
|
|
68
86
|
uploaderId: this.id,
|
|
87
|
+
metaJson: JSON.stringify({
|
|
88
|
+
kind: 'agent-workspace-file',
|
|
89
|
+
teamId,
|
|
90
|
+
agentId: this.id,
|
|
91
|
+
runId: req.requestId,
|
|
92
|
+
deviceId: deviceId ?? null,
|
|
93
|
+
pathKind: file.pathKind,
|
|
94
|
+
relativePath: file.relativePath,
|
|
95
|
+
originalPath: file.originalPath,
|
|
96
|
+
sha256: file.sha256,
|
|
97
|
+
}),
|
|
69
98
|
});
|
|
70
99
|
if (result)
|
|
71
100
|
artifactIds.push(result.id);
|
|
72
101
|
}
|
|
73
102
|
catch (err) {
|
|
74
|
-
logger.warn({ err: err.message, filePath }, 'artifact upload failed');
|
|
103
|
+
logger.warn({ err: err.message, filePath: file.archivedPath }, 'artifact upload failed');
|
|
75
104
|
}
|
|
76
105
|
}
|
|
77
106
|
}
|
|
@@ -85,6 +114,7 @@ export class AgentInstance {
|
|
|
85
114
|
}
|
|
86
115
|
catch (err) {
|
|
87
116
|
const message = errorMessage(err);
|
|
117
|
+
finishAgentWorkspaceRun(run, { files: archivedFiles, status: 'failed', error: message });
|
|
88
118
|
logger.error({ err: message, requestId: req.requestId, agentId: this.id }, 'dispatch failed');
|
|
89
119
|
socket.emit('error_event', {
|
|
90
120
|
agentId: this.id,
|
package/dist/connection.js
CHANGED
|
@@ -2,6 +2,7 @@ import { io } from 'socket.io-client';
|
|
|
2
2
|
import { logger } from './log.js';
|
|
3
3
|
import { uploadArtifact } from './uploader.js';
|
|
4
4
|
import { postProcess } from './post-process.js';
|
|
5
|
+
import { archiveOutputFiles, beginAgentWorkspaceRun, finishAgentWorkspaceRun, workspaceEnv, } from './workspace-manager.js';
|
|
5
6
|
function errorMessage(err) {
|
|
6
7
|
if (err instanceof Error && err.message)
|
|
7
8
|
return err.message;
|
|
@@ -57,32 +58,57 @@ export function createConnection(cfg, adapter) {
|
|
|
57
58
|
queue = queue.then(async () => {
|
|
58
59
|
const ctl = new AbortController();
|
|
59
60
|
const dispatchStart = Date.now();
|
|
61
|
+
const teamId = 'default';
|
|
62
|
+
const run = beginAgentWorkspaceRun({
|
|
63
|
+
teamId,
|
|
64
|
+
agentId: cfg.id,
|
|
65
|
+
agentName: cfg.name,
|
|
66
|
+
runId: req.requestId,
|
|
67
|
+
prompt: req.prompt,
|
|
68
|
+
projectDir: cfg.adapter.workspace,
|
|
69
|
+
});
|
|
70
|
+
let archivedFiles = [];
|
|
60
71
|
try {
|
|
61
72
|
const rawBody = await adapter.ask({
|
|
62
73
|
prompt: req.prompt,
|
|
63
74
|
history: req.history ?? [],
|
|
64
75
|
systemPrompt: cfg.adapter.systemPrompt,
|
|
65
76
|
workspace: cfg.adapter.workspace,
|
|
77
|
+
env: workspaceEnv(run),
|
|
66
78
|
}, ctl.signal);
|
|
67
|
-
const processed = await postProcess(rawBody, cfg.adapter.workspace, cfg.adapter.kind, dispatchStart
|
|
79
|
+
const processed = await postProcess(rawBody, cfg.adapter.workspace, cfg.adapter.kind, dispatchStart, {
|
|
80
|
+
outputDirs: [run.outputDir, run.intermediateDir],
|
|
81
|
+
});
|
|
82
|
+
archivedFiles = archiveOutputFiles(run, processed.outputFiles);
|
|
83
|
+
finishAgentWorkspaceRun(run, { replyText: processed.replyText, files: archivedFiles, status: 'completed' });
|
|
68
84
|
const artifactIds = [];
|
|
69
|
-
if (
|
|
85
|
+
if (archivedFiles.length > 0) {
|
|
70
86
|
const httpBase = cfg.server.url.replace(/\/agent$/, '');
|
|
71
|
-
for (const
|
|
87
|
+
for (const file of archivedFiles) {
|
|
72
88
|
try {
|
|
73
89
|
const result = await uploadArtifact({
|
|
74
90
|
serverUrl: httpBase,
|
|
75
91
|
token: cfg.server.token,
|
|
76
|
-
networkId:
|
|
77
|
-
filePath,
|
|
92
|
+
networkId: teamId,
|
|
93
|
+
filePath: file.archivedPath,
|
|
78
94
|
channelId: req.channelId,
|
|
79
95
|
uploaderId: cfg.id,
|
|
96
|
+
metaJson: JSON.stringify({
|
|
97
|
+
kind: 'agent-workspace-file',
|
|
98
|
+
teamId,
|
|
99
|
+
agentId: cfg.id,
|
|
100
|
+
runId: req.requestId,
|
|
101
|
+
pathKind: file.pathKind,
|
|
102
|
+
relativePath: file.relativePath,
|
|
103
|
+
originalPath: file.originalPath,
|
|
104
|
+
sha256: file.sha256,
|
|
105
|
+
}),
|
|
80
106
|
});
|
|
81
107
|
if (result)
|
|
82
108
|
artifactIds.push(result.id);
|
|
83
109
|
}
|
|
84
110
|
catch (err) {
|
|
85
|
-
logger.warn({ err: err.message, filePath }, 'artifact upload failed');
|
|
111
|
+
logger.warn({ err: err.message, filePath: file.archivedPath }, 'artifact upload failed');
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
114
|
}
|
|
@@ -95,6 +121,7 @@ export function createConnection(cfg, adapter) {
|
|
|
95
121
|
}
|
|
96
122
|
catch (err) {
|
|
97
123
|
const message = errorMessage(err);
|
|
124
|
+
finishAgentWorkspaceRun(run, { files: archivedFiles, status: 'failed', error: message });
|
|
98
125
|
logger.error({ err: message, requestId: req.requestId }, 'dispatch failed');
|
|
99
126
|
currentSocket.emit('error_event', {
|
|
100
127
|
at: Date.now(),
|
package/dist/device-daemon.js
CHANGED
|
@@ -6,6 +6,7 @@ import { logger } from './log.js';
|
|
|
6
6
|
import { AgentInstance } from './agent-instance.js';
|
|
7
7
|
import { pickAdapter } from './adapters/factory.js';
|
|
8
8
|
import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, collectSystemInfo } from './scanner.js';
|
|
9
|
+
import { syncWorkspaceArtifacts } from './workspace-sync.js';
|
|
9
10
|
function errorMessage(err) {
|
|
10
11
|
if (err instanceof Error && err.message)
|
|
11
12
|
return err.message;
|
|
@@ -103,7 +104,9 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
103
104
|
let socket = null;
|
|
104
105
|
let heartbeatTimer = null;
|
|
105
106
|
let rescanTimer = null;
|
|
107
|
+
let workspaceSyncTimer = null;
|
|
106
108
|
const RESCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
109
|
+
const WORKSPACE_SYNC_INTERVAL_MS = 2 * 60 * 1000;
|
|
107
110
|
const queues = new Map();
|
|
108
111
|
const httpBase = cfg.server.url.replace(/\/agent$/, '');
|
|
109
112
|
let firstConnect = true;
|
|
@@ -232,6 +235,14 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
232
235
|
return;
|
|
233
236
|
scanAndRegister(socket, false);
|
|
234
237
|
}, RESCAN_INTERVAL_MS);
|
|
238
|
+
syncWorkspaceArtifacts({ serverUrl: httpBase, token: cfg.server.token, networkId: cfg.networkId });
|
|
239
|
+
if (workspaceSyncTimer)
|
|
240
|
+
clearInterval(workspaceSyncTimer);
|
|
241
|
+
workspaceSyncTimer = setInterval(() => {
|
|
242
|
+
if (!socket?.connected)
|
|
243
|
+
return;
|
|
244
|
+
syncWorkspaceArtifacts({ serverUrl: httpBase, token: cfg.server.token, networkId: cfg.networkId });
|
|
245
|
+
}, WORKSPACE_SYNC_INTERVAL_MS);
|
|
235
246
|
});
|
|
236
247
|
socket.on('connect_error', (err) => {
|
|
237
248
|
logger.error({ err: err.message }, 'connect_error');
|
|
@@ -288,7 +299,8 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
288
299
|
req,
|
|
289
300
|
serverUrl: httpBase,
|
|
290
301
|
token: cfg.server.token,
|
|
291
|
-
networkId: cfg.networkId,
|
|
302
|
+
networkId: req.teamId ?? req.networkId ?? cfg.networkId,
|
|
303
|
+
deviceId: cfg.deviceId,
|
|
292
304
|
});
|
|
293
305
|
}).catch((err) => {
|
|
294
306
|
const message = errorMessage(err);
|
|
@@ -316,6 +328,10 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
316
328
|
clearInterval(rescanTimer);
|
|
317
329
|
rescanTimer = null;
|
|
318
330
|
}
|
|
331
|
+
if (workspaceSyncTimer) {
|
|
332
|
+
clearInterval(workspaceSyncTimer);
|
|
333
|
+
workspaceSyncTimer = null;
|
|
334
|
+
}
|
|
319
335
|
});
|
|
320
336
|
},
|
|
321
337
|
async stop() {
|
|
@@ -327,6 +343,10 @@ export function createDeviceDaemon(cfg, agents) {
|
|
|
327
343
|
clearInterval(rescanTimer);
|
|
328
344
|
rescanTimer = null;
|
|
329
345
|
}
|
|
346
|
+
if (workspaceSyncTimer) {
|
|
347
|
+
clearInterval(workspaceSyncTimer);
|
|
348
|
+
workspaceSyncTimer = null;
|
|
349
|
+
}
|
|
330
350
|
socket?.close();
|
|
331
351
|
socket = null;
|
|
332
352
|
},
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
function rootDir() {
|
|
6
|
+
return resolve(process.env.AGENTBEAN_HOME ?? join(homedir(), '.agentbean'));
|
|
7
|
+
}
|
|
8
|
+
function safeSegment(value) {
|
|
9
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
|
10
|
+
}
|
|
11
|
+
function ensureDir(dir) {
|
|
12
|
+
if (!existsSync(dir))
|
|
13
|
+
mkdirSync(dir, { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
export function getAgentWorkspaceDir(teamId, agentId) {
|
|
17
|
+
return ensureDir(join(rootDir(), 'teams', safeSegment(teamId), 'agents', safeSegment(agentId)));
|
|
18
|
+
}
|
|
19
|
+
function writeJson(path, value) {
|
|
20
|
+
ensureDir(dirname(path));
|
|
21
|
+
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
22
|
+
}
|
|
23
|
+
function fileHash(path) {
|
|
24
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
function uniqueDestination(dir, filename) {
|
|
27
|
+
const ext = extname(filename);
|
|
28
|
+
const stem = ext ? filename.slice(0, -ext.length) : filename;
|
|
29
|
+
let candidate = join(dir, filename);
|
|
30
|
+
let index = 1;
|
|
31
|
+
while (existsSync(candidate)) {
|
|
32
|
+
candidate = join(dir, `${stem}-${index}${ext}`);
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
export function beginAgentWorkspaceRun(input) {
|
|
38
|
+
const teamId = safeSegment(input.teamId);
|
|
39
|
+
const agentId = safeSegment(input.agentId);
|
|
40
|
+
const runId = safeSegment(input.runId);
|
|
41
|
+
const teamDir = ensureDir(join(rootDir(), 'teams', teamId));
|
|
42
|
+
const agentDir = ensureDir(join(teamDir, 'agents', agentId));
|
|
43
|
+
const runDir = ensureDir(join(agentDir, 'runs', runId));
|
|
44
|
+
const outputDir = ensureDir(join(runDir, 'outputs'));
|
|
45
|
+
const intermediateDir = ensureDir(join(runDir, 'intermediates'));
|
|
46
|
+
const logDir = ensureDir(join(runDir, 'logs'));
|
|
47
|
+
writeJson(join(teamDir, 'team.json'), {
|
|
48
|
+
id: input.teamId,
|
|
49
|
+
name: input.teamName ?? input.teamId,
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
writeJson(join(agentDir, 'agent.json'), {
|
|
53
|
+
id: input.agentId,
|
|
54
|
+
name: input.agentName ?? input.agentId,
|
|
55
|
+
projectDir: input.projectDir ?? null,
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
writeFileSync(join(runDir, 'prompt.md'), input.prompt);
|
|
59
|
+
writeJson(join(runDir, 'manifest.json'), {
|
|
60
|
+
teamId: input.teamId,
|
|
61
|
+
agentId: input.agentId,
|
|
62
|
+
runId: input.runId,
|
|
63
|
+
status: 'running',
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
files: [],
|
|
66
|
+
});
|
|
67
|
+
return { teamId: input.teamId, agentId: input.agentId, runId: input.runId, agentDir, runDir, outputDir, intermediateDir, logDir };
|
|
68
|
+
}
|
|
69
|
+
export function workspaceEnv(run) {
|
|
70
|
+
return {
|
|
71
|
+
AGENTBEAN_TEAM_ID: run.teamId,
|
|
72
|
+
AGENTBEAN_AGENT_ID: run.agentId,
|
|
73
|
+
AGENTBEAN_RUN_ID: run.runId,
|
|
74
|
+
AGENTBEAN_WORKSPACE: run.agentDir,
|
|
75
|
+
AGENTBEAN_OUTPUT_DIR: run.outputDir,
|
|
76
|
+
AGENTBEAN_INTERMEDIATE_DIR: run.intermediateDir,
|
|
77
|
+
AGENT_BEAN_OUTPUT_DIRS: [run.outputDir, run.intermediateDir].join(','),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function archiveOutputFiles(run, files) {
|
|
81
|
+
const archived = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const abs = isAbsolute(file) ? file : resolve(file);
|
|
85
|
+
if (seen.has(abs))
|
|
86
|
+
continue;
|
|
87
|
+
seen.add(abs);
|
|
88
|
+
let st;
|
|
89
|
+
try {
|
|
90
|
+
st = statSync(abs);
|
|
91
|
+
if (!st.isFile())
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const alreadyInRun = relative(run.runDir, abs);
|
|
98
|
+
const archivedPath = alreadyInRun && !alreadyInRun.startsWith('..') && !isAbsolute(alreadyInRun)
|
|
99
|
+
? abs
|
|
100
|
+
: uniqueDestination(run.outputDir, basename(abs));
|
|
101
|
+
if (archivedPath !== abs)
|
|
102
|
+
copyFileSync(abs, archivedPath);
|
|
103
|
+
const sizeBytes = statSync(archivedPath).size;
|
|
104
|
+
archived.push({
|
|
105
|
+
originalPath: abs,
|
|
106
|
+
archivedPath,
|
|
107
|
+
relativePath: relative(run.agentDir, archivedPath),
|
|
108
|
+
pathKind: 'output',
|
|
109
|
+
sha256: fileHash(archivedPath),
|
|
110
|
+
sizeBytes,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return archived;
|
|
114
|
+
}
|
|
115
|
+
export function finishAgentWorkspaceRun(run, input) {
|
|
116
|
+
if (input.replyText !== undefined) {
|
|
117
|
+
writeFileSync(join(run.runDir, 'response.md'), input.replyText);
|
|
118
|
+
}
|
|
119
|
+
writeJson(join(run.runDir, 'manifest.json'), {
|
|
120
|
+
teamId: run.teamId,
|
|
121
|
+
agentId: run.agentId,
|
|
122
|
+
runId: run.runId,
|
|
123
|
+
status: input.status,
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
error: input.error,
|
|
126
|
+
files: input.files.map((file) => ({
|
|
127
|
+
path: file.relativePath,
|
|
128
|
+
sha256: file.sha256,
|
|
129
|
+
sizeBytes: file.sizeBytes,
|
|
130
|
+
kind: file.pathKind,
|
|
131
|
+
originalPath: file.originalPath,
|
|
132
|
+
})),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, normalize } from 'node:path';
|
|
4
|
+
import { logger } from './log.js';
|
|
5
|
+
import { getAgentWorkspaceDir } from './workspace-manager.js';
|
|
6
|
+
function hashFile(path) {
|
|
7
|
+
try {
|
|
8
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function safeRelativePath(value) {
|
|
15
|
+
const normalized = normalize(value).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
16
|
+
if (!normalized || isAbsolute(normalized) || normalized.startsWith('..'))
|
|
17
|
+
return null;
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
export async function syncWorkspaceArtifacts(input) {
|
|
21
|
+
const base = input.serverUrl.replace(/\/agent$/, '');
|
|
22
|
+
let payload;
|
|
23
|
+
try {
|
|
24
|
+
const resp = await fetch(`${base}/api/networks/${encodeURIComponent(input.networkId)}/workspace`, {
|
|
25
|
+
headers: { Authorization: `Bearer ${input.token}` },
|
|
26
|
+
});
|
|
27
|
+
if (!resp.ok) {
|
|
28
|
+
logger.warn({ status: resp.status, body: await resp.text() }, 'workspace sync manifest rejected');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
payload = await resp.json();
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.warn({ err: err?.message }, 'workspace sync manifest failed');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!payload.ok || !payload.agents?.length)
|
|
38
|
+
return;
|
|
39
|
+
let downloaded = 0;
|
|
40
|
+
for (const agent of payload.agents) {
|
|
41
|
+
const agentDir = getAgentWorkspaceDir(input.networkId, agent.id);
|
|
42
|
+
for (const run of agent.runs) {
|
|
43
|
+
for (const file of run.files) {
|
|
44
|
+
const rel = safeRelativePath(file.relativePath);
|
|
45
|
+
if (!rel)
|
|
46
|
+
continue;
|
|
47
|
+
const dest = join(agentDir, rel);
|
|
48
|
+
if (file.sha256 && existsSync(dest) && hashFile(dest) === file.sha256)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
const resp = await fetch(`${base}${file.downloadUrl}`, {
|
|
52
|
+
headers: { Authorization: `Bearer ${input.token}` },
|
|
53
|
+
});
|
|
54
|
+
if (!resp.ok)
|
|
55
|
+
continue;
|
|
56
|
+
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
57
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
58
|
+
writeFileSync(dest, bytes);
|
|
59
|
+
downloaded += 1;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.warn({ err: err?.message, fileId: file.id }, 'workspace artifact download failed');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (downloaded > 0)
|
|
68
|
+
logger.info({ downloaded }, 'workspace artifacts synced');
|
|
69
|
+
}
|