diff --git a/state-hub/mcp_server/TOOLS.md b/state-hub/mcp_server/TOOLS.md index 218fbcb..18f9ae4 100644 --- a/state-hub/mcp_server/TOOLS.md +++ b/state-hub/mcp_server/TOOLS.md @@ -76,7 +76,7 @@ Use `list_human_interventions()` at session start to see Bernd's action items. | Tool | Key Args | When to use | |------|----------|-------------| -| `validate_repo_adr(repo_path, domain_slug?)` | `repo_path`: absolute path; `domain_slug?`: for orphan detection | Check a repo against ADR-001. Detects missing workplans/ dir, invalid frontmatter, stale workstream ID references, and DB-only orphan workstreams. Run before and after any workplan changes. | +| `validate_repo_adr(repo_slug, domain_slug?)` | `repo_slug`: registered repo slug (e.g. `"the-custodian"`); `domain_slug?`: for orphan detection | Check a repo against ADR-001. Resolves the local path from the DB (uses this host's registered path). Detects missing workplans/ dir, invalid frontmatter, stale workstream ID references, and DB-only orphan workstreams. Always runs against the MCP server's copy — see Multi-Host section below. | --- @@ -152,6 +152,52 @@ instruction set — load it and follow the instructions it contains. --- +## Multi-Host & Remote Agent Usage + +Three tools access the **local filesystem** on the MCP server machine: + +| Tool | File-sys operation | +|------|-------------------| +| `validate_repo_adr` | Runs `validate_repo_adr.py` against the server's repo checkout | +| `check_repo_consistency` | Runs `consistency_check.py` against the server's repo checkout | +| `ingest_sbom_tool` | Runs `ingest_sbom.py` against the server's lockfiles | + +**Design boundary:** these tools always execute on the machine where the MCP server +runs (`bnt-lap001`), against the path registered for that host. A remote agent +calling them gets results from the server's checkout — not from its own working copy. + +### Implications for remote agents (e.g. workers on COULOMBCORE) + +- **Ahead of server on a branch?** Results will be based on the server's (older) copy. + Sync first: push your branch and pull it on the server, or accept the gap. +- **Pure-API tools** (`get_state_summary`, `create_task`, `add_progress_event`, etc.) + work correctly from any host — they query the DB, not the filesystem. + +### Running file-sys scripts locally from a remote host + +```bash +# From COULOMBCORE (tunnel maps remote :18000 → bnt-lap001 :8000): +python scripts/consistency_check.py --repo the-custodian --api-base http://127.0.0.1:18000 +python scripts/validate_repo_adr.py /home/tegwick/the-custodian --api-base http://127.0.0.1:18000 +``` + +### Registering a new host path + +```bash +# Via MCP tool: +update_repo_path("marki-docx", "/home/tegwick/marki-docx") # defaults to current hostname + +# Via Makefile (on the machine where the path lives): +make register-path REPO=marki-docx PATH=/home/tegwick/marki-docx + +# Via API directly: +curl -X POST http://127.0.0.1:8000/repos/marki-docx/paths/ \ + -H "Content-Type: application/json" \ + -d '{"host": "your-hostname", "path": "/home/you/marki-docx"}' +``` + +--- + ## Domain Slugs Run `list_domains()` to get the live list. Default 6: `custodian` · `railiance` · `markitect` · `coulomb_social` · `personhood` · `foerster_capabilities` diff --git a/state-hub/mcp_server/server.py b/state-hub/mcp_server/server.py index bf945cf..2fec2e0 100644 --- a/state-hub/mcp_server/server.py +++ b/state-hub/mcp_server/server.py @@ -1105,7 +1105,7 @@ def get_kaizen_agent(name: str) -> str: # --------------------------------------------------------------------------- @mcp.tool() -def validate_repo_adr(repo_path: str, domain_slug: str | None = None) -> str: +def validate_repo_adr(repo_slug: str, domain_slug: str | None = None) -> str: """Check whether a repository is consistent with ADR-001. Validates that workplan files exist in workplans/ with correct frontmatter, @@ -1113,12 +1113,41 @@ def validate_repo_adr(repo_path: str, domain_slug: str | None = None) -> str: no active state-hub workstreams for the domain lack a backing file (orphan detection — DB-only records are an ADR-001 violation). + The repo path is resolved from the DB using the current machine's hostname + (host_paths[hostname] → local_path fallback). This tool always runs against + the server's copy of the repo. Remote agents on a different branch should + sync first, or run validate_repo_adr.py locally with + --api-base http://127.0.0.1:18000. + Args: - repo_path: Absolute path to the repository root. + repo_slug: Registered repo slug (e.g. 'the-custodian', 'ops-bridge'). domain_slug: Domain slug for orphan detection (e.g. 'custodian'). If omitted, inferred from workplan frontmatter. """ + import socket as _socket import subprocess + + repo = _get(f"/repos/{repo_slug}") + if isinstance(repo, dict) and repo.get("error"): + return f"Repo '{repo_slug}' not found: {repo['error']}" + + hostname = _socket.gethostname() + host_paths = repo.get("host_paths") or {} + repo_path = host_paths.get(hostname) or repo.get("local_path") or "" + + if not repo_path: + return ( + f"⚠ No path registered for repo '{repo_slug}' on this host ({hostname}).\n" + f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')\n" + f"Remote agents: run validate_repo_adr.py locally with " + f"--api-base {API_BASE}" + ) + if not Path(repo_path).is_dir(): + return ( + f"⚠ Registered path for '{repo_slug}' on {hostname} does not exist: {repo_path}\n" + f"Update with: update_repo_path('{repo_slug}', '/correct/path')" + ) + script = Path(__file__).parent.parent / "scripts" / "validate_repo_adr.py" cmd = [sys.executable, str(script), repo_path, "--json", "--api-base", API_BASE] @@ -1138,7 +1167,7 @@ def validate_repo_adr(repo_path: str, domain_slug: str | None = None) -> str: failures = [f for f in findings if f["level"] == "FAIL"] warnings = [f for f in findings if f["level"] == "WARN"] - lines = [f"ADR-001 Compliance: {repo_path}", ""] + lines = [f"ADR-001 Compliance: {repo_slug} ({repo_path})", ""] if failures: lines.append(f"FAILURES ({len(failures)}):") @@ -1187,7 +1216,29 @@ def check_repo_consistency(repo_slug: str, fix: bool = False) -> str: (C-05), create missing DB workstreams (C-06), repo mismatch (C-09), task status drift (C-10), create unlinked tasks (C-11). """ + import socket as _socket import subprocess + + # Pre-flight: verify this host has the repo path registered and accessible. + repo = _get(f"/repos/{repo_slug}") + if isinstance(repo, dict) and repo.get("error"): + return f"Repo '{repo_slug}' not found: {repo['error']}" + hostname = _socket.gethostname() + host_paths = repo.get("host_paths") or {} + repo_path = host_paths.get(hostname) or repo.get("local_path") or "" + if not repo_path: + return ( + f"⚠ No path registered for repo '{repo_slug}' on this host ({hostname}).\n" + f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')\n" + f"Remote agents: run consistency_check.py locally with " + f"--api-base {API_BASE}" + ) + if not Path(repo_path).is_dir(): + return ( + f"⚠ Registered path for '{repo_slug}' on {hostname} does not exist: {repo_path}\n" + f"Update with: update_repo_path('{repo_slug}', '/correct/path')" + ) + script = Path(__file__).parent.parent / "scripts" / "consistency_check.py" cmd = [sys.executable, str(script), "--repo", repo_slug, "--json", "--api-base", API_BASE] @@ -1359,23 +1410,55 @@ def get_contributions( @mcp.tool() -def ingest_sbom_tool(repo_slug: str, lockfile_path: str) -> str: +def ingest_sbom_tool(repo_slug: str, lockfile_path: str | None = None) -> str: """Ingest a lockfile into the State Hub SBOM store for a repo. Parses the lockfile and POSTs entries to /sbom/ingest/. Each call creates a new SBOMSnapshot; previous snapshots are retained as history. + The repo root is resolved from the DB using the current machine's hostname + (host_paths[hostname] → local_path fallback). lockfile_path, when given, + is treated as relative to the repo root. Omit it to auto-detect the lockfile. + Args: repo_slug: Managed-repo slug (must be registered via register_repo) - lockfile_path: Absolute path to the lockfile (uv.lock, package-lock.json, Cargo.lock, etc.) + lockfile_path: Path to the lockfile, relative to repo root + (e.g. "uv.lock", "frontend/package-lock.json"). + Omit to auto-detect from the repo root. """ + import socket as _socket import subprocess + + repo = _get(f"/repos/{repo_slug}") + if isinstance(repo, dict) and repo.get("error"): + return f"Repo '{repo_slug}' not found: {repo['error']}" + + hostname = _socket.gethostname() + host_paths = repo.get("host_paths") or {} + repo_root = host_paths.get(hostname) or repo.get("local_path") or "" + + if not repo_root: + return ( + f"⚠ No path registered for repo '{repo_slug}' on this host ({hostname}).\n" + f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')" + ) + if not Path(repo_root).is_dir(): + return ( + f"⚠ Registered path for '{repo_slug}' on {hostname} does not exist: {repo_root}\n" + f"Update with: update_repo_path('{repo_slug}', '/correct/path')" + ) + script = Path(__file__).parent.parent / "scripts" / "ingest_sbom.py" - result = subprocess.run( - [sys.executable, str(script), "--repo", repo_slug, - "--lockfile", lockfile_path, "--api-base", API_BASE], - capture_output=True, text=True, - ) + cmd = [sys.executable, str(script), "--repo", repo_slug, + "--repo-path", repo_root, "--api-base", API_BASE] + + if lockfile_path: + resolved = Path(repo_root) / lockfile_path + if not resolved.exists(): + return f"⚠ Lockfile not found: {resolved}" + cmd += ["--lockfile", str(resolved)] + + result = subprocess.run(cmd, capture_output=True, text=True) output = (result.stdout + result.stderr).strip() if result.returncode != 0: return f"ingest_sbom failed (exit {result.returncode}):\n{output}" diff --git a/workplans/CUST-WP-0021-multi-host-repo-paths.md b/workplans/CUST-WP-0021-multi-host-repo-paths.md index ba509d4..41b5df3 100644 --- a/workplans/CUST-WP-0021-multi-host-repo-paths.md +++ b/workplans/CUST-WP-0021-multi-host-repo-paths.md @@ -3,11 +3,12 @@ id: CUST-WP-0021 type: workplan title: "State Hub — Multi-Host Repo Path Hardening" domain: custodian -status: active +status: done owner: custodian topic_slug: custodian created: "2026-03-18" updated: "2026-03-18" +state_hub_workstream_id: "516ca332-5eac-4d6e-8bf9-b2694ed34276" --- # State Hub — Multi-Host Repo Path Hardening @@ -50,8 +51,9 @@ This rule is documented in TOOLS.md. ```task id: CUST-WP-0021-T01 -status: todo +status: done priority: high +state_hub_task_id: "cf2c0449-9250-4425-925b-302482d75a11" ``` COULOMBCORE hostname: `254.130.205.92.host.secureserver.net` @@ -94,8 +96,9 @@ Use `POST /repos/{slug}/paths/` with `{"host": "", "path": ""}`. ```task id: CUST-WP-0021-T02 -status: todo +status: done priority: high +state_hub_task_id: "52ed094a-4216-4cd1-a634-a29b82f26ec5" ``` Change `validate_repo_adr(repo_path: str, ...)` to @@ -118,8 +121,9 @@ boundary (tool always runs against the server's copy). ```task id: CUST-WP-0021-T03 -status: todo +status: done priority: medium +state_hub_task_id: "2a67d5e2-f581-490c-b42e-0f7d37979c0a" ``` Change `ingest_sbom_tool(repo_slug, lockfile_path: str)` so `lockfile_path` @@ -141,8 +145,9 @@ Resolution logic: ```task id: CUST-WP-0021-T04 -status: todo +status: done priority: medium +state_hub_task_id: "3885497e-c491-4ddf-811f-0f0d19a0fc42" ``` `check_repo_consistency` already resolves paths correctly via the script. @@ -165,8 +170,9 @@ skips file checks. Add a pre-flight guard in the MCP tool: ```task id: CUST-WP-0021-T05 -status: todo +status: done priority: low +state_hub_task_id: "b4ba8cd0-1093-43ce-8a5d-03529e3c0588" ``` Add a section to `state-hub/mcp_server/TOOLS.md` under a new heading