@growthub/cli 0.14.3 → 0.14.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 (29) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/resolvers/[integrationId]/route.js +157 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +5 -1
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +33 -1
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +86 -4
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +30 -5
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +2 -2
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +532 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +400 -188
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +36 -5
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +1 -1
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +1 -1
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +1 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +9 -1
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +14 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +24 -19
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-agent-teams.js +211 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-bootstrap-console.js +325 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-cockpit-console.js +206 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +7 -82
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/resolver-constructor.js +217 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-registry.js +99 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/unified-resolver-registry.js +545 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +2 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +30 -2
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +69 -0
  26. package/package.json +2 -2
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +0 -141
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +0 -64
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +0 -376
@@ -0,0 +1,157 @@
1
+ /**
2
+ * GET /api/resolvers/[integrationId]
3
+ *
4
+ * CMS SDK v1.5.1 — the governed, addressable endpoint every registered resolver
5
+ * is exposed at. A resolver is the workspace's provider-agnostic "API → governed
6
+ * rows" abstraction; this makes it a real, hittable Next.js route inside the
7
+ * monorepo, so other apps and external callers consume governed records by
8
+ * integrationId. One dynamic route serves every resolver (a projection of the
9
+ * unified registry) — runtime-agnostic and drift-free by construction.
10
+ *
11
+ * Identity: the path segment is matched canonically. The governed record keeps
12
+ * its human integrationId; the resolver registers under either the raw id
13
+ * (config-driven/Nango) or the slug (generated), so lookup tries the raw value
14
+ * then the slug — `/api/resolvers/my-crm` and `/api/resolvers/asana` both resolve.
15
+ *
16
+ * Same-level governance as every mutation/execution route (Governed Application
17
+ * Control Plane V1): `x-growthub-app-scope` is runtime-enforced, scope rejections
18
+ * emit a canonical outcome receipt, and under a scope the 404 body does NOT leak
19
+ * the list of other registered integrations. Secret-safe: tokens never leave the
20
+ * server, and error messages are redacted.
21
+ *
22
+ * Response — success: { ok, integrationId, resolverId, recordRef, connectorKind, recordCount, records }
23
+ * Response — no resolver: 404 { ok:false, reason:"no-resolver", registeredResolvers? }
24
+ * Response — scope violation: 422 AppScopeViolation
25
+ * Response — resolver error: 502 { ok:false, reason:"fetch-error", error }
26
+ */
27
+
28
+ import { NextResponse } from "next/server";
29
+ import { requireAppScope, checkScopedRegistryAccess } from "@/lib/workspace-app-registry";
30
+ import { readWorkspaceConfig } from "@/lib/workspace-config";
31
+ import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
32
+ import { readAdapterConfig } from "@/lib/adapters/env";
33
+ import { listGovernedWorkspaceIntegrations } from "@/lib/adapters/integrations";
34
+ import { loadAllResolvers } from "@/lib/adapters/integrations/resolver-loader";
35
+ import { getSourceResolver, listRegisteredResolvers } from "@/lib/adapters/integrations/source-resolver-registry";
36
+ import { slugifyIntegrationId } from "@/lib/unified-resolver-registry";
37
+
38
+ const MAX_LIMIT = 200;
39
+ const DEFAULT_LIMIT = 50;
40
+
41
+ /** Strip anything secret-shaped from an error string before it leaves the server. */
42
+ function redact(message) {
43
+ return String(message || "")
44
+ .replace(/(authorization|bearer|api[_-]?key|token|secret|password)\s*[:=]\s*\S+/gi, "$1 [redacted]")
45
+ .replace(/\bBearer\s+[A-Za-z0-9._-]+/gi, "Bearer [redacted]")
46
+ .slice(0, 300);
47
+ }
48
+
49
+ /** Find the governed api-registry row backing an integrationId (raw or slug). */
50
+ function findRecordRef(workspaceConfig, integrationId) {
51
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
52
+ const wantSlug = slugifyIntegrationId(integrationId, "");
53
+ for (const object of objects) {
54
+ if (object?.objectType !== "api-registry") continue;
55
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
56
+ const id = String(row?.integrationId || "").trim();
57
+ if (!id) continue;
58
+ if (id === integrationId || slugifyIntegrationId(id, "") === wantSlug) {
59
+ return { objectId: String(object.id || ""), rowName: String(row.Name || id), integrationId: id };
60
+ }
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ async function GET(request, context) {
67
+ const params = await context?.params;
68
+ const rawId = String(params?.integrationId || "").trim();
69
+ if (!rawId) {
70
+ return NextResponse.json({ ok: false, reason: "bad-request", error: "integrationId is required" }, { status: 400 });
71
+ }
72
+ const slugId = slugifyIntegrationId(rawId, "");
73
+
74
+ const workspaceConfig = await readWorkspaceConfig().catch(() => ({}));
75
+
76
+ // App-scope gate — data-plane isolation. Checked against both forms so a scoped
77
+ // agent cannot dodge scope by varying slug/raw. The 404 path below is also
78
+ // scope-aware so it never leaks the registered-integration list under a scope.
79
+ const scope = requireAppScope(request, workspaceConfig);
80
+ if (scope.scoped) {
81
+ const violation =
82
+ scope.violation ||
83
+ checkScopedRegistryAccess(scope.context, rawId) && checkScopedRegistryAccess(scope.context, slugId);
84
+ if (violation) {
85
+ await appendOutcomeReceipt({
86
+ kind: "agent-outcome",
87
+ lane: "untrusted-direct",
88
+ outcomeStatus: "blocked",
89
+ appId: violation.appScope || scope.appId,
90
+ summary: `resolver endpoint rejected (422 app scope): ${violation.violationType}`,
91
+ nextActions: violation.repairPlan,
92
+ });
93
+ return NextResponse.json(violation, { status: 422 });
94
+ }
95
+ }
96
+
97
+ await loadAllResolvers();
98
+ const resolver = getSourceResolver(rawId) || (slugId ? getSourceResolver(slugId) : null);
99
+ if (!resolver) {
100
+ const body = { ok: false, reason: "no-resolver", integrationId: rawId, resolverId: slugId };
101
+ // Only an UNSCOPED caller gets the discovery aid — under a scope this would
102
+ // leak other apps' integration ids.
103
+ if (!scope.scoped) {
104
+ body.registeredResolvers = listRegisteredResolvers();
105
+ if (slugId && slugId !== rawId && body.registeredResolvers.includes(slugId)) {
106
+ body.hint = `No resolver for "${rawId}". Did you mean the canonical id "${slugId}"?`;
107
+ } else {
108
+ body.hint = "Register this API in the Data Model API Registry cockpit and construct its resolver, then re-call this endpoint.";
109
+ }
110
+ }
111
+ return NextResponse.json(body, { status: 404 });
112
+ }
113
+
114
+ const { searchParams } = new URL(request.url);
115
+ const limitRaw = Number(searchParams.get("limit"));
116
+ const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, MAX_LIMIT) : DEFAULT_LIMIT;
117
+
118
+ const adapterConfig = readAdapterConfig();
119
+ let connection = null;
120
+ try {
121
+ const integrations = await listGovernedWorkspaceIntegrations();
122
+ connection = integrations.find((i) => i.provider === rawId || i.id === rawId || i.provider === slugId) || null;
123
+ } catch {
124
+ // Non-fatal — resolver falls back to env-only auth.
125
+ }
126
+
127
+ let records;
128
+ try {
129
+ records = await resolver.fetchRecords(adapterConfig, connection, {});
130
+ } catch (err) {
131
+ return NextResponse.json(
132
+ { ok: false, reason: "fetch-error", integrationId: rawId, error: redact(err?.message) || "resolver.fetchRecords threw" },
133
+ { status: 502 },
134
+ );
135
+ }
136
+
137
+ if (!Array.isArray(records)) {
138
+ return NextResponse.json(
139
+ { ok: false, reason: "bad-resolver-response", integrationId: rawId, error: "resolver.fetchRecords must return an array" },
140
+ { status: 502 },
141
+ );
142
+ }
143
+
144
+ return NextResponse.json({
145
+ ok: true,
146
+ integrationId: rawId,
147
+ resolverId: slugId,
148
+ // Downstream apps/agents know exactly which governed row they consumed.
149
+ recordRef: findRecordRef(workspaceConfig, rawId),
150
+ source: "resolver-endpoint",
151
+ connectorKind: typeof resolver.connectorKind === "string" ? resolver.connectorKind : null,
152
+ recordCount: records.length,
153
+ records: records.slice(0, limit),
154
+ });
155
+ }
156
+
157
+ export { GET };
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { NextResponse } from "next/server";
12
- import { readWorkspaceConfig } from "@/lib/workspace-config";
12
+ import { readWorkspaceConfig, describePersistenceMode } from "@/lib/workspace-config";
13
13
  import { computeConfiguredEnvRefs, listPersistenceAdapterReadiness } from "@/lib/env-status";
14
14
 
15
15
  async function GET() {
@@ -21,10 +21,14 @@ async function GET() {
21
21
  }
22
22
  const configuredEnvRefs = computeConfiguredEnvRefs(workspaceConfig, process.env);
23
23
  const persistenceAdapters = listPersistenceAdapterReadiness(process.env);
24
+ // Persistence mode + canSave so the scheduler provisioning cockpit can honestly
25
+ // gate the "set it up for me" action (server-file writes need a writable runtime).
26
+ const persistence = describePersistenceMode();
24
27
  return NextResponse.json({
25
28
  kind: "growthub-env-status-v1",
26
29
  configuredEnvRefs,
27
30
  persistenceAdapters,
31
+ persistence: { mode: persistence.mode, canSave: persistence.canSave === true },
28
32
  });
29
33
  }
30
34
 
@@ -56,6 +56,10 @@ import {
56
56
  findSwarmRunRows,
57
57
  summarizeSwarmRunProposal,
58
58
  } from "@/lib/workspace-swarm-proposal";
59
+ import {
60
+ CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE,
61
+ buildCeoBootstrapCompletion,
62
+ } from "@/lib/ceo-bootstrap-console";
59
63
 
60
64
  const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
61
65
 
@@ -267,8 +271,14 @@ async function POST(request) {
267
271
  );
268
272
  const resolverProposals = normalizedProposals.filter((p) => p?.type === RESOLVER_PROPOSAL_TYPE);
269
273
  const swarmProposals = normalizedProposals.filter((p) => SWARM_PROPOSAL_TYPES.includes(p?.type));
274
+ const ceoBootstrapProposals = normalizedProposals.filter(
275
+ (p) => p?.type === CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE
276
+ );
270
277
  const configProposals = normalizedProposals.filter(
271
- (p) => p?.type !== RESOLVER_PROPOSAL_TYPE && !SWARM_PROPOSAL_TYPES.includes(p?.type)
278
+ (p) =>
279
+ p?.type !== RESOLVER_PROPOSAL_TYPE &&
280
+ !SWARM_PROPOSAL_TYPES.includes(p?.type) &&
281
+ p?.type !== CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE
272
282
  );
273
283
 
274
284
  for (const proposal of resolverProposals) {
@@ -310,6 +320,28 @@ async function POST(request) {
310
320
  });
311
321
  }
312
322
 
323
+ // CEO bootstrap lane — stamps the "CEO setup complete" marker onto the
324
+ // well-known workspace-helper sandbox row in the EXISTING dataModel patch
325
+ // field. The builder refuses unless the loop is config-provably done (a
326
+ // ready swarm with a completed run), so completion is evidence, not a
327
+ // client assertion. No new object, no execution.
328
+ for (const proposal of ceoBootstrapProposals) {
329
+ const result = buildCeoBootstrapCompletion({
330
+ workspaceConfig: workingConfig,
331
+ completedAt: appliedAt,
332
+ completedBy: reviewedBy || "user",
333
+ });
334
+ if (!result.ok) {
335
+ skipped.push({ proposal, reason: result.error || "CEO bootstrap not ready to complete" });
336
+ continue;
337
+ }
338
+ workingConfig = result.config;
339
+ applied.push({
340
+ ...buildApplyReceipt({ ...proposal, affectedField: "dataModel" }, appliedAt, reviewedBy, sessionId),
341
+ summary: "CEO setup marked complete",
342
+ });
343
+ }
344
+
313
345
  for (const proposal of configProposals) {
314
346
  if (
315
347
  !proposal ||
@@ -6,6 +6,13 @@
6
6
  * Used by the generic resolver management panel and ResolverControlPanel in the
7
7
  * widget inspector. No provider names appear in the response shape.
8
8
  *
9
+ * CMS SDK v1.5.1 — additionally returns `registry`: the Unified API Resolver
10
+ * Registry index that correlates every governed `api-registry` record to its
11
+ * resolver (provenance, file, registered/tested state, response shape, score,
12
+ * next action, governed endpoint). The legacy fields are preserved verbatim.
13
+ * When the runtime is writable, the externalized index + endpoint manifest
14
+ * artifacts are written through so agents can read one file out of band.
15
+ *
9
16
  * Response:
10
17
  * {
11
18
  * files: string[],
@@ -16,25 +23,100 @@
16
23
  * hasListEntities: boolean,
17
24
  * configSchema: SchemaField[] | null
18
25
  * }[],
19
- * canUpload: boolean
26
+ * canUpload: boolean,
27
+ * registry: ResolverRegistryIndex // @growthub/api-contract/resolver-registry
20
28
  * }
21
29
  */
22
30
 
23
31
  import { NextResponse } from "next/server";
24
32
  import { loadAllResolvers, listResolverFiles } from "@/lib/adapters/integrations/resolver-loader";
25
33
  import { describeRegisteredResolvers } from "@/lib/adapters/integrations/source-resolver-registry";
26
- import { describePersistenceMode } from "@/lib/workspace-config";
34
+ import { describePersistenceMode, readWorkspaceConfig, readWorkspaceSourceRecords } from "@/lib/workspace-config";
35
+ import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
36
+ import { computeConfiguredEnvRefs } from "@/lib/env-status";
37
+ import { deriveResolverRegistry } from "@/lib/unified-resolver-registry";
38
+ import { readResolverFileMeta, persistResolverRegistryArtifacts } from "@/lib/server-resolver-registry";
27
39
 
28
40
  async function GET() {
29
41
  await loadAllResolvers();
30
42
  const files = await listResolverFiles();
31
43
  const resolvers = describeRegisteredResolvers();
32
44
  const persistence = describePersistenceMode();
45
+ const registeredIds = resolvers.map((r) => r.integrationId);
46
+
47
+ // Unified registry derivation — correlate every governed record to its
48
+ // resolver. All IO happens here; the deriver stays pure. Derivation failure is
49
+ // NEVER silently hidden: the response carries an explicit `registryStatus` so
50
+ // an agent/client can distinguish "no entries" from "registry failed", and a
51
+ // writable runtime's artifact-write outcome is reported, not swallowed.
52
+ let registry = null;
53
+ let registryStatus = "ok";
54
+ let registryError = null;
55
+ let artifactWritten = false;
56
+ let artifactReason = null;
57
+ try {
58
+ const [workspaceConfig, sourceRecords, fileMeta] = await Promise.all([
59
+ readWorkspaceConfig().catch(() => ({})),
60
+ readWorkspaceSourceRecords().then((r) => r || {}).catch(() => ({})),
61
+ readResolverFileMeta().catch(() => ({})),
62
+ ]);
63
+ let configuredEnvRefs = [];
64
+ try {
65
+ configuredEnvRefs = computeConfiguredEnvRefs(workspaceConfig) || [];
66
+ } catch {
67
+ configuredEnvRefs = [];
68
+ }
69
+ registry = deriveResolverRegistry({
70
+ workspaceConfig,
71
+ files,
72
+ registeredIds,
73
+ fileMeta,
74
+ sourceRecords,
75
+ runtime: { configuredEnvRefs },
76
+ });
77
+ // Write-through the externalized artifacts when writable. On a writable
78
+ // runtime a failed write is a real problem (stale projections) — surface it
79
+ // and emit a governance receipt; on read-only it is expected (live-only).
80
+ if (persistence.canSave) {
81
+ const result = await persistResolverRegistryArtifacts(registry).catch((err) => ({
82
+ written: false,
83
+ reason: err?.message || "artifact write threw",
84
+ }));
85
+ artifactWritten = Boolean(result?.written);
86
+ artifactReason = result?.reason || null;
87
+ if (!artifactWritten) {
88
+ await appendOutcomeReceipt({
89
+ kind: "agent-outcome",
90
+ lane: "server-authoritative",
91
+ outcomeStatus: "failed",
92
+ summary: `resolver registry artifact write failed: ${artifactReason || "unknown"}`,
93
+ }).catch(() => {});
94
+ }
95
+ } else {
96
+ artifactReason = persistence.reason || "read-only runtime — registry is live-only";
97
+ }
98
+ } catch (err) {
99
+ registry = null;
100
+ registryStatus = "degraded";
101
+ registryError = { reason: "derivation-failed", message: String(err?.message || err).slice(0, 300) };
102
+ await appendOutcomeReceipt({
103
+ kind: "agent-outcome",
104
+ lane: "server-authoritative",
105
+ outcomeStatus: "failed",
106
+ summary: `resolver registry derivation failed: ${registryError.message}`,
107
+ }).catch(() => {});
108
+ }
109
+
33
110
  return NextResponse.json({
34
111
  files,
35
- registeredIds: resolvers.map((r) => r.integrationId),
112
+ registeredIds,
36
113
  resolvers,
37
- canUpload: persistence.canSave
114
+ canUpload: persistence.canSave,
115
+ registry,
116
+ registryStatus,
117
+ registryError,
118
+ artifactWritten,
119
+ artifactReason,
38
120
  });
39
121
  }
40
122
 
@@ -56,14 +56,17 @@ export function ApiRegistryCreationCockpit({
56
56
  onCollapsedChange,
57
57
  }) {
58
58
  const [collapsed, setCollapsed] = useState(defaultCollapsed);
59
+ const hasVisibleAction = Array.isArray(state?.steps) && state.steps.some((step) => step?.action);
60
+ const shouldHide = Boolean(hideWhenComplete && state?.complete && !hasVisibleAction);
59
61
  useEffect(() => {
60
- setCollapsed(defaultCollapsed || Boolean(hideWhenComplete && state?.complete));
61
- }, [defaultCollapsed, hideWhenComplete, state?.complete, state?.integrationId]);
62
+ setCollapsed(defaultCollapsed || Boolean(hideWhenComplete && state?.complete && !hasVisibleAction));
63
+ }, [defaultCollapsed, hideWhenComplete, state?.complete, state?.integrationId, hasVisibleAction]);
62
64
  if (!state || !Array.isArray(state.steps)) return null;
63
- if (hideWhenComplete && state.complete) return null;
65
+ if (shouldHide) return null;
64
66
  const candidates = profile?.candidates || {};
65
67
  const candidateEntries = Object.entries(candidates).filter(([, v]) => v);
66
68
  const previewRow = dataSourcePreview?.row || null;
69
+ const workflowAction = state?.workflowAction || null;
67
70
  const toggleCollapsed = () => {
68
71
  const next = !collapsed;
69
72
  setCollapsed(next);
@@ -77,6 +80,28 @@ export function ApiRegistryCreationCockpit({
77
80
  onAction?.(action);
78
81
  };
79
82
 
83
+ if (hideWhenComplete && state.complete && workflowAction) {
84
+ return (
85
+ <section className="dm-api-action-card dm-api-action-card-workflow" aria-label="Workflow canvas">
86
+ <div className="dm-api-action-card-body">
87
+ <p className="dm-api-action-card-eyebrow">Workflow canvas</p>
88
+ <h3>Use this API in a workflow</h3>
89
+ <p>{workflowAction.description}</p>
90
+ </div>
91
+ <div className="dm-api-action-card-actions">
92
+ <button
93
+ type="button"
94
+ className="dm-btn-outline dm-api-action-card-cta"
95
+ disabled={disabled || Boolean(busyAction)}
96
+ onClick={() => runAction(workflowAction)}
97
+ >
98
+ {busyAction === `workflow:${workflowAction.id}` ? "Opening…" : workflowAction.label}
99
+ </button>
100
+ </div>
101
+ </section>
102
+ );
103
+ }
104
+
80
105
  return (
81
106
  <section className={`dm-api-action-card dm-cockpit${collapsed ? " is-collapsed" : ""}`} aria-label="API creation journey">
82
107
  <button
@@ -180,11 +205,11 @@ export function ApiRegistryCreationCockpit({
180
205
  </div>
181
206
  ) : null}
182
207
 
183
- {!collapsed && Array.isArray(receipts) && receipts.length ? (
208
+ {!collapsed && Array.isArray(receipts) && receipts.some((r) => r.ok) ? (
184
209
  <div className="dm-cockpit-receipts">
185
210
  <p className="dm-api-action-card-eyebrow">Receipts</p>
186
211
  <ul>
187
- {receipts.slice(0, 6).map((r, i) => (
212
+ {receipts.filter((r) => r.ok).slice(0, 6).map((r, i) => (
188
213
  <li key={`${r.at}-${i}`} className="dm-cockpit-receipt">
189
214
  <StatusChip mod={r.ok ? "ok" : "bad"} className="dm-cockpit-receipt-chip">{r.kind}</StatusChip>
190
215
  <span className="dm-cockpit-receipt-text">{r.detail}</span>
@@ -4,7 +4,7 @@ import { CheckCircle2, X } from "lucide-react";
4
4
 
5
5
  /**
6
6
  * Read-only summary of a successfully tested API Registry row.
7
- * Shown at the top of the sandbox-tool draft flow (not a blocking modal).
7
+ * Shown at the top of the workflow draft flow (not a blocking modal).
8
8
  */
9
9
  export function ApiRegistryReviewModal({ registryRow, onClose = null }) {
10
10
  if (!registryRow) return null;
@@ -22,7 +22,7 @@ export function ApiRegistryReviewModal({ registryRow, onClose = null }) {
22
22
  <p className="dm-api-review-banner-eyebrow">Connected API</p>
23
23
  <h3>{registryRow.Name || integrationId}</h3>
24
24
  <p>
25
- This endpoint returned a valid response. You can now turn it into a sandbox tool.
25
+ This endpoint returned a valid response. You can now use it in a workflow canvas.
26
26
  </p>
27
27
  <code className="dm-api-review-banner-route">
28
28
  {method} {baseUrl && endpoint ? `${baseUrl.replace(/\/+$/, "")}/${endpoint.replace(/^\/+/, "")}` : endpoint || baseUrl}