@growthub/cli 0.14.1 → 0.14.3

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 (49) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +4 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/agent-outcomes/route.js +85 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +187 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +36 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +152 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +21 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +88 -1
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +86 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +2 -2
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +21 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +338 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +49 -2
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +54 -11
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +113 -36
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +7 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +35 -169
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +85 -7
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +8 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-publish.js +179 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +1 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +89 -5
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-registry.js +539 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +11 -2
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +24 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-outcome-receipts.js +157 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +400 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +3 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +10 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +203 -0
  49. package/package.json +2 -2
@@ -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
@@ -20,9 +20,9 @@
20
20
  * no second product surface, no terminal emulator — just a uniform
21
21
  * readiness bridge.
22
22
  *
23
- * Status values stamped on the row are intentionally distinct between
24
- * confirmed-authenticated ("active") and merely-installed ("reachable")
25
- * so the pill cannot overclaim auth from a `--version` probe.
23
+ * The pill represents selected local host readiness. Legacy `reachable`
24
+ * metadata is rendered as Active so Data Model and workflow sidecars stay
25
+ * visually identical after a successful host switch/check path.
26
26
  */
27
27
 
28
28
  import { useCallback, useState } from "react";
@@ -32,7 +32,7 @@ import { getAgentHostCapabilities } from "@/lib/sandbox-agent-host-catalog";
32
32
 
