Skip to content

release: v0.14.1 — SBOM fix + #263 sync auto-bind + #257 diagnose CLI#267

Merged
jinhongkuan merged 40 commits into
mainfrom
release/v0.14.1
May 7, 2026
Merged

release: v0.14.1 — SBOM fix + #263 sync auto-bind + #257 diagnose CLI#267
jinhongkuan merged 40 commits into
mainfrom
release/v0.14.1

Conversation

@jinhongkuan

Copy link
Copy Markdown
Contributor

Fast-follow on v0.14.0. Bundles the dev work merged since v0.14.0 cut + the SBOM emitter fix that was the actual blocker for v0.14.0's publish chain.

Closes

Highlights

Cleanup

Merge resolution

main and dev still have divergent histories (option 3 again from PR #247). Local merge with --allow-unrelated-histories; 4 content conflicts resolved by taking dev's version (the v0.14.1 source of truth):

  • .github/workflows/publish.yml — dev's full SBOM steps (re-enabled now that sbom_emit is fixed) over main's if: false hotfix
  • pyproject.toml — dev's 0.14.1 over main's 0.14.0; dev's full dependency list over main's
  • RECOMMENDED_VERSION — dev's 0.14.1 over main's 0.14.0
  • CHANGELOG.md — dev's v0.14.1 + prior content over main's v0.13.7-9 entries (release tags preserve the v0.13.x history independently)

Test plan

  • grep clean (no conflict markers)
  • CI on this PR (assumes dev's CI was already green pre-merge)
  • After merge: tag v0.14.1, recreate GitHub Release, fire publish workflow. Verify SBOM artifacts attach this time — that's the whole point of v0.14.1.
  • Manual smoke: ingest fresh decision, edit code, commit. Sync should auto-bind + resolve compliance with zero user input.

🤖 Generated with Claude Code

Knapp-Kevin and others added 30 commits May 7, 2026 01:44
…SP-03 + OWASP-05)

Plan for OWASP-03 + OWASP-05 bundle. Both reduce to install/update
supply-chain posture declarations under the epic's offered "document
explicitly" option:
- OWASP-03: floor-only stance (uv/pipx authority is the install lock
  authority; no shipped requirements.lock)
- OWASP-05: option-(b) trust-on-first-use posture (TLS + organizational
  access + operator-side gating; cosign verification deferred to
  sigstore-python activation, currently stubbed at
  release/manifest_verify.py:_sigstore_verify)

Audit: round 1 PASS (L1, pure-doc). Adversarial-mode A04 scrutiny
resolved: documenting residual TLS-only-trust risk explicitly + escape
hatch + future-activation path is doctrine-positive (NOT silent
fail-open per A04).

Closes 2 of 3 remaining #218 sub-tasks. Only LLM-06 / #214 remains
after this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ts (#218 OWASP-03 + OWASP-05)

`docs/policies/install-trust-model.md` (new) — two-section operator-readable
trust-model document:

- §Install-time (OWASP-03): declares floor-only stance; uv/pipx as the
  install-lock authority; pin-install path for reproducibility; what
  changes for hosted deployment; OWASP A06 cross-reference for SBOM-based
  dependency audits.

- §Update-time (OWASP-05): declares TLS-only active stance for the
  RECOMMENDED_VERSION fetch; lists what's trusted (TLS, GitHub org
  controls, 1-hour cache); what's NOT trusted (content-level signature);
  operator escape hatch (pin install + manually verify wheel); future
  activation path (sigstore-python verifier + sign-recommended-version
  workflow + handlers/update.py verifier); OWASP A08 cross-reference.

`README.md` Compliance posture section now lists 4 policy files (was 3),
plus an explicit pointer to docs/RELEASE_EVIDENCE_PROCEDURE.md for
per-release change-control evidence.

`docs/research-brief-compliance-audit-2026-05-06.md` OWASP-03 and
OWASP-05 entries marked closed by docs/policies/install-trust-model.md
(matches Plan D + Plan C bidirectional cross-reference pattern).

`tests/test_install_trust_model_doc.py` (new) — 5 functional content-
contract tests covering the install-time + update-time section
commitments, cross-reference presence, README link, and research brief
closure pointers. Per the doctrine interpretation locked in Plan D's
audit: the unit IS the doc content; read_text() + assertion is genuine
unit invocation.

Closes #218 sub-tasks OWASP-03 + OWASP-05. Two more #218 items closed;
only LLM-06 / #214 remains in the epic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…update-trust-model

docs(compliance): install + update trust model bundle (#218 OWASP-03 + OWASP-05)
…b-task)

Plan for LLM-06 / #214 — the last remaining #218 sub-task. Mirrors
#237 LLM-11's hooks-manifest pattern for the skills-content surface.

Audit: round 1 VETO infrastructure-mismatch (invalid TOML mixing
single-bracketed [X] with double-bracketed [[X]] for the same hatch
hooks.custom key + wrong hatch plugin model) → round 2 PASS after
collapsing both build hooks into a single registered module.

Substrate observations folded into round-2 plan:
- LLM-06 framing: substrate REMOVES the design-constraint gate per
  the brief's framing (line 416); enables future remote-skill-loading
  without re-litigating signing
- Cosign activation timing: inherits #237's deferred sigstore-python
  stub; both verifiers activate together when sigstore wiring lands
- TOML emitter: manual emission with ~30 LOC ceiling acceptance bound
  (swap to tomli_w if exceeded)

Closes #218 sub-task LLM-06 (6 of 6 #218 sub-tasks; epic complete).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new release modules + 14 functional tests:

- release/skills_source.py — single source of truth: walk_skills() yields
  (skill_name, filename, file_bytes) for every signed-content file under
  skills/<skill_name>/. Filters to *.md and *.yaml; stray files at root
  silently skipped.

- release/skills_manifest_generator.py — pure-function deterministic TOML
  manifest writer. SHA-256 over file bytes, hex-encoded. Sorted-key
  serialization (skills lex-sorted; files within skill lex-sorted).
  Manual TOML emission stays well under the ~30 LOC ceiling acceptance
  bound from the round-1 audit substrate observation.

- release/skills_verify.py — sigstore-python verifier for the skills
  manifest. Mirrors release/manifest_verify.py from #237 LLM-11. Stub
  _sigstore_verify with explicit "deferred follow-up" framing; activates
  alongside #237's stub when the shared sigstore-python wiring lands.
  Per-file SHA-256 cross-check refuses on any mismatch.
  BICAMERAL_SKILLS_VERIFY_DISABLE=1 bypass writes severity-3
  verification_bypassed ledger event with manifest_kind="skills"
  (disambiguates from LLM-11 hooks-manifest bypass events).

14 functional tests:
- 7 generator tests (manifest derivation, SHA-256, byte-deterministic
  serialization, *.txt filtering, root-file skip, tomllib round-trip,
  lex-ordering)
- 7 verifier tests (positive verify, sig-invalid, sha256-mismatch,
  missing-manifest, swappable verifier hook, bypass-with-event,
  fail-closed-without-env-var)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…LLM-06)

`scripts/hooks_manifest_build_hook.py` — refactored into a single
`ManifestsBuildHook` class with two-step `initialize` that generates
both hooks-manifest.json AND skills-manifest.toml at wheel-build time.
Hatch's `[tool.hatch.build.targets.wheel.hooks.custom]` registers
exactly ONE plugin class per module path; consolidating into one class
is the correct hatch plugin pattern (round-1 audit caught the original
two-class-per-module model as wrong; verified in implementation when
hatch refused with "Multiple subclasses of `BuildHookInterface` found").

`pyproject.toml` — extends `[tool.hatch.build.targets.wheel.shared-data]`
table with one new entry mapping skills-manifest.toml into the wheel
at <sys.prefix>/share/bicameral-mcp/. The single `[tool.hatch.build.targets.wheel.hooks.custom]`
registration is unchanged.

`setup_wizard.py` — adds `_bundled_skills_manifest_paths()` discovery
helper (mirrors `_bundled_manifest_paths`) and `_verify_intended_skills_writes()`
helper (mirrors `_verify_intended_writes`). `_install_skills` gets one
new LOC: the verify-call before any file copy. Dev installs (no bundled
artifacts) get no-op via the discovery returning None — matches the
LLM-11 dev/prod gating pattern.

Wheel build smoke test verified: post-build wheel ships
`bicameral_mcp-*.data/data/share/bicameral-mcp/skills-manifest.toml`
at the proper hatch shared-data location.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…218 LLM-06)

`.github/workflows/publish.yml` — extends the publish pipeline with
two new steps: generate skills-manifest.toml under dist/share/ (mirrors
the hooks-manifest generation pattern), then cosign keyless-sign.
Outputs (txt + sig + crt) attached to the GitHub Release alongside
the hooks-manifest, SBOM, and tag-commit signed artifacts.

`docs/policies/host-trust-model.md` — extends the Server-side
guarantees table with a row for skills-manifest signature verification.
Cross-references LLM-06 / #214 / #218.

`docs/research-brief-compliance-audit-2026-05-06.md` — LLM-06 entry
marked closed by the new release/skills_* modules + the
ManifestsBuildHook skills-manifest generation. Closure pointer
explicit about: (a) substrate REMOVES the design-constraint gate
enabling future remote-skill-loading, (b) secondary benefit is
post-install in-place-tampering detection.

`tests/test_compliance_policy_docs.py` — 1 new content-contract test
verifying the host-trust-model.md skills-manifest row exists and
references _install_skills + skills-manifest.toml + LLM-06.

Closes #218 LLM-06 / #214. Epic #218 now COMPLETE (6 of 6 sub-tasks):
LLM-11 + OWASP-01 + SOC2-03 + OWASP-03 + OWASP-05 + LLM-06.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…igning

feat(release): skills/MANIFEST.toml signing — closes epic #218 (#214 LLM-06)
…bstantiated

Out-of-band substantiation: implementation already merged to dev via
PR #249 (merge commit b2fc66e); this ledger-only commit lands the seal
entry off dev tip.

Closes #214 (#218 sub-task LLM-06 — sign skills/ payload).
Closes #218 epic (6/6 sub-tasks complete).

Reality matches Promise: 11 planned files committed (3 phases of plan-F);
14 new functional tests (7 generator + 7 verifier; 2 generator tests
beyond plan-F enumeration for stronger contract coverage) plus 1
content-contract test extension all PASS. Wheel-build smoke test
verified skills-manifest.toml ships at hatch shared-data location
alongside hooks-manifest.json.

Logged deviation: build-hook consolidated deeper than the audit's
Path A — single ManifestsBuildHook class generating both manifests
in one initialize, per hatch's actual one-class-per-registered-module
constraint. Same auto-discovery semantics; cleaner module structure.

Cosign keyless signing of skills-manifest.toml mirrors #237 LLM-11
pipeline; deferred sigstore-python verifier wiring activates both
manifests together when it lands.

Plan: plan-F-llm-06-skills-manifest-signing.md
Audit: round 2 PASS (round-1 infrastructure-mismatch VETO cleared).
seal(#218 LLM-06): META_LEDGER entry #44 — skills manifest signing substantiated
…erators (#227)

Closes SOC2-06 + OWASP-06 fold per docs/research-brief-compliance-audit-2026-05-06.md
§ 2.2 + § 2.3.

Three phases:
- Phase 1: audit_log.py module + JsonFormatter + forbid-list + 22 functional tests
- Phase 2: server lifecycle + tool-invocation wrapper + dual-write at ingest
  refusal / preflight bypass sites + 15 functional tests
- Phase 3: operator policy doc + research-brief closure + 1 content-contract
  test extension + README compliance-posture row

Audit: round 1 VETO (specification-drift between dual-write helper code and
test specs requiring bidirectional exception independence) -> round 2 PASS
after applying Path A (wrap each call in try/except for bidirectional
exception isolation; helper-extraction guidance for preflight bypass site).
… Phase 1)

Closes SOC2-06 + OWASP-06 fold from
docs/research-brief-compliance-audit-2026-05-06.md § 2.2 + § 2.3.

audit_log.py (240 LOC) provides the operator-facing structured-log
emission surface. Distinct from preflight_telemetry.py's machine-join
JSONL writers and telemetry.py's PostHog outbound; this is the
incident-readability surface.

Channel: stderr-default; BICAMERAL_AUDIT_LOG=<path> override; =disabled
no-op; unwriteable path falls back to stderr with one warning marker.

Level: BICAMERAL_AUDIT_LOG_LEVEL=info|warn|error filters event types
via _LEVEL_BY_EVENT table.

Forbid-list: frozenset of 10 known content-bearing keys (decision_text,
file_paths, transcript, arguments, payload, content, text, body, output,
result_text); forbidden_keys_stripped field surfaces redactions to the
operator without exposing content.

22 functional tests pass (6 forbid-list + 8 format + 8 channel). Each
test invokes the unit and asserts on returned value or observable
side-effect (captured stderr, parsed JSON line, file contents). Per
qor/references/doctrine-test-functionality.md.

Plan: plan-227-structured-audit-log-emission.md
Audit: round 1 VETO specification-drift -> round 2 PASS via Path A
(bidirectional exception isolation in dual-write helper, scoped to
Phase 2).
…227 Phase 2)

server.py:
- serve_stdio() emits SERVER_START at entry; SERVER_SHUTDOWN in finally
- @server.call_tool() outer wrapper (was the existing impl) extracted to
  _call_tool_impl; new outer dispatcher captures tool_name + duration_ms +
  outcome_class (ok/refused/error) via try/except/finally, emits
  TOOL_INVOCATION on every dispatch regardless of return path
- arguments dict never enters the audit-log emit (forbid-list locked at
  the call site by construction; only session_id is extracted)

context.py:
- _emit_config_load_once() helper with module-level idempotent guard;
  invoked by BicameralContext.from_env() after instance construction
- emits CONFIG_LOAD with ingest_max_bytes / ingest_rate_limit_burst /
  ingest_rate_limit_refill_per_sec / guided_mode (int/bool only; no
  path-bearing fields sourced into the emit)

handlers/ingest.py:
- _emit_ingest_refusal_telemetry amended to dual-write; each call
  (preflight_telemetry.write_ingest_refusal_event + audit_log.emit)
  wrapped in independent try/except so failure of either MUST NOT
  block the other; original _IngestRefused propagates cleanly via
  caller's raise

DEVIATION (logged): plan-227 Phase 2 also called for dual-write at the
preflight bypass site. The v1 bypass surface is reverted (commit
d1e3914) so preflight_telemetry.write_bypass_event has no active caller
to wire. Test test_preflight_bypass_writes_both_jsonl_and_audit_log
dropped from this implementation; when the v1 surface returns (separate
work), the same dual-write helper pattern from
handlers.ingest._emit_ingest_refusal_telemetry becomes the template.
Documented in tests/test_audit_log_dual_write.py module docstring.

Tests: 14 functional tests pass (4 server lifecycle + 7 tool invocation
+ 3 ingest dual-write). Full ingest-gate regression: 114 sister tests
pass (canary + size + rate + sensitive + server-refusal). No
regressions introduced.

Plan: plan-227-structured-audit-log-emission.md
Audit: round 2 PASS
…hase 3)

docs/policies/audit-log.md (new) — operator-readable policy:
- Channel resolution table (BICAMERAL_AUDIT_LOG values vs behavior)
- Level filter table (info/warn/error event coverage)
- Closed event taxonomy with per-class fields
- Forbid-list discipline + symmetry with telemetry.py
- Failure semantics (POSIX-syslog precedent)
- Dual-write rationale at gate sites
- Integration patterns (logrotate, journalctl, file collectors)
- Log retention guidance for SOC 2 CC operators
- v1 out-of-scope items

docs/research-brief-compliance-audit-2026-05-06.md:
- SOC2-06 (line 316): Status closed by audit_log.py + policy doc
- OWASP-06 (line 355): Status closed (folded with SOC2-06)

README.md: "Compliance posture" section bumped from 3 -> 4 policy
files; new audit-log.md row added.

tests/test_compliance_policy_docs.py:
- test_audit_log_policy_doc_includes_channel_resolution_table —
  content-contract test locking the channel-resolution table presence
  + SOC2-06/OWASP-06 cross-reference
- test_audit_log_policy_doc_documents_event_taxonomy — locks
  bidirectional drift between AuditEventType enum and the operator-
  facing event taxonomy table

Tests: 8 content-contract tests pass (6 existing + 2 new). Full #227
regression: 150 tests pass (28 audit_log + 8 compliance docs +
114 ingest sister-suites).

Plan: plan-227-structured-audit-log-emission.md
Audit: round 2 PASS
…ated

Closes SOC2-06 (SOC 2 CC system-monitoring gap) and OWASP-06 (OWASP A09
logging gap) per docs/research-brief-compliance-audit-2026-05-06.md
§ 2.2 line 316 + § 2.3 line 355.

Reality matches Promise: 13 planned files (1 new module + 6 new test
files + 4 modified source files + 2 docs) shipped across 3 phases.
38 new functional tests pass; 114 ingest-gate sister regression tests
pass clean; 8 compliance content-contract tests pass. Full repo
regression: 1038 pass with 10 baseline-confirmed pre-existing failures
in untouched modules (zero net new regressions).

Logged deviation: preflight bypass dual-write deferred — v1 surface
reverted (commit d1e3914) so write_bypass_event has no active caller
in v0; helper pattern from handlers.ingest._emit_ingest_refusal_telemetry
becomes template when v1 surface returns. Captured in
tests/test_audit_log_dual_write.py docstring.

Audit: round 1 VETO specification-drift (dual-write helper code
contradicted bidirectional-independence test specs) -> round 2 PASS
via Path A (wrap each write in independent try/except).

Plan: plan-227-structured-audit-log-emission.md
feat(audit-log): structured audit-log emission for self-hosted operators (#227)
… Layer 1)

Closes #252 Issue 2 Layer 1 per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

Root cause class: "Invalid revision N for type Value" deserialization
errors from SurrealDB are wire-format version mismatches between the
surrealdb-py client and the on-disk SurrealKV record headers. With
`surrealdb>=2.0.0` (floor only), a routine `pip install --upgrade`
can swap the lib under an existing ledger and silently introduce
incompatibility — the failure mode that hit `jaune24` (#252).

Patch-level pin to ==2.0.0 is unambiguous (surrealdb 2.x has only one
patch published; future patches must verify the wire format and ship
with a deliberate migration path).

Future surrealdb bumps require:
- Schema-revision sentinel persisted in the ledger (#252 Layer 2)
- Operator-driven export/import (#252 Layer 4) for migration
- Explicit pin update in this file with rationale

This is privacy-preserving by construction — no customer data needed
to ship the fix.

Plan: docs/research-brief-252-privacy-preserving-ledger-remediation.md
…at awareness (#252)

Closes #252 Issue 2 Layer 2 per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

Two phases:
- Phase 1: extend schema_meta with surrealdb_client_version_at_first_write
  + at_last_write + last_write_at fields; add _write_wire_format_sentinel
  helper invoked from init_schema; bump SCHEMA_VERSION to 16; add no-op
  _migrate_v15_to_v16 to keep the registry happy on existing v15 ledgers.
- Phase 2: add LEDGER_SCHEMA_VERIFIED + LEDGER_VERSION_DRIFT to the
  AuditEventType enum + their level mapping. WARN-only posture per
  operator directive ("gating is observability, not obstruction"); the
  drift event lights up the surface but does NOT fail-fast — fail-fast
  belongs in Layer 5 where it pairs with a migration path.

~120 LOC + 11 functional tests across Phase 1 + 2. No new pip deps.
Layer 4's operator-facing policy-doc update for the new events is
intentionally deferred (avoids two-PR doc churn for the same surface).

Plan: plan-252-layer-2-schema-revision-sentinel.md
Strategy: docs/research-brief-252-privacy-preserving-ledger-remediation.md
…a table) (#252)

Round 1 VETO: infrastructure-mismatch (3 binding findings):
1. schema_meta is DELETEd on every _set_schema_version call (line 924-930);
   extending it for wire-format sentinel would wipe data on every migration.
2. client.query(sql, vars=None) — keyword is `vars`, not `bindings`.
3. init_schema() runs BEFORE migrate(), not after; sentinel call timing wrong.

Round 2 amendments (Path B per audit recommendation):
1. New dedicated `bicameral_meta` table (separate from `_META`/schema_meta);
   _BICAMERAL_META constant wired into init_schema's define-pass loop.
2. _write_wire_format_sentinel queries/updates `bicameral_meta` (clean
   from DELETE+CREATE concerns); uses positional `vars` dict matching the
   existing `_set_schema_version` convention.
3. Sentinel call moves to `adapter.connect()` AFTER both init_schema()
   and migrate() return successfully; on DestructiveMigrationRequired,
   the existing exception handler short-circuits and the sentinel is
   skipped (destructive-migration-pending warning is the higher-priority
   signal).

Round 1 advisory (drop xfail):
- Layer 2 NOW updates docs/policies/audit-log.md event-taxonomy table
  inline so the existing #227 test_audit_log_policy_doc_documents_event_taxonomy
  content-contract test stays green. No more deferred-doc churn argument.

~150 LOC + 13 functional tests across Phase 1 + 2. No new pip deps.
Implementer notes lock the option<datetime> smoke-test concern + the
two-layer try/except discipline in adapter.connect().

Plan: plan-252-layer-2-schema-revision-sentinel.md (round 2)
Strategy: docs/research-brief-252-privacy-preserving-ledger-remediation.md
…er 2)

Closes #252 Layer 2 per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

ledger/schema.py:
- New _BICAMERAL_META constant defining bicameral_meta table with
  surrealdb_client_version_at_first_write (option<string>),
  surrealdb_client_version_at_last_write (option<string>), and
  last_write_at (option<datetime>) fields. Wired into init_schema's
  define-pass loop alongside _META.
- New _write_wire_format_sentinel(client) helper that reads the
  recorded surrealdb-py version, updates last-write fields with
  importlib.metadata.version("surrealdb"), preserves at_first_write
  immutability after first set, and returns (recorded, running, status)
  where status ∈ {"first-write", "match", "drift"}.
- New _migrate_v15_to_v16 (no-op body; registry entry only) — the
  bicameral_meta DEFINEs are applied via init_schema's OVERWRITE pass;
  the registry entry exists so SchemaVersionTooNew is not raised on
  v15 ledgers transitioning to v16.
- SCHEMA_VERSION bumped 15 → 16; SCHEMA_COMPATIBILITY entry added.

ledger/adapter.py:
- New _emit_wire_format_sentinel() helper extracted from connect()
  per Razor advisory; called at the end of connect()'s success path
  (after _connected = True). Two-layer try/except: outer wraps
  _write_wire_format_sentinel raise; inner wraps audit_log.emit raise.
  Both required by the existing audit_log discipline ("audit log MUST
  NOT break callers"). On DestructiveMigrationRequired, the existing
  early-return correctly skips the sentinel.

audit_log.py:
- Two new AuditEventType values: LEDGER_SCHEMA_VERIFIED (info-level,
  fires on first-write or match) + LEDGER_VERSION_DRIFT (warn-level,
  fires on mismatch). Per operator directive ("gating is observability,
  not obstruction"), drift WARNs but does NOT fail-fast — fail-fast
  belongs in Layer 5 where it pairs with a migration path.

docs/policies/audit-log.md:
- Event-taxonomy table extended with rows for both new event types.
  Locks the existing #227 test_audit_log_policy_doc_documents_event_taxonomy
  content-contract test (which would have failed on Phase 2's enum
  extension without this update).

Tests: 21 new functional tests (10 sentinel-helper + 4 adapter-connect
integration + 3 migration + 4 audit-log event-type + 1 doc/code drift
lock) — all PASS. Ledger + audit_log sister regression: 108 tests pass
clean.

option<datetime> smoke-tested against memory:// fixture before relying
on it (per implementer note); SurrealDB v2 embedded supports the form,
so the planned fallback to non-option datetime was not needed.

Plan: plan-252-layer-2-schema-revision-sentinel.md
Audit: round 1 VETO infrastructure-mismatch -> round 2 PASS via Path B
(separate bicameral_meta table; positional vars dict; sentinel call at
adapter.connect post-init+migrate; policy-doc inline update).
…tantiated

Closes #252 Layer 2 per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

Reality matches Promise: 7 planned files (3 new tests + 4 modified
sources/docs) shipped across 2 phases. 21 new functional tests pass
(8 more than the plan's 13 — doctrine-positive expansion covering
PackageNotFoundError fallback, partial-row edge cases, and helper-
isolation paths). Ledger + audit_log + compliance_policy sister
regression: 108 tests pass clean.

Logged deviations:
1. _emit_wire_format_sentinel helper extraction in adapter.py per
   Razor advisory (non-VETO). Mirrors _emit_ingest_refusal_telemetry
   from #227. Doctrine-positive; no behavior change.
2. Test count expansion (13 -> 21) for edge-case coverage.

option<datetime> smoke-tested against memory:// fixture; works in
SurrealDB v2 embedded — fallback to non-option datetime not needed.

Audit: round 1 VETO infrastructure-mismatch (3 binding: schema_meta
DELETEd on every migration; bindings kwarg doesn't exist; init_schema
runs before migrate) -> round 2 PASS via Path B (separate
bicameral_meta table; positional vars dict; sentinel call at
adapter.connect post-init+migrate; policy-doc inline update).

Plan: plan-252-layer-2-schema-revision-sentinel.md
Strategy: docs/research-brief-252-privacy-preserving-ledger-remediation.md
fix(deps): pin surrealdb==2.0.0 — close wire-format drift surface (#252 Layer 1)
…sion-sentinel

feat(ledger): #252 Layer 2 — wire-format sentinel via bicameral_meta table
Privacy-preserving operator bug-report tool per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

Three phases:
- Phase 1: Diagnosis dataclass + _ALLOWED_FIELDS frozenset + async
  gather_diagnosis() with private readers (ledger metadata, bicameral_meta
  sentinel, table counts, audit-log tail merge across both JSONL +
  optional configured path, suggestion engine). Allowlist-by-field-name
  is the load-bearing privacy mechanism.
- Phase 2: format_diagnosis() markdown renderer + CLI subparser
  registration in server._register_subparsers + dispatch arm. Output
  is operator-pasteable markdown with 6 section headers.
- Phase 3: operator policy doc enumerating allowlisted fields +
  suggestion heuristic catalog; content-contract tests lock doc/code
  drift; README compliance-posture row added.

~280 LOC + 25 functional tests. No new pip deps. Implementation can
land before #256 merges (bicameral_meta reads are tolerant of missing
table); integration test for first-write status xfail-marked until
#256 reaches dev.

Five hardcoded suggestion heuristics: drift detected; recommended-
version mismatch; audit-log disabled; ledger > 100 MiB; schema version
old. Plugin extension is YAGNI for v1.

Audit-log tail covers BOTH ~/.bicameral/preflight_events.jsonl AND
BICAMERAL_AUDIT_LOG=<path> when configured (full-utility hybrid per
operator directive — refused the simplification tradeoff).

Plan: plan-252-layer-3-diagnose-cli.md
Strategy: docs/research-brief-252-privacy-preserving-ledger-remediation.md
Depends on: #252 Layer 2 (#256) for bicameral_meta table availability
…eport (#252 Layer 3)

Closes #252 Layer 3 per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

cli/diagnose.py (215 LOC): Diagnosis dataclass + _ALLOWED_FIELDS
frozenset (17 fields) + _CANONICAL_TABLES + format_diagnosis()
markdown renderer + main() entrypoint. Allowlist-by-field-name is
the load-bearing privacy mechanism.

cli/_diagnose_gather.py (244 LOC, helper-extracted per Razor advisory):
gather_diagnosis() async function + 5 private readers (ledger metadata,
bicameral_meta sentinel, schema_version, table_counts, audit-log tail
merge across both preflight_events.jsonl AND BICAMERAL_AUDIT_LOG=<path>
when configured) + 5-heuristic suggestion engine.

server.py: new "diagnose" subparser registration + dispatch arm.

handlers/update.py: public alias `fetch_recommended_version` for
clean cross-layer call from cli/diagnose without re-using private
underscore symbol.

docs/policies/diagnose-output.md: operator-readable allowlist policy
+ suggestion heuristic catalog + always-safe-to-paste guarantee.

README.md: Compliance posture section bumped 5 → 6 policy files.

Tests: 31 new functional tests across 4 files (allowlist parity +
gather scenarios + format rendering + CLI subprocess) plus 2 new
content-contract tests in test_compliance_policy_docs.py (allowlist
field doc-coverage + suggestion-catalog drift lock). All 41 PASS.

option<datetime> support inherited from #252 Layer 2 (not present on
this branch base; bicameral_meta read returns "first-write" status
on pre-Layer-2 ledgers — semantically equivalent for diagnostic
purposes).

Plan: plan-252-layer-3-diagnose-cli.md
Audit: round 1 PASS (no VETO — plan absorbed prior-round audit
learnings: separate-table architecture from Layer 2; allowlist +
content-contract test pattern from #227; helper-extraction
acknowledged in implementer notes).
…I substantiated

Closes #252 Layer 3 per
docs/research-brief-252-privacy-preserving-ledger-remediation.md.

Reality matches Promise: 9 planned files (4 new tests + 1 new policy
doc + 4 modified sources/docs) shipped across 3 phases. 41 new
functional tests pass (16 more than the plan's 25 — doctrine-positive
expansion covering per-reader unit boundary + edge cases like
PackageNotFoundError unknown branch, recent-events 5-cap enforcement,
table-counts canonical subset).

Logged deviations:
1. Helper-extraction split per Razor advisory (cli/diagnose.py 213 +
   cli/_diagnose_gather.py 244, both under 250). Mirrors
   cli/_link_commit_runner.py pattern.
2. drift_status semantics adjusted to "match" on fresh ledger (Layer 2's
   sentinel writes row at adapter.connect time; gather sees populated
   row by the time it runs). Test renamed to reflect Layer-2-integrated
   reality.
3. Test count expansion (25 -> 41).

Privacy posture: allowlist-by-field-name at the Diagnosis dataclass
level. Negative content-leak test verifies decision content never
appears in repr(Diagnosis). Forbidden-field-name negative lock catches
future field expansion using content-bearing names.

Three audit advisories all applied:
- Razor headroom: helper extraction
- Recommended-version fetch: public alias added
- CLI tests: network monkeypatched (no live HTTP)

Audit: round 1 PASS (no VETO — plan absorbed prior-round audit
learnings).

Plan: plan-252-layer-3-diagnose-cli.md
Strategy: docs/research-brief-252-privacy-preserving-ledger-remediation.md
Updates the requirements on [bm25s](https://github.com/xhluca/bm25s) to permit the latest version.
- [Release notes](https://github.com/xhluca/bm25s/releases)
- [Commits](xhluca/bm25s@0.2.0...0.3.8)

---
updated-dependencies:
- dependency-name: bm25s
  dependency-version: 0.3.8
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Updates the requirements on [sqlite-vec](https://TODO.com) to permit the latest version.

---
updated-dependencies:
- dependency-name: sqlite-vec
  dependency-version: 0.1.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
feat(cli): #252 Layer 3 — bicameral-mcp diagnose CLI
…-0.3.8

build(deps): update bm25s requirement from >=0.2.0 to >=0.3.8
jinhongkuan and others added 10 commits May 7, 2026 14:37
…c-gte-0.1.9

build(deps): update sqlite-vec requirement from >=0.1.0 to >=0.1.9
- pyproject.toml: 0.13.3 → 0.14.1 (dev was stuck at 0.13.3 throughout
  the v0.14.0 stream; bumping past v0.14.0 to align with what's actually
  on main)
- RECOMMENDED_VERSION: 0.13.3 → 0.14.1
- pyproject.toml scripts: drop `bicameral-mcp-classify` (broken since
  #244 deleted cli/classify.py — carryover cleanup from the v0.14.0
  release surgery)
- release/sbom_emit.py: install wheel into temp venv before scanning.
  Fixes the v0.14.0 publish-pipeline halt where
  `cyclonedx-py environment <wheel>` failed because the subcommand
  introspects a Python environment via a Python-executable path, not
  a wheel file. New flow: tempdir venv → pip install wheel + cyclonedx-bom
  → run `cyclonedx-py environment --output-file <out> <venv-python>`.
  Output is the wheel's actual dependency closure with no contamination
  from the build env. `--output-file` flag replaces v0.14.0's `-o` short
  form (cyclonedx-py 7.x dropped the alias).
- CHANGELOG.md: new ## v0.14.1 release header summarizing SBOM fix +
  #257 diagnose CLI + #259/#260 dependabot bumps. Demoted prior
  "[Unreleased]" content to "[Unreleased — pre-v0.14.0]" to mark the
  cutoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync's autonomy contract (skills/bicameral-sync/SKILL.md:203 — "Always
complete step 2 before responding ... runs autonomously after a commit.
Do not wait for user input.") broke whenever a touched decision had no
`binds_to` edge yet, which is the common case for any newly-ingested
decision. The agent had to be hand-walked: sync → "now bind" → "now sync
again". P0 because every onboarding session hits this on the first
post-ingest commit, breaking demo 02's headline value claim.

Root cause: `handlers/link_commit.py` injects `_GROUNDING_INSTRUCTION_*`
strings telling the agent what to do next, but the sync skill's only
explicit step (step 2) consumes `pending_compliance_checks`, not
`pending_grounding_checks`. Ungrounded entries fell through to "end of
skill, return to user."

Fix: new step 1.5 between current step 1 (Sync HEAD) and step 2 (Resolve
every pending compliance check). The new step:

- Triggers on `pending_grounding_checks` entries with `reason="ungrounded"`.
- Resolves the symbol via Grep/Read + validate_symbols on the touched
  file.
- Calls `bicameral.bind(decision_id, file_path, symbol_name)`.
- Concatenates the returned `PendingComplianceCheck` with any pendings
  from the original `link_commit` response and proceeds to step 2 in
  the same invocation — no second human nudge.

The `reason="symbol_disappeared"` (relocation) path is preserved
unchanged — that case still bails out for V2 atomic rebind, per
existing handlers/link_commit.py:77-92 logic.

A one-line *symbol* glossary lands at the head of step 1.5, on first
use of `symbol_name` in this skill (no separate bicameral-bind skill
file exists; bind is invoked from sync + ingest, with the glossary
introduced wherever the first invocation lives).

Acceptance per #263:
- [x] sync skill has explicit step consuming `pending_grounding_checks`
      reason=ungrounded → emits bind call
- [x] After bind, skill proceeds to compliance resolution in same
      invocation (concatenated pendings list)
- [x] reason=symbol_disappeared path unchanged
- [x] Symbol glossary line in skill

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New workflow `.github/workflows/notify-slack-on-bug.yml` triggers on
issues opened-with-`bug`-label OR labeled-with-`bug`-after-the-fact.
Posts a structured Slack message (header + title link + author/repo
context) via incoming webhook to the #telemetry channel.

Covers both:
- Manual issue filings labeled `bug`
- /bicameral-report-bug skill submissions (which file with labels
  `dev,bug` per skills/bicameral-report-bug/SKILL.md)

Setup required (one-time, by repo admin):
1. Create an incoming webhook for #telemetry on Slack
2. Add the URL as repo secret SLACK_TELEMETRY_WEBHOOK_URL

Without the secret the job is a no-op. The slack-github-action step
fails closed when the webhook URL is empty, so no spurious notifications.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
release(prep): v0.14.1 — SBOM emitter fix + version bumps
feat(infra): post to Slack #telemetry when a bug ticket is filed
This was the wrong shape — committing internal-ops Slack routing into
the public-ish repo. The cleaner pattern is GitHub's official Slack app
(slack.github.com), which lives entirely in Slack workspace config:
no code commit, no repo secret, can be toggled or filtered without
opening a PR.

Setup (one-time, by a Slack workspace admin in #telemetry):

  /github subscribe BicameralAI/bicameral-mcp issues +label:bug

That single command covers:
- Manual issue filings labeled `bug`
- /bicameral-report-bug skill submissions (which file with `dev,bug`)
- Late-labeled issues (re-fires when `bug` is added)

Reverts PR #265 (commit c545d6b).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
revert: drop notify-slack-on-bug workflow (use GitHub Slack app instead)
Same divergent-histories pattern as the v0.14.0 PR #247 merge —
took --allow-unrelated-histories with dev as source of truth for
the four content conflicts:

- .github/workflows/publish.yml: dev has the proper SBOM steps (with
  the new sbom_emit.py that installs the wheel into a temp venv).
  main has the if:false hotfix from #261/#262. Take dev — restoring
  SBOM gen for v0.14.1 is the whole point.
- pyproject.toml: dev=0.14.1, main=0.14.0. Take dev.
- RECOMMENDED_VERSION: dev=0.14.1, main=0.14.0. Take dev.
- CHANGELOG.md: dev has v0.14.1 + v0.14.0 release sections (with the
  v0.13.7-9 entries on main not folded in). Take dev — release tags
  preserve the v0.13.x history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@jinhongkuan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 15 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d2f3a65-8be6-444e-84cb-f33383a92ebe

📥 Commits

Reviewing files that changed from the base of the PR and between 14c36a8 and 0a991ab.

📒 Files selected for processing (50)
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • README.md
  • RECOMMENDED_VERSION
  • audit_log.py
  • cli/_diagnose_gather.py
  • cli/diagnose.py
  • context.py
  • docs/META_LEDGER.md
  • docs/policies/audit-log.md
  • docs/policies/diagnose-output.md
  • docs/policies/host-trust-model.md
  • docs/policies/install-trust-model.md
  • docs/research-brief-compliance-audit-2026-05-06.md
  • handlers/ingest.py
  • handlers/update.py
  • ledger/adapter.py
  • ledger/schema.py
  • plan-227-structured-audit-log-emission.md
  • plan-252-layer-2-schema-revision-sentinel.md
  • plan-252-layer-3-diagnose-cli.md
  • plan-E-owasp-03-and-05-install-update-trust-model.md
  • plan-F-llm-06-skills-manifest-signing.md
  • pyproject.toml
  • release/sbom_emit.py
  • release/skills_manifest_generator.py
  • release/skills_source.py
  • release/skills_verify.py
  • requirements.txt
  • scripts/hooks_manifest_build_hook.py
  • server.py
  • setup_wizard.py
  • skills/bicameral-sync/SKILL.md
  • tests/test_audit_log_channel.py
  • tests/test_audit_log_dual_write.py
  • tests/test_audit_log_forbid_list.py
  • tests/test_audit_log_format.py
  • tests/test_audit_log_ledger_event_types.py
  • tests/test_audit_log_server_lifecycle.py
  • tests/test_audit_log_tool_invocation.py
  • tests/test_compliance_policy_docs.py
  • tests/test_diagnose_allowlist.py
  • tests/test_diagnose_cli.py
  • tests/test_diagnose_format.py
  • tests/test_diagnose_gather.py
  • tests/test_install_trust_model_doc.py
  • tests/test_ledger_bicameral_meta_migration.py
  • tests/test_ledger_bicameral_meta_wire_format.py
  • tests/test_setup_wizard_skills_verify.py
  • tests/test_skills_manifest_generator.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v0.14.1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jinhongkuan jinhongkuan merged commit 2a9b003 into main May 7, 2026
11 checks passed
@jinhongkuan jinhongkuan deleted the release/v0.14.1 branch May 7, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants