@goplus/agentguard 1.1.28-beta.1 → 1.1.28

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 (66) hide show
  1. package/README.md +3 -2
  2. package/dist/action/detectors/exec.d.ts.map +1 -1
  3. package/dist/action/detectors/exec.js +265 -4
  4. package/dist/action/detectors/exec.js.map +1 -1
  5. package/dist/action/detectors/network.d.ts.map +1 -1
  6. package/dist/action/detectors/network.js +63 -0
  7. package/dist/action/detectors/network.js.map +1 -1
  8. package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
  9. package/dist/adapters/openclaw-plugin.js +2 -0
  10. package/dist/adapters/openclaw-plugin.js.map +1 -1
  11. package/dist/cli.js +14 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/installers.d.ts +1 -0
  14. package/dist/installers.d.ts.map +1 -1
  15. package/dist/installers.js +144 -6
  16. package/dist/installers.js.map +1 -1
  17. package/dist/runtime/evaluator.d.ts +4 -1
  18. package/dist/runtime/evaluator.d.ts.map +1 -1
  19. package/dist/runtime/evaluator.js +59 -10
  20. package/dist/runtime/evaluator.js.map +1 -1
  21. package/dist/runtime/protect.d.ts +1 -0
  22. package/dist/runtime/protect.d.ts.map +1 -1
  23. package/dist/runtime/protect.js +3 -1
  24. package/dist/runtime/protect.js.map +1 -1
  25. package/dist/runtime/types.d.ts +1 -0
  26. package/dist/runtime/types.d.ts.map +1 -1
  27. package/dist/tests/action.test.js +159 -1
  28. package/dist/tests/action.test.js.map +1 -1
  29. package/dist/tests/cli-init.test.js +8 -3
  30. package/dist/tests/cli-init.test.js.map +1 -1
  31. package/dist/tests/feed-cron.test.js +1 -1
  32. package/dist/tests/feed-cron.test.js.map +1 -1
  33. package/dist/tests/installer.test.js +47 -7
  34. package/dist/tests/installer.test.js.map +1 -1
  35. package/dist/tests/integration.test.js +26 -0
  36. package/dist/tests/integration.test.js.map +1 -1
  37. package/dist/tests/mcpb-manifest.test.d.ts +2 -0
  38. package/dist/tests/mcpb-manifest.test.d.ts.map +1 -0
  39. package/dist/tests/mcpb-manifest.test.js +83 -0
  40. package/dist/tests/mcpb-manifest.test.js.map +1 -0
  41. package/dist/tests/runtime-cloud.test.js +111 -0
  42. package/dist/tests/runtime-cloud.test.js.map +1 -1
  43. package/dist/utils/system-paths.d.ts +14 -0
  44. package/dist/utils/system-paths.d.ts.map +1 -0
  45. package/dist/utils/system-paths.js +172 -0
  46. package/dist/utils/system-paths.js.map +1 -0
  47. package/docs/SECURITY-POLICY.md +22 -0
  48. package/docs/hermes.md +46 -15
  49. package/docs/mcpb-build.md +49 -0
  50. package/package.json +5 -2
  51. package/plugins/hermes/README.md +78 -0
  52. package/plugins/hermes/__init__.py +13 -0
  53. package/plugins/hermes/bridge.py +305 -0
  54. package/plugins/hermes/plugin.py +116 -0
  55. package/plugins/hermes/plugin.yaml +19 -0
  56. package/plugins/hermes/tests/conftest.py +8 -0
  57. package/plugins/hermes/tests/helpers.py +70 -0
  58. package/plugins/hermes/tests/test_allow.py +30 -0
  59. package/plugins/hermes/tests/test_ask.py +18 -0
  60. package/plugins/hermes/tests/test_block.py +38 -0
  61. package/plugins/hermes/tests/test_command.py +29 -0
  62. package/plugins/hermes/tests/test_failmodes.py +57 -0
  63. package/plugins/hermes/tests/test_post.py +19 -0
  64. package/plugins/hermes/tests/test_resolution.py +35 -0
  65. package/plugins/hermes/tests/test_validation.py +43 -0
  66. package/skills/agentguard/action-policies.md +22 -0
@@ -0,0 +1,57 @@
1
+ """Engine failures fail closed (pre) unless fail-open is requested."""
2
+
3
+ import subprocess
4
+
5
+ import pytest
6
+
7
+ from helpers import make_protect_runner, register_with
8
+
9
+
10
+ def test_timeout_fails_closed_for_mapped_tool(monkeypatch):
11
+ monkeypatch.delenv("AGENTGUARD_HERMES_FAIL_OPEN", raising=False)
12
+ runner = make_protect_runner(raises=subprocess.TimeoutExpired(cmd="agentguard", timeout=10))
13
+ ctx, _ = register_with(runner)
14
+ result = ctx.hooks["pre_tool_call"]("terminal", {"command": "git status"})
15
+ assert isinstance(result, dict)
16
+ assert result["action"] == "block"
17
+ assert "fail-closed" in result["message"]
18
+
19
+
20
+ def test_os_error_fails_closed(monkeypatch):
21
+ monkeypatch.delenv("AGENTGUARD_HERMES_FAIL_OPEN", raising=False)
22
+ runner = make_protect_runner(raises=OSError("boom"))
23
+ ctx, _ = register_with(runner)
24
+ result = ctx.hooks["pre_tool_call"]("write_file", {"path": "/etc/hosts"})
25
+ assert isinstance(result, dict)
26
+ assert result["action"] == "block"
27
+
28
+
29
+ def test_fail_open_env_allows_on_engine_error(monkeypatch):
30
+ monkeypatch.setenv("AGENTGUARD_HERMES_FAIL_OPEN", "1")
31
+ runner = make_protect_runner(raises=OSError("boom"))
32
+ ctx, _ = register_with(runner)
33
+ result = ctx.hooks["pre_tool_call"]("terminal", {"command": "git status"})
34
+ assert result is None
35
+
36
+
37
+ def test_post_phase_never_blocks_on_engine_error(monkeypatch):
38
+ monkeypatch.delenv("AGENTGUARD_HERMES_FAIL_OPEN", raising=False)
39
+ runner = make_protect_runner(raises=subprocess.TimeoutExpired(cmd="agentguard", timeout=10))
40
+ ctx, _ = register_with(runner)
41
+ result = ctx.hooks["post_tool_call"]("terminal", {"command": "rm -rf /"})
42
+ assert result is None
43
+
44
+
45
+ def test_malformed_output_with_block_exit_code_blocks():
46
+ runner = make_protect_runner(stdout="not-json", returncode=2)
47
+ ctx, _ = register_with(runner)
48
+ result = ctx.hooks["pre_tool_call"]("terminal", {"command": "rm -rf /"})
49
+ assert isinstance(result, dict)
50
+ assert result["action"] == "block"
51
+
52
+
53
+ def test_malformed_output_with_ok_exit_code_allows():
54
+ runner = make_protect_runner(stdout="not-json", returncode=0)
55
+ ctx, _ = register_with(runner)
56
+ result = ctx.hooks["pre_tool_call"]("terminal", {"command": "echo hi"})
57
+ assert result is None
@@ -0,0 +1,19 @@
1
+ """post_tool_call is audit-only and never blocks."""
2
+
3
+ from helpers import make_protect_runner, register_with
4
+
5
+
6
+ def test_post_tool_call_never_blocks_even_on_block_decision():
7
+ ctx, _ = register_with(make_protect_runner(decision="block"))
8
+ result = ctx.hooks["post_tool_call"]("terminal", {"command": "rm -rf /"})
9
+ assert result is None
10
+
11
+
12
+ def test_bridge_post_phase_returns_none():
13
+ _, guard = register_with(make_protect_runner(decision="block"))
14
+ decision = guard.evaluate(
15
+ event="post_tool_call",
16
+ tool_name="terminal",
17
+ args={"command": "rm -rf /"},
18
+ )
19
+ assert decision is None
@@ -0,0 +1,35 @@
1
+ """Engine resolution: npx is opt-in; explicit binary is preferred."""
2
+
3
+ import bridge
4
+
5
+
6
+ def _only_npx(name):
7
+ return "/usr/bin/npx" if name == "npx" else None
8
+
9
+
10
+ def test_npx_not_used_without_optin(monkeypatch, tmp_path):
11
+ monkeypatch.delenv("AGENTGUARD_BIN", raising=False)
12
+ monkeypatch.delenv("AGENTGUARD_HERMES_HOOK", raising=False)
13
+ monkeypatch.delenv("AGENTGUARD_HERMES_ALLOW_NPX", raising=False)
14
+ monkeypatch.setattr(bridge.shutil, "which", _only_npx)
15
+ monkeypatch.setattr(bridge.Path, "home", classmethod(lambda cls: tmp_path))
16
+ argv, mode = bridge.AgentGuardBridge._resolve_invocation()
17
+ assert argv is None and mode is None
18
+
19
+
20
+ def test_npx_used_with_optin(monkeypatch, tmp_path):
21
+ monkeypatch.delenv("AGENTGUARD_BIN", raising=False)
22
+ monkeypatch.delenv("AGENTGUARD_HERMES_HOOK", raising=False)
23
+ monkeypatch.setenv("AGENTGUARD_HERMES_ALLOW_NPX", "1")
24
+ monkeypatch.setattr(bridge.shutil, "which", _only_npx)
25
+ monkeypatch.setattr(bridge.Path, "home", classmethod(lambda cls: tmp_path))
26
+ argv, mode = bridge.AgentGuardBridge._resolve_invocation()
27
+ assert mode == "protect" and argv[0] == "npx"
28
+
29
+
30
+ def test_agentguard_bin_is_preferred(monkeypatch, tmp_path):
31
+ monkeypatch.setenv("AGENTGUARD_BIN", "/opt/agentguard")
32
+ monkeypatch.delenv("AGENTGUARD_HERMES_HOOK", raising=False)
33
+ monkeypatch.setattr(bridge.Path, "home", classmethod(lambda cls: tmp_path))
34
+ argv, mode = bridge.AgentGuardBridge._resolve_invocation()
35
+ assert mode == "protect" and argv == ["/opt/agentguard", "protect"]
@@ -0,0 +1,43 @@
1
+ """Malformed mapped-tool payloads fail closed before the engine is invoked."""
2
+
3
+ from helpers import make_protect_runner, register_with
4
+
5
+
6
+ def test_terminal_without_command_blocks_without_engine():
7
+ calls = []
8
+ ctx, _ = register_with(make_protect_runner(decision="allow", calls=calls))
9
+ result = ctx.hooks["pre_tool_call"]("terminal", {})
10
+ assert isinstance(result, dict) and result["action"] == "block"
11
+ assert "missing" in result["message"]
12
+ assert calls == [] # engine never invoked on a malformed payload
13
+
14
+
15
+ def test_write_file_without_path_blocks():
16
+ ctx, _ = register_with(make_protect_runner(decision="allow"))
17
+ result = ctx.hooks["pre_tool_call"]("write_file", {"content": "x"})
18
+ assert isinstance(result, dict) and result["action"] == "block"
19
+
20
+
21
+ def test_web_extract_without_url_blocks():
22
+ ctx, _ = register_with(make_protect_runner(decision="allow"))
23
+ result = ctx.hooks["pre_tool_call"]("web_extract", {})
24
+ assert isinstance(result, dict) and result["action"] == "block"
25
+
26
+
27
+ def test_malformed_blocks_even_under_fail_open(monkeypatch):
28
+ monkeypatch.setenv("AGENTGUARD_HERMES_FAIL_OPEN", "1")
29
+ ctx, _ = register_with(make_protect_runner(decision="allow"))
30
+ result = ctx.hooks["pre_tool_call"]("terminal", {})
31
+ assert isinstance(result, dict) and result["action"] == "block"
32
+
33
+
34
+ def test_post_phase_does_not_validate():
35
+ ctx, _ = register_with(make_protect_runner(decision="block"))
36
+ result = ctx.hooks["post_tool_call"]("terminal", {})
37
+ assert result is None
38
+
39
+
40
+ def test_wellformed_payload_passes_validation():
41
+ ctx, _ = register_with(make_protect_runner(decision="allow"))
42
+ result = ctx.hooks["pre_tool_call"]("terminal", {"command": "ls"})
43
+ assert result is None
@@ -51,6 +51,28 @@ Scan request body for sensitive data. Priority determines risk level:
51
51
  6. POST/PUT to untrusted domain -> escalate medium to high
52
52
  7. Domain in allowlist -> ALLOW (low)
53
53
 
54
+ ### Social Account Actions
55
+
56
+ Mutating requests to X/Twitter or TweetClaw social account endpoints receive the
57
+ `SOCIAL_ACCOUNT_ACTION` risk tag and escalate to high risk. Balanced mode prompts
58
+ the operator before execution because these requests can post tweets, post tweet
59
+ replies, send direct messages, upload media, create monitors, register webhooks,
60
+ or run giveaway draws.
61
+
62
+ | Example | Risk |
63
+ |---------|------|
64
+ | `POST https://api.twitter.com/2/tweets` | high |
65
+ | `POST https://xquik.com/api/v1/x/tweets` | high |
66
+ | `POST https://xquik.com/api/v1/x/dm/12345` | high |
67
+ | `POST https://xquik.com/api/v1/x/media` | high |
68
+ | `POST https://xquik.com/api/v1/monitors` | high |
69
+ | `POST https://xquik.com/api/v1/webhooks` | high |
70
+ | `POST https://xquik.com/api/v1/draws` | high |
71
+
72
+ Read-only TweetClaw requests such as tweet search, user lookup, or follower
73
+ export remain low risk unless they hit another rule such as secret scanning,
74
+ high-risk TLD handling, or webhook exfiltration.
75
+
54
76
  ## Command Execution Detector
55
77
 
56
78
  ### Dangerous Commands (always DENY, critical)