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

@cots/sample

v2.0.0

Published

Cots.ai agent interceptor SDK — configure once, then every governed action is intercepted, evaluated, held for approval if needed, and proven in the audit ledger.

Readme

@cots/sample

Node.js / TypeScript SDK for the Cots.ai Agent Action Firewall & Compliance Audit Gateway. Configure once with your organization's tenantId, your agent's agentId, and this agent's own API key/secret, then wrap every real-world action your agent takes in guard() — it calls the interceptor first and only runs your code if the action is allowed (or gets approved by a human).

Installation

npm install @cots/sample

Quick start

Minimal setup: put your four values (plus, optionally, the gateway URL) in .env, load them with configFromEnv(), and hand them to createCotsClient():

# .env
COTS_TENANT_ID=ten_xxxxxxxxxx
COTS_AGENT_ID=agt_xxxxxxxxxx
COTS_API_KEY=capp_key_xxx
COTS_API_SECRET=capp_secret_xxx
COTS_ACTION_GATEWAY_API_URL=http://localhost:8083/api
import { configFromEnv, createCotsClient } from '@cots/sample';

const cots = createCotsClient(configFromEnv());

try {
  const result = await cots.guard(
    { targetSystem: 'Slack', actionType: 'send_slack_message', actionName: 'Post deployment notice' },
    async () => sendToSlackForReal('#ops', 'Deployment succeeded'),
  );
  console.log(result.decision, result.outcome);
  // decision: 'allowed' | 'blocked' | 'require_approval'
  // outcome:  'executed' | 'blocked' | 'timeout'
} catch (e) {
  // network/HTTP failure calling the interceptor itself -- distinct from a
  // policy decision (blocked/require_approval), which guard() returns, not throws
  console.error(e instanceof Error ? e.message : String(e));
}

A missing COTS_* var throws from configFromEnv(); a present-but-malformed one (swapped key/secret, wrong prefix, a garbled URL) throws from createCotsClient() itself — both fail fast, before any network call, instead of surfacing as a confusing 401 later. createAgentClient still works too (same client, createCotsClient is just the name we'd recommend going forward).

Wire format

Every intercept() call (including the ones guard() makes for you) sends this agent's identity as headers, not in the request body:

POST /v1/intercept
AgentID: <agentId>
TenantID: <tenantId>
APIKEY: <apiKey>
SECRET: <apiSecret>
Content-Type: application/json

{ "target_system": "Slack", "action_type": "send_slack_message", ... }

Configuration

| Option | Required | Default | Description | |---|---|---|---| | tenantId | yes | — | Your organization's ID | | agentId | yes | — | This agent's ID | | apiKey | yes | — | This agent's own API key (from generateCredentials(), see Onboarding below) | | apiSecret | yes | — | This agent's own API secret (same source as apiKey) | | actionGatewayApiUrl | no | http://localhost:8083/api | The one base URL every SDK call goes through — intercept(), approval-status polling, and ping() all hit this; it forwards /v1/intercept to the Rust interceptor and /heartbeat to agent-heartbeat internally | | approvalTimeoutMs | no | 30000 | How long guard()/waitForApproval() waits on a held action before giving up | | approvalPollMs | no | 2000 | Poll interval while waiting on a held action's decision |

The localhost defaults are for local development only — set every URL to your actual deployed endpoints everywhere else.

Reads COTS_TENANT_ID, COTS_AGENT_ID, COTS_API_KEY, COTS_API_SECRET, COTS_ACTION_GATEWAY_API_URL, COTS_APPROVAL_TIMEOUT_MS, COTS_APPROVAL_POLL_MS. COTS_TENANT_ID/COTS_AGENT_ID/COTS_API_KEY/COTS_API_SECRET are required; configFromEnv() throws if any of them is missing. That's the whole config a deployed agent needs — no login, no other service URLs.

Validation

Two layers, so a bad config never gets as far as a real network call:

  • MissingconfigFromEnv() throws immediately if any of the four required COTS_* vars isn't set.
  • MalformedcreateCotsClient()/createAgentClient() throws if a present value doesn't look right: tenantId not shaped like ten_..., agentId not shaped like agt_..., apiKey/ apiSecret not shaped like ..._key_<hex>/..._secret_<hex> (catches e.g. pasting the secret into the key field), or actionGatewayApiUrl not a valid URL. This runs whether the config came from configFromEnv() or a plain object literal you built yourself.

guard() — the one function most agents need

const result = await cots.guard(
  {
    targetSystem: 'Slack',
    actionType: 'send_slack_message',
    actionName: 'Post deployment notice',
    riskScore: 15,
  },
  async () => sendToSlackForReal('#ops', 'Deployment succeeded'),
);

guard() calls the interceptor first. If allowed, your callback runs immediately. If blocked, it never runs. If require_approval, the call holds and polls for the human's decision until one comes in (or approvalTimeoutMs elapses), then runs your callback only if approved.

NormalizedAction fields

| Field | Required | Description | |---|---|---| | targetSystem | yes | The system the action touches (e.g. "Slack", "Email", "Wallet Ledger") | | actionType | yes | Machine-readable action identifier (e.g. "send_slack_message", "deduct_money") | | actionName | no | Human-readable label shown in the audit trail | | principalId | no | The human/service identity the agent is acting on behalf of | | sessionId | no | Groups related actions under one session/conversation | | dataClassification | no | e.g. "pii", "confidential", "financial" — informs policy decisions | | riskScore | no | 0-100, informs risk tiering | | amount | no | For financial actions — policy thresholds evaluate against this | | recipient | no | Email address, phone number, etc. — used by recipient-domain policy rules | | prompt | no | The agent's actual instruction/content, when available — scanned server-side for prompt-injection patterns |

Lower-level API

guard() composes two calls you can also use directly if you want to handle the decision yourself:

const result = await cots.intercept(action);          // just the decision, no execution
const outcome = await cots.waitForApproval(actionEventId); // 'approved' | 'denied' | 'timeout'

ping() / startHeartbeat() — liveness

createCotsClient()/createAgentClient() already start pinging in the background the moment you construct the client — true minimal setup:

const cots = createCotsClient(configFromEnv());
// that's it -- "Last seen" on the Agents dashboard is now kept fresh automatically,
// every 60s, for as long as this process runs.

This is controlled by config, not a second method call (so you don't have to choose between "automatic" and "customized" — just configure it):

| Option | Default | Description | |---|---|---| | autoPing | true | Set false to opt out entirely (e.g. to call startHeartbeat() yourself with a custom onSuccess/onError) | | autoPingIntervalMs | 60000 | Interval for the automatic ping | | autoPingInfo | — | host/region/runtime_version sent with each automatic ping |

COTS_AUTO_PING=false / COTS_AUTO_PING_INTERVAL_MS do the same via configFromEnv().

ping() itself authenticates the same way as intercept() — no login, just this agent's own credentials — and confirms them by returning an explicit pong:

const pong = await cots.ping({ host: 'ip-10-0-0-4', region: 'us-east-1', runtime_version: '1.2.3' });
console.log(pong);
// { pong: true, agent_id: '...', tenant_id: '...', status: 'online', last_heartbeat_at: '...' }

host/region/runtime_version are all optional. Each successful ping() also (best-effort) refreshes the agent's last_seen_at shown on the Agents dashboard.

If you disabled autoPing (or want different timing/callbacks than the config gives you), startHeartbeat() does the periodic part for you — pings once immediately, then every intervalMs — instead of writing your own setInterval:

const cots = createCotsClient({ ...configFromEnv(), autoPing: false });

cots.startHeartbeat({
  info: { runtime_version: '1.2.3' },
  onSuccess: (pong) => console.log('[ping]', pong),
  onError: (e) => console.error('[ping] failed', e),
});

// later, if you ever need to:
cots.stopHeartbeat();

All fields are optional (defaults: no info, 60s interval, errors silently ignored). Calling startHeartbeat() again while one is already running is a no-op — this includes the automatic one autoPing starts, so pass your customization through autoPingIntervalMs/autoPingInfo (or set autoPing: false and call startHeartbeat() yourself) rather than expecting a second call to override it. The interval is unref()'d, so it won't by itself keep an otherwise-idle process (e.g. a short script) alive.

Onboarding

Registering an organization and an agent is a separate, one-time setup step — usually done through your Cots.ai dashboard, but also scriptable via the standalone ControlPlaneClient (unrelated to AgentClient/guard() — a deployed agent never touches this, it only ever gets handed the resulting tenantId/agentId/apiKey/apiSecret). onboardAgent() composes the whole sequence — register tenant, log in, register agent, mint credentials, grant an action surface, activate — into one call:

import { ControlPlaneClient } from '@cots/sample';

const controlPlane = new ControlPlaneClient({
  identityTenantUrl: 'http://localhost:8081/api',
  agentRegistryUrl: 'http://localhost:8082/api',
  actionGatewayApiUrl: 'http://localhost:8083/api',
  humanApprovalUrl: 'http://localhost:8089/api',
});

const result = await controlPlane.onboardAgent({
  tenantName: 'Acme Corp',
  adminEmail: '[email protected]',
  adminPassword: 'a real password',
  agentName: 'Ops Agent',
  targetSystem: 'Slack',
  allowedActionTypes: ['send_slack_message'],
});

// Store result.apiKey/result.apiSecret; they are never shown again. These,
// plus result.tenantId/result.agentId, are the only four values the
// deployed agent itself needs (see Configuration above).

Need more control over an individual step (e.g. onboarding a second agent onto an existing tenant, or inspecting activateAgent()'s returned PepConfig)? onboardAgent() is just a composition of registerTenant()login()registerAgent()generateCredentials()createActionSurface()activateAgent() — call them individually instead.

waitForApproval() (which guard() calls internally) authenticates as the agent itself — same AgentID/TenantID/APIKEY/SECRET headers as intercept() — so once the agent is configured with its own credentials, require_approval actions resolve on their own with no login anywhere in the running agent process. See examples/manual-test.ts for the full onboard-then-run flow.

What this SDK does not do

  • It doesn't send real Slack/SMS/email/payments — your execute() callback does that, however you like.
  • It doesn't cache or retry beyond the approval poll loop — the interceptor call itself is a single request; if it fails, guard()/intercept() throw.
  • It has no dependency on any particular agent framework (LangChain, MCP, etc.) — call guard() from wherever your agent decides to take an action, regardless of what's driving it.
  • It has no dependency on any particular web framework — works the same inside Express, Fastify, a Next.js route handler, an AWS Lambda, or a plain CLI script.

License

Copyright © Cots.ai. All rights reserved.