@flowdular/sdk 0.1.0 → 0.2.0

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 (174) hide show
  1. package/.ai/skills/test-hardening/SKILL.md +1 -1
  2. package/README.md +69 -12
  3. package/assets/flowdular-banner.png +0 -0
  4. package/modules/agents/package.json +1 -1
  5. package/modules/auth/package.json +1 -1
  6. package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
  7. package/modules/auth/src/server/endpoints.ts +3 -1
  8. package/modules/auth/src/server/runtime.ts +4 -0
  9. package/modules/automations/package.json +1 -1
  10. package/modules/automations-workflows-integration/package.json +1 -1
  11. package/modules/profile/package.json +1 -1
  12. package/modules/sandbox/package.json +1 -1
  13. package/modules/system/package.json +1 -1
  14. package/modules/users/package.json +1 -1
  15. package/modules/workflows/package.json +1 -1
  16. package/package.json +5 -13
  17. package/packages/ai-provider/package.json +1 -1
  18. package/packages/cli-protocol/package.json +1 -1
  19. package/packages/client/package.json +1 -1
  20. package/packages/client/src/routing.ts +39 -2
  21. package/packages/client/src/shell/BrandMark.tsrx +5 -1
  22. package/packages/client/src/shell/navigation.ts +3 -1
  23. package/packages/client/src/state.ts +6 -2
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/contracts/schemas/project.schema.json +16 -42
  26. package/packages/database/package.json +1 -1
  27. package/packages/database-pglite/package.json +1 -1
  28. package/packages/database-testing/package.json +1 -1
  29. package/packages/dev-console/package.json +1 -1
  30. package/packages/harness/package.json +1 -1
  31. package/packages/kernel/package.json +1 -1
  32. package/packages/server/package.json +1 -1
  33. package/packages/server/src/application-routes.ts +80 -8
  34. package/packages/server/src/index.ts +4 -1
  35. package/packages/server/src/web-html.ts +9 -5
  36. package/packages/server/src/web.ts +59 -7
  37. package/packages/ui/package.json +1 -1
  38. package/packages/coding-agent/package.json +0 -44
  39. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  40. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  41. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  42. package/packages/coding-agent/src/index.ts +0 -56
  43. package/packages/coding-agent/src/registry.ts +0 -119
  44. package/packages/coding-agent/src/roles/contract.ts +0 -60
  45. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  46. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  47. package/packages/coding-agent/src/roles/registry.ts +0 -156
  48. package/packages/coding-agent/src/roles/skills.ts +0 -72
  49. package/packages/coding-agent/src/roles/sync.ts +0 -88
  50. package/packages/coding-agent/src/types.ts +0 -102
  51. package/packages/coding-agent/src/workspace.ts +0 -275
  52. package/packages/sandbox/README.md +0 -691
  53. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  54. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  55. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  56. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  57. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  84. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  85. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  86. package/packages/sandbox/dist/client/favicon.svg +0 -1
  87. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  88. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  89. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  90. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  91. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  92. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  93. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  94. package/packages/sandbox/dist/server/entry.js +0 -91671
  95. package/packages/sandbox/dist/server/favicon.svg +0 -1
  96. package/packages/sandbox/dist/server/index.html +0 -161
  97. package/packages/sandbox/index.html +0 -159
  98. package/packages/sandbox/octane.config.ts +0 -36
  99. package/packages/sandbox/package.json +0 -83
  100. package/packages/sandbox/public/favicon.svg +0 -1
  101. package/packages/sandbox/public/og.png +0 -0
  102. package/packages/sandbox/src/App.tsrx +0 -924
  103. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  104. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  105. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  106. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  107. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  108. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  109. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  110. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  111. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  112. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  113. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  114. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  115. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  116. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  117. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  118. package/packages/sandbox/src/client/api.ts +0 -797
  119. package/packages/sandbox/src/client/attachments.ts +0 -73
  120. package/packages/sandbox/src/client/dashboard.ts +0 -46
  121. package/packages/sandbox/src/client/github-settings.ts +0 -48
  122. package/packages/sandbox/src/client/highlight.ts +0 -105
  123. package/packages/sandbox/src/client/i18n.ts +0 -32
  124. package/packages/sandbox/src/client/locales/en.json +0 -466
  125. package/packages/sandbox/src/client/locales/pl.json +0 -466
  126. package/packages/sandbox/src/client/session-labels.ts +0 -77
  127. package/packages/sandbox/src/client/state.ts +0 -108
  128. package/packages/sandbox/src/index.ts +0 -10
  129. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  130. package/packages/sandbox/src/preview/load-module.ts +0 -16
  131. package/packages/sandbox/src/server/attachments.ts +0 -308
  132. package/packages/sandbox/src/server/auto-review.ts +0 -181
  133. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  134. package/packages/sandbox/src/server/config.ts +0 -460
  135. package/packages/sandbox/src/server/dashboard.ts +0 -245
  136. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  137. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  138. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  139. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  140. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  141. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  142. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  143. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  144. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  145. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  146. package/packages/sandbox/src/server/dependencies.ts +0 -88
  147. package/packages/sandbox/src/server/diff.ts +0 -230
  148. package/packages/sandbox/src/server/gates.ts +0 -388
  149. package/packages/sandbox/src/server/index.ts +0 -139
  150. package/packages/sandbox/src/server/path-guard.ts +0 -282
  151. package/packages/sandbox/src/server/planning.ts +0 -749
  152. package/packages/sandbox/src/server/platform-client.ts +0 -189
  153. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  154. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  155. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  156. package/packages/sandbox/src/server/preview-database.ts +0 -109
  157. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  158. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  159. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  160. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  161. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  162. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  163. package/packages/sandbox/src/server/reference.ts +0 -251
  164. package/packages/sandbox/src/server/routes.ts +0 -2258
  165. package/packages/sandbox/src/server/runtime.ts +0 -188
  166. package/packages/sandbox/src/server/session-owner.ts +0 -23
  167. package/packages/sandbox/src/server/sessions.ts +0 -849
  168. package/packages/sandbox/src/server/spec.ts +0 -471
  169. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  170. package/packages/sandbox/src/server/turns.ts +0 -993
  171. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  172. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  173. package/packages/sandbox/src/styles.css +0 -1573
  174. package/packages/sandbox/vite.config.ts +0 -133
@@ -1,10 +0,0 @@
1
- export {
2
- createSandboxRoutes,
3
- createSandboxRuntime,
4
- findFlowdularWorkspace,
5
- } from './server/index.ts';
6
- export type {
7
- SandboxConfiguration,
8
- SandboxRuntime,
9
- SandboxSession,
10
- } from './server/index.ts';
@@ -1,373 +0,0 @@
1
- import { useEffect, useMemo } from 'octane';
2
- import {
3
- ApplicationShell,
4
- type ModuleClientContribution,
5
- } from '@flowdular/sdk/client';
6
- import { Alert, EmptyState, Tag } from '@flowdular/sdk/ui';
7
- import { cell, createStore, useValue } from 'segment-state';
8
- import { Head } from '@octanejs/seo';
9
- import type { RenderRouteProps } from '@octanejs/vite-plugin';
10
- import type { SessionView } from '../client/api.ts';
11
- import { signalReady } from '../client/state.ts';
12
- import { importDraftModule } from './load-module.ts';
13
- import {
14
- registerSandboxTranslations,
15
- restoreSandboxLocale,
16
- t,
17
- useTranslation,
18
- } from '../client/i18n.ts';
19
- import '@flowdular/sdk/ui/styles';
20
- import '../styles.css';
21
-
22
- registerSandboxTranslations();
23
-
24
- interface PreviewIdentity {
25
- readonly accountId: string;
26
- readonly email: string;
27
- readonly displayName: string;
28
- readonly role: string;
29
- readonly scopes: readonly string[];
30
- readonly tenantId: string;
31
- readonly tenants: readonly {
32
- readonly tenantId: string;
33
- readonly name: string;
34
- readonly slug: string;
35
- readonly role: string;
36
- }[];
37
- }
38
-
39
- interface PreviewPrincipal {
40
- readonly accountId: string;
41
- readonly email: string;
42
- readonly displayName: string;
43
- readonly role: string;
44
- readonly scopes: readonly string[];
45
- readonly tenantId: string;
46
- readonly tenants: readonly {
47
- readonly tenantId: string;
48
- readonly name: string;
49
- readonly slug: string;
50
- readonly role: string;
51
- }[];
52
- }
53
-
54
- function previewState() {
55
- const store = createStore({
56
- contributions: cell<readonly ModuleClientContribution[]>([]),
57
- principal: cell<PreviewPrincipal | null>(null),
58
- status: cell<'loading' | 'ready' | 'empty' | 'failed'>('loading'),
59
- message: '',
60
- moduleId: '',
61
- /* The view the shell opens on: the first navigation entry of the module
62
- the URL asked for, or of the first module that contributed one. */
63
- initialView: '',
64
- });
65
- return { store, state: store.state };
66
- }
67
-
68
- /* The preview runs against the session's own authentication runtime, so the
69
- draft module sees a real principal, real scopes, and a real CSRF token
70
- instead of a stub. The credential belongs to one ephemeral database. */
71
- interface PreviewModuleInfo {
72
- readonly id: string;
73
- readonly directory: string;
74
- readonly hasClient: boolean;
75
- readonly support?: boolean;
76
- }
77
-
78
- async function signInPreview(sessionId: string): Promise<{
79
- readonly principal: PreviewPrincipal;
80
- readonly csrfToken: string;
81
- readonly scopes: readonly string[];
82
- readonly modules: readonly PreviewModuleInfo[];
83
- readonly error: string | null;
84
- }> {
85
- document.cookie = `coreloom_preview=${sessionId}; path=/; samesite=strict`;
86
- const info = await (await fetch(
87
- '/sandbox/api/sessions/' + encodeURIComponent(sessionId) + '/preview',
88
- {
89
- headers: { accept: 'application/json' },
90
- },
91
- )).json() as {
92
- credentials?: { email: string; password: string };
93
- scopes?: readonly string[];
94
- modules?: readonly PreviewModuleInfo[];
95
- error?: string | { message?: string };
96
- };
97
- const credentials = info.credentials;
98
- if (!credentials) {
99
- throw new Error(typeof info.error === 'string'
100
- ? info.error
101
- : info.error?.message ?? t('sandbox.preview.sessionUnavailable'));
102
- }
103
- const response = await fetch('/api/auth/sign-in', {
104
- method: 'POST',
105
- headers: { 'content-type': 'application/json' },
106
- credentials: 'same-origin',
107
- body: JSON.stringify(credentials),
108
- });
109
- const session = await response.json() as {
110
- principal?: PreviewPrincipal;
111
- csrfToken?: string;
112
- error?: { message?: string };
113
- };
114
- if (!response.ok || !session.principal || !session.csrfToken) {
115
- throw new Error(session.error?.message ??
116
- t('sandbox.preview.identityFailed'));
117
- }
118
- return {
119
- principal: session.principal,
120
- csrfToken: session.csrfToken,
121
- scopes: info.scopes ?? [],
122
- modules: info.modules ?? [],
123
- error: typeof info.error === 'string' ? info.error : null,
124
- };
125
- }
126
-
127
- /* Draft sources are served through a sandbox route that names the session and
128
- the module directory, never through a host file system path. */
129
- function clientEntry(
130
- sessionId: string,
131
- directory: string,
132
- support = false,
133
- ): string {
134
- return `/${
135
- support ? 'preview-support' : 'preview-module'
136
- }/${encodeURIComponent(sessionId)}/${encodeURIComponent(
137
- directory,
138
- )}/src/client/index.ts`;
139
- }
140
-
141
- function sessionIdFromLocation(url: string): string {
142
- const segments = new URL(url, 'http://sandbox.local').pathname.split(
143
- '/',
144
- ).filter(Boolean);
145
- return segments[0] === 'preview' ? segments[1] ?? '' : '';
146
- }
147
-
148
- function identityFor(
149
- principal: PreviewPrincipal | null,
150
- contributions: readonly ModuleClientContribution[],
151
- moduleId: string,
152
- ): PreviewIdentity {
153
- /* The signed-in preview principal decides authority. Its scopes are widened
154
- only with the surfaces the draft modules declare, so every screen they
155
- contribute is reachable and nothing else appears. */
156
- const scopes = new Set<string>([
157
- 'system.workspace.access',
158
- ...(principal?.scopes ?? []),
159
- ]);
160
- for (const contribution of contributions) {
161
- for (const item of contribution.navigation ?? []) scopes.add(item.scope);
162
- for (const widget of contribution.widgets ?? []) scopes.add(widget.scope);
163
- }
164
- return {
165
- accountId: principal?.accountId ?? 'preview-account',
166
- email: principal?.email ?? 'preview@sandbox.local',
167
- displayName: principal?.displayName ?? t('sandbox.preview.identityName'),
168
- role: principal?.role ?? 'owner',
169
- scopes: [...scopes],
170
- tenantId: principal?.tenantId ?? 'preview-tenant',
171
- tenants: principal?.tenants && principal.tenants.length > 0
172
- ? principal.tenants
173
- : [
174
- {
175
- tenantId: 'preview-tenant',
176
- name: moduleId || t('sandbox.preview.workspace'),
177
- slug: 'preview',
178
- role: 'owner',
179
- },
180
- ],
181
- };
182
- }
183
-
184
- /* Element selection: the parent turns it on, the preview reports what was
185
- clicked, and the chat attaches it to the next message. */
186
- function installSelection(enabled: boolean): () => void {
187
- if (!enabled || typeof document === 'undefined') return () => undefined;
188
- const describe = (element: Element): string => {
189
- const parts: string[] = [];
190
- let current: Element | null = element;
191
- for (let depth = 0; current && depth < 4; depth += 1) {
192
- const classes = (current.getAttribute('class') ?? '').split(/\s+/).filter(
193
- Boolean,
194
- ).slice(0, 2).map((value) => '.' + value).join('');
195
- parts.unshift(current.tagName.toLowerCase() + classes);
196
- current = current.parentElement;
197
- }
198
- return parts.join(' > ');
199
- };
200
- const onClick = (event: MouseEvent) => {
201
- const target = event.target as Element | null;
202
- if (!target) return;
203
- event.preventDefault();
204
- event.stopPropagation();
205
- const view =
206
- target.closest('[data-flowdular-view]')?.getAttribute(
207
- 'data-flowdular-view',
208
- ) ??
209
- '';
210
- window.parent.postMessage(
211
- {
212
- type: 'flowdular:selection',
213
- label: (target.textContent ?? '').trim().slice(0, 80) ||
214
- target.tagName.toLowerCase(),
215
- selector: describe(target),
216
- view,
217
- text: (target.textContent ?? '').trim().slice(0, 400),
218
- },
219
- '*',
220
- );
221
- };
222
- const onOver = (event: MouseEvent) => {
223
- const target = event.target as HTMLElement | null;
224
- if (target) target.style.outline = '2px solid #2563eb';
225
- };
226
- const onOut = (event: MouseEvent) => {
227
- const target = event.target as HTMLElement | null;
228
- if (target) target.style.outline = '';
229
- };
230
- document.addEventListener('click', onClick, true);
231
- document.addEventListener('mouseover', onOver, true);
232
- document.addEventListener('mouseout', onOut, true);
233
- return () => {
234
- document.removeEventListener('click', onClick, true);
235
- document.removeEventListener('mouseover', onOver, true);
236
- document.removeEventListener('mouseout', onOut, true);
237
- };
238
- }
239
-
240
- export function PreviewHost(props?: RenderRouteProps) @{
241
- const { t } = useTranslation();
242
- const preview = useMemo(() => previewState(), []);
243
- const [contributions] = useValue(preview.state.contributions);
244
- const [principal] = useValue(preview.state.principal);
245
- const [status, setStatus] = useValue(preview.state.status);
246
- const [message, setMessage] = useValue(preview.state.message);
247
- const [moduleId, setModuleId] = useValue(preview.state.moduleId);
248
- const [initialView] = useValue(preview.state.initialView);
249
-
250
- useEffect(() => {
251
- restoreSandboxLocale();
252
- const url = props?.url ?? window.location.href;
253
- const sessionId = sessionIdFromLocation(url);
254
- const parameters = new URL(url, 'http://sandbox.local').searchParams;
255
- const selecting = parameters.has('select');
256
- /* The workbench opens the preview on one module of the session; every
257
- module still composes, so this only picks the first screen. */
258
- const openOn = parameters.get('module') ?? '';
259
- const teardown = installSelection(selecting);
260
-
261
- void (async () => {
262
- try {
263
- const response = await fetch(
264
- '/sandbox/api/sessions/' + encodeURIComponent(sessionId),
265
- { headers: { accept: 'application/json' } },
266
- );
267
- const view = await response.json() as
268
- SessionView & {
269
- error?: { message?: string };
270
- };
271
- if (!response.ok) {
272
- setMessage(
273
- view.error?.message ?? t('sandbox.preview.sessionUnavailable'),
274
- );
275
- setStatus('failed');
276
- return;
277
- }
278
- setModuleId(view.session.moduleId);
279
- const identity = await signInPreview(sessionId);
280
- /* Every draft module with a client entry contributes to the same
281
- shell, exactly as the platform composes them. */
282
- const created: ModuleClientContribution[] = [];
283
- const missing: string[] = [];
284
- let opening = '';
285
- for (const module of identity.modules) {
286
- if (!module.hasClient) continue;
287
- const loaded = await importDraftModule(
288
- clientEntry(sessionId, module.directory, module.support),
289
- );
290
- if (!loaded.createClientContribution) {
291
- missing.push(module.id);
292
- continue;
293
- }
294
- const contribution = loaded.createClientContribution({
295
- csrfToken: identity.csrfToken,
296
- scopes: [...identity.principal.scopes, ...identity.scopes],
297
- });
298
- created.push(contribution);
299
- const entry = contribution.navigation?.[0]?.viewId ?? '';
300
- if (
301
- entry &&
302
- (!opening || !module.support && !openOn ||
303
- module.directory === openOn)
304
- ) {
305
- opening = entry;
306
- }
307
- }
308
- if (created.length === 0) {
309
- setMessage(
310
- missing.length > 0
311
- ? missing.join(', ') + t('sandbox.preview.missingExport', {
312
- modules: missing.join(', '),
313
- })
314
- : t('sandbox.preview.noClient'),
315
- );
316
- setStatus('empty');
317
- return;
318
- }
319
- preview.store.act((transaction) => {
320
- transaction.set(preview.state.contributions, created);
321
- transaction.set(preview.state.principal, identity.principal);
322
- transaction.set(preview.state.initialView, opening);
323
- }, 'preview/loaded');
324
- setStatus('ready');
325
- } catch (error) {
326
- setMessage(
327
- error instanceof Error
328
- ? error.message
329
- : t('sandbox.preview.loadFailed'),
330
- );
331
- setStatus('empty');
332
- } finally {
333
- signalReady();
334
- }
335
- })();
336
-
337
- return teardown;
338
- }, []);
339
-
340
- /* The shell registers document metadata, so the preview provides the same
341
- Head boundary the platform gives it. */
342
- <Head>
343
- @if (status === 'ready' && contributions.length > 0) {
344
- <ApplicationShell
345
- contributions={contributions}
346
- identity={identityFor(principal, contributions, moduleId)}
347
- initialView={initialView || contributions[0]!.navigation?.[0]?.viewId ||
348
- 'overview'}
349
- onSignOut={() => undefined}
350
- onSwitchTenant={() => undefined}
351
- />
352
- } @else if (status === 'loading') {
353
- <div class="ui-view" style="padding: 24px">
354
- <p class="ui-help">{t('sandbox.preview.loading')}</p>
355
- </div>
356
- } @else {
357
- <div class="ui-view" style="padding: 24px">
358
- <EmptyState
359
- icon="flask"
360
- title={t('sandbox.preview.empty.title')}
361
- code="src/client/index.ts"
362
- >
363
- {message || t('sandbox.preview.empty.description')}
364
- </EmptyState>
365
- @if (status === 'failed') {
366
- <Alert>{message}</Alert>
367
- } @else {
368
- <Tag tone="info">{moduleId || t('sandbox.preview.draftModule')}</Tag>
369
- }
370
- </div>
371
- }
372
- </Head>
373
- }
@@ -1,16 +0,0 @@
1
- import type { ModuleClientContribution } from '@flowdular/sdk/client';
2
-
3
- export interface DraftModuleEntry {
4
- readonly createClientContribution?: (context: {
5
- readonly csrfToken: string;
6
- readonly scopes: readonly string[];
7
- }) => ModuleClientContribution;
8
- }
9
-
10
- /* The draft module lives in a session workspace outside this app, so its entry
11
- is resolved at runtime. The ignore comment stays in this plain module: the
12
- TSRX compiler drops comments from call arguments, which would make Vite warn
13
- about an unanalyzable import on every preview load. */
14
- export function importDraftModule(entry: string): Promise<DraftModuleEntry> {
15
- return import(/* @vite-ignore */ entry) as Promise<DraftModuleEntry>;
16
- }
@@ -1,308 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
- import { isAbsolute, join, relative, resolve } from 'node:path';
4
- import {
5
- readSession,
6
- sessionPaths,
7
- updateSession,
8
- type SandboxSession,
9
- type SessionAttachment,
10
- } from './sessions.ts';
11
- import { SandboxSetupError } from './workspace-root.ts';
12
-
13
- export const MAX_ATTACHMENTS = 10;
14
- export const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
15
- export const MAX_ATTACHMENT_NAME = 128;
16
-
17
- interface AttachmentType {
18
- readonly kind: SessionAttachment['kind'];
19
- readonly contentType: string;
20
- }
21
-
22
- /* The only extensions accepted. Images are verified by magic bytes; the
23
- concept/text formats are accepted by extension, with a NUL-byte guard that
24
- rejects a binary wearing a text extension. */
25
- const ATTACHMENT_TYPES: Readonly<Record<string, AttachmentType>> = {
26
- png: { kind: 'image', contentType: 'image/png' },
27
- jpg: { kind: 'image', contentType: 'image/jpeg' },
28
- jpeg: { kind: 'image', contentType: 'image/jpeg' },
29
- gif: { kind: 'image', contentType: 'image/gif' },
30
- webp: { kind: 'image', contentType: 'image/webp' },
31
- md: { kind: 'file', contentType: 'text/markdown; charset=utf-8' },
32
- txt: { kind: 'file', contentType: 'text/plain; charset=utf-8' },
33
- json: { kind: 'file', contentType: 'application/json; charset=utf-8' },
34
- csv: { kind: 'file', contentType: 'text/csv; charset=utf-8' },
35
- pdf: { kind: 'file', contentType: 'application/pdf' },
36
- svg: { kind: 'file', contentType: 'image/svg+xml' },
37
- };
38
-
39
- const ATTACHMENT_ID =
40
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
41
-
42
- /* Attachment ids are the only client-chosen segment that reaches a file path,
43
- so anything that is not an id the sandbox minted is refused before a path is
44
- built from it. */
45
- export function assertAttachmentId(value: string): string {
46
- if (!ATTACHMENT_ID.test(value)) {
47
- throw new SandboxSetupError(
48
- 'INVALID_ATTACHMENT_ID',
49
- 'The attachment id is not a sandbox attachment identifier.',
50
- );
51
- }
52
- return value;
53
- }
54
-
55
- function extensionOf(name: string): string {
56
- const dot = name.lastIndexOf('.');
57
- return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
58
- }
59
-
60
- /* The original filename reduced to a safe basename: no path segments, only
61
- [A-Za-z0-9._-], no `..` traversal, no leading dot, and bounded length with
62
- the extension preserved. */
63
- function safeAttachmentName(raw: string): string {
64
- const base = raw.split(/[/\\]/).pop() ?? '';
65
- let safe = base.replace(/[^A-Za-z0-9._-]/g, '_').replace(/\.{2,}/g, '.');
66
- safe = safe.replace(/^\.+/, '');
67
- if (safe.length > MAX_ATTACHMENT_NAME) {
68
- const dot = safe.lastIndexOf('.');
69
- const ext = dot > 0 ? safe.slice(dot) : '';
70
- safe = safe.slice(0, Math.max(1, MAX_ATTACHMENT_NAME - ext.length)) + ext;
71
- }
72
- if (!safe || safe === '.' || safe.includes('..')) {
73
- throw new SandboxSetupError(
74
- 'ATTACHMENT_NAME_INVALID',
75
- 'The attachment filename is not a valid name.',
76
- );
77
- }
78
- return safe;
79
- }
80
-
81
- /* Images must prove their format; the pdf and svg formats carry a recognisable
82
- header too. Text formats are accepted by extension. */
83
- function contentMatches(ext: string, bytes: Buffer): boolean {
84
- switch (ext) {
85
- case 'png':
86
- return (
87
- bytes.length >= 8 &&
88
- bytes[0] === 0x89 &&
89
- bytes[1] === 0x50 &&
90
- bytes[2] === 0x4e &&
91
- bytes[3] === 0x47
92
- );
93
- case 'jpg':
94
- case 'jpeg':
95
- return (
96
- bytes.length >= 3 &&
97
- bytes[0] === 0xff &&
98
- bytes[1] === 0xd8 &&
99
- bytes[2] === 0xff
100
- );
101
- case 'gif': {
102
- const head = bytes.subarray(0, 6).toString('latin1');
103
- return head === 'GIF87a' || head === 'GIF89a';
104
- }
105
- case 'webp':
106
- return (
107
- bytes.length >= 12 &&
108
- bytes.subarray(0, 4).toString('latin1') === 'RIFF' &&
109
- bytes.subarray(8, 12).toString('latin1') === 'WEBP'
110
- );
111
- case 'pdf':
112
- return bytes.subarray(0, 5).toString('latin1') === '%PDF-';
113
- case 'svg': {
114
- const head = bytes.subarray(0, 4096).toString('utf8').trimStart();
115
- return head.startsWith('<') && /<svg[\s>]/i.test(head);
116
- }
117
- default:
118
- /* Text formats: reject a binary payload carrying a text extension. */
119
- return !bytes.subarray(0, 8192).includes(0);
120
- }
121
- }
122
-
123
- function resolveType(name: string, bytes: Buffer): AttachmentType {
124
- const ext = extensionOf(name);
125
- const type = ATTACHMENT_TYPES[ext];
126
- if (!type) {
127
- throw new SandboxSetupError(
128
- 'ATTACHMENT_TYPE_REJECTED',
129
- `Attachments of type .${ext || '(none)'} are not allowed. Allowed: ${Object.keys(
130
- ATTACHMENT_TYPES,
131
- ).join(', ')}.`,
132
- );
133
- }
134
- if (!contentMatches(ext, bytes)) {
135
- throw new SandboxSetupError(
136
- 'ATTACHMENT_TYPE_REJECTED',
137
- `The file contents do not match a .${ext} file.`,
138
- );
139
- }
140
- return type;
141
- }
142
-
143
- export function attachmentContentType(name: string): string {
144
- return (
145
- ATTACHMENT_TYPES[extensionOf(name)]?.contentType ??
146
- 'application/octet-stream'
147
- );
148
- }
149
-
150
- /* Two attachments must never share a workspace filename, or one would overwrite
151
- the other in reference/attachments/. */
152
- function uniqueName(
153
- name: string,
154
- existing: readonly SessionAttachment[],
155
- ): string {
156
- const taken = new Set(existing.map((item) => item.name));
157
- if (!taken.has(name)) return name;
158
- const dot = name.lastIndexOf('.');
159
- const stem = dot > 0 ? name.slice(0, dot) : name;
160
- const ext = dot > 0 ? name.slice(dot) : '';
161
- for (let index = 2; index < 1000; index += 1) {
162
- const candidate = `${stem}-${index}${ext}`;
163
- if (!taken.has(candidate)) return candidate;
164
- }
165
- return `${randomUUID().slice(0, 8)}-${name}`;
166
- }
167
-
168
- function assertInside(root: string, path: string): string {
169
- const inside = relative(resolve(root), resolve(path));
170
- if (!inside || inside.startsWith('..') || isAbsolute(inside)) {
171
- throw new SandboxSetupError(
172
- 'ATTACHMENT_NAME_INVALID',
173
- 'The attachment path escapes the session directory.',
174
- );
175
- }
176
- return path;
177
- }
178
-
179
- function storedPath(
180
- paths: ReturnType<typeof sessionPaths>,
181
- meta: SessionAttachment,
182
- ): string {
183
- return assertInside(
184
- paths.attachments,
185
- join(paths.attachments, `${meta.id}-${meta.name}`),
186
- );
187
- }
188
-
189
- function workspacePath(
190
- paths: ReturnType<typeof sessionPaths>,
191
- meta: SessionAttachment,
192
- ): string {
193
- return assertInside(
194
- paths.workspaceAttachments,
195
- join(paths.workspaceAttachments, meta.name),
196
- );
197
- }
198
-
199
- export async function addAttachment(
200
- workspaceRoot: string,
201
- session: SandboxSession,
202
- input: { readonly name: string; readonly bytes: Buffer },
203
- ): Promise<SessionAttachment> {
204
- if (input.bytes.byteLength === 0) {
205
- throw new SandboxSetupError('ATTACHMENT_EMPTY', 'The attachment is empty.');
206
- }
207
- if (input.bytes.byteLength > MAX_ATTACHMENT_BYTES) {
208
- throw new SandboxSetupError(
209
- 'ATTACHMENT_TOO_LARGE',
210
- `Attachments are limited to ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MB.`,
211
- );
212
- }
213
- const current = await readSession(workspaceRoot, session.id);
214
- if (current.attachments.length >= MAX_ATTACHMENTS) {
215
- throw new SandboxSetupError(
216
- 'ATTACHMENT_LIMIT_REACHED',
217
- `A session may hold at most ${MAX_ATTACHMENTS} attachments.`,
218
- );
219
- }
220
- const type = resolveType(input.name, input.bytes);
221
- const meta: SessionAttachment = {
222
- id: randomUUID(),
223
- name: uniqueName(safeAttachmentName(input.name), current.attachments),
224
- kind: type.kind,
225
- size: input.bytes.byteLength,
226
- addedAt: Date.now(),
227
- };
228
- const paths = sessionPaths(workspaceRoot, current.id, current.moduleSuffix);
229
- await mkdir(paths.attachments, { recursive: true });
230
- await mkdir(paths.workspaceAttachments, { recursive: true });
231
- await writeFile(storedPath(paths, meta), input.bytes);
232
- await writeFile(workspacePath(paths, meta), input.bytes);
233
- await updateSession(workspaceRoot, current.id, {
234
- attachments: [...current.attachments, meta],
235
- });
236
- return meta;
237
- }
238
-
239
- export async function listAttachments(
240
- workspaceRoot: string,
241
- session: SandboxSession,
242
- ): Promise<readonly SessionAttachment[]> {
243
- return (await readSession(workspaceRoot, session.id)).attachments;
244
- }
245
-
246
- function findAttachment(
247
- attachments: readonly SessionAttachment[],
248
- attachmentId: string,
249
- ): SessionAttachment {
250
- const meta = attachments.find((item) => item.id === attachmentId);
251
- if (!meta) {
252
- throw new SandboxSetupError(
253
- 'ATTACHMENT_NOT_FOUND',
254
- 'No such attachment exists in this session.',
255
- );
256
- }
257
- return meta;
258
- }
259
-
260
- export async function removeAttachment(
261
- workspaceRoot: string,
262
- session: SandboxSession,
263
- attachmentId: string,
264
- ): Promise<void> {
265
- assertAttachmentId(attachmentId);
266
- const current = await readSession(workspaceRoot, session.id);
267
- const meta = findAttachment(current.attachments, attachmentId);
268
- const paths = sessionPaths(workspaceRoot, current.id, current.moduleSuffix);
269
- await rm(storedPath(paths, meta), { force: true });
270
- await rm(workspacePath(paths, meta), { force: true });
271
- await updateSession(workspaceRoot, current.id, {
272
- attachments: current.attachments.filter((item) => item.id !== attachmentId),
273
- });
274
- }
275
-
276
- export async function readAttachment(
277
- workspaceRoot: string,
278
- session: SandboxSession,
279
- attachmentId: string,
280
- ): Promise<{
281
- readonly bytes: Buffer;
282
- readonly contentType: string;
283
- readonly name: string;
284
- }> {
285
- assertAttachmentId(attachmentId);
286
- const current = await readSession(workspaceRoot, session.id);
287
- const meta = findAttachment(current.attachments, attachmentId);
288
- const paths = sessionPaths(workspaceRoot, current.id, current.moduleSuffix);
289
- const bytes = await readFile(storedPath(paths, meta));
290
- return {
291
- bytes,
292
- contentType: attachmentContentType(meta.name),
293
- name: meta.name,
294
- };
295
- }
296
-
297
- /* The note prepended to a turn's instruction when the session has attachments.
298
- The files are already in reference/attachments/, so the agent opens them with
299
- its normal file tools; the path plus this note is the whole contract. */
300
- export function attachmentInstruction(
301
- attachments: readonly SessionAttachment[],
302
- ): string | null {
303
- if (attachments.length === 0) return null;
304
- const list = attachments
305
- .map((item) => `${item.name} (${item.kind})`)
306
- .join(', ');
307
- return `Attachments the operator provided (in reference/attachments/): ${list}. Screenshots show the desired change; read the files before implementing.`;
308
- }