Skip to content

Commit e6fd28d

Browse files
committed
fix(provenance): record why a resolved-secret registry became incomplete
Incompleteness is one-way and fails every later model projection in the run, so a single trip surfaces to the user as 'Router model input could not be safely projected' and nothing else. Nine guards could set it and none said which, in a file that imported no logger at all — a production failure this week could not be attributed to any of them. Name each guard, and stop the decrypt catch from discarding its cause. Reasons are static literals and the logged input path is block/field names; no resolved value is recorded.
1 parent cdeb83d commit e6fd28d

1 file changed

Lines changed: 64 additions & 14 deletions

File tree

apps/sim/executor/utils/resolved-secret-trace-registry.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
13
import { decryptSecret } from '@/lib/core/security/encryption'
24
import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest-metadata'
35
import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
@@ -10,6 +12,30 @@ import {
1012
} from '@/executor/utils/resolved-secret-matcher'
1113
import { 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+
| 'filter-capacity-exceeded'
33+
| 'restored-checkpoint-unavailable'
34+
| 'value-provenance-untrusted'
35+
| 'value-provenance-import-failed'
36+
| 'value-provenance-filter-incomplete'
37+
| 'unspecified'
38+
1339
export const ANONYMOUS_SECRET_TRACE_REPLACEMENT = OPAQUE_RESOLVED_SECRET_REPLACEMENT
1440
export const RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION = 1
1541

@@ -669,7 +695,7 @@ export class ResolvedSecretTraceRegistry {
669695

670696
const entry = this.getVerifiedResolvedEntry(name, resolvedValue)
671697
if (!entry) {
672-
this.markInputPathIncomplete(path)
698+
this.markInputPathIncomplete(path, 'unverified-resolved-entry')
673699
return false
674700
}
675701

@@ -790,7 +816,7 @@ export class ResolvedSecretTraceRegistry {
790816
typeof projectedValue === 'string' &&
791817
state.projectedValue !== projectedValue
792818
) {
793-
this.markInputPathIncomplete(path)
819+
this.markInputPathIncomplete(path, 'projection-mismatch')
794820
return
795821
}
796822
for (const entryKey of entryKeys) state.entryKeys.add(entryKey)
@@ -846,7 +872,7 @@ export class ResolvedSecretTraceRegistry {
846872
if (current.raw !== null && typeof current.raw === 'object') {
847873
const standaloneName = canonicalPlaceholderName(current.projected as string)
848874
if (!standaloneName || !entryKeysByName.has(standaloneName)) {
849-
this.markInputPathIncomplete(current.path)
875+
this.markInputPathIncomplete(current.path, 'unresolved-placeholder')
850876
return
851877
}
852878
recordProjectedMarkerAcrossRawLeaves(
@@ -994,12 +1020,12 @@ export class ResolvedSecretTraceRegistry {
9941020
options: ImportResolvedSecretTraceProvenanceOptions
9951021
): Promise<boolean> {
9961022
if (!options.trusted || !isResolvedSecretTraceProvenanceV1(provenance)) {
997-
this.markIncomplete()
1023+
this.markIncomplete('untrusted-provenance')
9981024
return false
9991025
}
10001026

10011027
if (!provenance.complete) {
1002-
this.markIncomplete()
1028+
this.markIncomplete('source-provenance-incomplete')
10031029
}
10041030

10051031
const sameScope = scopesMatch(provenance.scope, this.scope)
@@ -1016,9 +1042,14 @@ export class ResolvedSecretTraceRegistry {
10161042
},
10171043
{ propagated: true }
10181044
)
1019-
} catch {
1045+
} catch (error) {
10201046
importedAll = false
1021-
this.markIncomplete()
1047+
logger.warn('Provenance entry could not be decrypted', {
1048+
error: getErrorMessage(error, 'Unknown error'),
1049+
named: entry.name !== undefined,
1050+
scopeWorkspaceId: this.scope?.workspaceId,
1051+
})
1052+
this.markIncomplete('entry-decrypt-failed')
10221053
}
10231054
}
10241055

@@ -1058,19 +1089,19 @@ export class ResolvedSecretTraceRegistry {
10581089
options: { trusted: boolean; inputPath?: ResolvedSecretInputPath }
10591090
): Promise<ImportResolvedSecretTraceProvenanceForValueResult> {
10601091
if (!options.trusted || !isResolvedSecretTraceProvenanceV1(provenance)) {
1061-
this.markInputPathIncomplete(options.inputPath)
1092+
this.markInputPathIncomplete(options.inputPath, 'value-provenance-untrusted')
10621093
return { success: false, matched: false }
10631094
}
10641095

10651096
const sourceRegistry = new ResolvedSecretTraceRegistry([], provenance.scope)
10661097
const sourceImported = await sourceRegistry.importProvenance(provenance, { trusted: true })
10671098
const filteredProvenance = sourceRegistry.exportProvenanceForValue(value)
10681099
if (!sourceImported) {
1069-
this.markInputPathIncomplete(options.inputPath)
1100+
this.markInputPathIncomplete(options.inputPath, 'value-provenance-import-failed')
10701101
return { success: false, matched: false }
10711102
}
10721103
if (!filteredProvenance.complete) {
1073-
this.markInputPathIncomplete(options.inputPath)
1104+
this.markInputPathIncomplete(options.inputPath, 'value-provenance-filter-incomplete')
10741105
return { success: true, matched: false }
10751106
}
10761107
const filteredImported = await this.importProvenance(filteredProvenance, { trusted: true })
@@ -1214,10 +1245,16 @@ export class ResolvedSecretTraceRegistry {
12141245
return !this.complete || this.incompleteInputPaths.size > 0
12151246
}
12161247

1217-
markIncomplete(): void {
1248+
markIncomplete(reason: ResolvedSecretIncompletenessReason = 'unspecified'): void {
12181249
if (!this.complete) return
12191250
this.complete = false
12201251
this.modelEgressRevision += 1
1252+
logger.warn('Resolved secret registry marked incomplete', {
1253+
reason,
1254+
scopeWorkspaceId: this.scope?.workspaceId,
1255+
activeEntryCount: this.activeEntries.size,
1256+
incompleteInputPathCount: this.incompleteInputPaths.size,
1257+
})
12211258
}
12221259

12231260
/**
@@ -1388,7 +1425,11 @@ export class ResolvedSecretTraceRegistry {
13881425
matcher = createResolvedSecretMatcher(
13891426
[...candidatesByScanLiteral.keys()].map((plaintext) => ({ plaintext, replacement: '' }))
13901427
)
1391-
} catch {
1428+
} catch (error) {
1429+
logger.warn('Provenance filter matcher could not be built', {
1430+
error: getErrorMessage(error, 'Unknown error'),
1431+
candidateCount: candidatesByScanLiteral.size,
1432+
})
13921433
return { complete: false }
13931434
}
13941435

@@ -1623,15 +1664,24 @@ export class ResolvedSecretTraceRegistry {
16231664
)
16241665
}
16251666

1626-
private markInputPathIncomplete(path: ResolvedSecretInputPath | undefined): void {
1667+
private markInputPathIncomplete(
1668+
path: ResolvedSecretInputPath | undefined,
1669+
reason: ResolvedSecretIncompletenessReason = 'unspecified'
1670+
): void {
16271671
if (!path || path.length === 0) {
1628-
this.markIncomplete()
1672+
this.markIncomplete(reason)
16291673
return
16301674
}
16311675
const key = inputPathKey(path)
16321676
if (this.incompleteInputPaths.has(key)) return
16331677
this.incompleteInputPaths.set(key, [...path])
16341678
this.modelEgressRevision += 1
1679+
logger.warn('Resolved secret input path marked incomplete', {
1680+
reason,
1681+
inputPath: path.join('.'),
1682+
scopeWorkspaceId: this.scope?.workspaceId,
1683+
activeEntryCount: this.activeEntries.size,
1684+
})
16351685
}
16361686

16371687
private copyIncompleteInputPathsTo(

0 commit comments

Comments
 (0)