Skip to content

Runner refresh exchange always 400s; keep a renewable credential for long-running apps #39

Open

Description

@birme

Source: nordiccare report (Slack #C0BS2FZ1YE7, 2026-11-05): "the app's token expires after an hour". Investigation shows the premise differs from what the entrypoint intends.

Not a regression, but a blocker for the hardening

Nothing changed recently. The header-only call has never matched what POST /runner-token/refresh expects (JSON body required since the endpoint was added on 2026-02-25; the runner block predates 2026-06-08, per git history, not re-run against old versions). It stays invisible because || true swallows the failure and the 365-day runner-refresh token stays in OSC_ACCESS_TOKEN.

This must be fixed before REJECT_RUNNER_REFRESH_TOKENS is enabled (token-service #116, default false, not set in infra-osaas). Once it is on, apps and sibling runners that rely on that token stop working. Blocks: enabling that flag in prod.

Findings (from code reading; the 400/401 was probed against dev and prod with a junk token)

  • The OSC_ACCESS_TOKEN a python My App receives is a runner-refresh token (type: runner-refresh, scope config-access, 365 days; osaas-lib-token src/pat/token.ts:48-73).
  • scripts/docker-entrypoint.sh:214-218 tries to exchange it at POST token.svc/runner-token/refresh for a 1h PAT, but sends the token only in an x-pat-jwt header. The endpoint requires a JSON body {"token": ...} (token-service src/model.ts:29-31): header only gives 400 body must be object.
  • curl -sf ... || true swallows the failure, so the 365-day token stays in the env var. Apps work today by accident; they depend on REJECT_RUNNER_REFRESH_TOKENS staying false (osaas-deploy-manager src/server.ts:219).
  • Sibling runners: wasm-runner (:128-130) has the same bug; golang-runner (:131-133) and dotnet-runner (:137-139) send a Bearer header with no body and also fail; web-runner (:303-305) and php-runner (:116-118) are correct.

Change (contract change, needs birme approval before merge)

Fixing the body alone turns today's 365-day env token into a 1h PAT and breaks any app using it after an hour. The two parts must ship together:

  1. POST {"token": $OSC_ACCESS_TOKEN}, as web-runner does.
  2. Keep the original token available as OSC_REFRESH_TOKEN so the app can call /runner-token/refresh itself; document with a Python snippet.
  3. Log a [CONFIG] warning on a failed exchange instead of || true.

Acceptance criteria

  • Items 1-3 above, in one PR
  • Tests for the success and failure paths
  • Consumer audit for apps reading OSC_ACCESS_TOKEN, and Contract change approved by birme: <url> on this ticket before merge
  • Sibling tickets filed for wasm-, golang- and dotnet-runner (linked below)

What a human does next

Nothing.

Activity

  1. self-assigned this
    on Nov 6, 2026
  2. birme commented on Nov 6, 2026

    @birme
    ContributorAuthor

    Sibling added for python-job-runner: Eyevinn/python-job-runner#6 (the audit's seventh runner; wasm, golang, dotnet are #37, #32, #31).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions