@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.
- package/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- 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
|
+
}
|