@axiom-lattice/gateway 2.1.116 → 3.0.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.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +37 -0
  3. package/dist/{a2a-ERG5RMUW.mjs → a2a-QMHEH3ZT.mjs} +77 -29
  4. package/dist/a2a-QMHEH3ZT.mjs.map +1 -0
  5. package/dist/index.js +1144 -177
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1013 -92
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +6 -5
  10. package/src/__tests__/a2a.test.ts +82 -0
  11. package/src/__tests__/workspace.test.ts +13 -3
  12. package/src/config/local-a2a-templates.ts +59 -0
  13. package/src/controllers/__tests__/assistant-local-a2a.test.ts +147 -0
  14. package/src/controllers/__tests__/local-a2a-templates.test.ts +112 -0
  15. package/src/controllers/assistant.ts +126 -4
  16. package/src/controllers/local-a2a-templates.ts +18 -0
  17. package/src/controllers/local-a2a.ts +149 -0
  18. package/src/controllers/tasks.ts +2 -3
  19. package/src/controllers/workspace.ts +144 -17
  20. package/src/index.ts +2 -10
  21. package/src/routes/__tests__/a2a-test-connection.test.ts +177 -0
  22. package/src/routes/a2a-test-connection.ts +126 -0
  23. package/src/routes/a2a.ts +100 -28
  24. package/src/routes/index.ts +27 -0
  25. package/src/routes/stt.ts +102 -0
  26. package/src/services/__tests__/local-a2a.test.ts +124 -0
  27. package/src/services/eval-runner.ts +6 -0
  28. package/src/services/local-a2a.ts +352 -0
  29. package/src/utils/__tests__/local-a2a-runtime.test.ts +77 -0
  30. package/src/utils/__tests__/path-containment.test.ts +35 -0
  31. package/src/utils/a2a-url.ts +20 -0
  32. package/src/utils/local-a2a-runtime.ts +107 -0
  33. package/src/utils/path-containment.ts +38 -0
  34. package/dist/a2a-ERG5RMUW.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -12,19 +12,386 @@ import {
12
12
  } from "./chunk-LHQY46YB.mjs";
13
13
 
14
14
  // src/index.ts
15
+ import "dotenv/config";
15
16
  import fastify from "fastify";
16
17
  import cors from "@fastify/cors";
17
18
  import multipart from "@fastify/multipart";
18
19
  import sensible from "@fastify/sensible";
19
20
  import websocket from "@fastify/websocket";
20
21
  import staticPlugin from "@fastify/static";
21
- import path2 from "path";
22
+ import path3 from "path";
22
23
  import { fileURLToPath } from "url";
23
24
 
24
25
  // src/controllers/assistant.ts
25
26
  import { agentInstanceManager, getStoreLattice } from "@axiom-lattice/core";
27
+ import { AgentType } from "@axiom-lattice/protocols";
26
28
  import { randomUUID } from "crypto";
27
29
  import { agentLatticeManager, eventBus } from "@axiom-lattice/core";
