@@ -89,6 +89,19 @@ export interface GitHubPullRequestContext {
8989 readonly issue_comments : IssueComment [ ] ;
9090}
9191
92+ /**
93+ * Trust classification for content sources in the hydrated context.
94+ * - 'trusted': authenticated user input (task_description — screened by guardrail at
95+ * submission, additionally sanitized during prompt assembly). Lower risk than external
96+ * sources but not exempt from defense-in-depth sanitization.
97+ * - 'untrusted-external': GitHub issues, PR bodies/comments (attacker-controllable,
98+ * sanitized + guardrail screened). Some PR sub-fields are not sanitized:
99+ * diff_hunk and diff_summary (code/patch content in markdown code blocks),
100+ * path (file paths), head_ref and base_ref (branch names).
101+ * - 'memory': memory records (sanitized, integrity-hashed)
102+ */
103+ export type ContentTrustLevel = 'trusted' | 'untrusted-external' | 'memory' ;
104+
92105/**
93106 * The result of the context hydration pipeline.
94107 */
@@ -104,6 +117,7 @@ export interface HydratedContext {
104117 readonly guardrail_blocked ?: string ;
105118 readonly resolved_branch_name ?: string ;
106119 readonly resolved_base_branch ?: string ;
120+ readonly content_trust ?: Readonly < Record < string , ContentTrustLevel > > ;
107121}
108122
109123// ---------------------------------------------------------------------------
@@ -860,6 +874,44 @@ export function assemblePrIterationPrompt(
860874 return parts . join ( '\n' ) ;
861875}
862876
877+ // ---------------------------------------------------------------------------
878+ // Content trust classification
879+ // ---------------------------------------------------------------------------
880+
881+ /**
882+ * Build the content_trust record from the sources list.
883+ * Maps each source to its trust classification:
884+ * - 'issue', 'pull_request' → 'untrusted-external'
885+ * - 'memory' → 'memory'
886+ * - 'task_description' → 'trusted'
887+ * Unknown sources default to 'untrusted-external' (fail-safe).
888+ */
889+ export function buildContentTrust ( sources : string [ ] ) : Record < string , ContentTrustLevel > {
890+ const trust : Record < string , ContentTrustLevel > = { } ;
891+ for ( const source of sources ) {
892+ switch ( source ) {
893+ case 'issue' :
894+ case 'pull_request' :
895+ trust [ source ] = 'untrusted-external' ;
896+ break ;
897+ case 'memory' :
898+ trust [ source ] = 'memory' ;
899+ break ;
900+ case 'task_description' :
901+ trust [ source ] = 'trusted' ;
902+ break ;
903+ default :
904+ logger . warn ( 'Unknown content source — defaulting to untrusted-external' , {
905+ source,
906+ metric_type : 'unknown_content_source' ,
907+ } ) ;
908+ trust [ source ] = 'untrusted-external' ;
909+ break ;
910+ }
911+ }
912+ return trust ;
913+ }
914+
863915// ---------------------------------------------------------------------------
864916// Main hydration pipeline
865917// ---------------------------------------------------------------------------
@@ -964,13 +1016,15 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
9641016 task_id : task . task_id , pr_number : task . pr_number , task_type : task . task_type ,
9651017 } ) ;
9661018 const fallbackPrompt = assembleUserPrompt ( task . task_id , task . repo , undefined , task . task_description ) ;
1019+ const fallbackSources = task . task_description ? [ 'task_description' ] : [ ] ;
9671020 return {
9681021 version : 1 ,
9691022 user_prompt : fallbackPrompt ,
970- sources : task . task_description ? [ 'task_description' ] : [ ] ,
1023+ sources : fallbackSources ,
9711024 token_estimate : estimateTokens ( fallbackPrompt ) ,
9721025 truncated : false ,
9731026 fallback_error : `Failed to fetch PR #${ task . pr_number } context from GitHub` ,
1027+ content_trust : buildContentTrust ( fallbackSources ) ,
9741028 } ;
9751029 }
9761030
@@ -1067,6 +1121,7 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
10671121 sources,
10681122 token_estimate : estimateTokens ( userPrompt ) ,
10691123 truncated,
1124+ content_trust : buildContentTrust ( sources ) ,
10701125 ...( guardrailBlocked && { guardrail_blocked : guardrailBlocked } ) ,
10711126 } ;
10721127
@@ -1096,6 +1151,7 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
10961151 sources,
10971152 token_estimate : tokenEstimate ,
10981153 truncated : budgetResult . truncated ,
1154+ content_trust : buildContentTrust ( sources ) ,
10991155 ...( guardrailBlocked && { guardrail_blocked : guardrailBlocked } ) ,
11001156 } ;
11011157 } catch ( err ) {
@@ -1120,13 +1176,15 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
11201176 metric_type : 'hydration_infra_failure' ,
11211177 } ) ;
11221178 const fallbackPrompt = assembleUserPrompt ( task . task_id , task . repo , undefined , task . task_description ) ;
1179+ const fallbackSources = task . task_description ? [ 'task_description' ] : [ ] ;
11231180 return {
11241181 version : 1 ,
11251182 user_prompt : fallbackPrompt ,
1126- sources : task . task_description ? [ 'task_description' ] : [ ] ,
1183+ sources : fallbackSources ,
11271184 token_estimate : estimateTokens ( fallbackPrompt ) ,
11281185 truncated : false ,
11291186 fallback_error : err instanceof Error ? err . message : String ( err ) ,
1187+ content_trust : buildContentTrust ( fallbackSources ) ,
11301188 } ;
11311189 }
11321190}
0 commit comments