Skip to content

feat(officers): officer_ci_logs — read why your own PR's Tekton CI failed (read-only) - #390

Merged
ZacxDev merged 15 commits into
mainfrom
officer/ci-log-read
Nov 1, 2026
Merged

ZacxDev merged 15 commits into
mainfrom
officer/ci-log-read

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Problem

A shipping officer whose CI runs as Tekton PipelineRuns in a cluster sees only "red": the commit statuses carry no target_url, the instance has no cluster access and no toolchain. So it escalates, even when one error line in the failed step's log gives the cause.

What this adds

officer_ci_logs{pr} is a read-only tool. It is offered only to a shipping officer that has a new ci_logs block, and muster-server answers it:

"ci_logs": {"namespace": "example-ci", "pipeline": "example-pr-check",
            "service_account": "muster-ci-logs"}

(commit_label defaults to pipeline.jquad.rocks/pr.branch.commit, pr_param to PR_NUMBER, max_bytes to 16384, with a range of 2048–65536.)

  1. Ownership uses feat(officership): officer_mark_ready — an officer converts its own draft PR to ready #389's single own-PR rule, owner.headAndAuthor (the same as owner.ownPR minus the base check): the head is a <prefix>… branch of repo and the author is the officer App's bot. An identity read failure fails closed through failClosed, which Gates.ownPR also uses now. There is no second copy of the rule. A refusal names head_branch or author, and happens before any cluster read.
  2. Match: it lists PipelineRuns with tekton.dev/pipeline=<pipeline>,<commit_label>=<the head GitHub reports now>. It re-checks those labels on each result, then keeps only runs whose pr_param equals the PR number. The newest of those is used, and older retries are counted.
  3. Failures: TaskRuns with Succeeded=False. For each one it reads only the failed step-* containers (every step on a timeout, never a sidecar), each as the tail of the log.
  4. Excerpt: it keeps --- FAIL, FAIL …, ✘, N failed, ERROR, SQLSTATE, panic:, Error:, x_test.go:N: and file.go:N:M: lines, deduplicated with a repeat count, then the last 15 lines. ANSI codes are stripped, each line is capped at 400 bytes, and all excerpts together stay within max_bytes, cut on character boundaries.
  5. It never returns an empty answer. state is one of no_run, running, succeeded ("no failure found") or failed, and there is always a note.

Secret safety. A PEM block is withheld whole, every line from BEGIN through END (to the end of the log if END is missing). A bare line of 40+ base64 characters is withheld too. Any other line that looks like it carries a credential (GitHub/AWS/Slack/sk- tokens, JWTs, PEM blocks, Authorization/Bearer headers, password=/token: assignments), and any env-dump NAME=value line, is withheld whole. URL user:pass@ is masked. This is a pattern filter, not a guarantee; the byte cap limits what a miss can carry. Only failed step containers are read, never sidecars.

Cluster access goes through the in-cluster API. Each call, muster's own identity mints a TokenRequest for service_account (600 s, default audience), and a client is built fresh from the API server, the CA and that token alone. muster's own token, and its token file, never reach the reads. The token is minted only after the ownership gates pass, once per call; a failed mint is a ci_runs refusal. RBAC: list on pipelineruns and taskruns and get on pods/log in the CI namespace; for muster's SA, create on serviceaccounts/token limited to that SA by resourceNames. If the source can't be built (not in a cluster), it is logged and the tool answers "unavailable". It does not refuse the boot.

The kickoff tells the officer to call officer_ci_logs before escalating a red check. docs/officers.md documents the block, the match rule, the excerpt and the RBAC.

Update after round 0

commit what targeted tests at it
1121311e test: a PEM block is withheld whole, in 5 shapes (block at the end with BEGIN outside the tail, no END, mid-log, log read starting mid-block, encrypted key's headers) officership red (5/5 subtests)
2513774d fix: RedactLines keeps BEGIN…END state; also withholds bare base64 lines green
de181720 test: TokenRequest minting (SA name and namespace, 600 s, default audience, empty token is an error), reader config carries only the minted token, no open on a refused call, mint failure is a refusal, token_file is an unknown field build fails in all 3 packages
b73b9833 feat: ci_logs.service_account and a per-call TokenRequest replace token_file; the duplicate ci_config gate is dropped green: officership 25, officers 13, api 1

Mutation checks, each killed by a named test: removing the BEGIN…END state (killed by the "encrypted key's headers" case); removing the base64 rule (killed by "log read starts mid-block"); copying muster's token file into the reader config; a 3600 s token; accepting an empty token; minting before the ownership check. The pr_param filter stays. The ownership helper is unchanged until #389 merges, then this branch is rebased onto it.

Rebased onto #389 (382194c5)

The commits were replayed, and the shas changed:

commit what targeted tests at it
9df7f4c1 tests first build fails
84ec8b65 feature green
74a588ad PEM test officership red (TestCILogsWithholdsAWholePEMBlock)
248a2bb8 PEM fix green
a37fa51f TokenRequest tests build fails
b3a78138 TokenRequest feature green: officership 25, officers 13, api 1

At b3a78138, the full officership and officers packages pass, and so do the api officer, ship, mark-ready and merge tests. vet is clean.

Conflicts, both features kept:

  • gates.go: took main's as-is, and deleted my refuseForeignHead/refuseForeignAuthor. MergePR uses Gates.ownPR again. I added failClosed, the fail-closed wrapper that Gates.ownPR now delegates to.
  • officer_ship.go: the header lists the four tools and then officer_ci_logs as the fifth. isShipTool includes both ToolMarkReady and ToolCILogs.
  • kickoff.go: both constants. shipNote keeps the ToolMarkReady arg and appends ciLogsNote(o).
  • docs/officers.md: the tools-table row with officer_mark_ready, plus the officer_ci_logs row.

Ownership mutation against the shared helper:

  • Disabling the whole author check in owner.headAndAuthor turns these red: TestCILogsRefusesAPullRequestThatIsNotTheOfficersOwn/{a_human's,another_bot's}_pull_request, TestEveryMergeGateRefusesItsOwnFailure, TestMarkReadyRefusesEverythingButOwnDraftToReady and TestThePushGateUsesTheGatesOwnPRRule.
  • Disabling only the login half fails only the "another bot" case, as expected, since AuthorType != "Bot" still refuses the human.

The message of 84ec8b65 still mentions the retired refuseForeignHead/refuseForeignAuthor. Its code doesn't use them.

Tests (red-first)

  • e3870cb9 contains only the tests. At that commit, internal/officership, internal/officers and internal/api fail to build because the tool doesn't exist yet.
  • At ee723cfd (the implementation), all three packages pass: 17 / 13 / 1 passing cases, run with -run 'CILogs|ParsePipelineRun'. The rest of internal/officership also passes, including the merge-gate tests that cover the ownership refactor.

What's covered: refusal of another author's PR, another bot's PR, a human branch and a fork (with zero cluster reads); picking the current head's run over an old head, the same head under another PR, another pipeline and an older retry (the fake reader ignores the selector, and the selector is pinned as a literal); following the head after a push; FAIL + SQLSTATE lines read only from the failed step; the byte cap across tasks plus valid UTF-8 at every cut; clear no-failure answers; credential lines withheld (with a positive control); config defaults and refusals; MCP offer and dispatch.

Mutation check. Each guard was disabled on purpose and a named test went red:

guard disabled killed by
author ownership check …RefusesAPullRequestThatIsNotTheOfficersOwn/{a_human's,another_bot's}_pull_request
pr_param filter …PicksTheRunForTheCurrentHead (it picked run-other-pr)
label re-check …PicksTheRunForTheCurrentHead, …FollowsTheHeadNotTheNewestRun (it picked other-pipeline-run)
byte cap …RespectsTheByteCap (695539 bytes against a cap of 4096)
redaction …WithholdsCredentialLines
failed-step filter …ExcerptCarriesTheFailAndErrorLines (it read step-provision)

Per the repo convention, CI runs the full suite; locally I ran only the new tests plus the officership package.

Not in this PR

Deploying it needs a pin bump plus the ci_logs block in the deployment's MUSTER_OFFICERS, along with the RBAC. A build without this change refuses the unknown ci_logs field at boot.

🤖 Generated with Claude Code

@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 0 (requirements & deletion) at ee723cf — verdict: requirement questioned (ownership refactor overlaps #389) + deletion candidates; no open premises. Requirements 11 (unattributed 5). Correction to the report: Tekton CI does run on draft PRs in the target repo (measured 2026-11-01); only the hosted review skips drafts. Round-1 pointer: line-wise redaction lets a PEM block's base64 body through while the docs say the block is withheld whole.

D1 — the ownership refactor (refuseForeignHead/refuseForeignAuthor) duplicates #389's Gates.ownPR and textually conflicts with it in gates.go, officer_ship.go, kickoff.go, docs/officers.md; keep one helper and rebase the second-merged PR onto it
D2 — the pr_param (PR_NUMBER) run filter and its config knob: a run for the same head sha under another PR has the same code and explains the same failure, so it guards nothing material
D3 — the ci_config gate inside CILogsReader.Read repeats the dispatcher's own rt.ci / rt.ciErr checks, which always run first

ZacxDev and others added 6 commits November 1, 2026 01:09
Red-first tests for a read-only officer tool that reads why the officer's
OWN pull request's Tekton CI failed (fake GitHub + fake cluster reader):

- ownership: another author's / another bot's PR, a human branch and a
  fork's branch are refused by the author / head_branch gate before any
  cluster read;
- matching: the newest PipelineRun labelled with the PR's CURRENT head sha
  and carrying its PR_NUMBER wins over an older head's run, the same head
  under another PR, another pipeline's run and an older retry; the label
  selector is pinned as a literal; the answer follows the head after a push;
- excerpt: the failed step's container only (never a sidecar or a passed
  step), with the --- FAIL / FAIL / playwright lines and the SQLSTATE error;
  every excerpt together within max_bytes, valid UTF-8 at every cut;
- no failure (succeeded / running / failed with no failed task / no run):
  a state and a note, failed_tasks an explicit empty list;
- credential-looking lines withheld, URL userinfo masked (with a positive
  control);
- the ci_logs config block's defaults and refusals, the offer rule and the
  kickoff line; the MCP offer + dispatch through internal/api.

These do not compile at this commit: the tool is the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ailed

A shipping officer whose CI is Tekton in a cluster it cannot reach saw only
"red" (the commit statuses carry no target_url; the instance has no cluster
access and no toolchain) and escalated, though the cause was one error line
in the failed step's log.

officer_ci_logs{pr}, offered to a SHIPPING officer with a new `ci_logs`
block (namespace, pipeline, token_file required; commit_label, pr_param,
max_bytes defaulted), answered by muster-server:

- ownership: the same head-branch and author rule as officer_merge_pr,
  now one function each (refuseForeignHead / refuseForeignAuthor) used by
  both;
- match: PipelineRuns with tekton.dev/pipeline=<pipeline>,<commit_label>=
  <the PR's CURRENT head sha>, re-checked on the labels, filtered on
  pr_param == the PR number; newest wins, older retries counted. The
  defaults are what pipeline-trigger-operator sets
  (pipeline.jquad.rocks/pr.branch.commit, PR_NUMBER);
- the failed TaskRuns' failed step-<name> containers only (never sidecars),
  tail-read with a byte limit, reduced to FAIL / ✘ / ERROR / SQLSTATE /
  panic / file:line lines (deduplicated) + the last 15 lines, credential-
  looking and env-assignment lines withheld, URL userinfo masked, the whole
  answer capped at max_bytes and cut on character boundaries;
- never empty: no_run / running / succeeded ("no failure found") / failed,
  with a note.

Reads the in-cluster API with the token in token_file (a dedicated
namespace-scoped ServiceAccount; never muster's own). A source that cannot
be built logs and answers "unavailable" rather than refusing the boot. The
kickoff tells the officer to call it before escalating red CI; docs/
officers.md documents the block, the match rule, the excerpt and the RBAC.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Redact is line-wise: it withholds a PEM block's BEGIN line, but the base64
body lines pass and land in the excerpt's tail (docs/officers.md says the
block is withheld whole). Red at this commit in all five shapes: the block
at the end of the log (BEGIN outside the 15-line tail), a block with no END
(the step died mid-print), and a block mid-log followed by the failure.
Plus the log read starting mid-block (only the body and END in it) and an
encrypted key cut short, whose Proc-Type/DEK-Info headers are not base64.
A real 2048-bit RSA key, generated per run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RedactLines carries state across a CI log: from a -----BEGIN …----- line
through its -----END …----- line every line is withheld (headers such as
Proc-Type/DEK-Info included), and to the end of the log when END never
came. A line that is nothing but 40+ base64 characters is withheld too,
for a block whose BEGIN fell before the log read's window. Excerpt now
redacts the whole log with it before picking lines and the tail.

Mutation-checked separately: without the BEGIN…END state the "encrypted
key's headers" case goes red; without the base64-line rule the "log read
starts mid-block" case goes red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tests first for replacing `ci_logs.token_file` (a never-expiring token
Secret) with a TokenRequest per call:

