instar 1.3.1247 → 1.3.1249
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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +20 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/testAsSelfValidation.js +1 -1
- package/dist/commands/testAsSelfValidation.js.map +1 -1
- package/dist/core/ExecutionJournal.d.ts.map +1 -1
- package/dist/core/ExecutionJournal.js +2 -0
- package/dist/core/ExecutionJournal.js.map +1 -1
- package/dist/core/JevSignalShadow.d.ts +98 -0
- package/dist/core/JevSignalShadow.d.ts.map +1 -0
- package/dist/core/JevSignalShadow.js +230 -0
- package/dist/core/JevSignalShadow.js.map +1 -0
- package/dist/core/MessagingToneGate.d.ts +10 -0
- package/dist/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +18 -0
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +17 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/durableSecretScrub.d.ts +6 -4
- package/dist/core/durableSecretScrub.d.ts.map +1 -1
- package/dist/core/durableSecretScrub.js +28 -6
- package/dist/core/durableSecretScrub.js.map +1 -1
- package/dist/core/redactUrl.d.ts.map +1 -1
- package/dist/core/redactUrl.js +1 -0
- package/dist/core/redactUrl.js.map +1 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/messaging/outbound-credential-guard.d.ts.map +1 -1
- package/dist/messaging/outbound-credential-guard.js +31 -8
- package/dist/messaging/outbound-credential-guard.js.map +1 -1
- package/dist/monitoring/ClaimObservation.js +1 -1
- package/dist/monitoring/ClaimObservation.js.map +1 -1
- package/dist/monitoring/PromiseBeacon.js +1 -1
- package/dist/monitoring/PromiseBeacon.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +30 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ContentClassifier.d.ts.map +1 -1
- package/dist/threadline/ContentClassifier.js +4 -0
- package/dist/threadline/ContentClassifier.js.map +1 -1
- package/dist/threadline/openConversationBrief.js +1 -1
- package/dist/threadline/openConversationBrief.js.map +1 -1
- package/package.json +1 -1
- package/scripts/audit-secret-patterns.mjs +1 -0
- package/scripts/jev-shadow-report.mjs +89 -0
- package/src/data/builtin-manifest.json +63 -63
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/{1.3.1247.md → 1.3.1249.md} +65 -0
- package/upgrades/side-effects/jev-signal-shadow.md +138 -0
- package/upgrades/side-effects/outbound-credential-wall-modern-key-formats.md +178 -0
|
@@ -23,6 +23,41 @@ Two honesty fixes ride along. An account that genuinely reports no usage window
|
|
|
23
23
|
|
|
24
24
|
This is a reader and presentation fix. No routing, placement, load-shedding or proactive-swap threshold changed. Their input improves — two exhausted accounts now present a real number where they previously presented nothing — which makes them more likely to route work away from a full account, not less.
|
|
25
25
|
|
|
26
|
+
A new, dark research instrument: the Jev signal-layer shadow. When turned on, every
|
|
27
|
+
message reaching the outbound tone gate is also shown to TypeSafe's Jev model with
|
|
28
|
+
seven yes/no questions — one for each existing deterministic check for jargon a
|
|
29
|
+
user shouldn't see (raw paths, terminal commands, config keys, internal endpoints,
|
|
30
|
+
paste-and-run code, environment variables, cron strings and internal ids). Each
|
|
31
|
+
answer is compared with what the existing check found and a content-free row is
|
|
32
|
+
written to `logs/jev-signal-shadow.jsonl`. It decides nothing, is never waited on,
|
|
33
|
+
and never stores message text.
|
|
34
|
+
|
|
35
|
+
It ships OFF (`intelligence.jevSignalShadow.enabled: false`), and it stays inert
|
|
36
|
+
even when enabled unless a future `soakEndsAt` is set and a `typesafe_api_key` is in
|
|
37
|
+
the vault. `scripts/jev-shadow-report.mjs` turns the log into per-check agreement
|
|
38
|
+
tables. Existing agents receive the dark default block on update.
|
|
39
|
+
|
|
40
|
+
The outbound live-credential wall — the one check that stops a message carrying an
|
|
41
|
+
API key or token from being sent, and the one check that cannot be overridden — did
|
|
42
|
+
not recognise the key formats providers issue today. Modern OpenAI keys
|
|
43
|
+
(`sk-proj-`, `sk-svcacct-`, `sk-admin-`, `sk-None-`), OpenRouter keys (`sk-or-v1-`)
|
|
44
|
+
and GitHub fine-grained tokens (`github_pat_`) all passed it, and the LLM review
|
|
45
|
+
behind the wall passed them too. Two prefix-anchored patterns in the shared secret
|
|
46
|
+
list now cover them, and the same list's durable-output redaction redacts them.
|
|
47
|
+
|
|
48
|
+
The wall also now strips invisible interleave characters (zero-width and other
|
|
49
|
+
format characters, the combining grapheme joiner, variation selectors) before
|
|
50
|
+
matching, scanning both the raw and the stripped text. Two send paths that skipped
|
|
51
|
+
the whole outbound authority — proxy/system-template Telegram replies and
|
|
52
|
+
Agent-Health-lane attention items — now still run the credential wall; their skip
|
|
53
|
+
continues to bypass tone judgment only. Eight hand-kept copies of credential
|
|
54
|
+
patterns across the codebase were brought up to the same shapes.
|
|
55
|
+
|
|
56
|
+
The new falsifiable timing fixture found two pre-existing quadratic patterns in the
|
|
57
|
+
shared list (`jwt` and `url-embedded-credential`); `url-embedded-credential` runs on
|
|
58
|
+
every outbound message as part of the wall and could stall the server for minutes on
|
|
59
|
+
a large pasted blob. Both are capped, with no change in what they detect.
|
|
60
|
+
|
|
26
61
|
The Subscriptions dashboard's quota bars are now colour-coded by how much of each account's limit is used. Every bar previously rendered in the same green regardless of utilization, so the only difference between an account at 4% and one at 92% was the bar's length — a distinction that is hard to read at a glance across a stacked list of accounts, which is exactly where the list is read.
|
|
27
62
|
|
|
28
63
|
Each bar's fill now carries one of three severity classes derived from the same clamped 0–100 integer that already sets its width: green below 75% used, amber from 75% through 89%, red at 90% and above. The percent text that has always sat beside the bar is unchanged, so the colour is redundant with information already on screen rather than the only way to read the value.
|
|
@@ -41,6 +76,16 @@ Two things will look a little different. An account that genuinely has no weekly
|
|
|
41
76
|
|
|
42
77
|
Nothing about how your work is spread across accounts changed.
|
|
43
78
|
|
|
79
|
+
Nothing changes for you. This adds a measuring tool I can switch on for a limited
|
|
80
|
+
time to test whether a small, cheap model can take over some of the checks I run
|
|
81
|
+
on my own messages. It never changes, delays or blocks what I send.
|
|
82
|
+
|
|
83
|
+
If I ever try to send you a message containing one of today's API keys or access
|
|
84
|
+
tokens — the current OpenAI and OpenRouter key formats, or GitHub's newer
|
|
85
|
+
fine-grained tokens — it is now stopped before it reaches you, the same way older key
|
|
86
|
+
formats already were. I'll refer to the key by name instead. Nothing changes for any
|
|
87
|
+
normal message.
|
|
88
|
+
|
|
44
89
|
Your Subscriptions dashboard now shows each account's quota bar in green, amber or red depending on how much of the limit is used, instead of always green. An account that is comfortable stays green, one that is getting tight turns amber at 75%, and one that is at or near its wall turns red at 90%. Nothing about how work is routed between your accounts changed — this only makes the page easier to scan. The percentage is still written next to every bar.
|
|
45
90
|
|
|
46
91
|
## Summary of New Capabilities
|
|
@@ -59,6 +104,16 @@ Your Subscriptions dashboard now shows each account's quota bar in green, amber
|
|
|
59
104
|
- Quota readings older than six hours are labelled with their age on the Subscriptions card.
|
|
60
105
|
- Back-compatible: rollouts with no `limit_id` are still read as codex-family; the new snapshot field is optional and absent on existing stored state.
|
|
61
106
|
|
|
107
|
+
- Dark, time-bounded comparison of Jev against the B1–B7 outbound jargon checks.
|
|
108
|
+
- Content-free comparison log plus a per-check confusion-matrix report script.
|
|
109
|
+
- Shadow calls metered under feature `jev-signal-shadow` in the LLM metrics.
|
|
110
|
+
|
|
111
|
+
- The non-overridable outbound credential wall covers modern OpenAI, OpenRouter and GitHub fine-grained key formats.
|
|
112
|
+
- Invisible-character interleaving no longer hides a key from the wall.
|
|
113
|
+
- Proxy replies and Agent-Health-lane attention items are covered by the wall.
|
|
114
|
+
- Durable-output redaction covers the same formats.
|
|
115
|
+
- Two latent event-loop stalls in the shared secret-pattern list removed.
|
|
116
|
+
|
|
62
117
|
- Subscriptions quota bars are colour-coded: green (<75% used), amber (75–89%), red (≥90%).
|
|
63
118
|
- The colour is derived from the already-clamped percent, so a bad reading cannot produce a misleading colour — it clamps to 0% / green or 100% / red.
|
|
64
119
|
- Colour is never the only signal: the "N% used" text is unchanged and always present.
|
|
@@ -79,6 +134,16 @@ Your Subscriptions dashboard now shows each account's quota bar in green, amber
|
|
|
79
134
|
- Unit (`tests/unit/quota-poller.test.ts`, 26 passing): both sides of the boundary — a windowless reading sets `noQuotaWindow`, a windowed one does not.
|
|
80
135
|
- Unit (`tests/unit/subscriptions-render.test.ts`, 97 passing): the three distinct card states (a bar, "reports no usage window", "No quota reading yet") and the age label appearing above the six-hour threshold and not below it.
|
|
81
136
|
|
|
137
|
+
- Unit `tests/unit/JevSignalShadow.test.ts` (17 passing): question set covers every detector kind; inert when disabled, with no soak window, past it, or without a key; compared and every not-compared reason reachable; no message text in any row or from a vendor error body; synchronous, never-throwing `observe()`; key cached; metering recorded.
|
|
138
|
+
- Integration `tests/integration/jev-signal-shadow-gate.test.ts` (6 passing): real gate verdict identical with a disabled shadow; verdict returns while the Jev call hangs; a throwing observer or a network failure cannot break the gate; report script builds deduplicated confusion matrices.
|
|
139
|
+
- E2E `tests/e2e/jev-signal-shadow-lifecycle.test.ts` (3 passing): the update path installs the dark default idempotently; the shipped state makes no call and writes no row; flipping the config with no restart produces a compared row.
|
|
140
|
+
|
|
141
|
+
- Unit `tests/unit/outbound-credential-wall-modern-formats.test.ts` (65 passing): every modern issued shape refused (generated at test time, nothing committed); no regression on older shapes; drift guard mapping every messaging pattern type to a wall kind or a stated exemption; cross-file ratchet over the eight copies; no false positives on prose, bare prefixes and kebab slugs; invisible-character strip across nine classes plus the separated-key regression guard; oversize still fails closed; scrubber offsets stay valid; per-pattern linear-time budget on a 64 KB unbroken run with interior word boundaries (no exemptions); capped `jwt`/`url` detection equivalence.
|
|
142
|
+
- Integration `tests/integration/telegram-reply-advisory-migration.test.ts` (18 passing): modern keys refused on a normal reply, on a proxy reply, and in a laned attention item; a clean proxy reply still sends.
|
|
143
|
+
- E2E `tests/e2e/tone-gate-advisory-migration-alive.test.ts` (12 passing): modern keys refused on the real boot.
|
|
144
|
+
- Measured: over ~366 MB of real text (Telegram history, repo, logs, state stores) the new patterns match zero strings through the full shipped pipeline. `url-embedded-credential` went from 2.4 s at 64 KB to 157 ms for a full megabyte.
|
|
145
|
+
- Independent second-pass review: concur (both nuances raised were acted on before commit).
|
|
146
|
+
|
|
82
147
|
- Visual: the shipped module and the shipped CSS rendered together against the account mix from the operator's reference screenshot — 0/22/68% green, 83/89% amber, 100% red.
|
|
83
148
|
- Unit (`tests/unit/subscriptions-render.test.ts`, 92 passing): both sides of both band boundaries (74/75 and 89/90), severity derived from the clamped number (1000 → critical, −50 → ok, non-numeric → ok, 74.6 → warn by rounding), the fill's class attribute, and a hostile percent proving the class stays a closed-set literal.
|
|
84
149
|
- Mutation-checked: flipping `>=` to `>` on the warn boundary fails the 75% case, so the boundary assertions are load-bearing rather than decorative.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Side-Effects Review — Jev signal-layer shadow (dark, measure-only)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `jev-signal-shadow`
|
|
4
|
+
**Date:** `2026-09-21`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent (see appended section)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `JevSignalShadow` (`src/core/JevSignalShadow.ts`), a research instrument that
|
|
11
|
+
sends each message reaching the outbound tone gate to TypeSafe's Jev model with
|
|
12
|
+
seven yes/no questions — one per deterministic B1–B7 artefact detector — and logs a
|
|
13
|
+
content-free comparison row to `logs/jev-signal-shadow.jsonl`. `MessagingToneGate`
|
|
14
|
+
gains `setSignalShadow()` and calls `observe(text)` once at the top of `review()`
|
|
15
|
+
inside a try/catch; `observe()` is synchronous, never throws, and never awaited.
|
|
16
|
+
`server.ts` constructs it after the tone gate with a live config read, the vault key
|
|
17
|
+
`typesafe_api_key`, and the feature-metrics recorder. `PostUpdateMigrator.migrateConfig`
|
|
18
|
+
adds the dark default `intelligence.jevSignalShadow` block (enabled:false,
|
|
19
|
+
soakEndsAt:null). `scripts/jev-shadow-report.mjs` reads the log into per-rule
|
|
20
|
+
confusion matrices plus coverage.
|
|
21
|
+
|
|
22
|
+
Spec: `docs/specs/jev-signal-layer-shadow.md` (converged; approved by Justin
|
|
23
|
+
2026-09-21, including the soak egress approval).
|
|
24
|
+
|
|
25
|
+
## Decision-point inventory
|
|
26
|
+
|
|
27
|
+
- *(none)* — the shadow writes an audit row and nothing reads it on any decision
|
|
28
|
+
path. `MessagingToneGate.review()` gains one guarded fire-and-forget call before
|
|
29
|
+
its existing logic; the verdict computation is untouched.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Over-block
|
|
34
|
+
|
|
35
|
+
No issue identified. The shadow holds no authority and cannot refuse, delay or alter
|
|
36
|
+
a message. Proven by the integration test that runs the real gate against a Jev stub
|
|
37
|
+
that never settles: the verdict returns while the call is still pending.
|
|
38
|
+
|
|
39
|
+
## 2. Under-block
|
|
40
|
+
|
|
41
|
+
Not applicable — it blocks nothing. What it can MISS as an instrument is recorded,
|
|
42
|
+
not hidden: every candidate that is not compared produces a row with a closed-enum
|
|
43
|
+
reason (timeout, http-error, oversize, skipped-concurrent, skipped-sample,
|
|
44
|
+
model-mismatch, disabled-no-key, soak-expired), so the report's coverage figure is
|
|
45
|
+
honest. Standing conditions (no key, soak expired) write one row per process, not
|
|
46
|
+
one per message.
|
|
47
|
+
|
|
48
|
+
## 3. Level-of-abstraction fit
|
|
49
|
+
|
|
50
|
+
The tone gate is the one place every outbound message already passes with its
|
|
51
|
+
detector signals available, so observing there is the correct layer. The shadow
|
|
52
|
+
re-runs `detectGateSignals` itself (cheap, deterministic) rather than threading
|
|
53
|
+
state out of the gate, keeping zero shared state with the decision path.
|
|
54
|
+
|
|
55
|
+
## 4. Signal vs authority compliance
|
|
56
|
+
|
|
57
|
+
Pure signal, and not even a consumed one yet: the output is a log for a later human
|
|
58
|
+
decision. Per `docs/signal-vs-authority.md`, adding an unconsumed detector is the
|
|
59
|
+
safest possible shape. Graduation into any authority is explicitly a separate spec.
|
|
60
|
+
|
|
61
|
+
## 5. Interactions
|
|
62
|
+
|
|
63
|
+
- **Gate latency:** synchronous cost per candidate is one config read, one sha256
|
|
64
|
+
(over at most the first 1 MB), one small append for any not-compared row
|
|
65
|
+
(skipped-sample, oversize, skipped-concurrent), and (when dispatching) one
|
|
66
|
+
detector pass and one JSON body. The vault key — a lookup that can block on a
|
|
67
|
+
macOS keychain subprocess — is read at construction (boot); after a miss or a 401/403 it is re-read at most once per 10 minutes, so a
|
|
68
|
+
revoked key costs one lookup per 10 minutes, not one per message. That rate-limited
|
|
69
|
+
re-read does run synchronously on the gate path, and only while the shadow is
|
|
70
|
+
enabled with a missing or revoked key. A key added to
|
|
71
|
+
the vault later is picked up within 10 minutes, no restart.
|
|
72
|
+
Network runs detached. Single-flight: at most one call in flight per process;
|
|
73
|
+
overflow is a row, never a queue, so a slow vendor cannot build memory pressure.
|
|
74
|
+
- **Feature metrics:** each call records under feature `jev-signal-shadow`,
|
|
75
|
+
framework `typesafe-api`, so token spend appears in `/metrics/features` beside
|
|
76
|
+
other LLM features. No double-counting with the gate's own LLM call.
|
|
77
|
+
- **Credential wall:** unaffected; the shadow sees the same text the gate sees and
|
|
78
|
+
sends it only to TypeSafe, and only while enabled.
|
|
79
|
+
- **Tests:** the shadow is optional (`signalShadow?`); every existing tone-gate test
|
|
80
|
+
constructs the gate without one and is unchanged.
|
|
81
|
+
|
|
82
|
+
## 6. External surfaces
|
|
83
|
+
|
|
84
|
+
While ENABLED, message text reaching the gate is sent to one more third party
|
|
85
|
+
(TypeSafe, under the MCA accepted 2026-09-20). This is the egress the operator
|
|
86
|
+
approved explicitly in the spec (`soak-egress-approved: true`). Ships DISABLED; the
|
|
87
|
+
14-day bound is mechanical (`soakEndsAt`, checked per candidate, survives restarts).
|
|
88
|
+
Log rows never contain message text: sha256, byte length, detector kinds, Jev
|
|
89
|
+
probabilities, reason enum. A vendor error body is never recorded. The vault key is
|
|
90
|
+
read once and cached, re-read only after a 401/403, and never logged.
|
|
91
|
+
|
|
92
|
+
## 7. Multi-machine posture
|
|
93
|
+
|
|
94
|
+
Machine-local BY DESIGN. The log is this machine's observation of its own gate
|
|
95
|
+
traffic (observation locality). Config is ordinary per-machine config; the soak is
|
|
96
|
+
enabled on this dev agent only. No user-facing notices, no durable state that must
|
|
97
|
+
follow a topic, no URLs.
|
|
98
|
+
|
|
99
|
+
## 8. Rollback cost
|
|
100
|
+
|
|
101
|
+
Operational: set `intelligence.jevSignalShadow.enabled` false — read live per
|
|
102
|
+
candidate, no restart. Code: revert the commit; the config block left behind is
|
|
103
|
+
inert (nothing else reads it). The log is inert data.
|
|
104
|
+
|
|
105
|
+
## Second-pass review
|
|
106
|
+
|
|
107
|
+
**Reviewer:** independent reviewer subagent
|
|
108
|
+
**Verdict:** Concern raised: the vault key read runs synchronously on the gate path, and after a 401/403 it runs again on every dispatched candidate.
|
|
109
|
+
|
|
110
|
+
The concern:
|
|
111
|
+
- `observeInner()` calls `readKey()` inline, inside `MessagingToneGate.review()`, whenever `cachedKey === undefined`. In production `readKey` builds a `SecretStore` and calls `.get()`. On macOS that resolves the master key with `execFileSync('security', …, { timeout: 5000 })`, which is a synchronous subprocess that blocks the whole event loop. It can take up to 5 s per lookup, and the resolution order may try two keychain accounts. This cost lands on the first enabled candidate.
|
|
112
|
+
- After a 401/403, `dispatch` resets `cachedKey = undefined`. The next candidate then re-reads the key synchronously. The key read happens before the single-flight check, so a revoked key that stays in the vault leads to one blocking keychain spawn per dispatched candidate, for the whole soak. The spec forbids exactly this ("never a per-call decrypt on the event loop", "never a gate delay").
|
|
113
|
+
- Suggested fix: resolve the key off the message path. Either read it inside the detached `dispatch()`, or warm it at construction or on a `setImmediate`. Also latch an auth failure, for example to a `disabled-no-key`-class status with a bounded re-read interval, instead of re-reading on every candidate.
|
|
114
|
+
|
|
115
|
+
Smaller accuracy points for this artifact (not blocking):
|
|
116
|
+
- §5 "Interactions / Gate latency" lists the synchronous cost as config read, sha256, detector pass and JSON body. It leaves out two synchronous costs: the keychain read above, and the `mkdirSync` + `appendFileSync` for the `not-compared` rows (`skipped-sample`, `oversize`, `skipped-concurrent`, status rows). Those rows are written on the gate path.
|
|
117
|
+
- The sha256 is computed over the full text before the 1 MB size guard, so hashing an oversize message is not bounded by the guard.
|
|
118
|
+
- A missing key is cached as `null` for the life of the process, so a key added to the vault after enabling needs a restart. That is consistent with "read once", but the operator should be told.
|
|
119
|
+
- The e2e test wires the real gate to a shadow built in the test. It does not exercise the `server.ts` construction: the live `intelligence.jevSignalShadow` read, the `SecretStore` key read and the `logs/` path. The tone-gate lifecycle standard asks for a test of the production initialization path.
|
|
120
|
+
|
|
121
|
+
What I checked and found sound:
|
|
122
|
+
- Verdict isolation: `observe()` is wrapped in try/catch both in the module and at the call site. `dispatch` is async and chained with `.catch().finally()`, so no promise rejection goes unhandled. `review()` never awaits the dispatch. The abort timer is bounded by `timeoutMs` and cleared in `finally`, so it cannot keep the process alive past that bound.
|
|
123
|
+
- Single-flight: `inFlight` is set only after every early return, and it is reset in `.finally()` on every path. `write()` and the metrics call each swallow their own errors, so neither can skip the reset.
|
|
124
|
+
- Leakage: rows carry only the sha256, byte length, detector kinds, probabilities, model and a closed-enum reason. The vendor response body is never read on a non-OK status. The API key appears only in the request header and never reaches a row, a metric or the console. The construction-failure log line prints only the error message.
|
|
125
|
+
- Disabled means inert: `enabled !== true` returns before hashing, any key read, any network call or any row. The integration and e2e tests assert zero rows.
|
|
126
|
+
- The soak bound is mechanical: a missing, unparseable or past `soakEndsAt` is inert and checked on every candidate. The block is read from live config, so the bound survives restarts. It writes one status row per process.
|
|
127
|
+
- Spec coverage: the closed reason set is complete and each reason has a test. Model pinning and `model-mismatch` exclusion, the migration (existence-checked, idempotent), the feature-metrics metering and the report script (dedupe by hash, per-rule confusion matrices, coverage, insufficient-evidence under 20 positives) are all present. All 26 tests in the three new files pass. The enabled-vs-disabled latency measurement belongs to the test-agent-live stage and is not claimed as done here.
|
|
128
|
+
|
|
129
|
+
### Re-review after fixes
|
|
130
|
+
|
|
131
|
+
**Verdict:** Concur with the review
|
|
132
|
+
|
|
133
|
+
- **Key read:** the constructor now reads the key once, at boot, through `refreshKey()`. A missing key, or a 401/403 (which clears both the key and the timestamp), is re-read at most once per `KEY_REREAD_MS` (10 min). The earlier failure mode, one blocking keychain spawn per dispatched message with a revoked key, is gone.
|
|
134
|
+
- **One wording point, not blocking:** a rate-limited re-read still runs synchronously inside `observeInner()`, so on the message path. The cost is at most one lookup per 10 minutes, and only while the shadow is enabled with a missing or revoked key. That bound is acceptable. §5's "never on the message path" should be read as applying to the first read only.
|
|
135
|
+
- **Boot-time read on dark agents:** construction now does a vault read at boot on every agent, including ones where the shadow is disabled. That is harmless. `SecretStore.read()` returns `{}` without touching the keychain when no vault file exists, it never generates a key on this path, and other boot code already reads the vault.
|
|
136
|
+
- **Hash bound:** sha256 is now computed over at most the first `maxScanBytes`, so hash cost is bounded.
|
|
137
|
+
- **Production wiring:** it is extracted into `buildJevSignalShadow()`. `server.ts` and the e2e test both build through it (live `intelligence` read, `readSecret('typesafe_api_key')`, the `logs/` path), so Tier 3 now covers the production construction path.
|
|
138
|
+
- **Everything else still holds:** single-flight reset, no rejection left unhandled, no leakage, disabled is inert, and the mechanical soak bound are unchanged. The three test files now hold 29 tests (up from 26), and all pass.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Side-Effects Review — Outbound credential wall: modern OpenAI and GitHub key formats
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `outbound-credential-wall-modern-key-formats`
|
|
4
|
+
**Date:** `2026-09-21`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent (see appended section)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The live-credential wall (`detectOutboundCredential`) is the one outbound check
|
|
11
|
+
documented as non-overridable, but modern OpenAI project keys (`sk-proj-`, plus
|
|
12
|
+
`sk-svcacct-`, `sk-admin-`, `sk-None-`, and OpenRouter's `sk-or-v1-`) and GitHub
|
|
13
|
+
fine-grained tokens (`github_pat_`) passed it, and the LLM authority behind it
|
|
14
|
+
passed them too. This change adds two lookbehind-anchored patterns to the shared
|
|
15
|
+
`DURABLE_SECRET_PATTERNS` list (`src/core/durableSecretScrub.ts`), strips
|
|
16
|
+
invisible interleave characters inside the wall only
|
|
17
|
+
(`src/messaging/outbound-credential-guard.ts`), runs the wall ALONE on the two
|
|
18
|
+
send paths that deliberately skip the full outbound authority (proxy/system-
|
|
19
|
+
template Telegram replies; Agent-Health-lane attention items) via a new
|
|
20
|
+
`refuseIfCredential` helper in `src/server/routes.ts`, brings eight hand-kept
|
|
21
|
+
pattern copies up to the modern shapes in each file's own convention
|
|
22
|
+
(`ContentClassifier`, `redactUrl`, `openConversationBrief`, `ExecutionJournal`,
|
|
23
|
+
`scripts/audit-secret-patterns.mjs`, `PromiseBeacon`, `ClaimObservation`,
|
|
24
|
+
`testAsSelfValidation`, and the embedded `post-action-reflection` hook template in
|
|
25
|
+
`PostUpdateMigrator`), corrects the list's stale "ORDER MATTERS" comment, and
|
|
26
|
+
linearises two PRE-EXISTING quadratic patterns (`jwt`, `url-embedded-credential`)
|
|
27
|
+
that the spec's new falsifiable timing fixture exposed.
|
|
28
|
+
|
|
29
|
+
Spec: `docs/specs/outbound-credential-wall-modern-key-formats.md` (converged in 3
|
|
30
|
+
rounds, cross-model `codex-cli:gpt-6-astra`; approved by Justin 2026-09-21).
|
|
31
|
+
|
|
32
|
+
**Scope note versus the approved spec.** The spec deferred the `jwt` quadratic to
|
|
33
|
+
ACT-029 with a temporary timing exemption. During the build, the spec's own new
|
|
34
|
+
unbroken-run fixture found a SECOND quadratic pattern, `url-embedded-credential`,
|
|
35
|
+
which — unlike `jwt` — is a hard-wall kind and therefore runs on every outbound
|
|
36
|
+
message (2.4 s at 64 KB; minutes at the 1 MB scan ceiling). Both turned out to
|
|
37
|
+
have a one-token, detection-preserving fix (cap the unbounded leading quantifier),
|
|
38
|
+
so both are fixed here and the exemption machinery the spec described is not
|
|
39
|
+
built. This is strictly smaller and safer than the spec's plan; ACT-029 closes
|
|
40
|
+
with this change.
|
|
41
|
+
|
|
42
|
+
## Decision-point inventory
|
|
43
|
+
|
|
44
|
+
- `detectOutboundCredential` (outbound live-credential wall) — **modify** — covers
|
|
45
|
+
six more issued key shapes; normalises invisible characters before matching.
|
|
46
|
+
- Telegram reply route, proxy / system-template skip — **modify** — the wall now
|
|
47
|
+
runs before the skip; the skip continues to bypass tone judgment exactly as
|
|
48
|
+
before.
|
|
49
|
+
- `POST /attention`, Agent-Health lane — **modify** — the wall now runs before the
|
|
50
|
+
lane branch; the lane continues to bypass the per-topic tone/jargon authority.
|
|
51
|
+
- Durable-output redaction (all `scrubForStore` consumers) — **modify** — redacts
|
|
52
|
+
the six new shapes; two existing patterns made linear with identical detection.
|
|
53
|
+
- Eight pattern copies — **modify** — each gains the modern shapes in its own
|
|
54
|
+
convention; none gains blocking authority it did not already have.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 1. Over-block
|
|
59
|
+
|
|
60
|
+
- **A long placeholder in the issued shape** (`sk-proj-` + 32 `X`s) is refused, by
|
|
61
|
+
design — the wall asserts shape, not liveness. Pinned by a unit test as the
|
|
62
|
+
intended behaviour.
|
|
63
|
+
- **A hyphen-preceded kebab tail** such as `x-sk-proj-<32+ kebab chars>` is refused
|
|
64
|
+
(the lookbehind rejects only letter/digit-joined text). Separator-joined real
|
|
65
|
+
keys are plausible, so this is the chosen direction; pinned by a unit test.
|
|
66
|
+
- Measured: over ~366 MB of real text on this machine (every stored Telegram
|
|
67
|
+
message, every tracked repo file, server logs, every JSON/JSONL state store),
|
|
68
|
+
the two new patterns match **zero** strings through the full shipped pipeline.
|
|
69
|
+
Prose that names the formats (`sk-proj-`, `github_pat_` with no body) and
|
|
70
|
+
letter-joined kebab slugs (`task-proj-…`, `desk-proj-…`) pass — pinned by tests.
|
|
71
|
+
- The `jwt`/`url` caps change no verdict on any tested input (equivalence probes
|
|
72
|
+
in the unit suite, including a digit-prefixed scheme and a 200-char run).
|
|
73
|
+
|
|
74
|
+
## 2. Under-block
|
|
75
|
+
|
|
76
|
+
- **A key glued directly to a letter or digit** (`ENVsk-proj-…`) is not matched —
|
|
77
|
+
the accepted cost of the lookbehind that keeps kebab slugs from being refused.
|
|
78
|
+
- **Cross-message, re-encoded (base64 etc.) or visibly interleaved keys** pass — a
|
|
79
|
+
per-message shape wall cannot see them; recorded in the spec's Known bypasses.
|
|
80
|
+
- **Combining-mark interleaves outside the stripped set** (`\p{Cf}`, U+034F,
|
|
81
|
+
U+FE00–U+FE0F) pass the wall.
|
|
82
|
+
- **Unwalled send surfaces remain**: `/publish`, `/view`, `/messages/send`,
|
|
83
|
+
`threadline_send`, the browser broker, and iMessage's text-less token path.
|
|
84
|
+
Tracked as ACT-028 (due 2026-10-11) with a per-surface decision required.
|
|
85
|
+
- **The scrubber does not strip invisible characters** (its spans are offsets
|
|
86
|
+
into the original text), so an interleaved key refused outbound can still
|
|
87
|
+
persist unredacted in a durable store. Tracked under ACT-027.
|
|
88
|
+
- **Three agent-installed copies** (`serendipity-capture.sh` template, the
|
|
89
|
+
`credential-leak-detector` skill content, and the remainder of a ~25-file
|
|
90
|
+
census) need migration machinery and are ACT-027 (due 2026-10-11).
|
|
91
|
+
|
|
92
|
+
## 3. Level-of-abstraction fit
|
|
93
|
+
|
|
94
|
+
Correct layer. The wall is a deterministic floor that runs before the LLM
|
|
95
|
+
authority precisely so it holds when no verdict is available (provider outage,
|
|
96
|
+
spawn-cap saturation). The two new route call sites reuse that same floor rather
|
|
97
|
+
than inventing a parallel check; `refuseIfCredential` is the wall and nothing
|
|
98
|
+
else, and returns the identical 422 body the in-funnel wall returns.
|
|
99
|
+
|
|
100
|
+
## 4. Signal vs authority compliance
|
|
101
|
+
|
|
102
|
+
Compliant under the documented exemption. `docs/signal-vs-authority.md` names
|
|
103
|
+
"hard-invariant validation" and "safety guards on irreversible actions" as the
|
|
104
|
+
cases where a deterministic check holds authority; a leaked credential cannot be
|
|
105
|
+
un-leaked, and a vendor-assigned literal prefix is structured data, not meaning.
|
|
106
|
+
No brittle check gains new blocking authority: the added patterns extend an
|
|
107
|
+
existing invariant's coverage, and the route changes apply the existing invariant
|
|
108
|
+
to paths that skipped it as an unintended side effect of skipping tone judgment.
|
|
109
|
+
|
|
110
|
+
## 5. Interactions
|
|
111
|
+
|
|
112
|
+
- **Shadows / is shadowed by:** on the normal reply path the wall still runs
|
|
113
|
+
inside `evaluateOutbound`; the new pre-skip call only fires when `isProxy` or a
|
|
114
|
+
system template is set, so there is no double-evaluation on the normal path.
|
|
115
|
+
- **System templates** are fully anchored regexes with closed placeholder classes
|
|
116
|
+
(`{duration}`, `{category}`, `{short_id}`), so a system-template send can never
|
|
117
|
+
contain a credential; the new check is inert there by construction (kept as
|
|
118
|
+
defence-in-depth should a template ever gain free text).
|
|
119
|
+
- **willRelay** hand-offs are deliberately unchanged: the lease holder's receive
|
|
120
|
+
path re-enters the full authority, which includes the wall.
|
|
121
|
+
- **Redaction consumers** (`DurableOutputScrubber`, `MessagingToneGate` provenance,
|
|
122
|
+
`FeedbackDrainService`, `MutualSshHealthController`, `GoalRealignment`, two
|
|
123
|
+
direct `scrubForStore` call sites) gain the new shapes; the zero-match corpus
|
|
124
|
+
measurement says no existing stored or outgoing text changes.
|
|
125
|
+
- **Timing:** the linearised `jwt`/`url` patterns remove a latent event-loop stall
|
|
126
|
+
on the tone gate's twice-per-message scrub and on the wall itself.
|
|
127
|
+
|
|
128
|
+
## 6. External surfaces
|
|
129
|
+
|
|
130
|
+
- A message carrying one of the six shapes now returns `422
|
|
131
|
+
credential-exposure-guard` instead of being delivered — the intended change,
|
|
132
|
+
visible to the sending agent, which is told to refer to the key by name.
|
|
133
|
+
- Laned attention items and proxy replies carrying a key now get the same 422.
|
|
134
|
+
- No change to any other agent, any config, or any external service. No timing
|
|
135
|
+
dependency.
|
|
136
|
+
|
|
137
|
+
## 7. Multi-machine posture
|
|
138
|
+
|
|
139
|
+
`unified`. Everything changed is code constants and code paths shipped identically
|
|
140
|
+
to every machine by the release. No state, no replication, no URLs. During a
|
|
141
|
+
rolling update an old-version machine still passes the new shapes until it
|
|
142
|
+
updates; that window is owned by the version-skew machinery.
|
|
143
|
+
|
|
144
|
+
## 8. Rollback cost
|
|
145
|
+
|
|
146
|
+
Revert the commit. No configuration, migration or stored state to unwind. The
|
|
147
|
+
`post-action-reflection` hook reverts on the next migration run (always-overwrite).
|
|
148
|
+
Redactions already applied to durable stores while the patterns were live stay
|
|
149
|
+
redacted — irreversible by the parent standard's design, and the intended
|
|
150
|
+
direction for a security pattern.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Second-pass review
|
|
155
|
+
|
|
156
|
+
**Reviewer:** independent reviewer subagent, 2026-09-21.
|
|
157
|
+
|
|
158
|
+
**Verdict: Concur with the review.** The reviewer checked every artifact claim against
|
|
159
|
+
the diff and running code: identical 422 body, no double-evaluation on either route
|
|
160
|
+
(the reply-route guard and `checkOutboundMessage` are mutually exclusive; laned vs
|
|
161
|
+
non-laned attention items take disjoint paths), the rendered `PostUpdateMigrator`
|
|
162
|
+
template regex, the lookbehind on real key shapes (bare, quoted, `KEY=`, `MY_KEY_`,
|
|
163
|
+
backtick all matched; only letter/digit-glued missed, as documented), the `u` flag
|
|
164
|
+
and raw-length-first ordering, and the `url` cap (no verdict change — the unanchored
|
|
165
|
+
regex still starts inside an overlong scheme).
|
|
166
|
+
|
|
167
|
+
Two nuances the reviewer raised, both acted on:
|
|
168
|
+
|
|
169
|
+
1. **Stripping could glue a key to a preceding letter** (`ENV<ZWSP>ghp_…`): the raw
|
|
170
|
+
text has a word boundary, the stripped text does not, so a stripped-only scan
|
|
171
|
+
would have been a small regression against main for the older `\b`-anchored
|
|
172
|
+
patterns. **Fixed before commit:** the wall now scans the raw text and, when it
|
|
173
|
+
differs, the stripped text; a regression test pins the separated-key case.
|
|
174
|
+
2. **The `jwt` cap is linear with a constant:** a 2,000-character dash-free header
|
|
175
|
+
segment matches under the old regex and not the new one (probability ~1e-7 for
|
|
176
|
+
base64url text, and `jwt` is scrub-only, not a wall kind), and a dash-dense 1 MB
|
|
177
|
+
input still costs ~2.2 s of scrubbing — bounded, not minutes. Accepted and
|
|
178
|
+
recorded here rather than claimed as free.
|