Codex CRITICAL + 4 HIGH + 5 MEDIUM/LOW from RULE 0.23 dual-review and
RULE 0.25 multi-critic swarm — all closed.
CRITICAL fix
- Model::slug() ledger compatibility: posterior.rs + select_kernel.rs
query `WHERE model = ?2 OR model = ?3`, binding canonical + legacy
slug pair via new `Model::legacy_slug()`. Production ledger rows
written under "haiku"/"sonnet"/"opus" remain visible to posterior
aggregation. Regression test ledger_legacy_slug_counted.
HIGH fixes
- cmd_select(): no longer early-returns on profile match. Profile's
default_model_ref now becomes DecisionInput.fallback; select() always
runs, posterior/kernel evidence wins if present. RULE 0.20 cost
optimisation restored for all 18 registered agents.
- Registry pricing SSoT: DecisionInput now carries Option<Arc<Registry>>.
estimated_cost() tries registry first; hardcoded match is documented
fallback only. select_posterior.rs no longer duplicates models.toml
constants.
- registry.rs portability: include_str!() embeds the three TOMLs at
compile time. load_embedded() new; disk path tried first via
KEI_REGISTRIES_DIR, embedded as fallback. `cargo install`d binaries
now find registries unconditionally. embedded_registry_matches_disk
test ensures embedded ≡ disk source.
- next_model() ambiguity: replaced Option<&Model> with EscalationResult
enum (Next(&Model) / AtTop / NotFound). Callers can distinguish typo
from ceiling. 5 new tests.
MEDIUM fixes
- posterior.rs u32 overflow: `(n_plus + n_minus) as u32` →
`u32::try_from(n_plus.saturating_add(n_minus)).unwrap_or(u32::MAX)`.
overflow_guard_on_huge_n test with i64::MAX.
- pick() unknown-model: now returns None when default_model_ref's model
is absent from registry. Inverted the deprecation guard.
- HOME unset: disk_registries_dir() returns None on empty HOME and
falls through to embedded registries. open_ledger() logs warning
and returns None instead of opening at malformed path.
- SQLite WAL + busy_timeout: applied to ledger connection in
open_ledger() — concurrent CLI invocations no longer SQLITE_BUSY.
LOW fixes
- impl Model consolidation: next_tier() moved to pricing.rs.
escalate.rs uses current.next_tier() instead of duplicating logic.
- complexity.rs: removed duplicate "ml-implementer" in HEAVY_ROLES.
- dna_class.rs: role("") now returns None instead of Some("").
Verification (orchestrator-side, RULE 0.13 §Verify-before-commit):
- cargo check → clean
- cargo test --release → 63 passed / 0 failed (was 58 → +5 new tests
cover legacy-slug, EscalationResult, overflow, unknown-model, embedded)
- Constructor Pattern → all files ≤ 200 LOC (max registry.rs 196)
- Largest fn from_ledger 28 LOC / limit 30
DNA-INDEX.md regenerated by kei-registry hook (cosmetic).
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes
follow-up-required:
- (none from this commit; next audit pass before merge to main)
Splits agent definition into stable provider + swappable model + role-bound
profile. Adding a new LLM API is one row in providers.toml; new model is
one row in models.toml; agent invocation picks any (provider, model) pair
through agent-profiles.toml default_model_ref.
- providers.toml: 10 providers — anthropic, openai, codex (OAuth), xai,
deepseek, google, ollama-local, mlx-local, lmstudio-local, litellm-proxy
- models.toml: 11 models with cost_*_per_mtok_micro + context_window +
verified_at + deprecated_at
- agent-profiles.toml: 18 representative profiles; manifest_path points
to the canonical .md in ~/.claude/agents/
Three-layer DNA per the new architecture:
agent-shell::<provider>:<model>:<caps>::<scope8>::<body8>-<nonce8>
This commit only adds registries — kei-model-router still hardcodes
the Claude-only Model enum. Wave 4 will rewire it to read TOML.
Profile bundles the crates kei-buddy needs at runtime:
kei-buddy, kei-telegram-webhook, kei-shared, kei-chat-store,
kei-social-store, kei-memory-sqlite, kei-router, kei-llm-bridge-mlx.
Used by keisei-marketplace cloud-init when user clicks 'хочу своего
KeiBuddy' on /keibuddy/setup — Hetzner VPS spawn pulls KeiSeiKit and
runs install.sh --profile=buddy --yes.
Previous wiremock conversion fixed the listener-lifecycle race but
left the underlying problem unsolved: `ensure_env()` mutates the
process-global ANTHROPIC_ENDPOINT, and parallel `cargo test` threads
race on that write. Manifested as 502 / "error sending request for
url …" on the first concurrent test pair under both macOS and Linux.
Annotate every #[tokio::test] in openai_loop_wiring.rs +
openai_compat.rs with `#[serial_test::serial]` — these are the only
tests that touch ANTHROPIC_ENDPOINT via shared_mock_anthropic.
serial_test enforces process-wide ordering so the env mutation +
HTTP request pair is atomic per test. All other tests stay parallel.
Stress: 5 parallel `cargo test` runs all green.
Previous `tests/common/mod.rs` spawned a mock Anthropic upstream via
hand-rolled axum + std:🧵:spawn + own current-thread tokio runtime
bound to 127.0.0.1:0. Stable on Linux runner; flaked on macOS GitHub
Actions runners:
thread 'streaming_responses_runs_real_loop_not_stub' panicked at
kei-cortex/tests/openai_loop_wiring.rs:277:5:
no responses delta event in stream: event: response.error
data: {"error":"model: anthropic request: error sending request
for url (http://127.0.0.1:49312/v1/messages)"}
Root cause traced to macOS-runner loopback / fd-limit pressure on the
dedicated-thread current-thread runtime. wiremock crate runs a
production-quality hyper-based mock server, manages its own listener
lifecycle, and survives the macOS runner constraints.
## Change
- `Cargo.toml`: add wiremock = workspace dev-dep (already 0.6 in workspace)
- `tests/common/mod.rs::MockAnthropicServer` rebuilt over wiremock::MockServer
- `build_mock(text)` mounts `POST /v1/messages → 200 + canned body` on a
wiremock instance
- `mock_anthropic_responding_with()` spins one per call on a parked
helper thread (preserves `MockAnthropicServer: 'static` lifetime for
`shared_mock_anthropic` `OnceLock` singleton)
- `shared_mock_anthropic()` API unchanged; existing test sites in
`tests/openai_loop_wiring.rs` + `tests/openai_compat.rs` continue to
work without modification
## Verification
`cargo test -p kei-cortex --test openai_loop_wiring`: 7/7 pass locally
`cargo test -p kei-cortex`: full suite green (428 lib + integration)
Also includes DNA-INDEX regenerate (auto-encyclopedia hook artefact;
0 vortex matches preserved).
Removes the two banned-project references (project-vortex::vortex and
project-vortex::vortex-constraints at lines 703/707 of DNA-INDEX.md
pre-regenerate) that surfaced in the public-readiness audit (P0
finding from sub-agent a2c1199a).
Source: ~/.claude/registry.sqlite row 391 +
~/.claude/registry-fragments/project-vortex__vortex-constraints.md.
Both removed locally so kei-registry encyclopedia regen no longer
emits the lines. auto-encyclopedia-refresh.sh PostToolUse:Edit|Write
hook will not re-add them on next run since the source row is gone.
If the Vortex agent project (cyber-banned per ~/.claude/rules/security.md)
needs that rule again, it should be registered into a SEPARATE local-only
registry (e.g. ~/.claude/registry-private.sqlite) so it never leaks into
the public encyclopedia path.
After regen: 0 vortex/neuralcloak/keidog/keinet matches in entire
KeiSeiKit-public tree (git grep). Public-readiness P0 = 0.
Replaces the rigid FSM after Intro/AskLanguage with a single LLM call per
turn that sees:
* persona (what's already known — slots not re-asked)
* recent 10 chat_log messages (history)
* top-5 kei-sage atoms relevant to user_text (graph-RAG, not embeddings)
* raw user_text
LLM returns JSON {slot_updates, response_text, done, focus} which drives
the next state + persona patch + reply. No embeddings, no vector store —
kei-sage's FTS5 + Obsidian-style atom graph is the retrieval layer.
New files:
* src/retrieval.rs (101 LOC) — retrieve_context(chat_log, topics,
chat_id, query, history_n, atoms_k) -> RetrievalContext
* src/conversational.rs (157 LOC) — conversational_step
(state, persona, context, text, extractor, lang) -> StepOutput
Modified:
* src/serve.rs::run_fsm — branch on state: Intro/AskLanguage still go
through legacy handle_step (jump-start); everything else routes to
conversational_step with retrieval context.
* src/lib.rs — module declarations.
Tests (5 new, 60 total passing):
* parses_well_formed_llm_response
* done_true_transitions_to_ready
* invalid_json_falls_back_gracefully
* retrieve_returns_empty_on_empty_stores
* retrieve_finds_seeded_data
Verify:
* cargo check -p kei-buddy: PASS
* cargo test -p kei-buddy --lib: 60/0 (was 55, +5)
Why graph-RAG instead of embeddings: kei-sage already in tree (atoms +
edges + BFS + PageRank + FTS5). Explicit edges (message → topic →
contact) beat opaque cosine similarity for personal-assistant memory
where relationships are typed. No sqlite-vec dep, no embedding cost.
NOT deployed yet — needs server rebuild.
Closes engine bugs #1, #2, #3 from the user's backlog.md entry dated
2026-05-11 "kei-refactor-engine — 4 false-positive bugs". Bug #4 was
fixed in d2c966d8 (wikilink path-norm + handoff scanner removal).
## Bug #1 — vendored marketplaces skip
Engine was scanning `plugins/marketplaces/claude-plugins-official/` —
vendored upstream code where Constructor Pattern thresholds don't
apply. ~246 cp-violations were from this tree.
Fix: `tree::should_skip_path()` central filter. Skips any path
component named `marketplaces`, `target`, `node_modules`, or `.git`.
Applied via `WalkDir::filter_entry()` in `collect_markdown`,
`collect_with_ext`, `scanners::cp::scan`, `scanners::orphans::scan`,
`scanners::orphans::all_basenames`. `scanners::cp::skip_dir` now
delegates to `should_skip_path` (removed the older inline
`/target/`-substring check).
## Bug #2 — hooks-share-matcher false-positive class
Claude Code hook chains are designed to support N hooks per event by
design. `scanners::hooks` was flagging every pair sharing a matcher
as a "redundancy conflict" — 9 hooks/medium findings in the last
deep-sleep run, every one false-positive.
Fix: `scanners::hooks::scan` reduced to a no-op stub returning
`Vec::new()`. Module docstring documents the retraction + future
direction (a real `hooks-validity` scanner for broken shebangs,
missing chmod, syntax errors would replace it).
## Bug #3 — `.patch` file not unified diff
Already resolved in prior commit (v0.14.1 retraction in patch.rs):
CLI default is `plan-autoresolve.md`, Phase C template references
`-autoresolve.md` suffix, `write_patch` is deprecated shim. Only
legacy `.patch` artefacts in sync-repo/reports/ remain — those are
audit trail, not active.
## Phase C draft file emission (deep-sleep-trigger-prompt.md §6.d)
The earlier Phase C template emitted `proposed_rule` markdown blocks
only — no actionable artefacts. Extended §6 with step 6.d: when
WITH_FORK=1 AND fork branch was created, ALSO write skeleton draft
files into the branch:
sync-repo/sleep-deep/YYYY-MM-DD/drafts/rules/<slug>.md
sync-repo/sleep-deep/YYYY-MM-DD/drafts/hooks/<slug>.sh
Drafts follow pattern-codifier-agent Phase 3 templates. Phase C does
NOT register hooks — that's pattern-codifier's job via /sleep-review
morning click-flow (skill Phase 3a added in ~/.claude commit 49a320d).
This closes the loop: Phase C surfaces draft → morning review clicks
approve → pattern-codifier installs → settings.json registered.
Smoke-test required in §6.d: every emitted `.sh` MUST `bash -n` clean
or be excluded from commit + listed in plan markdown.
## Results on ~/.claude/memory/sync-repo (live data)
| Scanner | Before | After | Delta |
|-----------|-------:|------:|------:|
| orphans | 108 | 1 | -107 |
| hooks | 2 | 0 | -2 |
| cp | 174 | 0 | -174 |
| **TOTAL** | 284 | 1 | -283 |
On full ~/.claude scan: total drops from ~1614 (per 2026-05-11
backlog) to 983 (cp=186 + orphans=797 — orphan count high because
~/.claude tree has many memory/chatlogs/ refs out-of-tree).
## Tests
12/12 pass on kei-conflict-scan workspace (4 unit + 8 integration).
Pre-existing `oversize_file_flagged` + `orphan_wikilinks_flagged`
still green; new `cross_repo_wikilink_not_flagged` +
`path_prefixed_wikilink_matches_basename` from d2c966d8 still green.
Private mirror at ~/Projects/KeiSeiKit/_primitives/_rust/ synced
(4 files: tree.rs, scanners/cp.rs, scanners/orphans.rs,
scanners/hooks.rs).
Closes backlog "engine-noise-2026-05-11" tag bugs #1, #2, #3.
Three follow-up atomics on top of the contacts/topics/sync wave.
## 1. AskLanguage state + ru/en localisation (default en)
New state `AskLanguage` inserted between `Intro` and `AskName`. Intro now
sends a bilingual greeting + language picker. AskLanguage parses
en/english/1/ru/русский/2/etc → persona_patch{"language":"<code>"} →
transitions to AskName with that language's prompt.
All later prompts (AskName / AskTone / AskInterests / AskHobbies /
TopicSpecifics / TopicNowLater / TopicResearch / AskSchedule / Ready)
read persona.language via Lang::from_persona and dispatch through
Strings::* helpers — two language tables, no fallthrough.
Back-compat migration: existing chats without `language` key (like the
user currently in topic_now_later) get an implicit "ru" patch on next
turn so their Russian onboarding continues without regression.
New files: strings.rs (164), machine_lang.rs (145).
Modified: state.rs (+AskLanguage variant), machine.rs (Intro→AskLanguage,
AskLanguage arm, migration guard), machine_helpers.rs, machine_tests.rs.
5 new tests (intro_to_ask_language, ask_language_en, ask_language_ru,
ask_language_invalid, migration_sets_ru_when_language_missing).
## 2. Real proposeTopicSources — removed TODO(phase2) stub
machine_lang.rs::step_topic_research now calls
extractor.extract(prompt, topic_title) with a {name, url, why} schema.
Parses JSON, formats numbered source list, transitions to TopicSources.
Failure paths (LLM error, empty array): graceful fallback prompt asking
user to suggest their own — still transitions to TopicSources so flow
doesn't deadlock.
3 new tests in machine_tests_topic_research.rs:
topic_research_yes_proposes_sources,
topic_research_yes_empty_sources_still_advances,
topic_research_no_skips_topic_sources.
## 3. Voice-message handling (Telegram voice/audio → STT → text pipeline)
kei-telegram-webhook: added Voice/Audio sub-structs on Message and
WebhookEvent::Voice variant. classify() detects message.voice OR
message.audio. 2 new tests in event.rs.
kei-buddy/src/voice.rs (178 LOC):
VoiceHandler { bot_token, stt: Arc<dyn SttBackend>, http }
transcribe_file(file_id, mime_type) does:
1. GET https://api.telegram.org/bot{token}/getFile?file_id=...
2. GET https://api.telegram.org/file/bot{token}/{file_path}
3. SttRequest { audio_bytes, mime_type, language: None } → backend.transcribe
4. Returns transcript text.
2 wiremock tests (download chain + 500 error mapping).
serve.rs adds voice: Option<Arc<VoiceHandler>> to BuddyContext;
on_event Voice arm: whitelist check → transcribe → handle_text (same
pipeline as if user typed). Voice unavailable: warn + ignore.
serve_runner.rs builds VoiceHandler from KEI_BUDDY_STT_BACKEND env.
kei-stt added as optional dep gated by serve feature. Default backend
whisper-local (no extra build deps).
TTS reply path deferred (next atomic).
## Verify
* cargo check --workspace: PASS
* cargo test -p kei-buddy --lib: 55 passed / 0 failed (was 41 → 50 → 53 → 55)
* cargo test -p kei-telegram-webhook --lib: 7 passed (was 5, +2 voice)
* cargo build -p kei-buddy --release: PASS (23.7s)
NOT deployed yet — three new things to roll out next:
* новые миграции (нет — БД без изменений)
* новые env: KEI_BUDDY_STT_BACKEND (optional)
* установка faster-whisper / piper-tts на сервер для STT
(без него Voice event просто warn-логируется и игнорируется)
Three atomics finish phase 3 of kei-buddy contacts integration:
## kei-buddy: contact-sync glue + slash commands (+5 tests)
New src/contacts_sync.rs (146 LOC):
* SyncReport { fetched, added, skipped, errors }
* sync_from_google(access_token, contacts) — builds GooglePeopleClient,
list_connections, dedups by (name+email) via search_contacts,
add_contact in loop
* sync_from_apple(apple_id, app_pw, addressbook_url, contacts) — same
pattern over ICloudCardDavClient.list_contacts
* All errors collected into report.errors; never panics, never propagates
New slash commands in commands.rs / command_exec.rs:
* /sync-google — reads GOOGLE_OAUTH_ACCESS_TOKEN env, calls sync_from_google,
Russian-formatted summary "Google: загружено N, добавлено M, пропущено K"
* /sync-apple — reads APPLE_ID + APPLE_APP_PASSWORD + APPLE_CARDDAV_URL,
calls sync_from_apple
* Missing env → human-readable "не настроено: …" response
* /help text updated
Deps added: kei-contacts-google + kei-contacts-apple as path deps.
## kei-contacts-google: pagination via nextPageToken (+1 test)
Refactor: client.rs 182→56 LOC; pagination logic + deserialization moved
to new src/pagination.rs (188 LOC). list_connections unchanged
(back-compat, returns first page only). New list_all_connections loops
via fetch_page(Some(token)) until token=None; hard cap 50 pages with
tracing::warn on cap.
Test list_all_connections_two_pages: wiremock returns page 1 with
nextPageToken="abc" + page 2 without; assert len = sum AND second
request carries pageToken=abc query.
## kei-contacts-apple: vCard line-folding + CardDAV auto-discovery (+2 tests)
vcard.rs +unfold() helper applied in parse_vcard per RFC 6350 §3.2:
continuation lines starting with space/tab strip the prefix and append
to previous line. Test parse_folded_vcard.
New src/discovery.rs (199 LOC): discover_addressbook() walks
.well-known/carddav → current-user-principal → addressbook-home-set →
first addressbook with C:addressbook resourcetype. Three PROPFIND
requests with canned XML bodies. Regex-based extract_first_href_under +
extract_addressbook_href helpers. Test discover_walks_three_propfinds
against 3-step wiremock fixture.
client.rs adds discover_addressbook_url() method calling discovery.
## Verify-before-commit
* cargo check --workspace: PASS
* cargo test -p kei-buddy --lib: 46/0 (was 41)
* cargo test -p kei-contacts-google: 5/0 (was 4, +1 pagination)
* cargo test -p kei-contacts-apple: 9/0 (was 7, +1 folding +1 discovery)
NOT deployed — user still in live conversation with bot.
Follow-up (deferred, non-blocking):
* Real iCloud smoke test for discover_addressbook_url — regex parser
may need adjustment for deeply-nested namespace prefixes
* Wiremock-backed integration test for sync_from_google glue (HTTP
layer already covered in kei-contacts-google tests)
Two architectural bugs in orphans scanner — both surfaced by morning
/sleep-review of deep-sleep/2026-05-12-0400 (108 false-positive
orphan-wikilinks; the engine was scanning sync-repo MEMORY.md and
flagging every `[[../../../rules/X]]` cross-repo ref as broken).
1. Asymmetric normalization in extract_wikilinks
- `all_basenames(root)` indexed file_stem (lowercase, no path)
- `extract_wikilinks` returned lowercased FULL link text including
`../../../`-prefix and `subdir/` segments
- Result: `[[chatlogs/X/Y]]` never matched `Y.md` in index, every
`[[../../../rules/X]]` always flagged orphan
Fix: `normalize_target(raw) -> Option<String>` strips path prefix,
strips `.md` suffix, returns None for `../`-rooted refs that escape
the scan tree (engine cannot validate cross-repo targets).
2. extract_handoffs scanner removed
- Regex `^\s*-\s*\*\*([a-z0-9][a-z0-9_-]{2,})\*\*` was matching every
prose bold-bullet, e.g. `- **english-jargon** — last 7d:` in
backlog.md or `- **L1-Path-C**:` in chatlogs.
- sync-repo scan: 0 real handoff sections present, 100% of matches
were prose. Real handoff syntax in agent-graph repos uses YAML
frontmatter, not prose markdown bullets.
- Scanner deleted along with its helper; wikilink scanner alone
covers the explicit `[[...]]` ref use case.
## Result on sync-repo (live data)
| Metric | Before | After |
|----------------|-------:|------:|
| orphan refs | 108 | 1 |
| false-positive | 107 | 0 |
Remaining 1 = legitimate `[[wikilink]]` literal in backlog.md prose.
## Tests added (already present in HEAD via prior fleet commit)
- `tests::cross_repo_ref_skipped` — `../../../foo` -> None
- `tests::path_prefixed_target_basenamed` — `chatlogs/X/Y` -> "Y"
- `tests::plain_basename_passes_through`
- `tests::md_suffix_stripped`
- integration `cross_repo_wikilink_not_flagged` (E2E)
- integration `path_prefixed_wikilink_matches_basename` (E2E)
12/12 tests pass. Release binary rebuilt + installed to ~/.cargo/bin/.
Private mirror at ~/Projects/KeiSeiKit/_primitives/... synced.
Closes backlog.md "engine bug #4" (added by user via prior /sleep-review).
Two parallel atoms in one commit. Both reuse existing KeiSeiKit
primitives (zero new crates) per RULE feedback_inventory_before_decompose.
## src/contacts.rs (200 LOC, +4 tests)
Adapter over kei-social-store. Address book + interaction log + relationship
graph for shared connections.
API:
* Contacts::from_path / from_memory
* add_contact / get_contact / search_contacts
* log_meet(person_id, target_id, channel, note) / interactions_for
* relationship_graph — returns Vec<Pair>, the kei-social-store output
* common_connections(a, b) — post-filters relationship_graph to find
target_ids that appear in pairs with BOTH a and b. This is the
"у нас с Денисом общий друг X" feature.
Pattern: Arc<Mutex<kei_social_store::Store>> + tokio::spawn_blocking,
mirroring chat_log.rs. Errors map to BuddyError::Memory.
Tests: add_and_get_contact_roundtrip / search_contacts_finds_by_name /
log_meet_and_list_interactions / common_connections_finds_shared_target.
## src/topics.rs (200 LOC, +4 tests)
Adapter over kei-sage. Topics + digest notes + FTS5 search. Each topic
is a sage Unit{unit_type="buddy_topic", category="kei-buddy",
source_path="kei-buddy/chat-{chat_id}/topic/{slug}"}. Digests are
Unit{unit_type="buddy_digest"} linked via add_edge(topic→digest,
edge_type="digest_for").
API:
* Topics::from_path / from_memory
* add_topic(chat_id, slug, title, content) — idempotent via path lookup
* add_digest(chat_id, topic_slug, timestamp, content) — creates Unit +
edge
* search(query, limit) — fts_search over all kei-buddy units
* digests_for(chat_id, topic_slug) — follows outgoing edges
* list_topics(chat_id) — raw SELECT scoped by source_path LIKE prefix
Tests: add_topic_then_search_finds_it / add_topic_is_idempotent /
add_digest_creates_edge_and_dest / list_topics_scopes_per_chat.
## Dependencies added
kei-social-store + kei-sage as local path deps. Both already in workspace,
no new external crates.
## Verify-before-commit
* cargo check -p kei-buddy: PASS
* cargo test -p kei-buddy --lib: 31/0 (was 23, +4 contacts +4 topics)
Net change: 4 files touched, ~400 LOC added across the two adapters.
NOT deployed. User still in active bot conversation.
After-Ready conversation was going to /dev/null. With this change every
inbound Telegram text + every bot response is persisted to a SQLite +
FTS5 archive via the existing kei-chat-store primitive (no new crate).
Each Telegram chat_id maps 1:1 to a kei-chat-store session
(project="kei-buddy", title="tg-<chat_id>", model="telegram"). Cache
prevents per-message session lookups.
New file:
* src/chat_log.rs (198 LOC) — ChatLog adapter wrapping
kei_chat_store::Store + a chat_id→session_id Mutex cache.
API: from_path / from_memory / ensure_session / log_user /
log_bot / search(query, chat_id?, limit). Errors map to
BuddyError::Memory and never propagate from on_event — chat-log
failure is logged but does not block the conversation.
Modified:
* Cargo.toml — kei-chat-store path dep added.
* src/lib.rs — pub mod chat_log + re-export ChatLog.
* src/serve.rs — BuddyContext gains Arc<ChatLog>;
process_text calls log_user before handle_step + log_bot after
send_message; ServeConfig gains chat_log_db_path.
* src/bin/kei-buddy.rs — KEI_BUDDY_CHAT_LOG_PATH env
(default ./kei-buddy-chat.db); migrate subcommand applies the
chat-store schema alongside buddy_state schema.
Tests (3 new in src/chat_log.rs, all pass):
* log_user_creates_session_and_message
* log_bot_uses_same_session_as_log_user
* different_chats_get_different_sessions
Verify-before-commit:
* cargo check -p kei-buddy (default): PASS
* cargo check -p kei-buddy --features extractor-openai: PASS
* cargo test -p kei-buddy --lib: 23 passed / 0 failed
(was 20 before this commit; 3 new ChatLog tests)
NOT deployed — user is in active conversation with the live bot.
Will roll forward when user signals readiness.
Two additions on top of the MVP serve binary:
1. Whitelist by chat_id (KEI_BUDDY_ALLOWED_CHAT_IDS env, CSV).
* BuddyContext gains Arc<Option<Vec<i64>>> allowed_chat_ids
* chat_allowed() check fires before process_text
* Non-whitelisted chats: warn-log + ignore (no response sent)
* None or empty list = accept all (back-compat with prior behaviour)
2. Real LLM wiring (KEI_BUDDY_LLM_PROXY / _LLM_KEY / _LLM_MODEL).
* When extractor-openai feature compiled in AND both proxy+key set,
run_serve instantiates OpenAiExtractor instead of MockExtractor
* Defaults: proxy=https://api.openai.com, key=OPENAI_API_KEY env,
model=gpt-4o-mini
* Fallback: warns + MockExtractor (state machine still walks, but
LLM-extracted fields are empty)
* extractor::OpenAiExtractor gains new_with_model(proxy, key, model);
model is now per-instance instead of compile-time DEFAULT_MODEL
3. start_listener extracted as helper — keeps run_serve readable across
the two feature-gated branches.
Verify-before-commit:
* cargo check -p kei-buddy (default): PASS
* cargo check -p kei-buddy --features extractor-openai: PASS
* cargo test -p kei-buddy --lib: 20/0 unchanged
Critical-path math (cargo workspace 105 crates × 3 matrix targets):
- Current profile: opt-level=z + lto=true + codegen-units=1 = compile
cost ~10-20× over default; observed wall-time ~17min/release run
- After P1+P2 stack: predicted ~4-5min cold, ~1.5min warm
== P1 — _primitives/_rust/Cargo.toml profile.release ==
- lto: true → "thin" (full LTO is 3-5× slower; thin keeps most opts)
- codegen-units: 1 → 16 (parallel codegen restored, was serial)
- Binary size cost: ~10-15% larger (acceptable for non-embedded targets)
- VERIFIED: cargo check --workspace exits clean
[REAL: ran in this session; 0 errors, warnings only]
== P2 — mold linker for Linux targets ==
- New: _primitives/_rust/.cargo/config.toml (7 LOC)
* x86_64-unknown-linux-gnu + aarch64-unknown-linux-gnu use clang+mold
* macOS targets unaffected (use system ld + LLVM)
- New step in .github/workflows/release.yml::build-release:
Install mold linker (Linux only) — apt-get mold clang
Gate: `if: contains(matrix.target, 'linux')`
- Inserted AFTER rust-toolchain BEFORE rust-cache
- Predicted gain: link phase 60s → 6s on Linux entries
== P3 — explicitly NOT applied ==
- Path-filter on docs-only commits considered + rejected per task spec:
Release tags should always rebuild even if commit only touches docs.
Files:
- _primitives/_rust/Cargo.toml (+2/-2 LOC)
- _primitives/_rust/.cargo/config.toml (NEW, 7 LOC)
- .github/workflows/release.yml (+5/-0 LOC, mold install step)
[ESTIMATE-HTC: rustc + mold benchmarks claim 3-5× and 5-10× respectively
on full release builds — not re-benchmarked on this 105-crate workspace
yet; will measure on next v* tag push]
NOTE: this commit does NOT retag — keigit publish 401 issue is on the
keigit-server side (verified: token works locally, 401 from runner IP)
and requires user-side action (fail2ban/Caddy whitelist GitHub Actions
IP ranges on 45.77.41.204). After user fixes that, next tag will
verify both speed gain AND publish success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.14.4 failed with same 401 despite local-probe showing path-scoped +
Basic-auth fallback work. Adding a diagnostic step BEFORE publish:
- npm whoami against keigit
- curl Bearer probe (read endpoint /api/v1/user)
- curl PUT probe (publish endpoint with empty body)
- npm config dump (registry resolution)
Will reveal:
- Whether token actually authenticates from runner network
- Whether npm correctly resolves @keisei:registry to keigit URL
- Whether something in CI environment is rewriting/blocking the auth header
Bump 0.14.4 → 0.14.5 to trigger fresh release run.
[FROM-JOURNAL: this session — local probe confirms .npmrc form works,
CI rejects with 401, narrowing to runner-environment issue]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.14.2 publish run reported "success" but @keisei/mcp-server NEVER
landed on keigit because:
1. Host-scoped `.npmrc` token (`//keigit.com/:_authToken=...`) was
silently ignored by npm 10 — every publish errored with ENEEDAUTH.
2. The publish loop's `|| echo ":⚠️:"` swallowed the failure
so the job exited 0 (W1+W3 finding F3).
Two fixes in one commit:
A) Path-scoped npmrc per Forgejo docs:
`//keigit.com/api/packages/keisei/npm/:_authToken=${KEIGIT_TOKEN}`
+ `always-auth=true` for scoped registry. Also tee'd to $HOME/.npmrc
so the publish loop's `cd packages/<pkg>` cwd doesn't lose the auth
line. [VERIFIED: curl PUT with Bearer to /api/packages/keisei/npm/
returns 400 "package is invalid" (auth ACCEPTED, payload bad) — auth
format is correct]
B) Hard-fail publish loop for packages with publishConfig:
- Iterate all packages
- For each: read .publishConfig presence
- If publish errors AND has publishConfig → record gated_failed=1
- If publish errors AND no publishConfig → notice "skipped" (adapter
without registry pin reached npm.org default, expected fail)
- End of loop: exit 1 if any gated_failed
- Adapters without publishConfig (gmail/grok/recall/telegram/youtube)
correctly skip; only @keisei/mcp-server is gated, and a real
failure now blocks the job.
Bump 0.14.2 → 0.14.3 (0.14.2 tag exists with previous failed publish).
Verification done locally:
- PAT owner Parfionovich is member of org keisei [REAL: api/v1/user
+ api/v1/users/Parfionovich/orgs]
- Bearer auth to keigit npm registry works [REAL: curl probe → 400
"package invalid", not 401 "unauthorized"]
- Cargo workspace clean [REAL: cargo check exit 0]
After tag v0.14.3:
- npm-publish job creates .npmrc with path-scoped auth
- Publishes @keisei/mcp-server@0.14.3 to https://keigit.com/api/packages/keisei/npm/
- Adapters skip cleanly (no publishConfig, no NPM_TOKEN)
- Job exits 0 only if mcp-server actually landed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.14.1 tag triggered Release workflow but npm-publish was SKIPPED
because Rust matrix entry x86_64-apple-darwin failed and release
job needs:[build-release, build-mcp-binary]; npm-publish needs:release.
Single Rust target failure → entire publish chain blocks. This was
the W3 Opus CI/build finding deferred from audit-batch-2.
Two fixes:
1. **Drop x86_64-apple-darwin from build-release matrix.**
GitHub's `macos-latest` runner is now Apple Silicon (M1+); cross-compile
to x86_64 needs an OpenSSL sysroot that the arm64 image doesn't ship.
`openssl-sys 0.9.114` build fails with "Could not find openssl via
pkg-config: pkg-config has not been configured to support
cross-compilation". Apple Silicon mandatory for new Macs since 2020;
x86 Mac is legacy. If a future user needs x86 darwin, re-add with
`experimental: true` and `openssl-sys` features=["vendored"].
2. **Decouple `npm-publish` from `release`.**
The npm package builds its own `dist/` from `_ts_packages/` — it does
NOT consume Rust release tarballs. Previously `needs: release` meant a
single Rust matrix failure blocked the npm publish even though the two
are architecturally independent. Now `needs: []` (parallel with
build-release matrix). KEIGIT_TOKEN-presence guard still gracefully
skips when secret is absent.
Bump version 0.14.1 → 0.14.2 (v0.14.1 tag already exists from prior run).
After re-tag v0.14.2:
- build-release matrix: 3 targets (was 4) — should all succeed
- build-mcp-binary: 5 platforms (unchanged) — already passed in 0.14.1 run
- release job: produces GitHub Release with 3 Rust tarballs + 5 MCP binaries
- npm-publish job: runs in PARALLEL, publishes @keisei/mcp-server@0.14.2
to keigit regardless of Rust matrix status
[FROM-JOURNAL: tasks.jsonl this session — v0.14.1 release run 25280711426
ran 14m wall, 8/9 jobs success, x86_64-darwin failed at openssl-sys
build, release+npm-publish skipped via needs-chain]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-tag bump. publishConfig.registry already pinned to
https://keigit.com/api/packages/keisei/npm/. KEIGIT_TOKEN secret
configured on github KeiSei84/KeiSeiKit-1.0 repo. keigit org
`keisei` (id=5) created and verified live.
Verification:
- `npm run build --workspace=@keisei/mcp-server` exits 0
[REAL: ran in this session]
- dist/index.js produced (4125 bytes)
- Token works: `GET /api/v1/user` with PAT → 200
- Registry empty: `GET /api/packages/keisei/npm/` → 404 (expected)
After tag v0.14.1 pushes, the release workflow's npm-publish job
runs `npm publish --access public` which routes via publishConfig
to keigit. Expected: package lands at
https://keigit.com/keisei/-/packages/npm/@keisei%2Fmcp-server
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12-agent audit (waves 3+4 Opus+Sonnet) on commit 88de01c found that 2 of
my prior fixes had regressions, plus the prev batch missed 8 stale-text
sites and 2 latent bugs. This batch closes them all.
== Regressions in audit-batch (88de01c) — now fixed ==
1. PRAGMA user_version=9 placement — could silently downgrade schema on
cross-version install (existing v10 DB → re-run reset to 9 →
migrations replay → ALTER TABLE duplicate-column errors)
- install/sql/outcome-only-schema.sql: PRAGMA moved OUTSIDE the
transaction (after COMMIT) for portability across SQLite versions
- install/lib-profile-outcome-only.sh::_outcome_install_ledger:
added downgrade guard — reads existing user_version BEFORE running
ANY init path; if >9, skips entirely (preserves newer schema)
- VERIFIED: simulated v10 DB → re-run prints "skipping init to
preserve newer schema"; user_version stays at 10 (was downgraded
to 9 in the prior batch) [REAL: ran in this session]
2. backup_file mv→cp workaround left orphan backups + bypassed rollback
contract (BACKUP_PAIRS not registered)
- install/lib-profile-outcome-only.sh: now manually appends to
BACKUP_PAIRS so rollback trap restores on later failure;
removes the .bak on success path
- Comment updated to explain the workaround vs backup_file mv
3. CLAUDE.md skip-guard "STATUS-TRUTH MARKER" was too broad —
false-positive on existing kit users (RULE 0.16 doc text matches)
- lib-profile-outcome-only.sh: changed grep to literal HTML comment
marker `<!-- outcome-only profile (KeiSeiKit) -->` (specific marker
written by the installer itself)
== Tier 1 missed in prev batch — now fixed ==
4. _ts_packages/package-lock.json referenced packages/cortex-ui which
does NOT exist on disk → npm ci would fail with ELSPROBLEMS in CI
- Regenerated via fresh `rm package-lock.json && npm install`
- npm ci now exits 0 cleanly [REAL: ran in this session]
- Lockfile shrunk 2403→0 lines on the cortex-ui section (full regen)
5. v3 triggers (branch length cap ≤256) were MISSING from
outcome-only-schema.sql — sqlite3 fallback path skipped a schema
feature that the Rust kei-ledger flow enforces, creating cross-flow
drift
- Added trg_agents_branch_len_ins + trg_agents_branch_len_upd
mirroring migrations_list.rs:30-44
- Header comment in outcome-only-schema.sql rewritten to match
current behavior (was stale)
- VERIFIED: end-to-end install creates 2 triggers [REAL: sqlite3
.schema | grep trg_agents_branch_len returns 2]
6. README.md:232 said "102 crates" while README.md:9 said "105 crates"
— internal contradiction in same doc
- README:232 → "105 workspace crates"
7. ARCHITECTURE.md:165 "53 Rust crates + 13 shell primitives" stale
- Updated to "105 Rust workspace crates (47 declared in MANIFEST.toml
`full` profile) + 14 shell primitives"
8. ARCHITECTURE.md:157 "45 /commands" stale
- Updated to 68
9. plugin.json + marketplace.json description strings still had
pre-fix counts (23 primitives / 39 skills / 9 hooks / 12 agents)
- Both rewritten to match README:9 SSoT (38 agents / 68 skills /
38 hooks / 105 workspace crates / 47 installable + 14 shell)
10. PROFILE-OUTCOME-ONLY.md:28-29 "What does NOT get installed" still
cited 102/67/37/82
- Updated to 105/68/38/85
11. encyclopedia/substrate-overview.md §6/§11/§12 still said
"80-char DNA"; §13 said "495 DNA indices"; §6 said "11 install
profiles (.../Cursor/Continue/etc)"
- All 4 sites fixed to current language (≥33-char variable, 565
DNAs, 12 install profiles)
12. docs/DNA-INDEX.md:1352 said wire format is "(80 chars)"
- Updated to "(≥33 chars; role + caps slugs are variable — see
docs/DNA-FORMAT.md)"
== Tier 2 honesty fixes ==
13. Wagner et al. 2004 citation in SLEEP-LAYER.md:26 lacked [VERIFIED]
marker (W3 doc consistency caught it)
- Added [VERIFIED: doi:10.1038/nature02223] + clarification that
the original study did not isolate a specific sleep stage; SWS
attribution comes from secondary literature (Diekelmann/Born)
14. PHILOSOPHY.md:125 attributed "overnight consolidation of un-finished
intentions" to Wagner 2004 — that paper is about insight gain on
the Number Reduction Task, not Zeigarnik-effect cued memory
- Rewritten to accurately describe Wagner 2004's actual finding +
[VERIFIED: doi:10.1038/nature02223]
Verification:
- `npm ci` in _ts_packages/ exits 0 [REAL: ran in this session]
- `cargo check --workspace` exits 0 in _primitives/_rust [REAL: ran in
this session]
- Outcome-only end-to-end fresh install produces user_version=9 +
2 triggers (correct schema shape)
- Outcome-only re-run against v10 DB preserves user_version=10
(downgrade guard works)
- CLAUDE.md skip-guard now triggers ONLY on literal marker, not on
RULE 0.16 phrase
NOT addressed in this batch (deferred to a future round):
- github KeiSei84/{KeiSeiKit, KeiSeiKit-1.0} 404 (user-side action:
publish repo or update refs)
- keigit user `keisei` does not exist (user-side: create org or
rename scope)
- KEIGIT_TOKEN secret not configured (user-side action)
- Forgejo registration disabled (admin-side)
- safeEqual timing leak in TS server (LOW per W3 reassessment)
- HTTP bind 0.0.0.0 default (MEDIUM)
- Unbounded request body (MEDIUM)
- Outcome-only confirm-screen bypass (RULE 0.1 spirit)
- Ledger fallthrough false summary
- Node 20 deprecation (deadline 2026-06-02, 30 days)
- Hook count triple-discrepancy (38 README / 53 DNA-INDEX / 35 maturity-row)
- 100-row router claim still in README:117 + PROFILE-OUTCOME-ONLY.md
- INSTALL.md numerics without [REAL:] markers
- Stale .bak files accumulation policy (cosmetic)
- README per-claim [REAL: ] markers for 6 of 7 numerics
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12-agent audit (2 waves Opus+Sonnet, 6 slices each) flagged 3 HIGH-tier
issues that BOTH waves agreed on, plus 5 doc-honesty findings. This
batch fixes the lot.
== CI green (was failing on main 1207cf5) ==
- _primitives/_rust/Cargo.toml — workspace tokio gains `io-std` feature
(needed by kei-mcp/src/main.rs which calls tokio::io::{stdin,stdout})
- _primitives/_rust/kei-mcp/Cargo.toml — dev-deps tokio gains `test-util`
feature (needed by tests/tools_call_timeout.rs for tokio::time::advance
and Builder::start_paused). Both verified locally:
`cargo check -p kei-mcp` ✓
`cargo test --no-run -p kei-mcp` ✓ (3 test binaries link)
[REAL: ran 2026-05-03 in this session]
== HIGH-tier audit fixes (consensus across waves) ==
1. SQLi escape in agent-outcome-backfill.sh:110
- 4 of 12 agents flagged: TOOL_USE_ID was JSON-derived and
interpolated raw into SQL. Allowlist on $SHIPPED protected today
but a future case-statement removal opened the surface.
- Fix: tiny `_sql_esc` helper that doubles single-quotes (SQL-99
standard escape), applied to SHIPPED + TOOL_USE_ID. STUBS already
integer-validated.
2. PRAGMA user_version=9 in install/sql/outcome-only-schema.sql
- W1 outcome-only critic flagged: the SQL fallback installed a
v9-equivalent flat schema but left user_version=0. A LATER
`kei-ledger init` (e.g. when user upgrades to full kit) would
re-run migrations v1-v9 and ALTER TABLE ADD COLUMN duplicate-error
mid-migration → broken DB.
- Fix: set PRAGMA user_version=9 before COMMIT so the binary's
migration runner sees current ≥ target and short-circuits.
3. backup_file mv→cp + uninstall macOS-portable awk
- W1+W2 outcome-only flagged: lib-backup.sh uses `mv` which DELETES
the target before _jq_merge_hooks runs; `|| true` swallowed the
subsequent jq read-error → silent settings.json loss.
- Fix in lib-profile-outcome-only.sh: `cp -p` aside, drop `|| true`,
return 1 on merge failure (trap restores).
- PROFILE-OUTCOME-ONLY.md uninstall used GNU sed `,+1` extension
which BSD sed (macOS) does not support — uninstall silently
no-op'd on macOS, leaving orphan CLAUDE.md text.
- Fix: replace with portable `awk` recipe; also added `rm -f` for
the agent-toolstats.jsonl sidecar (privacy completeness).
== Doc honesty pass (RULE 0.18 numerics + RULE 0.4 citations) ==
4. README.md count drift — verified all values against filesystem:
* 102→105 Rust crates (Cargo.toml workspace `members` count)
* 67→68 skills (`ls skills/ | wc -l`)
* 35→38 hooks (`grep -c '"command":' settings-snippet.json`)
* 37→38 agent manifests (`ls _manifests/*.toml | wc -l`)
* 82→85 substrate blocks (`find _blocks/ -name '*.md' | wc -l`)
* 18 capability atoms VERIFIED via `find _capabilities/ -name '*.md'`
(encyclopedia §3 row count of 17 is in a separate file and is a
known internal display issue, not changed in this commit)
* 495→565 active DNAs (per docs/DNA-INDEX.md header 2026-05-03)
Each value now carries a `[REAL: <command>]` style trailer per
RULE 0.18.
5. README.md DNA "80-char identity" → "≥33-char variable-length"
- W1+W2 reviewer-pass flagged FALSE: docs/DNA-FORMAT.md SSoT says
minimum 33 chars; 80 was nowhere in code or spec
- Fix in README.md:36 + docs/PHILOSOPHY.md:39 + docs/DNA-INDEX.md:1352
6. README.md "Eleven install profiles (... Cursor / Continue / Zed /
Aider / Docker / Nix)" — Cursor/Continue/Zed/Aider/Docker/Nix were
never install profiles, they were bridge targets
- Fix: list 12 actual profiles from _primitives/MANIFEST.toml,
mention bridges as separate concept
7. .claude-plugin/plugin.json license MIT → Apache-2.0
- W2-Sonnet reviewer flagged: LICENSE file is Apache-2.0 (since
2026-04-30 per NOTICE), but plugin.json still declared MIT —
plugin marketplace would show wrong license
8. docs/ARCHITECTURE.md:318 placeholder URL `https://example.invalid/...`
- W2-Sonnet reviewer flagged: dead link in published docs
- Fix: remove the bad href, describe ssl-rule-file as per-user
install outside the public repo
9. skills/sleep-on-it/SKILL.md Wagner et al. 2004 citation
- W1+W2 reviewer flagged RULE 0.4 violation: citation without
verification marker
- Fix: added [VERIFIED: doi:10.1038/nature02223] + clarification
that the original paper showed slow-wave-sleep (not strictly REM)
insight gain — our metaphor is a loose mapping
10. encyclopedia/substrate-overview.md §5 fabricated TS deps
- W1-Opus doc-consistency flagged RULE 0.4.b violation: 5 of 6
package rows had INVENTED dependency strings
(`recall-ai-sdk ^1.0.0`, `nodemailer-mock ^2.0.0`,
`telegram-typings ^4.10.0`, etc — none exist in the actual
package.json files)
- Fix: regenerated table from real `package.json` reads via
`node -p "require(...).dependencies"` for each of the 6 packages
- Fix: also corrected version drift (5 packages all 0.14.0 now)
Verification:
- Outcome-only end-to-end install against fake $HOME succeeds:
hooks installed, ledger schema at user_version=9, settings.json
created cleanly, all 5 documented files present
[REAL: ran 2026-05-03 in this session]
- `cargo check -p kei-mcp` + `cargo test --no-run -p kei-mcp` clean
Audit findings NOT yet addressed (deferred to next batch):
- README:65 git clone github URL — repo is private; reviewer flagged
external strangers cannot clone; will resolve via Quick Start rewrite
- npm.pkg.github.com / @keisei84 leftover sweep — both waves verified
ZERO refs, no fix needed
- safeEqual timing leak in TS server (W2 sec MEDIUM)
- HTTP server bind 0.0.0.0 (W2 sec MEDIUM)
- Unbounded request body (W2 ci MEDIUM)
- --dry-run silent ignored on non-outcome profiles (W1+W2 MEDIUM)
- Doc-link missing for MEMORY/DNA/LEDGER format specs from README
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire @keisei/mcp-server publish to the author-operated keigit.com
Forgejo npm registry. Verified live: keigit.com → 45.77.41.204 (Vultr,
public DNS), Caddy → Forgejo 9.0.3, TLS valid, /api/v1/version=200.
Why keigit, not GitHub Packages or npm.org:
- keigit IS the canonical npm registry for the @keisei scope (operator
runs it; no separate vendor account needed)
- npm scope @keisei stays @keisei (no rename to match a github org)
- Public DNS resolves from any client; auth via per-user PAT
- One auth surface for both the git remote and the npm registry
Files changed (7):
- _ts_packages/packages/mcp-server/package.json
· removed `private: true` (was blocking ALL publish, including ours)
· added publishConfig.registry = https://keigit.com/api/packages/keisei/npm/
so accidental `npm publish` cannot route to npm.org
· added repository field (provenance link to KeiSeiKit-1.0)
· added license: Apache-2.0
- README.md (2 hunks): maturity row + install section say
"published to keigit.com", show ~/.npmrc setup
- PLUGIN.md (3 hunks): same updates referencing keigit
- .claude-plugin/mcp-template.json: _comment updated
- docs/encyclopedia/substrate-overview.md (1 hunk): MCP row says
"alpha" not "stable" + clarifies registry+scope
- .github/workflows/release.yml: npm-publish job rewired:
· KEIGIT_TOKEN secret instead of NPM_TOKEN as gate
· Two-row .npmrc temp-write: @keisei → keigit.com (always when
KEIGIT_TOKEN set), npm.org auth as optional fallback
· .npmrc cleanup via `if: always()` step
- .gitignore: _ts_packages/.npmrc + .npmrc excluded (RULE 0.8)
Verification:
- node -e 'require("./.../package.json")' parses clean,
publishConfig pinned to keigit, private:false [REAL: ran in session]
- `npm run build --workspace=@keisei/mcp-server` → tsc -b exit 0,
dist/index.js produced [REAL: built in session]
- Server starts: `node dist/index.js` lives >1s, doesn't throw,
reports expected `[adapters] not installed` for un-built siblings
- keigit.com reachable from this machine: HTTP 200 root + Forgejo
9.0.3 version endpoint [REAL: curl ran in session]
Required user-side setup before first publish:
1. Create user/org `keisei` on keigit.com (web UI; currently /keisei → 404)
2. Generate a keigit PAT with write:package scope
3. Add as github repo secret KEIGIT_TOKEN
4. Push tag v0.14.1+ → release workflow's npm-publish job picks it up
History note:
- Earlier in this session a github-packages-scope-rename variant
(commit a5ef896) was pushed; reverted by 083bc06 because keigit
is the right registry. Current commit lands the keigit wiring on
top of the revert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renamed @keisei/mcp-server → @keisei84/mcp-server (scope must match
github org KeiSei84 for GitHub Packages publish). Replaced private:true
with publishConfig pinned to npm.pkg.github.com so an accidental
`npm publish` cannot leak to npm.org. CI npm-publish job rewired to
GitHub Packages auth (GITHUB_TOKEN with packages:write permission).
Why GitHub Packages, not npm.org:
- Authentication piggybacks on existing github org / PAT — no separate
account or NPM_TOKEN required for the core kit
- Scope @keisei84 maps 1:1 to org KeiSei84 (npm rule for github)
- Doesn't require public DNS for our private Forgejo (Tailscale-only
100.91.246.53 cannot be the publish target — IP-leak in public ref)
- Published artefacts live under github.com/orgs/KeiSei84/packages,
same access surface as the source repo
Why not @keisei (un-scoped or different scope):
- npm scope @keisei IS reachable on npm.org but we don't own it there
(would require email-verified npm account claim + ongoing maintenance)
- @keisei84 requires zero new accounts; works the moment KeiSei84 org
has packages enabled (github default)
Files changed (11):
- _ts_packages/packages/mcp-server/package.json — rename + publishConfig
+ repository field (required by GitHub Packages); removed private:true
- _ts_packages/package-lock.json — regenerated via `npm install`
(workspace recognises @keisei84/mcp-server symlink)
- README.md (2 hunks) — maturity row says "alpha" not
"alpha (unpublished)"; install section documents `~/.npmrc` setup
for `@keisei84:registry=https://npm.pkg.github.com/`
- PLUGIN.md (3 hunks) — same `~/.npmrc` setup; .mcp.json references
@keisei84/mcp-server; "not yet on npm" replaced with "lives on
GitHub Packages, not npm.org"
- .claude-plugin/mcp-template.json — args use @keisei84 scope
- _ts_packages/README.md (4 hunks) — package layout + npx examples
- docs/INSTALL.md, install/lib-rust.sh — comment refs
- docs/encyclopedia/substrate-overview.md (2 hunks) — package table +
publishing notes (was "published to keigit.com npm" — wrong; keigit
is a separate community-publish path for user-contributed packages,
not the destination for core @keisei84 packages)
- .github/workflows/release.yml — npm-publish job rebuilt:
· permissions: packages:write
· Two-scope .npmrc temp-write: @keisei84 → npm.pkg.github.com (always),
@keisei → npm.org (only if NPM_TOKEN secret set, else skipped per pkg)
· NODE_AUTH_TOKEN sourced from GITHUB_TOKEN
· .npmrc cleaned up via `if: always()` step
- .gitignore — _ts_packages/.npmrc + .npmrc excluded (RULE 0.8: auth
tokens never in git; CI temp-creates per-job)
Verification:
- `npm install` clean against new scope: node_modules/@keisei84/mcp-server
symlinks to packages/mcp-server, other adapters untouched in
node_modules/@keisei/* [REAL: install ran 2026-05-03 in this session]
- `npm run build --workspace=@keisei84/mcp-server` produces dist/index.js
[REAL: tsc -b exit 0]
- Server starts cleanly: `node dist/index.js` runs >1s, emits expected
"[adapters] not installed" warnings for un-built sibling adapters,
doesn't throw
- 17 references to old @keisei/mcp-server scope migrated; 0 left
[REAL: grep -rn "@keisei/mcp-server" returns 0 lines]
Bad-commit-hygiene note:
- Two earlier local commits (cb8dc2a + revert 474fe1c) attempted a
keigit.com-pinned variant; soft-reset past them so this commit lands
on top of public 368df5b. Bad commits never reached remote.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by parallel Haiku writer agents during 4-wave audit; covers the
substrate at the top-down explanatory level the reviewer asked for.
- substrate-overview.md (425 LOC) — top-down: what runs at install,
daily, nightly; data-flow ASCII diagrams; how the 4 layers fit
- hooks-and-blocks.md (394 LOC) — every hook + every assembler block,
with trigger event + severity + rule reference
- rust-crates-A-G.md (507 LOC) — first third of the 106 crates, one
paragraph per crate
- rust-crates-H-N.md (194 LOC) — middle third
- rust-crates-O-Z.md (59 LOC) — last third (smaller because alphabet)
- skills-and-agents.md (160 LOC) — 67 skills + 43 agent manifests,
one row each
Encyclopedia complements the auto-generated DNA-INDEX.md: that file
is mechanical (count + DNA prefix + sha8), this is narrative
(what does this thing do, when does it fire, how to use it).
Username-path leaks scrubbed via sed pre-commit:
- /Users/<user>/Projects/KeiSeiKit-public/ → <repo>/
- /Users/<user>/ → <home>/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
External reviewer raised 7 overclaim/scope concerns. Agents verified each
against source; this commit applies all fixes that landed in docs.
Honesty pass:
- README:25-29 — Cortex daemon track listed as alpha (was beta); MCP server
marked "alpha (unpublished) — install via local dist build"; Phase B
noted "auto-codification not yet wired (manual via /escalate-recurrence)";
keigit framed as author-operated mirror (KeiSei84 / private Forgejo),
not neutral community service
- README:95-97 — Cortex CLI/daemon track downgraded beta→alpha
with rationale (browser-app + VSCode-extension are concept-level)
- docs/ARCHITECTURE.md — added "Model router — current state (2026-05-03)"
subsection: per-call fixed estimate routing, NO 100-row Bayesian threshold
in current source (select.rs:74-124); reviewer suggestion deferred
- docs/SLEEP-LAYER.md — added Phase B scope clarification: morning report
is read-only markdown, no auto-codification path
- docs/PUBLISHING.md — aligned framing with README:43 ("author-operated
mirror" not "community registry"); added vendor-neutrality note that
substrate works against any npm-compatible registry
- mcp-server/package.json — added "private": true and description note
to prevent accidental publish before maturity gate
Portable format specs (reviewer asked for memory-repo agnosticism):
- docs/MEMORY-FORMAT.md (196 LOC) — JSONL schemas for traces / decisions /
agent-events with jq/awk/pandas recipes, grounded in actual writers
- docs/DNA-FORMAT.md (159 LOC) — DNA wire format ("type::caps::sha8")
with shell+python parsers
- docs/LEDGER-SCHEMA.md (199 LOC) — full SQLite DDL (agents +
skill_invocations + indexes + triggers) with sample queries
Auto-regen artifact:
- docs/DNA-INDEX.md — kei-registry regenerated count 564→565
Verification:
- All claims traced to file:line in source by agent a52b29ae
- All new docs ≤200 LOC per Constructor Pattern
- Reality verification verdicts: README/MCP/Phase-B/Cortex VERIFIED;
Bayesian-router PARTIAL (overclaim removed); keigit PARTIAL (framing
fixed in this commit); memory-format VERIFIED-FALSE (spec added)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two resource-exhaustion fixes from Opus Rust + Sonnet Rust audits.
1. kei-cortex per_user_locks DashMap unbounded growth (HIGH)
File: kei-cortex/src/state.rs
Bug: per_user_locks: DashMap<String, Arc<Mutex<()>>> inserted on every
distinct user_id; never evicted. Auth'd attacker with 1M unique user_ids
could OOM the daemon (~150 bytes/entry = 15GB at 100M entries).
Fix: replaced DashMap with tokio::sync::Mutex<LruCache<String,
Arc<TokioMutex<()>>>> capped at PER_USER_LOCK_CAP = 1024. Eviction is
safe because callers hold their own Arc clone for their critical section;
dropping the registry slot retires only the registry's reference. Used
tokio::sync::Mutex for the registry because LruCache::get mutates the
recency list and requires &mut self.
Constructor Pattern: state.rs split into state.rs (184 LOC) +
state_factories.rs (64 LOC, new). Tests added: user_lock_evicts_past_cap
(registry stays ≤1024 after 2048 inserts), user_lock_keeps_most_recent
(LRU recency preserved). Existing user_lock_is_stable_per_user +
user_lock_differs_per_user updated to async — sole call site
(handlers/portrait.rs) gains .await.
2. kei-runtime stdout/stderr cap was post-hoc (HIGH)
File: kei-runtime/src/invoke.rs
Bug: wait_with_output() buffered ALL child stdout/stderr; only cap_bytes
truncated AFTER the child finished. A malicious atom writing 10 GB stdout
(or a buggy one looping infinitely) OOM'd the runtime BEFORE the cap fired.
Fix: replaced wait_with_output() with two reader threads sharing
KillHandle = Arc<Mutex<Option<Child>>>. Each reader appends bytes up to
STREAM_CAP = 16 MiB; on cap exceedance the reader KILLS the child from
inside the reader thread (critical — otherwise the unbounded writer would
never EOF and a post-hoc kill would never fire). Both readers drain the
closing pipe to EOF and return. Truncation surfaces as
InvokeError::SubprocessError with explicit "exceeded N byte cap" message.
Constructor Pattern: invoke.rs decomposed into invoke.rs (159 LOC) +
invoke_io.rs (146 LOC, new) + invoke_error.rs (54 LOC, new). Test added:
invoke_kills_runaway_atom — stages a kei-flood script running cat
/dev/zero, verifies (a) non-zero exit, (b) stdout < 18 MiB, (c)
"cap"/"subprocess" in stderr.
cargo check --workspace: clean. cargo test -p kei-cortex -p kei-runtime
--test-threads=1: 471 pass / 0 fail. Pre-existing openai_loop_wiring.rs
parallel-run flake (state collision when test-threads>1) is unrelated and
unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opus Cross-cutting audit found a classic OIDC account-takeover hole in
kei-auth-google::verify(). Same class as the public Booking.com / Slack /
GitLab pattern.
Root cause: verify() accepted info.email from userinfo response as user_id
WITHOUT checking info.email_verified. A Google Workspace admin can mint
accounts with arbitrary unverified email aliases. Attacker then OAuth-flows
into the relying party using a victim's email as their alias and gets a
session bound to that user_id. No email verification = no auth.
Fix in 3 layers (defense in depth):
1. email_verified GATE
- client.rs: UserInfo gains email_verified: bool with #[serde(default)] —
absent field defaults to false (fail-closed).
- error.rs: new Error::EmailNotVerified variant.
- provider.rs::verify(): rejects with EmailNotVerified before any session
is built when email_verified != true.
2. sub AS PRIMARY user_id
- provider.rs::verify(): user_id = info.sub (Google's stable account id),
NOT info.email. Email is now mutable metadata only. Email reassignment
in Google Workspace cannot redirect an existing user_id binding.
3. id_token.sub CROSS-CHECK
- id_token.rs (new, 104 LOC): JWT-claims-only extract_sub() — parses
base64-payload without signature verification (signature verification
against Google JWKS is a documented follow-up atomar).
- provider.rs::verify(): when TokenResponse.id_token is present, decode
claims and require id_token.sub == userinfo.sub. New
Error::IdSubMismatch + IdTokenMalformed variants.
- This adds defense against a forged userinfo response even though
signature is not yet verified.
Constructor Pattern compliance: provider.rs split into provider.rs (181 LOC)
+ verify_helpers.rs (114 LOC, with unpack_challenge / check_state /
enforce_email_verified / cross_check_id_token_sub helpers). All files <200
LOC, all functions <30 LOC.
Tests added: tests/google_security_regression.rs (164 LOC, 5 dedicated
CVE-2023-7028 regression tests). All 26 tests pass:
- verify_rejects_unverified_email
- verify_rejects_missing_email_verified_field
- verify_uses_sub_not_email_as_user_id
- verify_rejects_id_token_sub_mismatch
- verify_accepts_matching_id_token_sub
cargo check --workspace clean. cargo test -p kei-auth-google: 26/26 pass.
Follow-up: JWT signature verification against Google's JWKS endpoint with
kid-based key cache + RS256/ES256 — separate atomar (~150 LOC).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent security hardenings from cross-cutting audits.
1. cortex /term PTY env leak + bind guard (HIGH — Sonnet Cross-cutting + Opus)
- kei-cortex/src/handlers/term_pty.rs: PTY spawn was inheriting daemon's
full process env (KEI_AUTH_KEY, ANTHROPIC_API_KEY, FAL_KEY, etc.) into
every authenticated /term shell. Combined with default cors_origin =
https://keisei.app, one stored XSS on keisei.app + one bearer token =
full local shell with all daemon secrets.
Added apply_safe_env() helper: env_clear() + re-set only HOME, PATH,
USER, LANG, TERM. Spawn helper invokes it before spawn_command.
- kei-cortex/src/main.rs: extracted build_config() helper; added
enforce_loopback_or_local_cors() guard called before serve.bind. Refuses
to start if bind addr is non-loopback AND cors_origin is a public
domain — prevents the XSS-to-shell scenario in production.
2. agent-stub-scan.sh stdin parsing (HIGH — multiple audits)
- hooks/agent-stub-scan.sh: previously read $CLAUDE_AGENT_TRANSCRIPT env
var which Claude Code does NOT set on PostToolUse:Agent. Hook silently
exited 0 — RULE 0.16 enforcement was dead-code in production.
Rewrote to read stdin JSON via jq, flatten .tool_response recursively
(string|array|object via the same pattern as agent-event-done.sh),
guard on .tool_name == "Agent" and command -v jq. Maintained WARN-tier
exit-0 with TODO marker for ENFORCE flip on 2026-05-05 (per RULE 0.16
§2 ladder).
3. magiclink revoke() silent no-op (HIGH — Opus Rust + Sonnet Cross-cutting)
- kei-auth-magiclink/src/{error,provider}.rs: revoke() previously returned
Ok(()) without doing anything. Operators expecting "revoke a session"
semantics from the AuthProvider trait got false success. Stolen magic-
link URLs remained valid until the 15-minute TTL.
Added Error::Unsupported variant. revoke() now returns
Err(Unsupported(...)) with explicit guidance: "rotate KEI_MAGICLINK_HMAC_
KEY to invalidate all live tokens, or maintain a deny-list at the caller
layer". Test provider_revoke_returns_unsupported_error confirms the
error variant is wired.
Tests: cargo check + cargo test both PASS. 444 functional tests across
kei-cortex (428 lib) + kei-auth-magiclink (16 lib + smoke). Pre-existing
openai_loop_wiring.rs 502 failures in routes/openai/{chat,responses}.rs are
NOT introduced by these fixes — separate unrelated triage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent shell hardening fixes from Opus Shell + Sonnet Shell audits.
1. disk-reclaim.sh Guard 3 — protect branches without upstream tracking (HIGH)
File: hooks/disk-reclaim.sh:88-101
Bug: when a worktree branch has no upstream tracking ref, `git log @{u}..`
exited non-zero and `unpushed=""` (empty). The check
`[ -n "$unpushed" ] && [ "$unpushed" != "0" ]` evaluated FALSE, so the
worktree fell through Guard 3 and was eligible for mtime-based pruning.
Local-only branches with committed work were silently deleted.
Fix: explicit two-branch logic. Run `git rev-parse --abbrev-ref @{u}` first;
only run the unpushed-count check if upstream exists. If no upstream, log
SKIP[no-upstream] and `continue` conservatively. New
`worktrees_skip_unpushed` counter increments in both unpushed paths.
2. secrets-pre-guard.sh — placeholder allowlist scope-narrow (MEDIUM)
File: hooks/secrets-pre-guard.sh:43-103
Bug: word "placeholder" anywhere in content disabled all secret-pattern
scanning for that whole Write. Allowlist was too broad — a doc with the
word "placeholder" in its prose could mask a real sk-ant- token elsewhere.
Fix: replaced global early-exit with per-line awk scan. New scan_pattern()
helper walks content line-by-line; each line matching a secret regex is
allowed ONLY if the SAME line also matches ALLOWLIST_RE. Doc prose can no
longer mask cross-line secrets. Added `dummy[_-]?(key|token|secret)` to
allowlist for legitimate test fixtures.
3. lib-rust-prebuild.sh — sha256 fail-closed (HIGH supply-chain)
File: install/lib-rust-prebuild.sh:75-88
Bug: when ${url}.sha256 404'd, installer printed WARNING and proceeded with
unverified tarball. A compromised github release uploader could ship a
malicious tarball, omit .sha256, and the installer would extract it into
~/.cargo/bin/.
Fix: missing .sha256 → ERROR + abort. Path A install fails → falls back to
Path B (cargo build from source). Override via KEI_ALLOW_UNVERIFIED_TARBALL=1
(visible per-call, intentional friction).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent manifest fixes from Opus TOML + Sonnet TOML + Sonnet Markdown
audits.
1. TOML scope-capture (Sonnet TOML, HIGH)
_manifests/critic-anti-pattern.toml + critic-perf.toml had [references]
appearing AFTER [[handoff]] array-of-tables. Per TOML spec, this makes
[references] parse as a SUB-TABLE of the last [[handoff]] element, not as
a top-level table. All references in those manifests were silently
unreachable by the assembler's top-level resolver.
Moved [references] block before [[handoff]] in both files. Added 3-line
warning comment immediately above [[handoff]] explaining the TOML scope rule
to future editors.
2. Dangling physics-deriver in role bodies (Opus TOML, HIGH)
Group F earlier (commit 57d3700) removed [[handoff]] blocks targeting
physics-deriver / patent-compliance / patent-researcher, but role text
strings + forbidden_domain arrays still referenced physics-deriver in:
- _manifests/ml-researcher.toml (lines 16, 41, 76, 89)
- _manifests/ml-implementer.toml (line 15)
- _manifests/infra-implementer.toml (line 16) — already scrubbed in P0
commit c250a9c as part of EC2-ID strip; leaving for context
Replaced live mentions with "architect" (canonical fallback). Historical
comments documenting the prior removal kept intentionally — they are
documentation, not live references.
3. Wrong rule paths (Opus TOML, MEDIUM)
ml-researcher.toml + ml-implementer.toml referenced files that don't exist
under their stated paths:
- path:user-rules/specialized-node-training.md → cfc-specialized-nodes.md
- path:user-rules/observable-classification.md → paradigm-native-measurement.md
Fixed both paths in both files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opus Markdown + Sonnet Markdown audit found that the 12 root kei-*.md files
each carried a "GENERATED by _assembler from _manifests/kei-<slug>.toml — DO
NOT EDIT" header pointing to manifests that DO NOT EXIST. The actual manifests
live at _manifests/<slug>.toml (no kei- prefix) and regenerate to
_generated/<slug>.md. Content had drifted (kei-architect.md had a "MODE — First
Principles" section absent from _generated/architect.md).
This was active confusion for any editor: the "DO NOT EDIT" header lied (no
manifest existed for regen), and editing the manifest at the implied path was
impossible.
Replaced each root kei-<slug>.md with a 14-LOC alias stub that:
- Tells readers the actual generated file lives at _generated/<slug>.md
- Tells readers the manifest source is _manifests/<slug>.toml (no kei- prefix)
- States explicitly: edit the manifest, never these aliases
- Preserves the root-level discoverability marker
Also fixes Group G's commit ddd13e6 follow-on damage: that commit appended
STATUS-TRUTH MARKER blocks to 5 of these root kei-*.md files thinking they
were generated outputs of real manifests. Those edits are now superseded by
the alias-stub form.
Net delta: +108 / -3787 (12 files shrink from full agent prompts to 14-LOC
stubs). Real prompts remain in _generated/ where they were generated from
the actual _manifests/<slug>.toml files.
Follow-up: add CI lint that root kei-*.md must match alias template byte-for-
byte. Prevents future drift back to the parallel-SSoT state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sonnet Markdown audit + Opus TOML audit (post-publish) caught two infrastructure
identity leaks in the public KeiSeiKit-1.0 mirror:
1. Tailscale CGNAT IP `100.91.246.53` (private Forgejo server) appeared 5×:
- BACKUP-INDEX.md:6,17 — including a PR URL exposing branch naming convention
- .forgejo/README.md:3,41,75,87
Replaced with `<private-forgejo>` placeholder. PR URL is now a template form
(no real branch name leaked).
2. Real AWS EC2 instance ID `i-0a8b747023809d451` appeared 2× in
_manifests/infra-implementer.toml:39,104 — directly inside an agent prompt
shipped publicly. Replaced with `<ec2-instance-id>` placeholder.
The IP itself is not internet-routable (Tailscale CGNAT), but the leak still
narrows OSINT scope and reveals our Forgejo-on-Tailscale topology. The EC2
instance ID is a real production resource identifier in our shared-tenancy
deployment; leaking it gives an attacker a confirmed target for AWS-API
enumeration if any other vector ever yields IAM access.
These leaks were already pushed to github main in commits a2b4dd6 + fc03c98.
The HEAD-only scrub clears the working tree and the next commit; full git
history scrub via git-filter-repo is a follow-up if the historical exposure
window matters operationally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes:
1. Author email update across the kit
- All `info@greendragon.info` references replaced with `parfionovich@keilab.io`
- Touched: NOTICE, README.md, _ts_packages/package.json (and 5 adapter packages),
plus 90+ Cargo.toml files
- Apache-2.0 attribution unchanged (Denis Parfionovich, 2026)
2. Cargo workspace.package SSoT for author/license/repository/homepage
- Added to [workspace.package]:
authors = ["Denis Parfionovich <parfionovich@keilab.io>"]
license = "Apache-2.0"
repository = "https://github.com/KeiSei84/KeiSeiKit-1.0"
homepage = "https://github.com/KeiSei84/KeiSeiKit-1.0"
- All ~89 member crates migrated from inline declarations to:
authors.workspace = true
license.workspace = true
(repository/homepage where applicable)
- Closes audit gap: kei-graph-stream, kei-cortex, kei-shared previously had no
license field at the crate level, blocking `cargo publish` on those.
Now they inherit Apache-2.0 from workspace.
- kei-scheduler/Cargo.toml: removed stray duplicate `authors` line introduced
by an earlier migration sweep.
cargo check --workspace: clean. No code changes; metadata-only migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from KeiSeiKit2.0 pr-review (~/Projects/KeiSeiKit2.0/skills/pr-review)
applied to commit range b346250..HEAD.
1. BLOCKER — SecretString silently leaked plaintext via Serialize.
File: _primitives/_rust/kei-runtime-core/src/secrets.rs
Was: derive(Serialize) + serde(transparent) -> serde_json::to_string(&secret)
emitted the raw plaintext in any parent struct with #[derive(Serialize)].
Debug was redacted but Serialize was not. Defeated the type's purpose.
Now: manual Serialize impl always emits literal "<redacted>". Deserialize
derive kept (callers need to read secrets from config/env).
Test serialize_emits_redacted_literal asserts JSON output is "\"<redacted>\"".
2. WARNING — PKCE code_verifier dropped before token exchange.
build_auth_url generated code_challenge = SHA256(verifier) but verify() never
threaded the verifier to the token endpoint. Token exchange submitted no
code_verifier, defeating the PKCE protection.
Files:
- _primitives/_rust/kei-runtime-core/src/traits/auth.rs:
AuthChallenge::OAuthCode now carries code_verifier: Option<String>.
Caller stores verifier alongside state in their session-store, exactly as
they already store state for CSRF check.
- _primitives/_rust/kei-auth-google/src/provider.rs:
verify() destructures code_verifier and passes to client.exchange_code(...).
- _primitives/_rust/kei-auth-apple/src/provider.rs:
same change.
Tests added (wiremock body assertions):
- google_smoke / apple_smoke: assert exchange request body contains
code_verifier=<value> when challenge carried Some(verifier).
- existing tests updated to construct OAuthCode { ..., code_verifier: None }.
Test split (Constructor Pattern 200 LOC):
- apple_smoke.rs grew over 200 LOC after PKCE test addition. Split into
apple_smoke.rs (provider tests) + apple_client_smoke.rs (client tests).
- same for google_smoke.rs / google_client_smoke.rs.
Test results: 31 passed; 0 failed across kei-auth, kei-auth-apple, kei-auth-google,
kei-runtime-core unit + integration tests. cargo check --workspace clean.
Breaking change: any caller that constructs AuthChallenge::OAuthCode outside this
workspace must add code_verifier field (None for legacy no-PKCE; Some for PKCE).
Compile-time surfaced gap, not runtime regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>