toolfactory 0.1.1

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 (228) hide show
  1. package/README.md +166 -0
  2. package/dist/bindings/index.d.ts +23 -0
  3. package/dist/bindings/index.js +29 -0
  4. package/dist/bindings/python.d.ts +33 -0
  5. package/dist/bindings/python.js +805 -0
  6. package/dist/bindings/python.test.d.ts +1 -0
  7. package/dist/bindings/python.test.js +316 -0
  8. package/dist/bindings/typescript.d.ts +39 -0
  9. package/dist/bindings/typescript.js +758 -0
  10. package/dist/bindings/typescript.test.d.ts +1 -0
  11. package/dist/bindings/typescript.test.js +264 -0
  12. package/dist/commands.d.ts +188 -0
  13. package/dist/commands.js +662 -0
  14. package/dist/commands.test.d.ts +1 -0
  15. package/dist/commands.test.js +215 -0
  16. package/dist/e2e.test.d.ts +1 -0
  17. package/dist/e2e.test.js +124 -0
  18. package/dist/hosts/browser.d.ts +10 -0
  19. package/dist/hosts/browser.js +94 -0
  20. package/dist/hosts/github.d.ts +66 -0
  21. package/dist/hosts/github.js +247 -0
  22. package/dist/hosts/openclaw.d.ts +4 -0
  23. package/dist/hosts/openclaw.js +94 -0
  24. package/dist/hosts/template.d.ts +15 -0
  25. package/dist/hosts/template.js +142 -0
  26. package/dist/hosts/web.d.ts +4 -0
  27. package/dist/hosts/web.js +89 -0
  28. package/dist/identity/name.d.ts +20 -0
  29. package/dist/identity/name.js +33 -0
  30. package/dist/identity/name.test.d.ts +1 -0
  31. package/dist/identity/name.test.js +37 -0
  32. package/dist/identity/read.d.ts +9 -0
  33. package/dist/identity/read.js +106 -0
  34. package/dist/identity/read.test.d.ts +1 -0
  35. package/dist/identity/read.test.js +76 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +6 -0
  38. package/dist/introspect/index.d.ts +15 -0
  39. package/dist/introspect/index.js +63 -0
  40. package/dist/model.d.ts +236 -0
  41. package/dist/model.js +214 -0
  42. package/dist/ops.d.ts +255 -0
  43. package/dist/ops.js +297 -0
  44. package/dist/project/apply.d.ts +35 -0
  45. package/dist/project/apply.js +360 -0
  46. package/dist/project/apply.test.d.ts +1 -0
  47. package/dist/project/apply.test.js +285 -0
  48. package/dist/project/gate.d.ts +129 -0
  49. package/dist/project/gate.js +597 -0
  50. package/dist/project/history.d.ts +62 -0
  51. package/dist/project/history.js +41 -0
  52. package/dist/project/load.d.ts +21 -0
  53. package/dist/project/load.js +82 -0
  54. package/dist/project/lock.d.ts +28 -0
  55. package/dist/project/lock.js +42 -0
  56. package/dist/project/plan.d.ts +5 -0
  57. package/dist/project/plan.js +96 -0
  58. package/dist/report/coverage.d.ts +22 -0
  59. package/dist/report/coverage.js +72 -0
  60. package/dist/report/coverage.test.d.ts +1 -0
  61. package/dist/report/coverage.test.js +93 -0
  62. package/dist/surfaces/agent-plugins.d.ts +10 -0
  63. package/dist/surfaces/agent-plugins.js +36 -0
  64. package/dist/surfaces/agents.d.ts +35 -0
  65. package/dist/surfaces/agents.js +609 -0
  66. package/dist/surfaces/agents.template.d.ts +8 -0
  67. package/dist/surfaces/agents.template.js +21 -0
  68. package/dist/surfaces/agents.test.d.ts +1 -0
  69. package/dist/surfaces/agents.test.js +181 -0
  70. package/dist/surfaces/browser-extension.d.ts +101 -0
  71. package/dist/surfaces/browser-extension.js +1034 -0
  72. package/dist/surfaces/browser-extension.test.d.ts +1 -0
  73. package/dist/surfaces/browser-extension.test.js +145 -0
  74. package/dist/surfaces/claude.d.ts +2 -0
  75. package/dist/surfaces/claude.js +81 -0
  76. package/dist/surfaces/codex.d.ts +13 -0
  77. package/dist/surfaces/codex.js +101 -0
  78. package/dist/surfaces/codex.test.d.ts +1 -0
  79. package/dist/surfaces/codex.test.js +67 -0
  80. package/dist/surfaces/cursor.d.ts +2 -0
  81. package/dist/surfaces/cursor.js +50 -0
  82. package/dist/surfaces/distribution.test.d.ts +1 -0
  83. package/dist/surfaces/distribution.test.js +146 -0
  84. package/dist/surfaces/dsh.d.ts +25 -0
  85. package/dist/surfaces/dsh.js +147 -0
  86. package/dist/surfaces/dsh.test.d.ts +1 -0
  87. package/dist/surfaces/dsh.test.js +100 -0
  88. package/dist/surfaces/external.d.ts +9 -0
  89. package/dist/surfaces/external.js +4 -0
  90. package/dist/surfaces/gemini.d.ts +29 -0
  91. package/dist/surfaces/gemini.js +68 -0
  92. package/dist/surfaces/gemini.test.d.ts +1 -0
  93. package/dist/surfaces/gemini.test.js +74 -0
  94. package/dist/surfaces/hermes-native.d.ts +53 -0
  95. package/dist/surfaces/hermes-native.js +507 -0
  96. package/dist/surfaces/hermes-native.test.d.ts +1 -0
  97. package/dist/surfaces/hermes-native.test.js +156 -0
  98. package/dist/surfaces/kernel.d.ts +5 -0
  99. package/dist/surfaces/kernel.js +63 -0
  100. package/dist/surfaces/mcp-registry.d.ts +19 -0
  101. package/dist/surfaces/mcp-registry.js +160 -0
  102. package/dist/surfaces/mcp-registry.test.d.ts +1 -0
  103. package/dist/surfaces/mcp-registry.test.js +79 -0
  104. package/dist/surfaces/mcpb.d.ts +11 -0
  105. package/dist/surfaces/mcpb.js +118 -0
  106. package/dist/surfaces/mcpb.test.d.ts +1 -0
  107. package/dist/surfaces/mcpb.test.js +104 -0
  108. package/dist/surfaces/npm.d.ts +6 -0
  109. package/dist/surfaces/npm.js +72 -0
  110. package/dist/surfaces/openclaw-native.d.ts +93 -0
  111. package/dist/surfaces/openclaw-native.js +956 -0
  112. package/dist/surfaces/openclaw-native.test.d.ts +1 -0
  113. package/dist/surfaces/openclaw-native.test.js +246 -0
  114. package/dist/surfaces/pypi.d.ts +5 -0
  115. package/dist/surfaces/pypi.js +82 -0
  116. package/dist/surfaces/readme.d.ts +7 -0
  117. package/dist/surfaces/readme.js +181 -0
  118. package/dist/surfaces/registry.d.ts +6 -0
  119. package/dist/surfaces/registry.js +73 -0
  120. package/dist/surfaces/shared.d.ts +49 -0
  121. package/dist/surfaces/shared.js +112 -0
  122. package/dist/surfaces/skill.d.ts +7 -0
  123. package/dist/surfaces/skill.js +118 -0
  124. package/dist/surfaces/skill.test.d.ts +1 -0
  125. package/dist/surfaces/skill.test.js +87 -0
  126. package/dist/surfaces/web.d.ts +97 -0
  127. package/dist/surfaces/web.js +1285 -0
  128. package/dist/surfaces/web.test.d.ts +1 -0
  129. package/dist/surfaces/web.test.js +255 -0
  130. package/dist/surfaces/workflows.d.ts +2 -0
  131. package/dist/surfaces/workflows.js +809 -0
  132. package/dist/surfaces/workflows.test.d.ts +1 -0
  133. package/dist/surfaces/workflows.test.js +563 -0
  134. package/dist/toolfactory/cli.d.ts +2 -0
  135. package/dist/toolfactory/cli.js +65 -0
  136. package/dist/toolfactory/config.d.ts +3 -0
  137. package/dist/toolfactory/config.js +27 -0
  138. package/dist/toolfactory/mcp.d.ts +37 -0
  139. package/dist/toolfactory/mcp.js +269 -0
  140. package/dist/toolfactory/types.d.ts +31 -0
  141. package/dist/toolfactory/types.js +10 -0
  142. package/dist/toolfactory/web.d.ts +4 -0
  143. package/dist/toolfactory/web.js +40 -0
  144. package/dist/validate/agent-plugins.d.ts +5 -0
  145. package/dist/validate/agent-plugins.js +46 -0
  146. package/package.json +68 -0
  147. package/schemas/agent-plugins/mcp.schema.json +120 -0
  148. package/schemas/agent-plugins/plugin.schema.json +65 -0
  149. package/src/bindings/index.ts +47 -0
  150. package/src/bindings/python.test.ts +374 -0
  151. package/src/bindings/python.ts +862 -0
  152. package/src/bindings/typescript.test.ts +329 -0
  153. package/src/bindings/typescript.ts +818 -0
  154. package/src/commands.test.ts +253 -0
  155. package/src/commands.ts +886 -0
  156. package/src/e2e.test.ts +169 -0
  157. package/src/hosts/browser.ts +106 -0
  158. package/src/hosts/github.ts +321 -0
  159. package/src/hosts/openclaw.ts +110 -0
  160. package/src/hosts/template.ts +149 -0
  161. package/src/hosts/web.ts +111 -0
  162. package/src/identity/name.test.ts +43 -0
  163. package/src/identity/name.ts +39 -0
  164. package/src/identity/read.test.ts +92 -0
  165. package/src/identity/read.ts +119 -0
  166. package/src/index.ts +11 -0
  167. package/src/introspect/index.ts +72 -0
  168. package/src/model.ts +386 -0
  169. package/src/ops.ts +333 -0
  170. package/src/project/apply.test.ts +348 -0
  171. package/src/project/apply.ts +417 -0
  172. package/src/project/gate.ts +717 -0
  173. package/src/project/history.ts +46 -0
  174. package/src/project/load.ts +104 -0
  175. package/src/project/lock.ts +54 -0
  176. package/src/project/plan.ts +101 -0
  177. package/src/report/coverage.test.ts +108 -0
  178. package/src/report/coverage.ts +113 -0
  179. package/src/surfaces/agent-plugins.ts +47 -0
  180. package/src/surfaces/agents.template.ts +32 -0
  181. package/src/surfaces/agents.test.ts +206 -0
  182. package/src/surfaces/agents.ts +729 -0
  183. package/src/surfaces/browser-extension.test.ts +175 -0
  184. package/src/surfaces/browser-extension.ts +1095 -0
  185. package/src/surfaces/claude.ts +99 -0
  186. package/src/surfaces/codex.test.ts +71 -0
  187. package/src/surfaces/codex.ts +105 -0
  188. package/src/surfaces/cursor.ts +62 -0
  189. package/src/surfaces/distribution.test.ts +166 -0
  190. package/src/surfaces/dsh.test.ts +110 -0
  191. package/src/surfaces/dsh.ts +159 -0
  192. package/src/surfaces/external.ts +13 -0
  193. package/src/surfaces/gemini.test.ts +80 -0
  194. package/src/surfaces/gemini.ts +84 -0
  195. package/src/surfaces/hermes-native.test.ts +179 -0
  196. package/src/surfaces/hermes-native.ts +584 -0
  197. package/src/surfaces/kernel.ts +68 -0
  198. package/src/surfaces/mcp-registry.test.ts +99 -0
  199. package/src/surfaces/mcp-registry.ts +190 -0
  200. package/src/surfaces/mcpb.test.ts +112 -0
  201. package/src/surfaces/mcpb.ts +130 -0
  202. package/src/surfaces/npm.ts +78 -0
  203. package/src/surfaces/openclaw-native.test.ts +283 -0
  204. package/src/surfaces/openclaw-native.ts +1070 -0
  205. package/src/surfaces/pypi.ts +84 -0
  206. package/src/surfaces/readme.ts +218 -0
  207. package/src/surfaces/registry.ts +79 -0
  208. package/src/surfaces/shared.ts +140 -0
  209. package/src/surfaces/skill.test.ts +103 -0
  210. package/src/surfaces/skill.ts +136 -0
  211. package/src/surfaces/web.test.ts +303 -0
  212. package/src/surfaces/web.ts +1310 -0
  213. package/src/surfaces/workflows.test.ts +677 -0
  214. package/src/surfaces/workflows.ts +936 -0
  215. package/src/toolfactory/cli.ts +74 -0
  216. package/src/toolfactory/config.ts +33 -0
  217. package/src/toolfactory/mcp.ts +311 -0
  218. package/src/toolfactory/types.ts +52 -0
  219. package/src/toolfactory/web.ts +45 -0
  220. package/src/validate/agent-plugins.ts +53 -0
  221. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  222. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  223. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  224. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  225. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  226. package/web/dist/assets/index-CCXhQKK3.css +2 -0
  227. package/web/dist/assets/index-D1PwZ3qS.js +10 -0
  228. package/web/dist/index.html +14 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Operations come from the code: spawn the kernel MCP server, list its tools, and
