@flowdular/sdk 0.1.0 → 0.2.1

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 (175) 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 +2 -2
  28. package/packages/database-pglite/src/driver.ts +21 -3
  29. package/packages/database-testing/package.json +1 -1
  30. package/packages/dev-console/package.json +1 -1
  31. package/packages/harness/package.json +1 -1
  32. package/packages/kernel/package.json +1 -1
  33. package/packages/server/package.json +1 -1
  34. package/packages/server/src/application-routes.ts +80 -8
  35. package/packages/server/src/index.ts +4 -1
  36. package/packages/server/src/web-html.ts +9 -5
  37. package/packages/server/src/web.ts +59 -7
  38. package/packages/ui/package.json +1 -1
  39. package/packages/coding-agent/package.json +0 -44
  40. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  41. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  42. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  43. package/packages/coding-agent/src/index.ts +0 -56
  44. package/packages/coding-agent/src/registry.ts +0 -119
  45. package/packages/coding-agent/src/roles/contract.ts +0 -60
  46. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  47. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  48. package/packages/coding-agent/src/roles/registry.ts +0 -156
  49. package/packages/coding-agent/src/roles/skills.ts +0 -72
  50. package/packages/coding-agent/src/roles/sync.ts +0 -88
  51. package/packages/coding-agent/src/types.ts +0 -102
  52. package/packages/coding-agent/src/workspace.ts +0 -275
  53. package/packages/sandbox/README.md +0 -691
  54. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  55. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  56. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  57. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  84. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  85. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  86. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  87. package/packages/sandbox/dist/client/favicon.svg +0 -1
  88. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  89. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  90. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  91. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  92. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  93. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  94. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  95. package/packages/sandbox/dist/server/entry.js +0 -91671
  96. package/packages/sandbox/dist/server/favicon.svg +0 -1
  97. package/packages/sandbox/dist/server/index.html +0 -161
  98. package/packages/sandbox/index.html +0 -159
  99. package/packages/sandbox/octane.config.ts +0 -36
  100. package/packages/sandbox/package.json +0 -83
  101. package/packages/sandbox/public/favicon.svg +0 -1
  102. package/packages/sandbox/public/og.png +0 -0
  103. package/packages/sandbox/src/App.tsrx +0 -924
  104. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  105. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  106. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  107. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  108. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  109. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  110. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  111. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  112. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  113. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  114. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  115. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  116. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  117. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  118. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  119. package/packages/sandbox/src/client/api.ts +0 -797
  120. package/packages/sandbox/src/client/attachments.ts +0 -73
  121. package/packages/sandbox/src/client/dashboard.ts +0 -46
  122. package/packages/sandbox/src/client/github-settings.ts +0 -48
  123. package/packages/sandbox/src/client/highlight.ts +0 -105
  124. package/packages/sandbox/src/client/i18n.ts +0 -32
  125. package/packages/sandbox/src/client/locales/en.json +0 -466
  126. package/packages/sandbox/src/client/locales/pl.json +0 -466
  127. package/packages/sandbox/src/client/session-labels.ts +0 -77
  128. package/packages/sandbox/src/client/state.ts +0 -108
  129. package/packages/sandbox/src/index.ts +0 -10
  130. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  131. package/packages/sandbox/src/preview/load-module.ts +0 -16
  132. package/packages/sandbox/src/server/attachments.ts +0 -308
  133. package/packages/sandbox/src/server/auto-review.ts +0 -181
  134. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  135. package/packages/sandbox/src/server/config.ts +0 -460
  136. package/packages/sandbox/src/server/dashboard.ts +0 -245
  137. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  138. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  139. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  140. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  141. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  142. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  143. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  144. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  145. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  146. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  147. package/packages/sandbox/src/server/dependencies.ts +0 -88
  148. package/packages/sandbox/src/server/diff.ts +0 -230
  149. package/packages/sandbox/src/server/gates.ts +0 -388
  150. package/packages/sandbox/src/server/index.ts +0 -139
  151. package/packages/sandbox/src/server/path-guard.ts +0 -282
  152. package/packages/sandbox/src/server/planning.ts +0 -749
  153. package/packages/sandbox/src/server/platform-client.ts +0 -189
  154. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  155. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  156. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  157. package/packages/sandbox/src/server/preview-database.ts +0 -109
  158. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  159. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  160. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  161. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  162. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  163. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  164. package/packages/sandbox/src/server/reference.ts +0 -251
  165. package/packages/sandbox/src/server/routes.ts +0 -2258
  166. package/packages/sandbox/src/server/runtime.ts +0 -188
  167. package/packages/sandbox/src/server/session-owner.ts +0 -23
  168. package/packages/sandbox/src/server/sessions.ts +0 -849
  169. package/packages/sandbox/src/server/spec.ts +0 -471
  170. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  171. package/packages/sandbox/src/server/turns.ts +0 -993
  172. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  173. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  174. package/packages/sandbox/src/styles.css +0 -1573
  175. 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
- }