fix: refresh provider tokens proactively and update git URLs on resume (#11296)

This commit is contained in:
Alona 2025-10-21 14:19:08 -04:00 committed by GitHub
parent 49f360d021
commit 267528fa82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 123 additions and 27 deletions

View File

@ -5,3 +5,6 @@ DOCKER_IMAGE=runtime
# These variables will be appended by the runtime_build.py script
# DOCKER_IMAGE_TAG=
# DOCKER_IMAGE_SOURCE_TAG=
DOCKER_IMAGE_TAG=oh_v0.59.0_image_nikolaik_s_python-nodejs_tag_python3.12-nodejs22
DOCKER_IMAGE_SOURCE_TAG=oh_v0.59.0_cwpsf0pego28lacp_p73ruf86qxiulkou

View File

@ -293,11 +293,12 @@ class TokenManager:
refresh_token_expires_at: int,
) -> dict[str, str | int] | None:
current_time = int(time.time())
# expire access_token ten minutes before actual expiration
# expire access_token four hours before actual expiration
# This ensures tokens are refreshed on resume to have at least 4 hours validity
access_expired = (
False
if access_token_expires_at == 0
else access_token_expires_at < current_time + 600
else access_token_expires_at < current_time + 14400
)
refresh_expired = (
False

View File

@ -4,6 +4,7 @@ import copy
import json
import os
import random
import shlex
import shutil
import string
import tempfile
@ -447,8 +448,12 @@ class Runtime(FileEditRuntimeMixin):
)
openhands_workspace_branch = f'openhands-workspace-{random_str}'
repo_path = self.workspace_root / dir_name
quoted_repo_path = shlex.quote(str(repo_path))
quoted_remote_repo_url = shlex.quote(remote_repo_url)
# Clone repository command
clone_command = f'git clone {remote_repo_url} {dir_name}'
clone_command = f'git clone {quoted_remote_repo_url} {quoted_repo_path}'
# Checkout to appropriate branch
checkout_command = (
@ -461,11 +466,35 @@ class Runtime(FileEditRuntimeMixin):
await call_sync_from_async(self.run_action, clone_action)
cd_checkout_action = CmdRunAction(
command=f'cd {dir_name} && {checkout_command}'
command=f'cd {quoted_repo_path} && {checkout_command}'
)
action = cd_checkout_action
self.log('info', f'Cloning repo: {selected_repository}')
await call_sync_from_async(self.run_action, action)
if remote_repo_url:
set_remote_action = CmdRunAction(
command=(
f'cd {quoted_repo_path} && '
f'git remote set-url origin {quoted_remote_repo_url}'
)
)
obs = await call_sync_from_async(self.run_action, set_remote_action)
if isinstance(obs, CmdOutputObservation) and obs.exit_code == 0:
self.log(
'info',
f'Set git remote origin to authenticated URL for {selected_repository}',
)
else:
self.log(
'warning',
(
'Failed to set git remote origin while ensuring fresh token '
f'for {selected_repository}: '
f'{obs.content if isinstance(obs, CmdOutputObservation) else "unknown error"}'
),
)
return dir_name
def maybe_run_setup_script(self):

View File

@ -8,7 +8,11 @@ from openhands.core.config import OpenHandsConfig
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
from openhands.events.action import Action
from openhands.events.action.commands import CmdRunAction
from openhands.events.observation import NullObservation, Observation
from openhands.events.observation import (
CmdOutputObservation,
NullObservation,
Observation,
)
from openhands.events.stream import EventStream
from openhands.integrations.provider import ProviderHandler, ProviderToken, ProviderType
from openhands.integrations.service_types import AuthenticationError, Repository
@ -73,6 +77,36 @@ class MockRuntime(Runtime):
def run_action(self, action: Action) -> Observation:
self.run_action_calls.append(action)
# Return a mock git remote URL for git remote get-url commands
# Use an OLD token to simulate token refresh scenario
if (
isinstance(action, CmdRunAction)
and 'git remote get-url origin' in action.command
):
# Extract provider from previous clone command
if len(self.run_action_calls) > 0:
clone_cmd = (
self.run_action_calls[0].command if self.run_action_calls else ''
)
if 'github.com' in clone_cmd:
mock_url = 'https://old_github_token@github.com/owner/repo.git'
elif 'gitlab.com' in clone_cmd:
mock_url = (
'https://oauth2:old_gitlab_token@gitlab.com/owner/repo.git'
)
else:
mock_url = 'https://github.com/owner/repo.git'
return CmdOutputObservation(
content=mock_url, command_id=-1, command='', exit_code=0
)
# Return success for git remote set-url commands
if (
isinstance(action, CmdRunAction)
and 'git remote set-url origin' in action.command
):
return CmdOutputObservation(
content='', command_id=-1, command='', exit_code=0
)
return NullObservation(content='')
def call_tool_mcp(self, action):
@ -330,22 +364,29 @@ async def test_clone_or_init_repo_github_with_token(temp_dir, monkeypatch):
result = await runtime.clone_or_init_repo(git_provider_tokens, 'owner/repo', None)
# Verify that git clone and checkout were called as separate commands
assert len(runtime.run_action_calls) == 2
# Verify that git clone, checkout, and git remote URL update were called
assert len(runtime.run_action_calls) == 3 # clone, checkout, set-url
assert isinstance(runtime.run_action_calls[0], CmdRunAction)
assert isinstance(runtime.run_action_calls[1], CmdRunAction)
assert isinstance(runtime.run_action_calls[2], CmdRunAction)
# Check that the first command is the git clone with the correct URL format with token
clone_cmd = runtime.run_action_calls[0].command
assert (
f'git clone https://{github_token}@github.com/owner/repo.git repo' in clone_cmd
)
assert f'https://{github_token}@github.com/owner/repo.git' in clone_cmd
expected_repo_path = str(runtime.workspace_root / 'repo')
assert expected_repo_path in clone_cmd
# Check that the second command is the checkout
checkout_cmd = runtime.run_action_calls[1].command
assert 'cd repo' in checkout_cmd
assert f'cd {expected_repo_path}' in checkout_cmd
assert 'git checkout -b openhands-workspace-' in checkout_cmd
# Check that the third command sets the remote URL immediately after clone
set_url_cmd = runtime.run_action_calls[2].command
assert f'cd {expected_repo_path}' in set_url_cmd
assert 'git remote set-url origin' in set_url_cmd
assert github_token in set_url_cmd
assert result == 'repo'
@ -363,20 +404,28 @@ async def test_clone_or_init_repo_github_no_token(temp_dir, monkeypatch):
mock_repo_and_patch(monkeypatch, provider=ProviderType.GITHUB)
result = await runtime.clone_or_init_repo(None, 'owner/repo', None)
# Verify that git clone and checkout were called as separate commands
assert len(runtime.run_action_calls) == 2
# Verify that git clone, checkout, and remote update were called
assert len(runtime.run_action_calls) == 3 # clone, checkout, set-url
assert isinstance(runtime.run_action_calls[0], CmdRunAction)
assert isinstance(runtime.run_action_calls[1], CmdRunAction)
assert isinstance(runtime.run_action_calls[2], CmdRunAction)
# Check that the first command is the git clone with the correct URL format without token
clone_cmd = runtime.run_action_calls[0].command
assert 'git clone https://github.com/owner/repo.git repo' in clone_cmd
expected_repo_path = str(runtime.workspace_root / 'repo')
assert 'git clone https://github.com/owner/repo.git' in clone_cmd
assert expected_repo_path in clone_cmd
# Check that the second command is the checkout
checkout_cmd = runtime.run_action_calls[1].command
assert 'cd repo' in checkout_cmd
assert f'cd {expected_repo_path}' in checkout_cmd
assert 'git checkout -b openhands-workspace-' in checkout_cmd
# Check that the third command sets the remote URL after clone
set_url_cmd = runtime.run_action_calls[2].command
assert f'cd {expected_repo_path}' in set_url_cmd
assert 'git remote set-url origin' in set_url_cmd
assert result == 'repo'
@ -403,23 +452,29 @@ async def test_clone_or_init_repo_gitlab_with_token(temp_dir, monkeypatch):
result = await runtime.clone_or_init_repo(git_provider_tokens, 'owner/repo', None)
# Verify that git clone and checkout were called as separate commands
assert len(runtime.run_action_calls) == 2
# Verify that git clone, checkout, and git remote URL update were called
assert len(runtime.run_action_calls) == 3 # clone, checkout, set-url
assert isinstance(runtime.run_action_calls[0], CmdRunAction)
assert isinstance(runtime.run_action_calls[1], CmdRunAction)
assert isinstance(runtime.run_action_calls[2], CmdRunAction)
# Check that the first command is the git clone with the correct URL format with token
clone_cmd = runtime.run_action_calls[0].command
assert (
f'git clone https://oauth2:{gitlab_token}@gitlab.com/owner/repo.git repo'
in clone_cmd
)
expected_repo_path = str(runtime.workspace_root / 'repo')
assert f'https://oauth2:{gitlab_token}@gitlab.com/owner/repo.git' in clone_cmd
assert expected_repo_path in clone_cmd
# Check that the second command is the checkout
checkout_cmd = runtime.run_action_calls[1].command
assert 'cd repo' in checkout_cmd
assert f'cd {expected_repo_path}' in checkout_cmd
assert 'git checkout -b openhands-workspace-' in checkout_cmd
# Check that the third command sets the remote URL immediately after clone
set_url_cmd = runtime.run_action_calls[2].command
assert f'cd {expected_repo_path}' in set_url_cmd
assert 'git remote set-url origin' in set_url_cmd
assert gitlab_token in set_url_cmd
assert result == 'repo'
@ -437,18 +492,24 @@ async def test_clone_or_init_repo_with_branch(temp_dir, monkeypatch):
mock_repo_and_patch(monkeypatch, provider=ProviderType.GITHUB)
result = await runtime.clone_or_init_repo(None, 'owner/repo', 'feature-branch')
# Verify that git clone and checkout were called as separate commands
assert len(runtime.run_action_calls) == 2
# Verify that git clone, checkout, and remote update were called
assert len(runtime.run_action_calls) == 3 # clone, checkout, set-url
assert isinstance(runtime.run_action_calls[0], CmdRunAction)
assert isinstance(runtime.run_action_calls[1], CmdRunAction)
assert isinstance(runtime.run_action_calls[2], CmdRunAction)
# Check that the first command is the git clone
clone_cmd = runtime.run_action_calls[0].command
expected_repo_path = str(runtime.workspace_root / 'repo')
assert 'git clone https://github.com/owner/repo.git' in clone_cmd
assert expected_repo_path in clone_cmd
# Check that the second command contains the correct branch checkout
checkout_cmd = runtime.run_action_calls[1].command
assert 'git clone https://github.com/owner/repo.git repo' in clone_cmd
assert 'cd repo' in checkout_cmd
assert f'cd {expected_repo_path}' in checkout_cmd
assert 'git checkout feature-branch' in checkout_cmd
set_url_cmd = runtime.run_action_calls[2].command
assert f'cd {expected_repo_path}' in set_url_cmd
assert 'git remote set-url origin' in set_url_cmd
assert 'git checkout -b' not in checkout_cmd # Should not create a new branch
assert result == 'repo'

View File

@ -14,7 +14,9 @@ from openhands.integrations.provider import (
ProviderToken,
ProviderType,
)
from openhands.server.routes.secrets import app as secrets_app
from openhands.server.routes.secrets import (
app as secrets_app,
)
from openhands.storage import get_file_store
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.secrets.file_secrets_store import FileSecretsStore