Repository navigation
feat(boot): end-to-end boot loop — super-set of #104 - #110
Merged
Merged
Conversation
The Vault GetAgentManifest manifest_json carries 'EncKey.json' as a string
that is already a pyenvector-compatible KeyEnvelope (provider_meta + entries
per third_party/evi/include/km/KeyEnvelope.hpp), produced by libevi's
evi_km_wrap_enc_key when keys were generated. The envector SDK loads it via
evi_km_unwrap_enc_key which expects that exact envelope shape.
The previous SaveKeys re-wrapped the bytes ('{"enc_key": "<base64>"}')
which would always fail the cgo unwrap downstream — boot would dial fine,
fetch the manifest, persist a corrupted file, then envector OpenKeysFromFile
would reject it and the daemon would never reach Active.
Renames SaveKeys → SaveEncKey to reflect the single-key scope (Vault no
longer ships EvalKey/SecKey to the plugin). Adds KeyDir(keyID) helper
returning the per-key directory path that envector's WithKeyPath expects
(parent of EncKey.json), so callers don't reconstruct the path inline.
Per spec/components/embedder.md §소켓 경로, the runed daemon socket lookup order is (1) env RUNE_EMBEDDER_SOCKET, (2) config.embedder.socket_path, (3) ~/.runed/embedding.sock default. Centralises that priority chain so both the boot loop and any test/diagnostic harness use the same resolution. ResolveSocketPath(configPath string) string — pass empty string when the config layer doesn't carry an explicit override; returns absolute path under $HOME for the default branch.
The Inject{Vault,Embedder,Envector} methods only set adapter clients on
services. Per-token Vault metadata (AgentID / AgentDEK / IndexName / KeyID)
also needs to reach CaptureService / RecallService / LifecycleService —
without it, capture's AES envelope sealing runs with a zero-value DEK and
recall / lifecycle diagnostics surface zero-value IndexName.
ApplyVaultBundle(*vault.Bundle) does the metadata propagation in one call:
- Capture gets AgentID, AgentDEK, IndexName
- Recall gets IndexName
- Lifecycle gets IndexName, KeyID, AgentDEK, EncKeyLoaded=len(EncKey)>0
Nil-bundle is a no-op for symmetry with other Inject* methods.
…ctor config Rewrites the boot loop to address five integration bugs in #104's first draft, plus a handful of hygiene fixes. Critical: 1. envector.ClientConfig.KeyDim is now set (DefaultKeyDim=1024 const matching Qwen3-Embedding-0.6B per spec). Without it, OpenKeysFromFile().validate() rejects the call with 'WithKeyDim required'. 2. envector.ClientConfig.KeyPath now points to the per-key directory (~/.rune/keys/<keyID>/) via keymanager.KeyDir, not its parent. envector resolves EncKey.json directly under WithKeyPath. 3. embedder.New now uses embedder.ResolveSocketPath("") instead of a TODO empty-string placeholder. 4. After Vault.GetAgentManifest, deps.ApplyVaultBundle(bundle) propagates AgentID / AgentDEK / IndexName / KeyID to the service structs (formerly missing → capture's AES envelope would seal with empty DEK). 5. keymanager.SaveEncKey replaces SaveKeys (matching the rename). Hygiene: - bootOnce helper isolates one boot attempt so partial-success cleanup becomes uniform: any failure after a NewClient succeeds closes the partial conns (vault/embedder/envector) before retrying. No more gRPC conn leak on retry. - RunBootLoop runs ctx.Err() at the top of each iteration so SIGTERM during sleep stops cleanly. - empty-config / config-load failures now record into m.lastError so diagnostics surfaces the cause; attempt counter increments uniformly. - 'persistent failure' log fires every 20 attempts based on attempts (was checked inline only on Vault path). - Added Manager.LastError() public reader for diagnostics tools. - BootAdapterInjector gains ApplyVaultBundle method for the metadata propagation step. Spec note (open follow-up): RunBootLoop returns on first Active. Re-init after dormant↔active transitions is the responsibility of service.LifecycleService.ReloadPipelines (which spawns a fresh RunBootLoop goroutine) — the boot loop itself does not loop forever after success, contrary to a literal reading of rune-mcp.md §부팅 시퀀스. Will revisit if reload behavior turns out wrong.
Adds 18 test cases / 3 packages:
internal/adapters/keymanager/keys_test.go (6 fn):
- WritesBytesVerbatim — file content byte-identical to input
(the regression that motivated this PR)
- EmptyIsNoop — nil / empty []byte both no-op, no dir created
- FilePerm0600 — written file is 0600
- DirPerm0700 — created keyDir is 0700
- KeyDir_ReturnsExpectedPath — $HOME/.rune/keys/<keyID>
- OverwritesExisting — repeat call replaces content
internal/adapters/embedder/socket_test.go (5 fn):
- EnvVarWins — RUNE_EMBEDDER_SOCKET trumps configPath
- ConfigUsedWhenEnvUnset — explicit config path used when env empty
- DefaultWhenNothingProvided — $HOME/.runed/embedding.sock fallback
- DefaultEndsInRunedDir — invariant on default suffix
- EmptyConfigStringFalsThrough — explicit empty string ≠ valid path
internal/mcp/apply_bundle_test.go (6 fn):
- PropagatesToCapture — AgentID / AgentDEK / IndexName set
- PropagatesToRecall — IndexName set
- PropagatesToLifecycle — IndexName / KeyID / AgentDEK / EncKeyLoaded
- EncKeyLoadedFalseWhenEmpty — len(EncKey)==0 → EncKeyLoaded=false
- NilBundleNoOp — nil bundle leaves services untouched
- NilServicesNoOp — nil-safe: empty Deps doesn't panic
All pass under $HOME redirection via t.Setenv (no real $HOME side
effects).
…hon parity)
Mirrors Python server.py _set_dormant_with_reason: when boot loop
encounters a terminal failure (config missing, vault creds missing, or
config.state already dormant), update ~/.rune/config.json with
state="dormant" + dormant_reason + dormant_since (RFC3339 UTC). The
next process spawn picks up the same state without re-attempting boot
until the user explicitly runs /rune:configure or /rune:reload_pipelines.
Adds:
- config.Save / SaveToPath — atomic-ish 0600 write (mirrors Python's
O_WRONLY|O_CREAT|O_TRUNC pattern).
- config.MarkDormant(reason) — load → check idempotency → update fields
→ save. Creates a fresh Config when config.json is missing (fresh
install path).
Wires three boot.go bootDormant branches:
- config.json missing → MarkDormant("not_configured")
- config.state already "dormant" → MarkDormant(refresh) — reaffirms
timestamp + reason on each spawn so
diagnostics shows the most recent
cause
- vault endpoint/token missing → MarkDormant("vault_unconfigured")
Vault transient failures (dial / GetAgentManifest) still retry in-memory
without touching config.json — those are recoverable network blips, not
user-actionable terminal states. Diverges from Python on this point
(Python = always disk-persist on Vault fail) per spec/components/rune-mcp.md
§부팅 시퀀스 retry policy.
Tests (9 fn / config_test): fresh-install create, existing-config field
preservation, idempotent same-reason, new-reason overwrite, 0600 perm,
DormantSince timestamp window, Save roundtrip, dir auto-create, error
prefix sanity.
config.Load wraps the underlying os.PathError via fmt.Errorf("...: %w", err),
so os.IsNotExist (which only checks the immediate error sentinel, not the
unwrap chain) returned false on fresh installs. The result: boot loop
treated missing config.json as a transient parse error and retried
forever instead of going dormant("not_configured").
Caught by smoke scenario #3 (rm ~/.rune/config.json → run binary):
before: ERROR boot: failed to load config err="config: read ...: no such file"
after: WARN boot: config.json not found — entering dormant
+ ~/.rune/config.json created with state=dormant reason=not_configured
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
couragehong
added a commit
that referenced
this pull request
May 8, 2026
The Go rune-mcp at internal/* + cmd/rune-mcp/ has reached parity (PR #102 + #110 + #117) and end-to-end verification (#118, #122, #124), which means the Python tree is now dead weight. Carrying both implementations is actively misleading: a fresh contributor following the in-repo install instructions would still land in mcp/ + agents/ and try to set up a venv that no longer ships, and parity audits keep re-discovering the Python source instead of treating the Go side as the source of truth. Removed ------- agents/common/ 12 files (config, embedding, llm, schemas) agents/retriever/ 4 files (query_processor, searcher, synthesizer) agents/scribe/ 12 files (detector, llm_extractor, handlers, server) agents/tests/ 16 files (pytest suite) agents/__init__.py agents/README.md (Python agents intro — gone with the impl) agents/SLACK_SETUP.md (Slack notifier setup for Python scribe) mcp/ 18 files (Python adapter + server + tests) requirements.txt (root Python dependency list) scripts/migrate_embeddings.py (one-off Python migration helper) Total: 67 files, 17,815 lines. Kept (intentional) ------------------ agents/claude/{scribe,retriever}.md referenced by .claude-plugin/ plugin.json — agent prompts that the runtime loads agents/codex/scribe.md Codex-side agent prompt agents/gemini/{scribe,retriever}.md Gemini-side agent prompts benchmark/ deferred (separate decision — rewrite in Go vs delete entirely) docs/v04/spec/python-mapping.md parity blueprint that maps Python source to Go destinations; useful as a historical record post-deletion docs/migration/*.md migration plan + audit trail — intentional history scripts/bootstrap-mcp.sh and other referenced by gemini-extension.json; Python-era shell scripts removal blocked on Gemini support decision (separate PR) Not in scope ------------ .github/workflows/pr-tests.yml + pr-comment.yml — Python pytest CI; handled by PR #125 (ci-drop-python). README.md, CLAUDE.md, SKILL.md, AGENT_INTEGRATION.md, GEMINI.md, CONTRIBUTING.md — top-level docs still describe the v0.3 install flow; rewrite scheduled separately so this commit stays focused on code deletion. Verification ------------ go build ./... passes go vet ./... passes go test ./... full suite passes (no test referenced deleted paths) grep across remaining .{go,md,json,sh,toml,yml,yaml} for the deleted paths returned zero hits — no dangling references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#104 commit 3개를 cherry-pick하고 그 위에 critical bug 수정, Python parity 정합성 체크 및 테스트 추가했습니다.
_init_pipelines/_set_dormant_with_reason와 cross-check 한 결과 6건의 bug + 5건의 hygiene 이슈를 발견했고 모두 수정했습니다.keymanager/keys.goKeyEnvelope.hpp) 인데 base64 + 자체 wrapping → libevievi_km_unwrap_enc_key가 거부. byte-verbatim write 로 수정boot.goenvector ClientConfigKeyDim누락 →OpenKeysFromFile().validate()가Dim<=0거부.DefaultKeyDim=1024추가 (Qwen3-Embedding-0.6B)boot.goenvector ClientConfigKeyPath: runedir+"/keys"→ envector 가 keyID 디렉토리 직접 기대.keymanager.KeyDir(bundle.KeyID)사용boot.goembedderembedder.New("") // TODO→ 빈 socket path.embedder.ResolveSocketPath()priority chain 구현 (env → config →~/.runed/embedding.sock)tools.goInject*AgentID/AgentDEK/IndexName/KeyID누락 → capture AES sealing 빈 DEK 으로 fail.Deps.ApplyVaultBundle신규config.MarkDormant(reason)로 disk 영구 기록 (Python_set_dormant_with_reason동등)Python parity 정합성
state != "active"strict check (server.py:L1544)cfg.State != "active"통합 분기 (""/ 알 수 없는 값도 dormant)MarkDormant의도된 spec divergence (변경 없음)
PIPELINE_NOT_READYHygiene
bootOncehelper 분리 (retry 시 conn cleanup) /bootResultenum (retry/active/dormant) / ctx-aware sleep 통일 / attempt counter 일관성 /Manager.LastError()readerTests
keymanager/keys_test.go(6) — byte-verbatim, perms, KeyDir, idempotencyembedder/socket_test.go(5) — env > config > default prioritymcp/apply_bundle_test.go(6) — Capture/Recall/Lifecycle propagation, nil-safetyconfig/dormant_test.go(10) — fresh install / 기존 vault 보존 / idempotent / overwrite / perm / timestamp window / Save roundtripVerification
go build ./...✅go test ./...✅