3
+ * snapshot the result verbatim to ops.json so every generator runs offline.
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { Client } from "@modelcontextprotocol/client";
8
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
9
+ import { getBinding } from "../bindings/index.js";
10
+ import type { Project } from "../model.js";
11
+ import { OPS_PATH, type Ops, TOOLFACTORY_VERSION, toOperation } from "../project/load.js";
12
+
13
+ export async function listTools(root: string, command: string, args: string[]): Promise<Ops> {
14
+ const transport = new StdioClientTransport({
15
+ command,
16
+ args,
17
+ cwd: root,
18
+ stderr: "pipe",
19
+ env: { ...(process.env as Record<string, string>), TOOLFACTORY_INTROSPECT: "1" },
20
+ });
21
+ const stderr: string[] = [];
22
+ transport.stderr?.on("data", (chunk: Buffer) => stderr.push(String(chunk)));
23
+ const client = new Client({ name: "toolfactory", version: TOOLFACTORY_VERSION });
24
+ try {
25
+ await client.connect(transport).catch((error: unknown) => {
26
+ const detail = stderr.join("").trim();
27
+ throw new Error(
28
+ `Could not start the kernel MCP server (${[command, ...args].join(" ")}): ${error instanceof Error ? error.message : String(error)}${detail ? `\n${detail}` : ""}`,
29
+ );
30
+ });
31
+ const tools: Ops["tools"] = [];
32
+ let cursor: string | undefined;
33
+ do {
34
+ const page = await client.listTools(cursor ? { cursor } : undefined);
35
+ tools.push(...(page.tools as Ops["tools"]));
36
+ cursor = page.nextCursor;
37
+ } while (cursor);
38
+ tools.sort((a, b) => a.name.localeCompare(b.name));
39
+ return { tools };
40
+ } finally {
41
+ await client.close().catch(() => {});
42
+ }
43
+ }
44
+
45
+ export function serializeOps(ops: Ops): string {
46
+ return `${JSON.stringify(ops, null, 2)}\n`;
47
+ }
48
+
49
+ /** Spawn the kernel and compare its `tools/list` with the committed snapshot; writes nothing. */
50
+ export async function snapshot(
51
+ project: Project,
52
+ ): Promise<{ ops: Ops; next: string; changed: boolean }> {
53
+ const kernel = project.tool.kernel ?? getBinding(project.tool.binding).kernelCommand(project);
54
+ const ops = await listTools(project.root, kernel.command, kernel.args);
55
+ for (const tool of ops.tools) toOperation(tool); // rejects a capability outside the closed set
56
+ const next = serializeOps(ops);
57
+ const path = join(project.root, OPS_PATH);
58
+ const changed = !existsSync(path) || readFileSync(path, "utf8") !== next;
59
+ return { ops, next, changed };
60
+ }
61
+
62
+ export async function introspect(
63
+ project: Project,
64
+ ): Promise<{ path: string; changed: boolean; ops: Ops }> {
65
+ const { ops, next, changed } = await snapshot(project);
66
+ if (changed) {
67
+ const path = join(project.root, OPS_PATH);
68
+ mkdirSync(dirname(path), { recursive: true });
69
+ writeFileSync(path, next);
70
+ }
71
+ return { path: OPS_PATH, changed, ops };
72
+ }
package/src/model.ts ADDED
@@ -0,0 +1,386 @@
1
+ /**
2
+ * The shared plane. Every generator and command works only with these types.
3
+ *
4
+ * Identity is authored in one file the ecosystem already defines (plugin.json,
5
+ * package.json, pyproject.toml, or a host manifest). Operations come from the
6
+ * author's code, snapshotted from the kernel MCP server's `tools/list`. The
7
+ * rest is derived and written in-tree, tracked by a lock file.
8
+ */
9
+ import { z } from "zod";
10
+
11
+ export const CAPABILITIES = [
12
+ "net",
13
+ "fs",
14
+ "shell",
15
+ "secret",
16
+ "browser",
17
+ "model",
18
+ "user-input",
19
+ "channel",
20
+ ] as const;
21
+ export type Capability = (typeof CAPABILITIES)[number];
22
+ export const PORTABLE_CAPABILITIES: ReadonlySet<Capability> = new Set([
23
+ "net",
24
+ "fs",
25
+ "shell",
26
+ "secret",
27
+ ]);
28
+
29
+ export const SURFACE_IDS = [
30
+ "skill",
31
+ "agent-plugins",
32
+ "claude",
33
+ "codex",
34
+ "cursor",
35
+ "mcp",
36
+ "mcp-registry",
37
+ "cli",
38
+ "npm",
39
+ "pypi",
40
+ "openclaw-native",
41
+ "hermes-native",
42
+ "clawhub",
43
+ "web",
44
+ "dsh",
45
+ "workflows",
46
+ "agents",
47
+ "readme",
48
+ "gemini",
49
+ "mcpb",
50
+ "browser-extension",
51
+ ] as const;
52
+ export type SurfaceId = (typeof SURFACE_IDS)[number];
53
+
54
+ export const BINDINGS = ["typescript", "python"] as const;
55
+ export type Binding = (typeof BINDINGS)[number];
56
+
57
+ /**
58
+ * What `init` selects when asked for nothing: the skills-first minimum that already serves every
59
+ * harness — a skill (installed anywhere by `npx skills add`), the Agent Plugins bundle, the
60
+ * kernel as an MCP server and a CLI, and the binding's package registry to distribute them. Every
61
+ * host-specific plugin is opt-in: add one only when that host needs what a skill and an MCP
62
+ * server cannot give it (a gateway tab, browser capability, a store listing).
63
+ */
64
+ export function defaultSurfaces(binding: Binding): SurfaceId[] {
65
+ return ["skill", "agent-plugins", "mcp", "cli", binding === "python" ? "pypi" : "npm"];
66
+ }
67
+
68
+ /** Package managers the generated workflows know how to drive; read from package.json `packageManager`. */
69
+ export const PACKAGE_MANAGERS = ["npm", "pnpm"] as const;
70
+ export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
71
+
72
+ /** Agent Plugins 1.0.0 name rule; the canonical name N every projection derives from. */
73
+ export const NAME_PATTERN = /^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
74
+
75
+ const jsonObject = z.record(z.string(), z.unknown());
76
+
77
+ /** `dev.toolfactory/tool.json` — the one file toolfactory owns. */
78
+ export const ToolConfigSchema = z
79
+ .object({
80
+ $schema: z.string().optional(),
81
+ schemaVersion: z.literal(1),
82
+ identity: z
83
+ .string()
84
+ .describe(
85
+ "Repo-relative path of the authored identity file (plugin.json, package.json, pyproject.toml, or a host manifest).",
86
+ ),
87
+ binding: z.enum(BINDINGS).describe("Language of the core logic and its kernel surfaces."),
88
+ runtime: z
89
+ .enum(["kernel", "none"])
90
+ .optional()
91
+ .describe(
92
+ "Whether this project has an executable kernel. `none` is for an instruction-only Codex skill/plugin and permits only the skill and codex surfaces.",
93
+ ),
94
+ surfaces: z
95
+ .array(z.enum(SURFACE_IDS))
96
+ .min(1)
97
+ .describe("Selected surfaces. A file exists iff a selected surface owns it."),
98
+ kernel: z
99
+ .object({
100
+ command: z.string(),
101
+ args: z.array(z.string()).default([]),
102
+ })
103
+ .optional()
104
+ .describe("How to spawn the kernel MCP server for introspection; defaults from the binding."),
105
+ bundle: z
106
+ .object({ runtime: z.enum(["package", "bundled"]).default("package") })
107
+ .default({ runtime: "package" })
108
+ .describe(
109
+ "How mcp.json reaches the kernel: a published package (npx/uvx) or a committed single-file build.",
110
+ ),
111
+ config: jsonObject
112
+ .optional()
113
+ .refine(
114
+ (schema) =>
115
+ Object.values(
116
+ (schema?.properties ?? {}) as Record<string, Record<string, unknown>>,
117
+ ).every(
118
+ (property) =>
119
+ !(property["x-toolfactory"] as { sensitive?: boolean } | undefined)?.sensitive ||
120
+ property.default === undefined,
121
+ ),
122
+ "A sensitive config property must not declare a default: it would be committed into every manifest.",
123
+ )
124
+ .describe(
125
+ "JSON Schema 2020-12 object for the tool's configuration; mark secrets with x-toolfactory.sensitive.",
126
+ ),
127
+ tests: z
128
+ .object({ examples: z.record(z.string(), jsonObject).default({}) })
129
+ .default({ examples: {} })
130
+ .describe("Example arguments per operation, used by the generated surface-smoke tests."),
131
+ npm: z.object({ scope: z.string().optional() }).optional(),
132
+ codex: z.object({ interface: jsonObject.optional() }).optional(),
133
+ browserExtension: z
134
+ .object({
135
+ endpoint: z
136
+ .string()
137
+ .optional()
138
+ .describe(
139
+ "The kernel's mcp --http URL the extension pairs with; defaults to the kernel's own default port.",
140
+ ),
141
+ authDomains: z
142
+ .array(z.string())
143
+ .optional()
144
+ .describe(
145
+ "Match patterns the extension may act in with the user's own session (host_permissions).",
146
+ ),
147
+ cookieExport: z
148
+ .boolean()
149
+ .optional()
150
+ .describe(
151
+ "Opt in to exporting the session cookies of authDomains to the kernel (needs the cookies permission; Chromium and Firefox only).",
152
+ ),
153
+ geckoId: z
154
+ .string()
155
+ .optional()
156
+ .describe("Firefox add-on id; defaults to <name>@<github owner>.github.io."),
157
+ icon: z
158
+ .string()
159
+ .optional()
160
+ .describe(
161
+ "Repo-relative source PNG or SVG the icon set is resized from; the placeholder set otherwise.",
162
+ ),
163
+ sidePanel: z
164
+ .boolean()
165
+ .optional()
166
+ .describe("Also register a Chromium side panel (Chromium only)."),
167
+ safari: z
168
+ .boolean()
169
+ .optional()
170
+ .describe("Add the macOS Safari packaging leg to the release workflow."),
171
+ })
172
+ .optional(),
173
+ mcpb: z
174
+ .object({ privacyPolicies: z.array(z.string()).optional() })
175
+ .optional()
176
+ .describe(
177
+ "Privacy-policy URLs of the services this tool talks to; Anthropic's Connectors Directory submission requires them in the MCPB manifest.",
178
+ ),
179
+ hermes: z.object({ toolset: z.string().optional() }).optional(),
180
+ openclaw: z
181
+ .object({
182
+ registers: z
183
+ .array(
184
+ z.object({
185
+ api: z
186
+ .string()
187
+ .describe("OpenClawPluginApi method, e.g. registerRealtimeVoiceProvider"),
188
+ contract: z
189
+ .string()
190
+ .describe("openclaw.plugin.json contracts key, e.g. realtimeVoiceProviders"),
191
+ ids: z.array(z.string()).default([]).describe("Ids registered under that contract"),
192
+ }),
193
+ )
194
+ .optional()
195
+ .describe(
196
+ "Host registrations the plugin makes beyond tools; one declaration drives the manifest contracts, the plugin inspector's expectations and the generated test.",
197
+ ),
198
+ activation: jsonObject
199
+ .optional()
200
+ .describe("openclaw.plugin.json activation; default onStartup."),
201
+ pluginApi: z
202
+ .string()
203
+ .optional()
204
+ .describe("Plugin API range for compat.pluginApi and the openclaw peer dependency."),
205
+ dependencies: z.record(z.string(), z.string()).optional(),
206
+ peerDependencies: z.record(z.string(), z.string()).optional(),
207
+ devDependencies: z.record(z.string(), z.string()).optional(),
208
+ })
209
+ .optional(),
210
+ })
211
+ .superRefine((tool, context) => {
212
+ if (tool.runtime !== "none") return;
213
+ const unsupported = tool.surfaces.filter(
214
+ (surface) => surface !== "skill" && surface !== "codex",
215
+ );
216
+ if (unsupported.length) {
217
+ context.addIssue({
218
+ code: "custom",
219
+ path: ["surfaces"],
220
+ message: `runtime "none" supports only skill and codex surfaces; unsupported: ${unsupported.join(", ")}.`,
221
+ });
222
+ }
223
+ })
224
+ .strict();
225
+ export type ToolConfig = z.infer<typeof ToolConfigSchema>;
226
+
227
+ /** An instruction-only plugin has author prose but no generated executable or MCP server. */
228
+ export function isInstructionOnly(tool: ToolConfig): boolean {
229
+ return tool.runtime === "none";
230
+ }
231
+
232
+ export interface Author {
233
+ name?: string;
234
+ email?: string;
235
+ url?: string;
236
+ }
237
+
238
+ /** Authored once, in the identity file; projected everywhere else. */
239
+ export interface Identity {
240
+ name: string;
241
+ version?: string;
242
+ description?: string;
243
+ author?: Author;
244
+ homepage?: string;
245
+ repository?: string;
246
+ license?: string;
247
+ keywords?: string[];
248
+ }
249
+
250
+ /** One MCP tool definition as returned by `tools/list`, plus the requirements it declares. */
251
+ export interface Operation {
252
+ name: string;
253
+ title?: string;
254
+ description?: string;
255
+ inputSchema: Record<string, unknown>;
256
+ outputSchema?: Record<string, unknown>;
257
+ annotations?: Record<string, unknown>;
258
+ requires: Capability[];
259
+ }
260
+
261
+ export type VerdictKind = "native" | "bridged" | "degraded" | "excluded";
262
+ export interface Verdict {
263
+ kind: VerdictKind;
264
+ /** Machine-readable reason, e.g. `excluded:mcp-no-host-capabilities`. Empty for `native`. */
265
+ reason?: string;
266
+ }
267
+
268
+ /** A whole generated file. */
269
+ export interface FullFile {
270
+ kind: "file";
271
+ path: string;
272
+ content: string;
273
+ mode?: number;
274
+ /**
275
+ * A build output: generated and locked like any other file, but not expected to exist.
276
+ * `check` does not report it missing (it is gitignored and rebuilt), only stale when present.
277
+ */
278
+ output?: true;
279
+ /** `content` is a link target, not file bytes: the file is a symbolic link (`.agents/skills/<N>`). */
280
+ symlink?: true;
281
+ }
282
+
283
+ export interface Region {
284
+ begin: string;
285
+ end: string;
286
+ content: string;
287
+ }
288
+
289
+ /**
290
+ * A file the author owns, in which toolfactory maintains one or more delimited regions.
291
+ * `template` is written when the file does not exist and must contain every marker.
292
+ */
293
+ export interface RegionFile {
294
+ kind: "region";
295
+ path: string;
296
+ regions: Region[];
297
+ template: string;
298
+ }
299
+
300
+ /**
301
+ * A structured file shared with the author (package.json, pyproject.toml): toolfactory
302
+ * owns exactly the keys in `patch` (deep-merged) and leaves every other key alone.
303
+ */
304
+ export interface MergeFile {
305
+ kind: "merge";
306
+ path: string;
307
+ format: "json" | "toml";
308
+ patch: Record<string, unknown>;
309
+ /** Dotted paths inside `patch` whose object toolfactory owns whole: replaced, never merged. */
310
+ owned?: string[];
311
+ }
312
+
313
+ export type PlannedFile = FullFile | RegionFile | MergeFile;
314
+
315
+ export interface Command {
316
+ /** Human label used in reports. */
317
+ label: string;
318
+ command: string;
319
+ args: string[];
320
+ cwd?: string;
321
+ env?: Record<string, string>;
322
+ }
323
+
324
+ export interface Project {
325
+ root: string;
326
+ tool: ToolConfig;
327
+ identity: Identity;
328
+ /** Unknown top-level keys of the identity file, preserved on rewrite. */
329
+ identityExtra: Record<string, unknown>;
330
+ operations: Operation[];
331
+ toolfactoryVersion: string;
332
+ /** From the root package.json `packageManager` field; npm when absent. */
333
+ packageManager?: PackageManager;
334
+ }
335
+
336
+ export interface Surface {
337
+ id: SurfaceId;
338
+ /** Pure: no I/O. Everything this surface writes for the project. */
339
+ plan(project: Project): PlannedFile[];
340
+ /** Upstream validators to run for this surface (tier 1). */
341
+ validate?(project: Project): Command[];
342
+ /** Per-operation verdict on this surface. Default: portable ⇒ native, else excluded. */
343
+ verdict?(operation: Operation, project: Project): Verdict;
344
+ /**
345
+ * Surfaces this one reads the output of. A selection that omits one is refused by name at
346
+ * plan time instead of emitting an artifact that points at a file nobody writes.
347
+ */
348
+ requires?: SurfaceId[];
349
+ }
350
+
351
+ export function isPortable(operation: Operation): boolean {
352
+ return operation.requires.every((capability) => PORTABLE_CAPABILITIES.has(capability));
353
+ }
354
+
355
+ /**
356
+ * The other half of the sensitive-config law above: a sensitive key must not declare a default
357
+ * (it would be committed into every manifest), and **no operation may take one as an argument**.
358
+ *
359
+ * An operation's `inputSchema` is projected onto MCP `tools/call`, the CLI flags, the skill's
360
+ * operations block, the web form and every host manifest at once, so a property named like a
361
+ * secret is a paste-into-chat path on every surface simultaneously. A secret reaches the kernel
362
+ * through config — the environment a host injects from its own masked store — and never through
363
+ * an argument. Enforced wherever a project is loaded, so `introspect`, `build` and `check` all
364
+ * refuse it.
365
+ */
366
+ export function assertNoSensitiveArgument(
367
+ config: Record<string, unknown> | undefined,
368
+ operations: readonly Operation[],
369
+ ): void {
370
+ const properties = (config?.properties ?? {}) as Record<string, Record<string, unknown>>;
371
+ const sensitive = new Set(
372
+ Object.entries(properties)
373
+ .filter(([, property]) => (property["x-toolfactory"] as { sensitive?: boolean })?.sensitive)
374
+ .map(([key]) => key),
375
+ );
376
+ if (!sensitive.size) return;
377
+ for (const operation of operations) {
378
+ const inputs = (operation.inputSchema.properties ?? {}) as Record<string, unknown>;
379
+ for (const name of Object.keys(inputs)) {
380
+ if (!sensitive.has(name)) continue;
381
+ throw new Error(
382
+ `Operation "${operation.name}" declares an input named "${name}", which is a sensitive config key: a secret reaches the kernel through config (the environment the host injects), never through an argument every surface would prompt for. Read it from config in the handler and drop the property.`,
383
+ );
384
+ }
385
+ }
386
+ }