@growthub/cli 0.13.9 → 0.14.1

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 (39) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Server Resolver Write V1 — the single, confined, gated fs write for resolver
3
+ * files. Used by the helper apply route's resolver.create lane (and available
4
+ * to register-resolver). Server-only.
5
+ *
6
+ * AWaC topology: writes only in filesystem mode; read-only runtimes throw a
7
+ * coded error with guidance (the same 409 contract the rest of the workspace
8
+ * uses). Path is confined to lib/adapters/integrations/resolvers — never
9
+ * escapes. Never logs file contents.
10
+ */
11
+
12
+ import { promises as fs } from "node:fs";
13
+ import path from "node:path";
14
+ import { describePersistenceMode } from "@/lib/workspace-config";
15
+ import { resolveResolverFilePath } from "@/lib/workspace-resolver-proposal";
16
+
17
+ const MAX_RESOLVER_SIZE = 256 * 1024;
18
+
19
+ /**
20
+ * Write a resolver file from a validated proposal. Returns
21
+ * { saved:true, path, filename } or throws a coded error:
22
+ * - WORKSPACE_PERSISTENCE_READ_ONLY (read-only runtime; carries guidance)
23
+ * - INVALID_RESOLVER_WRITE (bad path / code)
24
+ * - WORKSPACE_PERSISTENCE_PATH_REFUSED (escaped the resolver dir)
25
+ */
26
+ async function writeResolverProposalFile(proposal) {
27
+ const persistence = describePersistenceMode();
28
+ if (!persistence.canSave) {
29
+ const error = new Error(persistence.reason || "resolver write requires a writable filesystem runtime");
30
+ error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
31
+ error.guidance = persistence.guidance || "Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true or use local development mode.";
32
+ throw error;
33
+ }
34
+ const target = (proposal?.target && proposal.target.ok)
35
+ ? proposal.target
36
+ : resolveResolverFilePath(proposal?.payload?.integrationId);
37
+ if (!target || !target.ok) {
38
+ const error = new Error(target?.error || "invalid resolver target path");
39
+ error.code = "INVALID_RESOLVER_WRITE";
40
+ throw error;
41
+ }
42
+ const code = String(proposal?.code || "");
43
+ if (!code.includes("registerSourceResolver")) {
44
+ const error = new Error("resolver code must call registerSourceResolver()");
45
+ error.code = "INVALID_RESOLVER_WRITE";
46
+ throw error;
47
+ }
48
+ if (Buffer.byteLength(code, "utf8") > MAX_RESOLVER_SIZE) {
49
+ const error = new Error(`resolver file must be smaller than ${MAX_RESOLVER_SIZE / 1024} KB`);
50
+ error.code = "INVALID_RESOLVER_WRITE";
51
+ throw error;
52
+ }
53
+
54
+ const resolversDir = path.resolve(/*turbopackIgnore: true*/ process.cwd(), target.dir);
55
+ const outPath = path.join(resolversDir, target.filename);
56
+ if (path.dirname(outPath) !== resolversDir) {
57
+ const error = new Error("invalid filename — path traversal not allowed");
58
+ error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
59
+ throw error;
60
+ }
61
+
62
+ await fs.mkdir(resolversDir, { recursive: true });
63
+ await fs.writeFile(outPath, code, "utf8");
64
+ return { saved: true, path: target.path, filename: target.filename };
65
+ }
66
+
67
+ export { writeResolverProposalFile };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Serverless Upgrade Onboarding V1 — derives the one-time "upgrade your
3
+ * localhost workflow to a persistent, scheduled serverless workflow" nudge from
4
+ * the workspace configuration state, following the same one-time onboarding
5
+ * pattern as the lens walkthrough (workspace-ui-cache dismiss flag).
6
+ *
7
+ * It reads only the existing model: sandbox-environment rows and their
8
+ * `runLocality` field (already part of the governed schema). When the operator
9
+ * has workflows but none are serverless — and hasn't dismissed the nudge — the
10
+ * upgrade path shows once.
11
+ *
12
+ * Pure + deterministic; never throws. The dismiss flag is read by the caller
13
+ * from the governed workspace-ui-cache row and passed in.
14
+ */
15
+
16
+ const SERVERLESS_UPGRADE_DISMISS_FLAG = "serverlessUpgradeDismissed";
17
+ const UPGRADE_STATE_KIND = "growthub-serverless-upgrade-state-v1";
18
+
19
+ function isPlainObject(value) {
20
+ return value !== null && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ function clean(value) {
24
+ return String(value == null ? "" : value).trim();
25
+ }
26
+
27
+ /** Is a sandbox row configured to run serverless? */
28
+ function rowIsServerless(row) {
29
+ return isPlainObject(row) && clean(row.runLocality).toLowerCase() === "serverless";
30
+ }
31
+
32
+ /** Collect every sandbox-environment (workflow) row across the data model. */
33
+ function collectWorkflowRows(workspaceConfig) {
34
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
35
+ const rows = [];
36
+ for (const object of objects) {
37
+ if (object?.objectType !== "sandbox-environment") continue;
38
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
39
+ if (isPlainObject(row)) rows.push(row);
40
+ }
41
+ }
42
+ return rows;
43
+ }
44
+
45
+ /**
46
+ * Derive the serverless-upgrade onboarding state.
47
+ *
48
+ * @param {object} workspaceConfig
49
+ * @param {object} [options]
50
+ * @param {boolean} [options.dismissed] dismiss flag from workspace-ui-cache
51
+ */
52
+ function deriveServerlessUpgradeState(workspaceConfig, options = {}) {
53
+ const opts = isPlainObject(options) ? options : {};
54
+ const rows = collectWorkflowRows(workspaceConfig);
55
+ const serverlessCount = rows.filter(rowIsServerless).length;
56
+ const localCount = rows.length - serverlessCount;
57
+ const hasWorkflows = rows.length > 0;
58
+ const dismissed = opts.dismissed === true || String(opts.dismissed || "") === "true";
59
+ // Show the one-time path only when the operator has built workflows but none
60
+ // are serverless yet — the moment the upgrade mental model is most useful.
61
+ const showOnboarding = hasWorkflows && serverlessCount === 0 && !dismissed;
62
+ return {
63
+ kind: UPGRADE_STATE_KIND,
64
+ version: 1,
65
+ hasWorkflows,
66
+ workflowCount: rows.length,
67
+ serverlessCount,
68
+ localCount,
69
+ allLocal: hasWorkflows && serverlessCount === 0,
70
+ dismissed,
71
+ showOnboarding,
72
+ headline: showOnboarding
73
+ ? "Upgrade a workflow to serverless"
74
+ : serverlessCount > 0
75
+ ? "Serverless workflows are active"
76
+ : "Workflows run locally",
77
+ subheadline: showOnboarding
78
+ ? "Make it persistent and scheduled — it runs on invocation through your configured adapter, not just on this machine."
79
+ : "",
80
+ };
81
+ }
82
+
83
+ export {
84
+ SERVERLESS_UPGRADE_DISMISS_FLAG,
85
+ UPGRADE_STATE_KIND,
86
+ rowIsServerless,
87
+ collectWorkflowRows,
88
+ deriveServerlessUpgradeState,
89
+ };
@@ -405,9 +405,15 @@ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceR
405
405
  }, 0);
