mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix: refresh provider tokens proactively and update git URLs on resume (#11296)
This commit is contained in:
parent
49f360d021
commit
267528fa82
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user