harnessmith 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. harnessmith/__init__.py +3 -0
  2. harnessmith/catalog/__init__.py +166 -0
  3. harnessmith/catalog/mcp_servers.yaml +185 -0
  4. harnessmith/cli.py +348 -0
  5. harnessmith/cli_wizard.py +174 -0
  6. harnessmith/debuglog.py +59 -0
  7. harnessmith/generator.py +507 -0
  8. harnessmith/node_bootstrap.py +197 -0
  9. harnessmith/presets/__init__.py +50 -0
  10. harnessmith/presets/coding-assistant/mcp_prefill.yaml +13 -0
  11. harnessmith/presets/coding-assistant/spec.yaml +54 -0
  12. harnessmith/scaffold.py +145 -0
  13. harnessmith/spec.py +239 -0
  14. harnessmith/templates/.devcontainer/devcontainer.json.j2 +12 -0
  15. harnessmith/templates/.dockerignore.j2 +11 -0
  16. harnessmith/templates/.env.example.j2 +5 -0
  17. harnessmith/templates/.gitignore.j2 +17 -0
  18. harnessmith/templates/.python-version.j2 +1 -0
  19. harnessmith/templates/AGENTS.md.j2 +914 -0
  20. harnessmith/templates/Dockerfile.j2 +30 -0
  21. harnessmith/templates/LICENSE.j2 +21 -0
  22. harnessmith/templates/README.md.j2 +325 -0
  23. harnessmith/templates/RULES.md.j2 +10 -0
  24. harnessmith/templates/__launch_name__.bat.j2 +173 -0
  25. harnessmith/templates/__launch_name__.sh.j2 +143 -0
  26. harnessmith/templates/config.yaml.j2 +270 -0
  27. harnessmith/templates/pyproject.toml.j2 +50 -0
  28. harnessmith/templates/skills/example-skill/SKILL.md.j2 +30 -0
  29. harnessmith/templates/src/__project_slug__/__init__.py.j2 +3 -0
  30. harnessmith/templates/src/__project_slug__/harness/__init__.py.j2 +17 -0
  31. harnessmith/templates/src/__project_slug__/harness/config.py.j2 +681 -0
  32. harnessmith/templates/src/__project_slug__/harness/context.py.j2 +471 -0
  33. harnessmith/templates/src/__project_slug__/harness/debuglog.py.j2 +72 -0
  34. harnessmith/templates/src/__project_slug__/harness/extensions.py.j2 +188 -0
  35. harnessmith/templates/src/__project_slug__/harness/hooks.py.j2 +116 -0
  36. harnessmith/templates/src/__project_slug__/harness/interaction.py.j2 +266 -0
  37. harnessmith/templates/src/__project_slug__/harness/llm.py.j2 +425 -0
  38. harnessmith/templates/src/__project_slug__/harness/llm_anthropic.py.j2 +422 -0
  39. harnessmith/templates/src/__project_slug__/harness/loop.py.j2 +85 -0
  40. harnessmith/templates/src/__project_slug__/harness/mcp.py.j2 +1251 -0
  41. harnessmith/templates/src/__project_slug__/harness/memory.py.j2 +353 -0
  42. harnessmith/templates/src/__project_slug__/harness/mock.py.j2 +109 -0
  43. harnessmith/templates/src/__project_slug__/harness/paradigms/__init__.py.j2 +359 -0
  44. harnessmith/templates/src/__project_slug__/harness/paradigms/agent.py.j2 +236 -0
  45. harnessmith/templates/src/__project_slug__/harness/paradigms/ask.py.j2 +236 -0
  46. harnessmith/templates/src/__project_slug__/harness/paradigms/plan.py.j2 +240 -0
  47. harnessmith/templates/src/__project_slug__/harness/prompts.py.j2 +153 -0
  48. harnessmith/templates/src/__project_slug__/harness/session.py.j2 +316 -0
  49. harnessmith/templates/src/__project_slug__/harness/skills.py.j2 +143 -0
  50. harnessmith/templates/src/__project_slug__/harness/tools.py.j2 +357 -0
  51. harnessmith/templates/src/__project_slug__/harness/trace.py.j2 +110 -0
  52. harnessmith/templates/src/__project_slug__/harness/usage.py.j2 +207 -0
  53. harnessmith/templates/src/__project_slug__/interfaces/__init__.py.j2 +1 -0
  54. harnessmith/templates/src/__project_slug__/interfaces/cli.py.j2 +1261 -0
  55. harnessmith/templates/src/__project_slug__/interfaces/web.py.j2 +1456 -0
  56. harnessmith/templates/src/__project_slug__/interfaces/web_index.html.j2 +3296 -0
  57. harnessmith/templates/tests/_mcp_dummy_server.py.j2 +36 -0
  58. harnessmith/templates/tests/test_harness.py.j2 +2539 -0
  59. harnessmith/templates/tests/test_llm_anthropic.py.j2 +324 -0
  60. harnessmith/templates/tests/test_mcp.py.j2 +1126 -0
  61. harnessmith/templates/tests/test_memory.py.j2 +251 -0
  62. harnessmith/templates/tests/test_sessions.py.j2 +364 -0
  63. harnessmith/templates/tests/test_skills.py.j2 +112 -0
  64. harnessmith/templates/tests/test_web.py.j2 +1706 -0
  65. harnessmith/wizard/__init__.py +11 -0
  66. harnessmith/wizard/app.py +682 -0
  67. harnessmith/wizard/static/index.html +430 -0
  68. harnessmith-0.1.0.dist-info/METADATA +431 -0
  69. harnessmith-0.1.0.dist-info/RECORD +72 -0
  70. harnessmith-0.1.0.dist-info/WHEEL +4 -0
  71. harnessmith-0.1.0.dist-info/entry_points.txt +2 -0
  72. harnessmith-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """HarnessSmith — forge your own agent harness (config-to-code generator)."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,166 @@
1
+ """Static MCP server catalog — a generation-time convenience datasource (Slice 6).
2
+
3
+ Loaded by ``harnessmith new --mcp-server <name>`` and by presets to PREFILL the
4
+ generated repo's runtime ``config.yaml`` (``mcp.servers`` + the tool allowlist).
5
+ It is **not** a security gate and is **not** part of :class:`HarnessSpec` or its
6
+ snapshot — the real gate is the runtime allowlist + per-tool risk markers.
7
+ Secrets are referenced by env-var NAME only, never stored as values.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+
15
+ import yaml
16
+
17
+ CATALOG_PATH = Path(__file__).parent / "mcp_servers.yaml"
18
+
19
+ SAFE = "safe"
20
+ HIGH = "high"
21
+
22
+
23
+ class CatalogError(Exception):
24
+ """Raised when the catalog file or a requested server is invalid/missing."""
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CatalogTool:
29
+ name: str
30
+ risk: str = HIGH
31
+ default_enabled: bool = False
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class CatalogServer:
36
+ """One curated MCP server entry (transport + tools + provenance)."""
37
+
38
+ name: str
39
+ description: str = ""
40
+ transport: str = "stdio" # "stdio" | "remote"
41
+ command: str | None = None
42
+ args: list[str] = field(default_factory=list)
43
+ env: list[str] = field(default_factory=list) # env-var NAMES (secrets, from .env)
44
+ env_const: dict[str, str] = field(default_factory=dict) # literal non-secret env (e.g. MODE=stdio)
45
+ url: str | None = None
46
+ auth_env: str | None = None
47
+ requires: str | None = None # runtime prerequisite: "uv" | "node" | None
48
+ source: str = ""
49
+ updated: str = ""
50
+ tools: list[CatalogTool] = field(default_factory=list)
51
+
52
+ @property
53
+ def safe_tools(self) -> list[str]:
54
+ """Unprefixed names of read-only/low-risk tools (offered to plan/ask)."""
55
+ return [t.name for t in self.tools if t.risk == SAFE]
56
+
57
+ @property
58
+ def uvx_package(self) -> str | None:
59
+ """The pip/uvx package name for a uvx-launched server (else ``None``).
60
+
61
+ Handles both ``uvx <pkg>`` and ``uvx --from <pkg> <entrypoint>`` (used when
62
+ the package's console-script name differs from the package name) by taking
63
+ the first non-flag arg — matching the runtime warm path (``_warm_argv``)."""
64
+ if self.command == "uvx":
65
+ return next((a for a in self.args if not a.startswith("-")), None)
66
+ return None
67
+
68
+ def server_entry(self) -> dict:
69
+ """A ``config.yaml`` ``mcp.servers`` entry (env-var NAMES only)."""
70
+ entry: dict = {"name": self.name}
71
+ if self.description:
72
+ entry["description"] = self.description
73
+ if self.command:
74
+ entry["command"] = self.command
75
+ entry["args"] = list(self.args)
76
+ if self.env:
77
+ entry["env"] = list(self.env)
78
+ if self.env_const:
79
+ entry["env_const"] = dict(self.env_const)
80
+ else:
81
+ entry["url"] = self.url
82
+ if self.auth_env:
83
+ entry["auth_env"] = self.auth_env
84
+ if self.safe_tools:
85
+ entry["safe_tools"] = self.safe_tools
86
+ return entry
87
+
88
+ def allowlist_entries(self) -> list[dict]:
89
+ """``config.yaml`` ``tools`` allowlist entry for this server.
90
+
91
+ A single ``<server>__*`` wildcard that enables EVERY tool the server
92
+ exposes (present and future), so the full toolset is available by default
93
+ without listing each tool by name. Per-tool risk still comes from
94
+ ``safe_tools`` (read-only tools stay ``safe``; the rest are ``high``), and
95
+ any tool can be turned off individually later from ``config.yaml`` / the web
96
+ Tools panel.
97
+ """
98
+ return [{"name": f"{self.name}__*", "enabled": True}]
99
+
100
+
101
+ def _coerce_server(name: str, data: dict) -> CatalogServer:
102
+ tools = [
103
+ CatalogTool(
104
+ name=t["name"],
105
+ risk=t.get("risk", HIGH),
106
+ default_enabled=bool(t.get("default_enabled", False)),
107
+ )
108
+ for t in (data.get("tools") or [])
109
+ ]
110
+ return CatalogServer(
111
+ name=name,
112
+ description=data.get("description", ""),
113
+ transport=data.get("transport", "stdio"),
114
+ command=data.get("command"),
115
+ args=list(data.get("args") or []),
116
+ env=list(data.get("env") or []),
117
+ env_const={str(k): str(v) for k, v in (data.get("env_const") or {}).items()},
118
+ url=data.get("url"),
119
+ auth_env=data.get("auth_env"),
120
+ requires=data.get("requires"),
121
+ source=data.get("source", ""),
122
+ updated=str(data.get("updated", "")),
123
+ tools=tools,
124
+ )
125
+
126
+
127
+ def load_catalog(path: str | Path = CATALOG_PATH) -> dict[str, CatalogServer]:
128
+ """Load the catalog into a name -> :class:`CatalogServer` mapping."""
129
+ path = Path(path)
130
+ if not path.exists():
131
+ raise CatalogError(f"catalog file not found: {path}")
132
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
133
+ servers = data.get("servers") or {}
134
+ if not isinstance(servers, dict):
135
+ raise CatalogError("catalog 'servers' must be a mapping of name -> entry")
136
+ return {name: _coerce_server(name, entry) for name, entry in servers.items()}
137
+
138
+
139
+ def available_servers() -> list[str]:
140
+ """Names of curated catalog servers."""
141
+ return sorted(load_catalog())
142
+
143
+
144
+ def get_server(name: str) -> CatalogServer:
145
+ """Resolve a catalog server by name (raises :class:`CatalogError`)."""
146
+ catalog = load_catalog()
147
+ if name not in catalog:
148
+ known = ", ".join(sorted(catalog)) or "(none)"
149
+ raise CatalogError(f"unknown MCP server {name!r}; catalog has: {known}")
150
+ return catalog[name]
151
+
152
+
153
+ def resolve_servers(names: list[str]) -> list[CatalogServer]:
154
+ """Resolve catalog server names, de-duplicated, preserving first-seen order."""
155
+ catalog = load_catalog()
156
+ resolved: list[CatalogServer] = []
157
+ seen: set[str] = set()
158
+ for name in names:
159
+ if name in seen:
160
+ continue
161
+ if name not in catalog:
162
+ known = ", ".join(sorted(catalog)) or "(none)"
163
+ raise CatalogError(f"unknown MCP server {name!r}; catalog has: {known}")
164
+ resolved.append(catalog[name])
165
+ seen.add(name)
166
+ return resolved
@@ -0,0 +1,185 @@
1
+ # Curated MCP server catalog (HarnessSmith, Slice 6).
2
+ #
3
+ # A *generation-time convenience datasource* — used by `harnessmith new
4
+ # --mcp-server <name>` and by presets to PREFILL the generated repo's runtime
5
+ # config.yaml (mcp.servers + tool allowlist). It is NOT a security gate and is
6
+ # NOT part of HarnessSpec or its snapshot: the real gate is the runtime tool
7
+ # allowlist + per-tool risk markers (high-risk tools default OFF). Secrets are
8
+ # referenced by env-var NAME only — never values.
9
+ #
10
+ # Because the generated harness ships no built-in productivity tools, these MCP
11
+ # presets double as the *capability baseline*. Each prefilled server is enabled by
12
+ # a single `<server>__*` wildcard in config.yaml — its WHOLE toolset (read +
13
+ # mutating/shell) is on by default (narrow it after generation if you want).
14
+ # uvx-based servers (requires: uv) can be prewarmed at generation and baked into
15
+ # the Docker image for offline use; Node-based servers (requires: node) need
16
+ # Node/npx on the host.
17
+ #
18
+ # Per-tool `risk`: safe = read-only / low-impact (offered even to read-only
19
+ # paradigms like plan/ask); high = mutating / shell / network side effects
20
+ # (agent-only). `risk` feeds `safe_tools` (risk grading); the per-tool
21
+ # `default_enabled` flag is retained as documentation but the prefill now enables
22
+ # every tool via the wildcard regardless.
23
+ version: 1
24
+
25
+ servers:
26
+ fetch:
27
+ description: Fetch a URL from the internet and extract its content as markdown.
28
+ transport: stdio
29
+ command: uvx
30
+ args: [mcp-server-fetch]
31
+ requires: uv
32
+ source: https://pypi.org/project/mcp-server-fetch/
33
+ updated: "2026-06-04"
34
+ tools:
35
+ - {name: fetch, risk: safe, default_enabled: true}
36
+
37
+ web-search:
38
+ # Default web-search server: a keyless, multi-engine scraper that probes each
39
+ # engine for reachability and FAILS OVER between them (Bing / Baidu / DuckDuckGo
40
+ # / Brave / Sogou / …), so it keeps working when any single engine is slow or
41
+ # unreachable on a given network — much more resilient than a single-engine
42
+ # scraper. Node-based (npx); launched directly from a stable per-server install
43
+ # (see harness/mcp.py `_node_*`), never the ephemeral npx cache. `MODE=stdio`
44
+ # (env_const) keeps it a pure stdio MCP server — without it the default `both`
45
+ # mode also binds an HTTP port. Pinned (NOT @latest) so warm-on-first-run
46
+ # installs the exact version the later connect launches; bump it + `updated:`
47
+ # together when refreshing. Still an HTML scraper, so an engine layout change
48
+ # can degrade a single engine (the failover absorbs it).
49
+ description: >-
50
+ Multi-engine web search (Bing, Baidu, DuckDuckGo, Brave, Sogou, and more)
51
+ with automatic failover; keyless. Returns title, URL, and snippet per
52
+ result, and can fetch a result page's content. Some engines may be
53
+ unreachable on certain networks — it falls back across engines automatically.
54
+ transport: stdio
55
+ command: npx
56
+ args: ["-y", "open-websearch@2.1.11"]
57
+ env_const: {MODE: stdio}
58
+ requires: node
59
+ source: https://github.com/Aas-ee/open-webSearch
60
+ updated: "2026-06-14"
61
+ tools:
62
+ # All read-only network reads (search + fetch a page) -> safe, so the
63
+ # read-only plan/ask paradigms can search/read too.
64
+ - {name: search, risk: safe, default_enabled: true}
65
+ - {name: fetchWebContent, risk: safe, default_enabled: true}
66
+ - {name: fetchGithubReadme, risk: safe, default_enabled: true}
67
+ - {name: fetchCsdnArticle, risk: safe, default_enabled: false}
68
+ - {name: fetchLinuxDoArticle, risk: safe, default_enabled: false}
69
+ - {name: fetchJuejinArticle, risk: safe, default_enabled: false}
70
+
71
+ ddg-search:
72
+ # uvx-based (no Node) keyless fallback. DuckDuckGo is unreachable on some
73
+ # networks — prefer `web-search` (multi-engine) there; this stays as a
74
+ # lightweight uvx alternative for hosts without Node, or where DuckDuckGo is
75
+ # reachable. Add with `--mcp-server ddg-search`.
76
+ description: >-
77
+ Web search via DuckDuckGo (keyless) plus fetching a result page as
78
+ markdown. A uvx-based alternative (no Node); DuckDuckGo is unreachable on
79
+ some networks, where web-search is the better default.
80
+ transport: stdio
81
+ command: uvx
82
+ args: [duckduckgo-mcp-server]
83
+ requires: uv
84
+ source: https://github.com/nickclyde/duckduckgo-mcp-server
85
+ updated: "2026-06-14"
86
+ tools:
87
+ - {name: search, risk: safe, default_enabled: true}
88
+ - {name: fetch_content, risk: safe, default_enabled: true}
89
+
90
+ git:
91
+ description: Inspect and operate on local Git repositories (read tools on; write tools off).
92
+ transport: stdio
93
+ command: uvx
94
+ # No --repository pin on purpose: every git tool already takes a required
95
+ # `repo_path` arg, so pinning adds no default — it ONLY restricts which repo is
96
+ # reachable, AND makes mcp-server-git EXIT at startup when launched outside a
97
+ # git repo (it surfaces as the server going "unreachable: Connection closed").
98
+ # The server's health should reflect the tool, not the cwd; unpinned it stays
99
+ # up and the agent operates on whatever repo it points `repo_path` at.
100
+ args: [mcp-server-git]
101
+ requires: uv
102
+ source: https://pypi.org/project/mcp-server-git/
103
+ updated: "2026-06-04"
104
+ tools:
105
+ - {name: git_status, risk: safe, default_enabled: true}
106
+ - {name: git_diff_unstaged, risk: safe, default_enabled: true}
107
+ - {name: git_diff_staged, risk: safe, default_enabled: true}
108
+ - {name: git_diff, risk: safe, default_enabled: true}
109
+ - {name: git_log, risk: safe, default_enabled: true}
110
+ - {name: git_show, risk: safe, default_enabled: true}
111
+ - {name: git_branch, risk: safe, default_enabled: true}
112
+ - {name: git_add, risk: high, default_enabled: false}
113
+ - {name: git_reset, risk: high, default_enabled: false}
114
+ - {name: git_commit, risk: high, default_enabled: false}
115
+ - {name: git_create_branch, risk: high, default_enabled: false}
116
+ - {name: git_checkout, risk: high, default_enabled: false}
117
+ - {name: git_init, risk: high, default_enabled: false}
118
+
119
+ desktop-commander:
120
+ description: Terminal command execution + full filesystem read/write/edit (powerful; read tools safe, write/shell high-risk).
121
+ transport: stdio
122
+ command: npx
123
+ # `--silent` keeps npm OUT of the child's stdout. A stdio MCP child's stdout
124
+ # must be PURE JSON-RPC, but on first launch `npx` installs the package and
125
+ # (on older npm) leaks its `added N packages ...` summary to stdout — which the
126
+ # MCP reader then tries to parse as protocol, spamming parse tracebacks. npm
127
+ # logs/warnings go to stderr (captured separately), and `--silent` does NOT
128
+ # touch the launched server's own stdout, so the protocol stream is unaffected.
129
+ # Pinned (NOT @latest) on purpose: warm-on-first-run pre-fetches THIS exact
130
+ # version into the npx cache, and a pin is what makes the later connect a true
131
+ # offline cache hit — `@latest` would re-hit the registry to re-resolve and could
132
+ # miss the warmed copy. Bump the version + `updated:` together when refreshing.
133
+ args: ["--silent", "-y", "@wonderwhy-er/desktop-commander@0.2.42"]
134
+ requires: node
135
+ source: https://github.com/wonderwhy-er/DesktopCommanderMCP
136
+ updated: "2026-06-14"
137
+ # A representative subset (Desktop Commander exposes ~26 tools). The prefilled
138
+ # `desktop-commander__*` wildcard enables the WHOLE discovered set (needs Node);
139
+ # the per-tool `risk` below grades it. Read-only tools are `safe` so the
140
+ # read-only plan/ask paradigms can use them too (without them, plan/ask get NO
141
+ # filesystem access at all — see Slice 5); write/shell/config-mutating tools are
142
+ # `high` (agent-only, HITL-gated). Risk grading matches the discovered tools BY
143
+ # NAME, so naming the real read-only tools here is what lets plan/ask read files.
144
+ tools:
145
+ # Read-only (safe): also offered to the read-only plan/ask paradigms.
146
+ - {name: read_file, risk: safe, default_enabled: false}
147
+ - {name: read_multiple_files, risk: safe, default_enabled: false}
148
+ - {name: list_directory, risk: safe, default_enabled: false}
149
+ - {name: get_file_info, risk: safe, default_enabled: false}
150
+ - {name: start_search, risk: safe, default_enabled: false}
151
+ - {name: get_more_search_results, risk: safe, default_enabled: false}
152
+ - {name: list_searches, risk: safe, default_enabled: false}
153
+ # Mutating / shell / config writes (high): agent-only, off by default.
154
+ - {name: write_file, risk: high, default_enabled: false}
155
+ - {name: edit_block, risk: high, default_enabled: false}
156
+ - {name: create_directory, risk: high, default_enabled: false}
157
+ - {name: move_file, risk: high, default_enabled: false}
158
+ - {name: set_config_value, risk: high, default_enabled: false}
159
+ - {name: start_process, risk: high, default_enabled: false}
160
+ - {name: kill_process, risk: high, default_enabled: false}
161
+
162
+ # --- Extra candidates (not in the coding-assistant baseline). Add with
163
+ # `--mcp-server <name>` or paste the server block into config.yaml. ---
164
+
165
+ time:
166
+ description: Current time and timezone conversion utilities.
167
+ transport: stdio
168
+ command: uvx
169
+ args: [mcp-server-time]
170
+ requires: uv
171
+ source: https://pypi.org/project/mcp-server-time/
172
+ updated: "2026-06-04"
173
+ tools:
174
+ - {name: get_current_time, risk: safe, default_enabled: true}
175
+ - {name: convert_time, risk: safe, default_enabled: true}
176
+
177
+ github:
178
+ description: GitHub platform API (issues / PRs / repos). Remote MCP — needs a token.
179
+ transport: remote
180
+ url: https://api.githubcopilot.com/mcp/
181
+ auth_env: GITHUB_MCP_TOKEN
182
+ source: https://github.com/github/github-mcp-server
183
+ updated: "2026-06-04"
184
+ # Large surface (70+ tools); enable specific tools by name as needed.
185
+ tools: []