33
33
  const STATUS_LABEL = {
34
34
  active: "Active",
35
- reachable: "Reachable",
35
+ reachable: "Active",
36
36
  stale: "Stale",
37
37
  missing: "Missing",
38
38
  checking: "Checking",
@@ -40,10 +40,9 @@ const STATUS_LABEL = {
40
40
  };
41
41
 
42
42
  function statusKind(status) {
43
- if (status === "active") return "ok";
43
+ if (status === "active" || status === "reachable") return "ok";
44
44
  if (status === "stale") return "warn";
45
45
  if (status === "missing") return "bad";
46
- // "reachable" stays neutral — CLI is installed, but auth is NOT confirmed.
47
46
  return "";
48
47
  }
49
48
 
@@ -62,15 +61,28 @@ export function SandboxAgentAuthPanel({ objectId, rowName, draft, disabled, onPa
62
61
  const [busy, setBusy] = useState(null); // "status" | "login" | "logout" | null
63
62
  const [output, setOutput] = useState(null);
64
63
  const [message, setMessage] = useState("");
64
+ const [localAuthState, setLocalAuthState] = useState(null);
65
65
 
66
66
  const capabilities = getAgentHostCapabilities(draft);
67
67
  const providerMatchesHost = String(draft?.agentAuthProvider || "").trim() === String(draft?.agentHost || "").trim();
68
+ const localMatchesHost =
69
+ localAuthState?.provider &&
70
+ String(localAuthState.provider || "").trim() === String(capabilities?.slug || "").trim();
68
71
  const currentStatus =
69
- providerMatchesHost && typeof draft?.agentAuthStatus === "string" && draft.agentAuthStatus.trim()
70
- ? draft.agentAuthStatus.trim()
71
- : "unknown";
72
- const lastChecked = providerMatchesHost ? draft?.agentAuthLastChecked || "" : "";
73
- const lastMessage = providerMatchesHost ? draft?.agentAuthLastMessage || "" : "";
72
+ localMatchesHost && typeof localAuthState?.status === "string" && localAuthState.status.trim()
73
+ ? localAuthState.status.trim()
74
+ : providerMatchesHost && typeof draft?.agentAuthStatus === "string" && draft.agentAuthStatus.trim()
75
+ ? draft.agentAuthStatus.trim()
76
+ : "unknown";
77
+ const lastChecked = localMatchesHost && localAuthState?.checkedAt
78
+ ? localAuthState.checkedAt
79
+ : providerMatchesHost
80
+ ? draft?.agentAuthLastChecked || ""
81
+ : "";
82
+ const lastMessage =
83
+ localMatchesHost && typeof localAuthState?.message === "string"
84
+ ? localAuthState.message
85
+ : providerMatchesHost ? draft?.agentAuthLastMessage || "" : "";
74
86
  const displayMessage = normalizeAuthMessage(message || lastMessage, capabilities?.label)
75
87
  || (currentStatus === "unknown" ? "Run Check or Login to verify this local agent host." : "");
76
88
 
@@ -85,15 +97,23 @@ export function SandboxAgentAuthPanel({ objectId, rowName, draft, disabled, onPa
85
97
  const res = await fetch(endpoint, {
86
98
  method: "POST",
87
99
  headers: { "content-type": "application/json" },
88
- body: JSON.stringify({ objectId, name: rowName })
100
+ body: JSON.stringify({ objectId, name: rowName, agentHost: capabilities.slug })
89
101
  });
90
102
  const payload = await res.json();
91
103
  setOutput(payload);
92
104
  setMessage(payload.message || (payload.ok ? "Done" : payload.error || "Failed"));
105
+ if (payload.status) {
106
+ setLocalAuthState({
107
+ status: payload.status,
108
+ provider: payload.provider || capabilities.slug,
109
+ checkedAt: payload.checkedAt || new Date().toISOString(),
110
+ message: payload.message || ""
111
+ });
112
+ }
93
113
  if (typeof onPatchDraft === "function" && payload.status) {
94
114
  onPatchDraft({
95
115
  agentAuthStatus: payload.status,
96
- agentAuthProvider: payload.provider || draft?.agentHost || "unknown",
116
+ agentAuthProvider: payload.provider || capabilities.slug,
97
117
  agentAuthLastChecked: payload.checkedAt || new Date().toISOString(),
98
118
  agentAuthLastExitCode:
99
119
  typeof payload.exitCode === "number" ? payload.exitCode : null,
@@ -107,7 +127,7 @@ export function SandboxAgentAuthPanel({ objectId, rowName, draft, disabled, onPa
107
127
  setBusy(null);
108
128
  }
109
129
  },
110
- [canAct, objectId, rowName, onPatchDraft, draft?.agentHost]
130
+ [canAct, objectId, rowName, onPatchDraft, capabilities?.slug]
111
131
  );
112
132
 
113
133
  const onCheckStatus = () =>
@@ -6253,9 +6253,16 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
6253
6253
  .dm-sandbox-config > .dm-cockpit { width: 100%; margin: 12px 0 8px; box-sizing: border-box; }
6254
6254
  .dm-radio-row { display: grid; gap: 8px; }
6255
6255
  .dm-radio-row label, .dm-check-row { display: grid; grid-template-columns: 16px minmax(0,1fr); align-items: start; column-gap: 8px; color: #1f2937; font-size: 12px; line-height: 1.35; }
6256
+ .dm-check-row-compact { grid-template-columns: 16px max-content 22px; align-items: center; width: fit-content; max-width: 100%; }
6256
6257
  .dm-radio-row input[type="radio"], .dm-check-row input[type="checkbox"] { width: 14px; height: 14px; margin: 1px 0 0; padding: 0; box-shadow: none; accent-color: #111827; }
6257
6258
  .dm-radio-row span, .dm-check-row span { color: #1f2937; font-size: 12px; font-weight: 500; }
6258
6259
  .dm-check-row { cursor: pointer; }
6260
+ .dm-check-row-compact > label { color: #1f2937; font-size: 12px; font-weight: 500; cursor: pointer; }
6261
+ .dm-help-wrap { position: relative; display: inline-flex; align-items: center; min-width: 0; }
6262
+ .dm-icon-help { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 4px; background: transparent; color: #64748b; padding: 0; cursor: help; }
6263
+ .dm-icon-help:hover, .dm-icon-help:focus-visible { background: #f1f5f9; color: #334155; outline: none; }
6264
+ .dm-help-bubble { position: absolute; z-index: 80; left: 22px; top: 50%; transform: translateY(-50%); width: min(280px, calc(100vw - 48px)); display: none; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; box-shadow: 0 12px 30px rgba(15,23,42,.16); color: #334155; font-size: 11px; font-weight: 500; line-height: 1.4; padding: 8px 9px; }
6265
+ .dm-help-wrap:hover .dm-help-bubble, .dm-help-bubble.is-open { display: block; }
6259
6266
  .dm-select { position: relative; width: 100%; min-width: 0; font-size: 11px; }
6260
6267
  .dm-select-trigger { width: 100%; min-height: 32px; display: flex; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid #cbd5e1; border-radius: 7px; background: #fff; color: #111827; box-shadow: 0 1px 2px rgba(15,23,42,.05); font: inherit; font-size: 11px; padding: 6px 10px; text-align: left; cursor: pointer; transition: border-color .12s, box-shadow .12s, background .12s; }
6261
6268
  .dm-select-trigger:hover:not(:disabled) { border-color: #94a3b8; box-shadow: 0 2px 8px rgba(15,23,42,.08); }