@ryuhq/sdk 0.2.0 → 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 +51 -2
- 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.d.cts +1 -1
- package/dist/agent-plugin.d.ts +1 -1
- package/dist/agent.cjs +7 -0
- package/dist/agent.d.cts +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{app-DNaGmLVf.d.cts → app-B0Z9Ew_R.d.cts} +18 -6
- package/dist/{app-Bkw7LlCK.d.ts → app-C-BDJwfG.d.ts} +18 -6
- package/dist/builder.cjs +110 -11
- package/dist/builder.d.cts +7 -2
- package/dist/builder.d.ts +7 -2
- package/dist/builder.js +4 -3
- package/dist/chunk-4TPUZDTI.js +94 -0
- package/dist/{chunk-T5676WL2.js → chunk-BC3A7HMO.js} +1 -77
- package/dist/{chunk-IOLP5FFE.js → chunk-HLKJZAFK.js} +9 -2
- package/dist/{chunk-IEUQ3CDG.js → chunk-NZKVOSC2.js} +71 -5
- package/dist/chunk-QYFUNJOH.js +83 -0
- package/dist/{chunk-W3KPP4WN.js → chunk-SN2QBJUF.js} +20 -7
- package/dist/{chunk-ULSVL7EC.js → chunk-Z57QDDJR.js} +1 -1
- package/dist/{chunk-A3RGEPDG.js → chunk-ZTJWBRUL.js} +32 -0
- package/dist/cli.cjs +93 -8
- package/dist/cli.js +33 -8
- package/dist/index.cjs +297 -87
- package/dist/index.d.cts +10 -9
- package/dist/index.d.ts +10 -9
- package/dist/index.js +34 -19
- package/dist/manifest.cjs +76 -6
- package/dist/manifest.d.cts +157 -5
- package/dist/manifest.d.ts +157 -5
- package/dist/manifest.js +9 -1
- package/dist/mcp/server.d.cts +2 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/runnable.cjs +279 -71
- package/dist/runnable.d.cts +6 -3
- package/dist/runnable.d.ts +6 -3
- package/dist/runnable.js +11 -5
- package/dist/{tool-DSx2bFx8.d.ts → tool-AjkdFvhE.d.ts} +54 -4
- package/dist/{tool-u-VR0fLF.d.cts → tool-CgzW92O_.d.cts} +54 -4
- package/package.json +12 -3
- package/src/agent-plugin.ts +1 -1
- package/src/builder.ts +9 -0
- package/src/cli-security.test.ts +109 -0
- package/src/cli.ts +43 -10
- package/src/contracts-lockstep.test.ts +16 -2
- package/src/exports-lockstep.test.ts +1 -0
- package/src/generated/plugin-manifest.ts +82 -5
- package/src/index.ts +18 -0
- package/src/manifest.fixtures.test.ts +9 -3
- package/src/manifest.test.ts +69 -0
- package/src/manifest.ts +137 -18
- package/src/mcp/server.ts +2 -1
- package/src/runnable/action.test.ts +128 -0
- package/src/runnable/action.ts +202 -0
- package/src/runnable/app.ts +45 -12
- package/src/runnable/index.ts +18 -3
- package/src/runnable/primitives.test.ts +34 -0
- package/src/runnable/primitives.ts +59 -0
- package/src/runnable/runnable-types.ts +3 -0
- package/src/runnable/tool.ts +35 -4
- package/src/runnable/turn-hook.ts +4 -2
- 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
|
|
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.
|
|
3
|
+
"version": "0.2.2",
|
|
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,11 +93,11 @@
|
|
|
84
93
|
"clean": "rm -rf dist"
|
|
85
94
|
},
|
|
86
95
|
"dependencies": {
|
|
87
|
-
"@ryuhq/sdk-native": "
|
|
96
|
+
"@ryuhq/sdk-native": "workspace:*",
|
|
88
97
|
"zod": "^4.1.13"
|
|
89
98
|
},
|
|
90
99
|
"devDependencies": {
|
|
91
|
-
"@types/bun": "
|
|
100
|
+
"@types/bun": "catalog:",
|
|
92
101
|
"@types/node": "^22.20.1",
|
|
93
102
|
"json-schema-to-typescript": "^15.0.4",
|
|
94
103
|
"tsup": "^8.5.1",
|
package/src/agent-plugin.ts
CHANGED
|
@@ -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.
|
|
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 =
|
|
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(
|
|
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
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
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
|
|
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
|
|
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`, `
|
|
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
|
|
2999
|
-
|
|
3000
|
-
|
|
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,
|
|
@@ -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 };
|
|
@@ -419,7 +425,7 @@ describe("PluginManifestSchema preserves turn_hook.match", () => {
|
|
|
419
425
|
// `tool-firewall` is a packaged plugin, so its manifest lives in its
|
|
420
426
|
// package directory — there is no fixture copy any more.
|
|
421
427
|
readFileSync(
|
|
422
|
-
join(REPO_ROOT, "plugins-store/tool-firewall/manifest.json"),
|
|
428
|
+
join(REPO_ROOT, "plugins-store/plugins/tool-firewall/manifest.json"),
|
|
423
429
|
"utf8"
|
|
424
430
|
)
|
|
425
431
|
);
|