leftblock 0.4.0__tar.gz → 0.4.2__tar.gz

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 (29) hide show
  1. {leftblock-0.4.0 → leftblock-0.4.2}/PKG-INFO +12 -17
  2. {leftblock-0.4.0 → leftblock-0.4.2}/README.md +11 -16
  3. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/cli.py +13 -5
  4. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/data/gitleaks.toml +24 -67
  5. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock.egg-info/PKG-INFO +12 -17
  6. {leftblock-0.4.0 → leftblock-0.4.2}/pyproject.toml +5 -5
  7. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_false_positives.py +9 -6
  8. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_kanbus_advice.py +10 -2
  9. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_sensitive_rules.py +149 -162
  10. {leftblock-0.4.0 → leftblock-0.4.2}/LICENSE +0 -0
  11. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/__init__.py +0 -0
  12. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/allow.py +0 -0
  13. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/bypass.py +0 -0
  14. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/core.py +0 -0
  15. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/hooks.py +0 -0
  16. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock/merge.py +0 -0
  17. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock.egg-info/SOURCES.txt +0 -0
  18. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock.egg-info/dependency_links.txt +0 -0
  19. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock.egg-info/entry_points.txt +0 -0
  20. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock.egg-info/requires.txt +0 -0
  21. {leftblock-0.4.0 → leftblock-0.4.2}/leftblock.egg-info/top_level.txt +0 -0
  22. {leftblock-0.4.0 → leftblock-0.4.2}/setup.cfg +0 -0
  23. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_branding.py +0 -0
  24. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_bypasses.py +0 -0
  25. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_commit_gate.py +0 -0
  26. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_default_config.py +0 -0
  27. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_install.py +0 -0
  28. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_local_install.py +0 -0
  29. {leftblock-0.4.0 → leftblock-0.4.2}/tests/test_recorded_overrides.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leftblock
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: LeftBlock: a pluggable inspection gate for Git commits
5
5
  License: MIT License
6
6
 
@@ -124,7 +124,7 @@ configures the integration; it does not install scanner binaries.
124
124
  In the intended Python environment:
125
125
 
126
126
  ```bash
127
- python -m pip install leftblock==0.4.0
127
+ python -m pip install leftblock==0.4.2
128
128
  python -m leftblock.cli --help
129
129
  ```
130
130
 
@@ -397,11 +397,11 @@ PEM private keys, and other standard secret formats.
397
397
 
398
398
  | Rule | What it catches |
399
399
  |---|---|
400
- | `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com` and `@example.*` placeholders are allowlisted) |
400
+ | `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com`, no-reply addresses and `@example.*` placeholders are allowlisted) |
401
401
  | `leftblock-home-path-posix` | Home-folder paths `/Users/<name>`, `/home/<name>` |
402
402
  | `leftblock-home-path-windows` | `C:\Users\<name>` and `\Users\<name>` |
403
- | `leftblock-session-uuid` | Potential session UUIDs, with narrow exceptions for canonical Kanbus structural fields and example values |
404
- | `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values |
403
+ | `leftblock-session-uuid` | Potential session UUIDs (not flagged in Kanbus board data; example values and the `.kanbus.yml` `project_id` line are also allowed) |
404
+ | `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values (not flagged in Kanbus board data) |
405
405
  | `leftblock-openai-key` | `sk-`, `sk-proj-`, `sk-svcacct-` OpenAI keys |
406
406
  | `leftblock-anthropic-key` | `sk-ant-` Anthropic keys |
407
407
  | `leftblock-eth-private-key` | Ethereum-style private keys (`0x` + 64 hex) |
@@ -417,18 +417,13 @@ since it may already be exposed. Findings in Kanbus board data
417
417
  those files and that the content should be fixed through `kbs` (for example
418
418
  `kbs update` or `kbs comment`) rather than by hand.
419
419
 
420
- Kanbus exceptions apply to complete identifier property lines in
421
- `project/issues/*.json` and `project/events/*.json`. Event actor attribution
422
- must occupy a root property line in the canonical two-space format.
423
- Coordination claim references must occupy the canonical payload depth.
424
- The legacy `project/.cache/index.json` allows only canonical issue ID/parent
425
- lines and issue-filename keys with numeric modification times; cached
426
- descriptions and comments still receive the normal checks.
427
- Provider session fields, nested actor values, prose and arbitrary arrays
428
- remain scanned. A project prefix alone grants no exception, and a second
429
- value on the same line cannot inherit one. These checks recognize syntax;
430
- they do not validate a full JSON schema or prove an identifier is public.
431
- Other secret rules still apply to values in the accepted fields.
420
+ Kanbus is the mandated task tracker, so identifiers in its board data (actor
421
+ ids, session UUIDs, and UUIDs in any field or prose) are not sensitive and never
422
+ block a `kbs` write. Board data means `project/issues/*.json`,
423
+ `project/events/*.json` and `project/.cache/index.json`; the exception is
424
+ limited to the session-UUID and actor-id rules, so personal emails, home paths
425
+ and API or wallet keys in those files are still detected, and the same content
426
+ in any other path is still flagged.
432
427
 
433
428
  ### Crypto wallet material: what is and is not flagged
434
429
 
@@ -91,7 +91,7 @@ configures the integration; it does not install scanner binaries.
91
91
  In the intended Python environment:
92
92
 
93
93
  ```bash
94
- python -m pip install leftblock==0.4.0
94
+ python -m pip install leftblock==0.4.2
95
95
  python -m leftblock.cli --help
96
96
  ```
97
97
 
@@ -364,11 +364,11 @@ PEM private keys, and other standard secret formats.
364
364
 
365
365
  | Rule | What it catches |
366
366
  |---|---|
367
- | `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com` and `@example.*` placeholders are allowlisted) |
367
+ | `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com`, no-reply addresses and `@example.*` placeholders are allowlisted) |
368
368
  | `leftblock-home-path-posix` | Home-folder paths `/Users/<name>`, `/home/<name>` |
369
369
  | `leftblock-home-path-windows` | `C:\Users\<name>` and `\Users\<name>` |
370
- | `leftblock-session-uuid` | Potential session UUIDs, with narrow exceptions for canonical Kanbus structural fields and example values |
371
- | `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values |
370
+ | `leftblock-session-uuid` | Potential session UUIDs (not flagged in Kanbus board data; example values and the `.kanbus.yml` `project_id` line are also allowed) |
371
+ | `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values (not flagged in Kanbus board data) |
372
372
  | `leftblock-openai-key` | `sk-`, `sk-proj-`, `sk-svcacct-` OpenAI keys |
373
373
  | `leftblock-anthropic-key` | `sk-ant-` Anthropic keys |
374
374
  | `leftblock-eth-private-key` | Ethereum-style private keys (`0x` + 64 hex) |
@@ -384,18 +384,13 @@ since it may already be exposed. Findings in Kanbus board data
384
384
  those files and that the content should be fixed through `kbs` (for example
385
385
  `kbs update` or `kbs comment`) rather than by hand.
386
386
 
387
- Kanbus exceptions apply to complete identifier property lines in
388
- `project/issues/*.json` and `project/events/*.json`. Event actor attribution
389
- must occupy a root property line in the canonical two-space format.
390
- Coordination claim references must occupy the canonical payload depth.
391
- The legacy `project/.cache/index.json` allows only canonical issue ID/parent
392
- lines and issue-filename keys with numeric modification times; cached
393
- descriptions and comments still receive the normal checks.
394
- Provider session fields, nested actor values, prose and arbitrary arrays
395
- remain scanned. A project prefix alone grants no exception, and a second
396
- value on the same line cannot inherit one. These checks recognize syntax;
397
- they do not validate a full JSON schema or prove an identifier is public.
398
- Other secret rules still apply to values in the accepted fields.
387
+ Kanbus is the mandated task tracker, so identifiers in its board data (actor
388
+ ids, session UUIDs, and UUIDs in any field or prose) are not sensitive and never
389
+ block a `kbs` write. Board data means `project/issues/*.json`,
390
+ `project/events/*.json` and `project/.cache/index.json`; the exception is
391
+ limited to the session-UUID and actor-id rules, so personal emails, home paths
392
+ and API or wallet keys in those files are still detected, and the same content
393
+ in any other path is still flagged.
399
394
 
400
395
  ### Crypto wallet material: what is and is not flagged
401
396
 
@@ -348,11 +348,18 @@ RULE_HINTS = {
348
348
  bypass.MARKER_RULE: "Remove the marker and fix the content, or record a reasoned override.",
349
349
  bypass.IGNORE_RULE: "Don't suppress findings silently; record a reasoned override per finding.",
350
350
  }