30
+
31
+ // src/services/local-a2a.ts
32
+ import { spawn } from "child_process";
33
+ import { existsSync, statSync } from "fs";
34
+ import { dirname, join, parse, resolve } from "path";
35
+ import { setTimeout as delay } from "timers/promises";
36
+ var STDERR_TAIL_LIMIT = 4096;
37
+ function readBooleanEnv(name, defaultValue, env = process.env) {
38
+ const value = env[name];
39
+ if (value === void 0) return defaultValue;
40
+ return value === "1" || value.toLowerCase() === "true";
41
+ }
42
+ function findWorkspaceRoot(startDir) {
43
+ let current = resolve(startDir);
44
+ const { root } = parse(current);
45
+ while (current !== root) {
46
+ if (existsSync(join(current, "pnpm-workspace.yaml"))) {
47
+ return current;
48
+ }
49
+ const parent = dirname(current);
50
+ if (parent === current) return null;
51
+ current = parent;
52
+ }
53
+ }
54
+ function resolveLocalA2AWorkingDirectory(options) {
55
+ if (options.envWorkdir) return options.envWorkdir;
56
+ if (options.commandCwd) return options.commandCwd;
57
+ return findWorkspaceRoot(options.currentCwd) || options.currentCwd;
58
+ }
59
+ function validateWorkingDirectory(cwd) {
60
+ try {
61
+ const stat2 = statSync(cwd);
62
+ if (!stat2.isDirectory()) {
63
+ return `Working directory is not a directory: ${cwd}`;
64
+ }
65
+ return null;
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return `Working directory is not accessible: ${cwd} (${message})`;
69
+ }
70
+ }
71
+ function appendStderrTail(current, chunk) {
72
+ const next = `${current}${chunk}`;
73
+ if (next.length <= STDERR_TAIL_LIMIT) return next;
74
+ return next.slice(-STDERR_TAIL_LIMIT);
75
+ }
76
+ function formatExitMessage(exitReason, stderrTail) {
77
+ const stderr = stderrTail.trim();
78
+ return stderr ? `Exited before becoming ready (${exitReason}): ${stderr}` : `Exited before becoming ready (${exitReason})`;
79
+ }
80
+ async function probeReady(agentCardUrl, healthUrl, timeoutMs) {
81
+ const controller = new AbortController();
82
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
83
+ try {
84
+ if (healthUrl) {
85
+ const resp = await fetch(healthUrl, { signal: controller.signal });
86
+ if (!resp.ok) return false;
87
+ }
88
+ const cardResp = await fetch(agentCardUrl, { signal: controller.signal });
89
+ return cardResp.ok;
90
+ } catch {
91
+ return false;
92
+ } finally {
93
+ clearTimeout(timer);
94
+ }
95
+ }
96
+ var _LocalA2AService = class _LocalA2AService {
97
+ constructor() {
98
+ this.states = /* @__PURE__ */ new Map();
99
+ this.autoStartEnabled = readBooleanEnv("LOCAL_A2A_AUTO_START", true);
100
+ }
101
+ static getInstance() {
102
+ if (!_LocalA2AService.instance) {
103
+ _LocalA2AService.instance = new _LocalA2AService();
104
+ }
105
+ return _LocalA2AService.instance;
106
+ }
107
+ getOrCreateState(runtimeId, localRuntime) {
108
+ let state = this.states.get(runtimeId);
109
+ if (!state) {
110
+ state = {
111
+ runtimeId,
112
+ label: localRuntime.label || runtimeId,
113
+ status: this.autoStartEnabled ? "stopped" : "disabled",
114
+ enabled: this.autoStartEnabled,
115
+ assistantId: runtimeId,
116
+ agentCardUrl: localRuntime.agentCardUrl,
117
+ healthUrl: localRuntime.healthUrl,
118
+ localRuntime
119
+ };
120
+ this.states.set(runtimeId, state);
121
+ } else {
122
+ state.localRuntime = localRuntime;
123
+ state.label = localRuntime.label || state.label;
124
+ state.agentCardUrl = localRuntime.agentCardUrl;
125
+ state.healthUrl = localRuntime.healthUrl;
126
+ }
127
+ return state;
128
+ }
129
+ async getRegisteredState(runtimeId, localRuntime) {
130
+ const state = this.getOrCreateState(runtimeId, localRuntime);
131
+ if (state.process && state.status === "running") {
132
+ const alive = await probeReady(
133
+ localRuntime.agentCardUrl,
134
+ localRuntime.healthUrl,
135
+ 2e3
136
+ );
137
+ if (!alive) {
138
+ state.process = void 0;
139
+ state.pid = void 0;
140
+ state.status = "stopped";
141
+ state.message = "Process exited unexpectedly";
142
+ }
143
+ }
144
+ return this.snapshotState(state);
145
+ }
146
+ async startProvider(runtimeId, localRuntime) {
147
+ const state = this.getOrCreateState(runtimeId, localRuntime);
148
+ if (!state.enabled) {
149
+ state.status = "disabled";
150
+ state.message = "Disabled by configuration";
151
+ return this.snapshotState(state);
152
+ }
153
+ if (state.process && state.status === "running") {
154
+ return this.snapshotState(state);
155
+ }
156
+ state.enabled = true;
157
+ const { executable, args, cwd, env: cmdEnv } = localRuntime.command;
158
+ const resolvedCwd = resolveLocalA2AWorkingDirectory({
159
+ envWorkdir: process.env.LOCAL_A2A_WORKDIR || void 0,
160
+ commandCwd: cwd,
161
+ currentCwd: process.cwd()
162
+ });
163
+ const cwdError = validateWorkingDirectory(resolvedCwd);
164
+ if (cwdError) {
165
+ state.process = void 0;
166
+ state.pid = void 0;
167
+ state.status = "failed";
168
+ state.message = cwdError;
169
+ return this.snapshotState(state);
170
+ }
171
+ state.status = "starting";
172
+ state.message = "Starting";
173
+ let stderrTail = "";
174
+ const childEnv = cmdEnv ? { ...process.env, ...cmdEnv } : process.env;
175
+ const child = spawn(executable, args, {
176
+ cwd: resolvedCwd,
177
+ env: childEnv,
178
+ stdio: ["ignore", "pipe", "pipe"],
179
+ shell: false
180
+ });
181
+ state.process = child;
182
+ state.pid = child.pid ?? void 0;
183
+ child.once("error", (error) => {
184
+ const current = this.states.get(runtimeId);
185
+ if (!current) return;
186
+ current.process = void 0;
187
+ current.pid = void 0;
188
+ current.status = "failed";
189
+ const stderr = stderrTail.trim();
190
+ current.message = stderr ? `Failed to start: ${error.message}: ${stderr}` : `Failed to start: ${error.message}`;
191
+ });
192
+ child.stdout.on("data", (chunk) => {
193
+ const text = chunk.toString("utf8").trim();
194
+ if (text) console.log(`[local-a2a:${runtimeId}] ${text}`);
195
+ });
196
+ child.stderr.on("data", (chunk) => {
197
+ const rawText = chunk.toString("utf8");
198
+ stderrTail = appendStderrTail(stderrTail, rawText);
199
+ const text = rawText.trim();
200
+ if (text) console.error(`[local-a2a:${runtimeId}] ${text}`);
201
+ });
202
+ child.on("close", (code, signal) => {
203
+ const current = this.states.get(runtimeId);
204
+ if (!current) return;
205
+ current.process = void 0;
206
+ current.pid = void 0;
207
+ const exitReason = signal ?? code ?? "unknown";
208
+ if (current.status === "running") {
209
+ current.status = "stopped";
210
+ current.message = `Stopped (${exitReason})`;
211
+ } else if (current.status === "starting") {
212
+ current.status = "failed";
213
+ current.message = formatExitMessage(exitReason, stderrTail);
214
+ }
215
+ });
216
+ const ready = await this.waitUntilReady(localRuntime, state, 15e3);
217
+ if (!ready) {
218
+ if (state.process) {
219
+ state.process.kill("SIGTERM");
220
+ }
221
+ state.process = void 0;
222
+ state.pid = void 0;
223
+ if (state.status !== "failed") {
224
+ state.status = "failed";
225
+ state.message = "Server did not become ready";
226
+ }
227
+ return this.snapshotState(state);
228
+ }
229
+ state.status = "running";
230
+ state.message = "Running";
231
+ state.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
232
+ return this.snapshotState(state);
233
+ }
234
+ async stopProvider(runtimeId) {
235
+ const state = this.states.get(runtimeId);
236
+ if (!state) {
237
+ return this.emptyState(runtimeId);
238
+ }
239
+ const child = state.process;
240
+ if (child) {
241
+ child.kill("SIGTERM");
242
+ await delay(750);
243
+ if (child.exitCode === null) {
244
+ child.kill("SIGKILL");
245
+ }
246
+ state.process = void 0;
247
+ }
248
+ state.pid = void 0;
249
+ state.status = "stopped";
250
+ state.message = "Stopped";
251
+ state.lastStoppedAt = (/* @__PURE__ */ new Date()).toISOString();
252
+ return this.snapshotState(state);
253
+ }
254
+ async restartProvider(runtimeId, localRuntime) {
255
+ await this.stopProvider(runtimeId);
256
+ return this.startProvider(runtimeId, localRuntime);
257
+ }
258
+ removeState(runtimeId) {
259
+ const state = this.states.get(runtimeId);
260
+ if (state?.process) {
261
+ state.process.kill("SIGTERM");
262
+ state.process = void 0;
263
+ }
264
+ this.states.delete(runtimeId);
265
+ }
266
+ async waitUntilReady(localRuntime, state, timeoutMs) {
267
+ const start2 = Date.now();
268
+ while (Date.now() - start2 < timeoutMs) {
269
+ if (state.status === "failed") return false;
270
+ if (await probeReady(localRuntime.agentCardUrl, localRuntime.healthUrl, 1500)) {
271
+ return true;
272
+ }
273
+ await delay(500);
274
+ }
275
+ return false;
276
+ }
277
+ emptyState(runtimeId) {
278
+ return {
279
+ runtimeId,
280
+ label: runtimeId,
281
+ status: "stopped",
282
+ enabled: false
283
+ };
284
+ }
285
+ snapshotState(state) {
286
+ return {
287
+ runtimeId: state.runtimeId,
288
+ label: state.label,
289
+ status: state.status,
290
+ enabled: state.enabled,
291
+ assistantId: state.assistantId,
292
+ agentCardUrl: state.agentCardUrl,
293
+ healthUrl: state.healthUrl,
294
+ pid: state.pid,
295
+ message: state.message,
296
+ lastStartedAt: state.lastStartedAt,
297
+ lastStoppedAt: state.lastStoppedAt
298
+ };
299
+ }
300
+ };
301
+ _LocalA2AService.instance = null;
302
+ var LocalA2AService = _LocalA2AService;
303
+ var localA2AService = LocalA2AService.getInstance();
304
+
305
+ // src/utils/a2a-url.ts
306
+ var AGENT_CARD_PATH = "/.well-known/agent-card.json";
307
+ function normalizeA2AAgentCardUrl(value, defaultProtocol = "https://") {
308
+ const raw = value?.trim() ?? "";
309
+ if (!raw) return "";
310
+ const withProtocol = /^https?:\/\//i.test(raw) ? raw : `${defaultProtocol}${raw.replace(/^\/+/, "")}`;
311
+ const trimmed = withProtocol.replace(/\/+$/, "");
312
+ if (trimmed.endsWith(AGENT_CARD_PATH)) {
313
+ return trimmed;
314
+ }
315
+ return `${trimmed}${AGENT_CARD_PATH}`;
316
+ }
317
+
318
+ // src/utils/local-a2a-runtime.ts
319
+ function validateLocalRuntimeCommand(localRuntime) {
320
+ const runtime = localRuntime;
321
+ const executable = runtime.command?.executable;
322
+ if (typeof executable !== "string" || executable.trim().length === 0) {
323
+ return {
324
+ ok: false,
325
+ error: "Local managed A2A command executable is required"
326
+ };
327
+ }
328
+ const args = runtime.command?.args;
329
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
330
+ return {
331
+ ok: false,
332
+ error: "Local managed A2A command args must be an array of strings"
333
+ };
334
+ }
335
+ return { ok: true };
336
+ }
337
+ function getAgentCardPort(agentCardUrl) {
338
+ let url;
339
+ try {
340
+ url = new URL(agentCardUrl);
341
+ } catch {
342
+ return null;
343
+ }
344
+ if (url.port) {
345
+ const port = Number(url.port);
346
+ return Number.isInteger(port) ? port : null;
347
+ }
348
+ if (url.protocol === "http:") return 80;
349
+ if (url.protocol === "https:") return 443;
350
+ return null;
351
+ }
352
+ function parseCommandPort(args) {
353
+ for (let index = 0; index < args.length; index += 1) {
354
+ const arg = args[index];
355
+ const match = /^(--port|-p)(?:[=\s]+(\d+))?$/.exec(arg);
356
+ if (!match) continue;
357
+ const inlinePort = match[2];
358
+ if (inlinePort) return Number(inlinePort);
359
+ const nextArg = args[index + 1];
360
+ if (nextArg && /^\d+$/.test(nextArg)) {
361
+ return Number(nextArg);
362
+ }
363
+ }
364
+ return null;
365
+ }
366
+ function isLoopbackHost(hostname) {
367
+ const normalized = hostname.toLowerCase();
368
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
369
+ }
370
+ function validateLocalRuntimePort(localRuntime, agentCardUrl) {
371
+ let url;
372
+ try {
373
+ url = new URL(agentCardUrl);
374
+ } catch {
375
+ return { ok: true };
376
+ }
377
+ if (!isLoopbackHost(url.hostname) || !url.port) {
378
+ return { ok: true };
379
+ }
380
+ const commandPort = parseCommandPort(localRuntime.command.args);
381
+ if (commandPort === null) {
382
+ return { ok: true };
383
+ }
384
+ const agentCardPort = Number(url.port);
385
+ if (commandPort === agentCardPort) {
386
+ return { ok: true };
387
+ }
388
+ return {
389
+ ok: false,
390
+ error: `Local managed A2A command port (${commandPort}) does not match agentCardUrl port (${agentCardPort}). Update the command args or Agent Card URL so they use the same port.`
391
+ };
392
+ }
393
+
394
+ // src/controllers/assistant.ts
28
395
  function getTenantId(request) {
29
396
  const userTenantId = request.user?.tenantId;
30
397
  if (userTenantId) {
@@ -154,6 +521,79 @@ async function createAssistant(request, reply) {
154
521
  });
155
522
  }
156
523
  const id = data.id ?? randomUUID();