- mintCIToken asks for the CONFIGURED ServiceAccount in muster's own
  namespace, 600 s, default audience, once per mint; an empty token is
  an error (fake clientset reactor);
- ciReaderConfig keeps the API server and CA and carries ONLY the minted
  token: no token file (client-go prefers it over the token), client
  cert, basic auth, exec/auth plugin or impersonation of muster's own;
- the reader is opened once per call and never on a refused call; a mint
  failure is a ci_runs refusal naming the read token;
- config: `service_account` required and a name; `token_file` is now an
  unknown field.

Do not compile at this commit: the change is the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces `ci_logs.token_file` (a never-expiring ServiceAccount token
Secret mounted into muster-server) with `ci_logs.service_account`: for
each call muster's own in-cluster identity creates a TokenRequest for that
ServiceAccount in muster's namespace (expirationSeconds 600, default
audience), and the reading client is built FRESH from the API server, the
CA and that token alone (ciReaderConfig) — muster's own token, and its
token file that client-go would prefer, never reach the reads. The token
is minted only after the ownership gates pass, once per call; a failed
mint is a ci_runs refusal.

Also drops the ci_config gate inside CILogsReader.Read (internal/api
builds the reader only for a configured officer and already refuses
otherwise), and the docs now give the RBAC exactly as called: list on
pipelineruns/taskruns and get on pods/log in the CI namespace, plus create
on serviceaccounts/token for that one ServiceAccount for muster's own SA.

Mutation-checked: keeping muster's token file in the reader config,
3600 s, accepting an empty token, and minting before ownership each turn
a named test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev force-pushed the officer/ci-log-read branch from b73b983 to b3a7813 Compare November 1, 2026 06:14
@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 1 (full checklist) at b3a7813: 🔴 1 · 🟡 1 · 🟢 3 — fixes in progress; a delta round 2 follows.

  • 🔴 R1 flake.nix:206: goVendorHash stale after the new k8s imports ("nix outputs" CI red).
  • 🟡 Y1 internal/officership/cilogs.go:330,334: the secret filter misses keywords after _ (_PASSWORD, _TOKEN, …), set -x prefixed env lines, \r-separated lines, lowercase names and empty URL usernames — measured verbatim leaks in error lines.
  • 🟢 G1 PipelineRun message not redacted · G2 tail drops the log end on long lines, Truncated misses some cuts · G3 TaskRuns not re-checked against the selector.

Security questions checked and held: ownership (shared #389 helper, fail-closed), scoping, token never logged/returned, muster's own credential never used for reads, memory bounded, PEM withheld whole, RBAC/admission in civitai/agent-cluster#78bf61b04 match the calls exactly.

ZacxDev and others added 5 commits November 1, 2026 01:24
The new imports (k8s.io/api/authentication/v1, k8s.io/client-go/dynamic,
client-go fakes) change the vendored module tree; CI's nix outputs job
reported got sha256-O96XsPL9AexSc9n9LA3vAVmyeSh/5s7AgwVWN1LJN7s=.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Round 1 on #390 measured these leaking verbatim through the excerpt: a
keyword after an underscore (AWS_SECRET_ACCESS_KEY=, API_TOKEN=,
OAUTH_CLIENT_SECRET:), JSON "DATABASE_PASSWORD":"…", set -x traces
(+ DB_PASSWORD=, ++ export STRIPE_SECRET=), an assignment after a lone \r
(progress line), lowercase npm_config__authToken=, and an empty-username URL
(postgres://:pass@), plus three env assignments with no credential
keyword (a set -x trace, a lowercase name, one after a lone \r) that only
the env-assignment rule can catch. Red at this commit in all twelve. Positive control:
ordinary failure lines that name a password/token/secret pass unredacted
(green before and after).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- secretRE: the credential keyword may sit anywhere in a key (no leading
  word boundary, so _PASSWORD/_SECRET/_TOKEN/_ACCESS_KEY match), any case,
  with an optional quote before and after the separator ("KEY":"value");
  _pat added; github_pat_ unanchored.
- envLineRE: a set -x trace prefix (+ , ++ export) may precede the
  assignment, and lowercase names count.
- Excerpt splits on a lone \r too (progress lines).
- urlUserRE: an empty username (scheme://:pass@) is masked.
- docs/officers.md lists exactly what is withheld and masked.

Mutation-checked, each with its own killing case: restoring the leading
\b (4 keyword-after-underscore cases), the ^\s* env anchor (the + trace),
upper-case-only env names (the lowercase one), \n-only splitting (the \r
one), and a required URL username (the empty-user URL).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…kRuns, redacts the run message

Round 1 nits on #390:
- G1: the PipelineRun's message (run_message, and the no-failed-task
  note) goes through RedactLines too; TaskRun messages as well.
- G2: PodLog no longer sends LimitBytes (which keeps the START of the
  tail): it asks for the last 20,000 lines and keeps the last 8 MiB of
  them (readTail), at a line boundary, reporting cut. Truncated now
  reflects every cut: the read dropping the log's start, the byte cap,
  and a clipped emitted line. Docs say so.
- G3: each listed TaskRun's tekton.dev/pipelineRun label is re-checked
  against the run, like pickRun does for PipelineRuns.

Tests for each; mutation-checked: dropping the TaskRun re-check, the run
message redaction, reading the head instead of the tail (LimitReader at
maxBytes), and the clip-cut marking each turn a named test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestCILogsRespectsTheByteCap ran Excerpt 200 times over a 5000-line log:
10.7 s locally, and in CI's coverage run internal/officership took 572 s
(main: 1.3 s), which pushed the concurrently running internal/api package
past go test's 10 min timeout (test (postgres) on 3bc4f03). The loop
only needs budgets landing inside multi-byte characters; 40 lines of the
same shape do that. Now 0.19 s; the package 0.26 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 1 fixes landed (b3a7813..fca4e09); full CI green on fca4e09 incl. nix outputs and test (postgres). A delta round 2 follows.

1. flake.nix goVendorHash updated for the new k8s imports; nix outputs builds
2. the excerpt filter withholds credential-word keys anywhere in the key (incl. after `_`), JSON "KEY":"value", set -x prefixed and lowercase env assignments, lines split at a lone \r, and masks empty-username URLs; ordinary FAIL/ERROR lines that only name a secret still pass
3. the PipelineRun message is redacted
4. the log tail keeps the END (last 20000 lines, last 8 MiB at a line boundary) and `truncated` reflects every cut
5. listed TaskRuns are re-checked against their pipelineRun label
6. the byte-cap test no longer runs a 5000-line excerpt 200 times (CI timeout)

ZacxDev and others added 4 commits November 1, 2026 01:34
Tests first for round 2 on #390:
- F1/F2 must-pass controls, all withheld by the round-1 filter (red here):
  file_path:, output_path:, tokenizer:, a Secret named by its k8s path,
  pwd: <path>, and three logfmt/slog lines (time=… level=ERROR msg=… err=…,
  level=error msg=…, exit_code=1), and compat=…/upass=… (a short word only
  counts as a whole key segment). The round-1 lowercase case becomes
  'export deploy_conn_url=…' (bare lowercase is a logfmt key now).
- Must withhold: GITHUB_PAT= (whole segment) and level=error
  password=hunter2… (both already withheld: invariant guards), and, red
  here, set -x trace lines (+ / ++), docker login -p, mysql -pX,
  --password X, --token X, {"auth":"<b64>"}, Cookie:, Set-Cookie:, a URL's
  sig= and signature=, DB_PASS:, credentials:, basic auth: <b64>.
- F6: redactText splits on a lone \r (red).
- F4/F5: readTail with one read between max and 2*max (the final trim
  decides), a window starting on a line boundary keeps its first line and is
  not emptied (red), a last line longer than max keeps its end, a UTF-8 cut,
  exactly maxLines lines without a trailing newline is cut.
- F5: each clipped-line check reached alone (green here: invariant guard,
  mutation-checked in the fix commit).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…shapes

Round 2 on #390:
- F1: keyRE requires the credential word to END the key; long words may
  follow any prefix, short ones (pat, pass, pw, auth, credentials) only a
  segment break or the key's start; a value that is a path (absolute, or a
  lowercase namespace/name) is not a secret. file_path:, output_path:,
  tokenizer:, compat=, Secret: default/my-secret and pwd: <path> now pass;
  GITHUB_PAT=, DB_PASS:, {"auth":…}, credentials: are withheld.
- F2: a line-start env assignment is UPPER-case, or any name after export;
  bare lowercase key=value (logfmt/slog) passes; a credential key inside it
  is still withheld.
- F3: whole set -x trace lines, Cookie/Set-Cookie, signed-URL sig=/
  signature=/token=, --password/--token/--secret/--api-key VALUE, login
  -p VALUE / mysql -pVALUE, basic auth: <b64>.
- F6: redactText splits on a lone \r like Excerpt (one splitLines).
- docs/officers.md lists exactly what is withheld and what passes, and
  says plainly what still leaks.

Mutation-checked, each killed by its named case (a mutant that does not
build is reported as such, not as survived): the key-end anchor
(tokenizer:), the short-word segment anchor (compat=/upass=), the path
exclusion (Secret: default/my-secret), lowercase env anywhere (the logfmt
controls), the trace rule, the \r split in redactText.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…TF-8

Round 2 F4/F5 on #390. readTail keeps one byte before the window and:
- drops to the first newline only when that byte is not a newline (the
  window may already start a line);
- never drops to a newline that is the window's last byte (one line longer
  than maxBytes keeps its end instead of coming back empty);
- starts on a UTF-8 character boundary;
- counts a last line without a trailing newline, so exactly maxLines lines
  with none is cut.
Mutation-checked against TestReadTailEdges (whose first case is one read
between max and 2*max, so the final trim decides): each of the four, and
keeping the head in the final trim, turns it red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under CI's go test -race -cover, internal/officership took 30 s on e5a4c9b
(TestCILogsRespectsTheByteCap 5.4 s locally under -race alone); 800 lines
(~48 kB) is still twelve times the 4 kB cap the test asserts. Now 2.9 s
under -race -cover locally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 2 (delta) at fca4e09: 🔴 0 · 🟡 3 · 🟢 3 — ladder ends. All fixed un-re-audited in 7dc9483..7513263 (tests first; 13 mutants killed); full CI green on 7513263.

1. credential keys must end the identifier; short words count only as a whole segment or the key start; path values are not secrets — file_path/output_path/tokenizer/Secret:<ns/name>/pwd lines pass
2. lowercase key=value at line start counts only after export, so logfmt/slog lines pass
3. set -x trace lines, login -p/-pVALUE, --password/--token VALUE, {"auth":…}, Cookie/Set-Cookie, sig=/signature=, DB_PASS:, credentials:, basic auth: are withheld
4. readTail keeps the byte before the window, never empties an over-cap last line, starts on a UTF-8 boundary, counts a last line without trailing newline
5. the tail test pins which end is kept; each clipped-line check has its own case
6. redactText shares the lone-\r line split

@ZacxDev
ZacxDev marked this pull request as ready for review November 1, 2026 15:39
@ZacxDev
ZacxDev merged commit 5e19b5f into main Nov 1, 2026
19 checks passed
@ZacxDev
ZacxDev deleted the officer/ci-log-read branch November 1, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant