@growthub/cli 0.14.1 → 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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +49 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +54 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +113 -36
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +13 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +85 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +3 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/package.json +1 -1
|
@@ -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`
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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: () =>
|
|
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: () =>
|
|
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(
|
|
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
|
}));
|
|
@@ -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
|
|
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;
|
|
@@ -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
|
},
|
|
@@ -34,15 +34,14 @@
|
|
|
34
34
|
* on-disk auth state; this module only records *readiness*, not secrets.
|
|
35
35
|
*
|
|
36
36
|
* The status semantics are deliberately conservative:
|
|
37
|
-
* - "active"
|
|
38
|
-
*
|
|
39
|
-
* - "reachable"
|
|
40
|
-
* authentication is NOT yet confirmed
|
|
37
|
+
* - "active" the selected pinned host CLI is callable and ready for the
|
|
38
|
+
* local agent-host bridge, or a clean login exit completed
|
|
39
|
+
* - "reachable" legacy metadata value treated as active by the UI
|
|
41
40
|
* - "stale" the binary printed auth-shaped failure output
|
|
42
41
|
* - "missing" binary not found on PATH
|
|
43
42
|
*
|
|
44
|
-
* A
|
|
45
|
-
*
|
|
43
|
+
* A successful host probe promotes to "active" because the sidecar represents
|
|
44
|
+
* selected local host readiness, not provider-account auth semantics.
|
|
46
45
|
*/
|
|
47
46
|
|
|
48
47
|
import { spawn } from "node:child_process";
|
|
@@ -132,6 +131,38 @@ function assertAgentHostEligible(row, { requireLogin = false, requireLogout = fa
|
|
|
132
131
|
return { spec, agentHost };
|
|
133
132
|
}
|
|
134
133
|
|
|
134
|
+
function normalizeAgentHostOverride(agentHost) {
|
|
135
|
+
const nextAgentHost = String(agentHost || "").trim();
|
|
136
|
+
if (!nextAgentHost) return "";
|
|
137
|
+
if (!getHostAuthSpec(nextAgentHost)) {
|
|
138
|
+
const error = new Error(
|
|
139
|
+
`Agent auth setup is not registered for agentHost "${nextAgentHost}"`
|
|
140
|
+
);
|
|
141
|
+
error.code = "SANDBOX_AGENT_AUTH_HOST_UNSUPPORTED";
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return nextAgentHost;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applyAgentHostOverride(row, agentHost) {
|
|
148
|
+
if (!agentHost) return row;
|
|
149
|
+
return {
|
|
150
|
+
...row,
|
|
151
|
+
runLocality: "local",
|
|
152
|
+
adapter: "local-agent-host",
|
|
153
|
+
agentHost
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildAgentHostSelectionPatch(agentHost) {
|
|
158
|
+
if (!agentHost) return {};
|
|
159
|
+
return {
|
|
160
|
+
runLocality: "local",
|
|
161
|
+
adapter: "local-agent-host",
|
|
162
|
+
agentHost
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
function resolveHostBinary(row, spec) {
|
|
136
167
|
const candidates = [row?.agentCommand, row?.claudeCommand];
|
|
137
168
|
for (const candidate of candidates) {
|
|
@@ -205,9 +236,24 @@ function hasAny(patterns, text) {
|
|
|
205
236
|
return patterns.some((p) => p.test(text));
|
|
206
237
|
}
|
|
207
238
|
|
|
239
|
+
function deriveStatusFromAuthStatusJson(text) {
|
|
240
|
+
if (!text || typeof text !== "string") return null;
|
|
241
|
+
const trimmed = text.trim();
|
|
242
|
+
if (!trimmed.startsWith("{")) return null;
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(trimmed);
|
|
245
|
+
if (parsed && typeof parsed === "object" && typeof parsed.loggedIn === "boolean") {
|
|
246
|
+
return parsed.loggedIn ? "active" : "stale";
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
208
252
|
function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", spawnError }) {
|
|
209
253
|
if (spawnError) return spawnError.notFound ? "missing" : null;
|
|
210
254
|
const combined = `${stdout}\n${stderr}`;
|
|
255
|
+
const jsonStatus = deriveStatusFromAuthStatusJson(stdout) || deriveStatusFromAuthStatusJson(stderr);
|
|
256
|
+
if (jsonStatus) return jsonStatus;
|
|
211
257
|
if (hasAny(UNKNOWN_SUBCOMMAND_PATTERNS, combined)) return null; // fall back
|
|
212
258
|
if (hasAny(STALE_AUTH_PATTERNS, combined)) return "stale";
|
|
213
259
|
if (exitCode === 0) return "active";
|
|
@@ -219,7 +265,7 @@ function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", s
|
|
|
219
265
|
|
|
220
266
|
function deriveStatusFromVersionProbe({ exitCode, stderr, spawnError }) {
|
|
221
267
|
if (spawnError) return spawnError.notFound ? "missing" : "unknown";
|
|
222
|
-
if (typeof exitCode === "number" && exitCode === 0) return "
|
|
268
|
+
if (typeof exitCode === "number" && exitCode === 0) return "active";
|
|
223
269
|
const text = String(stderr || "");
|
|
224
270
|
if (hasAny(STALE_AUTH_PATTERNS, text)) return "stale";
|
|
225
271
|
return "unknown";
|
|
@@ -237,8 +283,8 @@ function deriveLoginStatus({ exitCode, stderr, stdout, timedOut, spawnError }) {
|
|
|
237
283
|
function shortMessage({ status, label, exitCode, error, loginUrl }) {
|
|
238
284
|
const name = label || "Local agent CLI";
|
|
239
285
|
if (error) return `${name}: ${redactSecrets(String(error))}`;
|
|
240
|
-
if (status === "active") return loginUrl ? "Login completed." : "
|
|
241
|
-
if (status === "reachable") return "
|
|
286
|
+
if (status === "active") return loginUrl ? "Login completed." : "Active.";
|
|
287
|
+
if (status === "reachable") return "Active.";
|
|
242
288
|
if (status === "stale") return "Authentication needs setup. Run Login, then run the sandbox again.";
|
|
243
289
|
if (status === "missing") return `${name} not found. Install it and try again.`;
|
|
244
290
|
if (status === "checking") return `Checking ${name}…`;
|
|
@@ -329,16 +375,18 @@ function runCommand({ binary, args, cwd, timeoutMs, stdin }) {
|
|
|
329
375
|
// Public API — login / logout / status
|
|
330
376
|
// ──────────────────────────────────────────────────────────────────────────
|
|
331
377
|
|
|
332
|
-
async function runAgentLogin({ objectId, name }) {
|
|
378
|
+
async function runAgentLogin({ objectId, name, agentHost }) {
|
|
333
379
|
const workspaceConfig = await readWorkspaceConfig();
|
|
334
380
|
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
335
381
|
if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
|
|
336
382
|
if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
|
|
337
383
|
|
|
338
|
-
const
|
|
384
|
+
const selectedAgentHost = normalizeAgentHostOverride(agentHost);
|
|
385
|
+
const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
|
|
386
|
+
const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogin: true });
|
|
339
387
|
|
|
340
|
-
const binary = resolveHostBinary(
|
|
341
|
-
const cwd = resolveCwd(
|
|
388
|
+
const binary = resolveHostBinary(effectiveRow, spec);
|
|
389
|
+
const cwd = resolveCwd(effectiveRow);
|
|
342
390
|
const startedAt = Date.now();
|
|
343
391
|
|
|
344
392
|
const result = await runCommand({
|
|
@@ -356,20 +404,21 @@ async function runAgentLogin({ objectId, name }) {
|
|
|
356
404
|
|
|
357
405
|
const patch = buildRowPatch({
|
|
358
406
|
status,
|
|
359
|
-
provider:
|
|
407
|
+
provider: effectiveAgentHost,
|
|
360
408
|
checkedAt,
|
|
361
409
|
exitCode: result.exitCode,
|
|
362
410
|
loginUrl,
|
|
363
411
|
label: spec.label,
|
|
364
412
|
spawnError: result.spawnError
|
|
365
413
|
});
|
|
414
|
+
Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
|
|
366
415
|
|
|
367
416
|
await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
|
|
368
417
|
|
|
369
418
|
return {
|
|
370
419
|
ok: status === "active",
|
|
371
420
|
status,
|
|
372
|
-
provider:
|
|
421
|
+
provider: effectiveAgentHost,
|
|
373
422
|
label: spec.label,
|
|
374
423
|
binary,
|
|
375
424
|
cwd,
|
|
@@ -384,16 +433,18 @@ async function runAgentLogin({ objectId, name }) {
|
|
|
384
433
|
};
|
|
385
434
|
}
|
|
386
435
|
|
|
387
|
-
async function runAgentLogout({ objectId, name }) {
|
|
436
|
+
async function runAgentLogout({ objectId, name, agentHost }) {
|
|
388
437
|
const workspaceConfig = await readWorkspaceConfig();
|
|
389
438
|
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
390
439
|
if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
|
|
391
440
|
if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
|
|
392
441
|
|
|
393
|
-
const
|
|
442
|
+
const selectedAgentHost = normalizeAgentHostOverride(agentHost);
|
|
443
|
+
const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
|
|
444
|
+
const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogout: true });
|
|
394
445
|
|
|
395
|
-
const binary = resolveHostBinary(
|
|
396
|
-
const cwd = resolveCwd(
|
|
446
|
+
const binary = resolveHostBinary(effectiveRow, spec);
|
|
447
|
+
const cwd = resolveCwd(effectiveRow);
|
|
397
448
|
const startedAt = Date.now();
|
|
398
449
|
|
|
399
450
|
let exitCode = null;
|
|
@@ -422,7 +473,7 @@ async function runAgentLogout({ objectId, name }) {
|
|
|
422
473
|
|
|
423
474
|
const patch = buildRowPatch({
|
|
424
475
|
status,
|
|
425
|
-
provider:
|
|
476
|
+
provider: effectiveAgentHost,
|
|
426
477
|
checkedAt,
|
|
427
478
|
exitCode,
|
|
428
479
|
loginUrl: null,
|
|
@@ -432,13 +483,14 @@ async function runAgentLogout({ objectId, name }) {
|
|
|
432
483
|
patch.agentAuthLastMessage = spawnError?.notFound
|
|
433
484
|
? shortMessage({ status: "missing", label: spec.label })
|
|
434
485
|
: `${spec.label} logged out — auth will be required before next run.`;
|
|
486
|
+
Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
|
|
435
487
|
|
|
436
488
|
await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
|
|
437
489
|
|
|
438
490
|
return {
|
|
439
491
|
ok: !spawnError,
|
|
440
492
|
status,
|
|
441
|
-
provider:
|
|
493
|
+
provider: effectiveAgentHost,
|
|
442
494
|
label: spec.label,
|
|
443
495
|
binary,
|
|
444
496
|
cwd,
|
|
@@ -451,16 +503,18 @@ async function runAgentLogout({ objectId, name }) {
|
|
|
451
503
|
};
|
|
452
504
|
}
|
|
453
505
|
|
|
454
|
-
async function checkAgentStatus({ objectId, name }) {
|
|
506
|
+
async function checkAgentStatus({ objectId, name, agentHost }) {
|
|
455
507
|
const workspaceConfig = await readWorkspaceConfig();
|
|
456
508
|
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
457
509
|
if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
|
|
458
510
|
if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
|
|
459
511
|
|
|
460
|
-
const
|
|
512
|
+
const selectedAgentHost = normalizeAgentHostOverride(agentHost);
|
|
513
|
+
const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
|
|
514
|
+
const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow);
|
|
461
515
|
|
|
462
|
-
const binary = resolveHostBinary(
|
|
463
|
-
const cwd = resolveCwd(
|
|
516
|
+
const binary = resolveHostBinary(effectiveRow, spec);
|
|
517
|
+
const cwd = resolveCwd(effectiveRow);
|
|
464
518
|
|
|
465
519
|
// Two-phase probe:
|
|
466
520
|
// 1. If the catalog declares an auth-status subcommand, try it first.
|
|
@@ -503,20 +557,21 @@ async function checkAgentStatus({ objectId, name }) {
|
|
|
503
557
|
|
|
504
558
|
const patch = buildRowPatch({
|
|
505
559
|
status,
|
|
506
|
-
provider:
|
|
560
|
+
provider: effectiveAgentHost,
|
|
507
561
|
checkedAt,
|
|
508
562
|
exitCode: usedResult.exitCode,
|
|
509
563
|
loginUrl: null,
|
|
510
564
|
label: spec.label,
|
|
511
565
|
spawnError: usedResult.spawnError
|
|
512
566
|
});
|
|
567
|
+
Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
|
|
513
568
|
|
|
514
569
|
await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
|
|
515
570
|
|
|
516
571
|
return {
|
|
517
572
|
ok: status === "active",
|
|
518
573
|
status,
|
|
519
|
-
provider:
|
|
574
|
+
provider: effectiveAgentHost,
|
|
520
575
|
label: spec.label,
|
|
521
576
|
binary,
|
|
522
577
|
cwd,
|
|
@@ -70,6 +70,7 @@ function deriveSandboxServerlessState(input = {}) {
|
|
|
70
70
|
|
|
71
71
|
const locality = clean(row.runLocality).toLowerCase() === "serverless" ? "serverless" : "local";
|
|
72
72
|
const isServerless = locality === "serverless";
|
|
73
|
+
const browserAccess = ["true", "1", "on", "yes"].includes(clean(row.browserAccess).toLowerCase());
|
|
73
74
|
const adapterId = clean(row.adapter);
|
|
74
75
|
const adapterChosen = Boolean(adapterId);
|
|
75
76
|
|
|
@@ -162,8 +163,8 @@ function deriveSandboxServerlessState(input = {}) {
|
|
|
162
163
|
label: isServerless ? "Run on the scheduler" : "Run locally",
|
|
163
164
|
status: "optional",
|
|
164
165
|
description: isServerless
|
|
165
|
-
?
|
|
166
|
-
:
|
|
166
|
+
? `Once the scheduler, auth, and store are ready, run delegates to the serverless scheduler.${browserAccess ? " Browser access travels with the run in the growthub-sandbox-run-v1 envelope (sandbox.browserAccess), so the remote handler grants the same capability as a local run." : ""}`
|
|
167
|
+
: `Run this workflow in-process.${browserAccess ? (adapterId === "local-intelligence" ? " Browser access is executed by the local-intelligence browser bridge." : " Browser access is engaged through the selected agent host's first-party browser integration.") : ""}`,
|
|
167
168
|
action: inline({ id: "run-sandbox", label: "Run" }),
|
|
168
169
|
});
|
|
169
170
|
|
|
@@ -192,6 +193,7 @@ function deriveSandboxServerlessState(input = {}) {
|
|
|
192
193
|
version: 1,
|
|
193
194
|
locality,
|
|
194
195
|
isServerless,
|
|
196
|
+
browserAccess,
|
|
195
197
|
adapterChosen,
|
|
196
198
|
schedulerLinked,
|
|
197
199
|
schedulerHealthy,
|
|
@@ -1044,6 +1044,12 @@ function validateSandboxEnvironmentRow(row, path, errors) {
|
|
|
1044
1044
|
if (row.allowList !== undefined && typeof row.allowList !== "string" && !Array.isArray(row.allowList)) {
|
|
1045
1045
|
errors.push(`${path}.allowList must be a comma-separated string or array of hostnames`);
|
|
1046
1046
|
}
|
|
1047
|
+
if (row.browserAccess !== undefined) {
|
|
1048
|
+
const value = String(row.browserAccess).trim().toLowerCase();
|
|
1049
|
+
if (!["", "true", "false", "0", "1", "on", "off"].includes(value)) {
|
|
1050
|
+
errors.push(`${path}.browserAccess must coerce to a boolean (true/false/on/off)`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1047
1053
|
if (row.instructions !== undefined && typeof row.instructions !== "string") {
|
|
1048
1054
|
errors.push(`${path}.instructions must be a string`);
|
|
1049
1055
|
}
|
|
@@ -69,6 +69,7 @@ const SWARM_EXECUTION_TARGET_FIELDS = [
|
|
|
69
69
|
"timeoutMs",
|
|
70
70
|
"networkAllow",
|
|
71
71
|
"allowList",
|
|
72
|
+
"browserAccess",
|
|
72
73
|
];
|
|
73
74
|
|
|
74
75
|
function clean(value) {
|
|
@@ -152,6 +153,7 @@ function resolveSwarmExecutionTarget(workspaceConfig, payload = {}) {
|
|
|
152
153
|
timeoutMs: String(clampPositiveInt(payload?.timeoutMs || helperRow?.timeoutMs, SWARM_DEFAULT_TIMEOUT_MS)),
|
|
153
154
|
networkAllow: clean(payload?.networkAllow || helperRow?.networkAllow),
|
|
154
155
|
allowList: clean(payload?.allowList || helperRow?.allowList),
|
|
156
|
+
browserAccess: clean(payload?.browserAccess || helperRow?.browserAccess),
|
|
155
157
|
inheritedFromObjectId: helperRow ? WORKSPACE_HELPER_SANDBOX_OBJECT_ID : "",
|
|
156
158
|
inheritedFromName: helperRow ? clean(helperRow.Name || WORKSPACE_HELPER_ROW_NAME) : "",
|
|
157
159
|
};
|
|
@@ -406,6 +408,7 @@ function buildSandboxRowFromSwarmProposal(workspaceConfig, proposal) {
|
|
|
406
408
|
envRefs: "",
|
|
407
409
|
networkAllow: executionTarget.networkAllow,
|
|
408
410
|
allowList: executionTarget.allowList,
|
|
411
|
+
browserAccess: executionTarget.browserAccess,
|
|
409
412
|
instructions: clean(payload.objective),
|
|
410
413
|
command: "",
|
|
411
414
|
timeoutMs: executionTarget.timeoutMs,
|