44from contextlib import asynccontextmanager
55from datetime import timedelta
66from types import SimpleNamespace
7- from unittest .mock import AsyncMock , MagicMock
7+ from unittest .mock import AsyncMock , MagicMock , call
88from uuid import uuid4
99
1010import asyncssh
@@ -72,7 +72,12 @@ async def list_messages(session, conversation):
7272@pytest .fixture
7373async def sandbox (druks_db , conversation , monkeypatch ):
7474 identity , _ = await SandboxIdentity .create (
75- druks_db , account_id = conversation .account_id , run_id = None , scoped_to = "chat" , secret_refs = []
75+ druks_db ,
76+ account_id = conversation .account_id ,
77+ run_id = None ,
78+ scoped_to = "chat" ,
79+ secret_refs = [],
80+ secrets_hash = "" ,
7681 )
7782 await identity .bind ("chat-sandbox" )
7883 host = SimpleNamespace (
@@ -157,6 +162,7 @@ async def test_chat_identity_authenticates_until_lease_expiry_and_is_not_an_orph
157162 secret_refs = [
158163 SecretRef (name = "mcp_druks_token" , secret_id = token .id , host = "hooks.example.com" )
159164 ],
165+ secrets_hash = "" ,
160166 )
161167 await identity .bind ("chat-sandbox" )
162168 bearer = entries ["mcp_druks_token" ].headers ["Authorization" ].removeprefix ("Bearer " )
@@ -200,7 +206,7 @@ async def attach(*, host_id):
200206 second = await Conversation .create (
201207 druks_db , account_id = conversation .account_id , body = "another conversation"
202208 )
203- config = SimpleNamespace (secret_refs = [], secrets = {})
209+ config = SimpleNamespace (secret_refs = [], secrets = {}, secrets_hash = "key-one" )
204210 first_host , _identity = await service .get_sandbox (
205211 druks_db ,
206212 conversation .account_id ,
@@ -217,7 +223,9 @@ async def attach(*, host_id):
217223 )
218224 login = await get_druks_account_token (druks_db , conversation .account_id , (), name = "login" )
219225 moved = SimpleNamespace (
220- secret_refs = [SecretRef (name = "claude_token" , secret_id = login .id )], secrets = {}
226+ secret_refs = [SecretRef (name = "claude_token" , secret_id = login .id )],
227+ secrets = {},
228+ secrets_hash = login .id ,
221229 )
222230 moved_host , _identity = await service .get_sandbox (
223231 druks_db ,
@@ -226,10 +234,19 @@ async def attach(*, host_id):
226234 allowed_tools = Toolkit .ALL ,
227235 secret_refs = [],
228236 )
237+ # A pasted key's entries are bound when the box is created, so another key needs a new box.
238+ rekeyed_host , _identity = await service .get_sandbox (
239+ druks_db ,
240+ second .account_id ,
241+ config = SimpleNamespace (secret_refs = moved .secret_refs , secrets = {}, secrets_hash = "key-two" ),
242+ allowed_tools = Toolkit .ALL ,
243+ secret_refs = [],
244+ )
229245
230246 assert first_host .id == second_host .id
231247 assert moved_host .id != first_host .id
232- release .assert_awaited_once_with (host_id = first_host .id )
248+ assert rekeyed_host .id != moved_host .id
249+ assert release .await_args_list == [call (host_id = first_host .id ), call (host_id = moved_host .id )]
233250
234251
235252async def test_delivered_turn_is_sent_again_when_the_bridge_never_got_the_prompt (
0 commit comments