@ziprack/sdk 0.1.2 → 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 +56 -0
- package/dist/docs/activity.md +144 -0
- package/dist/docs/ai.md +159 -0
- package/dist/docs/applet-packages.md +118 -0
- package/dist/docs/attempts.md +187 -0
- package/dist/docs/auth.md +106 -0
- package/dist/docs/broadcast.md +226 -0
- package/dist/docs/conformance.md +102 -0
- package/dist/docs/database.md +230 -0
- package/dist/docs/extending.md +261 -0
- package/dist/docs/installation.md +186 -0
- package/dist/docs/introduction.md +124 -0
- package/dist/docs/manifest.md +210 -0
- package/dist/docs/mcp.md +190 -0
- package/dist/docs/media.md +85 -0
- package/dist/docs/protocol.md +121 -0
- package/dist/docs/publishing.md +127 -0
- package/dist/docs/reporting.md +190 -0
- package/dist/docs/sdk.md +258 -0
- package/dist/docs/security.md +153 -0
- package/dist/docs/storage.md +131 -0
- package/dist/docs/studio.md +425 -0
- package/dist/docs/versioning.md +101 -0
- package/dist/docs/ziprack-1-authoring.md +182 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +3 -0
- package/dist/mcp/docs.d.ts +31 -0
- package/dist/mcp/docs.js +107 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/rpc.d.ts +33 -0
- package/dist/mcp/rpc.js +86 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +55 -0
- package/dist/mcp/tools.d.ts +20 -0
- package/dist/mcp/tools.js +106 -0
- package/dist/ziprack-sdk.iife.js.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Publishing
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Installing a Version](#installing-a-version)
|
|
5
|
+
- [Capability Diff Review](#capability-diff-review)
|
|
6
|
+
- [Promotion](#promotion)
|
|
7
|
+
- [The Review Queue](#the-review-queue)
|
|
8
|
+
- [Retiring a Version](#retiring-a-version)
|
|
9
|
+
- [The Audit Log](#the-audit-log)
|
|
10
|
+
|
|
11
|
+
<a name="introduction"></a>
|
|
12
|
+
## Introduction
|
|
13
|
+
|
|
14
|
+
`cn-sim/1` assumed CN authored every package. Accepting client-authored applets changes the problem
|
|
15
|
+
from untrusted *content* to untrusted *code*, and that calls for process rather than more sandboxing.
|
|
16
|
+
|
|
17
|
+
The pipeline is deliberately boring: every upload lands inactive, a human promotes it, and anything
|
|
18
|
+
that widens what the applet can reach needs an explicit approval first.
|
|
19
|
+
|
|
20
|
+
<a name="installing-a-version"></a>
|
|
21
|
+
## Installing a Version
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
php artisan ziprack:install dist-zip/my-applet-1.0.0.zip
|
|
25
|
+
php artisan ziprack:install dist-zip/my-applet-1.0.0.zip --activate # install and promote
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
or `POST /ziprack/admin/versions/upload` with the zip.
|
|
29
|
+
|
|
30
|
+
Installation validates the manifest against this Rack's capability set, records the version, runs
|
|
31
|
+
each declared capability's `onInstall()` hook inside a transaction, and only then extracts files. A
|
|
32
|
+
version arrives as `ready` but **not current**, unless it is the applet's first — there is nothing
|
|
33
|
+
to review a first version against, so it is promoted automatically.
|
|
34
|
+
|
|
35
|
+
Once files are extracted the Rack resolves declared media (the oEmbed lookup) and, when
|
|
36
|
+
`publishing.osv_lookup` is on, queues a batch of the SBOM's purls against OSV.dev. Findings attach
|
|
37
|
+
to the version and surface in the review queue; they do not block the install.
|
|
38
|
+
|
|
39
|
+
<a name="capability-diff-review"></a>
|
|
40
|
+
## Capability Diff Review
|
|
41
|
+
|
|
42
|
+
**This is the control that actually holds.** When a new version of an existing applet is uploaded,
|
|
43
|
+
the Rack diffs its `requires` block against the current version:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"added": ["ai@1", "db@1"],
|
|
48
|
+
"removed": [],
|
|
49
|
+
"profile_added": ["business_unit"],
|
|
50
|
+
"profile_removed": [],
|
|
51
|
+
"pinning_changed": true,
|
|
52
|
+
"pinned": true
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Any addition — a capability or a profile attribute — blocks promotion until someone approves it.
|
|
57
|
+
This is the Android permissions model, and it catches the realistic attack: a benign v1 that earns
|
|
58
|
+
trust, followed by a v2 that quietly widens its reach.
|
|
59
|
+
|
|
60
|
+
`pinning_changed` is flagged just as prominently, because adding or removing `attempt@1` changes the
|
|
61
|
+
applet's own update semantics from rolling to pinned or back. See
|
|
62
|
+
[Version Pinning](attempts.md#version-pinning).
|
|
63
|
+
|
|
64
|
+
Removals need no approval. Narrowing what an applet can reach is always allowed.
|
|
65
|
+
|
|
66
|
+
<a name="promotion"></a>
|
|
67
|
+
## Promotion
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
php artisan ziprack:promote my-applet 1.1.0
|
|
71
|
+
php artisan ziprack:promote my-applet 1.1.0 --force # approve the pending review, then promote
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
or `POST /ziprack/admin/applets/{applet}/versions/{version}/approve` followed by
|
|
75
|
+
`…/promote`.
|
|
76
|
+
|
|
77
|
+
Promotion is refused when the version is not `ready`, or when it has an unapproved capability
|
|
78
|
+
addition — the error names exactly what was added. On success, `current_version_id` moves and each
|
|
79
|
+
capability's `onPromote()` hook runs.
|
|
80
|
+
|
|
81
|
+
What promotion means depends on pinning:
|
|
82
|
+
|
|
83
|
+
- **Rolling applet** (no `attempt@1`): everyone is on the new version at their next page load.
|
|
84
|
+
- **Pinned applet** (`attempt@1`): new attempts start on the new version; in-progress attempts stay
|
|
85
|
+
on the one they started on until they complete. Two versions run side by side until the old one
|
|
86
|
+
drains.
|
|
87
|
+
|
|
88
|
+
<a name="the-review-queue"></a>
|
|
89
|
+
## The Review Queue
|
|
90
|
+
|
|
91
|
+
`GET /ziprack/admin/review-queue` collects everything awaiting a human decision rather than hiding
|
|
92
|
+
it behind a modal:
|
|
93
|
+
|
|
94
|
+
- Versions requesting new capabilities or profile attributes
|
|
95
|
+
- OSV findings from the SBOM
|
|
96
|
+
- The first upload from a publisher
|
|
97
|
+
- Manifest validation warnings that were overridden
|
|
98
|
+
|
|
99
|
+
A version's own page (`GET /ziprack/admin/applets/{applet}/versions/{version}`) carries the diff,
|
|
100
|
+
the SBOM summary, the findings and the publisher.
|
|
101
|
+
|
|
102
|
+
Each version records an attributable publisher, so a bad version can be traced and that publisher's
|
|
103
|
+
other applets reviewed.
|
|
104
|
+
|
|
105
|
+
<a name="retiring-a-version"></a>
|
|
106
|
+
## Retiring a Version
|
|
107
|
+
|
|
108
|
+
The kill switch:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
php artisan ziprack:retire my-applet 1.1.0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A retired version can no longer be launched. In-progress attempts pinned to it land on a holding
|
|
115
|
+
screen — a defined state with an explanation, not a blank frame. Retiring the current version leaves
|
|
116
|
+
the applet with no current version until another is promoted, which is deliberate: the operator
|
|
117
|
+
decides what to fall back to.
|
|
118
|
+
|
|
119
|
+
<a name="the-audit-log"></a>
|
|
120
|
+
## The Audit Log
|
|
121
|
+
|
|
122
|
+
With `ziprack.audit.enabled` on, capability calls are recorded per applet — type, participant,
|
|
123
|
+
outcome — excluding the high-volume types each capability already persists (`activity:statement`,
|
|
124
|
+
`attempt:state`, `broadcast:heartbeat`). Read it at `GET /ziprack/admin/audit-log`.
|
|
125
|
+
|
|
126
|
+
This is what an incident response actually needs: what the applet asked for, when, and what the Rack
|
|
127
|
+
answered. AI spend and quota usage are reported alongside it per applet.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Reporting
|
|
2
|
+
|
|
3
|
+
- [Composition](#composition)
|
|
4
|
+
- [The Admin API](#the-admin-api)
|
|
5
|
+
- [Who May Do What](#who-may-do-what)
|
|
6
|
+
- [Preview Runs](#preview-runs)
|
|
7
|
+
- [The CSV Export](#the-csv-export)
|
|
8
|
+
- [Metrics](#metrics)
|
|
9
|
+
- [Response Analysis](#response-analysis)
|
|
10
|
+
- [Building a UI](#building-a-ui)
|
|
11
|
+
|
|
12
|
+
<a name="composition"></a>
|
|
13
|
+
## Composition
|
|
14
|
+
|
|
15
|
+
`ziprack/admin` is **headless**: JSON endpoints, export and report services, no pages. The
|
|
16
|
+
deployment skeleton owns the UI, and replacing it entirely is expected — the API is the contract,
|
|
17
|
+
the pages are not.
|
|
18
|
+
|
|
19
|
+
The admin composes from declared capabilities rather than assuming every applet is a simulation.
|
|
20
|
+
Ask a Rack what to render:
|
|
21
|
+
|
|
22
|
+
```http
|
|
23
|
+
GET /ziprack/admin/applets/{applet}/panels
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"declared": ["activity@1", "attempt@1", "auth@1"],
|
|
29
|
+
"panels": ["activity", "attempt", "auth"],
|
|
30
|
+
"implemented": ["activity@1", "ai@1", "attempt@1", "auth@1", "broadcast@1", "db@1", "storage@1"]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`panels` is the intersection of what the current version declares and what this Rack implements —
|
|
35
|
+
render exactly those and nothing else.
|
|
36
|
+
|
|
37
|
+
| Declared | Renders |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `attempt@1` | Attempts started and completed, completion rate, metric cards |
|
|
40
|
+
| `activity@1` | Statement log per attempt; CSV export |
|
|
41
|
+
| `db@1` | Record browser per collection, from the declared JSON Schema |
|
|
42
|
+
| `storage@1` | Uploaded artifact gallery, scoped and permissioned |
|
|
43
|
+
| `ai@1` | Per-prompt spend and call volume |
|
|
44
|
+
| `broadcast@1` | Room history, participation by role, facilitator timeline |
|
|
45
|
+
|
|
46
|
+
<a name="the-admin-api"></a>
|
|
47
|
+
## The Admin API
|
|
48
|
+
|
|
49
|
+
All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `web`, `auth` and the
|
|
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).
|
|
52
|
+
|
|
53
|
+
| Route | Purpose |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `GET status` | Format, core version, capabilities, warnings |
|
|
56
|
+
| `GET applets` · `POST applets` | List and create |
|
|
57
|
+
| `GET applets/{applet}` · `PATCH applets/{applet}` | Show and update |
|
|
58
|
+
| `GET applets/{applet}/panels` | What to render |
|
|
59
|
+
| `POST versions/upload` · `POST applets/{applet}/versions` | Install a package |
|
|
60
|
+
| `GET applets/{applet}/versions/{version}` | Manifest, requires diff, SBOM, OSV findings, publisher |
|
|
61
|
+
| `POST …/{version}/approve` · `…/promote` · `…/retire` | The publishing actions |
|
|
62
|
+
| `GET review-queue` | Everything awaiting a decision |
|
|
63
|
+
| `GET participants` · `GET audit-log` | People and capability calls |
|
|
64
|
+
|
|
65
|
+
Capability packages add their own, and only when installed:
|
|
66
|
+
|
|
67
|
+
| Route | Capability |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `GET applets/{applet}/overview` · `…/attempts` · `…/attempts/{attempt}` | `attempt` |
|
|
70
|
+
| `GET applets/{applet}/attempts/{attempt}/statements` · `…/export.csv` · `…/metrics` | `activity` |
|
|
71
|
+
| `GET applets/{applet}/collections` · `…/collections/{name}/records` | `db` |
|
|
72
|
+
| `GET applets/{applet}/files` | `storage` |
|
|
73
|
+
| `GET applets/{applet}/ai/spend` · `…/ai/calls` | `ai` |
|
|
74
|
+
| `GET applets/{applet}/rooms` · `POST rooms` · `GET rooms/{room}` · `POST rooms/{room}/status` · `GET rooms/{room}/events` | `broadcast` |
|
|
75
|
+
| `GET participants/{participant}/profile` · `PUT …` | `auth` |
|
|
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
|
+
|
|
114
|
+
<a name="preview-runs"></a>
|
|
115
|
+
## Preview Runs
|
|
116
|
+
|
|
117
|
+
Every attempt query above — the table, the overview counts, the metric cards and the CSV export —
|
|
118
|
+
returns **live attempts only** unless told otherwise:
|
|
119
|
+
|
|
120
|
+
| `?preview=` | Returns |
|
|
121
|
+
|---|---|
|
|
122
|
+
| *(absent)* · `live` | Real runs. The default, and what a report should almost always show. |
|
|
123
|
+
| `preview` | Staff [preview runs](attempts.md#preview-runs) only |
|
|
124
|
+
| `all` | Both, with `is_preview` on each row to tell them apart |
|
|
125
|
+
|
|
126
|
+
An unrecognised value falls back to `live` rather than erroring, so a stale bookmark cannot quietly
|
|
127
|
+
turn preview data into a report.
|
|
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
|
+
|
|
134
|
+
<a name="the-csv-export"></a>
|
|
135
|
+
## The CSV Export
|
|
136
|
+
|
|
137
|
+
`GET /ziprack/admin/applets/{applet}/export.csv` produces one row per attempt:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
ID, Name, Email, {first segment label}, Status, Started At, Completed At,
|
|
141
|
+
{declared activities in sequence order, headed by label},
|
|
142
|
+
{undeclared activity ids that appeared, appended}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each activity cell is the **last** `result.response` for that activity in that attempt; non-scalar
|
|
146
|
+
values are JSON-encoded, and duplicate labels are disambiguated so the header row stays usable.
|
|
147
|
+
|
|
148
|
+
This is the one artifact every applet declaring `activity@1` produces identically, which is what
|
|
149
|
+
makes it worth protecting. The authoring consequences are covered in
|
|
150
|
+
[Designing for the Export](activity.md#designing-for-the-export) and are unchanged from `cn-sim/1`:
|
|
151
|
+
`id` is the data contract, `label` is the header, renaming either has consequences.
|
|
152
|
+
|
|
153
|
+
<a name="metrics"></a>
|
|
154
|
+
## Metrics
|
|
155
|
+
|
|
156
|
+
`GET /ziprack/admin/applets/{applet}/metrics` computes each declared
|
|
157
|
+
[metric](activity.md#metrics) over completed attempts: the pre mean, the post mean and the shift,
|
|
158
|
+
sliceable by the primary segment. Metrics need `attempt@1` as well as `activity@1`, which the
|
|
159
|
+
manifest validator enforces at upload rather than leaving to discover at report time.
|
|
160
|
+
|
|
161
|
+
<a name="response-analysis"></a>
|
|
162
|
+
## Response Analysis
|
|
163
|
+
|
|
164
|
+
The admin can summarise free-text answers across attempts:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
php artisan ziprack:response-analysis --applet=my-applet
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```http
|
|
171
|
+
GET /ziprack/admin/applets/{applet}/response-analysis
|
|
172
|
+
POST /ziprack/admin/applets/{applet}/response-analysis/refresh
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Summaries regenerate only for applets whose answers changed; schedule the command every few hours.
|
|
176
|
+
|
|
177
|
+
**This is a Rack feature, not an applet capability.** It operates on collected statements from any
|
|
178
|
+
applet declaring `activity@1`, whether or not that applet uses [`ai@1`](ai.md). The two are budgeted
|
|
179
|
+
and configured separately, and conflating them is the mistake this note exists to prevent.
|
|
180
|
+
|
|
181
|
+
<a name="building-a-ui"></a>
|
|
182
|
+
## Building a UI
|
|
183
|
+
|
|
184
|
+
The skeleton ships a reference admin over these endpoints. When you replace it:
|
|
185
|
+
|
|
186
|
+
1. Call `/panels` first and render from the answer. Hard-coding panels breaks the moment a Rack runs
|
|
187
|
+
a different capability set.
|
|
188
|
+
2. Render `db@1` record tables from the collection's declared JSON Schema — the schema is there
|
|
189
|
+
precisely so the UI does not need per-applet knowledge.
|
|
190
|
+
3. Treat the review queue as the landing page. It is where the decisions are.
|
package/dist/docs/sdk.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# The JavaScript SDK
|
|
2
|
+
|
|
3
|
+
- [Installation](#installation)
|
|
4
|
+
- [Booting](#booting)
|
|
5
|
+
- [Launch Data](#launch-data)
|
|
6
|
+
- [Acks and Errors](#acks-and-errors)
|
|
7
|
+
- [Events](#events)
|
|
8
|
+
- [Mock Mode](#mock-mode)
|
|
9
|
+
- [Testing](#testing)
|
|
10
|
+
- [Leaving the Applet](#leaving-the-applet)
|
|
11
|
+
- [The MCP Server](#the-mcp-server)
|
|
12
|
+
|
|
13
|
+
<a name="installation"></a>
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install @ziprack/sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Rack } from '@ziprack/sdk';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`Rack` is a singleton — one per frame, which is what an applet always is. `createRack()` returns a
|
|
25
|
+
fresh client for tests or unusual multi-frame setups.
|
|
26
|
+
|
|
27
|
+
A Rack also serves an IIFE build at `/ziprack/assets/sdk.js` that exposes `window.Rack` (and
|
|
28
|
+
`window.ZipRack`), for applets with no build step. It is the same code; prefer the npm package when you have a bundler, because mock
|
|
29
|
+
mode and the test host come with it.
|
|
30
|
+
|
|
31
|
+
<a name="booting"></a>
|
|
32
|
+
## Booting
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const launch = await Rack.init({ slug: 'my-applet' });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`init()` announces `rack:ready` to the parent, waits for `rack:launch`, and resolves with the launch
|
|
39
|
+
payload. It is safe to call repeatedly: subsequent calls return the same promise. Calls made before
|
|
40
|
+
the handshake completes are queued, not dropped, so you never have to gate your UI on initialisation
|
|
41
|
+
order.
|
|
42
|
+
|
|
43
|
+
| Option | Default | Purpose |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `slug` | — | Labels the mock session and locates `./manifest.json` when standalone. |
|
|
46
|
+
| `launchTimeoutMs` | `3000` | How long to wait for `rack:launch` before falling back to mock mode. |
|
|
47
|
+
| `ackTimeoutMs` | `5000` | Per-call ack timeout. Streamed calls reset it per chunk. |
|
|
48
|
+
| `manifestUrl` | `./manifest.json` | Where mock mode reads your manifest. |
|
|
49
|
+
| `transport` | — | Inject a transport (tests). Skips the handshake and mock detection. |
|
|
50
|
+
|
|
51
|
+
`Rack.mode` is `'pending'`, `'host'` or `'mock'`. `Rack.launched` tells you whether launch data has
|
|
52
|
+
arrived.
|
|
53
|
+
|
|
54
|
+
<a name="launch-data"></a>
|
|
55
|
+
## Launch Data
|
|
56
|
+
|
|
57
|
+
Everything the Rack is willing to tell you up front arrives in one payload — there is no second
|
|
58
|
+
round trip to fetch the participant, the attempt or the manifest.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface LaunchData {
|
|
62
|
+
schema: 'ziprack/1';
|
|
63
|
+
actor: { id: number; name: string };
|
|
64
|
+
applet: { slug: string; name: string; version: string };
|
|
65
|
+
capabilities: string[]; // as declared, e.g. ['auth@1', 'db@1']
|
|
66
|
+
media: Record<string, MediaEmbed>; // Rack-built player URLs by declared id
|
|
67
|
+
|
|
68
|
+
auth?: { user: LaunchUser };
|
|
69
|
+
attempt?: { id, status, state, segment };
|
|
70
|
+
activity?: { activities: LaunchActivity[] };
|
|
71
|
+
db?: { collections: Array<{ name, scope }> };
|
|
72
|
+
storage?: { uploads: Array<{ name, scope, mime, max_bytes }> };
|
|
73
|
+
ai?: { prompts: Array<{ id, mode }> };
|
|
74
|
+
broadcast?: { room, role, members, state, channels };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Each capability contributes its own key, and only when declared. The typed accessors read from the
|
|
79
|
+
same payload, so `Rack.auth.user()`, `Rack.attempt.state()` and `Rack.activity.activities()` are
|
|
80
|
+
synchronous and free.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
Rack.on('launch', (data) => render(data)); // fires immediately if already launched
|
|
84
|
+
Rack.declares('broadcast'); // did the manifest declare it?
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
<a name="acks-and-errors"></a>
|
|
88
|
+
## Acks and Errors
|
|
89
|
+
|
|
90
|
+
**Every SDK call resolves to an `AckResult` and never throws.** There is no `try`/`catch` ceremony
|
|
91
|
+
around a capability call; there is a branch on `ok`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
interface AckResult<T> {
|
|
95
|
+
ok: boolean;
|
|
96
|
+
data?: T;
|
|
97
|
+
error?: AckErrorCode;
|
|
98
|
+
message?: string; // human-readable, safe to log, not to show
|
|
99
|
+
details?: object; // validation errors, keyed by field
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const result = await Rack.db.collection('ideas').create({ title, status: 'draft' });
|
|
105
|
+
|
|
106
|
+
if (! result.ok) {
|
|
107
|
+
if (result.error === 'quota_exceeded') return showFull();
|
|
108
|
+
return showRetry();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const idea = result.data.record;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Code | Means |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `timeout` | No ack within the timeout. The shell retries the batch once; this is what you see after that. |
|
|
117
|
+
| `disposed` | The client was torn down while the call was in flight. |
|
|
118
|
+
| `persist_failed` | The Rack could not process the message. Already reported server-side. |
|
|
119
|
+
| `undeclared_capability` | The manifest does not declare it — a packaging bug, not a runtime condition. |
|
|
120
|
+
| `unknown_type` | Malformed or unimplemented message type. |
|
|
121
|
+
| `validation` | Payload failed the handler's rules or a declared schema. See `details`. |
|
|
122
|
+
| `forbidden` | Permitted by the manifest, refused for this participant (not the owner, not the facilitator). |
|
|
123
|
+
| `completed` | The attempt is finished; state, segment and attempt-scoped records are frozen. |
|
|
124
|
+
| `quota_exceeded` | Collection full, upload slot full, file too large. |
|
|
125
|
+
| `budget_exceeded` | An `ai@1` budget was reached. |
|
|
126
|
+
| `not_found` | Unknown record, file, channel or collection. |
|
|
127
|
+
| `conflict` | A concurrent write lost. |
|
|
128
|
+
|
|
129
|
+
**Do not block the participant on an ack.** The shell batches messages on a 300 ms window (up to 50
|
|
130
|
+
per request) and coalesces `attempt:state` so only the latest value is sent. Fire and forget for
|
|
131
|
+
progress and state; `await` only what you must confirm — typically the last answer before
|
|
132
|
+
`complete()`.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
void Rack.attempt.setState({ step: 4 }); // fire and forget
|
|
136
|
+
await Rack.activity.answer('outro.confidence', 5); // confirm, then:
|
|
137
|
+
await Rack.attempt.complete();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
<a name="events"></a>
|
|
141
|
+
## Events
|
|
142
|
+
|
|
143
|
+
The Rack pushes events into the frame — room broadcasts, presence changes, role grants. Subscribe
|
|
144
|
+
through the capability that owns them (see [Broadcast](broadcast.md)), or read the raw stream:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
Rack.on('event', ({ event, payload, channel, from }) => {
|
|
148
|
+
console.log(event, payload, channel, from?.participant_id);
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
<a name="mock-mode"></a>
|
|
153
|
+
## Mock Mode
|
|
154
|
+
|
|
155
|
+
Run `npm run dev` and there is no Rack, so the SDK becomes one. Mock mode reads your
|
|
156
|
+
`./manifest.json` and services every capability locally — which means declared capabilities,
|
|
157
|
+
activities, collections, prompts and uploads behave exactly as they will in production, including
|
|
158
|
+
refusing calls the manifest did not declare.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
[ziprack] running in mock mode (not embedded in a Rack)
|
|
162
|
+
[ziprack mock] activity:statement { verb: 'answered', … }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| Surface | Mock behaviour |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `attempt` | State and status persist in `localStorage` under `ziprack-mock:{slug}:*`. Refresh resumes. |
|
|
168
|
+
| `activity` | Statements are logged and kept in memory. |
|
|
169
|
+
| `db` | Records persist in `localStorage`, validated against the declared schema. |
|
|
170
|
+
| `storage` | Files are held as blob URLs and marked clean. |
|
|
171
|
+
| `ai` | Returns a stub conforming to `output_schema` — or your own handler. |
|
|
172
|
+
| `broadcast` | Uses `BroadcastChannel`, so **two tabs of your dev server form a room**. |
|
|
173
|
+
|
|
174
|
+
Two hooks exist only in mock mode:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
Rack.mock.ai('score_pitch', (vars) => ({ clarity: 4, feedback: `Nice work on "${vars.pitch}"` }));
|
|
178
|
+
Rack.mock.resetAttempt(); // replay a completed attempt
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Mock mode also engages if a real Rack fails to answer `rack:launch` within `launchTimeoutMs`. That
|
|
182
|
+
is a deliberate development convenience, and the console says so — if you see it in production,
|
|
183
|
+
something is wrong with the shell, not with your applet.
|
|
184
|
+
|
|
185
|
+
<a name="testing"></a>
|
|
186
|
+
## Testing
|
|
187
|
+
|
|
188
|
+
`@ziprack/sdk/testing` ships an in-memory Rack you can assert against.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { createRack } from '@ziprack/sdk';
|
|
192
|
+
import { FakeHost } from '@ziprack/sdk/testing';
|
|
193
|
+
import manifest from '../public/manifest.json';
|
|
194
|
+
|
|
195
|
+
it('records the answer and completes exactly once', async () => {
|
|
196
|
+
const host = new FakeHost(manifest);
|
|
197
|
+
const rack = createRack();
|
|
198
|
+
await rack.init({ transport: host.transport() });
|
|
199
|
+
|
|
200
|
+
await rack.activity.answer('intro.confidence', 5);
|
|
201
|
+
await rack.attempt.complete();
|
|
202
|
+
await rack.attempt.complete(); // idempotent
|
|
203
|
+
|
|
204
|
+
expect(host.answers()).toEqual({ 'intro.confidence': 5 });
|
|
205
|
+
expect(host.attempt().status).toBe('completed');
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Member | Gives you |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `host.transport()` | Pass to `rack.init({ transport })`. |
|
|
212
|
+
| `host.statements` | Every statement the applet emitted, in order. |
|
|
213
|
+
| `host.answers()` | The last `answered` value per activity — what reporting would export. |
|
|
214
|
+
| `host.attempt()`, `host.records(name)`, `host.files()` | Persisted state. |
|
|
215
|
+
| `host.push(event)` | Push a `rack:event` in, as the Rack would, to drive broadcast handlers. |
|
|
216
|
+
|
|
217
|
+
<a name="leaving-the-applet"></a>
|
|
218
|
+
## Leaving the Applet
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
Rack.eject();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Asks the Rack to navigate away — back to the applet list, or wherever the deployment sends people.
|
|
225
|
+
It needs no capability and takes no ack. In mock mode it logs what would have happened.
|
|
226
|
+
|
|
227
|
+
<a name="the-mcp-server"></a>
|
|
228
|
+
## The MCP Server
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
npx ziprack-mcp
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The SDK ships a stdio MCP server that gives an AI assistant these docs and the applet authoring
|
|
235
|
+
guidelines. The docs are bundled into the package when it is published, so what the assistant reads
|
|
236
|
+
always matches the SDK version in your `node_modules`, not whatever it remembers of an older one. The
|
|
237
|
+
starter already includes the configuration in `.mcp.json`:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"mcpServers": {
|
|
242
|
+
"ziprack": {
|
|
243
|
+
"command": "npx",
|
|
244
|
+
"args": ["ziprack-mcp"]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
| Tool | Purpose |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `ziprack_guidelines` | The rules every applet follows (`ziprack-1-authoring.md`). Read before writing applet code. |
|
|
253
|
+
| `ziprack_list_docs` | Every doc in reading order, with its section anchors. |
|
|
254
|
+
| `ziprack_search_docs` | The best-matching sections, in full, for one or more phrasings. |
|
|
255
|
+
| `ziprack_read_doc` | One doc, or one section of it by anchor. |
|
|
256
|
+
|
|
257
|
+
Every answer names the SDK version it came from. `ziprack-studio mcp` includes the same four tools,
|
|
258
|
+
so a Studio project needs only the one server.
|