@growthub/cli 0.14.0 → 0.14.2

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 (40) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  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 +179 -117
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  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 +136 -3
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  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-schema.js +6 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  40. package/package.json +1 -1
@@ -34,28 +34,97 @@ import path from "node:path";
34
34
  import { registerSandboxAdapter } from "./sandbox-adapter-registry.js";
35
35
 
36
36
  const MAX_OUTPUT_BYTES = 1024 * 256;
37
+ const TELEMETRY_MARKER = "GROWTHUB_AGENT_TELEMETRY:";
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
+ */
37
71
 
38
72
  /**
39
73
  * Canonical Paperclip host catalog — slugs mirror `AGENT_ADAPTER_TYPES`.
40
74
  *
41
75
  * Each entry declares the binary the operator must have on PATH and how to
42
- * invoke it for one-shot prompt execution. `argv` returns the argv array the
43
- * adapter should pass; `inputMode` chooses whether the user's command is sent
44
- * via stdin or as a positional argument. `installHint` is surfaced verbatim
45
- * 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).
46
84
  */
47
85
  const HOST_CATALOG = {
48
86
  claude_local: {
49
87
  label: "Claude Code (local)",
50
88
  binary: "claude",
51
- 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"] },
52
100
  inputMode: "stdin",
53
101
  installHint: "Install Claude Code: npm i -g @anthropic-ai/claude-code"
54
102
  },
55
103
  codex_local: {
56
104
  label: "Codex CLI (local)",
57
105
  binary: "codex",
58
- 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"] },
59
128
  inputMode: "stdin",
60
129
  installHint: "Install Codex CLI: npm i -g @openai/codex"
61
130
  },
@@ -63,6 +132,7 @@ const HOST_CATALOG = {
63
132
  label: "Cursor Agent (local)",
64
133
  binary: "cursor-agent",
65
134
  argv: () => ["--print"],
135
+ browser: { lane: "env-signal" },
66
136
  inputMode: "stdin",
67
137
  installHint: "Install Cursor Agent CLI: curl https://cursor.com/install -fsS | bash"
68
138
  },
@@ -70,6 +140,7 @@ const HOST_CATALOG = {
70
140
  label: "Gemini CLI (local)",
71
141
  binary: "gemini",
72
142
  argv: () => ["-p", "-"],
143
+ browser: { lane: "env-signal" },
73
144
  inputMode: "stdin",
74
145
  installHint: "Install Gemini CLI: npm i -g @google/gemini-cli"
75
146
  },
@@ -77,6 +148,7 @@ const HOST_CATALOG = {
77
148
  label: "OpenCode (local)",
78
149
  binary: "opencode",
79
150
  argv: () => ["run", "--quiet"],
151
+ browser: { lane: "env-signal" },
80
152
  inputMode: "stdin",
81
153
  installHint: "Install OpenCode: npm i -g opencode-ai"
82
154
  },
@@ -84,6 +156,7 @@ const HOST_CATALOG = {
84
156
  label: "Pi (local)",
85
157
  binary: "pi",
86
158
  argv: () => ["run", "--stdin"],
159
+ browser: { lane: "env-signal" },
87
160
  inputMode: "stdin",
88
161
  installHint: "Install Pi CLI: refer to your Paperclip Pi distribution"
89
162
  },
@@ -91,6 +164,7 @@ const HOST_CATALOG = {
91
164
  label: "Qwen Code (local)",
92
165
  binary: "qwen",
93
166
  argv: () => ["-p"],
167
+ browser: { lane: "env-signal" },
94
168
  inputMode: "stdin",
95
169
  installHint: "Install Qwen Code CLI: refer to your Qwen distribution"
96
170
  },
@@ -98,6 +172,7 @@ const HOST_CATALOG = {
98
172
  label: "Hermes Paperclip (local)",
99
173
  binary: "hermes",
100
174
  argv: () => ["run", "--stdin"],
175
+ browser: { lane: "env-signal" },
101
176
  inputMode: "stdin",
102
177
  installHint: "Install Hermes Paperclip adapter: npm i -g hermes-paperclip-adapter"
103
178
  },
@@ -105,6 +180,7 @@ const HOST_CATALOG = {
105
180
  label: "OpenClaw Gateway (local)",
106
181
  binary: "openclaw",
107
182
  argv: () => ["gateway", "exec", "--stdin"],
183
+ browser: { lane: "env-signal" },
108
184
  inputMode: "stdin",
109
185
  installHint: "Install OpenClaw Gateway: refer to your Paperclip distribution"
110
186
  }
@@ -118,6 +194,134 @@ function clampStream(buffer) {
118
194
  return `${head.toString("utf8")}\n…\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]`;
119
195
  }
120
196
 
197
+ function safeNonNegativeInt(value) {
198
+ if (value === null || value === undefined || value === "") return null;
199
+ const n = Number(value);
200
+ if (!Number.isFinite(n) || n < 0) return null;
201
+ return Math.floor(n);
202
+ }
203
+
204
+ function pickFirstNumber(...values) {
205
+ for (const value of values) {
206
+ const n = safeNonNegativeInt(value);
207
+ if (n != null) return n;
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function sumNumbers(...values) {
213
+ let total = 0;
214
+ let seen = false;
215
+ for (const value of values) {
216
+ const n = safeNonNegativeInt(value);
217
+ if (n == null) continue;
218
+ total += n;
219
+ seen = true;
220
+ }
221
+ return seen ? total : null;
222
+ }
223
+
224
+ function extractUsageFromObject(obj) {
225
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return { tokens: null, tools: null };
226
+ const usage = (obj.usage && typeof obj.usage === "object" && !Array.isArray(obj.usage))
227
+ ? obj.usage
228
+ : (obj.token_usage && typeof obj.token_usage === "object" && !Array.isArray(obj.token_usage))
229
+ ? obj.token_usage
230
+ : (obj.metadata?.usage && typeof obj.metadata.usage === "object" && !Array.isArray(obj.metadata.usage))
231
+ ? obj.metadata.usage
232
+ : (obj.result?.usage && typeof obj.result.usage === "object" && !Array.isArray(obj.result.usage))
233
+ ? obj.result.usage
234
+ : null;
235
+ const tokens = usage
236
+ ? pickFirstNumber(
237
+ usage.total_tokens,
238
+ usage.totalTokens,
239
+ usage.tokens,
240
+ sumNumbers(usage.input_tokens, usage.output_tokens),
241
+ sumNumbers(usage.prompt_tokens, usage.completion_tokens),
242
+ sumNumbers(usage.inputTokens, usage.outputTokens),
243
+ )
244
+ : pickFirstNumber(obj.total_tokens, obj.totalTokens, obj.tokens);
245
+ const toolArrays = [
246
+ obj.tool_calls,
247
+ obj.toolCalls,
248
+ obj.toolInvocations,
249
+ obj.message?.tool_calls,
250
+ obj.choices?.[0]?.message?.tool_calls,
251
+ obj.result?.tool_calls,
252
+ obj.result?.toolCalls,
253
+ ].filter(Array.isArray);
254
+ const tools = pickFirstNumber(
255
+ obj.tools,
256
+ obj.tool_count,
257
+ obj.toolCount,
258
+ ...toolArrays.map((items) => items.length),
259
+ );
260
+ return { tokens, tools };
261
+ }
262
+
263
+ function mergeTelemetry(base, next) {
264
+ return {
265
+ tokens: base.tokens ?? next.tokens ?? null,
266
+ tools: base.tools ?? next.tools ?? null,
267
+ };
268
+ }
269
+
270
+ function parseJsonMaybe(text) {
271
+ const value = String(text || "").trim();
272
+ if (!value || !/^[\[{]/.test(value)) return null;
273
+ try {
274
+ return JSON.parse(value);
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+
280
+ function extractMarkedTelemetry(text) {
281
+ let out = { tokens: null, tools: null };
282
+ for (const line of String(text || "").split(/\r?\n/)) {
283
+ const idx = line.indexOf(TELEMETRY_MARKER);
284
+ if (idx === -1) continue;
285
+ const json = line.slice(idx + TELEMETRY_MARKER.length).trim();
286
+ const parsed = parseJsonMaybe(json);
287
+ out = mergeTelemetry(out, extractUsageFromObject(parsed));
288
+ }
289
+ return out;
290
+ }
291
+
292
+ function extractJsonLineTelemetry(text) {
293
+ let out = { tokens: null, tools: null };
294
+ for (const line of String(text || "").split(/\r?\n/)) {
295
+ const parsed = parseJsonMaybe(line);
296
+ if (!parsed) continue;
297
+ out = mergeTelemetry(out, extractUsageFromObject(parsed));
298
+ }
299
+ return out;
300
+ }
301
+
302
+ function extractStderrTextTelemetry(stderrText) {
303
+ const text = String(stderrText || "");
304
+ const total = text.match(/\b(?:total\s+tokens|tokens\s+used)\s*(?:[:=]|\r?\n)\s*([0-9][0-9,]*)/i);
305
+ const input = text.match(/\b(?:input|prompt)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
306
+ const output = text.match(/\b(?:output|completion)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
307
+ const tools = text.match(/\b(?:tool\s+calls?|tools\s+used)\s*[:=]\s*([0-9][0-9,]*)/i);
308
+ const tokens = pickFirstNumber(total?.[1]?.replace(/,/g, ""), sumNumbers(input?.[1]?.replace(/,/g, ""), output?.[1]?.replace(/,/g, "")));
309
+ return {
310
+ tokens,
311
+ tools: pickFirstNumber(tools?.[1]?.replace(/,/g, ""), tokens != null ? 0 : null),
312
+ };
313
+ }
314
+
315
+ function extractAgentHostTelemetry({ stdout, stderr }) {
316
+ let out = { tokens: null, tools: null };
317
+ const stdoutJson = parseJsonMaybe(stdout);
318
+ if (stdoutJson && !Array.isArray(stdoutJson)) out = mergeTelemetry(out, extractUsageFromObject(stdoutJson));
319
+ out = mergeTelemetry(out, extractMarkedTelemetry(stderr));
320
+ out = mergeTelemetry(out, extractJsonLineTelemetry(stderr));
321
+ out = mergeTelemetry(out, extractStderrTextTelemetry(stderr));
322
+ return out;
323
+ }
324
+
121
325
  async function run(request) {
122
326
  const hostSlug = typeof request.agentHost === "string" ? request.agentHost.trim() : "";
123
327
  const host = HOST_CATALOG[hostSlug];
@@ -163,12 +367,13 @@ async function run(request) {
163
367
  GROWTHUB_SANDBOX_AGENT_HOST: hostSlug,
164
368
  GROWTHUB_SANDBOX_NET_ALLOW: request.networkAllow ? "1" : "0",
165
369
  GROWTHUB_SANDBOX_NET_ALLOWLIST: Array.isArray(request.allowList) ? request.allowList.join(",") : "",
370
+ GROWTHUB_SANDBOX_BROWSER_ACCESS: request.browserAccess ? "1" : "0",
166
371
  ...(request.env || {})
167
372
  };
168
373
 
169
374
  const timeoutMs = Number.isFinite(request.timeoutMs) && request.timeoutMs > 0 ? request.timeoutMs : 60000;
170
375
  const startedAt = Date.now();
171
- const argv = host.argv(command);
376
+ const argv = host.argv(request);
172
377
 
173
378
  return await new Promise((resolve) => {
174
379
  let stdout = Buffer.alloc(0);
@@ -238,12 +443,15 @@ async function run(request) {
238
443
  clearTimeout(timer);
239
444
  const durationMs = Date.now() - startedAt;
240
445
  const ok = !timedOut && exitCode === 0;
446
+ const stdoutText = clampStream(stdout);
447
+ const stderrText = clampStream(stderr);
448
+ const telemetry = extractAgentHostTelemetry({ stdout: stdoutText, stderr: stderrText });
241
449
  resolve({
242
450
  ok,
243
451
  exitCode: typeof exitCode === "number" ? exitCode : null,
244
452
  durationMs,
245
- stdout: clampStream(stdout),
246
- stderr: clampStream(stderr),
453
+ stdout: stdoutText,
454
+ stderr: stderrText,
247
455
  error: timedOut
248
456
  ? `timed out after ${timeoutMs}ms`
249
457
  : (ok ? undefined : `exit ${exitCode ?? signal ?? "unknown"}`),
@@ -253,8 +461,13 @@ async function run(request) {
253
461
  binary: host.binary,
254
462
  argv,
255
463
  inputMode: host.inputMode,
464
+ browserAccess: Boolean(request.browserAccess),
465
+ browserLane: request.browserAccess ? host.browser.lane : null,
256
466
  timedOut,
257
- signal: signal || null
467
+ signal: signal || null,
468
+ tokens: telemetry.tokens,
469
+ tools: telemetry.tools,
470
+ telemetrySource: telemetry.tokens != null || telemetry.tools != null ? "agent-host-reported" : "unreported"
258
471
  }
259
472
  });
260
473
  });
@@ -281,4 +494,4 @@ registerSandboxAdapter({
281
494
  run
282
495
  });
283
496
 
284
- export { HOST_CATALOG, SUPPORTED_HOSTS };
497
+ export { HOST_CATALOG, SUPPORTED_HOSTS, extractAgentHostTelemetry };
@@ -190,6 +190,10 @@ async function run(request) {
190
190
  endpoint,
191
191
  model,
192
192
  locality: "local",
193
+ // Truthful telemetry only — taken from the completion's usage block
194
+ // when the endpoint reports one, never estimated. Null means unknown.
195
+ tokens: Number.isFinite(outer?.usage?.total_tokens) ? outer.usage.total_tokens : null,
196
+ tools: Array.isArray(parsed.toolIntents) ? parsed.toolIntents.length : null,
193
197
  },
194
198
  };
195
199
  } catch (err) {
@@ -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"]