Skip to content

feat(agent): add a Devin hook adapter - #430

Open
tumberger wants to merge 3 commits into
mainfrom
jens/devin-agent-adapter
Open

tumberger wants to merge 3 commits into
mainfrom
jens/devin-agent-adapter

Conversation

@tumberger

@tumberger tumberger commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Registers a devin agent adapter so kontext hook --agent devin can translate
the Devin runtime's plugin hook payloads. Policy evaluation is untouched — the
adapter decodes the incoming event and encodes the decision in the form that
runtime reads from stdout. Same shape as the existing Codex adapter.

What's here

  • internal/hookruntime/devin.go — decode/encode
  • internal/agent/devin/ — registration, plus the blank import in main.go
  • hook.ErrSkipEvent — a skip contract honored by both hook runners

Decisions worth reviewing

prompt_id is dropped for now. It's a useful per-turn correlator, but it has
no home on hook.Event, and folding it into ToolInput would be wrong:
ToolInput becomes the Cedar evaluation context, so a synthetic attribute there
would be policy-visible and would change the parameters hash recorded on every
decision. A first-class field is a follow-up.

Only pre-tool-use carries a decision. Every other event is observation and
emits no directive. Anything other than an explicit allow blocks, so an
unrecognized decision value fails closed.

An updated tool input is refused, not applied. That runtime replaces
tool_input wholesale rather than merging, so emitting a partial rewrite would
silently drop every field the policy didn't restate, and the post-tool-use hook
would then observe only the rewritten input — leaving the original absent from
the ledger. Nothing populates UpdatedInput today, so this is a guard against a
future change rather than active logic. Input rewriting is worth supporting
properly later, with the pre-rewrite input as the authoritative ledger entry.

hook.ErrSkipEvent is new, and touches shared runners. Some agent runtimes
read a non-zero hook exit as "block the tool call", so reporting an unhandled
lifecycle event as a decode failure would stall the agent on a legitimate
signal. Both runners now exit successfully and emit nothing for a skipped event.
Unknown event names skip for the same reason; malformed input still fails, since
that indicates a bug or a contract change rather than an event we chose not to
handle. The change is additive — it only triggers on that specific sentinel, so
existing adapters are unaffected.

Follow-ups, deliberately not in this PR

Both change behavior for existing agents and deserve their own review:

  1. No recover() on the hook path. An unrecovered panic exits 2, which some
    runtimes read as "block" — so a nil-map bug becomes an opaque stall rather
    than a visible failure. Worth fixing for every adapter, not just this one.
  2. Runner selection. kontext hook uses the managed-observe lifecycle but
    exits 2 on any decode failure; --mode gets the runner that always writes a
    decision and exits 0, but bypasses the lifecycle and dials the Guard socket
    with no autostart. Neither branch is quite right for a hook that must never
    stall the agent.

Testing

New tests cover field mapping, prompt_id exclusion from the Cedar context,
large-integer precision, skip vs. failure classification, the block/allow encode
forms, observation events never emitting a directive, rewrite refusal, and
fail-closed behavior on an unknown decision.

go build ./..., go vet, and gofmt are clean. Full go test ./... passes
except two pre-existing load-sensitive timing tests in internal/guard/judge
(llama_server_test.go), which also fail on main under full-suite load and
pass in isolation.

Registers a `devin` agent so `kontext hook --agent devin` can translate the
Devin runtime's plugin hook payloads. Policy evaluation is unchanged: the
adapter only decodes the incoming event and encodes the decision in the form
that runtime reads from stdout.

Decode maps `session_id` and `tool_use_id` straight through — Devin session IDs
are already namespaced, so unlike the Codex adapter there is no prefix to add
and no synthetic ID to mint. `tool_response` is captured so post-tool-use
records an outcome rather than only an intent.

`prompt_id` is deliberately dropped for now. It is a useful per-turn correlator
but has no home on hook.Event, and folding it into ToolInput would be wrong:
ToolInput becomes the Cedar evaluation context, so a synthetic attribute there
would be policy-visible and would change the parameters hash recorded on every
decision. Adding a first-class field is left for a follow-up.

