Skip to content

Commit 85c23b2

Browse files
Merge pull request #6074 from colophon-group/fix-crawler/error-review-private-identifiers
Redact private identifiers from error review evidence
2 parents 8761ada + 9f9c10b commit 85c23b2

6 files changed

Lines changed: 192 additions & 13 deletions

File tree

apps/crawler/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.13.193
1+
0.13.194

apps/crawler/tests/test_error_review_bundle.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,42 @@
1313
SPEC.loader.exec_module(bundle)
1414

1515

16+
def test_redact_removes_private_identifiers_and_quoted_credentials():
17+
raw_resource_id = "a" * 64
18+
text = bundle._redact(
19+
"""
20+
endpoint=192.0.2.4 peer=[2001:db8::1]:8108
21+
posting=123e4567-e89b-12d3-a456-426614174000
22+
image=sha256:{raw_resource_id}
23+
TOKEN=plain-secret
24+
"api_key": "json-secret"
25+
"Authorization": "Bearer bearer-secret"
26+
""".replace("{raw_resource_id}", raw_resource_id).strip()
27+
)
28+
29+
for private_value in (
30+
"192.0.2.4",
31+
"2001:db8::1",
32+
"123e4567-e89b-12d3-a456-426614174000",
33+
raw_resource_id,
34+
"plain-secret",
35+
"json-secret",
36+
"bearer-secret",
37+
):
38+
assert private_value not in text
39+
assert text.count("<redacted-host-address>") == 2
40+
assert text.count("<redacted-resource-id>") == 2
41+
assert text.count("<redacted>") == 3
42+
43+
redacted_json = bundle._redact(
44+
'{"api_key": "json-secret", "Authorization": "Bearer bearer-secret"}'
45+
)
46+
assert json.loads(redacted_json) == {
47+
"api_key": "<redacted>",
48+
"Authorization": "Bearer <redacted>",
49+
}
50+
51+
1652
def test_parse_cgroup_key_values_ignores_malformed_rows():
1753
assert bundle._parse_cgroup_key_values("low 0\nhigh 2\nmalformed\noom nope\noom_kill 3\n") == {
1854
"low": 0,

apps/crawler/tests/test_maintenance_provenance.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def test_graceful_sigterm_exit_is_not_reported_as_forced_termination():
111111
assert pause["forced_termination"] is False
112112

113113

114-
def test_expected_marker_sigterm_brackets_window_without_becoming_a_failed_oneoff():
114+
def test_expected_marker_termination_brackets_window_without_becoming_a_failed_oneoff():
115115
provenance_contract = {
116116
"operation": "crawler-deploy",
117117
"issue": 3409,
@@ -151,7 +151,7 @@ def test_expected_marker_sigterm_brackets_window_without_becoming_a_failed_oneof
151151
"container_generation": "marker-generation",
152152
"compose_service": "maintenance-window",
153153
"compose_oneoff": "True",
154-
"event_exit_code": "143",
154+
"event_exit_code": "137",
155155
"maintenance_provenance": provenance_contract,
156156
},
157157
]
@@ -165,6 +165,105 @@ def test_expected_marker_sigterm_brackets_window_without_becoming_a_failed_oneof
165165
assert window["service_pauses"][0]["downtime_seconds"] == 50.0
166166

167167

