Joopler docs
AI governance

Agent governance

Register the AI agents you operate and the ones you bought, trace what they do run by run, approve the tool servers they call, and prove you can stop any of them.

An agent is not another AI tool. A tool is something your staff use; an agent is something that acts on your systems, unattended, on somebody's behalf - whether you built it or bought it. That carries a different set of obligations, and they are the ones ISO/IEC 42001, EU AI Act Article 14, and the NIST AI RMF all converge on: a named owner, an approved level of autonomy, a record of what it actually did, and a stop button somebody has pressed.

Joopler governs that in four parts: the register, the run trace, the tool-server approval, and discovery for the agents nobody registered.

Most companies will end up buying far more agents than they build, and a bought agent will never call an API you control to ask permission. Those are governed differently - through the credentials they hold rather than through a pre-action check - and that is covered in Agents you bought below.

The agent register

Register every agent at Agents in the left nav. Each one records:

  • Key - the name the runtime reports itself as, and the identity everything else hangs off.
  • Purpose and owner - the owner is one person, not a team. This is who gets called.
  • Autonomy - suggests only, acts with human approval, or autonomous within its limits.
  • Limits - which tools it may call, a ceiling on steps and write actions per run, daily call and spend caps, and the data-sensitivity threshold above which a human must approve.

An agent that is running but not registered is reported at the top of the page and fails the agent-inventory-complete control. The register is meant to match reality, not intentions.

Give the agent its own credential

On the agent's page, choose Issue credential. You get a jpt_ token, shown once.

That token is bound to that one agent. It can report that agent's runs and ask about that agent's actions, and nothing else - it cannot file telemetry as a different agent, and it reaches no other part of the API. Rotating replaces the previous token immediately, so there is never more than one live at a time.

Trace a run

The unit of governance is the run, not the call. A stream of tool-call rows tells you an agent was busy; a run tells you what it did, in what order, on whose behalf, and where somebody said yes.

Four HTTP calls, or the SDK once it is published. The HTTP contract is stable and is what the SDK wraps, so starting here costs nothing later.

@joopler/agent-sdk is not on npm yet. It ships inside the platform today. If you want it ahead of the public release, ask us and we will send you the package; otherwise use the HTTP calls below, which are the same contract.

import { JooplerAgent } from "@joopler/agent-sdk";

const joopler = new JooplerAgent({
  baseUrl: "https://api.joopler.com",
  token: process.env.JOOPLER_AGENT_TOKEN!,
  // "open" keeps the agent working if Joopler is unreachable (telemetry is lost);
  // "closed" treats an unanswerable check as a refusal.
  onUnreachable: "open",
});

const run = await joopler.startRun({
  runId: crypto.randomUUID(),
  user: "ava@company.com",
  trigger: "schedule",
  goal: "Triage new support tickets",
});

try {
  // Checks policy FIRST, executes only if allowed, records the step either way.
  const issue = await run.guardedCall(
    { server: "github", tool: "create_issue", write: true, args: { title } },
    () => github.createIssue({ title }),
  );
  await run.end("ok");
} catch (err) {
  await run.end("error", String(err));
}

The same thing over plain HTTP, which works today with no dependency:

curl -X POST https://api.joopler.com/v1/agents/runs \
  -H "Authorization: Bearer $JOOPLER_AGENT_TOKEN" \
  -H "content-type: application/json" \
  -d '{"runId":"2026-09-11-triage-001","trigger":"schedule","user":"ava@company.com"}'

curl -X POST https://api.joopler.com/v1/agents/runs/2026-09-11-triage-001/steps \
  -H "Authorization: Bearer $JOOPLER_AGENT_TOKEN" \
  -H "content-type: application/json" \
  -d '{"steps":[{"kind":"tool","server":"github","tool":"create_issue","write":true,"outcome":"ok","durationMs":210}]}'

