feat(import): bring your Hermes setup home — MCP integrations, skills, persona, provider keys - #42
Conversation
First slice of the "point Nebo at an existing agent install and adopt it" feature. Introduces a read-only importer under crates/server/src/import/: - detect(): fingerprints an install directory as Hermes or OpenClaw - scan(): walks a Hermes install (~/.hermes) into an ImportManifest describing what was found and what each piece becomes in Nebo, without writing to Nebo or touching the source directory - ImportManifest / ImportItem / TrustTier: the dry-run data model that the "here's what I'll import" confirmation renders Coverage for Hermes: config.yaml (mcp_servers), SOUL.md persona, memories/*.md, skills/**/SKILL.md, cron/jobs.json, .env + auth.json credentials, and state.db history. MCP entries are normalized into the canonical config block and parsed by the existing parse_mcp_servers_block, so the importer and paste-import share one parser. Trust tiers mirror paste-import: stdio MCP servers and script-bundling skills are Code tier (need confirm); remote MCP, personas, memory, and schedules are Content (auto). Credential scanning surfaces key names only, never secret values. Applying a manifest and the OpenClaw walker are follow-up slices. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
Hermes spells its MCP auth field `auth:` with values `oauth` | `header`; Nebo's mcp_integrations vocabulary is `none` | `oauth` | `api_key`. The normalization previously copied the value verbatim, so `auth: header` would have flowed into create_mcp_integration as an invalid auth_type when the apply slice lands. Extracts the Hermes-dialect normalization into normalize_mcp_block() so the mapping is directly testable through parse_mcp_servers_block, adds a header-auth server to the fixture, and widens the secret-leak assertion to cover header tokens across all manifest items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
…vider keys Second slice of the migration importer — the apply half. apply_hermes() turns a scanned Hermes install into real Nebo artifacts through the existing canonical write paths: - mcp_servers → create_mcp_integration rows (shared parse_mcp_servers_block) - skills/**/SKILL.md → user/skills/, copied wholesale so bundled resources survive; Hermes category nesting flattens to Nebo's flat user tier - SOUL.md → one employee: create_agent row + AGENT.md/agent.json/ manifest.json under user/agents/, mirroring the user-created pathway - known LLM provider keys in .env → auth_profiles, matching the existing provider-settings pathway's storage semantics The async apply() wrapper runs the canonical post-install steps: skill loader reload, finalize_agent_install (same finalizer as a marketplace hire), and the MCP bridge connect pass (sync_bridge widened to pub(crate)). Idempotent by skip: existing integration names, skill dirs, the agent dir, and provider profiles are left untouched and recorded in skipped, so re-running an import never duplicates or clobbers. Deferred pieces (memory, history, cron, channel tokens) are named in skipped rather than silently dropped. A test asserts the source directory is byte-identical after apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
Wires the migration importer to its first user-facing surface. Backend: - GET /api/v1/import/detect — probe default install locations (HERMES_HOME / ~/.hermes, OPENCLAW_HOME / OPENCLAW_STATE_DIR / ~/.openclaw), fingerprinted via import::detect, not just dir-exists. OpenClaw reports importable=false until its walker ships. - POST /api/v1/import/scan — read-only dry-run, returns the manifest. - POST /api/v1/import/apply — performs the import via import::apply. - Manifest/outcome JSON is camelCase to match the API surface. Frontend: - Settings → Import page: detected installs with one-tap Scan, manual path entry, grouped manifest preview with "runs code" badges on Code-tier items, import receipt including the skipped list. - Nav entry after MCP; typed API helpers in lib/api/import.ts (hand-written tier, nebo.ts is generated); en locale keys (other locales fall back to en). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
Replaces the hand-written lib/api/import.ts with the generated client as the single pathway to the import endpoints: - handlers/import.rs now returns typed response structs (DetectInstallsResponse / ScanInstallResponse / ApplyInstallResponse) instead of ad-hoc json! blobs, per the "generated TS interface is the single source of truth" convention in handlers/skills.rs - genapi overrides.go gains ImportItem / ImportManifest / ImportOutcome extraInterfaces (their Rust structs live in crates/server/src/import/, outside the generator's struct-scan dirs, and carry enum fields the parser doesn't model) - make gen: nebo.ts + neboComponents.ts pick up detectInstalls / scanInstall / applyInstall fully typed - Settings → Import page switched to the generated client; lib/api/import.ts deleted Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
…mbed Third slice of the migration importer. Hermes memory files stop being deferred and become native, searchable Nebo memories: - memories/*.md parsed into discrete entries (section-sign delimited, paragraph fallback, headings dropped) by one shared parser, so the dry-run counts exactly what the apply writes - each entry lands via the canonical upsert_memory pathway, scoped to the imported employee (ensure_local_user_id + agent_memory_scope), with deterministic content-hash keys (idempotent re-import), provenance tags/metadata, USER.md → tacit/preferences, MEMORY.md → tacit/general - vectors come from the SAME boot backfill native memories use: AppState gains the boot embedding_provider, and apply() spawns backfill_missing_embeddings after a memory-bearing import — FTS recall works immediately, vector recall as soon as the backfill runs - receipt plumbing: ImportOutcome.memories through overrides.go, make gen, the Settings page, and the en locale Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
…stamps
Fourth slice of the migration importer. Hermes conversations stop being
deferred and become real threads of the imported employee:
- read_history() opens state.db read-only (rusqlite) and derives
conversations from the messages table itself — grouped by session_id in
insertion order — rather than Hermes's version-varying sessions table,
with a minimal-column fallback and tolerant epoch parsing (int / float /
string timestamps)
- each conversation becomes a chat named agent:{id}:thread:{chat_id} —
the same session-name shape native threads use, so imported history
lists like any other conversation of that employee
- original timestamps survive via new importer-specific inserts in the
db crate (create_chat_imported / create_chat_message_imported, with
day_marker derived from the original epoch); titles come from the
first user message
- roles outside the chat_messages constraint (Hermes-internal
compression markers etc.) are filtered and counted in the receipt;
tool_calls JSON carries across; deterministic hermes-{session} chat
ids make re-import a no-op
- dry-run now shows real conversation/message counts from the same
reader the apply uses; receipt plumbed through overrides.go, make gen,
the Settings page, and the en locale
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
Fifth slice of the migration importer — OpenClaw installs now import
end-to-end, and the apply layer is restructured into the one funnel both
walkers feed:
- apply.rs: shared importers (import_mcp_block, import_skill_dirs,
import_agent, import_memory_files, import_conversations,
import_provider_env) with normalized SourceAgent / SourceConversation
types; apply_hermes and apply_openclaw are thin per-source assemblies,
and apply() dispatches on detect and finalizes EVERY created employee
- openclaw.rs: JSON5 config (json5 crate — comments, trailing commas,
unquoted keys), mcp.servers dialect normalization (streamable-http →
http, auth oauth / Authorization-header → api_key, disabled servers
skipped with a note), multi-agent enumeration (default workspace agent
+ agents.entries[] with ~/relative/absolute workspace resolution),
SOUL.md → soul + AGENTS.md → rules (dedicated agent columns),
per-agent memory (MEMORY.md / USER.md / memory/*.md daily notes),
and sessions/*.jsonl transcripts parsed tolerantly ({role,content}
top-level or under message, Anthropic content blocks flattened,
non-message lines counted and skipped)
- provider keys import from BOTH .env and the config env block
- import/parse.rs: shared memory splitting / titles / epoch coercion so
identical content imports identically from either system
- detect endpoint now reports OpenClaw importable; scan dispatches to
the OpenClaw walker (Settings page needs no changes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
Final surface of the migration importer: onboarding offers the one-tap
"we found your setup" moment, built so it can never slow onboarding down.
Speed:
- detection stays stat-calls only; onboarding fires the probe at mount
and races it against an 800ms timeout — no result in time simply means
no card (the import remains available in Settings → Import)
- Hermes scan counts history via SQL COUNT instead of reading every
message row; OpenClaw scan counts session logs by file and line
instead of JSON-parsing transcripts (full parsing stays apply-side)
Card (Done step, shown only when an importable install was detected):
found → "Bring it over" runs the dry-run scan and shows the summary
("2 employees · 27 skills · 5 MCP servers · 3 memory files · 2 run
locally") → "Import everything" applies → success line. "Not now"
dismisses; failures point to Settings → Import. Skippable, invisible
when nothing is detected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
Deep review — full diff read, claims verified against codeThe good — and it's substantial: one shared funnel with per-source walkers; reuses the canonical pathways end-to-end ( Findings, ranked1. Manifest/apply contract drift (blocking — violates the module's own invariant).
Fix direction: tier the manifest rows honestly ( 2. OpenClaw cross-agent chat-id collision (real data-loss bug). 3. Timestamp coercion misses ms and ISO. 4. Attach-by-name to pre-existing agents. When 5. No per-conversation transaction. A failure mid- 6. Tunnel exposure (ties to the known denylist-drift issue). The import routes merge into the same no-auth local block as the rest of the API. Nits: handlers take Verdict: architecture is right, pathways are canonical, tests are real. #1 and #2 should land before merge; #3–#5 are small and worth doing in the same pass. (Merge hold remains until Alma lifts it.) |
Addresses all six findings from the PR #42 review: 1. Dry-run over-promise (blocking): the manifest now lists ONLY what apply performs. Credential items are limited to known LLM provider keys with honest wording ("→ LLM provider profile" — auth_profiles stores keys as-given, so no more "encrypted store" claim); other .env keys, auth.json OAuth records, and cron jobs become manifest NOTES naming their future slice, and apply now emits a matching skipped receipt for auth.json instead of dropping it silently. provider_for_env_key is shared by scan and apply so they can't drift. 2. Chat-id collision (blocking, data loss): chat ids now carry the source agent's slug (openclaw-main-s1 vs openclaw-scout-s1), so two agents' same-named session files can never collide. The OpenClaw fixture deliberately gives both agents an s1.jsonl and asserts both conversations survive. 3. Timestamps: normalize_epoch maps ms/µs/ns magnitudes down to seconds (OpenClaw is Node — Date.now() is ms) and ISO-8601 strings parse via chrono; applied in both the SQLite reader and the JSONL reader. 4. Attach-by-name: follow-up memory/history now attach only to agent dirs carrying the importer's own "importedFrom" manifest marker, verified against the DB row id — a pre-existing user-created agent sharing the name inherits nothing (regression test included). 5. Partial-chat poisoning: each conversation is all-or-nothing — on a message insert failure the stub chat is deleted (messages follow via FK cascade), the failure lands in the receipt, and the rest of the import continues; re-runs can retry (regression test included). 6. Tunnel denylist: /api/v1/import/ joins is_blocked_path — the import endpoints read arbitrary local directories and copy credentials, an owner-machine operation never legitimate through the tunnel. Also repairs the pre-existing ConnectPayload test drift (missing hostname/platform) that kept nebo-comm's test suite from compiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFTXd9CWAcd9rHSZN4iPJu
|
All six review findings addressed in 1. Dry-run over-promise (blocking) — fixed. The manifest now lists only what apply performs. Credential items are limited to the known LLM provider keys with honest wording ( 2. Chat-id collision (blocking) — fixed. Chat ids carry the source agent's slug ( 3. Timestamps — fixed. 4. Attach-by-name — fixed. Follow-up memory/history attach only to agent dirs carrying the importer's own 5. Partial-chat poisoning — fixed. Each conversation is all-or-nothing: on a message-insert failure the stub chat is deleted (messages follow via FK cascade), the failure lands in the receipt, and the rest of the import continues. Regression test poisons a message id and asserts no stub remains and the other conversation still imports. 6. Tunnel denylist — done. 74/74 server lib tests and the full Generated by Claude Code |
Adds a Hermes import surface: REST endpoints + Settings → Import page that migrate MCP integrations, skills, the employee persona, and provider keys from a Hermes install into Nebo, with a generated API client (genapi overrides included).
Security-relevant: imports provider credentials — review focus on key handling, storage pathway (keyring/encryption), and endpoint auth.
🤖 Generated with Claude Code
https://claude.ai/code/session_015oqdbhhgyktgKu9iqKb3V2