zebflow 0.10.10

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# How Hub Works

Hub is the office-hosted package sharing service for Zebflow work.

## Core ideas

- one Zebflow Hub can publish packages
- another Zebflow project can consume them
- public packages can be browsed without a token
- private access can use scoped Hub tokens
- packages are typed and versioned
- a published version is immutable

## Why this matters

This lets Zebflow share project material natively, without forcing everything
through GitHub repos or plugin packaging first.

## Hub and Add+

Hub and Add+ are related but not identical.

- Hub is a package publishing and sharing authority.
- Add+ is the project-side surface for bringing reusable material into the
  current project.

Add+ may read from several source adapters:

- Built-in Catalog: offline packages or components embedded in the Zebflow
  binary.
- Hub: packages from a Zebflow Hub source.
- Git: packages or source folders from GitHub, GitLab, or another git remote.
- Local: local folders or uploaded package archives.

Zip is local when the archive is selected from the user's machine. If a zip is
downloaded from a URL, the source adapter is the remote system that provided
that URL.

All Add+ sources must be reviewed before they write. The review is intentionally
source-agnostic: it lists added/skipped/overwritten files, executable surfaces,
credentials, external URLs, database/filesystem effects, public endpoints,
schedules, large files, seed data, database initialization, warnings, and risk
level. Built-in UI components and Hub packages already use this review gate;
Git, folder, and zip sources should stage their contents and reuse the same
gate.

## Warnings and violations

Most of what the review reports is a **warning**: an effect the package has, for
you to accept or decline. A public webhook, a credential, an outbound URL, and a
destructive seed statement are all warnings.

A **violation** is different. It is not a stronger warning; it means the package
will not be installed at all, whoever approves it, and the risk level reads
`blocked`. There are three:

- a pipeline whose bytes the review could not read, because bytes the review
  never saw are bytes the install would still write
- a file whose type a project's repository does not accept
- a node bundle declaring a node kind this build already provides, which makes
  the whole project's node registry fail to load

The second is a file-extension check, not a guess about content. A package may
write source, documentation, data, and images. It may not write a `.sh`, a
`.dylib`, a `.env`, or any binary type this build has no reader for. The
refusal names the path and the extension.

The accepted set is the platform's, so every project has it without configuring
anything. A project may narrow it in `repo/zebflow.yaml` under
`spec.layout.allowed_extensions`; it cannot widen it. `zebflow.yaml`,
`zeb.lock`, `zebflow.init.json`, `schema.json`, and `.gitkeep` are accepted by
name whatever the list says, because an install cannot proceed without them.

A node bundle is judged by its own contract rather than by a project's list: it
materializes into `data/hub/nodes/` and never writes into `repo/`.

## Publishing refuses the same things

A violation refuses an install on every channel, so a release carrying one is a
release nobody can use -- including on the hub that stores it. Publishing is
therefore refused as well, locally and over the remote publish route, before
anything is written: no artifact file, no version row, no cover in the store.
The error names every violation, so you know what to fix.

This is in your favour rather than an extra hurdle. A release is immutable: once
a version number is published it can never be republished, and retracting it
keeps the coordinate reserved. Being refused costs you a retry. Being accepted
would have cost you the version.

Publishing reads the platform's extension set, not the narrowed one your own
project may declare, because the narrowed set is what *your* project accepts and
says nothing about what a receiver will.

## Database initialization

A package may carry SQL that runs at install, under `initial-data/`, `init/`,
or `seeds/`, in a `sekejap/` or `sqlite/` subfolder. Those are the only two
engines: there is no postgres or mysql seed execution, and both stores are
project-local ones the install creates, so package SQL cannot reach a database
you already had.

The review reports one `database_initialization` row per such file:

- `engine` and `source`: which engine replays the file, and where it came from
- `store` and `existing_data_at_risk`: which store receives it
- `statements`: a count per statement kind, with anything unrecognised counted
  under `OTHER` so nothing is dropped silently
- `tables`: every table the statements name
- `destructive`: the DROP, DELETE, TRUNCATE, and ALTER statements, quoted back

Every install review in the UI renders these rows in full: the Hub page's
project-bundle review, the project Hub page's node-bundle review, and Add+ in
the pipeline registry.

## Installing part of a project bundle

