mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-09 03:11:58 +00:00
* feat(profile): shareable profile distributions (pack/install/update/info) Closes #20456. Turns a profile into a portable, versioned artifact. Packs SOUL.md, config, skills, cron, and an env-var manifest into a tar.gz that others can install from a local path, URL, or git repo. Updates re-pull the distribution while preserving user data (memories, sessions, auth.json, .env) and the user's config.yaml overrides. New subcommands (under hermes profile, no parallel tree): hermes profile pack <name> [-o FILE] hermes profile install <source> [--name N] [--alias] [--force] [-y] hermes profile update <name> [--force-config] [-y] hermes profile info <name> Manifest (distribution.yaml at the profile root): name, version, hermes_requires, author, env_requires, distribution_owned. Security: - Installer shows manifest + env-var requirements before mutating disk; confirmation required unless -y. - auth.json and .env are never packed (same exclude set as profile export). - Cron jobs are packed but NOT auto-scheduled — user is pointed at 'hermes -p <name> cron list' to review. - Archive extraction rejects path traversal (../ members). - Alias creation is opt-in via --alias. Update semantics: - Distribution-owned paths (SOUL.md, skills/, cron/, mcp.json, manifest): replaced from the new archive. - config.yaml: preserved by default; --force-config to overwrite. - User-owned paths (memories/, sessions/, auth.json, .env, state.db*, logs/, workspace/, plans/, home/, *_cache/, local/): never touched. Version pin: hermes_requires accepts >=, <=, ==, !=, >, < or a bare version (treated as >=). Install fails with a clear error when the running Hermes version doesn't satisfy the spec. Sources supported by 'install': - Local .tar.gz / .tgz archive - Local directory - HTTP(S) URL pointing to a .tar.gz (uses httpx, already a dep) - Git URL (github.com/user/repo, https://..., git@..., ssh://, git://) Tests: 43 new unit tests (manifest parsing, version checks, env template, pack/install/update round-trip, config-preservation, security). E2E validated via real CLI invocations against an isolated HERMES_HOME covering pack, install with confirmation, update preservation, update --force-config, decline-preview, duplicate-install rejection, and version-requirement rejection. * refactor(profile-dist): git-only — drop tar.gz/HTTP transports and pack Scope-cut on top of the original distribution PR: a profile distribution is now exclusively a git repository (or a local directory during development). The tar.gz / HTTP archive transports and the matching `hermes profile pack` subcommand have been removed. Why: * GitHub tags, branches, and commits are already the right versioning primitive. Tag pushes do for us what 'pack + upload' did. * `hermes profile export` / `import` already cover local backup and restore; they are not a distribution format and stay untouched. * One transport means one install/update code path, one doc page, and one mental model. The extra source types doubled the surface for no real user win — GitHub auto-attaches release tarballs, and `git bundle` / `git clone --mirror` cover the airgap case. Changes: * hermes_cli/profile_distribution.py — removed pack_profile, _fetch_tar_archive (_http_fetch), _safe_extract, _archive_roots, _safe_parts, _find_dist_root, tarfile/io/urlparse imports. The new _stage_source has two arms: git URL → clone, local directory → use in place. * hermes_cli/main.py — removed the 'pack' subparser and action handler. Install help text updated to match the reduced source list. * tests/hermes_cli/test_profile_distribution.py — rewritten around a local-directory staging fixture. The install/update/describe suites now build a distribution tree on disk directly and install from it, which is what a real git clone produces after .git is stripped. Dropped TestPack, TestFindDistRoot, and the tar-specific security test. New tests cover _looks_like_git_url, env_example emission, hermes_requires enforcement, and 'installer does not import credentials if an author mistakenly leaks them in the staging tree'. * website/docs/reference/profile-commands.md — 'Distribution commands' section rewritten around git. Added a 'Publishing a distribution' section. export/import stay documented as local backup/restore. * website/docs/reference/cli-commands.md — dropped 'pack' from the profile subcommand table. * website/package.json — 'lint:diagrams' now passes --exclude-code-blocks to ascii-guard. Without it, markdown tables and box-drawing diagrams inside fenced code blocks were being misidentified as malformed ASCII boxes, blocking the PR's docs-site-checks CI with 8 false-positive errors. Validation: * Targeted suite: tests/hermes_cli/test_profile_distribution.py — 56/56 pass (down from 43 — reorganized to cover the new local-dir paths). * Regression: test_profiles.py + test_profile_export_credentials.py 102/102 still pass. export/import behaviour unchanged. * Docs lint: ascii-guard lint --exclude-code-blocks docs returns 0 errors (was 8 on the PR before the flag bump). * E2E: ran the real `hermes profile install`/`info` against a local staging dir under an isolated HERMES_HOME — install writes SOUL.md + skills to the target profile, info reads the manifest back, a bogus source produces a clear error, and `hermes profile pack` is now rejected by argparse as expected. * feat(profile-dist): distribution-aware list/show/delete + installed_at + env preview Polish pass on top of the git-only scope cut. Five additions, all small, wiring into existing commands rather than adding new surface. 1. `installed_at` timestamp on the manifest * Stamped automatically inside plan_install() on both fresh install and update — ISO-8601 UTC, seconds resolution. * Surfaced in `hermes profile info` as `Installed: <ts>`. * Lets users tell "installed 6 months ago, needs update" from "installed yesterday" without guessing from file mtimes. 2. `hermes profile list` grows a `Distribution` column * Plain profiles: "—" * Distribution profiles: "<name>@<version>" (e.g. `telemetry@1.2.3`) * ProfileInfo gains three optional fields — distribution_name, distribution_version, distribution_source — populated by a new _read_distribution_meta() helper that swallows manifest read errors so a broken distribution.yaml in one profile can't break `list` for the others. 3. `hermes profile show` and `hermes profile delete` surface distribution provenance * show: `Distribution: name@version` + `Installed from: <source>` plus a pointer to `hermes profile info <name>` for the full manifest. * delete: same lines in the pre-confirmation preview, so a user deleting "telemetry" can see it came from `github.com/kyle/telemetry-distribution` before they type `telemetry` to confirm. No change to the confirmation gate itself — deletion semantics are identical to plain profiles. 4. Install preview checks env vars against the current environment * Replaces the "Env vars you'll need to set:" header with a simpler "Env vars:" block. * Each required var is labeled: - `✓ set` — already in `os.environ` OR present as a key in the target profile's existing .env (update case). - `needs setting` — required but not found in either place. - `—` — optional. * Mirrors pip's "Requirement already satisfied" UX: no unnecessary nagging about keys the user already has configured. 5. Docs: private distributions * New "Private distributions" section in website/docs/reference/profile-commands.md explaining that we shell out to the user's `git` binary, so SSH keys / credential helpers / GitHub CLI stored creds all work transparently. One paragraph, two examples. * `hermes profile info` section updated to mention `Installed:`. Module-level hoist: * `from datetime import datetime, timezone` was previously lazy-imported inside plan_install(). Hoisted to module scope so tests can monkeypatch `hermes_cli.profile_distribution.datetime` to freeze time. Tests (+7): * TestInstalledAtStamp.test_install_stamps_installed_at — format check (4-digit year, 'T', +00:00 suffix). * TestInstalledAtStamp.test_update_refreshes_installed_at — freezes datetime.now() to 2099-01-01 and confirms update writes a new stamp. * TestProfileInfoDistribution.test_installed_distribution_shows_in_list — ProfileInfo.distribution_{name,version,source} populated after install. * TestProfileInfoDistribution.test_plain_profile_has_no_distribution_fields — plain profiles have None. * TestProfileInfoDistribution.test_malformed_manifest_does_not_break_list — broken distribution.yaml in one profile doesn't break list_profiles(). Validation: * 163/163 tests pass (56 distribution + 102 profile regression + 5 new from this commit — up from 158). * docs-lint: 0 errors. * E2E verified: install preview shows ✓/needs-setting per env var, `profile list` shows Distribution column, `profile show` + `delete` preview mentions source URL, `info` shows Installed: timestamp. * fix(profile-dist): clean errors + warn when overwriting plain profiles Two small polish fixes found during collision sweeps of the PR: 1. ValueError from validate_profile_name now caught cleanly * A distribution.yaml whose 'name' field can't be used as a profile identifier (spaces, path traversal, etc.) raises ValueError from hermes_cli.profiles.validate_profile_name, which was escaping as a raw Python traceback from 'hermes profile install/update/info'. * Broadened the except clause in all three handlers to catch (DistributionError, ValueError) — users now see: Error: Invalid profile name '../../etc/passwd'. Must match [a-z0-9][a-z0-9_-]{0,63} instead of a stack trace. 2. Install preview distinguishes plain profile overwrite from distribution re-install * When plan.target_dir exists and IS a distribution (has distribution.yaml), preview still shows the mild (profile exists — will overwrite distribution-owned files only) * When plan.target_dir exists but is a HAND-BUILT plain profile (no distribution.yaml), preview now shows a loud warning: ⚠ Profile exists but is NOT a distribution. Installing here will overwrite its SOUL.md, skills/, cron/, and mcp.json. Your memories, sessions, auth.json, and .env will be preserved, but any hand-edits to distribution-owned files will be lost. * Users who type 'hermes profile install foo --force' against a profile they hand-built now see what they're signing up for. User data is still safe (memories, sessions, auth, .env are in USER_OWNED_EXCLUDE), but custom SOUL/skills get stomped. Tests (+2): * TestErrorSurfaces.test_bad_profile_name_raises_valueerror_not_traceback * TestErrorSurfaces.test_path_traversal_name_rejected Validation: * 165/165 tests pass (was 163). * E2E: bad manifest names produce 'Error: Invalid profile name ...' with no traceback; installing over a plain profile shows the warning; re-installing over an existing distribution shows the normal overwrite message. * Bad HTTPS URLs still produce 'Error: git clone failed: ...' — git itself generates a clean enough message that no wrapper is needed. * 'install .' works correctly from any cwd. * fix(profiles): reject reserved names at validate time Before: `hermes profile create hermes` / `profile install` / `profile rename` all silently accepted reserved names like `hermes`, `test`, `tmp`, `root`, `sudo`. The profile directory was created; only alias creation failed (via check_alias_collision), leaving a confusingly-named profile on disk — e.g. `~/.hermes/profiles/hermes/` sitting next to `~/.hermes/` itself. The reserved set already exists (_RESERVED_NAMES, introduced alongside alias collision detection). This commit moves the check up one layer to validate_profile_name so every entry point — create, install, import, rename, dashboard web API — shares the same gate. The error message points the user at the cause without being cryptic: Error: Profile name 'hermes' is reserved — it collides with either the Hermes installation itself or a common system binary. Pick a different name. `default` continues to pass through (it's a special alias for ~/.hermes). _HERMES_SUBCOMMANDS (`chat`, `model`, `gateway`, etc.) stays at alias-collision time only — those are fine as bare profile names with `--no-alias`. Tests (+5): test_reserved_names_rejected parametrized over the full _RESERVED_NAMES set, matching the existing pattern in TestValidateProfileName. No existing test uses a reserved name as a profile identifier (greppped create_profile("hermes|test|tmp|root|sudo") — zero hits). Validation: * 170/170 tests pass in the profile suites. * E2E: `profile create hermes`, `profile install` with manifest name=hermes, and `profile install ... --name hermes` all produce the same clean `Error: Profile name 'hermes' is reserved ...` with rc=1 and no traceback. Normal names (`mybot`) still work. |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| conftest.py | ||
| test_ai_gateway_models.py | ||
| test_anthropic_model_flow_stale_oauth.py | ||
| test_anthropic_oauth_flow.py | ||
| test_anthropic_provider_persistence.py | ||
| test_api_key_providers.py | ||
| test_apply_model_switch_result_context.py | ||
| test_arcee_provider.py | ||
| test_argparse_flag_propagation.py | ||
| test_at_context_completion_filter.py | ||
| test_atomic_json_write.py | ||
| test_atomic_yaml_write.py | ||
| test_auth_codex_provider.py | ||
| test_auth_commands.py | ||
| test_auth_nous_provider.py | ||
| test_auth_profile_fallback.py | ||
| test_auth_provider_gate.py | ||
| test_auth_qwen_provider.py | ||
| test_auth_ssl_macos.py | ||
| test_auth_toctou_file_modes.py | ||
| test_aux_config.py | ||
| test_azure_detect.py | ||
| test_backup.py | ||
| test_banner.py | ||
| test_banner_git_state.py | ||
| test_banner_skills.py | ||
| test_bedrock_model_picker.py | ||
| test_chat_skills_flag.py | ||
| test_claw.py | ||
| test_clear_stale_base_url.py | ||
| test_cmd_update.py | ||
| test_coalesce_session_args.py | ||
| test_codex_cli_model_picker.py | ||
| test_codex_models.py | ||
| test_commands.py | ||
| test_completion.py | ||
| test_config.py | ||
| test_config_drift.py | ||
| test_config_env_expansion.py | ||
| test_config_env_refs.py | ||
| test_config_validation.py | ||
| test_container_aware_cli.py | ||
| test_copilot_auth.py | ||
| test_copilot_catalog_oauth_fallback.py | ||
| test_copilot_context.py | ||
| test_copilot_in_model_list.py | ||
| test_copilot_token_exchange.py | ||
| test_cron.py | ||
| test_curator_archive_prune.py | ||
| test_curator_run.py | ||
| test_curator_status.py | ||
| test_custom_provider_context_length.py | ||
| test_custom_provider_model_switch.py | ||
| test_dashboard_browser_safe_imports.py | ||
| test_dashboard_lifecycle_flags.py | ||
| test_dashboard_profiles_nav_label.py | ||
| test_debug.py | ||
| test_deprecated_cwd_warning.py | ||
| test_detect_api_mode_for_url.py | ||
| test_determine_api_mode_hostname.py | ||
| test_dingtalk_auth.py | ||
| test_discord_skill_clamp_warning.py | ||
| test_doctor.py | ||
| test_doctor_command_install.py | ||
| test_env_loader.py | ||
| test_env_sanitize_on_load.py | ||
| test_fallback_cmd.py | ||
| test_gateway.py | ||
| test_gateway_linger.py | ||
| test_gateway_runtime_health.py | ||
| test_gateway_service.py | ||
| test_gateway_wsl.py | ||
| test_gemini_free_tier_setup_block.py | ||
| test_gemini_provider.py | ||
| test_gmi_provider.py | ||
| test_goals.py | ||
| test_hooks_cli.py | ||
| test_ignore_user_config_flags.py | ||
| test_image_gen_picker.py | ||
| test_kanban_boards.py | ||
| test_kanban_cli.py | ||
| test_kanban_core_functionality.py | ||
| test_kanban_db.py | ||
| test_kanban_diagnostics.py | ||
| test_kanban_specify.py | ||
| test_kanban_specify_db.py | ||
| test_launcher.py | ||
| test_list_picker_providers.py | ||
| test_logs.py | ||
| test_managed_installs.py | ||
| test_mcp_add_command_dest.py | ||
| test_mcp_config.py | ||
| test_mcp_reload_confirm_gate.py | ||
| test_mcp_tools_config.py | ||
| test_memory_reset.py | ||
| test_model_catalog.py | ||
| test_model_normalize.py | ||
| test_model_picker_viewport.py | ||
| test_model_provider_persistence.py | ||
| test_model_switch_context_display.py | ||
| test_model_switch_copilot_api_mode.py | ||
| test_model_switch_custom_providers.py | ||
| test_model_switch_opencode_anthropic.py | ||
| test_model_switch_variant_tags.py | ||
| test_model_validation.py | ||
| test_models.py | ||
| test_models_dev_preferred_merge.py | ||
| test_non_ascii_credential.py | ||
| test_nous_hermes_non_agentic.py | ||
| test_nous_subscription.py | ||
| test_ollama_cloud_auth.py | ||
| test_ollama_cloud_provider.py | ||
| test_openai_codex_model_validation_fallback.py | ||
| test_opencode_go_flat_namespace.py | ||
| test_opencode_go_in_model_list.py | ||
| test_opencode_go_validation_fallback.py | ||
| test_overlay_slug_resolution.py | ||
| test_path_completion.py | ||
| test_pin_kanban_board_env.py | ||
| test_placeholder_usage.py | ||
| test_plugin_cli_registration.py | ||
| test_plugin_scanner_recursion.py | ||
| test_plugins.py | ||
| test_plugins_cmd.py | ||
| test_profile_distribution.py | ||
| test_profile_export_credentials.py | ||
| test_profiles.py | ||
| test_prompt_api_key.py | ||
| test_provider_config_validation.py | ||
| test_pty_bridge.py | ||
| test_reasoning_effort_menu.py | ||
| test_redact_config_bridge.py | ||
| test_regression_16767.py | ||
| test_relaunch.py | ||
| test_resolve_last_session.py | ||
| test_runtime_provider_resolution.py | ||
| test_session_browse.py | ||
| test_sessions_delete.py | ||
| test_set_config_value.py | ||
| test_setup.py | ||
| test_setup_agent_settings.py | ||
| test_setup_hermes_script.py | ||
| test_setup_irc.py | ||
| test_setup_matrix_e2ee.py | ||
| test_setup_model_provider.py | ||
| test_setup_noninteractive.py | ||
| test_setup_ollama_cloud_force_refresh.py | ||
| test_setup_openclaw_migration.py | ||
| test_setup_prompt_menus.py | ||
| test_setup_reconfigure.py | ||
| test_skills_config.py | ||
| test_skills_hub.py | ||
| test_skills_install_flags.py | ||
| test_skills_skip_confirm.py | ||
| test_skills_subparser.py | ||
| test_skin_engine.py | ||
| test_spotify_auth.py | ||
| test_status.py | ||
| test_status_model_provider.py | ||
| test_subparser_routing_fallback.py | ||
| test_subprocess_timeouts.py | ||
| test_suppress_eio_on_interrupt.py | ||
| test_tencent_tokenhub_provider.py | ||
| test_terminal_menu_fallbacks.py | ||
| test_timeouts.py | ||
| test_tips.py | ||
| test_tool_token_estimation.py | ||
| test_tools_config.py | ||
| test_tools_disable_enable.py | ||
| test_tui_npm_install.py | ||
| test_tui_resume_flow.py | ||
| test_update_autostash.py | ||
| test_update_check.py | ||
| test_update_config_clears_custom_fields.py | ||
| test_update_gateway_restart.py | ||
| test_update_hangup_protection.py | ||
| test_update_stale_dashboard.py | ||
| test_update_yes_flag.py | ||
| test_user_providers_model_switch.py | ||
| test_voice_wrapper.py | ||
| test_web_server.py | ||
| test_web_server_host_header.py | ||
| test_web_ui_build.py | ||
| test_webhook_cli.py | ||
| test_xiaomi_provider.py | ||