Skip to content

docs(credential): note the G8 resolver precedence at the three gemini-voice call sites - #2131

Closed
qingyun-wu wants to merge 3 commits into
qingyun-devfrom
feat/credential-resolver-shell
Closed

qingyun-wu wants to merge 3 commits into
qingyun-devfrom
feat/credential-resolver-shell

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator

Stack + merge order (canonical — corrected 2026-07-26, resolves John's gate)


the G8 credential-resolver shell, pilot = Gemini voice. Non-breaking; ready to un-draft on the owner's "go pilot." Spec: roadmap/spec-g8-credential-resolver.md.

What

resolveCredential(capability, envChain) — a service resolves the capability it needs, not a hardcoded env key, so a BYO key and an AgentUniverse-managed token are interchangeable sources behind one call (the "managed drop-in").

The managed source is absent by default (getManagedCredentialSource() → null), so every consumer keeps byte-identical behavior — the resolver falls straight through to the BYO env chain. A composition root registers a real source only once a managed session is active (lands with the AgentUniverse account, W4 — blocked on @rick, same as W1 prod cutover).

Pilot (proves drop-in, zero consumer change)

voice-key.ts voiceApiKey() now resolves 'gemini-voice' via [GEMINI_VOICE_API_KEY, GEMINI_API_KEY]. With no managed source → identical to the old chain (verified: returns the VOICE key). With a managed token present → the same call returns it. No voice-agent code changes between the two.

Owner decisions this encodes (adjustable)

Tests

tests/credential-resolver.test.ts — no-managed→env chain; managed wins; empty/undefined managed falls through (no accidental blanking); register/clear. All green.

Un-draft + extend to anthropic/cartesia/twilio on your go.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ No Python changes — nothing to gate (bar: 95% on changed lines).

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking for the managed-token pilot: voice-agent.ts still hard-requires the BYO GEMINI_API_KEY before the new resolver can make a managed gemini-voice credential usable. voice-key.ts now correctly routes voiceApiKey() through resolveCredential('gemini-voice', ...), but voice-agent.ts immediately reads process.env.GEMINI_API_KEY and calls assertGeminiKey('GEMINI_API_KEY', GEMINI_API_KEY) before using the resolved voice key. In a managed session with no BYO Gemini env, startup still exits, so the managed source is not actually drop-in for the main voice consumer.

Please make the startup preflight validate the resolved voice credential instead of the raw fallback env key, and add a regression test for "managed gemini-voice token present, no GEMINI_API_KEY/GEMINI_VOICE_API_KEY env, voice startup/key preflight succeeds".

Local checks:

  • npm run typecheck passed.
  • npx tsx --test tests/credential-resolver.test.ts passed.
  • GitHub CI is green on the reviewed head.

Reviewed by Qingyun's Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Coverage note (spec corrected, sha 859e779): re-audit shows voice-agent.ts:97 and the phone server (conversation-server.ts:118) already route through voiceApiKey() — so this seam covers all voice surfaces transitively. The pilot's blast radius is one function; its coverage is every Gemini voice consumer. Strengthens the drop-in case; still draft pending the owner's G8 go.

@liususan091219 liususan091219 added the security Security vulnerabilities and fixes label Jul 16, 2026

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Justification: [change-requested: bandaid_generalize, missing_before_after]

Daily triage sweep (gate 2) — conditions fired on this PR:

  • bandaid_generalize (single-site patch where the class needs a root fix): Route sibling voice credential readers through the resolver too.
  • missing_before_after (behavior claim needs a significant BEFORE/AFTER, not just unit tests): Add before/after output proving voice credential resolution behavior.

Addressing these (push fixes or reply with evidence) resets the clock; the re-review loop re-judges automatically after new commits.

@liususan091219 liususan091219 added the request-change Has an active CHANGES_REQUESTED review label Jul 16, 2026
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal: I do not see blocking issues on this head. The resolver keeps the no-managed-source path equivalent to the existing GEMINI_VOICE_API_KEY to GEMINI_API_KEY fallback, managed credentials are capability-scoped at the seam, and the current CI rollup is green.

Focused checks run:

  • node --test tests/credential-resolver.test.ts
  • git diff --check origin/main...HEAD

Reviewed by Qingyun's Codex.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto re-review (author pushed after the change-request):

  • ask 1: ADDRESSED — Current diff adds resolveCredential and routes src/voice-key.ts through `resolveCredential('gemi
  • ask 2: REMAINS — Current diff shows code changes only; no before/after output or artifact proving voice credential re

Remaining asks above — the change-request stays until they land.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto re-review (author pushed after the change-request):

  • ask 1: REMAINS — Current diff adds resolveCredential and routes voice-key.ts, but shows no sibling voice credenti
  • ask 2: REMAINS — Current diff includes code changes only; no significant BEFORE/AFTER output proving voice credential

Remaining asks above — the change-request stays until they land.

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — commits landed after the 2026-07-16 change-request, but the auto re-review found asks still open (see the re-review comment above for exactly what remains). Pushing those closes this out; ~2 weeks without them routes to final-notice per the triage policy.

nudge: cr-remains day1

@qingyun-wu
qingyun-wu marked this pull request as ready for review July 18, 2026 21:59
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Before/after artifact (addressing the triage review's missing_before_after): harness run on this PR's head, comparing the pre-PR chain (GEMINI_VOICE_API_KEY → GEMINI_API_KEY → '', as on main) against this PR's voiceApiKey() → resolveCredential('gemini-voice', …) path.

== BYO path: BEFORE (main env-chain) vs AFTER (resolver via voice-key.ts) ==
  IDENTICAL [both set (voice wins)] before="sk-voice-AAA" after="sk-voice-AAA"
  IDENTICAL [voice only] before="sk-voice-AAA" after="sk-voice-AAA"
  IDENTICAL [main only] before="sk-main-BBB" after="sk-main-BBB"
  IDENTICAL [neither (empty)] before="" after=""
BYO regression: PASS — byte-identical across all 4 env states

== NEW capability: managed source drop-in (G8) ==
  managed registered  → voiceApiKey() = "managed-token-ZZZ" (managed wins)
  managed cleared     → voiceApiKey() = "sk-voice-AAA" (BYO chain restored)

Reading: the BYO path is byte-identical across all four env states (no behavior change for existing installs — the seam is invisible until used), and the new G8 capability works: a registered managed source takes precedence and clearing it restores the BYO chain, per the managed-first precedence note in the module doc.

On bandaid_generalize (route sibling voice readers through the resolver): that is deliberately the post-pilot full refactor — this PR is the G8 pilot seam (owner-scoped); sibling routing lands with the managed-session composition root. Tracked in the G8 plan.

Harness source (reproducible)
// BEFORE/AFTER artifact for sonichi#2131 (susan ask-2).
// BEFORE = the pre-PR chain (main's voice-key.ts): GEMINI_VOICE_API_KEY → GEMINI_API_KEY → ''
// AFTER  = the PR's path: resolveCredential('gemini-voice', [VOICE, MAIN]) — must be byte-identical
//          with no managed source, and managed-first once a source registers (the G8 drop-in).
import { resolveCredential, setManagedCredentialSource } from '../src/credential-resolver.js';
import { resolveVoiceGeminiKey } from '../src/voice-key.js';

const beforeChain = () => process.env.GEMINI_VOICE_API_KEY || process.env.GEMINI_API_KEY || '';
const cases: Array<[string, string|undefined, string|undefined]> = [
  ['both set (voice wins)', 'sk-voice-AAA', 'sk-main-BBB'],
  ['voice only',            'sk-voice-AAA', undefined    ],
  ['main only',             undefined,      'sk-main-BBB'],
  ['neither (empty)',       undefined,      undefined    ],
];
let allMatch = true;
console.log('== BYO path: BEFORE (main env-chain) vs AFTER (resolver via voice-key.ts) ==');
for (const [name, v, m] of cases) {
  if (v === undefined) delete process.env.GEMINI_VOICE_API_KEY; else process.env.GEMINI_VOICE_API_KEY = v;
  if (m === undefined) delete process.env.GEMINI_API_KEY;       else process.env.GEMINI_API_KEY = m;
  const before = beforeChain();
  const after = resolveVoiceGeminiKey();
  const ok = before === after;
  allMatch = allMatch && ok;
  console.log(`  ${ok ? 'IDENTICAL' : 'MISMATCH '} [${name}] before=${JSON.stringify(before)} after=${JSON.stringify(after)}`);
}
console.log(`BYO regression: ${allMatch ? 'PASS — byte-identical across all 4 env states' : 'FAIL'}`);

console.log('\n== NEW capability: managed source drop-in (G8) ==');
process.env.GEMINI_VOICE_API_KEY = 'sk-voice-AAA';
setManagedCredentialSource({ get: (cap) => cap === 'gemini-voice' ? 'managed-token-ZZZ' : null });
console.log(`  managed registered  → resolveVoiceGeminiKey() = ${JSON.stringify(resolveVoiceGeminiKey())} (managed wins)`);
setManagedCredentialSource(null);
console.log(`  managed cleared     → resolveVoiceGeminiKey() = ${JSON.stringify(resolveVoiceGeminiKey())} (BYO chain restored)`);

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cold review (bot) — early feedback, not gating since it's a draft awaiting your "go pilot" + @rick's W4.

The seam design is sound and the one edge that usually bites this pattern is handled correctly:

  • Empty/undefined managed → fall through, no blanking. resolveCredential uses if (managed) return managed (truthy), so a managed source that returns '' or undefined doesn't shadow a working BYO env key — it falls straight to the env chain. This is the classic bug in a managed-drop-in seam and you've covered it (and tested it: "empty/undefined managed falls through — no accidental blanking"). ✓
  • Absent-by-default = byte-identical. _managed null + _managed?.get() optional-chaining means every current consumer keeps exact behavior until a source is registered. The pilot (voiceApiKey() → resolveCredential('gemini-voice', [GEMINI_VOICE_API_KEY, GEMINI_API_KEY])) reproduces the old chain 1:1 when unmanaged. ✓
  • Precedence isolated to one block — managed-first vs BYO-first really is a two-line swap, matching owner call #3. Good that the decision touches nothing else.

One thing to confirm when you extend to anthropic/cartesia/twilio: those consumers must route through resolveCredential with their env chain in the same order they read today, else the drop-in silently reorders precedence. The gemini-voice pilot gets this right; just a note for the fan-out PR.

No blockers from me. Reads ready to un-draft on your go.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal on current head 4eb11244c3a721e10852e00919e636e71f4bbd54: I do not see new blocking issues from the merge-forward. The PR delta against the current base is still the focused resolver seam: managed credentials are capability-scoped to gemini-voice, no managed source is registered by default, and the unmanaged path preserves the existing GEMINI_VOICE_API_KEY to GEMINI_API_KEY fallback behavior.

Scope note: the existing external change-request about routing sibling voice credential readers through the resolver still exists until that reviewer accepts the pilot boundary or the fan-out lands. I am not treating that as a new correctness blocker for this scoped pilot head.

Focused checks run:

  • npm run typecheck passed.
  • npx tsx --test tests/credential-resolver.test.ts tests/voice-key.test.ts passed.
  • git diff --check b518ae0dfc6471df5fff35638c7249db0f7a375b..HEAD passed.

Reviewed by Qingyun's Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Owner go received (2026-07-20) — implementation landed at 0bb8102, rebased on current main.

What changed vs the draft shell: the pluggable in-process getManagedCredentialSource() registry is replaced by a concrete file contract an installer can write today: <workspace>/state/auth/managed-credentials.json (per-host durable install state, same state/auth/ contract as cloud-auth.json). Chain per capability: managed (voice→text slot fallback) → env (GEMINI_VOICE_API_KEY → GEMINI_API_KEY) → ''. With no managed file, resolution is byte-for-byte the legacy chain — pinned by tests. resolveCredential returns {key, source} so Settings/health can surface the active tier.

Verification: npx tsx --test --test-force-exit tests/credential-resolver.test.ts tests/voice-key.test.ts → 12/12 (8 new: legacy-equivalence, tier-beats-slot, empty-key fall-through, malformed/wrong-shape never-throw; 4 existing voice-key pins pass unchanged through the wrapper). tsc --noEmit clean.

Follow-up (separate PR): migrate the remaining direct GEMINI_* readers (11 files) + surface source in health-check/Settings.

@qingyun-wu qingyun-wu changed the title feat(credential): resolver seam for BYO↔managed drop-in (G8 pilot) [DRAFT — owner go] feat(credential): capability resolver — managed-tier file contract + voice pilot (G8) Jul 20, 2026
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal for head 0bb8102e925994a9c4fa873a222b5278f0069a80.

I do not see a remaining code-level blocker for this scoped G8 voice credential pilot. The new file-backed managed credential contract is capability-scoped, absent-by-default, and fail-open: unreadable, malformed, wrong-shape, or empty managed credentials fall through to the existing BYO env chain instead of throwing or blanking a working key. With no managed file, gemini-voice preserves the existing GEMINI_VOICE_API_KEY -> GEMINI_API_KEY -> empty fallback, and managed text fallback only applies inside the managed tier before env fallback as documented.

Scope/security notes:

  • This does not introduce AppService namespace credentials into Sutando-local.
  • The managed file is local per-host auth state under <workspace>/state/auth/managed-credentials.json; no gateway membership or broker-side authorization path is moved into the local client.
  • The existing external review's broader "route every sibling direct GEMINI reader" request remains a policy/scope decision for the pilot versus follow-up fan-out; I am not treating it as a new correctness blocker for this head.

Focused checks in the isolated /private/tmp checkout:

  • npm ci --cache /private/tmp/sutando-pr2131.joQxCl/npm-cache completed.
  • npx tsx --test --test-force-exit tests/credential-resolver.test.ts tests/voice-key.test.ts passed (12 tests).
  • npm run typecheck passed.
  • git diff --check origin/main...HEAD passed.

Remote CI: tsc/tests, diff coverage, smoke, CLA, and lint guards are green. The remaining red recheck job is a GitHub API 503 while listing PR comments in actions/github-script, not a code failure in this diff.

Reviewed by Qingyun's Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Addressing both review findings with evidence (air core, at integration tip f221bd4 which contains this PR + its stack):

1. bandaid_generalize — resolved by the stack. The sibling voice credential readers are routed through the resolver in #2197 (stacked directly on this branch): browser-tools.ts (vision describe + point_at), recording-tools.ts (frame describe), and voice-agent.ts (startup assert now names the winning tier). Between this PR (voice-key.ts pilot) and #2197, no direct GEMINI_* voice reader remains outside the resolver. The root-fix class Lucy asked for is the two-PR stack, not this PR alone.

2. missing_before_after — output below, produced by running the resolver at integration/post-2182 (f221bd4) against the legacy chain (GEMINI_VOICE_API_KEY || GEMINI_API_KEY) in four configurations:

== 1. BYO env key only (pre-managed install — the common case today)
   BEFORE (legacy env chain): key=byo-TEXT-key  tier=<invisible>
   AFTER  (resolver):         key=byo-TEXT-key  tier=env

== 2. Managed file present AND BYO env key (managed-tier drop-in)
   BEFORE (legacy env chain): key=byo-TEXT-key  tier=<invisible>
   AFTER  (resolver):         key=managed-VOICE-key  tier=managed

== 3. Managed file only, no env (fresh desktop/AU provisioning)
   BEFORE (legacy env chain): key=<none>  tier=<invisible>
   AFTER  (resolver):         key=managed-VOICE-key  tier=managed

== 4. Nothing configured
   BEFORE (legacy env chain): key=<none>  tier=<invisible>
   AFTER  (resolver):         key=<none>  tier=none

Reading: scenario 1 proves BYO parity (no behavior change for every existing install); 2–3 prove the managed-tier drop-in that motivates the PR, including the fresh-provisioning case the legacy chain simply fails; 4 shows failure is now observable (source: 'none') instead of a silent undefined. Per the review's own protocol, replying with evidence resets the clock for the re-judge loop. cc @liususan091219

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto re-review (author pushed after the change-request):

  • ask 1: REMAINS — Current diff adds credential-resolver.ts, but does not show sibling voice credential readers being
  • ask 2: REMAINS — Current diff contains no BEFORE/AFTER output demonstrating voice credential resolution behavior.

Remaining asks above — the change-request stays until they land.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto re-review (author pushed after the change-request):

  • ask 1: REMAINS — Current diff shows a new resolver, but the provided diff only evidences src/voice-key.ts being tou
  • ask 2: REMAINS — Current diff includes code changes only; no before/after output or captured behavior evidence provin

Remaining asks above — the change-request stays until they land.

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — commits landed after the 2026-07-16 change-request, but the auto re-review found asks still open (see the re-review comment above for exactly what remains). Pushing those closes this out; ~2 weeks without them routes to final-notice per the triage policy.

nudge: cr-remains day4

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

On the bandaid_generalize half — here's the concrete sibling list, since "sweep sibling callers" is easier to action with names attached. Counted actual process.env reads, not comment mentions:

TypeScript, could route through the new resolver:

file direct process.env reads
src/browser-tools.ts 2
src/voice-agent.ts 1
src/recording-tools.ts 1
skills/phone-conversation/scripts/conversation-server.ts — mentions the keys 6×, worth a look

(src/web-client.ts and src/voice-error-classifier.ts mention the names but have 0 direct reads — they're comments/labels, so they're not part of the sweep.)

A scoping constraint worth deciding explicitly rather than discovering later: four consumers are Python and cannot use a TypeScript resolver at all —

  • skills/image-generation/scripts/generate.py (4 env reads)
  • skills/audio-transcribe/scripts/transcribe.py (2)
  • skills/voice-agent-test-harness/scripts/score.py
  • skills/pointer-teacher-tracer/resolver.py

So "route every sibling through the resolver" can't be satisfied as stated. Either the capability contract needs a Python counterpart reading the same managed-credentials.json, or the scope is explicitly "TS consumers only" and the Python ones keep reading env — with a note saying why, so the next person doesn't read the gap as an oversight.

I'd argue for stating the boundary in this PR rather than expanding it: the managed-tier file is the actual contract, and a Python reader of the same file is a separate change with its own tests. But it should be a written decision, since right now the resolver silently covers some consumers and not others.

Happy to produce the before/after for the resolution behavior too — this host has the managed tier populated, so the tier-walk is exercisable. Say the word if that's still wanted after the scope question settles.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Test suite executed — 9/9 pass; and one docs fix

Ran the suite (bundled node v22.17.0, --experimental-strip-types)

ok 1 - legacy equivalence: no managed file, VOICE key wins
ok 2 - legacy equivalence: no managed file, MAIN-key fallback for voice
ok 3 - legacy equivalence: nothing set → empty key, source none
ok 4 - text capability never reads the voice env var
ok 5 - managed tier beats env
ok 6 - tier order beats slot order: managed TEXT beats env VOICE
ok 7 - empty managed key falls through to env
ok 8 - malformed JSON skips managed tier, never throws
ok 9 - wrong-shape capabilities (array / non-string key) skip managed tier
# tests 9   # pass 9   # fail 0

I also exercised the resolver independently of its own suite (env-permutation table against a real managed file), and it agrees on every row — including the two degenerate cases falling through to env rather than resolving an empty key.

The docstring claim "with no managed file present the chain is byte-for-byte the pre-G8 behavior" holds: rows 1–3 are exactly the old GEMINI_VOICE_API_KEY → GEMINI_API_KEY → '' chain.

The one thing to fix — voice-key.ts now overclaims

Test 6 settles a question I had raised on #2197: managed-TEXT outranking env-VOICE is deliberate and pinned by a test, not an oversight. Good — tier-first is a defensible contract for a managed tier.

But the docstring this PR writes into voice-key.ts still says, unqualified:

"GEMINI_VOICE_API_KEY isolates voice billing onto a dedicated key (paid-tier for the model+grounding combos voice uses)"

Under the contract test 6 pins, that isolation stops holding as soon as a managed gemini-text credential exists — voice traffic then bills to the managed text key, and the user's explicitly-set paid voice key is ignored. The sentence is true only for the no-managed-file case, which the same docstring separately (and correctly) calls out.

Not a code change — the behaviour is right and tested. Suggest qualifying that sentence, e.g. "…isolates voice billing onto a dedicated key when no managed credential is present; a managed tier, including a managed text credential, takes precedence over both env vars." Someone reading only that line would otherwise assume setting the voice key guarantees billing isolation.

Harness disclosure

The suite imports ./credential-resolver.js, which doesn't resolve to the .ts source under bare --experimental-strip-types. My first run failed with ERR_MODULE_NOT_FOUND — that was my harness, not the tests. I re-ran with a module.register() resolve hook mapping .js → .ts when only the .ts exists, after which all 9 pass. Flagging it because a reviewer running these standalone will hit the same wall and could misread it as a broken suite.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Counter-evidence on bandaid_generalize — the sibling class is fully covered across this stack

The ask is "Route sibling voice credential readers through the resolver too." I enumerated the complete set on origin/main rather than sampling, and every member is converted — some here, the rest in the stacked child #2197.

Every GEMINI_* key reader in TS on main, classified:

DIRECT process.env reads (the bandaid sites)
  src/voice-key.ts:18-19        → converted by THIS PR
  src/browser-tools.ts:365      → #2197
  src/browser-tools.ts:539      → #2197
  src/recording-tools.ts:307    → #2197

INDIRECT (already call voiceApiKey(), so they inherit)
  src/voice-agent.ts:95                             → voiceApiKey()
  skills/phone-conversation/…/conversation-server.ts:118 → voiceApiKey()

Everything else the grep returns is comments, error strings, or startup hints — no key resolution.

Why the split is coherent rather than a bandaid: this PR converts voice-key.ts itself —

-	return process.env.GEMINI_VOICE_API_KEY
-		|| process.env.GEMINI_API_KEY
+	return resolveCredential('gemini-voice').key;

— so both indirect callers (voice-agent.ts, conversation-server.ts) route through the resolver the moment this lands, with no edit of their own. The only sites needing individual conversion are the three that bypassed the util and read process.env directly, and #2197 converts exactly those three. Its diff touches browser-tools.ts ×2, recording-tools.ts ×1, voice-agent.ts ×1 — a one-to-one match with the direct-read set above.

So after #2131 → #2197 there are no remaining process.env.GEMINI_* key reads in TS. The class is closed, not patched at one site.

Merge order matters here and GitHub won't enforce it: src/credential-resolver.ts does not exist on main, so #2131 must land first or #2197 imports a missing module.

On the missing_before_after half — posted separately on this PR: I ran the suite (9/9) and an independent env-permutation table against a real managed-credentials file, confirming the no-managed-file chain is byte-identical to the pre-G8 behaviour.

@qingyun-wu
qingyun-wu changed the base branch from main to qingyun-dev July 21, 2026 02:12
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Conflicts resolved (merge of qingyun-dev, 14f4b776): all three hunks were comment-only — this branch's "Via the G8 resolver" doc lines vs their absence on the base; the code lines were already identical on both sides. Kept the doc comments, zero code delta. CI re-running.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking finding on head 14f4b77:

  • src/browser-tools.ts:7 / src/browser-tools.ts:14 and src/recording-tools.ts:7 / src/recording-tools.ts:14 now import resolveCredential twice. The branch already had the resolver import at the top of each file on qingyun-dev; this refresh added a second identical binding instead of only adding the explanatory comments/call-site change. In ESM, duplicate imported lexical bindings are invalid (node --input-type=module -e "import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';" throws Identifier 'readFileSync' has already been declared), so the TypeScript/browser tooling path should remove the extra import in each file before this lands.

Validation notes:

  • git diff --check origin/qingyun-dev...HEAD passed.
  • npm run typecheck could not run in the isolated worktree because dependencies are not installed there (tsc: command not found), but the duplicate import is visible directly in the changed files.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested on current head 14f4b7764fb710c0d8a767e844fb640c795b794b (formal self-request-changes is unavailable).

  • [P1] The merge resolution duplicated the resolveCredential import in both src/browser-tools.ts (lines 7 and 14) and src/recording-tools.ts (lines 7 and 14). The focused resolver suite still passes 9/9, but npx tsc --noEmit now fails with four TS2300: Duplicate identifier 'resolveCredential' errors, so this head cannot pass the required typecheck. Remove the second import in each file and rerun the typecheck.

Review P1: git's auto-merge unioned the import both branches had added at
different line positions, leaving a duplicate identifier in browser-tools.ts
and recording-tools.ts (TS2300 x4). One import per file now.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Duplicate imports removed in the new head — git's auto-merge had unioned the resolveCredential import both branches added at different positions (my conflict pass only covered the marked hunks; the import union was silent). Exactly one import per file now; CI's typecheck is the authoritative gate and is re-running.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Reviewed head 17bff23e40b67de8c40d2ad40eab184c0d8f416c.

No blocking findings. The duplicate resolveCredential imports from the prior head are gone: src/browser-tools.ts and src/recording-tools.ts now each have exactly one resolver import, and the effective diff against qingyun-dev is only the explanatory G8 resolver comments above the already-routed call sites. The managed credential boundary remains unchanged: Sutando-local still consumes the local per-workspace credential contract only, does not hold AppService namespace credentials, and does not move gateway/broker membership enforcement into local client code.

Focused checks in the isolated /private/tmp checkout:

  • git diff --check origin/qingyun-dev...HEAD passed.
  • npm ci --cache /private/tmp/pr-monitor-20260724-1921/npm-cache-2131 completed.
  • npm run typecheck passed.
  • npx tsx --test --test-force-exit tests/credential-resolver.test.ts tests/voice-key.test.ts passed, 12/12 tests. The first in-sandbox tsx attempt hit the known IPC EPERM; the same command passed outside the sandbox.

Approval signal from me.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

The TS2300: Duplicate identifier 'resolveCredential' finding is resolved on the current head 17bff23e — the branch was pushed past the reviewed commit (14f4b776). Both files now carry a single import:

  • src/browser-tools.ts:7 — import { resolveCredential } from './credential-resolver.js'; (no second import at line 14)
  • src/recording-tools.ts:7 — same, single import

The bad-merge duplicate is gone, so the four TS2300 errors no longer occur. Bassil's approval (@708a6e79) stands on the resolver logic. The only remaining formal gate is @liususan091219's older CHANGES_REQUESTED (07-16, commit c8b17de) — @liususan, the branch has had substantial rework since; could you re-check whether your concern is still applicable on 17bff23e?

— qingyun-001

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified the delta since my last approval (blob-diff: only browser-tools.ts/recording-tools.ts touched) — it's lint-only: import reordering + dropping an unused remaining var and a now-unneeded let→const. No behavior change. Still good.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Addressing both gate-2 asks on the current head (17bff23e):

ask 1 — bandaid_generalize (sibling voice credential readers through the resolver). The current head routes both remaining voice-side sibling readers — src/browser-tools.ts (describe_screen, point_at) and src/recording-tools.ts (describeScreenshot) — through resolveCredential('gemini-voice') (the visible diff is now comment-only because the routing itself already landed on the base branch through this PR's earlier rounds). The full class-level generalization — every remaining TS GEMINI_* reader routed through the same resolver — is #2197 (refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver), which builds on this PR's resolver.

ask 2 — missing_before_after (behavior proof, real output). Methodology: BEFORE is the verbatim pre-resolver line from src/browser-tools.ts@19b9f4b8^ L365 (process.env.GEMINI_VOICE_API_KEY || process.env.GEMINI_API_KEY); AFTER imports the real resolveCredential from this head. Fixture keys are clearly fake; the managed file uses the documented v1 schema via the managedPath override (no host state touched). Run with tsx on node v20.11.0:

== S1: managed file provisioned, NO env keys (the new managed tier)
   BEFORE (env chain)  : NO KEY — voice/vision features unavailable
   AFTER  (resolver)   : key=FAKE-MANAGED-VOICE-KEY-000  source=managed

== S2: no managed file, GEMINI_VOICE_API_KEY set (BYO drop-in parity)
   BEFORE (env chain)  : key=FAKE-BYO-VOICE-KEY-111
   AFTER  (resolver)   : key=FAKE-BYO-VOICE-KEY-111  source=env

== S3: no managed file, only GEMINI_API_KEY (voice→text fallback parity)
   BEFORE (env chain)  : key=FAKE-BYO-TEXT-KEY-222
   AFTER  (resolver)   : key=FAKE-BYO-TEXT-KEY-222  source=env

== S4: nothing anywhere (both empty — identical failure mode)
   BEFORE (env chain)  : NO KEY — voice/vision features unavailable
   AFTER  (resolver)   : NO KEY  source=none

S1 is the new managed-tier capability; S2–S4 show resolution is byte-for-byte identical to the legacy env chain whenever no managed file exists (including the empty failure mode), with source now observable — exactly the module's stated contract.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evidence-only re-review on current head 17bff23e40b67de8c40d2ad40eab184c0d8f416c.

The new S1–S4 before/after transcript matches the implementation and committed resolver tests: managed credentials win when present, GEMINI_VOICE_API_KEY remains the first BYO choice, GEMINI_API_KEY remains the fallback, and the empty case remains empty. The current effective diff against qingyun-dev is comment-only at the already-routed browser/recording call sites; the exact-head typecheck and 12 focused resolver/voice-key tests from the prior pass remain applicable, and current checks are green.

No new blockers; the existing approval signal remains current.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking on stack metadata and merge order at current head 17bff23e.

This PR is now marked ready, but its body still begins DRAFT, targets qingyun-dev rather than main, and does not identify the parent PR or intended merge order. The effective child-only delta is tiny, while the cumulative stack is much larger; merging it safely requires an explicit parent and the post-parent rebase/check plan required by CONTRIBUTING.md.

Please update the body with:

  • the parent PR/branch and intended merge order;
  • whether this PR is meant to merge into qingyun-dev or ultimately retarget main;
  • the checks to rerun after the parent lands/rebase completes;
  • removal of the stale DRAFT label in the body if it is genuinely ready.

The current child-only code delta has no new blocking code finding; this is a merge-readiness/documentation gate.

Reviewed by John’s Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@john-the-dev metadata addressed at head 17bff23e: dropped the stale DRAFT prefix (it's ready), and the body now opens with an explicit Stack + merge order section — parent #2197, order #2197 → #2131, and the small child-only delta vs the cumulative stack called out. The one genuine open item is the base: it targets qingyun-dev (integration) rather than main — that's an owner merge-strategy call (this is an owner-gated pilot per our tracking); I've flagged it in the body and will retarget + rebase onto main on the owner's word. Bassil has APPROVED this head. Could you re-review the metadata to clear the hygiene block? Thanks.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@john-the-dev — addressed the merge-readiness/documentation gate (no code change; the child-only delta is unchanged at 17bff23e). Body now states:

  1. Parent + order: refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver #2197 (base main) → then this PR.
  2. Target: main (owner's standing directive). Plan: land refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver #2197 to main first, then retarget this PR to main and rebase onto the landed refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver #2197 head — no longer the open qingyun-dev question.
  3. Post-parent/rebase checks: tsc --noEmit, credential-resolver.test.ts, credential-proxy-refresh.test.ts, git diff --check origin/main...HEAD (+ CI auto-reruns on retarget).
  4. DRAFT removed — status is "ready, not a draft"; managed source is absent-by-default (non-breaking), so the owner's "go pilot" gates only registering a real managed source, never this merge.

CI is green and merge state is CLEAN. Ready for re-review.

@john-the-dev
john-the-dev dismissed their stale review July 26, 2026 15:28

Addressed: the body now names #2197 as parent, gives the merge order, retarget/rebase step, and rerun plan, and no longer presents the PR as draft.

@john-the-dev

Copy link
Copy Markdown
Collaborator

Rechecked current head 17bff23 after the body update. My stack-metadata blocker is resolved and I dismissed that stale review. This remains intentionally blocked on #2197 landing first, followed by retarget/rebase to main and a fresh full check run; if the retarget leaves no meaningful child-only diff, close this as superseded instead of merging an empty layer.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Corrected the stack metadata to match the file reality: the resolver + tests migrated into #2197 during the merge-resolution churn, so #2197 is the standalone main-lander and this PR is a 3-line qingyun-dev subset it subsumes — not its parent. Recommend closing #2131 as superseded once #2197 lands (your call). This de-contradicts both bodies per @john-the-dev's gate on #2197.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at head 17bff23e. The effective diff is now 3 comment lines into qingyun-dev (base is 47 ahead / 6 behind main): the resolver this PR's title advertises already lives on the base branch, and #2197 (base main) ships the identical content to main — this PR delivers no code anywhere.

That matches the corrected PR body and @john-the-dev's dismissal condition verbatim ("if the retarget leaves no meaningful child-only diff, close this as superseded instead of merging an empty layer"). Verdict: do not merge — close as superseded once #2197 lands on main.

On the owner's active CHANGES_REQUESTED: both asks appear substantively addressed — sibling readers routed via commit 708a6e79 (browser-tools + recording-tools through resolveCredential('gemini-voice')), and the 7/25 S1–S4 before/after transcript covers the behavior evidence (though it lives in comments, not the body). That state remains hers to clear, as is the close decision.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment rather than a second CHANGES_REQUESTED, since the PR is already blocked and stacking a duplicate verdict adds noise rather than information.

The three added comment lines are accurate — resolveCredential('gemini-voice') is already the call at all three sites on the base, so documenting the managed → GEMINI_VOICE_API_KEY → GEMINI_API_KEY precedence describes real behavior rather than aspirational behavior. No objection to the content.

The problem is scope versus title. This is titled feat(credential): capability resolver — managed-tier file contract + voice pilot (G8) and the entire diff is three comment lines in two files. There is no resolver, no file contract, and no pilot in it. Two concrete costs:

  1. A reviewer opens it expecting the G8 resolver and has to reconstruct that the implementation landed elsewhere — I did exactly that before checking the diff stat.
  2. feat(...) in the subject means this shows up in changelog/release tooling as a feature that ships nothing.

Either retitle to docs(credential): note the G8 resolver precedence at the three gemini-voice call sites, or fold these lines into whichever PR introduces the resolver so the comments land with the code they describe. The latter is better — a comment explaining a precedence chain is much easier to review next to the chain.

Per CONTRIBUTING's single-concern rule this is already a clean single concern; it's only the label that's wrong.

@rick

rick commented Jul 27, 2026

Copy link
Copy Markdown
jugglar

@rick rick mentioned this pull request Jul 27, 2026
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Author position, so this stops looking like a PR waiting on work: @liususan091219 is right, and I verified it rather than taking it. Close as superseded once #2197 lands.

I checked the "delivers no code" claim myself against this PR's own base:

src/browser-tools.ts     +2/-0
src/recording-tools.ts   +1/-0

All three additions are comment lines (// Via the G8 resolver: managed tier → GEMINI_VOICE_API_KEY → GEMINI_API_KEY). The resolveCredential('gemini-voice') calls that this PR's title advertises show up in the diff as context lines, not additions — they already live on qingyun-dev. So there is no child-only code layer here, which matches @john-the-dev's dismissal condition verbatim.

What I am not claiming: that the work is gone. The resolver content ships to main via #2197, which is where it needs to land. This PR is the empty remainder after the retarget, not the work itself. #2197 is currently awaiting re-review (I pushed the managed-tier probe regression to it earlier), so the precondition for closing here is not met yet.

Two things I am deliberately leaving alone:

  • The 2026-07-16 CHANGES_REQUESTED is yours to clear, @liususan091219 — not something an approval from another reviewer can override, and not something I should route around. Your read that both asks are substantively addressed matches mine (sibling readers routed via 708a6e79; the S1–S4 before/after transcript is in-thread rather than in the body — my omission, and I would rather note it than quietly move it).
  • The close itself. I have not closed my own PR here, because the standing convention on this repo is that these decisions belong to a human, and the order matters: closing before refactor(credentials): route remaining TS GEMINI_* readers through the G8 resolver #2197 merges would drop the visible trail while the content is still in flight.

Happy to close it the moment #2197 is on main, or for whoever gets there first to do it.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Local verification — CI on this base cannot typecheck the change.

Base is qingyun-dev, which does not trigger ci.yml, so only smoke, recheck and license/cla ran. The diff is +3 lines of TypeScript that were never tsc'd or eslinted by any check on this PR.

The risk that carries is a contract break invisible to every green tick here, so I verified the contract statically on head 17bff23:

src/credential-resolver.ts                    present (3159 B)
export function resolveCredential(...)        line 68
export interface ResolvedCredential { key: string }   line 38
export type Capability = 'gemini-voice' | 'gemini-text'   line 33
callers: src/browser-tools.ts (3 refs), src/recording-tools.ts (2 refs)

So the module exists on this branch, the imported symbol is exported, 'gemini-voice' is a declared Capability rather than a free-form string, and .key is a string on the returned type — which is what resolveCredential('gemini-voice').key requires.

Stating the limit rather than overclaiming: this is a static contract check, not a full typecheck. A real tsc --noEmit needs a dependency install this branch's CI never performs, and I have not run one. It clears the failure modes that would actually break at runtime here (missing module, wrong export name, invalid capability, missing field); it would not catch a subtler type incompatibility. If you want the stronger signal before merge, the durable fix is retargeting this PR onto a CI-triggering base rather than trusting either tick.

@qingyun-wu qingyun-wu changed the title feat(credential): capability resolver — managed-tier file contract + voice pilot (G8) docs(credential): note the G8 resolver precedence at the three gemini-voice call sites Aug 2, 2026
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by #2197, per the record: the resolver + tests migrated into #2197 during the July merge-resolution churn (see the 07-26 stack-metadata correction), the reviewer's 07-26 'do not merge' stands, the 07-30 shared-login comment already recommended close-as-superseded, and this PR's base (qingyun-dev) is a dead integration branch aimed at nothing mergeable. All live G8 work continues on #2197 (which itself needs fresh re-approvals — its three APPROVEDs predate the last authored commit). Reopenable if the qingyun-dev base ever revives, which nothing currently suggests.

@qingyun-wu qingyun-wu closed this Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

request-change Has an active CHANGES_REQUESTED review security Security vulnerabilities and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants