@growthub/cli 0.14.0 → 0.14.2

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 (40) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +179 -117
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +136 -3
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  40. package/package.json +1 -1
@@ -1,8 +1,19 @@
1
1
  "use client";
2
2
 
3
3
  import { useMemo } from "react";
4
- import { Plus, Trash2 } from "lucide-react";
4
+ import { Check, Plus, Trash2 } from "lucide-react";
5
5
  import { HOST_AUTH_CATALOG } from "@/lib/sandbox-agent-host-catalog";
6
+ import { SandboxAgentAuthPanel } from "./SandboxAgentAuthPanel.jsx";
7
+ import { isSandboxLocalAgentHost } from "@/lib/sandbox-agent-auth-eligibility";
8
+
9
+ const EMPTY_AGENT_AUTH_PATCH = {
10
+ agentAuthStatus: "",
11
+ agentAuthProvider: "",
12
+ agentAuthLastChecked: "",
13
+ agentAuthLastExitCode: "",
14
+ agentAuthLastMessage: "",
15
+ agentAuthLastLoginUrl: ""
16
+ };
6
17
 
7
18
  function getHostOptions() {
8
19
  return Object.entries(HOST_AUTH_CATALOG || {}).map(([slug, host]) => ({
@@ -11,31 +22,74 @@ function getHostOptions() {
11
22
  }));
12
23
  }
13
24
 
14
- function patchOrchestrator(graph, patch) {
25
+ function nodeSandboxRecordRef(objectId, rowName, nodeId) {
26
+ return {
27
+ objectId: String(objectId || "").trim(),
28
+ rowName: String(rowName || "").trim(),
29
+ nodeId: String(nodeId || "").trim()
30
+ };
31
+ }
32
+
33
+ function withRecordRef(patch, objectId, rowName, nodeId) {
34
+ return {
35
+ ...patch,
36
+ sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, nodeId)
37
+ };
38
+ }
39
+
40
+ function buildSubagentAuthDraft(sandboxRow, config) {
41
+ const agentHost = String(config?.agentHost || sandboxRow?.agentHost || "").trim();
42
+ if (!agentHost) return null;
43
+ return {
44
+ ...(sandboxRow || {}),
45
+ runLocality: "local",
46
+ adapter: "local-agent-host",
47
+ agentHost
48
+ };
49
+ }
50
+
51
+ function WorkflowCheckbox({ checked, disabled, onChange, children, title }) {
52
+ return (
53
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline dm-workflow-check" title={title}>
54
+ <input
55
+ type="checkbox"
56
+ checked={checked}
57
+ disabled={disabled}
58
+ onChange={(event) => onChange?.(event.target.checked)}
59
+ />
60
+ <span className="dm-workflow-check__box" aria-hidden="true">
61
+ {checked ? <Check size={13} strokeWidth={2.4} /> : null}
62
+ </span>
63
+ <span>{children}</span>
64
+ </label>
65
+ );
66
+ }
67
+
68
+ function patchOrchestrator(graph, patch, objectId, rowName) {
15
69
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
16
70
  return {
17
71
  ...graph,
18
72
  nodes: nodes.map((node) =>
19
73
  node?.type === "thinAdapter"
20
- ? { ...node, config: { ...(node.config || {}), ...patch } }
74
+ ? { ...node, config: { ...(node.config || {}), ...withRecordRef(patch, objectId, rowName, node.id) } }
21
75
  : node
22
76
  )
23
77
  };
24
78
  }
25
79
 
26
- function patchSynthesis(graph, patch) {
80
+ function patchSynthesis(graph, patch, objectId, rowName) {
27
81
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
28
82
  return {
29
83
  ...graph,
30
84
  nodes: nodes.map((node) =>
31
85
  node?.type === "tool-result"
32
- ? { ...node, config: { ...(node.config || {}), ...patch } }
86
+ ? { ...node, config: { ...(node.config || {}), ...withRecordRef(patch, objectId, rowName, node.id) } }
33
87
  : node
34
88
  )
35
89
  };
36
90
  }
37
91
 
38
- function patchSubagent(graph, nodeId, patch) {
92
+ function patchSubagent(graph, nodeId, patch, objectId, rowName) {
39
93
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
40
94
  return {
41
95
  ...graph,
@@ -44,7 +98,7 @@ function patchSubagent(graph, nodeId, patch) {
44
98
  ? {
45
99
  ...node,
46
100
  label: patch.role != null ? String(patch.role) : node.label,
47
- config: { ...(node.config || {}), ...patch }
101
+ config: { ...(node.config || {}), ...withRecordRef(patch, objectId, rowName, node.id) }
48
102
  }
49
103
  : node
50
104
  )
@@ -107,7 +161,7 @@ function patchSwarmConfig(graph, patch) {
107
161
  return { ...graph, swarm: { ...base, ...patch } };
108
162
  }
109
163
 
110
- export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
164
+ export function AgentSwarmPanel({ graph, objectId, rowName, sandboxRow, onSandboxRowPatch, onGraphChange, disabled }) {
111
165
  const hostOptions = useMemo(getHostOptions, []);
112
166
  if (!graph || typeof graph !== "object") return null;
113
167
 
@@ -122,6 +176,21 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
122
176
  onGraphChange?.(updater);
123
177
  }
124
178
 
179
+ function patchSubagentHost(nodeId, agentHost) {
180
+ const nextHost = String(agentHost || "").trim();
181
+ patchGraph((g) => patchSubagent(g, nodeId, {
182
+ agentHost: nextHost,
183
+ ...(nextHost ? { adapter: "local-agent-host" } : {})
184
+ }, objectId, rowName));
185
+ if (nextHost && typeof onSandboxRowPatch === "function") {
186
+ onSandboxRowPatch({
187
+ adapter: "local-agent-host",
188
+ agentHost: nextHost,
189
+ ...EMPTY_AGENT_AUTH_PATCH
190
+ });
191
+ }
192
+ }
193
+
125
194
  return (
126
195
  <div className="dm-orchestration-config dm-agent-swarm-panel">
127
196
  <div className="dm-orchestration-config__pane">
@@ -133,7 +202,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
133
202
  rows={3}
134
203
  value={orchestrator?.config?.prompt || ""}
135
204
  disabled={disabled || !orchestrator}
136
- onChange={(e) => patchGraph((g) => patchOrchestrator(g, { prompt: e.target.value }))}
205
+ onChange={(e) => patchGraph((g) => patchOrchestrator(g, { prompt: e.target.value }, objectId, rowName))}
137
206
  />
138
207
  </label>
139
208
  </div>
@@ -155,6 +224,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
155
224
  )}
156
225
  {subagents.map((node) => {
157
226
  const cfg = node.config || {};
227
+ const subagentAuthDraft = buildSubagentAuthDraft(sandboxRow, cfg);
158
228
  return (
159
229
  <div key={node.id} className="dm-agent-swarm-panel__subagent">
160
230
  <div className="dm-agent-swarm-panel__row">
@@ -163,7 +233,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
163
233
  placeholder="Role"
164
234
  value={cfg.role || node.label || ""}
165
235
  disabled={disabled}
166
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { role: e.target.value }))}
236
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { role: e.target.value }, objectId, rowName))}
167
237
  />
168
238
  <button
169
239
  type="button"
@@ -181,7 +251,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
181
251
  placeholder="One-sentence charter"
182
252
  value={cfg.description || ""}
183
253
  disabled={disabled}
184
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { description: e.target.value }))}
254
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { description: e.target.value }, objectId, rowName))}
185
255
  />
