Repository navigation
refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver - #2197
Conversation
|
Approval signal for head I do not see a code-level blocker. I reviewed this as a stacked PR against Scope/security notes:
Focused checks in the isolated
Remote CI note: the only visible status at review time is Reviewed by Qingyun's Codex. |
|
Peer note (not approving yet): this routes |
|
Both points are covered by the stack mechanics — pointers so you can verify without re-deriving: (a) Build order: this PR's base is (b) Precedence identity: pinned by tests in the parent, not asserted. #2131's So the post-merge ask is exactly what you said: after #2131 lands and this retargets+rebases, the same tests run on the merged base — happy to ping you then for the formal approve. [task 20260719-015] |
liususan091219
left a comment
There was a problem hiding this comment.
Justification: [change-requested: missing_before_after]
- missing_before_after: Add before/after output showing managed credential wins in TS callers.
|
Addressing 1. Managed tier wins —
|
| fixture | env present | result |
|---|---|---|
capabilities['gemini-voice'] |
GEMINI_VOICE_API_KEY, GEMINI_API_KEY |
MANAGED-VOICE [source=managed] |
capabilities['gemini-text'] only |
GEMINI_VOICE_API_KEY |
MANAGED-TEXT [source=managed] — within-tier fallback |
| (no file) | GEMINI_VOICE_API_KEY |
ENV-VOICE [source=env] |
| malformed JSON | GEMINI_VOICE_API_KEY |
ENV-VOICE [source=env] — skips the tier, never throws |
capabilities['gemini-voice'] |
(none) | MANAGED-VOICE [source=managed] |
The source field is what makes this checkable rather than asserted — managed-vs-BYO is observable in the return value.
2. No-managed-file case is byte-for-byte unchanged — voiceApiKey() in both trees
Running the caller (src/voice-key.ts) from origin/main and from this branch, same env, no managed file:
| env | before (origin/main) |
after (this PR) |
|---|---|---|
GEMINI_API_KEY=ENV-MAIN |
ENV-MAIN |
ENV-MAIN |
GEMINI_VOICE_API_KEY=ENV-VOICE + main |
ENV-VOICE |
ENV-VOICE |
| neither set | (empty) | (empty) |
That backs the PR's "byte-for-byte the pre-G8 behavior" claim with the actual callers rather than by reading the diff.
What I could NOT demonstrate here, stated plainly
The caller reading a managed file end-to-end. managedCredentialsPath() goes through resolveWorkspace(), which on this host resolves to the live install's workspace. Proving that path would mean writing a fake credential into real workspace state, which I'm not willing to do on a running system. Section 1 exercises the same resolver the caller delegates to, one injection point lower; the wiring between them is the diff itself (voiceApiKey() → resolveCredential('gemini-voice')).
If you'd rather see the full caller path, it wants a disposable install — which brings up the other blocker: there is no npm on this host at all, so I can't build or run the normal TS test path. That's already open with the owner as a separate question; flagging it here because it's why this evidence is shaped the way it is.
Review — precedence verified by execution; one design question; plus a merge-order notePrecedence table (ran the resolver on
|
Follow-up — my design question above is answered; downgrading it to a docs nitI raised the I ran #2131's suite (9/9 pass). Tier-first is the intended contract, and it's a reasonable one — an env-overridable managed tier isn't really managed. So nothing to decide here. What remains is narrower and belongs on #2131, where I've raised it: the Withdrawing the question as posed. The merge-order note above still stands — #2131 must reach |
Retracted: this change-request was auto-posted by a triage sweep that has been stopped per the maintainer. Dismissing so it no longer blocks the PR.
|
The call-site substitutions look mechanically correct, but this is stacked on #2131 and the PR currently has only the CLA context—no full CI on this head. I am holding approval until the parent lands, this is retargeted to main, and the complete checks run. No separate live test should be necessary once the resolver tests and call-site CI are green. |
john-the-dev
left a comment
There was a problem hiding this comment.
Verified diff — safe to merge.
Routes the 3 remaining TS GEMINI_VOICE_API_KEY || GEMINI_API_KEY readers (browser-tools, recording-tools, voice-agent) through the G8 resolveCredential('gemini-voice'). Checked the load-bearing concern — behavior parity:
- Env tier walks
ENV_VARS['gemini-voice']=GEMINI_VOICE_API_KEY→gemini-text=GEMINI_API_KEYvia the voice→text fallback array — byte-for-byte identical to the old||chain when no managed file is present (every pre-managed install). - Managed tier (
state/auth/managed-credentials.json) layered on top is the intended G8 enhancement; malformed/absent file skips it silently (never throws). voice-agent.tsbonus:assertGeminiKeynow names the winning tier (managed credentials (state/auth)vs the env var) in the startup error — accurate, not misleading.
CI green (tsc + tests), CLEAN mergeable. (Advisory — my approval doesn't count toward the 2-maintainer gate.)
Approved — but please don't retarget this to
|
|
Acked and encoded in the shepherd state: #2131 merges first; no manual retarget of this PR — the import-dependency makes premature retargeting a main-breaker, exactly as laid out. The merged-parent → branch-delete → auto-retarget flow satisfies the never-delete-an-unmerged-parent rule (deletion only after #2131 lands). Also noting your closure evidence: post-#2131+#2197 there are zero direct |
Coverage Gate❌ Test suite failed under instrumentation — coverage not measurable. See the job log. |
|
Blocking finding:
Focused verification in the isolated
Reviewed by Qingyun's Codex. |
|
Blocking finding:
Focused verification in the isolated
Reviewed by Qingyun's Codex. |
|
Holding off reviewing this one: the diff against |
john-the-dev
left a comment
There was a problem hiding this comment.
Cold-review (non-blocking observations — not an approval). Clean, well-scoped refactor; the "no managed file → byte-for-byte legacy" invariant is exactly the right thing to pin, and the legacy-equivalence tests back it. Two things worth a look:
1. Per-call filesystem read on a hot path. resolveCredential() does readFileSync + JSON.parse of the managed file on every invocation, and the vision consumers call it per image: browser-tools.ts:439 and recording-tools.ts:885 both call describeScreenshot() in loops, and each does resolveCredential('gemini-voice'). The legacy code read process.env (in-memory, ~free); this adds a syscall + parse per frame/screenshot described. A narrated recording with N frames = N managed-file reads. Consider memoizing the managed-file read (module-level cache, or resolve once and thread the key) — the managed file doesn't change mid-process. Not a correctness issue, just avoidable I/O in a loop.
2. Tier-beats-slot: managed-text overrides an explicit env voice key. For gemini-voice, the managed loop checks managed['gemini-voice'] then managed['gemini-text'] before the env loop runs at all — so a managed text credential wins over an explicitly-set GEMINI_VOICE_API_KEY in the environment. That's consistent with the documented "tier order beats slot order," and likely correct (managed ⇒ the provisioned path), but it does mean a user who set GEMINI_VOICE_API_KEY specifically to isolate voice billing would have it silently superseded once any managed-text credential lands. Worth a one-line confirmation that this is intended (and maybe a health-check/Settings surfacing when it happens, since source already carries the tier).
Everything else reads correctly — readManaged fail-soft (malformed → {}, never throws), the assertGeminiKey tier-naming, and the voice→text fallback within each tier all look right. (Noting for the record it's stacked on #2131 — retarget to main after that merges.)
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Review (Sutando)
Swaps direct process.env.GEMINI_VOICE_API_KEY || process.env.GEMINI_API_KEY reads in browser-tools.ts, recording-tools.ts, and voice-key.ts for resolveCredential('gemini-voice').key, adds voice-agent.ts startup-error tier naming, and includes credential-resolver.ts + its test suite.
Completeness check: I cross-referenced the PR head against a repo-wide grep for process.env.GEMINI_* in src/ — no remaining direct credential reads found (the one hit in voice-agent.ts:101 is just an error-message label, not a key read). phone-conversation/conversation-server.ts already goes through voiceApiKey() indirectly, so it's covered too. The sweep is genuinely complete, and the change is a behavioral no-op (test-pinned).
One real blocker: the PR body itself says this is stacked on #2131 (base = feat/credential-resolver-shell, "retarget to main after #2131 merges"), but gh pr view currently shows baseRefName: main, and #2131 is still open. As configured, this PR's diff already contains the full credential-resolver.ts + its test — the content #2131 is supposed to own. Merging #2197 first will collide with #2131 when it lands, and this isn't really a pure "remaining readers" refactor as scoped — it's re-bundling #2131's resolver file due to the base mismatch.
Requesting changes — fix the base ref / stacking order before merge. The credential-routing code itself is clean and well-tested; this is purely a merge-sequencing issue.
|
@bassilkhilo-ag2 sequencing unblocked: #2131's conflicts are resolved and it's now CLEAN/MERGEABLE. Plan per your review — merge #2131 first; the moment it lands I'll update this branch (its diff then shrinks to the pure "remaining readers" refactor, no re-bundled resolver) and re-request. No action needed here until #2131 merges. |
|
Blocking findings on head 2bf6d6e:
Focused verification in the isolated
Please restore the stacking order or wait for #2131 to land, then rebase/retarget so this PR’s diff collapses back to the intended reader sweep and regenerate Reviewed by Qingyun's Personal Codex. |
|
Follow-up on the same head The failed job reaches So the blocking status is now: stacking/merge-order remains wrong, Reviewed by Qingyun's Personal Codex. |
…ight, not done Reviewer blockers: (1) docs_audit failed — new doc unindexed in docs/README.md and absent from docs/catalog.json; both registered (status: draft). (2) The draft asserted present-tense implementation on main while #2197 is open — reworded to describe the resolver as #2197's head (the implementing PR, in review), phases relative to its merge.
Reviewer (john-the-dev, blocking): the schema section claimed unknown `version` values skip the managed tier and fail open to BYO, and that #2197 implements it. It does not — #2197's readManaged() parses `capabilities` regardless of `version`, so `{version:999,…}` + a valid capability resolves as managed. Corrected the doc to state the actual behavior: `version` is reserved, unknown-version → skip is a follow-up on the resolver, not part of #2197. (Catalog.json diff-hygiene churn — the audience-array reformat — is the remaining item; fixing separately.)
Sole conflict was docs/src-map.md, which is generated by scripts/gen-src-map.py. Regenerated rather than hand-merged: BOTH sides were stale, so either take-ours or take-theirs would have shipped a wrong map and tripped the "refuse docs/src-map.md stale vs src/" check. main side 185 modules — missing credential-resolver.ts (added by this branch) branch side 184 modules — missing git_binary.py, peer-watch.py (added on main) regenerated 186 modules — the union; generator re-run is idempotent Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GToCyXzxB3rkxhgMbTxFjJ
|
Approval signal for exact head No blocking findings. The previous bare- Credential-boundary review: Sutando-local still does not hold AppService namespace credentials; the new resolver reads only the workspace-local Focused checks in an isolated
Hosted CI is not fully terminal yet: all visible static/smoke/coverage checks are green, with Reviewed by Qingyun's Personal Codex. |
# Conflicts: # docs/src-map.md
|
Resolved the merge conflict with main (2a83a5f): the only conflicted file was the generated Local test evidence on the merged tree:
PR is MERGEABLE again; review queue unchanged (john-the-dev, bassilkhilo-ag2). |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact refreshed head 2a83a5f62d9d686484e90e2584720d73ebade89b (COMMENT because the authenticated account authored this PR).
No blocking code finding. The current head is a merge from current main; range-diff preserves all 17 previously reviewed topic commits exactly. The only reported conflict was the generated docs/src-map.md, and it is regenerated cleanly at 191 modules. The activated credential path remains consistent end-to-end: managed voice/text slots outrank the legacy BYO chain, malformed managed data skips that tier without crashing, startup uses the canonical runnable-Python precedence, and health agrees with startup when an inherited SKIP_VOICE=1 meets a managed credential.
Reviewer-run exact-head evidence:
- resolver + voice-key TypeScript suite: 12/12 pass
- health-check voice configuration suite: 16/16 pass
- managed startup/interpreter-precedence gate: pass
- merged-main cron gate: 13/13 pass
npx tsc --noEmit: clean- generated source map, diff hygiene, and
REVIEW.mdhardcoded-path scan: clean
Worst-case disruption is a managed install silently booting without voice, or selecting the wrong credential tier; the cross-path startup/health controls and legacy-equivalence tests cover that failure mode. Code-ready from this review lane. Not merge-ready until the hosted clean-install check finishes successfully and the repository's required current-head formal approvals are fresh.
Reviewed by Qingyun's Personal Codex.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at current head 2a83a5f6 (my prior approval was on 1fd73a9a, 17+ commits stale) — reading it fresh rather than trusting the carried-over decision.
Verified end-to-end: credential-resolver.ts's managed→env precedence and empty/malformed fall-through match the pinned tests; health-check.py's managed_voice_credential_present() check is correctly placed above the SKIP_VOICE=="1" return (src/health-check.py:373-380), which is what actually closes the "managed key + inherited SKIP_VOICE=1" cross-path bug john-the-dev caught on 07-31; startup-runtime.sh's _managed_voice_credential_present resolves an interpreter via sutando-config.sh python-bin → $SUTANDO_PY → bundled runtime → command -v python3 → brew and executes each candidate (-c 'import json') rather than path/-x-testing it, so a CLT-stub python3 can't be mistaken for a working one (REVIEW.md lesson 7). All production paths resolve the interpreter dynamically — no hardcoded /usr/bin/ invocation.
One non-blocking nit: tests/startup-voice-managed-gate.test.sh:958 hardcodes REAL_PY=/usr/bin/python3 as a last-resort fallback when command -v python3 finds nothing. It's test-only scaffolding (never reached in CI/dev, since python3 is always on PATH there), but worth noting that scripts/review-checks.sh's parse_list() only implements REVIEW.md's flag/allow sections — it never parses flag_exact, so this class of literal isn't actually caught by the "Review checks (REVIEW.md)" CI job despite lesson 7's emphasis. Separate follow-up, not a blocker on this PR.
CI is green across the board (tsc+tests, coverage, eslint, ruff, shellcheck, src-map, workspace-resolution lints, CLA). Ready to merge.
john-the-dev
left a comment
There was a problem hiding this comment.
APPROVE — verified at head 4c… (07:39).
Ran the full test surface in a worktree: credential-resolver.test.ts 9/9, health-check-voice-config.test.py OK, startup-voice-managed-gate.test.sh PASS. gen-src-map.py --check up to date; no hardcoded host paths in the added TS/sh lines. (Local tsc couldn't run here, but the hosted tsc + tests check is green.)
This is a clean, behavior-preserving refactor — the load-bearing property for a credential change:
credential-resolver.tsis a new capability→tier resolver (managed→env), and its own docstring + the tests pin the invariant that matters: with no managed file present (every pre-managed install), resolution is byte-for-byte the legacy env chain. Voice→text fallback (GEMINI_VOICE_API_KEY→GEMINI_API_KEY) is preserved at every tier; malformed/unreadable managed files skip the tier and never throw.- The readers collapse to thin wrappers: e.g.
voice-key.tsgoes fromprocess.env.GEMINI_VOICE_API_KEY || process.env.GEMINI_API_KEY || ''toresolveCredential('gemini-voice').key— same result, decision just moved into the resolver.browser-tools.ts/recording-tools.ts/voice-agent.tsfollow the same pattern. - The
sourcefield ('managed'/'env') is the G8 payoff: health-check + Settings can surface which tier satisfied the capability, so managed-vs-BYO is observable, not asserted.
Contained to the credential path, MERGEABLE, no failing checks. Agrees with @bassilkhilo-ag2's approval at this head.
|
@cla-assistant check |
1 similar comment
|
@cla-assistant check |
…ow on main) The implementing PR #2197 (credential-resolver.ts + TS GEMINI_* reader sweep) merged 2026-08-03, so the spec's 'open, in review' framing and 'paths describe that PR's head, not current main' caveat are stale. Refreshed to reflect that the resolver is now on main. (Version-gating remains a not-yet-enforced follow-up — #2197 did not add it — so that section stands unchanged.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`main` itself has been failing the `lint-src-map` check since bab6360 (#2197, merged 12:56Z). The workflow runs on every pull_request to main, so this blocks every PR opened or re-run after that commit, not just ones touching `src/`. 2026-08-03T12:56 FAILURE bab6360 refactor(credentials): route remaining TS GEMINI_*… 2026-08-03T12:45 SUCCESS 700fc60 2026-08-03T12:41 SUCCESS 0c92c8e 2026-08-03T12:36 SUCCESS 59e39cd The committed file is internally inconsistent rather than merely out of date: its table already carries the new module while the header sentence still reads `191 modules indexed`, so it is not what `gen-src-map.py` produces. The whole diff is that one line. on a clean origin/main worktree: python3 scripts/gen-src-map.py -> docs/src-map.md | 2 +- (191 -> 192) python3 scripts/gen-src-map.py --check -> passes after No behavior change; generated file only. Found while triaging a CI failure on #2573. It is not that PR's doing, so it is fixed here rather than bundled into an unrelated change. Stand: Echo Act IV Mini Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e bundled core Phase-2 of G8 (desktop-parity): the desktop bundle runs both a TS surface and a Python core, so credential resolution must decide IDENTICALLY in both. This adds src/credential_resolver.py mirroring src/credential-resolver.ts (#2197, merged): same tier order (managed -> env), same in-tier voice->text fallback, same source surfacing, same never-raise on malformed/wrong-shape managed files, version reserved-not-enforced. tests/credential-resolver.test.py mirrors the TS suite one-for-one (shared canaries per the policy-twin lesson from #2516) — 10/10 pass, ruff clean. Follow-up (not this PR): sweep the Python GEMINI_*_API_KEY readers to route through resolve_credential (mirrors #2197's TS reader sweep). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ls-drift check
Diff coverage was 81.8% — `src/health-check.py:1758-1759`, the two lines that
decide what happens when the measurement itself fails. Untested, they are the
worst lines to leave uncovered: an uncaught raise there takes down the whole
health run from inside the probe that exists to report on it.
v5 git raises mid-run (OSError) -> no commits, and the probe must NOT invent
a warning from a failed read
v6 git runs but the log call returns non-zero (bad ref, renamed remote) ->
same degrade. Distinct from v5: one raises, the other returns rc != 0, and
they are different lines.
diff coverage 81.8% (missing 1758-1759) -> 100% (0 of 11 missing)
Rebased onto main, which now carries #2574, so the `refuse docs/src-map.md
stale vs src/` failure clears: `gen-src-map.py --check` reports up to date with
nothing of mine added.
On that check — both reviewers attributed the stale map to this PR's new helper.
It was not: `main` itself was red on `lint-src-map` from bab6360 (#2197) until
#2574 landed at 13:29Z, and the delta was the same 191 -> 192 either way. The
remedy they asked for was right; the cause was upstream.
Stand: Echo Act IV Mini
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
john P1 (fffbee4): the doc claimed Settings/health-check report voice: managed/BYO/none, but current main does not — health-check.py reports 'managed/Gemini voice credential configured'/disabled and never consumes ResolvedCredential.source. #2197 ships source-RETURNING observability at the resolver seam; the three-state PRESENTATION is G5/G6 follow-up. Also names the open Python twin #2575 in phase-2/open-items instead of describing it only as future work.
…ls-drift check
Diff coverage was 81.8% — `src/health-check.py:1758-1759`, the two lines that
decide what happens when the measurement itself fails. Untested, they are the
worst lines to leave uncovered: an uncaught raise there takes down the whole
health run from inside the probe that exists to report on it.
v5 git raises mid-run (OSError) -> no commits, and the probe must NOT invent
a warning from a failed read
v6 git runs but the log call returns non-zero (bad ref, renamed remote) ->
same degrade. Distinct from v5: one raises, the other returns rc != 0, and
they are different lines.
diff coverage 81.8% (missing 1758-1759) -> 100% (0 of 11 missing)
Rebased onto main, which now carries #2574, so the `refuse docs/src-map.md
stale vs src/` failure clears: `gen-src-map.py --check` reports up to date with
nothing of mine added.
On that check — both reviewers attributed the stale map to this PR's new helper.
It was not: `main` itself was red on `lint-src-map` from bab6360 (#2197) until
#2574 landed at 13:29Z, and the delta was the same 191 -> 192 either way. The
remedy they asked for was right; the cause was upstream.
Stand: Echo Act IV Mini
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ble (#2573) * fix(health-check): a merged skill fix that was never pulled is invisible A checkout ONE commit behind main reported ok while the agent executed a skill whose merged fix it did not have. Found by living it on the 24/7 node. The existing `>= 10 commits behind` threshold is right — main moves several times a day and warning on any delta trains the reader to ignore the check. The problem is that a COUNT cannot distinguish one commit that rewrites a skill from nine that touch docs. Skills are the one layer with no other detector. `src/` needs a restart, so the `*-stale` probes catch it by comparing a running process to its source. A skill has no process: the agent re-reads the markdown from this checkout on every invocation, so an unpulled skill fix is simply not in effect and every cheap signal reads healthy. What happened: this node sat at `0c92c8e8`, two behind, probe green. One of the two was #2568, which moved the current-track anchor off the shared flat path — so the live `context-reconstruct` still instructed writing `state/current-track.md`, the shared path whose two-writer collision had destroyed a peer's anchor hours earlier. The running skill and the merged skill disagreed, and both looked correct. Now warns when any not-yet-pulled commit touches `skills/`, regardless of count. Same no-network, last-fetched-ref contract as `_commits_behind`, with the same consequence stated in the text: it can only under-report, never cry wolf. Evidence replays this node's real states rather than fixtures: HEAD=0c92c8e8 (the node at 12:48Z) behind=2, skill commits=[#2568] -> WARN live checkout now (1 behind, src/) -> OK The second is the one that matters for noise: ordinary drift stays quiet. new cases at parent : 5 FAIL new cases at HEAD : 8 ok 67 suites referencing health-check / git_binary : 67 pass, 0 fail py_compile clean; review-checks PASS v2/v3 are over-trigger controls and pass at parent by design; the witnesses are v1/v4. Case k (1 behind -> ok) is unchanged — its fixture commits at the repo root, so ordinary drift keeps its verdict. Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(health-check): cover the failed-measurement branches of the skills-drift check Diff coverage was 81.8% — `src/health-check.py:1758-1759`, the two lines that decide what happens when the measurement itself fails. Untested, they are the worst lines to leave uncovered: an uncaught raise there takes down the whole health run from inside the probe that exists to report on it. v5 git raises mid-run (OSError) -> no commits, and the probe must NOT invent a warning from a failed read v6 git runs but the log call returns non-zero (bad ref, renamed remote) -> same degrade. Distinct from v5: one raises, the other returns rc != 0, and they are different lines. diff coverage 81.8% (missing 1758-1759) -> 100% (0 of 11 missing) Rebased onto main, which now carries #2574, so the `refuse docs/src-map.md stale vs src/` failure clears: `gen-src-map.py --check` reports up to date with nothing of mine added. On that check — both reviewers attributed the stale map to this PR's new helper. It was not: `main` itself was red on `lint-src-map` from bab6360 (#2197) until #2574 landed at 13:29Z, and the delta was the same 191 -> 192 either way. The remedy they asked for was right; the cause was upstream. Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(health-check): gate skill drift on the effective tree diff, not commit-path history qingyun-wu and john-the-dev independently reproduced the same P1 on dce81b9, and they are right — I answered the wrong question. "Did a not-yet-pulled commit TOUCH skills/" is commit-path history. "Would pulling change any skill bytes" is a tree diff. They diverge whenever history is reversible: upstream adds `skills/demo/SKILL.md` and removes it in the next commit, a clone sits two behind, and git log --format=%s HEAD..origin/main -- skills/ -> 2 commits git diff --name-only HEAD..origin/main -- skills/ -> (empty) Pulling would change nothing, and the probe warned anyway. That is a false behavioral-staleness alarm — precisely the alert fatigue this check exists to argue against, produced by the check itself. Reproduced locally before fixing. The tree diff is now the GATE and history is only the message: nothing differing under the prefix returns nothing; only a real difference lists the commits, so the warning stays actionable. Renamed `_behind_commits_touching` -> `_behind_commits_changing`. The old name described what the code did and not what the caller needed, which is how the wrong question got asked in the first place. v7 net-zero add/remove -> no drift, no warn (the fixture ASSERTS history-yes/tree-no before asserting the verdict, so it cannot pass by failing to reproduce the disagreement) v7b the tree-diff call itself fails -> degrade closed, no drift claim (its own branch, distinct from v5's log-call failure — and the gate runs first, so falling through to history would restore the bug) v8 a real, unreverted skill change -> still reports, still warns v7 against the pre-fix body: FAIL (returned both commits, status warn) diff coverage 84.6% -> 100% (0 of 13) 68 suites referencing health-check / git_binary: 68 pass py_compile clean; gen-src-map --check up to date Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(health-check): cover the log call's failure branch, which the tree-diff gate made unreachable Required diff-coverage gate was red: src/health-check.py 94.1%, one changed line uncovered — the `out.returncode != 0` early return on the LOG call. It went uncovered because of the fix itself. v6 used to reach it by passing a bad ref; after the tree-diff gate landed, a bad ref fails at the DIFF and returns before the log is ever issued. The branch is still live in production — the diff can succeed while the log fails — and it must degrade the same way rather than return a half-answer built on a failed read. Reaching it needs the diff to SUCCEED with output and only the log to fail, which is what v7c stubs. The three failure controls now cover distinct lines rather than overlapping on one: v5 the log call RAISES v7b the tree-diff call RAISES (the gate runs first) v7c the diff succeeds, the log returns non-zero 94.1% (missing 1 line) -> 100% (0 of 17) suite: all invariants hold rebased onto current main; gen-src-map --check up to date Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r requirements, not shipped john's blocking review (20:37): lines 63-64 described desktop/AU writing the managed file atomically at mode 0600 in shipped-contract voice, but origin/main has no production writer — only readers (#2197 readManaged, startup, health) + test-fixture writes. Reframed writer + atomic/0600 as requirements on a not-yet-implemented provisioner, mirroring the adjacent version paragraph's shipped-vs-follow-up distinction, so no operator assumes a security property the product does not yet provide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e bundled core (#2575) Phase-2 of G8 (desktop-parity): the desktop bundle runs both a TS surface and a Python core, so credential resolution must decide IDENTICALLY in both. This adds src/credential_resolver.py mirroring src/credential-resolver.ts (#2197, merged): same tier order (managed -> env), same in-tier voice->text fallback, same source surfacing, same never-raise on malformed/wrong-shape managed files, version reserved-not-enforced. tests/credential-resolver.test.py mirrors the TS suite one-for-one (shared canaries per the policy-twin lesson from #2516) — 10/10 pass, ruff clean. Follow-up (not this PR): sweep the Python GEMINI_*_API_KEY readers to route through resolve_credential (mirrors #2197's TS reader sweep). Co-authored-by: Qingyun Wu <qingyun@ag2.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
* docs(g8): capability-not-key credential-resolver spec
The G8 design doc the parity plan assigns me ('progress G8 spec'):
generalizes #2197's src/credential-resolver.ts contract — capability
vocabulary, managed→env tier order with in-tier legacy fallback,
byte-identical-without-managed-file guarantee, source observability for
G5/G6 surfaces, managed-file schema + atomic-write rules, G7 interaction
(vault = tier addition, blocks nothing), 3-phase reader-sweep migration
(TS done in #2197; Python twin with shared canaries per the #2516 lesson;
then non-Gemini one provider at a time), and security notes (tier order
rationale + the source-surfacing guard).
Owner calls left open on purpose: managed pilot = Gemini voice; G7.
* fix(g8-spec): register in docs index+catalog; describe #2197 as in-flight, not done
Reviewer blockers: (1) docs_audit failed — new doc unindexed in
docs/README.md and absent from docs/catalog.json; both registered (status:
draft). (2) The draft asserted present-tense implementation on main while
#2197 is open — reworded to describe the resolver as #2197's head (the
implementing PR, in review), phases relative to its merge.
* docs(g8): version is reserved, not enforced by #2197 (fix the claim)
Reviewer (john-the-dev, blocking): the schema section claimed unknown
`version` values skip the managed tier and fail open to BYO, and that #2197
implements it. It does not — #2197's readManaged() parses `capabilities`
regardless of `version`, so `{version:999,…}` + a valid capability resolves as
managed. Corrected the doc to state the actual behavior: `version` is reserved,
unknown-version → skip is a follow-up on the resolver, not part of #2197.
(Catalog.json diff-hygiene churn — the audience-array reformat — is the
remaining item; fixing separately.)
* docs(g8): minimal catalog registration — drop the reformat churn
Reviewer: adding one catalog entry had rewritten every compact `audience`
array (271/177 for a one-entry add), obscuring the change. Restored main's
docs/catalog.json formatting and re-added ONLY the one new document entry in the
compact style — diff is now +7 lines, the registration itself.
* docs(g8): scope the capability-not-provider claim to shipped key-decoupling + alias contract
john's review: the vocabulary section claimed capabilities name functions-not-
providers and swap providers freely, but the canonical IDs gemini-voice/gemini-
text are provider-branded — an internal contradiction. Narrowed it: the SHIPPED
decoupling is from the key (a consumer never reads an env var); provider
neutrality is an explicit follow-up, delivered by registering role IDs
(voice-llm/text-llm) with the current IDs as aliases in the fallback table.
Until then the portability claim is scoped to managed<->BYO (same provider,
different tier), which `source` already surfaces. (Point 1 — version-gating —
was already marked a not-yet-enforced follow-up at this head.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(g8): #2197 merged — refresh spec refs (in-review→MERGED, paths now on main)
The implementing PR #2197 (credential-resolver.ts + TS GEMINI_* reader sweep)
merged 2026-08-03, so the spec's 'open, in review' framing and 'paths describe
that PR's head, not current main' caveat are stale. Refreshed to reflect that
the resolver is now on main. (Version-gating remains a not-yet-enforced
follow-up — #2197 did not add it — so that section stands unchanged.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(g8): scope rule 3 to the resolver seam; name open twin #2575
john P1 (fffbee4): the doc claimed Settings/health-check report
voice: managed/BYO/none, but current main does not — health-check.py
reports 'managed/Gemini voice credential configured'/disabled and never
consumes ResolvedCredential.source. #2197 ships source-RETURNING
observability at the resolver seam; the three-state PRESENTATION is G5/G6
follow-up. Also names the open Python twin #2575 in phase-2/open-items
instead of describing it only as future work.
* docs(g8): mark managed-file writer + atomic/0600 as future-provisioner requirements, not shipped
john's blocking review (20:37): lines 63-64 described desktop/AU writing the
managed file atomically at mode 0600 in shipped-contract voice, but origin/main
has no production writer — only readers (#2197 readManaged, startup, health) +
test-fixture writes. Reframed writer + atomic/0600 as requirements on a
not-yet-implemented provisioner, mirroring the adjacent version paragraph's
shipped-vs-follow-up distinction, so no operator assumes a security property the
product does not yet provide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Qingyun Wu <qingyun@ag2.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
Standalone to
main— not stacked. (Earlier revisions were stacked on #2131; #2197 was retargeted tomainand now carries the resolver + its tests directly, so it lands on its own. This body supersedes the prior "stacked / retarget-after-#2131" note — see Merge order below.)What
Lands the G8 credential resolver (capability, not key) and routes the TS
GEMINI_*readers through it:credential-resolver.ts(+tests/credential-resolver.test.ts): theresolveCredential(logical, [envFallbacks])seam — managed-tier (state/auth) → BYO env chain → typed-absent. Empty/undefined managed falls through (no accidental blanking of a working BYO key); absent-by-default is byte-identical for every current consumer.browser-tools.ts(vision describe +point_at),recording-tools.ts(frame describe): inlineGEMINI_VOICE_API_KEY || GEMINI_API_KEY→resolveCredential('gemini-voice').key. Identical resolution today; managed-aware the moment a managed file exists.voice-key.ts:voiceApiKey()→resolveCredential('gemini-voice', [GEMINI_VOICE_API_KEY, GEMINI_API_KEY])— reproduces the old chain 1:1 when unmanaged.voice-agent.ts: the startup key assert now names the winning tier (managed credentials (state/auth)vs the env-var name) — boot-time source observability, the point of G8.Merge order (canonical — resolves the prior contradictory metadata)
main. It is self-contained (resolver + tests + full TS reader sweep) and does not depend on docs(credential): note the G8 resolver precedence at the three gemini-voice call sites #2131.qingyun-devintegration branch with a 3-line subset (browser-tools.ts+2,recording-tools.ts+1) that is fully subsumed here. Once refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver #2197 is onmain, docs(credential): note the G8 resolver precedence at the three gemini-voice call sites #2131 carries no unique main-bound change and should close as superseded (owner's call — itsqingyun-devbase + review history are deliberate).Deliberately NOT in this PR
Python skill scripts (
image-generation,audio-transcribe) and shell consumers stay on their own env/.envchains: the scripts are standalone-by-contract ("can't import src/" — symlinked installs break relative imports), and managed installs don't exist yet (W4). Their managed-tier support lands when there's a real consumer to verify against, instead of duplicating unverifiable logic now.Tests
credential-resolver.test.tspins the chain semantics (managed-wins / BYO-order / empty-falls-through). Full TS suite green,tsc --noEmitclean; the call-site swaps are identical no-managed-file behavior (no behavior change).🤖 Generated with Claude Code