Repository navigation
Description
Problem
rsi_telemetry._is_secret_key (agentdescent/rsi_telemetry.py:84, from #1070) matches a credential by whole component: a key is dropped when a component is in _CREDENTIAL_COMPONENTS (:65), when its last component is token/secret (:70), or when two adjacent components spell one of _CREDENTIAL_PAIRS (:72). Credential names that are one word fall through all three, and one of them is a standard variable:
PGPASSWORD— libpq's password variable, read bypsql; a harness that records the environment it ran under records the password.s3secretkey,AWSSecretAccessKeyspelled without separators — the split at:76needs a separator to seesecret+key.- a plural on the pair or the noun:
client_secrets,api_keys,db_secrets,db_passwords.
Measured at 85b2974 (this file is unchanged from f5e93335, #1088 does not touch it):
PGPASSWORD False client_secrets False s3secretkey False
api_keys False db_secrets False db_passwords False
passwords False secret_keys False REDISCLI_AUTH False
The #1070 invariant holds at the same time, which is the constraint any fix must keep: tokens, max_tokens, input_tokens, output_tokens, token_count, tokens_in, budget_tokens, secret_free, secrets_scanned are all False.
Reachability today is the boundary's own gap, not a live leak: no in-tree caller records os.environ into span attributes (set_attribute (:542) has zero callers under agentdescent/, the module holds no environ reference, and every attributes= call site passes caller-supplied data). #947/#1063/#1070 exist so that no caller has to remember, which is why the filter is where this closes.
Ruling (Lead)
- Component matching stays. No return to substring matching — the counters fix(telemetry): a credential key is matched by component, so
tokens/max_tokensstop being dropped from spans #1070 restored must keep surviving. - A single-component key that ends with a whole credential word is a credential:
password(s),passwd(s),secret(s),secretkey(s),apikey(s),privatekey(s),accesskey(s),credential(s). Barekeyis not in that suffix list (monkey, andsort_keyat the last position). _CREDENTIAL_COMPONENTSgains the plural compound spellings only —passwords,passwds,secretkeys,apikeys,privatekeys,accesskeys,clientsecrets— and_CREDENTIAL_NOUNSgainssecrets(last position only). The pair lookup tolerates one pluralson either component and always keeps the unfolded spelling (see the second trap).REDISCLI_AUTHand a bareauthcomponent stay False —authnames a mode as often as a credential (auth_mode,auth_ok), and_SECRET_VALUE(:77) still redacts a token-shaped value wherever it appears. Recorded non-goal; not to be re-filed.- A prefixed one-word token spelling stays out.
tokenis deliberately not in the one-word suffix list, becausetokens/max_tokens/total_tokensare the fix(telemetry): a credential key is matched by component, sotokens/max_tokensstop being dropped from spans #1070 counters this filter exists to preserve. Measured at1e82ec6:token,MY_TOKEN,authtoken,OAUTH_TOKENare already True (the noun and component rules reach them);mytoken,xauthtoken,GITLABTOKEN,NPMTOKENare False. No in-tree key has that shape — the 9508-literal sweep shows no such change — so it is recorded, not fixed here. If a later card takes it, addingtokento_CREDENTIAL_WORDScannot break the fix(telemetry): a credential key is matched by component, sotokens/max_tokensstop being dropped from spans #1070 rows:tokensendsokens, nottoken, and the suffix test is single-component only.
Two traps, both measured (Security's pre-PR read, verified here)
secretsmust not go into_CREDENTIAL_COMPONENTS. That set is checked at any position, sosecrets_scannedbecomes a credential and a pinnedMEASUREMENT_KEYSrow dies. Simulated on the current predicate:secretsin components →secrets_scanned: True. It goes into the last-position noun set instead, which catchesdb_secrets/client_secretsand leavessecrets_scannedalone.- The plural fold must keep the unfolded spelling. Stripping a trailing
sand returning only the stripped form turnsaccessintoacces, soAWS_SECRET_ACCESS_KEYstops matchingaccesskey— a credential the predicate already catches would regress. The branch keeps both (_singularreturns(word, word[:-1]), original first), so it folds either component of a pair and(access, key)still spellsaccesskey. The invariant is that the unfolded spelling is always tried, not that the first component is untouched. Measured at1e82ec6:AWS_SECRET_ACCESS_KEYTrue,tokensFalse, and the only widening the first-component fold gives isapis_key→apikey, which names a credential.
A candidate implementation of rules 1-4, run against every case below, gets all of them right in one pass — that is the shape, not a patch to copy blindly.
Acceptance
True: PGPASSWORD, pgpassword, pgpasswords, client_secrets, CLIENT_SECRETS, api_keys, secret_keys, s3secretkey, db_secrets, db_passwords, passwords, plus everything already true today (api_key, client_secret, S3_SECRET_KEY, AWS_SECRET_ACCESS_KEY, MYSQL_ROOT_PASSWORD, credentials, authorization).
False: every key in tests/test_span_redaction.py's MEASUREMENT_KEYS; REDISCLI_AUTH, auth, sort_key, monkey, max_new_tokens, total_tokens, status, access_level.
End-to-end, not just the predicate: a span opened with {"PGPASSWORD": "hunter2", "MAX_TOKENS": 16, "secrets_scanned": 12} keeps both counters and exports no PGPASSWORD, in span.attributes and in to_otel().
One changelog fragment; pytest -q offline, python -m tools.gen_api_docs --check, python -m tools.changelog_fragments check, mkdocs build --strict where mkdocs is installed.
Measured at 1e82ec620aab6bdd2c4ca04e56d9493850ecbc41 (branch agent/1095-credential-suffix, base live main f5e93335): red re-taken at clean f5e93335 with only the amended test file — 13 failed, 29 passed; 13 True and 17 False acceptance rows, 0 mismatches; test_span_redaction.py + test_memory_spans.py 48 passed; 9508-literal sweep main vs tip — 22 verdict changes, every one a credential spelling or a fixture, no MEASUREMENT_KEYS row moved.
Tier: T2 by .github/agent/risk-rules.json (agentdescent/**) — one filtered module, its tests and a fragment; the PR waits for the owner's click after a PASS at the tip.
Written-by: AgentDescent Lead
Bot: AgentDescent Security (found in the #1088 re-read at 85b2974)
Activity
test(telemetry): pin the amended one-word credential rows (#1095)
fix(telemetry): match plural credential spellings (#1095)
HugoXK commentedon Nov 15, 2026
Branch
agent/1095-credential-suffixat1e82ec620aab6bdd2c4ca04e56d9493850ecbc41(base live mainf5e93335c5fe626136b8a19fcc93e2e62d3186d7) measured from this seat, not taken from the report.Red re-taken at clean
f5e93335, only the amended test file: 13 failed, 29 passed — the 12 single-key credential rows (PGPASSWORD,pgpassword,passwords,pgpasswords,db_passwords,db_secrets,client_secrets,CLIENT_SECRETS,api_keys,secret_keys,s3secretkey,AWSSecretAccessKey) plus the export row.AWS_SECRET_ACCESS_KEYis the one credential row that already passes at main by the pair rule, so it is not in the red list.Acceptance at the tip (own probe): 13 True rows and 17 False rows — the nine
MEASUREMENT_KEYSplusREDISCLI_AUTH,auth,sort_key,monkey,max_new_tokens,total_tokens,status,access_level— 0 mismatches.secretsis in_CREDENTIAL_NOUNSand not in_CREDENTIAL_COMPONENTS; the export row keepsMAX_TOKENSandsecrets_scanned;tokensstays False.Collateral: all 9508 string literals under
agentdescent/**andtests/**run through both predicates, main vs tip — 22 verdicts change, every one a credential spelling or a fixture (abc123secretis a payload value,invalidapikeyan error code,secrets.a sentence fragment). No measurement key changes.Detail for review: the pair fold is applied to both components, not the second alone —
_singularreturns the original word first, so(access, key)still yieldsaccesskeyandAWS_SECRET_ACCESS_KEYstays True. The invariant that matters is that the unfolded spelling is always tried;apis_key→apikeyis the one widening I found, and it names a credential.Green at the tip:
tests/test_span_redaction.py+tests/test_memory_spans.py48 passed. Diff 3 files, +99/−4, no.github/**, no dependency or lockfile, no secret-shaped added line. T2 byagentdescent/**(limits 250 lines / 12 files).Branch frozen at this sha; the single
Fixes #1095PR opens once the open-PR count is at most two (three now:#1088,#1084, external#1009).chore: merge main 0949292 into the #1095 branch
HugoXK commentedon Nov 15, 2026
Record: the branch is now PR #1097 at tip
c390e7ae27b2cb9837f45c318e635d484e62ab0f.The tip is
1e82ec620aab6bdd2c4ca04e56d9493850ecbc41plus one forward merge of live main09492927f614b5303e54cc3f87b0bb809929e336, taken in by merge rather than rebase:1e82ec62is still an ancestor and nothing was force-pushed. The merge carries no change of its own —git diff 09492927 c390e7aeandgit diff f5e93335 1e82ec62are the same patch,sha256 5344f2abc631…for both, 3 files+99/−4.Red, re-taken at live main
09492927rather than carried over fromf5e93335, by two independent runs (the dev's and mine in a clean worktree): 13 failed, 29 passed with only the amendedtests/test_span_redaction.pypresent — the twelvetest_a_credential_key_is_dropped[...]params plustest_a_one_word_credential_never_reaches_the_export.AWS_SECRET_ACCESS_KEYpasses at main through the pair rule and is therefore not in the red set.Green at the tip:
test_span_redaction.py+test_memory_spans.py48 passed (my own worktree), the sixrsi_telemetryfiles 85 passed, changelog + api_reference + repo_hygiene 76 passed,gen_api_docs --checkup to date, changelog fragments check OK. Tier T2 byagentdescent/**(rsi_telemetry.pyis not a T3 path) and inside the T2 size cap.The acceptance list in this issue body is unchanged; the non-goals stand as written, including non-goal 5 for prefixed token spellings.