351
- KANBUS_HINT = (
352
- "This file is written by Kanbus; fix the content through kbs "
353
- "(e.g. kbs update or comment), don't hand-edit it."
351
+ KANBUS_ISSUE_HINT = (
352
+ "This issue file is written by Kanbus; fix the content with kbs update "
353
+ "(or kbs comment), don't hand-edit it."
354
354
  )
355
- KANBUS_DATA = re.compile(r"(^|/)project/(issues|events)/[^/]+\.json$")
355
+ # Events are append-only history: kbs update/comment add a new event and leave
356
+ # the flagged text in place.
357
+ KANBUS_EVENT_HINT = (
358
+ "This Kanbus event is append-only history and can't be edited. If the issue "
359
+ "isn't committed yet, run kbs delete <issue id> and recreate it without the "
360
+ "content; otherwise ask the board owner."
361
+ )
362
+ KANBUS_DATA = re.compile(r"(^|/)project/(issues|events)/[^/]+\.jsonl?$")
356
363
 
357
364
 
358
365
  def print_failures(failures):
@@ -377,7 +384,8 @@ def print_failures(failures):
377
384
  if rule in RULE_HINTS:
378
385
  print(" Hint: " + RULE_HINTS[rule], file=sys.stderr)
379
386
  for name in kanbus_files:
380
- print(f" {_plain(name)}: {KANBUS_HINT}", file=sys.stderr)
387
+ hint = KANBUS_EVENT_HINT if KANBUS_DATA.search(name).group(2) == "events" else KANBUS_ISSUE_HINT
388
+ print(f" {_plain(name)}: {hint}", file=sys.stderr)
381
389
  else:
382
390
  print_warn(str(result.findings))
383
391
 
@@ -21,9 +21,10 @@
21
21
  # indistinguishable from SHA-256 and is intentionally left to the
22
22
  # generic-api-key keyword context (see docs).
23
23
  #
24
- # Allowlists are scoped to exact structural values and field names
25
- # (placeholder usernames/actor ids, Kanbus structural id fields, the
26
- # 00000000-0000-4000-8000-... example UUID shape) — never whole paths.
24
+ # Allowlists are scoped to exact values (placeholder usernames/actor ids,
25
+ # the 00000000-0000-4000-8000-... example UUID shape, the .kanbus.yml
26
+ # project_id line). The one path-wide exception is Kanbus board data, whose
27
+ # identifiers are not sensitive for the session-uuid and actor-id rules only.
27
28
 
28
29
  title = "LeftBlock sensitive-data rules"
29
30
 
@@ -62,6 +63,8 @@ keywords = ["@"]
62
63
  regexes = [
63
64
  # Infrastructure and placeholder identities, not personal ones.
64
65
  '''@users\.noreply\.github\.com''',
66
+ # No-reply addresses (e.g. commit Co-Authored-By trailers) are not personal.
67
+ '''(^|[^a-zA-Z0-9._%+\-'])no-?reply@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}''',
65
68
  '''git@github\.com''',
66
69
  '''@(example|github)\.(com|org|net|io|ai)''',
67
70
  ]
@@ -106,64 +109,19 @@ regexes = [
106
109
  '''^11111111-1111-4111-8111-1111111111[0-9a-f]{2}$''',
107
110
  ]
108
111
 
109
- # Kanbus serializes board records as indented JSON. Accept a complete
110
- # structural field on its own line in board issue/event files. Prefixes
111
- # alone, prose, array elements and a second value on that line do not
112
- # qualify. This is a syntax-based exception, not JSON schema validation.
113
- # Gitleaks includes the preceding newline in its line context; permit that
114
- # single boundary byte, plus CRLF endings, without spanning properties.
112
+ # Kanbus board data (issues, events and the derived cache index) is written by
113
+ # Kanbus and is the mandated tracker in these repositories. Its identifiers,
114
+ # including actor ids and session UUIDs, are not treated as sensitive
115
+ # (decision 2026-10-20), so LeftBlock never blocks a kbs write on them. Emails,
116
+ # home paths and keys in board files are still detected by their own rules.
115
117
  [[rules.allowlists]]
116
- description = "Kanbus board structural identifier fields"
117
- condition = "AND"
118
+ description = "Kanbus board data identifiers"
118
119
  paths = [
119
120
  '''(^|/)project/(issues|events)/[^/]+\.json$''',
120
- ]
121
- regexTarget = "line"
122
- regexes = [
123
- '''^\n?[ \t]*"(id|comment_id|event_id|issue_id|parent_id|parent|target|target_id|from|to)"[ \t]*:[ \t]*"(?:[A-Za-z][A-Za-z0-9_]*-)*[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
124
- ]
125
-
126
- # Event actor attribution is structural too. Keep this separate from
127
- # session_id/provider_session_id/resumed_session, which remain detected.
128
- [[rules.allowlists]]
129
- description = "Kanbus event UUID actor attribution"
130
- condition = "AND"
131
- paths = [
132
- '''(^|/)project/events/[^/]+\.json$''',
133
- ]
134
- regexTarget = "line"
135
- regexes = [
136
- '''^\n? "actor_id"[ \t]*:[ \t]*"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
137
- ]
138
-
139
- # Coordination events refer to a generated claim by its prefixed key.
140
- # Require the canonical payload-property depth; bare session values,
141
- # root/deeper properties, prose and neighboring values are not exempt.
142
- [[rules.allowlists]]
143
- description = "Kanbus coordination payload claim reference"
144
- condition = "AND"
145
- paths = [
146
- '''(^|/)project/events/[^/]+\.json$''',
147
- ]
148
- regexTarget = "line"
149
- regexes = [
150
- '''^\n? "claim_id"[ \t]*:[ \t]*"(?:[A-Za-z][A-Za-z0-9_]*-)+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
151
- ]
152
-
153
- # The legacy Kanbus cache duplicates issue identifiers and stores numeric
154
- # mtimes keyed by issue filename. Do not exempt the cache file: it also
155
- # contains full descriptions and comments that still require scanning.
156
- [[rules.allowlists]]
157
- description = "Kanbus cache issue identifiers and numeric filename mtimes"
158
- condition = "AND"
159
- paths = [
121
+ # Kanbus's LLM usage log (one JSON record per line).
122
+ '''(^|/)project/events/[^/]+\.jsonl$''',
160
123
  '''(^|/)project/\.cache/index\.json$''',
161
124
  ]
162
- regexTarget = "line"
163
- regexes = [
164
- '''^\n? "(id|parent)"[ \t]*:[ \t]*"(?:[A-Za-z][A-Za-z0-9_]*-)+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
165
- '''^\n? "(?:[A-Za-z][A-Za-z0-9_]*-)+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json"[ \t]*:[ \t]*-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?,?[ \t]*\r?$''',
166
- ]
167
125
 
168
126
  # Kanbus 0.40+ records a public per-project UUID v4 in .kanbus.yml so every
169
127
  # clone shares one realtime topic. Accept only the top-level YAML key with a
@@ -191,19 +149,18 @@ regexes = [
191
149
  '''^example[-_a-z0-9]*$''',
192
150
  ]
193
151
 
194
- # Kanbus event files record the local actor that made each board change
195
- # ("actor_id"). That is the board's attribution design, not a leaked user
196
- # id; scoped to project/events and a two-space root property line. Nested
197
- # payload actor values and attribution outside event files remain detected.
152
+ # Kanbus board data (issues, events and the derived cache index) is written by
153
+ # Kanbus and is the mandated tracker in these repositories. Its identifiers,
154
+ # including actor ids and session UUIDs, are not treated as sensitive
155
+ # (decision 2026-10-20), so LeftBlock never blocks a kbs write on them. Emails,
156
+ # home paths and keys in board files are still detected by their own rules.
198
157
  [[rules.allowlists]]
199
- description = "Kanbus board event actor attribution"
200
- condition = "AND"
158
+ description = "Kanbus board data identifiers"
201
159
  paths = [
202
- '''(^|/)project/events/[^/]+\.json$''',
203
- ]
204
- regexTarget = "line"
205
- regexes = [
206
- '''^\n? "actor_id"[ \t]*:[ \t]*"[^"]+",?[ \t]*\r?$''',
160
+ '''(^|/)project/(issues|events)/[^/]+\.json$''',
161
+ # Kanbus's LLM usage log (one JSON record per line).
162
+ '''(^|/)project/events/[^/]+\.jsonl$''',
163
+ '''(^|/)project/\.cache/index\.json$''',
207
164
  ]
208
165
 
209
166
  # --------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leftblock
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: LeftBlock: a pluggable inspection gate for Git commits
5
5
  License: MIT License
6
6
 
@@ -124,7 +124,7 @@ configures the integration; it does not install scanner binaries.
124
124
  In the intended Python environment:
125
125
 
126
126
  ```bash
127
- python -m pip install leftblock==0.4.0
127
+ python -m pip install leftblock==0.4.2
128
128
  python -m leftblock.cli --help
129
129
  ```
130
130
 
@@ -397,11 +397,11 @@ PEM private keys, and other standard secret formats.
397
397
 
398
398
  | Rule | What it catches |
399
399
  |---|---|
400
- | `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com` and `@example.*` placeholders are allowlisted) |
400
+ | `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com`, no-reply addresses and `@example.*` placeholders are allowlisted) |
401
401
  | `leftblock-home-path-posix` | Home-folder paths `/Users/<name>`, `/home/<name>` |
402
402
  | `leftblock-home-path-windows` | `C:\Users\<name>` and `\Users\<name>` |
403
- | `leftblock-session-uuid` | Potential session UUIDs, with narrow exceptions for canonical Kanbus structural fields and example values |
404
- | `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values |
403
+ | `leftblock-session-uuid` | Potential session UUIDs (not flagged in Kanbus board data; example values and the `.kanbus.yml` `project_id` line are also allowed) |
404
+ | `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values (not flagged in Kanbus board data) |
405
405
  | `leftblock-openai-key` | `sk-`, `sk-proj-`, `sk-svcacct-` OpenAI keys |
406
406
  | `leftblock-anthropic-key` | `sk-ant-` Anthropic keys |
407
407
  | `leftblock-eth-private-key` | Ethereum-style private keys (`0x` + 64 hex) |
@@ -417,18 +417,13 @@ since it may already be exposed. Findings in Kanbus board data
417
417
  those files and that the content should be fixed through `kbs` (for example
418
418
  `kbs update` or `kbs comment`) rather than by hand.
419
419
 
420
- Kanbus exceptions apply to complete identifier property lines in
421
- `project/issues/*.json` and `project/events/*.json`. Event actor attribution
422
- must occupy a root property line in the canonical two-space format.
423
- Coordination claim references must occupy the canonical payload depth.
424
- The legacy `project/.cache/index.json` allows only canonical issue ID/parent
425
- lines and issue-filename keys with numeric modification times; cached
426
- descriptions and comments still receive the normal checks.
427
- Provider session fields, nested actor values, prose and arbitrary arrays
428
- remain scanned. A project prefix alone grants no exception, and a second
429
- value on the same line cannot inherit one. These checks recognize syntax;
430
- they do not validate a full JSON schema or prove an identifier is public.
431
- Other secret rules still apply to values in the accepted fields.
420
+ Kanbus is the mandated task tracker, so identifiers in its board data (actor
421
+ ids, session UUIDs, and UUIDs in any field or prose) are not sensitive and never
422
+ block a `kbs` write. Board data means `project/issues/*.json`,
423
+ `project/events/*.json` and `project/.cache/index.json`; the exception is
424
+ limited to the session-UUID and actor-id rules, so personal emails, home paths
425
+ and API or wallet keys in those files are still detected, and the same content
426
+ in any other path is still flagged.
432
427
 
433
428
  ### Crypto wallet material: what is and is not flagged
434
429
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "leftblock"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "LeftBlock: a pluggable inspection gate for Git commits"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -27,14 +27,14 @@ leftblock = "leftblock.cli:main"
27
27
  version_toml = ["pyproject.toml:project.version"]
28
28
  commit_message = "chore(release): {version}"
29
29
  tag_format = "v{version}"
30
- # Keep the README install pin on the released version; NEW_VERSION is set by
31
- # semantic-release, and README.md is committed with the release.
30
+ # scripts/release_prepare.py keeps the README pin on NEW_VERSION and drops
31
+ # commit trailers from the generated changelog; both files are committed.
32
32
  build_command = """
33
- sed -i "s/leftblock==[0-9][0-9.]*/leftblock==$NEW_VERSION/" README.md
33
+ python scripts/release_prepare.py
34
34
  python -m pip install build
35
35
  python -m build
36
36
  """
37
- assets = ["README.md"]
37
+ assets = ["README.md", "CHANGELOG.md"]
38
38
 
39
39
  [tool.semantic_release.branches.main]
40
40
  match = "main"
@@ -73,12 +73,15 @@ class KanbusIdTests(unittest.TestCase):
73
73
  "notes.md": "see PPY-%s; session %s\n" % (_uuid(rng), _uuid(rng)),
74
74
  }))
75
75
 
76
- def test_prefixed_session_value_is_not_structural_bookkeeping(self):
77
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
78
- "project/events/synthetic.json": json.dumps({
79
- "session_id": "PPY-" + _uuid(_rng()),
80
- }, indent=2),
81
- }))
76
+ def test_session_value_with_prefix_passes_in_board_files_only(self):
77
+ doc = json.dumps({"session_id": "PPY-" + _uuid(_rng())}, indent=2)
78
+ self.assertEqual(
79
+ scan_and_rule_ids({"project/events/synthetic.json": doc}), [])
80
+ for path in ("src/synthetic.json", "project/other.json",
81
+ "project/events/sub/synthetic.json"):
82
+ with self.subTest(path=path):
83
+ self.assertIn("leftblock-session-uuid",
84
+ scan_and_rule_ids({path: doc}))
82
85
 
83
86
 
84
87
  def _uuid4(rng):
@@ -79,11 +79,19 @@ class KanbusBoardTests(RepositoryFixture):
79
79
  self.assertIn("leftblock-home-path-posix", proc.stderr)
80
80
  self.assertIn("Hint: Use a relative path, $HOME or ~ instead.", proc.stderr)
81
81
  self.assertEqual(
82
- proc.stderr.count("This file is written by Kanbus; fix the content through kbs "
83
- "(e.g. kbs update or comment), don't hand-edit it."), 1,
82
+ proc.stderr.count("This issue file is written by Kanbus; fix the content with kbs update"), 1,
84
83
  )
85
84
  self.assertNotIn(home_path, proc.stdout + proc.stderr)
86
85
 
86
+ def test_event_file_hint_says_delete_and_recreate_not_update(self):
87
+ self.write("project/events/2026-10-20T10-00-00.000Z__x.json",
88
+ '{\n "note": "see /' + 'Users/someone/notes"\n}\n')
89
+ proc = self.leftblock("check")
90
+ self.assertEqual(proc.returncode, 1)
91
+ self.assertIn("append-only history", proc.stderr)
92
+ self.assertIn("kbs delete <issue id>", proc.stderr)
93
+ self.assertNotIn("kbs update", proc.stderr)
94
+
87
95
  def test_hint_is_printed_once_per_rule(self):
88
96
  self.write("notes.md", "a /" + "Users/one/x\nb /" + "Users/two/y\n")
89
97
  proc = self.leftblock("check")
@@ -222,6 +222,17 @@ class SensitiveFixtureTests(unittest.TestCase):
222
222
  class NegativeTests(unittest.TestCase):
223
223
  """Public wallet addresses and hash-like hex must NOT be flagged."""
224
224
 
225
+ def test_no_reply_addresses_pass_but_lookalikes_still_flag(self):
226
+ at = "@"
227
+ self.assertEqual(scan_and_rule_ids({"CHANGELOG.md": "\n".join([
228
+ "Co-Authored-By: Bot <noreply" + at + "vendor.test>",
229
+ "From: no-reply" + at + "vendor.test",
230
+ ]) + "\n"}), [])
231
+ self.assertEqual(scan_and_rule_ids({"notes.md": "mail jo" + at + "vendor.test\n"}),
232
+ ["leftblock-personal-email"])
233
+ self.assertEqual(scan_and_rule_ids({"notes.md": "mail notnoreply" + at + "vendor.test\n"}),
234
+ ["leftblock-personal-email"])
235
+
225
236
  def test_public_wallet_addresses_and_hashes_not_flagged(self):
226
237
  rng = _rng()
227
238
  findings = scan_and_rule_ids({
@@ -332,172 +343,148 @@ def _uuid(rng):
332
343
 
333
344
  @unittest.skipUnless(GITLEAKS, "gitleaks not installed")
334
345
  class KanbusBoardAllowlistTests(unittest.TestCase):
335
- """Kanbus board bookkeeping passes; the same shapes elsewhere still fail."""
346
+ """Kanbus board data identifiers are not sensitive; nothing else changes.
347
+
348
+ Policy (2026-10-20): in board data, actor ids, session UUIDs and UUIDs in
349
+ any field or prose pass. Emails, home paths and keys are still detected
350
+ there, and the same content outside board data is still flagged.
351
+ """
352
+
353
+ BOARD_PATHS = (
354
+ "project/issues/PPY-x.json",
355
+ "project/events/2026-10-20T00-00-00.000Z__x.json",
356
+ "project/events/llm_usage.jsonl",
357
+ "project/.cache/index.json",
358
+ "sub/dir/project/issues/PPY-x.json",
359
+ "sub/dir/project/events/e.json",
360
+ "sub/dir/project/.cache/index.json",
361
+ )
362
+ NON_BOARD_PATHS = (
363
+ "notes.md",
364
+ "project/other.json",
365
+ "project/issues/sub/x.json",
366
+ "project/events/sub/x.json",
367
+ "project/events.json",
368
+ "project/issues/x.json.bak",
369
+ "project/issues/x.jsonl",
370
+ "src/project/issues-like/x.json",
371
+ "myproject/issues/x.json",
372
+ "project/.cache/other.json",
373
+ "project/.cache/sub/index.json",
374
+ )
336
375
 
337
376
  def setUp(self):
338
377
  self.rng = _rng()
339
378
 
340
- def test_prefixed_issue_ids_in_board_issue_files_pass(self):
341
- for prefix in ("PPY", "LIM", "blbs", "chatticus", "decision-flywheel"):
342
- iid = f"{prefix}-{_uuid(self.rng)}"
343
- doc = json.dumps({"id": iid, "parent": f"{prefix}-{_uuid(self.rng)}",
344
- "comments": [{"id": _uuid(self.rng), "text": "done"}]}, indent=2)
345
- self.assertEqual(scan_and_rule_ids({f"project/issues/{iid}.json": doc}), [], prefix)
346
-
347
- def test_event_actor_id_passes_only_in_board_events(self):
348
- event = json.dumps({"event_id": _uuid(self.rng), "issue_id": f"LIM-{_uuid(self.rng)}",
349
- "event_type": "issue_created", "actor" + "_id": "local" + "user"}, indent=2)
350
- self.assertEqual(scan_and_rule_ids({"project/events/2026-10-20T00-00-00.000Z__x.json": event}), [])
351
- self.assertIn("leftblock-actor-id", scan_and_rule_ids({"src/telemetry.json": event}))
352
- self.assertIn("leftblock-actor-id", scan_and_rule_ids({"project/issues/x.json": event}))
353
-
354
- def test_prefixed_ids_outside_board_files_still_flagged(self):
355
- doc = json.dumps({"id": f"LIM-{_uuid(self.rng)}"}, indent=2)
356
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({"src/state.json": doc}))
357
-
358
- def test_bare_uuid_in_board_free_text_still_flagged(self):
359
- doc = json.dumps({"id": f"PPY-{_uuid(self.rng)}",
360
- "description": f"session {_uuid(self.rng)} reproduced it"}, indent=2)
361
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({"project/issues/PPY-x.json": doc}))
362
-
363
- def test_event_uuid_actor_is_structural_only_in_event_attribution(self):
364
- actor = _uuid(self.rng)
365
- event = json.dumps({
366
- "schema_version": 1, "event_id": _uuid(self.rng),
367
- "issue_id": f"PPY-{_uuid(self.rng)}", "event_type": "comment_added",
368
- "occurred_at": "2026-10-20T00:00:00.000Z", "actor_id": actor,
369
- "payload": {"comment_id": _uuid(self.rng), "text": "Reviewed."},
370
- }, indent=2)
371
- self.assertEqual(scan_and_rule_ids({"project/events/synthetic.json": event}), [])
372
- for path in ("src/telemetry.json", "project/issues/synthetic.json"):
373
- with self.subTest(path=path):
374
- findings = scan_and_rule_ids({path: event})
375
- self.assertIn("leftblock-session-uuid", findings)
376
- self.assertIn("leftblock-actor-id", findings)
377
-
378
- def test_structural_id_does_not_hide_session_on_same_line(self):
379
- for prefix in ("PPY-", "BOTSPY-", "kanbus-", ""):
380
- with self.subTest(prefix=prefix):
381
- doc = json.dumps({
382
- "id": prefix + _uuid(self.rng),
383
- "session_id": _uuid(self.rng),
384
- })
385
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
386
- "project/issues/synthetic.json": doc,
387
- }))
388
-
389
- def test_session_uuid_fields_and_prose_remain_blocked_in_board_files(self):
390
- for field in ("session_id", "provider_session_id", "resumed_session", "description", "text"):
391
- for prefix in ("", "PPY-", "BOTSPY-", "kanbus-"):
392
- with self.subTest(field=field, prefix=prefix):
393
- doc = json.dumps({field: prefix + _uuid(self.rng)}, indent=2)
394
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
395
- "project/events/synthetic.json": doc,
396
- }))
397
-
398
- def test_structural_values_must_fill_the_field(self):
399
- value = _uuid(self.rng)
400
- for doc in (
401
- json.dumps({"id": "PPY-" + value + " extra"}, indent=2),
402
- json.dumps({"description": ['PPY-' + value]}, indent=2),
403
- json.dumps({"actor_id": value, "session_id": _uuid(self.rng)}),
404
- ):
405
- with self.subTest(doc_shape=len(doc)):
406
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
407
- "project/events/synthetic.json": doc,
408
- }))
409
-
410
- def test_all_structural_prefixes_still_flagged_outside_board_files(self):
411
- for prefix in ("", "PPY-", "BOTSPY-", "kanbus-"):
412
- with self.subTest(prefix=prefix):
413
- doc = json.dumps({"id": prefix + _uuid(self.rng)}, indent=2)
414
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
415
- "src/state.json": doc,
416
- }))
417
-
418
- def test_actor_attribution_does_not_allow_other_sensitive_rules(self):
419
- actor = "sk-proj-" + rand_openai_like(self.rng, 56)
420
- doc = json.dumps({"actor_id": actor}, indent=2)
421
- self.assertIn("leftblock-openai-key", scan_and_rule_ids({
422
- "project/events/synthetic.json": doc,
423
- }))
424
-
425
- def test_nested_actor_and_split_line_actor_remain_detected(self):
426
- nested = json.dumps({"payload": {"actor_id": _uuid(self.rng)}}, indent=2)
427
- findings = scan_and_rule_ids({"project/events/synthetic.json": nested})
428
- self.assertIn("leftblock-actor-id", findings)
429
- self.assertIn("leftblock-session-uuid", findings)
430
- split = '"actor_id"\n:\n"local' + 'user"'
431
- self.assertIn("leftblock-actor-id", scan_and_rule_ids({"src/state.json": split}))
432
-
433
- def test_example_or_cache_key_does_not_hide_another_uuid(self):
434
- example = "-".join(("00000000", "0000", "4000", "8000", "0123456789ab"))
435
- for first in (example, f"PPY-{_uuid(self.rng)}.json"):
436
- line = first + " session=" + _uuid(self.rng)
437
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
438
- "project/events/synthetic.json": line,
439
- }))
440
-
441
- def test_canonical_crlf_event_passes(self):
442
- event = json.dumps({
443
- "event_id": _uuid(self.rng), "actor_id": _uuid(self.rng),
444
- "issue_id": f"PPY-{_uuid(self.rng)}",
445
- }, indent=2).replace("\n", "\r\n")
446
- self.assertEqual(scan_and_rule_ids({"project/events/synthetic.json": event}), [])
447
-
448
- def test_coordination_claim_reference_passes_only_in_event_payload(self):
449
- claim = "claim-" + _uuid(self.rng)
450
- event = json.dumps({"event_type": "coordination.claim",
451
- "payload": {"claim_id": claim}}, indent=2)
452
- self.assertEqual(scan_and_rule_ids({"project/events/synthetic.json": event}), [])
453
- for path in ("project/issues/synthetic.json", "src/state.json"):
454
- with self.subTest(path=path):
455
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({path: event}))
456
- for doc in (
457
- json.dumps({"claim_id": claim}, indent=2),
458
- json.dumps({"payload": {"nested": {"claim_id": claim}}}, indent=2),
459
- json.dumps({"payload": {"claim_id": claim + " copied"}}, indent=2),
460
- json.dumps({"payload": {"session_id": claim}}, indent=2),
461
- json.dumps({"payload": {"claim_id": claim, "session_id": _uuid(self.rng)}}),
462
- json.dumps({"payload": {"claim_id": _uuid(self.rng)}}, indent=2),
463
- ):
464
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
465
- "project/events/synthetic.json": doc,
466
- }))
467
-
468
- def test_cache_bookkeeping_passes_without_exempting_cached_content(self):
379
+ def _content(self):
380
+ """Identifier-bearing content in many shapes (indented and minified)."""
381
+ u = lambda: _uuid(self.rng) # noqa: E731
382
+ actor_key = "actor" + "_id"
383
+ return {
384
+ "session_fields": json.dumps({
385
+ "session_id": u(), "provider_session_id": u(),
386
+ "resumed_session": u()}, indent=2),
387
+ "prose": json.dumps({
388
+ "description": "session " + u() + " reproduced it",
389
+ "text": "see PPY-" + u() + " and BOTSPY-" + u()}, indent=2),
390
+ "nested_actor": json.dumps(
391
+ {"payload": {actor_key: u()}}, indent=2),
392
+ "split_actor": '"%s"\n:\n"%s"' % (actor_key, u()),
393
+ "minified_event": json.dumps({
394
+ "event_id": u(), actor_key: u(), "session_id": u(),
395
+ "payload": {"comment_id": u(), "text": "x " + u()}}),
396
+ }
397
+
398
+ def test_identifiers_pass_in_every_board_path(self):
399
+ for path in self.BOARD_PATHS:
400
+ for shape, doc in self._content().items():
401
+ with self.subTest(path=path, shape=shape):
402
+ self.assertEqual(scan_and_rule_ids({path: doc}), [])
403
+
404
+ def test_session_style_fields_pass_with_any_prefix_in_board_files(self):
405
+ for path in self.BOARD_PATHS:
406
+ for field in ("session_id", "provider_session_id", "resumed_session"):
407
+ for prefix in ("", "PPY-", "BOTSPY-", "kanbus-"):
408
+ with self.subTest(path=path, field=field, prefix=prefix):
409
+ doc = json.dumps({field: prefix + _uuid(self.rng)}, indent=2)
410
+ self.assertEqual(scan_and_rule_ids({path: doc}), [])
411
+
412
+ def test_bare_and_prefixed_uuids_in_prose_pass_in_board_files(self):
413
+ for path in self.BOARD_PATHS:
414
+ for field in ("description", "text"):
415
+ for prefix in ("", "PPY-", "kanbus-"):
416
+ with self.subTest(path=path, field=field, prefix=prefix):
417
+ doc = json.dumps({
418
+ "id": "PPY-" + _uuid(self.rng),
419
+ field: "seen in %s%s today" % (prefix, _uuid(self.rng)),
420
+ }, indent=2)
421
+ self.assertEqual(scan_and_rule_ids({path: doc}), [])
422
+
423
+ def test_actor_ids_pass_nested_split_and_minified_in_board_files(self):
424
+ key = "actor" + "_id"
425
+ for value in (_uuid(self.rng), "local" + "user"):
426
+ shapes = {
427
+ "root": json.dumps({key: value}, indent=2),
428
+ "nested": json.dumps({"payload": {key: value}}, indent=2),
429
+ "split": '"%s"\n:\n"%s"' % (key, value),
430
+ "minified": json.dumps({"event_type": "x", key: value}),
431
+ "same_line_session": json.dumps(
432
+ {key: value, "session_id": _uuid(self.rng)}),
433
+ }
434
+ for path in self.BOARD_PATHS:
435
+ for shape, doc in shapes.items():
436
+ with self.subTest(path=path, shape=shape):
437
+ self.assertEqual(scan_and_rule_ids({path: doc}), [])
438
+
439
+ def test_canonical_crlf_event_and_cache_pass(self):
469
440
  iid = "PPY-" + _uuid(self.rng)
