ai-agentmanager 1.11.1 → 1.12.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/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 (!hook && !['GET', 'HEAD'].includes(req.method) && req.headers[CLIENT_HEADER] !== '1') {
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,53 @@ 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 designed from a task): design, edit, test, run, download.
529
+ // Agents started here report to this daemon (runs, cost, approvals).
530
+ const builder = new Builder({ 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() }) });
531
+ api.post('/agentkit/report', wrap((req) => reports.report(req.body ?? {})));
532
+ api.get('/agentkit/agents', wrap(() => reports.list()));
533
+ 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')));
534
+ api.delete('/agentkit/agents/:key', wrap((req) => reports.forget(req.params.key)));
535
+ // What an agent elsewhere needs to report here (this computer only: it shows the token).
536
+ 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 })));
537
+ api.get('/builder', wrap(() => builder.list()));
538
+ api.post('/builder', wrap((req) => builder.design({ task: req.body?.task, engine: req.body?.engine || 'claude', attachments: req.body?.attachments?.length ? manager.stagedText(req.body.attachments) : [] })));
539
+ api.get('/builder/:name', wrap((req) => builder.view(req.params.name)));
540
+ api.put('/builder/:name', wrap((req) => builder.update(req.params.name, req.body ?? {})));
541
+ api.post('/builder/:name/test', wrap((req) => {
542
+ builder.test(req.params.name).catch(() => {}); // in the background; the view says when it's done
543
+ return new Promise((r) => setTimeout(() => r(builder.view(req.params.name)), 50));
544
+ }));
545
+ api.post('/builder/:name/package', wrap((req) => {
546
+ builder.package(req.params.name, req.body?.platform || undefined).catch(() => {}); // checks first, then builds in the background
547
+ return new Promise((r) => setTimeout(() => r(builder.view(req.params.name)), 50));
548
+ }));
549
+ api.get('/builder/:name/executable/:platform', (req, res) => {
550
+ let file;
551
+ try {
552
+ file = builder.executable(req.params.name, req.params.platform);
553
+ } catch (e) {
554
+ return res.status(e.status ?? 500).json({ error: e.message });
555
+ }
556
+ res.download(file, path.basename(file));
557
+ });
558
+ api.post('/builder/:name/start', wrap((req) => builder.start(req.params.name)));
559
+ api.post('/builder/:name/stop', wrap((req) => builder.stop(req.params.name)));
560
+ api.delete('/builder/:name', wrap((req) => builder.remove(req.params.name)));
561
+ // The agent's folder as a .tar.gz (its own files; not its runs, memory or log).
562
+ api.get('/builder/:name/download', (req, res) => {
563
+ let dir;
564
+ try {
565
+ dir = builder.view(req.params.name).dir;
566
+ } catch (e) {
567
+ return res.status(e.status ?? 500).json({ error: e.message });
568
+ }
569
+ res.setHeader('content-type', 'application/gzip');
570
+ res.setHeader('content-disposition', `attachment; filename="${req.params.name}.tar.gz"`);
571
+ const tar = spawn('tar', ['-czf', '-', '--exclude', '.agentkit', '--exclude', 'agent.log', '--exclude', 'build.json', '--exclude', 'work', '--exclude', 'dist', '-C', path.dirname(dir), path.basename(dir)]);
572
+ tar.stdout.pipe(res);
573
+ tar.on('error', () => res.end());
574
+ });
519
575
  // Isolation: Docker, the agent image, the Claude token for containers, and the default for new agents.
520
576
  let buildError = null;
521
577
  const isolationView = async () => ({ ...(await dockerStatus(config)), building: isBuilding(), buildError, hasClaudeToken: Boolean(claudeToken()), engines: [...CONTAINER_ENGINES], default: config.isolation?.default ?? null });
@@ -751,7 +807,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
751
807
  const eventSinks = new Set();
752
808
  api.get('/events/stream', (req, res) => {
753
809
  const send = sse(req, res);
754
- send({ t: 'snapshot', agents: manager.list(), backlog: backlog.view(), projects: manager.listProjects(), races: races.list(), teams: teams.list() });
810
+ send({ t: 'snapshot', agents: manager.list(), backlog: backlog.view(), projects: manager.listProjects(), races: races.list(), teams: teams.list(), built: reports.list() });
755
811
  eventSinks.add(send);
756
812
  req.on('close', () => eventSinks.delete(send));
757
813
  });
@@ -844,6 +900,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
844
900
  for (const send of eventSinks) send(msg);
845
901
  };
846
902
  manager.on('change', (agent) => broadcast({ t: 'agent', agent }));
903
+ reports.on('change', () => broadcast({ t: 'built', reports: reports.list() }));
847
904
  manager.on('removed', (id) => {
848
905
  broadcast({ t: 'removed', id });
849
906
  live.closeAgent(id).catch(() => {});
@@ -880,7 +937,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
880
937
  const url = new URL(req.url, 'http://x');
881
938
  if (url.pathname === '/ws/events') {
882
939
  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() }));
940
+ 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
941
  ws.on('close', () => eventClients.delete(ws));
885
942
  return;
886
943
  }
@@ -967,6 +1024,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
967
1024
  updates.close();
968
1025
  assistant.close();
969
1026
  console.log(`[agentmgr] shutting down (${reason}); closing agents so their sessions are saved`);
1027
+ builder.stopAll();
970
1028
  await manager.stopAll({ final: true });
971
1029
  await manager.flush().catch((err) => console.error(`[agentmgr] final flush failed: ${err.message}`));
972
1030
  try {