@flowdular/sandbox 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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +696 -0
  3. package/bin/flowdular-sandbox.mjs +394 -0
  4. package/bin/register-types.mjs +53 -0
  5. package/dist/client/assets/App-ClMOrpyg.js +11 -0
  6. package/dist/client/assets/PreviewHost-BHiHrfbQ.js +1 -0
  7. package/dist/client/assets/auto-buGEq6de.js +3 -0
  8. package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  9. package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  10. package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  11. package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  12. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  13. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  14. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  15. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  16. package/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  17. package/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  18. package/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  19. package/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  20. package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  21. package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  22. package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  23. package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  24. package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  25. package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  26. package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  27. package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  28. package/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  29. package/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  30. package/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  31. package/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  32. package/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  33. package/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  34. package/dist/client/assets/index-CvwhlQ87.js +4 -0
  35. package/dist/client/assets/styles-BPWaGf55.js +3 -0
  36. package/dist/client/assets/styles-Dx1_KOag.css +1 -0
  37. package/dist/client/favicon.svg +1 -0
  38. package/dist/server/assets/auto-CVGNYyb3.js +12785 -0
  39. package/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +4125 -0
  40. package/dist/server/assets/nodefs-DVSI7kG0.js +24 -0
  41. package/dist/server/assets/opfs-ahp-D56iokt1.js +388 -0
  42. package/dist/server/assets/token-Bvaq7Anz.js +55 -0
  43. package/dist/server/assets/token-util-BqSvdIPQ.js +3 -0
  44. package/dist/server/assets/token-util-CktFFT4z.js +494 -0
  45. package/dist/server/entry.js +91671 -0
  46. package/dist/server/favicon.svg +1 -0
  47. package/dist/server/index.html +161 -0
  48. package/index.html +159 -0
  49. package/internal/coding-agent/package.json +44 -0
  50. package/internal/coding-agent/src/drivers/byok.ts +322 -0
  51. package/internal/coding-agent/src/drivers/claude-code.ts +290 -0
  52. package/internal/coding-agent/src/drivers/codex.ts +281 -0
  53. package/internal/coding-agent/src/index.ts +56 -0
  54. package/internal/coding-agent/src/registry.ts +119 -0
  55. package/internal/coding-agent/src/roles/contract.ts +60 -0
  56. package/internal/coding-agent/src/roles/defaults.ts +134 -0
  57. package/internal/coding-agent/src/roles/handoff.ts +41 -0
  58. package/internal/coding-agent/src/roles/registry.ts +156 -0
  59. package/internal/coding-agent/src/roles/skills.ts +72 -0
  60. package/internal/coding-agent/src/roles/sync.ts +88 -0
  61. package/internal/coding-agent/src/types.ts +102 -0
  62. package/internal/coding-agent/src/workspace.ts +275 -0
  63. package/octane.config.ts +36 -0
  64. package/package.json +68 -0
  65. package/public/favicon.svg +1 -0
  66. package/public/og.png +0 -0
  67. package/src/App.tsrx +924 -0
  68. package/src/client/ApprovalHandoff.tsrx +39 -0
  69. package/src/client/ChatPane.tsrx +694 -0
  70. package/src/client/ConnectView.tsrx +86 -0
  71. package/src/client/DiffPane.tsrx +150 -0
  72. package/src/client/EjectModal.tsrx +571 -0
  73. package/src/client/GateResults.tsrx +60 -0
  74. package/src/client/GitHubSettingsModal.tsrx +219 -0
  75. package/src/client/NewSessionForm.tsrx +214 -0
  76. package/src/client/SessionBar.tsrx +151 -0
  77. package/src/client/SessionPicker.tsrx +472 -0
  78. package/src/client/SpecEditor.tsrx +109 -0
  79. package/src/client/SpecReviewCard.tsrx +321 -0
  80. package/src/client/WorkCard.tsrx +128 -0
  81. package/src/client/WorkbenchModal.tsrx +161 -0
  82. package/src/client/WorkspaceMenu.tsrx +100 -0
  83. package/src/client/api.ts +797 -0
  84. package/src/client/attachments.ts +73 -0
  85. package/src/client/dashboard.ts +46 -0
  86. package/src/client/github-settings.ts +48 -0
  87. package/src/client/highlight.ts +105 -0
  88. package/src/client/i18n.ts +32 -0
  89. package/src/client/locales/en.json +466 -0
  90. package/src/client/locales/pl.json +466 -0
  91. package/src/client/session-labels.ts +77 -0
  92. package/src/client/state.ts +108 -0
  93. package/src/index.ts +10 -0
  94. package/src/preview/PreviewHost.tsrx +373 -0
  95. package/src/preview/load-module.ts +16 -0
  96. package/src/server/attachments.ts +308 -0
  97. package/src/server/auto-review.ts +181 -0
  98. package/src/server/checkpoints.ts +90 -0
  99. package/src/server/config.ts +460 -0
  100. package/src/server/dashboard.ts +245 -0
  101. package/src/server/delivery/configuration.ts +193 -0
  102. package/src/server/delivery/git-pr.ts +1473 -0
  103. package/src/server/delivery/index.ts +83 -0
  104. package/src/server/delivery/local.ts +151 -0
  105. package/src/server/delivery/official-modules.ts +516 -0
  106. package/src/server/delivery/plan.ts +218 -0
  107. package/src/server/delivery/policies.ts +239 -0
  108. package/src/server/delivery/record.ts +41 -0
  109. package/src/server/delivery/steps.ts +464 -0
  110. package/src/server/delivery/types.ts +158 -0
  111. package/src/server/dependencies.ts +88 -0
  112. package/src/server/diff.ts +230 -0
  113. package/src/server/gates.ts +388 -0
  114. package/src/server/index.ts +139 -0
  115. package/src/server/path-guard.ts +282 -0
  116. package/src/server/planning.ts +749 -0
  117. package/src/server/platform-client.ts +189 -0
  118. package/src/server/preview-database-host.ts +340 -0
  119. package/src/server/preview-database-protocol.ts +163 -0
  120. package/src/server/preview-database-proxy.ts +356 -0
  121. package/src/server/preview-database.ts +109 -0
  122. package/src/server/preview-ipc.ts +15 -0
  123. package/src/server/preview-modules.ts +83 -0
  124. package/src/server/preview-revision.ts +44 -0
  125. package/src/server/preview-runtime.ts +467 -0
  126. package/src/server/preview-worker-manager.ts +477 -0
  127. package/src/server/preview-worker.ts +168 -0
  128. package/src/server/reference.ts +251 -0
  129. package/src/server/routes.ts +2258 -0
  130. package/src/server/runtime.ts +188 -0
  131. package/src/server/session-owner.ts +23 -0
  132. package/src/server/sessions.ts +849 -0
  133. package/src/server/spec.ts +471 -0
  134. package/src/server/turn-lifecycle.ts +56 -0
  135. package/src/server/turns.ts +993 -0
  136. package/src/server/workspace-install.ts +266 -0
  137. package/src/server/workspace-root.ts +73 -0
  138. package/src/styles.css +1573 -0
  139. package/vite.config.ts +134 -0
package/README.md ADDED
@@ -0,0 +1,696 @@
1
+ <div align="center">
2
+
3
+ <picture>
4
+ <source
5
+ media="(prefers-color-scheme: dark)"
6
+ srcset="https://raw.githubusercontent.com/flowdular/flowdular/main/docs/assets/flowdular-logo-dark.svg"
7
+ />
8
+ <img
9
+ src="https://raw.githubusercontent.com/flowdular/flowdular/main/docs/assets/flowdular-logo.svg"
10
+ alt="Flowdular"
11
+ width="320"
12
+ />
13
+ </picture>
14
+
15
+ ### The Flowdular sandbox
16
+
17
+ Chat a change, watch it build behind the gates, preview it in the real
18
+ application, deliver it as code you own.
19
+
20
+ ![Node](https://img.shields.io/badge/Node-%E2%89%A5%2022.22.2-3A6BE0)
21
+ ![License](https://img.shields.io/badge/license-MIT-141B2E)
22
+ ![Built with OctaneJS](https://img.shields.io/badge/built%20with-OctaneJS-2557D6)
23
+ ![Status](https://img.shields.io/badge/status-preview-8290A8)
24
+
25
+ </div>
26
+
27
+ The sandbox is the workshop of [Flowdular](https://github.com/flowdular/flowdular),
28
+ the agentic foundation framework. It builds a change in an isolated workspace,
29
+ drives the coding agent your team already uses inside it, runs the same gates
30
+ the platform runs, and shows the result in the real application shell. A
31
+ session carries as many modules as the work touches: one turn writes in one
32
+ module, and the whole set is previewed and delivered together.
33
+
34
+ ```bash
35
+ npx @flowdular/sandbox # from a Flowdular workspace
36
+ npx @flowdular/sandbox --port 4320 --workspace /path/to/workspace
37
+ ```
38
+
39
+ The sandbox is published independently and depends on `@flowdular/sdk`. The SDK
40
+ does not include this application or the `flowdular-sandbox` launcher. Use a
41
+ sandbox release compatible with the application's SDK version; existing SDK
42
+ applications do not need their platform dependencies changed just to launch it.
43
+
44
+ The launcher finds the workspace by walking up to `flowdular.json`, then opens
45
+ http://127.0.0.1:4320.
46
+
47
+ ## What it does
48
+
49
+ - **Turns a brief into a module.** A planner names the modules and the first
50
+ specialist; business, UX, backend, frontend and agentic roles hand off inside
51
+ one session.
52
+ - **Runs your coding agent.** Claude Code or Codex CLI on your machine, or a
53
+ key you bring yourself. The sandbox never ships a model of its own.
54
+ - **Keeps the work isolated.** Every session gets its own pnpm workspace, its
55
+ own ephemeral databases, and a preview account that never touches your data.
56
+ - **Gates every turn.** Spec schema, module schema, dependencies, typecheck,
57
+ tests and format run against the draft before anything can land.
58
+ - **Delivers as code.** Eject into `modules/` and enable it, or open a pull
59
+ request with the gate evidence attached.
60
+
61
+ ## Requirements
62
+
63
+ Node.js 22.22.2 or newer, pnpm 11, a Flowdular workspace (a directory with
64
+ `flowdular.json`), and a running Flowdular application to connect to.
65
+
66
+ ## Documentation
67
+
68
+ - [Flowdular repository](https://github.com/flowdular/flowdular)
69
+ - [Architecture blueprint](https://github.com/flowdular/flowdular/blob/main/docs/architecture-blueprint.md)
70
+ - [Module contract (AGENTS.md)](https://github.com/flowdular/flowdular/blob/main/AGENTS.md)
71
+ - [Design system](https://github.com/flowdular/flowdular/blob/main/docs/design-system.md)
72
+
73
+ ## Connecting
74
+
75
+ The sandbox is a client of a running Flowdular application. It never opens the
76
+ platform database.
77
+
78
+ 1. In the application, open Administration, API tokens, and issue a token with
79
+ `sandbox.access.use` plus the read scopes the preview should see. Add
80
+ `sandbox.preview.data` for live data and `sandbox.modules.eject` for eject.
81
+ 2. Paste the token and the application address into the sandbox connect screen.
82
+
83
+ The token is encrypted at rest with a key in `.flowdular/sandbox/secret.key` and
84
+ is never returned to the browser. The application may run anywhere: the sandbox
85
+ works the same against `http://127.0.0.1:4310` and a deployment.
86
+
87
+ Sign-in is enforced on the platform side: the token authenticates as its
88
+ account, and `sandbox.core` must hold an active grant for it.
89
+
90
+ ## Security model
91
+
92
+ Every request to the sandbox API is checked before it does anything:
93
+
94
+ - `loopback` mode trusts the machine only. A request must be addressed to the
95
+ loopback name and the port the launcher bound (`Host: 127.0.0.1:4320` or
96
+ `localhost:4320`); a page on another local port cannot drive the sandbox by
97
+ name. State and configuration calls do not require a connection, because they
98
+ are how one gets connected; everything else does.
99
+ - `self-hosted` mode requires a browser session: `POST /sandbox/api/connect`
100
+ with an API token sets an `HttpOnly; SameSite=Strict; Secure` cookie that
101
+ expires after 12 hours. Without it, `GET /sandbox/api/state` answers 401 with
102
+ the mode and the application address, and nothing else.
103
+ - Every mutation must come from the sandbox page: `Sec-Fetch-Site` (when the
104
+ browser sends it) and `Origin` must match the sandbox host, and the request
105
+ must carry `x-flowdular-sandbox: 1`. A cross-site form post can do neither, so
106
+ it is refused with 403 before any body is read.
107
+ - The mode is the launcher's decision (`--mode`, `FD_SANDBOX_MODE`) and is
108
+ never accepted over HTTP. Changing the application address needs the token
109
+ for that application in the same request, so a stored token is never replayed
110
+ to another host.
111
+ - Session ids are UUIDs. Anything else in a `:id` segment or the preview cookie
112
+ is refused before a path is built from it, and a delete additionally checks
113
+ that the real path stays inside `.flowdular/sandbox/sessions`.
114
+ - Capabilities (`sandbox.modules.eject`, `sandbox.preview.data`) are read from
115
+ the acting principal: the browser session in `self-hosted` mode, the
116
+ configured connection in `loopback` mode.
117
+ - The preview API (`/api/*`) carries the same sandbox authentication. The bridge
118
+ never forwards `/api/auth/*` or `/api/sandbox/*`, and the preview's own
119
+ authentication runtime keeps sign-up closed: the only account is the seeded
120
+ preview account.
121
+ - Session views expose workspace-relative paths only. The preview loads draft
122
+ sources through `/preview-module/<session>/<module>/...`, which the sandbox
123
+ maps onto the session workspace itself.
124
+
125
+ ## Modes
126
+
127
+ | Mode | Binding | Coding agents |
128
+ | ------------- | -------------------- | ----------------------------------- |
129
+ | `loopback` | loopback interface | local `claude` and `codex`, or BYOK |
130
+ | `self-hosted` | configured interface | BYOK only |
131
+
132
+ `--mode` selects it, and a non-loopback `--host` forces `self-hosted`. A
133
+ sandbox that cannot prove it is loopback never offers a local binary, because a
134
+ local binary carries the operator's own login.
135
+
136
+ ## Sessions
137
+
138
+ The home dashboard lists the operator's ideas, current stages, recorded token
139
+ usage and provider-reported costs. A session summary breaks usage down by
140
+ specialist and offers archive, reject, restore and delete actions. Unknown
141
+ amounts are marked rather than treated as free work. See
142
+ [dashboard accounting and limitations](../../docs/sandbox-dashboard.md).
143
+
144
+ A session owns a directory under `.flowdular/sandbox/sessions/<id>`:
145
+
146
+ - `workspace/` is a Flowdular workspace and a pnpm workspace of its own: the
147
+ draft modules under `modules/`, the manifests of every other enabled module
148
+ for dependency validation, `reference/` with read-only copies of the platform
149
+ contracts, one complete example module and the skills under
150
+ `.ai/skills`, and the pointer file the coding agent auto-loads (`CLAUDE.md`
151
+ for claude, `AGENTS.md` for codex).
152
+ - `base/modules/<directory>` is the pristine copy an edit diffs against, one
153
+ per edited module.
154
+ - `chat.jsonl` is the durable transcript, including every agent event.
155
+ - `session.json` is the record. Its shape:
156
+
157
+ ```json
158
+ {
159
+ "id": "<uuid>",
160
+ "kind": "edit-module",
161
+ "moduleId": "parties.core",
162
+ "moduleSuffix": "parties",
163
+ "modules": [
164
+ { "id": "parties.core", "directory": "parties", "kind": "edit" },
165
+ { "id": "catalog.core", "directory": "catalog", "kind": "edit" }
166
+ ],
167
+ "title": "...",
168
+ "brief": "...",
169
+ "blueprint": "edit-module@1.0.0",
170
+ "role": "backend-engineer",
171
+ "driver": "codex",
172
+ "model": null,
173
+ "resumeIds": { "codex": "<thread id>" },
174
+ "autoContinue": true,
175
+ "chainDepth": 0,
176
+ "state": "previewing",
177
+ "createdAt": 0,
178
+ "updatedAt": 0,
179
+ "ejectedAt": null,
180
+ "archivedAt": null,
181
+ "registeredWithPlatform": true
182
+ }
183
+ ```
184
+
185
+ `modules` is the session: every entry is materialized in the workspace, diffed
186
+ against its own base, gated, previewed (all server routes and all client
187
+ contributions compose together) and delivered in one eject. `modules[0]` is the
188
+ primary module and is repeated as `moduleId` and `moduleSuffix`, which stay in
189
+ sync with it; the primary never changes after creation, so everything keyed on
190
+ it keeps working.
191
+
192
+ ### Modules of a session
193
+
194
+ The planner names them from the brief. Rules first: every existing module the
195
+ brief names by its whole dotted id (`parties.core`) or as `module parties` is a
196
+ change to that module, in the order the brief names them, and a `<domain>.core`
197
+ id the workspace does not have is a new module. A bare English word never
198
+ selects a module, and a file name (`package.json`) is never read as one. The
199
+ planner agent may name more than the rules found; it can never turn a known
200
+ module into a new one, and it can never drop a module the brief named. The
201
+ classification is the first system entry of the transcript.
202
+
203
+ A session can gain a module afterwards:
204
+
205
+ ```
206
+ POST /sandbox/api/sessions/:id/modules { "moduleId": "catalog.core" }
207
+ ```
208
+
209
+ It copies that workspace module into `workspace/modules/<directory>` and
210
+ `base/modules/<directory>`, appends it to `modules`, regenerates the workspace
211
+ manifests so the new draft is a project of the session's pnpm workspace (and no
212
+ longer a `link:` override), re-runs the install when the new `package.json`
213
+ changes the dependency signature, records a system entry, and answers with the
214
+ refreshed session view. Refusals, each a stable error code: `404
215
+ MODULE_NOT_FOUND` for a module this workspace does not have, `409
216
+ MODULE_ALREADY_IN_SESSION`, `409 SESSION_RUNNING` while a turn is in flight,
217
+ `409 SESSION_ARCHIVED`, and `409 SESSION_DELIVERED`. A checkpoint taken before
218
+ the module joined has no snapshot of it, so a rollback to that point leaves the
219
+ new module's files alone.
220
+
221
+ ### One turn, one module
222
+
223
+ The specialist works in a single module per turn, while the session context
224
+ lists all of them. The turn body takes an optional `module` (the draft module
225
+ directory):
226
+
227
+ ```
228
+ POST /sandbox/api/sessions/:id/turn { "message": "...", "module": "catalog" }
229
+ ```
230
+
231
+ Without it, the module the last handoff named decides, and otherwise the
232
+ primary. A `module` the session does not carry is refused with
233
+ `MODULE_NOT_IN_SESSION`. The active module is what the instruction calls the
234
+ target module, its directory is what the role's `allowedPaths` resolve against
235
+ (so a turn may write in that module only), and the state routing (specification,
236
+ manifest, server, screen) reads that module. The instruction also lists every
237
+ module of the session and says which one this turn owns. Each handoff carries
238
+ the module it belongs to: a failed gate hands the fix back in the module the
239
+ gate ran in, and an automatically continued turn stays there. Transcript entries
240
+ carry their module, so the conversation says where each turn worked.
241
+
242
+ ### Dependencies
243
+
244
+ The session workspace installs for real. `package.json` and
245
+ `pnpm-workspace.yaml` are generated at creation: the draft modules are the
246
+ workspace projects, every other workspace package (`@flowdular/*`, the other
247
+ modules) is an `overrides` entry pointing at the live checkout with `link:`,
248
+ the host `pnpm-lock.yaml` seeds the resolution so versions match the platform,
249
+ and the host `patches/` travel along. `pnpm install --offline` runs at creation
250
+ and again whenever a draft module's `package.json` changes (the fallback is
251
+ `--prefer-offline`, which fetches only what the store lacks). Measured on this
252
+ machine: an edit session of `parties.core` installs in about 0.5 s with the
253
+ seeded lockfile, a fresh new-module session in about 1 s, a re-run with
254
+ nothing changed in about 0.2 s. A failed install is reported as a failed
255
+ `dependencies` gate with the installer's output.
256
+
257
+ A draft that depends on another draft of the same session resolves the session
258
+ copy, because both are projects of that workspace; a module that is not part of
259
+ the session resolves the live checkout.
260
+
261
+ New module sessions start from a specification. Once it is approved, the
262
+ sandbox runs the same `module new` capability the CLI exposes, so the skeleton
263
+ is never hand-written, and formats the skeleton with the workspace Prettier
264
+ settings right away. Files the business manager already wrote under the module
265
+ (translations, for example) step aside for the scaffold and come back over it.
266
+
267
+ ### Lifecycle
268
+
269
+ A session can be archived, restored, and deleted from its dashboard summary,
270
+ or from the platform CLI (`flowdular sandbox session-archive`,
271
+ `flowdular sandbox session-delete`, dry run by default). Archived sessions are
272
+ hidden until "Show archived" and refuse new turns until restored. Deleting
273
+ removes the workspace, the base copy and the preview data; the record and the
274
+ transcript stay as a tombstone unless `keepTranscript: false` is passed. Both
275
+ actions refuse a session with a running turn unless asked to stop it
276
+ (`stop: true`), and both are recorded on the platform as
277
+ `sandbox.session.archived`, `sandbox.session.restored`,
278
+ `sandbox.session.deleted`.
279
+
280
+ ### Attachments
281
+
282
+ An operator can paste a screenshot or attach files (concepts, mockups, specs)
283
+ to a turn to show what they want changed. Attachments belong to the session:
284
+
285
+ - The bytes are stored under `.flowdular/sandbox/sessions/<id>/attachments/<attachmentId>-<safeName>`
286
+ and copied into the session workspace at `workspace/reference/attachments/<safeName>`,
287
+ so the coding agent, which may only read inside the workspace, opens them by
288
+ name with its normal file tools.
289
+ - `attachmentId` is a UUID. `safeName` is the original filename reduced to
290
+ `[A-Za-z0-9._-]` with no path segments, no `..` and no leading dot, capped at
291
+ 128 characters and made unique within the session.
292
+ - Limits: at most 10 per session, 5 MB per file, and only these extensions,
293
+ verified by extension plus a magic-byte sniff for the image and pdf/svg
294
+ formats: `png`, `jpg`, `jpeg`, `gif`, `webp`, `md`, `txt`, `json`, `csv`,
295
+ `pdf`, `svg`. Anything else is refused with a stable error code.
296
+ - The record carries them as `attachments: [{ id, name, kind: 'image' | 'file',
297
+ size, addedAt }]`, backfilled to `[]` for older sessions.
298
+
299
+ The endpoints, each behind the same `authorize()`, same-origin and
300
+ `x-flowdular-sandbox` boundary as every other mutation, and each validating the
301
+ session id and attachment id before building a path:
302
+
303
+ - `POST /sandbox/api/sessions/:id/attachments` with `{ name, contentBase64 }`
304
+ returns the created attachment metadata.
305
+ - `POST /sandbox/api/sessions/:id/attachments/:attachmentId/delete` removes one.
306
+ - `GET /sandbox/api/sessions/:id/attachments/:attachmentId` serves the bytes
307
+ with the right content type, `Content-Disposition: inline` and
308
+ `Cache-Control: private, no-store`, for the composer thumbnail.
309
+
310
+ When a turn runs and the session has attachments, the instruction the driver
311
+ receives is prefixed with a short note naming them and their kinds and pointing
312
+ at `reference/attachments/`; the operator's own message follows it. The user
313
+ entry in the transcript records which attachments were included. Both composers
314
+ support paste and an attach button and show each file as a chip with a remove
315
+ control. The new-session screen holds the files until the session exists: it
316
+ creates the session, uploads them to it, and only then starts the first turn, so
317
+ that turn's prompt already names them. It refuses a file the sandbox would
318
+ refuse (wrong extension, empty, over 5 MB, more than 10) before uploading, and a
319
+ failed upload keeps the created session and stops instead of starting a turn
320
+ that cannot see the file.
321
+
322
+ ### Checkpoints
323
+
324
+ Every turn that changes files leaves a restore point, so an operator can roll a
325
+ session's workspace back to an earlier state when a coding agent goes wrong
326
+ without losing the transcript.
327
+
328
+ - A snapshot of each draft module tree is copied to
329
+ `.flowdular/sandbox/sessions/<id>/checkpoints/<sequence>/modules/<directory>`,
330
+ excluding `node_modules`; `base/`, `reference/` and attachments are never
331
+ snapshotted. One is taken at session creation as the pristine start
332
+ (`sequence` 0), and one after every turn that produced a diff, keyed by that
333
+ turn's handoff chat entry so the transcript line and its restore point share
334
+ one sequence.
335
+ - The record carries them as `checkpoints: [{ sequence, at, label, role }]`,
336
+ oldest first, backfilled to `[]` for older sessions. `label` is a short human
337
+ line (the role that produced it, `the starting point` for the initial one).
338
+ - Bounded to the last 24: when a new one exceeds the cap the oldest is pruned,
339
+ directory and all, except the start, which is never dropped.
340
+
341
+ Restore replaces the draft files, keeps the transcript, and appends a marker:
342
+
343
+ - `POST /sandbox/api/sessions/:id/checkpoints/restore` with `{ sequence }`,
344
+ behind the same `authorize()`, same-origin and `x-flowdular-sandbox` boundary
345
+ as every other mutation, validating the session id first. The path is distinct
346
+ from `/restore`, which un-archives a session.
347
+ - It replaces `workspace/modules/<directory>` with the snapshot (the current
348
+ contents step aside, `node_modules` stays so the install survives), invalidates
349
+ the diff cache, appends a `system` entry (`Restored the workspace to the state
350
+ after <label> (turn <sequence>).`), sets the state back to `editing`, and
351
+ returns the refreshed session view.
352
+ - Refusals, each a stable error code: `400 INVALID_SESSION_ID` for a hostile id,
353
+ `400 INVALID_INPUT` for an absent or non-integer sequence,
354
+ `400 CHECKPOINT_NOT_FOUND` for a sequence the session has no snapshot for,
355
+ `409 SESSION_RUNNING` while a turn is in flight, `409 SESSION_ARCHIVED` for an
356
+ archived session, and `409 SESSION_DELIVERED` once the session was ejected
357
+ (start a new session to change the module again).
358
+
359
+ Each agent turn and handoff block that has a matching checkpoint shows a quiet
360
+ `Restore to here` affordance with an inline confirm; it is disabled while the
361
+ session runs. Restoring reloads the session so the transcript picks up the
362
+ marker and the preview refreshes.
363
+
364
+ ## Roles and routing
365
+
366
+ Every turn is driven by one specialist with its own instruction, writable paths
367
+ and gates: business manager, UX designer, frontend engineer, backend engineer,
368
+ agentic engineer. They are workspace configuration in `.ai/agents/sandbox` and
369
+ can be edited per workspace.
370
+
371
+ Nobody picks an agent to start. A session begins with one brief, and the
372
+ planner classifies it: new module or change, which modules, what to call them,
373
+ and which specialist takes the first turn (see "Modules of a session" for the
374
+ rules). The classification is the first system entry of the transcript, so a
375
+ wrong guess is corrected in the first message.
376
+
377
+ Later turns route the same way, against the module the turn targets. Its state
378
+ decides who works next
379
+ (no specification means the business manager, no server means the backend
380
+ engineer, no screen means the frontend engineer), and the words of the request
381
+ only choose between specialists that are already valid for that state. An
382
+ answer to a question goes back to the specialist who asked it. Every routed
383
+ turn says who took it and why; the role picker in the composer overrides it for
384
+ one turn, and the module picker beside it overrides which module that turn
385
+ works in.
386
+
387
+ The role documents in `.ai/agents/sandbox` are the source of truth; the
388
+ bundled defaults in `#coding-agent` are regenerated from them with
389
+ `pnpm --filter #coding-agent sync-roles`, and a test fails when the
390
+ two disagree.
391
+
392
+ The coding agent is chosen when the session starts, next to the brief, and the
393
+ composer can change it for a later turn. A session remembers its agent, so a
394
+ handed-off turn runs on the same one.
395
+
396
+ ## Handoffs
397
+
398
+ A turn never ends in silence. Every role closes its final message with one
399
+ line, `HANDOFF: <role-id> - <why>` or `HANDOFF: none - <why>` (a display name
400
+ or a trailing full stop is forgiven), and the orchestrator turns that into the
401
+ next step:
402
+
403
+ - **continue**: the named specialist takes over with a prompt that carries the
404
+ original brief. The line is honoured only when it names a role the finishing
405
+ role may hand to (its `handoff` list) and never itself; otherwise the state
406
+ routing decides and the transcript says why. With **Auto handoff** on, which
407
+ is the default, the server starts the next turn by itself on the same stream,
408
+ up to four chained turns per operator message (`chainDepth` in the record).
409
+ With it off, the transcript shows a `Continue with <role>` button instead.
410
+ - **approval**: every new or edited module stops before implementation until
411
+ the operator approves its current specification. The approval route moves
412
+ the `status` line to `approved` and records the SHA-256 hash of that exact
413
+ text in the session. An `approved` line written by an agent is not authority.
414
+ Editing the specification or requesting changes makes the recorded hash
415
+ stale and opens the approval gate again. In a multi-module session each
416
+ affected module needs its own current approved hash.
417
+ - **question**: the turn changed nothing and needs an answer, whatever the
418
+ specification's status. The answer routes back to the specialist who asked.
419
+ - **review**: the specialist reports the request satisfied. Run the gates and
420
+ eject when the change looks right. A business manager who updated a
421
+ specification stops at approval; only the approved handoff starts its
422
+ implementer.
423
+ - **blocked**: the coding agent errored. Nothing continues on its own.
424
+
425
+ A failed gate is its own handoff: the specialist that caused it fixes it before
426
+ anyone else works, so a broken change never travels down the chain. The fix
427
+ prompt carries the gate command and the first 4000 characters of its output;
428
+ the transcript keeps the whole output (head and tail of a long one).
429
+
430
+ ## Turn lifetime
431
+
432
+ A turn runs to completion on the server whatever happens to the browser: the
433
+ driver, the gates and the handoff are written to the transcript even when the
434
+ tab closed after the first event. The response is a subscription to that turn,
435
+ and closing it only unsubscribes; stopping is an explicit action. The session
436
+ reports whether a turn is still running (`running` in the session view and
437
+ `running` ids in the state), and a browser that reopens it attaches to the live
438
+ stream (`GET /sandbox/api/sessions/:id/turn/stream`) until the chain ends.
439
+
440
+ A new turn on a session that already has one supersedes it, and waits for the
441
+ old process to exit before it starts. Coding agents keep one writer per
442
+ conversation thread, so resuming a thread whose previous process is still alive
443
+ fails; the sandbox removes that race, and both local drivers recover from a
444
+ lock they did not cause by continuing on a fresh thread or session with the
445
+ conversation replayed.
446
+
447
+ ## Eject
448
+
449
+ Eject is a delivery with its own screen. It carries every module of the session
450
+ in one delivery, and the plan lists them: per module the files that land, how
451
+ many are overwritten, how many the session deleted and will be removed, the
452
+ packages it adds that the workspace does not have yet, and whether it has to be
453
+ enabled in the platform (new modules only). One confirmation applies them all.
454
+ The plan also names the gates that run first and whether the connected
455
+ application has to restart. Confirming runs it step
456
+ by step and reports each step as it happens:
457
+
458
+ 1. every gate, one by one (module gates once per draft module),
459
+ 2. the copy into `modules/` for every module of the session, then the removal
460
+ of the files an edit deleted,
461
+ 3. `pnpm install`, which links workspace packages and fetches whatever a module
462
+ newly declares,
463
+ 4. `module enable` for each new module, through the same capability the CLI
464
+ exposes,
465
+ 5. `auth sync-scopes` for each module, which grants the scopes the module's
466
+ specification declares to every workspace owner, because a module nobody has
467
+ permission for is installed and invisible,
468
+ 6. a platform typecheck with the modules in the composition,
469
+ 7. optionally a full build,
470
+ 8. a restart note: the connected application loads the new composition and
471
+ runs new migrations only when it starts, so a local `pnpm dev` has to be
472
+ restarted, and a remote application redeployed.
473
+
474
+ A failing gate stops the delivery before anything is written, and a failing
475
+ step stops it there: the session is marked delivered only when every step
476
+ passed, and the failure carries the step's output. A delivered session says so
477
+ in the session list, with the time it landed, and the platform records
478
+ `sandbox.module.ejected` with what landed.
479
+
480
+ Delivery is a target behind one interface (`DeliveryTarget` in
481
+ `src/server/delivery/types.ts`): `available`, `plan`, `apply`. The eject
482
+ request names the target (`target: 'workspace' | 'git-pr' | 'official-modules'`, default from
483
+ configuration); the plan answer lists `availableTargets` with a reason for each
484
+ one that cannot be used here, and the screen offers the choice only when more
485
+ than one is usable.
486
+
487
+ ### Contribute to Official Modules (`official-modules`)
488
+
489
+ Choose **Send to Official Modules** in the eject dialog, then confirm the displayed
490
+ repository and source scope. This target accepts one new module with a current
491
+ human-approved specification and passing exact-source auto-review. GitHub access
492
+ to `Flowdular/official-modules`, git, gh and pnpm are required. The repository must
493
+ be accessible to the configured GitHub identity; private repositories remain private.
494
+
495
+ The host repeats all sandbox gates, clones the registry to a temporary directory,
496
+ refuses an existing module ID/directory, copies the reviewed source and runs the
497
+ registry's `pnpm verify`. It records source-bound evidence, packages an immutable
498
+ release, and opens a PR to `Flowdular/official-modules` against `main`. Contributors
499
+ without upstream push rights use a personal fork. Only module source, its review
500
+ and release artifact enter the commit. Credentials stay out of package-script
501
+ environments, command arguments and PR text. The local application is not enabled
502
+ or changed. Existing branches are never overwritten on retry; only an identical
503
+ source tree can be reused after an interrupted PR request.
504
+
505
+ The target is included in default delivery targets. Projects with an explicit
506
+ `sandbox.delivery.targets` list must add `official-modules`. Disabling GitHub
507
+ in sandbox settings disables this target too. SDK/CLI npm releases must exist for
508
+ the clean registry installation; an unavailable dependency stops before any push.
509
+
510
+ This action submits code for maintainer review. It does not publish the registry,
511
+ merge the PR or bypass PostgreSQL CI. Existing registry modules and multi-module
512
+ changes use the contributor skills and a manually prepared branch for now.
513
+
514
+ ### As a pull request (`git-pr`)
515
+
516
+ The same change, committed on a branch and pushed, so review happens in the
517
+ repository and nothing in this working tree moves. One pull request carries
518
+ every module of the session, which makes it the unit for a change that spans
519
+ modules. Available when the workspace is a git work tree with at least one
520
+ commit, the configured remote exists, and the base branch can be read. A pull
521
+ request is opened when `gh` is signed in or a provider token is sealed in the
522
+ sandbox configuration. Without usable GitHub authentication the sandbox still
523
+ pushes the branch and returns a compare link. Set
524
+ `sandbox.delivery.git.provider` to `none` when that is always the intended
525
+ result. The steps:
526
+
527
+ 1. every gate, as above,
528
+ 2. `git fetch <remote> <base>`, a detached worktree of `<remote>/<base>` under
529
+ `.flowdular/sandbox/worktrees/<session>`, and the branch
530
+ `<prefix>/<module-dir>-<session id prefix>` in it,
531
+ 3. the copy and the removals into the worktree,
532
+ 4. `pnpm install --offline` there (`--prefer-offline` when a package is new),
533
+ 5. `module enable` for each new module, with the worktree as its root,
534
+ 6. a platform typecheck in the worktree,
535
+ 7. the guardrail check: `git status` in the worktree may list only
536
+ `modules/<dir>/**` of the session's modules and `pnpm-lock.yaml`. A delivery
537
+ with a new module may also change `flowdular.json`, `platform/package.json`
538
+ and `platform/src/generated/**`;
539
+ the count stays within `sandbox.delivery.maxChangedFiles` (else the
540
+ `.ai/policies/task-budgets.yaml` figure for the session kind); new packages
541
+ stay within `maxNewDependencies`. A violation names the paths and stops
542
+ before anything is committed,
543
+ 8. a check that an existing local or remote session branch belongs to this
544
+ session, `git add` of the allowed paths, a commit
545
+ `sandbox: add|update <module id>` with the session id and the gate summary,
546
+ `git push -u --force-with-lease`,
547
+ 9. `gh pr create` with a plain body: what changed, the gate table, the file
548
+ list (added, modified, removed), the specification versions and field diff,
549
+ detected deployment risks, the post-merge
550
+ `pnpm flowdular auth sync-scopes --module <id> --apply`, the session id, and a
551
+ reviewer note when `.ai/policies/path-ownership.yaml` says a cross-owner
552
+ change needs one. A second delivery of the same session updates the branch
553
+ and keeps the open pull request.
554
+
555
+ The worktree is removed whatever the outcome; the branch is kept on success
556
+ and deleted on failure. A newly pushed remote branch is also removed when PR
557
+ creation fails, while an earlier branch for the same session is preserved.
558
+ `sync-scopes` is not run: it is a runtime action on
559
+ the deployment's database, so it is the post-merge step in the body. Git,
560
+ GitHub CLI and pnpm receive only an allowlisted process environment. The raw
561
+ sealed token is passed only to GitHub CLI as `GH_TOKEN`; Git receives only a
562
+ process-local authorization header. Command output and pull request summary
563
+ text are redacted before they can reach the browser or GitHub.
564
+
565
+ The done screen shows the pull request (or compare) link, and
566
+ `.flowdular/sandbox/sessions/<id>/delivery.json` keeps the branch and the URL
567
+ for the session.
568
+
569
+ Open **Delivery settings** on the dashboard to configure GitHub delivery for
570
+ this sandbox. Project defaults apply unless local overrides are selected.
571
+ Repository, reviewers and delivery mode appear first; the source remote,
572
+ base branch, branch prefix and fork owner are under **Advanced settings**.
573
+ Saving the form does not create, push or merge a pull request. Push modes are:
574
+
575
+ - `auto` uses direct delivery only after GitHub confirms push permission and
576
+ refuses otherwise,
577
+ - `direct` pushes the session branch to the configured repository,
578
+ - `fork` uses the configured fork owner or the account returned by GitHub, and
579
+ creates the fork only after the operator confirms the eject.
580
+
581
+ The sandbox never creates a fork in `auto` mode. Selecting `fork` is the
582
+ operator's explicit consent. The repository must ignore `.flowdular/`; delivery
583
+ is refused otherwise so the temporary worktree cannot dirty the active
584
+ checkout.
585
+
586
+ The optional token is encrypted in `.flowdular/sandbox/config.json` with the
587
+ local sandbox key. The browser receives only its eight-character fingerprint.
588
+ Git receives it through process-local configuration, never in a command
589
+ argument or remote URL. The repository settings below remain authoritative
590
+ until the operator checks **Use custom settings for this sandbox**.
591
+ That local override never modifies `flowdular.json` and can be turned
592
+ off again from the same form.
593
+
594
+ Configuration lives in `flowdular.json`, all of it optional (defaults shown):
595
+
596
+ ```json
597
+ {
598
+ "sandbox": {
599
+ "delivery": {
600
+ "default": "workspace",
601
+ "targets": ["workspace", "git-pr"],
602
+ "git": {
603
+ "remote": "origin",
604
+ "baseBranch": "main",
605
+ "branchPrefix": "sandbox",
606
+ "provider": "github",
607
+ "reviewers": []
608
+ },
609
+ "maxChangedFiles": 18
610
+ }
611
+ }
612
+ }
613
+ ```
614
+
615
+ `provider: "none"` pushes the branch without opening a pull request.
616
+ `maxChangedFiles` is unset by default, which means the task budget applies.
617
+ The block is validated by `packages/contracts/schemas/project.schema.json`
618
+ (`pnpm flowdular doctor`) and read at request time.
619
+
620
+ ## Gates
621
+
622
+ The session's **Check modules** action opens per-module results with a plain
623
+ status and expandable diagnostic output. A skipped check is not a success.
624
+ Specification approval requires the matching module's readable review; switching
625
+ the specification editor to another module disables saving until that document
626
+ loads. A failed turn start retains the user's message and releases the composer.
627
+
628
+ On desktop the preview is a floating card over the right side of the chat
629
+ surface, with room reserved for messages and the composer. **Preview** also opens
630
+ the full-screen workbench on mobile, including sessions without file changes.
631
+
632
+ The browser regression in `tests/browser/session-workflow.mjs` exercises the real
633
+ UI with synthetic session, model and delivery responses. Start a sandbox against
634
+ a disposable workspace, then run:
635
+
636
+ ```bash
637
+ node packages/sandbox/tests/browser/session-workflow.mjs http://127.0.0.1:4438
638
+ ```
639
+
640
+ It requires Playwright with Chromium available. If Playwright is provided by an
641
+ external runtime, set `PLAYWRIGHT_MODULE` to its absolute `index.mjs` path.
642
+ Screenshots and results are saved in a temporary directory. No paid model calls,
643
+ platform mutations or GitHub operations are made by this regression.
644
+
645
+ Gates are a fixed list run by the sandbox: `spec-schema` and `module-schema`
646
+ once per session workspace, `dependencies`, `typecheck`, `tests` and `format`
647
+ once per draft module, with the module's own binaries from the session install.
648
+ After a turn the module gates run for the modules that hold changes, not for
649
+ every module of the session, and every result names the module it ran in. The
650
+ `dependencies` gate runs after every turn that changed files, whatever the role
651
+ lists. A failing gate is written back into the conversation, with its
652
+ command and output, so the next turn can fix it. An agent whose driver has a
653
+ shell may run the same commands itself; the sandbox still runs them after the
654
+ turn.
655
+
656
+ ## Preview
657
+
658
+ The preview renders the session's draft modules inside the real application
659
+ shell, so a screen looks exactly as it will in production, including the
660
+ navigation entry and dashboard widgets each module contributes. Every draft with
661
+ a client entry composes into the same shell, so the navigation shows all of
662
+ them; the module selector in the preview head opens the preview on one module's
663
+ first screen (`/preview/<session>?module=<directory>`), and the same selection
664
+ filters the diff.
665
+
666
+ The preview API is composed, not stubbed. A request from a preview screen is
667
+ answered in this order:
668
+
669
+ 1. The draft module's own routes, loaded from `src/platform.ts` in the session
670
+ workspace and reloaded whenever its sources change.
671
+ 2. The session's own `auth.core` routes, running on an ephemeral database with
672
+ a seeded preview account.
673
+ 3. The bridge to the connected application, for everything the draft does not
674
+ own.
675
+
676
+ That gives a draft screen a real principal, its declared scopes, a real CSRF
677
+ contract, and a real database of its own, while still reading live records from
678
+ other modules. Preview data has two modes: `fixtures` is fully offline, and
679
+ `bridge` forwards the leftovers to the connected application with the sandbox
680
+ token. The bridge is read only and refuses without the `sandbox.preview.data`
681
+ scope.
682
+
683
+ ### Auto-review
684
+
685
+ At the final completion handoff, a missing or stale `auto-review` result routes the
686
+ same specialist to a separate read-only review turn. The report covers correctness,
687
+ security, compatibility, lifecycle, tests and UI, with specific evidence. Findings
688
+ return to implementation. A passing report is stored by the server outside the
689
+ agent workspace and tied to every module file's content. Later edits invalidate it.
690
+ With auto-continue disabled, continue the generated review handoff manually.
691
+
692
+ All eject targets require a current report for every delivered module and passing
693
+ schema, dependency, typecheck, test and format checks. Missing or skipped results
694
+ block eject; empty test suites fail. Old sessions need a review before delivery.
695
+ The report is a model assessment and does not guarantee correctness; executable
696
+ checks remain mandatory, and operator spec approval stays separate.