@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.
- package/LICENSE +179 -0
- package/README.md +31 -0
- package/dist/agent.cjs +761 -0
- package/dist/agent.d.cts +3 -0
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +23 -0
- package/dist/chunk-GXHL5CO7.js +353 -0
- package/dist/chunk-KPKMMGVC.js +671 -0
- package/dist/chunk-ODFEUVPW.js +100 -0
- package/dist/cli.cjs +858 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +454 -0
- package/dist/index-CEbS1SlS.d.cts +988 -0
- package/dist/index-DAxq7Y0R.d.ts +988 -0
- package/dist/index.cjs +1900 -0
- package/dist/index.d.cts +759 -0
- package/dist/index.d.ts +759 -0
- package/dist/index.js +771 -0
- package/dist/manifest.cjs +399 -0
- package/dist/manifest.d.cts +355 -0
- package/dist/manifest.d.ts +355 -0
- package/dist/manifest.js +38 -0
- package/package.json +56 -0
- package/src/agent/agent.ts +208 -0
- package/src/agent/index.ts +51 -0
- package/src/agent/loop.test.ts +261 -0
- package/src/agent/loop.ts +259 -0
- package/src/agent/model-call.ts +190 -0
- package/src/agent/query.ts +40 -0
- package/src/agent/tools.ts +295 -0
- package/src/builder.ts +473 -0
- package/src/cli/dev.test.ts +178 -0
- package/src/cli/dev.ts +425 -0
- package/src/cli.ts +390 -0
- package/src/contracts-lockstep.test.ts +77 -0
- package/src/generated/plugin-manifest.ts +1121 -0
- package/src/index.ts +141 -0
- package/src/manifest.test.ts +610 -0
- package/src/manifest.ts +589 -0
- package/src/mcp/bridge.test.ts +196 -0
- package/src/mcp/client.ts +253 -0
- package/src/mcp/fixture-server.ts +23 -0
- package/src/mcp/server.ts +351 -0
- package/src/model/client.test.ts +107 -0
- package/src/model/client.ts +179 -0
- package/src/model/gateway.ts +41 -0
- package/src/plugin/ryu-plugin.ts +191 -0
- package/src/runnable/agent.ts +338 -0
- package/src/runnable/app.ts +233 -0
- package/src/runnable/index.ts +61 -0
- package/src/runnable/primitives-hostapi.test.ts +73 -0
- package/src/runnable/primitives.test.ts +286 -0
- package/src/runnable/primitives.ts +610 -0
- package/src/runnable/runnable-types.ts +113 -0
- package/src/runnable/runnable.test.ts +397 -0
- package/src/runnable/skill.ts +60 -0
- package/src/runnable/tool.ts +260 -0
- package/src/runnable/turn-hook.test.ts +81 -0
- package/src/runnable/turn-hook.ts +191 -0
- 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
|
+
}
|