SecurityAnalyzer that returns a SecurityRisk, and a ConfirmationPolicy decides what to do with that risk. By default OpenHands lets its LLM guess the risk. infy.integrations.openhands replaces that guess with infy’s deterministic policy and risk engine, and records every action to infy’s tamper-evident, hash-chained audit trail.
The confirmation gate stays OpenHands’ own. infy contributes the deterministic classification and the cryptographic audit. OpenHands’
ConfirmationPolicy still decides whether a given risk level pauses for a human.How it fits
The adapter is deliberately split into two layers.assess_action
The SDK-free core. Runs one action through infy’s policy, risk, and audit, and returns the OpenHands risk plus whether the policy permits it. Fully unit-tested.
build_analyzer
The thin wrapper that plugs the core into a real OpenHands
SecurityAnalyzerBase. Imports openhands.sdk lazily, so importing the module never requires the SDK.Install
assess_action needs only infy. You install openhands-sdk only to call build_analyzer.
The core: assess_action
assess_action is the tested heart of the integration. It takes a Governance object and one action, and returns a (openhands_risk, allowed) tuple.
1
Classify risk
It builds tool metadata (
verb, risk_tier, side_effect) and calls governance.risk.assess to get an infy risk tier.2
Evaluate policy
It builds an
AuthRequest for invoke_tool on tool_name and calls governance.policy.evaluate. The action is allowed unless the decision effect is Effect.FORBID.3
Audit
It calls
governance.audit.record with the actor, action, resource, decision (allow or deny), risk tier, reasons, and args. Every action is written, allowed or not.4
Map to a SecurityRisk value
It maps the infy tier to an OpenHands risk string. A policy denial is always surfaced as
"HIGH".Risk mapping
The infy risk tier maps to OpenHandsSecurityRisk values as follows.
OpenHands’ top tier is
HIGH, so infy’s critical folds into HIGH. A policy FORBID is also surfaced as HIGH, regardless of the computed tier, so OpenHands’ ConfirmationPolicy stops it.Example
Policy here is deny-by-default, an unlisted tool such as exfiltrate is denied and surfaced as HIGH even though it is not on the deny list.
Wiring it into OpenHands: build_analyzer
build_analyzer returns a SecurityAnalyzerBase subclass whose security_risk method delegates to assess_action.
riskmaps a tool name to{"verb", "risk_tier", "side_effect"}. These profiles feedassess_action.extractoverrides how(tool_name, args)are read from an action event. The default,_default_extract, tries the common action fields (tool_name,action,arguments,args) and falls back to the class name.
ConfirmationPolicy (for example ConfirmRisky). The analyzer classifies and audits every action; the confirmation policy decides what a given risk level does.
Action-shape extraction
OpenHands action objects vary by SDK version, so_default_extract is best-effort. It reads the tool name from the first of tool_name, action, or the action’s class name, and the arguments from arguments or args, coercing non-dict values into a {"value": ...} dict. If your SDK version exposes different fields, supply your own extract.
What is tested
Covered by tests/test_openhands_governance.py
Covered by tests/test_openhands_governance.py
- Denylisted actions return
HIGHandallowed is False, and are audited as a deny. - Unlisted actions are denied by default and surfaced as
HIGH. - Allowed reads return
LOWandallowed is True. - Risk tiers map to OpenHands levels, with
criticalfolding intoHIGH. - Every action is audited, and the chain passes
audit.verify(). build_analyzerimports the SDK lazily: with no SDK present, calling it raisesImportErrororModuleNotFoundError, proving importing the integration module never required the SDK.
Related
Governance overview
The policy, risk, approval, and audit engine behind this adapter.
Audit trail
The tamper-evident, hash-chained log and its
verify().