@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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { describe, expect, test } from "bun:test";
|
|
8
8
|
import {
|
|
9
9
|
CompanionSurfaceSchema,
|
|
10
|
+
ChatWidgetTemplateSchema,
|
|
10
11
|
labelImpersonatesSystemChrome,
|
|
11
12
|
PluginManifestSchema,
|
|
12
13
|
RunnableMetaSchema,
|
|
@@ -22,6 +23,34 @@ function baseManifest(overrides: Record<string, unknown> = {}) {
|
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
test("chat widget templates keep host rendering data-only and forward compatible", () => {
|
|
27
|
+
const available = ChatWidgetTemplateSchema.safeParse({
|
|
28
|
+
id: "meetings.chat",
|
|
29
|
+
title: "Meetings widget",
|
|
30
|
+
triggers: ["meetings widget"],
|
|
31
|
+
examples: ["Show my meetings"],
|
|
32
|
+
backing: { view_id: "surface:meetings" },
|
|
33
|
+
display_mode: "inline",
|
|
34
|
+
safe_action_ids: ["refresh"],
|
|
35
|
+
}).success;
|
|
36
|
+
const comingSoon = ChatWidgetTemplateSchema.safeParse({
|
|
37
|
+
id: "future.chat",
|
|
38
|
+
title: "Future widget",
|
|
39
|
+
backing: {},
|
|
40
|
+
display_mode: "compact-future-mode",
|
|
41
|
+
availability: "coming-soon",
|
|
42
|
+
}).success;
|
|
43
|
+
const unsafe = ChatWidgetTemplateSchema.safeParse({
|
|
44
|
+
id: "future/chat",
|
|
45
|
+
title: "Unsafe",
|
|
46
|
+
backing: { tool_id: "future.render" },
|
|
47
|
+
display_mode: "inline",
|
|
48
|
+
}).success;
|
|
49
|
+
expect(available).toBe(true);
|
|
50
|
+
expect(comingSoon).toBe(true);
|
|
51
|
+
expect(unsafe).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
25
54
|
// ── semver regex ──────────────────────────────────────────────────────────────
|
|
26
55
|
|
|
27
56
|
describe("PluginManifest version (semver regex)", () => {
|
|
@@ -171,7 +200,7 @@ describe("RunnableMetaSchema", () => {
|
|
|
171
200
|
id: "x",
|
|
172
201
|
name: "X",
|
|
173
202
|
kind: "tool",
|
|
174
|
-
config: { widget: true, slug: "
|
|
203
|
+
config: { widget: true, slug: "x.render" },
|
|
175
204
|
});
|
|
176
205
|
expect(parsed.success).toBe(true);
|
|
177
206
|
if (!parsed.success) {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The set is read from Core's tree so a new shipped plugin is covered the moment it
|
|
19
19
|
* lands, without touching this file. It spans BOTH homes: the packaged manifests
|
|
20
|
-
* live in `apps-store/<x>/manifest.json` and
|
|
20
|
+
* live in `apps-store/<x>/manifest.json` and
|
|
21
|
+
* `plugins-store/{plugins,lsp,external_plugins}/<x>/manifest.json` (Core
|
|
21
22
|
* `include_str!`s them from there), and only the ~13 Core-only ones remain under
|
|
22
23
|
* `apps/core/src/plugin_manifest/fixtures/`. Reading just the fixtures dir would
|
|
23
24
|
* still pass — on 13 files instead of 71 — so both roots are walked deliberately.
|
|
@@ -39,7 +40,12 @@ import {
|
|
|
39
40
|
const REPO_ROOT = join(import.meta.dir, "../../..");
|
|
40
41
|
const FIXTURES_DIR = join(REPO_ROOT, "apps/core/src/plugin_manifest/fixtures");
|
|
41
42
|
/** The package roots whose `manifest.json` Core compiles in directly. */
|
|
42
|
-
const PACKAGE_ROOTS = [
|
|
43
|
+
const PACKAGE_ROOTS = [
|
|
44
|
+
"apps-store",
|
|
45
|
+
"plugins-store/plugins",
|
|
46
|
+
"plugins-store/lsp",
|
|
47
|
+
"plugins-store/external_plugins",
|
|
48
|
+
];
|
|
43
49
|
|
|
44
50
|
interface RawManifest {
|
|
45
51
|
companion?: { label?: unknown };
|
|
@@ -127,18 +133,28 @@ const SDK_KNOWN_KINDS = new Set([
|
|
|
127
133
|
const VALID_HOOK_PHASES = new Set([
|
|
128
134
|
"post_assistant_turn",
|
|
129
135
|
"pre_user_turn",
|
|
136
|
+
"pre_model_select",
|
|
137
|
+
"action",
|
|
130
138
|
"session_start",
|
|
131
139
|
"stop",
|
|
132
140
|
"pre_tool_use",
|
|
133
141
|
"post_tool_use",
|
|
142
|
+
"tool_result",
|
|
134
143
|
"subagent_stop",
|
|
135
144
|
"session_end",
|
|
136
145
|
"notification",
|
|
146
|
+
"context",
|
|
147
|
+
"message_end",
|
|
148
|
+
"session_before_compact",
|
|
149
|
+
"session_compact",
|
|
150
|
+
"model_select",
|
|
151
|
+
"session_tree",
|
|
137
152
|
// Kernel lifecycle phases fired by Core's own subsystems (the workflow
|
|
138
153
|
// executor), not by any app.
|
|
139
154
|
"workflow_run_started",
|
|
140
155
|
"workflow_run_finished",
|
|
141
156
|
"workflow_run_failed",
|
|
157
|
+
"delegation_lifecycle",
|
|
142
158
|
]);
|
|
143
159
|
|
|
144
160
|
/** An **app event** — a phase some app declares in `contributes.hook_events`,
|
|
@@ -303,15 +319,15 @@ describe("glob_match oracle (ported from Core)", () => {
|
|
|
303
319
|
expect(globMatch("bash", "bashx")).toBe(false);
|
|
304
320
|
});
|
|
305
321
|
it("trailing star is a prefix match", () => {
|
|
306
|
-
expect(globMatch("bash*", "
|
|
322
|
+
expect(globMatch("bash*", "bash.run")).toBe(true);
|
|
307
323
|
expect(globMatch("bash*", "sh")).toBe(false);
|
|
308
324
|
});
|
|
309
325
|
it("leading star is a suffix match", () => {
|
|
310
|
-
expect(globMatch("*write", "
|
|
326
|
+
expect(globMatch("*write", "fs.write")).toBe(true);
|
|
311
327
|
expect(globMatch("*write", "writer")).toBe(false);
|
|
312
328
|
});
|
|
313
329
|
it("double star is a substring match", () => {
|
|
314
|
-
expect(globMatch("*edit*", "
|
|
330
|
+
expect(globMatch("*edit*", "editor.do_edit")).toBe(true);
|
|
315
331
|
expect(globMatch("*edit*", "read_only")).toBe(false);
|
|
316
332
|
});
|
|
317
333
|
it("an interior star degrades to an exact-literal match (Core has no compile step)", () => {
|
|
@@ -409,7 +425,7 @@ describe("PluginManifestSchema preserves turn_hook.match", () => {
|
|
|
409
425
|
// `tool-firewall` is a packaged plugin, so its manifest lives in its
|
|
410
426
|
// package directory — there is no fixture copy any more.
|
|
411
427
|
readFileSync(
|
|
412
|
-
join(REPO_ROOT, "plugins-store/tool-firewall/manifest.json"),
|
|
428
|
+
join(REPO_ROOT, "plugins-store/plugins/tool-firewall/manifest.json"),
|
|
413
429
|
"utf8"
|
|
414
430
|
)
|
|
415
431
|
);
|
package/src/manifest.test.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
import { agent, app, PluginBuilder, skill, tool, workflow } from "./builder.ts";
|
|
27
27
|
import { PluginManifestSchema } from "./manifest.ts";
|
|
28
28
|
import { defineApp } from "./runnable/app.ts";
|
|
29
|
+
import { defineTool } from "./runnable/tool.ts";
|
|
29
30
|
|
|
30
31
|
// ── builder unit tests ────────────────────────────────────────────────────────
|
|
31
32
|
|
|
@@ -302,7 +303,7 @@ describe("defineApp", () => {
|
|
|
302
303
|
const widgets = manifest.contributes?.widgets ?? [];
|
|
303
304
|
|
|
304
305
|
expect(widgets).toHaveLength(1);
|
|
305
|
-
expect(widgets[0]?.tool_id).toBe("
|
|
306
|
+
expect(widgets[0]?.tool_id).toBe("checklist.render");
|
|
306
307
|
expect(widgets[0]?.uri).toBe("ui://widget/checklist.html");
|
|
307
308
|
expect(widgets[0]?.ui_entry).toBe("src/checklist.tsx");
|
|
308
309
|
expect(widgets[0]?.mime).toBe("text/html+skybridge");
|
|
@@ -314,9 +315,9 @@ describe("defineApp", () => {
|
|
|
314
315
|
expect(manifest.runnables).toHaveLength(2);
|
|
315
316
|
expect(manifest.runnables.every((r) => r.kind === "tool")).toBe(true);
|
|
316
317
|
|
|
317
|
-
const render = manifest.runnables.find((r) => r.id === "
|
|
318
|
+
const render = manifest.runnables.find((r) => r.id === "checklist.render");
|
|
318
319
|
expect(render?.config).toMatchObject({
|
|
319
|
-
slug: "
|
|
320
|
+
slug: "checklist.render",
|
|
320
321
|
// The manifest is the only channel for a packed app: description +
|
|
321
322
|
// input_schema must survive so Core can rebuild a driveable tool.
|
|
322
323
|
description: "Render a checklist",
|
|
@@ -332,9 +333,9 @@ describe("defineApp", () => {
|
|
|
332
333
|
invoked: "Ready",
|
|
333
334
|
});
|
|
334
335
|
|
|
335
|
-
const toggle = manifest.runnables.find((r) => r.id === "
|
|
336
|
+
const toggle = manifest.runnables.find((r) => r.id === "checklist.toggle");
|
|
336
337
|
expect(toggle?.config).toMatchObject({
|
|
337
|
-
slug: "
|
|
338
|
+
slug: "checklist.toggle",
|
|
338
339
|
widget: false,
|
|
339
340
|
widget_accessible: true,
|
|
340
341
|
});
|
|
@@ -350,19 +351,62 @@ describe("defineApp", () => {
|
|
|
350
351
|
uiEntry: "src/chart.tsx",
|
|
351
352
|
tools: [{ name: "render", description: "Render a chart" }],
|
|
352
353
|
});
|
|
353
|
-
const render = manifest.runnables.find((r) => r.id === "
|
|
354
|
+
const render = manifest.runnables.find((r) => r.id === "chart.render");
|
|
354
355
|
expect(render?.config).toMatchObject({
|
|
355
356
|
widget: true,
|
|
356
357
|
widget_accessible: false,
|
|
357
358
|
});
|
|
358
359
|
// `server` override qualifies the tool id and the widget binding.
|
|
359
|
-
expect(manifest.contributes?.widgets[0]?.tool_id).toBe("
|
|
360
|
+
expect(manifest.contributes?.widgets[0]?.tool_id).toBe("chart.render");
|
|
360
361
|
// The widget uri still derives from the slug, not the server.
|
|
361
362
|
expect(manifest.contributes?.widgets[0]?.uri).toBe(
|
|
362
363
|
"ui://widget/chart-studio.html"
|
|
363
364
|
);
|
|
364
365
|
});
|
|
365
366
|
|
|
367
|
+
it("embeds an executable ToolRunnable in the widget tool", () => {
|
|
368
|
+
const renderTool = defineTool({
|
|
369
|
+
id: "support.render",
|
|
370
|
+
name: "Support answer",
|
|
371
|
+
schema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: { message: { type: "string" } },
|
|
374
|
+
required: ["message"],
|
|
375
|
+
},
|
|
376
|
+
run: async (input) => ({
|
|
377
|
+
content: [{ type: "text", text: input.message }],
|
|
378
|
+
structuredContent: { answer: input.message },
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
const manifest = defineApp({
|
|
382
|
+
id: "com.example.support",
|
|
383
|
+
title: "Support",
|
|
384
|
+
version: "1.0.0",
|
|
385
|
+
slug: "support",
|
|
386
|
+
uiEntry: "src/widget.html",
|
|
387
|
+
tools: [
|
|
388
|
+
{
|
|
389
|
+
name: "render",
|
|
390
|
+
description: "Answer a support question",
|
|
391
|
+
inputSchema: renderTool.schema as unknown as Record<string, unknown>,
|
|
392
|
+
runnable: renderTool,
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
const render = manifest.runnables[0];
|
|
397
|
+
|
|
398
|
+
expect(render?.config).toMatchObject({
|
|
399
|
+
backend: "inline_deno",
|
|
400
|
+
code: renderTool.code,
|
|
401
|
+
input_schema: renderTool.schema,
|
|
402
|
+
widget: true,
|
|
403
|
+
});
|
|
404
|
+
expect(manifest.permission_grants).toEqual([
|
|
405
|
+
"widget:render",
|
|
406
|
+
"tool:execute",
|
|
407
|
+
]);
|
|
408
|
+
});
|
|
409
|
+
|
|
366
410
|
it("round-trips through PluginManifestSchema without stripping widgets", () => {
|
|
367
411
|
// The load-bearing check: `contributes.widgets` is only preserved because it
|
|
368
412
|
// was added to `ContributesSchema`. A JSON round-trip proves the field
|
|
@@ -377,12 +421,12 @@ describe("defineApp", () => {
|
|
|
377
421
|
}
|
|
378
422
|
expect(parsed.data.contributes?.widgets).toHaveLength(1);
|
|
379
423
|
expect(parsed.data.contributes?.widgets[0]?.tool_id).toBe(
|
|
380
|
-
"
|
|
424
|
+
"checklist.render"
|
|
381
425
|
);
|
|
382
426
|
// description + input_schema survive the strict parse (the only channel for
|
|
383
427
|
// a packed app — no `generated.rs` on the Core side).
|
|
384
428
|
const render = parsed.data.runnables.find(
|
|
385
|
-
(r) => r.id === "
|
|
429
|
+
(r) => r.id === "checklist.render"
|
|
386
430
|
);
|
|
387
431
|
expect(render?.config?.description).toBe("Render a checklist");
|
|
388
432
|
expect(render?.config?.input_schema).toBeDefined();
|
|
@@ -423,7 +467,7 @@ describe("AppBuilder", () => {
|
|
|
423
467
|
expect(manifest.id).toBe("com.example.checklist");
|
|
424
468
|
expect(manifest.runnables).toHaveLength(2);
|
|
425
469
|
expect(manifest.contributes?.widgets).toHaveLength(1);
|
|
426
|
-
expect(manifest.contributes?.widgets[0]?.tool_id).toBe("
|
|
470
|
+
expect(manifest.contributes?.widgets[0]?.tool_id).toBe("checklist.render");
|
|
427
471
|
// The author's own grant, plus the `widget:render` the builder adds because
|
|
428
472
|
// this app synthesises a widget — without it Core silently degrades the
|
|
429
473
|
// widget to plain text.
|
|
@@ -774,3 +818,114 @@ describe("contributes.lsp_servers", () => {
|
|
|
774
818
|
expect(parsed.data.contributes?.lsp_servers).toEqual({});
|
|
775
819
|
});
|
|
776
820
|
});
|
|
821
|
+
|
|
822
|
+
describe("contributes.message_actions", () => {
|
|
823
|
+
it("preserves renderer-specific action args through the pack-path parse", () => {
|
|
824
|
+
const action = {
|
|
825
|
+
args: {
|
|
826
|
+
dispatch: "reactions.toggle",
|
|
827
|
+
renderer: "reaction-picker",
|
|
828
|
+
},
|
|
829
|
+
capability: "reactions.toggle",
|
|
830
|
+
id: "reactions.picker",
|
|
831
|
+
kind: "menu",
|
|
832
|
+
label: "Add reaction",
|
|
833
|
+
plugin: "@ryu/reactions",
|
|
834
|
+
target: "user",
|
|
835
|
+
};
|
|
836
|
+
const parsed = PluginManifestSchema.safeParse({
|
|
837
|
+
id: "com.example.reactions",
|
|
838
|
+
name: "Reactions",
|
|
839
|
+
version: "1.0.0",
|
|
840
|
+
runnables: [],
|
|
841
|
+
contributes: { message_actions: [action] },
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
expect(parsed.success).toBe(true);
|
|
845
|
+
if (!parsed.success) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
expect(parsed.data.contributes?.message_actions).toEqual([action]);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
describe("contributes.selection_actions", () => {
|
|
853
|
+
it("preserves host-owned selection dispatch args through the pack-path parse", () => {
|
|
854
|
+
const action = {
|
|
855
|
+
args: { dispatch: "side-chat.selection", intent: "explain" },
|
|
856
|
+
id: "side-chats.explain-selection",
|
|
857
|
+
kind: "button",
|
|
858
|
+
label: "Explain",
|
|
859
|
+
order: 110,
|
|
860
|
+
};
|
|
861
|
+
const parsed = PluginManifestSchema.safeParse({
|
|
862
|
+
id: "com.example.side-chats",
|
|
863
|
+
name: "Side Chats",
|
|
864
|
+
version: "1.0.0",
|
|
865
|
+
runnables: [],
|
|
866
|
+
contributes: { selection_actions: [action] },
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
expect(parsed.success).toBe(true);
|
|
870
|
+
if (!parsed.success) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
expect(parsed.data.contributes?.selection_actions).toEqual([action]);
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
describe("mcp_servers OAuth", () => {
|
|
878
|
+
const manifest = {
|
|
879
|
+
id: "com.example.mail",
|
|
880
|
+
mcp_servers: {
|
|
881
|
+
mail: {
|
|
882
|
+
auth: { client_id: "public-client", type: "oauth" },
|
|
883
|
+
type: "streamable-http",
|
|
884
|
+
url: "https://mcp.example.com/mcp",
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
name: "Mail",
|
|
888
|
+
permission_grants: ["mcp:server", "identity.read"],
|
|
889
|
+
runnables: [],
|
|
890
|
+
version: "1.0.0",
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
it("preserves the public OAuth declaration through the pack-path parse", () => {
|
|
894
|
+
const parsed = PluginManifestSchema.parse(manifest);
|
|
895
|
+
expect(parsed.mcp_servers?.mail?.auth).toEqual({
|
|
896
|
+
client_id: "public-client",
|
|
897
|
+
type: "oauth",
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("rejects secret auth fields and non-loopback plaintext URLs", () => {
|
|
902
|
+
expect(
|
|
903
|
+
PluginManifestSchema.safeParse({
|
|
904
|
+
...manifest,
|
|
905
|
+
mcp_servers: {
|
|
906
|
+
mail: {
|
|
907
|
+
...manifest.mcp_servers.mail,
|
|
908
|
+
auth: { client_secret: "nope", type: "oauth" },
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
}).success
|
|
912
|
+
).toBe(false);
|
|
913
|
+
expect(
|
|
914
|
+
PluginManifestSchema.safeParse({
|
|
915
|
+
...manifest,
|
|
916
|
+
mcp_servers: {
|
|
917
|
+
mail: { auth: { type: "oauth" }, url: "http://mcp.example.com" },
|
|
918
|
+
},
|
|
919
|
+
}).success
|
|
920
|
+
).toBe(false);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("requires both MCP and identity grants", () => {
|
|
924
|
+
expect(
|
|
925
|
+
PluginManifestSchema.safeParse({
|
|
926
|
+
...manifest,
|
|
927
|
+
permission_grants: ["mcp:server"],
|
|
928
|
+
}).success
|
|
929
|
+
).toBe(false);
|
|
930
|
+
});
|
|
931
|
+
});
|