Skip to content

Commit feb04dc

Browse files
Plumb custom secrets to runtime (All-Hands-AI#8330)
Co-authored-by: openhands <[email protected]>
1 parent 1f82717 commit feb04dc

File tree

17 files changed

+246
-13
lines changed

17 files changed

+246
-13
lines changed

frontend/src/state/chat-slice.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ export const chatSlice = createSlice({
212212
content += `\n\n- ${host} (port ${port})`;
213213
}
214214
}
215+
if (
216+
recallObs.extras.custom_secrets_descriptions &&
217+
Object.keys(recallObs.extras.custom_secrets_descriptions).length > 0
218+
) {
219+
content += `\n\n**Custom Secrets**`;
220+
for (const [name, description] of Object.entries(
221+
recallObs.extras.custom_secrets_descriptions,
222+
)) {
223+
content += `\n\n- $${name}: ${description}`;
224+
}
225+
}
215226
if (recallObs.extras.repo_instructions) {
216227
content += `\n\n**Repository Instructions:**\n\n${recallObs.extras.repo_instructions}`;
217228
}

frontend/src/types/core/observations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export interface RecallObservation extends OpenHandsObservationEvent<"recall"> {
123123
repo_directory?: string;
124124
repo_instructions?: string;
125125
runtime_hosts?: Record<string, number>;
126+
custom_secrets_descriptions?: Record<string, string>;
126127
additional_agent_instructions?: string;
127128
date?: string;
128129
microagent_knowledge?: MicroagentKnowledge[];

openhands/agenthub/codeact_agent/prompts/additional_info.j2

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ At the user's request, repository {{ repository_info.repo_name }} has been clone
88
{{ repository_instructions }}
99
</REPOSITORY_INSTRUCTIONS>
1010
{% endif %}
11-
{% if runtime_info and (runtime_info.available_hosts or runtime_info.additional_agent_instructions) -%}
11+
{% if runtime_info -%}
1212
<RUNTIME_INFORMATION>
1313
{% if runtime_info.available_hosts %}
1414
The user has access to the following hosts for accessing a web application,
@@ -24,6 +24,14 @@ For example, if you are using vite.config.js, you should set server.host and ser
2424
{% if runtime_info.additional_agent_instructions %}
2525
{{ runtime_info.additional_agent_instructions }}
2626
{% endif %}
27+
{% if runtime_info.custom_secrets_descriptions %}
28+
<CUSTOM_SECRETS>
29+
You are have access to the following environment variables
30+
{% for secret_name, secret_description in runtime_info.custom_secrets_descriptions.items() %}
31+
* $**{{ secret_name }}**: {{ secret_description }}
32+
{% endfor %}
33+
</CUSTOM_SECRETS>
34+
{% endif %}
2735
{% if runtime_info.date %}
2836
Today's date is {{ runtime_info.date }} (UTC).
2937
{% endif %}

openhands/core/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def create_memory(
154154

155155
if runtime:
156156
# sets available hosts
157-
memory.set_runtime_info(runtime)
157+
memory.set_runtime_info(runtime, {})
158158

159159
# loads microagents from repo/.openhands/microagents
160160
microagents: list[BaseMicroagent] = runtime.get_microagents_from_selected_repo(

openhands/events/observation/agent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class RecallObservation(Observation):
7474
runtime_hosts: dict[str, int] = field(default_factory=dict)
7575
additional_agent_instructions: str = ''
7676
date: str = ''
77+
custom_secrets_descriptions: dict[str, str] = field(default_factory=dict)
7778

7879
# knowledge
7980
microagent_knowledge: list[MicroagentKnowledge] = field(default_factory=list)
@@ -114,7 +115,8 @@ def __str__(self) -> str:
114115
f'repo_instructions={self.repo_instructions[:20]}...',
115116
f'runtime_hosts={self.runtime_hosts}',
116117
f'additional_agent_instructions={self.additional_agent_instructions[:20]}...',
117-
f'date={self.date}',
118+
f'date={self.date}'
119+
f'custom_secrets_descriptions={self.custom_secrets_descriptions}',
118120
]
119121
)
120122
else:

openhands/memory/conversation_memory.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,13 @@ def _process_observation(
451451
available_hosts=obs.runtime_hosts,
452452
additional_agent_instructions=obs.additional_agent_instructions,
453453
date=date,
454+
custom_secrets_descriptions=obs.custom_secrets_descriptions,
454455
)
455456
else:
456-
runtime_info = RuntimeInfo(date=date)
457+
runtime_info = RuntimeInfo(
458+
date=date,
459+
custom_secrets_descriptions=obs.custom_secrets_descriptions,
460+
)
457461

458462
repo_instructions = (
459463
obs.repo_instructions if obs.repo_instructions else ''

openhands/memory/memory.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ def _on_workspace_context_recall(
176176
microagent_knowledge=microagent_knowledge,
177177
content='Added workspace context',
178178
date=self.runtime_info.date if self.runtime_info is not None else '',
179+
custom_secrets_descriptions=self.runtime_info.custom_secrets_descriptions
180+
if self.runtime_info is not None
181+
else {},
179182
)
180183
return obs
181184
return None
@@ -266,7 +269,9 @@ def set_repository_info(self, repo_name: str, repo_directory: str) -> None:
266269
else:
267270
self.repository_info = None
268271

269-
def set_runtime_info(self, runtime: Runtime) -> None:
272+
def set_runtime_info(
273+
self, runtime: Runtime, custom_secrets_descriptions: dict[str, str]
274+
) -> None:
270275
"""Store runtime info (web hosts, ports, etc.)."""
271276
# e.g. { '127.0.0.1': 8080 }
272277
utc_now = datetime.now(timezone.utc)
@@ -277,9 +282,12 @@ def set_runtime_info(self, runtime: Runtime) -> None:
277282
available_hosts=runtime.web_hosts,
278283
additional_agent_instructions=runtime.additional_agent_instructions,
279284
date=date,
285+
custom_secrets_descriptions=custom_secrets_descriptions,
280286
)
281287
else:
282-
self.runtime_info = RuntimeInfo(date=date)
288+
self.runtime_info = RuntimeInfo(
289+
date=date, custom_secrets_descriptions=custom_secrets_descriptions
290+
)
283291

284292
def send_error_message(self, message_id: str, message: str):
285293
"""Sends an error message if the callback function was provided."""

openhands/server/listen_socket.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ async def connect(connection_id: str, environ: dict) -> None:
100100
git_provider_tokens = user_secrets.provider_tokens
101101

102102
session_init_args['git_provider_tokens'] = git_provider_tokens
103+
if user_secrets:
104+
session_init_args['custom_secrets'] = user_secrets.custom_secrets
103105

104106
conversation_init_data = ConversationInitData(**session_init_args)
105107

openhands/server/routes/manage_conversations.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from openhands.core.logger import openhands_logger as logger
1010
from openhands.events.action.message import MessageAction
1111
from openhands.integrations.provider import (
12+
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA,
1213
PROVIDER_TOKEN_TYPE,
1314
ProviderHandler,
1415
)
@@ -35,6 +36,7 @@
3536
get_auth_type,
3637
get_provider_tokens,
3738
get_user_id,
39+
get_user_secrets,
3840
)
3941
from openhands.server.user_auth.user_auth import AuthType
4042
from openhands.server.utils import get_conversation_store
@@ -44,6 +46,7 @@
4446
ConversationTrigger,
4547
)
4648
from openhands.storage.data_models.conversation_status import ConversationStatus
49+
from openhands.storage.data_models.user_secrets import UserSecrets
4750
from openhands.utils.async_utils import wait_all
4851
from openhands.utils.conversation_summary import get_default_conversation_title
4952

@@ -73,6 +76,7 @@ class InitSessionResponse(BaseModel):
7376
async def _create_new_conversation(
7477
user_id: str | None,
7578
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
79+
custom_secrets: CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA | None,
7680
selected_repository: str | None,
7781
selected_branch: str | None,
7882
initial_user_msg: str | None,
@@ -114,6 +118,7 @@ async def _create_new_conversation(
114118

115119
session_init_args['git_provider_tokens'] = git_provider_tokens
116120
session_init_args['selected_repository'] = selected_repository
121+
session_init_args['custom_secrets'] = custom_secrets
117122
session_init_args['selected_branch'] = selected_branch
118123
conversation_init_data = ConversationInitData(**session_init_args)
119124
logger.info('Loading conversation store')
@@ -174,6 +179,7 @@ async def new_conversation(
174179
data: InitSessionRequest,
175180
user_id: str = Depends(get_user_id),
176181
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
182+
user_secrets: UserSecrets = Depends(get_user_secrets),
177183
auth_type: AuthType | None = Depends(get_auth_type),
178184
) -> InitSessionResponse:
179185
"""Initialize a new session or join an existing one.
@@ -209,6 +215,7 @@ async def new_conversation(
209215
agent_loop_info = await _create_new_conversation(
210216
user_id=user_id,
211217
git_provider_tokens=provider_tokens,
218+
custom_secrets=user_secrets.custom_secrets,
212219
selected_repository=repository,
213220
selected_branch=selected_branch,
214221
initial_user_msg=initial_user_msg,

openhands/server/session/agent_session.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
from openhands.events.action import ChangeAgentStateAction, MessageAction
1717
from openhands.events.event import Event, EventSource
1818
from openhands.events.stream import EventStream
19-
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
19+
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE, ProviderHandler
2020
from openhands.mcp import add_mcp_tools_to_agent
2121
from openhands.memory.memory import Memory
2222
from openhands.microagent.microagent import BaseMicroagent
2323
from openhands.runtime import get_runtime_cls
2424
from openhands.runtime.base import Runtime
2525
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
2626
from openhands.security import SecurityAnalyzer, options
27+
from openhands.storage.data_models.user_secrets import UserSecrets
2728
from openhands.storage.files import FileStore
2829
from openhands.utils.async_utils import EXECUTOR, call_sync_from_async
2930
from openhands.utils.shutdown_listener import should_continue
@@ -82,6 +83,7 @@ async def start(
8283
agent: Agent,
8384
max_iterations: int,
8485
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
86+
custom_secrets: CUSTOM_SECRETS_TYPE | None = None,
8587
max_budget_per_task: float | None = None,
8688
agent_to_llm_config: dict[str, LLMConfig] | None = None,
8789
agent_configs: dict[str, AgentConfig] | None = None,
@@ -113,13 +115,17 @@ async def start(
113115
self._started_at = started_at
114116
finished = False # For monitoring
115117
runtime_connected = False
118+
119+
custom_secrets_handler = UserSecrets(custom_secrets=custom_secrets if custom_secrets else {})
120+
116121
try:
117122
self._create_security_analyzer(config.security.security_analyzer)
118123
runtime_connected = await self._create_runtime(
119124
runtime_name=runtime_name,
120125
config=config,
121126
agent=agent,
122127
git_provider_tokens=git_provider_tokens,
128+
custom_secrets=custom_secrets,
123129
selected_repository=selected_repository,
124130
selected_branch=selected_branch,
125131
)
@@ -157,12 +163,16 @@ async def start(
157163
self.memory = await self._create_memory(
158164
selected_repository=selected_repository,
159165
repo_directory=repo_directory,
166+
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions()
160167
)
161168

162169
if git_provider_tokens:
163170
provider_handler = ProviderHandler(provider_tokens=git_provider_tokens)
164171
await provider_handler.set_event_stream_secrets(self.event_stream)
165172

173+
if custom_secrets:
174+
custom_secrets_handler.set_event_stream_secrets(self.event_stream)
175+
166176
if not self._closed:
167177
if initial_message:
168178
self.event_stream.add_event(initial_message, EventSource.USER)
@@ -264,6 +274,7 @@ async def _create_runtime(
264274
config: AppConfig,
265275
agent: Agent,
266276
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
277+
custom_secrets: CUSTOM_SECRETS_TYPE | None = None,
267278
selected_repository: str | None = None,
268279
selected_branch: str | None = None,
269280
) -> bool:
@@ -281,9 +292,11 @@ async def _create_runtime(
281292
if self.runtime is not None:
282293
raise RuntimeError('Runtime already created')
283294

295+
custom_secrets_handler = UserSecrets(custom_secrets=custom_secrets or {})
296+
env_vars = custom_secrets_handler.get_env_vars()
297+
284298
self.logger.debug(f'Initializing runtime `{runtime_name}` now...')
285299
runtime_cls = get_runtime_cls(runtime_name)
286-
287300
if runtime_cls == RemoteRuntime:
288301
self.runtime = runtime_cls(
289302
config=config,
@@ -294,15 +307,17 @@ async def _create_runtime(
294307
headless_mode=False,
295308
attach_to_existing=False,
296309
git_provider_tokens=git_provider_tokens,
310+
env_vars=env_vars,
297311
user_id=self.user_id,
298312
)
299313
else:
300314
provider_handler = ProviderHandler(
301315
provider_tokens=git_provider_tokens
302316
or cast(PROVIDER_TOKEN_TYPE, MappingProxyType({}))
303317
)
304-
env_vars = await provider_handler.get_env_vars(expose_secrets=True)
305-
318+
319+
# Merge git provider tokens with custom secrets before passing over to runtime
320+
env_vars.update(await provider_handler.get_env_vars(expose_secrets=True))
306321
self.runtime = runtime_cls(
307322
config=config,
308323
event_stream=self.event_stream,
@@ -400,7 +415,7 @@ def _create_controller(
400415
return controller
401416

402417
async def _create_memory(
403-
self, selected_repository: str | None, repo_directory: str | None
418+
self, selected_repository: str | None, repo_directory: str | None, custom_secrets_descriptions: dict[str, str]
404419
) -> Memory:
405420
memory = Memory(
406421
event_stream=self.event_stream,
@@ -410,7 +425,7 @@ async def _create_memory(
410425

411426
if self.runtime:
412427
# sets available hosts and other runtime info
413-
memory.set_runtime_info(self.runtime)
428+
memory.set_runtime_info(self.runtime, custom_secrets_descriptions)
414429

415430
# loads microagents from repo/.openhands/microagents
416431
microagents: list[BaseMicroagent] = await call_sync_from_async(

openhands/server/session/conversation_init_data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pydantic import Field
22

3-
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
3+
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE
44
from openhands.storage.data_models.settings import Settings
55

66

@@ -10,6 +10,7 @@ class ConversationInitData(Settings):
1010
"""
1111

1212
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = Field(default=None, frozen=True)
13+
custom_secrets: CUSTOM_SECRETS_TYPE | None = Field(default=None, frozen=True)
1314
selected_repository: str | None = Field(default=None)
1415
replay_json: str | None = Field(default=None)
1516
selected_branch: str | None = Field(default=None)

openhands/server/session/session.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,12 @@ async def initialize_agent(
153153
git_provider_tokens = None
154154
selected_repository = None
155155
selected_branch = None
156+
custom_secrets = None
156157
if isinstance(settings, ConversationInitData):
157158
git_provider_tokens = settings.git_provider_tokens
158159
selected_repository = settings.selected_repository
159160
selected_branch = settings.selected_branch
161+
custom_secrets = settings.custom_secrets
160162

161163
try:
162164
await self.agent_session.start(
@@ -168,6 +170,7 @@ async def initialize_agent(
168170
agent_to_llm_config=self.config.get_agent_to_llm_config_map(),
169171
agent_configs=self.config.get_agent_configs(),
170172
git_provider_tokens=git_provider_tokens,
173+
custom_secrets=custom_secrets,
171174
selected_repository=selected_repository,
172175
selected_branch=selected_branch,
173176
initial_message=initial_message,

openhands/storage/data_models/user_secrets.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111
from pydantic.json import pydantic_encoder
1212

13+
from openhands.events.stream import EventStream
1314
from openhands.integrations.provider import (
1415
CUSTOM_SECRETS_TYPE,
1516
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA,
@@ -136,3 +137,31 @@ def convert_dict_to_mappingproxy(
136137
new_data['custom_secrets'] = secrets
137138

138139
return new_data
140+
141+
142+
def set_event_stream_secrets(self, event_stream: EventStream) -> None:
143+
"""
144+
This ensures that provider tokens and custom secrets masked from the event stream
145+
Args:
146+
event_stream: Agent session's event stream
147+
"""
148+
149+
secrets = self.get_env_vars()
150+
event_stream.set_secrets(secrets)
151+
152+
def get_env_vars(self) -> dict[str, str]:
153+
secret_store = self.model_dump(context={'expose_secrets': True})
154+
custom_secrets = secret_store.get('custom_secrets', {})
155+
secrets = {}
156+
for secret_name, value in custom_secrets.items():
157+
secrets[secret_name] = value['secret']
158+
159+
return secrets
160+
161+
162+
def get_custom_secrets_descriptions(self) -> dict[str, str]:
163+
secrets = {}
164+
for secret_name, secret in self.custom_secrets.items():
165+
secrets[secret_name] = secret.description
166+
167+
return secrets

openhands/utils/prompt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class RuntimeInfo:
1414
date: str
1515
available_hosts: dict[str, int] = field(default_factory=dict)
1616
additional_agent_instructions: str = ''
17+
custom_secrets_descriptions: dict[str, str] = field(default_factory=dict)
1718

1819

1920
@dataclass

0 commit comments

Comments
 (0)