@ryuhq/sdk 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuhq/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Ryu developer SDK: typed builders and CLI for authoring manifest.json Plugin bundles",
6
6
  "main": "./dist/index.cjs",
@@ -44,11 +44,11 @@
44
44
  "clean": "rm -rf dist"
45
45
  },
46
46
  "dependencies": {
47
- "@ryuhq/sdk-native": "workspace:*",
47
+ "@ryuhq/sdk-native": "0.1.4",
48
48
  "zod": "^4.1.13"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/bun": "catalog:",
51
+ "@types/bun": "^1.3.4",
52
52
  "json-schema-to-typescript": "^15.0.4",
53
53
  "tsup": "^8.5.1",
54
54
  "typescript": "^5"
@@ -226,8 +226,48 @@ export interface PluginManifest {
226
226
  * Permission grants this app declares it needs (e.g. `"mcp:web_search"`).
227
227
  * These are *declarations only* at this layer — no enforcement happens here;
228
228
  * the Gateway owns grant enforcement.
229
+ *
230
+ * This is the **app→host** lane and has nothing to do with
231
+ * [`permission_levels`], the **app→human** lane. See that field's doc comment
232
+ * for the three-way table; conflating the two is the likeliest future bug here.
233
+ *
234
+ * [`permission_levels`]: PluginManifest::permission_levels
229
235
  */
230
236
  permission_grants?: string[];
237
+ /**
238
+ * **The user-facing permission vocabulary this app declares** — the set of
239
+ * levels ("read", "edit", …) an administrator can later grant to a person or a
240
+ * team *inside* this app. Absent/empty = the app declares no vocabulary, which
241
+ * is every manifest predating this field.
242
+ *
243
+ * Spaces declaring `read` and `edit` is what makes "team X may edit in Spaces"
244
+ * expressible at all: a grant has to name a level, and a UI has to render a
245
+ * list of them. Without a declaration there is nothing to bind to.
246
+ *
247
+ * # Three lanes, one prefix — do not conflate them
248
+ *
249
+ * | field | direction | who decides | what it means |
250
+ * |---|---|---|---|
251
+ * | [`permission_grants`] | app → host | the **Gateway**, at install/enable | which host capabilities the app may *ask* for |
252
+ * | [`permissions`] ([`PermissionSet`]) | app → sandbox | **Core**, at spawn/exec | what the app's code may *touch* (FS paths, hosts, subprocess) |
253
+ * | `permission_levels` | app → human | an **admin**, per person/team | what a *person* may do inside the app |
254
+ *
255
+ * Only the first two are enforced today. This field is **declaration only**:
256
+ * nothing consumes it yet, so declaring `edit` gates nothing by itself. It is
257
+ * the vocabulary the ACL layer will bind grants against.
258
+ *
259
+ * # Ordering and implication
260
+ *
261
+ * Declaration order is display order — render the list as written. Strength is
262
+ * expressed with [`PermissionLevel::implies`] rather than a separate rank, so
263
+ * there is exactly one ordering and it cannot contradict itself: `edit` implying
264
+ * `read` means granting `edit` already conveys `read`, and no admin has to grant
265
+ * the same person both.
266
+ *
267
+ * [`permission_grants`]: PluginManifest::permission_grants
268
+ * [`permissions`]: PluginManifest::permissions
269
+ */
270
+ permission_levels?: PermissionLevel[];
231
271
  /**
232
272
  * **Unified, deny-by-default runtime permission set** — the single typed
233
273
  * grammar (`{fs, child_process, network, tool}`) Core lowers to every sandbox
@@ -236,17 +276,21 @@ export interface PluginManifest {
236
276
  * predating this field), so an app that declares nothing keeps today's exact
237
277
  * zero-permission sandbox posture.
238
278
  *
239
- * # Relationship to [`permission_grants`]
279
+ * # Relationship to [`permission_grants`] and [`permission_levels`]
240
280
  *
241
- * These are **two distinct lanes** that must not be conflated:
281
+ * These are **three distinct lanes** that must not be conflated:
242
282
  * - [`permission_grants`] are opaque strings the **Gateway** approves at
243
283
  * install/enable time — the *approval* lane (who is allowed to ask).
244
284
  * - `permissions` is the typed set **Core** lowers into the actual sandbox at
245
285
  * spawn/exec time — the *runtime-enforcement* lane (what the code can touch).
286
+ * - [`permission_levels`] is the app's *user-facing* vocabulary an admin grants
287
+ * to a person or team — it never reaches the sandbox at all.
246
288
  *
247
289
  * A grant says "this app may use the filesystem capability"; `permissions.fs`
248
290
  * says "…and here are the exact read/write paths the sandbox is opened with."
249
291
  *
292
+ * [`permission_levels`]: PluginManifest::permission_levels
293
+ *
250
294
  * # Altitude (manifest-level, per-runnable override is a followup)
251
295
  *
252
296
  * Declared at the manifest root because **both** current enforcement sites
@@ -1503,6 +1547,51 @@ export interface McpServerDecl {
1503
1547
  [k: string]: string;
1504
1548
  };
1505
1549
  }
1550
+ /**
1551
+ * One entry in an app's **user-facing permission vocabulary** — a level an admin
1552
+ * can grant to a person or a team inside that app (see
1553
+ * [`PluginManifest::permission_levels`], which also explains why this is a
1554
+ * different axis from `permission_grants` and `permissions`).
1555
+ *
1556
+ * Deliberately self-describing: an admin UI renders the grant picker from `label`
1557
+ * + `description` alone, so a level whose meaning lives only in the app's own docs
1558
+ * cannot exist.
1559
+ */
1560
+ export interface PermissionLevel {
1561
+ /**
1562
+ * One sentence telling an admin what granting this level actually allows.
1563
+ * Required for the same reason as [`label`]: the admin deciding is usually
1564
+ * not the person who wrote the app.
1565
+ *
1566
+ * [`label`]: PermissionLevel::label
1567
+ */
1568
+ description: string;
1569
+ /**
1570
+ * Stable machine id (e.g. `"read"`). Lower-case ASCII alphanumerics plus
1571
+ * `-`, `_` and `.`, at most [`MAX_PLUGIN_ID_LEN`] bytes, and unique within the
1572
+ * manifest.
1573
+ *
1574
+ * The alphabet is narrower than a plugin id's on purpose: these ids end up in
1575
+ * API paths and in persisted grant strings, so `Read` and `read` must not be
1576
+ * two levels that look identical to a human granting them.
1577
+ */
1578
+ id: string;
1579
+ /**
1580
+ * Ids of other levels in **this same manifest** that this level subsumes.
1581
+ *
1582
+ * This is the whole ordering mechanism — there is no separate rank, so the
1583
+ * order can never contradict itself. `edit` implying `read` means a person
1584
+ * granted `edit` already holds `read`; granting both is redundant, never
1585
+ * required. Resolved transitively by
1586
+ * [`resolve_implied_permission_levels`].
1587
+ */
1588
+ implies?: string[];
1589
+ /**
1590
+ * Short human label for the grant picker (e.g. `"Can edit"`). Required —
1591
+ * an unlabelled level is unrenderable.
1592
+ */
1593
+ label: string;
1594
+ }
1506
1595
  /**
1507
1596
  * The single, typed, **deny-by-default** permission set a plugin manifest
1508
1597
  * declares, lowered by Core to every sandbox backend.
@@ -2243,6 +2332,44 @@ export interface RouteSpec {
2243
2332
  * sidecar's REST routes (`/inboxes/:id`) can be declared faithfully.
2244
2333
  */
2245
2334
  path: string;
2335
+ /**
2336
+ * The [`PluginManifest::permission_levels`] id a caller must hold to reach this
2337
+ * route. Absent (the default) = ungated: Core forwards exactly as it always did,
2338
+ * so annotating is opt-in and no existing app changes behaviour.
2339
+ *
2340
+ * This is the only place a route→permission mapping can honestly live: Core
2341
+ * cannot know that an app's `/tabs/:id/close` is destructive, and the sidecar
2342
+ * cannot enforce it (it never sees the caller's identity, only Core's minted
2343
+ * hop token). Declaring it HERE — on the same [`RouteSpec`] the proxy already
2344
+ * matches to decide forward-or-404 — means the gate and the forward can never
2345
+ * disagree about which route is in play.
2346
+ *
2347
+ * Must name a level THIS manifest declares (enforced by
2348
+ * [`crate::manifest::validate_route_permissions`]); an app cannot gate its
2349
+ * routes on another app's vocabulary or on a level nobody can see to grant.
2350
+ *
2351
+ * Never annotate an [`RouteAuth::Public`] route: a public route exists for a
2352
+ * caller who holds no identity at all (an external webhook), and on an
2353
+ * org-bound node an anonymous caller is refused outright — the annotation would
2354
+ * turn a working inbound webhook into a permanent 403.
2355
+ *
2356
+ * [`PluginManifest::permission_levels`]: crate::manifest::PluginManifest::permission_levels
2357
+ */
2358
+ permission?: string | null;
2359
+ /**
2360
+ * Which `:param` of [`path`] names the resource [`permission`] is checked
2361
+ * against, so one route can be granted per-object (`"id"` on `/tabs/:id` gates
2362
+ * each tab separately). Absent = the whole app is the resource, which is what an
2363
+ * admin grants when the route identifies nothing (a `/settings` POST).
2364
+ *
2365
+ * Only meaningful alongside [`permission`], and the named param must actually
2366
+ * appear in [`path`] — both enforced at validation, because a typo here would
2367
+ * silently widen a rule the author wrote as per-object into a per-app one.
2368
+ *
2369
+ * [`path`]: RouteSpec::path
2370
+ * [`permission`]: RouteSpec::permission
2371
+ */
2372
+ resource_param?: string | null;
2246
2373
  }
2247
2374
  /**
2248
2375
  * A single downloaded executable: fetched (checksum-verified) into the