Opt-in Trace Record Module

Repository implementation note for gh-forgejo-shim, generated June 19, 2026 at 14:09.

Summary

Added the core trace-recording module and focused tests for opt-in JSONL diagnostics. The module is not wired into CLI or routing behavior by this change, but it exposes the small API that routing can call later.

Changes Made

Context

The main integration work is expected to happen elsewhere. This task only introduced the core trace module and tests, keeping the routing and CLI files untouched by this workstream.

The worktree already contained unrelated in-progress edits in routing and other untracked files. Those were left intact. The trace module does expose compatibility helpers used by that pending routing work, but this turn did not modify those files.

Important Implementation Details

Activation: FJ_SHIM_TRACE is treated as the JSONL output path. Empty, 0, false, no, and off disable tracing.
Body capture: FJ_SHIM_TRACE_BODY must be truthy before stdout or stderr excerpts are recorded. Sizes are recorded without body opt-in.
Record shape: records include timestamp, cwd, argv, route, host, repo, duration in milliseconds, exit code, and stdout/stderr byte summaries.
Routing API: routing can use TraceRecorder, build_record, capture_streams, append_trace, tracing_enabled, and suppress_stdout_body.

Relevant Diff Snippets

Focused excerpts from the new files show the public surface, safe append behavior, and tests that pin the security-sensitive cases.

diff --git a/src/gh_forgejo_shim/trace.py b/src/gh_forgejo_shim/trace.py
new file mode 100644
--- /dev/null
+++ b/src/gh_forgejo_shim/trace.py
@@ -0,0 +1,16 @@
+TRACE_ENV = "FJ_SHIM_TRACE"
+TRACE_BODY_ENV = "FJ_SHIM_TRACE_BODY"
+DEFAULT_MAX_EXCERPT_CHARS = 4096
+REDACTED = "<redacted>"
+AUTH_TOKEN_REDACTED = "[redacted: gh auth token output]"
+
+@dataclass(frozen=True)
+class TraceRecorder:
+    path: Path | None = None
+    include_body: bool = False
+    clock: Callable[[], datetime] = field(default_factory=lambda: _utc_now)
+    max_excerpt_chars: int = DEFAULT_MAX_EXCERPT_CHARS
@@ -0,0 +70,20 @@
+def tracing_enabled(env: Mapping[str, str] | None = None) -> bool:
+    values = env if env is not None else os.environ
+    value = values.get(TRACE_ENV, "").strip()
+    return bool(value) and value.lower() not in _FALSE_VALUES
+
+def suppress_stdout_body(argv: Sequence[str]) -> bool:
+    return _is_auth_token_command(argv)
+
+def append_jsonl(path: Path, record: Mapping[str, object]) -> None:
+    path.parent.mkdir(parents=True, exist_ok=True)
+    line = json.dumps(record, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
+    data = line.encode("utf-8")
+    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
+    try:
+        while data:
+            written = os.write(fd, data)
+            data = data[written:]
+    finally:
+        os.close(fd)

diff --git a/tests/test_trace.py b/tests/test_trace.py
new file mode 100644
--- /dev/null
+++ b/tests/test_trace.py
@@ -0,0 +20,21 @@
+def test_noop_when_trace_env_is_unset(self) -> None:
+    recorder = TraceRecorder.from_env({}, clock=fixed_now)
+    emitted = recorder.emit(
+        argv=["repo", "view"],
+        route=TraceRoute("delegate", "disabled"),
+        duration_ms=1,
+        exit_code=0,
+        stdout="ok",
+        stderr="",
+    )
+    self.assertFalse(emitted)
@@ -0,0 +120,18 @@
+def test_auth_token_stdout_is_suppressed_even_when_body_enabled(self) -> None:
+    token = "plain-forgejo-token-without-a-known-prefix"
+    recorder = TraceRecorder.from_env(
+        {"FJ_SHIM_TRACE": str(path), "FJ_SHIM_TRACE_BODY": "true"},
+        clock=fixed_now,
+    )
+    recorder.emit(argv=["auth", "token"], route=TraceRoute("forgejo"), stdout=f"{token}\n")
+    raw = path.read_text(encoding="utf-8")
+    record = json.loads(raw)
+    self.assertNotIn(token, raw)
+    self.assertEqual(record["stdout"]["excerpt"], AUTH_TOKEN_REDACTED)

Expected Impact for End-Users

No user-visible CLI behavior changes until the routing layer calls this module. Once integrated, users can opt into local JSONL trace records without logging command bodies by default, and can opt into bounded body excerpts when they need deeper diagnostics.

Validation

Issues, Limitations, and Mitigations

Follow-up Work