@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
@@ -44,6 +44,11 @@ import {
44
44
  validateOrchestrationGraph
45
45
  } from "@/lib/orchestration-graph";
46
46
  import { resolveConnectorAction } from "@/lib/orchestration-sidecar-routing";
47
+ import {
48
+ nodeSandboxRecordRef,
49
+ patchSandboxRowInConfig,
50
+ withGraphSandboxRecordRefs
51
+ } from "@/lib/orchestration-publish";
47
52
  import { OrchestrationGraphCanvas } from "../data-model/components/OrchestrationGraphCanvas.jsx";
48
53
  import { OrchestrationGraphEmptyCanvas } from "../data-model/components/OrchestrationGraphEmptyCanvas.jsx";
49
54
  import { OrchestrationNodeConfigPanel } from "../data-model/components/OrchestrationNodeConfigPanel.jsx";
@@ -117,47 +122,6 @@ function resolveRegistryRefForSandbox(workspaceConfig, sandboxRow) {
117
122
  return firstRegistryRow ? { object: firstRegistryObject, row: firstRegistryRow } : null;
118
123
  }
119
124
 
120
- function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
121
- const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
122
- return {
123
- ...workspaceConfig,
124
- dataModel: {
125
- ...workspaceConfig.dataModel,
126
- objects: objects.map((object) => {
127
- if (object?.id !== objectId) return object;
128
- const rows = Array.isArray(object.rows) ? object.rows : [];
129
- return {
130
- ...object,
131
- rows: rows.map((row, index) => (index === rowIndex ? { ...row, ...fields } : row)),
132
- };
133
- }),
134
- },
135
- };
136
- }
137
-
138
- function nodeSandboxRecordRef(objectId, rowName, nodeId) {
139
- return {
140
- objectId: String(objectId || "").trim(),
141
- rowName: String(rowName || "").trim(),
142
- nodeId: String(nodeId || "").trim()
143
- };
144
- }
145
-
146
- function withGraphSandboxRecordRefs(graph, objectId, rowName) {
147
- const parsed = parseOrchestrationGraph(graph) || graph;
148
- if (!parsed || typeof parsed !== "object") return parsed;
149
- return {
150
- ...parsed,
151
- nodes: (Array.isArray(parsed.nodes) ? parsed.nodes : []).map((node) => ({
152
- ...node,
153
- config: {
154
- ...(node?.config || {}),
155
- sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, node?.id)
156
- }
157
- }))
158
- };
159
- }
160
-
161
125
  const WORKFLOW_ACTION_GROUPS = [
162
126
  {
163
127
  label: "Data",
@@ -201,90 +165,6 @@ function getWorkspaceObjectOptions(workspaceConfig) {
201
165
  }));
202
166
  }
203
167
 
204
- function normalizeDeltaTags(tags) {
205
- return Array.from(new Set((Array.isArray(tags) ? tags : [])
206
- .map((tag) => String(tag || "").trim().toLowerCase())
207
- .filter(Boolean)));
208
- }
209
-
210
- function inferDeltaTagsForWorkflowNode(node, config) {
211
- const tags = [];
212
- const type = String(node?.type || "").trim();
213
- const action = String(config?.action || node?.id || "").trim();
214
- if (type === "thinAdapter") tags.push("model", "prompt", "routing");
215
- if (type === "ai-agent") tags.push("model", "prompt", "output");
216
- if (type === "data-action" || type === "data-trigger") tags.push("input", "output");
217
- if (type === "flow-control") tags.push("routing");
218
- if (type === "core-action") tags.push("runtime");
219
- if (type === "human-input") tags.push("input");
220
- if (action.includes("search") || action.includes("filter")) tags.push("evaluation", "guardrail");
221
- if (action.includes("delete") || config?.confirmationRequired) tags.push("guardrail");
222
- if (action.includes("http") || config?.url || config?.method) tags.push("routing", "input", "output");
223
- if (action.includes("email")) tags.push("input", "output");
224
- if (action.includes("delay") || config?.duration || config?.unit) tags.push("runtime");
225
- if (config?.objectId || config?.fieldMap || config?.filters) tags.push("input", "output");
226
- if (config?.model || config?.prompt) tags.push("model", "prompt");
227
- return normalizeDeltaTags(tags);
228
- }
229
-
230
- function getNodeDeltaRecords(previousGraph, nextGraph) {
231
- const previousNodes = new Map(
232
- (Array.isArray(previousGraph?.nodes) ? previousGraph.nodes : [])
233
- .map((node) => [String(node?.id || ""), node])
234
- .filter(([id]) => id)
235
- );
236
-
237
- return (Array.isArray(nextGraph?.nodes) ? nextGraph.nodes : [])
238
- .map((node) => {
239
- const nodeId = String(node?.id || "").trim();
240
- if (!nodeId) return null;
241
- const previous = previousNodes.get(nodeId);
242
- const config = node?.config && typeof node.config === "object" && !Array.isArray(node.config) ? node.config : {};
243
- const previousConfig = previous?.config && typeof previous.config === "object" && !Array.isArray(previous.config)
244
- ? previous.config
245
- : {};
246
- const currentComparable = JSON.stringify({
247
- type: node?.type || "",
248
- sandbox: node?.sandbox || "",
249
- label: node?.label || "",
250
- subtitle: node?.subtitle || "",
251
- config
252
- });
253
- const previousComparable = JSON.stringify({
254
- type: previous?.type || "",
255
- sandbox: previous?.sandbox || "",
256
- label: previous?.label || "",
257
- subtitle: previous?.subtitle || "",
258
- config: previousConfig
259
- });
260
- const explicitTags = normalizeDeltaTags(config.deltaTags);
261
- const deltaTags = explicitTags.length > 0 ? explicitTags : inferDeltaTagsForWorkflowNode(node, config);
262
- const changeReason = String(config.changeReason || "").trim();
263
- const changed = currentComparable !== previousComparable;
264
- if (!changed && !changeReason && deltaTags.length === 0) return null;
265
- return {
266
- nodeId,
267
- nodeType: String(node?.type || ""),
268
- label: String(node?.label || node?.sandbox || nodeId),
269
- sandboxRecordRef: config.sandboxRecordRef || null,
270
- changeReason,
271
- deltaTags,
272
- requiresRetest: config.requiresRetest !== false,
273
- previous: previous ? {
274
- type: String(previous.type || ""),
275
- sandbox: String(previous.sandbox || ""),
276
- label: String(previous.label || "")
277
- } : null,
278
- next: {
279
- type: String(node.type || ""),
280
- sandbox: String(node.sandbox || ""),
281
- label: String(node.label || "")
282
- }
283
- };
284
- })
285
- .filter(Boolean);
286
- }
287
-
288
168
  function makeWorkflowNode(action, workspaceConfig, graph) {
289
169
  const baseId = String(action.id || action.type || "step").replace(/[^a-zA-Z0-9_-]+/g, "-");
290
170
  const existingIds = new Set((Array.isArray(graph?.nodes) ? graph.nodes : []).map((node) => String(node.id)));
@@ -606,54 +486,31 @@ export default function WorkflowSurface() {
606
486
 
607
487
  async function publishGraph() {
608
488
  if (resolved.rowIndex < 0 || !objectId) return;
489
+ // Publish is server-authoritative: POST /api/workspace/workflow/publish
490
+ // verifies the saved draft + passing test against the persisted row and
491
+ // owns the version bump, delta record, and draft → live promotion.
492
+ // Direct PATCH of live workflow fields is rejected by the runtime policy.
609
493
  const serialized = serializeCurrentGraph();
610
- const draftPassed = sandboxRow?.orchestrationDraftTestPassed === true || String(sandboxRow?.orchestrationDraftTestPassed || "") === "true";
611
- const testedConfig = String(sandboxRow?.orchestrationDraftTestedConfig || "");
612
- if (!draftPassed || testedConfig !== serialized) {
613
- setSaveMessage("Publish blocked. Save and test this exact draft successfully before publishing.");
494
+ const savedDraft = String(sandboxRow?.[draftFieldName] || "");
495
+ if (dirty || serialized !== savedDraft) {
496
+ setSaveMessage("Publish blocked. Save this draft first — publish promotes the saved, tested draft.");
614
497
  return;
615
498
  }
616
499
  setPublishing(true);
617
500
  setSaveMessage("");
618
501
  try {
619
- const currentVersion = Number(sandboxRow?.version || 1);
620
- const nextVersion = Number.isFinite(currentVersion) ? String(currentVersion + 1) : "1";
621
- const previousDeltas = Array.isArray(sandboxRow?.orchestrationDeltas) ? sandboxRow.orchestrationDeltas : [];
622
- const previousPublishedGraph = parseOrchestrationGraph(sandboxRow?.[effectiveFieldName]);
623
- const graphWithRefs = withGraphSandboxRecordRefs(orchestrationGraph, objectId, rowId);
624
- const nodeDeltas = getNodeDeltaRecords(previousPublishedGraph, graphWithRefs);
625
- const deltaTags = normalizeDeltaTags(nodeDeltas.flatMap((delta) => delta.deltaTags));
626
- const changeReason = nodeDeltas.map((delta) => delta.changeReason).filter(Boolean).join("\n");
627
- const next = patchSandboxRowInConfig(workspaceConfig, objectId, resolved.rowIndex, {
628
- [effectiveFieldName]: serialized,
629
- [draftFieldName]: "",
630
- version: nextVersion,
631
- lifecycleStatus: "live",
632
- orchestrationDraftStatus: "published",
633
- orchestrationDraftTestPassed: false,
634
- orchestrationDraftTestedConfig: "",
635
- orchestrationPublishedAt: new Date().toISOString(),
636
- orchestrationDeltas: [
637
- ...previousDeltas,
638
- {
639
- at: new Date().toISOString(),
640
- version: nextVersion,
641
- field: effectiveFieldName,
642
- action: "publish",
643
- previousVersion: String(sandboxRow?.version || "1"),
644
- draftTestedAt: sandboxRow?.orchestrationDraftLastTested || "",
645
- draftRunId: sandboxRow?.orchestrationDraftLastRunId || "",
646
- changeReason,
647
- deltaTags,
648
- nodeDeltas,
649
- nodeCount: Array.isArray(orchestrationGraph?.nodes) ? orchestrationGraph.nodes.length : 0,
650
- edgeCount: Array.isArray(orchestrationGraph?.edges) ? orchestrationGraph.edges.length : 0
651
- }
652
- ]
502
+ const res = await fetch("/api/workspace/workflow/publish", {
503
+ method: "POST",
504
+ headers: { "content-type": "application/json" },
505
+ body: JSON.stringify({ objectId, name: rowId, field: effectiveFieldName }),
653
506
  });
654
- await persistWorkspace(next);
507
+ const payload = await res.json();
508
+ if (!res.ok || !payload.ok) {
509
+ throw new Error(payload.error || "Publish failed");
510
+ }
511
+ setWorkspaceConfig(payload.workspaceConfig || workspaceConfig);
655
512
  setDirty(false);
656
- setSaveMessage(`Published orchestration config v${nextVersion}.`);
513
+ setSaveMessage(`Published orchestration config v${payload.version}.`);
657
514
  } catch (err) {
658
515
  setSaveMessage(err.message || "Publish failed");
659
516
  } finally {
@@ -912,6 +769,7 @@ export default function WorkflowSurface() {
912
769
  })
913
770
  : null;
914
771
  const isServerlessWorkflow = Boolean(serverlessState?.isServerless);
772
+ const showServerlessUpgrade = String(sandboxRow?.adapter || "").trim() !== "local-intelligence";
915
773
 
916
774
  async function patchSandboxAndPersist(fields) {
917
775
  if (resolved.rowIndex < 0 || !objectId || !workspaceConfig) return;
@@ -1013,7 +871,7 @@ export default function WorkflowSurface() {
1013
871
  >
1014
872
  <ArrowUp size={13} />
1015
873
  </button>
1016
- {sandboxRow && (
874
+ {sandboxRow && showServerlessUpgrade && (
1017
875
  <button
1018
876
  type="button"
1019
877
  className={"dm-workflow-icon-btn dm-workflow-upgrade-btn" + (isServerlessWorkflow ? " is-serverless" : (upgradeState.showOnboarding ? " is-pulse" : ""))}
@@ -1051,7 +909,13 @@ export default function WorkflowSurface() {
1051
909
  <Power size={13} /> {publishing ? "Publishing" : "Publish"}
1052
910
  </button>
1053
911
  )}
1054
- <button type="button" className="dm-workflow-chip-btn" disabled={!sandboxRow} onClick={openTraceMode}>
912
+ <button
913
+ type="button"
914
+ className="dm-workflow-chip-btn"
915
+ onClick={() => {
916
+ if (sandboxRow) openTraceMode();
917
+ }}
918
+ >
1055
919
  <History size={13} /> See Runs
1056
920
  </button>
1057
921
  {sidecarMode === "trace" && (
@@ -1093,7 +957,7 @@ export default function WorkflowSurface() {
1093
957
 
1094
958
  {/* One-time serverless upgrade onboarding — shows only when the operator
1095
959
  has workflows but none are serverless, and hasn't dismissed it. */}
1096
- {sandboxRow && !upgradeOpen && upgradeState.showOnboarding ? (
960
+ {sandboxRow && showServerlessUpgrade && !upgradeOpen && upgradeState.showOnboarding ? (
1097
961
  <div className="workspace-template-context-banner dm-workflow-upgrade-nudge" role="note">
1098
962
  <div>
1099
963
  <strong>{upgradeState.headline}</strong>
@@ -1111,7 +975,7 @@ export default function WorkflowSurface() {
1111
975
  {/* Serverless cockpit — same derivation + cockpit interface as the API
1112
976
  Registry and sandbox lanes. Toggles patch the sandbox row; deep config
1113
977
  (scheduler/adapter) routes to the object's Data Model drawer. */}
1114
- {sandboxRow && upgradeOpen && serverlessState ? (
978
+ {sandboxRow && showServerlessUpgrade && upgradeOpen && serverlessState ? (
1115
979
  <div className="dm-workflow-upgrade-panel">
1116
980
  <div className="dm-workflow-upgrade-panel-head">
1117
981
  <span className="dm-api-action-card-eyebrow">Persistence &amp; scheduling</span>
@@ -1233,6 +1097,8 @@ export default function WorkflowSurface() {
1233
1097
  graph={orchestrationGraph}
1234
1098
  objectId={objectId}
1235
1099
  rowName={rowId}
1100
+ sandboxRow={sandboxRow}
1101
+ onSandboxRowPatch={patchSandboxRuntimeFields}
1236
1102
  disabled={false}
1237
1103
  onGraphChange={(updater) => {
1238
1104
  setOrchestrationGraph((g) => (typeof updater === "function" ? updater(g) : updater));
@@ -23,6 +23,32 @@ Agents and streamed APIs elsewhere in the sandbox stay orthogonal: serverless sw
23
23
 
24
24
  Sandbox rows reference **`authRef` / named env refs** — never literals in browser or config records. Scheduling uses the referenced API Registry row’s **`authRef`** merge rules identical to **`/api/workspace/test-source`**.
25
25
 
26
+ ## Browser access (`browserAccess`)
27
+
28
+ `browserAccess` is a first-class boolean column on the sandbox row, surfaced as a single toggle in the record sidecar's **Environment & Network** section. It is locality-agnostic and agent-host-agnostic: the saved record carries the capability, and each execution path grants it through the mechanism that path actually understands.
29
+
30
+ **Deterministic normalization** — browser access implies outbound network. The sidecar toggle stamps `networkAllow: "true"` when browser access is switched on, and `POST /api/workspace/sandbox-run` enforces the same implication server-side (`networkAllow || browserAccess`), so rows patched via the API behave identically to rows saved in the UI.
31
+
32
+ **This is the product's existing agent browser primitive, surfaced — not a new system.** The upstream Paperclip server already grants any agent browser access through one boolean: the agent config's `chrome` primitive (`ui/src/components/agent-config-primitives.tsx` — "Enable Claude's Chrome integration by passing --chrome"), gated by the chrome-lease service (`server/src/services/chrome-lease.ts`) before `adapter.execute()`. The CMS profile contract likewise speaks `allowBrowserBridge` and execution mode `"browser"`. `browserAccess` is the same bit on the governed sandbox row, so rows stay portable to the upstream adapter registry without translation — exactly like the host slugs.
33
+
34
+ **Local (`local-agent-host`)** — when the row's bit is on, each host engages its **first-party** browser integration; the adapter never invents flags or writes host config it cannot verify against the upstream tool (the same rule the auth catalog follows for login subcommands):
35
+
36
+ | Lane | Hosts | Mechanism |
37
+ | --- | --- | --- |
38
+ | `native-flag` | Claude Code | `--chrome` — Claude's own Chrome integration, the same flag the upstream server adapter passes for the agent `chrome` primitive. |
39
+ | `native-flag` | Codex | `--enable browser_use --enable in_app_browser` (with `--sandbox workspace-write`). |
40
+ | `env-signal` | Cursor, Gemini, Qwen, OpenCode, Pi, Hermes, OpenClaw Gateway | The host receives `GROWTHUB_SANDBOX_BROWSER_ACCESS=1` (mirroring the upstream browser-isolation context); whatever browser integration the operator has configured in that host honors the row's setting. |
41
+
42
+ The lane engaged for a run is recorded in `adapterMeta.browserLane`, and the run-console record projection surfaces `context.browserAccess` plus the full `adapterMeta`, so every run shows its browser proof. No host-global config (`~/.claude`, `~/.codex`, …) is ever mutated.
43
+
44
+ **Orchestration graph** — this is why browser access is node-level and host-agnostic with zero extra configuration: `thinAdapter` and `ai-agent` nodes execute through this same host catalog, so every node inherits the row's browser grant no matter which host runs it (subagent nodes through the existing node-level Network gate; orchestrator and synthesis phases directly).
45
+
46
+ One deliberate decision, stated explicitly: **Codex `workspace-write` on `networkAllow` alone is intentional.** Codex's `read-only` sandbox blocks all outbound network, so `workspace-write` is the least-privileged Codex mode where the row's network grant can take effect — and writes are confined to the sealed ephemeral workdir the adapter spawns into, never the operator's repo. Browser flags remain gated on `browserAccess` only; network alone never opens a browser.
47
+
48
+ **Local (`local-process`)** and every other adapter — the sealed RunRequest carries `browserAccess: boolean`, and the env contract publishes `GROWTHUB_SANDBOX_BROWSER_ACCESS=1|0` alongside `GROWTHUB_SANDBOX_NET_ALLOW(LIST)`, so any script or drop-zone adapter honors the row's setting without knowing about specific hosts.
49
+
50
+ **Serverless** — the `growthub-sandbox-run-v1` envelope carries `sandbox.browserAccess` (plus `networkAllow` / `allowList`), so a workflow upgraded from local to serverless keeps the identical capability contract: the Edge/QStash/cron handler reads one boolean and grants its own runtime's browser (e.g. a remote browser pool or hosted agent's browser tool). No host-specific knowledge crosses the wire — slugs and booleans only, never secrets.
51
+
26
52
  ## Not a widget source
27
53
 
28
54
  Workspace Builder excludes **`sandbox-environment`** from View widget bindings (execution records, not tabular KPI sources). See **`data-sources-api-registry.md`** in this folder.