524
+ if (data.graphDefinition && data.graphDefinition.type === AgentType.A2A_REMOTE) {
525
+ const a2aConfig = data.graphDefinition;
526
+ const warnings = [];
527
+ const isLocalManaged = !!a2aConfig.localRuntime;
528
+ a2aConfig.agentCardUrl = normalizeA2AAgentCardUrl(
529
+ a2aConfig.agentCardUrl,
530
+ isLocalManaged ? "http://" : "https://"
531
+ );
532
+ if (a2aConfig.localRuntime) {
533
+ const commandValidation = validateLocalRuntimeCommand(
534
+ a2aConfig.localRuntime
535
+ );
536
+ if (!commandValidation.ok) {
537
+ return reply.status(400).send({
538
+ success: false,
539
+ message: commandValidation.error
540
+ });
541
+ }
542
+ a2aConfig.localRuntime.command.executable = a2aConfig.localRuntime.command.executable.trim();
543
+ a2aConfig.localRuntime.agentCardUrl = normalizeA2AAgentCardUrl(
544
+ a2aConfig.localRuntime.agentCardUrl || a2aConfig.agentCardUrl,
545
+ "http://"
546
+ );
547
+ a2aConfig.agentCardUrl = a2aConfig.localRuntime.agentCardUrl;
548
+ const portValidation = validateLocalRuntimePort(
549
+ a2aConfig.localRuntime,
550
+ a2aConfig.agentCardUrl
551
+ );
552
+ if (!portValidation.ok) {
553
+ return reply.status(400).send({
554
+ success: false,
555
+ message: portValidation.error
556
+ });
557
+ }
558
+ }
559
+ if (a2aConfig.localRuntime) {
560
+ const existingAssistants = await getStoreLattice("default", "assistant").store.getAllAssistants(tenantId);
561
+ const targetPort = getAgentCardPort(a2aConfig.localRuntime.agentCardUrl);
562
+ const duplicate = existingAssistants.find((a) => {
563
+ if (a.id === id) return false;
564
+ const gd = a.graphDefinition && typeof a.graphDefinition === "object" ? a.graphDefinition : void 0;
565
+ if (!gd || gd.type !== AgentType.A2A_REMOTE) return false;
566
+ const lr = gd.localRuntime;
567
+ if (!lr) return false;
568
+ const existingAgentCardUrl = normalizeA2AAgentCardUrl(lr.agentCardUrl, "http://");
569
+ const existingPort = getAgentCardPort(existingAgentCardUrl);
570
+ if (targetPort !== null && existingPort !== null) {
571
+ return existingPort === targetPort;
572
+ }
573
+ return existingAgentCardUrl === a2aConfig.localRuntime.agentCardUrl;
574
+ });
575
+ if (duplicate) {
576
+ const conflictDetail = targetPort === null ? `agentCardUrl "${a2aConfig.localRuntime.agentCardUrl}"` : `port ${targetPort}`;
577
+ return reply.status(409).send({
578
+ success: false,
579
+ message: `A local managed A2A assistant using ${conflictDetail} already exists: "${duplicate.name}". Each local managed A2A assistant needs a unique port.`
580
+ });
581
+ }
582
+ }
583
+ try {
584
+ const resp = await fetch(a2aConfig.agentCardUrl, {
585
+ signal: AbortSignal.timeout(5e3)
586
+ });
587
+ if (!resp.ok) {
588
+ warnings.push(`agentCardUrl returned HTTP ${resp.status}`);
589
+ }
590
+ } catch {
591
+ warnings.push(`agentCardUrl is not reachable: ${a2aConfig.agentCardUrl}`);
592
+ }
593
+ if (warnings.length > 0) {
594
+ console.warn(`[createAssistant] A2A warnings for ${a2aConfig.agentCardUrl}:`, warnings);
595
+ }
596
+ }
157
597
  return upsertAssistant(tenantId, id, data, reply, true);
158
598
  }
159
599
  async function updateAssistant(request, reply) {
@@ -167,11 +607,28 @@ async function deleteAssistant(request, reply) {
167
607
  const { id } = request.params;
168
608
  const storeLattice = getStoreLattice("default", "assistant");
169
609
  const assistantStore = storeLattice.store;
610
+ const storedAssistant = await assistantStore.getAssistantById(tenantId, id);
611
+ const graphDefinition = storedAssistant?.graphDefinition;
612
+ const hasLocalRuntime = graphDefinition && typeof graphDefinition === "object" ? !!graphDefinition.localRuntime : false;
613
+ if (storedAssistant && hasLocalRuntime) {
614
+ localA2AService.removeState(id);
615
+ const deleted2 = await assistantStore.deleteAssistant(tenantId, id);
616
+ if (!deleted2) {
617
+ return reply.status(500).send({
618
+ success: false,
619
+ message: "Failed to delete assistant"
620
+ });
621
+ }
622
+ eventBus.publish("assistant:deleted", { id, tenantId });
623
+ return {
624
+ success: true,
625
+ message: "Deleted local A2A assistant"
626
+ };
627
+ }
170
628
  const agentConfig = await agentLatticeManager.getAgentConfigWithTenant(tenantId, id);
171
629
  const isCodeConfigured = !!agentConfig;
172
630
  if (isCodeConfigured) {
173
- const exists2 = await assistantStore.hasAssistant(tenantId, id);
174
- if (!exists2) {
631
+ if (!storedAssistant) {
175
632
  return reply.status(404).send({
176
633
  success: false,
177
634
  message: "Assistant not found (code-configured assistants cannot be deleted from code, only from store)"
@@ -184,8 +641,7 @@ async function deleteAssistant(request, reply) {
184
641
  message: "Deleted assistant from store (code-configured registration remains)"
185
642
  };
186
643
  }
187
- const exists = await assistantStore.hasAssistant(tenantId, id);
188
- if (!exists) {
644
+ if (!storedAssistant) {
189
645
  return reply.status(404).send({
190
646
  success: false,
191
647
  message: "Assistant not found"
@@ -319,7 +775,7 @@ var createRun = async (request, reply) => {
319
775
 
320
776
  `);
321
777
  if (!success) {
322
- await new Promise((resolve) => reply.raw.once("drain", resolve));
778
+ await new Promise((resolve3) => reply.raw.once("drain", resolve3));
323
779
  }
324
780
  }
325
781
  } catch (error) {
@@ -2911,15 +3367,14 @@ function getStore() {
2911
3367
  async function listTasks(request, reply) {
2912
3368
  try {
2913
3369
  const tenantId = getTenantId9(request);
2914
- const userId = getUserId4(request);
2915
3370
  const query = request.query;
2916
3371
  const store = getStore();
2917
3372
  const tasks = await store.list({
2918
3373
  tenantId,
2919
- ownerId: query.ownerId || userId,
2920
3374
  status: query.status,
2921
3375
  priority: query.priority,
2922
- ownerType: query.ownerType,
3376
+ ...query.ownerId && { ownerId: query.ownerId },
3377
+ ...query.ownerType && { ownerType: query.ownerType },
2923
3378
  metadata: query.metadata ? JSON.parse(query.metadata) : void 0,
2924
3379
  projectId: query.projectId,
2925
3380
  limit: query.limit ? parseInt(query.limit) : void 0,
@@ -4054,12 +4509,41 @@ function registerSandboxProxyRoutes(app2) {
4054
4509
  }
4055
4510
 
4056
4511
  // src/controllers/workspace.ts
4057
- import * as fs from "fs/promises";
4058
- import * as path from "path";
4512
+ import * as fs2 from "fs/promises";
4513
+ import * as path2 from "path";
4514
+ import * as os from "os";
4059
4515
  import { getStoreLattice as getStoreLattice7 } from "@axiom-lattice/core";
4060
4516
  import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
4061
4517
  import { getSandBoxManager as getSandBoxManager3 } from "@axiom-lattice/core";
4062
4518
  import { v4 as uuidv42 } from "uuid";
4519
+
4520
+ // src/utils/path-containment.ts
4521
+ import * as fs from "fs/promises";
4522
+ import * as path from "path";
4523
+ var PathOutsideRootError = class extends Error {
4524
+ constructor(rootPath, targetPath) {
4525
+ super(`Path is outside the allowed root: ${targetPath}`);
4526
+ this.rootPath = rootPath;
4527
+ this.targetPath = targetPath;
4528
+ this.name = "PathOutsideRootError";
4529
+ }
4530
+ };
4531
+ function isPathWithinRoot(rootPath, targetPath) {
4532
+ const relative2 = path.relative(rootPath, targetPath);
4533
+ return relative2 === "" || relative2 !== ".." && !relative2.startsWith(`..${path.sep}`) && !path.isAbsolute(relative2);
4534
+ }
4535
+ async function resolvePathWithinRoot(rootPath, targetPath) {
4536
+ const [resolvedRoot, resolvedTarget] = await Promise.all([
4537
+ fs.realpath(rootPath),
4538
+ fs.realpath(targetPath)
4539
+ ]);
4540
+ if (!isPathWithinRoot(resolvedRoot, resolvedTarget)) {
4541
+ throw new PathOutsideRootError(resolvedRoot, resolvedTarget);
4542
+ }
4543
+ return { rootPath: resolvedRoot, targetPath: resolvedTarget };
4544
+ }
4545
+
4546
+ // src/controllers/workspace.ts
4063
4547
  var WorkspaceController = class {
4064
4548
  constructor() {
4065
4549
  this.workspaceStore = getStoreLattice7("default", "workspace").store;
@@ -4220,16 +4704,23 @@ var WorkspaceController = class {
4220
4704
  }),
4221
4705
  workspace
4222
4706
  };
4223
- } else {
4224
- console.log(`[getBackend] using FilesystemBackend rootDir=/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`);
4225
- return {
4226
- backend: new FilesystemBackend({
4227
- rootDir: `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`,
4228
- virtualMode: true
4229
- }),
4230
- workspace
4231
- };
4232
4707
  }
4708
+ const rootDir = await this.getFilesystemRootDirectory(
4709
+ tenantId,
4710
+ workspaceId,
4711
+ projectId
4712
+ );
4713
+ console.log(`[getBackend] using FilesystemBackend rootDir=${rootDir}`);
4714
+ return {
4715
+ backend: new FilesystemBackend({
4716
+ rootDir,
4717
+ virtualMode: true
4718
+ }),
4719
+ workspace
4720
+ };
4721
+ }
4722
+ async getFilesystemRootDirectory(tenantId, workspaceId, projectId) {
4723
+ return `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`;
4233
4724
  }
4234
4725
  isBinaryContentType(filename) {
4235
4726
  const ext = filename.split(".").pop()?.toLowerCase() || "";
@@ -4418,21 +4909,21 @@ var WorkspaceController = class {
4418
4909
  async listPath(request) {
4419
4910
  const tenantId = this.getTenantId(request);
4420
4911
  const { workspaceId, projectId } = request.params;
4421
- const path3 = request.query.path || "/";
4912
+ const path4 = request.query.path || "/";
4422
4913
  const assistantId = request.query.assistantId;
4423
- console.log(`[listPath] tenantId=${tenantId} workspaceId=${workspaceId} projectId=${projectId} path=${path3} assistantId=${assistantId}`);
4424
- const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path3);
4425
- console.log(`[listPath] backend type=${backend.constructor.name} calling lsInfo(${path3})`);
4426
- const files = await backend.lsInfo(path3);
4914
+ console.log(`[listPath] tenantId=${tenantId} workspaceId=${workspaceId} projectId=${projectId} path=${path4} assistantId=${assistantId}`);
4915
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path4);
4916
+ console.log(`[listPath] backend type=${backend.constructor.name} calling lsInfo(${path4})`);
4917
+ const files = await backend.lsInfo(path4);
4427
4918
  console.log(`[listPath] result count=${files.length}`);
4428
4919
  return { success: true, data: files };
4429
4920
  }
4430
4921
  async readFile(request) {
4431
4922
  const tenantId = this.getTenantId(request);
4432
4923
  const { workspaceId, projectId } = request.params;
4433
- const { path: path3, offset = 0, limit = 1e3, assistantId } = request.query;
4434
- const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path3);
4435
- const content = await backend.read(path3, Number(offset), Number(limit));
4924
+ const { path: path4, offset = 0, limit = 1e3, assistantId } = request.query;
4925
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path4);
4926
+ const content = await backend.read(path4, Number(offset), Number(limit));
4436
4927
  return { success: true, data: { content, offset, limit } };
4437
4928
  }
4438
4929
  /**
@@ -4444,7 +4935,7 @@ var WorkspaceController = class {
4444
4935
  * - `path`: optional directory path relative to project root (e.g., "docs", "src/utils")
4445
4936
  * - `assistantId`: optional query parameter for sandbox identity matching
4446
4937
  * For sandbox storage, delegates to the sandbox upload API.
4447
- * For filesystem storage, writes directly under /lattice_store/workspaces/{workspaceId}/{projectId}.
4938
+ * For filesystem storage, writes under the same project root used by the file backend.
4448
4939
  */
4449
4940
  async uploadFile(request, reply) {
4450
4941
  const tenantId = this.getTenantId(request);
@@ -4478,7 +4969,7 @@ var WorkspaceController = class {
4478
4969
  workspaceId,
4479
4970
  projectId
4480
4971
  };
4481
- const realPath = pathValue ? path.posix.join(pathValue, filename) : path.posix.join("/project/uploads", filename);
4972
+ const realPath = pathValue ? path2.posix.join(pathValue, filename) : path2.posix.join("/project/uploads", filename);
4482
4973
  const isBinary = this.isBinaryContentType(filename);
4483
4974
  const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, realPath);
4484
4975
  if (volumeBackend && !isBinary) {
@@ -4489,7 +4980,7 @@ var WorkspaceController = class {
4489
4980
  const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
4490
4981
  await sandbox.file.uploadFile({ file: realPath, data: buffer });
4491
4982
  }
4492
- const relativePath = pathValue ? path.posix.join(pathValue, filename) : path.posix.join("/project/uploads", filename);
4983
+ const relativePath = pathValue ? path2.posix.join(pathValue, filename) : path2.posix.join("/project/uploads", filename);
4493
4984
  const result2 = {
4494
4985
  path: relativePath,
4495
4986
  name: filename,
@@ -4499,14 +4990,43 @@ var WorkspaceController = class {
4499
4990
  };
4500
4991
  return reply.status(200).send({ success: true, data: result2 });
4501
4992
  }
4502
- const rootDir = `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`;
4503
- const targetDir = pathValue ? path.join(rootDir, pathValue) : rootDir;
4504
- const targetPath = path.join(targetDir, filename);
4505
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
4506
- await fs.writeFile(targetPath, buffer);
4507
- const stat2 = await fs.stat(targetPath);
4993
+ const rootDir = await this.getFilesystemRootDirectory(
4994
+ tenantId,
4995
+ workspaceId,
4996
+ projectId
4997
+ );
4998
+ await fs2.mkdir(rootDir, { recursive: true });
4999
+ const relativeDirectory = pathValue?.replace(/^[/\\]+/, "") || ".";
5000
+ const targetDir = path2.resolve(rootDir, relativeDirectory);
5001
+ if (!isPathWithinRoot(path2.resolve(rootDir), targetDir)) {
5002
+ return reply.status(400).send({
5003
+ success: false,
5004
+ error: "Upload path must stay within the project directory"
5005
+ });
5006
+ }
5007
+ await fs2.mkdir(targetDir, { recursive: true });
5008
+ const [resolvedRoot, resolvedTargetDir] = await Promise.all([
5009
+ fs2.realpath(rootDir),
5010
+ fs2.realpath(targetDir)
5011
+ ]);
5012
+ if (!isPathWithinRoot(resolvedRoot, resolvedTargetDir)) {
5013
+ return reply.status(400).send({
5014
+ success: false,
5015
+ error: "Upload path must stay within the project directory"
5016
+ });
5017
+ }
5018
+ const safeFilename = path2.basename(filename);
5019
+ if (safeFilename !== filename) {
5020
+ return reply.status(400).send({
5021
+ success: false,
5022
+ error: "Invalid filename"
5023
+ });
5024
+ }
5025
+ const targetPath = path2.join(resolvedTargetDir, safeFilename);
5026
+ await fs2.writeFile(targetPath, buffer);
5027
+ const stat2 = await fs2.stat(targetPath);
4508
5028
  const result = {
4509
- path: pathValue ? `/${pathValue}/${filename}` : `/${filename}`,
5029
+ path: path2.posix.join("/", relativeDirectory, safeFilename),
4510
5030
  name: filename,
4511
5031
  is_dir: false,
4512
5032
  size: stat2.size,
@@ -4514,9 +5034,63 @@ var WorkspaceController = class {
4514
5034
  };
4515
5035
  return reply.status(200).send({ success: true, data: result });
4516
5036
  } catch (error) {
5037
+ const message = error instanceof Error ? error.message : String(error);
4517
5038
  return reply.status(500).send({
4518
5039
  success: false,
4519
- error: `Upload handler error: ${error?.message || String(error)}`
5040
+ error: `Upload handler error: ${message}`
5041
+ });
5042
+ }
5043
+ }
5044
+ /**
5045
+ * Browse local filesystem directories for the folder picker.
5046
+ *
5047
+ * Security constraints:
5048
+ * - Restricted to os.homedir()
5049
+ * - Blacklists system directories
5050
+ * - Returns only directory names, no file contents or hidden files
5051
+ */
5052
+ async browseFilesystem(request, reply) {
5053
+ const homeDir = os.homedir();
5054
+ const requestedPath = request.query.path || homeDir;
5055
+ const SYSTEM_BLACKLIST = ["/etc", "/sys", "/proc", "/var", "/tmp", "/dev"];
5056
+ try {
5057
+ const resolved = await resolvePathWithinRoot(homeDir, requestedPath);
5058
+ const normalized = resolved.targetPath;
5059
+ for (const blacklisted of SYSTEM_BLACKLIST) {
5060
+ if (isPathWithinRoot(blacklisted, normalized)) {
5061
+ return reply.status(403).send({
5062
+ success: false,
5063
+ error: `Cannot browse system directory: ${blacklisted}`
5064
+ });
5065
+ }
5066
+ }
5067
+ const entries = await fs2.readdir(normalized, { withFileTypes: true });
5068
+ const dirEntries = entries.filter((entry) => !entry.name.startsWith(".") && entry.isDirectory()).map((entry) => ({
5069
+ name: entry.name,
5070
+ path: path2.join(normalized, entry.name),
5071
+ isDirectory: true
5072
+ }));
5073
+ const parentPath = path2.dirname(normalized);
5074
+ const hasParent = parentPath !== normalized && isPathWithinRoot(resolved.rootPath, parentPath);
5075
+ return {
5076
+ success: true,
5077
+ data: {
5078
+ path: normalized,
5079
+ parentPath: hasParent ? parentPath : void 0,
5080
+ entries: dirEntries
5081
+ }
5082
+ };
5083
+ } catch (error) {
5084
+ if (error instanceof PathOutsideRootError) {
5085
+ return reply.status(403).send({
5086
+ success: false,
5087
+ error: "Browse restricted to home directory"
5088
+ });
5089
+ }
5090
+ const message = error instanceof Error ? error.message : String(error);
5091
+ return reply.status(400).send({
5092
+ success: false,
5093
+ error: `Cannot read directory: ${message}`
4520
5094
  });
4521
5095
  }
4522
5096
  }
@@ -4577,6 +5151,10 @@ function registerWorkspaceRoutes(app2) {
4577
5151
  "/api/workspaces/:workspaceId/projects/:projectId/viewfile",
4578
5152
  controller.viewFile.bind(controller)
4579
5153
  );
5154
+ app2.get(
5155
+ "/api/filesystem/browse",
5156
+ controller.browseFilesystem.bind(controller)
5157
+ );
4580
5158
  }
4581
5159
 
4582
5160
  // src/controllers/database-configs.ts
@@ -4796,7 +5374,7 @@ async function testDatabaseConnection(request, reply) {
4796
5374
  await db.connect();
4797
5375
  await db.listTables();
4798
5376
  const latency = Date.now() - startTime;
4799
- await new Promise((resolve) => setTimeout(resolve, 100));
5377
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
4800
5378
  await db.disconnect();
4801
5379
  await sqlDatabaseManager.removeDatabase(tenantId, testKey);
4802
5380
  return {
@@ -5690,6 +6268,9 @@ var EvalRunner = class {
5690
6268
  const runId = uuidv43();
5691
6269
  const concurrency = Math.max(1, Math.floor(project.concurrency) || 3);
5692
6270
  await store.createRun(tenantId, projectId, runId, { totalCases, concurrency });
6271
+ const tsc = project.targetServerConfig ?? {};
6272
+ const workspaceId = tsc.workspace_id || "";
6273
+ const projectIdFromConfig = tsc.project_id || "";
5693
6274
  const projectConfig = {
5694
6275
  projectName: project.name,
5695
6276
  version: project.version,
@@ -5697,7 +6278,9 @@ var EvalRunner = class {
5697
6278
  suites: evalSuites,
5698
6279
  judge_agent_config: { modelKey: resolvedJudgeModelKey },
5699
6280
  lattice_server_config: {
5700
- tenant_id: tenantId
6281
+ tenant_id: tenantId,
6282
+ workspace_id: workspaceId,
6283
+ project_id: projectIdFromConfig
5701
6284
  },
5702
6285
  concurrency
5703
6286
  };
@@ -7109,8 +7692,8 @@ function getTenantId14(request) {
7109
7692
  return request.headers["x-tenant-id"] || "default";
7110
7693
  }
7111
7694
  async function getInstallationStore() {
7112
- const { getStoreLattice: getStoreLattice30 } = await import("@axiom-lattice/core");
7113
- const store = getStoreLattice30("default", "channelInstallation").store;
7695
+ const { getStoreLattice: getStoreLattice31 } = await import("@axiom-lattice/core");
7696
+ const store = getStoreLattice31("default", "channelInstallation").store;
7114
7697
  if (store) return store;
7115
7698
  const { PostgreSQLChannelInstallationStore } = await import("@axiom-lattice/pg-stores");
7116
7699
  const databaseUrl = process.env.DATABASE_URL;
@@ -7591,8 +8174,8 @@ var log = {
7591
8174
  };
7592
8175
  var connections = /* @__PURE__ */ new Map();
7593
8176
  var pending = /* @__PURE__ */ new Map();
7594
- function getAgentIdFromPath(path3) {
7595
- const match = path3.match(/^\/api\/a2a\/bridge\/proxy\/([^/]+)/);
8177
+ function getAgentIdFromPath(path4) {
8178
+ const match = path4.match(/^\/api\/a2a\/bridge\/proxy\/([^/]+)/);
7596
8179
  return match ? match[1] : null;
7597
8180
  }
7598
8181
  function handleBridgeConnection(socket, _request) {
@@ -7700,12 +8283,12 @@ async function handleBridgeProxy(request, reply) {
7700
8283
  };
7701
8284
  const timeout = 3e5;
7702
8285
  try {
7703
- const response = await new Promise((resolve, reject) => {
8286
+ const response = await new Promise((resolve3, reject) => {
7704
8287
  const timer = setTimeout(() => {
7705
8288
  pending.delete(requestId);
7706
8289
  reject(new Error(`Bridge request timeout after ${timeout}ms`));
7707
8290
  }, timeout);
7708
- pending.set(requestId, { resolve, reject, timer, agentId });
8291
+ pending.set(requestId, { resolve: resolve3, reject, timer, agentId });
7709
8292
  try {
7710
8293
  if (conn.ws.readyState !== conn.ws.OPEN) {
7711
8294
  pending.delete(requestId);
@@ -7750,9 +8333,352 @@ function registerA2ABridgeRoutes(app2) {
7750
8333
  });
7751
8334
  }
7752
8335
 
8336
+ // src/routes/stt.ts
8337
+ import { getSTTClient, sttModelLatticeManager } from "@axiom-lattice/core";
8338
+ var ALLOWED_AUDIO_FORMATS = /* @__PURE__ */ new Set([
8339
+ "webm",
8340
+ "wav",
8341
+ "mp3",
8342
+ "m4a",
8343
+ "ogg",
8344
+ "flac",
8345
+ "aac"
8346
+ ]);
8347
+ var MAX_AUDIO_FILE_SIZE = 25 * 1024 * 1024;
8348
+ var registerSTTRoutes = (app2) => {
8349
+ app2.get("/api/stt/models", async (_request, reply) => {
8350
+ const models = sttModelLatticeManager.getAllSTTModelInfo();
8351
+ return reply.send({ models });
8352
+ });
8353
+ app2.post(
8354
+ "/api/stt/transcribe",
8355
+ {
8356
+ schema: {
8357
+ querystring: {
8358
+ type: "object",
8359
+ properties: {
8360
+ model: { type: "string" }
8361
+ }
8362
+ },
8363
+ response: {
8364
+ 200: {
8365
+ type: "object",
8366
+ properties: {
8367
+ text: { type: "string" },
8368
+ confidence: { type: "number" }
8369
+ }
8370
+ }
8371
+ }
8372
+ }
8373
+ },
8374
+ async (request, reply) => {
8375
+ const modelKey = request.query.model || "default";
8376
+ const data = await request.file({
8377
+ limits: { fileSize: MAX_AUDIO_FILE_SIZE }
8378
+ });
8379
+ if (!data) {
8380
+ return reply.status(400).send({ error: "No audio file provided" });
8381
+ }
8382
+ const buffer = await data.toBuffer();
8383
+ if (buffer.length === 0) {
8384
+ return reply.status(400).send({ error: "Empty audio file" });
8385
+ }
8386
+ const format = (data.filename.split(".").pop() || "webm").toLowerCase();
8387
+ if (!ALLOWED_AUDIO_FORMATS.has(format)) {
8388
+ return reply.status(400).send({ error: `Unsupported audio format: ${format}` });
8389
+ }
8390
+ try {
8391
+ let client;
8392
+ try {
8393
+ client = getSTTClient(modelKey);
8394
+ } catch {
8395
+ return reply.status(404).send({ error: `STT model "${modelKey}" not found` });
8396
+ }
8397
+ const result = await client.transcribe(buffer, format);
8398
+ return reply.send({
8399
+ text: result.text,
8400
+ confidence: result.confidence
8401
+ });
8402
+ } catch (error) {
8403
+ const sanitizedMessage = error instanceof Error ? error.message : "Transcription failed";
8404
+ request.log.error(
8405
+ { error: sanitizedMessage },
8406
+ "STT transcription failed"
8407
+ );
8408
+ return reply.status(500).send({ error: "Transcription service error. Please try again." });
8409
+ }
8410
+ }
8411
+ );
8412
+ };
8413
+
8414
+ // src/controllers/local-a2a.ts
8415
+ import { getStoreLattice as getStoreLattice15 } from "@axiom-lattice/core";
8416
+ import {
8417
+ AgentType as AgentType2
8418
+ } from "@axiom-lattice/protocols";
8419
+ function getTenantId17(request) {
8420
+ const userTenantId = request.user?.tenantId;
8421
+ if (userTenantId) return userTenantId;
8422
+ return request.headers["x-tenant-id"] || "default";
8423
+ }
8424
+ function getLocalRuntime(assistant) {
8425
+ const graphDefinition = assistant.graphDefinition;
8426
+ if (!graphDefinition || typeof graphDefinition !== "object") return void 0;
8427
+ if (graphDefinition.type !== AgentType2.A2A_REMOTE) return void 0;
8428
+ return graphDefinition.localRuntime;
8429
+ }
8430
+ async function getLocalA2AAssistants(tenantId) {
8431
+ const storeLattice = getStoreLattice15("default", "assistant");
8432
+ const assistants = await storeLattice.store.getAllAssistants(tenantId);
8433
+ const results = [];
8434
+ for (const assistant of assistants) {
8435
+ const runtime = getLocalRuntime(assistant);
8436
+ if (runtime) {
8437
+ results.push({ assistant, localRuntime: runtime });
8438
+ }
8439
+ }
8440
+ return results;
8441
+ }
8442
+ async function getLocalA2AAssistant(tenantId, assistantId) {
8443
+ const storeLattice = getStoreLattice15("default", "assistant");
8444
+ const assistant = await storeLattice.store.getAssistantById(tenantId, assistantId);
8445
+ if (!assistant) return null;
8446
+ const runtime = getLocalRuntime(assistant);
8447
+ if (!runtime) return null;
8448
+ return { assistant, localRuntime: runtime };
8449
+ }
8450
+ async function requireManagedAssistant(request, reply) {
8451
+ const assistantId = request.params.assistantId;
8452
+ if (!assistantId) {
8453
+ reply.status(400).send({ success: false, error: "assistantId is required" });
8454
+ return null;
8455
+ }
8456
+ const registered = await getLocalA2AAssistant(getTenantId17(request), assistantId);
8457
+ if (!registered) {
8458
+ reply.status(404).send({
8459
+ success: false,
8460
+ error: `Assistant ${assistantId} is not a local managed A2A assistant`
8461
+ });
8462
+ return null;
8463
+ }
8464
+ return { assistantId, localRuntime: registered.localRuntime };
8465
+ }
8466
+ async function getLocalA2AStatus(request, reply) {
8467
+ const tenantId = getTenantId17(request);
8468
+ const managed = await getLocalA2AAssistants(tenantId);
8469
+ const states = await Promise.all(
8470
+ managed.map(
8471
+ ({ assistant, localRuntime }) => localA2AService.getRegisteredState(assistant.id, localRuntime)
8472
+ )
8473
+ );
8474
+ reply.send({ success: true, data: { records: states, total: states.length } });
8475
+ }
8476
+ async function getLocalA2AProvider(request, reply) {
8477
+ const registered = await requireManagedAssistant(request, reply);
8478
+ if (!registered) return;
8479
+ const state = await localA2AService.getRegisteredState(
8480
+ registered.assistantId,
8481
+ registered.localRuntime
8482
+ );
8483
+ reply.send({ success: true, data: state });
8484
+ }
8485
+ async function startLocalA2AProvider(request, reply) {
8486
+ const registered = await requireManagedAssistant(request, reply);
8487
+ if (!registered) return;
8488
+ const state = await localA2AService.startProvider(
8489
+ registered.assistantId,
8490
+ registered.localRuntime
8491
+ );
8492
+ reply.send({ success: true, data: state });
8493
+ }
8494
+ async function stopLocalA2AProvider(request, reply) {
8495
+ const registered = await requireManagedAssistant(request, reply);
8496
+ if (!registered) return;
8497
+ const state = await localA2AService.stopProvider(registered.assistantId);
8498
+ reply.send({ success: true, data: state });
8499
+ }
8500
+ async function restartLocalA2AProvider(request, reply) {
8501
+ const registered = await requireManagedAssistant(request, reply);
8502
+ if (!registered) return;
8503
+ const state = await localA2AService.restartProvider(
8504
+ registered.assistantId,
8505
+ registered.localRuntime
8506
+ );
8507
+ reply.send({ success: true, data: state });
8508
+ }
8509
+
8510
+ // src/config/local-a2a-templates.ts
8511
+ import { cliA2AEntryPath } from "@axiom-lattice/cli-a2a/cli-path";
8512
+ function buildCliA2ACommand(provider, agentName, agentDescription) {
8513
+ return {
8514
+ executable: process.execPath,
8515
+ args: [
8516
+ cliA2AEntryPath,
8517
+ "--provider",
8518
+ provider,
8519
+ "--hostname",
8520
+ "localhost",
8521
+ "--advertise-host",
8522
+ "localhost",
8523
+ "--agent-name",
8524
+ agentName,
8525
+ "--agent-description",
8526
+ agentDescription,
8527
+ "--log-level",
8528
+ "info"
8529
+ ]
8530
+ };
8531
+ }
8532
+ var LOCAL_A2A_TEMPLATES = [
8533
+ {
8534
+ key: "claude",
8535
+ name: "Claude Code",
8536
+ command: buildCliA2ACommand(
8537
+ "claude",
8538
+ "Claude Code",
8539
+ "Local Claude Code wrapper exposed through A2A."
8540
+ )
8541
+ },
8542
+ {
8543
+ key: "codex",
8544
+ name: "Codex",
8545
+ command: buildCliA2ACommand(
8546
+ "codex",
8547
+ "Codex",
8548
+ "Local Codex wrapper exposed through A2A."
8549
+ )
8550
+ },
8551
+ {
8552
+ key: "opencode",
8553
+ name: "OpenCode",
8554
+ command: buildCliA2ACommand(
8555
+ "opencode",
8556
+ "OpenCode",
8557
+ "Local OpenCode wrapper exposed through A2A."
8558
+ )
8559
+ }
8560
+ ];
8561
+
8562
+ // src/controllers/local-a2a-templates.ts
8563
+ async function getLocalA2ATemplates(_request, reply) {
8564
+ reply.send({
8565
+ success: true,
8566
+ data: {
8567
+ records: LOCAL_A2A_TEMPLATES,
8568
+ total: LOCAL_A2A_TEMPLATES.length
8569
+ }
8570
+ });
8571
+ }
8572
+
8573
+ // src/routes/a2a-test-connection.ts
8574
+ import { randomUUID as randomUUID8 } from "crypto";
8575
+ function describeTestConnectionError(error, agentCardUrl) {
8576
+ if (!(error instanceof Error)) {
8577
+ return `Failed to fetch agent card from ${agentCardUrl}: ${String(error)}`;
8578
+ }
8579
+ const cause = error.cause;
8580
+ const causeParts = [];
8581
+ if (cause instanceof Error) {
8582
+ causeParts.push(cause.message);
8583
+ const causeCode = cause.code;
8584
+ if (causeCode && !causeParts.some((part) => part.includes(causeCode))) {
8585
+ causeParts.push(causeCode);
8586
+ }
8587
+ } else if (typeof cause === "object" && cause !== null) {
8588
+ const code = "code" in cause ? String(cause.code) : "";
8589
+ const message = "message" in cause ? String(cause.message) : "";
8590
+ if (message) causeParts.push(message);
8591
+ if (code && !message.includes(code)) causeParts.push(code);
8592
+ }
8593
+ const detail = causeParts.length > 0 ? ` (${causeParts.join(", ")})` : "";
8594
+ return `Failed to fetch agent card from ${agentCardUrl}: ${error.message}${detail}`;
8595
+ }
8596
+ async function ensureLocalRuntimeForConnectionTest(localRuntime, agentCardUrl) {
8597
+ const commandValidation = validateLocalRuntimeCommand(localRuntime);
8598
+ if (!commandValidation.ok) {
8599
+ return { error: commandValidation.error };
8600
+ }
8601
+ const testRuntime = {
8602
+ ...localRuntime,
8603
+ agentCardUrl,
8604
+ command: {
8605
+ ...localRuntime.command,
8606
+ executable: localRuntime.command.executable.trim()
8607
+ }
8608
+ };
8609
+ const portValidation = validateLocalRuntimePort(testRuntime, agentCardUrl);
8610
+ if (!portValidation.ok) {
8611
+ return { error: portValidation.error };
8612
+ }
8613
+ const state = await localA2AService.startProvider(
8614
+ `test-${randomUUID8()}`,
8615
+ testRuntime
8616
+ );
8617
+ if (state.status === "running") return { runtimeId: state.runtimeId };
8618
+ const detail = state.message ? ` ${state.message}` : "";
8619
+ localA2AService.removeState(state.runtimeId);
8620
+ return { error: `Local managed A2A server could not start for connection test.${detail}` };
8621
+ }
8622
+ async function readShortResponseBody(resp) {
8623
+ const body = await resp.text().catch(() => "");
8624
+ return body.length > 500 ? `${body.slice(0, 500)}...` : body;
8625
+ }
8626
+ function registerA2ATestConnectionRoute(app2) {
8627
+ app2.post("/api/a2a/test-connection", async (request, reply) => {
8628
+ const { agentCardUrl, apiKey, localRuntime } = request.body ?? {};
8629
+ const normalizedAgentCardUrl = normalizeA2AAgentCardUrl(
8630
+ agentCardUrl,
8631
+ localRuntime ? "http://" : "https://"
8632
+ );
8633
+ if (!normalizedAgentCardUrl) {
8634
+ return reply.status(400).send({ success: false, error: "agentCardUrl is required" });
8635
+ }
8636
+ let testRuntimeId = null;
8637
+ try {
8638
+ const headers = {};
8639
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
8640
+ if (localRuntime) {
8641
+ const localStartResult = await ensureLocalRuntimeForConnectionTest(localRuntime, normalizedAgentCardUrl);
8642
+ if ("error" in localStartResult) {
8643
+ return reply.send({ success: true, data: { reachable: false, error: localStartResult.error } });
8644
+ }
8645
+ testRuntimeId = localStartResult.runtimeId;
8646
+ }
8647
+ const resp = await fetch(normalizedAgentCardUrl, { headers, signal: AbortSignal.timeout(8e3) });
8648
+ if (!resp.ok) {
8649
+ const body = await readShortResponseBody(resp);
8650
+ const suffix = body ? `: ${body}` : "";
8651
+ return reply.send({ success: true, data: { reachable: false, error: `HTTP ${resp.status} ${resp.statusText}${suffix}` } });
8652
+ }
8653
+ const card = await resp.json();
8654
+ return reply.send({ success: true, data: { reachable: true, agentName: card.name, agentCard: card } });
8655
+ } catch (error) {
8656
+ return reply.send({
8657
+ success: true,
8658
+ data: {
8659
+ reachable: false,
8660
+ error: describeTestConnectionError(error, normalizedAgentCardUrl)
8661
+ }
8662
+ });
8663
+ } finally {
8664
+ if (testRuntimeId) {
8665
+ localA2AService.removeState(testRuntimeId);
8666
+ }
8667
+ }
8668
+ });
8669
+ }
8670
+
7753
8671
  // src/routes/index.ts
7754
8672
  var registerLatticeRoutes = (app2, channelDeps) => {
7755
8673
  registerA2ABridgeRoutes(app2);
8674
+ registerSTTRoutes(app2);
8675
+ app2.get("/api/local-a2a", getLocalA2AStatus);
8676
+ app2.get("/api/local-a2a/templates", getLocalA2ATemplates);
8677
+ app2.get("/api/local-a2a/:assistantId", getLocalA2AProvider);
8678
+ app2.post("/api/local-a2a/:assistantId/start", startLocalA2AProvider);
8679
+ app2.post("/api/local-a2a/:assistantId/stop", stopLocalA2AProvider);
8680
+ app2.post("/api/local-a2a/:assistantId/restart", restartLocalA2AProvider);
8681
+ registerA2ATestConnectionRoute(app2);
7756
8682
  app2.post("/api/runs", createRun);
7757
8683
  app2.post("/api/resume_stream", resumeStream);
7758
8684
  app2.post("/api/assistants/:assistantId/threads/:threadId/abort", abortRun);
@@ -8047,9 +8973,9 @@ function registerResourceRoutes(app2, controller) {
8047
8973
  import { ExportableEntityRegistry as ExportableEntityRegistry3 } from "@axiom-lattice/core";
8048
8974
 
8049
8975
  // src/export_registrations/skill.registration.ts
8050
- import { getStoreLattice as getStoreLattice15 } from "@axiom-lattice/core";
8976
+ import { getStoreLattice as getStoreLattice16 } from "@axiom-lattice/core";
8051
8977
  function getStore2() {
8052
- return getStoreLattice15("default", "skill").store;
8978
+ return getStoreLattice16("default", "skill").store;
8053
8979
  }
8054
8980
  var skillRegistration = {
8055
8981
  entityType: "skill",
@@ -8124,9 +9050,9 @@ var skillRegistration = {
8124
9050
  };
8125
9051
 
8126
9052
  // src/export_registrations/agent.registration.ts
8127
- import { getStoreLattice as getStoreLattice16 } from "@axiom-lattice/core";
9053
+ import { getStoreLattice as getStoreLattice17 } from "@axiom-lattice/core";
8128
9054
  function getStore3() {
8129
- return getStoreLattice16("default", "assistant").store;
9055
+ return getStoreLattice17("default", "assistant").store;
8130
9056
  }
8131
9057
  var agentRegistration = {
8132
9058
  entityType: "agent",
@@ -8193,9 +9119,9 @@ var agentRegistration = {
8193
9119
  };
8194
9120
 
8195
9121
  // src/export_registrations/binding.registration.ts
8196
- import { getStoreLattice as getStoreLattice17 } from "@axiom-lattice/core";
9122
+ import { getStoreLattice as getStoreLattice18 } from "@axiom-lattice/core";
8197
9123
  function getStore4() {
8198
- return getStoreLattice17("default", "channelBinding").store;
9124
+ return getStoreLattice18("default", "channelBinding").store;
8199
9125
  }
8200
9126
  var bindingRegistration = {
8201
9127
  entityType: "binding",
@@ -8283,9 +9209,9 @@ var bindingRegistration = {
8283
9209
  };
8284
9210
 
8285
9211
  // src/export_registrations/database-config.registration.ts
8286
- import { getStoreLattice as getStoreLattice18 } from "@axiom-lattice/core";
9212
+ import { getStoreLattice as getStoreLattice19 } from "@axiom-lattice/core";
8287
9213
  function getStore5() {
8288
- return getStoreLattice18("default", "database").store;
9214
+ return getStoreLattice19("default", "database").store;
8289
9215
  }
8290
9216
  var databaseConfigRegistration = {
8291
9217
  entityType: "database_config",
@@ -8365,9 +9291,9 @@ var databaseConfigRegistration = {
8365
9291
  };
8366
9292
 
8367
9293
  // src/export_registrations/metrics-config.registration.ts
8368
- import { getStoreLattice as getStoreLattice19 } from "@axiom-lattice/core";
9294
+ import { getStoreLattice as getStoreLattice20 } from "@axiom-lattice/core";
8369
9295
  function getStore6() {
8370
- return getStoreLattice19("default", "metrics").store;
9296
+ return getStoreLattice20("default", "metrics").store;
8371
9297
  }
8372
9298
  var metricsConfigRegistration = {
8373
9299
  entityType: "metrics_config",
@@ -8447,9 +9373,9 @@ var metricsConfigRegistration = {
8447
9373
  };
8448
9374
 
8449
9375
  // src/export_registrations/mcp-config.registration.ts
8450
- import { getStoreLattice as getStoreLattice20 } from "@axiom-lattice/core";
9376
+ import { getStoreLattice as getStoreLattice21 } from "@axiom-lattice/core";
8451
9377
  function getStore7() {
8452
- return getStoreLattice20("default", "mcp").store;
9378
+ return getStoreLattice21("default", "mcp").store;
8453
9379
  }
8454
9380
  var mcpConfigRegistration = {
8455
9381
  entityType: "mcp_config",
@@ -8532,9 +9458,9 @@ var mcpConfigRegistration = {
8532
9458
  };
8533
9459
 
8534
9460
  // src/export_registrations/connection.registration.ts
8535
- import { getStoreLattice as getStoreLattice21 } from "@axiom-lattice/core";
9461
+ import { getStoreLattice as getStoreLattice22 } from "@axiom-lattice/core";
8536
9462
  function getStore8() {
8537
- return getStoreLattice21("default", "connection").store;
9463
+ return getStoreLattice22("default", "connection").store;
8538
9464
  }
8539
9465
  async function listAllConnections(tenantId) {
8540
9466
  const store = getStore8();
@@ -8616,9 +9542,9 @@ var connectionRegistration = {
8616
9542
  };
8617
9543
 
8618
9544
  // src/export_registrations/collection.registration.ts
8619
- import { getStoreLattice as getStoreLattice22 } from "@axiom-lattice/core";
9545
+ import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
8620
9546
  function getStore9() {
8621
- return getStoreLattice22("default", "collection").store;
9547
+ return getStoreLattice23("default", "collection").store;
8622
9548
  }
8623
9549
  var collectionRegistration = {
8624
9550
  entityType: "collection",
@@ -8689,9 +9615,9 @@ var collectionRegistration = {
8689
9615
  };
8690
9616
 
8691
9617
  // src/export_registrations/channel-installation.registration.ts
8692
- import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
9618
+ import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
8693
9619
  function getStore10() {
8694
- return getStoreLattice23("default", "channelInstallation").store;
9620
+ return getStoreLattice24("default", "channelInstallation").store;
8695
9621
  }
8696
9622
  var channelInstallationRegistration = {
8697
9623
  entityType: "channel_installation",
@@ -8766,9 +9692,9 @@ var channelInstallationRegistration = {
8766
9692
  };
8767
9693
 
8768
9694
  // src/export_registrations/menu.registration.ts
8769
- import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
9695
+ import { getStoreLattice as getStoreLattice25 } from "@axiom-lattice/core";
8770
9696
  function getStore11() {
8771
- return getStoreLattice24("default", "menu").store;
9697
+ return getStoreLattice25("default", "menu").store;
8772
9698
  }
8773
9699
  var menuRegistration = {
8774
9700
  entityType: "menu",
@@ -8846,9 +9772,9 @@ var menuRegistration = {
8846
9772
  };
8847
9773
 
8848
9774
  // src/export_registrations/task.registration.ts
8849
- import { getStoreLattice as getStoreLattice25 } from "@axiom-lattice/core";
9775
+ import { getStoreLattice as getStoreLattice26 } from "@axiom-lattice/core";
8850
9776
  function getStore12() {
8851
- return getStoreLattice25("default", "task").store;
9777
+ return getStoreLattice26("default", "task").store;
8852
9778
  }
8853
9779
  var taskRegistration = {
8854
9780
  entityType: "task",
@@ -8934,9 +9860,9 @@ var taskRegistration = {
8934
9860
  };
8935
9861
 
8936
9862
  // src/export_registrations/a2a-api-key.registration.ts
8937
- import { getStoreLattice as getStoreLattice26 } from "@axiom-lattice/core";
9863
+ import { getStoreLattice as getStoreLattice27 } from "@axiom-lattice/core";
8938
9864
  function getStore13() {
8939
- return getStoreLattice26("default", "a2aApiKey").store;
9865
+ return getStoreLattice27("default", "a2aApiKey").store;
8940
9866
  }
8941
9867
  var a2aApiKeyRegistration = {
8942
9868
  entityType: "a2a_api_key",
@@ -9007,9 +9933,9 @@ var a2aApiKeyRegistration = {
9007
9933
  };
9008
9934
 
9009
9935
  // src/export_registrations/eval.registration.ts
9010
- import { getStoreLattice as getStoreLattice27 } from "@axiom-lattice/core";
9936
+ import { getStoreLattice as getStoreLattice28 } from "@axiom-lattice/core";
9011
9937
  function getStore14() {
9012
- return getStoreLattice27("default", "eval").store;
9938
+ return getStoreLattice28("default", "eval").store;
9013
9939
  }
9014
9940
  var evalRegistration = {
9015
9941
  entityType: "eval",
@@ -9353,10 +10279,10 @@ function registerAllBuiltinEntities() {
9353
10279
 
9354
10280
  // src/router/MessageRouter.ts
9355
10281
  import {
9356
- getStoreLattice as getStoreLattice28,
10282
+ getStoreLattice as getStoreLattice29,
9357
10283
  agentInstanceManager as agentInstanceManager6
9358
10284
  } from "@axiom-lattice/core";
9359
- import { randomUUID as randomUUID8 } from "crypto";
10285
+ import { randomUUID as randomUUID9 } from "crypto";
9360
10286
  var BindingNotFoundError = class extends Error {
9361
10287
  constructor(message) {
9362
10288
  super(message);
@@ -9516,7 +10442,7 @@ var MessageRouter = class {
9516
10442
  channel: message.channel,
9517
10443
  adapterChannel: adapter.channel
9518
10444
  }, "Thread resolved by adapter strategy");
9519
- const threadStore = getStoreLattice28("default", "thread").store;
10445
+ const threadStore = getStoreLattice29("default", "thread").store;
9520
10446
  try {
9521
10447
  await threadStore.createThread(
9522
10448
  tenantId,
@@ -9553,8 +10479,8 @@ var MessageRouter = class {
9553
10479
  }
9554
10480
  }
9555
10481
  if (!threadId) {
9556
- const threadStore = getStoreLattice28("default", "thread").store;
9557
- const newThreadId = randomUUID8();
10482
+ const threadStore = getStoreLattice29("default", "thread").store;
10483
+ const newThreadId = randomUUID9();
9558
10484
  console.log({
9559
10485
  event: "dispatch:thread:create",
9560
10486
  agentId,
@@ -9979,7 +10905,7 @@ ${summary}`
9979
10905
  console.log(
9980
10906
  `\u5C06\u5728 ${delayMs}ms \u540E\u91CD\u8BD5\u4EFB\u52A1 (${nextRetryCount}/${maxRetries})`
9981
10907
  );
9982
- await new Promise((resolve) => setTimeout(resolve, delayMs));
10908
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
9983
10909
  return handleAgentTask(taskRequest, nextRetryCount);
9984
10910
  }
9985
10911
  if (callback_event) {
@@ -10159,7 +11085,7 @@ import {
10159
11085
  getLoggerLattice,
10160
11086
  loggerLatticeManager,
10161
11087
  sandboxLatticeManager as sandboxLatticeManager2,
10162
- getStoreLattice as getStoreLattice29,
11088
+ getStoreLattice as getStoreLattice30,
10163
11089
  agentInstanceManager as agentInstanceManager8,
10164
11090
  createSandboxProvider,
10165
11091
  TokenCache,
@@ -10209,7 +11135,7 @@ var getHeaderValue = (header) => {
10209
11135
  }
10210
11136
  return header;
10211
11137
  };
10212
- var PUBLIC_ROUTES = ["/api/auth/login", "/api/auth/register", "/health"];
11138
+ var PUBLIC_ROUTES = ["/api/auth/login", "/api/auth/register", "/health", "/api/stt/models"];
10213
11139
  app.addHook("preHandler", async (request, reply) => {
10214
11140
  const user = extractUserFromAuthHeader(request.headers.authorization);
10215
11141
  if (user) {
@@ -10269,9 +11195,9 @@ app.register(multipart, {
10269
11195
  });
10270
11196
  app.register(websocket);
10271
11197
  var __filename = fileURLToPath(import.meta.url);
10272
- var __dirname = path2.dirname(__filename);
11198
+ var __dirname = path3.dirname(__filename);
10273
11199
  app.register(staticPlugin, {
10274
- root: path2.join(__dirname, "../public"),
11200
+ root: path3.join(__dirname, "../public"),
10275
11201
  prefix: "/"
10276
11202
  });
10277
11203
  app.setErrorHandler((error, request, reply) => {
@@ -10312,7 +11238,7 @@ function getConfiguredSandboxProvider() {
10312
11238
  }
10313
11239
  async function restoreMcpConnections() {
10314
11240
  try {
10315
- const storeLattice = getStoreLattice29("default", "mcp");
11241
+ const storeLattice = getStoreLattice30("default", "mcp");
10316
11242
  const store = storeLattice.store;
10317
11243
  if (!store) {
10318
11244
  logger4.info("MCP store not configured, skipping connection restoration");
@@ -10381,7 +11307,7 @@ var start = async (config) => {
10381
11307
  channelDeps = { router, installationStore };
10382
11308
  try {
10383
11309
  const a2aKeyStore = getStore15("default", "a2aApiKey").store;
10384
- const a2a = await import("./a2a-ERG5RMUW.mjs");
11310
+ const a2a = await import("./a2a-QMHEH3ZT.mjs");
10385
11311
  a2a.setA2AKeyStore(a2aKeyStore);
10386
11312
  await a2a.refreshStoreKeyMap();
10387
11313
  logger4.info("A2A key store initialized");
@@ -10394,7 +11320,7 @@ var start = async (config) => {
10394
11320
  }
10395
11321
  setEvalRunService(evalRunner);
10396
11322
  try {
10397
- const menuStore = getStoreLattice29("default", "menu").store;
11323
+ const menuStore = getStoreLattice30("default", "menu").store;
10398
11324
  setMenuRegistry(menuStore);
10399
11325
  logger4.info("Menu registry initialized");
10400
11326
  } catch {
@@ -10410,7 +11336,7 @@ var start = async (config) => {
10410
11336
  }
10411
11337
  try {
10412
11338
  const { ResourceController } = await import("./resources-VA7LSDKN.mjs");
10413
- const sharedResourceStore = getStoreLattice29("default", "sharedResource").store;
11339
+ const sharedResourceStore = getStoreLattice30("default", "sharedResource").store;
10414
11340
  const cache = new TokenCache();
10415
11341
  const resourceController = new ResourceController({
10416
11342
  store: sharedResourceStore,
@@ -10441,11 +11367,6 @@ var start = async (config) => {
10441
11367
  const target_port = config?.port || Number(process.env.PORT) || 4001;
10442
11368
  await app.listen({ port: target_port, host: "0.0.0.0" });
10443
11369
  logger4.info(`Lattice Gateway is running on port: ${target_port}`);
10444
- try {
10445
- logger4.info("AgentLifecycleManager initialized");
10446
- } catch (error) {
10447
- logger4.warn("Failed to initialize AgentLifecycleManager", { error });
10448
- }
10449
11370
  const queueServiceConfig = config?.queueServiceConfig;
10450
11371
  if (queueServiceConfig) {
10451
11372
  setQueueServiceType(queueServiceConfig.type);