186
256
  </label>
187
257
  <label className="dm-orchestration-config__field">
@@ -190,7 +260,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
190
260
  rows={2}
191
261
  value={cfg.taskPrompt || ""}
192
262
  disabled={disabled}
193
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { taskPrompt: e.target.value }))}
263
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { taskPrompt: e.target.value }, objectId, rowName))}
194
264
  />
195
265
  </label>
196
266
  <label className="dm-orchestration-config__field">
@@ -201,7 +271,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
201
271
  disabled={disabled}
202
272
  onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, {
203
273
  tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean)
204
- }))}
274
+ }, objectId, rowName))}
205
275
  />
206
276
  </label>
207
277
  <label className="dm-orchestration-config__field">
@@ -212,7 +282,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
212
282
  placeholder="0 = inherit"
213
283
  value={cfg.maxTokens || 0}
214
284
  disabled={disabled}
215
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { maxTokens: Math.max(0, Number(e.target.value) || 0) }))}
285
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { maxTokens: Math.max(0, Number(e.target.value) || 0) }, objectId, rowName))}
216
286
  />
217
287
  </label>
218
288
  <label className="dm-orchestration-config__field">
@@ -220,7 +290,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
220
290
  <select
221
291
  value={cfg.agentHost || ""}
222
292
  disabled={disabled}
