Repository navigation
Phone security: STIR/SHAKEN caller ID verification - #49
Merged
Merged
Conversation
Read StirVerstat from Twilio webhook and pass it through to the WebSocket handler. If a caller's number matches OWNER_NUMBER but StirVerstat is not TN-Validation-Passed-A (highest attestation), downgrade from owner to verified-only — blocking full computer access for potentially spoofed calls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sonichi
approved these changes
Mar 30, 2026
sonichi
left a comment
Owner
There was a problem hiding this comment.
Clean security improvement. STIR/SHAKEN caller ID verification prevents spoofed numbers from getting owner-level access.
Key points:
- Reads
StirVerstatfrom Twilio's form data (passed through STIR/SHAKEN) - If owner number matches but StirVerstat is NOT
TN-Validation-Passed-A, downgrades to verified (not owner) - Logs the downgrade for audit
- Passes StirVerstat through WebSocket params for downstream use
- No-op when StirVerstat is absent (graceful degradation for carriers that don't support it)
One note: STIR/SHAKEN is US-only. International calls won't have StirVerstat, so !stirVerstat path correctly falls through to number match. Good.
LGTM.
This was referenced May 24, 2026
sonichi
added a commit
that referenced
this pull request
May 25, 2026
#50/#51) (#1114) Three open CodeQL `js/stack-trace-exposure` warnings on main shipped `err.message`/`String(err)` directly to HTTP clients: - `src/web-client.ts:3835` — `/paidsubscriptions/data` (LAN-reachable when HTTP_HOST=0.0.0.0) - `src/web-client.ts:3874` — `/paidsubscriptions/scan` (localhost-only, but still LAN-visible via the LocalhostOnly check failure path) - `src/vision-tools.ts:583` — `/vision/*` control endpoints (127.0.0.1-bound but flagged by CodeQL's static dataflow) The pattern was `error: e?.message || String(e)` (web-client) / `error: (err as Error)?.message ?? String(err)` (vision-tools) in 6 catch sites total: 2 in web-client, 4 in vision-tools (lines 341/420/492/535 — all source the data that flowed into the line-583 respond() sink). Fix preserves diagnostic value via server-side `console.error(...)` of the full error, returns a short generic label (`'failed to read subscriptions'`, `'submitFrame failed'`, etc.) to the response body. Callers (Gemini tool consumers, the /paidsubscriptions UI) already branch on status code + ok-flag — they don't depend on the specific error text. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
sonichi
added a commit
that referenced
this pull request
May 25, 2026
closes #1119) (#1121) PR #1114 fixed 6 catch sites flagged by CodeQL `js/stack-trace-exposure` warnings #49 / #50 / #51. Lucy's post-merge audit caught 3 sibling sites with the same exposure pattern that CodeQL didn't flag (different upstream flow-analysis paths). Same surgical pattern as #1114: server-side `console.error(...)` + generic body label. ## Sites changed | Line | Endpoint | Pattern | |---|---|---| | 3744 | `/note-viewing` (POST) | `e instanceof Error ? e.message : 'parse failed'` → generic `'note-viewing parse failed'` | | 3806 | overlay control proxy | `'overlay control proxy failed: ' + e.message` → generic `'overlay control proxy failed'` | | 3823 | `/paidsubscriptions` (HTML) | `'Error reading subscriptions: ' + (e?.message || String(e))` → generic `'Error reading subscriptions'` | Each adds a `console.error('[web-client] /endpoint failed:', e)` server-side log so diagnostic info is preserved. ## Why these and not others Audited the file for `e.message`/`err.message` patterns; remaining occurrences (lines 1492, 1704, 2119, 2799, 3205, 3456, 3474) are all client-side JavaScript (browser-side dbg / status text) or stored in private state (`_parse_error` in the local data object, never sent in a response). Only the 3 above leaked through `res.end(...)` to a network response. ## Closes - Closes #1119 ## Test plan - [x] `npx tsc --noEmit -p .` clean - [ ] CodeQL re-scan after merge should report no new warnings for these sites (they weren't flagged before either, but the scrub closes them defensively) - [ ] Manual: trigger each endpoint failure → confirm response body is generic + full error in stderr 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219
added a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
Read StirVerstat from Twilio webhook and pass it through to the WebSocket handler. If a caller's number matches OWNER_NUMBER but StirVerstat is not TN-Validation-Passed-A (highest attestation), downgrade from owner to verified-only — blocking full computer access for potentially spoofed calls. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
liususan091219
added a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…onichi#49/sonichi#50/sonichi#51) (sonichi#1114) Three open CodeQL `js/stack-trace-exposure` warnings on main shipped `err.message`/`String(err)` directly to HTTP clients: - `src/web-client.ts:3835` — `/paidsubscriptions/data` (LAN-reachable when HTTP_HOST=0.0.0.0) - `src/web-client.ts:3874` — `/paidsubscriptions/scan` (localhost-only, but still LAN-visible via the LocalhostOnly check failure path) - `src/vision-tools.ts:583` — `/vision/*` control endpoints (127.0.0.1-bound but flagged by CodeQL's static dataflow) The pattern was `error: e?.message || String(e)` (web-client) / `error: (err as Error)?.message ?? String(err)` (vision-tools) in 6 catch sites total: 2 in web-client, 4 in vision-tools (lines 341/420/492/535 — all source the data that flowed into the line-583 respond() sink). Fix preserves diagnostic value via server-side `console.error(...)` of the full error, returns a short generic label (`'failed to read subscriptions'`, `'submitFrame failed'`, etc.) to the response body. Callers (Gemini tool consumers, the /paidsubscriptions UI) already branch on status code + ok-flag — they don't depend on the specific error text. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
closes sonichi#1119) (sonichi#1121) PR sonichi#1114 fixed 6 catch sites flagged by CodeQL `js/stack-trace-exposure` warnings sonichi#49 / sonichi#50 / sonichi#51. Lucy's post-merge audit caught 3 sibling sites with the same exposure pattern that CodeQL didn't flag (different upstream flow-analysis paths). Same surgical pattern as sonichi#1114: server-side `console.error(...)` + generic body label. ## Sites changed | Line | Endpoint | Pattern | |---|---|---| | 3744 | `/note-viewing` (POST) | `e instanceof Error ? e.message : 'parse failed'` → generic `'note-viewing parse failed'` | | 3806 | overlay control proxy | `'overlay control proxy failed: ' + e.message` → generic `'overlay control proxy failed'` | | 3823 | `/paidsubscriptions` (HTML) | `'Error reading subscriptions: ' + (e?.message || String(e))` → generic `'Error reading subscriptions'` | Each adds a `console.error('[web-client] /endpoint failed:', e)` server-side log so diagnostic info is preserved. ## Why these and not others Audited the file for `e.message`/`err.message` patterns; remaining occurrences (lines 1492, 1704, 2119, 2799, 3205, 3456, 3474) are all client-side JavaScript (browser-side dbg / status text) or stored in private state (`_parse_error` in the local data object, never sent in a response). Only the 3 above leaked through `res.end(...)` to a network response. ## Closes - Closes sonichi#1119 ## Test plan - [x] `npx tsc --noEmit -p .` clean - [ ] CodeQL re-scan after merge should report no new warnings for these sites (they weren't flagged before either, but the scrub closes them defensively) - [ ] Manual: trigger each endpoint failure → confirm response body is generic + full error in stderr 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Oct 1, 2026
…t-scope's docstring My own grep for leftover provenance used a #49[0-9][0-9] pattern, which never matched #4502 -- missed by Pro-dev's independent check, not mine. The pre-existing #4502/#4503 reference at the top of the file (predating this PR) is untouched; only the one this PR itself added to run()'s docstring is trimmed. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qingyun-wu
pushed a commit
that referenced
this pull request
Oct 4, 2026
…s cleared, not trusted (#4976) * fix(watcher): a foreign SUTANDO_INSTANCE_ID on the core's own inbox is cleared, not trusted A --force-restart (or any start) issued from inside another instance's own session inherits that instance's SUTANDO_INSTANCE_ID, even when --inbox explicitly names the core's own canonical <ws>/tasks. The watcher then watches the right directory while believing itself to be that OTHER worker's own inbox-watcher (#4502's rule: a worker's own inbox IS the routing decision) -- it never loads the core's task-event-handler config and silently stops dispatching anything it sees, with no error anywhere. Observed in production 2026-09-30, twice: pid 1184 (18:46) and its replacement pid 66624 (19:16), both with argv correctly reading `--role session --inbox <core's workspace>/tasks` but env carrying a different worker's SUTANDO_INSTANCE_ID, inherited from the shell that force-restarted them. Fix is deliberately narrow: only clears an explicitly-set SUTANDO_INSTANCE_ID when the inbox being served is the core's own canonical tasks/ dir (the same basename=="tasks" signal the existing identity-match gate already uses). It never touches a <ws>/deliveries/<id> inbox -- there, the pre-existing identity-match check already decides whether a mismatched starter must yield, and correcting the env there would make a wrong starter's identity agree with the inbox by fiat, defeating that check (this was my first, too-broad attempt; tests/watch-tasks-stream-unready-holder-restamp.test.sh scenario (d) and tests/watch-tasks-stream-inbox-and-workspace-env.test.py caught it). It also never invents an identity when SUTANDO_INSTANCE_ID was never set, since several tests legitimately run a core against a deliveries/-shaped inbox with no ambient identity at all. tests/watch-tasks-stream-config-hot-reload.test.py's worker case previously set SUTANDO_INSTANCE_ID="worker-1" while watching the bare <ws>/tasks dir -- a combination that cannot occur in production. Updated it to a realistic <ws>/deliveries/worker-1 inbox, which is what makes it a test of "a worker", not of "SUTANDO_INSTANCE_ID happens to be set". New tests/watch-tasks-stream-foreign-instance-id-corrected.test.py pins the fix directly: a foreign SUTANDO_INSTANCE_ID on the core's own inbox is corrected (an already-present handler still runs, and the correction is logged); control run against unmodified main fails cleanly, reproducing the incident exactly (TASK_FILE emitted, handler never runs). Full watch-tasks-stream*/watcher-* suite (29 files) re-run green except the pre-existing watch-tasks-stream-fallback-receipt-scope flake (confirmed failing identically on unmodified main; tracked at #4645) -- unrelated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * style: trim comment blocks to the repo's 2-line cap review-checks' prose-cap caught three blocks over the cap (two in the hot-reload test, one in the new regression test) and the main fix's own .sh comment, which the Python-only scanner doesn't cover but the same policy applies to. The narrative moved to the PR body; code comments keep only the constraint and a pointer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(test): realistic deliveries/<id> inbox for the fallback-receipt-scope worker cases Same gap as the hot-reload test fix: three of this file's six properties set SUTANDO_INSTANCE_ID="worker-1" while watching the bare <ws>/tasks dir — a combination that cannot occur in production and that the watcher's new foreign-identity correction (this branch's main fix) now treats as a mismatch on the core's own inbox, clearing it and breaking all three "a worker bypasses unconditionally" assertions. Caught by CI on this PR, not locally: my earlier local re-run of this file only surfaced the one already-known flake (#4645) in a truncated tail of the output and I didn't look past it — the full output showed 2/6 passed, not 5/6, meaning three genuine regressions were sitting unnoticed. CI's full log is what caught it. Fix: `run()` now takes the inbox shape from `watcher_instance` itself — <ws>/deliveries/<id> when set, <ws>/tasks when not — matching production and the hot-reload test's same fix. Back to 5/6 passed, identical to unmodified main (the one remaining failure is #4645, confirmed unrelated). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(watcher): clear the FULL inherited worker routing env, by realpath not basename kewei's PR review on this branch's prior head found two blockers: 1. Clearing only SUTANDO_INSTANCE_ID left SUTANDO_INBOX_KIND/ SUTANDO_INBOX_RESOLVER/SUTANDO_INBOX_RESOLVER_TIMEOUT/ SUTANDO_POOL_DELIVERY_SCRIPT behind -- a worker-shaped SUTANDO_INBOX_RESOLVER still runs on every entry at the core's inbox, rejects every plain task body (it expects a worker sentinel), and dispatch_task's 3-attempt retry loop gives up silently. Now the whole inherited worker-routing block is cleared together. 2. The canonical-inbox check compared basenames ("tasks" == "tasks"), which can misclassify a worker's own inbox if its path happens to end in .../tasks. Now it compares the inbox's realpath against <resolved workspace>/tasks, matching tasks-dir-resolve.sh's own notion of the core's canonical inbox. tests/watch-tasks-stream-foreign-instance-id-corrected.test.py grows two properties for these: (3) a full inherited worker env at the core's canonical inbox (incl. a real SUTANDO_INBOX_RESOLVER) still dispatches; (4)/(4b) a worker's own inbox whose basename happens to be "tasks" keeps its identity. 5/5 pass. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(watcher): kewei's 2nd review — realpath in the re-stamp path too, fix a weak test assertion Two more findings from kewei's formal review at f31f10e: 1. The unready-holder re-stamp path (around line 215) still decided "empty instance id means core" by `basename(inbox) == "tasks"`, even though the dispatch-time check a few lines above it was already switched to a realpath comparison. A `<ws>/tasks` symlinked to a physically-named directory (e.g. `physical-inbox`) has `basename(TASKS_DIR_ABS) == "physical-inbox"`, so the re-stamp silently never fired for a holder on a symlinked core inbox. Now reuses the same `CANONICAL_CORE_TASKS_DIR` realpath comparison already computed earlier in the file. New case (e) in tests/watch-tasks-stream-unready-holder-restamp.test.sh pins this: a symlinked core inbox re-stamps exactly like a physical one — confirmed to fail on the basename-only version and pass on the fix. 2. tests/watch-tasks-stream-foreign-instance-id-corrected.test.py property (3) asserted `"handle" in log`, but the log is shared across properties and property (1) already wrote "handle" to it — so (3) passed trivially even when its own task (task-full-env.txt) was never dispatched. The test handler now records which task file it actually ran for, and (1)/(3) each assert their own task by name. Confirmed: at the pre-Blocker-1-fix head (68e63b6) this version of (3) now correctly FAILS (it previously passed spuriously). 5/5 on tests/watch-tasks-stream-foreign-instance-id-corrected.test.py, all green on tests/watch-tasks-stream-unready-holder-restamp.test.sh. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(watcher): kewei's 3rd review — don't gate the re-stamp arm on inbox location; trim comment provenance Blocker 1: the re-stamp arm's "empty SUTANDO_INSTANCE_ID means core" check was changed in the prior commit to also require the inbox to equal the canonical <workspace>/tasks by realpath. That regressed a SUPPORTED configuration: a core watching a custom inbox with its own SUTANDO_WORKSPACE_DIR and no instance id (pinned by tests/watch-tasks-stream-inbox-and-workspace-env.test.py). The re-stamp arm's question is "is this seat the core?", which an empty instance id already answers on its own -- the inbox-location check belongs only to the earlier env-clearing block (deciding whether an INHERITED id is foreign), not here, where `sentinel-names-pid` already scopes the write to this seat's own sentinel. Dropped the inbox condition from the empty-id arm entirely. New case (f) in watch-tasks-stream-unready-holder-restamp.test.sh pins a custom inbox + separate workspace re-stamping correctly; confirmed to fail against the prior commit's realpath-gated version and pass on the fix. Blocker 3: trimmed comment references to the reviewer, review round, and issue numbers in tests/watch-tasks-stream-{config-hot-reload, foreign-instance-id-corrected,unready-holder-restamp}, keeping only the behavioral constraint each comment exists to state. Updated the foreign-instance-id-corrected module docstring, which still described two properties, to describe all five. Blocker 2 (the shipped-service live witness) is being run separately against this head once pushed. 5/5 on the corrected-id test, all green (including new case (f)) on the re-stamp test. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * style(test): trim remaining provenance from the foreign-instance-id-corrected docstring The module docstring still carried an issue number, a date and a pid in its module docstring, matching exactly what AGENTS.md's comment rule forbids. Kept the behavioral statement (a worker's own inbox is the routing decision), dropped the issue number, date and pid. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * style(test): trim the issue-number reference added to fallback-receipt-scope's docstring My own grep for leftover provenance used a #49[0-9][0-9] pattern, which never matched #4502 -- missed by Pro-dev's independent check, not mine. The pre-existing #4502/#4503 reference at the top of the file (predating this PR) is untouched; only the one this PR itself added to run()'s docstring is trimmed. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * style(test): condense case (f)'s comment to AGENTS.md's 2-line cap The prose-cap scanner only covers Python, so this .sh comment (4 lines) slipped through CI green while still violating AGENTS.md's comment rule. Kewei caught it by eye. Condensed to 2 lines, same constraint kept (empty-id-alone for a custom inbox + separate workspace). src/watch-tasks-stream.sh is untouched (blob 8549837, unchanged since 09df259) -- test comment + PR body only, no source change, so the shipped-service live witness need not be repeated. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
StirVerstatfrom Twilio webhook and pass through to WebSocket handlerOWNER_NUMBERbut StirVerstat is notTN-Validation-Passed-A, downgrade from owner to verified-onlyTN-Validation-Passed-A, curl tests confirm pass-throughTest plan
🤖 Generated with Claude Code