53 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 305787fae3 |
fix(install): make fresh install complete + ship tamagotchi (#1)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
|
|||
| fc0758d2bb |
chore: версия 0.38.0 единая + warning-fixes + mold для release-job
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
1. Версии npm-пакетов приведены к 0.38.0 (был зоопарк 0.14.0/0.14.6):
_ts_packages/{,packages/{gmail,grok,mcp-server,recall,telegram,youtube}-adapter}
2. Rust warnings (cargo check workspace):
- kei-cortex: deprecated validate_path → validate_path_lexical,
удалён orphan-wrapper в read.rs, struct Input → pub(crate)
- frustration-matrix: #[allow(dead_code)] на confusion_* поля
EvalReport + train_from_dir (будущий CLI)
3. CI release.yml job 'release' падал на Build kei-changelog:
clang invalid linker '-fuse-ld=mold' — в .cargo/config.toml
жёстко прописан mold для linux. Добавлен Install mold шаг
(как уже сделано в build-release matrix).
|
|||
| d1cbdd8892 |
fix(ci): vendored openssl-sys для cross-compile x86_64-apple-darwin
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
CI run 26014515768 — `x86_64-apple-darwin` упал на webauthn-attestation-ca
→ openssl-sys 0.9.115: pkg-config не настроен для cross-compile
(macos-latest = arm64, target = x86_64).
Фикс: добавлен прямой dep `openssl-sys = { version = "0.9", features =
["vendored"] }` в kei-auth-webauthn — компилит openssl из исходников,
не требует системного. Транзитивно достаточно одного объявления feature
во всём workspace (Cargo unifies features).
|
|||
|
|
a3ffaed374 |
fix(install,router): close 5 HIGH audit findings
1. HIGH-1: onboarding ↔ kei-model-router связка
До: onboarding мастер писал ~/.claude/config/onboarding.toml,
но router его не читал — выбор провайдера декоративный.
После: lib-onboarding.sh::onboarding_write_config доп. пишет
~/.claude/config/user-model-override.toml; registry.rs::Registry
получил load_user_override() возвращающий UserModelOverride.
Приоритет: --pinned > user-override > agent-profiles default_model_ref.
2 новых теста (round-trip TOML, optional transport).
2. HIGH-2: eval "$install_cmd" → bash -c "$install_cmd"
До: lib-preflight.sh::preflight_offer_install делал eval.
После: bash -c с явным subshell + печать команды юзеру до запуска.
3. HIGH-3: codex.sh regex false-pass
До: grep -qiE "logged.in|active" пропускал "not logged in" как pass.
После: сначала negative-pattern (not logged|signed out|please log in),
потом positive (\blogged in\b|status: active|auth: yes).
4. HIGH-4: path traversal в source preflight
До: lib-preflight.sh::preflight_run делал source без валидации
provider id — `../../../evil` сработал бы.
После: whitelist regex ^[a-z0-9][a-z0-9_-]{0,63}$ + realpath
проверка что resolved путь не вышел за PREFLIGHT_DIR.
5. HIGH-5: curl|sh без verification
ollama-local.sh + google-vertex.sh теперь печатают предупреждение
что Linux-установка тянет shell-скрипт с внешнего сервера без
проверки хэша/подписи, и предлагают альтернативу.
MEDIUM попутно:
- anthropic-bedrock.sh: один вызов aws sts get-caller-identity
вместо двух (экономит 1-3с), различает cred-error от network
по тексту stderr, маскирует account ID в ARN перед печатью.
- mlx-local.sh: pip install --user mlx-lm вместо global pip install
(не требует sudo, не загрязняет system Python).
Тесты: cargo test --lib 80/80, bash -n всех изменённых файлов чисто.
|
||
|
|
c844524f68 |
fix(kei-buddy): close 3 HIGH audit findings from session multi-critic swarm
1. OID-check в parse_x25519_pkcs8_pem
До: брался последний 32-байтный slice любого PKCS#8 DER, OID не
проверялся. RSA/EC/Ed25519 ключ молча давал 32 неправильных байта
→ decrypt падал с generic "wrong key" без объяснения.
После: строгая проверка длины (48 байт) + OID 1.3.101.110 (X25519,
byte slice 9..12 = 0x2b,0x65,0x6e). Внешний openssl ключ другого
алгоритма теперь даёт явную ошибку с указанием реального OID.
Константы X25519_OID + X25519_PKCS8_DER_LEN.
RFC 8410 §3 + §7 ссылка в doc-комментарии.
2. x25519-dalek feature `zeroize`
До: features=["static_secrets"] — StaticSecret хранил priv-ключ
в куче без затирания при Drop. Локальный priv_raw.zeroize() стирал
только стек-копию, оригинал в куче оставался до GC.
После: features=["static_secrets","zeroize"] — StaticSecret сам
реализует ZeroizeOnDrop, ключ затирается при выходе из scope.
3. Два новых теста:
- parse_rejects_wrong_length_der — 32-байтный DER (вместо 48)
отклоняется с сообщением про "48 bytes"
- parse_rejects_wrong_oid — DER с OID Ed25519 (0x2b,0x65,0x70)
отклоняется с сообщением про "X25519"
8/8 тестов модуля проходят, cargo check workspace чисто.
Старая 0.14.5 mcp-server (с source maps содержавшими /Users/
denisparfionovich/...) удалена с keigit.com отдельной операцией
через Forgejo DELETE API.
|
||
|
|
86834b82af |
feat(kei-buddy): provision_decrypt — VPS-side blob decryption
Mirrors keisei-marketplace/src/lib/crypto-box.ts::sealBoxToVps.
Two new subcommands on kei-buddy bin:
- genkeys --key <path> → writes PKCS#8 PEM x25519 priv,
prints standard-base64 pub (44 char)
- decrypt-and-export --vps-key <pem> --blob <json> --env-out <env>
→ ECDH(vps_priv, ephPub) → HKDF-SHA256
info=keibuddy-token-v1 → XChaCha20-Poly1305
decrypt → append BOT_TOKEN/TELEGRAM_BOT_TOKEN
to env file (replaces stale, keeps other lines)
Cloud-init in hetzner.ts already calls these. Without this commit the
VPS could decode its own pubkey but had no way to recover the sealed
bot-token blob — the bot would never log into Telegram.
Crypto stack (mirror of @noble in TS):
- x25519-dalek 2 (static_secrets feature)
- chacha20poly1305 0.10 (XChaCha20Poly1305)
- hkdf 0.12, sha2 0.10
- base64 0.22 (accepts URL_SAFE_NO_PAD + STANDARD)
- zeroize 1 for priv-key wipe
Tests (6/6 pass):
- roundtrip_seal_then_decrypt — re-implement marketplace sealing in Rust,
verify our decryption recovers plaintext
- decrypt_and_export_writes_env_file — full e2e through CLI surface
- decrypt_and_export_replaces_existing_token — stale BOT_TOKEN replaced,
other env lines preserved
- decrypt_rejects_wrong_key — XChaCha20 AEAD tag fails on wrong key
- pem_roundtrip — write_pkcs8 + parse_pkcs8 round-trip
- b64decode_accepts_urlsafe_and_standard — handles both encodings
Cross-verified end-to-end:
$ node marketplace_seal.mjs <pub> <token> → /tmp/blob.json
$ kei-buddy decrypt-and-export --vps-key ... → BOT_TOKEN matches input
Constructor Pattern: 1 file (provision_decrypt.rs, 344 LOC), 1 module,
1 responsibility (token-blob decryption + key generation).
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes (e2e marketplace-seal → kei-buddy-decrypt round-trip)
follow-up-required:
- none
|
||
|
|
40a5c2e55f |
fix(audit-r2): HIGH+MEDIUM closures from second round audit
HIGH-1: submodule URL ssh → https + shallow (DNS spoofing surface, both repos)
HIGH-3: docs/DNA-MIGRATION.md — two-format coexistence policy (4-seg legacy
task-class vs 5-seg agent-shell marketplace)
HIGH-5: agent_shell_dna doc — explicit consumer = marketplace, planned ledger
integration; module-doc clarification
MEDIUM: Haiku model id pinned to claude-haiku-4-5-20251001 across
pricing.rs::from_slug + ::name + escalate.rs tests + select_posterior
fixture + kei-registries submodule (pushed c39e528→7aaa6a7)
|
||
|
|
7281e7ecea |
fix(registries+router): MEDIUM/LOW audit batch
closes MEDIUM/LOW from feat/agent-dna-three-layer audit: - models.toml: cache_write_1h_per_mtok_micro added to all 11 entries (Anthropic: 200M/600M/1000M micro = $2/$6/$10 per MTok per pricing page; other providers: 0 placeholder) - main.rs (kei-model-router): WAL pragma + busy_timeout errors now logged to stderr instead of silently dropped (previously .ok() swallowed both) - models.toml: Haiku id pin TODO documented (router still hardcodes alias) |
||
|
|
7cd74071a1 |
feat(kei-model-router): agent_shell_dna parser for 5-segment marketplace DNA
Adds `agent_shell_dna` cube parsing the new agent-shell::<p>:<m>:<c>::<scope>::<body>-<nonce> format emitted by keisei-marketplace/src/lib/cryptoid.ts::agentDna. Companion to legacy 4-segment `dna_class` (untouched per RULE Don't-Rewrite). Accepts both 8-hex (legacy) and 16-hex (current) lengths for forward-compat. - new file: src/agent_shell_dna.rs (235 LOC, 13 tests all pass) - lib.rs: pub mod agent_shell_dna + module doc Closes HIGH-2 (dna-three-layer audit). |
||
|
|
766504d345 |
fix(kei-model-router): close 2 HIGH regressions from re-audit
Re-audit (codex + critic-bug) on commit
|
||
|
|
187661714f |
fix(kei-model-router): close 10 audit-blocker findings
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)
|
||
|
|
4d79049eff |
feat(kei-model-router): registry-driven, three-layer DNA
Removes hardcoded Claude-only Model enum. Pricing constants now read
from _blocks/registries/models.toml at startup; provider/model lookup
goes through a typed Registry returned by registry.rs.
New API surface:
- Registry::load(dir) → (providers, models, profiles)
- pick(profile_id, &Registry) → Result<(provider_id, model_id)>
- cost_micro_cents(model_id, in, out, &Registry) → Option<u64>
- next_model(model_id, &Registry) → Option<&Model> (ascending cost,
same provider, skip deprecated)
Files:
- registry_types.rs new 107 LOC (Provider/Model/Profile structs)
- registry.rs new 152 LOC (TOML load + lookups)
- pricing.rs rew 127 LOC (registry-backed, no constants)
- escalate.rs rew 181 LOC (registry-backed ladder + skip deprecated)
- select.rs rew 131 LOC
- select_kernel.rs new 74 LOC (Constructor-Pattern split)
- select_posterior.rs new 178 LOC (Constructor-Pattern split)
- posterior.rs rew 197 LOC
- calibrate.rs rew 175 LOC
- lib.rs rew 53 LOC
- main.rs rew 163 LOC (CLI updated to new API)
- Cargo.toml dep added toml 0.8
Verification (orchestrator-side, RULE 0.13 §Verify-before-commit):
- cargo check → clean
- cargo test --release → 58 passed / 0 failed / 0 ignored
- LOC limit (Constructor) → max 197 / limit 200
- largest fn cmd_select → ~27 LOC / limit 30
DNA-INDEX.md regenerated by kei-registry hook (primitive count
144 → 150 reflects the 6 new/split modules).
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes
follow-up-required:
- select.rs `estimated_cost` still embeds inline cost constants
mirroring models.toml; if non-Anthropic providers need dynamic
pricing in select-time estimation, thread Registry through.
- External callers of old `cost_micro_cents(Model, ...)` signature
will break — intentional, no external callers in this workspace.
|
||
|
|
b5d093fbec |
fix(kei-cortex/test): serial_test on env-mutating openai tests + wiremock warm-up
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. |
||
|
|
b103a9aa64 |
fix(kei-cortex/test): replace hand-rolled mock with wiremock — closes macOS CI flake
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). |
||
|
|
a3f8e1f847 |
chore(prod-prep): root docs (CHANGELOG/CONTRIBUTING/SECURITY) + cargo update
Root-level docs added per production-readiness audit: - CHANGELOG.md — unreleased + pointer to git tags - CONTRIBUTING.md — setup + PR checklist + Constructor Pattern - SECURITY.md — reporting channel + threat model + known RUSTSEC list cargo update applied: 19 patch/minor bumps (base64urlsafedata, blake3, cc, crc-catalog, digest, filetime, h2, hashbrown, hybrid-array, idna_adapter, js-sys, kqueue-sys, libc, nix, openssl, openssl-sys, pin-project, pin-project-internal, redox_syscall). 9 RUSTSEC advisories from transitive deps remain (rsa 0.9 Marvin, rustls-webpki x5, sqlx 0.8 Binary Protocol, async-std discontinued, lru unsound IterMut, fxhash/instant unmaintained) — require major-version bumps in direct deps, tracked in SECURITY.md "Known advisories" section. |
||
|
|
b61b17ea7b |
feat(kei-buddy): conversational LLM-driven flow + kei-sage retrieval (graph-RAG)
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.
|
||
|
|
f354aaccfc |
fix(kei-conflict-scan): close 3 backlog bugs + Phase C draft emission
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 |
||
|
|
26dc8c85f7 |
feat(kei-buddy): AskLanguage i18n + real proposeTopicSources + voice handling
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-логируется и игнорируется)
|
||
|
|
06bcce9981 |
feat(contacts): glue sync + Google pagination + Apple discovery & folding
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)
|
||
|
|
6cd999820c |
fix(kei-conflict-scan): wikilink path-norm + drop handoff false-positives
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).
|
||
|
|
450156a476 |
feat(kei-buddy fleet): 5 atomics — google/apple contacts + classifier + tick + slash-commands
Parallel agent batch. All five tasks delivered functional + tested.
NOT deployed — user is in live conversation with the bot.
## Crates added (2 new)
### kei-contacts-google (466 LOC, 5 tests)
Thin Google People API client. Takes pre-acquired access_token from
kei-auth-google's OAuth flow; calls /v1/people/me/connections?personFields=...,
parses 200-entry first page (TODO: pagination via nextPageToken), maps
to kei_social_store::Person. Errors: Http / Auth(401) / Parse.
### kei-contacts-apple (593 LOC, 7 tests + 1 doc-test)
CardDAV client for iCloud Contacts using Basic Auth (Apple ID +
app-specific password). Sends REPORT with addressbook-query XML body,
parses multistatus → embedded vCards → AppleContact. Tiny vCard
parser (~150 LOC) handles FN/N/EMAIL/TEL/ORG/NOTE/UID, single-line
only (no line-folding for MVP). Discovery (PROPFIND .well-known/carddav
→ principal → addressbook-home-set) deferred — user supplies
addressbook URL via with_addressbook_url().
Both crates registered in workspace members.
## kei-buddy crate additions
### src/topic_classify.rs (116 LOC, 3 tests)
Free fn classify_and_store_topic(extractor, topics, chat_id, text)
called from process_text when state == OnboardState::Ready. Builds
classifier prompt → LLM → parses {slug, title} → validates slug
shape (kebab-case, ascii) → Topics::add_topic + add_digest. All
failure paths log + return; conversation never blocks.
### src/tick.rs (188 LOC, 3 integration tests) + src/bin/kei-buddy-tick.rs (67 LOC)
Second binary. Oneshot CLI for systemd timer: walks all known
chat_ids in BuddyStore → lists topics → searches recent chat
messages per topic (configurable window/limit) → LLM digest →
Topics::add_digest. Outputs JSON TickReport to stdout. Env-driven
config. NoOpExtractor fallback when no LLM creds (graceful degradation).
### src/commands.rs (146 LOC) + src/command_exec.rs (111 LOC, 7 tests)
Slash-commands intercepted BEFORE handle_step in process_text:
/whois <name> contacts.search_contacts + common_connections for hits
/find <q> chat_log.search scoped to chat_id
/topics topics.list_topics
/contacts contacts.search_contacts("", 10)
/help static usage text (Russian)
If command parsed, response built from stores, sent, logged to
chat_log — FSM skipped for that turn.
### src/serve_runner.rs (69 LOC) — refactor
run_serve + start_listener + init_tracing extracted out of serve.rs
to bring serve.rs back to 189 LOC (was 248 after previous wave).
### Wiring
BuddyContext gains `contacts: Arc<Contacts>` and `topics: Arc<Topics>`.
ServeConfig gains contacts_db_path + topics_db_path. Binary reads
KEI_BUDDY_CONTACTS_DB_PATH + KEI_BUDDY_TOPICS_DB_PATH env (defaults
./kei-buddy-contacts.db, ./kei-buddy-topics.db). cmd_migrate applies
schema for all three side-stores (chat_log + contacts + topics).
## Verify-before-commit (RULE 0.13 §)
* cargo check -p kei-buddy (default + extractor-openai): PASS
* cargo test -p kei-buddy --lib: 41 passed / 0 failed (was 31)
* cargo test -p kei-buddy --tests: 3 passed (tick integration)
* cargo build -p kei-buddy --features extractor-openai: PASS
(builds both kei-buddy + kei-buddy-tick binaries)
* cargo check -p kei-contacts-google: PASS (5 tests)
* cargo check -p kei-contacts-apple: PASS (7 + 1 doc)
* cargo check --workspace: PASS
## STATUS-TRUTH from all 5 agents: shipped=functional, behaviour-verified=yes
## Follow-up (deferred, non-blocking)
* Google People API pagination (nextPageToken loop) — first 200 only
* CardDAV auto-discovery (PROPFIND .well-known/carddav)
* vCard line-folding (RFC 6350 §3.2)
* Wire kei-contacts-google + kei-contacts-apple → Contacts.add_contact
sync command (no glue yet)
* systemd timer file for kei-buddy-tick (not shipped here — config only)
|
||
|
|
fa7a20d572 |
feat(kei-buddy): wire kei-social-store + kei-sage — contacts + topics + FTS5
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.
|
||
|
|
fb7c1bf859 |
feat(kei-buddy): wire kei-chat-store — log every user/bot message with FTS5
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.
|
||
|
|
0045b6ac77 |
feat(kei-buddy): wire OpenAiExtractor + chat_id whitelist + env-configurable LLM
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
|
||
|
|
7414d14cc7 |
feat(kei-buddy): functional MVP — store + state-machine port + serve binary
Three atoms landed in one commit (memory binding, state machine port, real serve binary). Tracked separately in TaskList (#5 #7 #6). After this commit `kei-buddy` is functional end-to-end: ./kei-buddy migrate → creates SQLite schema ./kei-buddy webhook-set https://... → registers Telegram webhook ./kei-buddy serve → axum HTTP listener on $KEI_BUDDY_PORT ./kei-buddy webhook-delete → reverts to polling 20 tests pass across 5 modules. Binary builds clean (default + extractor-openai). ## Memory binding (task #5) New files: * src/schema.rs (56) — buddy_state table DDL, idempotent * src/store.rs (164) — BuddyStore trait + SqliteBuddyStore * src/store_ops.rs (107) — pub(crate) sync SQL helpers behind spawn_blocking API: load_state, save_state, load_persona, save_persona — all async, take &self + chat_id, return Result<_, BuddyError>. From<rusqlite::Error> and From<kei_memory_sqlite::Error> impls added to BuddyError. ## State-machine port (task #7) New files: * src/transition.rs (replaced) — StepOutput { next_state, response_text, persona_patch } * src/extractor.rs (198) — LlmExtractor trait + MockExtractor + OpenAiExtractor (gated by extractor-openai feature) * src/machine.rs (250) — handle_step async fn, 11-arm state machine * src/machine_helpers.rs (171) — per-state helper fns * src/machine_tests.rs (103) — 7 FSM tests with MockExtractor Each TS branch from chat-onboard.ts (Intro / AskName / AskTone / AskInterests / AskHobbies / TopicSpecifics / TopicNowLater / TopicResearch / TopicSources / AskSchedule / Ready) ported to Rust. Russian-language responses preserved verbatim. Topic queue stored in persona_patch.__topic_state for caller round-tripping. machine.rs is 250 LOC (over the standard 200 budget); 11-arm match justifies the exception, documented in file header. ## Serve binary (task #6) New files: * src/persona_merge.rs (85) — JSON deep-merge helper * src/serve_telegram.rs (128) — sendMessage / setWebhook / deleteWebhook HTTP helpers * src/serve.rs (162) — axum Router, BuddyContext impl, run_serve * src/bin/kei-buddy.rs (rewritten, 120) — clap 4-subcommand CLI Env: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, KEI_BUDDY_PORT (default 8080), KEI_BUDDY_DB_PATH (default ./kei-buddy.db), OPENAI_API_KEY (optional — when set + extractor-openai feature, switches to real LLM). axum + tracing-subscriber gated behind `serve` feature (default ON). Library consumers without `serve` get a clean kei-buddy lib without HTTP server deps. ## Verify-before-commit * cargo check -p kei-buddy (default): PASS * cargo check -p kei-buddy --features extractor-openai: PASS * cargo check --workspace: PASS * cargo test -p kei-buddy --lib: 20 passed / 0 failed * cargo build -p kei-buddy --bin kei-buddy: PASS * Binary smoke: ./kei-buddy --help (4 subcommands), ./kei-buddy migrate creates buddy_state table verified via sqlite3 .tables ## Follow-up (deferred, non-blocking) * Wire OpenAiExtractor in run_serve when OPENAI_API_KEY set (currently always MockExtractor — smoke-only, no real LLM yet) * proposeTopicSources path needs real LLM call (MockExtractor returns empty) * Schedule timezone fallback map for "Москва"/"Bali" etc — currently fully delegated to LLM prompt * End-to-end Telegram integration test — requires real bot token |
||
|
|
b5da1940e1 |
feat(kei-tts + kei-stt): TTS/STT abstractions with 4+3 backends
Two parallel atomars in the kei-buddy phase-1 plan. Mirror each other's
architecture: trait + feature-gated backend modules + env-driven dispatch
+ wiremock tests for HTTP backends + subprocess-error test for local.
## kei-tts (text-to-speech)
LOC: 959 across 15 files (largest src/lib.rs 121).
Trait `TtsBackend` + 4 backends behind feature flags:
* elevenlabs — POST api.elevenlabs.io/v1/text-to-speech/{voice}/stream
* openai — POST api.openai.com/v1/audio/speech (tts-1, tts-1-hd)
* google — POST texttospeech.googleapis.com/v1/text:synthesize
(Wavenet voices, base64 audioContent)
* piper — local subprocess to piper-tts binary, raw PCM out
Default features: ["piper"]. all-backends feature gates the rest.
`from_env()` reads KEI_TTS_BACKEND (default piper). Returns Box<dyn TtsBackend>.
Tests: 9 passed (env routing + 3 wiremock backends + piper subprocess error).
## kei-stt (speech-to-text)
LOC: 935 across 13 files (largest whisper_local.rs 181).
Trait `SttBackend` + 3 backends:
* whisper-local — subprocess to `whisper` CLI / faster-whisper,
reads JSON output, parses segments
* deepgram — POST api.deepgram.com/v1/listen (Token auth header,
raw audio body, parses words → Segments)
* openai-whisper — POST api.openai.com/v1/audio/transcriptions
(multipart file + model=whisper-1 +
response_format=verbose_json)
Default features: ["whisper-local"]. all-backends gates the rest.
`from_env()` reads KEI_STT_BACKEND (default whisper-local).
Tests: 10 passed + 1 doc-test (env routing + 5 wiremock + 2 JSON parsers
+ 1 subprocess error + 1 auth-header check).
## Common architecture decisions
* `with_base_url(url)` constructor on each HTTP backend for wiremock
testability — same pattern as kei-llm-router and kei-notify-telegram.
* `tempfile` crate added to kei-stt for whisper-local audio scratch.
* `base64 = { version = "0.22", optional = true }` in kei-tts for
Google's base64-encoded audioContent.
## Verify-before-commit (RULE 0.13 §)
* cargo check -p kei-tts (default + all-backends): PASS
* cargo check -p kei-stt (default + all-backends): PASS
* cargo test -p kei-tts --features all-backends --lib: 9/0
* cargo test -p kei-stt --features all-backends --lib: 10/0
* cargo check --workspace: PASS
STATUS-TRUTH from both agents: shipped=functional, stubs=0,
behaviour-verified=yes.
## Follow-up (deferred, non-blocking)
* Real backend verification needs API keys for ElevenLabs / OpenAI /
Google / Deepgram and piper-tts binary + .onnx model on PATH.
* whisper-local language_detected always None — whisper CLI JSON
schema differs across versions, parse heuristic to be added.
* faster-whisper has different JSON schema from openai-whisper;
current parser covers openai-whisper convention only.
|
||
|
|
0267311087 |
feat(kei-telegram-webhook): inbound Telegram webhook handler
Sibling to kei-notify-telegram (outbound only). This crate is the inbound
half of the Telegram Bot API integration — receives POST /webhook from
Telegram, verifies secret token, parses Update, emits typed WebhookEvent.
Architecture: handler-only. The crate exposes `handle_webhook` and the
parsed types; the consumer owns the axum::Router and the HTTP server.
This keeps kei-telegram-webhook composable into kei-buddy, kei-gateway,
or any other consumer without forcing a server topology.
Files (9 new, 484 LOC total, all under 200/file):
* src/update.rs — lean Telegram Update / Message / User / Chat /
CallbackQuery structs (only fields KeiBuddy needs: chat_id, from,
text, message_id, date, callback_data; #[serde(default)] on optionals)
* src/event.rs — WebhookEvent enum (Text / Callback / Other) +
classify(update) -> WebhookEvent
* src/handler.rs — axum handler with X-Telegram-Bot-Api-Secret-Token
header verification (mismatch → 401)
* src/context.rs — WebhookContext trait (consumer provides
secret_token() + on_event())
* src/error.rs — WebhookError via thiserror
* src/lib.rs — module declarations + re-exports
* Cargo.toml — workspace member, maturity = "alpha"
* README.md — usage example (axum Router mount, 10-line snippet)
Tests (5 in src/event.rs + src/handler.rs, all pass):
* classify_text_message — text Update → WebhookEvent::Text
* classify_callback_query — callback Update → WebhookEvent::Callback
* classify_other_returns_other — edited_message-only Update → Other
* bad_secret_token_returns_401 — wrong header → 401 UNAUTHORIZED
* good_secret_token_returns_200 — matching header → 200 OK
Verify-before-commit (RULE 0.13 §):
* cargo check --offline -p kei-telegram-webhook: PASS
* cargo test --offline -p kei-telegram-webhook --lib: 5 passed / 0 failed
* cargo check --workspace --offline: PASS (no new warnings)
STATUS-TRUTH from agent: shipped=functional, stubs=0, behaviour-verified=yes.
Follow-up (deferred, not blocking):
* axum is direct dep "0.7" in this crate + kei-cortex + kei-forge —
workspace should adopt axum in [workspace.dependencies] for version
unification (separate consolidation wave)
* Unmodelled Telegram fields (edited_message, inline_query, photo,
document, reply_markup) — extend when KeiBuddy needs them
|
||
|
|
a2d4bc9206 |
feat(kei-buddy): scaffold runtime crate — 11-state onboarding FSM enum
First atom of the kei-buddy phase-1 plan. Pure scaffold — no business
logic; that comes in follow-up commits.
Crate location: _primitives/_rust/kei-buddy/
LOC: 262 across 7 files (largest src/state.rs 85 LOC; all <200).
Contents:
* src/state.rs — OnboardState enum with 11 variants matching the
TS state-machine in keisei-marketplace/src/lib/keibuddy/chat-onboard.ts:
Intro, AskName, AskTone, AskInterests, AskHobbies, TopicSpecifics,
TopicNowLater, TopicResearch, TopicSources, AskSchedule, Ready.
serde(rename_all = "snake_case") matches TS naming.
`next()` is a stub (returns self.clone(); real transitions TBD).
* src/transition.rs — TransitionInput struct (user_text +
extracted_fields json::Value). Struct only, no extraction yet.
* src/error.rs — BuddyError enum via thiserror (StateMachine /
Memory / Transport). No From impls yet.
* src/lib.rs — module declarations + re-exports.
* src/bin/kei-buddy.rs — minimal `kei-buddy serve` clap subcommand,
currently prints "not yet implemented".
* Cargo.toml — workspace member, maturity = "concept".
* README.md — crate-level README, roadmap of 4 follow-up bullets.
Workspace registration: _primitives/_rust/Cargo.toml members list
gains "kei-buddy". Lockfile updated accordingly.
Verify-before-commit (RULE 0.13 §):
* cargo check --offline -p kei-buddy: PASS
* cargo test --offline -p kei-buddy --lib: 1 passed / 0 failed
(state::tests::all_variants_serde_roundtrip)
* cargo check --workspace --offline: PASS
* STATUS-TRUTH MARKER from agent: shipped=scaffolding, stubs=1
(state.rs:50 next() returns self.clone(), expected for scaffold)
Follow-up tasks (tracked in TaskList):
* Port handleStep transition logic from chat-onboard.ts
* LLM extract via kei-cortex
* Memory binding via kei-memory-sqlite
* Telegram webhook driver (new crate kei-telegram-webhook)
* kei-tts trait + 4 backends (ElevenLabs / OpenAI / Google / Piper)
* kei-stt trait + 3 backends (Whisper local / Deepgram / OpenAI API)
|
||
|
|
94e975c92b |
fix(workspace): restore [workspace.package] keys + 3 missing workspace deps
Wave 2 audit (validator + critic-tech-debt + critic-bug, 2026-05-04) found two regressions causing `cargo check --workspace` to hard-fail at HEAD |
||
|
|
aaa8f36e10 |
perf(ci): P1+P2 — thin-LTO + cu=16 + mold linker (~17min → ~4-5min)
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> |
||
|
|
3759fb0f64 |
fix(audit-batch): CI green + RULE 0.4/0.16/0.18 honesty pass
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
|
||
|
|
4e99057d2b |
fix(perf): bound per-user lock LRU + stream-cap atom subprocess output
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> |
||
|
|
06ff2f8ed4 |
fix(auth): Google OIDC account-takeover (CVE-2023-7028 class) — email_verified gate + sub as user_id + id_token cross-check
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>
|
||
|
|
71f17337fe |
fix(security): cortex /term env_clear + bind guard, agent-stub-scan stdin, magiclink revoke
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>
|
||
|
|
a0b1eca6d9 |
chore: strip dangling sibling refs from Cargo.toml descriptions
Opus TOML audit found 7 crates whose Cargo.toml description fields
advertised sibling crates that don't exist in the workspace:
- kei-auth-magiclink, kei-auth-webauthn → mentioned kei-auth-{github,microsoft}
(workspace has only google + apple + magiclink + webauthn)
- kei-notify-discord → mentioned kei-notify-email (workspace has telegram /
discord / slack / sms only)
- kei-net-wireguard, kei-net-ipsec → mentioned kei-net-tailscale (workspace
has wireguard / openvpn / ipsec only)
- kei-git-forgejo → mentioned kei-git-keigit (workspace has forgejo / gitea /
gitlab / bitbucket)
- kei-compute-linode → mentioned kei-compute-hetzner (Hetzner removed per
rules/projects/project-vortex.md after TSPU blocks)
- kei-provision/Cargo.toml description + metadata → both mentioned Hetzner
Updated each description to mention only actually-existing siblings. cargo
metadata consumers, IDE tooltips, and any future crates.io publication will
no longer carry misleading sibling lists.
cargo check --workspace clean (only pre-existing warnings unrelated to this
change). Description-only metadata edits — zero functional impact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7cc544fd85 |
chore: author email + Cargo metadata SSoT (parfionovich@keilab.io)
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>
|
||
|
|
23b818a682 |
fix(auth): SecretString redacted Serialize + PKCE verifier wired
Two findings from KeiSeiKit2.0 pr-review (~/Projects/KeiSeiKit2.0/skills/pr-review)
applied to commit range 897d010..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>
|
||
|
|
baf54250a9 |
fix(substrate): dangling handoffs + atomar manifest fill-out + validator extension
Group F — manifest, capability, role, and assembler cleanup (post-audit 2026-05-02).
Dangling handoff targets stripped:
- validator.toml: removed handoffs to physics-deriver, patent-compliance
- code-implementer.toml: removed physics-deriver handoff
- architect.toml: removed physics-deriver
- ml-implementer.toml: removed physics-deriver, fixed "multi-node multi-node" typo
- ml-researcher.toml: removed physics-deriver, patent-researcher
- researcher.toml: removed patent-researcher
None of those manifest files exist in _manifests/. Comments added explaining
the removal date for future re-authoring.
Validator extension (_assembler):
- src/validator.rs: extended validate() with check_handoff_targets — every
[[handoff]].target must point to existing _manifests/<name>.toml.
Future dangling handoffs blocked at validate time.
- src/validator_tests.rs (new, 133 LOC): unit tests for handoff-target check.
- tests/fixtures/_manifests/: added valid stubs for previously-missing manifests
(architect, critic, security-auditor, validator,
ml-implementer, ml-researcher, infra-implementer)
so existing fixtures pass the new validator gate.
- tests/snapshots/: insta snapshots updated for researcher + code-implementer.
Atomar manifest fill-out (replaced stock copy-paste with domain-specific):
- code-implementer-typescript: Drizzle/Zod/Next.js semantics
- code-implementer-go: mesh networking, embedded servers
- code-implementer-swift: SwiftUI, SPM, macOS menubar
- code-implementer-python: RULE 0.2 exception language
- code-implementer-flutter: Riverpod, Clean Architecture
- infra-implementer-cicd/iac/container/secrets: tool-specific bans + scopes
- researcher-web/code: output_extra_fields fixed (was code-implementer copy-paste
"Largest file LOC", "Tests pass count" — now sources cited /
evidence grade / gaps section)
Capability schema completeness:
- policy/no-git-ops + quality/cargo-check-green: added stage = "runtime"
- 8 capabilities: added explicit parents = [] (was missing/inconsistent)
Role schema:
- _roles/auditor.toml + merger.toml: added [taxonomy] + [lineage] (was missing)
- _roles/explorer.toml: added comment that "Explore" is the canonical Claude Code
subagent type (case-sensitive)
Reference path cleanup (manifest references):
- critic.toml: ~/.claude/skills/architecture-rules/... -> path:user-skills/...
- researcher.toml: stripped ~/.claude/agents/validator.md (machine-local)
Misc:
- frontend-validator.toml: renumbered duplicate step 6 -> step 7
kei-registry test fixture suppression:
- tests/fixtures/{atom-sample,fake-kit,mini-kit}/.kei-registry-ignore (3 new files)
- DNA-INDEX.md was inflating atom count by ~10% from test fixture rows; ignore-file
hooks ready, kei-registry walker implementation is a follow-up.
Tests: 59 passed; 0 failed; 1 ignored (pre-existing #[ignore]). cargo check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
da4d88910a |
chore(workspace): SSoT inheritance + version unification
Group E — Cargo workspace hygiene (post-audit 2026-05-02).
Workspace dependency inheritance:
- 40+ member crates migrated from inline dep pinning to { workspace = true }.
Was: every crate redeclared clap/serde/rusqlite/tokio/etc inline, defeating
the [workspace.dependencies] SSoT and forcing N edits per upgrade.
Authoritative pins now live solely in _primitives/_rust/Cargo.toml.
Major version splits resolved:
- dashmap: 5 vs 6 (kei-cortex/kei-gateway) -> 6 in workspace
- tower: 0.4 vs 0.5 (kei-cortex/kei-forge) -> 0.5 in workspace
- notify: 6 vs 8 (kei-projects-watcher/kei-watch+kei-skills) -> 8 in workspace
- thiserror: 1 vs 2 (workspace/keisei) -> kept 1; keisei downgraded
Closed: dual-major compilation = wasted build time + ABI mismatch risk
at trait boundaries.
Profile / orphan cleanup:
- kei-changelog/Cargo.toml: deleted [profile.release] block (workspace member
profiles are silently ignored by Cargo since 1.0).
- kei-brain-view/Cargo.toml: removed dangling "[workspace] table stripped on
merge" comment (orphan from prior decomposition).
rust-version SSoT:
- 27+ member crates migrated from inline rust-version = "1.75" to
rust-version.workspace = true. Workspace declares 1.77; the inline 1.75 pins
were stale and misleading (with resolver 2 the workspace MSRV won anyway).
cargo check --workspace: clean (only pre-existing sqlx-postgres future-incompat
warning + frustration-matrix dead-code warning, neither introduced by this change).
Note: _assembler/ lives outside _primitives/_rust workspace, so its Cargo.toml
was not touched here. Remaining edition-2024 question for _assembler is a
separate decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
cb1090bef3 |
fix(security): RCE allowlist + WebSocket auth + SSH option-injection
Group D — three independent security primitives hardening (post-audit 2026-05-02).
kei-runtime — atom invoke RCE allowlist:
- invoke.rs: is_safe_crate_name validator (regex ^kei-[a-z][a-z0-9-]+$);
rejects /, \\, .., :, absolute paths, empty, >128 chars.
InvalidAtom error variant.
stdout/stderr capped at 16 MiB (was unbounded).
- main.rs: InvalidAtom mapped to exit code 2.
- tests/invoke_exit_codes_smoke.rs: invoke_unsafe_crate_name_exits_2 added.
- Closes: any user able to write atoms/*.md with crate_name: "rm" or "sudo"
triggered arbitrary command execution.
kei-graph-stream — WebSocket bearer + Origin:
- auth.rs (new, 142 LOC): token load + bearer extraction + Origin allowlist +
ConstantTimeEq compare; 8 unit tests.
- ws.rs: ws_handler validates Origin + bearer before upgrade (403/401 on failure).
- main.rs: --public-bind-i-accept-the-leak flag required for non-loopback bind;
else bail!() with explicit error.
- tests/smoke.rs: rewritten with Origin + bearer headers via connect_async_with_config.
- Closes: WebSocket /stream had zero auth, zero Origin check; browser CSWSH could
subscribe to agent activity broadcast; KEI_GRAPH_STREAM_BIND env silently
accepted any SocketAddr.
kei-compute-baremetal — SSH option injection (CVE-2023-51385 class):
- ssh.rs: is_safe_user + is_safe_host validators (alphanumeric + -_.; reject leading -;
max 64 chars; no @, :, /, \\, space).
- ssh.rs: -- sentinel before user@host argv (OpenSSH 9.6+ stops flag parsing).
- ssh.rs: StrictHostKeyChecking=yes default; KEI_BAREMETAL_ACCEPT_NEW=1 for TOFU.
- error.rs: InvalidRegion variant.
- provider.rs: validators applied in target_for_spec + target_for_handle.
- Closes: spec.region "-oProxyCommand=evil" triggered local RCE before TCP connect.
Test results: 29 passed; 0 failed across all three crates. cargo check clean.
Findings: RCE allowlist (Wave-A) + WebSocket auth (Wave-B) + SSH injection (Wave-B)
were unique-per-retest discoveries. None present in original wave-1 audit.
Note: kei-compute-baremetal/src/provider.rs at 300 LOC (was 268; +32 from validators).
Pre-existing >200 LOC violation, fix scope was security-additions only. Follow-up:
split provider.rs into provider.rs (<200) + provider_tests.rs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9aa29aca15 |
fix(kei-cortex): SSRF + atomic token + body limits + capped reads
Group C — kei-cortex daemon security hardening (post-audit 2026-05-02).
- fal_ssrf.rs (new): validate_fal_url whitelist (fal.ai/.media/.run only).
Applied to upload_url, file_url, status_url, images[0].url,
and download_image. Closes SSRF where compromised fal response
could direct daemon to fetch IMDSv1 (169.254.169.254) and
stream cloud creds.
- fal_pipeline.rs (new): HTTP step functions extracted from fal.rs; fal.rs trimmed
to thin orchestrator (101 LOC, was over 200 LOC limit).
- auth.rs: save_token now writes to <path>.<nanos>.tmp + sync_all + rename. Was
non-atomic OpenOptions truncate+write — crash mid-write produced empty
token file -> bootstrap rotated -> stale clients locked out.
- routes.rs + routes_auth.rs (new): explicit DefaultBodyLimit per route — chat 256 KiB,
tool/apply 11 MiB, pet/interaction 64 KiB, tts 32 KiB.
Bearer auth middleware extracted to routes_auth.
- handlers/chat.rs: validate_body enforces MAX_MESSAGE_CHARS = 50_000. Closed cost
amplification where 1.99 MiB chat message billed 500K tokens
($1.50/turn at Sonnet pricing) on every send.
- anthropic_sse.rs: SseParser MAX_BUF = 1 MiB cap; was unbounded — peer streaming
1 GB without \\n\\n would OOM daemon.
- http_helpers.rs (new): HTTP_CLIENT: Lazy<reqwest::Client> shared across handlers
(was per-request Client::new() => 100-300ms TLS handshake
per chat turn, no HTTP/2 multiplexing, fd leak risk on
macOS TIME_WAIT).
- http_helpers.rs::read_capped: per-response body cap (16 KiB error / 64 MiB success).
Applied to anthropic, anthropic_invoker, elevenlabs,
fal_pipeline. Closed unbounded resp.text() / .bytes()
pattern that compromised upstream could exploit.
Test results: 462 passed; 0 failed (single-threaded). cargo check clean.
2 pre-existing port-binding flakes in openai_loop_wiring tests are unrelated.
Findings consensus: fal SSRF + body-size + bearer-token-atomicity appeared in
Wave-A retest; chat message cap + SSE buf cap appeared in Wave-A only. Would have
been missed by single audit pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8b0401b9db |
feat(auth): JWT verification + OAuth CSRF + PKCE + secret redaction
Group B — auth-crate security hardening (post-audit Sonnet test-retest 2026-05-02).
kei-auth-apple:
- jwt.rs: full ES256 JWKS signature verification (jsonwebtoken crate);
validates iss == https://appleid.apple.com, aud == client_id, exp, iat;
decode_id_token_unverified is now cfg(test)-only.
Module docstring promised this since v0.1 — now actually implemented.
- claims.rs (new): IdTokenClaims + AudClaim extracted from jwt.rs.
- error.rs: JwtVerify, JwtDecode, MissingClaim variants.
- client.rs: client_secret_jwt: SecretString (was String); exchange_code accepts
code_verifier: Option<&str> for PKCE.
- provider.rs: verify() does CSRF expected_state ConstantTimeEq + JWT verification;
build_auth_url accepts state + verifier and emits PKCE code_challenge.
- tests/apple_smoke.rs + helpers/: 6 tests including malformed-JWT + non-Apple OAuth +
400-mapping + provider_verify_csrf_mismatch_rejected.
kei-auth-google:
- pkce.rs (new): pkce_challenge + url_encode (RFC 7636 §B.1 test vector covered).
- client.rs: client_secret: SecretString; exchange_code accepts code_verifier.
- provider.rs: verify() rejects on state mismatch; build_auth_url emits S256 challenge.
- tests/google_smoke.rs: 7 tests including CSRF mismatch.
kei-auth:
- main.rs: resolve_token() supports stdin (-) and KEI_AUTH_TOKEN env. Token positional
arg leaked via /proc/<pid>/cmdline + shell history; same fix that v0.14.1
applied to --key.
- main.rs::key(): hard fail if KEI_AUTH_KEY len < 32 bytes (mirror of magiclink).
- tokens.rs::verify(): query_row(...).optional()? instead of .ok() — DB errors now
propagate instead of being swallowed as "token unknown".
kei-runtime-core:
- secrets.rs (new, 81 LOC): SecretString newtype with redacted Debug + zeroize-on-Drop.
Required by every auth crate that holds secret material.
- traits/auth.rs: AuthChallenge::Password.password is now SecretString;
OAuthCode { state, expected_state }.
- error.rs: CsrfStateMismatch variant.
Test results: 48 passed; 0 failed across kei-auth, kei-auth-apple, kei-auth-google,
kei-auth-magiclink, kei-runtime-core. cargo check --workspace clean.
Findings consensus: Apple JWT unverified + OAuth state CSRF appeared in all 3
audit waves (Wave-1 + Wave-A + Wave-B); PKCE absence + secret-derive-Debug appeared
only in Wave-A retest, would have been missed by single-pass audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
52a02dfbff |
feat(live-graph): WebSocket activity stream — orchestrator-centric live view
User pushback: "транслирует в онлайне какие агенты создаются? основное
окно агента, а дальше при запусках появляются новые ветки, мы показываем
в онлайне как агенты собираются и работают"
Earlier `kei-graph-export` rendered the static SUBSTRATE (all 581 atoms,
catalog-style). User wanted the LIFECYCLE: orchestrator at center, every
new agent as a fading-in branch, every tool call as a pulse, every
completion as a fade-out. TTL = until done; pure online, no history
accumulation per user direction.
Three-layer architecture, all conforming to schema /tmp/agent-events-schema.md:
LAYER 1 — Event emitters (4 hooks)
hooks/agent-event-spawn.sh PreToolUse:Agent → agent_spawn event
hooks/agent-event-done.sh PostToolUse:Agent → agent_done event
(parses STATUS-TRUTH MARKER for outcome,
computes cost_usd from token×pricing table)
hooks/tool-use-event.sh PreToolUse:Bash|Read|Edit|Write|Grep|Glob|NotebookEdit
→ tool_use event
hooks/skill-record.sh EXTENDED — second emit step writes skill_use
event in addition to existing kei-ledger
record-skill call
All 4 are POSIX /bin/sh, defensive (never block, exit 0), bypass via
KEI_EVENTS_BYPASS=1. Append-only JSONL to
~/.claude/memory/agent-events.jsonl.
Smoke: 4 synthetic invocations cover spawn/done/tool/filter cases.
LAYER 2 — kei-graph-stream Rust daemon
_primitives/_rust/kei-graph-stream/ (~480 LOC, 5 files + 1 test)
- Tails events.jsonl every 200ms (poll-based, no notify dep).
- Parses each event, updates AliveState (insert on spawn, remove on done).
- Broadcasts {"type":"event","data":<event>} to all WebSocket clients.
- On client connect: sends {"type":"snapshot","alive":[...]} first.
- Heartbeat: {"type":"ping"} every 30s.
- axum 0.7 + ws feature (already in Cargo.lock via kei-cortex).
- Bypass: KEI_GRAPH_STREAM_BYPASS=1.
Bound to 127.0.0.1:8201 (loopback only). Endpoints:
GET /stream → WebSocket upgrade
GET /health → "kei-graph-stream alive"
4 unit + 1 integration test. cargo build clean.
Installed binary: ~/.cargo/bin/kei-graph-stream
Launchd plist: io.keisei.graph-stream (RunAtLoad, KeepAlive)
Loaded as PID 52678, /health 200 OK verified.
LAYER 3 — live-graph.html (single-file frontend)
~/Projects/lbm-graph-viz/live-graph.html (~464 LOC, self-contained)
- SVG full-viewport, dark #0f172a, CSS grid background.
- Pinned center node "main" (orchestrator), gold #fbbf24, glowing.
- Agents radiate via D3 force-simulation; color-by-model
(sonnet=green, opus=red, haiku=blue, default=gray).
- On agent_spawn: fade-in 300ms, edge from main to new node.
- On tool_use: pulse on agent node (r 8→12→8 over 400ms) +
floating tool name label fades 800ms.
- On agent_done: outcome-color flash → fade-out 800ms → remove.
- WebSocket client: ws://127.0.0.1:8201/stream, exponential-backoff
reconnect (1s→30s).
- Top-right status badge: ● connected | ○ reconnecting | ✕ disconnected.
- Bottom counters: alive / spawned / tool calls / done / last event age.
- No build step. D3 v7 from CDN. Pure HTML+JS+CSS.
End-to-end smoke (this machine, just now):
- daemon health 200 OK
- hook injected agent_spawn → daemon broadcasts → AliveState=1
- hook injected agent_done → daemon broadcasts → AliveState=0
- frontend file syntax-checked clean
What this does NOT do (deferred, by user direction "это онлайн"):
- History persistence — agents who finished are GONE from the graph.
Per-session log remains in events.jsonl + sleep-sync if user wants
to consult later, but the live view is RIGHT NOW only.
- Sub-agent attribution beyond "main" — orchestrator-direct tool calls
show on the orchestrator node. Sub-agent's internal tool calls would
need session-id correlation; current schema has agent_id="main"
placeholder for non-Agent tool calls.
- Replay mode — no time-scrubber. Possible follow-up if useful.
- Auth on WebSocket — bound to 127.0.0.1 only. Local-only by design.
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes
follow-up-required:
- Sub-agent tool-call attribution (correlate session_id chain)
- Replay mode with time scrubber (if user finds use)
- Tool aggregator nodes ("Bash bucket" with N) instead of per-agent pulses
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a31a056f61 |
feat(graph): live runtime DNA viewer — kei-graph-export + lbm-graph-viz adapter
User pushback: "можно нашего Кейси подключить к обсидиан? будет в
онлайне строить граф из всех наших агентов?"
Closer-to-question architecture: don't build new Obsidian plugin —
re-use the legacy `~/Projects/lbm-graph-viz/` D3 viewer (lineage:
keicode → living-graph → lbm → lbm-graph-viz → keisei-graph). Strip
its Hebbian/co-change edges, replace with DNA-derived edges from the
kei-registry + kei-ledger. Open in any browser, file://...index.html.
NEW Rust crate `_primitives/_rust/kei-graph-export/` (~440 LOC, 5 files)
Reads:
~/.claude/registry.sqlite (730 active blocks)
~/.claude/agents/ledger.sqlite (6 agents post-cleanup)
_manifests/*.toml (38 agent manifests)
Emits 581-node, 291-edge graph. Edge types:
block_dep 171 manifest → atom (blocks=[])
path_ref 99 manifest → atom (path:NAME refs)
branch_lineage 11 parent_branch → branch
agent_uses_manifest 10 agent → manifest (slug from branch name)
Output formats:
--format spaces-fragment → `window.RUNTIME_SPACE = {...}` JS file
--format json → raw {nodes, links} for downstream tools
Block-name lookup is multi-resolution: each block is registered under
display name + lowercased + file-stem slug (from path basename) so
manifest references like `blocks = ["baseline"]` resolve to a registry
row whose `name` column holds "BASELINE — inherit from Main Claude".
Without this fix the graph had 0 block_dep edges; with it, 171.
NEW background updater `hooks/graph-export-watcher.sh` + launchd plist
template `_primitives/templates/io.keisei.graph-export.plist`
5-second loop:
while true; do
kei-graph-export --format spaces-fragment --output <viz>/data-runtime.js.tmp
mv <viz>/data-runtime.js.tmp <viz>/data-runtime.js # atomic
sleep 5
done
launchd plist substitutes `HOME_DIR` and `HOOKS_DIR` placeholders at
install time. RunAtLoad=true, KeepAlive=true. Logs to
~/.claude/memory/graph-export.log. Bypass: GRAPH_EXPORT_BYPASS=1.
Loaded into user-side launchd (PID 16474 confirmed running). File
mtime advances every 5s — live updates verified.
PATCH `~/Projects/lbm-graph-viz/index.html` (outside kit, surgical)
Three changes:
1. Add `<script src="data-runtime.js">` BEFORE `spaces.js` (window
global available when SPACES is defined).
2. After spaces.js: `if (window.RUNTIME_SPACE) SPACES.runtime = window.RUNTIME_SPACE;`
3. Auto-refresh setInterval(5s): fetch data-runtime.js, eval (re-
assigns window.RUNTIME_SPACE), hash-compare, re-render via
`rebuildGraph()` if currently viewing the runtime space.
window.RUNTIME_SPACE (not const RUNTIME_SPACE) avoids the
"const cannot be re-declared" error on subsequent eval() calls.
Effect: open file://~/Projects/lbm-graph-viz/index.html in any
browser, switch to "Runtime" space — full DNA graph of every agent /
atom / skill / branch / manifest / hook / primitive / rule, force-
laid-out by D3. Updates every 5 seconds without page reload.
What this does NOT do (deferred):
- Obsidian mirror — separate work, would emit .md per node into
~/Projects/KeiSeiVault/. Useful for backlinks navigation but
file-watcher latency similar to current 5s polling.
- Skill-invocation edges — table is empty until next Skill tool
use; will populate naturally.
- Scoped queries (orphan finder, hot-path PageRank). Out of scope
for v1; the JSON --format export feeds any downstream tool.
- `agent_uses_manifest` heuristic warns on unknown subagent slugs
(e.g. `physics-deriver` with no manifest yet). Non-fatal.
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes
follow-up-required:
- Obsidian vault mirror (Phase C, separate work)
- Skill-edges populate from real Skill use (not blockered)
- Hot-path PageRank highlighting in viewer (cosmetic)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e073df6c98 |
feat(tracking): close 3 last observability gaps — toolStats + skill-record + numeric-claims journal
Closes the loop on "without full tracking the system can't make decisions" (user pushback on partial coverage). Three gaps that left the inference layer blind are now wired: GAP #1 — agent toolStats / token counts / cache hits captured ================================================================ `agent-outcome-backfill.sh` now appends one JSONL row per spawn to `~/.claude/memory/time-metrics/agent-toolstats.jsonl` with: agent_id, outcome, stubs, ts, tool_use_count, duration_ms, tool_stats {Read:N, Bash:M, ...}, tokens_in, tokens_out, cache_read, cache_write Sidecar journal (no schema migration). Production payload's .tool_response.totalToolUseCount / totalDurationMs / toolStats / usage fields land directly. Smoke-tested with synthetic spawn — row written. GAP #2 — skill_invocations table actually receives writes ================================================================ The `skill_invocations` table (schema v8) had 0 rows because no caller existed for `skill_metrics::record_invocation`. Added two pieces: (a) `kei-ledger record-skill <name> --success {0|1}` CLI subcommand Mirrors record-cost; same dispatch shape. Optional `--agent-id`, `--trajectory-id`, `--duration-ms`, `--db`. Validates non-empty name + duration ≥ 0. Outputs `{"ok":true,"skill":"...","ts":N}`. (b) `hooks/skill-record.sh` — PostToolUse:Skill hook. 50 LOC POSIX. Detects Skill tool calls, derives success heuristic from tool_response (exit_code / status / content non-empty), shells out to `kei-ledger record-skill`. Bypass via SKILL_RECORD_BYPASS=1. 83 kei-ledger tests pass (16 unit + 67 integration). Smoke-tested end-to-end: `kei-ledger record-skill test-skill --success 1` inserts a row with correct fields. Phase D nightly skill-metrics decisions (archive if unused N days, re-extract if success<60% over M days, validated if >20 calls + >90% success) now have data to consume. GAP #3 — numeric-claims.jsonl receives every evidence-tagged claim ================================================================ RULE 0.18 mandated three markers `[REAL:]` / `[FROM-JOURNAL:]` / `[ESTIMATE-HTC:]` on every numeric/duration/cost claim, but no hook appended valid claims to the journal — the calibration data RULE 0.18 promised never accumulated. `hooks/numeric-claims-record.sh` — Stop hook, 140 LOC POSIX. Reads transcript_path from stdin, locates the last assistant message via recursive flatten (same pattern as agent-outcome-backfill.sh after the production-payload-shape fix), regex-extracts every `<phrase> [<TIER>: <pointer>]` triple, appends one JSONL row per claim. Idempotent within 1-second window to avoid double-recording on repeat Stop fires. Bypass via NUMERIC_CLAIMS_RECORD_BYPASS=1. Smoke test: synthetic transcript with 3 markers (REAL + ESTIMATE-HTC + FROM-JOURNAL) produced exactly 3 well-formed JSONL rows. Settings.json ================================================================ - PostToolUse:Skill matcher created (or augmented if already present) with skill-record.sh. - Stop:* matcher gains numeric-claims-record.sh after the existing chain (stop-verify, task-timer, session-end-dump, extract-task- durations, chat-numeric-postflag, affect-threshold-check, enrich-from-jsonl). What this does NOT do (deferred): - Backfill `skill_invocations` from past traces (history started today; Phase D cohort builds forward from now). - Migrate the agent toolStats sidecar JSONL into a proper ledger column. Append-only file is fine for the current scale. - Refactor main.rs (now 233 LOC, was 212; pre-existing CP debt flagged by skill-record agent — separate cleanup PR). === STATUS-TRUTH MARKER === shipped: functional stubs: 0 cargo-check: PASS behaviour-verified: yes follow-up-required: - kei-ledger main.rs Constructor Pattern split (212→233 LOC) - Verify in next session: skill_invocations gets rows from real Skill tool use; numeric-claims.jsonl gets rows from real assistant messages with markers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
af46684330 |
feat(secrets+catalog): orphan-detector for env vars + image/video/voice models
Two parallel agents (both Sonnet 4.6 via the just-activated tier system)
extended the substrate-unified-registry. First end-to-end proof that the
Phase 4 router refactor saves money: no Opus spawns this round.
PART 1 — `kei-registry secrets` subcommand (Agent A — code-implementer)
Reads env-var NAMES from `~/.claude/secrets/.env` (RULE 0.8 SSoT) and
per-project `secrets/*.env`, greps the kit tree for usages, reports
orphans (defined but unreferenced). Live run on this kit found 26 keys,
11 ORPHAN — actionable cleanup candidates incl. GitHub OAuth client
creds, Godaddy keys, KeiGit admin creds, KEI_MEMORY_TOKEN.
Files:
- `_primitives/_rust/kei-registry/src/secrets.rs` (152 LOC) — pure
read-side cube. SecretsReport + KeyRow types, env-file parser
(KEY=value lines, validates `^[A-Z][A-Z0-9_]*$`), walkdir-based
scanner with skips (target/ node_modules/ .git/ _generated/),
word-boundary regex per key. ASCII + JSON render.
- `_primitives/_rust/kei-registry/src/secrets_tests.rs` (125 LOC) —
5 unit tests covering env parse, scan correctness, word-boundary
regression (`MY_KEY` ≠ `MY_KEY_EXTRA`), JSON roundtrip, ORPHAN marker.
- `_primitives/_rust/kei-registry/src/secrets_handler.rs` (58 LOC) —
CLI dispatch handler.
- `cli.rs`, `handlers.rs`, `lib.rs` extended with Secrets variant.
Resolves the asymmetry called out in the design discussion: paths got
atomization (commit
|
||
|
|
cb1fddeabb |
feat(model-tier+branch-dna): activate cost router + give branches DNA
Phase 4 of substrate-unified-registry: turn on the existing
kei-model-router by changing manifest defaults from `model = "opus"`
to `model = "sonnet"` for routine agents, and give every git branch
a deterministic DNA in the kei-status dashboard.
The model-tier system was BUILT (`_primitives/_rust/kei-model-router/`
crate with Beta posterior, complexity τ-estimator, escalate ladder,
calibrate subcommand) and the advisor hook
(`~/.claude/hooks/model-router-advisor.sh`) was REGISTERED. But every
ledger row from this session ran on Opus because:
1. All 38 manifests hard-coded `model = "opus"` → no chance for the
router to recommend cheaper.
2. The orchestrator (me) ignored the stderr advisory.
This commit closes (1). (2) is a behavioural change tracked separately.
Manifest reclassification (4 Opus + 34 Sonnet):
Opus (hard reasoning):
- architect (system-design synthesis)
- ml-implementer (Math-First paradigm)
- ml-researcher (literature analysis)
- security-auditor (deep risk synthesis)
Sonnet (everything else):
- 8 code-implementer-* + code-implementer
- 5 critic-* + critic
- 6 infra-implementer-* + infra-implementer
- 4 researcher-* + researcher
- 6 validator-* + validator
- 3 security-auditor-{differential,supply-chain,variant}
- cost-guardian, fal-ai-runner, frontend-validator, modal-runner
Regenerated all 38 `_generated/*.md` so the YAML frontmatter `model:`
field matches the manifest.
Branch DNA (kei-registry status):
- New `compute_branch_dna(name, commit_sha)` in `status.rs`. Format
`branch:
|
||
|
|
d6dbdee870 |
feat(kei-registry): status subcommand — cross-cutting substrate dashboard
Phase 3 of substrate-unified-registry: a single command shows every
live artefact across the three sources without merging stores.
`kei-registry status` joins:
1. `blocks` table (kei-registry SQLite) — active counts per BlockType,
plus the registered path-atoms with DNA prefix + body sha8.
2. `git for-each-ref refs/heads` (shell-out, no DB persistence) — local
branches, current marker, ahead/behind via `upstream:track,nobracket`.
3. `agents` table (kei-ledger SQLite) — fork counts per status
(running/done/failed/merged/rejected). Missing ledger DB → section
skipped, never an error.
Output: ASCII multi-section table by default; `--format json` for
machine consumption.
Files:
- `_primitives/_rust/kei-registry/src/status.rs` — new module, ~270
LOC. Pure read-side per Constructor Pattern. 7 unit tests cover
`parse_track` (in sync / ahead / behind / both / "gone"), DNA prefix
rendering, and empty-status section presence.
- `_primitives/_rust/kei-registry/src/cli.rs` — new `Status` variant
with `--db`, `--git-repo`, `--ledger-db`, `--format` flags.
- `_primitives/_rust/kei-registry/src/handlers.rs` — `handle_status`
dispatcher, ASCII/JSON branching.
- `_primitives/_rust/kei-registry/src/lib.rs` — module export.
End-to-end run from kit root shows the prior gap: 17 local branches
(many `worktree-agent-*` orphans), kei-ledger summary 4 running /
158 done / 35 failed / 7 merged / 0 rejected — visibility the user
asked for ("в каждой сессии видеть, чтобы не бегать по диску в
поисках несмерженных").
What this does NOT do (Phase 4):
- No orphan detection (`kei-status orphans`) — counts only.
- No auto-registration of branches into kei-ledger (Phase 2). Branches
come from live `git for-each-ref` shell-out; if the repo moves or
is deleted the row vanishes from the dashboard. Acceptable for v1.
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes
follow-up-required:
- Phase 2 (post-checkout hook → kei-ledger auto-register)
- Phase 4 (orphan detection: branches with no commits in N days,
path-atoms with no consumers, agent forks stuck running)
- --filter flags (--type, --status) for targeted queries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
22ae9d1de5 |
fix(kei-registry): short_path strips _blocks/_manifests/_atoms/_roles/_caps/agents
Root-cause of the username-path leak in DNA-INDEX.md (107 atom rows in v0.17 — sed-patched in |
||
|
|
c35e1ae9ca |
chore(kit): wire kei-db-contract into installer + drop final #[path] hack
A1 — install.sh wiring for kei-db-contract:
- install/lib-substrate.sh substrate_core_binaries(): add kei-db-contract
to always-copy list. End users now get the binary in ~/.cargo/bin/
immediately after install (no manual cargo install --path needed).
A2 — Wave B follow-up: drop #[path] hack from guard_test_corpus.rs
- tests/guard_test_corpus.rs: #[path = "../src/injection_*"] mod ...
→ use kei_memory::injection_guard::scan
- Now uses Wave B's [lib] target like tests/integration.rs already does.
- 4 tests still pass.
Verified via cargo test: 18 lib + 4 corpus + 3 ingest_guard + 1 injection_unit
+ 4 dedup + 8 integration + 4 ingest_real_trace = 42 tests, all green.
=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
cargo-test: PASS (42 tests, 0 failures)
behaviour-verified: yes
follow-up-required:
- tests/ingest_guard_tests.rs already migrated (Wave A's earlier work)
- kei-db-contract still requires kit user to have run install.sh; existing
installs need re-run. Kit ledger-validate should add post-install probe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