223
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { agentHost: e.target.value }))}
293
+ onChange={(e) => patchSubagentHost(node.id, e.target.value)}
224
294
  >
225
295
  <option value="">Inherit</option>
226
296
  {hostOptions.map((opt) => (
@@ -228,27 +298,30 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
228
298
  ))}
229
299
  </select>
230
300
  </label>
231
- <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
232
- <input
233
- type="checkbox"
234
- checked={cfg.required !== false}
235
- disabled={disabled}
236
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { required: e.target.checked }))}
301
+ {subagentAuthDraft && isSandboxLocalAgentHost(subagentAuthDraft) && (
302
+ <SandboxAgentAuthPanel
303
+ objectId={objectId}
304
+ rowName={rowName}
305
+ draft={subagentAuthDraft}
306
+ disabled={disabled || typeof onSandboxRowPatch !== "function"}
307
+ onPatchDraft={onSandboxRowPatch}
237
308
  />
238
- <span>Required</span>
239
- </label>
240
- <label
241
- className="dm-orchestration-config__field dm-orchestration-config__field-inline"
309
+ )}
310
+ <WorkflowCheckbox
311
+ checked={cfg.required !== false}
312
+ disabled={disabled}
313
+ onChange={(checked) => patchGraph((g) => patchSubagent(g, node.id, { required: checked }, objectId, rowName))}
314
+ >
315
+ Required
316
+ </WorkflowCheckbox>
317
+ <WorkflowCheckbox
318
+ checked={cfg.networkAccess === true}
319
+ disabled={disabled}
242
320
  title="Network is granted only when both this and the row's networkAllow are on."
321
+ onChange={(checked) => patchGraph((g) => patchSubagent(g, node.id, { networkAccess: checked }, objectId, rowName))}
243
322
  >
244
- <input
245
- type="checkbox"
246
- checked={cfg.networkAccess === true}
247
- disabled={disabled}
248
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { networkAccess: e.target.checked }))}
249
- />
250
- <span>Network</span>
251
- </label>
323
+ Network
324
+ </WorkflowCheckbox>
252
325
  </div>
253
326
  );
254
327
  })}
@@ -315,7 +388,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
315
388
  rows={2}
316
389
  value={synthesis?.config?.outcomePrompt || ""}
317
390
  disabled={disabled}
318
- onChange={(e) => patchGraph((g) => patchSynthesis(g, { outcomePrompt: e.target.value }))}
391
+ onChange={(e) => patchGraph((g) => patchSynthesis(g, { outcomePrompt: e.target.value }, objectId, rowName))}
319
392
  />
320
393
  </label>
321
394
  </div>
@@ -42,6 +42,7 @@ import {
42
42
  Trash2,
43
43
  Type,
44
44
  Unlock,
45
+ Upload,
45
46
  Users,
46
47
  X,
47
48
  Zap,
@@ -646,6 +647,14 @@ function SandboxTraceFieldButton({ label, value, disabled, onOpen }) {
646
647
  );
647
648
  }
648
649
 
