@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,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
- }