ai-agentmanager 1.11.0 → 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.
@@ -0,0 +1,115 @@
1
+ // One headless turn of an engine's CLI: a prompt in, the answer (and session, cost) out. The agent
2
+ // keeps one engine session per conversation, so a follow-up continues it. Tools come from the
3
+ // agent's MCP servers; the allow list says what runs without asking. "approve" tools ask a person
4
+ // through the approver (Claude), or are kept out of reach (engines with no way to ask).
5
+ import { spawn } from 'node:child_process';
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { createRequire } from 'node:module';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ import { allowedOutright } from './approvals.js';
13
+
14
+ // Inside a packaged agent? (node:sea only exists from Node 20.12; the daemon supports older 20s.)
15
+ const isSea = () => {
16
+ try {
17
+ return createRequire(import.meta.url)('node:sea').isSea();
18
+ } catch {
19
+ return false;
20
+ }
21
+ };
22
+ const AGENTKIT_BIN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'agentkit.js');
23
+ const ACP_CLIENT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'acp-client.js');
24
+ // Overrides for tests and odd installs: AGENTKIT_CLAUDE_BIN, AGENTKIT_CODEX_BIN, AGENTKIT_MUSE_BIN
25
+ // (a command, optionally with arguments: "node /path/fake.js").
26
+ const binOf = (name, env) => (env[`AGENTKIT_${name.toUpperCase()}_BIN`] ?? name).split(' ');
27
+
28
+ function run(cmd, args, { input, cwd, env, timeoutMs }) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(cmd, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
31
+ let out = '';
32
+ let err = '';
33
+ const timer = setTimeout(() => child.kill('SIGTERM'), timeoutMs);
34
+ child.stdout.on('data', (d) => (out += d));
35
+ child.stderr.on('data', (d) => (err += d));
36
+ child.on('error', (e) => (clearTimeout(timer), reject(Object.assign(e, { message: `Could not start ${cmd}: ${e.message}` }))));
37
+ child.on('close', (code) => {
38
+ clearTimeout(timer);
39
+ if (code === 0) resolve(out);
40
+ else reject(new Error(`${path.basename(cmd)} exited with ${code}: ${(err || out).trim().slice(-400)}`));
41
+ });
42
+ child.stdin.end(input ?? '');
43
+ });
44
+ }
45
+
46
+ /** The MCP config file the engine loads (Claude's format: { mcpServers: {...} }), plus `extra` servers. */
47
+ export function writeMcpConfig(agent, file, extra = {}) {
48
+ const servers = { ...agent.tools, ...extra };
49
+ if (!Object.keys(servers).length) return null;
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ fs.writeFileSync(file, JSON.stringify({ mcpServers: servers }, null, 2), { mode: 0o600 });
52
+ return file;
53
+ }
54
+
55
+ /** The approver as an MCP server: this program (`<agent> approver` when packaged) asking `url`. */
56
+ export const APPROVER = 'agentkit_approval';
57
+ export function approverServer({ url, token }) {
58
+ return { command: process.execPath, args: isSea() ? ['approver'] : [AGENTKIT_BIN, 'approver'], env: { AGENTKIT_APPROVAL_URL: url, AGENTKIT_APPROVAL_TOKEN: token } };
59
+ }
60
+
61
+ /**
62
+ * One turn. `session`: the engine session to continue (null starts one); returns
63
+ * { text, session, costUsd }. `system` goes in as an extra system prompt where the engine has one,
64
+ * else ahead of the prompt.
65
+ */
66
+ export async function turn(agent, { prompt, system = '', session = null, mcpFile = null, approver = false, env = process.env, timeoutMs = 15 * 60_000 }) {
67
+ fs.mkdirSync(agent.workdir, { recursive: true });
68
+ const cwd = agent.workdir;
69
+ if (agent.engine === 'claude') {
70
+ const [cmd, ...pre] = binOf('claude', env);
71
+ const sid = session ?? crypto.randomUUID();
72
+ const args = [...pre, '-p', '--output-format', 'json', session ? '--resume' : '--session-id', sid];
73
+ if (agent.model) args.push('--model', agent.model);
74
+ if (system) args.push('--append-system-prompt', system);
75
+ // Self-contained: the agent's own MCP servers only (none when it has none), and none of the
76
+ // person's own Claude settings (their MCP servers, hooks, permissions) leak in.
77
+ args.push('--setting-sources', 'project', '--strict-mcp-config');
78
+ if (mcpFile) args.push('--mcp-config', mcpFile);
79
+ // The approve list wins: an allow pattern that also covers an approval tool goes to the approver.
80
+ const allow = approver ? allowedOutright(agent) : agent.allow;
81
+ if (allow.length) args.push('--allowedTools', ...allow);
82
+ // With an approver, Claude asks it before anything not allowed outright (it says no to tools
83
+ // off the approve list); without one, approval tools are simply not available.
84
+ if (approver) args.push('--permission-prompt-tool', `mcp__${APPROVER}__decide`);
85
+ else if (agent.approve.length) args.push('--disallowedTools', ...agent.approve);
86
+ const out = await run(cmd, args, { input: prompt, cwd, env, timeoutMs });
87
+ try {
88
+ const j = JSON.parse(out);
89
+ if (j.is_error) throw new Error(String(j.result ?? 'the engine reported an error'));
90
+ return { text: String(j.result ?? ''), session: j.session_id ?? sid, costUsd: j.total_cost_usd ?? null };
91
+ } catch (e) {
92
+ if (e instanceof SyntaxError) return { text: out.trim(), session: sid, costUsd: null };
93
+ throw e;
94
+ }
95
+ }
96
+ if (agent.engine === 'codex') {
97
+ const [cmd, ...pre] = binOf('codex', env);
98
+ const base = ['exec', '--skip-git-repo-check', '--sandbox', 'workspace-write', ...(agent.model ? ['--model', agent.model] : [])];
99
+ const args = [...pre, ...(session ? [...base, 'resume', session] : base), '-'];
100
+ const out = await run(cmd, args, { input: system ? `${system}\n\n---\n\n${prompt}` : prompt, cwd, env, timeoutMs });
101
+ // Codex prints its session id in a header line ("session id: …").
102
+ const sid = session ?? /session id:\s*([0-9a-f-]{36})/i.exec(out)?.[1] ?? null;
103
+ return { text: out.trim().split(/\n-{3,}\n|\ntokens used/i)[0].trim(), session: sid, costUsd: null };
104
+ }
105
+ if (agent.engine === 'muse') {
106
+ const [cmd, ...pre] = binOf('muse', env);
107
+ const args = [...pre, 'exec', ...(session ? ['--session-id', session] : []), ...(agent.model ? ['--model', agent.model] : []), '--', system ? `${system}\n\n---\n\n${prompt}` : prompt];
108
+ return { text: (await run(cmd, args, { cwd, env, timeoutMs })).trim(), session, costUsd: null };
109
+ }
110
+ // acp:<name>: our ACP client in one-shot mode, with the agent's command from AGENTKIT_ACP_<NAME>.
111
+ const name = agent.engine.slice(4);
112
+ const command = (env[`AGENTKIT_ACP_${name.toUpperCase().replace(/-/g, '_')}`] ?? name).split(' ');
113
+ const out = await run(process.execPath, [ACP_CLIENT, '--once', '--', ...command], { input: system ? `${system}\n\n---\n\n${prompt}` : prompt, cwd, env, timeoutMs });
114
+ return { text: out.trim(), session: null, costUsd: null };
115
+ }
@@ -0,0 +1,131 @@
1
+ // The agent over HTTP: a chat page for people, a small API for apps, webhooks for triggers, and
2
+ // A2A (the agent-to-agent protocol: an agent card, and tasks over JSON-RPC) for other agents.
3
+ import crypto from 'node:crypto';
4
+ import http from 'node:http';
5
+ import express from 'express';
6
+
7
+ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
8
+
9
+ /** The A2A agent card: who the agent is and how to reach it. */
10
+ export function agentCard(agent, base) {
11
+ const s = agent.spec;
12
+ return {
13
+ protocolVersion: '0.3.0',
14
+ name: s.title,
15
+ description: s.description || s.goal,
16
+ url: `${base}/a2a`,
17
+ preferredTransport: 'JSONRPC',
18
+ version: '1.0.0',
19
+ capabilities: { streaming: false, pushNotifications: false },
20
+ defaultInputModes: ['text/plain'],
21
+ defaultOutputModes: ['text/plain'],
22
+ skills: [{ id: s.name, name: s.title, description: s.goal, tags: [s.engine] }],
23
+ };
24
+ }
25
+
26
+ const taskOf = (t) => ({
27
+ kind: 'task',
28
+ id: t.id,
29
+ contextId: t.contextId,
30
+ status: { state: t.state, timestamp: t.at, ...(t.result != null ? { message: { kind: 'message', role: 'agent', messageId: `${t.id}-r`, parts: [{ kind: 'text', text: t.result }] } } : {}) },
31
+ ...(t.result != null && t.state === 'completed' ? { artifacts: [{ artifactId: `${t.id}-a`, parts: [{ kind: 'text', text: t.result }] }] } : {}),
32
+ });
33
+
34
+ const PAGE = (agent) => `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
35
+ <title>${esc(agent.spec.title)}</title><style>
36
+ :root{--bg:#f6f7f9;--panel:#fff;--text:#1c2330;--muted:#667085;--border:#dde1e7;--accent:#3b5bdb}
37
+ @media (prefers-color-scheme:dark){:root{--bg:#0d0f14;--panel:#151821;--text:#e5e8ef;--muted:#8b93a7;--border:#2a2f3c;--accent:#6d8bff}}
38
+ body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column;height:100vh}
39
+ header{padding:12px 16px;border-bottom:1px solid var(--border);background:var(--panel)}header b{font-size:16px}header p{margin:2px 0 0;color:var(--muted);font-size:13px}
40
+ #log{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px}
41
+ .m{max-width:760px;padding:10px 12px;border-radius:10px;white-space:pre-wrap;word-break:break-word}.you{align-self:flex-end;background:var(--accent);color:#fff}.agent{background:var(--panel);border:1px solid var(--border)}
42
+ #ap{display:flex;flex-direction:column;gap:8px;padding:0 16px}#ap:not(:empty){padding:12px 16px;border-bottom:1px solid var(--border)}.ap{padding:10px 12px;border-radius:10px;border:1px solid #e8a33d;background:var(--panel)}.ap pre{margin:6px 0;font-size:12px;white-space:pre-wrap;max-height:160px;overflow:auto}.ap .no{background:transparent;color:var(--text);border:1px solid var(--border)}
43
+ .step{font-size:12px;color:var(--muted)}form{display:flex;gap:8px;padding:12px 16px;border-top:1px solid var(--border);background:var(--panel)}
44
+ textarea{flex:1;resize:none;font:inherit;padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--bg);color:inherit}button{font:inherit;padding:8px 14px;border-radius:8px;border:0;background:var(--accent);color:#fff}
45
+ </style></head><body><header><b>${esc(agent.spec.title)}</b><p>${esc(agent.spec.description || agent.spec.goal)}</p></header>
46
+ <div id="ap"></div><div id="log"></div><form id="f"><textarea id="t" rows="2" placeholder="Ask or tell the agent…"></textarea><button>Send</button></form>
47
+ <script>
48
+ const log=document.getElementById('log');const conv=localStorage.getItem('conv')||crypto.randomUUID();localStorage.setItem('conv',conv);
49
+ const add=(cls,text)=>{const d=document.createElement('div');d.className='m '+cls;d.textContent=text;log.append(d);log.scrollTop=log.scrollHeight;return d};
50
+ document.getElementById('f').onsubmit=async(e)=>{e.preventDefault();const t=document.getElementById('t');const text=t.value.trim();if(!text)return;t.value='';add('you',text);
51
+ const wait=add('agent step','Working on it…');const r=await fetch('api/chat',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({message:text,conversation:conv})}).then(r=>r.json()).catch(e=>({error:e.message}));
52
+ wait.remove();add('agent',r.result??('Error: '+r.error))};
53
+ const ap=document.getElementById('ap');const esc=(s)=>String(s).replace(/[&<>"]/g,(c)=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]);
54
+ async function approvals(){const l=await fetch('api/approvals').then(r=>r.json()).catch(()=>[]);ap.innerHTML=l.map(a=>'<div class="ap"><b>Approve '+esc(a.tool)+'?</b> <span class="step">asked '+new Date(a.at).toLocaleTimeString()+'</span><pre>'+esc(JSON.stringify(a.input,null,2))+'</pre><button data-id="'+esc(a.id)+'" data-allow="1">Approve</button> <button class="no" data-id="'+esc(a.id)+'">Deny</button></div>').join('')}
55
+ ap.onclick=async(e)=>{const b=e.target.closest('button[data-id]');if(!b)return;b.disabled=true;await fetch('api/approvals/'+b.dataset.id,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({allow:!!b.dataset.allow})});approvals()};
56
+ approvals();setInterval(approvals,2000);
57
+ document.getElementById('t').onkeydown=(e)=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();document.getElementById('f').requestSubmit()}};
58
+ </script></body></html>`;
59
+
60
+ /** Start the HTTP side; resolves with { server, url, close }. */
61
+ export function startHttp(agent, { port, host = '127.0.0.1', log = () => {} } = {}) {
62
+ const app = express();
63
+ app.use(express.json({ limit: '2mb' }));
64
+ const tasks = new Map(); // A2A task id -> { id, contextId, state, result, at }
65
+ const wrap = (fn) => (req, res) => Promise.resolve(fn(req, res)).then((v) => v !== undefined && res.json(v)).catch((err) => res.status(err.status ?? 500).json({ error: err.message }));
66
+
67
+ app.get('/', (req, res) => res.type('html').send(PAGE(agent)));
68
+ app.get('/api/health', (req, res) => res.json({ ok: true, agent: agent.spec.name, engine: agent.spec.engine, running: agent.current, triggers: agent.spec.triggers.map((t) => t.label) }));
69
+ app.post('/api/chat', wrap(async (req) => {
70
+ const message = String(req.body?.message ?? '').trim();
71
+ if (!message) throw Object.assign(new Error('message is required'), { status: 400 });
72
+ return agent.run(message, { source: 'chat', conversation: String(req.body?.conversation ?? '').slice(0, 80) || crypto.randomUUID() });
73
+ }));
74
+ // Tools waiting for a person's approval, and their answers (JSON only: a page elsewhere can't post here).
75
+ app.get('/api/approvals', wrap(() => agent.approvals.pending()));
76
+ app.post('/api/approvals/:id', wrap((req) => {
77
+ if (!req.is('application/json') || typeof req.body?.allow !== 'boolean') throw Object.assign(new Error('Send {"allow": true|false}'), { status: 400 });
78
+ if (!agent.approvals.decide(req.params.id, req.body.allow, { by: 'the chat page', note: String(req.body.note ?? '') })) throw Object.assign(new Error('Already decided'), { status: 409 });
79
+ return { ok: true };
80
+ }));
81
+ agent.approvals.channels.add('chat');
82
+ app.post('/api/runs', wrap((req) => agent.run(String(req.body?.input ?? ''), { source: 'api' })));
83
+ app.get('/api/runs', wrap(() => agent.runs()));
84
+ app.get('/api/runs/:id', wrap((req) => agent.runLog(req.params.id) ?? Promise.reject(Object.assign(new Error('No such run'), { status: 404 }))));
85
+
86
+ // Webhook triggers: the body (JSON or text) is the run's input.
87
+ for (const t of agent.spec.triggers.filter((x) => x.kind === 'webhook')) {
88
+ app.post(t.path, express.text({ type: () => true, limit: '2mb' }), wrap(async (req) => {
89
+ const run = agent.run(typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {}), { source: t.label });
90
+ run.catch(() => {});
91
+ return { accepted: true };
92
+ }));
93
+ log(`trigger: ${t.label}`);
94
+ }
95
+
96
+ // A2A.
97
+ if (agent.spec.interfaces.a2a) {
98
+ app.get('/.well-known/agent.json', (req, res) => res.json(agentCard(agent, `http://${req.headers.host}`)));
99
+ app.get('/.well-known/agent-card.json', (req, res) => res.json(agentCard(agent, `http://${req.headers.host}`)));
100
+ app.post('/a2a', async (req, res) => {
101
+ const { id, method, params = {} } = req.body ?? {};
102
+ const reply = (result) => res.json({ jsonrpc: '2.0', id, result });
103
+ const fail = (code, message) => res.json({ jsonrpc: '2.0', id, error: { code, message } });
104
+ if (method === 'message/send') {
105
+ const msg = params.message ?? {};
106
+ const text = (msg.parts ?? []).filter((p) => p.kind === 'text' || p.type === 'text').map((p) => p.text).join('\n').trim();
107
+ if (!text) return fail(-32602, 'message.parts needs a text part');
108
+ const t = { id: crypto.randomUUID(), contextId: msg.contextId ?? crypto.randomUUID(), state: 'working', result: null, at: new Date().toISOString() };
109
+ tasks.set(t.id, t);
110
+ const r = await agent.run(text, { source: 'a2a', conversation: `a2a:${t.contextId}` });
111
+ Object.assign(t, { state: r.status === 'failed' ? 'failed' : 'completed', result: r.result, at: new Date().toISOString() });
112
+ return reply(taskOf(t));
113
+ }
114
+ if (method === 'tasks/get') {
115
+ const t = tasks.get(params.id);
116
+ return t ? reply(taskOf(t)) : fail(-32001, 'Task not found');
117
+ }
118
+ return fail(-32601, `Method not found: ${method}`);
119
+ });
120
+ }
121
+
122
+ const server = http.createServer(app);
123
+ return new Promise((resolve, reject) => {
124
+ server.once('error', reject);
125
+ server.listen(port, host, () => {
126
+ const url = `http://${host}:${server.address().port}`;
127
+ log(`chat and API: ${url}${agent.spec.interfaces.a2a ? ` · A2A card: ${url}/.well-known/agent.json` : ''}`);
128
+ resolve({ server, url, close: () => (agent.approvals.channels.delete('chat'), new Promise((r) => server.close(r))) });
129
+ });
130
+ });
131
+ }
@@ -0,0 +1,114 @@
1
+ // Package an agent as one executable (Node's single executable applications): the runtime bundled
2
+ // with esbuild, the agent's files embedded as assets, injected with postject into the official Node
3
+ // binary (from nodejs.org, same version as this one, checksum-verified, cached) for each platform.
4
+ // The prepared blob depends only on the Node version, so every platform builds from here; macOS
5
+ // ones must be signed, so they build on a Mac. The engine CLIs it drives are installed separately.
6
+ import { execFile } from 'node:child_process';
7
+ import crypto from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { createRequire } from 'node:module';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { loadAgent } from './spec.js';
14
+
15
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
16
+ // Assembled at run time: the literal must not appear in the bundle we inject (this file is part of
17
+ // it), or postject finds the sentinel twice.
18
+ const FUSE = ['NODE', 'SEA', 'FUSE', 'fce680ab2cc467b6e072b8b5df1996b2'].join('_');
19
+ export const HOST = `${process.platform === 'darwin' ? 'macos' : process.platform}-${process.arch}`;
20
+ export const PLATFORMS = ['macos-arm64', 'macos-x64', 'linux-x64', 'linux-arm64'];
21
+ const sh = (cmd, args, opts = {}) =>
22
+ new Promise((resolve, reject) =>
23
+ execFile(cmd, args, { timeout: 20 * 60_000, maxBuffer: 64 * 1024 * 1024, ...opts }, (e, out, err) => (e ? reject(new Error(`${path.basename(cmd)} failed: ${`${err}\n${out}`.replace(/\x1b\[[0-9;]*m/g, '').trim().split('\n').slice(-4).join(' ') || e.message}`)) : resolve(String(out)))),
24
+ );
25
+
26
+ /** The agent's own files to embed: agent.yaml, its instructions file, tests, and anything it names. */
27
+ function agentFiles(dir) {
28
+ const keep = ['agent.yaml', 'agent.yml', 'agent.json', 'instructions.md', 'tests.yaml', 'README.md'];
29
+ return keep.filter((f) => fs.existsSync(path.join(dir, f)));
30
+ }
31
+
32
+ /** One file: the runtime and its libraries (express, yaml, commander) as CommonJS. */
33
+ async function bundle(outFile) {
34
+ const esbuild = await import('esbuild');
35
+ await esbuild.build({
36
+ entryPoints: [path.join(ROOT, 'src', 'agentkit', 'sea-main.js')],
37
+ outfile: outFile,
38
+ bundle: true,
39
+ platform: 'node',
40
+ format: 'cjs',
41
+ target: 'node22',
42
+ // import.meta.url doesn't exist in CommonJS; give the modules that use it this file's URL.
43
+ define: { 'import.meta.url': '__agentkitUrl' },
44
+ banner: { js: "const __agentkitUrl = require('node:url').pathToFileURL(__filename).href;" },
45
+ logLevel: 'error',
46
+ });
47
+ }
48
+
49
+ /** The official Node binary for a platform (this Node's version), downloaded once and checked. */
50
+ export async function nodeBinary(platform, { cacheDir, log = () => {} } = {}) {
51
+ const v = process.version;
52
+ const [osName, arch] = platform.split('-');
53
+ const dist = `node-${v}-${osName === 'macos' ? 'darwin' : 'linux'}-${arch}`;
54
+ const dir = path.join(cacheDir, dist);
55
+ const bin = path.join(dir, 'node');
56
+ if (fs.existsSync(bin)) return bin;
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ const base = `https://nodejs.org/dist/${v}`;
59
+ log(`downloading Node ${v} for ${platform}`);
60
+ const sums = await (await fetch(`${base}/SHASUMS256.txt`)).text();
61
+ const want = new RegExp(`^([0-9a-f]{64})\\s+${dist}\\.tar\\.gz$`, 'm').exec(sums)?.[1];
62
+ if (!want) throw new Error(`nodejs.org has no ${dist}.tar.gz`);
63
+ const res = await fetch(`${base}/${dist}.tar.gz`);
64
+ if (!res.ok) throw new Error(`Download of ${dist} failed (HTTP ${res.status})`);
65
+ const tgz = Buffer.from(await res.arrayBuffer());
66
+ const got = crypto.createHash('sha256').update(tgz).digest('hex');
67
+ if (got !== want) throw new Error(`${dist}.tar.gz failed its checksum; not using it`);
68
+ const tmp = path.join(dir, 'node.tar.gz');
69
+ fs.writeFileSync(tmp, tgz);
70
+ await sh('tar', ['-xzf', tmp, '-C', dir, '--strip-components', '2', `${dist}/bin/node`]);
71
+ fs.rmSync(tmp);
72
+ return bin;
73
+ }
74
+
75
+ /** Inject the blob into a copy of `nodeBin` (signature removed and re-applied for macOS). */
76
+ async function inject(nodeBin, blob, out, { mac }) {
77
+ fs.copyFileSync(nodeBin, out);
78
+ fs.chmodSync(out, 0o755);
79
+ if (mac) await sh('codesign', ['--remove-signature', out]);
80
+ await sh(process.execPath, [createRequire(import.meta.url).resolve('postject/dist/cli.js'), out, 'NODE_SEA_BLOB', blob, '--sentinel-fuse', FUSE, ...(mac ? ['--macho-segment-name', 'NODE_SEA'] : [])]);
81
+ if (mac) await sh('codesign', ['--sign', '-', out]);
82
+ }
83
+
84
+ /**
85
+ * Build `<outDir>/<name>-<platform>` for an agent folder (any platform; macOS ones on a Mac).
86
+ * Returns { file, platform, size }.
87
+ */
88
+ export async function packageAgent(agentDir, { outDir, platform = HOST, cacheDir = path.join(os.homedir(), '.agentmanager', 'node-dist'), log = () => {} } = {}) {
89
+ const spec = loadAgent(agentDir);
90
+ if (spec.engine.startsWith('acp:')) throw Object.assign(new Error('ACP engines are not supported in a packaged agent yet; run its folder with agentkit'), { status: 400 });
91
+ if (!PLATFORMS.includes(platform)) throw Object.assign(new Error(`platform: one of ${PLATFORMS.join(', ')}`), { status: 400 });
92
+ if (platform.startsWith('macos-') && process.platform !== 'darwin') throw Object.assign(new Error('macOS executables are built on a Mac (they must be signed there)'), { status: 400 });
93
+ fs.mkdirSync(outDir, { recursive: true });
94
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), `agentkit-pack-${spec.name}-`));
95
+ try {
96
+ log('bundling the runtime');
97
+ await bundle(path.join(work, 'main.cjs'));
98
+ const files = agentFiles(spec.dir);
99
+ const assets = { 'manifest.json': path.join(work, 'manifest.json') };
100
+ for (const f of files) assets[`files/${f}`] = path.join(spec.dir, f);
101
+ const build = crypto.createHash('sha256').update(files.map((f) => fs.readFileSync(path.join(spec.dir, f))).join('\0')).digest('hex').slice(0, 16);
102
+ fs.writeFileSync(assets['manifest.json'], JSON.stringify({ name: spec.name, title: spec.title, build, files }));
103
+ const out = path.join(outDir, `${spec.name}-${platform}`);
104
+ fs.writeFileSync(path.join(work, 'sea.json'), JSON.stringify({ main: path.join(work, 'main.cjs'), output: path.join(work, 'blob'), disableExperimentalSEAWarning: true, useCodeCache: false, assets }));
105
+ log('preparing the blob');
106
+ await sh(process.execPath, ['--experimental-sea-config', path.join(work, 'sea.json')]);
107
+ const nodeBin = await nodeBinary(platform, { cacheDir, log });
108
+ log(`making the executable for ${platform}`);
109
+ await inject(nodeBin, path.join(work, 'blob'), out, { mac: platform.startsWith('macos-') });
110
+ return { file: out, platform, size: fs.statSync(out).size };
111
+ } finally {
112
+ fs.rmSync(work, { recursive: true, force: true });
113
+ }
114
+ }
@@ -0,0 +1,106 @@
1
+ // Reporting to AI Agent Manager: the agent says who it is, sends each run (status, cost, result)
2
+ // and its open approvals, and picks up the decisions people made there. It checks in every 30
3
+ // seconds (every 2 while an approval waits), and right away after a run or a new approval.
4
+ import https from 'node:https';
5
+ import os from 'node:os';
6
+
7
+ const EXCERPT = 2000;
8
+
9
+ /**
10
+ * POST JSON over https to a server with a self-signed certificate (AI Agent Manager's remote
11
+ * access), trusting only the certificate whose SHA-256 fingerprint is `pin`. The body is sent only
12
+ * after the fingerprint matches.
13
+ */
14
+ export function pinnedPost(url, { headers, body, pin, timeoutMs = 15_000 }) {
15
+ const want = String(pin).replace(/:/g, '').toLowerCase();
16
+ return new Promise((resolve, reject) => {
17
+ const req = https.request(url, { method: 'POST', headers: { ...headers, 'content-length': Buffer.byteLength(body) }, rejectUnauthorized: false, agent: false, timeout: timeoutMs }, (res) => {
18
+ let text = '';
19
+ res.on('data', (d) => (text += d));
20
+ res.on('end', () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, json: async () => JSON.parse(text) }));
21
+ });
22
+ req.on('socket', (sock) =>
23
+ sock.once('secureConnect', () => {
24
+ const got = sock.getPeerX509Certificate?.()?.fingerprint256.replace(/:/g, '').toLowerCase();
25
+ if (got !== want) return req.destroy(new Error('the certificate does not match AGENTKIT_REPORT_CERT'));
26
+ req.end(body);
27
+ }),
28
+ );
29
+ req.on('timeout', () => req.destroy(new Error('timed out')));
30
+ req.on('error', reject);
31
+ });
32
+ }
33
+
34
+ export function startReporting(agent, { url, token, cert = null, chat = () => null, version = null, log = () => {}, fetchImpl = fetch, everyMs = 30_000, fastMs = 2_000 }) {
35
+ const endpoint = `${url.replace(/\/+$/, '')}/api/agentkit/report`;
36
+ const post = cert && endpoint.startsWith('https:') ? (u, o) => pinnedPost(u, { headers: o.headers, body: o.body, pin: cert }) : fetchImpl;
37
+ const startedAt = new Date().toISOString();
38
+ let unsent = []; // runs not yet acknowledged
39
+ let timer = null;
40
+ let busy = false;
41
+ let again = false;
42
+ let ok = null;
43
+ let stopped = false;
44
+
45
+ const onRun = (r) => {
46
+ unsent = [...unsent, { id: r.id, status: r.status, source: r.source, costUsd: r.costUsd, ms: r.ms, result: String(r.result ?? '').slice(0, EXCERPT), at: new Date().toISOString() }].slice(-50);
47
+ push();
48
+ };
49
+ const onApprovals = () => push();
50
+ agent.on('run', onRun);
51
+ agent.on('approvals', onApprovals);
52
+
53
+ async function push() {
54
+ if (stopped) return;
55
+ if (busy) return void (again = true);
56
+ busy = true;
57
+ clearTimeout(timer);
58
+ const s = agent.spec;
59
+ const sending = unsent;
60
+ const body = {
61
+ agent: { name: s.name, title: s.title, description: s.description, engine: s.engine, triggers: s.triggers.map((t) => t.label), host: os.hostname(), pid: process.pid, chat: chat(), version, startedAt },
62
+ current: agent.current,
63
+ runs: sending,
64
+ pending: agent.approvals.pending(),
65
+ };
66
+ try {
67
+ const res = await post(endpoint, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(15_000) });
68
+ if (!res.ok) throw new Error(res.status === 401 ? 'the report token was not accepted' : `HTTP ${res.status}`);
69
+ const reply = await res.json();
70
+ unsent = unsent.filter((r) => !sending.includes(r));
71
+ for (const d of reply.decisions ?? []) agent.approvals.decide(d.id, d.allow, { by: d.by ?? 'AI Agent Manager', note: d.note ?? '' });
72
+ agent.approvals.channels.add('manager');
73
+ if (ok !== true) log(`reporting to AI Agent Manager at ${url}`);
74
+ ok = true;
75
+ } catch (e) {
76
+ agent.approvals.channels.delete('manager');
77
+ if (ok !== false) log(`could not report to AI Agent Manager (${e.message}); trying again`);
78
+ ok = false;
79
+ } finally {
80
+ busy = false;
81
+ if (!stopped) {
82
+ if (again) (again = false), setImmediate(push);
83
+ else timer = setTimeout(push, agent.approvals.pending().length ? fastMs : everyMs);
84
+ }
85
+ }
86
+ }
87
+
88
+ push();
89
+ // `push()` checks in now (say, once the chat page is up); `stop()` ends reporting.
90
+ return {
91
+ push,
92
+ /** Wait (a few seconds at most) until every run is sent: before a one-off run exits. */
93
+ flush: async (ms = 5000) => {
94
+ for (const end = Date.now() + ms; (unsent.length || busy) && Date.now() < end; ) {
95
+ if (!busy) push();
96
+ await new Promise((r) => setTimeout(r, 100));
97
+ }
98
+ },
99
+ stop: () => {
100
+ stopped = true;
101
+ clearTimeout(timer);
102
+ agent.off('run', onRun);
103
+ agent.off('approvals', onApprovals);
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,25 @@
1
+ // The entry point of a packaged agent (a single executable made by package.js). Its own files
2
+ // (agent.yaml, instructions, tests) are embedded; the first run unpacks them to ~/.agentkit/<name>/,
3
+ // where its memory and runs live too. Later runs keep that folder, so edits to its agent.yaml stay
4
+ // (until a newer build of the same agent replaces the files it shipped).
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import sea from 'node:sea';
9
+ import { main } from './cli.js';
10
+
11
+ const manifest = JSON.parse(sea.getAsset('manifest.json', 'utf8'));
12
+ const dir = process.env.AGENTKIT_HOME ? path.resolve(process.env.AGENTKIT_HOME) : path.join(os.homedir(), '.agentkit', manifest.name);
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ const stamp = path.join(dir, '.build');
15
+ const shipped = fs.existsSync(stamp) ? fs.readFileSync(stamp, 'utf8').trim() : '';
16
+ if (shipped !== manifest.build) {
17
+ for (const f of manifest.files) {
18
+ const target = path.join(dir, f);
19
+ fs.mkdirSync(path.dirname(target), { recursive: true });
20
+ fs.writeFileSync(target, Buffer.from(sea.getAsset(`files/${f}`)));
21
+ }
22
+ fs.writeFileSync(stamp, manifest.build);
23
+ }
24
+
25
+ main(process.argv.slice(2), { name: manifest.name, defaultRef: dir, packaged: true });
@@ -0,0 +1,118 @@
1
+ // Keep an agent running in the background: a launchd agent (macOS) or a systemd user service
2
+ // (Linux) that starts it at login and restarts it if it crashes. It runs through your login shell,
3
+ // so it finds the engine CLIs and the tokens your terminal has.
4
+ import { execFile } from 'node:child_process';
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+
9
+ const sh = (cmd, args) => new Promise((resolve, reject) => execFile(cmd, args, (e, out, err) => (e ? reject(new Error(String(err || e.message).trim())) : resolve(String(out)))));
10
+ const xml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
11
+ const q = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
12
+
13
+ /** Where the service files go, and what they're called, for an agent. */
14
+ export function servicePaths(name, home = os.homedir()) {
15
+ const label = `dev.agentkit.${name}`;
16
+ return {
17
+ label,
18
+ plist: path.join(home, 'Library', 'LaunchAgents', `${label}.plist`),
19
+ unitName: `agentkit-${name}.service`,
20
+ unit: path.join(home, '.config', 'systemd', 'user', `agentkit-${name}.service`),
21
+ };
22
+ }
23
+
24
+ /**
25
+ * What the service runs: `argv` starts the agent in the foreground (`<exe> run` for a packaged one,
26
+ * `node agentkit.js run <dir>` otherwise); settings that change where it lives or which engine
27
+ * binaries it uses carry over.
28
+ */
29
+ export function serviceSpec({ name, dir, argv, env = process.env, shell = process.env.SHELL || '/bin/sh' }) {
30
+ const keep = {};
31
+ for (const [k, v] of Object.entries(env)) if (/^AGENTKIT_/.test(k) && v) keep[k] = v;
32
+ return { name, dir, log: path.join(dir, 'agent.log'), env: keep, command: [shell, '-l', '-c', `exec ${argv.map(q).join(' ')}`] };
33
+ }
34
+
35
+ export function plist(s, label = servicePaths(s.name).label) {
36
+ return `<?xml version="1.0" encoding="UTF-8"?>
37
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
38
+ <plist version="1.0">
39
+ <dict>
40
+ <key>Label</key><string>${xml(label)}</string>
41
+ <key>ProgramArguments</key>
42
+ <array>
43
+ ${s.command.map((a) => ` <string>${xml(a)}</string>`).join('\n')}
44
+ </array>
45
+ <key>RunAtLoad</key><true/>
46
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
47
+ <key>WorkingDirectory</key><string>${xml(s.dir)}</string>
48
+ <key>EnvironmentVariables</key>
49
+ <dict>
50
+ ${Object.entries(s.env).map(([k, v]) => ` <key>${xml(k)}</key><string>${xml(v)}</string>`).join('\n')}
51
+ </dict>
52
+ <key>StandardOutPath</key><string>${xml(s.log)}</string>
53
+ <key>StandardErrorPath</key><string>${xml(s.log)}</string>
54
+ </dict>
55
+ </plist>
56
+ `;
57
+ }
58
+
59
+ export function unit(s) {
60
+ const cmd = s.command.map((a) => `"${a.replace(/(["\\])/g, '\\$1')}"`).join(' ');
61
+ return `[Unit]
62
+ Description=agentkit agent ${s.name}
63
+
64
+ [Service]
65
+ ExecStart=${cmd}
66
+ WorkingDirectory=${s.dir}
67
+ Restart=on-failure
68
+ ${Object.entries(s.env).map(([k, v]) => `Environment=${k}=${v}\n`).join('')}StandardOutput=append:${s.log}
69
+ StandardError=append:${s.log}
70
+
71
+ [Install]
72
+ WantedBy=default.target
73
+ `;
74
+ }
75
+
76
+ const uid = () => process.getuid();
77
+
78
+ export async function install(s) {
79
+ const p = servicePaths(s.name);
80
+ if (process.platform === 'darwin') {
81
+ fs.mkdirSync(path.dirname(p.plist), { recursive: true });
82
+ await sh('launchctl', ['bootout', `gui/${uid()}/${p.label}`]).catch(() => {});
83
+ fs.writeFileSync(p.plist, plist(s, p.label));
84
+ await sh('launchctl', ['bootstrap', `gui/${uid()}`, p.plist]);
85
+ return { file: p.plist, manager: 'launchd' };
86
+ }
87
+ if (process.platform === 'linux') {
88
+ fs.mkdirSync(path.dirname(p.unit), { recursive: true });
89
+ fs.writeFileSync(p.unit, unit(s));
90
+ await sh('systemctl', ['--user', 'daemon-reload']);
91
+ await sh('systemctl', ['--user', 'enable', '--now', p.unitName]);
92
+ return { file: p.unit, manager: 'systemd', note: 'To keep it running when you are logged out: loginctl enable-linger $USER' };
93
+ }
94
+ throw new Error(`Background service is not supported on ${process.platform}`);
95
+ }
96
+
97
+ export async function uninstall(name) {
98
+ const p = servicePaths(name);
99
+ if (process.platform === 'darwin') {
100
+ await sh('launchctl', ['bootout', `gui/${uid()}/${p.label}`]).catch(() => {});
101
+ fs.rmSync(p.plist, { force: true });
102
+ return { file: p.plist };
103
+ }
104
+ if (process.platform === 'linux') {
105
+ await sh('systemctl', ['--user', 'disable', '--now', p.unitName]).catch(() => {});
106
+ fs.rmSync(p.unit, { force: true });
107
+ await sh('systemctl', ['--user', 'daemon-reload']).catch(() => {});
108
+ return { file: p.unit };
109
+ }
110
+ throw new Error(`Background service is not supported on ${process.platform}`);
111
+ }
112
+
113
+ export async function status(name) {
114
+ const p = servicePaths(name);
115
+ if (process.platform === 'darwin') return { installed: fs.existsSync(p.plist), running: await sh('launchctl', ['print', `gui/${uid()}/${p.label}`]).then((o) => /state = running/.test(o), () => false), file: p.plist };
116
+ if (process.platform === 'linux') return { installed: fs.existsSync(p.unit), running: await sh('systemctl', ['--user', 'is-active', p.unitName]).then(() => true, () => false), file: p.unit };
117
+ return { installed: false, running: false, file: null };
118
+ }