@growthub/cli 0.14.4 → 0.14.5

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 (20) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/resolvers/[integrationId]/route.js +157 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +5 -1
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +86 -4
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +30 -5
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +2 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +400 -188
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +1 -1
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +1 -1
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +1 -1
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +3 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +24 -19
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +7 -82
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/resolver-constructor.js +217 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-registry.js +99 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/unified-resolver-registry.js +545 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +30 -2
  17. package/package.json +2 -2
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +0 -141
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +0 -64
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +0 -376
@@ -78,21 +78,22 @@ import { isSandboxLocalAgentHost } from "@/lib/sandbox-agent-auth-eligibility";
78
78
  import { StatusPill } from "./StatusPill.jsx";
79
79
  import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
80
80
  import { SourceTestPanel } from "./SourceTestPanel.jsx";
81
- import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
82
- import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
83
81
  import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
84
82
  import { ApiRegistryCreationCockpit } from "./ApiRegistryCreationCockpit.jsx";
85
83
  import { deriveApiRegistryCreationState } from "@/lib/api-registry-creation-flow";
86
84
  import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
87
85
  import { profileApiResponse, recommendResolver } from "@/lib/api-response-profile";
86
+ import { constructResolverProposal } from "@/lib/resolver-constructor";
88
87
  import { classifyCreationError } from "@/lib/creation-error-recovery";
89
88
  import {
90
- buildSandboxRowFromApiRegistry,
89
+ buildDefaultOrchestrationGraphFromRegistry,
91
90
  findSandboxRowsForRegistry,
92
91
  buildDataSourceRowFromApiRegistry,
93
92
  findDataSourceRowsForRegistry,
94
93
  getOrchestrationGraphUiState,
95
- redactSecretsFromText
94
+ redactSecretsFromText,
95
+ serializeOrchestrationGraph,
96
+ slugifyName
96
97
  } from "@/lib/orchestration-graph";
