Opt-in Trace Record Module
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
- Created
src/gh_forgejo_shim/trace.pywith env-gated tracing usingFJ_SHIM_TRACEandFJ_SHIM_TRACE_BODY. - Added JSONL append support with parent-directory creation, append mode, deterministic JSON key ordering, and restrictive file creation mode.
- Added redaction helpers for auth headers, token-like keys, known GitHub token prefixes, token environment variables, and URL credentials.
- Added body excerpt handling that is disabled by default and suppresses
gh auth tokenstdout even when body tracing is enabled. - Created
tests/test_trace.pycovering redaction, no-op behavior, JSONL writes, opt-in body excerpts, and auth token suppression.
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
FJ_SHIM_TRACE is treated as the JSONL output path. Empty, 0, false, no, and off disable tracing.FJ_SHIM_TRACE_BODY must be truthy before stdout or stderr excerpts are recorded. Sizes are recorded without body opt-in.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
python3 -m py_compile src/gh_forgejo_shim/trace.py tests/test_trace.pypassed.python3 -m unittest tests.test_tracepassed: 5 tests.python3 -m unittestpassed: 119 tests.
Issues, Limitations, and Mitigations
- Trace collection is not wired into
cli.pyorrouting.pyby this task. Mitigation: the module exposes the intended call surface for the integration agent. - Body excerpts are deliberately bounded and redacted, but arbitrary nonstandard secrets can still be hard to identify perfectly. Mitigation: body capture remains opt-in and
gh auth tokenstdout is fully suppressed. - Trace write failures return
falseinstead of interrupting command execution. Mitigation: tracing remains diagnostic and should not break normal shim use.
Follow-up Work
- Integrate
TraceRecorderor the functional helpers into routing once the main agent owns that layer. - Document the trace environment variables in user-facing configuration docs after integration is complete.