@@ -172,6 +172,27 @@ export interface FridayReflexLearnedFactApprovalResult {
172172 lastConfirmedAt : string ;
173173}
174174
175+ export interface FridayReflexSecureFactStageResult {
176+ secretId : string ;
177+ scope : string ;
178+ refKey : string ;
179+ createdAt ?: string ;
180+ updatedAt ?: string ;
181+ }
182+
183+ export interface FridayReflexSecureFactStagerInput {
184+ userId : string ;
185+ key : string ;
186+ subject : string ;
187+ value : string ;
188+ candidateId : string ;
189+ origin : FridayReflexCandidate [ "origin" ] ;
190+ sourceRunId ?: string ;
191+ sessionKey ?: string ;
192+ nowIso : string ;
193+ evidence : Record < string , JsonValue > ;
194+ }
195+
175196export interface CreateFridayReflexServiceDeps {
176197 db : FridaySqliteLayer ;
177198 candidateRepo : FridayReflexCandidateRepository ;
@@ -190,6 +211,15 @@ export interface CreateFridayReflexServiceDeps {
190211 nowIso : string ;
191212 evidence : Record < string , JsonValue > ;
192213 } ) => FridayReflexLearnedFactApprovalResult ;
214+ secureFactStager ?: ( input : FridayReflexSecureFactStagerInput ) => FridayReflexSecureFactStageResult ;
215+ secureFactRejecter ?: ( input : {
216+ userId : string ;
217+ candidateId : string ;
218+ secretId : string ;
219+ scope : string ;
220+ refKey : string ;
221+ nowIso : string ;
222+ } ) => { deleted : boolean } ;
193223 skillGenerator ?: FridaySkillGeneratorService ;
194224 workflowGenerator ?: FridayWorkflowGeneratorService ;
195225 learningEventWriter ?: ( events : FridayLearningEventAppendInput [ ] ) => void ;
@@ -261,7 +291,7 @@ function learnedFactSubjectSlug(subject: string): string | null {
261291
262292function extractLearnedFactFromRunTask ( task : string | undefined ) : {
263293 key : string ;
264- value : JsonValue ;
294+ value : string ;
265295 statement : string ;
266296 subject : string ;
267297} | null {
@@ -288,6 +318,32 @@ function extractLearnedFactFromRunTask(task: string | undefined): {
288318 } ;
289319}
290320
321+ function extractSecureFactFromRunTask ( task : string | undefined ) : {
322+ key : string ;
323+ value : string ;
324+ subject : string ;
325+ } | null {
326+ const text = task ?. trim ( ) . slice ( 0 , LEARNED_FACT_TASK_MAX_LENGTH ) ?? "" ;
327+ if ( ! text ) return null ;
328+ if ( / (?: d o n o t | d o n ' t | d o n t | n e v e r | 不 要 | 别 | 不 用 | 不 许 ) .{ 0 , 40 } (?: r e m e m b e r | l e a r n | r e c o r d | s t o r e | 记 住 | 学 习 | 保 存 ) / iu. test ( text ) ) {
329+ return null ;
330+ }
331+
332+ const english = / (?: r e m e m b e r | l e a r n | n o t e | r e c o r d | f o r f u t u r e r e f e r e n c e ) [ , : ] ? \s + (?: t h a t \s + ) ? ( (?: m y | o u r | t h e | t h i s | a | a n ) \s + [ ^ . ! ? ; \n : = ] { 2 , 80 } ?) \s + (?: i s | a r e | = | : ) \s + ( [ ^ . ! ? ; \n ] { 1 , 160 } ) / iu. exec ( text ) ;
333+ const chinese = / (?: 请 记 住 | 记 住 | 学 习 | 以 后 记 得 | 以 后 请 记 得 ) [: : ] ? \s * ( (?: 我 的 | 我 们 的 | 这 个 | 本 项 目 | 项 目 ) ? [ ^ , 。 ! ? \n : = : ] { 2 , 50 } ?) (?: 是 | 为 | = | : | : ) \s * ( [ ^ , 。 ! ? \n ] { 1 , 120 } ) / u. exec ( text ) ;
334+ const subject = ( english ?. [ 1 ] ?? chinese ?. [ 1 ] ?? "" ) . trim ( ) ;
335+ const value = cleanLearnedFactValue ( english ?. [ 2 ] ?? chinese ?. [ 2 ] ?? "" ) ;
336+ if ( ! subject || ! value ) return null ;
337+ if ( ! isFridaySensitiveLearningCandidate ( text ) && ! isFridaySensitiveLearningCandidate ( subject , value ) ) return null ;
338+ const slug = learnedFactSubjectSlug ( subject ) ;
339+ if ( ! slug ) return null ;
340+ return {
341+ key : `secure.${ slug } ` ,
342+ value,
343+ subject,
344+ } ;
345+ }
346+
291347function buildCandidateContent ( candidate : FridayReflexCandidate ) : string {
292348 const content = readPayloadString ( candidate . payload , "content" )
293349 ?? readPayloadString ( candidate . payload , "text" )
@@ -611,6 +667,20 @@ export function createFridayReflexService(
611667 ) ?? null ;
612668 }
613669
670+ function findPendingSecureFactCandidate ( db : FridayReflexDb , input : {
671+ userId : string ;
672+ key : string ;
673+ } ) : FridayReflexCandidate | null {
674+ return deps . candidateRepo . list ( db , {
675+ userId : input . userId ,
676+ kind : "secure_fact" ,
677+ limit : 200 ,
678+ } ) . find ( ( candidate ) =>
679+ ( candidate . status === "proposed" || candidate . status === "ready_for_review" || candidate . status === "testing" )
680+ && candidate . payload . key === input . key ,
681+ ) ?? null ;
682+ }
683+
614684 function createLearnedFactCandidateFromRunTask ( input : {
615685 userId : string ;
616686 runId ?: string ;
@@ -659,6 +729,88 @@ export function createFridayReflexService(
659729 } ) ;
660730 }
661731
732+ function createSecureFactCandidateFromRunTask ( input : {
733+ userId : string ;
734+ runId ?: string ;
735+ sessionKey ?: string ;
736+ channelKind ?: string ;
737+ channelUserId ?: string ;
738+ task ?: string ;
739+ } ) : FridayReflexCandidate | null {
740+ if ( ! deps . secureFactStager ) return null ;
741+ const extracted = extractSecureFactFromRunTask ( input . task ) ;
742+ if ( ! extracted ) return null ;
743+ const existing = deps . db . withReadConnection ( ( db ) =>
744+ findPendingSecureFactCandidate ( db , {
745+ userId : input . userId ,
746+ key : extracted . key ,
747+ } ) ) ;
748+ if ( existing ) return existing ;
749+
750+ const candidateId = deps . idGenerator ( ) ;
751+ const now = deps . nowIso ( ) ;
752+ const origin : FridayReflexCandidate [ "origin" ] = input . channelKind ? "channel" : "post_run" ;
753+ const redactedEvidence : Record < string , JsonValue > = {
754+ requiresExplicitConfirmation : true ,
755+ source : "post_run_task_text" ,
756+ extractionMode : "explicit_sensitive_fact_pattern" ,
757+ statement : `${ extracted . subject } = [encrypted secret pending review]` ,
758+ valueRedacted : true ,
759+ safetyBoundary : "encrypted_secret_staged_pending_review" ,
760+ } ;
761+ let staged : FridayReflexSecureFactStageResult ;
762+ try {
763+ staged = deps . secureFactStager ( {
764+ userId : input . userId ,
765+ key : extracted . key ,
766+ subject : extracted . subject ,
767+ value : extracted . value ,
768+ candidateId,
769+ origin,
770+ ...( input . runId ? { sourceRunId : input . runId } : { } ) ,
771+ ...( input . sessionKey ? { sessionKey : input . sessionKey } : { } ) ,
772+ nowIso : now ,
773+ evidence : redactedEvidence ,
774+ } ) ;
775+ } catch {
776+ return null ;
777+ }
778+
779+ return deps . db . withWriteTransaction ( ( db ) =>
780+ deps . candidateRepo . insert ( db , {
781+ id : candidateId ,
782+ nowIso : now ,
783+ userId : input . userId ,
784+ kind : "secure_fact" ,
785+ origin,
786+ status : "ready_for_review" ,
787+ sourceRunId : input . runId ,
788+ sessionKey : input . sessionKey ,
789+ channelKind : input . channelKind ,
790+ channelUserId : input . channelUserId ,
791+ title : `Review encrypted fact: ${ extracted . subject . slice ( 0 , 80 ) } ` ,
792+ summary : "Friday detected an explicit sensitive fact request. The value was staged in encrypted secret storage and stays redacted until you review it." ,
793+ payload : {
794+ key : extracted . key ,
795+ valueRedacted : true ,
796+ confidence : 0.84 ,
797+ secretId : staged . secretId ,
798+ secretScope : staged . scope ,
799+ secretRefKey : staged . refKey ,
800+ } ,
801+ evidence : {
802+ ...redactedEvidence ,
803+ secretId : staged . secretId ,
804+ secretScope : staged . scope ,
805+ secretRefKey : staged . refKey ,
806+ stagedAt : now ,
807+ stagedSecretCreatedAt : staged . createdAt ?? null ,
808+ } ,
809+ confidence : 0.84 ,
810+ riskTier : 3 ,
811+ } ) ) ;
812+ }
813+
662814 function createPreferenceConfirmationCandidate ( db : FridayReflexDb , input : {
663815 userId : string ;
664816 category : FridayUserPreferenceCategory ;
@@ -1300,6 +1452,28 @@ export function createFridayReflexService(
13001452 learnedFactLastConfirmedAt : result . lastConfirmedAt ,
13011453 ...( result . factId ? { learnedFactId : result . factId } : { } ) ,
13021454 } ;
1455+ } else if ( candidate . kind === "secure_fact" ) {
1456+ const key = readPayloadString ( candidate . payload , "key" ) ;
1457+ const secretId = readPayloadString ( candidate . payload , "secretId" ) ;
1458+ const secretScope = readPayloadString ( candidate . payload , "secretScope" ) ;
1459+ const secretRefKey = readPayloadString ( candidate . payload , "secretRefKey" ) ;
1460+ if ( ! key || ! secretId || ! secretScope || ! secretRefKey ) {
1461+ throw new FridayDomainError (
1462+ "REFLEX_SECURE_FACT_INVALID" ,
1463+ "Secure-fact candidate payload requires key and encrypted secret reference metadata." ,
1464+ { httpStatus : 400 } ,
1465+ ) ;
1466+ }
1467+ evidence = {
1468+ ...evidence ,
1469+ secureFactConfirmed : true ,
1470+ secureFactConfirmedAt : deps . nowIso ( ) ,
1471+ secureFactKey : key ,
1472+ secretId,
1473+ secretScope,
1474+ secretRefKey,
1475+ valueRedacted : true ,
1476+ } ;
13031477 } else if ( candidate . kind === "preference" ) {
13041478 const category = readPayloadString ( candidate . payload , "category" ) as FridayUserPreferenceCategory | undefined ;
13051479 const key = readPayloadString ( candidate . payload , "key" ) ;
@@ -1361,15 +1535,39 @@ export function createFridayReflexService(
13611535 } ,
13621536
13631537 rejectCandidate ( input ) {
1538+ const candidate = this . getCandidate ( { userId : input . userId , candidateId : input . candidateId } ) ;
1539+ const evidence : Record < string , JsonValue > = {
1540+ rejectedBy : input . userId ,
1541+ rejectedAt : deps . nowIso ( ) ,
1542+ ...( input . reason ? { reason : input . reason } : { } ) ,
1543+ } ;
1544+ if ( candidate . kind === "secure_fact" ) {
1545+ const secretId = readPayloadString ( candidate . payload , "secretId" ) ;
1546+ const secretScope = readPayloadString ( candidate . payload , "secretScope" ) ;
1547+ const secretRefKey = readPayloadString ( candidate . payload , "secretRefKey" ) ;
1548+ if ( secretId && secretScope && secretRefKey && deps . secureFactRejecter ) {
1549+ const result = deps . secureFactRejecter ( {
1550+ userId : input . userId ,
1551+ candidateId : input . candidateId ,
1552+ secretId,
1553+ scope : secretScope ,
1554+ refKey : secretRefKey ,
1555+ nowIso : deps . nowIso ( ) ,
1556+ } ) ;
1557+ evidence . stagedSecretDeleted = result . deleted ;
1558+ evidence . secretId = secretId ;
1559+ evidence . secretScope = secretScope ;
1560+ evidence . secretRefKey = secretRefKey ;
1561+ } else {
1562+ evidence . stagedSecretDeleted = false ;
1563+ evidence . secureFactRejectionCleanup = "unavailable_or_missing_secret_ref" ;
1564+ }
1565+ }
13641566 return updateCandidateStatus ( {
13651567 userId : input . userId ,
13661568 candidateId : input . candidateId ,
13671569 status : "rejected" ,
1368- evidence : {
1369- rejectedBy : input . userId ,
1370- rejectedAt : deps . nowIso ( ) ,
1371- ...( input . reason ? { reason : input . reason } : { } ) ,
1372- } ,
1570+ evidence,
13731571 } ) ;
13741572 } ,
13751573
@@ -1488,6 +1686,8 @@ export function createFridayReflexService(
14881686 const created : FridayReflexCandidate [ ] = [ ] ;
14891687 const toolSequence = input . toolSequence ?? [ ] ;
14901688 if ( input . outcome === "success" ) {
1689+ const secureFactCandidate = createSecureFactCandidateFromRunTask ( input ) ;
1690+ if ( secureFactCandidate ) created . push ( secureFactCandidate ) ;
14911691 const learnedFactCandidate = createLearnedFactCandidateFromRunTask ( input ) ;
14921692 if ( learnedFactCandidate ) created . push ( learnedFactCandidate ) ;
14931693 }
0 commit comments