Repository navigation
Inline meeting ID lookup + 3-tier phone access control - #30
Conversation
937553a to
1bdbadc
Compare
- call_contact: searches macOS Contacts by name, dials directly (bypasses task bridge) - lookup_meeting_id: returns Zoom meeting ID + passcode from env - Phone agent: stricter hang_up (wait for caller goodbye), "How to think" section, inline tools list for verified callers, GitHub repo URL in instructions - Context injection for child calls - Add PERSONAL_CLAUDE.md.example template, gitignore PERSONAL_CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1bdbadc to
3ebfc5e
Compare
sonichi
left a comment
There was a problem hiding this comment.
Reviewed the updated PR. Looks good overall — clean diff after the rebase.
Approve:
lookup_meeting_id— clean, reads env vars, returns ID + passcodecall_contact— good Contact search, handles multiple matches, auto-calls single match- "How to think" instructions — useful guidance for phone agent decision-making
- Inline tools list injection for verified callers
- Stricter
hang_up— waits for caller's explicit goodbye, prevents premature disconnects PERSONAL_CLAUDE.md.example— nice per-user rules template
One question: Child call context injection (line 295) re-adds getSafeContext(). We removed this on main per owner feedback ("only context needed for the outbound call should be provided"). The purpose string from core already contains the needed info. Should we keep or drop the broader context for child calls?
Otherwise ready to merge.
15fd056 to
ac0ac2a
Compare
a3194ee to
457a934
Compare
These belong with the personal overrides PR, not the inline tools PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove getSafeContext() from child calls — the purpose string from core already carries the needed info. Avoids leaking broader context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
457a934 to
654f287
Compare
Owner (OWNER_NUMBER): work tool + all inline tools (full computer access) Verified (VERIFIED_CALLERS, not owner): limited tools (volume, brightness, time, meeting ID lookup). No task delegation or screen control. Unverified: conversation + hang_up only. Access determined by caller/callee phone number, not call type. Removes child-call-based inheritance of verification status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unverified callers now get anyCallerTools too, not just hang_up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gemini sometimes passes 0.8 instead of 80 for "80%". Detect and convert fractional values to the expected 0-100 range. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
xlsx file for manual testing of 3-tier access control (owner/verified/unverified). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verified test matrix for 3-tier access: owner, verified, unverified. Located at tests/manual/ for contributor test assignments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip +, -, spaces and prepend country code for 10-digit numbers. Fixes: child call to 6464706470 not matching +16464706470 in VERIFIED_CALLERS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
import 'dotenv/config' skips vars already in the shell environment. With override: true, .env always takes precedence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Child call instructions were telling the agent to decline all lookups. Now dynamically lists available tools based on callee's access tier. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Marked items tested during this session's phone calls. Remaining items unchecked for future testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Template at tests/manual/phone-access-control.md.example with empty tables. Agents fill in phone-access-control.md locally (gitignored) with test results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One table with all 3 tiers as columns instead of 3 separate tables. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explains how each user can verify their own security settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users make calls from each tier, then ask their agent to read transcripts and auto-fill the test table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dropped it. The purpose string from core already carries the needed info, and broader context shouldn't leak to child calls — especially now with 3-tier access control where the callee may not be the owner. Also added 3-tier phone access control in this PR:
Key changes: Partially tested — owner summon/call_contact/work, verified lookup_meeting_id, unverified volume/scroll-blocked all verified via phone calls. |
sonichi
left a comment
There was a problem hiding this comment.
Re-review — PR #30 with 3-tier access control
Changes look clean. Key additions:
- OWNER_NUMBER env var ✅ — separates owner from verified callers. Good.
- 3-tier tool access ✅ —
ownerOnlyTools,anyCallerTools,configurableToolsexports from inline-tools.ts. Clean separation. - Dropped child call context injection ✅ — per earlier discussion.
- Security test template ✅ —
tests/security/phone-access-control.md.example - dotenv override — changed to
dotenv.config({ override: true }). Why override? This means env vars set before the script (e.g., from startup.sh) get overridden by .env. Could cause issues if startup.sh sets values intentionally. - .env.example updated with clear 3-tier documentation ✅
One question: the dotenv({ override: true }) change — is this intentional? The rest of the codebase uses import 'dotenv/config' which doesn't override.
Otherwise LGTM — ready to merge.
override: true was a workaround for stale shell env during debugging. Not needed in production — .env should be the source of truth and startup.sh may set vars intentionally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverted — back to |
sonichi
left a comment
There was a problem hiding this comment.
All feedback addressed — context injection dropped, dotenv reverted. 3-tier phone access control looks solid. LGTM, ready to merge.
* Add inline call_contact + lookup_meeting_id tools, improve phone agent - call_contact: searches macOS Contacts by name, dials directly (bypasses task bridge) - lookup_meeting_id: returns Zoom meeting ID + passcode from env - Phone agent: stricter hang_up (wait for caller goodbye), "How to think" section, inline tools list for verified callers, GitHub repo URL in instructions - Context injection for child calls - Add PERSONAL_CLAUDE.md.example template, gitignore PERSONAL_CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Move .gitignore and PERSONAL_CLAUDE.md.example to PR sonichi#37 These belong with the personal overrides PR, not the inline tools PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Drop child call context injection per review Remove getSafeContext() from child calls — the purpose string from core already carries the needed info. Avoids leaking broader context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add 3-tier phone access control: owner, verified, unverified Owner (OWNER_NUMBER): work tool + all inline tools (full computer access) Verified (VERIFIED_CALLERS, not owner): limited tools (volume, brightness, time, meeting ID lookup). No task delegation or screen control. Unverified: conversation + hang_up only. Access determined by caller/callee phone number, not call type. Removes child-call-based inheritance of verification status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Give any-caller tools (volume, brightness, time) to all callers Unverified callers now get anyCallerTools too, not just hang_up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix volume/brightness: normalize 0-1 values from Gemini to 0-100 Gemini sometimes passes 0.8 instead of 80 for "80%". Detect and convert fractional values to the expected 0-100 range. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Move toggle_tasks to owner-only tools Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add phone access control test spreadsheet xlsx file for manual testing of 3-tier access control (owner/verified/unverified). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add manual test checklist for phone access control Verified test matrix for 3-tier access: owner, verified, unverified. Located at tests/manual/ for contributor test assignments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalize phone numbers for access control matching Strip +, -, spaces and prepend country code for 10-digit numbers. Fixes: child call to 6464706470 not matching +16464706470 in VERIFIED_CALLERS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use dotenv override to always read fresh .env values import 'dotenv/config' skips vars already in the shell environment. With override: true, .env always takes precedence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Tell child calls about their available tools Child call instructions were telling the agent to decline all lookups. Now dynamically lists available tools based on callee's access tier. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update test checklist with manually verified items Marked items tested during this session's phone calls. Remaining items unchecked for future testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add phone access control test template, gitignore results Template at tests/manual/phone-access-control.md.example with empty tables. Agents fill in phone-access-control.md locally (gitignored) with test results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Consolidate test template into single table One table with all 3 tiers as columns instead of 3 separate tables. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add usage instructions to access control test template Explains how each user can verify their own security settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update test instructions: make 3 calls, agent fills in results Users make calls from each tier, then ask their agent to read transcripts and auto-fill the test table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Rename tests/manual to tests/security Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove tracked phone-access-control.md (now gitignored) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert dotenv override — use standard import 'dotenv/config' override: true was a workaround for stale shell env during debugging. Not needed in production — .env should be the source of truth and startup.sh may set vars intentionally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… surfaces) (#3131) Picks up the five merged bodhi PRs the voice context-budget design depends on (sonichi/bodhi_realtime_agent #33, #34, #31, #30, #32): - upstream send counters + getDiagnostics() (attempted/queued/skipped/threw, split raw/wire bytes, per-outcome timestamps; reset per transport generation) - typed connection-lifecycle events (attempt / setup-ok / setup-failed / attempt-close / generation-close, correlated by connectAttemptId) - onUsageMetadata (promptTokenCount et al), reachable from VoiceSession - mediaResolution passthrough (session-wide media token cost) - optional compressionConfig thresholds ({} = server defaults), sent as the int64-over-JSON strings the API expects Plus review hardening from the merge train: observer isolation on both new callback surfaces, reconnect replay counted AND moved off the deprecated `media` field onto the supported video/audio slots, and superseded-dial setup-failed fenced like every other stale-dial signal. Pin only; no sutando code change. Consumers land separately (Phase 1: ledger sampling, logicalSessionId, up=/ctx= health segments, shadow matrix). Evidence: Full TS suite at this pin: 1096 pass / 0 fail / 2 skipped. Live round trip through the PINNED artifact (not the SDK directly) against gemini-2.5-flash-native-audio-preview-12-2025 — connect with mediaResolution=LOW + compressionConfig:{} accepted; text in -> model audio out; every new surface read while doing it: connected in 304ms; lifecycle so far: attempt -> setup-ok text slot : {"attempted":1,"queued":1,"queuedRawBytes":26} generation : 1 audio chunks : 1 (first after 1442ms) usage samples : 1 promptTokenCount 375 -> 375 lifecycle full: attempt -> setup-ok -> generation-close LIVE ROUND TRIP: PASS Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ow matrix (#3132) * chore(deps): bump bodhi pin ca0f61de -> 0d506ead (B1-B5 observability surfaces) Picks up the five merged bodhi PRs the voice context-budget design depends on (sonichi/bodhi_realtime_agent #33, #34, #31, #30, #32): - upstream send counters + getDiagnostics() (attempted/queued/skipped/threw, split raw/wire bytes, per-outcome timestamps; reset per transport generation) - typed connection-lifecycle events (attempt / setup-ok / setup-failed / attempt-close / generation-close, correlated by connectAttemptId) - onUsageMetadata (promptTokenCount et al), reachable from VoiceSession - mediaResolution passthrough (session-wide media token cost) - optional compressionConfig thresholds ({} = server defaults), sent as the int64-over-JSON strings the API expects Plus review hardening from the merge train: observer isolation on both new callback surfaces, reconnect replay counted AND moved off the deprecated `media` field onto the supported video/audio slots, and superseded-dial setup-failed fenced like every other stale-dial signal. Pin only; no sutando code change. Consumers land separately (Phase 1: ledger sampling, logicalSessionId, up=/ctx= health segments, shadow matrix). Evidence: Full TS suite at this pin: 1096 pass / 0 fail / 2 skipped. Live round trip through the PINNED artifact (not the SDK directly) against gemini-2.5-flash-native-audio-preview-12-2025 — connect with mediaResolution=LOW + compressionConfig:{} accepted; text in -> model audio out; every new surface read while doing it: connected in 304ms; lifecycle so far: attempt -> setup-ok text slot : {"attempted":1,"queued":1,"queuedRawBytes":26} generation : 1 audio chunks : 1 (first after 1442ms) usage samples : 1 promptTokenCount 375 -> 375 lifecycle full: attempt -> setup-ok -> generation-close LIVE ROUND TRIP: PASS Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(voice): P7 Tranche B — upstream ledger, lineage, ctx gauge, shadow matrix The health matrix's most common verdict on 2026-08-17 was insufficient-evidence (speech-without-model-event; upstream-hop-unobserved) — 28 times in one day. The right verdict, and a useless one: up=n/a on every line because the agent→Gemini hop had no counters. The bumped bodhi pin exposes them; this wires the sutando half, dark. Ledger (voice-audio-health.ts): - coverage becomes an ordered LedgerCoverage level with observesUpstreamSend(); the live snapshot stays 'session-only'. - samples bodhi's VoiceSession.getDiagnostics() on ledger ticks only (never the frame path; §D7.0b untouched). Absent or throwing ⇒ up=n/a — null means unobserved, never zero. - [Health] renders up={aQ/s,aKB/s,vQ/s,vKB/s,drop,buf:n/a} from per-tick deltas; a generation change renders that tick from zero rather than a negative rate. ctx={tok,age} appears once usage metadata is seen. - logicalSessionId/lineageState derived sutando-side from lifecycle events (design §1.1): minted on a handle-less setup-ok; resumed keeps it; suspected-sever (setup-failed with a handle, or handle-accepted 1008 close) is TERMINAL — only a fresh lineage exits it, by minting. Context occupancy belongs to the lineage and resets with it. - persisted rows (full and reduced) carry every evaluator input: counters, per-outcome timestamps, generation, lineage, contextTokens, echoSuppressed. Matrix (voice-health-matrix.ts): - structural guard BEFORE every delta row: a transport-generation change inside the window is insufficient-evidence — upstream counters reset per generation, so row 5's timestamps are as unusable across it as row 4's deltas. - row 4 upstream-send-dead: eligible ingress, zero attempts — a dead call path. Row 4b upstream-send-failing: attempts, zero queued, named by guard outcome. Both exclude echo-suppressed windows (suppression returns before sendAudio and would masquerade as a dead path). - row 5 renamed model-silent → post-sdk-silent, reachable only through the lossless-window proof: dAttempted >= dDelivered ∧ dQueued == dAttempted ∧ lastQueuedAt >= speech.lastAboveFloorAt. Aggregate counters cannot say WHICH frame queued; losslessness proves the speech-bearing ones necessarily did. A lossy window stays insufficient-evidence — at aggregate resolution it genuinely cannot distinguish "model went quiet" from "we dropped the speech". voice-agent.ts: - onUsageMetadata/onConnectionLifecycle wired into the VoiceSession config; getSessionDiagnostics injected into the ledger. - a SHADOW evaluator runs the 'session+egress' ruleset every health tick and logs [Matrix-shadow] on verdict change, telemetry-only. Canary 3a later compares live vs shadow on identical input; without the shadow, 3a has no baseline. The live verdict stream is byte-for-byte unchanged. - the [Health] line now carries vision={sent,defB,defG,disp} while streaming — whether the vision throttle engaged was previously unknowable from the log (deferredBudget existed but was never surfaced; investigation F2). Tests: 7 new ledger cases (lineage state machine incl. terminal suspected-sever, ctx latch+reset, up= rendering, generation-reset rendering, n/a on unobserved/throwing, replayable persisted row) and 8 new/updated matrix cases (rows 4/4b, structural guard, lossless/lossy/order-unproven, echo exclusion, live-at-session-only unchanged, row-5 rename). Full suite: 1109 pass / 0 fail / 2 skipped. tsc clean. prose-cap PASS. Stacked on voice/bodhi-pin-b1-b5 (#3131); merge order pin → this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(voice): P7 Tranche B round 2 — seven codex findings on #3132, all confirmed - Matrix: an unobserved→observed diagnostics transition could FABRICATE post-sdk-silent — the generation guard fired only when both generations were non-null, while unobserved counters baselined as placeholder zeros, so generation-lifetime totals diffed as one window (the review repro'd it). Upstream deltas now require a valid window: observed baseline, same generation. Rows 4/4b gate on it; row 5 downgrades to insufficient-evidence 'upstream-baseline-unobserved'. - Matrix: MatrixResult gains the §1.5 facts — attemptedAudioAdvanced, queuedAudioAdvanced, losslessWindowWithSpeechQueued, transportGenerationChanged, echoSuppressedAdvanced — computed before any row can return, so every result carries an honest set. The watchdog integration reads facts, never verdict strings; verdict-independence needs them to exist. - Matrix: row 4b named the guard outcome from generation-lifetime totals, letting earlier skips masquerade as the current failure mode. The baseline now carries skippedNoSession/threw; reasons are window deltas. - Ledger: up= rendered cumulative totals over one dt after a missed diagnostics sample (900 queues → "30.0/s" out of thin air). A tick with no adjacent observed baseline renders up=n/a(baselining) and establishes the window; rates resume next tick. A generation change WITH an adjacent baseline still renders from zero — the new generation's counters span at most the window. - Ledger: §1.1's generation→lineage reconciliation records now exist — every lineage commit/re-label persists a reason:'lineage' row carrying connectAttemptId, transportGeneration, resolved logicalSessionId, final state, so offline analysis re-attributes earlier rows instead of trusting a stale in-row id. A failed handle-less dial commits nothing and emits nothing. - Ledger: the reduced persistence row was NOT replayable — flattened upstream fields, missing speech/heartbeat/clientTotals/lastModelEventAt. It now carries every matrix-evaluator input SNAPSHOT-SHAPED (§1.6 option 1) plus the recorded decision: verdict, facts, reasons (§1.6 option 2). A row still over budget strips only the episode list and says so. Replay is pinned: the evaluator produces an identical baseline, verdict, and facts from the full and reduced forms of the same tick. - voice-agent: promptTokensDetails now reaches the ledger and both persistence payloads — §1.4's modality split is what separates "video filled the context" from "conversation did". Held and reset per lineage like the scalar it details. Tests: 9 new/extended cases pin the two review repros (null→observed fabrication, missed-sample rate), 4b window-delta naming, facts honesty at every coverage, reconciliation records, the modality breakdown, and full/reduced replay equivalence. Full suite green; tsc clean; eslint 0 errors (14 pre-existing warnings untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(voice): matrix facts are data-level — coverage- and verdict-independent Codex round 3, two findings on the round-2 commit, both confirmed: 1. Facts depended on coverage and on the verdict path: identical diagnostics produced all-false upstream facts under 'session-only' but true facts under 'session+egress', and a CONNECTING tick returned before facts were derived. §1.5's obligation is the opposite — the watchdog reads facts and must behave identically under every coverage mode and verdict, so a future coverage flip must not change watchdog behaviour. Facts are now computed FIRST, from data validity alone (adjacent same-epoch baseline, both generations observed and equal): facts say what the data shows; coverage gates only which VERDICTS may be claimed. The live verdict stream is unchanged — facts never drive verdicts. 2. upstreamWindowValid required only the BASELINE generation to be non-null. A typed sample with counters present but a null CURRENT generation would diff counters across generations and could emit post-sdk-silent. The window now requires both ends observed; the row-5 downgrade reason is renamed 'upstream-baseline-unobserved' → 'upstream-window-unobserved' since it covers both directions. Tests: the session-only facts test inverted into the §1.5 pin (same input, same facts, different claim across coverage modes); observed→null direction pinned; a new verdict-independence case proves an early reconnect-window return carries the same facts as the ACTIVE evaluation of identical input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(voice): name why the clamp cannot hide a silent reset sonichi asked whether a counter reset without a generation bump would render aQ/s:0.0 indistinguishably from 'the agent stopped sending'. It cannot: at the pinned bodhi commit the reset and the generation bump are adjacent lines in one synchronous block, and each is the only such site in the file. Recording the commit so the next reader re-checks the coupling instead of re-deriving it. * docs(voice): the try/catch guards the TDZ, not the typeof qingyun-wu's review nit, verified: typeof on a let/const inside its temporal dead zone throws ReferenceError rather than yielding 'undefined', so the typeof check cannot be what makes a tick racing construction safe. Behaviour was already correct — only the comment credited the wrong mechanism. * fix(voice): an unobserved upstream baseline must not read as a real zero john-the-dev's review finding. makeBaseline collapsed the four upstream counters to 0 when `upstream` was absent, while upstreamWindowObserved inferred the previous tick's observedness from transportGeneration being non-null. Those are independent fields off the same diag, so a baseline with a real generation and absent counters passed the guard and the next tick diffed against placeholder zeros — turning lifetime totals into a "window" and letting losslessWindowWithSpeechQueued go true on a window that was never measured. The counters are now `number | null` with no `?? 0`, and the guard checks the counter itself rather than using generation-nullness as a proxy. Unobserved is now unrepresentable as zero instead of relying on two fields moving together. Defence-in-depth at the current pin, not a live bug: bodhi's getDiagnostics (0d506ead:575) returns upstream and transportGeneration unconditionally, and `upstream` is always an object — so the two cannot diverge today. That is an invariant held in another repo, which is exactly why the guard should not depend on it. Also amends the structural-guard comment: it is gated on observesUpstreamSend, so below session+egress row 5 is protected by its own honesty branch, not by that guard. Control: the added test fails without the fix (39/40) and passes with it (40/40). * fix(voice): echoSuppressed carries the same unobserved-vs-zero defect john-the-dev's follow-up to 2af342d: that fix made the four upstream counters nullable but left echoSuppressed on `?? 0`, which fires when the whole `diag` is null — reachable by construction, since sampleDiagnostics() returns null whenever getSessionDiagnostics is absent or throws, and the option's own doc says "Absent or throwing => upstream stays unobserved". So an unobserved tick recorded 0, and the next observed tick turned a generation-lifetime total into a one-window delta on echoSuppressedAdvanced, which the watchdog reads. Same violation of this module's stated contract ("Null fields mean UNOBSERVED ... never zero"), one field over; it escaped because the nullability lived in the container rather than the field. echoSuppressed is now `number | null` on both SessionDiagnosticsSample and AudioHealthSnapshot, the `?? 0` is gone, and dEchoSuppressed is gated on both ends being observed — the same shape as its siblings. Also drops a dead assertion they caught in the previous regression test: `r.facts.upstreamWindowObserved` is a local const, not a MatrixFacts member, so `?? false` made it unfailable. Replaced with two real members. Control: the added test fails without the fix (40/41) and passes with it (41/41). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
lookup_meeting_idtool (no task bridge delay)OWNER_NUMBER): full access — work tool + all inline toolsVERIFIED_CALLERS): limited tools (volume, brightness, time, lookup_meeting_id)dotenv({ override: true })so .env always takes precedencetests/security/phone-access-control.md.example— each user can make 3 calls (owner/verified/unverified), then ask their agent to read phone transcripts and auto-fill the test resultsTest plan
tests/security/phone-access-control.md.example🤖 Generated with Claude Code