@dual-os/sdk 0.0.0-stage → 0.8.0-next.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 (214) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +19 -2
  3. package/dist/Action.d.ts +201 -0
  4. package/dist/Action.d.ts.map +1 -0
  5. package/dist/Action.js +172 -0
  6. package/dist/Action.js.map +1 -0
  7. package/dist/AgentChannel.d.ts +1717 -0
  8. package/dist/AgentChannel.d.ts.map +1 -0
  9. package/dist/AgentChannel.js +708 -0
  10. package/dist/AgentChannel.js.map +1 -0
  11. package/dist/AgentChannelConformance.d.ts +1543 -0
  12. package/dist/AgentChannelConformance.d.ts.map +1 -0
  13. package/dist/AgentChannelConformance.js +163 -0
  14. package/dist/AgentChannelConformance.js.map +1 -0
  15. package/dist/AgentChannelRuntime.d.ts +78 -0
  16. package/dist/AgentChannelRuntime.d.ts.map +1 -0
  17. package/dist/AgentChannelRuntime.js +270 -0
  18. package/dist/AgentChannelRuntime.js.map +1 -0
  19. package/dist/AiProvider.d.ts +132 -0
  20. package/dist/AiProvider.d.ts.map +1 -0
  21. package/dist/AiProvider.js +100 -0
  22. package/dist/AiProvider.js.map +1 -0
  23. package/dist/AuthMethod.d.ts +381 -0
  24. package/dist/AuthMethod.d.ts.map +1 -0
  25. package/dist/AuthMethod.js +225 -0
  26. package/dist/AuthMethod.js.map +1 -0
  27. package/dist/AuthMethodBinding.d.ts +77 -0
  28. package/dist/AuthMethodBinding.d.ts.map +1 -0
  29. package/dist/AuthMethodBinding.js +31 -0
  30. package/dist/AuthMethodBinding.js.map +1 -0
  31. package/dist/AuthMethodExecution.d.ts +223 -0
  32. package/dist/AuthMethodExecution.d.ts.map +1 -0
  33. package/dist/AuthMethodExecution.js +277 -0
  34. package/dist/AuthMethodExecution.js.map +1 -0
  35. package/dist/ConnectionResolver.d.ts +219 -0
  36. package/dist/ConnectionResolver.d.ts.map +1 -0
  37. package/dist/ConnectionResolver.js +153 -0
  38. package/dist/ConnectionResolver.js.map +1 -0
  39. package/dist/DefinitionError.d.ts +67 -0
  40. package/dist/DefinitionError.d.ts.map +1 -0
  41. package/dist/DefinitionError.js +82 -0
  42. package/dist/DefinitionError.js.map +1 -0
  43. package/dist/DualRegistry.d.ts +71 -0
  44. package/dist/DualRegistry.d.ts.map +1 -0
  45. package/dist/DualRegistry.js +96 -0
  46. package/dist/DualRegistry.js.map +1 -0
  47. package/dist/DualRuntime.d.ts +279 -0
  48. package/dist/DualRuntime.d.ts.map +1 -0
  49. package/dist/DualRuntime.js +311 -0
  50. package/dist/DualRuntime.js.map +1 -0
  51. package/dist/Files.d.ts +80 -0
  52. package/dist/Files.d.ts.map +1 -0
  53. package/dist/Files.js +66 -0
  54. package/dist/Files.js.map +1 -0
  55. package/dist/Group.d.ts +106 -0
  56. package/dist/Group.d.ts.map +1 -0
  57. package/dist/Group.js +86 -0
  58. package/dist/Group.js.map +1 -0
  59. package/dist/Http.d.ts +342 -0
  60. package/dist/Http.d.ts.map +1 -0
  61. package/dist/Http.js +677 -0
  62. package/dist/Http.js.map +1 -0
  63. package/dist/Mcp.d.ts +109 -0
  64. package/dist/Mcp.d.ts.map +1 -0
  65. package/dist/Mcp.js +176 -0
  66. package/dist/Mcp.js.map +1 -0
  67. package/dist/OAuth2TokenRefresher.d.ts +18 -0
  68. package/dist/OAuth2TokenRefresher.d.ts.map +1 -0
  69. package/dist/OAuth2TokenRefresher.js +19 -0
  70. package/dist/OAuth2TokenRefresher.js.map +1 -0
  71. package/dist/OAuthFlow.d.ts +71 -0
  72. package/dist/OAuthFlow.d.ts.map +1 -0
  73. package/dist/OAuthFlow.js +488 -0
  74. package/dist/OAuthFlow.js.map +1 -0
  75. package/dist/Plugin.d.ts +383 -0
  76. package/dist/Plugin.d.ts.map +1 -0
  77. package/dist/Plugin.js +249 -0
  78. package/dist/Plugin.js.map +1 -0
  79. package/dist/PluginRuntime.d.ts +3186 -0
  80. package/dist/PluginRuntime.d.ts.map +1 -0
  81. package/dist/PluginRuntime.js +331 -0
  82. package/dist/PluginRuntime.js.map +1 -0
  83. package/dist/Runner.d.ts +1 -0
  84. package/dist/Runner.js +124 -0
  85. package/dist/Runner.js.map +1 -0
  86. package/dist/ToolFile.d.ts +23 -0
  87. package/dist/ToolFile.d.ts.map +1 -0
  88. package/dist/ToolFile.js +33 -0
  89. package/dist/ToolFile.js.map +1 -0
  90. package/dist/Trigger.d.ts +358 -0
  91. package/dist/Trigger.d.ts.map +1 -0
  92. package/dist/Trigger.js +287 -0
  93. package/dist/Trigger.js.map +1 -0
  94. package/dist/_virtual/_rolldown/runtime.js +13 -0
  95. package/dist/index.d.ts +24 -0
  96. package/dist/index.js +24 -0
  97. package/dist/internal/auth-context.d.ts +23 -0
  98. package/dist/internal/auth-context.d.ts.map +1 -0
  99. package/dist/internal/auth-context.js +1 -0
  100. package/dist/internal/auth-execution.d.ts +27 -0
  101. package/dist/internal/auth-execution.d.ts.map +1 -0
  102. package/dist/internal/auth-execution.js +66 -0
  103. package/dist/internal/auth-execution.js.map +1 -0
  104. package/dist/internal/auth-runtime.d.ts +5 -0
  105. package/dist/internal/auth-runtime.d.ts.map +1 -0
  106. package/dist/internal/auth-runtime.js +16 -0
  107. package/dist/internal/auth-runtime.js.map +1 -0
  108. package/dist/internal/capabilities.d.ts +24 -0
  109. package/dist/internal/capabilities.d.ts.map +1 -0
  110. package/dist/internal/capabilities.js +22 -0
  111. package/dist/internal/capabilities.js.map +1 -0
  112. package/dist/internal/children.d.ts +34 -0
  113. package/dist/internal/children.d.ts.map +1 -0
  114. package/dist/internal/children.js +53 -0
  115. package/dist/internal/children.js.map +1 -0
  116. package/dist/internal/collection-utils.d.ts +4 -0
  117. package/dist/internal/collection-utils.d.ts.map +1 -0
  118. package/dist/internal/collection-utils.js +6 -0
  119. package/dist/internal/collection-utils.js.map +1 -0
  120. package/dist/internal/dual/actions.d.ts +33 -0
  121. package/dist/internal/dual/actions.d.ts.map +1 -0
  122. package/dist/internal/dual/actions.js +26 -0
  123. package/dist/internal/dual/actions.js.map +1 -0
  124. package/dist/internal/dual/invoke.d.ts +63 -0
  125. package/dist/internal/dual/invoke.d.ts.map +1 -0
  126. package/dist/internal/dual/invoke.js +75 -0
  127. package/dist/internal/dual/invoke.js.map +1 -0
  128. package/dist/internal/dual/registry.d.ts +125 -0
  129. package/dist/internal/dual/registry.d.ts.map +1 -0
  130. package/dist/internal/dual/registry.js +172 -0
  131. package/dist/internal/dual/registry.js.map +1 -0
  132. package/dist/internal/dual/triggers.d.ts +29 -0
  133. package/dist/internal/dual/triggers.d.ts.map +1 -0
  134. package/dist/internal/dual/triggers.js +26 -0
  135. package/dist/internal/dual/triggers.js.map +1 -0
  136. package/dist/internal/handler-context.d.ts +9 -0
  137. package/dist/internal/handler-context.d.ts.map +1 -0
  138. package/dist/internal/handler-context.js +31 -0
  139. package/dist/internal/handler-context.js.map +1 -0
  140. package/dist/internal/identifier.d.ts +6 -0
  141. package/dist/internal/identifier.d.ts.map +1 -0
  142. package/dist/internal/identifier.js +28 -0
  143. package/dist/internal/identifier.js.map +1 -0
  144. package/dist/internal/plugin-handlers.d.ts +38 -0
  145. package/dist/internal/plugin-handlers.d.ts.map +1 -0
  146. package/dist/internal/plugin-handlers.js +52 -0
  147. package/dist/internal/plugin-handlers.js.map +1 -0
  148. package/dist/internal/proto.d.ts +7 -0
  149. package/dist/internal/proto.d.ts.map +1 -0
  150. package/dist/internal/proto.js +10 -0
  151. package/dist/internal/proto.js.map +1 -0
  152. package/dist/internal/schema-runtime.d.ts +6 -0
  153. package/dist/internal/schema-runtime.d.ts.map +1 -0
  154. package/dist/internal/schema-runtime.js +10 -0
  155. package/dist/internal/schema-runtime.js.map +1 -0
  156. package/dist/internal/trigger-runtime-shared.d.ts +41 -0
  157. package/dist/internal/trigger-runtime-shared.d.ts.map +1 -0
  158. package/dist/internal/trigger-runtime-shared.js +132 -0
  159. package/dist/internal/trigger-runtime-shared.js.map +1 -0
  160. package/dist/internal/trigger-runtime.d.ts +12 -0
  161. package/dist/internal/trigger-runtime.d.ts.map +1 -0
  162. package/dist/internal/trigger-runtime.js +95 -0
  163. package/dist/internal/trigger-runtime.js.map +1 -0
  164. package/dist/internal/types.d.ts +7 -0
  165. package/dist/internal/types.d.ts.map +1 -0
  166. package/dist/internal/types.js +1 -0
  167. package/dist/remote-runtime.d.ts +585 -0
  168. package/dist/remote-runtime.d.ts.map +1 -0
  169. package/dist/remote-runtime.js +367 -0
  170. package/dist/remote-runtime.js.map +1 -0
  171. package/package.json +55 -4
  172. package/src/Action.ts +370 -0
  173. package/src/AgentChannel.ts +860 -0
  174. package/src/AgentChannelConformance.ts +124 -0
  175. package/src/AgentChannelRuntime.ts +616 -0
  176. package/src/AiProvider.ts +255 -0
  177. package/src/AuthMethod.ts +954 -0
  178. package/src/AuthMethodBinding.ts +112 -0
  179. package/src/AuthMethodExecution.ts +542 -0
  180. package/src/ConnectionResolver.ts +265 -0
  181. package/src/DefinitionError.ts +115 -0
  182. package/src/DualRegistry.ts +152 -0
  183. package/src/DualRuntime.ts +1081 -0
  184. package/src/Files.ts +89 -0
  185. package/src/Group.ts +292 -0
  186. package/src/Http.ts +1205 -0
  187. package/src/Mcp.ts +299 -0
  188. package/src/OAuth2TokenRefresher.ts +22 -0
  189. package/src/OAuthFlow.ts +874 -0
  190. package/src/Plugin.ts +912 -0
  191. package/src/PluginRuntime.ts +414 -0
  192. package/src/Runner.ts +248 -0
  193. package/src/ToolFile.ts +28 -0
  194. package/src/Trigger.ts +722 -0
  195. package/src/index.ts +158 -0
  196. package/src/internal/auth-context.ts +27 -0
  197. package/src/internal/auth-execution.ts +241 -0
  198. package/src/internal/auth-runtime.ts +20 -0
  199. package/src/internal/capabilities.ts +55 -0
  200. package/src/internal/children.ts +126 -0
  201. package/src/internal/collection-utils.ts +8 -0
  202. package/src/internal/dual/actions.ts +116 -0
  203. package/src/internal/dual/invoke.ts +307 -0
  204. package/src/internal/dual/registry.ts +340 -0
  205. package/src/internal/dual/triggers.ts +128 -0
  206. package/src/internal/handler-context.ts +57 -0
  207. package/src/internal/identifier.ts +27 -0
  208. package/src/internal/plugin-handlers.ts +146 -0
  209. package/src/internal/proto.ts +8 -0
  210. package/src/internal/schema-runtime.ts +15 -0
  211. package/src/internal/trigger-runtime-shared.ts +449 -0
  212. package/src/internal/trigger-runtime.ts +223 -0
  213. package/src/internal/types.ts +12 -0
  214. package/src/remote-runtime.ts +469 -0
