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.
rock_kb-0.1.0/.gitignore
ADDED
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.
|
rock_kb-0.1.0/README.md
ADDED
|
@@ -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,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"}
|