@ziprack/sdk 0.1.1 → 0.2.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/broadcast/index.d.ts +8 -1
- package/dist/core/mock/MockRack.d.ts +6 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/docs/README.md +52 -0
- package/dist/docs/activity.md +140 -0
- package/dist/docs/ai.md +159 -0
- package/dist/docs/applet-packages.md +118 -0
- package/dist/docs/attempts.md +184 -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 +176 -0
- package/dist/docs/extending.md +256 -0
- package/dist/docs/installation.md +185 -0
- package/dist/docs/introduction.md +124 -0
- package/dist/docs/manifest.md +209 -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 +146 -0
- package/dist/docs/sdk.md +258 -0
- package/dist/docs/security.md +127 -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 +11 -2
- 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/{store-DSV5q7C7.js → store-BXGon4iJ.js} +15 -5
- package/dist/store-BXGon4iJ.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 +10 -4
- package/dist/store-DSV5q7C7.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Link } from '../core/link';
|
|
2
|
-
import type { AckResult, JsonObject, LaunchRoom, RackEvent, RoomMember, RoomRole, RoomStatus } from '../core/types';
|
|
2
|
+
import type { AckResult, JsonObject, LaunchRoom, RackEvent, RoomInvite, RoomMember, RoomRole, RoomStatus } from '../core/types';
|
|
3
3
|
export declare const ROOM_CHANNEL = "$room";
|
|
4
4
|
export type RoomEventName = 'presence' | 'state' | 'role' | 'status';
|
|
5
5
|
export interface RoomEvents {
|
|
@@ -68,6 +68,13 @@ export declare class BroadcastApi {
|
|
|
68
68
|
/** Locally known state (launch snapshot plus pushed updates). */
|
|
69
69
|
snapshot: () => JsonObject;
|
|
70
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Where the next person scans to get in. Only the Rack knows its own host
|
|
73
|
+
* and join prefix, so it hands back the code and the URL; rendering them as
|
|
74
|
+
* a slide, a link or a QR is yours. Refused with `conflict` once the room is
|
|
75
|
+
* locked or closed, so a stale QR can be taken down.
|
|
76
|
+
*/
|
|
77
|
+
invite(): Promise<AckResult<RoomInvite>>;
|
|
71
78
|
/** Claim the facilitator role (opt_in rooms). */
|
|
72
79
|
claim(): Promise<AckResult<{
|
|
73
80
|
role: RoomRole;
|
|
@@ -131,6 +131,12 @@ export declare class MockRack {
|
|
|
131
131
|
/** An earlier facilitator wins; a later optimistic one steps down. */
|
|
132
132
|
private reconcileFacilitator;
|
|
133
133
|
private onRoomMessage;
|
|
134
|
+
/**
|
|
135
|
+
* A mock room is two tabs of the dev server, so the invite is this page:
|
|
136
|
+
* scan it and the second tab joins over BroadcastChannel, exactly as the
|
|
137
|
+
* real join URL would.
|
|
138
|
+
*/
|
|
139
|
+
private invite;
|
|
134
140
|
private setRole;
|
|
135
141
|
private publish;
|
|
136
142
|
private stateSet;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# zipRACK Documentation
|
|
2
|
+
|
|
3
|
+
zipRACK is a micro app framework: a Laravel host — **the Rack** — that runs sandboxed static web
|
|
4
|
+
apps — **applets** — and services their capability requests over `postMessage`. An applet owns
|
|
5
|
+
everything the participant sees. The Rack owns login, persistence, AI access, real-time coordination
|
|
6
|
+
and reporting.
|
|
7
|
+
|
|
8
|
+
These docs cover both halves. If you are building an applet, start with
|
|
9
|
+
[Applet Packages](applet-packages.md). If you are standing up or maintaining a Rack, start with
|
|
10
|
+
[Installation](installation.md).
|
|
11
|
+
|
|
12
|
+
## Prologue
|
|
13
|
+
|
|
14
|
+
- [Introduction](introduction.md) — the trust boundary, the core model, the objects
|
|
15
|
+
- [Versioning](versioning.md) — the three version axes and why they never couple
|
|
16
|
+
|
|
17
|
+
## Building an applet
|
|
18
|
+
|
|
19
|
+
- [Applet Packages](applet-packages.md) — the zip, the installer rules, the starter
|
|
20
|
+
- [The Manifest](manifest.md) — every field, every rule the validator enforces
|
|
21
|
+
- [The JavaScript SDK](sdk.md) — `Rack.init()`, acks, errors, mock mode, testing
|
|
22
|
+
- [zipRACK Studio](studio.md) — the visual builder, the derived manifest, the MCP server
|
|
23
|
+
- [The Bridge Protocol](protocol.md) — the envelope, for anyone not using the SDK
|
|
24
|
+
|
|
25
|
+
## Capabilities
|
|
26
|
+
|
|
27
|
+
- [Authentication](auth.md) — `auth@1`
|
|
28
|
+
- [Attempts](attempts.md) — `attempt@1`
|
|
29
|
+
- [Activity](activity.md) — `activity@1`
|
|
30
|
+
- [Database](database.md) — `db@1`
|
|
31
|
+
- [Storage](storage.md) — `storage@1`
|
|
32
|
+
- [AI](ai.md) — `ai@1`
|
|
33
|
+
- [Broadcast](broadcast.md) — `broadcast@1`
|
|
34
|
+
- [Media](media.md) — declared embeds, no capability required
|
|
35
|
+
|
|
36
|
+
## Running a Rack
|
|
37
|
+
|
|
38
|
+
- [Installation](installation.md) — composing a Rack from capability packages
|
|
39
|
+
- [Security](security.md) — the sandbox, the CSP, the threats it does not cover
|
|
40
|
+
- [Publishing](publishing.md) — install, capability diff, review queue, promotion, kill switch
|
|
41
|
+
- [Reporting](reporting.md) — the admin API, CSV export, metrics
|
|
42
|
+
- [Conformance](conformance.md) — proving a deployment is still a Rack
|
|
43
|
+
|
|
44
|
+
## Extending
|
|
45
|
+
|
|
46
|
+
- [Writing a Capability](extending.md) — the `Capability` interface, end to end
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
Also in this repository: [`ziprack-1-authoring.md`](ziprack-1-authoring.md), a one-page condensed
|
|
51
|
+
brief suitable for dropping into an applet project as `CLAUDE.md`. The same content, expanded, is
|
|
52
|
+
published to the CN IDEAS skills library as an instruction set.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Activity — `activity@1`
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Declaring Activities](#declaring-activities)
|
|
5
|
+
- [Emitting Statements](#emitting-statements)
|
|
6
|
+
- [Activity Types](#activity-types)
|
|
7
|
+
- [Metrics](#metrics)
|
|
8
|
+
- [Designing for the Export](#designing-for-the-export)
|
|
9
|
+
|
|
10
|
+
<a name="introduction"></a>
|
|
11
|
+
## Introduction
|
|
12
|
+
|
|
13
|
+
A **statement** is an append-only record that *an actor did a verb on an activity with a result*.
|
|
14
|
+
It is the audit trail, and because every statement has the same shape, it is also the one export
|
|
15
|
+
that works identically for every applet on every Rack.
|
|
16
|
+
|
|
17
|
+
Statements are never updated and never deleted. Re-answering is normal: reporting reads the **last**
|
|
18
|
+
`answered` statement per activity per attempt.
|
|
19
|
+
|
|
20
|
+
`db@1` does not replace this. Statements are what happened; [records](database.md) are what is
|
|
21
|
+
currently true.
|
|
22
|
+
|
|
23
|
+
<a name="declaring-activities"></a>
|
|
24
|
+
## Declaring Activities
|
|
25
|
+
|
|
26
|
+
Every question you report on must be declared. At least one activity is required.
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
"activities": [
|
|
30
|
+
{ "id": "intro.connection", "type": "rating", "label": "How connected do you feel to patients?",
|
|
31
|
+
"scale": { "min": 1, "max": 6 }, "sequence": 1 },
|
|
32
|
+
{ "id": "rd.corine.reaction", "type": "text", "label": "What stood out to you about Corine's story?",
|
|
33
|
+
"segment": "track", "sequence": 2 }
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Field | Rules |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `id` | Required, max 150, unique. **This is the data contract.** |
|
|
40
|
+
| `type` | Required, max 100. Drives how the admin renders and exports the value. |
|
|
41
|
+
| `label` | Required, max 1000. Becomes the CSV header and the analysis heading. |
|
|
42
|
+
| `scale` | Optional object, for `rating` and `scale` types. |
|
|
43
|
+
| `segment` | Optional. Must name a declared [segment](attempts.md#segments) key, or be `null` for shared. |
|
|
44
|
+
| `sequence` | Optional integer ≥ 0. Controls export column order; defaults to array position. |
|
|
45
|
+
|
|
46
|
+
Use stable, dotted, lowercase ids: `{phase}.{topic}.{question}`. An undeclared id still records, but
|
|
47
|
+
it exports unlabeled and appended after every declared column — useful as a safety net, never as a
|
|
48
|
+
plan.
|
|
49
|
+
|
|
50
|
+
<a name="emitting-statements"></a>
|
|
51
|
+
## Emitting Statements
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// The common case: a committed answer.
|
|
55
|
+
await Rack.activity.answer('intro.connection', 5);
|
|
56
|
+
|
|
57
|
+
// Navigation and progress. Fire and forget.
|
|
58
|
+
void Rack.activity.progress('rd.chapter_2');
|
|
59
|
+
|
|
60
|
+
// Anything else.
|
|
61
|
+
void Rack.activity.statement({
|
|
62
|
+
verb: 'viewed',
|
|
63
|
+
activity: { id: 'rd.corine.video', type: 'media' },
|
|
64
|
+
result: { duration_seconds: 184 },
|
|
65
|
+
context: { autoplay: false },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`answer()` emits `verb: 'answered'` with the value in `result.response` — **the only field reporting
|
|
70
|
+
reads** — and fills `activity.type` from your manifest declaration. `progress()` emits
|
|
71
|
+
`verb: 'progressed'` with `activity.type: 'step'`.
|
|
72
|
+
|
|
73
|
+
Verbs match `^[a-z][a-z0-9_.:-]*$`. `occurred_at` defaults to now, in ISO 8601; pass it yourself only
|
|
74
|
+
when replaying something that genuinely happened earlier.
|
|
75
|
+
|
|
76
|
+
Read your own declarations when you need them:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
Rack.activity.activities(); // LaunchActivity[]
|
|
80
|
+
Rack.activity.activity('intro.connection'); // { id, type, label, scale, … } | null
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Rendering a question from its declared `label` and `scale` keeps the participant's wording and the
|
|
84
|
+
CSV header identical by construction — a small discipline that pays for itself the first time
|
|
85
|
+
someone edits a question.
|
|
86
|
+
|
|
87
|
+
<a name="activity-types"></a>
|
|
88
|
+
## Activity Types
|
|
89
|
+
|
|
90
|
+
`type` is free text, but the admin understands these:
|
|
91
|
+
|
|
92
|
+
| `type` | `result.response` | Used for |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `text` | string | Free text. Feeds admin AI response analysis. |
|
|
95
|
+
| `rating` | number within `scale` | Likert or self-rating. Eligible for metrics. |
|
|
96
|
+
| `scale` | number within `scale` | Semantic alias of `rating`. |
|
|
97
|
+
| `choice` | string key, or array of keys | Single or multi-select. |
|
|
98
|
+
| `custom` | any JSON | Anything else. Exported JSON-encoded. |
|
|
99
|
+
|
|
100
|
+
<a name="metrics"></a>
|
|
101
|
+
## Metrics
|
|
102
|
+
|
|
103
|
+
A metric pairs a pre and a post rating of the same thing, and gives the admin a shift card rather
|
|
104
|
+
than two columns to eyeball.
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
"reporting": {
|
|
108
|
+
"metrics": [
|
|
109
|
+
{ "key": "connection", "label": "Connection",
|
|
110
|
+
"pre": "intro.connection", "post": "outro.connection" }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`pre` and `post` must be declared activity ids, and metrics require `attempt@1`, because they are
|
|
116
|
+
computed over completed attempts. Both activities should use the same `scale`, or the shift means
|
|
117
|
+
nothing.
|
|
118
|
+
|
|
119
|
+
<a name="designing-for-the-export"></a>
|
|
120
|
+
## Designing for the Export
|
|
121
|
+
|
|
122
|
+
The CSV an admin downloads is:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
ID, Name, Email, {first segment label}, Status, Started At, Completed At,
|
|
126
|
+
{declared activities in sequence order, headed by label},
|
|
127
|
+
{undeclared activity ids, appended}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Each cell is the last `result.response` for that activity in that attempt; non-scalars are
|
|
131
|
+
JSON-encoded. Which yields three rules worth writing on the wall:
|
|
132
|
+
|
|
133
|
+
1. **`id` is the data contract.** Renaming one between versions splits a column: old attempts keep
|
|
134
|
+
the old column, new attempts fill a new one.
|
|
135
|
+
2. **`label` is the header.** Renaming one is free, and changes only how the column reads.
|
|
136
|
+
3. **`sequence` is the column order.** Set it deliberately; the export reads top to bottom the way
|
|
137
|
+
the experience does.
|
|
138
|
+
|
|
139
|
+
Write labels as full sentences, exactly as the participant sees them. An admin reading a
|
|
140
|
+
spreadsheet six months later has no other copy of the question.
|
package/dist/docs/ai.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AI — `ai@1`
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Declaring Prompts](#declaring-prompts)
|
|
5
|
+
- [Analysis](#analysis)
|
|
6
|
+
- [Chat](#chat)
|
|
7
|
+
- [Budgets](#budgets)
|
|
8
|
+
- [Untrusted Variables](#untrusted-variables)
|
|
9
|
+
- [Not This Capability](#not-this-capability)
|
|
10
|
+
|
|
11
|
+
<a name="introduction"></a>
|
|
12
|
+
## Introduction
|
|
13
|
+
|
|
14
|
+
**The applet never sees a key and never sends a prompt.** It calls a prompt template the manifest
|
|
15
|
+
declared and the Rack stores, supplying *variables*, never instructions.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const { data } = await Rack.ai.ask('score_pitch', { pitch, track });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Four things fall out of that design at once:
|
|
22
|
+
|
|
23
|
+
- Prompt injection *from the applet* is structurally impossible — there is no instruction channel.
|
|
24
|
+
- Model, system prompt and output schema are Rack-owned, so they can be tuned or swapped without
|
|
25
|
+
republishing the applet.
|
|
26
|
+
- Cost is governable: hard caps per prompt, per attempt, per participant, per applet, enforced
|
|
27
|
+
server-side.
|
|
28
|
+
- **No CSP change is required.** The call travels as a `postMessage`, not a `fetch`.
|
|
29
|
+
|
|
30
|
+
<a name="declaring-prompts"></a>
|
|
31
|
+
## Declaring Prompts
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"prompts": [
|
|
35
|
+
{
|
|
36
|
+
"id": "score_pitch",
|
|
37
|
+
"mode": "analysis",
|
|
38
|
+
"template": "Score this team pitch for clarity and specificity.\n\nPitch: {{pitch}}\nTrack: {{track}}",
|
|
39
|
+
"vars": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": { "pitch": { "type": "string" }, "track": { "type": "string" } },
|
|
42
|
+
"required": ["pitch"]
|
|
43
|
+
},
|
|
44
|
+
"output_schema": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"clarity": { "type": "integer", "minimum": 1, "maximum": 5 },
|
|
48
|
+
"feedback": { "type": "string" }
|
|
49
|
+
},
|
|
50
|
+
"required": ["clarity", "feedback"]
|
|
51
|
+
},
|
|
52
|
+
"budget": { "max_calls_per_attempt": 3, "max_output_tokens": 400 }
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Field | Rules |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `id` | Required. `^[a-z][a-z0-9_.-]*$`, max 100, unique. |
|
|
60
|
+
| `mode` | Required. `analysis` or `chat`. |
|
|
61
|
+
| `template` | Required, max 20 000. `{{placeholder}}` for each variable. |
|
|
62
|
+
| `vars` | Optional JSON Schema (`type: object`). **If present, every placeholder must be declared in it.** |
|
|
63
|
+
| `output_schema` | Required for `analysis`, `type: object`. |
|
|
64
|
+
| `budget` | Optional. `max_calls_per_attempt`, `max_calls_per_participant`, `max_output_tokens`. |
|
|
65
|
+
| `model` | Optional hint. Honoured only if the deployment allowlists it. |
|
|
66
|
+
|
|
67
|
+
<a name="analysis"></a>
|
|
68
|
+
## Analysis
|
|
69
|
+
|
|
70
|
+
Structured output, conforming to the declared `output_schema`. For scoring a response, summarising
|
|
71
|
+
back to the participant, or branching on what someone wrote.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
interface Score { clarity: number; feedback: string }
|
|
75
|
+
|
|
76
|
+
const result = await Rack.ai.ask<Score>('score_pitch', { pitch, track: 'rd' });
|
|
77
|
+
|
|
78
|
+
if (! result.ok) {
|
|
79
|
+
return result.error === 'budget_exceeded'
|
|
80
|
+
? showGenericFeedback()
|
|
81
|
+
: showRetry();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render(result.data.output.feedback, result.data.output.clarity);
|
|
85
|
+
result.data.usage; // { input_tokens, output_tokens }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The Rack validates the model's output against your schema before it acks, so `output` either matches
|
|
89
|
+
the schema or the call fails. You do not need defensive parsing — you do need a path for the failure
|
|
90
|
+
case, because a model can be unavailable.
|
|
91
|
+
|
|
92
|
+
Always write the non-AI fallback. An applet whose experience stalls when a call fails has made an
|
|
93
|
+
LLM into a dependency the framework was designed to keep optional.
|
|
94
|
+
|
|
95
|
+
<a name="chat"></a>
|
|
96
|
+
## Chat
|
|
97
|
+
|
|
98
|
+
Conversational, with history held by the Rack and scoped to the attempt or room. The applet never
|
|
99
|
+
holds the transcript.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const chat = Rack.ai.chat('coach');
|
|
103
|
+
|
|
104
|
+
const reply = await chat.send(message, (delta, soFar) => {
|
|
105
|
+
bubble.textContent = soFar; // streams as it arrives
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (reply.ok) {
|
|
109
|
+
bubble.textContent = reply.data.text;
|
|
110
|
+
reply.data.message_id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await chat.reset(); // start a fresh conversation
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Streaming works through chunked acks: partials carry `delta`, the terminal ack carries the full
|
|
117
|
+
`text` and `usage`. Each chunk resets the ack timeout, and the whole call allows 60 s.
|
|
118
|
+
|
|
119
|
+
Messages cap at 4000 characters and history at 40 messages (`ziprack-ai.max_message_length`,
|
|
120
|
+
`history_messages`); older turns fall off rather than failing the call.
|
|
121
|
+
|
|
122
|
+
<a name="budgets"></a>
|
|
123
|
+
## Budgets
|
|
124
|
+
|
|
125
|
+
Every layer is enforced server-side, and the applet cannot raise any of them:
|
|
126
|
+
|
|
127
|
+
| Limit | Declared in | Default | Ceiling |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| Calls per attempt | `budget.max_calls_per_attempt` | 20 | — |
|
|
130
|
+
| Calls per participant | `budget.max_calls_per_participant` | 200 | — |
|
|
131
|
+
| Output tokens | `budget.max_output_tokens` | 800 | 2000 (`ziprack-ai.budget.max_output_tokens_cap`) |
|
|
132
|
+
| Calls per applet per day | deployment | 5000 | — |
|
|
133
|
+
| Calls per Rack per month | deployment | 100 000 | — |
|
|
134
|
+
| Variable payload | deployment | 16 KB | — |
|
|
135
|
+
|
|
136
|
+
Exceeding any of them returns `budget_exceeded`. Declare the budget your experience actually needs:
|
|
137
|
+
a lower cap is a smaller incident, and the number is visible to whoever reviews the capability diff.
|
|
138
|
+
|
|
139
|
+
Spend is reported per prompt in the admin API (`/ziprack/admin/applets/{applet}/ai/spend`).
|
|
140
|
+
|
|
141
|
+
<a name="untrusted-variables"></a>
|
|
142
|
+
## Untrusted Variables
|
|
143
|
+
|
|
144
|
+
Variables still reach a model. Assume a participant will type adversarial text into any field you
|
|
145
|
+
pass through, and design the template so that it would not matter:
|
|
146
|
+
|
|
147
|
+
- Put participant text **after** the instructions, clearly delimited, and say what it is.
|
|
148
|
+
- Never ask the model to follow instructions found inside a variable.
|
|
149
|
+
- Prefer `analysis` with a tight `output_schema` over free-form text you will parse yourself — a
|
|
150
|
+
schema-constrained answer is a much smaller blast radius.
|
|
151
|
+
- Show model output as model output. Do not render it as if it were Rack-authored guidance.
|
|
152
|
+
|
|
153
|
+
<a name="not-this-capability"></a>
|
|
154
|
+
## Not This Capability
|
|
155
|
+
|
|
156
|
+
The admin console's **AI Response Analysis** — summaries of free-text answers across attempts — is a
|
|
157
|
+
Rack feature operating on collected [statements](activity.md). It works for any applet declaring
|
|
158
|
+
`activity@1`, whether or not that applet uses `ai@1` at all, and it is configured and budgeted
|
|
159
|
+
separately. See [Reporting](reporting.md).
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Applet Packages
|
|
2
|
+
|
|
3
|
+
- [The Zip](#the-zip)
|
|
4
|
+
- [Installer Rules](#installer-rules)
|
|
5
|
+
- [The Starter](#the-starter)
|
|
6
|
+
- [Relative URLs](#relative-urls)
|
|
7
|
+
- [The SBOM](#the-sbom)
|
|
8
|
+
- [Building and Packaging](#building-and-packaging)
|
|
9
|
+
|
|
10
|
+
<a name="the-zip"></a>
|
|
11
|
+
## The Zip
|
|
12
|
+
|
|
13
|
+
An applet ships as a single zip, named for the applet and the version it contains:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{slug}-{version}.zip
|
|
17
|
+
├── manifest.json required, at the zip root
|
|
18
|
+
├── sbom.json required — CycloneDX, or package-lock.json
|
|
19
|
+
├── index.html the entry (configurable via manifest.entry)
|
|
20
|
+
└── assets/… everything the entry references, by RELATIVE path
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Nothing else is special. The Rack extracts the archive, keeps the file tree intact, and serves it
|
|
24
|
+
through a token-prefixed path. There is no build step on the server, no server-side templating, no
|
|
25
|
+
framework to satisfy — whatever your bundler emits is what runs.
|
|
26
|
+
|
|
27
|
+
<a name="installer-rules"></a>
|
|
28
|
+
## Installer Rules
|
|
29
|
+
|
|
30
|
+
`PackageInstaller` is security code, identical on every Rack, and it refuses the whole upload rather
|
|
31
|
+
than skipping a bad file:
|
|
32
|
+
|
|
33
|
+
- **Static extensions only.** `html js mjs css map json wasm txt vtt svg png jpg jpeg gif webp ico
|
|
34
|
+
woff woff2 ttf mp3 mp4 wav webm ogg pdf`. A `.php` in your `dist/` rejects the package.
|
|
35
|
+
- **No `..`, no absolute paths, no symlinks, no NUL bytes.** Dotfiles and `__MACOSX/` are skipped
|
|
36
|
+
silently, so a zip built on macOS is fine.
|
|
37
|
+
- **50 MB by default** (`ziprack.max_upload_mb`).
|
|
38
|
+
- **`manifest.entry` must exist** in the zip and end in `.html`.
|
|
39
|
+
- **The slug must match** the applet you are uploading to, and it is immutable once created.
|
|
40
|
+
- **A `version` string uploads once per applet.** Bump it every time, including during testing.
|
|
41
|
+
|
|
42
|
+
Installation runs inside a transaction: the version row is created, every declared capability's
|
|
43
|
+
`onInstall()` hook runs — this is where `db@1` creates the collection tables and their generated
|
|
44
|
+
columns — and only then are files extracted. A failure anywhere rolls the whole thing back and
|
|
45
|
+
deletes the extracted directory.
|
|
46
|
+
|
|
47
|
+
<a name="the-starter"></a>
|
|
48
|
+
## The Starter
|
|
49
|
+
|
|
50
|
+
`starter/` is a minimal, working `ziprack/1` applet: plain Vite and TypeScript, `@ziprack/sdk`, one
|
|
51
|
+
question, one statement, one completion.
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm install
|
|
55
|
+
npm run dev # mock mode: the whole experience runs standalone
|
|
56
|
+
npm run package # dist-zip/{slug}-{version}.zip, ready to upload
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Edit `public/manifest.json` first — `slug`, `name`, `version`, and the capabilities you intend to
|
|
60
|
+
call. Everything else follows from there.
|
|
61
|
+
|
|
62
|
+
<a name="relative-urls"></a>
|
|
63
|
+
## Relative URLs
|
|
64
|
+
|
|
65
|
+
The Rack serves your package from `/ziprack/pkg/{token}/…`, where `{token}` is a signed, expiring
|
|
66
|
+
value. A root-relative URL like `/assets/app.js` therefore 404s in production while working
|
|
67
|
+
perfectly on your dev server — the single most common packaging mistake.
|
|
68
|
+
|
|
69
|
+
With Vite, one line prevents it:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export default defineConfig({
|
|
73
|
+
base: './', // required
|
|
74
|
+
build: { outDir: 'dist', emptyOutDir: true },
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The same applies to anything you hand-write: `./assets/logo.svg`, never `/assets/logo.svg`.
|
|
79
|
+
|
|
80
|
+
<a name="the-sbom"></a>
|
|
81
|
+
## The SBOM
|
|
82
|
+
|
|
83
|
+
Every package carries a `sbom.json` (CycloneDX) or a `package-lock.json` at its root. An applet is
|
|
84
|
+
*built output*, so there is nothing to audit unless the lockfile comes with it — the same reason
|
|
85
|
+
`npm audit` reads a lockfile rather than code.
|
|
86
|
+
|
|
87
|
+
On install the Rack extracts the purl list and batches it against OSV.dev. Findings land in the
|
|
88
|
+
[review queue](publishing.md) rather than blocking the upload.
|
|
89
|
+
|
|
90
|
+
Be clear about the limit: the SBOM is self-attested. A hostile publisher can ship an accurate
|
|
91
|
+
lockfile alongside a tampered bundle. This catches negligence, not malice — which is still worth
|
|
92
|
+
having, because negligence is the common case.
|
|
93
|
+
|
|
94
|
+
<a name="building-and-packaging"></a>
|
|
95
|
+
## Building and Packaging
|
|
96
|
+
|
|
97
|
+
`npm run package` in the starter does the whole job:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// scripts/package.js — abbreviated
|
|
101
|
+
const manifest = JSON.parse(readFileSync('dist/manifest.json', 'utf8'));
|
|
102
|
+
|
|
103
|
+
execSync('npm sbom --sbom-format cyclonedx --omit dev', { cwd: lockDir }) // → dist/sbom.json
|
|
104
|
+
execSync(`zip -r -X "${zipPath}" . -x ".*" -x "__MACOSX*"`, { cwd: 'dist' });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Two details worth keeping if you write your own: `-X` drops extra file attributes, and excluding
|
|
108
|
+
dotfiles keeps `.DS_Store` out of an archive the installer would otherwise skip anyway.
|
|
109
|
+
|
|
110
|
+
Upload the result through the admin API, or from the deployment:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
php artisan ziprack:install dist-zip/my-applet-1.0.0.zip
|
|
114
|
+
php artisan ziprack:promote my-applet 1.0.0
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Uploads land **unpublished**. Promotion is always a separate, deliberate action — see
|
|
118
|
+
[Publishing](publishing.md).
|