Repository navigation
Description
[Frontier] [MODEL15-03] Renew route credentials before listing so a fresh process survives token expiry
Parent
S15 — #316. Follow-up of MODEL15-02 (#372, PR #376).
Model tier
Frontier: cross-repository contract adoption on the credential/renewal path that gates every MODEL15 step.
Problem (from the Schway coordinator's #376 review, finding A1/B1)
PR #376 caches listed catalog choices per process, so a running process survives access-token expiry. A process that STARTS after expiry still fails: listRouteModels never renews and returns needs-login (route_needs_login), and createModel({ catalogModel }) needs a listed choice. Schway now provides inference-free renewal.
Upstream delivery (verified)
chayapats/schway#455 merged via #457 at aac2fc0afdfe53f0a5076cb9d3edfaf33d280581 (CI x64 + ARM64 6,107 pass). Artifact /Users/Shared/schway-followups-a.V8iI/schway-1.0.0-rc.2-aac2fc0.tgz, SHA-256 2801a5a4a322faabba276a0f607f39027784e2e1e73b720937c77b0f4f0027ae (verified by the orchestrator; tree = Schway main aac2fc0). Version stays 1.0.0-rc.2; pin by commit. Additive contract under schway.host.release/v2, capability routeRenewalContract: "schway.host.route-renewal/v1":
HostAuthBinding.renew(options?: HostRouteRenewalOptions): Promise<HostRouteRenewal>
renewAuthRoute(selection: Omit<HostAuthSelection, "model">, options?: HostRouteRenewalOptions): Promise<HostRouteRenewal>
interface HostRouteRenewalOptions { readonly signal?: AbortSignal; readonly deadlineMs?: number } // 1–86,400,000
interface HostRouteRenewal { readonly outcome: "fresh" | "renewed" | "failed" | "cancelled" | "limit_exceeded"; readonly renewalRequests: 0 | 1; readonly auth: Omit<HostAuthReadiness, "model"> }At most one token request, only when the stored access token is expired or inside the refresh skew; never inference/catalog/API-key requests; shares the write guard, lifecycle, single-flight renewal, fencing and counting with route runs. Decide on outcome, not auth.status, when a renewal stops. A lifecycle close() returns busy while a sent renewal settles; retry it (bounded ~30 s + store write). Recommended flow: one createHostCredentialLifecycle() shared by createHost and listRouteModels; renew() (or renewAuthRoute) before listing; then list; then createModel({ catalogModel, effort: { level, mode: 'exact' }, authorizeDispatch }); re-list after sign-in/logout, invalid-input, or provider rejection.
Deliverable
Jit vendors aac2fc0, shares one credential lifecycle between the MODEL15 host and listing, renews before an uncached listing (inside the existing listChoices seam), maps renewal outcomes to honest owner-visible codes, counts the renewal request in usage/evidence, and closes the lifecycle correctly on shutdown (retrying busy).
Acceptance criteria
- Vendored artifact matches SHA-256
2801a5a4…0f4f0027ae; only publicschway/hostimports. - A fresh process with an expired access token renews once (no inference, no catalog before renewal), lists, and completes the step.
- A valid token renews nothing (
fresh, 0 requests);needs-login/rejected/rate-limited/unreachable renewals surface distinct codes; a cancelled renewal reportscancelledand sends no inference. - Renewals and runs on one lifecycle share one token request (counted once).
- Shutdown retries lifecycle
close()onbusywithin its bound so a rotated token is not lost. - docs/contracts/s15-model-selection-v1.md and the pin lines updated; the "fresh process after expiry needs schway#455" limit removed.
Tests and evidence
- Fixture token with past
expiresAtin a fresh service → one renewal, then listing, then a completed step;providerRequests.renewalRequests= 1. - Valid token →
fresh, zero token requests. - Renewal
needs-login/rejected→ distinct codes, zero inference. - Cancellation before/after the token request → no inference; outcome mapping per contract.
- Shared lifecycle: concurrent renew + run → one token request.
- Packed-consumer suite against the new tarball.
Requirements
FR-02/13/14/18/21/27/31; NFR-02/03/05/06/09. Acceptance: S15-AC14.
Blocked by
None (schway#455 merged).
Do not
Do not import Schway internals; do not send inference to renew; do not run Jit's full suite locally (CI runs it); do not add AI attribution; do not merge the PR yourself.
Definition of done
Locally: typecheck, git diff --check, focused model/conversation/packed-consumer tests. CI runs the full suite. PR body includes Closes #381.
Activity
fix(s15): share one credential lifecycle across every composed route …
fix(s15): keep cumulative renewal counts across re-lists and cover re…
fix(s15): fence credential writes by route ownership and bound lifecy…
test(s15): cover rotated-grant shutdown, cached re-list, Codex and jo…