@decocms/mesh-sdk 1.2.1 → 1.2.3
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/README.md +10 -10
- package/package.json +7 -4
- package/src/context/index.ts +5 -1
- package/src/context/project-context.tsx +68 -29
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-collections.ts +179 -63
- package/src/hooks/use-connection.ts +50 -4
- package/src/hooks/use-mcp-client.ts +81 -11
- package/src/hooks/use-mcp-prompts.ts +16 -6
- package/src/hooks/use-mcp-resources.ts +15 -5
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +119 -4
- package/src/lib/bridge-transport.test.ts +368 -0
- package/src/lib/bridge-transport.ts +6 -0
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +193 -36
- package/src/lib/default-model.ts +281 -0
- package/src/lib/mcp-oauth.ts +139 -17
- package/src/lib/query-keys.ts +20 -4
- package/src/lib/server-client-bridge.ts +4 -0
- package/src/lib/usage.test.ts +229 -0
- package/src/lib/usage.ts +187 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/plugin-context-provider.tsx +99 -0
- package/src/plugins/topbar-portal.tsx +118 -0
- package/src/types/ai-providers.ts +86 -0
- package/src/types/connection.ts +43 -20
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +171 -0
- package/src/types/index.ts +48 -1
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +514 -109
package/src/types/virtual-mcp.ts
CHANGED
|
@@ -8,20 +8,7 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* - "inclusion": Include selected tools/connections (default behavior)
|
|
13
|
-
* - "exclusion": Exclude selected tools/connections (inverse filter)
|
|
14
|
-
*/
|
|
15
|
-
const ToolSelectionModeSchema = z
|
|
16
|
-
.enum(["inclusion", "exclusion"])
|
|
17
|
-
.describe(
|
|
18
|
-
"Tool selection mode: 'inclusion' = include selected (default), 'exclusion' = exclude selected",
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
export type ToolSelectionMode = z.infer<typeof ToolSelectionModeSchema>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Virtual MCP connection schema - defines which connection and tools/resources/prompts are included/excluded
|
|
11
|
+
* Virtual MCP connection schema - defines which connection and tools/resources/prompts are included
|
|
25
12
|
*/
|
|
26
13
|
const VirtualMCPConnectionSchema = z.object({
|
|
27
14
|
connection_id: z.string().describe("Connection ID"),
|
|
@@ -29,68 +16,496 @@ const VirtualMCPConnectionSchema = z.object({
|
|
|
29
16
|
.array(z.string())
|
|
30
17
|
.nullable()
|
|
31
18
|
.describe(
|
|
32
|
-
"Selected tool names.
|
|
19
|
+
"Selected tool names. null = all tools included, array = only these tools included",
|
|
33
20
|
),
|
|
34
21
|
selected_resources: z
|
|
35
22
|
.array(z.string())
|
|
36
23
|
.nullable()
|
|
37
24
|
.describe(
|
|
38
|
-
"Selected resource URIs or patterns. Supports * and ** wildcards for pattern matching.
|
|
25
|
+
"Selected resource URIs or patterns. Supports * and ** wildcards for pattern matching. null = all resources included, array = only these resources included",
|
|
39
26
|
),
|
|
40
27
|
selected_prompts: z
|
|
41
28
|
.array(z.string())
|
|
42
29
|
.nullable()
|
|
43
30
|
.describe(
|
|
44
|
-
"Selected prompt names.
|
|
31
|
+
"Selected prompt names. null = all prompts included, array = only these prompts included",
|
|
45
32
|
),
|
|
46
33
|
});
|
|
47
34
|
|
|
48
35
|
export type VirtualMCPConnection = z.infer<typeof VirtualMCPConnectionSchema>;
|
|
49
36
|
|
|
50
37
|
/**
|
|
51
|
-
* Virtual MCP
|
|
52
|
-
* Compliant with collections binding pattern
|
|
38
|
+
* Virtual MCP connection schema for input (Create/Update) - fields can be optional
|
|
53
39
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
const VirtualMCPConnectionInputSchema = VirtualMCPConnectionSchema.extend({
|
|
41
|
+
selected_tools: VirtualMCPConnectionSchema.shape.selected_tools.optional(),
|
|
42
|
+
selected_resources:
|
|
43
|
+
VirtualMCPConnectionSchema.shape.selected_resources.optional(),
|
|
44
|
+
selected_prompts:
|
|
45
|
+
VirtualMCPConnectionSchema.shape.selected_prompts.optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pinned view schema - a tool view pinned to a virtual MCP
|
|
50
|
+
*/
|
|
51
|
+
const VirtualMcpPinnedViewSchema = z.object({
|
|
52
|
+
connectionId: z.string(),
|
|
53
|
+
toolName: z.string(),
|
|
54
|
+
label: z.string(),
|
|
55
|
+
icon: z.string().nullable().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export type VirtualMcpPinnedView = z.infer<typeof VirtualMcpPinnedViewSchema>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A single tab declared by an agent in `metadata.ui.layout.tabs`. Rendered
|
|
62
|
+
* after the fixed system tabs (Instructions / Connections / Layout / Env)
|
|
63
|
+
* in the unified chat layout's right panel.
|
|
64
|
+
*/
|
|
65
|
+
export const VirtualMcpUILayoutTabSchema = z.object({
|
|
66
|
+
id: z.string().describe("Stable id; used as React key and ?tab= value"),
|
|
67
|
+
title: z.string().describe("Tab label"),
|
|
68
|
+
icon: z.string().optional().describe("Optional lucide icon name"),
|
|
69
|
+
view: z.object({
|
|
70
|
+
type: z.literal("ext-app"),
|
|
71
|
+
appId: z.string(),
|
|
72
|
+
args: z.record(z.string(), z.unknown()).optional(),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type VirtualMcpUILayoutTab = z.infer<typeof VirtualMcpUILayoutTabSchema>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Layout-specific settings stored under `metadata.ui.layout`. Controls which
|
|
80
|
+
* main view opens by default and which additional right-panel tabs are
|
|
81
|
+
* permanently available for the agent.
|
|
82
|
+
*/
|
|
83
|
+
export const VirtualMcpUILayoutSchema = z.object({
|
|
84
|
+
defaultMainView: z
|
|
85
|
+
.object({
|
|
86
|
+
type: z.string(),
|
|
87
|
+
id: z.string().optional(),
|
|
88
|
+
toolName: z.string().optional(),
|
|
89
|
+
})
|
|
90
|
+
.nullable()
|
|
91
|
+
.optional(),
|
|
92
|
+
/**
|
|
93
|
+
* When true, the chat panel is open alongside the main view on first
|
|
94
|
+
* load. Ignored when `defaultMainView.type === "chat"` (chat is always
|
|
95
|
+
* open in that case). Absent / null / false → chat is closed unless the
|
|
96
|
+
* default view is chat.
|
|
97
|
+
*/
|
|
98
|
+
chatDefaultOpen: z.boolean().nullable().optional(),
|
|
99
|
+
tabs: z.array(VirtualMcpUILayoutTabSchema).optional(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export type VirtualMcpUILayout = z.infer<typeof VirtualMcpUILayoutSchema>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Tile UI declared by a home agent. When present, the `/$org` home page
|
|
106
|
+
* renders the resource as an iframe inside the agent's tile (same MCP UI
|
|
107
|
+
* iframe pattern used for tool results in chat).
|
|
108
|
+
*
|
|
109
|
+
* The resource lives on a specific underlying connection — not the virtual
|
|
110
|
+
* MCP gateway — so we store the source `connectionId` here. The host opens
|
|
111
|
+
* an MCP client to that connection directly so tool calls from inside the
|
|
112
|
+
* iframe hit bare tool names (the gateway would otherwise reject calls that
|
|
113
|
+
* don't carry its namespace prefix).
|
|
114
|
+
*/
|
|
115
|
+
const VirtualMcpHomeTileSchema = z.object({
|
|
116
|
+
/**
|
|
117
|
+
* Optional for backward compatibility with tiles saved before this field
|
|
118
|
+
* existed. The home API drops tiles that don't carry a connectionId (they
|
|
119
|
+
* can't render correctly without it), but parsing must still succeed so
|
|
120
|
+
* existing virtual MCPs don't fail output validation on COLLECTION_*_LIST.
|
|
121
|
+
*/
|
|
122
|
+
connectionId: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe(
|
|
126
|
+
"Connection that owns the resource — the host opens a direct MCP client to this connection so the iframe can call tools by their bare names.",
|
|
127
|
+
),
|
|
128
|
+
resourceUri: z
|
|
129
|
+
.string()
|
|
130
|
+
.describe(
|
|
131
|
+
"ui:// resource URI exposed by `connectionId`. Read on the home page and rendered via MCPAppRenderer.",
|
|
132
|
+
),
|
|
133
|
+
minHeight: z.number().int().positive().optional(),
|
|
134
|
+
maxHeight: z.number().int().positive().optional(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export type VirtualMcpHomeTile = z.infer<typeof VirtualMcpHomeTileSchema>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Virtual MCP UI customization schema
|
|
141
|
+
*/
|
|
142
|
+
const VirtualMcpUISchema = z.object({
|
|
143
|
+
banner: z.string().nullable().optional(),
|
|
144
|
+
bannerColor: z.string().nullable().optional(),
|
|
145
|
+
icon: z.string().nullable().optional(),
|
|
146
|
+
themeColor: z.string().nullable().optional(),
|
|
147
|
+
pinnedViews: z.array(VirtualMcpPinnedViewSchema).nullable().optional(),
|
|
148
|
+
layout: VirtualMcpUILayoutSchema.nullable().optional(),
|
|
149
|
+
/**
|
|
150
|
+
* Legacy single-tile slot. Still honored by the home-next-actions
|
|
151
|
+
* endpoint when `homeTiles` is empty/absent. New writes go to
|
|
152
|
+
* `homeTiles` so agents can surface more than one UI on the home
|
|
153
|
+
* board.
|
|
154
|
+
*/
|
|
155
|
+
homeTile: VirtualMcpHomeTileSchema.nullable().optional(),
|
|
156
|
+
/**
|
|
157
|
+
* Multiple home tiles per agent. Each entry becomes its own tile on
|
|
158
|
+
* the org home board, rendered via MCPAppRenderer against the
|
|
159
|
+
* resource's owning connection.
|
|
160
|
+
*/
|
|
161
|
+
homeTiles: z.array(VirtualMcpHomeTileSchema).nullable().optional(),
|
|
162
|
+
/**
|
|
163
|
+
* Curated list of prompt names to surface on the home board. When
|
|
164
|
+
* absent / null, the BE falls back to listing every prompt the
|
|
165
|
+
* agent's gateway exposes (today's behavior). An explicit empty
|
|
166
|
+
* array means "no prompts" — useful for an agent that only wants to
|
|
167
|
+
* surface its UI tiles.
|
|
168
|
+
*/
|
|
169
|
+
homePrompts: z.array(z.string()).nullable().optional(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export type VirtualMcpUI = z.infer<typeof VirtualMcpUISchema>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Canonical reader for an agent's pinned home tiles. Prefers the
|
|
176
|
+
* `homeTiles` array; falls back to the legacy single `homeTile` slot so
|
|
177
|
+
* agents written before the multi-tile migration still surface their
|
|
178
|
+
* tile. Returning `[]` (rather than null) keeps callers branchless.
|
|
179
|
+
*/
|
|
180
|
+
export function getHomeTiles(
|
|
181
|
+
ui: VirtualMcpUI | null | undefined,
|
|
182
|
+
): VirtualMcpHomeTile[] {
|
|
183
|
+
const arr = ui?.homeTiles;
|
|
184
|
+
if (Array.isArray(arr) && arr.length > 0) return arr;
|
|
185
|
+
const legacy = ui?.homeTile;
|
|
186
|
+
return legacy ? [legacy] : [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Shell-portable env var name: must start with a letter or underscore and
|
|
191
|
+
* contain only letters, digits, and underscores. Same shape parse-dotenv
|
|
192
|
+
* enforces on imports. Single source of truth — the form editor and the
|
|
193
|
+
* paste flow both validate against this.
|
|
194
|
+
*/
|
|
195
|
+
export const ENV_VAR_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
196
|
+
|
|
197
|
+
const envVarKey = z.string().min(1).regex(ENV_VAR_KEY_RE, {
|
|
198
|
+
message:
|
|
199
|
+
"Env var key must start with a letter or underscore and contain only letters, digits, and underscores.",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* One env var declaration on a virtual MCP. Literal values live inline in
|
|
204
|
+
* metadata; secret values store a stable secretId that mesh resolves against
|
|
205
|
+
* the credential vault on every SANDBOX_START. The env var KEY is independent of
|
|
206
|
+
* the secret's NAME — a single secret can back multiple env keys across
|
|
207
|
+
* different agents.
|
|
208
|
+
*/
|
|
209
|
+
const RuntimeEnvEntrySchema = z.discriminatedUnion("kind", [
|
|
210
|
+
z.object({
|
|
211
|
+
key: envVarKey,
|
|
212
|
+
kind: z.literal("literal"),
|
|
213
|
+
value: z.string(),
|
|
214
|
+
}),
|
|
215
|
+
z.object({
|
|
216
|
+
key: envVarKey,
|
|
217
|
+
kind: z.literal("secret"),
|
|
218
|
+
secretId: z.string().min(1),
|
|
219
|
+
}),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
export type RuntimeEnvEntry = z.infer<typeof RuntimeEnvEntrySchema>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* User-pinned runtime configuration stored under `metadata.runtime`. Empty
|
|
226
|
+
* fields fall back to autodetect on the next SANDBOX_START.
|
|
227
|
+
*/
|
|
228
|
+
const RuntimeMetadataSchema = z.object({
|
|
229
|
+
selected: z
|
|
60
230
|
.string()
|
|
61
231
|
.nullable()
|
|
62
232
|
.optional()
|
|
63
|
-
.describe(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
233
|
+
.describe(
|
|
234
|
+
"User-selected package manager (npm | pnpm | yarn | bun | deno). Null/absent means autodetect on next SANDBOX_START.",
|
|
235
|
+
),
|
|
236
|
+
port: z
|
|
237
|
+
.string()
|
|
238
|
+
.nullable()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe(
|
|
241
|
+
"User-selected dev server port as a string (allows '' / null for unset). Null/absent means autodetect.",
|
|
242
|
+
),
|
|
243
|
+
path: z
|
|
244
|
+
.string()
|
|
245
|
+
.nullable()
|
|
246
|
+
.optional()
|
|
247
|
+
.describe(
|
|
248
|
+
"Optional path (relative to repo root) to the directory containing package.json. Null/absent means repo root. Forwarded as `application.packageManager.path` to the daemon config.",
|
|
249
|
+
),
|
|
250
|
+
env: z
|
|
251
|
+
.array(RuntimeEnvEntrySchema)
|
|
252
|
+
.nullable()
|
|
253
|
+
.optional()
|
|
254
|
+
.describe(
|
|
255
|
+
"Env vars injected on every SANDBOX_START. Literal entries inline their value; secret entries store a secretId that mesh resolves via the credential vault before posting /_sandbox/config.",
|
|
256
|
+
),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export type RuntimeMetadata = z.infer<typeof RuntimeMetadataSchema>;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* GitHub repository linked to a virtual MCP
|
|
263
|
+
*/
|
|
264
|
+
const GithubRepoSchema = z.object({
|
|
265
|
+
url: z.string().describe("GitHub repository URL"),
|
|
266
|
+
owner: z.string().describe("Repository owner"),
|
|
267
|
+
name: z.string().describe("Repository name"),
|
|
268
|
+
installationId: z
|
|
269
|
+
.number()
|
|
270
|
+
.optional()
|
|
271
|
+
.describe(
|
|
272
|
+
"GitHub App installation ID. Absent when the repo was linked without a GitHub connection (public-clone mode).",
|
|
273
|
+
),
|
|
274
|
+
connectionId: z
|
|
68
275
|
.string()
|
|
69
276
|
.optional()
|
|
70
|
-
.describe(
|
|
277
|
+
.describe(
|
|
278
|
+
"ID of the mcp-github connection used for authentication. Absent for public repos cloned without credentials.",
|
|
279
|
+
),
|
|
280
|
+
});
|
|
71
281
|
|
|
72
|
-
|
|
73
|
-
|
|
282
|
+
export type GithubRepo = z.infer<typeof GithubRepoSchema>;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A single sandbox record in the per-(user, branch, kind) sandbox map — the
|
|
286
|
+
* provider-issued handle plus the preview URL the UI renders.
|
|
287
|
+
*
|
|
288
|
+
* `sandboxProviderKind` lets the UI construct daemon URLs correctly:
|
|
289
|
+
* - cluster: daemon is reached via the mesh proxy; preview URL is the
|
|
290
|
+
* per-claim HTTPRoute host (in-cluster) or a local port-forward (kind dev).
|
|
291
|
+
* - user-desktop: daemon is reached directly via the user's link binary.
|
|
292
|
+
*
|
|
293
|
+
* `previewUrl` is nullable: blank / tool sandboxes (no `workload`, no dev
|
|
294
|
+
* server) have nothing to render. UI code MUST check before constructing
|
|
295
|
+
* an iframe URL.
|
|
296
|
+
*/
|
|
297
|
+
export const SandboxRecordSchema = z.object({
|
|
298
|
+
sandboxHandle: z.string().describe("Provider-specific handle"),
|
|
299
|
+
previewUrl: z
|
|
300
|
+
.string()
|
|
301
|
+
.nullable()
|
|
302
|
+
.describe(
|
|
303
|
+
"URL where the sandbox's iframe-proxied UI is served, or null when the sandbox has no dev server (blank / tool sandboxes).",
|
|
304
|
+
),
|
|
305
|
+
sandboxApiUrl: z
|
|
74
306
|
.string()
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
307
|
+
.nullable()
|
|
308
|
+
.optional()
|
|
309
|
+
.describe(
|
|
310
|
+
"Daemon's public URL — what cluster→daemon RPCs target. Equal to previewUrl for user-desktop; null/absent for the cluster provider (routes through cluster ingress).",
|
|
311
|
+
),
|
|
312
|
+
sandboxProviderKind: z
|
|
313
|
+
// Canonical set. Migration 092 rewrote every persisted legacy value
|
|
314
|
+
// ("docker", "agent-sandbox", "desktop", "remote-user", "host",
|
|
315
|
+
// "freestyle") to a canonical kind, and migration 097 dropped the
|
|
316
|
+
// retired "local-docker" kind; readers no longer accept those strings —
|
|
317
|
+
// Zod will reject them at parse time.
|
|
318
|
+
.enum(["cluster", "user-desktop"])
|
|
319
|
+
.optional(),
|
|
320
|
+
createdAt: z
|
|
321
|
+
.number()
|
|
322
|
+
.optional()
|
|
323
|
+
.describe(
|
|
324
|
+
"Epoch ms the entry was first written by SANDBOX_START. Used by the booting overlay to show a stable elapsed timer that survives browser reloads. Optional for backward compatibility with entries written before this field existed.",
|
|
325
|
+
),
|
|
326
|
+
startedWith: z
|
|
327
|
+
.object({
|
|
328
|
+
packageManager: z
|
|
329
|
+
.string()
|
|
330
|
+
.nullable()
|
|
331
|
+
.optional()
|
|
332
|
+
.describe("metadata.runtime.selected at the time of SANDBOX_START"),
|
|
333
|
+
port: z
|
|
334
|
+
.string()
|
|
335
|
+
.nullable()
|
|
336
|
+
.optional()
|
|
337
|
+
.describe("metadata.runtime.port at the time of SANDBOX_START"),
|
|
338
|
+
path: z
|
|
339
|
+
.string()
|
|
340
|
+
.nullable()
|
|
341
|
+
.optional()
|
|
342
|
+
.describe("metadata.runtime.path at the time of SANDBOX_START"),
|
|
343
|
+
})
|
|
344
|
+
.optional()
|
|
345
|
+
.describe(
|
|
346
|
+
"Snapshot of metadata.runtime fields (selected/port/path) used at SANDBOX_START. The Preview tab compares the live metadata.runtime against this to decide if a restart is required to apply changes.",
|
|
347
|
+
),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
export type SandboxRecord = z.infer<typeof SandboxRecordSchema>;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Strict parser for a single sandbox record. Migration 091 rewrote every
|
|
354
|
+
* persisted legacy value (`runnerKind`, `vmId`, legacy kind names), so
|
|
355
|
+
* readers no longer accept those shapes — Zod throws on any leftover.
|
|
356
|
+
*/
|
|
357
|
+
export function parseSandboxRecord(raw: unknown): SandboxRecord {
|
|
358
|
+
return SandboxRecordSchema.parse(raw);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The active sandbox provider kinds. */
|
|
362
|
+
export type SandboxProviderKind = "cluster" | "user-desktop";
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Parse a `sandboxMap[user][branch]` cell into the kind-keyed v2 shape.
|
|
366
|
+
*
|
|
367
|
+
* Migration 087 rewrote every cell to the 3-level layout
|
|
368
|
+
* (`sandboxProviderKind → SandboxRecord`) and migration 091 rewrote every
|
|
369
|
+
* legacy kind value; this reader is now strict — entries that fail to parse
|
|
370
|
+
* (e.g. a stray cell missing required fields) are skipped, but no legacy
|
|
371
|
+
* key/value normalization happens.
|
|
372
|
+
*/
|
|
373
|
+
export function parseBranchMap(
|
|
374
|
+
raw: unknown,
|
|
375
|
+
): Partial<Record<SandboxProviderKind, SandboxRecord>> {
|
|
376
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
377
|
+
const obj = raw as Record<string, unknown>;
|
|
378
|
+
|
|
379
|
+
const out: Partial<Record<SandboxProviderKind, SandboxRecord>> = {};
|
|
380
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
381
|
+
if (!v || typeof v !== "object") continue;
|
|
382
|
+
if (k !== "cluster" && k !== "user-desktop") {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
out[k] = SandboxRecordSchema.parse(v);
|
|
387
|
+
} catch {
|
|
388
|
+
// Skip malformed entries rather than throw — readers stay forgiving
|
|
389
|
+
// about unexpected shapes within a known-key cell.
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Maps a user to their sandbox records per (branch, sandboxProviderKind).
|
|
397
|
+
* Lookup: sandboxMap[userId][branch][sandboxProviderKind] -> SandboxRecord
|
|
398
|
+
*
|
|
399
|
+
* Multiple threads on the same (userId, branch, kind) share one sandbox.
|
|
400
|
+
* Cloud and local sandboxes can coexist on the same branch as siblings.
|
|
401
|
+
*
|
|
402
|
+
* The schema is strict v2. Strict input/output types here are load-bearing
|
|
403
|
+
* for `useForm<…>(zodResolver(…))` callers, whose generic depends on
|
|
404
|
+
* `z.input` being identical to `z.output`. A `z.preprocess` here widens
|
|
405
|
+
* `z.input` to `unknown` and breaks the form.
|
|
406
|
+
*/
|
|
407
|
+
export const SandboxMapSchema = z.record(
|
|
408
|
+
z.string().describe("userId"),
|
|
409
|
+
z.record(
|
|
410
|
+
z.string().describe("branch"),
|
|
411
|
+
z.record(z.string().describe("sandboxProviderKind"), SandboxRecordSchema),
|
|
78
412
|
),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
export type SandboxMap = z.infer<typeof SandboxMapSchema>;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Normalize a raw `metadata.sandboxMap` value into v2 shape on read. Use this
|
|
419
|
+
* in storage adapters BEFORE returning data that will be Zod-validated against
|
|
420
|
+
* `VirtualMCPEntitySchema` (or any schema embedding `SandboxMapSchema`).
|
|
421
|
+
*
|
|
422
|
+
* After migration 091, the stored shape is the strict 3-level
|
|
423
|
+
* `userId → branch → sandboxProviderKind → SandboxRecord`. Returns `{}` for
|
|
424
|
+
* missing / malformed input rather than throwing — readers stay forgiving
|
|
425
|
+
* about unexpected per-row corruption; the strict schema catches any
|
|
426
|
+
* residual issues at validation time.
|
|
427
|
+
*/
|
|
428
|
+
export function normalizeSandboxMap(raw: unknown): SandboxMap {
|
|
429
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
430
|
+
const out: SandboxMap = {};
|
|
431
|
+
for (const [userId, userVal] of Object.entries(
|
|
432
|
+
raw as Record<string, unknown>,
|
|
433
|
+
)) {
|
|
434
|
+
if (!userVal || typeof userVal !== "object" || Array.isArray(userVal)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const userOut: SandboxMap[string] = {};
|
|
438
|
+
for (const [branch, branchVal] of Object.entries(
|
|
439
|
+
userVal as Record<string, unknown>,
|
|
440
|
+
)) {
|
|
441
|
+
const normalized = parseBranchMap(branchVal);
|
|
442
|
+
if (Object.keys(normalized).length > 0) {
|
|
443
|
+
userOut[branch] = normalized as SandboxMap[string][string];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (Object.keys(userOut).length > 0) {
|
|
447
|
+
out[userId] = userOut;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Virtual MCP entity schema - single source of truth
|
|
455
|
+
* Compliant with collections binding pattern
|
|
456
|
+
*/
|
|
457
|
+
export const VirtualMCPEntitySchema = z.object({
|
|
458
|
+
// Base collection entity fields
|
|
459
|
+
id: z.string().describe("Unique identifier"),
|
|
460
|
+
title: z.string().describe("Human-readable name"),
|
|
461
|
+
description: z.string().nullable().describe("Description"),
|
|
462
|
+
icon: z.string().nullable().describe("Icon URL"),
|
|
463
|
+
created_at: z.string().describe("Creation timestamp"),
|
|
464
|
+
updated_at: z.string().describe("Last update timestamp"),
|
|
465
|
+
created_by: z.string().describe("User ID who created this item"),
|
|
466
|
+
updated_by: z
|
|
467
|
+
.string()
|
|
468
|
+
.optional()
|
|
469
|
+
.describe("User ID who last updated this item"),
|
|
470
|
+
|
|
471
|
+
// Entity-specific fields
|
|
472
|
+
organization_id: z.string().describe("Organization ID this item belongs to"),
|
|
79
473
|
status: z.enum(["active", "inactive"]).describe("Current status"),
|
|
474
|
+
pinned: z.boolean().describe("Whether this space is pinned to the sidebar"),
|
|
80
475
|
// Metadata (stored in connections.metadata)
|
|
476
|
+
// Normalize null/undefined to { instructions: null } for consistent form tracking
|
|
81
477
|
metadata: z
|
|
82
478
|
.object({
|
|
83
|
-
instructions: z
|
|
479
|
+
instructions: z
|
|
480
|
+
.string()
|
|
481
|
+
.nullable()
|
|
482
|
+
.describe("Instructions also used as system prompt"),
|
|
483
|
+
enabled_plugins: z
|
|
484
|
+
.array(z.string())
|
|
485
|
+
.nullable()
|
|
486
|
+
.optional()
|
|
487
|
+
.describe("List of enabled plugin IDs"),
|
|
488
|
+
ui: VirtualMcpUISchema.nullable()
|
|
489
|
+
.optional()
|
|
490
|
+
.describe("UI customization settings"),
|
|
491
|
+
githubRepo: GithubRepoSchema.nullable()
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("Linked GitHub repository"),
|
|
494
|
+
runtime: RuntimeMetadataSchema.nullable()
|
|
495
|
+
.optional()
|
|
496
|
+
.describe(
|
|
497
|
+
"User-pinned runtime config (package manager, dev port). Empty fields = autodetect.",
|
|
498
|
+
),
|
|
499
|
+
sandboxMap: SandboxMapSchema.optional().describe(
|
|
500
|
+
"Per-user, per-branch sandbox mapping: sandboxMap[userId][branch] -> { sandboxHandle, previewUrl }",
|
|
501
|
+
),
|
|
84
502
|
})
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
.describe("Additional metadata including MCP server instructions"),
|
|
503
|
+
.loose()
|
|
504
|
+
.describe("Metadata"),
|
|
88
505
|
// Nested connections
|
|
89
506
|
connections: z
|
|
90
507
|
.array(VirtualMCPConnectionSchema)
|
|
91
|
-
.describe(
|
|
92
|
-
"Connections with their selected tools (behavior depends on tool_selection_mode)",
|
|
93
|
-
),
|
|
508
|
+
.describe("Connections with their selected tools, resources, and prompts"),
|
|
94
509
|
});
|
|
95
510
|
|
|
96
511
|
/**
|
|
@@ -108,49 +523,46 @@ export const VirtualMCPCreateDataSchema = z.object({
|
|
|
108
523
|
.nullable()
|
|
109
524
|
.optional()
|
|
110
525
|
.describe("Optional description"),
|
|
111
|
-
|
|
112
|
-
.default("inclusion")
|
|
113
|
-
.describe("Tool selection mode (defaults to 'inclusion')"),
|
|
114
|
-
icon: z.string().nullable().optional().describe("Optional icon URL"),
|
|
526
|
+
icon: z.string().nullish().describe("Optional icon URL"),
|
|
115
527
|
status: z
|
|
116
528
|
.enum(["active", "inactive"])
|
|
117
529
|
.optional()
|
|
118
530
|
.default("active")
|
|
119
531
|
.describe("Initial status"),
|
|
532
|
+
pinned: z.boolean().optional().default(false).describe("Pin to sidebar"),
|
|
120
533
|
metadata: z
|
|
121
534
|
.object({
|
|
122
|
-
instructions: z
|
|
535
|
+
instructions: z
|
|
536
|
+
.string()
|
|
537
|
+
.nullable()
|
|
538
|
+
.optional()
|
|
539
|
+
.describe("MCP server instructions"),
|
|
540
|
+
enabled_plugins: z
|
|
541
|
+
.array(z.string())
|
|
542
|
+
.nullable()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("List of enabled plugin IDs"),
|
|
545
|
+
ui: VirtualMcpUISchema.nullable()
|
|
546
|
+
.optional()
|
|
547
|
+
.describe("UI customization settings"),
|
|
548
|
+
githubRepo: GithubRepoSchema.nullable()
|
|
549
|
+
.optional()
|
|
550
|
+
.describe("Linked GitHub repository"),
|
|
551
|
+
runtime: RuntimeMetadataSchema.nullable()
|
|
552
|
+
.optional()
|
|
553
|
+
.describe(
|
|
554
|
+
"User-pinned runtime config (package manager, dev port). Empty fields = autodetect.",
|
|
555
|
+
),
|
|
556
|
+
sandboxMap: SandboxMapSchema.optional().describe(
|
|
557
|
+
"Per-user, per-branch sandbox mapping: sandboxMap[userId][branch] -> { sandboxHandle, previewUrl }",
|
|
558
|
+
),
|
|
123
559
|
})
|
|
560
|
+
.loose()
|
|
124
561
|
.nullable()
|
|
125
562
|
.optional()
|
|
126
563
|
.describe("Additional metadata including MCP server instructions"),
|
|
127
564
|
connections: z
|
|
128
|
-
.array(
|
|
129
|
-
z.object({
|
|
130
|
-
connection_id: z.string().describe("Connection ID"),
|
|
131
|
-
selected_tools: z
|
|
132
|
-
.array(z.string())
|
|
133
|
-
.nullable()
|
|
134
|
-
.optional()
|
|
135
|
-
.describe(
|
|
136
|
-
"Selected tool names (null/undefined = all tools or full exclusion)",
|
|
137
|
-
),
|
|
138
|
-
selected_resources: z
|
|
139
|
-
.array(z.string())
|
|
140
|
-
.nullable()
|
|
141
|
-
.optional()
|
|
142
|
-
.describe(
|
|
143
|
-
"Selected resource URIs or patterns with * and ** wildcards (null/undefined = all resources)",
|
|
144
|
-
),
|
|
145
|
-
selected_prompts: z
|
|
146
|
-
.array(z.string())
|
|
147
|
-
.nullable()
|
|
148
|
-
.optional()
|
|
149
|
-
.describe(
|
|
150
|
-
"Selected prompt names (null/undefined = all prompts or full exclusion)",
|
|
151
|
-
),
|
|
152
|
-
}),
|
|
153
|
-
)
|
|
565
|
+
.array(VirtualMCPConnectionInputSchema)
|
|
154
566
|
.describe(
|
|
155
567
|
"Connections to include/exclude (can be empty for exclusion mode)",
|
|
156
568
|
),
|
|
@@ -168,49 +580,42 @@ export const VirtualMCPUpdateDataSchema = z.object({
|
|
|
168
580
|
.nullable()
|
|
169
581
|
.optional()
|
|
170
582
|
.describe("New description (null to clear)"),
|
|
171
|
-
|
|
172
|
-
"New tool selection mode",
|
|
173
|
-
),
|
|
174
|
-
icon: z
|
|
175
|
-
.string()
|
|
176
|
-
.nullable()
|
|
177
|
-
.optional()
|
|
178
|
-
.describe("New icon URL (null to clear)"),
|
|
583
|
+
icon: z.string().nullish().describe("New icon URL"),
|
|
179
584
|
status: z.enum(["active", "inactive"]).optional().describe("New status"),
|
|
585
|
+
pinned: z.boolean().optional().describe("Pin/unpin from sidebar"),
|
|
180
586
|
metadata: z
|
|
181
587
|
.object({
|
|
182
|
-
instructions: z
|
|
588
|
+
instructions: z
|
|
589
|
+
.string()
|
|
590
|
+
.nullable()
|
|
591
|
+
.optional()
|
|
592
|
+
.describe("MCP server instructions"),
|
|
593
|
+
enabled_plugins: z
|
|
594
|
+
.array(z.string())
|
|
595
|
+
.nullable()
|
|
596
|
+
.optional()
|
|
597
|
+
.describe("List of enabled plugin IDs"),
|
|
598
|
+
ui: VirtualMcpUISchema.nullable()
|
|
599
|
+
.optional()
|
|
600
|
+
.describe("UI customization settings"),
|
|
601
|
+
githubRepo: GithubRepoSchema.nullable()
|
|
602
|
+
.optional()
|
|
603
|
+
.describe("Linked GitHub repository"),
|
|
604
|
+
runtime: RuntimeMetadataSchema.nullable()
|
|
605
|
+
.optional()
|
|
606
|
+
.describe(
|
|
607
|
+
"User-pinned runtime config (package manager, dev port). Empty fields = autodetect.",
|
|
608
|
+
),
|
|
609
|
+
sandboxMap: SandboxMapSchema.optional().describe(
|
|
610
|
+
"Per-user, per-branch sandbox mapping: sandboxMap[userId][branch] -> { sandboxHandle, previewUrl }",
|
|
611
|
+
),
|
|
183
612
|
})
|
|
613
|
+
.loose()
|
|
184
614
|
.nullable()
|
|
185
615
|
.optional()
|
|
186
616
|
.describe("Additional metadata including MCP server instructions"),
|
|
187
617
|
connections: z
|
|
188
|
-
.array(
|
|
189
|
-
z.object({
|
|
190
|
-
connection_id: z.string().describe("Connection ID"),
|
|
191
|
-
selected_tools: z
|
|
192
|
-
.array(z.string())
|
|
193
|
-
.nullable()
|
|
194
|
-
.optional()
|
|
195
|
-
.describe(
|
|
196
|
-
"Selected tool names (null/undefined = all tools or full exclusion)",
|
|
197
|
-
),
|
|
198
|
-
selected_resources: z
|
|
199
|
-
.array(z.string())
|
|
200
|
-
.nullable()
|
|
201
|
-
.optional()
|
|
202
|
-
.describe(
|
|
203
|
-
"Selected resource URIs or patterns with * and ** wildcards (null/undefined = all resources)",
|
|
204
|
-
),
|
|
205
|
-
selected_prompts: z
|
|
206
|
-
.array(z.string())
|
|
207
|
-
.nullable()
|
|
208
|
-
.optional()
|
|
209
|
-
.describe(
|
|
210
|
-
"Selected prompt names (null/undefined = all prompts or full exclusion)",
|
|
211
|
-
),
|
|
212
|
-
}),
|
|
213
|
-
)
|
|
618
|
+
.array(VirtualMCPConnectionInputSchema)
|
|
214
619
|
.optional()
|
|
215
620
|
.describe("New connections (replaces existing)"),
|
|
216
621
|
});
|