Skip to content

Commit 67b9c5f

Browse files
fix(migration): close silent-pass, false-positive and wrong-guidance defects across the skill
Found by external review (Codex bot, a full-PR rubric pass, and a sweep of the 38 PR files no prior review round had opened). All thirteen sit OUTSIDE lint-required-messaging-profile.py, which is the only file earlier rounds examined. Discovery and validation could not see what they claimed to check: - scan-twilio-usage.sh matched PHP with a doubled backslash under `grep -F`, where it means two literal backslashes. No PHP project has ever been detected: every one scanned as zero files and passed discovery silently. - run-validation.sh skipped a dedicated .twiml/.texml file whose root element is not <Response>, so a broken or unconverted document was never validated. A generic .xml stays exempt. - validate-migration.sh reported PASS on files it could not read; every grep ends `2>/dev/null || true`, so an unreadable file contributes no matches. It now fails: what cannot be read cannot be certified. - lint-telnyx-correctness.sh lexed alias suffixes (.phtml, .kt, .bash) without canonicalising, so their comments were never masked and dead code blocked valid migrations. The sibling scanners already canonicalised. - lint-telnyx-correctness.sh downgraded EVERY issue to a warning whenever any product was hybrid, so a leftover Twilio messaging import was waived because voice was kept. The waiver is now scoped to the product actually kept. - post-test-diagnostic.sh read count keys at the top level (they live under `summary`) and compared uppercase status values against lowercase ones, so it reported "Fails: 0 / All clear" while validation was failing. - smoke-test.sh appended a second "000" to curl's own output, making the variable "000000" and the unreachable-webhook branch dead code. Guidance that does not work, verified against the real SDKs: - Ruby examples called Telnyx::Webhook.construct_event, which does not exist (checked against telnyx 5.158.0). NameError is a StandardError, so the usual rescue swallowed it and every webhook was rejected with 403. Five call sites across two skills now use client.webhooks.unwrap(payload, headers:, key:). - The Go verifier accepted any signed payload regardless of age and consumed the request body without restoring it. It now enforces the documented 5-minute tolerance, restores the body, and checks the key length before ed25519.Verify, which panics on a wrong-length key. - test-webhooks-local.py sends a deliberately INVALID signature, yet scored a 2xx as PASS. It passed handlers with no signature verification and failed every handler that verified one. 401/403 is now the pass, and a 2xx is reported as INSECURE. - webrtc-migration.md told readers to write <Dial supervisorRole>, which is a Conference REST API parameter, not a TeXML attribute. The skill's own validator rejects it and the runtime drops it silently. Verified: 250 tests pass; a 366-case contract corpus passes with no known gaps; each fix carries a control proving it does not over-correct, and each new contract was mutation-checked against the pre-fix code.
1 parent e5e5732 commit 67b9c5f

39 files changed

Lines changed: 669 additions & 132 deletions

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ user_invocable: true
1010
metadata:
1111
author: telnyx
1212
product: migration
13-
compatibility: "Requires bash 4+, jq, curl. macOS ships bash 3.2 — scripts auto-upgrade via Homebrew bash if available (brew install bash)."
13+
compatibility: "Requires bash 4+, jq, curl, python3. macOS ships bash 3.2 — scripts auto-upgrade via Homebrew bash if available (brew install bash). python3 is mandatory: the scanners and the correctness linter exit 2 without it."
1414
---
1515

1616
# Twilio to Telnyx Migration

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/references/messaging-migration.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,17 @@ client = Telnyx(
359359
public_key=os.environ['TELNYX_PUBLIC_KEY'],
360360
)
361361

