@ryuhq/sdk 0.0.5 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.cjs +4 -1
- package/dist/agent.d.cts +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{chunk-KPKMMGVC.js → chunk-MTUBUPIV.js} +4 -1
- package/dist/{chunk-GXHL5CO7.js → chunk-XTUK5I6I.js} +110 -2
- package/dist/cli.cjs +163 -10
- package/dist/cli.js +56 -9
- package/dist/{index-DAxq7Y0R.d.ts → index-B6SkaAjJ.d.ts} +4 -4
- package/dist/{index-CEbS1SlS.d.cts → index-BvAB5eMk.d.cts} +4 -4
- package/dist/index.cjs +167 -9
- package/dist/index.d.cts +30 -12
- package/dist/index.d.ts +30 -12
- package/dist/index.js +57 -8
- package/dist/manifest.cjs +112 -2
- package/dist/manifest.d.cts +131 -10
- package/dist/manifest.d.ts +131 -10
- package/dist/manifest.js +5 -1
- package/package.json +4 -4
- package/src/builder.ts +4 -4
- package/src/cli.ts +98 -13
- package/src/contracts-lockstep.test.ts +4 -4
- package/src/generated/plugin-manifest.ts +1284 -24
- package/src/manifest-schema.test.ts +182 -0
- package/src/manifest.fixtures.test.ts +447 -0
- package/src/manifest.test.ts +100 -16
- package/src/manifest.ts +189 -17
- package/src/plugin/ryu-plugin.ts +1 -1
- package/src/runnable/agent.ts +3 -3
- package/src/runnable/app.test.ts +67 -0
- package/src/runnable/app.ts +58 -4
- package/src/runnable/primitives.test.ts +1 -5
- package/src/runnable/primitives.ts +6 -5
- package/src/runnable/tool.ts +4 -4
- package/src/runnable/turn-hook.ts +33 -4
|
@@ -11,6 +11,36 @@
|
|
|
11
11
|
* manifest types change).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* One selectable option for a [`SettingsFieldType::Select`] field.
|
|
16
|
+
*
|
|
17
|
+
* Accepts both spellings the desktop's `parseOptions` accepts: a bare string
|
|
18
|
+
* (value and label are the same) or an object with an explicit `label`. Keeping
|
|
19
|
+
* both is not indulgence — the bare-string form is what every hand-written
|
|
20
|
+
* manifest reaches for, and rejecting it would push authors into boilerplate for
|
|
21
|
+
* the common case.
|
|
22
|
+
*/
|
|
23
|
+
export type SettingsFieldOption =
|
|
24
|
+
| string
|
|
25
|
+
| {
|
|
26
|
+
/**
|
|
27
|
+
* Display label. Absent = show the raw `value`.
|
|
28
|
+
*/
|
|
29
|
+
label?: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* The value persisted to the preference key.
|
|
32
|
+
*/
|
|
33
|
+
value: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What a capability provider acts on — see [`ProvidesEntry::target`].
|
|
37
|
+
*
|
|
38
|
+
* Deliberately two coarse values rather than a taxonomy. The only question a user
|
|
39
|
+
* needs answered before swapping is "will this act on the machine in front of me,
|
|
40
|
+
* or somewhere else?", and a finer vocabulary (container / VM / cloud / another
|
|
41
|
+
* host) would be guesswork the manifests cannot honestly support.
|
|
42
|
+
*/
|
|
43
|
+
export type ProviderTarget = "local-machine" | "remote-desktop";
|
|
14
44
|
/**
|
|
15
45
|
* A host surface a plugin can declare support for via `targets`.
|
|
16
46
|
*
|
|
@@ -19,12 +49,12 @@
|
|
|
19
49
|
* An **empty/absent** `targets` list means the plugin runs on *every* surface —
|
|
20
50
|
* that is the backward-compatible default and MUST NOT be read as "hidden".
|
|
21
51
|
*/
|
|
22
|
-
export type Surface = "gateway" | "core" | "desktop" | "island" | "mobile" | "extension" | "web" | "cli";
|
|
52
|
+
export type Surface = "gateway" | "core" | "desktop" | "island" | "mobile" | "extension" | "web" | "cli" | "unknown";
|
|
23
53
|
|
|
24
54
|
/**
|
|
25
|
-
* An installable Ryu App manifest (`
|
|
55
|
+
* An installable Ryu App manifest (`manifest.json`).
|
|
26
56
|
*
|
|
27
|
-
* Modelled on Codex's `
|
|
57
|
+
* Modelled on Codex's `manifest.json` pattern: a thin descriptor that bundles one or
|
|
28
58
|
* more [`RunnableEntry`] items (agents, workflows, tools, skills, companions,
|
|
29
59
|
* channels, engines, policies), lists the permission grants the app requires, and
|
|
30
60
|
* optionally declares a Companion surface (an in-desktop overlay or sidebar panel).
|
|
@@ -36,6 +66,10 @@ export type Surface = "gateway" | "core" | "desktop" | "island" | "mobile" | "ex
|
|
|
36
66
|
* [`crate::schema::validate_runnable`] function.
|
|
37
67
|
*/
|
|
38
68
|
export interface PluginManifest {
|
|
69
|
+
/**
|
|
70
|
+
* Primary brand accent color, hex (Ryu extension: `accentColor`).
|
|
71
|
+
*/
|
|
72
|
+
accentColor?: string | null;
|
|
39
73
|
/**
|
|
40
74
|
* Activation events that lazily wake the plugin — VS-Code `activationEvents`.
|
|
41
75
|
* Recognised tokens: `"*"` (always active / eager), `"onStartup"`, `"onChat"`,
|
|
@@ -82,6 +116,12 @@ export interface PluginManifest {
|
|
|
82
116
|
* [`ui_code_sha256`]: PluginManifest::ui_code_sha256
|
|
83
117
|
*/
|
|
84
118
|
backend_sha256?: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Detail-page hero banner spec ({colors,style,seed}); opaque passthrough (Ryu ext).
|
|
121
|
+
*/
|
|
122
|
+
banner?: {
|
|
123
|
+
[k: string]: unknown;
|
|
124
|
+
};
|
|
85
125
|
/**
|
|
86
126
|
* Human-readable capability strings (Ryu extension). When absent the detail
|
|
87
127
|
* builder DERIVES these from `permission_grants` via
|
|
@@ -125,6 +165,29 @@ export interface PluginManifest {
|
|
|
125
165
|
* Homepage/website URL (Claude `homepage`; emitted as `website`).
|
|
126
166
|
*/
|
|
127
167
|
homepage?: string | null;
|
|
168
|
+
/**
|
|
169
|
+
* Icon-primitive id for the listing card (Ryu extension: `icon`). An
|
|
170
|
+
* Iconify/icons0 `prefix:name`, a bare Hugeicons name, or a URL — resolved by
|
|
171
|
+
* the shared `Icon` primitive. Distinct from `icon_url`: this is a GLYPH id the
|
|
172
|
+
* card masks with `currentColor`, `icon_url` is a raster logo. When absent the
|
|
173
|
+
* card falls back to `icon_url`, then a default glyph.
|
|
174
|
+
*/
|
|
175
|
+
icon?: string | null;
|
|
176
|
+
/**
|
|
177
|
+
* CSS background for the icon square (Ryu extension: `iconBackground`).
|
|
178
|
+
*/
|
|
179
|
+
iconBackground?: string | null;
|
|
180
|
+
/**
|
|
181
|
+
* Dithered-gradient background for the card's icon square (Ryu extension:
|
|
182
|
+
* `iconDither`). Opaque passthrough `{ from, to?, direction? }` mirroring
|
|
183
|
+
* dither-kit's `DitherGradient` props (`from`/`to` are a palette-colour name or
|
|
184
|
+
* a hue number, `direction` is up|down|left|right). Kept as raw JSON like
|
|
185
|
+
* `banner` so an untrusted/typo'd value never fails the manifest parse — the
|
|
186
|
+
* render layer validates and falls back before painting.
|
|
187
|
+
*/
|
|
188
|
+
iconDither?: {
|
|
189
|
+
[k: string]: unknown;
|
|
190
|
+
};
|
|
128
191
|
/**
|
|
129
192
|
* Logo URL (contract key `iconUrl`; Ryu extension).
|
|
130
193
|
*/
|
|
@@ -141,6 +204,20 @@ export interface PluginManifest {
|
|
|
141
204
|
* SPDX license identifier (Claude `license`).
|
|
142
205
|
*/
|
|
143
206
|
license?: string | null;
|
|
207
|
+
/**
|
|
208
|
+
* Declarative **stdio MCP servers** this plugin registers into Core's MCP
|
|
209
|
+
* registry on enable and deregisters on disable/uninstall. Each entry is a
|
|
210
|
+
* [`McpServerDecl`] keyed by the server name the registry uses (the same key a
|
|
211
|
+
* user's `mcp.json` would use). This is the manifest-owned successor to Core's
|
|
212
|
+
* hardcoded built-in MCP servers: a plugin declares its server here instead of
|
|
213
|
+
* Core baking a `com.ryu.<app>` server into `builtin_servers()`. Empty for the
|
|
214
|
+
* common case (a plugin that ships no MCP server). A user `mcp.json` entry with
|
|
215
|
+
* the same name still wins (user-overrides-builtin precedence is preserved by
|
|
216
|
+
* the registry).
|
|
217
|
+
*/
|
|
218
|
+
mcp_servers?: {
|
|
219
|
+
[k: string]: McpServerDecl;
|
|
220
|
+
};
|
|
144
221
|
/**
|
|
145
222
|
* Human-readable display name shown in the app store / launcher.
|
|
146
223
|
*/
|
|
@@ -238,6 +315,15 @@ export interface PluginManifest {
|
|
|
238
315
|
* common case (no bundled process).
|
|
239
316
|
*/
|
|
240
317
|
sidecars?: SidecarSpec[];
|
|
318
|
+
/**
|
|
319
|
+
* Provenance hint for the marketplace index: `"builtin"`, an `owner/repo`
|
|
320
|
+
* slug, or a git/raw URL an external plugin ships from. Absent ⇒ `"builtin"`.
|
|
321
|
+
* This is an index HINT only — Core derives the real trust tier from
|
|
322
|
+
* `plugins::builtins` membership at runtime, NOT from this field. Consumed by
|
|
323
|
+
* the marketplace generator (`tools/mirror-plugins.sh`) to populate each
|
|
324
|
+
* entry's `source`/`builtin` pair.
|
|
325
|
+
*/
|
|
326
|
+
source?: string | null;
|
|
241
327
|
/**
|
|
242
328
|
* Per-surface support + UI declaration — the richer successor to [`targets`].
|
|
243
329
|
*
|
|
@@ -311,17 +397,40 @@ export interface CompanionSurface {
|
|
|
311
397
|
/**
|
|
312
398
|
* VS-Code-style **contribution points** (`contributes` in `package.json`).
|
|
313
399
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* `
|
|
317
|
-
* copy of the runnable — the loader cross-validates
|
|
318
|
-
* exists in `runnables`, so a typo is caught at load.
|
|
400
|
+
* The original five surfaces (`commands`/`tools`/`agents`/`workflows`/`policies`)
|
|
401
|
+
* are lists of [`ContributionId`] references into the manifest's `runnables`: the
|
|
402
|
+
* plugin *declares* that runnable `X` contributes to that surface. This is
|
|
403
|
+
* declare-by-id, not a second copy of the runnable — the loader cross-validates
|
|
404
|
+
* that every referenced id exists in `runnables`, so a typo is caught at load.
|
|
405
|
+
*
|
|
406
|
+
* Most surfaces added since are **self-contained**: they carry their own payload
|
|
407
|
+
* and reference no runnable at all (`widgets`, `views`, `dock_panels`,
|
|
408
|
+
* `sidebar_sections`, `sidebar_buttons`, `settings_tabs`, `composer_controls`,
|
|
409
|
+
* `slash_commands`, `turn_hooks`, `tool_filters`, `lsp_servers`).
|
|
319
410
|
*
|
|
320
411
|
* # Extending
|
|
321
412
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
413
|
+
* Adding a surface is two decisions, and getting either wrong is silent:
|
|
414
|
+
*
|
|
415
|
+
* 1. **Id-reference or self-contained?** An id-reference surface is a
|
|
416
|
+
* `Vec<ContributionId>` and MUST be chained into [`Contributes::referenced_ids`]
|
|
417
|
+
* so the loader can catch a typo. A self-contained surface must be left OUT of
|
|
418
|
+
* it — every id in it names something other than a runnable (a PATH binary, a
|
|
419
|
+
* route, a tool namespace), so including it would reject every valid manifest.
|
|
420
|
+
* `referenced_ids` therefore covers exactly the five original surfaces and
|
|
421
|
+
* nothing else; that omission is deliberate, not an oversight to be tidied up.
|
|
422
|
+
* 2. **Core-interpreted or client-rendered?** If Core acts on the payload
|
|
423
|
+
* (`tool_filters`, `turn_hooks`, `widgets`, `lsp_servers`) it gets a fully typed
|
|
424
|
+
* struct, because a key Core does not know is by construction a key Core cannot
|
|
425
|
+
* act on. If a client shell renders it (`views`, `dock_panels`,
|
|
426
|
+
* `sidebar_sections`, `settings_tabs`, `composer_controls`) it stays opaque
|
|
427
|
+
* JSON, because deserializing into a struct here would DROP any key this Core
|
|
428
|
+
* build does not know about and a newer desktop would lose exactly the fields it
|
|
429
|
+
* was shipped to render.
|
|
430
|
+
*
|
|
431
|
+
* Client-rendered surfaces are then served, tagged with the owning plugin id, from
|
|
432
|
+
* `GET /api/plugins/contributions`. Core-interpreted ones deliberately are not —
|
|
433
|
+
* they are gathered at their own consumption site instead.
|
|
325
434
|
*/
|
|
326
435
|
export interface Contributes {
|
|
327
436
|
/**
|
|
@@ -334,12 +443,218 @@ export interface Contributes {
|
|
|
334
443
|
commands?: ContributionId[];
|
|
335
444
|
/**
|
|
336
445
|
* Declarative **native** UI widgets the plugin contributes to the desktop
|
|
337
|
-
* composer
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
446
|
+
* composer. Core stores these verbatim and serves them via
|
|
447
|
+
* `GET /api/plugins/contributions` (tagged with the owning `plugin` id); the
|
|
448
|
+
* desktop renders the known control types. Opaque to Core (the renderer owns
|
|
449
|
+
* interpretation) so a new control type needs no Core change — an entry Core has
|
|
450
|
+
* never heard of is forwarded byte-for-byte, so a desktop newer than the node it
|
|
451
|
+
* talks to still gets everything it was shipped to render.
|
|
452
|
+
*
|
|
453
|
+
* # The control vocabulary
|
|
454
|
+
*
|
|
455
|
+
* Every entry is an object carrying `id`, a `type` discriminant, a `label` and a
|
|
456
|
+
* `flag`; the remaining keys belong to that type. `flag` is universal because the
|
|
457
|
+
* per-request `plugin_flags` map is the composer's ONLY channel to the turn — a
|
|
458
|
+
* control the turn hook cannot observe would do nothing. `type` is deliberately NOT
|
|
459
|
+
* an enum (same reasoning as [`ViewContribution::view`]): an unknown member must
|
|
460
|
+
* reach a newer shell intact rather than being rejected at load by an older Core.
|
|
461
|
+
* The vocabulary the desktop composer understands today:
|
|
462
|
+
*
|
|
463
|
+
* - `"toggle"` — a switch row in the composer "+" menu, with an optional
|
|
464
|
+
* `description`. Flipping it puts `flag: true` into `plugin_flags`. This is the
|
|
465
|
+
* original — and until now the ONLY — rendered type.
|
|
466
|
+
* - `"select"` — a menu/segmented picker. Carries an `options` array of
|
|
467
|
+
* `{ value, label, description?, icon? }` plus an optional `default`. The chosen
|
|
468
|
+
* `value` (a string, not a bool) lands in `plugin_flags[flag]`, so a plugin can
|
|
469
|
+
* offer modes ("fast" / "thorough") instead of on/off.
|
|
470
|
+
* - `"chip"` — an inline pill in the composer bar showing a LIVE value rather than
|
|
471
|
+
* a menu row. Carries an optional `icon` and a `source` (the same
|
|
472
|
+
* `@ryu/app-host/views` `ViewSource` a declarative view uses) the shell polls for
|
|
473
|
+
* the displayed text, and exposes/clears its value through `flag`. This is what a
|
|
474
|
+
* rich bespoke control (a recording indicator, a selected-clip pill) needs in
|
|
475
|
+
* order to stop being hand-written host code.
|
|
476
|
+
* - `"action"` — a button that DISPATCHES rather than holding state. Carries an
|
|
477
|
+
* optional `icon` and a `capability` (+ optional `args`) the shell invokes
|
|
478
|
+
* through the plugin's granted capability seam — never inline code, and never a
|
|
479
|
+
* capability the owning plugin was not granted — then marks `flag` so the turn
|
|
480
|
+
* hook sees that it fired.
|
|
481
|
+
*
|
|
482
|
+
* A control may also carry `placement` (`"menu"`, the default, or `"bar"`) and
|
|
483
|
+
* `order`; the renderer, not Core, decides what to do with an unknown key.
|
|
484
|
+
*
|
|
485
|
+
* Renderers MUST ignore an entry whose `type` they do not know (the desktop
|
|
486
|
+
* filters by `type`), so shipping a new control type degrades to "not shown on
|
|
487
|
+
* older shells" instead of breaking the composer.
|
|
341
488
|
*/
|
|
342
489
|
composer_controls?: unknown[];
|
|
490
|
+
/**
|
|
491
|
+
* **Deletable data categories** the app owns — one "Delete all X" row in
|
|
492
|
+
* Settings → Danger Zone (see [`DataCategoryContribution`]).
|
|
493
|
+
*
|
|
494
|
+
* The danger zone used to be two hardcoded lists that had to be edited
|
|
495
|
+
* together: a `DataCategory` enum in Core and a `CATEGORIES` array carrying the
|
|
496
|
+
* user-facing copy in the closed desktop source. Monitors and Meetings are
|
|
497
|
+
* app-owned data, so both lists named apps — which meant a node where Monitors
|
|
498
|
+
* was never enabled still offered to delete monitors, and the count was always
|
|
499
|
+
* 0. Declaring the category here makes the owning app the single source of both
|
|
500
|
+
* its existence and its wording, and makes the row appear and disappear with
|
|
501
|
+
* the app instead of with a client-side feature-detect.
|
|
502
|
+
*
|
|
503
|
+
* # Core-interpreted, so a typed struct — and NOT on the contributions endpoint
|
|
504
|
+
*
|
|
505
|
+
* Core has to resolve the id to something that can actually count and delete
|
|
506
|
+
* the rows, so per this type's own doc comment this gets a typed struct rather
|
|
507
|
+
* than opaque JSON, and it is gathered at its consumption site
|
|
508
|
+
* (`GET /api/data/counts`, which serves each category's descriptor next to its
|
|
509
|
+
* live count) rather than at `GET /api/plugins/contributions` — the same
|
|
510
|
+
* disposition as [`Contributes::tool_filters`] and [`Contributes::lsp_servers`].
|
|
511
|
+
*
|
|
512
|
+
* # Declaration, not implementation
|
|
513
|
+
*
|
|
514
|
+
* A declared category is served only when Core knows how to clear it; an id
|
|
515
|
+
* Core does not implement is skipped with a warn rather than being offered as a
|
|
516
|
+
* button that 400s. That split is deliberate and not a stepping stone to a
|
|
517
|
+
* generic HTTP truncate: clearing monitors has to tear down each monitor's
|
|
518
|
+
* backing scheduler job, and clearing meetings has to broadcast on the meetings
|
|
519
|
+
* SSE stream, so a blind `DELETE /monitors` would leave jobs ticking forever.
|
|
520
|
+
* The manifest owns *whether the row exists and what it says*; Core owns *what
|
|
521
|
+
* deleting actually entails*.
|
|
522
|
+
*/
|
|
523
|
+
data_categories?: DataCategoryContribution[];
|
|
524
|
+
/**
|
|
525
|
+
* App-registered **workspace dock panels** — a tab in the desktop's bottom or
|
|
526
|
+
* right dock (Terminal / Code Review / Browser / Simulator live there today).
|
|
527
|
+
* This is the seam that lets an app OWN its dock tab instead of the shell
|
|
528
|
+
* welding the app into a closed `TabKind` union: `@ryu/browser` and
|
|
529
|
+
* `@ryu/simulator` are apps, and their tabs are contributions, not enum
|
|
530
|
+
* variants. Self-contained + opaque `spec` (see [`DockPanelContribution`]), so a
|
|
531
|
+
* new panel capability needs no Core change; served + tagged with the owning
|
|
532
|
+
* `plugin` id at `GET /api/plugins/contributions`.
|
|
533
|
+
*/
|
|
534
|
+
dock_panels?: DockPanelContribution[];
|
|
535
|
+
/**
|
|
536
|
+
* **App events this plugin emits** — the *provider* half of the hook system,
|
|
537
|
+
* and the mirror image of [`Contributes::turn_hooks`] (the *consumer* half).
|
|
538
|
+
*
|
|
539
|
+
* Core's own hook phases (`post_assistant_turn`, `pre_tool_use`, `context`, …)
|
|
540
|
+
* are a closed set built into `plugin_host`, so before this surface existed a
|
|
541
|
+
* plugin could only react to things happening *in a chat turn*. An app that
|
|
542
|
+
* owns a real-world lifecycle — a meeting ending, a workflow run failing, an
|
|
543
|
+
* alert firing — had no way to let anything else react to it. That forced the
|
|
544
|
+
* classic anti-pattern: every consumer polls the producer's HTTP routes, and
|
|
545
|
+
* every new integration is bespoke wiring between two apps that must both be
|
|
546
|
+
* changed.
|
|
547
|
+
*
|
|
548
|
+
* Declaring an event here makes it a first-class hook phase. Any other plugin
|
|
549
|
+
* consumes it by naming it in a `turn_hooks[].on`, and any workflow consumes it
|
|
550
|
+
* with an `event` trigger — neither the producer nor Core learns anything about
|
|
551
|
+
* the consumer. Apps therefore both **provide** and **consume** over one
|
|
552
|
+
* mechanism.
|
|
553
|
+
*
|
|
554
|
+
* # Ids are namespaced, and that is what makes collisions impossible
|
|
555
|
+
*
|
|
556
|
+
* Every id MUST be `<owning plugin id>#<event name>` — the owning half is
|
|
557
|
+
* checked against the manifest's own `id` at load, and the name half is
|
|
558
|
+
* `[a-z0-9][a-z0-9._-]*`. Because a Core phase name never contains `/`, an app
|
|
559
|
+
* literally cannot declare an event that shadows one, no reserved-word list
|
|
560
|
+
* required. It is also why the emit path can authorize purely from the
|
|
561
|
+
* manifest: the caller's authenticated plugin id must be the id in the event
|
|
562
|
+
* name, so an app can only ever emit its **own** events.
|
|
563
|
+
*
|
|
564
|
+
* # Core-interpreted, so a typed struct
|
|
565
|
+
*
|
|
566
|
+
* Core reads this table to authorize emits and to serve the event catalog, so
|
|
567
|
+
* per this type's own doc comment it gets a typed struct rather than opaque
|
|
568
|
+
* JSON. It names event strings rather than runnable ids, so it is
|
|
569
|
+
* **self-contained** and stays out of [`Contributes::referenced_ids`].
|
|
570
|
+
*/
|
|
571
|
+
hook_events?: HookEventContribution[];
|
|
572
|
+
/**
|
|
573
|
+
* **Language servers** the plugin declares, keyed by server name — the
|
|
574
|
+
* agent-neutral mirror of Claude Code's `.lsp.json` / `lspServers`, so a config
|
|
575
|
+
* written for either host loads in the other:
|
|
576
|
+
*
|
|
577
|
+
* ```json
|
|
578
|
+
* "lsp_servers": {
|
|
579
|
+
* "go": { "command": "gopls", "args": ["serve"], "extensionToLanguage": { ".go": "go" } }
|
|
580
|
+
* }
|
|
581
|
+
* ```
|
|
582
|
+
*
|
|
583
|
+
* Only the container key is Ryu's (`lsp_servers`, snake_case like every sibling
|
|
584
|
+
* here); every key INSIDE a server entry is Claude's own camelCase spelling
|
|
585
|
+
* verbatim, because that body is what actually travels between the two hosts.
|
|
586
|
+
* No `lspServers` alias is accepted on purpose. `lsp_servers` — this exact
|
|
587
|
+
* spelling — is registered in the SDK's zod mirror (`ContributesSchema` in
|
|
588
|
+
* `packages/sdk/src/manifest.ts`), and that mirror STRIPS every key it does not
|
|
589
|
+
* list. An alias would therefore parse here and be silently deleted at
|
|
590
|
+
* `ryu pack` time, before the manifest is signed, which is a worse failure than
|
|
591
|
+
* a key that never parsed at all. One spelling, registered in both places.
|
|
592
|
+
*
|
|
593
|
+
* The plugin ships CONFIG ONLY, never the server binary — `command` is resolved
|
|
594
|
+
* from `PATH` at spawn time and a missing binary is a visible skip, not a load
|
|
595
|
+
* error. Core spawns and supervises these processes itself, so unlike the
|
|
596
|
+
* client-rendered surfaces above this one is fully typed
|
|
597
|
+
* ([`LspServerContribution`]) and is NOT served from
|
|
598
|
+
* `GET /api/plugins/contributions`; it is gathered at the spawn site, the same
|
|
599
|
+
* disposition as [`Contributes::tool_filters`].
|
|
600
|
+
*
|
|
601
|
+
* # Ordering is part of the contract
|
|
602
|
+
*
|
|
603
|
+
* Registration is **first-registration-wins per file extension**: if two enabled
|
|
604
|
+
* servers both claim `.go`, the first one registered owns it, the others never
|
|
605
|
+
* start for that extension, and the spawn site warns naming the owner. That rule
|
|
606
|
+
* is only reproducible if iteration order is, so this is a [`BTreeMap`] — it
|
|
607
|
+
* iterates lexicographically by server key, never in hash order and never in
|
|
608
|
+
* JSON authoring order. The full resolved invariant across a node is
|
|
609
|
+
* **(plugin enable order, then server key ascending)**.
|
|
610
|
+
*
|
|
611
|
+
* Note this makes the tie-break deterministic, not byte-identical to Claude
|
|
612
|
+
* Code's, which falls out of JS object insertion order. Two servers fighting
|
|
613
|
+
* over one extension is a misconfiguration in either host; what matters is that
|
|
614
|
+
* the same node always resolves it the same way and says who won.
|
|
615
|
+
*/
|
|
616
|
+
lsp_servers?: {
|
|
617
|
+
[k: string]: LspServerContribution;
|
|
618
|
+
};
|
|
619
|
+
/**
|
|
620
|
+
* **Pi extensions** the plugin ships — TypeScript files the managed `ryu` (Pi)
|
|
621
|
+
* agent loads at process start:
|
|
622
|
+
*
|
|
623
|
+
* ```json
|
|
624
|
+
* "pi_extensions": [
|
|
625
|
+
* { "id": "shell", "file": "pi-extensions/ryu-shell.ts",
|
|
626
|
+
* "description": "background bash for the managed Pi agent" }
|
|
627
|
+
* ]
|
|
628
|
+
* ```
|
|
629
|
+
*
|
|
630
|
+
* Pi ships none of plan mode, sub-agents, permission prompts or background bash
|
|
631
|
+
* and says so deliberately in its own docs — "you can build or install those
|
|
632
|
+
* workflows as extensions or packages". This surface is that seam: the
|
|
633
|
+
* capabilities Core used to hardcode into the spawn path become plugins the user
|
|
634
|
+
* can enable and disable, and a third party can ship one at all.
|
|
635
|
+
*
|
|
636
|
+
* # This is UNSANDBOXED code, and the tier gate is not optional
|
|
637
|
+
*
|
|
638
|
+
* A [`Contributes::turn_hooks`] body runs in the deny-by-default Deno sandbox
|
|
639
|
+
* behind capability-gated `host.*` calls. A file named here runs **inside the Pi
|
|
640
|
+
* process** with full host privilege: the first-party ones spawn children and
|
|
641
|
+
* POST to Core. That is the same arbitrary-code-execution class as
|
|
642
|
+
* [`PluginManifest::mcp_servers`], so Core gates it identically — Core tier is
|
|
643
|
+
* auto-allowed, Community tier needs an operator-allowlisted grant, and the gate
|
|
644
|
+
* sits at the materializer, because writing the file is what makes it run.
|
|
645
|
+
*
|
|
646
|
+
* # Core-interpreted, so a typed struct — and NOT on the contributions endpoint
|
|
647
|
+
*
|
|
648
|
+
* Core resolves each `file` and projects it into the managed Pi's config dir, so
|
|
649
|
+
* per this type's own doc comment it gets a typed struct and is gathered at its
|
|
650
|
+
* consumption site (`pi_config::app_extensions`) rather than served from
|
|
651
|
+
* `GET /api/plugins/contributions` — the same disposition as
|
|
652
|
+
* [`Contributes::lsp_servers`].
|
|
653
|
+
*
|
|
654
|
+
* The `file` is deliberately NOT hydrated into an inline string the way a
|
|
655
|
+
* `code_file` is; see [`PluginManifest::pi_extension_refs`] for why.
|
|
656
|
+
*/
|
|
657
|
+
pi_extensions?: PiExtensionContribution[];
|
|
343
658
|
/**
|
|
344
659
|
* Gateway policies the plugin contributes (referenced by runnable id).
|
|
345
660
|
*/
|
|
@@ -347,24 +662,71 @@ export interface Contributes {
|
|
|
347
662
|
/**
|
|
348
663
|
* Declarative settings tabs the plugin contributes (model pickers, text
|
|
349
664
|
* fields bound to preference keys). Served + rendered the same way.
|
|
665
|
+
*
|
|
666
|
+
* The **contract** for each entry is [`SettingsTabContribution`] — that is what
|
|
667
|
+
* the published JSON Schema advertises (`schemars(with = …)`) and what the
|
|
668
|
+
* loader holds every manifest to at import (see `validate_settings_tab`), so a
|
|
669
|
+
* malformed tab is rejected with a diagnostic instead of reaching the desktop
|
|
670
|
+
* and being silently dropped by the renderer's defensive parser.
|
|
671
|
+
*
|
|
672
|
+
* The *stored* type stays `serde_json::Value` on purpose. `GET
|
|
673
|
+
* /api/plugins/contributions` tags each entry in place with its owning `plugin`
|
|
674
|
+
* id and forwards it verbatim; deserializing into the struct here would silently
|
|
675
|
+
* DROP any key this Core build does not know about, so a desktop newer than the
|
|
676
|
+
* node it talks to would lose exactly the fields it was shipped to render. Parse
|
|
677
|
+
* once at the validation chokepoint, forward the original bytes.
|
|
678
|
+
*/
|
|
679
|
+
settings_tabs?: SettingsTabContribution[];
|
|
680
|
+
/**
|
|
681
|
+
* App-registered sidebar **buttons** — a single nav row (e.g. Memory →
|
|
682
|
+
* `/library/memory`). The button-shaped sibling of [`Contributes::sidebar_sections`]
|
|
683
|
+
* (no live list, just a label/icon + a client route). See [`SidebarButtonContribution`].
|
|
684
|
+
*/
|
|
685
|
+
sidebar_buttons?: SidebarButtonContribution[];
|
|
686
|
+
/**
|
|
687
|
+
* App-registered sidebar **sections** — a header plus a live list of rows the
|
|
688
|
+
* shell fetches from a declared Core `/api/` path. Lets an app own its sidebar
|
|
689
|
+
* section (Canvas/Whiteboard/Meetings recent-doc lists) instead of the shell
|
|
690
|
+
* hardcoding it. Self-contained + opaque `spec` (see [`SidebarSectionContribution`]),
|
|
691
|
+
* so a new section capability needs no Core change; served + tagged with the
|
|
692
|
+
* owning `plugin` id at `GET /api/plugins/contributions`.
|
|
350
693
|
*/
|
|
351
|
-
|
|
694
|
+
sidebar_sections?: SidebarSectionContribution[];
|
|
352
695
|
/**
|
|
353
696
|
* Slash commands the plugin contributes (e.g. `/goal`). The desktop maps the
|
|
354
697
|
* command to a `plugin_flags`/message action; the plugin's turn hook reads
|
|
355
698
|
* the resulting message. Served + rendered the same way.
|
|
356
699
|
*/
|
|
357
700
|
slash_commands?: unknown[];
|
|
701
|
+
/**
|
|
702
|
+
* Tools this plugin wants **hidden** from the model's offered tool list —
|
|
703
|
+
* the declarative half of a tool firewall (see [`ToolFilterContribution`]).
|
|
704
|
+
*
|
|
705
|
+
* Purely declarative here: this contract defines and validates the shape, and
|
|
706
|
+
* the filter is applied where tools are offered to the model. Like
|
|
707
|
+
* [`Contributes::turn_hooks`] this is self-contained (the ids name tools from
|
|
708
|
+
* *other* plugins/servers by design — hiding your own tool is just not
|
|
709
|
+
* declaring it), so it is NOT cross-validated against `runnables`.
|
|
710
|
+
*/
|
|
711
|
+
tool_filters?: ToolFilterContribution[];
|
|
358
712
|
/**
|
|
359
713
|
* Callable tools the plugin contributes (referenced by runnable id).
|
|
360
714
|
*/
|
|
361
715
|
tools?: ContributionId[];
|
|
362
716
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
717
|
+
* Hooks the plugin contributes — server-side logic that runs at a hook
|
|
718
|
+
* boundary and returns a directive. These are **self-contained** (they carry
|
|
719
|
+
* their own inline `code`), so they are NOT cross-validated against
|
|
720
|
+
* `runnables` like the id-reference surfaces above; the Core `plugin_host`
|
|
721
|
+
* runtime executes them in the sandbox.
|
|
722
|
+
*
|
|
723
|
+
* The field name is historical. It originally held only *chat* turn
|
|
724
|
+
* boundaries (`post_assistant_turn`, `pre_user_turn`); a hook's `on` is now
|
|
725
|
+
* any hook phase, including an **app event** another plugin declared in its
|
|
726
|
+
* [`Contributes::hook_events`] (`@example/meetings#meeting.ended`). It is
|
|
727
|
+
* deliberately NOT renamed: `turn_hooks` is load-bearing in every packaged
|
|
728
|
+
* manifest, the published JSON Schema, the SDK's TS mirror and the loader's
|
|
729
|
+
* invariant tests, and the rename would buy nothing but churn.
|
|
368
730
|
*/
|
|
369
731
|
turn_hooks?: TurnHookContribution[];
|
|
370
732
|
/**
|
|
@@ -410,18 +772,568 @@ export interface ContributionId {
|
|
|
410
772
|
*/
|
|
411
773
|
title?: string | null;
|
|
412
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* One **deletable data category** an app owns (see [`Contributes::data_categories`]).
|
|
777
|
+
*
|
|
778
|
+
* Everything the Danger Zone needs to draw and arm one destructive row, so the copy
|
|
779
|
+
* lives with the app whose data it describes rather than in the desktop's source.
|
|
780
|
+
*/
|
|
781
|
+
export interface DataCategoryContribution {
|
|
782
|
+
/**
|
|
783
|
+
* The word the user must type to arm the delete. Absent = the [`noun`], which is
|
|
784
|
+
* the right default often enough that requiring it would just be ceremony.
|
|
785
|
+
* Matched case-insensitively by the client.
|
|
786
|
+
*
|
|
787
|
+
* [`noun`]: DataCategoryContribution::noun
|
|
788
|
+
*/
|
|
789
|
+
confirm_word?: string | null;
|
|
790
|
+
/**
|
|
791
|
+
* Exactly what disappears, shown in the confirm dialog. Required, and required
|
|
792
|
+
* to be specific: this is the last thing the user reads before an irreversible
|
|
793
|
+
* delete, and "this cannot be undone" tells them nothing they did not know.
|
|
794
|
+
*/
|
|
795
|
+
detail: string;
|
|
796
|
+
/**
|
|
797
|
+
* Stable id — this is the `category` a `POST /api/data/clear` names, so it is
|
|
798
|
+
* the app's half of the delete contract and renaming it breaks the button.
|
|
799
|
+
*/
|
|
800
|
+
id: string;
|
|
801
|
+
/**
|
|
802
|
+
* Plural noun for the live count line ("42 monitors" / "No monitors") and the
|
|
803
|
+
* "N deleted" toast. Lower-case: it is used mid-sentence.
|
|
804
|
+
*/
|
|
805
|
+
noun: string;
|
|
806
|
+
/**
|
|
807
|
+
* The destructive button label and confirm-dialog title ("Delete all monitors").
|
|
808
|
+
*/
|
|
809
|
+
title: string;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* One app-registered **workspace dock panel** — a tab in the desktop's bottom or
|
|
813
|
+
* right dock (see [`Contributes::dock_panels`]).
|
|
814
|
+
*
|
|
815
|
+
* The dock sibling of [`ViewContribution`] / [`SidebarSectionContribution`]: a typed
|
|
816
|
+
* envelope (`id` / `title` / `icon` / `placement`) around an OPAQUE description of
|
|
817
|
+
* what the tab renders. Core stores it verbatim, tags it with the owning `plugin` id
|
|
818
|
+
* at `GET /api/plugins/contributions`, and never interprets `panel` or `spec` — so a
|
|
819
|
+
* new panel capability is a renderer change, never a Core change.
|
|
820
|
+
*
|
|
821
|
+
* # The `panel` vocabulary
|
|
822
|
+
*
|
|
823
|
+
* `panel` is the render-mode discriminant the desktop's dock renderer switches on.
|
|
824
|
+
* It is a plain `String` (not an enum) for the same reason [`ViewContribution::view`]
|
|
825
|
+
* is: an unknown member must reach a newer shell intact rather than being rejected at
|
|
826
|
+
* load by an older Core. The vocabulary the desktop understands today:
|
|
827
|
+
*
|
|
828
|
+
* - `"companion"` — mount the app's sandboxed companion surface in the dock. The
|
|
829
|
+
* `spec` names it: `{ "companion": "<runnable id>" }`. This is the third-party
|
|
830
|
+
* path: an app ships one companion UI and can surface it in the dock, the sidebar,
|
|
831
|
+
* or a full tab without any host code.
|
|
832
|
+
* - `"view"` — render one of the plugin's own [`Contributes::views`] entries inside
|
|
833
|
+
* the dock chrome: `{ "view": "<view id>" }`. Data-only, drawn with the host's own
|
|
834
|
+
* `@ryu/ui` components, so a dock panel gets the Raycast tier for free.
|
|
835
|
+
* - `"native"` — the shell's OWN component, registered under `<plugin>/<id>`. This is
|
|
836
|
+
* the migration seam for first-party apps whose panel is hand-written React driving
|
|
837
|
+
* their sidecar through the ext-proxy (`@ryu/browser`, `@ryu/simulator`): the
|
|
838
|
+
* *component* stays in the shell, but its existence, label, icon and placement stop
|
|
839
|
+
* being a hardcoded `TabKind` variant and become the app's own declaration, so
|
|
840
|
+
* disabling the app removes the tab. An unknown `<plugin>/<id>` simply renders
|
|
841
|
+
* nothing — a native panel is never a code channel.
|
|
842
|
+
*
|
|
843
|
+
* The full `spec` shape is owned by the shared TS vocabulary (`@ryu/app-host/views`
|
|
844
|
+
* `DockPanelSpec`), NOT by this contract.
|
|
845
|
+
*/
|
|
846
|
+
export interface DockPanelContribution {
|
|
847
|
+
/**
|
|
848
|
+
* Optional glyph id resolved by the shell's Icon primitive (Iconify/Hugeicons).
|
|
849
|
+
*/
|
|
850
|
+
icon?: string | null;
|
|
851
|
+
/**
|
|
852
|
+
* Stable id for this panel within the plugin (the dock's tab key, namespaced by
|
|
853
|
+
* the shell as `plugin:<pluginId>:<id>` so two apps can reuse an id).
|
|
854
|
+
*/
|
|
855
|
+
id: string;
|
|
856
|
+
/**
|
|
857
|
+
* Optional ordering hint within the dock's tab-type menu (lower = earlier).
|
|
858
|
+
*/
|
|
859
|
+
order?: number | null;
|
|
860
|
+
/**
|
|
861
|
+
* The render-mode discriminant (`"companion"`, `"view"`, `"native"`, …). Opaque
|
|
862
|
+
* to Core; an unknown member is passed through so a newer shell can render it.
|
|
863
|
+
*/
|
|
864
|
+
panel: string;
|
|
865
|
+
/**
|
|
866
|
+
* Which dock the panel opens in. Defaults to [`DockPanelPlacement::Bottom`], the
|
|
867
|
+
* drawer a terminal-shaped panel belongs in — and falls back to it for an
|
|
868
|
+
* unrecognised dock too, rather than failing the whole manifest
|
|
869
|
+
* (see [`deserialize_dock_panel_placement`]).
|
|
870
|
+
*/
|
|
871
|
+
placement?: "bottom" | "right" | "both";
|
|
872
|
+
/**
|
|
873
|
+
* The payload for the render mode (`{ "companion": … }` / `{ "view": … }` / any
|
|
874
|
+
* future panel capability). Opaque to Core — the desktop dock renderer interprets
|
|
875
|
+
* it per `panel`. Absent = the mode needs no payload (the `"native"` case).
|
|
876
|
+
*/
|
|
877
|
+
spec?: {
|
|
878
|
+
[k: string]: unknown;
|
|
879
|
+
};
|
|
880
|
+
/**
|
|
881
|
+
* Tab label shown on the dock tab strip and in the "new tab" menu.
|
|
882
|
+
*/
|
|
883
|
+
title: string;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* One **app event** a plugin declares it emits (a [`Contributes::hook_events`]
|
|
887
|
+
* row). This is a *declaration*, not code: the event is raised at runtime by the
|
|
888
|
+
* plugin's own sidecar calling the `events.emit` kernel capability, and Core
|
|
889
|
+
* checks the emit against this table.
|
|
890
|
+
*
|
|
891
|
+
* The payload the emitter sends is delivered to every consumer as `ctx.event`, so
|
|
892
|
+
* [`Self::payload_example`] is the contract a consumer author reads. Keep it
|
|
893
|
+
* honest — it is the only description of the payload anyone gets.
|
|
894
|
+
*/
|
|
895
|
+
export interface HookEventContribution {
|
|
896
|
+
/**
|
|
897
|
+
* What the event means and, critically, *when* it fires — including whether it
|
|
898
|
+
* can fire more than once for the same subject.
|
|
899
|
+
*/
|
|
900
|
+
description?: string | null;
|
|
901
|
+
/**
|
|
902
|
+
* The fully-qualified event id: `<owning plugin id>#<event name>`, e.g.
|
|
903
|
+
* `@example/meetings#meeting.ended`. Validated at load against the owning
|
|
904
|
+
* manifest's `id`; see [`Contributes::hook_events`] for why the namespace is
|
|
905
|
+
* mandatory rather than conventional.
|
|
906
|
+
*
|
|
907
|
+
* Name the event after **what happened**, in the past tense, never after who
|
|
908
|
+
* should react to it: a consumer that renames the producer's event to suit
|
|
909
|
+
* itself is exactly the coupling this surface removes. The house patterns are
|
|
910
|
+
* `x.started` / `x.ended` / `x.failed` for a lifecycle, `x.ready` for a
|
|
911
|
+
* produced artifact, and `x.created` / `x.updated` / `x.deleted` for state.
|
|
912
|
+
*/
|
|
913
|
+
id: string;
|
|
914
|
+
/**
|
|
915
|
+
* An example of the payload delivered as `ctx.event`. Documentation, not a
|
|
916
|
+
* schema: Core forwards whatever the emitter sends verbatim and validates
|
|
917
|
+
* nothing beyond the size cap, so this exists for the human writing a consumer.
|
|
918
|
+
*/
|
|
919
|
+
payload_example?: {
|
|
920
|
+
[k: string]: unknown;
|
|
921
|
+
};
|
|
922
|
+
/**
|
|
923
|
+
* Human-readable title for the event picker (workflow trigger UI, docs).
|
|
924
|
+
*/
|
|
925
|
+
title: string;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* One **language server** a plugin declares (see [`Contributes::lsp_servers`]).
|
|
929
|
+
*
|
|
930
|
+
* Field-for-field Claude Code's language-server config, camelCase on the wire, so
|
|
931
|
+
* the same JSON body loads in either host. Required by Claude's spec: `command`
|
|
932
|
+
* and `extensionToLanguage`. Everything else is optional and defaulted here to
|
|
933
|
+
* Claude's documented default.
|
|
934
|
+
*
|
|
935
|
+
* # Why `command` and `extensionToLanguage` are `#[serde(default)]` anyway
|
|
936
|
+
*
|
|
937
|
+
* They are required by the SPEC, not by serde, and that is deliberate. Claude Code
|
|
938
|
+
* **skips** a server whose config is invalid and starts the rest; making either
|
|
939
|
+
* field a non-defaulted serde field would instead turn a missing one into a parse
|
|
940
|
+
* error on the entire [`PluginManifest`], costing the plugin every runnable,
|
|
941
|
+
* sidecar and tool it ships over one broken language-server entry. Defaulting them
|
|
942
|
+
* is what makes the per-server skip reachable at all: the manifest parses, and
|
|
943
|
+
* [`LspServerContribution::validate`] reports the reason at the spawn site.
|
|
944
|
+
*
|
|
945
|
+
* Unknown keys are dropped rather than rejected (no `deny_unknown_fields`
|
|
946
|
+
* anywhere in this file), so a field from a newer Claude release costs a plugin
|
|
947
|
+
* nothing.
|
|
948
|
+
*/
|
|
949
|
+
export interface LspServerContribution {
|
|
950
|
+
/**
|
|
951
|
+
* Arguments passed to [`command`](LspServerContribution::command)
|
|
952
|
+
* (e.g. `["serve"]` for `gopls`).
|
|
953
|
+
*/
|
|
954
|
+
args?: string[];
|
|
955
|
+
/**
|
|
956
|
+
* The server executable, resolved from `PATH` at spawn time (`gopls`,
|
|
957
|
+
* `rust-analyzer`, `typescript-language-server`, …).
|
|
958
|
+
*
|
|
959
|
+
* The plugin ships the CONFIG, never the binary. A `command` that is not on
|
|
960
|
+
* `PATH` is a graceful skip with a visible reason — the user is told which
|
|
961
|
+
* server did not start and why, and the rest of the node is unaffected.
|
|
962
|
+
* Defaulted to `""` so a missing one is a skipped server, not a dead manifest
|
|
963
|
+
* (see the type doc).
|
|
964
|
+
*/
|
|
965
|
+
command?: string;
|
|
966
|
+
/**
|
|
967
|
+
* Push this server's diagnostics into the model's context after edits. Defaults
|
|
968
|
+
* to **true** (Claude Code parity); same `default` caveat as
|
|
969
|
+
* [`restart_on_crash`](LspServerContribution::restart_on_crash).
|
|
970
|
+
*/
|
|
971
|
+
diagnostics?: boolean;
|
|
972
|
+
/**
|
|
973
|
+
* Extra environment variables for the server process, merged over the inherited
|
|
974
|
+
* environment.
|
|
975
|
+
*/
|
|
976
|
+
env?: {
|
|
977
|
+
[k: string]: string;
|
|
978
|
+
};
|
|
979
|
+
/**
|
|
980
|
+
* File extension → LSP language id (`{ ".go": "go" }`) — the map that decides
|
|
981
|
+
* which files this server handles, and the thing two servers can collide on.
|
|
982
|
+
*
|
|
983
|
+
* Claude Code authors keys with a leading dot and in lowercase; a hand-written
|
|
984
|
+
* manifest will not always. Compare through
|
|
985
|
+
* [`normalize_lsp_extension_key`] (or read
|
|
986
|
+
* [`normalized_extensions`](LspServerContribution::normalized_extensions))
|
|
987
|
+
* rather than indexing this map directly, so `go`, `.go` and `.GO` all resolve
|
|
988
|
+
* to the same entry. Empty ⇒ the server claims nothing and is skipped.
|
|
989
|
+
*/
|
|
990
|
+
extensionToLanguage?: {
|
|
991
|
+
[k: string]: string;
|
|
992
|
+
};
|
|
993
|
+
/**
|
|
994
|
+
* Sent verbatim as `initializationOptions` in the LSP `initialize` request.
|
|
995
|
+
* Opaque JSON on purpose: the shape is the individual language server's, and
|
|
996
|
+
* Ryu is a courier for it, not an interpreter. Absent = send none.
|
|
997
|
+
*/
|
|
998
|
+
initializationOptions?: {
|
|
999
|
+
[k: string]: unknown;
|
|
1000
|
+
};
|
|
1001
|
+
/**
|
|
1002
|
+
* Cap on automatic restarts before the server is left down. Absent = the spawn
|
|
1003
|
+
* site's own default; meaningless when
|
|
1004
|
+
* [`restart_on_crash`](LspServerContribution::restart_on_crash) is false.
|
|
1005
|
+
*/
|
|
1006
|
+
maxRestarts?: number | null;
|
|
1007
|
+
/**
|
|
1008
|
+
* Restart the server when it exits unexpectedly. Defaults to **true** (Claude
|
|
1009
|
+
* Code parity).
|
|
1010
|
+
*
|
|
1011
|
+
* Note this needs an explicit default fn: a bare `#[serde(default)]` on a
|
|
1012
|
+
* `bool` yields `false` and would silently invert the documented behaviour.
|
|
1013
|
+
* Like [`McpServerDecl::enabled`] it carries no `skip_serializing_if`, so the
|
|
1014
|
+
* value always ships and a reader never has to know the default.
|
|
1015
|
+
*/
|
|
1016
|
+
restartOnCrash?: boolean;
|
|
1017
|
+
/**
|
|
1018
|
+
* Sent verbatim as the payload of `workspace/didChangeConfiguration` once the
|
|
1019
|
+
* server is initialized. Opaque for the same reason as
|
|
1020
|
+
* [`initialization_options`](LspServerContribution::initialization_options).
|
|
1021
|
+
* Absent = send nothing.
|
|
1022
|
+
*/
|
|
1023
|
+
settings?: {
|
|
1024
|
+
[k: string]: unknown;
|
|
1025
|
+
};
|
|
1026
|
+
/**
|
|
1027
|
+
* Milliseconds to wait for a clean `shutdown`/`exit` before killing the
|
|
1028
|
+
* process. Absent = the spawn site's own default.
|
|
1029
|
+
*
|
|
1030
|
+
* That default is the one place this type knowingly parts company with Claude
|
|
1031
|
+
* Code, whose reference says an unset `shutdownTimeout` means **no timeout
|
|
1032
|
+
* applies** — it waits on a wedged server indefinitely. Ryu's spawn sites
|
|
1033
|
+
* impose a finite one (5s in `assets/pi-extensions/ryu-lsp.ts`, documented at
|
|
1034
|
+
* the constant), because Pi is spawned per session and an unbounded wait would
|
|
1035
|
+
* hold every teardown open behind one unresponsive server. An explicitly
|
|
1036
|
+
* declared value is honoured verbatim, so a config written for either host
|
|
1037
|
+
* still behaves identically; only the *unset* case differs.
|
|
1038
|
+
*/
|
|
1039
|
+
shutdownTimeout?: number | null;
|
|
1040
|
+
/**
|
|
1041
|
+
* Milliseconds to wait for `initialize` to come back before giving up on the
|
|
1042
|
+
* server. Absent = the spawn site's own default.
|
|
1043
|
+
*/
|
|
1044
|
+
startupTimeout?: number | null;
|
|
1045
|
+
/**
|
|
1046
|
+
* How the host talks to the server: `"stdio"` (the default, and the only
|
|
1047
|
+
* transport Core implements today) or `"socket"`.
|
|
1048
|
+
*
|
|
1049
|
+
* A plain `String` and not an enum, matching this file's other discriminants
|
|
1050
|
+
* ([`ViewContribution::view`], [`DockPanelContribution::panel`]). The reason is
|
|
1051
|
+
* sharper here than for those: [`DockPanelPlacement`] can afford to coerce an
|
|
1052
|
+
* unrecognised value to its default because a panel opening in the wrong dock is
|
|
1053
|
+
* cosmetic, whereas coercing an unrecognised transport to `stdio` would spawn a
|
|
1054
|
+
* process and then speak a protocol it does not understand. The verbatim string
|
|
1055
|
+
* survives instead, and the spawn site refuses what it cannot drive — see
|
|
1056
|
+
* [`LspTransport`] and [`LspServerContribution::transport_kind`].
|
|
1057
|
+
*/
|
|
1058
|
+
transport?: string;
|
|
1059
|
+
/**
|
|
1060
|
+
* Root directory the server is rooted at. Absent (the common case) = the
|
|
1061
|
+
* session's workspace root, which is why this is an `Option` rather than a
|
|
1062
|
+
* defaulted `String`: "unset, inherit the workspace" and "explicitly rooted
|
|
1063
|
+
* somewhere" are different instructions.
|
|
1064
|
+
*/
|
|
1065
|
+
workspaceFolder?: string | null;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* One **Pi extension** a plugin ships (a [`Contributes::pi_extensions`] row).
|
|
1069
|
+
*
|
|
1070
|
+
* Carries a path, never a body: unlike [`TurnHookContribution`] there is no inline
|
|
1071
|
+
* `code` twin, because nothing downstream reads the source as a string.
|
|
1072
|
+
*/
|
|
1073
|
+
export interface PiExtensionContribution {
|
|
1074
|
+
/**
|
|
1075
|
+
* Optional human-facing one-liner (what the extension adds to the agent).
|
|
1076
|
+
*/
|
|
1077
|
+
description?: string | null;
|
|
1078
|
+
/**
|
|
1079
|
+
* Path to the TypeScript source, relative to the plugin root — exactly
|
|
1080
|
+
* `pi-extensions/<name>.ts`. See [`validate_pi_extension_path`].
|
|
1081
|
+
*/
|
|
1082
|
+
file: string;
|
|
1083
|
+
/**
|
|
1084
|
+
* Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`).
|
|
1085
|
+
*
|
|
1086
|
+
* Part of the materialized file name, so it is what makes one plugin's
|
|
1087
|
+
* extensions distinguishable from another's on disk — and why it is validated
|
|
1088
|
+
* with the same alphabet as an event name rather than left free-form.
|
|
1089
|
+
*/
|
|
1090
|
+
id: string;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* One **settings tab** a plugin contributes (see [`Contributes::settings_tabs`]).
|
|
1094
|
+
*
|
|
1095
|
+
* A tab is EITHER declarative (`fields`, rendered by the shared plugin-settings
|
|
1096
|
+
* renderer against Core's preference store) OR a named `view` the shell resolves to
|
|
1097
|
+
* a bespoke component — for an app whose settings genuinely cannot be expressed as
|
|
1098
|
+
* a list of fields. A tab with neither renders as an empty section, which the
|
|
1099
|
+
* desktop's defensive parser drops on the floor; the loader rejects it instead so
|
|
1100
|
+
* the author gets told.
|
|
1101
|
+
*/
|
|
1102
|
+
export interface SettingsTabContribution {
|
|
1103
|
+
/**
|
|
1104
|
+
* The declarative fields this tab renders. Empty is only legal alongside a
|
|
1105
|
+
* `view`.
|
|
1106
|
+
*/
|
|
1107
|
+
fields?: SettingsFieldContribution[];
|
|
1108
|
+
/**
|
|
1109
|
+
* Stable id for this tab within the plugin — the settings nav routes to it and
|
|
1110
|
+
* the renderer keys by it. Required: the desktop's fallback (`<plugin>.settings`)
|
|
1111
|
+
* collides the moment a plugin declares a second tab.
|
|
1112
|
+
*/
|
|
1113
|
+
id: string;
|
|
1114
|
+
/**
|
|
1115
|
+
* Which settings dialog this tab lands in. Absent/unrecognised = `node`.
|
|
1116
|
+
*/
|
|
1117
|
+
scope?: "node" | "user";
|
|
1118
|
+
/**
|
|
1119
|
+
* Header label for the section. Absent = `"Settings"`, matching the renderer.
|
|
1120
|
+
*/
|
|
1121
|
+
title?: string;
|
|
1122
|
+
/**
|
|
1123
|
+
* A rich settings view this app ships instead of declarative `fields`. Opaque
|
|
1124
|
+
* here — the settings renderer owns the vocabulary and resolves the name to a
|
|
1125
|
+
* component (first-party) or a sandboxed UI (third-party).
|
|
1126
|
+
*/
|
|
1127
|
+
view?: string | null;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* One configurable field inside a [`SettingsTabContribution`], bound to exactly
|
|
1131
|
+
* one preference key.
|
|
1132
|
+
*
|
|
1133
|
+
* `pref_key` is both the storage binding (`GET/PUT /api/preferences/:key`) **and**
|
|
1134
|
+
* the field's identity — the renderer keys its React elements by it — so two
|
|
1135
|
+
* fields sharing one `pref_key` inside a tab is a bug, not a shorthand, and the
|
|
1136
|
+
* loader rejects it.
|
|
1137
|
+
*
|
|
1138
|
+
* The `default`/`required`/`min`/`max`/`min_length`/`max_length` block is
|
|
1139
|
+
* validation metadata: declaring it is how a plugin gets its settings checked at
|
|
1140
|
+
* *import* instead of discovering at runtime that a user typed `"maybe"` into what
|
|
1141
|
+
* the hook reads as a number. It is cross-checked against `type` at load, because
|
|
1142
|
+
* validation metadata that is silently ignored (a `min` on a toggle) is worse than
|
|
1143
|
+
* none — it reads as a guarantee that was never enforced.
|
|
1144
|
+
*/
|
|
1145
|
+
export interface SettingsFieldContribution {
|
|
1146
|
+
/**
|
|
1147
|
+
* Default value, in the field's own JSON type (bool for a toggle, number for
|
|
1148
|
+
* a number, string elsewhere) — NOT the stringified form preferences are
|
|
1149
|
+
* stored in, so a manifest stays readable and the type is checkable.
|
|
1150
|
+
*/
|
|
1151
|
+
default?: {
|
|
1152
|
+
[k: string]: unknown;
|
|
1153
|
+
};
|
|
1154
|
+
/**
|
|
1155
|
+
* Helper caption shown under the field.
|
|
1156
|
+
*/
|
|
1157
|
+
description?: string | null;
|
|
1158
|
+
/**
|
|
1159
|
+
* Display label. Absent = the renderer shows the `pref_key`.
|
|
1160
|
+
*/
|
|
1161
|
+
label?: string | null;
|
|
1162
|
+
/**
|
|
1163
|
+
* Inclusive upper bound for a [`SettingsFieldType::Number`].
|
|
1164
|
+
*/
|
|
1165
|
+
max?: number | null;
|
|
1166
|
+
/**
|
|
1167
|
+
* Maximum length for a text/textarea value.
|
|
1168
|
+
*/
|
|
1169
|
+
max_length?: number | null;
|
|
1170
|
+
/**
|
|
1171
|
+
* Inclusive lower bound for a [`SettingsFieldType::Number`].
|
|
1172
|
+
*/
|
|
1173
|
+
min?: number | null;
|
|
1174
|
+
/**
|
|
1175
|
+
* Minimum length for a text/textarea value.
|
|
1176
|
+
*/
|
|
1177
|
+
min_length?: number | null;
|
|
1178
|
+
/**
|
|
1179
|
+
* Choices for a [`SettingsFieldType::Select`]; required for that type and
|
|
1180
|
+
* inert for every other one.
|
|
1181
|
+
*/
|
|
1182
|
+
options?: SettingsFieldOption[];
|
|
1183
|
+
/**
|
|
1184
|
+
* Placeholder for text / model-picker inputs.
|
|
1185
|
+
*/
|
|
1186
|
+
placeholder?: string | null;
|
|
1187
|
+
/**
|
|
1188
|
+
* The preference key this field reads/writes. Required, non-empty, and
|
|
1189
|
+
* restricted to a path-safe alphabet (it becomes a URL path segment).
|
|
1190
|
+
*/
|
|
1191
|
+
pref_key: string;
|
|
1192
|
+
/**
|
|
1193
|
+
* Whether the user must supply a value (advisory: enforced by the renderer,
|
|
1194
|
+
* declared here so the contract is one place).
|
|
1195
|
+
*/
|
|
1196
|
+
required?: boolean;
|
|
1197
|
+
/**
|
|
1198
|
+
* Granularity for a [`SettingsFieldType::Number`] — the increment its stepper
|
|
1199
|
+
* moves by, and the grid a typed value must land on.
|
|
1200
|
+
*
|
|
1201
|
+
* Distinct from [`Self::min`]/[`Self::max`], which bound the range: a value can
|
|
1202
|
+
* sit inside the range and still be meaningless at this field's resolution
|
|
1203
|
+
* (`0.5` where the setting counts whole pages). The renderer enforces it, so a
|
|
1204
|
+
* field that declares it rejects an off-grid value rather than persisting one
|
|
1205
|
+
* the plugin cannot use.
|
|
1206
|
+
*/
|
|
1207
|
+
step?: number | null;
|
|
1208
|
+
/**
|
|
1209
|
+
* The control to render. Absent or unrecognised = a plain text input.
|
|
1210
|
+
*/
|
|
1211
|
+
type?: "text" | "textarea" | "number" | "toggle" | "select" | "model_picker" | "agent_picker" | "secret";
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* One app-registered **sidebar button** — a single nav row (the button-shaped
|
|
1215
|
+
* sibling of [`SidebarSectionContribution`]). No live list: just a label/icon and a
|
|
1216
|
+
* client route the shell opens with `openTab`. Migrates hardcoded header-chrome
|
|
1217
|
+
* buttons (e.g. Memory) to the owning app.
|
|
1218
|
+
*/
|
|
1219
|
+
export interface SidebarButtonContribution {
|
|
1220
|
+
/**
|
|
1221
|
+
* Optional glyph id resolved by the shell's Icon primitive.
|
|
1222
|
+
*/
|
|
1223
|
+
icon?: string | null;
|
|
1224
|
+
/**
|
|
1225
|
+
* Stable id for this button within the plugin.
|
|
1226
|
+
*/
|
|
1227
|
+
id: string;
|
|
1228
|
+
/**
|
|
1229
|
+
* Optional placement hint among the sidebar buttons.
|
|
1230
|
+
*/
|
|
1231
|
+
order?: number | null;
|
|
1232
|
+
/**
|
|
1233
|
+
* The client route this button opens (e.g. `"/library/memory"`).
|
|
1234
|
+
*/
|
|
1235
|
+
target: string;
|
|
1236
|
+
/**
|
|
1237
|
+
* Button label.
|
|
1238
|
+
*/
|
|
1239
|
+
title: string;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* One app-registered **sidebar section** — a header plus a live list of rows the
|
|
1243
|
+
* desktop's compact sidebar renderer draws (the app-owned replacement for the
|
|
1244
|
+
* hardcoded Canvas/Whiteboard/Meetings sections). A typed envelope around an opaque
|
|
1245
|
+
* `spec` (the `SidebarSectionSpec` in `@ryu/app-host/views`: a `ViewSource` for the
|
|
1246
|
+
* rows, an `itemTarget` route template for `openTab`, optional `itemActions` and a
|
|
1247
|
+
* `create` action). Core stores it verbatim and tags it with the owning `plugin` id;
|
|
1248
|
+
* the `spec` stays opaque so a new section capability is a renderer change, not a
|
|
1249
|
+
* Core change.
|
|
1250
|
+
*/
|
|
1251
|
+
export interface SidebarSectionContribution {
|
|
1252
|
+
/**
|
|
1253
|
+
* Optional glyph id resolved by the shell's Icon primitive (Iconify/Hugeicons).
|
|
1254
|
+
*/
|
|
1255
|
+
icon?: string | null;
|
|
1256
|
+
/**
|
|
1257
|
+
* Stable id for this section within the plugin (namespaced into the shell's
|
|
1258
|
+
* section key as `plugin:<pluginId>:<id>`).
|
|
1259
|
+
*/
|
|
1260
|
+
id: string;
|
|
1261
|
+
/**
|
|
1262
|
+
* Optional placement hint among the sidebar sections (lower = higher up).
|
|
1263
|
+
*/
|
|
1264
|
+
order?: number | null;
|
|
1265
|
+
/**
|
|
1266
|
+
* The opaque section spec (source/itemTarget/itemActions/create). Interpreted by
|
|
1267
|
+
* the desktop renderer, never by Core. Absent = a header with no rows.
|
|
1268
|
+
*/
|
|
1269
|
+
spec?: {
|
|
1270
|
+
[k: string]: unknown;
|
|
1271
|
+
};
|
|
1272
|
+
/**
|
|
1273
|
+
* Header label shown in the sidebar and the Customize dialog.
|
|
1274
|
+
*/
|
|
1275
|
+
title: string;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* One **tool filter**: a fully-qualified tool id a plugin wants withheld from the
|
|
1279
|
+
* model's offered tool list.
|
|
1280
|
+
*
|
|
1281
|
+
* Tools are namespaced `<server>__<tool>` (e.g. `browser__navigate`), so `tool`
|
|
1282
|
+
* must carry the namespace — a bare `navigate` would be ambiguous across servers
|
|
1283
|
+
* and is rejected at load. A **trailing** `*` is a prefix wildcard, which is how a
|
|
1284
|
+
* plugin withholds a whole server (`shadow__*`); it is the only wildcard position
|
|
1285
|
+
* allowed, because an interior or leading `*` invites a pattern that silently
|
|
1286
|
+
* matches far more than the author pictured.
|
|
1287
|
+
*
|
|
1288
|
+
* This type is declaration + validation only. The filter is **applied** where the
|
|
1289
|
+
* tool list is assembled for the model (the MCP offer site in
|
|
1290
|
+
* `apps/core/src/sidecar/mcp`), which calls [`ToolFilterContribution::matches`] so
|
|
1291
|
+
* the wildcard rule has exactly one implementation. Hiding a tool from the model
|
|
1292
|
+
* is not a security boundary — it does not revoke the capability, it only stops the
|
|
1293
|
+
* tool being advertised; enforcement stays with permissions and grants.
|
|
1294
|
+
*/
|
|
1295
|
+
export interface ToolFilterContribution {
|
|
1296
|
+
/**
|
|
1297
|
+
* Why the plugin hides it — surfaced in the plugin's listing so a user can see
|
|
1298
|
+
* what a plugin is removing from the model's view before installing it.
|
|
1299
|
+
*/
|
|
1300
|
+
reason?: string | null;
|
|
1301
|
+
/**
|
|
1302
|
+
* Fully-qualified tool id (`<server>__<tool>`), optionally ending in `*` to
|
|
1303
|
+
* hide every tool whose id starts with the preceding prefix.
|
|
1304
|
+
*/
|
|
1305
|
+
tool: string;
|
|
1306
|
+
}
|
|
413
1307
|
/**
|
|
414
1308
|
* A server-side chat turn hook contributed by a plugin. The `code` is a JS body
|
|
415
1309
|
* run in the plugin sandbox with `ctx` (the turn context) and `host` (the
|
|
416
1310
|
* capability bridge: `host.sideModel`, `host.storage`, `host.log`) in scope; it
|
|
417
1311
|
* returns a directive (`{kind:"none"}` | `{kind:"note",text}` |
|
|
418
1312
|
* `{kind:"continue",text}`). See Core's `plugin_host`.
|
|
1313
|
+
*
|
|
1314
|
+
* The body is authored as a **file** ([`code_file`]) and hydrated into [`code`]
|
|
1315
|
+
* at parse time — see [`PluginManifest::hydrate_code_files`] for why the two
|
|
1316
|
+
* fields are a source-form/wire-form pair rather than alternatives.
|
|
1317
|
+
*
|
|
1318
|
+
* [`code`]: Self::code
|
|
1319
|
+
* [`code_file`]: Self::code_file
|
|
419
1320
|
*/
|
|
420
1321
|
export interface TurnHookContribution {
|
|
421
1322
|
/**
|
|
422
1323
|
* The JS hook body executed in the sandbox (returns a directive).
|
|
1324
|
+
*
|
|
1325
|
+
* Empty in a **source** manifest that declares [`Self::code_file`] instead;
|
|
1326
|
+
* [`PluginManifest::hydrate_code_files`] fills it in before any consumer sees
|
|
1327
|
+
* the manifest, and [`PluginManifest::validate`] refuses a manifest where it
|
|
1328
|
+
* is still empty. Every read site therefore keeps reading exactly this field.
|
|
423
1329
|
*/
|
|
424
|
-
code
|
|
1330
|
+
code?: string;
|
|
1331
|
+
/**
|
|
1332
|
+
* Path to the file holding the hook body, relative to the plugin root
|
|
1333
|
+
* (`hooks/<name>.js`) — the authoring form. Mutually exclusive with
|
|
1334
|
+
* [`Self::code`]; see [`PluginManifest::hydrate_code_files`].
|
|
1335
|
+
*/
|
|
1336
|
+
code_file?: string | null;
|
|
425
1337
|
/**
|
|
426
1338
|
* Stable id for this hook (for logging/audit), unique within the plugin.
|
|
427
1339
|
*/
|
|
@@ -546,6 +1458,51 @@ export interface EnginesReq {
|
|
|
546
1458
|
*/
|
|
547
1459
|
ryu: string;
|
|
548
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* One declarative **stdio MCP server** a plugin registers (see
|
|
1463
|
+
* [`PluginManifest::mcp_servers`]).
|
|
1464
|
+
*
|
|
1465
|
+
* This is the manifest-side, dependency-free mirror of Core's runtime
|
|
1466
|
+
* `McpServerConfig`: pure data (schemars/serde only) so it can live in
|
|
1467
|
+
* kernel-contracts, with Core lowering it into its registry type on enable. A
|
|
1468
|
+
* server is spawned per request as `command args…` (stdio); `command_env` lets
|
|
1469
|
+
* the manifest name an env var Core resolves to an absolute binary path
|
|
1470
|
+
* (e.g. `RYU_GHOST_BIN`) so a downloaded `~/.ryu/bin` binary can override the
|
|
1471
|
+
* bare `command`.
|
|
1472
|
+
*/
|
|
1473
|
+
export interface McpServerDecl {
|
|
1474
|
+
/**
|
|
1475
|
+
* Arguments passed to the command.
|
|
1476
|
+
*/
|
|
1477
|
+
args?: string[];
|
|
1478
|
+
/**
|
|
1479
|
+
* Executable to spawn (e.g. `npx`, an absolute path, or a `~/.ryu/bin` name).
|
|
1480
|
+
*/
|
|
1481
|
+
command: string;
|
|
1482
|
+
/**
|
|
1483
|
+
* Optional env var whose value, when set, OVERRIDES [`command`] with an
|
|
1484
|
+
* absolute binary path. Lets a plugin ship a bare `command` that Core repoints
|
|
1485
|
+
* at a profile-specific downloaded binary. Absent ⇒ use `command` verbatim.
|
|
1486
|
+
*
|
|
1487
|
+
* [`command`]: McpServerDecl::command
|
|
1488
|
+
*/
|
|
1489
|
+
command_env?: string | null;
|
|
1490
|
+
/**
|
|
1491
|
+
* Optional human description for the MCP listing endpoint.
|
|
1492
|
+
*/
|
|
1493
|
+
description?: string | null;
|
|
1494
|
+
/**
|
|
1495
|
+
* When false, the server is registered but skipped by list/call. Defaults to
|
|
1496
|
+
* true so a bare `{ command }` entry just works.
|
|
1497
|
+
*/
|
|
1498
|
+
enabled?: boolean;
|
|
1499
|
+
/**
|
|
1500
|
+
* Extra environment variables for the server process.
|
|
1501
|
+
*/
|
|
1502
|
+
env?: {
|
|
1503
|
+
[k: string]: string;
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
549
1506
|
/**
|
|
550
1507
|
* The single, typed, **deny-by-default** permission set a plugin manifest
|
|
551
1508
|
* declares, lowered by Core to every sandbox backend.
|
|
@@ -614,6 +1571,12 @@ export interface ProvidesEntry {
|
|
|
614
1571
|
* this against their [`Requires::capabilities`].
|
|
615
1572
|
*/
|
|
616
1573
|
capability: string;
|
|
1574
|
+
/**
|
|
1575
|
+
* Preferred pick among the providers of a [`Self::selectable`] capability when
|
|
1576
|
+
* the user has set no override. At most one provider per capability may declare
|
|
1577
|
+
* it. Meaningless (and ignored) on a non-selectable capability.
|
|
1578
|
+
*/
|
|
1579
|
+
default?: boolean;
|
|
617
1580
|
/**
|
|
618
1581
|
* The grant a consumer must hold (Gateway-approved) to invoke this capability
|
|
619
1582
|
* via the broker. Absent = no extra grant beyond declaring the edge.
|
|
@@ -625,12 +1588,62 @@ export interface ProvidesEntry {
|
|
|
625
1588
|
* cross-validates that the named sidecar declares a matching route.
|
|
626
1589
|
*/
|
|
627
1590
|
route?: string | null;
|
|
1591
|
+
/**
|
|
1592
|
+
* Opt in to the **selectable** flavour: many providers of this capability may
|
|
1593
|
+
* be enabled at once and the user *picks* one, exactly like a local engine.
|
|
1594
|
+
*
|
|
1595
|
+
* A non-selectable capability (the original, strict flavour used by `rag` /
|
|
1596
|
+
* `engines`) treats a second enabled provider as an explicit
|
|
1597
|
+
* `BindingError::Ambiguous` refusal. A selectable one resolves deterministically
|
|
1598
|
+
* instead: user override > sole provider > the provider declaring
|
|
1599
|
+
* [`Self::default_provider`] > lexicographically-lowest provider id. The pick is
|
|
1600
|
+
* a pure function of the candidate set, so the disable-safety reconstruction
|
|
1601
|
+
* argument in Core's binding registry is unchanged.
|
|
1602
|
+
*
|
|
1603
|
+
* Selectability is a property of the *capability*, so every provider of a given
|
|
1604
|
+
* capability must agree on the flag; the loader rejects a mixed declaration.
|
|
1605
|
+
*/
|
|
1606
|
+
selectable?: boolean;
|
|
628
1607
|
/**
|
|
629
1608
|
* The local `name` of one of this manifest's declared `sidecars` that serves
|
|
630
1609
|
* the capability. The loader cross-validates it exists. Absent = an in-process
|
|
631
1610
|
* capability with no dedicated sidecar (the broker declines to proxy it).
|
|
632
1611
|
*/
|
|
633
1612
|
sidecar?: string | null;
|
|
1613
|
+
/**
|
|
1614
|
+
* WHAT this provider acts on, when the capability controls a machine or an
|
|
1615
|
+
* environment rather than answering a query.
|
|
1616
|
+
*
|
|
1617
|
+
* Exists because "swap the provider" quietly means two different things.
|
|
1618
|
+
* Swapping `web.search` from exa to tavily changes who answers; the question is
|
|
1619
|
+
* the same. Swapping `computer.control` from ghost to bytebot changes **which
|
|
1620
|
+
* computer gets typed on** — ghost drives the machine Ryu runs on, bytebot
|
|
1621
|
+
* drives the desktop `bytebotd` runs on (a containerized Linux desktop in the
|
|
1622
|
+
* shipped product). A picker that renders those two swaps identically is
|
|
1623
|
+
* telling the user something false, and until this field existed the
|
|
1624
|
+
* distinction lived only in a prose `description` that nothing structured
|
|
1625
|
+
* could read.
|
|
1626
|
+
*
|
|
1627
|
+
* Absent = not applicable or unspecified. That is the honest default for the
|
|
1628
|
+
* capabilities where locality is meaningless (`web.search`, `memory`, `rag`),
|
|
1629
|
+
* and it is deliberately NOT [`ProviderTarget::LocalMachine`]: defaulting to
|
|
1630
|
+
* "this machine" would silently mislabel every future hosted provider that
|
|
1631
|
+
* forgets to declare it.
|
|
1632
|
+
*/
|
|
1633
|
+
target?: ProviderTarget | null;
|
|
1634
|
+
/**
|
|
1635
|
+
* Capability **verb → this provider's tool** bindings, the seam that keeps the
|
|
1636
|
+
* model-visible tool surface stable across a swap.
|
|
1637
|
+
*
|
|
1638
|
+
* The key is a canonical verb from the host's capability verb table (e.g.
|
|
1639
|
+
* `"web__search"`); the value names the provider's own registered tool plus the
|
|
1640
|
+
* argument/response mapping into the canonical shape. A provider that omits a
|
|
1641
|
+
* verb simply does not serve it — the facade reports the verb unavailable
|
|
1642
|
+
* rather than guessing.
|
|
1643
|
+
*/
|
|
1644
|
+
tools?: {
|
|
1645
|
+
[k: string]: CapabilityToolBinding;
|
|
1646
|
+
};
|
|
634
1647
|
/**
|
|
635
1648
|
* The capability's own semver version (independent of the plugin version), so
|
|
636
1649
|
* a consumer's [`CapabilityReq::min_version`] floor can be checked against the
|
|
@@ -638,6 +1651,198 @@ export interface ProvidesEntry {
|
|
|
638
1651
|
*/
|
|
639
1652
|
version: string;
|
|
640
1653
|
}
|
|
1654
|
+
/**
|
|
1655
|
+
* How one capability **verb** maps onto a concrete provider tool.
|
|
1656
|
+
*
|
|
1657
|
+
* The facade tool (`web__search`, `browser__navigate`, …) is registered by the host
|
|
1658
|
+
* from its canonical verb table; at call time it resolves the capability's bound
|
|
1659
|
+
* provider, reads this binding, renames the arguments, re-enters tool dispatch on
|
|
1660
|
+
* [`Self::tool`], and maps the response back. Swapping the provider therefore
|
|
1661
|
+
* changes neither the tool id nor its schema.
|
|
1662
|
+
*/
|
|
1663
|
+
export interface CapabilityToolBinding {
|
|
1664
|
+
/**
|
|
1665
|
+
* Optional provider-shipped ADAPTER: JavaScript that maps this verb onto the
|
|
1666
|
+
* provider's tool when the shapes are too far apart for the declarative fields
|
|
1667
|
+
* above to bridge.
|
|
1668
|
+
*
|
|
1669
|
+
* The declarative path ([`Self::args`] … [`Self::response`]) stays the default
|
|
1670
|
+
* and covers the ~80% of providers that are a rename plus a field map: no code
|
|
1671
|
+
* review, no sandbox, no supply-chain surface, and a third party ships one file.
|
|
1672
|
+
* But some provider shapes no amount of JSON can express — an async job API that
|
|
1673
|
+
* must be polled (`POST /crawl` → job id → `GET /crawl/{id}`), a token vocabulary
|
|
1674
|
+
* that needs per-provider normalization, a body that must read a `pref:` value.
|
|
1675
|
+
* Growing the grammar one vendor quirk at a time pushed provider-specific logic
|
|
1676
|
+
* into shared kernel code; an adapter puts it back in the provider's own manifest.
|
|
1677
|
+
*
|
|
1678
|
+
* Present = the adapter REPLACES the declarative mapping for this verb: it
|
|
1679
|
+
* receives the canonical arguments and returns the canonical result, and
|
|
1680
|
+
* [`Self::args`] / [`Self::arg_template`] / [`Self::arg_clamp`] / [`Self::response`]
|
|
1681
|
+
* are not applied (the adapter is doing that job). [`Self::tool`] still names the
|
|
1682
|
+
* target and is still the ONLY tool the adapter can reach.
|
|
1683
|
+
*/
|
|
1684
|
+
adapter?: CapabilityAdapter | null;
|
|
1685
|
+
/**
|
|
1686
|
+
* Per-argument numeric limits this provider can actually honour, keyed by the
|
|
1687
|
+
* **canonical** argument name (before any rename).
|
|
1688
|
+
*
|
|
1689
|
+
* Exists because canonical schemas describe what agents may ask for, while
|
|
1690
|
+
* providers differ in what they accept: `web__search.limit` allows up to 100,
|
|
1691
|
+
* but Brave's `count` maxes at 20. Without this, selecting Brave turns a
|
|
1692
|
+
* perfectly valid `limit: 50` into an upstream 4xx — the swap stops being
|
|
1693
|
+
* transparent, which is the entire point of the facade. Clamping is the right
|
|
1694
|
+
* resolution rather than erroring: the caller asked for "up to N", and fewer
|
|
1695
|
+
* results is a normal outcome, whereas a failed search is not.
|
|
1696
|
+
*/
|
|
1697
|
+
arg_clamp?: {
|
|
1698
|
+
[k: string]: ArgBounds;
|
|
1699
|
+
};
|
|
1700
|
+
/**
|
|
1701
|
+
* Constant arguments merged into every call (provider-specific knobs the
|
|
1702
|
+
* canonical schema does not expose, e.g. `{"search_depth": "advanced"}`).
|
|
1703
|
+
*/
|
|
1704
|
+
arg_defaults?: {
|
|
1705
|
+
[k: string]: unknown;
|
|
1706
|
+
};
|
|
1707
|
+
/**
|
|
1708
|
+
* A request-body TEMPLATE this provider needs, with `{canonical_arg}`
|
|
1709
|
+
* placeholders substituted from the call.
|
|
1710
|
+
*
|
|
1711
|
+
* `args` renames flat keys and `[]` wraps a scalar in an array; neither can build
|
|
1712
|
+
* a NESTED shape. Real APIs need them: Mem0's write endpoint takes
|
|
1713
|
+
* `messages: [{role, content}]`, so without a template the whole write half of
|
|
1714
|
+
* that provider is unbindable — which is precisely the gap that made Ryu's
|
|
1715
|
+
* memory bridges inert while Hermes, which writes per-provider adapter CODE, had
|
|
1716
|
+
* none. This closes it declaratively instead of admitting code per provider.
|
|
1717
|
+
*
|
|
1718
|
+
* A string that is EXACTLY `"{arg}"` is replaced by that argument's value with
|
|
1719
|
+
* its JSON type preserved (`5` stays a number); a string merely CONTAINING
|
|
1720
|
+
* `{arg}` interpolates as text. An argument consumed by the template is not also
|
|
1721
|
+
* passed through, so it cannot appear twice under two names.
|
|
1722
|
+
*/
|
|
1723
|
+
arg_template?: {
|
|
1724
|
+
[k: string]: unknown;
|
|
1725
|
+
};
|
|
1726
|
+
/**
|
|
1727
|
+
* Canonical argument name → this provider's argument name. A canonical argument
|
|
1728
|
+
* with no entry is passed through under its own name; map it to the empty string
|
|
1729
|
+
* to drop it (the provider cannot express it).
|
|
1730
|
+
*/
|
|
1731
|
+
args?: {
|
|
1732
|
+
[k: string]: string;
|
|
1733
|
+
};
|
|
1734
|
+
/**
|
|
1735
|
+
* Optional response normalization into the canonical result shape. Absent = the
|
|
1736
|
+
* provider's output is returned verbatim under `{ provider, raw }`.
|
|
1737
|
+
*/
|
|
1738
|
+
response?: CapabilityResponseMap | null;
|
|
1739
|
+
/**
|
|
1740
|
+
* The provider's own fully-qualified tool id (e.g. `"exa__search"`,
|
|
1741
|
+
* `"app__firecrawl_scrape"`) that implements this verb.
|
|
1742
|
+
*/
|
|
1743
|
+
tool: string;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Provider-shipped JavaScript that maps one capability verb onto one provider tool.
|
|
1747
|
+
*
|
|
1748
|
+
* Runs in the SAME Deno sandbox as an `inline_deno` plugin tool, under the same
|
|
1749
|
+
* [`crate`-level] grant model: the providing plugin must hold `tool:execute`, so
|
|
1750
|
+
* shipping code is a visible, approvable act rather than a silent one.
|
|
1751
|
+
*
|
|
1752
|
+
* The program is handed:
|
|
1753
|
+
* - `input` — the canonical verb arguments, after layer defaults are applied.
|
|
1754
|
+
* - `defaults` — the provider's resolved `arg_defaults`, including any `pref:`
|
|
1755
|
+
* tokens already looked up. This is what lets an adapter read per-install
|
|
1756
|
+
* configuration a template could not (`arg_template` expands from the CALLER's
|
|
1757
|
+
* arguments, so it can never see a resolved preference).
|
|
1758
|
+
* - `callTool(args)` — invokes the provider's own [`CapabilityToolBinding::tool`]
|
|
1759
|
+
* and resolves to its raw response. It takes NO tool id: the target is fixed by
|
|
1760
|
+
* the manifest, so sandboxed code cannot redirect the call at another tool. An
|
|
1761
|
+
* adapter therefore grants no authority the declarative path did not already
|
|
1762
|
+
* grant — it is strictly the same single re-entry, expressed as code.
|
|
1763
|
+
*
|
|
1764
|
+
* It returns the canonical result shape, which the facade passes through unchanged.
|
|
1765
|
+
*
|
|
1766
|
+
* **Bounded by the sandbox wall-clock.** A run gets `DEFAULT_DEADLINE_SECS` of
|
|
1767
|
+
* active compute, and time spent awaiting a tool call counts against it. An
|
|
1768
|
+
* adapter that polls an async job must therefore treat "still running" as a normal
|
|
1769
|
+
* outcome to report, not something to wait out.
|
|
1770
|
+
*/
|
|
1771
|
+
export interface CapabilityAdapter {
|
|
1772
|
+
/**
|
|
1773
|
+
* The adapter body. Evaluated as the tail of a sandbox program that has already
|
|
1774
|
+
* bound `input`, `defaults`, `callTool` and `callNamed`; it `return`s the
|
|
1775
|
+
* canonical result.
|
|
1776
|
+
*
|
|
1777
|
+
* Empty in a **source** manifest that declares [`Self::code_file`] instead;
|
|
1778
|
+
* [`PluginManifest::hydrate_code_files`] fills it in at parse time and
|
|
1779
|
+
* [`PluginManifest::validate`] refuses a manifest where it is still empty.
|
|
1780
|
+
*/
|
|
1781
|
+
code?: string;
|
|
1782
|
+
/**
|
|
1783
|
+
* Path to the file holding the adapter body, relative to the plugin root
|
|
1784
|
+
* (`adapters/<verb>.js`) — the authoring form. Mutually exclusive with
|
|
1785
|
+
* [`Self::code`]; see [`PluginManifest::hydrate_code_files`].
|
|
1786
|
+
*/
|
|
1787
|
+
code_file?: string | null;
|
|
1788
|
+
/**
|
|
1789
|
+
* ADDITIONAL provider tool ids this adapter may call, beyond
|
|
1790
|
+
* [`CapabilityToolBinding::tool`], reachable from the body as
|
|
1791
|
+
* `callNamed(id, args)`.
|
|
1792
|
+
*
|
|
1793
|
+
* Exists because a whole class of real APIs is two calls, not one: an async job
|
|
1794
|
+
* API starts work at one endpoint and reads the result from another
|
|
1795
|
+
* (`POST /crawl` → job id → `GET /crawl/{id}`). A single-tool adapter cannot
|
|
1796
|
+
* express that, so those providers would stay unbindable — the gap that
|
|
1797
|
+
* excluded every async API from every layer.
|
|
1798
|
+
*
|
|
1799
|
+
* This is an ALLOWLIST fixed by the manifest and checked host-side: a name not
|
|
1800
|
+
* listed here (and not [`CapabilityToolBinding::tool`]) is refused. Sandboxed
|
|
1801
|
+
* code chooses only *among* tools the provider declared, never a tool of its
|
|
1802
|
+
* own — which is what keeps the id-taking form from becoming an escalation seam.
|
|
1803
|
+
*/
|
|
1804
|
+
tools?: string[];
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Inclusive numeric bounds a provider can honour for one canonical argument.
|
|
1808
|
+
* Integers, not floats. Every clampable canonical argument is a COUNT — result
|
|
1809
|
+
* limits, crawl depth, page caps — so `i64` is the honest type, and it keeps the
|
|
1810
|
+
* whole manifest tree `Eq` (a float would force `PartialEq`-only all the way up
|
|
1811
|
+
* through `ProvidesEntry` and `PluginManifest`) while avoiding float comparison.
|
|
1812
|
+
*/
|
|
1813
|
+
export interface ArgBounds {
|
|
1814
|
+
/**
|
|
1815
|
+
* Largest value the provider accepts. Absent = no upper bound.
|
|
1816
|
+
*/
|
|
1817
|
+
max?: number | null;
|
|
1818
|
+
/**
|
|
1819
|
+
* Smallest value the provider accepts. Absent = no lower bound.
|
|
1820
|
+
*/
|
|
1821
|
+
min?: number | null;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Normalizes one provider's response into the capability's canonical shape.
|
|
1825
|
+
*
|
|
1826
|
+
* Deliberately a flat rename table rather than a general transform language: the
|
|
1827
|
+
* canonical shapes are small and list-of-records shaped, and a manifest that can
|
|
1828
|
+
* run arbitrary extraction logic is a much larger trust surface.
|
|
1829
|
+
*/
|
|
1830
|
+
export interface CapabilityResponseMap {
|
|
1831
|
+
/**
|
|
1832
|
+
* Canonical per-item field name → the provider's field name (dotted paths
|
|
1833
|
+
* allowed). Fields with no entry are dropped from the canonical item but remain
|
|
1834
|
+
* available under the item's `raw` key.
|
|
1835
|
+
*/
|
|
1836
|
+
fields?: {
|
|
1837
|
+
[k: string]: string;
|
|
1838
|
+
};
|
|
1839
|
+
/**
|
|
1840
|
+
* Dotted path to the provider's result array within its response (e.g.
|
|
1841
|
+
* `"results"`, `"data.items"`). Absent = the response itself is the array, or —
|
|
1842
|
+
* when it is not an array — a single record.
|
|
1843
|
+
*/
|
|
1844
|
+
results?: string | null;
|
|
1845
|
+
}
|
|
641
1846
|
/**
|
|
642
1847
|
* `requires` block — the plugin's **plugin-to-plugin** dependencies.
|
|
643
1848
|
*
|
|
@@ -719,7 +1924,7 @@ export interface CapabilityReq {
|
|
|
719
1924
|
min_version?: string | null;
|
|
720
1925
|
}
|
|
721
1926
|
/**
|
|
722
|
-
* A single Runnable entry inside a `
|
|
1927
|
+
* A single Runnable entry inside a `manifest.json` manifest.
|
|
723
1928
|
*
|
|
724
1929
|
* Each entry carries the identity fields from [`crate::runnable::RunnableMeta`]
|
|
725
1930
|
* plus an optional typed config blob. The `kind` field drives which config shape
|
|
@@ -945,6 +2150,20 @@ export interface SidecarSpec {
|
|
|
945
2150
|
* How Core obtains and runs the process.
|
|
946
2151
|
*/
|
|
947
2152
|
process: BinarySpec | ExternalRuntimeConfig1 | LocalProcessSpec | NodeProcessSpec;
|
|
2153
|
+
/**
|
|
2154
|
+
* Optional **model-provider** declaration: when present, this sidecar serves an
|
|
2155
|
+
* OpenAI-compatible endpoint and Core registers it as a selectable provider once
|
|
2156
|
+
* the process reports healthy, then deregisters it when the plugin is disabled or
|
|
2157
|
+
* uninstalled. This is what makes a third-party *auth bridge* possible without a
|
|
2158
|
+
* Core change: the plugin performs its own login/refresh, serves `/v1`, and
|
|
2159
|
+
* declares that fact here. Absent = the sidecar is not a model provider.
|
|
2160
|
+
*
|
|
2161
|
+
* A sidecar cannot self-register: it holds only `RYU_EXT_TOKEN` (scoped to the
|
|
2162
|
+
* ext-proxy hop and `/api/host/*`), and the host-RPC vocabulary has no
|
|
2163
|
+
* provider-registration method. Registration is therefore Core-side, driven by
|
|
2164
|
+
* this declaration.
|
|
2165
|
+
*/
|
|
2166
|
+
provides_provider?: ProviderRegistrationSpec | null;
|
|
948
2167
|
}
|
|
949
2168
|
/**
|
|
950
2169
|
* Declares the host-API grant subset a sidecar *process* may exercise via the
|
|
@@ -1069,6 +2288,47 @@ export interface LocalProcessSpec {
|
|
|
1069
2288
|
export interface NodeProcessSpec {
|
|
1070
2289
|
kind: "node";
|
|
1071
2290
|
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Declares that a [`SidecarSpec`] serves an OpenAI-compatible model endpoint Core
|
|
2293
|
+
* should register as a provider while the sidecar is healthy.
|
|
2294
|
+
*
|
|
2295
|
+
* Security posture: the declared [`id`] is validated against the built-in provider
|
|
2296
|
+
* table at registration and a collision is REFUSED, never merged. Without that guard
|
|
2297
|
+
* a plugin could claim a built-in id (`openai-codex`, `anthropic`) and silently
|
|
2298
|
+
* redirect the user's subscription traffic — and their live bearer token — to an
|
|
2299
|
+
* attacker-controlled `baseUrl`. Core also stamps [`OWNER_FIELD`] into the written
|
|
2300
|
+
* entry so deregistration can only ever remove an entry this plugin created.
|
|
2301
|
+
*
|
|
2302
|
+
* [`id`]: ProviderRegistrationSpec::id
|
|
2303
|
+
* [`OWNER_FIELD`]: crate::schema::PROVIDER_OWNER_FIELD
|
|
2304
|
+
*/
|
|
2305
|
+
export interface ProviderRegistrationSpec {
|
|
2306
|
+
/**
|
|
2307
|
+
* Pi `api` type the endpoint speaks. Defaults to `"openai-completions"`.
|
|
2308
|
+
*/
|
|
2309
|
+
api?: string | null;
|
|
2310
|
+
/**
|
|
2311
|
+
* Path prefix appended to `http://127.0.0.1:<port>` to form the provider's
|
|
2312
|
+
* `baseUrl`. Defaults to `"/v1"`.
|
|
2313
|
+
*/
|
|
2314
|
+
base_path?: string | null;
|
|
2315
|
+
/**
|
|
2316
|
+
* Provider id as it appears in the model picker. Must not collide with a built-in
|
|
2317
|
+
* provider id, and must be a safe single token (lowercase alphanumerics, `-`, `_`).
|
|
2318
|
+
*/
|
|
2319
|
+
id: string;
|
|
2320
|
+
/**
|
|
2321
|
+
* Human-readable label for the picker. Defaults to [`id`] when absent.
|
|
2322
|
+
*
|
|
2323
|
+
* [`id`]: ProviderRegistrationSpec::id
|
|
2324
|
+
*/
|
|
2325
|
+
label?: string | null;
|
|
2326
|
+
/**
|
|
2327
|
+
* Optional model ids to seed the entry with, for an endpoint whose `GET /models`
|
|
2328
|
+
* discovery is unavailable or slow. Absent = rely on discovery.
|
|
2329
|
+
*/
|
|
2330
|
+
models?: string[];
|
|
2331
|
+
}
|
|
1072
2332
|
/**
|
|
1073
2333
|
* One [`PluginManifest::surfaces`] entry: the support level plus an optional UI
|
|
1074
2334
|
* descriptor the surface shell resolves (opaque here — pure data).
|
|
@@ -1083,7 +2343,7 @@ export interface SurfaceEntry {
|
|
|
1083
2343
|
/**
|
|
1084
2344
|
* How much of the plugin this surface supports.
|
|
1085
2345
|
*/
|
|
1086
|
-
support?: "full" | "limited" | "list" | "commands" | "none";
|
|
2346
|
+
support?: "full" | "limited" | "list" | "commands" | "none" | "unknown";
|
|
1087
2347
|
/**
|
|
1088
2348
|
* Optional surface-specific UI descriptor (bundle id, mount point, …),
|
|
1089
2349
|
* interpreted by the surface's app host. Opaque to the contract.
|