Skip to content

Commit 344579f

Browse files
V37 Gate 10: Harden conversation redaction scanning
Replace unbounded private-key PEM regex redaction in conversation persistence and telemetry with bounded string scanning. Add closed and unclosed PEM coverage, update V37 promotion/security documentation, and refresh the promotion readiness artifact.\n\nChecks run: pnpm --filter @bitcode/api test -- --runTestsByPath src/conversations/__tests__/privacy.test.ts src/conversations/__tests__/telemetry.test.ts; pnpm --filter @bitcode/api build; pnpm run check:v37-gate7; pnpm run check:v37-gate8; pnpm run check:v37-gate10; pnpm run check:v37-promotion-readiness; git diff --check; git diff --cached --check.
1 parent 0561471 commit 344579f

12 files changed

Lines changed: 141 additions & 12 deletions

.bitcode/v37-promotion-readiness-report.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@
346346
{
347347
"relativePath": "BITCODE_SPEC_V37.md",
348348
"present": true,
349-
"digest": "sha256:865e5e929cc7a4d51e1009519910c31cc8b8402ceace11e76a609f93afc27251",
349+
"digest": "sha256:43a927465914e8f435d65e195d53ef63a7f9e8ba11ee1733400d0a3c6b7bf3f3",
350350
"requiredTokens": [
351351
{
352352
"token": "V37 promotion readiness canon",
@@ -365,7 +365,7 @@
365365
{
366366
"relativePath": "BITCODE_SPEC_V37_DELTA.md",
367367
"present": true,
368-
"digest": "sha256:2084f8e4fda4ae284fe41e680afc4af32051f3af4c0766d7733941bc00644887",
368+
"digest": "sha256:5474db49b715401c294cf044d513e7d8e62763834289265e841c17d341a61873",
369369
"requiredTokens": [
370370
{
371371
"token": "Gate 10: V37 Promotion Readiness",
@@ -384,7 +384,7 @@
384384
{
385385
"relativePath": "BITCODE_SPEC_V37_NOTES.md",
386386
"present": true,
387-
"digest": "sha256:9efc013a721a4f010f7f5328ffd673f6cef8860f6b2a1e0584156db831e0ef41",
387+
"digest": "sha256:42bbb9d3e77377dd2ddda7935a29d0fa4f175942c2fcc790b3b607da295e0933",
388388
"requiredTokens": [
389389
{
390390
"token": "Gate 10: V37 Promotion Readiness",
@@ -403,7 +403,7 @@
403403
{
404404
"relativePath": "BITCODE_SPEC_V37_PARITY_MATRIX.md",
405405
"present": true,
406-
"digest": "sha256:ee9848b1b851fe7ead17bd2267ffeb96f5150e40435d929698d7fe29f09c1959",
406+
"digest": "sha256:07f237b864378366485334fadd62a21f227299e0baaa90dc74e39709a08f0a24",
407407
"requiredTokens": [
408408
{
409409
"token": "## Gate 10 Parity",
@@ -422,7 +422,7 @@
422422
{
423423
"relativePath": "SPECIFICATIONS_ROADMAP.md",
424424
"present": true,
425-
"digest": "sha256:6cf0fa3085d6b3467fe2762a322b578f6b97d0532e7386d95060f023e32f0d93",
425+
"digest": "sha256:b3a91a0c5c29fc125b8f66ef1f5920c46a14a8c8c61bd645cf7a0138c9f0d1d4",
426426
"requiredTokens": [
427427
{
428428
"token": "V37 Gate 10 closure anchor",
@@ -437,7 +437,7 @@
437437
{
438438
"relativePath": "README.md",
439439
"present": true,
440-
"digest": "sha256:b755fce70d06ee22b4871135816a8fd9ea2632c0b08c7992ca215a0166ccecac",
440+
"digest": "sha256:acad3ea210eed868af76747907274ef8a03206ac6dc6b9ea8c254a7fed3a8fdc",
441441
"requiredTokens": [
442442
{
443443
"token": "check:v37-gate10",

BITCODE_SPEC_V37.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ operator private notes, unpaid AssetPack source, ledger write authority, and
214214
wallet signing authority before durable storage, telemetry, export, replay, or
215215
incident repair.
216216

217+
Private-key PEM material is redacted by bounded string scanning before generic
218+
secret pattern replacement. Conversation persistence and telemetry redaction
219+
must not depend on unbounded multiline regular expressions for private-key
220+
blocks; unclosed PEM-shaped input is treated as secret material through the end
221+
of the text.
222+
217223
Gate 7 covers seven persistence operations: persist message, restore history,
218224
export history, delete history, retain history, replay history, and incident
219225
repair. Export may only include source-safe data visible to the requesting
@@ -321,6 +327,9 @@ source-unsafe, stale, or disconnected from source evidence; when workflow,
321327
promotion, spec-family, runtime, or proven-generator support is missing; or
322328
when any value-bearing mainnet, credential, protected source, raw protected
323329
prompt, unpaid AssetPack source, or wallet private material is admitted.
330+
Promotion security scanning is also a closing signal: CodeQL or equivalent
331+
static-analysis alerts against source-safe redaction, persistence, telemetry,
332+
or promotion paths must be repaired before the V37 version branch can promote.
324333

325334
Gate 10 adds `generate:v37-promotion-readiness`,
326335
`check:v37-promotion-readiness`, `check:v37-gate10`, package tests for

BITCODE_SPEC_V37_DELTA.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ V37 / draft V38 posture preparation, package tests, workflow wiring, and
230230
Closure acceptance:
231231

232232
- V37 promotion checks validate all Conversations artifacts, contracts, UI proof, telemetry/docs/runbook bindings, privacy/redaction evidence, handoff evidence, rehearsal proof, and generated proof appendix support;
233+
- V37 promotion security checks require persistence and telemetry redaction to use bounded private-key PEM scanning rather than unbounded multiline regular expressions, with closed and unclosed PEM-shaped payloads covered by API tests;
233234
- promotion scripts support V37 command planning, dry-run, generated proof output, and derived promotion commit body generation;
234235
- promotion rewrites runtime posture to active V37 / draft V38 only after validations pass.
235236

BITCODE_SPEC_V37_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ redacted. Export is source-safe, delete leaves only a tombstone proof,
135135
retention never escalates visibility, replay uses prompt template ids and
136136
parsed result shapes, and incident repair operates over proof roots and
137137
redaction verdicts.
138+
Private-key PEM material is handled by bounded string scanning in the shared
139+
conversation redaction path so malformed or unclosed private-key-shaped input
140+
is redacted without polynomial multiline regular-expression behavior.
138141

139142
Gate 8 adds `ConversationTelemetryProofHooks` so conversation sessions,
140143
messages, streams, tools, source selectors, Terminal handoffs, retries,
@@ -148,6 +151,8 @@ panel ids, runbook ids, and redacted error classes only. Protected prompts,
148151
protected model responses, protected source payloads, provider tokens, wallet
149152
private material, settlement private payloads, unpaid AssetPack source,
150153
ledger write authority, and wallet signing authority remain forbidden.
154+
Telemetry uses the same bounded private-key redaction before truncation so
155+
large secret-shaped metadata cannot survive as a preview artifact.
151156

152157
Gate 9 closure adds `ConversationRehearsal` so local and staging-testnet
153158
Conversations are rehearsed before promotion readiness. The package source

BITCODE_SPEC_V37_PARITY_MATRIX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ Gate 9 exact rehearsal statement: local and staging-testnet rehearsals exercise
289289
| Promotion automation supports V37 | `scripts/promote-bitcode-canon.mjs`, `scripts/prepare-bitcode-spec-family-promotion.mjs`, `scripts/prepare-bitcode-runtime-canon-promotion.mjs`, and `.github/workflows/v37-canon-promotion.yml` | closed |
290290
| Runtime posture is promotable | V37 promotion rewrites V36 active / V37 draft to V37 active / draft V38 only after validation | closed |
291291
| Promotion payloads are source-safe | credentials, protected source, raw protected prompts, unpaid AssetPack source, wallet private material, ledger write authority, and wallet signing authority are forbidden | closed |
292+
| Static security findings block promotion | Conversation persistence and telemetry private-key redaction use bounded scanning with API tests for closed and unclosed PEM-shaped input | closed |
292293
| Workflow and package tests are wired | `scripts/check-v37-gate10-promotion-readiness.mjs`, `.github/workflows/bitcode-gate-quality.yml`, `.github/workflows/bitcode-canon-quality.yml`, and `.github/workflows/v37-canon-promotion.yml` | closed |
293294

294295
## Inherited V36 implementation matrix

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ Conversation artifact, generated `BITCODE_SPEC_V37_PROVEN.md` support,
9696
preparation from V36 active / V37 draft to V37 active / draft V38 without
9797
serializing credentials, protected source, raw protected prompts, unpaid
9898
AssetPack source, or wallet private material.
99+
Promotion hardening also keeps Conversation persistence and telemetry redaction
100+
on bounded private-key PEM scanning with closed/unclosed PEM tests so static
101+
security findings block promotion instead of being waived.
99102
V36 Gate 2 anchors market-wide activity through the package-owned
100103
`ExchangeActivityBook` and the source-safe generated artifact
101104
`.bitcode/v36-exchange-activity-book.json`, including listing, bid, ask,

SPECIFICATIONS_ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
- V37 Gate 8 closure anchor: Website Conversations depth now owns package-backed `ConversationTelemetryProofHooks` source, deterministic `.bitcode/v37-conversation-telemetry-proof-hooks.json`, `source-safe-conversation-telemetry-proof-hooks-metadata`, session, message, stream, tool, source selector, Terminal handoff, retry, error, and completion telemetry families, dashboard panel ids, runbook ids, correlation ids, proof roots, telemetry roots, API stream proof hook metadata, source-safe UI telemetry preview, public docs, internal runbooks, package tests, API tests, UI tests, workflow wiring, and `check:v37-gate8`.
4949
- V37 Gate 9 closure anchor: Website Conversations depth now owns package-backed `ConversationRehearsal` source, deterministic `.bitcode/v37-conversation-rehearsal.json`, `source-safe-conversation-rehearsal-metadata`, local and staging-testnet rehearsals exercise chat, streaming, writing, source selector, Terminal handoff, restore, retry, redaction, and error flows, rehearsal logs/screenshots are source-safe, route/UI checks, telemetry roots, and value-bearing mainnet blocking are visible, fullscreen Rehearsal Proof UI, package tests, UI tests, workflow wiring, and `check:v37-gate9`.
5050
- V37 Gate 10 closure anchor: Website Conversations depth now owns package-backed `ConversationPromotionReadinessReport` source, deterministic `.bitcode/v37-promotion-readiness-report.json`, `source-safe-conversation-promotion-readiness-metadata`, source-safe coverage for all V37 Conversation artifacts, `BITCODE_SPEC_V37_PROVEN.md` generation support, promotion command dry-run support, `v37-canon-promotion.yml`, active V37 / draft V38 posture preparation, package tests, workflow wiring, and `check:v37-gate10`.
51+
- V37 promotion hardening anchor: Conversation persistence and telemetry redaction use bounded private-key PEM scanning with closed/unclosed PEM API tests so static security findings remain promotion-blocking rather than waived.
5152
- Purpose: concise running index of Bitcode/ENGI specification history, current work, and planned work.
5253

5354
This roadmap is not an active system specification.

packages/api/src/conversations/__tests__/privacy.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ describe('conversation persistence privacy redaction', () => {
4444
expect((redacted.value as Record<string, unknown>).token_type).toBe('source');
4545
});
4646

47+
it('redacts closed and unclosed private key PEM blocks without exposing key material', () => {
48+
const closedKey = [
49+
'before',
50+
'-----BEGIN PRIVATE KEY-----',
51+
'abc123',
52+
'-----END PRIVATE KEY-----',
53+
'after',
54+
].join('\n');
55+
const unclosedKey = `${'-----BEGIN PRIVATE KEY-----'.repeat(200)}untrusted-tail`;
56+
57+
const closed = redactConversationPersistenceValue({ note: closedKey });
58+
const unclosed = redactConversationPersistenceValue({ note: unclosedKey });
59+
60+
expect(JSON.stringify(closed.value)).toContain('[redacted:conversation-persistence-secret]');
61+
expect(JSON.stringify(closed.value)).not.toContain('abc123');
62+
expect(JSON.stringify(unclosed.value)).toContain('[redacted:conversation-persistence-secret]');
63+
expect(JSON.stringify(unclosed.value)).not.toContain('untrusted-tail');
64+
expect(closed.redactionApplied).toBe(true);
65+
expect(unclosed.redactionApplied).toBe(true);
66+
});
67+
4768
it('admits only envelopes with the full forbidden disclosure boundary', () => {
4869
const envelope = buildConversationPersistenceEnvelope({
4970
operationId: 'export_history',

packages/api/src/conversations/__tests__/telemetry.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,33 @@ describe('conversation telemetry proof hooks', () => {
4141
});
4242
});
4343

44+
it('redacts private key PEM-shaped telemetry metadata before truncation', () => {
45+
const hook = buildConversationTelemetryProofHook({
46+
eventFamily: 'session',
47+
eventKind: 'conversation.session.info',
48+
status: 'started',
49+
metadata: {
50+
pem: [
51+
'prefix',
52+
'-----BEGIN OPENSSH PRIVATE KEY-----',
53+
'abcdefghijklmnopqrstuvwxyz1234567890',
54+
'-----END OPENSSH PRIVATE KEY-----',
55+
'suffix',
56+
].join('\n'),
57+
unclosed: `${'-----BEGIN PRIVATE KEY-----'.repeat(200)}tail`,
58+
},
59+
});
60+
61+
const serialized = JSON.stringify(hook);
62+
expect(serialized).toContain('[redacted:conversation-telemetry-secret]');
63+
expect(serialized).not.toContain('abcdefghijklmnopqrstuvwxyz1234567890');
64+
expect(serialized).not.toContain('tail');
65+
expect(assertConversationTelemetryProofHookSourceSafe(hook)).toEqual({
66+
admitted: true,
67+
reason: 'source_safe_conversation_telemetry_proof_hook',
68+
});
69+
});
70+
4471
it('attaches telemetry proof hooks to conversation stream execution rows', () => {
4572
const event = buildConversationStreamEvent({
4673
eventKind: 'completion_decision',

packages/api/src/conversations/privacy.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as crypto from 'crypto';
22

3+
import { redactPemPrivateKeyBlocks } from './secret-redaction';
4+
35
export type ConversationPersistenceVisibilityTier =
46
| 'public'
57
| 'user_visible'
@@ -48,7 +50,6 @@ const SECRET_VALUE_PATTERNS = [
4850
new RegExp('eyJ[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}', 'gu'),
4951
/Bearer\s+[A-Za-z0-9._-]{16,}/giu,
5052
/(?:password|secret|token|private_key)\s*[:=]\s*\S{8,}/giu,
51-
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/gu,
5253
];
5354

5455
export const CONVERSATION_PERSISTENCE_MUST_NOT_EXPOSE = [
@@ -103,8 +104,9 @@ function shouldRedactKey(key: string) {
103104
}
104105

105106
export function redactConversationPersistenceText(value: string): ConversationPersistenceRedactionResult<string> {
106-
let redacted = value;
107-
let redactionApplied = false;
107+
const privateKeyRedaction = redactPemPrivateKeyBlocks(value, '[redacted:conversation-persistence-secret]');
108+
let redacted = privateKeyRedaction.value;
109+
let redactionApplied = privateKeyRedaction.redactionApplied;
108110

109111
for (const pattern of SECRET_VALUE_PATTERNS) {
110112
redacted = redacted.replace(pattern, () => {

0 commit comments

Comments
 (0)