getbased-agent-stack 0.4.0__tar.gz → 0.4.1__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.4.1}/PKG-INFO +1 -1
  2. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/pyproject.toml +1 -1
  3. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/__init__.py +1 -1
  4. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/systemd/getbased-dashboard.service +3 -7
  5. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/systemd/getbased-rag.service +7 -8
  6. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1/src/getbased_agent_stack.egg-info}/PKG-INFO +1 -1
  7. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/SOURCES.txt +1 -0
  8. getbased_agent_stack-0.4.1/tests/test_systemd_units.py +109 -0
  9. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/LICENSE +0 -0
  10. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/README.md +0 -0
  11. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/setup.cfg +0 -0
  12. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/cli.py +0 -0
  13. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/env_file.py +0 -0
  14. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/mcp_configs.py +0 -0
  15. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/units.py +0 -0
  16. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/dependency_links.txt +0 -0
  17. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/entry_points.txt +0 -0
  18. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/requires.txt +0 -0
  19. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/top_level.txt +0 -0
  20. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/tests/test_cli.py +0 -0
  21. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/tests/test_env_file.py +0 -0
  22. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/tests/test_integration.py +0 -0
  23. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/tests/test_mcp_configs.py +0 -0
  24. {getbased_agent_stack-0.4.0 → getbased_agent_stack-0.4.1}/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.4.1
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
@@ -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.4.1"
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"
@@ -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.4.1
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
@@ -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
@@ -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
+ )