A platform Hub project install accepts three flags, all true when omitted:

- `include_code`: write source entries -- pipelines, pages, docs, everything
  that is not schema or seed SQL
- `include_schema`: write the schema and seed `.sql` files into `repo/`
- `execute_schema`: replay that SQL into the project's own stores

`execute_schema: false` with `include_schema: true` installs the schema as
files and leaves the stores untouched, so the SQL can be reviewed and run by
hand. `execute_schema: true` with `include_schema: false` is rejected rather
than downgraded, and so is a request that includes nothing at all. The install
response names every entry the scope skipped and every seed script that was
written but not run.

`zebflow.yaml`, `zeb.lock`, and `zebflow.init.json` are always written: a
project without its configuration is not a smaller install, it is a broken one.

In the UI these three flags are checkboxes in the Hub page's install review,
each on by default. Turning the schema off turns execution off with it and locks
that checkbox, so the form cannot assemble the combination the API refuses.

## Hub is not backup

Hub is for distribution. Backup is for preservation.

Hub packages are versioned, reusable project material that can move through Zebflow Hub,
GitHub, GitLab, local folders, or zip files. They should exclude private
runtime state, credentials, logs, caches, `.git/`, and full production data.

Backups may include project data, files, database snapshots, selected logs,
runtime state, and credential references or encrypted secrets when explicitly
allowed. Restore belongs to the backup system, not Hub.

## Common flow

1. enable the platform Hub service
2. create a publisher and scoped token
3. publish a package to `/api/hub/remote/assets`
4. register a Hub source at platform or project level
5. grant platform-owned source access to projects when sharing is intended
6. browse packages in Hub or Add+
7. review safety effects
8. add the package into the local project workspace

## Access model

Hub access is explicit:

- Hub service: where the Hub lives
- Hub access source: URL, read token, title, and capability-bearing access
- Hub grant: assignment of a platform-owned source to `all_projects` or one
  selected project

Registering a Hub service does not grant project access. Creating a platform
Hub source also does not grant project access. Projects see platform-owned
sources only through an explicit grant, or through sources they add inside the
project itself.

This supports internal software-house Hubs: a `reusables-producer` project can
publish shared assets, while platform admins grant read access to all client
projects without asking every project user to configure the source manually.

## Package kinds

Current canonical package kinds are:

- `pipeline_bundle`
- `template_bundle`
- `folder_bundle`
- `project_bundle`
- `node_bundle`
- `rwe_library`
- `skill` — one `skills/<name>/` folder with a `SKILL.md`; Add copies it to
  `skills/<name>/` under the receiving project's source root (whatever folder
  was requested), where the project's agent lists it with `skill_list` and
  where it shadows a blessed skill of the same name. Publish source:
  `skill_folder` with `skills/<name>` as the reference; the folder is refused
  unless its `SKILL.md` names itself, has a description and a license
  (`HUB_SKILL_INVALID`). Contract: `docs/contracts/kinds/skill`.

Unknown package kinds are rejected. This keeps Hub packages predictable and
prevents arbitrary package categories from appearing in consumers.

## Add and Install

Add+ should feel closer to Unity or Blender asset import than npm package
installation. Hub packages should follow the same safety model when they are
added through Add+.

- Add: canonical Hub action that brings a package into the current project.
- Add to current project: write the package into its normal destination.
- Clone as folder: Add mode that writes a larger package, folder, starter, or
  workflow bundle as a separate folder.
- Install: reserve for app-level runnable packages or full project/app
  templates.

For normal Hub packages, use Add. Imported pipelines should land as draft unless
the user explicitly activates them.

Add is copy/clone based for `pipeline_bundle`, `template_bundle`,
`folder_bundle`, and `project_bundle`: Hub does not keep ownership of the
resulting project files and does not track an installation state after Add.
Users can change the added source like any other project code.

`node_bundle` and `rwe_library` are the two exceptions — a node bundle
materializes into `data/hub/nodes/` and an RWE library's runtime lands under
`data/hub/rwe-libraries/`, and both are recorded as real dependencies: a
`node_bundle` install is written into `zeb.lock`, and an `rwe_library` install
is written into both `zeb.lock` and `zebflow.yaml` (`rwe.libraries`), pinning
the version and digest the way any dependency lock does.

### Where an added package lands

A package's file paths are relative to the publishing project's layout, and the
receiving project may keep its own files elsewhere. A release records the layout
its paths were produced by, and Add translates each file into the equivalent
directory of the receiving project:

- source (pipelines, pages, styles, shared components) goes to the target folder
  inside this project's source root, defaulting to `hub/{package_id}`
- assets go to this project's asset directory, under the same folder name, so
  they are served at `/static/{owner}/{project}/...`
- docs go to this project's docs directory, under the same folder name

Anything else the package carries — its own `zebflow.yaml`, exported schema
documents, node interfaces — stays inside the package's own folder. One project
holds one of each of those, so a second copy is kept readable rather than
written over the project's own.

The safety review reports these destinations, and Add writes exactly the
destinations the review reported.

## Safety review

Before Add, Zebflow should show what the package can affect:

- files added or overwritten
- pipelines registered
- nodes used
- credentials required
- external URLs and domains
- database schemas, tables, collections, graphs, or indexes touched
- filesystem paths touched
- outbound connections opened, and code the package supplies that will run
- schedules, webhooks, and public endpoints created
- secrets required but not included
- large files and optional seed/demo data

The package brings source into the project. It does not bring trust.

### How the review knows what a node does

Every effect above that names a node kind is answered from the node catalog,
never from the shape of the kind's name.

- **Native nodes** declare their own capabilities in Rust, so the answer is a
  constant of the build and a misspelling does not compile.
- **Composite nodes** — what a third-party bundle mostly ships — declare
  nothing. Their capabilities are the union of the nodes their function
  pipelines compose, so a bundle cannot understate what it does without also
  removing the nodes that do it. Their function pipelines are reviewed like any
  other pipeline.
- **WASM nodes** carry no capability at all. The host instantiates every module
  with no imports and no WASI, so a module gets memory and arithmetic and
  nothing else to call.

A capability is what a kind *can* reach in some configuration, not what one
instance of it does. A node kind the review cannot account for is named in the
warnings rather than passed over silently — it is usually a node provided by a
bundle installed separately, which is a normal thing for a package to depend on.

A bundle that declares a node kind this build already provides is refused
outright. That is not a judgement about the bundle's behaviour: the kind is in
its own manifest, the catalog it collides with is fixed at compile time, and
installing it makes the whole project's node registry fail to load, not just
that one node.

## Gallery metadata

Hub cards show package identity and presentation metadata:

- title
- package id
- latest version
- package kind
- tags
- publisher name and publisher URL
- optional cover image

Cover images are Hub media, not arbitrary files from the install payload.
Installable files live in `artifact.files`; gallery media lives in the package
media contract and is served through:

```text
/api/hub/remote/assets/{package_id}/media/{media_name}
```

The target gallery contract also allows optional screenshots and YouTube demo
links, but those are structured metadata, not arbitrary external embeds.

## Release immutability

A published `package@version` is fixed. Correcting anything — content, title,
description — means publishing a new version.

A second publish of a version that already exists is refused with
`HUB_VERSION_EXISTS` (HTTP 409) on both forms of the publish route —
`.../hub/remote/assets/publish` and `/api/hub/remote/assets`. The refusal
happens before anything is written, so the stored artifact, its digest, and
its creation time are left untouched.

This is what makes `zeb.lock` meaningful: a lock entry pins a version to a
digest, and a digest only means something when the bytes it names cannot change
underneath it. If a release could be overwritten, every project that pinned it
would report a tampered dependency the next time it checked.

## Presentation

A release carries what installing it requires. Everything a human reads while
choosing — summary, long-form markdown, cover, gallery — lives on the mutable
package row beside it, and is edited without publishing anything:

```text
PATCH /api/projects/{owner}/{project}/hub/assets/{package_id}/presentation
{ "summary": "...", "description_md": "...", "image_file_path": "...", "gallery": { ... } }
```

Authorised by the same publisher token as publish. Every field is optional and
an omitted field is left alone. No release, digest, or creation time moves.

Publishing follows the same rule: a publish that carries no summary, no
description, and no `image_file_path` leaves the existing presentation exactly
as it was.

## Retraction

`DELETE /api/hub/remote/assets/{package_id}` retracts. It does not delete.

