RFC password

RFC password protection work in progress

frdel committed Nov 18, 2024 at 21:16 UTC 05cbaa0f4dd9ce967be37d337debfbb649cbef95
9 files changed +122 -31
.gitattributes
+1 -1
@@ -1,2 +1,2 @@
1 # Auto detect text files and perform LF normalization
2 -* text=auto
2 +* text=auto eol=lf
\ No newline at end of file
docker/run/Dockerfile
+2 -1
@@ -36,7 +36,8 @@ RUN bash /ins/install_A0.sh
36 RUN bash /ins/install_searxng.sh
37
38 # Expose ports
39 -EXPOSE 22 80
39 +# EXPOSE 22
40 +EXPOSE 80
41
42 # initialize runtime
43 CMD ["/bin/bash", "/exe/initialize.sh"]
\ No newline at end of file
docker/run/fs/ins/install_A0.sh
+4 -1
@@ -11,7 +11,10 @@ source /opt/venv/bin/activate
11 # Ensure the virtual environment and pip setup
12 pip install --upgrade pip ipython requests
13
14 -# Install A0 python packages
14 +# Install some packages in specific variants
15 +pip install torch --index-url https://download.pytorch.org/whl/cpu
16 +
17 +# Install remaining A0 python packages
18 pip install -r /a0/requirements.txt
19
20 # Preload A0
python/helpers/dotenv.py
+1
@@ -5,6 +5,7 @@ from dotenv import load_dotenv as _load_dotenv
5
6 KEY_AUTH_LOGIN = "AUTH_LOGIN"
7 KEY_AUTH_PASSWORD = "AUTH_PASSWORD"
8 +KEY_RFC_PASSWORD = "RFC_PASSWORD"
9
10 def load_dotenv():
11 _load_dotenv(get_dotenv_file_path(), override=True)
python/helpers/rfc.py
+43 -12
@@ -3,9 +3,16 @@ import inspect
3 import json
4 from typing import Any, TypedDict
5 import aiohttp
6 +import hmac
7 +import hashlib
8 +
9 +from python.helpers import dotenv
10 +
11
12 # Remote Function Call library
13 # Call function via http request
14 +# Secured by pre-shared key
15 +
16
17 class RFCInput(TypedDict):
18 module: str
@@ -14,19 +21,32 @@ class RFCInput(TypedDict):
21 kwargs: dict[str, Any]
22
23
17 -async def call_rfc(url: str, module: str, function_name: str, args: list, kwargs: dict):
18 - input = {
19 - "module": module,
20 - "function_name": function_name,
21 - "args": args,
22 - "kwargs": kwargs,
23 - }
24 - input_json = json.dumps(input)
25 - result = await _send_json_data(url, input_json)
24 +class RFCCall(TypedDict):
25 + rfc_input: str
26 + hash: str
27 +
28 +
29 +async def call_rfc(
30 + url: str, password: str, module: str, function_name: str, args: list, kwargs: dict
31 +):
32 + input = RFCInput(
33 + module=module,
34 + function_name=function_name,
35 + args=args,
36 + kwargs=kwargs,
37 + )
38 + call = RFCCall(
39 + rfc_input=json.dumps(input), hash=hash_data(json.dumps(input), password)
40 + )
41 + result = await _send_json_data(url, json.dumps(call))
42 return result
43
44
29 -async def handle_rfc(input: RFCInput):
45 +async def handle_rfc(rfc_call: RFCCall, password: str):
46 + if not verify_data(rfc_call["rfc_input"], rfc_call["hash"], password):
47 + raise Exception("Invalid RFC hash")
48 +
49 + input: RFCInput = json.loads(rfc_call["rfc_input"])
50 return await _call_function(
51 input["module"], input["function_name"], *input["args"], **input["kwargs"]
52 )
@@ -50,5 +70,16 @@ def _get_function(module: str, function_name: str):
70
71 async def _send_json_data(url: str, data: str):
72 async with aiohttp.ClientSession() as session:
53 - async with session.post(url, json=data) as response:
54 - return await response.json()
\ No newline at end of file
73 + async with session.post(
74 + url,
75 + json=data,
76 + ) as response:
77 + return await response.json()
78 +
79 +
80 +def hash_data(data: str, password: str):
81 + return hmac.new(password.encode(), data.encode(), hashlib.sha256).hexdigest()
82 +
83 +
84 +def verify_data(data: str, hash: str, password: str):
85 + return hash_data(data, password) == hash
python/helpers/runtime.py
+32 -14
@@ -1,6 +1,6 @@
1 import argparse
2 from typing import Any, Callable, Coroutine
3 -from python.helpers import rfc, docker
3 +from python.helpers import dotenv, rfc, docker, settings
4
5 parser = argparse.ArgumentParser()
6 args = {}
@@ -42,8 +42,10 @@ def is_development() -> bool:
42 async def call_development_function(func: Callable, *args, **kwargs):
43 if is_development():
44 url = _get_rfc_url()
45 + password = _get_rfc_password()
46 return await rfc.call_rfc(
47 url=url,
48 + password=password,
49 module=func.__module__,
50 function_name=func.__name__,
51 args=list(args),
@@ -53,17 +55,33 @@ async def call_development_function(func: Callable, *args, **kwargs):
55 return await func(*args, **kwargs)
56
57
58 +async def handle_rfc(rfc_call: rfc.RFCCall):
59 + return await rfc.handle_rfc(rfc_call=rfc_call, password=_get_rfc_password())
60 +
61 +
62 +def _get_rfc_password() -> str:
63 + password = dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD)
64 + if not password:
65 + raise Exception("No RFC password, cannot handle RFC calls.")
66 + return password
67 +
68 +
69 def _get_rfc_url() -> str:
57 - if get_arg("rfc_url"):
58 - return str(get_arg("rfc_url"))
59 - global dockerman
60 - if dockerman is None:
61 - dockerman = docker.DockerContainerManager(
62 - image="agent-zero-run",
63 - name="agent-zero-development",
64 - ports={"55080": 80, "55022": 22},
65 - volumes={},
66 - logger=None,
67 - )
68 - conts = dockerman.get_image_containers()
69 - return f"http://localhost:{conts[0]['web_port']}/rfc"
70 + url = settings.get_settings()["rfc_url"]
71 + if not url.endswith("/"):
72 + url += "/"
73 + url += "url"
74 + return url
75 + # if get_arg("rfc_url"):
76 + # return str(get_arg("rfc_url"))
77 + # global dockerman
78 + # if dockerman is None:
79 + # dockerman = docker.DockerContainerManager(
80 + # image="agent-zero-run",
81 + # name="agent-zero-development",
82 + # ports={"55080": 80, "55022": 22},
83 + # volumes={},
84 + # logger=None,
85 + # )
86 + # conts = dockerman.get_image_containers()
87 + # return f"http://localhost:{conts[0]['web_port']}/rfc"
python/helpers/settings.py
+36
@@ -34,6 +34,9 @@ class Settings(TypedDict):
34 auth_login: str
35 auth_password: str
36
37 + rfc_url: str
38 + rfc_password: str
39 +
40
41 class PartialSettings(Settings, total=False):
42 pass
@@ -357,6 +360,34 @@ def convert_out(settings: Settings) -> SettingsOutput:
360 "fields": agent_fields,
361 }
362
363 + dev_fields: list[SettingsField] = []
364 +
365 + dev_fields.append(
366 + {
367 + "id": "rfc_url",
368 + "title": "RFC Destination URL",
369 + "description": "URL for remote function calls. RFCs are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker.",
370 + "type": "input",
371 + "value": settings["rfc_url"],
372 + }
373 + )
374 +
375 + dev_fields.append(
376 + {
377 + "id": "rfc_password",
378 + "title": "RFC Password",
379 + "description": "Password for remote function calls. Passwords must match on both systems. RFCs can not be used with empty password.",
380 + "type": "password",
381 + "value": dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD),
382 + }
383 + )
384 +
385 + dev_section: SettingsSection = {
386 + "title": "Development",
387 + "description": "Parameters for A0 framework development.",
388 + "fields": dev_fields,
389 + }
390 +
391 result: SettingsOutput = {
392 "sections": [
393 agent_section,
@@ -365,6 +396,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
396 embed_model_section,
397 api_keys_section,
398 auth_section,
399 + dev_section,
400 ]
401 }
402 return result
@@ -476,6 +508,7 @@ def _remove_sensitive_settings(settings: Settings):
508 settings["api_keys"] = {}
509 settings["auth_login"] = ""
510 settings["auth_password"] = ""
511 + settings["rfc_password"] = ""
512
513
514 def _write_sensitive_settings(settings: Settings):
@@ -483,6 +516,7 @@ def _write_sensitive_settings(settings: Settings):
516 dotenv.save_dotenv_value(key.upper(), val)
517 dotenv.save_dotenv_value(dotenv.KEY_AUTH_LOGIN, settings["auth_login"])
518 dotenv.save_dotenv_value(dotenv.KEY_AUTH_PASSWORD, settings["auth_password"])
519 + dotenv.save_dotenv_value(dotenv.KEY_RFC_PASSWORD, settings["rfc_password"])
520
521
522 def _get_default_settings() -> Settings:
@@ -504,6 +538,8 @@ def _get_default_settings() -> Settings:
538 agent_prompts_subdir="default",
539 agent_memory_subdir="default",
540 agent_knowledge_subdir="custom",
541 + rfc_url="http://localhost:55080",
542 + rfc_password="",
543 )
544
545
run_ui.py
+2 -2
@@ -542,8 +542,8 @@ async def handle_rfc():
542 # data sent to the server
543 input = json.loads(request.get_json())
544
545 - # handle RFC call
546 - result = await rfc.handle_rfc(input)
545 + # handle RFC
546 + result = await runtime.handle_rfc(input)
547 return jsonify(result)
548
549
webui/settings.css
+1
@@ -38,6 +38,7 @@ select {
38
39 .modal-header ul {
40 margin-bottom: 0;
41 + line-height: 2em;
42 }
43
44 .modal-content {