@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,1121 @@
1
+ /**
2
+ * GENERATED FILE — DO NOT EDIT.
3
+ *
4
+ * Source of truth: crates/ryu-kernel-contracts (Rust) via the checked-in
5
+ * schemas/plugin-manifest.schema.json. Regenerate with:
6
+ *
7
+ * bun run generate:contracts
8
+ *
9
+ * (after re-blessing the schema with
10
+ * `RYU_REGEN_SCHEMAS=1 cargo test -p ryu-kernel-contracts` when the Rust
11
+ * manifest types change).
12
+ */
13
+
14
+ /**
15
+ * A host surface a plugin can declare support for via `targets`.
16
+ *
17
+ * `core` is the headless node (a Core running with no UI at all).
18
+ *
19
+ * An **empty/absent** `targets` list means the plugin runs on *every* surface —
20
+ * that is the backward-compatible default and MUST NOT be read as "hidden".
21
+ */
22
+ export type Surface = "gateway" | "core" | "desktop" | "island" | "mobile" | "extension" | "web" | "cli";
23
+
24
+ /**
25
+ * An installable Ryu App manifest (`plugin.json`).
26
+ *
27
+ * Modelled on Codex's `plugin.json` pattern: a thin descriptor that bundles one or
28
+ * more [`RunnableEntry`] items (agents, workflows, tools, skills, companions,
29
+ * channels, engines, policies), lists the permission grants the app requires, and
30
+ * optionally declares a Companion surface (an in-desktop overlay or sidebar panel).
31
+ *
32
+ * # Per-kind config
33
+ *
34
+ * Each Runnable entry carries an optional `config` blob whose schema is
35
+ * determined by its `kind`. See [`crate::schema`] for the per-kind structs and the
36
+ * [`crate::schema::validate_runnable`] function.
37
+ */
38
+ export interface PluginManifest {
39
+ /**
40
+ * Activation events that lazily wake the plugin — VS-Code `activationEvents`.
41
+ * Recognised tokens: `"*"` (always active / eager), `"onStartup"`, `"onChat"`,
42
+ * `"onCommand:<id>"`, `"onRoute"` (fired the first time a lazy sidecar is woken
43
+ * by an inbound proxy hit), and `"onCapabilityCall"` (the broker analogue —
44
+ * fired when a lazy provider sidecar is woken by a capability-broker hit). An
45
+ * **empty** list means *eager* activation (back-compat: every existing manifest
46
+ * keeps activating on enable). The activation runtime firing these events lives
47
+ * in Core's `RunnableRegistry::register_active` + `fire_activation_event`;
48
+ * `onStartup`/`onChat`/`onRoute`/`onCapabilityCall` fire from Core, while
49
+ * `onCommand:<id>` fires from the desktop command palette.
50
+ */
51
+ activation_events?: string[];
52
+ /**
53
+ * Publisher/author. Claude `author` — a bare string or an object with a
54
+ * `name` field; the detail builder extracts the display string into
55
+ * `developer`. Kept as a raw value so both shapes round-trip.
56
+ */
57
+ author?: {
58
+ [k: string]: unknown;
59
+ };
60
+ /**
61
+ * The plugin's **backend bundle** — the JavaScript source of the extension-host
62
+ * entry module a [`crate::schema::SidecarProcess::Node`] sidecar runs (RFC Option
63
+ * B). This is the backend analogue of `ui_code`: a payload blob that Core writes
64
+ * to the plugin dir at the node sidecar's declared `entry` path at spawn, then
65
+ * loads via the embedded host bootstrap. Unlike `ui_code` (which the install path
66
+ * splits into a DB column so the on-disk manifest stays small), the backend blob
67
+ * rides **inline** in the manifest so the spawn path is self-contained (it reads
68
+ * the reconstituted manifest, no separate carriage channel) AND, for a
69
+ * marketplace plugin, the code is INSIDE the Gateway-signed surface — the whole
70
+ * backend is signed, not merely hash-bound. Absent for a plugin with no node
71
+ * backend. Written by `ryu pack`/`ryu publish`.
72
+ */
73
+ backend_code?: string | null;
74
+ /**
75
+ * Lower-case hex `sha256(utf8_bytes(backend_code))` — the integrity gate for the
76
+ * node backend, mirroring [`ui_code_sha256`]. When present, Core recomputes the
77
+ * hash over the on-disk entry file at spawn and **refuses to start** the node
78
+ * sidecar on mismatch (fail-closed), so an entry file swapped on disk between
79
+ * install and spawn can never run. Absent = trust the bundle as written (the same
80
+ * posture `ui_code_sha256` uses when omitted).
81
+ *
82
+ * [`ui_code_sha256`]: PluginManifest::ui_code_sha256
83
+ */
84
+ backend_sha256?: string | null;
85
+ /**
86
+ * Human-readable capability strings (Ryu extension). When absent the detail
87
+ * builder DERIVES these from `permission_grants` via
88
+ * [`crate::schema::capabilities_from_grants`]; declared values are used verbatim.
89
+ */
90
+ capabilities?: string[];
91
+ /**
92
+ * Free-text category (Claude `category`).
93
+ */
94
+ category?: string | null;
95
+ /**
96
+ * Optional Companion surface descriptor: an in-desktop overlay or sidebar panel
97
+ * the app may register. Absent when the app has no Companion surface.
98
+ */
99
+ companion?: CompanionSurface | null;
100
+ /**
101
+ * VS-Code-style **contribution points**: a declare-by-id block naming which
102
+ * of the manifest's `runnables` the plugin contributes to each extensible
103
+ * surface. Every id referenced here MUST exist in `runnables` (the loader
104
+ * cross-validates). Absent when the plugin contributes nothing extra
105
+ * (the common case — a plugin's `runnables` are already its contributions).
106
+ */
107
+ contributes?: Contributes | null;
108
+ /**
109
+ * Long plaintext/markdown description. Empty when absent (the built-in card
110
+ * historically emitted `""` for this; preserved).
111
+ */
112
+ description?: string | null;
113
+ /**
114
+ * Required Ryu engine version (VS-Code `engines.vscode` analogue). When
115
+ * present, `engines.ryu` is a semver **requirement** (e.g. `">=0.3.0"`) and
116
+ * the loader rejects the manifest if the running Core version does not
117
+ * satisfy it. Absent = compatible with any Core version.
118
+ */
119
+ engines?: EnginesReq | null;
120
+ /**
121
+ * Prompt-chip examples (contract key `examplePrompts`; Ryu extension).
122
+ */
123
+ examplePrompts?: string[];
124
+ /**
125
+ * Homepage/website URL (Claude `homepage`; emitted as `website`).
126
+ */
127
+ homepage?: string | null;
128
+ /**
129
+ * Logo URL (contract key `iconUrl`; Ryu extension).
130
+ */
131
+ iconUrl?: string | null;
132
+ /**
133
+ * Reverse-domain unique identifier for the app (e.g. `"com.example.my-app"`).
134
+ */
135
+ id: string;
136
+ /**
137
+ * Search keywords / tags (Claude `keywords`).
138
+ */
139
+ keywords?: string[];
140
+ /**
141
+ * SPDX license identifier (Claude `license`).
142
+ */
143
+ license?: string | null;
144
+ /**
145
+ * Human-readable display name shown in the app store / launcher.
146
+ */
147
+ name: string;
148
+ /**
149
+ * Permission grants this app declares it needs (e.g. `"mcp:web_search"`).
150
+ * These are *declarations only* at this layer — no enforcement happens here;
151
+ * the Gateway owns grant enforcement.
152
+ */
153
+ permission_grants?: string[];
154
+ /**
155
+ * **Unified, deny-by-default runtime permission set** — the single typed
156
+ * grammar (`{fs, child_process, network, tool}`) Core lowers to every sandbox
157
+ * backend (wasmtime WASI preopens, Docker `--mount`/`--network` flags, Deno
158
+ * `--allow-*` flags). Absent = **deny-all** (the default for every manifest
159
+ * predating this field), so an app that declares nothing keeps today's exact
160
+ * zero-permission sandbox posture.
161
+ *
162
+ * # Relationship to [`permission_grants`]
163
+ *
164
+ * These are **two distinct lanes** that must not be conflated:
165
+ * - [`permission_grants`] are opaque strings the **Gateway** approves at
166
+ * install/enable time — the *approval* lane (who is allowed to ask).
167
+ * - `permissions` is the typed set **Core** lowers into the actual sandbox at
168
+ * spawn/exec time — the *runtime-enforcement* lane (what the code can touch).
169
+ *
170
+ * A grant says "this app may use the filesystem capability"; `permissions.fs`
171
+ * says "…and here are the exact read/write paths the sandbox is opened with."
172
+ *
173
+ * # Altitude (manifest-level, per-runnable override is a followup)
174
+ *
175
+ * Declared at the manifest root because **both** current enforcement sites
176
+ * resolve their config from the owning manifest, not from a sub-entry: an
177
+ * `inline_deno` tool's backend is resolved from the manifest by
178
+ * `McpRegistry::resolve_app_tool_backend`, and a managed sidecar is spawned
179
+ * from the manifest by `ManifestSidecar`. A per-[`crate::schema::ToolConfig`] /
180
+ * per-[`crate::schema::SidecarSpec`] override is a clean future extension (the
181
+ * resolver would fall back to this manifest-level set) but is intentionally not
182
+ * in v1.
183
+ */
184
+ permissions?: PermissionSet | null;
185
+ /**
186
+ * Privacy policy URL (contract key `privacyPolicyUrl`; Ryu extension).
187
+ */
188
+ privacyPolicyUrl?: string | null;
189
+ /**
190
+ * **Capabilities this plugin provides** — the inverse of
191
+ * [`Requires::capabilities`]. Each entry names a capability the plugin's
192
+ * sidecar can serve for other plugins through the capability broker, binding
193
+ * the capability to one of this manifest's declared `sidecars` + a proxied
194
+ * route. Absent/empty for the common case (a plugin that consumes but does not
195
+ * provide capabilities). The loader cross-validates that every referenced
196
+ * `sidecar`/`route` exists (like `contributes`).
197
+ */
198
+ provides?: ProvidesEntry[];
199
+ /**
200
+ * **Plugin-to-plugin dependencies** — the other plugins this one needs (the
201
+ * npm-shaped edge that lets the app decompose into a kernel + features).
202
+ * Resolved into a topological enable order by Core's `plugins::graph`.
203
+ *
204
+ * Absent = **no dependencies** (every manifest predating this field).
205
+ */
206
+ requires?: Requires | null;
207
+ /**
208
+ * The Runnables this app bundles. Each entry uses [`RunnableEntry`] from the
209
+ * [`crate::schema`] module so heterogeneous Runnables (agents, workflows,
210
+ * tools, skills, companions, channels, engines, policies) can be listed
211
+ * together with their per-kind config.
212
+ */
213
+ runnables: RunnableEntry[];
214
+ /**
215
+ * Optional declarative **external runtime** the plugin needs (e.g. a Python
216
+ * venv + pip deps + assets, like the TTS sidecar). The provisioner lives in
217
+ * Core (`crate::sidecar::external_runtime`); this is the declaration (#449).
218
+ * Absent for the common case (no external interpreter needed).
219
+ */
220
+ runtime?: ExternalRuntimeConfig | null;
221
+ /**
222
+ * App-Store gallery screenshot URLs (Ryu extension).
223
+ */
224
+ screenshots?: string[];
225
+ /**
226
+ * Optional companion/config setup card, or an array of such steps (Ryu
227
+ * extension). Opaque to Core — passed through to the detail payload verbatim.
228
+ */
229
+ setup?: {
230
+ [k: string]: unknown;
231
+ };
232
+ /**
233
+ * Declarative **managed sidecars** the plugin ships (the app ⇄ sidecar
234
+ * bridge): each is a long-running child process Core downloads/provisions,
235
+ * spawns, and health-monitors via the Core `SidecarManager` on enable,
236
+ * exactly like a built-in sidecar. Gated at enable by the `sidecar:process`
237
+ * grant (Core-tier auto; Community needs the approved grant). Empty for the
238
+ * common case (no bundled process).
239
+ */
240
+ sidecars?: SidecarSpec[];
241
+ /**
242
+ * Per-surface support + UI declaration — the richer successor to [`targets`].
243
+ *
244
+ * When **present**, this map is authoritative and [`targets`] is ignored: a
245
+ * surface is supported iff it has an entry whose [`SurfaceSupport`] is not
246
+ * [`SurfaceSupport::None`], and an **absent key means the surface is not
247
+ * supported** (see [`PluginManifest::supports_surface`]). When **absent**, the
248
+ * predicate falls back to the legacy [`targets`] semantics (empty/absent =
249
+ * every surface) — so every manifest that predates this field keeps its exact
250
+ * behaviour. Never make an absent `surfaces` mean "no surfaces".
251
+ *
252
+ * [`targets`]: PluginManifest::targets
253
+ */
254
+ surfaces?: {
255
+ [k: string]: SurfaceEntry;
256
+ } | null;
257
+ /**
258
+ * Short one-line tagline shown under the name (Ryu extension).
259
+ */
260
+ tagline?: string | null;
261
+ /**
262
+ * Host surfaces this plugin runs on (desktop / island / mobile / …).
263
+ *
264
+ * **Empty or absent = runs on EVERY surface.** This is the backward-compatible
265
+ * default and must never be read as "runs nowhere" — every manifest that
266
+ * predates this field declares no targets and must keep surfacing everywhere.
267
+ * Filtering happens ONLY when this list is explicitly non-empty, and only at
268
+ * the read/surface boundary (see [`PluginManifest::supports_surface`]) — never
269
+ * in the storage layer, so an unsupported-target plugin stays installable and
270
+ * inspectable.
271
+ */
272
+ targets?: Surface[];
273
+ /**
274
+ * Terms-of-service URL (contract key `termsOfServiceUrl`; Ryu extension).
275
+ */
276
+ termsOfServiceUrl?: string | null;
277
+ /**
278
+ * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
279
+ * sandboxed-UI code to this manifest. Because the Gateway signs the manifest
280
+ * verbatim (canonical key-sorted encoding), this hash is INSIDE the signed
281
+ * surface while the `ui_code` blob itself rides OUTSIDE it as payload; the
282
+ * install path recomputes the hash over the fetched code and rejects a
283
+ * mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI) and
284
+ * for unsigned seed items. Written by `ryu pack`/`ryu publish`.
285
+ */
286
+ ui_code_sha256?: string | null;
287
+ /**
288
+ * Semver version string (e.g. `"1.0.0"`).
289
+ */
290
+ version: string;
291
+ }
292
+ /**
293
+ * Companion surface descriptor — an optional in-desktop overlay or sidebar panel
294
+ * an App may register. Fields mirror the UX primitives a Companion widget needs;
295
+ * all are optional except `label`.
296
+ */
297
+ export interface CompanionSurface {
298
+ /**
299
+ * Icon identifier (resolved by the desktop shell).
300
+ */
301
+ icon?: string | null;
302
+ /**
303
+ * Display label for the companion panel tab or tooltip.
304
+ */
305
+ label: string;
306
+ /**
307
+ * Keyboard shortcut string (e.g. `"ctrl+shift+r"`).
308
+ */
309
+ shortcut?: string | null;
310
+ }
311
+ /**
312
+ * VS-Code-style **contribution points** (`contributes` in `package.json`).
313
+ *
314
+ * Each field is a list of [`ContributionId`] references into the manifest's
315
+ * `runnables`: the plugin *declares* that runnable `X` contributes to the
316
+ * `commands`/`tools`/`agents`/… surface. This is declare-by-id, not a second
317
+ * copy of the runnable — the loader cross-validates that every referenced id
318
+ * exists in `runnables`, so a typo is caught at load.
319
+ *
320
+ * # Extending
321
+ *
322
+ * Add a new surface = add a new `#[serde(default)] pub <surface>: Vec<ContributionId>`
323
+ * field here. The cross-validation in [`Contributes::referenced_ids`] picks it
324
+ * up automatically.
325
+ */
326
+ export interface Contributes {
327
+ /**
328
+ * Agents the plugin contributes (referenced by runnable id).
329
+ */
330
+ agents?: ContributionId[];
331
+ /**
332
+ * Command-palette commands the plugin contributes (referenced by runnable id).
333
+ */
334
+ commands?: ContributionId[];
335
+ /**
336
+ * Declarative **native** UI widgets the plugin contributes to the desktop
337
+ * composer (e.g. a `toggle` that sets a `plugin_flags` entry, or a `chip`).
338
+ * Core stores these verbatim and serves them via `GET /api/plugins/contributions`;
339
+ * the desktop renders the known widget types. Opaque to Core (the renderer
340
+ * owns interpretation) so new widget types need no Core change.
341
+ */
342
+ composer_controls?: unknown[];
343
+ /**
344
+ * Gateway policies the plugin contributes (referenced by runnable id).
345
+ */
346
+ policies?: ContributionId[];
347
+ /**
348
+ * Declarative settings tabs the plugin contributes (model pickers, text
349
+ * fields bound to preference keys). Served + rendered the same way.
350
+ */
351
+ settings_tabs?: unknown[];
352
+ /**
353
+ * Slash commands the plugin contributes (e.g. `/goal`). The desktop maps the
354
+ * command to a `plugin_flags`/message action; the plugin's turn hook reads
355
+ * the resulting message. Served + rendered the same way.
356
+ */
357
+ slash_commands?: unknown[];
358
+ /**
359
+ * Callable tools the plugin contributes (referenced by runnable id).
360
+ */
361
+ tools?: ContributionId[];
362
+ /**
363
+ * Chat turn hooks the plugin contributes — server-side logic that runs at a
364
+ * turn boundary (e.g. `post_assistant_turn`) and returns a directive. These
365
+ * are **self-contained** (they carry their own inline `code`), so they are
366
+ * NOT cross-validated against `runnables` like the id-reference surfaces
367
+ * above; the Core `plugin_host` runtime executes them in the sandbox.
368
+ */
369
+ turn_hooks?: TurnHookContribution[];
370
+ /**
371
+ * **Declarative views** the plugin contributes (the Raycast tier). Each entry
372
+ * is a [`ViewContribution`]: a typed envelope (`id`/`view`) around an **opaque**
373
+ * `spec` payload the host renderer interprets. The app returns DATA
374
+ * (`items`/`columns`/`actions`/`fields`) — never code — and the shell renders it
375
+ * with the host's own `@ryu/ui` components (desktop) or the compact command-bar
376
+ * idiom (island), so one spec renders natively on every surface and cannot be
377
+ * made ugly. Like [`composer_controls`]/[`settings_tabs`] this is **self-contained**
378
+ * (not cross-validated against `runnables`), and the `view` discriminant + `spec`
379
+ * stay opaque to Core so a new view kind needs no Core change — the renderer owns
380
+ * the vocabulary (`list-detail`, `data-table`, `form`, `action-panel`,
381
+ * `filter-bar`, `empty-state`, `stat-card-row`).
382
+ *
383
+ * [`composer_controls`]: Contributes::composer_controls
384
+ * [`settings_tabs`]: Contributes::settings_tabs
385
+ */
386
+ views?: ViewContribution[];
387
+ /**
388
+ * App widgets the plugin contributes (Ryu Apps). Each binds a tool id to a
389
+ * `ui://widget/<slug>.html` template the tool renders inline in chat. The
390
+ * field is shape-identical to the SDK `manifest.ts` `WidgetContribution`.
391
+ */
392
+ widgets?: WidgetContribution[];
393
+ /**
394
+ * Workflows the plugin contributes (referenced by runnable id).
395
+ */
396
+ workflows?: ContributionId[];
397
+ }
398
+ /**
399
+ * A single contribution: a reference (by `id`) to a runnable declared in the
400
+ * manifest's `runnables` list, optionally with a human-facing title (e.g. the
401
+ * label a command shows in the palette).
402
+ */
403
+ export interface ContributionId {
404
+ /**
405
+ * The runnable id this contribution points at. Must exist in `runnables`.
406
+ */
407
+ id: string;
408
+ /**
409
+ * Optional display title (e.g. the palette label for a command).
410
+ */
411
+ title?: string | null;
412
+ }
413
+ /**
414
+ * A server-side chat turn hook contributed by a plugin. The `code` is a JS body
415
+ * run in the plugin sandbox with `ctx` (the turn context) and `host` (the
416
+ * capability bridge: `host.sideModel`, `host.storage`, `host.log`) in scope; it
417
+ * returns a directive (`{kind:"none"}` | `{kind:"note",text}` |
418
+ * `{kind:"continue",text}`). See Core's `plugin_host`.
419
+ */
420
+ export interface TurnHookContribution {
421
+ /**
422
+ * The JS hook body executed in the sandbox (returns a directive).
423
+ */
424
+ code: string;
425
+ /**
426
+ * Stable id for this hook (for logging/audit), unique within the plugin.
427
+ */
428
+ id: string;
429
+ /**
430
+ * Optional cheap pre-gate. When present, Core's `plugin_host` evaluates it
431
+ * in Rust **before** spawning the sandbox, so an idle hook (e.g. double-check
432
+ * with its toggle off, or goal with no active condition) costs a flag/prefix
433
+ * check or one KV read instead of a Deno process. This is what makes it safe
434
+ * to ship these hooks **enabled by default** on every surface. Absent (or all
435
+ * fields empty) → the hook always runs, preserving prior behaviour.
436
+ */
437
+ match?: HookMatch | null;
438
+ /**
439
+ * The turn boundary this hook fires on. Today only `"post_assistant_turn"`.
440
+ */
441
+ on: string;
442
+ }
443
+ /**
444
+ * A declarative pre-gate for a [`TurnHookContribution`]. The conditions are
445
+ * OR-ed: the hook runs if **any** present condition matches. An empty match
446
+ * (every field default) means "always run". Kept intentionally small — richer
447
+ * matching belongs inside the hook JS, this only exists to skip the sandbox
448
+ * spawn on turns where the hook provably cannot act.
449
+ */
450
+ export interface HookMatch {
451
+ /**
452
+ * Run if the last user message (trimmed) starts with any of these prefixes,
453
+ * e.g. `["/goal"]`. This is how a slash-command hook wakes up.
454
+ */
455
+ commands?: string[];
456
+ /**
457
+ * Run only if the request set this composer flag true (`ctx.flags[flag]`),
458
+ * e.g. `"io.ryu.double-check"`.
459
+ */
460
+ flag?: string | null;
461
+ /**
462
+ * Run if the plugin has stored state for this conversation (its default KV
463
+ * namespace has a value keyed by `conversation_id`), e.g. an active goal.
464
+ */
465
+ stateful?: boolean;
466
+ /**
467
+ * Run if the tool being called (`ctx.tool_name`) matches any of these
468
+ * patterns — for `pre_tool_use` / `post_tool_use` hooks. A pattern is a tool
469
+ * id with optional leading/trailing `*` wildcards (`"*"` = every tool,
470
+ * `"bash*"` = ids starting with `bash`). This keeps a tool-firewall hook from
471
+ * spawning the sandbox on every unrelated tool call.
472
+ */
473
+ tools?: string[];
474
+ }
475
+ /**
476
+ * One **declarative view** contribution (the Raycast tier — see [`Contributes::views`]).
477
+ *
478
+ * A typed envelope around an opaque `spec`: Core stores it verbatim, tags it with
479
+ * the owning `plugin` id at `GET /api/plugins/contributions`, and forwards it to the
480
+ * surface shell, which maps `view` + `spec` to native components. The `spec` shape is
481
+ * owned by the shared TS vocabulary (`@ryu/app-host/views`), NOT by this contract, so
482
+ * adding a view kind is a renderer change, never a Core change.
483
+ */
484
+ export interface ViewContribution {
485
+ /**
486
+ * Stable id for this view within the plugin (route/anchor key, unique per plugin).
487
+ */
488
+ id: string;
489
+ /**
490
+ * The DATA payload for the view (items/columns/actions/fields/…). Opaque to Core
491
+ * — the shared renderer interprets it per the `view` kind. Absent = an empty view.
492
+ */
493
+ spec?: {
494
+ [k: string]: unknown;
495
+ };
496
+ /**
497
+ * Optional human-facing title (tab label / palette entry). Absent = the shell
498
+ * derives one from the view kind or the plugin name.
499
+ */
500
+ title?: string | null;
501
+ /**
502
+ * The vocabulary member this view renders as — the discriminant the per-surface
503
+ * renderer switches on (`"list-detail"`, `"data-table"`, `"form"`,
504
+ * `"action-panel"`, `"filter-bar"`, `"empty-state"`, `"stat-card-row"`). Opaque
505
+ * to Core; an unknown kind is passed through so a newer shell can render it.
506
+ */
507
+ view: string;
508
+ }
509
+ /**
510
+ * One app-widget contribution (Ryu Apps). Binds the tool that renders the widget
511
+ * to its HTML template. `ui_entry` is the source entry the SDK `ryu pack` builds
512
+ * into the self-contained HTML for third-party apps; built-in apps serve HTML
513
+ * from the in-process provider and leave it unset.
514
+ */
515
+ export interface WidgetContribution {
516
+ /**
517
+ * Default display mode (`inline` | `fullscreen` | `pip`).
518
+ */
519
+ default_display_mode?: string;
520
+ /**
521
+ * Widget MIME dialect (default `text/html+skybridge`).
522
+ */
523
+ mime?: string;
524
+ /**
525
+ * The fully-qualified tool id whose result renders this widget.
526
+ */
527
+ tool_id: string;
528
+ /**
529
+ * Source entry (e.g. `src/apps/checklist/index.tsx`) for `ryu pack`.
530
+ */
531
+ ui_entry?: string | null;
532
+ /**
533
+ * `ui://widget/<slug>.html` — the widget resource uri.
534
+ */
535
+ uri: string;
536
+ }
537
+ /**
538
+ * `engines` block — the required Ryu version, mirroring VS-Code's
539
+ * `engines.vscode`. `ryu` is a semver **requirement** string.
540
+ */
541
+ export interface EnginesReq {
542
+ /**
543
+ * Semver requirement the running Core version must satisfy (e.g. `">=0.3.0"`,
544
+ * `"^1.2"`). Parsed as a [`semver::VersionReq`]; an unparseable value or an
545
+ * unsatisfied requirement causes the loader to reject the manifest.
546
+ */
547
+ ryu: string;
548
+ }
549
+ /**
550
+ * The single, typed, **deny-by-default** permission set a plugin manifest
551
+ * declares, lowered by Core to every sandbox backend.
552
+ *
553
+ * This is the one grammar that replaces three historically-disjoint ones:
554
+ * the wasmtime/Docker [`crate`]-external `SandboxCapabilities` (typed but
555
+ * unreachable from a manifest), the Deno PTC's hardcoded zero-allow-flag spawn,
556
+ * and the opaque grant strings. A manifest declares ONE `permissions` block and
557
+ * Core lowers it to WASI preopens, Docker mount/network flags, or Deno
558
+ * `--allow-*` flags as appropriate.
559
+ *
560
+ * **Every field defaults to empty/false — the zero value is deny-all.** A missing
561
+ * `permissions` block (or an explicit `{}`) is byte-for-byte the same posture as
562
+ * today's zero-permission sandbox, which is what preserves the existing live
563
+ * deny-all tests.
564
+ */
565
+ export interface PermissionSet {
566
+ /**
567
+ * Whether the sandboxed code may spawn child processes. `false` (default) =
568
+ * no subprocess execution. Lowers to Deno's `--allow-run`; the wasmtime/Docker
569
+ * lowering has no subprocess channel to open, so this is a no-op there (a WASI
570
+ * module cannot fork, and the Docker exec is a single fixed argv).
571
+ */
572
+ child_process?: boolean;
573
+ fs?: FsPermissions;
574
+ /**
575
+ * Outbound network permission. `false`/absent (default) = no network; `true` =
576
+ * all hosts; a list of `host[:port]` entries = only those hosts (the shape
577
+ * Deno's `--allow-net` supports). See [`NetworkPermission`].
578
+ */
579
+ network?: boolean | string[];
580
+ /**
581
+ * **Declaration-only** in v1: the registry tool ids this plugin's sandboxed
582
+ * code may call through the stdio `tools.*` bridge. Tools are brokered over
583
+ * stdout/stdin by Core (never an OS capability), so this does NOT lower to any
584
+ * `--allow-*` flag; it records intent and is a clean future extension for the
585
+ * `SandboxToolInvoker` allowlist. Empty (default) records no extra tool intent.
586
+ */
587
+ tool?: string[];
588
+ }
589
+ /**
590
+ * Filesystem read/write path allowlists. Empty = no FS access.
591
+ */
592
+ export interface FsPermissions {
593
+ /**
594
+ * Absolute paths the sandbox may **read**. Empty = no read access.
595
+ */
596
+ read?: string[];
597
+ /**
598
+ * Absolute paths the sandbox may **write**. Empty = no write access.
599
+ */
600
+ write?: string[];
601
+ }
602
+ /**
603
+ * One **provided capability** entry (in [`PluginManifest::provides`]).
604
+ *
605
+ * Binds an abstract capability name to a concrete serving surface on THIS
606
+ * manifest: the local `sidecar` name whose declared HTTP `route` implements the
607
+ * capability, plus the `grant` a consumer must hold to invoke it. The broker
608
+ * routes a consumer's `/api/host/capability/<cap>` call to this sidecar's route
609
+ * using the *provider's* minted token — the consumer never sees it.
610
+ */
611
+ export interface ProvidesEntry {
612
+ /**
613
+ * The capability name this plugin serves (e.g. `"rag"`). Consumers match on
614
+ * this against their [`Requires::capabilities`].
615
+ */
616
+ capability: string;
617
+ /**
618
+ * The grant a consumer must hold (Gateway-approved) to invoke this capability
619
+ * via the broker. Absent = no extra grant beyond declaring the edge.
620
+ */
621
+ grant?: string | null;
622
+ /**
623
+ * The proxied sub-path (on the named sidecar's [`crate::schema::HttpProxySpec`])
624
+ * the broker forwards capability calls to (e.g. `"/rag/query"`). The loader
625
+ * cross-validates that the named sidecar declares a matching route.
626
+ */
627
+ route?: string | null;
628
+ /**
629
+ * The local `name` of one of this manifest's declared `sidecars` that serves
630
+ * the capability. The loader cross-validates it exists. Absent = an in-process
631
+ * capability with no dedicated sidecar (the broker declines to proxy it).
632
+ */
633
+ sidecar?: string | null;
634
+ /**
635
+ * The capability's own semver version (independent of the plugin version), so
636
+ * a consumer's [`CapabilityReq::min_version`] floor can be checked against the
637
+ * capability contract rather than the app release.
638
+ */
639
+ version: string;
640
+ }
641
+ /**
642
+ * `requires` block — the plugin's **plugin-to-plugin** dependencies.
643
+ *
644
+ * This is the npm-shaped edge that lets the app decompose into a minimal kernel
645
+ * plus features: a plugin declares the other plugins it needs, and the lifecycle
646
+ * (Core's `plugins::graph`) resolves them into a topological enable order.
647
+ *
648
+ * Distinct from [`EnginesReq`], which constrains plugin→**Core** (the engine
649
+ * version). `requires` constrains plugin→**plugin**.
650
+ *
651
+ * Absent (the default, and the case for every manifest that predates this field)
652
+ * means *no dependencies* — the plugin enables standalone exactly as before.
653
+ */
654
+ export interface Requires {
655
+ /**
656
+ * Other plugins that must be installed (and are auto-enabled, in dependency
657
+ * order) before this one can enable.
658
+ */
659
+ apps?: AppDependency[];
660
+ /**
661
+ * **Capabilities** this plugin requires — the layered, provider-agnostic edge
662
+ * (`requires: [rag]`) that the capability broker resolves to a concrete
663
+ * provider app at bind time. Distinct from [`apps`]: an `apps` edge names a
664
+ * specific plugin id; a `capabilities` edge names an abstract capability and
665
+ * lets the binding registry pick (or the user override) which enabled provider
666
+ * serves it. Each is lowered to an app-id graph edge once bound, so the
667
+ * topological enable/disable/cycle machinery is shared. Empty for the common
668
+ * case.
669
+ *
670
+ * [`apps`]: Requires::apps
671
+ */
672
+ capabilities?: CapabilityReq[];
673
+ /**
674
+ * Permission grants implied by the dependencies. Declaration only — the
675
+ * Gateway remains the sole authority on what a grant *allows* (Core decides
676
+ * what runs; the Gateway decides what is permitted).
677
+ */
678
+ grants?: string[];
679
+ }
680
+ /**
681
+ * A single plugin-to-plugin dependency edge.
682
+ */
683
+ export interface AppDependency {
684
+ /**
685
+ * The `id` of the plugin this one depends on.
686
+ */
687
+ id: string;
688
+ /**
689
+ * Optional **minimum** version the dependency must satisfy.
690
+ *
691
+ * A bare version (`"1.2.0"`) is a *minimum*, i.e. `">=1.2.0"` — deliberately
692
+ * NOT semver's default caret (`^1.2.0`), which would reject `2.0.0`. Explicit
693
+ * comparator syntax (`">=1.2, <2"`, `"^1.2"`, `"~1.2"`) is honoured verbatim.
694
+ * See [`parse_min_version`], the single parser both validation and resolution
695
+ * use.
696
+ */
697
+ min_version?: string | null;
698
+ }
699
+ /**
700
+ * One **required capability** edge (in [`Requires::capabilities`]).
701
+ *
702
+ * Names an abstract capability plus an optional minimum *capability* version. The
703
+ * version floor is checked at bind time against the bound provider's
704
+ * [`ProvidesEntry::version`] — NOT against the provider plugin's own semver — so a
705
+ * lowered graph edge carries no `min_version` (the app-version gate would compare
706
+ * the wrong number). See the capability broker in Core.
707
+ */
708
+ export interface CapabilityReq {
709
+ /**
710
+ * The capability name (e.g. `"rag"`, `"tts"`). Matched against a provider's
711
+ * [`ProvidesEntry::capability`].
712
+ */
713
+ capability: string;
714
+ /**
715
+ * Optional minimum **capability** version the bound provider must satisfy
716
+ * (bare `"1.2.0"` = `">=1.2.0"`, via [`parse_min_version`]). Absent = any
717
+ * version of the capability is acceptable.
718
+ */
719
+ min_version?: string | null;
720
+ }
721
+ /**
722
+ * A single Runnable entry inside a `plugin.json` manifest.
723
+ *
724
+ * Each entry carries the identity fields from [`crate::runnable::RunnableMeta`]
725
+ * plus an optional typed config blob. The `kind` field drives which config shape
726
+ * is expected; validation via [`validate_runnable`] checks that
727
+ * required-per-kind fields are present.
728
+ */
729
+ export interface RunnableEntry {
730
+ /**
731
+ * Per-kind configuration. Some kinds (e.g. `agent`) treat this as
732
+ * optional (sensible defaults apply); others (e.g. `tool`, `workflow`)
733
+ * require it. [`validate_runnable`] enforces the rules.
734
+ */
735
+ config?: {
736
+ [k: string]: unknown;
737
+ };
738
+ /**
739
+ * Stable unique identifier within this app (e.g. `"tool-web-search"`).
740
+ */
741
+ id: string;
742
+ /**
743
+ * Discriminant that determines which per-kind config struct is required.
744
+ */
745
+ kind: "agent" | "workflow" | "tool" | "skill" | "companion" | "channel" | "engine" | "policy";
746
+ /**
747
+ * Human-readable display name.
748
+ */
749
+ name: string;
750
+ }
751
+ /**
752
+ * code surface the Gateway must permit before it runs.
753
+ */
754
+ export interface ExternalRuntimeConfig {
755
+ /**
756
+ * Assets to fetch into `~/.ryu` before first run.
757
+ */
758
+ assets?: AssetSpec[];
759
+ /**
760
+ * The module/entrypoint to run (e.g. `"ryu_tts"` → `python -m ryu_tts`).
761
+ */
762
+ entry: string;
763
+ /**
764
+ * Environment variables layered onto the runtime process at spawn. Values may
765
+ * use `${RYU_DIR}` — expanded to the Core data dir (`~/.ryu`) at spawn — so a
766
+ * runtime can point caches/outputs at Core-owned paths without hardcoding an
767
+ * absolute path in the (portable) manifest. Nothing else is interpolated.
768
+ */
769
+ env?: {
770
+ [k: string]: string;
771
+ };
772
+ /**
773
+ * Health-check path on the runtime's server (e.g. `"/health"`).
774
+ */
775
+ health_path?: string | null;
776
+ /**
777
+ * Runtime kind. `"python"` is the only provisionable kind today; others are
778
+ * accepted (round-trip) but provisioning returns an "unsupported" error.
779
+ *
780
+ * Defaults to `"python"` so this config can be nested inside the internally
781
+ * `#[serde(tag = "kind")]`-tagged [`SidecarProcess::Python`] variant: there the
782
+ * outer enum consumes the `"kind"` key as its discriminant, so the inner field
783
+ * would otherwise be reported missing — the classic internally-tagged collision.
784
+ * Standalone use still round-trips an explicit `kind`.
785
+ */
786
+ kind?: string;
787
+ /**
788
+ * Port the runtime's HTTP server binds to (adopt-or-spawn check).
789
+ */
790
+ port?: number | null;
791
+ /**
792
+ * Optional env var the Python child reads for its **bind port**. When set, Core
793
+ * injects `<port_env> = profile-shifted([`SidecarSpec::port`])` at spawn, so the
794
+ * child binds the same profile-aware port Core health-checks + proxies to — the
795
+ * Python-sidecar analogue of [`LocalProcessSpec::port_env`] (without it a static
796
+ * port env collides across concurrent Core profiles).
797
+ */
798
+ port_env?: string | null;
799
+ /**
800
+ * Optional pyproject *extra* to install (`pip install -e ".[<extra>]"`).
801
+ */
802
+ pyproject_extra?: string | null;
803
+ /**
804
+ * Optional Python version hint (e.g. `"3.11"`). Advisory.
805
+ */
806
+ python_version?: string | null;
807
+ /**
808
+ * pip requirement specs to install into the venv.
809
+ */
810
+ requirements?: string[];
811
+ /**
812
+ * Optional **source archive** to extract into the runtime dir before the venv
813
+ * is built. Needed when the entry module is a *first-party package the plugin
814
+ * ships* (not on PyPI): a `pip install -e ".[extra]"` needs the package's
815
+ * `pyproject.toml` + sources on disk first. Single-file `assets` cannot deliver
816
+ * a source tree; this does. Omit for a pure-PyPI runtime.
817
+ */
818
+ source?: SourceArchiveSpec | null;
819
+ }
820
+ /**
821
+ * A single asset an external runtime needs, fetched before first run. Either a
822
+ * direct https URL or an `hf:<owner>/<repo>/<path>` reference; `dest_under_ryu`
823
+ * is the relative directory beneath `~/.ryu` where it lands (Core-owned) — the
824
+ * filename is derived from the source's last path segment.
825
+ */
826
+ export interface AssetSpec {
827
+ /**
828
+ * Destination directory relative to `~/.ryu` (e.g. `"models/hf"`); the
829
+ * fetched file lands at `~/.ryu/<dest_under_ryu>/<filename>`. Must be a
830
+ * traversal-safe relative path (no `..`, not absolute).
831
+ */
832
+ dest_under_ryu: string;
833
+ /**
834
+ * Optional SHA-256 for checksum verification (direct-URL assets).
835
+ */
836
+ sha256?: string | null;
837
+ /**
838
+ * A direct **https** URL, or an `hf:<owner>/<repo>/<path>` reference to a
839
+ * single file on the Hub. A repo-only `hf:<owner>/<repo>` ref (no file path)
840
+ * is **not** provisionable yet — full-repo snapshot needs Hub tree-listing
841
+ * that is not wired into the provisioner. The provisioner
842
+ * (`crate::sidecar::external_runtime`) rejects `http://` and other schemes.
843
+ */
844
+ source: string;
845
+ }
846
+ /**
847
+ * A source-tree archive an external runtime extracts into its runtime dir before
848
+ * provisioning (venv + `pip install -e .`). Distinct from [`AssetSpec`], which
849
+ * fetches a *single file* into `~/.ryu`; this delivers a whole package tree the
850
+ * plugin owns.
851
+ */
852
+ export interface SourceArchiveSpec {
853
+ /**
854
+ * Archive format: `"tar.gz"` or `"zip"`. Extracted whole-tree into the runtime
855
+ * dir so the package's `pyproject.toml` lands at its root.
856
+ */
857
+ format: string;
858
+ /**
859
+ * Optional lower-case-hex SHA-256 of the archive; when present the download is
860
+ * verified and re-fetched on mismatch (fail-closed).
861
+ */
862
+ sha256?: string | null;
863
+ /**
864
+ * Direct **https** URL to the archive. Non-https is rejected by the SSRF egress
865
+ * screen at download time.
866
+ */
867
+ url: string;
868
+ }
869
+ /**
870
+ * A declarative **managed sidecar** a plugin may declare: a long-running child
871
+ * process Core owns end-to-end (download/provision → spawn → health-check →
872
+ * stop), registered into the Core `SidecarManager` on enable so it rides the
873
+ * *same* managed lifecycle (health monitor + resource sampler +
874
+ * `/api/sidecar/status`) as a built-in sidecar.
875
+ *
876
+ * This is the **app ⇄ sidecar bridge**: it lets a capability sidecar (ghost,
877
+ * shadow, a TTS engine, …) be a fully manifest-defined app instead of hardcoded
878
+ * Rust, and lets a third-party app ship its own process under a Gateway grant.
879
+ * Infra sidecars (llama.cpp, the gateway, embeddings) stay Core substrate and are
880
+ * deliberately NOT expressible here.
881
+ *
882
+ * The process is obtained one of two ways ([`SidecarProcess`]): a downloaded
883
+ * **binary**, or a **Python** runtime (reusing [`ExternalRuntimeConfig`] — venv +
884
+ * pip + assets). Both are gated at enable by the `sidecar:process` grant; nothing
885
+ * is hardcoded — the binary URL, args, env, port, and health path are all data.
886
+ */
887
+ export interface SidecarSpec {
888
+ /**
889
+ * Health-check path on the process's server (default `"/health"`). A GET to
890
+ * `http://127.0.0.1:<port><health_path>` returning 2xx marks it healthy.
891
+ */
892
+ health_path?: string;
893
+ /**
894
+ * Optional **host-API** declaration: the subset of the owning plugin's approved
895
+ * grants the sidecar *process* may exercise via an authenticated callback into
896
+ * Core (`/api/host/*`, bearer = the plugin's minted `RYU_EXT_TOKEN`). Absent =
897
+ * the sidecar may not call back into Core at all (deny-all). Additive.
898
+ */
899
+ host_api?: HostApiSpec | null;
900
+ /**
901
+ * Optional **HTTP proxy** declaration: when present, Core exposes a public
902
+ * reverse-proxy front (`/api/ext/<plugin_id>/*`) onto this sidecar, so a
903
+ * manifest-declared sidecar becomes a full first-class *app* reachable by any
904
+ * client — the generic form of the hand-coded `ryu-mail` proxy. Absent = the
905
+ * sidecar is an internal capability with no external HTTP surface (only Core's
906
+ * own health probe reaches it). Additive: existing sidecars get `None`.
907
+ */
908
+ http?: HttpProxySpec | null;
909
+ /**
910
+ * **Idle-stop timeout**, in seconds — scale-to-zero for this sidecar. When set,
911
+ * Core stops the process after it has served no request for this long (and has
912
+ * none in flight); the next proxy/broker hit wakes it again (see [`lazy`]). Must
913
+ * be `>= 30` (a shorter window churns the process). Absent = never idle-stopped
914
+ * by manifest declaration (the operator-level [`RYU_SIDECAR_IDLE_SECS`] env can
915
+ * still opt a sidecar in). Additive; independent of [`lazy`] — an eager sidecar
916
+ * may declare an idle timeout and will then wake-on-demand after a reap.
917
+ *
918
+ * [`lazy`]: SidecarSpec::lazy
919
+ * [`RYU_SIDECAR_IDLE_SECS`]: the manager's env-seeded idle config.
920
+ */
921
+ idle_stop_secs?: number | null;
922
+ /**
923
+ * **Lazy activation** — spawn-on-first-use instead of at plugin-enable. When
924
+ * `true` the sidecar is *registered* (claims its port, appears in
925
+ * `/api/sidecar/status` as not-running) at enable but its process is NOT started
926
+ * until the first proxy/broker hit wakes it on demand; a bounded health-wait
927
+ * warms it before the request is forwarded. `false` (the default) keeps the
928
+ * eager behaviour every existing manifest has: started at enable. Additive.
929
+ */
930
+ lazy?: boolean;
931
+ /**
932
+ * Local name, unique within the plugin. Namespaced to `<plugin_id>/<name>` at
933
+ * registration so it never collides with a built-in sidecar or another
934
+ * plugin's. Must be a safe single path segment (no `/`, `\`, `..`, or NUL).
935
+ */
936
+ name: string;
937
+ /**
938
+ * TCP port the process's HTTP server binds to, used to build the health-check
939
+ * URL. The plugin is responsible for choosing a free port — there is **no port
940
+ * registry in v1**, so a collision with a built-in (e.g. llama.cpp on 8080) is
941
+ * the plugin author's responsibility to avoid.
942
+ */
943
+ port: number;
944
+ /**
945
+ * How Core obtains and runs the process.
946
+ */
947
+ process: BinarySpec | ExternalRuntimeConfig1 | LocalProcessSpec | NodeProcessSpec;
948
+ }
949
+ /**
950
+ * Declares the host-API grant subset a sidecar *process* may exercise via the
951
+ * authenticated `/api/host/*` callback into Core. The listed grants are the ceiling;
952
+ * Core still intersects them with the plugin's *approved* grants (post-Gateway
953
+ * validation) at call time, so a manifest can never widen its own authority here.
954
+ */
955
+ export interface HostApiSpec {
956
+ /**
957
+ * The grant strings (same vocabulary as `permission_grants`, e.g.
958
+ * `"hook:side-model"`) the sidecar backend may exercise via `/api/host/*`.
959
+ */
960
+ grants?: string[];
961
+ }
962
+ /**
963
+ * Declares the reverse-proxy front Core mounts onto a [`SidecarSpec`]. This is the
964
+ * **data** form of what `apps/core/src/sidecar/mail.rs` hand-codes: the exact set of
965
+ * external routes and their per-route auth posture. Core rejects any request whose
966
+ * sub-path is not one of [`routes`] (404), preserving mail's exact-route safety as a
967
+ * declaration instead of a hardcoded router.
968
+ *
969
+ * [`routes`]: HttpProxySpec::routes
970
+ */
971
+ export interface HttpProxySpec {
972
+ /**
973
+ * Maximum request body Core will buffer and forward, in bytes. Absent ⇒ Core's
974
+ * conservative default. Caps the proxy's memory exposure per request.
975
+ */
976
+ max_body_bytes?: number | null;
977
+ /**
978
+ * Optional path prefix prepended to the forwarded sub-path when Core builds the
979
+ * upstream URL on the sidecar (e.g. `mount = "/api/mail"` turns an external
980
+ * `/api/ext/<id>/status` into an upstream `/api/mail/status`). Absent/empty ⇒
981
+ * the sub-path after `/api/ext/<plugin_id>` is forwarded verbatim. Must start
982
+ * with `/` when present.
983
+ */
984
+ mount?: string | null;
985
+ /**
986
+ * Optional **public mount** — a stable, externally-committed URL prefix under
987
+ * which Core ALSO exposes this sidecar's routes, instead of only the generic
988
+ * `/api/ext/<plugin_id>/*` catch-all (e.g. `"/api/mail"` for a mail app whose
989
+ * inbound-webhook URL is baked into an external forwarder). Registered at
990
+ * `create_router` build time and only honoured for **built-in** manifests
991
+ * (axum routers are immutable after serve, so a runtime-installed third-party
992
+ * app cannot claim a custom prefix — it keeps `/api/ext/<id>/*`). Absent = no
993
+ * public mount (the common case). The routes + per-route auth are the SAME
994
+ * [`routes`] list; this only changes the public prefix they answer on.
995
+ *
996
+ * [`routes`]: HttpProxySpec::routes
997
+ */
998
+ public_mount?: string | null;
999
+ /**
1000
+ * The exact set of proxied routes. Each entry's [`RouteSpec::path`] is matched
1001
+ * against the incoming sub-path (the segment after `/api/ext/<plugin_id>`),
1002
+ * supporting `:param` and trailing `*rest` wildcards. A request whose sub-path
1003
+ * matches **none** of these is refused with 404 — undeclared paths are never
1004
+ * forwarded (the security property that makes this a safe generalization of the
1005
+ * mail proxy's fixed route list).
1006
+ */
1007
+ routes?: RouteSpec[];
1008
+ }
1009
+ /**
1010
+ * One declared proxied route: a path pattern plus its auth posture.
1011
+ */
1012
+ export interface RouteSpec {
1013
+ /**
1014
+ * Auth posture for this route. Defaults to [`RouteAuth::Protected`] (secure by
1015
+ * default): the request must carry the node bearer exactly as any other
1016
+ * protected Core route. `public` opts a route out (e.g. an HMAC-authed inbound
1017
+ * webhook whose external caller cannot hold the node token).
1018
+ */
1019
+ auth?: "protected" | "public";
1020
+ /**
1021
+ * Path pattern for the sub-path after `/api/ext/<plugin_id>` (must start with
1022
+ * `/`). Supports `:param` (matches one non-empty segment) and a trailing
1023
+ * `*rest` (matches the remainder), mirroring axum/matchit patterns so a
1024
+ * sidecar's REST routes (`/inboxes/:id`) can be declared faithfully.
1025
+ */
1026
+ path: string;
1027
+ }
1028
+ /**
1029
+ * A single downloaded executable: fetched (checksum-verified) into the
1030
+ * plugin's `bin/` dir, made executable, then spawned with `args` + `env`.
1031
+ */
1032
+ export interface BinarySpec {
1033
+ kind: "binary";
1034
+ }
1035
+ /**
1036
+ * A Python runtime: the existing external-runtime provisioner (venv + pip +
1037
+ * assets) builds the environment, then `python -m <entry>` is spawned.
1038
+ * Reuses [`ExternalRuntimeConfig`] verbatim (its `port`/`health_path` are
1039
+ * ignored here — the [`SidecarSpec`]'s own fields drive the health check).
1040
+ */
1041
+ export interface ExternalRuntimeConfig1 {
1042
+ kind: "python";
1043
+ }
1044
+ /**
1045
+ * A binary **already present on the host** — a sibling Ryu ships alongside Core
1046
+ * (e.g. `ryu-mail`), or something on `PATH`. Spawned directly with **no download**.
1047
+ * This is the escape hatch for first-party sidecars built in the same repo, which
1048
+ * have no release-artifact URL. Not for third-party apps (they should declare a
1049
+ * downloadable [`Binary`]).
1050
+ *
1051
+ * [`Binary`]: SidecarProcess::Binary
1052
+ */
1053
+ export interface LocalProcessSpec {
1054
+ kind: "local";
1055
+ }
1056
+ /**
1057
+ * A **managed JavaScript backend** — the extension-host runtime (RFC Option B).
1058
+ * Core spawns a small first-party bootstrap (embedded in the binary) under `bun`
1059
+ * (preferred) or `node`, which loads the plugin's declared `entry` module and
1060
+ * calls its exported `activate(context)`; the module may register an HTTP request
1061
+ * handler that the `/api/ext/<id>/*` proxy forwards to. The `entry` bundle rides
1062
+ * as the owning manifest's `backend_code` payload (mirroring `ui_code`) and is
1063
+ * written to the plugin dir + integrity-checked against `backend_sha256` at spawn.
1064
+ * Because it is still a [`SidecarSpec`] it inherits the whole managed lifecycle
1065
+ * (lazy/wake, idle-stop, health monitor, PATH cap-shims, per-plugin `RYU_EXT_*`
1066
+ * token, `RouteAuth` proxying). Gated by the experimental-plugin-runtime flag and,
1067
+ * for Community-tier plugins, by the `sidecar:process` grant exactly like a binary.
1068
+ */
1069
+ export interface NodeProcessSpec {
1070
+ kind: "node";
1071
+ }
1072
+ /**
1073
+ * One [`PluginManifest::surfaces`] entry: the support level plus an optional UI
1074
+ * descriptor the surface shell resolves (opaque here — pure data).
1075
+ */
1076
+ export interface SurfaceEntry {
1077
+ /**
1078
+ * Terminal subcommands this app contributes to the `cli` surface (the TUI's
1079
+ * `ryu <app> <cmd>` dispatcher). Only meaningful on the `cli` surface entry;
1080
+ * ignored on other surfaces. Empty/absent = the app contributes no commands.
1081
+ */
1082
+ commands?: CliCommandSpec[];
1083
+ /**
1084
+ * How much of the plugin this surface supports.
1085
+ */
1086
+ support?: "full" | "limited" | "list" | "commands" | "none";
1087
+ /**
1088
+ * Optional surface-specific UI descriptor (bundle id, mount point, …),
1089
+ * interpreted by the surface's app host. Opaque to the contract.
1090
+ */
1091
+ ui?: {
1092
+ [k: string]: unknown;
1093
+ };
1094
+ }
1095
+ /**
1096
+ * One terminal subcommand an app contributes to the `cli` surface (the TUI's
1097
+ * `ryu <app> <cmd>` dispatcher). Routed through Core's `ext_proxy` to the app's
1098
+ * sidecar: Core forwards `<method> /api/ext/<plugin_id><path>`. `path` MUST be a
1099
+ * route the app's sidecar declares in `http.routes`, or the proxy 404s.
1100
+ */
1101
+ export interface CliCommandSpec {
1102
+ /**
1103
+ * HTTP method for the `ext_proxy` call. Absent = `POST`.
1104
+ */
1105
+ method?: string | null;
1106
+ /**
1107
+ * Subcommand token, e.g. `status` in `ryu mail status`.
1108
+ */
1109
+ name: string;
1110
+ /**
1111
+ * Sub-path appended after `/api/ext/<plugin_id>`. Validated by
1112
+ * [`validate_cli_command_path`] at manifest load: it MUST be an absolute
1113
+ * (`/`-leading), traversal-free sub-path — no `..` segment in any form — so it
1114
+ * cannot escape the plugin's proxy scope when a URL parser normalizes it.
1115
+ */
1116
+ path: string;
1117
+ /**
1118
+ * One-line help shown in `ryu <app>` / `ryu <app> --help`.
1119
+ */
1120
+ summary?: string | null;
1121
+ }