@growthub/cli 0.13.1 → 0.13.4
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/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 +48 -3
- 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/OrchestrationNodeConfigPanel.jsx +136 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +713 -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/globals.css +514 -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 +72 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +778 -140
- 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-graph-runner.js +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +384 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +323 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +32 -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 +629 -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 +542 -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-schema.js +111 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +9 -0
- package/package.json +1 -1
|
@@ -265,10 +265,152 @@ function normalizeFieldSettings(fieldSettings, columns) {
|
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Resolve a sidecar source-records entry for a live-backed Data Model object.
|
|
270
|
+
*
|
|
271
|
+
* The sidecar (`growthub.source-records.json`) is keyed by `sourceId` — the
|
|
272
|
+
* canonical workspace-source-records key — and the live-backed object can
|
|
273
|
+
* carry that key in three places. We try them in safe fallback order so
|
|
274
|
+
* older configs continue to hydrate without any migration:
|
|
275
|
+
*
|
|
276
|
+
* 1. `object.id` ← refresh-sources writes use object.id as the key
|
|
277
|
+
* 2. `object.sourceId` ← explicit column on the live-backed row
|
|
278
|
+
* 3. `object.binding.sourceId`← canonical binding-level reference
|
|
279
|
+
*
|
|
280
|
+
* Returns `null` when no records are available, so callers fall back to the
|
|
281
|
+
* config-owned `object.rows[]`.
|
|
282
|
+
*/
|
|
283
|
+
function findSourceRecordsForObject(object, sourceRecords) {
|
|
284
|
+
if (!sourceRecords || typeof sourceRecords !== "object" || Array.isArray(sourceRecords)) return null;
|
|
285
|
+
const candidates = [object?.id, object?.sourceId, object?.binding?.sourceId]
|
|
286
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
287
|
+
.filter(Boolean);
|
|
288
|
+
for (const key of candidates) {
|
|
289
|
+
const entry = sourceRecords[key];
|
|
290
|
+
if (entry && Array.isArray(entry.records)) return { key, entry };
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Infer a lightweight field type for a column from existing rows.
|
|
297
|
+
*
|
|
298
|
+
* This is a runtime projection — it does not persist anywhere and does not
|
|
299
|
+
* require a schema migration. The Chart Hydration Inspector and Twenty-style
|
|
300
|
+
* field-type-aware controls (date granularity, ratio mode, prefix/suffix)
|
|
301
|
+
* read it to decide which UI affordances to show.
|
|
302
|
+
*
|
|
303
|
+
* Returns one of: text | number | boolean | date | select | multi-value |
|
|
304
|
+
* relation-like | json.
|
|
305
|
+
*
|
|
306
|
+
* `existingTypeHints` is the per-table `fieldSettings.types` map — when
|
|
307
|
+
* present it wins over inference, so operators who have already declared a
|
|
308
|
+
* type on the Data Model object don't get overridden by the runtime guesser.
|
|
309
|
+
*/
|
|
310
|
+
function inferFieldType(column, rows, existingTypeHints) {
|
|
311
|
+
const hint = existingTypeHints && typeof existingTypeHints === "object" ? existingTypeHints[column] : "";
|
|
312
|
+
if (typeof hint === "string" && hint.trim()) return hint.trim();
|
|
313
|
+
if (!Array.isArray(rows) || rows.length === 0) return "text";
|
|
314
|
+
|
|
315
|
+
const lower = String(column || "").toLowerCase();
|
|
316
|
+
if (/(^|_)(id|_id)$/.test(lower) || lower.endsWith("ref") || lower === "registryid") return "relation-like";
|
|
317
|
+
if (lower.includes("date") || lower.includes("_at") || lower === "created" || lower === "updated" || lower.endsWith("at")) return "date";
|
|
318
|
+
|
|
319
|
+
let numeric = 0;
|
|
320
|
+
let boolean = 0;
|
|
321
|
+
let date = 0;
|
|
322
|
+
let multi = 0;
|
|
323
|
+
let json = 0;
|
|
324
|
+
let nonEmpty = 0;
|
|
325
|
+
const distinct = new Set();
|
|
326
|
+
for (const row of rows) {
|
|
327
|
+
const value = row?.[column];
|
|
328
|
+
if (value === undefined || value === null || value === "") continue;
|
|
329
|
+
nonEmpty += 1;
|
|
330
|
+
if (typeof value === "boolean") boolean += 1;
|
|
331
|
+
if (typeof value === "number" && Number.isFinite(value)) numeric += 1;
|
|
332
|
+
if (typeof value === "string") {
|
|
333
|
+
const trimmed = value.trim();
|
|
334
|
+
const asNumber = Number(trimmed.replace(/,/g, ""));
|
|
335
|
+
if (Number.isFinite(asNumber) && /^-?\d+(\.\d+)?$/.test(trimmed.replace(/,/g, ""))) numeric += 1;
|
|
336
|
+
if (trimmed === "true" || trimmed === "false") boolean += 1;
|
|
337
|
+
if (!Number.isNaN(Date.parse(trimmed)) && /\d{4}/.test(trimmed)) date += 1;
|
|
338
|
+
if (trimmed.includes(",") && trimmed.split(",").length > 1) multi += 1;
|
|
339
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) json += 1;
|
|
340
|
+
distinct.add(trimmed);
|
|
341
|
+
} else if (Array.isArray(value)) {
|
|
342
|
+
multi += 1;
|
|
343
|
+
} else if (typeof value === "object") {
|
|
344
|
+
json += 1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!nonEmpty) return "text";
|
|
348
|
+
if (json / nonEmpty > 0.5) return "json";
|
|
349
|
+
if (date / nonEmpty > 0.6) return "date";
|
|
350
|
+
if (boolean / nonEmpty > 0.6) return "boolean";
|
|
351
|
+
if (numeric / nonEmpty > 0.6) return "number";
|
|
352
|
+
if (multi / nonEmpty > 0.5) return "multi-value";
|
|
353
|
+
// Low-cardinality string columns look like select fields.
|
|
354
|
+
if (nonEmpty >= 3 && distinct.size > 0 && distinct.size <= Math.max(2, Math.floor(nonEmpty / 2))) return "select";
|
|
355
|
+
return "text";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildFieldMetadata(columns, rows, existingTypeHints) {
|
|
359
|
+
return (Array.isArray(columns) ? columns : []).map((column) => {
|
|
360
|
+
const type = inferFieldType(column, rows, existingTypeHints);
|
|
361
|
+
return {
|
|
362
|
+
id: column,
|
|
363
|
+
label: column,
|
|
364
|
+
type,
|
|
365
|
+
isNumeric: type === "number",
|
|
366
|
+
isDate: type === "date",
|
|
367
|
+
isBoolean: type === "boolean",
|
|
368
|
+
isSelectLike: type === "select",
|
|
369
|
+
isMultiValue: type === "multi-value",
|
|
370
|
+
isRelationLike: type === "relation-like",
|
|
371
|
+
isJson: type === "json"
|
|
372
|
+
};
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function deriveManualObjectTable(object, options = {}) {
|
|
269
377
|
const columns = Array.isArray(object.columns) ? object.columns.filter(Boolean) : [];
|
|
270
|
-
const
|
|
378
|
+
const configRows = Array.isArray(object.rows)
|
|
379
|
+
? object.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row))
|
|
380
|
+
: [];
|
|
271
381
|
const source = object.source || object.label || object.name || "Manual object";
|
|
382
|
+
|
|
383
|
+
let hydratedRows = configRows;
|
|
384
|
+
let hydratedColumns = columns;
|
|
385
|
+
let liveSource = null;
|
|
386
|
+
const isLiveBacked = object?.binding?.sourceStorage === "workspace-source-records";
|
|
387
|
+
if (isLiveBacked) {
|
|
388
|
+
const sourceRecords = options?.sourceRecords;
|
|
389
|
+
const match = findSourceRecordsForObject(object, sourceRecords);
|
|
390
|
+
if (match) {
|
|
391
|
+
const records = match.entry.records.filter((row) => row && typeof row === "object" && !Array.isArray(row));
|
|
392
|
+
hydratedRows = records;
|
|
393
|
+
if (!columns.length && records.length) {
|
|
394
|
+
const inferred = new Set();
|
|
395
|
+
records.forEach((row) => Object.keys(row).forEach((key) => inferred.add(key)));
|
|
396
|
+
hydratedColumns = Array.from(inferred);
|
|
397
|
+
}
|
|
398
|
+
liveSource = {
|
|
399
|
+
sourceRecordKey: match.key,
|
|
400
|
+
fetchedAt: typeof match.entry.fetchedAt === "string" ? match.entry.fetchedAt : null,
|
|
401
|
+
recordCount: Number.isFinite(match.entry.recordCount) ? match.entry.recordCount : records.length,
|
|
402
|
+
integrationId: typeof match.entry.integrationId === "string" ? match.entry.integrationId : null
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const fieldSettings = normalizeFieldSettings(object.fieldSettings, hydratedColumns);
|
|
408
|
+
const sourceBadge = (() => {
|
|
409
|
+
if (isLiveBacked) return "live";
|
|
410
|
+
if (object?.objectType === "data-source") return "api";
|
|
411
|
+
if (object?.objectType === "api-registry") return "api";
|
|
412
|
+
return "manual";
|
|
413
|
+
})();
|
|
272
414
|
return {
|
|
273
415
|
id: `manual-object:${object.id || source}`,
|
|
274
416
|
label: object.label || object.name || source,
|
|
@@ -276,15 +418,18 @@ function deriveManualObjectTable(object) {
|
|
|
276
418
|
objectType: object.objectType || "custom",
|
|
277
419
|
icon: object.icon || null,
|
|
278
420
|
pickerHidden: Boolean(object.pickerHidden),
|
|
279
|
-
columns,
|
|
280
|
-
rows,
|
|
421
|
+
columns: hydratedColumns,
|
|
422
|
+
rows: hydratedRows,
|
|
281
423
|
binding: object.binding || { mode: "manual", source: "Data Model" },
|
|
282
424
|
relations: Array.isArray(object.relations) ? object.relations : [],
|
|
283
425
|
mutable: true,
|
|
284
426
|
storage: "manual-object",
|
|
285
427
|
objectId: object.id,
|
|
286
428
|
widgetRefs: [],
|
|
287
|
-
fieldSettings
|
|
429
|
+
fieldSettings,
|
|
430
|
+
liveSource,
|
|
431
|
+
sourceBadge,
|
|
432
|
+
fieldMetadata: buildFieldMetadata(hydratedColumns, hydratedRows, fieldSettings?.types)
|
|
288
433
|
};
|
|
289
434
|
}
|
|
290
435
|
|
|
@@ -304,7 +449,19 @@ const HIDDEN_HELPER_OBJECT_IDS = new Set([
|
|
|
304
449
|
"nav-folders",
|
|
305
450
|
]);
|
|
306
451
|
|
|
307
|
-
|
|
452
|
+
/**
|
|
453
|
+
* List the workspace Data Model tables — the union of manual `dataModel.objects[]`
|
|
454
|
+
* and the widget-derived tables surfaced for the Data Model browser.
|
|
455
|
+
*
|
|
456
|
+
* @param {object} workspaceConfig — the persisted `growthub.config.json` shape.
|
|
457
|
+
* @param {object} [options]
|
|
458
|
+
* @param {object} [options.sourceRecords] — the sidecar map read from
|
|
459
|
+
* `growthub.source-records.json`. Live-backed objects (binding.sourceStorage
|
|
460
|
+
* === "workspace-source-records") use this to hydrate runtime rows without
|
|
461
|
+
* mutating the persisted config. Pass `undefined` to disable hydration; the
|
|
462
|
+
* config-owned `object.rows[]` is the fallback in either case.
|
|
463
|
+
*/
|
|
464
|
+
function listWorkspaceDataModelTables(workspaceConfig, options = {}) {
|
|
308
465
|
const widgetEntries = listWidgetEntries(workspaceConfig);
|
|
309
466
|
const refsByObjectId = widgetEntries.reduce((map, { widget, location }) => {
|
|
310
467
|
const binding = widget?.config?.binding;
|
|
@@ -321,7 +478,7 @@ function listWorkspaceDataModelTables(workspaceConfig) {
|
|
|
321
478
|
const manualObjects = normalizeManualObjects(workspaceConfig)
|
|
322
479
|
.filter((object) => !HIDDEN_HELPER_OBJECT_IDS.has(object?.id))
|
|
323
480
|
.map((object) => {
|
|
324
|
-
const table = deriveManualObjectTable(object);
|
|
481
|
+
const table = deriveManualObjectTable(object, options);
|
|
325
482
|
return { ...table, widgetRefs: refsByObjectId.get(object.id) || [] };
|
|
326
483
|
});
|
|
327
484
|
const widgetTables = widgetEntries
|
|
@@ -185,6 +185,17 @@ function buildStableSystemPrompt(intent) {
|
|
|
185
185
|
`Known object types: ${KNOWN_OBJECT_TYPES.join(", ")}`,
|
|
186
186
|
`PATCH allowlist (only these top-level keys can be mutated): ${PATCH_ALLOWLIST.join(", ")}`,
|
|
187
187
|
"",
|
|
188
|
+
"## When configuring dashboard widgets",
|
|
189
|
+
"- Use Data Model objects as source authority.",
|
|
190
|
+
"- Bind widgets by objectId and source metadata.",
|
|
191
|
+
"- For charts, compute widget.config.values from rows.",
|
|
192
|
+
"- Never copy source rows into chart widget config.",
|
|
193
|
+
"- Never store secrets in widget config, Data Model rows, source records, browser state, localStorage, or exported templates.",
|
|
194
|
+
"- Use source records for live-backed data.",
|
|
195
|
+
"- Use filters, grouping, aggregation, style, and values projection as nested widget config only.",
|
|
196
|
+
"- Reset invalid axis, filter, group, and sort settings when source changes.",
|
|
197
|
+
"- Mark recomputed values as unsaved unless PATCH succeeds.",
|
|
198
|
+
"",
|
|
188
199
|
"## Valid proposal types and their target patch field",
|
|
189
200
|
WORKSPACE_HELPER_PROPOSAL_TYPES.map(
|
|
190
201
|
(t) => ` ${t} → ${PROPOSAL_TYPE_TO_PATCH_FIELD[t]}`
|
|
@@ -46,7 +46,25 @@ const KNOWN_CHART_TYPES = ["bar-vertical", "bar-horizontal", "line", "pie", "sum
|
|
|
46
46
|
const KNOWN_FILTER_OPERATORS = ["eq", "ne", "contains", "gt", "lt", "isEmpty", "isNotEmpty"];
|
|
47
47
|
const KNOWN_FILTER_CONJUNCTIONS = ["and", "or"];
|
|
48
48
|
const KNOWN_SORT_DIRECTIONS = ["asc", "desc"];
|
|
49
|
-
|
|
49
|
+
// Aggregation vocabulary kept in sync with `lib/workspace-chart-values.js`.
|
|
50
|
+
// V1 charts use the first five; the Twenty-style row-presence operations
|
|
51
|
+
// (countAll/countEmpty/countNotEmpty/countUnique, percentEmpty/percentNotEmpty)
|
|
52
|
+
// are valid both as `yAxis.aggregation` (legacy key) and `yAxis.operation`
|
|
53
|
+
// (preferred key going forward). The validator accepts both for back-compat.
|
|
54
|
+
const KNOWN_AGGREGATIONS = [
|
|
55
|
+
"sum",
|
|
56
|
+
"avg",
|
|
57
|
+
"count",
|
|
58
|
+
"countAll",
|
|
59
|
+
"countEmpty",
|
|
60
|
+
"countNotEmpty",
|
|
61
|
+
"countUnique",
|
|
62
|
+
"percentEmpty",
|
|
63
|
+
"percentNotEmpty",
|
|
64
|
+
"min",
|
|
65
|
+
"max"
|
|
66
|
+
];
|
|
67
|
+
const KNOWN_DATE_GRANULARITIES = ["day", "week", "month", "quarter", "year"];
|
|
50
68
|
const KNOWN_SANDBOX_RUNTIMES = ["python", "node", "bash"];
|
|
51
69
|
/** Where execution is delegated: locally (process / agent-host CLI) or to a scheduler webhook (Supabase Edge, QStash, Vercel cron hitting your URL, etc.). */
|
|
52
70
|
const KNOWN_SANDBOX_RUN_LOCALITY = ["local", "serverless"];
|
|
@@ -612,12 +630,26 @@ function validateChartAxis(axis, path, errors) {
|
|
|
612
630
|
if (axis.aggregation !== undefined && !KNOWN_AGGREGATIONS.includes(axis.aggregation)) {
|
|
613
631
|
errors.push(`${path}.aggregation must be one of ${KNOWN_AGGREGATIONS.join(", ")}`);
|
|
614
632
|
}
|
|
633
|
+
// `operation` is the Twenty-style preferred key; accepted in addition to
|
|
634
|
+
// `aggregation` so older configs round-trip cleanly.
|
|
635
|
+
if (axis.operation !== undefined && !KNOWN_AGGREGATIONS.includes(axis.operation)) {
|
|
636
|
+
errors.push(`${path}.operation must be one of ${KNOWN_AGGREGATIONS.join(", ")}`);
|
|
637
|
+
}
|
|
615
638
|
if (axis.groupBy !== undefined && typeof axis.groupBy !== "string") {
|
|
616
639
|
errors.push(`${path}.groupBy must be a string`);
|
|
617
640
|
}
|
|
618
641
|
if (axis.omitZero !== undefined && typeof axis.omitZero !== "boolean") {
|
|
619
642
|
errors.push(`${path}.omitZero must be a boolean`);
|
|
620
643
|
}
|
|
644
|
+
if (axis.cumulative !== undefined && typeof axis.cumulative !== "boolean") {
|
|
645
|
+
errors.push(`${path}.cumulative must be a boolean`);
|
|
646
|
+
}
|
|
647
|
+
if (axis.splitMultiValueFields !== undefined && typeof axis.splitMultiValueFields !== "boolean") {
|
|
648
|
+
errors.push(`${path}.splitMultiValueFields must be a boolean`);
|
|
649
|
+
}
|
|
650
|
+
if (axis.dateGranularity !== undefined && !KNOWN_DATE_GRANULARITIES.includes(axis.dateGranularity)) {
|
|
651
|
+
errors.push(`${path}.dateGranularity must be one of ${KNOWN_DATE_GRANULARITIES.join(", ")}`);
|
|
652
|
+
}
|
|
621
653
|
if (axis.min !== undefined && typeof axis.min !== "string" && typeof axis.min !== "number") {
|
|
622
654
|
errors.push(`${path}.min must be a string or number`);
|
|
623
655
|
}
|
|
@@ -635,12 +667,33 @@ function validateChartStyle(style, path, errors) {
|
|
|
635
667
|
if (style.colors !== undefined && typeof style.colors !== "string") {
|
|
636
668
|
errors.push(`${path}.colors must be a string`);
|
|
637
669
|
}
|
|
670
|
+
if (style.manualColor !== undefined && typeof style.manualColor !== "string") {
|
|
671
|
+
errors.push(`${path}.manualColor must be a string`);
|
|
672
|
+
}
|
|
638
673
|
if (style.axisName !== undefined && typeof style.axisName !== "string") {
|
|
639
674
|
errors.push(`${path}.axisName must be a string`);
|
|
640
675
|
}
|
|
641
676
|
if (style.dataLabels !== undefined && typeof style.dataLabels !== "boolean") {
|
|
642
677
|
errors.push(`${path}.dataLabels must be a boolean`);
|
|
643
678
|
}
|
|
679
|
+
if (style.legend !== undefined && typeof style.legend !== "boolean") {
|
|
680
|
+
errors.push(`${path}.legend must be a boolean`);
|
|
681
|
+
}
|
|
682
|
+
if (style.stacked !== undefined && typeof style.stacked !== "boolean") {
|
|
683
|
+
errors.push(`${path}.stacked must be a boolean`);
|
|
684
|
+
}
|
|
685
|
+
if (style.compact !== undefined && typeof style.compact !== "boolean") {
|
|
686
|
+
errors.push(`${path}.compact must be a boolean`);
|
|
687
|
+
}
|
|
688
|
+
if (style.prefix !== undefined && typeof style.prefix !== "string") {
|
|
689
|
+
errors.push(`${path}.prefix must be a string`);
|
|
690
|
+
}
|
|
691
|
+
if (style.suffix !== undefined && typeof style.suffix !== "string") {
|
|
692
|
+
errors.push(`${path}.suffix must be a string`);
|
|
693
|
+
}
|
|
694
|
+
if (style.centerValue !== undefined && typeof style.centerValue !== "string") {
|
|
695
|
+
errors.push(`${path}.centerValue must be a string`);
|
|
696
|
+
}
|
|
644
697
|
}
|
|
645
698
|
|
|
646
699
|
function validateWidgetConfig(kind, config, path, errors) {
|
|
@@ -1003,6 +1056,63 @@ function validateSandboxEnvironmentRow(row, path, errors) {
|
|
|
1003
1056
|
errors.push(`${path}.${traceField} must be a string when present`);
|
|
1004
1057
|
}
|
|
1005
1058
|
}
|
|
1059
|
+
// Sandbox Local Agent Auth Onboarding V1 — governance for the safe auth
|
|
1060
|
+
// metadata fields stamped by the auth helper / API routes.
|
|
1061
|
+
const KNOWN_AGENT_AUTH_STATUSES_INLINE = [
|
|
1062
|
+
"active",
|
|
1063
|
+
"reachable",
|
|
1064
|
+
"stale",
|
|
1065
|
+
"missing",
|
|
1066
|
+
"checking",
|
|
1067
|
+
"unknown"
|
|
1068
|
+
];
|
|
1069
|
+
if (row.agentAuthStatus !== undefined && row.agentAuthStatus !== "" && row.agentAuthStatus !== null) {
|
|
1070
|
+
const authStatus = String(row.agentAuthStatus).trim().toLowerCase();
|
|
1071
|
+
if (!KNOWN_AGENT_AUTH_STATUSES_INLINE.includes(authStatus)) {
|
|
1072
|
+
errors.push(`${path}.agentAuthStatus must be one of ${KNOWN_AGENT_AUTH_STATUSES_INLINE.join(", ")}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
for (const authField of [
|
|
1076
|
+
"agentAuthProvider",
|
|
1077
|
+
"agentAuthLastChecked",
|
|
1078
|
+
"agentAuthLastMessage",
|
|
1079
|
+
"agentAuthLastLoginUrl"
|
|
1080
|
+
]) {
|
|
1081
|
+
const value = row[authField];
|
|
1082
|
+
if (value !== undefined && value !== null && value !== "" && typeof value !== "string") {
|
|
1083
|
+
errors.push(`${path}.${authField} must be a string when present`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (
|
|
1087
|
+
row.agentAuthLastExitCode !== undefined
|
|
1088
|
+
&& row.agentAuthLastExitCode !== null
|
|
1089
|
+
&& row.agentAuthLastExitCode !== ""
|
|
1090
|
+
) {
|
|
1091
|
+
const code = Number(row.agentAuthLastExitCode);
|
|
1092
|
+
if (!Number.isFinite(code)) {
|
|
1093
|
+
errors.push(`${path}.agentAuthLastExitCode must be a finite number when present`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// Defensive: refuse to ever persist token-shaped field names on a sandbox
|
|
1097
|
+
// row. The helper only writes the SAFE_ROW_PATCH_FIELDS whitelist, but
|
|
1098
|
+
// this guard catches any out-of-band PATCH that tries to stash a secret.
|
|
1099
|
+
const FORBIDDEN_AUTH_ROW_FIELDS = [
|
|
1100
|
+
"token",
|
|
1101
|
+
"apiKey",
|
|
1102
|
+
"authToken",
|
|
1103
|
+
"accessToken",
|
|
1104
|
+
"refreshToken",
|
|
1105
|
+
"bearer",
|
|
1106
|
+
"password",
|
|
1107
|
+
"secret",
|
|
1108
|
+
"sessionKey",
|
|
1109
|
+
"claudeToken"
|
|
1110
|
+
];
|
|
1111
|
+
for (const forbidden of FORBIDDEN_AUTH_ROW_FIELDS) {
|
|
1112
|
+
if (Object.prototype.hasOwnProperty.call(row, forbidden)) {
|
|
1113
|
+
errors.push(`${path}.${forbidden} is not allowed on a sandbox row — auth secrets must stay in the local CLI's own store`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1006
1116
|
}
|
|
1007
1117
|
|
|
1008
1118
|
const NAV_FOLDERS_OBJECT_ID = "nav-folders";
|
|
@@ -88,9 +88,17 @@
|
|
|
88
88
|
"apps/workspace/app/api/workspace/resolvers/route.js",
|
|
89
89
|
"apps/workspace/app/api/workspace/sandbox-adapters/route.js",
|
|
90
90
|
"apps/workspace/app/api/workspace/sandbox-run/route.js",
|
|
91
|
+
"apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js",
|
|
92
|
+
"apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js",
|
|
93
|
+
"apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js",
|
|
91
94
|
"apps/workspace/app/api/settings/integrations/route.js",
|
|
92
95
|
"apps/workspace/lib/workspace-schema.js",
|
|
93
96
|
"apps/workspace/lib/workspace-config.js",
|
|
97
|
+
"apps/workspace/lib/workspace-chart-values.js",
|
|
98
|
+
"apps/workspace/lib/sandbox-agent-auth.js",
|
|
99
|
+
"apps/workspace/lib/sandbox-agent-auth-eligibility.js",
|
|
100
|
+
"apps/workspace/lib/sandbox-agent-auth-redaction.js",
|
|
101
|
+
"apps/workspace/lib/sandbox-agent-host-catalog.js",
|
|
94
102
|
"apps/workspace/lib/domain/portal.js",
|
|
95
103
|
"apps/workspace/lib/domain/integrations.js",
|
|
96
104
|
"apps/workspace/lib/adapters/env.js",
|
|
@@ -115,6 +123,7 @@
|
|
|
115
123
|
"apps/workspace/app/data-model/page.jsx",
|
|
116
124
|
"apps/workspace/app/data-model/components/DataModelShell.jsx",
|
|
117
125
|
"apps/workspace/app/data-model/components/HelperSidecar.jsx",
|
|
126
|
+
"apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx",
|
|
118
127
|
"apps/workspace/app/views/[viewId]/page.jsx",
|
|
119
128
|
"apps/workspace/lib/adapters/payments/index.js",
|
|
120
129
|
"apps/workspace/lib/adapters/persistence/index.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthub/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "CLI control plane for Growthub Local and Agent Workspace as Code: export, fork, inspect, operate, sync, and optionally activate governed AI workspaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|