@ryuhq/sdk 0.0.5

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.
Files changed (61) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +31 -0
  3. package/dist/agent.cjs +761 -0
  4. package/dist/agent.d.cts +3 -0
  5. package/dist/agent.d.ts +3 -0
  6. package/dist/agent.js +23 -0
  7. package/dist/chunk-GXHL5CO7.js +353 -0
  8. package/dist/chunk-KPKMMGVC.js +671 -0
  9. package/dist/chunk-ODFEUVPW.js +100 -0
  10. package/dist/cli.cjs +858 -0
  11. package/dist/cli.d.cts +1 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +454 -0
  14. package/dist/index-CEbS1SlS.d.cts +988 -0
  15. package/dist/index-DAxq7Y0R.d.ts +988 -0
  16. package/dist/index.cjs +1900 -0
  17. package/dist/index.d.cts +759 -0
  18. package/dist/index.d.ts +759 -0
  19. package/dist/index.js +771 -0
  20. package/dist/manifest.cjs +399 -0
  21. package/dist/manifest.d.cts +355 -0
  22. package/dist/manifest.d.ts +355 -0
  23. package/dist/manifest.js +38 -0
  24. package/package.json +56 -0
  25. package/src/agent/agent.ts +208 -0
  26. package/src/agent/index.ts +51 -0
  27. package/src/agent/loop.test.ts +261 -0
  28. package/src/agent/loop.ts +259 -0
  29. package/src/agent/model-call.ts +190 -0
  30. package/src/agent/query.ts +40 -0
  31. package/src/agent/tools.ts +295 -0
  32. package/src/builder.ts +473 -0
  33. package/src/cli/dev.test.ts +178 -0
  34. package/src/cli/dev.ts +425 -0
  35. package/src/cli.ts +390 -0
  36. package/src/contracts-lockstep.test.ts +77 -0
  37. package/src/generated/plugin-manifest.ts +1121 -0
  38. package/src/index.ts +141 -0
  39. package/src/manifest.test.ts +610 -0
  40. package/src/manifest.ts +589 -0
  41. package/src/mcp/bridge.test.ts +196 -0
  42. package/src/mcp/client.ts +253 -0
  43. package/src/mcp/fixture-server.ts +23 -0
  44. package/src/mcp/server.ts +351 -0
  45. package/src/model/client.test.ts +107 -0
  46. package/src/model/client.ts +179 -0
  47. package/src/model/gateway.ts +41 -0
  48. package/src/plugin/ryu-plugin.ts +191 -0
  49. package/src/runnable/agent.ts +338 -0
  50. package/src/runnable/app.ts +233 -0
  51. package/src/runnable/index.ts +61 -0
  52. package/src/runnable/primitives-hostapi.test.ts +73 -0
  53. package/src/runnable/primitives.test.ts +286 -0
  54. package/src/runnable/primitives.ts +610 -0
  55. package/src/runnable/runnable-types.ts +113 -0
  56. package/src/runnable/runnable.test.ts +397 -0
  57. package/src/runnable/skill.ts +60 -0
  58. package/src/runnable/tool.ts +260 -0
  59. package/src/runnable/turn-hook.test.ts +81 -0
  60. package/src/runnable/turn-hook.ts +191 -0
  61. package/src/runnable/workflow.ts +76 -0
@@ -0,0 +1,589 @@
1
+ /**
2
+ * Ryu SDK manifest types — TypeScript mirror of the Core `plugin_manifest` and
3
+ * `runnable` schemas (`apps/core/src/plugin_manifest/mod.rs` and
4
+ * `apps/core/src/runnable/mod.rs`).
5
+ *
6
+ * These types must stay in sync with the Rust serde shapes so that a manifest
7
+ * authored here deserialises cleanly by `PluginManifestLoader::load()` in Core.
8
+ *
9
+ * Design note on engine/model fields: every field that holds an engine name,
10
+ * model id, or provider reference is typed as `string` — never a union of
11
+ * known provider literals. A new provider must never require an SDK change.
12
+ */
13
+
14
+ import { createRequire } from "node:module";
15
+ import { z } from "zod";
16
+
17
+ // ── Lazy, optional native addon ──────────────────────────────────────────────
18
+ //
19
+ // The Rust-cored validation helpers at the bottom of this file delegate to the
20
+ // `@ryuhq/sdk-native` napi addon (`crates/ryu-sdk-napi`). That addon is a
21
+ // prebuilt, platform-specific `.node` binary and is *not* always present — e.g.
22
+ // in a fresh `create-ryu-app` scaffold context, which imports this module only
23
+ // for `PluginManifestSchema` (pure-JS zod). Importing `@ryuhq/sdk/manifest`
24
+ // must therefore never hard-require the addon at module load. We load it lazily
25
+ // on first use of a helper that needs it, cache it, and throw a descriptive
26
+ // error only if a caller actually invokes those helpers without the addon.
27
+ type NativeAddon = {
28
+ validatePluginId(id: string): void;
29
+ parseAndValidateManifest(manifestJson: string): string;
30
+ pluginManifestJsonSchema(): string;
31
+ };
32
+
33
+ let cachedNative: NativeAddon | null = null;
34
+ let nativeLoadError: Error | null = null;
35
+
36
+ /**
37
+ * Load the `@ryuhq/sdk-native` addon on demand. Uses a synchronous `require`
38
+ * (via `createRequire`) so the surrounding helpers can stay synchronous, and
39
+ * works in both the ESM and CJS builds (tsup `shims` provides `import.meta.url`
40
+ * in the CJS output). Throws a descriptive error when the addon is absent.
41
+ */
42
+ function loadNative(): NativeAddon {
43
+ if (cachedNative) {
44
+ return cachedNative;
45
+ }
46
+ if (nativeLoadError) {
47
+ throw nativeLoadError;
48
+ }
49
+ try {
50
+ const req = createRequire(import.meta.url);
51
+ cachedNative = req("@ryuhq/sdk-native") as NativeAddon;
52
+ return cachedNative;
53
+ } catch (cause) {
54
+ nativeLoadError = new Error(
55
+ "@ryuhq/sdk-native (the Rust-cored napi addon) is not available; " +
56
+ "Core-strict manifest validation requires it. Build/install the addon, " +
57
+ "or use PluginManifestSchema (pure-JS zod) for authoring-time validation.",
58
+ { cause }
59
+ );
60
+ throw nativeLoadError;
61
+ }
62
+ }
63
+
64
+ // ── RunnableKind ─────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * The kind of a Runnable. Mirrors `RunnableKind` in
68
+ * `apps/core/src/runnable/mod.rs`.
69
+ */
70
+ export const RunnableKindSchema = z.enum([
71
+ "agent",
72
+ "workflow",
73
+ "tool",
74
+ "skill",
75
+ // A companion surface (in-desktop panel). Added so a packable plugin can
76
+ // declare a companion runnable that flows through Core's existing Companion
77
+ // handler → app_contrib → `GET /api/plugins/contributions` → the desktop
78
+ // `/plugin/<id>` route. Its `config.ui_entry` (see `CompanionRunnableConfigSchema`)
79
+ // is what `ryu pack` bundles into `ui_code`.
80
+ "companion",
81
+ ]);
82
+
83
+ export type RunnableKind = z.infer<typeof RunnableKindSchema>;
84
+
85
+ // ── RunnableMeta ─────────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Kind-agnostic identity snapshot of a Runnable. Mirrors `RunnableMeta` in
89
+ * `apps/core/src/runnable/mod.rs`.
90
+ */
91
+ export const RunnableMetaSchema = z.object({
92
+ /** Stable unique identifier (e.g. `"agent-researcher"`). */
93
+ id: z.string().min(1),
94
+ /** Human-readable display name. */
95
+ name: z.string().min(1),
96
+ /** Which kind of runnable this entry describes. */
97
+ kind: RunnableKindSchema,
98
+ /**
99
+ * Optional per-kind config blob. Mirrors Core's `RunnableEntry.config`
100
+ * (`Option<serde_json::Value>`) so a manifest authored here round-trips
101
+ * through Core-strict validation. Left opaque (a record) at this authoring
102
+ * layer; the per-kind shape is enforced by Core's `validate_runnable`.
103
+ *
104
+ * For a `companion` runnable, `config.ui_entry` names the plugin's UI entry
105
+ * module (relative to the manifest dir). `ryu pack` bundles that entry into
106
+ * the emitted `ui_code`; Core's `CompanionConfig.ui_entry` is the lockstep
107
+ * field so a packed companion validates.
108
+ */
109
+ config: z.record(z.string(), z.unknown()).optional(),
110
+ });
111
+
112
+ export type RunnableMeta = z.infer<typeof RunnableMetaSchema>;
113
+
114
+ // ── CompanionSurface ─────────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * True when a companion `label` impersonates first-party Ryu/system chrome.
118
+ *
119
+ * Mirrors Core's `label_impersonates_system_chrome`
120
+ * (`apps/core/src/plugin_manifest/schema.rs`) and the desktop `validatePluginRoute`
121
+ * title gate (`apps/desktop/src/contributions/host/rpc.ts`): a plugin's visible
122
+ * label may not contain `"ryu"` or `"system"` (case-insensitive), so a third-party
123
+ * companion can never pose as built-in UI. The desktop host's mandatory,
124
+ * non-removable `"Plugin ·"` attribution prefix is the primary guarantee; this is
125
+ * defense in depth enforced at the authoring seam so a hostile label is rejected
126
+ * before `ryu pack`/publish rather than at load.
127
+ */
128
+ export function labelImpersonatesSystemChrome(label: string): boolean {
129
+ const lower = label.toLowerCase();
130
+ return lower.includes("ryu") || lower.includes("system");
131
+ }
132
+
133
+ /**
134
+ * Optional in-desktop overlay / sidebar panel descriptor. Mirrors
135
+ * `CompanionSurface` in `apps/core/src/plugin_manifest/mod.rs`.
136
+ */
137
+ export const CompanionSurfaceSchema = z.object({
138
+ /** Display label for the companion panel tab or tooltip. Anti-impersonation:
139
+ * may not pose as first-party Ryu/system chrome (see
140
+ * {@link labelImpersonatesSystemChrome}). */
141
+ label: z
142
+ .string()
143
+ .min(1)
144
+ .refine((value) => !labelImpersonatesSystemChrome(value), {
145
+ message:
146
+ "companion label must not impersonate system chrome (must not contain 'ryu' or 'system')",
147
+ }),
148
+ /** Icon identifier resolved by the desktop shell. */
149
+ icon: z.string().optional(),
150
+ /** Keyboard shortcut string (e.g. `"ctrl+shift+r"`). */
151
+ shortcut: z.string().optional(),
152
+ });
153
+
154
+ export type CompanionSurface = z.infer<typeof CompanionSurfaceSchema>;
155
+
156
+ // ── Contributes (turn hooks + declarative UI) ────────────────────────────────
157
+
158
+ /**
159
+ * A server-side chat turn hook. Mirrors `TurnHookContribution` in
160
+ * `apps/core/src/plugin_manifest/mod.rs`. `code` is a JS body run in the plugin
161
+ * sandbox with `ctx` + `host` in scope; it returns a directive. Authors usually
162
+ * build this via `defineTurnHook` rather than writing the string by hand.
163
+ */
164
+ export const TurnHookContributionSchema = z.object({
165
+ /** Stable id for this hook (unique within the plugin). */
166
+ id: z.string().min(1),
167
+ /** Turn boundary this fires on. Today only `"post_assistant_turn"`. */
168
+ on: z.string().min(1).default("post_assistant_turn"),
169
+ /** The JS hook body executed in the sandbox (returns a directive). */
170
+ code: z.string().min(1),
171
+ });
172
+
173
+ export type TurnHookContribution = z.infer<typeof TurnHookContributionSchema>;
174
+
175
+ // ── WidgetContribution (Ryu Apps) ─────────────────────────────────────────────
176
+
177
+ /** Default widget MIME dialect. Mirrors Core `default_widget_mime`. */
178
+ const DEFAULT_WIDGET_MIME = "text/html+skybridge";
179
+ /** Default widget display mode. Mirrors Core `default_widget_display_mode`. */
180
+ const DEFAULT_WIDGET_DISPLAY_MODE = "inline";
181
+
182
+ /**
183
+ * One app-widget contribution (Ryu Apps). Binds the render tool that produces the
184
+ * widget to its `ui://widget/<slug>.html` template. Shape-identical to Core's
185
+ * `WidgetContribution` (`apps/core/src/plugin_manifest/mod.rs`): built-in apps
186
+ * serve the HTML from the in-process provider and leave `ui_entry` unset, while a
187
+ * third-party app authored here sets `ui_entry` so `ryu pack` bundles the source
188
+ * into the manifest's `ui_code`.
189
+ */
190
+ export const WidgetContributionSchema = z.object({
191
+ /** The fully-qualified tool id whose result renders this widget. */
192
+ tool_id: z.string().min(1),
193
+ /** `ui://widget/<slug>.html` — the widget resource uri. */
194
+ uri: z.string().min(1),
195
+ /** Source entry (e.g. `src/apps/checklist/index.tsx`) for `ryu pack`. */
196
+ ui_entry: z.string().optional(),
197
+ /** Widget MIME dialect (default `text/html+skybridge`). */
198
+ mime: z.string().default(DEFAULT_WIDGET_MIME),
199
+ /** Default display mode (`inline` | `fullscreen` | `pip`). */
200
+ default_display_mode: z.string().default(DEFAULT_WIDGET_DISPLAY_MODE),
201
+ });
202
+
203
+ export type WidgetContribution = z.infer<typeof WidgetContributionSchema>;
204
+
205
+ // ── ToolAppConfig (Ryu Apps per-tool config) ─────────────────────────────────
206
+
207
+ /**
208
+ * The `config` blob carried by a Ryu App's `kind:"tool"` runnable. Core's strict
209
+ * `ToolConfig` (`apps/core/src/plugin_manifest/schema.rs`) requires `slug` and
210
+ * ignores unknown fields on the current shape; the widget flags below are read by
211
+ * Core's `register_app_tool_with_widget` synthesis path (a separate Core unit) to
212
+ * rebuild the `_meta` binding, mirroring how the in-process `apps::tools()`
213
+ * derives `outputTemplate` / `toolInvocation` / `widgetAccessible`.
214
+ */
215
+ export const ToolAppConfigSchema = z.object({
216
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
217
+ slug: z.string().min(1),
218
+ /** The tool description the model reads when choosing it. Carried here because a
219
+ * packed app's manifest is the only channel (there is no `generated.rs`); Core's
220
+ * app-tool synthesis reads it back onto the `RegistryTool`. */
221
+ description: z.string(),
222
+ /** JSON Schema for the tool's arguments (used for validation + the LLM tool
223
+ * surface). Snake_case to match `widget_accessible`. Absent = no arguments. */
224
+ input_schema: z.record(z.string(), z.unknown()).optional(),
225
+ /** True when calling this tool renders the app's widget (carries the template). */
226
+ widget: z.boolean().default(false),
227
+ /** True when a mounted widget may `callTool` this tool (a companion), or when a
228
+ * render tool's widget may call any companion the app declares. */
229
+ widget_accessible: z.boolean().default(false),
230
+ /** Optional status label shown while the render tool runs. */
231
+ invoking: z.string().optional(),
232
+ /** Optional status label shown when the render tool finishes. */
233
+ invoked: z.string().optional(),
234
+ });
235
+
236
+ export type ToolAppConfig = z.infer<typeof ToolAppConfigSchema>;
237
+
238
+ /**
239
+ * The `contributes` block. Mirrors `Contributes` in
240
+ * `apps/core/src/plugin_manifest/mod.rs`. The declarative UI surfaces
241
+ * (`composer_controls` / `settings_tabs` / `slash_commands`) are passed verbatim
242
+ * to the desktop renderer, so they are typed loosely here (records).
243
+ */
244
+ export const ContributesSchema = z.object({
245
+ turn_hooks: z.array(TurnHookContributionSchema).default([]),
246
+ composer_controls: z.array(z.record(z.string(), z.unknown())).default([]),
247
+ settings_tabs: z.array(z.record(z.string(), z.unknown())).default([]),
248
+ slash_commands: z.array(z.record(z.string(), z.unknown())).default([]),
249
+ /** App widgets (Ryu Apps). Each binds a render tool id to its
250
+ * `ui://widget/<slug>.html` template. Mirrors the Rust-side
251
+ * `Contributes.widgets` field, without which the CLI's zod parse would strip
252
+ * every widget an app authored here declares. */
253
+ widgets: z.array(WidgetContributionSchema).default([]),
254
+ });
255
+
256
+ export type Contributes = z.infer<typeof ContributesSchema>;
257
+
258
+ // ── SetupStep (listing companion/config card) ────────────────────────────────
259
+
260
+ /**
261
+ * One optional post-install setup/companion card step surfaced on the
262
+ * marketplace detail dialog (Phase 1.5 Ryu extension). All fields optional so a
263
+ * card can be a bare call-to-action or a labelled instruction. `ryu publish`
264
+ * forwards this into the publish body's `setup` field.
265
+ */
266
+ export const SetupStepSchema = z.object({
267
+ /** Card heading (e.g. the companion app name). */
268
+ title: z.string().optional(),
269
+ /** Instruction body shown under the title. */
270
+ description: z.string().optional(),
271
+ /** Label for the optional action button. */
272
+ actionLabel: z.string().optional(),
273
+ /** URL the action button opens (validated server-side on publish). */
274
+ actionUrl: z.string().optional(),
275
+ });
276
+
277
+ export type SetupStep = z.infer<typeof SetupStepSchema>;
278
+
279
+ // ── Requires (plugin-to-plugin dependencies) ─────────────────────────────────
280
+
281
+ /**
282
+ * A single plugin-to-plugin dependency edge. Mirrors `AppDependency` in
283
+ * `apps/core/src/plugin_manifest/mod.rs`.
284
+ *
285
+ * `min_version` is snake_case on the wire (Core declares no serde rename) and is
286
+ * a **minimum**, not a caret range: a bare `"1.2.0"` means `">=1.2.0"`, so an
287
+ * installed `2.0.0` satisfies it. Explicit comparator syntax (`">=1.2, <2"`,
288
+ * `"^1.2"`, `"~1.2"`) is honoured verbatim by Core's `parse_min_version`.
289
+ */
290
+ export const AppDependencySchema = z.object({
291
+ /** The `id` of the plugin this one depends on. */
292
+ id: z.string().min(1, "dependency id is required"),
293
+ /** Optional MINIMUM version the dependency must satisfy (`"1.2.0"` = `">=1.2.0"`). */
294
+ min_version: z.string().min(1).optional(),
295
+ });
296
+
297
+ export type AppDependency = z.infer<typeof AppDependencySchema>;
298
+
299
+ /**
300
+ * A single **capability** edge — the layered, provider-agnostic dependency
301
+ * (`requires: [{ capability: "rag" }]`) the capability broker resolves to a
302
+ * concrete provider app at bind time. Mirrors `CapabilityReq` in
303
+ * `crates/ryu-kernel-contracts/src/manifest.rs` (the canonical contract):
304
+ * `{ capability, min_version? }`. Distinct from an `apps` edge (which names a
305
+ * specific plugin id); a `capabilities` edge names an abstract capability and
306
+ * lets the binding registry pick — or the user override — which enabled provider
307
+ * serves it. This is the field the composable `defineAgent` slots lower to.
308
+ */
309
+ export const CapabilityReqSchema = z.object({
310
+ /** Capability name (e.g. `"rag"`, `"memory"`, `"tts"`). Matched against a
311
+ * provider's `provides[].capability`. */
312
+ capability: z.string().min(1, "capability name is required"),
313
+ /** Optional MINIMUM capability version the bound provider must satisfy
314
+ * (`"1.2.0"` = `">=1.2.0"`). Absent = any version. */
315
+ min_version: z.string().min(1).optional(),
316
+ });
317
+
318
+ export type CapabilityReq = z.infer<typeof CapabilityReqSchema>;
319
+
320
+ /**
321
+ * The `requires` block — this plugin's dependencies. Mirrors `Requires` in
322
+ * `apps/core/src/plugin_manifest/mod.rs`.
323
+ *
324
+ * Core resolves `apps` into a topological enable order (`plugins::graph`):
325
+ * enabling this plugin auto-enables its dependencies first, and disabling a
326
+ * dependency is REFUSED (409) while an enabled dependent still needs it.
327
+ *
328
+ * **Absent = no dependencies** — the backward-compatible default every manifest
329
+ * predating this field carries.
330
+ */
331
+ export const RequiresSchema = z.object({
332
+ /** Other plugins that must be installed + enabled before this one enables. */
333
+ apps: z.array(AppDependencySchema).default([]),
334
+ /**
335
+ * Abstract capability edges the broker resolves to a bound provider at
336
+ * enable time. Mirrors `Requires::capabilities` in
337
+ * `crates/ryu-kernel-contracts` — an `apps` edge names a specific plugin; a
338
+ * `capabilities` edge names a capability and lets the binding registry choose
339
+ * the provider. Each is lowered to an app-id graph edge once bound, so the
340
+ * enable/disable/cycle machinery is shared. Empty for the common case.
341
+ */
342
+ capabilities: z.array(CapabilityReqSchema).default([]),
343
+ /**
344
+ * Permission grants implied by the dependencies. Declaration only — the
345
+ * Gateway remains the sole authority on what a grant *allows*, and Core's
346
+ * dependency graph resolves `apps` only.
347
+ */
348
+ grants: z.array(z.string()).default([]),
349
+ });
350
+
351
+ export type Requires = z.infer<typeof RequiresSchema>;
352
+
353
+ // ── Surface (targets) ────────────────────────────────────────────────────────
354
+
355
+ /**
356
+ * A host surface a plugin can declare support for via `targets`. Mirrors Core's
357
+ * `Surface` enum (`#[serde(rename_all = "kebab-case")]`), so these eight tokens
358
+ * are the exact wire values — also the vocabulary of the `x-ryu-surface` request
359
+ * header Core filters listings on.
360
+ */
361
+ export const SurfaceSchema = z.enum([
362
+ /** The Ryu Gateway. */
363
+ "gateway",
364
+ /** A headless Core node (no UI). */
365
+ "core",
366
+ /** The Tauri desktop app. */
367
+ "desktop",
368
+ /** The Electron dynamic-island companion. */
369
+ "island",
370
+ /** The Expo/React-Native mobile app. */
371
+ "mobile",
372
+ /** The browser extension. */
373
+ "extension",
374
+ /** The Next.js web app. */
375
+ "web",
376
+ /** The terminal client. */
377
+ "cli",
378
+ ]);
379
+
380
+ export type Surface = z.infer<typeof SurfaceSchema>;
381
+
382
+ // ── PluginManifest ───────────────────────────────────────────────────────────
383
+
384
+ /**
385
+ * Full schema for a `plugin.json` Plugin manifest. Mirrors `PluginManifest` in
386
+ * `apps/core/src/plugin_manifest/mod.rs`.
387
+ *
388
+ * Validation rules (matching Core's `PluginManifestLoader`):
389
+ * - `id` must be non-empty
390
+ * - `version` must be a valid semver string (MAJOR.MINOR.PATCH)
391
+ * - `runnables` may be empty for a "surface-only" plugin, but each entry must be
392
+ * a valid `RunnableMeta`
393
+ */
394
+ export const PluginManifestSchema = z.object({
395
+ /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
396
+ id: z.string().min(1, "id is required"),
397
+
398
+ /** Human-readable display name shown in the plugin store / launcher. */
399
+ name: z.string().min(1, "name is required"),
400
+
401
+ /**
402
+ * Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
403
+ * whose version is not valid semver; the regex here enforces the same rule at
404
+ * SDK-build time.
405
+ */
406
+ version: z
407
+ .string()
408
+ .regex(
409
+ /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
410
+ "version must be a valid semver string (e.g. 1.0.0)"
411
+ ),
412
+
413
+ /**
414
+ * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
415
+ * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
416
+ * write it here BEFORE the manifest is signed, so the hash rides INSIDE the
417
+ * Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
418
+ * Core's install path recomputes the hash over the fetched code and rejects a
419
+ * mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
420
+ * Mirrors Core's `PluginManifest.ui_code_sha256`.
421
+ */
422
+ ui_code_sha256: z.string().nullish(),
423
+
424
+ /** The Runnables this plugin bundles. */
425
+ runnables: z.array(RunnableMetaSchema).default([]),
426
+
427
+ /**
428
+ * Permission grants this plugin declares it needs (e.g. `"mcp:web_search"`).
429
+ * Declarations only — grant enforcement is the Gateway's responsibility.
430
+ */
431
+ permission_grants: z.array(z.string()).default([]),
432
+
433
+ /**
434
+ * Optional Companion surface (an in-desktop overlay or sidebar panel).
435
+ * Absent when the plugin has no Companion surface.
436
+ */
437
+ companion: CompanionSurfaceSchema.optional(),
438
+
439
+ /**
440
+ * VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
441
+ * `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
442
+ * enabled flag, so `["*"]` is the usual value.
443
+ */
444
+ activation_events: z.array(z.string()).default([]),
445
+
446
+ /**
447
+ * Contribution points: server-side turn hooks + declarative UI widgets.
448
+ * Absent for a plugin that contributes nothing here.
449
+ */
450
+ contributes: ContributesSchema.optional(),
451
+
452
+ /**
453
+ * **Plugin-to-plugin dependencies** — the other plugins this one needs. Core
454
+ * resolves them into a topological enable order (dependencies enable first;
455
+ * disabling one is refused while an enabled dependent needs it).
456
+ *
457
+ * Absent = **no dependencies**, the backward-compatible default. Kept
458
+ * `.optional()` (never defaulted) so a manifest that declares none serialises
459
+ * with no `requires` key at all, exactly like Core's
460
+ * `#[serde(skip_serializing_if = "Option::is_none")]`.
461
+ */
462
+ requires: RequiresSchema.optional(),
463
+
464
+ /**
465
+ * Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
466
+ * surface** — the backward-compatible default, which must never be read as
467
+ * "runs nowhere". Core filters only when the list is explicitly non-empty, and
468
+ * only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
469
+ * an unsupported-target plugin stays installable and inspectable.
470
+ */
471
+ targets: z.array(SurfaceSchema).default([]),
472
+
473
+ /**
474
+ * Optional per-item AFFILIATE terms: the commission paid to a referrer when a
475
+ * referred user buys this (paid) item. `value` is basis points for `percent`
476
+ * (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
477
+ * falls back to the seller org owner's default affiliate terms. This is the
478
+ * authoring surface for the marketplace publish body's `affiliate` field (the
479
+ * server re-validates it); it only takes effect on a paid item.
480
+ */
481
+ affiliate: z
482
+ .object({
483
+ enabled: z.boolean().default(false),
484
+ rule: z
485
+ .object({
486
+ type: z.enum(["percent", "flat"]),
487
+ value: z.number().nonnegative(),
488
+ recurring: z.boolean().default(false),
489
+ durationMonths: z.number().int().positive().nullish(),
490
+ fundedBy: z.enum(["platform", "seller"]).default("platform"),
491
+ })
492
+ .optional(),
493
+ })
494
+ .optional(),
495
+
496
+ // ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
497
+ // Optional store-listing fields a plugin author declares so the marketplace
498
+ // detail dialog renders a richer App-Store-style preview. Field names align
499
+ // with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
500
+ // one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
501
+ // rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
502
+ // body (not inside the signed manifest blob) so the control plane stores them.
503
+ // All optional + additive: a manifest omitting them still validates.
504
+
505
+ /** Longer plain/markdown description shown in the detail dialog. */
506
+ description: z.string().optional(),
507
+ /** Short one-line pitch shown under the name (Ryu extension). */
508
+ tagline: z.string().optional(),
509
+ /**
510
+ * Publisher identity. A bare string OR a Claude-style object; `ryu publish`
511
+ * resolves it to the display `developer` (`author.name` when an object).
512
+ */
513
+ author: z
514
+ .union([
515
+ z.string(),
516
+ z.object({
517
+ name: z.string(),
518
+ email: z.string().optional(),
519
+ url: z.string().optional(),
520
+ }),
521
+ ])
522
+ .optional(),
523
+ /** Project/marketing homepage — maps to the listing `website` (Claude field). */
524
+ homepage: z.string().optional(),
525
+ /** Free-text search keywords (Claude field). */
526
+ keywords: z.array(z.string()).optional(),
527
+ /** Taxonomy category beyond the runnable kinds (Claude field). */
528
+ category: z.string().optional(),
529
+ /** SPDX-ish license identifier (Claude field). */
530
+ license: z.string().optional(),
531
+ /** Square logo/icon URL for the listing card + detail header. */
532
+ iconUrl: z.string().optional(),
533
+ /** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
534
+ screenshots: z.array(z.string()).optional(),
535
+ /** Privacy policy URL surfaced on detail (Ryu extension). */
536
+ privacyPolicyUrl: z.string().optional(),
537
+ /** Terms-of-service URL surfaced on detail (Ryu extension). */
538
+ termsOfServiceUrl: z.string().optional(),
539
+ /**
540
+ * Human-readable capability strings (Ryu extension). When omitted the control
541
+ * plane derives a default from `permission_grants`, so declaring this is only
542
+ * needed to override the derived labels.
543
+ */
544
+ capabilities: z.array(z.string()).optional(),
545
+ /** Example prompt chips shown on detail (Ryu extension). */
546
+ examplePrompts: z.array(z.string()).optional(),
547
+ /**
548
+ * Optional companion/config card (Ryu extension): a single setup step or an
549
+ * array of steps guiding the user through post-install configuration.
550
+ */
551
+ setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional(),
552
+ });
553
+
554
+ export type PluginManifest = z.infer<typeof PluginManifestSchema>;
555
+
556
+ // ── Rust-cored validation helpers (via @ryuhq/sdk-native) ───────────────────────
557
+ //
558
+ // These delegate to the `crates/ryu-sdk` Rust core through the native addon, so
559
+ // they apply the *exact same* rules Core enforces on load. Note: Core's manifest
560
+ // model uses richer per-kind `RunnableEntry` configs, while the zod
561
+ // `PluginManifestSchema` above models the SDK's simpler authoring shape
562
+ // (runnables = identity metadata only). Until those shapes are reconciled
563
+ // (follow-up), use the zod schema for SDK authoring and these helpers when you
564
+ // need Core-strict validation of a full `plugin.json`.
565
+
566
+ /**
567
+ * Validate a plugin id with Core's strict reverse-domain, path-traversal-safe
568
+ * rules. Throws a descriptive `Error` when invalid.
569
+ */
570
+ export function validatePluginId(id: string): void {
571
+ loadNative().validatePluginId(id);
572
+ }
573
+
574
+ /**
575
+ * Validate a full `plugin.json` string against Core's authoritative rules
576
+ * (id, semver, per-kind runnable config contracts). Returns the normalized
577
+ * manifest JSON string, or throws.
578
+ */
579
+ export function validateManifestStrict(manifestJson: string): string {
580
+ return loadNative().parseAndValidateManifest(manifestJson);
581
+ }
582
+
583
+ /**
584
+ * The Core-derived JSON Schema for a `plugin.json`, as a parsed object. Stays in
585
+ * lockstep with the Rust types because it is emitted from them.
586
+ */
587
+ export function coreManifestJsonSchema(): unknown {
588
+ return JSON.parse(loadNative().pluginManifestJsonSchema());
589
+ }