Skip to content

feat(proxy): sideband governance API for hook-based adapters (#12)#62

Merged
olivrg merged 6 commits into
mainfrom
feat/sideband-evaluate-api
Jun 13, 2026
Merged

feat(proxy): sideband governance API for hook-based adapters (#12)#62
olivrg merged 6 commits into
mainfrom
feat/sideband-evaluate-api

Conversation

@olivrg

@olivrg olivrg commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Description

Implements #12 — the first step of the OpenClaw adapter runway. Adds four experimental endpoints on the SDK sideband (127.0.0.1:3200) so hook-based agent frameworks (OpenClaw first, future adapters next) can drive Helio's policy engine
without an MCP transport to interpose on. Helio acts as the policy decision point; the framework's hook is the enforcement point.

Supporting changes:

Enforcement-grade note: the hook path is documented as host-enforced — a cooperative tier, deliberately not marketed as proxy-grade. The structural guarantee is unchanged where it physically holds (MCP path).

Follow-ups (not in this PR): policy primitives #13, dashboard rendering #16, the adapter itself #11.

Closes #12

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (no functional changes)
  • Documentation
  • CI / build / tooling

Packages Affected

  • packages/proxy
  • packages/dashboard
  • packages/python-sdk
  • Root config / monorepo tooling
  • docs/
  • examples/

Checklist

  • I have read CONTRIBUTING.md
  • My code follows the existing style (ESLint + Prettier pass)
  • TypeScript strict mode — no any types or @ts-ignore without justification
  • I have added or updated tests for my changes
  • All CI checks pass (pnpm secrets:scan, pnpm docs:check:ci, pnpm audit --audit-level=high, pnpm build, pnpm lint, pnpm format:check, pnpm typecheck, pnpm test)
  • I have updated documentation if this changes user-facing behavior
  • Commit messages follow Conventional Commits (e.g. feat:, fix:, docs:)

How to Test

  1. Enable the sideband (sdk.enabled: true in helio.yaml), run npx @gethelio/proxy start, and note the printed HELIO_ADAPTER_TOKEN. POST /evaluate to 127.0.0.1:3200 with { "origin": "openclaw", "tool": { "name": "send" }, "arguments": {} } and Authorization: Bearer <adapter token>; confirm a decision + evaluation_id come back.
  2. Run the full evaluate → audit loop: take the evaluation_id, POST /audit { "evaluation_id": "...", "status": "success" }; confirm 201, then replay the identical payload and confirm 200 { already_finalized: true } (idempotent, no double-count).
  3. Scoped tokens: confirm /evaluate rejects the HELIO_SDK_TOKEN with 401, and /evidence rejects the HELIO_ADAPTER_TOKEN with 401.
  4. Shared budget: with a rate_limit rule, consume a slot via the sideband (/evaluate + /audit) and confirm a subsequent MCP tools/call sees the reduced budget.
  5. pnpm --filter @gethelio/proxy test — full suite (1513 tests) passes, including decision-pipeline, governance-service, governance-api, and sideband-shared-limiter.

Additional Context

Endpoints ship labeled experimental until a second adapter validates the contract's neutrality. Full reference in docs/adapter-api.md.

olivrg added 6 commits June 13, 2026 20:54
…date (#12)

Primitives the sideband governance path builds on:

- RateLimiter.record() / SpendLimiter.record() unconditionally commit a call
  against the window (even over the limit) for the deferred-consumption model:
  /evaluate peeks, /audit records once the call has run. Warnings fire only
  while within the limit, matching check(). Spend record() throws on a
  negative/non-finite amount (those are rejected at /evaluate; reaching record()
  is a logic bug).
- ToolAnnotationCache.updateSingle() merges one tool definition incrementally
  without rebuilding the cache-wide present/current maps, so per-origin adapter
  caches can baseline tools one /evaluate at a time without degrading log-mode
  drift evaluation.
- Extract canonicalize()/sortKeysDeep() into util/canonical-json for reuse by
  the audit idempotency hash.
Move the decision half of GovernedForwarder.handleToolsCall (rule evaluation,
drift gate, flag_destructive escalation, evidence/dependency grounding,
stricter-of-both log mode, dry-run determination) into policy/decision-pipeline
as a pure decide() that never touches limiter state. The forwarder now calls
decide() and applies the limiter step itself, exactly as before.

Behavior-preserving: MCP enforcement is bit-identical and the existing proxy
test suite passes unmodified. This lets the sideband governance path (#12)
reuse one decision engine instead of forking security-critical orchestration.
Shared non-MCP-origin record shape for #12/#13/#16:

- record_kind discriminator (tool_call | drift_event | install_scan |
  evaluation_expired), origin (mcp or an adapter origin string), and a metadata
  JSON column (reserved keys channel_id/sender_id/sender_name/conversation_id).
- Columns join REQUIRED_AUDIT_COLUMNS (clean-break migration, consistent with
  prior schema additions) plus record_kind/origin indexes and query filters.
- GovernedForwarder tags its writes origin:'mcp' with the appropriate
  record_kind (tool_call / drift_event).
Adapter-owned approvals where the framework runs its own approval UI:

- ApprovalRouter.createNativeTicket()/resolveNativeTicket() create and resolve
  a queue ticket (channel_name native:<origin>) and fire the onSubmit/onResolve
  SSE callbacks, but hold no promise, start no timeout/escalation timers, and
  notify no channel (the adapter notifies). resolveNativeTicket refuses
  router-managed tickets so a held MCP request can never be left dangling.
- Dashboard approve/deny/break-glass reject native tickets with 409
  native_ticket (an operator decision cannot propagate to the adapter's UI).
- New first-class 'cancelled' ApprovalStatus (proxy enum, queue.resolve,
  APPROVAL_STATUSES, dashboard badge) instead of overloading client_disconnected.
Four experimental endpoints on the SDK sideband so hook-based frameworks
(OpenClaw first) can drive the policy engine without an MCP transport:

- POST /evaluate — decide a tool call, side-effect-free on counters; carries an
  optional tool definition for per-origin drift baselining
- POST /audit — record the outcome, consuming counters; idempotent on
  evaluation_id via canonical-payload tombstones
- POST /install-scan — observational until install-time rules (#13)
- POST /approval/:id/resolve — record a natively-handled approval

GovernanceService owns the pending-evaluation registry (TTL + memory budgets),
per-origin drift caches, the D5 finalize/response matrix, and on-access deadline
enforcement on both /audit and resolve. Fail-closed wiring: construction and
hot-reload throw GovernanceConfigError if an approval-capable policy has no
ApprovalRouter. Mounted on the SDK sideband with a scoped HELIO_ADAPTER_TOKEN
(distinct from HELIO_SDK_TOKEN) and a 1 MiB body limit; the CLI reuses the MCP
limiters/queue/router/audit-writer so the budget is shared across both paths.
Adds sdk.evaluation_ttl config and an optional id arg to AuditWriter push.
- New docs/adapter-api.md: the four endpoints, the enforcement-grade ladder
  (structural / network / host-enforced), the normative fail-closed adapter
  requirement, scoped tokens, idempotency, the crash-TTL/TOCTOU caveats, and the
  experimental label.
- README gains the enforcement-grade ladder so the structural promise is scoped
  to where it physically holds; configuration.md documents sdk.evaluation_ttl
  and the dual SDK/adapter tokens; audit.md documents record_kind/origin/
  metadata; sideband-api.md disambiguates the dashboard sideband from the SDK
  sideband.
@olivrg olivrg force-pushed the feat/sideband-evaluate-api branch from 04d4953 to 592f2dd Compare June 13, 2026 19:55
@olivrg olivrg merged commit e1788f3 into main Jun 13, 2026
3 checks passed
@olivrg olivrg deleted the feat/sideband-evaluate-api branch June 13, 2026 20:00
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.

Sideband decision API: framework-neutral governance endpoints for hook-based adapters

1 participant