Encode emits a decision only for pre-tool-use; every other event is observation
and emits no directive. Anything other than an explicit allow blocks, so an
unrecognized decision value fails closed.

An updated tool input is refused rather than applied. That runtime replaces
tool_input wholesale instead of merging, so emitting a partial rewrite would
silently drop every field the policy did not restate, and the post-tool-use
hook would then observe only the rewritten input — leaving the original absent
from the ledger. Nothing populates UpdatedInput today, so this is a guard
against a future change rather than active logic.

Adds hook.ErrSkipEvent for events an adapter recognizes but does not translate,
honored by both hook runners. Some agent runtimes read a non-zero hook exit as
"block the tool call", so reporting an unhandled lifecycle event as a decode
failure would stall the agent on a legitimate signal. Unknown event names skip
for the same reason; malformed input still fails, since that indicates a bug or
a contract change rather than an event we chose not to handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@tumberger
tumberger marked this pull request as ready for review July 30, 2026 22:55
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a Devin hook adapter and a shared sentinel for silently skipping non-actionable lifecycle events.

  • Registers devin in the agent registry and CLI binary.
  • Translates Devin pre-tool-use and lifecycle payloads into shared hook events while preserving numeric precision.
  • Encodes policy denials using Devin’s blocking response and refuses unsupported input rewrites.
  • Updates both hook runners to exit successfully without output when an adapter returns ErrSkipEvent.
  • Adds coverage for registration, payload mapping, decision encoding, malformed input, and skip behavior.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking issues identified.

The Devin adapter follows the existing registry and hook-runner boundaries, limits directives to pre-tool-use events, fails closed on non-allow decisions, and keeps deliberate event skips distinct from malformed input.

Important Files Changed

Filename Overview
internal/hookruntime/devin.go Implements Devin payload decoding, supported-event filtering, input normalization, fail-closed decision encoding, and rewrite refusal without an identified defect.
internal/agent/devin/devin.go Registers the Devin adapter and correctly delegates the shared agent interface to the new codec.
internal/hookruntime/runner.go Treats the new wrapped skip sentinel as successful no-output completion while preserving existing behavior for ordinary decode errors.
internal/guard/hookruntime/runtime.go Adds equivalent skip handling to the mode-aware runner without changing policy evaluation or malformed-input behavior.
internal/hook/domain.go Introduces a dedicated, wrappable sentinel distinguishing deliberately unsupported events from malformed payloads.
cmd/kontext/main.go Blank-imports the Devin package so its adapter is available through --agent devin.

Sequence Diagram

sequenceDiagram
    participant Devin as Devin runtime
    participant CLI as kontext hook
    participant Adapter as Devin adapter
    participant Policy as Policy runtime
    Devin->>CLI: Hook payload on stdin
    CLI->>Adapter: DecodeDevinEvent
    alt Unsupported lifecycle event
        Adapter-->>CLI: ErrSkipEvent
        CLI-->>Devin: Exit 0 with no output
    else Supported event
        Adapter-->>CLI: hook.Event
        CLI->>Policy: Evaluate or ingest event
        Policy-->>CLI: hook.Result
        CLI->>Adapter: EncodeDevinResult
        alt PreToolUse denied
            Adapter-->>Devin: "decision=block and reason"
        else Allowed or observation event
            Adapter-->>Devin: Empty directive object
        end
    end
Loading

Reviews (1): Last reviewed commit: "feat(agent): add a Devin hook adapter" | Re-trigger Greptile

tumberger and others added 2 commits July 31, 2026 09:50
hostedAgentIdentity mapped only Claude and Cowork, so a Devin session exported an
empty agent_provider alongside agent "devin". The hosted ledger accepts that and
the session still shows its own name, but an empty provider reads downstream as
"provider unknown" rather than as the vendor it came from, and it cannot be
grouped with other endpoints from the same vendor.

Maps devin to ("cognition", "devin"), matching the existing shape where the
provider is the vendor and the agent is the product. Verified against a running
daemon: a Devin session now records ("cognition", "devin") beside a Claude
session's ("anthropic", "claude_code").

Codex is still unmapped and still exports an empty provider. That is pre-existing
and left alone here rather than changed as a drive-by.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant