@ziprack/sdk 0.2.1 → 0.3.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.
- package/dist/auth/index.d.ts +11 -0
- package/dist/core/mock/MockRack.d.ts +15 -0
- package/dist/core/rack.d.ts +9 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/docs/README.md +3 -3
- package/dist/docs/applet-packages.md +18 -8
- package/dist/docs/auth.md +76 -0
- package/dist/docs/broadcast.md +1 -1
- package/dist/docs/conformance.md +14 -13
- package/dist/docs/database.md +253 -10
- package/dist/docs/extending.md +70 -4
- package/dist/docs/installation.md +9 -4
- package/dist/docs/introduction.md +7 -5
- package/dist/docs/manifest.md +32 -0
- package/dist/docs/mcp.md +15 -3
- package/dist/docs/protocol.md +42 -0
- package/dist/docs/publishing.md +87 -8
- package/dist/docs/reporting.md +11 -4
- package/dist/docs/sdk.md +28 -4
- package/dist/docs/security.md +53 -0
- package/dist/docs/storage.md +1 -1
- package/dist/docs/studio.md +1 -1
- package/dist/docs/versioning.md +16 -0
- package/dist/docs/ziprack-1-authoring.md +43 -7
- package/dist/index.js +41 -18
- package/dist/index.js.map +1 -1
- package/dist/{store-BXGon4iJ.js → store-DQbG_t2D.js} +15 -6
- package/dist/store-DQbG_t2D.js.map +1 -0
- package/dist/testing.js +1 -1
- package/dist/ziprack-sdk.iife.js +1 -1
- package/dist/ziprack-sdk.iife.js.map +1 -1
- package/package.json +1 -1
- package/dist/store-BXGon4iJ.js.map +0 -1
package/dist/docs/extending.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
- [Message Handlers](#message-handlers)
|
|
8
8
|
- [Launch Payload](#launch-payload)
|
|
9
9
|
- [Install Hooks](#install-hooks)
|
|
10
|
+
- [Promotion Hooks](#promotion-hooks)
|
|
11
|
+
- [Rack Roles](#rack-roles)
|
|
10
12
|
- [Conformance and Status](#conformance-and-status)
|
|
11
13
|
- [The SDK Side](#the-sdk-side)
|
|
12
14
|
|
|
@@ -60,7 +62,8 @@ class SurveyCapability extends AbstractCapability
|
|
|
60
62
|
| `validateManifest()` | Cross-field checks; add errors to the validator. |
|
|
61
63
|
| `messageHandlers()` | `type => handler class`. |
|
|
62
64
|
| `launchPayload()` | What to merge into `rack:launch` under this capability's id. |
|
|
63
|
-
| `onInstall()` / `onPromote()` / `onRetire()` | Version lifecycle hooks. `onInstall()` runs inside the install transaction, so row changes only. |
|
|
65
|
+
| `onInstall()` / `onPromote()` / `onRetire()` | Version lifecycle hooks. `onInstall()` runs inside the install transaction, after extraction, so row changes only. `onPromote()` runs after the promotion commits. |
|
|
66
|
+
| `preparePromotion()` | Only with `ParticipatesInPromotion`: carry state over when the current version changes, or refuse. See [Promotion Hooks](#promotion-hooks). |
|
|
64
67
|
| `adminRoutes()` | Headless admin routes, registered only when `ziprack/admin` is installed. |
|
|
65
68
|
| `conformanceTests()` | Checks contributed to the conformance suite. |
|
|
66
69
|
| `statusChecks()` | Warnings for `ziprack:status` and `/ziprack/health`. |
|
|
@@ -208,15 +211,78 @@ public function onInstall(AppletVersion $version, ?AppletVersion $previous): voi
|
|
|
208
211
|
}
|
|
209
212
|
```
|
|
210
213
|
|
|
211
|
-
`onInstall()` runs inside the install transaction,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
`onInstall()` runs inside the install transaction, after the package is extracted, with the
|
|
215
|
+
previous current version for comparison. It can read the package's files from
|
|
216
|
+
`$version->storage_path` on `ziprack.disk` — which is how `db@1` checks seed files and migration
|
|
217
|
+
names. Keep it to checking and to rows the live version cannot see: the version is not current yet,
|
|
218
|
+
so anything that changes what running sessions read belongs in [promotion](#promotion-hooks). If it
|
|
219
|
+
throws, the whole installation rolls back and extracted files are removed; throw a
|
|
220
|
+
`PackageInstallException` to give the uploader a message.
|
|
214
221
|
|
|
215
222
|
Keep it to row changes. `CREATE`, `ALTER` and `DROP` commit any open transaction on MySQL and
|
|
216
223
|
MariaDB, which breaks the install and lets the conformance suite's data escape its rollback. If a
|
|
217
224
|
capability needs tables, ship them as migrations; the conformance suite fails any check that commits
|
|
218
225
|
its transaction.
|
|
219
226
|
|
|
227
|
+
<a name="promotion-hooks"></a>
|
|
228
|
+
## Promotion Hooks
|
|
229
|
+
|
|
230
|
+
A capability that holds per-applet state which has to follow the current version — `db@1` and its
|
|
231
|
+
records — implements `ZipRack\Core\Contracts\ParticipatesInPromotion`:
|
|
232
|
+
|
|
233
|
+
```php
|
|
234
|
+
use ZipRack\Core\Contracts\ParticipatesInPromotion;
|
|
235
|
+
|
|
236
|
+
class SurveyCapability extends AbstractCapability implements ParticipatesInPromotion
|
|
237
|
+
{
|
|
238
|
+
public function preparePromotion(AppletVersion $version, ?AppletVersion $outgoing, array $input): ?Closure
|
|
239
|
+
{
|
|
240
|
+
$plan = $this->planner->plan($version, $outgoing);
|
|
241
|
+
|
|
242
|
+
if ($plan->losesAnswers() && ! ($input['accept_loss'] ?? false)) {
|
|
243
|
+
throw new PromotionBlockedException('Version '.$version->version.' drops surveys that have answers.');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return fn () => $this->planner->apply($plan); // inside the promotion transaction
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`PromoteVersion::handle($version, $force, $input)` calls it on every capability the *target*
|
|
252
|
+
version declares, whenever the current version actually changes, forwards or back:
|
|
253
|
+
|
|
254
|
+
1. `preparePromotion()` runs **before** the transaction opens, so slow or file-writing work — `db@1`
|
|
255
|
+
writes its snapshot here — stays out of it. Throw a `PromotionBlockedException` to refuse.
|
|
256
|
+
2. The closures it returned run **inside** the promotion transaction, before `current_version_id`
|
|
257
|
+
moves. Row changes only, for the same DDL reason as `onInstall()`.
|
|
258
|
+
3. After the commit, `onPromote()` runs.
|
|
259
|
+
|
|
260
|
+
`$input` is this capability's share of the promotion input, keyed by its id: the admin API's
|
|
261
|
+
promote body passes `db: {…}` to `db@1`. A refusal that carries something the caller should show
|
|
262
|
+
extends `PromotionBlockedException`; `db@1` throws `ZipRack\Core\Installer\DataCutoverRequired`,
|
|
263
|
+
with a `reason` and the plan as `details()`, which the admin API turns into a `409`.
|
|
264
|
+
|
|
265
|
+
<a name="rack-roles"></a>
|
|
266
|
+
## Rack Roles
|
|
267
|
+
|
|
268
|
+
An applet's declared [roles](auth.md#roles) can be assigned to everyone holding one of the Rack's
|
|
269
|
+
own roles. zipRACK asks the Rack what those are through `ZipRack\Core\Contracts\RackRoles`:
|
|
270
|
+
|
|
271
|
+
```php
|
|
272
|
+
interface RackRoles
|
|
273
|
+
{
|
|
274
|
+
/** @return array<string, string> key => label */
|
|
275
|
+
public function all(): array;
|
|
276
|
+
|
|
277
|
+
/** @return list<string> keys */
|
|
278
|
+
public function of(Authenticatable $user): array;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The default, `GateRackRoles`, has one role, `administrator`, from the `ziprack.admin` gate. A Rack
|
|
283
|
+
with authors, facilitators or departments binds its own, the same way it binds
|
|
284
|
+
[`AppletAccess`](reporting.md#who-may-do-what).
|
|
285
|
+
|
|
220
286
|
<a name="conformance-and-status"></a>
|
|
221
287
|
## Conformance and Status
|
|
222
288
|
|
|
@@ -99,7 +99,7 @@ ones you will actually touch.
|
|
|
99
99
|
|---|---|
|
|
100
100
|
| `ziprack-auth.php` | `releasable` — the single place a deployment answers "what can applets see about our people"; `claim_map` |
|
|
101
101
|
| `ziprack-attempt.php` | `max_state_bytes` (64 KB), `max_segment_keys` (20) |
|
|
102
|
-
| `ziprack-db.php` | `max_records_per_scope` (5000), `max_record_bytes` (64 KB), `max_indexed_paths` (8), `list_limit` (100) |
|
|
102
|
+
| `ziprack-db.php` | `max_records_per_scope` (5000), `max_record_bytes` (64 KB), `max_indexed_paths` (8), `list_limit` (100), `snapshots.disk` (`ziprack.disk`; must be persistent), `snapshots.keep` (10) |
|
|
103
103
|
| `ziprack-storage.php` | `disk`, `max_bytes_cap` (50 MB), `url_ttl_minutes` (10), `scanner` (`null` \| `clamav`) |
|
|
104
104
|
| `ziprack-ai.php` | `provider`, `models.analysis`, `models.chat`, `allowed_models`, the whole `budget` block |
|
|
105
105
|
| `ziprack-broadcast.php` | `join_prefix`, `facilitator_grace_seconds` (60), `presence_window_seconds` (45), `max_event_bytes` |
|
|
@@ -122,6 +122,7 @@ validation. Defaults apply when a manifest declares nothing.
|
|
|
122
122
|
| `GET /ziprack/f/{file}` | Signed, short-lived download of a stored file (`storage`). |
|
|
123
123
|
| `POST /ziprack/s/{session}/broadcast/auth` | Rack-side WebSocket channel authentication (`broadcast`). |
|
|
124
124
|
| `GET /ziprack/health` | Fleet visibility: format, core version, capabilities, warnings. |
|
|
125
|
+
| `GET /ziprack/migration-runner` | The no-network page a Rack UI frames, sandboxed, to run [data migrations](database.md#data-migrations) at promotion. |
|
|
125
126
|
| `GET /r/{code}` | Join a room by code (`broadcast`). |
|
|
126
127
|
| `*` `/ziprack/admin/*` | The headless admin API (`ziprack/admin`). |
|
|
127
128
|
|
|
@@ -172,8 +173,8 @@ the deployment owns. A Rack can add an approved image host; it cannot reintroduc
|
|
|
172
173
|
|
|
173
174
|
| Command | Does |
|
|
174
175
|
|---|---|
|
|
175
|
-
| `ziprack:install {zip} [--
|
|
176
|
-
| `ziprack:promote {slug} {version} [--force]` | Make a version current. `--force` also approves a pending capability review. |
|
|
176
|
+
| `ziprack:install {zip} [--slug=] [--activate] [--force]` | Install a package as a new version; `--slug` installs into that existing applet. `--activate` promotes it, `--force` past a pending capability review. |
|
|
177
|
+
| `ziprack:promote {slug} {version} [--force] [--accept-data-loss] [--keep-invalid]` | Make a version current. `--force` also approves a pending capability review; `--accept-data-loss` confirms a [data cutover](publishing.md#the-data-cutover) that rejects or drops records, and `--keep-invalid` keeps rejected records instead of deleting them. |
|
|
177
178
|
| `ziprack:retire {slug} {version}` | Kill switch: the version can no longer be launched. |
|
|
178
179
|
| `ziprack:status [--json]` | Format, core version, installed capabilities, warnings. |
|
|
179
180
|
| `ziprack:conformance [--json]` | Verify this Rack honours `ziprack/1`. |
|
|
@@ -182,5 +183,9 @@ the deployment owns. A Rack can add an approved image host; it cannot reintroduc
|
|
|
182
183
|
| `ziprack:roster:import {csv}` | Import profile attributes by email. |
|
|
183
184
|
| `ziprack:room:create {slug} [--name=] [--mode=] [--roster=]` | Create a room and print its join code. |
|
|
184
185
|
| `ziprack:rooms:tick` | Vacate and reassign dropped facilitators. **Schedule every minute.** |
|
|
185
|
-
| `ziprack:db:prune [--force]` |
|
|
186
|
+
| `ziprack:db:prune [--stranded] [--force]` | Delete collections no current manifest declares, and their records. `--stranded` also deletes applet-scoped rows earlier versions left behind before v1.8. |
|
|
187
|
+
| `ziprack:db:recover {slug} {version} [collection] [--replace] [--force]` | Bring those stranded rows back into the live dataset. |
|
|
188
|
+
| `ziprack:db:snapshot {slug}` · `ziprack:db:snapshots {slug}` | Snapshot an applet's data; list its snapshots. |
|
|
189
|
+
| `ziprack:db:restore {slug} {snapshot} [--force]` | Replace an applet's data with a snapshot and make that version current again. |
|
|
190
|
+
| `ziprack:mcp:token {email} [--name=]` · `ziprack:mcp:revoke {email} [token] [--all]` | Mint and revoke [MCP](mcp.md) tokens. |
|
|
186
191
|
| `ziprack:response-analysis [--applet=] [--force]` | Regenerate admin AI summaries. **Schedule every few hours.** |
|
|
@@ -40,7 +40,7 @@ The iframe is rendered with:
|
|
|
40
40
|
|
|
41
41
|
```html
|
|
42
42
|
<iframe sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
43
|
-
allow="autoplay; fullscreen; picture-in-picture; encrypted-media">
|
|
43
|
+
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write">
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Note the absence of `allow-same-origin`. The applet document has an **opaque origin**: no cookies,
|
|
@@ -72,11 +72,12 @@ ever cross the boundary.
|
|
|
72
72
|
| **Applet** | One experience or app, identified by an immutable `slug`. Has many versions. | core |
|
|
73
73
|
| **Version** | One uploaded zip. Immutable. Exactly one is current. | core |
|
|
74
74
|
| **Participant** | The person in front of the applet, resolved from the Rack's own users (or a guest cookie). | core |
|
|
75
|
+
| **Role** | A name the participant holds in an applet: built in (`member`, `guest`, `applet-author`, `rack-administrator`) or declared by the manifest and assigned by the Rack. Sent at launch. | core |
|
|
75
76
|
| **Attempt** | One participant's run. Holds `status`, `state`, `segment`. Pinned to the version it started on. | `attempt@1` |
|
|
76
77
|
| **Statement** | Append-only record: actor did verb on activity with result. The audit trail and the uniform export. | `activity@1` |
|
|
77
|
-
| **Record** | A mutable row in a declared collection, at a declared scope. Working state. | `db@1` |
|
|
78
|
+
| **Record** | A mutable row in a declared collection, at a declared scope. Working state that carries over to new versions. | `db@1` |
|
|
78
79
|
| **Stored file** | A participant upload, addressable only once scanned clean. | `storage@1` |
|
|
79
|
-
| **Room** | A shared session: an applet
|
|
80
|
+
| **Room** | A shared session: an applet plus its members, each `participant` or `facilitator`. | `broadcast@1` |
|
|
80
81
|
| **Profile attribute** | A Rack-owned fact about the participant, released only if requested and permitted. | `auth@1` |
|
|
81
82
|
|
|
82
83
|
Statements and records coexist because they answer different questions. A statement is *what
|
|
@@ -103,8 +104,9 @@ any call the manifest did not declare.
|
|
|
103
104
|
Capability versions are independent of the format string and of the Composer package version. See
|
|
104
105
|
[Versioning](versioning.md).
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
`Rack.eject()`, which asks the Rack to
|
|
107
|
+
Three things need no capability: [declared media](media.md) (`Rack.media.embedUrl(id)`),
|
|
108
|
+
[roles](auth.md#roles) (`Rack.auth.hasRole(name)`), and `Rack.eject()`, which asks the Rack to
|
|
109
|
+
leave the applet.
|
|
108
110
|
|
|
109
111
|
<a name="what-ziprack-is-not"></a>
|
|
110
112
|
## What zipRACK Is Not
|
package/dist/docs/manifest.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
- [Introduction](#introduction)
|
|
4
4
|
- [Core Fields](#core-fields)
|
|
5
5
|
- [The Requires Block](#the-requires-block)
|
|
6
|
+
- [Roles](#roles)
|
|
6
7
|
- [Capability Sections](#capability-sections)
|
|
7
8
|
- [A Complete Example](#a-complete-example)
|
|
8
9
|
- [Validation Errors](#validation-errors)
|
|
@@ -42,6 +43,7 @@ upload meaningful: the manifest is a contract a reviewer can read.
|
|
|
42
43
|
| `entry` | Optional, default `index.html`. Relative, no `..`, ends in `.html`. |
|
|
43
44
|
| `requires` | Required, even if empty. See below. |
|
|
44
45
|
| `media` | Optional. See [Media](media.md). |
|
|
46
|
+
| `roles` | Optional. The applet's own roles. See [Roles](#roles). |
|
|
45
47
|
|
|
46
48
|
Everything else at the top level belongs to a capability. A section whose owning capability is not
|
|
47
49
|
declared is a validation error, and so is a section no installed capability owns.
|
|
@@ -64,6 +66,28 @@ declared is a validation error, and so is a section no installed capability owns
|
|
|
64
66
|
- Declaring `attempt@1` has a side effect: it pins every participant to the version they started on.
|
|
65
67
|
See [Versioning](versioning.md#version-pinning).
|
|
66
68
|
|
|
69
|
+
<a name="roles"></a>
|
|
70
|
+
## Roles
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"roles": [
|
|
74
|
+
{ "name": "dungeon-master", "label": "Dungeon Master", "description": "Runs the session." },
|
|
75
|
+
{ "name": "spectator", "grant": "guests" }
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A core key, so it needs no capability. Every launch tells the applet which roles the participant
|
|
80
|
+
holds; the Rack decides who holds them. See [Roles](auth.md#roles) for how they resolve.
|
|
81
|
+
|
|
82
|
+
| Field | Rules |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `name` | Required. Lowercase words joined by hyphens, `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`, max 40, unique. Not one of the built-in roles: `rack-administrator`, `applet-author`, `member`, `guest`. |
|
|
85
|
+
| `label` | Optional, max 60. Shown to whoever assigns the role. Defaults to `name`. |
|
|
86
|
+
| `description` | Optional, max 255. |
|
|
87
|
+
| `grant` | Optional. Who holds it before anyone is assigned: `everyone`, `members`, `guests` or `assigned` (the default — nobody). |
|
|
88
|
+
|
|
89
|
+
At most 20 roles.
|
|
90
|
+
|
|
67
91
|
<a name="capability-sections"></a>
|
|
68
92
|
## Capability Sections
|
|
69
93
|
|
|
@@ -111,6 +135,10 @@ An applet exercising every capability at once. Most applets need three or four.
|
|
|
111
135
|
{ "id": "debrief.clip", "provider": "youtube", "key": "dQw4w9WgXcQ" }
|
|
112
136
|
],
|
|
113
137
|
|
|
138
|
+
"roles": [
|
|
139
|
+
{ "name": "judge", "label": "Judge", "description": "Scores the final pitches." }
|
|
140
|
+
],
|
|
141
|
+
|
|
114
142
|
"segments": [
|
|
115
143
|
{
|
|
116
144
|
"key": "track",
|
|
@@ -205,6 +233,10 @@ one message. The checks that catch people out:
|
|
|
205
233
|
| `A room-scoped collection requires the [broadcast] capability` | Scope implies capability. |
|
|
206
234
|
| `Indexed path [votes] must be a string, integer, number or boolean property declared in the schema` | You can only index what you declared, and only scalars. |
|
|
207
235
|
| `Only applet-scoped collections can be seeded` | A seed is shared reference data; see [Seeding](database.md#seeding). |
|
|
236
|
+
| `Role names are lowercase words joined by hyphens, like "dungeon-master".` | `roles[].name` has capitals, underscores or spaces. |
|
|
237
|
+
| `Role [guest] is one every Rack provides …; it cannot be declared.` | `rack-administrator`, `applet-author`, `member` and `guest` are built in. |
|
|
238
|
+
| `Duplicate role [judge].` | Role names are unique. |
|
|
239
|
+
| `A role's grant is one of everyone, members, guests or assigned.` | Any other `grant`. |
|
|
208
240
|
| `Placeholder {{pitch}} is not declared in vars.properties` | Prompt templates and their `vars` schema must agree. |
|
|
209
241
|
| `Analysis prompts need an output_schema of type object` | `mode: "analysis"` is structured output by definition. |
|
|
210
242
|
| `The $room channel is reserved for the Rack` | Pick another channel name. |
|
package/dist/docs/mcp.md
CHANGED
|
@@ -100,7 +100,7 @@ user is no longer an admin.
|
|
|
100
100
|
| `rack-status` | mcp | Format, implemented capabilities, health notes. Needs `ViewRack` |
|
|
101
101
|
| `list-applets`, `get-applet`, `get-applet-version` | mcp | Applets, versions, review state, manifest |
|
|
102
102
|
| `create-upload` | mcp | A signed upload URL (see below) |
|
|
103
|
-
| `promote-version` | mcp | Make a version current. It cannot approve, so it cannot force |
|
|
103
|
+
| `promote-version` | mcp | Make a version current. It cannot approve, so it cannot force. See below |
|
|
104
104
|
| `get-scaffold-skill` | mcp | The applet scaffold skill's files |
|
|
105
105
|
| `list-attempts`, `get-attempt`, `attempt-overview` | attempt | |
|
|
106
106
|
| `list-statements`, `applet-metrics` | activity | |
|
|
@@ -118,6 +118,17 @@ Capability tools are only present when their capability package is installed.
|
|
|
118
118
|
- **Audit log.** Writes (`mcp:upload`, `mcp:promote`) go to the audit log with the admin and the
|
|
119
119
|
OAuth client or token name they came through.
|
|
120
120
|
|
|
121
|
+
`promote-version` takes `slug`, `version`, and for the [data cutover](publishing.md#the-data-cutover):
|
|
122
|
+
|
|
123
|
+
| Argument | |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `accept_data_loss` | Promote even though the new version's collections reject or drop existing records. Pass it only after the user has seen what would be lost. |
|
|
126
|
+
| `keep_invalid` | With `accept_data_loss`, keep the records the new schema rejects instead of deleting them. |
|
|
127
|
+
|
|
128
|
+
A refused cutover comes back as a validation error carrying the message and the plan's notes, so
|
|
129
|
+
the agent can tell the user what would change. A version with data migrations is always refused:
|
|
130
|
+
they run in the browser, so it must be promoted from the applet's page in the Rack.
|
|
131
|
+
|
|
121
132
|
<a name="uploading-a-package"></a>
|
|
122
133
|
## Uploading a Package
|
|
123
134
|
|
|
@@ -133,8 +144,9 @@ The rules in [Publishing](publishing.md) are unchanged.
|
|
|
133
144
|
|
|
134
145
|
- **Default.** A later version installs as not current.
|
|
135
146
|
- **With `activate: true`.** The version is promoted, unless it asks for capabilities that no admin
|
|
136
|
-
has approved yet. In
|
|
137
|
-
review queue
|
|
147
|
+
has approved yet, or its data cutover needs a decision. In either case the response carries a
|
|
148
|
+
`warning` and the version stays installed but not current: approve it in the review queue, or
|
|
149
|
+
promote it with `promote-version` (or from the Rack, when it has migrations).
|
|
138
150
|
- **Reuse.** A used or expired URL answers `410`. Call `create-upload` again.
|
|
139
151
|
|
|
140
152
|
<a name="the-scaffold-skill"></a>
|
package/dist/docs/protocol.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- [Streaming](#streaming)
|
|
8
8
|
- [Events](#events)
|
|
9
9
|
- [What the Shell Does](#what-the-shell-does)
|
|
10
|
+
- [The Migration Runner](#the-migration-runner)
|
|
10
11
|
|
|
11
12
|
<a name="when-you-need-this"></a>
|
|
12
13
|
## When You Need This
|
|
@@ -51,6 +52,10 @@ The applet announces readiness as early as it can; the host replies with the ful
|
|
|
51
52
|
(see [Launch Data](sdk.md#launch-data)). Do not send capability calls before `rack:launch` arrives —
|
|
52
53
|
queue them, as the SDK does.
|
|
53
54
|
|
|
55
|
+
The core of the payload is `schema`, `actor`, `applet`, `capabilities`, `media` and `roles` — the
|
|
56
|
+
participant's [roles](auth.md#roles) in this applet, as a list of names, sent whatever the manifest
|
|
57
|
+
declares. Each declared capability adds its own key.
|
|
58
|
+
|
|
54
59
|
<a name="calls-and-acks"></a>
|
|
55
60
|
## Calls and Acks
|
|
56
61
|
|
|
@@ -119,3 +124,40 @@ Knowing its behaviour explains timings you would otherwise find surprising:
|
|
|
119
124
|
`/ziprack/s/{session}/stream/{requestId}` and relays each chunk as a partial ack.
|
|
120
125
|
- **Completion.** A successful `attempt:complete` flips the Rack's own completion chrome — which is
|
|
121
126
|
why a completed attempt lands on its final screen rather than restarting.
|
|
127
|
+
|
|
128
|
+
<a name="the-migration-runner"></a>
|
|
129
|
+
## The Migration Runner
|
|
130
|
+
|
|
131
|
+
A second, unrelated frame protocol, between the Rack's own UI and the page that runs a package's
|
|
132
|
+
[data migrations](database.md#data-migrations) at promotion. No applet is involved: the parent is
|
|
133
|
+
the Rack page promoting the version, the child is `GET /ziprack/migration-runner` (route
|
|
134
|
+
`ziprack.migration-runner`), framed sandboxed (see [Security](security.md#the-migration-runner)).
|
|
135
|
+
You need this only if you are building a Rack UI.
|
|
136
|
+
|
|
137
|
+
Messages are plain objects, not `ziprack` envelopes. Everything the runner posts carries
|
|
138
|
+
`ziprackMigrationRunner: true`; it accepts messages only from `window.parent`, and runs once per
|
|
139
|
+
page load.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
runner → parent { ziprackMigrationRunner: true, type: "ready" }
|
|
143
|
+
parent → runner { type: "run", steps, collections, context, timeoutMs? }
|
|
144
|
+
runner → parent { ziprackMigrationRunner: true, type: "log", step, level, text } … as it runs
|
|
145
|
+
runner → parent { ziprackMigrationRunner: true, type: "done", collections, ms }
|
|
146
|
+
or { ziprackMigrationRunner: true, type: "error", step, code, message, stack? }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Field | |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `steps` | `[{ name, direction: "up" \| "down", source }]`, in run order — the plan's steps from `GET …/cutover`. |
|
|
152
|
+
| `collections` | `{ name: { scope, records } }` — `collections` from `GET …/cutover/data`. |
|
|
153
|
+
| `context` | `{ applet, from, to }`: the slug and the outgoing and target version strings. |
|
|
154
|
+
| `timeoutMs` | Per step. Default 30000. |
|
|
155
|
+
| `level` | `meta`, `step`, `ok`, `fail`, `log`, `info`, `warn` or `error`. |
|
|
156
|
+
| `code` | `missing_up`, `missing_down` (the migration has no such function), `threw`, `timeout`, `bad_result` (it returned something other than `{ collection: records[] }`). |
|
|
157
|
+
|
|
158
|
+
Each step's source is imported as a module from a `blob:` URL and called with a copy of the
|
|
159
|
+
records; a returned object replaces the collections it names. `done.collections` holds every
|
|
160
|
+
collection, by name, as a list of records: pass it to `POST …/cutover/check` and then, with the
|
|
161
|
+
plan's fingerprint and the logged lines, to `…/promote` as `db.records`, `db.fingerprint` and
|
|
162
|
+
`db.log`. After an `error`, nothing is promoted. The runner also renders every line as a terminal,
|
|
163
|
+
so the parent can show the frame itself as the log.
|
package/dist/docs/publishing.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- [Installing a Version](#installing-a-version)
|
|
5
5
|
- [Capability Diff Review](#capability-diff-review)
|
|
6
6
|
- [Promotion](#promotion)
|
|
7
|
+
- [The Data Cutover](#the-data-cutover)
|
|
7
8
|
- [The Review Queue](#the-review-queue)
|
|
8
9
|
- [Retiring a Version](#retiring-a-version)
|
|
9
10
|
- [The Audit Log](#the-audit-log)
|
|
@@ -27,12 +28,15 @@ php artisan ziprack:install dist-zip/my-applet-1.0.0.zip --activate # install
|
|
|
27
28
|
|
|
28
29
|
or `POST /ziprack/admin/versions/upload` with the zip.
|
|
29
30
|
|
|
30
|
-
Installation validates the manifest against this Rack's capability set,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
to
|
|
31
|
+
Installation validates the manifest against this Rack's capability set, then, inside one
|
|
32
|
+
transaction, records the version, extracts its files and runs each declared capability's
|
|
33
|
+
`onInstall()` hook — after extraction, so a hook can read the package. `db@1` uses it to check seed
|
|
34
|
+
files and migration names and to register collection names the applet has never had; it does not
|
|
35
|
+
touch existing data. A failure anywhere rolls the transaction back and deletes the extracted
|
|
36
|
+
directory. A version arrives as `ready` but **not current**, unless it is the applet's first —
|
|
37
|
+
there is nothing to review a first version against, so it is promoted automatically.
|
|
34
38
|
|
|
35
|
-
Once
|
|
39
|
+
Once installed the Rack resolves declared media (the oEmbed lookup) and, when
|
|
36
40
|
`publishing.osv_lookup` is on, queues a batch of the SBOM's purls against OSV.dev. Findings attach
|
|
37
41
|
to the version and surface in the review queue; they do not block the install.
|
|
38
42
|
|
|
@@ -74,9 +78,84 @@ php artisan ziprack:promote my-applet 1.1.0 --force # approve the pending rev
|
|
|
74
78
|
or `POST /ziprack/admin/applets/{applet}/versions/{version}/approve` followed by
|
|
75
79
|
`…/promote`.
|
|
76
80
|
|
|
77
|
-
Promotion is refused when the version is not `ready`,
|
|
78
|
-
addition — the error names exactly what was added
|
|
79
|
-
|
|
81
|
+
Promotion is refused when the version is not `ready`, when it has an unapproved capability
|
|
82
|
+
addition — the error names exactly what was added — or when carrying the applet's data over needs
|
|
83
|
+
a decision (below). Otherwise the capabilities' promotion work and the move of `current_version_id`
|
|
84
|
+
commit in one transaction, then each capability's `onPromote()` hook runs.
|
|
85
|
+
|
|
86
|
+
<a name="the-data-cutover"></a>
|
|
87
|
+
## The Data Cutover
|
|
88
|
+
|
|
89
|
+
A version declaring `db@1` brings the applet's records over with it (see
|
|
90
|
+
[Data Across Versions](database.md#data-across-versions)). The Rack plans the cutover first, and
|
|
91
|
+
refuses with a reason while the plan needs a decision:
|
|
92
|
+
|
|
93
|
+
| Reason | Means | To promote |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `migration_required` | [Data migrations](database.md#data-migrations) have to run: the version's pending `up`s, or `down`s for applied migrations it lacks. | Promote from the Rack's UI, which runs them in the browser. |
|
|
96
|
+
| `data_changed` | The data changed between planning the cutover (or running the migrations) and committing it. | Review the plan and promote again; run the migrations again. |
|
|
97
|
+
| `version_changed` | Another promotion committed while this one was being prepared. | Review the new plan and promote again. |
|
|
98
|
+
| `migration_invalid` | What the migrations returned does not fit the version. The plan carries the check. | Fix the migration and upload a new version. |
|
|
99
|
+
| `data_loss` | No migration runs, and the new schema or quota rejects existing records, or a dropped collection holds records. | Confirm with `accept_data_loss`. |
|
|
100
|
+
|
|
101
|
+
Over the admin API the refusal is a `409`:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"message": "Version 2.0.0 changes collections that hold data. Review what changes and confirm.",
|
|
106
|
+
"reason": "data_loss",
|
|
107
|
+
"plan": {
|
|
108
|
+
"version": { "id": 12, "version": "2.0.0" },
|
|
109
|
+
"from": { "id": 9, "version": "1.4.0" },
|
|
110
|
+
"status": "confirm",
|
|
111
|
+
"needs_browser": false,
|
|
112
|
+
"rollback": false,
|
|
113
|
+
"steps": [],
|
|
114
|
+
"collections": [
|
|
115
|
+
{
|
|
116
|
+
"name": "questions", "scope": "applet", "change": "changed", "records": 240,
|
|
117
|
+
"diff": [{ "path": "difficulty", "change": "required_property_added", "detail": "New required property difficulty." }],
|
|
118
|
+
"invalid": 240, "invalid_samples": [{ "id": "…", "errors": { "difficulty": ["…"] } }],
|
|
119
|
+
"over_quota": 0, "seed": "skipped"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"notes": [{ "level": "warning", "message": "240 of 240 records in [questions] do not match the new schema." }],
|
|
123
|
+
"fingerprint": "…"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
and the decision goes back in the promote body under `db`:
|
|
129
|
+
|
|
130
|
+
| Key | |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `accept_data_loss` | Confirm a `data_loss` plan. A JSON boolean; anything else is a `422`. |
|
|
133
|
+
| `invalid` | `discard` (the default) deletes the records the new schema rejects; `keep` leaves them. |
|
|
134
|
+
| `records`, `fingerprint`, `log` | A migrating version: what the migrations returned, by collection; the fingerprint of the data they ran on; the runner's log, kept with the snapshot. |
|
|
135
|
+
|
|
136
|
+
`GET …/versions/{version}/cutover` returns the same plan without promoting, including each step's
|
|
137
|
+
source. A Rack UI promoting a migrating version reads `…/cutover/data` (the records and their
|
|
138
|
+
fingerprint), runs the steps in the [migration runner](protocol.md#the-migration-runner), checks the
|
|
139
|
+
result with `POST …/cutover/check` (`{records}` → `{ok, errors, collections}`), then promotes with
|
|
140
|
+
`db.records` and `db.fingerprint`. The server checks the records again inside the transaction.
|
|
141
|
+
|
|
142
|
+
From the command line and over MCP a lossy plan can be confirmed but a migration cannot run:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
php artisan ziprack:promote my-applet 2.0.0 --accept-data-loss
|
|
146
|
+
php artisan ziprack:promote my-applet 2.0.0 --accept-data-loss --keep-invalid
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Every cutover that changes data is preceded by a [snapshot](database.md#snapshots).
|
|
150
|
+
|
|
151
|
+
**Uploading with activate.** `ziprack:install --activate`, an admin upload with `activate`, and
|
|
152
|
+
MCP's `create-upload` with `activate: true` try the promotion straight after installing, with no
|
|
153
|
+
cutover decision. If the cutover needs one, the version is installed but not current: the admin
|
|
154
|
+
upload and the MCP upload answer `201` with a `warning` and `cutover: {reason, plan}`, and the
|
|
155
|
+
command prints the refusal and exits non-zero. Promote it once the decision is made.
|
|
156
|
+
|
|
157
|
+
A version that drops `db@1` still goes through the cutover: the applet's collections all count as
|
|
158
|
+
removed, so promoting it needs the same confirmation as removing them one by one.
|
|
80
159
|
|
|
81
160
|
What promotion means depends on pinning:
|
|
82
161
|
|
package/dist/docs/reporting.md
CHANGED
|
@@ -32,7 +32,8 @@ GET /ziprack/admin/applets/{applet}/panels
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
`panels` is the intersection of what the current version declares and what this Rack implements —
|
|
35
|
-
render exactly those and nothing else.
|
|
35
|
+
render exactly those and nothing else. It also contains `roles` when the current version declares
|
|
36
|
+
any [roles](auth.md#roles), for the screen that assigns them.
|
|
36
37
|
|
|
37
38
|
| Declared | Renders |
|
|
38
39
|
|---|---|
|
|
@@ -56,9 +57,10 @@ All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `w
|
|
|
56
57
|
| `GET applets` · `POST applets` | List and create |
|
|
57
58
|
| `GET applets/{applet}` · `PATCH applets/{applet}` | Show and update |
|
|
58
59
|
| `GET applets/{applet}/panels` | What to render |
|
|
59
|
-
| `
|
|
60
|
+
| `GET applets/{applet}/roles` · `PUT applets/{applet}/roles/{role}` | Declared roles and their assignments; replace one role's ([Roles](auth.md#rack-side)) |
|
|
61
|
+
| `POST versions/upload` · `POST applets/{applet}/versions` | Install a package; with `activate`, a blocked promotion answers `201` with `warning` (and `cutover`) |
|
|
60
62
|
| `GET applets/{applet}/versions/{version}` | Manifest, requires diff, SBOM, OSV findings, publisher |
|
|
61
|
-
| `POST …/{version}/approve` · `…/promote` · `…/retire` | The publishing actions |
|
|
63
|
+
| `POST …/{version}/approve` · `…/promote` · `…/retire` | The publishing actions. `promote` takes `force` and `db`, and answers `409` when the [data cutover](publishing.md#the-data-cutover) needs a decision |
|
|
62
64
|
| `GET review-queue` | Everything awaiting a decision |
|
|
63
65
|
| `GET participants` · `GET audit-log` | People and capability calls |
|
|
64
66
|
|
|
@@ -69,6 +71,8 @@ Capability packages add their own, and only when installed:
|
|
|
69
71
|
| `GET applets/{applet}/overview` · `…/attempts` · `…/attempts/{attempt}` | `attempt` |
|
|
70
72
|
| `GET applets/{applet}/attempts/{attempt}/statements` · `…/export.csv` · `…/metrics` | `activity` |
|
|
71
73
|
| `GET applets/{applet}/collections` · `…/collections/{name}/records` | `db` |
|
|
74
|
+
| `GET applets/{applet}/versions/{version}/cutover` · `…/cutover/data` · `POST …/cutover/check` | `db` — the cutover plan, the records migrations run on, a check of what they return |
|
|
75
|
+
| `GET applets/{applet}/snapshots` · `POST …/snapshots` · `GET …/snapshots/{snapshot}/log` · `…/download` · `POST …/restore` | `db` — [snapshots](database.md#snapshots); a restore requires `confirm` set to the applet's slug |
|
|
72
76
|
| `GET applets/{applet}/files` | `storage` |
|
|
73
77
|
| `GET applets/{applet}/ai/spend` · `…/ai/calls` | `ai` |
|
|
74
78
|
| `GET applets/{applet}/rooms` · `POST rooms` · `GET rooms/{room}` · `POST rooms/{room}/status` · `GET rooms/{room}/events` | `broadcast` |
|
|
@@ -84,7 +88,7 @@ zipRACK asks, and the Rack answers. Every check goes through one contract,
|
|
|
84
88
|
|---|---|---|
|
|
85
89
|
| `UseAdmin` | `ziprack.use-admin` | The admin API, the MCP server, personal MCP tokens |
|
|
86
90
|
| `CreateApplet` | `ziprack.create-applet` | `POST applets`, `POST versions/upload`, an agent's upload without a slug |
|
|
87
|
-
| `ManageApplet` | `ziprack.manage-applet` | Every `applets/{applet}` route; unpublished and `?preview` player launches |
|
|
91
|
+
| `ManageApplet` | `ziprack.manage-applet` | Every `applets/{applet}` route — promoting, assigning roles, the cutover routes, taking and restoring snapshots; unpublished and `?preview` player launches |
|
|
88
92
|
| `ReviewVersions` | `ziprack.review` | `GET review-queue`, approving, promoting with `force` past review |
|
|
89
93
|
| `ViewRack` | `ziprack.view-rack` | `GET status`, `GET audit-log`, every participant, the `rack-status` tool |
|
|
90
94
|
|
|
@@ -188,3 +192,6 @@ The skeleton ships a reference admin over these endpoints. When you replace it:
|
|
|
188
192
|
2. Render `db@1` record tables from the collection's declared JSON Schema — the schema is there
|
|
189
193
|
precisely so the UI does not need per-applet knowledge.
|
|
190
194
|
3. Treat the review queue as the landing page. It is where the decisions are.
|
|
195
|
+
4. Show the cutover plan before promoting, and run migrations in the sandboxed
|
|
196
|
+
[migration runner](protocol.md#the-migration-runner). A Rack UI is the only place a version with
|
|
197
|
+
data migrations can be promoted.
|
package/dist/docs/sdk.md
CHANGED
|
@@ -64,8 +64,9 @@ interface LaunchData {
|
|
|
64
64
|
applet: { slug: string; name: string; version: string };
|
|
65
65
|
capabilities: string[]; // as declared, e.g. ['auth@1', 'db@1']
|
|
66
66
|
media: Record<string, MediaEmbed>; // Rack-built player URLs by declared id
|
|
67
|
+
roles?: string[]; // e.g. ['member', 'judge']; absent before Rack v1.8
|
|
67
68
|
|
|
68
|
-
auth?: { user: LaunchUser };
|
|
69
|
+
auth?: { user: LaunchUser; withheld?: string[] };
|
|
69
70
|
attempt?: { id, status, state, segment };
|
|
70
71
|
activity?: { activities: LaunchActivity[] };
|
|
71
72
|
db?: { collections: Array<{ name, scope }> };
|
|
@@ -75,9 +76,11 @@ interface LaunchData {
|
|
|
75
76
|
}
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
Each capability contributes its own key, and only when declared.
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Each capability contributes its own key, and only when declared. `roles` is core and always
|
|
80
|
+
present; read it through `Rack.auth.roles()`, `Rack.auth.hasRole(name)` and `Rack.auth.isGuest()`,
|
|
81
|
+
which need no capability (see [Roles](auth.md#roles)). The typed accessors read from the same
|
|
82
|
+
payload, so `Rack.auth.user()`, `Rack.auth.roles()`, `Rack.attempt.state()` and
|
|
83
|
+
`Rack.activity.activities()` are synchronous and free.
|
|
81
84
|
|
|
82
85
|
```ts
|
|
83
86
|
Rack.on('launch', (data) => render(data)); // fires immediately if already launched
|
|
@@ -178,6 +181,14 @@ Rack.mock.ai('score_pitch', (vars) => ({ clarity: 4, feedback: `Nice work on "${
|
|
|
178
181
|
Rack.mock.resetAttempt(); // replay a completed attempt
|
|
179
182
|
```
|
|
180
183
|
|
|
184
|
+
The mock participant is a `member` holding the manifest's `everyone` and `members` grants. Try
|
|
185
|
+
other identities from the page URL:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
http://localhost:5173/?ziprack-guest a guest, with the guests grants
|
|
189
|
+
http://localhost:5173/?ziprack-roles=judge,applet-author as if the Rack had assigned these
|
|
190
|
+
```
|
|
191
|
+
|
|
181
192
|
Mock mode also engages if a real Rack fails to answer `rack:launch` within `launchTimeoutMs`. That
|
|
182
193
|
is a deliberate development convenience, and the console says so — if you see it in production,
|
|
183
194
|
something is wrong with the shell, not with your applet.
|
|
@@ -214,6 +225,19 @@ it('records the answer and completes exactly once', async () => {
|
|
|
214
225
|
| `host.attempt()`, `host.records(name)`, `host.files()` | Persisted state. |
|
|
215
226
|
| `host.push(event)` | Push a `rack:event` in, as the Rack would, to drive broadcast handlers. |
|
|
216
227
|
|
|
228
|
+
`new FakeHost(manifest, options)` takes the mock Rack's options. Those worth knowing:
|
|
229
|
+
|
|
230
|
+
| Option | |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `guest` | Launch as a guest: `guest` instead of `member`, and the `guests` grants. |
|
|
233
|
+
| `roles` | Roles held on top of the grants, as if assigned. Any name, built-in ones included. |
|
|
234
|
+
| `role` | The room role, `participant` or `facilitator`, instead of the one the assignment mode gives. |
|
|
235
|
+
| `participantId`, `participantName` | The actor. |
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const host = new FakeHost(manifest, { roles: ['judge'] });
|
|
239
|
+
```
|
|
240
|
+
|
|
217
241
|
<a name="leaving-the-applet"></a>
|
|
218
242
|
## Leaving the Applet
|
|
219
243
|
|