nexus-agentd 0.1.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/README.md +253 -0
- package/dist/acp/runtime.d.ts +26 -0
- package/dist/acp/runtime.js +420 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +24 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +115 -0
- package/dist/drivers/claude.d.ts +2 -0
- package/dist/drivers/claude.js +9 -0
- package/dist/drivers/codex.d.ts +2 -0
- package/dist/drivers/codex.js +9 -0
- package/dist/drivers/index.d.ts +4 -0
- package/dist/drivers/index.js +30 -0
- package/dist/drivers/openclaw.d.ts +2 -0
- package/dist/drivers/openclaw.js +13 -0
- package/dist/drivers/opencode.d.ts +2 -0
- package/dist/drivers/opencode.js +9 -0
- package/dist/drivers/pi.d.ts +13 -0
- package/dist/drivers/pi.js +31 -0
- package/dist/drivers/stdio.d.ts +13 -0
- package/dist/drivers/stdio.js +114 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/drivers/types.js +1 -0
- package/dist/events.d.ts +13 -0
- package/dist/events.js +40 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +48 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +170 -0
- package/dist/session-contract.d.ts +12 -0
- package/dist/session-contract.js +1 -0
- package/dist/session.d.ts +26 -0
- package/dist/session.js +210 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +7 -0
- package/dist/workspace.d.ts +7 -0
- package/dist/workspace.js +34 -0
- package/nexus-agentd.example.json +43 -0
- package/package.json +52 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { SessionNotFoundError } from './session.js';
|
|
4
|
+
export function createAgentdServer(config, sessions) {
|
|
5
|
+
return http.createServer((request, response) => {
|
|
6
|
+
void handleRequest(config, sessions, request, response).catch((error) => {
|
|
7
|
+
if (response.headersSent) {
|
|
8
|
+
response.destroy(error instanceof Error ? error : undefined);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const status = error instanceof SessionNotFoundError
|
|
12
|
+
? 404
|
|
13
|
+
: error instanceof RequestError
|
|
14
|
+
? error.status
|
|
15
|
+
: 500;
|
|
16
|
+
writeJson(response, status, {
|
|
17
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async function handleRequest(config, sessions, request, response) {
|
|
23
|
+
const url = new URL(request.url || '/', 'http://localhost');
|
|
24
|
+
if (url.pathname === '/health' && request.method === 'GET') {
|
|
25
|
+
writeJson(response, 200, { ok: true });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
authenticate(request, config.authToken);
|
|
29
|
+
if (url.pathname === '/v1/agents' && request.method === 'GET') {
|
|
30
|
+
writeJson(response, 200, { agents: await sessions.listAgents() });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname === '/v1/sessions' && request.method === 'POST') {
|
|
34
|
+
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
35
|
+
assertOnlyKeys(body, ['agentId', 'workspace']);
|
|
36
|
+
const agentId = requiredString(body.agentId, 'agentId');
|
|
37
|
+
const workspace = requiredString(body.workspace, 'workspace');
|
|
38
|
+
writeJson(response, 201, await sessions.create(agentId, workspace));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const match = url.pathname.match(/^\/v1\/sessions\/([^/]+)(?:\/(message|cancel|events))?$/);
|
|
42
|
+
if (!match)
|
|
43
|
+
throw new RequestError(404, 'Route not found');
|
|
44
|
+
const sessionId = decodeURIComponent(match[1]);
|
|
45
|
+
const action = match[2];
|
|
46
|
+
if (!action && request.method === 'GET') {
|
|
47
|
+
writeJson(response, 200, sessions.get(sessionId));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (action === 'message' && request.method === 'POST') {
|
|
51
|
+
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
52
|
+
assertOnlyKeys(body, ['message']);
|
|
53
|
+
writeJson(response, 202, await sessions.message(sessionId, requiredString(body.message, 'message')));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (action === 'cancel' && request.method === 'POST') {
|
|
57
|
+
if (Number(request.headers['content-length'] || 0) > 0) {
|
|
58
|
+
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
59
|
+
assertOnlyKeys(body, []);
|
|
60
|
+
}
|
|
61
|
+
writeJson(response, 200, await sessions.cancel(sessionId));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (action === 'events' && request.method === 'GET') {
|
|
65
|
+
streamEvents(request, response, sessions, sessionId, url.searchParams.get('after') ||
|
|
66
|
+
stringHeader(request.headers['last-event-id']));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
throw new RequestError(405, 'Method not allowed');
|
|
70
|
+
}
|
|
71
|
+
function streamEvents(request, response, sessions, sessionId, after) {
|
|
72
|
+
sessions.get(sessionId);
|
|
73
|
+
response.writeHead(200, {
|
|
74
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
75
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
76
|
+
Connection: 'keep-alive',
|
|
77
|
+
'X-Accel-Buffering': 'no'
|
|
78
|
+
});
|
|
79
|
+
response.flushHeaders?.();
|
|
80
|
+
for (const event of sessions.eventsAfter(sessionId, after)) {
|
|
81
|
+
writeEvent(response, event);
|
|
82
|
+
}
|
|
83
|
+
const unsubscribe = sessions.subscribe(sessionId, (event) => writeEvent(response, event));
|
|
84
|
+
const heartbeat = setInterval(() => response.write(': heartbeat\n\n'), 15_000);
|
|
85
|
+
const close = () => {
|
|
86
|
+
clearInterval(heartbeat);
|
|
87
|
+
unsubscribe();
|
|
88
|
+
};
|
|
89
|
+
request.once('close', close);
|
|
90
|
+
response.once('close', close);
|
|
91
|
+
}
|
|
92
|
+
function writeEvent(response, event) {
|
|
93
|
+
if (response.destroyed || response.writableEnded)
|
|
94
|
+
return;
|
|
95
|
+
response.write(`id: ${event.id}\n`);
|
|
96
|
+
response.write(`event: ${event.type}\n`);
|
|
97
|
+
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
98
|
+
}
|
|
99
|
+
function authenticate(request, expected) {
|
|
100
|
+
const value = stringHeader(request.headers.authorization);
|
|
101
|
+
if (!value.startsWith('Bearer ')) {
|
|
102
|
+
throw new RequestError(401, 'Bearer token is required');
|
|
103
|
+
}
|
|
104
|
+
const actual = value.slice(7);
|
|
105
|
+
const left = Buffer.from(actual);
|
|
106
|
+
const right = Buffer.from(expected);
|
|
107
|
+
if (left.length !== right.length || !timingSafeEqual(left, right)) {
|
|
108
|
+
throw new RequestError(401, 'Invalid Bearer token');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function readJsonBody(request, maxBytes) {
|
|
112
|
+
const declared = Number(request.headers['content-length']);
|
|
113
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
114
|
+
throw new RequestError(413, 'Request body is too large');
|
|
115
|
+
}
|
|
116
|
+
const chunks = [];
|
|
117
|
+
let total = 0;
|
|
118
|
+
for await (const value of request) {
|
|
119
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
120
|
+
total += chunk.length;
|
|
121
|
+
if (total > maxBytes)
|
|
122
|
+
throw new RequestError(413, 'Request body is too large');
|
|
123
|
+
chunks.push(chunk);
|
|
124
|
+
}
|
|
125
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
126
|
+
if (!raw.trim())
|
|
127
|
+
return {};
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
131
|
+
throw new Error('body must be an object');
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new RequestError(400, `Invalid JSON body: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function assertOnlyKeys(body, allowed) {
|
|
140
|
+
const accepted = new Set(allowed);
|
|
141
|
+
const unknown = Object.keys(body).filter((key) => !accepted.has(key));
|
|
142
|
+
if (unknown.length) {
|
|
143
|
+
throw new RequestError(400, `Unsupported request fields: ${unknown.join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function requiredString(value, name) {
|
|
147
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
148
|
+
if (!text)
|
|
149
|
+
throw new RequestError(400, `${name} is required`);
|
|
150
|
+
return text;
|
|
151
|
+
}
|
|
152
|
+
function stringHeader(value) {
|
|
153
|
+
return Array.isArray(value) ? value[0] || '' : value || '';
|
|
154
|
+
}
|
|
155
|
+
function writeJson(response, status, value) {
|
|
156
|
+
const body = `${JSON.stringify(value)}\n`;
|
|
157
|
+
response.writeHead(status, {
|
|
158
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
159
|
+
'Content-Length': Buffer.byteLength(body),
|
|
160
|
+
'Cache-Control': 'no-store'
|
|
161
|
+
});
|
|
162
|
+
response.end(body);
|
|
163
|
+
}
|
|
164
|
+
class RequestError extends Error {
|
|
165
|
+
status;
|
|
166
|
+
constructor(status, message) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.status = status;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentdEventType, AgentdPendingRequest, AgentdSessionState } from './types.js';
|
|
2
|
+
export interface AcpSessionSink {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly state: AgentdSessionState;
|
|
5
|
+
readonly acpSessionId?: string;
|
|
6
|
+
setAcpSessionId(id: string): void;
|
|
7
|
+
setState(state: AgentdSessionState, error?: string): void;
|
|
8
|
+
appendOutput(text: string): void;
|
|
9
|
+
setPending(request: AgentdPendingRequest): void;
|
|
10
|
+
clearPending(): void;
|
|
11
|
+
emit(type: AgentdEventType, data?: unknown): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDriver } from './drivers/index.js';
|
|
2
|
+
import type { WorkspacePolicy } from './workspace.js';
|
|
3
|
+
import type { AgentdAgentView, AgentdConfig, AgentdEvent, AgentdSessionView } from './types.js';
|
|
4
|
+
export declare class SessionManager {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly workspacePolicy;
|
|
7
|
+
private readonly drivers;
|
|
8
|
+
private sessions;
|
|
9
|
+
private cleanupTimer?;
|
|
10
|
+
constructor(config: AgentdConfig, workspacePolicy: WorkspacePolicy, drivers: Map<string, AgentDriver>);
|
|
11
|
+
startCleanup(): void;
|
|
12
|
+
listAgents(): Promise<AgentdAgentView[]>;
|
|
13
|
+
create(agentId: string, workspaceInput: string): Promise<AgentdSessionView>;
|
|
14
|
+
get(id: string): AgentdSessionView;
|
|
15
|
+
message(id: string, message: string): Promise<AgentdSessionView>;
|
|
16
|
+
cancel(id: string): Promise<AgentdSessionView>;
|
|
17
|
+
eventsAfter(id: string, after?: string): AgentdEvent[];
|
|
18
|
+
subscribe(id: string, listener: (event: AgentdEvent) => void): () => boolean;
|
|
19
|
+
shutdown(): Promise<void>;
|
|
20
|
+
private require;
|
|
21
|
+
private cleanup;
|
|
22
|
+
}
|
|
23
|
+
export declare class SessionNotFoundError extends Error {
|
|
24
|
+
readonly sessionId: string;
|
|
25
|
+
constructor(sessionId: string);
|
|
26
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { SessionEventLog } from './events.js';
|
|
3
|
+
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
4
|
+
class ManagedSession {
|
|
5
|
+
agentId;
|
|
6
|
+
workspace;
|
|
7
|
+
maxOutputChars;
|
|
8
|
+
id = randomUUID();
|
|
9
|
+
createdAt = Date.now();
|
|
10
|
+
events;
|
|
11
|
+
state = 'created';
|
|
12
|
+
updatedAt = this.createdAt;
|
|
13
|
+
acpSessionId;
|
|
14
|
+
output = '';
|
|
15
|
+
error;
|
|
16
|
+
pendingRequest;
|
|
17
|
+
runtime;
|
|
18
|
+
constructor(agentId, workspace, maxEvents, maxOutputChars) {
|
|
19
|
+
this.agentId = agentId;
|
|
20
|
+
this.workspace = workspace;
|
|
21
|
+
this.maxOutputChars = maxOutputChars;
|
|
22
|
+
this.events = new SessionEventLog(this.id, maxEvents);
|
|
23
|
+
this.events.append('session_state', { state: this.state });
|
|
24
|
+
}
|
|
25
|
+
attach(runtime) {
|
|
26
|
+
this.runtime = runtime;
|
|
27
|
+
}
|
|
28
|
+
setAcpSessionId(id) {
|
|
29
|
+
this.acpSessionId = id;
|
|
30
|
+
this.updatedAt = Date.now();
|
|
31
|
+
}
|
|
32
|
+
setState(state, error) {
|
|
33
|
+
this.state = state;
|
|
34
|
+
this.error = error;
|
|
35
|
+
this.updatedAt = Date.now();
|
|
36
|
+
const type = state === 'completed'
|
|
37
|
+
? 'completed'
|
|
38
|
+
: state === 'failed'
|
|
39
|
+
? 'failed'
|
|
40
|
+
: state === 'canceled'
|
|
41
|
+
? 'canceled'
|
|
42
|
+
: 'session_state';
|
|
43
|
+
this.events.append(type, { state, ...(error ? { error } : {}) });
|
|
44
|
+
}
|
|
45
|
+
appendOutput(text) {
|
|
46
|
+
if (!text)
|
|
47
|
+
return;
|
|
48
|
+
this.output = `${this.output}${text}`;
|
|
49
|
+
if (this.output.length > this.maxOutputChars) {
|
|
50
|
+
this.output = `…[truncated by nexus-agentd]\n${this.output.slice(-this.maxOutputChars)}`;
|
|
51
|
+
}
|
|
52
|
+
this.updatedAt = Date.now();
|
|
53
|
+
}
|
|
54
|
+
setPending(request) {
|
|
55
|
+
this.pendingRequest = structuredClone(request);
|
|
56
|
+
this.state = request.kind === 'permission' ? 'permission_required' : 'input_required';
|
|
57
|
+
this.updatedAt = Date.now();
|
|
58
|
+
this.events.append(request.kind === 'permission'
|
|
59
|
+
? 'permission_required'
|
|
60
|
+
: 'input_required', request);
|
|
61
|
+
}
|
|
62
|
+
clearPending() {
|
|
63
|
+
this.pendingRequest = undefined;
|
|
64
|
+
this.updatedAt = Date.now();
|
|
65
|
+
}
|
|
66
|
+
emit(type, data) {
|
|
67
|
+
this.updatedAt = Date.now();
|
|
68
|
+
this.events.append(type, data);
|
|
69
|
+
}
|
|
70
|
+
async message(message) {
|
|
71
|
+
if (!this.runtime)
|
|
72
|
+
throw new Error('ACP runtime is unavailable');
|
|
73
|
+
if (this.pendingRequest) {
|
|
74
|
+
await this.runtime.respondPending(message);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (this.state === 'running') {
|
|
78
|
+
throw new Error('ACP session is already processing a prompt');
|
|
79
|
+
}
|
|
80
|
+
this.output = '';
|
|
81
|
+
this.error = undefined;
|
|
82
|
+
this.updatedAt = Date.now();
|
|
83
|
+
void this.runtime.prompt(message).catch((error) => {
|
|
84
|
+
if (this.state !== 'canceled')
|
|
85
|
+
this.setState('failed', errorMessage(error));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async cancel() {
|
|
89
|
+
await this.runtime?.cancel();
|
|
90
|
+
}
|
|
91
|
+
async dispose() {
|
|
92
|
+
await this.runtime?.dispose();
|
|
93
|
+
}
|
|
94
|
+
snapshot() {
|
|
95
|
+
return {
|
|
96
|
+
id: this.id,
|
|
97
|
+
acpSessionId: this.acpSessionId,
|
|
98
|
+
agentId: this.agentId,
|
|
99
|
+
workspace: this.workspace,
|
|
100
|
+
state: this.state,
|
|
101
|
+
output: this.output || undefined,
|
|
102
|
+
error: this.error,
|
|
103
|
+
artifacts: [],
|
|
104
|
+
pendingRequest: this.pendingRequest
|
|
105
|
+
? structuredClone(this.pendingRequest)
|
|
106
|
+
: undefined,
|
|
107
|
+
lastEventId: this.events.lastId,
|
|
108
|
+
createdAt: this.createdAt,
|
|
109
|
+
updatedAt: this.updatedAt
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export class SessionManager {
|
|
114
|
+
config;
|
|
115
|
+
workspacePolicy;
|
|
116
|
+
drivers;
|
|
117
|
+
sessions = new Map();
|
|
118
|
+
cleanupTimer;
|
|
119
|
+
constructor(config, workspacePolicy, drivers) {
|
|
120
|
+
this.config = config;
|
|
121
|
+
this.workspacePolicy = workspacePolicy;
|
|
122
|
+
this.drivers = drivers;
|
|
123
|
+
}
|
|
124
|
+
startCleanup() {
|
|
125
|
+
if (this.cleanupTimer)
|
|
126
|
+
return;
|
|
127
|
+
this.cleanupTimer = setInterval(() => void this.cleanup(), 60_000);
|
|
128
|
+
this.cleanupTimer.unref?.();
|
|
129
|
+
}
|
|
130
|
+
async listAgents() {
|
|
131
|
+
return Promise.all(Array.from(this.drivers.values()).map((driver) => driver.probe()));
|
|
132
|
+
}
|
|
133
|
+
async create(agentId, workspaceInput) {
|
|
134
|
+
const driver = this.drivers.get(agentId);
|
|
135
|
+
if (!driver)
|
|
136
|
+
throw new Error(`Configured ACP agent not found: ${agentId}`);
|
|
137
|
+
const workspace = await this.workspacePolicy.resolve(workspaceInput);
|
|
138
|
+
const session = new ManagedSession(agentId, workspace, this.config.maxEventsPerSession, this.config.maxOutputChars);
|
|
139
|
+
this.sessions.set(session.id, session);
|
|
140
|
+
const runtime = new AcpProcessRuntime(driver, session);
|
|
141
|
+
session.attach(runtime);
|
|
142
|
+
try {
|
|
143
|
+
await runtime.start(workspace);
|
|
144
|
+
return session.snapshot();
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
session.setState('failed', errorMessage(error));
|
|
148
|
+
await session.dispose();
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
get(id) {
|
|
153
|
+
return this.require(id).snapshot();
|
|
154
|
+
}
|
|
155
|
+
async message(id, message) {
|
|
156
|
+
const text = String(message || '');
|
|
157
|
+
if (!text.trim())
|
|
158
|
+
throw new Error('message is required');
|
|
159
|
+
const session = this.require(id);
|
|
160
|
+
await session.message(text);
|
|
161
|
+
return session.snapshot();
|
|
162
|
+
}
|
|
163
|
+
async cancel(id) {
|
|
164
|
+
const session = this.require(id);
|
|
165
|
+
await session.cancel();
|
|
166
|
+
return session.snapshot();
|
|
167
|
+
}
|
|
168
|
+
eventsAfter(id, after) {
|
|
169
|
+
return this.require(id).events.after(after);
|
|
170
|
+
}
|
|
171
|
+
subscribe(id, listener) {
|
|
172
|
+
return this.require(id).events.subscribe(listener);
|
|
173
|
+
}
|
|
174
|
+
async shutdown() {
|
|
175
|
+
if (this.cleanupTimer)
|
|
176
|
+
clearInterval(this.cleanupTimer);
|
|
177
|
+
this.cleanupTimer = undefined;
|
|
178
|
+
await Promise.allSettled(Array.from(this.sessions.values()).map((session) => session.dispose()));
|
|
179
|
+
this.sessions.clear();
|
|
180
|
+
}
|
|
181
|
+
require(id) {
|
|
182
|
+
const session = this.sessions.get(id);
|
|
183
|
+
if (!session)
|
|
184
|
+
throw new SessionNotFoundError(id);
|
|
185
|
+
return session;
|
|
186
|
+
}
|
|
187
|
+
async cleanup() {
|
|
188
|
+
const cutoff = Date.now() - this.config.sessionTtlMs;
|
|
189
|
+
for (const [id, session] of this.sessions) {
|
|
190
|
+
if (session.updatedAt > cutoff ||
|
|
191
|
+
session.state === 'running' ||
|
|
192
|
+
session.state === 'permission_required' ||
|
|
193
|
+
session.state === 'input_required') {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
await session.dispose();
|
|
197
|
+
this.sessions.delete(id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export class SessionNotFoundError extends Error {
|
|
202
|
+
sessionId;
|
|
203
|
+
constructor(sessionId) {
|
|
204
|
+
super(`Nexus Gateway session not found: ${sessionId}`);
|
|
205
|
+
this.sessionId = sessionId;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function errorMessage(error) {
|
|
209
|
+
return error instanceof Error ? error.message : String(error);
|
|
210
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type AgentdSessionState = 'created' | 'running' | 'input_required' | 'permission_required' | 'completed' | 'failed' | 'canceled';
|
|
2
|
+
export type PermissionPolicy = 'ask' | 'deny';
|
|
3
|
+
export declare const agentdDriverKinds: readonly ["opencode", "claude", "codex", "pi", "openclaw"];
|
|
4
|
+
export type AgentdDriverKind = (typeof agentdDriverKinds)[number];
|
|
5
|
+
export interface AgentdDriverConfig {
|
|
6
|
+
driver: AgentdDriverKind;
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
command?: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
inheritEnv?: string[];
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
permissionPolicy?: PermissionPolicy;
|
|
15
|
+
permissionTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentdConfig {
|
|
18
|
+
listen: {
|
|
19
|
+
host: string;
|
|
20
|
+
port: number;
|
|
21
|
+
};
|
|
22
|
+
authToken: string;
|
|
23
|
+
workspaceRoots: string[];
|
|
24
|
+
maxRequestBytes: number;
|
|
25
|
+
maxEventsPerSession: number;
|
|
26
|
+
maxOutputChars: number;
|
|
27
|
+
sessionTtlMs: number;
|
|
28
|
+
agents: Record<string, AgentdDriverConfig>;
|
|
29
|
+
}
|
|
30
|
+
export interface AgentdAgentView {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
protocol: 'acp';
|
|
35
|
+
ready: boolean;
|
|
36
|
+
version?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface AgentdPendingRequest {
|
|
40
|
+
id: string;
|
|
41
|
+
kind: 'permission' | 'input';
|
|
42
|
+
prompt: string;
|
|
43
|
+
options?: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
kind?: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export interface AgentdArtifact {
|
|
50
|
+
id?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
text?: string;
|
|
54
|
+
url?: string;
|
|
55
|
+
filename?: string;
|
|
56
|
+
mediaType?: string;
|
|
57
|
+
metadata?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface AgentdSessionView {
|
|
60
|
+
id: string;
|
|
61
|
+
acpSessionId?: string;
|
|
62
|
+
agentId: string;
|
|
63
|
+
workspace: string;
|
|
64
|
+
state: AgentdSessionState;
|
|
65
|
+
output?: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
artifacts: AgentdArtifact[];
|
|
68
|
+
pendingRequest?: AgentdPendingRequest;
|
|
69
|
+
lastEventId?: string;
|
|
70
|
+
createdAt: number;
|
|
71
|
+
updatedAt: number;
|
|
72
|
+
}
|
|
73
|
+
export type AgentdEventType = 'session_state' | 'assistant_chunk' | 'thought_chunk' | 'plan' | 'tool_call' | 'tool_update' | 'terminal_output' | 'file_activity' | 'permission_required' | 'input_required' | 'completed' | 'failed' | 'canceled';
|
|
74
|
+
export interface AgentdEvent {
|
|
75
|
+
id: string;
|
|
76
|
+
sessionId: string;
|
|
77
|
+
type: AgentdEventType;
|
|
78
|
+
timestamp: number;
|
|
79
|
+
data?: unknown;
|
|
80
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export class WorkspacePolicy {
|
|
4
|
+
roots;
|
|
5
|
+
constructor(roots) {
|
|
6
|
+
this.roots = roots;
|
|
7
|
+
}
|
|
8
|
+
static async create(configuredRoots) {
|
|
9
|
+
const roots = await Promise.all(configuredRoots.map(async (root) => normalize(await realpath(root))));
|
|
10
|
+
return new WorkspacePolicy(Array.from(new Set(roots)));
|
|
11
|
+
}
|
|
12
|
+
async resolve(input) {
|
|
13
|
+
if (!input?.trim())
|
|
14
|
+
throw new Error('workspace is required');
|
|
15
|
+
const candidate = normalize(await realpath(input));
|
|
16
|
+
const allowed = this.roots.some((root) => isWithin(root, candidate));
|
|
17
|
+
if (!allowed)
|
|
18
|
+
throw new Error(`workspace is outside the configured allowlist: ${input}`);
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
listRoots() {
|
|
22
|
+
return [...this.roots];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function isWithin(root, candidate) {
|
|
26
|
+
const relative = path.relative(root, candidate);
|
|
27
|
+
return (relative === '' ||
|
|
28
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
29
|
+
relative !== '..' &&
|
|
30
|
+
!path.isAbsolute(relative)));
|
|
31
|
+
}
|
|
32
|
+
function normalize(value) {
|
|
33
|
+
return path.resolve(value);
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"listen": {
|
|
3
|
+
"host": "127.0.0.1",
|
|
4
|
+
"port": 8787
|
|
5
|
+
},
|
|
6
|
+
"authToken": "env:NEXUS_AGENTD_TOKEN",
|
|
7
|
+
"workspaceRoots": [
|
|
8
|
+
"/data/repos"
|
|
9
|
+
],
|
|
10
|
+
"agents": {
|
|
11
|
+
"opencode": {
|
|
12
|
+
"driver": "opencode",
|
|
13
|
+
"command": "opencode",
|
|
14
|
+
"args": ["acp"],
|
|
15
|
+
"permissionPolicy": "ask",
|
|
16
|
+
"inheritEnv": [
|
|
17
|
+
"OPENAI_API_KEY",
|
|
18
|
+
"ANTHROPIC_API_KEY"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"claude": {
|
|
22
|
+
"driver": "claude",
|
|
23
|
+
"command": "claude-agent-acp",
|
|
24
|
+
"permissionPolicy": "ask"
|
|
25
|
+
},
|
|
26
|
+
"codex": {
|
|
27
|
+
"driver": "codex",
|
|
28
|
+
"command": "codex-acp",
|
|
29
|
+
"permissionPolicy": "ask"
|
|
30
|
+
},
|
|
31
|
+
"pi": {
|
|
32
|
+
"driver": "pi",
|
|
33
|
+
"command": "pi-acp",
|
|
34
|
+
"permissionPolicy": "ask"
|
|
35
|
+
},
|
|
36
|
+
"openclaw": {
|
|
37
|
+
"driver": "openclaw",
|
|
38
|
+
"command": "openclaw",
|
|
39
|
+
"args": ["acp"],
|
|
40
|
+
"permissionPolicy": "ask"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nexus-agentd",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local Nexus Gateway daemon that exposes allowlisted Coding Agents through HTTP/SSE and ACP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nexus-agentd": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"nexus-agentd.example.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.0.0"
|
|
18
|
+
},
|
|
19
|
+
"author": "lumia1998",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/lumia1998/koishi-plugin-agent-nexus.git",
|
|
23
|
+
"directory": "packages/nexus-agentd"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/lumia1998/koishi-plugin-agent-nexus/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/lumia1998/koishi-plugin-agent-nexus/tree/main/packages/nexus-agentd#readme",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"agent-client-protocol",
|
|
31
|
+
"acp",
|
|
32
|
+
"agent",
|
|
33
|
+
"gateway",
|
|
34
|
+
"coding-agent"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
|
+
"test": "tsx --test test/*.test.ts",
|
|
40
|
+
"prepack": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@agentclientprotocol/sdk": "1.3.0",
|
|
44
|
+
"zod": "^3.25.76"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.14.0",
|
|
48
|
+
"tsx": "^4.21.0",
|
|
49
|
+
"typescript": "^5.6.0"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|