@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +24 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +14 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +74 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +72 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +123 -27
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +224 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +754 -92
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +224 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +32 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +530 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +10 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/RunSetupPanel.jsx +261 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +119 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +779 -138
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +91 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +28 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +366 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +34 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-eligibility.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-redaction.js +64 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +665 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-host-catalog.js +168 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +595 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +164 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +11 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +111 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +14 -0
- 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
|
+
};
|