@axiom-lattice/gateway 2.1.117 → 3.0.1

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 (35) hide show
  1. package/.turbo/turbo-build.log +12 -12
  2. package/CHANGELOG.md +38 -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 +1098 -181
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +967 -96
  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/eval.ts +6 -2
  17. package/src/controllers/local-a2a-templates.ts +18 -0
  18. package/src/controllers/local-a2a.ts +149 -0
  19. package/src/controllers/tasks.ts +2 -3
  20. package/src/controllers/workspace.ts +144 -17
  21. package/src/index.ts +1 -9
  22. package/src/routes/__tests__/a2a-test-connection.test.ts +177 -0
  23. package/src/routes/a2a-test-connection.ts +126 -0
  24. package/src/routes/a2a.ts +100 -28
  25. package/src/routes/index.ts +23 -0
  26. package/src/services/__tests__/eval-runner.test.ts +131 -0
  27. package/src/services/__tests__/local-a2a.test.ts +124 -0
  28. package/src/services/eval-runner.ts +50 -3
  29. package/src/services/local-a2a.ts +354 -0
  30. package/src/utils/__tests__/local-a2a-runtime.test.ts +77 -0
  31. package/src/utils/__tests__/path-containment.test.ts +35 -0
  32. package/src/utils/a2a-url.ts +20 -0
  33. package/src/utils/local-a2a-runtime.ts +107 -0
  34. package/src/utils/path-containment.ts +38 -0
  35. package/dist/a2a-ERG5RMUW.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -12,19 +12,387 @@ 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
