@growthub/cli 0.13.2 → 0.13.5

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 (42) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +24 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +14 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +74 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +67 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +77 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +72 -4
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +123 -27
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +224 -1
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +754 -92
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +224 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +32 -1
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +530 -9
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +8 -1
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +10 -7
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/RunSetupPanel.jsx +261 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +119 -9
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +779 -138
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +91 -14
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +35 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +28 -3
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +366 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +34 -3
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-eligibility.js +50 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-redaction.js +64 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +665 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-host-catalog.js +168 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +595 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +164 -7
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +11 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +111 -1
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +14 -0
  42. package/package.json +1 -1
@@ -0,0 +1,665 @@
1
+ /**
2
+ * Sandbox Local Agent Auth Onboarding V1 — server-side helper.
3
+ *
4
+ * Sandbox rows with `adapter: "local-agent-host"` route execution through
5
+ * the thin host adapter at `lib/adapters/sandboxes/default-local-agent-host.js`.
6
+ * That adapter is intentionally **execution-only**: it spawns the CLI and
7
+ * captures stdout/stderr/exit code, and it does NOT manage host auth state.
8
+ *
9
+ * Auth setup is a separate concern — preparing the local CLI before a
10
+ * sandbox row can run successfully. This helper is the workspace API
11
+ * surface for that preflight. It is host-agnostic: the per-host commands
12
+ * (login subcommand, logout subcommand, status probe) live in
13
+ * `lib/sandbox-agent-host-catalog.js`. Adding a host means editing the
14
+ * catalog — never extending this file.
15
+ *
16
+ * Responsibilities:
17
+ * - resolve a sandbox row by `objectId` + `name`
18
+ * - guard on adapter + agentHost + runLocality eligibility
19
+ * - resolve the binary (row override, defaults to the catalog default)
20
+ * - spawn the catalog-declared subcommands
21
+ * - capture stdout, stderr, login URL, exit code
22
+ * - redact anything token-shaped before returning to the browser
23
+ * - stamp ONLY safe metadata back onto the sandbox row:
24
+ *
25
+ * agentAuthStatus "active" | "reachable" | "stale" | "missing"
26
+ * | "checking" | "unknown"
27
+ * agentAuthProvider the host slug, e.g. "claude_local"
28
+ * agentAuthLastChecked ISO timestamp
29
+ * agentAuthLastExitCode number | null
30
+ * agentAuthLastMessage short human-readable summary
31
+ * agentAuthLastLoginUrl string | null (login URL if printed)
32
+ *
33
+ * Raw tokens NEVER touch `growthub.config.json`. The host CLI keeps its own
34
+ * on-disk auth state; this module only records *readiness*, not secrets.
35
+ *
36
+ * The status semantics are deliberately conservative:
37
+ * - "active" a real auth probe confirmed authentication (auth-status
38
+ * exit 0 with auth-shaped output, or a clean login exit)
39
+ * - "reachable" the binary is callable (version probe exit 0) — but
40
+ * authentication is NOT yet confirmed
41
+ * - "stale" the binary printed auth-shaped failure output
42
+ * - "missing" binary not found on PATH
43
+ *
44
+ * A `--version` probe NEVER promotes to "active". The next sandbox-run is
45
+ * the final source of truth for session readiness.
46
+ */
47
+
48
+ import { spawn } from "node:child_process";
49
+ import { promisify } from "node:util";
50
+ import { execFile } from "node:child_process";
51
+ import {
52
+ describePersistenceMode,
53
+ readWorkspaceConfig,
54
+ writeWorkspaceConfig
55
+ } from "@/lib/workspace-config";
56
+ import {
57
+ DEFAULT_LOGIN_TIMEOUT_MS,
58
+ DEFAULT_LOGOUT_TIMEOUT_MS,
59
+ DEFAULT_PROBE_TIMEOUT_MS,
60
+ getAgentHostCapabilities,
61
+ getHostAuthSpec
62
+ } from "@/lib/sandbox-agent-host-catalog";
63
+ import {
64
+ KNOWN_AGENT_AUTH_STATUSES,
65
+ SAFE_ROW_PATCH_FIELDS,
66
+ redactSecrets
67
+ } from "@/lib/sandbox-agent-auth-redaction";
68
+
69
+ const execFileAsync = promisify(execFile);
70
+
71
+ const MAX_CAPTURED_BYTES = 64 * 1024;
72
+
73
+ // ──────────────────────────────────────────────────────────────────────────
74
+ // Resolution
75
+ // ──────────────────────────────────────────────────────────────────────────
76
+
77
+ function findSandboxRow(workspaceConfig, objectId, name) {
78
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
79
+ ? workspaceConfig.dataModel.objects
80
+ : [];
81
+ const object = objects.find(
82
+ (entry) => entry?.id === objectId && entry?.objectType === "sandbox-environment"
83
+ );
84
+ if (!object) return { object: null, row: null, rowIndex: -1 };
85
+ const wantedName = String(name || "").trim();
86
+ const rows = Array.isArray(object.rows) ? object.rows : [];
87
+ const rowIndex = rows.findIndex((row) => String(row?.Name || "").trim() === wantedName);
88
+ if (rowIndex === -1) return { object, row: null, rowIndex: -1 };
89
+ return { object, row: rows[rowIndex], rowIndex };
90
+ }
91
+
92
+ function assertAgentHostEligible(row, { requireLogin = false, requireLogout = false } = {}) {
93
+ const adapter = String(row?.adapter || "").trim();
94
+ if (adapter !== "local-agent-host") {
95
+ const error = new Error(
96
+ `Agent auth setup applies only to adapter "local-agent-host" (got "${adapter || "<unset>"}")`
97
+ );
98
+ error.code = "SANDBOX_AGENT_AUTH_ADAPTER_MISMATCH";
99
+ throw error;
100
+ }
101
+ const runLocality = String(row?.runLocality || "").trim().toLowerCase();
102
+ if (runLocality === "serverless") {
103
+ const error = new Error(
104
+ "Agent auth setup is not supported when runLocality is `serverless` — auth lives on the local machine."
105
+ );
106
+ error.code = "SANDBOX_AGENT_AUTH_LOCALITY_MISMATCH";
107
+ throw error;
108
+ }
109
+ const agentHost = String(row?.agentHost || "").trim();
110
+ const spec = getHostAuthSpec(agentHost);
111
+ if (!spec) {
112
+ const error = new Error(
113
+ `Agent auth setup is not registered for agentHost "${agentHost || "<unset>"}"`
114
+ );
115
+ error.code = "SANDBOX_AGENT_AUTH_HOST_UNSUPPORTED";
116
+ throw error;
117
+ }
118
+ if (requireLogin && !Array.isArray(spec.loginCommand)) {
119
+ const error = new Error(
120
+ `Host "${agentHost}" does not declare a documented login subcommand. ${spec.notes || "Sign in via the host CLI directly."}`
121
+ );
122
+ error.code = "SANDBOX_AGENT_AUTH_LOGIN_UNSUPPORTED";
123
+ throw error;
124
+ }
125
+ if (requireLogout && !Array.isArray(spec.logoutCommand)) {
126
+ const error = new Error(
127
+ `Host "${agentHost}" does not declare a documented logout subcommand. ${spec.notes || "Sign out via the host CLI directly."}`
128
+ );
129
+ error.code = "SANDBOX_AGENT_AUTH_LOGOUT_UNSUPPORTED";
130
+ throw error;
131
+ }
132
+ return { spec, agentHost };
133
+ }
134
+
135
+ function resolveHostBinary(row, spec) {
136
+ const candidates = [row?.agentCommand, row?.claudeCommand];
137
+ for (const candidate of candidates) {
138
+ if (typeof candidate === "string" && candidate.trim()) {
139
+ return candidate.trim();
140
+ }
141
+ }
142
+ return spec.binary;
143
+ }
144
+
145
+ function resolveCwd(row) {
146
+ const cwd = row?.cwd;
147
+ if (typeof cwd === "string" && cwd.trim()) return cwd.trim();
148
+ return process.cwd();
149
+ }
150
+
151
+ // ──────────────────────────────────────────────────────────────────────────
152
+ // Output handling
153
+ //
154
+ // Redaction utilities live in `sandbox-agent-auth-redaction.js` so they can
155
+ // be imported without pulling in Next.js path-aliased modules — keeps the
156
+ // unit test surface lean.
157
+ // ──────────────────────────────────────────────────────────────────────────
158
+
159
+ function clampOutput(text) {
160
+ if (typeof text !== "string") return "";
161
+ if (text.length <= MAX_CAPTURED_BYTES) return text;
162
+ const head = text.slice(0, MAX_CAPTURED_BYTES);
163
+ return `${head}\n…\n[output truncated at ${MAX_CAPTURED_BYTES} bytes]`;
164
+ }
165
+
166
+ function extractLoginUrl(combined) {
167
+ if (typeof combined !== "string" || !combined) return null;
168
+ const match = combined.match(/https?:\/\/[^\s]+auth[^\s]*/)
169
+ || combined.match(/https?:\/\/[^\s]+(?:login|oauth|sign[_-]?in)[^\s]*/i);
170
+ return match ? match[0] : null;
171
+ }
172
+
173
+ // ──────────────────────────────────────────────────────────────────────────
174
+ // Status pattern recognition
175
+ // ──────────────────────────────────────────────────────────────────────────
176
+
177
+ const STALE_AUTH_PATTERNS = [
178
+ /not\s+logged\s+in/i,
179
+ /login\s+required/i,
180
+ /authentication\s+required/i,
181
+ /please\s+(?:log\s+in|sign\s+in)/i,
182
+ /run\s+[`'"]?[a-z][a-z0-9_-]*\s+auth\s+login/i,
183
+ /unauthorized/i,
184
+ /invalid\s+credentials/i,
185
+ /session\s+expired/i
186
+ ];
187
+
188
+ const ACTIVE_AUTH_PATTERNS = [
189
+ /logged\s+in\s+as/i,
190
+ /authenticated\s+as/i,
191
+ /session\s+active/i,
192
+ /auth(?:entication)?\s+ok/i
193
+ ];
194
+
195
+ const UNKNOWN_SUBCOMMAND_PATTERNS = [
196
+ /unknown\s+(?:command|subcommand|option)/i,
197
+ /did\s+you\s+mean/i,
198
+ /no\s+such\s+command/i,
199
+ /invalid\s+command/i,
200
+ /usage:/i
201
+ ];
202
+
203
+ function hasAny(patterns, text) {
204
+ if (!text) return false;
205
+ return patterns.some((p) => p.test(text));
206
+ }
207
+
208
+ function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", spawnError }) {
209
+ if (spawnError) return spawnError.notFound ? "missing" : null;
210
+ const combined = `${stdout}\n${stderr}`;
211
+ if (hasAny(UNKNOWN_SUBCOMMAND_PATTERNS, combined)) return null; // fall back
212
+ if (hasAny(STALE_AUTH_PATTERNS, combined)) return "stale";
213
+ if (exitCode === 0) return "active";
214
+ if (typeof exitCode === "number" && exitCode !== 0) {
215
+ return hasAny(STALE_AUTH_PATTERNS, combined) ? "stale" : null;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ function deriveStatusFromVersionProbe({ exitCode, stderr, spawnError }) {
221
+ if (spawnError) return spawnError.notFound ? "missing" : "unknown";
222
+ if (typeof exitCode === "number" && exitCode === 0) return "reachable";
223
+ const text = String(stderr || "");
224
+ if (hasAny(STALE_AUTH_PATTERNS, text)) return "stale";
225
+ return "unknown";
226
+ }
227
+
228
+ function deriveLoginStatus({ exitCode, stderr, stdout, timedOut, spawnError }) {
229
+ if (spawnError) return spawnError.notFound ? "missing" : "unknown";
230
+ if (timedOut) return "unknown";
231
+ if (typeof exitCode === "number" && exitCode === 0) return "active";
232
+ const combined = `${stdout || ""}\n${stderr || ""}`;
233
+ if (hasAny(STALE_AUTH_PATTERNS, combined)) return "stale";
234
+ return "stale";
235
+ }
236
+
237
+ function shortMessage({ status, label, exitCode, error, loginUrl }) {
238
+ const name = label || "Local agent CLI";
239
+ if (error) return `${name}: ${redactSecrets(String(error))}`;
240
+ if (status === "active") return loginUrl ? "Login completed." : "Authenticated.";
241
+ if (status === "reachable") return "CLI reachable. Run Login to verify authentication.";
242
+ if (status === "stale") return "Authentication needs setup. Run Login, then run the sandbox again.";
243
+ if (status === "missing") return `${name} not found. Install it and try again.`;
244
+ if (status === "checking") return `Checking ${name}…`;
245
+ if (typeof exitCode === "number") return `${name} exited with code ${exitCode}.`;
246
+ return "Not checked yet. Run Check or Login.";
247
+ }
248
+
249
+ // ──────────────────────────────────────────────────────────────────────────
250
+ // Process orchestration
251
+ // ──────────────────────────────────────────────────────────────────────────
252
+
253
+ function runCommand({ binary, args, cwd, timeoutMs, stdin }) {
254
+ return new Promise((resolve) => {
255
+ let stdoutBuf = "";
256
+ let stderrBuf = "";
257
+ let timedOut = false;
258
+ let resolved = false;
259
+ let child;
260
+
261
+ try {
262
+ child = spawn(binary, args, {
263
+ cwd,
264
+ stdio: stdin === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
265
+ env: { ...process.env }
266
+ });
267
+ } catch (error) {
268
+ resolve({
269
+ exitCode: null,
270
+ stdout: "",
271
+ stderr: "",
272
+ timedOut: false,
273
+ spawnError: {
274
+ message: error?.message || `failed to spawn ${binary}`,
275
+ notFound: error?.code === "ENOENT"
276
+ }
277
+ });
278
+ return;
279
+ }
280
+
281
+ const timer = setTimeout(() => {
282
+ timedOut = true;
283
+ try { child.kill("SIGKILL"); } catch {}
284
+ }, timeoutMs);
285
+
286
+ child.stdout?.on("data", (chunk) => {
287
+ if (stdoutBuf.length < MAX_CAPTURED_BYTES) stdoutBuf += chunk.toString("utf8");
288
+ });
289
+ child.stderr?.on("data", (chunk) => {
290
+ if (stderrBuf.length < MAX_CAPTURED_BYTES) stderrBuf += chunk.toString("utf8");
291
+ });
292
+
293
+ child.on("error", (error) => {
294
+ if (resolved) return;
295
+ resolved = true;
296
+ clearTimeout(timer);
297
+ resolve({
298
+ exitCode: null,
299
+ stdout: stdoutBuf,
300
+ stderr: stderrBuf,
301
+ timedOut,
302
+ spawnError: {
303
+ message: error?.message || "spawn failed",
304
+ notFound: error?.code === "ENOENT"
305
+ }
306
+ });
307
+ });
308
+
309
+ child.on("close", (exitCode) => {
310
+ if (resolved) return;
311
+ resolved = true;
312
+ clearTimeout(timer);
313
+ resolve({
314
+ exitCode: typeof exitCode === "number" ? exitCode : null,
315
+ stdout: stdoutBuf,
316
+ stderr: stderrBuf,
317
+ timedOut,
318
+ spawnError: null
319
+ });
320
+ });
321
+
322
+ if (stdin !== undefined) {
323
+ try { child.stdin.end(stdin); } catch {}
324
+ }
325
+ });
326
+ }
327
+
328
+ // ──────────────────────────────────────────────────────────────────────────
329
+ // Public API — login / logout / status
330
+ // ──────────────────────────────────────────────────────────────────────────
331
+
332
+ async function runAgentLogin({ objectId, name }) {
333
+ const workspaceConfig = await readWorkspaceConfig();
334
+ const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
335
+ if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
336
+ if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
337
+
338
+ const { spec, agentHost } = assertAgentHostEligible(row, { requireLogin: true });
339
+
340
+ const binary = resolveHostBinary(row, spec);
341
+ const cwd = resolveCwd(row);
342
+ const startedAt = Date.now();
343
+
344
+ const result = await runCommand({
345
+ binary,
346
+ args: spec.loginCommand,
347
+ cwd,
348
+ timeoutMs: spec.loginTimeoutMs || DEFAULT_LOGIN_TIMEOUT_MS
349
+ });
350
+
351
+ const stdout = clampOutput(redactSecrets(result.stdout));
352
+ const stderr = clampOutput(redactSecrets(result.stderr));
353
+ const loginUrl = extractLoginUrl(`${result.stdout || ""}\n${result.stderr || ""}`);
354
+ const status = deriveLoginStatus(result);
355
+ const checkedAt = new Date().toISOString();
356
+
357
+ const patch = buildRowPatch({
358
+ status,
359
+ provider: agentHost,
360
+ checkedAt,
361
+ exitCode: result.exitCode,
362
+ loginUrl,
363
+ label: spec.label,
364
+ spawnError: result.spawnError
365
+ });
366
+
367
+ await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
368
+
369
+ return {
370
+ ok: status === "active",
371
+ status,
372
+ provider: agentHost,
373
+ label: spec.label,
374
+ binary,
375
+ cwd,
376
+ exitCode: result.exitCode,
377
+ timedOut: result.timedOut,
378
+ durationMs: Date.now() - startedAt,
379
+ stdout,
380
+ stderr,
381
+ loginUrl,
382
+ message: patch.agentAuthLastMessage,
383
+ checkedAt
384
+ };
385
+ }
386
+
387
+ async function runAgentLogout({ objectId, name }) {
388
+ const workspaceConfig = await readWorkspaceConfig();
389
+ const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
390
+ if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
391
+ if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
392
+
393
+ const { spec, agentHost } = assertAgentHostEligible(row, { requireLogout: true });
394
+
395
+ const binary = resolveHostBinary(row, spec);
396
+ const cwd = resolveCwd(row);
397
+ const startedAt = Date.now();
398
+
399
+ let exitCode = null;
400
+ let stdout = "";
401
+ let stderr = "";
402
+ let spawnError = null;
403
+ try {
404
+ const { stdout: out, stderr: err } = await execFileAsync(binary, spec.logoutCommand, {
405
+ cwd,
406
+ timeout: DEFAULT_LOGOUT_TIMEOUT_MS
407
+ });
408
+ exitCode = 0;
409
+ stdout = out || "";
410
+ stderr = err || "";
411
+ } catch (error) {
412
+ exitCode = typeof error?.code === "number" ? error.code : null;
413
+ stdout = error?.stdout || "";
414
+ stderr = error?.stderr || error?.message || "";
415
+ if (error?.code === "ENOENT") {
416
+ spawnError = { message: stderr, notFound: true };
417
+ }
418
+ }
419
+
420
+ const status = spawnError?.notFound ? "missing" : "stale";
421
+ const checkedAt = new Date().toISOString();
422
+
423
+ const patch = buildRowPatch({
424
+ status,
425
+ provider: agentHost,
426
+ checkedAt,
427
+ exitCode,
428
+ loginUrl: null,
429
+ label: spec.label,
430
+ spawnError
431
+ });
432
+ patch.agentAuthLastMessage = spawnError?.notFound
433
+ ? shortMessage({ status: "missing", label: spec.label })
434
+ : `${spec.label} logged out — auth will be required before next run.`;
435
+
436
+ await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
437
+
438
+ return {
439
+ ok: !spawnError,
440
+ status,
441
+ provider: agentHost,
442
+ label: spec.label,
443
+ binary,
444
+ cwd,
445
+ exitCode,
446
+ durationMs: Date.now() - startedAt,
447
+ stdout: clampOutput(redactSecrets(stdout)),
448
+ stderr: clampOutput(redactSecrets(stderr)),
449
+ message: patch.agentAuthLastMessage,
450
+ checkedAt
451
+ };
452
+ }
453
+
454
+ async function checkAgentStatus({ objectId, name }) {
455
+ const workspaceConfig = await readWorkspaceConfig();
456
+ const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
457
+ if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
458
+ if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
459
+
460
+ const { spec, agentHost } = assertAgentHostEligible(row);
461
+
462
+ const binary = resolveHostBinary(row, spec);
463
+ const cwd = resolveCwd(row);
464
+
465
+ // Two-phase probe:
466
+ // 1. If the catalog declares an auth-status subcommand, try it first.
467
+ // A clean exit lets us label the row as "active".
468
+ // 2. Always fall back to the catalog versionProbe — exit 0 maps to
469
+ // "reachable", NEVER "active". This is the same contract for every
470
+ // host so the UI mental model stays uniform.
471
+ let status = null;
472
+ let usedProbe = "version";
473
+ let usedResult = null;
474
+
475
+ if (Array.isArray(spec.authStatusProbe) && spec.authStatusProbe.length) {
476
+ const authStatusResult = await runCommand({
477
+ binary,
478
+ args: spec.authStatusProbe,
479
+ cwd,
480
+ timeoutMs: DEFAULT_PROBE_TIMEOUT_MS
481
+ });
482
+ status = deriveStatusFromAuthStatusProbe(authStatusResult);
483
+ if (status !== null) {
484
+ usedProbe = "auth-status";
485
+ usedResult = authStatusResult;
486
+ }
487
+ }
488
+
489
+ if (status === null) {
490
+ const versionResult = await runCommand({
491
+ binary,
492
+ args: spec.versionProbe || ["--version"],
493
+ cwd,
494
+ timeoutMs: DEFAULT_PROBE_TIMEOUT_MS
495
+ });
496
+ status = deriveStatusFromVersionProbe(versionResult);
497
+ usedResult = versionResult;
498
+ }
499
+
500
+ const checkedAt = new Date().toISOString();
501
+ const stdout = clampOutput(redactSecrets(usedResult.stdout));
502
+ const stderr = clampOutput(redactSecrets(usedResult.stderr));
503
+
504
+ const patch = buildRowPatch({
505
+ status,
506
+ provider: agentHost,
507
+ checkedAt,
508
+ exitCode: usedResult.exitCode,
509
+ loginUrl: null,
510
+ label: spec.label,
511
+ spawnError: usedResult.spawnError
512
+ });
513
+
514
+ await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
515
+
516
+ return {
517
+ ok: status === "active",
518
+ status,
519
+ provider: agentHost,
520
+ label: spec.label,
521
+ binary,
522
+ cwd,
523
+ exitCode: usedResult.exitCode,
524
+ probe: usedProbe,
525
+ stdout,
526
+ stderr,
527
+ message: patch.agentAuthLastMessage,
528
+ checkedAt
529
+ };
530
+ }
531
+
532
+ // ──────────────────────────────────────────────────────────────────────────
533
+ // Row patch — safe metadata only
534
+ // ──────────────────────────────────────────────────────────────────────────
535
+
536
+ function buildRowPatch({ status, provider, checkedAt, exitCode, loginUrl, label, spawnError }) {
537
+ const safe = {
538
+ agentAuthStatus: KNOWN_AGENT_AUTH_STATUSES.includes(status) ? status : "unknown",
539
+ agentAuthProvider: String(provider || "").trim() || "unknown",
540
+ agentAuthLastChecked: checkedAt,
541
+ agentAuthLastExitCode: typeof exitCode === "number" ? exitCode : null,
542
+ agentAuthLastMessage: shortMessage({
543
+ status,
544
+ label,
545
+ exitCode,
546
+ loginUrl,
547
+ error: spawnError?.message
548
+ }),
549
+ agentAuthLastLoginUrl: loginUrl || ""
550
+ };
551
+ for (const key of Object.keys(safe)) {
552
+ if (!SAFE_ROW_PATCH_FIELDS.includes(key)) delete safe[key];
553
+ }
554
+ return safe;
555
+ }
556
+
557
+ async function applyRowPatch({ workspaceConfig, object, rowIndex, patch }) {
558
+ const persistence = describePersistenceMode();
559
+ if (!persistence.canSave) return false;
560
+ try {
561
+ const objects = Array.isArray(workspaceConfig.dataModel?.objects)
562
+ ? workspaceConfig.dataModel.objects
563
+ : [];
564
+ const nextObjects = objects.map((entry) => {
565
+ if (entry.id !== object.id) return entry;
566
+ const rows = Array.isArray(entry.rows) ? entry.rows : [];
567
+ const nextRows = rows.map((existingRow, index) => {
568
+ if (index !== rowIndex) return existingRow;
569
+ return { ...existingRow, ...patch };
570
+ });
571
+ return { ...entry, rows: nextRows };
572
+ });
573
+ await writeWorkspaceConfig({
574
+ dataModel: { ...(workspaceConfig.dataModel || {}), objects: nextObjects }
575
+ });
576
+ return true;
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+
582
+ function notFoundError(message) {
583
+ const error = new Error(message);
584
+ error.code = "SANDBOX_AGENT_AUTH_NOT_FOUND";
585
+ return error;
586
+ }
587
+
588
+ // ──────────────────────────────────────────────────────────────────────────
589
+ // Workspace Metadata Graph V1 — safe agent host readiness metadata.
590
+ //
591
+ // Pure helper that distills a sandbox row into the safe metadata the
592
+ // workspace metadata graph + UI inspector can show. Reads ONLY the safe,
593
+ // allowlisted readiness fields the auth helper persists on the row. Never
594
+ // echoes raw tokens, login URLs, or stdout/stderr — those live inside the
595
+ // helper's response object, not the row patch.
596
+ // ──────────────────────────────────────────────────────────────────────────
597
+
598
+ function describeAgentHostReadinessMetadata(row) {
599
+ if (!row || typeof row !== "object") return null;
600
+ const adapter = String(row?.adapter || "").trim();
601
+ const agentHost = String(row?.agentHost || "").trim();
602
+ const runLocality = String(row?.runLocality || "").trim();
603
+ const safe = {};
604
+ for (const key of SAFE_ROW_PATCH_FIELDS) {
605
+ if (Object.prototype.hasOwnProperty.call(row, key)) {
606
+ safe[key] = row[key];
607
+ }
608
+ }
609
+ return {
610
+ kind: "workspaceAgentHostReadiness",
611
+ adapter,
612
+ agentHost,
613
+ runLocality,
614
+ status: KNOWN_AGENT_AUTH_STATUSES.includes(safe.agentAuthStatus) ? safe.agentAuthStatus : "unknown",
615
+ provider: String(safe.agentAuthProvider || agentHost || "unknown").trim(),
616
+ lastChecked: String(safe.agentAuthLastChecked || "").trim(),
617
+ lastExitCode: typeof safe.agentAuthLastExitCode === "number" ? safe.agentAuthLastExitCode : null,
618
+ lastMessage: String(safe.agentAuthLastMessage || "").trim(),
619
+ lastLoginUrl: String(safe.agentAuthLastLoginUrl || "").trim()
620
+ };
621
+ }
622
+
623
+ // ──────────────────────────────────────────────────────────────────────────
624
+ // Backwards-compatible Claude aliases (legacy)
625
+ // ──────────────────────────────────────────────────────────────────────────
626
+
627
+ const runClaudeLogin = runAgentLogin;
628
+ const runClaudeLogout = runAgentLogout;
629
+ const checkClaudeStatus = checkAgentStatus;
630
+ function assertClaudeLocalEligible(row) {
631
+ const { spec, agentHost } = assertAgentHostEligible(row);
632
+ if (agentHost !== "claude_local") {
633
+ const error = new Error(
634
+ `Expected agentHost "claude_local", got "${agentHost}"`
635
+ );
636
+ error.code = "SANDBOX_AGENT_AUTH_HOST_MISMATCH";
637
+ throw error;
638
+ }
639
+ return { spec, agentHost };
640
+ }
641
+ function resolveClaudeBinary(row) {
642
+ const spec = getHostAuthSpec(String(row?.agentHost || "claude_local").trim()) || getHostAuthSpec("claude_local");
643
+ return resolveHostBinary(row, spec);
644
+ }
645
+
646
+ export {
647
+ KNOWN_AGENT_AUTH_STATUSES,
648
+ SAFE_ROW_PATCH_FIELDS,
649
+ assertAgentHostEligible,
650
+ assertClaudeLocalEligible,
651
+ buildRowPatch,
652
+ checkAgentStatus,
653
+ checkClaudeStatus,
654
+ describeAgentHostReadinessMetadata,
655
+ findSandboxRow,
656
+ getAgentHostCapabilities,
657
+ redactSecrets,
658
+ resolveCwd,
659
+ resolveHostBinary,
660
+ resolveClaudeBinary,
661
+ runAgentLogin,
662
+ runAgentLogout,
663
+ runClaudeLogin,
664
+ runClaudeLogout
665
+ };