406
406
  const widgetAdded = widgetCount > 0;
407
407
 
408
- const workflowMatch = findWorkflowRow(workspaceConfig, () => true);
408
+ const workflowMatch = findWorkflowRow(workspaceConfig, (_row, object) => safeString(object?.id).trim() !== "workspace-helper-sandbox");
409
409
  const workflowCreated = Boolean(workflowMatch?.row);
410
410
  const workflowRun = deriveLatestRunStatus(workflowMatch?.row);
411
+ const workflowObjectId = safeString(workflowMatch?.object?.id).trim();
412
+ const workflowRowName = safeString(workflowMatch?.row?.Name || workflowMatch?.row?.name || workflowMatch?.row?.slug || workflowMatch?.row?.id).trim();
413
+ const workflowField = workflowMatch?.row?.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph";
414
+ const workflowHref = workflowObjectId && workflowRowName
415
+ ? `/workflows?object=${encodeURIComponent(workflowObjectId)}&row=${encodeURIComponent(workflowRowName)}&field=${encodeURIComponent(workflowField)}`
416
+ : "";
411
417
 
412
418
  const steps = [
413
419
  {
@@ -449,8 +455,9 @@ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceR
449
455
  ? "Sandbox workflow scaffolded."
450
456
  : "Open Workflows to assemble your first automation.",
451
457
  status: workflowCreated ? "complete" : "pending",
452
- href: "/workflows",
453
- cta: workflowCreated ? "Open Workflows" : "New workflow",
458
+ href: workflowCreated ? workflowHref : "",
459
+ action: workflowCreated ? "" : "create-workflow",
460
+ cta: workflowCreated ? "Open workflow" : "New workflow",
454
461
  },
455
462
  {
456
463
  id: "run-workflow",
@@ -461,7 +468,7 @@ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceR
461
468
  ? "Last run failed — open the trace and fix the failing node."
462
469
  : "Click Test inside the workflow to do a first run.",
463
470
  status: workflowRun.ok ? "complete" : (workflowCreated ? "pending" : "blocked"),
464
- href: "/workflows",
471
+ href: workflowCreated ? workflowHref : "",
465
472
  hint: workflowRun.ok || workflowCreated ? "" : "Create a workflow first.",
466
473
  cta: workflowRun.ok ? "View runs" : "Open workflow",
467
474
  },
@@ -937,6 +937,13 @@ function createTypedBusinessObject(workspaceConfig, { name, objectType = "custom
937
937
  ? workspaceConfig.dataModel
938
938
  : {};
939
939
  const id = uniqueObjectId(workspaceConfig, label);
940
+ const relations = preset.relations
941
+ ? preset.relations.map((relation) => {
942
+ const next = { ...relation };
943
+ if (next.statusAllowlist == null) delete next.statusAllowlist;
944
+ return next;
945
+ })
946
+ : [];
940
947
  const object = {
941
948
  id,
942
949
  label,
@@ -946,7 +953,7 @@ function createTypedBusinessObject(workspaceConfig, { name, objectType = "custom
946
953
  columns,
947
954
  rows: [],
948
955
  binding: { mode: "manual", source: "Data Model" },
949
- relations: preset.relations ? preset.relations.map((r) => ({ ...r })) : [],
956
+ relations,
950
957
  fieldSettings: normalizeFieldSettings({}, columns)
951
958
  };
952
959
  return {
@@ -37,6 +37,15 @@ const WORKSPACE_HELPER_PROPOSAL_TYPES = [
37
37
  "dataModel.row.add",
38
38
  "repair.binding",
39
39
  "explain.object",
40
+ // Server-file lane (AWaC: NOT a config PATCH field). Routed in helper/apply to
41
+ // the confined, gated resolver write — never through writeWorkspaceConfig.
42
+ "resolver.create",
43
+ // Swarm lane — governed sandbox-environment rows in the EXISTING dataModel
44
+ // patch field. Apply normalizes the intent payload into an agent-swarm-v1
45
+ // graph via buildDefaultAgentSwarmGraph; execution stays behind sandbox-run.
46
+ "swarm.run.propose",
47
+ "swarm.workflow.save",
48
+ "swarm.run.resume",
40
49
  ];
41
50
 
42
51
  const PROPOSAL_TYPE_TO_PATCH_FIELD = {
@@ -50,6 +59,13 @@ const PROPOSAL_TYPE_TO_PATCH_FIELD = {
50
59
  "dataModel.row.add": "dataModel",
51
60
  "repair.binding": "dataModel",
52
61
  "explain.object": "dataModel",
62
+ // Sentinel — resolver.create writes a server file, not a config field. It is
63
+ // explicitly excluded from the PATCH allowlist and handled by its own lane.
64
+ "resolver.create": "server-file",
65
+ // Swarm rows are dataModel rows. No new PATCH field is introduced.
66
+ "swarm.run.propose": "dataModel",
67
+ "swarm.workflow.save": "dataModel",
68
+ "swarm.run.resume": "dataModel",
53
69
  };
54
70
 
55
71
  const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
@@ -62,7 +78,7 @@ const INTENT_DESCRIPTIONS = {
62
78
  create_widget:
63
79
  "Suggest which widgetTypes belong on the workspace based on the object schema and target KPIs. Propose widgetType entries and canvas widget placements.",
64
80
  register_api:
65
- "Draft API Registry rows (dataModel object of objectType api-registry) including integration labels, credential prompts, base URL, endpoint, auth header, and method.",
81
+ "Draft API Registry rows (dataModel object of objectType api-registry) including integration labels, credential prompts, base URL, endpoint, auth header, and method. When the user wants the response shaped into governed rows (a Data Source) or describes a nested/paginated response, also recommend a resolver in the `summary` and propose the matching data-source object (objectType data-source) that references the registry row by registryId — guiding them into the resolver / Data Source lane. Credentials are runtime env keys (REF / REF_API_KEY / REF_TOKEN in .env.local), never stored in config.",
66
82
  create_object:
67
83
  "Translate the user's domain language into a new dataModel object: objectType, label, columns, starter rows, and field settings that make sense for their business.",
68
84
  edit_view:
@@ -71,6 +87,8 @@ const INTENT_DESCRIPTIONS = {
71
87
  "Inspect the workspace snapshot for broken references, missing bindings, empty objects, or incomplete views. Propose the minimum changes needed to repair each issue.",
72
88
  explain:
73
89
  "Return a clear explanation of what one or more workspace objects, widgets, or configurations do. Use the explain.object proposal type — payload is { explanation: string }.",
90
+ swarm:
91
+ "Propose a governed agent swarm via swarm.run.propose. You are propose-only: do not execute, do not store credentials. Describe the swarm objective, agent roles, task prompts, tools, maxConcurrency, and outcomeCriteria as an intent payload — the server normalizes it into the governed agent-swarm-v1 graph and execution happens only through sandbox-run after the user applies the proposal.",
74
92
  };
75
93
 
76
94
  /**
@@ -85,6 +103,10 @@ const INTENT_DESCRIPTIONS = {
85
103
  * `create_object` when the prompt mentions both "object" and "API".
86
104
  */
87
105
  const INTENT_HEURISTIC_PATTERNS = [
106
+ { intent: "swarm", patterns: [
107
+ /\b(swarm|sub-?agents?|multi-?agent|agent\s+team|orchestrat(e|or|ion))\b/i,
108
+ /\b(run|launch|spawn|dispatch)\b.*\b(agents?|workers?)\b.*\b(parallel|swarm|workflow)\b/i,
109
+ ]},
88
110
  { intent: "register_api", patterns: [
89
111
  /\b(api|endpoint|webhook|integration|connector|oauth|bearer\s+token|auth\s+header)\b/i,
90
112
  /\b(register|connect|wire|hook\s*up)\b.*\b(api|endpoint|webhook|service|integration)\b/i,
@@ -196,6 +218,13 @@ function buildStableSystemPrompt(intent) {
196
218
  "- Reset invalid axis, filter, group, and sort settings when source changes.",
197
219
  "- Mark recomputed values as unsaved unless PATCH succeeds.",
198
220
  "",
221
+ "## When proposing agent swarms (swarm.run.propose)",
222
+ "- You may propose swarm.run.propose. You are propose-only. Do not execute. Do not store credentials.",
223
+ "- Describe the swarm objective, agent roles, tasks, tools, maxConcurrency, and outcomeCriteria.",
224
+ "- The server will normalize this into the governed agent-swarm graph; execution happens only through sandbox-run after apply.",
225
+ "- payload shape: { name, description?, objective, agents: [{ id?, role, description?, taskPrompt, tools?, required?, maxTokens?, timeoutMs? }], maxConcurrency?, outcomeCriteria?, runLocality?, agentHost?, adapter? }",
226
+ "- adapter, when set, must be local-agent-host or local-intelligence.",
227
+ "",
199
228
  "## Valid proposal types and their target patch field",
200
229
  WORKSPACE_HELPER_PROPOSAL_TYPES.map(
201
230
  (t) => ` ${t} → ${PROPOSAL_TYPE_TO_PATCH_FIELD[t]}`
@@ -545,8 +545,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
545
545
  const graphEdges = Array.isArray(graph?.edges) ? graph.edges : [];
546
546
  const workflowMetadataId = stableId("workflow", objectId, rowName);
547
547
  const sandboxMetadataId = stableId("sandbox", objectId, rowName);
548
- const agentHost = safeString(row.agentHost).trim();
549
- const adapter = safeString(row.adapter).trim();
548
+ const rowAgentHost = safeString(row.agentHost).trim();
549
+ const rowAdapter = safeString(row.adapter).trim();
550
550
  const inputSchema = graphNodes.length ? discoverRunInputSchema(graph) : { requiresInput: false, fields: [] };
551
551
  const inputFields = Array.isArray(inputSchema?.fields) ? inputSchema.fields : [];
552
552
 
@@ -560,8 +560,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
560
560
  lifecycleStatus: safeString(row.lifecycleStatus).trim() || "draft",
561
561
  version: safeString(row.version).trim() || "1",
562
562
  sandboxMetadataId,
563
- agentHost,
564
- adapter,
563
+ agentHost: rowAgentHost,
564
+ adapter: rowAdapter,
565
565
  runLocality: safeString(row.runLocality).trim(),
566
566
  nodeCount: graphNodes.length,
567
567
  edgeCount: graphEdges.length,
@@ -578,6 +578,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
578
578
  const sourceType = safeString(config.sourceType).trim();
579
579
  const sourceId = safeString(config.sourceId).trim();
580
580
  const integrationId = safeString(config.integrationId).trim();
581
+ const nodeAgentHost = safeString(config.agentHost || rowAgentHost).trim();
582
+ const nodeAdapter = safeString(config.adapter || rowAdapter).trim();
581
583
  const filterClauses = collectFilterClauses({ op: config.filterMode, clauses: config.filters });
582
584
  const writesObjectId = safeString(config.writeObjectId || config.targetObjectId).trim();
583
585
  const readsObjectId = sourceId || safeString(config.objectId).trim();
@@ -603,8 +605,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
603
605
  inputFieldCount: inputs.length,
604
606
  inputFieldIds: inputs.map((field) => field.id),
605
607
  sandboxMetadataId,
606
- agentHost,
607
- adapter,
608
+ agentHost: nodeAgentHost,
609
+ adapter: nodeAdapter,
608
610
  permissions: nodeType === "api-registry-call" ? ["integration:read"] : []
609
611
  });
610
612
  }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Workspace Resolver Proposal V1 — the governed resolver-file lane for the
3
+ * helper, aligned to AWaC topology + the Causation-ITT loop.
4
+ *
5
+ * AWaC boundary: a resolver is a SERVER FILE under
6
+ * lib/adapters/integrations/resolvers/, NOT workspace config. It therefore must
7
+ * NOT travel through the PATCH allowlist (dashboards|widgetTypes|canvas|
8
+ * dataModel). It is its own proposal lane with affectedField "server-file",
9
+ * gated by the same filesystem/read-only persistence rule, and it emits a
10
+ * receipt — so "state → eligibility → guidance → action → evidence" closes.
11
+ *
12
+ * This module is PURE (build/validate/generate). The actual confined fs write
13
+ * lives in lib/server-resolver-write.js (server-only). Secret-safe: the
14
+ * generated resolver reads its secret from the server env at run time via the
15
+ * authRef candidate keys — never an inlined value.
16
+ */
17
+
18
+ const RESOLVER_PROPOSAL_TYPE = "resolver.create";
19
+ const RESOLVER_AFFECTED_FIELD = "server-file";
20
+ const RESOLVER_DIR = "lib/adapters/integrations/resolvers";
21
+
22
+ function clean(value) {
23
+ return String(value == null ? "" : value).trim();
24
+ }
25
+
26
+ function slugify(value, fallback = "resolver") {
27
+ const slug = clean(value)
28
+ .toLowerCase()
29
+ .replace(/\.js$/, "")
30
+ .replace(/[^a-z0-9-]/g, "-")
31
+ .replace(/-+/g, "-")
32
+ .replace(/^-|-$/g, "")
33
+ .slice(0, 64);
34
+ return slug || fallback;
35
+ }
36
+
37
+ function envCandidates(ref) {
38
+ const token = clean(ref).replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
39
+ if (!token) return [];
40
+ return Array.from(new Set([token, `${token}_API_KEY`, `${token}_TOKEN`]));
41
+ }
42
+
43
+ /**
44
+ * Confine a resolver filename to the resolver dir. Mirrors the register-resolver
45
+ * route's traversal guard so the studio previews exactly what the write accepts.
46
+ */
47
+ function resolveResolverFilePath(name) {
48
+ const base = slugify(name, "resolver");
49
+ if (base.includes("/") || base.includes("..")) {
50
+ return { ok: false, path: null, filename: null, dir: RESOLVER_DIR, error: "resolver filename must not contain path segments" };
51
+ }
52
+ const filename = `${base}.js`;
53
+ return { ok: true, path: `${RESOLVER_DIR}/${filename}`, filename, dir: RESOLVER_DIR, error: null };
54
+ }
55
+
56
+ function safeJsString(value) {
57
+ return JSON.stringify(clean(value));
58
+ }
59
+
60
+ /**
61
+ * Generate a concrete, runnable resolver module for a specific API. baseUrl /
62
+ * endpoint / rootPath / idField are baked in from the registry row + the
63
+ * response profile, so the file works for THIS API. The secret is read from the
64
+ * server env via the authRef candidate keys at run time — never inlined.
65
+ */
66
+ function generateResolverCode({ integrationId, baseUrl, endpoint, method, authRef, headerName, prefix, rootPath, idField, entityType }) {
67
+ const id = slugify(integrationId, "integration");
68
+ const url = `${clean(baseUrl).replace(/\/+$/, "")}/${clean(endpoint).replace(/^\/+/, "")}`.replace(/\/$/, "") || clean(baseUrl) || clean(endpoint);
69
+ const m = (clean(method).toUpperCase() || "GET");
70
+ const candidates = envCandidates(authRef);
71
+ const header = clean(headerName) || "authorization";
72
+ const pfx = clean(prefix);
73
+ const root = clean(rootPath);
74
+ const idf = clean(idField) || "id";
75
+ const ent = clean(entityType) || "records";
76
+
77
+ return `// Resolver for "${id}" — generated by the governed helper resolver studio.
78
+ // Server file. Reads its secret from the server env at run time (candidates:
79
+ // ${candidates.join(", ") || "none"}); never hard-code a secret here.
80
+ import { registerSourceResolver } from "../source-resolver-registry.js";
81
+
82
+ const ENV_CANDIDATES = ${JSON.stringify(candidates)};
83
+ function readSecret() {
84
+ for (const key of ENV_CANDIDATES) {
85
+ if (process.env[key]) return process.env[key];
86
+ }
87
+ return "";
88
+ }
89
+ function valueAtPath(obj, path) {
90
+ if (!path) return obj;
91
+ return String(path).split(".").filter(Boolean).reduce((acc, part) => (acc == null ? acc : acc[part]), obj);
92
+ }
93
+
94
+ registerSourceResolver({
95
+ integrationId: ${safeJsString(id)},
96
+ label: ${safeJsString(`${id} (${ent})`)},
97
+ // fetchRecords runs server-side. Secret stays in the server env.
98
+ fetchRecords: async () => {
99
+ const secret = readSecret();
100
+ const res = await fetch(${safeJsString(url)}, {
101
+ method: ${safeJsString(m)},
102
+ headers: {
103
+ accept: "application/json",
104
+ ...(secret ? { ${safeJsString(header)}: ${pfx ? `\`${pfx} \${secret}\`` : "secret"} } : {}),
105
+ },
106
+ });
107
+ const raw = res.headers.get("content-type")?.includes("application/json") ? await res.json() : await res.text();
108
+ const extracted = valueAtPath(raw, ${safeJsString(root)});
109
+ const items = Array.isArray(extracted) ? extracted : (Array.isArray(raw) ? raw : []);
110
+ return items.map((item, index) => ({
111
+ id: item && item[${safeJsString(idf)}] != null ? String(item[${safeJsString(idf)}]) : String(index),
112
+ ...item,
113
+ }));
114
+ },
115
+ });
116
+ `;
117
+ }
118
+
119
+ /**
120
+ * Build an inert resolver.create proposal. Carries the target path, the
121
+ * generated code, security notes, and the resolver's input/output contract.
122
+ * Nothing is written until the apply lane writes the file (gated).
123
+ */
124
+ function buildResolverProposal(input = {}) {
125
+ const integrationId = slugify(input.integrationId, "integration");
126
+ const target = resolveResolverFilePath(integrationId);
127
+ const code = generateResolverCode({
128
+ integrationId,
129
+ baseUrl: input.baseUrl,
130
+ endpoint: input.endpoint,
131
+ method: input.method,
132
+ authRef: input.authRef,
133
+ headerName: input.headerName,
134
+ prefix: input.prefix,
135
+ rootPath: input.rootPath,
136
+ idField: input.idField,
137
+ entityType: input.entityType,
138
+ });
139
+ return {
140
+ type: RESOLVER_PROPOSAL_TYPE,
141
+ affectedField: RESOLVER_AFFECTED_FIELD,
142
+ rationale: `Generate a server resolver that shapes ${integrationId}'s response${clean(input.rootPath) ? ` at "${clean(input.rootPath)}"` : ""} into governed rows.`,
143
+ confidence: 0.85,
144
+ target,
145
+ language: "javascript",
146
+ code,
147
+ security: [
148
+ "Server file — only written in filesystem mode (read-only runtimes get a 409 + guidance).",
149
+ "Reads its secret from the server env at run time; never inlines a value.",
150
+ `Path confined to ${RESOLVER_DIR}.`,
151
+ ],
152
+ contract: {
153
+ inputs: ["registry baseUrl/endpoint", `authRef env (${envCandidates(input.authRef).join(", ") || "none"})`],
154
+ output: `Record[] with a stable id (from "${clean(input.idField) || "id"}").`,
155
+ },
156
+ payload: {
157
+ integrationId,
158
+ rootPath: clean(input.rootPath),
159
+ entityType: clean(input.entityType) || "records",
160
+ filename: target.filename,
161
+ },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Validate a resolver.create proposal before the apply lane writes it.
167
+ * Returns { ok, error }.
168
+ */
169
+ function validateResolverProposal(proposal) {
170
+ if (!proposal || proposal.type !== RESOLVER_PROPOSAL_TYPE) {
171
+ return { ok: false, error: "not a resolver.create proposal" };
172
+ }
173
+ if (proposal.affectedField !== RESOLVER_AFFECTED_FIELD) {
174
+ return { ok: false, error: `resolver.create affectedField must be "${RESOLVER_AFFECTED_FIELD}"` };
175
+ }
176
+ const target = proposal.target || resolveResolverFilePath(proposal.payload?.integrationId);
177
+ if (!target || !target.ok) {
178
+ return { ok: false, error: target?.error || "invalid resolver target path" };
179
+ }
180
+ const code = clean(proposal.code);
181
+ if (!code) return { ok: false, error: "resolver code is empty" };
182
+ if (!code.includes("registerSourceResolver")) {
183
+ return { ok: false, error: "resolver code must call registerSourceResolver()" };
184
+ }
185
+ if (!target.filename.endsWith(".js")) {
186
+ return { ok: false, error: "resolver file must have a .js extension" };
187
+ }
188
+ return { ok: true, error: null, target };
189
+ }
190
+
191
+ export {
192
+ RESOLVER_PROPOSAL_TYPE,
193
+ RESOLVER_AFFECTED_FIELD,
194
+ RESOLVER_DIR,
195
+ resolveResolverFilePath,
196
+ generateResolverCode,
197
+ buildResolverProposal,
198
+ validateResolverProposal,
199
+ envCandidates,
200
+ };