97
98
  import {
98
99
  FIELD_TYPE_ICON_NAMES,
@@ -1228,17 +1229,17 @@ function DataModelRecordDrawer({
1228
1229
  const [sandboxHistoryMessage, setSandboxHistoryMessage] = useState("");
1229
1230
  const [loadingSandboxHistory, setLoadingSandboxHistory] = useState(false);
1230
1231
  const [expandedJson, setExpandedJson] = useState(null);
1231
- const [sandboxToolFlow, setSandboxToolFlow] = useState(null);
1232
- const [sandboxToolDraft, setSandboxToolDraft] = useState({});
1233
- const [sandboxToolCreating, setSandboxToolCreating] = useState(false);
1234
- const [createdSandboxMeta, setCreatedSandboxMeta] = useState(null);
1235
- const [createdSandboxTesting, setCreatedSandboxTesting] = useState(false);
1236
- const [createdSandboxTestMessage, setCreatedSandboxTestMessage] = useState("");
1232
+ const [creatingWorkflowCanvas, setCreatingWorkflowCanvas] = useState(false);
1237
1233
  const [creatingDataSource, setCreatingDataSource] = useState(false);
1238
1234
  const [createdDataSourceMeta, setCreatedDataSourceMeta] = useState(null);
1239
1235
  const [dataSourceMessage, setDataSourceMessage] = useState("");
1240
1236
  const [cockpitBusy, setCockpitBusy] = useState("");
1241
1237
  const [cockpitCollapsed, setCockpitCollapsed] = useState(false);
1238
+ // CMS SDK v1.5.1 — a staged, constructed resolver awaiting one-screen review.
1239
+ // null until the operator clicks "Construct resolver"; cleared on apply/cancel.
1240
+ const [resolverConstruct, setResolverConstruct] = useState(null);
1241
+ const [resolverConstructBusy, setResolverConstructBusy] = useState(false);
1242
+ const [resolverConstructMessage, setResolverConstructMessage] = useState("");
1242
1243
  // Real runtime truth for the creation cockpit: which auth refs resolve in the
1243
1244
  // server runtime, and the live source-records sidecar. Fetched (never guessed)
1244
1245
  // so auth/refresh readiness reflect actual state, and refreshed after actions.
@@ -1265,10 +1266,7 @@ function DataModelRecordDrawer({
1265
1266
  setSandboxHistory([]);
1266
1267
  setSandboxHistoryMessage("");
1267
1268
  setExpandedJson(null);
1268
- setSandboxToolFlow(null);
1269
- setSandboxToolDraft({});
1270
- setCreatedSandboxMeta(null);
1271
- setCreatedSandboxTestMessage("");
1269
+ setCreatingWorkflowCanvas(false);
1272
1270
  setCreatingDataSource(false);
1273
1271
  setCreatedDataSourceMeta(null);
1274
1272
  setDataSourceMessage("");
@@ -1423,7 +1421,17 @@ function DataModelRecordDrawer({
1423
1421
  function ensureSandboxColumns(config, sandboxTable) {
1424
1422
  let next = config;
1425
1423
  let current = sandboxTable;
1426
- for (const field of ["orchestrationConfig", "description"]) {
1424
+ for (const field of [
1425
+ "orchestrationDraftConfig",
1426
+ "orchestrationDraftStatus",
1427
+ "orchestrationDraftUpdatedAt",
1428
+ "orchestrationDraftBaseVersion",
1429
+ "orchestrationDraftTestPassed",
1430
+ "orchestrationDraftTestedConfig",
1431
+ "description",
1432
+ "connectorKind",
1433
+ "executionLane"
1434
+ ]) {
1427
1435
  if (!current.columns.includes(field)) {
1428
1436
  next = addTableField(next, current, field);
1429
1437
  const tables = listWorkspaceDataModelTables(next);
@@ -1433,59 +1441,216 @@ function DataModelRecordDrawer({
1433
1441
  return { config: next, sandboxTable: current };
1434
1442
  }
1435
1443
 
1436
- function createSandboxToolFromRegistry() {
1437
- if (!sandboxToolDraft?.name?.trim()) return;
1444
+ function drawerId(prefix) {
1445
+ return `${prefix}-${Math.random().toString(36).slice(2, 8)}-${Date.now().toString(36)}`;
1446
+ }
1447
+
1448
+ function addWorkflowFolderShortcut(dataModel, workflow) {
1449
+ const objects = Array.isArray(dataModel?.objects) ? dataModel.objects : [];
1450
+ const seededObjects = objects.some((object) => object?.id === "nav-folders")
1451
+ ? objects
1452
+ : [
1453
+ ...objects,
1454
+ {
1455
+ id: "nav-folders",
1456
+ label: "Custom Folders",
1457
+ source: "Custom Folders",
1458
+ objectType: "custom",
1459
+ icon: "Folder",
1460
+ columns: ["name", "order", "collapsed", "items"],
1461
+ rows: [],
1462
+ binding: { mode: "manual", source: "Custom Folders" }
1463
+ }
1464
+ ];
1465
+ const navIndex = seededObjects.findIndex((object) => object?.id === "nav-folders");
1466
+ const navObject = seededObjects[navIndex];
1467
+ const rows = Array.isArray(navObject?.rows) ? navObject.rows : [];
1468
+ const folderName = "Builder";
1469
+ const item = {
1470
+ id: drawerId("item"),
1471
+ type: "workflow",
1472
+ objectId: workflow.objectId,
1473
+ rowId: workflow.rowId,
1474
+ fieldName: "orchestrationConfig",
1475
+ label: workflow.label,
1476
+ builderManaged: true,
1477
+ icon: "GitBranch",
1478
+ color: "#111827",
1479
+ iconBg: "#f3f4f6"
1480
+ };
1481
+ const existingFolder = rows.find((row) => String(row?.name || "").trim().toLowerCase() === folderName.toLowerCase());
1482
+ const nextRows = existingFolder
1483
+ ? rows.map((row) => {
1484
+ if (row !== existingFolder) return row;
1485
+ const items = Array.isArray(row.items) ? row.items : [];
1486
+ const exists = items.some((entry) => entry?.type === "workflow" && entry?.objectId === item.objectId && entry?.rowId === item.rowId);
1487
+ return exists ? row : { ...row, collapsed: false, items: [...items, item] };
1488
+ })
1489
+ : [
1490
+ ...rows,
1491
+ {
1492
+ id: drawerId("folder"),
1493
+ name: folderName,
1494
+ order: rows.length,
1495
+ collapsed: false,
1496
+ icon: "Folder",
1497
+ color: "#f97316",
1498
+ iconBg: "#fff7ed",
1499
+ items: [item]
1500
+ }
1501
+ ];
1502
+ return {
1503
+ ...dataModel,
1504
+ objects: seededObjects.map((object, index) => index === navIndex ? { ...navObject, rows: nextRows } : object)
1505
+ };
1506
+ }
1507
+
1508
+ function workflowGraphFromApiRegistry(registryRow) {
1509
+ const graph = buildDefaultOrchestrationGraphFromRegistry(registryRow, {
1510
+ rootPath: creationProfile?.arrayPath || "",
1511
+ });
1512
+ const humanInputNode = {
1513
+ id: "human-input",
1514
+ type: "human-input",
1515
+ label: "Human Input",
1516
+ subtitle: "Manual trigger",
1517
+ config: {
1518
+ action: "form",
1519
+ title: "Run API workflow",
1520
+ instructions: `Trigger ${String(registryRow?.integrationId || "this API").trim()} with optional inputs.`,
1521
+ required: false,
1522
+ requiresInput: false,
1523
+ fields: []
1524
+ }
1525
+ };
1526
+ return {
1527
+ ...graph,
1528
+ nodes: [humanInputNode, ...(Array.isArray(graph.nodes) ? graph.nodes : [])],
1529
+ edges: [
1530
+ { from: "human-input", to: "input", passes: "manual-input" },
1531
+ ...(Array.isArray(graph.edges) ? graph.edges : [])
1532
+ ]
1533
+ };
1534
+ }
1535
+
1536
+ async function openWorkflowCanvasFromRegistry() {
1438
1537
  const integrationId = String(draft?.integrationId || "").trim();
1439
- if (integrationId && findSandboxRowsForRegistry(workspaceConfig, integrationId).length > 0) {
1440
- setCreatedSandboxTestMessage("A sandbox tool already exists for this API Registry entry. Open it instead of creating a duplicate.");
1538
+ if (!integrationId) {
1539
+ setDataSourceMessage("This API Registry row needs an integrationId before a workflow can use it.");
1441
1540
  return;
1442
1541
  }
1443
- setSandboxToolCreating(true);
1542
+ setCreatingWorkflowCanvas(true);
1543
+ setDataSourceMessage("");
1444
1544
  try {
1445
- onSave((config) => {
1446
- let next = config;
1447
- if (integrationId && findSandboxRowsForRegistry(next, integrationId).length > 0) {
1448
- return next;
1449
- }
1450
- let sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
1451
- if (!sandboxTable) {
1452
- next = createTypedBusinessObject(next, {
1453
- name: "Sandbox Environments",
1454
- objectType: "sandbox-environment"
1455
- });
1456
- sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
1545
+ const currentRes = await fetch("/api/workspace", { cache: "no-store" });
1546
+ const currentPayload = await currentRes.json();
1547
+ if (!currentRes.ok || !currentPayload.workspaceConfig) {
1548
+ throw new Error(currentPayload.error || "Failed to load workspace");
1549
+ }
1550
+ let next = currentPayload.workspaceConfig;
1551
+ const existingWorkflow = (() => {
1552
+ const objects = Array.isArray(next?.dataModel?.objects) ? next.dataModel.objects : [];
1553
+ for (const object of objects) {
1554
+ if (object?.objectType !== "sandbox-environment") continue;
1555
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
1556
+ const matches = findSandboxRowsForRegistry({ dataModel: { objects: [object] } }, integrationId).some((candidate) => candidate === row);
1557
+ if (matches) return { objectId: object.id, rowName: row.Name };
1558
+ }
1457
1559
  }
1458
- if (!sandboxTable) return next;
1459
- const ensured = ensureSandboxColumns(next, sandboxTable);
1460
- next = ensured.config;
1461
- sandboxTable = ensured.sandboxTable;
1462
- const newRow = buildSandboxRowFromApiRegistry(next, draft, {
1463
- name: sandboxToolDraft.name,
1464
- description: sandboxToolDraft.description,
1465
- runLocality: sandboxToolDraft.runLocality,
1466
- adapter: sandboxToolDraft.adapter,
1467
- authRef: sandboxToolDraft.authRef,
1468
- envRefs: sandboxToolDraft.envRefs,
1469
- networkAllow: sandboxToolDraft.networkAllow,
1470
- timeoutMs: sandboxToolDraft.timeoutMs,
1471
- rootPath: sandboxToolDraft.rootPath,
1472
- instructions: sandboxToolDraft.instructions,
1473
- agentHost: sandboxToolDraft.agentHost,
1474
- schedulerRegistryId: sandboxToolDraft.schedulerRegistryId,
1475
- orchestrationGraph: sandboxToolDraft.orchestrationGraph
1476
- });
1477
- next = appendRowsToTable(next, sandboxTable, [newRow]);
1478
- setCreatedSandboxMeta({
1479
- objectId: sandboxTable.objectId,
1480
- name: newRow.Name,
1481
- authRef: newRow.authRef || sandboxToolDraft.authRef
1560
+ return null;
1561
+ })();
1562
+ if (existingWorkflow?.objectId && existingWorkflow?.rowName) {
1563
+ router.push(`/workflows?object=${encodeURIComponent(existingWorkflow.objectId)}&row=${encodeURIComponent(existingWorkflow.rowName)}&field=orchestrationConfig`);
1564
+ return;
1565
+ }
1566
+
1567
+ let sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
1568
+ if (!sandboxTable) {
1569
+ next = createTypedBusinessObject(next, {
1570
+ name: "Sandbox Environments",
1571
+ objectType: "sandbox-environment"
1482
1572
  });
1483
- setSandboxToolFlow("created");
1484
- onFocusSandboxRow?.({ rowName: newRow.Name, deferOpen: true });
1485
- return next;
1573
+ sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
1574
+ }
1575
+ if (!sandboxTable) throw new Error("No sandbox workflow object exists.");
1576
+ const ensured = ensureSandboxColumns(next, sandboxTable);
1577
+ next = ensured.config;
1578
+ sandboxTable = ensured.sandboxTable;
1579
+
1580
+ const rows = Array.isArray(sandboxTable.rows) ? sandboxTable.rows : [];
1581
+ const baseRowId = slugifyName(`${integrationId}-workflow`) || "api-workflow";
1582
+ const existingNames = new Set(rows.map((row) => String(row?.Name || row?.name || row?.id || "").trim()));
1583
+ let rowId = baseRowId;
1584
+ let suffix = 2;
1585
+ while (existingNames.has(rowId)) {
1586
+ rowId = `${baseRowId}-${suffix}`;
1587
+ suffix += 1;
1588
+ }
1589
+ const nowIso = new Date().toISOString();
1590
+ const draftGraph = workflowGraphFromApiRegistry(draft);
1591
+ const workflowRow = {
1592
+ Name: rowId,
1593
+ runLocality: "local",
1594
+ schedulerRegistryId: "",
1595
+ runtime: "node",
1596
+ adapter: "local-agent-host",
1597
+ agentHost: "claude_local",
1598
+ intelligenceType: "agent-host",
1599
+ localModel: "",
1600
+ localEndpoint: "",
1601
+ intelligenceAdapterMode: "ollama",
1602
+ envRefs: "",
1603
+ networkAllow: "false",
1604
+ allowList: "",
1605
+ instructions: `Draft workflow for ${integrationId}. The canvas starts with a human input trigger and an API Registry call node.`,
1606
+ command: "",
1607
+ orchestrationDraftConfig: serializeOrchestrationGraph(draftGraph),
1608
+ orchestrationDraftStatus: "draft",
1609
+ orchestrationDraftUpdatedAt: nowIso,
1610
+ orchestrationDraftBaseVersion: "0",
1611
+ orchestrationDraftTestPassed: false,
1612
+ orchestrationDraftTestedConfig: "",
1613
+ timeoutMs: "180000",
1614
+ resolverTemplateId: String(draft?.resolverTemplateId || "").trim(),
1615
+ connectorKind: "local-agent-host",
1616
+ executionLane: "workflow",
1617
+ status: "draft",
1618
+ lastTested: "",
1619
+ lastRunId: "",
1620
+ lastSourceId: "",
1621
+ lastResponse: "",
1622
+ description: String(draft?.description || "").trim()
1623
+ };
1624
+ next = appendRowsToTable(next, sandboxTable, [workflowRow]);
1625
+ const finalDataModel = addWorkflowFolderShortcut(next.dataModel, {
1626
+ objectId: sandboxTable.objectId,
1627
+ rowId,
1628
+ label: rowId
1486
1629
  });
1630
+ const patchBody = { dataModel: finalDataModel };
1631
+ const preflightResponse = await fetch("/api/workspace/patch/preflight", {
1632
+ method: "POST",
1633
+ headers: { "content-type": "application/json" },
1634
+ body: JSON.stringify(patchBody)
1635
+ });
1636
+ const preflightPayload = await preflightResponse.json();
1637
+ if (!preflightResponse.ok || !preflightPayload.ok) {
1638
+ throw new Error("Workflow creation is blocked by workspace policy.");
1639
+ }
1640
+ const response = await fetch("/api/workspace", {
1641
+ method: "PATCH",
1642
+ headers: { "content-type": "application/json" },
1643
+ body: JSON.stringify(patchBody)
1644
+ });
1645
+ const payload = await response.json();
1646
+ if (!response.ok || !payload.workspaceConfig) {
1647
+ throw new Error("Workflow creation failed. Try again.");
1648
+ }
1649
+ router.push(`/workflows?object=${encodeURIComponent(sandboxTable.objectId)}&row=${encodeURIComponent(rowId)}&field=orchestrationConfig`);
1650
+ } catch (err) {
1651
+ setDataSourceMessage(redactSecretsFromText(err.message || "Failed to open workflow canvas"));
1487
1652
  } finally {
1488
- setSandboxToolCreating(false);
1653
+ setCreatingWorkflowCanvas(false);
1489
1654
  }
1490
1655
  }
1491
1656
 
@@ -1633,11 +1798,103 @@ function DataModelRecordDrawer({
1633
1798
  }
1634
1799
  }
1635
1800
 
1801
+ // CMS SDK v1.5.1 — construct the governed resolver from the tested response
1802
+ // shape and stage it for one-screen review. No blank form: rootPath / idField
1803
+ // / entityType / auth header are all derived (or surfaced as `blanks` when the
1804
+ // row is missing a target). Nothing is written until the operator confirms.
1805
+ function stageResolverConstruct() {
1806
+ const integrationId = String(draft?.integrationId || "").trim();
1807
+ if (!integrationId) {
1808
+ setResolverConstructMessage("This API Registry row needs an integrationId before a resolver can be constructed.");
1809
+ return;
1810
+ }
1811
+ const profile = profileApiResponse(draft?.lastResponse);
1812
+ const recommendation = profile ? recommendResolver(profile) : null;
1813
+ const result = constructResolverProposal({
1814
+ row: draft,
1815
+ profile,
1816
+ recommendation,
1817
+ recordRef: { objectId: table?.objectId, rowName: draft?.Name || integrationId },
1818
+ });
1819
+ setResolverConstructMessage("");
1820
+ setResolverConstruct(result);
1821
+ }
1822
+
1823
+ // Apply the staged resolver through the GOVERNED lane only (helper/apply →
1824
+ // writeResolverProposalFile), mark the row wired (resolverTemplateId), then
1825
+ // re-test through the resolver so the user sees green without leaving the
1826
+ // drawer. config-driven (Nango) and unsupported kinds never reach here.
1827
+ async function applyResolverConstruct() {
1828
+ const staged = resolverConstruct;
1829
+ if (!staged || staged.mode !== "file" || !staged.proposal || !staged.ok) return;
1830
+ const integrationId = String(draft?.integrationId || "").trim();
1831
+ setResolverConstructBusy(true);
1832
+ setResolverConstructMessage("");
1833
+ try {
1834
+ const res = await fetch("/api/workspace/helper/apply", {
1835
+ method: "POST",
1836
+ headers: { "content-type": "application/json" },
1837
+ body: JSON.stringify({ proposals: [staged.proposal], reviewedBy: "cockpit" }),
1838
+ });
1839
+ const payload = await res.json();
1840
+ const appliedOne = Array.isArray(payload.applied)
1841
+ ? payload.applied.find((a) => a.type === "resolver.create")
1842
+ : null;
1843
+ if (!res.ok || !appliedOne) {
1844
+ const skip = Array.isArray(payload.skipped) ? payload.skipped[0]?.reason : null;
1845
+ const msg = redactSecretsFromText(skip || payload.error || payload.guidance || "Resolver apply failed");
1846
+ setResolverConstructMessage(msg);
1847
+ pushReceipt({ kind: "resolver-construct", ok: false, detail: msg });
1848
+ return;
1849
+ }
1850
+ // Mark the row wired so the creation journey closes (resolverTemplateId
1851
+ // becomes a real registered resolver id, not the "custom-http" passthrough).
1852
+ onSave((config) => {
1853
+ const t = listWorkspaceDataModelTables(config).find((x) => x.objectId === table?.objectId);
1854
+ if (!t) return config;
1855
+ const idx = (t.rows || []).findIndex((r) => String(r?.integrationId || "").trim() === integrationId);
1856
+ if (idx < 0) return config;
1857
+ return updateTableCell(config, t, idx, "resolverTemplateId", integrationId);
1858
+ });
1859
+ pushReceipt({
1860
+ kind: "resolver-construct",
1861
+ ok: true,
1862
+ detail: `Resolver "${appliedOne.resolverFilename || integrationId}" constructed and wired.`,
1863
+ });
1864
+ // Re-test through the new resolver — the secret stays server-side.
1865
+ try {
1866
+ const testRes = await fetch("/api/workspace/test-source", {
1867
+ method: "POST",
1868
+ headers: { "content-type": "application/json" },
1869
+ body: JSON.stringify({ integrationId, binding: {} }),
1870
+ });
1871
+ const testPayload = await testRes.json();
1872
+ if (testPayload?.ok) {
1873
+ pushReceipt({ kind: "resolver-test", ok: true, detail: `Resolver returned ${testPayload.recordCount ?? 0} record(s).` });
1874
+ } else {
1875
+ pushReceipt({
1876
+ kind: "resolver-test",
1877
+ ok: false,
1878
+ detail: redactSecretsFromText(testPayload?.error || testPayload?.reason || "test-source did not return ok"),
1879
+ });
1880
+ }
1881
+ } catch {
1882
+ /* non-fatal — the resolver is written; refresh re-tests it later */
1883
+ }
1884
+ setResolverConstruct(null);
1885
+ await reloadCreationSignals();
1886
+ } catch (err) {
1887
+ setResolverConstructMessage(redactSecretsFromText(err.message || "Resolver apply failed"));
1888
+ } finally {
1889
+ setResolverConstructBusy(false);
1890
+ }
1891
+ }
1892
+
1636
1893
  // The creation cockpit emits a single action descriptor per step; map it to
1637
1894
  // the drawer's existing governed handlers. No new mutation paths.
1638
1895
  async function handleCockpitAction(action) {
1639
1896
  if (!action || !action.id) return;
1640
- const tag = `${action.stepId}:${action.id}`;
1897
+ const tag = `${action.stepId || "workflow"}:${action.id}`;
1641
1898
  setCockpitBusy(tag);
1642
1899
  try {
1643
1900
  switch (action.id) {
@@ -1649,6 +1906,11 @@ function DataModelRecordDrawer({
1649
1906
  onClose();
1650
1907
  router.push(action.href || "/settings");
1651
1908
  break;
1909
+ case "construct-resolver":
1910
+ // CMS SDK v1.5.1 — construct the governed resolver from the tested
1911
+ // response shape (no blank form) and stage it for one-screen review.
1912
+ stageResolverConstruct();
1913
+ break;
1652
1914
  case "open-resolver":
1653
1915
  // Hand off to the governed helper widget — the resolver proposal lane.
1654
1916
  // Carries the integrationId so the helper can scope a resolver proposal.
@@ -1665,8 +1927,9 @@ function DataModelRecordDrawer({
1665
1927
  case "open-data-source":
1666
1928
  openDataSourceRow(action.objectId);
1667
1929
  break;
1668
- case "create-sandbox-tool":
1669
- setSandboxToolFlow("draft");
1930
+ case "create-workflow-canvas":
1931
+ case "open-workflow-canvas":
1932
+ await openWorkflowCanvasFromRegistry();
1670
1933
  break;
1671
1934
  case "refresh-source":
1672
1935
  await refreshLinkedSource({ objectId: action.objectId });
@@ -1704,78 +1967,6 @@ function DataModelRecordDrawer({
1704
1967
  }
1705
1968
  : null;
1706
1969
 
1707
- async function runSandboxToolByName({ objectId, name }) {
1708
- const rowName = String(name || "").trim();
1709
- const objectIdValue = String(objectId || "").trim();
1710
- if (!rowName || !objectIdValue) return;
1711
- setCreatedSandboxTesting(true);
1712
- setCreatedSandboxTestMessage("");
1713
- try {
1714
- const res = await fetch("/api/workspace/sandbox-run", {
1715
- method: "POST",
1716
- headers: { "content-type": "application/json" },
1717
- body: JSON.stringify({ objectId: objectIdValue, name: rowName }),
1718
- });
1719
- const payload = await res.json();
1720
- const responseText = redactSecretsFromText(JSON.stringify(payload.response ?? payload, null, 2));
1721
- const status = payload.ok && String(payload.status || "").toLowerCase() === "connected" ? "connected" : "failed";
1722
- const testedAt = payload.response?.ranAt || new Date().toISOString();
1723
- const lastRunId = payload.runId || payload.response?.runId || "";
1724
- const lastSourceId = payload.sourceId || payload.response?.sourceId || "";
1725
- onSave((config) => {
1726
- const sandboxTable = listWorkspaceDataModelTables(config).find((t) => t.objectType === "sandbox-environment");
1727
- if (!sandboxTable) return config;
1728
- const idx = (sandboxTable.rows || []).findIndex((r) => String(r?.Name || "").trim() === rowName);
1729
- if (idx < 0) return config;
1730
- let next = updateTableCell(config, sandboxTable, idx, "status", status);
1731
- next = updateTableCell(next, sandboxTable, idx, "lastTested", testedAt);
1732
- next = updateTableCell(next, sandboxTable, idx, "lastRunId", lastRunId);
1733
- next = updateTableCell(next, sandboxTable, idx, "lastSourceId", lastSourceId);
1734
- next = updateTableCell(next, sandboxTable, idx, "lastResponse", responseText);
1735
- return next;
1736
- });
1737
- const safeError = redactSecretsFromText(
1738
- payload.response?.error || payload.error || "Sandbox run failed"
1739
- );
1740
- setCreatedSandboxTestMessage(
1741
- status === "connected"
1742
- ? "Sandbox run succeeded — lastResponse and source record saved."
1743
- : safeError
1744
- );
1745
- } catch (err) {
1746
- setCreatedSandboxTestMessage(redactSecretsFromText(err.message || "Sandbox run failed"));
1747
- } finally {
1748
- setCreatedSandboxTesting(false);
1749
- }
1750
- }
1751
-
1752
- function resolveSandboxTableMeta() {
1753
- const sandboxTable = tables.find((t) => t.objectType === "sandbox-environment")
1754
- || (workspaceConfig ? listWorkspaceDataModelTables(workspaceConfig).find((t) => t.objectType === "sandbox-environment") : null);
1755
- return sandboxTable?.objectId ? { objectId: sandboxTable.objectId, table: sandboxTable } : null;
1756
- }
1757
-
1758
- async function runCreatedSandboxTest() {
1759
- if (!createdSandboxMeta?.objectId || !createdSandboxMeta?.name) return;
1760
- await runSandboxToolByName(createdSandboxMeta);
1761
- }
1762
-
1763
- async function runExistingSandboxTool({ name }) {
1764
- const meta = resolveSandboxTableMeta();
1765
- if (!meta) {
1766
- setCreatedSandboxTestMessage("No sandbox-environment table in this workspace.");
1767
- return;
1768
- }
1769
- await runSandboxToolByName({ objectId: meta.objectId, name });
1770
- }
1771
-
1772
- function openSandboxToolRow({ name }) {
1773
- const rowName = String(name || "").trim();
1774
- if (!rowName) return;
1775
- onFocusSandboxRow?.({ rowName });
1776
- onClose();
1777
- }
1778
-
1779
1970
  async function runSandbox() {
1780
1971
  if (!table.objectId) {
1781
1972
  setSandboxMessage("Missing object id for this sandbox table.");
@@ -1874,7 +2065,7 @@ function DataModelRecordDrawer({
1874
2065
  onClearInitialSidecar?.();
1875
2066
  }
1876
2067
 
1877
- const drawerWide = sandboxToolFlow === "draft" || sidecarMode === "trace";
2068
+ const drawerWide = sidecarMode === "trace";
1878
2069
  const hideRecordFields = isSandbox && sidecarMode === "trace";
1879
2070
 
1880
2071
  return (
@@ -1903,7 +2094,7 @@ function DataModelRecordDrawer({
1903
2094
  {sandboxRunning ? "Running…" : "Execute"}
1904
2095
  </button>
1905
2096
  )}
1906
- {!isSandbox && sandboxToolFlow !== "draft" && (
2097
+ {!isSandbox && (
1907
2098
  <button type="button" className="dm-sidebar-close" onClick={() => setEditMode((current) => !current)} aria-label="Toggle edit mode">
1908
2099
  <Pencil size={16} />
1909
2100
  </button>
@@ -1913,7 +2104,7 @@ function DataModelRecordDrawer({
1913
2104
  </button>
1914
2105
  </div>
1915
2106
  </header>
1916
- {table.objectType === "data-source" && sandboxToolFlow !== "draft" && (
2107
+ {table.objectType === "data-source" && (
1917
2108
  <SourceTestPanel
1918
2109
  status={draft.status}
1919
2110
  testing={testing}
@@ -1923,41 +2114,13 @@ function DataModelRecordDrawer({
1923
2114
  />
1924
2115
  )}
1925
2116
  <div className="dm-record-scroll" ref={drawerScrollRef}>
1926
- {isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
1927
- <section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
1928
- <div className="dm-api-action-card-body">
1929
- <p className="dm-api-action-card-eyebrow">Sandbox tool created</p>
1930
- <h3>{createdSandboxMeta.name}</h3>
1931
- <p>Governed sandbox row saved with orchestrationGraph. Run test to persist lastResponse — nothing auto-runs.</p>
1932
- {createdSandboxTestMessage && <p className="dm-sandbox-tool-test-msg">{createdSandboxTestMessage}</p>}
1933
- </div>
1934
- <div className="dm-api-action-card-actions">
1935
- <button
1936
- type="button"
1937
- className="dm-btn-outline dm-api-action-card-cta"
1938
- disabled={saving}
1939
- onClick={() => openSandboxToolRow({ name: createdSandboxMeta.name })}
1940
- >
1941
- Open sandbox tool
1942
- </button>
1943
- <button
1944
- type="button"
1945
- className="dm-btn-primary-sm dm-api-action-card-cta"
1946
- disabled={createdSandboxTesting || saving}
1947
- onClick={runCreatedSandboxTest}
1948
- >
1949
- {createdSandboxTesting ? "Running…" : "Run sandbox"}
1950
- </button>
1951
- </div>
1952
- </section>
1953
- )}
1954
- {isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "confirm" && creationState && (
2117
+ {isApiRegistry && creationState && (
1955
2118
  <>
1956
2119
  <ApiRegistryCreationCockpit
1957
2120
  state={creationState}
1958
2121
  onAction={handleCockpitAction}
1959
2122
  busyAction={cockpitBusy}
1960
- disabled={saving || creatingDataSource || testing}
2123
+ disabled={saving || creatingDataSource || testing || creatingWorkflowCanvas}
1961
2124
  profile={creationProfile}
1962
2125
  resolverRec={creationResolverRec}
1963
2126
  receipts={creationReceipts}
@@ -1966,28 +2129,77 @@ function DataModelRecordDrawer({
1966
2129
  hideWhenComplete
1967
2130
  onCollapsedChange={setCockpitCollapsed}
1968
2131
  />
2132
+ {resolverConstruct ? (
2133
+ <section className="dm-api-action-card dm-cockpit" aria-label="Construct resolver review">
2134
+ <div className="dm-cockpit-shape">
2135
+ <div className="dm-cockpit-shape-head">
2136
+ <p className="dm-api-action-card-eyebrow">Make this API usable · {resolverConstruct.connectorKind}</p>
2137
+ {resolverConstruct.detected?.confidence ? (
2138
+ <span className={`dm-db-status ${resolverConstruct.detected.confidence === "high" ? "ok" : resolverConstruct.detected.confidence === "low" ? "bad" : "warn"}`}>
2139
+ <span />
2140
+ {resolverConstruct.detected.confidence} confidence
2141
+ </span>
2142
+ ) : null}
2143
+ </div>
2144
+ {resolverConstruct.detected?.sentence ? (
2145
+ <p className="dm-cockpit-step-desc"><b>{resolverConstruct.detected.sentence}</b></p>
2146
+ ) : null}
2147
+ <p className="dm-cockpit-step-desc">{resolverConstruct.reason}</p>
2148
+ {resolverConstruct.mode === "file" && resolverConstruct.authRef ? (
2149
+ <p className="dm-cockpit-step-hint">Safe: the secret stays server-side via <code>{resolverConstruct.authRef}</code> — it never reaches the browser or the generated file.</p>
2150
+ ) : null}
2151
+ {resolverConstruct.mode === "file" && resolverConstruct.prefill ? (
2152
+ <div className="dm-cockpit-fields">
2153
+ <span className="dm-cockpit-field"><b>records at</b>{resolverConstruct.prefill.rootPath || "top-level"}</span>
2154
+ <span className="dm-cockpit-field"><b>row id</b>{resolverConstruct.prefill.idField}</span>
2155
+ <span className="dm-cockpit-field"><b>entity</b>{resolverConstruct.prefill.entityType}</span>
2156
+ {resolverConstruct.proposal?.target?.path ? (
2157
+ <span className="dm-cockpit-field"><b>file</b>{resolverConstruct.proposal.target.path}</span>
2158
+ ) : null}
2159
+ {resolverConstruct.endpoint ? (
2160
+ <span className="dm-cockpit-field"><b>endpoint</b>{resolverConstruct.endpoint}</span>
2161
+ ) : null}
2162
+ </div>
2163
+ ) : null}
2164
+ {resolverConstruct.mode === "config-driven" && resolverConstruct.endpoint ? (
2165
+ <div className="dm-cockpit-fields">
2166
+ <span className="dm-cockpit-field"><b>type</b>config-driven (no file)</span>
2167
+ <span className="dm-cockpit-field"><b>endpoint</b>{resolverConstruct.endpoint}</span>
2168
+ </div>
2169
+ ) : null}
2170
+ {resolverConstruct.blanks?.length ? (
2171
+ <p className="dm-cockpit-step-hint">Missing on the row: {resolverConstruct.blanks.join(", ")}</p>
2172
+ ) : null}
2173
+ {resolverConstruct.mode === "file" && resolverConstruct.ok ? (
2174
+ <p className="dm-cockpit-step-hint">After apply: the resolver file is written via the governed lane, the row is marked wired, and it's re-tested automatically. If the re-test fails, the resolver is still written — the receipt distinguishes &ldquo;written&rdquo; from &ldquo;runtime failed&rdquo;.</p>
2175
+ ) : null}
2176
+ {resolverConstructMessage ? <p className="dm-cockpit-step-hint">{resolverConstructMessage}</p> : null}
2177
+ <div className="dm-cockpit-shape-head">
2178
+ {resolverConstruct.mode === "file" && resolverConstruct.ok ? (
2179
+ <button
2180
+ type="button"
2181
+ className="dm-btn-primary-sm"
2182
+ disabled={resolverConstructBusy}
2183
+ onClick={applyResolverConstruct}
2184
+ >
2185
+ {resolverConstructBusy ? "Applying…" : "Apply resolver"}
2186
+ </button>
2187
+ ) : null}
2188
+ <button
2189
+ type="button"
2190
+ className="dm-btn-outline"
2191
+ disabled={resolverConstructBusy}
2192
+ onClick={() => { setResolverConstruct(null); setResolverConstructMessage(""); }}
2193
+ >
2194
+ {resolverConstruct.mode === "file" && resolverConstruct.ok ? "Cancel" : "Dismiss"}
2195
+ </button>
2196
+ </div>
2197
+ </div>
2198
+ </section>
2199
+ ) : null}
1969
2200
  {dataSourceMessage ? <p className="dm-sandbox-tool-test-msg">{dataSourceMessage}</p> : null}
1970
2201
  </>
1971
2202
  )}
1972
- {isApiRegistry && sandboxToolFlow === "draft" && (
1973
- <SandboxToolDraftPanel
1974
- registryRow={draft}
1975
- draftOptions={sandboxToolDraft}
1976
- disabled={saving || sandboxToolCreating}
1977
- onDraftChange={setSandboxToolDraft}
1978
- onRequestConfirm={() => setSandboxToolFlow("confirm")}
1979
- onCancel={() => setSandboxToolFlow(null)}
1980
- />
1981
- )}
1982
- <SandboxToolConfirmModal
1983
- open={isApiRegistry && sandboxToolFlow === "confirm"}
1984
- toolName={sandboxToolDraft?.name || ""}
1985
- authRef={sandboxToolDraft?.authRef || draft.authRef}
1986
- orchestrationGraph={sandboxToolDraft?.orchestrationGraph}
1987
- creating={sandboxToolCreating}
1988
- onConfirm={createSandboxToolFromRegistry}
1989
- onCancel={() => setSandboxToolFlow("draft")}
1990
- />
1991
2203
  {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && sandboxMessage && (
1992
2204
  <SandboxRunPanel
1993
2205
  status={draft.status}
@@ -2024,7 +2236,7 @@ function DataModelRecordDrawer({
2024
2236
  />
2025
2237
  )}
2026
2238
  <div className="dm-record-fields">
2027
- {isApiRegistry && sandboxToolFlow === "draft" ? null : hideRecordFields ? null : isSandbox ? (
2239
+ {hideRecordFields ? null : isSandbox ? (
2028
2240
  <SandboxRecordFields
2029
2241
  draft={draft}
2030
2242
  setDraft={setDraft}