@growthub/cli 0.14.1 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +4 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/agent-outcomes/route.js +85 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +187 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +36 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +152 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +21 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +88 -1
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +86 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +2 -2
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +21 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +338 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +49 -2
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +54 -11
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +113 -36
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +7 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +35 -169
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +85 -7
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +8 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-publish.js +179 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +1 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +89 -5
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-registry.js +539 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +11 -2
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +24 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-outcome-receipts.js +157 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +400 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +3 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +10 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +203 -0
  49. package/package.json +2 -2
@@ -36,27 +36,95 @@ import { registerSandboxAdapter } from "./sandbox-adapter-registry.js";
36
36
  const MAX_OUTPUT_BYTES = 1024 * 256;
37
37
  const TELEMETRY_MARKER = "GROWTHUB_AGENT_TELEMETRY:";
38
38
 
39
+ /**
40
+ * Browser access — the product's OWN agent browser primitive, surfaced.
41
+ *
42
+ * The upstream Paperclip server already gives every agent browser access
43
+ * through one boolean: the agent config's `chrome` primitive (see
44
+ * `ui/src/components/agent-config-primitives.tsx` — "Enable Claude's Chrome
45
+ * integration by passing --chrome") gated by the chrome-lease service in
46
+ * `server/src/services/chrome-lease.ts` before `adapter.execute()`. The
47
+ * sandbox row's `browserAccess` is the SAME bit on the governed Data Model
48
+ * side, so a row stays portable to the upstream adapter registry without
49
+ * translation — exactly like the host slugs themselves.
50
+ *
51
+ * When `browserAccess` is on, each host engages its FIRST-PARTY browser
52
+ * integration — nothing is invented or injected by this adapter:
53
+ *
54
+ * native-flag the host CLI has a first-party browser flag and
55
+ * argv(request) appends it (Claude Code `--chrome`,
56
+ * Codex `--enable browser_use --enable in_app_browser`).
57
+ * env-signal the host CLI has no documented one-shot browser flag; it
58
+ * receives GROWTHUB_SANDBOX_BROWSER_ACCESS=1 (mirroring the
59
+ * upstream browser-isolation context) and its own browser
60
+ * integration — whatever the operator has configured in that
61
+ * host — honors the row's setting. We never fabricate a flag
62
+ * or write host config we cannot verify against the upstream
63
+ * tool, the same rule the auth catalog follows for login
64
+ * subcommands.
65
+ *
66
+ * In the orchestration graph this is what makes browser access node-level
67
+ * and host-agnostic: thinAdapter / ai-agent nodes execute through this same
68
+ * catalog, so every node inherits the row's browser grant regardless of
69
+ * which host runs it.
70
+ */
71
+
39
72
  /**
40
73
  * Canonical Paperclip host catalog — slugs mirror `AGENT_ADAPTER_TYPES`.
41
74
  *
42
75
  * Each entry declares the binary the operator must have on PATH and how to
43
- * invoke it for one-shot prompt execution. `argv` returns the argv array the
44
- * adapter should pass; `inputMode` chooses whether the user's command is sent
45
- * via stdin or as a positional argument. `installHint` is surfaced verbatim
46
- * when the binary is not found, so operators get an actionable error.
76
+ * invoke it for one-shot prompt execution. `argv(request)` receives the sealed
77
+ * RunRequest and returns the argv array the adapter should pass — hosts with
78
+ * native capability flags (network sandbox mode, browser access) derive them
79
+ * deterministically from the governed row's saved settings. `inputMode`
80
+ * chooses whether the user's command is sent via stdin or as a positional
81
+ * argument. `installHint` is surfaced verbatim when the binary is not found,
82
+ * so operators get an actionable error. `browser` declares how the host's
83
+ * first-party browser integration is engaged (see above).
47
84
  */
48
85
  const HOST_CATALOG = {
49
86
  claude_local: {
50
87
  label: "Claude Code (local)",
51
88
  binary: "claude",
52
- argv: () => ["-p", "--output-format", "text"],
89
+ argv: (request = {}) => {
90
+ const args = ["-p", "--output-format", "text"];
91
+ if (request.browserAccess) {
92
+ // Claude Code's first-party Chrome integration — the same flag the
93
+ // upstream server adapter passes when the agent config's `chrome`
94
+ // primitive is on.
95
+ args.push("--chrome");
96
+ }
97
+ return args;
98
+ },
99
+ browser: { lane: "native-flag", flags: ["--chrome"] },
53
100
  inputMode: "stdin",
54
101
  installHint: "Install Claude Code: npm i -g @anthropic-ai/claude-code"
55
102
  },
56
103
  codex_local: {
57
104
  label: "Codex CLI (local)",
58
105
  binary: "codex",
59
- argv: () => ["exec", "--skip-git-repo-check", "--sandbox", "read-only", "-"],
106
+ argv: (request = {}) => {
107
+ // INTENTIONAL: networkAllow alone selects `workspace-write`. Codex's
108
+ // `read-only` sandbox blocks ALL outbound network, so workspace-write is
109
+ // the least-privileged Codex mode where the row's network grant can take
110
+ // effect — and writes are confined to the sealed ephemeral workdir the
111
+ // adapter spawns into (cwd), never the operator's repo. Browser flags
112
+ // remain gated on browserAccess only; network alone never opens a browser.
113
+ const netOn = Boolean(request.networkAllow);
114
+ const browserOn = Boolean(request.browserAccess);
115
+ const args = [
116
+ "exec",
117
+ "--skip-git-repo-check",
118
+ "--sandbox",
119
+ netOn ? "workspace-write" : "read-only",
120
+ ];
121
+ if (browserOn) {
122
+ args.push("--enable", "browser_use", "--enable", "in_app_browser");
123
+ }
124
+ args.push("-");
125
+ return args;
126
+ },
127
+ browser: { lane: "native-flag", flags: ["--enable", "browser_use", "--enable", "in_app_browser"] },
60
128
  inputMode: "stdin",
61
129
  installHint: "Install Codex CLI: npm i -g @openai/codex"
62
130
  },
@@ -64,6 +132,7 @@ const HOST_CATALOG = {
64
132
  label: "Cursor Agent (local)",
65
133
  binary: "cursor-agent",
66
134
  argv: () => ["--print"],
135
+ browser: { lane: "env-signal" },
67
136
  inputMode: "stdin",
68
137
  installHint: "Install Cursor Agent CLI: curl https://cursor.com/install -fsS | bash"
69
138
  },
@@ -71,6 +140,7 @@ const HOST_CATALOG = {
71
140
  label: "Gemini CLI (local)",
72
141
  binary: "gemini",
73
142
  argv: () => ["-p", "-"],
143
+ browser: { lane: "env-signal" },
74
144
  inputMode: "stdin",
75
145
  installHint: "Install Gemini CLI: npm i -g @google/gemini-cli"
76
146
  },
@@ -78,6 +148,7 @@ const HOST_CATALOG = {
78
148
  label: "OpenCode (local)",
79
149
  binary: "opencode",
80
150
  argv: () => ["run", "--quiet"],
151
+ browser: { lane: "env-signal" },
81
152
  inputMode: "stdin",
82
153
  installHint: "Install OpenCode: npm i -g opencode-ai"
83
154
  },
@@ -85,6 +156,7 @@ const HOST_CATALOG = {
85
156
  label: "Pi (local)",
86
157
  binary: "pi",
87
158
  argv: () => ["run", "--stdin"],
159
+ browser: { lane: "env-signal" },
88
160
  inputMode: "stdin",
89
161
  installHint: "Install Pi CLI: refer to your Paperclip Pi distribution"
90
162
  },
@@ -92,6 +164,7 @@ const HOST_CATALOG = {
92
164
  label: "Qwen Code (local)",
93
165
  binary: "qwen",
94
166
  argv: () => ["-p"],
167
+ browser: { lane: "env-signal" },
95
168
  inputMode: "stdin",
96
169
  installHint: "Install Qwen Code CLI: refer to your Qwen distribution"
97
170
  },
@@ -99,6 +172,7 @@ const HOST_CATALOG = {
99
172
  label: "Hermes Paperclip (local)",
100
173
  binary: "hermes",
101
174
  argv: () => ["run", "--stdin"],
175
+ browser: { lane: "env-signal" },
102
176
  inputMode: "stdin",
103
177
  installHint: "Install Hermes Paperclip adapter: npm i -g hermes-paperclip-adapter"
104
178
  },
@@ -106,6 +180,7 @@ const HOST_CATALOG = {
106
180
  label: "OpenClaw Gateway (local)",
107
181
  binary: "openclaw",
108
182
  argv: () => ["gateway", "exec", "--stdin"],
183
+ browser: { lane: "env-signal" },
109
184
  inputMode: "stdin",
110
185
  installHint: "Install OpenClaw Gateway: refer to your Paperclip distribution"
111
186
  }
@@ -292,12 +367,13 @@ async function run(request) {
292
367
  GROWTHUB_SANDBOX_AGENT_HOST: hostSlug,
293
368
  GROWTHUB_SANDBOX_NET_ALLOW: request.networkAllow ? "1" : "0",
294
369
  GROWTHUB_SANDBOX_NET_ALLOWLIST: Array.isArray(request.allowList) ? request.allowList.join(",") : "",
370
+ GROWTHUB_SANDBOX_BROWSER_ACCESS: request.browserAccess ? "1" : "0",
295
371
  ...(request.env || {})
296
372
  };
297
373
 
298
374
  const timeoutMs = Number.isFinite(request.timeoutMs) && request.timeoutMs > 0 ? request.timeoutMs : 60000;
299
375
  const startedAt = Date.now();
300
- const argv = host.argv(command);
376
+ const argv = host.argv(request);
301
377
 
302
378
  return await new Promise((resolve) => {
303
379
  let stdout = Buffer.alloc(0);
@@ -385,6 +461,8 @@ async function run(request) {
385
461
  binary: host.binary,
386
462
  argv,
387
463
  inputMode: host.inputMode,
464
+ browserAccess: Boolean(request.browserAccess),
465
+ browserLane: request.browserAccess ? host.browser.lane : null,
388
466
  timedOut,
389
467
  signal: signal || null,
390
468
  tokens: telemetry.tokens,
@@ -108,6 +108,7 @@ async function run(request) {
108
108
  GROWTHUB_SANDBOX_RUN_ID: request.runId || "",
109
109
  GROWTHUB_SANDBOX_NET_ALLOW: request.networkAllow ? "1" : "0",
110
110
  GROWTHUB_SANDBOX_NET_ALLOWLIST: Array.isArray(request.allowList) ? request.allowList.join(",") : "",
111
+ GROWTHUB_SANDBOX_BROWSER_ACCESS: request.browserAccess ? "1" : "0",
111
112
  ...(request.env || {})
112
113
  };
113
114
 
@@ -177,7 +178,8 @@ async function run(request) {
177
178
  timedOut,
178
179
  signal: signal || null,
179
180
  networkAllow: Boolean(request.networkAllow),
180
- allowList: Array.isArray(request.allowList) ? request.allowList : []
181
+ allowList: Array.isArray(request.allowList) ? request.allowList : [],
182
+ browserAccess: Boolean(request.browserAccess)
181
183
  }
182
184
  });
183
185
  });
@@ -10,6 +10,7 @@
10
10
  import "./default-local-process.js";
11
11
  import "./default-local-agent-host.js";
12
12
  import "./default-local-intelligence.js";
13
+ import "./adapters/local-intelligence-browser-access.js";
13
14
  import { loadAllSandboxAdapters } from "./adapter-loader.js";
14
15
 
15
16
  let baseLoaded = true; // default-local-process registered via static import
@@ -27,6 +27,9 @@
27
27
  * timeoutMs: number, // hard cap, capped at SANDBOX_MAX_TIMEOUT_MS
28
28
  * networkAllow: boolean, // allow outbound network from inside the sandbox
29
29
  * allowList: string[], // hostnames the user explicitly allowed
30
+ * browserAccess: boolean, // row-level browser capability (implies networkAllow);
31
+ * // adapters surface it natively where the target supports
32
+ * // it and always publish GROWTHUB_SANDBOX_BROWSER_ACCESS
30
33
  * env: Record<string,string>, // server-resolved env (NEVER sent to browser)
31
34
  * envRefSlugs: string[], // ref slugs resolved (kept for record metadata)
32
35
  * envRefsMissing: string[], // slugs the server could not resolve (audit-only)
@@ -99,7 +102,8 @@ function describeRegisteredSandboxAdapters() {
99
102
  slug,
100
103
  label: host?.label || slug,
101
104
  binary: host?.binary || null,
102
- installHint: host?.installHint || null
105
+ installHint: host?.installHint || null,
106
+ browserLane: host?.browser?.lane || "env-signal"
103
107
  }))
104
108
  : null
105
109
  }));
@@ -11,6 +11,7 @@ const SANDBOX_ENVIRONMENT_FIELDS = {
11
11
  options: ["local", "serverless"]
12
12
  },
13
13
  networkAllow: { editor: "boolean-toggle" },
14
+ browserAccess: { editor: "boolean-toggle" },
14
15
  lifecycleStatus: {
15
16
  editor: "select",
16
17
  options: ["draft", "live"]
@@ -288,6 +288,7 @@ async function runThroughAdapter({
288
288
  timeoutMs,
289
289
  networkAllow,
290
290
  allowList,
291
+ browserAccess,
291
292
  env,
292
293
  envRefSlugs,
293
294
  envRefsMissing,
@@ -331,6 +332,7 @@ async function runThroughAdapter({
331
332
  timeoutMs,
332
333
  networkAllow,
333
334
  allowList,
335
+ browserAccess: browserAccess === true,
334
336
  env,
335
337
  envRefSlugs,
336
338
  envRefsMissing,
@@ -400,6 +402,7 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
400
402
  timeoutMs: clampPositiveInt(orchestratorNode?.config?.timeoutMs, DEFAULT_ORCHESTRATOR_TIMEOUT_MS),
401
403
  networkAllow: executionContext.networkAllow === true,
402
404
  allowList: executionContext.allowList || [],
405
+ browserAccess: executionContext.browserAccess === true,
403
406
  env,
404
407
  envRefSlugs: executionContext.envRefSlugs || [],
405
408
  envRefsMissing: executionContext.envRefsMissing || [],
@@ -488,6 +491,9 @@ async function dispatchSubagentTask({
488
491
  command,
489
492
  timeoutMs: clampPositiveInt(subagentConfig.timeoutMs, executionContext.timeoutMs || DEFAULT_SUBAGENT_TIMEOUT_MS),
490
493
  networkAllow: subagentConfig.networkAccess === true && executionContext.networkAllow === true,
494
+ // Browser is a superset of network: a subagent only inherits the row's
495
+ // browser access through the same node-level network gate.
496
+ browserAccess: subagentConfig.networkAccess === true && executionContext.browserAccess === true,
491
497
  allowList: executionContext.allowList || [],
492
498
  env,
493
499
  envRefSlugs: executionContext.envRefSlugs || [],
@@ -568,6 +574,7 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
568
574
  timeoutMs: clampPositiveInt(cfg.timeoutMs, DEFAULT_SYNTHESIS_TIMEOUT_MS),
569
575
  networkAllow: executionContext.networkAllow === true,
570
576
  allowList: executionContext.allowList || [],
577
+ browserAccess: executionContext.browserAccess === true,
571
578
  env,
572
579
  envRefSlugs: executionContext.envRefSlugs || [],
573
580
  envRefsMissing: executionContext.envRefsMissing || [],
@@ -822,6 +829,7 @@ async function runAgentSwarmGraphIfPresent({
822
829
  envRefsMissing: executionContext?.envRefsMissing || [],
823
830
  networkAllow: executionContext?.networkAllow === true,
824
831
  allowList: executionContext?.allowList || [],
832
+ browserAccess: executionContext?.browserAccess === true,
825
833
  timeoutMs: clampPositiveInt(timeoutMs, DEFAULT_SUBAGENT_TIMEOUT_MS),
826
834
  sandboxName: executionContext?.sandboxName || row?.Name || "swarm",
827
835
  onEvent: executionContext?.onEvent,
@@ -281,6 +281,9 @@ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs,
281
281
 
282
282
  const apiNode = extractApiRegistryCallNode(graph);
283
283
  if (!apiNode?.config) {
284
+ if ((Array.isArray(graph.nodes) ? graph.nodes : []).some((node) => node?.type === "ai-agent")) {
285
+ return null;
286
+ }
284
287
  return {
285
288
  ok: false,
286
289
  exitCode: 1,
@@ -157,9 +157,10 @@ function validateOrchestrationGraph(graph) {
157
157
  } else {
158
158
  const hasThinAdapter = graph.nodes.some((n) => n?.type === "thinAdapter");
159
159
  const hasApi = graph.nodes.some((n) => n?.type === "api-registry-call");
160
+ const hasAiAgent = graph.nodes.some((n) => n?.type === "ai-agent");
160
161
  const hasResult = graph.nodes.some((n) => n?.type === "tool-result");
161
- if (!hasThinAdapter && !hasApi) errors.push("orchestrationGraph requires an api-registry-call node");
162
- if (!hasThinAdapter && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
162
+ if (!hasThinAdapter && !hasApi && !hasAiAgent) errors.push("orchestrationGraph requires an executable node");
163
+ if (!hasThinAdapter && !hasAiAgent && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
163
164
  }
164
165
  }
165
166
  if (!Array.isArray(graph.edges)) {
@@ -712,6 +713,7 @@ function buildCanonicalNode(nodeId, registryRow = {}, options = {}) {
712
713
  function getNextCanonicalNodeId(graph) {
713
714
  const parsed = parseOrchestrationGraph(graph) || graph;
714
715
  if ((parsed?.nodes || []).some((n) => n?.type === "thinAdapter")) return null;
716
+ if ((parsed?.nodes || []).some((n) => n?.type === "ai-agent")) return null;
715
717
  const ids = new Set((parsed?.nodes || []).map((n) => String(n.id)));
716
718
  for (const id of CANONICAL_NODE_ORDER) {
717
719
  if (!ids.has(id)) return id;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Orchestration publish helpers — shared by the Workflows surface (client)
3
+ * and POST /api/workspace/workflow/publish (server-authoritative publish).
4
+ *
5
+ * These were previously private functions inside app/workflows/WorkflowSurface.jsx.
6
+ * They are extracted so the *server* owns publish computation (version bump,
7
+ * delta records, draft → live promotion) and the client only renders state.
8
+ * Pure functions; the only dependency is the orchestration-graph parser.
9
+ */
10
+
11
+ import { parseOrchestrationGraph } from "@/lib/orchestration-graph";
12
+
13
+ function nodeSandboxRecordRef(objectId, rowName, nodeId) {
14
+ return {
15
+ objectId: String(objectId || "").trim(),
16
+ rowName: String(rowName || "").trim(),
17
+ nodeId: String(nodeId || "").trim()
18
+ };
19
+ }
20
+
21
+ function withGraphSandboxRecordRefs(graph, objectId, rowName) {
22
+ const parsed = parseOrchestrationGraph(graph) || graph;
23
+ if (!parsed || typeof parsed !== "object") return parsed;
24
+ return {
25
+ ...parsed,
26
+ nodes: (Array.isArray(parsed.nodes) ? parsed.nodes : []).map((node) => ({
27
+ ...node,
28
+ config: {
29
+ ...(node?.config || {}),
30
+ sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, node?.id)
31
+ }
32
+ }))
33
+ };
34
+ }
35
+
36
+ function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
37
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
38
+ return {
39
+ ...workspaceConfig,
40
+ dataModel: {
41
+ ...workspaceConfig.dataModel,
42
+ objects: objects.map((object) => {
43
+ if (object?.id !== objectId) return object;
44
+ const rows = Array.isArray(object.rows) ? object.rows : [];
45
+ return {
46
+ ...object,
47
+ rows: rows.map((row, index) => (index === rowIndex ? { ...row, ...fields } : row)),
48
+ };
49
+ }),
50
+ },
51
+ };
52
+ }
53
+
54
+ function normalizeDeltaTags(tags) {
55
+ return Array.from(new Set((Array.isArray(tags) ? tags : [])
56
+ .map((tag) => String(tag || "").trim().toLowerCase())
57
+ .filter(Boolean)));
58
+ }
59
+
60
+ function inferDeltaTagsForWorkflowNode(node, config) {
61
+ const tags = [];
62
+ const type = String(node?.type || "").trim();
63
+ const action = String(config?.action || node?.id || "").trim();
64
+ if (type === "thinAdapter") tags.push("model", "prompt", "routing");
65
+ if (type === "ai-agent") tags.push("model", "prompt", "output");
66
+ if (type === "data-action" || type === "data-trigger") tags.push("input", "output");
67
+ if (type === "flow-control") tags.push("routing");
68
+ if (type === "core-action") tags.push("runtime");
69
+ if (type === "human-input") tags.push("input");
70
+ if (action.includes("search") || action.includes("filter")) tags.push("evaluation", "guardrail");
71
+ if (action.includes("delete") || config?.confirmationRequired) tags.push("guardrail");
72
+ if (action.includes("http") || config?.url || config?.method) tags.push("routing", "input", "output");
73
+ if (action.includes("email")) tags.push("input", "output");
74
+ if (action.includes("delay") || config?.duration || config?.unit) tags.push("runtime");
75
+ if (config?.objectId || config?.fieldMap || config?.filters) tags.push("input", "output");
76
+ if (config?.model || config?.prompt) tags.push("model", "prompt");
77
+ return normalizeDeltaTags(tags);
78
+ }
79
+
80
+ function getNodeDeltaRecords(previousGraph, nextGraph) {
81
+ const previousNodes = new Map(
82
+ (Array.isArray(previousGraph?.nodes) ? previousGraph.nodes : [])
83
+ .map((node) => [String(node?.id || ""), node])
84
+ .filter(([id]) => id)
85
+ );
86
+
87
+ return (Array.isArray(nextGraph?.nodes) ? nextGraph.nodes : [])
88
+ .map((node) => {
89
+ const nodeId = String(node?.id || "").trim();
90
+ if (!nodeId) return null;
91
+ const previous = previousNodes.get(nodeId);
92
+ const config = node?.config && typeof node.config === "object" && !Array.isArray(node.config) ? node.config : {};
93
+ const previousConfig = previous?.config && typeof previous.config === "object" && !Array.isArray(previous.config)
94
+ ? previous.config
95
+ : {};
96
+ const currentComparable = JSON.stringify({
97
+ type: node?.type || "",
98
+ sandbox: node?.sandbox || "",
99
+ label: node?.label || "",
100
+ subtitle: node?.subtitle || "",
101
+ config
102
+ });
103
+ const previousComparable = JSON.stringify({
104
+ type: previous?.type || "",
105
+ sandbox: previous?.sandbox || "",
106
+ label: previous?.label || "",
107
+ subtitle: previous?.subtitle || "",
108
+ config: previousConfig
109
+ });
110
+ const explicitTags = normalizeDeltaTags(config.deltaTags);
111
+ const deltaTags = explicitTags.length > 0 ? explicitTags : inferDeltaTagsForWorkflowNode(node, config);
112
+ const changeReason = String(config.changeReason || "").trim();
113
+ const changed = currentComparable !== previousComparable;
114
+ if (!changed && !changeReason && deltaTags.length === 0) return null;
115
+ return {
116
+ nodeId,
117
+ nodeType: String(node?.type || ""),
118
+ label: String(node?.label || node?.sandbox || nodeId),
119
+ sandboxRecordRef: config.sandboxRecordRef || null,
120
+ changeReason,
121
+ deltaTags,
122
+ requiresRetest: config.requiresRetest !== false,
123
+ previous: previous ? {
124
+ type: String(previous.type || ""),
125
+ sandbox: String(previous.sandbox || ""),
126
+ label: String(previous.label || "")
127
+ } : null,
128
+ next: {
129
+ type: String(node.type || ""),
130
+ sandbox: String(node.sandbox || ""),
131
+ label: String(node.label || "")
132
+ }
133
+ };
134
+ })
135
+ .filter(Boolean);
136
+ }
137
+
138
+ /**
139
+ * Resolve which live field this row publishes into and which draft field
140
+ * feeds it.
141
+ *
142
+ * Precedence (matching the Workflows surface):
143
+ * 1. An explicit `requestedField` ("orchestrationConfig" | "orchestrationGraph")
144
+ * — the surface preserves the URL-selected field when no live graph
145
+ * exists yet, so the publish request may carry it.
146
+ * 2. Whichever live field is populated.
147
+ * 3. Whichever DRAFT field is populated — a row whose only state is
148
+ * `orchestrationDraftGraph` publishes into `orchestrationGraph`,
149
+ * never silently into `orchestrationConfig`.
150
+ * 4. Default `orchestrationConfig`.
151
+ */
152
+ function resolveWorkflowFieldNames(row, requestedField) {
153
+ const hasGraphValue = (value) => Boolean(String(value ?? "").trim());
154
+ const draftFor = (live) => (live === "orchestrationConfig" ? "orchestrationDraftConfig" : "orchestrationDraftGraph");
155
+ if (requestedField === "orchestrationConfig" || requestedField === "orchestrationGraph") {
156
+ return { liveField: requestedField, draftField: draftFor(requestedField) };
157
+ }
158
+ let liveField;
159
+ if (hasGraphValue(row?.orchestrationConfig)) {
160
+ liveField = "orchestrationConfig";
161
+ } else if (hasGraphValue(row?.orchestrationGraph)) {
162
+ liveField = "orchestrationGraph";
163
+ } else if (!hasGraphValue(row?.orchestrationDraftConfig) && hasGraphValue(row?.orchestrationDraftGraph)) {
164
+ liveField = "orchestrationGraph";
165
+ } else {
166
+ liveField = "orchestrationConfig";
167
+ }
168
+ return { liveField, draftField: draftFor(liveField) };
169
+ }
170
+
171
+ export {
172
+ getNodeDeltaRecords,
173
+ inferDeltaTagsForWorkflowNode,
174
+ nodeSandboxRecordRef,
175
+ normalizeDeltaTags,
176
+ patchSandboxRowInConfig,
177
+ resolveWorkflowFieldNames,
178
+ withGraphSandboxRecordRefs
179
+ };
@@ -685,6 +685,7 @@ function normalizeRunConsoleRecord(record) {
685
685
  envRefsMissing: Array.isArray(record.envRefsMissing) ? record.envRefsMissing.slice() : [],
686
686
  networkAllow: Boolean(record.networkAllow),
687
687
  allowList: Array.isArray(record.allowList) ? record.allowList.slice() : [],
688
+ browserAccess: Boolean(record.browserAccess),
688
689
  adapterMeta,
689
690
  templateTrace
690
691
  },