Skip to content

[Frontier] [MODEL15-03] Renew route credentials before listing so a fresh process survives token expiry #381

Closed

Description

@chayapats

[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 public schway/host imports.
  • 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 reports cancelled and sends no inference.
  • Renewals and runs on one lifecycle share one token request (counted once).
  • Shutdown retries lifecycle close() on busy within 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

  1. Fixture token with past expiresAt in a fresh service → one renewal, then listing, then a completed step; providerRequests.renewalRequests = 1.
  2. Valid token → fresh, zero token requests.
  3. Renewal needs-login/rejected → distinct codes, zero inference.
  4. Cancellation before/after the token request → no inference; outcome mapping per contract.
  5. Shared lifecycle: concurrent renew + run → one token request.
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentImplementation-ready; start only when required blockers and ownership checks pass.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions