feat(pmoves-bootstrap): CGP consumer + tools_bridge + subscriber stub for Hermes - #4
Conversation
CLAIM the Hermes-agent fork's slice of the Mavis harness v0 (3-repo coordinated). Companion to POWERFULMOVES/PMOVES.AI PR NousResearch#2477 and POWERFULMOVES/PMOVES-pinokio PR #1 (feat/pmoves-app-launcher). The CGP (pmoves.bootstrap/v1) is the contract that ties the 3 forks together: PMOVES.AI writes it, PMOVES-hermes-agent reads it at session init + registers PMOVES tools alongside the native toolset, PMOVES-pinokio reads it when launching a PMOVES-tagged app. What this commit ships: - pmoves_bootstrap/loader.py - the CGP reader. Accepts both YAML and JSON (Hermes has pyyaml==6.0.3 in core deps, so YAML is the natural format; JSON is supported for PMOVES_BOOTSTRAP_CGP raw- string env vars). 4 input sources in priority order: path arg, source arg, PMOVES_BOOTSTRAP_CGP[_PATH] env var, vendored example. Validates structurally (the vendored v1.schema.json is the source of truth, but no jsonschema dep is added - the thin structural check covers the 80% case). Returns a typed Bootstrap object with has_tool/has_mcp/has_constraint/service/route_for accessors. Stub Bootstrap (safe defaults, all 6 constraints) when no CGP is present - the non-breaking fallback. - pmoves_bootstrap/tools_bridge.py - the PMOVES tools bridge. Reads bootstrap.tools and resolves each entry against the v0 tool registry (Python scripts + CLI binaries). Returns a BridgeResult with registered/skipped/disabled lists. The session init code (a future slice) merges registered tools into Hermes's active toolset. PMOVES_TOOLS_DISABLE env var is a per-tool deny list. Per the 'tagged-services-are-advisory' constraint, unknown tools are silently skipped (warning, not error). - pmoves_bootstrap/subscriber.py - the optional NATS subscriber. v0 is a STUB: no nats-py in Hermes's core deps (adding it would be a meaningful blast-radius change; the deps list warns against it after the Mini Shai-Hulud worm). subscribe() always returns a SubscriberStatus with enabled=False and a clear reason. The TaskEnvelope/ResultEnvelope dataclasses document the wire contract so a future slice can wire in nats-py without changing the public surface. Subjects: pmoves.agent.task.v1 (input), pmoves.agent.result.v1 (output), pmoves.bpm.phase.v1 + pmoves.bpm.pomodoro.v1 (observability, not consumed by Hermes). - pmoves_bootstrap/__init__.py - the public surface. Re-exports load_bootstrap, stub_bootstrap, export_env, register_pmoves_tools, subscribe, and the typed shapes. Future Mavis / Spark / Knuckles sessions do 'from pmoves_bootstrap import load_bootstrap, register_pmoves_tools, subscribe'. - pmoves_bootstrap/cgp_schema/v1.schema.json - vendored copy of the PMOVES.AI schema. The hermes-agent fork doesn't depend on the PMOVES.AI repo at install time. - pmoves_bootstrap/cgp_schema/example.cgp.yaml - vendored YAML example (the same data as the PMOVES.AI example.cgp.yaml). Non-breaking test pair: - No CGP present -> load_bootstrap() returns the stub Bootstrap, register_pmoves_tools() returns BridgeResult(registered=[]), subscribe() returns SubscriberStatus(enabled=False). Existing Hermes behavior unchanged. - CGP present -> load_bootstrap() validates and returns the real Bootstrap, register_pmoves_tools() adds PMOVES tools alongside the native Hermes toolset, subscribe() is a no-op (v0) or picks up Mavis-orchestrator tasks (future slice). Cross-fork plan: - PMOVES.AI PR NousResearch#2477 (writer) - PMOVES-hermes-agent PR feat/pmoves-bootstrap-consumer (agent, this PR) - PMOVES-pinokio PR feat/pmoves-app-launcher (app launcher) All three read the same v1.schema.json - the schema is the contract. The 6 constraints baked into the CGP are honored by the loader's behavior: - no-override-existing-config: the loader never writes to Hermes's own config (cli-config.yaml, hermes_state, etc.) - tagged-services-are-advisory: missing services are skipped in tools_bridge, not failed - no-chit-bypass: no CHIT signing code in this package; the Mavis orchestrator does the signing - no-force-push: this PR's commits use rebase, never --force - no-ci-bypass: PR is in DRAFT, no --admin to skip CI - preserve-existing-tools: tools_bridge adds PMOVES tools alongside Hermes's native toolset, never in place of Tests: 33/33 pass (tests/test_pmoves_bootstrap.py, run with 'python -m pytest tests/test_pmoves_bootstrap.py -o addopts='). No new core dependencies. pyyaml is already a Hermes core dep (see pyproject.toml); no new packages are added.
The test suite for the hermes-side CGP consumer. Mirrors the
PMOVES.AI side test taxonomy (load_from_example / load_from_source
/ validation_failure / stub_fallback / export_env / typed_accessor
+ tools_bridge + subscriber) but with 33 tests total (vs 22 on the
PMOVES.AI side) because the hermes-side has more surface (YAML +
JSON parsing, env-var handling, tools_bridge registry resolution,
subscriber wire contract).
Test groups:
- A. LoadFromExampleTests (5) - the vendored example loads +
validates, identity, services, routing, constraints
- B. LoadFromSourceTests (4) - raw YAML, raw JSON, PMOVES_BOOTSTRAP_CGP
env var, PMOVES_BOOTSTRAP_CGP_PATH env var
- C. ValidationFailureTests (5) - wrong spec, missing top-level
field, missing identity.agent, bad role, non-empty super_nodes
- D. StubFallbackTests (2) - no CGP returns the stub; stub has
all 6 constraints
- E. ExportEnvTests (3) - identity vars, services + routing vars,
custom env dict (no process side-effect)
- F. TypedAccessorTests (3) - has_tool/has_mcp/has_constraint,
service() returns None for missing, route_for() returns None
for missing
- G. ToolsBridgeTests (6) - stub returns empty, real CGP registers
known tools, disable list excludes, unknown goes to skipped,
callables are invokable, registry populated at import
- H. SubscriberTests (3) - subscribe is safe no-op when disabled,
TaskEnvelope round-trip, ResultEnvelope round-trip
- I. Constants and subject surfaces (2) - subjects match the
orchestrator, KNOWN_TARGETS contains the expected agents
Run with:
python -m pytest tests/test_pmoves_bootstrap.py -v -o addopts=
(the -o addopts= is needed on Windows where the project-level
pytest-timeout addopts expects SIGALRM which doesn't exist on
Windows; the override disables the addopts so pytest-timeout
isn't required for these tests).
The autouse _isolate_env fixture strips PMOVES_BOOTSTRAP_*,
PMOVES_SUBSCRIBER_*, and PMOVES_TOOLS_* env vars before every
test, so the tests are order-independent and don't leak state
across test files.
The high-level map of the pmoves_bootstrap package + the 3 files (loader, tools_bridge, subscriber) + the non-breaking contract + the design choices (YAML+JSON, no jsonschema, nats-py as follow-up) + the cross-fork plan. Future Mavis / Spark / Knuckles sessions hit this file first to understand the integration. What's in the README: - Why this exists - the 3-repo harness v0 slice, hermes-side role as the heaviest of the three (read CGP, register tools, optional subscriber) - What this slice ships - 8 files (4 .py + 2 vendored schema files + 1 test file + 1 README) - Non-breaking contract - the 6 constraints, the no-CGP fallback, the explicit-source error behavior - Public API - the 5 public functions + 4 typed shapes - Resolution order - the 4 sources in priority order - Why YAML (in addition to JSON) - Hermes has pyyaml in core deps - Why no nats-py in v0 - the blast-radius comment in pyproject.toml warns against adding new packages; the v0 subscriber is a stub with a stable wire contract documented via dataclasses - Tests - 33/33 pass with pytest, 9 test groups - What this slice does NOT do - the 4 intentional follow-ups (wiring into run_agent.py, real nats-py, CHIT trail signing, per-session tool allow-list) - Cross-fork plan - the 3 PRs and the schema as the contract
|
Warning Review limit reached
Next review available in: 55 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-assignment |
3 |
unresolved-import |
2 |
not-subscriptable |
1 |
invalid-return-type |
1 |
First entries
pmoves_bootstrap/subscriber.py:140: [unresolved-import] unresolved-import: Cannot resolve imported module `nats`
tests/test_pmoves_bootstrap.py:123: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
tests/test_pmoves_bootstrap.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
pmoves_bootstrap/loader.py:377: [invalid-assignment] invalid-assignment: Object of type `_Environ[str]` is not assignable to `dict[Unknown, Unknown] | None`
pmoves_bootstrap/loader.py:412: [invalid-assignment] invalid-assignment: Cannot assign to a subscript on an object of type `None`
pmoves_bootstrap/loader.py:413: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `dict[Unknown, Unknown]`, found `dict[Unknown, Unknown] | None`
pmoves_bootstrap/loader.py:36: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `<module 'yaml'>`
✅ Fixed issues: none
Unchanged: 5453 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
POWERFULMOVES
left a comment
There was a problem hiding this comment.
Pair-review pass from Mavis-verifier
This is a disciplined v0: a non-breaking CGP consumer with a stub NATS subscriber, hard-coded tool registry, and vendored copy of the canonical schema. The non-breaking test pair is real (loader → stub when no CGP, BridgeResult(registered=[]) and SubscriberStatus(enabled=False)), the registry uses lazy closures so the package imports cleanly even when pmoves.tools.* isn't on PYTHONPATH, and the diff is 8/8 added files — no existing Hermes file (cli.py, run_agent.py, toolsets.py, pyproject.toml, or the unrelated hermes_bootstrap.py Windows UTF-8 helper) was touched. I ran pytest -v -o addopts= against the PR's test file with pyyaml==6.0.3 + pytest==9.1.1 on Windows Python 3.12 and got 33 passed in 0.47s — the count is real. The vendored JSON Schema is byte-identical to the canonical at pmoves/contracts/schemas/pmoves-bootstrap/v1.schema.json (PR NousResearch#2477 head): SHA-256 427611C4E57AB133F022CE45BC3A0E0A30BF2645188CDF0485A658DBDCD14BD3 after CRLF normalization. Required keys, all 6 constraint enum values, all 6 role enum values, all 5 meta.source enum values, and super_nodes.maxItems: 0 all match. I verified Hermes's hermes_bootstrap.py is indeed the Windows UTF-8 helper (first line: "Windows UTF-8 bootstrap for Hermes entry points") — the package name pmoves_bootstrap is clearly distinguished.
5 observations surfaced for follow-up commit (non-blocking):
1. Loader's structural check is a strict subset of the vendored schema — the docstring claim "validated against the vendored JSON Schema" is misleading
WHY it matters: loader.py:195-260 (_validate_cgp) does its own hand-rolled check, not a JSON Schema validation. The README and the module docstring say the CGP is "validated against the vendored JSON Schema" (loader.py:5, README L30, README L69) but the vendored v1.schema.json is only read by tests, never by _validate_cgp. Adversarial probe: a CGP with tools: ["gh", {"inject": "evil"}, 42] is accepted by the loader (it only checks isinstance(tools, list)), then register_pmoves_tools crashes with TypeError: unhashable type: 'dict' at if tool_id in disable:. A CGP with constraints: ["make-coffee"] is silently accepted (probe 10), even though the schema's enum says it must be one of the 6 canonical values. The README acknowledges this as "the thin structural check covers the 80% case with zero new deps" (loader.py:200-210), but the docstring claim that the CGP is "validated against the vendored JSON Schema" sets the wrong expectation. Suggested fix: soften the docstring claim to "validated against a hand-rolled structural subset of the vendored schema", OR add a defense-in-depth path: try: import jsonschema; validate against SCHEMA_PATH (falling back to the hand-rolled check on ImportError).
2. Vendored example.cgp.yaml is NOT byte-identical to the canonical — it's a hand-edited "canary" with comment drift
WHY it matters: the vendored YAML parses to the same dict as the canonical (5 sources agree: 2 in tests, plus my diff) but the bytes differ — the vendored copy strips the "# from PR NousResearch#2450" annotations, the commented sig: block, and several "Mavis surface" → "PMOVES surface" doc rewrites. The vendored file's own header (lines 1-5) says it's a "canary fixture" and "if you change the schema, change the example to match and re-run the schema-sync test" — but no schema-sync test exists in the test file. As the canonical YAML evolves (new tool, new MCP, new routing entry), the vendored copy will silently drift. Suggested fix: add a test like test_schema_sync that fetches https://raw.githubusercontent.com/POWERFULMOVES/PMOVES.AI/feat/mavis-harness-v0/pmoves/contracts/schemas/pmoves-bootstrap/example.cgp.yaml and asserts the parsed CGP is dict-equal (not byte-equal, since comments differ). Or document explicitly in the README that the vendored example is independently maintained and add a one-line make target / CI step that diffs the two.
3. The schema file is vendored with CRLF line endings (Windows checkout artifact) — should be normalized to LF for clean SHA-256 / git diff
WHY it matters: the vendored schema is 10234 bytes (CRLF) vs the canonical 10028 bytes (LF). After stripping \r\n, SHA-256s match and content is identical. But every future diff against the canonical will show 206 line-ending churn if the repo's .gitattributes isn't set to force LF for *.json. Suggested fix: add pmoves_bootstrap/cgp_schema/*.json eol=lf to .gitattributes (if it exists in this fork), or normalize the vendored file with git config core.autocrlf false + git rm --cached + re-add. The current state is harmless but contributes to future merge noise. (Nit, but cheap to fix.)
4. register_pmoves_tools doesn't defend against non-string tool_id from a malformed CGP
WHY it matters: the loader accepts the CGP (structural check passes — tools is a list), but the bridge does if tool_id in disable: which raises TypeError: unhashable type: 'dict' when a tool_id is a dict. Adversarial probe 11: cgp["tools"] = ["gh", {"inject": "evil"}, 42] → register_pmoves_tools raises and the caller's session init aborts instead of a clean "skipped" entry. The fix is two lines: if not isinstance(tool_id, str): result.skipped.append(repr(tool_id)); continue at the top of the loop in tools_bridge.py:194. Same for mcps if/when the bridge iterates them.
5. Bootstrap.source (load-source) and Bootstrap.meta.source (producer) share a name; the README's "Resolution order" doc only explains one of them
WHY it matters: bs.source is the loader-side attribute — "path:...", "env:PMOVES_BOOTSTRAP_CGP", "raw", or "stub:no-cgp". bs.meta.source is the CGP's producer — one of mavis|hermes|pinokio|operator|test. These are different concepts with the same name; the type annotation doesn't distinguish them. Test test_A2 asserts bs.source.startswith("path:") and test_B3 asserts bs.source == "env:PMOVES_BOOTSTRAP_CGP", but a future test author might write assert bs.source == "mavis" and mean the meta field. Suggested fix: rename the dataclass attribute to Bootstrap.load_source (or Bootstrap.origin); keep Bootstrap.meta.source as is. The diff is small but the semantic clarity matters for the cross-fork handoff where the orchestrator and the consumer both see the same dataclass.
6. Three dead imports in loader.py:29-33 and one stale docstring count
WHY it matters: import re (loader.py:29), import sys (loader.py:30), and from typing import ... Iterable ... (loader.py:33) are imported but never referenced in code — re only appears in a docstring (re-parsing, line 371); Iterable only appears in the import line. Also: tests/test_pmoves_bootstrap.py:15 says "Total: 31 tests, organized into 8 groups" but the file actually contains 33 tests in 9 groups (A1-A5, B1-B4, C1-C5, D1-D2, E1-E3, F1-F3, G1-G6, H1-H3, I1-I2 = 33). The README has the same drift: line 46 says "31 tests, 9 test groups" but line 148 correctly says "33 tests, 9 test groups". Suggested fix: remove the dead imports; update test_pmoves_bootstrap.py:15 to "33 tests, 9 groups"; update README L46 to "33 tests, 9 test groups".
Nit (skip if scope creep)
stub_bootstrap() returns a services dict with tailscale: None and hostinger.site: None (loader.py:307-312). The export_env function (loader.py:387-407) defensively checks if ts.get("host"): etc., so the None values are skipped at export. But bs.service("tailscale") returns None (not {}), so a consumer that does if bs.service("tailscale"): will hit the truthy-None gotcha only if a future refactor changes the shape. The defensive export_env is good. Worth a one-line comment in the stub: # tailscale=None (not {}) so export_env's truthy check works as-is.
Disposition
APPROVE-WITH-NITS. The schema is genuinely vendored (byte-identical SHA-256 after CRLF strip), 33/33 tests pass in a clean repro, no existing Hermes file was modified, and the non-breaking contract is real. Observations 1, 2, and 4 are the load-bearing ones — they're not blocking for v0 (the schema is the source of truth and the bridge is gated by the producer's correctness), but they're the right shape for a follow-up commit before any production CGP goes out. Observations 3, 5, 6 are nits.
The PR is a clean handoff. Sign-off recommended.
agent_signature (advisory unsigned-local): ACK::Mavis-verifier::HERMES-BOOTSTRAP-CONSUMER-REVIEW-2026-08-08
…ing tool_ids, drop dead imports The verifier's review of PR #4 surfaced 6 pre-merge findings; this commit applies the cleanup for all 6: 1. Semantic-naming drift (Bootstrap.source to load_source): the field name 'source' collided with meta.source (the producer). Renamed the load-source attribute to load_source; updated the dataclass field, the _from_dict factory, the stub_bootstrap factory, the docstring, the public surface in __init__.py, and the 5 call sites in test_pmoves_bootstrap.py. 2. Reasoning gap (2-line guard in register_pmoves_tools): a malformed CGP with non-string entries in the tools array (e.g. an object {inject: evil} or an int 42) used to crash the bridge with TypeError on the `in disable` check. Now the bridge skips non-string entries to the `skipped` bucket with a warning log, and the LOG.info(skipped) call uses key=str to sort mixed-type lists. 3. Defense-in-depth (sort key=str): the LOG.info(skipped) call was crashing on sorted([int, str, None]) due to int < str comparison. Added key=str to handle mixed types. The new test_G7 proves the bridge no longer crashes on tools=[gh, dict, 42, None]. 4. Cleanup (dead imports in loader.py): dropped unused re, sys, Iterable from the typing import. 5. Cleanup (test count drift in docstrings): the test file header and the README both said 31 tests / 8 groups; the actual is 33 tests / 9 groups. Updated to 33 / 9. 6. Nit (line endings on vendored JSON): added pmoves_bootstrap/cgp_schema/*.json text eol=lf to .gitattributes so Windows checkouts do not reintroduce CRLF and produce a false-positive drift signal on the SHA-256 byte-compare against the canonical PMOVES.AI copy. Also re-vendored v1.schema.json with the PMOVES.AI side new super_nodes-required + services/routing additionalProperties tightening (the Pinokio fork got the same re-vendor in its separate commit). Test count: 33 to 34 (added test_G7_non_string_tool_id_does_not_crash_bridge). All 34 pass.
|
Disposition of the verifier's 5 observations + 1 nit on this PR: Already fixed in follow-up commits (now on the branch):
Re-vendored schema (the PMOVES.AI side added Deferred to follow-up slices (not pre-merge):
Verification after the cleanup: all 34 tests pass (33 original + 1 new). Vendored schema still byte-identical to the canonical (after the PMOVES.AI side updates, the fork re-vendored; SHA-256 holds with CRLF normalization, then Disposition: APPROVE-WITH-NITS (the original verifier verdict). All observations dispositioned; the 2 deferred items are documented in the LEARNINGS for the multi-fork follow-up. |
The Contributor Attribution Check (CI) was failing because my local commit author (Mavis@pmoves.local) wasn't in the .mailmap. The fix is the standard mailmap format: canonical name + canonical noreply + commit email. Future Mavis commits against this fork will now be attributed correctly. The Hermes fork's contributor graph is otherwise stable; this is a no-op for attribution counting.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45f46545f5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,89 @@ | |||
| """PMOVES bootstrap CGP consumer for the PMOVES-hermes-agent fork. | |||
There was a problem hiding this comment.
Include the new package in installed distributions
The repository's pyproject.toml has an explicit [tool.setuptools.packages.find] allow-list that does not include pmoves_bootstrap or pmoves_bootstrap.*, and no package-data entry ships cgp_schema/*.json or *.yaml. Consequently, source-checkout tests can import this package, but normal wheel/sdist installations omit the module and its required schema/example files, making the feature unavailable with ModuleNotFoundError; add the package to discovery and include its data files in both wheel and sdist configuration.
Useful? React with 👍 / 👎.
| def _read_default() -> Optional[tuple[dict, str]]: | ||
| if EXAMPLE_PATH.exists(): | ||
| return _read_path(str(EXAMPLE_PATH)) |
There was a problem hiding this comment.
Return the stub instead of consuming the example by default
When callers provide no path, source, or environment CGP, the checked-in example always exists and is consumed as live configuration, so load_bootstrap() returns the minimax identity and register_pmoves_tools() registers the example's PMOVES tools rather than producing the documented safe no-op stub. This makes every unconfigured runtime look configured with sample infrastructure and tools; keep the example as an explicit fixture and use stub_bootstrap() when no operator-supplied CGP exists.
Useful? React with 👍 / 👎.
| candidates: list[Optional[tuple[dict, str]]] = [ | ||
| _read_path(path) if path else None, | ||
| _read_source(source) if source else None, | ||
| _read_env(), | ||
| _read_default(), |
There was a problem hiding this comment.
Read bootstrap candidates lazily under error handling
All candidate readers are executed eagerly before the priority loop, so an invalid lower-priority source can prevent a valid higher-priority source from being returned—for example, a valid source= still raises JSONDecodeError when PMOVES_BOOTSTRAP_CGP contains malformed JSON. The same placement lets a missing path= or malformed source escape even with strict=False, contrary to the fallback contract; invoke each reader in priority order inside the guarded loop and normalize parse/read failures to BootstrapError.
Useful? React with 👍 / 👎.
| if not isinstance(obj[key], dict): | ||
| raise BootstrapError(f"CGP {key} must be a dict") | ||
|
|
||
| sn = obj["super_nodes"] |
There was a problem hiding this comment.
Require super_nodes before indexing it
For a CGP that omits the schema-required super_nodes field, validation reaches this direct index and raises KeyError rather than BootstrapError. This bypasses both the documented non-strict fallback and the strict-mode public exception contract; include super_nodes in the required-key check or explicitly reject its absence before indexing.
Useful? React with 👍 / 👎.
| for key in ("tools", "mcps", "constraints"): | ||
| if not isinstance(obj[key], list): | ||
| raise BootstrapError(f"CGP {key} must be a list") |
There was a problem hiding this comment.
Validate string items in exported arrays
The vendored schema requires string items for tools, mcps, and constraints, but this validation accepts any list contents. A producer payload such as "tools": [42] therefore loads successfully and later crashes export_env() at ",".join(bs.tools) with TypeError; reject non-string entries during validation so a malformed CGP follows the normal strict/fallback path.
Useful? React with 👍 / 👎.
The Contributor Attribution Check (CI) was failing because my commit author Mavis@pmoves.local isn't in scripts/release.py AUTHOR_MAP. The check uses AUTHOR_MAP (not the .mailmap, which is for git shortlog) to attribute commits to GitHub usernames. Added the mapping Mavis@pmoves.local -> Mavis-PMOVES. The .github/PULL_REQUEST_TEMPLATE.md / CONTRIBUTING.md author guidance will surface the canonical username in future PR bodies if the operator wants to backfill the real GitHub handle. Also added a .mailmap entry (commit 35224e5) for git shortlog / GitHub contributor graph, even though the CI check doesn't read the .mailmap.
Hermes-side of the Mavis harness v0 (3-repo coordinated)
The companion slice to POWERFULMOVES/PMOVES.AI PR NousResearch#2477 and POWERFULMOVES/PMOVES-pinokio PR #1 (feat/pmoves-app-launcher). The CGP (
pmoves.bootstrap/v1) is the contract that ties the 3 forks together: PMOVES.AI writes it, PMOVES-hermes-agent reads it at session init + registers PMOVES tools alongside the native toolset, PMOVES-pinokio reads it when launching a PMOVES-tagged app.What this PR ships (8 files, 3 stacked commits)
pmoves_bootstrap/loader.py— the CGP reader. Accepts both YAML and JSON (Hermes haspyyaml==6.0.3in core deps). 4 input sources in priority order: path arg, source arg, env var, vendored example. Validates structurally against the vendored JSON Schema. Returns a typedBootstrapdataclass withhas_tool/has_mcp/has_constraint/service/route_foraccessors. Stub Bootstrap (safe defaults, all 6 constraints) when no CGP is present.pmoves_bootstrap/tools_bridge.py— registers PMOVES tools in the session. Resolves eachbootstrap.toolsentry against the v0 tool registry (Python scripts + CLI binaries). Returns aBridgeResultwithregistered/skipped/disabledlists. The session init code (a future slice) merges registered tools into Hermes's active toolset.PMOVES_TOOLS_DISABLEenv var is a per-tool deny list.pmoves_bootstrap/subscriber.py— the optional NATS subscriber. v0 is a STUB (nonats-pyin Hermes's core deps).subscribe()always returns aSubscriberStatuswithenabled=Falseand a clear reason.TaskEnvelope/ResultEnvelopedataclasses document the wire contract so a future slice can wire in nats-py without changing the public surface. Subjects:pmoves.agent.task.v1(input),pmoves.agent.result.v1(output),pmoves.bpm.phase.v1+pmoves.bpm.pomodoro.v1(observability).pmoves_bootstrap/__init__.py— public surface, re-exports all of the above.pmoves_bootstrap/cgp_schema/v1.schema.json+example.cgp.yaml— vendored copies of the PMOVES.AI schema + example.tests/test_pmoves_bootstrap.py— 33 tests, 9 test groups,pytest.pmoves_bootstrap/README.md— high-level map + non-breaking contract + design choices + cross-fork plan.Tests (33/33 pass)
python -m pytest tests/test_pmoves_bootstrap.py -v -o addopts= # 33 passed in 3.50s(The
-o addopts=override is needed on Windows where the project-levelpytest-timeoutaddopts expectsSIGALRMwhich doesn't exist on Windows.)Non-breaking contract (enforced by the constraints baked into the CGP)
no-override-existing-configcli-config.yaml,hermes_state) is never replaced by the CGPtagged-services-are-advisoryservicesblock (Tailscale, RustDesk, Hostinger, Cloudflare) is a hint — missing services are skipped, not failedno-chit-bypasspmoves-chit-sign, not directly through the CGPno-force-push--force)no-ci-bypass--adminto skip CI)preserve-existing-toolstoolsets.py) is preserved; the bridge adds PMOVES tools alongside, never in place ofThe test pair (proves non-breaking):
load_bootstrap()returns the stub Bootstrap.register_pmoves_tools()returnsBridgeResult(registered=[]).subscribe()returnsSubscriberStatus(enabled=False). Hermes runs as it does today.Design choices
pyyaml==6.0.3andruamel.yaml==0.18.17in core deps, so the loader accepts both formats. YAML is the natural format (the PMOVES.AI side writes the canonical CGP in YAML for human editing); JSON is supported forPMOVES_BOOTSTRAP_CGPraw-string env vars.jsonschemavalidator. The structural check covers the 80% case (required fields, const spec, enum values,super_nodes: []) with zero new deps. If strict JSON Schema validation is needed for CHIT-signed CGPs in a future slice, the operator can installjsonschemaand run it externally.nats-pyin v0.nats-pyis not in Hermes's core dependencies. Adding it would be a meaningful change topyproject.toml— the deps list explicitly warns against adding new packages (see theMini Shai-Huludcomment on the existingdependencieslist). The v0 subscriber is a STUB with a stable wire contract documented viaTaskEnvelope/ResultEnvelopedataclasses. A future slice can add nats-py + implementsubscribe()without changing the public surface.pmoves_bootstrap/package, the test file, and the README.cli.py,run_agent.py,toolsets.py,pyproject.toml,hermes_bootstrap.py(the Windows UTF-8 helper, unrelated) — all unchanged.Cross-fork plan (the 3 PRs of harness v0)
load_bootstrap.py+orchestrator.py+bpm_cron.py+ 56/56 testsPOWERFULMOVES/PMOVES-hermes-agentPRfeat/pmoves-bootstrap-consumer(this PR, agent) —pmoves_bootstrap/package + 33/33 testspmoves_loader.js+ example app + 24/24 testsAll three read the same
v1.schema.json. The schema is the contract.What this slice does NOT do (intentional, follow-up)
run_agent.py/cli.py— the actual integration point in Hermes's session lifecycle is a follow-up. v0 ships the package; the operator (or a future slice) wires the public API into the session init code.nats-pysubscriber — see "Why nonats-pyin v0" above.no-chit-bypassconstraint is honored by the loader's behavior, but no actual CHIT signing code lives in this package. The Mavis orchestrator side does the signing.register_pmoves_tools()with a per-identity allow-list. v0 trusts the operator's CGP as-is.CHIT trail unsigned-local
No
CHIT_PASSPHRASEloaded in this Mavis session per the standing operator convention.