168+
def test_transient_unmonitored_service_does_not_stretch_a_maintenance_window():
169+
provenance_contract = {
170+
"operation": "crawler-deploy",
171+
"issue": 3409,
172+
"revision": "d" * 40,
173+
"budget_seconds": 1800,
174+
}
175+
events = [
176+
{
177+
"source": "docker_event",
178+
"event_at": "2026-07-23T04:00:00+00:00",
179+
"action": "die",
180+
"container_generation": "old-init-generation",
181+
"compose_service": "murmur-shim-runtime-init",
182+
"compose_oneoff": "False",
183+
"event_exit_code": "0",
184+
},
185+
{
186+
"source": "docker_event",
187+
"event_at": "2026-07-23T05:00:00+00:00",
188+
"action": "start",
189+
"container_generation": "marker-generation",
190+
"compose_service": "maintenance-window",
191+
"compose_oneoff": "True",
192+
"maintenance_provenance": provenance_contract,
193+
},
194+
{
195+
"source": "docker_event",
196+
"event_at": "2026-07-23T05:01:00+00:00",
197+
"action": "die",
198+
"container_generation": "marker-generation",
199+
"compose_service": "maintenance-window",
200+
"compose_oneoff": "True",
201+
"event_exit_code": "137",
202+
"maintenance_provenance": provenance_contract,
203+
},
204+
{
205+
"source": "docker_event",
206+
"event_at": "2026-07-23T05:01:01+00:00",
207+
"action": "start",
208+
"container_generation": "new-init-generation",
209+
"compose_service": "murmur-shim-runtime-init",
210+
"compose_oneoff": "False",
211+
},
212+
{
213+
"source": "docker_event",
214+
"event_at": "2026-07-23T05:01:05+00:00",
215+
"action": "die",
216+
"container_generation": "new-init-generation",
217+
"compose_service": "murmur-shim-runtime-init",
218+
"compose_oneoff": "False",
219+
"event_exit_code": "0",
220+
},
221+
]
222+
223+
summary = provenance.correlate_events(events)
224+
225+
assert summary["unattributed_service_pauses"] == []
226+
window = summary["maintenance_windows"][0]
227+
assert window["duration_seconds"] == 60.0
228+
assert window["service_pauses"] == []
229+
assert window["status"] == "completed"
230+
231+
232+
def test_marker_oom_remains_actionable():
233+
provenance_contract = {
234+
"operation": "crawler-deploy",
235+
"issue": 3409,
236+
"revision": "d" * 40,
237+
"budget_seconds": 1800,
238+
}
239+
events = [
240+
{
241+
"source": "docker_event",
242+
"event_at": "2026-07-23T05:00:00+00:00",
243+
"action": "start",
244+
"container_generation": "marker-generation",
245+
"compose_service": "maintenance-window",
246+
"compose_oneoff": "True",
247+
"maintenance_provenance": provenance_contract,
248+
},
249+
{
250+
"source": "docker_event",
251+
"event_at": "2026-07-23T05:00:10+00:00",
252+
"action": "oom",
253+
"container_generation": "marker-generation",
254+
"compose_service": "maintenance-window",
255+
"compose_oneoff": "True",
256+
"state": {"oom_killed": True},
257+
"maintenance_provenance": provenance_contract,
258+
},
259+
]
260+
261+
window = provenance.correlate_events(events)["maintenance_windows"][0]
262+
263+
assert window["status"] == "actionable"
264+
assert window["actionable_reasons"] == ["oom"]
265+
266+
168267
def test_missing_service_restoration_remains_actionable():
169268
events = [
170269
event

docs/14-error-review-routine.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ survive Docker's volatile event buffer and container recreation. The root
6060
collector allowlists the journal again and one-way transforms raw IDs from
6161
legacy schema rows before the bundle becomes readable by `codex-runner`.
6262

63+
Every file written to the runner bundle passes the same final redaction
64+
boundary. In addition to credential shapes, it removes IPv4/IPv6 host
65+
addresses, UUID-shaped domain/resource identifiers, and raw 64-hex
66+
Docker/image identifiers. The 16-hex `container_generation` remains available
67+
for same-generation comparisons without exposing the underlying resource ID.
68+
6369
Production maintenance attribution is deterministic rather than inferred from
6470
container names. Repository-owned one-offs and the
6571
`/usr/local/sbin/jobseek-maintenance` wrapper attach an all-or-nothing
@@ -73,15 +79,21 @@ classifying instability:
7379

7480
- A single validated maintenance window owns only service pauses that overlap
7581
it or its bounded two-minute correlation edge.
82+
- Service-pause correlation is limited to the eight monitored long-running
83+
crawler services: Redis, three HTTP workers, the browser worker, exporter,
84+
drain, and Alloy. Transient Compose init services cannot stretch a crawler
85+
maintenance window.
7686
- Missing, partial, invalid, or conflicting provenance remains unattributed;
7787
never infer authorization from a name or an adjacent unlabelled one-off.
7888
- Authorized maintenance is reported as a maintenance outcome, with its
7989
tracking issue, revision, downtime, termination mode, and restoration
8090
health, rather than as spontaneous worker instability.
8191
- OOM/native exits, forced termination, nonzero maintenance one-offs, budget
8292
overruns, and failed restoration remain actionable even in an authorized
83-
window. Update the maintenance issue or create a deduplicated operational
84-
follow-up instead of reopening an unrelated instability issue.
93+
window. The wrapper-owned marker's expected termination is not a failed
94+
maintenance one-off; marker OOM still is. Update the maintenance issue or
95+
create a deduplicated operational follow-up instead of reopening an
96+
unrelated instability issue.
8597

8698
Compatibility fallback:
8799
[`.claude/commands/jobseek-error-review.md`](../.claude/commands/jobseek-error-review.md).

scripts/codex-error-review-bundle.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from __future__ import annotations
1010

1111
import argparse
12+
import ipaddress
1213
import json
1314
import os
1415
import re
@@ -47,17 +48,17 @@
4748
REDACTIONS: tuple[tuple[re.Pattern[str], str], ...] = (
4849
(
4950
re.compile(
50-
r"(?i)\b(authorization|proxy-authorization)\s*[:=]\s*"
51-
r"(bearer|basic)\s+[A-Za-z0-9._~+/\-]+=*"
51+
r"""(?i)(\b(?:authorization|proxy-authorization)["']?\s*[:=]\s*"""
52+
r"""["']?(?:bearer|basic)\s+)[A-Za-z0-9._~+/\-]+=*"""
5253
),
53-
r"\1: <redacted>",
54+
r"\1<redacted>",
5455
),
5556
(
5657
re.compile(
57-
r"(?i)\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API[_-]?KEY|PRIVATE[_-]?KEY)"
58-
r"[A-Z0-9_]*)\s*[:=]\s*([^\s,;\"']+)"
58+
r"""(?i)(\b[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API[_-]?KEY|PRIVATE[_-]?KEY)"""
59+
r"""[A-Z0-9_]*["']?\s*[:=]\s*["']?)(?!<redacted>)[^\s,;"']+"""
5960
),
60-
r"\1=<redacted>",
61+
r"\1<redacted>",
6162
),
6263
(
6364
re.compile(r"(?i)\b(bearer)\s+[A-Za-z0-9._~+/\-]+=*"),
@@ -75,11 +76,30 @@
7576
"-----BEGIN PRIVATE KEY-----<redacted>-----END PRIVATE KEY-----",
7677
),
7778
)
79+
IPV4_RE = re.compile(r"(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])")
80+
IPV6_CANDIDATE_RE = re.compile(
81+
r"(?<![0-9A-Fa-f:])(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
82+
r"(?![0-9A-Fa-f:])"
83+
)
84+
UUID_RE = re.compile(r"(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b")
85+
RAW_RESOURCE_ID_RE = re.compile(r"(?i)(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])")
86+
87+
88+
def _redact_ipv6(match: re.Match[str]) -> str:
89+
try:
90+
address = ipaddress.ip_address(match.group(0))
91+
except ValueError:
92+
return match.group(0)
93+
return "<redacted-host-address>" if address.version == 6 else match.group(0)
7894

7995

8096
def _redact(text: str) -> str:
8197
for pattern, replacement in REDACTIONS:
8298
text = pattern.sub(replacement, text)
99+
text = IPV4_RE.sub("<redacted-host-address>", text)
100+
text = IPV6_CANDIDATE_RE.sub(_redact_ipv6, text)
101+
text = UUID_RE.sub("<redacted-resource-id>", text)
102+
text = RAW_RESOURCE_ID_RE.sub("<redacted-resource-id>", text)
83103
return text
84104

85105

scripts/jobseek_maintenance_provenance.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030

3131
CORRELATION_PADDING_SECONDS = 120
3232
MERGE_GAP_SECONDS = 180
33+
MONITORED_SERVICES = frozenset(
34+
{
35+
"redis",
36+
"worker-1",
37+
"worker-2",
38+
"worker-3",
39+
"browser-1",
40+
"exporter",
41+
"drain",
42+
"alloy",
43+
}
44+
)
3345

3446
LIFECYCLE_FIELDS = frozenset(
3547
{
@@ -336,7 +348,7 @@ def _service_pauses(
336348
by_service: dict[str, list[tuple[datetime, dict[str, Any]]]] = {}
337349
for event_at, event in events:
338350
service = str(event.get("compose_service", ""))
339-
if not service or _is_oneoff(event) or service == "maintenance-window":
351+
if service not in MONITORED_SERVICES or _is_oneoff(event):
340352
continue
341353
action = str(event.get("action", ""))
342354
if action not in {"kill", "stop", "die", "oom", "start", "restart"}:
@@ -438,7 +450,7 @@ def _public_window(window: dict[str, Any]) -> dict[str, object]:
438450
for interval in sorted(window["oneoffs"], key=lambda item: item["start"]):
439451
code = interval.get("exit_code")
440452
is_marker = interval["compose_service"] == "maintenance-window"
441-
if code not in (None, 0) and not (is_marker and code == 143):
453+
if code not in (None, 0) and not is_marker:
442454
reasons.add("oneoff_nonzero_exit")
443455
if interval.get("oom"):
444456
reasons.add("oom")

0 commit comments

Comments
 (0)