rock-kb 0.1.0__tar.gz

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.
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ .venv/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ *.pyc
8
+ docs/log/
9
+ data/
10
+ service/dist/
11
+ service/.wrangler/
12
+ service/node_modules/
13
+ agent/*.generated.tmp
rock_kb-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: rock-kb
3
+ Version: 0.1.0
4
+ Summary: Thin CLI client for the public Rock RMS agent knowledge base.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ # rock-kb
9
+
10
+ Thin terminal client for the public Rock RMS agent knowledge base.
11
+
12
+ Current public install from GitHub, before the PyPI package is released:
13
+
14
+ ```bash
15
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb search "check-in labels not printing"
16
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb get check-in
17
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb claims workflows --min-tier source_backed
18
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb dashboard
19
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb mcp-config
20
+ ```
21
+
22
+ After the client is published to PyPI, the shorter package-registry form works:
23
+
24
+ ```bash
25
+ uvx rock-kb search "check-in labels not printing"
26
+ uvx rock-kb get check-in
27
+ uvx rock-kb claims workflows --min-tier source_backed
28
+ uvx rock-kb dashboard
29
+ uvx rock-kb mcp-config
30
+ ```
31
+
32
+ From a local `rock-agent-kb` checkout before the package is published:
33
+
34
+ ```bash
35
+ uv run --project clients/python rock-kb search "check-in labels not printing"
36
+ uv run --project clients/python rock-kb validate bundle.jsonl
37
+ ROCK_KB_TOKEN=<issued-token> uv run --project clients/python rock-kb submit bundle.jsonl --org <org-id>
38
+ ```
39
+
40
+ Set `ROCK_KB_URL` to point at a staging service. Set `ROCK_KB_TOKEN` when submitting bundles.
41
+
42
+ Hosted submission is token-gated per organization. If `rock-kb submit` reports
43
+ that `ROCK_KB_TOKEN` is required, ask a Rock KB maintainer to issue a token for
44
+ the reviewed `orgs/<org-id>.yaml` registration. Store the token in an
45
+ environment variable, CI/app secret, or local secret store such as macOS
46
+ Keychain; do not save it in repo files.
@@ -0,0 +1,39 @@
1
+ # rock-kb
2
+
3
+ Thin terminal client for the public Rock RMS agent knowledge base.
4
+
5
+ Current public install from GitHub, before the PyPI package is released:
6
+
7
+ ```bash
8
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb search "check-in labels not printing"
9
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb get check-in
10
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb claims workflows --min-tier source_backed
11
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb dashboard
12
+ uvx --from 'git+https://github.com/ONE-ALL-Church/rock-agent-kb#subdirectory=clients/python' rock-kb mcp-config
13
+ ```
14
+
15
+ After the client is published to PyPI, the shorter package-registry form works:
16
+
17
+ ```bash
18
+ uvx rock-kb search "check-in labels not printing"
19
+ uvx rock-kb get check-in
20
+ uvx rock-kb claims workflows --min-tier source_backed
21
+ uvx rock-kb dashboard
22
+ uvx rock-kb mcp-config
23
+ ```
24
+
25
+ From a local `rock-agent-kb` checkout before the package is published:
26
+
27
+ ```bash
28
+ uv run --project clients/python rock-kb search "check-in labels not printing"
29
+ uv run --project clients/python rock-kb validate bundle.jsonl
30
+ ROCK_KB_TOKEN=<issued-token> uv run --project clients/python rock-kb submit bundle.jsonl --org <org-id>
31
+ ```
32
+
33
+ Set `ROCK_KB_URL` to point at a staging service. Set `ROCK_KB_TOKEN` when submitting bundles.
34
+
35
+ Hosted submission is token-gated per organization. If `rock-kb submit` reports
36
+ that `ROCK_KB_TOKEN` is required, ask a Rock KB maintainer to issue a token for
37
+ the reviewed `orgs/<org-id>.yaml` registration. Store the token in an
38
+ environment variable, CI/app secret, or local secret store such as macOS
39
+ Keychain; do not save it in repo files.
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "rock-kb"
3
+ version = "0.1.0"
4
+ description = "Thin CLI client for the public Rock RMS agent knowledge base."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [project.scripts]
10
+ rock-kb = "rock_kb_client.cli:main"
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/rock_kb_client"]
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from urllib import request
9
+
10
+ from .validator import validate_bundle
11
+
12
+ DEFAULT_BASE_URL = "https://rock-agent-kb.oneandall.church"
13
+ USER_AGENT = "rock-kb-client/0.1 (+https://github.com/ONE-ALL-Church/rock-agent-kb)"
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ parser = argparse.ArgumentParser(prog="rock-kb")
18
+ parser.add_argument("--url", default=os.environ.get("ROCK_KB_URL", DEFAULT_BASE_URL), help="Rock KB service base URL")
19
+ subparsers = parser.add_subparsers(dest="command", required=True)
20
+
21
+ search = subparsers.add_parser("search")
22
+ search.add_argument("query")
23
+ search.add_argument("--limit", type=int, default=10)
24
+ search.add_argument("--min-tier", default="routing_context_only")
25
+
26
+ subparsers.add_parser("concepts")
27
+ get = subparsers.add_parser("get")
28
+ get.add_argument("concept_id")
29
+
30
+ claims = subparsers.add_parser("claims")
31
+ claims.add_argument("concept_id")
32
+ claims.add_argument("--tier")
33
+ claims.add_argument("--min-tier", default="routing_context_only")
34
+
35
+ subparsers.add_parser("manifest")
36
+ subparsers.add_parser("dashboard")
37
+
38
+ validate = subparsers.add_parser("validate")
39
+ validate.add_argument("bundle", type=Path)
40
+
41
+ submit = subparsers.add_parser("submit")
42
+ submit.add_argument("bundle", type=Path)
43
+ submit.add_argument("--org", required=True)
44
+
45
+ subparsers.add_parser("mcp-config")
46
+
47
+ args = parser.parse_args(argv)
48
+ base_url = str(args.url).rstrip("/")
49
+
50
+ if args.command == "search":
51
+ return print_json(get_json(f"{base_url}/search?q={quote(args.query)}&limit={args.limit}&min_tier={quote(args.min_tier)}"))
52
+ if args.command == "concepts":
53
+ return print_json(get_json(f"{base_url}/concepts"))
54
+ if args.command == "get":
55
+ return print_text(get_text(f"{base_url}/concepts/{quote(args.concept_id)}.md"))
56
+ if args.command == "claims":
57
+ suffix = f"?min_tier={quote(args.min_tier)}"
58
+ if args.tier:
59
+ suffix += f"&tier={quote(args.tier)}"
60
+ return print_json(get_json(f"{base_url}/claims/{quote(args.concept_id)}{suffix}"))
61
+ if args.command == "manifest":
62
+ return print_json(get_json(f"{base_url}/manifest.json"))
63
+ if args.command == "dashboard":
64
+ return print_json(get_json(f"{base_url}/operations/dashboard"))
65
+ if args.command == "validate":
66
+ errors = validate_bundle(args.bundle)
67
+ if errors:
68
+ for error in errors:
69
+ print(f"ERROR {error}", file=sys.stderr)
70
+ return 1
71
+ return print_json({"status": "ok", "file": str(args.bundle)})
72
+ if args.command == "submit":
73
+ errors = validate_bundle(args.bundle)
74
+ if errors:
75
+ for error in errors:
76
+ print(f"ERROR {error}", file=sys.stderr)
77
+ return 1
78
+ token = os.environ.get("ROCK_KB_TOKEN")
79
+ if not token:
80
+ print("ERROR ROCK_KB_TOKEN is required for submit", file=sys.stderr)
81
+ return 1
82
+ rows = [json.loads(line) for line in args.bundle.read_text(encoding="utf-8").splitlines() if line.strip()]
83
+ return print_json(post_json(f"{base_url}/submit", {"org_id": args.org, "bundle": rows}, token=token))
84
+ if args.command == "mcp-config":
85
+ return print_json(
86
+ {
87
+ "mcpServers": {
88
+ "rock-kb": {
89
+ "type": "http",
90
+ "url": f"{base_url}/mcp"
91
+ }
92
+ }
93
+ }
94
+ )
95
+ return 1
96
+
97
+
98
+ def get_json(url: str):
99
+ return json.loads(get_text(url))
100
+
101
+
102
+ def get_text(url: str) -> str:
103
+ req = request.Request(url, headers={"user-agent": USER_AGENT})
104
+ with request.urlopen(req) as response:
105
+ return response.read().decode("utf-8")
106
+
107
+
108
+ def post_json(url: str, payload: dict, token: str):
109
+ body = json.dumps(payload).encode("utf-8")
110
+ req = request.Request(
111
+ url,
112
+ data=body,
113
+ method="POST",
114
+ headers={
115
+ "content-type": "application/json",
116
+ "authorization": f"Bearer {token}",
117
+ "user-agent": USER_AGENT,
118
+ "accept": "application/json",
119
+ },
120
+ )
121
+ with request.urlopen(req) as response:
122
+ return json.loads(response.read().decode("utf-8"))
123
+
124
+
125
+ def quote(value: str) -> str:
126
+ from urllib.parse import quote as url_quote
127
+
128
+ return url_quote(value, safe="")
129
+
130
+
131
+ def print_json(value) -> int:
132
+ print(json.dumps(value, indent=2, sort_keys=True))
133
+ return 0
134
+
135
+
136
+ def print_text(value: str) -> int:
137
+ print(value, end="" if value.endswith("\n") else "\n")
138
+ return 0
139
+
140
+
141
+ if __name__ == "__main__":
142
+ raise SystemExit(main())
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from urllib.parse import urlparse
7
+
8
+ SCHEMA = "rock-kb-org-contribution-v1"
9
+ CONTRIBUTION_TYPES = {
10
+ "task_card",
11
+ "troubleshooting_pattern",
12
+ "release_caveat",
13
+ "entity_note",
14
+ "guide_section",
15
+ "source_link",
16
+ "open_question",
17
+ }
18
+ PUBLIC_REVIEW_STATUSES = {"redaction_reviewed", "approved_for_public_distillation"}
19
+ CONFIDENCE_VALUES = {"low", "medium", "high", "needs_review"}
20
+ REQUIRED_FIELDS = {
21
+ "schema",
22
+ "contribution_id",
23
+ "org_id",
24
+ "concept_ids",
25
+ "contribution_type",
26
+ "title",
27
+ "distilled_summary",
28
+ "source_urls",
29
+ "source_record_ids",
30
+ "redaction_attestation",
31
+ "review_status",
32
+ "license_attestation",
33
+ "confidence",
34
+ "needs_live_verification",
35
+ }
36
+ PRIVATE_FIELD_NAMES = {
37
+ "raw_text",
38
+ "full_text",
39
+ "content",
40
+ "html",
41
+ "markdown",
42
+ "transcript",
43
+ "media_url",
44
+ "private_path",
45
+ "private_source_paths",
46
+ "private_corpus_pointer",
47
+ }
48
+ PRIVATE_PATH_PREFIXES = (
49
+ "data/review/",
50
+ "data/media/",
51
+ "data/normalized/",
52
+ "data/raw-manifests/",
53
+ "data/index/",
54
+ )
55
+ DIRECT_MEDIA_URL_HINTS = (
56
+ ".mp3",
57
+ ".mp4",
58
+ ".m3u8",
59
+ ".mpd",
60
+ "player.vimeo.com",
61
+ "oauth2_token_id=",
62
+ "access_token=",
63
+ "signature=",
64
+ )
65
+ SENSITIVE_PATTERNS = (
66
+ re.compile(r"(?i)(?:^|[^A-Za-z0-9_-])(password|secret|api[_-]?key|token)\s*[:=]\s*['\"]?[^'\"\s]+"),
67
+ re.compile(r"(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])"),
68
+ re.compile(r"(?i)connectionstring\s*[:=]"),
69
+ )
70
+
71
+
72
+ def validate_bundle(path: Path) -> list[str]:
73
+ errors: list[str] = []
74
+ seen_ids: dict[str, str] = {}
75
+ for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
76
+ if not line.strip():
77
+ continue
78
+ label = f"{path}:{line_number}"
79
+ try:
80
+ row = json.loads(line)
81
+ except json.JSONDecodeError as exc:
82
+ errors.append(f"{label} invalid JSONL: {exc.msg}")
83
+ continue
84
+ if not isinstance(row, dict):
85
+ errors.append(f"{label} row must be an object")
86
+ continue
87
+ contribution_id = str(row.get("contribution_id") or "")
88
+ if contribution_id:
89
+ if contribution_id in seen_ids:
90
+ errors.append(f"{label} duplicate contribution_id {contribution_id}; first seen at {seen_ids[contribution_id]}")
91
+ else:
92
+ seen_ids[contribution_id] = label
93
+ errors.extend(validate_row(row, label))
94
+ return errors
95
+
96
+
97
+ def validate_row(row: dict, label: str) -> list[str]:
98
+ errors: list[str] = []
99
+ missing = sorted(field for field in REQUIRED_FIELDS if field not in row or row.get(field) is None)
100
+ if missing:
101
+ errors.append(f"{label} missing fields: {', '.join(missing)}")
102
+ if row.get("schema") != SCHEMA:
103
+ errors.append(f"{label} schema must be {SCHEMA}")
104
+ if row.get("contribution_type") not in CONTRIBUTION_TYPES:
105
+ errors.append(f"{label} invalid contribution_type")
106
+ if row.get("review_status") not in PUBLIC_REVIEW_STATUSES:
107
+ errors.append(f"{label} public contribution must be redaction_reviewed or approved_for_public_distillation")
108
+ if row.get("confidence") not in CONFIDENCE_VALUES:
109
+ errors.append(f"{label} invalid confidence")
110
+ if not isinstance(row.get("needs_live_verification"), bool):
111
+ errors.append(f"{label} needs_live_verification must be true or false")
112
+ if not isinstance(row.get("concept_ids"), list) or not row.get("concept_ids"):
113
+ errors.append(f"{label} concept_ids must be a non-empty list")
114
+ if not isinstance(row.get("source_urls"), list):
115
+ errors.append(f"{label} source_urls must be a list")
116
+ if not isinstance(row.get("source_record_ids"), list):
117
+ errors.append(f"{label} source_record_ids must be a list")
118
+ if not row.get("source_urls") and not row.get("source_record_ids"):
119
+ errors.append(f"{label} must include source_urls or source_record_ids")
120
+ for url in row.get("source_urls") or []:
121
+ if not isinstance(url, str):
122
+ errors.append(f"{label} source_urls must contain strings")
123
+ elif url and urlparse(url).scheme and urlparse(url).scheme not in {"http", "https"}:
124
+ errors.append(f"{label} source_urls must use http or https URLs")
125
+ if not truthy_attestation(row.get("redaction_attestation")):
126
+ errors.append(f"{label} redaction_attestation must be true or an affirmative string")
127
+ if not truthy_attestation(row.get("license_attestation")):
128
+ errors.append(f"{label} license_attestation must be true or an affirmative string")
129
+ errors.extend(f"{label} {message}" for message in find_leaks(row))
130
+ return errors
131
+
132
+
133
+ def find_leaks(row: dict) -> list[str]:
134
+ errors: list[str] = []
135
+ for field_path in prohibited_public_field_paths(row):
136
+ errors.append(f"contains prohibited public field: {field_path}")
137
+ serialized = json.dumps(row, ensure_ascii=False)
138
+ for line in serialized.splitlines():
139
+ for pattern in SENSITIVE_PATTERNS:
140
+ if pattern.search(line):
141
+ errors.append(f"contains sensitive-looking value: {line.strip()[:120]}")
142
+ break
143
+ for path, value in string_values(row):
144
+ lowered = value.lower()
145
+ if any(hint in lowered for hint in DIRECT_MEDIA_URL_HINTS):
146
+ errors.append(f"contains direct/tokenized media URL at {path}: {value[:120]}")
147
+ normalized = value.replace("\\", "/")
148
+ if any(prefix in normalized for prefix in PRIVATE_PATH_PREFIXES):
149
+ errors.append(f"contains private path reference at {path}: {value[:120]}")
150
+ return errors
151
+
152
+
153
+ def prohibited_public_field_paths(value, prefix: str = "$") -> list[str]:
154
+ paths: list[str] = []
155
+ if isinstance(value, dict):
156
+ for key, nested in value.items():
157
+ path = f"{prefix}.{key}"
158
+ if key in PRIVATE_FIELD_NAMES:
159
+ paths.append(path)
160
+ paths.extend(prohibited_public_field_paths(nested, path))
161
+ elif isinstance(value, list):
162
+ for index, nested in enumerate(value):
163
+ paths.extend(prohibited_public_field_paths(nested, f"{prefix}[{index}]"))
164
+ return paths
165
+
166
+
167
+ def string_values(value, prefix: str = "$") -> list[tuple[str, str]]:
168
+ values: list[tuple[str, str]] = []
169
+ if isinstance(value, dict):
170
+ for key, nested in value.items():
171
+ values.extend(string_values(nested, f"{prefix}.{key}"))
172
+ elif isinstance(value, list):
173
+ for index, nested in enumerate(value):
174
+ values.extend(string_values(nested, f"{prefix}[{index}]"))
175
+ elif isinstance(value, str):
176
+ values.append((prefix, value))
177
+ return values
178
+
179
+
180
+ def truthy_attestation(value) -> bool:
181
+ if value is True:
182
+ return True
183
+ return isinstance(value, str) and value.strip().lower() in {"true", "yes", "reviewed", "attested", "approved"}