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 {