omnigent-ai--omnigent
158 行
5.1 KiB
Python
158 行
5.1 KiB
Python
"""Hermes ``pre_tool_call`` shell hook for Omnigent policy enforcement.
|
|
|
|
Registered as a ``pre_tool_call`` hook in the per-session
|
|
``HERMES_HOME/config.yaml`` written by :func:`_populate_hermes_home`
|
|
in :mod:`hermes_executor`.
|
|
|
|
Hermes pipes a JSON payload to stdin before each tool execution::
|
|
|
|
{
|
|
"hook_event_name": "pre_tool_call",
|
|
"tool_name": "terminal",
|
|
"tool_input": {"command": "rm -rf /"},
|
|
"session_id": "...",
|
|
"cwd": "..."
|
|
}
|
|
|
|
The hook evaluates ``PHASE_TOOL_CALL`` policy via the Omnigent server.
|
|
To block, it writes to stdout::
|
|
|
|
{"decision": "block", "reason": "..."}
|
|
|
|
Empty JSON or ``{}`` means allow.
|
|
|
|
Environment variables (set by the wrapper shell script):
|
|
|
|
_OMNIGENT_SERVER_URL : Base URL of the Omnigent server
|
|
(e.g. ``http://127.0.0.1:6767``).
|
|
_OMNIGENT_SESSION_ID : Session / conversation ID for policy
|
|
evaluation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
def main() -> None:
|
|
server_url = os.environ.get("_OMNIGENT_SERVER_URL", "")
|
|
session_id = os.environ.get("_OMNIGENT_SESSION_ID", "")
|
|
|
|
if not server_url or not session_id:
|
|
# No server wired -- fail open (allow).
|
|
json.dump({}, sys.stdout)
|
|
return
|
|
|
|
try:
|
|
payload = json.load(sys.stdin)
|
|
except (json.JSONDecodeError, EOFError, ValueError):
|
|
json.dump({}, sys.stdout)
|
|
return
|
|
|
|
tool_name = payload.get("tool_name") or "unknown"
|
|
tool_input = payload.get("tool_input") or {}
|
|
|
|
# Omnigent relay tools are already gated when the relay dispatches them back
|
|
# through the server's tool path; gating them here too parks a duplicate approval
|
|
# card whose long-poll hangs. Hermes' own tools lack the prefix and stay gated.
|
|
if tool_name.startswith(("mcp_omnigent_", "mcp__omnigent__")):
|
|
json.dump({}, sys.stdout)
|
|
return
|
|
|
|
# Build the evaluation request matching the server's EvaluationRequest
|
|
# schema.
|
|
eval_body: dict[str, object] = {
|
|
"event": {
|
|
"type": "PHASE_TOOL_CALL",
|
|
"target": "",
|
|
"data": {
|
|
"name": tool_name,
|
|
"arguments": tool_input if isinstance(tool_input, dict) else {},
|
|
},
|
|
"context": {},
|
|
},
|
|
}
|
|
|
|
url = f"{server_url.rstrip('/')}/v1/sessions/{session_id}/policies/evaluate"
|
|
|
|
try:
|
|
from omnigent.native_policy_hook import (
|
|
policy_hook_reauth,
|
|
policy_hook_request_headers,
|
|
post_evaluate_with_retry,
|
|
)
|
|
|
|
headers = policy_hook_request_headers()
|
|
reauth = policy_hook_reauth(server_url, headers)
|
|
resp, api_error = post_evaluate_with_retry(
|
|
url=url,
|
|
headers=headers,
|
|
eval_request=eval_body,
|
|
# One day — must match the server's ``ask_timeout`` so the hook
|
|
# stays alive while the human responds to the web-UI approval card.
|
|
read_timeout=86400.0,
|
|
hook_label="hermes pre_tool_call",
|
|
# Re-mint the baked one-shot token if it lapses mid-session.
|
|
reauth=reauth,
|
|
)
|
|
except Exception: # noqa: BLE001 -- fail open on import / unexpected error
|
|
json.dump({}, sys.stdout)
|
|
return
|
|
|
|
if resp is None:
|
|
# Network error / retry budget exhausted -- fail closed so a
|
|
# transient server outage doesn't let unreviewed tools through.
|
|
detail = api_error or reauth.failure_reason
|
|
json.dump(
|
|
{
|
|
"decision": "block",
|
|
"reason": (
|
|
f"Policy evaluation unavailable: {detail}"
|
|
if detail
|
|
else "Policy evaluation unavailable"
|
|
),
|
|
},
|
|
sys.stdout,
|
|
)
|
|
return
|
|
|
|
try:
|
|
result = resp.json()
|
|
except Exception: # noqa: BLE001
|
|
json.dump(
|
|
{"decision": "block", "reason": "Malformed policy response"},
|
|
sys.stdout,
|
|
)
|
|
return
|
|
|
|
action = result.get("result", "POLICY_ACTION_ALLOW")
|
|
reason = result.get("reason", "")
|
|
|
|
if action == "POLICY_ACTION_DENY":
|
|
out: dict[str, str] = {"decision": "block"}
|
|
if reason:
|
|
out["reason"] = f"Tool '{tool_name}' denied by Omnigent policy: {reason}"
|
|
else:
|
|
out["reason"] = f"Tool '{tool_name}' denied by Omnigent policy"
|
|
json.dump(out, sys.stdout)
|
|
elif action == "POLICY_ACTION_ASK":
|
|
# The server resolves ASK by parking the HTTP request until the
|
|
# human decides via the web-UI approval card and returning a hard
|
|
# ALLOW/DENY. Receiving ASK here means the gate was not held
|
|
# — fail closed rather than granting unreviewed permission.
|
|
out = {"decision": "block"}
|
|
if reason:
|
|
out["reason"] = f"Tool '{tool_name}' requires approval: {reason}"
|
|
else:
|
|
out["reason"] = f"Tool '{tool_name}' requires approval"
|
|
json.dump(out, sys.stdout)
|
|
else:
|
|
# ALLOW or UNSPECIFIED — empty JSON means no objection.
|
|
json.dump({}, sys.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|