Skip to content

Couragehong/feat/boot loop stack - #109

Closed
couragehong wants to merge 9 commits into
feat/go-migrationfrom
couragehong/feat/boot-loop-stack
Closed

couragehong wants to merge 9 commits into
feat/go-migrationfrom
couragehong/feat/boot-loop-stack

Conversation

@couragehong

Copy link
Copy Markdown
Contributor

Summary

  • What changed:
  • Why:
  • Scope:

Validation

  • Tests run (or explain why not):
  • Docs updated (if behavior/setup changed)

Cross-Agent Invariants

  • scripts/bootstrap-mcp.sh remains the single source of truth for runtime prep (venv/deps/self-heal)
  • No agent-specific script duplicates bootstrap/setup logic
  • Agent-specific scripts remain thin adapters (registration/wiring only)
  • Codex-only commands (codex mcp ...) are clearly separated from cross-agent/common instructions
  • Claude/Gemini/OpenAI instructions do not include Codex-only commands
  • SKILL.md, commands/rune/*.toml, and AGENT_INTEGRATION.md stay consistent on boundaries

Notes for Reviewers

  • Risk areas:
  • Backward compatibility impact:
  • Follow-up work (if any):

esifea and others added 9 commits May 7, 2026 12:41
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.
@couragehong couragehong closed this May 7, 2026
@couragehong
couragehong deleted the couragehong/feat/boot-loop-stack branch May 7, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants