@inhuman.tools/plugin-sdk 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Load-boundary normalize for plugin ABI v1. Produces plain records; branding
3
+ * and instanceof are not part of the contract.
4
+ */
5
+ import type { EnvCapability } from "@here.build/arrival";
6
+ import type { SystemPrompt } from "@inhuman.tools/harness-commons";
7
+ import { z, ZodType } from "zod";
8
+
9
+ import type { ExamAdmit, PluginProjection, PluginStorageHandles } from "./projection.js";
10
+ import {
11
+ botCallToolDef,
12
+ botDef,
13
+ exclusiveSlotsDef,
14
+ functionToolDef,
15
+ inhumanBrand,
16
+ inhumanShape,
17
+ mcpServerDef,
18
+ pluginProjectionDef,
19
+ REASONING_EFFORTS,
20
+ type ToolRepresentation,
21
+ NormalizedActionDefinition,
22
+ } from "./schemas/plugin.js";
23
+
24
+ function parseNamed<T>(schema: z.ZodType<T>, value: unknown, context: string): T {
25
+ const result = schema.safeParse(value);
26
+ if (result.success) return result.data;
27
+ const issue = result.error.issues[0];
28
+ throw new Error(`${context}: ${issue?.message ?? "invalid"}`);
29
+ }
30
+
31
+ function declaredRepresentation(value: unknown, fallback: ToolRepresentation): ToolRepresentation {
32
+ return value === "tool" || value === "symbol" || value === "both" ? value : fallback;
33
+ }
34
+
35
+ export type PluginCallContext = {
36
+ readonly root?: string;
37
+ readonly turn?: unknown;
38
+ readonly completion?: unknown | null;
39
+ readonly invocation?: unknown;
40
+ readonly signal?: AbortSignal;
41
+ /** Prefix projection for this plugin. Tool body proposes metadata via `propose`. */
42
+ readonly storage?: PluginStorageHandles;
43
+ readonly [key: string]: unknown;
44
+ };
45
+
46
+ export type BotMcpBinding = {
47
+ readonly server: string;
48
+ readonly tools: readonly string[];
49
+ readonly approval?: "allow" | "policy";
50
+ };
51
+
52
+ export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
53
+
54
+ export function isReasoningEffort(value: unknown): value is ReasoningEffort {
55
+ return z.enum(REASONING_EFFORTS).safeParse(value).success;
56
+ }
57
+
58
+ export type BotDefinition = {
59
+ readonly name: string;
60
+ readonly recipeScope: "invocation" | "session" | "workspace" | "global";
61
+ readonly instructions: SystemPrompt;
62
+ /** Parent-facing one-liner. Inventory speech. */
63
+ readonly description?: string;
64
+ readonly tools?: readonly string[];
65
+ readonly mcp?: readonly BotMcpBinding[];
66
+ readonly keepAlive?: boolean;
67
+ /** Other bot names this concept may invoke. Same-plugin, resolved at load. */
68
+ readonly invokes?: readonly string[];
69
+ readonly reasoningEffort?: ReasoningEffort;
70
+ /** Ordered try-list of catalog model ids. Applied at mint when a resolver exists. */
71
+ readonly modelPreferences?: readonly string[];
72
+ readonly skills?: readonly string[];
73
+ };
74
+
75
+ export type McpServerDefinition = {
76
+ readonly name: string;
77
+ readonly visibility: "private" | "public";
78
+ readonly scope: "session" | "workspace" | "global";
79
+ readonly transport: {
80
+ readonly type: "stdio";
81
+ readonly command: string;
82
+ readonly args: readonly string[];
83
+ readonly cwd: "plugin" | "workspace";
84
+ readonly env?:
85
+ | Record<string, string>
86
+ | ((ctx: { readonly pluginRoot: string; readonly workspaceRoot: string }) => Record<string, string>);
87
+ };
88
+ };
89
+
90
+ export type FunctionToolDefinition<I = unknown, O = unknown> = {
91
+ readonly type: "function";
92
+ readonly name: string;
93
+ readonly description?: string;
94
+ readonly class?: string;
95
+ readonly representation: ToolRepresentation;
96
+ readonly input: ZodType<I>;
97
+ readonly output: ZodType<O>;
98
+ readonly run: (input: I, ctx: PluginCallContext) => O | Promise<O>;
99
+ readonly triggersReactLoop: boolean;
100
+ readonly isSideChannel: boolean;
101
+ readonly tapeHidden: boolean;
102
+ readonly question: string | null;
103
+ readonly effect: string | null;
104
+ readonly caption: string | null;
105
+ readonly exam?: ExamAdmit<I, unknown, unknown>;
106
+ };
107
+
108
+ export type BotCallToolDefinition<I = unknown, O = unknown> = {
109
+ readonly type: "bot-call";
110
+ readonly name: string;
111
+ readonly description?: string;
112
+ readonly class?: string;
113
+ readonly representation: ToolRepresentation;
114
+ readonly input: ZodType<I>;
115
+ readonly output: ZodType<O>;
116
+ readonly target: string;
117
+ readonly prepare?: (input: I, ctx: PluginCallContext) => unknown;
118
+ readonly background?: boolean;
119
+ readonly lifetime?: "turn" | "call";
120
+ readonly show?: "result" | "omit" | "turn";
121
+ readonly deadlineMs?: number;
122
+ readonly maxResultTokens?: number;
123
+ readonly supportsNotify?: boolean;
124
+ readonly triggersReactLoop: boolean;
125
+ readonly isSideChannel: boolean;
126
+ readonly tapeHidden: boolean;
127
+ readonly question: string | null;
128
+ readonly effect: string | null;
129
+ readonly caption: string | null;
130
+ };
131
+
132
+ export type ToolDefinition<I = unknown, O = unknown> = FunctionToolDefinition<I, O> | BotCallToolDefinition<I, O>;
133
+
134
+ export type NormalizedPluginDefinition = {
135
+ readonly apiVersion: 1;
136
+ readonly tools: readonly ToolDefinition[];
137
+ readonly bots: readonly BotDefinition[];
138
+ readonly mcpServers: readonly McpServerDefinition[];
139
+ readonly actions: readonly NormalizedActionDefinition[];
140
+ readonly envCapabilities: readonly EnvCapability[];
141
+ readonly exclusiveSlots: readonly string[];
142
+ readonly projection?: PluginProjection<any, any>;
143
+ };
144
+
145
+ function toolRecord(value: unknown): Record<string, unknown> | null {
146
+ if (value == null || typeof value !== "object") return null;
147
+ if ((value as { [inhumanBrand.tool]?: boolean })[inhumanBrand.tool] === true) {
148
+ const tool = value as { toToolDefinition: () => ToolDefinition };
149
+ return tool.toToolDefinition() as unknown as Record<string, unknown>;
150
+ }
151
+ return value as Record<string, unknown>;
152
+ }
153
+
154
+ function actionRecord(value: unknown): Record<string, unknown> | null {
155
+ if (value == null || typeof value !== "object") return null;
156
+ if ((value as { [inhumanBrand.action]?: boolean })[inhumanBrand.action] === true) {
157
+ const action = value as NormalizedActionDefinition;
158
+ return {
159
+ name: action.name,
160
+ description: action.description,
161
+ seat: action.seat,
162
+ input: action.input,
163
+ run: action.run,
164
+ };
165
+ }
166
+ return value as Record<string, unknown>;
167
+ }
168
+
169
+ function pluginBag(value: unknown): Record<string, unknown> | null {
170
+ if (value == null || typeof value !== "object") return null;
171
+ const record = value as Record<string, unknown> & { default?: unknown };
172
+ if ((value as { [inhumanBrand.plugin]?: boolean })[inhumanBrand.plugin] === true) {
173
+ const plugin = value as {
174
+ tools?: readonly unknown[];
175
+ actions?: readonly unknown[];
176
+ envCapabilities?: readonly EnvCapability[];
177
+ bots?: readonly unknown[];
178
+ mcpServers?: readonly unknown[];
179
+ exclusiveSlots?: readonly string[];
180
+ projection?: unknown;
181
+ apiVersion?: unknown;
182
+ };
183
+ return {
184
+ apiVersion: plugin.apiVersion,
185
+ tools: plugin.tools,
186
+ actions: plugin.actions,
187
+ envCapabilities: plugin.envCapabilities,
188
+ bots: plugin.bots,
189
+ mcpServers: plugin.mcpServers,
190
+ exclusiveSlots: plugin.exclusiveSlots,
191
+ projection: plugin.projection,
192
+ };
193
+ }
194
+ if ("default" in record && record.default != null) return pluginBag(record.default);
195
+ return record;
196
+ }
197
+
198
+ export function validateSupportedVersion(bag: Record<string, unknown>, pluginName = ""): void {
199
+ const version = bag.apiVersion;
200
+ if (version === undefined) return;
201
+ if (version === 1) return;
202
+ throw new Error(`${pluginName}: unsupported plugin apiVersion ${String(version)}`);
203
+ }
204
+
205
+ function normalizeTool(raw: unknown, index: number, pluginName: string): ToolDefinition {
206
+ const row = toolRecord(raw);
207
+ if (row == null) throw new Error(`${pluginName}: tools[${index}] is not an object`);
208
+ if (row.type === "bot-message") {
209
+ throw new Error(`${pluginName}: tools[${index}] type bot-message is reserved in v1`);
210
+ }
211
+ const context = `${pluginName}: tools[${index}]`;
212
+ const parsed =
213
+ row.type === "bot-call"
214
+ ? parseNamed(botCallToolDef, row, context)
215
+ : parseNamed(functionToolDef, row.type === "function" ? row : { ...row, type: undefined }, context);
216
+ const base = {
217
+ name: parsed.name,
218
+ description: parsed.description,
219
+ class: parsed.class,
220
+ representation: declaredRepresentation(parsed.representation, parsed.type === "bot-call" ? "tool" : "symbol"),
221
+ input: parsed.input as ZodType,
222
+ output: parsed.output as ZodType,
223
+ triggersReactLoop: parsed.triggersReactLoop !== false,
224
+ isSideChannel: parsed.isSideChannel === true,
225
+ tapeHidden: parsed.tapeHidden === true,
226
+ question: parsed.question,
227
+ effect: parsed.effect,
228
+ caption: parsed.caption,
229
+ };
230
+ if (parsed.type === "bot-call") {
231
+ return {
232
+ type: "bot-call",
233
+ ...base,
234
+ target: parsed.target,
235
+ prepare: parsed.prepare as BotCallToolDefinition["prepare"],
236
+ background: parsed.background === true,
237
+ lifetime: parsed.lifetime,
238
+ show: parsed.show,
239
+ deadlineMs: parsed.deadlineMs,
240
+ maxResultTokens: parsed.maxResultTokens,
241
+ supportsNotify: parsed.supportsNotify === true,
242
+ };
243
+ }
244
+ const exam = parsed.exam as FunctionToolDefinition["exam"];
245
+ if (exam != null && parsed.run == null) {
246
+ return {
247
+ type: "function",
248
+ ...base,
249
+ exam,
250
+ run: () => {
251
+ throw new TypeError(`${pluginName}: tools[${index}] exam tools do not execute through run`);
252
+ },
253
+ };
254
+ }
255
+ return {
256
+ type: "function",
257
+ ...base,
258
+ exam,
259
+ run: parsed.run as FunctionToolDefinition["run"],
260
+ };
261
+ }
262
+
263
+ function normalizeProjection(raw: unknown, pluginName: string): PluginProjection | undefined {
264
+ if (raw == null) return undefined;
265
+ return parseNamed(pluginProjectionDef, raw, `${pluginName}: projection`) as PluginProjection;
266
+ }
267
+
268
+ function parseExclusiveSlots(pluginName: string, bag: Record<string, unknown>): readonly string[] {
269
+ const raw = Array.isArray(bag.exclusiveSlots) ? bag.exclusiveSlots : [];
270
+ return parseNamed(exclusiveSlotsDef, raw, `${pluginName}: exclusiveSlots`);
271
+ }
272
+
273
+ export function normalizeDefinitions(exported: unknown, pluginName = ""): NormalizedPluginDefinition {
274
+ const bag = pluginBag(exported);
275
+ if (bag == null) throw new Error(`${pluginName}: plugin export is not an object`);
276
+ validateSupportedVersion(bag, pluginName);
277
+ const toolsRaw = Array.isArray(bag.tools) ? bag.tools : [];
278
+ const actionsRaw = Array.isArray(bag.actions) ? bag.actions : [];
279
+ const envRaw = Array.isArray(bag.envCapabilities) ? bag.envCapabilities : [];
280
+ const botsRaw = Array.isArray(bag.bots) ? bag.bots : [];
281
+ const serversRaw = Array.isArray(bag.mcpServers) ? bag.mcpServers : [];
282
+ const tools = toolsRaw.map((tool, index) => normalizeTool(tool, index, pluginName));
283
+ const toolNames = new Set<string>();
284
+ for (const tool of tools) {
285
+ if (toolNames.has(tool.name)) throw new Error(`${pluginName}: tool "${tool.name}" collides`);
286
+ toolNames.add(tool.name);
287
+ }
288
+ const actions = actionsRaw.map((action, index) => {
289
+ const row = actionRecord(action);
290
+ if (row == null) throw new Error(`${pluginName}: actions[${index}] is not an object`);
291
+ return parseNamed(inhumanShape.normalizedActionDef, row, `${pluginName}: actions[${index}]`);
292
+ });
293
+ const actionNames = new Set<string>();
294
+ for (const action of actions) {
295
+ if (actionNames.has(action.name)) throw new Error(`${pluginName}: action "${action.name}" collides`);
296
+ actionNames.add(action.name);
297
+ }
298
+ const envCapabilities = envRaw.map((cap, index) => {
299
+ try {
300
+ return inhumanShape.envCapability.parse(cap);
301
+ } catch {
302
+ throw new Error(`${pluginName}: envCapabilities[${index}] is invalid`);
303
+ }
304
+ });
305
+ const bots = botsRaw.map((bot, index) => parseNamed(botDef, bot, `${pluginName}: bots[${index}]`));
306
+ const botNames = new Set<string>();
307
+ for (const bot of bots) {
308
+ if (botNames.has(bot.name)) throw new Error(`${pluginName}: bot "${bot.name}" collides`);
309
+ botNames.add(bot.name);
310
+ }
311
+ const mcpServers = serversRaw.map((server, index) =>
312
+ parseNamed(mcpServerDef, server, `${pluginName}: mcpServers[${index}]`),
313
+ );
314
+ const serverNames = new Set<string>();
315
+ for (const server of mcpServers) {
316
+ if (serverNames.has(server.name)) throw new Error(`${pluginName}: mcpServer "${server.name}" collides`);
317
+ serverNames.add(server.name);
318
+ }
319
+ const projection = normalizeProjection(bag.projection, pluginName);
320
+ return {
321
+ apiVersion: 1,
322
+ tools,
323
+ bots,
324
+ mcpServers,
325
+ actions,
326
+ envCapabilities,
327
+ exclusiveSlots: parseExclusiveSlots(pluginName, bag),
328
+ projection,
329
+ };
330
+ }
331
+
332
+ export function resolveReferences(definition: NormalizedPluginDefinition, pluginName = ""): NormalizedPluginDefinition {
333
+ const botNames = new Set(definition.bots.map((bot) => bot.name));
334
+ const serverNames = new Set(definition.mcpServers.map((server) => server.name));
335
+ for (const tool of definition.tools) {
336
+ if (tool.type === "bot-call" && !botNames.has(tool.target)) {
337
+ throw new Error(`${pluginName}: tool "${tool.name}" target "${tool.target}" is unresolved`);
338
+ }
339
+ }
340
+ for (const bot of definition.bots) {
341
+ for (const binding of bot.mcp ?? []) {
342
+ if (!serverNames.has(binding.server)) {
343
+ throw new Error(`${pluginName}: bot "${bot.name}" binding server "${binding.server}" is unresolved`);
344
+ }
345
+ }
346
+ for (const target of bot.invokes ?? []) {
347
+ if (!botNames.has(target)) {
348
+ throw new Error(`${pluginName}: bot "${bot.name}" invokes unresolved "${target}"`);
349
+ }
350
+ }
351
+ }
352
+ return definition;
353
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Trajectory projection ABI. The host runs each pack `reduce` over plugin
3
+ * metadata on the session graph. Hooks read the head projection and stamp the wire.
4
+ * This package does not import the session document.
5
+ */
6
+
7
+ export type PluginProjection<S = unknown, A = unknown> = {
8
+ readonly initial: () => S;
9
+ readonly reduce: (state: S, action: A) => S;
10
+ /** Model-proposed metadata that may only be written sealed by the host. */
11
+ readonly hostOnly?: (action: A) => boolean;
12
+ };
13
+
14
+ /** Prefix projection at this invocation. `propose` is the action, not the next state. */
15
+ export type PluginStorageHandles<S = unknown, A = unknown> = {
16
+ readonly session: S;
17
+ readonly invocation: S;
18
+ readonly project?: S;
19
+ readonly global?: S;
20
+ propose(action: A): void;
21
+ };
22
+
23
+ /** Host-computed exam result. Pack `settle` turns this into metadata. */
24
+ export type ExamVerdict = {
25
+ readonly ok: boolean;
26
+ readonly comments: string;
27
+ };
28
+
29
+ /**
30
+ * Frozen exam admit. Host copies `program` into a ProgrammaticJob and
31
+ * seals `admit` onto that node. Model `expr` is ignored. `settle` is
32
+ * the sealed close — host does not know the pack's verdict action.
33
+ */
34
+ export type ExamAdmit<TIn = unknown, S = unknown, A = unknown> = {
35
+ readonly program: (state: S, input: TIn) => string | { readonly error: string };
36
+ readonly admit: (state: S, input: TIn) => A;
37
+ readonly settle?: (state: S, input: TIn, result: ExamVerdict) => A;
38
+ };
@@ -0,0 +1 @@
1
+ export * from "./plugin.js";
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Zod shapes for plugin-sdk constructors. Tools, actions, envCapabilities,
3
+ * bots, mcpServers. plugin.json extension format is the daemon's, not this
4
+ * package's.
5
+ */
6
+ import type { EnvCapability } from "@here.build/arrival";
7
+ import { asSystemPrompt } from "@inhuman.tools/harness-commons";
8
+ import { z } from "zod";
9
+
10
+ export const inhumanBrand = {
11
+ tool: Symbol.for("@inhuman.tools/InhumanTool"),
12
+ action: Symbol.for("@inhuman.tools/InhumanAction"),
13
+ plugin: Symbol.for("@inhuman.tools/InhumanPlugin"),
14
+ } as const;
15
+
16
+ const zodType = z.custom<z.ZodType>(
17
+ (value) => value != null && typeof (value as { parse?: unknown }).parse === "function",
18
+ { message: "zod schema required" },
19
+ );
20
+
21
+ const fn = z.custom<(...args: never[]) => unknown>((value) => typeof value === "function", {
22
+ message: "JS function required",
23
+ });
24
+
25
+ /** `tool` is completions tools[]. `symbol` is a scheme_repl name. `both` is both. */
26
+ export const toolRepresentation = z.enum(["tool", "symbol", "both"]);
27
+ export type ToolRepresentation = z.infer<typeof toolRepresentation>;
28
+
29
+ const tapeFace = z.string().trim().min(1, "tape face must be a non-empty string").nullable().default(null);
30
+
31
+ const toolBase = {
32
+ name: z.string().trim().min(1, "InhumanTool: non-empty name required"),
33
+ description: z.string().optional(),
34
+ class: z.string().optional(),
35
+ /** Omitted function tools are symbols. Omitted bot-calls are tools. */
36
+ representation: toolRepresentation.optional(),
37
+ input: zodType,
38
+ output: zodType,
39
+ triggersReactLoop: z.boolean().optional(),
40
+ isSideChannel: z.boolean().optional(),
41
+ tapeHidden: z.boolean().optional(),
42
+ question: tapeFace,
43
+ effect: tapeFace,
44
+ caption: tapeFace,
45
+ exam: z
46
+ .object({
47
+ program: fn,
48
+ admit: fn,
49
+ settle: fn.optional(),
50
+ prefixAsk: fn.optional(),
51
+ finish: fn.optional(),
52
+ })
53
+ .optional(),
54
+ };
55
+
56
+ export const functionToolDef = z
57
+ .object({
58
+ ...toolBase,
59
+ type: z.literal("function").optional(),
60
+ run: fn.optional(),
61
+ })
62
+ .superRefine((row, ctx) => {
63
+ if (row.exam == null && row.run == null) {
64
+ ctx.addIssue({ code: "custom", message: "function requires run" });
65
+ }
66
+ });
67
+
68
+ function catalogShow(raw: string | undefined): "result" | "omit" | "turn" {
69
+ if (raw === "silent" || raw === "omit") return "omit";
70
+ if (raw === "turn") return "turn";
71
+ return "result";
72
+ }
73
+
74
+ function catalogLifetime(raw: string | undefined): "turn" | "call" {
75
+ if (raw === "call") return "call";
76
+ return "turn";
77
+ }
78
+
79
+ export const botCallToolDef = z
80
+ .object({
81
+ ...toolBase,
82
+ type: z.literal("bot-call"),
83
+ target: z.string().trim().min(1, "bot-call target required"),
84
+ prepare: fn.optional(),
85
+ background: z.boolean().optional(),
86
+ lifetime: z.enum(["turn", "call"]).optional(),
87
+ show: z.enum(["result", "omit", "turn", "silent"]).optional(),
88
+ delivery: z.enum(["result", "silent", "turn"]).optional(),
89
+ deadlineMs: z.number().optional(),
90
+ maxResultTokens: z.number().optional(),
91
+ supportsNotify: z.boolean().optional(),
92
+ })
93
+ .superRefine((row, ctx) => {
94
+ const raw = row.show ?? row.delivery;
95
+ if (raw === "turn" && (row.background !== true || !(row.deadlineMs != null && row.deadlineMs > 0))) {
96
+ ctx.addIssue({ code: "custom", message: "show turn requires background and positive deadlineMs" });
97
+ }
98
+ })
99
+ .transform((row) => {
100
+ const { delivery: _delivery, show: rawShow, lifetime: rawLifetime, ...rest } = row;
101
+ return {
102
+ ...rest,
103
+ show: catalogShow(rawShow ?? _delivery),
104
+ lifetime: catalogLifetime(rawLifetime),
105
+ };
106
+ });
107
+
108
+ export const toolDef = z.union([functionToolDef, botCallToolDef]);
109
+
110
+ export const normalizedActionDef = z.object({
111
+ name: z.string().trim().min(1, "InhumanAction: non-empty name required"),
112
+ description: z.string().optional(),
113
+ seat: z.enum(["session", "harness"]).default("session"),
114
+ input: zodType.optional(),
115
+ run: fn,
116
+ });
117
+
118
+ export const actionDef = normalizedActionDef;
119
+
120
+ const brandedTool = z.custom<{ readonly name: string }>(
121
+ (value) =>
122
+ typeof value === "object" && value !== null && (value as { [k: symbol]: unknown })[inhumanBrand.tool] === true,
123
+ { message: "must be InhumanTool" },
124
+ );
125
+
126
+ const plainTool = z.union([functionToolDef, botCallToolDef]);
127
+
128
+ const brandedAction = z.custom<{ readonly name: string }>(
129
+ (value) =>
130
+ typeof value === "object" && value !== null && (value as { [k: symbol]: unknown })[inhumanBrand.action] === true,
131
+ { message: "must be InhumanAction" },
132
+ );
133
+
134
+ const plainAction = normalizedActionDef;
135
+
136
+ export const envCapability = z.custom<EnvCapability>(
137
+ (value) =>
138
+ typeof value === "object" &&
139
+ value !== null &&
140
+ typeof (value as { name?: unknown }).name === "string" &&
141
+ typeof (value as { exports?: unknown }).exports === "function",
142
+ { message: "must be Arrival EnvCapability" },
143
+ );
144
+
145
+ export const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
146
+
147
+ const nonempty = z.string().trim().min(1);
148
+
149
+ export const botMcpBindingDef = z
150
+ .object({
151
+ server: nonempty,
152
+ tools: z.array(nonempty).default([]),
153
+ approval: z.enum(["allow", "policy"]).optional(),
154
+ })
155
+ .transform((row) => ({
156
+ server: row.server,
157
+ tools: row.tools,
158
+ approval: row.approval === "allow" ? ("allow" as const) : ("policy" as const),
159
+ }));
160
+
161
+ export const botDef = z.preprocess(
162
+ (value) => {
163
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return value;
164
+ const rec = { ...(value as Record<string, unknown>) };
165
+ if (rec.recipeScope === undefined && "scope" in rec) rec.recipeScope = rec.scope;
166
+ delete rec.scope;
167
+ return rec;
168
+ },
169
+ z
170
+ .object({
171
+ name: nonempty,
172
+ recipeScope: z.enum(["invocation", "session", "workspace", "global"]),
173
+ instructions: nonempty,
174
+ description: z.string().trim().optional(),
175
+ tools: z.array(nonempty).default([]),
176
+ mcp: z.array(botMcpBindingDef).default([]),
177
+ keepAlive: z.boolean().optional(),
178
+ invokes: z.array(nonempty).default([]),
179
+ reasoningEffort: z.enum(REASONING_EFFORTS).optional(),
180
+ modelPreferences: z.array(nonempty).default([]),
181
+ skills: z.array(nonempty).default([]),
182
+ })
183
+ .transform((row) => ({
184
+ name: row.name,
185
+ recipeScope: row.recipeScope,
186
+ instructions: asSystemPrompt(row.instructions),
187
+ ...(row.description === undefined || row.description === "" ? {} : { description: row.description }),
188
+ tools: row.tools,
189
+ mcp: row.mcp,
190
+ keepAlive: row.keepAlive === true,
191
+ invokes: row.invokes,
192
+ ...(row.reasoningEffort === undefined ? {} : { reasoningEffort: row.reasoningEffort }),
193
+ modelPreferences: row.modelPreferences,
194
+ skills: row.skills,
195
+ })),
196
+ );
197
+
198
+ export const mcpServerDef = z.object({
199
+ name: nonempty,
200
+ visibility: z.enum(["private", "public"]),
201
+ scope: z.enum(["session", "workspace", "global"]),
202
+ transport: z.object({
203
+ type: z.literal("stdio"),
204
+ command: nonempty,
205
+ args: z.array(z.string()).default([]),
206
+ cwd: z.enum(["plugin", "workspace"]),
207
+ env: z
208
+ .union([
209
+ z.record(z.string(), z.string()),
210
+ z.custom<(ctx: { readonly pluginRoot: string; readonly workspaceRoot: string }) => Record<string, string>>(
211
+ (value) => typeof value === "function",
212
+ { message: "JS function required" },
213
+ ),
214
+ ])
215
+ .optional(),
216
+ }),
217
+ });
218
+
219
+ export const pluginProjectionDef = z.object({
220
+ initial: z.custom<() => unknown>((value) => typeof value === "function", { message: "JS function required" }),
221
+ reduce: z.custom<(state: unknown, action: unknown) => unknown>((value) => typeof value === "function", {
222
+ message: "JS function required",
223
+ }),
224
+ hostOnly: z
225
+ .custom<(action: unknown) => boolean>((value) => typeof value === "function", { message: "JS function required" })
226
+ .optional(),
227
+ });
228
+
229
+ export const exclusiveSlotsDef = z.array(nonempty);
230
+
231
+ export const pluginDef = z
232
+ .object({
233
+ apiVersion: z.literal(1).optional(),
234
+ tools: z.array(z.union([brandedTool, plainTool])).default([]),
235
+ actions: z.array(z.union([brandedAction, plainAction])).default([]),
236
+ envCapabilities: z.array(envCapability).default([]),
237
+ bots: z.array(botDef).default([]),
238
+ mcpServers: z.array(mcpServerDef).default([]),
239
+ exclusiveSlots: exclusiveSlotsDef.default([]),
240
+ projection: pluginProjectionDef.optional(),
241
+ })
242
+ .superRefine((bag, ctx) => {
243
+ const seen = new Set<string>();
244
+ for (const [index, action] of bag.actions.entries()) {
245
+ const name =
246
+ typeof action === "object" && action !== null && "name" in action
247
+ ? String((action as { name?: unknown }).name ?? "")
248
+ : "";
249
+ if (seen.has(name)) {
250
+ ctx.addIssue({
251
+ code: "custom",
252
+ message: `action "${name}" collides`,
253
+ path: ["actions", index],
254
+ });
255
+ }
256
+ seen.add(name);
257
+ }
258
+ });
259
+
260
+ export const toolContext = z
261
+ .object({
262
+ root: z.string().optional(),
263
+ signal: z.custom<AbortSignal>((value) => value instanceof AbortSignal).optional(),
264
+ turn: z.unknown().optional(),
265
+ completion: z.unknown().nullable().optional(),
266
+ invocation: z.unknown().optional(),
267
+ storage: z
268
+ .object({
269
+ session: z.unknown(),
270
+ invocation: z.unknown(),
271
+ project: z.unknown().optional(),
272
+ global: z.unknown().optional(),
273
+ propose: fn,
274
+ })
275
+ .optional(),
276
+ })
277
+ .passthrough();
278
+
279
+ export type NormalizedActionDefinition = z.infer<typeof normalizedActionDef>;
280
+
281
+ export const inhumanShape = {
282
+ toolDef,
283
+ functionToolDef,
284
+ botCallToolDef,
285
+ actionDef,
286
+ normalizedActionDef,
287
+ pluginDef,
288
+ botDef,
289
+ botMcpBindingDef,
290
+ mcpServerDef,
291
+ pluginProjectionDef,
292
+ exclusiveSlotsDef,
293
+ toolContext,
294
+ envCapability,
295
+ } as const;