Conversation
A Claude Desktop extension writes the literal `${user_config.<field>}` into
the engine environment for every field the user has not filled in. The value
is non-empty, so every presence check downstream read it as a real
credential: doctor reported the source healthy, preflight returned ready, and
the backend sent the placeholder upstream and surfaced the vendor's auth
error instead of falling back.
Reject a whole-value `${user_config.*}` placeholder once, at the end of
get_config(), so the legacy ScrapeCreators spelling, the multi-key rotation,
and the OpenAI auth fields assembled above are all covered by one sweep. The
process environment is cleared for the same keys, since doctor's GitHub
record, the GitHub backend token, and bird_x's subprocess environment read
the variable directly. Rejected names are published on the config so the
diagnostics can report the templated state instead of counting it absent.
The match is anchored to the whole trimmed value and narrowed to the
extension namespace, so a real credential containing `$` or braces and
shell-default syntax such as `${VAR:-default}` are untouched.
Fixes mvanhorn#1081
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…g them absent The boundary sweep in get_config() already makes a rejected placeholder read as unset, which fixes the flags but not the explanation: the user sees "credentials present: none" on a setup that looks complete to them. Name the rejected keys where the two diagnostics already speak. preflight appends them to action_items, so the object reports action_needed instead of ready; doctor carries them on the setup block, since the text renderer receives the assembled report and never the config, and prints them on the setup line as counted-unset. Both surfaces stay unchanged when no placeholder was rejected. Fixes mvanhorn#1081 Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…tics Both diagnostics re-derived `sorted(config.get(_TEMPLATE_CONFIG_KEYS) or [])` independently. Expose it as `env.templated_config_keys()` so the record key has one reader, in the same spirit as `include_sources` and `is_setup_complete`. The preflight action item now formats its list with the module's existing `_format_names` instead of a second hand-built join. Also drops an inert `list()` copy in the doctor renderer, and covers the sweep's export ordering: the YT knob export loop runs before the sweep and passes the key through on `is not None`, so a sweep placed before that loop would leave the placeholder in the environment. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Greptile SummaryThis PR treats whole-value
Confidence Score: 5/5The PR appears safe to merge; both previous findings are resolved in the current code and no actionable new defect remains. The fallback credential fix remains present, and the subsequent ScrapeCreators rotation now ensures a restored comma-separated fallback is reduced to one usable key before reaching a backend. No blocking or independently actionable new failure was established.
|
| Filename | Overview |
|---|---|
| skills/last30days/scripts/lib/env.py | Rejects whole-value extension templates, restores lower-priority values, reapplies ScrapeCreators rotation, and synchronizes derived OpenAI auth state. |
| skills/last30days/scripts/lib/doctor.py | Reports unresolved template names without exposing credential values. |
| skills/last30days/scripts/lib/permission_preflight.py | Marks unresolved templates as actionable configuration problems. |
| tests/test_env_unsubstituted_template.py | Covers matching boundaries, environment cleanup, fallback resolution, key rotation, auth-state consistency, and downstream provider behavior. |
| tests/test_source_outcomes.py | Makes the partial-outcome regression test independent of the wall clock. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Load environment and lower-priority sources] --> B[Build merged config]
B --> C{Whole-value user_config template?}
C -- No --> D[Keep resolved value]
C -- Yes --> E[Remove placeholder from process environment]
E --> F[Resolve lower-priority value or declared default]
F --> G[Reapply ScrapeCreators key rotation]
G --> H{Usable value remains?}
H -- Yes --> I[Expose configured credential]
H -- No --> J[Record key as unsubstituted and unset]
J --> K[Doctor and permission preflight report action]
Reviews (4): Last reviewed commit: "fix(env): reapply the key rotation to a ..." | Re-trigger Greptile
…tting `test_pipeline_records_both_mode_semantic_leg_failure_as_partial` let the pipeline derive its window from the wall clock while its fixture item is dated 2026-08-10. Once the calendar moved 30 days past that date the item was filtered out, the source held zero items, and the recorded state stayed ERROR instead of being downgraded to PARTIAL - so the check failed without any code changing. Pin both ends of the window with `as_of_date` and `lookback_days`, matching the pinned-window convention already used elsewhere in the suite, and assert the derived range so the test fails loudly if either pin is later dropped. No production change: the freshness filter is behaving correctly. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Two follow-ups from re-reading this branch's own change.
The placeholder pattern used `[^{}]*` for the field name, which also matched
shell-default syntax in the extension namespace - `${user_config.x:-default}`,
`${user_config.}`, `${user_config.x y}` were all treated as unexpanded
placeholders and blanked, even though the adjacent comment claimed that form was
out of scope. Restrict the field name to the identifier charset the manifest
emits, which is what the comment already described.
The sweep comment also claimed every consumer agrees the credential is unset.
The sweep is bounded by the keys get_config registers, so that is not true for a
credential read straight from the environment under a name it does not register.
Say so, rather than documenting coverage the code does not have.
No behavior change for a real placeholder or a real credential.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
A host placeholder occupies the highest-priority source, so it shadowed any credential the user already had in .env, Keychain, or pass. The sweep then replaced it with '' without consulting those sources, discarding a valid credential: the run reported and behaved as though nothing was configured. Rejection now means "absent" rather than "empty". The placeholder is cleared from the process environment and the key is re-resolved from the lower-priority sources exactly as it would be had the host never written it. A lower-priority value that is itself a placeholder is not treated as a credential. The record the diagnostics read now lists only the keys left genuinely unset, so a key that resolved is not reported as unconfigured - which also stops the diagnostics nagging about a setup that works. A templated OPENAI_API_KEY now resets the derived OPENAI_AUTH_STATUS/SOURCE instead of leaving them claiming a healthy key. Reported by Greptile as P1 on mvanhorn#1126, and independently by the first review round. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The ScrapeCreators multi-key rotation runs before the placeholder sweep, so a comma-separated key list restored from a lower-priority source was assigned whole. The backend then received "k1,k2" as a single credential and failed authentication, even though valid fallback keys were configured. Extract the rotation and reapply it after the sweep, so a restored list is rotated to one key like any other. A second pass over an already-rotated value is a no-op. If the rotation picks an element that is itself a placeholder, it is rejected rather than handed to a backend. Reported by Greptile as P1 on mvanhorn#1126. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
|
Speaking as Matt's firstmate: Captain OK via Firstmate (FM-L30-0912C): merging this PR as corrective with green CI. |
Summary
A Claude Desktop extension writes the literal string
${user_config.<field>}into the engine environment for every field the user has not filled in. The value is non-empty, so every presence check downstream counted it as a real credential:doctorreported the source healthy,permission_preflightreturnedreadywithsafe: true, and the backend sent the placeholder upstream and surfaced the vendor's401/400/422instead of falling back. A whole-value${user_config.*}placeholder is now rejected once at the config boundary, so every consumer agrees the credential is unset, and both diagnostics name it as unset rather than reading it as "nothing configured".Rejection means absent, not empty: the placeholder is cleared from the process environment and the key is re-resolved from the lower-priority sources exactly as it would be had the host never written it. A real
.env, Keychain, orpasscredential the placeholder was shadowing is therefore used rather than discarded.This branch also repairs a pre-existing test failure that was blocking CI.
tests/test_source_outcomes.py::test_pipeline_records_both_mode_semantic_leg_failure_as_partiallet the pipeline derive its freshness window from the wall clock while its fixture item is dated2026-08-10; once the calendar moved 30 days past that date the item was filtered out and the recorded state stayederrorinstead ofpartial. The failure reproduces on untouchedmain, so the default branch is currently red for this test — the repair is here so this PR's CI can go green, but a maintainer may prefer it to land independently.Testing
uv run pytestFull suite: 4690 passed, 6 skipped, 0 failed. Coverage
89.89%against the84%floor inpyproject.toml, which is unmodified.New coverage:
tests/test_env_unsubstituted_template.py(the boundary predicate, the sweep, the environment clearance, the shell-default exclusions, and the three fallback behaviours below), plus additions totests/test_doctor.pyandtests/test_permission_preflight.py. Each new case was observed failing against the commit before its fix.Validated end to end with the issue's own repro — templated credentials in the process environment:
Before the change the same run reported both keys as present and healthy.
The test repair was verified clock-independent by asserting the derived window (
2026-07-21 … 2026-08-20) rather than only the outcome. A suite-wide scan for the same rot — every window-deriving call site with an unpinned window, out-of-window fixtures, and an assertion that items survived — reported the one site before the fix and none after.Changelog
changelog.d/1081.fixed.mdskip-changeloglabel)Agent disclosure
AI review
Three review passes in total: two internal rounds (correctness, security, testing, adversarial) and one external review (Greptile) that ran on each pushed head.
The internal rounds produced findings recorded below. The external review raised two further issues, both valid and both fixed in this branch:
.env, Keychain, orpasscredential the placeholder was shadowing was lost. Rejection now re-resolves; a lower-priority value that is itself a placeholder is not treated as a credential. This was also the first internal round's top finding, so two independent reviews converged on it.k1,k2as one credential. The rotation is now extracted and reapplied after the sweep.Earlier in the branch, a comment claimed the extension namespace excluded shell-default syntax while the pattern matched it; the pattern was tightened and the claim made true.
Security
The change mutates the process environment (
os.environ.pop) inside the config loader. Reviewed and judged safe: a value can only be popped when the resolved config value is a whole-value placeholder, which requires the environment's own value to be that placeholder or falsy — so a real credential can never be removed.Fixture-recording redaction is not weakened. The sweep clears only placeholder strings, and the recording session also snapshots
os.environat start, so no real secret loses its redaction.The rejected key names (never values) are rendered into diagnostics; they come from the config's own key set, so no secret can leak through them.
Notes
The fix is scoped to the placeholder namespace the issue names (
${user_config.*}) and to whole-value matches. A future host that emits a placeholder with surrounding text would read as configured again.The sweep covers the keys
get_configregisters. A credential read straight from the environment under a name it does not register —LAST30DAYS_API_KEY, or a bareSCRAPE_CREATORS_API_KEYspelling left behind after the canonical key resolved — keeps its placeholder. The code comment states this bound; widening the sweep is left to review.Relationship to this change
Related issues
Fixes #1081
Session-settled decisions carried from planning: reject the template once at the config boundary rather than at each presence check (user-directed, over patching only
doctor._setup_block).Unapplied review findings
Reviewed by
correctness,security,testing,adversarial, and Greptile. Every bullet below sitsat confidence anchor 50 — below the bar for unattended application — so none were applied. They are
surfaced here for the reviewer to fix in this branch, dismiss, or carry past merge.
Round 1
P2 —skills/last30days/scripts/lib/env.py— Template is blanked, not unset; lower-priority credentials lostFixed — the sweep now re-resolves from the lower-priority sources. Also raised independently by
Greptile as P1.
P2 —
skills/last30days/scripts/lib/env.py— Hosted bearerLAST30DAYS_API_KEYescapes the sweepThe sweep scans only registered config keys, but
LAST30DAYS_API_KEYis deliberately not one(see
tests/test_env_doc_contract.pyDOC_ONLY_KEYS) and is read straight from the environment byhosted.py, then sent asAuthorization: Bearer <key>. Not reachable via today'smcp/manifest.json.Suggested fix: scan
os.environfor whole-value placeholders rather than only config keys.P3 —skills/last30days/scripts/lib/env.py—OPENAI_AUTH_STATUS/SOURCEleft stale after the key is emptiedFixed — a rejected
OPENAI_API_KEYnow resets the pair tomissing/none.P3 —
tests/test_doctor.py— Doctor "not listed as present" assertion can never failThe test injects only the rejected-names record, with no credential value, so
keys_presentisdeterministically
Falseand the negative assertion cannot fail for any input. Suggested fix:drive it from a real
env.get_config()with the templated env var set.P3 —
skills/last30days/scripts/lib/env.py— Emptied config contradicts absent-env for the one empty-means-disable keyFor
LAST30DAYS_YT_PLAYER_CLIENT,''is a documented disable while absent-from-env means theandroiddefault, so config and environment can disagree for that key. Suggested fix: exempt keyswhose unset state is an empty string.
P3 —skills/last30days/scripts/lib/env.py—os.environ.popmakes the rejection non-idempotent across a secondget_config()Addressed in substance — the record now lists only keys left genuinely unset, so a second load
that finds no placeholder no longer claims a stale templated state. Still worth deciding whether the
record should survive a reload.
Round 2
P2 —
skills/last30days/scripts/lib/env.py— The branch ships a global mutation whose replacement semantics are still undecidedRaised as a merge-strategy call, not a code defect. The sweep rewrites config values and clears
process-environment entries for every invocation. Suggested resolution: land the read-only half (the
predicate, the record, and the two diagnostics) separately from the mutation, or decide the
replacement semantics first and gate the mutation.
P3 —
skills/last30days/scripts/lib/env.py— Process-environment clearance is name-boundedA
SCRAPE_CREATORS_API_KEYspelling left behind after the canonical key resolved still holds itsplaceholder in the environment, and so does any unregistered name. The code comment now states this
bound; widening the sweep (or clearing known aliases) remains open.
P3 —
tests/test_source_outcomes.py— Therange_fromassertion does not guard its pinlookback_days=30equalspipeline.run's own default, so deleting that keyword leavesrange_fromunchanged and the test still passes; only therange_toassertion is load-bearing.Suggested fix: state that in the test, or pin a non-default width.
P3 —
tests/test_doctor.py— The new doctor assertion proves render plumbing, not the sweep→record couplingIt hand-builds
{env.TEMPLATE_CONFIG_KEYS: [...]}, so a mismatch between the key the sweep writesand the key the renderer reads would not be caught. Suggested fix: drive it through
env.get_config().P3 —
tests/test_env_unsubstituted_template.py— No test asserts no environment name still holds a placeholder afterget_config()The suite checks the specific keys it sets. Suggested fix: assert over
os.environfor anyremaining whole-value
${user_config.*}placeholder among the extension-wired names.P3 — coverage — No end-to-end test that a rejected credential makes a backend fall back
The changelog's central claim is tested indirectly (config emptied, one provider flag false).
Suggested fix: assert
env.get_reddit_source(config) is Noneafter a templatedSCRAPECREATORS_API_KEY, or that a templated Google key removes the provider fromdiagnose().Review runs:
cr-1081-1789061901(internal round 1),cr-1126-fix2-1789068256(internal round 2), plus Greptile on each pushed head.