@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.
Files changed (66) hide show
  1. package/README.md +51 -2
  2. package/dist/action.cjs +839 -0
  3. package/dist/action.d.cts +88 -0
  4. package/dist/action.d.ts +88 -0
  5. package/dist/action.js +8 -0
  6. package/dist/agent-plugin.d.cts +1 -1
  7. package/dist/agent-plugin.d.ts +1 -1
  8. package/dist/agent.cjs +7 -0
  9. package/dist/agent.d.cts +1 -1
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +2 -2
  12. package/dist/{app-DNaGmLVf.d.cts → app-B0Z9Ew_R.d.cts} +18 -6
  13. package/dist/{app-Bkw7LlCK.d.ts → app-C-BDJwfG.d.ts} +18 -6
  14. package/dist/builder.cjs +110 -11
  15. package/dist/builder.d.cts +7 -2
  16. package/dist/builder.d.ts +7 -2
  17. package/dist/builder.js +4 -3
  18. package/dist/chunk-4TPUZDTI.js +94 -0
  19. package/dist/{chunk-T5676WL2.js → chunk-BC3A7HMO.js} +1 -77
  20. package/dist/{chunk-IOLP5FFE.js → chunk-HLKJZAFK.js} +9 -2
  21. package/dist/{chunk-IEUQ3CDG.js → chunk-NZKVOSC2.js} +71 -5
  22. package/dist/chunk-QYFUNJOH.js +83 -0
  23. package/dist/{chunk-W3KPP4WN.js → chunk-SN2QBJUF.js} +20 -7
  24. package/dist/{chunk-ULSVL7EC.js → chunk-Z57QDDJR.js} +1 -1
  25. package/dist/{chunk-A3RGEPDG.js → chunk-ZTJWBRUL.js} +32 -0
  26. package/dist/cli.cjs +93 -8
  27. package/dist/cli.js +33 -8
  28. package/dist/index.cjs +297 -87
  29. package/dist/index.d.cts +10 -9
  30. package/dist/index.d.ts +10 -9
  31. package/dist/index.js +34 -19
  32. package/dist/manifest.cjs +76 -6
  33. package/dist/manifest.d.cts +157 -5
  34. package/dist/manifest.d.ts +157 -5
  35. package/dist/manifest.js +9 -1
  36. package/dist/mcp/server.d.cts +2 -1
  37. package/dist/mcp/server.d.ts +2 -1
  38. package/dist/runnable.cjs +279 -71
  39. package/dist/runnable.d.cts +6 -3
  40. package/dist/runnable.d.ts +6 -3
  41. package/dist/runnable.js +11 -5
  42. package/dist/{tool-DSx2bFx8.d.ts → tool-AjkdFvhE.d.ts} +54 -4
  43. package/dist/{tool-u-VR0fLF.d.cts → tool-CgzW92O_.d.cts} +54 -4
  44. package/package.json +12 -3
  45. package/src/agent-plugin.ts +1 -1
  46. package/src/builder.ts +9 -0
  47. package/src/cli-security.test.ts +109 -0
  48. package/src/cli.ts +43 -10
  49. package/src/contracts-lockstep.test.ts +16 -2
  50. package/src/exports-lockstep.test.ts +1 -0
  51. package/src/generated/plugin-manifest.ts +82 -5
  52. package/src/index.ts +18 -0
  53. package/src/manifest.fixtures.test.ts +9 -3
  54. package/src/manifest.test.ts +69 -0
  55. package/src/manifest.ts +137 -18
  56. package/src/mcp/server.ts +2 -1
  57. package/src/runnable/action.test.ts +128 -0
  58. package/src/runnable/action.ts +202 -0
  59. package/src/runnable/app.ts +45 -12
  60. package/src/runnable/index.ts +18 -3
  61. package/src/runnable/primitives.test.ts +34 -0
  62. package/src/runnable/primitives.ts +59 -0
  63. package/src/runnable/runnable-types.ts +3 -0
  64. package/src/runnable/tool.ts +35 -4
  65. package/src/runnable/turn-hook.ts +4 -2
  66. package/src/slash-command.test.ts +69 -0
@@ -0,0 +1,94 @@
1
+ import {
2
+ defineTool,
3
+ inlineToolRunnable
4
+ } from "./chunk-QYFUNJOH.js";
5
+ import {
6
+ PluginManifestSchema
7
+ } from "./chunk-NZKVOSC2.js";
8
+
9
+ // src/runnable/action.ts
10
+ function deriveAnnotations(effect, annotations) {
11
+ const resolved = {
12
+ ...annotations,
13
+ readOnlyHint: annotations?.readOnlyHint ?? effect === "read",
14
+ destructiveHint: annotations?.destructiveHint ?? effect === "mutate"
15
+ };
16
+ if (effect === "read" && resolved.destructiveHint) {
17
+ throw new Error(
18
+ "[ryu-sdk] read actions cannot set annotations.destructiveHint=true"
19
+ );
20
+ }
21
+ if (effect === "mutate" && resolved.readOnlyHint) {
22
+ throw new Error(
23
+ "[ryu-sdk] mutate actions cannot set annotations.readOnlyHint=true"
24
+ );
25
+ }
26
+ return resolved;
27
+ }
28
+ function actionManifestEntry(action) {
29
+ const options = {
30
+ action: true,
31
+ annotations: action.annotations,
32
+ description: action.description,
33
+ needsApproval: action.needsApproval,
34
+ ...action.outputSchema ? { outputSchema: action.outputSchema } : {}
35
+ };
36
+ return inlineToolRunnable(action, options);
37
+ }
38
+ function actionToManifest(action, options) {
39
+ const grants = [.../* @__PURE__ */ new Set([...options.grants ?? [], "tool:execute"])];
40
+ const raw = {
41
+ id: options.id,
42
+ name: options.name ?? action.name,
43
+ version: options.version,
44
+ runnables: [actionManifestEntry(action)],
45
+ permission_grants: grants,
46
+ activation_events: [...options.activationEvents ?? ["*"]],
47
+ targets: [...options.targets ?? []]
48
+ };
49
+ const result = PluginManifestSchema.safeParse(raw);
50
+ if (!result.success) {
51
+ const first = result.error.issues[0];
52
+ const field = first?.path.join(".") ?? "unknown";
53
+ const message = first?.message ?? "validation failed";
54
+ throw new Error(
55
+ `[ryu-sdk] action manifest validation failed at '${field}': ${message}`
56
+ );
57
+ }
58
+ return result.data;
59
+ }
60
+ function defineAction(options) {
61
+ const annotations = deriveAnnotations(options.effect, options.annotations);
62
+ const tool = defineTool({
63
+ description: options.description,
64
+ id: options.id,
65
+ name: options.name,
66
+ run: options.run,
67
+ schema: options.schema
68
+ });
69
+ const action = {
70
+ ...tool,
71
+ action: true,
72
+ annotations,
73
+ description: options.description,
74
+ effect: options.effect,
75
+ needsApproval: options.needsApproval ?? false,
76
+ ...options.outputSchema ? { outputSchema: options.outputSchema } : {},
77
+ toManifest(manifestOptions) {
78
+ return actionToManifest(action, manifestOptions);
79
+ },
80
+ toMcpTool(context) {
81
+ return {
82
+ name: action.id,
83
+ description: action.description,
84
+ inputSchema: action.schema,
85
+ run: (input) => action.run(input, context)
86
+ };
87
+ }
88
+ };
89
+ return action;
90
+ }
91
+
92
+ export {
93
+ defineAction
94
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PluginManifestSchema
3
- } from "./chunk-IEUQ3CDG.js";
3
+ } from "./chunk-NZKVOSC2.js";
4
4
 
5
5
  // src/runnable/agent.ts
6
6
  var CAPABILITY_SLOTS = ["rag", "memory", "tts", "stt"];
@@ -146,80 +146,6 @@ function defineSkill(options) {
146
146
  };
147
147
  }
148
148
 
149
- // src/runnable/tool.ts
150
- function validateInput(input, schema, toolId) {
151
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
152
- throw new Error(
153
- `[ryu-sdk] Tool "${toolId}" input must be an object, got ${JSON.stringify(input)}`
154
- );
155
- }
156
- const record = input;
157
- for (const requiredKey of schema.required ?? []) {
158
- if (!(requiredKey in record)) {
159
- throw new Error(
160
- `[ryu-sdk] Tool "${toolId}" input missing required field "${requiredKey}"`
161
- );
162
- }
163
- }
164
- for (const [key, prop] of Object.entries(schema.properties)) {
165
- if (!(key in record)) {
166
- continue;
167
- }
168
- const value = record[key];
169
- if (!checkType(value, prop.type)) {
170
- throw new Error(
171
- `[ryu-sdk] Tool "${toolId}" input field "${key}" expected type "${prop.type}", got ${JSON.stringify(value)}`
172
- );
173
- }
174
- }
175
- }
176
- function checkType(value, type) {
177
- switch (type) {
178
- case "string":
179
- return typeof value === "string";
180
- case "number":
181
- case "integer":
182
- return typeof value === "number";
183
- case "boolean":
184
- return typeof value === "boolean";
185
- case "array":
186
- return Array.isArray(value);
187
- case "object":
188
- return typeof value === "object" && value !== null && !Array.isArray(value);
189
- default:
190
- return false;
191
- }
192
- }
193
- function defineTool(options) {
194
- const { id, name, schema, run } = options;
195
- const code = `return await (${run.toString()})(input, host);`;
196
- return {
197
- id,
198
- name,
199
- kind: "tool",
200
- schema,
201
- code,
202
- run(input, ctx) {
203
- validateInput(input, schema, id);
204
- return run(input, ctx);
205
- }
206
- };
207
- }
208
- function inlineToolRunnable(tool, options) {
209
- return {
210
- id: tool.id,
211
- name: tool.name,
212
- kind: "tool",
213
- config: {
214
- slug: tool.id,
215
- backend: "inline_deno",
216
- code: tool.code,
217
- input_schema: tool.schema,
218
- ...options?.description ? { description: options.description } : {}
219
- }
220
- };
221
- }
222
-
223
149
  // src/runnable/workflow.ts
224
150
  function defineWorkflow(options) {
225
151
  const { id, name, run } = options;
@@ -234,7 +160,5 @@ function defineWorkflow(options) {
234
160
  export {
235
161
  defineAgent,
236
162
  defineSkill,
237
- defineTool,
238
- inlineToolRunnable,
239
163
  defineWorkflow
240
164
  };
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  defineApp
3
- } from "./chunk-W3KPP4WN.js";
3
+ } from "./chunk-SN2QBJUF.js";
4
4
  import {
5
5
  PluginManifestSchema,
6
6
  RunnableMetaSchema
7
- } from "./chunk-IEUQ3CDG.js";
7
+ } from "./chunk-NZKVOSC2.js";
8
8
 
9
9
  // src/builder.ts
10
10
  var RunnableBuilder = class {
@@ -218,6 +218,7 @@ var AppBuilder = class {
218
218
  _dependencies = [];
219
219
  _requiredCapabilities = [];
220
220
  _requiredGrants = [];
221
+ _slashCommands = [];
221
222
  _targets = [];
222
223
  /** Set the reverse-domain app id (e.g. `"com.example.checklist"`). */
223
224
  id(value) {
@@ -274,6 +275,11 @@ var AppBuilder = class {
274
275
  this._tools.push(spec);
275
276
  return this;
276
277
  }
278
+ /** Add a slash command and its optional sequential argument choices. */
279
+ slashCommand(command) {
280
+ this._slashCommands.push(command);
281
+ return this;
282
+ }
277
283
  /**
278
284
  * Declare a **plugin-to-plugin dependency** (auto-enabled, in dependency order,
279
285
  * before this app). `minVersion` is a MINIMUM (`"1.2.0"` = `">=1.2.0"`).
@@ -321,6 +327,7 @@ var AppBuilder = class {
321
327
  uiEntry: this._uiEntry,
322
328
  tools: this._tools,
323
329
  grants: this._grants,
330
+ slashCommands: this._slashCommands,
324
331
  ...this._server ? { server: this._server } : {},
325
332
  ...this._displayMode ? { displayMode: this._displayMode } : {},
326
333
  ...this._mime ? { mime: this._mime } : {},
@@ -162,10 +162,18 @@ var ChatWidgetTemplateSchema = z.object({
162
162
  }).superRefine((value, ctx) => {
163
163
  const count = Number(Boolean(value.backing.tool_id)) + Number(Boolean(value.backing.view_id));
164
164
  if (count !== 1 && value.availability === "available") {
165
- ctx.addIssue({ code: "custom", path: ["backing"], message: "available templates need exactly one backing tool_id or view_id" });
165
+ ctx.addIssue({
166
+ code: "custom",
167
+ path: ["backing"],
168
+ message: "available templates need exactly one backing tool_id or view_id"
169
+ });
166
170
  }
167
171
  if (count > 1) {
168
- ctx.addIssue({ code: "custom", path: ["backing"], message: "backing must declare at most one of tool_id or view_id" });
172
+ ctx.addIssue({
173
+ code: "custom",
174
+ path: ["backing"],
175
+ message: "backing must declare at most one of tool_id or view_id"
176
+ });
169
177
  }
170
178
  });
171
179
  var ToolAppConfigSchema = z.object({
@@ -188,6 +196,33 @@ var ToolAppConfigSchema = z.object({
188
196
  /** Optional status label shown when the render tool finishes. */
189
197
  invoked: z.string().optional()
190
198
  });
199
+ var SlashCommandOptionSchema = z.object({
200
+ description: z.string().optional(),
201
+ label: z.string().min(1),
202
+ value: z.string().min(1)
203
+ }).passthrough();
204
+ var SlashCommandCustomOptionSchema = z.union([
205
+ z.literal(true),
206
+ z.object({
207
+ description: z.string().optional(),
208
+ label: z.string().min(1).optional()
209
+ }).passthrough()
210
+ ]);
211
+ var SlashCommandArgumentSchema = z.object({
212
+ allow_custom: z.boolean().optional(),
213
+ custom: SlashCommandCustomOptionSchema.optional(),
214
+ description: z.string().optional(),
215
+ name: z.string().min(1),
216
+ options: z.array(SlashCommandOptionSchema).optional()
217
+ }).passthrough();
218
+ var SlashCommandContributionSchema = z.object({
219
+ args: z.array(SlashCommandArgumentSchema).optional(),
220
+ body: z.string().optional(),
221
+ command: z.string().min(1),
222
+ description: z.string().optional(),
223
+ id: z.string().optional(),
224
+ parameters: z.array(SlashCommandArgumentSchema).optional()
225
+ }).passthrough();
191
226
  var ContributesSchema = z.object({
192
227
  turn_hooks: z.array(TurnHookContributionSchema).default([]),
193
228
  /** App events this plugin EMITS — the provider half of the hook system, whose
@@ -202,7 +237,7 @@ var ContributesSchema = z.object({
202
237
  * feature declaration before signing. */
203
238
  chat_features: z.array(z.record(z.string(), z.unknown())).default([]),
204
239
  settings_tabs: z.array(z.record(z.string(), z.unknown())).default([]),
205
- slash_commands: z.array(z.record(z.string(), z.unknown())).default([]),
240
+ slash_commands: z.array(SlashCommandContributionSchema).default([]),
206
241
  /** App widgets (Ryu Apps). Each binds a render tool id to its
207
242
  * `ui://widget/<slug>.html` template. Mirrors the Rust-side
208
243
  * `Contributes.widgets` field, without which the CLI's zod parse would strip
@@ -269,7 +304,10 @@ var ContributesSchema = z.object({
269
304
  output_styles: z.array(OutputStyleContributionSchema).default([]),
270
305
  /** Per-message actions contributed by an enabled plugin. Kept as loose records
271
306
  * so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
272
- message_actions: z.array(z.record(z.string(), z.unknown())).default([])
307
+ message_actions: z.array(z.record(z.string(), z.unknown())).default([]),
308
+ /** Buttons contributed to the floating text-selection toolbar. Kept as loose
309
+ * records so host-owned dispatch args survive `ryu pack` unchanged. */
310
+ selection_actions: z.array(z.record(z.string(), z.unknown())).default([])
273
311
  });
274
312
  var SetupStepSchema = z.object({
275
313
  /** Card heading (e.g. the companion app name). */
@@ -419,6 +457,9 @@ var PluginManifestSchema = z.object({
419
457
  /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
420
458
  "version must be a valid semver string (e.g. 1.0.0)"
421
459
  ),
460
+ /** Core-owned release maturity metadata. The Rust contract validates the
461
+ * richer shape; the SDK authoring parser must preserve it for pack/publish. */
462
+ stability: z.unknown().optional(),
422
463
  /**
423
464
  * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
424
465
  * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
@@ -436,8 +477,26 @@ var PluginManifestSchema = z.object({
436
477
  * Declarations only — grant enforcement is the Gateway's responsibility.
437
478
  */
438
479
  permission_grants: z.array(z.string()).default([]),
480
+ /** Deny-by-default sandbox permissions. Core remains authoritative; this
481
+ * schema mirrors the fields so `ryu pack` cannot strip them. */
482
+ permissions: z.object({
483
+ fs: z.object({
484
+ read: z.array(z.string()).default([]),
485
+ write: z.array(z.string()).default([])
486
+ }).optional(),
487
+ child_process: z.boolean().optional(),
488
+ run: z.array(z.string()).default([]),
489
+ network: z.union([z.boolean(), z.array(z.string())]).optional(),
490
+ tool: z.array(z.string()).default([])
491
+ }).optional(),
492
+ /** Core-owned permission presentation levels. Preserved verbatim here and
493
+ * validated by Core's authoritative manifest contract. */
494
+ permission_levels: z.unknown().optional(),
439
495
  /** Remote or stdio MCP servers registered by this plugin. */
440
496
  mcp_servers: z.record(z.string(), McpServerDeclSchema).optional(),
497
+ /** Core-owned sidecar declarations. Their full process/HTTP schema stays in
498
+ * the Rust contract; this authoring layer must never strip them. */
499
+ sidecars: z.unknown().optional(),
441
500
  /**
442
501
  * Optional Companion surface (an in-desktop overlay or sidebar panel).
443
502
  * Absent when the plugin has no Companion surface.
@@ -473,6 +532,9 @@ var PluginManifestSchema = z.object({
473
532
  * an unsupported-target plugin stays installable and inspectable.
474
533
  */
475
534
  targets: z.array(SurfaceSchema).default([]),
535
+ /** Rich per-surface declarations (`support`, UI, contributed commands, …).
536
+ * Core owns and validates the nested vocabulary. */
537
+ surfaces: z.unknown().optional(),
476
538
  /**
477
539
  * Host version floors — the semver requirement each surface must satisfy for
478
540
  * this plugin to install. Mirrors Core's `EnginesReq`
@@ -587,7 +649,7 @@ var PluginManifestSchema = z.object({
587
649
  * array of steps guiding the user through post-install configuration.
588
650
  */
589
651
  setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional()
590
- }).superRefine((manifest, context) => {
652
+ }).passthrough().superRefine((manifest, context) => {
591
653
  const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
592
654
  (server) => server.auth?.type === "oauth"
593
655
  );
@@ -626,6 +688,10 @@ export {
626
688
  WidgetContributionSchema,
627
689
  ChatWidgetTemplateSchema,
628
690
  ToolAppConfigSchema,
691
+ SlashCommandOptionSchema,
692
+ SlashCommandCustomOptionSchema,
693
+ SlashCommandArgumentSchema,
694
+ SlashCommandContributionSchema,
629
695
  ContributesSchema,
630
696
  SetupStepSchema,
631
697
  AppDependencySchema,
@@ -0,0 +1,83 @@
1
+ // src/runnable/tool.ts
2
+ function validateInput(input, schema, toolId) {
3
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
4
+ throw new Error(
5
+ `[ryu-sdk] Tool "${toolId}" input must be an object, got ${JSON.stringify(input)}`
6
+ );
7
+ }
8
+ const record = input;
9
+ for (const requiredKey of schema.required ?? []) {
10
+ if (!(requiredKey in record)) {
11
+ throw new Error(
12
+ `[ryu-sdk] Tool "${toolId}" input missing required field "${requiredKey}"`
13
+ );
14
+ }
15
+ }
16
+ for (const [key, prop] of Object.entries(schema.properties)) {
17
+ if (!(key in record)) {
18
+ continue;
19
+ }
20
+ const value = record[key];
21
+ if (!checkType(value, prop.type)) {
22
+ throw new Error(
23
+ `[ryu-sdk] Tool "${toolId}" input field "${key}" expected type "${prop.type}", got ${JSON.stringify(value)}`
24
+ );
25
+ }
26
+ }
27
+ }
28
+ function checkType(value, type) {
29
+ switch (type) {
30
+ case "string":
31
+ return typeof value === "string";
32
+ case "number":
33
+ case "integer":
34
+ return typeof value === "number";
35
+ case "boolean":
36
+ return typeof value === "boolean";
37
+ case "array":
38
+ return Array.isArray(value);
39
+ case "object":
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ default:
42
+ return false;
43
+ }
44
+ }
45
+ function defineTool(options) {
46
+ const { description, id, name, schema, run } = options;
47
+ const code = `return await (${run.toString()})(input, host);`;
48
+ return {
49
+ id,
50
+ name,
51
+ kind: "tool",
52
+ schema,
53
+ ...description === void 0 ? {} : { description },
54
+ code,
55
+ run(input, ctx) {
56
+ validateInput(input, schema, id);
57
+ return run(input, ctx);
58
+ }
59
+ };
60
+ }
61
+ function inlineToolRunnable(tool, options) {
62
+ return {
63
+ id: tool.id,
64
+ name: tool.name,
65
+ kind: "tool",
66
+ config: {
67
+ slug: tool.id,
68
+ backend: "inline_deno",
69
+ code: tool.code,
70
+ input_schema: tool.schema,
71
+ ...options?.description === void 0 ? tool.description === void 0 ? {} : { description: tool.description } : { description: options.description },
72
+ ...options?.outputSchema ? { output_schema: options.outputSchema } : {},
73
+ ...options?.annotations ? { annotations: options.annotations } : {},
74
+ ...options?.action === void 0 ? {} : { action: options.action },
75
+ ...options?.needsApproval === void 0 ? {} : { needs_approval: options.needsApproval }
76
+ }
77
+ };
78
+ }
79
+
80
+ export {
81
+ defineTool,
82
+ inlineToolRunnable
83
+ };
@@ -1,6 +1,9 @@
1
+ import {
2
+ inlineToolRunnable
3
+ } from "./chunk-QYFUNJOH.js";
1
4
  import {
2
5
  PluginManifestSchema
3
- } from "./chunk-IEUQ3CDG.js";
6
+ } from "./chunk-NZKVOSC2.js";
4
7
 
5
8
  // src/runnable/app.ts
6
9
  var DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
@@ -13,6 +16,13 @@ function withWidgetRenderGrant(grants, widgets) {
13
16
  }
14
17
  return out;
15
18
  }
19
+ function withToolExecuteGrant(grants, tools) {
20
+ const out = [...grants];
21
+ if (tools.some((tool) => tool.runnable !== void 0) && !out.includes("tool:execute")) {
22
+ out.push("tool:execute");
23
+ }
24
+ return out;
25
+ }
16
26
  function appToolId(server, name) {
17
27
  return `${server}.${name}`;
18
28
  }
@@ -27,7 +37,9 @@ function defineApp(options) {
27
37
  for (const spec of options.tools) {
28
38
  const isRender = spec.accessible !== true;
29
39
  const id = appToolId(server, spec.name);
40
+ const executable = spec.runnable ? inlineToolRunnable(spec.runnable) : void 0;
30
41
  const config = {
42
+ ...executable?.config ?? {},
31
43
  slug: id,
32
44
  description: spec.description,
33
45
  widget: isRender,
@@ -61,7 +73,7 @@ function defineApp(options) {
61
73
  hook_events: [],
62
74
  composer_controls: [],
63
75
  settings_tabs: [],
64
- slash_commands: [],
76
+ slash_commands: options.slashCommands ?? [],
65
77
  sidebar_sections: [],
66
78
  sidebar_buttons: [],
67
79
  dock_panels: [],
@@ -79,6 +91,7 @@ function defineApp(options) {
79
91
  pi_extensions: [],
80
92
  output_styles: [],
81
93
  message_actions: [],
94
+ selection_actions: [],
82
95
  widgets
83
96
  };
84
97
  const raw = {
@@ -92,15 +105,15 @@ function defineApp(options) {
92
105
  // Core gates widget promotion on declared-AND-enabled-AND-granted, and a
93
106
  // missing grant fails as `DeniedNoGrant` — which is an `info!` log and
94
107
  // nothing else. The widget silently renders as plain text, with no error
95
- // in the UI and nothing pointing at the manifest. Every app scaffolded
96
- // through `defineApp` hit that, because the only fix was a grant string
97
- // the templates never mention and the builder never added; the one
98
- // working example on disk hand-writes it.
108
+ // in the UI and nothing pointing at the manifest.
99
109
  //
100
110
  // Added only when there is a widget to render, and unioned rather than
101
111
  // overwritten so an author's own `grants` list survives and re-declaring
102
112
  // it is not an error.
103
- permission_grants: withWidgetRenderGrant(options.grants ?? [], widgets),
113
+ permission_grants: withToolExecuteGrant(
114
+ withWidgetRenderGrant(options.grants ?? [], widgets),
115
+ options.tools
116
+ ),
104
117
  activation_events: options.activationEvents ?? ["*"],
105
118
  contributes,
106
119
  // `targets: []` means EVERY surface, so an app that declares none is
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createPrimitives,
3
3
  httpPrimitiveTransport
4
- } from "./chunk-A3RGEPDG.js";
4
+ } from "./chunk-ZTJWBRUL.js";
5
5
  import {
6
6
  defineModel
7
7
  } from "./chunk-HACAGK65.js";
@@ -154,6 +154,31 @@ var PRIMITIVE_BINDINGS = {
154
154
  method: "background.stop",
155
155
  grant: "background:control"
156
156
  },
157
+ "storage.get": {
158
+ transport: "bridge",
159
+ method: "storage.get",
160
+ grant: "storage:kv"
161
+ },
162
+ "storage.set": {
163
+ transport: "bridge",
164
+ method: "storage.set",
165
+ grant: "storage:kv"
166
+ },
167
+ "storage.delete": {
168
+ transport: "bridge",
169
+ method: "storage.delete",
170
+ grant: "storage:kv"
171
+ },
172
+ "storage.keys": {
173
+ transport: "bridge",
174
+ method: "storage.keys",
175
+ grant: "storage:kv"
176
+ },
177
+ "storage.compareAndSet": {
178
+ transport: "bridge",
179
+ method: "storage.compareAndSet",
180
+ grant: "storage:kv"
181
+ },
157
182
  // Host-direct media data-path (the host holds the node token; returns data: URLs).
158
183
  "image.generate": {
159
184
  transport: "direct",
@@ -196,6 +221,13 @@ function createPrimitives(transport) {
196
221
  process_id: input.processId
197
222
  })
198
223
  },
224
+ storage: {
225
+ get: (input) => transport.bridge("storage.get", input),
226
+ set: (input) => transport.bridge("storage.set", input).then(() => void 0),
227
+ delete: (input) => transport.bridge("storage.delete", input).then(() => void 0),
228
+ keys: (input) => transport.bridge("storage.keys", input ?? {}).then((value) => Array.isArray(value) ? value : []),
229
+ compareAndSet: (input) => transport.bridge("storage.compareAndSet", input)
230
+ },
199
231
  rag: {
200
232
  retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
201
233
  embed: (input) => brokerCall(transport, "rag", "embed", input),