@growthub/cli 0.14.1 → 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 (30) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +14 -1
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +49 -2
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +54 -11
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +113 -36
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +7 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +13 -4
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +85 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +8 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +4 -2
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +1 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +3 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  30. package/package.json +1 -1
@@ -15,7 +15,7 @@
15
15
  * NEVER written to `growthub.config.json`.
16
16
  *
17
17
  * Request body:
18
- * { objectId: string, name: string }
18
+ * { objectId: string, name: string, agentHost?: string }
19
19
  *
20
20
  * Response:
21
21
  * {
@@ -49,6 +49,7 @@ async function POST(request) {
49
49
 
50
50
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
51
51
  const name = typeof body?.name === "string" ? body.name.trim() : "";
52
+ const agentHost = typeof body?.agentHost === "string" ? body.agentHost.trim() : "";
52
53
  if (!objectId || !name) {
53
54
  return NextResponse.json(
54
55
  { ok: false, error: "objectId and name are required" },
@@ -57,7 +58,7 @@ async function POST(request) {
57
58
  }
58
59
 
59
60
  try {
60
- const result = await runAgentLogin({ objectId, name });
61
+ const result = await runAgentLogin({ objectId, name, agentHost });
61
62
  return NextResponse.json(result);
62
63
  } catch (error) {
63
64
  return NextResponse.json(
@@ -10,7 +10,7 @@
10
10
  * host's `notes` string.
11
11
  *
12
12
  * Request body:
13
- * { objectId: string, name: string }
13
+ * { objectId: string, name: string, agentHost?: string }
14
14
  *
15
15
  * Response:
16
16
  * {
@@ -42,6 +42,7 @@ async function POST(request) {
42
42
 
43
43
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
44
44
  const name = typeof body?.name === "string" ? body.name.trim() : "";
45
+ const agentHost = typeof body?.agentHost === "string" ? body.agentHost.trim() : "";
45
46
  if (!objectId || !name) {
46
47
  return NextResponse.json(
47
48
  { ok: false, error: "objectId and name are required" },
@@ -50,7 +51,7 @@ async function POST(request) {
50
51
  }
51
52
 
52
53
  try {
53
- const result = await runAgentLogout({ objectId, name });
54
+ const result = await runAgentLogout({ objectId, name, agentHost });
54
55
  return NextResponse.json(result);
55
56
  } catch (error) {
56
57
  return NextResponse.json(
@@ -20,7 +20,7 @@
20
20
  * load.
21
21
  *
22
22
  * Request body:
23
- * { objectId: string, name: string }
23
+ * { objectId: string, name: string, agentHost?: string }
24
24
  *
25
25
  * Response (success):
26
26
  * {
@@ -52,6 +52,7 @@ async function POST(request) {
52
52
 
53
53
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
54
54
  const name = typeof body?.name === "string" ? body.name.trim() : "";
55
+ const agentHost = typeof body?.agentHost === "string" ? body.agentHost.trim() : "";
55
56
  if (!objectId || !name) {
56
57
  return NextResponse.json(
57
58
  { ok: false, error: "objectId and name are required" },
@@ -60,7 +61,7 @@ async function POST(request) {
60
61
  }
61
62
 
62
63
  try {
63
- const result = await checkAgentStatus({ objectId, name });
64
+ const result = await checkAgentStatus({ objectId, name, agentHost });
64
65
  return NextResponse.json(result);
65
66
  } catch (error) {
66
67
  return NextResponse.json(
@@ -43,6 +43,7 @@
43
43
  * envRefsMissing: string[],
44
44
  * networkAllow: boolean,
45
45
  * allowList: string[],
46
+ * browserAccess: boolean, // first-class browser capability (implies networkAllow)
46
47
  * adapterMeta?: Record<string, unknown>
47
48
  * }
48
49
  * }
@@ -173,6 +174,7 @@ async function runServerlessScheduler({
173
174
  timeoutMs,
174
175
  networkAllow,
175
176
  allowList,
177
+ browserAccess,
176
178
  envRefSlugs,
177
179
  envRefsResolved,
178
180
  envRefsMissing
@@ -251,6 +253,7 @@ async function runServerlessScheduler({
251
253
  timeoutMs,
252
254
  networkAllow,
253
255
  allowList,
256
+ browserAccess,
254
257
  envRefSlugs,
255
258
  envRefsResolved,
256
259
  envRefsMissing
@@ -353,6 +356,7 @@ function buildRunResponse({
353
356
  envRefsMissing,
354
357
  networkAllow,
355
358
  allowList,
359
+ browserAccess,
356
360
  result,
357
361
  timeoutMs,
358
362
  row,
@@ -384,6 +388,7 @@ function buildRunResponse({
384
388
  envRefsMissing,
385
389
  networkAllow,
386
390
  allowList,
391
+ browserAccess,
387
392
  adapterMeta: result.adapterMeta || null
388
393
  };
389
394
  if (row && (row.resolverTemplateId || row.connectorKind || row.executionLane)) {
@@ -497,7 +502,11 @@ async function executeSandboxRun(body, { emit } = {}) {
497
502
  let adapterId = (typeof rowForRun.adapter === "string" && rowForRun.adapter.trim()) ? rowForRun.adapter.trim() : DEFAULT_SANDBOX_ADAPTER;
498
503
  const agentHost = typeof rowForRun.agentHost === "string" ? rowForRun.agentHost.trim() : "";
499
504
  const schedulerRegistryId = typeof rowForRun.schedulerRegistryId === "string" ? rowForRun.schedulerRegistryId.trim() : "";
500
- const networkAllow = coerceBoolean(rowForRun.networkAllow);
505
+ const browserAccess = coerceBoolean(rowForRun.browserAccess);
506
+ // Browser access implies outbound network — the same deterministic
507
+ // normalization the sidecar toggle applies, enforced server-side so
508
+ // rows patched via the API behave identically to rows saved in the UI.
509
+ const networkAllow = coerceBoolean(rowForRun.networkAllow) || browserAccess;
501
510
  const allowList = parseSandboxAllowList(rowForRun.allowList);
502
511
  const envRefSlugs = parseSandboxEnvRefs(rowForRun.envRefs);
503
512
  const command = typeof rowForRun.command === "string" ? rowForRun.command : "";
@@ -576,6 +585,7 @@ async function executeSandboxRun(body, { emit } = {}) {
576
585
  envRefsResolved,
577
586
  networkAllow,
578
587
  allowList,
588
+ browserAccess,
579
589
  instructions,
580
590
  command,
581
591
  timeoutMs,
@@ -607,6 +617,7 @@ async function executeSandboxRun(body, { emit } = {}) {
607
617
  timeoutMs,
608
618
  networkAllow,
609
619
  allowList,
620
+ browserAccess,
610
621
  envRefSlugs,
611
622
  envRefsResolved,
612
623
  envRefsMissing
@@ -640,6 +651,7 @@ async function executeSandboxRun(body, { emit } = {}) {
640
651
  timeoutMs,
641
652
  networkAllow,
642
653
  allowList,
654
+ browserAccess,
643
655
  env,
644
656
  envRefSlugs,
645
657
  envRefsMissing,
@@ -680,6 +692,7 @@ async function executeSandboxRun(body, { emit } = {}) {
680
692
  envRefsMissing,
681
693
  networkAllow,
682
694
  allowList,
695
+ browserAccess,
683
696
  result,
684
697
  timeoutMs,
685
698
  row: rowForRun,
@@ -255,7 +255,7 @@ function WorkspaceHelperSetupModal({ workspaceConfig, open, onClose, onSaved })
255
255
  />
256
256
  serverless
257
257
  </label>
258
- <small>Local uses process sandbox or Paperclip agent host on this machine. Serverless delegates to an API Registry URL.</small>
258
+ <small>Choose local execution or a scheduled serverless run.</small>
259
259
  </div>
260
260
  <div className="workspace-helper-setup-field-stack">
261
261
  <label>
@@ -3,6 +3,17 @@
3
3
  import { useMemo } from "react";
4
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]) => ({
@@ -26,6 +37,17 @@ function withRecordRef(patch, objectId, rowName, nodeId) {
26
37
  };
27
38
  }
28
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
+
29
51
  function WorkflowCheckbox({ checked, disabled, onChange, children, title }) {
30
52
  return (
31
53
  <label className="dm-orchestration-config__field dm-orchestration-config__field-inline dm-workflow-check" title={title}>
@@ -139,7 +161,7 @@ function patchSwarmConfig(graph, patch) {
139
161
  return { ...graph, swarm: { ...base, ...patch } };
140
162
  }
141
163
 
142
- export function AgentSwarmPanel({ graph, objectId, rowName, onGraphChange, disabled }) {
164
+ export function AgentSwarmPanel({ graph, objectId, rowName, sandboxRow, onSandboxRowPatch, onGraphChange, disabled }) {
143
165
  const hostOptions = useMemo(getHostOptions, []);
144
166
  if (!graph || typeof graph !== "object") return null;
145
167
 
@@ -154,6 +176,21 @@ export function AgentSwarmPanel({ graph, objectId, rowName, onGraphChange, disab
154
176
  onGraphChange?.(updater);
155
177
  }
156
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
+
157
194
  return (
158
195
  <div className="dm-orchestration-config dm-agent-swarm-panel">
159
196
  <div className="dm-orchestration-config__pane">
@@ -187,6 +224,7 @@ export function AgentSwarmPanel({ graph, objectId, rowName, onGraphChange, disab
187
224
  )}
188
225
  {subagents.map((node) => {
189
226
  const cfg = node.config || {};
227
+ const subagentAuthDraft = buildSubagentAuthDraft(sandboxRow, cfg);
190
228
  return (
191
229
  <div key={node.id} className="dm-agent-swarm-panel__subagent">
192
230
  <div className="dm-agent-swarm-panel__row">
@@ -252,7 +290,7 @@ export function AgentSwarmPanel({ graph, objectId, rowName, onGraphChange, disab
252
290
  <select
253
291
  value={cfg.agentHost || ""}
254
292
  disabled={disabled}
255
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { agentHost: e.target.value }, objectId, rowName))}
293
+ onChange={(e) => patchSubagentHost(node.id, e.target.value)}
256
294
  >
257
295
  <option value="">Inherit</option>
258
296
  {hostOptions.map((opt) => (
@@ -260,6 +298,15 @@ export function AgentSwarmPanel({ graph, objectId, rowName, onGraphChange, disab
260
298
  ))}
261
299
  </select>
262
300
  </label>
301
+ {subagentAuthDraft && isSandboxLocalAgentHost(subagentAuthDraft) && (
302
+ <SandboxAgentAuthPanel
303
+ objectId={objectId}
304
+ rowName={rowName}
305
+ draft={subagentAuthDraft}
306
+ disabled={disabled || typeof onSandboxRowPatch !== "function"}
307
+ onPatchDraft={onSandboxRowPatch}
308
+ />
309
+ )}
263
310
  <WorkflowCheckbox
264
311
  checked={cfg.required !== false}
265
312
  disabled={disabled}
@@ -647,6 +647,14 @@ function SandboxTraceFieldButton({ label, value, disabled, onOpen }) {
647
647
  );
648
648
  }
649
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
+
650
658
  function SandboxRecordFields({
651
659
  draft,
652
660
  setDraft,
@@ -705,8 +713,21 @@ function SandboxRecordFields({
705
713
  return { ...fields, ...EMPTY_AGENT_AUTH_PATCH };
706
714
  }
707
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
+
708
726
  function setRunLocality(next) {
709
727
  const fields = { runLocality: next };
728
+ if (next === "serverless") {
729
+ fields.schedulerRegistryId = String(draft.schedulerRegistryId || "").trim() || defaultSchedulerRegistryId();
730
+ }
710
731
  if (next === "serverless" && ["local-agent-host", "local-intelligence"].includes(String(draft.adapter || "").trim())) {
711
732
  fields.adapter = "local-process";
712
733
  fields.agentHost = "";
@@ -724,6 +745,8 @@ function SandboxRecordFields({
724
745
  }
725
746
 
726
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());
727
750
 
728
751
  // Same cockpit interface + mental model as the API Registry lane, driven by
729
752
  // the serverless/scheduling/persistence derivation. Steps are status-only
@@ -735,6 +758,7 @@ function SandboxRecordFields({
735
758
  persistenceAdapters: serverlessSignals.persistenceAdapters,
736
759
  inlineEditing: true,
737
760
  });
761
+ const showServerlessUpgrade = String(draft.adapter || "").trim() !== "local-intelligence";
738
762
  function handleServerlessAction(action) {
739
763
  if (!action) return;
740
764
  if (action.id === "toggle-locality") setRunLocality(serverlessState.isServerless ? "local" : "serverless");
@@ -743,12 +767,14 @@ function SandboxRecordFields({
743
767
 
744
768
  return (
745
769
  <div className="dm-sandbox-config">
746
- <ApiRegistryCreationCockpit
747
- state={serverlessState}
748
- onAction={handleServerlessAction}
749
- disabled={!table.mutable || saving}
750
- eyebrow={serverlessState.isServerless ? "Serverless workflow" : "Workflow runtime"}
751
- />
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
+ )}
752
778
  <DrawerSection title="Identity & Mode">
753
779
  <label className="dm-record-field">
754
780
  <span>Name</span>
@@ -791,7 +817,7 @@ function SandboxRecordFields({
791
817
  onChange={setRunLocality}
792
818
  />
793
819
  <p className="dm-cell-empty" style={{ fontSize: 11, marginTop: 6 }}>
794
- 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.
795
821
  </p>
796
822
 
797
823
  {locality === "serverless" && table.objectId && (
@@ -883,7 +909,7 @@ function SandboxRecordFields({
883
909
  </label>
884
910
 
885
911
  <p className="dm-cell-empty" style={{ fontSize: 11, marginTop: 0 }}>
886
- 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.
887
913
  </p>
888
914
  </div>
889
915
  )}
@@ -921,10 +947,12 @@ function SandboxRecordFields({
921
947
  </div>
922
948
 
923
949
  <ToggleField
924
- checked={netOn}
925
- disabled={!table.mutable || saving}
950
+ checked={netOn || browserOn}
951
+ disabled={!table.mutable || saving || (browserOn && !netOn)}
926
952
  label="Network allow-list mode"
927
- 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."}
928
956
  onChange={(on) => patchFields({ networkAllow: on ? "true" : "false" })}
929
957
  />
930
958
 
@@ -937,6 +965,21 @@ function SandboxRecordFields({
937
965
  onBlur={(event) => patchFields({ allowList: event.target.value })}
938
966
  />
939
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
+ )}
940
983
  </DrawerSection>
941
984
 
942
985
  <DrawerSection title="Prompt & Limits">
@@ -20,6 +20,12 @@ const LOCAL_AGENT_ADAPTERS = [
20
20
  { value: "local-agent-host", label: "Local agent host" },
21
21
  { value: "local-intelligence", label: "Local intelligence" }
22
22
  ];
23
+ const LOCAL_INTELLIGENCE_MODE_OPTIONS = [
24
+ { value: "ollama", label: "ollama (OLLAMA_BASE_URL + /v1/chat/completions)" },
25
+ { value: "lmstudio", label: "lmstudio (LMSTUDIO_BASE_URL)" },
26
+ { value: "vllm", label: "vllm (VLLM_BASE_URL required)" },
27
+ { value: "custom-openai-compatible", label: "custom (use Chat completions URL above)" }
28
+ ];
23
29
  const EMPTY_AGENT_AUTH_PATCH = {
24
30
  agentAuthStatus: "",
25
31
  agentAuthProvider: "",
@@ -427,9 +433,9 @@ function LocalAgentHostControls({
427
433
  onSandboxRowPatch
428
434
  }) {
429
435
  const row = sandboxRow && typeof sandboxRow === "object" ? sandboxRow : {};
430
- const runLocality = String(row.runLocality || "local").trim().toLowerCase() === "serverless" ? "serverless" : "local";
431
436
  const adapter = String(row.adapter || "local-process").trim() || "local-process";
432
437
  const agentHost = String(row.agentHost || "").trim();
438
+ const browserOn = ["true", "1", "on", "yes"].includes(String(row.browserAccess || "").trim().toLowerCase());
433
439
  const hostOptions = getAgentHostOptions();
434
440
  const canPatch = typeof onSandboxRowPatch === "function";
435
441
 
@@ -444,36 +450,9 @@ function LocalAgentHostControls({
444
450
  return (
445
451
  <div className="dm-orchestration-config__section dm-workflow-agent-runtime">
446
452
  <span>Local agent runtime</span>
447
- <div className="dm-sandbox-locality-toggle" role="group" aria-label="Run locality">
448
- {["local", "serverless"].map((mode) => (
449
- <button
450
- key={mode}
451
- type="button"
452
- className={runLocality === mode ? "is-active" : ""}
453
- disabled={disabled || !canPatch}
454
- onClick={() => {
455
- const fields = { runLocality: mode };
456
- if (mode === "serverless" && ["local-agent-host", "local-intelligence"].includes(adapter)) {
457
- fields.adapter = "local-process";
458
- fields.agentHost = "";
459
- patchWithClearedAgentAuth(fields);
460
- return;
461
- }
462
- patch(fields);
463
- }}
464
- >
465
- {mode === "local" ? "Local" : "Serverless"}
466
- </button>
467
- ))}
468
- </div>
469
453
  <p className="dm-orchestration-config__hint">
470
454
  Same runtime fields as the Data Model sandbox sidecar. Local agent host uses the Paperclip thin adapter on this machine.
471
455
  </p>
472
- {runLocality === "serverless" && (
473
- <p className="dm-orchestration-config__hint">
474
- Serverless delegates execution to the configured scheduler/API Registry row; local CLI auth is not used.
475
- </p>
476
- )}
477
456
  <label className="dm-orchestration-config__field">
478
457
  <span>Execution adapter</span>
479
458
  <select
@@ -482,6 +461,7 @@ function LocalAgentHostControls({
482
461
  onChange={(event) => {
483
462
  const nextAdapter = event.target.value;
484
463
  patchWithClearedAgentAuth({
464
+ runLocality: "local",
485
465
  adapter: nextAdapter,
486
466
  agentHost: nextAdapter === "local-agent-host" ? (agentHost || "claude_local") : ""
487
467
  });
@@ -492,13 +472,17 @@ function LocalAgentHostControls({
492
472
  ))}
493
473
  </select>
494
474
  </label>
495
- {runLocality === "local" && adapter === "local-agent-host" && (
475
+ {adapter === "local-agent-host" && (
496
476
  <label className="dm-orchestration-config__field">
497
477
  <span>Agent host (Paperclip)</span>
498
478
  <select
499
479
  value={agentHost}
500
480
  disabled={disabled || !canPatch}
501
- onChange={(event) => patchWithClearedAgentAuth({ agentHost: event.target.value })}
481
+ onChange={(event) => patchWithClearedAgentAuth({
482
+ runLocality: "local",
483
+ adapter: "local-agent-host",
484
+ agentHost: event.target.value
485
+ })}
502
486
  >
503
487
  <option value="">Select host...</option>
504
488
  {hostOptions.map((item) => (
@@ -507,7 +491,7 @@ function LocalAgentHostControls({
507
491
  </select>
508
492
  </label>
509
493
  )}
510
- {runLocality === "local" && adapter === "local-agent-host" && isSandboxLocalAgentHost(row) && (
494
+ {adapter === "local-agent-host" && isSandboxLocalAgentHost(row) && (
511
495
  <SandboxAgentAuthPanel
512
496
  objectId={objectId}
513
497
  rowName={rowName}
@@ -516,10 +500,63 @@ function LocalAgentHostControls({
516
500
  onPatchDraft={patch}
517
501
  />
518
502
  )}
503
+ {adapter === "local-intelligence" && (
504
+ <div className="dm-sandbox-local-intel">
505
+ <label className="dm-orchestration-config__field">
506
+ <span>Concrete model id</span>
507
+ <input
508
+ value={row.localModel || ""}
509
+ disabled={disabled || !canPatch}
510
+ placeholder="gemma3:4b"
511
+ onChange={(event) => patch({ runLocality: "local", localModel: event.target.value })}
512
+ />
513
+ </label>
514
+ <label className="dm-orchestration-config__field">
515
+ <span>Chat completions URL (optional)</span>
516
+ <input
517
+ value={row.localEndpoint || ""}
518
+ disabled={disabled || !canPatch}
519
+ placeholder="http://127.0.0.1:11434/v1/chat/completions"
520
+ onChange={(event) => patch({ runLocality: "local", localEndpoint: event.target.value })}
521
+ />
522
+ </label>
523
+ <label className="dm-orchestration-config__field">
524
+ <span>Resolver mode</span>
525
+ <select
526
+ value={String(row.intelligenceAdapterMode || "ollama").trim().toLowerCase()}
527
+ disabled={disabled || !canPatch}
528
+ onChange={(event) => patch({ runLocality: "local", intelligenceAdapterMode: event.target.value })}
529
+ >
530
+ {LOCAL_INTELLIGENCE_MODE_OPTIONS.map((item) => (
531
+ <option key={item.value} value={item.value}>{item.label}</option>
532
+ ))}
533
+ </select>
534
+ </label>
535
+ <p className="dm-orchestration-config__hint">
536
+ Uses Instructions + Command as the task payload. With sandbox 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.
537
+ </p>
538
+ {browserOn && (
539
+ <p className="dm-orchestration-config__hint">
540
+ This workflow's AI-agent nodes inherit browser access only when their node-level Network permission is enabled.
541
+ </p>
542
+ )}
543
+ </div>
544
+ )}
519
545
  </div>
520
546
  );
521
547
  }
522
548
 
549
+ function buildNodeAgentAuthDraft(sandboxRow, config) {
550
+ const agentHost = String(config?.agentHost || sandboxRow?.agentHost || "").trim();
551
+ if (!agentHost) return null;
552
+ return {
553
+ ...(sandboxRow || {}),
554
+ runLocality: "local",
555
+ adapter: "local-agent-host",
556
+ agentHost
557
+ };
558
+ }
559
+
523
560
  export function OrchestrationNodeConfigPanel({
524
561
  node,
525
562
  onConfigChange,
@@ -577,6 +614,28 @@ export function OrchestrationNodeConfigPanel({
577
614
 
578
615
  const registryConnected = isApiRegistryTestSuccessful(registryRow);
579
616
  const responseMode = config.responseMode || config.mode || "json";
617
+ const nodeAgentAuthDraft = type === "ai-agent" ? buildNodeAgentAuthDraft(sandboxRow, config) : null;
618
+ const canPatchSandboxRow = typeof onSandboxRowPatch === "function";
619
+ const sandboxBrowserOn = ["true", "1", "on", "yes"].includes(String(sandboxRow?.browserAccess || "").trim().toLowerCase());
620
+ const sandboxAdapter = String(sandboxRow?.adapter || "").trim();
621
+ const nodeAdapter = String(config.adapter || "").trim() || sandboxAdapter;
622
+ const nodeUsesLocalIntelligence = nodeAdapter === "local-intelligence";
623
+
624
+ function patchNodeAgentHost(agentHost) {
625
+ const nextHost = String(agentHost || "").trim();
626
+ patchConfig({
627
+ agentHost: nextHost,
628
+ ...(nextHost ? { adapter: "local-agent-host" } : {})
629
+ });
630
+ if (nextHost && canPatchSandboxRow) {
631
+ onSandboxRowPatch({
632
+ runLocality: "local",
633
+ adapter: "local-agent-host",
634
+ agentHost: nextHost,
635
+ ...EMPTY_AGENT_AUTH_PATCH
636
+ });
637
+ }
638
+ }
580
639
 
581
640
  return (
582
641
  <div className="dm-orchestration-config">
@@ -964,7 +1023,7 @@ export function OrchestrationNodeConfigPanel({
964
1023
  <select
965
1024
  value={config.agentHost || ""}
966
1025
  disabled={disabled}
967
- onChange={(e) => patchConfig({ agentHost: e.target.value })}
1026
+ onChange={(e) => patchNodeAgentHost(e.target.value)}
968
1027
  >
969
1028
  <option value="">Inherit</option>
970
1029
  {Object.entries(HOST_AUTH_CATALOG || {}).map(([slug, host]) => (
@@ -972,6 +1031,15 @@ export function OrchestrationNodeConfigPanel({
972
1031
  ))}
973
1032
  </select>
974
1033
  </label>
1034
+ {nodeAgentAuthDraft && isSandboxLocalAgentHost(nodeAgentAuthDraft) && (
1035
+ <SandboxAgentAuthPanel
1036
+ objectId={objectId}
1037
+ rowName={rowName}
1038
+ draft={nodeAgentAuthDraft}
1039
+ disabled={disabled || !canPatchSandboxRow}
1040
+ onPatchDraft={onSandboxRowPatch}
1041
+ />
1042
+ )}
975
1043
  <WorkflowCheckbox
976
1044
  checked={config.required !== false}
977
1045
  disabled={disabled}
@@ -982,10 +1050,12 @@ export function OrchestrationNodeConfigPanel({
982
1050
  <WorkflowCheckbox
983
1051
  checked={config.networkAccess === true}
984
1052
  disabled={disabled}
985
- title="Network is granted only when both this and the row's networkAllow are on."
1053
+ title={sandboxBrowserOn && nodeUsesLocalIntelligence
1054
+ ? "Network and browser are granted only when this node permission is on and the sandbox row has browser access on."
1055
+ : "Network is granted only when both this and the row's networkAllow are on. The row's browser access inherits through the same gate."}
986
1056
  onChange={(checked) => patchConfig({ networkAccess: checked })}
987
1057
  >
988
- Network
1058
+ {sandboxBrowserOn && nodeUsesLocalIntelligence ? "Network + browser" : "Network"}
989
1059
  </WorkflowCheckbox>
990
1060
  </div>
991
1061
  )}
@@ -1025,8 +1095,15 @@ export function OrchestrationNodeConfigPanel({
1025
1095
  <WorkflowCheckbox checked={config.canWriteDraft === true} disabled={disabled} onChange={(checked) => patchConfig({ canWriteDraft: checked })}>
1026
1096
  Write draft changes only
1027
1097
  </WorkflowCheckbox>
1028
- <WorkflowCheckbox checked={config.networkAccess === true} disabled={disabled} onChange={(checked) => patchConfig({ networkAccess: checked })}>
1029
- Allow network access
1098
+ <WorkflowCheckbox
1099
+ checked={config.networkAccess === true}
1100
+ disabled={disabled}
1101
+ title={sandboxBrowserOn && nodeUsesLocalIntelligence
1102
+ ? "This node gets browser access only when this permission and the sandbox row Browser access toggle are both on."
1103
+ : undefined}
1104
+ onChange={(checked) => patchConfig({ networkAccess: checked })}
1105
+ >
1106
+ {sandboxBrowserOn && nodeUsesLocalIntelligence ? "Allow network + browser access" : "Allow network access"}
1030
1107
  </WorkflowCheckbox>
1031
1108
  </div>
1032
1109
  <KeyValueRows