werktools 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 (59) hide show
  1. werktools/__init__.py +3 -0
  2. werktools/__main__.py +16 -0
  3. werktools/catalog.py +223 -0
  4. werktools/classify.py +179 -0
  5. werktools/cli.py +2090 -0
  6. werktools/envelope.py +42 -0
  7. werktools/hub/__init__.py +2 -0
  8. werktools/hub/allowlist.py +318 -0
  9. werktools/hub/approvals.py +461 -0
  10. werktools/hub/capabilities.py +38 -0
  11. werktools/hub/capability_select.py +135 -0
  12. werktools/hub/contracts.py +474 -0
  13. werktools/hub/dashboard.py +1467 -0
  14. werktools/hub/dashboard_fonts.py +16 -0
  15. werktools/hub/diagnose.py +120 -0
  16. werktools/hub/discovery.py +367 -0
  17. werktools/hub/export_rules.py +246 -0
  18. werktools/hub/invariants.py +176 -0
  19. werktools/hub/ledger.py +64 -0
  20. werktools/hub/lifecycle.py +658 -0
  21. werktools/hub/onboarding.py +505 -0
  22. werktools/hub/policy.py +146 -0
  23. werktools/hub/pool.py +138 -0
  24. werktools/hub/registry.py +215 -0
  25. werktools/hub/registry_db.py +289 -0
  26. werktools/hub/registry_seed.json +1592 -0
  27. werktools/hub/relay.py +289 -0
  28. werktools/hub/render.py +139 -0
  29. werktools/hub/runtime_row.py +36 -0
  30. werktools/hub/runtimes.py +376 -0
  31. werktools/hub/server.py +654 -0
  32. werktools/hub/status.py +111 -0
  33. werktools/hub/store_bridge.py +87 -0
  34. werktools/hub/workers.py +323 -0
  35. werktools/ledger.py +167 -0
  36. werktools/policy.py +102 -0
  37. werktools/profile.py +104 -0
  38. werktools/redaction.py +107 -0
  39. werktools/server.py +123 -0
  40. werktools/tools/__init__.py +2 -0
  41. werktools/tools/audit.py +186 -0
  42. werktools/tools/bench.py +1205 -0
  43. werktools/tools/canon.py +242 -0
  44. werktools/tools/cost.py +267 -0
  45. werktools/tools/data_gate.py +392 -0
  46. werktools/tools/eval.py +124 -0
  47. werktools/tools/integration_gate.py +318 -0
  48. werktools/tools/mine.py +255 -0
  49. werktools/tools/skills.py +73 -0
  50. werktools/tools/skills_discover.py +322 -0
  51. werktools/tools/swarm.py +257 -0
  52. werktools/tools/trace.py +227 -0
  53. werktools/tools/truth.py +274 -0
  54. werktools/tools/vault.py +388 -0
  55. werktools-0.1.0.dist-info/METADATA +421 -0
  56. werktools-0.1.0.dist-info/RECORD +59 -0
  57. werktools-0.1.0.dist-info/WHEEL +4 -0
  58. werktools-0.1.0.dist-info/entry_points.txt +3 -0
  59. werktools-0.1.0.dist-info/licenses/LICENSE +661 -0
werktools/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """werktools - shared foundation primitives for the WERK Tools suite."""
2
+
3
+ __version__ = "0.1.0"
werktools/__main__.py ADDED
@@ -0,0 +1,16 @@
1
+ """werktools-swarm entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main as cli_main
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int:
11
+ args = list(sys.argv[1:] if argv is None else argv)
12
+ return cli_main(["swarm", *args])
13
+
14
+
15
+ if __name__ == "__main__":
16
+ raise SystemExit(main())
werktools/catalog.py ADDED
@@ -0,0 +1,223 @@
1
+ """Generic local catalog primitive for Wave-5 catalog tools.
2
+
3
+ A catalog is a directory of cards. Cards are plain JSON files or Markdown
4
+ files with a small `Key: value` header block. Everything here is local,
5
+ deterministic, and read-only: catalogs describe assets, they never run them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import warnings
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ _HEADER_PATTERN = re.compile(r"^(?P<key>[A-Za-z][A-Za-z _-]*):\s*(?P<value>.*)$")
18
+ _RISK_VALUES = ("read", "write", "destructive", "external", "secret", "unknown")
19
+
20
+ # Trust taxonomy (metadata only — never an enforcement decision in P1).
21
+ # Ordered most- to least-trusted; the default and every unknown value fail
22
+ # closed to the lowest tier.
23
+ TRUST_TIERS = ("Official", "Security-Scanned", "Community-Unverified")
24
+ DEFAULT_TRUST_TIER = "Community-Unverified"
25
+ _TRUST_SOURCE_MAX = 64
26
+ _TRUST_NOTE_MAX = 200
27
+
28
+
29
+ def normalize_trust_tier(value: Any) -> str:
30
+ """Return a known trust tier, failing closed to Community-Unverified."""
31
+ text = str(value).strip() if value is not None else ""
32
+ return text if text in TRUST_TIERS else DEFAULT_TRUST_TIER
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class CatalogCard:
37
+ """One described local asset (skill, connector, hook, ...)."""
38
+
39
+ card_id: str
40
+ kind: str
41
+ title: str
42
+ summary: str
43
+ tags: tuple[str, ...]
44
+ profiles: tuple[str, ...]
45
+ source: str
46
+ risk: str
47
+ requires_approval: bool
48
+ metadata: dict[str, Any] = field(default_factory=dict)
49
+ created_at: str = ""
50
+ trust_tier: str = DEFAULT_TRUST_TIER
51
+ trust_source: str = ""
52
+ trust_note: str = ""
53
+
54
+ @classmethod
55
+ def from_dict(cls, raw: dict[str, Any]) -> "CatalogCard":
56
+ metadata = raw.get("metadata", {})
57
+ return cls(
58
+ card_id=str(raw["card_id"]),
59
+ kind=str(raw.get("kind", "card")),
60
+ title=str(raw.get("title", raw["card_id"])),
61
+ summary=str(raw.get("summary", "")),
62
+ tags=tuple(str(item) for item in raw.get("tags", ())),
63
+ # Missing profiles means public; an EXPLICIT empty list means
64
+ # visible to nobody and must not silently widen to public.
65
+ profiles=tuple(str(item) for item in raw.get("profiles", ("*",))),
66
+ source=str(raw.get("source", "")),
67
+ risk=str(raw.get("risk", "unknown")),
68
+ requires_approval=bool(raw.get("requires_approval", False)),
69
+ metadata=dict(metadata) if isinstance(metadata, dict) else {},
70
+ created_at=str(raw.get("created_at", "")),
71
+ trust_tier=normalize_trust_tier(raw.get("trust_tier")),
72
+ trust_source=str(raw.get("trust_source", ""))[:_TRUST_SOURCE_MAX],
73
+ trust_note=str(raw.get("trust_note", ""))[:_TRUST_NOTE_MAX],
74
+ )
75
+
76
+ def to_dict(self) -> dict[str, Any]:
77
+ return {
78
+ "card_id": self.card_id,
79
+ "kind": self.kind,
80
+ "title": self.title,
81
+ "summary": self.summary,
82
+ "tags": list(self.tags),
83
+ "profiles": list(self.profiles),
84
+ "source": self.source,
85
+ "risk": self.risk,
86
+ "requires_approval": self.requires_approval,
87
+ "metadata": self.metadata,
88
+ "created_at": self.created_at,
89
+ "trust_tier": self.trust_tier,
90
+ "trust_source": self.trust_source,
91
+ "trust_note": self.trust_note,
92
+ }
93
+
94
+
95
+ def _csv(value: str) -> tuple[str, ...]:
96
+ return tuple(part.strip() for part in value.split(",") if part.strip())
97
+
98
+
99
+ def _parse_markdown_card(path: Path, kind: str) -> CatalogCard:
100
+ text = path.read_text(encoding="utf-8")
101
+ title = path.stem
102
+ headers: dict[str, str] = {}
103
+ summary = ""
104
+ body_lines: list[str] = []
105
+ # Only the FIRST contiguous run of `Key: value` lines is the header
106
+ # block; a later KV-looking block is body and must not override headers.
107
+ state = "preamble"
108
+ for line in text.splitlines():
109
+ stripped = line.strip()
110
+ if stripped.startswith("# ") and title == path.stem:
111
+ title = stripped[2:].strip()
112
+ continue
113
+ if state == "preamble":
114
+ if not stripped:
115
+ continue
116
+ matched = _HEADER_PATTERN.match(stripped)
117
+ if matched:
118
+ headers[matched.group("key").strip().lower()] = matched.group("value").strip()
119
+ state = "headers"
120
+ continue
121
+ state = "body"
122
+ body_lines.append(stripped)
123
+ continue
124
+ if state == "headers":
125
+ if not stripped:
126
+ state = "body"
127
+ continue
128
+ matched = _HEADER_PATTERN.match(stripped)
129
+ if matched:
130
+ headers[matched.group("key").strip().lower()] = matched.group("value").strip()
131
+ continue
132
+ state = "body"
133
+ body_lines.append(stripped)
134
+ continue
135
+ if stripped:
136
+ body_lines.append(stripped)
137
+ elif body_lines:
138
+ break
139
+ if body_lines:
140
+ summary = " ".join(body_lines)
141
+ risk = headers.get("risk", "unknown").lower()
142
+ return CatalogCard(
143
+ card_id=path.stem,
144
+ kind=kind,
145
+ title=title,
146
+ summary=summary,
147
+ tags=_csv(headers.get("tags", "")),
148
+ profiles=_csv(headers.get("profiles", "")) or ("*",),
149
+ source=str(path.resolve()),
150
+ risk=risk if risk in _RISK_VALUES else "unknown",
151
+ requires_approval=headers.get("requires approval", headers.get("requires_approval", "")).lower()
152
+ in {"true", "yes"},
153
+ metadata={},
154
+ created_at=headers.get("date", ""),
155
+ trust_tier=normalize_trust_tier(headers.get("trust_tier", headers.get("trust"))),
156
+ trust_source=headers.get("trust_source", "")[:_TRUST_SOURCE_MAX],
157
+ trust_note=headers.get("trust_note", "")[:_TRUST_NOTE_MAX],
158
+ )
159
+
160
+
161
+ def load_cards(directory: str | Path, kind: str) -> list[CatalogCard]:
162
+ """Load JSON and Markdown cards from a local directory, skipping bad files."""
163
+ root = Path(directory)
164
+ if not root.exists():
165
+ return []
166
+ cards: list[CatalogCard] = []
167
+ for path in sorted(root.glob("*.json")):
168
+ try:
169
+ raw = json.loads(path.read_text(encoding="utf-8"))
170
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
171
+ warnings.warn(f"load_cards: skipping {path}: {exc}", stacklevel=2)
172
+ continue
173
+ if not isinstance(raw, dict):
174
+ warnings.warn(f"load_cards: skipping {path}: root value is not a dict", stacklevel=2)
175
+ continue
176
+ try:
177
+ card = CatalogCard.from_dict(raw)
178
+ except (KeyError, TypeError) as exc:
179
+ warnings.warn(f"load_cards: skipping {path}: {exc}", stacklevel=2)
180
+ continue
181
+ cards.append(
182
+ CatalogCard.from_dict({**card.to_dict(), "kind": kind, "source": card.source or str(path.resolve())})
183
+ )
184
+ for path in sorted(root.glob("*.md")):
185
+ try:
186
+ cards.append(_parse_markdown_card(path, kind))
187
+ except (OSError, UnicodeDecodeError) as exc:
188
+ warnings.warn(f"load_cards: skipping {path}: {exc}", stacklevel=2)
189
+ continue
190
+ return sorted(cards, key=lambda card: card.card_id)
191
+
192
+
193
+ def visible_cards(cards: list[CatalogCard], profile: str) -> list[CatalogCard]:
194
+ """Filter cards by profile visibility (`*` means public)."""
195
+ return [card for card in cards if "*" in card.profiles or profile in card.profiles]
196
+
197
+
198
+ def _tokens(text: str) -> set[str]:
199
+ return {token for token in re.findall(r"[a-z0-9]+", text.lower()) if len(token) > 2}
200
+
201
+
202
+ def match_cards(cards: list[CatalogCard], query: str, limit: int = 5) -> list[CatalogCard]:
203
+ """Rank cards by deterministic token overlap with the query."""
204
+ needle = _tokens(query)
205
+ if not needle:
206
+ return []
207
+ scored: list[tuple[int, str, CatalogCard]] = []
208
+ for card in cards:
209
+ haystack = _tokens(f"{card.title} {card.summary} {' '.join(card.tags)}")
210
+ score = len(needle & haystack)
211
+ if score:
212
+ scored.append((score, card.card_id, card))
213
+ scored.sort(key=lambda item: (-item[0], item[1]))
214
+ return [card for _, _, card in scored[: max(0, limit)]]
215
+
216
+
217
+ def export_cards(cards: list[CatalogCard], out_path: str | Path) -> list[dict[str, Any]]:
218
+ """Write cards as deterministic JSON and return the exported payload."""
219
+ payload = [card.to_dict() for card in sorted(cards, key=lambda card: card.card_id)]
220
+ target = Path(out_path)
221
+ target.parent.mkdir(parents=True, exist_ok=True)
222
+ target.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
223
+ return payload
werktools/classify.py ADDED
@@ -0,0 +1,179 @@
1
+ """Offline heuristic risk-classifier for MCP tool manifests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import warnings
7
+
8
+ _RISK_ORDER = ("low", "medium", "high", "critical")
9
+
10
+ _SIGNAL_RULES: tuple[tuple[str, str, tuple[str, ...]], ...] = (
11
+ (
12
+ "shell/exec",
13
+ "critical",
14
+ (
15
+ r"\bshell\b",
16
+ r"\bexec\b",
17
+ r"\bexecute\b",
18
+ r"\bsubprocess\b",
19
+ r"\beval\b",
20
+ # "spawn a worker/thread" is concurrency vocabulary, not shell access.
21
+ r"\bspawn\b(?!\s+(?:a\s+|an\s+|the\s+)?(?:worker|thread))",
22
+ r"\bcmd\b",
23
+ # bare "command" over-flags CLI help text, but run/system command
24
+ # phrasing is genuine shell vocabulary.
25
+ r"\brun(?:s|ning)?[_\-\s]+(?:a\s+|an\s+|any\s+|arbitrary\s+)?commands?\b",
26
+ r"\bsystem[_\-\s]commands?\b",
27
+ r"\bcommand[_\-\s](?:execution|injection)\b",
28
+ ),
29
+ ),
30
+ (
31
+ "injection-phrasing",
32
+ "critical",
33
+ (
34
+ r"ignore\s+previous",
35
+ r"system\s+prompt",
36
+ r"disregard\s+(your\s+)?(previous\s+)?instructions?",
37
+ r"forget\s+(all\s+)?previous",
38
+ r"new\s+instructions?",
39
+ ),
40
+ ),
41
+ (
42
+ "secret/credential",
43
+ "high",
44
+ (
45
+ r"\bapi[_\-\s]?key\b",
46
+ r"\bpassword\b",
47
+ r"\bsecret\b",
48
+ # bare "token" is ML-tokenizer vocabulary; require a credential qualifier.
49
+ r"\b(?:access|api|auth|bearer|oauth|refresh|secret|session)[_\-\s]?tokens?\b",
50
+ r"\bcredential\b",
51
+ r"\bauth[_\-]?key\b",
52
+ ),
53
+ ),
54
+ (
55
+ "fs-write/delete/destructive",
56
+ "high",
57
+ (
58
+ r"\bdelete(?:[_-]|\b)",
59
+ r"\bremove(?:[_-]|\b)",
60
+ r"\bwrite(?:[_-]|\b)",
61
+ r"\boverwrite\b",
62
+ r"\btruncate\b",
63
+ r"\bunlink\b",
64
+ r"\berase\b",
65
+ r"\bdestruct\b",
66
+ ),
67
+ ),
68
+ (
69
+ "network/fetch/url",
70
+ "medium",
71
+ (
72
+ r"\bfetch\b",
73
+ r"\burl\b",
74
+ r"\bhttp\b",
75
+ r"\bdownload\b",
76
+ r"\brequest\b",
77
+ r"\bwebhook\b",
78
+ r"\bapi\b",
79
+ r"\bendpoint\b",
80
+ ),
81
+ ),
82
+ )
83
+
84
+
85
+ def _bump_risk(current: str, candidate: str) -> str:
86
+ try:
87
+ candidate_idx = _RISK_ORDER.index(candidate)
88
+ except ValueError:
89
+ warnings.warn(
90
+ f"_bump_risk: unknown risk level {candidate!r}; falling back to 'critical'",
91
+ stacklevel=2,
92
+ )
93
+ return "critical"
94
+ try:
95
+ current_idx = _RISK_ORDER.index(current)
96
+ except ValueError:
97
+ warnings.warn(
98
+ f"_bump_risk: unknown risk level {current!r}; falling back to 'critical'",
99
+ stacklevel=2,
100
+ )
101
+ return "critical"
102
+ if candidate_idx > current_idx:
103
+ return candidate
104
+ return current
105
+
106
+
107
+ def _collect_text(manifest: dict) -> str:
108
+ parts: list[str] = []
109
+ name = manifest.get("name")
110
+ if isinstance(name, str):
111
+ parts.append(name)
112
+ description = manifest.get("description")
113
+ if isinstance(description, str):
114
+ parts.append(description)
115
+ return " ".join(parts)
116
+
117
+
118
+ def _collect_schema_keys(manifest: dict) -> list[str]:
119
+ schema = manifest.get("inputSchema")
120
+ if not isinstance(schema, dict):
121
+ return []
122
+ properties = schema.get("properties")
123
+ if not isinstance(properties, dict):
124
+ return []
125
+ return [key for key in properties if isinstance(key, str)]
126
+
127
+
128
+ def _collect_schema_descriptions(manifest: dict) -> list[str]:
129
+ """Collect 'description' and 'title' string values from inputSchema.properties.
130
+
131
+ Injection phrasing can be embedded in property metadata text, not just in
132
+ key names or the tool's own description. Including them closes that evasion
133
+ path.
134
+ """
135
+ schema = manifest.get("inputSchema")
136
+ if not isinstance(schema, dict):
137
+ return []
138
+ properties = schema.get("properties")
139
+ if not isinstance(properties, dict):
140
+ return []
141
+ texts: list[str] = []
142
+ for prop_value in properties.values():
143
+ if not isinstance(prop_value, dict):
144
+ continue
145
+ for field in ("description", "title"):
146
+ val = prop_value.get(field)
147
+ if isinstance(val, str):
148
+ texts.append(val)
149
+ return texts
150
+
151
+
152
+ def classify_tool(manifest: dict) -> dict:
153
+ """Classify a tool manifest by static keyword heuristics only."""
154
+ if not isinstance(manifest, dict):
155
+ return {"risk": "low", "signals": [], "reasons": []}
156
+
157
+ combined = " ".join([
158
+ _collect_text(manifest),
159
+ *_collect_schema_keys(manifest),
160
+ *_collect_schema_descriptions(manifest),
161
+ ])
162
+ signals: list[str] = []
163
+ reasons: list[str] = []
164
+ risk = "low"
165
+
166
+ for signal, signal_risk, patterns in _SIGNAL_RULES:
167
+ matched = [
168
+ pattern
169
+ for pattern in patterns
170
+ if re.search(pattern, combined, re.IGNORECASE)
171
+ ]
172
+ if not matched:
173
+ continue
174
+
175
+ signals.append(signal)
176
+ reasons.append(f"{signal} detected (matched: {', '.join(matched[:3])})")
177
+ risk = _bump_risk(risk, signal_risk)
178
+
179
+ return {"risk": risk, "signals": signals, "reasons": reasons}