Releases: mwtcmi/frogman
v2.4.0
v2.4.0 queue management CRUD
Closes the queue tooling gap exposed by the 2026-06-03 CDR-rebuild
demo, where a model could rebuild extensions, inbound routes, and
outbound routes from CDR but stopped at queues.
Five new write tools:
- fm_add_queue
- fm_update_queue
- fm_remove_queue
- fm_add_queue_member
- fm_remove_queue_member
All PERM_WRITE with confirm:true required; dry-run preview by default.
Writes go through the canonical legacy queues_add() / queues_del()
that FreePBX itself uses on form submit; Queues BMO has no write
facade and the REST API has no POST/DELETE. Edit path is del +
re-add, mirroring the GUI's "edit" case.
fm_remove_queue pre-flight scans 11 core destination columns for
ext-queues references and refuses to delete when found; force:true
overrides. fm_add_queue_member is persistent and distinct from the
pre-existing AMI-runtime fm_queue_add_agent (descriptions cross-
reference so the right one is picked).
Chat anchors: add queue, rename queue, set queue strategy, set queue
timeout, remove queue, add member to queue, remove member from
queue. "member" disambiguates persistent CRUD from the existing
runtime shorthand. fm_lint_typeahead reports 0 gaps across 284
parser anchors.
Tool count: 241 to 246.
v2.3.0
v2.3.0 call reporting
Adds rich call-center analytics on top of FreePBX's existing call data
so the MCP client and chat surface can compose detailed per-call,
per-queue, and per-agent reports without leaving the FreePBX walls.
Seven new read-only reporting tools:
- fm_get_cel
- fm_call_timeline
- fm_cel_transfers
- fm_get_queue_log
- fm_queue_metrics
- fm_agent_metrics
- fm_queue_wallboard
Four existing CDR rollup tools fixed in place:
- fm_get_busiest_extensions (PSTN-as-extension bug)
- fm_get_peak_hours (multi-leg fan-out inflation via linkedid)
- fm_get_cdr_stats (same dedup plus non-call filter)
- fm_get_cdr (non-call filter with overfetch+trim)
Shared hygiene helpers on AbstractTool, date-range chat anchors for
every reporting command, chat formatter that groups CEL events by
call for digest output, and an independent security review with
zero high-confidence findings.
Tool count: 234 to 241.
v2.2.1
v2.2.1 docs sweep
No code change. Tool-count drift fix.
The module.xml description, CLAUDE.md, INTEGRATION.md, and
README.md all stated "228 tools" through v2.2.0 even though
the live count moved 228 -> 233 (v2.1.0, +5 outbound route
tools) -> 234 (v2.2.0, +fm_update_ivr). All four user-facing
surfaces now correctly state 234.
Historical changelog entries in module.xml are intentionally
left at their original numbers (each was accurate for its
release).
v2.2.0
v2.2.0 fm_update_ivr
One new write tool (233 to 234): fm_update_ivr. Closes the IVR
CRUD gap (add / get / list / delete were already there; update
was missing).
Behavior
Selective field merge: any field not supplied keeps its current
value. Supported fields: name, description, announcement,
directdial, timeout (1-600s), timeout_destination,
invalid_destination, timeout_recording, invalid_recording,
timeout_loops, invalid_loops, retvm. Entries, if supplied,
replace the existing set in full (BMO has no append-one API).
PERM_WRITE with confirm:true required; dry-run preview by
default with a clean field-by-field diff. No-op detection
skips the write when nothing changed.
Routing surface
Fully in-walls via the Ivr BMO facade for both reads and
writes (Ivr->getDetails, getAllEntries, saveDetails,
saveEntry). No Component-direct, no raw SQL, no GraphQL, no
shell.
Framework quirks handled in code
-
Ivr->getDetails fetches with PDO::FETCH_BOTH so the row has
both string and numeric keys. Iterating into REPLACE INTO
would try to bind:0,:1, etc. and fail HY093. Filtered
to string keys before merge. -
Ivr->getDetails runs name and description through
htmlentities before return. Writing back unchanged would
persistTom & Jerryliterally. html_entity_decode
applied before merge. Both quirks documented inline.
Validation
Two-layer reject on entries:
- timeout: numeric range 1-600 seconds
- entries[].selection and entries[].dest: non-empty, plus
reject\r\n\0(CR/LF/NUL framing) and;(Asterisk
dialplan comment marker which would silently truncate the
goto target).
BMO saveEntry uses parameterized PDO under the hood so SQL
injection is impossible at the storage layer; the tool-side
validation is defense-in-depth against dialplan-injection
shapes.
Chat-formatter hardening
Every chat-interpolated user-controlled value passes through
Frogman::sanitizeForChat per the GHSA-7qvv convention.
Chat anchors
rename ivr <id> to <name>(most common single-field case)set ivr <id> timeout to <seconds>
Multi-field updates and entries changes stay JSON/MCP only,
same pattern as fm_update_outbound_route. fm_lint_typeahead
reports gap_count=0.
Why now
Built ahead of a planned separate Fleet module for multi-site
PBX management. Fleet's apply template workflow fans out
IVR updates across many branches; atomic in-place update beats
delete-then-add for reconciliation (no IVR-not-found window,
no entry history loss).
v2.1.0
v2.1.0 outbound route CRUD + pattern CRUD
Five new write tools (228 to 233):
- fm_add_outbound_route
- fm_update_outbound_route
- fm_remove_outbound_route
- fm_add_outbound_pattern
- fm_remove_outbound_pattern
All five are PERM_WRITE with confirm:true required and dry-run
preview by default.
Routing surface
Reads use the Core BMO facade (getAllRoutes, getRoute,
getRouteByID, getRoutePatternsByID, getRouteTrunksByID,
listTrunks). Writes go through
FreePBX\modules\Core\Components\Outboundrouting (add,
editById, deleteById, updatePatterns), which is the canonical
write surface FreePBX itself uses via its deprecated
functions.deprecated.php wrappers. Core has no facade method
for route writes and Core's GraphQL Routes.php is a 10-line
stub with no write scope, so Component-direct is the highest
rung available. Header comment in AddOutboundRoute.php
documents the routing-surface ladder.
Inputs
Patterns accept either pipe-form ({match:"011|."}) or
explicit form ({prefix:"011", match:"."}). Trunks accept
either trunk IDs or trunk names, resolved server-side.
Two-layer validation on pattern fields: framework reject for
CR/LF/NUL/;/, at the tool surface, plus a whitelist of
[0-9*#+\-.\[\]XNZ] matching Outboundrouting's own filter.
Route name ^[a-zA-Z0-9_\-\s]{1,50}$, password ^\d{4,15}$,
outcid ^\+?[0-9*#]{2,18}$.
Defensive guards
- Emergency 911/999/112 refusal on fm_remove_outbound_route
and fm_remove_outbound_pattern, with the escape path
documented (use fm_update_outbound_route to swap the pattern
first) - Last-pattern guard on fm_remove_outbound_pattern (prevents
zombie routes) - Duplicate guard on fm_add_outbound_pattern
- Inline safety warnings on international prefixes
(011/0011/00) without PIN and unbounded catch-all patterns
without PIN protection, surfaced in both dry-run preview and
success message
Chat anchors
add outbound route <name> trunk <trunk> pattern <pattern>
(one-shot)remove outbound route <name>add pattern <pattern> to <route>add pattern <pattern> prepend <digits> to <route>remove pattern <pattern> from <route>
fm_update_outbound_route stays JSON/MCP-only (field surface
too wide for a chat anchor). Five new helpText placeholders so
typeahead picks them up; fm_lint_typeahead reports
gap_count=0.
Chat-formatter hardening
Every chat-interpolated user-controlled value across all five
tools passes through Frogman::sanitizeForChat, matching the
convention established in v1.7.1 to close the GHSA-7qvv
prose-interpolation shape. DB-sourced fields (route names) and
user-supplied identifiers (not-found errors) are the
load-bearing sanitization targets; pattern strings are
sanitized for convention but provably safe by construction
after the whitelist filter. Direct payload probe (backticks,
{{cmd:}}, CRLF) confirmed neutralized.
Other
module.xml gains a <more-info> tag pointing at the GitHub
repo so the FreePBX module-admin "More info" link resolves to
the repo README instead of the generic FreePBX help-system
query.
v2.0.0
v2.0.0 first signed release
No code changes, no new tools (still 228). Every release tarball now
ships with module.sig signed by my Sangoma-trusted key
(021979A8A1002442). The key carries a trust signature from FreePBX
Module Signing v2 master, published to keyserver.ubuntu.com, so any
FreePBX install resolves the trust chain on first verify and
verifyModule() returns status 129 (GOOD | TRUSTED).
Heads-up: the Signature column in fwconsole ma list reads "Unknown"
rather than "Sangoma" because FreePBX hardcodes the "Sangoma" label to
two specific Sangoma mirror keys (Moduleadmin.class.php:1251). Display
quirk, not a verification failure.
v1.8.3
v1.8.3 module licensing view plus notification rendering polish
v1.8.2
v1.8.2 Tokens sidebar plus token-tool chat-message hardening
UI: new left-sidebar Tokens section lists every API token with level
badge, last-used recency, and a stale/never-used/revoked flag. Click a
row for a detail card. Active rows show Revoke (soft, sets active=0,
keeps audit paper trail). Revoked rows show Delete (hard, drops the
row). + New token entry at the top opens the existing create chat
wizard. Two-step Revoke then Delete UX makes accidental clicks
recoverable for one step.
Schema: oc_api_tokens.last_used_at integer column populated by the
token-auth path on successful match, throttled to one UPDATE per token
per 60s. Idempotent SHOW COLUMNS migration in install.php.
Tool surface: fm_list_api_tokens surfaces last_used_at, a human
relative string, plus stale (60-day threshold on active tokens) and
never_used boolean flags. fm_revoke_api_token and fm_delete_api_token
now accept either an id or a username, and either word order
("revoke api token X", "revoke X token", "delete X token", etc.).
Ambiguous matches error and demand id rather than picking silently.
Hardening: Frogman::sanitizeForChat() is now public so CreateApiToken,
RevokeApiToken, and DeleteApiToken can sanitize the username and
description fields they interpolate into chat output. Closes the
prose-interpolation gap that GHSA-7qvv left open for these three tools.
Tool count unchanged (228).
v1.8.1
v1.8.1 — click-to-call hardening + inbound-route UX polish
fm_originate_call rewritten internally with strict input validation, a
device-vs-user-mode channel selector, and array-form AMI Originate.
Outbound-route ACL preserved (slightly tightened vs. the prior
PJSIP-direct channel since the second leg now always routes through
from-internal).
New reverse chat anchor call <number> from <ext> complements the
existing call <ext> to <number>.
fm_add_inbound_route now warns when the resolved destination targets
a device-only extension (the route will work, but FreePBX's GUI
Inbound Routes editor will render the destination as unknown/red
because core_destinations() builds the dropdown from the users
table). Confirmed-success message leads with ✅ to distinguish from
the dry-run preview.
fm_reload success message replaced with a randomized five-line pool.
No new tools (count stays at 228). Patch release per the established
convention. Security-reviewed clean — no new attack surface.
v1.8.0
v1.8.0 — Tier 2 compliance audits: admin passwords + open dial patterns
Two new read-only fm_audit_* tools that extend the toll-fraud-chain coverage to the two highest-value Tier 2 surfaces.
fm_audit_admin_passwords (new)
Audits FreePBX GUI admin accounts (ampusers) for weak passwords. Hashes a curated common-password corpus with SHA1 (the algorithm FreePBX uses for ampusers.password_sha1) and compares to stored hashes.
Detects:
- Critical — Password equals username (e.g.
admin/admin). - Critical — Password matches a known-weak value, categorized into:
- Common defaults —
password,admin,root,welcome,letmein,changeme,default,secret,guest,test - Vendor defaults —
freepbx,asterisk,sangoma,pbx,voip,sip,dial - Numeric defaults —
1234,12345,123456,0000,111111,222222,666666,4321,54321,654321 - Keyboard walks —
qwerty,qwertyuiop,asdfgh,zxcvbn,qwerty123 - Other common weak —
iloveyou,monkey,dragon,abc123,password1,admin123,root123,login,master
- Common defaults —
- High —
password_sha1is not a 40-char hex SHA1 (unmigrated or corrupted row).
The stored hash is never echoed in findings — only the username and reason category. Recommendation steers admins to the native FreePBX Admin → Administrators page rather than offering to write back through Frogman.
Data source: direct read of ampusers (justified — ampuser.class.php has no list-all method; it constructs per-user via username). SELECT username, password_sha1 FROM ampusers is the only sensible path. Read-only.
Chat phrases: audit admin passwords, audit passwords, audit ampusers.
fm_audit_open_dial_patterns (new)
Audits outbound-route dial patterns for permissiveness that admits unbounded destinations.
Detects:
- High — Pure catch-all
_.or_!(matches any dialed digit string). - High — Wildcard at positions 1–2 (caller controls almost every digit, e.g.
_X.,_XX.,_[1-9]X.). - Medium — Wildcard at positions 3–6 (e.g.
_NXX.,_NXXNXX.) — narrower than NANP minimum. - Info — Wildcard at position 7+ (commonly bounded but worth a glance).
Skips:
- International prefixes (
011,0011,00) — owned byfm_audit_outbound_international. - Well-known short codes (
911,999,112,411,611,711,811,*98,*97) — intentional dial-plan entries that would otherwise false-positive.
Tokenization: [ranges] count as a single position, so _[1-9]X. correctly classifies as "wildcard at position 2" (high), not "wildcard at position 5".
Data source: BMO-only — Core->getAllRoutes + Core->getRoutePatternsByID. Read-only.
Chat phrases: audit open dial patterns, audit permissive patterns, audit dial patterns.
Rollup integration
Both audits register into the fm_audit_posture meta-tool automatically — they appear under the Action needed or Clean section of the report card with display names "Admin Passwords" and "Open Dial Patterns" and drill-down phrases for chat.
Permissions
Both tools are PERM_ADMIN. Findings disclose either credential weakness or dial-plan permissiveness, which is recon-useful — admin-only is the right tier alongside the rest of the audit family.
Verification
- Admin password tool: injected
SHA1('admin')on a test account, audit fired withcritical+ correct category, hash not leaked in output, revert clean. - Open dial pattern tool: standalone classifier exercise across 14 pattern shapes (catch-alls, NANP, internationals, short codes, character classes). All tiers and skips fire as designed.
Tool count
228 tools (+2 from 1.7.3).