The artifact bytes of every release are destroyed. The package and version rows
survive, marked retracted with a timestamp and the publisher's reason, and
`package@version` can never be published again — a second publish of a retracted
coordinate is refused with `HUB_VERSION_RETRACTED`.

Freeing the coordinates would have been the hole in immutability: a lockfile
pinning the old digest would report a tampered dependency for content that was
simply republished. Keeping them means a coordinate always names one thing, or
nothing.

A retracted release stays listed and explorable. Its detail response still
describes it, with `artifact: null` and the retracted marker saying why; the
artifact and install routes answer `410 Gone`.

## Token scopes

A hub token holds `hub:read`, `hub:publish`, `hub:manage`, or a combination.
Any other value is refused at creation with `HUB_TOKEN_SCOPE_INVALID` (HTTP
400), naming the value and the accepted set, and a token with no usable scope is
refused too. A scope the publisher record does not grant is refused with
`HUB_PUBLISHER_SCOPE_DENIED`.

## Carried and referenced files

Each file in a release supplies its bytes one of two ways, never both:

| Field | Meaning |
| --- | --- |
| `content` | the bytes, inline in the release document |
| `artifact` | a SHA-256 digest the bytes are fetched and verified against |

Publishing decides per file, by size. A file up to 1 MiB is carried, which keeps
a release one self-contained document. A file over 1 MiB is referenced: the
release records only its digest and the bytes go into the Hub's
content-addressed store. Two releases shipping the same file store it once.

A reference carries a digest and never a URL, so the location comes from the
channel the package is installed through:

```text
Blessed shelf  <data root>/services/hub-local/artifacts/{sha256}
Remote hub     GET /api/hub/remote/assets/{package_id}/{version}/artifacts/{sha256}
Local file     ./artifacts/{sha256} beside the document
```

The remote artifact route serves one release's referenced bytes and applies that
release's own rules: a private package needs the publisher's token, a retracted
release answers `410 Gone`, and a digest the release does not name answers
`404`.

Installing from a remote hub fetches every referenced artifact first, checks it
against the declared size while it streams, and verifies its digest before
anything is written. Redirects are not followed. A fetch that fails, is refused,
overruns its declared size, or hashes to the wrong digest refuses the install
with the project untouched:

| Code | Meaning |
| --- | --- |
| `HUB_ARTIFACT_MISSING` | the hub answered, and does not have it |
| `HUB_ARTIFACT_FORBIDDEN` | the hub refused to serve it |
| `HUB_ARTIFACT_REDIRECTED` | the hub answered with a redirect, which is not followed |
| `HUB_ARTIFACT_FETCH_FAILED` | the request itself failed |
| `HUB_ARTIFACT_SIZE_MISMATCH` | the bytes are not the size the release declares |
| `HUB_ARTIFACT_DIGEST_MISMATCH` | the bytes are not the bytes the release names |

Verified bytes are kept in this instance's content-addressed fetch cache
(`platform/cache/hub-artifacts/`), so reviewing a package and then installing
it fetches once. Neither hub store is written by a fetch: the blessed shelf is
seed-only and the Public Hub store is publish-only.

The safety review reads referenced bytes the same way the install does. A
referenced pipeline is scanned from its real content, not from an empty file.

## Storage

There are two hub stores under the platform data root, one format, one
machinery, two roots:

```text
services/hub-local/    the blessed shelf: seeded from the release at every
                       boot (check-first), written by nothing else, read
                       in-process by blessed installs
services/hub-public/   the Public Hub service's own store: the ONE publish
                       target (publisher/token/grant ACL), served over
                       /api/hub/remote/*; exists only where the hub service
                       is placed and enabled
```

Each store keeps the same internal layout:

```text
services/{store}/hub.db
services/{store}/packages/{package_id}/versions/{version}/artifact.json
services/{store}/artifacts/{sha256}
```

The package metadata is in `hub.db`; the installable package payload is in
`artifact.json`. Referenced file bytes and cover images share
`artifacts/{sha256}`, named by their own digest.

Publishing goes only to `services/hub-public/` — the retired local-write
publish endpoint (`POST .../hub/assets/publish`) is gone, and no publisher
token can write the blessed shelf. Publishing from a project uses
`POST .../hub/remote/assets/publish` with a publisher token (project-source
body), or `POST /api/hub/remote/assets` with a prebuilt release document.