@ryuhq/sdk 0.0.17 → 0.1.3

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.
@@ -110,6 +110,14 @@ var HookEventContributionSchema = z.object({
110
110
  /** Example of the `ctx.event` payload. Documentation, not a validated schema. */
111
111
  payload_example: z.record(z.string(), z.unknown()).optional()
112
112
  });
113
+ var PiExtensionContributionSchema = z.object({
114
+ /** Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`). */
115
+ id: z.string().min(1),
116
+ /** Path to the source, relative to the plugin root: `pi-extensions/<name>.ts`. */
117
+ file: z.string().min(1),
118
+ /** Optional one-liner describing what the extension adds to the agent. */
119
+ description: z.string().optional()
120
+ });
113
121
  var DEFAULT_WIDGET_MIME = "text/html+skybridge";
114
122
  var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
115
123
  var WidgetContributionSchema = z.object({
@@ -171,6 +179,14 @@ var ContributesSchema = z.object({
171
179
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
172
180
  * strip the dock panel an app declares here. */
173
181
  dock_panels: z.array(z.record(z.string(), z.unknown())).default([]),
182
+ /** Deletable data categories the app owns — one "Delete all X" row in Settings
183
+ * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
184
+ * it the CLI's zod parse would strip the declaration before signing, and the
185
+ * app's danger-zone row would simply never appear on any node that installed
186
+ * the packed bundle. Loosely typed here for the same reason as the surfaces
187
+ * above — Core is the layer that types it, because Core is the layer that has
188
+ * to resolve the id to something that can actually delete the rows. */
189
+ data_categories: z.array(z.record(z.string(), z.unknown())).default([]),
174
190
  /** Language servers the plugin declares, keyed by server name — the mirror of
175
191
  * Claude Code's `.lsp.json` / `lspServers`, so a config written for either host
176
192
  * loads in the other. Mirrors the Rust-side `Contributes.lsp_servers`; without
@@ -183,7 +199,16 @@ var ContributesSchema = z.object({
183
199
  * on its way through `ryu pack` — the same silent-deletion bug this field
184
200
  * exists to fix, one level down. Core is the layer that types it, because Core
185
201
  * is the layer that acts on it. */
186
- lsp_servers: z.record(z.string(), z.record(z.string(), z.unknown())).default({})
202
+ lsp_servers: z.record(z.string(), z.record(z.string(), z.unknown())).default({}),
203
+ /** Pi extensions the plugin ships — TypeScript the managed `ryu` (Pi) agent
204
+ * loads at process start. Mirrors the Rust-side `Contributes.pi_extensions`;
205
+ * without it the CLI's zod parse would strip the declaration before signing,
206
+ * and the packed plugin would ship a `pi-extensions/` folder nothing loads.
207
+ *
208
+ * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
209
+ * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
210
+ * Claude Code's to extend. */
211
+ pi_extensions: z.array(PiExtensionContributionSchema).default([])
187
212
  });
188
213
  var SetupStepSchema = z.object({
189
214
  /** Card heading (e.g. the companion app name). */
@@ -420,6 +445,7 @@ export {
420
445
  CompanionSurfaceSchema,
421
446
  TurnHookContributionSchema,
422
447
  HookEventContributionSchema,
448
+ PiExtensionContributionSchema,
423
449
  WidgetContributionSchema,
424
450
  ToolAppConfigSchema,
425
451
  ContributesSchema,
package/dist/cli.cjs CHANGED
@@ -396,6 +396,14 @@ var HookEventContributionSchema = import_zod.z.object({
396
396
  /** Example of the `ctx.event` payload. Documentation, not a validated schema. */
397
397
  payload_example: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
398
398
  });
399
+ var PiExtensionContributionSchema = import_zod.z.object({
400
+ /** Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`). */
401
+ id: import_zod.z.string().min(1),
402
+ /** Path to the source, relative to the plugin root: `pi-extensions/<name>.ts`. */
403
+ file: import_zod.z.string().min(1),
404
+ /** Optional one-liner describing what the extension adds to the agent. */
405
+ description: import_zod.z.string().optional()
406
+ });
399
407
  var DEFAULT_WIDGET_MIME = "text/html+skybridge";
400
408
  var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
401
409
  var WidgetContributionSchema = import_zod.z.object({
@@ -457,6 +465,14 @@ var ContributesSchema = import_zod.z.object({
457
465
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
458
466
  * strip the dock panel an app declares here. */
459
467
  dock_panels: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
468
+ /** Deletable data categories the app owns — one "Delete all X" row in Settings
469
+ * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
470
+ * it the CLI's zod parse would strip the declaration before signing, and the
471
+ * app's danger-zone row would simply never appear on any node that installed
472
+ * the packed bundle. Loosely typed here for the same reason as the surfaces
473
+ * above — Core is the layer that types it, because Core is the layer that has
474
+ * to resolve the id to something that can actually delete the rows. */
475
+ data_categories: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
460
476
  /** Language servers the plugin declares, keyed by server name — the mirror of
461
477
  * Claude Code's `.lsp.json` / `lspServers`, so a config written for either host
462
478
  * loads in the other. Mirrors the Rust-side `Contributes.lsp_servers`; without
@@ -469,7 +485,16 @@ var ContributesSchema = import_zod.z.object({
469
485
  * on its way through `ryu pack` — the same silent-deletion bug this field
470
486
  * exists to fix, one level down. Core is the layer that types it, because Core
471
487
  * is the layer that acts on it. */
472
- lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({})
488
+ lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({}),
489
+ /** Pi extensions the plugin ships — TypeScript the managed `ryu` (Pi) agent
490
+ * loads at process start. Mirrors the Rust-side `Contributes.pi_extensions`;
491
+ * without it the CLI's zod parse would strip the declaration before signing,
492
+ * and the packed plugin would ship a `pi-extensions/` folder nothing loads.
493
+ *
494
+ * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
495
+ * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
496
+ * Claude Code's to extend. */
497
+ pi_extensions: import_zod.z.array(PiExtensionContributionSchema).default([])
473
498
  });
474
499
  var SetupStepSchema = import_zod.z.object({
475
500
  /** Card heading (e.g. the companion app name). */
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  PluginManifestSchema
4
- } from "./chunk-SKVIJH5I.js";
4
+ } from "./chunk-XTUK5I6I.js";
5
5
  import {
6
6
  ModelClient,
7
7
  resolveGatewayUrl
package/dist/index.cjs CHANGED
@@ -940,6 +940,14 @@ var HookEventContributionSchema = import_zod.z.object({
940
940
  /** Example of the `ctx.event` payload. Documentation, not a validated schema. */
941
941
  payload_example: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
942
942
  });
943
+ var PiExtensionContributionSchema = import_zod.z.object({
944
+ /** Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`). */
945
+ id: import_zod.z.string().min(1),
946
+ /** Path to the source, relative to the plugin root: `pi-extensions/<name>.ts`. */
947
+ file: import_zod.z.string().min(1),
948
+ /** Optional one-liner describing what the extension adds to the agent. */
949
+ description: import_zod.z.string().optional()
950
+ });
943
951
  var DEFAULT_WIDGET_MIME = "text/html+skybridge";
944
952
  var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
945
953
  var WidgetContributionSchema = import_zod.z.object({
@@ -1001,6 +1009,14 @@ var ContributesSchema = import_zod.z.object({
1001
1009
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
1002
1010
  * strip the dock panel an app declares here. */
1003
1011
  dock_panels: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
1012
+ /** Deletable data categories the app owns — one "Delete all X" row in Settings
1013
+ * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
1014
+ * it the CLI's zod parse would strip the declaration before signing, and the
1015
+ * app's danger-zone row would simply never appear on any node that installed
1016
+ * the packed bundle. Loosely typed here for the same reason as the surfaces
1017
+ * above — Core is the layer that types it, because Core is the layer that has
1018
+ * to resolve the id to something that can actually delete the rows. */
1019
+ data_categories: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
1004
1020
  /** Language servers the plugin declares, keyed by server name — the mirror of
1005
1021
  * Claude Code's `.lsp.json` / `lspServers`, so a config written for either host
1006
1022
  * loads in the other. Mirrors the Rust-side `Contributes.lsp_servers`; without
@@ -1013,7 +1029,16 @@ var ContributesSchema = import_zod.z.object({
1013
1029
  * on its way through `ryu pack` — the same silent-deletion bug this field
1014
1030
  * exists to fix, one level down. Core is the layer that types it, because Core
1015
1031
  * is the layer that acts on it. */
1016
- lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({})
1032
+ lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({}),
1033
+ /** Pi extensions the plugin ships — TypeScript the managed `ryu` (Pi) agent
1034
+ * loads at process start. Mirrors the Rust-side `Contributes.pi_extensions`;
1035
+ * without it the CLI's zod parse would strip the declaration before signing,
1036
+ * and the packed plugin would ship a `pi-extensions/` folder nothing loads.
1037
+ *
1038
+ * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
1039
+ * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
1040
+ * Claude Code's to extend. */
1041
+ pi_extensions: import_zod.z.array(PiExtensionContributionSchema).default([])
1017
1042
  });
1018
1043
  var SetupStepSchema = import_zod.z.object({
1019
1044
  /** Card heading (e.g. the companion app name). */
@@ -1310,6 +1335,10 @@ function defineApp(options) {
1310
1335
  // takes no `contributes` passthrough. An app that wants to declare language
1311
1336
  // servers writes them in a hand-authored `manifest.json`.
1312
1337
  lsp_servers: {},
1338
+ // Same reason again: a danger-zone category and a Pi extension are both
1339
+ // hand-authored declarations, not something derivable from runnables.
1340
+ data_categories: [],
1341
+ pi_extensions: [],
1313
1342
  widgets
1314
1343
  };
1315
1344
  const raw = {
@@ -1929,13 +1958,16 @@ function definePlugin(options) {
1929
1958
  settings_tabs: options.settingsTabs ?? [],
1930
1959
  slash_commands: options.slashCommands ?? [],
1931
1960
  lsp_servers: options.lspServers ?? {},
1932
- // A turn-hook plugin contributes no app widgets, sidebar entries or dock
1933
- // panels; the fields are required on the resolved `Contributes` type (zod
1934
- // defaults applied), so set them explicitly.
1961
+ // A turn-hook plugin contributes no app widgets, sidebar entries, dock
1962
+ // panels, danger-zone categories or Pi extensions; the fields are required
1963
+ // on the resolved `Contributes` type (zod defaults applied), so set them
1964
+ // explicitly.
1935
1965
  widgets: [],
1936
1966
  sidebar_sections: [],
1937
1967
  sidebar_buttons: [],
1938
- dock_panels: []
1968
+ dock_panels: [],
1969
+ data_categories: [],
1970
+ pi_extensions: []
1939
1971
  };
1940
1972
  const tools = options.tools ?? [];
1941
1973
  const runnables = tools.map((t) => inlineToolRunnable(t));
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  coreManifestJsonSchema,
13
13
  validateManifestStrict,
14
14
  validatePluginId
15
- } from "./chunk-SKVIJH5I.js";
15
+ } from "./chunk-XTUK5I6I.js";
16
16
  import {
17
17
  Agent,
18
18
  PRIMITIVE_BINDINGS,
@@ -98,6 +98,10 @@ function defineApp(options) {
98
98
  // takes no `contributes` passthrough. An app that wants to declare language
99
99
  // servers writes them in a hand-authored `manifest.json`.
100
100
  lsp_servers: {},
101
+ // Same reason again: a danger-zone category and a Pi extension are both
102
+ // hand-authored declarations, not something derivable from runnables.
103
+ data_categories: [],
104
+ pi_extensions: [],
101
105
  widgets
102
106
  };
103
107
  const raw = {
@@ -717,13 +721,16 @@ function definePlugin(options) {
717
721
  settings_tabs: options.settingsTabs ?? [],
718
722
  slash_commands: options.slashCommands ?? [],
719
723
  lsp_servers: options.lspServers ?? {},
720
- // A turn-hook plugin contributes no app widgets, sidebar entries or dock
721
- // panels; the fields are required on the resolved `Contributes` type (zod
722
- // defaults applied), so set them explicitly.
724
+ // A turn-hook plugin contributes no app widgets, sidebar entries, dock
725
+ // panels, danger-zone categories or Pi extensions; the fields are required
726
+ // on the resolved `Contributes` type (zod defaults applied), so set them
727
+ // explicitly.
723
728
  widgets: [],
724
729
  sidebar_sections: [],
725
730
  sidebar_buttons: [],
726
- dock_panels: []
731
+ dock_panels: [],
732
+ data_categories: [],
733
+ pi_extensions: []
727
734
  };
728
735
  const tools = options.tools ?? [];
729
736
  const runnables = tools.map((t) => inlineToolRunnable(t));
package/dist/manifest.cjs CHANGED
@@ -25,6 +25,7 @@ __export(manifest_exports, {
25
25
  CompanionSurfaceSchema: () => CompanionSurfaceSchema,
26
26
  ContributesSchema: () => ContributesSchema,
27
27
  HookEventContributionSchema: () => HookEventContributionSchema,
28
+ PiExtensionContributionSchema: () => PiExtensionContributionSchema,
28
29
  PluginManifestSchema: () => PluginManifestSchema,
29
30
  RequiresSchema: () => RequiresSchema,
30
31
  RunnableKindSchema: () => RunnableKindSchema,
@@ -157,6 +158,14 @@ var HookEventContributionSchema = import_zod.z.object({
157
158
  /** Example of the `ctx.event` payload. Documentation, not a validated schema. */
158
159
  payload_example: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
159
160
  });
161
+ var PiExtensionContributionSchema = import_zod.z.object({
162
+ /** Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`). */
163
+ id: import_zod.z.string().min(1),
164
+ /** Path to the source, relative to the plugin root: `pi-extensions/<name>.ts`. */
165
+ file: import_zod.z.string().min(1),
166
+ /** Optional one-liner describing what the extension adds to the agent. */
167
+ description: import_zod.z.string().optional()
168
+ });
160
169
  var DEFAULT_WIDGET_MIME = "text/html+skybridge";
161
170
  var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
162
171
  var WidgetContributionSchema = import_zod.z.object({
@@ -218,6 +227,14 @@ var ContributesSchema = import_zod.z.object({
218
227
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
219
228
  * strip the dock panel an app declares here. */
220
229
  dock_panels: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
230
+ /** Deletable data categories the app owns — one "Delete all X" row in Settings
231
+ * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
232
+ * it the CLI's zod parse would strip the declaration before signing, and the
233
+ * app's danger-zone row would simply never appear on any node that installed
234
+ * the packed bundle. Loosely typed here for the same reason as the surfaces
235
+ * above — Core is the layer that types it, because Core is the layer that has
236
+ * to resolve the id to something that can actually delete the rows. */
237
+ data_categories: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
221
238
  /** Language servers the plugin declares, keyed by server name — the mirror of
222
239
  * Claude Code's `.lsp.json` / `lspServers`, so a config written for either host
223
240
  * loads in the other. Mirrors the Rust-side `Contributes.lsp_servers`; without
@@ -230,7 +247,16 @@ var ContributesSchema = import_zod.z.object({
230
247
  * on its way through `ryu pack` — the same silent-deletion bug this field
231
248
  * exists to fix, one level down. Core is the layer that types it, because Core
232
249
  * is the layer that acts on it. */
233
- lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({})
250
+ lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({}),
251
+ /** Pi extensions the plugin ships — TypeScript the managed `ryu` (Pi) agent
252
+ * loads at process start. Mirrors the Rust-side `Contributes.pi_extensions`;
253
+ * without it the CLI's zod parse would strip the declaration before signing,
254
+ * and the packed plugin would ship a `pi-extensions/` folder nothing loads.
255
+ *
256
+ * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
257
+ * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
258
+ * Claude Code's to extend. */
259
+ pi_extensions: import_zod.z.array(PiExtensionContributionSchema).default([])
234
260
  });
235
261
  var SetupStepSchema = import_zod.z.object({
236
262
  /** Card heading (e.g. the companion app name). */
@@ -466,6 +492,7 @@ function coreManifestJsonSchema() {
466
492
  CompanionSurfaceSchema,
467
493
  ContributesSchema,
468
494
  HookEventContributionSchema,
495
+ PiExtensionContributionSchema,
469
496
  PluginManifestSchema,
470
497
  RequiresSchema,
471
498
  RunnableKindSchema,
@@ -115,6 +115,32 @@ declare const HookEventContributionSchema: z.ZodObject<{
115
115
  payload_example: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
116
116
  }, z.core.$strip>;
117
117
  type HookEventContribution = z.infer<typeof HookEventContributionSchema>;
118
+ /**
119
+ * One Pi extension the plugin ships — a TypeScript file the managed `ryu` (Pi)
120
+ * agent loads at process start. Mirrors Rust `PiExtensionContribution`.
121
+ *
122
+ * Carries a PATH, never a body: unlike `turn_hooks` there is no inline `code`
123
+ * twin, because nothing downstream reads the source as a string.
124
+ *
125
+ * That makes it a SIDECAR FILE, and `ryu pack` emits a single JSON bundle — so a
126
+ * plugin installed from a packed bundle arrives without its `pi-extensions/`
127
+ * directory and Core resolves the declaration to a visible skip. Same open gap as
128
+ * `skills/**`, which the bundle likewise does not carry. Today the path that works
129
+ * is a plugin whose directory is on disk (a built-in, a satellite checkout, a dev
130
+ * tree). Do not "fix" this by inlining the source into the manifest: a 50 KB
131
+ * TypeScript program escaped into a JSON string is the unauditable form the whole
132
+ * `code_file` extraction exists to prevent.
133
+ *
134
+ * Note this is UNSANDBOXED code: it runs inside the agent process with full host
135
+ * privilege, so Core gates it behind the operator-only `pi:extension` grant for
136
+ * any non-built-in plugin.
137
+ */
138
+ declare const PiExtensionContributionSchema: z.ZodObject<{
139
+ id: z.ZodString;
140
+ file: z.ZodString;
141
+ description: z.ZodOptional<z.ZodString>;
142
+ }, z.core.$strip>;
143
+ type PiExtensionContribution = z.infer<typeof PiExtensionContributionSchema>;
118
144
  /**
119
145
  * One app-widget contribution (Ryu Apps). Binds the render tool that produces the
120
146
  * widget to its `ui://widget/<slug>.html` template. Shape-identical to Core's
@@ -187,7 +213,13 @@ declare const ContributesSchema: z.ZodObject<{
187
213
  sidebar_sections: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
188
214
  sidebar_buttons: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
189
215
  dock_panels: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
216
+ data_categories: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
190
217
  lsp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
218
+ pi_extensions: z.ZodDefault<z.ZodArray<z.ZodObject<{
219
+ id: z.ZodString;
220
+ file: z.ZodString;
221
+ description: z.ZodOptional<z.ZodString>;
222
+ }, z.core.$strip>>>;
191
223
  }, z.core.$strip>;
192
224
  type Contributes = z.infer<typeof ContributesSchema>;
193
225
  /**
@@ -338,7 +370,13 @@ declare const PluginManifestSchema: z.ZodObject<{
338
370
  sidebar_sections: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
339
371
  sidebar_buttons: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
340
372
  dock_panels: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
373
+ data_categories: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
341
374
  lsp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
375
+ pi_extensions: z.ZodDefault<z.ZodArray<z.ZodObject<{
376
+ id: z.ZodString;
377
+ file: z.ZodString;
378
+ description: z.ZodOptional<z.ZodString>;
379
+ }, z.core.$strip>>>;
342
380
  }, z.core.$strip>>;
343
381
  requires: z.ZodOptional<z.ZodObject<{
344
382
  apps: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -365,8 +403,8 @@ declare const PluginManifestSchema: z.ZodObject<{
365
403
  enabled: z.ZodDefault<z.ZodBoolean>;
366
404
  rule: z.ZodOptional<z.ZodObject<{
367
405
  type: z.ZodEnum<{
368
- flat: "flat";
369
406
  percent: "percent";
407
+ flat: "flat";
370
408
  }>;
371
409
  value: z.ZodNumber;
372
410
  recurring: z.ZodDefault<z.ZodBoolean>;
@@ -435,4 +473,4 @@ declare function validateManifestStrict(manifestJson: string): string;
435
473
  */
436
474
  declare function coreManifestJsonSchema(): unknown;
437
475
 
438
- export { type AppDependency, AppDependencySchema, type CapabilityReq, CapabilityReqSchema, type CompanionSurface, CompanionSurfaceSchema, type Contributes, ContributesSchema, type HookEventContribution, HookEventContributionSchema, type PluginManifest, PluginManifestSchema, type Requires, RequiresSchema, type RunnableKind, RunnableKindSchema, type RunnableMeta, RunnableMetaSchema, type SetupStep, SetupStepSchema, type Surface, SurfaceSchema, type ToolAppConfig, ToolAppConfigSchema, type TurnHookContribution, TurnHookContributionSchema, type WidgetContribution, WidgetContributionSchema, coreManifestJsonSchema, labelImpersonatesSystemChrome, validateManifestStrict, validatePluginId };
476
+ export { type AppDependency, AppDependencySchema, type CapabilityReq, CapabilityReqSchema, type CompanionSurface, CompanionSurfaceSchema, type Contributes, ContributesSchema, type HookEventContribution, HookEventContributionSchema, type PiExtensionContribution, PiExtensionContributionSchema, type PluginManifest, PluginManifestSchema, type Requires, RequiresSchema, type RunnableKind, RunnableKindSchema, type RunnableMeta, RunnableMetaSchema, type SetupStep, SetupStepSchema, type Surface, SurfaceSchema, type ToolAppConfig, ToolAppConfigSchema, type TurnHookContribution, TurnHookContributionSchema, type WidgetContribution, WidgetContributionSchema, coreManifestJsonSchema, labelImpersonatesSystemChrome, validateManifestStrict, validatePluginId };
@@ -115,6 +115,32 @@ declare const HookEventContributionSchema: z.ZodObject<{
115
115
  payload_example: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
116
116
  }, z.core.$strip>;
117
117
  type HookEventContribution = z.infer<typeof HookEventContributionSchema>;
118
+ /**
119
+ * One Pi extension the plugin ships — a TypeScript file the managed `ryu` (Pi)
120
+ * agent loads at process start. Mirrors Rust `PiExtensionContribution`.
121
+ *
122
+ * Carries a PATH, never a body: unlike `turn_hooks` there is no inline `code`
123
+ * twin, because nothing downstream reads the source as a string.
124
+ *
125
+ * That makes it a SIDECAR FILE, and `ryu pack` emits a single JSON bundle — so a
126
+ * plugin installed from a packed bundle arrives without its `pi-extensions/`
127
+ * directory and Core resolves the declaration to a visible skip. Same open gap as
128
+ * `skills/**`, which the bundle likewise does not carry. Today the path that works
129
+ * is a plugin whose directory is on disk (a built-in, a satellite checkout, a dev
130
+ * tree). Do not "fix" this by inlining the source into the manifest: a 50 KB
131
+ * TypeScript program escaped into a JSON string is the unauditable form the whole
132
+ * `code_file` extraction exists to prevent.
133
+ *
134
+ * Note this is UNSANDBOXED code: it runs inside the agent process with full host
135
+ * privilege, so Core gates it behind the operator-only `pi:extension` grant for
136
+ * any non-built-in plugin.
137
+ */
138
+ declare const PiExtensionContributionSchema: z.ZodObject<{
139
+ id: z.ZodString;
140
+ file: z.ZodString;
141
+ description: z.ZodOptional<z.ZodString>;
142
+ }, z.core.$strip>;
143
+ type PiExtensionContribution = z.infer<typeof PiExtensionContributionSchema>;
118
144
  /**
119
145
  * One app-widget contribution (Ryu Apps). Binds the render tool that produces the
120
146
  * widget to its `ui://widget/<slug>.html` template. Shape-identical to Core's
@@ -187,7 +213,13 @@ declare const ContributesSchema: z.ZodObject<{
187
213
  sidebar_sections: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
188
214
  sidebar_buttons: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
189
215
  dock_panels: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
216
+ data_categories: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
190
217
  lsp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
218
+ pi_extensions: z.ZodDefault<z.ZodArray<z.ZodObject<{
219
+ id: z.ZodString;
220
+ file: z.ZodString;
221
+ description: z.ZodOptional<z.ZodString>;
222
+ }, z.core.$strip>>>;
191
223
  }, z.core.$strip>;
192
224
  type Contributes = z.infer<typeof ContributesSchema>;
193
225
  /**
@@ -338,7 +370,13 @@ declare const PluginManifestSchema: z.ZodObject<{
338
370
  sidebar_sections: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
339
371
  sidebar_buttons: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
340
372
  dock_panels: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
373
+ data_categories: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
341
374
  lsp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
375
+ pi_extensions: z.ZodDefault<z.ZodArray<z.ZodObject<{
376
+ id: z.ZodString;
377
+ file: z.ZodString;
378
+ description: z.ZodOptional<z.ZodString>;
379
+ }, z.core.$strip>>>;
342
380
  }, z.core.$strip>>;
343
381
  requires: z.ZodOptional<z.ZodObject<{
344
382
  apps: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -365,8 +403,8 @@ declare const PluginManifestSchema: z.ZodObject<{
365
403
  enabled: z.ZodDefault<z.ZodBoolean>;
366
404
  rule: z.ZodOptional<z.ZodObject<{
367
405
  type: z.ZodEnum<{
368
- flat: "flat";
369
406
  percent: "percent";
407
+ flat: "flat";
370
408
  }>;
371
409
  value: z.ZodNumber;
372
410
  recurring: z.ZodDefault<z.ZodBoolean>;
@@ -435,4 +473,4 @@ declare function validateManifestStrict(manifestJson: string): string;
435
473
  */
436
474
  declare function coreManifestJsonSchema(): unknown;
437
475
 
438
- export { type AppDependency, AppDependencySchema, type CapabilityReq, CapabilityReqSchema, type CompanionSurface, CompanionSurfaceSchema, type Contributes, ContributesSchema, type HookEventContribution, HookEventContributionSchema, type PluginManifest, PluginManifestSchema, type Requires, RequiresSchema, type RunnableKind, RunnableKindSchema, type RunnableMeta, RunnableMetaSchema, type SetupStep, SetupStepSchema, type Surface, SurfaceSchema, type ToolAppConfig, ToolAppConfigSchema, type TurnHookContribution, TurnHookContributionSchema, type WidgetContribution, WidgetContributionSchema, coreManifestJsonSchema, labelImpersonatesSystemChrome, validateManifestStrict, validatePluginId };
476
+ export { type AppDependency, AppDependencySchema, type CapabilityReq, CapabilityReqSchema, type CompanionSurface, CompanionSurfaceSchema, type Contributes, ContributesSchema, type HookEventContribution, HookEventContributionSchema, type PiExtensionContribution, PiExtensionContributionSchema, type PluginManifest, PluginManifestSchema, type Requires, RequiresSchema, type RunnableKind, RunnableKindSchema, type RunnableMeta, RunnableMetaSchema, type SetupStep, SetupStepSchema, type Surface, SurfaceSchema, type ToolAppConfig, ToolAppConfigSchema, type TurnHookContribution, TurnHookContributionSchema, type WidgetContribution, WidgetContributionSchema, coreManifestJsonSchema, labelImpersonatesSystemChrome, validateManifestStrict, validatePluginId };
package/dist/manifest.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  CompanionSurfaceSchema,
5
5
  ContributesSchema,
6
6
  HookEventContributionSchema,
7
+ PiExtensionContributionSchema,
7
8
  PluginManifestSchema,
8
9
  RequiresSchema,
9
10
  RunnableKindSchema,
@@ -17,13 +18,14 @@ import {
17
18
  labelImpersonatesSystemChrome,
18
19
  validateManifestStrict,
19
20
  validatePluginId
20
- } from "./chunk-SKVIJH5I.js";
21
+ } from "./chunk-XTUK5I6I.js";
21
22
  export {
22
23
  AppDependencySchema,
23
24
  CapabilityReqSchema,
24
25
  CompanionSurfaceSchema,
25
26
  ContributesSchema,
26
27
  HookEventContributionSchema,
28
+ PiExtensionContributionSchema,
27
29
  PluginManifestSchema,
28
30
  RequiresSchema,
29
31
  RunnableKindSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuhq/sdk",
3
- "version": "0.0.17",
3
+ "version": "0.1.3",
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,7 +44,7 @@
44
44
  "clean": "rm -rf dist"
45
45
  },
46
46
  "dependencies": {
47
- "@ryuhq/sdk-native": "0.1.0",
47
+ "@ryuhq/sdk-native": "0.1.3",
48
48
  "zod": "^4.1.13"
49
49
  },
50
50
  "devDependencies": {
@@ -49,7 +49,7 @@ export type ProviderTarget = "local-machine" | "remote-desktop";
49
49
  * An **empty/absent** `targets` list means the plugin runs on *every* surface —
50
50
  * that is the backward-compatible default and MUST NOT be read as "hidden".
51
51
  */
52
- export type Surface = "gateway" | "core" | "desktop" | "island" | "mobile" | "extension" | "web" | "cli";
52
+ export type Surface = "gateway" | "core" | "desktop" | "island" | "mobile" | "extension" | "web" | "cli" | "unknown";
53
53
 
54
54
  /**
55
55
  * An installable Ryu App manifest (`manifest.json`).
@@ -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
@@ -487,6 +531,40 @@ export interface Contributes {
487
531
  * older shells" instead of breaking the composer.
488
532
  */
489
533
  composer_controls?: unknown[];
534
+ /**
535
+ * **Deletable data categories** the app owns — one "Delete all X" row in
536
+ * Settings → Danger Zone (see [`DataCategoryContribution`]).
537
+ *
538
+ * The danger zone used to be two hardcoded lists that had to be edited
539
+ * together: a `DataCategory` enum in Core and a `CATEGORIES` array carrying the
540
+ * user-facing copy in the closed desktop source. Monitors and Meetings are
541
+ * app-owned data, so both lists named apps — which meant a node where Monitors
542
+ * was never enabled still offered to delete monitors, and the count was always
543
+ * 0. Declaring the category here makes the owning app the single source of both
544
+ * its existence and its wording, and makes the row appear and disappear with
545
+ * the app instead of with a client-side feature-detect.
546
+ *
547
+ * # Core-interpreted, so a typed struct — and NOT on the contributions endpoint
548
+ *
549
+ * Core has to resolve the id to something that can actually count and delete
550
+ * the rows, so per this type's own doc comment this gets a typed struct rather
551
+ * than opaque JSON, and it is gathered at its consumption site
552
+ * (`GET /api/data/counts`, which serves each category's descriptor next to its
553
+ * live count) rather than at `GET /api/plugins/contributions` — the same
554
+ * disposition as [`Contributes::tool_filters`] and [`Contributes::lsp_servers`].
555
+ *
556
+ * # Declaration, not implementation
557
+ *
558
+ * A declared category is served only when Core knows how to clear it; an id
559
+ * Core does not implement is skipped with a warn rather than being offered as a
560
+ * button that 400s. That split is deliberate and not a stepping stone to a
561
+ * generic HTTP truncate: clearing monitors has to tear down each monitor's
562
+ * backing scheduler job, and clearing meetings has to broadcast on the meetings
563
+ * SSE stream, so a blind `DELETE /monitors` would leave jobs ticking forever.
564
+ * The manifest owns *whether the row exists and what it says*; Core owns *what
565
+ * deleting actually entails*.
566
+ */
567
+ data_categories?: DataCategoryContribution[];
490
568
  /**
491
569
  * App-registered **workspace dock panels** — a tab in the desktop's bottom or
492
570
  * right dock (Terminal / Code Review / Browser / Simulator live there today).
@@ -582,6 +660,45 @@ export interface Contributes {
582
660
  lsp_servers?: {
583
661
  [k: string]: LspServerContribution;
584
662
  };
663
+ /**
664
+ * **Pi extensions** the plugin ships — TypeScript files the managed `ryu` (Pi)
665
+ * agent loads at process start:
666
+ *
667
+ * ```json
668
+ * "pi_extensions": [
669
+ * { "id": "shell", "file": "pi-extensions/ryu-shell.ts",
670
+ * "description": "background bash for the managed Pi agent" }
671
+ * ]
672
+ * ```
673
+ *
674
+ * Pi ships none of plan mode, sub-agents, permission prompts or background bash
675
+ * and says so deliberately in its own docs — "you can build or install those
676
+ * workflows as extensions or packages". This surface is that seam: the
677
+ * capabilities Core used to hardcode into the spawn path become plugins the user
678
+ * can enable and disable, and a third party can ship one at all.
679
+ *
680
+ * # This is UNSANDBOXED code, and the tier gate is not optional
681
+ *
682
+ * A [`Contributes::turn_hooks`] body runs in the deny-by-default Deno sandbox
683
+ * behind capability-gated `host.*` calls. A file named here runs **inside the Pi
684
+ * process** with full host privilege: the first-party ones spawn children and
685
+ * POST to Core. That is the same arbitrary-code-execution class as
686
+ * [`PluginManifest::mcp_servers`], so Core gates it identically — Core tier is
687
+ * auto-allowed, Community tier needs an operator-allowlisted grant, and the gate
688
+ * sits at the materializer, because writing the file is what makes it run.
689
+ *
690
+ * # Core-interpreted, so a typed struct — and NOT on the contributions endpoint
691
+ *
692
+ * Core resolves each `file` and projects it into the managed Pi's config dir, so
693
+ * per this type's own doc comment it gets a typed struct and is gathered at its
694
+ * consumption site (`pi_config::app_extensions`) rather than served from
695
+ * `GET /api/plugins/contributions` — the same disposition as
696
+ * [`Contributes::lsp_servers`].
697
+ *
698
+ * The `file` is deliberately NOT hydrated into an inline string the way a
699
+ * `code_file` is; see [`PluginManifest::pi_extension_refs`] for why.
700
+ */
701
+ pi_extensions?: PiExtensionContribution[];
585
702
  /**
586
703
  * Gateway policies the plugin contributes (referenced by runnable id).
587
704
  */
@@ -699,6 +816,42 @@ export interface ContributionId {
699
816
  */
700
817
  title?: string | null;
701
818
  }
819
+ /**
820
+ * One **deletable data category** an app owns (see [`Contributes::data_categories`]).
821
+ *
822
+ * Everything the Danger Zone needs to draw and arm one destructive row, so the copy
823
+ * lives with the app whose data it describes rather than in the desktop's source.
824
+ */
825
+ export interface DataCategoryContribution {
826
+ /**
827
+ * The word the user must type to arm the delete. Absent = the [`noun`], which is
828
+ * the right default often enough that requiring it would just be ceremony.
829
+ * Matched case-insensitively by the client.
830
+ *
831
+ * [`noun`]: DataCategoryContribution::noun
832
+ */
833
+ confirm_word?: string | null;
834
+ /**
835
+ * Exactly what disappears, shown in the confirm dialog. Required, and required
836
+ * to be specific: this is the last thing the user reads before an irreversible
837
+ * delete, and "this cannot be undone" tells them nothing they did not know.
838
+ */
839
+ detail: string;
840
+ /**
841
+ * Stable id — this is the `category` a `POST /api/data/clear` names, so it is
842
+ * the app's half of the delete contract and renaming it breaks the button.
843
+ */
844
+ id: string;
845
+ /**
846
+ * Plural noun for the live count line ("42 monitors" / "No monitors") and the
847
+ * "N deleted" toast. Lower-case: it is used mid-sentence.
848
+ */
849
+ noun: string;
850
+ /**
851
+ * The destructive button label and confirm-dialog title ("Delete all monitors").
852
+ */
853
+ title: string;
854
+ }
702
855
  /**
703
856
  * One app-registered **workspace dock panel** — a tab in the desktop's bottom or
704
857
  * right dock (see [`Contributes::dock_panels`]).
@@ -955,6 +1108,31 @@ export interface LspServerContribution {
955
1108
  */
956
1109
  workspaceFolder?: string | null;
957
1110
  }
1111
+ /**
1112
+ * One **Pi extension** a plugin ships (a [`Contributes::pi_extensions`] row).
1113
+ *
1114
+ * Carries a path, never a body: unlike [`TurnHookContribution`] there is no inline
1115
+ * `code` twin, because nothing downstream reads the source as a string.
1116
+ */
1117
+ export interface PiExtensionContribution {
1118
+ /**
1119
+ * Optional human-facing one-liner (what the extension adds to the agent).
1120
+ */
1121
+ description?: string | null;
1122
+ /**
1123
+ * Path to the TypeScript source, relative to the plugin root — exactly
1124
+ * `pi-extensions/<name>.ts`. See [`validate_pi_extension_path`].
1125
+ */
1126
+ file: string;
1127
+ /**
1128
+ * Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`).
1129
+ *
1130
+ * Part of the materialized file name, so it is what makes one plugin's
1131
+ * extensions distinguishable from another's on disk — and why it is validated
1132
+ * with the same alphabet as an event name rather than left free-form.
1133
+ */
1134
+ id: string;
1135
+ }
958
1136
  /**
959
1137
  * One **settings tab** a plugin contributes (see [`Contributes::settings_tabs`]).
960
1138
  *
@@ -1369,6 +1547,51 @@ export interface McpServerDecl {
1369
1547
  [k: string]: string;
1370
1548
  };
1371
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
+ }
1372
1595
  /**
1373
1596
  * The single, typed, **deny-by-default** permission set a plugin manifest
1374
1597
  * declares, lowered by Core to every sandbox backend.
@@ -2109,6 +2332,44 @@ export interface RouteSpec {
2109
2332
  * sidecar's REST routes (`/inboxes/:id`) can be declared faithfully.
2110
2333
  */
2111
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;
2112
2373
  }
2113
2374
  /**
2114
2375
  * A single downloaded executable: fetched (checksum-verified) into the
@@ -2209,7 +2470,7 @@ export interface SurfaceEntry {
2209
2470
  /**
2210
2471
  * How much of the plugin this surface supports.
2211
2472
  */
2212
- support?: "full" | "limited" | "list" | "commands" | "none";
2473
+ support?: "full" | "limited" | "list" | "commands" | "none" | "unknown";
2213
2474
  /**
2214
2475
  * Optional surface-specific UI descriptor (bundle id, mount point, …),
2215
2476
  * interpreted by the surface's app host. Opaque to the contract.
package/src/manifest.ts CHANGED
@@ -239,6 +239,41 @@ export const HookEventContributionSchema = z.object({
239
239
 
240
240
  export type HookEventContribution = z.infer<typeof HookEventContributionSchema>;
241
241
 
242
+ // ── PiExtensionContribution ───────────────────────────────────────────────────
243
+
244
+ /**
245
+ * One Pi extension the plugin ships — a TypeScript file the managed `ryu` (Pi)
246
+ * agent loads at process start. Mirrors Rust `PiExtensionContribution`.
247
+ *
248
+ * Carries a PATH, never a body: unlike `turn_hooks` there is no inline `code`
249
+ * twin, because nothing downstream reads the source as a string.
250
+ *
251
+ * That makes it a SIDECAR FILE, and `ryu pack` emits a single JSON bundle — so a
252
+ * plugin installed from a packed bundle arrives without its `pi-extensions/`
253
+ * directory and Core resolves the declaration to a visible skip. Same open gap as
254
+ * `skills/**`, which the bundle likewise does not carry. Today the path that works
255
+ * is a plugin whose directory is on disk (a built-in, a satellite checkout, a dev
256
+ * tree). Do not "fix" this by inlining the source into the manifest: a 50 KB
257
+ * TypeScript program escaped into a JSON string is the unauditable form the whole
258
+ * `code_file` extraction exists to prevent.
259
+ *
260
+ * Note this is UNSANDBOXED code: it runs inside the agent process with full host
261
+ * privilege, so Core gates it behind the operator-only `pi:extension` grant for
262
+ * any non-built-in plugin.
263
+ */
264
+ export const PiExtensionContributionSchema = z.object({
265
+ /** Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`). */
266
+ id: z.string().min(1),
267
+ /** Path to the source, relative to the plugin root: `pi-extensions/<name>.ts`. */
268
+ file: z.string().min(1),
269
+ /** Optional one-liner describing what the extension adds to the agent. */
270
+ description: z.string().optional(),
271
+ });
272
+
273
+ export type PiExtensionContribution = z.infer<
274
+ typeof PiExtensionContributionSchema
275
+ >;
276
+
242
277
  // ── WidgetContribution (Ryu Apps) ─────────────────────────────────────────────
243
278
 
244
279
  /** Default widget MIME dialect. Mirrors Core `default_widget_mime`. */
@@ -335,6 +370,14 @@ export const ContributesSchema = z.object({
335
370
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
336
371
  * strip the dock panel an app declares here. */
337
372
  dock_panels: z.array(z.record(z.string(), z.unknown())).default([]),
373
+ /** Deletable data categories the app owns — one "Delete all X" row in Settings
374
+ * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
375
+ * it the CLI's zod parse would strip the declaration before signing, and the
376
+ * app's danger-zone row would simply never appear on any node that installed
377
+ * the packed bundle. Loosely typed here for the same reason as the surfaces
378
+ * above — Core is the layer that types it, because Core is the layer that has
379
+ * to resolve the id to something that can actually delete the rows. */
380
+ data_categories: z.array(z.record(z.string(), z.unknown())).default([]),
338
381
  /** Language servers the plugin declares, keyed by server name — the mirror of
339
382
  * Claude Code's `.lsp.json` / `lspServers`, so a config written for either host
340
383
  * loads in the other. Mirrors the Rust-side `Contributes.lsp_servers`; without
@@ -350,6 +393,15 @@ export const ContributesSchema = z.object({
350
393
  lsp_servers: z
351
394
  .record(z.string(), z.record(z.string(), z.unknown()))
352
395
  .default({}),
396
+ /** Pi extensions the plugin ships — TypeScript the managed `ryu` (Pi) agent
397
+ * loads at process start. Mirrors the Rust-side `Contributes.pi_extensions`;
398
+ * without it the CLI's zod parse would strip the declaration before signing,
399
+ * and the packed plugin would ship a `pi-extensions/` folder nothing loads.
400
+ *
401
+ * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
402
+ * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
403
+ * Claude Code's to extend. */
404
+ pi_extensions: z.array(PiExtensionContributionSchema).default([]),
353
405
  });
354
406
 
355
407
  export type Contributes = z.infer<typeof ContributesSchema>;
@@ -228,6 +228,10 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
228
228
  // takes no `contributes` passthrough. An app that wants to declare language
229
229
  // servers writes them in a hand-authored `manifest.json`.
230
230
  lsp_servers: {},
231
+ // Same reason again: a danger-zone category and a Pi extension are both
232
+ // hand-authored declarations, not something derivable from runnables.
233
+ data_categories: [],
234
+ pi_extensions: [],
231
235
  widgets,
232
236
  };
233
237
 
@@ -176,13 +176,16 @@ export function definePlugin(options: DefinePluginOptions): PluginManifest {
176
176
  settings_tabs: options.settingsTabs ?? [],
177
177
  slash_commands: options.slashCommands ?? [],
178
178
  lsp_servers: options.lspServers ?? {},
179
- // A turn-hook plugin contributes no app widgets, sidebar entries or dock
180
- // panels; the fields are required on the resolved `Contributes` type (zod
181
- // defaults applied), so set them explicitly.
179
+ // A turn-hook plugin contributes no app widgets, sidebar entries, dock
180
+ // panels, danger-zone categories or Pi extensions; the fields are required
181
+ // on the resolved `Contributes` type (zod defaults applied), so set them
182
+ // explicitly.
182
183
  widgets: [],
183
184
  sidebar_sections: [],
184
185
  sidebar_buttons: [],
185
186
  dock_panels: [],
187
+ data_categories: [],
188
+ pi_extensions: [],
186
189
  };
187
190
  // Ship each inline tool as a `kind:"tool"` runnable (Core's `inline_deno`
188
191
  // backend). Shipping tools requires the `tool:execute` grant; add it once.