curl -X POST https://api.joopler.com/v1/agents/runs/2026-09-11-triage-001/end \
  -H "Authorization: Bearer $JOOPLER_AGENT_TOKEN" \
  -d '{"status":"ok"}'

Every step is scanned for secrets, prompt injection, and data classification, then signed into the evidence ledger. Arguments are never stored - only a hash of the redacted text plus what the scan found - so the trail is verifiable without holding your content. Closing the run signs one summary record covering the whole thing, which is the record an auditor verifies.

Agent tool calls also continue to appear on the AI usage dashboard next to your other AI traffic, so nothing lives in a parallel universe.

Enforce before the action

POST /v1/agents/check is the decision point. Call it before executing a tool: a verdict that arrives afterwards is a log entry, not a control. One call applies every limit at once - the agent's lifecycle, its tool list, the approval state of the server the tool belongs to, the arguments, the run's budget, and the approval threshold.

It answers one of three ways:

  • allow - proceed.
  • deny - do not proceed, with a machine-readable code (tool-blocked, write-cap, mcp-drifted, and so on).
  • needs-approval - the action is legitimate and waiting on a human. Send it again with approvedBy once somebody has approved it.

Inline enforcement follows the same Growth-plan rule as the rest of the gateway: below Growth the decision is still made and recorded but always returns allow, and the response says enforced: false. Stopping an agent is never gated - a kill switch that depends on a billing tier is not a kill switch.

The stop button

On the agent's page, Stop agent does two things in one act: the register refuses everything that agent asks for from then on, and its credential is revoked so it cannot even reach the API.

Test the stop button runs the same thing as a drill and records the test. Each agent needs a test within the last twelve months to pass agent-kill-switch-tested, because an untested stop button is a claim rather than a control.

This only applies to an agent you operate, where the stop actually stops something. On an agent marked as bought, the test is refused rather than recorded, and points you at a containment drill instead. Banking a passing control for a kill switch that does not reach the agent would be worse than having no control at all.

Agents you bought

An agent you purchased runs on somebody else's infrastructure. You cannot put a pre-action check in front of it, and pressing Stop agent in Joopler would stop nothing. What you do control is the credentials it holds into your systems, and that is the whole governance surface.

Register one at Agents -> Add agent -> Bought. Rather than describing it, pick the product from the catalogue - the support, research, coding and workflow agents actually on the market are already in there, with their category, the systems they typically reach, and whether the vendor hosts them. Picking from the catalogue fills in the defaults, so the register entry is a click rather than a form.

Discovery finds the ones nobody registered. It reads your identity graph for non-human principals that look like an agent product, your own cloud for model callers, and your assistant configuration for tool servers, then matches what it finds against the same catalogue. Each finding is presented for approval rather than investigation: accept it and it becomes a register entry with the vendor already identified.

Permissions, measured against what you approved

Once a bought agent is registered, Joopler reconciles the credentials it holds on each provider and compares them to the scope you approved - not to what it had yesterday. A vendor that quietly widens what its agent can touch shows as drift, scored by how far the new reach goes:

New reachSeverity
Directory or identity accessHigh
The ability to writeMedium
Read accessLow

Permissions being removed never raises the severity. And a provider Joopler could not read this cycle is excluded from the comparison rather than treated as empty, so a connector outage never reads as "your agent lost its access."

Containment

For an agent you cannot instrument, containment means revoking its credential where that credential actually lives, and the steps differ by provider. The agent's page gives you the real revocation path for each one, ordered fastest first, and tells you which take effect immediately and which wait for a token to expire - an IAM policy change is evaluated on the next request, while an OAuth grant revocation is not felt until the current access token ages out.

Marking a credential revoked in Joopler records the action you took, not its effect. The evidence says so explicitly. Confirming the effect is a separate step, and the difference between them survives into the record.

The containment drill

Stopping a bought agent is a first-class exercise, filed the same way as any other: a date, a signer, and a statement of what was done.

