Skip to content

fix(flows): keep gateway tokens fresh across flow execution - #500

Closed
albertoperdomo2 wants to merge 3 commits into
mainfrom
fix/flow-gateway-token-refresh
Closed

albertoperdomo2 wants to merge 3 commits into
mainfrom
fix/flow-gateway-token-refresh

Conversation

@albertoperdomo2

@albertoperdomo2 albertoperdomo2 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Multi-step flows fail mid-run with APIError: Unauthorized: invalid_token from the internal provider gateway. Earlier steps succeed; a later step is rejected once the gateway token — TTL 900s by default (ARCHE_GATEWAY_TOKEN_TTL_SECONDS) — expires.

Two failure shapes, both observed on 2026-08-31 (Codebase Hunter flow runs):

  1. Expiry between steps. The runner syncs provider access once at flow start; a 17-minute run's token expired before the final step.
  2. Partially aged token at start. A flow starting 7 minutes after an interactive visit skipped re-syncing (shouldRefreshProviderAccess threshold) and inherited a token with <8 min of TTL left, which expired mid-run.

During execution tokens were never refreshed, and invalid_token was absent from isRetryableFlowRunError, making expiry a terminal run failure even though retries resume from the failed node and re-sync on the way in.

Changes

  • Forced refresh at flow startensureProviderAccessFreshForExecution gains force, which bypasses only the freshness-age skip; the provider-sync lock, credential-hash check, and active-run deferral still apply. Threaded through ensureWorkspaceRunningForExecution(slug, userId, { forceProviderRefresh: true }) from both flow entry points (executeClaimedFlowRun, resumeClaimedFlowRun). Only the already-running path honors the flag — a freshly started workspace was just synced.
  • Step-boundary refreshexecuteFlowNodes calls ensureProviderAccessFreshForExecution before each node, after the cancellation and lease checks. At a boundary the flow's own message run is finalized, so the deferral fires only for unrelated active runs — exactly when a concurrent sync (which disposes the OpenCode instance) must not abort in-flight generation. A failed boundary refresh logs a warning and does not fail the run; genuine auth failures still surface from the step itself.
  • Retryable auth failuresinvalid_token added to isRetryableFlowRunError, so an expiry that slips through costs one step re-run after backoff instead of failing the run.

Security posture

No new token class and no TTL/claims/issuance changes: tokens stay short-lived and scoped per user, workspace, provider, and credential version. Refreshes serialize through the existing per-slug sync lock, and force is set only by server-side flow paths, never from user input.

OpenSpec

Adds flow-gateway-token-refresh change with a flow-execution spec delta (start-of-run freshness, step-boundary freshness, retryable gateway auth failures). openspec validate flow-gateway-token-refresh --strict passes.

Test plan

  • providers.test.ts — forced refresh runs despite a fresh matching sync record; forced refresh still defers while the workspace has active runs
  • session-execution.test.tsforceProviderRefresh threaded through as force: true on the running path
  • runner.test.ts — flow start passes the force flag; one refresh per node in a two-node flow; run still succeeds when the boundary refresh rejects
  • retry-policy.test.tsAPIError: Unauthorized: invalid_token classified retryable
  • pnpm exec vitest run — 146 tests across the touched and transitively affected suites pass; eslint clean on changed files
  • CI green

Non-goals / follow-up

Mid-step refresh without the instance dispose (disposeInstance: false already exists) is left for a follow-up: it only helps if OpenCode re-reads auth keys per request instead of caching them at provider creation — needs one verification against a live instance. Operators running long single steps can raise ARCHE_GATEWAY_TOKEN_TTL_SECONDS above the longest expected step in the meantime.

Depends on #497 for scheduled flows to trigger at all on archectl deployments, but is independently correct for manual runs and the Ansible path.

Multi-step flows failed mid-run with `APIError: Unauthorized:
invalid_token` from the internal provider gateway: the runner synced
provider access exactly once before the flow started, gateway tokens
carry a short TTL (900s default), and a flow could also inherit a
partially aged token when it started shortly after interactive
activity. `invalid_token` was not retryable, so expiry failed the run
terminally.

- Force a provider-access refresh at flow start (`force` flag on
  `ensureProviderAccessFreshForExecution`, threaded through
  `ensureWorkspaceRunningForExecution`) so flows start with a full-TTL
  token; the credential-hash check and active-run deferral still apply.
- Refresh provider access before each flow node in `executeFlowNodes`.
  At a step boundary the flow's own message run is finalized, so this
  only defers when an unrelated run is active — the case where a
  concurrent sync (which disposes the instance) must not abort
  in-flight generation. A failed boundary refresh warns and continues.
- Classify `invalid_token` as retryable so an expiry that survives the
  above costs one step re-run after backoff; the retry re-syncs and
  resumes from the failed node.

No changes to token TTL, claims, or issuance. Mid-step refresh without
dispose is deliberately left for a follow-up pending verification that
OpenCode re-reads auth keys per request.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@albertoperdomo2

Copy link
Copy Markdown
Contributor Author

/build

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

📦 PR Workspace Image Built Successfully

Default (amd64):

ghcr.io/peaberry-studio/arche/workspace:pr-500

Optional arm64:

ghcr.io/peaberry-studio/arche/workspace:pr-500-arm64

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

📦 PR Image Built Successfully

Default (amd64):

ghcr.io/peaberry-studio/arche/web:pr-500

Optional arm64:

ghcr.io/peaberry-studio/arche/web:pr-500-arm64

@albertoperdomo2

Copy link
Copy Markdown
Contributor Author

/rerun

The step-boundary refresh used the freshness-threshold check, which is
tuned for interactive cadence: it only fires once the sync is older
than TTL minus the 60s skew. Flow steps run for minutes, so boundaries
rarely land in that window — an 11-minute step started with 10 minutes
of token life left and still failed at expiry (2026-09-02 11:07 run),
defeating the between-steps refresh for exactly the flows it was built
for.

Force the refresh before every step after the first, so each step
starts with the full gateway-token TTL; mid-step expiry now requires a
single step longer than the TTL, which the retryable classification
bounds to one step re-run. The first iteration stays exempt because
the run entry points already force a fresh sync before the loop.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@albertoperdomo2

Copy link
Copy Markdown
Contributor Author

/build

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

📦 PR Workspace Image Built Successfully

Default (amd64):

ghcr.io/peaberry-studio/arche/workspace:pr-500

Optional arm64:

ghcr.io/peaberry-studio/arche/workspace:pr-500-arm64

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

📦 PR Image Built Successfully

Default (amd64):

ghcr.io/peaberry-studio/arche/web:pr-500

Optional arm64:

ghcr.io/peaberry-studio/arche/web:pr-500-arm64

@albertoperdomo2

Copy link
Copy Markdown
Contributor Author

Superseded by #478, which absorbs the token-freshness work — forced refresh at flow start, forced between-steps refresh (the threshold check skips for multi-minute steps), and retryable invalid_token — along with the stop/recovery and archectl fixes. Closing per the umbrella plan.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant