ai-agentmanager 1.11.1 → 1.13.0
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/CHANGELOG.md +20 -0
- package/bin/agentkit.js +10 -0
- package/docs/agentkit.md +175 -0
- package/package.json +7 -3
- package/public/app.js +44 -6
- package/public/built.js +332 -0
- package/public/index.html +22 -0
- package/public/style.css +30 -0
- package/src/agentkit/agent.js +171 -0
- package/src/agentkit/approvals.js +97 -0
- package/src/agentkit/cli.js +153 -0
- package/src/agentkit/engines.js +128 -0
- package/src/agentkit/http.js +131 -0
- package/src/agentkit/package.js +114 -0
- package/src/agentkit/report.js +106 -0
- package/src/agentkit/sea-main.js +25 -0
- package/src/agentkit/service.js +118 -0
- package/src/agentkit/spec.js +109 -0
- package/src/agentkit/stdio.js +95 -0
- package/src/agentkit/triggers.js +60 -0
- package/src/agentkit-reports.js +117 -0
- package/src/builder.js +436 -0
- package/src/manager.js +25 -2
- package/src/server.js +74 -5
package/src/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
8
9
|
import express from 'express';
|
|
9
10
|
import { WebSocketServer } from 'ws';
|
|
10
11
|
import { ensureHome, HOST, loadConfig, paths, PORT, saveConfig } from './config.js';
|
|
@@ -35,6 +36,8 @@ import * as remote from './remote.js';
|
|
|
35
36
|
import * as service from './service.js';
|
|
36
37
|
import { jiraView, saveJira } from './jira.js';
|
|
37
38
|
import { CONTAINER_ENGINES, buildImage, claudeToken, dockerStatus, isBuilding, saveSecrets } from './container.js';
|
|
39
|
+
import { AgentkitReports } from './agentkit-reports.js';
|
|
40
|
+
import { Builder } from './builder.js';
|
|
38
41
|
import { Store } from './store.js';
|
|
39
42
|
import { about, latestRelease } from './about.js';
|
|
40
43
|
|
|
@@ -64,8 +67,9 @@ function isAllowedOrigin(origin, port) {
|
|
|
64
67
|
const PUBLIC_REMOTE_API = new Set(['POST /api/login', 'GET /api/session']);
|
|
65
68
|
// Updates are not here: a signed-in device (a phone) can install the newest release too. It only
|
|
66
69
|
// ever installs an official release from where this copy came from (git tag or npm) and restarts.
|
|
67
|
-
const LOCAL_ONLY_API = /^\/api\/(remote(\/|$)|notify(\/|$)|hub(\/|$)|triggers(\/|$)|service$|jira$|isolation\/|editors$|agents\/[\w-]+\/open-in$|shutdown$)/;
|
|
70
|
+
const LOCAL_ONLY_API = /^\/api\/(remote(\/|$)|notify(\/|$)|hub(\/|$)|triggers(\/|$)|service$|jira$|isolation\/|editors$|agentkit\/connect$|agents\/[\w-]+\/open-in$|shutdown$)/;
|
|
68
71
|
const HOOK_PATH = '/api/hooks/agents';
|
|
72
|
+
const REPORT_PATH = '/api/agentkit/report';
|
|
69
73
|
const NO_REPO_KINDS = new Set(['general', 'assistant']);
|
|
70
74
|
|
|
71
75
|
export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
@@ -162,6 +166,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
162
166
|
return req.device ? null : 'Sign in required';
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
const reports = new AgentkitReports();
|
|
165
170
|
const app = express();
|
|
166
171
|
app.disable('x-powered-by');
|
|
167
172
|
app.use((req, res, next) => {
|
|
@@ -171,10 +176,14 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
171
176
|
// The webhook signs in with its token instead of a device session or the client header.
|
|
172
177
|
const bearer = /^Bearer (.+)$/i.exec(req.headers.authorization ?? '')?.[1];
|
|
173
178
|
const hook = req.method === 'POST' && req.url.split('?')[0] === HOOK_PATH && tokenMatches(req.headers['x-agentmgr-token'] ?? bearer, config.triggers?.webhook?.enabled ? config.triggers.webhook.token : null);
|
|
179
|
+
// So do agents built with agentkit reporting in (from this computer or another one).
|
|
180
|
+
const reportPath = req.method === 'POST' && req.url.split('?')[0] === REPORT_PATH;
|
|
181
|
+
const reporting = reportPath && tokenMatches(bearer, reports.token);
|
|
174
182
|
const why = denied(req);
|
|
175
|
-
if (why && !(hook && why === 'Sign in required')) return res.status(why === 'Sign in required' ? 401 : 403).json({ error: why });
|
|
183
|
+
if (why && !((hook || reporting) && why === 'Sign in required')) return res.status(why === 'Sign in required' ? 401 : 403).json({ error: why });
|
|
176
184
|
if (req.url.split('?')[0] === HOOK_PATH && !hook) return res.status(401).json({ error: 'Bad or missing token' });
|
|
177
|
-
if (
|
|
185
|
+
if (reportPath && !reporting) return res.status(401).json({ error: 'Bad or missing report token' });
|
|
186
|
+
if (!hook && !reporting && !['GET', 'HEAD'].includes(req.method) && req.headers[CLIENT_HEADER] !== '1') {
|
|
178
187
|
return res.status(403).json({ error: `Missing ${CLIENT_HEADER} header` });
|
|
179
188
|
}
|
|
180
189
|
next();
|
|
@@ -516,6 +525,64 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
516
525
|
api.get('/agents/:id/memory', wrap((req) => memory.read(req.params.id)));
|
|
517
526
|
api.put('/agents/:id/memory', wrap((req) => memory.write(req.params.id, req.body?.text)));
|
|
518
527
|
api.post('/agents/:id/learn', wrap((req) => memory.learn(req.params.id, req.body?.text, { by: req.headers['x-agentmgr-agent'] ? undefined : 'you' })));
|
|
528
|
+
// The agent builder (agentkit agents built from a task, each developed in an agent session
|
|
529
|
+
// working in its folder): build, change, test, run, package, download. Agents started here
|
|
530
|
+
// report to this daemon (runs, cost, approvals).
|
|
531
|
+
const sessions = {
|
|
532
|
+
create: (opts) => manager.create({ ...opts, worktree: false }),
|
|
533
|
+
get: (id) => {
|
|
534
|
+
const a = manager.get(id);
|
|
535
|
+
return a ? manager.view(a) : null;
|
|
536
|
+
},
|
|
537
|
+
tell: (id, text) => manager.tell(id, text),
|
|
538
|
+
stop: (id) => manager.stop(id).catch(() => {}),
|
|
539
|
+
};
|
|
540
|
+
const builder = new Builder({ sessions, oneShot: (prompt) => manager.oneShot(null, prompt, { ...manager.writerFor({}), timeoutMs: 300_000 }), reportEnv: () => ({ AGENTKIT_REPORT_URL: `http://127.0.0.1:${port}`, AGENTKIT_REPORT_TOKEN: reports.reportToken() }) });
|
|
541
|
+
api.post('/agentkit/report', wrap((req) => reports.report(req.body ?? {})));
|
|
542
|
+
api.get('/agentkit/agents', wrap(() => reports.list()));
|
|
543
|
+
api.post('/agentkit/agents/:key/approvals/:id', wrap((req) => reports.decide(req.params.key, req.params.id, req.body?.allow === true, req.device ? `${req.device.name ?? 'a signed-in device'}` : 'you')));
|
|
544
|
+
api.delete('/agentkit/agents/:key', wrap((req) => reports.forget(req.params.key)));
|
|
545
|
+
// What an agent elsewhere needs to report here (this computer only: it shows the token).
|
|
546
|
+
api.get('/agentkit/connect', wrap(() => ({ token: reports.reportToken(), local: `http://127.0.0.1:${port}`, remote: remoteServer ? remote.remoteUrls(remotePort).map((u) => u.url) : [], cert: remoteServer ? remote.certificateFingerprint() : null })));
|
|
547
|
+
api.get('/builder', wrap(() => builder.list()));
|
|
548
|
+
api.post('/builder', wrap((req) => builder.design({ task: req.body?.task, engine: req.body?.engine || 'claude', name: req.body?.name || undefined, attachments: req.body?.attachments, permissionMode: req.body?.permissionMode || undefined, model: req.body?.model || undefined })));
|
|
549
|
+
api.post('/builder/:name/change', wrap((req) => builder.change(req.params.name, req.body?.text)));
|
|
550
|
+
api.get('/builder/:name', wrap((req) => builder.view(req.params.name)));
|
|
551
|
+
api.put('/builder/:name', wrap((req) => builder.update(req.params.name, req.body ?? {})));
|
|
552
|
+
api.post('/builder/:name/test', wrap((req) => {
|
|
553
|
+
builder.test(req.params.name).catch(() => {}); // in the background; the view says when it's done
|
|
554
|
+
return new Promise((r) => setTimeout(() => r(builder.view(req.params.name)), 50));
|
|
555
|
+
}));
|
|
556
|
+
api.post('/builder/:name/package', wrap((req) => {
|
|
557
|
+
builder.package(req.params.name, req.body?.platform || undefined).catch(() => {}); // checks first, then builds in the background
|
|
558
|
+
return new Promise((r) => setTimeout(() => r(builder.view(req.params.name)), 50));
|
|
559
|
+
}));
|
|
560
|
+
api.get('/builder/:name/executable/:platform', (req, res) => {
|
|
561
|
+
let file;
|
|
562
|
+
try {
|
|
563
|
+
file = builder.executable(req.params.name, req.params.platform);
|
|
564
|
+
} catch (e) {
|
|
565
|
+
return res.status(e.status ?? 500).json({ error: e.message });
|
|
566
|
+
}
|
|
567
|
+
res.download(file, path.basename(file));
|
|
568
|
+
});
|
|
569
|
+
api.post('/builder/:name/start', wrap((req) => builder.start(req.params.name)));
|
|
570
|
+
api.post('/builder/:name/stop', wrap((req) => builder.stop(req.params.name)));
|
|
571
|
+
api.delete('/builder/:name', wrap((req) => builder.remove(req.params.name)));
|
|
572
|
+
// The agent's folder as a .tar.gz (its own files; not its runs, memory or log).
|
|
573
|
+
api.get('/builder/:name/download', (req, res) => {
|
|
574
|
+
let dir;
|
|
575
|
+
try {
|
|
576
|
+
dir = builder.view(req.params.name).dir;
|
|
577
|
+
} catch (e) {
|
|
578
|
+
return res.status(e.status ?? 500).json({ error: e.message });
|
|
579
|
+
}
|
|
580
|
+
res.setHeader('content-type', 'application/gzip');
|
|
581
|
+
res.setHeader('content-disposition', `attachment; filename="${req.params.name}.tar.gz"`);
|
|
582
|
+
const tar = spawn('tar', ['-czf', '-', '--exclude', '.agentkit', '--exclude', 'agent.log', '--exclude', 'build.json', '--exclude', 'work', '--exclude', 'dist', '-C', path.dirname(dir), path.basename(dir)]);
|
|
583
|
+
tar.stdout.pipe(res);
|
|
584
|
+
tar.on('error', () => res.end());
|
|
585
|
+
});
|
|
519
586
|
// Isolation: Docker, the agent image, the Claude token for containers, and the default for new agents.
|
|
520
587
|
let buildError = null;
|
|
521
588
|
const isolationView = async () => ({ ...(await dockerStatus(config)), building: isBuilding(), buildError, hasClaudeToken: Boolean(claudeToken()), engines: [...CONTAINER_ENGINES], default: config.isolation?.default ?? null });
|
|
@@ -751,7 +818,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
751
818
|
const eventSinks = new Set();
|
|
752
819
|
api.get('/events/stream', (req, res) => {
|
|
753
820
|
const send = sse(req, res);
|
|
754
|
-
send({ t: 'snapshot', agents: manager.list(), backlog: backlog.view(), projects: manager.listProjects(), races: races.list(), teams: teams.list() });
|
|
821
|
+
send({ t: 'snapshot', agents: manager.list(), backlog: backlog.view(), projects: manager.listProjects(), races: races.list(), teams: teams.list(), built: reports.list() });
|
|
755
822
|
eventSinks.add(send);
|
|
756
823
|
req.on('close', () => eventSinks.delete(send));
|
|
757
824
|
});
|
|
@@ -844,6 +911,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
844
911
|
for (const send of eventSinks) send(msg);
|
|
845
912
|
};
|
|
846
913
|
manager.on('change', (agent) => broadcast({ t: 'agent', agent }));
|
|
914
|
+
reports.on('change', () => broadcast({ t: 'built', reports: reports.list() }));
|
|
847
915
|
manager.on('removed', (id) => {
|
|
848
916
|
broadcast({ t: 'removed', id });
|
|
849
917
|
live.closeAgent(id).catch(() => {});
|
|
@@ -880,7 +948,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
880
948
|
const url = new URL(req.url, 'http://x');
|
|
881
949
|
if (url.pathname === '/ws/events') {
|
|
882
950
|
eventClients.add(ws);
|
|
883
|
-
ws.send(JSON.stringify({ t: 'snapshot', agents: manager.list(), backlog: backlog.view(), projects: manager.listProjects(), races: races.list(), teams: teams.list() }));
|
|
951
|
+
ws.send(JSON.stringify({ t: 'snapshot', agents: manager.list(), backlog: backlog.view(), projects: manager.listProjects(), races: races.list(), teams: teams.list(), built: reports.list() }));
|
|
884
952
|
ws.on('close', () => eventClients.delete(ws));
|
|
885
953
|
return;
|
|
886
954
|
}
|
|
@@ -967,6 +1035,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
967
1035
|
updates.close();
|
|
968
1036
|
assistant.close();
|
|
969
1037
|
console.log(`[agentmgr] shutting down (${reason}); closing agents so their sessions are saved`);
|
|
1038
|
+
builder.stopAll();
|
|
970
1039
|
await manager.stopAll({ final: true });
|
|
971
1040
|
await manager.flush().catch((err) => console.error(`[agentmgr] final flush failed: ${err.message}`));
|
|
972
1041
|
try {
|