From c5ea2ba07be494d86342ff5d5b56153862fc140d Mon Sep 17 00:00:00 2001 From: automateyournetwork Date: Sat, 10 Oct 2026 07:20:55 -0400 Subject: [PATCH 1/5] docs(spec147): start installer follow-up intake after dotenv merge --- .../spec.md | 2 +- .../verification.md | 4 ++ specs/147-installer-followups/plan.md | 24 +++++++++ specs/147-installer-followups/research.md | 25 ++++++++++ specs/147-installer-followups/spec.md | 50 +++++++++++++++++++ specs/147-installer-followups/tasks.md | 14 ++++++ specs/147-installer-followups/verification.md | 7 +++ 7 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 specs/147-installer-followups/plan.md create mode 100644 specs/147-installer-followups/research.md create mode 100644 specs/147-installer-followups/spec.md create mode 100644 specs/147-installer-followups/tasks.md create mode 100644 specs/147-installer-followups/verification.md diff --git a/specs/146-dotenv-onboarding-preservation/spec.md b/specs/146-dotenv-onboarding-preservation/spec.md index 5e1e7faa..7ed2f582 100644 --- a/specs/146-dotenv-onboarding-preservation/spec.md +++ b/specs/146-dotenv-onboarding-preservation/spec.md @@ -1,6 +1,6 @@ # Spec 146: Preserve filled dotenv settings during onboarding -Status: implemented and locally verified; awaiting review/publication. Scope authorized by the owner's request to investigate and fix initialization ignoring a filled `.env`. Date: 2026-10-09. +Status: complete; merged in PR #288 on 2026-10-10 after all 31 PR checks passed. Source metadata: 1.6.1; no tag or GitHub release published. Scope authorized by the owner's request to investigate and fix initialization ignoring a filled `.env`. Created: 2026-10-09. ## User scenarios and acceptance diff --git a/specs/146-dotenv-onboarding-preservation/verification.md b/specs/146-dotenv-onboarding-preservation/verification.md index ce341650..379324b1 100644 --- a/specs/146-dotenv-onboarding-preservation/verification.md +++ b/specs/146-dotenv-onboarding-preservation/verification.md @@ -22,3 +22,7 @@ Python dotenv and Node dotenv differ on unquoted hashes without preceding whites All planned local checks passed. No live user-host diagnosis, provider login, daemon installation/restart, live Hermes credentials or device changes are claimed. Existing runtime configuration presence remains the wizard-skip criterion; a partial upstream config may still require manual onboarding. Repository wrappers outside installation are not migrated to new path semantics by this fix. Startup device listing was attempted and failed on the existing testbed's unsupported `connections.defaults.arguments` key. MemPalace's configured server file is unavailable. GAIT recorded the session; no network state was inferred and no external tickets/messages were created. + +## PR closeout — 2026-10-10 + +Owner explicitly authorized commit, PR and merge. [PR #288](https://github.com/automateyournetwork/netclaw/pull/288) merged at 2026-10-10T11:19:33Z with all **31 checks successful** for head `813a6dc3174922b84ad185769cd3aff4f094b33d`. Merge commit: `aecf438ee56ad28337b3f5d715c8d8afa62605ae`. Returned to main and fast-forwarded before creating draft `147-installer-followups`. No tag/release publication or runtime/device change was performed. diff --git a/specs/147-installer-followups/plan.md b/specs/147-installer-followups/plan.md new file mode 100644 index 00000000..b5be3056 --- /dev/null +++ b/specs/147-installer-followups/plan.md @@ -0,0 +1,24 @@ +# Plan: Installer follow-ups + +**Branch**: `147-installer-followups` +**Date**: 2026-10-10 +**Spec**: [spec.md](spec.md) +**Status**: Intake draft; technical design awaits the failure reports. + +Follow the checked-in Spec Kit workflow manually: specify, clarify, research/plan, tasks, analyze, implement and verify. The slash-command integration is unavailable in this session. + +## Technical context + +Existing entry points are `scripts/install.sh` and `scripts/setup.sh`; shared orchestration is in `scripts/lib/`. Installer helpers use Bash and Python. Existing unit and installer contract suites provide isolated reproduction infrastructure. Affected host, component, dependency and runtime versions remain unknown. + +## Sequence + +1. Capture each owner-supplied report and compare it with merged spec 146 and pending PR #287. +2. Reproduce confirmed failures on an isolated baseline; record assumptions and missing evidence. +3. Define the smallest affected implementation surface and expected before/after behavior. +4. Amend this plan and its tasks before editing runtime code. +5. Implement and verify agreed fixes with appropriate regressions, update operator documentation, and prepare patch release metadata after completion. + +## Constitution check + +Preserve credentials and operator state; no provider calls, daemon operations, device configuration or system package installs are needed for draft intake. No new MCP/skill/HUD capability is proposed. Reassess artifact coherence after the concrete scope is known. No version bump for this draft. diff --git a/specs/147-installer-followups/research.md b/specs/147-installer-followups/research.md new file mode 100644 index 00000000..08df17ca --- /dev/null +++ b/specs/147-installer-followups/research.md @@ -0,0 +1,25 @@ +# Research: Installer follow-ups + +## Baseline + +Start from main after merging [PR #288](https://github.com/automateyournetwork/netclaw/pull/288), spec 146. That patch imports checkout dotenv settings before onboarding/setup, preserves runtime assignments, and propagates onboarding failures. Its release metadata is 1.6.1. + +The prior reporter's exact init command and host were unknown. Do not reuse that uncertainty as evidence of a different installer bug. + +## Existing related work + +[PR #287](https://github.com/automateyournetwork/netclaw/pull/287), `codex/fix-installer-preflight`, was open and draft when inspected on 2026-10-10. Its author describes host/component preflight, interpreter compatibility, per-run logs and Zabbix installation working-directory repair under spec 049. These are PR claims pending review here, not newly reproduced findings. Recheck the PR before selecting overlapping work. + +## Pending evidence + +The owner will provide new reports. For each, capture what is available: + +- Install command and selected runtime/profile/components. +- OS/version/architecture and relevant Python/Node/runtime versions. +- Fresh installation or retry; repository revision if known. +- First relevant error and failing stage, with credentials and private addresses removed. +- Expected result, actual result and reproducibility. + +Do not request a complete dotenv file or unredacted logs. No new root cause, solution, dependency or platform guarantee is selected yet. + +Merged baseline: `aecf438ee56ad28337b3f5d715c8d8afa62605ae`, PR #288 merged 2026-10-10T11:19:33Z after all 31 checks passed for `813a6dc3174922b84ad185769cd3aff4f094b33d`. diff --git a/specs/147-installer-followups/spec.md b/specs/147-installer-followups/spec.md new file mode 100644 index 00000000..a73859c2 --- /dev/null +++ b/specs/147-installer-followups/spec.md @@ -0,0 +1,50 @@ +# Feature Specification: Installer follow-ups + +**Feature Branch**: `147-installer-followups` +**Created**: 2026-10-10 +**Status**: Draft — waiting for the owner's installer failure details +**Input**: Close spec 146 through PR/merge, then start a new spec for other installer problems. The owner will provide the reports separately. + +## User Scenarios & Testing + +### User Story 1 — Resolve a reported installation failure (Priority: P1) + +An operator reports an installer failure. Capture enough sanitized evidence to reproduce it, identify the responsible stage, and define the expected successful behavior before choosing a fix. + +**Independent test**: Each accepted report receives a reproduction that fails on the recorded baseline and passes after its scoped fix. + +**Acceptance scenarios**: + +1. Given a new report, when triage is complete, its command, affected component/stage, expected behavior, actual error and known host/runtime versions are recorded; missing facts remain explicitly unknown. +2. Given a confirmed repository defect, when its fix is accepted, a meaningful regression reproduces the original failure and verifies the corrected result. +3. Given an issue already addressed by an existing PR, when the reports are compared, this spec links and coordinates that work instead of independently duplicating it. + +### Edge cases to assess against actual reports + +- Fresh installs versus reruns with existing runtime configuration and credentials. +- Unsupported host or interpreter versus a repository defect. +- Partial installs or failed dependencies followed by misleading success. +- Issues already fixed in main or covered by pending installer work. + +These are triage considerations, not claims that new defects have been found. + +## Requirements + +- **FR-001**: Record each supplied failure and its evidence in this spec before implementation. +- **FR-002**: Keep observed failures, hypotheses, upstream limitations and existing fixes distinct. +- **FR-003**: Define the affected platforms/components and acceptance criteria per confirmed issue; preserve unrelated operator configuration and the spec 146 dotenv behavior. +- **FR-004**: Use isolated temporary homes/runtimes and dummy credentials for reproduction where possible. Keep credentials, private topology and private logs out of repository artifacts. +- **FR-005**: Update research, plan and tasks with the selected solution before implementation; report relevant verification and untested host/live boundaries honestly. + +## Success Criteria + +- Every accepted fix has a traceable report, baseline, expected behavior and regression evidence. +- Relevant installer regressions and repository checks pass without weakening assertions. +- Existing credentials and unrelated runtime configuration remain intact. +- Source release metadata advances once only when this spec's eventual fix scope is completed; this draft does not advance the version. + +## Assumptions and scope boundary + +No new failure details have been supplied yet. The owner explicitly said they will send them for this spec. This draft authorizes intake and planning, not an invented installer rewrite or speculative package/runtime changes. + +Spec 146 is the preceding dotenv/onboarding fix. PR #287 separately proposes host preflight and Zabbix repairs; its status must be rechecked when reports arrive. No merge of that PR is part of this draft. diff --git a/specs/147-installer-followups/tasks.md b/specs/147-installer-followups/tasks.md new file mode 100644 index 00000000..1ff38cdb --- /dev/null +++ b/specs/147-installer-followups/tasks.md @@ -0,0 +1,14 @@ +# Tasks: Installer follow-ups + +## Intake + +- [x] T001 Close the preceding dotenv fix through its authorized PR and merge. +- [x] T002 Create the new numbered spec and branch from updated main. +- [x] T003 Record existing related work and the evidence needed for triage. +- [ ] T004 Receive the owner's new installer failure reports. +- [ ] T005 Reproduce and classify each accepted report; coordinate overlap with PR #287. +- [ ] T006 Define concrete acceptance criteria and update research, plan and implementation tasks. + +## Implementation gate + +Implementation, regression commands and release tasks will be specified after T004–T006. No speculative code changes or additional source-release bump are part of this draft. diff --git a/specs/147-installer-followups/verification.md b/specs/147-installer-followups/verification.md new file mode 100644 index 00000000..e5fa6961 --- /dev/null +++ b/specs/147-installer-followups/verification.md @@ -0,0 +1,7 @@ +# Verification: Draft intake + +This spec contains planning artifacts only. No new installer defect or fix has been claimed or tested. + +The preceding dotenv patch's verification is recorded in [spec 146](../146-dotenv-onboarding-preservation/verification.md) and [PR #288](https://github.com/automateyournetwork/netclaw/pull/288). This does not establish acceptance for future reports. + +Draft validation on 2026-10-10: `python3 scripts/verify-spec-artifacts.py` passed (131 specs, four historical exceptions); `git diff --check` passed; `python3 scripts/prepare-release.py --check` passed for the unchanged 1.6.1 baseline. No new source-release bump is warranted for intake documents. New failure reproductions and regression results will be recorded here when the owner supplies the reports. From e8b230beeb56566137d0538d1f3a50faad82214b Mon Sep 17 00:00:00 2001 From: automateyournetwork Date: Sat, 10 Oct 2026 07:45:19 -0400 Subject: [PATCH 2/5] docs(spec147): triage installer reports against merged preflight fixes --- .../node-preflight-evidence.json | 41 ++++++++ specs/147-installer-followups/plan.md | 51 ++++++---- .../registration-evidence.json | 21 ++++ specs/147-installer-followups/reports.md | 47 +++++++++ .../reproduce-transports.py | 91 ++++++++++++++++++ specs/147-installer-followups/research.md | 36 ++++--- specs/147-installer-followups/spec.md | 95 ++++++++++++------- specs/147-installer-followups/tasks.md | 44 +++++++-- .../transport-evidence.json | 52 ++++++++++ specs/147-installer-followups/verification.md | 53 ++++++++++- 10 files changed, 454 insertions(+), 77 deletions(-) create mode 100644 specs/147-installer-followups/node-preflight-evidence.json create mode 100644 specs/147-installer-followups/registration-evidence.json create mode 100644 specs/147-installer-followups/reports.md create mode 100644 specs/147-installer-followups/reproduce-transports.py create mode 100644 specs/147-installer-followups/transport-evidence.json diff --git a/specs/147-installer-followups/node-preflight-evidence.json b/specs/147-installer-followups/node-preflight-evidence.json new file mode 100644 index 00000000..b45f6f16 --- /dev/null +++ b/specs/147-installer-followups/node-preflight-evidence.json @@ -0,0 +1,41 @@ +{ + "baseline": "b4334bf", + "fixture_only": true, + "cases": [ + { + "node": "v18.20.8", + "preflight_ok": true, + "errors": [] + }, + { + "node": "v20.20.2", + "preflight_ok": true, + "errors": [] + }, + { + "node": "v22.0.0", + "preflight_ok": true, + "errors": [] + }, + { + "node": "v24.15.0", + "preflight_ok": true, + "errors": [] + }, + { + "node": "v24.16.0", + "preflight_ok": true, + "errors": [] + }, + { + "node": "v25.9.0", + "preflight_ok": true, + "errors": [] + }, + { + "node": "v26.1.0", + "preflight_ok": true, + "errors": [] + } + ] +} diff --git a/specs/147-installer-followups/plan.md b/specs/147-installer-followups/plan.md index b5be3056..1adef62e 100644 --- a/specs/147-installer-followups/plan.md +++ b/specs/147-installer-followups/plan.md @@ -1,24 +1,43 @@ -# Plan: Installer follow-ups +# Implementation Plan: Installer and first-use reliability -**Branch**: `147-installer-followups` -**Date**: 2026-10-10 -**Spec**: [spec.md](spec.md) -**Status**: Intake draft; technical design awaits the failure reports. - -Follow the checked-in Spec Kit workflow manually: specify, clarify, research/plan, tasks, analyze, implement and verify. The slash-command integration is unavailable in this session. +**Branch**: `147-installer-followups` | **Updated**: 2026-10-10 | **Spec**: [spec.md](spec.md) +**Status**: Concrete scope defined; production implementation pending. ## Technical context -Existing entry points are `scripts/install.sh` and `scripts/setup.sh`; shared orchestration is in `scripts/lib/`. Installer helpers use Bash and Python. Existing unit and installer contract suites provide isolated reproduction infrastructure. Affected host, component, dependency and runtime versions remain unknown. +Bash 3.2-compatible orchestration, standard-library Python helpers and the existing contract-test runner. Start from Nick's preflight and Zabbix fixes on main `b4334bf`. Keep the installer's runtime-specific OpenClaw/Hermes paths and managed component environment records. + +## Delivery order + +### Phase A — Runtime compatibility and bootstrap (US1–US2) + +- Add one declared/versioned OpenClaw Node rule consumed by preflight and core installation. Reject unsupported major/minor/patch combinations before mutation; keep Hermes-specific requirements separate where applicable. +- Keep Python/component rules from PR #287. Explain uv as an actual selected-component prerequisite; any future interactive repair must choose compatible packages and refresh both installer and component interpreters. +- Construct the npm command from detected npm capabilities/version and the selected OpenClaw target. Narrow allow-scripts to reviewed first-party needs; prefer a user-owned prefix or existing version manager when the global prefix is not writable. Preserve existing npm settings. +- Propagate installation and executable verification failures without suggesting privilege escalation for unrelated errors. + +### Phase B — Component access and launcher coverage (US3) + +- Inventory the twelve reported components in one explicit access-path contract: component id, server/skill names, mode, executable, transport, expected tools, required configuration and verification. +- Add supported native registrations or an explicit supported skill path; bind Python/console launchers to successful runtime records. Resolve external checkout entry points against actual packaged code rather than guessing paths. +- Reuse pyATS's owned stdio bridge and its managed runtime; do not introduce a mandatory persistent/public HTTP service. +- Preserve custom entries, managed ownership state, credentials and persona/testbed files. Retain write approval/filter semantics when making tools natively visible. +- Update catalog/inventory/doc counts coherently when an external integration moves into native registration. + +### Phase C — Readiness and first-use evidence (US4–US5) + +- Produce structured per-component stages: artifacts, access path/registration, discovery, endpoint verification. Render that evidence directly; do not have the model infer readiness. +- Use bounded native-runtime discovery plus direct protocol diagnostics. Check structured errors and expected tool presence even when the probe exits 0. Separate empty legitimate servers from missing required tools. +- Keep credential-dependent or unrequested endpoint operations unverified. Validate local/remote Ollama against its configured endpoint/model without requiring a local binary for remote use. +- Add an explicit synthetic tool canary with provenance and negative cases. Live CML/pyATS canaries require configured, authorized read-only targets and remain separately recorded. +- Reproduce any remaining CML stdio failure with exact runtime/server versions and sanitized command/env/cwd differences; escalate upstream only with a minimal reproducer. + +## Validation strategy -## Sequence +Use temporary HOME/state/config paths and fake endpoints for install orchestration. Matrix includes macOS arm64/Linux, Node support boundaries, npm pre-11.16/11.16+/12+, compatible/incompatible/explicit Python, isolated package targets, altered PATH, repeat installs, config preservation, failed/empty discovery, explicit HTTP transport and remote Ollama without local CLI. -1. Capture each owner-supplied report and compare it with merged spec 146 and pending PR #287. -2. Reproduce confirmed failures on an isolated baseline; record assumptions and missing evidence. -3. Define the smallest affected implementation surface and expected before/after behavior. -4. Amend this plan and its tasks before editing runtime code. -5. Implement and verify agreed fixes with appropriate regressions, update operator documentation, and prepare patch release metadata after completion. +Run affected unit tests and the installer suite, then declaration/reconciliation checks. Real-host evidence supplements fixtures; no full live catalog or arbitrary model reliability claim follows from a passing offline suite. -## Constitution check +## Constitution and scope -Preserve credentials and operator state; no provider calls, daemon operations, device configuration or system package installs are needed for draft intake. No new MCP/skill/HUD capability is proposed. Reassess artifact coherence after the concrete scope is known. No version bump for this draft. +Existing device/write approvals, credentials and operator files must survive. No global environment purge, blanket script-policy bypass or speculative dependency upgrade. Installer/registration/skills/docs/tests change as needed; no unrelated HUD redesign is proposed. No additional release bump for research; prepare one patch release when agreed implementation and checks are complete. diff --git a/specs/147-installer-followups/registration-evidence.json b/specs/147-installer-followups/registration-evidence.json new file mode 100644 index 00000000..2e737752 --- /dev/null +++ b/specs/147-installer-followups/registration-evidence.json @@ -0,0 +1,21 @@ +{ + "baseline": "b4334bf", + "selected_components": [ + "pyats", + "netbox", + "servicenow", + "nvd-cve", + "subnet-calc", + "wikipedia", + "markmap", + "drawio-rfc", + "packet-buddy", + "nmap", + "gtrace", + "tts" + ], + "generated_native_registration_count": 0, + "generator_returncode": 0, + "skill_invocation_paths_exist": true, + "live_tool_availability_not_tested": true +} diff --git a/specs/147-installer-followups/reports.md b/specs/147-installer-followups/reports.md new file mode 100644 index 00000000..5c069510 --- /dev/null +++ b/specs/147-installer-followups/reports.md @@ -0,0 +1,47 @@ +# Report triage — 2026-10-10 + +Baseline: main `b4334bf`, merged into the spec branch as `cf12ec1`. Reports are user-supplied evidence, not a claim that all issues still exist in this revision. Private home paths and device data are omitted. + +## macOS / Homebrew / Hermes report + +The pasted run selected 108 components. It first found Apple's Python 3.9.6 and no uv; Homebrew installed Python 3.14, but the same process retained an earlier component interpreter. Later errors named missing Go 1.25+ and Zabbix's unresolved relative vendor requirement. Its final summary reported eight failed components; only two component failures and prerequisite excerpts are available. The referenced archive was not attached. + +| Report | Current disposition | Evidence / next action | +| --- | --- | --- | +| uv absent | Missing prerequisite; early detection now merged | PR #287 checks uv/uvx before mutation. pyATS/GAIT setup uses uv to create isolated runtimes; inspected code does not pin a uv version. Keep actionable installation guidance. | +| Python 3.9 retained after Homebrew repair | Historical retry path; new preflight changes the flow | Current full installer stops at preflight before legacy repair offers. Existing 3.12 selection and explicit override preservation are tested. Any future repair must re-resolve both interpreters; do not reinstall generic latest Python blindly. | +| Python 3.14 allegedly unsupported on Apple Silicon | Incorrect general explanation | CPython 3.14 supports Apple Silicon. Individual packages/native wheels impose their own bounds; PR #287 models known limits (for example Panorama and Zoom). | +| Forward requires Go >=1.25 | Addressed by merged preflight | Go, CGO, compiler and Apple command-line-tool checks now precede component installation. This is not proof of a real Forward deployment. | +| Zabbix `./vendor/zabbix-mcp-server` missing | Fixed by Nick's merged PR #287 | Installation now changes into the Zabbix component directory before pip evaluates the requirement; focused regression passes. | +| Eight failed components | Partially evidenced | Do not invent the six unnamed failures. Add them when sanitized logs become available. | + +## Linux / OpenClaw report dated 2026-09-13 + +The report describes Debian/Ubuntu, Node 20.20.2, 23 selected components, remote self-hosted Ollama and an unspecified checkout revision. It names OpenClaw 2026.9.4 in npm output. Its local workarounds are evidence about that host; none were applied to this workstation. + +| ID | Report | Current disposition | +| --- | --- | --- | +| L1 | REPO_ROOT unset without Claw Certification | Fixed by earlier installer changes. Five affected path tests pass with REPO_ROOT unset. | +| L2 | Node prerequisite too low | **Confirmed remaining defect.** Current preflight and core checks accept >=18. Fixture checks accept Node 18/20/22/24.15/25 even though current OpenClaw's published engine range excludes them. | +| L3 | npm EACCES guidance favors sudo | **Remaining installer behavior.** Generic helper assumes failed npm installation usually needs root and offers sudo. Provide targeted, rootless guidance and truthful failure propagation. Running one package command with sudo is distinct from running the entire installer as root; do not conflate them. | +| L4 | npm allowScripts warnings prove skipped builds | **Version-dependent claim; install compatibility gap remains.** Current command omits the flag. Official docs say npm 11.16 warns but still runs scripts; npm 12 blocks unapproved scripts. Use version-aware first-party approval, not the report's unconditional five-package list. | +| L5 | Ollama CLI absent after selecting remote provider | **Not proof of failure.** A remote provider needs its configured host/model, not a local binary. Add endpoint/model readiness verification. Current upstream onboarding has evolved; exact historical behavior is unverified. | +| L6 | Packet Buddy downgrades shared dependencies | Installer isolation addressed by spec 133 and framework migration. Current component installs select isolated targets and record launchers. Regression tests pass; this does not repair old global packages or prove every live integration. Skill-based launch bindings still need attention (L9). | +| L7 | Deploy ignores existing OpenClaw config | Fixed by current unconditional selected-registration merge. Preservation tests pass for both supported config shapes. | +| L8 | Twelve selected components missing native entries | **Confirmed.** Running the actual generator for those twelve yields zero native registrations with exit 0. Some have MCP_CALL-based skills, so absence from the native registry is not proof of no invocation mechanism. Current pyATS already has a private loopback HTTP bridge behind stdio. | +| L9 | Interpreter mismatch at runtime | Fixed for generated native Python/console registrations with runtime records. **Residual skill path:** e.g. NetBox examples spawn bare python3; MCP_CALL splits/spawns that command without consulting component runtime records. | +| L10 | OpenClaw MCP client fails both transports | **Narrow reproduction obtained; broad blame unproven.** Installed 2026.7.1-2 discovers synthetic stdio and explicit streamable-http correctly. URL-only HTTP sends GET, discovers no tool, but CLI returns 0. This is a candidate explanation for part of the HTTP report, not proof about 2026.9.4 or CML. | +| L11 | Persona overwritten on rerun | Current deploy preserves existing persona/testbed paths; regression passes. | +| L12 | Agent invents CML capacity after tool failure | Serious reported first-use failure, not independently reproduced here. Add deterministic readiness and an evidence-backed canary; do not claim installer prompts alone prevent all hallucinations. | +| L13 | Installation summary implies usable tools | **Remaining gap.** Artifact checks still print OK based on a file/directory/runner, and remote entries can be OK without connectivity. Separate install, registration, discovery and endpoint readiness. | + +## Verified corrections to suggested fixes + +- Do not add a mandatory persistent 0.0.0.0:8080 pyATS daemon: the owned bridge already starts a private temporary loopback server with the correct managed interpreter. +- Do not use a public/general Python 3.14 ban. Compatibility belongs to the selected packages and architectures. +- Do not require a local Ollama binary merely because a remote Ollama provider is selected. +- Do not infer lifecycle-script blocking from npm 11 warning text alone. +- Do not treat direct MCP success as proof that the runtime's command, env, cwd, credentials and transport match. +- Do not treat `openclaw mcp probe` exit 0 as proof of tool discovery. + +See [research](research.md) for sources and [verification](verification.md) for measured checks. diff --git a/specs/147-installer-followups/reproduce-transports.py b/specs/147-installer-followups/reproduce-transports.py new file mode 100644 index 00000000..0667b8f1 --- /dev/null +++ b/specs/147-installer-followups/reproduce-transports.py @@ -0,0 +1,91 @@ +"""Probe synthetic discovery servers in an isolated OpenClaw state directory. + +No provider calls, actual device tools, package installation or daemon operations. +Pass explicit installed Node/CLI paths; this does not update the runtime. +""" +import argparse +import http.server +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import threading + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--node', type=Path, required=True) +parser.add_argument('--cli', type=Path, required=True) +parser.add_argument('--runtime-label', required=True) +parser.add_argument('--output', type=Path, required=True) +args = parser.parse_args() +node, cli = args.node.resolve(), args.cli.resolve() +fixture = '''import json,sys +def answer(m): + method=m.get('method') + if method=='initialize': return {'protocolVersion':m.get('params',{}).get('protocolVersion','2025-03-26'),'capabilities':{'tools':{}},'serverInfo':{'name':'netclaw-fixture','version':'1.0.0'}} + if method=='tools/list': return {'tools':[{'name':'fixture_ping','description':'Synthetic local fixture; no network operations','inputSchema':{'type':'object','properties':{}}}]} + if method=='resources/list': return {'resources':[]} + if method=='resources/templates/list': return {'resourceTemplates':[]} + if method=='prompts/list': return {'prompts':[]} + return {} +if __name__=='__main__': + for line in sys.stdin: + m=json.loads(line) + with open(sys.argv[1],'a') as log: log.write(m.get('method','unknown')+'\\n') + if 'id' in m: print(json.dumps({'jsonrpc':'2.0','id':m['id'],'result':answer(m)}),flush=True) +''' +namespace = {'__name__': 'fixture_module'} +exec(fixture, namespace) +answer = namespace['answer'] +events = [] + +class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, *_): pass + def do_GET(self): + events.append({'verb': 'GET', 'method': None}) + self.send_response(405); self.send_header('Content-Length', '0'); self.end_headers() + def do_POST(self): + m = json.loads(self.rfile.read(int(self.headers.get('Content-Length', '0')))) + events.append({'verb': 'POST', 'method': m.get('method')}) + body = json.dumps({'jsonrpc': '2.0', 'id': m.get('id'), 'result': answer(m)}).encode() if 'id' in m else b'' + self.send_response(200 if body else 202) + self.send_header('Content-Type', 'application/json'); self.send_header('Content-Length', str(len(body))); self.end_headers() + self.wfile.write(body) + +server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler) +threading.Thread(target=server.serve_forever, daemon=True).start() +results = {'runtime': args.runtime_label, 'node': subprocess.check_output([str(node), '--version'], text=True).strip(), 'fixture_only': True, 'cases': []} +try: + with tempfile.TemporaryDirectory(prefix='netclaw147-probe-') as directory: + base = Path(directory) + script = base / 'fixture.py'; script.write_text(fixture) + for name, entry in [ + ('stdio', {'command': sys.executable, 'args': ['-u', str(script), str(base/'stdio-methods')]}), + ('http-explicit', {'url': f'http://127.0.0.1:{server.server_port}/mcp', 'transport': 'streamable-http'}), + ('http-url-only', {'url': f'http://127.0.0.1:{server.server_port}/mcp'}), + ]: + state = base / name; state.mkdir() + entry.update({'connectionTimeoutMs': 5000, 'requestTimeoutMs': 5000}) + config = state / 'openclaw.json'; config.write_text(json.dumps({'mcp': {'servers': {'fixture': entry}}})) + env = {'HOME': str(base), 'PATH': str(node.parent) + os.pathsep + '/usr/bin:/bin', + 'OPENCLAW_STATE_DIR': str(state), 'OPENCLAW_CONFIG_PATH': str(config), 'NO_COLOR': '1'} + events.clear() + try: + result = subprocess.run([str(node), str(cli), 'mcp', 'probe', 'fixture', '--json'], + env=env, cwd=base, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=25) + case = {'name': name, 'returncode': result.returncode, 'fixture_tool_discovered': 'fixture_ping' in result.stdout, + 'http_methods': list(events)} + if name == 'stdio': + log = base / 'stdio-methods' + case['stdio_methods'] = log.read_text().splitlines() if log.exists() else [] + if result.returncode: + # Fixture environment only; show bounded diagnostics to investigate failures. + print(name, (result.stdout + result.stderr)[-1800:], file=sys.stderr) + except subprocess.TimeoutExpired: + case = {'name': name, 'timed_out': True, 'http_methods': list(events)} + results['cases'].append(case) +finally: + server.shutdown(); server.server_close() +print(json.dumps(results, indent=2)) +args.output.write_text(json.dumps(results, indent=2)+'\n') diff --git a/specs/147-installer-followups/research.md b/specs/147-installer-followups/research.md index 08df17ca..faf0ca6b 100644 --- a/specs/147-installer-followups/research.md +++ b/specs/147-installer-followups/research.md @@ -1,25 +1,31 @@ -# Research: Installer follow-ups +# Research: Installer and first-use reliability -## Baseline +## Current baseline and integration -Start from main after merging [PR #288](https://github.com/automateyournetwork/netclaw/pull/288), spec 146. That patch imports checkout dotenv settings before onboarding/setup, preserves runtime assignments, and propagates onboarding failures. Its release metadata is 1.6.1. +Fetched origin and confirmed PR #287 merged at 2026-10-10T11:25:31Z, main `b4334bf255cd53aecd2e81f51abe63deea53f1df`. Fast-forwarded local main and merged into spec 147 without conflicts. This supersedes the intake draft's statement that #287 was still open. Dotenv PR #288 is also included. -The prior reporter's exact init command and host were unknown. Do not reuse that uncertainty as evidence of a different installer bug. +The reports mix historical defects, dependency/platform requirements and residual first-use gaps. [reports.md](reports.md) preserves every supplied issue without treating the report's overall conclusion as current-main fact. -## Existing related work +## Independent evidence -[PR #287](https://github.com/automateyournetwork/netclaw/pull/287), `codex/fix-installer-preflight`, was open and draft when inspected on 2026-10-10. Its author describes host/component preflight, interpreter compatibility, per-run logs and Zabbix installation working-directory repair under spec 049. These are PR claims pending review here, not newly reproduced findings. Recheck the PR before selecting overlapping work. +1. **Host checks:** the actual preflight evaluator with otherwise-ready host fixtures accepts all of Node 18.20.8, 20.20.2, 22.0.0, 24.15.0, 24.16.0, 25.9.0 and 26.1.0. Current upstream OpenClaw excludes the first four and Node 25. [Evidence](node-preflight-evidence.json). +2. **Missing native coverage:** the actual selected-registration generator returns zero entries and exit 0 for the reported twelve components. pyATS and NetBox skills do have MCP_CALL invocation examples, so native absence and total unavailability are distinct. [Evidence](registration-evidence.json). +3. **Native binding versus skills:** `install-mcp-config.py` substitutes recorded interpreters for native entries. `netbox-reconcile/SKILL.md` still invokes a child `python3 -u $NETBOX_MCP_SCRIPT`; `mcp-call.py` splits and spawns without looking up runtime records. +4. **Readiness:** `verify_file`, `verify_dir`, `verify_runner` and `verify_remote` verify artifacts or declared transport, not actual runtime discovery/endpoint operation. CI catalog coverage includes externally invoked integrations and cannot prove runtime readiness. +5. **Transport:** isolated real OpenClaw 2026.7.1-2, Node 24.19.0, synthetic servers only. Stdio receives initialize/initialized/tools-list and discovers the fixture tool. Explicit streamable-http POSTs initialize and discovers it. URL-only HTTP sends GET without discovery and exits 0. [Evidence](transport-evidence.json), [portable reproduction](reproduce-transports.py). This is not the reporter's exact runtime or server implementation. +6. **Existing regressions:** 113 focused tests pass across preflight, independent component paths, isolated runtime selection, native configuration merging/binding, preservation and dotenv onboarding. -## Pending evidence +## Primary sources checked 2026-10-10 -The owner will provide new reports. For each, capture what is available: +- [OpenClaw Node compatibility](https://docs.openclaw.ai/install/node-compatibility): published engines `>=24.16.0 <25 || >=26.1.0`; installed older runtimes can have different ranges. Match the installation target rather than a moving general minimum. +- [OpenClaw installation](https://docs.openclaw.ai/install): npm 12 / 11.16+ use `--allow-scripts=openclaw`; <=11.15 omit it. npm 11.16 warning-only behavior is distinct from npm 12 blocking. +- [npm install-script policy](https://docs.npmjs.com/cli/v11/commands/npm-install-scripts/): global installs use per-install allow-scripts rather than a project approval command. Do not broaden policy to every dependency. +- [Python 3.14 on macOS](https://docs.python.org/3.14/using/mac.html): universal2 supports Apple Silicon and Intel; package wheel support is a separate issue. +- [OpenClaw Ollama setup](https://docs.openclaw.ai/providers/ollama/setup): configured local/LAN/remote hosts are supported and setup includes host/model checks in current upstream. A local executable is not a necessary condition for a remote provider. +- [OpenClaw MCP connections](https://docs.openclaw.ai/tools/mcp): explicit streamable-http transport and runtime discovery probes are documented. -- Install command and selected runtime/profile/components. -- OS/version/architecture and relevant Python/Node/runtime versions. -- Fresh installation or retry; repository revision if known. -- First relevant error and failing stage, with credentials and private addresses removed. -- Expected result, actual result and reproducibility. +## Design decisions -Do not request a complete dotenv file or unredacted logs. No new root cause, solution, dependency or platform guarantee is selected yet. +Build on merged preflight, isolation, deployment and dotenv helpers. First fix runtime compatibility/bootstrap, then close explicit tool access and launcher gaps, then add trustworthy readiness/first-use verification. Preserve transport boundaries and existing approvals. Diagnose original CML failures with matched command/env/cwd/version and a minimal protocol fixture before proposing an upstream patch. -Merged baseline: `aecf438ee56ad28337b3f5d715c8d8afa62605ae`, PR #288 merged 2026-10-10T11:19:33Z after all 31 checks passed for `813a6dc3174922b84ad185769cd3aff4f094b33d`. +No real credentials, package installation, model/provider request, daemon restart or device operation was performed for this investigation. diff --git a/specs/147-installer-followups/spec.md b/specs/147-installer-followups/spec.md index a73859c2..b4780826 100644 --- a/specs/147-installer-followups/spec.md +++ b/specs/147-installer-followups/spec.md @@ -1,50 +1,81 @@ -# Feature Specification: Installer follow-ups +# Feature Specification: Installer and first-use reliability -**Feature Branch**: `147-installer-followups` -**Created**: 2026-10-10 -**Status**: Draft — waiting for the owner's installer failure details -**Input**: Close spec 146 through PR/merge, then start a new spec for other installer problems. The owner will provide the reports separately. +**Feature Branch**: `147-installer-followups` +**Created**: 2026-10-10 +**Status**: Specified from user reports and current-main investigation; implementation pending +**Baseline**: `b4334bf255cd53aecd2e81f51abe63deea53f1df` (includes Nick's merged PR #287 and dotenv PR #288). + +## Problem and evidence + +Users report that an install can fail late, select incompatible dependencies, or finish with network tools unavailable. Two reports were supplied: a macOS/Homebrew/Hermes run selecting 108 components, and a Linux/OpenClaw/Ollama first-use report dated 2026-09-13. Their exact repository revisions are unknown. [Report triage](reports.md) distinguishes their observations from independently verified current behavior. + +The priority is a usable, verifiable path from component selection to agent tool discovery. Existing host preflight, component isolation, configuration merging and preservation fixes remain the baseline. ## User Scenarios & Testing -### User Story 1 — Resolve a reported installation failure (Priority: P1) +### US1 — Select a supported host and runtime (P1) + +An operator receives an actionable, aggregated preflight before installation or onboarding mutates state. + +1. Unsupported Node versions are rejected for the selected OpenClaw target, including unsupported odd release lines and insufficient patch versions. +2. Existing Python 3.12 is preferred where appropriate; an explicit compatible interpreter remains respected. Interpreter support and individual package/architecture support are separate checks. +3. A selection requiring uv, uvx, Go/CGO, Docker or other declared tools reports those prerequisites together. An all-components selection on macOS identifies Linux-only components. +4. Package-manager instructions select compatible versions and resolve both installer and component interpreter paths after any approved repair. + +### US2 — Install the runtime without ambiguous success (P1) + +The OpenClaw install command follows the selected Node/npm release contract. Permission failures give a user-owned installation path; errors are not presumed to be privilege failures. Lifecycle scripts required by the target runtime run under a narrow, version-aware policy. + +Acceptance covers npm before 11.16, npm 11.16+, npm 12+, permission errors, failed package installation, missing/broken runtime executable and runtime configuration preservation. No blanket approval of arbitrary dependency scripts or automatic model downloads. + +### US3 — Discover the tools that were selected (P1) + +For the reported twelve components, a successful supported install provides an explicit agent-access path with a launcher matching the installed environment. Native MCP registration is preferred; any intentional skill-mediated path is explicitly identified and verified. -An operator reports an installer failure. Capture enough sanitized evidence to reproduce it, identify the responsible stage, and define the expected successful behavior before choosing a fix. +1. Selecting pyATS, NetBox and CML produces usable bindings without copying the entire template over runtime configuration. +2. Each affected Python component uses its recorded interpreter even when a different Python appears first on the gateway PATH. +3. pyATS reuses the owned stdio-to-loopback-HTTP bridge. A standalone, public HTTP daemon is not required. +4. A canary with a known synthetic tool is discovered by the selected runtime, using explicit transport where required. +5. Reruns preserve persona files, credentials, custom registrations and unrelated settings. Native exposure retains existing write approvals and tool restrictions. -**Independent test**: Each accepted report receives a reproduction that fails on the recorded baseline and passes after its scoped fix. +### US4 — Understand first-use readiness (P1) -**Acceptance scenarios**: +The final result distinguishes installed artifacts, registration, protocol/tool discovery, provider readiness and actual endpoint verification. -1. Given a new report, when triage is complete, its command, affected component/stage, expected behavior, actual error and known host/runtime versions are recorded; missing facts remain explicitly unknown. -2. Given a confirmed repository defect, when its fix is accepted, a meaningful regression reproduces the original failure and verifies the corrected result. -3. Given an issue already addressed by an existing PR, when the reports are compared, this spec links and coordinates that work instead of independently duplicating it. +1. Missing launchers, failed handshakes, empty/unexpected tool lists and a zero exit code with a failed structured probe cannot produce a tools-ready result. +2. Configured remote services whose credentials/connectivity were not checked are marked unverified. +3. Readiness is deterministic installer/runtime output, independent of the selected model's prose. +4. An optional operator-requested first-use canary verifies a harmless tool call and its provenance. A failed tool must yield an explicit unavailable result; fabricated network facts never count as acceptance evidence. No claim that prompts alone eliminate arbitrary model hallucinations. -### Edge cases to assess against actual reports +### US5 — Use local or remote Ollama correctly (P2) -- Fresh installs versus reruns with existing runtime configuration and credentials. -- Unsupported host or interpreter versus a repository defect. -- Partial installs or failed dependencies followed by misleading success. -- Issues already fixed in main or covered by pending installer work. +Readiness checks resolve the configured endpoint and exact model. A remote host does not require an Ollama executable on the NetClaw host. Report unreachable endpoints, missing models, incompatible API routing and unverified tool-call capability distinctly. No automatic pull, provider inference or live device operation is part of this specification's investigation. -These are triage considerations, not claims that new defects have been found. +## Functional requirements -## Requirements +- **FR-001**: Trace every report to its disposition and evidence; retain fixes already merged rather than reimplementing historical failures. +- **FR-002**: Share the Node compatibility rule between preflight and runtime installation. Validate the version range for the selected runtime target and document its source/date. +- **FR-003**: Make npm installation version-aware and offer a rootless path without silently changing a user's global npm policy. +- **FR-004**: Map all twelve reported components to an explicit access mode, executable/transport, credential requirements and verification method; a selected unsupported path must be reported. +- **FR-005**: Preserve per-component dependency isolation and recorded launcher bindings, including skill-based invocation. Do not purge or repurpose existing user-managed environments. +- **FR-006**: Check discovery through the selected runtime as well as direct MCP protocol tests; evaluate structured results and expected capabilities, not only exit codes. +- **FR-007**: Return failure for required install/registration/discovery failures; distinguish skipped, unverified and credential-dependent checks from success. Preserve per-run diagnostic logs without exposing secrets. +- **FR-008**: Resolve Ollama readiness from configured endpoint/model; distinguish local CLI integrations from a remote AI provider. +- **FR-009**: Preserve dotenv import, USER/persona/testbed files, custom launchers and existing approval controls. Native registration must not broaden write permissions. +- **FR-010**: Record supported platform/runtime versions and tests, plus explicit live/host boundaries. A release bump follows completed implementation, not this specification. -- **FR-001**: Record each supplied failure and its evidence in this spec before implementation. -- **FR-002**: Keep observed failures, hypotheses, upstream limitations and existing fixes distinct. -- **FR-003**: Define the affected platforms/components and acceptance criteria per confirmed issue; preserve unrelated operator configuration and the spec 146 dotenv behavior. -- **FR-004**: Use isolated temporary homes/runtimes and dummy credentials for reproduction where possible. Keep credentials, private topology and private logs out of repository artifacts. -- **FR-005**: Update research, plan and tasks with the selected solution before implementation; report relevant verification and untested host/live boundaries honestly. +## Scope and boundaries -## Success Criteria +Reported components: `pyats netbox servicenow nvd-cve subnet-calc wikipedia markmap drawio-rfc packet-buddy nmap gtrace tts`; CML is the registered comparison case. -- Every accepted fix has a traceable report, baseline, expected behavior and regression evidence. -- Relevant installer regressions and repository checks pass without weakening assertions. -- Existing credentials and unrelated runtime configuration remain intact. -- Source release metadata advances once only when this spec's eventual fix scope is completed; this draft does not advance the version. +Nick's PR #287 is already merged: host/component preflight, Go prerequisites, Python bounds and Zabbix cwd repair are retained and regression-tested. Spec 133 and related changes already address REPO_ROOT coupling, shared installer dependency targets and native registration merging/binding. Dotenv preservation is spec 146. -## Assumptions and scope boundary +The full log archive mentioned in the pasted conversation was not supplied. Six of the eight failed macOS components are not identified here. Exact original OpenClaw/npm versions and CML spawn diagnostics remain incomplete. No allegation of a general OpenClaw MCP defect is treated as proven. -No new failure details have been supplied yet. The owner explicitly said they will send them for this spec. This draft authorizes intake and planning, not an invented installer rewrite or speculative package/runtime changes. +## Success criteria -Spec 146 is the preceding dotenv/onboarding fix. PR #287 separately proposes host preflight and Zabbix repairs; its status must be rechecked when reports arrive. No merge of that PR is part of this draft. +- Every report has a recorded current disposition; every remaining fix has a failing baseline reproduction and passing regression. +- All twelve reported access paths are accounted for; required selected paths cannot be silently omitted. +- Supported native stdio and explicit Streamable HTTP fixture discovery succeeds through the tested runtime; failed/empty discovery cannot look ready. +- macOS arm64 and Linux fixture coverage plus applicable real-host evidence are recorded honestly. +- Relevant installer/unit/declaration checks pass without weakening assertions or changing real operator credentials. diff --git a/specs/147-installer-followups/tasks.md b/specs/147-installer-followups/tasks.md index 1ff38cdb..88944a34 100644 --- a/specs/147-installer-followups/tasks.md +++ b/specs/147-installer-followups/tasks.md @@ -1,14 +1,38 @@ -# Tasks: Installer follow-ups +# Tasks: Installer and first-use reliability -## Intake +## Intake and baseline -- [x] T001 Close the preceding dotenv fix through its authorized PR and merge. -- [x] T002 Create the new numbered spec and branch from updated main. -- [x] T003 Record existing related work and the evidence needed for triage. -- [ ] T004 Receive the owner's new installer failure reports. -- [ ] T005 Reproduce and classify each accepted report; coordinate overlap with PR #287. -- [ ] T006 Define concrete acceptance criteria and update research, plan and implementation tasks. +- [x] T001 Close spec 146 through PR #288 and start spec 147. +- [x] T002 Receive macOS/Hermes and Linux/OpenClaw reports. +- [x] T003 Pull main, verify Nick's PR #287 is merged and integrate it into this branch. +- [x] T004 Classify every supplied issue in reports.md against current code. +- [x] T005 Reproduce Node policy and native registration gaps; run isolated real-runtime transport probes. +- [x] T006 Verify relevant merged fixes (113 focused tests); define scope and design. -## Implementation gate +## Phase A — Runtime compatibility/bootstrap -Implementation, regression commands and release tasks will be specified after T004–T006. No speculative code changes or additional source-release bump are part of this draft. +- [ ] T007 Add shared target-aware Node compatibility rule and boundary fixtures. +- [ ] T008 Add version-aware narrow npm lifecycle-script handling and rootless install recovery. +- [ ] T009 Ensure runtime-install/executable failure propagation and clear Python/uv guidance. + +## Phase B — Component access + +- [ ] T010 Declare access paths and expected capabilities for all twelve reported components. +- [ ] T011 Bind supported native registrations and skill launchers to managed runtimes. +- [ ] T012 Verify pyATS/CML and representative utility discovery without bypassing approval controls. +- [ ] T013 Preserve operator state and reconcile inventory/registration/documentation surfaces. + +## Phase C — First-use readiness + +- [ ] T014 Add structured readiness stages and bounded discovery with expected-tool checks. +- [ ] T015 Check configured local/remote Ollama endpoint/model readiness and document deferred/live checks. +- [ ] T016 Add evidence-backed synthetic canary and unavailable-tool negative cases. +- [ ] T017 Reproduce any residual CML failure on matching versions before assigning upstream cause. + +## Completion + +- [ ] T018 Run appropriate unit/installer contracts and declaration/reconciliation checks. +- [ ] T019 Record real-host/live acceptance boundaries and operator recovery instructions. +- [ ] T020 Prepare one patch release proposal after implementation completes. + +The six unnamed macOS component failures and exact original CML/runtime diagnostics remain pending evidence. They do not prevent work on the confirmed scoped defects; they are not invented acceptance cases. diff --git a/specs/147-installer-followups/transport-evidence.json b/specs/147-installer-followups/transport-evidence.json new file mode 100644 index 00000000..e0b9f846 --- /dev/null +++ b/specs/147-installer-followups/transport-evidence.json @@ -0,0 +1,52 @@ +{ + "runtime": "OpenClaw 2026.7.1-2", + "node": "v24.19.0", + "fixture_only": true, + "cases": [ + { + "name": "stdio", + "returncode": 0, + "fixture_tool_discovered": true, + "http_methods": [], + "stdio_methods": [ + "initialize", + "notifications/initialized", + "tools/list" + ] + }, + { + "name": "http-explicit", + "returncode": 0, + "fixture_tool_discovered": true, + "http_methods": [ + { + "verb": "POST", + "method": "initialize" + }, + { + "verb": "POST", + "method": "notifications/initialized" + }, + { + "verb": "GET", + "method": null + }, + { + "verb": "POST", + "method": "tools/list" + } + ] + }, + { + "name": "http-url-only", + "returncode": 0, + "fixture_tool_discovered": false, + "http_methods": [ + { + "verb": "GET", + "method": null + } + ] + } + ] +} diff --git a/specs/147-installer-followups/verification.md b/specs/147-installer-followups/verification.md index e5fa6961..0d8e3060 100644 --- a/specs/147-installer-followups/verification.md +++ b/specs/147-installer-followups/verification.md @@ -1,7 +1,52 @@ -# Verification: Draft intake +# Verification — investigation on 2026-10-10 -This spec contains planning artifacts only. No new installer defect or fix has been claimed or tested. +## Baseline -The preceding dotenv patch's verification is recorded in [spec 146](../146-dotenv-onboarding-preservation/verification.md) and [PR #288](https://github.com/automateyournetwork/netclaw/pull/288). This does not establish acceptance for future reports. +Local main fast-forwarded to `b4334bf255cd53aecd2e81f51abe63deea53f1df` and merged into `147-installer-followups` as `cf12ec1`. Includes PR #287's host preflight/Zabbix fixes and PR #288's dotenv fix. No new production installer implementation is claimed here. -Draft validation on 2026-10-10: `python3 scripts/verify-spec-artifacts.py` passed (131 specs, four historical exceptions); `git diff --check` passed; `python3 scripts/prepare-release.py --check` passed for the unchanged 1.6.1 baseline. No new source-release bump is warranted for intake documents. New failure reproductions and regression results will be recorded here when the owner supplies the reports. +## Existing regressions + +Command: + +```bash +.contract-test-envs/unit/bin/python -m pytest -q \ + tests/unit/test_installer_preflight.py \ + tests/unit/test_installer_component_paths.py \ + tests/unit/test_installer_python_runtime.py \ + tests/unit/test_installer_mcp_config.py \ + tests/unit/test_n2n_install_preservation.py \ + tests/unit/test_dotenv_onboarding.py +``` + +**PASS: 113 tests in 11.84 seconds.** Includes Zabbix cwd, independent component paths, Python upper bounds, prerequisite aggregation, runtime records, config preservation and dotenv ordering. Fixtures do not prove a live Forward/CML/Zabbix deployment. + +## Remaining defect evidence + +- [Node preflight](node-preflight-evidence.json): current evaluator accepts versions incompatible with current upstream OpenClaw's engine range. These results describe the failing baseline, not desired behavior. +- [Registration generator](registration-evidence.json): selected twelve components produce zero native entries, exit 0. Skill-based paths exist for some; no inference that every mechanism is absent. +- [Transport probes](transport-evidence.json): installed OpenClaw 2026.7.1-2 with Node 24.19.0, synthetic servers, isolated temporary HOME/config. Stdio and explicit streamable-http discover the expected fixture tool. URL-only HTTP sends GET and exposes none despite exit 0. + +Repeat transport diagnosis with explicit installed binary paths: + +```bash +python3 specs/147-installer-followups/reproduce-transports.py \ + --node /absolute/path/to/node \ + --cli /absolute/path/to/openclaw/openclaw.mjs \ + --runtime-label 'the tested OpenClaw version' \ + --output /tmp/netclaw-transport-evidence.json +``` + +The fixture accepts initialization and discovery only. It starts its own temporary loopback HTTP server; it never calls a model or device and does not alter the installed runtime or gateway. + +## Limits + +The original log archive is unavailable; six macOS component failures are unnamed. The original CML server, Ollama endpoint, runtime config and precise command/environment are not reproduced. Native CLI probe success is not a gateway agent-turn or model grounding test. No secrets, real provider calls, real package installs, service restarts or device operations occurred. + +## Specification checks + +- `python3 scripts/verify-spec-artifacts.py`: PASS, 131 specs checked with four legacy exceptions. +- Reproduction helper compilation and `--help`: PASS. +- Final portable reproduction helper rerun: completed all three cases and reproduced the discovery results above. +- `git diff --check`: PASS. + +Implementation and live acceptance remain pending tasks, not passing results. From 5a027edef64cca64f7af43e70dca9caa7d28e2ff Mon Sep 17 00:00:00 2001 From: automateyournetwork Date: Sat, 10 Oct 2026 08:06:09 -0400 Subject: [PATCH 3/5] fix(installer): bind selected tools and verify first-use readiness --- .env.example | 5 + CHANGELOG.md | 4 + README.md | 6 +- SOUL.md | 8 + TOOLS.md | 9 + VERSION | 2 +- config/installer-access.json | 162 ++++++++++ config/installer-runtime.json | 10 + config/openclaw.json | 143 +++++++++ config/python-components/servicenow.txt | 2 + docs/INSTALLER-READINESS.md | 120 ++++++++ docs/releases/1.6.2.md | 41 +++ scripts/component-launch.py | 125 ++++++++ scripts/install-mcp-config.py | 13 + scripts/install.sh | 31 +- scripts/installer-preflight.py | 14 +- scripts/installer-readiness.py | 281 ++++++++++++++++++ scripts/lib/install-steps.sh | 86 ++---- scripts/lib/preflight.sh | 2 +- scripts/lib/runtime-install.sh | 30 ++ scripts/mcp-call.py | 35 ++- scripts/mcp-probe.py | 104 +++++++ scripts/runtime-policy.py | 54 ++++ scripts/verify-catalog-coverage.py | 14 +- scripts/verify-inventory-counts.py | 13 - specs/147-installer-followups/blog-draft.md | 22 ++ .../implementation-probes.json | 137 +++++++++ specs/147-installer-followups/plan.md | 2 +- specs/147-installer-followups/reports.md | 10 + .../reproduce-installed-components.py | 49 +++ specs/147-installer-followups/research.md | 23 ++ specs/147-installer-followups/spec.md | 2 +- specs/147-installer-followups/tasks.md | 30 +- specs/147-installer-followups/verification.md | 20 +- tests/unit/test_installer_exit_status.py | 3 +- tests/unit/test_installer_followups.py | 127 ++++++++ tests/unit/test_installer_preflight.py | 4 +- .../test_installer_prerequisite_isolation.py | 3 +- .../test_installer_python_prerequisites.py | 4 +- tests/unit/test_installer_readiness.py | 126 ++++++++ workspace/skills/drawio-diagram/SKILL.md | 2 + .../skills/gtrace-path-analysis/SKILL.md | 2 + workspace/skills/markmap-viz/SKILL.md | 2 + workspace/skills/netbox-reconcile/SKILL.md | 2 + workspace/skills/nmap-network-scan/SKILL.md | 2 + workspace/skills/nvd-cve/SKILL.md | 2 + workspace/skills/packet-analysis/SKILL.md | 2 + workspace/skills/pyats-network/SKILL.md | 2 + workspace/skills/rfc-lookup/SKILL.md | 2 + .../servicenow-change-workflow/SKILL.md | 2 + workspace/skills/subnet-calculator/SKILL.md | 2 + workspace/skills/wikipedia-research/SKILL.md | 2 + 52 files changed, 1778 insertions(+), 122 deletions(-) create mode 100644 config/installer-access.json create mode 100644 config/installer-runtime.json create mode 100644 config/python-components/servicenow.txt create mode 100644 docs/INSTALLER-READINESS.md create mode 100644 docs/releases/1.6.2.md create mode 100644 scripts/component-launch.py create mode 100644 scripts/installer-readiness.py create mode 100644 scripts/lib/runtime-install.sh create mode 100644 scripts/mcp-probe.py create mode 100644 scripts/runtime-policy.py create mode 100644 specs/147-installer-followups/blog-draft.md create mode 100644 specs/147-installer-followups/implementation-probes.json create mode 100644 specs/147-installer-followups/reproduce-installed-components.py create mode 100644 tests/unit/test_installer_followups.py create mode 100644 tests/unit/test_installer_readiness.py diff --git a/.env.example b/.env.example index 891fc8d5..2de7fba3 100644 --- a/.env.example +++ b/.env.example @@ -1038,3 +1038,8 @@ EQUINIX_SERVICENOW_PASSWORD= # FASTMCP_DOCKET_URL=redis://localhost:6379/0 # FASTMCP_TASKS_ENCRYPTION_KEY= # Arguments/results also require backend access controls; see docs/MCP-TASKS.md. + +# Installer-managed launch bindings (normally written automatically by install.sh). +# Paths only; credentials remain in the runtime .env. Explicit process env takes precedence. +# NETCLAW_RUNTIME_ROOT=/absolute/path/to/runtime-home/python-runtimes +# NETCLAW_RUNTIME_ENV=/absolute/path/to/runtime-home/.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab5aacc..63913074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ NetClaw source releases use `1.x.y`. Feature specs increment the minor version and reset the patch; fixes, documentation and maintenance increment the patch. Every release records its numbered specs. Component and mobile distribution versions are independent. +## [1.6.2] - 2026-10-10 + +Spec [147-installer-followups](specs/147-installer-followups/spec.md). See [release notes](docs/releases/1.6.2.md). + ## [1.6.1] - 2026-10-09 - Import filled checkout dotenv settings before runtime onboarding while preserving existing runtime credentials; add a previewable repair command. diff --git a/README.md b/README.md index 6ef71d11..5f9b9198 100644 --- a/README.md +++ b/README.md @@ -1510,7 +1510,7 @@ NetClaw's WebEx support is **bidirectional**: ### Prerequisites - A **Cisco WebEx account** (free or paid) -- **Node.js 18+** and **OpenClaw 2026.3.x** (already installed if you ran `install.sh`) +- **Node.js 24.16+ (24.x) or 26.1+** and a compatible **OpenClaw** (already installed if you ran `install.sh`) - **ngrok** (free tier works) — only required for local/development setups where the gateway is not publicly reachable ### Step 1: Create a WebEx Bot @@ -2752,7 +2752,7 @@ netclaw/ The installer is an interactive TUI (pure bash — works over SSH, degrades gracefully without a TTY). Install logic lives in `scripts/lib/` (`common.sh` helpers, `tui.sh` interface, `catalog.sh` component catalog + profiles, `install-steps.sh` one install function per component). 1. **Pick your components** — choose an install profile (Recommended, Cisco, Multivendor, Cloud, Security, Labs, Observability, Minimal, Everything) or hand-pick from all 72 components in a categorized multi-select checklist. Re-running the installer preselects what you already have. -2. **Checks prerequisites** — Node.js >= 18, Python 3, pip3, git, npx — offers to run the install commands for anything missing (apt/dnf/yum/pacman/apk/brew), and handles PEP 668 externally-managed Pythons so pip installs work on modern distros +2. **Checks prerequisites** — runtime-specific Node compatibility, Python/component bounds, npm, git and selected uv/Go/Docker/native tools before mutation. Python dependencies use isolated environments; system package protections remain enabled. 3. **Installs OpenClaw** — `npm install -g openclaw@latest` 4. **Imports filled `.env` settings, then runs OpenClaw onboard** — preserves existing runtime values; configures AI provider, gateway, channels and daemon service; stops on wizard failure, then verifies the gateway service actually started (systemd unit / LaunchAgent / port probe) and offers a retry with diagnostics if it didn't 5. **Installs the selected MCP servers** — each component clones/pip-installs/pulls exactly what it needs (`./scripts/install.sh --list` shows every component and what it provides) @@ -2793,7 +2793,7 @@ The `%ENV{NETCLAW_PASSWORD}` syntax pulls credentials from environment variables ## Prerequisites -- Node.js >= 18 (>= 22 recommended for OpenClaw) +- OpenClaw: Node.js **>=24.16.0 <25 or >=26.1.0**; Node 26 is recommended. Use a Node version manager if your distro ships an older release. Hermes keeps a separate Node floor for its Node-based components. See [installer readiness](docs/INSTALLER-READINESS.md). - Python 3.x with pip3 - git - Network devices accessible via SSH (for pyATS) diff --git a/SOUL.md b/SOUL.md index beab1acb..4791da03 100644 --- a/SOUL.md +++ b/SOUL.md @@ -740,6 +740,14 @@ Every session starts with a GAIT branch and ends with a GAIT log. This is not op If you forget GAIT, the session has no record. That is unacceptable in a production network. +### First-use readiness + +Read the installer readiness report before claiming a selected integration works. +Artifact presence, MCP discovery, endpoint access and a successful agent tool call +are different evidence. If a tool is unavailable, say so and name the failing stage; +never fill missing device or lab data with plausible values. Native registrations +retain the same production approval rules as skills. See [installer readiness](docs/INSTALLER-READINESS.md). + ### Gathering State Before answering any question about the network, **always gather real data first**. Never guess. Use the pyats-network skill to run show commands. Genie parsers return structured JSON for 100+ IOS-XE commands. diff --git a/TOOLS.md b/TOOLS.md index 0fe97ed4..15f1e7e3 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -1000,3 +1000,12 @@ Build passes; 358/359 HUD tests pass (remaining known 127.0.0.2 Mac fixture). Qwen3-TTS Base via MLX Audio is the researched own-voice candidate, not installed or benchmarked. Keep future recordings in ~/.openclaw/pal/voices, not the public asset directory. Research/license sources are in spec144/research.md. + +## Installer access and readiness (spec 147) + +The twelve reported skill-only components now have native registrations (thirteen +servers because Draw.io and RFC are separate). `config/installer-access.json` and +`scripts/component-launch.py` bind both native access and recognized MCP_CALL skill +commands to the installed interpreter/source/transport. No new write permission is +granted. `logs/install/run-*/readiness.json` separates artifacts, registration, +discovery and unverified endpoint/agent operations. See [operator guidance](docs/INSTALLER-READINESS.md). diff --git a/VERSION b/VERSION index 9c6d6293..fdd3be6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.1 +1.6.2 diff --git a/config/installer-access.json b/config/installer-access.json new file mode 100644 index 00000000..2ed04792 --- /dev/null +++ b/config/installer-access.json @@ -0,0 +1,162 @@ +{ + "schema_version": 1, + "components": { + "pyats": { + "servers": [ + "pyats-mcp" + ], + "script": "scripts/pyats-stdio.py", + "script_env": "PYATS_MCP_SCRIPT", + "bridge": true, + "required_env": [ + "PYATS_TESTBED_PATH" + ], + "expected_tools": [ + "pyats_list_devices" + ] + }, + "netbox": { + "servers": [ + "netbox-mcp" + ], + "module": "netbox_mcp_server.server", + "pythonpath": "mcp-servers/netbox-mcp-server/src", + "script": "mcp-servers/netbox-mcp-server/src/netbox_mcp_server/server.py", + "script_env": "NETBOX_MCP_SCRIPT", + "args": [ + "--transport", + "stdio" + ], + "required_env": [ + "NETBOX_URL", + "NETBOX_TOKEN" + ], + "expected_tools": [ + "netbox_get_objects" + ] + }, + "servicenow": { + "servers": [ + "servicenow-mcp" + ], + "module": "servicenow_mcp.cli", + "pythonpath": "mcp-servers/servicenow-mcp/src", + "script": "mcp-servers/servicenow-mcp/src/servicenow_mcp/cli.py", + "script_env": "SERVICENOW_MCP_SCRIPT", + "required_env": [ + "SERVICENOW_INSTANCE_URL" + ], + "expected_tools": [ + "list_tool_packages" + ] + }, + "nvd-cve": { + "servers": [ + "nvd-cve-mcp" + ], + "module": "mcp_nvd.main", + "pythonpath": "mcp-servers/mcp-nvd", + "script": "mcp-servers/mcp-nvd/mcp_nvd/main.py", + "script_env": "NVD_MCP_SCRIPT", + "args": [ + "--transport", + "stdio" + ], + "expected_tools": [ + "search_cve" + ] + }, + "subnet-calc": { + "servers": [ + "subnet-calc-mcp" + ], + "script": "mcp-servers/subnet-calculator-mcp/servers/subnetcalculator_mcp.py", + "script_env": "SUBNET_MCP_SCRIPT", + "expected_tools": [ + "subnet_calculator" + ] + }, + "wikipedia": { + "servers": [ + "wikipedia-mcp" + ], + "script": "mcp-servers/Wikipedia_MCP/main.py", + "script_env": "WIKIPEDIA_MCP_SCRIPT", + "expected_tools": [ + "search_pages" + ] + }, + "markmap": { + "servers": [ + "markmap-mcp" + ], + "command": "node", + "script": "mcp-servers/markmap_mcp/markmap-mcp/dist/index.js", + "script_env": "MARKMAP_MCP_SCRIPT", + "expected_tools": [ + "markmap_generate" + ] + }, + "drawio-rfc": { + "servers": [ + "drawio-mcp", + "rfc-mcp" + ], + "variants": { + "drawio-mcp": [ + "npx", + "-y", + "@drawio/mcp" + ], + "rfc-mcp": [ + "npx", + "-y", + "@mjpitz/mcp-rfc" + ] + }, + "expected_tools": [] + }, + "packet-buddy": { + "servers": [ + "packet-buddy-mcp" + ], + "script": "mcp-servers/packet-buddy-mcp/server.py", + "script_env": "PACKET_BUDDY_MCP_SCRIPT", + "expected_tools": [ + "list_pcaps" + ] + }, + "nmap": { + "servers": [ + "nmap-mcp" + ], + "script": "mcp-servers/nmap-mcp/server.py", + "script_env": "NMAP_MCP_SCRIPT", + "cwd": "mcp-servers/nmap-mcp", + "expected_tools": [] + }, + "gtrace": { + "servers": [ + "gtrace-mcp" + ], + "command": "gtrace", + "command_env": "GTRACE_MCP_BIN", + "args": [ + "mcp" + ], + "expected_tools": [ + "traceroute" + ] + }, + "tts": { + "servers": [ + "tts-mcp" + ], + "script": "mcp-servers/tts-mcp/server.py", + "script_env": "TTS_MCP_SCRIPT", + "expected_tools": [ + "list_voices" + ] + } + } +} diff --git a/config/installer-runtime.json b/config/installer-runtime.json new file mode 100644 index 00000000..eb82a8b7 --- /dev/null +++ b/config/installer-runtime.json @@ -0,0 +1,10 @@ +{ + "openclaw_package": "openclaw@latest", + "node_ranges": { + "openclaw": [{"min": [24, 16, 0], "max_exclusive": [25, 0, 0]}, {"min": [26, 1, 0]}], + "hermes": [{"min": [18, 0, 0]}] + }, + "node_descriptions": {"openclaw": ">=24.16.0 <25 or >=26.1.0", "hermes": ">=18 (Node-based components)"}, + "checked": "2026-10-10", + "source": "https://docs.openclaw.ai/install/node-compatibility" +} diff --git a/config/openclaw.json b/config/openclaw.json index 80701528..2c4a60e6 100644 --- a/config/openclaw.json +++ b/config/openclaw.json @@ -1334,6 +1334,149 @@ "EQUINIX_SERVICENOW_USERNAME": "${EQUINIX_SERVICENOW_USERNAME:-}", "EQUINIX_SERVICENOW_PASSWORD": "${EQUINIX_SERVICENOW_PASSWORD:-}" } + }, + "pyats-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "pyats", + "--server", + "pyats-mcp" + ], + "transport": "stdio" + }, + "netbox-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "netbox", + "--server", + "netbox-mcp" + ], + "transport": "stdio" + }, + "servicenow-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "servicenow", + "--server", + "servicenow-mcp" + ], + "transport": "stdio" + }, + "nvd-cve-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "nvd-cve", + "--server", + "nvd-cve-mcp" + ], + "transport": "stdio" + }, + "subnet-calc-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "subnet-calc", + "--server", + "subnet-calc-mcp" + ], + "transport": "stdio" + }, + "wikipedia-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "wikipedia", + "--server", + "wikipedia-mcp" + ], + "transport": "stdio" + }, + "markmap-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "markmap", + "--server", + "markmap-mcp" + ], + "transport": "stdio" + }, + "drawio-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "drawio-rfc", + "--server", + "drawio-mcp" + ], + "transport": "stdio" + }, + "rfc-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "drawio-rfc", + "--server", + "rfc-mcp" + ], + "transport": "stdio" + }, + "packet-buddy-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "packet-buddy", + "--server", + "packet-buddy-mcp" + ], + "transport": "stdio" + }, + "nmap-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "nmap", + "--server", + "nmap-mcp" + ], + "transport": "stdio" + }, + "gtrace-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "gtrace", + "--server", + "gtrace-mcp" + ], + "transport": "stdio" + }, + "tts-mcp": { + "command": "python3", + "args": [ + "-u", + "scripts/component-launch.py", + "tts", + "--server", + "tts-mcp" + ], + "transport": "stdio" } } } diff --git a/config/python-components/servicenow.txt b/config/python-components/servicenow.txt new file mode 100644 index 00000000..0717c00b --- /dev/null +++ b/config/python-components/servicenow.txt @@ -0,0 +1,2 @@ +# ServiceNow uses the low-level SDK1 Server API. Do not share with MCP2 integrations. +mcp>=1.13,<2 diff --git a/docs/INSTALLER-READINESS.md b/docs/INSTALLER-READINESS.md new file mode 100644 index 00000000..385d5583 --- /dev/null +++ b/docs/INSTALLER-READINESS.md @@ -0,0 +1,120 @@ +# Install and first-use readiness + +NetClaw's installer checks host prerequisites, installs isolated component runtimes, +merges selected MCP registrations, and then checks tool discovery. A file on disk is +not evidence that a gateway can call a tool. The final private +`/logs/install/run-*/readiness.json` records these stages separately. + +## Supported bootstrap + +OpenClaw installation currently requires Node `>=24.16.0 <25 || >=26.1.0`; prefer +Node 26 with your existing version manager. This is the declared target contract in +`config/installer-runtime.json`, checked against [upstream](https://docs.openclaw.ai/install/node-compatibility) +on 2026-10-10. Hermes retains its own Node requirement for Node-based components. +The installer checks the same rule in preflight and installation. + +Use Python 3.12 for the broadest declared component coverage. CPython 3.14 runs on +Apple Silicon; some selected packages have tighter Python or architecture bounds. +On Homebrew, install compatible prerequisites and select their executables before +retrying, for example: + +```bash +brew install python@3.12 uv +export PATH="$(brew --prefix python@3.12)/libexec/bin:$PATH" +export NETCLAW_PY="$(brew --prefix python@3.12)/bin/python3.12" +./scripts/install.sh --profile recommended --preflight +``` + +`uv` supplies managed pyATS/GAIT environments; no specific uv release is pinned. +Forward additionally requires Go 1.25+, CGO and a working C compiler/Apple Command +Line Tools. `--all` includes Linux-only components; inspect preflight on macOS and +select compatible components. The prerequisite report lists all known blockers. + +For npm 11.16+ the installer uses the narrow `--allow-scripts=openclaw` option; +older npm omits it. npm 11.16 warnings alone do not establish that scripts were +blocked; npm 12 changes that policy. See [OpenClaw installation](https://docs.openclaw.ai/install). +An unwritable global prefix selects `--prefix "$HOME/.local"` for this installation +without rewriting npm configuration. Keep that directory's `bin` on PATH in new +shells. Failures stop installation; the installer does not retry npm under sudo. + +## Tool access + +These formerly skill-only integrations now get selected native MCP registrations: + +| Component | Native server(s) | Launch path | +| --- | --- | --- | +| pyats | pyats-mcp | Existing stdio bridge → managed private loopback HTTP child | +| netbox | netbox-mcp | Recorded Python, `netbox_mcp_server.server`, source `src` path | +| servicenow | servicenow-mcp | Recorded isolated SDK1 Python, `servicenow_mcp.cli` | +| nvd-cve | nvd-cve-mcp | Recorded Python, `mcp_nvd.main --transport stdio` | +| subnet-calc | subnet-calc-mcp | Recorded Python, calculator script | +| wikipedia | wikipedia-mcp | Recorded Python, Wikipedia script | +| markmap | markmap-mcp | Node, installed Markmap build | +| drawio-rfc | drawio-mcp, rfc-mcp | Declared npx packages | +| packet-buddy | packet-buddy-mcp | Recorded Python, Packet Buddy script | +| nmap | nmap-mcp | Recorded Python, scanner source directory | +| gtrace | gtrace-mcp | Installed `gtrace mcp` binary | +| tts | tts-mcp | Recorded Python, speech script | + +`config/installer-access.json` declares paths, required settings and expected tools. +`scripts/component-launch.py` is an executable launcher, not a new MCP server: it +passes the existing protocol through unchanged. Native registrations and recognized +legacy `MCP_CALL` skill commands use this same launcher. Custom commands, runtime +policy, approval fields, credentials and existing persona files are preserved. +Registration grants no write approval; production change control still applies. + +Skills can also call the component explicitly: + +```bash +python3 "$MCP_CALL" --component netbox --list-tools +python3 "$MCP_CALL" --component subnet-calc subnet_calculator '{"cidr":"192.0.2.0/30"}' +``` + +The second command is a harmless local first-use canary. Verify the tool result +contains the requested CIDR. A model-generated table is not tool evidence. If a +call fails, report the failure; never replace unavailable device data with examples. +This canary does not prove a model will always ground its answers correctly. + +## Read the result and retry + +- `failed`: install, launch, registration, discovery or configured Ollama check failed; + the installer exits nonzero. Inspect the named component and its per-run log. +- `configuration_required`: supply the listed settings or resolve a disabled entry; + the component is not ready. No credential value is included in the report. +- `discovery_verified`: expected tools were exposed through the tested access path. + Actual device/service operations and a gateway agent turn remain unverified. +- `unverified`: a check was not performed or a component has a separate access path. + Remote declarations and filesystem checks alone never establish connectivity. + +OpenClaw discovery uses its native CLI and checks structured server/tool results, +not just exit status. Hermes currently checks its saved registration and performs +direct stdio discovery; agent-visible Hermes discovery remains explicitly unverified. +HTTP-only Hermes probes remain unverified. Public HTTP pyATS daemons are unnecessary. +For custom HTTP registrations, specify the actual transport (`streamable-http` or +SSE); do not infer it merely from a URL. + +Retry failed installation steps with the exact runtime and components you selected: + +```bash +./scripts/install.sh --runtime openclaw --add "pyats netbox cml" +``` + +After editing settings, rerun checks without package installation: + +```bash +python3 scripts/installer-readiness.py --runtime openclaw \ + --runtime-root "$HOME/.openclaw/python-runtimes" \ + --config "$HOME/.openclaw/openclaw.json" --components "pyats netbox cml" \ + --output "$HOME/.openclaw/readiness.json" --probe --check-provider +``` + +For Hermes use its home/config.yaml and `--runtime hermes`; check AI-provider +readiness with Hermes itself. Local/remote Ollama on OpenClaw is checked using the +configured endpoint and model catalog/capabilities. A remote Ollama host requires no +local Ollama executable. Checks never pull models, generate text, or call device +tools. Advertised tool capability is not a completed agent turn. + +The original reports did not include six named macOS failures or a reproducible CML +spawn trace. Those remain evidence gaps. Use a harmless authorized read against your +actual target after discovery; neither synthetic fixtures nor CI certify every +external service or package/OS combination. diff --git a/docs/releases/1.6.2.md b/docs/releases/1.6.2.md new file mode 100644 index 00000000..94b60e17 --- /dev/null +++ b/docs/releases/1.6.2.md @@ -0,0 +1,41 @@ +# NetClaw 1.6.2 + +Spec: [147-installer-followups](../../specs/147-installer-followups/spec.md) + +## Changes + +Installer follow-up from the macOS and Linux first-use reports. Builds on Nick's +merged host-preflight/Zabbix repair and the dotenv preservation fix. + +- Enforces one runtime-specific Node policy and npm-version-aware lifecycle script + handling. Uses a user-owned prefix when the global prefix is unwritable; runtime + installation/executable failures stop the installer. +- Registers thirteen native MCP servers for the twelve reported missing components. + Native and recognized skill calls share recorded Python interpreters, module + paths and explicit stdio transport. pyATS keeps its private HTTP bridge. +- Reports artifacts, registration, discovery, configuration requirements and provider + readiness separately. Empty/failed probes cannot masquerade as working tools. +- Checks configured local/remote Ollama model availability and advertised tool + support without requiring a local CLI for remote use or downloading models. + +## Upgrade and compatibility + +Pull main and rerun `./scripts/install.sh --add ""` with the same +runtime selection. Existing credentials, custom registrations, persona files and +approval controls remain preserved. Old global packages are not purged. Custom +launch conflicts stop deployment for manual reconciliation. + +OpenClaw requires Node `>=24.16.0 <25 || >=26.1.0` for the declared current target; +Hermes retains its own component Node floor. See [readiness and recovery](../INSTALLER-READINESS.md). +No tag or published release is created by preparing these source release notes. + +## Validation and known limitations + +[Verification](../../specs/147-installer-followups/verification.md) records unit, +reconciliation, actual native discovery and a local calculator canary. Test package +installation used a disposable environment; no operator gateway or device changed. + +Original live CML/pyATS access, the six unnamed macOS failures and arbitrary model +answer grounding remain unverified. Hermes native agent discovery is not certified; +its direct stdio probe is labelled accordingly. A successful tool listing proves +neither endpoint credentials nor a completed agent turn. diff --git a/scripts/component-launch.py b/scripts/component-launch.py new file mode 100644 index 00000000..d0df4bd3 --- /dev/null +++ b/scripts/component-launch.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Launch a declared integration in its installed environment (no shell, no installs). + +Used by native registrations and mcp-call, so both use the same interpreter, +source path and transport. This passes MCP unchanged; it grants no permissions. +""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +import sys + +ROOT = Path(__file__).resolve().parents[1] +CONTRACT = json.loads((ROOT / 'config/installer-access.json').read_text())['components'] + + +def runtime_root(env): + home = env.get('OPENCLAW_STATE_DIR') or env.get('OPENCLAW_HOME') or str(Path.home() / '.openclaw') + if env.get('NETCLAW_RUNTIME') == 'hermes': + home = env.get('HERMES_HOME', str(Path.home() / '.hermes')) + return Path(env.get('NETCLAW_RUNTIME_ROOT', str(Path(home) / 'python-runtimes'))) + + +def environment(env): + result = dict(env) + path = Path(env.get('NETCLAW_RUNTIME_ENV', str(runtime_root(env).parent / '.env'))) + if path.is_file(): + spec = importlib.util.spec_from_file_location('launch_dotenv', ROOT / 'scripts/write-env.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + # Literal parsing only; explicit runtime/entry env wins over dotenv. + for key, value in module.values(path.read_text()).items(): + result.setdefault(key, value) + return result + + +def resolve(component, server=None, env=None, repo=ROOT): + env = environment(os.environ if env is None else env) + rule = CONTRACT[component] + server = server or rule['servers'][0] + if server not in rule['servers']: + raise ValueError('Server does not belong to the selected component') + cwd = repo / rule.get('cwd', '.') + if not cwd.is_dir(): + raise ValueError(f'{component}: source directory missing; rerun install.sh --add {component}') + if 'variants' in rule: + parts = list(rule['variants'][server]) + else: + script = env.get(rule.get('script_env', '')) or (str(repo / rule['script']) if 'script' in rule else None) + if script and not Path(script).is_file(): + raise ValueError(f'{component}: server script missing; rerun install.sh --add {component}') + command = rule.get('command') + if not command: + if rule.get('bridge'): + command = sys.executable + env.setdefault('PYATS_VENV', str(runtime_root(env).parent / 'pyats-venv')) + else: + record = runtime_root(env) / 'records' / component + if not record.is_file(): + raise ValueError(f'{component}: no installed interpreter record; rerun install.sh --add {component}') + command = record.read_text().strip() + if not Path(command).is_absolute() or not os.access(command, os.X_OK): + raise ValueError(f'{component}: recorded interpreter unavailable; rerun install.sh --add {component}') + env['PATH'] = str(Path(command).parent) + os.pathsep + env.get('PATH', os.defpath) + else: + command = env.get(rule.get('command_env', '')) or command + parts = [command] + # A custom script path is an operator override; never replace it with + # the default module. Standard source modules need their src on path. + default_script = str(repo / rule['script']) if 'script' in rule else None + if 'module' in rule and script == default_script: + parts += ['-u', '-m', rule['module']] + elif script: + parts += (['-u'] if not rule.get('command') else []) + [script] + parts += rule.get('args', []) + if rule.get('pythonpath'): + source = str(repo / rule['pythonpath']) + env['PYTHONPATH'] = source + (os.pathsep + env['PYTHONPATH'] if env.get('PYTHONPATH') else '') + executable = shutil.which(parts[0], path=env.get('PATH', os.defpath)) + if not executable: + raise ValueError(f'{component}: launcher unavailable; rerun install.sh --add {component}') + parts[0] = executable + return parts, env, str(cwd) + + +def legacy_component(parts, env, repo=ROOT): + """Recognize only exact declared script/module/command invocations.""" + for component, rule in CONTRACT.items(): + if 'variants' in rule: + for server, command in rule['variants'].items(): + if parts == command: + return component, server + elif rule.get('command') == 'gtrace' and parts == ['gtrace', 'mcp']: + return component, rule['servers'][0] + elif rule.get('script'): + paths = {str(repo / rule['script'])} + if env.get(rule.get('script_env', '')): + paths.add(env[rule['script_env']]) + tail = parts[1:] + if tail[:1] == ['-u']: + tail = tail[1:] + if parts[0] in ('python', 'python3', 'node', env.get('PYATS_PYTHON')) and len(tail) == 1 and tail[0] in paths: + return component, rule['servers'][0] + return None + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('component', choices=sorted(CONTRACT)) + parser.add_argument('--server') + args = parser.parse_args() + try: + command, env, cwd = resolve(args.component, args.server) + os.chdir(cwd) + os.execvpe(command[0], command, env) + except (OSError, ValueError) as error: + # Do not dump env/config, upstream diagnostics or credential values. + print(str(error) if isinstance(error, ValueError) else 'Component launch failed; check installation paths.', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/install-mcp-config.py b/scripts/install-mcp-config.py index 7735b41c..9274648e 100644 --- a/scripts/install-mcp-config.py +++ b/scripts/install-mcp-config.py @@ -6,6 +6,7 @@ import json import os from pathlib import Path +import sys import time @@ -47,6 +48,8 @@ def bind_entry(entry, repo, runtime): # Canonical source-venv templates follow the successful runtime record # after recovery. Custom interpreters, uvx and Node retain their command. value = result.get('command', '') + if value in ('python', 'python3') and not runtime: + result['command'] = sys.executable if value.startswith(('mcp-servers/', 'scripts/')): result['command'] = str(repo / value) if 'args' in result: @@ -87,6 +90,11 @@ def merge_config(path, generated, template): updated[key] = entry[key] else: updated.pop(key, None) + updated['env'] = {**entry.get('env', {}), **existing.get('env', {})} + if not updated['env']: + updated.pop('env') + if 'transport' in entry: + updated.setdefault('transport', entry['transport']) servers[name] = updated else: servers[name] = entry @@ -122,6 +130,11 @@ def main(): record = args.runtime_root / 'records' / component runtime = record.read_text().strip() if record.exists() else None generated[name] = bind_entry(entry, repo, runtime) + if 'scripts/component-launch.py' in entry.get('args', []): + generated[name].setdefault('env', {}).update({ + 'NETCLAW_RUNTIME_ROOT': str(args.runtime_root.resolve()), + 'NETCLAW_RUNTIME_ENV': str(args.runtime_root.resolve().parent / '.env'), + }) writer.write_private(args.output, json.dumps({'mcpServers': generated}, indent=2) + '\n') if args.config: merge_config(args.config, generated, template) diff --git a/scripts/install.sh b/scripts/install.sh index 2e3dd73d..a3972c67 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -506,7 +506,7 @@ echo "" # Verify installation (selected components only) # ═══════════════════════════════════════════ -log_step "Verifying installation..." +log_step "Checking installed artifacts (tool discovery follows setup)..." SERVERS_OK=0 SERVERS_FAIL=0 @@ -514,7 +514,7 @@ SERVERS_FAIL=0 verify_file() { local name="$1" path="$2" if [ -f "$path" ]; then - log_info "$name: OK" + log_info "$name: artifact present (discovery pending)" SERVERS_OK=$((SERVERS_OK + 1)) else log_error "$name: MISSING ($path)" @@ -525,7 +525,7 @@ verify_file() { verify_dir() { local name="$1" path="$2" if [ -d "$path" ]; then - log_info "$name: OK" + log_info "$name: directory present (discovery pending)" SERVERS_OK=$((SERVERS_OK + 1)) else log_warn "$name: NOT INSTALLED ($path missing)" @@ -536,7 +536,7 @@ verify_dir() { verify_cmd_or_module() { local name="$1" cmd="$2" module="$3" hint="$4" if command -v "$cmd" &> /dev/null || python3 -c "import $module" 2>/dev/null; then - log_info "$name: OK" + log_info "$name: executable/module present (discovery pending)" SERVERS_OK=$((SERVERS_OK + 1)) else log_warn "$name: NOT INSTALLED ($hint)" @@ -547,7 +547,7 @@ verify_cmd_or_module() { verify_runner() { local name="$1" cmd="$2" note="$3" if command -v "$cmd" &> /dev/null; then - log_info "$name: OK ($note)" + log_info "$name: launcher present ($note; discovery pending)" SERVERS_OK=$((SERVERS_OK + 1)) else log_warn "$name: NOT AVAILABLE ($cmd not installed)" @@ -556,7 +556,7 @@ verify_runner() { } verify_remote() { - log_info "$1: OK (remote server — no local install)" + log_info "$1: remote declaration present (connectivity unverified)" SERVERS_OK=$((SERVERS_OK + 1)) } @@ -651,7 +651,7 @@ done verify_file "MCP Call Script" "$NETCLAW_DIR/scripts/mcp-call.py" echo "" -log_info "Verification: $SERVERS_OK OK, $SERVERS_FAIL FAILED" +log_info "Artifacts: $SERVERS_OK present/deferred, $SERVERS_FAIL FAILED; this is not tool readiness." SUCCESSFUL_COMPONENTS="" for id in $SELECTED; do @@ -727,6 +727,15 @@ if [ -f "$SETUP_SCRIPT" ] && tui_is_tty; then fi fi +# Check the actual registered commands after credentials/setup are available. +# Missing credentials are configuration-required; failed discovery is a failure. +READINESS_FAILED=0 +log_step "Checking selected MCP discovery and provider readiness..." +python3 "$SCRIPT_DIR/installer-readiness.py" --runtime "$RUNTIME" \ + --runtime-root "$NETCLAW_RUNTIME_ROOT" --config "$RUNTIME_CONFIG" \ + --components "$SELECTED" --failed-components "$FAILED_COMPONENTS $VERIFY_FAILED_COMPONENTS" \ + --output "$INSTALL_LOG_DIR/readiness.json" --probe --check-provider || READINESS_FAILED=1 + echo "" echo "=========================================" echo " Next Steps" @@ -781,7 +790,7 @@ if [ -n "$PROBLEM_COMPONENTS" ]; then fi done echo "" - echo " Everything else installed fine. Retry just these with:" + echo " Retry these installation failures with:" echo "" echo " ./scripts/install.sh --add \"$(echo $PROBLEM_COMPONENTS | tr '\n' ' ' | sed 's/ $//')\"" echo "" @@ -790,6 +799,10 @@ fi if [ "$CORE_FAILED" -ne 0 ]; then log_error "Core installation or configuration deployment failed. Review the errors above and $INSTALL_LOG_DIR/core-tokens.log before retrying." fi -if [ -n "$PROBLEM_COMPONENTS" ] || [ "$SERVERS_FAIL" -ne 0 ] || [ "$CORE_FAILED" -ne 0 ]; then +if [ "$READINESS_FAILED" -ne 0 ]; then + log_error "Tool/provider readiness failed. Inspect $INSTALL_LOG_DIR/readiness.json; installation is incomplete." +fi +if [ -n "$PROBLEM_COMPONENTS" ] || [ "$SERVERS_FAIL" -ne 0 ] || [ "$CORE_FAILED" -ne 0 ] || [ "$READINESS_FAILED" -ne 0 ]; then exit 1 fi +log_info "Installation steps finished. Review readiness.json for configuration-required/unverified checks before using tools." diff --git a/scripts/installer-preflight.py b/scripts/installer-preflight.py index 4f2bb452..8b04f9c5 100644 --- a/scripts/installer-preflight.py +++ b/scripts/installer-preflight.py @@ -16,6 +16,9 @@ ROOT = Path(__file__).resolve().parents[1] POLICY_PATH = ROOT / 'config/installer-preflight.json' +_spec = importlib.util.spec_from_file_location('runtime_policy', ROOT / 'scripts/runtime-policy.py') +runtime_policy = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(runtime_policy) def load_policy(): @@ -114,15 +117,15 @@ def vendor_source_ready(rule, env): return False -def evaluate(policy, selected, system, arch, executable, host, launchers=None, explicit_venv=''): +def evaluate(policy, selected, system, arch, executable, host, launchers=None, explicit_venv='', runtime='openclaw'): core = [] for command in policy['core_commands']: if not host.command(command): core.append(command + ' missing from PATH') node = host.output(['node', '--version']) if host.command('node') else None - node_match = re.match(r'^v?(\d+)\.', node or '') - if node is not None and (not node_match or int(node_match[1]) < 18): - core.append('Node.js >=18 required; select a supported Node.js on PATH') + if node is not None and not runtime_policy.node_supported(node, runtime): + core.append('Node.js ' + runtime_policy.POLICY['node_descriptions'][runtime] + + ' required; select a supported Node.js on PATH (for example nvm install 26)') elif host.command('node') and node is None: core.append('Could not read Node.js version') installer_python = host.python(host.command('python3')) if host.command('python3') else None @@ -205,6 +208,7 @@ def main(argv=None): parser.add_argument('--platform-only', action='store_true') parser.add_argument('--validate-policy', action='store_true') parser.add_argument('--json', action='store_true') + parser.add_argument('--runtime', choices=['openclaw', 'hermes'], default='openclaw') args = parser.parse_args(argv) try: policy, catalog = load_policy() @@ -222,7 +226,7 @@ def main(argv=None): print(component + '|' + reason) return 0 report = evaluate(policy, selected, args.os, args.arch, args.python, Host(), launch_commands(), - os.environ.get('NETCLAW_VENV', '')) + os.environ.get('NETCLAW_VENV', ''), args.runtime) if args.json: print(json.dumps(report)) else: diff --git a/scripts/installer-readiness.py b/scripts/installer-readiness.py new file mode 100644 index 00000000..65d5fc9f --- /dev/null +++ b/scripts/installer-readiness.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +"""Report installed artifacts, access, discovery and provider readiness separately. + +Default is configuration-only. --probe performs bounded MCP discovery, never +network device tool calls. --check-provider reads the configured Ollama model +catalog/capabilities; it does not generate text or download models. +""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.error +import urllib.request +from urllib.parse import urlsplit, urlunsplit + +ROOT = Path(__file__).resolve().parents[1] + + +def load(name): + spec = importlib.util.spec_from_file_location(name.replace('-', '_'), ROOT / 'scripts' / (name + '.py')) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +launcher = load('component-launch') +registration = load('install-mcp-config') +probe = load('mcp-probe') +writer = load('write-env') + + +def expand(value, env): + def replace(match): + key, default = match.groups() + return env.get(key) or (default if default is not None else match.group(0)) + return re.sub(r'\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}', replace, value) + + +def native_result(data, server, expected): + """Do not accept the historical empty-catalog/exit-0 probe result.""" + if not isinstance(data, dict) or data.get('diagnostics') or data.get('error'): + return False + row = data.get('servers', {}).get(server, {}) + names = data.get('tools', []) + return (isinstance(row.get('tools'), int) and row['tools'] > 0 and + isinstance(names, list) and bool(names) and + all(any(isinstance(name, str) and (name == tool or name.endswith('_'+tool)) + for name in names) for tool in expected)) + + +def native_probe(server, expected, env, config, timeout): + cli = shutil.which('openclaw', path=env.get('PATH')) + if not cli: + return {'status':'failed', 'reason':'runtime_cli_missing'} + probe_env = {**env, 'OPENCLAW_CONFIG_PATH':str(config)} + try: + # Capture stdout/stderr privately and bound both time and reported data. + with tempfile.TemporaryFile() as output: + process = subprocess.Popen([cli, 'mcp', 'probe', server, '--json'], + env=probe_env, stdin=subprocess.DEVNULL, stdout=output, + stderr=subprocess.DEVNULL, start_new_session=True) + try: + rc = process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + probe.stop(process) + return {'status':'failed', 'reason':'runtime_probe_timeout'} + output.seek(0) + raw = output.read(8 * 1024 * 1024 + 1) + if rc or len(raw) > 8 * 1024 * 1024: + return {'status':'failed', 'reason':'runtime_probe_failed'} + data = json.loads(raw) + if not native_result(data, server, expected): + return {'status':'failed', 'reason':'runtime_expected_tools_missing'} + return {'status':'verified', 'tool_count':data['servers'][server]['tools']} + except (OSError, ValueError, TypeError, AttributeError): + return {'status':'failed', 'reason':'runtime_probe_invalid_response'} + + +def missing_credentials(rule, env): + missing = [key for key in rule.get('required_env', []) if not env.get(key)] + if 'SERVICENOW_INSTANCE_URL' in rule.get('required_env', []): + auth = env.get('SERVICENOW_AUTH_TYPE') or 'basic' + required = {'basic':['SERVICENOW_USERNAME','SERVICENOW_PASSWORD'], + 'oauth':['SERVICENOW_CLIENT_ID','SERVICENOW_CLIENT_SECRET','SERVICENOW_TOKEN_URL'], + 'api_key':['SERVICENOW_API_KEY']}.get(auth, ['SERVICENOW_AUTH_TYPE']) + missing += [key for key in required if not env.get(key)] + return missing + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, *_args, **_kwargs): + raise ValueError('redirect_refused') + + +def http_json(url, headers, body=None, timeout=5): + data = json.dumps(body).encode() if body is not None else None + request = urllib.request.Request(url, data=data, headers={**headers,'Content-Type':'application/json'}) + with urllib.request.build_opener(NoRedirect()).open(request, timeout=timeout) as response: + raw = response.read(4 * 1024 * 1024 + 1) + if len(raw) > 4 * 1024 * 1024: + raise ValueError('response_too_large') + return json.loads(raw) + + +def ollama_readiness(config, env, check=False, request=http_json): + defaults = config.get('agents', {}).get('defaults', {}) + selection = defaults.get('model', '') + model = selection.get('primary', '') if isinstance(selection, dict) else selection + if not isinstance(model, str) or not model.startswith('ollama/'): + return {'status':'unverified', 'reason':'not_an_ollama_primary', 'tool_calling':'unverified'} + model = model.split('/', 1)[1] + provider = config.get('models', {}).get('providers', {}).get('ollama', {}) + endpoint = provider.get('baseUrl') or env.get('OLLAMA_BASE_URL') or 'http://127.0.0.1:11434' + endpoint = expand(endpoint, env) + parsed = urlsplit(endpoint) + if parsed.scheme not in ('http', 'https') or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment: + return {'status':'failed', 'reason':'invalid_ollama_endpoint', 'tool_calling':'unverified'} + # OpenAI-compatible /v1 config still uses Ollama's native catalog endpoints. + path = parsed.path.rstrip('/') + if path.endswith('/v1'): + path = path[:-3] + base = urlunsplit((parsed.scheme, parsed.netloc, path, '', '')) + if not check: + return {'status':'unverified', 'reason':'provider_check_not_requested', 'tool_calling':'unverified'} + key = provider.get('apiKey') or env.get('OLLAMA_API_KEY') or '' + if not isinstance(key, str): + return {'status':'unverified', 'reason':'provider_secret_reference_requires_runtime', 'tool_calling':'unverified'} + key = expand(key, env) + if '${' in key: + return {'status':'configuration_required', 'reason':'provider_key_unresolved', 'tool_calling':'unverified'} + headers = {'Authorization':'Bearer '+key} if key else {} + try: + catalog = request(base+'/api/tags', headers) + names = {row.get('name') or row.get('model') for row in catalog.get('models', [])} + candidates = {model} if ':' in model else {model, model+':latest'} + match = next((name for name in names if name in candidates), None) + if not match: + return {'status':'failed', 'reason':'ollama_model_missing', 'tool_calling':'unverified'} + info = request(base+'/api/show', headers, {'model':match}) + capabilities = info.get('capabilities') + if not isinstance(capabilities, list): + return {'status':'unverified', 'reason':'ollama_model_capabilities_unknown', 'tool_calling':'unverified'} + if 'tools' not in capabilities: + return {'status':'failed', 'reason':'ollama_model_has_no_tool_capability', 'tool_calling':'unsupported'} + return {'status':'verified', 'reason':'ollama_model_available', 'tool_calling':'advertised_not_exercised'} + except (OSError, ValueError, TypeError, AttributeError): + # URLs, response bodies and credentials never enter the saved report. + return {'status':'failed', 'reason':'ollama_endpoint_or_api_unavailable', 'tool_calling':'unverified'} + + +def check_server(name, entry, component, rule, env, config, runtime, do_probe, timeout): + row = {'server':name, 'access':'native_mcp', 'registration':'verified', + 'artifacts':'unverified', 'discovery':{'status':'unverified'}, 'endpoint':'unverified'} + if entry.get('enabled') is False: + row['discovery'] = {'status':'configuration_required', 'reason':'server_disabled'} + return row + child_env = dict(env) + child_env.update({k:expand(str(v), env) for k,v in entry.get('env', {}).items()}) + missing = missing_credentials(rule, child_env) + unresolved = any('${' in str(v) for v in entry.get('env', {}).values() if '${' in expand(str(v), child_env)) + try: + if component in launcher.CONTRACT: + launcher.resolve(component, name, env=child_env) + command = entry.get('command') + if command: + command = expand(command, child_env) + executable = shutil.which(command, path=child_env.get('PATH')) + if not executable: + raise ValueError('launcher_missing') + parts = [executable] + [expand(v, child_env) for v in entry.get('args', [])] + cwd = Path(entry.get('cwd', str(ROOT))) + if not cwd.is_dir(): + raise ValueError('cwd_missing') + for arg in parts[1:]: + if arg.endswith(('.py', '.js', '.mjs')) and '/' in arg and not (cwd/arg).is_file(): + raise ValueError('script_missing') + row['artifacts'] = 'verified' + else: + row['artifacts'] = 'remote' + if missing or unresolved: + row['discovery'] = {'status':'configuration_required', 'reason':'missing_credentials', 'variables':missing} + return row + if not do_probe: + return row + expected = rule.get('expected_tools', []) + if runtime == 'openclaw': + row['discovery'] = native_probe(name, expected, child_env, config, timeout) + elif command: + row['discovery'] = probe.discover(parts, child_env, str(cwd), timeout, expected) + # Direct discovery is not proof of Hermes's agent-visible catalog. + row['access'] = 'hermes_registered_direct_probe' + row['agent_discovery'] = 'unverified' + else: + row['discovery'] = {'status':'unverified', 'reason':'hermes_remote_probe_not_available'} + return row + except (OSError, ValueError, KeyError): + row['artifacts'] = 'failed' + row['discovery'] = {'status':'failed', 'reason':'launcher_or_runtime_missing'} + return row + + +def report(selected, failed, templates, live, env, config_path, runtime, do_probe=False, timeout=25): + components = [] + for component in selected: + rule = launcher.CONTRACT.get(component, {}) + expected = rule.get('servers') or [name for name in templates if registration.component_for(name) == component] + row = {'component':component, 'installation':'failed' if component in failed else 'recorded', 'servers':[]} + if component in failed: + row['status'] = 'failed' + elif not expected: + row['status'] = 'unverified' + row['reason'] = 'no_native_binding_declared; consult_component_skill' + else: + for name in expected: + if name not in live: + row['servers'].append({'server':name, 'registration':'failed', + 'discovery':{'status':'failed','reason':'not_registered'}, 'endpoint':'unverified'}) + else: + row['servers'].append(check_server(name, live[name], component, rule, env, + config_path, runtime, do_probe, timeout)) + statuses = {item['discovery']['status'] for item in row['servers']} + row['status'] = ('failed' if 'failed' in statuses else 'configuration_required' + if 'configuration_required' in statuses else 'discovery_verified' + if statuses == {'verified'} else 'unverified') + components.append(row) + return {'schema_version':1, 'runtime':runtime, 'components':components, + 'endpoint_operations':'unverified', 'agent_turn':'unverified', + 'ok':not any(row['status'] == 'failed' for row in components)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--runtime', choices=['openclaw','hermes'], default='openclaw') + parser.add_argument('--runtime-root', type=Path, required=True) + parser.add_argument('--config', type=Path, required=True) + parser.add_argument('--components', required=True) + parser.add_argument('--failed-components', default='') + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--probe', action='store_true') + parser.add_argument('--check-provider', action='store_true') + parser.add_argument('--timeout', type=float, default=25) + args = parser.parse_args() + if not 0 < args.timeout <= 120: + parser.error('timeout must be >0 and <=120 seconds') + env = launcher.environment({**os.environ,'NETCLAW_RUNTIME_ROOT':str(args.runtime_root), + 'NETCLAW_RUNTIME_ENV':str(args.runtime_root.parent/'.env')}) + try: + templates = json.loads((ROOT/'config/openclaw.json').read_text())['mcpServers'] + if args.runtime == 'openclaw': + config = json.loads(args.config.read_text()) + live = config.get('mcp', {}).get('servers', config.get('mcpServers', {})) + else: + import yaml + config = yaml.safe_load(args.config.read_text()) or {} + live = config.get('mcp_servers', {}) + result = report(args.components.split(), set(args.failed_components.split()), templates, live, + env, args.config, args.runtime, args.probe, args.timeout) + result['provider'] = (ollama_readiness(config, env, args.check_provider) if args.runtime == 'openclaw' + else {'status':'unverified','reason':'check_provider_with_hermes'}) + if result['provider']['status'] == 'failed': + result['ok'] = False + writer.write_private(args.output, json.dumps(result, indent=2)+'\n') + for row in result['components']: + print(f"[{row['status'].upper()}] {row['component']}") + print('Provider: '+result['provider']['status']) + print('Device/endpoint operations and agent answers remain unverified; discovery does not prove either.') + print('Readiness report: '+str(args.output)) + return 0 if result['ok'] else 1 + except (OSError, ValueError, KeyError, TypeError, ImportError): + print('Readiness could not inspect runtime configuration; no readiness claim made.', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/lib/install-steps.sh b/scripts/lib/install-steps.sh index f13e8f0a..ccc1d525 100644 --- a/scripts/lib/install-steps.sh +++ b/scripts/lib/install-steps.sh @@ -6,6 +6,7 @@ # servers cannot import from. # shellcheck source=scripts/lib/pip-helper.sh source "$(dirname "${BASH_SOURCE[0]}")/pip-helper.sh" +source "$(dirname "${BASH_SOURCE[0]}")/runtime-install.sh" # NetClaw install steps — one function per component. # Extracted mechanically from the original monolithic install.sh (all install # logic preserved). Sourced by scripts/install.sh; requires lib/common.sh first. @@ -106,13 +107,13 @@ prereqs_offer_install() { if [ "${NODE_TOO_OLD:-0}" -eq 1 ]; then local node_cmd="" case "$PKG_MGR" in - apt) node_cmd="curl -fsSL https://deb.nodesource.com/setup_22.x | ${spe}bash - && ${sp}apt-get install -y nodejs" ;; - dnf|yum) node_cmd="curl -fsSL https://rpm.nodesource.com/setup_22.x | ${spe}bash - && ${sp}${PKG_MGR} install -y nodejs" ;; + apt) node_cmd="curl -fsSL https://deb.nodesource.com/setup_26.x | ${spe}bash - && ${sp}apt-get install -y nodejs" ;; + dnf|yum) node_cmd="curl -fsSL https://rpm.nodesource.com/setup_26.x | ${spe}bash - && ${sp}${PKG_MGR} install -y nodejs" ;; brew) node_cmd="brew install node" ;; esac if [ -n "$node_cmd" ]; then echo "" - echo " Node.js is older than 18. It can be upgraded now with:" + echo " Node.js does not satisfy the selected runtime. Upgrade with:" echo -e " ${CYAN}${node_cmd}${NC}" echo "" read -rp "Run the Node.js upgrade now? [Y/n] " RUN_NODE_UP @@ -121,7 +122,7 @@ prereqs_offer_install() { if bash -c "$node_cmd"; then ran=0; else log_error "Node.js upgrade failed."; fi fi else - log_warn "Upgrade Node.js to >= 18 manually (https://nodejs.org/ or nvm)." + log_warn "Select supported Node.js (26.1+ or 24.16+ on OpenClaw) manually (https://nodejs.org/ or nvm)." fi fi @@ -137,13 +138,11 @@ MISSING_IDS="" NODE_TOO_OLD=0 if ! check_command node; then - log_error "Node.js is required (>= 18). Install from https://nodejs.org/" + log_error "Node.js is required. See config/installer-runtime.json and https://nodejs.org/" MISSING=1 MISSING_IDS="$MISSING_IDS nodejs npm" else - NODE_VERSION=$(node --version | sed 's/v//' | cut -d. -f1) - if [ "$NODE_VERSION" -lt 18 ]; then - log_error "Node.js >= 18 required. Found: $(node --version)" + if ! python3 "$NETCLAW_DIR/scripts/runtime-policy.py" --runtime "${RUNTIME:-openclaw}" --node "$(node --version)"; then MISSING=1 NODE_TOO_OLD=1 else @@ -196,6 +195,12 @@ if [ "$MISSING" -eq 1 ]; then if [ "$attempt" = "first" ] && prereqs_offer_install; then echo "" log_step "Re-checking prerequisites..." + hash -r + if [ "${NETCLAW_PY_EXPLICIT:-0}" != 1 ]; then + NETCLAW_PY="$(command -v python3)" + netclaw_choose_component_python + export NETCLAW_PY + fi core_prereqs retry return fi @@ -217,34 +222,6 @@ echo "" # ── Step 2: Install OpenClaw ──────────────────────────────────── -# Run a command; if it fails and we aren't root, offer to retry it with sudo. -# Global npm installs commonly hit EACCES when the npm prefix (e.g. /usr/lib) -# is root-owned. Returns the final exit status. -_run_or_offer_sudo() { - if "$@"; then - return 0 - fi - if [ "$(id -u)" = "0" ] || ! command -v sudo &> /dev/null; then - return 1 - fi - log_warn "'$*' failed — this usually means it needs root permissions." - if [ -t 0 ]; then - echo "" - echo " It can be retried with:" - echo -e " ${CYAN}sudo $*${NC}" - echo "" - read -rp "Retry with sudo now? [Y/n] " RETRY_SUDO - RETRY_SUDO="${RETRY_SUDO:-y}" - if [[ "$RETRY_SUDO" =~ ^[Yy] ]]; then - sudo "$@" - return $? - fi - else - log_warn "Re-run manually with: sudo $*" - fi - return 1 -} - # ── Step 2: Install the agent runtime (OpenClaw or Hermes) ────── core_runtime() { log_step "Installing $RUNTIME_NAME..." @@ -258,6 +235,7 @@ if [ "$RUNTIME" = "hermes" ]; then if ! curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash; then log_error "Hermes install script failed." log_warn "Re-run manually: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash" + return 1 fi # Hermes installs the launcher to ~/.local/bin — make it reachable now. case ":$PATH:" in @@ -269,29 +247,24 @@ if [ "$RUNTIME" = "hermes" ]; then else log_error "hermes not found on PATH after install" log_warn "Add it to PATH: export PATH=\"\$HOME/.local/bin:\$PATH\"" + return 1 fi fi + hermes version >/dev/null 2>&1 || { log_error "Hermes executable verification failed"; return 1; } echo "" return 0 fi # ── OpenClaw (default) ── if command -v openclaw &> /dev/null; then - log_info "OpenClaw already installed: $(openclaw --version 2>/dev/null || echo 'version unknown')" + if ! openclaw --version; then + log_error "Existing OpenClaw executable failed; repair it before onboarding." + return 1 + fi + log_info "OpenClaw already installed." else log_info "Installing OpenClaw via npm..." - if ! _run_or_offer_sudo npm install -g openclaw@latest; then - log_error "npm install -g openclaw@latest failed." - log_warn "Alternative without root — use a user-level npm prefix:" - log_warn " npm config set prefix ~/.local && export PATH=\"\$HOME/.local/bin:\$PATH\"" - log_warn " npm install -g openclaw@latest" - fi - if command -v openclaw &> /dev/null; then - log_info "OpenClaw installed successfully" - else - log_error "openclaw not found on PATH after install" - log_warn "Try: export PATH=\"$(npm config get prefix 2>/dev/null || echo /usr/local)/bin:\$PATH\"" - fi + netclaw_install_openclaw || return 1 fi echo "" @@ -604,6 +577,7 @@ if [ ! -d "$MARKMAP_INNER" ]; then log_warn "Nested markmap-mcp/ not found, trying top-level..." BUILD_DIR="$MARKMAP_MCP_DIR" fi +MARKMAP_INNER="$BUILD_DIR" log_info "Building Markmap MCP..." # Subshell keeps the installer's cwd intact even when the build fails. @@ -647,7 +621,7 @@ clone_or_pull "$NETBOX_MCP_DIR" "https://github.com/netboxlabs/netbox-mcp-server log_info "Installing NetBox dependencies..." netclaw_pip_install httpx "fastmcp==4.0.11" "fastmcp-tasks==4.0.11" requests pydantic pydantic-settings 2>/dev/null || \ - log_warn "Some NetBox deps failed" + { log_error "NetBox dependency installation failed"; return 1; } log_info "NetBox MCP ready: python3 -m netbox_mcp_server.server" @@ -709,8 +683,8 @@ SERVICENOW_MCP_DIR="$MCP_DIR/servicenow-mcp" clone_or_pull "$SERVICENOW_MCP_DIR" "https://github.com/echelon-ai-labs/servicenow-mcp.git" log_info "Installing ServiceNow dependencies..." -netclaw_pip_install "mcp[cli]>=1.3.0" requests "pydantic>=2.0.0" python-dotenv starlette uvicorn httpx PyYAML 2>/dev/null || \ - log_warn "Some ServiceNow deps failed" +netclaw_pip_install "mcp[cli]>=1.13,<2" requests "pydantic>=2.0.0" python-dotenv starlette uvicorn httpx PyYAML 2>/dev/null || \ + { log_error "ServiceNow dependency installation failed"; return 1; } log_info "ServiceNow MCP ready" @@ -783,8 +757,7 @@ NVD_MCP_DIR="$MCP_DIR/mcp-nvd" clone_or_pull "$NVD_MCP_DIR" "https://github.com/marcoeg/mcp-nvd.git" log_info "Installing NVD dependencies..." -cd "$NVD_MCP_DIR" && netclaw_pip_install -e . 2>/dev/null && cd "$NETCLAW_DIR" || \ - log_warn "NVD MCP install failed" +(cd "$NVD_MCP_DIR" && netclaw_pip_install -e .) || return 1 log_info "NVD CVE MCP ready: python3 -m mcp_nvd.main" @@ -2730,7 +2703,8 @@ if [ "$RUNTIME" = "hermes" ]; then --sidecar "$RUNTIME_HOME/netclaw-mcp-servers.yaml"; then log_info "Registered NetClaw MCP servers into $RUNTIME_CONFIG" else - log_warn "MCP translation reported an error — check $RUNTIME_CONFIG" + log_error "MCP translation failed — check $RUNTIME_CONFIG" + return 1 fi else log_warn "config/openclaw.json not found in repo — no MCP servers registered" @@ -2781,6 +2755,8 @@ OPENCLAW_ENV="$RUNTIME_ENV" _set_env_default "PYATS_TESTBED_PATH" "$RUNTIME_WORKSPACE/testbed/testbed.yaml" _set_env_var "PYATS_MCP_SCRIPT" "$PYATS_SCRIPT" _set_env_var "MCP_CALL" "$NETCLAW_DIR/scripts/mcp-call.py" +_set_env_var "NETCLAW_RUNTIME_ROOT" "$NETCLAW_RUNTIME_ROOT" +_set_env_var "NETCLAW_RUNTIME_ENV" "$RUNTIME_ENV" _set_env_var "MARKMAP_MCP_SCRIPT" "$MARKMAP_INNER/dist/index.js" _set_env_var "GAIT_MCP_SCRIPT" "$NETCLAW_DIR/scripts/gait-stdio.py" _set_env_var "NETBOX_MCP_SCRIPT" "$NETBOX_MCP_DIR/src/netbox_mcp_server/server.py" diff --git a/scripts/lib/preflight.sh b/scripts/lib/preflight.sh index be003323..87ab8764 100644 --- a/scripts/lib/preflight.sh +++ b/scripts/lib/preflight.sh @@ -40,5 +40,5 @@ netclaw_platform_reason() { netclaw_component_preflight() { python3 "$SCRIPT_DIR/installer-preflight.py" --os "$NETCLAW_OS" \ - --arch "$NETCLAW_ARCH" --python "$NETCLAW_PY" --components "$SELECTED" + --arch "$NETCLAW_ARCH" --python "$NETCLAW_PY" --components "$SELECTED" --runtime "${RUNTIME:-openclaw}" } diff --git a/scripts/lib/runtime-install.sh b/scripts/lib/runtime-install.sh new file mode 100644 index 00000000..0c747942 --- /dev/null +++ b/scripts/lib/runtime-install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Rootless, per-install npm options. Never rewrite global npm configuration. +netclaw_install_openclaw() { + local policy="$NETCLAW_DIR/scripts/runtime-policy.py" npm_options option prefix probe + local npm_args=() + python3 "$policy" --runtime openclaw --node "$(node --version)" || return 1 + npm_options="$(python3 "$policy" --npm "$(npm --version)")" || return 1 + while IFS= read -r option; do npm_args+=("$option"); done <<< "$npm_options" + prefix="$(npm config get prefix)" || return 1 + [ -n "$prefix" ] && [ "$prefix" != undefined ] || { log_error "Cannot determine npm prefix"; return 1; } + probe="$prefix/lib/node_modules" + while [ ! -e "$probe" ] && [ "$probe" != / ]; do probe="$(dirname "$probe")"; done + if [ ! -w "$probe" ] || { [ -e "$prefix/bin/openclaw" ] && [ ! -w "$prefix/bin/openclaw" ]; }; then + prefix="$HOME/.local" + log_info "npm's global prefix is not writable; installing OpenClaw with --prefix $prefix" + npm_args+=(--prefix "$prefix") + fi + if ! npm "${npm_args[@]}"; then + log_error "OpenClaw npm installation failed. Review npm's error above; no sudo retry was attempted." + log_info "For EACCES, use a user-owned Node version manager or npm --prefix \"\$HOME/.local\"." + return 1 + fi + export PATH="$prefix/bin:$PATH" + hash -r + if ! command -v openclaw >/dev/null 2>&1 || ! openclaw --version; then + log_error "OpenClaw executable verification failed after npm install." + return 1 + fi + log_info "OpenClaw installed. Keep $prefix/bin on PATH in new shells." +} diff --git a/scripts/mcp-call.py b/scripts/mcp-call.py index 96cbb631..1b3b40ef 100755 --- a/scripts/mcp-call.py +++ b/scripts/mcp-call.py @@ -18,6 +18,15 @@ import sys import time import tempfile +import importlib.util +from pathlib import Path + + +def component_helper(): + spec = importlib.util.spec_from_file_location('mcp_launch', Path(__file__).with_name('component-launch.py')) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module def send(proc, msg): @@ -42,7 +51,7 @@ def recv(proc, timeout=30, expected_id=None): raw = proc.stdout.readline() if not raw: - continue + return None line = raw.decode(errors="replace").strip() if not line: @@ -92,6 +101,12 @@ def read_stderr(proc): def main(): + # Explicit component mode and legacy skills share the installer's binding. + component = None + if len(sys.argv) > 2 and sys.argv[1] == '--component': + component = sys.argv.pop(2) + sys.argv.pop(1) + sys.argv.insert(1, component) if len(sys.argv) < 3: print(f"Usage: {sys.argv[0]} [arguments-json]", file=sys.stderr) sys.exit(1) @@ -108,7 +123,16 @@ def main(): else: args_json = {} - cmd_parts, env = split_server_command(server_cmd) + helper = component_helper() + cwd = None + try: + cmd_parts, env = split_server_command(server_cmd) + match = (component, None) if component else helper.legacy_component(cmd_parts, env) + if match: + cmd_parts, env, cwd = helper.resolve(*match, env=env) + except (KeyError, OSError, ValueError): + print('Component unavailable: check its installed runtime and launcher; no tool was called.', file=sys.stderr) + sys.exit(1) error_log = tempfile.TemporaryFile() proc = subprocess.Popen( cmd_parts, @@ -116,6 +140,7 @@ def main(): stdout=subprocess.PIPE, stderr=error_log, env=env, + cwd=cwd, ) proc.stderr = error_log @@ -132,7 +157,7 @@ def main(): }, }) init_resp = recv(proc, timeout=60, expected_id=0) - if not init_resp: + if not init_resp or 'error' in init_resp or not isinstance(init_resp.get('result'), dict): stderr_output = read_stderr(proc) if stderr_output: print(f"Error: No response to initialize\n{stderr_output}", file=sys.stderr) @@ -148,8 +173,8 @@ def main(): send(proc, { "jsonrpc": "2.0", "id": 1, - "method": "tools/call", - "params": {"name": tool_name, "arguments": args_json}, + "method": "tools/list" if tool_name == '--list-tools' else "tools/call", + "params": {} if tool_name == '--list-tools' else {"name": tool_name, "arguments": args_json}, }) tool_timeout = float(os.environ.get("MCP_CALL_TIMEOUT", "30")) resp = recv(proc, timeout=tool_timeout, expected_id=1) diff --git a/scripts/mcp-probe.py b/scripts/mcp-probe.py new file mode 100644 index 00000000..06e7e5de --- /dev/null +++ b/scripts/mcp-probe.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Bounded, read-only stdio discovery. Never invokes a device/provider tool.""" +import json +import os +import select +import signal +import subprocess +import tempfile +import time + + +class ProbeError(Exception): + pass + + +def stop(proc): + # Include owned bridge children; never stop an unrelated daemon. + if proc.poll() is not None: + return + try: + os.killpg(proc.pid, signal.SIGTERM) + except ProcessLookupError: + return + except PermissionError: + # macOS can refuse killpg for an already exiting group; reap first. + if proc.poll() is not None: + return + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + os.killpg(proc.pid, signal.SIGKILL) + proc.wait() + + +def discover(command, env, cwd, timeout=20, expected=()): + deadline = time.monotonic() + timeout + with tempfile.TemporaryFile() as errors: + proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=errors, env=env, cwd=cwd, start_new_session=True, bufsize=0) + buffer = b'' + total = 0 + + def send(message): + proc.stdin.write((json.dumps(message)+'\n').encode()) + + def response(identifier): + nonlocal buffer, total + while time.monotonic() < deadline: + if b'\n' not in buffer: + if not select.select([proc.stdout], [], [], max(0, deadline-time.monotonic()))[0]: + break + chunk = os.read(proc.stdout.fileno(), 65536) + if not chunk: + raise ProbeError('server_closed') + total += len(chunk) + if total > 8 * 1024 * 1024: + raise ProbeError('response_too_large') + buffer += chunk + continue + line, buffer = buffer.split(b'\n', 1) + try: + data = json.loads(line) + except (ValueError, UnicodeError): + # Ignore banners, but do not confuse them with protocol success. + continue + if not isinstance(data, dict) or data.get('id') != identifier: + continue + if 'error' in data or not isinstance(data.get('result'), dict): + raise ProbeError('protocol_error') + return data['result'] + raise ProbeError('timeout') + + try: + send({'jsonrpc':'2.0','id':0,'method':'initialize','params':{ + 'protocolVersion':'2025-06-18','capabilities':{}, + 'clientInfo':{'name':'netclaw-readiness','version':'1'}}}) + init = response(0) + if not init.get('protocolVersion') or not isinstance(init.get('capabilities'), dict): + raise ProbeError('invalid_initialize') + send({'jsonrpc':'2.0','method':'notifications/initialized'}) + names, cursor = set(), None + for identifier in range(1, 21): + send({'jsonrpc':'2.0','id':identifier,'method':'tools/list', + 'params':{'cursor':cursor} if cursor else {}}) + result = response(identifier) + if not isinstance(result.get('tools'), list): + raise ProbeError('invalid_tool_list') + for tool in result['tools']: + if not isinstance(tool, dict) or not isinstance(tool.get('name'), str): + raise ProbeError('invalid_tool_list') + names.add(tool['name']) + cursor = result.get('nextCursor') + if not cursor: + break + else: + raise ProbeError('pagination_limit') + if not names or not set(expected) <= names: + raise ProbeError('expected_tools_missing') + return {'status':'verified', 'tool_count':len(names), 'tools':sorted(names)} + except (OSError, ProbeError) as error: + return {'status':'failed', 'reason':str(error) if isinstance(error, ProbeError) else 'process_io_error'} + finally: + stop(proc) diff --git a/scripts/runtime-policy.py b/scripts/runtime-policy.py new file mode 100644 index 00000000..b1c5881e --- /dev/null +++ b/scripts/runtime-policy.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Shared installer target contract; no network calls or package mutations.""" +import argparse +import json +from pathlib import Path +import re + +POLICY = json.loads((Path(__file__).resolve().parents[1] / 'config/installer-runtime.json').read_text()) + + +def version(value): + match = re.fullmatch(r'v?(\d+)\.(\d+)\.(\d+)', value.strip()) + if not match: + raise ValueError('Expected a stable major.minor.patch version') + return tuple(map(int, match.groups())) + + +def node_supported(value, runtime='openclaw'): + try: + actual = version(value) + except ValueError: + return False + return any(actual >= tuple(rule['min']) and ( + 'max_exclusive' not in rule or actual < tuple(rule['max_exclusive']) + ) for rule in POLICY['node_ranges'][runtime]) + + +def npm_args(value): + actual = version(value) + args = ['install', '-g', POLICY['openclaw_package']] + if actual >= (11, 16, 0): + args.append('--allow-scripts=openclaw') + return args + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--runtime', choices=['openclaw', 'hermes'], default='openclaw') + parser.add_argument('--node') + parser.add_argument('--npm') + args = parser.parse_args() + if args.npm is not None: + try: + print('\n'.join(npm_args(args.npm))) + return 0 + except ValueError as error: + parser.error(str(error)) + print('Node.js ' + POLICY['node_descriptions'][args.runtime] + + ' required; select a supported version with your Node version manager (for example nvm install 26).') + return 0 if args.node is None or node_supported(args.node, args.runtime) else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/verify-catalog-coverage.py b/scripts/verify-catalog-coverage.py index d3557c47..3143da04 100755 --- a/scripts/verify-catalog-coverage.py +++ b/scripts/verify-catalog-coverage.py @@ -65,6 +65,8 @@ # clean prefix/suffix relationship to the id itself (pre-existing naming # conventions that predate this feature, plus this feature's own additions). GROUPED_CONFIG_EXACT = { + "drawio-mcp": "drawio-rfc", + "rfc-mcp": "drawio-rfc", "sketchfab-mcp": "threejs-viz", "comfyui-mcp": "comfyui-viz", "topology-diagram-mcp": "comfyui-viz", # spec 121 federated pipeline, Stage A @@ -118,15 +120,15 @@ "tavus-pal-mcp": "optional HUD-invoked stdio facade; deliberately absent from the main agent registry to prevent recursive agent dispatch (spec 144)", "netclaw-dot-mcp": "operator-run HTTP service (netclaw-dot.service) that ChatGPT Dots call over OAuth; deliberately NOT registered as an agent MCP (spec 134)", "gait_mcp": "registered as 'gait-mcp'; underscore/hyphen naming mismatch", - "pyATS_MCP": "external — installed on demand via pip (EXTERNAL_INTEGRATIONS: pyATS)", + "pyATS_MCP": "registered pyats-mcp uses the managed-source stdio bridge (spec 147)", "ISE_MCP": "external — installed on demand (EXTERNAL_INTEGRATIONS: Cisco ISE)", "ACI_MCP": "external — installed on demand (EXTERNAL_INTEGRATIONS: Cisco ACI)", - "Wikipedia_MCP": "external — installed on demand (EXTERNAL_INTEGRATIONS: Wikipedia)", - "markmap_mcp": "external — installed on demand (EXTERNAL_INTEGRATIONS: Markmap)", - "mcp-nvd": "external — installed on demand (EXTERNAL_INTEGRATIONS: NVD CVE)", + "Wikipedia_MCP": "registered wikipedia-mcp uses the installed component launcher (spec 147)", + "markmap_mcp": "registered markmap-mcp uses the installed component launcher (spec 147)", + "mcp-nvd": "registered nvd-cve-mcp uses the installed component launcher (spec 147)", "mcp-nautobot": "external community alternative (EXTERNAL_INTEGRATIONS: Nautobot community)", - "packet-buddy-mcp": "external — installed on demand (EXTERNAL_INTEGRATIONS: Packet Buddy)", - "subnet-calculator-mcp": "external — bundled (EXTERNAL_INTEGRATIONS: Subnet Calculator)", + "packet-buddy-mcp": "registered packet-buddy-mcp uses its isolated runtime (spec 147)", + "subnet-calculator-mcp": "registered subnet-calc-mcp uses the installed component launcher (spec 147)", "CiscoFMC-MCP-server-community": "community source for the 'fmc' component; registered as 'cisco-fmc-mcp'", "nautobot-mcp-v2": "backs the registered 'nautobot-mcp' entry (directory name differs from key)", "AAP-Enterprise-MCP-Server": "single source backing all four registered aap-* entries", diff --git a/scripts/verify-inventory-counts.py b/scripts/verify-inventory-counts.py index 1e651853..88725c1c 100755 --- a/scripts/verify-inventory-counts.py +++ b/scripts/verify-inventory-counts.py @@ -41,18 +41,14 @@ # Servers table and mcp-servers/ vendored directories as of 2026-07-07. EXTERNAL_INTEGRATIONS = [ "Tavus Pal", - "pyATS", "F5 BIG-IP", "Catalyst Center", "Cisco ACI", "Cisco ISE", - "NetBox", "Nautobot (community, aiopnet/mcp-nautobot)", "Itential IAP", - "ServiceNow", "Microsoft Graph", "GitHub", - "Packet Buddy", "Cisco CML", "Cisco NSO", "Cisco FMC", @@ -70,13 +66,7 @@ "GCP Cloud Monitoring", "GCP Cloud Logging", "GCP Resource Manager", - "NVD CVE", - "Subnet Calculator", "GAIT", - "Wikipedia", - "Markmap", - "Draw.io", - "RFC Lookup", "Juniper JunOS", "Arista CVP", "UML MCP", @@ -86,8 +76,6 @@ "Grafana", "Prometheus", "Kubeshark", - "nmap", - "gtrace", "AAP Controller", "AAP EDA", "AAP Lint", @@ -107,7 +95,6 @@ "Memory MCP", "SNMP Trap Receiver", "Syslog Receiver", - "Text-to-Speech (TTS)", # Installed via OpenClaw's own ClawHub skill-marketplace mechanism, not a # vendored mcp-servers/ clone -- see spec 050 research.md R3. "Computer Use", diff --git a/specs/147-installer-followups/blog-draft.md b/specs/147-installer-followups/blog-draft.md new file mode 100644 index 00000000..e675581c --- /dev/null +++ b/specs/147-installer-followups/blog-draft.md @@ -0,0 +1,22 @@ +# Draft: Make installation results mean something + +NetClaw's macOS and Linux feedback showed why an installer must check more than +files on disk. A component could appear installed while the agent lacked its +registration, launched a different Python, or failed its first protocol handshake. + +Spec 147 builds on Nick's host-preflight and Zabbix fix. It adds native registrations +for the twelve reported missing components, shares launch bindings with existing +skills, tightens Node/npm bootstrap checks and records discovery separately from +endpoint readiness. Remote Ollama is checked at its configured host rather than +assuming it requires a local executable. + +The most useful test result was an HTTP probe that exited successfully without +exposing a tool. The new check examines the result itself. Real OpenClaw discovery +also passed for four component catalogs in disposable environments, and a calculator +canary returned the expected TEST-NET subnet. + +These checks do not certify every external appliance or every model's answers. +They make the evidence and remaining gaps visible, so the next failure has a place +to start. User credentials, persona files and approval controls remain preserved. + +Draft for John's review. WordPress was unavailable in this session; not published. diff --git a/specs/147-installer-followups/implementation-probes.json b/specs/147-installer-followups/implementation-probes.json new file mode 100644 index 00000000..eefcdf19 --- /dev/null +++ b/specs/147-installer-followups/implementation-probes.json @@ -0,0 +1,137 @@ +{ + "native": { + "schema_version": 1, + "runtime": "openclaw", + "components": [ + { + "component": "packet-buddy", + "installation": "recorded", + "servers": [ + { + "server": "packet-buddy-mcp", + "access": "native_mcp", + "registration": "verified", + "artifacts": "verified", + "discovery": { + "status": "verified", + "tool_count": 12 + }, + "endpoint": "unverified" + } + ], + "status": "discovery_verified" + }, + { + "component": "subnet-calc", + "installation": "recorded", + "servers": [ + { + "server": "subnet-calc-mcp", + "access": "native_mcp", + "registration": "verified", + "artifacts": "verified", + "discovery": { + "status": "verified", + "tool_count": 1 + }, + "endpoint": "unverified" + } + ], + "status": "discovery_verified" + }, + { + "component": "tts", + "installation": "recorded", + "servers": [ + { + "server": "tts-mcp", + "access": "native_mcp", + "registration": "verified", + "artifacts": "verified", + "discovery": { + "status": "verified", + "tool_count": 2 + }, + "endpoint": "unverified" + } + ], + "status": "discovery_verified" + }, + { + "component": "netbox", + "installation": "recorded", + "servers": [ + { + "server": "netbox-mcp", + "access": "native_mcp", + "registration": "verified", + "artifacts": "verified", + "discovery": { + "status": "verified", + "tool_count": 4 + }, + "endpoint": "unverified" + } + ], + "status": "discovery_verified" + } + ], + "endpoint_operations": "unverified", + "agent_turn": "unverified", + "ok": true, + "provider": { + "status": "unverified", + "reason": "not_an_ollama_primary", + "tool_calling": "unverified" + } + }, + "subnet_canary": { + "exit": 0, + "result": { + "content": [ + { + "text": "{\"network_address\":\"192.0.2.0\",\"broadcast_address\":\"192.0.2.3\",\"netmask\":\"255.255.255.252\",\"wildcard_mask\":\"0.0.0.3\",\"prefix_length\":30,\"with_netmask\":\"192.0.2.0/255.255.255.252\",\"with_hostmask\":\"192.0.2.0/0.0.0.3\",\"num_addresses\":4,\"usable_hosts_count\":2,\"usable_hosts_preview\":[\"192.0.2.1\",\"192.0.2.2\"],\"first_usable\":\"192.0.2.1\",\"last_usable\":\"192.0.2.2\",\"address_range\":\"192.0.2.1 - 192.0.2.2\",\"previous_subnet\":\"192.0.1.252/30\",\"next_subnet\":\"192.0.2.4/30\",\"host_bits\":2,\"total_bits\":32,\"is_private\":true,\"is_global\":false,\"is_link_local\":false,\"is_multicast\":false,\"is_loopback\":false,\"is_reserved\":false,\"is_unspecified\":false,\"summary\":\"CIDR 192.0.2.0/30 has 4 total addresses with 2 usable hosts (30 prefix, 2 host bits).\",\"note\":\"Showing first 10 usable hosts only (capped for large subnets).\"}", + "type": "text" + } + ], + "isError": false, + "structuredContent": { + "network_address": "192.0.2.0", + "broadcast_address": "192.0.2.3", + "netmask": "255.255.255.252", + "wildcard_mask": "0.0.0.3", + "prefix_length": 30, + "with_netmask": "192.0.2.0/255.255.255.252", + "with_hostmask": "192.0.2.0/0.0.0.3", + "num_addresses": 4, + "usable_hosts_count": 2, + "usable_hosts_preview": [ + "192.0.2.1", + "192.0.2.2" + ], + "first_usable": "192.0.2.1", + "last_usable": "192.0.2.2", + "address_range": "192.0.2.1 - 192.0.2.2", + "previous_subnet": "192.0.1.252/30", + "next_subnet": "192.0.2.4/30", + "host_bits": 2, + "total_bits": 32, + "is_private": true, + "is_global": false, + "is_link_local": false, + "is_multicast": false, + "is_loopback": false, + "is_reserved": false, + "is_unspecified": false, + "summary": "CIDR 192.0.2.0/30 has 4 total addresses with 2 usable hosts (30 prefix, 2 host bits).", + "note": "Showing first 10 usable hosts only (capped for large subnets)." + } + }, + "stderr_present": false, + "requested_cidr_returned": true + }, + "runtime": "OpenClaw 2026.7.1-2", + "node": "24.19.0", + "python": "3.12.12", + "scope": "Temporary HOME/config/venv; installer-reviewed calculator patch applied to temporary source; synthetic NetBox credentials; discovery only plus local TEST-NET calculator call; no vendor/device/model calls." +} diff --git a/specs/147-installer-followups/plan.md b/specs/147-installer-followups/plan.md index 1adef62e..354efe5d 100644 --- a/specs/147-installer-followups/plan.md +++ b/specs/147-installer-followups/plan.md @@ -1,7 +1,7 @@ # Implementation Plan: Installer and first-use reliability **Branch**: `147-installer-followups` | **Updated**: 2026-10-10 | **Spec**: [spec.md](spec.md) -**Status**: Concrete scope defined; production implementation pending. +**Status**: Implemented; verification and live boundaries are recorded in verification.md. ## Technical context diff --git a/specs/147-installer-followups/reports.md b/specs/147-installer-followups/reports.md index 5c069510..c56f7b12 100644 --- a/specs/147-installer-followups/reports.md +++ b/specs/147-installer-followups/reports.md @@ -45,3 +45,13 @@ The report describes Debian/Ubuntu, Node 20.20.2, 23 selected components, remote - Do not treat `openclaw mcp probe` exit 0 as proof of tool discovery. See [research](research.md) for sources and [verification](verification.md) for measured checks. + +## Shipped disposition after spec 147 implementation + +- L2/L3/L4: one Node target contract; npm-version-aware first-party script approval; user-owned prefix on permission conflicts; install/executable failure propagation. +- L8/L9: all twelve components have thirteen selected native registrations and a shared native/skill launch contract. Module paths and recorded interpreters are used. Existing pyATS bridge is retained. +- L5/L13: structured discovery and configured Ollama endpoint/model checks, truthful failure exits and explicit configuration-required/unverified stages. +- L10: synthetic transports and four actual component catalogs verified through installed OpenClaw; original CML/runtime combination remains unreproduced, not labelled fixed upstream. +- L12: failed-tool exits, deterministic readiness, SOUL guidance and a real local calculator canary added. Arbitrary Ollama answer grounding is not certified. +- L1/L6/L7/L11 and macOS preflight/Zabbix fixes: retained and regression-tested. No need to reimplement merged fixes. +- Six unnamed macOS failures and real network/lab acceptance still need the missing evidence; no blanket all-components installation claim is made. diff --git a/specs/147-installer-followups/reproduce-installed-components.py b/specs/147-installer-followups/reproduce-installed-components.py new file mode 100644 index 00000000..9a1da77a --- /dev/null +++ b/specs/147-installer-followups/reproduce-installed-components.py @@ -0,0 +1,49 @@ +"""Disposable component/native discovery and calculator canary; no real credentials.""" +from pathlib import Path +import argparse +import json, os, subprocess, sys, tempfile, shutil +parser=argparse.ArgumentParser(description='Prepare a disposable test venv and probe four installed components; downloads declared test dependencies with uv.') +parser.add_argument('--node-bin', type=Path, required=True) +parser.add_argument('--output', type=Path, required=True) +args=parser.parse_args() +root=Path(__file__).resolve().parents[2] +node_bin=args.node_bin.resolve() +components=['packet-buddy','subnet-calc','tts','netbox'] +results={} +with tempfile.TemporaryDirectory(prefix='netclaw147-owned-probe-') as directory: + home=Path(directory); runtime=home/'python-runtimes'; records=runtime/'records';records.mkdir(parents=True) + stage=home/'source'; (stage/'mcp-servers').mkdir(parents=True) + shutil.copytree(root/'mcp-servers/subnet-calculator-mcp',stage/'mcp-servers/subnet-calculator-mcp',ignore=shutil.ignore_patterns('.git','.venv')) + subprocess.run([sys.executable,str(root/'scripts/apply-fastmcp-patches.py'),'--root',str(stage),'--component','subnet-calc'],check=True) + subprocess.run(['uv','venv','--python','3.12',str(home/'test-venv')],check=True) + python=home/'test-venv/bin/python' + subprocess.run(['uv','pip','install','--python',str(python),'-r',str(root/'mcp-servers/tts-mcp/requirements.txt'), + 'python-dotenv','pydantic-settings','requests'],check=True) + for component in components: (records/component).write_text(str(python)) + env={'HOME':str(home),'PATH':str(node_bin)+':/usr/bin:/bin','NETCLAW_RUNTIME_ROOT':str(runtime), + 'NETCLAW_RUNTIME_ENV':str(home/'.env'),'OPENCLAW_STATE_DIR':str(home),'PYTHON_DOTENV_DISABLED':'1', + 'SUBNET_MCP_SCRIPT':str(stage/'mcp-servers/subnet-calculator-mcp/servers/subnetcalculator_mcp.py'), + 'NETBOX_URL':'http://127.0.0.1:9','NETBOX_TOKEN':'fixture','FASTMCP_CHECK_FOR_UPDATES':'off','TTS_OUTPUT_DIR':str(home/'tts'),'PACKET_BUDDY_PCAP_DIR':str(home/'pcaps')} + for component in components: + r=subprocess.run([sys.executable,str(root/'scripts/mcp-call.py'),'--component',component,'--list-tools'],env=env,cwd=home,capture_output=True,text=True,timeout=35) + assert r.returncode==0,(component,r.stderr) + (home/'.env').write_text(''.join(key+'='+json.dumps(value)+'\n' for key,value in env.items())) + config=home/'openclaw.json' + r=subprocess.run([sys.executable,str(root/'scripts/install-mcp-config.py'),'--repo',str(root), + '--runtime-root',str(runtime),'--components',' '.join(components),'--output',str(home/'generated.json'), + '--config',str(config)],env=env,capture_output=True,text=True) + assert r.returncode==0,r.stderr + r=subprocess.run([sys.executable,str(root/'scripts/installer-readiness.py'),'--runtime-root',str(runtime), + '--config',str(config),'--components',' '.join(components),'--probe','--output',str(home/'readiness.json')], + env=env,capture_output=True,text=True,timeout=110) + print(r.stdout,r.stderr) + results['native']=json.loads((home/'readiness.json').read_text()) + r=subprocess.run([sys.executable,str(root/'scripts/mcp-call.py'),'--component','subnet-calc','subnet_calculator', + '{"cidr":"192.0.2.0/30"}'],env=env,cwd=home,capture_output=True,text=True,timeout=45) + assert not r.stderr,r.stderr + results['subnet_canary']={'exit':r.returncode,'result':json.loads(r.stdout) if r.stdout else {}, + 'stderr_present':bool(r.stderr)} + # Actual response must contain the requested TEST-NET CIDR, never model prose. + results['subnet_canary']['requested_cidr_returned']='192.0.2.0' in r.stdout and '255.255.255.252' in r.stdout +print(json.dumps(results,indent=2)) +args.output.write_text(json.dumps(results,indent=2)+'\n') diff --git a/specs/147-installer-followups/research.md b/specs/147-installer-followups/research.md index faf0ca6b..eabf520e 100644 --- a/specs/147-installer-followups/research.md +++ b/specs/147-installer-followups/research.md @@ -29,3 +29,26 @@ The reports mix historical defects, dependency/platform requirements and residua Build on merged preflight, isolation, deployment and dotenv helpers. First fix runtime compatibility/bootstrap, then close explicit tool access and launcher gaps, then add trustworthy readiness/first-use verification. Preserve transport boundaries and existing approvals. Diagnose original CML failures with matched command/env/cwd/version and a minimal protocol fixture before proposing an upstream patch. No real credentials, package installation, model/provider request, daemon restart or device operation was performed for this investigation. + +## Implementation decisions + +Owner explicitly requested fixes followed by commit/PR/merge. Added thirteen native +registrations for all twelve reported components; moved the same thirteen names +out of the external inventory so integration totals remain unchanged. Both native +registrations and recognized legacy skill invocations share a stdlib exec launcher. +The launcher reads literal runtime dotenv settings and recorded interpreter paths; +NetBox/ServiceNow/NVD use module invocation with their source roots. ServiceNow's +low-level SDK1 remains isolated from MCP2. No approval policy or write gate is changed. + +Readiness uses actual OpenClaw discovery and its structured server/tool results. +Hermes has a direct stdio probe with agent discovery explicitly unverified. Endpoint +operations are always a separate unverified stage. Ollama checks only native model +catalog/capabilities, not model generation or tool-call correctness. This bounds the +claim we can substantiate instead of treating a successful install as live acceptance. + +Initial four-component smoke preparation exposed test-environment gaps: an +unpatched legacy calculator copy, an absent edge-tts dependency, and settings not +persisted into the temporary runtime dotenv. Applying the existing reviewed patch, +installing declared dependencies in a disposable venv and writing fixture settings +reproduced a fresh install's layout. All four then passed native discovery. +No operator component environment was rebuilt or altered by that smoke test. diff --git a/specs/147-installer-followups/spec.md b/specs/147-installer-followups/spec.md index b4780826..75fa6b27 100644 --- a/specs/147-installer-followups/spec.md +++ b/specs/147-installer-followups/spec.md @@ -2,7 +2,7 @@ **Feature Branch**: `147-installer-followups` **Created**: 2026-10-10 -**Status**: Specified from user reports and current-main investigation; implementation pending +**Status**: Implemented for confirmed scoped defects; live acceptance boundaries recorded **Baseline**: `b4334bf255cd53aecd2e81f51abe63deea53f1df` (includes Nick's merged PR #287 and dotenv PR #288). ## Problem and evidence diff --git a/specs/147-installer-followups/tasks.md b/specs/147-installer-followups/tasks.md index 88944a34..c4e7242a 100644 --- a/specs/147-installer-followups/tasks.md +++ b/specs/147-installer-followups/tasks.md @@ -11,28 +11,30 @@ ## Phase A — Runtime compatibility/bootstrap -- [ ] T007 Add shared target-aware Node compatibility rule and boundary fixtures. -- [ ] T008 Add version-aware narrow npm lifecycle-script handling and rootless install recovery. -- [ ] T009 Ensure runtime-install/executable failure propagation and clear Python/uv guidance. +- [x] T007 Add shared target-aware Node compatibility rule and boundary fixtures. +- [x] T008 Add version-aware narrow npm lifecycle-script handling and rootless install recovery. +- [x] T009 Ensure runtime-install/executable failure propagation and clear Python/uv guidance. ## Phase B — Component access -- [ ] T010 Declare access paths and expected capabilities for all twelve reported components. -- [ ] T011 Bind supported native registrations and skill launchers to managed runtimes. -- [ ] T012 Verify pyATS/CML and representative utility discovery without bypassing approval controls. -- [ ] T013 Preserve operator state and reconcile inventory/registration/documentation surfaces. +- [x] T010 Declare access paths and expected capabilities for all twelve reported components. +- [x] T011 Bind supported native registrations and skill launchers to managed runtimes. +- [x] T012 Verify pyATS/CML and representative utility discovery without bypassing approval controls. +- [x] T013 Preserve operator state and reconcile inventory/registration/documentation surfaces. ## Phase C — First-use readiness -- [ ] T014 Add structured readiness stages and bounded discovery with expected-tool checks. -- [ ] T015 Check configured local/remote Ollama endpoint/model readiness and document deferred/live checks. -- [ ] T016 Add evidence-backed synthetic canary and unavailable-tool negative cases. -- [ ] T017 Reproduce any residual CML failure on matching versions before assigning upstream cause. +- [x] T014 Add structured readiness stages and bounded discovery with expected-tool checks. +- [x] T015 Check configured local/remote Ollama endpoint/model readiness and document deferred/live checks. +- [x] T016 Add evidence-backed synthetic canary and unavailable-tool negative cases. +- [x] T017 Record transport diagnosis and limits: synthetic transports plus four real catalogs verified on installed OpenClaw; exact historical CML/runtime trace unavailable, no upstream cause assigned. ## Completion -- [ ] T018 Run appropriate unit/installer contracts and declaration/reconciliation checks. -- [ ] T019 Record real-host/live acceptance boundaries and operator recovery instructions. -- [ ] T020 Prepare one patch release proposal after implementation completes. +- [x] T018 Run appropriate unit/installer contracts and declaration/reconciliation checks. +- [x] T019 Record real-host/live acceptance boundaries and operator recovery instructions. +- [x] T020 Prepare one patch release proposal after implementation completes. The six unnamed macOS component failures and exact original CML/runtime diagnostics remain pending evidence. They do not prevent work on the confirmed scoped defects; they are not invented acceptance cases. + +Release proposal: 1.6.2. PR/merge completion is recorded in verification.md once CI passes. Original live target acceptance remains a documented external evidence gap. diff --git a/specs/147-installer-followups/verification.md b/specs/147-installer-followups/verification.md index 0d8e3060..b289cd9c 100644 --- a/specs/147-installer-followups/verification.md +++ b/specs/147-installer-followups/verification.md @@ -1,8 +1,8 @@ -# Verification — investigation on 2026-10-10 +# Verification — investigation and implementation on 2026-10-10 ## Baseline -Local main fast-forwarded to `b4334bf255cd53aecd2e81f51abe63deea53f1df` and merged into `147-installer-followups` as `cf12ec1`. Includes PR #287's host preflight/Zabbix fixes and PR #288's dotenv fix. No new production installer implementation is claimed here. +Local main fast-forwarded to `b4334bf255cd53aecd2e81f51abe63deea53f1df` and merged into `147-installer-followups` as `cf12ec1`. Includes PR #287's host preflight/Zabbix fixes and PR #288's dotenv fix. Implementation below follows this baseline. ## Existing regressions @@ -49,4 +49,18 @@ The original log archive is unavailable; six macOS component failures are unname - Final portable reproduction helper rerun: completed all three cases and reproduced the discovery results above. - `git diff --check`: PASS. -Implementation and live acceptance remain pending tasks, not passing results. +## Implementation verification + +- `python3 scripts/run-contract-tests.py --suite unit --json`: PASS, **826 passed / 2 skipped**, 94 isolated test files, 82.10 seconds. Includes the new runtime policy, launcher, readiness and failure-exit cases. +- Reconciliation: catalog, documentation counts, registration portability, package references and dependency policy all PASS. +- FastMCP compatibility: 36 owned servers, zero failures. MCP Tasks adoption: PASS. +- Bash syntax, Python compilation, spec artifacts and `git diff --check`: PASS. +- [Actual component probes](implementation-probes.json): OpenClaw 2026.7.1-2 / Node 24.19.0 on macOS arm64; temporary HOME, runtime config and Python 3.12.12 venv. Native discovery passed for Packet Buddy (12 tools), Subnet Calculator (1), TTS (2) and NetBox (4). NetBox used synthetic loopback credentials; no endpoint call was made. +- Calculator canary: actual `mcp-call --component subnet-calc` returned the expected 192.0.2.0/30 network, /30 mask and two usable hosts. No model generated that result. +- Negative cases cover RPC initialization error, closed/partial stdout, timeout, empty catalogs with exit 0, missing registration/runtime/credentials, and missing/unreachable/non-tool-capable Ollama models. + +To reproduce the four-component smoke, use [reproduce-installed-components.py](reproduce-installed-components.py) with `--node-bin /absolute/node/bin --output /tmp/netclaw-components.json`. It explicitly downloads declared test dependencies using uv into a disposable environment; it applies the existing reviewed calculator migration only to temporary source. It does not install or change operator component runtimes. Runtime dotenv settings are part of the fixture, matching installer deployment. + +The initial smoke setup intentionally exposed failures when dependencies/patches/settings were absent; readiness flagged them instead of reporting success. After preparing declared source/dependencies and persisting fixture settings, all four passed. This is representative discovery evidence, not a full-catalog fresh-host install or the original CML/pyATS endpoint reproduction. + +Release metadata: proposed patch 1.6.2. Live acceptance and arbitrary model answer grounding remain unverified; no tag or published release is implied. PR CI/merge evidence follows below. diff --git a/tests/unit/test_installer_exit_status.py b/tests/unit/test_installer_exit_status.py index 825feca2..6163c7bd 100644 --- a/tests/unit/test_installer_exit_status.py +++ b/tests/unit/test_installer_exit_status.py @@ -7,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[2] -@pytest.mark.parametrize('failure', ['component', 'verification', 'tokens', 'swallowed-pip', 'none']) +@pytest.mark.parametrize('failure', ['component', 'verification', 'tokens', 'swallowed-pip', 'readiness', 'none']) def test_cli_failure_is_visible_to_automation(tmp_path, failure): scripts = tmp_path / 'scripts' scripts.mkdir() @@ -18,6 +18,7 @@ def test_cli_failure_is_visible_to_automation(tmp_path, failure): (tmp_path / 'mcp-servers/pyATS_MCP/pyats_mcp_server.py').touch() # Host checks have their own entrypoint suite; isolate downstream exit handling. (scripts / 'installer-preflight.py').write_text('raise SystemExit(0)\n') + (scripts / 'installer-readiness.py').write_text('import os\nraise SystemExit(1 if os.environ["FIXTURE_FAILURE"] == "readiness" else 0)\n') (scripts / 'mcp-call.py').touch() (scripts / 'netclaw').write_text('#!/bin/sh\nexit 0\n') (scripts / 'netclaw').chmod(0o700) diff --git a/tests/unit/test_installer_followups.py b/tests/unit/test_installer_followups.py new file mode 100644 index 00000000..32359430 --- /dev/null +++ b/tests/unit/test_installer_followups.py @@ -0,0 +1,127 @@ +"""Regression coverage for installer feedback; no real installs or operator config.""" +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[2] + + +def load(name): + spec = importlib.util.spec_from_file_location(name.replace('-', '_'), ROOT / 'scripts' / (name + '.py')) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +policy = load('runtime-policy') +launch = load('component-launch') +register = load('install-mcp-config') + + +@pytest.mark.parametrize('value,ok', [('18.20.8', False), ('20.20.2', False), ('22.22.0', False), + ('24.15.9', False), ('24.16.0', True), ('v24.19.0', True), ('25.9.0', False), + ('26.0.9', False), ('26.1.0', True), ('26.11.0', True), ('26.1.0-rc.1', False), ('garbage', False)]) +def test_openclaw_node_boundaries(value, ok): + assert policy.node_supported(value) is ok + + +def test_hermes_does_not_inherit_openclaw_floor(): + assert policy.node_supported('22.0.0', 'hermes') + + +@pytest.mark.parametrize('version,flag', [('10.9.0',False),('11.15.9',False),('11.16.0',True),('12.0.0',True)]) +def test_npm_script_policy(version, flag): + args = policy.npm_args(version) + assert ('--allow-scripts=openclaw' in args) is flag + assert not any('koffi' in arg or '=all' in arg for arg in args) + + +def test_all_reported_components_registered(): + templates = json.loads((ROOT / 'config/openclaw.json').read_text())['mcpServers'] + assert len(launch.CONTRACT) == 12 + for component, rule in launch.CONTRACT.items(): + for server in rule['servers']: + entry = templates[server] + assert register.component_for(server) == component + assert entry['transport'] == 'stdio' + assert entry['args'][1:3] == ['scripts/component-launch.py', component] + assert not any(k in entry for k in ('approval','autoApprove','toolFilter')) + + +def make_runtime(tmp_path, component): + runtime = tmp_path / 'runtimes' + record = runtime / 'records' / component + record.parent.mkdir(parents=True, exist_ok=True) + python = tmp_path / 'component/bin/python' + python.parent.mkdir(parents=True, exist_ok=True) + python.symlink_to(sys.executable) + record.write_text(str(python)) + return runtime, python + + +def test_netbox_module_uses_record_and_source_not_gateway_path(tmp_path): + runtime, python = make_runtime(tmp_path, 'netbox') + env = {'NETCLAW_RUNTIME_ROOT': str(runtime), 'PATH': '/wrong/python/bin', 'NETBOX_TOKEN': 'fixture'} + parts, child_env, cwd = launch.resolve('netbox', env=env) + assert parts == [str(python), '-u', '-m', 'netbox_mcp_server.server', '--transport', 'stdio'] + assert child_env['PYTHONPATH'] == str(ROOT / 'mcp-servers/netbox-mcp-server/src') + assert child_env['NETBOX_TOKEN'] == 'fixture' + assert cwd == str(ROOT) + + +def test_missing_record_fails_instead_of_global_fallback(tmp_path): + with pytest.raises(ValueError, match='interpreter record'): + launch.resolve('netbox', env={'NETCLAW_RUNTIME_ROOT':str(tmp_path / 'runtimes')}) + + +def test_literal_env_precedence_and_custom_script_preserved(tmp_path): + runtime, python = make_runtime(tmp_path, 'netbox') + custom = tmp_path / 'custom.py'; custom.write_text('') + (tmp_path / '.env').write_text('NETBOX_TOKEN="$(do-not-execute)"\nNETBOX_URL="https://fixture.invalid"\n') + parts, env, _ = launch.resolve('netbox', env={'NETCLAW_RUNTIME_ROOT':str(runtime), + 'NETBOX_TOKEN':'explicit', 'NETBOX_MCP_SCRIPT':str(custom)}) + assert parts[:3] == [str(python), '-u', str(custom)] + assert env['NETBOX_TOKEN'] == 'explicit' + assert env['NETBOX_URL'] == 'https://fixture.invalid' + + +def test_legacy_skill_matches_only_owned_invocations(): + cmd=['python3','-u',str(ROOT/'mcp-servers/netbox-mcp-server/src/netbox_mcp_server/server.py')] + assert launch.legacy_component(cmd,{}) == ('netbox','netbox-mcp') + assert launch.legacy_component(['python3','/operator/custom.py'],{}) is None + + +def test_generated_entries_carry_runtime_location_without_credentials(tmp_path): + out=tmp_path/'selected.json' + result=subprocess.run([sys.executable,str(ROOT/'scripts/install-mcp-config.py'), '--repo',str(ROOT), + '--runtime-root',str(tmp_path/'runtimes'),'--components',' '.join(launch.CONTRACT), '--output',str(out)], + capture_output=True,text=True) + assert result.returncode==0,result.stderr + entries=json.loads(out.read_text())['mcpServers'] + assert len(entries)==13 + for entry in entries.values(): + assert Path(entry['command']).is_absolute() + assert entry['env']['NETCLAW_RUNTIME_ROOT']==str(tmp_path/'runtimes') + assert set(entry['env'])=={'NETCLAW_RUNTIME_ROOT','NETCLAW_RUNTIME_ENV'} + + +@pytest.mark.parametrize('npm_rc,version_rc,expected', [(0,0,0),(1,0,1),(0,1,1)]) +def test_npm_failure_and_executable_check_propagate(tmp_path,npm_rc,version_rc,expected): + bin_dir=tmp_path/'bin';bin_dir.mkdir() + for name,body in { + 'node':'echo v24.16.0', + 'npm':f'case "$1" in --version) echo 12.0.0;; config) echo "{tmp_path}";; *) printf "%s\\n" "$@" > "{tmp_path}/args"; exit {npm_rc};; esac', + 'openclaw':f'exit {version_rc}', + 'sudo':'exit 99', + }.items(): + p=bin_dir/name;p.write_text('#!/bin/sh\n'+body+'\n');p.chmod(0o700) + env={**os.environ,'PATH':str(bin_dir)+os.pathsep+os.environ['PATH'],'HOME':str(tmp_path),'NETCLAW_DIR':str(ROOT)} + cmd='source scripts/lib/runtime-install.sh\nlog_info() { :; }; log_error() { :; }; netclaw_install_openclaw' + r=subprocess.run(['bash','-c',cmd],cwd=ROOT,env=env,capture_output=True,text=True) + assert r.returncode==expected,r.stdout+r.stderr + assert '--allow-scripts=openclaw' in (tmp_path/'args').read_text() diff --git a/tests/unit/test_installer_preflight.py b/tests/unit/test_installer_preflight.py index 8edeb278..b00ccb2e 100644 --- a/tests/unit/test_installer_preflight.py +++ b/tests/unit/test_installer_preflight.py @@ -21,7 +21,7 @@ def __init__(self, version=(3, 12), system='Darwin', arch='arm64', commands=()): self.commands = set(POLICY['core_commands']) | {'python3'} | set(commands) self.python_info = {'version': version, 'os': system, 'arch': arch, 'venv': True, 'ensurepip': True, 'isolated': True, 'libc': ['glibc','2.36']} - self.outputs = {('node', '--version'): 'v22.0.0', ('go', 'version'): 'go version go1.25.1 darwin/arm64', + self.outputs = {('node', '--version'): 'v24.16.0', ('go', 'version'): 'go version go1.25.1 darwin/arm64', ('go', 'env', 'CGO_ENABLED'): '1', ('go', 'env', 'CC'): 'clang', ('xcode-select', '-p'): '/fixture/CommandLineTools', ('docker', 'info', '--format', '{{.ServerVersion}}'): '28.0.0'} @@ -194,7 +194,7 @@ def test_installer_preflight_never_writes_operator_state(tmp_path, preflight, co bin_dir = tmp_path/'bin'; bin_dir.mkdir() (bin_dir/'python3').symlink_to(sys.executable) for name in ('node','npm','npx'): - executable(bin_dir/name, 'if [ "$1" = --version ]; then echo v22.0.0; exit 0; fi\necho unexpected-install >&2\nexit 42\n') + executable(bin_dir/name, 'if [ "$1" = --version ]; then echo v24.16.0; exit 0; fi\necho unexpected-install >&2\nexit 42\n') executable(bin_dir/'uname', 'case "$1" in -s) echo Darwin;; -m) echo arm64;; -r) echo fixture;; esac\n') args = ['/bin/bash','scripts/install.sh','--runtime','hermes','--components',component] if preflight: args.append('--preflight') diff --git a/tests/unit/test_installer_prerequisite_isolation.py b/tests/unit/test_installer_prerequisite_isolation.py index 2da4b9c5..a795bc8e 100644 --- a/tests/unit/test_installer_prerequisite_isolation.py +++ b/tests/unit/test_installer_prerequisite_isolation.py @@ -11,13 +11,14 @@ def test_prerequisites_do_not_disable_pep668(tmp_path): env.pop('PIP_BREAK_SYSTEM_PACKAGES', None) script = r''' set -eu +NETCLAW_DIR="$PWD" source scripts/lib/install-steps.sh log_step() { :; } log_info() { :; } log_warn() { :; } log_error() { return 1; } check_command() { return 0; } -node() { echo v22.0.0; } +node() { echo v24.16.0; } # The actual PEP668 detector sees a managed interpreter. python3() { return 0; } NETCLAW_PY=python3 diff --git a/tests/unit/test_installer_python_prerequisites.py b/tests/unit/test_installer_python_prerequisites.py index c3af1b76..b0bea4cb 100644 --- a/tests/unit/test_installer_python_prerequisites.py +++ b/tests/unit/test_installer_python_prerequisites.py @@ -47,14 +47,16 @@ def run(tmp_path, body, **extra): def test_prerequisite_checks(tmp_path, installer_old, base_old, component, uv_missing, ok): interpreter(tmp_path / 'base-python', supported=not base_old) result = run(tmp_path, r''' +NETCLAW_DIR="$PWD" source scripts/lib/install-steps.sh log_step() { echo "$*"; } log_info() { echo "$*"; } log_warn() { echo "$*"; } log_error() { echo "$*"; } check_command() { [ "$1" != uv ] || [ "$FIXTURE_UV_MISSING" != 1 ]; } -node() { echo v22.0.0; } +node() { echo v24.16.0; } python3() { + if [[ "$1" == */runtime-policy.py ]]; then command python3 "$@"; return; fi case "${2:-}" in *'sys.version_info >='*) [ "$FIXTURE_INSTALLER_OLD" != 1 ] ;; *) return 1 ;; diff --git a/tests/unit/test_installer_readiness.py b/tests/unit/test_installer_readiness.py new file mode 100644 index 00000000..b3eb4040 --- /dev/null +++ b/tests/unit/test_installer_readiness.py @@ -0,0 +1,126 @@ +"""Runtime discovery is evidence, not an exit code or a directory check.""" +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import time + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('readiness', ROOT/'scripts/installer-readiness.py') +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +@pytest.mark.parametrize('data,valid', [ + ({'servers':{'s':{'tools':1}}, 'tools':['mcp_s_expected']}, True), + ({'servers':{'s':{'tools':0}}, 'tools':[]},False), + ({'servers':{'other':{'tools':1}}, 'tools':['mcp_s_expected']},False), + ({'servers':{'s':{'tools':1}}, 'tools':['mcp_s_wrong']},False), + ({'servers':{'s':{'tools':1}}, 'tools':['expected'],'diagnostics':[{'message':'failed'}]},False), + ({'error':'failed'},False), +]) +def test_native_probe_requires_expected_discovery(data,valid): + assert module.native_result(data,'s',['expected']) is valid + + +@pytest.mark.parametrize('mode,reason', [('ok',None),('empty','expected_tools_missing'), + ('init-error','protocol_error'),('partial','timeout'),('closed','server_closed')]) +def test_real_stdio_lifecycle_and_negative_cases(tmp_path,mode,reason): + script=tmp_path/'fixture.py' + script.write_text('''import json,sys,time +mode=sys.argv[1] +for line in sys.stdin: + m=json.loads(line) + if mode=='closed': sys.exit(0) + if mode=='partial': sys.stdout.write('{');sys.stdout.flush();time.sleep(10) + if 'id' not in m: continue + if mode=='init-error': result={'error':{'code':-32603,'message':'fixture secret must not be emitted'}} + elif m['method']=='initialize': result={'result':{'protocolVersion':'2025-06-18','capabilities':{'tools':{}},'serverInfo':{'name':'fixture','version':'1'}}} + else: result={'result':{'tools':[] if mode=='empty' else [{'name':'expected','inputSchema':{'type':'object'}}]}} + print(json.dumps({'jsonrpc':'2.0','id':m['id'],**result}),flush=True) +''') + started=time.monotonic() + result=module.probe.discover([sys.executable,str(script),mode],{},str(tmp_path),timeout=.8,expected=['expected']) + assert time.monotonic()-started<4 + assert result['status']==('failed' if reason else 'verified') + if reason: assert result['reason']==reason + assert 'secret' not in json.dumps(result) + + +def ollama_config(): + return {'agents':{'defaults':{'model':{'primary':'ollama/network-model:latest'}}}, + 'models':{'providers':{'ollama':{'baseUrl':'http://fixture.invalid:11434/v1','apiKey':'${OLLAMA_API_KEY}'}}}} + + +def test_remote_ollama_uses_configured_endpoint_without_cli(): + calls=[] + def request(url,headers,body=None): + calls.append((url,headers,body)) + return {'models':[{'name':'network-model:latest'}]} if url.endswith('/tags') else {'capabilities':['tools','completion']} + result=module.ollama_readiness(ollama_config(),{'OLLAMA_API_KEY':'fixture-private'},True,request) + assert result=={'status':'verified','reason':'ollama_model_available','tool_calling':'advertised_not_exercised'} + assert calls[0][0]=='http://fixture.invalid:11434/api/tags' + assert calls[1][2]=={'model':'network-model:latest'} + assert 'fixture-private' not in json.dumps(result) + + +@pytest.mark.parametrize('outcome,reason',[('missing','ollama_model_missing'),('no-tools','ollama_model_has_no_tool_capability'), + ('offline','ollama_endpoint_or_api_unavailable')]) +def test_provider_errors_are_not_ready(outcome,reason): + def request(url,*_): + if outcome=='offline': raise OSError('secret URL/password in upstream exception') + if url.endswith('/tags'): return {'models':[] if outcome=='missing' else [{'name':'network-model:latest'}]} + return {'capabilities':['completion']} + result=module.ollama_readiness(ollama_config(),{'OLLAMA_API_KEY':'fixture'},True,request) + assert result['status']=='failed' + assert result['reason']==reason + assert 'secret' not in json.dumps(result) + + +def test_provider_check_is_opt_in_and_does_not_download(): + result=module.ollama_readiness(ollama_config(),{},False,lambda *_:pytest.fail('unexpected request')) + assert result['status']=='unverified' + + +def test_failed_install_and_missing_registration_cannot_be_success(tmp_path): + result=module.report(['netbox','pyats'],{'netbox'},{},{},{'NETCLAW_RUNTIME_ROOT':str(tmp_path)},tmp_path/'cfg','openclaw') + assert not result['ok'] + assert [r['status'] for r in result['components']]==['failed','failed'] + + +def test_artifacts_do_not_claim_discovery_or_endpoint_readiness(tmp_path): + result=module.check_server('fixture',{'command':sys.executable,'args':[], 'cwd':str(tmp_path)}, + 'fixture',{}, {},tmp_path/'cfg','openclaw',False,1) + assert result['artifacts']=='verified' + assert result['discovery']['status']=='unverified' + assert result['endpoint']=='unverified' + + +def test_missing_credentials_are_configuration_required_without_launch(tmp_path,monkeypatch): + monkeypatch.setattr(module.launcher,'resolve',lambda *a,**k: ([],{},str(tmp_path))) + monkeypatch.setattr(module,'native_probe',lambda *a,**k: pytest.fail('must not launch')) + result=module.check_server('netbox-mcp',{'command':sys.executable},'netbox',module.launcher.CONTRACT['netbox'], + {},tmp_path/'cfg','openclaw',True,1) + assert result['discovery']['status']=='configuration_required' + assert result['discovery']['variables']==['NETBOX_URL','NETBOX_TOKEN'] + + +def test_zero_exit_probe_with_empty_catalog_fails_cli(tmp_path): + bin_dir=tmp_path/'bin';bin_dir.mkdir() + cli=bin_dir/'openclaw' + cli.write_text('#!/bin/sh\nprintf \'%s\\n\' \'{"servers":{"bgp-intel-mcp":{"tools":0}},"tools":[]}\'\n') + cli.chmod(0o700) + config=tmp_path/'config.json' + config.write_text(json.dumps({'mcp':{'servers':{'bgp-intel-mcp':{'command':sys.executable}}}})) + out=tmp_path/'readiness.json' + env={'PATH':str(bin_dir)+os.pathsep+os.defpath,'HOME':str(tmp_path)} + result=subprocess.run([sys.executable,str(ROOT/'scripts/installer-readiness.py'), '--runtime-root',str(tmp_path/'runtimes'), + '--config',str(config),'--components','bgp-intel','--output',str(out),'--probe'],env=env,capture_output=True,text=True) + assert result.returncode==1,result.stdout+result.stderr + report=json.loads(out.read_text()) + assert report['components'][0]['status']=='failed' + assert out.stat().st_mode & 0o777 == 0o600 diff --git a/workspace/skills/drawio-diagram/SKILL.md b/workspace/skills/drawio-diagram/SKILL.md index 0612f9f9..ffbc0299 100644 --- a/workspace/skills/drawio-diagram/SKILL.md +++ b/workspace/skills/drawio-diagram/SKILL.md @@ -16,6 +16,8 @@ Generate network diagrams using two approaches: --- +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## Mode 1: Native File Generation (Official Skill-CLI) Generate native `.drawio` files directly. Optionally export to PNG, SVG, or PDF with the diagram XML embedded (so the exported file remains editable in draw.io). diff --git a/workspace/skills/gtrace-path-analysis/SKILL.md b/workspace/skills/gtrace-path-analysis/SKILL.md index 2e38f792..af7405df 100644 --- a/workspace/skills/gtrace-path-analysis/SKILL.md +++ b/workspace/skills/gtrace-path-analysis/SKILL.md @@ -9,6 +9,8 @@ metadata: # Network Path Analysis with gtrace +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## How to Call the gtrace MCP Tools ```bash diff --git a/workspace/skills/markmap-viz/SKILL.md b/workspace/skills/markmap-viz/SKILL.md index e169e8dd..2522c949 100644 --- a/workspace/skills/markmap-viz/SKILL.md +++ b/workspace/skills/markmap-viz/SKILL.md @@ -9,6 +9,8 @@ metadata: # Markmap Mind Map Visualization +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## How to Call the Tools The Markmap MCP server provides 5 tools. Call them via mcp-call: diff --git a/workspace/skills/netbox-reconcile/SKILL.md b/workspace/skills/netbox-reconcile/SKILL.md index 3f43771a..3b58b7f0 100644 --- a/workspace/skills/netbox-reconcile/SKILL.md +++ b/workspace/skills/netbox-reconcile/SKILL.md @@ -9,6 +9,8 @@ metadata: # NetBox Reconciliation +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## Golden Rule **NetBox is READ-WRITE.** The MCP has full API access to create and update devices, IPs, interfaces, VLANs, and cables. However, during reconciliation, discrepancies are reported and ticketed first — they are NEVER auto-corrected without explicit human approval. NetBox is the intended state. If reality differs from NetBox, either the network is wrong or NetBox needs updating. NetClaw can update NetBox when explicitly authorized by the operator. diff --git a/workspace/skills/nmap-network-scan/SKILL.md b/workspace/skills/nmap-network-scan/SKILL.md index 3f284b68..b73da5f8 100644 --- a/workspace/skills/nmap-network-scan/SKILL.md +++ b/workspace/skills/nmap-network-scan/SKILL.md @@ -9,6 +9,8 @@ metadata: # Network Scanning with nmap +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## How to Call the nmap MCP Tools ```bash diff --git a/workspace/skills/nvd-cve/SKILL.md b/workspace/skills/nvd-cve/SKILL.md index dd9022eb..ec2227b8 100644 --- a/workspace/skills/nvd-cve/SKILL.md +++ b/workspace/skills/nvd-cve/SKILL.md @@ -9,6 +9,8 @@ metadata: # NVD CVE Vulnerability Search +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## Available Tools ### 1. `get_cve` — Look Up a Specific CVE by ID diff --git a/workspace/skills/packet-analysis/SKILL.md b/workspace/skills/packet-analysis/SKILL.md index 4bfd3682..de612b65 100644 --- a/workspace/skills/packet-analysis/SKILL.md +++ b/workspace/skills/packet-analysis/SKILL.md @@ -8,6 +8,8 @@ tags: [packets, pcap, wireshark, tshark, troubleshooting] # Packet Analysis Skill +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## MCP Server - **Script**: `$PACKET_BUDDY_MCP_SCRIPT` - **Invocation**: `python3 $MCP_CALL "python3 -u $PACKET_BUDDY_MCP_SCRIPT" ''` diff --git a/workspace/skills/pyats-network/SKILL.md b/workspace/skills/pyats-network/SKILL.md index 83fa4d73..05b9298f 100644 --- a/workspace/skills/pyats-network/SKILL.md +++ b/workspace/skills/pyats-network/SKILL.md @@ -9,6 +9,8 @@ metadata: # pyATS Network Device Tool +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## Server & Testbed - **Server script:** `$PYATS_MCP_SCRIPT` diff --git a/workspace/skills/rfc-lookup/SKILL.md b/workspace/skills/rfc-lookup/SKILL.md index 7932d542..b945657b 100644 --- a/workspace/skills/rfc-lookup/SKILL.md +++ b/workspace/skills/rfc-lookup/SKILL.md @@ -9,6 +9,8 @@ metadata: # IETF RFC Lookup +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## How to Use Send requests to the RFC MCP server via mcp-call: diff --git a/workspace/skills/servicenow-change-workflow/SKILL.md b/workspace/skills/servicenow-change-workflow/SKILL.md index 87644b5a..2724cfd9 100644 --- a/workspace/skills/servicenow-change-workflow/SKILL.md +++ b/workspace/skills/servicenow-change-workflow/SKILL.md @@ -9,6 +9,8 @@ metadata: # ServiceNow Change Workflow +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## Golden Rule **NEVER execute a network change without an approved Change Request.** The only exception is an Emergency change, which still requires a CR and immediate human notification. diff --git a/workspace/skills/subnet-calculator/SKILL.md b/workspace/skills/subnet-calculator/SKILL.md index 180ea732..bc8fe69c 100644 --- a/workspace/skills/subnet-calculator/SKILL.md +++ b/workspace/skills/subnet-calculator/SKILL.md @@ -9,6 +9,8 @@ metadata: # Subnet Calculator (IPv4 + IPv6) +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## Available Tools ### 1. `subnet_calculator` — IPv4 Subnet Details diff --git a/workspace/skills/wikipedia-research/SKILL.md b/workspace/skills/wikipedia-research/SKILL.md index 934481df..9e9b291d 100644 --- a/workspace/skills/wikipedia-research/SKILL.md +++ b/workspace/skills/wikipedia-research/SKILL.md @@ -9,6 +9,8 @@ metadata: # Wikipedia Research +Installer note: native registration and the documented MCP_CALL invocation share the installed component launcher. Check `readiness.json` before first use; failed discovery is not device state. Existing write approvals still apply. + ## How to Call the Tools The Wikipedia MCP server uses FastMCP with typed input objects. All arguments must use the `{"input": {...}}` format. Call them via mcp-call: From b913e8fd8a1fe4828fe778ae7903182555c833f2 Mon Sep 17 00:00:00 2001 From: automateyournetwork Date: Sat, 10 Oct 2026 08:10:41 -0400 Subject: [PATCH 4/5] Make installer launcher tests independent of local component clones --- tests/unit/test_installer_followups.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_installer_followups.py b/tests/unit/test_installer_followups.py index 32359430..a8c2a21f 100644 --- a/tests/unit/test_installer_followups.py +++ b/tests/unit/test_installer_followups.py @@ -64,19 +64,30 @@ def make_runtime(tmp_path, component): return runtime, python -def test_netbox_module_uses_record_and_source_not_gateway_path(tmp_path): +@pytest.fixture +def netbox_repo(tmp_path): + # Optional component clones are absent on a clean checkout/CI runner. + # Keep launcher tests independent of the developer's installed components. + repo = tmp_path / 'repo' + script = repo / launch.CONTRACT['netbox']['script'] + script.parent.mkdir(parents=True) + script.write_text('') + return repo + + +def test_netbox_module_uses_record_and_source_not_gateway_path(tmp_path, netbox_repo): runtime, python = make_runtime(tmp_path, 'netbox') env = {'NETCLAW_RUNTIME_ROOT': str(runtime), 'PATH': '/wrong/python/bin', 'NETBOX_TOKEN': 'fixture'} - parts, child_env, cwd = launch.resolve('netbox', env=env) + parts, child_env, cwd = launch.resolve('netbox', env=env, repo=netbox_repo) assert parts == [str(python), '-u', '-m', 'netbox_mcp_server.server', '--transport', 'stdio'] - assert child_env['PYTHONPATH'] == str(ROOT / 'mcp-servers/netbox-mcp-server/src') + assert child_env['PYTHONPATH'] == str(netbox_repo / 'mcp-servers/netbox-mcp-server/src') assert child_env['NETBOX_TOKEN'] == 'fixture' - assert cwd == str(ROOT) + assert cwd == str(netbox_repo) -def test_missing_record_fails_instead_of_global_fallback(tmp_path): +def test_missing_record_fails_instead_of_global_fallback(tmp_path, netbox_repo): with pytest.raises(ValueError, match='interpreter record'): - launch.resolve('netbox', env={'NETCLAW_RUNTIME_ROOT':str(tmp_path / 'runtimes')}) + launch.resolve('netbox', env={'NETCLAW_RUNTIME_ROOT':str(tmp_path / 'runtimes')}, repo=netbox_repo) def test_literal_env_precedence_and_custom_script_preserved(tmp_path): From bcf791fdc27d2d25e845addd2886e49d44171972 Mon Sep 17 00:00:00 2001 From: automateyournetwork Date: Sat, 10 Oct 2026 08:11:19 -0400 Subject: [PATCH 5/5] Record installer validation and remaining live acceptance limits --- specs/147-installer-followups/tasks.md | 4 ++-- specs/147-installer-followups/verification.md | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/specs/147-installer-followups/tasks.md b/specs/147-installer-followups/tasks.md index c4e7242a..e8fda3ef 100644 --- a/specs/147-installer-followups/tasks.md +++ b/specs/147-installer-followups/tasks.md @@ -19,7 +19,7 @@ - [x] T010 Declare access paths and expected capabilities for all twelve reported components. - [x] T011 Bind supported native registrations and skill launchers to managed runtimes. -- [x] T012 Verify pyATS/CML and representative utility discovery without bypassing approval controls. +- [x] T012 Verify pyATS/CML launch contracts with fixtures and representative utility discovery without bypassing approval controls; original live pyATS/CML targets remain unverified. - [x] T013 Preserve operator state and reconcile inventory/registration/documentation surfaces. ## Phase C — First-use readiness @@ -37,4 +37,4 @@ The six unnamed macOS component failures and exact original CML/runtime diagnostics remain pending evidence. They do not prevent work on the confirmed scoped defects; they are not invented acceptance cases. -Release proposal: 1.6.2. PR/merge completion is recorded in verification.md once CI passes. Original live target acceptance remains a documented external evidence gap. +Release metadata: 1.6.2. [PR #289](https://github.com/automateyournetwork/netclaw/pull/289) records final CI and merge status. Original live target acceptance remains a documented external evidence gap. diff --git a/specs/147-installer-followups/verification.md b/specs/147-installer-followups/verification.md index b289cd9c..073f196a 100644 --- a/specs/147-installer-followups/verification.md +++ b/specs/147-installer-followups/verification.md @@ -40,7 +40,7 @@ The fixture accepts initialization and discovery only. It starts its own tempora ## Limits -The original log archive is unavailable; six macOS component failures are unnamed. The original CML server, Ollama endpoint, runtime config and precise command/environment are not reproduced. Native CLI probe success is not a gateway agent-turn or model grounding test. No secrets, real provider calls, real package installs, service restarts or device operations occurred. +The original log archive is unavailable; six macOS component failures are unnamed. The original CML server, Ollama endpoint, runtime config and precise command/environment are not reproduced. Native CLI probe success is not a gateway agent-turn or model grounding test. The baseline investigation made no package installs. Implementation verification below installed test dependencies only in a disposable environment. No secrets, real provider calls, operator service restarts or device operations occurred. ## Specification checks @@ -61,6 +61,8 @@ The original log archive is unavailable; six macOS component failures are unname To reproduce the four-component smoke, use [reproduce-installed-components.py](reproduce-installed-components.py) with `--node-bin /absolute/node/bin --output /tmp/netclaw-components.json`. It explicitly downloads declared test dependencies using uv into a disposable environment; it applies the existing reviewed calculator migration only to temporary source. It does not install or change operator component runtimes. Runtime dotenv settings are part of the fixture, matching installer deployment. -The initial smoke setup intentionally exposed failures when dependencies/patches/settings were absent; readiness flagged them instead of reporting success. After preparing declared source/dependencies and persisting fixture settings, all four passed. This is representative discovery evidence, not a full-catalog fresh-host install or the original CML/pyATS endpoint reproduction. +The initial smoke setup was missing dependencies/patches/settings; readiness flagged those failures instead of reporting success. After preparing declared source/dependencies and persisting fixture settings, all four passed. This is representative discovery evidence, not a full-catalog fresh-host install or the original CML/pyATS endpoint reproduction. -Release metadata: proposed patch 1.6.2. Live acceptance and arbitrary model answer grounding remain unverified; no tag or published release is implied. PR CI/merge evidence follows below. +The installer contract suite passed locally with Python 3.12 and in the first PR CI run. An initial local attempt used Python 3.10 and stopped during test-environment setup because the memory package requires Python 3.11 or later. The first PR unit job exposed two tests that depended on a locally installed NetBox source checkout. They now create temporary source fixtures; all 26 tests in that file pass locally. No launcher assertion was weakened. + +Release metadata: patch 1.6.2. Live acceptance and arbitrary model answer grounding remain unverified; no tag or published release is implied. [PR #289](https://github.com/automateyournetwork/netclaw/pull/289) records final CI and merge status.