getbased-agent-stack 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {getbased_agent_stack-0.4.0/src/getbased_agent_stack.egg-info → getbased_agent_stack-0.5.0}/PKG-INFO +13 -2
  2. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/README.md +12 -1
  3. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/pyproject.toml +1 -1
  4. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/__init__.py +1 -1
  5. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/cli.py +29 -7
  6. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/systemd/getbased-dashboard.service +3 -7
  7. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/systemd/getbased-rag.service +7 -8
  8. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0/src/getbased_agent_stack.egg-info}/PKG-INFO +13 -2
  9. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack.egg-info/SOURCES.txt +1 -0
  10. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/tests/test_cli.py +51 -0
  11. getbased_agent_stack-0.5.0/tests/test_systemd_units.py +109 -0
  12. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/LICENSE +0 -0
  13. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/setup.cfg +0 -0
  14. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/env_file.py +0 -0
  15. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/mcp_configs.py +0 -0
  16. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack/units.py +0 -0
  17. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack.egg-info/dependency_links.txt +0 -0
  18. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack.egg-info/entry_points.txt +0 -0
  19. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack.egg-info/requires.txt +0 -0
  20. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/src/getbased_agent_stack.egg-info/top_level.txt +0 -0
  21. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/tests/test_env_file.py +0 -0
  22. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/tests/test_integration.py +0 -0
  23. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/tests/test_mcp_configs.py +0 -0
  24. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.5.0}/tests/test_units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
5
5
  License-Expression: GPL-3.0-only
6
6
  Requires-Python: >=3.10
@@ -26,7 +26,18 @@ Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agent
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- pipx install "getbased-agent-stack[full]"
29
+ pipx install --include-deps "getbased-agent-stack[full]"
30
+ ```
31
+
32
+ The `--include-deps` flag is required — it exposes `getbased-mcp`, `lens`, and `getbased-dashboard` alongside `getbased-stack` on your PATH. Without it, pipx only links the stack's own entry point and the MCP/rag/dashboard binaries stay hidden inside the venv.
33
+
34
+ `uv` users: install each package as its own tool instead, since `uv tool` has no `--include-deps` equivalent yet:
35
+
36
+ ```bash
37
+ uv tool install getbased-mcp
38
+ uv tool install "getbased-rag[full]"
39
+ uv tool install getbased-dashboard
40
+ uv tool install "getbased-agent-stack[full]"
30
41
  ```
31
42
 
32
43
  Pulls:
@@ -7,7 +7,18 @@ Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agent
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- pipx install "getbased-agent-stack[full]"
10
+ pipx install --include-deps "getbased-agent-stack[full]"
11
+ ```
12
+
13
+ The `--include-deps` flag is required — it exposes `getbased-mcp`, `lens`, and `getbased-dashboard` alongside `getbased-stack` on your PATH. Without it, pipx only links the stack's own entry point and the MCP/rag/dashboard binaries stay hidden inside the venv.
14
+
15
+ `uv` users: install each package as its own tool instead, since `uv tool` has no `--include-deps` equivalent yet:
16
+
17
+ ```bash
18
+ uv tool install getbased-mcp
19
+ uv tool install "getbased-rag[full]"
20
+ uv tool install getbased-dashboard
21
+ uv tool install "getbased-agent-stack[full]"
11
22
  ```
12
23
 
13
24
  Pulls:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "getbased-agent-stack"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
@@ -5,4 +5,4 @@ plus a small CLI that proxies to the real binaries. Everything
5
5
  interesting lives in the sibling repos.
