Skip to content

Embedding hooks: external decider, transactional event hook, required agent auth - #170

Open
cachafla wants to merge 9 commits into
mainfrom
aa/extension-points
Open

cachafla wants to merge 9 commits into
mainfrom
aa/extension-points

Conversation

@cachafla

@cachafla cachafla commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What this adds

Embedding hooks that let another Go service run Atryum as its enforcement engine and own the decisions. Standalone Atryum (no options set) behaves as before.

  • WithDecider(d): every tool call on every path (MCP proxy, direct invoke, hook Submit, the Managed Agents bridge) is handed to d, which returns allow, notify, deny or pending. With a decider set, Atryum's own approval rules, plans, default policy provider, operator routes and UI are not used, and the summarizer doesn't send tool arguments to Atryum's LLM backend. A decider error, unknown verdict or panic denies the call.
  • WithEventHook(h): h runs inside the database transaction that changes an invocation, so an embedder can record events atomically with the state change. Invocation writes are now transactional (internal/store/tx.go). If the decision can't be recorded, the call is denied and never runs. Results recorded after execution use a context the client can't cancel, so a disconnect can't drop them.
  • WithRequireAgentAuth(): refuses to start without an [[auth]] block, refuses auth_debug.skip_verify, rejects agent routes with no verified identity (401), ignores caller-supplied agent_id, and requires HTTPS issuer and JWKS URLs except on loopback.
  • WithDatabaseURL(url) and Migrate: let an embedder supply the database and run migrations, and start with a role that lacks CREATE once the schema exists.
  • Approval waits are read from the database rather than an in-process channel, and cancelling after an approval keeps the recorded approval.

docs/architecture.md and CHANGELOG.md are updated.

Why

Agent Authority (ValidMind's commercial control plane) embeds Atryum through pkg/atryum and needs to plug in its own decisions and append-only event record. The hooks are generic, so they live here.

Tests

go test ./... passes. New tests cover the decider on each path, deny on decider error and panic, transaction rollback when the hook fails, fail-closed when a decision can't be recorded, results surviving a client disconnect, cancel-after-approve, required agent auth, and the summarizer under a decider on the MCP proxy path. Each was checked to fail without its fix.

🤖 Generated with Claude Code

Rebased onto #168 (main 7518c28) — 15 October

  • Conflicts kept both sides. In decider mode, Routes still returns early with the agent routes only, ahead of Nibz/claude/api keys #168's operator middleware. runServer keeps Nibz/claude/api keys #168's identity wiring next to SetAgentRoutesOnly and UseExternalDecider.
  • New: WithAgentAPIKeys(enabled bool) (4e3d884). Nibz/claude/api keys #168 accepted atr_ agent API keys on the agent routes unconditionally.
    • Keys stay on by default for standalone Atryum and are now off by default under WithDecider, whose embedder keeps its own agent registry.
    • With keys off, an atr_ bearer gets 401 on /mcp/, /api/v1/invocations and /api/v1/external/*, even with no IdP configured, where an unknown bearer would otherwise be ignored.
    • Tests cover both modes, with and without an IdP, using a real issued key, plus the option's defaults.
  • The commit AA pinned before the rebase stays fetchable as the tag aa/pin-e479127.

cachafla and others added 9 commits October 15, 2026 08:42
Embedding programs can now decide every tool call (allow, notify, deny, or
pending with a review ID) and receive every invocation change inside its
database transaction. With a decider set, Atryum skips its rules, plans and
default policy and mounts only the agent-facing routes.

Every invocation state change is now written with its event in one
transaction, and a human-gated MCP call waits on its database row instead of
an in-process channel, so a decision recorded by another process releases
it. Decisions are compare-and-set on status. Wake-up is still by polling;
LISTEN/NOTIFY is the follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migrate applies built-in and extension migrations without starting the
server, for an embedding program's separate migrate step. The environment
variable lets such a program's other commands share one database setting
with the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a decider set, the MCP proxy no longer offers the atryum_rules and plan
tools, annotates tools/list with Atryum rules, appends rules to denials, or
returns initialize instructions that point at them: the embedding program's
decider, not Atryum's rules table, governs the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the embedding extension points:

- A decision that cannot be recorded before dispatch now denies the call
  instead of running it with no record. Denials whose record fails are
  logged loudly instead of dropped.
- After a tool has run, its outcome is written with a context the caller's
  disconnect cannot cancel, and a record failure is logged, not returned,
  so the agent is not invited to retry a call that already ran.
- A held call whose caller goes away re-reads its row before failing it,
  so a recorded approval is kept, and retries with backoff rather than
  leaving the row held with no waiter.
- A decider panic is recovered and denies the call. Under a decider, held
  calls are not sent to the summarizer and plans are not matched.
- WithRequireAgentAuth refuses to start without an enabled [[auth]] issuer
  or with skip_verify, and agent routes reject requests without a verified
  identity, so a caller-supplied agent_id never stands in for one.
- WithDatabaseURL replaces the ATRYUM_DATABASE_URL override, which changed
  standalone behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Postgres checks CREATE on the schema even for CREATE TABLE IF NOT EXISTS
on an existing table, so a least-privilege service role could not start
Atryum. The migrations tables are now created only when missing; a pending
migration still fails fast for such a role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under WithRequireAgentAuth, an issuer or JWKS URL must be HTTPS; plain HTTP
is allowed only on loopback, for local development. Keys fetched over plain
HTTP could be swapped in transit.

The summarizer-under-decider test now holds a call on the MCP proxy path
(Invoke), where the leak was; it fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#168 enables atr_ agent API keys on the agent routes unconditionally. An
embedder with WithDecider registers agents itself and would not know a
key's agents.id, so keys are now off by default there (standalone keeps
them on), and WithAgentAPIKeys overrides either way. With keys off, an
atr_ bearer gets 401 on the agent routes even where an unrecognised bearer
would otherwise pass (no IdP).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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