470
- cache = {
441
+ event = json.dumps({
442
+ "event_id": _uuid(self.rng), "actor" + "_id": _uuid(self.rng),
443
+ "issue_id": iid}, indent=2).replace("\n", "\r\n")
444
+ self.assertEqual(scan_and_rule_ids({"project/events/e.json": event}), [])
445
+ cache = json.dumps({
471
446
  "file_mtimes": {iid + ".json": 1792500000.25},
472
- "issues": [{"id": iid, "parent": "PPY-" + _uuid(self.rng)}],
447
+ "issues": [{"id": iid, "description": "s " + _uuid(self.rng),
448
+ "session_id": _uuid(self.rng)}]}, indent=2)
449
+ self.assertEqual(scan_and_rule_ids({"project/.cache/index.json": cache}), [])
450
+
451
+ def test_same_identifiers_still_flagged_outside_board_paths(self):
452
+ for path in self.NON_BOARD_PATHS:
453
+ for shape, doc in self._content().items():
454
+ with self.subTest(path=path, shape=shape):
455
+ findings = scan_and_rule_ids({path: doc})
456
+ self.assertIn("leftblock-session-uuid", findings)
457
+
458
+ def test_actor_ids_still_flagged_outside_board_paths(self):
459
+ key = "actor" + "_id"
460
+ doc = json.dumps({key: "local" + "user"}, indent=2)
461
+ split = '"%s"\n:\n"%s"' % (key, "local" + "user")
462
+ for path in self.NON_BOARD_PATHS:
463
+ for shape, content in (("root", doc), ("split", split)):
464
+ with self.subTest(path=path, shape=shape):
465
+ self.assertIn("leftblock-actor-id",
466
+ scan_and_rule_ids({path: content}))
467
+
468
+ def test_personal_email_home_path_and_key_still_flagged_in_board_files(self):
469
+ email = "jane." + "doe@" + "example-corp." + "t" + "est"
470
+ user = "jd" + "oe"
471
+ cases = {
472
+ "leftblock-personal-email": json.dumps(
473
+ {"assignee": email}, indent=2),
474
+ "leftblock-home-path-posix": json.dumps(
475
+ {"description": "see /Users/%s/work" % user}, indent=2),
476
+ "leftblock-openai-key": json.dumps(
477
+ {"text": "key sk-proj-" + rand_openai_like(self.rng, 56)},
478
+ indent=2),
473
479
  }
474
- doc = json.dumps(cache, indent=2)
475
- path = "project/.cache/index.json"
476
- self.assertEqual(scan_and_rule_ids({path: doc}), [])
477
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({"src/index.json": doc}))
478
- for field in ("id", "parent"):
479
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
480
- path: json.dumps({field: iid}, indent=2),
481
- }))
482
- for field in ("description", "session_id"):
483
- cache["issues"][0][field] = _uuid(self.rng)
484
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
485
- path: json.dumps(cache, indent=2),
486
- }))
487
- del cache["issues"][0][field]
488
- cache["issues"][0]["actor_id"] = "sk-proj-" + rand_openai_like(self.rng, 56)
489
- self.assertIn("leftblock-openai-key", scan_and_rule_ids({
490
- path: json.dumps(cache, indent=2),
491
- }))
492
-
493
- def test_cache_key_allowance_cannot_hide_another_uuid(self):
494
- key = "PPY-" + _uuid(self.rng) + ".json"
495
- value = _uuid(self.rng)
496
- for line in (
497
- f' "{key}": 123.5, "session_id": "{value}"',
498
- f' "{key}": "{value}"',
499
- f' "{key}": {{"session_id": "{value}"}}',
500
- ):
501
- self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
502
- "project/.cache/index.json": line,
503
- }))
480
+ for path in self.BOARD_PATHS:
481
+ for rule, doc in cases.items():
482
+ with self.subTest(path=path, rule=rule):
483
+ self.assertIn(rule, scan_and_rule_ids({path: doc}))
484
+
485
+ def test_key_in_actor_field_still_flagged_in_board_files(self):
486
+ doc = json.dumps({"actor" + "_id": "sk-proj-" + rand_openai_like(self.rng, 56)},
487
+ indent=2)
488
+ for path in self.BOARD_PATHS:
489
+ with self.subTest(path=path):
490
+ self.assertIn("leftblock-openai-key", scan_and_rule_ids({path: doc}))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes