@growthub/cli 0.9.13 → 0.9.16

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/README.md +17 -5
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +27 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +41 -9
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/list-entities/route.js +67 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-source/route.js +124 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +127 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/register-resolver/route.js +119 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +41 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-adapters/route.js +21 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +634 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +126 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +130 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +1349 -222
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1048 -4
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +1540 -433
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/data-sources-api-registry.md +141 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +32 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +57 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolvers/README.md +133 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolvers/google-analytics.js +160 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +85 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapter-loader.js +58 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/README.md +63 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +284 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +194 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +33 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +113 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +79 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +211 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +126 -7
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +16 -0
  33. package/dist/index.js +1764 -40677
  34. package/package.json +2 -2
@@ -0,0 +1,634 @@
1
+ /**
2
+ * POST /api/workspace/sandbox-run
3
+ *
4
+ * Executes one row of a `sandbox-environment` governed Data Model object via
5
+ * the registered sandbox adapter, then writes the result into:
6
+ *
7
+ * 1. `growthub.source-records.json` (sidecar, versioned run history) —
8
+ * keyed by `sandbox:<objectId>:<slug(name)>`. Each invocation appends a
9
+ * record so the full history travels with the workspace artifact.
10
+ * 2. The row in `growthub.config.json` — stamps `status`, `lastTested`,
11
+ * and a compact `lastResponse` JSON so the existing Data Model drawer
12
+ * test bar surfaces the result with no UI rewrite.
13
+ *
14
+ * The route is provider-agnostic for **local** runs: adapters live under
15
+ * `lib/adapters/sandboxes/adapters/` plus bundled defaults.
16
+ *
17
+ * When `runLocality === "serverless"`, execution is delegated with an outbound
18
+ * HTTP request to an **API Registry** row referenced by `schedulerRegistryId`
19
+ * (same FK pattern as Data Source → registryId). Credentials resolve
20
+ * server-side only (authRef env); the JSON body never includes secret values.
21
+ * Your Edge function / QStash worker / cron handler returns JSON or plain text,
22
+ * surfaced as stdout / exitCode — keeping the sandbox row shape identical so
23
+ * Data Sources downstream can normalize either locality.
24
+ *
25
+ * Request body:
26
+ * { objectId: string, name: string }
27
+ *
28
+ * Response (success):
29
+ * {
30
+ * ok: boolean,
31
+ * status: "connected" | "failed",
32
+ * runId: string,
33
+ * adapter: string,
34
+ * runtime: string,
35
+ * exitCode: number | null,
36
+ * durationMs: number,
37
+ * persisted: boolean,
38
+ * sourceId: string | null,
39
+ * response: { // saved into row.lastResponse
40
+ * runLocality, schedulerRegistryId?, runtime, adapter, exitCode, durationMs,
41
+ * stdout, stderr, error?,
42
+ * envRefsResolved: string[], // slug names only — never values
43
+ * envRefsMissing: string[],
44
+ * networkAllow: boolean,
45
+ * allowList: string[],
46
+ * adapterMeta?: Record<string, unknown>
47
+ * }
48
+ * }
49
+ */
50
+
51
+ import { NextResponse } from "next/server";
52
+ import { promises as fs } from "node:fs";
53
+ import os from "node:os";
54
+ import path from "node:path";
55
+ import {
56
+ describePersistenceMode,
57
+ readWorkspaceConfig,
58
+ readWorkspaceSourceRecords,
59
+ writeWorkspaceConfig,
60
+ writeWorkspaceSourceRecords
61
+ } from "@/lib/workspace-config";
62
+ import {
63
+ DEFAULT_SANDBOX_ADAPTER,
64
+ DEFAULT_SANDBOX_RUN_LOCALITY,
65
+ KNOWN_SANDBOX_RUNTIMES,
66
+ SANDBOX_DEFAULT_TIMEOUT_MS,
67
+ SANDBOX_MAX_TIMEOUT_MS
68
+ } from "@/lib/workspace-schema";
69
+ import {
70
+ parseSandboxAllowList,
71
+ parseSandboxEnvRefs,
72
+ sandboxRunSourceId
73
+ } from "@/lib/workspace-data-model";
74
+ import {
75
+ ensureSandboxAdaptersLoaded,
76
+ getSandboxAdapter
77
+ } from "@/lib/adapters/sandboxes";
78
+
79
+ function envKeyCandidates(ref) {
80
+ const token = String(ref || "")
81
+ .trim()
82
+ .replace(/[^a-z0-9]+/gi, "_")
83
+ .replace(/^_+|_+$/g, "")
84
+ .toUpperCase();
85
+ return Array.from(new Set([
86
+ token,
87
+ token ? `${token}_API_KEY` : "",
88
+ token ? `${token}_TOKEN` : ""
89
+ ].filter(Boolean)));
90
+ }
91
+
92
+ function readServerSecret(authRef) {
93
+ for (const key of envKeyCandidates(authRef)) {
94
+ if (process.env[key]) return { key, value: process.env[key] };
95
+ }
96
+ return null;
97
+ }
98
+
99
+ function coerceBoolean(value) {
100
+ if (value === true || value === false) return value;
101
+ const text = String(value ?? "").trim().toLowerCase();
102
+ return ["true", "1", "on", "yes"].includes(text);
103
+ }
104
+
105
+ function normalizeRunLocality(row) {
106
+ const raw = String(row?.runLocality ?? "").trim().toLowerCase();
107
+ if (raw === "serverless") return "serverless";
108
+ if (raw === "local") return "local";
109
+ return DEFAULT_SANDBOX_RUN_LOCALITY;
110
+ }
111
+
112
+ function normalizeMethod(value) {
113
+ const method = String(value || "GET").trim().toUpperCase();
114
+ return ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method) ? method : "GET";
115
+ }
116
+
117
+ function buildSchedulerUrl(record) {
118
+ const baseUrl = String(record?.baseUrl || "").trim();
119
+ const endpoint = String(record?.endpoint || "").trim();
120
+ const raw = endpoint || baseUrl;
121
+ if (!raw) throw new Error("baseUrl or endpoint is required");
122
+ if (/^https?:\/\//i.test(endpoint)) return endpoint;
123
+ if (!baseUrl) throw new Error("baseUrl is required when endpoint is relative");
124
+ return `${baseUrl.replace(/\/+$/, "")}/${endpoint.replace(/^\/+/, "")}`;
125
+ }
126
+
127
+ function buildAuthHeaders(record, secretValue) {
128
+ if (!secretValue) return {};
129
+ const headerName = String(record?.authHeaderName || record?.authHeader || "x-api-key").trim();
130
+ if (!headerName) return {};
131
+ const prefix = String(record?.authPrefix || "").trim();
132
+ return { [headerName]: prefix ? `${prefix} ${secretValue}` : secretValue };
133
+ }
134
+
135
+ function findRegistryRecord(workspaceConfig, registryId) {
136
+ const id = String(registryId || "").trim();
137
+ if (!id) return null;
138
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
139
+ for (const objectItem of objects) {
140
+ if (objectItem?.objectType !== "api-registry") continue;
141
+ const rows = Array.isArray(objectItem.rows) ? objectItem.rows : [];
142
+ const match = rows.find(
143
+ (r) => String(r?.integrationId || "").trim() === id
144
+ || String(r?.id || "").trim() === id
145
+ || String(r?.Name || "").trim() === id
146
+ );
147
+ if (match) return match;
148
+ }
149
+ return null;
150
+ }
151
+
152
+ async function runServerlessScheduler({
153
+ workspaceConfig,
154
+ row,
155
+ runId,
156
+ ranAt,
157
+ workspaceId,
158
+ objectId,
159
+ sandboxName,
160
+ runtime,
161
+ adapterId,
162
+ agentHost,
163
+ command,
164
+ instructions,
165
+ timeoutMs,
166
+ networkAllow,
167
+ allowList,
168
+ envRefSlugs,
169
+ envRefsResolved,
170
+ envRefsMissing
171
+ }) {
172
+ const registryId = String(row.schedulerRegistryId || "").trim();
173
+ if (!registryId) {
174
+ return {
175
+ ok: false,
176
+ exitCode: null,
177
+ durationMs: 0,
178
+ stdout: "",
179
+ stderr: "",
180
+ error: "schedulerRegistryId is required when runLocality is serverless",
181
+ adapterMeta: { locality: "serverless", mode: "registry-delegation", registryId: null }
182
+ };
183
+ }
184
+
185
+ const registryRecord = findRegistryRecord(workspaceConfig, registryId);
186
+ if (!registryRecord) {
187
+ return {
188
+ ok: false,
189
+ exitCode: null,
190
+ durationMs: 0,
191
+ stdout: "",
192
+ stderr: "",
193
+ error: `no API Registry row for integrationId ${registryId}`,
194
+ adapterMeta: { locality: "serverless", mode: "registry-delegation", registryId }
195
+ };
196
+ }
197
+
198
+ let url;
199
+ try {
200
+ url = buildSchedulerUrl(registryRecord);
201
+ } catch (err) {
202
+ return {
203
+ ok: false,
204
+ exitCode: null,
205
+ durationMs: 0,
206
+ stdout: "",
207
+ stderr: "",
208
+ error: err.message || "invalid scheduler URL",
209
+ adapterMeta: { locality: "serverless", registryId }
210
+ };
211
+ }
212
+
213
+ let method = normalizeMethod(registryRecord.method);
214
+ if (!["POST", "PUT", "PATCH"].includes(method)) {
215
+ method = "POST";
216
+ }
217
+
218
+ const authRef = registryRecord.authRef || registryRecord.integrationId;
219
+ const secretEntry = readServerSecret(authRef);
220
+ const secret = secretEntry?.value || "";
221
+
222
+ const outboundTimeout = Math.min(Math.max(timeoutMs, 1000), 120000);
223
+ const startedAt = Date.now();
224
+ const controller = new AbortController();
225
+ const timer = setTimeout(() => controller.abort(), outboundTimeout);
226
+
227
+ const payloadBody = {
228
+ kind: "growthub-sandbox-run-v1",
229
+ runId,
230
+ ranAt,
231
+ workspaceId: workspaceId || null,
232
+ runLocality: "serverless",
233
+ objectId,
234
+ name: sandboxName,
235
+ sandbox: {
236
+ runtime,
237
+ adapter: adapterId,
238
+ agentHost: agentHost || null,
239
+ lifecycleStatus: String(row.lifecycleStatus || "draft").trim().toLowerCase() === "live" ? "live" : "draft",
240
+ version: row.version ?? "",
241
+ instructions,
242
+ command,
243
+ timeoutMs,
244
+ networkAllow,
245
+ allowList,
246
+ envRefSlugs,
247
+ envRefsResolved,
248
+ envRefsMissing
249
+ }
250
+ };
251
+
252
+ try {
253
+ const response = await fetch(url, {
254
+ method,
255
+ headers: {
256
+ accept: "application/json, text/plain;q=0.9,*/*;q=0.8",
257
+ "content-type": "application/json",
258
+ ...buildAuthHeaders(registryRecord, secret)
259
+ },
260
+ body: JSON.stringify(payloadBody),
261
+ signal: controller.signal
262
+ });
263
+ const durationMs = Date.now() - startedAt;
264
+ const contentType = response.headers.get("content-type") || "";
265
+ const rawPayload = contentType.includes("application/json") ? await response.json() : await response.text();
266
+
267
+ if (typeof rawPayload === "string") {
268
+ return {
269
+ ok: response.ok,
270
+ exitCode: response.ok ? 0 : 1,
271
+ durationMs,
272
+ stdout: rawPayload,
273
+ stderr: "",
274
+ error: response.ok ? undefined : `HTTP ${response.status}`,
275
+ adapterMeta: {
276
+ locality: "serverless",
277
+ registryId,
278
+ url,
279
+ httpStatus: response.status,
280
+ schedulerMethod: method
281
+ }
282
+ };
283
+ }
284
+
285
+ const stdout = typeof rawPayload.stdout === "string"
286
+ ? rawPayload.stdout
287
+ : JSON.stringify(rawPayload.result ?? rawPayload, null, 2);
288
+ const stderr = typeof rawPayload.stderr === "string" ? rawPayload.stderr : "";
289
+ let exitCode;
290
+ if (typeof rawPayload.exitCode === "number") {
291
+ exitCode = rawPayload.exitCode;
292
+ } else if (response.ok && rawPayload.ok !== false) {
293
+ exitCode = 0;
294
+ } else {
295
+ exitCode = 1;
296
+ }
297
+ const innerOk = response.ok && rawPayload.ok !== false && exitCode === 0;
298
+
299
+ return {
300
+ ok: innerOk,
301
+ exitCode,
302
+ durationMs: typeof rawPayload.durationMs === "number" ? rawPayload.durationMs : durationMs,
303
+ stdout,
304
+ stderr,
305
+ error: rawPayload.error || (!innerOk ? `HTTP ${response.status}` : undefined),
306
+ adapterMeta: {
307
+ locality: "serverless",
308
+ registryId,
309
+ url,
310
+ httpStatus: response.status,
311
+ schedulerMethod: method
312
+ }
313
+ };
314
+ } catch (error) {
315
+ const durationMs = Date.now() - startedAt;
316
+ return {
317
+ ok: false,
318
+ exitCode: null,
319
+ durationMs,
320
+ stdout: "",
321
+ stderr: "",
322
+ error: error.name === "AbortError" ? `scheduler request timed out after ${outboundTimeout}ms` : (error.message || "scheduler fetch failed"),
323
+ adapterMeta: { locality: "serverless", registryId, url, aborted: error.name === "AbortError" }
324
+ };
325
+ } finally {
326
+ clearTimeout(timer);
327
+ }
328
+ }
329
+
330
+ function buildRunResponse({
331
+ runId,
332
+ ranAt,
333
+ runLocality,
334
+ schedulerRegistryId,
335
+ runtime,
336
+ adapterId,
337
+ agentHost,
338
+ command,
339
+ instructions,
340
+ lifecycleStatus,
341
+ version,
342
+ envRefsResolved,
343
+ envRefsMissing,
344
+ networkAllow,
345
+ allowList,
346
+ result,
347
+ timeoutMs
348
+ }) {
349
+ return {
350
+ runId,
351
+ ranAt,
352
+ runLocality,
353
+ schedulerRegistryId: schedulerRegistryId ? String(schedulerRegistryId).trim() : null,
354
+ runtime,
355
+ adapter: adapterId,
356
+ agentHost: agentHost || null,
357
+ lifecycleStatus,
358
+ version,
359
+ instructions,
360
+ command,
361
+ timeoutMs,
362
+ exitCode: result.exitCode,
363
+ durationMs: result.durationMs,
364
+ stdout: result.stdout,
365
+ stderr: result.stderr,
366
+ error: result.error || undefined,
367
+ envRefsResolved,
368
+ envRefsMissing,
369
+ networkAllow,
370
+ allowList,
371
+ adapterMeta: result.adapterMeta || null
372
+ };
373
+ }
374
+
375
+ function findSandboxRow(workspaceConfig, objectId, name) {
376
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
377
+ const object = objects.find((entry) => entry?.id === objectId && entry?.objectType === "sandbox-environment");
378
+ if (!object) return { object: null, row: null, rowIndex: -1 };
379
+ const wantedName = String(name || "").trim();
380
+ const rows = Array.isArray(object.rows) ? object.rows : [];
381
+ const rowIndex = rows.findIndex((row) => String(row?.Name || "").trim() === wantedName);
382
+ if (rowIndex === -1) return { object, row: null, rowIndex: -1 };
383
+ return { object, row: rows[rowIndex], rowIndex };
384
+ }
385
+
386
+ async function GET(request) {
387
+ const { searchParams } = new URL(request.url);
388
+ const objectId = String(searchParams.get("objectId") || "").trim();
389
+ const name = String(searchParams.get("name") || "").trim();
390
+ if (!objectId || !name) {
391
+ return NextResponse.json({ ok: false, error: "objectId and name are required" }, { status: 400 });
392
+ }
393
+
394
+ const sourceId = sandboxRunSourceId(objectId, name);
395
+ if (!sourceId) {
396
+ return NextResponse.json({ ok: false, error: "could not derive sandbox sourceId" }, { status: 400 });
397
+ }
398
+
399
+ const existing = await readWorkspaceSourceRecords(sourceId);
400
+ const records = Array.isArray(existing?.records) ? existing.records : [];
401
+ return NextResponse.json({
402
+ ok: true,
403
+ sourceId,
404
+ recordCount: records.length,
405
+ records: records.slice(-25).reverse()
406
+ });
407
+ }
408
+
409
+ async function POST(request) {
410
+ let body;
411
+ try {
412
+ body = await request.json();
413
+ } catch {
414
+ return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
415
+ }
416
+
417
+ const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
418
+ const name = typeof body?.name === "string" ? body.name.trim() : "";
419
+ if (!objectId || !name) {
420
+ return NextResponse.json({ ok: false, error: "objectId and name are required" }, { status: 400 });
421
+ }
422
+
423
+ const workspaceConfig = await readWorkspaceConfig();
424
+ const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
425
+ if (!object) {
426
+ return NextResponse.json({ ok: false, error: `no sandbox-environment object with id ${objectId}` }, { status: 404 });
427
+ }
428
+ if (!row) {
429
+ return NextResponse.json({ ok: false, error: `no sandbox row named ${name} in object ${objectId}` }, { status: 404 });
430
+ }
431
+
432
+ const runLocality = normalizeRunLocality(row);
433
+ const runtime = KNOWN_SANDBOX_RUNTIMES.includes(row.runtime) ? row.runtime : "node";
434
+ let adapterId = (typeof row.adapter === "string" && row.adapter.trim()) ? row.adapter.trim() : DEFAULT_SANDBOX_ADAPTER;
435
+ const agentHost = typeof row.agentHost === "string" ? row.agentHost.trim() : "";
436
+ const schedulerRegistryId = typeof row.schedulerRegistryId === "string" ? row.schedulerRegistryId.trim() : "";
437
+ const networkAllow = coerceBoolean(row.networkAllow);
438
+ const allowList = parseSandboxAllowList(row.allowList);
439
+ const envRefSlugs = parseSandboxEnvRefs(row.envRefs);
440
+ const command = typeof row.command === "string" ? row.command : "";
441
+ const instructions = typeof row.instructions === "string" ? row.instructions.trim() : "";
442
+ const agentCommand = instructions
443
+ ? `Instructions:\n${instructions}\n\nPrompt:\n${command}`
444
+ : command;
445
+ const lifecycleStatus = String(row.lifecycleStatus || "draft").trim().toLowerCase() === "live" ? "live" : "draft";
446
+ const version = row.version ?? "";
447
+ const requestedTimeout = Number(row.timeoutMs);
448
+ const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0
449
+ ? Math.min(requestedTimeout, SANDBOX_MAX_TIMEOUT_MS)
450
+ : SANDBOX_DEFAULT_TIMEOUT_MS;
451
+
452
+ if (runLocality === "serverless" && adapterId === "local-agent-host") {
453
+ return NextResponse.json({
454
+ ok: false,
455
+ error: "`local-agent-host` applies only when runLocality is local. Switch run locality or choose a process adapter for serverless delegation."
456
+ }, { status: 400 });
457
+ }
458
+
459
+ const env = {};
460
+ const envRefsResolved = [];
461
+ const envRefsMissing = [];
462
+ for (const slug of envRefSlugs) {
463
+ const resolved = readServerSecret(slug);
464
+ if (resolved) {
465
+ env[resolved.key] = resolved.value;
466
+ envRefsResolved.push(slug);
467
+ } else {
468
+ envRefsMissing.push(slug);
469
+ }
470
+ }
471
+
472
+ const runId = `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
473
+ const ranAt = new Date().toISOString();
474
+
475
+ let result;
476
+ let effectiveAdapterId = adapterId;
477
+
478
+ if (runLocality === "serverless") {
479
+ effectiveAdapterId = "serverless";
480
+ result = await runServerlessScheduler({
481
+ workspaceConfig,
482
+ row,
483
+ runId,
484
+ ranAt,
485
+ workspaceId: workspaceConfig?.id ?? null,
486
+ objectId,
487
+ sandboxName: row.Name || name,
488
+ runtime,
489
+ adapterId,
490
+ agentHost,
491
+ command,
492
+ instructions,
493
+ timeoutMs,
494
+ networkAllow,
495
+ allowList,
496
+ envRefSlugs,
497
+ envRefsResolved,
498
+ envRefsMissing
499
+ });
500
+ } else {
501
+ await ensureSandboxAdaptersLoaded();
502
+ const adapter = getSandboxAdapter(adapterId);
503
+ if (!adapter) {
504
+ return NextResponse.json({
505
+ ok: false,
506
+ error: `sandbox adapter not registered: ${adapterId}`,
507
+ hint: "Drop a file under lib/adapters/sandboxes/adapters/ that calls registerSandboxAdapter()"
508
+ }, { status: 404 });
509
+ }
510
+ if (Array.isArray(adapter.supportedRuntimes) && adapter.supportedRuntimes.length && !adapter.supportedRuntimes.includes(runtime)) {
511
+ return NextResponse.json({
512
+ ok: false,
513
+ error: `adapter ${adapterId} does not support runtime ${runtime}`,
514
+ supportedRuntimes: adapter.supportedRuntimes
515
+ }, { status: 400 });
516
+ }
517
+
518
+ const workdir = await fs.mkdtemp(path.join(os.tmpdir(), "growthub-sandbox-"));
519
+ try {
520
+ result = await adapter.run({
521
+ runId,
522
+ name: row.Name || name,
523
+ runtime,
524
+ agentHost,
525
+ command: adapterId === "local-agent-host" ? agentCommand : command,
526
+ timeoutMs,
527
+ networkAllow,
528
+ allowList,
529
+ env,
530
+ envRefSlugs,
531
+ envRefsMissing,
532
+ workdir,
533
+ ranAt
534
+ });
535
+ } catch (error) {
536
+ result = {
537
+ ok: false,
538
+ exitCode: null,
539
+ durationMs: 0,
540
+ stdout: "",
541
+ stderr: "",
542
+ error: error?.message || "adapter threw",
543
+ adapterMeta: { adapter: adapterId }
544
+ };
545
+ } finally {
546
+ fs.rm(workdir, { recursive: true, force: true }).catch(() => {});
547
+ }
548
+ }
549
+
550
+ const response = buildRunResponse({
551
+ runId,
552
+ ranAt,
553
+ runLocality,
554
+ schedulerRegistryId: runLocality === "serverless" ? schedulerRegistryId : null,
555
+ runtime,
556
+ adapterId: effectiveAdapterId,
557
+ agentHost,
558
+ command,
559
+ instructions,
560
+ lifecycleStatus,
561
+ version,
562
+ envRefsResolved,
563
+ envRefsMissing,
564
+ networkAllow,
565
+ allowList,
566
+ result,
567
+ timeoutMs
568
+ });
569
+
570
+ const sourceId = sandboxRunSourceId(objectId, row.Name || name);
571
+ const persistence = describePersistenceMode();
572
+ const status = response.exitCode === 0 && !response.error ? "connected" : "failed";
573
+
574
+ let persisted = false;
575
+ let persistError = null;
576
+
577
+ if (sourceId && persistence.canSave) {
578
+ try {
579
+ const existing = await readWorkspaceSourceRecords(sourceId);
580
+ const priorRecords = Array.isArray(existing?.records) ? existing.records : [];
581
+ const nextRecords = [...priorRecords, response].slice(-50);
582
+ await writeWorkspaceSourceRecords(sourceId, nextRecords, {
583
+ integrationId: sourceId,
584
+ fetchedAt: ranAt
585
+ });
586
+ persisted = true;
587
+ } catch (error) {
588
+ persistError = error?.message || "failed to persist sandbox run record";
589
+ }
590
+
591
+ try {
592
+ const compactResponse = JSON.stringify(response, null, 2);
593
+ const sourceIdValue = sourceId || "";
594
+ const objects = Array.isArray(workspaceConfig.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
595
+ const nextObjects = objects.map((entry) => {
596
+ if (entry.id !== object.id) return entry;
597
+ const rows = Array.isArray(entry.rows) ? entry.rows : [];
598
+ const nextRows = rows.map((existingRow, index) => {
599
+ if (index !== rowIndex) return existingRow;
600
+ return {
601
+ ...existingRow,
602
+ status,
603
+ lastTested: ranAt,
604
+ lastRunId: runId,
605
+ lastSourceId: sourceIdValue,
606
+ lastResponse: compactResponse
607
+ };
608
+ });
609
+ return { ...entry, rows: nextRows };
610
+ });
611
+ await writeWorkspaceConfig({
612
+ dataModel: { ...(workspaceConfig.dataModel || {}), objects: nextObjects }
613
+ });
614
+ } catch (error) {
615
+ persistError = persistError || error?.message || "failed to stamp row status";
616
+ }
617
+ }
618
+
619
+ return NextResponse.json({
620
+ ok: response.exitCode === 0 && !response.error,
621
+ status,
622
+ runId,
623
+ adapter: effectiveAdapterId,
624
+ runtime,
625
+ exitCode: response.exitCode,
626
+ durationMs: response.durationMs,
627
+ persisted,
628
+ persistError,
629
+ sourceId,
630
+ response
631
+ });
632
+ }
633
+
634
+ export { GET, POST };