feat: hermes and opencode integration (adapters + auto-config)#12
feat: hermes and opencode integration (adapters + auto-config)#12mayankjain0141 wants to merge 3 commits into
Conversation
- HermesAdapter: detects hermes payloads by cwd+hook_event_name presence,
returns {} on allow and {"decision":"block","reason":"..."} on deny, always
exit 0 (hermes reads JSON body, not exit code)
- init() in adapter_hermes.go prepends HermesAdapter before ClaudeCodeAdapter
so the more-specific format is detected first (both carry hook_event_name)
- integrations/hermes/plugin.yaml: hermes plugin manifest
- integrations/hermes/__init__.py: stdlib-only Python plugin calling HTTP /v1/check
- integrations/opencode/package.json: npm package manifest
- integrations/opencode/src/index.ts: TypeScript plugin with onToolCalled hook
Add steps 6b and 6c to nixis setup that detect and patch hermes-agent config.yaml (shell hook registration) and opencode opencode.json (instructions entry). Both steps are optional and skip gracefully when the tool is not installed. Uninstall steps 2b/2c clean up the entries.
99bda32 to
b7e3b4e
Compare
|
Nice adapter direction — the Hermes/OpenCode split exposes a subtle governance issue that is worth making first-class before more host adapters land. A
For the dashboard/audit trail, I would record the effective enforcement boundary separately from the policy decision, e.g. {
"host": "hermes|opencode|claude_code",
"hookEvent": "pre_tool_call|session.next.tool.called",
"policyDecision": "allow|audit|deny|require_approval",
"enforcementMode": "blocking|audit_only|fail_open",
"hostResponse": "block_json|exit_code_2|none",
"deniedButNotEnforced": false,
"reasonClass": "destructive_git",
"auditRef": "audit_..."
}That keeps the agent/operator from reading “policy denied” as “the host definitely prevented execution” when the adapter could only observe or had to fail open. It also gives a clean place for the model-visible denial vs operator audit split: the model gets a short reason class/safe alternative; the operator audit keeps policy ids, hashes, taint state, cwd hash, retry safety, etc. I put a tiny checker/example of that split here while testing the same boundary: https://github.com/caioribeiroclw-pixel/pluribus/tree/main/examples/agent-firewall-denial-audit No need to adopt that shape exactly; the main point is just to preserve |
|
@caioribeiroclw-pixel Great catch. The biggest issue for me is exactly what you pointed out: seeing "deny" in an audit log strongly implies the action was actually blocked. If an adapter is only observing and can't enforce, that's pretty misleading. I also think this becomes more important as we add more adapters. Different hosts have different capabilities — some can block, some are audit-only, some may fail open. If we don't represent that distinction explicitly, we'll end up handling it inconsistently across adapters later. Your proposed shape makes sense. I'll update #12 so the adapter passes through its enforcement capability/mode and the audit record reflects what actually happened rather than only what the policy evaluation returned. The pluribus example was helpful as well. The reason-class split lines up with something I'd been considering but hadn't fully settled on yet, still thinking on this. For reason classification, I'm leaning toward an explicit Is that roughly the approach you ended up taking? |
|
Yes — I would make For a policy set that large, I’d avoid making the first version a migration blocker. Something like:
So the audit line can say: policy decision was deny, adapter enforcement was blocking/audit-only/fail-open, and the primary reason class was X. That gives you useful aggregate safety reporting without trying to reverse-engineer intent from CEL/name/text later. I’d probably classify only the policies touched by #12 plus a few high-value examples first, then backfill the rest opportunistically once the enum feels stable. |
Summary
cmd/nixis-hook/adapter_hermes.go): detects hermes payloads bycwdfield, returns hermes-native{"decision":"block"}JSON on deny, fails open if daemon unreachable. Prepended in adapter registry viainit()so it takes priority overClaudeCodeAdapter(both carryhook_event_name).integrations/hermes/): stdlib-only plugin that registerspre_tool_call/post_tool_callhooks, calls nixis HTTP/v1/checkat 200ms timeout. Install to~/.hermes/plugins/nixis/.integrations/opencode/): event-subscription plugin that subscribes tosession.next.tool.calledfor audit/classification. OpenCode has no blocking hook mechanism so this is audit-only by architecture.nixis setupauto-config (cmd/nixis/setup_hermes.go,setup_opencode.go): two new optional steps detect and patch hermesconfig.yaml(shell hook registration with# nixis-begin/endmarkers) and opencodeopencode.json(instructions entry). Both skip gracefully if tool not installed; full uninstall support included.Test plan
go test -race ./cmd/nixis-hook/— 10 new hermes adapter tests passgo test -race ./cmd/nixis/— 21 new setup tests (12 hermes + 9 opencode) pass