@ryuhq/sdk 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +51 -2
  2. package/dist/action.cjs +839 -0
  3. package/dist/action.d.cts +88 -0
  4. package/dist/action.d.ts +88 -0
  5. package/dist/action.js +8 -0
  6. package/dist/agent-plugin.d.cts +1 -1
  7. package/dist/agent-plugin.d.ts +1 -1
  8. package/dist/agent.cjs +7 -0
  9. package/dist/agent.d.cts +1 -1
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +2 -2
  12. package/dist/{app-DNaGmLVf.d.cts → app-B0Z9Ew_R.d.cts} +18 -6
  13. package/dist/{app-Bkw7LlCK.d.ts → app-C-BDJwfG.d.ts} +18 -6
  14. package/dist/builder.cjs +110 -11
  15. package/dist/builder.d.cts +7 -2
  16. package/dist/builder.d.ts +7 -2
  17. package/dist/builder.js +4 -3
  18. package/dist/chunk-4TPUZDTI.js +94 -0
  19. package/dist/{chunk-T5676WL2.js → chunk-BC3A7HMO.js} +1 -77
  20. package/dist/{chunk-IOLP5FFE.js → chunk-HLKJZAFK.js} +9 -2
  21. package/dist/{chunk-IEUQ3CDG.js → chunk-NZKVOSC2.js} +71 -5
  22. package/dist/chunk-QYFUNJOH.js +83 -0
  23. package/dist/{chunk-W3KPP4WN.js → chunk-SN2QBJUF.js} +20 -7
  24. package/dist/{chunk-ULSVL7EC.js → chunk-Z57QDDJR.js} +1 -1
  25. package/dist/{chunk-A3RGEPDG.js → chunk-ZTJWBRUL.js} +32 -0
  26. package/dist/cli.cjs +93 -8
  27. package/dist/cli.js +33 -8
  28. package/dist/index.cjs +297 -87
  29. package/dist/index.d.cts +10 -9
  30. package/dist/index.d.ts +10 -9
  31. package/dist/index.js +34 -19
  32. package/dist/manifest.cjs +76 -6
  33. package/dist/manifest.d.cts +157 -5
  34. package/dist/manifest.d.ts +157 -5
  35. package/dist/manifest.js +9 -1
  36. package/dist/mcp/server.d.cts +2 -1
  37. package/dist/mcp/server.d.ts +2 -1
  38. package/dist/runnable.cjs +279 -71
  39. package/dist/runnable.d.cts +6 -3
  40. package/dist/runnable.d.ts +6 -3
  41. package/dist/runnable.js +11 -5
  42. package/dist/{tool-DSx2bFx8.d.ts → tool-AjkdFvhE.d.ts} +54 -4
  43. package/dist/{tool-u-VR0fLF.d.cts → tool-CgzW92O_.d.cts} +54 -4
  44. package/package.json +11 -2
  45. package/src/agent-plugin.ts +1 -1
  46. package/src/builder.ts +9 -0
  47. package/src/cli-security.test.ts +109 -0
  48. package/src/cli.ts +43 -10
  49. package/src/contracts-lockstep.test.ts +16 -2
  50. package/src/exports-lockstep.test.ts +1 -0
  51. package/src/generated/plugin-manifest.ts +82 -5
  52. package/src/index.ts +18 -0
  53. package/src/manifest-schema.test.ts +1 -1
  54. package/src/manifest.fixtures.test.ts +9 -3
  55. package/src/manifest.test.ts +69 -0
  56. package/src/manifest.ts +137 -18
  57. package/src/mcp/server.ts +2 -1
  58. package/src/runnable/action.test.ts +128 -0
  59. package/src/runnable/action.ts +202 -0
  60. package/src/runnable/app.ts +45 -12
  61. package/src/runnable/index.ts +18 -3
  62. package/src/runnable/primitives.test.ts +34 -0
  63. package/src/runnable/primitives.ts +59 -0
  64. package/src/runnable/runnable-types.ts +3 -0
  65. package/src/runnable/tool.ts +35 -4
  66. package/src/runnable/turn-hook.ts +4 -2
  67. package/src/slash-command.test.ts +69 -0
@@ -278,6 +278,36 @@ interface BackgroundClient {
278
278
  process_id: string;
279
279
  }>;
280
280
  }
281
+ /** Durable app-owned key/value state (`storage:kv` via the host bridge). */
282
+ interface StorageClient {
283
+ /** Atomically replace a value when it still equals `expected`. */
284
+ compareAndSet(input: {
285
+ expected: string | null;
286
+ key: string;
287
+ namespace?: string;
288
+ value: string | null;
289
+ }): Promise<boolean>;
290
+ /** Delete one value. */
291
+ delete(input: {
292
+ key: string;
293
+ namespace?: string;
294
+ }): Promise<void>;
295
+ /** Read one value; missing keys return `null`. */
296
+ get(input: {
297
+ key: string;
298
+ namespace?: string;
299
+ }): Promise<string | null>;
300
+ /** List keys in an app-owned namespace. */
301
+ keys(input?: {
302
+ namespace?: string;
303
+ }): Promise<string[]>;
304
+ /** Write one string value. JSON can be encoded by the app when needed. */
305
+ set(input: {
306
+ key: string;
307
+ namespace?: string;
308
+ value: string;
309
+ }): Promise<void>;
310
+ }
281
311
  /** TTS primitive — speech synthesis (`crates/ryu-tts`). */
282
312
  interface TtsClient {
283
313
  /**
@@ -330,6 +360,7 @@ interface RyuPrimitives {
330
360
  memory: MemoryClient;
331
361
  rag: RagClient;
332
362
  realtime: RealtimeClient;
363
+ storage: StorageClient;
333
364
  stt: SttClient;
334
365
  tts: TtsClient;
335
366
  }
@@ -393,6 +424,8 @@ interface RunnableContext {
393
424
  sessionId?: string;
394
425
  /** Signal to abort a long-running run. */
395
426
  signal?: AbortSignal;
427
+ /** Durable app-owned KV state, gated by the manifest's `storage:kv` grant. */
428
+ storage?: StorageClient;
396
429
  /** STT primitive: transcribe (`crates/ryu-stt`). */
397
430
  stt?: SttClient;
398
431
  /** TTS primitive: speak (`crates/ryu-tts`). */
@@ -472,9 +505,13 @@ interface ToolSchema {
472
505
  required?: string[];
473
506
  /** Type is always "object" for a tool's top-level input schema. */
474
507
  type: "object";
508
+ /** Preserve additional JSON Schema keywords for Core/MCP consumers. */
509
+ [key: string]: unknown;
475
510
  }
476
511
  /** Options accepted by `defineTool`. */
477
512
  interface ToolOptions<TInput extends Record<string, unknown>, TOutput> {
513
+ /** Optional description surfaced to models and tool discovery. */
514
+ description?: string;
478
515
  /** Stable unique identifier (e.g. "tool-web-search"). */
479
516
  id: string;
480
517
  /** Human-readable display name. */
@@ -514,6 +551,8 @@ interface ToolRunnable<TInput extends Record<string, unknown> = Record<string, u
514
551
  * the sandbox form is the second parameter aliased to `host`.
515
552
  */
516
553
  readonly code: string;
554
+ /** Optional description surfaced to models and tool discovery. */
555
+ readonly description?: string;
517
556
  readonly kind: "tool";
518
557
  /** JSON Schema for this tool's input — compatible with Core's ToolInfo.schema. */
519
558
  readonly schema: ToolSchema;
@@ -548,6 +587,19 @@ interface ToolRunnable<TInput extends Record<string, unknown> = Record<string, u
548
587
  * ```
549
588
  */
550
589
  declare function defineTool<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput = unknown>(options: ToolOptions<TInput, TOutput>): ToolRunnable<TInput, TOutput>;
590
+ /** Optional metadata lowered alongside a tool's executable backend. */
591
+ interface InlineToolManifestOptions {
592
+ /** Marks the entry as a semantic Ryu Action. */
593
+ action?: boolean;
594
+ /** MCP-style effect annotations, for example `readOnlyHint`. */
595
+ annotations?: Record<string, boolean | undefined>;
596
+ /** Description shown in Core's unified tool catalog. */
597
+ description?: string;
598
+ /** Force the human approval gate before the tool runs. */
599
+ needsApproval?: boolean;
600
+ /** JSON Schema for the structured result. */
601
+ outputSchema?: Record<string, unknown>;
602
+ }
551
603
  /**
552
604
  * Convert a {@link ToolRunnable} into a `manifest.json` `kind:"tool"` runnable that
553
605
  * ships its `run` body as Core's `inline_deno` backend. The emitted config
@@ -558,8 +610,6 @@ declare function defineTool<TInput extends Record<string, unknown> = Record<stri
558
610
  *
559
611
  * The plugin must declare the `tool:execute` grant (see `definePlugin`).
560
612
  */
561
- declare function inlineToolRunnable(tool: ToolRunnable, options?: {
562
- description?: string;
563
- }): RunnableMeta;
613
+ declare function inlineToolRunnable(tool: Pick<ToolRunnable, "code" | "description" | "id" | "name" | "schema">, options?: InlineToolManifestOptions): RunnableMeta;
564
614
 
565
- export { type BackgroundClient as B, type DurableClient as D, type EnginesClient as E, type GatewayClient as G, type HttpPrimitiveTransportOptions as H, type ImageClient as I, type JsonSchemaProperty as J, type MemoryClient as M, PRIMITIVE_BINDINGS as P, type RagChunk as R, type SttClient as S, type ToolRunnable as T, type BackgroundProcess as a, type MemoryItem as b, type PrimitiveBinding as c, type PrimitiveTransport as d, type RagClient as e, type RagRerankResult as f, type RealtimeClient as g, type RealtimeSubscription as h, type Runnable as i, type RunnableContext as j, type RyuPrimitives as k, type ToolOptions as l, type ToolSchema as m, type TtsClient as n, createPrimitives as o, defineTool as p, httpPrimitiveTransport as q, inlineToolRunnable as r };
615
+ export { type BackgroundClient as B, type DurableClient as D, type EnginesClient as E, type GatewayClient as G, type HttpPrimitiveTransportOptions as H, type ImageClient as I, type JsonSchemaProperty as J, type MemoryClient as M, PRIMITIVE_BINDINGS as P, type RagChunk as R, type StorageClient as S, type ToolRunnable as T, type BackgroundProcess as a, type InlineToolManifestOptions as b, type MemoryItem as c, type PrimitiveBinding as d, type PrimitiveTransport as e, type RagClient as f, type RagRerankResult as g, type RealtimeClient as h, type RealtimeSubscription as i, type Runnable as j, type RunnableContext as k, type RyuPrimitives as l, type SttClient as m, type ToolOptions as n, type ToolSchema as o, type TtsClient as p, createPrimitives as q, defineTool as r, httpPrimitiveTransport as s, inlineToolRunnable as t };
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@ryuhq/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Ryu developer SDK: typed builders and CLI for authoring manifest.json Plugin bundles",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/amajorai/ryu"
9
+ },
6
10
  "main": "./dist/index.cjs",
7
11
  "types": "./dist/index.d.ts",
8
12
  "exports": {
@@ -26,6 +30,11 @@
26
30
  "import": "./dist/agent.js",
27
31
  "require": "./dist/agent.cjs"
28
32
  },
33
+ "./action": {
34
+ "types": "./dist/action.d.ts",
35
+ "import": "./dist/action.js",
36
+ "require": "./dist/action.cjs"
37
+ },
29
38
  "./model": {
30
39
  "types": "./dist/model.d.ts",
31
40
  "import": "./dist/model.js",
@@ -84,7 +93,7 @@
84
93
  "clean": "rm -rf dist"
85
94
  },
86
95
  "dependencies": {
87
- "@ryuhq/sdk-native": "0.2.0",
96
+ "@ryuhq/sdk-native": "0.2.3",
88
97
  "zod": "^4.1.13"
89
98
  },
90
99
  "devDependencies": {
@@ -29,7 +29,7 @@
29
29
  * is deliberately NOT a copy of the whole native manifest — that would be a second
30
30
  * source of truth with a stale-copy failure mode.
31
31
  *
32
- * Both `plugins-store/*` and `apps-store/*` use this one `PluginManifest` shape, so
32
+ * Both `plugins-store/{plugins,lsp,external_plugins}/*` and `apps-store/*` use this one `PluginManifest` shape, so
33
33
  * one converter covers both stores.
34
34
  */
35
35
 
package/src/builder.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  CompanionSurface,
17
17
  PluginManifest,
18
18
  RunnableMeta,
19
+ SlashCommandContribution,
19
20
  Surface,
20
21
  } from "./manifest.ts";
21
22
  import { PluginManifestSchema, RunnableMetaSchema } from "./manifest.ts";
@@ -325,6 +326,7 @@ export class AppBuilder {
325
326
  private readonly _dependencies: AppDependency[] = [];
326
327
  private readonly _requiredCapabilities: CapabilityReq[] = [];
327
328
  private readonly _requiredGrants: string[] = [];
329
+ private readonly _slashCommands: SlashCommandContribution[] = [];
328
330
  private readonly _targets: Surface[] = [];
329
331
 
330
332
  /** Set the reverse-domain app id (e.g. `"com.example.checklist"`). */
@@ -393,6 +395,12 @@ export class AppBuilder {
393
395
  return this;
394
396
  }
395
397
 
398
+ /** Add a slash command and its optional sequential argument choices. */
399
+ slashCommand(command: SlashCommandContribution): this {
400
+ this._slashCommands.push(command);
401
+ return this;
402
+ }
403
+
396
404
  /**
397
405
  * Declare a **plugin-to-plugin dependency** (auto-enabled, in dependency order,
398
406
  * before this app). `minVersion` is a MINIMUM (`"1.2.0"` = `">=1.2.0"`).
@@ -448,6 +456,7 @@ export class AppBuilder {
448
456
  uiEntry: this._uiEntry,
449
457
  tools: this._tools,
450
458
  grants: this._grants,
459
+ slashCommands: this._slashCommands,
451
460
  ...(this._server ? { server: this._server } : {}),
452
461
  ...(this._displayMode ? { displayMode: this._displayMode } : {}),
453
462
  ...(this._mime ? { mime: this._mime } : {}),
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ symlinkSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+
15
+ const REPO_ROOT = join(import.meta.dir, "../../..");
16
+ const CLI = join(import.meta.dir, "cli.ts");
17
+ const temporaryDirectories: string[] = [];
18
+
19
+ afterEach(() => {
20
+ for (const directory of temporaryDirectories.splice(0)) {
21
+ rmSync(directory, { force: true, recursive: true });
22
+ }
23
+ });
24
+
25
+ function temporaryPackage(): string {
26
+ const directory = mkdtempSync(join(tmpdir(), "ryu-sdk-pack-"));
27
+ temporaryDirectories.push(directory);
28
+ return directory;
29
+ }
30
+
31
+ function runPack(directory: string) {
32
+ return spawnSync(process.execPath, [CLI, "pack", directory], {
33
+ encoding: "utf8",
34
+ });
35
+ }
36
+
37
+ describe.skipIf(process.platform === "win32")(
38
+ "ryu pack package containment",
39
+ () => {
40
+ it("rejects external symlinks for hooks, adapters, and output styles", () => {
41
+ const scenarios = [
42
+ {
43
+ manifest: "plugins-store/plugins/tool-firewall/manifest.json",
44
+ linkedFile: "hooks/pre.js",
45
+ copiedFiles: ["hooks/post.js"],
46
+ },
47
+ {
48
+ manifest:
49
+ "plugins-store/external_plugins/cloudflare-browser-run/manifest.json",
50
+ linkedFile: "adapters/browser.navigate.js",
51
+ copiedFiles: [
52
+ "adapters/browser.screenshot.js",
53
+ "adapters/browser.snapshot.js",
54
+ ],
55
+ },
56
+ {
57
+ manifest: "plugins-store/plugins/output-styles/manifest.json",
58
+ linkedFile: "output-styles/i-have-adhd.md",
59
+ copiedFiles: [
60
+ "output-styles/bro.md",
61
+ "output-styles/eli5.md",
62
+ "output-styles/explanatory.md",
63
+ "output-styles/gen-z.md",
64
+ "output-styles/learning.md",
65
+ "output-styles/no-ai-slop.md",
66
+ "output-styles/no-hype.md",
67
+ "output-styles/plain-technical.md",
68
+ "output-styles/plain-text.md",
69
+ "output-styles/proactive.md",
70
+ ],
71
+ },
72
+ ];
73
+
74
+ for (const scenario of scenarios) {
75
+ const directory = temporaryPackage();
76
+ const sourceRoot = dirname(join(REPO_ROOT, scenario.manifest));
77
+ writeFileSync(
78
+ join(directory, "manifest.json"),
79
+ readFileSync(join(REPO_ROOT, scenario.manifest))
80
+ );
81
+ for (const file of scenario.copiedFiles) {
82
+ mkdirSync(dirname(join(directory, file)), { recursive: true });
83
+ writeFileSync(
84
+ join(directory, file),
85
+ readFileSync(join(sourceRoot, file))
86
+ );
87
+ }
88
+ mkdirSync(dirname(join(directory, scenario.linkedFile)), {
89
+ recursive: true,
90
+ });
91
+ writeFileSync(
92
+ join(directory, scenario.linkedFile),
93
+ readFileSync(join(sourceRoot, scenario.linkedFile))
94
+ );
95
+ const workingResult = runPack(directory);
96
+ expect(workingResult.status).toBe(0);
97
+ unlinkSync(join(directory, scenario.linkedFile));
98
+
99
+ const external = join(dirname(directory), `${Date.now()}-outside.txt`);
100
+ writeFileSync(external, "private host data");
101
+ symlinkSync(external, join(directory, scenario.linkedFile));
102
+
103
+ const result = runPack(directory);
104
+ expect(result.status).toBe(1);
105
+ rmSync(external, { force: true });
106
+ }
107
+ });
108
+ }
109
+ );
package/src/cli.ts CHANGED
@@ -27,10 +27,12 @@ import {
27
27
  existsSync,
28
28
  mkdirSync,
29
29
  readFileSync,
30
+ realpathSync,
30
31
  rmSync,
31
32
  writeFileSync,
33
+ writeSync,
32
34
  } from "node:fs";
33
- import { join, resolve } from "node:path";
35
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
34
36
  import {
35
37
  AGENT_PLUGIN_MANIFEST_FILE,
36
38
  AGENT_PLUGIN_MCP_FILE,
@@ -70,7 +72,7 @@ function printUsage(): void {
70
72
  }
71
73
 
72
74
  function exitError(message: string): never {
73
- process.stderr.write(`error: ${message}\n`);
75
+ writeSync(process.stderr.fd, `error: ${message}\n`);
74
76
  process.exit(1);
75
77
  }
76
78
 
@@ -148,6 +150,28 @@ function loadManifest(dir: string): LoadedManifest {
148
150
  const CODE_FILE_DIRS = ["hooks", "adapters"];
149
151
  const CODE_FILE_PATH = /^(hooks|adapters)\/[A-Za-z0-9_][A-Za-z0-9._-]*\.m?js$/;
150
152
 
153
+ function containedPackageFile(dir: string, rel: string, label: string): string {
154
+ let realRoot: string;
155
+ let realTarget: string;
156
+ try {
157
+ realRoot = realpathSync(dir);
158
+ realTarget = realpathSync(join(dir, rel));
159
+ } catch (error) {
160
+ exitError(`${label}: could not resolve '${rel}': ${String(error)}`);
161
+ }
162
+ const fromRoot = relative(realRoot, realTarget);
163
+ if (
164
+ fromRoot === ".." ||
165
+ fromRoot.startsWith(`..${sep}`) ||
166
+ isAbsolute(fromRoot)
167
+ ) {
168
+ exitError(
169
+ `${label}: '${rel}' resolves outside the plugin package (${realTarget})`
170
+ );
171
+ }
172
+ return realTarget;
173
+ }
174
+
151
175
  /**
152
176
  * Read one `code_file` and return its contents, or exit with a clear error.
153
177
  *
@@ -161,7 +185,7 @@ function readCodeFile(dir: string, rel: string, label: string): string {
161
185
  `${label}: code_file '${rel}' must be exactly '<${CODE_FILE_DIRS.join("|")}>/<name>.js' with no traversal`
162
186
  );
163
187
  }
164
- const path = join(dir, rel);
188
+ const path = containedPackageFile(dir, rel, label);
165
189
  let body: string;
166
190
  try {
167
191
  body = readFileSync(path, "utf8");
@@ -275,7 +299,7 @@ function inlineOutputStyleFiles(
275
299
  }
276
300
  let body: string;
277
301
  try {
278
- body = readFileSync(join(dir, rel), "utf8");
302
+ body = readFileSync(containedPackageFile(dir, rel, label), "utf8");
279
303
  } catch (err) {
280
304
  exitError(`${label}: could not read file '${rel}': ${String(err)}`);
281
305
  }
@@ -324,12 +348,12 @@ function resolveUiEntry(manifest: LoadedManifest): string | null {
324
348
  return null;
325
349
  }
326
350
 
327
- // Resolve a companion's `ui_format` discriminator. `"html"` (Path B) means the
328
- // `ui_entry` file is ALREADY a self-contained HTML document (a
329
- // vite-plugin-singlefile build for a heavy app like the whiteboard) and must be
330
- // shipped VERBATIM as `ui_code` — NOT run through `Bun.build`, which would try to
331
- // bundle an HTML file as an ESM entry and fail. Anything else (absent / `"js"`) is
332
- // the default: `ui_entry` is an ESM module `Bun.build` bundles into `ui_code`.
351
+ // Resolve a UI entry's format. `"html"` means the `ui_entry` file is ALREADY a
352
+ // self-contained HTML document (a vite-plugin-singlefile build for a heavy
353
+ // companion, or a hand-authored widget) and must be shipped VERBATIM as
354
+ // `ui_code` — NOT run through `Bun.build`, which would try to bundle an HTML
355
+ // file as an ESM entry and fail. Anything else (absent / `"js"`) is the default:
356
+ // `ui_entry` is an ESM module `Bun.build` bundles into `ui_code`.
333
357
  function resolveUiFormat(manifest: LoadedManifest): "html" | "js" {
334
358
  for (const runnable of manifest.runnables) {
335
359
  if (runnable.kind !== "companion") {
@@ -341,6 +365,15 @@ function resolveUiFormat(manifest: LoadedManifest): "html" | "js" {
341
365
  return "html";
342
366
  }
343
367
  }
368
+ for (const widget of manifest.contributes?.widgets ?? []) {
369
+ const entry = widget.ui_entry;
370
+ if (
371
+ typeof entry === "string" &&
372
+ entry.trim().toLowerCase().endsWith(".html")
373
+ ) {
374
+ return "html";
375
+ }
376
+ }
344
377
  return "js";
345
378
  }
346
379
 
@@ -9,7 +9,7 @@
9
9
  * Schema, and the generated types stay describing the same manifest:
10
10
  *
11
11
  * 1. a known-good repo manifest (apps-store/mail/manifest.json — the first
12
- * fully manifest-driven app) parses with the zod `PluginManifestSchema`;
12
+ * fully manifest-driven app) parses without losing any top-level wire field;
13
13
  * 2. every key the schema marks required exists in the schema, the generated
14
14
  * TS, and the fixture;
15
15
  * 3. every top-level key the fixture uses is a key the wire model knows.
@@ -42,12 +42,26 @@ const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf8")) as {
42
42
  properties: Record<string, unknown>;
43
43
  };
44
44
  const generatedSource = readFileSync(GENERATED_PATH, "utf8");
45
+ const CORE_FIELDS_PREVIOUSLY_STRIPPED = [
46
+ "permission_levels",
47
+ "sidecars",
48
+ "stability",
49
+ "surfaces",
50
+ ] as const;
45
51
 
46
52
  describe("contracts lockstep (zod ↔ blessed JSON Schema ↔ generated TS)", () => {
47
- test("the known-good mail manifest.json parses with the zod authoring schema", () => {
53
+ test("the known-good mail manifest survives the zod pack path without field loss", () => {
48
54
  const parsed = PluginManifestSchema.parse(fixture);
49
55
  expect(parsed.id).toBe("@ryu/mail");
50
56
  expect(parsed.runnables.length).toBeGreaterThan(0);
57
+ for (const key of Object.keys(fixture)) {
58
+ expect(Object.hasOwn(parsed, key), key).toBe(true);
59
+ }
60
+ for (const key of CORE_FIELDS_PREVIOUSLY_STRIPPED) {
61
+ expect(schema.properties).toHaveProperty(key);
62
+ expect(fixture).toHaveProperty(key);
63
+ expect(parsed[key]).toEqual(fixture[key]);
64
+ }
51
65
  });
52
66
 
53
67
  test("blessed schema describes PluginManifest with the required identity keys", () => {
@@ -57,6 +57,7 @@ describe("package exports ↔ tsup entries lockstep", () => {
57
57
  // Each of these is referenced by real consumers (fumadocs cookbooks,
58
58
  // examples/, the desktop host, create-ryu-app) and must never 404.
59
59
  const documented = [
60
+ "action",
60
61
  "manifest",
61
62
  "agent",
62
63
  "model",
@@ -374,7 +374,7 @@ export interface PluginManifest {
374
374
  permission_levels?: PermissionLevel[];
375
375
  /**
376
376
  * **Unified, deny-by-default runtime permission set** — the single typed
377
- * grammar (`{fs, child_process, network, tool}`) Core lowers to every sandbox
377
+ * grammar (`{fs, child_process, run, network, tool}`) Core lowers to every sandbox
378
378
  * backend (wasmtime WASI preopens, Docker `--mount`/`--network` flags, Deno
379
379
  * `--allow-*` flags). Absent = **deny-all** (the default for every manifest
380
380
  * predating this field), so an app that declares nothing keeps today's exact
@@ -578,7 +578,8 @@ export interface CompanionSurface {
578
578
  * and reference no runnable at all (`widgets`, `views`, `dock_panels`,
579
579
  * `sidebar_sections`, `sidebar_buttons`, `settings_tabs`, `composer_controls`,
580
580
  * `chat_features`, `slash_commands`, `turn_hooks`, `tool_filters`, `lsp_servers`,
581
- * `message_actions`, `context_menu_items`, `agent_edit_panels`).
581
+ * `message_actions`, `selection_actions`, `context_menu_items`,
582
+ * `agent_edit_panels`).
582
583
  *
583
584
  * # Extending
584
585
  *
@@ -961,6 +962,16 @@ export interface Contributes {
961
962
  * Gateway policies the plugin contributes (referenced by runnable id).
962
963
  */
963
964
  policies?: ContributionId[];
965
+ /**
966
+ * Buttons the plugin contributes to the floating text-selection toolbar.
967
+ * This is the bridge between enabled apps/plugins and shared chat blocks:
968
+ * Core validates and tags the declaration, while the desktop owns the
969
+ * rendered toolbar and dispatches the selected text. A selection action may
970
+ * either name a granted `capability` or provide a host-owned `args.dispatch`
971
+ * (for example, a first-party shell action such as Side Chat). Self-contained
972
+ * + opaque for the same forward-compatibility reason as `message_actions`.
973
+ */
974
+ selection_actions?: SelectionActionContribution[];
964
975
  /**
965
976
  * Declarative settings tabs the plugin contributes (model pickers, text
966
977
  * fields bound to preference keys). Served + rendered the same way.
@@ -1720,6 +1731,49 @@ export interface PiExtensionContribution {
1720
1731
  */
1721
1732
  id: string;
1722
1733
  }
1734
+ /**
1735
+ * One button a plugin contributes to the floating text-selection toolbar (see
1736
+ * [`Contributes::selection_actions`]).
1737
+ *
1738
+ * `capability` is optional because a host-owned renderer can use an opaque
1739
+ * `args.dispatch` bridge instead. The desktop never executes manifest code: it
1740
+ * only renders this label and forwards the selected text to the owning host
1741
+ * handler.
1742
+ */
1743
+ export interface SelectionActionContribution {
1744
+ /**
1745
+ * Static renderer/dispatch arguments. The selected text is supplied by the
1746
+ * host at click time and is never serialized into the manifest.
1747
+ */
1748
+ args?: {
1749
+ [k: string]: unknown;
1750
+ };
1751
+ /**
1752
+ * Optional granted capability for a plugin-owned dispatch.
1753
+ */
1754
+ capability?: string | null;
1755
+ /**
1756
+ * Optional glyph id resolved by the shell's icon primitive.
1757
+ */
1758
+ icon?: string | null;
1759
+ /**
1760
+ * Stable id for this action within the plugin.
1761
+ */
1762
+ id: string;
1763
+ /**
1764
+ * Render mode. The current desktop renders `"button"`; this remains open
1765
+ * so newer shells can add a mode without making older cores reject it.
1766
+ */
1767
+ kind: string;
1768
+ /**
1769
+ * Accessible label shown in the selection toolbar.
1770
+ */
1771
+ label: string;
1772
+ /**
1773
+ * Sort position among contributed selection actions (ascending).
1774
+ */
1775
+ order?: number | null;
1776
+ }
1723
1777
  /**
1724
1778
  * One **settings tab** a plugin contributes (see [`Contributes::settings_tabs`]).
1725
1779
  *
@@ -1848,6 +1902,13 @@ export interface SettingsFieldContribution {
1848
1902
  * buttons (e.g. Memory) to the owning app.
1849
1903
  */
1850
1904
  export interface SidebarButtonContribution {
1905
+ /**
1906
+ * Optional mount context passed to the owning Companion when the button opens it.
1907
+ * The host applies this only to the button's own app surface.
1908
+ */
1909
+ context?: {
1910
+ [k: string]: unknown;
1911
+ } | null;
1851
1912
  /**
1852
1913
  * Optional glyph id resolved by the shell's Icon primitive.
1853
1914
  */
@@ -2477,6 +2538,12 @@ export interface PermissionSet {
2477
2538
  * Deno's `--allow-net` supports). See [`NetworkPermission`].
2478
2539
  */
2479
2540
  network?: boolean | string[];
2541
+ /**
2542
+ * Executable names sandboxed code may spawn when [`Self::child_process`] is
2543
+ * true. Core lowers this to Deno's scoped `--allow-run=<name,...>` list in
2544
+ * addition to declared capability shims. Empty grants no arbitrary binary.
2545
+ */
2546
+ run?: string[];
2480
2547
  /**
2481
2548
  * **Declaration-only** in v1: the registry tool ids this plugin's sandboxed
2482
2549
  * code may call through the stdio `tools.*` bridge. Tools are brokered over
@@ -2995,9 +3062,12 @@ export interface ExternalRuntimeConfig {
2995
3062
  */
2996
3063
  export interface AssetSpec {
2997
3064
  /**
2998
- * Destination directory relative to `~/.ryu` (e.g. `"models/hf"`); the
2999
- * fetched file lands at `<runtime>/assets/<dest_under_runtime>/<filename>`. Must
3000
- * be a traversal-safe relative path (no `..`, not absolute).
3065
+ * Destination directory relative to the runtime's `assets/` directory
3066
+ * (e.g. `"models/hf"`). The fetched file lands at
3067
+ * `<runtime>/assets/<dest_under_runtime>/<filename>`. Must be a
3068
+ * traversal-safe relative path (no `..`, not absolute). The old
3069
+ * `dest_under_ryu` spelling is accepted as a wire alias but is never
3070
+ * resolved against the shared Core data directory.
3001
3071
  */
3002
3072
  dest_under_runtime: string;
3003
3073
  /**
@@ -3229,6 +3299,13 @@ export interface RouteSpec {
3229
3299
  * webhook whose external caller cannot hold the node token).
3230
3300
  */
3231
3301
  auth?: "protected" | "public";
3302
+ /**
3303
+ * Optional HTTP method selector for this path (canonical uppercase such as
3304
+ * `GET` or `POST`). Absent preserves the legacy behavior and matches every
3305
+ * method. Declare one row per method when reads and writes share a path but
3306
+ * require different permission levels.
3307
+ */
3308
+ method?: string | null;
3232
3309
  /**
3233
3310
  * Path pattern for the sub-path after `/api/ext/<plugin_id>` (must start with
3234
3311
  * `/`). Supports `:param` (matches one non-empty segment) and a trailing
package/src/index.ts CHANGED
@@ -43,6 +43,10 @@ export type {
43
43
  Requires,
44
44
  RunnableKind,
45
45
  RunnableMeta,
46
+ SlashCommandArgument,
47
+ SlashCommandContribution,
48
+ SlashCommandCustomOption,
49
+ SlashCommandOption,
46
50
  Surface,
47
51
  ToolAppConfig,
48
52
  TurnHookContribution,
@@ -59,6 +63,10 @@ export {
59
63
  RequiresSchema,
60
64
  RunnableKindSchema,
61
65
  RunnableMetaSchema,
66
+ SlashCommandArgumentSchema,
67
+ SlashCommandContributionSchema,
68
+ SlashCommandCustomOptionSchema,
69
+ SlashCommandOptionSchema,
62
70
  SurfaceSchema,
63
71
  ToolAppConfigSchema,
64
72
  validateManifestStrict,
@@ -107,6 +115,14 @@ export type {
107
115
  ThemeContribution,
108
116
  } from "./plugin/ryu-plugin.ts";
109
117
  export { toDisposable } from "./plugin/ryu-plugin.ts";
118
+ export type {
119
+ ActionAnnotations,
120
+ ActionEffect,
121
+ ActionManifestOptions,
122
+ ActionOptions,
123
+ ActionRunnable,
124
+ } from "./runnable/action.ts";
125
+ export { defineAction } from "./runnable/action.ts";
110
126
  export type {
111
127
  AgentCard,
112
128
  AgentManifestOptions,
@@ -140,6 +156,7 @@ export type {
140
156
  RealtimeClient,
141
157
  RealtimeSubscription,
142
158
  RyuPrimitives,
159
+ StorageClient,
143
160
  SttClient,
144
161
  TtsClient,
145
162
  } from "./runnable/primitives.ts";
@@ -156,6 +173,7 @@ export type {
156
173
  export type { SkillOptions } from "./runnable/skill.ts";
157
174
  export { defineSkill } from "./runnable/skill.ts";
158
175
  export type {
176
+ InlineToolManifestOptions,
159
177
  JsonSchemaProperty,
160
178
  ToolOptions,
161
179
  ToolRunnable,
@@ -6,8 +6,8 @@
6
6
 
7
7
  import { describe, expect, test } from "bun:test";
8
8
  import {
9
- CompanionSurfaceSchema,
10
9
  ChatWidgetTemplateSchema,
10
+ CompanionSurfaceSchema,
11
11
  labelImpersonatesSystemChrome,
12
12
  PluginManifestSchema,
13
13
  RunnableMetaSchema,