@@ -10,6 +10,7 @@ import {
1010 IntegrationDetectionResult ,
1111 IntegrationSlug ,
1212 mergeAuthTemplates ,
13+ sha256Hex ,
1314 ToolName ,
1415 ToolResult ,
1516 type AuthMethodDescriptor ,
@@ -413,17 +414,32 @@ const introspectHeadersForConnection = (
413414 return { headers, queryParams } ;
414415} ;
415416
416- /** Introspect a config live or from its stored JSON, applying connection auth.
417- * `parseIntrospectionJson` short-circuits the network when a schema snapshot is
418- * present; otherwise this introspects the endpoint with the rendered credential. */
417+ /** Resolve a config's introspection snapshot text: legacy rows inline it in
418+ * `introspectionJson`; new rows point at the plugin blob store via
419+ * `introspectionHash`. Null when the integration has no snapshot (live
420+ * introspection territory). */
421+ const loadIntrospectionJson = (
422+ storage : GraphqlStore ,
423+ config : GraphqlIntegrationConfig ,
424+ ) : Effect . Effect < string | null , StorageFailure > => {
425+ if ( config . introspectionJson != null ) return Effect . succeed ( config . introspectionJson ) ;
426+ if ( config . introspectionHash != null ) return storage . getIntrospection ( config . introspectionHash ) ;
427+ return Effect . succeed ( null ) ;
428+ } ;
429+
430+ /** Introspect a config live or from its stored snapshot, applying connection
431+ * auth. A non-null `introspectionJson` (loaded via `loadIntrospectionJson`)
432+ * short-circuits the network; otherwise this introspects the endpoint with
433+ * the rendered credential. */
419434const introspectForConnection = (
420435 config : GraphqlIntegrationConfig ,
436+ introspectionJson : string | null ,
421437 values : Record < string , string | null > ,
422438 templateSlug : AuthTemplateSlug | null ,
423439 httpClientLayer : Layer . Layer < HttpClient . HttpClient > ,
424440) : Effect . Effect < IntrospectionResult , GraphqlIntrospectionError > => {
425- if ( config . introspectionJson ) {
426- return parseIntrospectionJson ( config . introspectionJson ) ;
441+ if ( introspectionJson != null ) {
442+ return parseIntrospectionJson ( introspectionJson ) ;
427443 }
428444 const auth = introspectHeadersForConnection ( config , values , templateSlug ) ;
429445 return introspect (
@@ -463,13 +479,15 @@ const materializeOperations = (
463479 Object . assign ( queryParams , rendered . queryParams ) ;
464480 }
465481
466- const introspection = config . introspectionJson
467- ? yield * parseIntrospectionJson ( config . introspectionJson )
468- : yield * introspect (
469- config . endpoint ,
470- Object . keys ( headers ) . length > 0 ? headers : undefined ,
471- Object . keys ( queryParams ) . length > 0 ? queryParams : undefined ,
472- ) . pipe ( Effect . provide ( httpClientLayer ) ) ;
482+ const introspectionJson = yield * loadIntrospectionJson ( ctx . storage , config ) ;
483+ const introspection =
484+ introspectionJson != null
485+ ? yield * parseIntrospectionJson ( introspectionJson )
486+ : yield * introspect (
487+ config . endpoint ,
488+ Object . keys ( headers ) . length > 0 ? headers : undefined ,
489+ Object . keys ( queryParams ) . length > 0 ? queryParams : undefined ,
490+ ) . pipe ( Effect . provide ( httpClientLayer ) ) ;
473491
474492 const { result } = yield * extract ( introspection ) . pipe (
475493 Effect . catch ( ( ) =>
@@ -577,55 +595,68 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {
577595 } ) ;
578596
579597 const addIntegrationTransaction = ( input : GraphqlAddIntegrationInput , slug : IntegrationSlug ) =>
580- ctx . transaction (
581- Effect . gen ( function * ( ) {
582- const baseConfig = buildConfig ( input ) ;
583-
584- // No pre-supplied schema → register WITHOUT introspecting. Tools (and
585- // their operation bindings) are produced lazily when a connection is
586- // created (`resolveTools`) / a tool is first invoked (`invokeTool`),
587- // using that connection's credential.
588- if ( baseConfig . introspectionJson === undefined ) {
589- yield * ctx . core . integrations . register ( {
598+ Effect . gen ( function * ( ) {
599+ const baseConfig = buildConfig ( input ) ;
600+
601+ // No pre-supplied schema → register WITHOUT introspecting. Tools (and
602+ // their operation bindings) are produced lazily when a connection is
603+ // created (`resolveTools`) / a tool is first invoked (`invokeTool`),
604+ // using that connection's credential.
605+ if ( baseConfig . introspectionJson === undefined ) {
606+ yield * ctx . transaction (
607+ ctx . core . integrations . register ( {
590608 slug,
591609 description : baseConfig . name ,
592610 config : baseConfig ,
593611 canRemove : true ,
594612 canRefresh : true ,
595- } ) ;
596- return { slug : String ( slug ) , name : baseConfig . name , toolCount : 0 } ;
597- }
613+ } ) ,
614+ ) ;
615+ return { slug : String ( slug ) , name : baseConfig . name , toolCount : 0 } ;
616+ }
598617
599- // Pre-supplied introspection JSON: parse it offline (no network) and
600- // persist the operation bindings + snapshot so production stays offline.
601- const introspection = yield * parseIntrospectionJson ( baseConfig . introspectionJson ) ;
602- const { result } = yield * extract ( introspection ) ;
603- const prepared = prepareOperations ( result . fields , introspection ) ;
604-
605- // Snapshot the resolved schema so tool production never needs a live
606- // HTTP layer (D6: tools are spec-derived and identical per connection).
607- const config = GraphqlIntegrationConfig . make ( {
608- ...baseConfig ,
609- introspectionJson : JSON . stringify ( { data : introspection } ) ,
610- } ) ;
618+ // Pre-supplied introspection JSON: parse it offline (no network) and
619+ // persist the operation bindings + snapshot so production stays offline.
620+ const introspection = yield * parseIntrospectionJson ( baseConfig . introspectionJson ) ;
621+ const { result } = yield * extract ( introspection ) ;
622+ const prepared = prepareOperations ( result . fields , introspection ) ;
623+
624+ // Snapshot the resolved schema so tool production never needs a live
625+ // HTTP layer (D6: tools are spec-derived and identical per connection).
626+ // The snapshot text goes to the plugin blob store (content-addressed,
627+ // written OUTSIDE the transaction — re-puts are idempotent and an
628+ // aborted register leaves only an unreferenced blob), and the config
629+ // carries only its hash.
630+ const snapshotJson = JSON . stringify ( { data : introspection } ) ;
631+ const introspectionHash = yield * sha256Hex ( snapshotJson ) ;
632+ const { introspectionJson : _inline , ...withoutInline } = baseConfig ;
633+ const config = GraphqlIntegrationConfig . make ( {
634+ ...withoutInline ,
635+ introspectionHash,
636+ } ) ;
611637
612- yield * ctx . storage . replaceOperations ( String ( slug ) , toStoredOperations ( slug , prepared ) ) ;
638+ yield * ctx . storage . putIntrospection ( introspectionHash , snapshotJson ) ;
613639
614- yield * ctx . core . integrations . register ( {
615- slug,
616- description : config . name ,
617- config,
618- canRemove : true ,
619- canRefresh : true ,
620- } ) ;
640+ yield * ctx . transaction (
641+ Effect . gen ( function * ( ) {
642+ yield * ctx . storage . replaceOperations ( String ( slug ) , toStoredOperations ( slug , prepared ) ) ;
621643
622- return {
623- slug : String ( slug ) ,
624- name : config . name ,
625- toolCount : prepared . length ,
626- } ;
627- } ) ,
628- ) ;
644+ yield * ctx . core . integrations . register ( {
645+ slug,
646+ description : config . name ,
647+ config,
648+ canRemove : true ,
649+ canRefresh : true ,
650+ } ) ;
651+ } ) ,
652+ ) ;
653+
654+ return {
655+ slug : String ( slug ) ,
656+ name : config . name ,
657+ toolCount : prepared . length ,
658+ } ;
659+ } ) ;
629660
630661 const configureIntegration = ( slug : string , input : GraphqlConfigureInput ) =>
631662 Effect . gen ( function * ( ) {
@@ -649,6 +680,9 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {
649680 ...( current . introspectionJson !== undefined
650681 ? { introspectionJson : current . introspectionJson }
651682 : { } ) ,
683+ ...( current . introspectionHash !== undefined
684+ ? { introspectionHash : current . introspectionHash }
685+ : { } ) ,
652686 ...( ( input . headers ?? current . headers ) !== undefined
653687 ? { headers : input . headers ?? current . headers }
654688 : { } ) ,
@@ -852,26 +886,30 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => {
852886 resolveTools : ( {
853887 config,
854888 template,
889+ storage,
855890 getValues,
856891 } : {
857892 readonly config : IntegrationConfig ;
858893 readonly template : AuthTemplateSlug | null ;
894+ readonly storage : GraphqlStore ;
859895 readonly getValues : ( ) => Effect . Effect < Record < string , string | null > , unknown > ;
860896 } ) =>
861897 Effect . gen ( function * ( ) {
862898 const decoded = yield * decodeGraphqlIntegrationConfig ( config ) . pipe ( Effect . option ) ;
863899 if ( Option . isNone ( decoded ) ) return { tools : [ ] } ;
864900 const graphqlConfig = decoded . value ;
901+ const introspectionJson = yield * loadIntrospectionJson ( storage , graphqlConfig ) ;
865902 // Live introspection (no stored snapshot) needs the connection's
866903 // credential inputs for auth-required endpoints; resolve them lazily.
867904 const values =
868- graphqlConfig . introspectionJson === undefined
905+ introspectionJson == null
869906 ? yield * getValues ( ) . pipe (
870907 Effect . catch ( ( ) => Effect . succeed ( { } as Record < string , string | null > ) ) ,
871908 )
872909 : ( { } as Record < string , string | null > ) ;
873910 const introspection = yield * introspectForConnection (
874911 graphqlConfig ,
912+ introspectionJson ,
875913 values ,
876914 template ,
877915 options ?. httpClientLayer ?? httpClientLayerFallback ,
0 commit comments