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

@agenticcontrolplane/fx

v0.1.0

Published

Agentic Control Plane for fx (vercel-labs/fx) — a governed protocol middleware for fx's Agent Client Protocol mode, checking every tool call against your policies before fx's own permission prompt reaches the client.

Readme

@agenticcontrolplane/fx

tests zero dependencies license

Agentic Control Plane for fx, Vercel Labs' Zig coding agent: every tool call fx acp wants to run is checked against your policies before fx's own permission prompt ever reaches you, and every decision is recorded.

acp-fx is not a plugin loaded inside fx — fx has no plugin API. It's a protocol middleware: a small stdio proxy that sits between a real Agent Client Protocol client (Zed, or any ACP driver) and the fx acp subprocess, forwarding every JSON-RPC message transparently in both directions except one: session/request_permission.

Zed (or any ACP client)  ──stdio──▶  acp-fx  ──stdio──▶  fx acp
                          ◀──────────────────────────────
                                        │
                          session/request_permission
                          intercepted here, checked against
                          POST /govern/tool-use, resolved or
                          forwarded to the client per the
                          table below

Why a proxy, not a plugin

fx's ACP server (src/acp/server.zig) already emits a session/request_permission JSON-RPC request for every approval-gated tool call, with the tool's rawInput attached. That is a complete interception point — acp-fx answers it directly (allow/deny) or relays it to the real client (ask), without fx needing to know acp-fx exists. Point your ACP client at acp-fx -- fx acp instead of fx acp and nothing else about your fx setup changes.

Install

Not yet published to npm (the name @agenticcontrolplane/fx is reserved in package.json). For now, install from git:

git clone https://github.com/agentic-control-plane/fx-acp-plugin.git ~/dev/fx-acp-plugin
cd ~/dev/fx-acp-plugin && npm link   # puts `acp-fx` on your PATH

Get a key at cloud.agenticcontrolplane.com and save it to ~/.acp/credentials, or set ACP_API_KEY / ACP_BEARER_TOKEN. Requires Node 22+.

No key? acp-fx says so loudly on stderr at startup and stays out of the way — see Failure posture; it never bricks the session, it just stops being able to consult policy.

Usage

Command line

acp-fx [--unattended] [--timeout-ms N] [--no-output-governance] -- <command> [args...]

acp-fx -- fx acp
acp-fx --unattended -- fx acp   # for a scripted/CI driver with no human on the other end

Everything after -- is the command acp-fx spawns and proxies to. Point it at whatever runs fx acp — a bare fx acp, or an absolute path if fx isn't on the spawned process's PATH.

Zed

Zed's external agent config goes under agent_servers in settings.json (type, command, args, env, cwd — confirmed against Zed's docs and community examples; agent: open settings opens this block for you):

{
  "agent_servers": {
    "fx (governed)": {
      "type": "custom",
      "command": "acp-fx",
      "args": ["--", "/absolute/path/to/fx", "acp"],
      "env": {}
    }
  }
}

Use an absolute path to acp-fx too if npm link didn't put it on the shell Zed launches with. Run dev: open acp logs from Zed's command palette to see the raw traffic if something looks wrong — that's genuine ACP traffic between Zed and acp-fx, not fx-internal logging.

How it works

| fx message | What acp-fx does | |---|---| | session/request_permission (request, fx → client) | Intercepted. POST /govern/tool-use, then resolved per the table below. | | session/update with tool_call_update (notification, fx → client) | Output governance (see below) — forwarded, possibly with its displayed text replaced. | | Everything else, both directions | Passed through byte-for-byte, unmodified. |

Decision table

| Governance decision | Attended (a client is connected) | Unattended (--unattended) | |---|---|---| | allow | Reply allow_once directly to fx. The client never sees the prompt. | Same. | | deny | Reply reject_once directly to fx. | Same. | | ask | Forward the original session/request_permission to the real client — a human decides, same as running fx directly. | Reply reject_once — an unattended agent cannot self-approve ("empty chair"). | | gateway unreachable / timeout (~3s, one transport retry) | Forward to the client, fail-open, with a loud ⚠ UNGOVERNED line on stderr. | Reply reject_once, fail-closed. | | no credential found | Treated identically to "gateway unreachable" — an uncredentialed control plane must never be mistaken for a live one. | Same. |

A response is decided in allow_once / reject_once only — acp-fx never returns allow_always, even when the policy says allow, so every subsequent call for the same tool still goes through governance instead of fx caching a blanket approval.

One pending permission request at a time: fx's own beginOutboundRequest/awaitPermissionDecision (src/acp/server.zig) never has more than one session/request_permission in flight, so acp-fx does no correlation bookkeeping — the forwarded request's id round-trips straight back through the client→fx passthrough. Other fx traffic (progress notifications, etc.) is never blocked behind a pending decision; see the "interleaved" test in test/bridge.test.mjs.

Every decision gets one audit line on stderr: tool, kind, decision, source, latency, call id, session id, and reason when present.

Output governance

fx's session/update tool_call_update notifications carry the tool's result text to the client — this is a genuine, empirically-confirmed exposure point (see "Protocol uncertainties" below), so acp-fx also posts it to POST /govern/tool-output and can block or redact what's displayed. Disable with --no-output-governance.

This is display-layer only. ACP tool results do not round-trip back into fx's own model context — fx already sent the raw result to its model before this notification goes out to the client. Blocking or redacting here changes what the human sees in Zed; it cannot un-show the output to the model that already acted on it. Treat it as DLP-for-the-transcript, not as a second enforcement point on what the agent knows.

