@@ -731,8 +731,22 @@ function projectInteractionFormField(field: InteractionFormField): InteractionFo
731731 } ) ,
732732 } ;
733733 if ( field . kind !== 'single_select' && field . kind !== 'multi_select' ) {
734+ // `name` is a protocol identity returned to the tool; a string default is
735+ // rendered into an input the user reads and accepts.
736+ if ( field . kind === 'string' && field . default !== undefined ) {
737+ return {
738+ ...field ,
739+ ...display ,
740+ default : projectInteractionReviewText (
741+ field . default ,
742+ INTERACTION_FORM_VALUE_MAX_BYTES ,
743+ true ,
744+ ) ,
745+ } ;
746+ }
734747 return { ...field , ...display } ;
735748 }
749+ // Select values are protocol identities; labels are their display text.
736750 const options = field . options . map ( ( option ) => ( {
737751 ...option ,
738752 label : projectInteractionReviewText ( option . label , INTERACTION_FORM_FIELD_LABEL_MAX_BYTES ) ,
@@ -1248,6 +1262,45 @@ function assertFormHasAcceptedAnswer(request: InteractionFormRequest): void {
12481262 }
12491263 serializedLimit ( answer , INTERACTION_ANSWER_SERIALIZED_MAX_BYTES , 'Interaction form answer' ) ;
12501264 assertAcceptedFormAnswerFitsCanonicalOutcome ( answer ) ;
1265+ assertEveryFormAnswerFitsCanonicalOutcome ( request ) ;
1266+ }
1267+
1268+ /**
1269+ * Admission must reserve the whole legal answer envelope, not only a smallest
1270+ * witness. This intentionally over-approximates format-constrained strings:
1271+ * rejecting an over-large form is safe, whereas accepting one that can later
1272+ * reject a valid user answer strands the interaction.
1273+ */
1274+ function assertEveryFormAnswerFitsCanonicalOutcome ( request : InteractionFormRequest ) : void {
1275+ const values = Object . fromEntries (
1276+ request . fields . map ( ( field ) => [ field . name , formFieldMaximumEnvelope ( field ) ] ) ,
1277+ ) ;
1278+ const answer = { kind : 'form' as const , action : 'accept' as const , values } ;
1279+ serializedLimit ( answer , INTERACTION_ANSWER_SERIALIZED_MAX_BYTES , 'Interaction form answer' ) ;
1280+ assertAcceptedFormAnswerFitsCanonicalOutcome ( answer ) ;
1281+ }
1282+
1283+ function formFieldMaximumEnvelope ( field : InteractionFormField ) : InteractionFormValue {
1284+ if ( field . kind === 'string' ) {
1285+ const maximumCodePoints = Math . min (
1286+ field . maxLength ?? INTERACTION_FORM_VALUE_MAX_BYTES ,
1287+ Math . floor ( INTERACTION_FORM_VALUE_MAX_BYTES / 4 ) ,
1288+ ) ;
1289+ return '😀' . repeat ( maximumCodePoints ) ;
1290+ }
1291+ if ( field . kind === 'number' || field . kind === 'integer' ) return - 1.7976931348623157e308 ;
1292+ if ( field . kind === 'boolean' ) return false ;
1293+ if ( field . kind === 'single_select' ) {
1294+ return field . options . reduce (
1295+ ( longest , option ) =>
1296+ Buffer . byteLength ( option . value ) > Buffer . byteLength ( longest ) ? option . value : longest ,
1297+ field . options [ 0 ] ! . value ,
1298+ ) ;
1299+ }
1300+ return [ ...field . options ]
1301+ . sort ( ( left , right ) => Buffer . byteLength ( right . value ) - Buffer . byteLength ( left . value ) )
1302+ . slice ( 0 , field . maxItems ?? field . options . length )
1303+ . map ( ( option ) => option . value ) ;
12511304}
12521305
12531306function assertAcceptedFormAnswerFitsCanonicalOutcome (
0 commit comments