They exist because of an incident. A session was destroyed three times before
anything was recording, and the third time the log was running: bsky.social
refused the refresh in its own words, under a client_id fingerprint with no
redirect URI, and jacquard deleted the session on the spot. atgc logs oauth --incident reproduces the whole of it.
The invariant is that a credential appears only as a fingerprint. That is a
type — Fp — everywhere it can be, and one audited function where it cannot,
which is an argv.
The log record shape is a public interface, deliberately. atgc logs <name> --json prints each line as it is on disk, so the envelope in
logging/file.rs and the variants and fields of the three Event
enums are the --json schema and carry docs/output.md's stability
rules: a new field is a feat, a rename or a removal is a breaking
!. Said on the types as well as in the docs, since the person about
to rename a field is reading the enum and not the page
Three defects in the reader, found by audit rather than by a report.
--incident ignored every filter on the command line. The filters
were built and then never applied on that path, so the analysis ran
over both generations — months of history — while the verdict it
printed advised narrowing with --since, which changed nothing. Every
conclusion an incident view draws is about a population : which
invocations overlapped, whether more than one login is in the window,
how often a refresh token was seen. --lines deliberately still does
not apply, since a verdict that moved with how many rows fit on a
screen would be worse than one drawn from too much.
One invalid byte discarded a whole generation. The read was
read_to_string, which is all-or-nothing on encoding, so the writer
killed mid-write that read_all's doc promises to survive — cut
inside a multi-byte character — turned every well-formed line behind it
into "nothing has been recorded yet". Bytes and a lossy decode now
leave the damage in the one line it belongs to, and live_len is taken
off the bytes, since it is the file offset --follow resumes from and
a zero there reprints the file.
--since 99999999999999999d multiplied before it range-checked.
n * seconds on a value parse::<i64> accepts happily: a panic in a
build with overflow checks and, in one without, a wrapped negative
delta that subtracts to an instant in the future — so the window
excludes everything and the command reports "no events matched" over a
log that has them. It is a checked_mul into the same Usage refusal
every other unreadable spec gets. This reaches --since and --until
on all three logs subcommands and on atgc search, which is the same
blast radius as the byte-split bug above it in this list
Append-only OAuth event log at ~/.config/atgc/oauth.jsonl, mode 0600 —
typed events for every token request (with the client_id actually
sent), refusal (with the OAuth error code and a scrubbed body), store
read/write, session get/upsert/delete, prune and logout, stamped with a
per-invocation id. Written as one bounded write(2) under O_APPEND so
concurrent invocations cannot tear a line. Secrets appear only as
SHA-256 prefixes
auth log — read oauth.jsonl back. Grouped by invocation by default,
--interleave for strict time order across processes, -f to follow
(through a rotation, not into a deleted inode). Filters by invocation,
event kind (token_*), account DID and time; --failures for the
short version; --json passes the raw lines through so the jq
workflow still composes. --incident does the comparison the next item
asks for. Deserializes into the writer's own event enum, so a new event
variant is a compile error here rather than a silently unrendered line
logs oauth — the OAuth log reader, moved out of auth log and given
a command group of its own, because the OAuth log is about to have
siblings and a second reader hanging off auth would have named the
wrong thing twice. The reader module moved with it, authlog.rs to
logs/oauth.rs
auth log removed outright. It survived one release as a hidden alias
that printed the new spelling, and that was the wrong trade: its whole
audience is people already reading their own machine's output, who can
see logs oauth in atgc logs --help, and the alias cost a variant, a
wrapper struct and a note on stderr to save them a word.
This entry used to defend the bare atgc login alias on the opposite
reasoning — that it is the first thing a new user types. That alias has
since been removed too, on a rule this note did not weigh: atgc does
not lift subcommands to the top level, whoever the audience is
logs pds — every write atgc sends to a PDS, in pds.jsonl. One line
leaving and one coming back, with the collection, record key, the
swapRecord/swapCommit precondition and the CID the record landed at; an
applyWrites lists its ops so a stack reconcile is legible one op at a
time. Observed at the transport like the OAuth log, so no call site can
forget to log and none can be added without being logged. Record
content is never written — only a fingerprint of it
The reader is generic over its event type (logs/render.rs), so every
command is one rendering rather than several that resemble each other,
and the writers share logging/file.rs: one envelope, one size cap, one
rotation, one invocation id across all the files
logs git — every git subprocess atgc starts, in git.jsonl. One line
leaving and one coming back, with the argv, the -C directory, the
exit status, git's stderr on a failure, and — for an explicit list of
ref-moving subcommands — where HEAD stood before and after. That list
is the design: probing HEAD costs a subprocess, and asking either side
of every rev-parse would double what atgc spawns to record nothing.
Every git process now starts from one Run builder in
clients/git/run.rs, so patch.rs's stdin runner and review.rs's
are covered by construction; cmd/report.rs's git --version was the
last one outside it and now calls run::version()
Credentials never reach git.jsonl: a remote URL's whole userinfo, a
-c key=value value and a git config value all become an
eight-character fingerprint, the last by an allowlist (user.name,
user.email) so an unknown key is redacted rather than trusted. Stdin
is a length and the environment is variable names only. An argv is the
one field in any of these logs that cannot be typed Fp, so the
redaction is one function with one test rather than a type
--since no longer panics on a non-ASCII value. render.rs took the
duration's unit with split_at(len - 1), a byte index, so --since 2д
landed off a char boundary and aborted in core — exit 101 where the
ASCII path had always exited 2 with the sentence naming the spellings
that do work. It reached every logs subcommand on both --since and
--until, and search through the same shared parser. The unit is the
last char now, so an unreadable value is a usage error whatever
alphabet it is written in. The truncated verbatim duplicate of the
function's own doc comment is gone with it, and render.rs has its
first tests — 1,262 lines carrying none, which is not unrelated to how
this survived
logs pds loses a column whenever colour is on. Three of the four
action values were painted and the fourth was a bare string, and the
{:<8} ran after the paint, so on a tty the padding counted SGR bytes
and applied none — update rows and the rest disagreed by two columns.
Correct under NO_COLOR and in a pipe, which is why the tests passed.
Fixed with term::column::pad_to along with every other fixed column;
the two logs oauth sites that were right only because short_inv
happens to return exactly eight characters now go through pad_start,
so they are right by construction rather than by luck
A token endpoint's error body reaches the terminal unscrubbed. The rule
is stated in logging/oauth.rs and applied to oauth.jsonl alone:
auth.rs dumps {e:#?} to stderr under --debug on the same value
the next line passes through scrub_text on its way to the log, and
two bail! sites interpolate {e} into text a person sees with no
--debug at all. jacquard's RequestError::HttpStatusWithBody embeds
the response body in Display and OAuthError::Request is
#[error(transparent)], so it arrives whole. Making dump_err itself
scrub makes the guarantee structural rather than per-caller, which is
what Fp already does for the log
One oauth.jsonl event is written unscrubbed: AuthorizeFailed.error
is clipped but not passed through scrub_text, where both its siblings
are. Narrow — a public client's PAR body holds no long-lived credential
— but it is a hole in the one invariant that file exists to keep, and
the Fp type cannot cover a plain String field
scrub_text under-matches, and its doc claims the opposite. It redacts
quoted values only: traced against access_token=abc123, the = and
the alphabetic run are skipped, the scan halts on a digit that is not a
quote, and the value survives. Form-encoded shapes reach Debug
through RequestError's url field. Blank an unquoted value to the
next &, whitespace, , or } — and fix the sentence that says it
over-matches rather than under-matches
--debug logs one raw oauth-state nonce, where every other site
fingerprints it through redact_key
--debug / ATGC_DEBUG=1 — HTTP traffic, token claims, full error bodies