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

@petriflow/gate

v0.1.0

Published

Framework-agnostic Petri net gating for AI agent tool access control. Define safety constraints as Petri nets — tools are only allowed when an enabled transition permits them.

Readme

@petriflow/gate

Framework-agnostic Petri net gating for AI agent tool access control. Define safety constraints as Petri nets — tools are only allowed when an enabled transition permits them.

Built on @petriflow/engine. Used by @petriflow/pi-extension (pi-mono) and @petriflow/openclaw (OpenClaw).

Why

LLM agents need guardrails, but hardcoded allow/deny lists are too rigid and per-call confirmation is too noisy. Petri nets let you express stateful safety constraints: "allow delete only after a successful backup", "allow push only after commit", "allow sending a message only after reading the channel".

This package provides the core gating logic with no framework dependencies — adapter packages wire it into specific agent runtimes.

Defining a skill net

import { defineSkillNet } from "@petriflow/gate";

const toolApproval = defineSkillNet({
  name: "tool-approval",
  places: ["idle", "ready"],
  terminalPlaces: [],
  freeTools: ["ls", "read", "grep", "find"],  // Always allowed
  initialMarking: { idle: 1, ready: 0 },
  transitions: [
    { name: "start", type: "auto", inputs: ["idle"], outputs: ["ready"] },
    { name: "execShell", type: "manual", inputs: ["ready"], outputs: ["ready"], tools: ["bash"] },
    { name: "execWrite", type: "manual", inputs: ["ready"], outputs: ["ready"], tools: ["write", "edit"] },
  ],
});

Key concepts

Transition types

  • auto — fires immediately when the tool is called and the transition is enabled
  • manual — requires human approval via ctx.confirm() before firing

Free tools

Tools listed in freeTools are always allowed regardless of net state. Use this for read-only, side-effect-free tools.

Tool mapping

Split one physical tool into multiple virtual tools based on input content:

const net = defineSkillNet({
  // ...
  toolMapper: (event) => {
    if (event.toolName !== "bash") return event.toolName;
    const cmd = event.input.command as string;
    if (/\bgit\s+commit\b/.test(cmd)) return "git-commit";
    if (/\bgit\s+push\b/.test(cmd)) return "git-push";
    return "bash";
  },
  freeTools: ["bash"],  // Plain bash is free
  transitions: [
    { name: "commit", type: "manual", inputs: ["working"], outputs: ["committed"], tools: ["git-commit"] },
    { name: "push", type: "manual", inputs: ["committed"], outputs: ["working"], tools: ["git-push"] },
  ],
});

Deferred transitions

Allow the tool call immediately but only advance the net when the tool succeeds:

{
  name: "backup",
  type: "auto",
  inputs: ["ready"],
  outputs: ["backedUp"],
  tools: ["backup"],
  deferred: true,  // Fires on successful tool_result, not tool_call
}

If the tool fails (isError: true), the transition doesn't fire and the marking stays unchanged.

Semantic validation

Add domain-specific checks beyond what net structure alone enforces:

const net = defineSkillNet({
  // ...
  validateToolCall: (event, resolvedTool, transition, state) => {
    if (resolvedTool === "destructive") {
      const target = extractTarget(event.input);
      const covered = state.meta.backedUpPaths.some(p => covers(p, target));
      if (!covered) return { block: true, reason: `Target '${target}' not backed up` };
    }
  },
  onDeferredResult: (event, resolvedTool, transition, state) => {
    // Record metadata when a deferred transition resolves
    state.meta.backedUpPaths.push(extractPath(event.input));
  },
});

Using the gate

Single net (low-level)

import { handleToolCall, handleToolResult, createGateState, autoAdvance } from "@petriflow/gate";

const state = createGateState(autoAdvance(net, { ...net.initialMarking }));

const decision = await handleToolCall(
  { toolCallId: "1", toolName: "bash", input: { command: "rm -rf build/" } },
  { hasUI: true, confirm: async (title, msg) => window.confirm(msg) },
  net,
  state,
);

if (decision?.block) {
  console.log(`Blocked: ${decision.reason}`);
}

Multi-net composition (GateManager)

import { createGateManager } from "@petriflow/gate";

// Static — all nets always active
const manager = createGateManager([netA, netB]);

// Registry — dynamic activation/deactivation
const manager = createGateManager({
  registry: { netA, netB, netC },
  active: ["netA"],
});

const decision = await manager.handleToolCall(event, ctx);
manager.handleToolResult(resultEvent);
manager.addNet("netB");     // Registry mode only
manager.removeNet("netA");  // Registry mode only
manager.formatStatus();     // "netA (active): ready:1\nnetB (inactive): idle:1"
manager.formatSystemPrompt(); // Markdown for LLM context

Composition semantics

When multiple nets are composed, each net independently classifies a tool call:

| Verdict | Meaning | |---|---| | free | Tool is in the net's freeTools — always allowed | | abstain | Tool doesn't appear in any of the net's transitions — no opinion | | gated | An enabled transition covers this tool — allowed (pending approval/validation) | | blocked | The net has jurisdiction but no enabled transition — rejected |

One blocked verdict from any net rejects the call. If no net blocks, gated nets fire their transitions. If all nets are free or abstain, the call passes through.

API

| Export | Description | |---|---| | defineSkillNet(config) | Type-safe skill net constructor | | createGateManager(input) | Multi-net manager (array or registry config) | | handleToolCall(event, ctx, net, state) | Single-net tool call gating | | handleToolResult(event, net, state) | Single-net deferred resolution | | autoAdvance(net, marking) | Fire structural (non-tool) auto transitions | | createGateState(marking) | Initialize gate state with marking | | classifyNets(nets, states, event) | Phase 1 structural check (non-mutating) | | composedToolCall(getNets, getStates, event, ctx) | Full 4-phase composed gating | | formatMarking(marking) | Format marking for display ("ready:1, working:0") | | getEnabledToolTransitions(net, marking) | List currently available tool transitions | | resolveTool(net, event) | Apply tool mapper |

Tests

bun test packages/gate