1+ import { createLogger } from '@sim/logger'
2+ import { getErrorMessage } from '@sim/utils/errors'
13import { decryptSecret } from '@/lib/core/security/encryption'
24import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest-metadata'
35import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
@@ -10,6 +12,49 @@ import {
1012} from '@/executor/utils/resolved-secret-matcher'
1113import { getResolvedSecretMatcherCapacityFailure } from '@/executor/utils/resolved-secret-matcher-capacity'
1214
15+ const logger = createLogger ( 'ResolvedSecretTraceRegistry' )
16+
17+ /**
18+ * Why a registry stopped being able to vouch for what it projects.
19+ *
20+ * Incompleteness is one-way and fails every later model projection in the run, surfacing to the
21+ * user as a single opaque sentence. Recording which guard tripped is the only way to tell a
22+ * genuine containment from a matcher that merely could not decide — the reasons are static
23+ * literals and the logged path is block/field names, never a resolved value.
24+ */
25+ type ResolvedSecretIncompletenessReason =
26+ | 'untrusted-provenance'
27+ | 'source-provenance-incomplete'
28+ | 'entry-decrypt-failed'
29+ | 'unverified-resolved-entry'
30+ | 'projection-mismatch'
31+ | 'unresolved-placeholder'
32+ | 'provenance-capacity-exceeded'
33+ | 'restored-checkpoint-unavailable'
34+ | 'constructed-incomplete'
35+ | 'inherited-incomplete-source'
36+ | 'inherited-incomplete-input-path'
37+ | 'tool-call-scope-mismatch'
38+ | 'value-provenance-untrusted'
39+ | 'value-provenance-import-failed'
40+ | 'value-provenance-filter-incomplete'
41+ | 'unspecified'
42+
43+ /**
44+ * Reasons that carry an upstream fault forward rather than originating one.
45+ *
46+ * The origin already reported itself, so these log a level lower to stop one fault from reading
47+ * as several. Every other reason logs at error: incompleteness is one-way, so it permanently
48+ * fails the run's remaining model projections, and error is the only level that survives every
49+ * default the logger falls back to — production, test, and a self-hosted chart that sets no
50+ * `LOG_LEVEL` at all.
51+ */
52+ const PROPAGATED_INCOMPLETENESS_REASONS = new Set < ResolvedSecretIncompletenessReason > ( [
53+ 'inherited-incomplete-source' ,
54+ 'inherited-incomplete-input-path' ,
55+ 'source-provenance-incomplete' ,
56+ ] )
57+
1358export const ANONYMOUS_SECRET_TRACE_REPLACEMENT = OPAQUE_RESOLVED_SECRET_REPLACEMENT
1459export const RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION = 1
1560
@@ -552,7 +597,7 @@ export class ResolvedSecretTraceRegistry {
552597 this . scope = scope ? cloneProvenanceScope ( scope ) : undefined
553598 this . completeProvenanceEnvelopeBytes = serializedProvenanceEnvelopeByteSize ( true , this . scope )
554599 if ( this . completeProvenanceEnvelopeBytes > MAX_SERIALIZED_PROVENANCE_BYTES ) {
555- this . markIncomplete ( )
600+ this . markIncomplete ( 'provenance-capacity-exceeded' )
556601 }
557602 let catalogEntriesSeen = 0
558603 for ( const entry of catalogEntries ) {
@@ -580,7 +625,7 @@ export class ResolvedSecretTraceRegistry {
580625 }
581626 this . copyResolvedInputPathsTo ( fork )
582627 this . copyIncompleteInputPathsTo ( fork )
583- if ( ! this . complete ) fork . markIncomplete ( )
628+ if ( ! this . complete ) fork . markIncomplete ( 'inherited-incomplete-source' )
584629 return fork
585630 }
586631
@@ -591,12 +636,12 @@ export class ResolvedSecretTraceRegistry {
591636 ) : ResolvedSecretTraceRegistry {
592637 const fork = new ResolvedSecretTraceRegistry ( this . catalog . values ( ) , this . scope )
593638 if ( ! this . complete ) {
594- fork . markIncomplete ( )
639+ fork . markIncomplete ( 'inherited-incomplete-source' )
595640 return fork
596641 }
597642
598643 if ( this . hasIncompleteInputPathOverlapping ( paths ) ) {
599- fork . markIncomplete ( )
644+ fork . markIncomplete ( 'inherited-incomplete-input-path' )
600645 return fork
601646 }
602647
@@ -620,14 +665,14 @@ export class ResolvedSecretTraceRegistry {
620665 fork . addActiveEntry ( { ...entry } , { propagated : true } )
621666 }
622667 }
623- if ( this . isPermanentlyIncomplete ( ) ) fork . markIncomplete ( )
668+ if ( this . isPermanentlyIncomplete ( ) ) fork . markIncomplete ( 'inherited-incomplete-source' )
624669 return fork
625670 }
626671
627672 /** Merges one settled tool-call registry into the turn-scoped registry. */
628673 mergeToolCallRegistry ( child : ResolvedSecretTraceRegistry ) : void {
629674 if ( ! scopesMatch ( this . scope , child . scope ) || ! child . isComplete ( ) ) {
630- this . markIncomplete ( )
675+ this . markIncomplete ( 'tool-call-scope-mismatch' )
631676 return
632677 }
633678
@@ -649,7 +694,7 @@ export class ResolvedSecretTraceRegistry {
649694 if ( resolvedValue . length === 0 ) return false
650695 const entry = this . getVerifiedResolvedEntry ( name , resolvedValue )
651696 if ( ! entry ) {
652- this . markIncomplete ( )
697+ this . markIncomplete ( 'unverified-resolved-entry' )
653698 return false
654699 }
655700
@@ -669,7 +714,7 @@ export class ResolvedSecretTraceRegistry {
669714
670715 const entry = this . getVerifiedResolvedEntry ( name , resolvedValue )
671716 if ( ! entry ) {
672- this . markInputPathIncomplete ( path )
717+ this . markInputPathIncomplete ( path , 'unverified-resolved-entry' )
673718 return false
674719 }
675720
@@ -790,7 +835,7 @@ export class ResolvedSecretTraceRegistry {
790835 typeof projectedValue === 'string' &&
791836 state . projectedValue !== projectedValue
792837 ) {
793- this . markInputPathIncomplete ( path )
838+ this . markInputPathIncomplete ( path , 'projection-mismatch' )
794839 return
795840 }
796841 for ( const entryKey of entryKeys ) state . entryKeys . add ( entryKey )
@@ -846,7 +891,7 @@ export class ResolvedSecretTraceRegistry {
846891 if ( current . raw !== null && typeof current . raw === 'object' ) {
847892 const standaloneName = canonicalPlaceholderName ( current . projected as string )
848893 if ( ! standaloneName || ! entryKeysByName . has ( standaloneName ) ) {
849- this . markInputPathIncomplete ( current . path )
894+ this . markInputPathIncomplete ( current . path , 'unresolved-placeholder' )
850895 return
851896 }
852897 recordProjectedMarkerAcrossRawLeaves (
@@ -994,12 +1039,12 @@ export class ResolvedSecretTraceRegistry {
9941039 options : ImportResolvedSecretTraceProvenanceOptions
9951040 ) : Promise < boolean > {
9961041 if ( ! options . trusted || ! isResolvedSecretTraceProvenanceV1 ( provenance ) ) {
997- this . markIncomplete ( )
1042+ this . markIncomplete ( 'untrusted-provenance' )
9981043 return false
9991044 }
10001045
10011046 if ( ! provenance . complete ) {
1002- this . markIncomplete ( )
1047+ this . markIncomplete ( 'source-provenance-incomplete' )
10031048 }
10041049
10051050 const sameScope = scopesMatch ( provenance . scope , this . scope )
@@ -1016,9 +1061,14 @@ export class ResolvedSecretTraceRegistry {
10161061 } ,
10171062 { propagated : true }
10181063 )
1019- } catch {
1064+ } catch ( error ) {
10201065 importedAll = false
1021- this . markIncomplete ( )
1066+ logger . error ( 'Provenance entry could not be decrypted' , {
1067+ error : getErrorMessage ( error , 'Unknown error' ) ,
1068+ named : entry . name !== undefined ,
1069+ scopeWorkspaceId : this . scope ?. workspaceId ,
1070+ } )
1071+ this . markIncomplete ( 'entry-decrypt-failed' )
10221072 }
10231073 }
10241074
@@ -1058,19 +1108,19 @@ export class ResolvedSecretTraceRegistry {
10581108 options : { trusted : boolean ; inputPath ?: ResolvedSecretInputPath }
10591109 ) : Promise < ImportResolvedSecretTraceProvenanceForValueResult > {
10601110 if ( ! options . trusted || ! isResolvedSecretTraceProvenanceV1 ( provenance ) ) {
1061- this . markInputPathIncomplete ( options . inputPath )
1111+ this . markInputPathIncomplete ( options . inputPath , 'value-provenance-untrusted' )
10621112 return { success : false , matched : false }
10631113 }
10641114
10651115 const sourceRegistry = new ResolvedSecretTraceRegistry ( [ ] , provenance . scope )
10661116 const sourceImported = await sourceRegistry . importProvenance ( provenance , { trusted : true } )
10671117 const filteredProvenance = sourceRegistry . exportProvenanceForValue ( value )
10681118 if ( ! sourceImported ) {
1069- this . markInputPathIncomplete ( options . inputPath )
1119+ this . markInputPathIncomplete ( options . inputPath , 'value-provenance-import-failed' )
10701120 return { success : false , matched : false }
10711121 }
10721122 if ( ! filteredProvenance . complete ) {
1073- this . markInputPathIncomplete ( options . inputPath )
1123+ this . markInputPathIncomplete ( options . inputPath , 'value-provenance-filter-incomplete' )
10741124 return { success : true , matched : false }
10751125 }
10761126 const filteredImported = await this . importProvenance ( filteredProvenance , { trusted : true } )
@@ -1214,10 +1264,19 @@ export class ResolvedSecretTraceRegistry {
12141264 return ! this . complete || this . incompleteInputPaths . size > 0
12151265 }
12161266
1217- markIncomplete ( ) : void {
1267+ markIncomplete ( reason : ResolvedSecretIncompletenessReason = 'unspecified' ) : void {
12181268 if ( ! this . complete ) return
12191269 this . complete = false
12201270 this . modelEgressRevision += 1
1271+ const details = {
1272+ reason,
1273+ scopeWorkspaceId : this . scope ?. workspaceId ,
1274+ activeEntryCount : this . activeEntries . size ,
1275+ incompleteInputPathCount : this . incompleteInputPaths . size ,
1276+ }
1277+ const message = 'Resolved secret registry marked incomplete'
1278+ if ( PROPAGATED_INCOMPLETENESS_REASONS . has ( reason ) ) logger . warn ( message , details )
1279+ else logger . error ( message , details )
12211280 }
12221281
12231282 /**
@@ -1388,7 +1447,11 @@ export class ResolvedSecretTraceRegistry {
13881447 matcher = createResolvedSecretMatcher (
13891448 [ ...candidatesByScanLiteral . keys ( ) ] . map ( ( plaintext ) => ( { plaintext, replacement : '' } ) )
13901449 )
1391- } catch {
1450+ } catch ( error ) {
1451+ logger . error ( 'Provenance filter matcher could not be built' , {
1452+ error : getErrorMessage ( error , 'Unknown error' ) ,
1453+ candidateCount : candidatesByScanLiteral . size ,
1454+ } )
13921455 return { complete : false }
13931456 }
13941457
@@ -1623,15 +1686,27 @@ export class ResolvedSecretTraceRegistry {
16231686 )
16241687 }
16251688
1626- private markInputPathIncomplete ( path : ResolvedSecretInputPath | undefined ) : void {
1689+ private markInputPathIncomplete (
1690+ path : ResolvedSecretInputPath | undefined ,
1691+ reason : ResolvedSecretIncompletenessReason = 'unspecified'
1692+ ) : void {
16271693 if ( ! path || path . length === 0 ) {
1628- this . markIncomplete ( )
1694+ this . markIncomplete ( reason )
16291695 return
16301696 }
16311697 const key = inputPathKey ( path )
16321698 if ( this . incompleteInputPaths . has ( key ) ) return
16331699 this . incompleteInputPaths . set ( key , [ ...path ] )
16341700 this . modelEgressRevision += 1
1701+ const details = {
1702+ reason,
1703+ inputPath : path . join ( '.' ) ,
1704+ scopeWorkspaceId : this . scope ?. workspaceId ,
1705+ activeEntryCount : this . activeEntries . size ,
1706+ }
1707+ const message = 'Resolved secret input path marked incomplete'
1708+ if ( PROPAGATED_INCOMPLETENESS_REASONS . has ( reason ) ) logger . warn ( message , details )
1709+ else logger . error ( message , details )
16351710 }
16361711
16371712 private copyIncompleteInputPathsTo (
@@ -1672,7 +1747,7 @@ export class ResolvedSecretTraceRegistry {
16721747 entryBytes >
16731748 MAX_SERIALIZED_PROVENANCE_BYTES
16741749 ) {
1675- this . markIncomplete ( )
1750+ this . markIncomplete ( 'provenance-capacity-exceeded' )
16761751 return
16771752 }
16781753 this . activeEntries . set ( key , entry )
@@ -1733,7 +1808,7 @@ export async function createResolvedSecretTraceRegistry(
17331808 options . restoreTrusted === true &&
17341809 options . restoredCheckpointVersion !== undefined
17351810 ) {
1736- registry . markIncomplete ( )
1811+ registry . markIncomplete ( 'restored-checkpoint-unavailable' )
17371812 }
17381813
17391814 return registry
@@ -1744,6 +1819,6 @@ export function createIncompleteResolvedSecretTraceRegistry(
17441819 scope ?: ResolvedSecretTraceScopeV1
17451820) : ResolvedSecretTraceRegistry {
17461821 const registry = new ResolvedSecretTraceRegistry ( [ ] , scope )
1747- registry . markIncomplete ( )
1822+ registry . markIncomplete ( 'constructed-incomplete' )
17481823 return registry
17491824}
0 commit comments