@ziprack/sdk 0.2.0 → 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/db/index.d.ts +2 -1
- package/dist/docs/README.md +7 -3
- package/dist/docs/activity.md +4 -0
- package/dist/docs/applet-packages.md +18 -8
- package/dist/docs/attempts.md +6 -3
- 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 +308 -11
- package/dist/docs/extending.md +76 -5
- package/dist/docs/installation.md +11 -5
- package/dist/docs/introduction.md +7 -5
- package/dist/docs/manifest.md +34 -1
- package/dist/docs/mcp.md +202 -0
- package/dist/docs/protocol.md +42 -0
- package/dist/docs/publishing.md +87 -8
- package/dist/docs/reporting.md +55 -4
- package/dist/docs/sdk.md +28 -4
- package/dist/docs/security.md +87 -8
- 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 +45 -9
- 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/mcp.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# The Rack MCP Server
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Installing](#installing)
|
|
5
|
+
- [Connecting an Agent](#connecting-an-agent)
|
|
6
|
+
- [Tools](#tools)
|
|
7
|
+
- [Uploading a Package](#uploading-a-package)
|
|
8
|
+
- [The Scaffold Skill](#the-scaffold-skill)
|
|
9
|
+
- [Contributing Tools From a Capability](#contributing-tools)
|
|
10
|
+
|
|
11
|
+
<a name="introduction"></a>
|
|
12
|
+
## Introduction
|
|
13
|
+
|
|
14
|
+
`ziprack/mcp` puts a [Model Context Protocol](https://modelcontextprotocol.io) server on the Rack,
|
|
15
|
+
at `/ziprack/mcp`. An agent connected to it acts as one of the Rack's admins. It can list applets
|
|
16
|
+
and their versions, read what participants produced, upload and promote packages, and fetch the
|
|
17
|
+
skill that scaffolds a new applet.
|
|
18
|
+
|
|
19
|
+
It is the Rack-side counterpart of the two authoring servers, which run locally with no Rack.
|
|
20
|
+
`ziprack-mcp` serves the SDK docs and `ziprack-studio mcp` serves Studio blocks (see
|
|
21
|
+
[The JavaScript SDK](sdk.md) and [zipRACK Studio](studio.md)).
|
|
22
|
+
|
|
23
|
+
Agents authenticate in one of two ways. Clients that can complete a browser OAuth redirect use
|
|
24
|
+
Passport OAuth, as claude.ai and Claude Code do. Clients such as Cursor, which only accept
|
|
25
|
+
localhost redirects, use a minted token. Both kinds are Passport tokens with the `mcp:use` scope on
|
|
26
|
+
the `api` guard. Every call needs `ziprack.use-admin`, and tools see only the applets the caller may
|
|
27
|
+
see (see [Who May Do What](reporting.md#who-may-do-what)).
|
|
28
|
+
|
|
29
|
+
<a name="installing"></a>
|
|
30
|
+
## Installing
|
|
31
|
+
|
|
32
|
+
The package is optional. A Rack without it never needs Passport.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
composer require ziprack/mcp
|
|
36
|
+
php artisan passport:install # keys, migrations, and the personal access client
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The Rack must do the rest itself, because a package can't impose it:
|
|
40
|
+
|
|
41
|
+
- **API guard.** In `config/auth.php`, add
|
|
42
|
+
`'api' => ['driver' => 'passport', 'provider' => 'users']`.
|
|
43
|
+
- **User model.** It uses `Laravel\Passport\HasApiTokens` and implements
|
|
44
|
+
`Laravel\Passport\Contracts\OAuthenticatable`.
|
|
45
|
+
- **Consent screen.** Call `Passport::authorizationView(...)` to render your approve and deny page.
|
|
46
|
+
Post it with plain HTML forms, not Inertia visits: the redirect goes to the client's origin or
|
|
47
|
+
a custom scheme.
|
|
48
|
+
- **Login redirect.** Add `AuthenticationException::redirectUsing(fn () => route('login'))`, so
|
|
49
|
+
that `/oauth/authorize` sends a signed-out admin to the login page rather than returning a 401.
|
|
50
|
+
- **Custom schemes.** `php artisan vendor:publish --tag=mcp-config`, then set
|
|
51
|
+
`custom_schemes => ['claude', 'cursor', 'vscode']` in `config/mcp.php`.
|
|
52
|
+
- **Optional client model.** Override `skipsAuthorization()` on a custom Passport client model so a
|
|
53
|
+
client that already holds a token isn't asked for consent again.
|
|
54
|
+
|
|
55
|
+
`config/ziprack-mcp.php` is published with the `ziprack-config` tag:
|
|
56
|
+
|
|
57
|
+
| Key | Default | |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `enabled` | `true` | Register the endpoint, upload route and token routes |
|
|
60
|
+
| `path` | `ziprack/mcp` | The endpoint |
|
|
61
|
+
| `middleware` | `auth:api`, `mcp:use` scope, `can:ziprack.use-admin`, `throttle:60,1` | |
|
|
62
|
+
| `oauth_routes` | `true` | laravel/mcp's discovery and dynamic client registration routes |
|
|
63
|
+
| `token_ttl_days` | `30` | Lifetime of a minted token |
|
|
64
|
+
| `upload_ttl_minutes` | `15` | Lifetime of a signed upload URL |
|
|
65
|
+
|
|
66
|
+
<a name="connecting-an-agent"></a>
|
|
67
|
+
## Connecting an Agent
|
|
68
|
+
|
|
69
|
+
**Claude Code and claude.ai (OAuth).** Add the endpoint URL. The client discovers the
|
|
70
|
+
authorization server from `/.well-known/oauth-protected-resource`, registers itself, and sends the
|
|
71
|
+
admin to the consent screen.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
claude mcp add --transport http my-rack https://rack.example.com/ziprack/mcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Cursor and other localhost-only clients (minted token).** Mint a token, either in the Rack's admin
|
|
78
|
+
UI (backed by `GET|POST /ziprack/admin/mcp/tokens` and `DELETE /ziprack/admin/mcp/tokens/{id}`) or
|
|
79
|
+
from the command line:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
php artisan ziprack:mcp:token admin@example.com --name="Cursor laptop"
|
|
83
|
+
php artisan ziprack:mcp:revoke admin@example.com --all
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then send the token as a bearer header:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "mcpServers": { "my-rack": { "url": "https://rack.example.com/ziprack/mcp", "headers": { "Authorization": "Bearer <token>" } } } }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The plaintext is shown once. A token acts as the admin who minted it, and it stops working once that
|
|
93
|
+
user is no longer an admin.
|
|
94
|
+
|
|
95
|
+
<a name="tools"></a>
|
|
96
|
+
## Tools
|
|
97
|
+
|
|
98
|
+
| Tool | From | |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `rack-status` | mcp | Format, implemented capabilities, health notes. Needs `ViewRack` |
|
|
101
|
+
| `list-applets`, `get-applet`, `get-applet-version` | mcp | Applets, versions, review state, manifest |
|
|
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. See below |
|
|
104
|
+
| `get-scaffold-skill` | mcp | The applet scaffold skill's files |
|
|
105
|
+
| `list-attempts`, `get-attempt`, `attempt-overview` | attempt | |
|
|
106
|
+
| `list-statements`, `applet-metrics` | activity | |
|
|
107
|
+
| `list-collections`, `list-records` | db | |
|
|
108
|
+
| `list-files` | storage | |
|
|
109
|
+
| `ai-spend` | ai | |
|
|
110
|
+
| `list-rooms`, `get-room` | broadcast | |
|
|
111
|
+
|
|
112
|
+
Capability tools are only present when their capability package is installed.
|
|
113
|
+
|
|
114
|
+
- **Same logic as the admin API.** Data tools run the same controller actions as the
|
|
115
|
+
[admin API](reporting.md), so filters, pagination and preview handling match what the admin UI
|
|
116
|
+
shows. Live data is the default; pass `preview: "preview"` or `preview: "all"` to include staff
|
|
117
|
+
preview runs.
|
|
118
|
+
- **Audit log.** Writes (`mcp:upload`, `mcp:promote`) go to the audit log with the admin and the
|
|
119
|
+
OAuth client or token name they came through.
|
|
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
|
+
|
|
132
|
+
<a name="uploading-a-package"></a>
|
|
133
|
+
## Uploading a Package
|
|
134
|
+
|
|
135
|
+
MCP has no binary arguments, and an agent can't push a 50 MB zip through its context. Instead,
|
|
136
|
+
`create-upload` returns a signed, single-use URL with a ready-made `curl` command:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
curl -sS -H 'Accept: application/json' -F 'package=@dist-zip/my-applet-1.1.0.zip' '<upload_url>'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The response is the installed version, in the same shape as `POST /ziprack/admin/versions/upload`.
|
|
143
|
+
The rules in [Publishing](publishing.md) are unchanged.
|
|
144
|
+
|
|
145
|
+
- **Default.** A later version installs as not current.
|
|
146
|
+
- **With `activate: true`.** The version is promoted, unless it asks for capabilities that no admin
|
|
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).
|
|
150
|
+
- **Reuse.** A used or expired URL answers `410`. Call `create-upload` again.
|
|
151
|
+
|
|
152
|
+
<a name="the-scaffold-skill"></a>
|
|
153
|
+
## The Scaffold Skill
|
|
154
|
+
|
|
155
|
+
The canonical `ziprack-scaffold` skill lives in `packages/mcp/resources/skills/ziprack-scaffold/`.
|
|
156
|
+
|
|
157
|
+
- **Tool.** `get-scaffold-skill` returns every file of the skill, keyed by path.
|
|
158
|
+
- **Prompt.** `install-scaffold-skill` (with `agent: "claude"` or `"cursor"`) tells the agent to
|
|
159
|
+
write those files into `.claude/skills/` or `.cursor/skills/`.
|
|
160
|
+
|
|
161
|
+
The skill scaffolds an applet on `@ziprack/sdk`, and its last step publishes that applet through
|
|
162
|
+
`create-upload`.
|
|
163
|
+
|
|
164
|
+
<a name="contributing-tools"></a>
|
|
165
|
+
## Contributing Tools From a Capability
|
|
166
|
+
|
|
167
|
+
A capability contributes tools the same way it contributes admin routes:
|
|
168
|
+
|
|
169
|
+
1. Implement `ZipRack\Core\Capabilities\ProvidesMcpTools` on the capability class.
|
|
170
|
+
2. Return tool class names from `mcpTools()`.
|
|
171
|
+
3. Have each tool extend `ZipRack\Mcp\Tools\ZipRackTool`. It provides the helpers below.
|
|
172
|
+
|
|
173
|
+
Helpers on `ZipRackTool`:
|
|
174
|
+
|
|
175
|
+
| Helper | |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `applet()` | Resolves the `slug` argument among the applets the caller may see |
|
|
178
|
+
| `visibleApplets()` | A query of the applets the caller may see |
|
|
179
|
+
| `owned()` | Loads a model that belongs to that applet |
|
|
180
|
+
| `callAdmin()` | Runs your admin controller action in-process and returns its JSON |
|
|
181
|
+
| `audit()` | Writes an audit-log row for a write |
|
|
182
|
+
|
|
183
|
+
The tool classes only load when `ziprack/mcp` is installed, so list it under `suggest`, not
|
|
184
|
+
`require`.
|
|
185
|
+
|
|
186
|
+
A tool that only some users should have takes `#[RequiresAbility(Ability::ViewRack)]` (or any
|
|
187
|
+
other ability). Everyone else is neither offered it nor able to call it. Tools that take a slug
|
|
188
|
+
need nothing extra: `applet()` already sees only the caller's applets.
|
|
189
|
+
|
|
190
|
+
<a name="adding-tools-from-the-rack"></a>
|
|
191
|
+
## Adding Tools From the Rack
|
|
192
|
+
|
|
193
|
+
The Rack can add tools of its own, for what only it knows about, such as its roles or its
|
|
194
|
+
per-applet settings. Register them from a service provider:
|
|
195
|
+
|
|
196
|
+
```php
|
|
197
|
+
app(ZipRack\Mcp\ToolRegistry::class)->register(SetGuestAccess::class, ListMyApplets::class);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Rack tools extend `ZipRackTool` like any other, so they get the helpers above, the audit log and
|
|
201
|
+
`#[RequiresAbility]`. A Rack tool with the same name (`#[Name('list-applets')]`) as a zipRACK or
|
|
202
|
+
capability tool replaces it.
|
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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- [Composition](#composition)
|
|
4
4
|
- [The Admin API](#the-admin-api)
|
|
5
|
+
- [Who May Do What](#who-may-do-what)
|
|
5
6
|
- [Preview Runs](#preview-runs)
|
|
6
7
|
- [The CSV Export](#the-csv-export)
|
|
7
8
|
- [Metrics](#metrics)
|
|
@@ -31,7 +32,8 @@ GET /ziprack/admin/applets/{applet}/panels
|
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
`panels` is the intersection of what the current version declares and what this Rack implements —
|
|
34
|
-
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.
|
|
35
37
|
|
|
36
38
|
| Declared | Renders |
|
|
37
39
|
|---|---|
|
|
@@ -46,7 +48,8 @@ render exactly those and nothing else.
|
|
|
46
48
|
## The Admin API
|
|
47
49
|
|
|
48
50
|
All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `web`, `auth` and the
|
|
49
|
-
`ziprack.admin` gate
|
|
51
|
+
`ziprack.use-admin` gate. Every route with an `{applet}`, including capability routes, also requires
|
|
52
|
+
`ziprack.manage-applet` for that applet. See [Who May Do What](#who-may-do-what).
|
|
50
53
|
|
|
51
54
|
| Route | Purpose |
|
|
52
55
|
|---|---|
|
|
@@ -54,9 +57,10 @@ All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `w
|
|
|
54
57
|
| `GET applets` · `POST applets` | List and create |
|
|
55
58
|
| `GET applets/{applet}` · `PATCH applets/{applet}` | Show and update |
|
|
56
59
|
| `GET applets/{applet}/panels` | What to render |
|
|
57
|
-
| `
|
|
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`) |
|
|
58
62
|
| `GET applets/{applet}/versions/{version}` | Manifest, requires diff, SBOM, OSV findings, publisher |
|
|
59
|
-
| `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 |
|
|
60
64
|
| `GET review-queue` | Everything awaiting a decision |
|
|
61
65
|
| `GET participants` · `GET audit-log` | People and capability calls |
|
|
62
66
|
|
|
@@ -67,11 +71,50 @@ Capability packages add their own, and only when installed:
|
|
|
67
71
|
| `GET applets/{applet}/overview` · `…/attempts` · `…/attempts/{attempt}` | `attempt` |
|
|
68
72
|
| `GET applets/{applet}/attempts/{attempt}/statements` · `…/export.csv` · `…/metrics` | `activity` |
|
|
69
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 |
|
|
70
76
|
| `GET applets/{applet}/files` | `storage` |
|
|
71
77
|
| `GET applets/{applet}/ai/spend` · `…/ai/calls` | `ai` |
|
|
72
78
|
| `GET applets/{applet}/rooms` · `POST rooms` · `GET rooms/{room}` · `POST rooms/{room}/status` · `GET rooms/{room}/events` | `broadcast` |
|
|
73
79
|
| `GET participants/{participant}/profile` · `PUT …` | `auth` |
|
|
74
80
|
|
|
81
|
+
<a name="who-may-do-what"></a>
|
|
82
|
+
## Who May Do What
|
|
83
|
+
|
|
84
|
+
zipRACK asks, and the Rack answers. Every check goes through one contract,
|
|
85
|
+
`ZipRack\Core\Contracts\AppletAccess`, with the applet in hand when there is one:
|
|
86
|
+
|
|
87
|
+
| Ability | Gate | Asked for |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `UseAdmin` | `ziprack.use-admin` | The admin API, the MCP server, personal MCP tokens |
|
|
90
|
+
| `CreateApplet` | `ziprack.create-applet` | `POST applets`, `POST versions/upload`, an agent's upload without a slug |
|
|
91
|
+
| `ManageApplet` | `ziprack.manage-applet` | Every `applets/{applet}` route — promoting, assigning roles, the cutover routes, taking and restoring snapshots; unpublished and `?preview` player launches |
|
|
92
|
+
| `ReviewVersions` | `ziprack.review` | `GET review-queue`, approving, promoting with `force` past review |
|
|
93
|
+
| `ViewRack` | `ziprack.view-rack` | `GET status`, `GET audit-log`, every participant, the `rack-status` tool |
|
|
94
|
+
|
|
95
|
+
`scopeVisible()` narrows the applet list, the MCP tools' slug lookups and `list-applets` to what
|
|
96
|
+
the user may see. An applet they may not see reads as not installed.
|
|
97
|
+
|
|
98
|
+
The default binding, `AdminOnlyAccess`, answers every ability with the `ziprack.admin` gate
|
|
99
|
+
(`$user->is_admin`), so a Rack that binds nothing, or only redefines that gate, behaves as before.
|
|
100
|
+
A Rack with other roles binds its own:
|
|
101
|
+
|
|
102
|
+
```php
|
|
103
|
+
$this->app->singleton(AppletAccess::class, RackAppletAccess::class);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Applets have an **owner**, a morph like a version's publisher: whoever published the first
|
|
107
|
+
version, or created the empty applet. `$applet->isOwnedBy($user)` is the usual basis for
|
|
108
|
+
`ManageApplet`. A package whose slug already belongs to an applet the publisher cannot manage is
|
|
109
|
+
refused, so an upload cannot land in someone else's applet.
|
|
110
|
+
|
|
111
|
+
**Participants** are Rack-wide. A user with `ViewRack` sees all of them; anyone else sees the
|
|
112
|
+
participants with audit-log entries on an applet they can see, through `GET participants` and the
|
|
113
|
+
`auth` profile routes. `ZipRack\Core\Access\ParticipantVisibility` applies the rule.
|
|
114
|
+
|
|
115
|
+
To react to publishing, listen for `ZipRack\Core\Events\VersionInstalled` (with the publisher),
|
|
116
|
+
`VersionApproved` and `VersionPromoted`.
|
|
117
|
+
|
|
75
118
|
<a name="preview-runs"></a>
|
|
76
119
|
## Preview Runs
|
|
77
120
|
|
|
@@ -87,6 +130,11 @@ returns **live attempts only** unless told otherwise:
|
|
|
87
130
|
An unrecognised value falls back to `live` rather than erroring, so a stale bookmark cannot quietly
|
|
88
131
|
turn preview data into a report.
|
|
89
132
|
|
|
133
|
+
The `db@1` record browser (`…/collections` and `…/collections/{name}/records`) takes the same
|
|
134
|
+
`?preview=` with the same default, and the collection `records` counts follow it. `ziprack_statements`
|
|
135
|
+
and `ziprack_records` both carry an `is_preview` column set from the launch session, so preview data
|
|
136
|
+
is saved, never mixed in, and easy to delete in one query when you want it gone.
|
|
137
|
+
|
|
90
138
|
<a name="the-csv-export"></a>
|
|
91
139
|
## The CSV Export
|
|
92
140
|
|
|
@@ -144,3 +192,6 @@ The skeleton ships a reference admin over these endpoints. When you replace it:
|
|
|
144
192
|
2. Render `db@1` record tables from the collection's declared JSON Schema — the schema is there
|
|
145
193
|
precisely so the UI does not need per-applet knowledge.
|
|
146
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
|
|