@itc-steve/pi-fgt

Pi extension: read-only multi-device FortiGate (FortiOS REST) tools — firewall, VPN, FortiAP, FortiSwitch

Packages

Package details

extension

Install @itc-steve/pi-fgt from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@itc-steve/pi-fgt
Package
@itc-steve/pi-fgt
Version
1.5.0
Published
Aug 27, 2026
Downloads
608/mo · 30/wk
Author
itc-steve
License
MIT
Types
extension
Size
477.1 KB
Dependencies
2 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

Not affiliated with Fortinet. Independent open-source project. FortiGate, FortiOS, FortiAP, FortiSwitch, and related names are trademarks of Fortinet, Inc.

pi-fgt is a pi extension that turns FortiOS REST into 191 typed, read-only tools. Ask your agent about a firewall instead of clicking through the GUI.

No MCP server. No write verbs. Tokens stay in env files. Every device is hidden from the model until you pick it, every session.

Install

pi install npm:@itc-steve/pi-fgt

Then /reload.

Setup

Run /fortigate add — an interactive wizard asks where to store the device (session or fortigate.json), where to store the token (session memory or fortigate.env), then name, host/URL, VDOM, and the token in a masked prompt. It connection-tests before writing anything; a failed test saves nothing. The env key name (FORTIGATE_<NAME>_TOKEN) is generated for you. Private fortigate.env is written 0600; shared config uses 0660.

The wizard is TUI-only by design: tokens never pass through command args or model-visible text, and there is no non-interactive fallback.

Command Effect
/fortigate add Add a device (session or persistent) + token
/fortigate token Set session/persistent token, or clear the session token
/fortigate edit Change url / vdom / TLS verification (token untouched)
/fortigate remove Remove a device, optionally delete its unused env key

TLS starts at verifySsl: true. Only after a certificate failure does the wizard offer an explicit insecure retry.

Manual files

Or write the two files yourself. fortigate.json says which devices exist; fortigate.env holds the tokens.

cp /path/to/pi-fgt/fortigate.json.example ~/.pi/agent/fortigate.json
cp /path/to/pi-fgt/fortigate.env.example  ~/.pi/agent/fortigate.env
chmod 600 ~/.pi/agent/fortigate.env

~/.pi/agent/fortigate.json:

{
  "sessionDefault": "off",
  "fortigateDefault": "off",
  "maxResponseBytes": 24000,
  "devices": {
    "edge": {
      "url": "https://fw01.example.com:443",
      "tokenEnv": "FORTIGATE_EDGE_TOKEN",
      "vdom": "root",
      "verifySsl": true
    }
  }
}

fortigateDefault: off (missing = none), on (every configured device), a name ("edge"), or a list (["edge","core"]).

~/.pi/agent/fortigate.env:

FORTIGATE_EDGE_TOKEN=your-api-token

Token resolves from a session token first, then process.env[tokenEnv], then fortigate.env. Never put the token in the JSON.

For multiple Unix users sharing one writable device map and token file, point every Pi process at a group-writable directory:

export PI_FORTIGATE_CONFIG_DIR=/data/fortigate

Persistent mutations are locked across processes (.fortigate.lock in the config dir). Shared writes keep fortigate.json at 0664 and fortigate.env at 0660; directory access still controls which users can read tokens.

Shared mode is stricter than private mode: tokens resolve from fortigate.env only — process.env is ignored, so one user's shell cannot supply another's token — and any device whose tokenEnv is outside FORTIGATE_<NAME>_TOKEN is rejected at load, so a writable shared config cannot point the extension at an unrelated secret.

Use

Run /fortigate — a picker opens (↑↓ move, enter/space toggle, / search, esc done). Devices you don't select stay invisible to the model: not listed, not resolvable, not reachable.

Command Effect
/fortigate / on / devices Enable tools + open picker
/fortigate off Disable tools, clear all temporary state
/fortigate toggle Off → on+picker, on → off
/fortigate status On/off, per-device storage and credential source (never values)
/fortigate filters Which response fields are excluded
/fortigate add / token / edit / remove Setup wizards (see Setup)

fortigateDefault seeds which devices the AI can see at session start (off = none, on = all configured keys, or one or more names: "edge" / ["edge","core"]). Missing = off. Preselecting any device also turns the tools on. Selection is still per session: /fortigate off or /new clears it; the JSON only sets the next session's default. /fortigate add selects that device for the rest of the current session.

Session devices and session tokens are memory-only too: /fortigate off or a new session drops them.

Every tool takes an optional device name — omit it when only one device is selected. Names match case-insensitively and by unique substring (edgeedge-fw); ambiguous input lists candidates instead of guessing.

Safety

  • GET only — attempt_write_operation refuses with no network I/O
  • VDOM pinned from device config; caller cannot override
  • Path/name validation on escape hatches and ids
  • Tokens only via env / fortigate.env / session memory — never in JSON, never in command args, masked in the wizard prompt
  • Shared config dir: process.env tokens ignored, tokenEnv namespace enforced
  • Setup wizards refuse to run outside TUI mode; connection errors are redacted and length-bounded
  • Device exposure opt-in per session (fortigateDefault seeds the next session; live selection is in-memory)

Tools

191 read-only tools covering system health, fabric/HA, routing, firewall config and live stats, VPN/SD-WAN, wireless, FortiSwitch, users, UTM, admin and logs. All 288 documented FortiOS 7.4 GET endpoints are either a typed tool or reachable via the get_config_object / get_monitor_resource escape hatches — prefer the typed tools, they carry paging and filter hints the raw paths don't.

Response filters

FortiOS responses are enormous. Default filters strip noise fields before anything reaches the model — on a live FGT70F (v7.4.12), 434 KB of raw responses became 45 KB:

tool raw filtered cut
get_interfaces_config 178.9 KB 3.9 KB 98%
get_firewall_policies 41.0 KB 2.7 KB 93%
get_address_objects 168.5 KB 17.7 KB 89%
get_fortiaps 9.0 KB 1.6 KB 82%
get_firewall_sessions 14.5 KB 6.5 KB 55%

Filtered responses carry a _filtered stamp naming what was dropped. verbose=true returns raw records. To change the rules permanently:

cp /path/to/pi-fgt/fortigate-filters.example.json ~/.pi/agent/fortigate-filters.json

Filters shrink what you fetch; they don't replace fetching less. Narrow at source first: log action/srcip/dstip/policyid, object name, session policyid, interface link=up|down, policy zero_only=true, or WiFi poor_only=true.

Precedence, first match wins:

  1. tools.<name>.keep[] — always survives
  2. tools.<name>.allowlist[] — ONLY these fields are returned (strongest)
  3. dropKeys / dropPrefixes / dropSuffixes — explicit + group rules
  4. dropValuesbyValue placeholders, disableDefaults
  5. dropEmpty — empty string / array / object / null

A field in an allowlist is immune to rule 4, so a meaningful default like logtraffic: "disable" is never silently dropped.

Structural groups reshape a payload rather than drop keys:

group what it does tools
apps_compact apps:[{id,name,protocol,port}]["udp/53"] sessions, fortiview
resource_history CPU/mem time series → current + one sample resource usage, performance
switch_port_counts ports[]port_count / ports_up get_fortiswitches
ipsec_compact proxyid[] trees → phase2[] + derived status get_ipsec_tunnels

Limits:

"limits": {
  "maxResponseBytes": null,   // null = defer to fortigate.json
  "maxArrayItems": 20,        // array trim size when a payload is over budget
  "maxExpandRequests": 40     // fan-out cap for get_fqdn_addresses
}

Getting data back — every noise family is a named group; flip one boolean:

{ "groups": { "uuid": { "exclude": false } } }   // UUIDs return everywhere

Re-enabling a group also re-admits its fields past a tool allowlist. To lift an allowlist entirely:

{ "tools": { "get_firewall_policies": { "allowlist": null } } }

Your file is deep-merged over the defaults — only specify what you change. Invalid JSON falls back to defaults with a warning rather than breaking tools.

Defaults worth knowing:

  • Excluded: uuid, ZTNA, IPv6 blocks, DiffServ/ToS, PPTP/L2TP, *-negate, duplicate identity fields, FortiOS internal indexes, switch-controller-*, WiFi MCS/rate-score telemetry
  • Kept on purpose: country/srcmac on sessions, noise on WiFi clients
  • Set audit.verboseBypassesFilters: false to keep filtering even on verbose=true calls

Notes

FortiOS 7.6 relocated some monitor endpoints; the client surfaces relocation hints on 404.

v1.2+ device selection is session-scoped and in-memory. Safe to delete any leftover ~/.pi/agent/fortigate.state.json.

License

MIT © itc-steve