@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,365 @@
1
+ import { Composio } from '@composio/core';
2
+ import { LangchainProvider } from '@composio/langchain';
3
+ import { z } from 'zod';
4
+ import type { PluginTool, RuntimeContext } from '../../plugin-api/types';
5
+
6
+ /**
7
+ * Minimal shape of a LangChain `DynamicStructuredTool` the composio session
8
+ * returns. The `schema` field is typed broadly because `DynamicStructuredTool`
9
+ * permits either a Zod schema or a raw JSON schema; at runtime the composio
10
+ * `LangchainProvider` always emits Zod (via `jsonSchemaToZodSchema`), but the
11
+ * structural interface stays wide so a non-conforming SDK shape doesn't crash
12
+ * the build. {@link toolSchema} narrows the value when wrapping each entry.
13
+ */
14
+ export interface ComposioSessionTool {
15
+ readonly name: string;
16
+ readonly description: string;
17
+ readonly schema: unknown;
18
+ invoke(input: unknown): Promise<unknown>;
19
+ }
20
+
21
+ /**
22
+ * Factory that opens a Composio session for the current user and returns the
23
+ * dynamic tool list. Injected into the plugin so tests can stub the SDK out
24
+ * entirely.
25
+ */
26
+ export type ComposioSessionFactory = (
27
+ args: ComposioSessionFactoryArgs,
28
+ ) => Promise<ComposioSessionTool[]>;
29
+
30
+ export interface ComposioSessionFactoryArgs {
31
+ /** Resolved at boot from the plugin's configSchema. */
32
+ apiKey: string;
33
+ /** Resolved at boot from the plugin's configSchema. */
34
+ baseUrl: string;
35
+ /** UCAN invocation token, minted per request — forwarded as a header. */
36
+ ucanInvocation: string;
37
+ /** User identifier; composio uses it as the session `user_id`. */
38
+ userId: string;
39
+ /** Optional `x-ixo-network` header value forwarded to composio. */
40
+ network?: string;
41
+ }
42
+
43
+ /**
44
+ * Default {@link ComposioSessionFactory}: opens a real Composio session using
45
+ * `@composio/core` + `@composio/langchain`.
46
+ */
47
+ export function createDefaultComposioSessionFactory(): ComposioSessionFactory {
48
+ return async ({ apiKey, baseUrl, ucanInvocation, userId, network }) => {
49
+ const defaultHeaders: Record<string, string> = {
50
+ 'x-ucan-invocation': ucanInvocation,
51
+ };
52
+ if (network) defaultHeaders['x-ixo-network'] = network;
53
+
54
+ const composio = new Composio({
55
+ apiKey,
56
+ provider: new LangchainProvider(),
57
+ baseURL: baseUrl,
58
+ defaultHeaders,
59
+ });
60
+ const session = await composio.create(userId);
61
+ const tools = await session.tools();
62
+ return tools;
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Tool *definition* — the session-independent slice of a
68
+ * {@link ComposioSessionTool}. The per-user/per-request part (the UCAN
69
+ * invocation header baked into the session) is applied at invoke time.
70
+ */
71
+ export interface ComposioToolDef {
72
+ name: string;
73
+ description: string;
74
+ schema: unknown;
75
+ }
76
+
77
+ export interface CachedComposioDefs {
78
+ defs: ComposioToolDef[];
79
+ expiresAt: number;
80
+ }
81
+
82
+ /** Cache of session tool definitions, keyed per user (see {@link defsCacheKey}). */
83
+ export type ComposioDefsCache = Map<string, CachedComposioDefs>;
84
+
85
+ /**
86
+ * The composio session surface is a stable meta-toolset (search / execute /
87
+ * manage-connections); it shifts only when the user's connected-account state
88
+ * flips. A short TTL keeps that fresh while removing the session-create +
89
+ * tools-list round-trips from nearly every chat turn.
90
+ */
91
+ export const COMPOSIO_TOOL_DEFS_TTL_MS = 5 * 60 * 1000;
92
+
93
+ /**
94
+ * Hard ceiling on cached users. The expired-entry sweep is what bounds the
95
+ * cache in practice (entries outlive their user by at most one TTL); the cap
96
+ * only kicks in if more distinct users than this are active inside a single
97
+ * TTL window, evicting the soonest-to-expire entries first.
98
+ */
99
+ export const COMPOSIO_DEFS_CACHE_MAX_ENTRIES = 1000;
100
+
101
+ function defsCacheKey(baseUrl: string, userId: string): string {
102
+ return `${baseUrl}::${userId}`;
103
+ }
104
+
105
+ /**
106
+ * Cache keys with a background definition refresh in flight — guards a burst
107
+ * of turns from each opening its own session when an entry expires.
108
+ */
109
+ const refreshInFlight = new Set<string>();
110
+
111
+ /**
112
+ * Drop long-expired entries (and, if still over the cap, the
113
+ * soonest-to-expire ones). Runs on every cache write, so in a long-running
114
+ * multi-tenant isolate one-off users' schemas are reclaimed instead of
115
+ * accumulating for the isolate lifetime. Entries get one extra TTL of grace
116
+ * past expiry — a recently-expired entry is still served stale while its
117
+ * background refresh runs, so reclaiming it immediately would put the
118
+ * session-open round-trip back on that user's next turn.
119
+ */
120
+ function pruneDefsCache(cache: ComposioDefsCache, now: number): void {
121
+ for (const [key, entry] of cache) {
122
+ if (entry.expiresAt + COMPOSIO_TOOL_DEFS_TTL_MS <= now) cache.delete(key);
123
+ }
124
+ const surplus = cache.size - COMPOSIO_DEFS_CACHE_MAX_ENTRIES;
125
+ if (surplus <= 0) return;
126
+ const soonestFirst = [...cache.entries()]
127
+ .sort((a, b) => a[1].expiresAt - b[1].expiresAt)
128
+ .slice(0, surplus);
129
+ for (const [key] of soonestFirst) cache.delete(key);
130
+ }
131
+
132
+ export interface CreateComposioToolsOptions {
133
+ apiKey: string;
134
+ baseUrl: string;
135
+ ucanInvocation: string;
136
+ userId: string;
137
+ network?: string;
138
+ /** Override the session factory — defaults to the real SDK client. */
139
+ sessionFactory?: ComposioSessionFactory;
140
+ /**
141
+ * Optional definitions cache (owned by the plugin instance). When provided
142
+ * and warm, the session open is deferred to the first actual tool
143
+ * invocation; when absent, every call opens a session — the pre-cache
144
+ * behaviour.
145
+ */
146
+ defsCache?: ComposioDefsCache;
147
+ /** Logger for background-refresh failures; defaults to silent. */
148
+ logger?: { warn(message: string): void };
149
+ }
150
+
151
+ /**
152
+ * Permissive fallback schema for composio tools whose `schema` field is not
153
+ * a Zod schema (e.g. a raw JSON schema from a non-default provider). Used by
154
+ * {@link toolSchema} when narrowing fails so the agent still sees the tool
155
+ * with a valid descriptor — the composio backend itself validates args.
156
+ */
157
+ const FALLBACK_ARGS_SCHEMA = z.record(z.string(), z.unknown());
158
+
159
+ /**
160
+ * Narrow a composio session tool's `schema` field to a Zod schema.
161
+ *
162
+ * The default `LangchainProvider` always converts the upstream JSON Schema to
163
+ * a Zod schema, so this branch is the hot path. Custom providers that emit
164
+ * raw JSON Schema (rare) fall through to the permissive record schema so the
165
+ * tool is still discoverable.
166
+ */
167
+ function toolSchema(schema: unknown): z.ZodType {
168
+ return schema instanceof z.ZodType ? schema : FALLBACK_ARGS_SCHEMA;
169
+ }
170
+
171
+ /** Composio tool-router meta-tool that batches one or more tool executions. */
172
+ const MULTI_EXECUTE_TOOL = 'COMPOSIO_MULTI_EXECUTE_TOOL';
173
+
174
+ /**
175
+ * Coerce the model's `COMPOSIO_MULTI_EXECUTE_TOOL` input to the exact envelope
176
+ * Composio's (strict) schema accepts: `{ tools, sync_response_to_workbench }`.
177
+ *
178
+ * The multi-execute meta-tool is the one piece of the tool-router flow the
179
+ * model keeps malforming — across runs it has dropped the required
180
+ * `sync_response_to_workbench` control flag and hallucinated extra top-level
181
+ * keys (e.g. a spurious `session`). Prompt guidance reduces but doesn't
182
+ * eliminate this. Rebuilding the envelope here is deterministic:
183
+ *
184
+ * - keep the model's `tools` payload verbatim — the only part it must
185
+ * genuinely author (a genuinely-empty call still fails upstream because
186
+ * `tools` is required, which is correct: there is nothing to run);
187
+ * - default `sync_response_to_workbench` to `false` (return each result
188
+ * inline rather than offloading to the remote workbench) unless the model
189
+ * set it explicitly;
190
+ * - drop every other top-level key.
191
+ *
192
+ * This pins the envelope *shape* — a stable meta-tool contract — not any tool
193
+ * slugs, which live in Composio's registry and must not be hardcoded here.
194
+ */
195
+ function isArgsRecord(value: unknown): value is Record<string, unknown> {
196
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
197
+ }
198
+
199
+ function normalizeArgs(toolName: string, args: unknown): unknown {
200
+ if (toolName !== MULTI_EXECUTE_TOOL || !isArgsRecord(args)) {
201
+ return args;
202
+ }
203
+ return {
204
+ tools: args.tools,
205
+ sync_response_to_workbench:
206
+ typeof args.sync_response_to_workbench === 'boolean'
207
+ ? args.sync_response_to_workbench
208
+ : false,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Wrap a single composio session tool as a {@link PluginTool}. The handler
214
+ * forwards the args through the underlying `invoke` and propagates errors so
215
+ * the agent surfaces a clean failure rather than a silent empty response.
216
+ */
217
+ function wrapAsPluginTool(sessionTool: ComposioSessionTool): PluginTool {
218
+ return {
219
+ name: sessionTool.name,
220
+ description: sessionTool.description,
221
+ schema: toolSchema(sessionTool.schema),
222
+ handler: async (args, ctx: RuntimeContext) => {
223
+ try {
224
+ return await sessionTool.invoke(normalizeArgs(sessionTool.name, args));
225
+ } catch (error) {
226
+ const detail =
227
+ error instanceof Error
228
+ ? `${error.name}: ${error.message}`
229
+ : String(error);
230
+ ctx.logger.error(
231
+ `[composio] tool "${sessionTool.name}" failed: ${detail}`,
232
+ );
233
+ throw error;
234
+ }
235
+ },
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Bind cached definitions to a lazily-opened session. The session (with this
241
+ * request's UCAN invocation) is created on the first actual invocation and
242
+ * shared by every composio tool in the request — turns that never call
243
+ * composio pay zero composio round-trips.
244
+ */
245
+ function buildLazySessionTools(
246
+ defs: ComposioToolDef[],
247
+ sessionArgs: ComposioSessionFactoryArgs,
248
+ sessionFactory: ComposioSessionFactory,
249
+ ): PluginTool[] {
250
+ let sessionByName: Promise<Map<string, ComposioSessionTool>> | null = null;
251
+ const connect = (): Promise<Map<string, ComposioSessionTool>> => {
252
+ if (!sessionByName) {
253
+ sessionByName = sessionFactory(sessionArgs).then(
254
+ (tools) => new Map(tools.map((t) => [t.name, t])),
255
+ );
256
+ // A failed session open must not poison the rest of the run — clear
257
+ // the memo so a later invocation retries with a fresh session.
258
+ sessionByName.catch(() => {
259
+ sessionByName = null;
260
+ });
261
+ }
262
+ return sessionByName;
263
+ };
264
+
265
+ return defs.map((def) =>
266
+ wrapAsPluginTool({
267
+ name: def.name,
268
+ description: def.description,
269
+ schema: def.schema,
270
+ invoke: async (input: unknown) => {
271
+ const byName = await connect();
272
+ const sessionTool = byName.get(def.name);
273
+ if (!sessionTool) {
274
+ throw new Error(
275
+ `composio no longer exposes "${def.name}" — cached definition is stale, retry shortly.`,
276
+ );
277
+ }
278
+ return sessionTool.invoke(input);
279
+ },
280
+ }),
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Build the composio plugin's request-time tool list.
286
+ *
287
+ * 1. Opens a composio session via the configured factory (or, with a warm
288
+ * `defsCache`, defers the open to the first actual invocation).
289
+ * 2. Wraps each returned tool into a {@link PluginTool} that proxies args
290
+ * to the session tool's `invoke`.
291
+ *
292
+ * Returns an empty list if the session factory yields no tools (e.g. the
293
+ * user has no connected accounts and composio omits the management tools).
294
+ */
295
+ export async function createComposioTools(
296
+ opts: CreateComposioToolsOptions,
297
+ ): Promise<PluginTool[]> {
298
+ const sessionFactory =
299
+ opts.sessionFactory ?? createDefaultComposioSessionFactory();
300
+
301
+ const sessionArgs: ComposioSessionFactoryArgs = {
302
+ apiKey: opts.apiKey,
303
+ baseUrl: opts.baseUrl,
304
+ ucanInvocation: opts.ucanInvocation,
305
+ userId: opts.userId,
306
+ network: opts.network,
307
+ };
308
+
309
+ const cacheKey = defsCacheKey(opts.baseUrl, opts.userId);
310
+ const cached = opts.defsCache?.get(cacheKey);
311
+ if (cached) {
312
+ // An expired entry is served as-is (the session meta-toolset only shifts
313
+ // when the user's connected-account state flips) while a background
314
+ // refresh re-snapshots it — TTL expiry never puts the session-open +
315
+ // tools-list round-trips back on a chat turn.
316
+ const defsCache = opts.defsCache;
317
+ if (
318
+ defsCache &&
319
+ cached.expiresAt <= Date.now() &&
320
+ !refreshInFlight.has(cacheKey)
321
+ ) {
322
+ refreshInFlight.add(cacheKey);
323
+ void sessionFactory(sessionArgs)
324
+ .then((sessionTools) => {
325
+ defsCache.set(cacheKey, {
326
+ defs: sessionTools.map(({ name, description, schema }) => ({
327
+ name,
328
+ description,
329
+ schema,
330
+ })),
331
+ expiresAt: Date.now() + COMPOSIO_TOOL_DEFS_TTL_MS,
332
+ });
333
+ pruneDefsCache(defsCache, Date.now());
334
+ })
335
+ .catch((err: unknown) => {
336
+ // Keep serving the stale defs; the next expired-cache turn
337
+ // retries the refresh.
338
+ opts.logger?.warn(
339
+ `[composio] background tool-defs refresh failed: ${err instanceof Error ? err.message : String(err)}`,
340
+ );
341
+ })
342
+ .finally(() => {
343
+ refreshInFlight.delete(cacheKey);
344
+ });
345
+ }
346
+ return buildLazySessionTools(cached.defs, sessionArgs, sessionFactory);
347
+ }
348
+
349
+ const sessionTools = await sessionFactory(sessionArgs);
350
+ if (opts.defsCache) {
351
+ opts.defsCache.set(cacheKey, {
352
+ defs: sessionTools.map(({ name, description, schema }) => ({
353
+ name,
354
+ description,
355
+ schema,
356
+ })),
357
+ expiresAt: Date.now() + COMPOSIO_TOOL_DEFS_TTL_MS,
358
+ });
359
+ // Prune after the write so the cap accounts for the entry just added —
360
+ // the fresh entry has the latest expiry, so it always survives.
361
+ pruneDefsCache(opts.defsCache, Date.now());
362
+ }
363
+
364
+ return sessionTools.map(wrapAsPluginTool);
365
+ }
@@ -0,0 +1,43 @@
1
+ import type { RuntimeContext } from '../../plugin-api/types';
2
+ import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
3
+
4
+ /**
5
+ * Mint a UCAN invocation token addressed to the composio-worker.
6
+ *
7
+ * 1. Resolves the composio-worker DID via `runCtx.ucan.resolveServiceDid`
8
+ * (delegates the did:web lookup + cache to the UCAN service).
9
+ * 2. Mints an `ixo:sandbox` invocation using the user's signing key.
10
+ *
11
+ * Returns `null` when DID resolution fails, when minting throws, or when no
12
+ * signing material is available. Every failure is contained inside the shared
13
+ * UCAN helpers — nothing throws past this boundary. Callers degrade silently:
14
+ * the composio client is simply not constructed and the plugin contributes no
15
+ * tools. The "no delegation present" case logs at `debug`; a genuine
16
+ * resolve/mint failure logs at `warn`.
17
+ */
18
+ export async function mintComposioInvocation(
19
+ runCtx: RuntimeContext,
20
+ composioBaseUrl: string,
21
+ ): Promise<string | null> {
22
+ const composioDid = await resolveServiceDidSafely(
23
+ runCtx,
24
+ composioBaseUrl,
25
+ 'composio',
26
+ );
27
+ if (!composioDid) return null;
28
+
29
+ // skipCache: the composio-worker enforces single-use replay protection
30
+ // per invocation CID (see composio-worker `lib/validation.ts`
31
+ // `createInvocationStore`, KV-backed, 120s default TTL). A cached
32
+ // invocation would be replayed on the second request and rejected with
33
+ // `401 REPLAY: Invocation has already been used`. Mint fresh every call.
34
+ return mintInvocationSafely(
35
+ runCtx,
36
+ { did: composioDid, capability: 'ixo:sandbox' },
37
+ 'composio',
38
+ // can: composio routes through the sandbox capability, and the user's
39
+ // delegation grants `sandbox/*`. A `'*'` claim is satisfiable only by a
40
+ // `'*'` grant — `'*'.startsWith('sandbox/')` is false.
41
+ { skipCache: true, can: 'sandbox/*' },
42
+ );
43
+ }
@@ -0,0 +1,214 @@
1
+ import { z } from 'zod';
2
+ import { OraclePlugin, type PluginEnv } from '../../plugin-api/oracle-plugin';
3
+ import type {
4
+ PluginManifest,
5
+ PluginTool,
6
+ RuntimeContext,
7
+ } from '../../plugin-api/types';
8
+ import {
9
+ createComposioTools,
10
+ type ComposioDefsCache,
11
+ type ComposioSessionFactory,
12
+ } from './composio-tools';
13
+ import { mintComposioInvocation } from './composio-ucan';
14
+
15
+ const configSchema = z.object({
16
+ COMPOSIO_API_KEY: z.string().min(1, 'COMPOSIO_API_KEY must not be empty.'),
17
+ COMPOSIO_BASE_URL: z
18
+ .string()
19
+ .url('COMPOSIO_BASE_URL must be a valid HTTP(S) URL.')
20
+ .default('https://composio.ixo.earth'),
21
+ });
22
+
23
+ /**
24
+ * Sibling env vars the plugin reads but does not own. `NETWORK` is declared
25
+ * in the core base schema; the plugin forwards it as `x-ixo-network` when
26
+ * present so the composio-worker can route to the right IXO environment.
27
+ */
28
+ const siblingEnvSchema = z.object({
29
+ NETWORK: z.string().optional(),
30
+ });
31
+
32
+ const manifest: PluginManifest = {
33
+ title: 'Composio',
34
+ summary:
35
+ 'Hundreds of integrations in one plugin: web search, news, finance, academic research, Gmail, GitHub, Linear, Slack, Google Calendar, Notion, Jira, HubSpot, and much more — all invoked on behalf of the user through Composio. When in doubt, Composio likely has a tool for it.',
36
+ whenToUse: [
37
+ 'Composio exposes only a few directly-callable meta-tools: `COMPOSIO_MANAGE_CONNECTIONS` (auth), `COMPOSIO_SEARCH_TOOLS` (discover), and `COMPOSIO_MULTI_EXECUTE_TOOL` (run). Specific app tools (e.g. `GMAIL_SEND_EMAIL`, `COMPOSIO_SEARCH_FINANCE`) are NOT directly callable — discover them first, then run them through `COMPOSIO_MULTI_EXECUTE_TOOL`.',
38
+ 'Standard flow: (1) for any connected-app action call `COMPOSIO_MANAGE_CONNECTIONS` with the toolkit FIRST — if it returns a `redirect_url`, show it as a clickable markdown link and stop; (2) call `COMPOSIO_SEARCH_TOOLS`, describing the action in natural language, to find the exact tool slug(s); (3) if unsure of a tool’s parameters, call `COMPOSIO_GET_TOOL_SCHEMAS` with those slugs to fetch their exact input schema; (4) call `COMPOSIO_MULTI_EXECUTE_TOOL` to execute.',
39
+ 'CRITICAL — `COMPOSIO_MULTI_EXECUTE_TOOL` args are EXACTLY `{ "tools": [{ "tool_slug": "<SLUG>", "arguments": { ... } }], "sync_response_to_workbench": false }` and nothing else — no `session`, `id`, or other top-level keys. NEVER pass a tool name as a top-level key (e.g. `{ "COMPOSIO_SEARCH_FINANCE": {...} }`); always wrap each call inside the `tools` array as `tool_slug` + `arguments`.',
40
+ 'Web, news, finance, academic, and trend searches — and fetching the readable content of a URL — live in the `COMPOSIO_SEARCH_*` family. They need no connection: discover the one you want via `COMPOSIO_SEARCH_TOOLS`, then run it through `COMPOSIO_MULTI_EXECUTE_TOOL`.',
41
+ 'Email, calendar, issue trackers, docs, CRMs (Gmail, Outlook, Google Calendar, GitHub, Linear, Jira, Notion, Slack, HubSpot, …): verify the connection, discover the tool, then execute.',
42
+ 'No native skill covers the requested action — discover what Composio offers with `COMPOSIO_SEARCH_TOOLS` before giving up.',
43
+ ],
44
+ whenNotToUse: [
45
+ 'A native skill or sub-agent already covers the action more precisely — prefer the skill.',
46
+ 'Scraping a complex page that needs JavaScript rendering or main-content extraction — use the Firecrawl sub-agent instead.',
47
+ 'IXO entity lookups — use the Domain Indexer.',
48
+ 'Normal conversation or general question with no search or external SaaS interaction.',
49
+ 'NEVER write, guess, or fabricate any URL yourself — the only valid auth link is the `redirect_url` returned by `COMPOSIO_MANAGE_CONNECTIONS`. Typing a link from memory is forbidden.',
50
+ ],
51
+ examples: [
52
+ {
53
+ user: 'What is the current Bitcoin price?',
54
+ thought:
55
+ 'A search task — no connection needed. Discover the right search tool first.',
56
+ tool: 'COMPOSIO_SEARCH_TOOLS',
57
+ args: {
58
+ query: 'search the web for live financial / crypto market prices',
59
+ },
60
+ },
61
+ {
62
+ user: '(after COMPOSIO_SEARCH_TOOLS surfaces COMPOSIO_SEARCH_FINANCE)',
63
+ thought:
64
+ 'Run the discovered tool through the multi-execute meta-tool — wrapped in the tools array, never as a top-level key.',
65
+ tool: 'COMPOSIO_MULTI_EXECUTE_TOOL',
66
+ args: {
67
+ tools: [
68
+ {
69
+ tool_slug: 'COMPOSIO_SEARCH_FINANCE',
70
+ arguments: { query: 'Bitcoin price USD today' },
71
+ },
72
+ ],
73
+ sync_response_to_workbench: false,
74
+ },
75
+ },
76
+ {
77
+ user: 'Create a Linear issue for this bug',
78
+ thought:
79
+ 'A SaaS action — verify Linear is connected before discovering or running any Linear tool.',
80
+ tool: 'COMPOSIO_MANAGE_CONNECTIONS',
81
+ args: { toolkit: 'linear' },
82
+ },
83
+ {
84
+ user: 'Send an email to the team',
85
+ thought:
86
+ 'A SaaS action — verify Gmail is connected first, then discover GMAIL_SEND_EMAIL and run it via COMPOSIO_MULTI_EXECUTE_TOOL.',
87
+ tool: 'COMPOSIO_MANAGE_CONNECTIONS',
88
+ args: { toolkit: 'gmail' },
89
+ },
90
+ ],
91
+ tags: [
92
+ 'composio',
93
+ 'integration',
94
+ 'saas',
95
+ 'tools',
96
+ 'web-search',
97
+ 'news',
98
+ 'finance',
99
+ ],
100
+ category: 'integration',
101
+ visibility: 'on-demand',
102
+ stability: 'stable',
103
+ };
104
+
105
+ export interface ComposioPluginOptions {
106
+ /**
107
+ * Override the session factory — primarily for tests so they can skip the
108
+ * real `@composio/core` client and the network call it makes.
109
+ */
110
+ sessionFactory?: ComposioSessionFactory;
111
+ /**
112
+ * Override the UCAN minting step — primarily for tests so they can return
113
+ * a fixed token without invoking the real UCAN service.
114
+ */
115
+ mintInvocation?: (
116
+ runCtx: RuntimeContext,
117
+ baseUrl: string,
118
+ ) => Promise<string | null>;
119
+ }
120
+
121
+ /**
122
+ * Composio plugin.
123
+ *
124
+ * Tools are discovered dynamically per request: the plugin mints a UCAN
125
+ * invocation addressed to the composio-worker, opens a session for the
126
+ * current user, and exposes each returned tool to the agent.
127
+ *
128
+ * Auth is UCAN-only — no Matrix-OpenID fallback. If minting fails (no
129
+ * signing key, no cached delegation, did:web unresolved) the plugin
130
+ * contributes zero tools and the agent simply does not see composio that
131
+ * request. Visibility is `on-demand` so the plugin is discoverable via
132
+ * `list_capabilities` rather than burning prompt budget every call.
133
+ */
134
+ export class ComposioPlugin extends OraclePlugin {
135
+ readonly name = 'composio';
136
+
137
+ readonly version = '1.0.0';
138
+
139
+ readonly manifest = manifest;
140
+
141
+ override readonly configSchema = configSchema;
142
+
143
+ override readonly autoDetectHint = 'COMPOSIO_API_KEY';
144
+
145
+ private readonly sessionFactoryOverride?: ComposioSessionFactory;
146
+ private readonly mintInvocationOverride?: (
147
+ runCtx: RuntimeContext,
148
+ baseUrl: string,
149
+ ) => Promise<string | null>;
150
+
151
+ /**
152
+ * Per-user session tool definitions. Warm entries let `createComposioTools`
153
+ * skip the session-open + tools-list round-trips on the chat hot path and
154
+ * open the session lazily on first invocation instead.
155
+ */
156
+ private readonly toolDefsCache: ComposioDefsCache = new Map();
157
+
158
+ constructor(opts: ComposioPluginOptions = {}) {
159
+ super();
160
+ this.sessionFactoryOverride = opts.sessionFactory;
161
+ this.mintInvocationOverride = opts.mintInvocation;
162
+ }
163
+
164
+ override autoDetect(env: PluginEnv): boolean {
165
+ return (
166
+ typeof env.COMPOSIO_API_KEY === 'string' &&
167
+ env.COMPOSIO_API_KEY.length > 0
168
+ );
169
+ }
170
+
171
+ override async getRequestTools(rtCtx: RuntimeContext): Promise<PluginTool[]> {
172
+ const parsed = configSchema.safeParse(rtCtx.config);
173
+ if (!parsed.success) {
174
+ rtCtx.logger.warn(
175
+ `[composio] skipping — invalid configuration: ${parsed.error.issues
176
+ .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
177
+ .join('; ')}`,
178
+ );
179
+ return [];
180
+ }
181
+
182
+ const siblings = siblingEnvSchema.safeParse(rtCtx.config);
183
+ const network = siblings.success ? siblings.data.NETWORK : undefined;
184
+
185
+ const mint = this.mintInvocationOverride ?? mintComposioInvocation;
186
+ const ucanInvocation = await mint(rtCtx, parsed.data.COMPOSIO_BASE_URL);
187
+ if (!ucanInvocation) {
188
+ rtCtx.logger.warn(
189
+ '[composio] skipping — UCAN invocation could not be minted.',
190
+ );
191
+ return [];
192
+ }
193
+
194
+ try {
195
+ return await createComposioTools({
196
+ apiKey: parsed.data.COMPOSIO_API_KEY,
197
+ baseUrl: parsed.data.COMPOSIO_BASE_URL,
198
+ ucanInvocation,
199
+ userId: rtCtx.user.did,
200
+ network,
201
+ sessionFactory: this.sessionFactoryOverride,
202
+ defsCache: this.toolDefsCache,
203
+ logger: rtCtx.logger,
204
+ });
205
+ } catch (error) {
206
+ const detail =
207
+ error instanceof Error
208
+ ? `${error.name}: ${error.message}`
209
+ : String(error);
210
+ rtCtx.logger.error(`[composio] failed to load tools: ${detail}`);
211
+ return [];
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ COMPOSIO_DEFS_CACHE_MAX_ENTRIES,
3
+ COMPOSIO_TOOL_DEFS_TTL_MS,
4
+ createComposioTools,
5
+ createDefaultComposioSessionFactory,
6
+ type CachedComposioDefs,
7
+ type ComposioDefsCache,
8
+ type ComposioSessionFactory,
9
+ type ComposioSessionFactoryArgs,
10
+ type ComposioSessionTool,
11
+ type ComposioToolDef,
12
+ type CreateComposioToolsOptions,
13
+ } from './composio-tools';
14
+ export { mintComposioInvocation } from './composio-ucan';
15
+ export { ComposioPlugin, type ComposioPluginOptions } from './composio.plugin';
@@ -0,0 +1,57 @@
1
+ import type { PluginSubAgent, PluginTool } from '../../plugin-api/types';
2
+
3
+ const formatToolDocs = (tools: PluginTool[]): string =>
4
+ tools
5
+ .map((t) => {
6
+ const description = t.description?.trim() ?? 'No description provided.';
7
+ return `- \`${t.name}\`: ${description}`;
8
+ })
9
+ .join('\n');
10
+
11
+ const buildSystemPrompt = (tools: PluginTool[]): string =>
12
+ `
13
+ You are the Domain Indexer Agent. You are the specialist for searching the IXO Domain Indexer—treat it like Google for IXO entities (organizations, projects, DAOs, agents, compositions, events).
14
+
15
+ Core expectations:
16
+ - Always clarify the user’s goal and translate it into a concrete search query or DID lookup.
17
+ - Never call tools with empty parameters; \`domain_indexer_search\` always needs a query, \`get_domain_card\` always needs a DID.
18
+ - Explain what you are searching for, summarize the results, and cite relevant DIDs or entity names.
19
+ - When multiple results appear, compare them briefly and suggest next steps.
20
+
21
+ Task discipline:
22
+ - You are a sub-agent invoked by the main agent. You receive a single task message — that is ALL the context you have.
23
+ - If the task is unclear, ambiguous, or missing critical details (IDs, names, scope, what to do), do NOT guess. Instead, STOP immediately and return a clear message explaining what information you need. The main agent will ask the user and re-invoke you with a complete task.
24
+ - Never loop or retry the same failing approach. If something fails twice, return the error and stop.
25
+ - Complete the requested task and STOP. Do not do additional unrequested work.
26
+
27
+ ### Available Domain Indexer Tools
28
+ ${formatToolDocs(tools)}
29
+
30
+ Workflow:
31
+ 1. Decide if you need search (find relevant entities) or a card lookup (get summary/overview/FAQ for a known DID).
32
+ 2. Provide detailed, structured tool inputs (query text, limits, filters, or DID).
33
+ 3. Parse the response—highlight summary, overview, FAQ, URLs, and keywords.
34
+ 4. Surface gaps or follow-ups — if more context is needed (from memory, portal, etc.), say so and the main agent will use the appropriate tools.
35
+ `.trim();
36
+
37
+ const buildDescription = (tools: PluginTool[]): string => {
38
+ const names = tools.map((t) => t.name).join(', ');
39
+ return `Domain Indexer specialist using (${names}) to discover IXO entities, summaries, overviews, and FAQs.`;
40
+ };
41
+
42
+ /**
43
+ * Build the Domain Indexer sub-agent definition. Tools are supplied by the
44
+ * plugin and closed over `DOMAIN_INDEXER_URL` from the merged config.
45
+ */
46
+ export function createDomainIndexerSubAgent(
47
+ tools: PluginTool[],
48
+ ): PluginSubAgent {
49
+ return {
50
+ name: 'Domain Indexer Agent',
51
+ description: buildDescription(tools),
52
+ systemPrompt: buildSystemPrompt(tools),
53
+ tools,
54
+ model: 'subagent',
55
+ middlewares: [],
56
+ };
57
+ }