@itc-steve/pi-fgt
Pi extension: read-only multi-device FortiGate (FortiOS REST) tools — firewall, VPN, FortiAP, FortiSwitch
Package details
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 (edge → edge-fw); ambiguous input lists candidates instead of guessing.
Safety
- GET only —
attempt_write_operationrefuses 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.envtokens ignored,tokenEnvnamespace enforced - Setup wizards refuse to run outside TUI mode; connection errors are redacted and length-bounded
- Device exposure opt-in per session (
fortigateDefaultseeds 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:
tools.<name>.keep[]— always survivestools.<name>.allowlist[]— ONLY these fields are returned (strongest)dropKeys/dropPrefixes/dropSuffixes— explicit + group rulesdropValues—byValueplaceholders,disableDefaultsdropEmpty— 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/srcmacon sessions,noiseon WiFi clients - Set
audit.verboseBypassesFilters: falseto keep filtering even onverbose=truecalls
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.