@a5c-ai/adapters-gateway 5.1.1-staging.52898ebfc24f

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.
Files changed (202) hide show
  1. package/README.md +20 -0
  2. package/dist/auth/bootstrap.d.ts +89 -0
  3. package/dist/auth/bootstrap.d.ts.map +1 -0
  4. package/dist/auth/bootstrap.js +222 -0
  5. package/dist/auth/bootstrap.js.map +1 -0
  6. package/dist/auth/hashing.d.ts +4 -0
  7. package/dist/auth/hashing.d.ts.map +1 -0
  8. package/dist/auth/hashing.js +27 -0
  9. package/dist/auth/hashing.js.map +1 -0
  10. package/dist/auth/middleware.d.ts +3 -0
  11. package/dist/auth/middleware.d.ts.map +1 -0
  12. package/dist/auth/middleware.js +17 -0
  13. package/dist/auth/middleware.js.map +1 -0
  14. package/dist/auth/tokens.d.ts +45 -0
  15. package/dist/auth/tokens.d.ts.map +1 -0
  16. package/dist/auth/tokens.js +186 -0
  17. package/dist/auth/tokens.js.map +1 -0
  18. package/dist/builtin-adapters.d.ts +17 -0
  19. package/dist/builtin-adapters.d.ts.map +1 -0
  20. package/dist/builtin-adapters.js +119 -0
  21. package/dist/builtin-adapters.js.map +1 -0
  22. package/dist/config.d.ts +37 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +97 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/fanout/client-conn.d.ts +20 -0
  27. package/dist/fanout/client-conn.d.ts.map +1 -0
  28. package/dist/fanout/client-conn.js +53 -0
  29. package/dist/fanout/client-conn.js.map +1 -0
  30. package/dist/fanout/subscriber.d.ts +12 -0
  31. package/dist/fanout/subscriber.d.ts.map +1 -0
  32. package/dist/fanout/subscriber.js +40 -0
  33. package/dist/fanout/subscriber.js.map +1 -0
  34. package/dist/index.d.ts +30 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +52 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/kanban/lib/config-loader.d.ts +29 -0
  39. package/dist/kanban/lib/config-loader.d.ts.map +1 -0
  40. package/dist/kanban/lib/config-loader.js +166 -0
  41. package/dist/kanban/lib/config-loader.js.map +1 -0
  42. package/dist/kanban/lib/config.d.ts +3 -0
  43. package/dist/kanban/lib/config.d.ts.map +1 -0
  44. package/dist/kanban/lib/config.js +6 -0
  45. package/dist/kanban/lib/config.js.map +1 -0
  46. package/dist/kanban/lib/create-global-registry.d.ts +28 -0
  47. package/dist/kanban/lib/create-global-registry.d.ts.map +1 -0
  48. package/dist/kanban/lib/create-global-registry.js +53 -0
  49. package/dist/kanban/lib/create-global-registry.js.map +1 -0
  50. package/dist/kanban/lib/dispatch-context-audit.d.ts +12 -0
  51. package/dist/kanban/lib/dispatch-context-audit.d.ts.map +1 -0
  52. package/dist/kanban/lib/dispatch-context-audit.js +44 -0
  53. package/dist/kanban/lib/dispatch-context-audit.js.map +1 -0
  54. package/dist/kanban/lib/error-handler.d.ts +28 -0
  55. package/dist/kanban/lib/error-handler.d.ts.map +1 -0
  56. package/dist/kanban/lib/error-handler.js +61 -0
  57. package/dist/kanban/lib/error-handler.js.map +1 -0
  58. package/dist/kanban/lib/global-registry.d.ts +49 -0
  59. package/dist/kanban/lib/global-registry.d.ts.map +1 -0
  60. package/dist/kanban/lib/global-registry.js +18 -0
  61. package/dist/kanban/lib/global-registry.js.map +1 -0
  62. package/dist/kanban/lib/parser.d.ts +36 -0
  63. package/dist/kanban/lib/parser.d.ts.map +1 -0
  64. package/dist/kanban/lib/parser.js +585 -0
  65. package/dist/kanban/lib/parser.js.map +1 -0
  66. package/dist/kanban/lib/path-resolver.d.ts +2 -0
  67. package/dist/kanban/lib/path-resolver.d.ts.map +1 -0
  68. package/dist/kanban/lib/path-resolver.js +16 -0
  69. package/dist/kanban/lib/path-resolver.js.map +1 -0
  70. package/dist/kanban/lib/review-service.d.ts +63 -0
  71. package/dist/kanban/lib/review-service.d.ts.map +1 -0
  72. package/dist/kanban/lib/review-service.js +571 -0
  73. package/dist/kanban/lib/review-service.js.map +1 -0
  74. package/dist/kanban/lib/run-cache.d.ts +36 -0
  75. package/dist/kanban/lib/run-cache.d.ts.map +1 -0
  76. package/dist/kanban/lib/run-cache.js +313 -0
  77. package/dist/kanban/lib/run-cache.js.map +1 -0
  78. package/dist/kanban/lib/server-init.d.ts +26 -0
  79. package/dist/kanban/lib/server-init.d.ts.map +1 -0
  80. package/dist/kanban/lib/server-init.js +179 -0
  81. package/dist/kanban/lib/server-init.js.map +1 -0
  82. package/dist/kanban/lib/services/automation-rule-service.d.ts +97 -0
  83. package/dist/kanban/lib/services/automation-rule-service.d.ts.map +1 -0
  84. package/dist/kanban/lib/services/automation-rule-service.js +806 -0
  85. package/dist/kanban/lib/services/automation-rule-service.js.map +1 -0
  86. package/dist/kanban/lib/services/automation-webhook-service.d.ts +44 -0
  87. package/dist/kanban/lib/services/automation-webhook-service.d.ts.map +1 -0
  88. package/dist/kanban/lib/services/automation-webhook-service.js +405 -0
  89. package/dist/kanban/lib/services/automation-webhook-service.js.map +1 -0
  90. package/dist/kanban/lib/services/backlog-query-service.d.ts +130 -0
  91. package/dist/kanban/lib/services/backlog-query-service.d.ts.map +1 -0
  92. package/dist/kanban/lib/services/backlog-query-service.js +1972 -0
  93. package/dist/kanban/lib/services/backlog-query-service.js.map +1 -0
  94. package/dist/kanban/lib/services/dispatch-context-label-service.d.ts +39 -0
  95. package/dist/kanban/lib/services/dispatch-context-label-service.d.ts.map +1 -0
  96. package/dist/kanban/lib/services/dispatch-context-label-service.js +160 -0
  97. package/dist/kanban/lib/services/dispatch-context-label-service.js.map +1 -0
  98. package/dist/kanban/lib/services/kanban-storage.d.ts +36 -0
  99. package/dist/kanban/lib/services/kanban-storage.d.ts.map +1 -0
  100. package/dist/kanban/lib/services/kanban-storage.js +26 -0
  101. package/dist/kanban/lib/services/kanban-storage.js.map +1 -0
  102. package/dist/kanban/lib/services/run-query-service.d.ts +79 -0
  103. package/dist/kanban/lib/services/run-query-service.d.ts.map +1 -0
  104. package/dist/kanban/lib/services/run-query-service.js +202 -0
  105. package/dist/kanban/lib/services/run-query-service.js.map +1 -0
  106. package/dist/kanban/lib/services/task-tag-service.d.ts +39 -0
  107. package/dist/kanban/lib/services/task-tag-service.d.ts.map +1 -0
  108. package/dist/kanban/lib/services/task-tag-service.js +145 -0
  109. package/dist/kanban/lib/services/task-tag-service.js.map +1 -0
  110. package/dist/kanban/lib/settings-section-storage.d.ts +13 -0
  111. package/dist/kanban/lib/settings-section-storage.d.ts.map +1 -0
  112. package/dist/kanban/lib/settings-section-storage.js +38 -0
  113. package/dist/kanban/lib/settings-section-storage.js.map +1 -0
  114. package/dist/kanban/lib/source-discovery.d.ts +10 -0
  115. package/dist/kanban/lib/source-discovery.d.ts.map +1 -0
  116. package/dist/kanban/lib/source-discovery.js +201 -0
  117. package/dist/kanban/lib/source-discovery.js.map +1 -0
  118. package/dist/kanban/lib/utils.d.ts +8 -0
  119. package/dist/kanban/lib/utils.d.ts.map +1 -0
  120. package/dist/kanban/lib/utils.js +116 -0
  121. package/dist/kanban/lib/utils.js.map +1 -0
  122. package/dist/kanban/lib/watcher.d.ts +14 -0
  123. package/dist/kanban/lib/watcher.d.ts.map +1 -0
  124. package/dist/kanban/lib/watcher.js +221 -0
  125. package/dist/kanban/lib/watcher.js.map +1 -0
  126. package/dist/kanban/lib/workspace-lifecycle.d.ts +68 -0
  127. package/dist/kanban/lib/workspace-lifecycle.d.ts.map +1 -0
  128. package/dist/kanban/lib/workspace-lifecycle.js +1085 -0
  129. package/dist/kanban/lib/workspace-lifecycle.js.map +1 -0
  130. package/dist/kanban/routes.d.ts +2 -0
  131. package/dist/kanban/routes.d.ts.map +1 -0
  132. package/dist/kanban/routes.js +1358 -0
  133. package/dist/kanban/routes.js.map +1 -0
  134. package/dist/kanban/types/breakpoint.d.ts +13 -0
  135. package/dist/kanban/types/breakpoint.d.ts.map +1 -0
  136. package/dist/kanban/types/breakpoint.js +3 -0
  137. package/dist/kanban/types/breakpoint.js.map +1 -0
  138. package/dist/kanban/types/index.d.ts +173 -0
  139. package/dist/kanban/types/index.d.ts.map +1 -0
  140. package/dist/kanban/types/index.js +3 -0
  141. package/dist/kanban/types/index.js.map +1 -0
  142. package/dist/logging.d.ts +7 -0
  143. package/dist/logging.d.ts.map +1 -0
  144. package/dist/logging.js +22 -0
  145. package/dist/logging.js.map +1 -0
  146. package/dist/notifications/types.d.ts +18 -0
  147. package/dist/notifications/types.d.ts.map +1 -0
  148. package/dist/notifications/types.js +2 -0
  149. package/dist/notifications/types.js.map +1 -0
  150. package/dist/notifications/webhook-out.d.ts +3 -0
  151. package/dist/notifications/webhook-out.d.ts.map +1 -0
  152. package/dist/notifications/webhook-out.js +55 -0
  153. package/dist/notifications/webhook-out.js.map +1 -0
  154. package/dist/pairing/short-code.d.ts +20 -0
  155. package/dist/pairing/short-code.d.ts.map +1 -0
  156. package/dist/pairing/short-code.js +50 -0
  157. package/dist/pairing/short-code.js.map +1 -0
  158. package/dist/protocol/errors.d.ts +10 -0
  159. package/dist/protocol/errors.d.ts.map +1 -0
  160. package/dist/protocol/errors.js +15 -0
  161. package/dist/protocol/errors.js.map +1 -0
  162. package/dist/protocol/frames.d.ts +107 -0
  163. package/dist/protocol/frames.d.ts.map +1 -0
  164. package/dist/protocol/frames.js +146 -0
  165. package/dist/protocol/frames.js.map +1 -0
  166. package/dist/protocol/v1.d.ts +111 -0
  167. package/dist/protocol/v1.d.ts.map +1 -0
  168. package/dist/protocol/v1.js +2 -0
  169. package/dist/protocol/v1.js.map +1 -0
  170. package/dist/runs/event-log-index.d.ts +29 -0
  171. package/dist/runs/event-log-index.d.ts.map +1 -0
  172. package/dist/runs/event-log-index.js +210 -0
  173. package/dist/runs/event-log-index.js.map +1 -0
  174. package/dist/runs/event-log.d.ts +25 -0
  175. package/dist/runs/event-log.d.ts.map +1 -0
  176. package/dist/runs/event-log.js +104 -0
  177. package/dist/runs/event-log.js.map +1 -0
  178. package/dist/runs/hook-broker.d.ts +18 -0
  179. package/dist/runs/hook-broker.d.ts.map +1 -0
  180. package/dist/runs/hook-broker.js +110 -0
  181. package/dist/runs/hook-broker.js.map +1 -0
  182. package/dist/runs/manager.d.ts +57 -0
  183. package/dist/runs/manager.d.ts.map +1 -0
  184. package/dist/runs/manager.js +757 -0
  185. package/dist/runs/manager.js.map +1 -0
  186. package/dist/runs/session-runtime.d.ts +8 -0
  187. package/dist/runs/session-runtime.d.ts.map +1 -0
  188. package/dist/runs/session-runtime.js +291 -0
  189. package/dist/runs/session-runtime.js.map +1 -0
  190. package/dist/runs/types.d.ts +55 -0
  191. package/dist/runs/types.d.ts.map +1 -0
  192. package/dist/runs/types.js +2 -0
  193. package/dist/runs/types.js.map +1 -0
  194. package/dist/server.d.ts +15 -0
  195. package/dist/server.d.ts.map +1 -0
  196. package/dist/server.js +702 -0
  197. package/dist/server.js.map +1 -0
  198. package/dist/static/webui-server.d.ts +2 -0
  199. package/dist/static/webui-server.d.ts.map +1 -0
  200. package/dist/static/webui-server.js +97 -0
  201. package/dist/static/webui-server.js.map +1 -0
  202. package/package.json +68 -0