362+
def seen_event(event_id):
363+
"""Claim an event id exactly once. Truthy only for a FIRST delivery.
364+
365+
Shown with Redis because the claim must be atomic and shared across every
366+
worker and instance; a module-level set silently stops deduplicating the
367+
moment you run more than one worker. `NX` makes SET succeed only when the
368+
key is absent, so two concurrent retries cannot both win. The 24h TTL keeps
369+
the key space bounded while comfortably outlasting the retry window.
370+
"""
371+
return redis.set(f'telnyx:event:{event_id}', '1', nx=True, ex=86400)
372+
362373
def verified_telnyx_data():
363374
raw_body = request.get_data(as_text=True) # original body, before parsing
364375
try:
@@ -372,6 +383,21 @@ def sms():
372383
event = verified_telnyx_data()
373384
if event.get('event_type') != 'message.received':
374385
return '', 200
386+
387+
# DEDUPLICATE BEFORE REPLYING. Telnyx retries a webhook until it gets a
388+
# 2xx, so a slow handler, a timeout or a deploy mid-request delivers the
389+
# SAME event again. The reply below is a BILLABLE outbound SMS: without
390+
# this guard a retry sends the customer a second message and bills you for
391+
# it. Every webhook carries a stable `id`; record it FIRST and drop the
392+
# event if it is already known.
393+
#
394+
# `seen_event` must be atomic and shared across every worker and instance —
395+
# an in-process set does not deduplicate behind more than one worker.
396+
# Redis: `SET <id> 1 NX EX 86400` returns falsy when the key already
397+
# exists. A unique index on an events table works equally well.
398+
if not seen_event(event['id']): # falsy => already processed
399+
return '', 200
400+
375401
payload = event['payload']
376402
from_number = payload['from']['phone_number'] # the person who texted us
377403
to_number = payload['to'][0]['phone_number'] # our number they texted

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/references/voice-migration.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -406,21 +406,50 @@ try {
406406
// Telnyx webhook signature validation in Go
407407
// Use the telnyx-go SDK or verify Ed25519 manually:
408408
import (
409+
"bytes"
409410
"crypto/ed25519"
410411
"encoding/base64"
411412
"io"
412413
"net/http"
414+
"strconv"
415+
"time"
413416
)
414417

415-
func verifyWebhook(r *http.Request, publicKeyBase64 string) bool {
416-
bodyBytes, _ := io.ReadAll(r.Body)
418+
// Returns the verified body. The caller must use THIS slice - reading r.Body
419+
// again yields nothing, because io.ReadAll consumes it.
420+
func verifyWebhook(r *http.Request, publicKeyBase64 string) ([]byte, bool) {
421+
bodyBytes, err := io.ReadAll(r.Body)
422+
if err != nil {
423+
return nil, false
424+
}
425+
// Restore the body so a handler that decodes r.Body still works. Without
426+
// this the handler reads zero bytes and fails on every request.
427+
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
428+
417429
signature := r.Header.Get("telnyx-signature-ed25519")
418430
timestamp := r.Header.Get("telnyx-timestamp")
419-
// Concatenate timestamp + "|" + payload, verify with Ed25519 public key
420-
pubKeyBytes, _ := base64.StdEncoding.DecodeString(publicKeyBase64)
421-
sigBytes, _ := base64.StdEncoding.DecodeString(signature)
431+
432+
// REJECT STALE DELIVERIES. Without a freshness check any captured signed
433+
// payload - from a log dump, a proxy trace, a shared staging endpoint -
434+
// verifies forever, so an attacker can replay it indefinitely. Telnyx
435+
// documents a 5-minute tolerance.
436+
ts, err := strconv.ParseInt(timestamp, 10, 64)
437+
if err != nil || time.Since(time.Unix(ts, 0)) > 5*time.Minute {
438+
return nil, false
439+
}
440+
441+
pubKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
442+
// ed25519.Verify PANICS on a wrong-length key, so a misconfigured
443+
// TELNYX_PUBLIC_KEY would crash the handler instead of rejecting.
444+
if err != nil || len(pubKeyBytes) != ed25519.PublicKeySize {
445+
return nil, false
446+
}
447+
sigBytes, err := base64.StdEncoding.DecodeString(signature)
448+
if err != nil {
449+
return nil, false
450+
}
422451
message := []byte(timestamp + "|" + string(bodyBytes))
423-
return ed25519.Verify(ed25519.PublicKey(pubKeyBytes), message, sigBytes)
452+
return bodyBytes, ed25519.Verify(ed25519.PublicKey(pubKeyBytes), message, sigBytes)
424453
}
425454
```
426455

@@ -435,7 +464,10 @@ post '/webhook' do
435464
signature = request.env['HTTP_TELNYX_SIGNATURE_ED25519']
436465
timestamp = request.env['HTTP_TELNYX_TIMESTAMP']
437466
begin
438-
Telnyx::Webhook.construct_event(payload, signature, timestamp, public_key: 'YOUR_PUBLIC_KEY')
467+
# Verification lives on the CLIENT. There is no Telnyx::Webhook module -
468+
# naming one raises NameError, which `rescue` swallows, so every webhook
469+
# would be rejected with 403.
470+
client.webhooks.unwrap(payload, headers: request.env, key: ENV['TELNYX_PUBLIC_KEY'])
439471
# Signature valid
440472
rescue Telnyx::SignatureVerificationError
441473
halt 403, 'Forbidden'

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/references/webhook-migration.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,11 @@ func verifyWebhook(payload, signature, timestamp, publicKeyBase64 string) bool {
171171
```ruby
172172
require 'telnyx'
173173
client = Telnyx::Client.new(api_key: 'YOUR_API_KEY')
174-
Telnyx::Webhook.construct_event(payload, signature, timestamp, public_key: 'YOUR_PUBLIC_KEY')
174+
# Verification lives on the CLIENT: client.webhooks.unwrap(payload, headers:).
175+
# There is no Telnyx::Webhook module - referencing one raises NameError, and
176+
# because NameError is a StandardError the usual `rescue StandardError` around
177+
# it swallows the error and rejects EVERY webhook with 403.
178+
client.webhooks.unwrap(payload, headers: request.env, key: ENV['TELNYX_PUBLIC_KEY'])
175179
```
176180

177181
## Framework-Specific Examples
@@ -282,11 +286,10 @@ post '/webhooks/messaging' do
282286

283287
# Verify signature
284288
begin
285-
Telnyx::Webhook.construct_event(
289+
client.webhooks.unwrap(
286290
payload,
287-
request.env['HTTP_TELNYX_SIGNATURE_ED25519'],
288-
request.env['HTTP_TELNYX_TIMESTAMP'],
289-
public_key: ENV['TELNYX_PUBLIC_KEY']
291+
headers: request.env,
292+
key: ENV['TELNYX_PUBLIC_KEY']
290293
)
291294
rescue StandardError
292295
halt 403, 'Forbidden'
@@ -382,7 +385,7 @@ class WebhooksController < ApplicationController
382385
request.headers['telnyx-timestamp']
383386

384387
begin
385-
Telnyx::Webhook.construct_event(
388+
client.webhooks.unwrap(
386389
payload,
387390
signature,
388391
timestamp,

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/references/webrtc-migration.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,15 @@ These patterns apply when migrating Twilio-based contact centers, PBX systems, o
592592
593593
**Twilio pattern**: Conferences are required for any scenario with more than 2 call legs or where you need supervisor features (listen, whisper, barge). Even simple call transfers often use conferences.
594594
595-
**Telnyx pattern**: Conferences are only needed for true multi-party audio (3+ participants). For two-party calls with supervisor features, use `<Dial>` with `supervisorRole`:
595+
**Telnyx pattern**: Conferences are only needed for true multi-party audio (3+ participants).
596+
597+
> **`supervisorRole` is a Conference REST API parameter, not a TeXML attribute.**
598+
> It is set when joining a participant to a conference via the API
599+
> (`supervisor_role: barge | whisper | monitor`), and there is no
600+
> `<Dial supervisorRole="...">` in TeXML — the skill's own validator rejects the
601+
> attribute and the runtime silently drops it, so a migration written that way
602+
> loses supervisor behaviour with no error. Supervisor features therefore still
603+
> require a conference; only plain two-party calls avoid one.
596604
597605
```javascript
598606
// Twilio: requires conference for supervisor

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/scripts/lint-telnyx-correctness.sh

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,33 @@ elif [ -n "$STATE_FILE" ] && [ ! -f "$STATE_FILE" ]; then
406406
echo "Warning: --state-file '$STATE_FILE' not found, ignoring" >&2
407407
fi
408408

409+
# Waive only for the product that is actually KEPT on Twilio.
410+
#
411+
# Testing `-n "$KEPT_ON_TWILIO"` alone made ANY hybrid state a GLOBAL waiver: in
412+
# a `--product messaging` run, a leftover Twilio messaging import was downgraded
413+
# to a warning - and the linter exited clean - merely because an unrelated
414+
# product such as voice was kept on Twilio. That is a silent pass on exactly the
415+
# residue the check exists to find.
416+
#
417+
# The product is passed per call site because these checks are not all inside a
418+
# product-scoped block; the global ones (residual imports, client instantiation,
419+
# directory names, docs) legitimately span products and pass "any", which waives
420+
# whenever ANY product is kept - the original behaviour, but now stated at the
421+
# call site rather than applied silently everywhere.
422+
kept_on_twilio() {
423+
local product="$1"
424+
[ -n "$KEPT_ON_TWILIO" ] || return 1
425+
[ "$product" = "any" ] && return 0
426+
case ",$KEPT_ON_TWILIO," in
427+
*",$product,"*) return 0 ;;
428+
*) return 1 ;;
429+
esac
430+
}
431+
409432
lint_issue_or_hybrid_warn() {
410-
if [ -n "$KEPT_ON_TWILIO" ]; then
433+
local product="$1"
434+
shift
435+
if kept_on_twilio "$product"; then
411436
lint_warn "$1" "$2 (hybrid deployment — $KEPT_ON_TWILIO kept on Twilio)" "$3" "${4:-}"
412437
else
413438
lint_issue "$1" "$2" "$3" "${4:-}"
@@ -454,7 +479,7 @@ if product_applies "messaging"; then
454479
fi
455480
count=$(count_matches "$twilio_create_calls")
456481
if [ "$count" -gt 0 ]; then
457-
lint_issue_or_hybrid_warn "twilio_messages_create" \
482+
lint_issue_or_hybrid_warn "messaging" "twilio_messages_create" \
458483
"Twilio .messages.create() request using body found at $count call site(s)" \
459484
"Use telnyx.messages.send() (Python) or telnyx.messages.create() with the text parameter (JavaScript/Ruby)" \
460485
"$(matches_to_json "$twilio_create_calls")"
@@ -595,8 +620,14 @@ for directory, child_dirs, filenames in os.walk(root):
595620
source = path.read_text(encoding="utf-8", errors="replace")
596621
# Keep string/template contents (where live TeXML is commonly stored),
597622
# while blanking host-language comments without shifting line numbers.
623+
# CANONICALISE first. lex_source only treats canonical suffixes as
624+
# comment-bearing, so an alias (.phtml, .kt, .kts, .scala, .bash, ...)
625+
# was lexed as an unknown language and its comments were never masked -
626+
# a commented-out <Gather speechModel> then blocked a valid migration.
627+
# The sibling scanners already call canonical_suffix here; this was the
628+
# only consumer passing the raw suffix through.
598629
scan_source = analyzer.lex_source(
599-
source, path.suffix.lower()
630+
source, analyzer.canonical_suffix(path)
600631
).without_comments
601632
scan_source = re.sub(
602633
r"<!--.*?-->|<!\[CDATA\[.*?\]\]>",
@@ -789,7 +820,7 @@ if product_applies "all"; then
789820
twilio_webhook_mw=$(search_files "(twilio\.webhook\(|@validate_twilio_request|RequestValidator\(|twilio.*validateRequest|validateExpressRequest)" "*.py" "*.js" "*.ts" "*.rb")
790821
twilio_mw_count=$(count_matches "$twilio_webhook_mw")
791822
if [ "$twilio_mw_count" -gt 0 ]; then
792-
lint_issue_or_hybrid_warn "twilio_webhook_middleware" \
823+
lint_issue_or_hybrid_warn "any" "twilio_webhook_middleware" \
793824
"Twilio webhook middleware/validator still present in $twilio_mw_count file(s)" \
794825
"Remove if original had validate:false (it was a no-op). Replace with Ed25519 if original actually validated." \
795826
"$(matches_to_json "$twilio_webhook_mw")"
@@ -833,7 +864,7 @@ done
833864
doc_files=$(echo "$doc_files" | sed '/^$/d')
834865
doc_count=$(echo "$doc_files" | sed '/^$/d' | wc -l | tr -d ' ')
835866
if [ "$doc_count" -gt 0 ]; then
836-
lint_issue_or_hybrid_warn "docs_still_twilio" \
867+
lint_issue_or_hybrid_warn "any" "docs_still_twilio" \
837868
"Documentation files still reference Twilio (not migration-related references) in $doc_count file(s)" \
838869
"Update README/docs: replace Twilio service names, env vars, setup instructions, and URLs with Telnyx equivalents" \
839870
"$(echo "$doc_files" | sed '/^$/d' | head -10 | jq -R -s '{files: (split("\n") | map(select(length > 0)))}' 2>/dev/null || echo '{"files":[]}')"
@@ -850,7 +881,7 @@ section_header "Residual Twilio Patterns"
850881
matches=$(search_live_files comments '(from twilio|import[ (].*twilio|require.*twilio|using Twilio|import com\.twilio|use[[:space:]]+\\?Twilio|new[[:space:]]+\\?Twilio)' "*.py" "*.js" "*.ts" "*.rb" "*.go" "*.java" "*.php" "*.cs")
851882
count=$(count_matches "$matches")
852883
if [ "$count" -gt 0 ]; then
853-
lint_issue_or_hybrid_warn "residual_twilio_imports" \
884+
lint_issue_or_hybrid_warn "any" "residual_twilio_imports" \
854885
"Residual Twilio imports found in $count file(s)" \
855886
"Remove Twilio imports — migration should replace them with Telnyx equivalents" \
856887
"$(matches_to_json "$matches")"
@@ -862,7 +893,7 @@ fi
862893
matches=$(search_live_files code '(Client\(.*account_sid|Twilio\(|twilio\.Twilio\(|new Twilio\.)' "*.py" "*.js" "*.ts" "*.rb" "*.go" "*.java" "*.php")
863894
count=$(count_matches "$matches")
864895
if [ "$count" -gt 0 ]; then
865-
lint_issue_or_hybrid_warn "twilio_client_instantiation" \
896+
lint_issue_or_hybrid_warn "any" "twilio_client_instantiation" \
866897
"Twilio client instantiation found in $count file(s)" \
867898
"Replace with Telnyx client: from telnyx import Telnyx; client = Telnyx(api_key=...) (Python) or new Telnyx({ apiKey: ... }) (JS)" \
868899
"$(matches_to_json "$matches")"
@@ -877,7 +908,7 @@ twilio_dirs=$(find "$PROJECT_ROOT" -mindepth 1 \
877908
-o -type d -iname '*twilio*' -print 2>/dev/null || true)
878909
twilio_dir_count=$(echo "$twilio_dirs" | sed '/^$/d' | wc -l | tr -d ' ')
879910
if [ "$twilio_dir_count" -gt 0 ] && [ -n "$(echo "$twilio_dirs" | sed '/^$/d')" ]; then
880-
lint_issue_or_hybrid_warn "twilio_directory_names" \
911+
lint_issue_or_hybrid_warn "any" "twilio_directory_names" \
881912
"Found $twilio_dir_count directory name(s) containing 'twilio'" \
882913
"Rename directories: replace 'twilio' with 'telnyx' in directory names (e.g., feature/twilio/ → feature/telnyx/)" \
883914
"$(echo "$twilio_dirs" | sed '/^$/d' | head -10 | jq -R -s '{directories: (split("\n") | map(select(length > 0)))}' 2>/dev/null || echo '{"directories":[]}')"

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/scripts/post-test-diagnostic.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ if [ -f "$LINT_SCRIPT" ]; then
214214
[ -f "$PROJECT_ROOT/twilio-scan.json" ] && SCAN_ARG="--scan-json $PROJECT_ROOT/twilio-scan.json"
215215
LINT_JSON=$(bash "$LINT_SCRIPT" --json $SCAN_ARG "$PROJECT_ROOT" 2>/dev/null || echo "")
216216
if [ -n "$LINT_JSON" ]; then
217-
LINT_ISSUES=$(echo "$LINT_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('issues',0))" 2>/dev/null || echo "?")
218-
LINT_WARNS=$(echo "$LINT_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('warnings',0))" 2>/dev/null || echo "?")
219-
LINT_PASSES=$(echo "$LINT_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('passes',0))" 2>/dev/null || echo "?")
217+
LINT_ISSUES=$(echo "$LINT_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary',{}).get('issues',0))" 2>/dev/null || echo "?")
218+
LINT_WARNS=$(echo "$LINT_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary',{}).get('warnings',0))" 2>/dev/null || echo "?")
219+
LINT_PASSES=$(echo "$LINT_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary',{}).get('passes',0))" 2>/dev/null || echo "?")
220220
echo " Issues: $LINT_ISSUES Warnings: $LINT_WARNS Passes: $LINT_PASSES"
221221

222222
# Show issue details
@@ -247,18 +247,18 @@ if [ -f "$VALIDATE_SCRIPT" ]; then
247247
[ -f "$PROJECT_ROOT/twilio-scan.json" ] && SCAN_ARG="--scan-json $PROJECT_ROOT/twilio-scan.json"
248248
VALIDATE_JSON=$(bash "$VALIDATE_SCRIPT" --json $SCAN_ARG "$PROJECT_ROOT" 2>/dev/null || echo "")
249249
if [ -n "$VALIDATE_JSON" ]; then
250-
VAL_FAILS=$(echo "$VALIDATE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('fail_count',0))" 2>/dev/null || echo "?")
251-
VAL_WARNS=$(echo "$VALIDATE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('warn_count',0))" 2>/dev/null || echo "?")
252-
VAL_PASSES=$(echo "$VALIDATE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('pass_count',0))" 2>/dev/null || echo "?")
250+
VAL_FAILS=$(echo "$VALIDATE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary',{}).get('fail',0))" 2>/dev/null || echo "?")
251+
VAL_WARNS=$(echo "$VALIDATE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary',{}).get('warn',0))" 2>/dev/null || echo "?")
252+
VAL_PASSES=$(echo "$VALIDATE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary',{}).get('pass',0))" 2>/dev/null || echo "?")
253253
echo " Fails: $VAL_FAILS Warnings: $VAL_WARNS Passes: $VAL_PASSES"
254254

255255
echo "$VALIDATE_JSON" | python3 -c "
256256
import sys, json
257257
d = json.load(sys.stdin)
258258
for check in d.get('checks', []):
259-
if check.get('status') == 'FAIL':
259+
if str(check.get('status','')).lower() in ('fail', 'issue'):
260260
print(f\" FAIL: {check.get('name', '?')}\")
261-
elif check.get('status') == 'WARN':
261+
elif str(check.get('status','')).lower() == 'warn':
262262
print(f\" WARN: {check.get('name', '?')}\")
263263
" 2>/dev/null || true
264264
else

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/scripts/run-validation.sh

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ if [ "$INCLUDE_TEXML" = true ]; then
149149
# failed the run on a stale bundle inside it - one run contradicting
150150
# itself on the same tree.
151151
TEXML_FILES=()
152+
TEXML_INVALID_ROOTS=()
152153
while IFS= read -r -d '' file; do
153154
# Only TeXML documents belong in validate-texml.sh. Every *.xml went in
154155
# before, so a Maven pom.xml or Android layout was "validated" as TeXML
@@ -178,14 +179,33 @@ PYEOF
178179
)
179180
if [ "$root" = "Response" ]; then
180181
TEXML_FILES+=("$file")
182+
else
183+
# A DEDICATED .twiml/.texml extension declares the file's intent: it is
184+
# meant to be a TeXML document, so a root that is not <Response> is a
185+
# BROKEN document, not someone else's XML. Skipping it silently meant a
186+
# misspelled or unconverted root (<Respones>, or a left-behind TwiML
187+
# wrapper) was never validated and the migration certified clean.
188+
# A generic .xml stays exempt - a pom.xml or an Android layout genuinely
189+
# is not ours to judge.
190+
case "${file,,}" in
191+
*.twiml|*.texml) TEXML_INVALID_ROOTS+=("$file:${root:-<no element>}") ;;
192+
esac
181193
fi
182194
done < <(find "$PROJECT_ROOT" \
183195
\( -name node_modules -o -name .git -o -name vendor -o -name __pycache__ \
184196
-o -name venv -o -name .venv -o -name dist -o -name build \
185197
-o -name .next -o -name .nuxt -o -name coverage -o -name .tox \) -prune \
186198
-o \( -iname "*.xml" -o -iname "*.twiml" -o -iname "*.texml" \) -print0 2>/dev/null)
187199

188-
if [ ${#TEXML_FILES[@]} -eq 0 ]; then
200+
# Reported BEFORE the "no documents found" branch: a tree whose only TeXML
201+
# files all have broken roots would otherwise print "none found - skipping"
202+
# and pass, which is the silent certification this check exists to prevent.
203+
if [ ${#TEXML_INVALID_ROOTS[@]} -gt 0 ]; then
204+
for entry in "${TEXML_INVALID_ROOTS[@]}"; do
205+
echo -e " ${RED}FAIL${NC} $(basename "${entry%:*}") — root element is <${entry##*:}>, expected <Response>"
206+
done
207+
RESULTS="${RESULTS}texml:fail,"
208+
elif [ ${#TEXML_FILES[@]} -eq 0 ]; then
189209
echo -e " ${BLUE}INFO${NC} No TeXML documents found — skipping TeXML validation"
190210
RESULTS="${RESULTS}texml:skip,"
191211
else

providers/claude/plugins/telnyx-platform/skills/telnyx-twilio-migration/scripts/scan-twilio-usage.sh

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,14 @@ SDK_PATTERNS=(
318318
# Java
319319
'import com.twilio'
320320
'com.twilio.'
321-
# PHP
322-
'use Twilio\\'
323-
'Twilio\\'
321+
# PHP. ONE backslash: this array is searched with `grep -F` (fixed string),
322+
# where '\\' means two LITERAL backslashes and matches nothing real - PHP
323+
# namespaces are written `use Twilio\Rest\Client`. Every PHP Twilio project
324+
# therefore scanned as zero files and the whole language passed discovery
325+
# silently. The doubling reads as correct next to the regex patterns below,
326+
# which is why it survived: only the -F consumer makes it wrong.
327+
'use Twilio\'
328+
'Twilio\'
324329
# C# / .NET
325330
'using Twilio'
326331
'Twilio.'

0 commit comments

Comments
 (0)