An outage during output governance is a silent pass-through (the original notification goes out unchanged) — it's observability on a display notification, not a policy gate; the loud fail-open/fail-closed posture above is reserved for the pre-execution check, where it actually controls whether something runs.

Failure posture

Same posture as the rest of the ACP family (gatewaystack-connect#690): one transport-failure retry (never a retry on an HTTP status — a 429 is the server answering, re-rolling it deepens the rate limit), then:

  • Attended sessions fail open, loudly. The request goes to the real client so a human can still decide; stderr gets a ⚠ UNGOVERNED line and (best-effort) ~/.acp/lapse.log gets a row.
  • Unattended sessions fail closed. reject_once, no exceptions — nobody is there to be the fallback.

Configuration

Environment variables (all optional):

| Variable | Default | Purpose | |---|---|---| | ACP_API_KEY / ACP_BEARER_TOKEN | ~/.acp/credentials or ~/.acp/proxy-key | Workspace key. | | ACP_GOVERN_BASE / ACP_API_BASE | https://govern.agenticcontrolplane.com | Gateway, or your self-hosted one. | | ACP_FX_UNATTENDED | unset | Same as passing --unattended. |

FX_PERMISSION_MODE is a special case, not a pass-through: acp-fx always strips it from the spawned fx process's environment, regardless of what value your shell has it set to. FX_PERMISSION_MODE=yolo bypasses fx's entire permission system (src/core/app/app_lifecycle.zig loadPermissionMode) — with yolo, fx never emits session/request_permission at all, which would make every decision this shim makes moot. See lib/spawn-env.js. fx's own default (confirmed ask, both by reading the source and by observing a live session/new response with no env var set) is left alone rather than forced, so a future fx release changing its default doesn't require a change here too — as long as it never changes it to yolo.

Protocol uncertainties

Verified empirically against the fx v0.0.3 binary and by reading the vendored Zig source (src/acp/prompt.zig, src/acp/server.zig, src/acp/types.zig, src/core/app/app_lifecycle.zig, src/core/shared/types.zig) rather than assumed from the ACP spec alone:

  • The original tool name never reaches the wire. session/request_permission's toolCall carries a human-readable title (e.g. "Run: rm -rf /tmp/scratch") and a coarse kind (read/edit/delete/move/execute/search/other), not fx's internal tool identifier. acp-fx sends title as tool_name in the governance payload and adds tool_kind alongside it so policy can match on the coarse category too — write rules against title substrings and tool_kind, not against an internal tool-name enum, because fx doesn't expose one.
  • A real session/request_permission requires a real model call, which in turn requires a real AI_GATEWAY_API_KEY. This was not available in this environment, so the deny/allow path is verified two ways: (a) the initialize handshake is verified end-to-end against the real fx v0.0.3 binary with a dummy key (this alone doesn't require a model call and passes); (b) the full request→decide→reply round trip is verified against a hand-rolled fixture (test/fixtures/fake-fx.mjs) that speaks the same wire shape fx does, confirmed line-for-line against src/acp/server.zig's own test cases. If fx's wire shape for session/request_permission changes in a future release, that fixture (and the withReplacedText/isToolCallUpdate helpers in lib/bridge.js) is the place to check first.
  • Unattended detection is flag-based, not inferred, because a stdio bridge always has something on the other end of clientIn/clientOut — there's no reliable signal like pi's ctx.hasUI to read. acp-fx defaults to attended (the primary use case is a real client like Zed) and requires an explicit --unattended flag or ACP_FX_UNATTENDED=1 for scripted/CI use. Running an unattended driver without that flag risks a real hang: an ask decision would forward session/request_permission to a "client" that never answers it, and fx's one-pending-request-at-a-time constraint means the whole session stalls behind it. If you're scripting fx through acp-fx with no human loop, always pass --unattended.
  • No synthetic session/update on deny. acp-fx could inject an extra notification explaining why a call was denied, but that risks stepping on fx's own ownership of that notification stream in ways not fully characterized from the outside. Per the task's own fallback, denial reasons are logged to stderr only; the client sees the tool call resolve as reject_once, the same shape fx already produces when a human declines its own prompt.
  • fx's own reviewer-model caveat stays fx's. fx has a documented, separate feature where a reviewer model can second-guess tool calls; that's orthogonal to this middleware and out of scope here — acp-fx only ever sees what fx already decided needs a permission check.

Test

npm test        # 40 tests: decision mapping, fail posture, env stripping, NDJSON framing, e2e
  • test/ndjson.test.mjs, test/govern.test.mjs, test/spawn-env.test.mjs, test/bridge.test.mjs — pure unit tests, no network, no subprocess (govern tests use a local node:http stub, never the production gateway).
  • test/e2e.test.mjs — spawns real child processes. One test drives the real fx v0.0.3 binary from ~/dev/fx-acp-spike/bin/fx through the initialize handshake (skipped automatically if that binary isn't present on the machine running the tests); two more drive test/fixtures/fake-fx.mjs, a minimal hand-rolled ACP speaker, through the full deny/allow session/request_permission round trip against a local governance stub.
  • scripts/smoke.shnot run by npm test. A human-runnable script against the real production gateway; see the script's own header for what it checks and what it needs (ACP_API_KEY pointed at a real workspace).

MIT