6
6
  """
7
7
 
8
- __version__ = "0.4.0"
8
+ __version__ = "0.4.1"
@@ -86,7 +86,14 @@ def _yesno(msg: str, default: bool = True) -> bool:
86
86
 
87
87
 
88
88
  def cmd_init(args: argparse.Namespace) -> int:
89
+ # --yes / -y runs the full flow with defaults and no prompts. Intended
90
+ # for scripted installs (curl | bash) where stdin is unavailable and
91
+ # the EOF-returns-default path still triggers a getpass echo warning.
92
+ non_interactive = getattr(args, "yes", False)
93
+
89
94
  print("getbased-stack init — one-time setup")
95
+ if non_interactive:
96
+ print("Running non-interactive (--yes) — takes default answers on every prompt.")
90
97
  print(
91
98
  "Writes ~/.config/getbased/env, (optionally) installs + starts systemd\n"
92
99
  "user units for rag and dashboard. Idempotent: safe to re-run."
@@ -98,11 +105,15 @@ def cmd_init(args: argparse.Namespace) -> int:
98
105
  current_token = existing.get("GETBASED_TOKEN", "")
99
106
  masked = "****" + current_token[-4:] if current_token else "(unset)"
100
107
  print(f"[1/4] getbased sync token (current: {masked})")
101
- token = _prompt(
102
- "Paste GETBASED_TOKEN (press Enter to keep current / skip)",
103
- default=current_token,
104
- secret=True,
105
- )
108
+ if non_interactive:
109
+ token = current_token
110
+ print(" keeping current value (set with `getbased-stack set GETBASED_TOKEN=…` later).")
111
+ else:
112
+ token = _prompt(
113
+ "Paste GETBASED_TOKEN (press Enter to keep current / skip)",
114
+ default=current_token,
115
+ secret=True,
116
+ )
106
117
 
107
118
  # 2. API key
108
119
  key_path = Path(existing.get("LENS_API_KEY_FILE", str(_default_api_key_file())))
@@ -127,7 +138,10 @@ def cmd_init(args: argparse.Namespace) -> int:
127
138
 
128
139
  # 4. install units
129
140
  print("\n[4/4] install systemd user units?")
130
- if _yesno("install + start getbased-rag + getbased-dashboard?", default=True):
141
+ install_units = True if non_interactive else _yesno(
142
+ "install + start getbased-rag + getbased-dashboard?", default=True
143
+ )
144
+ if install_units:
131
145
  mgr = units.UnitManager()
132
146
  for line in mgr.install(enable=True, start=True):
133
147
  print(f" {line}")
@@ -304,7 +318,15 @@ def build_parser() -> argparse.ArgumentParser:
304
318
  )
305
319
  sub = p.add_subparsers(dest="command")
306
320
 
307
- sub.add_parser("init", help="Interactive one-time setup (token, API key, units).")
321
+ pinit = sub.add_parser(
322
+ "init", help="Interactive one-time setup (token, API key, units)."
323
+ )
324
+ pinit.add_argument(
325
+ "-y",
326
+ "--yes",
327
+ action="store_true",
328
+ help="Non-interactive: take defaults on every prompt. Use for scripted installs.",
329
+ )
308
330
 
309
331
  pi = sub.add_parser("install", help="Install + start the systemd user units.")
310
332
  pi.add_argument("--no-enable", action="store_true", help="Copy files only; don't enable.")
@@ -16,7 +16,9 @@ ExecStart=%h/.local/bin/getbased-dashboard serve
16
16
  Restart=always
17
17
  RestartSec=5s
18
18
 
19
- # Hardening the dashboard is a localhost-only HTTP server.
19
+ # User-mode-compatible hardening only see getbased-rag.service for
20
+ # the list of CAP_SYS_ADMIN-requiring directives that fail with
21
+ # 218/CAPABILITIES under `systemctl --user` and are deliberately omitted.
20
22
  NoNewPrivileges=true
21
23
  ProtectSystem=strict
22
24
  ProtectHome=read-only
@@ -24,13 +26,7 @@ ProtectHome=read-only
24
26
  # the shared env file when the user edits the token in the MCP tab.
25
27
  ReadWritePaths=%h/.local/state/getbased %h/.config/getbased
26
28
  PrivateTmp=true
27
- ProtectKernelTunables=true
28
- ProtectKernelModules=true
29
- ProtectControlGroups=true
30
- RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
31
- RestrictNamespaces=true
32
29
  LockPersonality=true
33
- RestrictRealtime=true
34
30
 
35
31
  [Install]
36
32
  WantedBy=default.target
@@ -22,20 +22,19 @@ ExecStart=%h/.local/bin/lens serve
22
22
  Restart=always
23
23
  RestartSec=5s
24
24
 
25
- # Hardening the server only needs its data dir + a network socket.
25
+ # User-mode-compatible hardening only. Directives that require
26
+ # CAP_SYS_ADMIN (ProtectKernelTunables, ProtectKernelModules,
27
+ # ProtectControlGroups, RestrictNamespaces, RestrictAddressFamilies,
28
+ # RestrictRealtime, MemoryDenyWriteExecute) fail with 218/CAPABILITIES
29
+ # under `systemctl --user` on a non-privileged bus and are omitted.
30
+ # For a hardened system-service deployment, promote this unit to
31
+ # /etc/systemd/system/ and re-add those directives.
26
32
  NoNewPrivileges=true
27
33
  ProtectSystem=strict
28
34
  ProtectHome=read-only
29
35
  ReadWritePaths=%h/.local/share/getbased
30
36
  PrivateTmp=true
31
- ProtectKernelTunables=true
32
- ProtectKernelModules=true
33
- ProtectControlGroups=true
34
- RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
35
- RestrictNamespaces=true
36
37
  LockPersonality=true
37
- MemoryDenyWriteExecute=false # ONNX Runtime needs W^X exception
38
- RestrictRealtime=true
39
38
 
40
39
  [Install]
41
40
  WantedBy=default.target
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
5
5
  License-Expression: GPL-3.0-only
6
6
  Requires-Python: >=3.10
@@ -26,7 +26,18 @@ Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agent
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- pipx install "getbased-agent-stack[full]"
29
+ pipx install --include-deps "getbased-agent-stack[full]"
30
+ ```
31
+
32
+ The `--include-deps` flag is required — it exposes `getbased-mcp`, `lens`, and `getbased-dashboard` alongside `getbased-stack` on your PATH. Without it, pipx only links the stack's own entry point and the MCP/rag/dashboard binaries stay hidden inside the venv.
33
+
34
+ `uv` users: install each package as its own tool instead, since `uv tool` has no `--include-deps` equivalent yet:
35
+
36
+ ```bash
37
+ uv tool install getbased-mcp
38
+ uv tool install "getbased-rag[full]"
39
+ uv tool install getbased-dashboard
40
+ uv tool install "getbased-agent-stack[full]"
30
41
  ```
31
42
 
32
43
  Pulls:
@@ -18,4 +18,5 @@ tests/test_cli.py
18
18
  tests/test_env_file.py
19
19
  tests/test_integration.py
20
20
  tests/test_mcp_configs.py
21
+ tests/test_systemd_units.py
21
22
  tests/test_units.py
@@ -295,6 +295,57 @@ def test_init_reuses_existing_api_key(stack_home, fake_shell, monkeypatch):
295
295
  assert key_path.read_text().strip() == "preexisting_key"
296
296
 
297
297
 
298
+ def test_init_yes_flag_skips_all_prompts(stack_home, fake_shell, monkeypatch):
299
+ """`init --yes` must not call input() or getpass() at all. Scripted
300
+ installers (curl | bash) can't service prompts and the EOF fallback
301
+ triggers a Python getpass echo warning that pollutes output.
302
+ Strict assertion: any prompt call fails the test."""
303
+ def _forbid_input(*a, **kw):
304
+ raise AssertionError("input() called under --yes")
305
+
306
+ def _forbid_getpass(*a, **kw):
307
+ raise AssertionError("getpass() called under --yes")
308
+
309
+ monkeypatch.setattr("builtins.input", _forbid_input)
310
+ monkeypatch.setattr("getpass.getpass", _forbid_getpass)
311
+
312
+ rc, out, _ = _run(["init", "--yes"])
313
+ assert rc == 0
314
+ # Banner reflects the mode so the user sees what happened
315
+ assert "non-interactive" in out.lower()
316
+ # Env file + units still land
317
+ assert env_file.env_file_path().exists()
318
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-rag.service").exists()
319
+
320
+
321
+ def test_init_yes_installs_units_without_asking(stack_home, fake_shell, monkeypatch):
322
+ """Default for the install-units prompt is Yes, so --yes must also
323
+ install + start. If this regressed to skip, install.sh would
324
+ silently leave services off."""
325
+ monkeypatch.setattr("builtins.input", lambda *a, **kw: "")
326
+ monkeypatch.setattr("getpass.getpass", lambda *a, **kw: "")
327
+
328
+ _run(["init", "--yes"])
329
+ # UnitManager.install() writes service files under XDG_CONFIG_HOME
330
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-rag.service").exists()
331
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-dashboard.service").exists()
332
+
333
+
334
+ def test_init_yes_preserves_existing_token(stack_home, fake_shell, monkeypatch):
335
+ """Non-interactive mode must not nuke a previously-saved token —
336
+ it takes the 'keep current' default, same as pressing Enter."""
337
+ env_file.write_env_file(
338
+ {"GETBASED_TOKEN": "keep_me", "GETBASED_STACK_MANAGED": "1"}
339
+ )
340
+ # No input/getpass expected, but stub defensively in case a future
341
+ # code path adds an unguarded prompt — test still catches it.
342
+ monkeypatch.setattr("builtins.input", lambda *a, **kw: (_ for _ in ()).throw(AssertionError("input under --yes")))
343
+ monkeypatch.setattr("getpass.getpass", lambda *a, **kw: (_ for _ in ()).throw(AssertionError("getpass under --yes")))
344
+
345
+ _run(["init", "-y"])
346
+ assert env_file.read_env_file()["GETBASED_TOKEN"] == "keep_me"
347
+
348
+
298
349
  def test_init_is_reentrant(stack_home, fake_shell, monkeypatch):
299
350
  """Running init twice in a row must not break anything — second call
300
351
  should be a cheap idempotent update, not a destructive rewrite."""
@@ -0,0 +1,109 @@
1
+ """Regression tests for the bundled systemd unit files.
2
+
3
+ Found during the Hermes 0.4.0 clean-install smoke test: several hardening
4
+ directives (ProtectKernelTunables, RestrictNamespaces, etc.) require
5
+ CAP_SYS_ADMIN and fail with 218/CAPABILITIES under `systemctl --user`.
6
+ Inline `# comment` on a directive line also breaks parsing (systemd
7
+ appends the comment to the value).
8
+
9
+ These tests lock in both fixes by asserting on unit-file content."""
10
+ from __future__ import annotations
11
+
12
+ import pytest
13
+
14
+ from getbased_agent_stack import units
15
+
16
+
17
+ # Capability-requiring directives that fail under `systemctl --user`.
18
+ # Any of these in a bundled unit would bring back the CAPABILITIES 218
19
+ # error on a user install.
20
+ FORBIDDEN_USER_MODE_DIRECTIVES = (
21
+ "ProtectKernelTunables",
22
+ "ProtectKernelModules",
23
+ "ProtectControlGroups",
24
+ "RestrictNamespaces",
25
+ "RestrictAddressFamilies",
26
+ "RestrictRealtime",
27
+ "MemoryDenyWriteExecute",
28
+ "SystemCallFilter",
29
+ "SystemCallArchitectures",
30
+ "CapabilityBoundingSet",
31
+ "AmbientCapabilities",
32
+ )
33
+
34
+
35
+ def _directive_values(text: str) -> "list[tuple[str, str]]":
36
+ """Return (key, value) pairs for every `KEY=VALUE` line that is not a
37
+ comment, preserving order. Section headers excluded."""
38
+ out: "list[tuple[str, str]]" = []
39
+ for line in text.splitlines():
40
+ stripped = line.strip()
41
+ if not stripped or stripped.startswith(("#", "[")):
42
+ continue
43
+ if "=" not in stripped:
44
+ continue
45
+ key, _, val = stripped.partition("=")
46
+ out.append((key.strip(), val))
47
+ return out
48
+
49
+
50
+ @pytest.mark.parametrize("name,text", units.bundled_units())
51
+ def test_no_user_mode_forbidden_directives(name, text):
52
+ """Capability-requiring directives must not appear in bundled units —
53
+ they're incompatible with `systemctl --user` and prevent first start."""
54
+ for key, _ in _directive_values(text):
55
+ assert key not in FORBIDDEN_USER_MODE_DIRECTIVES, (
56
+ f"{name} has {key}= which fails under systemctl --user "
57
+ f"(needs CAP_SYS_ADMIN, causes 218/CAPABILITIES)"
58
+ )
59
+
60
+
61
+ @pytest.mark.parametrize("name,text", units.bundled_units())
62
+ def test_no_inline_comments_on_directives(name, text):
63
+ """systemd does not strip inline `# comments` from directive values.
64
+ A line like `MemoryDenyWriteExecute=false # needed for ONNX` parses
65
+ the full string (including '# needed for ONNX') as the value, which
66
+ breaks boolean directives and was a real Hermes install bug."""
67
+ for key, val in _directive_values(text):
68
+ # An '#' AFTER meaningful content on a directive line is an inline
69
+ # comment. Leading-# lines were filtered already by _directive_values.
70
+ assert "#" not in val, (
71
+ f"{name} directive {key}= has an inline comment; systemd won't "
72
+ f"strip it. Move the comment to its own line."
73
+ )
74
+
75
+
76
+ @pytest.mark.parametrize("name,text", units.bundled_units())
77
+ def test_restart_always_not_on_failure(name, text):
78
+ """Restart=always (not on-failure) so a clean SIGTERM triggers restart.
79
+ `on-failure` was the exact bug that kept lens-rag.service dead on Hermes
80
+ for 5 hours — don't regress that."""
81
+ restart_values = [val for key, val in _directive_values(text) if key == "Restart"]
82
+ assert restart_values, f"{name} has no Restart= directive"
83
+ assert restart_values[-1] == "always", (
84
+ f"{name} has Restart={restart_values[-1]}; must be `always` so clean "
85
+ f"SIGTERM still brings the service back."
86
+ )
87
+
88
+
89
+ @pytest.mark.parametrize("name,text", units.bundled_units())
90
+ def test_reads_shared_env_file(name, text):
91
+ """Every unit must source the shared env file via EnvironmentFile=
92
+ so the GETBASED_STACK_MANAGED flag + token + paths are available."""
93
+ values = [val for key, val in _directive_values(text) if key == "EnvironmentFile"]
94
+ assert any(
95
+ "getbased/env" in v for v in values
96
+ ), f"{name} does not source %h/.config/getbased/env via EnvironmentFile="
97
+
98
+
99
+ @pytest.mark.parametrize("name,text", units.bundled_units())
100
+ def test_sets_stack_managed_flag(name, text):
101
+ """The opt-in flag must be set via Environment= so the Python loader
102
+ inside the binary picks up the shared file. Without this, services
103
+ run without the shared config and every user has to duplicate env
104
+ into the unit file manually."""
105
+ envs = [val for key, val in _directive_values(text) if key == "Environment"]
106
+ flag_set = any(e.startswith("GETBASED_STACK_MANAGED=1") for e in envs)
107
+ assert flag_set, (
108
+ f"{name} does not set GETBASED_STACK_MANAGED=1 via Environment="
109
+ )