@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,366 @@
1
+ /**
2
+ * Live Runs Console V2 — manual run input contract.
3
+ *
4
+ * Pure helper for discovering, validating, normalizing, and redacting the
5
+ * manual run-input envelope used by form / human-input nodes in the
6
+ * orchestration graph.
7
+ *
8
+ * Contract:
9
+ *
10
+ * {
11
+ * "kind": "growthub-workflow-run-inputs-v1",
12
+ * "source": "manual",
13
+ * "values": { "<fieldId>": "<safe string>" },
14
+ * "files": [{ "id": "<attachmentId>", "name": "...", "size": <bytes> }]
15
+ * }
16
+ *
17
+ * Invariants:
18
+ * - No React, no fetch, no workspace mutation.
19
+ * - Secret-looking values (Bearer tokens, api_key= patterns, password=, etc.)
20
+ * are redacted before display and before persistence.
21
+ * - Field IDs marked as `secret` are stripped to `secretRef` only — raw
22
+ * values are never accepted from the browser into a run record.
23
+ * - Backward compatible: workflows without human-input nodes return
24
+ * { requiresInput: false } and the rest of the pipeline behaves as it
25
+ * did in V1.
26
+ */
27
+
28
+ import { parseOrchestrationGraph, redactSecretsFromText } from "./orchestration-graph.js";
29
+
30
+ const RUN_INPUTS_KIND = "growthub-workflow-run-inputs-v1";
31
+ const MAX_RUN_INPUT_VALUES = 64;
32
+ const MAX_RUN_INPUT_FIELD_BYTES = 8 * 1024;
33
+ const MAX_RUN_INPUT_TOTAL_BYTES = 64 * 1024;
34
+ const MAX_RUN_INPUT_FILES = 16;
35
+
36
+ const KNOWN_FIELD_TYPES = new Set([
37
+ "text",
38
+ "string",
39
+ "email",
40
+ "url",
41
+ "number",
42
+ "integer",
43
+ "textarea",
44
+ "json",
45
+ "boolean",
46
+ "checkbox",
47
+ "secretRef",
48
+ "secret-ref",
49
+ "select"
50
+ ]);
51
+
52
+ const SECRETISH_KEYS = /^(api[_-]?key|token|password|secret|authorization|bearer)$/i;
53
+
54
+ function safeString(value) {
55
+ if (value == null) return "";
56
+ return typeof value === "string" ? value : String(value);
57
+ }
58
+
59
+ function byteLength(value) {
60
+ try {
61
+ return Buffer.byteLength(safeString(value), "utf8");
62
+ } catch {
63
+ return safeString(value).length;
64
+ }
65
+ }
66
+
67
+ function parseFieldDescriptor(entry) {
68
+ if (!entry || typeof entry !== "object") return null;
69
+ const idRaw = safeString(entry.key || entry.id || entry.name).trim();
70
+ if (!idRaw) return null;
71
+ const id = idRaw.replace(/\s+/g, "_");
72
+ const descriptor = safeString(entry.value ?? entry.type ?? entry.help).trim();
73
+ const lower = descriptor.toLowerCase();
74
+ let type = "text";
75
+ let helpText = "";
76
+ if (KNOWN_FIELD_TYPES.has(lower)) {
77
+ type = lower === "secret-ref" ? "secretRef" : lower;
78
+ } else if (descriptor) {
79
+ helpText = descriptor;
80
+ }
81
+ const isSecret = type === "secretRef" || SECRETISH_KEYS.test(id);
82
+ return {
83
+ id,
84
+ label: safeString(entry.label || idRaw).trim() || id,
85
+ type: isSecret ? "secretRef" : type,
86
+ required: entry.required !== false && entry.optional !== true,
87
+ helpText,
88
+ isSecret
89
+ };
90
+ }
91
+
92
+ function discoverRunInputSchema(graphValue) {
93
+ const graph = parseOrchestrationGraph(graphValue) || graphValue || null;
94
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
95
+ const fields = [];
96
+ const seen = new Set();
97
+ let requiresInput = false;
98
+ let title = "";
99
+ let instructions = "";
100
+
101
+ for (const node of nodes) {
102
+ const type = safeString(node?.type).trim();
103
+ const action = safeString(node?.config?.action).trim();
104
+ const isHumanInputForm = type === "human-input" || action === "form";
105
+ if (!isHumanInputForm) continue;
106
+ const config = node?.config && typeof node.config === "object" ? node.config : {};
107
+ if (!title) title = safeString(config.title).trim();
108
+ if (!instructions) instructions = safeString(config.instructions).trim();
109
+ const requiredFlag = config.required !== false && config.requiresInput !== false;
110
+ if (requiredFlag) requiresInput = true;
111
+ const rawFields = Array.isArray(config.fields) ? config.fields : [];
112
+ for (const raw of rawFields) {
113
+ const descriptor = parseFieldDescriptor(raw);
114
+ if (!descriptor) continue;
115
+ if (seen.has(descriptor.id)) continue;
116
+ seen.add(descriptor.id);
117
+ if (!requiredFlag) descriptor.required = false;
118
+ fields.push(descriptor);
119
+ }
120
+ }
121
+
122
+ return {
123
+ kind: RUN_INPUTS_KIND,
124
+ requiresInput: requiresInput && fields.length > 0,
125
+ title: title || "Run inputs",
126
+ instructions,
127
+ fields
128
+ };
129
+ }
130
+
131
+ function coerceFieldValue(field, raw) {
132
+ if (raw == null) return "";
133
+ if (field?.isSecret || field?.type === "secretRef") {
134
+ if (raw && typeof raw === "object" && "secretRef" in raw) return raw;
135
+ return safeString(raw);
136
+ }
137
+ if (field.type === "boolean" || field.type === "checkbox") {
138
+ if (typeof raw === "boolean") return raw;
139
+ const text = safeString(raw).trim().toLowerCase();
140
+ return ["1", "true", "yes", "on"].includes(text);
141
+ }
142
+ if (field.type === "number" || field.type === "integer") {
143
+ const num = Number(raw);
144
+ return Number.isFinite(num) ? num : "";
145
+ }
146
+ if (field.type === "json") {
147
+ if (typeof raw === "string") {
148
+ try {
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ return raw.slice(0, MAX_RUN_INPUT_FIELD_BYTES);
152
+ }
153
+ }
154
+ return raw;
155
+ }
156
+ return safeString(raw);
157
+ }
158
+
159
+ function redactValueForPersistence(field, value) {
160
+ if (field?.isSecret) {
161
+ if (value && typeof value === "object" && value.secretRef) {
162
+ return { secretRef: safeString(value.secretRef).trim() };
163
+ }
164
+ return { secretRef: "[redacted]" };
165
+ }
166
+ if (typeof value === "string") {
167
+ const truncated = value.length > MAX_RUN_INPUT_FIELD_BYTES
168
+ ? `${value.slice(0, MAX_RUN_INPUT_FIELD_BYTES)}…`
169
+ : value;
170
+ return redactSecretsFromText(truncated);
171
+ }
172
+ if (typeof value === "number" || typeof value === "boolean") return value;
173
+ if (value == null) return "";
174
+ try {
175
+ return JSON.parse(redactSecretsFromText(JSON.stringify(value)));
176
+ } catch {
177
+ return "";
178
+ }
179
+ }
180
+
181
+ function normalizeFile(entry) {
182
+ if (!entry || typeof entry !== "object") return null;
183
+ const id = safeString(entry.id || entry.attachmentId).trim();
184
+ if (!id) return null;
185
+ const name = safeString(entry.name || entry.filename || "").trim();
186
+ const size = Number(entry.size);
187
+ return {
188
+ id,
189
+ name,
190
+ size: Number.isFinite(size) && size >= 0 ? size : 0
191
+ };
192
+ }
193
+
194
+ function validateRunInputsEnvelope(envelope, schema) {
195
+ if (envelope == null) return { ok: true, missing: [], unknown: [] };
196
+ if (typeof envelope !== "object" || Array.isArray(envelope)) {
197
+ return { ok: false, error: "runInputs must be an object", missing: [], unknown: [] };
198
+ }
199
+ const knownFieldIds = new Set((schema?.fields || []).map((f) => f.id));
200
+ const values = envelope.values && typeof envelope.values === "object" && !Array.isArray(envelope.values)
201
+ ? envelope.values
202
+ : {};
203
+ const valueKeys = Object.keys(values);
204
+ if (valueKeys.length > MAX_RUN_INPUT_VALUES) {
205
+ return { ok: false, error: `runInputs.values exceeds ${MAX_RUN_INPUT_VALUES} fields`, missing: [], unknown: [] };
206
+ }
207
+ let totalBytes = 0;
208
+ for (const key of valueKeys) {
209
+ const bytes = byteLength(values[key]);
210
+ if (bytes > MAX_RUN_INPUT_FIELD_BYTES) {
211
+ return { ok: false, error: `runInputs.values["${key}"] exceeds ${MAX_RUN_INPUT_FIELD_BYTES} bytes`, missing: [], unknown: [] };
212
+ }
213
+ totalBytes += bytes;
214
+ }
215
+ if (totalBytes > MAX_RUN_INPUT_TOTAL_BYTES) {
216
+ return { ok: false, error: `runInputs.values exceeds ${MAX_RUN_INPUT_TOTAL_BYTES} bytes`, missing: [], unknown: [] };
217
+ }
218
+ const files = Array.isArray(envelope.files) ? envelope.files : [];
219
+ if (files.length > MAX_RUN_INPUT_FILES) {
220
+ return { ok: false, error: `runInputs.files exceeds ${MAX_RUN_INPUT_FILES} entries`, missing: [], unknown: [] };
221
+ }
222
+ const missing = (schema?.fields || [])
223
+ .filter((f) => f.required && !(f.id in values))
224
+ .map((f) => f.id);
225
+ const unknown = valueKeys.filter((id) => knownFieldIds.size > 0 && !knownFieldIds.has(id));
226
+ return { ok: missing.length === 0, missing, unknown };
227
+ }
228
+
229
+ function normalizeRunInputsEnvelope(value, schema) {
230
+ if (value == null) return null;
231
+ if (typeof value !== "object" || Array.isArray(value)) return null;
232
+ const schemaFields = Array.isArray(schema?.fields) ? schema.fields : [];
233
+ const fieldsById = new Map(schemaFields.map((f) => [f.id, f]));
234
+ const rawValues = value.values && typeof value.values === "object" && !Array.isArray(value.values)
235
+ ? value.values
236
+ : {};
237
+ const safeValues = {};
238
+ for (const [key, raw] of Object.entries(rawValues)) {
239
+ const id = safeString(key).trim();
240
+ if (!id) continue;
241
+ const field = fieldsById.get(id) || parseFieldDescriptor({ key: id, value: "" });
242
+ const coerced = coerceFieldValue(field, raw);
243
+ safeValues[id] = redactValueForPersistence(field, coerced);
244
+ }
245
+ const rawFiles = Array.isArray(value.files) ? value.files : [];
246
+ const files = rawFiles.map(normalizeFile).filter(Boolean).slice(0, MAX_RUN_INPUT_FILES);
247
+ const source = safeString(value.source || "manual").trim() || "manual";
248
+ return {
249
+ kind: RUN_INPUTS_KIND,
250
+ source,
251
+ values: safeValues,
252
+ files
253
+ };
254
+ }
255
+
256
+ function redactRunInputsEnvelope(value) {
257
+ if (!value || typeof value !== "object") return null;
258
+ const fields = Array.isArray(value.fields) ? value.fields : [];
259
+ const fieldsById = new Map(fields.map((f) => [f.id || f.key, f]));
260
+ const out = {
261
+ kind: safeString(value.kind || RUN_INPUTS_KIND).trim() || RUN_INPUTS_KIND,
262
+ source: safeString(value.source || "manual").trim() || "manual",
263
+ values: {},
264
+ files: []
265
+ };
266
+ const rawValues = value.values && typeof value.values === "object" && !Array.isArray(value.values)
267
+ ? value.values
268
+ : {};
269
+ for (const [key, raw] of Object.entries(rawValues)) {
270
+ const field = fieldsById.get(key) || { id: key, isSecret: SECRETISH_KEYS.test(key) };
271
+ out.values[key] = redactValueForPersistence(field, raw);
272
+ }
273
+ const rawFiles = Array.isArray(value.files) ? value.files : [];
274
+ out.files = rawFiles.map(normalizeFile).filter(Boolean);
275
+ return out;
276
+ }
277
+
278
+ function summarizeRunInputs(value) {
279
+ if (!value || typeof value !== "object") {
280
+ return { source: "", fieldCount: 0, fileCount: 0, fieldIds: [] };
281
+ }
282
+ const values = value.values && typeof value.values === "object" && !Array.isArray(value.values)
283
+ ? value.values
284
+ : {};
285
+ const files = Array.isArray(value.files) ? value.files : [];
286
+ const fieldIds = Object.keys(values);
287
+ return {
288
+ source: safeString(value.source || "manual").trim() || "manual",
289
+ fieldCount: fieldIds.length,
290
+ fileCount: files.length,
291
+ fieldIds
292
+ };
293
+ }
294
+
295
+ function buildInputPayloadForRunner(envelope) {
296
+ if (!envelope || typeof envelope !== "object") return {};
297
+ const values = envelope.values && typeof envelope.values === "object" && !Array.isArray(envelope.values)
298
+ ? envelope.values
299
+ : {};
300
+ const out = {};
301
+ for (const [key, raw] of Object.entries(values)) {
302
+ if (raw && typeof raw === "object" && "secretRef" in raw) {
303
+ continue;
304
+ }
305
+ out[key] = raw;
306
+ }
307
+ return out;
308
+ }
309
+
310
+ /**
311
+ * Workspace Metadata Graph V1 — typed run-input descriptors.
312
+ *
313
+ * Returns a metadata-compatible list:
314
+ * { id, label, type, required, secretRefOnly, sourceNodeId, workflowId }
315
+ *
316
+ * Used by the metadata store + workflow sidecar so the manual run input
317
+ * schema is a single typed contract — never re-derived inside UI code.
318
+ *
319
+ * Existing redaction + size limits remain enforced by
320
+ * `validateRunInputsEnvelope` / `normalizeRunInputsEnvelope`. This helper
321
+ * is descriptor-only and does not mutate or echo any field value.
322
+ */
323
+ function describeRunInputMetadataItems({ workflowId, graph, objectId, rowId } = {}) {
324
+ const schema = discoverRunInputSchema(graph);
325
+ const fields = Array.isArray(schema?.fields) ? schema.fields : [];
326
+ const workflowKey = String(workflowId || (objectId && rowId ? `${objectId}::${rowId}` : "")).trim();
327
+ const sourceNodeId = findFirstHumanInputNodeId(graph);
328
+ return fields.map((field) => ({
329
+ kind: "workspaceRunInput",
330
+ id: field.id,
331
+ label: field.label,
332
+ type: field.type,
333
+ required: Boolean(field.required),
334
+ isSecret: Boolean(field.isSecret),
335
+ secretRefOnly: Boolean(field.isSecret) || field.type === "secretRef",
336
+ sourceNodeId,
337
+ workflowId: workflowKey
338
+ }));
339
+ }
340
+
341
+ function findFirstHumanInputNodeId(graphValue) {
342
+ const graph = parseOrchestrationGraph(graphValue) || graphValue || null;
343
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
344
+ for (const node of nodes) {
345
+ const type = String(node?.type || "").trim();
346
+ const action = String(node?.config?.action || "").trim();
347
+ if (type === "human-input" || action === "form") return String(node?.id || "").trim() || "human-input";
348
+ }
349
+ return "";
350
+ }
351
+
352
+ export {
353
+ RUN_INPUTS_KIND,
354
+ MAX_RUN_INPUT_VALUES,
355
+ MAX_RUN_INPUT_FIELD_BYTES,
356
+ MAX_RUN_INPUT_TOTAL_BYTES,
357
+ MAX_RUN_INPUT_FILES,
358
+ discoverRunInputSchema,
359
+ normalizeRunInputsEnvelope,
360
+ redactRunInputsEnvelope,
361
+ summarizeRunInputs,
362
+ validateRunInputsEnvelope,
363
+ buildInputPayloadForRunner,
364
+ describeRunInputMetadataItems,
365
+ parseFieldDescriptor
366
+ };
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { redactSecretsFromText } from "./orchestration-graph.js";
6
+ import { redactRunInputsEnvelope, summarizeRunInputs } from "./orchestration-run-inputs.js";
6
7
 
7
8
  function safeParseJson(text) {
8
9
  const raw = String(text || "").trim();
@@ -31,10 +32,15 @@ function parseSandboxRunTrace(lastResponse) {
31
32
  output: "",
32
33
  ranAt: "",
33
34
  envRefsResolved: [],
34
- envRefsMissing: []
35
+ envRefsMissing: [],
36
+ input: null,
37
+ inputSummary: null,
38
+ exports: null
35
39
  };
36
40
  }
37
41
  const outputRaw = parsed.output ?? parsed.normalizedOutput ?? parsed.response;
42
+ const inputRaw = parsed.input || parsed.runInputs || null;
43
+ const safeInput = inputRaw ? redactRunInputsEnvelope(inputRaw) : null;
38
44
  return {
39
45
  status: parsed.status || (parsed.exitCode === 0 && !parsed.error ? "connected" : parsed.error ? "failed" : ""),
40
46
  runId: String(parsed.runId || "").trim(),
@@ -51,12 +57,25 @@ function parseSandboxRunTrace(lastResponse) {
51
57
  ),
52
58
  ranAt: String(parsed.ranAt || "").trim(),
53
59
  envRefsResolved: Array.isArray(parsed.envRefsResolved) ? parsed.envRefsResolved : [],
54
- envRefsMissing: Array.isArray(parsed.envRefsMissing) ? parsed.envRefsMissing : []
60
+ envRefsMissing: Array.isArray(parsed.envRefsMissing) ? parsed.envRefsMissing : [],
61
+ input: safeInput,
62
+ inputSummary: safeInput ? summarizeRunInputs(safeInput) : null,
63
+ exports: parsed.exports && typeof parsed.exports === "object" ? parsed.exports : null,
64
+ swarm: parsed.swarm && typeof parsed.swarm === "object" ? parsed.swarm : null,
65
+ logTree: Array.isArray(parsed.logTree) ? parsed.logTree : null
55
66
  };
56
67
  }
57
68
 
58
69
  function normalizeRunRecord(record) {
59
70
  if (!record || typeof record !== "object") return null;
71
+ const outputRaw = record.output ?? record.normalizedOutput ?? record.response;
72
+ const outputText = typeof outputRaw === "string"
73
+ ? outputRaw
74
+ : outputRaw != null
75
+ ? JSON.stringify(outputRaw, null, 2)
76
+ : "";
77
+ const inputRaw = record.input || record.runInputs || null;
78
+ const safeInput = inputRaw ? redactRunInputsEnvelope(inputRaw) : null;
60
79
  return {
61
80
  runId: String(record.runId || "").trim(),
62
81
  ranAt: String(record.ranAt || "").trim(),
@@ -65,8 +84,20 @@ function normalizeRunRecord(record) {
65
84
  error: redactSecretsFromText(record.error || ""),
66
85
  stdout: redactSecretsFromText(typeof record.stdout === "string" ? record.stdout : ""),
67
86
  stderr: redactSecretsFromText(record.stderr || ""),
87
+ output: redactSecretsFromText(outputText),
88
+ runtime: String(record.runtime || "").trim(),
89
+ adapter: String(record.adapter || "").trim(),
90
+ runLocality: String(record.runLocality || "").trim(),
91
+ status: String(record.status || "").trim(),
92
+ envRefsResolved: Array.isArray(record.envRefsResolved) ? record.envRefsResolved : [],
93
+ envRefsMissing: Array.isArray(record.envRefsMissing) ? record.envRefsMissing : [],
94
+ adapterMeta: record.adapterMeta && typeof record.adapterMeta === "object" ? record.adapterMeta : null,
95
+ templateTrace: record.templateTrace && typeof record.templateTrace === "object" ? record.templateTrace : null,
68
96
  lifecycleStatus: String(record.lifecycleStatus || "").trim(),
69
- version: String(record.version || "").trim()
97
+ version: String(record.version || "").trim(),
98
+ input: safeInput,
99
+ inputSummary: safeInput ? summarizeRunInputs(safeInput) : null,
100
+ exports: record.exports && typeof record.exports === "object" ? record.exports : null
70
101
  };
71
102
  }
72
103
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Pure eligibility predicates for the Sandbox Local Agent Auth Onboarding
3
+ * V1 panel. Extracted so it can be imported by both the React client
4
+ * component (`.jsx`) and the Node-side unit tests (`.js` — no JSX runtime,
5
+ * no DOM).
6
+ *
7
+ * The auth panel is host-agnostic: any sandbox row with
8
+ * `adapter: "local-agent-host"`, `runLocality !== "serverless"`, and an
9
+ * `agentHost` that is registered in the host auth catalog renders the
10
+ * panel. Per-host capabilities (login / logout buttons) flow from the
11
+ * catalog — this file only decides whether to render at all.
12
+ *
13
+ * No node-specific imports, no React. Safe to ship in any runtime.
14
+ */
15
+
16
+ import { KNOWN_HOST_AUTH_SLUGS } from "./sandbox-agent-host-catalog.js";
17
+
18
+ const LOCAL_AGENT_HOST_ADAPTER = "local-agent-host";
19
+ // Legacy export kept for code that hard-codes the Claude slug elsewhere.
20
+ const CLAUDE_LOCAL_HOST = "claude_local";
21
+
22
+ /**
23
+ * @returns {boolean} true when the row should surface the agent auth panel.
24
+ */
25
+ function isSandboxLocalAgentHost(row) {
26
+ if (!row || typeof row !== "object" || Array.isArray(row)) return false;
27
+ const adapter = String(row.adapter || "").trim();
28
+ const agentHost = String(row.agentHost || "").trim();
29
+ const runLocality = String(row.runLocality || "").trim().toLowerCase();
30
+ if (adapter !== LOCAL_AGENT_HOST_ADAPTER) return false;
31
+ if (!KNOWN_HOST_AUTH_SLUGS.includes(agentHost)) return false;
32
+ if (runLocality === "serverless") return false;
33
+ return true;
34
+ }
35
+
36
+ /**
37
+ * Backwards-compatible alias — Claude-specific predicate. Kept for any
38
+ * import that still references the original name; new code should use
39
+ * `isSandboxLocalAgentHost` so it works for every local-agent-host slug.
40
+ */
41
+ function isSandboxClaudeLocal(row) {
42
+ return isSandboxLocalAgentHost(row) && String(row?.agentHost || "").trim() === CLAUDE_LOCAL_HOST;
43
+ }
44
+
45
+ export {
46
+ CLAUDE_LOCAL_HOST,
47
+ LOCAL_AGENT_HOST_ADAPTER,
48
+ isSandboxClaudeLocal,
49
+ isSandboxLocalAgentHost
50
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Pure utilities for the sandbox agent auth helper.
3
+ *
4
+ * Extracted so the constants and the redaction function can be imported
5
+ * without pulling in Next.js path-aliased modules (`@/lib/workspace-config`
6
+ * etc.). Tests load this file directly to verify the runtime behaviour of
7
+ * the redaction and status taxonomy.
8
+ *
9
+ * No Next-aliased imports. No node-only APIs. Safe to ship in any runtime.
10
+ */
11
+
12
+ const KNOWN_AGENT_AUTH_STATUSES = Object.freeze([
13
+ "active", // confirmed authenticated (login exit 0, or `auth status` exit 0)
14
+ "reachable", // CLI installed and callable, but auth NOT yet confirmed
15
+ "stale", // CLI reachable but auth-shaped failure detected
16
+ "missing", // binary not found on PATH
17
+ "checking", // transient UI state during a probe
18
+ "unknown" // indeterminate
19
+ ]);
20
+
21
+ const SAFE_ROW_PATCH_FIELDS = Object.freeze([
22
+ "agentAuthStatus",
23
+ "agentAuthProvider",
24
+ "agentAuthLastChecked",
25
+ "agentAuthLastExitCode",
26
+ "agentAuthLastMessage",
27
+ "agentAuthLastLoginUrl"
28
+ ]);
29
+
30
+ const TOKEN_PATTERNS = [
31
+ /sk-ant-[A-Za-z0-9_-]{8,}/g,
32
+ /sk-[A-Za-z0-9_-]{20,}/g,
33
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
34
+ /Bearer\s+[A-Za-z0-9._-]{16,}/gi
35
+ ];
36
+
37
+ const TOKEN_PREFIX_PATTERNS = [
38
+ /(access[_-]?token["']?\s*[:=]\s*)["']?[^\s"',}]+/gi,
39
+ /(refresh[_-]?token["']?\s*[:=]\s*)["']?[^\s"',}]+/gi,
40
+ /(oauth[_-]?token["']?\s*[:=]\s*)["']?[^\s"',}]+/gi,
41
+ /(session[_-]?(?:key|token)["']?\s*[:=]\s*)["']?[^\s"',}]+/gi,
42
+ /(api[_-]?key["']?\s*[:=]\s*)["']?[^\s"',}]+/gi,
43
+ /(password["']?\s*[:=]\s*)["']?[^\s"',}]+/gi
44
+ ];
45
+
46
+ function redactSecrets(text) {
47
+ if (typeof text !== "string" || !text) return "";
48
+ let next = text;
49
+ for (const pattern of TOKEN_PATTERNS) {
50
+ next = next.replace(pattern, "[redacted]");
51
+ }
52
+ for (const pattern of TOKEN_PREFIX_PATTERNS) {
53
+ next = next.replace(pattern, "$1[redacted]");
54
+ }
55
+ return next;
56
+ }
57
+
58
+ export {
59
+ KNOWN_AGENT_AUTH_STATUSES,
60
+ SAFE_ROW_PATCH_FIELDS,
61
+ TOKEN_PATTERNS,
62
+ TOKEN_PREFIX_PATTERNS,
63
+ redactSecrets
64
+ };