@growthub/cli 0.13.1 → 0.13.4

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 (33) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +24 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +14 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +74 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +67 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +77 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +48 -3
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +123 -27
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +136 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +713 -92
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +224 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +32 -1
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +514 -9
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +8 -1
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +10 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/RunSetupPanel.jsx +261 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +72 -7
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +778 -140
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +91 -14
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +35 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +15 -3
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +384 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +323 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +32 -3
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-eligibility.js +50 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-redaction.js +64 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +629 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-host-catalog.js +168 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +542 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +164 -7
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +11 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +111 -1
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +9 -0
  33. package/package.json +1 -1
@@ -28,6 +28,7 @@ import {
28
28
  Mail,
29
29
  Maximize2,
30
30
  MoreHorizontal,
31
+ Play,
31
32
  Plus,
32
33
  Pencil,
33
34
  Search,
@@ -67,6 +68,8 @@ import {
67
68
  } from "@/lib/workspace-data-model";
68
69
  import { ReferencePicker } from "./ReferencePicker.jsx";
69
70
  import { SandboxRunPanel } from "./SandboxRunPanel.jsx";
71
+ import { SandboxAgentAuthPanel } from "./SandboxAgentAuthPanel.jsx";
72
+ import { isSandboxLocalAgentHost } from "@/lib/sandbox-agent-auth-eligibility";
70
73
  import { StatusPill } from "./StatusPill.jsx";
71
74
  import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
72
75
  import { SourceTestPanel } from "./SourceTestPanel.jsx";
@@ -135,6 +138,15 @@ const OBJECT_TYPE_DEFS = [
135
138
  // ─── Lane / badge meta (objectTypeBadge from dm-shared) ────────────────────────
136
139
 
137
140
  const SANDBOX_RUNTIME_OPTIONS = ["python", "node", "bash"];
141
+ const EMPTY_FIELD_SETTING_LIST = Object.freeze([]);
142
+ const EMPTY_AGENT_AUTH_PATCH = {
143
+ agentAuthStatus: "",
144
+ agentAuthProvider: "",
145
+ agentAuthLastChecked: "",
146
+ agentAuthLastExitCode: "",
147
+ agentAuthLastMessage: "",
148
+ agentAuthLastLoginUrl: ""
149
+ };
138
150
  const FIELD_TYPE_CHOICES = [
139
151
  { value: "text", label: "Text", icon: "Type", sample: "Field name" },
140
152
  { value: "number", label: "Number", icon: "Hash", sample: "Amount" },
@@ -618,10 +630,17 @@ function SandboxRecordFields({
618
630
  ));
619
631
  }
620
632
 
633
+ function withClearedAgentAuth(fields) {
634
+ return { ...fields, ...EMPTY_AGENT_AUTH_PATCH };
635
+ }
636
+
621
637
  function setRunLocality(next) {
622
638
  const fields = { runLocality: next };
623
639
  if (next === "serverless" && ["local-agent-host", "local-intelligence"].includes(String(draft.adapter || "").trim())) {
624
640
  fields.adapter = "local-process";
641
+ fields.agentHost = "";
642
+ patchFields(withClearedAgentAuth(fields));
643
+ return;
625
644
  }
626
645
  patchFields(fields);
627
646
  }
@@ -708,7 +727,10 @@ function SandboxRecordFields({
708
727
  value={String(draft.adapter || "local-process").trim() || "local-process"}
709
728
  disabled={!table.mutable || saving}
710
729
  options={sandboxAdapters.length === 0 ? [{ value: "local-process", label: "local-process" }] : sandboxAdapters.map((a) => ({ value: a.id, label: a.label }))}
711
- onChange={(nextValue) => patchFields({ adapter: nextValue })}
730
+ onChange={(nextValue) => patchFields(withClearedAgentAuth({
731
+ adapter: nextValue,
732
+ agentHost: nextValue === "local-agent-host" ? draft.agentHost || "" : ""
733
+ }))}
712
734
  />
713
735
  </label>
714
736
 
@@ -720,7 +742,7 @@ function SandboxRecordFields({
720
742
  disabled={!table.mutable || saving}
721
743
  placeholder="Select host..."
722
744
  options={(selectedAdapterMeta?.hostCatalog || []).map((h) => ({ value: h.slug, label: h.label }))}
723
- onChange={(nextValue) => patchFields({ agentHost: nextValue })}
745
+ onChange={(nextValue) => patchFields(withClearedAgentAuth({ agentHost: nextValue }))}
724
746
  />
725
747
  </label>
726
748
  )}
@@ -990,21 +1012,27 @@ function DataModelRecordDrawer({
990
1012
  const [sidecarMode, setSidecarMode] = useState(null);
991
1013
  const [traceField, setTraceField] = useState(null);
992
1014
  const [traceRunId, setTraceRunId] = useState("");
1015
+ const drawerKeyRef = useRef("");
993
1016
 
994
1017
  useEffect(() => {
1018
+ const drawerKey = `${table.id || table.objectId || table.source}:${rowIndex}:${row?.Name || row?.id || ""}`;
1019
+ const sameDrawerRecord = drawerKeyRef.current === drawerKey;
1020
+ drawerKeyRef.current = drawerKey;
995
1021
  setDraft(row || {});
996
- setEditMode(false);
997
1022
  setPendingColumns(table.columns || []);
998
1023
  setPendingHidden(table.fieldSettings?.hidden || []);
999
- setTestMessage("");
1000
- setSandboxMessage("");
1001
- setSandboxHistory([]);
1002
- setSandboxHistoryMessage("");
1003
- setExpandedJson(null);
1004
- setSandboxToolFlow(null);
1005
- setSandboxToolDraft({});
1006
- setCreatedSandboxMeta(null);
1007
- setCreatedSandboxTestMessage("");
1024
+ if (!sameDrawerRecord) {
1025
+ setEditMode(false);
1026
+ setTestMessage("");
1027
+ setSandboxMessage("");
1028
+ setSandboxHistory([]);
1029
+ setSandboxHistoryMessage("");
1030
+ setExpandedJson(null);
1031
+ setSandboxToolFlow(null);
1032
+ setSandboxToolDraft({});
1033
+ setCreatedSandboxMeta(null);
1034
+ setCreatedSandboxTestMessage("");
1035
+ }
1008
1036
  if (initialSidecar?.mode === "graph") {
1009
1037
  setSidecarMode("graph");
1010
1038
  setTraceField(null);
@@ -1013,12 +1041,12 @@ function DataModelRecordDrawer({
1013
1041
  setSidecarMode("trace");
1014
1042
  setTraceField(initialSidecar.field || "lastResponse");
1015
1043
  setTraceRunId(String(initialSidecar.runId || row?.lastRunId || "").trim());
1016
- } else {
1044
+ } else if (!sameDrawerRecord) {
1017
1045
  setSidecarMode(null);
1018
1046
  setTraceField(null);
1019
1047
  setTraceRunId("");
1020
1048
  }
1021
- }, [row, rowIndex, initialSidecar]);
1049
+ }, [row, rowIndex, initialSidecar, table.id, table.objectId, table.source, table.columns, table.fieldSettings?.hidden]);
1022
1050
 
1023
1051
  if (rowIndex === null || rowIndex === undefined || !row) return null;
1024
1052
 
@@ -1371,6 +1399,17 @@ function DataModelRecordDrawer({
1371
1399
  <h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
1372
1400
  </div>
1373
1401
  <div className="dm-record-drawer-actions">
1402
+ {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
1403
+ <button
1404
+ type="button"
1405
+ className="dm-btn-primary-sm dm-record-head-run"
1406
+ disabled={sandboxRunning || saving || !String(draft.Name || "").trim()}
1407
+ onClick={runSandbox}
1408
+ >
1409
+ <Play size={13} aria-hidden />
1410
+ {sandboxRunning ? "Running…" : "Run sandbox"}
1411
+ </button>
1412
+ )}
1374
1413
  {!isSandbox && sandboxToolFlow !== "draft" && (
1375
1414
  <button type="button" className="dm-sidebar-close" onClick={() => setEditMode((current) => !current)} aria-label="Toggle edit mode">
1376
1415
  <Pencil size={16} />
@@ -1450,7 +1489,7 @@ function DataModelRecordDrawer({
1450
1489
  onConfirm={createSandboxToolFromRegistry}
1451
1490
  onCancel={() => setSandboxToolFlow("draft")}
1452
1491
  />
1453
- {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
1492
+ {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && sandboxMessage && (
1454
1493
  <SandboxRunPanel
1455
1494
  status={draft.status}
1456
1495
  sandboxRunning={sandboxRunning}
@@ -1458,6 +1497,21 @@ function DataModelRecordDrawer({
1458
1497
  disabled={saving}
1459
1498
  canRun={Boolean(String(draft.Name || "").trim())}
1460
1499
  onRun={runSandbox}
1500
+ agentAuthStatus={draft.agentAuthStatus}
1501
+ agentAuthHint={
1502
+ isSandboxLocalAgentHost(draft) && ["stale", "missing"].includes(String(draft.agentAuthStatus || ""))
1503
+ ? "Agent auth may be stale — open the auth panel above."
1504
+ : null
1505
+ }
1506
+ />
1507
+ )}
1508
+ {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && isSandboxLocalAgentHost(draft) && (
1509
+ <SandboxAgentAuthPanel
1510
+ objectId={table.objectId}
1511
+ rowName={String(draft.Name || "").trim()}
1512
+ draft={draft}
1513
+ disabled={saving || sandboxRunning}
1514
+ onPatchDraft={(patch) => setDraft((current) => ({ ...current, ...patch }))}
1461
1515
  />
1462
1516
  )}
1463
1517
  {isSandbox && sidecarMode === "graph" && (
@@ -1596,9 +1650,12 @@ function DataModelTableSurface({
1596
1650
  focusSandboxRowName,
1597
1651
  onFocusSandboxRowConsumed,
1598
1652
  onFocusSandboxRow,
1653
+ selectedRecordIndex,
1654
+ onSelectedRecordIndexChange,
1599
1655
  }) {
1600
1656
  const router = useRouter();
1601
1657
  const [selectedRow, setSelectedRow] = useState(null);
1658
+ const [localSelectedOriginalIndex, setLocalSelectedOriginalIndex] = useState(null);
1602
1659
  const [initialSidecar, setInitialSidecar] = useState(null);
1603
1660
  const [fieldName, setFieldName] = useState("");
1604
1661
  const [fieldType, setFieldType] = useState("text");
@@ -1616,10 +1673,17 @@ function DataModelTableSurface({
1616
1673
  const [pageSize, setPageSize] = useState(15);
1617
1674
  const [pageIndex, setPageIndex] = useState(0);
1618
1675
  const fieldInputRef = useRef(null);
1676
+ const selectedOriginalIndex = selectedRecordIndex ?? localSelectedOriginalIndex;
1677
+
1678
+ function selectOriginalIndex(index) {
1679
+ setLocalSelectedOriginalIndex(index);
1680
+ onSelectedRecordIndexChange?.(index);
1681
+ }
1619
1682
 
1620
1683
  useEffect(() => { if (addingField) fieldInputRef.current?.focus(); }, [addingField]);
1621
1684
  useEffect(() => {
1622
1685
  setSelectedRow(null);
1686
+ selectOriginalIndex(null);
1623
1687
  setSelectedRows(new Set());
1624
1688
  setConfirmDeleteSelection(false);
1625
1689
  setLastSelectedRowIndex(null);
@@ -1633,7 +1697,15 @@ function DataModelTableSurface({
1633
1697
  setFilterDraft({ fieldId: table.columns[0] || "", operator: "eq", value: "" });
1634
1698
  }, [table.id, table.columns]);
1635
1699
 
1636
- const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null };
1700
+ const settings = useMemo(() => {
1701
+ const fieldSettings = table.fieldSettings || {};
1702
+ return {
1703
+ hidden: Array.isArray(fieldSettings.hidden) ? fieldSettings.hidden : EMPTY_FIELD_SETTING_LIST,
1704
+ order: Array.isArray(fieldSettings.order) ? fieldSettings.order : (table.columns || EMPTY_FIELD_SETTING_LIST),
1705
+ sort: Array.isArray(fieldSettings.sort) ? fieldSettings.sort : EMPTY_FIELD_SETTING_LIST,
1706
+ filter: fieldSettings.filter || null
1707
+ };
1708
+ }, [table.fieldSettings, table.columns]);
1637
1709
  const orderedColumns = useMemo(() => mergeColumnOrder(settings.order, table.columns), [settings.order, table.columns]);
1638
1710
  const visibleColumns = useMemo(() => orderedColumns.filter((column) => !settings.hidden.includes(column)), [orderedColumns, settings.hidden]);
1639
1711
  const rowEntries = useMemo(() => {
@@ -1669,7 +1741,8 @@ function DataModelTableSurface({
1669
1741
  if (visibleIndex < 0) return;
1670
1742
  const pageForRow = Math.floor(visibleIndex / pageSize);
1671
1743
  setPageIndex(pageForRow);
1672
- setSelectedRow(visibleIndex - pageForRow * pageSize);
1744
+ setSelectedRow(visibleIndex);
1745
+ selectOriginalIndex(originalIndex);
1673
1746
  onFocusSandboxRowConsumed?.();
1674
1747
  }, [focusSandboxRowName, table.id, table.objectType, table.rows, rowEntries, pageSize, onFocusSandboxRowConsumed]);
1675
1748
 
@@ -1847,11 +1920,14 @@ function DataModelTableSurface({
1847
1920
  const rowIndexes = Array.from(selectedRows).sort((a, b) => b - a);
1848
1921
  onSave((config) => rowIndexes.reduce((nextConfig, rowIndex) => deleteTableRow(nextConfig, table, rowIndex), config));
1849
1922
  setSelectedRow(null);
1923
+ selectOriginalIndex(null);
1850
1924
  setConfirmDeleteSelection(false);
1851
1925
  clearRowSelection();
1852
1926
  }
1853
1927
 
1854
- const selectedEntry = selectedRow === null ? null : rowEntries[selectedRow];
1928
+ const selectedEntry = selectedOriginalIndex === null
1929
+ ? (selectedRow === null ? null : rowEntries[selectedRow])
1930
+ : rowEntries.find((entry) => entry.originalIndex === selectedOriginalIndex) || null;
1855
1931
  const selectedRecord = selectedEntry?.row || null;
1856
1932
 
1857
1933
  return (
@@ -2035,7 +2111,14 @@ function DataModelTableSurface({
2035
2111
  const visibleIndex = pageStart + rowIndex;
2036
2112
  const displayIndex = visibleIndex + 1;
2037
2113
  return (
2038
- <tr key={`${originalIndex}:${visibleIndex}`} className={`${selectedRow === visibleIndex ? "selected" : ""}${selectedRows.has(originalIndex) ? " multi-selected" : ""}`} onClick={() => setSelectedRow(visibleIndex)}>
2114
+ <tr
2115
+ key={`${originalIndex}:${visibleIndex}`}
2116
+ className={`${selectedOriginalIndex === originalIndex ? "selected" : ""}${selectedRows.has(originalIndex) ? " multi-selected" : ""}`}
2117
+ onClick={() => {
2118
+ setSelectedRow(visibleIndex);
2119
+ selectOriginalIndex(originalIndex);
2120
+ }}
2121
+ >
2039
2122
  <td className="dm-db-rownum">
2040
2123
  {table.mutable ? (
2041
2124
  <button type="button" className="dm-row-select" aria-label={selectedRows.has(originalIndex) ? `Deselect row ${displayIndex}` : `Select row ${displayIndex}`} aria-pressed={selectedRows.has(originalIndex)} onClick={(event) => { event.stopPropagation(); toggleRowSelection(originalIndex, visibleIndex, event); }}>
@@ -2087,11 +2170,12 @@ function DataModelTableSurface({
2087
2170
  if (column === "orchestrationGraph" || column === "orchestrationConfig") {
2088
2171
  openSandboxGraph(column, row);
2089
2172
  return;
2090
- }
2091
- const sidecar = sandboxSidecarForColumn(column, row);
2092
- setSelectedRow(visibleIndex);
2093
- setInitialSidecar(sidecar);
2094
- }}
2173
+ }
2174
+ const sidecar = sandboxSidecarForColumn(column, row);
2175
+ setSelectedRow(visibleIndex);
2176
+ selectOriginalIndex(originalIndex);
2177
+ setInitialSidecar(sidecar);
2178
+ }}
2095
2179
  >
2096
2180
  {column === "orchestrationGraph" || column === "orchestrationConfig"
2097
2181
  ? (getOrchestrationGraphUiState(row?.[column]) === "populated" ? "Edit graph" : "Start graph")
@@ -2139,9 +2223,9 @@ function DataModelTableSurface({
2139
2223
  tables={tables}
2140
2224
  workspaceConfig={workspaceConfig}
2141
2225
  rowIndex={selectedEntry?.originalIndex ?? null}
2142
- row={selectedRecord}
2143
- saving={saving}
2144
- onClose={() => { setSelectedRow(null); setInitialSidecar(null); }}
2226
+ row={selectedRecord}
2227
+ saving={saving}
2228
+ onClose={() => { setSelectedRow(null); selectOriginalIndex(null); setInitialSidecar(null); }}
2145
2229
  onSave={onSave}
2146
2230
  onFocusSandboxRow={onFocusSandboxRow}
2147
2231
  initialSidecar={initialSidecar}
@@ -2449,6 +2533,7 @@ export default function DataModelShell() {
2449
2533
  const [helperInitialThread, setHelperInitialThread] = useState(null);
2450
2534
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
2451
2535
  const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
2536
+ const [selectedRecordByTable, setSelectedRecordByTable] = useState({});
2452
2537
  const pendingPatchRef = useRef({});
2453
2538
  const saveTimerRef = useRef(null);
2454
2539
 
@@ -2540,6 +2625,9 @@ export default function DataModelShell() {
2540
2625
  );
2541
2626
 
2542
2627
  const selectedTable = tables.find((t) => t.source === selectedSource) || tables[0] || null;
2628
+ const selectedTableKey = selectedTable
2629
+ ? String(selectedTable.objectId || selectedTable.id || selectedTable.source || "")
2630
+ : "";
2543
2631
 
2544
2632
  const focusSandboxEnvironmentRow = useCallback(({ rowName, deferOpen = false } = {}) => {
2545
2633
  const wanted = String(rowName || "").trim();
@@ -2903,6 +2991,14 @@ export default function DataModelShell() {
2903
2991
  focusSandboxRowName={focusSandboxRowName}
2904
2992
  onFocusSandboxRowConsumed={() => setFocusSandboxRowName(null)}
2905
2993
  onFocusSandboxRow={focusSandboxEnvironmentRow}
2994
+ selectedRecordIndex={selectedTableKey ? selectedRecordByTable[selectedTableKey] ?? null : null}
2995
+ onSelectedRecordIndexChange={(index) => {
2996
+ if (!selectedTableKey) return;
2997
+ setSelectedRecordByTable((current) => ({
2998
+ ...current,
2999
+ [selectedTableKey]: index
3000
+ }));
3001
+ }}
2906
3002
  />
2907
3003
  </section>
2908
3004
  )
@@ -7,10 +7,33 @@ import {
7
7
  FILTER_OPERATORS,
8
8
  isApiRegistryTestSuccessful
9
9
  } from "@/lib/orchestration-graph";
10
+ import { SandboxAgentAuthPanel } from "./SandboxAgentAuthPanel.jsx";
11
+ import { isSandboxLocalAgentHost } from "@/lib/sandbox-agent-auth-eligibility";
12
+ import { HOST_AUTH_CATALOG } from "@/lib/sandbox-agent-host-catalog";
10
13
 
11
14
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
12
15
  const MODEL_OPTIONS = ["Claude Opus 4.6", "Claude Sonnet 4.5", "GPT-5.2", "Local agent host"];
13
16
  const OUTPUT_TYPES = ["Text", "Number", "Boolean", "JSON", "Record ID"];
17
+ const LOCAL_AGENT_ADAPTERS = [
18
+ { value: "local-process", label: "Local process" },
19
+ { value: "local-agent-host", label: "Local agent host" },
20
+ { value: "local-intelligence", label: "Local intelligence" }
21
+ ];
22
+ const EMPTY_AGENT_AUTH_PATCH = {
23
+ agentAuthStatus: "",
24
+ agentAuthProvider: "",
25
+ agentAuthLastChecked: "",
26
+ agentAuthLastExitCode: "",
27
+ agentAuthLastMessage: "",
28
+ agentAuthLastLoginUrl: ""
29
+ };
30
+
31
+ function getAgentHostOptions() {
32
+ return Object.entries(HOST_AUTH_CATALOG || {}).map(([slug, host]) => ({
33
+ value: slug,
34
+ label: host?.label || slug
35
+ }));
36
+ }
14
37
  function normalizeTags(tags) {
15
38
  return Array.from(new Set((Array.isArray(tags) ? tags : [])
16
39
  .map((tag) => String(tag || "").trim().toLowerCase())
@@ -378,6 +401,107 @@ function VersionDeltaControls({ node, config, sandboxRow, onChange, disabled })
378
401
  );
379
402
  }
380
403
 
404
+ function LocalAgentHostControls({
405
+ sandboxRow,
406
+ objectId,
407
+ rowName,
408
+ disabled,
409
+ onSandboxRowPatch
410
+ }) {
411
+ const row = sandboxRow && typeof sandboxRow === "object" ? sandboxRow : {};
412
+ const runLocality = String(row.runLocality || "local").trim().toLowerCase() === "serverless" ? "serverless" : "local";
413
+ const adapter = String(row.adapter || "local-process").trim() || "local-process";
414
+ const agentHost = String(row.agentHost || "").trim();
415
+ const hostOptions = getAgentHostOptions();
416
+ const canPatch = typeof onSandboxRowPatch === "function";
417
+
418
+ function patch(fields) {
419
+ onSandboxRowPatch?.(fields);
420
+ }
421
+
422
+ function patchWithClearedAgentAuth(fields) {
423
+ patch({ ...fields, ...EMPTY_AGENT_AUTH_PATCH });
424
+ }
425
+
426
+ return (
427
+ <div className="dm-orchestration-config__section dm-workflow-agent-runtime">
428
+ <span>Local agent runtime</span>
429
+ <div className="dm-sandbox-locality-toggle" role="group" aria-label="Run locality">
430
+ {["local", "serverless"].map((mode) => (
431
+ <button
432
+ key={mode}
433
+ type="button"
434
+ className={runLocality === mode ? "is-active" : ""}
435
+ disabled={disabled || !canPatch}
436
+ onClick={() => {
437
+ const fields = { runLocality: mode };
438
+ if (mode === "serverless" && ["local-agent-host", "local-intelligence"].includes(adapter)) {
439
+ fields.adapter = "local-process";
440
+ fields.agentHost = "";
441
+ patchWithClearedAgentAuth(fields);
442
+ return;
443
+ }
444
+ patch(fields);
445
+ }}
446
+ >
447
+ {mode === "local" ? "Local" : "Serverless"}
448
+ </button>
449
+ ))}
450
+ </div>
451
+ <p className="dm-orchestration-config__hint">
452
+ Same runtime fields as the Data Model sandbox sidecar. Local agent host uses the Paperclip thin adapter on this machine.
453
+ </p>
454
+ {runLocality === "serverless" && (
455
+ <p className="dm-orchestration-config__hint">
456
+ Serverless delegates execution to the configured scheduler/API Registry row; local CLI auth is not used.
457
+ </p>
458
+ )}
459
+ <label className="dm-orchestration-config__field">
460
+ <span>Execution adapter</span>
461
+ <select
462
+ value={adapter}
463
+ disabled={disabled || !canPatch}
464
+ onChange={(event) => {
465
+ const nextAdapter = event.target.value;
466
+ patchWithClearedAgentAuth({
467
+ adapter: nextAdapter,
468
+ agentHost: nextAdapter === "local-agent-host" ? (agentHost || "claude_local") : ""
469
+ });
470
+ }}
471
+ >
472
+ {LOCAL_AGENT_ADAPTERS.map((item) => (
473
+ <option key={item.value} value={item.value}>{item.label}</option>
474
+ ))}
475
+ </select>
476
+ </label>
477
+ {runLocality === "local" && adapter === "local-agent-host" && (
478
+ <label className="dm-orchestration-config__field">
479
+ <span>Agent host (Paperclip)</span>
480
+ <select
481
+ value={agentHost}
482
+ disabled={disabled || !canPatch}
483
+ onChange={(event) => patchWithClearedAgentAuth({ agentHost: event.target.value })}
484
+ >
485
+ <option value="">Select host...</option>
486
+ {hostOptions.map((item) => (
487
+ <option key={item.value} value={item.value}>{item.label}</option>
488
+ ))}
489
+ </select>
490
+ </label>
491
+ )}
492
+ {runLocality === "local" && adapter === "local-agent-host" && isSandboxLocalAgentHost(row) && (
493
+ <SandboxAgentAuthPanel
494
+ objectId={objectId}
495
+ rowName={rowName}
496
+ draft={row}
497
+ disabled={disabled || !canPatch}
498
+ onPatchDraft={patch}
499
+ />
500
+ )}
501
+ </div>
502
+ );
503
+ }
504
+
381
505
  export function OrchestrationNodeConfigPanel({
382
506
  node,
383
507
  onConfigChange,
@@ -386,6 +510,9 @@ export function OrchestrationNodeConfigPanel({
386
510
  registryRow,
387
511
  workspaceConfig,
388
512
  sandboxRow,
513
+ objectId,
514
+ rowName,
515
+ onSandboxRowPatch,
389
516
  activeTab: controlledTab,
390
517
  onTabChange
391
518
  }) {
@@ -778,6 +905,15 @@ export function OrchestrationNodeConfigPanel({
778
905
  {MODEL_OPTIONS.map((model) => <option key={model} value={model}>{model}</option>)}
779
906
  </select>
780
907
  </label>
908
+ {(config.model || MODEL_OPTIONS[0]) === "Local agent host" && (
909
+ <LocalAgentHostControls
910
+ sandboxRow={sandboxRow}
911
+ objectId={objectId}
912
+ rowName={rowName}
913
+ disabled={disabled}
914
+ onSandboxRowPatch={onSandboxRowPatch}
915
+ />
916
+ )}
781
917
  <label className="dm-orchestration-config__field">
782
918
  <span>Input prompt</span>
783
919
  <textarea