+ return null;
54
+ }
55
+ function resolveLocalA2AWorkingDirectory(options) {
56
+ if (options.envWorkdir) return options.envWorkdir;
57
+ if (options.commandCwd) return options.commandCwd;
58
+ return findWorkspaceRoot(options.currentCwd) || options.currentCwd;
59
+ }
60
+ function validateWorkingDirectory(cwd) {
61
+ try {
62
+ const stat2 = statSync(cwd);
63
+ if (!stat2.isDirectory()) {
64
+ return `Working directory is not a directory: ${cwd}`;
65
+ }
66
+ return null;
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ return `Working directory is not accessible: ${cwd} (${message})`;
70
+ }
71
+ }
72
+ function appendStderrTail(current, chunk) {
73
+ const next = `${current}${chunk}`;
74
+ if (next.length <= STDERR_TAIL_LIMIT) return next;
75
+ return next.slice(-STDERR_TAIL_LIMIT);
76
+ }
77
+ function formatExitMessage(exitReason, stderrTail) {
78
+ const stderr = stderrTail.trim();
79
+ return stderr ? `Exited before becoming ready (${exitReason}): ${stderr}` : `Exited before becoming ready (${exitReason})`;
80
+ }
81
+ async function probeReady(agentCardUrl, healthUrl, timeoutMs) {
82
+ const controller = new AbortController();
83
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
84
+ try {
85
+ if (healthUrl) {
86
+ const resp = await fetch(healthUrl, { signal: controller.signal });
87
+ if (!resp.ok) return false;
88
+ }
89
+ const cardResp = await fetch(agentCardUrl, { signal: controller.signal });
90
+ return cardResp.ok;
91
+ } catch {
92
+ return false;
93
+ } finally {
94
+ clearTimeout(timer);
95
+ }
96
+ }
97
+ var _LocalA2AService = class _LocalA2AService {
98
+ constructor() {
99
+ this.states = /* @__PURE__ */ new Map();
100
+ this.autoStartEnabled = readBooleanEnv("LOCAL_A2A_AUTO_START", true);
101
+ }
102
+ static getInstance() {
103
+ if (!_LocalA2AService.instance) {
104
+ _LocalA2AService.instance = new _LocalA2AService();
105
+ }
106
+ return _LocalA2AService.instance;
107
+ }
108
+ getOrCreateState(runtimeId, localRuntime) {
109
+ let state = this.states.get(runtimeId);
110
+ if (!state) {
111
+ state = {
112
+ runtimeId,
113
+ label: localRuntime.label || runtimeId,
114
+ status: this.autoStartEnabled ? "stopped" : "disabled",
115
+ enabled: this.autoStartEnabled,
116
+ assistantId: runtimeId,
117
+ agentCardUrl: localRuntime.agentCardUrl,
118
+ healthUrl: localRuntime.healthUrl,
119
+ localRuntime
120
+ };
121
+ this.states.set(runtimeId, state);
122
+ } else {
123
+ state.localRuntime = localRuntime;
124
+ state.label = localRuntime.label || state.label;
125
+ state.agentCardUrl = localRuntime.agentCardUrl;
126
+ state.healthUrl = localRuntime.healthUrl;
127
+ }
128
+ return state;
129
+ }
130
+ async getRegisteredState(runtimeId, localRuntime) {
131
+ const state = this.getOrCreateState(runtimeId, localRuntime);
132
+ if (state.process && state.status === "running") {
133
+ const alive = await probeReady(
134
+ localRuntime.agentCardUrl,
135
+ localRuntime.healthUrl,
136
+ 2e3
137
+ );
138
+ if (!alive) {
139
+ state.process = void 0;
140
+ state.pid = void 0;
141
+ state.status = "stopped";
142
+ state.message = "Process exited unexpectedly";
143
+ }
144
+ }
145
+ return this.snapshotState(state);
146
+ }
147
+ async startProvider(runtimeId, localRuntime) {
148
+ const state = this.getOrCreateState(runtimeId, localRuntime);
149
+ if (!state.enabled) {
150
+ state.status = "disabled";
151
+ state.message = "Disabled by configuration";
152
+ return this.snapshotState(state);
153
+ }
154
+ if (state.process && state.status === "running") {
155
+ return this.snapshotState(state);
156
+ }
157
+ state.enabled = true;
158
+ const { executable, args, cwd, env: cmdEnv } = localRuntime.command;
159
+ const resolvedCwd = resolveLocalA2AWorkingDirectory({
160
+ envWorkdir: process.env.LOCAL_A2A_WORKDIR || void 0,
161
+ commandCwd: cwd,
162
+ currentCwd: process.cwd()
163
+ });
164
+ const cwdError = validateWorkingDirectory(resolvedCwd);
165
+ if (cwdError) {
166
+ state.process = void 0;
167
+ state.pid = void 0;
168
+ state.status = "failed";
169
+ state.message = cwdError;
170
+ return this.snapshotState(state);
171
+ }
172
+ state.status = "starting";
173
+ state.message = "Starting";
174
+ let stderrTail = "";
175
+ const childEnv = cmdEnv ? { ...process.env, ...cmdEnv } : process.env;
176
+ const child = spawn(executable, args, {
177
+ cwd: resolvedCwd,
178
+ env: childEnv,
179
+ stdio: ["ignore", "pipe", "pipe"],
180
+ shell: false
181
+ });
182
+ state.process = child;
183
+ state.pid = child.pid ?? void 0;
184
+ child.once("error", (error) => {
185
+ const current = this.states.get(runtimeId);
186
+ if (!current) return;
187
+ current.process = void 0;
188
+ current.pid = void 0;
189
+ current.status = "failed";
190
+ const stderr = stderrTail.trim();
191
+ current.message = stderr ? `Failed to start: ${error.message}: ${stderr}` : `Failed to start: ${error.message}`;
192
+ });
193
+ child.stdout.on("data", (chunk) => {
194
+ const text = chunk.toString("utf8").trim();
195
+ if (text) console.log(`[local-a2a:${runtimeId}] ${text}`);
196
+ });
197
+ child.stderr.on("data", (chunk) => {
198
+ const rawText = chunk.toString("utf8");
199
+ stderrTail = appendStderrTail(stderrTail, rawText);
200
+ const text = rawText.trim();
201
+ if (text) console.error(`[local-a2a:${runtimeId}] ${text}`);
202
+ });
203
+ child.on("close", (code, signal) => {
204
+ const current = this.states.get(runtimeId);
205
+ if (!current) return;
206
+ current.process = void 0;
207
+ current.pid = void 0;
208
+ const exitReason = signal ?? code ?? "unknown";
209
+ if (current.status === "running") {
210
+ current.status = "stopped";
211
+ current.message = `Stopped (${exitReason})`;
212
+ } else if (current.status === "starting") {
213
+ current.status = "failed";
214
+ current.message = formatExitMessage(exitReason, stderrTail);
215
+ }
216
+ });
217
+ const ready = await this.waitUntilReady(localRuntime, state, 15e3);
218
+ if (!ready) {
219
+ if (state.process) {
220
+ state.process.kill("SIGTERM");
221
+ }
222
+ state.process = void 0;
223
+ state.pid = void 0;
224
+ if (state.status !== "failed") {
225
+ state.status = "failed";
226
+ state.message = "Server did not become ready";
227
+ }
228
+ return this.snapshotState(state);
229
+ }
230
+ state.status = "running";
231
+ state.message = "Running";
232
+ state.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
233
+ return this.snapshotState(state);
234
+ }
235
+ async stopProvider(runtimeId) {
236
+ const state = this.states.get(runtimeId);
237
+ if (!state) {
238
+ return this.emptyState(runtimeId);
239
+ }
240
+ const child = state.process;
241
+ if (child) {
242
+ child.kill("SIGTERM");
243
+ await delay(750);
244
+ if (child.exitCode === null) {
245
+ child.kill("SIGKILL");
246
+ }
247
+ state.process = void 0;
248
+ }
249
+ state.pid = void 0;
250
+ state.status = "stopped";
251
+ state.message = "Stopped";
252
+ state.lastStoppedAt = (/* @__PURE__ */ new Date()).toISOString();
253
+ return this.snapshotState(state);
254
+ }
255
+ async restartProvider(runtimeId, localRuntime) {
256
+ await this.stopProvider(runtimeId);
257
+ return this.startProvider(runtimeId, localRuntime);
258
+ }
259
+ removeState(runtimeId) {
260
+ const state = this.states.get(runtimeId);
261
+ if (state?.process) {
262
+ state.process.kill("SIGTERM");
263
+ state.process = void 0;
264
+ }
265
+ this.states.delete(runtimeId);
266
+ }
267
+ async waitUntilReady(localRuntime, state, timeoutMs) {
268
+ const start2 = Date.now();
269
+ while (Date.now() - start2 < timeoutMs) {
270
+ if (state.status === "failed") return false;
271
+ if (await probeReady(localRuntime.agentCardUrl, localRuntime.healthUrl, 1500)) {
272
+ return true;
273
+ }
274
+ await delay(500);
275
+ }
276
+ return false;
277
+ }
278
+ emptyState(runtimeId) {
279
+ return {
280
+ runtimeId,
281
+ label: runtimeId,
282
+ status: "stopped",
283
+ enabled: false
284
+ };
285
+ }
286
+ snapshotState(state) {
287
+ return {
288
+ runtimeId: state.runtimeId,
289
+ label: state.label,
290
+ status: state.status,
291
+ enabled: state.enabled,
292
+ assistantId: state.assistantId,
293
+ agentCardUrl: state.agentCardUrl,
294
+ healthUrl: state.healthUrl,
295
+ pid: state.pid,
296
+ message: state.message,
297
+ lastStartedAt: state.lastStartedAt,
298
+ lastStoppedAt: state.lastStoppedAt
299
+ };
300
+ }
301
+ };
302
+ _LocalA2AService.instance = null;
303
+ var LocalA2AService = _LocalA2AService;
304
+ var localA2AService = LocalA2AService.getInstance();
305
+
306
+ // src/utils/a2a-url.ts
307
+ var AGENT_CARD_PATH = "/.well-known/agent-card.json";
308
+ function normalizeA2AAgentCardUrl(value, defaultProtocol = "https://") {
309
+ const raw = value?.trim() ?? "";
310
+ if (!raw) return "";
311
+ const withProtocol = /^https?:\/\//i.test(raw) ? raw : `${defaultProtocol}${raw.replace(/^\/+/, "")}`;
312
+ const trimmed = withProtocol.replace(/\/+$/, "");
313
+ if (trimmed.endsWith(AGENT_CARD_PATH)) {
314
+ return trimmed;
315
+ }
316
+ return `${trimmed}${AGENT_CARD_PATH}`;
317
+ }
318
+
319
+ // src/utils/local-a2a-runtime.ts
320
+ function validateLocalRuntimeCommand(localRuntime) {
321
+ const runtime = localRuntime;
322
+ const executable = runtime.command?.executable;
323
+ if (typeof executable !== "string" || executable.trim().length === 0) {
324
+ return {
325
+ ok: false,
326
+ error: "Local managed A2A command executable is required"
327
+ };
328
+ }
329
+ const args = runtime.command?.args;
330
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
331
+ return {
332
+ ok: false,
333
+ error: "Local managed A2A command args must be an array of strings"
334
+ };
335
+ }
336
+ return { ok: true };
337
+ }
338
+ function getAgentCardPort(agentCardUrl) {
339
+ let url;
340
+ try {
341
+ url = new URL(agentCardUrl);
342
+ } catch {
343
+ return null;
344
+ }
345
+ if (url.port) {
346
+ const port = Number(url.port);
347
+ return Number.isInteger(port) ? port : null;
348
+ }
349
+ if (url.protocol === "http:") return 80;
350
+ if (url.protocol === "https:") return 443;
351
+ return null;
352
+ }
353
+ function parseCommandPort(args) {
354
+ for (let index = 0; index < args.length; index += 1) {
355
+ const arg = args[index];
356
+ const match = /^(--port|-p)(?:[=\s]+(\d+))?$/.exec(arg);
357
+ if (!match) continue;
358
+ const inlinePort = match[2];
359
+ if (inlinePort) return Number(inlinePort);
360
+ const nextArg = args[index + 1];
361
+ if (nextArg && /^\d+$/.test(nextArg)) {
362
+ return Number(nextArg);
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+ function isLoopbackHost(hostname) {
368
+ const normalized = hostname.toLowerCase();
369
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
370
+ }
371
+ function validateLocalRuntimePort(localRuntime, agentCardUrl) {
372
+ let url;
373
+ try {
374
+ url = new URL(agentCardUrl);
375
+ } catch {
376
+ return { ok: true };
377
+ }
378
+ if (!isLoopbackHost(url.hostname) || !url.port) {
379
+ return { ok: true };
380
+ }
381
+ const commandPort = parseCommandPort(localRuntime.command.args);
382
+ if (commandPort === null) {
383
+ return { ok: true };
384
+ }
385
+ const agentCardPort = Number(url.port);
386
+ if (commandPort === agentCardPort) {
387
+ return { ok: true };
388
+ }
389
+ return {
390
+ ok: false,
391
+ 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.`
392
+ };
393
+ }
394
+
395
+ // src/controllers/assistant.ts
28
396
  function getTenantId(request) {
29
397
  const userTenantId = request.user?.tenantId;
30
398
  if (userTenantId) {
@@ -154,6 +522,79 @@ async function createAssistant(request, reply) {
154
522
  });
155
523
  }
156
524
  const id = data.id ?? randomUUID();
525
+ if (data.graphDefinition && data.graphDefinition.type === AgentType.A2A_REMOTE) {
526
+ const a2aConfig = data.graphDefinition;
527
+ const warnings = [];
528
+ const isLocalManaged = !!a2aConfig.localRuntime;
529
+ a2aConfig.agentCardUrl = normalizeA2AAgentCardUrl(
530
+ a2aConfig.agentCardUrl,
531
+ isLocalManaged ? "http://" : "https://"
532
+ );
533
+ if (a2aConfig.localRuntime) {
534
+ const commandValidation = validateLocalRuntimeCommand(
535
+ a2aConfig.localRuntime
536
+ );
537
+ if (!commandValidation.ok) {
538
+ return reply.status(400).send({
539
+ success: false,
540
+ message: commandValidation.error
541
+ });
542
+ }
543
+ a2aConfig.localRuntime.command.executable = a2aConfig.localRuntime.command.executable.trim();
544
+ a2aConfig.localRuntime.agentCardUrl = normalizeA2AAgentCardUrl(
545
+ a2aConfig.localRuntime.agentCardUrl || a2aConfig.agentCardUrl,
546
+ "http://"
547
+ );
548
+ a2aConfig.agentCardUrl = a2aConfig.localRuntime.agentCardUrl;
549
+ const portValidation = validateLocalRuntimePort(
550
+ a2aConfig.localRuntime,
551
+ a2aConfig.agentCardUrl
552
+ );
553
+ if (!portValidation.ok) {
554
+ return reply.status(400).send({
555
+ success: false,
556
+ message: portValidation.error
557
+ });
558
+ }
559
+ }
560
+ if (a2aConfig.localRuntime) {
561
+ const existingAssistants = await getStoreLattice("default", "assistant").store.getAllAssistants(tenantId);
562
+ const targetPort = getAgentCardPort(a2aConfig.localRuntime.agentCardUrl);
563
+ const duplicate = existingAssistants.find((a) => {
564
+ if (a.id === id) return false;
565
+ const gd = a.graphDefinition && typeof a.graphDefinition === "object" ? a.graphDefinition : void 0;
566
+ if (!gd || gd.type !== AgentType.A2A_REMOTE) return false;
567
+ const lr = gd.localRuntime;
568
+ if (!lr) return false;
569
+ const existingAgentCardUrl = normalizeA2AAgentCardUrl(lr.agentCardUrl, "http://");
570
+ const existingPort = getAgentCardPort(existingAgentCardUrl);
571
+ if (targetPort !== null && existingPort !== null) {
572
+ return existingPort === targetPort;
573
+ }
574
+ return existingAgentCardUrl === a2aConfig.localRuntime.agentCardUrl;
575
+ });
576
+ if (duplicate) {
577
+ const conflictDetail = targetPort === null ? `agentCardUrl "${a2aConfig.localRuntime.agentCardUrl}"` : `port ${targetPort}`;
578
+ return reply.status(409).send({
579
+ success: false,
580
+ message: `A local managed A2A assistant using ${conflictDetail} already exists: "${duplicate.name}". Each local managed A2A assistant needs a unique port.`
581
+ });
582
+ }
583
+ }
584
+ try {
585
+ const resp = await fetch(a2aConfig.agentCardUrl, {
586
+ signal: AbortSignal.timeout(5e3)
587
+ });
588
+ if (!resp.ok) {
589
+ warnings.push(`agentCardUrl returned HTTP ${resp.status}`);
590
+ }
591
+ } catch {
592
+ warnings.push(`agentCardUrl is not reachable: ${a2aConfig.agentCardUrl}`);
593
+ }
594
+ if (warnings.length > 0) {
595
+ console.warn(`[createAssistant] A2A warnings for ${a2aConfig.agentCardUrl}:`, warnings);
596
+ }
597
+ }
157
598
  return upsertAssistant(tenantId, id, data, reply, true);
158
599
  }
159
600
  async function updateAssistant(request, reply) {
@@ -167,11 +608,28 @@ async function deleteAssistant(request, reply) {
167
608
  const { id } = request.params;
168
609
  const storeLattice = getStoreLattice("default", "assistant");
169
610
  const assistantStore = storeLattice.store;
611
+ const storedAssistant = await assistantStore.getAssistantById(tenantId, id);
612
+ const graphDefinition = storedAssistant?.graphDefinition;
613
+ const hasLocalRuntime = graphDefinition && typeof graphDefinition === "object" ? !!graphDefinition.localRuntime : false;
614
+ if (storedAssistant && hasLocalRuntime) {
615
+ localA2AService.removeState(id);
616
+ const deleted2 = await assistantStore.deleteAssistant(tenantId, id);
617
+ if (!deleted2) {
618
+ return reply.status(500).send({
619
+ success: false,
620
+ message: "Failed to delete assistant"
621
+ });
622
+ }
623
+ eventBus.publish("assistant:deleted", { id, tenantId });
624
+ return {
625
+ success: true,
626
+ message: "Deleted local A2A assistant"
627
+ };
628
+ }
170
629
  const agentConfig = await agentLatticeManager.getAgentConfigWithTenant(tenantId, id);
171
630
  const isCodeConfigured = !!agentConfig;
172
631
  if (isCodeConfigured) {
173
- const exists2 = await assistantStore.hasAssistant(tenantId, id);
174
- if (!exists2) {
632
+ if (!storedAssistant) {
175
633
  return reply.status(404).send({
176
634
  success: false,
177
635
  message: "Assistant not found (code-configured assistants cannot be deleted from code, only from store)"
@@ -184,8 +642,7 @@ async function deleteAssistant(request, reply) {
184
642
  message: "Deleted assistant from store (code-configured registration remains)"
185
643
  };
186
644
  }
187
- const exists = await assistantStore.hasAssistant(tenantId, id);
188
- if (!exists) {
645
+ if (!storedAssistant) {
189
646
  return reply.status(404).send({
190
647
  success: false,
191
648
  message: "Assistant not found"
@@ -319,7 +776,7 @@ var createRun = async (request, reply) => {
319
776
 
320
777
  `);
321
778
  if (!success) {
322
- await new Promise((resolve) => reply.raw.once("drain", resolve));
779
+ await new Promise((resolve3) => reply.raw.once("drain", resolve3));
323
780
  }
324
781
  }
325
782
  } catch (error) {
@@ -2911,15 +3368,14 @@ function getStore() {
2911
3368
  async function listTasks(request, reply) {
2912
3369
  try {
2913
3370
  const tenantId = getTenantId9(request);
2914
- const userId = getUserId4(request);
2915
3371
  const query = request.query;
2916
3372
  const store = getStore();
2917
3373
  const tasks = await store.list({
2918
3374
  tenantId,
2919
- ownerId: query.ownerId || userId,
2920
3375
  status: query.status,
2921
3376
  priority: query.priority,
2922
- ownerType: query.ownerType,
3377
+ ...query.ownerId && { ownerId: query.ownerId },
3378
+ ...query.ownerType && { ownerType: query.ownerType },
2923
3379
  metadata: query.metadata ? JSON.parse(query.metadata) : void 0,
2924
3380
  projectId: query.projectId,
2925
3381
  limit: query.limit ? parseInt(query.limit) : void 0,
@@ -4054,12 +4510,41 @@ function registerSandboxProxyRoutes(app2) {
4054
4510
  }
4055
4511
 
4056
4512
  // src/controllers/workspace.ts
4057
- import * as fs from "fs/promises";
4058
- import * as path from "path";
4513
+ import * as fs2 from "fs/promises";
4514
+ import * as path2 from "path";
4515
+ import * as os from "os";
4059
4516
  import { getStoreLattice as getStoreLattice7 } from "@axiom-lattice/core";
4060
4517
  import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
4061
4518
  import { getSandBoxManager as getSandBoxManager3 } from "@axiom-lattice/core";
4062
4519
  import { v4 as uuidv42 } from "uuid";
4520
+
4521
+ // src/utils/path-containment.ts
4522
+ import * as fs from "fs/promises";
4523
+ import * as path from "path";
4524
+ var PathOutsideRootError = class extends Error {
4525
+ constructor(rootPath, targetPath) {
4526
+ super(`Path is outside the allowed root: ${targetPath}`);
4527
+ this.rootPath = rootPath;
4528
+ this.targetPath = targetPath;
4529
+ this.name = "PathOutsideRootError";
4530
+ }
4531
+ };
4532
+ function isPathWithinRoot(rootPath, targetPath) {
4533
+ const relative2 = path.relative(rootPath, targetPath);
4534
+ return relative2 === "" || relative2 !== ".." && !relative2.startsWith(`..${path.sep}`) && !path.isAbsolute(relative2);
4535
+ }
4536
+ async function resolvePathWithinRoot(rootPath, targetPath) {
4537
+ const [resolvedRoot, resolvedTarget] = await Promise.all([
4538
+ fs.realpath(rootPath),
4539
+ fs.realpath(targetPath)
4540
+ ]);
4541
+ if (!isPathWithinRoot(resolvedRoot, resolvedTarget)) {
4542
+ throw new PathOutsideRootError(resolvedRoot, resolvedTarget);
4543
+ }
4544
+ return { rootPath: resolvedRoot, targetPath: resolvedTarget };
4545
+ }
4546
+
4547
+ // src/controllers/workspace.ts
4063
4548
  var WorkspaceController = class {
4064
4549
  constructor() {
4065
4550
  this.workspaceStore = getStoreLattice7("default", "workspace").store;
@@ -4220,16 +4705,23 @@ var WorkspaceController = class {
4220
4705
  }),
4221
4706
  workspace
4222
4707
  };
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
4708
  }
4709
+ const rootDir = await this.getFilesystemRootDirectory(
4710
+ tenantId,
4711
+ workspaceId,
4712
+ projectId
4713
+ );
4714
+ console.log(`[getBackend] using FilesystemBackend rootDir=${rootDir}`);
4715
+ return {
4716
+ backend: new FilesystemBackend({
4717
+ rootDir,
4718
+ virtualMode: true
4719
+ }),
4720
+ workspace
4721
+ };
4722
+ }
4723
+ async getFilesystemRootDirectory(tenantId, workspaceId, projectId) {
4724
+ return `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`;
4233
4725
  }
4234
4726
  isBinaryContentType(filename) {
4235
4727
  const ext = filename.split(".").pop()?.toLowerCase() || "";
@@ -4418,21 +4910,21 @@ var WorkspaceController = class {
4418
4910
  async listPath(request) {
4419
4911
  const tenantId = this.getTenantId(request);
4420
4912
  const { workspaceId, projectId } = request.params;
4421
- const path3 = request.query.path || "/";
4913
+ const path4 = request.query.path || "/";
4422
4914
  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);
4915
+ console.log(`[listPath] tenantId=${tenantId} workspaceId=${workspaceId} projectId=${projectId} path=${path4} assistantId=${assistantId}`);
4916
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path4);
4917
+ console.log(`[listPath] backend type=${backend.constructor.name} calling lsInfo(${path4})`);
4918
+ const files = await backend.lsInfo(path4);
4427
4919
  console.log(`[listPath] result count=${files.length}`);
4428
4920
  return { success: true, data: files };
4429
4921
  }
4430
4922
  async readFile(request) {
4431
4923
  const tenantId = this.getTenantId(request);
4432
4924
  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));
4925
+ const { path: path4, offset = 0, limit = 1e3, assistantId } = request.query;
4926
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path4);
4927
+ const content = await backend.read(path4, Number(offset), Number(limit));
4436
4928
  return { success: true, data: { content, offset, limit } };
4437
4929
  }
4438
4930
  /**
@@ -4444,7 +4936,7 @@ var WorkspaceController = class {
4444
4936
  * - `path`: optional directory path relative to project root (e.g., "docs", "src/utils")
4445
4937
  * - `assistantId`: optional query parameter for sandbox identity matching
4446
4938
  * For sandbox storage, delegates to the sandbox upload API.
4447
- * For filesystem storage, writes directly under /lattice_store/workspaces/{workspaceId}/{projectId}.
4939
+ * For filesystem storage, writes under the same project root used by the file backend.
4448
4940
  */
4449
4941
  async uploadFile(request, reply) {
4450
4942
  const tenantId = this.getTenantId(request);
@@ -4478,7 +4970,7 @@ var WorkspaceController = class {
4478
4970
  workspaceId,
4479
4971
  projectId
4480
4972
  };
4481
- const realPath = pathValue ? path.posix.join(pathValue, filename) : path.posix.join("/project/uploads", filename);
4973
+ const realPath = pathValue ? path2.posix.join(pathValue, filename) : path2.posix.join("/project/uploads", filename);
4482
4974
  const isBinary = this.isBinaryContentType(filename);
4483
4975
  const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, realPath);
4484
4976
  if (volumeBackend && !isBinary) {
@@ -4489,7 +4981,7 @@ var WorkspaceController = class {
4489
4981
  const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
4490
4982
  await sandbox.file.uploadFile({ file: realPath, data: buffer });
4491
4983
  }
4492
- const relativePath = pathValue ? path.posix.join(pathValue, filename) : path.posix.join("/project/uploads", filename);
4984
+ const relativePath = pathValue ? path2.posix.join(pathValue, filename) : path2.posix.join("/project/uploads", filename);
4493
4985
  const result2 = {
4494
4986
  path: relativePath,
4495
4987
  name: filename,
@@ -4499,14 +4991,43 @@ var WorkspaceController = class {
4499
4991
  };
4500
4992
  return reply.status(200).send({ success: true, data: result2 });
4501
4993
  }
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);
4994
+ const rootDir = await this.getFilesystemRootDirectory(
4995
+ tenantId,
4996
+ workspaceId,
4997
+ projectId
4998
+ );
4999
+ await fs2.mkdir(rootDir, { recursive: true });
5000
+ const relativeDirectory = pathValue?.replace(/^[/\\]+/, "") || ".";
5001
+ const targetDir = path2.resolve(rootDir, relativeDirectory);
5002
+ if (!isPathWithinRoot(path2.resolve(rootDir), targetDir)) {
5003
+ return reply.status(400).send({
5004
+ success: false,
5005
+ error: "Upload path must stay within the project directory"
5006
+ });
5007
+ }
5008
+ await fs2.mkdir(targetDir, { recursive: true });
5009
+ const [resolvedRoot, resolvedTargetDir] = await Promise.all([
5010
+ fs2.realpath(rootDir),
5011
+ fs2.realpath(targetDir)
5012
+ ]);
5013
+ if (!isPathWithinRoot(resolvedRoot, resolvedTargetDir)) {
5014
+ return reply.status(400).send({
5015
+ success: false,
5016
+ error: "Upload path must stay within the project directory"
5017
+ });
5018
+ }
5019
+ const safeFilename = path2.basename(filename);
5020
+ if (safeFilename !== filename) {
5021
+ return reply.status(400).send({
5022
+ success: false,
5023
+ error: "Invalid filename"
5024
+ });
5025
+ }
5026
+ const targetPath = path2.join(resolvedTargetDir, safeFilename);
5027
+ await fs2.writeFile(targetPath, buffer);
5028
+ const stat2 = await fs2.stat(targetPath);
4508
5029
  const result = {
4509
- path: pathValue ? `/${pathValue}/${filename}` : `/${filename}`,
5030
+ path: path2.posix.join("/", relativeDirectory, safeFilename),
4510
5031
  name: filename,
4511
5032
  is_dir: false,
4512
5033
  size: stat2.size,
@@ -4514,9 +5035,63 @@ var WorkspaceController = class {
4514
5035
  };
4515
5036
  return reply.status(200).send({ success: true, data: result });
4516
5037
  } catch (error) {
5038
+ const message = error instanceof Error ? error.message : String(error);
4517
5039
  return reply.status(500).send({
4518
5040
  success: false,
4519
- error: `Upload handler error: ${error?.message || String(error)}`
5041
+ error: `Upload handler error: ${message}`
5042
+ });
5043
+ }
5044
+ }
5045
+ /**
5046
+ * Browse local filesystem directories for the folder picker.
5047
+ *
5048
+ * Security constraints:
5049
+ * - Restricted to os.homedir()
5050
+ * - Blacklists system directories
5051
+ * - Returns only directory names, no file contents or hidden files
5052
+ */
5053
+ async browseFilesystem(request, reply) {
5054
+ const homeDir = os.homedir();
5055
+ const requestedPath = request.query.path || homeDir;
5056
+ const SYSTEM_BLACKLIST = ["/etc", "/sys", "/proc", "/var", "/tmp", "/dev"];
5057
+ try {
5058
+ const resolved = await resolvePathWithinRoot(homeDir, requestedPath);
5059
+ const normalized = resolved.targetPath;
5060
+ for (const blacklisted of SYSTEM_BLACKLIST) {
5061
+ if (isPathWithinRoot(blacklisted, normalized)) {
5062
+ return reply.status(403).send({
5063
+ success: false,
5064
+ error: `Cannot browse system directory: ${blacklisted}`
5065
+ });
5066
+ }
5067
+ }
5068
+ const entries = await fs2.readdir(normalized, { withFileTypes: true });
5069
+ const dirEntries = entries.filter((entry) => !entry.name.startsWith(".") && entry.isDirectory()).map((entry) => ({
5070
+ name: entry.name,
5071
+ path: path2.join(normalized, entry.name),
5072
+ isDirectory: true
5073
+ }));
5074
+ const parentPath = path2.dirname(normalized);
5075
+ const hasParent = parentPath !== normalized && isPathWithinRoot(resolved.rootPath, parentPath);
5076
+ return {
5077
+ success: true,
5078
+ data: {
5079
+ path: normalized,
5080
+ parentPath: hasParent ? parentPath : void 0,
5081
+ entries: dirEntries
5082
+ }
5083
+ };
5084
+ } catch (error) {
5085
+ if (error instanceof PathOutsideRootError) {
5086
+ return reply.status(403).send({
5087
+ success: false,
5088
+ error: "Browse restricted to home directory"
5089
+ });
5090
+ }
5091
+ const message = error instanceof Error ? error.message : String(error);
5092
+ return reply.status(400).send({
5093
+ success: false,
5094
+ error: `Cannot read directory: ${message}`
4520
5095
  });
4521
5096
  }
4522
5097
  }
@@ -4577,6 +5152,10 @@ function registerWorkspaceRoutes(app2) {
4577
5152
  "/api/workspaces/:workspaceId/projects/:projectId/viewfile",
4578
5153
  controller.viewFile.bind(controller)
4579
5154
  );
5155
+ app2.get(
5156
+ "/api/filesystem/browse",
5157
+ controller.browseFilesystem.bind(controller)
5158
+ );
4580
5159
  }
4581
5160
 
4582
5161
  // src/controllers/database-configs.ts
@@ -4796,7 +5375,7 @@ async function testDatabaseConnection(request, reply) {
4796
5375
  await db.connect();
4797
5376
  await db.listTables();
4798
5377
  const latency = Date.now() - startTime;
4799
- await new Promise((resolve) => setTimeout(resolve, 100));
5378
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
4800
5379
  await db.disconnect();
4801
5380
  await sqlDatabaseManager.removeDatabase(tenantId, testKey);
4802
5381
  return {
@@ -5630,11 +6209,12 @@ var EvalRunner = class {
5630
6209
  constructor() {
5631
6210
  this.runs = /* @__PURE__ */ new Map();
5632
6211
  this.eventEmitter = new EventEmitter();
6212
+ this.processStartedAt = /* @__PURE__ */ new Date();
5633
6213
  }
5634
6214
  getEventEmitter() {
5635
6215
  return this.eventEmitter;
5636
6216
  }
5637
- async startRun(tenantId, projectId) {
6217
+ async startRun(tenantId, projectId, suiteIds) {
5638
6218
  const store = this.getEvalStore();
5639
6219
  const project = await store.getProjectById(tenantId, projectId);
5640
6220
  if (!project) throw new Error("Project not found");
@@ -5644,10 +6224,27 @@ var EvalRunner = class {
5644
6224
  }
5645
6225
  }
5646
6226
  const existingRuns = await store.getRunsByTenant(tenantId, { projectId, status: "running" });
5647
- if (existingRuns.length > 0) {
6227
+ for (const run of existingRuns) {
6228
+ const orphaned = !this.runs.has(run.id) && run.createdAt < this.processStartedAt;
6229
+ if (orphaned) {
6230
+ await store.updateRunStatus(tenantId, run.id, {
6231
+ status: "failed",
6232
+ error: "Gateway restarted \u2014 run orphaned",
6233
+ completedAt: /* @__PURE__ */ new Date()
6234
+ });
6235
+ }
6236
+ }
6237
+ const stillRunning = await store.getRunsByTenant(tenantId, { projectId, status: "running" });
6238
+ if (stillRunning.length > 0) {
5648
6239
  throw new Error("A run is already in progress for this project");
5649
6240
  }
5650
- const suites = await store.getSuitesByProject(tenantId, projectId);
6241
+ let suites = await store.getSuitesByProject(tenantId, projectId);
6242
+ if (suiteIds && suiteIds.length > 0) {
6243
+ suites = suites.filter((s) => suiteIds.includes(s.id));
6244
+ if (suites.length === 0) {
6245
+ throw new Error("No suites match the requested suiteIds filter");
6246
+ }
6247
+ }
5651
6248
  const evalSuites = [];
5652
6249
  let totalCases = 0;
5653
6250
  for (const suite of suites) {
@@ -5690,6 +6287,9 @@ var EvalRunner = class {
5690
6287
  const runId = uuidv43();
5691
6288
  const concurrency = Math.max(1, Math.floor(project.concurrency) || 3);
5692
6289
  await store.createRun(tenantId, projectId, runId, { totalCases, concurrency });
6290
+ const tsc = project.targetServerConfig ?? {};
6291
+ const workspaceId = tsc.workspace_id || "";
6292
+ const projectIdFromConfig = tsc.project_id || "";
5693
6293
  const projectConfig = {
5694
6294
  projectName: project.name,
5695
6295
  version: project.version,
@@ -5697,7 +6297,9 @@ var EvalRunner = class {
5697
6297
  suites: evalSuites,
5698
6298
  judge_agent_config: { modelKey: resolvedJudgeModelKey },
5699
6299
  lattice_server_config: {
5700
- tenant_id: tenantId
6300
+ tenant_id: tenantId,
6301
+ workspace_id: workspaceId,
6302
+ project_id: projectIdFromConfig
5701
6303
  },
5702
6304
  concurrency
5703
6305
  };
@@ -5741,6 +6343,15 @@ var EvalRunner = class {
5741
6343
  const runPromise = (async () => {
5742
6344
  try {
5743
6345
  const evalProject = new LatticeEvalProject(projectConfig, onCaseComplete);
6346
+ const calibration = await evalProject.calibrateJudge();
6347
+ if (!calibration.ok) {
6348
+ await store.updateRunStatus(tenantId, runId, {
6349
+ status: "failed",
6350
+ error: `Judge calibration failed: ${calibration.reason}`,
6351
+ completedAt: /* @__PURE__ */ new Date()
6352
+ });
6353
+ throw new Error(`Judge calibration failed: ${calibration.reason}`);
6354
+ }
5744
6355
  const { report } = await evalProject.runAllSuitesBatch(concurrency, abortController.signal);
5745
6356
  if (!abortController.signal.aborted) {
5746
6357
  const completedCount = stats.passed + stats.failed;
@@ -6128,11 +6739,12 @@ function registerEvalRoutes(app2) {
6128
6739
  try {
6129
6740
  const tenantId = getTenantId13(request);
6130
6741
  const { pid } = request.params;
6131
- const runId = await evalRunner.startRun(tenantId, pid);
6742
+ const body = request.body ?? {};
6743
+ const runId = await evalRunner.startRun(tenantId, pid, body?.suiteIds);
6132
6744
  reply.status(202).send({ success: true, message: "Run started", data: { run_id: runId } });
6133
6745
  } catch (err) {
6134
6746
  const msg = err.message;
6135
- const code = msg === "Project not found" ? 404 : msg.includes("already in progress") ? 409 : 500;
6747
+ const code = msg === "Project not found" ? 404 : msg.includes("already in progress") ? 409 : msg.includes("No suites match") ? 400 : 500;
6136
6748
  reply.status(code).send({ success: false, message: msg });
6137
6749
  }
6138
6750
  });
@@ -7109,8 +7721,8 @@ function getTenantId14(request) {
7109
7721
  return request.headers["x-tenant-id"] || "default";
7110
7722
  }
7111
7723
  async function getInstallationStore() {
7112
- const { getStoreLattice: getStoreLattice30 } = await import("@axiom-lattice/core");
7113
- const store = getStoreLattice30("default", "channelInstallation").store;
7724
+ const { getStoreLattice: getStoreLattice31 } = await import("@axiom-lattice/core");
7725
+ const store = getStoreLattice31("default", "channelInstallation").store;
7114
7726
  if (store) return store;
7115
7727
  const { PostgreSQLChannelInstallationStore } = await import("@axiom-lattice/pg-stores");
7116
7728
  const databaseUrl = process.env.DATABASE_URL;
@@ -7591,8 +8203,8 @@ var log = {
7591
8203
  };
7592
8204
  var connections = /* @__PURE__ */ new Map();
7593
8205
  var pending = /* @__PURE__ */ new Map();
7594
- function getAgentIdFromPath(path3) {
7595
- const match = path3.match(/^\/api\/a2a\/bridge\/proxy\/([^/]+)/);
8206
+ function getAgentIdFromPath(path4) {
8207
+ const match = path4.match(/^\/api\/a2a\/bridge\/proxy\/([^/]+)/);
7596
8208
  return match ? match[1] : null;
7597
8209
  }
7598
8210
  function handleBridgeConnection(socket, _request) {
@@ -7700,12 +8312,12 @@ async function handleBridgeProxy(request, reply) {
7700
8312
  };
7701
8313
  const timeout = 3e5;
7702
8314
  try {
7703
- const response = await new Promise((resolve, reject) => {
8315
+ const response = await new Promise((resolve3, reject) => {
7704
8316
  const timer = setTimeout(() => {
7705
8317
  pending.delete(requestId);
7706
8318
  reject(new Error(`Bridge request timeout after ${timeout}ms`));
7707
8319
  }, timeout);
7708
- pending.set(requestId, { resolve, reject, timer, agentId });
8320
+ pending.set(requestId, { resolve: resolve3, reject, timer, agentId });
7709
8321
  try {
7710
8322
  if (conn.ws.readyState !== conn.ws.OPEN) {
7711
8323
  pending.delete(requestId);
@@ -7828,10 +8440,274 @@ var registerSTTRoutes = (app2) => {
7828
8440
  );
7829
8441
  };
7830
8442
 
8443
+ // src/controllers/local-a2a.ts
8444
+ import { getStoreLattice as getStoreLattice15 } from "@axiom-lattice/core";
8445
+ import {
8446
+ AgentType as AgentType2
8447
+ } from "@axiom-lattice/protocols";
8448
+ function getTenantId17(request) {
8449
+ const userTenantId = request.user?.tenantId;
8450
+ if (userTenantId) return userTenantId;
8451
+ return request.headers["x-tenant-id"] || "default";
8452
+ }
8453
+ function getLocalRuntime(assistant) {
8454
+ const graphDefinition = assistant.graphDefinition;
8455
+ if (!graphDefinition || typeof graphDefinition !== "object") return void 0;
8456
+ if (graphDefinition.type !== AgentType2.A2A_REMOTE) return void 0;
8457
+ return graphDefinition.localRuntime;
8458
+ }
8459
+ async function getLocalA2AAssistants(tenantId) {
8460
+ const storeLattice = getStoreLattice15("default", "assistant");
8461
+ const assistants = await storeLattice.store.getAllAssistants(tenantId);
8462
+ const results = [];
8463
+ for (const assistant of assistants) {
8464
+ const runtime = getLocalRuntime(assistant);
8465
+ if (runtime) {
8466
+ results.push({ assistant, localRuntime: runtime });
8467
+ }
8468
+ }
8469
+ return results;
8470
+ }
8471
+ async function getLocalA2AAssistant(tenantId, assistantId) {
8472
+ const storeLattice = getStoreLattice15("default", "assistant");
8473
+ const assistant = await storeLattice.store.getAssistantById(tenantId, assistantId);
8474
+ if (!assistant) return null;
8475
+ const runtime = getLocalRuntime(assistant);
8476
+ if (!runtime) return null;
8477
+ return { assistant, localRuntime: runtime };
8478
+ }
8479
+ async function requireManagedAssistant(request, reply) {
8480
+ const assistantId = request.params.assistantId;
8481
+ if (!assistantId) {
8482
+ reply.status(400).send({ success: false, error: "assistantId is required" });
8483
+ return null;
8484
+ }
8485
+ const registered = await getLocalA2AAssistant(getTenantId17(request), assistantId);
8486
+ if (!registered) {
8487
+ reply.status(404).send({
8488
+ success: false,
8489
+ error: `Assistant ${assistantId} is not a local managed A2A assistant`
8490
+ });
8491
+ return null;
8492
+ }
8493
+ return { assistantId, localRuntime: registered.localRuntime };
8494
+ }
8495
+ async function getLocalA2AStatus(request, reply) {
8496
+ const tenantId = getTenantId17(request);
8497
+ const managed = await getLocalA2AAssistants(tenantId);
8498
+ const states = await Promise.all(
8499
+ managed.map(
8500
+ ({ assistant, localRuntime }) => localA2AService.getRegisteredState(assistant.id, localRuntime)
8501
+ )
8502
+ );
8503
+ reply.send({ success: true, data: { records: states, total: states.length } });
8504
+ }
8505
+ async function getLocalA2AProvider(request, reply) {
8506
+ const registered = await requireManagedAssistant(request, reply);
8507
+ if (!registered) return;
8508
+ const state = await localA2AService.getRegisteredState(
8509
+ registered.assistantId,
8510
+ registered.localRuntime
8511
+ );
8512
+ reply.send({ success: true, data: state });
8513
+ }
8514
+ async function startLocalA2AProvider(request, reply) {
8515
+ const registered = await requireManagedAssistant(request, reply);
8516
+ if (!registered) return;
8517
+ const state = await localA2AService.startProvider(
8518
+ registered.assistantId,
8519
+ registered.localRuntime
8520
+ );
8521
+ reply.send({ success: true, data: state });
8522
+ }
8523
+ async function stopLocalA2AProvider(request, reply) {
8524
+ const registered = await requireManagedAssistant(request, reply);
8525
+ if (!registered) return;
8526
+ const state = await localA2AService.stopProvider(registered.assistantId);
8527
+ reply.send({ success: true, data: state });
8528
+ }
8529
+ async function restartLocalA2AProvider(request, reply) {
8530
+ const registered = await requireManagedAssistant(request, reply);
8531
+ if (!registered) return;
8532
+ const state = await localA2AService.restartProvider(
8533
+ registered.assistantId,
8534
+ registered.localRuntime
8535
+ );
8536
+ reply.send({ success: true, data: state });
8537
+ }
8538
+
8539
+ // src/config/local-a2a-templates.ts
8540
+ import { cliA2AEntryPath } from "@axiom-lattice/cli-a2a/cli-path";
8541
+ function buildCliA2ACommand(provider, agentName, agentDescription) {
8542
+ return {
8543
+ executable: process.execPath,
8544
+ args: [
8545
+ cliA2AEntryPath,
8546
+ "--provider",
8547
+ provider,
8548
+ "--hostname",
8549
+ "localhost",
8550
+ "--advertise-host",
8551
+ "localhost",
8552
+ "--agent-name",
8553
+ agentName,
8554
+ "--agent-description",
8555
+ agentDescription,
8556
+ "--log-level",
8557
+ "info"
8558
+ ]
8559
+ };
8560
+ }
8561
+ var LOCAL_A2A_TEMPLATES = [
8562
+ {
8563
+ key: "claude",
8564
+ name: "Claude Code",
8565
+ command: buildCliA2ACommand(
8566
+ "claude",
8567
+ "Claude Code",
8568
+ "Local Claude Code wrapper exposed through A2A."
8569
+ )
8570
+ },
8571
+ {
8572
+ key: "codex",
8573
+ name: "Codex",
8574
+ command: buildCliA2ACommand(
8575
+ "codex",
8576
+ "Codex",
8577
+ "Local Codex wrapper exposed through A2A."
8578
+ )
8579
+ },
8580
+ {
8581
+ key: "opencode",
8582
+ name: "OpenCode",
8583
+ command: buildCliA2ACommand(
8584
+ "opencode",
8585
+ "OpenCode",
8586
+ "Local OpenCode wrapper exposed through A2A."
8587
+ )
8588
+ }
8589
+ ];
8590
+
8591
+ // src/controllers/local-a2a-templates.ts
8592
+ async function getLocalA2ATemplates(_request, reply) {
8593
+ reply.send({
8594
+ success: true,
8595
+ data: {
8596
+ records: LOCAL_A2A_TEMPLATES,
8597
+ total: LOCAL_A2A_TEMPLATES.length
8598
+ }
8599
+ });
8600
+ }
8601
+
8602
+ // src/routes/a2a-test-connection.ts
8603
+ import { randomUUID as randomUUID8 } from "crypto";
8604
+ function describeTestConnectionError(error, agentCardUrl) {
8605
+ if (!(error instanceof Error)) {
8606
+ return `Failed to fetch agent card from ${agentCardUrl}: ${String(error)}`;
8607
+ }
8608
+ const cause = error.cause;
8609
+ const causeParts = [];
8610
+ if (cause instanceof Error) {
8611
+ causeParts.push(cause.message);
8612
+ const causeCode = cause.code;
8613
+ if (causeCode && !causeParts.some((part) => part.includes(causeCode))) {
8614
+ causeParts.push(causeCode);
8615
+ }
8616
+ } else if (typeof cause === "object" && cause !== null) {
8617
+ const code = "code" in cause ? String(cause.code) : "";
8618
+ const message = "message" in cause ? String(cause.message) : "";
8619
+ if (message) causeParts.push(message);
8620
+ if (code && !message.includes(code)) causeParts.push(code);
8621
+ }
8622
+ const detail = causeParts.length > 0 ? ` (${causeParts.join(", ")})` : "";
8623
+ return `Failed to fetch agent card from ${agentCardUrl}: ${error.message}${detail}`;
8624
+ }
8625
+ async function ensureLocalRuntimeForConnectionTest(localRuntime, agentCardUrl) {
8626
+ const commandValidation = validateLocalRuntimeCommand(localRuntime);
8627
+ if (!commandValidation.ok) {
8628
+ return { error: commandValidation.error };
8629
+ }
8630
+ const testRuntime = {
8631
+ ...localRuntime,
8632
+ agentCardUrl,
8633
+ command: {
8634
+ ...localRuntime.command,
8635
+ executable: localRuntime.command.executable.trim()
8636
+ }
8637
+ };
8638
+ const portValidation = validateLocalRuntimePort(testRuntime, agentCardUrl);
8639
+ if (!portValidation.ok) {
8640
+ return { error: portValidation.error };
8641
+ }
8642
+ const state = await localA2AService.startProvider(
8643
+ `test-${randomUUID8()}`,
8644
+ testRuntime
8645
+ );
8646
+ if (state.status === "running") return { runtimeId: state.runtimeId };
8647
+ const detail = state.message ? ` ${state.message}` : "";
8648
+ localA2AService.removeState(state.runtimeId);
8649
+ return { error: `Local managed A2A server could not start for connection test.${detail}` };
8650
+ }
8651
+ async function readShortResponseBody(resp) {
8652
+ const body = await resp.text().catch(() => "");
8653
+ return body.length > 500 ? `${body.slice(0, 500)}...` : body;
8654
+ }
8655
+ function registerA2ATestConnectionRoute(app2) {
8656
+ app2.post("/api/a2a/test-connection", async (request, reply) => {
8657
+ const { agentCardUrl, apiKey, localRuntime } = request.body ?? {};
8658
+ const normalizedAgentCardUrl = normalizeA2AAgentCardUrl(
8659
+ agentCardUrl,
8660
+ localRuntime ? "http://" : "https://"
8661
+ );
8662
+ if (!normalizedAgentCardUrl) {
8663
+ return reply.status(400).send({ success: false, error: "agentCardUrl is required" });
8664
+ }
8665
+ let testRuntimeId = null;
8666
+ try {
8667
+ const headers = {};
8668
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
8669
+ if (localRuntime) {
8670
+ const localStartResult = await ensureLocalRuntimeForConnectionTest(localRuntime, normalizedAgentCardUrl);
8671
+ if ("error" in localStartResult) {
8672
+ return reply.send({ success: true, data: { reachable: false, error: localStartResult.error } });
8673
+ }
8674
+ testRuntimeId = localStartResult.runtimeId;
8675
+ }
8676
+ const resp = await fetch(normalizedAgentCardUrl, { headers, signal: AbortSignal.timeout(8e3) });
8677
+ if (!resp.ok) {
8678
+ const body = await readShortResponseBody(resp);
8679
+ const suffix = body ? `: ${body}` : "";
8680
+ return reply.send({ success: true, data: { reachable: false, error: `HTTP ${resp.status} ${resp.statusText}${suffix}` } });
8681
+ }
8682
+ const card = await resp.json();
8683
+ return reply.send({ success: true, data: { reachable: true, agentName: card.name, agentCard: card } });
8684
+ } catch (error) {
8685
+ return reply.send({
8686
+ success: true,
8687
+ data: {
8688
+ reachable: false,
8689
+ error: describeTestConnectionError(error, normalizedAgentCardUrl)
8690
+ }
8691
+ });
8692
+ } finally {
8693
+ if (testRuntimeId) {
8694
+ localA2AService.removeState(testRuntimeId);
8695
+ }
8696
+ }
8697
+ });
8698
+ }
8699
+
7831
8700
  // src/routes/index.ts
7832
8701
  var registerLatticeRoutes = (app2, channelDeps) => {
7833
8702
  registerA2ABridgeRoutes(app2);
7834
8703
  registerSTTRoutes(app2);
8704
+ app2.get("/api/local-a2a", getLocalA2AStatus);
8705
+ app2.get("/api/local-a2a/templates", getLocalA2ATemplates);
8706
+ app2.get("/api/local-a2a/:assistantId", getLocalA2AProvider);
8707
+ app2.post("/api/local-a2a/:assistantId/start", startLocalA2AProvider);
8708
+ app2.post("/api/local-a2a/:assistantId/stop", stopLocalA2AProvider);
8709
+ app2.post("/api/local-a2a/:assistantId/restart", restartLocalA2AProvider);
8710
+ registerA2ATestConnectionRoute(app2);
7835
8711
  app2.post("/api/runs", createRun);
7836
8712
  app2.post("/api/resume_stream", resumeStream);
7837
8713
  app2.post("/api/assistants/:assistantId/threads/:threadId/abort", abortRun);
@@ -8126,9 +9002,9 @@ function registerResourceRoutes(app2, controller) {
8126
9002
  import { ExportableEntityRegistry as ExportableEntityRegistry3 } from "@axiom-lattice/core";
8127
9003
 
8128
9004
  // src/export_registrations/skill.registration.ts
8129
- import { getStoreLattice as getStoreLattice15 } from "@axiom-lattice/core";
9005
+ import { getStoreLattice as getStoreLattice16 } from "@axiom-lattice/core";
8130
9006
  function getStore2() {
8131
- return getStoreLattice15("default", "skill").store;
9007
+ return getStoreLattice16("default", "skill").store;
8132
9008
  }
8133
9009
  var skillRegistration = {
8134
9010
  entityType: "skill",
@@ -8203,9 +9079,9 @@ var skillRegistration = {
8203
9079
  };
8204
9080
 
8205
9081
  // src/export_registrations/agent.registration.ts
8206
- import { getStoreLattice as getStoreLattice16 } from "@axiom-lattice/core";
9082
+ import { getStoreLattice as getStoreLattice17 } from "@axiom-lattice/core";
8207
9083
  function getStore3() {
8208
- return getStoreLattice16("default", "assistant").store;
9084
+ return getStoreLattice17("default", "assistant").store;
8209
9085
  }
8210
9086
  var agentRegistration = {
8211
9087
  entityType: "agent",
@@ -8272,9 +9148,9 @@ var agentRegistration = {
8272
9148
  };
8273
9149
 
8274
9150
  // src/export_registrations/binding.registration.ts
8275
- import { getStoreLattice as getStoreLattice17 } from "@axiom-lattice/core";
9151
+ import { getStoreLattice as getStoreLattice18 } from "@axiom-lattice/core";
8276
9152
  function getStore4() {
8277
- return getStoreLattice17("default", "channelBinding").store;
9153
+ return getStoreLattice18("default", "channelBinding").store;
8278
9154
  }
8279
9155
  var bindingRegistration = {
8280
9156
  entityType: "binding",
@@ -8362,9 +9238,9 @@ var bindingRegistration = {
8362
9238
  };
8363
9239
 
8364
9240
  // src/export_registrations/database-config.registration.ts
8365
- import { getStoreLattice as getStoreLattice18 } from "@axiom-lattice/core";
9241
+ import { getStoreLattice as getStoreLattice19 } from "@axiom-lattice/core";
8366
9242
  function getStore5() {
8367
- return getStoreLattice18("default", "database").store;
9243
+ return getStoreLattice19("default", "database").store;
8368
9244
  }
8369
9245
  var databaseConfigRegistration = {
8370
9246
  entityType: "database_config",
@@ -8444,9 +9320,9 @@ var databaseConfigRegistration = {
8444
9320
  };
8445
9321
 
8446
9322
  // src/export_registrations/metrics-config.registration.ts
8447
- import { getStoreLattice as getStoreLattice19 } from "@axiom-lattice/core";
9323
+ import { getStoreLattice as getStoreLattice20 } from "@axiom-lattice/core";
8448
9324
  function getStore6() {
8449
- return getStoreLattice19("default", "metrics").store;
9325
+ return getStoreLattice20("default", "metrics").store;
8450
9326
  }
8451
9327
  var metricsConfigRegistration = {
8452
9328
  entityType: "metrics_config",
@@ -8526,9 +9402,9 @@ var metricsConfigRegistration = {
8526
9402
  };
8527
9403
 
8528
9404
  // src/export_registrations/mcp-config.registration.ts
8529
- import { getStoreLattice as getStoreLattice20 } from "@axiom-lattice/core";
9405
+ import { getStoreLattice as getStoreLattice21 } from "@axiom-lattice/core";
8530
9406
  function getStore7() {
8531
- return getStoreLattice20("default", "mcp").store;
9407
+ return getStoreLattice21("default", "mcp").store;
8532
9408
  }
8533
9409
  var mcpConfigRegistration = {
8534
9410
  entityType: "mcp_config",
@@ -8611,9 +9487,9 @@ var mcpConfigRegistration = {
8611
9487
  };
8612
9488
 
8613
9489
  // src/export_registrations/connection.registration.ts
8614
- import { getStoreLattice as getStoreLattice21 } from "@axiom-lattice/core";
9490
+ import { getStoreLattice as getStoreLattice22 } from "@axiom-lattice/core";
8615
9491
  function getStore8() {
8616
- return getStoreLattice21("default", "connection").store;
9492
+ return getStoreLattice22("default", "connection").store;
8617
9493
  }
8618
9494
  async function listAllConnections(tenantId) {
8619
9495
  const store = getStore8();
@@ -8695,9 +9571,9 @@ var connectionRegistration = {
8695
9571
  };
8696
9572
 
8697
9573
  // src/export_registrations/collection.registration.ts
8698
- import { getStoreLattice as getStoreLattice22 } from "@axiom-lattice/core";
9574
+ import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
8699
9575
  function getStore9() {
8700
- return getStoreLattice22("default", "collection").store;
9576
+ return getStoreLattice23("default", "collection").store;
8701
9577
  }
8702
9578
  var collectionRegistration = {
8703
9579
  entityType: "collection",
@@ -8768,9 +9644,9 @@ var collectionRegistration = {
8768
9644
  };
8769
9645
 
8770
9646
  // src/export_registrations/channel-installation.registration.ts
8771
- import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
9647
+ import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
8772
9648
  function getStore10() {
8773
- return getStoreLattice23("default", "channelInstallation").store;
9649
+ return getStoreLattice24("default", "channelInstallation").store;
8774
9650
  }
8775
9651
  var channelInstallationRegistration = {
8776
9652
  entityType: "channel_installation",
@@ -8845,9 +9721,9 @@ var channelInstallationRegistration = {
8845
9721
  };
8846
9722
 
8847
9723
  // src/export_registrations/menu.registration.ts
8848
- import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
9724
+ import { getStoreLattice as getStoreLattice25 } from "@axiom-lattice/core";
8849
9725
  function getStore11() {
8850
- return getStoreLattice24("default", "menu").store;
9726
+ return getStoreLattice25("default", "menu").store;
8851
9727
  }
8852
9728
  var menuRegistration = {
8853
9729
  entityType: "menu",
@@ -8925,9 +9801,9 @@ var menuRegistration = {
8925
9801
  };
8926
9802
 
8927
9803
  // src/export_registrations/task.registration.ts
8928
- import { getStoreLattice as getStoreLattice25 } from "@axiom-lattice/core";
9804
+ import { getStoreLattice as getStoreLattice26 } from "@axiom-lattice/core";
8929
9805
  function getStore12() {
8930
- return getStoreLattice25("default", "task").store;
9806
+ return getStoreLattice26("default", "task").store;
8931
9807
  }
8932
9808
  var taskRegistration = {
8933
9809
  entityType: "task",
@@ -9013,9 +9889,9 @@ var taskRegistration = {
9013
9889
  };
9014
9890
 
9015
9891
  // src/export_registrations/a2a-api-key.registration.ts
9016
- import { getStoreLattice as getStoreLattice26 } from "@axiom-lattice/core";
9892
+ import { getStoreLattice as getStoreLattice27 } from "@axiom-lattice/core";
9017
9893
  function getStore13() {
9018
- return getStoreLattice26("default", "a2aApiKey").store;
9894
+ return getStoreLattice27("default", "a2aApiKey").store;
9019
9895
  }
9020
9896
  var a2aApiKeyRegistration = {
9021
9897
  entityType: "a2a_api_key",
@@ -9086,9 +9962,9 @@ var a2aApiKeyRegistration = {
9086
9962
  };
9087
9963
 
9088
9964
  // src/export_registrations/eval.registration.ts
9089
- import { getStoreLattice as getStoreLattice27 } from "@axiom-lattice/core";
9965
+ import { getStoreLattice as getStoreLattice28 } from "@axiom-lattice/core";
9090
9966
  function getStore14() {
9091
- return getStoreLattice27("default", "eval").store;
9967
+ return getStoreLattice28("default", "eval").store;
9092
9968
  }
9093
9969
  var evalRegistration = {
9094
9970
  entityType: "eval",
@@ -9432,10 +10308,10 @@ function registerAllBuiltinEntities() {
9432
10308
 
9433
10309
  // src/router/MessageRouter.ts
9434
10310
  import {
9435
- getStoreLattice as getStoreLattice28,
10311
+ getStoreLattice as getStoreLattice29,
9436
10312
  agentInstanceManager as agentInstanceManager6
9437
10313
  } from "@axiom-lattice/core";
9438
- import { randomUUID as randomUUID8 } from "crypto";
10314
+ import { randomUUID as randomUUID9 } from "crypto";
9439
10315
  var BindingNotFoundError = class extends Error {
9440
10316
  constructor(message) {
9441
10317
  super(message);
@@ -9595,7 +10471,7 @@ var MessageRouter = class {
9595
10471
  channel: message.channel,
9596
10472
  adapterChannel: adapter.channel
9597
10473
  }, "Thread resolved by adapter strategy");
9598
- const threadStore = getStoreLattice28("default", "thread").store;
10474
+ const threadStore = getStoreLattice29("default", "thread").store;
9599
10475
  try {
9600
10476
  await threadStore.createThread(
9601
10477
  tenantId,
@@ -9632,8 +10508,8 @@ var MessageRouter = class {
9632
10508
  }
9633
10509
  }
9634
10510
  if (!threadId) {
9635
- const threadStore = getStoreLattice28("default", "thread").store;
9636
- const newThreadId = randomUUID8();
10511
+ const threadStore = getStoreLattice29("default", "thread").store;
10512
+ const newThreadId = randomUUID9();
9637
10513
  console.log({
9638
10514
  event: "dispatch:thread:create",
9639
10515
  agentId,
@@ -10058,7 +10934,7 @@ ${summary}`
10058
10934
  console.log(
10059
10935
  `\u5C06\u5728 ${delayMs}ms \u540E\u91CD\u8BD5\u4EFB\u52A1 (${nextRetryCount}/${maxRetries})`
10060
10936
  );
10061
- await new Promise((resolve) => setTimeout(resolve, delayMs));
10937
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
10062
10938
  return handleAgentTask(taskRequest, nextRetryCount);
10063
10939
  }
10064
10940
  if (callback_event) {
@@ -10238,7 +11114,7 @@ import {
10238
11114
  getLoggerLattice,
10239
11115
  loggerLatticeManager,
10240
11116
  sandboxLatticeManager as sandboxLatticeManager2,
10241
- getStoreLattice as getStoreLattice29,
11117
+ getStoreLattice as getStoreLattice30,
10242
11118
  agentInstanceManager as agentInstanceManager8,
10243
11119
  createSandboxProvider,
10244
11120
  TokenCache,
@@ -10348,9 +11224,9 @@ app.register(multipart, {
10348
11224
  });
10349
11225
  app.register(websocket);
10350
11226
  var __filename = fileURLToPath(import.meta.url);
10351
- var __dirname = path2.dirname(__filename);
11227
+ var __dirname = path3.dirname(__filename);
10352
11228
  app.register(staticPlugin, {
10353
- root: path2.join(__dirname, "../public"),
11229
+ root: path3.join(__dirname, "../public"),
10354
11230
  prefix: "/"
10355
11231
  });
10356
11232
  app.setErrorHandler((error, request, reply) => {
@@ -10391,7 +11267,7 @@ function getConfiguredSandboxProvider() {
10391
11267
  }
10392
11268
  async function restoreMcpConnections() {
10393
11269
  try {
10394
- const storeLattice = getStoreLattice29("default", "mcp");
11270
+ const storeLattice = getStoreLattice30("default", "mcp");
10395
11271
  const store = storeLattice.store;
10396
11272
  if (!store) {
10397
11273
  logger4.info("MCP store not configured, skipping connection restoration");
@@ -10460,7 +11336,7 @@ var start = async (config) => {
10460
11336
  channelDeps = { router, installationStore };
10461
11337
  try {
10462
11338
  const a2aKeyStore = getStore15("default", "a2aApiKey").store;
10463
- const a2a = await import("./a2a-ERG5RMUW.mjs");
11339
+ const a2a = await import("./a2a-QMHEH3ZT.mjs");
10464
11340
  a2a.setA2AKeyStore(a2aKeyStore);
10465
11341
  await a2a.refreshStoreKeyMap();
10466
11342
  logger4.info("A2A key store initialized");
@@ -10473,7 +11349,7 @@ var start = async (config) => {
10473
11349
  }
10474
11350
  setEvalRunService(evalRunner);
10475
11351
  try {
10476
- const menuStore = getStoreLattice29("default", "menu").store;
11352
+ const menuStore = getStoreLattice30("default", "menu").store;
10477
11353
  setMenuRegistry(menuStore);
10478
11354
  logger4.info("Menu registry initialized");
10479
11355
  } catch {
@@ -10489,7 +11365,7 @@ var start = async (config) => {
10489
11365
  }
10490
11366
  try {
10491
11367
  const { ResourceController } = await import("./resources-VA7LSDKN.mjs");
10492
- const sharedResourceStore = getStoreLattice29("default", "sharedResource").store;
11368
+ const sharedResourceStore = getStoreLattice30("default", "sharedResource").store;
10493
11369
  const cache = new TokenCache();
10494
11370
  const resourceController = new ResourceController({
10495
11371
  store: sharedResourceStore,
@@ -10520,11 +11396,6 @@ var start = async (config) => {
10520
11396
  const target_port = config?.port || Number(process.env.PORT) || 4001;
10521
11397
  await app.listen({ port: target_port, host: "0.0.0.0" });
10522
11398
  logger4.info(`Lattice Gateway is running on port: ${target_port}`);
10523
- try {
10524
- logger4.info("AgentLifecycleManager initialized");
10525
- } catch (error) {
10526
- logger4.warn("Failed to initialize AgentLifecycleManager", { error });
10527
- }
10528
11399
  const queueServiceConfig = config?.queueServiceConfig;
10529
11400
  if (queueServiceConfig) {
10530
11401
  setQueueServiceType(queueServiceConfig.type);