pipx, subagent paths, alpine directives, CSRF allowed origins autoset
frdel committed
Dec 19, 2025 at 11:30 UTC
dd018d66a49e79259d443b7655375eadd885f5e4
7 files changed
+128
-80
agent.py
+3
-3
@@ -619,14 +619,14 @@ class Agent:
619
return system_prompt
620
621
def parse_prompt(self, _prompt_file: str, **kwargs):
622
- dirs = subagents.get_agent_paths_chain(self, "prompts")
622
+ dirs = subagents.get_paths(self, "prompts")
623
prompt = files.parse_file(
624
_prompt_file, _directories=dirs, _agent=self, **kwargs
625
)
626
return prompt
627
628
def read_prompt(self, file: str, **kwargs) -> str:
629
- dirs = subagents.get_agent_paths_chain(self, "prompts")
629
+ dirs = subagents.get_paths(self, "prompts")
630
prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs)
631
prompt = files.remove_code_fences(prompt)
632
return prompt
@@ -958,7 +958,7 @@ class Agent:
958
classes = []
959
960
# search for tools in agent's folder hierarchy
961
- paths = subagents.get_agent_paths_chain(self, "tools", name + ".py", default_root="python")
961
+ paths = subagents.get_paths(self, "tools", name + ".py", default_root="python")
962
for path in paths:
963
try:
964
classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type]
docker/base/fs/ins/install_python.sh
+1
-1
@@ -20,7 +20,7 @@ python3.13 -m venv /opt/venv
20
source /opt/venv/bin/activate
21
22
# upgrade pip and install static packages
23
-pip install --no-cache-dir --upgrade pip ipython requests
23
+pip install --no-cache-dir --upgrade pip pipx ipython requests
24
25
echo "====================PYTHON PYVENV===================="
26
python/api/csrf_token.py
+38
-2
@@ -11,6 +11,8 @@ from python.helpers.api import (
11
from python.helpers import runtime, dotenv, login
12
import fnmatch
13
14
+ALLOWED_ORIGINS_KEY = "ALLOWED_ORIGINS"
15
+
16
17
class GetCsrfToken(ApiHandler):
18
@@ -44,9 +46,11 @@ class GetCsrfToken(ApiHandler):
46
}
47
48
async def check_allowed_origin(self, request: Request):
47
- # if login is required, this che
49
+ # if login is required, this check is unnecessary
50
if login.is_login_required():
51
return {"ok": True, "origin": "", "allowed_origins": ""}
52
+ # initialize allowed origins if not yet set
53
+ self.initialize_allowed_origins(request)
54
# otherwise, check if the origin is allowed
55
return await self.is_allowed_origin(request)
56
@@ -66,6 +70,7 @@ class GetCsrfToken(ApiHandler):
70
)
71
return {"ok": match, "origin": origin, "allowed_origins": allowed_origins}
72
73
+
74
def get_origin_from_request(self, request: Request):
75
# get from origin
76
r = request.headers.get("Origin") or request.environ.get("HTTP_ORIGIN")
@@ -88,7 +93,7 @@ class GetCsrfToken(ApiHandler):
93
# get the allowed origins from the environment
94
allowed_origins = [
95
origin.strip()
91
- for origin in (dotenv.get_dotenv_value("ALLOWED_ORIGINS") or "").split(",")
96
+ for origin in (dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY) or "").split(",")
97
if origin.strip()
98
]
99
@@ -110,3 +115,34 @@ class GetCsrfToken(ApiHandler):
115
116
def get_default_allowed_origins(self) -> list[str]:
117
return ["*://localhost:*", "*://127.0.0.1:*", "*://0.0.0.0:*"]
118
+
119
+ def initialize_allowed_origins(self, request: Request):
120
+ """
121
+ If A0 is hosted on a server, add the first visit origin to ALLOWED_ORIGINS.
122
+ This simplifies deployment process as users can access their new instance without
123
+ additional setup while keeping it secure.
124
+ """
125
+ # dotenv value is already set, do nothing
126
+ denv = dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY)
127
+ if denv:
128
+ return
129
+
130
+ # get the origin from the request
131
+ req_origin = self.get_origin_from_request(request)
132
+ if not req_origin:
133
+ return
134
+
135
+ # check if the origin is allowed by default
136
+ allowed_origins = self.get_default_allowed_origins()
137
+ match = any(
138
+ fnmatch.fnmatch(req_origin, allowed_origin)
139
+ for allowed_origin in allowed_origins
140
+ )
141
+ if match:
142
+ return
143
+
144
+ # if not, add it to the allowed origins
145
+ allowed_origins.append(req_origin)
146
+ dotenv.save_dotenv_value(ALLOWED_ORIGINS_KEY, ",".join(allowed_origins))
147
+
148
+
\ No newline at end of file
python/extensions/agent_init/_15_load_profile_settings.py
+1
-1
@@ -10,7 +10,7 @@ class LoadProfileSettings(Extension):
10
if not self.agent or not self.agent.config.profile:
11
return
12
13
- config_files = subagents.get_agent_paths_chain(self.agent, "settings.json", include_default=False)
13
+ config_files = subagents.get_paths(self.agent, "settings.json", include_default=False)
14
15
settings_override = {}
16
for settings_path in config_files:
python/helpers/extension.py
+1
-1
@@ -30,7 +30,7 @@ async def call_extensions(
30
from python.helpers import projects, subagents
31
32
# search for extension folders in all agent's paths
33
- paths = subagents.get_agent_paths_chain(agent, "extensions", extension_point, default_root="python")
33
+ paths = subagents.get_paths(agent, "extensions", extension_point, default_root="python")
34
all_exts = [cls for path in paths for cls in _get_extensions(path)]
35
36
# merge: first ocurrence of file name is the override
python/helpers/subagents.py
+40
-61
@@ -246,84 +246,63 @@ def get_available_agents_dict(
246
return filtered_agents
247
248
249
-def get_agent_paths(
250
- agent: "Agent", *subpaths, must_exist_completely: bool = True
251
-) -> list[str]:
252
- """Returns list of possible paths for the given agent and subpaths. Order is from lowest priority (global)."""
253
-
254
- if not agent or not agent.config.profile:
255
- return []
256
- from python.helpers import projects
257
-
258
- project_name = projects.get_context_project_name(agent.context)
259
- return get_agent_profile_paths(
260
- agent.config.profile,
261
- project_name,
262
- *subpaths,
263
- must_exist_completely=must_exist_completely,
264
- )
265
-
266
-
267
-def get_agent_paths_chain(
249
+def get_paths(
250
agent: "Agent|None",
251
*subpaths,
270
- must_exist_completely: bool = True,
252
+ must_exist_completely: bool = True,
253
include_project: bool = True,
254
include_user: bool = True,
255
include_default: bool = True,
256
default_root: str = "",
257
) -> list[str]:
258
"""Returns list of file paths for the given agent and subpaths, searched in order of priority:
277
- project/agents/, usr/agents/, agents/, project/, usr/, default."""
278
- from python.helpers import projects
259
+ project/agents/, project/, usr/agents/, agents/, usr/, default."""
260
+ paths: list[str] = []
261
+ check_subpaths = subpaths if must_exist_completely else []
262
+ profile_name = agent.config.profile if agent and agent.config.profile else ""
263
+ project_name = ""
264
280
- if agent and agent.config.profile:
281
- project_name = projects.get_context_project_name(agent.context)
282
- paths = get_agent_profile_paths(
283
- agent.config.profile,
284
- project_name,
285
- *subpaths,
286
- must_exist_completely=must_exist_completely,
287
- )
288
- list.reverse(paths) # reverse for proper priority
289
- else:
290
- paths = []
291
- project_name = ""
265
+ if include_project and agent:
266
+ from python.helpers import projects
267
293
- if include_project and project_name:
294
- path = projects.get_project_meta_folder(project_name, *subpaths)
295
- if (not must_exist_completely) or files.exists(path):
268
+ project_name = projects.get_context_project_name(agent.context) or ""
269
+
270
+ if project_name and profile_name:
271
+ # project/agents/<profile>/...
272
+ project_agent_dir = projects.get_project_meta_folder(
273
+ project_name, "agents", profile_name
274
+ )
275
+ if files.exists(files.get_abs_path(project_agent_dir, *check_subpaths)):
276
+ paths.append(files.get_abs_path(project_agent_dir, *subpaths))
277
+
278
+ if project_name:
279
+ # project/.a0proj/...
280
+ path = projects.get_project_meta_folder(project_name, *subpaths)
281
+ if (not must_exist_completely) or files.exists(path):
282
+ paths.append(path)
283
+
284
+ if profile_name:
285
+
286
+ # usr/agents/<profile>/...
287
+ path = files.get_abs_path(USER_AGENTS_DIR, profile_name, *subpaths)
288
+ if (not must_exist_completely) or files.exists(files.get_abs_path(USER_AGENTS_DIR, profile_name, *check_subpaths)):
289
paths.append(path)
290
+
291
+ # agents/<profile>/...
292
+ path = files.get_abs_path(DEFAULT_AGENTS_DIR, profile_name, *subpaths)
293
+ if (not must_exist_completely) or files.exists(files.get_abs_path(DEFAULT_AGENTS_DIR, profile_name, *check_subpaths)):
294
+ paths.append(path)
295
+
296
if include_user:
297
+ # usr/...
298
path = files.get_abs_path(USER_DIR, *subpaths)
299
if (not must_exist_completely) or files.exists(path):
300
paths.append(path)
301
+
302
if include_default:
303
+ # default_root/...
304
path = files.get_abs_path(default_root, *subpaths)
305
if (not must_exist_completely) or files.exists(path):
306
paths.append(path)
305
- return paths
307
307
-
308
-def get_agent_profile_paths(
309
- name: str,
310
- project_name: str | None = None,
311
- *subpaths,
312
- must_exist_completely: bool = True,
313
-) -> list[str]:
314
- result = []
315
- check_subpaths = subpaths if must_exist_completely else []
316
-
317
- if files.exists(files.get_abs_path(DEFAULT_AGENTS_DIR, name, *check_subpaths)):
318
- result.append(files.get_abs_path(DEFAULT_AGENTS_DIR, name, *subpaths))
319
- if files.exists(files.get_abs_path(USER_AGENTS_DIR, name, *check_subpaths)):
320
- result.append(files.get_abs_path(USER_AGENTS_DIR, name, *subpaths))
321
- if project_name:
322
- from python.helpers import projects
323
-
324
- project_agent_dir = projects.get_project_meta_folder(
325
- project_name, "agents", name
326
- )
327
- if files.exists(files.get_abs_path(project_agent_dir, *check_subpaths)):
328
- result.append(files.get_abs_path(project_agent_dir, *subpaths))
329
- return result
308
+ return paths
webui/js/initFw.js
+44
-11
@@ -10,15 +10,48 @@ await import("../vendor/alpine/alpine.min.js");
10
11
// add x-destroy directive to alpine
12
Alpine.directive(
13
- "destroy",
14
- (el, { expression }, { evaluateLater, cleanup }) => {
15
- const onDestroy = evaluateLater(expression);
16
- cleanup(() => onDestroy());
17
- }
18
-);
13
+ "destroy",
14
+ (_el, { expression }, { evaluateLater, cleanup }) => {
15
+ const onDestroy = evaluateLater(expression);
16
+ cleanup(() => onDestroy());
17
+ }
18
+ );
19
20
-// add x-create directive to alpine
21
-Alpine.directive("create", (_el, { expression }, { evaluateLater }) => {
22
- const onCreate = evaluateLater(expression);
23
- onCreate();
24
-});
20
+ // add x-create directive to alpine
21
+ Alpine.directive(
22
+ "create",
23
+ (_el, { expression }, { evaluateLater }) => {
24
+ const onCreate = evaluateLater(expression);
25
+ onCreate();
26
+ }
27
+ );
28
+
29
+ // run every second if the component is active
30
+ Alpine.directive(
31
+ "every-second",
32
+ (_el, { expression }, { evaluateLater, cleanup }) => {
33
+ const onTick = evaluateLater(expression);
34
+ const intervalId = setInterval(() => onTick(), 1000);
35
+ cleanup(() => clearInterval(intervalId));
36
+ }
37
+ );
38
+
39
+ // run every minute if the component is active
40
+ Alpine.directive(
41
+ "every-minute",
42
+ (_el, { expression }, { evaluateLater, cleanup }) => {
43
+ const onTick = evaluateLater(expression);
44
+ const intervalId = setInterval(() => onTick(), 60_000);
45
+ cleanup(() => clearInterval(intervalId));
46
+ }
47
+ );
48
+
49
+ // run every hour if the component is active
50
+ Alpine.directive(
51
+ "every-hour",
52
+ (_el, { expression }, { evaluateLater, cleanup }) => {
53
+ const onTick = evaluateLater(expression);
54
+ const intervalId = setInterval(() => onTick(), 3_600_000);
55
+ cleanup(() => clearInterval(intervalId));
56
+ }
57
+ );