@ryuhq/sdk 0.1.14 → 0.2.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/README.md +69 -7
- package/dist/action.cjs +839 -0
- package/dist/action.d.cts +88 -0
- package/dist/action.d.ts +88 -0
- package/dist/action.js +8 -0
- package/dist/agent-plugin.cjs +46 -0
- package/dist/agent-plugin.d.cts +43 -33
- package/dist/agent-plugin.d.ts +43 -33
- package/dist/agent-plugin.js +1 -1
- package/dist/agent.cjs +16 -0
- package/dist/agent.d.cts +377 -1
- package/dist/agent.d.ts +377 -1
- package/dist/agent.js +4 -2
- package/dist/app-B0Z9Ew_R.d.cts +141 -0
- package/dist/app-C-BDJwfG.d.ts +141 -0
- package/dist/builder.cjs +1194 -0
- package/dist/builder.d.cts +217 -0
- package/dist/builder.d.ts +217 -0
- package/dist/builder.js +29 -0
- package/dist/chunk-4TPUZDTI.js +94 -0
- package/dist/chunk-BC3A7HMO.js +164 -0
- package/dist/chunk-FZSFZOIN.js +200 -0
- package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
- package/dist/chunk-HLKJZAFK.js +361 -0
- package/dist/chunk-IKEDLLFY.js +19 -0
- package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
- package/dist/{chunk-AO2KJRDD.js → chunk-NZKVOSC2.js} +192 -3
- package/dist/chunk-QYFUNJOH.js +83 -0
- package/dist/chunk-SN2QBJUF.js +148 -0
- package/dist/chunk-TLDPEGC7.js +21 -0
- package/dist/chunk-TXSHHZF2.js +0 -0
- package/dist/chunk-VLIRNNAE.js +154 -0
- package/dist/{chunk-MTUBUPIV.js → chunk-Z57QDDJR.js} +8 -227
- package/dist/chunk-ZTJWBRUL.js +282 -0
- package/dist/cli.cjs +347 -18
- package/dist/cli.js +128 -23
- package/dist/client-D5U6ssPc.d.cts +84 -0
- package/dist/client-D5U6ssPc.d.ts +84 -0
- package/dist/index.cjs +810 -86
- package/dist/index.d.cts +19 -634
- package/dist/index.d.ts +19 -634
- package/dist/index.js +82 -699
- package/dist/manifest.cjs +200 -4
- package/dist/manifest.d.cts +260 -5
- package/dist/manifest.d.ts +260 -5
- package/dist/manifest.js +15 -1
- package/dist/mcp/client.cjs +180 -0
- package/dist/mcp/client.d.cts +49 -0
- package/dist/mcp/client.d.ts +49 -0
- package/dist/mcp/client.js +10 -0
- package/dist/mcp/server.cjs +370 -0
- package/dist/mcp/server.d.cts +127 -0
- package/dist/mcp/server.d.ts +127 -0
- package/dist/mcp/server.js +9 -0
- package/dist/mcp.cjs +376 -0
- package/dist/mcp.d.cts +2 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +17 -0
- package/dist/model.cjs +141 -0
- package/dist/model.d.cts +33 -0
- package/dist/model.d.ts +33 -0
- package/dist/model.js +18 -0
- package/dist/plugin.cjs +46 -0
- package/dist/plugin.d.cts +215 -0
- package/dist/plugin.d.ts +215 -0
- package/dist/plugin.js +8 -0
- package/dist/runnable.cjs +1438 -0
- package/dist/runnable.d.cts +274 -0
- package/dist/runnable.d.ts +274 -0
- package/dist/runnable.js +34 -0
- package/dist/{index-B6SkaAjJ.d.ts → tool-AjkdFvhE.d.ts} +88 -461
- package/dist/{index-BvAB5eMk.d.cts → tool-CgzW92O_.d.cts} +88 -461
- package/package.json +48 -3
- package/src/agent/loop.test.ts +4 -4
- package/src/agent/tools.ts +3 -3
- package/src/agent-plugin.test.ts +58 -0
- package/src/agent-plugin.ts +109 -36
- package/src/builder.ts +9 -0
- package/src/cli/dev.test.ts +26 -47
- package/src/cli/dev.ts +10 -2
- package/src/cli-security.test.ts +109 -0
- package/src/cli.ts +141 -25
- package/src/contracts-lockstep.test.ts +16 -2
- package/src/exports-lockstep.test.ts +93 -0
- package/src/generated/plugin-manifest.ts +322 -29
- package/src/index.ts +53 -0
- package/src/manifest-schema.test.ts +30 -1
- package/src/manifest.fixtures.test.ts +22 -6
- package/src/manifest.test.ts +165 -10
- package/src/manifest.ts +454 -188
- package/src/mcp/index.ts +18 -0
- package/src/mcp/server.ts +2 -1
- package/src/model/index.ts +22 -0
- package/src/plugin/ryu-plugin.ts +82 -0
- package/src/runnable/action.test.ts +128 -0
- package/src/runnable/action.ts +202 -0
- package/src/runnable/app.test.ts +2 -0
- package/src/runnable/app.ts +50 -14
- package/src/runnable/index.ts +20 -3
- package/src/runnable/primitives.test.ts +34 -0
- package/src/runnable/primitives.ts +116 -0
- package/src/runnable/runnable-types.ts +3 -0
- package/src/runnable/tool.ts +36 -5
- package/src/runnable/turn-hook.ts +8 -3
- package/src/slash-command.test.ts +69 -0
package/src/manifest.ts
CHANGED
|
@@ -344,6 +344,54 @@ export const WidgetContributionSchema = z.object({
|
|
|
344
344
|
|
|
345
345
|
export type WidgetContribution = z.infer<typeof WidgetContributionSchema>;
|
|
346
346
|
|
|
347
|
+
/** Metadata-only chat affordance. The host owns rendering and dispatch; the
|
|
348
|
+
* manifest carries identifiers and copy only. */
|
|
349
|
+
export const ChatWidgetTemplateSchema = z
|
|
350
|
+
.object({
|
|
351
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/),
|
|
352
|
+
title: z.string().min(1),
|
|
353
|
+
description: z.string().optional(),
|
|
354
|
+
triggers: z.array(z.string()).default([]),
|
|
355
|
+
examples: z.array(z.string()).default([]),
|
|
356
|
+
backing: z.object({
|
|
357
|
+
tool_id: z
|
|
358
|
+
.string()
|
|
359
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/)
|
|
360
|
+
.optional(),
|
|
361
|
+
view_id: z
|
|
362
|
+
.string()
|
|
363
|
+
.regex(/^[a-z0-9][a-z0-9._:-]*$/)
|
|
364
|
+
.optional(),
|
|
365
|
+
}),
|
|
366
|
+
display_mode: z.string().min(1),
|
|
367
|
+
safe_action_ids: z
|
|
368
|
+
.array(z.string().regex(/^[a-z0-9][a-z0-9._-]*$/))
|
|
369
|
+
.default([]),
|
|
370
|
+
availability: z.string().default("available"),
|
|
371
|
+
})
|
|
372
|
+
.superRefine((value, ctx) => {
|
|
373
|
+
const count =
|
|
374
|
+
Number(Boolean(value.backing.tool_id)) +
|
|
375
|
+
Number(Boolean(value.backing.view_id));
|
|
376
|
+
if (count !== 1 && value.availability === "available") {
|
|
377
|
+
ctx.addIssue({
|
|
378
|
+
code: "custom",
|
|
379
|
+
path: ["backing"],
|
|
380
|
+
message:
|
|
381
|
+
"available templates need exactly one backing tool_id or view_id",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (count > 1) {
|
|
385
|
+
ctx.addIssue({
|
|
386
|
+
code: "custom",
|
|
387
|
+
path: ["backing"],
|
|
388
|
+
message: "backing must declare at most one of tool_id or view_id",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
export type ChatWidgetTemplate = z.infer<typeof ChatWidgetTemplateSchema>;
|
|
394
|
+
|
|
347
395
|
// ── ToolAppConfig (Ryu Apps per-tool config) ─────────────────────────────────
|
|
348
396
|
|
|
349
397
|
/**
|
|
@@ -355,7 +403,7 @@ export type WidgetContribution = z.infer<typeof WidgetContributionSchema>;
|
|
|
355
403
|
* derives `outputTemplate` / `toolInvocation` / `widgetAccessible`.
|
|
356
404
|
*/
|
|
357
405
|
export const ToolAppConfigSchema = z.object({
|
|
358
|
-
/** MCP tool slug this runnable wraps — the fully-qualified `<server
|
|
406
|
+
/** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
|
|
359
407
|
slug: z.string().min(1),
|
|
360
408
|
/** The tool description the model reads when choosing it. Carried here because a
|
|
361
409
|
* packed app's manifest is the only channel (there is no `generated.rs`); Core's
|
|
@@ -377,10 +425,70 @@ export const ToolAppConfigSchema = z.object({
|
|
|
377
425
|
|
|
378
426
|
export type ToolAppConfig = z.infer<typeof ToolAppConfigSchema>;
|
|
379
427
|
|
|
428
|
+
/** One selectable value for a plugin/app slash-command argument. */
|
|
429
|
+
export const SlashCommandOptionSchema = z
|
|
430
|
+
.object({
|
|
431
|
+
description: z.string().optional(),
|
|
432
|
+
label: z.string().min(1),
|
|
433
|
+
value: z.string().min(1),
|
|
434
|
+
})
|
|
435
|
+
.passthrough();
|
|
436
|
+
|
|
437
|
+
export type SlashCommandOption = z.infer<typeof SlashCommandOptionSchema>;
|
|
438
|
+
|
|
439
|
+
/** A registered free-form option shown alongside an argument's choices. */
|
|
440
|
+
export const SlashCommandCustomOptionSchema = z.union([
|
|
441
|
+
z.literal(true),
|
|
442
|
+
z
|
|
443
|
+
.object({
|
|
444
|
+
description: z.string().optional(),
|
|
445
|
+
label: z.string().min(1).optional(),
|
|
446
|
+
})
|
|
447
|
+
.passthrough(),
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
export type SlashCommandCustomOption = z.infer<
|
|
451
|
+
typeof SlashCommandCustomOptionSchema
|
|
452
|
+
>;
|
|
453
|
+
|
|
454
|
+
/** One sequential argument in a plugin/app slash command. */
|
|
455
|
+
export const SlashCommandArgumentSchema = z
|
|
456
|
+
.object({
|
|
457
|
+
allow_custom: z.boolean().optional(),
|
|
458
|
+
custom: SlashCommandCustomOptionSchema.optional(),
|
|
459
|
+
description: z.string().optional(),
|
|
460
|
+
name: z.string().min(1),
|
|
461
|
+
options: z.array(SlashCommandOptionSchema).optional(),
|
|
462
|
+
})
|
|
463
|
+
.passthrough();
|
|
464
|
+
|
|
465
|
+
export type SlashCommandArgument = z.infer<typeof SlashCommandArgumentSchema>;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* A command registered by a plugin or Ryu App. `args` is the preferred key;
|
|
469
|
+
* `parameters` is accepted as a readable alias for hand-authored manifests.
|
|
470
|
+
* Each argument's options are plugin-owned, so the shell never needs a closed
|
|
471
|
+
* enum for app-specific values.
|
|
472
|
+
*/
|
|
473
|
+
export const SlashCommandContributionSchema = z
|
|
474
|
+
.object({
|
|
475
|
+
args: z.array(SlashCommandArgumentSchema).optional(),
|
|
476
|
+
body: z.string().optional(),
|
|
477
|
+
command: z.string().min(1),
|
|
478
|
+
description: z.string().optional(),
|
|
479
|
+
id: z.string().optional(),
|
|
480
|
+
parameters: z.array(SlashCommandArgumentSchema).optional(),
|
|
481
|
+
})
|
|
482
|
+
.passthrough();
|
|
483
|
+
|
|
484
|
+
export type SlashCommandContribution = z.infer<
|
|
485
|
+
typeof SlashCommandContributionSchema
|
|
486
|
+
>;
|
|
487
|
+
|
|
380
488
|
/**
|
|
381
489
|
* The `contributes` block. Mirrors `Contributes` in
|
|
382
490
|
* `apps/core/src/plugin_manifest/mod.rs`. The declarative UI surfaces
|
|
383
|
-
* (`composer_controls` / `
|
|
491
|
+
* (`composer_controls` / `chat_features` / `settings_tabs`) are passed verbatim
|
|
384
492
|
* to the desktop renderer, so they are typed loosely here (records).
|
|
385
493
|
*/
|
|
386
494
|
export const ContributesSchema = z.object({
|
|
@@ -391,13 +499,20 @@ export const ContributesSchema = z.object({
|
|
|
391
499
|
* signing, leaving an app that emits events nothing is allowed to subscribe to. */
|
|
392
500
|
hook_events: z.array(HookEventContributionSchema).default([]),
|
|
393
501
|
composer_controls: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
502
|
+
/** Chat feature descriptors whose behavior is implemented by the host shell.
|
|
503
|
+
* Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
|
|
504
|
+
* authoring schema prevents `ryu pack` from silently deleting a plugin's chat
|
|
505
|
+
* feature declaration before signing. */
|
|
506
|
+
chat_features: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
394
507
|
settings_tabs: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
395
|
-
slash_commands: z.array(
|
|
508
|
+
slash_commands: z.array(SlashCommandContributionSchema).default([]),
|
|
396
509
|
/** App widgets (Ryu Apps). Each binds a render tool id to its
|
|
397
510
|
* `ui://widget/<slug>.html` template. Mirrors the Rust-side
|
|
398
511
|
* `Contributes.widgets` field, without which the CLI's zod parse would strip
|
|
399
512
|
* every widget an app authored here declares. */
|
|
400
513
|
widgets: z.array(WidgetContributionSchema).default([]),
|
|
514
|
+
/** Metadata-only chat widget templates. */
|
|
515
|
+
chat_widget_templates: z.array(ChatWidgetTemplateSchema).optional(),
|
|
401
516
|
/** App-registered sidebar sections (header + live list) and buttons (single nav
|
|
402
517
|
* rows). Loosely typed here — the shell owns the spec vocabulary — matching how
|
|
403
518
|
* `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
|
|
@@ -410,6 +525,13 @@ export const ContributesSchema = z.object({
|
|
|
410
525
|
* Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
|
|
411
526
|
* strip the dock panel an app declares here. */
|
|
412
527
|
dock_panels: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
528
|
+
/** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
|
|
529
|
+
* typed for the same reason as the surfaces above — the shell owns the
|
|
530
|
+
* `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
|
|
531
|
+
* without it the CLI's zod parse would strip every live activity an app
|
|
532
|
+
* declares here, so a packed bundle would ship a dock that silently stays
|
|
533
|
+
* empty. */
|
|
534
|
+
live_activities: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
413
535
|
/** Deletable data categories the app owns — one "Delete all X" row in Settings
|
|
414
536
|
* → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
|
|
415
537
|
* it the CLI's zod parse would strip the declaration before signing, and the
|
|
@@ -450,6 +572,12 @@ export const ContributesSchema = z.object({
|
|
|
450
572
|
* would be no residue to notice — the plugin would install clean and contribute
|
|
451
573
|
* nothing. */
|
|
452
574
|
output_styles: z.array(OutputStyleContributionSchema).default([]),
|
|
575
|
+
/** Per-message actions contributed by an enabled plugin. Kept as loose records
|
|
576
|
+
* so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
|
|
577
|
+
message_actions: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
578
|
+
/** Buttons contributed to the floating text-selection toolbar. Kept as loose
|
|
579
|
+
* records so host-owned dispatch args survive `ryu pack` unchanged. */
|
|
580
|
+
selection_actions: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
453
581
|
});
|
|
454
582
|
|
|
455
583
|
export type Contributes = z.infer<typeof ContributesSchema>;
|
|
@@ -615,6 +743,78 @@ export const EnginesReqSchema = z.object({
|
|
|
615
743
|
|
|
616
744
|
export type EnginesReq = z.infer<typeof EnginesReqSchema>;
|
|
617
745
|
|
|
746
|
+
export const McpServerAuthSchema = z
|
|
747
|
+
.object({
|
|
748
|
+
client_id: z.string().min(1).optional(),
|
|
749
|
+
type: z.literal("oauth"),
|
|
750
|
+
})
|
|
751
|
+
.strict();
|
|
752
|
+
|
|
753
|
+
export const McpServerDeclSchema = z
|
|
754
|
+
.object({
|
|
755
|
+
args: z.array(z.string()).default([]),
|
|
756
|
+
auth: McpServerAuthSchema.optional(),
|
|
757
|
+
command: z.string().optional(),
|
|
758
|
+
command_env: z.string().optional(),
|
|
759
|
+
description: z.string().optional(),
|
|
760
|
+
enabled: z.boolean().default(true),
|
|
761
|
+
env: z.record(z.string(), z.string()).default({}),
|
|
762
|
+
headers: z.record(z.string(), z.string()).default({}),
|
|
763
|
+
type: z
|
|
764
|
+
.enum(["stdio", "http", "streamable-http", "streamable_http", "sse"])
|
|
765
|
+
.optional(),
|
|
766
|
+
url: z.url().optional(),
|
|
767
|
+
})
|
|
768
|
+
.superRefine((server, context) => {
|
|
769
|
+
if (!(server.command || server.url)) {
|
|
770
|
+
context.addIssue({
|
|
771
|
+
code: "custom",
|
|
772
|
+
message: "an MCP server requires command or url",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (!server.auth) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (server.command || server.type === "stdio" || !server.url) {
|
|
779
|
+
context.addIssue({
|
|
780
|
+
code: "custom",
|
|
781
|
+
message: "OAuth is supported only for remote HTTP MCP servers",
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (
|
|
786
|
+
Object.keys(server.headers).some(
|
|
787
|
+
(name) => name.toLowerCase() === "authorization"
|
|
788
|
+
)
|
|
789
|
+
) {
|
|
790
|
+
context.addIssue({
|
|
791
|
+
code: "custom",
|
|
792
|
+
message: "OAuth cannot be combined with a static Authorization header",
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const url = new URL(server.url);
|
|
796
|
+
const loopback =
|
|
797
|
+
url.hostname === "localhost" ||
|
|
798
|
+
url.hostname.startsWith("127.") ||
|
|
799
|
+
url.hostname === "[::1]" ||
|
|
800
|
+
url.hostname === "::1";
|
|
801
|
+
if (url.username || url.password || url.hash) {
|
|
802
|
+
context.addIssue({
|
|
803
|
+
code: "custom",
|
|
804
|
+
message: "OAuth MCP URLs cannot contain credentials or fragments",
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
808
|
+
context.addIssue({
|
|
809
|
+
code: "custom",
|
|
810
|
+
message: "OAuth MCP URLs must use HTTPS except on loopback",
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
export type McpServerAuth = z.infer<typeof McpServerAuthSchema>;
|
|
816
|
+
export type McpServerDecl = z.infer<typeof McpServerDeclSchema>;
|
|
817
|
+
|
|
618
818
|
// ── PluginManifest ───────────────────────────────────────────────────────────
|
|
619
819
|
|
|
620
820
|
/**
|
|
@@ -627,203 +827,269 @@ export type EnginesReq = z.infer<typeof EnginesReqSchema>;
|
|
|
627
827
|
* - `runnables` may be empty for a "surface-only" plugin, but each entry must be
|
|
628
828
|
* a valid `RunnableMeta`
|
|
629
829
|
*/
|
|
630
|
-
export const PluginManifestSchema = z
|
|
631
|
-
|
|
632
|
-
|
|
830
|
+
export const PluginManifestSchema = z
|
|
831
|
+
.object({
|
|
832
|
+
/** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
|
|
833
|
+
id: z.string().min(1, "id is required"),
|
|
633
834
|
|
|
634
|
-
|
|
635
|
-
|
|
835
|
+
/** Human-readable display name shown in the plugin store / launcher. */
|
|
836
|
+
name: z.string().min(1, "name is required"),
|
|
636
837
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
838
|
+
/**
|
|
839
|
+
* Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
|
|
840
|
+
* whose version is not valid semver; the regex here enforces the same rule at
|
|
841
|
+
* SDK-build time.
|
|
842
|
+
*/
|
|
843
|
+
version: z
|
|
844
|
+
.string()
|
|
845
|
+
.regex(
|
|
846
|
+
/^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
|
|
847
|
+
"version must be a valid semver string (e.g. 1.0.0)"
|
|
848
|
+
),
|
|
648
849
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
* write it here BEFORE the manifest is signed, so the hash rides INSIDE the
|
|
653
|
-
* Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
|
|
654
|
-
* Core's install path recomputes the hash over the fetched code and rejects a
|
|
655
|
-
* mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
|
|
656
|
-
* Mirrors Core's `PluginManifest.ui_code_sha256`.
|
|
657
|
-
*/
|
|
658
|
-
ui_code_sha256: z.string().nullish(),
|
|
850
|
+
/** Core-owned release maturity metadata. The Rust contract validates the
|
|
851
|
+
* richer shape; the SDK authoring parser must preserve it for pack/publish. */
|
|
852
|
+
stability: z.unknown().optional(),
|
|
659
853
|
|
|
660
|
-
|
|
661
|
-
|
|
854
|
+
/**
|
|
855
|
+
* Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
|
|
856
|
+
* sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
|
|
857
|
+
* write it here BEFORE the manifest is signed, so the hash rides INSIDE the
|
|
858
|
+
* Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
|
|
859
|
+
* Core's install path recomputes the hash over the fetched code and rejects a
|
|
860
|
+
* mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
|
|
861
|
+
* Mirrors Core's `PluginManifest.ui_code_sha256`.
|
|
862
|
+
*/
|
|
863
|
+
ui_code_sha256: z.string().nullish(),
|
|
662
864
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
* Declarations only — grant enforcement is the Gateway's responsibility.
|
|
666
|
-
*/
|
|
667
|
-
permission_grants: z.array(z.string()).default([]),
|
|
865
|
+
/** The Runnables this plugin bundles. */
|
|
866
|
+
runnables: z.array(RunnableMetaSchema).default([]),
|
|
668
867
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Permission grants this plugin declares it needs (e.g. `"mcp:web_search"`).
|
|
870
|
+
* Declarations only — grant enforcement is the Gateway's responsibility.
|
|
871
|
+
*/
|
|
872
|
+
permission_grants: z.array(z.string()).default([]),
|
|
674
873
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
874
|
+
/** Deny-by-default sandbox permissions. Core remains authoritative; this
|
|
875
|
+
* schema mirrors the fields so `ryu pack` cannot strip them. */
|
|
876
|
+
permissions: z
|
|
877
|
+
.object({
|
|
878
|
+
fs: z
|
|
879
|
+
.object({
|
|
880
|
+
read: z.array(z.string()).default([]),
|
|
881
|
+
write: z.array(z.string()).default([]),
|
|
882
|
+
})
|
|
883
|
+
.optional(),
|
|
884
|
+
child_process: z.boolean().optional(),
|
|
885
|
+
run: z.array(z.string()).default([]),
|
|
886
|
+
network: z.union([z.boolean(), z.array(z.string())]).optional(),
|
|
887
|
+
tool: z.array(z.string()).default([]),
|
|
888
|
+
})
|
|
889
|
+
.optional(),
|
|
681
890
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
*/
|
|
686
|
-
contributes: ContributesSchema.optional(),
|
|
891
|
+
/** Core-owned permission presentation levels. Preserved verbatim here and
|
|
892
|
+
* validated by Core's authoritative manifest contract. */
|
|
893
|
+
permission_levels: z.unknown().optional(),
|
|
687
894
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
* resolves them into a topological enable order (dependencies enable first;
|
|
691
|
-
* disabling one is refused while an enabled dependent needs it).
|
|
692
|
-
*
|
|
693
|
-
* Absent = **no dependencies**, the backward-compatible default. Kept
|
|
694
|
-
* `.optional()` (never defaulted) so a manifest that declares none serialises
|
|
695
|
-
* with no `requires` key at all, exactly like Core's
|
|
696
|
-
* `#[serde(skip_serializing_if = "Option::is_none")]`.
|
|
697
|
-
*/
|
|
698
|
-
requires: RequiresSchema.optional(),
|
|
895
|
+
/** Remote or stdio MCP servers registered by this plugin. */
|
|
896
|
+
mcp_servers: z.record(z.string(), McpServerDeclSchema).optional(),
|
|
699
897
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
* "runs nowhere". Core filters only when the list is explicitly non-empty, and
|
|
704
|
-
* only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
|
|
705
|
-
* an unsupported-target plugin stays installable and inspectable.
|
|
706
|
-
*/
|
|
707
|
-
targets: z.array(SurfaceSchema).default([]),
|
|
898
|
+
/** Core-owned sidecar declarations. Their full process/HTTP schema stays in
|
|
899
|
+
* the Rust contract; this authoring layer must never strip them. */
|
|
900
|
+
sidecars: z.unknown().optional(),
|
|
708
901
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
* `ryu` is the **Core** floor and the only required key (it is the legacy
|
|
715
|
-
* spelling; every manifest in the wild carries just that one). The rest are
|
|
716
|
-
* optional per-surface floors.
|
|
717
|
-
*
|
|
718
|
-
* REGRESSION THIS FIXES: `engines` was absent from this schema entirely, and
|
|
719
|
-
* zod strips unlisted keys — so `ryu pack` silently dropped the whole block
|
|
720
|
-
* from every bundle it produced. A plugin could declare a Core floor, publish,
|
|
721
|
-
* and ship a bundle that declared none. Any new host floor MUST be added here
|
|
722
|
-
* as well as in the Rust contract, or it does not survive packing.
|
|
723
|
-
*/
|
|
724
|
-
engines: EnginesReqSchema.optional(),
|
|
902
|
+
/**
|
|
903
|
+
* Optional Companion surface (an in-desktop overlay or sidebar panel).
|
|
904
|
+
* Absent when the plugin has no Companion surface.
|
|
905
|
+
*/
|
|
906
|
+
companion: CompanionSurfaceSchema.optional(),
|
|
725
907
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
908
|
+
/**
|
|
909
|
+
* VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
|
|
910
|
+
* `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
|
|
911
|
+
* enabled flag, so `["*"]` is the usual value.
|
|
912
|
+
*/
|
|
913
|
+
activation_events: z.array(z.string()).default([]),
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Contribution points: server-side turn hooks + declarative UI widgets.
|
|
917
|
+
* Absent for a plugin that contributes nothing here.
|
|
918
|
+
*/
|
|
919
|
+
contributes: ContributesSchema.optional(),
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* **Plugin-to-plugin dependencies** — the other plugins this one needs. Core
|
|
923
|
+
* resolves them into a topological enable order (dependencies enable first;
|
|
924
|
+
* disabling one is refused while an enabled dependent needs it).
|
|
925
|
+
*
|
|
926
|
+
* Absent = **no dependencies**, the backward-compatible default. Kept
|
|
927
|
+
* `.optional()` (never defaulted) so a manifest that declares none serialises
|
|
928
|
+
* with no `requires` key at all, exactly like Core's
|
|
929
|
+
* `#[serde(skip_serializing_if = "Option::is_none")]`.
|
|
930
|
+
*/
|
|
931
|
+
requires: RequiresSchema.optional(),
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
|
|
935
|
+
* surface** — the backward-compatible default, which must never be read as
|
|
936
|
+
* "runs nowhere". Core filters only when the list is explicitly non-empty, and
|
|
937
|
+
* only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
|
|
938
|
+
* an unsupported-target plugin stays installable and inspectable.
|
|
939
|
+
*/
|
|
940
|
+
targets: z.array(SurfaceSchema).default([]),
|
|
941
|
+
|
|
942
|
+
/** Rich per-surface declarations (`support`, UI, contributed commands, …).
|
|
943
|
+
* Core owns and validates the nested vocabulary. */
|
|
944
|
+
surfaces: z.unknown().optional(),
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Host version floors — the semver requirement each surface must satisfy for
|
|
948
|
+
* this plugin to install. Mirrors Core's `EnginesReq`
|
|
949
|
+
* (`crates/core/kernel-contracts/src/manifest.rs`).
|
|
950
|
+
*
|
|
951
|
+
* `ryu` is the **Core** floor and the only required key (it is the legacy
|
|
952
|
+
* spelling; every manifest in the wild carries just that one). The rest are
|
|
953
|
+
* optional per-surface floors.
|
|
954
|
+
*
|
|
955
|
+
* REGRESSION THIS FIXES: `engines` was absent from this schema entirely, and
|
|
956
|
+
* zod strips unlisted keys — so `ryu pack` silently dropped the whole block
|
|
957
|
+
* from every bundle it produced. A plugin could declare a Core floor, publish,
|
|
958
|
+
* and ship a bundle that declared none. Any new host floor MUST be added here
|
|
959
|
+
* as well as in the Rust contract, or it does not survive packing.
|
|
960
|
+
*/
|
|
961
|
+
engines: EnginesReqSchema.optional(),
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Optional per-item AFFILIATE terms: the commission paid to a referrer when a
|
|
965
|
+
* referred user buys this (paid) item. `value` is basis points for `percent`
|
|
966
|
+
* (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
|
|
967
|
+
* falls back to the seller org owner's default affiliate terms. This is the
|
|
968
|
+
* authoring surface for the marketplace publish body's `affiliate` field (the
|
|
969
|
+
* server re-validates it); it only takes effect on a paid item.
|
|
970
|
+
*/
|
|
971
|
+
affiliate: z
|
|
972
|
+
.object({
|
|
973
|
+
enabled: z.boolean().default(false),
|
|
974
|
+
rule: z
|
|
975
|
+
.object({
|
|
976
|
+
type: z.enum(["percent", "flat"]),
|
|
977
|
+
value: z.number().nonnegative(),
|
|
978
|
+
recurring: z.boolean().default(false),
|
|
979
|
+
durationMonths: z.number().int().positive().nullish(),
|
|
980
|
+
fundedBy: z.enum(["platform", "seller"]).default("platform"),
|
|
981
|
+
})
|
|
982
|
+
.optional(),
|
|
983
|
+
})
|
|
984
|
+
.optional(),
|
|
985
|
+
|
|
986
|
+
// ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
|
|
987
|
+
// Optional store-listing fields a plugin author declares so the marketplace
|
|
988
|
+
// detail dialog renders a richer App-Store-style preview. Field names align
|
|
989
|
+
// with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
|
|
990
|
+
// one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
|
|
991
|
+
// rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
|
|
992
|
+
// body (not inside the signed manifest blob) so the control plane stores them.
|
|
993
|
+
// All optional + additive: a manifest omitting them still validates.
|
|
994
|
+
|
|
995
|
+
/** Longer plain/markdown description shown in the detail dialog. */
|
|
996
|
+
description: z.string().optional(),
|
|
997
|
+
/** Short one-line pitch shown under the name (Ryu extension). */
|
|
998
|
+
tagline: z.string().optional(),
|
|
999
|
+
/**
|
|
1000
|
+
* Publisher identity. A bare string OR a Claude-style object; `ryu publish`
|
|
1001
|
+
* resolves it to the display `developer` (`author.name` when an object).
|
|
1002
|
+
*/
|
|
1003
|
+
author: z
|
|
1004
|
+
.union([
|
|
1005
|
+
z.string(),
|
|
1006
|
+
z.object({
|
|
1007
|
+
name: z.string(),
|
|
1008
|
+
email: z.string().optional(),
|
|
1009
|
+
url: z.string().optional(),
|
|
1010
|
+
}),
|
|
1011
|
+
])
|
|
1012
|
+
.optional(),
|
|
1013
|
+
/** Public source repository URL (Claude/Codex `repository`). */
|
|
1014
|
+
repository: z.string().url().optional(),
|
|
1015
|
+
/** True when the provider operates outside the local Ryu runtime. */
|
|
1016
|
+
external: z.boolean().optional(),
|
|
1017
|
+
/** Project/marketing homepage — maps to the listing `website` (Claude field). */
|
|
1018
|
+
homepage: z.string().optional(),
|
|
1019
|
+
/** Free-text search keywords (Claude field). */
|
|
1020
|
+
keywords: z.array(z.string()).optional(),
|
|
1021
|
+
/** Stable Marketplace filter labels (Ryu extension). */
|
|
1022
|
+
tags: z.array(z.string()).optional(),
|
|
1023
|
+
/** Taxonomy category beyond the runnable kinds (Claude field). */
|
|
1024
|
+
category: z.string().optional(),
|
|
1025
|
+
/** SPDX-ish license identifier (Claude field). */
|
|
1026
|
+
license: z.string().optional(),
|
|
1027
|
+
/** Square logo/icon URL for the listing card + detail header. */
|
|
1028
|
+
iconUrl: z.string().optional(),
|
|
1029
|
+
/**
|
|
1030
|
+
* Icon-primitive id for the listing card (Ryu extension): an Iconify/icons0
|
|
1031
|
+
* `prefix:name`, a bare Hugeicons name, or a URL, resolved by the shared `Icon`
|
|
1032
|
+
* primitive. A monochrome GLYPH masked with the current text colour — distinct
|
|
1033
|
+
* from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
|
|
1034
|
+
*/
|
|
1035
|
+
icon: z.string().optional(),
|
|
1036
|
+
/** Optional detail-page hero banner metadata forwarded to the marketplace. */
|
|
1037
|
+
banner: z.record(z.string(), z.unknown()).optional(),
|
|
1038
|
+
/**
|
|
1039
|
+
* Dithered-gradient background for the card's icon square (Ryu extension),
|
|
1040
|
+
* mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
|
|
1041
|
+
* name (`green`, `blue`, `purple`, `pink`, `orange`, `red`, `grey`) or a hue
|
|
1042
|
+
* number (0–360); `direction` is where `to` ends up. Renders behind the glyph in
|
|
1043
|
+
* place of a flat `iconBackground`; the render layer validates + falls back.
|
|
1044
|
+
*/
|
|
1045
|
+
iconDither: z
|
|
1046
|
+
.object({
|
|
1047
|
+
from: z.union([z.string(), z.number()]),
|
|
1048
|
+
to: z.union([z.string(), z.number()]).optional(),
|
|
1049
|
+
direction: z.enum(["up", "down", "left", "right"]).optional(),
|
|
1050
|
+
})
|
|
1051
|
+
.optional(),
|
|
1052
|
+
/** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
|
|
1053
|
+
screenshots: z.array(z.string()).optional(),
|
|
1054
|
+
/** Privacy policy URL surfaced on detail (Ryu extension). */
|
|
1055
|
+
privacyPolicyUrl: z.string().optional(),
|
|
1056
|
+
/** Terms-of-service URL surfaced on detail (Ryu extension). */
|
|
1057
|
+
termsOfServiceUrl: z.string().optional(),
|
|
1058
|
+
/**
|
|
1059
|
+
* Human-readable capability strings (Ryu extension). When omitted the control
|
|
1060
|
+
* plane derives a default from `permission_grants`, so declaring this is only
|
|
1061
|
+
* needed to override the derived labels.
|
|
1062
|
+
*/
|
|
1063
|
+
capabilities: z.array(z.string()).optional(),
|
|
1064
|
+
/** Example prompt chips shown on detail (Ryu extension). */
|
|
1065
|
+
examplePrompts: z.array(z.string()).optional(),
|
|
1066
|
+
/**
|
|
1067
|
+
* Optional companion/config card (Ryu extension): a single setup step or an
|
|
1068
|
+
* array of steps guiding the user through post-install configuration.
|
|
1069
|
+
*/
|
|
1070
|
+
setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional(),
|
|
1071
|
+
})
|
|
1072
|
+
// Core's Rust-derived schema is the full wire authority. Keep this deliberately
|
|
1073
|
+
// forward-compatible so a newly-added Core field survives SDK pack/publish even
|
|
1074
|
+
// before the simpler authoring schema grows first-class validation for it.
|
|
1075
|
+
.passthrough()
|
|
1076
|
+
.superRefine((manifest, context) => {
|
|
1077
|
+
const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
|
|
1078
|
+
(server) => server.auth?.type === "oauth"
|
|
1079
|
+
);
|
|
1080
|
+
if (!hasOAuthServer) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
for (const grant of ["mcp:server", "identity.read"] as const) {
|
|
1084
|
+
if (!manifest.permission_grants.includes(grant)) {
|
|
1085
|
+
context.addIssue({
|
|
1086
|
+
code: "custom",
|
|
1087
|
+
message: `OAuth MCP servers require the ${grant} permission grant`,
|
|
1088
|
+
path: ["permission_grants"],
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
827
1093
|
|
|
828
1094
|
export type PluginManifest = z.infer<typeof PluginManifestSchema>;
|
|
829
1095
|
|