Skip to content

Commit bb7e37f

Browse files
Merge pull request #905 from yasinBursali/fix/run-install-hook-resolver-env
fix(host-agent): use new hooks schema + sandboxed env for install hooks
2 parents 7133a06 + 36d3c7f commit bb7e37f

File tree

2 files changed

+65
-36
lines changed

2 files changed

+65
-36
lines changed

dream-server/bin/dream-host-agent.py

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -212,40 +212,6 @@ def _precreate_data_dirs(service_id: str):
212212
logger.warning("Failed to pre-create %s: %s", dir_path, e)
213213

214214

215-
def _resolve_setup_hook(ext_dir: Path) -> Path | None:
216-
"""Read manifest to find setup_hook path. Returns None if no hook defined."""
217-
manifest_path = None
218-
for name in ("manifest.yaml", "manifest.yml"):
219-
candidate = ext_dir / name
220-
if candidate.exists():
221-
manifest_path = candidate
222-
break
223-
if manifest_path is None:
224-
return None
225-
try:
226-
import yaml
227-
manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
228-
except (ImportError, OSError):
229-
return None
230-
if not isinstance(manifest, dict):
231-
return None
232-
service_def = manifest.get("service", {})
233-
if not isinstance(service_def, dict):
234-
return None
235-
setup_hook = service_def.get("setup_hook", "")
236-
if not isinstance(setup_hook, str) or not setup_hook:
237-
return None
238-
hook_path = (ext_dir / setup_hook).resolve()
239-
try:
240-
hook_path.relative_to(ext_dir.resolve())
241-
except ValueError:
242-
logger.warning("Path traversal attempt in setup_hook for %s: %s", ext_dir.name, setup_hook)
243-
return None
244-
if not hook_path.is_file():
245-
return None
246-
return hook_path
247-
248-
249215
def docker_compose_action(service_id: str, action: str) -> tuple:
250216
flags = resolve_compose_flags()
251217
if action == "start":
@@ -958,11 +924,27 @@ def _run_install():
958924
if run_setup_hook:
959925
_write_progress(service_id, "setup_hook", "Running setup...")
960926
ext_dir = USER_EXTENSIONS_DIR / service_id
961-
hook_path = _resolve_setup_hook(ext_dir)
927+
hook_path = _resolve_hook(ext_dir, "post_install")
962928
if hook_path:
929+
# Minimal allowlist env — mirror _execute_hook (L856-866)
930+
# to prevent leaking host-agent secrets to extension scripts.
931+
manifest = _read_manifest(ext_dir)
932+
service_def = manifest.get("service", {}) if manifest else {}
933+
if not isinstance(service_def, dict):
934+
service_def = {}
935+
hook_env = {
936+
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
937+
"HOME": os.environ.get("HOME", ""),
938+
"SERVICE_ID": service_id,
939+
"SERVICE_PORT": str(service_def.get("port", 0)),
940+
"SERVICE_DATA_DIR": str(DATA_DIR / service_id),
941+
"DREAM_VERSION": DREAM_VERSION,
942+
"GPU_BACKEND": GPU_BACKEND,
943+
"HOOK_NAME": "post_install",
944+
}
963945
result = subprocess.run(
964946
["bash", str(hook_path), str(INSTALL_DIR), GPU_BACKEND],
965-
cwd=str(ext_dir),
947+
cwd=str(ext_dir), env=hook_env,
966948
capture_output=True, text=True,
967949
timeout=SUBPROCESS_TIMEOUT_START,
968950
)

dream-server/extensions/services/dashboard-api/tests/test_host_agent.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,50 @@ def test_missing_cache_file_is_noop(self, tmp_path, monkeypatch):
180180
monkeypatch.setattr(_mod, "INSTALL_DIR", install_dir)
181181

182182
invalidate_compose_cache() # must not raise
183+
184+
185+
# --- Install setup-hook env allowlist (regression) ---
186+
#
187+
# Locks in the fix that strips host-agent secrets from the env passed to
188+
# extension setup hooks during _handle_install. A source-level check is used
189+
# because the subprocess.run call lives inside a nested closure started on a
190+
# daemon thread, which makes dynamic mocking fragile.
191+
192+
193+
class TestInstallHookEnvAllowlist:
194+
195+
def _install_source(self):
196+
import inspect
197+
return inspect.getsource(_mod.AgentHandler._handle_install)
198+
199+
def test_setup_hook_subprocess_run_passes_env_kwarg(self):
200+
src = self._install_source()
201+
assert "env=hook_env" in src, (
202+
"setup_hook subprocess.run must pass env=hook_env "
203+
"(regression: do not fall back to inheriting os.environ)"
204+
)
205+
206+
def test_setup_hook_env_excludes_host_agent_secrets(self):
207+
src = self._install_source()
208+
for secret in ("AGENT_API_KEY", "DREAM_AGENT_KEY", "DASHBOARD_API_KEY"):
209+
assert secret not in src, (
210+
f"_handle_install must not reference {secret}; "
211+
"extension setup hooks must not receive host-agent secrets"
212+
)
213+
214+
def test_setup_hook_env_contains_allowlist_keys(self):
215+
src = self._install_source()
216+
for key in (
217+
"PATH", "HOME", "SERVICE_ID", "SERVICE_PORT",
218+
"SERVICE_DATA_DIR", "DREAM_VERSION", "GPU_BACKEND", "HOOK_NAME",
219+
):
220+
assert f'"{key}"' in src, (
221+
f"setup_hook env allowlist missing required key {key}"
222+
)
223+
224+
def test_setup_hook_uses_resolve_hook_with_post_install(self):
225+
src = self._install_source()
226+
assert '_resolve_hook(ext_dir, "post_install")' in src, (
227+
"setup_hook must use _resolve_hook(..., 'post_install'); "
228+
"the legacy _resolve_setup_hook has been removed"
229+
)

0 commit comments

Comments
 (0)