cluxion-agentplugin-preprocessing 0.3.0__tar.gz → 0.3.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 (84) hide show
  1. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/PKG-INFO +4 -2
  2. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/README.md +3 -1
  3. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/pyproject.toml +1 -1
  4. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/__init__.py +1 -1
  5. cluxion_agentplugin_preprocessing-0.3.1/src/cluxion_agentplugin_preprocessing/guard_watch.py +112 -0
  6. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/plugin.py +6 -1
  7. cluxion_agentplugin_preprocessing-0.3.1/tests/test_guard_watch.py +173 -0
  8. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/.github/profile/README.md +0 -0
  9. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/.gitignore +0 -0
  10. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/Docs/README.md +0 -0
  11. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/LICENSE +0 -0
  12. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/adapters/claude/.claude-plugin/plugin.json +0 -0
  13. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/adapters/claude/skills/preprocess/SKILL.md +0 -0
  14. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/adapters/codex/config-snippet.toml +0 -0
  15. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/cluxion-Docs/README.md +0 -0
  16. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/cluxion-Docs/architecture.md +0 -0
  17. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/cluxion-Docs/harness-logic.md +0 -0
  18. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/cluxion-Docs/honesty-preprocessing.md +0 -0
  19. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/cluxion-Docs/install-and-operations.md +0 -0
  20. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/cluxion-Docs/security.md +0 -0
  21. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/Cargo.lock +0 -0
  22. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/Cargo.toml +0 -0
  23. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/pyproject.toml +0 -0
  24. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/context.rs +0 -0
  25. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/dispatch.rs +0 -0
  26. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/guard.rs +0 -0
  27. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/lib.rs +0 -0
  28. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/main.rs +0 -0
  29. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/queue.rs +0 -0
  30. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/rust/cluxion_queue/src/types.rs +0 -0
  31. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
  32. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
  33. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
  34. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
  35. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
  36. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/__init__.py +0 -0
  37. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/__main__.py +0 -0
  38. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/adapters/__init__.py +0 -0
  39. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/adapters/contract.py +0 -0
  40. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/adapters/grok_build.py +0 -0
  41. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/adapters/hermes.py +0 -0
  42. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/adapters/spec.py +0 -0
  43. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/bootstrap.py +0 -0
  44. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/cli.py +0 -0
  45. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/__init__.py +0 -0
  46. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/clarification.py +0 -0
  47. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/context_compress.py +0 -0
  48. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/dispatch_store.py +0 -0
  49. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/harness.py +0 -0
  50. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/intent.py +0 -0
  51. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/ledger.py +0 -0
  52. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/ledger_codec.py +0 -0
  53. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/plan_codec.py +0 -0
  54. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/preprocess.py +0 -0
  55. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/types.py +0 -0
  56. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/core/work_queue.py +0 -0
  57. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/guard_daemon_host.py +0 -0
  58. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/models/__init__.py +0 -0
  59. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/models/supervisor.py +0 -0
  60. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
  61. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/resources/__init__.py +0 -0
  62. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/resources/guard_bridge.py +0 -0
  63. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/resources/py_queue.py +0 -0
  64. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
  65. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
  66. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/web/__init__.py +0 -0
  67. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/src/cluxion_runtime/web/browser_bridge.py +0 -0
  68. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_browser_bridge.py +0 -0
  69. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_clarification.py +0 -0
  70. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_cluxion_runtime_spine.py +0 -0
  71. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_context_compress.py +0 -0
  72. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_contract.py +0 -0
  73. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_dispatch_store.py +0 -0
  74. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_guard.py +0 -0
  75. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_ledger.py +0 -0
  76. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_queue_backends.py +0 -0
  77. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_runtime_adapter_cli.py +0 -0
  78. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_rust_queue.py +0 -0
  79. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/runtime/test_supervisor.py +0 -0
  80. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/test_bootstrap.py +0 -0
  81. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/test_hermes_config.py +0 -0
  82. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/test_packaging_policy.py +0 -0
  83. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/test_plugin.py +0 -0
  84. {cluxion_agentplugin_preprocessing-0.3.0 → cluxion_agentplugin_preprocessing-0.3.1}/tests/test_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluxion-agentplugin-preprocessing
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff.
5
5
  Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
6
6
  Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
@@ -108,6 +108,8 @@ cluxion-runtime plan --surface hermes --prompt "작업 설명"
108
108
  | `cluxion_context_compress` | context 사용률 초과 시 결정론적 압축 (pinned/recent 보존) |
109
109
  | `cluxion_guard` | 실시간 리소스 guard (`status`/`start`/`stop`/`enforce`/`auto-enforce`) |
110
110
 
111
+ Hermes에서는 `on_session_start` hook이 guard daemon을 자동 시작합니다. `CLUXION_GUARD_AUTOSTART=0` 또는 `false`로 끌 수 있고, `post_tool_call` hook은 기본 30초마다 report-only `auto_enforce(dry_run=True)`를 실행합니다. 실제 적용은 `CLUXION_GUARD_AUTO_APPLY=1` 또는 `true`일 때만 켜집니다.
112
+
111
113
  ## 문서
112
114
 
113
115
  - [Docs/README.md](Docs/README.md) — **처음 읽는 분** + 목차
@@ -120,4 +122,4 @@ cluxion-runtime plan --surface hermes --prompt "작업 설명"
120
122
 
121
123
  ## License
122
124
 
123
- Apache-2.0
125
+ Apache-2.0
@@ -76,6 +76,8 @@ cluxion-runtime plan --surface hermes --prompt "작업 설명"
76
76
  | `cluxion_context_compress` | context 사용률 초과 시 결정론적 압축 (pinned/recent 보존) |
77
77
  | `cluxion_guard` | 실시간 리소스 guard (`status`/`start`/`stop`/`enforce`/`auto-enforce`) |
78
78
 
79
+ Hermes에서는 `on_session_start` hook이 guard daemon을 자동 시작합니다. `CLUXION_GUARD_AUTOSTART=0` 또는 `false`로 끌 수 있고, `post_tool_call` hook은 기본 30초마다 report-only `auto_enforce(dry_run=True)`를 실행합니다. 실제 적용은 `CLUXION_GUARD_AUTO_APPLY=1` 또는 `true`일 때만 켜집니다.
80
+
79
81
  ## 문서
80
82
 
81
83
  - [Docs/README.md](Docs/README.md) — **처음 읽는 분** + 목차
@@ -88,4 +90,4 @@ cluxion-runtime plan --surface hermes --prompt "작업 설명"
88
90
 
89
91
  ## License
90
92
 
91
- Apache-2.0
93
+ Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cluxion-agentplugin-preprocessing"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,6 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.3.1"
6
6
 
7
7
  __all__ = ["__version__"]
@@ -0,0 +1,112 @@
1
+ """Hermes hook wiring for automatic guard daemon watch.
2
+
3
+ The hook surface is intentionally best-effort: failures are reported once to
4
+ stderr and never raised into the host agent.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+ import threading
12
+ import time
13
+ from typing import Any
14
+
15
+ from cluxion_runtime.resources import guard_bridge
16
+
17
+ AUTOSTART_ENV = "CLUXION_GUARD_AUTOSTART"
18
+ AUTO_APPLY_ENV = "CLUXION_GUARD_AUTO_APPLY"
19
+ WATCH_INTERVAL_ENV = "CLUXION_GUARD_WATCH_INTERVAL"
20
+ DEFAULT_WATCH_INTERVAL_SECONDS = 30.0
21
+ WARNING_INTERVAL_SECONDS = 300.0
22
+
23
+ _lock = threading.Lock()
24
+ _last_watch_at: float | None = None
25
+ _last_warning_at: float | None = None
26
+
27
+
28
+ def on_session_start(**_: Any) -> None:
29
+ """Start the guard daemon unless ``CLUXION_GUARD_AUTOSTART=0`` or ``false``.
30
+
31
+ Startup is idempotent: an already-running daemon is success. Hook failures
32
+ are logged as one concise stderr warning and never propagated to the host.
33
+ """
34
+ if not _autostart_enabled():
35
+ return
36
+ try:
37
+ result = guard_bridge.start_daemon()
38
+ except Exception as exc:
39
+ _warn(f"cluxion guard autostart failed: {exc}")
40
+ return
41
+ if not result.get("ok", False):
42
+ reason = result.get("reason") or result.get("error") or "unknown"
43
+ _warn(f"cluxion guard autostart failed: {reason}")
44
+
45
+
46
+ def post_tool_call(**_: Any) -> None:
47
+ """Run a throttled guard watch after tool calls.
48
+
49
+ By default this is report-only and calls ``auto_enforce(..., dry_run=True)``.
50
+ Set ``CLUXION_GUARD_AUTO_APPLY=1`` or ``true`` to pass ``dry_run=False`` and
51
+ let the existing owned-only fail-closed enforcement path terminate
52
+ candidates.
53
+ """
54
+ global _last_watch_at
55
+
56
+ now = time.monotonic()
57
+ with _lock:
58
+ if _last_watch_at is not None and now - _last_watch_at < _watch_interval_seconds():
59
+ return
60
+ _last_watch_at = now
61
+
62
+ try:
63
+ result = guard_bridge.auto_enforce([os.getpid()], dry_run=not _auto_apply_enabled())
64
+ except Exception as exc:
65
+ _warn(f"cluxion guard watch failed: {exc}")
66
+ return
67
+ if not result.get("triggered", False) or not result.get("dry_run", True):
68
+ return
69
+
70
+ _warn_triggered(result, now)
71
+
72
+
73
+ def _autostart_enabled() -> bool:
74
+ return os.environ.get(AUTOSTART_ENV, "1").strip().lower() not in {"0", "false"}
75
+
76
+
77
+ def _auto_apply_enabled() -> bool:
78
+ return os.environ.get(AUTO_APPLY_ENV, "").strip().lower() in {"1", "true"}
79
+
80
+
81
+ def _watch_interval_seconds() -> float:
82
+ raw = os.environ.get(WATCH_INTERVAL_ENV)
83
+ if raw is None:
84
+ return DEFAULT_WATCH_INTERVAL_SECONDS
85
+ try:
86
+ return max(0.0, float(raw))
87
+ except ValueError:
88
+ return DEFAULT_WATCH_INTERVAL_SECONDS
89
+
90
+
91
+ def _warn_triggered(result: dict[str, Any], now: float) -> None:
92
+ global _last_warning_at
93
+
94
+ with _lock:
95
+ if _last_warning_at is not None and now - _last_warning_at < WARNING_INTERVAL_SECONDS:
96
+ return
97
+ _last_warning_at = now
98
+
99
+ pids = [str(entry.get("pid")) for entry in result.get("candidates", []) if isinstance(entry, dict)]
100
+ reasons = [str(reason) for reason in result.get("trigger_reasons", [])]
101
+ _warn(
102
+ "cluxion guard triggered"
103
+ f" candidates={','.join(pids) if pids else 'none'}"
104
+ f" reasons={'; '.join(reasons) if reasons else 'unknown'}"
105
+ )
106
+
107
+
108
+ def _warn(message: str) -> None:
109
+ print(message, file=sys.stderr)
110
+
111
+
112
+ __all__ = ["on_session_start", "post_tool_call"]
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from cluxion_agentplugin_preprocessing import runner
8
+ from cluxion_agentplugin_preprocessing import guard_watch, runner
9
9
  from cluxion_agentplugin_preprocessing.schemas import (
10
10
  BOOTSTRAP_SCHEMA,
11
11
  BROWSER_CLICK_SCHEMA,
@@ -30,6 +30,11 @@ if TYPE_CHECKING:
30
30
 
31
31
  def register(ctx: object) -> None:
32
32
  """Register Cluxion preprocessing tools with the host agent."""
33
+ register_hook = getattr(ctx, "register_hook", None)
34
+ if callable(register_hook):
35
+ register_hook("on_session_start", guard_watch.on_session_start)
36
+ register_hook("post_tool_call", guard_watch.post_tool_call)
37
+
33
38
  ctx.register_tool(
34
39
  name="cluxion_plan",
35
40
  toolset="cluxion",
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pytest
6
+
7
+ from cluxion_agentplugin_preprocessing import guard_watch, plugin
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def reset_guard_watch(monkeypatch: pytest.MonkeyPatch) -> None:
12
+ monkeypatch.setattr(guard_watch, "_last_watch_at", None)
13
+ monkeypatch.setattr(guard_watch, "_last_warning_at", None)
14
+ monkeypatch.delenv(guard_watch.AUTOSTART_ENV, raising=False)
15
+ monkeypatch.delenv(guard_watch.AUTO_APPLY_ENV, raising=False)
16
+ monkeypatch.delenv(guard_watch.WATCH_INTERVAL_ENV, raising=False)
17
+
18
+
19
+ class HookContext:
20
+ def __init__(self) -> None:
21
+ self.hooks: dict[str, object] = {}
22
+ self.tools: dict[str, object] = {}
23
+
24
+ def register_hook(self, name: str, handler: object) -> None:
25
+ self.hooks[name] = handler
26
+
27
+ def register_tool(self, *, name: str, **kwargs: object) -> None:
28
+ self.tools[name] = kwargs
29
+
30
+
31
+ class ToolOnlyContext:
32
+ def __init__(self) -> None:
33
+ self.tools: dict[str, object] = {}
34
+
35
+ def register_tool(self, *, name: str, **kwargs: object) -> None:
36
+ self.tools[name] = kwargs
37
+
38
+
39
+ def test_register_adds_guard_hooks_when_supported() -> None:
40
+ ctx = HookContext()
41
+
42
+ plugin.register(ctx)
43
+
44
+ assert ctx.hooks == {
45
+ "on_session_start": guard_watch.on_session_start,
46
+ "post_tool_call": guard_watch.post_tool_call,
47
+ }
48
+ assert "cluxion_guard" in ctx.tools
49
+
50
+
51
+ def test_register_tolerates_context_without_hooks() -> None:
52
+ ctx = ToolOnlyContext()
53
+
54
+ plugin.register(ctx)
55
+
56
+ assert "cluxion_guard" in ctx.tools
57
+
58
+
59
+ def test_autostart_default_on(monkeypatch: pytest.MonkeyPatch) -> None:
60
+ calls = 0
61
+
62
+ def fake_start_daemon() -> dict[str, object]:
63
+ nonlocal calls
64
+ calls += 1
65
+ return {"ok": True, "started": False, "reason": "already_running"}
66
+
67
+ monkeypatch.setattr(guard_watch.guard_bridge, "start_daemon", fake_start_daemon)
68
+
69
+ guard_watch.on_session_start(session_id="s1", telemetry_schema_version=1)
70
+
71
+ assert calls == 1
72
+
73
+
74
+ @pytest.mark.parametrize("value", ["0", "false", "False"])
75
+ def test_autostart_env_gate_off(monkeypatch: pytest.MonkeyPatch, value: str) -> None:
76
+ calls = 0
77
+
78
+ def fake_start_daemon() -> dict[str, object]:
79
+ nonlocal calls
80
+ calls += 1
81
+ return {"ok": True}
82
+
83
+ monkeypatch.setenv(guard_watch.AUTOSTART_ENV, value)
84
+ monkeypatch.setattr(guard_watch.guard_bridge, "start_daemon", fake_start_daemon)
85
+
86
+ guard_watch.on_session_start(session_id="s1", telemetry_schema_version=1)
87
+
88
+ assert calls == 0
89
+
90
+
91
+ def test_autostart_exception_is_swallowed_with_warning(
92
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
93
+ ) -> None:
94
+ def fake_start_daemon() -> dict[str, object]:
95
+ raise RuntimeError("boom")
96
+
97
+ monkeypatch.setattr(guard_watch.guard_bridge, "start_daemon", fake_start_daemon)
98
+
99
+ guard_watch.on_session_start(session_id="s1", telemetry_schema_version=1)
100
+
101
+ captured = capsys.readouterr()
102
+ assert captured.out == ""
103
+ assert "cluxion guard autostart failed: boom" in captured.err
104
+
105
+
106
+ def test_watch_throttle(monkeypatch: pytest.MonkeyPatch) -> None:
107
+ now = 100.0
108
+ calls: list[dict[str, Any]] = []
109
+
110
+ def fake_monotonic() -> float:
111
+ return now
112
+
113
+ def fake_auto_enforce(owned_roots: list[int], *, dry_run: bool) -> dict[str, object]:
114
+ calls.append({"owned_roots": owned_roots, "dry_run": dry_run})
115
+ return {"ok": True, "triggered": False, "dry_run": dry_run}
116
+
117
+ monkeypatch.setattr(guard_watch.time, "monotonic", fake_monotonic)
118
+ monkeypatch.setattr(guard_watch.guard_bridge, "auto_enforce", fake_auto_enforce)
119
+
120
+ guard_watch.post_tool_call(tool_name="terminal", telemetry_schema_version=1)
121
+ guard_watch.post_tool_call(tool_name="terminal", telemetry_schema_version=1)
122
+ now = 131.0
123
+ guard_watch.post_tool_call(tool_name="terminal", telemetry_schema_version=1)
124
+
125
+ assert len(calls) == 2
126
+ assert calls[0]["dry_run"] is True
127
+ assert calls[1]["dry_run"] is True
128
+
129
+
130
+ def test_triggered_dry_run_warning_is_rate_limited(
131
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
132
+ ) -> None:
133
+ now = 100.0
134
+
135
+ def fake_monotonic() -> float:
136
+ return now
137
+
138
+ def fake_auto_enforce(owned_roots: list[int], *, dry_run: bool) -> dict[str, object]:
139
+ return {
140
+ "ok": True,
141
+ "triggered": True,
142
+ "dry_run": dry_run,
143
+ "candidates": [{"pid": 123}, {"pid": 456}],
144
+ "trigger_reasons": ["cpu_avg 99.0 >= sustained_cpu 85.0"],
145
+ }
146
+
147
+ monkeypatch.setenv(guard_watch.WATCH_INTERVAL_ENV, "0")
148
+ monkeypatch.setattr(guard_watch.time, "monotonic", fake_monotonic)
149
+ monkeypatch.setattr(guard_watch.guard_bridge, "auto_enforce", fake_auto_enforce)
150
+
151
+ guard_watch.post_tool_call(tool_name="terminal", telemetry_schema_version=1)
152
+ guard_watch.post_tool_call(tool_name="terminal", telemetry_schema_version=1)
153
+
154
+ captured = capsys.readouterr()
155
+ assert captured.out == ""
156
+ assert captured.err.count("cluxion guard triggered") == 1
157
+ assert "candidates=123,456" in captured.err
158
+ assert "cpu_avg 99.0 >= sustained_cpu 85.0" in captured.err
159
+
160
+
161
+ def test_auto_apply_passes_dry_run_false(monkeypatch: pytest.MonkeyPatch) -> None:
162
+ calls: list[dict[str, Any]] = []
163
+
164
+ def fake_auto_enforce(owned_roots: list[int], *, dry_run: bool) -> dict[str, object]:
165
+ calls.append({"owned_roots": owned_roots, "dry_run": dry_run})
166
+ return {"ok": True, "triggered": False, "dry_run": dry_run}
167
+
168
+ monkeypatch.setenv(guard_watch.AUTO_APPLY_ENV, "true")
169
+ monkeypatch.setattr(guard_watch.guard_bridge, "auto_enforce", fake_auto_enforce)
170
+
171
+ guard_watch.post_tool_call(session_id="s1", telemetry_schema_version=1)
172
+
173
+ assert calls == [{"owned_roots": [guard_watch.os.getpid()], "dry_run": False}]