instar 1.3.696 → 1.3.698

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  2. package/dist/config/ConfigDefaults.js +9 -0
  3. package/dist/config/ConfigDefaults.js.map +1 -1
  4. package/dist/core/MessagingToneGate.d.ts +56 -0
  5. package/dist/core/MessagingToneGate.d.ts.map +1 -1
  6. package/dist/core/MessagingToneGate.js +63 -5
  7. package/dist/core/MessagingToneGate.js.map +1 -1
  8. package/dist/core/ask-when-authorized.d.ts +32 -0
  9. package/dist/core/ask-when-authorized.d.ts.map +1 -0
  10. package/dist/core/ask-when-authorized.js +73 -0
  11. package/dist/core/ask-when-authorized.js.map +1 -0
  12. package/dist/core/bias-to-action-telemetry.d.ts +35 -0
  13. package/dist/core/bias-to-action-telemetry.d.ts.map +1 -0
  14. package/dist/core/bias-to-action-telemetry.js +40 -0
  15. package/dist/core/bias-to-action-telemetry.js.map +1 -0
  16. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  17. package/dist/core/devGatedFeatures.js +6 -0
  18. package/dist/core/devGatedFeatures.js.map +1 -1
  19. package/dist/core/standing-authorization.d.ts +72 -0
  20. package/dist/core/standing-authorization.d.ts.map +1 -0
  21. package/dist/core/standing-authorization.js +130 -0
  22. package/dist/core/standing-authorization.js.map +1 -0
  23. package/dist/core/types.d.ts +30 -0
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/core/types.js.map +1 -1
  26. package/dist/messaging/TelegramAdapter.d.ts +12 -0
  27. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  28. package/dist/messaging/TelegramAdapter.js +8 -0
  29. package/dist/messaging/TelegramAdapter.js.map +1 -1
  30. package/dist/server/routes.d.ts.map +1 -1
  31. package/dist/server/routes.js +99 -1
  32. package/dist/server/routes.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/data/builtin-manifest.json +46 -46
  35. package/upgrades/1.3.697.md +56 -0
  36. package/upgrades/1.3.698.md +90 -0
  37. package/upgrades/side-effects/bias-to-action.md +126 -0
  38. package/upgrades/side-effects/tonegate-floor-click-links.md +147 -0
@@ -0,0 +1,126 @@
1
+ # Side-Effects Review — Standing-Authorization signal for B17_FALSE_BLOCKER (bias-to-action)
2
+
3
+ **Version / slug:** `bias-to-action`
4
+ **Date:** `2026-06-29`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent (Phase 5 — REQUIRED: touches the outbound gate authority + a uid/forwarded security path)`
7
+
8
+ ## Summary of the change
9
+
10
+ Feeds the existing `MessagingToneGate` B17_FALSE_BLOCKER rule a **standing-authorization signal** so that "re-asking the operator for an approval they ALREADY granted" is recognized as the false blocker it is — the structural answer to the 2026-06-27 incident where I stopped at "ready for your go-ahead to build" despite a live preapproval. Two cheap, deterministic, deps-injected producers (`detectAskWhenAuthorized` — does the outbound text seek permission?; `resolveStandingAuthorization` — is there a VERIFIED-operator, non-forwarded, in-window grant?) feed the LLM gate, which makes the semantic call (does this grant cover THIS specific, non-FLOOR action?). **Ships OBSERVE-ONLY + dev-gated DARK**: on a dev agent it records a would-fire to `logs/bias-to-action.jsonl` (uid HASH + ask-phrase token, never a raw quote) and changes NO message; the live B17 firing is a separate future `observeOnly:false` operator decision. Files: `src/core/standing-authorization.ts`, `src/core/ask-when-authorized.ts`, `src/core/bias-to-action-telemetry.ts` (new); `src/core/MessagingToneGate.ts` (signal/context fields, render, B17 sub-clause); `src/server/routes.ts` (resolver wiring + observe-only telemetry + lifeline forwarded persistence + context attach); `src/messaging/TelegramAdapter.ts` (LogEntry `forwarded` + both ingress writers); `src/core/types.ts` + `src/config/ConfigDefaults.ts` (config); `src/core/devGatedFeatures.ts` (dev-gate registry); tests + the dark-gate line-map.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `MessagingToneGate` **B17_FALSE_BLOCKER** rule (LLM authority) — **modify** — gains a STANDING-AUTHORIZATION sub-clause: an "asking for approval" message the existing carve-out would exempt becomes a B17 false blocker WHEN a verified grant plausibly covers THIS exact, non-FLOOR action. Judged by meaning; under-fire bias on every uncertainty. Inert until graduated.
15
+ - `detectAskWhenAuthorized` (signal-producer) — **add** — cheap deterministic "is the agent asking permission?" signal. No authority.
16
+ - `resolveStandingAuthorization` (signal-producer) — **add** — deterministic "is there a verified-operator non-forwarded in-window grant?" resolver. Deps-injected; no authority.
17
+ - `bias-to-action-telemetry.buildBiasToActionWouldFire` (observe-only telemetry) — **add** — pure record builder; no authority, no delivery effect.
18
+ - Telegram message-log `forwarded` field (data) — **add** — explicit boolean on both ingress writers; pure data, no decision surface of its own.
19
+
20
+ ---
21
+
22
+ ## 1. Over-block
23
+
24
+ **Shipped state (observe-only): NONE — the gate verdict is never changed.** In observe-only mode (the default, and the only mode shipping) `standingAuthorization` is NOT attached to the gate context, so B17 behaves exactly as today; the feature only writes a telemetry line.
25
+
26
+ **When graduated (`observeOnly:false`, a future operator decision):** the live risk is suppressing a NEEDED ask — the gate misjudges that an in-scope grant covers an action it doesn't, and tells the agent to act instead of asking. Mitigations baked in: (a) the gate judges by MEANING with full conversational context (a smart authority, not a brittle matcher); (b) a hard FLOOR-action carve-out (irreversible / cost-bearing / out-of-scope / policy-sensitive ALWAYS legitimately needs the ask, even with a live in-scope grant); (c) a decisive UNDER-FIRE bias — any uncertainty about coverage, floor-ness, or grant presence does NOT fire B17 (a needless ask is harmless; suppressing a needed one is the harm); (d) observe-only-first with a named FP-rate exit criterion before graduation.
27
+
28
+ ---
29
+
30
+ ## 2. Under-block
31
+
32
+ A genuine false-blocker ask still PASSES (no B17 fire) whenever the grant cannot be PROVEN: a forwarded-unknown legacy message-log row (no `forwarded` field), a row with a missing/blank uid, a grant outside the recency window, or a topic with no bound verified operator. This is the deliberate fail-safe direction (an unprovable grant never suppresses an ask). Also: the resolver's grant-phrase list is finite, so an operator's idiosyncratic phrasing of a grant may not be recognized — again failing toward sending the ask, never toward suppressing it.
33
+
34
+ ---
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ Correct by construction. The two new modules are **detectors** (cheap, deterministic, deps-injected, no context, no authority) that FEED the existing **smart authority** (the LLM-backed B17 rule, which already has recent-message context). This is exactly the signal→authority shape `docs/signal-vs-authority.md` prescribes — not a new parallel brittle gate. The resolver re-uses the existing `TopicOperatorStore.asVerifiedOperator` (the authoritative verified-operator binding) and the existing `TelegramAdapter.getTopicHistory` read path rather than re-implementing either.
39
+
40
+ ---
41
+
42
+ ## 4. Signal vs authority compliance
43
+
44
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
45
+
46
+ - [x] **No — this change produces a signal consumed by an existing smart gate.**
47
+
48
+ `detectAskWhenAuthorized` and `resolveStandingAuthorization` produce signals/context only; they hold zero block authority. The B17 decision stays with the LLM gate, which already reasons over conversational context and carve-outs and judges the new sub-clause BY MEANING (the gate-prompts-judge-by-meaning ratchet still passes). The observe-only telemetry is a pure record producer. No brittle logic anywhere owns a block/allow decision.
49
+
50
+ ---
51
+
52
+ ## 5. Interactions
53
+
54
+ - **Shadowing / citation precedence:** B17's citation precedence (B15 > B16 > B17 > B18) is unchanged. The standing-authorization context can NEVER flip a B1–B7/B15 leak HOLD — D7 renders `evidenceQuote` as boundary-quoted, secret-scrubbed untrusted DATA, and the gate's existing leak rules take precedence. The MessagingToneGate suite (incl. the leak-HOLD regression) passes.
55
+ - **Double-fire:** none. The resolver/telemetry run once per outbound review inside `evaluateOutbound`, alongside the other signal producers, before the single `review()` call.
56
+ - **Races:** the observe-only log write is append-only fire-and-forget; the `forwarded` field is additive to `LogEntry` (both writers persist it; the tail-cache and JSONL scan read it). No shared mutable state with concurrent cleanup.
57
+ - **Feedback loops:** none — the telemetry log is write-only observability; nothing reads it back into the gate.
58
+ - **Forwarded-persistence interaction:** the explicit `forwarded: false` diverges from the adjacent "spread `{forwarded:true}`, omit when false" idiom (TOPIC-PROFILE round-5). Verified the TOPIC-PROFILE forwarded-rejection still reads `forwarded === true` (an explicit `false` is correctly treated as not-forwarded by that path), and the TelegramAdapter suite passes.
59
+
60
+ ---
61
+
62
+ ## 6. External surfaces
63
+
64
+ - **Other agents / users:** none — dev-gated dark on the fleet (`monitoring.biasToAction.enabled` omitted → resolves dark off-dev). No HTTP route added.
65
+ - **Persistent state:** appends to `logs/bias-to-action.jsonl` (machine-local, append-only, observe-only telemetry — uid HASH + ask-phrase token + source enum + grant timestamp; NEVER a raw uid or raw operator quote). Adds a `forwarded` boolean to Telegram message-log rows (additive; other log readers ignore unknown fields).
66
+ - **External systems:** none (no Telegram/Slack/GitHub send; observe-only writes a local log).
67
+ - **Operator surface (Mobile-Complete):** **No operator-facing actions** — no route, no form, no PIN-gated action. The graduation lever is a config flag set by the operator out-of-band, not an in-product action.
68
+
69
+ ---
70
+
71
+ ## 6b. Operator-surface quality
72
+
73
+ **No operator surface — not applicable.** This change stages no `dashboard/*` renderer, approval page, or grant/secret form.
74
+
75
+ ---
76
+
77
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
78
+
79
+ **machine-local BY DESIGN**, with reason: the observe-only telemetry exists to measure THIS detector's false-positive rate on THIS machine's real outbound before any warn/block surface is built — a pure per-machine observability stream (`logs/bias-to-action.jsonl`), exactly like the principalCoherence and raw-text-request observe-only logs. The resolver reads the LOCAL topic's verified-operator binding (`TopicOperatorStore`, already machine-local / topic-holder-authoritative) and the LOCAL message-log history; a topic moved to another machine resolves against the holder's own operator-binding + history (consistent with the existing operator-binding read — the working-set carrier already follows a moved topic's message log).
80
+
81
+ - **User-facing notices?** No — observe-only writes a log; it sends nothing, so no one-voice gating needed.
82
+ - **Durable state stranding on transfer?** The telemetry log is per-machine FP measurement (not behavior), so it intentionally does not follow a topic; the `forwarded` field rides the same message-log the working-set carrier already moves.
83
+ - **URLs across machine boundaries?** None generated.
84
+
85
+ ---
86
+
87
+ ## 8. Rollback cost
88
+
89
+ - **Hot-fix:** pure code + config + one new telemetry log. Revert the branch, ship the next patch. The fleet sees NO behavioral/verdict change (the resolver + grant context + telemetry are dev-gated dark, so no standing-authorization logic runs and no message is ever altered there). The one fleet-wide difference is static: the B17 prompt now carries the STANDING-AUTHORIZATION sub-clause text + the `renderStandingAuthorization` "no verified standing authorization" placeholder — rendered unconditionally like every other static B-rule, and self-neutralizing (present:false → the sub-clause does not apply and the existing approval carve-out stands; the 55-test gate suite incl. leak-HOLD passes against it). Even a dev agent never altered a message (observe-only). So there is NO user-visible regression to roll back.
90
+ - **Data migration:** none required. `logs/bias-to-action.jsonl` is append-only telemetry, safe to leave or delete. The new `forwarded` field on message-log rows is additive and backward-compatible (legacy rows read as forwarded-unknown, which the resolver treats fail-safe).
91
+ - **Agent state repair:** none — no agent needs notification or reset.
92
+ - **User visibility:** none during the rollback window (observe-only, dark on fleet).
93
+
94
+ ---
95
+
96
+ ## Conclusion
97
+
98
+ The change is the minimal, correctly-layered embodiment of the approved spec: two deterministic signal-producers feeding the existing smart B17 authority, shipped observe-only + dev-gated dark so it measures its own false-positive rate before it ever changes a message. The review surfaced and confirmed the three load-bearing safety properties — (1) the standing-authorization context can never flip a leak HOLD, (2) every uncertainty fails toward sending the ask, and (3) a forwarded/unattributable grant never counts — each covered by tests on the real read/write paths. The forwarded-persistence work (explicit `false` on both ingress paths) is the load-bearing security substrate and is wiring-tested through the real `getTopicHistory`. Clear to ship as observe-only; live B17 firing remains a deliberate future operator decision.
99
+
100
+ ---
101
+
102
+ ## Second-pass review (if required)
103
+
104
+ **Reviewer:** independent reviewer subagent (adversarial, security-focused)
105
+ **Independent read of the artifact: CONCUR**
106
+
107
+ The reviewer independently traced all six security-critical claims against the actual code + the wiring/gate tests and could not break any:
108
+
109
+ 1. **Leak-HOLD flip** — Safe. `standingAuthorization` is attached to the gate context ONLY in the `observeOnly === false` branch; in the shipped observe-only/dark state it stays `undefined`. When attached, `renderStandingAuthorization` JSON-encodes the quote in a fresh `AUTH_BOUNDARY_…`, length-bounds to 280, and the route pre-scrubs via `scrubString`; labeled untrusted DATA; citation precedence (B15>B16>B17>B18) untouched. Leak-HOLD regressions pass.
110
+ 2. **Attribution** — Safe. Matched on `String(telegramUserId) === String(verified uid)` (authenticated `msg.from.id`), never `fromUser`/content-name; blank uid is a non-match (no wildcard); the `fromUser` filter excludes the agent's own sends (no self-grant).
111
+ 3. **Forwarded third-party content** — Safe. Resolver counts only strict `forwarded === false`; both ingress writers persist an explicit boolean; legacy rows (no field) fail-safe; the lone downstream `forwarded` consumer (topicProfileIngress) is a truthy check, so explicit `false` doesn't break it.
112
+ 4. **Telemetry leak** — Safe. Emits only ISO time, kind, topicId, source enum, ask-phrase token (agent's own canned phrase, bounded), uid HASH (12 hex), grantedAt. No raw uid, no operator words.
113
+ 5. **Fail-open / inert** — Confirmed. Whole block is try/catch fail-open; telemetry write independently try-caught; on the fleet `resolveDevAgentGate` returns false so nothing runs.
114
+ 6. **Dev-gate** — Correct. ConfigDefaults omits `enabled`; `resolveDevAgentGate` funnel; registered in `DEV_GATED_FEATURES`. Observe-only path confirmed NOT to attach the grant.
115
+
116
+ One non-blocking observation (no fix to code required): the B17 sub-clause prompt text renders fleet-wide even where the resolver is dark — addressed by softening §8's wording above (the fleet sees no behavioral change; the static prompt section is self-neutralizing). Nothing security-critical is broken.
117
+
118
+ ---
119
+
120
+ ## Evidence pointers
121
+
122
+ - Unit: `tests/unit/standing-authorization.test.ts` (11), `tests/unit/ask-when-authorized.test.ts` (16), `tests/unit/bias-to-action-telemetry.test.ts` (6).
123
+ - Wiring-integrity (real read/write paths): `tests/unit/bias-to-action-wiring.test.ts` (8) — forwarded persistence on BOTH ingress paths incl. explicit `false`; resolver fed from the real `getTopicHistory` honoring verified-uid-only + forwarded + no-operator fail-safes.
124
+ - Gate: `tests/unit/MessagingToneGate.test.ts` (55) incl. the leak-HOLD regression + gate-prompts-judge-by-meaning ratchet.
125
+ - Ratchets: `npm run lint` clean (incl. `lint-dev-agent-dark-gate`); `tests/unit/lint-dev-agent-dark-gate.test.ts` line-map updated (+9 shift documented).
126
+ - Spec: `docs/specs/BIAS-TO-ACTION-SPEC.md` (review-convergence + approved:true), ELI16 `docs/specs/BIAS-TO-ACTION-SPEC.eli16.md`.
@@ -0,0 +1,147 @@
1
+ # Side-Effects Review — Tone-gate deterministic floor: click-link carve-out
2
+
3
+ **Tier:** 1 (small, surgical, safe-direction false-positive fix). **Risk floor will signal Tier 2** (messaging block/allow gate) — Phase-5 second-pass review performed regardless; `belowFloor` audit accepted (I hold authority; the override is recorded).
4
+ **Anchor:** `docs/specs/tone-gate-graceful-degradation.md` (the F4 deterministic floor this fix extends) + `docs/specs/tonegate-floor-click-links.eli16.md`. The precise-subject spec is the shipped F4 postmortem fix; this is a follow-up false-positive correction to that floor. (Not Tier-2-anchored: that spec carries no `approved:true`, and I must never self-approve.)
5
+ **Files:** `src/core/MessagingToneGate.ts`, `tests/unit/MessagingToneGate.test.ts`
6
+
7
+ ## What changed
8
+
9
+ 1. **`scrubClickLinksForFloor(text)` (new, exported):** removes scheme'd `http(s)://…`
10
+ URLs (the click-destination token) from a copy of the text BEFORE the floor's
11
+ signal scan — UNLESS the text carries a CALL instruction: a fetch tool
12
+ (`curl`/`wget`/`xh`/`httpie`), an uppercase HTTP method against a URL/path
13
+ (`POST https://…` / `GET /…`), or an imperative call phrase ("hit/call/invoke/
14
+ issue/send … endpoint/url/api/request"). On any of those it returns the text
15
+ UNCHANGED, so the floor sees the call verbatim and blocks as today.
16
+ 2. **`detectDeterministicLeak(text)`:** now scans `scrubClickLinksForFloor(text)`
17
+ instead of raw `text` — for BOTH `detectGateSignals` and `detectInternalIdLeak`.
18
+ Nothing else about the floor changed; the LLM path is untouched.
19
+
20
+ ## 1. Over-block
21
+
22
+ - **Before:** the floor over-blocked EVERY clickable link an agent shared during an
23
+ LLM outage (private view, tunnel, dashboard, Secret-Drop, Telegraph, download) —
24
+ the host/port/path/token tripped the `api-endpoint` / `cron-or-slug` signals.
25
+ This is the bug being fixed (it just blocked a real Secret-Drop link).
26
+ - **After:** a bare clickable URL no longer trips the floor. No new over-block is
27
+ introduced — the scrub only REMOVES match surface, never adds a rule.
28
+
29
+ ## 2. Under-block
30
+
31
+ - The scrub removes scheme'd URLs only. A leak embedded in a URL's path/query
32
+ (e.g. a file path inside `…/download?path=.claude/CLAUDE.md`) is removed along
33
+ with the URL — acceptable, because the agent deliberately shared that as a
34
+ click destination (the same intent the LLM path already passes). A file path or
35
+ command written OUTSIDE a URL is left fully intact and still HELD (proven by the
36
+ file-path + bare-CLI tests).
37
+ - A call instruction that uses none of the three recognized forms (no curl/wget,
38
+ no uppercase method, no "hit/call/…" phrase) — e.g. a lowercase prose "open this
39
+ api 〈url〉" — would have its URL scrubbed. This is acceptable: such phrasing reads
40
+ as an open instruction, the host/path is gone, and no command/path/secret
41
+ escapes. The floor is the DEGRADED path (LLM down); the LLM path catches intent
42
+ precisely when up. Erring toward delivering a link beats silencing the user.
43
+
44
+ ## 3. Level-of-abstraction fit
45
+
46
+ - Correct layer. The fix lives exactly where the false positive lives — the
47
+ deterministic floor (`detectDeterministicLeak`), the degraded-path counterpart
48
+ to the LLM judge's existing intent-based B5 carve-out. It does NOT touch
49
+ `GateSignalDetectors` (those feed the LLM prompt too and must stay literal); it
50
+ pre-processes only the floor's input. The smarter gate (the LLM) already owns
51
+ the precise judgment when available; this only narrows the dumb fallback's known
52
+ false-positive class.
53
+
54
+ ## 4. Signal vs authority compliance
55
+
56
+ - The floor HOLDS authority (it blocks under degradation) but with deliberately
57
+ brittle logic — that is its established design (F4: never silently cut the user
58
+ off; hold only hard artifact leaks). This change only REDUCES that authority's
59
+ false-positive surface in the safe direction (fewer wrong holds), never expands
60
+ what it lets through. No new blocking authority is added. Per
61
+ `docs/signal-vs-authority.md`, loosening a brittle authority's over-block while
62
+ preserving its leak-safety is the correct direction.
63
+
64
+ ## 5. Interactions
65
+
66
+ - `buildDegradedToneResult` (both degrade sites: provider-throw in `review()` and
67
+ the slow-stall seam in `reviewWithinBudget`) calls `detectDeterministicLeak`, so
68
+ both degrade sites inherit the carve-out identically — no divergence.
69
+ - The `failClosedOnExhaustion: true` operator override is unaffected: it bypasses
70
+ the floor entirely (pure hold), so its behavior is unchanged.
71
+ - No race, no double-fire, no shadowing — pure synchronous string pre-processing.
72
+
73
+ ## 6. External surfaces
74
+
75
+ - No new route, no config key, no schema, no migration. Behavior visible to users
76
+ only as: during an LLM outage, a shared click-link now reaches them instead of
77
+ being silently held. No timing/runtime-state dependence (pure function).
78
+
79
+ ## 7. Multi-machine posture
80
+
81
+ - **Machine-local BY DESIGN, identical on every machine.** The tone gate runs
82
+ per-message on whichever machine is serving; this is a pure-function change to
83
+ that per-message logic. No state, no replication, no cross-machine surface.
84
+ Every machine applies the identical floor. No one-voice / transfer / URL-survival
85
+ concern (it has no durable state and emits no notice).
86
+
87
+ ## 8. Rollback cost
88
+
89
+ - Trivial. Revert the two edits in `MessagingToneGate.ts` (delete the helper +
90
+ restore `detectGateSignals(text)` / `detectInternalIdLeak(text)`). No data, no
91
+ config, no agent-state repair. Always-on with no flag because the prior behavior
92
+ was simply incorrect.
93
+
94
+ ## Tests
95
+
96
+ - `tests/unit/MessagingToneGate.test.ts` — new `deterministic floor — click-link
97
+ carve-out` describe (12 cases): each click-link class (Secret-Drop / private
98
+ view / dashboard / Telegraph / download) PASSES the floor; a `curl` call, an
99
+ uppercase-method call, and a "hit the endpoint" phrase still HOLD; a file-path
100
+ leak still HOLDS (`B2_FILE_PATH`); a bare CLI command alongside a link still
101
+ HOLDS; `scrubClickLinksForFloor` unit behavior (strips a URL, leaves a curl line
102
+ unchanged); and an end-to-end `review()` degrade-and-SEND of a Secret-Drop link
103
+ on provider throw. Full file: 53/53 green. Budget + no-silent-fallbacks +
104
+ dark-gate lint ratchets: green. tsc clean.
105
+
106
+ ## Agent awareness
107
+
108
+ - No CLAUDE.md change required: this is an internal correctness fix to an existing
109
+ safety gate with no new user-facing capability, route, or config knob. The
110
+ user-visible effect (links reach you during an outage) needs no agent
111
+ instruction.
112
+
113
+ ## Phase-5 second-pass review (independent reviewer)
114
+
115
+ **VERDICT: Concur with the review** — the change preserves leak-safety while
116
+ fixing the false positive. The reviewer empirically ran the scrub against eight
117
+ bypass attempts and verified:
118
+
119
+ - No new leak class escapes. Leaks written OUTSIDE a URL are fully preserved
120
+ (env-var `GITHUB_TOKEN=…`, config-key, file-path `/Users/…/id_rsa` all still
121
+ trip after the scrub). The scrub regex consumes only scheme'd URL tokens up to
122
+ whitespace/`)`/`]`, so an adjacent space-separated leak survives — the
123
+ safe-direction claim is real.
124
+ - "Secret inside a URL escapes" is out-of-scope by correct layering, not a gap:
125
+ the floor has no secret detector (raw-credential redaction is a separate
126
+ `redactSecrets`/`guardProxyOutput` pass), so the scrub removes nothing the floor
127
+ was protecting. A token inside a URL the agent chose to share as a click
128
+ destination is the intended open-vs-call trade (matches the LLM B5 carve-out).
129
+ - Call-instruction suppression is sound (curl / uppercase-method-vs-URL /
130
+ "hit … endpoint" all still HOLD). The one documented loosening (a file path
131
+ inside a `…/download?path=…` URL) is acknowledged in §2 and acceptable on the
132
+ degraded path.
133
+ - Internal-id detector on scrubbed text does not weaken meaningfully: a bare
134
+ `CMT-1234` in prose (the actual anti-pattern) is untouched; only an id embedded
135
+ in a shared click-URL is skipped — benign by the rule's own intent.
136
+ - Both degrade sites inherit the carve-out via the single
137
+ `buildDegradedToneResult → detectDeterministicLeak` chokepoint; the
138
+ `failClosedOnExhaustion:true` override (pure hold) is unaffected. No race/state.
139
+
140
+ **Non-blocking concern raised → RESOLVED in this PR (no deferral):** the reviewer
141
+ flagged latent O(n²) backtracking in `CALL_PHRASE` (`\s+(?:…)?\s*` adjacency),
142
+ harmless at Telegram's ~4096-char cap (~9ms) but a hardening risk for any future
143
+ unbounded-input caller. Fixed immediately: the article group now carries its own
144
+ trailing `\s+` (`(?:(?:this|the|a)\s+)?`), eliminating the ambiguous whitespace
145
+ split — linear time. Guarded by a new test (`scrubClickLinksForFloor runs in
146
+ linear time on a long whitespace run`, 200k-char adversarial input < 200ms) and a
147
+ no-article call-phrase HOLD test.