DocumentationReference

Skillgate: Turn REST APIs into Governed Agent Skills

Skillgate converts selected HazelJS (or OpenAPI) endpoints into agent skills — filtered, classified, and wired into Agent OS and optionally MCP. It does not invent business logic from your spec. Domain reasoning stays in your agent prompt / AgentRuntime. Skillgate owns which APIs become tools and how safely.

Quick Reference

  • Purpose: Curate a small, safe skill surface from OpenAPI or Hazel controllers, then register it on ToolRegistry (and optionally MCP).
  • When to use: You already have REST controllers and want an agent that can call a subset of them with HITL on writes.
  • Key concepts: opt-in tags / x-hazel-skill / @AgentSkill, read vs write vs destructive, Skillgate.fromOpenApi / fromModule, register, report, toMcpServer, defaultSkillgateOptions.
  • Dependencies: @hazeljs/skillgate, @hazeljs/agent; optional @hazeljs/swagger (fromModule), @hazeljs/mcp (toMcpServer), @hazeljs/core.
  • Common mistakes: include.mode: 'all' in production; dumping every CRUD route; skipping approval on writes; ignoring report().denied / tool-count warnings.

Mental model

OpenAPI / Hazel controllers
        │
        ▼
   Skillgate filter (opt-in)
        │
        ▼
   Classify (read / write / destructive / admin)
        │
        ▼
   GovernedSkill[]  ──register──►  ToolRegistry (+ HITL flags)
        │
        ├──► AgentRuntime.execute(...)
        └──► toMcpServer(...)  (optional)
LayerOwns
Your controllersBusiness logic & auth
SkillgateAllowlist, classification, HTTP invokers, approval defaults
Agent OSLoop, policies, budgets, durable HITL, leases, identity
MCPExternal tool protocol export

Install

npm install @hazeljs/skillgate @hazeljs/agent
# optional:
npm install @hazeljs/swagger @hazeljs/mcp @hazeljs/cli

Or scaffold:

hazel add skillgate
hazel skillgate init

Path A — From OpenAPI JSON

import { Skillgate } from '@hazeljs/skillgate';
import { ToolRegistry } from '@hazeljs/agent';

const gate = Skillgate.fromOpenApi(spec, {
  include: { tags: ['agent'] }, // default opt-in tags: agent, skillgate
  invoke: {
    baseUrl: 'http://127.0.0.1:3000',
    headers: { Authorization: 'Bearer ${API_TOKEN}' }, // env interpolation
  },
});

const registry = new ToolRegistry();
gate.register(registry, 'api-concierge');
console.log(gate.report());

CLI preview (no runtime):

hazel skillgate from-openapi ./openapi.json

Headers and SSRF

  • ${ENV_NAME} in invoke.headers is expanded from process.env.
  • Set invoke.ssrfProtection: true when baseUrl comes from untrusted input (blocks private / loopback / cloud-metadata hosts). First-party localhost backends usually leave this false.

Path B — From a Hazel module (no hand-written OpenAPI)

Tag controllers with agent / skillgate, or mark methods with @AgentSkill:

import { Controller, Get, Post, Body, Param, ApiTags, HazelModule } from '@hazeljs/core';
import { AgentSkill, Skillgate } from '@hazeljs/skillgate';

@ApiTags('agent')
@Controller('/orders')
class OrdersController {
  @Get('/:id')
  @AgentSkill({ description: 'Fetch an order by id', readOnly: true })
  getOrder(@Param('id') id: string) {
    return { id, status: 'shipped' };
  }

  @Post('/:id/refund')
  @AgentSkill({
    description: 'Refund an order',
    requiresApproval: true,
    class: 'write',
  })
  refund(@Param('id') id: string, @Body() body: { amount: number }) {
    return { id, refunded: true, ...body };
  }
}

@HazelModule({ controllers: [OrdersController] })
class AppModule {}

const gate = Skillgate.fromModule(AppModule, {
  swagger: { title: 'Orders', servers: [{ url: 'http://127.0.0.1:3000' }] },
  invoke: { baseUrl: 'http://127.0.0.1:3000' },
});

fromModule requires @hazeljs/swagger. It calls createOpenApiDocument, then (by default) merges @AgentSkill → OpenAPI x-hazel-skill via enrichAgentSkills: true.

Safety defaults

ClassHow it is inferredDefault behavior
readGET / HEADreadOnly: true, no approval
writePOST / PUT / PATCHrequiresApproval: true
destructiveDELETE (and configured methods)Denied unless classify.allowDestructive
adminPaths matching /admin, /internal, /debug, health, etc.Denied unless classify.allowAdmin

Tool-count guardrails (overridable):

KnobDefault
warnAbove12 — warning in report().warnings
maxTools24 — throws unless force: true
strictDescriptionsoff — when on, rejects placeholder summaries

Production-safe merge helper:

import { defaultSkillgateOptions, Skillgate } from '@hazeljs/skillgate';

const gate = Skillgate.fromOpenApi(spec, defaultSkillgateOptions({
  invoke: { baseUrl: process.env.API_BASE_URL! },
  classify: { allowDestructive: false },
}));

Include modes (curation)

Skillgate.fromOpenApi(spec, {
  include: {
    mode: 'opt-in',              // default — prefer this in prod
    tags: ['agent', 'skillgate'],
    operationIds: ['getOrder'],
    paths: [/^\/orders/],
    methods: ['GET', 'POST'],
    deny: [/admin/i, 'internalHealth'],
  },
});
ModeBehavior
opt-inOnly ops with matching tags, x-hazel-skill, operationIds, or paths
allEvery non-denied op — warns; avoid in production

OpenAPI extension: x-hazel-skill

paths:
  /orders/{id}:
    get:
      operationId: getOrder
      tags: [agent]
      summary: Fetch an order by id
      x-hazel-skill:
        readOnly: true
        name: get_order
    post:
      operationId: refundOrder
      tags: [agent]
      x-hazel-skill:
        requiresApproval: true
        class: write

Boolean form x-hazel-skill: true means “enabled with defaults.”

@AgentSkill decorator

OptionMeaning
nameTool name (default: method name)
descriptionShown to the LLM
readOnlyMarks read skill
requiresApprovalHITL before invoke
classForce read | write | destructive | admin
enabled: falseExplicitly not a skill

Helpers: getAgentSkillMetadata, getAgentSkillMethods, isAgentSkill, toXHazelSkill.

Registration & Agent OS

register attaches HTTP invokers via createSkillInvoker and sets tool-driver metadata used by Agent OS / MCP:

  • requiresApproval, readOnly, capability (skillgate.{class}.{name})
  • riskLevel: read → low, write → medium, destructive → high
  • idempotent for reads
import { AgentRuntime, ToolRegistry } from '@hazeljs/agent';
import { Skillgate } from '@hazeljs/skillgate';

const gate = Skillgate.fromModule(AppModule, {
  invoke: { baseUrl: 'http://127.0.0.1:3000' },
});

const registry = new ToolRegistry();
gate.register(registry, 'api-concierge');

const runtime = new AgentRuntime({
  llmProvider,
  durableSuspend: true, // crash-safe HITL for write approvals
  // wire registry / AgentModule as usual in your app
});

await runtime.execute('api-concierge', userGoal, {
  contract: { name: 'api-slo', maxLatencyMs: 30_000 },
  budget: { maxTokens: 50_000 },
});

Inspect what was curated:

const { included, denied, warnings } = gate.report();
console.table(included.map((s) => ({ name: s.name, class: s.class, hitl: s.requiresApproval })));
console.table(denied.map((s) => ({ name: s.name, reason: s.denyReason })));

MCP export

const server = gate.toMcpServer({
  name: 'hazel-api-skills',
  version: '1.0.0',
  agentName: 'api-concierge',
});
server.listenStdio();

Requires @hazeljs/mcp. MCP tools inherit Skillgate annotations / _meta for tool-driver parity with Agent OS.

End-to-end recipe (API + agent + MCP)

  1. Build REST controllers; tag opt-in surface with @ApiTags('agent') and @AgentSkill.
  2. Skillgate.fromModule(AppModule, { invoke: { baseUrl } }).
  3. gate.register(registry, 'api-concierge') inside your agent module.
  4. Run with Agent OS options (loop, policy, durable HITL, budget).
  5. Optionally gate.toMcpServer(...) for Cursor / Claude Desktop.
  6. Keep skill count ≤ ~12 for reliable tool selection.

Starter: hazeljs-skillgate-agent-starter (OpenAPI demo + MCP entrypoint).

Troubleshooting

SymptomFix
Zero skillsAdd tags / @AgentSkill / x-hazel-skill; check include.mode
SkillgateConfigError on maxToolsCurate surface or raise maxTools / force (not recommended)
Writes run without HITLEnsure classify.writeRequiresApproval (default true) and Agent OS durableSuspend / approvals wired
fromModule throwsInstall @hazeljs/swagger
toMcpServer throwsInstall @hazeljs/mcp
Localhost blockedLeave ssrfProtection false for first-party apps

What not to do

  • Auto-agentize every route (mode: 'all') without curation
  • Claim the agent “learns” business rules from OpenAPI alone
  • Expose admin / destructive tools without HITL and audit
  • Register 30+ skills and expect reliable tool choice