okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""Every secret lives in creds.json, and only the daemon reads or writes it. 2 3A boot moves secrets still kept in auth.json, mcp-credentials.json or a 4config.json profile into creds.json; clients then change them through the 5daemon, which never answers with a secret. Both are only visible through a 6real daemon: the move happens at boot, and the keys prove themselves in the 7requests the provider receives. 8""" 9 10import json 11import stat 12import unittest 13 14from harness import Albedo ,Provider ,exclusive ,text 15 16 17class CredentialsTest (unittest .TestCase ): 18def setUp (self ): 19self .provider = Provider (lambda _request :text ("ok" )) 20self .addCleanup (self .provider .close ) 21 22def app_for (self ,prepare = None ): 23app = Albedo (self .provider ,prepare = prepare ) 24app .__enter__ () 25self .addCleanup (app .__exit__ ,None ,None ,None ) 26return app 27 28def authorization (self ,app ): 29"""The Authorization header of a turn on the active profile.""" 30start = len (self .provider .requests ) 31session = app .session () 32app .prompt (session ,"which key" ).close () 33app .idle (session ) 34return self .provider .requests [start :][- 1 ]["authorization" ] 35 36def summary (self ,app ): 37with app .api ("/settings" )as response : 38return response .read ().decode () 39 40# exclusive: restarts the daemon to migrate global credential files 41@exclusive 42def test_a_boot_moves_every_secret_into_creds_json (self ): 43app = self .app_for () 44# Written while the daemon runs, so the restart below is what moves them. 45for name ,content in { 46"auth.json" : {"anthropic" : [{"type" :"oauth" ,"access" :"old-access" }]}, 47"mcp-credentials.json" : {"servers" : {"docs" : {"bearerToken" :"old-token" }}}, 48 }.items (): 49 (app .home / name ).write_text (json .dumps (content )) 50 (app .home / name ).chmod (0o600 ) 51config = json .loads ((app .home / "config.json" ).read_text ()) 52config ["providers" ][app .profile ]["apiKey" ]= "fixture-key" 53 (app .home / "config.json" ).write_text (json .dumps (config )) 54app .restart () 55creds = json .loads ((app .home / "creds.json" ).read_text ()) 56self .assertEqual (creds ["accounts" ]["anthropic" ][0 ]["access" ],"old-access" ) 57self .assertEqual (creds ["mcp" ]["docs" ], {"bearerToken" :"old-token" }) 58self .assertEqual (creds ["providers" ][app .profile ], {"apiKey" :"fixture-key" }) 59self .assertEqual (stat .S_IMODE ((app .home / "creds.json" ).stat ().st_mode ),0o600 ) 60config = json .loads ((app .home / "config.json" ).read_text ()) 61self .assertNotIn ("apiKey" ,config ["providers" ][app .profile ]) 62self .assertFalse ((app .home / "auth.json" ).exists ()) 63self .assertFalse ((app .home / "mcp-credentials.json" ).exists ()) 64backups = {path .name .split ("-before-creds-" )[0 ]for path in self .backups (app )} 65self .assertEqual (backups , {"auth.json" ,"mcp-credentials.json" ,"config.json" }) 66with app .api ("/server" )as response : 67notices = json .load (response )["notices" ] 68migrated = " " .join ( 69notice ["message" ]for notice in notices if notice ["kind" ]== "migration" 70 ) 71for name in ("auth.json" ,"mcp-credentials.json" ,"config.json" ): 72self .assertIn (name ,migrated ) 73self .assertNotIn ("old-access" ,migrated ) 74self .assertNotIn ("old-token" ,migrated ) 75# Not even their owner reads them without a chmod first. 76self .assertEqual ( 77 {stat .S_IMODE (path .stat ().st_mode )for path in self .backups (app )}, {0 } 78 ) 79# The moved key still reaches the provider. 80self .assertEqual (self .authorization (app ),"Bearer fixture-key" ) 81 82@exclusive 83def test_clients_change_secrets_without_reading_them (self ): 84# No legacy config key: clients store and rotate only this fixture's 85# credential, without depending on startup migration of config.json. 86app = Albedo ( 87self .provider , 88providers = { 89f"fixture- { self . provider . route } " : { 90"baseUrl" :self .provider .url , 91"model" :"fixture-model" , 92"protocol" :"chat_completions" , 93 } 94 }, 95 ) 96app .__enter__ () 97self .addCleanup (app .__exit__ ,None ,None ,None ) 98server = app .profile 99 100def patch (group ,body ): 101route = f"/settings?group= { group } " 102with app .api (route )as response : 103json .load (response ) 104revision = response .headers ["ETag" ] 105with app .api ( 106route , 107body , 108method = "PATCH" , 109headers = { 110"If-Match" :revision , 111"Content-Type" :"application/merge-patch+json" , 112 }, 113 )as response : 114return json .load (response )["resource" ]["value" ] 115 116patch ("providers" , {"profiles" : {app .profile : {"api_key" :"fixture-key" }}}) 117self .assertEqual (self .authorization (app ),"Bearer fixture-key" ) 118patch ("providers" , {"profiles" : {app .profile : {"api_key" :"rotated-key" }}}) 119self .assertEqual (self .authorization (app ),"Bearer rotated-key" ) 120value = patch ( 121"mcp" , 122 { 123"definitions" : { 124server : { 125"enabled" :False , 126"transport" :"http" , 127"url" :"http://127.0.0.1:1/mcp" , 128"secrets" : { 129"bearer_token" :"secret-token" , 130"headers" : {"X-Team" :"secret-team" }, 131 }, 132 } 133 } 134 }, 135 ) 136self .assertEqual ( 137value ["definitions" ][server ]["secret_presence" ], 138 {"bearer_token" :True ,"headers" : ["X-Team" ],"environment" : []}, 139 ) 140value = patch ( 141"mcp" , 142 { 143"definitions" : { 144server : { 145"secrets" : { 146"bearer_token" :None , 147"environment" : {"TOKEN" :"secret-env" }, 148 } 149 } 150 } 151 }, 152 ) 153self .assertEqual ( 154value ["definitions" ][server ]["secret_presence" ], 155 {"bearer_token" :False ,"headers" : ["X-Team" ],"environment" : ["TOKEN" ]}, 156 ) 157summary = self .summary (app ) 158self .assertFalse ( 159any ( 160secret in summary 161for secret in ( 162"rotated-key" , 163"secret-token" , 164"secret-team" , 165"secret-env" , 166"fixture-key" , 167 ) 168 ) 169 ) 170patch ("mcp" , {"definitions" : {server :None }}) 171with (app .home / "creds.json" ).open ()as source : 172self .assertNotIn (server ,json .load (source ).get ("mcp" , {})) 173 174def backups (self ,app ): 175return list ((app .home / "backups" ).glob ("*-before-creds-*" ))