@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/mcp/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ryuhq/sdk/mcp — MCP server + client surfaces.
|
|
3
|
+
*
|
|
4
|
+
* `McpServer` handles the MCP `initialize` handshake, `tools/list`,
|
|
5
|
+
* `tools/call`, and the content-block envelope over stdio; `listTools` /
|
|
6
|
+
* `callTool` drive a stdio MCP server. Re-exported here so consumers can
|
|
7
|
+
* import from `@ryuhq/sdk/mcp` (or the narrower `/mcp/server` and
|
|
8
|
+
* `/mcp/client` subpaths) as a single entry point.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type { McpStdioCommand, McpTool } from "./client.ts";
|
|
12
|
+
export { callTool, listTools, MCP_PROTOCOL_VERSION } from "./client.ts";
|
|
13
|
+
export type {
|
|
14
|
+
JsonSchema,
|
|
15
|
+
PassthroughRegistration,
|
|
16
|
+
SdkRunnable,
|
|
17
|
+
} from "./server.ts";
|
|
18
|
+
export { McpServer, unwrapContent } from "./server.ts";
|
package/src/mcp/server.ts
CHANGED
|
@@ -28,7 +28,8 @@ import { callTool, listTools, MCP_PROTOCOL_VERSION } from "./client.ts";
|
|
|
28
28
|
/** JSON Schema fragment — enough to describe a tool's input arguments. */
|
|
29
29
|
export interface JsonSchema {
|
|
30
30
|
description?: string;
|
|
31
|
-
|
|
31
|
+
/** Property schemas may use any JSON Schema dialect or nested shape. */
|
|
32
|
+
properties?: Record<string, unknown>;
|
|
32
33
|
required?: string[];
|
|
33
34
|
type?: string;
|
|
34
35
|
[key: string]: unknown;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ryuhq/sdk/model — the gateway-mandatory model client.
|
|
3
|
+
*
|
|
4
|
+
* Every model call routes through the Ryu Gateway, never a direct provider.
|
|
5
|
+
* Re-exports the `ModelClient` / `defineModel` surface plus the gateway
|
|
6
|
+
* resolution + egress helpers, so consumers can import from
|
|
7
|
+
* `@ryuhq/sdk/model` as a single entry point.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
ChatDelta,
|
|
12
|
+
ChatMessage,
|
|
13
|
+
ChatResult,
|
|
14
|
+
ModelClientOptions,
|
|
15
|
+
} from "./client.ts";
|
|
16
|
+
export { defineModel, ModelClient } from "./client.ts";
|
|
17
|
+
export {
|
|
18
|
+
assertAllowedEgressUrl,
|
|
19
|
+
DEFAULT_GATEWAY_URL,
|
|
20
|
+
resolveGatewayToken,
|
|
21
|
+
resolveGatewayUrl,
|
|
22
|
+
} from "./gateway.ts";
|
package/src/plugin/ryu-plugin.ts
CHANGED
|
@@ -125,6 +125,27 @@ export interface ThemeContribution {
|
|
|
125
125
|
* are RPCs over the postMessage bridge; the plugin never holds a Core token or
|
|
126
126
|
* Tauri IPC handle directly. */
|
|
127
127
|
export interface RyuHostServices {
|
|
128
|
+
/** Read and cooperatively stop Core-visible background processes. */
|
|
129
|
+
background: {
|
|
130
|
+
list(input?: { producer?: string; running_only?: boolean }): Promise<
|
|
131
|
+
{
|
|
132
|
+
process_id: string;
|
|
133
|
+
command: string;
|
|
134
|
+
cwd: string;
|
|
135
|
+
elapsed_ms: number;
|
|
136
|
+
running: boolean;
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
}[]
|
|
139
|
+
>;
|
|
140
|
+
stop(input: { process_id: string }): Promise<{
|
|
141
|
+
ok: boolean;
|
|
142
|
+
requested: boolean;
|
|
143
|
+
process_id: string;
|
|
144
|
+
}>;
|
|
145
|
+
};
|
|
146
|
+
/** Read-only feature discovery. Older hosts may omit this method; treat that
|
|
147
|
+
* as an empty capability set rather than assuming a native surface exists. */
|
|
148
|
+
capabilities?(): Promise<RyuHostCapabilities>;
|
|
128
149
|
/** Run a registered command by id (built-in or contributed). */
|
|
129
150
|
commands: { execute(id: string, ...args: unknown[]): Promise<unknown> };
|
|
130
151
|
/** Gateway-governed model access (chat/embed). Mirrors `@ryuhq/sdk` model
|
|
@@ -140,6 +161,21 @@ export interface RyuHostServices {
|
|
|
140
161
|
* or any other agent field (invariant: no capability returns a secret). Gated
|
|
141
162
|
* by the `core:list_agents` grant. */
|
|
142
163
|
listAgents(): Promise<{ id: string; name: string }[]>;
|
|
164
|
+
/** Mobile-native effects. These methods remain optional at the type boundary
|
|
165
|
+
* because desktop, web, extension, and terminal hosts fail closed. */
|
|
166
|
+
native?: {
|
|
167
|
+
haptics(input: RyuNativeHapticsInput): Promise<RyuNativeHapticsResult>;
|
|
168
|
+
notifications: {
|
|
169
|
+
create(
|
|
170
|
+
input: RyuNativeNotificationInput
|
|
171
|
+
): Promise<RyuNativeNotificationResult>;
|
|
172
|
+
};
|
|
173
|
+
liveActivities: {
|
|
174
|
+
update(
|
|
175
|
+
input: RyuNativeLiveActivityInput
|
|
176
|
+
): Promise<RyuNativeLiveActivityResult>;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
143
179
|
/** Open a tab at a path (built-in or a route this plugin contributed). */
|
|
144
180
|
openTab(path: string): void;
|
|
145
181
|
/** Read/write the plugin's own Spaces docs (scoped by grant). */
|
|
@@ -152,6 +188,52 @@ export interface RyuHostServices {
|
|
|
152
188
|
};
|
|
153
189
|
}
|
|
154
190
|
|
|
191
|
+
/** The token-free, read-only native feature inventory returned by a host. */
|
|
192
|
+
export interface RyuHostCapabilities {
|
|
193
|
+
androidOngoingNotifications: boolean;
|
|
194
|
+
browserNotifications: boolean;
|
|
195
|
+
dynamicIsland: boolean;
|
|
196
|
+
haptics: boolean;
|
|
197
|
+
hardwareBleRelay: boolean;
|
|
198
|
+
liveActivities: boolean;
|
|
199
|
+
localNotifications: boolean;
|
|
200
|
+
platform: "android" | "browser" | "ios" | "unknown";
|
|
201
|
+
pushRegistration: boolean;
|
|
202
|
+
quickActions: boolean;
|
|
203
|
+
sounds: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type RyuNativeHapticStyle = "light" | "success" | "warning" | "error";
|
|
207
|
+
|
|
208
|
+
export interface RyuNativeHapticsInput {
|
|
209
|
+
style: RyuNativeHapticStyle;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface RyuNativeNotificationInput {
|
|
213
|
+
body: string;
|
|
214
|
+
title: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface RyuNativeLiveActivityInput {
|
|
218
|
+
conversationId: string;
|
|
219
|
+
detail: string;
|
|
220
|
+
status: "running" | "waiting" | "review" | "done" | "error";
|
|
221
|
+
title: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface RyuNativeHapticsResult {
|
|
225
|
+
signaled: true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface RyuNativeNotificationResult {
|
|
229
|
+
id: string;
|
|
230
|
+
scheduled: true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface RyuNativeLiveActivityResult {
|
|
234
|
+
updated: true;
|
|
235
|
+
}
|
|
236
|
+
|
|
155
237
|
// ── The host API a plugin's activate() receives ───────────────────────────────
|
|
156
238
|
|
|
157
239
|
/** Everything a plugin can contribute. Each `register*` returns a
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { PluginManifestSchema } from "../manifest.ts";
|
|
3
|
+
import { defineAction } from "./action.ts";
|
|
4
|
+
import type { GatewayClient, RunnableContext } from "./runnable-types.ts";
|
|
5
|
+
|
|
6
|
+
const gateway: GatewayClient = {
|
|
7
|
+
chat: async () => ({ content: "ok", finishReason: "stop" }),
|
|
8
|
+
async *stream() {
|
|
9
|
+
yield { content: "ok", finishReason: null };
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const context: RunnableContext = { gateway };
|
|
14
|
+
|
|
15
|
+
const ticketAction = defineAction({
|
|
16
|
+
id: "action-create-ticket",
|
|
17
|
+
name: "Create Ticket",
|
|
18
|
+
description: "Create a support ticket for a customer.",
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
customer: { type: "string" },
|
|
23
|
+
summary: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["customer", "summary"],
|
|
26
|
+
},
|
|
27
|
+
outputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
id: { type: "string" },
|
|
31
|
+
},
|
|
32
|
+
required: ["id"],
|
|
33
|
+
},
|
|
34
|
+
effect: "mutate",
|
|
35
|
+
needsApproval: true,
|
|
36
|
+
run: async ({ customer, summary }) => ({
|
|
37
|
+
id: `${customer}:${summary}`,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("defineAction", () => {
|
|
42
|
+
it("returns a governed action with the same Runnable execution contract", async () => {
|
|
43
|
+
expect(ticketAction.kind).toBe("tool");
|
|
44
|
+
expect(ticketAction.action).toBe(true);
|
|
45
|
+
expect(ticketAction.description).toBe(
|
|
46
|
+
"Create a support ticket for a customer."
|
|
47
|
+
);
|
|
48
|
+
expect(ticketAction.schema.required).toEqual(["customer", "summary"]);
|
|
49
|
+
expect(ticketAction.outputSchema?.required).toEqual(["id"]);
|
|
50
|
+
expect(ticketAction.annotations).toEqual({
|
|
51
|
+
destructiveHint: true,
|
|
52
|
+
readOnlyHint: false,
|
|
53
|
+
});
|
|
54
|
+
expect(ticketAction.needsApproval).toBe(true);
|
|
55
|
+
expect(
|
|
56
|
+
await ticketAction.run({ customer: "acme", summary: "Login" }, context)
|
|
57
|
+
).toEqual({ id: "acme:Login" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects an action whose declared effect conflicts with its annotations", () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
defineAction({
|
|
63
|
+
id: "action-conflict",
|
|
64
|
+
name: "Conflicting Action",
|
|
65
|
+
description: "An invalid action.",
|
|
66
|
+
schema: { type: "object", properties: {} },
|
|
67
|
+
effect: "read",
|
|
68
|
+
annotations: { destructiveHint: true },
|
|
69
|
+
run: async () => null,
|
|
70
|
+
})
|
|
71
|
+
).toThrow(/destructiveHint/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("lowers to a Core-compatible governed inline tool manifest", () => {
|
|
75
|
+
const manifest = ticketAction.toManifest({
|
|
76
|
+
id: "com.example.support",
|
|
77
|
+
version: "1.0.0",
|
|
78
|
+
grants: ["storage:kv"],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(() => PluginManifestSchema.parse(manifest)).not.toThrow();
|
|
82
|
+
expect(manifest.permission_grants).toEqual(["storage:kv", "tool:execute"]);
|
|
83
|
+
expect(manifest.runnables).toHaveLength(1);
|
|
84
|
+
expect(manifest.runnables[0]).toMatchObject({
|
|
85
|
+
id: "action-create-ticket",
|
|
86
|
+
name: "Create Ticket",
|
|
87
|
+
kind: "tool",
|
|
88
|
+
});
|
|
89
|
+
expect(manifest.runnables[0]?.config).toMatchObject({
|
|
90
|
+
slug: "action-create-ticket",
|
|
91
|
+
backend: "inline_deno",
|
|
92
|
+
action: true,
|
|
93
|
+
description: "Create a support ticket for a customer.",
|
|
94
|
+
input_schema: ticketAction.schema,
|
|
95
|
+
output_schema: ticketAction.outputSchema,
|
|
96
|
+
annotations: ticketAction.annotations,
|
|
97
|
+
needs_approval: true,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("adapts the same implementation to the SDK MCP server", async () => {
|
|
102
|
+
const mcpTool = ticketAction.toMcpTool(context);
|
|
103
|
+
|
|
104
|
+
expect(mcpTool.name).toBe("action-create-ticket");
|
|
105
|
+
expect(mcpTool.description).toBe("Create a support ticket for a customer.");
|
|
106
|
+
expect(mcpTool.inputSchema).toEqual(ticketAction.schema);
|
|
107
|
+
expect(await mcpTool.run({ customer: "acme", summary: "MCP" })).toEqual({
|
|
108
|
+
id: "acme:MCP",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("allows explicit read-only actions without approval", () => {
|
|
113
|
+
const action = defineAction({
|
|
114
|
+
id: "action-find-ticket",
|
|
115
|
+
name: "Find Ticket",
|
|
116
|
+
description: "Find a ticket by id.",
|
|
117
|
+
schema: { type: "object", properties: { id: { type: "string" } } },
|
|
118
|
+
effect: "read",
|
|
119
|
+
run: async ({ id }) => ({ id }),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(action.annotations).toEqual({
|
|
123
|
+
readOnlyHint: true,
|
|
124
|
+
destructiveHint: false,
|
|
125
|
+
});
|
|
126
|
+
expect(action.needsApproval).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Ryu Action authoring API.
|
|
3
|
+
*
|
|
4
|
+
* An Action is the semantic contract for a business operation: one description,
|
|
5
|
+
* input schema, output schema, implementation, and effect declaration. It lowers
|
|
6
|
+
* to Ryu's existing governed `inline_deno` Tool backend, so this adds a coherent
|
|
7
|
+
* authoring seam without introducing a second Core execution runtime.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type PluginManifest,
|
|
12
|
+
PluginManifestSchema,
|
|
13
|
+
type RunnableMeta,
|
|
14
|
+
type Surface,
|
|
15
|
+
} from "../manifest.ts";
|
|
16
|
+
import type { SdkRunnable } from "../mcp/server.ts";
|
|
17
|
+
import type { RunnableContext } from "./runnable-types.ts";
|
|
18
|
+
import {
|
|
19
|
+
defineTool,
|
|
20
|
+
type InlineToolManifestOptions,
|
|
21
|
+
inlineToolRunnable,
|
|
22
|
+
type ToolRunnable,
|
|
23
|
+
type ToolSchema,
|
|
24
|
+
} from "./tool.ts";
|
|
25
|
+
|
|
26
|
+
/** The two effects Core can enforce from tool annotations. */
|
|
27
|
+
export type ActionEffect = "mutate" | "read";
|
|
28
|
+
|
|
29
|
+
/** MCP-compatible effect hints plus any future provider-neutral boolean hints. */
|
|
30
|
+
export interface ActionAnnotations {
|
|
31
|
+
destructiveHint?: boolean;
|
|
32
|
+
idempotentHint?: boolean;
|
|
33
|
+
openWorldHint?: boolean;
|
|
34
|
+
readOnlyHint?: boolean;
|
|
35
|
+
[key: string]: boolean | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Options for defining a canonical Action. */
|
|
39
|
+
export interface ActionOptions<
|
|
40
|
+
TInput extends Record<string, unknown>,
|
|
41
|
+
TOutput,
|
|
42
|
+
> {
|
|
43
|
+
/** Additional MCP-compatible effect hints. */
|
|
44
|
+
annotations?: ActionAnnotations;
|
|
45
|
+
/** Description the agent reads when deciding whether to call the action. */
|
|
46
|
+
description: string;
|
|
47
|
+
/** Explicit effect used by read-only and approval enforcement. */
|
|
48
|
+
effect: ActionEffect;
|
|
49
|
+
/** Stable action id, also used as the generated Core tool slug. */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Human-readable action name. */
|
|
52
|
+
name: string;
|
|
53
|
+
/** Require human approval even when global smart mode would not classify it. */
|
|
54
|
+
needsApproval?: boolean;
|
|
55
|
+
/** JSON Schema for the structured action result. */
|
|
56
|
+
outputSchema?: Record<string, unknown>;
|
|
57
|
+
/** The one implementation used by local callers and the packaged tool body. */
|
|
58
|
+
run(input: TInput, ctx: RunnableContext): Promise<TOutput>;
|
|
59
|
+
/** JSON Schema for all action inputs. */
|
|
60
|
+
schema: ToolSchema;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Options for lowering one Action into a standalone Ryu plugin manifest. */
|
|
64
|
+
export interface ActionManifestOptions {
|
|
65
|
+
/** Plugin activation events; defaults to eager activation. */
|
|
66
|
+
activationEvents?: readonly string[];
|
|
67
|
+
/** Extra Gateway grants required by the action body. */
|
|
68
|
+
grants?: readonly string[];
|
|
69
|
+
/** Reverse-domain plugin id (for example `com.acme.support`). */
|
|
70
|
+
id: string;
|
|
71
|
+
/** Display name; defaults to the Action name. */
|
|
72
|
+
name?: string;
|
|
73
|
+
/** Host surfaces this plugin targets. */
|
|
74
|
+
targets?: readonly Surface[];
|
|
75
|
+
/** Plugin semver. */
|
|
76
|
+
version: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A ToolRunnable with Action semantics and manifest/MCP adapters. */
|
|
80
|
+
export interface ActionRunnable<
|
|
81
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
82
|
+
TOutput = unknown,
|
|
83
|
+
> extends ToolRunnable<TInput, TOutput> {
|
|
84
|
+
/** Discriminates this semantic contract from an ordinary ToolRunnable. */
|
|
85
|
+
readonly action: true;
|
|
86
|
+
/** Effect hints lowered into Core's existing tool metadata. */
|
|
87
|
+
readonly annotations: ActionAnnotations;
|
|
88
|
+
/** Required action description. */
|
|
89
|
+
readonly description: string;
|
|
90
|
+
/** Declared effect. */
|
|
91
|
+
readonly effect: ActionEffect;
|
|
92
|
+
/** Whether Core must queue approval before execution. */
|
|
93
|
+
readonly needsApproval: boolean;
|
|
94
|
+
/** Structured result schema, when the action returns one. */
|
|
95
|
+
readonly outputSchema?: Record<string, unknown>;
|
|
96
|
+
/** Lower this Action to a validated, installable plugin manifest. */
|
|
97
|
+
toManifest(options: ActionManifestOptions): PluginManifest;
|
|
98
|
+
/** Adapt this Action to the SDK MCP server using the same implementation. */
|
|
99
|
+
toMcpTool(context: RunnableContext): SdkRunnable;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function deriveAnnotations(
|
|
103
|
+
effect: ActionEffect,
|
|
104
|
+
annotations: ActionAnnotations | undefined
|
|
105
|
+
): ActionAnnotations {
|
|
106
|
+
const resolved = {
|
|
107
|
+
...annotations,
|
|
108
|
+
readOnlyHint: annotations?.readOnlyHint ?? effect === "read",
|
|
109
|
+
destructiveHint: annotations?.destructiveHint ?? effect === "mutate",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (effect === "read" && resolved.destructiveHint) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"[ryu-sdk] read actions cannot set annotations.destructiveHint=true"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (effect === "mutate" && resolved.readOnlyHint) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"[ryu-sdk] mutate actions cannot set annotations.readOnlyHint=true"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function actionManifestEntry(action: ActionRunnable): RunnableMeta {
|
|
127
|
+
const options: InlineToolManifestOptions = {
|
|
128
|
+
action: true,
|
|
129
|
+
annotations: action.annotations,
|
|
130
|
+
description: action.description,
|
|
131
|
+
needsApproval: action.needsApproval,
|
|
132
|
+
...(action.outputSchema ? { outputSchema: action.outputSchema } : {}),
|
|
133
|
+
};
|
|
134
|
+
return inlineToolRunnable(action, options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function actionToManifest(
|
|
138
|
+
action: ActionRunnable,
|
|
139
|
+
options: ActionManifestOptions
|
|
140
|
+
): PluginManifest {
|
|
141
|
+
const grants = [...new Set([...(options.grants ?? []), "tool:execute"])];
|
|
142
|
+
const raw = {
|
|
143
|
+
id: options.id,
|
|
144
|
+
name: options.name ?? action.name,
|
|
145
|
+
version: options.version,
|
|
146
|
+
runnables: [actionManifestEntry(action)],
|
|
147
|
+
permission_grants: grants,
|
|
148
|
+
activation_events: [...(options.activationEvents ?? ["*"])],
|
|
149
|
+
targets: [...(options.targets ?? [])],
|
|
150
|
+
};
|
|
151
|
+
const result = PluginManifestSchema.safeParse(raw);
|
|
152
|
+
if (!result.success) {
|
|
153
|
+
const first = result.error.issues[0];
|
|
154
|
+
const field = first?.path.join(".") ?? "unknown";
|
|
155
|
+
const message = first?.message ?? "validation failed";
|
|
156
|
+
throw new Error(
|
|
157
|
+
`[ryu-sdk] action manifest validation failed at '${field}': ${message}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return result.data;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Define one business operation that can be run locally, packaged as a Core
|
|
165
|
+
* tool, or registered on an SDK MCP server without rewriting its implementation.
|
|
166
|
+
*/
|
|
167
|
+
export function defineAction<
|
|
168
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
169
|
+
TOutput = unknown,
|
|
170
|
+
>(options: ActionOptions<TInput, TOutput>): ActionRunnable<TInput, TOutput> {
|
|
171
|
+
const annotations = deriveAnnotations(options.effect, options.annotations);
|
|
172
|
+
const tool = defineTool({
|
|
173
|
+
description: options.description,
|
|
174
|
+
id: options.id,
|
|
175
|
+
name: options.name,
|
|
176
|
+
run: options.run,
|
|
177
|
+
schema: options.schema,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const action: ActionRunnable<TInput, TOutput> = {
|
|
181
|
+
...tool,
|
|
182
|
+
action: true,
|
|
183
|
+
annotations,
|
|
184
|
+
description: options.description,
|
|
185
|
+
effect: options.effect,
|
|
186
|
+
needsApproval: options.needsApproval ?? false,
|
|
187
|
+
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
|
188
|
+
toManifest(manifestOptions) {
|
|
189
|
+
return actionToManifest(action, manifestOptions);
|
|
190
|
+
},
|
|
191
|
+
toMcpTool(context) {
|
|
192
|
+
return {
|
|
193
|
+
name: action.id,
|
|
194
|
+
description: action.description,
|
|
195
|
+
inputSchema: action.schema,
|
|
196
|
+
run: (input) => action.run(input as TInput, context),
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return action;
|
|
202
|
+
}
|
package/src/runnable/app.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ function checklistApp(grants?: string[]) {
|
|
|
9
9
|
slug: "checklist",
|
|
10
10
|
title: "Checklist",
|
|
11
11
|
version: "1.0.0",
|
|
12
|
+
uiEntry: "src/checklist.tsx",
|
|
12
13
|
...(grants ? { grants } : {}),
|
|
13
14
|
tools: [{ name: "render", description: "Render a checklist" }],
|
|
14
15
|
});
|
|
@@ -48,6 +49,7 @@ describe("defineApp widget grant", () => {
|
|
|
48
49
|
slug: "tools",
|
|
49
50
|
title: "Tools",
|
|
50
51
|
version: "1.0.0",
|
|
52
|
+
uiEntry: "src/tools.tsx",
|
|
51
53
|
tools: [{ name: "toggle", description: "Toggle", accessible: true }],
|
|
52
54
|
});
|
|
53
55
|
expect(manifest.contributes?.widgets ?? []).toHaveLength(0);
|
package/src/runnable/app.ts
CHANGED
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
* - A **companion** tool (`accessible:true`) is a call target a mounted widget
|
|
15
15
|
* may invoke: it carries `widget_accessible:true` and gets no widget template.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
17
|
+
* A tool may also attach a `ToolRunnable`. When present, its self-contained run
|
|
18
|
+
* body is emitted as Core's grant-gated `inline_deno` backend, so the widget and
|
|
19
|
+
* the behavior ship in one bundle. Without a runnable, the tool remains the
|
|
20
|
+
* declarative pass-through form: the widget renders from
|
|
21
|
+
* `window.openai.toolInput`/`toolOutput` and Core echoes the validated arguments
|
|
22
|
+
* as `structuredContent`. `ryu pack` bundles the `uiEntry` source into the
|
|
23
|
+
* manifest's `ui_code`.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
import type {
|
|
@@ -26,11 +28,13 @@ import type {
|
|
|
26
28
|
PluginManifest,
|
|
27
29
|
Requires,
|
|
28
30
|
RunnableMeta,
|
|
31
|
+
SlashCommandContribution,
|
|
29
32
|
Surface,
|
|
30
33
|
ToolAppConfig,
|
|
31
34
|
WidgetContribution,
|
|
32
35
|
} from "../manifest.ts";
|
|
33
36
|
import { PluginManifestSchema } from "../manifest.ts";
|
|
37
|
+
import { inlineToolRunnable, type ToolRunnable } from "./tool.ts";
|
|
34
38
|
|
|
35
39
|
/** The default widget MIME dialect (mirrors Core `default_widget_mime`). */
|
|
36
40
|
const DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
|
|
@@ -59,6 +63,21 @@ function withWidgetRenderGrant(
|
|
|
59
63
|
return out;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/** Add the grant required for any executable inline tool body. */
|
|
67
|
+
function withToolExecuteGrant(
|
|
68
|
+
grants: readonly string[],
|
|
69
|
+
tools: readonly AppToolSpec[]
|
|
70
|
+
): string[] {
|
|
71
|
+
const out = [...grants];
|
|
72
|
+
if (
|
|
73
|
+
tools.some((tool) => tool.runnable !== undefined) &&
|
|
74
|
+
!out.includes("tool:execute")
|
|
75
|
+
) {
|
|
76
|
+
out.push("tool:execute");
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/** One tool a Ryu App declares. */
|
|
63
82
|
export interface AppToolSpec {
|
|
64
83
|
/**
|
|
@@ -74,8 +93,15 @@ export interface AppToolSpec {
|
|
|
74
93
|
invoked?: string;
|
|
75
94
|
/** Status label shown while a render tool runs (e.g. `"Plotting chart…"`). */
|
|
76
95
|
invoking?: string;
|
|
77
|
-
/** Tool name (unqualified). The wire id is `<server
|
|
96
|
+
/** Tool name (unqualified). The wire id is `<server>.<name>`. */
|
|
78
97
|
name: string;
|
|
98
|
+
/**
|
|
99
|
+
* Optional executable body for this tool. Core receives it as an
|
|
100
|
+
* `inline_deno` runnable and runs it in the deny-by-default tool sandbox.
|
|
101
|
+
* The body must be self-contained; use the injected `host` capability surface
|
|
102
|
+
* for governed model calls and other platform primitives.
|
|
103
|
+
*/
|
|
104
|
+
runnable?: Pick<ToolRunnable, "code" | "id" | "name" | "schema">;
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
/**
|
|
@@ -116,6 +142,8 @@ export interface DefineAppOptions {
|
|
|
116
142
|
requires?: DefineAppRequires;
|
|
117
143
|
/** MCP server namespace for the tool ids. Defaults to `slug`. */
|
|
118
144
|
server?: string;
|
|
145
|
+
/** Slash commands this app exposes in the chat composer. */
|
|
146
|
+
slashCommands?: SlashCommandContribution[];
|
|
119
147
|
/**
|
|
120
148
|
* App slug — used to build the widget uri (`ui://widget/<slug>.html`) and, when
|
|
121
149
|
* `server` is omitted, the MCP server namespace that qualifies each tool id.
|
|
@@ -141,7 +169,7 @@ export interface DefineAppOptions {
|
|
|
141
169
|
|
|
142
170
|
/** Build a fully-qualified tool id from a server namespace and tool name. */
|
|
143
171
|
export function appToolId(server: string, name: string): string {
|
|
144
|
-
return `${server}
|
|
172
|
+
return `${server}.${name}`;
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
/**
|
|
@@ -182,8 +210,12 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
182
210
|
for (const spec of options.tools) {
|
|
183
211
|
const isRender = spec.accessible !== true;
|
|
184
212
|
const id = appToolId(server, spec.name);
|
|
213
|
+
const executable = spec.runnable
|
|
214
|
+
? inlineToolRunnable(spec.runnable)
|
|
215
|
+
: undefined;
|
|
185
216
|
|
|
186
|
-
const config: ToolAppConfig = {
|
|
217
|
+
const config: ToolAppConfig & Record<string, unknown> = {
|
|
218
|
+
...(executable?.config ?? {}),
|
|
187
219
|
slug: id,
|
|
188
220
|
description: spec.description,
|
|
189
221
|
widget: isRender,
|
|
@@ -213,16 +245,18 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
213
245
|
|
|
214
246
|
const contributes: Contributes = {
|
|
215
247
|
turn_hooks: [],
|
|
248
|
+
chat_features: [],
|
|
216
249
|
// This builder synthesises an app from its runnables; an app that emits
|
|
217
250
|
// events declares them in a hand-authored `manifest.json`, same as
|
|
218
251
|
// `lsp_servers` below.
|
|
219
252
|
hook_events: [],
|
|
220
253
|
composer_controls: [],
|
|
221
254
|
settings_tabs: [],
|
|
222
|
-
slash_commands: [],
|
|
255
|
+
slash_commands: options.slashCommands ?? [],
|
|
223
256
|
sidebar_sections: [],
|
|
224
257
|
sidebar_buttons: [],
|
|
225
258
|
dock_panels: [],
|
|
259
|
+
live_activities: [],
|
|
226
260
|
// Empty for the same reason as every sibling family above: this builder
|
|
227
261
|
// synthesises `widgets` from the app's own runnables and nothing else, and
|
|
228
262
|
// takes no `contributes` passthrough. An app that wants to declare language
|
|
@@ -235,6 +269,8 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
235
269
|
data_categories: [],
|
|
236
270
|
pi_extensions: [],
|
|
237
271
|
output_styles: [],
|
|
272
|
+
message_actions: [],
|
|
273
|
+
selection_actions: [],
|
|
238
274
|
widgets,
|
|
239
275
|
};
|
|
240
276
|
|
|
@@ -249,15 +285,15 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
249
285
|
// Core gates widget promotion on declared-AND-enabled-AND-granted, and a
|
|
250
286
|
// missing grant fails as `DeniedNoGrant` — which is an `info!` log and
|
|
251
287
|
// nothing else. The widget silently renders as plain text, with no error
|
|
252
|
-
// in the UI and nothing pointing at the manifest.
|
|
253
|
-
// through `defineApp` hit that, because the only fix was a grant string
|
|
254
|
-
// the templates never mention and the builder never added; the one
|
|
255
|
-
// working example on disk hand-writes it.
|
|
288
|
+
// in the UI and nothing pointing at the manifest.
|
|
256
289
|
//
|
|
257
290
|
// Added only when there is a widget to render, and unioned rather than
|
|
258
291
|
// overwritten so an author's own `grants` list survives and re-declaring
|
|
259
292
|
// it is not an error.
|
|
260
|
-
permission_grants:
|
|
293
|
+
permission_grants: withToolExecuteGrant(
|
|
294
|
+
withWidgetRenderGrant(options.grants ?? [], widgets),
|
|
295
|
+
options.tools
|
|
296
|
+
),
|
|
261
297
|
activation_events: options.activationEvents ?? ["*"],
|
|
262
298
|
contributes,
|
|
263
299
|
// `targets: []` means EVERY surface, so an app that declares none is
|