@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moxxy (moxxy.ai)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,50 @@
1
+ import type { ClientSession as Session } from '@moxxy/sdk';
2
+ import type { Channel, ChannelHandle, ChannelStartOptsBase, PermissionResolver } from '@moxxy/sdk';
3
+ export interface HttpChannelOptions {
4
+ readonly port?: number;
5
+ readonly host?: string;
6
+ /** Bearer token required on every protected route. If unset, auth is disabled (dev-only). */
7
+ readonly authToken?: string;
8
+ /** Max turns running concurrently on the shared session; excess requests get
9
+ * a 429. Bounds provider-stream fan-out and history interleaving. */
10
+ readonly maxConcurrentTurns?: number;
11
+ /** Abort an SSE turn whose consumer has stalled (socket open but not reading)
12
+ * for this many ms, so a half-open client can't keep a provider stream alive
13
+ * forever. A live-but-slow consumer resets the timer on every flushed write.
14
+ * Defaults to 120_000; set <= 0 to disable (not recommended for a network
15
+ * bind). */
16
+ readonly streamStallMs?: number;
17
+ /**
18
+ * Tool names that the model is allowed to call without further interaction.
19
+ * This is the entire permission story for HTTP — there's no human in the
20
+ * loop to click "allow", so the operator declares trust upfront. Anything
21
+ * not in this list is denied.
22
+ */
23
+ readonly allowedTools?: ReadonlyArray<string>;
24
+ readonly logger?: {
25
+ info?(msg: string, meta?: Record<string, unknown>): void;
26
+ warn?(msg: string, meta?: Record<string, unknown>): void;
27
+ };
28
+ }
29
+ export interface HttpStartOpts extends ChannelStartOptsBase {
30
+ readonly session: Session;
31
+ }
32
+ export declare class HttpChannel implements Channel<HttpStartOpts> {
33
+ readonly name = "http";
34
+ readonly permissionResolver: PermissionResolver;
35
+ private readonly port;
36
+ private readonly host;
37
+ private readonly authToken;
38
+ private readonly maxConcurrentTurns;
39
+ private readonly streamStallMs;
40
+ private readonly logger;
41
+ private server;
42
+ private boundPortValue;
43
+ /** The actual TCP port the server bound to after {@link start}. Equals the
44
+ * configured port, or the OS-assigned one when `port: 0` (ephemeral) — so
45
+ * callers (and tests) can address the server without guessing a free port. */
46
+ get boundPort(): number;
47
+ constructor(opts?: HttpChannelOptions);
48
+ start(startOpts: HttpStartOpts): Promise<ChannelHandle>;
49
+ }
50
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;0EACsE;IACtE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;iBAIa;IACb,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC1D,CAAC;CACH;AAED,MAAM,WAAW,aAAc,SAAQ,oBAAoB;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,qBAAa,WAAY,YAAW,OAAO,CAAC,aAAa,CAAC;IACxD,QAAQ,CAAC,IAAI,UAAU;IACvB,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,cAAc,CAAK;IAE3B;;mFAE+E;IAC/E,IAAI,SAAS,IAAI,MAAM,CAEtB;gBAEW,IAAI,GAAE,kBAAuB;IAoBnC,KAAK,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;CA6H9D"}
@@ -0,0 +1,164 @@
1
+ import { createServer } from 'node:http';
2
+ import { createAllowListResolver, denyByDefaultResolver } from '@moxxy/sdk';
3
+ import { routeRequest, TurnLimiter } from './router.js';
4
+ /** Hosts auth-disabled mode is only safe to bind on. A non-loopback bind with
5
+ * no token would expose an unauthenticated agent endpoint to the network. */
6
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']);
7
+ export class HttpChannel {
8
+ name = 'http';
9
+ permissionResolver;
10
+ port;
11
+ host;
12
+ authToken;
13
+ maxConcurrentTurns;
14
+ streamStallMs;
15
+ logger;
16
+ server = null;
17
+ boundPortValue = 0;
18
+ /** The actual TCP port the server bound to after {@link start}. Equals the
19
+ * configured port, or the OS-assigned one when `port: 0` (ephemeral) — so
20
+ * callers (and tests) can address the server without guessing a free port. */
21
+ get boundPort() {
22
+ return this.boundPortValue;
23
+ }
24
+ constructor(opts = {}) {
25
+ this.port = opts.port ?? 3737;
26
+ this.host = opts.host ?? '127.0.0.1';
27
+ this.authToken = opts.authToken ?? null;
28
+ this.maxConcurrentTurns =
29
+ typeof opts.maxConcurrentTurns === 'number' && opts.maxConcurrentTurns > 0
30
+ ? Math.floor(opts.maxConcurrentTurns)
31
+ : 4;
32
+ // undefined => router uses its DEFAULT_STREAM_STALL_MS; a finite number
33
+ // (including <= 0 to disable) is honored as-is.
34
+ this.streamStallMs =
35
+ typeof opts.streamStallMs === 'number' && Number.isFinite(opts.streamStallMs)
36
+ ? opts.streamStallMs
37
+ : undefined;
38
+ this.logger = opts.logger;
39
+ this.permissionResolver = opts.allowedTools && opts.allowedTools.length > 0
40
+ ? createAllowListResolver([...opts.allowedTools])
41
+ : denyByDefaultResolver;
42
+ }
43
+ async start(startOpts) {
44
+ // Guard against a double-start: unconditionally reassigning `this.server`
45
+ // would orphan the first server (its port stays bound, sockets stay open)
46
+ // since `stop()` only ever closes the most-recently-assigned one.
47
+ if (this.server) {
48
+ throw new Error('HttpChannel is already started — call stop() before starting again.');
49
+ }
50
+ // Refuse to expose an unauthenticated agent endpoint to the network. With
51
+ // no token the only gate is the deny-by-default tool resolver; a
52
+ // non-loopback bind would still let anyone reach /v1/turn.
53
+ if (this.authToken === null && !LOOPBACK_HOSTS.has(this.host)) {
54
+ throw new Error(`HttpChannel refuses to bind non-loopback host "${this.host}" without an authToken. ` +
55
+ 'Set authToken (or MOXXY_HTTP_TOKEN), or bind 127.0.0.1.');
56
+ }
57
+ const ctx = {
58
+ session: startOpts.session,
59
+ authToken: this.authToken,
60
+ logger: this.logger,
61
+ turnLimiter: new TurnLimiter(this.maxConcurrentTurns),
62
+ ...(this.streamStallMs !== undefined ? { streamStallMs: this.streamStallMs } : {}),
63
+ };
64
+ const server = createServer(async (req, res) => {
65
+ const handler = routeRequest(req);
66
+ if (!handler) {
67
+ res.writeHead(404, { 'content-type': 'application/json' });
68
+ res.end(JSON.stringify({ error: 'not_found', path: req.url }));
69
+ return;
70
+ }
71
+ try {
72
+ await handler(req, res, ctx);
73
+ }
74
+ catch (err) {
75
+ // Log the full error server-side; return a generic message so internal
76
+ // paths/provider details can't leak to a (possibly remote) caller.
77
+ this.logger?.warn?.('http handler threw', { err: String(err) });
78
+ if (!res.headersSent) {
79
+ res.writeHead(500, { 'content-type': 'application/json' });
80
+ res.end(JSON.stringify({ error: 'internal' }));
81
+ }
82
+ else {
83
+ try {
84
+ res.end();
85
+ }
86
+ catch { /* ignore */ }
87
+ }
88
+ }
89
+ });
90
+ // Tighten timeouts so a slow-loris client can't tie up sockets by dribbling
91
+ // headers/bodies. Defaults (requestTimeout 300s) are too generous for an
92
+ // agent endpoint. Cap total connections so a flood can't exhaust handles.
93
+ server.headersTimeout = 10_000;
94
+ server.requestTimeout = 30_000;
95
+ server.keepAliveTimeout = 5_000;
96
+ server.maxConnections = 256;
97
+ this.server = server;
98
+ // `running` rejects on a post-listen server error and resolves on a clean
99
+ // close. Declared up front so the persistent error handler installed below
100
+ // (after the listen-scoped one is detached) can settle it.
101
+ let rejectRunning;
102
+ let resolveRunning;
103
+ const running = new Promise((resolve, reject) => {
104
+ resolveRunning = resolve;
105
+ rejectRunning = reject;
106
+ });
107
+ // A fire-and-forget caller may never attach a .catch; without this default
108
+ // observer a post-listen server error escalates to a process-level
109
+ // unhandledRejection (fatal under --unhandled-rejections=strict). Real
110
+ // awaiters still see the rejection — a settled promise fans out to all.
111
+ running.catch(() => { });
112
+ const listening = new Promise((resolve, reject) => {
113
+ // Scoped to the listen handshake only — detached once we're listening so
114
+ // it can't no-op on the already-settled promise (and swallow runtime
115
+ // errors). The persistent handler below takes over afterwards.
116
+ const onListenError = (err) => reject(err);
117
+ server.once('error', onListenError);
118
+ server.listen(this.port, this.host, () => {
119
+ server.off('error', onListenError);
120
+ // A runtime server error after listen would otherwise be unhandled
121
+ // (Node re-throws an 'error' with no listener). Log it and fail the
122
+ // `running` promise so callers awaiting it observe the crash instead of
123
+ // mistaking it for a clean shutdown.
124
+ server.on('error', (err) => {
125
+ this.logger?.warn?.('http server error', { err: String(err) });
126
+ rejectRunning(err);
127
+ });
128
+ const addr = server.address();
129
+ this.boundPortValue = typeof addr === 'object' && addr ? addr.port : this.port;
130
+ this.logger?.info?.('http channel listening', {
131
+ host: this.host,
132
+ port: this.boundPortValue,
133
+ authEnabled: this.authToken !== null,
134
+ });
135
+ resolve();
136
+ });
137
+ });
138
+ await listening;
139
+ server.once('close', () => resolveRunning());
140
+ return {
141
+ running,
142
+ stop: async () => {
143
+ const srv = this.server;
144
+ // Clear the handle first so a concurrent/double stop() is a no-op rather
145
+ // than racing two close() calls on the same server (the second close
146
+ // gets an 'ERR_SERVER_NOT_RUNNING' error in its callback).
147
+ if (this.server === srv) {
148
+ this.server = null;
149
+ this.boundPortValue = 0;
150
+ }
151
+ await new Promise((resolve) => {
152
+ if (!srv)
153
+ return resolve();
154
+ srv.close((err) => {
155
+ if (err)
156
+ this.logger?.warn?.('http server close error', { err: String(err) });
157
+ resolve();
158
+ });
159
+ });
160
+ },
161
+ };
162
+ }
163
+ }
164
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAQ5E,OAAO,EAAE,YAAY,EAAE,WAAW,EAAsB,MAAM,aAAa,CAAC;AAE5E;8EAC8E;AAC9E,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAiCtF,MAAM,OAAO,WAAW;IACb,IAAI,GAAG,MAAM,CAAC;IACd,kBAAkB,CAAqB;IAC/B,IAAI,CAAS;IACb,IAAI,CAAS;IACb,SAAS,CAAgB;IACzB,kBAAkB,CAAS;IAC3B,aAAa,CAAqB;IAClC,MAAM,CAA+B;IAC9C,MAAM,GAAkB,IAAI,CAAC;IAC7B,cAAc,GAAG,CAAC,CAAC;IAE3B;;mFAE+E;IAC/E,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED,YAAY,OAA2B,EAAE;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,WAAW,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;QACxC,IAAI,CAAC,kBAAkB;YACrB,OAAO,IAAI,CAAC,kBAAkB,KAAK,QAAQ,IAAI,IAAI,CAAC,kBAAkB,GAAG,CAAC;gBACxE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC;gBACrC,CAAC,CAAC,CAAC,CAAC;QACR,wEAAwE;QACxE,gDAAgD;QAChD,IAAI,CAAC,aAAa;YAChB,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC;gBAC3E,CAAC,CAAC,IAAI,CAAC,aAAa;gBACpB,CAAC,CAAC,SAAS,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;YACzE,CAAC,CAAC,uBAAuB,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;YACjD,CAAC,CAAC,qBAAqB,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAwB;QAClC,0EAA0E;QAC1E,0EAA0E;QAC1E,kEAAkE;QAClE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACzF,CAAC;QAED,0EAA0E;QAC1E,iEAAiE;QACjE,2DAA2D;QAC3D,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CACb,kDAAkD,IAAI,CAAC,IAAI,0BAA0B;gBACnF,yDAAyD,CAC5D,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAkB;YACzB,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,MAAM,EAAE,IAAI,CAAC,MAAiC;YAC9C,WAAW,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC,kBAAkB,CAAC;YACrD,GAAG,CAAC,IAAI,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnF,CAAC;QAEF,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YAC7C,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC/D,OAAO;YACT,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAC/B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,uEAAuE;gBACvE,mEAAmE;gBACnE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAChE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;oBACrB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;oBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;gBACjD,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC;wBAAC,GAAG,CAAC,GAAG,EAAE,CAAC;oBAAC,CAAC;oBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;gBAC3C,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,4EAA4E;QAC5E,yEAAyE;QACzE,0EAA0E;QAC1E,MAAM,CAAC,cAAc,GAAG,MAAM,CAAC;QAC/B,MAAM,CAAC,cAAc,GAAG,MAAM,CAAC;QAC/B,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC;QAChC,MAAM,CAAC,cAAc,GAAG,GAAG,CAAC;QAE5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,0EAA0E;QAC1E,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAI,aAAsC,CAAC;QAC3C,IAAI,cAA2B,CAAC;QAChC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACpD,cAAc,GAAG,OAAO,CAAC;YACzB,aAAa,GAAG,MAAM,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,2EAA2E;QAC3E,mEAAmE;QACnE,uEAAuE;QACvE,wEAAwE;QACxE,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAExB,MAAM,SAAS,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACtD,yEAAyE;YACzE,qEAAqE;YACrE,+DAA+D;YAC/D,MAAM,aAAa,GAAG,CAAC,GAAY,EAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1D,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;YACpC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE;gBACvC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;gBACnC,mEAAmE;gBACnE,oEAAoE;gBACpE,wEAAwE;gBACxE,qCAAqC;gBACrC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAY,EAAE,EAAE;oBAClC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,mBAAmB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAC/D,aAAa,CAAC,GAAG,CAAC,CAAC;gBACrB,CAAC,CAAC,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC9B,IAAI,CAAC,cAAc,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;gBAC/E,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,wBAAwB,EAAE;oBAC5C,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,IAAI,EAAE,IAAI,CAAC,cAAc;oBACzB,WAAW,EAAE,IAAI,CAAC,SAAS,KAAK,IAAI;iBACrC,CAAC,CAAC;gBACH,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,SAAS,CAAC;QAEhB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAE7C,OAAO;YACL,OAAO;YACP,IAAI,EAAE,KAAK,IAAI,EAAE;gBACf,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC;gBACxB,yEAAyE;gBACzE,qEAAqE;gBACrE,2DAA2D;gBAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;oBACnB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;gBAC1B,CAAC;gBACD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;oBAClC,IAAI,CAAC,GAAG;wBAAE,OAAO,OAAO,EAAE,CAAC;oBAC3B,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;wBAChB,IAAI,GAAG;4BAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,yBAAyB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;wBAC9E,OAAO,EAAE,CAAC;oBACZ,CAAC,CAAC,CAAC;gBACL,CAAC,CAAC,CAAC;YACL,CAAC;SACF,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { type Plugin } from '@moxxy/sdk';
2
+ export { HttpChannel, type HttpChannelOptions, type HttpStartOpts } from './channel.js';
3
+ export { routeRequest, handleHealth, handleTurn, handleTurnStream, handleTurnAudio, turnRequestSchema, type TurnRequest, type RouterContext, type RouteHandler, } from './router.js';
4
+ export declare const httpChannelDef: import("@moxxy/sdk").ChannelDef<import("./channel.js").HttpStartOpts>;
5
+ export declare const httpChannelPlugin: Plugin;
6
+ export default httpChannelPlugin;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAGtE,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,cAAc,uEAiCzB,CAAC;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAI9B,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ import { defineChannel, definePlugin } from '@moxxy/sdk';
2
+ import { HttpChannel } from './channel.js';
3
+ export { HttpChannel } from './channel.js';
4
+ export { routeRequest, handleHealth, handleTurn, handleTurnStream, handleTurnAudio, turnRequestSchema, } from './router.js';
5
+ export const httpChannelDef = defineChannel({
6
+ name: 'http',
7
+ description: 'Request-response HTTP channel. POST /v1/turn + SSE streaming. Bearer-token auth + allow-list perms.',
8
+ create: (deps) => {
9
+ const opts = deps.options ?? {};
10
+ return new HttpChannel({
11
+ port: typeof opts.port === 'number' ? opts.port : undefined,
12
+ host: typeof opts.host === 'string' ? opts.host : undefined,
13
+ authToken: typeof opts.authToken === 'string' ? opts.authToken : process.env.MOXXY_HTTP_TOKEN,
14
+ allowedTools: Array.isArray(opts.allowedTools) ? opts.allowedTools : undefined,
15
+ maxConcurrentTurns: typeof opts.maxConcurrentTurns === 'number' ? opts.maxConcurrentTurns : undefined,
16
+ streamStallMs: typeof opts.streamStallMs === 'number' ? opts.streamStallMs : undefined,
17
+ logger: deps.logger,
18
+ });
19
+ },
20
+ isAvailable: async (deps) => {
21
+ const tools = deps.options?.['allowedTools'];
22
+ const authToken = deps.options?.['authToken'] ?? process.env.MOXXY_HTTP_TOKEN;
23
+ if (!authToken) {
24
+ return {
25
+ ok: false,
26
+ reason: "No auth token. Set MOXXY_HTTP_TOKEN or pass channels.http.authToken in moxxy.config.ts.",
27
+ };
28
+ }
29
+ if (!Array.isArray(tools) || tools.length === 0) {
30
+ return {
31
+ ok: false,
32
+ reason: 'No allowed-tools list. The HTTP channel needs an upfront tool allow-list since there is no human in the loop. Set channels.http.allowedTools in moxxy.config.ts.',
33
+ };
34
+ }
35
+ return { ok: true };
36
+ },
37
+ });
38
+ export const httpChannelPlugin = definePlugin({
39
+ name: '@moxxy/plugin-channel-http',
40
+ version: '0.0.0',
41
+ channels: [httpChannelDef],
42
+ });
43
+ export default httpChannelPlugin;
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAe,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,WAAW,EAA+C,MAAM,cAAc,CAAC;AACxF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAIlB,MAAM,aAAa,CAAC;AAErB,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAC;IAC1C,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,qGAAqG;IAClH,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;QAChC,OAAO,IAAI,WAAW,CAAC;YACrB,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YAC3D,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YAC3D,SAAS,EAAE,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB;YAC7F,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,YAAyB,CAAC,CAAC,CAAC,SAAS;YAC5F,kBAAkB,EAAE,OAAO,IAAI,CAAC,kBAAkB,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS;YACrG,aAAa,EAAE,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;YACtF,MAAM,EAAE,IAAI,CAAC,MAAe;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAC9E,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,yFAAyF;aAClG,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EACJ,kKAAkK;aACrK,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAW,YAAY,CAAC;IACpD,IAAI,EAAE,4BAA4B;IAClC,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE,CAAC,cAAc,CAAC;CAC3B,CAAC,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import type { ClientSession as Session } from '@moxxy/sdk';
4
+ import type { MoxxyEvent } from '@moxxy/sdk';
5
+ export declare const turnRequestSchema: z.ZodObject<{
6
+ prompt: z.ZodString;
7
+ model: z.ZodOptional<z.ZodString>;
8
+ systemPrompt: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ prompt: string;
11
+ model?: string | undefined;
12
+ systemPrompt?: string | undefined;
13
+ }, {
14
+ prompt: string;
15
+ model?: string | undefined;
16
+ systemPrompt?: string | undefined;
17
+ }>;
18
+ export type TurnRequest = z.infer<typeof turnRequestSchema>;
19
+ /**
20
+ * Counting semaphore gating how many turns run concurrently on the single
21
+ * shared session. The HTTP server otherwise accepts unlimited parallel
22
+ * requests, each calling `session.runTurn` on the SAME session — N concurrent
23
+ * clients would spawn N provider streams (cost/memory/connection blowup) and
24
+ * interleave one shared conversation history. `tryAcquire` is non-blocking so
25
+ * excess requests get a fast 429 instead of queueing unbounded.
26
+ */
27
+ export declare class TurnLimiter {
28
+ private readonly max;
29
+ private inUse;
30
+ constructor(max: number);
31
+ tryAcquire(): boolean;
32
+ release(): void;
33
+ get active(): number;
34
+ }
35
+ export interface RouterContext {
36
+ readonly session: Session;
37
+ readonly authToken: string | null;
38
+ readonly logger?: {
39
+ warn(msg: string, meta?: Record<string, unknown>): void;
40
+ };
41
+ /** Bounds concurrent in-flight turns on the shared session. Optional so
42
+ * existing embedders/tests keep working unbounded; the channel always
43
+ * supplies one. */
44
+ readonly turnLimiter?: TurnLimiter;
45
+ /** Abort an SSE turn whose consumer has stalled (socket open but not reading)
46
+ * for this many ms. Bounds the backpressure park in {@link handleTurnStream}
47
+ * so a half-open client can't keep a provider stream (and billing) alive
48
+ * forever. Optional; defaults to {@link DEFAULT_STREAM_STALL_MS}. */
49
+ readonly streamStallMs?: number;
50
+ }
51
+ /** Default write-side idle timeout for an SSE turn. A live-but-slow consumer
52
+ * resets this on every flushed write; only a truly stalled (open-but-silent)
53
+ * reader trips it, at which point we abort the turn — the same outcome as a
54
+ * clean disconnect, just without waiting for a TCP close that may never come.
55
+ * Generous so normal inter-event gaps (model thinking, tool calls) don't trip
56
+ * it. */
57
+ export declare const DEFAULT_STREAM_STALL_MS = 120000;
58
+ export type RouteHandler = (req: IncomingMessage, res: ServerResponse, ctx: RouterContext) => Promise<void>;
59
+ /** Match HTTP request to a handler. Returns null if no route matches. */
60
+ export declare function routeRequest(req: IncomingMessage): RouteHandler | null;
61
+ export declare function handleHealth(_req: IncomingMessage, res: ServerResponse): Promise<void>;
62
+ /** Run options forwarded to `session.runTurn`, minus the abort `signal`
63
+ * (which `driveTurn` owns). */
64
+ export type TurnRunOptions = Omit<Parameters<Session['runTurn']>[1] & object, 'signal'>;
65
+ /** Hard cap on events buffered for a single non-streaming turn. The buffered
66
+ * path holds every event in memory and returns them in one JSON blob; a
67
+ * runaway/tool-looping turn could otherwise grow this without bound. Once
68
+ * exceeded we abort the turn rather than keep allocating. Generous enough that
69
+ * no normal turn hits it. */
70
+ export declare const MAX_BUFFERED_EVENTS = 50000;
71
+ /**
72
+ * Drive a single buffered (non-streaming) turn to completion: drain every
73
+ * event, abort the turn if the client hangs up (so the model stops billing
74
+ * with nobody listening), and pull out the final assistant message. Shared by
75
+ * `handleTurn` and `handleTurnAudio` so the abort wiring and event/assistant
76
+ * extraction live in exactly one place. Re-throws on turn failure so each
77
+ * caller can shape its own error reply.
78
+ */
79
+ export declare function driveTurn(session: Session, prompt: string, runOptions: TurnRunOptions, res: ServerResponse): Promise<{
80
+ events: MoxxyEvent[];
81
+ assistant: string;
82
+ }>;
83
+ export declare function handleTurn(req: IncomingMessage, res: ServerResponse, ctx: RouterContext): Promise<void>;
84
+ /**
85
+ * Audio-in turn. Designed for iOS Shortcuts and curl: the client POSTs
86
+ * raw audio bytes with `Content-Type: audio/<format>`. Optional query
87
+ * params (`model`, `language`, `systemPrompt`) tune the run.
88
+ *
89
+ * The session must have an active Transcriber registered (e.g. via
90
+ * `@moxxy/plugin-stt-whisper`); without one the endpoint returns 503
91
+ * rather than transparently dropping the audio.
92
+ */
93
+ export declare function handleTurnAudio(req: IncomingMessage, res: ServerResponse, ctx: RouterContext): Promise<void>;
94
+ export declare function handleTurnStream(req: IncomingMessage, res: ServerResponse, ctx: RouterContext): Promise<void>;
95
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;;;;GAOG;AACH,qBAAa,WAAW;IAEV,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,OAAO,CAAC,KAAK,CAAK;gBACW,GAAG,EAAE,MAAM;IACxC,UAAU,IAAI,OAAO;IAKrB,OAAO,IAAI,IAAI;IAGf,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9E;;wBAEoB;IACpB,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC;;;0EAGsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;UAKU;AACV,eAAO,MAAM,uBAAuB,SAAU,CAAC;AAE/C,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAgB5G,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,YAAY,GAAG,IAAI,CAWtE;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAGf;AAuBD;gCACgC;AAChC,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,QAAQ,CAAC,CAAC;AAExF;;;;8BAI8B;AAC9B,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,cAAc,EAC1B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBtD;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CAsFf;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CAyGf"}