hiro-agent 0.1.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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hiro Security, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: hiro-agent
3
+ Version: 0.1.0
4
+ Summary: AI security review agent for code, plans, and infrastructure
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: claude-agent-sdk>=0.1.37
9
+ Requires-Dist: click>=8.0
10
+ Requires-Dist: structlog
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == 'dev'
13
+ Requires-Dist: pytest-asyncio; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # hiro-agent
17
+
18
+ AI security review agent for code, plans, and infrastructure. Integrates with Claude Code, Cursor, VSCode Copilot, and Codex CLI to enforce security reviews before commits and plan finalization.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install hiro-agent
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Set up hooks for your AI coding tools
30
+ hiro setup
31
+
32
+ # Review code changes
33
+ git diff | hiro review-code
34
+
35
+ # Review an implementation plan
36
+ cat plan.md | hiro review-plan
37
+
38
+ # Review infrastructure configuration
39
+ hiro review-infra main.tf
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ | Command | Description |
45
+ |---------|-------------|
46
+ | `hiro review-code` | Security review of code changes (stdin: git diff) |
47
+ | `hiro review-plan` | STRIDE threat model review of a plan (stdin) |
48
+ | `hiro review-infra` | IaC security review (file arg or stdin) |
49
+ | `hiro setup` | Auto-detect and configure all AI coding tools |
50
+ | `hiro verify` | Verify hook integrity against installed version |
51
+
52
+ ### Setup Options
53
+
54
+ ```bash
55
+ hiro setup # Auto-detect all tools
56
+ hiro setup --claude-code # Claude Code only
57
+ hiro setup --cursor # Cursor only
58
+ hiro setup --vscode # VSCode Copilot only
59
+ hiro setup --codex # Codex CLI only
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ Set `HIRO_API_KEY` to connect to the Hiro platform for organizational context (security policies, memories, org profile). Without it, reviews still run using your `ANTHROPIC_API_KEY` directly.
65
+
66
+ ```bash
67
+ export HIRO_API_KEY=hiro_ak_... # Optional: Hiro platform context
68
+ export ANTHROPIC_API_KEY=sk-ant-... # Required if HIRO_API_KEY not set
69
+ ```
70
+
71
+ ## How It Works
72
+
73
+ 1. **`hiro setup`** installs hook scripts in `.hiro/hooks/` and configures your AI coding tool to call them
74
+ 2. Hooks track file modifications and block commits until `hiro review-code` has run
75
+ 3. Hooks track plan creation and block finalization until `hiro review-plan` has run
76
+ 4. Review agents use `claude-agent-sdk` to spawn a Claude instance that performs the security review
77
+ 5. When connected to Hiro (`HIRO_API_KEY`), reviews are enriched with your org's security policy, accepted risks, and architecture context
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,66 @@
1
+ # hiro-agent
2
+
3
+ AI security review agent for code, plans, and infrastructure. Integrates with Claude Code, Cursor, VSCode Copilot, and Codex CLI to enforce security reviews before commits and plan finalization.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install hiro-agent
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Set up hooks for your AI coding tools
15
+ hiro setup
16
+
17
+ # Review code changes
18
+ git diff | hiro review-code
19
+
20
+ # Review an implementation plan
21
+ cat plan.md | hiro review-plan
22
+
23
+ # Review infrastructure configuration
24
+ hiro review-infra main.tf
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `hiro review-code` | Security review of code changes (stdin: git diff) |
32
+ | `hiro review-plan` | STRIDE threat model review of a plan (stdin) |
33
+ | `hiro review-infra` | IaC security review (file arg or stdin) |
34
+ | `hiro setup` | Auto-detect and configure all AI coding tools |
35
+ | `hiro verify` | Verify hook integrity against installed version |
36
+
37
+ ### Setup Options
38
+
39
+ ```bash
40
+ hiro setup # Auto-detect all tools
41
+ hiro setup --claude-code # Claude Code only
42
+ hiro setup --cursor # Cursor only
43
+ hiro setup --vscode # VSCode Copilot only
44
+ hiro setup --codex # Codex CLI only
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ Set `HIRO_API_KEY` to connect to the Hiro platform for organizational context (security policies, memories, org profile). Without it, reviews still run using your `ANTHROPIC_API_KEY` directly.
50
+
51
+ ```bash
52
+ export HIRO_API_KEY=hiro_ak_... # Optional: Hiro platform context
53
+ export ANTHROPIC_API_KEY=sk-ant-... # Required if HIRO_API_KEY not set
54
+ ```
55
+
56
+ ## How It Works
57
+
58
+ 1. **`hiro setup`** installs hook scripts in `.hiro/hooks/` and configures your AI coding tool to call them
59
+ 2. Hooks track file modifications and block commits until `hiro review-code` has run
60
+ 3. Hooks track plan creation and block finalization until `hiro review-plan` has run
61
+ 4. Review agents use `claude-agent-sdk` to spawn a Claude instance that performs the security review
62
+ 5. When connected to Hiro (`HIRO_API_KEY`), reviews are enriched with your org's security policy, accepted risks, and architecture context
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "hiro-agent"
3
+ version = "0.1.0"
4
+ description = "AI security review agent for code, plans, and infrastructure"
5
+ requires-python = ">=3.11"
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ dependencies = [
9
+ "claude-agent-sdk>=0.1.37",
10
+ "structlog",
11
+ "click>=8.0",
12
+ ]
13
+
14
+ [project.scripts]
15
+ hiro = "hiro_agent.cli:main"
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/hiro_agent"]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ asyncio_mode = "strict"
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest", "pytest-asyncio"]
@@ -0,0 +1,3 @@
1
+ """AI security review agent for code, plans, and infrastructure."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,104 @@
1
+ """Shared agent runner for local security review agents.
2
+
3
+ CLAUDECODE="" prevents claude-agent-sdk from detecting a nested Claude Code
4
+ session and rejecting the spawn. This is intentional — the review agent is
5
+ a separate subprocess, not a nested invocation of the caller's session.
6
+ """
7
+
8
+ import os
9
+
10
+ import structlog
11
+ from claude_agent_sdk import (
12
+ AssistantMessage,
13
+ ClaudeAgentOptions,
14
+ TextBlock,
15
+ query,
16
+ )
17
+ from claude_agent_sdk.types import McpHttpServerConfig
18
+
19
+ logger = structlog.get_logger(__name__)
20
+
21
+ # Hardcoded — not configurable to prevent SSRF. HTTPS enforced.
22
+ HIRO_MCP_URL = "https://api.hiro.is/mcp/architect"
23
+ HIRO_BACKEND_URL = "https://api.hiro.is"
24
+
25
+
26
+ def _get_mcp_config() -> dict[str, McpHttpServerConfig]:
27
+ """Build MCP server config for connecting to Hiro.
28
+
29
+ Returns an empty dict when HIRO_API_KEY is not set (MCP context
30
+ tools will be unavailable but the review still runs).
31
+ """
32
+ key = os.environ.get("HIRO_API_KEY", "")
33
+ if not key:
34
+ return {}
35
+ return {
36
+ "hiro": McpHttpServerConfig(
37
+ url=HIRO_MCP_URL,
38
+ headers={"Authorization": f"Bearer {key}"},
39
+ ),
40
+ }
41
+
42
+
43
+ def _get_agent_env() -> dict[str, str]:
44
+ """Build env vars for the agent subprocess.
45
+
46
+ When HIRO_API_KEY is set, route LLM calls through the Hiro backend
47
+ proxy to Bedrock (keeps source code within AWS infrastructure).
48
+ Otherwise the agent uses the developer's ANTHROPIC_API_KEY directly.
49
+ """
50
+ env: dict[str, str] = {"CLAUDECODE": ""}
51
+ api_key = os.environ.get("HIRO_API_KEY", "")
52
+ if api_key:
53
+ env["ANTHROPIC_BASE_URL"] = f"{HIRO_BACKEND_URL}/api/llm-proxy"
54
+ env["ANTHROPIC_API_KEY"] = api_key
55
+ return env
56
+
57
+
58
+ async def run_review_agent(
59
+ prompt: str,
60
+ system_prompt: str,
61
+ *,
62
+ cwd: str | None = None,
63
+ allowed_tools: list[str] | None = None,
64
+ max_turns: int = 15,
65
+ ) -> str:
66
+ """Run a local review agent and return its final text output.
67
+
68
+ The agent connects to the Hiro MCP server for organizational context
69
+ (memories, security policy, org profile) and optionally has filesystem
70
+ access via Read/Grep/Glob tools.
71
+
72
+ Only read-only MCP tools are allowed — remember, set_org_context, and
73
+ forget are explicitly excluded to prevent the review agent from
74
+ modifying organizational state.
75
+ """
76
+ mcp_config = _get_mcp_config()
77
+
78
+ # MCP context tools are only available when connected to Hiro
79
+ mcp_tools = []
80
+ if mcp_config:
81
+ mcp_tools = [
82
+ "mcp__hiro__get_org_context",
83
+ "mcp__hiro__recall",
84
+ "mcp__hiro__get_security_policy",
85
+ ]
86
+
87
+ options = ClaudeAgentOptions(
88
+ cwd=cwd,
89
+ allowed_tools=(allowed_tools or []) + mcp_tools,
90
+ system_prompt=system_prompt,
91
+ mcp_servers=mcp_config,
92
+ permission_mode="acceptEdits",
93
+ max_turns=max_turns,
94
+ env=_get_agent_env(),
95
+ )
96
+
97
+ summary = ""
98
+ async for message in query(prompt=prompt, options=options):
99
+ if isinstance(message, AssistantMessage):
100
+ for block in message.content:
101
+ if isinstance(block, TextBlock):
102
+ summary = block.text
103
+
104
+ return summary
@@ -0,0 +1,111 @@
1
+ """CLI entry point: hiro review-code, review-plan, review-infra, setup, verify."""
2
+
3
+ import asyncio
4
+ import os
5
+ import sys
6
+
7
+ import click
8
+ import structlog
9
+
10
+ logger = structlog.get_logger(__name__)
11
+
12
+ # 2 MB stdin cap to prevent accidental piping of huge files
13
+ MAX_STDIN_BYTES = 2 * 1024 * 1024
14
+
15
+
16
+ def _read_stdin(command_name: str) -> str:
17
+ """Read stdin with a 2MB size cap."""
18
+ if sys.stdin.isatty():
19
+ click.echo(f"Error: No input on stdin. Pipe content to `hiro {command_name}`.", err=True)
20
+ raise SystemExit(1)
21
+ data = sys.stdin.buffer.read(MAX_STDIN_BYTES + 1)
22
+ if len(data) > MAX_STDIN_BYTES:
23
+ click.echo(
24
+ f"Error: Input exceeds 2MB limit ({len(data)} bytes). "
25
+ "Reduce the input size or review files individually.",
26
+ err=True,
27
+ )
28
+ raise SystemExit(1)
29
+ return data.decode("utf-8", errors="replace")
30
+
31
+
32
+ @click.group()
33
+ @click.version_option(package_name="hiro-agent")
34
+ def main() -> None:
35
+ """Hiro — AI security review agent."""
36
+
37
+
38
+ @main.command("review-code")
39
+ @click.option("--context", "-c", default="", help="Additional context about the code.")
40
+ def review_code_cmd(context: str) -> None:
41
+ """Review code changes for security issues. Reads diff from stdin."""
42
+ from hiro_agent.review_code import review_code
43
+
44
+ diff = _read_stdin("review-code")
45
+ if not diff.strip():
46
+ click.echo("Error: Empty input. Pipe a diff: git diff | hiro review-code", err=True)
47
+ raise SystemExit(1)
48
+
49
+ cwd = os.getcwd()
50
+ result = asyncio.run(review_code(diff, cwd=cwd, context=context))
51
+ click.echo(result)
52
+
53
+
54
+ @main.command("review-plan")
55
+ @click.option("--context", "-c", default="", help="Additional context about the plan.")
56
+ def review_plan_cmd(context: str) -> None:
57
+ """Review an implementation plan for security concerns. Reads from stdin."""
58
+ from hiro_agent.review_plan import review_plan
59
+
60
+ plan = _read_stdin("review-plan")
61
+ if not plan.strip():
62
+ click.echo("Error: Empty input. Pipe a plan: cat plan.md | hiro review-plan", err=True)
63
+ raise SystemExit(1)
64
+
65
+ result = asyncio.run(review_plan(plan, context=context))
66
+ click.echo(result)
67
+
68
+
69
+ @main.command("review-infra")
70
+ @click.argument("filepath", required=False)
71
+ def review_infra_cmd(filepath: str | None) -> None:
72
+ """Review infrastructure config for security issues. File arg or stdin."""
73
+ from hiro_agent.review_infra import review_infrastructure
74
+
75
+ if filepath:
76
+ filename = os.path.basename(filepath)
77
+ filepath = os.path.abspath(filepath)
78
+ cwd = os.path.dirname(filepath)
79
+ result = asyncio.run(
80
+ review_infrastructure(filepath, filename=filename, cwd=cwd)
81
+ )
82
+ else:
83
+ config = _read_stdin("review-infra")
84
+ if not config.strip():
85
+ click.echo("Error: Empty input. Usage: hiro review-infra <file>", err=True)
86
+ raise SystemExit(1)
87
+ result = asyncio.run(
88
+ review_infrastructure(config, filename="stdin")
89
+ )
90
+
91
+ click.echo(result)
92
+
93
+
94
+ @main.command()
95
+ @click.option("--claude-code", "tools", flag_value="claude-code", help="Claude Code only.")
96
+ @click.option("--cursor", "tools", flag_value="cursor", help="Cursor only.")
97
+ @click.option("--vscode", "tools", flag_value="vscode", help="VSCode Copilot only.")
98
+ @click.option("--codex", "tools", flag_value="codex", help="Codex CLI only.")
99
+ def setup(tools: str | None) -> None:
100
+ """Configure AI coding tool hooks for security review enforcement."""
101
+ from hiro_agent.setup_hooks import run_setup
102
+
103
+ run_setup(tool_filter=tools)
104
+
105
+
106
+ @main.command()
107
+ def verify() -> None:
108
+ """Verify hook integrity against installed package version."""
109
+ from hiro_agent.setup_hooks import run_verify
110
+
111
+ run_verify()
File without changes
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """Enforce Hiro code review before committing.
3
+
4
+ Tracks whether files have been modified since the last review_code call.
5
+ Blocks git commit until a code review has been run on the changes.
6
+
7
+ Tool-agnostic: works with Claude Code, Cursor, VSCode Copilot, and
8
+ as a git pre-commit hook. Detects caller format automatically.
9
+
10
+ State stored in .hiro/.state/ (not tool-specific directories).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import stat
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ STATE_DIR = Path(".hiro/.state")
24
+
25
+
26
+ def _state_path(session_id: str) -> Path:
27
+ safe = session_id.replace("/", "_")
28
+ return STATE_DIR / f"code_review_{safe}.json"
29
+
30
+
31
+ def _load(session_id: str) -> dict[str, Any]:
32
+ path = _state_path(session_id)
33
+ if not path.exists():
34
+ return {}
35
+ try:
36
+ return json.loads(path.read_text())
37
+ except Exception:
38
+ return {}
39
+
40
+
41
+ def _save(session_id: str, state: dict[str, Any]) -> None:
42
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
43
+ os.chmod(STATE_DIR, stat.S_IRWXU) # 0700
44
+ state["updated_at"] = int(time.time())
45
+ path = _state_path(session_id)
46
+ path.write_text(json.dumps(state))
47
+ os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 0600
48
+
49
+
50
+ def _allow() -> None:
51
+ print("{}")
52
+
53
+
54
+ def _deny(message: str) -> None:
55
+ print(
56
+ json.dumps(
57
+ {
58
+ "hookSpecificOutput": {
59
+ "hookEventName": "PreToolUse",
60
+ "permissionDecision": "deny",
61
+ },
62
+ "systemMessage": message,
63
+ }
64
+ )
65
+ )
66
+
67
+
68
+ def _is_git_commit(command: str) -> bool:
69
+ """Check if a bash command is a git commit."""
70
+ stripped = command.strip()
71
+ parts = stripped.split("&&")
72
+ for part in parts:
73
+ tokens = part.strip().split()
74
+ if len(tokens) >= 2 and tokens[0] == "git" and tokens[1] == "commit":
75
+ return True
76
+ return False
77
+
78
+
79
+ def _is_review_command(tool_name: str, tool_input: dict[str, Any]) -> bool:
80
+ """Check if this is a code review action (MCP tool or CLI command)."""
81
+ if tool_name == "mcp__hiro__review_code":
82
+ return True
83
+ if tool_name == "Bash":
84
+ cmd = tool_input.get("command", "")
85
+ # Match both old (hiro_review.review_code) and new (hiro review-code) forms
86
+ if "hiro_review.review_code" in cmd or "hiro review-code" in cmd:
87
+ return True
88
+ return False
89
+
90
+
91
+ def main() -> int:
92
+ try:
93
+ input_data = json.load(sys.stdin)
94
+ except Exception:
95
+ _allow()
96
+ return 0
97
+
98
+ event = input_data.get("hook_event_name", "")
99
+ session_id = input_data.get("session_id", "default")
100
+ tool_name = input_data.get("tool_name", "")
101
+ tool_input = input_data.get("tool_input", {})
102
+
103
+ # --- PostToolUse: track edits and reviews ---
104
+ if event == "PostToolUse":
105
+ if tool_name in ("Edit", "Write"):
106
+ state = _load(session_id)
107
+ files = state.get("modified_files", [])
108
+ file_path = tool_input.get("file_path", "")
109
+ if file_path and file_path not in files:
110
+ files.append(file_path)
111
+ state["modified_files"] = files
112
+ state["needs_review"] = True
113
+ _save(session_id, state)
114
+
115
+ elif _is_review_command(tool_name, tool_input):
116
+ # Review done — clear the flag
117
+ state = _load(session_id)
118
+ state["needs_review"] = False
119
+ state["modified_files"] = []
120
+ _save(session_id, state)
121
+
122
+ _allow()
123
+ return 0
124
+
125
+ # --- PreToolUse: block git commit if review is pending ---
126
+ if event == "PreToolUse" and tool_name == "Bash":
127
+ command = tool_input.get("command", "")
128
+ if _is_git_commit(command):
129
+ state = _load(session_id)
130
+ if state.get("needs_review"):
131
+ files = state.get("modified_files", [])
132
+ file_list = ", ".join(files[-5:])
133
+ if len(files) > 5:
134
+ file_list += f" and {len(files) - 5} more"
135
+ _deny(
136
+ f"Commit blocked: {len(files)} file(s) modified since last "
137
+ f"security review ({file_list}). Run "
138
+ "`git diff | hiro review-code` "
139
+ "before committing."
140
+ )
141
+ return 0
142
+
143
+ _allow()
144
+ return 0
145
+
146
+ _allow()
147
+ return 0
148
+
149
+
150
+ if __name__ == "__main__":
151
+ raise SystemExit(main())