Skip to content

feat(proxy): context-aware policy primitives (#13)#65

Merged
olivrg merged 3 commits into
mainfrom
feat/policy-context-primitives
Jun 15, 2026
Merged

feat(proxy): context-aware policy primitives (#13)#65
olivrg merged 3 commits into
mainfrom
feat/policy-context-primitives

Conversation

@olivrg

@olivrg olivrg commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Description

Adds three context-aware policy primitives for hook-based (host-enforced) adapters, building on the #12 sideband governance API. Third step of the OpenClaw adapter runway (#12#13#16#11).

  • match.metadata.* — rules can match on adapter-supplied context (channel_id, sender_id, sender_name, conversation_id) plus a virtual agent_id key (read from the request column). Bare-string = eq, or an operator object (eq/neq/contains/regex, regex through the existing ReDoS analyzer). Threaded through the shared decision pipeline; inert on the MCP path by construction (no metadata there). Reserved agent_id inside metadata is rejected as a service-layer invariant (400 reserved_metadata_key).
  • scope: by: sender_id — rate/spend limits keyed per adapter sender, backed by a GovernanceService reservation registry that caps distinct sender keys (fail-closed, sender:*-scoped only, default 50k) so caller-minted sender ids cannot starve the shared MCP-path limiter. Rejected at config when sdk.enabled is false; falls back to tool: with a warning on the MCP path.
  • deny_install — install-time policy (policies.install) evaluated by POST /install-scan. Blocked installs persist policy_decision: deny + block_reason: install_denied and render as a deny in the dashboard.

MCP-path behavior is unchanged (the full existing proxy suite passes); docs (policies.md, adapter-api.md, audit.md) updated alongside the code.

Closes #13

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. match.metadata — add a rule match: { metadata: { channel_id: "C1" } }, action: deny, enable the SDK sideband (sdk.enabled: true), and POST /evaluate with metadata: { channel_id: "C1" }decision: deny; with channel_id: "C2" or no metadata → allow. Confirm the same rule never fires on the MCP path.
  2. sender_id limits — a rate_limit rule with limits: { key: sender_id, … }: two /evaluate→/audit loops for the same sender_id consume one bucket; a different sender is independent. Set sdk.enabled: false and confirm helio validate rejects key: sender_id.
  3. deny_install — add policies.install.rules with a deny_install rule (match: { name: "evil-*", source: npm }), POST /install-scan a matching package → decision: deny, and confirm the audit row shows record_kind: install_scan + block_reason: install_denied.
  4. Reserved keyPOST /evaluate with metadata: { agent_id: "x" }400 reserved_metadata_key.
  5. pnpm --filter @gethelio/proxy test (1565) and pnpm --filter @gethelio/dashboard test (281) — full suites pass.

Additional Context

olivrg added 3 commits June 15, 2026 13:16
Three policy primitives for hook-based (host-enforced) adapters, building on
the sideband governance API:

- match.metadata.*: match rules on adapter-supplied context (channel_id,
  sender_id, sender_name, conversation_id) plus a virtual agent_id key.
  Threaded through the shared decision pipeline; inert on the MCP path by
  construction. Reserved agent_id is rejected inside metadata as a
  service-layer invariant.
- scope by sender_id: rate/spend limits keyed per adapter sender, backed by a
  GovernanceService reservation registry that caps distinct sender keys
  (fail-closed, sender:* only) so sideband traffic cannot starve the shared
  MCP-path limiter. Rejected at config when the sideband is disabled.
- deny_install: install-time policy (policies.install) evaluated by
  /install-scan; blocked installs persist policy_decision=deny +
  block_reason=install_denied and render as a deny in the dashboard.

Docs (policies.md, adapter-api.md, audit.md) updated alongside.
MCP-path behavior is unchanged; the full existing proxy suite passes.
The merged #12 sideband code carried D#/R#/F#/§ labels pointing at the
gitignored implementation plan — unresolvable for external contributors.
Replace them with issue references or plain prose, and fix a stale
install-scan comment that #13 made inaccurate. Comments only; no behavior
change.
A reviewer reading governance-api.ts can't tell why the metadata schema has no
agent_id guard. Record in-code that reserved-key rejection is intentionally
service-layer only (covers direct embedders too), so the point isn't reopened.
Comment only; no behavior change.
@olivrg olivrg merged commit 19a367a into main Jun 15, 2026
3 checks passed
@olivrg olivrg deleted the feat/policy-context-primitives branch June 15, 2026 16:34
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.

Policy engine: context-aware primitives

1 participant