create-flowdular 0.6.0 → 0.6.2
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.
- package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
- package/agent-template/.agents/skills/module-update/SKILL.md +1 -1
- package/agent-template/.agents/skills/perf-audit/SKILL.md +1 -1
- package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.ai/agents/README.md +1 -1
- package/agent-template/.ai/agents/sandbox/agentic-engineer.md +1 -1
- package/agent-template/.ai/agents/sandbox/backend-engineer.md +1 -1
- package/agent-template/.ai/agents/sandbox/business-manager.md +2 -4
- package/agent-template/.ai/agents/sandbox/frontend-engineer.md +1 -1
- package/agent-template/.ai/agents/sandbox/ux-designer.md +1 -1
- package/agent-template/.ai/platform-capabilities.md +4 -4
- package/agent-template/.ai/policies/model-routing.yaml +2 -1
- package/agent-template/.ai/policies/task-budgets.yaml +1 -1
- package/agent-template/.ai/references/catalog/migrations/0001_catalog_core.up.sql +2 -2
- package/agent-template/.ai/references/catalog/migrations/0002_catalog_history.up.sql +2 -2
- package/agent-template/.ai/references/catalog/migrations/0003_catalog_history_service_actors.up.sql +2 -2
- package/agent-template/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.up.sql +2 -2
- package/agent-template/.ai/references/catalog/module.json +3 -3
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +3 -3
- package/agent-template/.ai/references/catalog/src/client/CatalogItemForm.tsrx +2 -2
- package/agent-template/.ai/references/catalog/src/services/migration.ts +8 -8
- package/agent-template/.ai/references/catalog/tests/migrations.test.ts +1 -1
- package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
- package/agent-template/.ai/skills/module-update/SKILL.md +1 -1
- package/agent-template/.ai/skills/perf-audit/SKILL.md +1 -1
- package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
- package/agent-template/.claude/skills/module-update/SKILL.md +1 -1
- package/agent-template/.claude/skills/perf-audit/SKILL.md +1 -1
- package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/docs/adr/0003-module-settings.md +2 -0
- package/agent-template/docs/agent-contract.md +1 -1
- package/agent-template/docs/cli-extensions.md +1 -0
- package/agent-template/docs/cli.md +16 -0
- package/agent-template/docs/configuration.md +32 -4
- package/agent-template/docs/database-adapters.md +10 -2
- package/agent-template/docs/design-system.md +6 -2
- package/agent-template/docs/getting-started.md +5 -1
- package/agent-template/docs/module-distribution.md +1 -2
- package/agent-template/docs/modules.md +5 -3
- package/agent-template/docs/sandbox.md +64 -7
- package/package.json +1 -1
- package/template/default/.env.example +5 -0
- package/template/default/infra/docker/.env.example +5 -0
- package/template/default/infra/docker/Dockerfile +5 -1
- package/template/default/infra/docker/compose.yaml +4 -0
- package/template/default/infra/kubernetes/deployment.yaml +5 -0
- package/template/default/infra/sdk-module-manifests.mjs +118 -0
- package/template/default/infra/vercel/build.mjs +9 -0
- package/template/default/modules/example/package.json +1 -1
- package/template/default/package.json +2 -3
- package/template/default/platform/octane.config.ts +53 -15
- package/template/default/platform/package.json +1 -1
- package/template/default/platform/scripts/dev.mjs +66 -21
- package/template/default/platform/src/server/lifecycle.ts +325 -0
- package/template/default/platform/src/server/setup/modules.ts +28 -32
- package/template/default/platform/src/server/setup/page.ts +54 -3
- package/template/default/platform/src/server/setup/routes.ts +1 -0
- package/template/default/platform/src/server/setup/seed.ts +51 -4
- package/agent-template/.ai/references/catalog.provenance.json +0 -65
|
@@ -20,3 +20,5 @@ The first setting is `auth.core.allowSignUp`. The server is authoritative and re
|
|
|
20
20
|
- Administration: `GET /api/settings` (`system.settings.read`) lists every declaration with its current tenant value and metadata; `POST /api/settings/update` (`system.settings.manage`, session only) validates against the declaration, stores or clears the value, and appends `settings.updated` to the auth audit trail. Secrets are write-only: the API returns whether a value is set, never the value. Administration > Modules renders the selected module's declarations in its Settings drawer section. Administration > Settings contains only workspace and organization settings.
|
|
21
21
|
- `emailConfirmation` cannot be enabled while no mail transport is composed; the API refuses with `MAIL_TRANSPORT_REQUIRED` and the screen shows the setting as locked.
|
|
22
22
|
- The cross-module read rule (declared dependency, shared, non-secret) is not enforced by `get`; it is a review rule until a requester-aware read exists.
|
|
23
|
+
- Platform-scoped writes (amendment, 2026-10-06): every workspace owner holds `system.settings.manage`, so `POST /api/settings/update` changes a platform-scoped setting only for a principal of the operator workspace, the tenant id the deployment sets in `FD_OPERATOR_TENANT`, and refuses anyone else with 403 `PLATFORM_SETTING_OPERATOR_ONLY`; `GET /api/settings` locks those rows outside it. Unset, no workspace changes one.
|
|
24
|
+
- Recorded operator workspace (amendment, 2026-10-06, 0.6.1): auth.core records the operator workspace in `auth_operator_workspace`, at most one row under forced row-level security bound to the workspace it names. Any path that creates the first workspace of an empty database records it in the same transaction, a deployment with exactly one workspace and no record records it when auth.core opens the database, and `pnpm flowdular auth operator-set` is the only way to change it. `FD_OPERATOR_TENANT` becomes an override: set, it alone decides, and a value that is not a workspace id leaves no operator. system.core resolves the operator on every settings request; while none is known every platform row is locked with `system.settings.platformOperatorUnset` and the write refusal keeps `PLATFORM_SETTING_OPERATOR_ONLY`. This supersedes "unset, no workspace changes one" above.
|
|
@@ -18,7 +18,7 @@ Use the already selected task skill. Consult `.ai/references/catalog` for implem
|
|
|
18
18
|
7. Every mutation calls `sessionMutationDenial(octane, auth)` first and reads its body with `readJsonObject` plus `requiredString`, `optionalString`, `requiredInteger`. Clients send `content-type: application/json`, `x-csrf-token`, and `credentials: 'same-origin'`.
|
|
19
19
|
8. Routes mount only through `src/platform.ts` exporting `createServerComposition(context)` with `platform.server: true` in `module.json` and a `./platform` export in `package.json`; the context carries `auth`, `settings`, `agentTools`, `agentDefinitions`, `capabilities` and `databases`. A database module passes `context.databases` into one runtime, which acquires and releases one provider lease lazily; `prepare` stays read-only and never opens a database. A composition may return `settings`, read-only `prepare`, `start`, background-work `stop`, and final `dispose`. Client contributions mount only through `createClientContribution(context)` in `src/client/index.ts` with `platform.client: true`. `pnpm flowdular module validate` fails on a missing entry (`PLATFORM_*`).
|
|
20
20
|
9. Never edit the composition by hand: `platform/octane.config.ts`, `platform/src/App.tsrx`, `platform/src/generated/**`, `platform/package.json` dependencies and `modules.enabled` in `flowdular.json` are written by `pnpm flowdular module enable <id> --apply` and `pnpm flowdular module sync --apply`.
|
|
21
|
-
10. `pnpm flowdular module enable <id> --apply` grants the spec permissions to every tenant owner as its last step; `pnpm flowdular auth sync-scopes --module <id> --apply` re-grants later (new permission, another database). Members receive scopes only through `MEMBER_SCOPES` in `modules/auth/src/acl/scopes.ts`, a core change.
|
|
21
|
+
10. `pnpm flowdular module enable <id> --apply` grants the spec permissions to every tenant owner as its last step; first-run setup grants every enabled module's permissions to the owner it creates, so a module enabled before the first workspace needs no extra step; `pnpm flowdular auth sync-scopes --module <id> --apply` re-grants later (new permission, another database). Members receive scopes only through `MEMBER_SCOPES` in `modules/auth/src/acl/scopes.ts`, a core change.
|
|
22
22
|
11. Declare every imported package in the module `package.json`; the sandbox `dependencies` gate and the eject fail otherwise. Every relative import carries its `.ts` or `.tsrx` extension.
|
|
23
23
|
12. Use the CLI for discovery, validation and scaffolding: `doctor`, `spec validate`, `module validate`, `module new`, `module enable`, `auth sync-scopes`. Run destructive, external or production capabilities only with what the runner demands (`--apply`, `--confirm`, `--spec`) and never work around a refusal.
|
|
24
24
|
13. Gates are `spec-schema`, `module-schema`, `dependencies`, `typecheck`, `tests`, `format`. The sandbox runs `dependencies` plus the ones your role lists after every turn, per draft module, and feeds a failure back to you; with a shell you may run the module's own gate commands yourself, never installs, network or git. From a checkout run them yourself and `pnpm verify` before any pull request.
|
|
@@ -77,6 +77,7 @@ The CLI imports this code only after the exact command or capability is invoked.
|
|
|
77
77
|
- External and non-local destructive module capabilities remain disabled until a signed approval verifier is configured.
|
|
78
78
|
- A workspace-local destructive capability must declare `localOnly` and a typed confirmation token. It remains dry-run unless both `--apply` and the exact `--confirm` value are present, and it is blocked outside development and test.
|
|
79
79
|
- `capability list`, `capability describe`, and `capability run` use the same descriptors and handlers as direct commands.
|
|
80
|
+
- A command refuses by throwing. An error with a stable `code` (`^[A-Z][A-Z0-9_]{0,63}$`) and a numeric HTTP `status`, the shape of every module service error, keeps its code in the envelope and the human output. So does a coded error from a platform service the runner hands the command, such as a `DatabaseError` from `context.databases` or a refused local database. Any other error is reported as `COMMAND_FAILED`. Only the code and the message are printed, never the stack, the cause or other fields.
|
|
80
81
|
- Core commands may reuse an extension: `module enable <id> --apply` runs the `auth.scopes.sync` capability of `auth.core` after regenerating the composition, so a freshly enabled module is visible to workspace owners without a second command.
|
|
81
82
|
|
|
82
83
|
The complete customer example is in `.ai/examples/customer-cli-extension`. New module scaffolds include the catalog and implementation files when the approved spec declares the `cli` capability.
|
|
@@ -134,6 +134,8 @@ flowdular auth sync-scopes --module <id> [--apply] # re-grant a module's scopes
|
|
|
134
134
|
flowdular auth workspaces [--limit <n>] # workspaces of this deployment and their owners
|
|
135
135
|
flowdular auth workspace-create --name <name> --owner-email <email> --owner-name <name> [--slug <id>] [--password-env <VAR>] [--actor <label>] [--apply]
|
|
136
136
|
flowdular auth member-add --workspace <slug|id> --email <email> [--role <key>] [--actor <label>] [--apply]
|
|
137
|
+
flowdular auth operator # the recorded operator workspace and whether FD_OPERATOR_TENANT overrides it
|
|
138
|
+
flowdular auth operator-set <id|slug> [--actor <label>] [--apply]
|
|
137
139
|
flowdular auth secrets-rotate [--apply] # re-seal enrolled TOTP secrets with the current MFA key
|
|
138
140
|
flowdular auth greenfield # destructive local auth reset (setup quick)
|
|
139
141
|
|
|
@@ -197,6 +199,20 @@ Both commands append an audit row to the workspace trail whose actor is
|
|
|
197
199
|
what exists, with each workspace's owners, so the slug or id for the other
|
|
198
200
|
commands is at hand.
|
|
199
201
|
|
|
202
|
+
The first workspace of an empty database is recorded as the operator
|
|
203
|
+
workspace, the one whose owners change the branding and the other platform
|
|
204
|
+
settings. `auth operator` shows the record and says whether
|
|
205
|
+
`FD_OPERATOR_TENANT` is set in that shell, which overrides the record wherever
|
|
206
|
+
the deployment sets it. `auth operator-set` names another workspace, previews
|
|
207
|
+
without `--apply`, and with it releases the current operator and records the
|
|
208
|
+
new one, each with an audit row in that workspace's trail. If it fails between
|
|
209
|
+
the two, no workspace is the operator until the same command runs again.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
flowdular auth operator
|
|
213
|
+
flowdular auth operator-set northwind --apply
|
|
214
|
+
```
|
|
215
|
+
|
|
200
216
|
## Workspace scripts
|
|
201
217
|
|
|
202
218
|
```bash
|
|
@@ -18,6 +18,32 @@ deployments must set the secret keys.
|
|
|
18
18
|
| `FD_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
|
|
19
19
|
| `FD_METRICS` | `false` | Expose `GET /api/metrics`; see [operations.md](operations.md) |
|
|
20
20
|
| `FD_METRICS_TOKEN` | none | Bearer token a metrics scrape must present |
|
|
21
|
+
| `FD_OPERATOR_TENANT` | none | Tenant id that overrides the recorded operator workspace |
|
|
22
|
+
|
|
23
|
+
A platform-scoped module setting has one value for every workspace: the
|
|
24
|
+
branding, sign-up and session policy, the mail relay and the platform settings
|
|
25
|
+
of other modules. Only the operator workspace changes them: a principal there
|
|
26
|
+
holding `system.settings.manage` edits them in Administration. Every other
|
|
27
|
+
workspace sees them read-only, and a write from it is refused with 403
|
|
28
|
+
`PLATFORM_SETTING_OPERATOR_ONLY`.
|
|
29
|
+
|
|
30
|
+
auth.core records the operator workspace. First-run setup records the workspace
|
|
31
|
+
it creates, in the transaction that creates it, and so does any other path that
|
|
32
|
+
creates the first workspace of an empty database (`auth workspace-create`,
|
|
33
|
+
`sandbox provision`, `setup quick`). `pnpm flowdular auth operator` shows the
|
|
34
|
+
record and `pnpm flowdular auth operator-set <id|slug> --apply` moves it to
|
|
35
|
+
another workspace; the next request sees the change, with no restart.
|
|
36
|
+
|
|
37
|
+
`FD_OPERATOR_TENANT` is an override. Set, the workspace whose tenant id it names
|
|
38
|
+
is the operator and the record is not consulted; a value that is not the id of
|
|
39
|
+
an existing workspace leaves no operator rather than falling back to the record.
|
|
40
|
+
Leave it empty to use the record.
|
|
41
|
+
|
|
42
|
+
A deployment upgraded from 0.6.0 with exactly one workspace records that
|
|
43
|
+
workspace the first time it starts. With two or more nothing is recorded, every
|
|
44
|
+
platform row is locked with a reason that names `auth operator-set`, and the
|
|
45
|
+
stored values keep applying until the operator runs that command or sets the
|
|
46
|
+
variable.
|
|
21
47
|
|
|
22
48
|
A `web` process serves HTTP only: it never starts a module worker and never
|
|
23
49
|
claims queued work from a request, so a deployment of `web` processes also needs
|
|
@@ -33,7 +59,8 @@ A Vercel deployment works this way; see
|
|
|
33
59
|
|
|
34
60
|
The name, the document title, the description, the link preview image, the
|
|
35
61
|
browser icon, the theme colour and the logo are not environment variables: they
|
|
36
|
-
are `system.core` settings
|
|
62
|
+
are platform-scoped `system.core` settings a principal with
|
|
63
|
+
`system.settings.manage` in the operator workspace changes under
|
|
37
64
|
Administration, Branding, and every change is audited. One value serves the
|
|
38
65
|
whole deployment, so the sign-in screen and a shared link carry it too, and a
|
|
39
66
|
setting nobody changed renders the product's own.
|
|
@@ -622,9 +649,10 @@ deployment on the old names keeps working and the server logs one
|
|
|
622
649
|
replacement. The platform name wins when both are set, and every refusal names
|
|
623
650
|
the variable the deployment actually set.
|
|
624
651
|
|
|
625
|
-
The relay is also five platform-scoped `auth.core` settings, edited
|
|
626
|
-
Administration, Modules: `mailTransport`
|
|
627
|
-
`mailSmtpUrl` (secret, write only),
|
|
652
|
+
The relay is also five platform-scoped `auth.core` settings, edited from the
|
|
653
|
+
operator workspace under Administration, Modules: `mailTransport`
|
|
654
|
+
(`environment`, `none` or `smtp`), `mailSmtpUrl` (secret, write only),
|
|
655
|
+
`mailFrom`, `mailRequireTls` and
|
|
628
656
|
`mailRejectUnauthorized`. `mailTransport` decides which source wins. It is
|
|
629
657
|
`environment` by default, and while it stays there the `FD_MAIL_*`
|
|
630
658
|
configuration above is in effect exactly as described, deprecation warnings
|
|
@@ -32,7 +32,12 @@ const databases = createDatabaseProvider(config, {
|
|
|
32
32
|
});
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`@flowdular/sdk/database` owns no driver, so the caller supplies both.
|
|
35
|
+
`@flowdular/sdk/database` owns no driver, so the caller supplies both. The provider
|
|
36
|
+
listens for the `error` events node-postgres emits on a pool and on a leased
|
|
37
|
+
client, so a connection the server closes (a suspended compute, a failover, a
|
|
38
|
+
restart) costs at most the query or transaction it interrupts, never the
|
|
39
|
+
process; a factory
|
|
40
|
+
needs no listener of its own. Composition
|
|
36
41
|
injects the result as `PlatformServerContext.databases`, and that is the only
|
|
37
42
|
way a module reaches storage. `GET /api/ready` reports the live adapter and
|
|
38
43
|
answers 503 while the database is unreachable.
|
|
@@ -72,7 +77,10 @@ maximum.
|
|
|
72
77
|
|
|
73
78
|
With the `pglite` adapter the data directory is the only setting that applies.
|
|
74
79
|
Under `NODE_ENV=test` the directory is ignored and the database is held in
|
|
75
|
-
memory.
|
|
80
|
+
memory. A directory is open once per process: every provider on it in that
|
|
81
|
+
process shares the one embedded database (a development server composes more
|
|
82
|
+
than one), and another process is refused with `LOCAL_DATABASE_LOCKED` until
|
|
83
|
+
the last of them closes it.
|
|
76
84
|
|
|
77
85
|
## Three roles
|
|
78
86
|
|
|
@@ -233,7 +233,10 @@ or than the narrowest table beside it; more than two actions always sit in the
|
|
|
233
233
|
More menu, and the column then keeps the width of that one button. The menu
|
|
234
234
|
opens over the page, arrows, Home and End move between items, Enter or Space
|
|
235
235
|
runs one, and Escape or Tab closes it and returns focus to the button. A refused
|
|
236
|
-
action stays in the menu, announced as unavailable, with its `reason`.
|
|
236
|
+
action stays in the menu, announced as unavailable, with its `reason`. The
|
|
237
|
+
action column stays pinned to the right edge while the table scrolls, except a
|
|
238
|
+
single action, which never folds: in a card narrower than three times its
|
|
239
|
+
column it scrolls with the row instead of covering the cells beside it.
|
|
237
240
|
|
|
238
241
|
**Clickable rows.** A table with `onSelect` makes its first cell a button, so
|
|
239
242
|
Tab reaches the row and Enter or Space opens it. That column holds text, never
|
|
@@ -442,7 +445,8 @@ Rendered by components, not written by hand: `ui-page-head*`, `ui-search`,
|
|
|
442
445
|
`ui-table__lead` with `ui-table__toggle` and `ui-table__open` (the first cell's
|
|
443
446
|
expand and open buttons), `ui-table__hide-*` (a column hidden below that width),
|
|
444
447
|
`ui-table__reveal-*` (the expand button, details list and detail shown below
|
|
445
|
-
that width), `ui-table--fold-*` (the action fold step),
|
|
448
|
+
that width), `ui-table--fold-*` (the action fold step), `ui-table--unpin-*`
|
|
449
|
+
(the step below which a single action scrolls with its row),
|
|
446
450
|
`ui-table__placeholder-body` (the loading and empty row content),
|
|
447
451
|
`ui-table__details` (+`-list`), `ui-table__detail`,
|
|
448
452
|
`ui-table-more` (+`--always`), `ui-table-menu` (+`__scrim`, `__label`),
|
|
@@ -33,7 +33,11 @@ workspace and owner account. Embedded PostgreSQL is already configured. Restart
|
|
|
33
33
|
Vite HMR covers TSRX, TypeScript and styles. The launcher keeps tool warnings
|
|
34
34
|
quiet; use `pnpm dev -- --verbose` for full diagnostics. `pnpm dev` runs
|
|
35
35
|
`module sync` first, so a composition change is picked up without a manual
|
|
36
|
-
step.
|
|
36
|
+
step. Ctrl+C, SIGTERM or a closed terminal stops the server after open requests
|
|
37
|
+
finish and background work drains, within six seconds. A second Ctrl+C does not
|
|
38
|
+
cut that short, because pnpm already delivers every Ctrl+C more than once;
|
|
39
|
+
Ctrl+\ stops it at once.
|
|
40
|
+
The session lives in an HttpOnly cookie and carries the scopes of the
|
|
37
41
|
selected tenant membership. A bookmark pointing at another workspace you
|
|
38
42
|
belong to switches the session on load.
|
|
39
43
|
|
|
@@ -77,8 +77,7 @@ release. No direct registry installation path remains.
|
|
|
77
77
|
Automation that imported `installModule` from `flowdular/distribution` must use
|
|
78
78
|
the host CLI plan and apply commands; that direct install export was removed.
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
`sandbox.delivery.targets` to `workspace` or `git-pr`; `git-pr` points to the
|
|
80
|
+
Sandbox delivery targets are `workspace` and `git-pr`; `git-pr` points to the
|
|
82
81
|
platform repository configured in `flowdular.json`. A catalog publisher can
|
|
83
82
|
use any Git repository and publish immutable artifacts independently. Historical
|
|
84
83
|
review and RFC documents retain the old project name as provenance.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A module is a self-contained slice of the product: its own permissions,
|
|
4
4
|
endpoints, migrations, services, screens, translations and tests, wired into the
|
|
5
|
-
platform without touching a core file. `.ai/references/catalog` is the
|
|
5
|
+
platform without touching a core file. `.ai/references/catalog` is the reference
|
|
6
6
|
implementation; copy its shape.
|
|
7
7
|
|
|
8
8
|
## Lifecycle
|
|
@@ -181,7 +181,9 @@ package is not linked yet, and regenerates
|
|
|
181
181
|
grants the scopes declared by each newly enabled module to every workspace owner
|
|
182
182
|
through `auth sync-scopes`; a failed grant is reported as
|
|
183
183
|
`MODULE_SCOPES_SYNC_FAILED`, and when `auth.core` is unavailable the grant is
|
|
184
|
-
skipped with a warning.
|
|
184
|
+
skipped with a warning. That grant reaches only workspaces that already exist,
|
|
185
|
+
so first-run setup grants the permissions of every enabled module to the owner
|
|
186
|
+
it creates.
|
|
185
187
|
|
|
186
188
|
`disable` refuses while another enabled module depends on the target, then
|
|
187
189
|
removes it and regenerates. `system.core` and `auth.core` are protected.
|
|
@@ -417,4 +419,4 @@ RuleSync generates the discovery copies for supported coding agents:
|
|
|
417
419
|
`test-hardening`, `cli-extension`, `agent-tool-design`,
|
|
418
420
|
`business-agent-design`, `workflow-development`, `release-eject-pr`.
|
|
419
421
|
|
|
420
|
-
|
|
422
|
+
Business modules kept outside this repository install through [module distribution](module-distribution.md), which covers install, update, lock verification and release checks.
|
|
@@ -57,7 +57,18 @@ written.
|
|
|
57
57
|
|
|
58
58
|
An application already serving on the platform port is left alone, and no
|
|
59
59
|
credential is prepared for it. `--platform` and `--platform-port` say otherwise
|
|
60
|
-
explicitly; `--no-platform` never starts one.
|
|
60
|
+
explicitly; `--no-platform` never starts one. An application the launcher
|
|
61
|
+
started stops with it, including when the terminal closes or the launcher is
|
|
62
|
+
killed. It gets eight seconds to drain before it is killed, two more than the
|
|
63
|
+
six the development server gives itself; both figures live in
|
|
64
|
+
`@flowdular/sdk/dev-console/shutdown`. The installs, gates and Git commands the
|
|
65
|
+
sandbox runs stop with it the same way.
|
|
66
|
+
|
|
67
|
+
One sandbox runs per workspace. A second launcher, or `pnpm eval`, on a
|
|
68
|
+
workspace whose sandbox is running exits with the PID of the process that holds
|
|
69
|
+
it. The hold is the directory `.flowdular/sandbox/workspace.lock`; it goes away
|
|
70
|
+
when that process ends, and one left by a process that was killed outright is
|
|
71
|
+
taken over by the next start.
|
|
61
72
|
|
|
62
73
|
## The credential is prepared, not pasted
|
|
63
74
|
|
|
@@ -325,7 +336,13 @@ and a human review remain the repository's own gate.
|
|
|
325
336
|
## Decisions the specialist needs
|
|
326
337
|
|
|
327
338
|
A specialist that cannot continue without a business decision ends its reply
|
|
328
|
-
with one fenced block tagged `questions` holding a single JSON object
|
|
339
|
+
with one fenced block tagged `questions` holding a single JSON object. An
|
|
340
|
+
implementer (every role but the business manager) asks this way for anything
|
|
341
|
+
the approved specification does not decide, such as a new error code, field,
|
|
342
|
+
permission, state or changed behaviour, and leaves that part unbuilt instead of
|
|
343
|
+
deciding it and mentioning it in prose. Every turn's instruction carries the
|
|
344
|
+
format and the limits below from `questions.ts`, the module that parses the
|
|
345
|
+
block.
|
|
329
346
|
|
|
330
347
|
````
|
|
331
348
|
```questions
|
|
@@ -349,9 +366,17 @@ of 1 to 120 characters, and a recommendation that must be one of those options.
|
|
|
349
366
|
`allowFreeText` defaults to false, and a question with neither an option nor
|
|
350
367
|
free text cannot be answered, so it is refused. The block has to be the last
|
|
351
368
|
thing in the reply apart from the mandatory handoff line, and a reply carries at
|
|
352
|
-
most one. A block the sandbox cannot read is a
|
|
353
|
-
|
|
354
|
-
|
|
369
|
+
most one. A block the sandbox cannot read is never a failed turn and never
|
|
370
|
+
dropped in silence: the transcript says why it was refused, and the specialist
|
|
371
|
+
gets the reason and these limits for one repair turn. A second refusal in a row
|
|
372
|
+
stops for the operator, who answers in words. A turn that asks stops for the
|
|
373
|
+
answers, never for approval, and the approval route refuses a module with open
|
|
374
|
+
questions (`409 QUESTIONS_PENDING`). A gate that failed in the same turn waits
|
|
375
|
+
too: the handoff names it, the gates run again after the answering turn, and a
|
|
376
|
+
failure that remains then goes back to the specialist. Such a session is in the
|
|
377
|
+
`awaiting-answers` state, so `awaiting-approval` only ever means an approval.
|
|
378
|
+
`sandbox.core` has no state for open questions, so the platform record keeps
|
|
379
|
+
`awaiting-approval` for it.
|
|
355
380
|
|
|
356
381
|
A readable block is stored on the session as `pendingQuestions`, with the
|
|
357
382
|
transcript sequence of the message that asked, the role that asked and the
|
|
@@ -370,8 +395,11 @@ POST /sandbox/api/sessions/:id/answers
|
|
|
370
395
|
```
|
|
371
396
|
|
|
372
397
|
behind the same origin, header and ownership checks as every other mutation. It
|
|
373
|
-
clears `pendingQuestions` and starts the next turn in the
|
|
374
|
-
|
|
398
|
+
clears `pendingQuestions` and starts the next turn in the module the questions
|
|
399
|
+
were about, with the decisions leading the request text. The business manager's
|
|
400
|
+
own questions go back to it. An implementer's questions go to the business
|
|
401
|
+
manager first (`answeringRole` in `planning.ts`), and so does an answer the
|
|
402
|
+
operator types in the message box instead:
|
|
375
403
|
|
|
376
404
|
```
|
|
377
405
|
Decisions:
|
|
@@ -387,6 +415,35 @@ INVALID_INPUT` for a body that leaves a question unanswered, names a question
|
|
|
387
415
|
the session did not ask, exceeds 400 characters, or gives an answer that is not
|
|
388
416
|
one of the offered options when the specialist allowed no free text.
|
|
389
417
|
|
|
418
|
+
### Answers to an implementer's questions
|
|
419
|
+
|
|
420
|
+
The business manager applies the answers to `spec/module.yaml`, and the text it
|
|
421
|
+
leaves behind decides what happens next:
|
|
422
|
+
|
|
423
|
+
- An answer that changes what the module must do is recorded in the
|
|
424
|
+
specification, which goes back to `draft`. Its hash no longer matches the
|
|
425
|
+
approval, so the session waits in `awaiting-approval` with the implementer
|
|
426
|
+
that asked as the next role, and implementation is refused until the operator
|
|
427
|
+
approves the new hash. Approving resumes that implementer with its decisions.
|
|
428
|
+
- An answer the approved text already decides leaves the file untouched. The
|
|
429
|
+
approval still holds, and the implementer that asked continues at once with
|
|
430
|
+
its decisions.
|
|
431
|
+
|
|
432
|
+
The wait is read from the transcript (`specFollowUp` in `planning.ts`): the
|
|
433
|
+
newest handoff that is not the business manager's own is the implementer's
|
|
434
|
+
question, so the implementer still resumes when the business manager asks a
|
|
435
|
+
question of its own in between. A gate the implementer left failing runs again
|
|
436
|
+
on the business manager's turn. When it still fails, only a repair the business
|
|
437
|
+
manager takes in that module runs first; any other waits until the implementer
|
|
438
|
+
has resumed with its decisions, and the gates run again after that turn.
|
|
439
|
+
|
|
440
|
+
The `module-rules` gate backs the rule for permissions: `permissions-specified`
|
|
441
|
+
fails a module whose `src/acl/permissions.ts`, or an inline `permission:`
|
|
442
|
+
value, names a permission the specification does not list. Error codes have no
|
|
443
|
+
structured list in a specification and the shipped modules throw many codes
|
|
444
|
+
their specifications never name, so a matching rule for them would fail correct
|
|
445
|
+
modules; the instruction and the question card cover them.
|
|
446
|
+
|
|
390
447
|
## Operator commands
|
|
391
448
|
|
|
392
449
|
```bash
|
package/package.json
CHANGED
|
@@ -84,6 +84,11 @@ FD_AUTH_MAIL_TRANSPORT=none
|
|
|
84
84
|
FD_AUTH_SMTP_URL=
|
|
85
85
|
FD_AUTH_MAIL_FROM=
|
|
86
86
|
|
|
87
|
+
# Tenant id that overrides the operator workspace, the one that changes platform
|
|
88
|
+
# settings such as the branding and the mail relay. Empty, the workspace auth.core
|
|
89
|
+
# recorded at first-run setup decides (pnpm flowdular auth operator shows it).
|
|
90
|
+
FD_OPERATOR_TENANT=
|
|
91
|
+
|
|
87
92
|
# Read by infra/docker/compose.yaml only. It builds the three URLs above from
|
|
88
93
|
# these passwords and publishes the container port on FD_PORT.
|
|
89
94
|
FD_POSTGRES_SUPERUSER_PASSWORD=
|
|
@@ -61,6 +61,11 @@ FD_AUTH_PUBLIC_ORIGIN=
|
|
|
61
61
|
# behind TLS, set true and set FD_AUTH_PUBLIC_ORIGIN to its HTTPS origin.
|
|
62
62
|
FD_AUTH_SECURE_COOKIE=
|
|
63
63
|
|
|
64
|
+
# Tenant id that overrides the operator workspace, the one that changes platform
|
|
65
|
+
# settings such as the branding and the mail relay. Empty, the workspace auth.core
|
|
66
|
+
# recorded at first-run setup decides (pnpm flowdular auth operator shows it).
|
|
67
|
+
FD_OPERATOR_TENANT=
|
|
68
|
+
|
|
64
69
|
# Prometheus exposition on GET /api/metrics. Leave FD_METRICS unset to keep it off.
|
|
65
70
|
FD_METRICS=false
|
|
66
71
|
FD_METRICS_TOKEN=
|
|
@@ -18,6 +18,7 @@ RUN pnpm build
|
|
|
18
18
|
RUN mkdir -p module-plans \
|
|
19
19
|
&& if [ ! -f flowdular.modules.lock.json ]; then printf '{"schemaVersion":1,"modules":[]}\n' > flowdular.modules.lock.json; fi \
|
|
20
20
|
&& if [ ! -f flowdular.module-sources.json ]; then printf '{"schemaVersion":1,"sources":[]}\n' > flowdular.module-sources.json; fi
|
|
21
|
+
RUN mkdir -p /sdk-manifests && node infra/sdk-module-manifests.mjs . /sdk-manifests
|
|
21
22
|
|
|
22
23
|
FROM node:24-bookworm-slim AS runtime
|
|
23
24
|
|
|
@@ -31,7 +32,9 @@ RUN groupadd --system --gid 1001 octane \
|
|
|
31
32
|
&& chown octane:octane /data
|
|
32
33
|
|
|
33
34
|
# The server bundle is self-contained: platform/dist/server/entry.js imports
|
|
34
|
-
# only node:* built-ins, so no node_modules (and no devDependencies) ship.
|
|
35
|
+
# only node:* built-ins, so no node_modules (and no devDependencies) ship. The
|
|
36
|
+
# platform does read the manifests and specs of the modules @flowdular/sdk
|
|
37
|
+
# ships, so the last copy puts those files alone where platform/ resolves them.
|
|
35
38
|
COPY --from=builder --chown=octane:octane /workspace/platform/dist ./platform/dist
|
|
36
39
|
COPY --from=builder --chown=octane:octane /workspace/platform/package.json ./platform/package.json
|
|
37
40
|
COPY --chown=octane:octane infra/docker/app-entrypoint.mjs infra/docker/database-urls.mjs ./infra/docker/
|
|
@@ -40,6 +43,7 @@ COPY --from=builder --chown=octane:octane /workspace/flowdular.modules.lock.json
|
|
|
40
43
|
COPY --from=builder --chown=octane:octane /workspace/flowdular.module-sources.json ./flowdular.module-sources.json
|
|
41
44
|
COPY --from=builder --chown=octane:octane /workspace/module-plans ./module-plans
|
|
42
45
|
COPY --from=builder --chown=octane:octane /workspace/modules ./modules
|
|
46
|
+
COPY --from=builder --chown=octane:octane /sdk-manifests/ ./
|
|
43
47
|
|
|
44
48
|
USER octane
|
|
45
49
|
EXPOSE 3000
|
|
@@ -124,6 +124,10 @@ services:
|
|
|
124
124
|
FD_APPLICATION_PATH: ${FD_APPLICATION_PATH:-/app}
|
|
125
125
|
FD_SETUP_AUTO_RESTART: 'true'
|
|
126
126
|
FD_AUTH_ALLOW_SIGN_UP: 'false'
|
|
127
|
+
# Tenant id that overrides the operator workspace, the one that changes
|
|
128
|
+
# platform settings such as the branding and the mail relay. Empty, the
|
|
129
|
+
# workspace auth.core recorded at first-run setup decides.
|
|
130
|
+
FD_OPERATOR_TENANT: ${FD_OPERATOR_TENANT:-}
|
|
127
131
|
# TLS terminates in front of this container. Only a plain-HTTP run on a
|
|
128
132
|
# workstation may set FD_AUTH_SECURE_COOKIE=false in .env.
|
|
129
133
|
FD_AUTH_SECURE_COOKIE: ${FD_AUTH_SECURE_COOKIE:-true}
|
|
@@ -61,6 +61,11 @@ spec:
|
|
|
61
61
|
key: backgroundUrl
|
|
62
62
|
- name: FD_AUTH_ALLOW_SIGN_UP
|
|
63
63
|
value: 'false'
|
|
64
|
+
# Tenant id that overrides the operator workspace, the one that
|
|
65
|
+
# changes platform settings such as the branding and the mail relay.
|
|
66
|
+
# Empty, the workspace auth.core recorded at first-run setup decides.
|
|
67
|
+
- name: FD_OPERATOR_TENANT
|
|
68
|
+
value: ''
|
|
64
69
|
- name: FD_AUTH_SECURE_COOKIE
|
|
65
70
|
value: 'true'
|
|
66
71
|
# With none, invitations and password resets are refused. The smtp
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cp,
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
realpath,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from 'node:fs/promises';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/* A deployment ships no node_modules, yet the platform lists the modules
|
|
13
|
+
@flowdular/sdk ships by resolving the SDK's module index from platform/. This
|
|
14
|
+
copies what that lookup reads (the index, each indexed module.json and its
|
|
15
|
+
spec/module.yaml, and a package.json exporting the index) to the same place
|
|
16
|
+
under the deployment root. A workspace without the SDK copies nothing.
|
|
17
|
+
|
|
18
|
+
Usage: node infra/sdk-module-manifests.mjs <workspace> <deployment root> */
|
|
19
|
+
|
|
20
|
+
/* The platform refuses a larger index, so the copy does too. */
|
|
21
|
+
const INDEX_LIMIT = 256;
|
|
22
|
+
const UNRESOLVED = new Set([
|
|
23
|
+
'MODULE_NOT_FOUND',
|
|
24
|
+
'ERR_PACKAGE_PATH_NOT_EXPORTED',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const [workspaceArgument, deploymentArgument] = process.argv.slice(2);
|
|
28
|
+
if (!workspaceArgument || !deploymentArgument) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'Usage: node infra/sdk-module-manifests.mjs <workspace> <deployment root>',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const workspace = resolve(workspaceArgument);
|
|
34
|
+
const destination = join(
|
|
35
|
+
resolve(deploymentArgument),
|
|
36
|
+
'platform/node_modules/@flowdular/sdk',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function sdkIndexPath() {
|
|
40
|
+
try {
|
|
41
|
+
return createRequire(join(workspace, 'platform/package.json')).resolve(
|
|
42
|
+
'@flowdular/sdk/modules.json',
|
|
43
|
+
);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (UNRESOLVED.has(error.code)) return null;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function insidePath(root, path) {
|
|
51
|
+
const fromRoot = relative(root, path);
|
|
52
|
+
return (
|
|
53
|
+
Boolean(fromRoot) && !fromRoot.startsWith('..') && !isAbsolute(fromRoot)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Copies a file the index names, refusing one that leaves the SDK lexically or
|
|
58
|
+
through a link. An absent optional file is skipped. */
|
|
59
|
+
async function copyFromSdk(sdkRoot, path, optional = false) {
|
|
60
|
+
const lexical = resolve(sdkRoot, path);
|
|
61
|
+
if (!insidePath(sdkRoot, lexical)) {
|
|
62
|
+
throw new Error(`SDK module file escapes the package: ${path}`);
|
|
63
|
+
}
|
|
64
|
+
let source;
|
|
65
|
+
try {
|
|
66
|
+
source = await realpath(lexical);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (optional && error.code === 'ENOENT') return;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (!insidePath(sdkRoot, source) || !(await lstat(source)).isFile()) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`SDK module file must be a regular file inside the package: ${path}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const target = join(destination, relative(sdkRoot, lexical));
|
|
77
|
+
await mkdir(dirname(target), { recursive: true });
|
|
78
|
+
await cp(source, target);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const indexPath = sdkIndexPath();
|
|
82
|
+
if (indexPath) {
|
|
83
|
+
const sdkRoot = await realpath(dirname(indexPath));
|
|
84
|
+
const indexSource = await readFile(indexPath, 'utf8');
|
|
85
|
+
const index = JSON.parse(indexSource);
|
|
86
|
+
if (
|
|
87
|
+
index.schemaVersion !== 1 ||
|
|
88
|
+
!Array.isArray(index.modules) ||
|
|
89
|
+
index.modules.length > INDEX_LIMIT
|
|
90
|
+
) {
|
|
91
|
+
throw new Error('Invalid SDK module index.');
|
|
92
|
+
}
|
|
93
|
+
await mkdir(destination, { recursive: true });
|
|
94
|
+
for (const entry of index.modules) {
|
|
95
|
+
if (typeof entry?.manifest !== 'string') {
|
|
96
|
+
throw new Error('Invalid SDK module manifest path.');
|
|
97
|
+
}
|
|
98
|
+
await copyFromSdk(sdkRoot, entry.manifest);
|
|
99
|
+
await copyFromSdk(
|
|
100
|
+
sdkRoot,
|
|
101
|
+
join(dirname(entry.manifest), 'spec/module.yaml'),
|
|
102
|
+
true,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
await writeFile(join(destination, 'modules.json'), indexSource);
|
|
106
|
+
await writeFile(
|
|
107
|
+
join(destination, 'package.json'),
|
|
108
|
+
`${JSON.stringify(
|
|
109
|
+
{
|
|
110
|
+
name: '@flowdular/sdk',
|
|
111
|
+
private: true,
|
|
112
|
+
exports: { './modules.json': './modules.json' },
|
|
113
|
+
},
|
|
114
|
+
null,
|
|
115
|
+
'\t',
|
|
116
|
+
)}\n`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -160,6 +160,15 @@ async function writeFunction(directory, runtimeRole) {
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
const sdkManifests = spawnSync(
|
|
164
|
+
process.execPath,
|
|
165
|
+
[join(repositoryRoot, 'infra/sdk-module-manifests.mjs'), root, directory],
|
|
166
|
+
{ stdio: 'inherit' },
|
|
167
|
+
);
|
|
168
|
+
if (sdkManifests.error) throw sdkManifests.error;
|
|
169
|
+
if (sdkManifests.status !== 0) {
|
|
170
|
+
throw new Error('Copying the SDK module manifests failed.');
|
|
171
|
+
}
|
|
163
172
|
await copyRegular(
|
|
164
173
|
join(repositoryRoot, 'infra/vercel/handler.mjs'),
|
|
165
174
|
join(directory, 'handler.mjs'),
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"format": "prettier --write .",
|
|
16
16
|
"format:check": "prettier --check .",
|
|
17
17
|
"flowdular": "flowdular",
|
|
18
|
-
"cl": "flowdular",
|
|
19
18
|
"doctor": "flowdular doctor",
|
|
20
19
|
"verify": "pnpm rules:check && pnpm typecheck && pnpm test && flowdular spec validate --all && flowdular module validate && pnpm format:check",
|
|
21
20
|
"rules:generate": "rulesync generate",
|
|
@@ -23,10 +22,10 @@
|
|
|
23
22
|
"build": "flowdular module sync --apply && pnpm --filter @app/platform build"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
|
-
"@flowdular/sandbox": "0.6.
|
|
25
|
+
"@flowdular/sandbox": "0.6.2",
|
|
27
26
|
"@tsrx/prettier-plugin": "0.3.120",
|
|
28
27
|
"prettier": "3.6.2",
|
|
29
|
-
"flowdular": "0.6.
|
|
28
|
+
"flowdular": "0.6.2",
|
|
30
29
|
"rulesync": "16.21.0"
|
|
31
30
|
}
|
|
32
31
|
}
|