@moxxy/plugin-channel-http 0.26.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/LICENSE +21 -0
- package/dist/channel.d.ts +50 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +164 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +95 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +354 -0
- package/dist/router.js.map +1 -0
- package/package.json +67 -0
- package/src/attach.test.ts +85 -0
- package/src/channel.test.ts +165 -0
- package/src/channel.ts +211 -0
- package/src/index.ts +58 -0
- package/src/integration.test.ts +254 -0
- package/src/router.test.ts +665 -0
- package/src/router.ts +424 -0
package/dist/router.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readRequestBody, bearerTokenMatches } from '@moxxy/sdk/server';
|
|
3
|
+
export const turnRequestSchema = z.object({
|
|
4
|
+
prompt: z.string().min(1),
|
|
5
|
+
model: z.string().optional(),
|
|
6
|
+
systemPrompt: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Counting semaphore gating how many turns run concurrently on the single
|
|
10
|
+
* shared session. The HTTP server otherwise accepts unlimited parallel
|
|
11
|
+
* requests, each calling `session.runTurn` on the SAME session — N concurrent
|
|
12
|
+
* clients would spawn N provider streams (cost/memory/connection blowup) and
|
|
13
|
+
* interleave one shared conversation history. `tryAcquire` is non-blocking so
|
|
14
|
+
* excess requests get a fast 429 instead of queueing unbounded.
|
|
15
|
+
*/
|
|
16
|
+
export class TurnLimiter {
|
|
17
|
+
max;
|
|
18
|
+
inUse = 0;
|
|
19
|
+
constructor(max) {
|
|
20
|
+
this.max = max;
|
|
21
|
+
}
|
|
22
|
+
tryAcquire() {
|
|
23
|
+
if (this.inUse >= this.max)
|
|
24
|
+
return false;
|
|
25
|
+
this.inUse += 1;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
release() {
|
|
29
|
+
if (this.inUse > 0)
|
|
30
|
+
this.inUse -= 1;
|
|
31
|
+
}
|
|
32
|
+
get active() {
|
|
33
|
+
return this.inUse;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Default write-side idle timeout for an SSE turn. A live-but-slow consumer
|
|
37
|
+
* resets this on every flushed write; only a truly stalled (open-but-silent)
|
|
38
|
+
* reader trips it, at which point we abort the turn — the same outcome as a
|
|
39
|
+
* clean disconnect, just without waiting for a TCP close that may never come.
|
|
40
|
+
* Generous so normal inter-event gaps (model thinking, tool calls) don't trip
|
|
41
|
+
* it. */
|
|
42
|
+
export const DEFAULT_STREAM_STALL_MS = 120_000;
|
|
43
|
+
/** Shape an internal/server-side error for the wire: log the full detail
|
|
44
|
+
* server-side and return a stable, generic message so filesystem paths,
|
|
45
|
+
* provider internals, or system-prompt fragments embedded in `err` never leak
|
|
46
|
+
* to a (possibly remote) caller. Client-input validation errors (400) are NOT
|
|
47
|
+
* routed through this — echoing the caller's own malformed input is safe. */
|
|
48
|
+
function publicError(ctx, logMsg, err) {
|
|
49
|
+
ctx.logger?.warn(logMsg, { err: err instanceof Error ? err.message : String(err) });
|
|
50
|
+
return 'internal error';
|
|
51
|
+
}
|
|
52
|
+
/** Match HTTP request to a handler. Returns null if no route matches. */
|
|
53
|
+
export function routeRequest(req) {
|
|
54
|
+
const rawUrl = req.url ?? '/';
|
|
55
|
+
// Strip the query string before matching — `/v1/turn/audio?model=...`
|
|
56
|
+
// is the same route as `/v1/turn/audio`. The handler reads query
|
|
57
|
+
// params off req.url itself.
|
|
58
|
+
const pathname = rawUrl.split('?')[0] ?? rawUrl;
|
|
59
|
+
if (req.method === 'GET' && pathname === '/v1/health')
|
|
60
|
+
return handleHealth;
|
|
61
|
+
if (req.method === 'POST' && pathname === '/v1/turn')
|
|
62
|
+
return handleTurn;
|
|
63
|
+
if (req.method === 'POST' && pathname === '/v1/turn/stream')
|
|
64
|
+
return handleTurnStream;
|
|
65
|
+
if (req.method === 'POST' && pathname === '/v1/turn/audio')
|
|
66
|
+
return handleTurnAudio;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
export async function handleHealth(_req, res) {
|
|
70
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
71
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
72
|
+
}
|
|
73
|
+
function checkAuth(req, expected) {
|
|
74
|
+
if (!expected)
|
|
75
|
+
return true;
|
|
76
|
+
// Constant-time compare of the full `Bearer <token>` header so the token
|
|
77
|
+
// isn't recoverable byte-by-byte via response timing.
|
|
78
|
+
return bearerTokenMatches(req.headers.authorization, `Bearer ${expected}`);
|
|
79
|
+
}
|
|
80
|
+
async function readBody(req, max = 64 * 1024) {
|
|
81
|
+
return (await readRequestBody(req, max)).toString('utf8');
|
|
82
|
+
}
|
|
83
|
+
/** Audio uploads need a much larger cap than JSON; 10 MB covers a few
|
|
84
|
+
* minutes of Opus voice (Telegram caps voice notes at 50 MB, but
|
|
85
|
+
* realistic notes are well under that). */
|
|
86
|
+
const DEFAULT_AUDIO_MAX = 10 * 1024 * 1024;
|
|
87
|
+
function reply(res, status, body) {
|
|
88
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify(body));
|
|
90
|
+
}
|
|
91
|
+
/** Hard cap on events buffered for a single non-streaming turn. The buffered
|
|
92
|
+
* path holds every event in memory and returns them in one JSON blob; a
|
|
93
|
+
* runaway/tool-looping turn could otherwise grow this without bound. Once
|
|
94
|
+
* exceeded we abort the turn rather than keep allocating. Generous enough that
|
|
95
|
+
* no normal turn hits it. */
|
|
96
|
+
export const MAX_BUFFERED_EVENTS = 50_000;
|
|
97
|
+
/**
|
|
98
|
+
* Drive a single buffered (non-streaming) turn to completion: drain every
|
|
99
|
+
* event, abort the turn if the client hangs up (so the model stops billing
|
|
100
|
+
* with nobody listening), and pull out the final assistant message. Shared by
|
|
101
|
+
* `handleTurn` and `handleTurnAudio` so the abort wiring and event/assistant
|
|
102
|
+
* extraction live in exactly one place. Re-throws on turn failure so each
|
|
103
|
+
* caller can shape its own error reply.
|
|
104
|
+
*/
|
|
105
|
+
export async function driveTurn(session, prompt, runOptions, res) {
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const onClose = () => controller.abort();
|
|
108
|
+
res.on('close', onClose);
|
|
109
|
+
const events = [];
|
|
110
|
+
try {
|
|
111
|
+
for await (const event of session.runTurn(prompt, { ...runOptions, signal: controller.signal })) {
|
|
112
|
+
events.push(event);
|
|
113
|
+
if (events.length >= MAX_BUFFERED_EVENTS) {
|
|
114
|
+
controller.abort();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
res.off('close', onClose);
|
|
121
|
+
}
|
|
122
|
+
const finalAssistant = events.findLast((e) => e.type === 'assistant_message');
|
|
123
|
+
const assistant = finalAssistant && finalAssistant.type === 'assistant_message' ? finalAssistant.content : '';
|
|
124
|
+
return { events, assistant };
|
|
125
|
+
}
|
|
126
|
+
export async function handleTurn(req, res, ctx) {
|
|
127
|
+
if (!checkAuth(req, ctx.authToken)) {
|
|
128
|
+
reply(res, 401, { error: 'unauthorized' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
const raw = await readBody(req);
|
|
134
|
+
body = turnRequestSchema.parse(JSON.parse(raw));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
reply(res, 400, { error: 'bad_request', message: err instanceof Error ? err.message : String(err) });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (ctx.turnLimiter && !ctx.turnLimiter.tryAcquire()) {
|
|
141
|
+
reply(res, 429, { error: 'too_many_turns', message: 'concurrent turn limit reached; retry shortly' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await driveTurn(ctx.session, body.prompt, {
|
|
147
|
+
...(body.model ? { model: body.model } : {}),
|
|
148
|
+
...(body.systemPrompt ? { systemPrompt: body.systemPrompt } : {}),
|
|
149
|
+
}, res);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
reply(res, 500, { error: 'turn_failed', message: publicError(ctx, 'http turn failed', err) });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
ctx.turnLimiter?.release();
|
|
157
|
+
}
|
|
158
|
+
reply(res, 200, { events: result.events, assistant: result.assistant });
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Audio-in turn. Designed for iOS Shortcuts and curl: the client POSTs
|
|
162
|
+
* raw audio bytes with `Content-Type: audio/<format>`. Optional query
|
|
163
|
+
* params (`model`, `language`, `systemPrompt`) tune the run.
|
|
164
|
+
*
|
|
165
|
+
* The session must have an active Transcriber registered (e.g. via
|
|
166
|
+
* `@moxxy/plugin-stt-whisper`); without one the endpoint returns 503
|
|
167
|
+
* rather than transparently dropping the audio.
|
|
168
|
+
*/
|
|
169
|
+
export async function handleTurnAudio(req, res, ctx) {
|
|
170
|
+
if (!checkAuth(req, ctx.authToken)) {
|
|
171
|
+
reply(res, 401, { error: 'unauthorized' });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const transcriber = ctx.session.transcribers.tryGetActive();
|
|
175
|
+
if (!transcriber) {
|
|
176
|
+
reply(res, 503, {
|
|
177
|
+
error: 'no_transcriber',
|
|
178
|
+
message: 'No active Transcriber on this session. Install @moxxy/plugin-stt-whisper (or another transcriber plugin) and activate it before POSTing audio.',
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const contentType = (req.headers['content-type'] ?? '').toLowerCase();
|
|
183
|
+
if (!contentType.startsWith('audio/')) {
|
|
184
|
+
reply(res, 415, {
|
|
185
|
+
error: 'unsupported_media_type',
|
|
186
|
+
message: "Expected Content-Type: audio/* (e.g. audio/ogg, audio/m4a, audio/mpeg).",
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let bytes;
|
|
191
|
+
try {
|
|
192
|
+
bytes = await readRequestBody(req, DEFAULT_AUDIO_MAX);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
reply(res, 413, { error: 'payload_too_large', message: err instanceof Error ? err.message : String(err) });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (bytes.length === 0) {
|
|
199
|
+
reply(res, 400, { error: 'empty_body', message: 'audio body is empty' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Pull tuning params off the query string — keeping them out of the
|
|
203
|
+
// body lets the payload remain raw audio (cleanest curl / Shortcut flow).
|
|
204
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
205
|
+
const model = url.searchParams.get('model') ?? undefined;
|
|
206
|
+
const language = url.searchParams.get('language') ?? undefined;
|
|
207
|
+
const promptHint = url.searchParams.get('prompt') ?? undefined;
|
|
208
|
+
const systemPrompt = url.searchParams.get('systemPrompt') ?? undefined;
|
|
209
|
+
let transcript;
|
|
210
|
+
try {
|
|
211
|
+
const result = await transcriber.transcribe(new Uint8Array(bytes), {
|
|
212
|
+
mimeType: contentType,
|
|
213
|
+
...(language ? { language } : {}),
|
|
214
|
+
...(promptHint ? { prompt: promptHint } : {}),
|
|
215
|
+
});
|
|
216
|
+
transcript = result.text.trim();
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
reply(res, 502, { error: 'transcription_failed', message: publicError(ctx, 'http audio transcription failed', err) });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!transcript) {
|
|
223
|
+
reply(res, 422, { error: 'empty_transcript', message: 'transcriber returned empty text' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (ctx.turnLimiter && !ctx.turnLimiter.tryAcquire()) {
|
|
227
|
+
reply(res, 429, { error: 'too_many_turns', message: 'concurrent turn limit reached; retry shortly' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let result;
|
|
231
|
+
try {
|
|
232
|
+
result = await driveTurn(ctx.session, transcript, {
|
|
233
|
+
...(model ? { model } : {}),
|
|
234
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
235
|
+
}, res);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
reply(res, 500, { error: 'turn_failed', message: publicError(ctx, 'http audio turn failed', err) });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
ctx.turnLimiter?.release();
|
|
243
|
+
}
|
|
244
|
+
reply(res, 200, { transcript, events: result.events, assistant: result.assistant });
|
|
245
|
+
}
|
|
246
|
+
export async function handleTurnStream(req, res, ctx) {
|
|
247
|
+
if (!checkAuth(req, ctx.authToken)) {
|
|
248
|
+
reply(res, 401, { error: 'unauthorized' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let body;
|
|
252
|
+
try {
|
|
253
|
+
const raw = await readBody(req);
|
|
254
|
+
body = turnRequestSchema.parse(JSON.parse(raw));
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
reply(res, 400, { error: 'bad_request', message: err instanceof Error ? err.message : String(err) });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (ctx.turnLimiter && !ctx.turnLimiter.tryAcquire()) {
|
|
261
|
+
reply(res, 429, { error: 'too_many_turns', message: 'concurrent turn limit reached; retry shortly' });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
res.writeHead(200, {
|
|
265
|
+
'content-type': 'text/event-stream',
|
|
266
|
+
'cache-control': 'no-cache',
|
|
267
|
+
connection: 'keep-alive',
|
|
268
|
+
});
|
|
269
|
+
// Abort the turn when the client hangs up — without this the model keeps
|
|
270
|
+
// generating (and billing) with nothing consuming the SSE stream.
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
const onClose = () => controller.abort();
|
|
273
|
+
res.on('close', onClose);
|
|
274
|
+
// Degrade, never crash: an unhandled 'error' on a ServerResponse is fatal
|
|
275
|
+
// (uncaughtException). Treat any response-socket error like a disconnect —
|
|
276
|
+
// abort the turn and let the finally clean up — instead of letting it
|
|
277
|
+
// propagate and take down the whole runner process.
|
|
278
|
+
const onError = (err) => {
|
|
279
|
+
ctx.logger?.warn('http stream response error', {
|
|
280
|
+
err: err instanceof Error ? err.message : String(err),
|
|
281
|
+
});
|
|
282
|
+
controller.abort();
|
|
283
|
+
};
|
|
284
|
+
res.on('error', onError);
|
|
285
|
+
// A stalled-but-open consumer (reads nothing, never closes the socket) would
|
|
286
|
+
// otherwise park `safeWrite` on 'drain' indefinitely, keeping the provider
|
|
287
|
+
// stream — and billing — alive forever. The request/keep-alive timeouts cover
|
|
288
|
+
// header/body reads, NOT an in-progress response. `res.setTimeout` fires on
|
|
289
|
+
// write-side socket inactivity; a live-but-slow consumer resets it on every
|
|
290
|
+
// flushed write, so only a true stall trips it. We abort (same as a close).
|
|
291
|
+
const stallMs = ctx.streamStallMs ?? DEFAULT_STREAM_STALL_MS;
|
|
292
|
+
if (stallMs > 0) {
|
|
293
|
+
res.setTimeout(stallMs, () => {
|
|
294
|
+
ctx.logger?.warn('http stream consumer stalled; aborting turn', { stallMs });
|
|
295
|
+
controller.abort();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// Respect TCP backpressure: a slow/stalled-but-open consumer would otherwise
|
|
299
|
+
// let Node's internal write queue grow unbounded (a tool-heavy turn emits
|
|
300
|
+
// thousands of large events) until OOM. When `write` returns false we pause
|
|
301
|
+
// until 'drain', escaping early if the turn is aborted (client gone/stalled).
|
|
302
|
+
const safeWrite = async (chunk) => {
|
|
303
|
+
if (res.writableEnded || res.destroyed)
|
|
304
|
+
return;
|
|
305
|
+
if (res.write(chunk))
|
|
306
|
+
return;
|
|
307
|
+
if (controller.signal.aborted)
|
|
308
|
+
return;
|
|
309
|
+
await new Promise((resolve) => {
|
|
310
|
+
const done = () => {
|
|
311
|
+
res.off('drain', done);
|
|
312
|
+
controller.signal.removeEventListener('abort', done);
|
|
313
|
+
resolve();
|
|
314
|
+
};
|
|
315
|
+
res.once('drain', done);
|
|
316
|
+
controller.signal.addEventListener('abort', done, { once: true });
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
for await (const event of ctx.session.runTurn(body.prompt, {
|
|
321
|
+
...(body.model ? { model: body.model } : {}),
|
|
322
|
+
...(body.systemPrompt ? { systemPrompt: body.systemPrompt } : {}),
|
|
323
|
+
signal: controller.signal,
|
|
324
|
+
})) {
|
|
325
|
+
await safeWrite(`data: ${JSON.stringify(event)}\n\n`);
|
|
326
|
+
}
|
|
327
|
+
await safeWrite('data: [DONE]\n\n');
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
await safeWrite(`event: error\ndata: ${JSON.stringify({ message: publicError(ctx, 'http stream turn failed', err) })}\n\n`);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
res.off('close', onClose);
|
|
334
|
+
res.off('error', onError);
|
|
335
|
+
// Disarm the stall timer so it can't fire (and abort) after the stream has
|
|
336
|
+
// already settled, and so the timer doesn't keep a handle alive.
|
|
337
|
+
if (stallMs > 0) {
|
|
338
|
+
try {
|
|
339
|
+
res.setTimeout(0);
|
|
340
|
+
}
|
|
341
|
+
catch { /* response already torn down */ }
|
|
342
|
+
}
|
|
343
|
+
ctx.turnLimiter?.release();
|
|
344
|
+
// The client may have hung up mid-stream; end() on an already-ended/
|
|
345
|
+
// destroyed response throws ERR_STREAM_WRITE_AFTER_END.
|
|
346
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
347
|
+
try {
|
|
348
|
+
res.end();
|
|
349
|
+
}
|
|
350
|
+
catch { /* socket already gone */ }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moxxy/plugin-channel-http",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Request-response HTTP channel for moxxy. POST /v1/turn drives one turn synchronously; POST /v1/turn/stream returns Server-Sent Events. Bearer-token auth + allow-list permission flow (no human in the loop).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"moxxy",
|
|
7
|
+
"agent",
|
|
8
|
+
"channel",
|
|
9
|
+
"http",
|
|
10
|
+
"api"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://moxxy.ai",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/moxxy-ai/moxxy/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/moxxy-ai/moxxy.git",
|
|
19
|
+
"directory": "packages/plugin-channel-http"
|
|
20
|
+
},
|
|
21
|
+
"author": "Michal Makowski <michal.makowski97@gmail.com>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"moxxy": {
|
|
40
|
+
"plugin": {
|
|
41
|
+
"entry": "./dist/index.js",
|
|
42
|
+
"kind": "channel"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"zod": "^3.24.0",
|
|
47
|
+
"@moxxy/sdk": "0.26.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.10.0",
|
|
51
|
+
"typescript": "^5.7.3",
|
|
52
|
+
"vitest": "^2.1.8",
|
|
53
|
+
"@moxxy/core": "0.26.0",
|
|
54
|
+
"@moxxy/mode-default": "0.26.0",
|
|
55
|
+
"@moxxy/testing": "0.0.44",
|
|
56
|
+
"@moxxy/runner": "0.2.30",
|
|
57
|
+
"@moxxy/tools-builtin": "0.0.39",
|
|
58
|
+
"@moxxy/tsconfig": "0.0.0",
|
|
59
|
+
"@moxxy/vitest-preset": "0.0.0"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsc -p tsconfig.json",
|
|
63
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"clean": "rm -rf dist .turbo"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-as-thin-client test: run the HttpChannel against a RemoteSession
|
|
3
|
+
* instead of a local Session. The session (provider + loop) lives in a
|
|
4
|
+
* RunnerServer; the HTTP channel is a separate "process" that attaches over a
|
|
5
|
+
* unix socket and proxies turns to the runner. This is the `moxxy http
|
|
6
|
+
* --attach` shape, end-to-end.
|
|
7
|
+
*/
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { Session, autoAllowResolver, silentLogger } from '@moxxy/core';
|
|
12
|
+
import { defineProvider, definePlugin } from '@moxxy/sdk';
|
|
13
|
+
import { FakeProvider, textReply } from '@moxxy/testing';
|
|
14
|
+
import { defaultModePlugin } from '@moxxy/mode-default';
|
|
15
|
+
import {
|
|
16
|
+
startRunnerServer,
|
|
17
|
+
connectRemoteSession,
|
|
18
|
+
type RunnerServer,
|
|
19
|
+
type RemoteSession,
|
|
20
|
+
} from '@moxxy/runner';
|
|
21
|
+
import { HttpChannel } from './channel.js';
|
|
22
|
+
import type { ChannelHandle } from '@moxxy/sdk';
|
|
23
|
+
|
|
24
|
+
function buildRunnerSession(): Session {
|
|
25
|
+
const provider = new FakeProvider({ script: [textReply('hello via the runner')] });
|
|
26
|
+
const session = new Session({
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
logger: silentLogger,
|
|
29
|
+
permissionResolver: autoAllowResolver,
|
|
30
|
+
});
|
|
31
|
+
session.pluginHost.registerStatic(
|
|
32
|
+
definePlugin({
|
|
33
|
+
name: 'http-attach-shim',
|
|
34
|
+
providers: [
|
|
35
|
+
defineProvider({
|
|
36
|
+
name: provider.name,
|
|
37
|
+
models: [...provider.models],
|
|
38
|
+
createClient: () => provider,
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
session.providers.setActive(provider.name);
|
|
44
|
+
session.pluginHost.registerStatic(defaultModePlugin);
|
|
45
|
+
return session;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tmpSocket(): string {
|
|
49
|
+
return path.join(os.tmpdir(), `moxxy-http-attach-${Math.random().toString(36).slice(2, 10)}.sock`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let server: RunnerServer | null = null;
|
|
53
|
+
let remote: RemoteSession | null = null;
|
|
54
|
+
let handle: ChannelHandle | null = null;
|
|
55
|
+
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
await handle?.stop();
|
|
58
|
+
await remote?.close();
|
|
59
|
+
await server?.close();
|
|
60
|
+
handle = null;
|
|
61
|
+
remote = null;
|
|
62
|
+
server = null;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('HttpChannel attached to a runner', () => {
|
|
66
|
+
it('POST /v1/turn drives a turn on the remote runner session', async () => {
|
|
67
|
+
const socketPath = tmpSocket();
|
|
68
|
+
server = await startRunnerServer(buildRunnerSession(), { socketPath });
|
|
69
|
+
remote = await connectRemoteSession({ socketPath, role: 'http' });
|
|
70
|
+
|
|
71
|
+
const channel = new HttpChannel({ port: 0, host: '127.0.0.1', allowedTools: [] });
|
|
72
|
+
handle = await channel.start({ session: remote });
|
|
73
|
+
|
|
74
|
+
const res = await fetch(`http://127.0.0.1:${channel.boundPort}/v1/turn`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'content-type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ prompt: 'say hi' }),
|
|
78
|
+
});
|
|
79
|
+
expect(res.status).toBe(200);
|
|
80
|
+
const body = (await res.json()) as { events: Array<{ type: string }>; assistant: string };
|
|
81
|
+
expect(body.assistant).toContain('hello via the runner');
|
|
82
|
+
expect(body.events.some((e) => e.type === 'user_prompt')).toBe(true);
|
|
83
|
+
expect(body.events.some((e) => e.type === 'assistant_message')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|