@@ -0,0 +1,340 @@
1
+ /** Materialized Dual registry DTOs and resolver. */
2
+ import { Schema } from "effect";
3
+
4
+ import * as AgentChannel from "../../AgentChannel.ts";
5
+ import * as AiProvider from "../../AiProvider.ts";
6
+ import type * as AuthMethod from "../../AuthMethod.ts";
7
+ import type { PluginInstallation } from "../../DualRuntime.ts";
8
+ import type * as Plugin from "../../Plugin.ts";
9
+
10
+ import { collectActionEntries } from "./actions.ts";
11
+ import { collectTriggerEntries } from "./triggers.ts";
12
+
13
+ export interface MaterializedRegistryOptions {
14
+ readonly schemaJson?: ((schema: Schema.Top) => unknown) | undefined;
15
+ readonly projections?: Map<Schema.Top, unknown> | undefined;
16
+ /** Selects the capability versions whose schemas are projected; the others carry `undefined` schemas. */
17
+ readonly projectSchemas?: ((capabilityId: string, version: string) => boolean) | undefined;
18
+ }
19
+
20
+ /** @since 0.0.0 @category registry */
21
+ export interface MaterializedAuthMethod {
22
+ readonly id: string;
23
+ readonly providerId: string;
24
+ readonly key: string;
25
+ readonly name: string | undefined;
26
+ readonly description: string | undefined;
27
+ readonly kind: AuthMethod.AuthMethodKind;
28
+ readonly setupSchemaJson: unknown | null;
29
+ readonly inputSchemaJson: unknown | null;
30
+ readonly materialSchemaJson: unknown;
31
+ readonly metadataJson: unknown;
32
+ }
33
+
34
+ /** @since 0.0.0 @category registry */
35
+ export interface MaterializedProvider {
36
+ readonly kind: "tools" | "ai";
37
+ readonly id: string;
38
+ readonly key: string;
39
+ readonly name: string | undefined;
40
+ readonly description: string | undefined;
41
+ readonly iconUrl: string | undefined;
42
+ readonly iconUrlDark: string | undefined;
43
+ readonly categories: readonly Plugin.Category[];
44
+ readonly homepageUrl: string | undefined;
45
+ readonly docsUrl: string | undefined;
46
+ /** Whether a shell in a run sandbox may use the provider's Connections through Dual's gateway. */
47
+ readonly sandboxAccess: boolean;
48
+ readonly sourcePluginKey: string | undefined;
49
+ readonly sourcePluginVersion: string | undefined;
50
+ readonly authMethods: readonly MaterializedAuthMethod[];
51
+ readonly capabilityVersions: readonly MaterializedCapabilityVersion[];
52
+ }
53
+
54
+ /** @since 0.0.0 @category registry */
55
+ export interface MaterializedActionCapabilityVersion {
56
+ readonly kind: "action";
57
+ readonly requireAuth: boolean;
58
+ readonly capabilityId: string;
59
+ readonly key: string;
60
+ readonly name: string | undefined;
61
+ readonly description: string | undefined;
62
+ readonly version: string;
63
+ readonly sourcePluginKey: string | undefined;
64
+ readonly sourcePluginVersion: string | undefined;
65
+ readonly authMethodIds: readonly string[];
66
+ readonly inputSchemaJson: unknown;
67
+ readonly outputSchemaJson: unknown;
68
+ readonly examplesJson: unknown;
69
+ }
70
+
71
+ /** @since 0.0.0 @category registry */
72
+ export interface MaterializedTriggerCapabilityVersion {
73
+ readonly kind: "trigger";
74
+ readonly requireAuth: boolean;
75
+ readonly capabilityId: string;
76
+ readonly key: string;
77
+ readonly name: string | undefined;
78
+ readonly description: string | undefined;
79
+ readonly version: string;
80
+ readonly sourcePluginKey: string | undefined;
81
+ readonly sourcePluginVersion: string | undefined;
82
+ readonly authMethodIds: readonly string[];
83
+ readonly triggerKind: "webhook" | "polling";
84
+ readonly configSchemaJson: unknown;
85
+ readonly outputSchemaJson: unknown;
86
+ readonly stateSchemaJson: unknown | undefined;
87
+ }
88
+
89
+ /** @since 0.0.0 @category registry */
90
+ export interface MaterializedChannelCapabilityVersion {
91
+ readonly kind: "agent_channel";
92
+ readonly requireAuth: boolean;
93
+ readonly capabilityId: string;
94
+ readonly key: string;
95
+ readonly name: string | undefined;
96
+ readonly description: string | undefined;
97
+ readonly version: string;
98
+ readonly sourcePluginKey: string | undefined;
99
+ readonly sourcePluginVersion: string | undefined;
100
+ readonly authMethodIds: readonly string[];
101
+ readonly definitionSettingsSchemaJson: unknown;
102
+ readonly deploymentSettingsSchemaJson: unknown;
103
+ readonly stateSchemaJson: unknown;
104
+ readonly operationInputSchemaJson: unknown;
105
+ readonly operationOutputSchemaJson: unknown;
106
+ readonly extensionsSchemaJson: Readonly<Record<string, unknown>>;
107
+ readonly featuresJson: AgentChannel.Features;
108
+ readonly exposure: typeof AgentChannel.Exposure.Type;
109
+ /** Absent for the original protocol. */
110
+ readonly protocol?: AgentChannel.Protocol;
111
+ readonly requiredChecks: typeof AgentChannel.RequiredChecks.Type;
112
+ }
113
+ export type MaterializedCapabilityVersion =
114
+ | MaterializedChannelCapabilityVersion
115
+ | MaterializedActionCapabilityVersion
116
+ | MaterializedTriggerCapabilityVersion;
117
+
118
+ /** @since 0.0.0 @category registry */
119
+ export interface MaterializedRegistry {
120
+ readonly dualIdentifier: string;
121
+ readonly providers: readonly MaterializedProvider[];
122
+ }
123
+
124
+ const authMethodId = (providerId: string, methodId: string): string => `${providerId}/${methodId}`;
125
+
126
+ // FIXME(live-schema-validation): this JSON Schema is a lossy catalog/snapshot projection.
127
+ // Hard validation must use the live SDK Schema declarations for the pinned version.
128
+ const schemaJson = (schema: Schema.Top): unknown =>
129
+ Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" });
130
+
131
+ const authMethodMetadataJson = (method: AuthMethod.Any): unknown => {
132
+ if (method.oauth2 === undefined) {
133
+ return {};
134
+ }
135
+
136
+ const staticClient =
137
+ typeof method.oauth2.client === "function" ? undefined : method.oauth2.client;
138
+
139
+ // An endpoint derived from the setup has no value until a connection supplies
140
+ // one, so the registry — which describes the method, not a connection — omits
141
+ // it, exactly as it already omits a client built by a hook.
142
+ const staticEndpoint = (endpoint: string | ((...args: never) => string) | undefined) =>
143
+ typeof endpoint === "function" ? undefined : endpoint;
144
+ const discoveryUrl = staticEndpoint(method.oauth2.discoveryUrl);
145
+ const authorizationUrl = staticEndpoint(method.oauth2.authorizationUrl);
146
+ const tokenUrl = staticEndpoint(method.oauth2.tokenUrl);
147
+
148
+ return {
149
+ oauth2: {
150
+ scopes: method.oauth2.scopes,
151
+ ...(staticClient === undefined
152
+ ? {}
153
+ : { clientAuthentication: staticClient.authentication ?? "basic" }),
154
+ ...(discoveryUrl === undefined ? {} : { discoveryUrl }),
155
+ ...(authorizationUrl === undefined ? {} : { authorizationUrl }),
156
+ ...(tokenUrl === undefined ? {} : { tokenUrl }),
157
+ ...(method.oauth2.pkce === undefined ? {} : { pkce: method.oauth2.pkce }),
158
+ ...(method.oauth2.authorizationParams === undefined
159
+ ? {}
160
+ : { authorizationParams: method.oauth2.authorizationParams }),
161
+ ...(method.oauth2.tokenParams === undefined
162
+ ? {}
163
+ : { tokenParams: method.oauth2.tokenParams }),
164
+ },
165
+ };
166
+ };
167
+
168
+ /**
169
+ * Materialize the provider/capability registry of the installed plugin releases. This is the
170
+ * canonical SDK-owned resolver used by server persistence/bootstrap code.
171
+ *
172
+ * @since 0.0.0
173
+ * @category registry
174
+ */
175
+ export const materializeResolvedRegistry = (input: {
176
+ readonly dualIdentifier: string;
177
+ readonly plugins: readonly PluginInstallation[];
178
+ readonly aiProviders: readonly AiProvider.Any[];
179
+ readonly schemaJson?: ((schema: Schema.Top) => unknown) | undefined;
180
+ readonly projections?: Map<Schema.Top, unknown> | undefined;
181
+ readonly projectSchemas?: ((capabilityId: string, version: string) => boolean) | undefined;
182
+ }): MaterializedRegistry => {
183
+ // Plugins reuse schema values across capabilities, auth methods and app
184
+ // versions, so project each one once per materialization. The memo is dropped
185
+ // with the caller's materialization rather than retaining every projection for
186
+ // the lifetime of the app definition.
187
+ const projectedSchemas = input.projections ?? new Map<Schema.Top, unknown>();
188
+ const projectSchemaUncached = input.schemaJson ?? schemaJson;
189
+ const projectSchema = (schema: Schema.Top): unknown => {
190
+ if (projectedSchemas.has(schema)) return projectedSchemas.get(schema);
191
+ const projected = projectSchemaUncached(schema);
192
+ projectedSchemas.set(schema, projected);
193
+ return projected;
194
+ };
195
+ const projectSchemasFor = input.projectSchemas ?? (() => true);
196
+ const capabilityVersionsByProvider = new Map<string, MaterializedCapabilityVersion[]>();
197
+ const addCapabilityVersion = (
198
+ providerId: string,
199
+ capabilityVersion: MaterializedCapabilityVersion,
200
+ ) => {
201
+ const versions = capabilityVersionsByProvider.get(providerId) ?? [];
202
+ versions.push(capabilityVersion);
203
+ capabilityVersionsByProvider.set(providerId, versions);
204
+ };
205
+
206
+ for (const entry of collectActionEntries(input.plugins)) {
207
+ const projected = projectSchemasFor(entry.actionId, entry.pluginVersion);
208
+ addCapabilityVersion(entry.providerId, {
209
+ authMethodIds: entry.authMethods.map(method =>
210
+ authMethodId(entry.providerId, method.identifier),
211
+ ),
212
+ capabilityId: entry.actionId,
213
+ description: entry.action.description,
214
+ examplesJson: entry.action.examples,
215
+ inputSchemaJson: projected ? projectSchema(entry.action.input) : undefined,
216
+ key: entry.localId,
217
+ kind: "action" as const,
218
+ requireAuth: entry.action.requireAuth !== false,
219
+ name: entry.action.name,
220
+ outputSchemaJson: projected ? projectSchema(entry.action.output) : undefined,
221
+ sourcePluginKey: entry.providerId,
222
+ sourcePluginVersion: entry.pluginVersion,
223
+ version: entry.pluginVersion,
224
+ });
225
+ }
226
+
227
+ for (const entry of collectTriggerEntries(input.plugins)) {
228
+ const projected = projectSchemasFor(entry.triggerId, entry.pluginVersion);
229
+ addCapabilityVersion(entry.providerId, {
230
+ authMethodIds: entry.authMethods.map(method =>
231
+ authMethodId(entry.providerId, method.identifier),
232
+ ),
233
+ capabilityId: entry.triggerId,
234
+ configSchemaJson: projected ? projectSchema(entry.trigger.config) : undefined,
235
+ description: entry.trigger.description,
236
+ key: entry.localId,
237
+ kind: "trigger" as const,
238
+ requireAuth: entry.trigger.requireAuth !== false,
239
+ name: entry.trigger.name,
240
+ outputSchemaJson: projected ? projectSchema(entry.trigger.output) : undefined,
241
+ sourcePluginKey: entry.providerId,
242
+ sourcePluginVersion: entry.pluginVersion,
243
+ stateSchemaJson:
244
+ projected && entry.trigger.state !== undefined
245
+ ? projectSchema(entry.trigger.state)
246
+ : undefined,
247
+ triggerKind: entry.trigger.kind,
248
+ version: entry.pluginVersion,
249
+ });
250
+ }
251
+
252
+ for (const { plugin, version } of input.plugins) {
253
+ for (const channel of plugin.agentChannels) {
254
+ const definition = channel.definition;
255
+ AgentChannel.validateDefinition(definition, plugin.identifier, plugin.authMethods);
256
+ addCapabilityVersion(plugin.identifier, {
257
+ authMethodIds: definition.authMethods.map(method =>
258
+ authMethodId(plugin.identifier, method.identifier),
259
+ ),
260
+ capabilityId: `${plugin.identifier}/${channel.identifier}`,
261
+ description: channel.description,
262
+ key: channel.identifier,
263
+ kind: "agent_channel",
264
+ requireAuth: true,
265
+ name: channel.name,
266
+ sourcePluginKey: plugin.identifier,
267
+ sourcePluginVersion: version,
268
+ version,
269
+ ...AgentChannel.projectContract(definition, projectSchema),
270
+ });
271
+ }
272
+ }
273
+ const toolProviders: readonly MaterializedProvider[] = input.plugins.map(
274
+ ({ plugin, version }) => ({
275
+ authMethods: plugin.authMethods.map(method => ({
276
+ description: method.description,
277
+ id: authMethodId(plugin.identifier, method.identifier),
278
+ inputSchemaJson: method.input === undefined ? null : projectSchema(method.input),
279
+ key: method.identifier,
280
+ kind: method.kind,
281
+ materialSchemaJson: projectSchema(method.material),
282
+ metadataJson: authMethodMetadataJson(method),
283
+ name: method.name,
284
+ providerId: plugin.identifier,
285
+ setupSchemaJson: method.setup === undefined ? null : projectSchema(method.setup),
286
+ })),
287
+ capabilityVersions: capabilityVersionsByProvider.get(plugin.identifier) ?? [],
288
+ categories: plugin.categories,
289
+ description: plugin.description,
290
+ docsUrl: plugin.docsUrl,
291
+ homepageUrl: plugin.homepageUrl,
292
+ iconUrl: plugin.iconUrl,
293
+ iconUrlDark: plugin.iconUrlDark,
294
+ id: plugin.identifier,
295
+ key: plugin.identifier,
296
+ kind: "tools" as const,
297
+ name: plugin.name,
298
+ sandboxAccess: plugin.sandboxAccess !== undefined,
299
+ sourcePluginKey: plugin.identifier,
300
+ sourcePluginVersion: version,
301
+ }),
302
+ );
303
+
304
+ const aiProviders: readonly MaterializedProvider[] = input.aiProviders.flatMap(provider => {
305
+ const id = AiProvider.registryId(provider.identifier);
306
+
307
+ return [
308
+ {
309
+ authMethods: provider.authMethods.map(method => ({
310
+ description: method.description,
311
+ id: authMethodId(id, method.identifier),
312
+ inputSchemaJson: method.input === undefined ? null : projectSchema(method.input),
313
+ key: method.identifier,
314
+ kind: method.kind,
315
+ materialSchemaJson: projectSchema(method.material),
316
+ metadataJson: authMethodMetadataJson(method),
317
+ name: method.name,
318
+ providerId: id,
319
+ setupSchemaJson: method.setup === undefined ? null : projectSchema(method.setup),
320
+ })),
321
+ capabilityVersions: [],
322
+ categories: provider.categories,
323
+ description: provider.description,
324
+ docsUrl: provider.docsUrl,
325
+ homepageUrl: provider.homepageUrl,
326
+ iconUrl: provider.iconUrl,
327
+ iconUrlDark: provider.iconUrlDark,
328
+ id,
329
+ key: provider.identifier,
330
+ kind: "ai" as const,
331
+ name: provider.name,
332
+ sandboxAccess: false,
333
+ sourcePluginKey: undefined,
334
+ sourcePluginVersion: undefined,
335
+ },
336
+ ];
337
+ });
338
+
339
+ return { dualIdentifier: input.dualIdentifier, providers: [...toolProviders, ...aiProviders] };
340
+ };
@@ -0,0 +1,128 @@
1
+ import type * as AuthMethod from "../../AuthMethod.ts";
2
+ import type { PluginInstallation } from "../../DualRuntime.ts";
3
+ import type * as Group from "../../Group.ts";
4
+ import type * as Plugin from "../../Plugin.ts";
5
+ import type * as Trigger from "../../Trigger.ts";
6
+ import { walkCapabilities } from "../capabilities.ts";
7
+ import type { MachineIdSeparator } from "../identifier.ts";
8
+ import type { IsAny, IsGenericArray } from "../types.ts";
9
+
10
+ export interface TriggerEntry<
11
+ Id extends string = string,
12
+ ProviderId extends string = string,
13
+ TriggerDef extends Trigger.Any = Trigger.Any,
14
+ AllowedMethods extends readonly AuthMethod.Any[] = readonly AuthMethod.Any[],
15
+ > {
16
+ readonly triggerId: Id;
17
+ readonly providerId: ProviderId;
18
+ readonly trigger: TriggerDef;
19
+ readonly authMethods: AllowedMethods;
20
+ }
21
+
22
+ type TriggerEntriesFromTriggers<
23
+ Triggers extends readonly Trigger.Any[],
24
+ ProviderId extends string,
25
+ Prefix extends string,
26
+ AllowedMethods extends readonly AuthMethod.Any[],
27
+ > =
28
+ IsGenericArray<Triggers> extends true
29
+ ? TriggerEntry<string, ProviderId, Trigger.Any, AllowedMethods>
30
+ : Triggers[number] extends infer TriggerDef
31
+ ? TriggerDef extends Trigger.Any
32
+ ? TriggerEntry<
33
+ `${Prefix}${typeof MachineIdSeparator}${TriggerDef["identifier"]}`,
34
+ ProviderId,
35
+ TriggerDef,
36
+ AllowedMethods
37
+ >
38
+ : never
39
+ : never;
40
+
41
+ type TriggerEntriesFromGroups<
42
+ Groups extends readonly Group.Any[],
43
+ ProviderId extends string,
44
+ Prefix extends string,
45
+ AllowedMethods extends readonly AuthMethod.Any[],
46
+ > =
47
+ IsGenericArray<Groups> extends true
48
+ ? TriggerEntry<string, ProviderId, Trigger.Any, AllowedMethods>
49
+ : Groups[number] extends infer GroupDef
50
+ ? GroupDef extends Group.Any
51
+ ? GroupTriggerEntries<
52
+ GroupDef,
53
+ ProviderId,
54
+ `${Prefix}${typeof MachineIdSeparator}${GroupDef["identifier"]}`,
55
+ AllowedMethods
56
+ >
57
+ : never
58
+ : never;
59
+
60
+ type GroupTriggerEntries<
61
+ GroupDef extends Group.Any,
62
+ ProviderId extends string,
63
+ Prefix extends string,
64
+ AllowedMethods extends readonly AuthMethod.Any[],
65
+ > =
66
+ | TriggerEntriesFromTriggers<GroupDef["triggers"], ProviderId, Prefix, AllowedMethods>
67
+ | TriggerEntriesFromGroups<GroupDef["groups"], ProviderId, Prefix, AllowedMethods>;
68
+
69
+ type PluginTriggerEntries<P extends Plugin.Any> =
70
+ | TriggerEntriesFromTriggers<P["triggers"], P["identifier"], P["identifier"], P["authMethods"]>
71
+ | TriggerEntriesFromGroups<P["groups"], P["identifier"], P["identifier"], P["authMethods"]>;
72
+
73
+ export type AllTriggerEntries<Plugins extends readonly Plugin.Any[]> =
74
+ IsAny<Plugins> extends true
75
+ ? TriggerEntry
76
+ : IsGenericArray<Plugins> extends true
77
+ ? TriggerEntry
78
+ : Plugins[number] extends infer P
79
+ ? P extends Plugin.Any
80
+ ? PluginTriggerEntries<P>
81
+ : never
82
+ : never;
83
+
84
+ export interface RuntimeTriggerEntry<
85
+ out TriggerDef extends Trigger.Any = Trigger.Any,
86
+ out AllowedMethods extends readonly AuthMethod.Any[] = readonly AuthMethod.Any[],
87
+ out TriggerId extends string = string,
88
+ out ProviderId extends string = string,
89
+ > extends Trigger.RuntimeTriggerEntry<TriggerDef, AllowedMethods, TriggerId, ProviderId> {
90
+ readonly localId: string;
91
+ readonly pluginVersion: string;
92
+ }
93
+
94
+ const entriesOf = (installation: PluginInstallation): readonly RuntimeTriggerEntry[] =>
95
+ walkCapabilities(installation.plugin, owner => owner.triggers).map(walked => ({
96
+ authMethods: installation.plugin.authMethods,
97
+ localId: walked.localId,
98
+ pluginVersion: installation.version,
99
+ providerId: installation.plugin.identifier,
100
+ trigger: walked.node,
101
+ triggerId: walked.canonicalId,
102
+ }));
103
+
104
+ export const collectTriggerEntries = (
105
+ installations: readonly PluginInstallation[],
106
+ ): readonly RuntimeTriggerEntry[] => installations.flatMap(entriesOf);
107
+
108
+ const triggerIndex = new WeakMap<
109
+ readonly PluginInstallation[],
110
+ ReadonlyMap<string, RuntimeTriggerEntry>
111
+ >();
112
+
113
+ /** A trigger of the installed releases, or of the exact `version` when the caller pins one. */
114
+ export const resolveRuntimeTrigger = (
115
+ installations: readonly PluginInstallation[],
116
+ triggerId: string,
117
+ version?: string,
118
+ ): RuntimeTriggerEntry | undefined => {
119
+ let index = triggerIndex.get(installations);
120
+ if (index === undefined) {
121
+ index = new Map(collectTriggerEntries(installations).map(entry => [entry.triggerId, entry]));
122
+ triggerIndex.set(installations, index);
123
+ }
124
+ const entry = index.get(triggerId);
125
+ return entry === undefined || (version !== undefined && entry.pluginVersion !== version)
126
+ ? undefined
127
+ : entry;
128
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A plugin's handlers live in the layer context as one group entry, keyed by
3
+ * plugin id, so merging layer outputs grows with the number of plugins and
4
+ * not with their actions. A handler keeps the services of its own build, never
5
+ * the groups of other plugins, and a runtime resolves groups through one
6
+ * index entry, so per-call context copies grow with the services in the app.
7
+ */
8
+ import { Array, Context, Predicate, String } from "effect";
9
+
10
+ const groupKeyPrefix = "@dual-os/sdk/Plugin/Handlers/";
11
+
12
+ export const handlerGroupKey = (pluginId: string): string => `${groupKeyPrefix}${pluginId}`;
13
+
14
+ const isGroupKey = String.startsWith(groupKeyPrefix);
15
+
16
+ class HandlerIndex extends Context.Service<HandlerIndex, ReadonlyMap<string, unknown>>()(
17
+ "@dual-os/sdk/internal/handler-context/HandlerIndex",
18
+ ) {}
19
+
20
+ export const withoutGroups = (context: Context.Context<never>): Context.Context<never> => {
21
+ const services = Array.filter(
22
+ Array.fromIterable(context.mapUnsafe),
23
+ entry => !isGroupKey(entry[0]),
24
+ );
25
+ return services.length === context.mapUnsafe.size
26
+ ? context
27
+ : Context.makeUnsafe(new Map(services));
28
+ };
29
+
30
+ export const indexGroups = (context: Context.Context<never>): Context.Context<never> => {
31
+ const entries = Array.fromIterable(context.mapUnsafe);
32
+ return Context.add(
33
+ Context.makeUnsafe(new Map(Array.filter(entries, entry => !isGroupKey(entry[0])))),
34
+ HandlerIndex,
35
+ new Map(Array.filter(entries, entry => isGroupKey(entry[0]))),
36
+ );
37
+ };
38
+
39
+ export const mergeUnder = <Input>(
40
+ lower: Context.Context<never>,
41
+ input: Context.Context<Input>,
42
+ ): Context.Context<Input> =>
43
+ Array.reduce(Array.fromIterable(lower.mapUnsafe), input, (merged, entry) =>
44
+ input.mapUnsafe.has(entry[0]) ? merged : Context.addUnsafe(merged, entry[0], entry[1]),
45
+ );
46
+
47
+ export const findHandler = (
48
+ context: Context.Context<never>,
49
+ providerId: string,
50
+ handlerKey: string,
51
+ ): unknown => {
52
+ const groupKey = handlerGroupKey(providerId);
53
+ const group =
54
+ context.mapUnsafe.get(groupKey) ?? Context.getOrUndefined(context, HandlerIndex)?.get(groupKey);
55
+
56
+ return Predicate.isMap(group) ? group.get(handlerKey) : undefined;
57
+ };
@@ -0,0 +1,27 @@
1
+ import { Schema } from "effect";
2
+
3
+ import { InvalidIdentifierError, type DefinitionIdentifierKind } from "../DefinitionError.ts";
4
+
5
+ export const MachineIdSeparator = "/" as const;
6
+
7
+ const identifierSchema = Schema.String.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/));
8
+ const isIdentifier = Schema.is(identifierSchema);
9
+
10
+ export const validateIdentifier = (kind: DefinitionIdentifierKind, identifier: string): void => {
11
+ if (identifier.length === 0) {
12
+ throw new InvalidIdentifierError({ identifier, kind, reason: "empty" });
13
+ }
14
+
15
+ if (identifier.includes(MachineIdSeparator)) {
16
+ throw new InvalidIdentifierError({
17
+ identifier,
18
+ kind,
19
+ reason: "contains-reserved-separator",
20
+ reservedCharacter: MachineIdSeparator,
21
+ });
22
+ }
23
+
24
+ if (!isIdentifier(identifier)) {
25
+ throw new InvalidIdentifierError({ identifier, kind, reason: "invalid-format" });
26
+ }
27
+ };