package/dist/server.js ADDED
@@ -0,0 +1,702 @@
1
+ import * as http from 'node:http';
2
+ import { Hono } from 'hono';
3
+ import { WebSocketServer } from 'ws';
4
+ import { resolveWorkspaceDefaultCwd, WorkspaceService } from '@a5c-ai/comm-adapter';
5
+ import { BootstrapAuthService, MemoryBootstrapAuthStore, SqliteBootstrapAuthStore, } from './auth/bootstrap.js';
6
+ import { authenticateBearerToken } from './auth/middleware.js';
7
+ import { MemoryTokenStore, SqliteTokenStore } from './auth/tokens.js';
8
+ import { createGatewayRunClient, listRunnableGatewayAgents, listRunnableGatewayAgentNames } from './builtin-adapters.js';
9
+ import { ClientConn } from './fanout/client-conn.js';
10
+ import { createGatewayLogger } from './logging.js';
11
+ import { ShortCodeStore } from './pairing/short-code.js';
12
+ import { decodeFrame } from './protocol/frames.js';
13
+ import { GATEWAY_CLOSE_CODES } from './protocol/errors.js';
14
+ import { RunManager } from './runs/manager.js';
15
+ import { registerKanbanRoutes } from './kanban/routes.js';
16
+ import { resolveWebuiRoot, serveWebuiRequest } from './static/webui-server.js';
17
+ const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
18
+ function generateRunId() {
19
+ let out = '';
20
+ for (let index = 0; index < 26; index += 1) {
21
+ out += CROCKFORD[Math.floor(Math.random() * CROCKFORD.length)];
22
+ }
23
+ return out;
24
+ }
25
+ function nodeRequestToFetchRequest(req) {
26
+ const protocol = 'http';
27
+ const url = `${protocol}://${req.headers.host ?? '127.0.0.1'}${req.url ?? '/'}`;
28
+ return new Request(url, {
29
+ method: req.method,
30
+ headers: new Headers(req.headers),
31
+ body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req,
32
+ duplex: req.method === 'GET' || req.method === 'HEAD' ? undefined : 'half',
33
+ });
34
+ }
35
+ function writeFetchResponse(res, response) {
36
+ res.statusCode = response.status;
37
+ response.headers.forEach((value, key) => {
38
+ res.setHeader(key, value);
39
+ });
40
+ return response.arrayBuffer().then((buffer) => {
41
+ res.end(Buffer.from(buffer));
42
+ });
43
+ }
44
+ function createHelloFrame(serverVersion) {
45
+ return {
46
+ type: 'hello',
47
+ protocolVersions: ['1'],
48
+ serverVersion,
49
+ serverTime: new Date().toISOString(),
50
+ };
51
+ }
52
+ function resolveTokenStore(config) {
53
+ if (config.tokenStore)
54
+ return config.tokenStore;
55
+ if (config.tokenStoreKind === 'memory')
56
+ return new MemoryTokenStore();
57
+ return new SqliteTokenStore(config.tokenDbPath);
58
+ }
59
+ function resolveBootstrapAuthStore(config) {
60
+ if (config.tokenStoreKind === 'memory') {
61
+ return new MemoryBootstrapAuthStore();
62
+ }
63
+ return new SqliteBootstrapAuthStore(config.tokenDbPath);
64
+ }
65
+ function sanitizeAttachments(value) {
66
+ if (!Array.isArray(value)) {
67
+ return undefined;
68
+ }
69
+ const attachments = value.flatMap((entry) => {
70
+ if (!entry || typeof entry !== 'object') {
71
+ return [];
72
+ }
73
+ const record = entry;
74
+ return [{
75
+ filePath: typeof record['filePath'] === 'string' ? record['filePath'] : undefined,
76
+ url: typeof record['url'] === 'string' ? record['url'] : undefined,
77
+ base64: typeof record['base64'] === 'string' ? record['base64'] : undefined,
78
+ mimeType: typeof record['mimeType'] === 'string' ? record['mimeType'] : undefined,
79
+ name: typeof record['name'] === 'string' ? record['name'] : undefined,
80
+ }];
81
+ }).filter((attachment) => attachment.filePath || attachment.url || attachment.base64);
82
+ return attachments.length > 0 ? attachments : undefined;
83
+ }
84
+ function readApprovalMode(value) {
85
+ return value === 'yolo' || value === 'prompt' || value === 'deny' ? value : undefined;
86
+ }
87
+ function readPaginationOptions(requestUrl) {
88
+ const url = new URL(requestUrl);
89
+ const offsetRaw = Number.parseInt(url.searchParams.get('offset') ?? '0', 10);
90
+ const limitRaw = url.searchParams.get('limit');
91
+ const limitParsed = limitRaw == null || limitRaw.length === 0 ? null : Number.parseInt(limitRaw, 10);
92
+ return {
93
+ offset: Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 0,
94
+ limit: Number.isFinite(limitParsed) && limitParsed != null && limitParsed >= 0 ? limitParsed : null,
95
+ };
96
+ }
97
+ function paginateItems(items, options) {
98
+ const total = items.length;
99
+ const offset = Math.min(options.offset, total);
100
+ const limit = options.limit == null ? total : options.limit;
101
+ const paged = options.limit == null ? items : items.slice(offset, offset + limit);
102
+ return {
103
+ items: paged,
104
+ pagination: {
105
+ total,
106
+ offset,
107
+ limit,
108
+ hasMore: options.limit == null ? false : offset + paged.length < total,
109
+ },
110
+ };
111
+ }
112
+ function readTailFlag(requestUrl) {
113
+ const url = new URL(requestUrl);
114
+ const raw = url.searchParams.get('tail');
115
+ return raw === '1' || raw === 'true';
116
+ }
117
+ export function createGatewayServer(config, logger = createGatewayLogger()) {
118
+ const gatewayClient = config.client ?? createGatewayRunClient();
119
+ const tokenStore = resolveTokenStore(config);
120
+ const bootstrapAuthStore = resolveBootstrapAuthStore(config);
121
+ const bootstrapAuth = new BootstrapAuthService(config.bootstrapAuth, bootstrapAuthStore, tokenStore);
122
+ const runManager = new RunManager(config, logger);
123
+ const workspaceService = new WorkspaceService();
124
+ const webuiRoot = config.enableWebui ? resolveWebuiRoot(config.webuiRoot) : null;
125
+ const shortCodeStore = new ShortCodeStore();
126
+ const app = new Hono();
127
+ const server = http.createServer(async (req, res) => {
128
+ const response = await app.fetch(nodeRequestToFetchRequest(req));
129
+ await writeFetchResponse(res, response);
130
+ });
131
+ const wss = new WebSocketServer({ noServer: true });
132
+ const connections = new Set();
133
+ let started = false;
134
+ app.get('/healthz', (context) => context.json({
135
+ ok: true,
136
+ serverTime: new Date().toISOString(),
137
+ serverVersion: config.serverVersion,
138
+ runs: {
139
+ active: runManager.list().filter((entry) => entry.status === 'running').length,
140
+ },
141
+ }));
142
+ registerKanbanRoutes(app);
143
+ async function requireAuth(authorization) {
144
+ return await authenticateBearerToken(tokenStore, authorization);
145
+ }
146
+ async function resolveAvailableAgents() {
147
+ return await listRunnableGatewayAgentNames(gatewayClient);
148
+ }
149
+ async function resolveAvailableAgentDescriptors() {
150
+ return await listRunnableGatewayAgents(gatewayClient);
151
+ }
152
+ async function ensureAgentAvailable(agent) {
153
+ const availableAgents = await resolveAvailableAgents();
154
+ return availableAgents.includes(agent);
155
+ }
156
+ app.get('/api/v1/tokens', async (context) => {
157
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
158
+ if (!tokenRecord) {
159
+ return context.json({ error: 'unauthorized' }, 401);
160
+ }
161
+ return context.json({
162
+ authenticatedAs: tokenRecord.name,
163
+ tokens: await tokenStore.list(),
164
+ });
165
+ });
166
+ app.post('/api/v1/tokens', async (context) => {
167
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
168
+ if (!tokenRecord) {
169
+ return context.json({ error: 'unauthorized' }, 401);
170
+ }
171
+ const body = await context.req.json();
172
+ const created = await tokenStore.create({
173
+ name: typeof body['name'] === 'string' && body['name'].trim().length > 0 ? body['name'] : 'gateway-client',
174
+ ttlMs: typeof body['ttlMs'] === 'number' ? body['ttlMs'] : null,
175
+ });
176
+ return context.json(created, 201);
177
+ });
178
+ app.post('/api/v1/tokens/:id/revoke', async (context) => {
179
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
180
+ if (!tokenRecord) {
181
+ return context.json({ error: 'unauthorized' }, 401);
182
+ }
183
+ const revoked = await tokenStore.revoke(context.req.param('id'));
184
+ return context.json({ revoked });
185
+ });
186
+ app.post('/api/v1/bootstrap/login', async (context) => {
187
+ const bootstrapState = await bootstrapAuth.describe();
188
+ if (!bootstrapState.enabled) {
189
+ return context.json({ error: 'bootstrap_auth_disabled' }, 404);
190
+ }
191
+ const body = await context.req.json();
192
+ const issuedToken = await bootstrapAuth.login({
193
+ username: typeof body['username'] === 'string' ? body['username'] : '',
194
+ password: typeof body['password'] === 'string' ? body['password'] : '',
195
+ clientName: typeof body['clientName'] === 'string' ? body['clientName'] : null,
196
+ ttlMs: typeof body['ttlMs'] === 'number' ? body['ttlMs'] : null,
197
+ });
198
+ if (!issuedToken) {
199
+ return context.json({ error: 'invalid_bootstrap_credentials' }, 401);
200
+ }
201
+ return context.json({ issuedToken }, 201);
202
+ });
203
+ const listDispatches = async (context) => {
204
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
205
+ if (!tokenRecord) {
206
+ return context.json({ error: 'unauthorized' }, 401);
207
+ }
208
+ const paged = paginateItems(runManager.list(), readPaginationOptions(context.req.url));
209
+ return context.json({
210
+ dispatches: paged.items,
211
+ runs: paged.items,
212
+ pagination: paged.pagination,
213
+ });
214
+ };
215
+ app.get('/api/v1/dispatches', listDispatches);
216
+ app.get('/api/v1/runs', listDispatches);
217
+ app.get('/api/v1/sessions', async (context) => {
218
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
219
+ if (!tokenRecord) {
220
+ return context.json({ error: 'unauthorized' }, 401);
221
+ }
222
+ const paged = paginateItems(await runManager.listSessions(), readPaginationOptions(context.req.url));
223
+ return context.json({ sessions: paged.items, pagination: paged.pagination });
224
+ });
225
+ app.get('/api/v1/workspaces', async (context) => {
226
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
227
+ if (!tokenRecord) {
228
+ return context.json({ error: 'unauthorized' }, 401);
229
+ }
230
+ const inventory = await runManager.listWorkspaces();
231
+ const paged = paginateItems([...(inventory.workspaces ?? [])], readPaginationOptions(context.req.url));
232
+ return context.json({
233
+ ...inventory,
234
+ workspaces: paged.items,
235
+ pagination: paged.pagination,
236
+ });
237
+ });
238
+ app.post('/api/v1/workspaces', async (context) => {
239
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
240
+ if (!tokenRecord) {
241
+ return context.json({ error: 'unauthorized' }, 401);
242
+ }
243
+ const body = await context.req.json();
244
+ const action = typeof body['action'] === 'string' ? body['action'] : 'create';
245
+ if (action === 'create') {
246
+ const name = typeof body['name'] === 'string' ? body['name'] : '';
247
+ const repos = Array.isArray(body['repos']) ? body['repos'] : [];
248
+ const workspace = await workspaceService.createWorkspace({
249
+ name,
250
+ repos: repos.flatMap((entry) => {
251
+ if (!entry || typeof entry !== 'object' || typeof entry.path !== 'string') {
252
+ return [];
253
+ }
254
+ return [{
255
+ path: String(entry.path),
256
+ alias: typeof entry.alias === 'string'
257
+ ? String(entry.alias)
258
+ : undefined,
259
+ }];
260
+ }),
261
+ mode: body['mode'] === 'symlink' ? 'symlink' : 'worktree',
262
+ });
263
+ return context.json({ workspace: { ...workspace, defaultCwd: resolveWorkspaceDefaultCwd(workspace) } }, 201);
264
+ }
265
+ const workspaceId = typeof body['workspaceId'] === 'string' ? body['workspaceId'] : '';
266
+ if (!workspaceId) {
267
+ return context.json({ error: 'workspaceId_required' }, 400);
268
+ }
269
+ if (action === 'archive') {
270
+ return context.json({ workspace: await workspaceService.archiveWorkspace(workspaceId) });
271
+ }
272
+ if (action === 'cleanup') {
273
+ return context.json({ workspace: await workspaceService.cleanupWorkspace(workspaceId) });
274
+ }
275
+ if (action === 'recover') {
276
+ return context.json({ workspace: await workspaceService.recoverWorkspace(workspaceId) });
277
+ }
278
+ if (action === 'delete') {
279
+ await workspaceService.deleteWorkspace(workspaceId, { forceCleanup: body['force'] === true });
280
+ return context.json({ deleted: workspaceId });
281
+ }
282
+ return context.json({ error: 'unsupported_action' }, 400);
283
+ });
284
+ app.get('/api/v1/sessions/:sessionId', async (context) => {
285
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
286
+ if (!tokenRecord) {
287
+ return context.json({ error: 'unauthorized' }, 401);
288
+ }
289
+ const session = await runManager.getSession(context.req.param('sessionId'));
290
+ if (!session) {
291
+ return context.json({ error: 'not_found' }, 404);
292
+ }
293
+ return context.json(session);
294
+ });
295
+ app.get('/api/v1/sessions/:sessionId/full', async (context) => {
296
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
297
+ if (!tokenRecord) {
298
+ return context.json({ error: 'unauthorized' }, 401);
299
+ }
300
+ const session = await runManager.getSessionContent(context.req.param('sessionId'));
301
+ if (!session) {
302
+ return context.json({ error: 'not_found' }, 404);
303
+ }
304
+ return context.json(session);
305
+ });
306
+ app.post('/api/v1/sessions', async (context) => {
307
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
308
+ if (!tokenRecord) {
309
+ return context.json({ error: 'unauthorized' }, 401);
310
+ }
311
+ const body = await context.req.json();
312
+ const agent = String(body['agent'] ?? '');
313
+ if (!(await ensureAgentAvailable(agent))) {
314
+ return context.json({ error: 'agent_unavailable', agent }, 400);
315
+ }
316
+ const forkSessionId = typeof body['forkSessionId'] === 'string' ? body['forkSessionId'] : undefined;
317
+ const forkSourceSession = forkSessionId ? await runManager.getSession(forkSessionId) : null;
318
+ const run = await runManager.start({
319
+ agent,
320
+ model: typeof body['model'] === 'string' ? body['model'] : undefined,
321
+ prompt: typeof body['prompt'] === 'string' ? body['prompt'] : '',
322
+ cwd: typeof body['cwd'] === 'string'
323
+ ? body['cwd']
324
+ : forkSourceSession?.cwd ?? forkSourceSession?.workspace?.currentPath ?? forkSourceSession?.workspace?.workspaceDefaultCwd,
325
+ workspaceId: typeof body['workspaceId'] === 'string'
326
+ ? body['workspaceId']
327
+ : forkSourceSession?.workspaceId ?? forkSourceSession?.workspace?.workspaceId,
328
+ forkSessionId,
329
+ }, {
330
+ tokenId: tokenRecord.id,
331
+ name: tokenRecord.name,
332
+ remoteAddress: context.req.header('x-forwarded-for') ?? null,
333
+ });
334
+ return context.json({ run, sourceSessionId: forkSessionId ?? null }, 201);
335
+ });
336
+ app.get('/api/v1/sessions/:sessionId/messages', async (context) => {
337
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
338
+ if (!tokenRecord) {
339
+ return context.json({ error: 'unauthorized' }, 401);
340
+ }
341
+ const session = await runManager.getSessionContent(context.req.param('sessionId'));
342
+ if (!session) {
343
+ return context.json({ error: 'not_found' }, 404);
344
+ }
345
+ const paginationOptions = readPaginationOptions(context.req.url);
346
+ const messages = Array.isArray(session.messages) ? [...session.messages] : [];
347
+ const tail = readTailFlag(context.req.url);
348
+ const effectiveOptions = tail && paginationOptions.limit != null
349
+ ? {
350
+ offset: Math.max(0, messages.length - paginationOptions.limit),
351
+ limit: paginationOptions.limit,
352
+ }
353
+ : paginationOptions;
354
+ const paged = paginateItems(messages, effectiveOptions);
355
+ return context.json({
356
+ sessionId: session.sessionId,
357
+ messages: paged.items,
358
+ pagination: paged.pagination,
359
+ });
360
+ });
361
+ app.post('/api/v1/sessions/:sessionId/messages', async (context) => {
362
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
363
+ if (!tokenRecord) {
364
+ return context.json({ error: 'unauthorized' }, 401);
365
+ }
366
+ const body = await context.req.json();
367
+ const sessionId = context.req.param('sessionId');
368
+ const prompt = typeof body['prompt'] === 'string' ? body['prompt'] : '';
369
+ const run = await runManager.sendSessionInput(sessionId, prompt, {
370
+ tokenId: tokenRecord.id,
371
+ name: tokenRecord.name,
372
+ remoteAddress: context.req.header('x-forwarded-for') ?? null,
373
+ }, {
374
+ agent: typeof body['agent'] === 'string' ? body['agent'] : undefined,
375
+ model: typeof body['model'] === 'string' ? body['model'] : undefined,
376
+ attachments: sanitizeAttachments(body['attachments']),
377
+ approvalMode: readApprovalMode(body['approvalMode']),
378
+ });
379
+ if (!run) {
380
+ return context.json({ error: 'not_found' }, 404);
381
+ }
382
+ const session = await runManager.getSession(sessionId);
383
+ return context.json({ run, session }, 200);
384
+ });
385
+ app.get('/api/v1/agents', async (context) => {
386
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
387
+ if (!tokenRecord) {
388
+ return context.json({ error: 'unauthorized' }, 401);
389
+ }
390
+ const agentDescriptors = await resolveAvailableAgentDescriptors();
391
+ const agents = agentDescriptors.map((entry) => entry.agent);
392
+ return context.json({ agents, agentDescriptors });
393
+ });
394
+ const createDispatch = async (context) => {
395
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
396
+ if (!tokenRecord) {
397
+ return context.json({ error: 'unauthorized' }, 401);
398
+ }
399
+ const body = await context.req.json();
400
+ const agent = String(body['agent'] ?? '');
401
+ if (!(await ensureAgentAvailable(agent))) {
402
+ return context.json({ error: 'agent_unavailable', agent }, 400);
403
+ }
404
+ const run = await runManager.start(body, {
405
+ tokenId: tokenRecord.id,
406
+ name: tokenRecord.name,
407
+ remoteAddress: context.req.header('x-forwarded-for') ?? null,
408
+ });
409
+ return context.json(run, 201);
410
+ };
411
+ app.post('/api/v1/dispatches', createDispatch);
412
+ app.post('/api/v1/runs', createDispatch);
413
+ const getDispatch = async (context) => {
414
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
415
+ if (!tokenRecord) {
416
+ return context.json({ error: 'unauthorized' }, 401);
417
+ }
418
+ const runId = context.req.param('runId') ?? '';
419
+ const run = runManager.get(runId);
420
+ if (!run) {
421
+ return context.json({ error: 'not_found' }, 404);
422
+ }
423
+ return context.json(run);
424
+ };
425
+ app.get('/api/v1/dispatches/:runId', getDispatch);
426
+ app.get('/api/v1/runs/:runId', getDispatch);
427
+ const stopDispatch = async (context) => {
428
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
429
+ if (!tokenRecord) {
430
+ return context.json({ error: 'unauthorized' }, 401);
431
+ }
432
+ const runId = context.req.param('runId') ?? '';
433
+ const stopped = await runManager.stop(runId);
434
+ return context.json({ stopped }, stopped ? 200 : 404);
435
+ };
436
+ app.post('/api/v1/dispatches/:runId/stop', stopDispatch);
437
+ app.post('/api/v1/runs/:runId/stop', stopDispatch);
438
+ app.post('/api/v1/pairing/register', async (context) => {
439
+ const tokenRecord = await requireAuth(context.req.header('authorization'));
440
+ if (!tokenRecord) {
441
+ return context.json({ error: 'unauthorized' }, 401);
442
+ }
443
+ const body = await context.req.json();
444
+ const record = shortCodeStore.register({
445
+ code: typeof body['code'] === 'string' ? body['code'] : undefined,
446
+ url: typeof body['url'] === 'string' ? body['url'] : new URL(context.req.url).origin,
447
+ token: typeof body['token'] === 'string' ? body['token'] : '',
448
+ ttlMs: typeof body['ttlMs'] === 'number' ? body['ttlMs'] : undefined,
449
+ });
450
+ return context.json({ code: record.code, url: record.url, expiresAt: record.expiresAt }, 201);
451
+ });
452
+ app.post('/api/v1/pairing/consume', async (context) => {
453
+ const body = await context.req.json();
454
+ const code = typeof body['code'] === 'string' ? body['code'] : '';
455
+ const record = shortCodeStore.consume(code);
456
+ if (!record) {
457
+ return context.json({ error: 'not_found' }, 404);
458
+ }
459
+ return context.json(record);
460
+ });
461
+ app.get('*', async (context) => {
462
+ if (!config.enableWebui) {
463
+ return context.text('Not Found', 404);
464
+ }
465
+ const response = await serveWebuiRequest(context.req.path, webuiRoot);
466
+ return response ?? context.text('Not Found', 404);
467
+ });
468
+ server.on('upgrade', async (req, socket, head) => {
469
+ const initialRecord = await authenticateBearerToken(tokenStore, req.headers.authorization);
470
+ wss.handleUpgrade(req, socket, head, (ws) => {
471
+ const conn = new ClientConn(ws, config.maxPendingFrames, initialRecord);
472
+ let activeTokenRecord = initialRecord ?? null;
473
+ connections.add(conn);
474
+ const authTimeout = setTimeout(() => {
475
+ if (!conn.authenticated) {
476
+ conn.close(GATEWAY_CLOSE_CODES.unauthorized, 'auth timeout');
477
+ }
478
+ }, config.unauthenticatedTimeoutMs);
479
+ if (initialRecord) {
480
+ conn.send(createHelloFrame(config.serverVersion));
481
+ }
482
+ ws.on('message', async (rawData) => {
483
+ let rawFrame;
484
+ try {
485
+ rawFrame = JSON.parse(rawData.toString());
486
+ }
487
+ catch (error) {
488
+ conn.close(GATEWAY_CLOSE_CODES.invalidFrame, error instanceof Error ? error.message : 'invalid frame');
489
+ return;
490
+ }
491
+ const requestId = typeof rawFrame['id'] === 'string' ? rawFrame['id'] : null;
492
+ let frame;
493
+ try {
494
+ frame = decodeFrame(rawData.toString());
495
+ }
496
+ catch (error) {
497
+ if (activeTokenRecord && requestId && typeof rawFrame['type'] === 'string') {
498
+ const response = await handleRequestFrame(rawFrame, requestId, activeTokenRecord, conn);
499
+ if (response) {
500
+ conn.sendJson(response);
501
+ return;
502
+ }
503
+ }
504
+ logger.warn('Rejected websocket frame', {
505
+ requestId,
506
+ type: typeof rawFrame['type'] === 'string' ? rawFrame['type'] : null,
507
+ error: error instanceof Error ? error.message : 'invalid frame',
508
+ });
509
+ conn.close(GATEWAY_CLOSE_CODES.invalidFrame, error instanceof Error ? error.message : 'invalid frame');
510
+ return;
511
+ }
512
+ if (!conn.authenticated) {
513
+ if (frame.type !== 'auth') {
514
+ conn.close(GATEWAY_CLOSE_CODES.unauthorized, 'auth required');
515
+ return;
516
+ }
517
+ const authRecord = await tokenStore.verify(frame.token);
518
+ if (!authRecord) {
519
+ conn.close(GATEWAY_CLOSE_CODES.unauthorized, 'invalid token');
520
+ return;
521
+ }
522
+ await tokenStore.touch(authRecord.id);
523
+ conn.authenticate(authRecord);
524
+ activeTokenRecord = authRecord;
525
+ conn.send(createHelloFrame(config.serverVersion));
526
+ return;
527
+ }
528
+ if (frame.type === 'ping') {
529
+ conn.send({ type: 'pong' });
530
+ return;
531
+ }
532
+ if (frame.type === 'subscribe') {
533
+ void runManager.subscribe(conn, frame.runId, frame.sinceSeq ?? 0);
534
+ return;
535
+ }
536
+ if (frame.type === 'unsubscribe') {
537
+ runManager.unsubscribe(conn, frame.runId);
538
+ return;
539
+ }
540
+ if (frame.type === 'session.subscribe') {
541
+ void runManager.subscribeSession(conn, frame.sessionId);
542
+ return;
543
+ }
544
+ if (frame.type === 'session.unsubscribe') {
545
+ runManager.unsubscribeSession(conn, frame.sessionId);
546
+ return;
547
+ }
548
+ if (frame.type === 'hook.decision') {
549
+ const accepted = runManager.submitHookDecision(conn, frame);
550
+ if (requestId) {
551
+ conn.sendJson({ id: requestId, ok: accepted });
552
+ }
553
+ return;
554
+ }
555
+ if (activeTokenRecord && requestId) {
556
+ const response = await handleRequestFrame(rawFrame, requestId, activeTokenRecord, conn);
557
+ if (response) {
558
+ conn.sendJson(response);
559
+ return;
560
+ }
561
+ }
562
+ });
563
+ ws.on('close', () => {
564
+ clearTimeout(authTimeout);
565
+ runManager.removeConnection(conn);
566
+ connections.delete(conn);
567
+ });
568
+ });
569
+ });
570
+ return {
571
+ tokenStore,
572
+ runManager,
573
+ get address() {
574
+ const bound = server.address();
575
+ return {
576
+ host: config.host,
577
+ port: typeof bound === 'object' && bound ? bound.port : config.port,
578
+ };
579
+ },
580
+ async start() {
581
+ if (started)
582
+ return;
583
+ await bootstrapAuth.initialize();
584
+ await new Promise((resolve, reject) => {
585
+ server.once('error', reject);
586
+ server.listen(config.port, config.host, () => {
587
+ server.off('error', reject);
588
+ resolve();
589
+ });
590
+ });
591
+ started = true;
592
+ logger.info('Gateway server listening', {
593
+ host: config.host,
594
+ port: config.port,
595
+ });
596
+ },
597
+ async stop() {
598
+ if (!started)
599
+ return;
600
+ await runManager.shutdown();
601
+ for (const conn of connections) {
602
+ conn.close(1001, 'server shutdown');
603
+ }
604
+ await new Promise((resolve) => {
605
+ wss.close(() => resolve());
606
+ });
607
+ await new Promise((resolve, reject) => {
608
+ server.close((error) => {
609
+ if (error) {
610
+ reject(error);
611
+ return;
612
+ }
613
+ resolve();
614
+ });
615
+ });
616
+ const closeableStore = tokenStore;
617
+ closeableStore.close?.();
618
+ bootstrapAuthStore.close?.();
619
+ started = false;
620
+ },
621
+ };
622
+ async function handleRequestFrame(rawFrame, id, tokenRecord, conn) {
623
+ switch (rawFrame['type']) {
624
+ case 'agents.list':
625
+ return { id, agents: await resolveAvailableAgents() };
626
+ case 'session.start': {
627
+ const runId = typeof rawFrame['runId'] === 'string' && rawFrame['runId'].trim().length > 0
628
+ ? rawFrame['runId']
629
+ : generateRunId();
630
+ const agent = String(rawFrame['agent'] ?? '');
631
+ if (!(await ensureAgentAvailable(agent))) {
632
+ return { id, error: 'agent_unavailable', agent };
633
+ }
634
+ if (conn) {
635
+ await runManager.subscribe(conn, runId, 0);
636
+ }
637
+ const run = await runManager.start({
638
+ runId,
639
+ agent,
640
+ model: typeof rawFrame['model'] === 'string' ? rawFrame['model'] : undefined,
641
+ prompt: typeof rawFrame['prompt'] === 'string' ? rawFrame['prompt'] : '',
642
+ attachments: sanitizeAttachments(rawFrame['attachments']),
643
+ approvalMode: readApprovalMode(rawFrame['approvalMode']),
644
+ sessionId: typeof rawFrame['sessionId'] === 'string' ? rawFrame['sessionId'] : undefined,
645
+ cwd: typeof rawFrame['cwd'] === 'string' ? rawFrame['cwd'] : undefined,
646
+ workspaceId: typeof rawFrame['workspaceId'] === 'string' ? rawFrame['workspaceId'] : undefined,
647
+ forkSessionId: typeof rawFrame['forkSessionId'] === 'string' ? rawFrame['forkSessionId'] : undefined,
648
+ }, {
649
+ tokenId: tokenRecord.id,
650
+ name: tokenRecord.name,
651
+ remoteAddress: null,
652
+ });
653
+ return { id, run };
654
+ }
655
+ case 'run.stop': {
656
+ const stopped = await runManager.stop(String(rawFrame['runId'] ?? ''));
657
+ return { id, stopped };
658
+ }
659
+ case 'session.message': {
660
+ const sessionId = String(rawFrame['sessionId'] ?? '');
661
+ const run = await runManager.sendSessionInput(sessionId, String(rawFrame['prompt'] ?? ''), {
662
+ tokenId: tokenRecord.id,
663
+ name: tokenRecord.name,
664
+ remoteAddress: null,
665
+ }, {
666
+ agent: typeof rawFrame['agent'] === 'string' ? rawFrame['agent'] : undefined,
667
+ model: typeof rawFrame['model'] === 'string' ? rawFrame['model'] : undefined,
668
+ attachments: sanitizeAttachments(rawFrame['attachments']),
669
+ approvalMode: readApprovalMode(rawFrame['approvalMode']),
670
+ });
671
+ if (!run) {
672
+ return { id, error: 'not_found' };
673
+ }
674
+ if (conn) {
675
+ await runManager.subscribe(conn, run.runId, 0);
676
+ await runManager.subscribeSession(conn, sessionId);
677
+ }
678
+ const session = await runManager.getSession(sessionId);
679
+ return { id, ok: true, run, session };
680
+ }
681
+ case 'pairing.register': {
682
+ const record = shortCodeStore.register({
683
+ code: typeof rawFrame['code'] === 'string' ? rawFrame['code'] : undefined,
684
+ url: typeof rawFrame['url'] === 'string' ? rawFrame['url'] : 'http://127.0.0.1:7878',
685
+ token: typeof rawFrame['token'] === 'string' ? rawFrame['token'] : '',
686
+ });
687
+ return { id, type: 'pairing.consumed', ...record };
688
+ }
689
+ case 'pairing.consume': {
690
+ const record = shortCodeStore.consume(String(rawFrame['code'] ?? ''));
691
+ return record ? { id, type: 'pairing.consumed', ...record } : { id, error: 'not_found' };
692
+ }
693
+ case 'session.subscribe':
694
+ return { id, ok: true };
695
+ case 'session.unsubscribe':
696
+ return { id, ok: true };
697
+ default:
698
+ return null;
699
+ }
700
+ }
701
+ }
702
+ //# sourceMappingURL=server.js.map