mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Implements the hermes-agent HSP/1 sync CLIENT against the frozen wire
contract (~/src/specs/collective-wisdom/hsp-1-contract.md §8), tested
against an in-process mock HSP server.
tools/skills_sync_client.py (new, low-level; does NOT import the CLI):
* Full 64-hex sha256 content addressing + canonical JSON (§2.1/§2.5,
OI-5) — kept distinct from the truncated local content_hash namespace.
* HSPClient: capabilities/refs/objects GET, batch object upload
(multipart, raw bytes per §1/§4.2), CAS ref (§4.4) with 409->HSPConflict.
* Object building: skill dir -> blob/tree/commit; exec-bit preserved,
symlinks skipped, oversize (413) surfaced; profile-root category trees.
* push/pull + three-way merge (M1-C): reuses the origin/user/incoming
decision semantics of skills_sync.py; non-overlap -> merge commit +
retry CAS; true overlap -> refs/user/<owner>/conflict/<n> + surface.
* DEV-PHASE gate: sync is INERT unless the resolved Nous token carries
tool_gateway_admin===true (decoded from the bearer; server re-verifies).
* Auth reuses resolve_nous_runtime_credentials() (no refresh reimpl).
* maybe_push_skills / maybe_pull_skills gate-and-swallow entrypoints.
Opt-in (M1-D): tools/skill_usage.set_sync / is_sync_enabled — a `sync`
flag on the .usage.json sidecar; nothing syncs unless opted in. Only
agent-created/user-authored skills are eligible (bundled/hub excluded).
Hooks:
* Debounced push in skill_manage success block (after the write gate).
* Periodic pull at the two curator tick sites (gateway housekeeping loop
+ CLI startup).
CLI: hermes sync status|pull|push|now|enable|disable
(hermes_cli/subcommands/sync.py + cmd_sync in main.py).
Tests: tests/tools/test_skills_sync_client.py — 29 tests (addressing,
canonicalization, dev gate, opt-in, object building, merge decisions, and
e2e push/pull/idempotency/conflict against a stdlib mock HSP server).
44 lines
1.9 KiB
Python
44 lines
1.9 KiB
Python
"""``hermes sync`` subcommand parser (HSP/1 personal skill sync).
|
|
|
|
Cloned from ``hermes_cli/subcommands/cron.py`` — same injected-handler shape
|
|
(``func=cmd_sync``) so this module does not import ``main`` (cycle avoidance).
|
|
|
|
Commands:
|
|
hermes sync status -- show gate/opt-in/head state
|
|
hermes sync pull -- pull the owner's HEAD, materialize opted-in skills
|
|
hermes sync push -- push opted-in skills to the owner's HEAD
|
|
hermes sync now -- pull then push (full reconcile)
|
|
hermes sync enable <skill> -- opt a skill into sync (M1-D)
|
|
hermes sync disable <skill> -- opt a skill out of sync
|
|
|
|
Sync is INERT unless the resolved Nous token carries the DEV-PHASE gate claim
|
|
(tool_gateway_admin) AND a sync base URL is configured. The commands report
|
|
that state rather than failing opaquely.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
|
|
|
|
def build_sync_parser(subparsers, *, cmd_sync: Callable) -> None:
|
|
"""Attach the ``sync`` subcommand (and its sub-actions) to ``subparsers``."""
|
|
sync_parser = subparsers.add_parser(
|
|
"sync",
|
|
help="Personal skill sync (HSP/1)",
|
|
description="Sync agent-created and user-authored skills across devices.",
|
|
)
|
|
sync_sub = sync_parser.add_subparsers(dest="sync_command")
|
|
|
|
sync_sub.add_parser("status", help="Show sync gate, opt-in, and head state")
|
|
sync_sub.add_parser("pull", help="Pull the owner's HEAD and materialize opted-in skills")
|
|
sync_sub.add_parser("push", help="Push opted-in skills to the owner's HEAD")
|
|
sync_sub.add_parser("now", help="Reconcile now: pull then push")
|
|
|
|
enable = sync_sub.add_parser("enable", help="Opt a skill into sync")
|
|
enable.add_argument("skill", help="Skill name (frontmatter name / directory name)")
|
|
|
|
disable = sync_sub.add_parser("disable", help="Opt a skill out of sync")
|
|
disable.add_argument("skill", help="Skill name (frontmatter name / directory name)")
|
|
|
|
sync_parser.set_defaults(func=cmd_sync)
|