@bli-cockpit/telemetry-core 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/evidence-upload.d.ts +16 -4
- package/dist/ingest-dto.d.ts +12 -3
- package/dist/privacy.d.ts +4 -1
- package/dist/secret-guards.d.ts +19 -1
- package/dist/secret-guards.js +88 -4
- package/dist/source-adapter.d.ts +8 -2
- package/dist/upload-failure-class.d.ts +35 -0
- package/dist/upload-failure-class.js +67 -4
- package/package.json +1 -1
|
@@ -28,7 +28,10 @@ export declare const RawEvidenceLegacyUploadResponseSchema: z.ZodObject<{
|
|
|
28
28
|
byte_size: z.ZodNumber;
|
|
29
29
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
30
30
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
31
|
-
status: z.
|
|
31
|
+
status: z.ZodEnum<{
|
|
32
|
+
sanitized: "sanitized";
|
|
33
|
+
scanned_clean: "scanned_clean";
|
|
34
|
+
}>;
|
|
32
35
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
33
36
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
34
37
|
local_collector: "local_collector";
|
|
@@ -90,7 +93,10 @@ export declare const RawEvidenceUploadBeginObjectSchema: z.ZodObject<{
|
|
|
90
93
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
91
94
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
92
95
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
93
|
-
status: z.
|
|
96
|
+
status: z.ZodEnum<{
|
|
97
|
+
sanitized: "sanitized";
|
|
98
|
+
scanned_clean: "scanned_clean";
|
|
99
|
+
}>;
|
|
94
100
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
95
101
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
96
102
|
local_collector: "local_collector";
|
|
@@ -186,7 +192,10 @@ export declare const RawEvidenceUploadBeginRequestSchema: z.ZodObject<{
|
|
|
186
192
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
187
193
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
188
194
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
189
|
-
status: z.
|
|
195
|
+
status: z.ZodEnum<{
|
|
196
|
+
sanitized: "sanitized";
|
|
197
|
+
scanned_clean: "scanned_clean";
|
|
198
|
+
}>;
|
|
190
199
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
191
200
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
192
201
|
local_collector: "local_collector";
|
|
@@ -351,7 +360,10 @@ export declare const RawEvidenceUploadCommitResponseSchema: z.ZodObject<{
|
|
|
351
360
|
chunk_count: z.ZodNumber;
|
|
352
361
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
353
362
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
354
|
-
status: z.
|
|
363
|
+
status: z.ZodEnum<{
|
|
364
|
+
sanitized: "sanitized";
|
|
365
|
+
scanned_clean: "scanned_clean";
|
|
366
|
+
}>;
|
|
355
367
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
356
368
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
357
369
|
local_collector: "local_collector";
|
package/dist/ingest-dto.d.ts
CHANGED
|
@@ -290,7 +290,10 @@ export declare const TelemetryIngestEventDtoSchema: z.ZodObject<{
|
|
|
290
290
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
291
291
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
292
292
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
293
|
-
status: z.
|
|
293
|
+
status: z.ZodEnum<{
|
|
294
|
+
sanitized: "sanitized";
|
|
295
|
+
scanned_clean: "scanned_clean";
|
|
296
|
+
}>;
|
|
294
297
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
295
298
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
296
299
|
local_collector: "local_collector";
|
|
@@ -763,7 +766,10 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
|
|
|
763
766
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
764
767
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
765
768
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
766
|
-
status: z.
|
|
769
|
+
status: z.ZodEnum<{
|
|
770
|
+
sanitized: "sanitized";
|
|
771
|
+
scanned_clean: "scanned_clean";
|
|
772
|
+
}>;
|
|
767
773
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
768
774
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
769
775
|
local_collector: "local_collector";
|
|
@@ -913,7 +919,10 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
|
|
|
913
919
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
914
920
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
915
921
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
916
|
-
status: z.
|
|
922
|
+
status: z.ZodEnum<{
|
|
923
|
+
sanitized: "sanitized";
|
|
924
|
+
scanned_clean: "scanned_clean";
|
|
925
|
+
}>;
|
|
917
926
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
918
927
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
919
928
|
local_collector: "local_collector";
|
package/dist/privacy.d.ts
CHANGED
|
@@ -68,7 +68,10 @@ export declare const RawEvidencePointerSchema: z.ZodObject<{
|
|
|
68
68
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
69
69
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
70
70
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
71
|
-
status: z.
|
|
71
|
+
status: z.ZodEnum<{
|
|
72
|
+
sanitized: "sanitized";
|
|
73
|
+
scanned_clean: "scanned_clean";
|
|
74
|
+
}>;
|
|
72
75
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
73
76
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
74
77
|
local_collector: "local_collector";
|
package/dist/secret-guards.d.ts
CHANGED
|
@@ -43,9 +43,27 @@ export declare const RawEvidenceRedactionRangeSchema: z.ZodObject<{
|
|
|
43
43
|
rule_id: z.ZodString;
|
|
44
44
|
}, z.core.$strict>;
|
|
45
45
|
export type RawEvidenceRedactionRange = z.infer<typeof RawEvidenceRedactionRangeSchema>;
|
|
46
|
+
/**
|
|
47
|
+
* The verdict of a secret scan — NOT "did the bytes change" (BLI-3277).
|
|
48
|
+
*
|
|
49
|
+
* `sanitized` means the scan matched and the uploaded bytes differ from what was
|
|
50
|
+
* read. `scanned_clean` means the same scan ran over the same bytes and matched
|
|
51
|
+
* nothing, so the uploaded bytes are the original ones. The second value exists
|
|
52
|
+
* because every reader downstream asks "was this scanned?", and for 70% of
|
|
53
|
+
* transcripts — the ones with no secret in them — the honest answer used to be
|
|
54
|
+
* an absent record, which reads identically to "nobody ever looked".
|
|
55
|
+
*/
|
|
56
|
+
export declare const RawEvidenceRedactionStatusSchema: z.ZodEnum<{
|
|
57
|
+
sanitized: "sanitized";
|
|
58
|
+
scanned_clean: "scanned_clean";
|
|
59
|
+
}>;
|
|
60
|
+
export type RawEvidenceRedactionStatus = z.infer<typeof RawEvidenceRedactionStatusSchema>;
|
|
46
61
|
export declare const RawEvidenceRedactionMetadataSchema: z.ZodObject<{
|
|
47
62
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
48
|
-
status: z.
|
|
63
|
+
status: z.ZodEnum<{
|
|
64
|
+
sanitized: "sanitized";
|
|
65
|
+
scanned_clean: "scanned_clean";
|
|
66
|
+
}>;
|
|
49
67
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
50
68
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
51
69
|
local_collector: "local_collector";
|
package/dist/secret-guards.js
CHANGED
|
@@ -153,14 +153,28 @@ export const RawEvidenceRedactionRangeSchema = z
|
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
|
+
/**
|
|
157
|
+
* The verdict of a secret scan — NOT "did the bytes change" (BLI-3277).
|
|
158
|
+
*
|
|
159
|
+
* `sanitized` means the scan matched and the uploaded bytes differ from what was
|
|
160
|
+
* read. `scanned_clean` means the same scan ran over the same bytes and matched
|
|
161
|
+
* nothing, so the uploaded bytes are the original ones. The second value exists
|
|
162
|
+
* because every reader downstream asks "was this scanned?", and for 70% of
|
|
163
|
+
* transcripts — the ones with no secret in them — the honest answer used to be
|
|
164
|
+
* an absent record, which reads identically to "nobody ever looked".
|
|
165
|
+
*/
|
|
166
|
+
export const RawEvidenceRedactionStatusSchema = z.enum([
|
|
167
|
+
"sanitized",
|
|
168
|
+
"scanned_clean",
|
|
169
|
+
]);
|
|
156
170
|
export const RawEvidenceRedactionMetadataSchema = z
|
|
157
171
|
.object({
|
|
158
172
|
schema_version: z.literal("raw-evidence-redaction.v1"),
|
|
159
|
-
status:
|
|
173
|
+
status: RawEvidenceRedactionStatusSchema,
|
|
160
174
|
mode: z.literal("deterministic_text_replacement"),
|
|
161
175
|
applied_by: z.array(RawEvidenceRedactionSourceSchema).min(1),
|
|
162
|
-
rule_counts: z.array(RawEvidenceRedactionRuleCountSchema)
|
|
163
|
-
secret_like_match_count: z.number().int().
|
|
176
|
+
rule_counts: z.array(RawEvidenceRedactionRuleCountSchema),
|
|
177
|
+
secret_like_match_count: z.number().int().nonnegative(),
|
|
164
178
|
redacted_fields: z.array(z.string().trim().min(1).max(160)).default([]),
|
|
165
179
|
redacted_ranges: z.array(RawEvidenceRedactionRangeSchema).max(200).default([]),
|
|
166
180
|
original_content_hash_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
@@ -168,7 +182,77 @@ export const RawEvidenceRedactionMetadataSchema = z
|
|
|
168
182
|
original_byte_size: z.number().int().nonnegative().optional(),
|
|
169
183
|
sanitized_byte_size: z.number().int().nonnegative().optional(),
|
|
170
184
|
})
|
|
171
|
-
.strict()
|
|
185
|
+
.strict()
|
|
186
|
+
// The per-status invariants the old `z.literal("sanitized")` + `.min(1)` +
|
|
187
|
+
// `.positive()` shape enforced structurally. They still hold for a sanitized
|
|
188
|
+
// record; a clean one has to prove the opposite — no rule fired, no range was
|
|
189
|
+
// replaced, no field was masked, and the bytes came out the way they went in.
|
|
190
|
+
.superRefine((metadata, context) => {
|
|
191
|
+
if (metadata.status === "sanitized") {
|
|
192
|
+
if (metadata.rule_counts.length === 0) {
|
|
193
|
+
context.addIssue({
|
|
194
|
+
code: "custom",
|
|
195
|
+
message: "a sanitized redaction must name at least one rule",
|
|
196
|
+
path: ["rule_counts"],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (metadata.secret_like_match_count < 1) {
|
|
200
|
+
context.addIssue({
|
|
201
|
+
code: "custom",
|
|
202
|
+
message: "a sanitized redaction must count at least one match",
|
|
203
|
+
path: ["secret_like_match_count"],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (metadata.rule_counts.length > 0) {
|
|
209
|
+
context.addIssue({
|
|
210
|
+
code: "custom",
|
|
211
|
+
message: "a clean scan cannot name a rule that fired",
|
|
212
|
+
path: ["rule_counts"],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (metadata.secret_like_match_count !== 0) {
|
|
216
|
+
context.addIssue({
|
|
217
|
+
code: "custom",
|
|
218
|
+
message: "a clean scan cannot count a match",
|
|
219
|
+
path: ["secret_like_match_count"],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (metadata.redacted_ranges.length > 0) {
|
|
223
|
+
context.addIssue({
|
|
224
|
+
code: "custom",
|
|
225
|
+
message: "a clean scan cannot redact a range",
|
|
226
|
+
path: ["redacted_ranges"],
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (metadata.redacted_fields.length > 0) {
|
|
230
|
+
context.addIssue({
|
|
231
|
+
code: "custom",
|
|
232
|
+
message: "a clean scan cannot redact a field",
|
|
233
|
+
path: ["redacted_fields"],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (metadata.original_content_hash_sha256 !== undefined &&
|
|
237
|
+
metadata.sanitized_content_hash_sha256 !== undefined &&
|
|
238
|
+
metadata.original_content_hash_sha256 !==
|
|
239
|
+
metadata.sanitized_content_hash_sha256) {
|
|
240
|
+
context.addIssue({
|
|
241
|
+
code: "custom",
|
|
242
|
+
message: "a clean scan cannot change the content hash",
|
|
243
|
+
path: ["sanitized_content_hash_sha256"],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (metadata.original_byte_size !== undefined &&
|
|
247
|
+
metadata.sanitized_byte_size !== undefined &&
|
|
248
|
+
metadata.original_byte_size !== metadata.sanitized_byte_size) {
|
|
249
|
+
context.addIssue({
|
|
250
|
+
code: "custom",
|
|
251
|
+
message: "a clean scan cannot change the byte size",
|
|
252
|
+
path: ["sanitized_byte_size"],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
172
256
|
// Same fence as the detector (BLI-3116) — the two must agree or a prefixed
|
|
173
257
|
// name would be detected and then left unredacted, which costs whole sessions
|
|
174
258
|
// (see findSecretRedactionMatches).
|
package/dist/source-adapter.d.ts
CHANGED
|
@@ -76,7 +76,10 @@ export declare const SourceScanEventSummarySchema: z.ZodObject<{
|
|
|
76
76
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
77
77
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
78
78
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
79
|
-
status: z.
|
|
79
|
+
status: z.ZodEnum<{
|
|
80
|
+
sanitized: "sanitized";
|
|
81
|
+
scanned_clean: "scanned_clean";
|
|
82
|
+
}>;
|
|
80
83
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
81
84
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
82
85
|
local_collector: "local_collector";
|
|
@@ -190,7 +193,10 @@ export declare const SourceScanResultSchema: z.ZodObject<{
|
|
|
190
193
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
191
194
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
192
195
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
193
|
-
status: z.
|
|
196
|
+
status: z.ZodEnum<{
|
|
197
|
+
sanitized: "sanitized";
|
|
198
|
+
scanned_clean: "scanned_clean";
|
|
199
|
+
}>;
|
|
194
200
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
195
201
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
196
202
|
local_collector: "local_collector";
|
|
@@ -66,6 +66,41 @@ export declare const COMMIT_CRASHED_PLATFORM = "commit_crashed_platform";
|
|
|
66
66
|
* eligible sync offers the same bytes again.
|
|
67
67
|
*/
|
|
68
68
|
export declare const DELIVERY_BACKOFF_HOLDING = "delivery_backoff_holding";
|
|
69
|
+
/**
|
|
70
|
+
* Raw evidence was never offered because the session's attribution state is not
|
|
71
|
+
* one the upload policy accepts (BLI-3272).
|
|
72
|
+
*
|
|
73
|
+
* `isRawEvidenceUploadableAttributionState` refuses a session whose attribution
|
|
74
|
+
* produced neither a repository identity nor an approved-root synthetic
|
|
75
|
+
* workspace. That refusal is a real, explainable decision — but until this label
|
|
76
|
+
* existed the report builders answered it by spreading `{}`, so the row reached
|
|
77
|
+
* `ambient_codex_sessions` with `upload_state = NULL` AND `upload_reason = NULL`.
|
|
78
|
+
* 1,256 sessions sat in that hole on 2026-08-29, 197 of them first seen inside a
|
|
79
|
+
* week from current 0.2.30 machines, every one `unattributed` /
|
|
80
|
+
* `cwd_outside_scanned_worktrees`. A withheld session that says nothing is the
|
|
81
|
+
* exact failure the session-first contract forbids, and the migration comment
|
|
82
|
+
* claiming "every write path now sets it" was false for all of them.
|
|
83
|
+
*
|
|
84
|
+
* Written with the deciding attribution reason appended after a colon
|
|
85
|
+
* (`not_uploadable_attribution_state:cwd_outside_scanned_worktrees`) so the
|
|
86
|
+
* upload half of the row explains itself without a join;
|
|
87
|
+
* `baseUploadFailureReason` strips that qualifier for classification.
|
|
88
|
+
*
|
|
89
|
+
* `deterministic` for the same reason as the bare attribution labels below: the
|
|
90
|
+
* scan completed and produced no target, so repeating it changes nothing.
|
|
91
|
+
* Widening the operator's approved roots can change it — a human decision, not a
|
|
92
|
+
* retry.
|
|
93
|
+
*/
|
|
94
|
+
export declare const NOT_UPLOADABLE_ATTRIBUTION_STATE = "not_uploadable_attribution_state";
|
|
95
|
+
/**
|
|
96
|
+
* Compose the withheld-by-attribution reason for one session.
|
|
97
|
+
*
|
|
98
|
+
* The detail is the attribution reason that decided it. Anything outside the
|
|
99
|
+
* safe-label charset — colons included, so the stem always ends at the first one
|
|
100
|
+
* — collapses to `_`, and an empty or unusable detail degrades to the bare stem
|
|
101
|
+
* rather than inventing one.
|
|
102
|
+
*/
|
|
103
|
+
export declare function notUploadableAttributionStateReason(attributionReason: string | null | undefined): string;
|
|
69
104
|
/**
|
|
70
105
|
* The listed reason a composed label is a variant of, or the label unchanged.
|
|
71
106
|
*
|
|
@@ -72,6 +72,53 @@ export const COMMIT_CRASHED_PLATFORM = "commit_crashed_platform";
|
|
|
72
72
|
* eligible sync offers the same bytes again.
|
|
73
73
|
*/
|
|
74
74
|
export const DELIVERY_BACKOFF_HOLDING = "delivery_backoff_holding";
|
|
75
|
+
/**
|
|
76
|
+
* Raw evidence was never offered because the session's attribution state is not
|
|
77
|
+
* one the upload policy accepts (BLI-3272).
|
|
78
|
+
*
|
|
79
|
+
* `isRawEvidenceUploadableAttributionState` refuses a session whose attribution
|
|
80
|
+
* produced neither a repository identity nor an approved-root synthetic
|
|
81
|
+
* workspace. That refusal is a real, explainable decision — but until this label
|
|
82
|
+
* existed the report builders answered it by spreading `{}`, so the row reached
|
|
83
|
+
* `ambient_codex_sessions` with `upload_state = NULL` AND `upload_reason = NULL`.
|
|
84
|
+
* 1,256 sessions sat in that hole on 2026-08-29, 197 of them first seen inside a
|
|
85
|
+
* week from current 0.2.30 machines, every one `unattributed` /
|
|
86
|
+
* `cwd_outside_scanned_worktrees`. A withheld session that says nothing is the
|
|
87
|
+
* exact failure the session-first contract forbids, and the migration comment
|
|
88
|
+
* claiming "every write path now sets it" was false for all of them.
|
|
89
|
+
*
|
|
90
|
+
* Written with the deciding attribution reason appended after a colon
|
|
91
|
+
* (`not_uploadable_attribution_state:cwd_outside_scanned_worktrees`) so the
|
|
92
|
+
* upload half of the row explains itself without a join;
|
|
93
|
+
* `baseUploadFailureReason` strips that qualifier for classification.
|
|
94
|
+
*
|
|
95
|
+
* `deterministic` for the same reason as the bare attribution labels below: the
|
|
96
|
+
* scan completed and produced no target, so repeating it changes nothing.
|
|
97
|
+
* Widening the operator's approved roots can change it — a human decision, not a
|
|
98
|
+
* retry.
|
|
99
|
+
*/
|
|
100
|
+
export const NOT_UPLOADABLE_ATTRIBUTION_STATE = "not_uploadable_attribution_state";
|
|
101
|
+
/** Longest label the ambient envelope's `SafeLabelSchema` accepts. */
|
|
102
|
+
const MAX_UPLOAD_REASON_LENGTH = 120;
|
|
103
|
+
/**
|
|
104
|
+
* Compose the withheld-by-attribution reason for one session.
|
|
105
|
+
*
|
|
106
|
+
* The detail is the attribution reason that decided it. Anything outside the
|
|
107
|
+
* safe-label charset — colons included, so the stem always ends at the first one
|
|
108
|
+
* — collapses to `_`, and an empty or unusable detail degrades to the bare stem
|
|
109
|
+
* rather than inventing one.
|
|
110
|
+
*/
|
|
111
|
+
export function notUploadableAttributionStateReason(attributionReason) {
|
|
112
|
+
const detail = (attributionReason ?? "")
|
|
113
|
+
.trim()
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9_.-]+/gu, "_")
|
|
116
|
+
.replace(/^_+|_+$/gu, "");
|
|
117
|
+
if (!detail)
|
|
118
|
+
return NOT_UPLOADABLE_ATTRIBUTION_STATE;
|
|
119
|
+
const budget = MAX_UPLOAD_REASON_LENGTH - NOT_UPLOADABLE_ATTRIBUTION_STATE.length - 1;
|
|
120
|
+
return `${NOT_UPLOADABLE_ATTRIBUTION_STATE}:${detail.slice(0, budget)}`;
|
|
121
|
+
}
|
|
75
122
|
const UPLOAD_FAILURE_CLASSES = {
|
|
76
123
|
// Budgets and locks: the file was fine and the run simply ran out of room or
|
|
77
124
|
// was beaten to it. The next pass has room.
|
|
@@ -117,6 +164,10 @@ const UPLOAD_FAILURE_CLASSES = {
|
|
|
117
164
|
cwd_not_a_repo: "deterministic",
|
|
118
165
|
no_repo_signals: "deterministic",
|
|
119
166
|
multiple_worktrees_close_scores: "deterministic",
|
|
167
|
+
// The upload policy declined the session's attribution state outright
|
|
168
|
+
// (BLI-3272). Same shape of answer as the four above — the scan finished and
|
|
169
|
+
// there is nowhere to put the object — so the same class.
|
|
170
|
+
[NOT_UPLOADABLE_ATTRIBUTION_STATE]: "deterministic",
|
|
120
171
|
// Working as designed. Never retry these — a retry here is an attempt to get
|
|
121
172
|
// a redaction guard to change its mind.
|
|
122
173
|
secret_like_content_guard: "withheld_by_policy",
|
|
@@ -137,10 +188,22 @@ const HTTP_STATUS_SUFFIX = /_http_\d{3}$/u;
|
|
|
137
188
|
* still classifies as `unknown`, exactly as before.
|
|
138
189
|
*/
|
|
139
190
|
export function baseUploadFailureReason(reason) {
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
return reason;
|
|
143
|
-
|
|
191
|
+
const httpStem = reason.replace(HTTP_STATUS_SUFFIX, "");
|
|
192
|
+
if (httpStem !== reason) {
|
|
193
|
+
return Object.hasOwn(UPLOAD_FAILURE_CLASSES, httpStem) ? httpStem : reason;
|
|
194
|
+
}
|
|
195
|
+
// Same contract for the `:detail` qualifier the withheld-by-attribution label
|
|
196
|
+
// carries (BLI-3272): the colon is only a separator when what precedes it is
|
|
197
|
+
// already a classified reason, so nothing an unlisted label happens to
|
|
198
|
+
// contain can smuggle it into a class.
|
|
199
|
+
const separator = reason.indexOf(":");
|
|
200
|
+
if (separator > 0) {
|
|
201
|
+
const qualifierStem = reason.slice(0, separator);
|
|
202
|
+
if (Object.hasOwn(UPLOAD_FAILURE_CLASSES, qualifierStem)) {
|
|
203
|
+
return qualifierStem;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return reason;
|
|
144
207
|
}
|
|
145
208
|
/** Classify a persisted upload reason. Unlisted reasons stay `unknown`. */
|
|
146
209
|
export function classifyUploadFailure(reason) {
|