| @@ -2,8 +2,15 @@ import { Effect, Inspectable, Layer, Option, Predicate, Schema } from "effect"; · 1 unmodified line |
| 2 | import { FetchHttpClient, type HttpClient } from "effect/unstable/http"; | 2 | import { FetchHttpClient, type HttpClient } from "effect/unstable/http"; |
| 3 | import { fumadb } from "@executor-js/fumadb"; | 3 | import { fumadb } from "@executor-js/fumadb"; |
| 4 | import { memoryAdapter } from "@executor-js/fumadb/adapters/memory"; | 4 | import { memoryAdapter } from "@executor-js/fumadb/adapters/memory"; |
| 5 | import { withQueryContext, type Condition, type ConditionBuilder } from "@executor-js/fumadb/query"; | 5 | import { |
| 6 | import { schema as fumaSchema, type RelationsMap } from "@executor-js/fumadb/schema"; | 6 | withQueryContext, |
| | 7 | type Condition, |
| | 8 | type ConditionBuilder, |
| | 9 | } from "@executor-js/fumadb/query"; |
| | 10 | import { |
| | 11 | schema as fumaSchema, |
| | 12 | type RelationsMap, |
| | 13 | } from "@executor-js/fumadb/schema"; |
| 7 | import type { AnyColumn } from "@executor-js/fumadb/schema"; | 14 | import type { AnyColumn } from "@executor-js/fumadb/schema"; |
| 8 | import { generateKeyBetween } from "fractional-indexing"; | 15 | import { generateKeyBetween } from "fractional-indexing"; |
| 9 | | 16 | |
| @@ -16,7 +23,12 @@ import { · 6 unmodified lines |
| 16 | type FumaTables, | 23 | type FumaTables, |
| 17 | type StorageFailure, | 24 | type StorageFailure, |
| 18 | } from "./fuma-runtime"; | 25 | } from "./fuma-runtime"; |
| 19 | import { makeFumaBlobStore, pluginBlobStore, type BlobStore, type OwnerPartitions } from "./blob"; | 26 | import { |
| | 27 | makeFumaBlobStore, |
| | 28 | pluginBlobStore, |
| | 29 | type BlobStore, |
| | 30 | type OwnerPartitions, |
| | 31 | } from "./blob"; |
| 20 | import { coreToolsPlugin } from "./core-tools"; | 32 | import { coreToolsPlugin } from "./core-tools"; |
| 21 | import type { | 33 | import type { |
| 22 | Connection, | 34 | Connection, |
| @@ -85,7 +97,10 @@ import type { · 62 unmodified lines |
| 85 | IntegrationConfig, | 97 | IntegrationConfig, |
| 86 | RegisterIntegrationInput, | 98 | RegisterIntegrationInput, |
| 87 | } from "./integration"; | 99 | } from "./integration"; |
| 88 | import { makeOAuthService, type MintOAuthConnectionInput } from "./oauth-service"; | 100 | import { |
| | 101 | makeOAuthService, |
| | 102 | type MintOAuthConnectionInput, |
| | 103 | } from "./oauth-service"; |
| 89 | import type { OAuthService } from "./oauth-client"; | 104 | import type { OAuthService } from "./oauth-client"; |
| 90 | import { | 105 | import { |
| 91 | comparePolicyRow, | 106 | comparePolicyRow, |
| @@ -130,7 +145,12 @@ import { · 38 unmodified lines |
| 130 | type ExecutorOwnerPolicyContext, | 145 | type ExecutorOwnerPolicyContext, |
| 131 | } from "./owner-policy"; | 146 | } from "./owner-policy"; |
| 132 | import { ToolSchemaView, type IntegrationDetectionResult } from "./types"; | 147 | import { ToolSchemaView, type IntegrationDetectionResult } from "./types"; |
| 133 | import { type Tool, type ToolAnnotations, type ToolDef, type ToolListFilter } from "./tool"; | 148 | import { |
| | 149 | type Tool, |
| | 150 | type ToolAnnotations, |
| | 151 | type ToolDef, |
| | 152 | type ToolListFilter, |
| | 153 | } from "./tool"; |
| 134 | import { buildToolTypeScriptPreview } from "./schema-types"; | 154 | import { buildToolTypeScriptPreview } from "./schema-types"; |
| 135 | import { collectReferencedDefinitions } from "./schema-refs"; | 155 | import { collectReferencedDefinitions } from "./schema-refs"; |
| 136 | import { | 156 | import { |
| @@ -155,7 +175,9 @@ const MAX_APPROVAL_ARGUMENT_PREVIEW_CHARS = 4_000; · 18 unmodified lines |
| 155 | const acceptAllHandler: ElicitationHandler = () => | 175 | const acceptAllHandler: ElicitationHandler = () => |
| 156 | Effect.succeed(ElicitationResponse.make({ action: "accept" })); | 176 | Effect.succeed(ElicitationResponse.make({ action: "accept" })); |
| 157 | | 177 | |
| 158 | const resolveElicitationHandler = (onElicitation: OnElicitation): ElicitationHandler => | 178 | const resolveElicitationHandler = ( |
| | 179 | onElicitation: OnElicitation, |
| | 180 | ): ElicitationHandler => |
| 159 | onElicitation === "accept-all" ? acceptAllHandler : onElicitation; | 181 | onElicitation === "accept-all" ? acceptAllHandler : onElicitation; |
| 160 | | 182 | |
| 161 | // --------------------------------------------------------------------------- | 183 | // --------------------------------------------------------------------------- |
| @@ -171,7 +193,8 @@ export interface ParsedToolAddress { · 9 unmodified lines |
| 171 | readonly tool: ToolName; | 193 | readonly tool: ToolName; |
| 172 | } | 194 | } |
| 173 | | 195 | |
| 174 | const isOwner = (value: string): value is Owner => value === "org" || value === "user"; | 196 | const isOwner = (value: string): value is Owner => |
| | 197 | value === "org" || value === "user"; |
| 175 | | 198 | |
| 176 | /** Parse a callable address; null when it's not a well-formed | 199 | /** Parse a callable address; null when it's not a well-formed |
| 177 | * `tools.<integration>.<owner>.<connection>.<tool>`. | 200 | * `tools.<integration>.<owner>.<connection>.<tool>`. |
| @@ -190,16 +213,17 @@ export const parseToolAddress = (address: string): ParsedToolAddress | null => { · 12 unmodified lines |
| 190 | cut = address.indexOf(".", cut + 1); | 213 | cut = address.indexOf(".", cut + 1); |
| 191 | if (cut === -1) return null; | 214 | if (cut === -1) return null; |
| 192 | } | 215 | } |
| 193 | const [prefix, integration, owner, connection] = address.slice(0, cut).split(".") as [ | 216 | const [prefix, integration, owner, connection] = address |
| 194 | string, | 217 | .slice(0, cut) |
| 195 | string, | 218 | .split(".") as [string, string, string, string]; |
| 196 | string, | | |
| 197 | string, | | |
| 198 | ]; | | |
| 199 | const tool = address.slice(cut + 1); | 219 | const tool = address.slice(cut + 1); |
| 200 | if (prefix !== ADDRESS_PREFIX) return null; | 220 | if (prefix !== ADDRESS_PREFIX) return null; |
| 201 | if (!isOwner(owner)) return null; | 221 | if (!isOwner(owner)) return null; |
| 202 | if (integration.length === 0 || connection.length === 0 || tool.length === 0) { | 222 | if ( |
| | 223 | integration.length === 0 || |
| | 224 | connection.length === 0 || |
| | 225 | tool.length === 0 |
| | 226 | ) { |
| 203 | return null; | 227 | return null; |
| 204 | } | 228 | } |
| 205 | return { | 229 | return { |
| @@ -215,7 +239,9 @@ export const connectionAddress = ( · 9 unmodified lines |
| 215 | integration: IntegrationSlug, | 239 | integration: IntegrationSlug, |
| 216 | connection: ConnectionName, | 240 | connection: ConnectionName, |
| 217 | ): ConnectionAddress => | 241 | ): ConnectionAddress => |
| 218 | ConnectionAddress.make(`${ADDRESS_PREFIX}.${integration}.${owner}.${connection}`); | 242 | ConnectionAddress.make( |
| | 243 | `${ADDRESS_PREFIX}.${integration}.${owner}.${connection}`, |
| | 244 | ); |
| 219 | | 245 | |
| 220 | export const toolAddress = ( | 246 | export const toolAddress = ( |
| 221 | owner: Owner, | 247 | owner: Owner, |
| @@ -223,7 +249,9 @@ export const toolAddress = ( · 1 unmodified line |
| 223 | connection: ConnectionName, | 249 | connection: ConnectionName, |
| 224 | tool: ToolName, | 250 | tool: ToolName, |
| 225 | ): ToolAddress => | 251 | ): ToolAddress => |
| 226 | ToolAddress.make(`${ADDRESS_PREFIX}.${integration}.${owner}.${connection}.${tool}`); | 252 | ToolAddress.make( |
| | 253 | `${ADDRESS_PREFIX}.${integration}.${owner}.${connection}.${tool}`, |
| | 254 | ); |
| 227 | | 255 | |
| 228 | // --------------------------------------------------------------------------- | 256 | // --------------------------------------------------------------------------- |
| 229 | // Owner key helpers — every owned-row write stamps `tenant`, `owner`, | 257 | // Owner key helpers — every owned-row write stamps `tenant`, `owner`, |
| @@ -241,17 +269,34 @@ interface OwnedKeys { · 11 unmodified lines |
| 241 | // core-table query unioned with the in-memory static pool. | 269 | // core-table query unioned with the in-memory static pool. |
| 242 | // --------------------------------------------------------------------------- | 270 | // --------------------------------------------------------------------------- |
| 243 | | 271 | |
| | 272 | // ExecutorWrapper — a plugin-contributed executor middleware. Given the |
| | 273 | // assembled executor and the per-request selector string (e.g. an MCP |
| | 274 | // `?toolkit=` value), it returns a possibly-narrowed executor. A plugin |
| | 275 | // contributes one by exposing a `wrapExecutor` method on its extension; the |
| | 276 | // assembler collects them and the execution stack runs each before the engine |
| | 277 | // is built. Core knows nothing about what a wrapper narrows to — that domain |
| | 278 | // (toolkits, etc.) lives entirely in the contributing plugin. |
| | 279 | export type ExecutorWrapper = <TPlugins extends readonly AnyPlugin[]>( |
| | 280 | base: Executor<TPlugins>, |
| | 281 | selector: string, |
| | 282 | ) => Effect.Effect<Executor<TPlugins>, StorageFailure>; |
| | 283 | |
| 244 | export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { | 284 | export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { |
| 245 | readonly integrations: { | 285 | readonly integrations: { |
| 246 | readonly list: () => Effect.Effect<readonly Integration[], StorageFailure>; | 286 | readonly list: () => Effect.Effect<readonly Integration[], StorageFailure>; |
| 247 | readonly get: (slug: IntegrationSlug) => Effect.Effect<Integration | null, StorageFailure>; | 287 | readonly get: ( |
| | 288 | slug: IntegrationSlug, |
| | 289 | ) => Effect.Effect<Integration | null, StorageFailure>; |
| 248 | readonly update: ( | 290 | readonly update: ( |
| 249 | slug: IntegrationSlug, | 291 | slug: IntegrationSlug, |
| 250 | patch: { readonly name?: string; readonly description?: string }, | 292 | patch: { readonly name?: string; readonly description?: string }, |
| 251 | ) => Effect.Effect<void, IntegrationNotFoundError | StorageFailure>; | 293 | ) => Effect.Effect<void, IntegrationNotFoundError | StorageFailure>; |
| 252 | readonly remove: ( | 294 | readonly remove: ( |
| 253 | slug: IntegrationSlug, | 295 | slug: IntegrationSlug, |
| 254 | ) => Effect.Effect<void, IntegrationRemovalNotAllowedError | StorageFailure>; | 296 | ) => Effect.Effect< |
| | 297 | void, |
| | 298 | IntegrationRemovalNotAllowedError | StorageFailure |
| | 299 | >; |
| 255 | readonly detect: ( | 300 | readonly detect: ( |
| 256 | url: string, | 301 | url: string, |
| 257 | ) => Effect.Effect<readonly IntegrationDetectionResult[], StorageFailure>; | 302 | ) => Effect.Effect<readonly IntegrationDetectionResult[], StorageFailure>; |
| @@ -271,7 +316,9 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 13 unmodified lines |
| 271 | readonly integration?: IntegrationSlug; | 316 | readonly integration?: IntegrationSlug; |
| 272 | readonly owner?: Owner; | 317 | readonly owner?: Owner; |
| 273 | }) => Effect.Effect<readonly Connection[], StorageFailure>; | 318 | }) => Effect.Effect<readonly Connection[], StorageFailure>; |
| 274 | readonly get: (ref: ConnectionRef) => Effect.Effect<Connection | null, StorageFailure>; | 319 | readonly get: ( |
| | 320 | ref: ConnectionRef, |
| | 321 | ) => Effect.Effect<Connection | null, StorageFailure>; |
| 275 | /** Edit user-curated metadata (description, identityLabel). Credentials and | 322 | /** Edit user-curated metadata (description, identityLabel). Credentials and |
| 276 | * OAuth lifecycle fields are not editable here. */ | 323 | * OAuth lifecycle fields are not editable here. */ |
| 277 | readonly update: ( | 324 | readonly update: ( |
| @@ -294,21 +341,35 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 16 unmodified lines |
| 294 | readonly oauth: OAuthService; | 341 | readonly oauth: OAuthService; |
| 295 | | 342 | |
| 296 | readonly tools: { | 343 | readonly tools: { |
| 297 | readonly list: (filter?: ToolListFilter) => Effect.Effect<readonly Tool[], StorageFailure>; | 344 | readonly list: ( |
| 298 | readonly schema: (address: ToolAddress) => Effect.Effect<ToolSchemaView | null, StorageFailure>; | 345 | filter?: ToolListFilter, |
| | 346 | ) => Effect.Effect<readonly Tool[], StorageFailure>; |
| | 347 | readonly schema: ( |
| | 348 | address: ToolAddress, |
| | 349 | ) => Effect.Effect<ToolSchemaView | null, StorageFailure>; |
| 299 | }; | 350 | }; |
| 300 | | 351 | |
| 301 | readonly providers: { | 352 | readonly providers: { |
| 302 | readonly list: () => Effect.Effect<readonly ProviderKey[]>; | 353 | readonly list: () => Effect.Effect<readonly ProviderKey[]>; |
| 303 | readonly items: (key: ProviderKey) => Effect.Effect<readonly ProviderEntry[], StorageFailure>; | 354 | readonly items: ( |
| | 355 | key: ProviderKey, |
| | 356 | ) => Effect.Effect<readonly ProviderEntry[], StorageFailure>; |
| 304 | }; | 357 | }; |
| 305 | | 358 | |
| 306 | readonly policies: { | 359 | readonly policies: { |
| 307 | readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>; | 360 | readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>; |
| 308 | readonly create: (input: CreateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>; | 361 | readonly create: ( |
| 309 | readonly update: (input: UpdateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>; | 362 | input: CreateToolPolicyInput, |
| 310 | readonly remove: (input: RemoveToolPolicyInput) => Effect.Effect<void, StorageFailure>; | 363 | ) => Effect.Effect<ToolPolicy, StorageFailure>; |
| 311 | readonly resolve: (address: ToolAddress) => Effect.Effect<EffectivePolicy, StorageFailure>; | 364 | readonly update: ( |
| | 365 | input: UpdateToolPolicyInput, |
| | 366 | ) => Effect.Effect<ToolPolicy, StorageFailure>; |
| | 367 | readonly remove: ( |
| | 368 | input: RemoveToolPolicyInput, |
| | 369 | ) => Effect.Effect<void, StorageFailure>; |
| | 370 | readonly resolve: ( |
| | 371 | address: ToolAddress, |
| | 372 | ) => Effect.Effect<EffectivePolicy, StorageFailure>; |
| 312 | }; | 373 | }; |
| 313 | | 374 | |
| 314 | readonly execute: ( | 375 | readonly execute: ( |
| @@ -322,7 +383,10 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 7 unmodified lines |
| 322 | | 383 | |
| 323 | export interface ExecutorDb { | 384 | export interface ExecutorDb { |
| 324 | readonly db: FumaDb<any>; | 385 | readonly db: FumaDb<any>; |
| 325 | readonly close?: () => Effect.Effect<void, StorageFailure> | Promise<void> | void; | 386 | readonly close?: () => |
| | 387 | | Effect.Effect<void, StorageFailure> |
| | 388 | | Promise<void> |
| | 389 | | void; |
| 326 | } | 390 | } |
| 327 | | 391 | |
| 328 | export type ExecutorDbInput = FumaDb<any> | ExecutorDb; | 392 | export type ExecutorDbInput = FumaDb<any> | ExecutorDb; |
| @@ -331,7 +395,9 @@ export type ExecutorDbFactory = (config: { · 2 unmodified lines |
| 331 | readonly tables: FumaTables; | 395 | readonly tables: FumaTables; |
| 332 | }) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>; | 396 | }) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>; |
| 333 | | 397 | |
| 334 | export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly []> { | 398 | export interface ExecutorConfig< |
| | 399 | TPlugins extends readonly AnyPlugin[] = readonly [], |
| | 400 | > { |
| 335 | /** The org / workspace this executor is bound to. `owner: "org"` rows file | 401 | /** The org / workspace this executor is bound to. `owner: "org"` rows file |
| 336 | * here. */ | 402 | * here. */ |
| 337 | readonly tenant: Tenant; | 403 | readonly tenant: Tenant; |
| @@ -392,7 +458,10 @@ const validateExecutorOwnerPolicyTables = (tables: FumaTables): void => { · 54 unmodified lines |
| 392 | } | 458 | } |
| 393 | }; | 459 | }; |
| 394 | | 460 | |
| 395 | const validateExecutorDbTables = (required: FumaTables, actual: FumaTables): void => { | 461 | const validateExecutorDbTables = ( |
| | 462 | required: FumaTables, |
| | 463 | actual: FumaTables, |
| | 464 | ): void => { |
| 396 | const missing = Object.keys(required) | 465 | const missing = Object.keys(required) |
| 397 | .filter((tableName) => !actual[tableName]) | 466 | .filter((tableName) => !actual[tableName]) |
| 398 | .sort(); | 467 | .sort(); |
| @@ -408,18 +477,27 @@ const validateExecutorDbTables = (required: FumaTables, actual: FumaTables): voi · 9 unmodified lines |
| 408 | }); | 477 | }); |
| 409 | }; | 478 | }; |
| 410 | | 479 | |
| 411 | const storageFailureFromUnknown = (message: string, cause: unknown): StorageFailure => | 480 | const storageFailureFromUnknown = ( |
| | 481 | message: string, |
| | 482 | cause: unknown, |
| | 483 | ): StorageFailure => |
| 412 | isStorageFailure(cause) ? cause : new StorageError({ message, cause }); | 484 | isStorageFailure(cause) ? cause : new StorageError({ message, cause }); |
| 413 | | 485 | |
| 414 | const pluginStorageFailure = (pluginId: string, hook: string, cause: unknown): StorageFailure => | 486 | const pluginStorageFailure = ( |
| | 487 | pluginId: string, |
| | 488 | hook: string, |
| | 489 | cause: unknown, |
| | 490 | ): StorageFailure => |
| 415 | storageFailureFromUnknown(`${hook} failed for plugin ${pluginId}`, cause); | 491 | storageFailureFromUnknown(`${hook} failed for plugin ${pluginId}`, cause); |
| 416 | | 492 | |
| 417 | const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => { | 493 | const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => { |
| 418 | const version = "1.0.0"; | 494 | const version = "1.0.0"; |
| 419 | const latestSchema = fumaSchema<string, FumaTables, RelationsMap<FumaTables>>({ | 495 | const latestSchema = fumaSchema<string, FumaTables, RelationsMap<FumaTables>>( |
| 420 | version, | 496 | { |
| 421 | tables, | 497 | version, |
| 422 | }); | 498 | tables, |
| | 499 | }, |
| | 500 | ); |
| 423 | const factory = fumadb({ | 501 | const factory = fumadb({ |
| 424 | namespace: "executor_memory", | 502 | namespace: "executor_memory", |
| 425 | schemas: [latestSchema], | 503 | schemas: [latestSchema], |
| @@ -434,7 +512,9 @@ const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => { · 8 unmodified lines |
| 434 | // JSON helpers + row → public projection conversions | 512 | // JSON helpers + row → public projection conversions |
| 435 | // --------------------------------------------------------------------------- | 513 | // --------------------------------------------------------------------------- |
| 436 | | 514 | |
| 437 | const decodeJsonFromString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString); | 515 | const decodeJsonFromString = Schema.decodeUnknownOption( |
| | 516 | Schema.UnknownFromJsonString, |
| | 517 | ); |
| 438 | | 518 | |
| 439 | const decodeJsonColumn = (value: unknown): unknown => { | 519 | const decodeJsonColumn = (value: unknown): unknown => { |
| 440 | if (value === null || value === undefined) return undefined; | 520 | if (value === null || value === undefined) return undefined; |
| @@ -482,9 +562,14 @@ const rowToConnection = (row: ConnectionRow): Connection => { · 41 unmodified lines |
| 482 | identityLabel: row.identity_label ?? null, | 562 | identityLabel: row.identity_label ?? null, |
| 483 | description: row.description ?? null, | 563 | description: row.description ?? null, |
| 484 | expiresAt: row.expires_at == null ? null : Number(row.expires_at), | 564 | expiresAt: row.expires_at == null ? null : Number(row.expires_at), |
| 485 | oauthClient: row.oauth_client == null ? null : OAuthClientSlug.make(String(row.oauth_client)), | 565 | oauthClient: |
| | 566 | row.oauth_client == null |
| | 567 | ? null |
| | 568 | : OAuthClientSlug.make(String(row.oauth_client)), |
| 486 | oauthClientOwner: | 569 | oauthClientOwner: |
| 487 | row.oauth_client_owner == null ? null : (String(row.oauth_client_owner) as Owner), | 570 | row.oauth_client_owner == null |
| | 571 | ? null |
| | 572 | : (String(row.oauth_client_owner) as Owner), |
| 488 | oauthScope: row.oauth_scope == null ? null : String(row.oauth_scope), | 573 | oauthScope: row.oauth_scope == null ? null : String(row.oauth_scope), |
| 489 | }; | 574 | }; |
| 490 | }; | 575 | }; |
| @@ -505,7 +590,10 @@ const normalizeConnectionInputs = ( · 14 unmodified lines |
| 505 | input: ConnectionValueInput, | 590 | input: ConnectionValueInput, |
| 506 | ): readonly NormalizedConnectionInput[] => { | 591 | ): readonly NormalizedConnectionInput[] => { |
| 507 | if ("inputs" in input) { | 592 | if ("inputs" in input) { |
| 508 | return Object.entries(input.inputs).map(([variable, origin]) => ({ variable, origin })); | 593 | return Object.entries(input.inputs).map(([variable, origin]) => ({ |
| | 594 | variable, |
| | 595 | origin, |
| | 596 | })); |
| 509 | } | 597 | } |
| 510 | if ("values" in input) { | 598 | if ("values" in input) { |
| 511 | return Object.entries(input.values).map(([variable, value]) => ({ | 599 | return Object.entries(input.values).map(([variable, value]) => ({ |
| @@ -532,7 +620,8 @@ const connectionItemIds = (row: ConnectionRow): Record<string, string> => { · 20 unmodified lines |
| 532 | // schema columns); `Tool.inputSchema`/`outputSchema` are optional and stay | 620 | // schema columns); `Tool.inputSchema`/`outputSchema` are optional and stay |
| 533 | // absent for those callers — `tools.schema` is the schema-bearing surface. | 621 | // absent for those callers — `tools.schema` is the schema-bearing surface. |
| 534 | const rowToTool = ( | 622 | const rowToTool = ( |
| 535 | row: ToolInvocationRow & Partial<Pick<ToolRow, "input_schema" | "output_schema">>, | 623 | row: ToolInvocationRow & |
| | 624 | Partial<Pick<ToolRow, "input_schema" | "output_schema">>, |
| 536 | annotations?: ToolAnnotations, | 625 | annotations?: ToolAnnotations, |
| 537 | ): Tool => { | 626 | ): Tool => { |
| 538 | const owner = row.owner as Owner; | 627 | const owner = row.owner as Owner; |
| @@ -549,7 +638,9 @@ const rowToTool = ( · 10 unmodified lines |
| 549 | description: row.description, | 638 | description: row.description, |
| 550 | inputSchema: decodeJsonColumn(row.input_schema), | 639 | inputSchema: decodeJsonColumn(row.input_schema), |
| 551 | outputSchema: decodeJsonColumn(row.output_schema), | 640 | outputSchema: decodeJsonColumn(row.output_schema), |
| 552 | annotations: annotations ?? (decodeJsonColumn(row.annotations) as ToolAnnotations | undefined), | 641 | annotations: |
| | 642 | annotations ?? |
| | 643 | (decodeJsonColumn(row.annotations) as ToolAnnotations | undefined), |
| 553 | }; | 644 | }; |
| 554 | }; | 645 | }; |
| 555 | | 646 | |
| @@ -580,7 +671,10 @@ type CoreFindFirstOptions<TName extends CoreTableName = CoreTableName> = Omit< · 24 unmodified lines |
| 580 | >; | 671 | >; |
| 581 | /** The narrowed row a projected query returns: the selected columns keep | 672 | /** The narrowed row a projected query returns: the selected columns keep |
| 582 | * their types, everything else is absent. */ | 673 | * their types, everything else is absent. */ |
| 583 | type CoreProjectedRow<TName extends CoreTableName, TSelect> = TSelect extends readonly (infer K)[] | 674 | type CoreProjectedRow< |
| | 675 | TName extends CoreTableName, |
| | 676 | TSelect, |
| | 677 | > = TSelect extends readonly (infer K)[] |
| 584 | ? Pick<CoreRow<TName>, Extract<K, keyof CoreRow<TName>>> | 678 | ? Pick<CoreRow<TName>, Extract<K, keyof CoreRow<TName>>> |
| 585 | : CoreRow<TName>; | 679 | : CoreRow<TName>; |
| 586 | | 680 | |
| @@ -613,7 +707,9 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({ · 26 unmodified lines |
| 613 | tableName: TName, | 707 | tableName: TName, |
| 614 | options?: { readonly where?: CoreWhere }, | 708 | options?: { readonly where?: CoreWhere }, |
| 615 | ): Effect.Effect<number, StorageFailure> => | 709 | ): Effect.Effect<number, StorageFailure> => |
| 616 | fuma.use(`${tableName}.count`, (db) => asLooseStorageDb(db).count(tableName, options)), | 710 | fuma.use(`${tableName}.count`, (db) => |
| | 711 | asLooseStorageDb(db).count(tableName, options), |
| | 712 | ), |
| 617 | create: <TName extends CoreTableName>( | 713 | create: <TName extends CoreTableName>( |
| 618 | tableName: TName, | 714 | tableName: TName, |
| 619 | row: Record<string, unknown>, | 715 | row: Record<string, unknown>, |
| @@ -628,7 +724,9 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({ · 8 unmodified lines |
| 628 | rows.length === 0 | 724 | rows.length === 0 |
| 629 | ? Effect.void | 725 | ? Effect.void |
| 630 | : fuma | 726 | : fuma |
| 631 | .use(`${tableName}.createMany`, (db) => asLooseStorageDb(db).createMany(tableName, rows)) | 727 | .use(`${tableName}.createMany`, (db) => |
| | 728 | asLooseStorageDb(db).createMany(tableName, rows), |
| | 729 | ) |
| 632 | .pipe(Effect.asVoid), | 730 | .pipe(Effect.asVoid), |
| 633 | deleteMany: <TName extends CoreTableName>( | 731 | deleteMany: <TName extends CoreTableName>( |
| 634 | tableName: TName, | 732 | tableName: TName, |
| @@ -637,20 +735,38 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({ · 2 unmodified lines |
| 637 | fuma.use(`${tableName}.deleteMany`, (db) => | 735 | fuma.use(`${tableName}.deleteMany`, (db) => |
| 638 | asLooseStorageDb(db).deleteMany(tableName, options), | 736 | asLooseStorageDb(db).deleteMany(tableName, options), |
| 639 | ), | 737 | ), |
| 640 | findFirst: <TName extends CoreTableName, const TOptions extends CoreFindFirstOptions<TName>>( | 738 | findFirst: < |
| | 739 | TName extends CoreTableName, |
| | 740 | const TOptions extends CoreFindFirstOptions<TName>, |
| | 741 | >( |
| 641 | tableName: TName, | 742 | tableName: TName, |
| 642 | options: TOptions, | 743 | options: TOptions, |
| 643 | ): Effect.Effect<CoreProjectedRow<TName, TOptions["select"]> | null, StorageFailure> => | 744 | ): Effect.Effect< |
| | 745 | CoreProjectedRow<TName, TOptions["select"]> | null, |
| | 746 | StorageFailure |
| | 747 | > => |
| 644 | fuma.use(`${tableName}.findFirst`, (db) => | 748 | fuma.use(`${tableName}.findFirst`, (db) => |
| 645 | asLooseStorageDb(db).findFirst(tableName, options), | 749 | asLooseStorageDb(db).findFirst(tableName, options), |
| 646 | ) as Effect.Effect<CoreProjectedRow<TName, TOptions["select"]> | null, StorageFailure>, | 750 | ) as Effect.Effect< |
| 647 | findMany: <TName extends CoreTableName, const TOptions extends CoreFindManyOptions<TName>>( | 751 | CoreProjectedRow<TName, TOptions["select"]> | null, |
| | 752 | StorageFailure |
| | 753 | >, |
| | 754 | findMany: < |
| | 755 | TName extends CoreTableName, |
| | 756 | const TOptions extends CoreFindManyOptions<TName>, |
| | 757 | >( |
| 648 | tableName: TName, | 758 | tableName: TName, |
| 649 | options: TOptions = {} as TOptions, | 759 | options: TOptions = {} as TOptions, |
| 650 | ): Effect.Effect<readonly CoreProjectedRow<TName, TOptions["select"]>[], StorageFailure> => | 760 | ): Effect.Effect< |
| | 761 | readonly CoreProjectedRow<TName, TOptions["select"]>[], |
| | 762 | StorageFailure |
| | 763 | > => |
| 651 | fuma.use(`${tableName}.findMany`, (db) => | 764 | fuma.use(`${tableName}.findMany`, (db) => |
| 652 | asLooseStorageDb(db).findMany(tableName, options), | 765 | asLooseStorageDb(db).findMany(tableName, options), |
| 653 | ) as Effect.Effect<readonly CoreProjectedRow<TName, TOptions["select"]>[], StorageFailure>, | 766 | ) as Effect.Effect< |
| | 767 | readonly CoreProjectedRow<TName, TOptions["select"]>[], |
| | 768 | StorageFailure |
| | 769 | >, |
| 654 | updateMany: <TName extends CoreTableName>( | 770 | updateMany: <TName extends CoreTableName>( |
| 655 | tableName: TName, | 771 | tableName: TName, |
| 656 | options: { | 772 | options: { |
| @@ -670,7 +786,9 @@ type CoreDb = ReturnType<typeof makeCoreDb>; · 13 unmodified lines |
| 670 | // [user, org]; writes/deletes name an explicit owner. | 786 | // [user, org]; writes/deletes name an explicit owner. |
| 671 | // --------------------------------------------------------------------------- | 787 | // --------------------------------------------------------------------------- |
| 672 | | 788 | |
| 673 | const pluginStorageEntryFromRow = <T>(row: CoreRow<"plugin_storage">): PluginStorageEntry<T> => ({ | 789 | const pluginStorageEntryFromRow = <T>( |
| | 790 | row: CoreRow<"plugin_storage">, |
| | 791 | ): PluginStorageEntry<T> => ({ |
| 674 | id: pluginStorageId({ | 792 | id: pluginStorageId({ |
| 675 | pluginId: row.plugin_id, | 793 | pluginId: row.plugin_id, |
| 676 | collection: row.collection, | 794 | collection: row.collection, |
| @@ -681,21 +799,28 @@ const pluginStorageEntryFromRow = <T>(row: CoreRow<"plugin_storage">): PluginSto · 4 unmodified lines |
| 681 | collection: row.collection, | 799 | collection: row.collection, |
| 682 | key: row.key, | 800 | key: row.key, |
| 683 | data: row.data as T, | 801 | data: row.data as T, |
| 684 | createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at), | 802 | createdAt: |
| 685 | updatedAt: row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at), | 803 | row.created_at instanceof Date ? row.created_at : new Date(row.created_at), |
| | 804 | updatedAt: |
| | 805 | row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at), |
| 686 | }); | 806 | }); |
| 687 | | 807 | |
| 688 | const pluginStorageIndexSpecFields = (spec: PluginStorageRuntimeIndexSpec): readonly string[] => | 808 | const pluginStorageIndexSpecFields = ( |
| 689 | typeof spec === "string" ? [spec] : spec; | 809 | spec: PluginStorageRuntimeIndexSpec, |
| | 810 | ): readonly string[] => (typeof spec === "string" ? [spec] : spec); |
| 690 | | 811 | |
| 691 | const pluginStorageCollectionIndexedFields = ( | 812 | const pluginStorageCollectionIndexedFields = ( |
| 692 | definition: PluginStorageRuntimeCollectionDefinition, | 813 | definition: PluginStorageRuntimeCollectionDefinition, |
| 693 | ): ReadonlySet<string> => | 814 | ): ReadonlySet<string> => |
| 694 | new Set(definition.indexes.flatMap((spec) => pluginStorageIndexSpecFields(spec))); | 815 | new Set( |
| | 816 | definition.indexes.flatMap((spec) => pluginStorageIndexSpecFields(spec)), |
| | 817 | ); |
| 695 | | 818 | |
| 696 | const pluginStorageQueryValidationError = ( | 819 | const pluginStorageQueryValidationError = ( |
| 697 | definition: PluginStorageRuntimeCollectionDefinition, | 820 | definition: PluginStorageRuntimeCollectionDefinition, |
| 698 | query: PluginStorageCollectionQueryInput<PluginStorageCollectionDefinition> | undefined, | 821 | query: |
| | 822 | | PluginStorageCollectionQueryInput<PluginStorageCollectionDefinition> |
| | 823 | | undefined, |
| 699 | ): StorageError | null => { | 824 | ): StorageError | null => { |
| 700 | if (!query) return null; | 825 | if (!query) return null; |
| 701 | const indexedFields = pluginStorageCollectionIndexedFields(definition); | 826 | const indexedFields = pluginStorageCollectionIndexedFields(definition); |
| @@ -711,13 +836,19 @@ const pluginStorageQueryValidationError = ( · 9 unmodified lines |
| 711 | }); | 836 | }); |
| 712 | } | 837 | } |
| 713 | } | 838 | } |
| 714 | if (query.limit !== undefined && (!Number.isInteger(query.limit) || query.limit < 0)) { | 839 | if ( |
| | 840 | query.limit !== undefined && |
| | 841 | (!Number.isInteger(query.limit) || query.limit < 0) |
| | 842 | ) { |
| 715 | return new StorageError({ | 843 | return new StorageError({ |
| 716 | message: `Plugin storage collection "${definition.name}" received an invalid query limit`, | 844 | message: `Plugin storage collection "${definition.name}" received an invalid query limit`, |
| 717 | cause: undefined, | 845 | cause: undefined, |
| 718 | }); | 846 | }); |
| 719 | } | 847 | } |
| 720 | if (query.offset !== undefined && (!Number.isInteger(query.offset) || query.offset < 0)) { | 848 | if ( |
| | 849 | query.offset !== undefined && |
| | 850 | (!Number.isInteger(query.offset) || query.offset < 0) |
| | 851 | ) { |
| 721 | return new StorageError({ | 852 | return new StorageError({ |
| 722 | message: `Plugin storage collection "${definition.name}" received an invalid query offset`, | 853 | message: `Plugin storage collection "${definition.name}" received an invalid query offset`, |
| 723 | cause: undefined, | 854 | cause: undefined, |
| @@ -726,17 +857,35 @@ const pluginStorageQueryValidationError = ( · 2 unmodified lines |
| 726 | return null; | 857 | return null; |
| 727 | }; | 858 | }; |
| 728 | | 859 | |
| 729 | const isPluginStorageRecord = (value: unknown): value is Readonly<Record<string, unknown>> => | 860 | const isPluginStorageRecord = ( |
| | 861 | value: unknown, |
| | 862 | ): value is Readonly<Record<string, unknown>> => |
| 730 | value !== null && typeof value === "object" && !Array.isArray(value); | 863 | value !== null && typeof value === "object" && !Array.isArray(value); |
| 731 | | 864 | |
| 732 | const pluginStorageWhereOperators = ["eq", "in", "gt", "gte", "lt", "lte"] as const; | 865 | const pluginStorageWhereOperators = [ |
| | 866 | "eq", |
| | 867 | "in", |
| | 868 | "gt", |
| | 869 | "gte", |
| | 870 | "lt", |
| | 871 | "lte", |
| | 872 | ] as const; |
| 733 | | 873 | |
| 734 | const isPluginStorageWhereFilter = (value: unknown): value is Readonly<Record<string, unknown>> => | 874 | const isPluginStorageWhereFilter = ( |
| 735 | isPluginStorageRecord(value) && pluginStorageWhereOperators.some((operator) => operator in value); | 875 | value: unknown, |
| | 876 | ): value is Readonly<Record<string, unknown>> => |
| | 877 | isPluginStorageRecord(value) && |
| | 878 | pluginStorageWhereOperators.some((operator) => operator in value); |
| 736 | | 879 | |
| 737 | const pluginStorageComparableValue = (value: unknown): string | number | boolean | null => { | 880 | const pluginStorageComparableValue = ( |
| | 881 | value: unknown, |
| | 882 | ): string | number | boolean | null => { |
| 738 | if (value instanceof Date) return value.getTime(); | 883 | if (value instanceof Date) return value.getTime(); |
| 739 | if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") { | 884 | if ( |
| | 885 | typeof value === "number" || |
| | 886 | typeof value === "string" || |
| | 887 | typeof value === "boolean" |
| | 888 | ) { |
| 740 | return value; | 889 | return value; |
| 741 | } | 890 | } |
| 742 | if (value == null) return null; | 891 | if (value == null) return null; |
| @@ -755,8 +904,13 @@ const comparePluginStorageValues = (left: unknown, right: unknown): number => { · 12 unmodified lines |
| 755 | const pluginStorageDataField = (data: unknown, field: string): unknown => | 904 | const pluginStorageDataField = (data: unknown, field: string): unknown => |
| 756 | isPluginStorageRecord(data) ? data[field] : undefined; | 905 | isPluginStorageRecord(data) ? data[field] : undefined; |
| 757 | | 906 | |
| 758 | const matchesWhereOperator = (operator: string, value: unknown, operand: unknown): boolean => { | 907 | const matchesWhereOperator = ( |
| 759 | if (operator === "eq") return comparePluginStorageValues(value, operand) === 0; | 908 | operator: string, |
| | 909 | value: unknown, |
| | 910 | operand: unknown, |
| | 911 | ): boolean => { |
| | 912 | if (operator === "eq") |
| | 913 | return comparePluginStorageValues(value, operand) === 0; |
| 760 | if (operator === "in") { | 914 | if (operator === "in") { |
| 761 | return ( | 915 | return ( |
| 762 | Array.isArray(operand) && | 916 | Array.isArray(operand) && |
| @@ -764,9 +918,11 @@ const matchesWhereOperator = (operator: string, value: unknown, operand: unknown · 1 unmodified line |
| 764 | ); | 918 | ); |
| 765 | } | 919 | } |
| 766 | if (operator === "gt") return comparePluginStorageValues(value, operand) > 0; | 920 | if (operator === "gt") return comparePluginStorageValues(value, operand) > 0; |
| 767 | if (operator === "gte") return comparePluginStorageValues(value, operand) >= 0; | 921 | if (operator === "gte") |
| | 922 | return comparePluginStorageValues(value, operand) >= 0; |
| 768 | if (operator === "lt") return comparePluginStorageValues(value, operand) < 0; | 923 | if (operator === "lt") return comparePluginStorageValues(value, operand) < 0; |
| 769 | if (operator === "lte") return comparePluginStorageValues(value, operand) <= 0; | 924 | if (operator === "lte") |
| | 925 | return comparePluginStorageValues(value, operand) <= 0; |
| 770 | return false; | 926 | return false; |
| 771 | }; | 927 | }; |
| 772 | | 928 | |
| @@ -802,9 +958,12 @@ const makePluginStorageFacade = (input: { · 29 unmodified lines |
| 802 | readonly owner: OwnerBinding; | 958 | readonly owner: OwnerBinding; |
| 803 | }): PluginStorageFacade => { | 959 | }): PluginStorageFacade => { |
| 804 | // Owner partitions: org always, plus this subject's user partition. | 960 | // Owner partitions: org always, plus this subject's user partition. |
| 805 | const readOwners: readonly Owner[] = input.owner.subject == null ? ["org"] : ["user", "org"]; | 961 | const readOwners: readonly Owner[] = |
| | 962 | input.owner.subject == null ? ["org"] : ["user", "org"]; |
| 806 | | 963 | |
| 807 | const ownerSubject = (owner: Owner): { owner: Owner; subject: string } | null => { | 964 | const ownerSubject = ( |
| | 965 | owner: Owner, |
| | 966 | ): { owner: Owner; subject: string } | null => { |
| 808 | if (owner === "org") return { owner: "org", subject: ORG_SUBJECT }; | 967 | if (owner === "org") return { owner: "org", subject: ORG_SUBJECT }; |
| 809 | if (input.owner.subject == null) return null; | 968 | if (input.owner.subject == null) return null; |
| 810 | return { owner: "user", subject: String(input.owner.subject) }; | 969 | return { owner: "user", subject: String(input.owner.subject) }; |
| @@ -821,7 +980,11 @@ const makePluginStorageFacade = (input: { · 10 unmodified lines |
| 821 | key === undefined ? true : b("key", "=", key), | 980 | key === undefined ? true : b("key", "=", key), |
| 822 | ); | 981 | ); |
| 823 | | 982 | |
| 824 | const whereOwner = (owner: Owner, collection: string, key: string): CoreWhere => { | 983 | const whereOwner = ( |
| | 984 | owner: Owner, |
| | 985 | collection: string, |
| | 986 | key: string, |
| | 987 | ): CoreWhere => { |
| 825 | const os = ownerSubject(owner); | 988 | const os = ownerSubject(owner); |
| 826 | return (b: AnyCb) => | 989 | return (b: AnyCb) => |
| 827 | b.and( | 990 | b.and( |
| @@ -843,19 +1006,28 @@ const makePluginStorageFacade = (input: { · 15 unmodified lines |
| 843 | }); | 1006 | }); |
| 844 | | 1007 | |
| 845 | const getVisible = <T>(collection: string, key: string) => | 1008 | const getVisible = <T>(collection: string, key: string) => |
| 846 | input.core.findMany("plugin_storage", { where: whereFor(collection, key) }).pipe( | 1009 | input.core |
| 847 | Effect.map((rows) => sortByOwnerPrecedence(rows)[0] ?? null), | 1010 | .findMany("plugin_storage", { where: whereFor(collection, key) }) |
| 848 | Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)), | 1011 | .pipe( |
| 849 | ); | 1012 | Effect.map((rows) => sortByOwnerPrecedence(rows)[0] ?? null), |
| | 1013 | Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)), |
| | 1014 | ); |
| 850 | | 1015 | |
| 851 | const getForOwnerImpl = <T>(owner: Owner, collection: string, key: string) => | 1016 | const getForOwnerImpl = <T>(owner: Owner, collection: string, key: string) => |
| 852 | input.core | 1017 | input.core |
| 853 | .findFirst("plugin_storage", { | 1018 | .findFirst("plugin_storage", { |
| 854 | where: whereOwner(owner, collection, key), | 1019 | where: whereOwner(owner, collection, key), |
| 855 | }) | 1020 | }) |
| 856 | .pipe(Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null))); | 1021 | .pipe( |
| | 1022 | Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)), |
| | 1023 | ); |
| 857 | | 1024 | |
| 858 | const putImpl = <T>(owner: Owner, collection: string, key: string, data: unknown) => | 1025 | const putImpl = <T>( |
| | 1026 | owner: Owner, |
| | 1027 | collection: string, |
| | 1028 | key: string, |
| | 1029 | data: unknown, |
| | 1030 | ) => |
| 859 | Effect.gen(function* () { | 1031 | Effect.gen(function* () { |
| 860 | const os = ownerSubject(owner); | 1032 | const os = ownerSubject(owner); |
| 861 | if (!os) { | 1033 | if (!os) { |
| @@ -935,7 +1107,10 @@ const makePluginStorageFacade = (input: { · 73 unmodified lines |
| 935 | offset < uniqueKeys.length; | 1107 | offset < uniqueKeys.length; |
| 936 | offset += PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE | 1108 | offset += PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE |
| 937 | ) { | 1109 | ) { |
| 938 | const batchKeys = uniqueKeys.slice(offset, offset + PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE); | 1110 | const batchKeys = uniqueKeys.slice( |
| | 1111 | offset, |
| | 1112 | offset + PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE, |
| | 1113 | ); |
| 939 | yield* input.core.deleteMany("plugin_storage", { | 1114 | yield* input.core.deleteMany("plugin_storage", { |
| 940 | where: (b) => | 1115 | where: (b) => |
| 941 | b.and( | 1116 | b.and( |
| @@ -1013,7 +1188,9 @@ const makePluginStorageFacade = (input: { · 71 unmodified lines |
| 1013 | yield* deleteManyImpl(owner, os.subject, entries); | 1188 | yield* deleteManyImpl(owner, os.subject, entries); |
| 1014 | }); | 1189 | }); |
| 1015 | | 1190 | |
| 1016 | const queryCollection = <TDefinition extends PluginStorageCollectionDefinition>( | 1191 | const queryCollection = < |
| | 1192 | TDefinition extends PluginStorageCollectionDefinition, |
| | 1193 | >( |
| 1017 | definition: TDefinition, | 1194 | definition: TDefinition, |
| 1018 | queryInput?: PluginStorageCollectionQueryInput<TDefinition>, | 1195 | queryInput?: PluginStorageCollectionQueryInput<TDefinition>, |
| 1019 | ) => | 1196 | ) => |
| @@ -1031,7 +1208,9 @@ const makePluginStorageFacade = (input: { · 11 unmodified lines |
| 1031 | }); | 1208 | }); |
| 1032 | const filtered = sortByOwnerPrecedence(rows) | 1209 | const filtered = sortByOwnerPrecedence(rows) |
| 1033 | .filter((row) => | 1210 | .filter((row) => |
| 1034 | queryInput?.keyPrefix === undefined ? true : row.key.startsWith(queryInput.keyPrefix), | 1211 | queryInput?.keyPrefix === undefined |
| | 1212 | ? true |
| | 1213 | : row.key.startsWith(queryInput.keyPrefix), |
| 1035 | ) | 1214 | ) |
| 1036 | .filter((row) => | 1215 | .filter((row) => |
| 1037 | rowMatchesPluginStorageWhere( | 1216 | rowMatchesPluginStorageWhere( |
| @@ -1053,7 +1232,8 @@ const makePluginStorageFacade = (input: { · 15 unmodified lines |
| 1053 | if (compared !== 0) return compared; | 1232 | if (compared !== 0) return compared; |
| 1054 | } | 1233 | } |
| 1055 | return ( | 1234 | return ( |
| 1056 | ownerRank(left.owner as Owner) - ownerRank(right.owner as Owner) || | 1235 | ownerRank(left.owner as Owner) - |
| | 1236 | ownerRank(right.owner as Owner) || |
| 1057 | left.key.localeCompare(right.key) | 1237 | left.key.localeCompare(right.key) |
| 1058 | ); | 1238 | ); |
| 1059 | }) | 1239 | }) |
| @@ -1065,7 +1245,9 @@ const makePluginStorageFacade = (input: { · 5 unmodified lines |
| 1065 | ? sorted.slice(offset) | 1245 | ? sorted.slice(offset) |
| 1066 | : sorted.slice(offset, offset + queryInput.limit); | 1246 | : sorted.slice(offset, offset + queryInput.limit); |
| 1067 | return limited.map((row) => | 1247 | return limited.map((row) => |
| 1068 | pluginStorageEntryFromRow<PluginStorageCollectionData<TDefinition>>(row), | 1248 | pluginStorageEntryFromRow<PluginStorageCollectionData<TDefinition>>( |
| | 1249 | row, |
| | 1250 | ), |
| 1069 | ); | 1251 | ); |
| 1070 | }); | 1252 | }); |
| 1071 | | 1253 | |
| @@ -1073,15 +1255,24 @@ const makePluginStorageFacade = (input: { · 1 unmodified line |
| 1073 | collection: (definition) => ({ | 1255 | collection: (definition) => ({ |
| 1074 | get: (storageInput) => | 1256 | get: (storageInput) => |
| 1075 | getVisible(definition.name, storageInput.key) as Effect.Effect< | 1257 | getVisible(definition.name, storageInput.key) as Effect.Effect< |
| 1076 | PluginStorageEntry<PluginStorageCollectionData<typeof definition>> | null, | 1258 | PluginStorageEntry< |
| | 1259 | PluginStorageCollectionData<typeof definition> |
| | 1260 | > | null, |
| 1077 | StorageFailure | 1261 | StorageFailure |
| 1078 | >, | 1262 | >, |
| 1079 | getForOwner: (storageInput) => | 1263 | getForOwner: (storageInput) => |
| 1080 | getForOwnerImpl(storageInput.owner, definition.name, storageInput.key) as Effect.Effect< | 1264 | getForOwnerImpl( |
| 1081 | PluginStorageEntry<PluginStorageCollectionData<typeof definition>> | null, | 1265 | storageInput.owner, |
| | 1266 | definition.name, |
| | 1267 | storageInput.key, |
| | 1268 | ) as Effect.Effect< |
| | 1269 | PluginStorageEntry< |
| | 1270 | PluginStorageCollectionData<typeof definition> |
| | 1271 | > | null, |
| 1082 | StorageFailure | 1272 | StorageFailure |
| 1083 | >, | 1273 | >, |
| 1084 | list: (storageInput) => queryCollection(definition, { keyPrefix: storageInput?.keyPrefix }), | 1274 | list: (storageInput) => |
| | 1275 | queryCollection(definition, { keyPrefix: storageInput?.keyPrefix }), |
| 1085 | put: (storageInput) => | 1276 | put: (storageInput) => |
| 1086 | putImpl( | 1277 | putImpl( |
| 1087 | storageInput.owner, | 1278 | storageInput.owner, |
| @@ -1094,12 +1285,20 @@ const makePluginStorageFacade = (input: { · 6 unmodified lines |
| 1094 | >, | 1285 | >, |
| 1095 | query: (storageInput) => queryCollection(definition, storageInput), | 1286 | query: (storageInput) => queryCollection(definition, storageInput), |
| 1096 | count: (storageInput) => | 1287 | count: (storageInput) => |
| 1097 | queryCollection(definition, storageInput).pipe(Effect.map((rows) => rows.length)), | 1288 | queryCollection(definition, storageInput).pipe( |
| 1098 | remove: (storageInput) => removeImpl(storageInput.owner, definition.name, storageInput.key), | 1289 | Effect.map((rows) => rows.length), |
| | 1290 | ), |
| | 1291 | remove: (storageInput) => |
| | 1292 | removeImpl(storageInput.owner, definition.name, storageInput.key), |
| 1099 | }), | 1293 | }), |
| 1100 | get: (storageInput) => getVisible(storageInput.collection, storageInput.key), | 1294 | get: (storageInput) => |
| | 1295 | getVisible(storageInput.collection, storageInput.key), |
| 1101 | getForOwner: (storageInput) => | 1296 | getForOwner: (storageInput) => |
| 1102 | getForOwnerImpl(storageInput.owner, storageInput.collection, storageInput.key), | 1297 | getForOwnerImpl( |
| | 1298 | storageInput.owner, |
| | 1299 | storageInput.collection, |
| | 1300 | storageInput.key, |
| | 1301 | ), |
| 1103 | list: (storageInput) => | 1302 | list: (storageInput) => |
| 1104 | Effect.gen(function* () { | 1303 | Effect.gen(function* () { |
| 1105 | const rows = yield* input.core.findMany("plugin_storage", { | 1304 | const rows = yield* input.core.findMany("plugin_storage", { |
| @@ -1114,11 +1313,18 @@ const makePluginStorageFacade = (input: { · 8 unmodified lines |
| 1114 | .map((row) => pluginStorageEntryFromRow(row)); | 1313 | .map((row) => pluginStorageEntryFromRow(row)); |
| 1115 | }), | 1314 | }), |
| 1116 | put: (storageInput) => | 1315 | put: (storageInput) => |
| 1117 | putImpl(storageInput.owner, storageInput.collection, storageInput.key, storageInput.data), | 1316 | putImpl( |
| 1118 | putMany: (storageInput) => putManyImpl(storageInput.owner, storageInput.entries), | 1317 | storageInput.owner, |
| | 1318 | storageInput.collection, |
| | 1319 | storageInput.key, |
| | 1320 | storageInput.data, |
| | 1321 | ), |
| | 1322 | putMany: (storageInput) => |
| | 1323 | putManyImpl(storageInput.owner, storageInput.entries), |
| 1119 | remove: (storageInput) => | 1324 | remove: (storageInput) => |
| 1120 | removeImpl(storageInput.owner, storageInput.collection, storageInput.key), | 1325 | removeImpl(storageInput.owner, storageInput.collection, storageInput.key), |
| 1121 | removeMany: (storageInput) => removeManyImpl(storageInput.owner, storageInput.entries), | 1326 | removeMany: (storageInput) => |
| | 1327 | removeManyImpl(storageInput.owner, storageInput.entries), |
| 1122 | }; | 1328 | }; |
| 1123 | }; | 1329 | }; |
| 1124 | | 1330 | |
| @@ -1161,7 +1367,9 @@ const EXECUTOR_SOURCE: StaticSourceDecl = { · 36 unmodified lines |
| 1161 | tools: [], | 1367 | tools: [], |
| 1162 | }; | 1368 | }; |
| 1163 | | 1369 | |
| 1164 | const isReadonlyRecord = (value: unknown): value is Readonly<Record<PropertyKey, unknown>> => | 1370 | const isReadonlyRecord = ( |
| | 1371 | value: unknown, |
| | 1372 | ): value is Readonly<Record<PropertyKey, unknown>> => |
| 1165 | typeof value === "object" && value !== null; | 1373 | typeof value === "object" && value !== null; |
| 1166 | | 1374 | |
| 1167 | type StandardJsonSchemaSide = "input" | "output"; | 1375 | type StandardJsonSchemaSide = "input" | "output"; |
| @@ -1180,10 +1388,14 @@ const staticToolSchemaRoot = ( · 12 unmodified lines |
| 1180 | const jsonSchema = standard["jsonSchema"]; | 1388 | const jsonSchema = standard["jsonSchema"]; |
| 1181 | if (!isReadonlyRecord(jsonSchema)) return schema; | 1389 | if (!isReadonlyRecord(jsonSchema)) return schema; |
| 1182 | const materialize = (jsonSchema as StandardJsonSchemaFns)[side]; | 1390 | const materialize = (jsonSchema as StandardJsonSchemaFns)[side]; |
| 1183 | return typeof materialize === "function" ? materialize({ target: "draft-07" }) : jsonSchema; | 1391 | return typeof materialize === "function" |
| | 1392 | ? materialize({ target: "draft-07" }) |
| | 1393 | : jsonSchema; |
| 1184 | }; | 1394 | }; |
| 1185 | | 1395 | |
| 1186 | export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = readonly []>( | 1396 | export const createExecutor = < |
| | 1397 | const TPlugins extends readonly AnyPlugin[] = readonly [], |
| | 1398 | >( |
| 1187 | config: ExecutorConfig<TPlugins>, | 1399 | config: ExecutorConfig<TPlugins>, |
| 1188 | ): Effect.Effect<Executor<TPlugins>, StorageFailure> => | 1400 | ): Effect.Effect<Executor<TPlugins>, StorageFailure> => |
| 1189 | Effect.gen(function* () { | 1401 | Effect.gen(function* () { |
| @@ -1213,7 +1425,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 23 unmodified lines |
| 1213 | return { tenant, owner, subject }; | 1425 | return { tenant, owner, subject }; |
| 1214 | }; | 1426 | }; |
| 1215 | | 1427 | |
| 1216 | const requireUserSubject = (owner: Owner): Effect.Effect<void, StorageFailure> => | 1428 | const requireUserSubject = ( |
| | 1429 | owner: Owner, |
| | 1430 | ): Effect.Effect<void, StorageFailure> => |
| 1217 | owner === "user" && subject == null | 1431 | owner === "user" && subject == null |
| 1218 | ? Effect.fail( | 1432 | ? Effect.fail( |
| 1219 | new StorageError({ | 1433 | new StorageError({ |
| @@ -1236,7 +1450,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 16 unmodified lines |
| 1236 | | 1450 | |
| 1237 | const tables = yield* Effect.try({ | 1451 | const tables = yield* Effect.try({ |
| 1238 | try: () => collectTables(), | 1452 | try: () => collectTables(), |
| 1239 | catch: (cause) => storageFailureFromUnknown("Failed to collect executor tables", cause), | 1453 | catch: (cause) => |
| | 1454 | storageFailureFromUnknown("Failed to collect executor tables", cause), |
| 1240 | }); | 1455 | }); |
| 1241 | const dbInput = yield* Effect.suspend(() => { | 1456 | const dbInput = yield* Effect.suspend(() => { |
| 1242 | if (!config.db) return Effect.succeed(createDefaultMemoryDb(tables)); | 1457 | if (!config.db) return Effect.succeed(createDefaultMemoryDb(tables)); |
| @@ -1251,7 +1466,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 8 unmodified lines |
| 1251 | validateExecutorDbTables(tables, rootDbUntyped.internal.tables); | 1466 | validateExecutorDbTables(tables, rootDbUntyped.internal.tables); |
| 1252 | validateExecutorOwnerPolicyTables(rootDbUntyped.internal.tables); | 1467 | validateExecutorOwnerPolicyTables(rootDbUntyped.internal.tables); |
| 1253 | }, | 1468 | }, |
| 1254 | catch: (cause) => storageFailureFromUnknown("Failed to validate executor tables", cause), | 1469 | catch: (cause) => |
| | 1470 | storageFailureFromUnknown("Failed to validate executor tables", cause), |
| 1255 | }); | 1471 | }); |
| 1256 | | 1472 | |
| 1257 | const ownerContext: ExecutorOwnerPolicyContext = { tenant, subject }; | 1473 | const ownerContext: ExecutorOwnerPolicyContext = { tenant, subject }; |
| @@ -1259,7 +1475,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1 unmodified line |
| 1259 | const fuma = makeFumaClient(rootDb); | 1475 | const fuma = makeFumaClient(rootDb); |
| 1260 | const core = makeCoreDb(fuma); | 1476 | const core = makeCoreDb(fuma); |
| 1261 | const blobs = config.blobs ?? makeFumaBlobStore(fuma); | 1477 | const blobs = config.blobs ?? makeFumaBlobStore(fuma); |
| 1262 | const transaction = <A, E>(effect: Effect.Effect<A, E>) => fuma.transaction(effect); | 1478 | const transaction = <A, E>(effect: Effect.Effect<A, E>) => |
| | 1479 | fuma.transaction(effect); |
| 1263 | | 1480 | |
| 1264 | // Populated once, never mutated after startup. | 1481 | // Populated once, never mutated after startup. |
| 1265 | const staticTools = new Map<string, StaticTools>(); | 1482 | const staticTools = new Map<string, StaticTools>(); |
| @@ -1270,7 +1487,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 4 unmodified lines |
| 1270 | | 1487 | |
| 1271 | const staticToolOwner = (): Owner => (subject == null ? "org" : "user"); | 1488 | const staticToolOwner = (): Owner => (subject == null ? "org" : "user"); |
| 1272 | const staticToolConnection = (source: StaticSourceDecl): ConnectionName => | 1489 | const staticToolConnection = (source: StaticSourceDecl): ConnectionName => |
| 1273 | ConnectionName.make(source.id === EXECUTOR_SOURCE_ID ? "coreTools" : "static"); | 1490 | ConnectionName.make( |
| | 1491 | source.id === EXECUTOR_SOURCE_ID ? "coreTools" : "static", |
| | 1492 | ); |
| 1274 | | 1493 | |
| 1275 | const staticSources = (): readonly StaticSourceDecl[] => { | 1494 | const staticSources = (): readonly StaticSourceDecl[] => { |
| 1276 | const byId = new Map<string, StaticSourceDecl>(); | 1495 | const byId = new Map<string, StaticSourceDecl>(); |
| @@ -1280,7 +1499,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines |
| 1280 | return [...byId.values()]; | 1499 | return [...byId.values()]; |
| 1281 | }; | 1500 | }; |
| 1282 | | 1501 | |
| 1283 | const staticSourceToIntegration = (source: StaticSourceDecl): Integration => ({ | 1502 | const staticSourceToIntegration = ( |
| | 1503 | source: StaticSourceDecl, |
| | 1504 | ): Integration => ({ |
| 1284 | slug: IntegrationSlug.make(source.id), | 1505 | slug: IntegrationSlug.make(source.id), |
| 1285 | name: source.name, | 1506 | name: source.name, |
| 1286 | description: source.name, | 1507 | description: source.name, |
| @@ -1336,6 +1557,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 49 unmodified lines |
| 1336 | }; | 1557 | }; |
| 1337 | | 1558 | |
| 1338 | const extensions: Record<string, object> = {}; | 1559 | const extensions: Record<string, object> = {}; |
| | 1560 | // Generic executor-wrapper hook: a plugin may expose `wrapExecutor` on its |
| | 1561 | // extension to narrow the executor per request (the toolkits plugin does). |
| | 1562 | // Collected here, run by the execution stack — core stays domain-agnostic. |
| | 1563 | const executorWrappers: ExecutorWrapper[] = []; |
| 1339 | | 1564 | |
| 1340 | // ------------------------------------------------------------------ | 1565 | // ------------------------------------------------------------------ |
| 1341 | // Owner condition builders. The owner policy already restricts reads to | 1566 | // Owner condition builders. The owner policy already restricts reads to |
| @@ -1389,7 +1614,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 47 unmodified lines |
| 1389 | const performTokenRefresh = ( | 1614 | const performTokenRefresh = ( |
| 1390 | row: ConnectionRow, | 1615 | row: ConnectionRow, |
| 1391 | provider: CredentialProvider, | 1616 | provider: CredentialProvider, |
| 1392 | ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> => | 1617 | ): Effect.Effect< |
| | 1618 | string | null, |
| | 1619 | StorageFailure | CredentialResolutionError |
| | 1620 | > => |
| 1393 | Effect.gen(function* () { | 1621 | Effect.gen(function* () { |
| 1394 | const owner = row.owner as Owner; | 1622 | const owner = row.owner as Owner; |
| 1395 | const reauth = (message: string): CredentialResolutionError => | 1623 | const reauth = (message: string): CredentialResolutionError => |
| @@ -1404,15 +1632,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 8 unmodified lines |
| 1404 | // Load the backing app by the owner STORED on the connection (a Personal | 1632 | // Load the backing app by the owner STORED on the connection (a Personal |
| 1405 | // connection may be backed by a shared Workspace app) — no derivation. | 1633 | // connection may be backed by a shared Workspace app) — no derivation. |
| 1406 | const clientOwner = (row.oauth_client_owner ?? row.owner) as Owner; | 1634 | const clientOwner = (row.oauth_client_owner ?? row.owner) as Owner; |
| 1407 | const clientRow = yield* loadOAuthClientRow(clientOwner, String(row.oauth_client)); | 1635 | const clientRow = yield* loadOAuthClientRow( |
| | 1636 | clientOwner, |
| | 1637 | String(row.oauth_client), |
| | 1638 | ); |
| 1408 | if (!clientRow) { | 1639 | if (!clientRow) { |
| 1409 | return yield* reauth(`OAuth client "${row.oauth_client}" is no longer registered.`); | 1640 | return yield* reauth( |
| | 1641 | `OAuth client "${row.oauth_client}" is no longer registered.`, |
| | 1642 | ); |
| 1410 | } | 1643 | } |
| 1411 | | 1644 | |
| 1412 | // The secret is stored in the provider (a vault item id), not inline. | 1645 | // The secret is stored in the provider (a vault item id), not inline. |
| 1413 | const clientSecret = clientRow.client_secret_item_id | 1646 | const clientSecret = clientRow.client_secret_item_id |
| 1414 | ? ((yield* provider.get(ProviderItemId.make(String(clientRow.client_secret_item_id)))) ?? | 1647 | ? ((yield* provider.get( |
| 1415 | "") | 1648 | ProviderItemId.make(String(clientRow.client_secret_item_id)), |
| | 1649 | )) ?? "") |
| 1416 | : ""; | 1650 | : ""; |
| 1417 | // Re-request the scopes this connection was GRANTED (RFC 6749 §6: a | 1651 | // Re-request the scopes this connection was GRANTED (RFC 6749 §6: a |
| 1418 | // refresh must not exceed the originally-granted scope). Empty → omit | 1652 | // refresh must not exceed the originally-granted scope). Empty → omit |
| @@ -1433,7 +1667,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines |
| 1433 | clientId: String(clientRow.client_id), | 1667 | clientId: String(clientRow.client_id), |
| 1434 | clientSecret, | 1668 | clientSecret, |
| 1435 | scopes: grantedScopes, | 1669 | scopes: grantedScopes, |
| 1436 | resource: clientRow.resource ? String(clientRow.resource) : undefined, | 1670 | resource: clientRow.resource |
| | 1671 | ? String(clientRow.resource) |
| | 1672 | : undefined, |
| 1437 | endpointUrlPolicy: config.oauthEndpointUrlPolicy, | 1673 | endpointUrlPolicy: config.oauthEndpointUrlPolicy, |
| 1438 | }).pipe( | 1674 | }).pipe( |
| 1439 | // A client_credentials failure is never a rotated-refresh-token | 1675 | // A client_credentials failure is never a rotated-refresh-token |
| @@ -1451,11 +1687,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 11 unmodified lines |
| 1451 | ) | 1687 | ) |
| 1452 | : yield* Effect.gen(function* () { | 1688 | : yield* Effect.gen(function* () { |
| 1453 | if (!row.refresh_item_id) { | 1689 | if (!row.refresh_item_id) { |
| 1454 | return yield* reauth("No refresh token is stored for this connection."); | 1690 | return yield* reauth( |
| | 1691 | "No refresh token is stored for this connection.", |
| | 1692 | ); |
| 1455 | } | 1693 | } |
| 1456 | const refreshToken = yield* provider.get(ProviderItemId.make(row.refresh_item_id)); | 1694 | const refreshToken = yield* provider.get( |
| | 1695 | ProviderItemId.make(row.refresh_item_id), |
| | 1696 | ); |
| 1457 | if (!refreshToken) { | 1697 | if (!refreshToken) { |
| 1458 | return yield* reauth("Stored refresh token could not be resolved."); | 1698 | return yield* reauth( |
| | 1699 | "Stored refresh token could not be resolved.", |
| | 1700 | ); |
| 1459 | } | 1701 | } |
| 1460 | return yield* refreshAccessToken({ | 1702 | return yield* refreshAccessToken({ |
| 1461 | tokenUrl: String(clientRow.token_url), | 1703 | tokenUrl: String(clientRow.token_url), |
| @@ -1465,7 +1707,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines |
| 1465 | scopes: grantedScopes, | 1707 | scopes: grantedScopes, |
| 1466 | // RFC 8707: keep the re-minted token bound to the same resource | 1708 | // RFC 8707: keep the re-minted token bound to the same resource |
| 1467 | // (MCP servers require this on refresh). | 1709 | // (MCP servers require this on refresh). |
| 1468 | resource: clientRow.resource ? String(clientRow.resource) : undefined, | 1710 | resource: clientRow.resource |
| | 1711 | ? String(clientRow.resource) |
| | 1712 | : undefined, |
| 1469 | endpointUrlPolicy: config.oauthEndpointUrlPolicy, | 1713 | endpointUrlPolicy: config.oauthEndpointUrlPolicy, |
| 1470 | }).pipe( | 1714 | }).pipe( |
| 1471 | Effect.mapError((cause) => | 1715 | Effect.mapError((cause) => |
| @@ -1489,14 +1733,22 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 17 unmodified lines |
| 1489 | const tokenItemId = | 1733 | const tokenItemId = |
| 1490 | connectionItemIds(row)[PRIMARY_INPUT_VARIABLE] ?? | 1734 | connectionItemIds(row)[PRIMARY_INPUT_VARIABLE] ?? |
| 1491 | `connection:${row.owner}:${row.integration}:${row.name}:${PRIMARY_INPUT_VARIABLE}`; | 1735 | `connection:${row.owner}:${row.integration}:${row.name}:${PRIMARY_INPUT_VARIABLE}`; |
| 1492 | yield* provider.set(ProviderItemId.make(tokenItemId), token.access_token); | 1736 | yield* provider.set( |
| | 1737 | ProviderItemId.make(tokenItemId), |
| | 1738 | token.access_token, |
| | 1739 | ); |
| 1493 | if (token.refresh_token && row.refresh_item_id) { | 1740 | if (token.refresh_token && row.refresh_item_id) { |
| 1494 | yield* provider.set(ProviderItemId.make(row.refresh_item_id), token.refresh_token); | 1741 | yield* provider.set( |
| | 1742 | ProviderItemId.make(row.refresh_item_id), |
| | 1743 | token.refresh_token, |
| | 1744 | ); |
| 1495 | } | 1745 | } |
| 1496 | } | 1746 | } |
| 1497 | | 1747 | |
| 1498 | const nextExpiresAt = | 1748 | const nextExpiresAt = |
| 1499 | typeof token.expires_in === "number" ? Date.now() + token.expires_in * 1000 : null; | 1749 | typeof token.expires_in === "number" |
| | 1750 | ? Date.now() + token.expires_in * 1000 |
| | 1751 | : null; |
| 1500 | const set: Record<string, unknown> = { | 1752 | const set: Record<string, unknown> = { |
| 1501 | expires_at: nextExpiresAt, | 1753 | expires_at: nextExpiresAt, |
| 1502 | updated_at: new Date(), | 1754 | updated_at: new Date(), |
| @@ -1518,7 +1770,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 15 unmodified lines |
| 1518 | const refreshConnectionToken = ( | 1770 | const refreshConnectionToken = ( |
| 1519 | row: ConnectionRow, | 1771 | row: ConnectionRow, |
| 1520 | provider: CredentialProvider, | 1772 | provider: CredentialProvider, |
| 1521 | ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> => | 1773 | ): Effect.Effect< |
| | 1774 | string | null, |
| | 1775 | StorageFailure | CredentialResolutionError |
| | 1776 | > => |
| 1522 | // Share a single refresh per connection so concurrent resolves of the same | 1777 | // Share a single refresh per connection so concurrent resolves of the same |
| 1523 | // connection all await one refresh-token grant (the AS rotates the refresh | 1778 | // connection all await one refresh-token grant (the AS rotates the refresh |
| 1524 | // token; parallel grants would race on a consumed token — v1's refresh | 1779 | // token; parallel grants would race on a consumed token — v1's refresh |
| @@ -1530,7 +1785,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5 unmodified lines |
| 1530 | if (existing) return yield* existing; | 1785 | if (existing) return yield* existing; |
| 1531 | // `Effect.cached` memoizes the grant onto a deferred: it runs once and | 1786 | // `Effect.cached` memoizes the grant onto a deferred: it runs once and |
| 1532 | // replays to every awaiter sharing this entry. | 1787 | // replays to every awaiter sharing this entry. |
| 1533 | const memoized = yield* Effect.cached(performTokenRefresh(row, provider)); | 1788 | const memoized = yield* Effect.cached( |
| | 1789 | performTokenRefresh(row, provider), |
| | 1790 | ); |
| 1534 | const gated = memoized.pipe( | 1791 | const gated = memoized.pipe( |
| 1535 | Effect.ensuring(Effect.sync(() => refreshInFlight.delete(key))), | 1792 | Effect.ensuring(Effect.sync(() => refreshInFlight.delete(key))), |
| 1536 | ); | 1793 | ); |
| @@ -1547,7 +1804,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 10 unmodified lines |
| 1547 | // first (always single-input → `{ token: <access> }`). | 1804 | // first (always single-input → `{ token: <access> }`). |
| 1548 | const resolveConnectionValues = ( | 1805 | const resolveConnectionValues = ( |
| 1549 | row: ConnectionRow, | 1806 | row: ConnectionRow, |
| 1550 | ): Effect.Effect<Record<string, string | null>, StorageFailure | CredentialResolutionError> => | 1807 | ): Effect.Effect< |
| | 1808 | Record<string, string | null>, |
| | 1809 | StorageFailure | CredentialResolutionError |
| | 1810 | > => |
| 1551 | Effect.gen(function* () { | 1811 | Effect.gen(function* () { |
| 1552 | const provider = credentialProviders.get(row.provider); | 1812 | const provider = credentialProviders.get(row.provider); |
| 1553 | if (!provider) { | 1813 | if (!provider) { |
| @@ -1557,13 +1817,16 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines |
| 1557 | } | 1817 | } |
| 1558 | // OAuth connections refresh their access token before resolving when | 1818 | // OAuth connections refresh their access token before resolving when |
| 1559 | // it has expired (or is within the skew window). | 1819 | // it has expired (or is within the skew window). |
| 1560 | const expiresAt = row.expires_at == null ? null : Number(row.expires_at); | 1820 | const expiresAt = |
| | 1821 | row.expires_at == null ? null : Number(row.expires_at); |
| 1561 | if (row.oauth_client != null && shouldRefreshToken({ expiresAt })) { | 1822 | if (row.oauth_client != null && shouldRefreshToken({ expiresAt })) { |
| 1562 | const access = yield* refreshConnectionToken(row, provider); | 1823 | const access = yield* refreshConnectionToken(row, provider); |
| 1563 | return { [PRIMARY_INPUT_VARIABLE]: access }; | 1824 | return { [PRIMARY_INPUT_VARIABLE]: access }; |
| 1564 | } | 1825 | } |
| 1565 | const out: Record<string, string | null> = {}; | 1826 | const out: Record<string, string | null> = {}; |
| 1566 | for (const [variable, itemId] of Object.entries(connectionItemIds(row))) { | 1827 | for (const [variable, itemId] of Object.entries( |
| | 1828 | connectionItemIds(row), |
| | 1829 | )) { |
| 1567 | out[variable] = yield* provider.get(ProviderItemId.make(itemId)); | 1830 | out[variable] = yield* provider.get(ProviderItemId.make(itemId)); |
| 1568 | } | 1831 | } |
| 1569 | return out; | 1832 | return out; |
| @@ -1584,7 +1847,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines |
| 1584 | * callers that only ever need one value. */ | 1847 | * callers that only ever need one value. */ |
| 1585 | const resolveConnectionValue = ( | 1848 | const resolveConnectionValue = ( |
| 1586 | row: ConnectionRow, | 1849 | row: ConnectionRow, |
| 1587 | ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> => | 1850 | ): Effect.Effect< |
| | 1851 | string | null, |
| | 1852 | StorageFailure | CredentialResolutionError |
| | 1853 | > => |
| 1588 | resolveConnectionValues(row).pipe( | 1854 | resolveConnectionValues(row).pipe( |
| 1589 | Effect.map((values) => values[PRIMARY_INPUT_VARIABLE] ?? null), | 1855 | Effect.map((values) => values[PRIMARY_INPUT_VARIABLE] ?? null), |
| 1590 | ); | 1856 | ); |
| @@ -1644,7 +1910,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 53 unmodified lines |
| 1644 | // plugin's `describeAuthMethods` hook. The hook is plugin-authored, so a | 1910 | // plugin's `describeAuthMethods` hook. The hook is plugin-authored, so a |
| 1645 | // throw (malformed config it didn't guard) degrades to `[]` rather than | 1911 | // throw (malformed config it didn't guard) degrades to `[]` rather than |
| 1646 | // failing the catalog read. | 1912 | // failing the catalog read. |
| 1647 | const describeAuthMethodsForRow = (row: IntegrationRow): readonly AuthMethodDescriptor[] => { | 1913 | const describeAuthMethodsForRow = ( |
| | 1914 | row: IntegrationRow, |
| | 1915 | ): readonly AuthMethodDescriptor[] => { |
| 1648 | const runtime = runtimes.get(row.plugin_id); | 1916 | const runtime = runtimes.get(row.plugin_id); |
| 1649 | const describe = runtime?.plugin.describeAuthMethods; | 1917 | const describe = runtime?.plugin.describeAuthMethods; |
| 1650 | if (!describe) return []; | 1918 | if (!describe) return []; |
| @@ -1657,7 +1925,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 6 unmodified lines |
| 1657 | } | 1925 | } |
| 1658 | }; | 1926 | }; |
| 1659 | | 1927 | |
| 1660 | const describeDisplayUrlForRow = (row: IntegrationRow): string | undefined => { | 1928 | const describeDisplayUrlForRow = ( |
| | 1929 | row: IntegrationRow, |
| | 1930 | ): string | undefined => { |
| 1661 | const runtime = runtimes.get(row.plugin_id); | 1931 | const runtime = runtimes.get(row.plugin_id); |
| 1662 | const describe = runtime?.plugin.describeIntegrationDisplay; | 1932 | const describe = runtime?.plugin.describeIntegrationDisplay; |
| 1663 | if (!describe) return undefined; | 1933 | if (!describe) return undefined; |
| @@ -1671,14 +1941,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines |
| 1671 | } | 1941 | } |
| 1672 | }; | 1942 | }; |
| 1673 | | 1943 | |
| 1674 | const integrationsList = (): Effect.Effect<readonly Integration[], StorageFailure> => | 1944 | const integrationsList = (): Effect.Effect< |
| | 1945 | readonly Integration[], |
| | 1946 | StorageFailure |
| | 1947 | > => |
| 1675 | core | 1948 | core |
| 1676 | .findMany("integration", {}) | 1949 | .findMany("integration", {}) |
| 1677 | .pipe( | 1950 | .pipe( |
| 1678 | Effect.map((rows) => [ | 1951 | Effect.map((rows) => [ |
| 1679 | ...staticSources().map(staticSourceToIntegration), | 1952 | ...staticSources().map(staticSourceToIntegration), |
| 1680 | ...rows.map((row) => | 1953 | ...rows.map((row) => |
| 1681 | rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayUrlForRow(row)), | 1954 | rowToIntegration( |
| | 1955 | row, |
| | 1956 | describeAuthMethodsForRow(row), |
| | 1957 | describeDisplayUrlForRow(row), |
| | 1958 | ), |
| 1682 | ), | 1959 | ), |
| 1683 | ]), | 1960 | ]), |
| 1684 | ); | 1961 | ); |
| @@ -1687,11 +1964,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2 unmodified lines |
| 1687 | slug: IntegrationSlug, | 1964 | slug: IntegrationSlug, |
| 1688 | ): Effect.Effect<Integration | null, StorageFailure> => | 1965 | ): Effect.Effect<Integration | null, StorageFailure> => |
| 1689 | Effect.gen(function* () { | 1966 | Effect.gen(function* () { |
| 1690 | const staticSource = staticSources().find((source) => source.id === String(slug)); | 1967 | const staticSource = staticSources().find( |
| | 1968 | (source) => source.id === String(slug), |
| | 1969 | ); |
| 1691 | if (staticSource) return staticSourceToIntegration(staticSource); | 1970 | if (staticSource) return staticSourceToIntegration(staticSource); |
| 1692 | const row = yield* findIntegrationRow(slug); | 1971 | const row = yield* findIntegrationRow(slug); |
| 1693 | return row | 1972 | return row |
| 1694 | ? rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayUrlForRow(row)) | 1973 | ? rowToIntegration( |
| | 1974 | row, |
| | 1975 | describeAuthMethodsForRow(row), |
| | 1976 | describeDisplayUrlForRow(row), |
| | 1977 | ) |
| 1695 | : null; | 1978 | : null; |
| 1696 | }); | 1979 | }); |
| 1697 | | 1980 | |
| @@ -1700,7 +1983,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2 unmodified lines |
| 1700 | ): Effect.Effect<IntegrationRecord | null, StorageFailure> => | 1983 | ): Effect.Effect<IntegrationRecord | null, StorageFailure> => |
| 1701 | findIntegrationRow(slug).pipe( | 1984 | findIntegrationRow(slug).pipe( |
| 1702 | Effect.map((row) => | 1985 | Effect.map((row) => |
| 1703 | row ? rowToIntegrationRecord(row, describeAuthMethodsForRow(row)) : null, | 1986 | row |
| | 1987 | ? rowToIntegrationRecord(row, describeAuthMethodsForRow(row)) |
| | 1988 | : null, |
| 1704 | ), | 1989 | ), |
| 1705 | ); | 1990 | ); |
| 1706 | | 1991 | |
| @@ -1755,7 +2040,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 48 unmodified lines |
| 1755 | const now = new Date(); | 2040 | const now = new Date(); |
| 1756 | const set: Record<string, unknown> = { updated_at: now }; | 2041 | const set: Record<string, unknown> = { updated_at: now }; |
| 1757 | if (patch.name !== undefined) set.name = patch.name; | 2042 | if (patch.name !== undefined) set.name = patch.name; |
| 1758 | if (patch.description !== undefined) set.description = patch.description; | 2043 | if (patch.description !== undefined) |
| | 2044 | set.description = patch.description; |
| 1759 | if (patch.config !== undefined) { | 2045 | if (patch.config !== undefined) { |
| 1760 | set.config = patch.config; | 2046 | set.config = patch.config; |
| 1761 | // A config change can change the derived tools. The writer can only | 2047 | // A config change can change the derived tools. The writer can only |
| @@ -1782,7 +2068,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 20 unmodified lines |
| 1782 | | 2068 | |
| 1783 | const integrationsRemove = ( | 2069 | const integrationsRemove = ( |
| 1784 | slug: IntegrationSlug, | 2070 | slug: IntegrationSlug, |
| 1785 | ): Effect.Effect<void, IntegrationRemovalNotAllowedError | StorageFailure> => | 2071 | ): Effect.Effect< |
| | 2072 | void, |
| | 2073 | IntegrationRemovalNotAllowedError | StorageFailure |
| | 2074 | > => |
| 1786 | transaction( | 2075 | transaction( |
| 1787 | Effect.gen(function* () { | 2076 | Effect.gen(function* () { |
| 1788 | const existing = yield* findIntegrationRow(slug); | 2077 | const existing = yield* findIntegrationRow(slug); |
| @@ -1811,7 +2100,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 22 unmodified lines |
| 1811 | const result = yield* runtime.plugin | 2100 | const result = yield* runtime.plugin |
| 1812 | .detect({ ctx: runtime.ctx, url }) | 2101 | .detect({ ctx: runtime.ctx, url }) |
| 1813 | .pipe( | 2102 | .pipe( |
| 1814 | Effect.mapError((cause) => pluginStorageFailure(runtime.plugin.id, "detect", cause)), | 2103 | Effect.mapError((cause) => |
| | 2104 | pluginStorageFailure(runtime.plugin.id, "detect", cause), |
| | 2105 | ), |
| 1815 | ); | 2106 | ); |
| 1816 | if (result) results.push(result); | 2107 | if (result) results.push(result); |
| 1817 | } | 2108 | } |
| @@ -1825,7 +2116,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines |
| 1825 | const produceConnectionTools = ( | 2116 | const produceConnectionTools = ( |
| 1826 | integrationRow: IntegrationRow, | 2117 | integrationRow: IntegrationRow, |
| 1827 | ref: ConnectionRef, | 2118 | ref: ConnectionRef, |
| 1828 | ): Effect.Effect<readonly Tool[], IntegrationNotFoundError | StorageFailure> => | 2119 | ): Effect.Effect< |
| | 2120 | readonly Tool[], |
| | 2121 | IntegrationNotFoundError | StorageFailure |
| | 2122 | > => |
| 1829 | Effect.gen(function* () { | 2123 | Effect.gen(function* () { |
| 1830 | const runtime = runtimes.get(integrationRow.plugin_id); | 2124 | const runtime = runtimes.get(integrationRow.plugin_id); |
| 1831 | const keys = yield* Effect.try({ | 2125 | const keys = yield* Effect.try({ |
| @@ -1894,14 +2188,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 62 unmodified lines |
| 1894 | integration: rowToIntegration(integrationRow), | 2188 | integration: rowToIntegration(integrationRow), |
| 1895 | config: decodeJsonColumn(integrationRow.config), | 2189 | config: decodeJsonColumn(integrationRow.config), |
| 1896 | connection: ref, | 2190 | connection: ref, |
| 1897 | template: existingRow ? AuthTemplateSlug.make(existingRow.template) : null, | 2191 | template: existingRow |
| | 2192 | ? AuthTemplateSlug.make(existingRow.template) |
| | 2193 | : null, |
| 1898 | storage: runtime.storage, | 2194 | storage: runtime.storage, |
| 1899 | getValue: () => resolveConnectionValueByRef(ref), | 2195 | getValue: () => resolveConnectionValueByRef(ref), |
| 1900 | getValues: () => resolveConnectionValuesByRef(ref), | 2196 | getValues: () => resolveConnectionValuesByRef(ref), |
| 1901 | }) | 2197 | }) |
| 1902 | .pipe( | 2198 | .pipe( |
| 1903 | Effect.mapError((cause) => | 2199 | Effect.mapError((cause) => |
| 1904 | pluginStorageFailure(integrationRow.plugin_id, "resolveTools", cause), | 2200 | pluginStorageFailure( |
| | 2201 | integrationRow.plugin_id, |
| | 2202 | "resolveTools", |
| | 2203 | cause, |
| | 2204 | ), |
| 1905 | ), | 2205 | ), |
| 1906 | ); | 2206 | ); |
| 1907 | | 2207 | |
| @@ -1922,17 +2222,19 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines |
| 1922 | updated_at: now, | 2222 | updated_at: now, |
| 1923 | })); | 2223 | })); |
| 1924 | | 2224 | |
| 1925 | const definitionRows = Object.entries(result.definitions ?? {}).map(([name, schema]) => ({ | 2225 | const definitionRows = Object.entries(result.definitions ?? {}).map( |
| 1926 | tenant: keys.tenant, | 2226 | ([name, schema]) => ({ |
| 1927 | owner: keys.owner, | 2227 | tenant: keys.tenant, |
| 1928 | subject: keys.subject, | 2228 | owner: keys.owner, |
| 1929 | integration: String(ref.integration), | 2229 | subject: keys.subject, |
| 1930 | connection: String(ref.name), | 2230 | integration: String(ref.integration), |
| 1931 | plugin_id: integrationRow.plugin_id, | 2231 | connection: String(ref.name), |
| 1932 | name, | 2232 | plugin_id: integrationRow.plugin_id, |
| 1933 | schema, | 2233 | name, |
| 1934 | created_at: now, | 2234 | schema, |
| 1935 | })); | 2235 | created_at: now, |
| | 2236 | }), |
| | 2237 | ); |
| 1936 | | 2238 | |
| 1937 | yield* transaction( | 2239 | yield* transaction( |
| 1938 | Effect.gen(function* () { | 2240 | Effect.gen(function* () { |
| @@ -2024,16 +2326,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 85 unmodified lines |
| 2024 | const itemIds: Record<string, string> = {}; | 2326 | const itemIds: Record<string, string> = {}; |
| 2025 | if (external.length > 0 && pasted.length > 0) { | 2327 | if (external.length > 0 && pasted.length > 0) { |
| 2026 | return yield* new InvalidConnectionInputError({ | 2328 | return yield* new InvalidConnectionInputError({ |
| 2027 | message: "A connection cannot mix pasted and external-provider inputs.", | 2329 | message: |
| | 2330 | "A connection cannot mix pasted and external-provider inputs.", |
| 2028 | }); | 2331 | }); |
| 2029 | } | 2332 | } |
| 2030 | if (external.length > 0) { | 2333 | if (external.length > 0) { |
| 2031 | const providers = new Set( | 2334 | const providers = new Set( |
| 2032 | external.map((i) => ("from" in i.origin ? String(i.origin.from.provider) : "")), | 2335 | external.map((i) => |
| | 2336 | "from" in i.origin ? String(i.origin.from.provider) : "", |
| | 2337 | ), |
| 2033 | ); | 2338 | ); |
| 2034 | if (providers.size > 1) { | 2339 | if (providers.size > 1) { |
| 2035 | return yield* new InvalidConnectionInputError({ | 2340 | return yield* new InvalidConnectionInputError({ |
| 2036 | message: "A connection's inputs must all use the same external provider.", | 2341 | message: |
| | 2342 | "A connection's inputs must all use the same external provider.", |
| 2037 | }); | 2343 | }); |
| 2038 | } | 2344 | } |
| 2039 | const [only] = [...providers]; | 2345 | const [only] = [...providers]; |
| @@ -2045,7 +2351,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5 unmodified lines |
| 2045 | } | 2351 | } |
| 2046 | providerKey = only ?? ""; | 2352 | providerKey = only ?? ""; |
| 2047 | for (const i of external) { | 2353 | for (const i of external) { |
| 2048 | if ("from" in i.origin) itemIds[i.variable] = String(i.origin.from.id); | 2354 | if ("from" in i.origin) |
| | 2355 | itemIds[i.variable] = String(i.origin.from.id); |
| 2049 | } | 2356 | } |
| 2050 | } else { | 2357 | } else { |
| 2051 | const provider = defaultWritableProvider(); | 2358 | const provider = defaultWritableProvider(); |
| @@ -2083,7 +2390,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 31 unmodified lines |
| 2083 | identity_label: input.identityLabel ?? null, | 2390 | identity_label: input.identityLabel ?? null, |
| 2084 | // Re-saving a credential keeps an existing curated description | 2391 | // Re-saving a credential keeps an existing curated description |
| 2085 | // unless the caller explicitly provides one. | 2392 | // unless the caller explicitly provides one. |
| 2086 | ...(input.description !== undefined ? { description: input.description } : {}), | 2393 | ...(input.description !== undefined |
| | 2394 | ? { description: input.description } |
| | 2395 | : {}), |
| 2087 | updated_at: now, | 2396 | updated_at: now, |
| 2088 | }; | 2397 | }; |
| 2089 | if (existing) { | 2398 | if (existing) { |
| @@ -2127,7 +2436,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 37 unmodified lines |
| 2127 | }; | 2436 | }; |
| 2128 | // Produce + persist tools for the new connection. | 2437 | // Produce + persist tools for the new connection. |
| 2129 | yield* produceConnectionTools(integrationRow, ref).pipe( | 2438 | yield* produceConnectionTools(integrationRow, ref).pipe( |
| 2130 | Effect.catchTag("IntegrationNotFoundError", () => Effect.succeed([] as readonly Tool[])), | 2439 | Effect.catchTag("IntegrationNotFoundError", () => |
| | 2440 | Effect.succeed([] as readonly Tool[]), |
| | 2441 | ), |
| 2131 | ); | 2442 | ); |
| 2132 | | 2443 | |
| 2133 | const row = yield* findConnectionRow(ref); | 2444 | const row = yield* findConnectionRow(ref); |
| @@ -2236,7 +2547,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 102 unmodified lines |
| 2236 | // Produce + persist tools for the minted connection (same path | 2547 | // Produce + persist tools for the minted connection (same path |
| 2237 | // connections.create uses). | 2548 | // connections.create uses). |
| 2238 | yield* produceConnectionTools(integrationRow, ref).pipe( | 2549 | yield* produceConnectionTools(integrationRow, ref).pipe( |
| 2239 | Effect.catchTag("IntegrationNotFoundError", () => Effect.succeed([] as readonly Tool[])), | 2550 | Effect.catchTag("IntegrationNotFoundError", () => |
| | 2551 | Effect.succeed([] as readonly Tool[]), |
| | 2552 | ), |
| 2240 | ); | 2553 | ); |
| 2241 | | 2554 | |
| 2242 | const row = yield* findConnectionRow(ref); | 2555 | const row = yield* findConnectionRow(ref); |
| @@ -2275,13 +2588,19 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 32 unmodified lines |
| 2275 | filter?.integration === undefined | 2588 | filter?.integration === undefined |
| 2276 | ? true | 2589 | ? true |
| 2277 | : b("integration", "=", String(filter.integration)), | 2590 | : b("integration", "=", String(filter.integration)), |
| 2278 | filter?.owner === undefined ? true : b("owner", "=", filter.owner), | 2591 | filter?.owner === undefined |
| | 2592 | ? true |
| | 2593 | : b("owner", "=", filter.owner), |
| 2279 | ), | 2594 | ), |
| 2280 | }) | 2595 | }) |
| 2281 | .pipe(Effect.map((rows) => rows.map(rowToConnection))); | 2596 | .pipe(Effect.map((rows) => rows.map(rowToConnection))); |
| 2282 | | 2597 | |
| 2283 | const connectionsGet = (ref: ConnectionRef): Effect.Effect<Connection | null, StorageFailure> => | 2598 | const connectionsGet = ( |
| 2284 | findConnectionRow(ref).pipe(Effect.map((row) => (row ? rowToConnection(row) : null))); | 2599 | ref: ConnectionRef, |
| | 2600 | ): Effect.Effect<Connection | null, StorageFailure> => |
| | 2601 | findConnectionRow(ref).pipe( |
| | 2602 | Effect.map((row) => (row ? rowToConnection(row) : null)), |
| | 2603 | ); |
| 2285 | | 2604 | |
| 2286 | const connectionsUpdate = ( | 2605 | const connectionsUpdate = ( |
| 2287 | ref: ConnectionRef, | 2606 | ref: ConnectionRef, |
| @@ -2297,8 +2616,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 9 unmodified lines |
| 2297 | }); | 2616 | }); |
| 2298 | } | 2617 | } |
| 2299 | const set: Record<string, unknown> = { updated_at: new Date() }; | 2618 | const set: Record<string, unknown> = { updated_at: new Date() }; |
| 2300 | if (input.description !== undefined) set.description = input.description; | 2619 | if (input.description !== undefined) |
| 2301 | if (input.identityLabel !== undefined) set.identity_label = input.identityLabel; | 2620 | set.description = input.description; |
| | 2621 | if (input.identityLabel !== undefined) |
| | 2622 | set.identity_label = input.identityLabel; |
| 2302 | yield* core.updateMany("connection", { | 2623 | yield* core.updateMany("connection", { |
| 2303 | where: (b: AnyCb) => | 2624 | where: (b: AnyCb) => |
| 2304 | b.and( | 2625 | b.and( |
| @@ -2326,7 +2647,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 21 unmodified lines |
| 2326 | }); | 2647 | }); |
| 2327 | } | 2648 | } |
| 2328 | const integrationRow = yield* findIntegrationRow(ref.integration); | 2649 | const integrationRow = yield* findIntegrationRow(ref.integration); |
| 2329 | const runtime = integrationRow ? runtimes.get(integrationRow.plugin_id) : undefined; | 2650 | const runtime = integrationRow |
| | 2651 | ? runtimes.get(integrationRow.plugin_id) |
| | 2652 | : undefined; |
| 2330 | if (integrationRow && runtime?.plugin.removeConnection) { | 2653 | if (integrationRow && runtime?.plugin.removeConnection) { |
| 2331 | yield* runtime.plugin | 2654 | yield* runtime.plugin |
| 2332 | .removeConnection({ | 2655 | .removeConnection({ |
| @@ -2336,7 +2659,11 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines |
| 2336 | }) | 2659 | }) |
| 2337 | .pipe( | 2660 | .pipe( |
| 2338 | Effect.mapError((cause) => | 2661 | Effect.mapError((cause) => |
| 2339 | pluginStorageFailure(integrationRow.plugin_id, "removeConnection", cause), | 2662 | pluginStorageFailure( |
| | 2663 | integrationRow.plugin_id, |
| | 2664 | "removeConnection", |
| | 2665 | cause, |
| | 2666 | ), |
| 2340 | ), | 2667 | ), |
| 2341 | ); | 2668 | ); |
| 2342 | } | 2669 | } |
| @@ -2385,11 +2712,23 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 42 unmodified lines |
| 2385 | // Tools (read surface) | 2712 | // Tools (read surface) |
| 2386 | // ------------------------------------------------------------------ | 2713 | // ------------------------------------------------------------------ |
| 2387 | | 2714 | |
| 2388 | const matchesToolFilter = (tool: Tool, filter: ToolListFilter | undefined): boolean => { | 2715 | const matchesToolFilter = ( |
| | 2716 | tool: Tool, |
| | 2717 | filter: ToolListFilter | undefined, |
| | 2718 | ): boolean => { |
| 2389 | if (!filter) return true; | 2719 | if (!filter) return true; |
| 2390 | if (filter.integration !== undefined && tool.integration !== filter.integration) return false; | 2720 | if ( |
| 2391 | if (filter.owner !== undefined && tool.owner !== filter.owner) return false; | 2721 | filter.integration !== undefined && |
| 2392 | if (filter.connection !== undefined && tool.connection !== filter.connection) return false; | 2722 | tool.integration !== filter.integration |
| | 2723 | ) |
| | 2724 | return false; |
| | 2725 | if (filter.owner !== undefined && tool.owner !== filter.owner) |
| | 2726 | return false; |
| | 2727 | if ( |
| | 2728 | filter.connection !== undefined && |
| | 2729 | tool.connection !== filter.connection |
| | 2730 | ) |
| | 2731 | return false; |
| 2393 | if (filter.query !== undefined) { | 2732 | if (filter.query !== undefined) { |
| 2394 | const q = filter.query.toLowerCase(); | 2733 | const q = filter.query.toLowerCase(); |
| 2395 | const hay = `${tool.name} ${tool.description}`.toLowerCase(); | 2734 | const hay = `${tool.name} ${tool.description}`.toLowerCase(); |
| @@ -2410,18 +2749,25 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines |
| 2410 | }); | 2749 | }); |
| 2411 | if (revised.length === 0) return; | 2750 | if (revised.length === 0) return; |
| 2412 | const revisedAt = new Map( | 2751 | const revisedAt = new Map( |
| 2413 | revised.map((row) => [row.slug, Number(row.config_revised_at)] as const), | 2752 | revised.map( |
| | 2753 | (row) => [row.slug, Number(row.config_revised_at)] as const, |
| | 2754 | ), |
| 2414 | ); | 2755 | ); |
| 2415 | const connections = yield* core.findMany("connection", { | 2756 | const connections = yield* core.findMany("connection", { |
| 2416 | where: (b: AnyCb) => b.or(...revised.map((row) => b("integration", "=", row.slug))), | 2757 | where: (b: AnyCb) => |
| | 2758 | b.or(...revised.map((row) => b("integration", "=", row.slug))), |
| 2417 | }); | 2759 | }); |
| 2418 | for (const connection of connections) { | 2760 | for (const connection of connections) { |
| 2419 | const revisedTime = revisedAt.get(connection.integration); | 2761 | const revisedTime = revisedAt.get(connection.integration); |
| 2420 | if (revisedTime === undefined) continue; | 2762 | if (revisedTime === undefined) continue; |
| 2421 | const syncedAt = | 2763 | const syncedAt = |
| 2422 | connection.tools_synced_at == null ? 0 : Number(connection.tools_synced_at); | 2764 | connection.tools_synced_at == null |
| | 2765 | ? 0 |
| | 2766 | : Number(connection.tools_synced_at); |
| 2423 | if (syncedAt >= revisedTime) continue; | 2767 | if (syncedAt >= revisedTime) continue; |
| 2424 | const integrationRow = revised.find((row) => row.slug === connection.integration); | 2768 | const integrationRow = revised.find( |
| | 2769 | (row) => row.slug === connection.integration, |
| | 2770 | ); |
| 2425 | if (!integrationRow) continue; | 2771 | if (!integrationRow) continue; |
| 2426 | yield* produceConnectionTools(integrationRow, { | 2772 | yield* produceConnectionTools(integrationRow, { |
| 2427 | owner: connection.owner as Owner, | 2773 | owner: connection.owner as Owner, |
| @@ -2439,7 +2785,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 11 unmodified lines |
| 2439 | } | 2785 | } |
| 2440 | }); | 2786 | }); |
| 2441 | | 2787 | |
| 2442 | const toolsList = (filter?: ToolListFilter): Effect.Effect<readonly Tool[], StorageFailure> => | 2788 | const toolsList = ( |
| | 2789 | filter?: ToolListFilter, |
| | 2790 | ): Effect.Effect<readonly Tool[], StorageFailure> => |
| 2443 | Effect.gen(function* () { | 2791 | Effect.gen(function* () { |
| 2444 | yield* syncStaleConnectionTools; | 2792 | yield* syncStaleConnectionTools; |
| 2445 | // Projected: the list surface is metadata (address, description, | 2793 | // Projected: the list surface is metadata (address, description, |
| @@ -2451,7 +2799,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5 unmodified lines |
| 2451 | filter?.integration === undefined | 2799 | filter?.integration === undefined |
| 2452 | ? true | 2800 | ? true |
| 2453 | : b("integration", "=", String(filter.integration)), | 2801 | : b("integration", "=", String(filter.integration)), |
| 2454 | filter?.owner === undefined ? true : b("owner", "=", filter.owner), | 2802 | filter?.owner === undefined |
| | 2803 | ? true |
| | 2804 | : b("owner", "=", filter.owner), |
| 2455 | filter?.connection === undefined | 2805 | filter?.connection === undefined |
| 2456 | ? true | 2806 | ? true |
| 2457 | : b("connection", "=", String(filter.connection)), | 2807 | : b("connection", "=", String(filter.connection)), |
| @@ -2507,7 +2857,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 49 unmodified lines |
| 2507 | defs: new Map(), | 2857 | defs: new Map(), |
| 2508 | }), | 2858 | }), |
| 2509 | catch: (cause) => | 2859 | catch: (cause) => |
| 2510 | storageFailureFromUnknown("Failed to build static tool TypeScript preview", cause), | 2860 | storageFailureFromUnknown( |
| | 2861 | "Failed to build static tool TypeScript preview", |
| | 2862 | cause, |
| | 2863 | ), |
| 2511 | }).pipe(Effect.option); | 2864 | }).pipe(Effect.option); |
| 2512 | return ToolSchemaView.make({ | 2865 | return ToolSchemaView.make({ |
| 2513 | address, | 2866 | address, |
| @@ -2517,7 +2870,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines |
| 2517 | outputSchema: tool.outputSchema, | 2870 | outputSchema: tool.outputSchema, |
| 2518 | inputTypeScript: Option.getOrUndefined(preview)?.inputTypeScript, | 2871 | inputTypeScript: Option.getOrUndefined(preview)?.inputTypeScript, |
| 2519 | outputTypeScript: Option.getOrUndefined(preview)?.outputTypeScript, | 2872 | outputTypeScript: Option.getOrUndefined(preview)?.outputTypeScript, |
| 2520 | typeScriptDefinitions: Option.getOrUndefined(preview)?.typeScriptDefinitions, | 2873 | typeScriptDefinitions: |
| | 2874 | Option.getOrUndefined(preview)?.typeScriptDefinitions, |
| 2521 | }); | 2875 | }); |
| 2522 | } | 2876 | } |
| 2523 | | 2877 | |
| @@ -2544,7 +2898,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 20 unmodified lines |
| 2544 | ), | 2898 | ), |
| 2545 | }); | 2899 | }); |
| 2546 | const defs = new Map<string, unknown>(); | 2900 | const defs = new Map<string, unknown>(); |
| 2547 | for (const def of definitionRows) defs.set(def.name, decodeJsonColumn(def.schema)); | 2901 | for (const def of definitionRows) |
| | 2902 | defs.set(def.name, decodeJsonColumn(def.schema)); |
| 2548 | | 2903 | |
| 2549 | const referenced = collectReferencedDefinitions( | 2904 | const referenced = collectReferencedDefinitions( |
| 2550 | [tool.inputSchema, tool.outputSchema], | 2905 | [tool.inputSchema, tool.outputSchema], |
| @@ -2558,7 +2913,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines |
| 2558 | defs, | 2913 | defs, |
| 2559 | }), | 2914 | }), |
| 2560 | catch: (cause) => | 2915 | catch: (cause) => |
| 2561 | storageFailureFromUnknown("Failed to build tool TypeScript preview", cause), | 2916 | storageFailureFromUnknown( |
| | 2917 | "Failed to build tool TypeScript preview", |
| | 2918 | cause, |
| | 2919 | ), |
| 2562 | }).pipe(Effect.option); | 2920 | }).pipe(Effect.option); |
| 2563 | | 2921 | |
| 2564 | const view = preview; | 2922 | const view = preview; |
| @@ -2574,7 +2932,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 9 unmodified lines |
| 2574 | : undefined, | 2932 | : undefined, |
| 2575 | inputTypeScript: Option.getOrUndefined(view)?.inputTypeScript, | 2933 | inputTypeScript: Option.getOrUndefined(view)?.inputTypeScript, |
| 2576 | outputTypeScript: Option.getOrUndefined(view)?.outputTypeScript, | 2934 | outputTypeScript: Option.getOrUndefined(view)?.outputTypeScript, |
| 2577 | typeScriptDefinitions: Option.getOrUndefined(view)?.typeScriptDefinitions, | 2935 | typeScriptDefinitions: |
| | 2936 | Option.getOrUndefined(view)?.typeScriptDefinitions, |
| 2578 | }); | 2937 | }); |
| 2579 | }); | 2938 | }); |
| 2580 | | 2939 | |
| @@ -2583,7 +2942,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2 unmodified lines |
| 2583 | // ------------------------------------------------------------------ | 2942 | // ------------------------------------------------------------------ |
| 2584 | | 2943 | |
| 2585 | const providersList = (): Effect.Effect<readonly ProviderKey[]> => | 2944 | const providersList = (): Effect.Effect<readonly ProviderKey[]> => |
| 2586 | Effect.sync(() => credentialProviderOrder.map((key) => ProviderKey.make(key))); | 2945 | Effect.sync(() => |
| | 2946 | credentialProviderOrder.map((key) => ProviderKey.make(key)), |
| | 2947 | ); |
| 2587 | | 2948 | |
| 2588 | const providersItems = ( | 2949 | const providersItems = ( |
| 2589 | key: ProviderKey, | 2950 | key: ProviderKey, |
| @@ -2609,13 +2970,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 19 unmodified lines |
| 2609 | ? String(tool.address) | 2970 | ? String(tool.address) |
| 2610 | : `${tool.integration}.${tool.owner}.${tool.connection}.${tool.name}`; | 2971 | : `${tool.integration}.${tool.owner}.${tool.connection}.${tool.name}`; |
| 2611 | | 2972 | |
| 2612 | const policiesList = (): Effect.Effect<readonly ToolPolicy[], StorageFailure> => | 2973 | const policiesList = (): Effect.Effect< |
| | 2974 | readonly ToolPolicy[], |
| | 2975 | StorageFailure |
| | 2976 | > => |
| 2613 | core | 2977 | core |
| 2614 | .findMany("tool_policy", {}) | 2978 | .findMany("tool_policy", {}) |
| 2615 | .pipe( | 2979 | .pipe( |
| 2616 | Effect.map((rows) => | 2980 | Effect.map((rows) => |
| 2617 | [...rows] | 2981 | [...rows] |
| 2618 | .sort((a, b) => ownerRankForRow(a) - ownerRankForRow(b) || comparePolicyRow(a, b)) | 2982 | .sort( |
| | 2983 | (a, b) => |
| | 2984 | ownerRankForRow(a) - ownerRankForRow(b) || |
| | 2985 | comparePolicyRow(a, b), |
| | 2986 | ) |
| 2619 | .map(rowToToolPolicy), | 2987 | .map(rowToToolPolicy), |
| 2620 | ), | 2988 | ), |
| 2621 | ); | 2989 | ); |
| @@ -2648,7 +3016,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 26 unmodified lines |
| 2648 | .map((row) => row.position) | 3016 | .map((row) => row.position) |
| 2649 | .sort() | 3017 | .sort() |
| 2650 | .at(0); | 3018 | .at(0); |
| 2651 | const position = input.position ?? generateKeyBetween(null, minPosition ?? null); | 3019 | const position = |
| | 3020 | input.position ?? generateKeyBetween(null, minPosition ?? null); |
| 2652 | const id = PolicyId.make( | 3021 | const id = PolicyId.make( |
| 2653 | `pol_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, | 3022 | `pol_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, |
| 2654 | ); | 3023 | ); |
| @@ -2677,7 +3046,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 22 unmodified lines |
| 2677 | cause: undefined, | 3046 | cause: undefined, |
| 2678 | }); | 3047 | }); |
| 2679 | } | 3048 | } |
| 2680 | const where = (b: AnyCb) => b.and(byOwner(input.owner)(b), b("id", "=", input.id)); | 3049 | const where = (b: AnyCb) => |
| | 3050 | b.and(byOwner(input.owner)(b), b("id", "=", input.id)); |
| 2681 | const existing = yield* core.findFirst("tool_policy", { where }); | 3051 | const existing = yield* core.findFirst("tool_policy", { where }); |
| 2682 | if (!existing) { | 3052 | if (!existing) { |
| 2683 | return yield* new StorageError({ | 3053 | return yield* new StorageError({ |
| @@ -2691,12 +3061,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines |
| 2691 | if (input.position !== undefined) set.position = input.position; | 3061 | if (input.position !== undefined) set.position = input.position; |
| 2692 | yield* core.updateMany("tool_policy", { where, set }); | 3062 | yield* core.updateMany("tool_policy", { where, set }); |
| 2693 | const updated = yield* core.findFirst("tool_policy", { where }); | 3063 | const updated = yield* core.findFirst("tool_policy", { where }); |
| 2694 | return rowToToolPolicy(updated ?? ({ ...existing, ...set } as ToolPolicyRow)); | 3064 | return rowToToolPolicy( |
| | 3065 | updated ?? ({ ...existing, ...set } as ToolPolicyRow), |
| | 3066 | ); |
| 2695 | }); | 3067 | }); |
| 2696 | | 3068 | |
| 2697 | const policiesRemove = (input: RemoveToolPolicyInput): Effect.Effect<void, StorageFailure> => | 3069 | const policiesRemove = ( |
| | 3070 | input: RemoveToolPolicyInput, |
| | 3071 | ): Effect.Effect<void, StorageFailure> => |
| 2698 | core.deleteMany("tool_policy", { | 3072 | core.deleteMany("tool_policy", { |
| 2699 | where: (b: AnyCb) => b.and(byOwner(input.owner)(b), b("id", "=", input.id)), | 3073 | where: (b: AnyCb) => |
| | 3074 | b.and(byOwner(input.owner)(b), b("id", "=", input.id)), |
| 2700 | }); | 3075 | }); |
| 2701 | | 3076 | |
| 2702 | const policiesResolve = ( | 3077 | const policiesResolve = ( |
| @@ -2721,20 +3096,31 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 18 unmodified lines |
| 2721 | ), | 3096 | ), |
| 2722 | }); | 3097 | }); |
| 2723 | if (row) { | 3098 | if (row) { |
| 2724 | const annotations = decodeJsonColumn(row.annotations) as ToolAnnotations | undefined; | 3099 | const annotations = decodeJsonColumn(row.annotations) as |
| | 3100 | | ToolAnnotations |
| | 3101 | | undefined; |
| 2725 | requiresApproval = annotations?.requiresApproval; | 3102 | requiresApproval = annotations?.requiresApproval; |
| 2726 | } | 3103 | } |
| 2727 | } | 3104 | } |
| 2728 | return resolveEffectivePolicy(toolId, policyRows, ownerRankForRow, requiresApproval); | 3105 | return resolveEffectivePolicy( |
| | 3106 | toolId, |
| | 3107 | policyRows, |
| | 3108 | ownerRankForRow, |
| | 3109 | requiresApproval, |
| | 3110 | ); |
| 2729 | }); | 3111 | }); |
| 2730 | | 3112 | |
| 2731 | // ------------------------------------------------------------------ | 3113 | // ------------------------------------------------------------------ |
| 2732 | // Elicitation | 3114 | // Elicitation |
| 2733 | // ------------------------------------------------------------------ | 3115 | // ------------------------------------------------------------------ |
| 2734 | | 3116 | |
| 2735 | const defaultElicitationHandler = resolveElicitationHandler(config.onElicitation); | 3117 | const defaultElicitationHandler = resolveElicitationHandler( |
| | 3118 | config.onElicitation, |
| | 3119 | ); |
| 2736 | | 3120 | |
| 2737 | const pickHandler = (options: InvokeOptions | undefined): ElicitationHandler => | 3121 | const pickHandler = ( |
| | 3122 | options: InvokeOptions | undefined, |
| | 3123 | ): ElicitationHandler => |
| 2738 | options?.onElicitation | 3124 | options?.onElicitation |
| 2739 | ? resolveElicitationHandler(options.onElicitation) | 3125 | ? resolveElicitationHandler(options.onElicitation) |
| 2740 | : defaultElicitationHandler; | 3126 | : defaultElicitationHandler; |
| @@ -2796,15 +3182,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 55 unmodified lines |
| 2796 | | 3182 | |
| 2797 | const TOOL_SUGGESTION_LIMIT = 5; | 3183 | const TOOL_SUGGESTION_LIMIT = 5; |
| 2798 | | 3184 | |
| 2799 | const toolSuggestions = (rows: readonly ToolInvocationRow[]): readonly ToolAddress[] => | 3185 | const toolSuggestions = ( |
| 2800 | rows.map((row) => rowToTool(row).address); | 3186 | rows: readonly ToolInvocationRow[], |
| | 3187 | ): readonly ToolAddress[] => rows.map((row) => rowToTool(row).address); |
| 2801 | | 3188 | |
| 2802 | const toolRowsForConnectionWhere = (parsed: ParsedToolAddress) => (b: AnyCb) => | 3189 | const toolRowsForConnectionWhere = |
| 2803 | b.and( | 3190 | (parsed: ParsedToolAddress) => (b: AnyCb) => |
| 2804 | byOwner(parsed.owner)(b), | 3191 | b.and( |
| 2805 | b("integration", "=", String(parsed.integration)), | 3192 | byOwner(parsed.owner)(b), |
| 2806 | b("connection", "=", String(parsed.connection)), | 3193 | b("integration", "=", String(parsed.integration)), |
| 2807 | ); | 3194 | b("connection", "=", String(parsed.connection)), |
| | 3195 | ); |
| 2808 | | 3196 | |
| 2809 | const searchToolRowsForConnection = ( | 3197 | const searchToolRowsForConnection = ( |
| 2810 | parsed: ParsedToolAddress, | 3198 | parsed: ParsedToolAddress, |
| @@ -2842,7 +3230,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 31 unmodified lines |
| 2842 | return Effect.gen(function* () { | 3230 | return Effect.gen(function* () { |
| 2843 | // oxlint-disable executor/no-instanceof-error, executor/no-unknown-error-message, executor/no-manual-tag-check -- boundary: normalize arbitrary unknown plugin failures into a human-readable message for ToolInvocationError/telemetry | 3231 | // oxlint-disable executor/no-instanceof-error, executor/no-unknown-error-message, executor/no-manual-tag-check -- boundary: normalize arbitrary unknown plugin failures into a human-readable message for ToolInvocationError/telemetry |
| 2844 | const formatInvocationCauseMessage = (cause: unknown): string => { | 3232 | const formatInvocationCauseMessage = (cause: unknown): string => { |
| 2845 | if (cause instanceof Error && cause.message.length > 0) return cause.message; | 3233 | if (cause instanceof Error && cause.message.length > 0) |
| | 3234 | return cause.message; |
| 2846 | // Non-Error / empty-message causes: `String(plainObject)` renders | 3235 | // Non-Error / empty-message causes: `String(plainObject)` renders |
| 2847 | // "[object Object]", which is what telemetry then shows as the only | 3236 | // "[object Object]", which is what telemetry then shows as the only |
| 2848 | // label for the failure. Prefer the tag, else stringify structurally. | 3237 | // label for the failure. Prefer the tag, else stringify structurally. |
| @@ -2886,7 +3275,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 37 unmodified lines |
| 2886 | pattern: policy.pattern ?? "*", | 3275 | pattern: policy.pattern ?? "*", |
| 2887 | }); | 3276 | }); |
| 2888 | } | 3277 | } |
| 2889 | yield* enforceApproval(staticEntry.tool.annotations, address, args, policy, handler); | 3278 | yield* enforceApproval( |
| | 3279 | staticEntry.tool.annotations, |
| | 3280 | address, |
| | 3281 | args, |
| | 3282 | policy, |
| | 3283 | handler, |
| | 3284 | ); |
| 2890 | return yield* wrapInvocationError( | 3285 | return yield* wrapInvocationError( |
| 2891 | staticEntry.tool.handler({ | 3286 | staticEntry.tool.handler({ |
| 2892 | ctx: staticEntry.ctx, | 3287 | ctx: staticEntry.ctx, |
| @@ -2917,7 +3312,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 24 unmodified lines |
| 2917 | if (!row) { | 3312 | if (!row) { |
| 2918 | const searchMatches = yield* searchToolRowsForConnection(parsed); | 3313 | const searchMatches = yield* searchToolRowsForConnection(parsed); |
| 2919 | const connectionTools = | 3314 | const connectionTools = |
| 2920 | searchMatches.length > 0 ? searchMatches : yield* findToolRowsForConnection(parsed); | 3315 | searchMatches.length > 0 |
| | 3316 | ? searchMatches |
| | 3317 | : yield* findToolRowsForConnection(parsed); |
| 2921 | return yield* new ToolNotFoundError({ | 3318 | return yield* new ToolNotFoundError({ |
| 2922 | address, | 3319 | address, |
| 2923 | suggestions: toolSuggestions(connectionTools), | 3320 | suggestions: toolSuggestions(connectionTools), |
| @@ -2927,7 +3324,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines |
| 2927 | // Resolve policy (owner-ranked). | 3324 | // Resolve policy (owner-ranked). |
| 2928 | const toolForPolicy = rowToTool(row); | 3325 | const toolForPolicy = rowToTool(row); |
| 2929 | const policyRows = yield* core.findMany("tool_policy", {}); | 3326 | const policyRows = yield* core.findMany("tool_policy", {}); |
| 2930 | const annotations = decodeJsonColumn(row.annotations) as ToolAnnotations | undefined; | 3327 | const annotations = decodeJsonColumn(row.annotations) as |
| | 3328 | | ToolAnnotations |
| | 3329 | | undefined; |
| 2931 | const policy = resolveEffectivePolicy( | 3330 | const policy = resolveEffectivePolicy( |
| 2932 | normalizedPolicyId(toolForPolicy), | 3331 | normalizedPolicyId(toolForPolicy), |
| 2933 | policyRows, | 3332 | policyRows, |
| @@ -2982,7 +3381,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 48 unmodified lines |
| 2982 | .pipe(wrapInvocationError); | 3381 | .pipe(wrapInvocationError); |
| 2983 | resolvedAnnotations = map[String(parsed.tool)] ?? annotations; | 3382 | resolvedAnnotations = map[String(parsed.tool)] ?? annotations; |
| 2984 | } | 3383 | } |
| 2985 | yield* enforceApproval(resolvedAnnotations, address, args, policy, handler); | 3384 | yield* enforceApproval( |
| | 3385 | resolvedAnnotations, |
| | 3386 | address, |
| | 3387 | args, |
| | 3388 | policy, |
| | 3389 | handler, |
| | 3390 | ); |
| 2986 | | 3391 | |
| 2987 | // Resolve every named credential input (`variable → value`); `value` is | 3392 | // Resolve every named credential input (`variable → value`); `value` is |
| 2988 | // the primary `token` for single-input + OAuth callers. | 3393 | // the primary `token` for single-input + OAuth callers. |
| @@ -2995,7 +3400,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 6 unmodified lines |
| 2995 | template: AuthTemplateSlug.make(connectionRow.template), | 3400 | template: AuthTemplateSlug.make(connectionRow.template), |
| 2996 | value: values[PRIMARY_INPUT_VARIABLE] ?? null, | 3401 | value: values[PRIMARY_INPUT_VARIABLE] ?? null, |
| 2997 | values, | 3402 | values, |
| 2998 | config: integrationRow ? decodeJsonColumn(integrationRow.config) : undefined, | 3403 | config: integrationRow |
| | 3404 | ? decodeJsonColumn(integrationRow.config) |
| | 3405 | : undefined, |
| 2999 | }; | 3406 | }; |
| 3000 | | 3407 | |
| 3001 | return yield* wrapInvocationError( | 3408 | return yield* wrapInvocationError( |
| @@ -3035,7 +3442,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 33 unmodified lines |
| 3035 | subject, | 3442 | subject, |
| 3036 | ownedKeys: (owner: Owner) => ownedKeys(owner), | 3443 | ownedKeys: (owner: Owner) => ownedKeys(owner), |
| 3037 | defaultWritableProvider, | 3444 | defaultWritableProvider, |
| 3038 | mintOAuthConnection: (input: MintOAuthConnectionInput) => mintOAuthConnection(input), | 3445 | mintOAuthConnection: (input: MintOAuthConnectionInput) => |
| | 3446 | mintOAuthConnection(input), |
| 3039 | // Resolve the integration's DECLARED oauth scopes for a (integration, | 3447 | // Resolve the integration's DECLARED oauth scopes for a (integration, |
| 3040 | // template): load the row, run the owning plugin's auth-method projector, | 3448 | // template): load the row, run the owning plugin's auth-method projector, |
| 3041 | // and return the matching oauth method's declared `oauth.scopes`. Drives | 3449 | // and return the matching oauth method's declared `oauth.scopes`. Drives |
| @@ -3043,15 +3451,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1 unmodified line |
| 3043 | // client on a broad integration still requests the integration's full | 3451 | // client on a broad integration still requests the integration's full |
| 3044 | // scope set. Empty (no row / no oauth method / no declared scopes) ⇒ the | 3452 | // scope set. Empty (no row / no oauth method / no declared scopes) ⇒ the |
| 3045 | // union collapses to the client's scopes (current behavior). | 3453 | // union collapses to the client's scopes (current behavior). |
| 3046 | resolveDeclaredOAuthScopes: (integration: IntegrationSlug, template: AuthTemplateSlug) => | 3454 | resolveDeclaredOAuthScopes: ( |
| | 3455 | integration: IntegrationSlug, |
| | 3456 | template: AuthTemplateSlug, |
| | 3457 | ) => |
| 3047 | findIntegrationRow(integration).pipe( | 3458 | findIntegrationRow(integration).pipe( |
| 3048 | Effect.map((row): readonly string[] => { | 3459 | Effect.map((row): readonly string[] => { |
| 3049 | if (!row) return []; | 3460 | if (!row) return []; |
| 3050 | const methods = describeAuthMethodsForRow(row); | 3461 | const methods = describeAuthMethodsForRow(row); |
| 3051 | const match = | 3462 | const match = |
| 3052 | methods.find( | 3463 | methods.find( |
| 3053 | (m: AuthMethodDescriptor) => m.kind === "oauth" && m.template === String(template), | 3464 | (m: AuthMethodDescriptor) => |
| 3054 | ) ?? methods.find((m: AuthMethodDescriptor) => m.kind === "oauth"); | 3465 | m.kind === "oauth" && m.template === String(template), |
| | 3466 | ) ?? |
| | 3467 | methods.find((m: AuthMethodDescriptor) => m.kind === "oauth"); |
| 3055 | return match?.oauth?.scopes ?? []; | 3468 | return match?.oauth?.scopes ?? []; |
| 3056 | }), | 3469 | }), |
| 3057 | ), | 3470 | ), |
| @@ -3101,7 +3514,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 43 unmodified lines |
| 3101 | httpClientLayer: config.httpClientLayer ?? FetchHttpClient.layer, | 3514 | httpClientLayer: config.httpClientLayer ?? FetchHttpClient.layer, |
| 3102 | core: { | 3515 | core: { |
| 3103 | integrations: { | 3516 | integrations: { |
| 3104 | register: (input: RegisterIntegrationInput) => integrationsRegister(plugin.id, input), | 3517 | register: (input: RegisterIntegrationInput) => |
| | 3518 | integrationsRegister(plugin.id, input), |
| 3105 | update: (slug, patch) => integrationsUpdate(slug, patch), | 3519 | update: (slug, patch) => integrationsUpdate(slug, patch), |
| 3106 | list: () => integrationsList(), | 3520 | list: () => integrationsList(), |
| 3107 | get: (slug) => integrationsGetRecord(slug), | 3521 | get: (slug) => integrationsGetRecord(slug), |
| @@ -3156,6 +3570,12 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 48 unmodified lines |
| 3156 | if (plugin.extension) { | 3570 | if (plugin.extension) { |
| 3157 | extensions[plugin.id] = extension; | 3571 | extensions[plugin.id] = extension; |
| 3158 | } | 3572 | } |
| | 3573 | // A plugin that narrows the executor exposes `wrapExecutor` on its |
| | 3574 | // extension; collect it into the generic hook the execution stack runs. |
| | 3575 | const wrapExecutor = (extension as { wrapExecutor?: ExecutorWrapper }) |
| | 3576 | .wrapExecutor; |
| | 3577 | if (typeof wrapExecutor === "function") |
| | 3578 | executorWrappers.push(wrapExecutor); |
| 3159 | | 3579 | |
| 3160 | const decls = plugin.staticSources ? plugin.staticSources(extension) : []; | 3580 | const decls = plugin.staticSources ? plugin.staticSources(extension) : []; |
| 3161 | for (const source of decls) { | 3581 | for (const source of decls) { |
| @@ -3212,7 +3632,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 50 unmodified lines |
| 3212 | yield* runtime.plugin | 3632 | yield* runtime.plugin |
| 3213 | .close() | 3633 | .close() |
| 3214 | .pipe( | 3634 | .pipe( |
| 3215 | Effect.mapError((cause) => pluginStorageFailure(runtime.plugin.id, "close", cause)), | 3635 | Effect.mapError((cause) => |
| | 3636 | pluginStorageFailure(runtime.plugin.id, "close", cause), |
| | 3637 | ), |
| 3216 | ); | 3638 | ); |
| 3217 | } | 3639 | } |
| 3218 | } | 3640 | } |
| @@ -3269,8 +3691,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 50 unmodified lines |
| 3269 | close, | 3691 | close, |
| 3270 | }; | 3692 | }; |
| 3271 | | 3693 | |
| 3272 | const toExecutor = (value: unknown): Executor<TPlugins> => value as Executor<TPlugins>; | 3694 | const toExecutor = (value: unknown): Executor<TPlugins> => |
| 3273 | return toExecutor(Object.assign(base, extensions)); | 3695 | value as Executor<TPlugins>; |
| | 3696 | return toExecutor(Object.assign(base, extensions, { executorWrappers })); |
| 3274 | }); | 3697 | }); |
| 3275 | | 3698 | |
| 3276 | // Helper alias so the inline literal used for the optimistic projection in | 3699 | // Helper alias so the inline literal used for the optimistic projection in |