Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@
| **ticket-019** | [`README.md`](./ticket-019/README.md) | - | - | - | - | - |
| **ticket-020** | [`README.md`](./ticket-020/README.md) | - | - | - | - | - |
| **ticket-021** | [`README.md`](./ticket-021/README.md) | - | - | - | - | - |
| **ticket-022** | [`README.md`](./ticket-022/README.md) | - | - | - | - | - |
| **ticket-023** | [`README.md`](./ticket-023/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
18 changes: 18 additions & 0 deletions project/ticket-023/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Ticket 023: Diagnostic metadata parity and bounded process reads

- **ID**: ticket-023
- **Owner**: agent:codex-root
- **Status**: IN_PROGRESS
- **Workflow state**: PUBLICATION
- **Authorization**: SESSION_EXECUTION_AUTHORIZATION; user requested continued tests/fixes and previously authorized protected publication.

## Goal and scope

Fix four reproduced native provider metadata discrepancies in merged ticket-021 and bound process metadata reads in ticket-022. Source and tests only in the declared three paths; no IDE signals or restarts, no database recovery or protected CI edits.

## Acceptance criteria

- [x] AC-01: BOM, UTF16, unsupported JSON and first-line budget match Python.
- [x] AC-02: FIFO, links and oversized process metadata refuse promptly in both paths.
- [x] AC-03: Normal output, identity and privacy remain unchanged, with safe cached-worker fallback.
- [x] AC-04: Full tests including real X11 and governance pass; exact-head protected publication.
102 changes: 102 additions & 0 deletions project/ticket-023/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-023",
"summary": "Fix native diagnostic metadata parity and bounded proc reads",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "health"
},
"allowedPaths": [
"project/ticket-023/**",
"TODO.md",
"project/TICKETS.md",
"src/uncrash/diagnostics.py",
"src/uncrash/snapshot.rs",
"tests/test_diagnostics.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [
"ticket-021",
"ticket-022"
],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "2d82ec367845db10774eb7c1645e0fb8d67bea60",
"targetBranch": "main",
"outcome": "Repair reproduced provider metadata decoding/line-limit parity and prevent blocking or unbounded session process reads; publish through exact-head independent validation.",
"nonGoals": [
"No user IDE signals, restarts or session database repairs",
"No protected CI/profile/policy changes or dependency additions"
],
"complexity": "M",
"estimatedMinutes": 75,
"budgets": {
"maxImplementationFiles": 3,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Retain native acceleration for supported metadata, send unsupported JSON encodings/forms to Python, match first-line byte budget including newline, reject unsafe/nonregular metadata and bound all process reads in both paths.",
"components": [
{
"name": "native-diagnostics",
"paths": [
"src/uncrash/diagnostics.py",
"src/uncrash/snapshot.rs",
"tests/test_diagnostics.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Protected revert of this bounded remediation; retain previous installed CLI release."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest tests/test_diagnostics.py -q"
],
"evidence": "Provider BOM/UTF16/unsupported JSON uses correct Python fallback; exact line budget parity."
},
{
"criterion": "AC-02",
"commands": [
"python -m pytest tests/test_diagnostics.py -q"
],
"evidence": "Process metadata FIFO, symlink and oversize reads refuse promptly in worker and Python fallback."
},
{
"criterion": "AC-03",
"commands": [
"python -m pytest tests/test_diagnostics.py -q"
],
"evidence": "Normal provider/process output and privacy remain unchanged; no compiler on cold queries."
},
{
"criterion": "AC-04",
"commands": [
"python -m pytest -q",
"./project/governance-check.sh --base 2d82ec367845db10774eb7c1645e0fb8d67bea60",
"protected OneDev and independent Validator"
],
"evidence": "Full mandatory X11 and governance/product checks plus protected publication."
}
]
}
}
33 changes: 21 additions & 12 deletions src/uncrash/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@


def _stat(path):
fields = (path / 'stat').read_text().rsplit(')', 1)[1].split()
fields = _read_regular_bytes(path / 'stat', 65536).decode().rsplit(')', 1)[1].split()
return {'pid': int(path.name), 'ppid': int(fields[1]), 'start_ticks': fields[19],
'session_id': int(fields[3]), 'tty_nr': int(fields[4])}

Expand All @@ -43,6 +43,14 @@ def _read_regular_bytes(path, maximum):
return raw


def _read_regular_prefix(path, maximum):
fd = os.open(no_links(path), os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, 'rb') as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise ValueError('metadata is not a regular file')
return stream.read(maximum)


def _read_json(path, maximum=32 * 1024**2):
raw = _read_regular_bytes(path, maximum)
return json.loads(raw), raw
Expand Down Expand Up @@ -82,8 +90,8 @@ def session_hosts(proc_root=Path('/proc'), state_root=None):
return native_data
now = datetime.now(timezone.utc).isoformat()
try:
boot = (proc_root / 'sys/kernel/random/boot_id').read_text().strip()
except OSError:
boot = _read_regular_bytes(proc_root / 'sys/kernel/random/boot_id', 65536).decode().strip()
except (OSError, ValueError, RecoveryError):
boot = None
nodes = {}; rejected = Counter()
for path in proc_root.iterdir():
Expand All @@ -93,15 +101,14 @@ def session_hosts(proc_root=Path('/proc'), state_root=None):
if path.stat().st_uid != os.getuid():
continue
before = _stat(path)
name = (path / 'comm').read_text().strip()
name = _read_regular_bytes(path / 'comm', 65536).decode().strip()
exe = os.readlink(path / 'exe')
basename = Path(exe).name
provider = basename if basename in PROVIDERS else None
role = 'unknown'
# Only recognize bounded executable arguments; never retain prompt arguments.
if provider or basename in {'node', 'nodejs', 'java'}:
with (path / 'cmdline').open('rb') as stream:
args = stream.read(65536).split(b'\0')
args = _read_regular_prefix(path / 'cmdline', 65536).split(b'\0')
if basename in {'node', 'nodejs'} and len(args) > 1:
entry = args[1].decode(errors='replace')
if '/@anthropic-ai/claude-code/' in entry: provider = 'claude'
Expand All @@ -121,16 +128,15 @@ def session_hosts(proc_root=Path('/proc'), state_root=None):
if node['terminal'] and node['role'] == 'unknown': node['role'] = 'client'
node['pid_namespace'] = os.readlink(path / 'ns/pid')
node['inherited_markers'] = {}
with (path / 'environ').open('rb') as stream:
fields = stream.read(262144).split(b'\0')
fields = _read_regular_prefix(path / 'environ', 262144).split(b'\0')
for field in fields:
k, sep, v = field.partition(b'=')
if sep and k in {b'TERMINAL_EMULATOR', b'TERM_SESSION_ID'}:
node['inherited_markers'][k.decode()] = v[:256].decode(errors='replace')
if before != _stat(path):
rejected['identity_changed'] += 1; continue
nodes[before['pid']] = node
except (OSError, ValueError, IndexError):
except (OSError, ValueError, IndexError, RecoveryError):
rejected['unavailable'] += 1
rows = []
for pid in sorted(nodes):
Expand All @@ -148,7 +154,7 @@ def session_hosts(proc_root=Path('/proc'), state_root=None):
try:
current = _stat(proc_root / str(member['pid']))
if any(current[k] != member[k] for k in ('pid', 'ppid', 'start_ticks')): stable = False
except (OSError, ValueError, IndexError): stable = False
except (OSError, ValueError, IndexError, RecoveryError): stable = False
if not stable: host = None
rows.append({k: v for k, v in node.items() if k != 'host_app'} | {
'current_host': host or 'unknown', 'ancestry_stable': stable, 'ancestors': chain,
Expand Down Expand Up @@ -397,7 +403,10 @@ def provider_inventory(home, check_integrity=False, state_root=None):
files = list((home / '.codex/sessions').rglob('*.jsonl'))
for path in files:
try:
with no_links(path).open('rb') as stream:
fd = os.open(no_links(path), os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, 'rb') as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise ValueError('metadata is not a regular file')
raw = stream.readline(1024**2 + 1)
if len(raw) > 1024**2:
raise ValueError('oversized metadata')
Expand All @@ -411,7 +420,7 @@ def provider_inventory(home, check_integrity=False, state_root=None):
ids.append(sid)
label = meta.get('source')
labels[label if isinstance(label, str) else 'structured-source'] += 1
except (OSError, ValueError, KeyError, TypeError, AttributeError):
except (OSError, ValueError, KeyError, TypeError, AttributeError, RecoveryError):
invalid += 1
result['codex'] = {
'files': len(files),
Expand Down
Loading
Loading