DEV Community

Cover image for Turn a DevOps API into Governed Agent Skills with NodeJS
Nisa Fatima
Nisa Fatima

Posted on

Turn a DevOps API into Governed Agent Skills with NodeJS

It's 3 AM. A production service is misbehaving, you're on-call, and you'd love an agent that can pull the service's health and tee up a restart for you. The catch is obvious: an agent with raw access to a DevOps API is a liability. One bad call could scale you into a huge bill or delete an incident record you needed.

So the real question isn't "can the agent reach the API." It's "which calls should it be allowed to make at all, and how should the dangerous ones be treated differently from the safe ones." That decision is what Skillgate handles, and it's the part we actually build and run in this post.

Scope, up front

This post is about the classification and curation layer: turning an OpenAPI spec into a governed set of skills. Skillgate decides which endpoints become tools, marks which are read-only, flags which writes should require approval, and denies the destructive ones outright.

Wiring an approval flag to a live human-approval pause, and making that pause survive a crash, is the job of the Agent OS runtime, not Skillgate. We link to it at the end. The demo here does not implement that runtime, and this post does not pretend it does.

The problem Skillgate solves

Point an LLM at a DevOps API and you have three bad options:

  • Expose nothing. The agent is useless.
  • Expose everything. Now the model can call DELETE and scale on a whim.
  • Hand-whitelist every route. It works until the API changes, then it rots.

Skillgate replaces all three with opt-in curation plus automatic risk classification. You choose a small surface, and every endpoint on it gets a class based on its method and shape.

From REST endpoint to agent skill

Skillgate's input is an ordinary REST API described by an OpenAPI spec. Nothing about the API is agent-aware. It's the same deploy, scaling, and incident routes your platform already exposes. Each endpoint is described in the standard OpenAPI shape: a method, a path, some parameters, a description, and tags. A representative operation from the DevOps spec looks like this:

{
  "operationId": "getServiceLogs",
  "summary": "Get service logs",
  "method": "get",
  "path": "/api/services/{serviceId}/logs",
  "tags": ["services", "read-only"],
  "parameters": [
    { "name": "serviceId", "in": "path", "type": "string", "required": true },
    { "name": "lines", "in": "query", "type": "integer" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Skillgate reads each operation like that and generates one agent skill from it. The skill it produced for this endpoint is:

{
  "name": "getServiceLogs",
  "description": "Get service logs",
  "method": "GET",
  "path": "/api/services/{serviceId}/logs",
  "parameters": [
    { "name": "serviceId", "in": "path", "type": "string", "required": true },
    { "name": "lines", "in": "query", "type": "integer" }
  ],
  "class": "read",
  "readOnly": true,
  "requiresApproval": false
}
Enter fullscreen mode Exit fullscreen mode

The mapping is direct:

  • The operation's name and description become the skill's name and description. That text is what a model reads to decide when to reach for the skill, which is why strictDescriptions: true in the config matters.
  • The endpoint's parameters become the skill's typed inputs. The model fills serviceId and lines the same way it fills any function's arguments.
  • The HTTP method decides the class. GET becomes a read skill, POST, PUT, and PATCH become write skills that pick up an approval flag, and DELETE is destructive.
  • The invoke config from the next section (base URL and headers) is how the skill runs. When the skill is called, Skillgate makes the real HTTP request to that endpoint and hands the response back.

So "turning a REST API into an agent" is concretely this: one tool per selected endpoint, with typed inputs mirrored from the spec, a risk class assigned from the method, and a live HTTP call behind it. The model planning over those tools is the runtime step that needs an LLM attached. The transformation itself is what Skillgate does, and the report later in this post is the proof of it.

The configuration

The whole setup is one call. This is the real config from the project:

Skillgate.fromOpenApi(devOpsOpenApiSpec, {
  agentName: 'devops-on-call',
  invoke: {
    baseUrl: process.env.DEVOPS_API_BASE_URL || 'http://localhost:4000',
    headers: {
      Authorization: 'Bearer ${DEVOPS_API_TOKEN}',
      'Content-Type': 'application/json',
    },
    ssrfProtection: false,
  },
  include: {
    mode: 'opt-in',
    tags: ['services', 'deployments', 'incidents'],
  },
  warnAbove: 12,
  maxTools: 24,
  strictDescriptions: true,
})
Enter fullscreen mode Exit fullscreen mode

A few choices worth calling out:

  • include.mode: 'opt-in' with a tags filter means nothing becomes a skill unless you asked for it. Only endpoints tagged services, deployments, or incidents are considered.
  • warnAbove and maxTools are the tool-count guardrails. More on those below.
  • ssrfProtection is off here because the target is a first-party service on localhost. Turn it on when your base URL points at an external host.

The spec itself is loaded from a local TypeScript file in this demo, not fetched from a URL or a running server.

What Skillgate produced

The DevOps spec has ten endpoints: five GET, four POST, one DELETE. After the opt-in tag filter and classification, Skillgate registered:

  • 9 included skills
  • 1 denied skill

Of the nine included: five are read-only with no approval flag, and four are writes flagged as requiring approval. The denied one is the DELETE.

Here are the endpoints it exposed:

GET  /api/services
GET  /api/services/{serviceId}/status
GET  /api/services/{serviceId}/logs
GET  /api/services/{serviceId}/metrics
POST /api/services/{serviceId}/restart
POST /api/services/{serviceId}/scale
POST /api/deployments
POST /api/deployments/{deploymentId}/rollback
GET  /api/incidents
Enter fullscreen mode Exit fullscreen mode

The proof: the report

Skillgate can print exactly what it decided. This is a trimmed slice of the real report() output, one read skill, one write skill, and the denied one:

{
  "included": [
    {
      "name": "getServiceStatus",
      "method": "GET",
      "path": "/api/services/{serviceId}/status",
      "class": "read",
      "readOnly": true,
      "requiresApproval": false
    },
    {
      "name": "restartService",
      "method": "POST",
      "path": "/api/services/{serviceId}/restart",
      "class": "write",
      "readOnly": false,
      "requiresApproval": true
    }
  ],
  "denied": [
    {
      "name": "deleteIncident",
      "method": "DELETE",
      "path": "/api/incidents/{incidentId}",
      "class": "destructive",
      "denied": true,
      "denyReason": "destructive method DELETE denied (set classify.allowDestructive to allow)"
    }
  ],
  "warnings": []
}
Enter fullscreen mode Exit fullscreen mode

The full report lists all nine included skills plus the denied one. Every entry carries its class, its readOnly flag, and its requiresApproval flag, so the governance decision is visible and auditable before the model ever sees a tool.

How the classification works

The model is simple and predictable:

  • GET becomes a read skill. Safe, no approval flag.
  • POST becomes a write skill with requiresApproval: true.
  • DELETE is classed destructive and denied unless you set allowDestructive.

One honest detail about admin and internal routes. In this demo they aren't denied by an explicit rule, they're simply never tagged into the opt-in surface, so the tag filter leaves them out. Both paths get you to the same place: they don't become tools.

The flag is a contract, not the enforcement

This is the part the first draft of this post got wrong, so it's worth being precise.

requiresApproval: true is metadata that Skillgate attaches to a skill. It's the contract that says "this operation should not run unhelped." The enforcement, pausing a run, capturing a human decision, and resuming after a crash, lives in the Agent OS runtime, not in Skillgate.

So in this demo, Skillgate has already done the valuable part: it separated the safe operations from the sensitive ones and blocked the destructive one, before any model touched the list. Turning the approval flag into a real pause is the next layer, covered in the Agent OS and Gamma docs linked at the end.

Tool-count guardrails

warnAbove: 12 and maxTools: 24 exist to stop tool explosion. Models pick poorly once the tool list gets long, so Skillgate warns past twelve tools and refuses past twenty-four.

With nine skills, this demo sits comfortably under the line, so the guardrail doesn't fire here. It's the same mechanism that would stop you dumping a two-hundred-endpoint API onto a model, which is the whole subject of the companion CMS post.

Run it

npm install --legacy-peer-deps
npm run build
npm run dev
Enter fullscreen mode Exit fullscreen mode

The app runs on http://localhost:3000, with the HazelJS Inspector at http://localhost:3000/__hazel.

Print the governance decision:

curl -s http://localhost:3000/devops/skillgate/report
Enter fullscreen mode Exit fullscreen mode

One honest setup note: no LLM provider is configured out of the box. On boot you'll see "No AI providers configured. Set API keys or enable Ollama with OLLAMA_ENABLED=true." That means this runs as a classification demo. Skillgate does its curation and produces the report whether or not a model is attached. Add a provider when you want to drive an actual agent loop against these skills.

Relevant environment variables, all optional for the demo:

  • PORT (defaults to 3000)
  • DEVOPS_API_BASE_URL (defaults to http://localhost:4000)
  • DEVOPS_API_TOKEN (used in the auth header, not required for the demo)

What's real today, and what's next

Real and reproducible right now:

  • OpenAPI in, curated and classified skills out
  • Read, write, and destructive classes assigned automatically
  • The destructive DELETE denied by default, with a clear reason
  • A printable report of every decision

The runtime layer you would add next:

  • Turning the requiresApproval flag into a live human-approval pause
  • Making that pause crash-safe with durable runs and SQL-backed state
  • Attaching an LLM so the agent can actually plan and call these skills

Complete Project: DevOPs on call Agent

Takeaway

HazelJS Skillgate is the gate that decides which endpoints become tools and how each one is classed, before the model touches anything. That curation and classification is genuinely useful on its own: it's the difference between handing an agent a raw API and handing it a short, labeled, pre-governed set of skills. The enforcement of those labels is a separate and well-documented layer, and that's exactly how it should be.

Learn more

Top comments (0)