curl -X POST https://api.joopler.com/v1/agents/$KEY/containment/drill \
  -H "Authorization: Bearer $CLERK_TOKEN" -H "content-type: application/json" \
  -d '{"statement":"Revoked the OAuth grant in Google Workspace and confirmed the
       next scheduled run failed to authenticate.","signedBy":"ava@example.com"}'

It is graded. A drill where the revocation took effect and somebody observed it records as proven. A drill where the action was taken but the effect was never observed records as attested, and the two are different words in the evidence. A drill you did not really run does not quietly become a passing control.

Tool servers

Every MCP or tool server your agents can reach is listed at Tool servers. A server appears there the moment an agent calls it, when a host reports its manifest, or when discovery finds it in a config file.

Approval is recorded against a fingerprint of the server's tool definitions, not its name. A tool's description is an instruction the model follows, so a server that keeps its tool names and rewrites what those tools say has changed its behavior while looking identical to any inventory tracking names. Joopler flags it as changed since approval, names which tools were redefined, added, or removed, and - if the agent's policy says so - refuses to let the agent call it until somebody reviews the new definitions.

Report a manifest at connect time:

await joopler.reportServer({
  key: "files",
  transport: "stdio",
  tools: await mcpClient.listTools(),
});

Finding agents nobody registered

The network is the weakest place to look for an agent. A local MCP server over stdio never crosses it, a model invoked inside your own VPC never leaves it, and at the TLS layer an agent and a person calling the same provider look identical. So Agent discovery looks in the four places agents actually show themselves, in the order they are worth trusting:

  1. Your identity graph. Machine credentials and client-credential apps holding AI grants. An agent has to hold credentials to act, and a credential is far more findable than a packet. Pulled live from your identity provider.
  2. Your cloud control plane. Bedrock and equivalent model activity by principal, pulled nightly from CloudTrail for a connected AWS account. This traffic never egresses, so no proxy, secure web gateway, or CASB can see it. Note that InvokeModel is a CloudTrail data event: post those from Athena or CloudWatch Logs if you record them.
  3. Endpoint and repo configuration. MCP host configs (claude_desktop_config.json, .cursor/mcp.json, .vscode/mcp.json, .mcp.json) and agent frameworks in dependency manifests, posted from your MDM or a repo scan. This is the only way a local stdio server is found at all.
  4. Traffic behavior. Computed from your own AI telemetry. Destination tells you nothing here, so this reads cadence, hours, volume, and client - a hint worth a look, not a verdict.

Anything you collect yourself goes to the same endpoint:

curl -X POST https://api.joopler.com/v1/agent-discovery/scan \
  -H "Authorization: Bearer $CLERK_TOKEN" -H "content-type: application/json" \
  -d '{"files":[{"location":"MBP-ava","path":"claude_desktop_config.json","source":"jamf","content":"{...}"}]}'

Each finding is either registered as an agent or dismissed with a reason, so the inbox empties by being acted on. A dismissal survives re-scans.

The controls this produces

ControlPasses when
agent-inventory-completeNo agent is producing telemetry that is not in the register
agent-owner-assignedEvery live agent names an accountable person
agent-autonomy-approvedNo fully autonomous agent runs with no limits at all
agent-activity-loggedRuns are recorded and signed into the evidence ledger
agent-kill-switch-testedEvery agent's stop button was exercised in the last 12 months
mcp-servers-approvedEvery tool server in use is approved and unchanged since

These map to ISO/IEC 42001 (A.3.2, A.4.2, A.6.2.6, A.6.2.8, A.9.2, A.10.2, A.10.4), EU AI Act Articles 12, 14, 19, 25 and 26, and NIST AI RMF GOVERN 1.6, GOVERN 1.7, GOVERN 2.1, GOVERN 3.2, GOVERN 6.1, MAP 3.5, MAP 4.1, MANAGE 2.4, MANAGE 4.1 and MEASURE 2.4. They are evaluated nightly and on demand.

Agent governance