-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmodule.xml
More file actions
205 lines (171 loc) · 59.8 KB
/
module.xml
File metadata and controls
205 lines (171 loc) · 59.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
<module>
<rawname>frogman</rawname>
<name>Frogman</name>
<version>2.3.0</version>
<publisher>Frogman Project</publisher>
<license>AGPLv3</license>
<licenselink>https://www.gnu.org/licenses/agpl-3.0.txt</licenselink>
<category>Admin</category>
<description>Complete PBX control through natural language from a web console, chat platforms, MCP, or any API client. 241 tools built entirely on FreePBX's native interfaces (BMO, GraphQL, AMI, fwconsole).</description>
<more-info>https://github.com/mwtcmi/frogman</more-info>
<changelog><![CDATA[
*2.3.0* Call reporting initiative. This release brings 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. The motivation: until now Frogman could query CDR rollups and live queue status, but had no way to surface per-call event timelines, queue analytics (offered, answered, abandoned, service level, ASA, AHT, occupancy), or a real-time wallboard view. Seven new read-only reporting tools added (234 to 241). fm_get_cel reads Channel Event Log (asteriskcdrdb.cel) with filters on date range, linkedid, uniqueid, event types, context, channel name; returns raw rows plus event-type counts; chat surface collapses multi-event-per-call output into a per-call digest grouped by linkedid (one row per call with time, src and dst, answered or no-answer, bridge count, duration, markers for transfer/park/pickup, drill-down chip to the full timeline). fm_call_timeline reconstructs a single call from CEL: accepts linkedid (preferred) or uniqueid (walked to root); returns channels with originator/answerer/dialed roles, bridges with participant entry/exit, transfers with target_linkedid for client follow-up (scoped not auto-walked so latency stays predictable), IVR legs paired by APP_START/APP_END for Background/Read/Playback/WaitExten, park events, pickup events, and the raw event sequence for client re-aggregation. fm_cel_transfers lists blind and attended transfer events in a window with transferer (ext, name, channel), call duration before transfer, and target_linkedid for the post-transfer leg. fm_get_queue_log reads asteriskcdrdb.queuelog with filters on date, queue, agent (extension or full Local/X@from-queue/n interface), callid, event types; degrades cleanly when the table does not exist (PBX without queues configured). fm_queue_metrics computes the standard per-queue rollup: offered, answered, abandoned, abandonment rate, service level (default formula answered_within_T over offered, T=20s, both configurable via service_level_threshold and service_level_variant), average speed of answer, average handle time, talk time, longest waits; the formula choice is echoed in the output so consumers know which variant produced the number. fm_agent_metrics computes the per-agent scorecard: calls handled, talk time, ring-no-answer count, session time from ADDMEMBER/REMOVEMEMBER pairs, available time, occupancy (talk over talk+available), pauses grouped by reason; strips Local/<ext>@from-queue/n to bare extension for the human key while preserving the raw interface under agent.interface; open sessions and open pauses at window end are closed at the last event time so currently-active shifts are not undercounted. fm_queue_wallboard parses `queue show` (AMI Command) into a per-queue live snapshot: callers waiting with current wait time, longest current wait, agents split into available, on_call, and paused (with pause reason); graceful empty when no queues are configured. Four existing CDR rollup tools fixed in place. fm_get_busiest_extensions stops treating CDR src as an extension: resolves both legs against the live extensions list via isInternalExtension, splits per-extension counts into inbound/outbound/internal, and excludes non-call records (paging multicast, lockdown beacons, echo tests) by default with include_non_calls flag to restore the old behavior; previously, inbound PSTN calls surfaced the caller's phone number as if it were an extension. fm_get_peak_hours collapses multi-leg CDR rows via linkedid (fallback uniqueid root) so one conversation counts once, not the two or three rows the Local/...;1 + Local/...;2 fan-out produces; returns both deduped calls_per_hour and raw_rows_per_hour for sanity checks. fm_get_cdr_stats applies the same leg-dedup and non-call filter, reporting both deduped total_calls and raw total_raw_rows so the dedup impact is visible. fm_get_cdr applies the non-call filter by default with overfetch+trim to preserve the requested LIMIT semantics; include_non_calls flag restores all-record visibility. Shared hygiene helpers on AbstractTool used by every reporting path: isInternalExtension(number) checks whether a digit string matches a configured FreePBX extension; getInternalExtensions() returns the full extension number list (used for IN-clause SQL prefilters); lookupExtensionName(number) returns the display name (named lookupExtensionName rather than getExtensionName to avoid a method-visibility collision with TraceCallFlow's private getExtensionName, which PHP's child-stricter-than-parent rule would otherwise reject); dedupeByCall(rows) collapses multi-leg rows keyed by linkedid (fallback uniqueid root); isRealCall(row, denyContexts) filters non-call records (MulticastRTP channels, fm-lockdown-* contexts, app-echo-test, *43 feature code, app-blackhole) and accepts an extra deny-patterns list for deployment-specific noise; applyDefaultReportWindow(params) applies today 00:00 to now when neither date_from nor date_to is set, so an unbounded query cannot accidentally scan a year of rows; isSafeLinkedid(v) validates the linkedid format before embedding in {{cmd:...}} chips (sanitizeForChat strips backticks/{{ but not the chip's own }} and | delimiters, so this helper is the defense-in-depth gate). Chat surface additions: new 'Call Reporting' section in helpText covering the seven new commands; all reporting anchors accept a trailing time clause (today, yesterday, this week, last week, this month, last month, last N hours/days/weeks/months, from <date> to <date>), parsed by ChatParser::parseTimeClause and applied to the tool's date_from/date_to params with the tool's own applyDefaultReportWindow default left in place when the clause is empty or unrecognized; the same time-clause grammar extends the existing CDR anchors (cdr stats, peak hours, busiest extensions, show cdr, calls from <ext>) so 'peak hours yesterday' and 'cdr stats from 2026-05-01 to 2026-05-31' work in chat the same way they work via MCP. Security: every chat-interpolated user-controlled value passes through sanitizeForChat per the GHSA-7qvv convention; the two formatters that embed linkedid inside a {{cmd:...}} chip (fm_get_cel and fm_cel_transfers) gate the embed behind isSafeLinkedid and fall back to sanitized plain text otherwise; the AMI command in fm_queue_wallboard hardens its only user-supplied component (queue name) via the [A-Za-z0-9_-] character allowlist before concatenation; all eleven affected tools declare PERM_READ; an independent security review reported zero high-confidence findings. fm_lint_typeahead reports 0 gaps across 277 parser anchors. Tool count: 234 to 241.
*2.2.1* Docs sweep. No code change. Tool-count drift fix: module.xml description, CLAUDE.md, INTEGRATION.md, and README.md all said "228 tools" through v2.2.0 even though the actual count moved 228 to 233 to 234 with the outbound route and IVR-update additions. All four user-facing surfaces now correctly state 234. Historical changelog entries left untouched (each was accurate for its release).
*2.2.0* fm_update_ivr. One new write tool (233 to 234) closing the IVR CRUD gap: read current details + entries, merge supplied fields, write back via Ivr->saveDetails + Ivr->saveEntry. PERM_WRITE with confirm:true required; dry-run preview by default with field-by-field diff. Fully in-walls (Ivr BMO facade for both reads and writes, no Component-direct, no raw SQL, no GraphQL). Two FreePBX framework quirks handled in code with explanatory comments: (1) Ivr->getDetails fetches with PDO::FETCH_BOTH so the result has duplicate string + numeric keys; iterating into REPLACE INTO would try to bind `:0`, `:1`, etc. and fail HY093 — filter to string keys before merge; (2) Ivr->getDetails runs name and description through htmlentities before return, so writing back unchanged would persist "Tom & Jerry" literally — html_entity_decode before merge. Selective field merge means unspecified fields keep their current values; the GUI's tinyint defaults (timeout_append_announce, accept_pound_key, etc.) are preserved. Entries, if supplied, replace the existing set in full because Ivr->saveEntry is a delete-then-insert operation with no append API. No-op detection skips the write entirely when nothing changed. Two-layer validation on entries: timeout numeric range 1-600 seconds; each entry needs non-empty selection + dest; selection and dest reject `\r\n\0;` (CR/LF/NUL framing chars plus Asterisk `;` comment marker which would silently truncate the goto target). sanitizeForChat applied across all chat-interpolated values per the GHSA-7qvv convention. Chat anchors: `rename ivr <id> to <name>` (most common single-field case) and `set ivr <id> timeout to <seconds>`. Multi-field updates and entries changes stay JSON/MCP only, same pattern as fm_update_outbound_route. Built ahead of a planned separate Fleet module (multi-site IVR template management) where atomic in-place update beats delete-then-add for fan-out reconciliation.
*2.1.0* Outbound route CRUD plus 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 at PERM_WRITE with confirm:true required; dry-run preview by default. Reads stay on the Core BMO facade (getRoute, getRouteByID, getRoutePatternsByID, getRouteTrunksByID, getAllRoutes, 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 wrappers. The Core BMO 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 full routing-surface ladder so future readers do not have to re-walk it. Pattern inputs accept either pipe-form ({match:"011|."}) or explicit form ({prefix:"011", match:"."}), normalized in a shared helper. Trunks accept either trunk IDs or trunk names, resolved server-side against listTrunks. Two-layer validation on pattern fields: framework reject for CR/LF/NUL/semicolon/comma at the tool surface, then a whitelist of [0-9*#+\-.\[\]XNZ] that matches Outboundrouting's own filter exactly. Route name validated to ^[a-zA-Z0-9_\-\s]{1,50}$; password to ^\d{4,15}$; outcid to ^\+?[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 (update pattern away first); last-pattern guard on fm_remove_outbound_pattern preventing zombie routes; duplicate guard on fm_add_outbound_pattern preventing accidental double-adds; 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 because the field surface is 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 from Core->getRoute) and user-supplied identifiers (not-found error messages) are the load-bearing sanitization targets; pattern strings are sanitized for convention but are provably safe by construction after the [0-9*#+\-.\[\]XNZ] filter. Direct payload probe (backticks, {{cmd:}}, CRLF) confirmed neutralized. Also: 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.
*2.0.0* First signed release. No code changes, no new tools (still 228); this milestone wires PGP module signing into the release pipeline. The signing key (021979A8A1002442) 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. Every tag push now produces a tarball with module.sig included; verifyModule() on the install side returns status 129 (GOOD | TRUSTED). 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); this is a display quirk, not a verification failure. Release-pipeline changes: .github/workflows/release.yml gains GPG setup (imports private key from GPG_PRIVATE_KEY secret, presets passphrase from GPG_PASSPHRASE in gpg-agent), a Sign module step (clones FreePBX/devtools and runs sign.php against the extracted tree), and an extended verify-tarball-contents check confirming module.sig made it into the .tgz before the GitHub release is created. bin/sign-module.sh added as a local-dev fallback that reproduces sign.php's output byte-for-byte without requiring PHP installed.
*1.8.3* Module licensing view + notification rendering polish. No new tools (still 228). New chat phrase `show licensing` (also `list licensing`, `show modules licensing`) renders commercial modules in two bulleted groups, Licensed and Unlicensed, with expiry dates where Sysadmin's CommercialLicense surface returns them and a Sangoma portal renewal link at the bottom. Licensed status comes from Sysadmin->getModuleLicenseInformation(); expiry dates come from the CommercialLicense->loadCommercialLicenseContent() HTML that fm_get_license_info already parses. A module rolls up to Licensed when either the structured map flags it true or it carries a non-expired future-dated expiry, which keeps deployment-covered modules like Broadcast on the right side of the line. fm_module_list gains a `licensing: true` param; the summary view exposes a 🔐 Licensing chip in the footer when commercial modules are installed. Notification rendering hardening: list_notifications and the single-notification detail view now strip the raw HTML that FreePBX modules sometimes embed in display_text and extended_text (Font Awesome icon spans, inline styles, full HTML tables on Commercial Module Maintenance), inject visible separators at block boundaries so adjacent rows don't glue together as "ModuleExpires OnPaging Pro08 Jun 2026", linkify GHSA IDs to the FreePBX security-reporting repo's advisory page (the canonical location since the global /advisories/<id> endpoint 404s on un-promoted entries), and preserve `<a href>` links from FreePBX admin-page references such as the Port Management link in scd_requirement_* notifications. Three special-cased detail-line shapes render with chips and module-name resolution: NEWUPDATES "<module> <new> (current: <cur>)" becomes status + upgrade chips; VULNERABILITIES_FIXED "<module> (Cur v. X) should be upgraded to v. Y..." becomes the same chip pair with GHSA references inlined; FW_TAMPERED "Module: <display>, File: <path>" extracts the internal module name from the path so the status chip resolves correctly (display names like "CDR Reports" aren't addressable, internal name "cdr" is). fm_delete_notification accepts `id` alone now and resolves module via Notifications->list_all(); both forms still work. The tool refuses notifications FreePBX marks candelete=0 (BADDEST and other config-error states) with a chat-friendly explanation pointing at the underlying config to fix, replacing the previous misleading "dismissed" message that safe_delete silently no-op'd through. ListNotifications exposes candelete so the formatter only emits the dismiss chip when dismissal will actually clear the notification. New chat phrases: `dismiss notification <id>` (id-only, recommended for hand-typing after `list notifications`), `dismiss notification <module> <id>` (two-arg, used by the dismiss chip). Small copy fixes: UpdateActivation references `show license` instead of `sc status` (fm_sc_status was parked with other commercial-module integrations in v1.6.0; show license reads license state via BMO and works regardless), and several bold wrappings around clickable chips were removed because the chat formatter's bold regex escapeHtml's its captured body and was mangling the chip's rendered span into literal text.
*1.8.2* Tokens sidebar and supporting hardening. No new tools (still 228); this release adds a UI surface for API-token lifecycle, a `last_used_at` column, broader parser shapes for revoke/delete, and `sanitizeForChat()` coverage on the three token-management tools. New left-sidebar section in the Frogman console renders every token with its level badge, last-used recency, and a stale/never-used/revoked flag. Click a row to expand a detail card with the id, description, created/last-used timestamps, and the contextually-appropriate action button. Two-step destructive UX: active tokens show **Revoke** (soft, sets `active = 0`, keeps the row as audit paper trail), revoked tokens show **Delete** (hard, DROPs the row). The only path from active to deleted goes through revoked, so a single misclick is recoverable for one more deliberate step. New `+ New token` entry at the top of the list opens the existing `create api token <username>` chat wizard so creation, listing, revocation, and deletion are all one click away. Schema: new `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 so wallboard-style pollers don't hammer the row). Idempotent SHOW COLUMNS migration in install.php mirrors the chat_input pattern. fm_list_api_tokens surfaces `last_used_at`, a human relative string ("47d ago" or "never"), plus `stale` (60-day threshold on active tokens) and `never_used` boolean flags. Parser flexibility: revoke and delete now accept either an id OR a username, and either word order ("revoke api token X", "revoke X token", "delete X token", etc.). Usernames are non-digit captures resolved server-side via parameterized lookup. Ambiguous matches (multiple rows for the same username) error and demand an id rather than silently picking. fm_revoke_api_token skips already-revoked rows when resolving by username (so "revoke ksbot" Just Works after a prior KSbot was revoked); fm_delete_api_token considers both active and revoked. Audit hardening on token-management messages: `Frogman::sanitizeForChat()` is now public so the three token tools (CreateApiToken, RevokeApiToken, DeleteApiToken) can sanitize the username/description fields they interpolate into chat output. Closes the prose-interpolation gap that GHSA-7qvv left open for these specific tools (every other formatter case was already covered; the token tools predated the helper and were the last interpolation site still raw). Sidebar JS renders every token-derived field through `escapeHtml` defense-in-depth and uses confirm() modals with action-specific wording before firing the underlying tool.
*1.8.1* Click-to-call hardening + inbound-route UX polish. No new tools (still 228). fm_originate_call rewritten internally: validate() now rejects non-digit ext, missing/empty dest, CR/LF/NUL/`;`/`,` injection chars in dest, and dest patterns outside `^\+?[0-9*#]{2,18}$`; existence check uses Core->getDevice() so device-only extensions are correctly recognized (the prior getUser check returned empty for them); channel selection distinguishes user-mode extensions (`Local/<ext>@from-internal`, so DND/FMFM/ring-groups behave as expected) from device-only ones (the device's own dial string, since from-internal,<ext> would otherwise land on "cannot complete as dialed"); CallerID on the ringing extension's screen now shows "Call <dest>" so the user knows what they're picking up for. Outbound-route ACL is preserved (and slightly tightened vs. the prior PJSIP-direct path) since the second leg routes through from-internal in both modes. New reverse chat anchor `call <number> from <ext>` (also matches `from ext 101`, `from extension 101`, `+` E.164 prefix) — original `call <ext> to <number>` anchor unchanged. fm_add_inbound_route gains a device-only advisory: when the resolved destination is from-did-direct,<ext>,1 for an extension that has a device but no user record, dry-run and success messages surface a clear ⚠️ explaining that the route will work but the FreePBX GUI's Inbound Routes editor will render the destination as unknown/red (root cause: core_destinations() builds the dropdown from core_users_list(), which only includes extensions present in the `users` table). Confirmed-success message now leads with ✅ so the post-confirm state is visually distinct from the dry-run preview. fm_reload's success message replaced with a randomized five-line pool (mix of ✅ confident and 🐸 Tango-flavored) so the post-reload reply doesn't read robotic. Security-reviewed: no new attack surface (AMI Originate now uses the array form with strictly-validated values; all new chat-output interpolations are either hardcoded prose or digit-validated captures).
*1.8.0* Tier 2 compliance audits — admin password weakness + 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 (hashes a curated weak-password corpus — common defaults, vendor defaults like freepbx/asterisk/sangoma, numeric defaults, keyboard walks, and other common-weak words — with SHA1 and compares against `ampusers.password_sha1`; flags username==password matches as critical; flags non-40-hex-char hashes as high for corrupted/unmigrated rows; the stored hash is never echoed in findings, only the username + reason category), fm_audit_open_dial_patterns (tokenizes outbound-route dial patterns so `[ranges]` count as one position, flags pure catch-all `_.`/`_!` as high, wildcards at positions 1–2 as high, 3–6 as medium, 7+ as info; skips patterns owned by fm_audit_outbound_international (011/0011/00) and well-known short codes (911, 999, 112, *98, etc.) to avoid false positives on intentional dial-plan entries). Both PERM_ADMIN. fm_audit_open_dial_patterns is BMO-only (Core->getAllRoutes + getRoutePatternsByID). fm_audit_admin_passwords reads ampusers directly — there is no list-all BMO surface on ampuser.class.php (it constructs per-user via username), so a single SELECT username, password_sha1 FROM ampusers is the only sensible path; the tool is read-only and never writes. Chat phrases: `audit admin passwords`, `audit ampusers`, `audit open dial patterns`, `audit dial patterns`. Both register into the fm_audit_posture rollup automatically (Score: X of N clean). Tool count climbs to 228 (+2 from 1.7.3).
*1.7.3* Chat console renders root-relative markdown links. The v1.7.2 Caller ID posture audit included a clickable `[✏️ Edit in GUI](/admin/config.php?display=extensions&extdisplay=<id>)` link on every extension finding, but the chat client's formatMarkdown regex only matched absolute `https?://` URLs — a side effect of the GHSA-7qvv hardening that constrained schemes to block `javascript:` / `data:` / `vbscript:`. Server-relative paths can't carry a scheme at all, so excluding them was overzealous. Regex widened in assets/js/chat.js to accept `https?://...` OR a single-leading-slash path `/...`. Protocol-relative URLs (`//host/...`) remain blocked via a `(?!\/)` negative lookahead so a tool response can't phish to another origin; `javascript:` / `data:` / `vbscript:` still fall through to plain text because they have no `/` or `http(s)://` prefix. escapeHtml still wraps the value so a stray `"` can't break out of `href="..."`. Verified across seven URL shapes (relative, https, protocol-relative, javascript:, /javascript: path-shaped, fragment-only, query-only) — only the intended two match. Tool count unchanged (226).
*1.7.2* Caller ID posture audit + posture report-card layout. New tool fm_audit_caller_id_posture extends the Tier 1 compliance audit family with the fifth check in the toll-fraud chain: trunks with no outbound CID, outbound routes whose CID is blank AND every trunk in the priority list is also blank (calls would leave the system unidentified), routes that force-override extension CID (outcid_mode=on — surfaces precedence so admins know extension-level CIDs are ignored on that route), extensions with CID overrides that don't match any DID owned by this PBX (potential spoof / STIR-SHAKEN attestation drop), placeholder/test strings (default, freepbx, asterisk, sangoma, anonymous, 5555555555, 1111111111, etc.) at any tier, and malformed numbers (not 7–15 digits after normalization). DID ownership comparison is NANP-normalized (+1/1 prefix and formatting stripped) so `+1 555 123 4567` and `5551234567` round-trip equal; the `_.` any-DID catchall and `s` no-CID-supplied markers are excluded from the owned-DID set. Read-only, PERM_ADMIN, BMO-only (Core->listTrunks + getTrunkDetails, Core->getAllRoutes + getRouteTrunksByID, Core->getAllDevicesByType + getUser, Core->getAllDIDs — no direct writes, no commercial-module deps). Findings include a clickable "Edit in GUI" link for extension findings that hands the fix back to FreePBX's native extensions page (`config.php?display=extensions&extdisplay=<id>`) — Frogman doesn't try to fix CID itself, since trunk/route CID changes touch precedence and STIR-SHAKEN signing config best left to the GUI. Chat phrases: `audit caller id`, `audit cid`, `audit caller id posture`. Also: the fm_audit_posture meta-tool's chat output is redesigned to a report-card layout ("Score: X of N clean", grouped Action needed / Clean / Errored sections) with per-audit display names instead of raw tool slugs; rollup automatically includes the new audit. 226 tools, +1.
*1.7.1* Posture meta-tool + chat-formatter hardening. New tool fm_audit_posture runs every fm_audit_* compliance check and returns a consolidated report card (total findings, severity rollup, per-audit summary, clickable drill-down phrase). New chat-formatter cases for the five audit tools render findings with severity icons + backtick-wrapped target IDs. Hardening: new sanitizeForChat() helper in Frogman.class.php strips backticks, control chars (CRLF/NUL), and the `{{` / `[` lead-in characters that would otherwise trigger client-side formatMarkdown patterns ({{cmd:}}, {{download:}}, [text](url)) — closes the prose-interpolation gap that GHSA-7qvv's fix left open for any future formatter case that interpolates user-controlled fields. Verified against six injection vectors (script tag, backtick breakout, CRLF, {{cmd:}}, [link](url), bold) — all neutralized server-side before the chat client sees them. Chat phrases: `audit all`, `audit posture`, `security check`. 225 tools, +1.
*1.7.0* Compliance audit tools — Tier 1 of the toll-fraud chain. Four new read-only `fm_audit_*` tools that report posture without changing config: fm_audit_voicemail_pins (mailboxes with weak/default PINs: empty, equals mailbox number, common defaults, repeating digits, sequential, sub-4-digit), fm_audit_extension_secrets (extensions with empty/default/short/low-entropy SIP secrets — secret values are never returned), fm_audit_orphan_dids (inbound routes with no destination set, plus catchall route detection as info-level finding), fm_audit_outbound_international (outbound routes with dial patterns starting 011/0011/00 — the international-prefix toll-fraud surface). All four are PERM_ADMIN, all four use BMO methods only (Voicemail->getVoicemail, Core->getAllDevicesByType + getDevice, Core->getAllDIDs, Core->getAllRoutes + getRoutePatternsByID — no direct writes, no commercial-module dependencies). Chat phrases: `audit voicemail pins`, `audit extension secrets`, `audit orphan dids`, `audit international`. Each tool returns count + severity_counts + findings array + one-line summary. Findings sorted by severity descending, ties broken by natural-sort of the target identifier. 224 tools, +4.
*1.6.9* CI-only patch release. The release workflow now uses `fetch-tags: true` on the checkout step so the annotated tag object is actually downloaded; previously the workflow pulled the commit (`--no-tags +<sha>:refs/tags/<name>`) which left the local ref as a lightweight tag, and `for-each-ref %(contents)` returned the commit message instead of the tag annotation. v1.6.8 shipped with its commit subject as the release body; this fixes that. No code changes; no user-visible behaviour change.
*1.6.8* Natural-language expansion layer for chat. New parser helper Tools/Interpret.php sits between users and ChatParser's strict regex waterfall: rewrites conversational/polite/emotional phrasing into the canonical command forms ChatParser already understands, then re-parses with fuzzy matching disabled. Pure text-in, text-out — no DB, no session, no LLM, no schema changes. Risk-gated confidence model (read 0.78 / write 0.88 / state 0.91 / unknown 0.95) means a rewrite only auto-runs when both the rule and the resulting command-shape agree it's safe; partial matches surface a "tried reading that as X — please rephrase" prompt instead of guessing. Handles request wrappers ("please", "could you", "i would like to"), phrasal verbs ("have a look at" → "show", "switch off" → "disable", "spin up" → "create"), assistant-directed abuse ("you idiot ..."), trailing urgency ("... ASAP"), numeric shorthand ("delete 1001" → "delete extension 1001"), and anchored state phrases ("1001 is sick" → "enable dnd on 1001", "1001 is back" → "disable dnd on 1001", "1001 is wfh" → "set followme 1001"). New `isCorrectionCancel()` helper wired into three pending-action cancel points so corrections like "no no", "that's not what I meant", "wrong command", "cancel that" cancel a pending confirm instead of being read as wizard answers. Off-switchable via kvstore key `frogman_interpret_mode=off`. Audit log captures both forms end-to-end (chat_input = raw, interpreted_as = rewritten); the v1.6.7 columns light up automatically. Also: removed raw ANSI escape from `frogman>` prompt in `fwconsole frogman:chat` that displayed as literal `\033[36m...\033[0m` on some terminals. Contributed by Kieran Byrne (FreePBX UK). 220 tools, unchanged.
*1.6.7* Audit-log now preserves chat-origin natural language. Two new columns on oc_audit_log: chat_input (raw user message for chat-initiated tool calls) and interpreted_as (canonical form after natural-language normalisation, populated when an upstream interpret/rewrite step fires; NULL otherwise). auditIntent() accepts both as optional trailing args; non-chat invocation paths (HTTP API, GraphQL, CLI, MCP) leave both NULL, so existing call sites are backwards-compatible. fm_audit_search surfaces both fields. Inline secrets in chat_input (password, vmpwd, token, etc.) are pattern-redacted before persistence via a new sibling helper redactChatInput() — key-name redaction (redactSensitive) operates on arrays and doesn't fit free text. Migration is idempotent (SHOW COLUMNS guard in install.php); safe to re-run. No breaking changes, no security advisory — this fills a forensic gap surfaced while reviewing an upstream community contribution that adds a natural-language expansion layer on top of ChatParser.
*1.6.6* **Security release.** Patches one advisory surfaced during a fresh internal audit on 2026-05-13. 220 tools, unchanged.
- GHSA (filed at ship time): stored XSS in the chat console's markdown formatter. The four template patterns in assets/js/chat.js's formatMarkdown — inline code, bold, markdown links, and download links — inserted regex capture groups as raw HTML, so any tool response that reflected a user-controlled field (extension name, ring-group description, etc.) could carry an HTML payload that the browser would execute when another admin viewed it through Frogman chat. FreePBX's own admin GUI escapes these fields properly (freepbx_htmlspecialchars throughout the views); the formatter was the leaky side. Real exposure depends on deployment topology — single-admin deployments are unaffected; multi-admin deployments with tier separation in oc_permissions face a write-tier-to-admin-tier escalation vector (lower-tier admin injects via a field they have write access to → higher-tier admin views via Frogman chat → payload runs in higher-tier session). No known affected deployments at disclosure time given the experimental project status. Fix: every regex capture going into HTML now passes through escapeHtml() before insertion, plus a scheme-allowlist on download URLs (block javascript:/data:/vbscript:). The chat console's CSP-less DOM-construction path means escaping at the formatter is the right defensive layer.
*1.6.5* New tool: fm_delete_userman_user. Wraps Userman->deleteUserByID() for cleaning up orphaned User Manager rows — useful after macro tests, manual extension deletes, or any scenario where the Userman side gets stranded. Accepts either numeric uid or username, requires confirm:true, dry-run preview surfaces whether the user has a linked extension (which is NOT deleted by this tool — by design). Chat: "delete userman user <username>" / "delete userman user <uid>". 220 tools, +1.
*1.6.4* Bug-fix sweep — eight issues closed across two batches plus the closeout of the long-standing #10 "global page toggle" as architecturally not feasible. No security advisories (all already-known issues). Tool count unchanged (219). New CI: tag-triggered GitHub Actions release workflow that builds the tarball from `git archive` and creates the release automatically; manual `gh release create` flow still works as a fallback.
- #28: fm_toggle_time_condition's missing-row check used `empty($tc)`, but `getTimeCondition()` returns a default-shaped non-empty array when no row matches. "Undefined array key 'displayname'" reached the caller instead of the documented "Time condition {id} not found" message. Now checks the populated identity field.
- #23: fm_reload's active-call counter was missing the `isAsteriskInternalChannel()` filter that other channel-counting tools already used. A stray `Message/ast_msg_queue` ghost would falsely trigger the reload-confirm prompt ("There are 1 active call(s)..."). Fixed.
- #26: fm_dialplan_remove validated `name` only with `!empty()`. The name then flowed into `DialplanFile::contextExists()` / `removeContext()`, both of which use regex on extensions_custom.conf — a name with regex metacharacters, newlines, or `]` could confuse the locator. Now allowlist-validated as `^[a-zA-Z0-9_-]+$` (every legitimate FreePBX context name matches).
- #17: generated passwords (in CreateAdmin and ResetPassword) used `random_int()` for character picks but `str_shuffle()` for the final shuffle. `str_shuffle` uses `mt_rand` internally and is not cryptographically secure, partially undoing the entropy guarantee. Replaced with `AbstractTool::secureShuffle()` — Fisher-Yates with `random_int` for each swap. Helper lives on AbstractTool so the next caller doesn't fall into the same trap.
- #19: fm_get_voicemail interpolated `$ext` directly into a `/var/spool/asterisk/voicemail/default/{ext}` filesystem path. Voicemail BMO's `getMailbox()` throws first for non-existent boxes today, so the path traversal vector wasn't actually exploitable — but the validation gap was real. One-line numeric check added as defense in depth against a future BMO behavior change.
- #22: fm_fwconsole's allowlist regex was anchored at start (`^`) but not at end of token. `context` matched `contextual`, `contextfoo`, and any future `context-prefixed` subcommand. Same bug shape applied to every token in the allowlist AND the parallel readonly-detection regex. Added trailing `(\\s|$)` to both — token must end at whitespace (then args) or end-of-string. Admin-only attack surface, so not exploitable as RCE by low-privilege callers — hardening fix.
- #24: fm_repair_userman_links and fm_ringgroup_add_member returned `dry_run: false` on a no-op early-exit path even when called without `confirm:true`. Broke the contract every other write tool follows (dry_run reflects whether confirm was passed, not whether a write happened). Both now return `dry_run: !confirm` plus a new `noop: true` field to distinguish "nothing to do because already correct" from a real preview.
- #14: fm_sip_trace used FIXED paths in /tmp for its state files (`.log`, `.meta`, `.pgid`). Two admins running traces concurrently overwrote each other's pgid, then a stop from either killed whichever process group was tracked. Two concurrent traces would capture IDENTICAL data anyway (same Asterisk log, same filter), so the "real" use case is "I forgot I was already tracing" accidents. `start` now refuses with a chat-friendly "trace already running, expires in Ns" message; pass `force: true` to replace the running trace.
- #10: closed as architecturally constrained. "Headless key/button toggle on every page" requires injecting JS/markup into every FreePBX admin page; no update-safe extension point exists for this in FreePBX 17. The tractable half (a `gui` chat command that navigates back to the relevant admin page based on the last-run tool) was noted in the close-out comment.
*1.6.3* **Security release.** Patches one advisory plus five additional bugs surfaced during the v1.6.1 post-release follow-up. Tool count unchanged (219).
- GHSA-q4c4-5cr4-8q47 (High, CVSS 7.1): privilege-scoping pattern. Eight tools that surface admin-grade data (AMI manager secrets, outbound PINs, full dialplan, root SSH command, backup paths, CDR/PII, arbitrary GraphQL execution, raw AMI endpoint dumps with trunk credentials) were inheriting the default PERM_READ level. Read-tier callers could pull data outside their authorization scope. fm_list_managers, fm_list_pinsets, fm_show_context, fm_get_mcp_config, fm_backup_status, fm_whos_calling, fm_run_saved_query, and fm_diagnose_trunk now require PERM_ADMIN. fm_diagnose_trunk additionally redacts credential-bearing lines (password, md5_cred, oauth_secret, refresh_token, dtls_private_key) from the AMI endpoint dump before storing in the response or audit log — defense in depth, since admin-tier callers also shouldn't have plaintext credentials persisted indefinitely. Real-world exposure at disclosure time was nil (no PERM_READ tokens in active use on any known deployment); fix landed proactively.
- Issue #15: fm_did_destination_map and fm_list_all_dids rendered every queue destination as the unnamed "Queue NNN" label. Root cause was `Tools/AbstractTool.php`'s describeDestination() helper running a query with `WHERE keyword='descr'` against queues_config — that filter is the schema of queues_details, not queues_config (which has descr as a direct column). Fixed by querying `descr` directly, matching the pattern SearchPbx already uses.
- Issue #11: every conference chat command (`who's in conference 600`, `kick X from conference 600`, `mute/unmute X in conference 600`, `lock/unlock conference 600`) returned `Parameter "room" is required` and never reached its tool. Root cause was a parameter-name mismatch: ChatParser built params with key `id`, every Confbridge* tool validates `room`. Same fix uncovered a coupled second bug in two of the five patterns — mute/unmute sent `state=on/off` and lock/unlock sent `state=lock/unlock`, but both tools read `params['action']`, so unmute silently muted and unlock silently locked. All three mismatches fixed in one pass.
- Issue #12: Confbridge* tools accepted any string for `room`, `channel`, and `action` and forwarded them straight to AMI. ConfbridgeListLive's `room` flows into a line-framed AMI Command string — a newline could disturb parsing. Now validated: `room` matches `[a-zA-Z0-9._-]+`, `channel` matches `[a-zA-Z0-9._/-]+` (Asterisk channel format), `action` allowlisted to `['mute','unmute']` or `['lock','unlock']` per tool.
- Issue #21: three write tools accepted free-text inputs that should be allowlist-validated. ToggleTimecondition now requires `state` to be one of `{0, 1, 2}` (was casting `(int)` after the fact, so 99/abc reached setState unfiltered). UpdateSipNat now validates `external_ip` as either a valid IP or a hostname (FreePBX externip accepts both), and `local_network` as comma/semicolon-separated entries each being a valid IP or CIDR (v4 or v6) with prefix range checked. SetCallForward now requires `type` to be one of `{CF, CFB, CFU}` and `ext` to be numeric.
- Issue #1 (closed via this release): README now has an explicit "Other MCP clients" subsection naming Claude Desktop, Claude Code, Cursor, Windsurf as tested with the same SSH-transport pattern, plus a pointer to INTEGRATION.md for non-MCP integrations (LangChain, OpenAI function calling, Slack/Teams/Telegram bots).
**Web-console impact:** non-admin web-console users (sessions without admin in oc_permissions) will be denied access to four chat-exposed tools — `fm_audit_search`, `fm_get_mcp_config`, `fm_backup_status`, `fm_whos_calling`. The chat console's live audit panel is unaffected — it uses a separate handleAuditFeed() endpoint that bypasses fm_audit_search. Sites that legitimately need read-tier users to access any of these can grant them admin via `set permission <user> to admin`.
*1.6.2* Install/upgrade-path fix for v1.6.1. The v1.6.1 install.php contained `$db = FreePBX::Database();` at file scope. FreePBX `require`s install.php from inside _runscripts() in modulefunctions.class.php, so the bare assignment leaked into the parent scope and replaced the legacy DB-class `$db` global with a BMO `FreePBX\Database` instance. The legacy DB's `escapeSimple` strips quotes (PearDB-compatible); the BMO Database's `escapeSimple` is `PDO::quote()` which keeps them. modulefunctions.class.php:2077 then double-quoted the version string and the modulename, producing `UPDATE modules SET version=''1.6.2'' WHERE modulename=''frogman''` — a SQL syntax error that aborted the install partway, leaving the module in a "Disabled; Pending upgrade" state requiring manual `mysql -e \"UPDATE modules SET version=...\"` recovery. v1.6.2 renames the local to `$frogmanDb` and documents the constraint. Anyone who hit this on v1.6.1: after upgrading to v1.6.2, run `fwconsole ma install frogman` again and it will complete cleanly. No security fixes; this is the install-path regression fix only.
*1.6.1* **Security release.** Patches four advisories disclosed via responsible disclosure on the FreePBX community forum on 2026-05-13:
- GHSA-pxfc-q72v-jh8m (Critical, CVSS 9.9): dialplan template parameter injection in fm_dialplan_apply. A PERM_WRITE caller could inject arbitrary Asterisk dialplan (System(), SHELL(), unexpected Goto) via newlines or special characters in any template parameter. Templates now validate inputs against per-parameter whitelists, with a framework-level reject for newline/CR/null in any value across all 9 templates.
- GHSA-9xf5-9ghq-p6cw (High, CVSS 7.4): API tokens stored plaintext in oc_api_tokens.token. A DB read (backup leak, SQL injection elsewhere, filesystem access) surfaced reusable tokens at the granted permission level. Storage now uses sha256$<hash> format; install.php migrates existing rows idempotently. **Existing user-held tokens continue to authenticate without re-issuance** — the raw value the user has hashes to the new stored format.
- GHSA-3p65-2prr-cfvf (Medium, CVSS 6.5): plaintext passwords / device secrets / tokens persisted into oc_audit_log via auditOutcome. Any PERM_READ caller could recover them via fm_audit_search. Added a redactSensitive() filter that scrubs known sensitive keys before persistence; fm_audit_search bumped to PERM_ADMIN; install.php scrubs historical entries. **Historical scrub is irreversible** — if you have compliance reasons to preserve the pre-patch oc_audit_log contents, back it up encrypted before upgrading.
- GHSA-f5jv-9rh8-gq7c (Medium, CVSS 6.5): fm_update_extension implemented as delete-and-recreate (delDevice + delUser + Userman->deleteUserByID + processQuickCreate with hardcoded vm=no), silently wiping voicemail, follow-me, call-forward, and Userman records on every "update." Switched to FreePBX's own internal edit pattern (delUser + addUser with editmode=true on user side; same for device when secret changes; Userman never touched), preserving every unchanged field.
Patches developed in temporary private forks tied to each advisory; back-merged on publish. SECURITY.md added at the repo root pointing at GitHub's private vulnerability report form for future disclosures. Thanks to the FreePBX community member whose AI-assisted code review surfaced the originals.
*1.6.0* (219 tools). Scope tightened to open-source-module integrations only. Parked nine tools that integrated with Sangoma commercial modules (Endpoint Manager via EPM BMO, DPMA via AMI, Sangoma Connect via Sangomaconnect BMO): fm_diagnose_sangoma_phone, fm_get_sangoma_phone, fm_list_sangoma_phones, fm_reboot_sangoma_phone, fm_dpma_alerts, fm_dpma_license_status, fm_list_sangoma_multicast_zones, fm_sangoma_emergency_alert, fm_sc_status. Files moved to Tools/_parked/ with .parked extension; the registry's Tools/*.php scan skips them but the code stays in git history and is recoverable. Retained read-only license/system utilities that read commercial-module surfaces without driving the modules themselves: fm_get_license_info, fm_update_activation, fm_list_cos. Removed corresponding ChatParser anchors, README catalog entries (Sangoma Connect, Sangoma Paging — DPMA Multicast), and the requireCommercialLicense() helper from AbstractTool (its only callers — the EPM reboot and emergency-alert tools — are parked).
*1.5.0* (228 tools). New: backup-monitoring trio — fm_list_backup_jobs ("list backup jobs"), fm_backup_status ("backup status" / "backup status for <name>"), fm_list_backup_runs ("list backup runs" / "list failed backup jobs"). All three are BMO-only (\FreePBX::Backup, \FreePBX::Filestore) and read-only. Status detects in-flight runs from runningBackupstatus, uses \Cron\CronExpression (already autoloaded by /etc/freepbx.conf, no new dep) to compute next/previous scheduled ticks, and emits failed_inferred rows for scheduled jobs whose previous tick has no successful artifact past a 1h grace window. Mirrors FreePBX Backup's own getLocalFiles() fallback when a recorded backupfile path doesn't match the actual artifact location. Replaces and supersedes the prior minimal fm_list_backups (which just returned listBackups() unenriched). New: fm_list_sangoma_multicast_zones + fm_sangoma_emergency_alert ("page emergency", in-walls multicast paging to Sangoma P-series via DPMA, LAN-only — drops the old flite/sox orphan dependencies and stays inside FreePBX's documented surfaces). UX: fm_check_upgrades now renders each upgradable module as its own row with an explicit "⬆️ Upgrade" chip (previously the per-row chip's label was just "⬆️ <name>" which read like static text), keeping the bottom "⬆️ Upgrade all" chip. Chat parser additions: list backup jobs, show backup status, backup status for <name>, list backup runs, list backup runs for <name>, list failed backups / list failed backup jobs / backup failures. Lint: fm_lint_typeahead 0 gaps across 250 parser anchors / 226 typeahead suggestions.
*1.4.1* (223 tools). Fixed: fm_module_install now auto-downloads from the configured repos when a module isn't yet locally available (previously errored out asking the user to run `fwconsole ma download` first), and no longer falsely reports success when `fwconsole ma install` exits 0 on "Cannot find module" — failures are now detected from output text too. fm_show_context ("show asterisk context <name>") was returning "Command 'dialplan show 'name'' failed" because escapeshellarg wrapped the name in single quotes that Asterisk CLI rejects, and its fwconsole context fallback wasn't a real command — rewritten to AMI-only (within the FreePBX walls) with a clean error if AMI is down. UX: fm_list_trunks output gained inline Enable/Disable action chips per trunk based on current state, and bare "enable trunk" / "disable trunk" (no ID) now routes to the trunk list so users can pick the target without having to first remember the ID. Typeahead now also harvests every chat command exposed via the sidebar (data-cmd / data-paste in views/main.php), so adding or removing a sidebar button updates the suggestion list automatically. Help text and sidebar paste templates converted to bracket-placeholder syntax across the board (e.g. "forward [ext] to [number]" instead of "forward 1001 to 5551234567"); chat input now auto-selects the first placeholder after a typeahead pick or sidebar paste so the user can immediately type to replace it; sendMessage guards against unfilled placeholders before they hit the parser. Internal: every fwconsole invocation across 14 tools now routes through a single AbstractTool::runFwconsole() wrapper so a future FreePBX-side change to the binary path or argv shape is patched in one place rather than 16. fm_get_sip_settings stopped reading /etc/asterisk/rtp_additional.conf directly and stopped SELECTing kvstore_Sipsettings — both go through Sipsettings::getConfig() now so storage moves cleanly across FreePBX updates; same tool also strips the leftover "Privilege: Command" header AMI prefixes Command responses with. Typeahead coverage pass: filled gaps for Sangoma Connect (sc status, sangoma connect status), MCP/Connect setup, KB search, DID map, allow/unallow, peak hours, busiest extensions, search/find, export, when-someone-calls wizard, sys info, reset password, revoke token, and need/check reload. Suggestion count grew 164 → 215. New fm_lint_typeahead dev tool walks every parser anchor and reports any whose keyword group has no backtick phrase in helpText, so future commands can't silently slip out of the typeahead. fm_add_ringgroup now normalizes member input across any separator (commas, spaces, mixed, newlines) before joining with the canonical dash so the FreePBX GUI editor stacks extensions one-per-line; previously space-separated input ("create ringgroup 700 with 101 102 103") landed in storage with literal spaces and the GUI rendered them on one line.
*1.4.0* (223 tools). New: fm_did_destination_map ("did map" / "inbound map" / "where do my dids go") — Mermaid horizontal flowchart of every inbound DID and its first-hop destination, deduped so multiple DIDs into the same RG share one node. Typeahead command suggestions in the chat input — type 2+ chars to see matching commands, ↑↓ to pick, Tab/Enter to fill (164 phrases auto-extracted from help text). UX: rich "show extension" detail with live registration cross-reference, recording mode chips, NAT/codec triple, and quick-action chips at the bottom; "list all dids" now resolves dialplan strings to friendly labels ("ivr-8,s,1" → "IVR: Main-Menu"); Sangoma tools all bulleted-block formatters with phone emoji; trimmed sidebar Commands section from 75 buttons to 10 curated templates (typeahead handles the long tail); cache-busted asset URLs for chat.js/css. Fixed: fm_originate_call ("call X to Y") was double-broken — wrong param names AND wrong arity to astman->Originate; show extension N hit the substring/search pattern before the digit-only get pattern (so "show extension 100" returned 100 + 1100); fm_list_active_calls / fm_whos_on_the_phone / dashboard active-call count all now filter Asterisk internal Message/AsyncGoto worker channels; fm_list_contacts handled User Manager group schema (uid not id, numbers pre-populated) and falls back through fname+lname → displayname → core users.name → "Ext NNNN" instead of rendering empty bold; fm_create_admin Userman + addAMPUser arity for FreePBX 17. Refactor: AbstractTool gained describeDestination and isAsteriskInternalChannel helpers, used by 5 tools — same regex/SQL was previously inlined.
*1.3.1* (222 tools). Fixed: fm_sip_trace was capturing 0 data on calls placed during the window (grep ran once on the existing log instead of tail-following new lines, plus output-buffering meant short traces never flushed); SaveQuery and DeleteSavedQuery now require confirm:true (audit pass surfaced two write tools missing the dry-run gate). New: guided wizard for "add inbound route" with smart destination shorthand (1001 → from-did-direct,1001,1; vm 1001, rg 600, ivr 1, tc 1) and skippable description prompt; macro wizard infrastructure that chains multiple tools with optional yes/no gates and a single end-of-flow confirmation; first macro "onboard new employee" handles extension + Follow Me + ring group + DID + outbound CID + reload in one flow, surfacing the auto-generated UCP password in a Credentials footer. UI: new Wizards sidebar section in the chat console.
*1.3.0* Bug-fix and UX release (222 tools). Fixed: ring-timer now actually applies to live calls; outbound caller ID now actually applies; new extensions can now log in to UCP (auto-wired into default groups + extension assignment, auto-generated UCP password surfaced with reset hint); Set Follow Me works from chat as a guided wizard ("set follow me" or "set follow me 1001"); Module install/uninstall/enable/disable/upgrade now route correctly from chat (rebrand straggler); Module upgrade now distinguishes "already up-to-date" from real upgrades and from errors. New: fm_repair_userman_links to backfill UCP wiring on existing User Manager users ("repair userman" / "fix ucp logins"); fm_check_upgrades for an online repo check ("check for upgrades"); list-modules summary view with clickable license-bucket filters; multi-step wizard infrastructure in ChatParser. Auth: API token's level column is now the source of truth for token-authenticated calls (was being silently ignored); localhost-trust resolves to admin.
*1.2.1* Sangoma Connect status + email-onboarding chain (220 tools): fm_sc_status (SC preflight diagnostic — license, domain, cert, seats, next-step hint with clickable purchase/cert links), fm_set_extension_email (dual-layer email update — userman + voicemail-to-email via Voicemail::updateMailbox), fm_add_extension now always creates a linked User Manager user, fm_update_activation rewritten to use the deployment-ID-based fwconsole activate command (full GUI-equivalent handshake, backgrounded for chat compat — fixes stale-license-cache bug). New ChatParser pending_input mechanism for free-text follow-ups: voicemail-enable → "Add email?" → input prompt with clickable Skip → reload chain. Chat: "sc status", "sangoma connect status", "set email 1099 a@b.com", "add ext 1100 name Foo email a@b.com".
*1.2.0* Tier 1 Sangoma/DPMA tools (218 tools): fm_diagnose_sangoma_phone (composite — EPM mapping, license, registration, DPMA state, firmware audit, alerts), fm_list_sangoma_phones, fm_get_sangoma_phone, fm_dpma_alerts, fm_dpma_license_status, fm_reboot_sangoma_phone. Chat: "diagnose sangoma 1005", "list sangoma phones", "sangoma phone 1005", "dpma alerts", "dpma license", "reboot sangoma 1005". All BMO-first, AMI for runtime state, no shell-outs.
*1.1.1* Cap chat/MCP entry points at 256MB so a runaway tool/parser bug fails in ~1s instead of stalling Apache workers
*1.1.0* Add fm_whos_on_the_phone (212 tools); fix ChatParser OOM from recursive synonym expansion on "create inbound route"; route questions ending with "?" to KB search; security: shell injection fix in FwconsoleCmd, AMI allowlist, input sanitization; tool prefix oc_ → fm_
*1.0.0* Initial release: 211 tools, MCP server, web console, CLI chat, knowledge base RAG, call flow visualizer, API token auth
]]></changelog> <depends>
<version>17.0.1</version>
<module>api ge 17.0.1</module>
</depends>
<database>
<table name="oc_audit_log">
<field name="id" type="integer" unsigned="true" primarykey="true" autoincrement="true"/>
<field name="tool" type="string" length="100" default=""/>
<field name="params" type="text" notnull="false"/>
<field name="user_id" type="integer" notnull="false"/>
<field name="session_id" type="string" length="64" notnull="false"/>
<field name="intent" type="text" notnull="false"/>
<field name="chat_input" type="text" notnull="false"/>
<field name="interpreted_as" type="text" notnull="false"/>
<field name="status" type="string" length="20" default="pending"/>
<field name="detail" type="text" notnull="false"/>
<field name="created_at" type="integer" unsigned="true" default="0"/>
<field name="completed_at" type="integer" unsigned="true" notnull="false"/>
<key name="idx_oc_audit_tool" type="index">
<column name="tool"/>
</key>
<key name="idx_oc_audit_user" type="index">
<column name="user_id"/>
</key>
<key name="idx_oc_audit_session" type="index">
<column name="session_id"/>
</key>
<key name="idx_oc_audit_created" type="index">
<column name="created_at"/>
</key>
</table>
<table name="oc_sessions">
<field name="id" type="string" length="64" primarykey="true"/>
<field name="user_id" type="integer" notnull="false"/>
<field name="started_at" type="integer" unsigned="true" default="0"/>
<field name="last_activity" type="integer" unsigned="true" default="0"/>
<field name="context" type="text" notnull="false"/>
<field name="status" type="string" length="20" default="active"/>
</table>
<table name="oc_saved_queries">
<field name="id" type="integer" unsigned="true" primarykey="true" autoincrement="true"/>
<field name="name" type="string" length="100" default=""/>
<field name="query" type="text" notnull="false"/>
<field name="param_spec" type="text" notnull="false"/>
<field name="created_by" type="integer" notnull="false"/>
<field name="created_at" type="integer" unsigned="true" default="0"/>
<key name="idx_oc_sq_name" type="unique">
<column name="name"/>
</key>
</table>
<table name="oc_jobs">
<field name="id" type="integer" unsigned="true" primarykey="true" autoincrement="true"/>
<field name="tool" type="string" length="100" default=""/>
<field name="params" type="text" notnull="false"/>
<field name="user_id" type="integer" notnull="false"/>
<field name="status" type="string" length="20" default="queued"/>
<field name="result" type="text" notnull="false"/>
<field name="created_at" type="integer" unsigned="true" default="0"/>
<field name="started_at" type="integer" unsigned="true" notnull="false"/>
<field name="completed_at" type="integer" unsigned="true" notnull="false"/>
<key name="idx_oc_jobs_status" type="index">
<column name="status"/>
</key>
</table>
<table name="oc_aliases">
<field name="id" type="integer" unsigned="true" primarykey="true" autoincrement="true"/>
<field name="alias" type="string" length="100" default=""/>
<field name="tool" type="string" length="100" default=""/>
<field name="default_params" type="text" notnull="false"/>
<field name="created_by" type="integer" notnull="false"/>
<key name="idx_oc_alias_name" type="unique">
<column name="alias"/>
</key>
</table>
<table name="oc_api_tokens">
<field name="id" type="integer" unsigned="true" primarykey="true" autoincrement="true"/>
<field name="username" type="string" length="100" default=""/>
<field name="token" type="string" length="80" default=""/>
<field name="description" type="string" length="255" notnull="false"/>
<field name="level" type="string" length="20" default="read"/>
<field name="active" type="integer" unsigned="true" default="1"/>
<field name="created_at" type="integer" unsigned="true" default="0"/>
<field name="last_used_at" type="integer" unsigned="true" default="0"/>
<key name="idx_oc_token" type="unique">
<column name="token"/>
</key>
</table>
</database>
<menuitems>
<frogman category="Admin">Frogman</frogman>
</menuitems>
<console>
<command>
<name>frogman:tool</name>
<class>Tool</class>
</command>
<command>
<name>frogman:chat</name>
<class>Chat</class>
</command>
</console>
<supported>
<version>17.0</version>
</supported>
</module>