@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,460 +0,0 @@
1
- import {
2
- flowdularEnvironment,
3
- flowdularStateDirectory,
4
- } from '@flowdular/sdk/kernel/runtime-config';
5
- import {
6
- createCipheriv,
7
- createDecipheriv,
8
- createHash,
9
- randomBytes,
10
- } from 'node:crypto';
11
- import { constants } from 'node:fs';
12
- import { chmod, lstat, mkdir, open } from 'node:fs/promises';
13
- import { join } from 'node:path';
14
- import type { SandboxRuntimeMode } from '@flowdular/sdk/coding-agent';
15
- import type { AiProviderKind } from '@flowdular/sdk/ai-provider';
16
- import { SandboxSetupError } from './workspace-root.ts';
17
-
18
- export const SANDBOX_DIRECTORY = '.flowdular/sandbox';
19
-
20
- export function sandboxDirectory(workspaceRoot: string): string {
21
- // The parent selects this path before applying the worker filesystem ceiling.
22
- // A worker cannot inspect sibling state roots to rediscover that selection.
23
- if (process.env.FD_INTERNAL_SANDBOX_PREVIEW_WORKER === '1') {
24
- const root = process.env.FD_INTERNAL_SANDBOX_STATE_ROOT;
25
- if (root !== '.flowdular' && root !== '.coreloom')
26
- throw new Error('Missing preview state root.');
27
- return join(workspaceRoot, root, 'sandbox');
28
- }
29
- return join(flowdularStateDirectory(workspaceRoot), 'sandbox');
30
- }
31
-
32
- export type PreviewDataMode = 'fixtures' | 'bridge';
33
-
34
- export interface SealedSecret {
35
- readonly iv: string;
36
- readonly tag: string;
37
- readonly ciphertext: string;
38
- }
39
-
40
- export interface ByokProviderConfiguration {
41
- readonly kind: AiProviderKind;
42
- readonly model: string;
43
- readonly resourceName?: string;
44
- readonly baseURL?: string;
45
- readonly credential: SealedSecret | null;
46
- }
47
-
48
- export type GitHubDeliveryMode = 'auto' | 'direct' | 'fork';
49
-
50
- /* Repository delivery is local sandbox configuration, not project source.
51
- One operator can use a fork while another can push directly without either
52
- changing flowdular.json for the whole team. */
53
- export interface GitHubDeliveryConfiguration {
54
- readonly enabled: boolean;
55
- /* False keeps the repository-owned sandbox.delivery.git values authoritative.
56
- True applies the operator's machine-local remote and branch preferences. */
57
- readonly overridesProject: boolean;
58
- readonly remote: string;
59
- /* owner/name of the repository that receives the pull request. Null means
60
- derive it from the configured remote. */
61
- readonly repository: string | null;
62
- readonly baseBranch: string;
63
- readonly branchPrefix: string;
64
- readonly mode: GitHubDeliveryMode;
65
- readonly forkOwner: string | null;
66
- readonly reviewers: readonly string[];
67
- }
68
-
69
- export interface SandboxConfiguration {
70
- readonly version: 1;
71
- readonly mode: SandboxRuntimeMode;
72
- /* Origin of the Flowdular application this sandbox connects to. It may be a
73
- local development server or a remote deployment. */
74
- readonly platformUrl: string;
75
- readonly platformToken: SealedSecret | null;
76
- readonly driver: string;
77
- readonly driverModel: string | null;
78
- readonly previewData: PreviewDataMode;
79
- readonly byok: ByokProviderConfiguration | null;
80
- /* Token for the pull-request provider of the git-pr delivery, handed to gh
81
- as GH_TOKEN when the operator's own gh login is not usable. */
82
- readonly gitProviderToken: SealedSecret | null;
83
- readonly github: GitHubDeliveryConfiguration;
84
- }
85
-
86
- export const DEFAULT_GITHUB_CONFIGURATION: GitHubDeliveryConfiguration = {
87
- enabled: true,
88
- overridesProject: false,
89
- remote: 'origin',
90
- repository: null,
91
- baseBranch: 'main',
92
- branchPrefix: 'sandbox',
93
- mode: 'auto',
94
- forkOwner: null,
95
- reviewers: [],
96
- };
97
-
98
- export const DEFAULT_CONFIGURATION: SandboxConfiguration = {
99
- version: 1,
100
- mode: 'loopback',
101
- platformUrl: 'http://127.0.0.1:4310',
102
- platformToken: null,
103
- driver: 'claude-code',
104
- driverModel: null,
105
- previewData: 'fixtures',
106
- byok: null,
107
- gitProviderToken: null,
108
- github: DEFAULT_GITHUB_CONFIGURATION,
109
- };
110
-
111
- const GIT_NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
112
- const MAX_GIT_NAME_LENGTH = 120;
113
- const MAX_GITHUB_REVIEWERS = 20;
114
- const GITHUB_REPOSITORY =
115
- /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
116
- const GITHUB_ACCOUNT = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
117
-
118
- function invalidGitHub(detail: string): SandboxSetupError {
119
- return new SandboxSetupError(
120
- 'GITHUB_CONFIG_INVALID',
121
- `The GitHub integration is not usable: ${detail}`,
122
- );
123
- }
124
-
125
- export function assertGitName(value: string, field: string): string {
126
- if (value.length > MAX_GIT_NAME_LENGTH || !GIT_NAME.test(value)) {
127
- throw invalidGitHub(`${field} must be a plain git name.`);
128
- }
129
- return value;
130
- }
131
-
132
- export function assertGitHubRepository(value: string | null): string | null {
133
- if (value === null || value === '') return null;
134
- if (!GITHUB_REPOSITORY.test(value)) {
135
- throw invalidGitHub('repository must use the owner/name form.');
136
- }
137
- return value;
138
- }
139
-
140
- export function assertGitHubAccount(value: string | null): string | null {
141
- if (value === null || value === '') return null;
142
- if (!GITHUB_ACCOUNT.test(value)) {
143
- throw invalidGitHub('forkOwner must be a GitHub account name.');
144
- }
145
- return value;
146
- }
147
-
148
- export function assertGitHubDeliveryMode(value: string): GitHubDeliveryMode {
149
- if (value !== 'auto' && value !== 'direct' && value !== 'fork') {
150
- throw invalidGitHub('mode must be auto, direct, or fork.');
151
- }
152
- return value;
153
- }
154
-
155
- export function resolveGitHubConfiguration(
156
- value: Partial<GitHubDeliveryConfiguration> | undefined,
157
- ): GitHubDeliveryConfiguration {
158
- const defaults = DEFAULT_GITHUB_CONFIGURATION;
159
- if (
160
- value?.overridesProject !== undefined &&
161
- typeof value.overridesProject !== 'boolean'
162
- ) {
163
- throw invalidGitHub('overridesProject must be a boolean.');
164
- }
165
- const reviewers = value?.reviewers ?? defaults.reviewers;
166
- if (
167
- !Array.isArray(reviewers) ||
168
- reviewers.length > MAX_GITHUB_REVIEWERS ||
169
- !reviewers.every((reviewer) => GITHUB_ACCOUNT.test(reviewer))
170
- ) {
171
- throw invalidGitHub('reviewers must list GitHub account names.');
172
- }
173
- const resolved = {
174
- enabled: value?.enabled ?? defaults.enabled,
175
- remote: assertGitName(value?.remote ?? defaults.remote, 'remote'),
176
- repository: assertGitHubRepository(value?.repository ?? null),
177
- baseBranch: assertGitName(
178
- value?.baseBranch ?? defaults.baseBranch,
179
- 'baseBranch',
180
- ),
181
- branchPrefix: assertGitName(
182
- value?.branchPrefix ?? defaults.branchPrefix,
183
- 'branchPrefix',
184
- ),
185
- mode: assertGitHubDeliveryMode(value?.mode ?? defaults.mode),
186
- forkOwner: assertGitHubAccount(value?.forkOwner ?? null),
187
- reviewers: [...new Set(reviewers)],
188
- };
189
- /* Older config files have no explicit override marker. Preserve genuine
190
- custom values, but do not let a fully materialized default object shadow
191
- the repository-owned delivery configuration. */
192
- const differsFromDefault =
193
- resolved.enabled !== defaults.enabled ||
194
- resolved.remote !== defaults.remote ||
195
- resolved.repository !== defaults.repository ||
196
- resolved.baseBranch !== defaults.baseBranch ||
197
- resolved.branchPrefix !== defaults.branchPrefix ||
198
- resolved.mode !== defaults.mode ||
199
- resolved.forkOwner !== defaults.forkOwner ||
200
- resolved.reviewers.length !== defaults.reviewers.length ||
201
- resolved.reviewers.some(
202
- (reviewer, index) => reviewer !== defaults.reviewers[index],
203
- );
204
- return {
205
- ...resolved,
206
- overridesProject: value?.overridesProject ?? differsFromDefault,
207
- };
208
- }
209
-
210
- function configPath(workspaceRoot: string): string {
211
- return join(sandboxDirectory(workspaceRoot), 'config.json');
212
- }
213
-
214
- function keyPath(workspaceRoot: string): string {
215
- return join(sandboxDirectory(workspaceRoot), 'secret.key');
216
- }
217
-
218
- function unsafeLocalPath(path: string): SandboxSetupError {
219
- return new SandboxSetupError(
220
- 'UNSAFE_SANDBOX_CONFIG_PATH',
221
- `The sandbox local path ${path} must be regular and must not be a symbolic link.`,
222
- );
223
- }
224
-
225
- async function assertSafeLocalPath(
226
- workspaceRoot: string,
227
- path: string,
228
- createDirectory: boolean,
229
- ): Promise<void> {
230
- for (const directory of [
231
- flowdularStateDirectory(workspaceRoot),
232
- sandboxDirectory(workspaceRoot),
233
- ]) {
234
- try {
235
- const info = await lstat(directory);
236
- if (info.isSymbolicLink() || !info.isDirectory()) {
237
- throw unsafeLocalPath(directory);
238
- }
239
- await chmod(directory, 0o700);
240
- } catch (error) {
241
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
242
- if (!createDirectory) return;
243
- await mkdir(directory, { mode: 0o700 });
244
- }
245
- }
246
- try {
247
- const info = await lstat(path);
248
- if (info.isSymbolicLink() || !info.isFile()) throw unsafeLocalPath(path);
249
- } catch (error) {
250
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
251
- }
252
- }
253
-
254
- async function readLocalFile(path: string): Promise<string> {
255
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
256
- try {
257
- await handle.chmod(0o600);
258
- return await handle.readFile('utf8');
259
- } finally {
260
- await handle.close();
261
- }
262
- }
263
-
264
- async function writeLocalFile(path: string, value: string): Promise<void> {
265
- const handle = await open(
266
- path,
267
- constants.O_WRONLY |
268
- constants.O_CREAT |
269
- constants.O_TRUNC |
270
- constants.O_NOFOLLOW,
271
- 0o600,
272
- );
273
- try {
274
- await handle.chmod(0o600);
275
- await handle.writeFile(value, 'utf8');
276
- } finally {
277
- await handle.close();
278
- }
279
- }
280
-
281
- /* Secrets stay on the machine that runs the sandbox: encrypted at rest with a
282
- permission-restricted local key, never written to the workspace repository
283
- and never returned to the browser. */
284
- async function localKey(workspaceRoot: string): Promise<Buffer> {
285
- const path = keyPath(workspaceRoot);
286
- try {
287
- await assertSafeLocalPath(workspaceRoot, path, false);
288
- const encoded = await readLocalFile(path);
289
- const key = Buffer.from(encoded.trim(), 'base64');
290
- if (key.byteLength === 32) return key;
291
- } catch (error) {
292
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
293
- }
294
- await assertSafeLocalPath(workspaceRoot, path, true);
295
- const key = randomBytes(32);
296
- await writeLocalFile(path, key.toString('base64'));
297
- return key;
298
- }
299
-
300
- export async function sealSecret(
301
- workspaceRoot: string,
302
- value: string,
303
- ): Promise<SealedSecret> {
304
- const key = await localKey(workspaceRoot);
305
- const iv = randomBytes(12);
306
- const cipher = createCipheriv('aes-256-gcm', key, iv);
307
- const ciphertext = Buffer.concat([
308
- cipher.update(value, 'utf8'),
309
- cipher.final(),
310
- ]);
311
- return {
312
- iv: iv.toString('base64'),
313
- tag: cipher.getAuthTag().toString('base64'),
314
- ciphertext: ciphertext.toString('base64'),
315
- };
316
- }
317
-
318
- export async function openSecret(
319
- workspaceRoot: string,
320
- sealed: SealedSecret,
321
- ): Promise<string> {
322
- const key = await localKey(workspaceRoot);
323
- const decipher = createDecipheriv(
324
- 'aes-256-gcm',
325
- key,
326
- Buffer.from(sealed.iv, 'base64'),
327
- );
328
- decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
329
- return Buffer.concat([
330
- decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
331
- decipher.final(),
332
- ]).toString('utf8');
333
- }
334
-
335
- export function secretFingerprint(sealed: SealedSecret | null): string | null {
336
- return sealed
337
- ? createHash('sha256').update(sealed.ciphertext).digest('hex').slice(0, 8)
338
- : null;
339
- }
340
-
341
- export function assertPlatformUrl(value: string): string {
342
- let url: URL;
343
- try {
344
- url = new URL(value);
345
- } catch {
346
- throw new SandboxSetupError(
347
- 'INVALID_PLATFORM_URL',
348
- 'The platform address must be an absolute URL.',
349
- );
350
- }
351
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
352
- throw new SandboxSetupError(
353
- 'INVALID_PLATFORM_URL',
354
- 'The platform address must use http or https.',
355
- );
356
- }
357
- const loopback =
358
- url.hostname === 'localhost' ||
359
- url.hostname === '::1' ||
360
- url.hostname === '[::1]' ||
361
- /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
362
- if (url.protocol !== 'https:' && !loopback) {
363
- throw new SandboxSetupError(
364
- 'INVALID_PLATFORM_URL',
365
- 'The platform address must use HTTPS unless it is a loopback address.',
366
- );
367
- }
368
- return url.origin;
369
- }
370
-
371
- /* The launcher decides the mode from the interface it actually bound, so a
372
- stored configuration can never grant loopback trust to a public listener. */
373
- function modeFromEnvironment(stored: SandboxRuntimeMode): SandboxRuntimeMode {
374
- const value = flowdularEnvironment(process.env).FD_SANDBOX_MODE;
375
- if (value === 'loopback' || value === 'self-hosted') return value;
376
- return stored;
377
- }
378
-
379
- export async function loadSandboxConfiguration(
380
- workspaceRoot: string,
381
- ): Promise<SandboxConfiguration> {
382
- try {
383
- await assertSafeLocalPath(workspaceRoot, configPath(workspaceRoot), false);
384
- const stored = JSON.parse(
385
- await readLocalFile(configPath(workspaceRoot)),
386
- ) as Partial<SandboxConfiguration>;
387
- return {
388
- ...DEFAULT_CONFIGURATION,
389
- ...stored,
390
- version: 1,
391
- mode: modeFromEnvironment(stored.mode ?? DEFAULT_CONFIGURATION.mode),
392
- platformUrl: assertPlatformUrl(
393
- stored.platformUrl ?? DEFAULT_CONFIGURATION.platformUrl,
394
- ),
395
- github: resolveGitHubConfiguration(stored.github),
396
- };
397
- } catch (error) {
398
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
399
- return {
400
- ...DEFAULT_CONFIGURATION,
401
- mode: modeFromEnvironment(DEFAULT_CONFIGURATION.mode),
402
- };
403
- }
404
- throw error;
405
- }
406
- }
407
-
408
- export async function saveSandboxConfiguration(
409
- workspaceRoot: string,
410
- configuration: SandboxConfiguration,
411
- ): Promise<SandboxConfiguration> {
412
- const path = configPath(workspaceRoot);
413
- await assertSafeLocalPath(workspaceRoot, path, true);
414
- await writeLocalFile(path, `${JSON.stringify(configuration, null, '\t')}\n`);
415
- return configuration;
416
- }
417
-
418
- export interface SafeSandboxConfiguration {
419
- readonly mode: SandboxRuntimeMode;
420
- readonly platformUrl: string;
421
- readonly platformTokenFingerprint: string | null;
422
- readonly driver: string;
423
- readonly driverModel: string | null;
424
- readonly previewData: PreviewDataMode;
425
- readonly byok: {
426
- readonly kind: AiProviderKind;
427
- readonly model: string;
428
- readonly credentialFingerprint: string | null;
429
- } | null;
430
- readonly github: GitHubDeliveryConfiguration & {
431
- readonly tokenFingerprint: string | null;
432
- };
433
- }
434
-
435
- /* What the browser is allowed to see. Credentials never leave the server. */
436
- export function safeConfiguration(
437
- configuration: SandboxConfiguration,
438
- ): SafeSandboxConfiguration {
439
- return {
440
- mode: configuration.mode,
441
- platformUrl: configuration.platformUrl,
442
- platformTokenFingerprint: secretFingerprint(configuration.platformToken),
443
- driver: configuration.driver,
444
- driverModel: configuration.driverModel,
445
- previewData: configuration.previewData,
446
- byok: configuration.byok
447
- ? {
448
- kind: configuration.byok.kind,
449
- model: configuration.byok.model,
450
- credentialFingerprint: secretFingerprint(
451
- configuration.byok.credential,
452
- ),
453
- }
454
- : null,
455
- github: {
456
- ...configuration.github,
457
- tokenFingerprint: secretFingerprint(configuration.gitProviderToken),
458
- },
459
- };
460
- }
@@ -1,245 +0,0 @@
1
- import { createReadStream } from 'node:fs';
2
- import { readFile } from 'node:fs/promises';
3
- import { createInterface } from 'node:readline';
4
- import { join } from 'node:path';
5
- import { readDeliveryRecord } from './delivery/record.ts';
6
- import { ownsSession } from './session-owner.ts';
7
- import { hashSpec } from './spec.ts';
8
- import {
9
- modulePathOf,
10
- sessionPaths,
11
- type ChatEntry,
12
- type SandboxSession,
13
- type SessionOwner,
14
- } from './sessions.ts';
15
-
16
- export type WorkStatus =
17
- | 'draft'
18
- | 'working'
19
- | 'approval'
20
- | 'approved'
21
- | 'changes'
22
- | 'rejected'
23
- | 'attention'
24
- | 'review'
25
- | 'delivered'
26
- | 'archived';
27
-
28
- export interface WorkUsage {
29
- inputTokens: number;
30
- outputTokens: number;
31
- totalTokens: number;
32
- reportedTurns: number;
33
- missingUsage: number;
34
- knownCostUsd: number;
35
- pricedTurns: number;
36
- unpricedTurns: number;
37
- }
38
-
39
- export interface DashboardRow {
40
- readonly session: SandboxSession;
41
- readonly personal: boolean;
42
- readonly status: WorkStatus;
43
- readonly usage: WorkUsage;
44
- readonly approvedModules: number;
45
- readonly phases: readonly {
46
- readonly role: string;
47
- readonly usage: WorkUsage;
48
- }[];
49
- }
50
-
51
- export interface SandboxDashboard {
52
- readonly rows: readonly DashboardRow[];
53
- readonly usage: WorkUsage;
54
- readonly counts: Readonly<Record<WorkStatus, number>>;
55
- readonly legacySessions: number;
56
- readonly deletedSessions: number;
57
- }
58
-
59
- export function emptyUsage(): WorkUsage {
60
- return {
61
- inputTokens: 0,
62
- outputTokens: 0,
63
- totalTokens: 0,
64
- reportedTurns: 0,
65
- missingUsage: 0,
66
- knownCostUsd: 0,
67
- pricedTurns: 0,
68
- unpricedTurns: 0,
69
- };
70
- }
71
-
72
- export function addUsage(target: WorkUsage, source: WorkUsage): void {
73
- for (const key of Object.keys(target) as (keyof WorkUsage)[])
74
- target[key] += source[key];
75
- }
76
-
77
- /* A streaming projection, not another billing ledger. Keep only counters and
78
- the latest decision, never prompts or tool output. Monotonic sequence ids
79
- make duplicate transcript lines harmless without an unbounded id set. */
80
- export async function summarizeTranscript(
81
- entries: AsyncIterable<ChatEntry> | Iterable<ChatEntry>,
82
- ) {
83
- const usage = emptyUsage();
84
- const phases = new Map<string, WorkUsage>();
85
- const decisions = new Map<string, ChatEntry['decision']>();
86
- let sequence = 0;
87
- let pending: WorkUsage | null = null;
88
- for await (const entry of entries) {
89
- if (entry.sequence <= sequence) continue;
90
- sequence = entry.sequence;
91
- if (entry.decision && entry.module)
92
- decisions.set(entry.module, entry.decision);
93
- const event = entry.event;
94
- if (event?.type !== 'turn.started' && event?.type !== 'turn.completed')
95
- continue;
96
- const role = entry.role || 'unknown';
97
- let phase = phases.get(role);
98
- if (!phase) {
99
- phase = emptyUsage();
100
- phases.set(role, phase);
101
- }
102
- if (event.type === 'turn.started') {
103
- if (pending) {
104
- pending.missingUsage++;
105
- pending.unpricedTurns++;
106
- }
107
- pending = phase;
108
- continue;
109
- }
110
- pending = null;
111
- const values = [
112
- event.usage?.inputTokens,
113
- event.usage?.outputTokens,
114
- event.usage?.totalTokens,
115
- ];
116
- const valid =
117
- values.every((value) => Number.isSafeInteger(value) && value >= 0) &&
118
- (event.finishReason === 'stop' || values.some((value) => value > 0));
119
- if (valid) {
120
- phase.inputTokens += event.usage.inputTokens;
121
- phase.outputTokens += event.usage.outputTokens;
122
- phase.totalTokens += event.usage.totalTokens;
123
- phase.reportedTurns++;
124
- } else phase.missingUsage++;
125
- if (
126
- typeof event.costUsd === 'number' &&
127
- Number.isFinite(event.costUsd) &&
128
- event.costUsd >= 0
129
- ) {
130
- phase.knownCostUsd += event.costUsd;
131
- phase.pricedTurns++;
132
- } else phase.unpricedTurns++;
133
- }
134
- if (pending) {
135
- pending.missingUsage++;
136
- pending.unpricedTurns++;
137
- }
138
- for (const phase of phases.values()) addUsage(usage, phase);
139
- return {
140
- usage,
141
- phases: [...phases].map(([role, usage]) => ({ role, usage })),
142
- decisions,
143
- };
144
- }
145
-
146
- async function* transcript(path: string): AsyncIterable<ChatEntry> {
147
- const stream = createReadStream(path, { encoding: 'utf8' });
148
- const lines = createInterface({ input: stream, crlfDelay: Infinity });
149
- try {
150
- for await (const line of lines) {
151
- if (!line.trim()) continue;
152
- try {
153
- yield JSON.parse(line) as ChatEntry;
154
- } catch {
155
- throw new Error(
156
- 'The session history is incomplete. Reload the dashboard.',
157
- );
158
- }
159
- }
160
- } catch (error) {
161
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
162
- } finally {
163
- lines.close();
164
- stream.destroy();
165
- }
166
- }
167
-
168
- export async function buildDashboard(
169
- root: string,
170
- sessions: readonly SandboxSession[],
171
- owner: SessionOwner | null,
172
- running: ReadonlySet<string>,
173
- ): Promise<SandboxDashboard> {
174
- const usage = emptyUsage();
175
- const rows: DashboardRow[] = [];
176
- const counts: Record<WorkStatus, number> = {
177
- draft: 0,
178
- working: 0,
179
- approval: 0,
180
- approved: 0,
181
- changes: 0,
182
- rejected: 0,
183
- attention: 0,
184
- review: 0,
185
- delivered: 0,
186
- archived: 0,
187
- };
188
- let legacySessions = 0;
189
- let deletedSessions = 0;
190
- for (const session of sessions) {
191
- const personal = ownsSession(session, owner);
192
- const paths = sessionPaths(root, session.id, session.moduleSuffix);
193
- const summary = await summarizeTranscript(transcript(paths.chatLog));
194
- if (personal) addUsage(usage, summary.usage);
195
- else legacySessions++;
196
- if (session.state === 'deleted') {
197
- if (personal) deletedSessions++;
198
- continue;
199
- }
200
- let approvedModules = 0;
201
- for (const module of session.modules) {
202
- if (!module.specHash || !module.specApprovedAt) continue;
203
- const spec = await readFile(
204
- join(modulePathOf(paths, module.directory), 'spec/module.yaml'),
205
- 'utf8',
206
- ).catch(() => null);
207
- if (spec && hashSpec(spec) === module.specHash) approvedModules++;
208
- }
209
- const delivery = session.ejectedAt
210
- ? await readDeliveryRecord(paths.root)
211
- : null;
212
- const status: WorkStatus = session.rejectedAt
213
- ? 'rejected'
214
- : session.archivedAt
215
- ? 'archived'
216
- : session.ejectedAt
217
- ? delivery?.target === 'workspace'
218
- ? 'delivered'
219
- : 'review'
220
- : running.has(session.id)
221
- ? 'working'
222
- : session.state === 'failed' || session.state === 'blocked'
223
- ? 'attention'
224
- : session.state === 'awaiting-approval'
225
- ? 'approval'
226
- : approvedModules === session.modules.length &&
227
- approvedModules > 0
228
- ? 'approved'
229
- : [...summary.decisions.values()].includes(
230
- 'changes-requested',
231
- )
232
- ? 'changes'
233
- : 'draft';
234
- if (personal) counts[status]++;
235
- rows.push({
236
- session,
237
- personal,
238
- status,
239
- usage: summary.usage,
240
- phases: summary.phases,
241
- approvedModules,
242
- });
243
- }
244
- return { rows, usage, counts, legacySessions, deletedSessions };
245
- }