@ixo/oracle-runtime-workers 0.2.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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,356 @@
1
+ import { z } from 'zod';
2
+ import { tool } from '../../../plugin-api/tool-helper';
3
+ import type { PluginTool, RuntimeContext } from '../../../plugin-api/types';
4
+ import { UcanMintUnavailableError } from '../../../plugin-api/ucan-errors';
5
+
6
+ /**
7
+ * Raw capsule shape returned by ai-skills. Validated via Zod at the network
8
+ * boundary so a malformed registry response surfaces as a clean parse error
9
+ * rather than a downstream type-confusion bug.
10
+ */
11
+ const capsuleSchema = z.object({
12
+ cid: z.string(),
13
+ name: z.string(),
14
+ description: z.string().default(''),
15
+ license: z.string().nullable().optional(),
16
+ compatibility: z.string().nullable().optional(),
17
+ allowedTools: z.string().nullable().optional(),
18
+ metadata: z.record(z.string(), z.string()).nullable().optional(),
19
+ archiveSize: z.number().optional(),
20
+ createdAt: z.string().optional(),
21
+ // Set by ai-skills when the response row is private (i.e. owned by the
22
+ // caller). Absent for public rows.
23
+ visibility: z.enum(['public', 'private']).optional(),
24
+ ownerDid: z.string().nullable().optional(),
25
+ oracleDid: z.string().nullable().optional(),
26
+ });
27
+ type Capsule = z.infer<typeof capsuleSchema>;
28
+
29
+ const paginationSchema = z.object({
30
+ total: z.number(),
31
+ limit: z.number(),
32
+ offset: z.number(),
33
+ hasMore: z.boolean(),
34
+ });
35
+
36
+ const listResponseSchema = z.object({
37
+ capsules: z.array(capsuleSchema),
38
+ pagination: paginationSchema,
39
+ });
40
+
41
+ const searchResponseSchema = z.object({
42
+ query: z.string(),
43
+ count: z.number(),
44
+ capsules: z.array(capsuleSchema),
45
+ });
46
+
47
+ /** Normalised skill record exposed to the agent. */
48
+ export interface MergedSkill {
49
+ title: string;
50
+ description: string;
51
+ path: string;
52
+ /** `public` — registry skill visible to everyone; `private` — owned by the current (oracle, user) pair. */
53
+ source: 'public' | 'private';
54
+ cid?: string;
55
+ createdAt?: string;
56
+ }
57
+
58
+ const LIST_DESCRIPTION = `List available skills from the IXO skills registry — the caller's **published** private skills first, then public registry skills.
59
+
60
+ Each entry includes:
61
+ - title: skill name
62
+ - description: skill description
63
+ - path: absolute sandbox path to the skill folder
64
+ - source: "private" (your published skill) or "public" (registry)
65
+ - cid: required by load_skill and by sandbox_run (when the run depends on the skill). Never use a CID as a file path.`;
66
+
67
+ const SEARCH_DESCRIPTION = `Search the caller's published skills and the public IXO registry by query.
68
+
69
+ Matching published (private) skills come first, then public registry results. Each entry includes title, description, path, source ("private" | "public"), and cid.`;
70
+
71
+ const listSchema = z.object({
72
+ limit: z
73
+ .number()
74
+ .min(1)
75
+ .max(100)
76
+ .optional()
77
+ .describe('Optional: number of skills to return (1-100, default: 20).'),
78
+ offset: z
79
+ .number()
80
+ .min(0)
81
+ .optional()
82
+ .describe('Optional: pagination offset (default: 0).'),
83
+ });
84
+
85
+ const searchSchema = z.object({
86
+ q: z
87
+ .string()
88
+ .min(1, 'Search query is required')
89
+ .describe(
90
+ 'Search query (e.g. "pptx", "invoice", "presentation", "docx"). Required.',
91
+ ),
92
+ limit: z
93
+ .number()
94
+ .min(1)
95
+ .max(50)
96
+ .optional()
97
+ .describe('Optional: max results to return (1-50, default: 10).'),
98
+ });
99
+
100
+ function normalizeRegistryCapsule(capsule: Capsule): MergedSkill {
101
+ return {
102
+ title: capsule.name,
103
+ description: capsule.description ?? '',
104
+ path: `/workspace/skills/${capsule.name}`,
105
+ source: capsule.visibility === 'private' ? 'private' : 'public',
106
+ cid: capsule.cid,
107
+ createdAt: capsule.createdAt
108
+ ? new Date(capsule.createdAt).toISOString()
109
+ : undefined,
110
+ };
111
+ }
112
+
113
+ interface RegistryFetcherOptions {
114
+ baseUrl: string;
115
+ network: string;
116
+ skillsUcan: string | undefined;
117
+ signal?: AbortSignal;
118
+ }
119
+
120
+ /**
121
+ * Build the auth/network header set for outbound ai-skills requests. When a
122
+ * UCAN invocation is available, ai-skills returns the caller's private
123
+ * skills alongside the public ones; without it, only public rows come back.
124
+ */
125
+ function buildRegistryHeaders(opts: RegistryFetcherOptions): HeadersInit {
126
+ const headers: Record<string, string> = {
127
+ 'X-IXO-Network': opts.network,
128
+ };
129
+ if (opts.skillsUcan) {
130
+ headers['Authorization'] = `Bearer ${opts.skillsUcan}`;
131
+ headers['X-Auth-Type'] = 'ucan';
132
+ }
133
+ return headers;
134
+ }
135
+
136
+ type RegistryListResponse = z.infer<typeof listResponseSchema>;
137
+
138
+ async function fetchRegistryCapsules(
139
+ opts: RegistryFetcherOptions,
140
+ limit: number,
141
+ offset: number,
142
+ ): Promise<RegistryListResponse> {
143
+ const url = new URL('/capsules', opts.baseUrl);
144
+ url.searchParams.set('limit', limit.toString());
145
+ url.searchParams.set('offset', offset.toString());
146
+
147
+ const response = await fetch(url.toString(), {
148
+ headers: buildRegistryHeaders(opts),
149
+ signal: opts.signal,
150
+ });
151
+ if (!response.ok) {
152
+ throw new Error(`List skills failed: ${response.statusText}`);
153
+ }
154
+ return listResponseSchema.parse(await response.json());
155
+ }
156
+
157
+ async function searchRegistryCapsules(
158
+ opts: RegistryFetcherOptions,
159
+ q: string,
160
+ limit: number,
161
+ ): Promise<Capsule[]> {
162
+ const url = new URL('/capsules/search', opts.baseUrl);
163
+ url.searchParams.set('q', q);
164
+ url.searchParams.set('limit', limit.toString());
165
+
166
+ const response = await fetch(url.toString(), {
167
+ headers: buildRegistryHeaders(opts),
168
+ signal: opts.signal,
169
+ });
170
+ if (!response.ok) {
171
+ throw new Error(`Search skills failed: ${response.statusText}`);
172
+ }
173
+
174
+ const data = searchResponseSchema.parse(await response.json());
175
+
176
+ // Dedup by name. When a private (caller-owned) and a public row share a
177
+ // name, prefer the private one — the user's own skill always wins. Among
178
+ // entries of the same source, keep the newest by createdAt.
179
+ const skillsMap = new Map<string, Capsule>();
180
+ for (const capsule of data.capsules) {
181
+ const existing = skillsMap.get(capsule.name);
182
+ if (!existing) {
183
+ skillsMap.set(capsule.name, capsule);
184
+ continue;
185
+ }
186
+ const incomingPrivate = capsule.visibility === 'private';
187
+ const existingPrivate = existing.visibility === 'private';
188
+ if (incomingPrivate && !existingPrivate) {
189
+ skillsMap.set(capsule.name, capsule);
190
+ continue;
191
+ }
192
+ if (!incomingPrivate && existingPrivate) {
193
+ continue;
194
+ }
195
+ const isNewer =
196
+ capsule.createdAt && existing.createdAt
197
+ ? new Date(capsule.createdAt).getTime() >
198
+ new Date(existing.createdAt).getTime()
199
+ : false;
200
+ if (isNewer) skillsMap.set(capsule.name, capsule);
201
+ }
202
+ return Array.from(skillsMap.values());
203
+ }
204
+
205
+ /**
206
+ * Mints an `ixo:skills` UCAN invocation for outbound calls to the skills
207
+ * registry, or resolves `undefined` when the tools should run public-only.
208
+ * Implementations must never throw — auth trouble degrades, it does not fail
209
+ * the tool.
210
+ */
211
+ export type SkillsUcanBuilder = (
212
+ skillsServiceUrl: string,
213
+ runCtx: RuntimeContext,
214
+ ) => Promise<string | undefined>;
215
+
216
+ /**
217
+ * Default {@link SkillsUcanBuilder}. Public-only when the oracle has no
218
+ * signing key (`ctx.ucan.hasSigningKey()` is `false`) or the registry's
219
+ * did:web document cannot be resolved; otherwise mints via
220
+ * `ctx.ucan.mintInvocation`. An expected mint failure
221
+ * (`UcanMintUnavailableError` — no delegation for this user) logs at debug;
222
+ * any other failure logs at warn. Both fall back to public-only.
223
+ */
224
+ export function createDefaultSkillsUcanBuilder(): SkillsUcanBuilder {
225
+ return async (skillsServiceUrl, runCtx) => {
226
+ if (!runCtx.ucan.hasSigningKey()) {
227
+ runCtx.logger.debug?.(
228
+ '[skills] no signing key — calling the registry public-only',
229
+ );
230
+ return undefined;
231
+ }
232
+ let skillsDid: string | null;
233
+ try {
234
+ skillsDid = await runCtx.ucan.resolveServiceDid(skillsServiceUrl);
235
+ } catch (err) {
236
+ runCtx.logger.warn(
237
+ `[skills] could not resolve the registry did:web (${skillsServiceUrl}); calling public-only: ${
238
+ err instanceof Error ? err.message : String(err)
239
+ }`,
240
+ );
241
+ return undefined;
242
+ }
243
+ if (!skillsDid) return undefined;
244
+ try {
245
+ return await runCtx.ucan.mintInvocation(
246
+ { did: skillsDid, capability: 'ixo:skills' },
247
+ // Claim the ability the user's delegation actually grants. A `'*'`
248
+ // claim is satisfiable only by a `'*'` grant.
249
+ { can: 'skills/*' },
250
+ );
251
+ } catch (err) {
252
+ if (err instanceof UcanMintUnavailableError) {
253
+ runCtx.logger.debug?.(`[skills] ${err.message}; calling public-only`);
254
+ } else {
255
+ runCtx.logger.warn(
256
+ `[skills] minting the ixo:skills invocation failed; calling public-only: ${
257
+ err instanceof Error ? err.message : String(err)
258
+ }`,
259
+ );
260
+ }
261
+ return undefined;
262
+ }
263
+ };
264
+ }
265
+
266
+ export interface SkillsToolsOptions {
267
+ /** Resolved at boot from the plugin's `configSchema`. */
268
+ baseUrl: string;
269
+ /** Routing hint forwarded to ai-skills as `X-IXO-Network`. */
270
+ network: string;
271
+ /** Mints the optional `ixo:skills` UCAN per call. */
272
+ ucanBuilder: SkillsUcanBuilder;
273
+ }
274
+
275
+ /**
276
+ * Build the two read-only skills tools — `list_skills` and `search_skills`.
277
+ * Both close over the registry base URL but defer UCAN minting to the handler
278
+ * so each call resolves the freshest invocation for the current user.
279
+ */
280
+ export function createSkillsTools(opts: SkillsToolsOptions): PluginTool[] {
281
+ const list = tool(
282
+ async (rawArgs, runCtx) => {
283
+ const params = listSchema.parse(rawArgs);
284
+ const limit = params.limit ?? 20;
285
+ const offset = params.offset ?? 0;
286
+
287
+ const skillsUcan = await opts.ucanBuilder(opts.baseUrl, runCtx);
288
+
289
+ const registryResult = await fetchRegistryCapsules(
290
+ {
291
+ baseUrl: opts.baseUrl,
292
+ network: opts.network,
293
+ skillsUcan,
294
+ signal: runCtx.abortSignal,
295
+ },
296
+ limit,
297
+ offset,
298
+ );
299
+
300
+ const registry = registryResult.capsules.map(normalizeRegistryCapsule);
301
+ const privateRegistry = registry.filter((s) => s.source === 'private');
302
+ const publicRegistry = registry.filter((s) => s.source === 'public');
303
+ const skills: MergedSkill[] = [...privateRegistry, ...publicRegistry];
304
+
305
+ return {
306
+ skills,
307
+ pagination: registryResult.pagination,
308
+ privateSkillCount: privateRegistry.length,
309
+ };
310
+ },
311
+ {
312
+ name: 'list_skills',
313
+ description: LIST_DESCRIPTION,
314
+ schema: listSchema,
315
+ },
316
+ );
317
+
318
+ const search = tool(
319
+ async (rawArgs, runCtx) => {
320
+ const params = searchSchema.parse(rawArgs);
321
+ const limit = params.limit ?? 10;
322
+
323
+ const skillsUcan = await opts.ucanBuilder(opts.baseUrl, runCtx);
324
+
325
+ const capsules = await searchRegistryCapsules(
326
+ {
327
+ baseUrl: opts.baseUrl,
328
+ network: opts.network,
329
+ skillsUcan,
330
+ signal: runCtx.abortSignal,
331
+ },
332
+ params.q,
333
+ limit,
334
+ );
335
+
336
+ const registry = capsules.map(normalizeRegistryCapsule);
337
+ const privateRegistry = registry.filter((s) => s.source === 'private');
338
+ const publicRegistry = registry.filter((s) => s.source === 'public');
339
+ const skills: MergedSkill[] = [...privateRegistry, ...publicRegistry];
340
+
341
+ return {
342
+ query: params.q,
343
+ count: skills.length,
344
+ privateSkillCount: privateRegistry.length,
345
+ skills,
346
+ };
347
+ },
348
+ {
349
+ name: 'search_skills',
350
+ description: SEARCH_DESCRIPTION,
351
+ schema: searchSchema,
352
+ },
353
+ );
354
+
355
+ return [list, search];
356
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod';
2
+ import { OraclePlugin } from '../../../plugin-api/oracle-plugin';
3
+ import type {
4
+ PluginContext,
5
+ PluginManifest,
6
+ PluginTool,
7
+ } from '../../../plugin-api/types';
8
+ import {
9
+ createDefaultSkillsUcanBuilder,
10
+ createSkillsTools,
11
+ type SkillsUcanBuilder,
12
+ } from './skills-tools';
13
+
14
+ const DEFAULT_SKILLS_BASE_URL = 'https://capsules.skills.ixo.earth';
15
+
16
+ const configSchema = z.object({
17
+ SKILLS_CAPSULES_BASE_URL: z.string().url().default(DEFAULT_SKILLS_BASE_URL),
18
+ });
19
+
20
+ /**
21
+ * Sibling env vars read at boot. `NETWORK` is owned by the core base env
22
+ * schema and forwarded to ai-skills as the `X-IXO-Network` routing hint.
23
+ */
24
+ const siblingEnvSchema = z.object({
25
+ NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).optional(),
26
+ });
27
+
28
+ const manifest: PluginManifest = {
29
+ title: 'Skills',
30
+ summary:
31
+ "Discover IXO skill capsules — the caller's published private skills first, then the public registry.",
32
+ whenToUse: [
33
+ 'User asks "what skills are available?" or "what can you do?".',
34
+ 'User asks the agent to find a skill for a specific task ("a skill for invoices", "is there a skill for KYC?").',
35
+ ],
36
+ whenNotToUse: [
37
+ 'Executing a skill — this runtime only lists and searches the registry.',
38
+ ],
39
+ examples: [
40
+ {
41
+ user: 'Do you have a skill that can generate an invoice?',
42
+ thought: 'Skill discovery — search the registry.',
43
+ tool: 'search_skills',
44
+ },
45
+ ],
46
+ tags: ['skills', 'capsules', 'registry', 'ucan'],
47
+ category: 'data',
48
+ visibility: 'always',
49
+ stability: 'stable',
50
+ };
51
+
52
+ export interface SkillsPluginOptions {
53
+ /**
54
+ * Override the UCAN minting helper. Tests inject a stub here so the plugin
55
+ * never touches did:web resolution / the UCAN service; production code
56
+ * lets the default builder do the network work.
57
+ */
58
+ ucanBuilder?: SkillsUcanBuilder;
59
+ }
60
+
61
+ /**
62
+ * Skills plugin — the read-only half of the Node runtime's skills plugin.
63
+ *
64
+ * Exposes `list_skills` and `search_skills` over the IXO skills registry
65
+ * (ai-skills). When the oracle has a signing key, both tools mint an
66
+ * `ixo:skills` UCAN invocation per call so the registry can surface the
67
+ * caller's own published private skills alongside the public ones; without
68
+ * one (`ctx.ucan.hasSigningKey()` is `false`) they call public-only. They
69
+ * never throw on auth issues.
70
+ *
71
+ * Unlike the Node plugin this one has no `dependsOn: ['sandbox']` — skill
72
+ * *execution* (`sandbox_run`) is not part of the Workers runtime.
73
+ */
74
+ export class SkillsPlugin extends OraclePlugin {
75
+ static readonly NAME = 'skills';
76
+
77
+ readonly name = SkillsPlugin.NAME;
78
+ readonly version = '1.0.0';
79
+ readonly manifest = manifest;
80
+ override readonly configSchema = configSchema;
81
+
82
+ private readonly ucanBuilderOverride?: SkillsUcanBuilder;
83
+
84
+ constructor(opts: SkillsPluginOptions = {}) {
85
+ super();
86
+ this.ucanBuilderOverride = opts.ucanBuilder;
87
+ }
88
+
89
+ override getTools(ctx: PluginContext): PluginTool[] {
90
+ const parsed = configSchema.safeParse(ctx.config);
91
+ if (!parsed.success) {
92
+ throw new Error(
93
+ `skills: invalid configuration: ${parsed.error.issues
94
+ .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
95
+ .join('; ')}`,
96
+ );
97
+ }
98
+
99
+ const siblings = siblingEnvSchema.safeParse(ctx.config);
100
+ const network = siblings.success
101
+ ? (siblings.data.NETWORK ?? 'mainnet')
102
+ : 'mainnet';
103
+
104
+ const ucanBuilder =
105
+ this.ucanBuilderOverride ?? createDefaultSkillsUcanBuilder();
106
+
107
+ return createSkillsTools({
108
+ baseUrl: parsed.data.SKILLS_CAPSULES_BASE_URL,
109
+ network,
110
+ ucanBuilder,
111
+ });
112
+ }
113
+ }
@@ -0,0 +1,8 @@
1
+ export { WeatherPlugin, weatherNow } from './weather.plugin';
2
+ export {
3
+ describeWeatherCode,
4
+ getCurrentWeather,
5
+ getForecast,
6
+ } from './weather-client';
7
+ export type { CurrentWeather, Forecast, Units } from './weather-client';
8
+ export type { LastQueryStore, LastWeatherQuery } from './weather-tools';
@@ -0,0 +1,153 @@
1
+ import { z } from 'zod';
2
+
3
+ /** Open-Meteo HTTP helpers. No API key, no SDK — `fetch` + Zod. */
4
+
5
+ export type Units = 'celsius' | 'fahrenheit';
6
+
7
+ const GEOCODE_URL = 'https://geocoding-api.open-meteo.com/v1/search';
8
+ const FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
9
+
10
+ const geoSchema = z.object({
11
+ results: z
12
+ .array(
13
+ z.object({
14
+ latitude: z.number(),
15
+ longitude: z.number(),
16
+ name: z.string(),
17
+ country: z.string().optional(),
18
+ }),
19
+ )
20
+ .optional(),
21
+ });
22
+
23
+ const currentSchema = z.object({
24
+ current_weather: z.object({
25
+ temperature: z.number(),
26
+ windspeed: z.number(),
27
+ weathercode: z.number(),
28
+ }),
29
+ });
30
+
31
+ const forecastSchema = z.object({
32
+ daily: z.object({
33
+ time: z.array(z.string()),
34
+ temperature_2m_max: z.array(z.number()),
35
+ temperature_2m_min: z.array(z.number()),
36
+ weathercode: z.array(z.number()),
37
+ }),
38
+ });
39
+
40
+ /** Common Open-Meteo weathercodes → human label. */
41
+ export function describeWeatherCode(code: number): string {
42
+ if (code === 0) return 'clear';
43
+ if (code >= 1 && code <= 3) return 'partly cloudy';
44
+ if (code >= 45 && code <= 48) return 'fog';
45
+ if (code >= 51 && code <= 67) return 'rain';
46
+ if (code >= 71 && code <= 77) return 'snow';
47
+ if (code >= 80 && code <= 82) return 'showers';
48
+ if (code >= 95 && code <= 99) return 'thunderstorm';
49
+ return 'unknown';
50
+ }
51
+
52
+ async function fetchJson(url: string, signal?: AbortSignal): Promise<unknown> {
53
+ const resp = await fetch(url, { signal });
54
+ if (!resp.ok)
55
+ throw new Error(`Open-Meteo HTTP ${resp.status} ${resp.statusText}`);
56
+ return resp.json();
57
+ }
58
+
59
+ async function geocode(city: string, signal?: AbortSignal) {
60
+ const params = new URLSearchParams({ name: city, count: '1' });
61
+ const data = await fetchJson(`${GEOCODE_URL}?${params}`, signal);
62
+ return geoSchema.parse(data).results?.[0] ?? null;
63
+ }
64
+
65
+ export interface CurrentWeather {
66
+ city: string;
67
+ country: string | undefined;
68
+ latitude: number;
69
+ longitude: number;
70
+ temp: number;
71
+ windKmh: number;
72
+ conditions: string;
73
+ units: Units;
74
+ }
75
+
76
+ export async function getCurrentWeather(
77
+ city: string,
78
+ units: Units,
79
+ signal?: AbortSignal,
80
+ ): Promise<CurrentWeather | null> {
81
+ const loc = await geocode(city, signal);
82
+ if (!loc) return null;
83
+ const params = new URLSearchParams({
84
+ latitude: String(loc.latitude),
85
+ longitude: String(loc.longitude),
86
+ current_weather: 'true',
87
+ timezone: 'auto',
88
+ temperature_unit: units,
89
+ });
90
+ const data = await fetchJson(`${FORECAST_URL}?${params}`, signal);
91
+ const parsed = currentSchema.parse(data);
92
+ return {
93
+ city: loc.name,
94
+ country: loc.country,
95
+ latitude: loc.latitude,
96
+ longitude: loc.longitude,
97
+ temp: parsed.current_weather.temperature,
98
+ windKmh: parsed.current_weather.windspeed,
99
+ conditions: describeWeatherCode(parsed.current_weather.weathercode),
100
+ units,
101
+ };
102
+ }
103
+
104
+ export interface Forecast {
105
+ city: string;
106
+ country: string | undefined;
107
+ latitude: number;
108
+ longitude: number;
109
+ units: Units;
110
+ timezone: string;
111
+ days: Array<{
112
+ date: string;
113
+ tempMax: number;
114
+ tempMin: number;
115
+ conditions: string;
116
+ }>;
117
+ }
118
+
119
+ export async function getForecast(
120
+ city: string,
121
+ days: number,
122
+ units: Units,
123
+ timezone: string,
124
+ signal?: AbortSignal,
125
+ ): Promise<Forecast | null> {
126
+ const cappedDays = Math.max(1, Math.min(7, Math.floor(days)));
127
+ const loc = await geocode(city, signal);
128
+ if (!loc) return null;
129
+ const params = new URLSearchParams({
130
+ latitude: String(loc.latitude),
131
+ longitude: String(loc.longitude),
132
+ daily: 'temperature_2m_max,temperature_2m_min,weathercode',
133
+ forecast_days: String(cappedDays),
134
+ timezone,
135
+ temperature_unit: units,
136
+ });
137
+ const data = await fetchJson(`${FORECAST_URL}?${params}`, signal);
138
+ const parsed = forecastSchema.parse(data);
139
+ return {
140
+ city: loc.name,
141
+ country: loc.country,
142
+ latitude: loc.latitude,
143
+ longitude: loc.longitude,
144
+ units,
145
+ timezone,
146
+ days: parsed.daily.time.map((date, i) => ({
147
+ date,
148
+ tempMax: parsed.daily.temperature_2m_max[i] ?? Number.NaN,
149
+ tempMin: parsed.daily.temperature_2m_min[i] ?? Number.NaN,
150
+ conditions: describeWeatherCode(parsed.daily.weathercode[i] ?? -1),
151
+ })),
152
+ };
153
+ }
@@ -0,0 +1,27 @@
1
+ import { createMiddleware } from 'langchain';
2
+ import type { AgentMiddleware, PluginContext } from '../../../plugin-api/types';
3
+
4
+ /**
5
+ * Tiny observation middleware — logs a line before and after every model
6
+ * call, with elapsed time. Nothing weather-specific; this hook demonstrates
7
+ * that a plugin can attach middleware around the LLM loop.
8
+ *
9
+ * The timer lives on a closure-scoped variable per agent build. Concurrent
10
+ * model calls on the same middleware instance would interleave timings —
11
+ * acceptable for a demo, but a real plugin needing accurate per-call timing
12
+ * would push start times onto a stack or use a per-call id from the runtime.
13
+ */
14
+ export function buildWeatherMiddleware(ctx: PluginContext): AgentMiddleware {
15
+ let startedAt = 0;
16
+ return createMiddleware({
17
+ name: 'WeatherLoggingMiddleware',
18
+ beforeModel: async () => {
19
+ startedAt = Date.now();
20
+ ctx.logger.log('model call started');
21
+ },
22
+ afterModel: async () => {
23
+ const elapsed = startedAt > 0 ? Date.now() - startedAt : -1;
24
+ ctx.logger.log(`model call complete (${elapsed}ms)`);
25
+ },
26
+ });
27
+ }