650
+ // Human labels for the per-host browser lanes declared in the
651
+ // local-agent-host catalog — surfaced so the operator's mental model matches
652
+ // exactly what the adapter does under the hood when browserAccess is on.
653
+ const BROWSER_LANE_LABELS = {
654
+ "native-flag": "browser enabled through the host CLI's first-party browser integration flags.",
655
+ "env-signal": "host receives GROWTHUB_SANDBOX_BROWSER_ACCESS=1 — its own configured browser integration honors this setting."
656
+ };
657
+
649
658
  function SandboxRecordFields({
650
659
  draft,
651
660
  setDraft,
@@ -704,8 +713,21 @@ function SandboxRecordFields({
704
713
  return { ...fields, ...EMPTY_AGENT_AUTH_PATCH };
705
714
  }
706
715
 
716
+ function defaultSchedulerRegistryId() {
717
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
718
+ for (const object of objects) {
719
+ if (object?.objectType !== "api-registry") continue;
720
+ const row = (object.rows || []).find((r) => String(r?.integrationId || "").trim());
721
+ if (row) return String(row.integrationId || "").trim();
722
+ }
723
+ return "";
724
+ }
725
+
707
726
  function setRunLocality(next) {
708
727
  const fields = { runLocality: next };
728
+ if (next === "serverless") {
729
+ fields.schedulerRegistryId = String(draft.schedulerRegistryId || "").trim() || defaultSchedulerRegistryId();
730
+ }
709
731
  if (next === "serverless" && ["local-agent-host", "local-intelligence"].includes(String(draft.adapter || "").trim())) {
710
732
  fields.adapter = "local-process";
711
733
  fields.agentHost = "";
@@ -723,6 +745,8 @@ function SandboxRecordFields({
723
745
  }
724
746
 
725
747
  const netOn = ["true", "1", "on", "yes"].includes(String(draft.networkAllow || "").trim().toLowerCase());
748
+ const browserOn = ["true", "1", "on", "yes"].includes(String(draft.browserAccess || "").trim().toLowerCase());
749
+ const browserHostMeta = (selectedAdapterMeta?.hostCatalog || []).find((h) => h.slug === String(draft.agentHost || "").trim());
726
750
 
727
751
  // Same cockpit interface + mental model as the API Registry lane, driven by
728
752
  // the serverless/scheduling/persistence derivation. Steps are status-only
@@ -734,6 +758,7 @@ function SandboxRecordFields({
734
758
  persistenceAdapters: serverlessSignals.persistenceAdapters,
735
759
  inlineEditing: true,
736
760
  });
761
+ const showServerlessUpgrade = String(draft.adapter || "").trim() !== "local-intelligence";
737
762
  function handleServerlessAction(action) {
738
763
  if (!action) return;
739
764
  if (action.id === "toggle-locality") setRunLocality(serverlessState.isServerless ? "local" : "serverless");
@@ -742,12 +767,14 @@ function SandboxRecordFields({
742
767
 
743
768
  return (
744
769
  <div className="dm-sandbox-config">
745
- <ApiRegistryCreationCockpit
746
- state={serverlessState}
747
- onAction={handleServerlessAction}
748
- disabled={!table.mutable || saving}
749
- eyebrow={serverlessState.isServerless ? "Serverless workflow" : "Workflow runtime"}
750
- />
770
+ {showServerlessUpgrade && (
771
+ <ApiRegistryCreationCockpit
772
+ state={serverlessState}
773
+ onAction={handleServerlessAction}
774
+ disabled={!table.mutable || saving}
775
+ eyebrow={serverlessState.isServerless ? "Serverless workflow" : "Workflow runtime"}
776
+ />
777
+ )}
751
778
  <DrawerSection title="Identity & Mode">
752
779
  <label className="dm-record-field">
753
780
  <span>Name</span>
@@ -790,7 +817,7 @@ function SandboxRecordFields({
790
817
  onChange={setRunLocality}
791
818
  />
792
819
  <p className="dm-cell-empty" style={{ fontSize: 11, marginTop: 6 }}>
793
- Local uses process sandbox or Paperclip agent host on this machine. Serverless delegates to an API Registry URL (no local agent CLI).
820
+ Choose local execution or a scheduled serverless run.
794
821
  </p>
795
822
 
796
823
  {locality === "serverless" && table.objectId && (
@@ -882,7 +909,7 @@ function SandboxRecordFields({
882
909
  </label>
883
910
 
884
911
  <p className="dm-cell-empty" style={{ fontSize: 11, marginTop: 0 }}>
885
- Uses <strong>Instructions</strong> + <strong>Command</strong> as the task payload. Tool intents in the JSON response are proposals only and are not executed by the workspace.
912
+ Uses <strong>Instructions</strong> + <strong>Command</strong> as the task payload. With browser access off, tool intents stay proposals. With browser access on, browser tool intents execute through the local browser bridge before the final JSON response is returned.
886
913
  </p>
887
914
  </div>
888
915
  )}
@@ -920,10 +947,12 @@ function SandboxRecordFields({
920
947
  </div>
921
948
 
922
949
  <ToggleField
923
- checked={netOn}
924
- disabled={!table.mutable || saving}
950
+ checked={netOn || browserOn}
951
+ disabled={!table.mutable || saving || (browserOn && !netOn)}
925
952
  label="Network allow-list mode"
926
- description="When enabled, local runs honor GROWTHUB_SANDBOX_NET_* and the allow list below."
953
+ description={browserOn && !netOn
954
+ ? "Network enabled by browser access — the run route grants it even though this row's networkAllow is off. Turn browser access off to control network independently."
955
+ : "When enabled, local runs honor GROWTHUB_SANDBOX_NET_* and the allow list below."}
927
956
  onChange={(on) => patchFields({ networkAllow: on ? "true" : "false" })}
928
957
  />
929
958
 
@@ -936,6 +965,21 @@ function SandboxRecordFields({
936
965
  onBlur={(event) => patchFields({ allowList: event.target.value })}
937
966
  />
938
967
  </label>
968
+
969
+ <ToggleField
970
+ checked={browserOn}
971
+ disabled={!table.mutable || saving}
972
+ label="Browser access"
973
+ description="Allows this sandbox to use a real browser. Also enables network. Local intelligence uses the Playwright browser bridge; Codex/Claude use their native browser modes."
974
+ onChange={(on) => patchFields(on
975
+ ? { browserAccess: "true", networkAllow: "true" }
976
+ : { browserAccess: "false" })}
977
+ />
978
+ {browserOn && String(draft.adapter || "").trim() === "local-agent-host" && browserHostMeta && (
979
+ <p className="dm-cell-empty" style={{ fontSize: 11, marginTop: 4 }}>
980
+ {browserHostMeta.label}: {BROWSER_LANE_LABELS[browserHostMeta.browserLane] || BROWSER_LANE_LABELS["env-signal"]}
981
+ </p>
982
+ )}
939
983
  </DrawerSection>
940
984
 
941
985
  <DrawerSection title="Prompt & Limits">
@@ -1843,7 +1887,9 @@ function DataModelRecordDrawer({
1843
1887
  <header className="dm-record-drawer-head">
1844
1888
  <div>
1845
1889
  <p>Record</p>
1846
- <h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
1890
+ <h2 title={draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}>
1891
+ {draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}
1892
+ </h2>
1847
1893
  </div>
1848
1894
  <div className="dm-record-drawer-actions">
1849
1895
  {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
@@ -1854,7 +1900,7 @@ function DataModelRecordDrawer({
1854
1900
  onClick={runSandbox}
1855
1901
  >
1856
1902
  <Play size={13} aria-hidden />
1857
- {sandboxRunning ? "Running…" : "Run sandbox"}
1903
+ {sandboxRunning ? "Running…" : "Execute"}
1858
1904
  </button>
1859
1905
  )}
1860
1906
  {!isSandbox && sandboxToolFlow !== "draft" && (
@@ -2457,9 +2503,9 @@ function DataModelTableSurface({
2457
2503
  const a = document.createElement("a");
2458
2504
  a.href = url; a.download = `${table.source.replace(/\s+/g, "-").toLowerCase()}.csv`;
2459
2505
  a.click(); URL.revokeObjectURL(url);
2460
- }}>Export CSV</button>
2506
+ }}><Download size={13} />Export CSV</button>
2461
2507
  )}
2462
- {table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}>Import CSV</button>}
2508
+ {table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}><Upload size={13} />Import CSV</button>}
2463
2509
  {table.mutable && (
2464
2510
  <button type="button" className="dm-btn-primary-sm" disabled={saving} onClick={() => onSave((config) => addTableRow(config, table))}>
2465
2511
  <Plus size={13} />Add record
@@ -3544,6 +3590,17 @@ export default function DataModelShell() {
3544
3590
  router.push(`/?dashboard=${encodeURIComponent(target.dashboardId)}`);
3545
3591
  }
3546
3592
  }}
3593
+ onOpenSwarmWorkflow={(target) => {
3594
+ const objectId = String(target?.objectId || "").trim();
3595
+ const rowName = String(target?.name || "").trim();
3596
+ if (!objectId || !rowName) return;
3597
+ const params = new URLSearchParams({
3598
+ object: objectId,
3599
+ row: rowName,
3600
+ field: "orchestrationGraph"
3601
+ });
3602
+ router.push(`/workflows?${params.toString()}`);
3603
+ }}
3547
3604
  onApplied={(updatedConfig) => {
3548
3605
  // Anchor the user on the most recently created/updated Data Model
3549
3606
  // object so a helper-driven object.create lands on the surface