@@ -43,31 +43,93 @@ function hasImplicitGlobalKey(value) {
4343 return Object . keys ( value ) . some ( ( key ) => blockedKeys . has ( String ( key ) . trim ( ) . toLowerCase ( ) ) ) ;
4444}
4545
46- function assertExplicitLaunchInputs ( { toolId = "" , payloadJson = null , paletteJson = null , argumentCount = 0 } ) {
47- const toolIdText = typeof toolId === "string" ? toolId . trim ( ) : "" ;
48- if ( ! toolIdText ) {
49- throw new Error ( "launch contract violation: toolId is required." ) ;
46+ function isWrapperJsonLike ( value ) {
47+ if ( ! isPlainObject ( value ) ) {
48+ return false ;
5049 }
51- if ( argumentCount < 2 || argumentCount > 3 ) {
52- throw new Error ( `launch contract violation: launch(toolId, payloadJson, paletteJson?) expected 2-3 args, received ${ argumentCount } .` ) ;
50+ const keys = Object . keys ( value ) . map ( ( key ) => String ( key ) . trim ( ) . toLowerCase ( ) ) ;
51+ if ( keys . includes ( "payloadjson" ) || keys . includes ( "palettejson" ) ) {
52+ return true ;
53+ }
54+ if ( keys . includes ( "wrapper" ) || keys . includes ( "wrapped" ) ) {
55+ return true ;
56+ }
57+ if ( keys . includes ( "payload" ) && isPlainObject ( value . payload ) ) {
58+ return true ;
59+ }
60+ if ( keys . includes ( "palette" ) && isPlainObject ( value . palette ) ) {
61+ return true ;
62+ }
63+ return false ;
64+ }
65+
66+ function hasFallbackAttempt ( value ) {
67+ const blockedPrefixes = [ "default" , "fallback" , "tryloadpreset" , "buildpreset" ] ;
68+ const stack = [ value ] ;
69+ while ( stack . length > 0 ) {
70+ const node = stack . pop ( ) ;
71+ if ( ! isPlainObject ( node ) && ! Array . isArray ( node ) ) {
72+ continue ;
73+ }
74+ if ( Array . isArray ( node ) ) {
75+ node . forEach ( ( entry ) => stack . push ( entry ) ) ;
76+ continue ;
77+ }
78+ Object . entries ( node ) . forEach ( ( [ key , nestedValue ] ) => {
79+ const normalized = String ( key ) . trim ( ) . toLowerCase ( ) ;
80+ if ( blockedPrefixes . some ( ( prefix ) => normalized . startsWith ( prefix ) ) ) {
81+ throw new Error ( "launch contract violation: fallback attempt detected in input JSON." ) ;
82+ }
83+ stack . push ( nestedValue ) ;
84+ } ) ;
5385 }
86+ return false ;
87+ }
88+
89+ function computeInputFingerprint ( value , label ) {
90+ try {
91+ return JSON . stringify ( value ) ;
92+ } catch {
93+ throw new Error ( `launch contract violation: ${ label } must be JSON-serializable.` ) ;
94+ }
95+ }
96+
97+ export function validateInput ( payloadJson , paletteJson = null ) {
98+ const payloadBefore = computeInputFingerprint ( payloadJson , "payloadJson" ) ;
99+ const paletteBefore = paletteJson === null ? null : computeInputFingerprint ( paletteJson , "paletteJson" ) ;
54100 if ( ! isPlainObject ( payloadJson ) ) {
55- throw new Error ( ` launch contract violation: payloadJson must be an object for ${ toolIdText } .` ) ;
101+ throw new Error ( " launch contract violation: missing payloadJson object." ) ;
56102 }
57103 if ( paletteJson !== null && ! isPlainObject ( paletteJson ) ) {
58- throw new Error ( `launch contract violation: paletteJson must be an object or null for ${ toolIdText } .` ) ;
104+ throw new Error ( "launch contract violation: paletteJson must be an object or null." ) ;
105+ }
106+ if ( isWrapperJsonLike ( payloadJson ) || ( paletteJson !== null && isWrapperJsonLike ( paletteJson ) ) ) {
107+ throw new Error ( "launch contract violation: wrapper JSON detected." ) ;
59108 }
60- if ( isParentJsonLike ( payloadJson ) ) {
61- throw new Error ( ` launch contract violation: parent JSON usage detected in payloadJson for ${ toolIdText } .` ) ;
109+ if ( isParentJsonLike ( payloadJson ) || ( paletteJson !== null && isParentJsonLike ( paletteJson ) ) ) {
110+ throw new Error ( " launch contract violation: parent JSON detected." ) ;
62111 }
63- if ( paletteJson !== null && isParentJsonLike ( paletteJson ) ) {
64- throw new Error ( ` launch contract violation: parent JSON usage detected in paletteJson for ${ toolIdText } .` ) ;
112+ if ( hasImplicitGlobalKey ( payloadJson ) || ( paletteJson !== null && hasImplicitGlobalKey ( paletteJson ) ) ) {
113+ throw new Error ( " launch contract violation: implicit/global input keys detected." ) ;
65114 }
66- if ( hasImplicitGlobalKey ( payloadJson ) ) {
67- throw new Error ( `launch contract violation: implicit/global input keys detected in payloadJson for ${ toolIdText } .` ) ;
115+ hasFallbackAttempt ( payloadJson ) ;
116+ if ( paletteJson !== null ) {
117+ hasFallbackAttempt ( paletteJson ) ;
68118 }
69- if ( paletteJson !== null && hasImplicitGlobalKey ( paletteJson ) ) {
70- throw new Error ( `launch contract violation: implicit/global input keys detected in paletteJson for ${ toolIdText } .` ) ;
119+ const payloadAfter = computeInputFingerprint ( payloadJson , "payloadJson" ) ;
120+ const paletteAfter = paletteJson === null ? null : computeInputFingerprint ( paletteJson , "paletteJson" ) ;
121+ if ( payloadBefore !== payloadAfter || paletteBefore !== paletteAfter ) {
122+ throw new Error ( "launch contract violation: mutation detected during input validation." ) ;
123+ }
124+ }
125+
126+ function assertExplicitLaunchInputs ( { toolId = "" , argumentCount = 0 } ) {
127+ const toolIdText = typeof toolId === "string" ? toolId . trim ( ) : "" ;
128+ if ( ! toolIdText ) {
129+ throw new Error ( "launch contract violation: toolId is required." ) ;
130+ }
131+ if ( argumentCount < 2 || argumentCount > 3 ) {
132+ throw new Error ( `launch contract violation: launch(toolId, payloadJson, paletteJson?) expected 2-3 args, received ${ argumentCount } .` ) ;
71133 }
72134}
73135
@@ -211,10 +273,9 @@ export function createToolHostRuntime(options = {}) {
211273
212274 assertExplicitLaunchInputs ( {
213275 toolId,
214- payloadJson,
215- paletteJson,
216276 argumentCount : arguments . length
217277 } ) ;
278+ validateInput ( payloadJson , paletteJson ) ;
218279 const toolIdText = typeof toolId === "string" ? toolId . trim ( ) : "" ;
219280 const toolEntry = getToolHostEntryById ( manifest , toolIdText ) ;
220281 if ( ! toolEntry ) {
0 commit comments