@ziprack/sdk 0.2.0 → 0.2.1
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/db/index.d.ts +2 -1
- package/dist/docs/README.md +4 -0
- package/dist/docs/activity.md +4 -0
- package/dist/docs/attempts.md +6 -3
- package/dist/docs/database.md +64 -10
- package/dist/docs/extending.md +8 -3
- package/dist/docs/installation.md +2 -1
- package/dist/docs/manifest.md +2 -1
- package/dist/docs/mcp.md +190 -0
- package/dist/docs/reporting.md +45 -1
- package/dist/docs/security.md +34 -8
- package/dist/docs/ziprack-1-authoring.md +2 -2
- package/dist/index.js.map +1 -1
- package/dist/ziprack-sdk.iife.js.map +1 -1
- package/package.json +1 -1
package/dist/db/index.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export interface ListQuery {
|
|
|
14
14
|
export interface RecordRow<T extends object = JsonObject> {
|
|
15
15
|
id: string;
|
|
16
16
|
data: T;
|
|
17
|
-
|
|
17
|
+
/** The participant who created it; null for records seeded from the package. */
|
|
18
|
+
owner_id: number | null;
|
|
18
19
|
created_at: string;
|
|
19
20
|
updated_at: string;
|
|
20
21
|
}
|
package/dist/docs/README.md
CHANGED
|
@@ -39,11 +39,15 @@ These docs cover both halves. If you are building an applet, start with
|
|
|
39
39
|
- [Security](security.md) — the sandbox, the CSP, the threats it does not cover
|
|
40
40
|
- [Publishing](publishing.md) — install, capability diff, review queue, promotion, kill switch
|
|
41
41
|
- [Reporting](reporting.md) — the admin API, CSV export, metrics
|
|
42
|
+
- [Who may do what](reporting.md#who-may-do-what) — `AppletAccess`, abilities, applet owners
|
|
42
43
|
- [Conformance](conformance.md) — proving a deployment is still a Rack
|
|
44
|
+
- [The Rack MCP Server](mcp.md) — agents that read applet data and upload packages, over OAuth or minted tokens
|
|
43
45
|
|
|
44
46
|
## Extending
|
|
45
47
|
|
|
46
48
|
- [Writing a Capability](extending.md) — the `Capability` interface, end to end
|
|
49
|
+
- [Contributing MCP tools](mcp.md#contributing-tools) — `ProvidesMcpTools`
|
|
50
|
+
- [Adding MCP tools from the Rack](mcp.md#adding-tools-from-the-rack) — `ToolRegistry`
|
|
47
51
|
|
|
48
52
|
---
|
|
49
53
|
|
package/dist/docs/activity.md
CHANGED
|
@@ -73,6 +73,10 @@ reads** — and fills `activity.type` from your manifest declaration. `progress(
|
|
|
73
73
|
Verbs match `^[a-z][a-z0-9_.:-]*$`. `occurred_at` defaults to now, in ISO 8601; pass it yourself only
|
|
74
74
|
when replaying something that genuinely happened earlier.
|
|
75
75
|
|
|
76
|
+
A statement emitted while staff [preview](attempts.md#preview-runs) the applet is stored with
|
|
77
|
+
`is_preview` set, with or without an attempt, so an audit trail can leave it out. The applet does
|
|
78
|
+
nothing different.
|
|
79
|
+
|
|
76
80
|
Read your own declarations when you need them:
|
|
77
81
|
|
|
78
82
|
```ts
|
package/dist/docs/attempts.md
CHANGED
|
@@ -110,7 +110,7 @@ GET /a/{slug}?preview=1 resume my open preview run
|
|
|
110
110
|
GET /a/{slug}?preview=new start a fresh one
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Both require
|
|
113
|
+
Both require `ziprack.manage-applet` for the applet. For anyone else the parameter is **ignored rather than
|
|
114
114
|
refused**, so a link pasted into a chat never dead-ends a participant on an error page.
|
|
115
115
|
|
|
116
116
|
A preview run is a real attempt — same pinning, same statements, same records, same AI budget — but
|
|
@@ -133,8 +133,11 @@ The live attempt keeps `attempt_number` 1 for the lifetime of the applet; previe
|
|
|
133
133
|
numbers above it. Starting a fresh preview never touches the live run, so you can hammer the preview
|
|
134
134
|
button on an applet you have already completed for real without disturbing your own record of it.
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
The preview comes from the launch, not the attempt, so it reaches capabilities that have no attempt
|
|
137
|
+
at all. Statements and `db@1` records written during a preview carry their own `is_preview`, and the
|
|
138
|
+
record browser hides them by default too (see [Reporting](reporting.md#preview-runs)).
|
|
139
|
+
|
|
140
|
+
What preview does **not** do is partition every capability's storage. Per-applet aggregates — [`ai@1`](ai.md) spend, the
|
|
138
141
|
[`storage@1`](storage.md) gallery — count preview activity like any other. Budget accordingly on a
|
|
139
142
|
Rack where people preview a lot.
|
|
140
143
|
|
package/dist/docs/database.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- [Queries](#queries)
|
|
8
8
|
- [Permissions](#permissions)
|
|
9
9
|
- [Quotas](#quotas)
|
|
10
|
+
- [Seeding](#seeding)
|
|
10
11
|
- [How It Is Stored](#how-it-is-stored)
|
|
11
12
|
|
|
12
13
|
<a name="introduction"></a>
|
|
@@ -53,6 +54,7 @@ uniform export that makes [statements](activity.md) valuable for a pile of untyp
|
|
|
53
54
|
| `indexed` | Optional, max 8 paths. Each must be a scalar property declared in the schema. Dotted paths allowed. |
|
|
54
55
|
| `quota` | Optional. `max_records`, `max_bytes` — ceilings you may lower, never raise. |
|
|
55
56
|
| `permissions` | Optional. `update` and `delete`: `owner` (default), `members`, `facilitator`. |
|
|
57
|
+
| `seed` | Optional, `applet` scope only. Path to a JSON file in the package. See [Seeding](#seeding). |
|
|
56
58
|
|
|
57
59
|
<a name="scopes"></a>
|
|
58
60
|
## Scopes
|
|
@@ -90,7 +92,7 @@ Every call resolves to an `AckResult`. A record comes back as:
|
|
|
90
92
|
interface RecordRow<T> {
|
|
91
93
|
id: string; // uuid
|
|
92
94
|
data: T;
|
|
93
|
-
owner_id: number;
|
|
95
|
+
owner_id: number | null; // the participant who created it; null when seeded
|
|
94
96
|
created_at: string;
|
|
95
97
|
updated_at: string;
|
|
96
98
|
}
|
|
@@ -156,20 +158,72 @@ Declared quotas are enforced server-side regardless of what the applet believes:
|
|
|
156
158
|
Over quota, `create()` returns `quota_exceeded`. Handle it — a shared board hitting its cap during a
|
|
157
159
|
live session is a real event, not a theoretical one.
|
|
158
160
|
|
|
161
|
+
<a name="seeding"></a>
|
|
162
|
+
## Seeding
|
|
163
|
+
|
|
164
|
+
An `applet` collection can ship its initial records in the package. This is how you bring along a
|
|
165
|
+
catalog, a question bank or any other data an existing app kept in a JSON file:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"name": "products",
|
|
170
|
+
"scope": "applet",
|
|
171
|
+
"schema": { "type": "object", "properties": { "sku": { "type": "string" }, "title": { "type": "string" } } },
|
|
172
|
+
"indexed": ["sku"],
|
|
173
|
+
"seed": "data/products.json"
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The seed file is a JSON array of records, each one a document exactly as `create()` would take it:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
[
|
|
181
|
+
{ "sku": "A-100", "title": "Starter kit" },
|
|
182
|
+
{ "sku": "A-200", "title": "Refill pack" }
|
|
183
|
+
]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The installer loads it when the version is installed, inside the same transaction, and holds every
|
|
187
|
+
record to the rules a write would face: it must be an object, it must match the schema, it must fit
|
|
188
|
+
`max_bytes`, and the file must fit `max_records`. One bad record refuses the whole package, and the
|
|
189
|
+
error names the record's index and the failing path. A package with a bad seed never installs.
|
|
190
|
+
|
|
191
|
+
Things to know:
|
|
192
|
+
|
|
193
|
+
- **Only `applet` scope.** No room, participant or attempt exists at install, so nothing else has
|
|
194
|
+
a scope to seed into.
|
|
195
|
+
- **Every version gets its own copy.** `applet` records belong to the version, so each version
|
|
196
|
+
loads its own seed file. Records written to the previous version stay with that version.
|
|
197
|
+
- **Seeded records have no owner.** `owner_id` is `null`, so under the default `owner` permission
|
|
198
|
+
participants can read them but cannot change them. Declare `members` or `facilitator` if they
|
|
199
|
+
should be editable.
|
|
200
|
+
- **No `RecordChanged` events.** Nobody is listening to a version that has not launched yet.
|
|
201
|
+
- **The quota still applies.** A seed is capped at 5000 records (`max_records_per_scope`), and so
|
|
202
|
+
is everything later written to that scope.
|
|
203
|
+
- **Not in the mock Rack.** The SDK's `MockRack` does not load seed files. In local development,
|
|
204
|
+
create the records from your dev harness.
|
|
205
|
+
|
|
159
206
|
<a name="how-it-is-stored"></a>
|
|
160
207
|
## How It Is Stored
|
|
161
208
|
|
|
162
209
|
Worth knowing, because it explains the constraints above.
|
|
163
210
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
211
|
+
Every collection's records live in one shared `ziprack_records` table, created by a migration and
|
|
212
|
+
keyed by collection and scope. The document lives in a JSON column; filters and ordering on an
|
|
213
|
+
`indexed` path read that path out of the JSON, typed from the declared schema so numbers compare as
|
|
214
|
+
numbers. Every query is narrowed by an index on `(collection_id, scope_id)` first, and the quota caps
|
|
215
|
+
a scope's records, so that read is always over a small, bounded set. The declared schema is still
|
|
216
|
+
the query plan: only `indexed` paths can be filtered or sorted, so there is no need for containment
|
|
217
|
+
queries over undeclared keys.
|
|
218
|
+
|
|
219
|
+
A record written while staff [preview](attempts.md#preview-runs) the applet is stored like any other,
|
|
220
|
+
with `is_preview` set when it is created. The flag never changes after that, so a shared board keeps
|
|
221
|
+
the flag of whoever wrote each card. Applets never see it; it exists for the admin, which leaves
|
|
222
|
+
preview records out unless asked.
|
|
223
|
+
|
|
224
|
+
**Installing a version never changes the database schema.** Adding a collection or an indexed path
|
|
225
|
+
only writes rows, so the install stays one transaction on every database. Collections that no
|
|
226
|
+
current manifest declares any more keep their records until you run:
|
|
173
227
|
|
|
174
228
|
```sh
|
|
175
229
|
php artisan ziprack:db:prune
|
package/dist/docs/extending.md
CHANGED
|
@@ -60,7 +60,7 @@ class SurveyCapability extends AbstractCapability
|
|
|
60
60
|
| `validateManifest()` | Cross-field checks; add errors to the validator. |
|
|
61
61
|
| `messageHandlers()` | `type => handler class`. |
|
|
62
62
|
| `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. |
|
|
63
|
+
| `onInstall()` / `onPromote()` / `onRetire()` | Version lifecycle hooks. `onInstall()` runs inside the install transaction, so row changes only. |
|
|
64
64
|
| `adminRoutes()` | Headless admin routes, registered only when `ziprack/admin` is installed. |
|
|
65
65
|
| `conformanceTests()` | Checks contributed to the conformance suite. |
|
|
66
66
|
| `statusChecks()` | Warnings for `ziprack:status` and `/ziprack/health`. |
|
|
@@ -204,14 +204,19 @@ untrusted frame: it is the one place where over-sharing is silent.
|
|
|
204
204
|
```php
|
|
205
205
|
public function onInstall(AppletVersion $version, ?AppletVersion $previous): void
|
|
206
206
|
{
|
|
207
|
-
$this->installer->install($version, $previous); //
|
|
207
|
+
$this->installer->install($version, $previous); // row changes only; no DDL
|
|
208
208
|
}
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
`onInstall()` runs inside the install transaction, with the previous current version for comparison
|
|
212
|
-
— which is what lets `db@1`
|
|
212
|
+
— which is what lets `db@1` register, update and orphan collections as they change between
|
|
213
213
|
versions. If it throws, the whole installation rolls back and extracted files are removed.
|
|
214
214
|
|
|
215
|
+
Keep it to row changes. `CREATE`, `ALTER` and `DROP` commit any open transaction on MySQL and
|
|
216
|
+
MariaDB, which breaks the install and lets the conformance suite's data escape its rollback. If a
|
|
217
|
+
capability needs tables, ship them as migrations; the conformance suite fails any check that commits
|
|
218
|
+
its transaction.
|
|
219
|
+
|
|
215
220
|
<a name="conformance-and-status"></a>
|
|
216
221
|
## Conformance and Status
|
|
217
222
|
|
|
@@ -85,6 +85,7 @@ ones you will actually touch.
|
|
|
85
85
|
| `asset_token_ttl_hours` | `6` | Lifetime of the signed package-asset token. |
|
|
86
86
|
| `session_ttl_hours` | `6` | Lifetime of a launch session. |
|
|
87
87
|
| `csp.img_hosts`, `csp.media_hosts` | `[]` | Per-deployment allowlist. A bare scheme is rejected. |
|
|
88
|
+
| `player.frame_allow` | `[]` | Features added to the applet iframe's `allow` list, e.g. `microphone`. Bare names from a fixed set only. |
|
|
88
89
|
| `media.providers` | `['vimeo','youtube']` | Drives `frame-src` and manifest validation. |
|
|
89
90
|
| `publishing.require_sbom` | `true` | Refuse packages with no SBOM. |
|
|
90
91
|
| `publishing.require_review_for_new_capabilities` | `true` | Block promotion until a widened `requires` is approved. |
|
|
@@ -102,7 +103,7 @@ ones you will actually touch.
|
|
|
102
103
|
| `ziprack-storage.php` | `disk`, `max_bytes_cap` (50 MB), `url_ttl_minutes` (10), `scanner` (`null` \| `clamav`) |
|
|
103
104
|
| `ziprack-ai.php` | `provider`, `models.analysis`, `models.chat`, `allowed_models`, the whole `budget` block |
|
|
104
105
|
| `ziprack-broadcast.php` | `join_prefix`, `facilitator_grace_seconds` (60), `presence_window_seconds` (45), `max_event_bytes` |
|
|
105
|
-
| `ziprack-admin.php` | `prefix` (`ziprack/admin`), `middleware` (`web`, `auth`, `can:ziprack.admin`) |
|
|
106
|
+
| `ziprack-admin.php` | `prefix` (`ziprack/admin`), `middleware` (`web`, `auth`, `can:ziprack.use-admin`) |
|
|
106
107
|
|
|
107
108
|
Ceilings in these files are **hard**: a manifest asking for a larger quota or budget fails
|
|
108
109
|
validation. Defaults apply when a manifest declares nothing.
|
package/dist/docs/manifest.md
CHANGED
|
@@ -72,7 +72,7 @@ declared is a validation error, and so is a section no installed capability owns
|
|
|
72
72
|
| `segments[]` | `attempt@1` | `{key, label, values[{key,label}]}` — dimensions the admin filters by |
|
|
73
73
|
| `activities[]` | `activity@1` | `{id, type, label, scale?, segment?, sequence?}` — required, at least one |
|
|
74
74
|
| `reporting.metrics[]` | `activity@1` (+ `attempt@1`) | `{key, label, pre, post}` — pairs two declared activities |
|
|
75
|
-
| `collections[]` | `db@1` | `{name, scope, schema, indexed?, quota?, permissions?}` |
|
|
75
|
+
| `collections[]` | `db@1` | `{name, scope, schema, indexed?, quota?, permissions?, seed?}` |
|
|
76
76
|
| `uploads[]` | `storage@1` | `{name, scope, mime[], max_bytes?, max_files?}` |
|
|
77
77
|
| `prompts[]` | `ai@1` | `{id, mode, template, vars?, output_schema?, budget?, model?}` |
|
|
78
78
|
| `broadcast` | `broadcast@1` | `{channels?, facilitator_events?}` |
|
|
@@ -204,6 +204,7 @@ one message. The checks that catch people out:
|
|
|
204
204
|
| `reporting.metrics needs the attempt capability` | Metrics are computed over completed attempts. |
|
|
205
205
|
| `A room-scoped collection requires the [broadcast] capability` | Scope implies capability. |
|
|
206
206
|
| `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
|
+
| `Only applet-scoped collections can be seeded` | A seed is shared reference data; see [Seeding](database.md#seeding). |
|
|
207
208
|
| `Placeholder {{pitch}} is not declared in vars.properties` | Prompt templates and their `vars` schema must agree. |
|
|
208
209
|
| `Analysis prompts need an output_schema of type object` | `mode: "analysis"` is structured output by definition. |
|
|
209
210
|
| `The $room channel is reserved for the Rack` | Pick another channel name. |
|
package/dist/docs/mcp.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
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 |
|
|
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
|
+
<a name="uploading-a-package"></a>
|
|
122
|
+
## Uploading a Package
|
|
123
|
+
|
|
124
|
+
MCP has no binary arguments, and an agent can't push a 50 MB zip through its context. Instead,
|
|
125
|
+
`create-upload` returns a signed, single-use URL with a ready-made `curl` command:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
curl -sS -H 'Accept: application/json' -F 'package=@dist-zip/my-applet-1.1.0.zip' '<upload_url>'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The response is the installed version, in the same shape as `POST /ziprack/admin/versions/upload`.
|
|
132
|
+
The rules in [Publishing](publishing.md) are unchanged.
|
|
133
|
+
|
|
134
|
+
- **Default.** A later version installs as not current.
|
|
135
|
+
- **With `activate: true`.** The version is promoted, unless it asks for capabilities that no admin
|
|
136
|
+
has approved yet. In that case the response carries a `warning` and the version waits in the
|
|
137
|
+
review queue.
|
|
138
|
+
- **Reuse.** A used or expired URL answers `410`. Call `create-upload` again.
|
|
139
|
+
|
|
140
|
+
<a name="the-scaffold-skill"></a>
|
|
141
|
+
## The Scaffold Skill
|
|
142
|
+
|
|
143
|
+
The canonical `ziprack-scaffold` skill lives in `packages/mcp/resources/skills/ziprack-scaffold/`.
|
|
144
|
+
|
|
145
|
+
- **Tool.** `get-scaffold-skill` returns every file of the skill, keyed by path.
|
|
146
|
+
- **Prompt.** `install-scaffold-skill` (with `agent: "claude"` or `"cursor"`) tells the agent to
|
|
147
|
+
write those files into `.claude/skills/` or `.cursor/skills/`.
|
|
148
|
+
|
|
149
|
+
The skill scaffolds an applet on `@ziprack/sdk`, and its last step publishes that applet through
|
|
150
|
+
`create-upload`.
|
|
151
|
+
|
|
152
|
+
<a name="contributing-tools"></a>
|
|
153
|
+
## Contributing Tools From a Capability
|
|
154
|
+
|
|
155
|
+
A capability contributes tools the same way it contributes admin routes:
|
|
156
|
+
|
|
157
|
+
1. Implement `ZipRack\Core\Capabilities\ProvidesMcpTools` on the capability class.
|
|
158
|
+
2. Return tool class names from `mcpTools()`.
|
|
159
|
+
3. Have each tool extend `ZipRack\Mcp\Tools\ZipRackTool`. It provides the helpers below.
|
|
160
|
+
|
|
161
|
+
Helpers on `ZipRackTool`:
|
|
162
|
+
|
|
163
|
+
| Helper | |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `applet()` | Resolves the `slug` argument among the applets the caller may see |
|
|
166
|
+
| `visibleApplets()` | A query of the applets the caller may see |
|
|
167
|
+
| `owned()` | Loads a model that belongs to that applet |
|
|
168
|
+
| `callAdmin()` | Runs your admin controller action in-process and returns its JSON |
|
|
169
|
+
| `audit()` | Writes an audit-log row for a write |
|
|
170
|
+
|
|
171
|
+
The tool classes only load when `ziprack/mcp` is installed, so list it under `suggest`, not
|
|
172
|
+
`require`.
|
|
173
|
+
|
|
174
|
+
A tool that only some users should have takes `#[RequiresAbility(Ability::ViewRack)]` (or any
|
|
175
|
+
other ability). Everyone else is neither offered it nor able to call it. Tools that take a slug
|
|
176
|
+
need nothing extra: `applet()` already sees only the caller's applets.
|
|
177
|
+
|
|
178
|
+
<a name="adding-tools-from-the-rack"></a>
|
|
179
|
+
## Adding Tools From the Rack
|
|
180
|
+
|
|
181
|
+
The Rack can add tools of its own, for what only it knows about, such as its roles or its
|
|
182
|
+
per-applet settings. Register them from a service provider:
|
|
183
|
+
|
|
184
|
+
```php
|
|
185
|
+
app(ZipRack\Mcp\ToolRegistry::class)->register(SetGuestAccess::class, ListMyApplets::class);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Rack tools extend `ZipRackTool` like any other, so they get the helpers above, the audit log and
|
|
189
|
+
`#[RequiresAbility]`. A Rack tool with the same name (`#[Name('list-applets')]`) as a zipRACK or
|
|
190
|
+
capability tool replaces it.
|
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)
|
|
@@ -46,7 +47,8 @@ render exactly those and nothing else.
|
|
|
46
47
|
## The Admin API
|
|
47
48
|
|
|
48
49
|
All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `web`, `auth` and the
|
|
49
|
-
`ziprack.admin` gate
|
|
50
|
+
`ziprack.use-admin` gate. Every route with an `{applet}`, including capability routes, also requires
|
|
51
|
+
`ziprack.manage-applet` for that applet. See [Who May Do What](#who-may-do-what).
|
|
50
52
|
|
|
51
53
|
| Route | Purpose |
|
|
52
54
|
|---|---|
|
|
@@ -72,6 +74,43 @@ Capability packages add their own, and only when installed:
|
|
|
72
74
|
| `GET applets/{applet}/rooms` · `POST rooms` · `GET rooms/{room}` · `POST rooms/{room}/status` · `GET rooms/{room}/events` | `broadcast` |
|
|
73
75
|
| `GET participants/{participant}/profile` · `PUT …` | `auth` |
|
|
74
76
|
|
|
77
|
+
<a name="who-may-do-what"></a>
|
|
78
|
+
## Who May Do What
|
|
79
|
+
|
|
80
|
+
zipRACK asks, and the Rack answers. Every check goes through one contract,
|
|
81
|
+
`ZipRack\Core\Contracts\AppletAccess`, with the applet in hand when there is one:
|
|
82
|
+
|
|
83
|
+
| Ability | Gate | Asked for |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `UseAdmin` | `ziprack.use-admin` | The admin API, the MCP server, personal MCP tokens |
|
|
86
|
+
| `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 |
|
|
88
|
+
| `ReviewVersions` | `ziprack.review` | `GET review-queue`, approving, promoting with `force` past review |
|
|
89
|
+
| `ViewRack` | `ziprack.view-rack` | `GET status`, `GET audit-log`, every participant, the `rack-status` tool |
|
|
90
|
+
|
|
91
|
+
`scopeVisible()` narrows the applet list, the MCP tools' slug lookups and `list-applets` to what
|
|
92
|
+
the user may see. An applet they may not see reads as not installed.
|
|
93
|
+
|
|
94
|
+
The default binding, `AdminOnlyAccess`, answers every ability with the `ziprack.admin` gate
|
|
95
|
+
(`$user->is_admin`), so a Rack that binds nothing, or only redefines that gate, behaves as before.
|
|
96
|
+
A Rack with other roles binds its own:
|
|
97
|
+
|
|
98
|
+
```php
|
|
99
|
+
$this->app->singleton(AppletAccess::class, RackAppletAccess::class);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Applets have an **owner**, a morph like a version's publisher: whoever published the first
|
|
103
|
+
version, or created the empty applet. `$applet->isOwnedBy($user)` is the usual basis for
|
|
104
|
+
`ManageApplet`. A package whose slug already belongs to an applet the publisher cannot manage is
|
|
105
|
+
refused, so an upload cannot land in someone else's applet.
|
|
106
|
+
|
|
107
|
+
**Participants** are Rack-wide. A user with `ViewRack` sees all of them; anyone else sees the
|
|
108
|
+
participants with audit-log entries on an applet they can see, through `GET participants` and the
|
|
109
|
+
`auth` profile routes. `ZipRack\Core\Access\ParticipantVisibility` applies the rule.
|
|
110
|
+
|
|
111
|
+
To react to publishing, listen for `ZipRack\Core\Events\VersionInstalled` (with the publisher),
|
|
112
|
+
`VersionApproved` and `VersionPromoted`.
|
|
113
|
+
|
|
75
114
|
<a name="preview-runs"></a>
|
|
76
115
|
## Preview Runs
|
|
77
116
|
|
|
@@ -87,6 +126,11 @@ returns **live attempts only** unless told otherwise:
|
|
|
87
126
|
An unrecognised value falls back to `live` rather than erroring, so a stale bookmark cannot quietly
|
|
88
127
|
turn preview data into a report.
|
|
89
128
|
|
|
129
|
+
The `db@1` record browser (`…/collections` and `…/collections/{name}/records`) takes the same
|
|
130
|
+
`?preview=` with the same default, and the collection `records` counts follow it. `ziprack_statements`
|
|
131
|
+
and `ziprack_records` both carry an `is_preview` column set from the launch session, so preview data
|
|
132
|
+
is saved, never mixed in, and easy to delete in one query when you want it gone.
|
|
133
|
+
|
|
90
134
|
<a name="the-csv-export"></a>
|
|
91
135
|
## The CSV Export
|
|
92
136
|
|
package/dist/docs/security.md
CHANGED
|
@@ -23,7 +23,7 @@ lean on the second where the first is what is needed.
|
|
|
23
23
|
|
|
24
24
|
```html
|
|
25
25
|
<iframe sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
26
|
-
allow="autoplay; fullscreen; picture-in-picture; encrypted-media"
|
|
26
|
+
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write"
|
|
27
27
|
src="/ziprack/pkg/{token}/index.html"></iframe>
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -39,6 +39,26 @@ Communication is `postMessage` only, and the host honours a message solely when
|
|
|
39
39
|
the applet's `contentWindow`. Origin is never compared, because an opaque origin makes comparison
|
|
40
40
|
meaningless — a detail worth understanding before anyone "fixes" it.
|
|
41
41
|
|
|
42
|
+
### Extending the frame's permissions
|
|
43
|
+
|
|
44
|
+
The `allow` list above is the fixed base. A deployment may add features an applet needs, such as
|
|
45
|
+
`microphone` for one that records audio:
|
|
46
|
+
|
|
47
|
+
```php
|
|
48
|
+
'player' => [
|
|
49
|
+
'frame_allow' => ['microphone'], // ZIPRACK_PLAYER_FRAME_ALLOW=microphone
|
|
50
|
+
],
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Only bare feature names from `FramePermissions::GRANTABLE` are accepted: `microphone`, `camera`,
|
|
54
|
+
`display-capture`, `geolocation`, `clipboard-read`, `screen-wake-lock`, `web-share` and `midi`. An
|
|
55
|
+
origin list (`*`, `microphone *`, `microphone 'self'`) or an unknown name is dropped, so every
|
|
56
|
+
feature keeps the default `'src'` allowlist and reaches the applet's frame alone.
|
|
57
|
+
`FramePermissions::violations()` reports refused entries, and `ziprack:status` fails on them.
|
|
58
|
+
|
|
59
|
+
The setting is Rack-wide: every applet on the Rack is offered the feature, and the browser still
|
|
60
|
+
prompts the participant before granting it.
|
|
61
|
+
|
|
42
62
|
<a name="the-content-security-policy"></a>
|
|
43
63
|
## The Content Security Policy
|
|
44
64
|
|
|
@@ -47,16 +67,22 @@ add approved hosts, never a bare scheme.
|
|
|
47
67
|
|
|
48
68
|
| Directive | Value |
|
|
49
69
|
|---|---|
|
|
50
|
-
| `default-src` | `'self'` |
|
|
51
|
-
| `script-src` | `'self' 'unsafe-inline' 'unsafe-eval' blob:` |
|
|
52
|
-
| `style-src` | `'self' 'unsafe-inline'` + configured font CDNs |
|
|
53
|
-
| `font-src` | `'self' data:` + configured font CDNs |
|
|
54
|
-
| `img-src` | `'self' data: blob:` + deployment allowlist |
|
|
55
|
-
| `media-src` | `'self' blob:` + deployment allowlist |
|
|
56
|
-
| `connect-src` | `'self'` |
|
|
70
|
+
| `default-src` | `'self'` + Rack origin |
|
|
71
|
+
| `script-src` | `'self'` + Rack origin, `'unsafe-inline' 'unsafe-eval' blob:` |
|
|
72
|
+
| `style-src` | `'self'` + Rack origin, `'unsafe-inline'` + configured font CDNs |
|
|
73
|
+
| `font-src` | `'self'` + Rack origin, `data:` + configured font CDNs |
|
|
74
|
+
| `img-src` | `'self'` + Rack origin, `data: blob:` + deployment allowlist |
|
|
75
|
+
| `media-src` | `'self'` + Rack origin, `blob:` + deployment allowlist |
|
|
76
|
+
| `connect-src` | `'self'` + Rack origin |
|
|
57
77
|
| `frame-src` | the allowlisted media providers' player hosts, else `'none'` |
|
|
58
78
|
| `frame-ancestors`, `base-uri`, `form-action` | `'self'` |
|
|
59
79
|
|
|
80
|
+
**Rack origin** is the exact origin of `app.url` (scheme, host, non-default port), e.g.
|
|
81
|
+
`https://rack.example.com`. It is listed because the applet frame's origin is opaque: Chrome resolves
|
|
82
|
+
`'self'` against the URL the document came from, but Safari resolves it against the opaque origin and
|
|
83
|
+
matches nothing, blocking the applet's own bundle. `APP_URL` must therefore be the origin the Rack is
|
|
84
|
+
actually served from.
|
|
85
|
+
|
|
60
86
|
**Why `img-src` matters more than it looks.** A hostile applet can exfiltrate anything it holds with
|
|
61
87
|
a single tag:
|
|
62
88
|
|
|
@@ -154,10 +154,10 @@ Rack all fail. `window.open` works. The entry document's CSP is:
|
|
|
154
154
|
|
|
155
155
|
| Directive | Allowed |
|
|
156
156
|
|---|---|
|
|
157
|
-
| `script-src` | `'self' 'unsafe-inline' 'unsafe-eval' blob:` — bundle every dependency |
|
|
157
|
+
| `script-src` | `'self'` + the Rack's origin, `'unsafe-inline' 'unsafe-eval' blob:` — bundle every dependency |
|
|
158
158
|
| `style-src`, `font-src` | `'self'` + Bunny/Google fonts |
|
|
159
159
|
| `img-src`, `media-src` | `'self' data: blob:` + the deployment's allowlist. **Not `https:`.** |
|
|
160
|
-
| `connect-src` | `'self'` — no external APIs, no analytics |
|
|
160
|
+
| `connect-src` | `'self'` + the Rack's origin — no external APIs, no analytics |
|
|
161
161
|
| `frame-src` | Vimeo and YouTube players, via declared `media` only |
|
|
162
162
|
|
|
163
163
|
Ship your images and media inside the zip. A persistent Rack bar sits above the frame and cannot be
|