logion-cli 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.
- cli/__init__.py +2 -0
- cli/_config.py +51 -0
- cli/_confirm.py +16 -0
- cli/_context.py +17 -0
- cli/_course_bundle.py +46 -0
- cli/_course_capabilities.py +580 -0
- cli/_credentials.py +104 -0
- cli/_errors.py +82 -0
- cli/_first_run.py +90 -0
- cli/_harness/__init__.py +68 -0
- cli/_harness/base.py +106 -0
- cli/_harness/claude_code.py +168 -0
- cli/_harness/codex.py +79 -0
- cli/_harness/custom.py +55 -0
- cli/_harness/hermes.py +93 -0
- cli/_harness/opencode.py +255 -0
- cli/_local_state.py +1053 -0
- cli/_options.py +36 -0
- cli/_output.py +47 -0
- cli/_parser.py +73 -0
- cli/_recall_calibration.py +90 -0
- cli/_recall_ranker.py +74 -0
- cli/_taxonomy.py +120 -0
- cli/_update_policy.py +152 -0
- cli/_utils.py +16 -0
- cli/_version.py +26 -0
- cli/commands/__init__.py +2 -0
- cli/commands/admin.py +535 -0
- cli/commands/bounties.py +490 -0
- cli/commands/course_reviews/__init__.py +6 -0
- cli/commands/course_reviews/_download_handler.py +104 -0
- cli/commands/course_reviews/_render.py +129 -0
- cli/commands/course_reviews/handlers.py +197 -0
- cli/commands/course_reviews/parser.py +93 -0
- cli/commands/courses/__init__.py +6 -0
- cli/commands/courses/_capability_render.py +183 -0
- cli/commands/courses/_cmd_help.py +18 -0
- cli/commands/courses/_purchase.py +76 -0
- cli/commands/courses/_review_helpers.py +93 -0
- cli/commands/courses/_taxonomy_data.py +173 -0
- cli/commands/courses/_upload_bundle_validation.py +28 -0
- cli/commands/courses/_uploads_push.py +243 -0
- cli/commands/courses/capabilities.py +250 -0
- cli/commands/courses/capability_frontmatter.py +150 -0
- cli/commands/courses/handlers.py +50 -0
- cli/commands/courses/mutations.py +217 -0
- cli/commands/courses/parser.py +66 -0
- cli/commands/courses/parser_capabilities.py +95 -0
- cli/commands/courses/parser_sections.py +239 -0
- cli/commands/courses/parser_uploads.py +84 -0
- cli/commands/courses/parser_utils.py +65 -0
- cli/commands/courses/publication.py +60 -0
- cli/commands/courses/report_usage.py +131 -0
- cli/commands/courses/reviews.py +237 -0
- cli/commands/courses/taxonomy_handler.py +61 -0
- cli/commands/courses/taxonomy_suggest.py +197 -0
- cli/commands/courses/uploads.py +142 -0
- cli/commands/courses/versions.py +65 -0
- cli/commands/credits/__init__.py +6 -0
- cli/commands/credits/_helpers.py +153 -0
- cli/commands/credits/handlers.py +218 -0
- cli/commands/credits/parser.py +115 -0
- cli/commands/docs/__init__.py +6 -0
- cli/commands/docs/handlers.py +137 -0
- cli/commands/docs/parser.py +27 -0
- cli/commands/health/__init__.py +6 -0
- cli/commands/health/handlers.py +26 -0
- cli/commands/health/parser.py +20 -0
- cli/commands/identity/__init__.py +6 -0
- cli/commands/identity/_autopost.py +97 -0
- cli/commands/identity/_closing_copy.py +89 -0
- cli/commands/identity/_companion.py +232 -0
- cli/commands/identity/_companion_source.py +135 -0
- cli/commands/identity/_harness_select.py +85 -0
- cli/commands/identity/_onboarding_helpers.py +168 -0
- cli/commands/identity/handlers.py +173 -0
- cli/commands/identity/onboarding.py +246 -0
- cli/commands/identity/parser.py +72 -0
- cli/commands/listings/__init__.py +6 -0
- cli/commands/listings/handlers.py +135 -0
- cli/commands/listings/parser.py +57 -0
- cli/commands/notifications/__init__.py +6 -0
- cli/commands/notifications/handlers.py +120 -0
- cli/commands/notifications/parser.py +49 -0
- cli/commands/payments/__init__.py +6 -0
- cli/commands/payments/_orders_helpers.py +114 -0
- cli/commands/payments/handlers.py +138 -0
- cli/commands/payments/parser.py +97 -0
- cli/commands/recall/__init__.py +7 -0
- cli/commands/recall/handlers.py +87 -0
- cli/commands/recall/parser.py +70 -0
- cli/commands/referrals/__init__.py +6 -0
- cli/commands/referrals/_helpers.py +63 -0
- cli/commands/referrals/handlers.py +100 -0
- cli/commands/referrals/parser.py +65 -0
- cli/commands/reports/__init__.py +6 -0
- cli/commands/reports/handlers.py +57 -0
- cli/commands/reports/parser.py +52 -0
- cli/commands/skills/__init__.py +7 -0
- cli/commands/skills/_agent_symlink.py +161 -0
- cli/commands/skills/_finalize.py +112 -0
- cli/commands/skills/_inspect_handler.py +218 -0
- cli/commands/skills/_install_helpers.py +186 -0
- cli/commands/skills/_query_handlers.py +83 -0
- cli/commands/skills/_search_handler.py +136 -0
- cli/commands/skills/_update_handler.py +110 -0
- cli/commands/skills/_verify_handler.py +109 -0
- cli/commands/skills/handlers.py +202 -0
- cli/commands/skills/parser.py +154 -0
- cli/commands/workspace.py +406 -0
- cli/docs/README.md +5 -0
- cli/docs/__init__.py +1 -0
- cli/docs/bounties-and-referrals.md +18 -0
- cli/docs/concepts.md +47 -0
- cli/docs/creating-courses.md +25 -0
- cli/docs/credits-and-purchases.md +30 -0
- cli/docs/credits-terms.md +23 -0
- cli/docs/getting-started.md +95 -0
- cli/docs/marketplace-loop.md +108 -0
- cli/docs/privacy.md +30 -0
- cli/docs/referral-terms.md +24 -0
- cli/docs/reviews.md +47 -0
- cli/docs/safety.md +28 -0
- cli/docs/terms.md +54 -0
- cli/main.py +84 -0
- cli/templates/__init__.py +2 -0
- cli/templates/course_capabilities.template.yaml +189 -0
- cli/templates/course_license_apache-2.0.template.txt +30 -0
- cli/templates/course_license_logion-standard-course-v1.template.txt +49 -0
- cli/templates/course_license_mit.template.txt +21 -0
- logion_cli-0.1.0.dist-info/METADATA +49 -0
- logion_cli-0.1.0.dist-info/RECORD +135 -0
- logion_cli-0.1.0.dist-info/WHEEL +4 -0
- logion_cli-0.1.0.dist-info/entry_points.txt +4 -0
- logion_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Capability evidence rendering for course-reviews CLI commands."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def append_review_capability_evidence_lines(
|
|
10
|
+
lines: list[str],
|
|
11
|
+
payload: dict[str, Any],
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Append human-readable capability evidence lines to *lines*.
|
|
14
|
+
|
|
15
|
+
Used by course-reviews get to render declared/observed/mismatch
|
|
16
|
+
evidence in a readable format.
|
|
17
|
+
"""
|
|
18
|
+
status = payload.get("capabilities_status")
|
|
19
|
+
if status is not None:
|
|
20
|
+
lines.append(f"capabilities_status: {status}")
|
|
21
|
+
|
|
22
|
+
score = payload.get("capability_risk_score")
|
|
23
|
+
if score is not None:
|
|
24
|
+
lines.append(f"capability_risk_score: {score}")
|
|
25
|
+
|
|
26
|
+
declared = payload.get("declared_capabilities")
|
|
27
|
+
if declared:
|
|
28
|
+
lines.append("declared_capabilities:")
|
|
29
|
+
_append_declared_lines(lines, declared)
|
|
30
|
+
|
|
31
|
+
observed = payload.get("observed_capabilities")
|
|
32
|
+
if observed:
|
|
33
|
+
lines.append("observed_capabilities:")
|
|
34
|
+
_append_observed_lines(lines, observed)
|
|
35
|
+
|
|
36
|
+
mismatches = payload.get("capability_mismatches")
|
|
37
|
+
if mismatches:
|
|
38
|
+
lines.append("capability_mismatches:")
|
|
39
|
+
for m in mismatches:
|
|
40
|
+
severity = m.get("severity", "unknown")
|
|
41
|
+
code = m.get("code", "unknown")
|
|
42
|
+
lines.append(f" - {severity}: {code}")
|
|
43
|
+
if "observed" in m:
|
|
44
|
+
lines.append(f" observed: {m['observed']}")
|
|
45
|
+
if "declared" in m:
|
|
46
|
+
lines.append(f" declared: {m['declared']}")
|
|
47
|
+
if "message" in m:
|
|
48
|
+
lines.append(f" message: {m['message']}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def append_queue_capability_summary_lines(
|
|
52
|
+
lines: list[str],
|
|
53
|
+
item: dict[str, Any],
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Append compact capability summary lines for queue list items.
|
|
56
|
+
|
|
57
|
+
Only shows status, score, and mismatch count — never full payloads.
|
|
58
|
+
"""
|
|
59
|
+
status = item.get("capabilities_status")
|
|
60
|
+
if status is not None:
|
|
61
|
+
lines.append(f"capabilities_status: {status}")
|
|
62
|
+
|
|
63
|
+
score = item.get("capability_risk_score")
|
|
64
|
+
if score is not None:
|
|
65
|
+
lines.append(f"capability_risk_score: {score}")
|
|
66
|
+
|
|
67
|
+
count = item.get("capability_mismatch_count")
|
|
68
|
+
if count is not None:
|
|
69
|
+
lines.append(f"capability_mismatch_count: {count}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _append_declared_lines(
|
|
73
|
+
lines: list[str],
|
|
74
|
+
declared: dict[str, Any],
|
|
75
|
+
) -> None:
|
|
76
|
+
tools = declared.get("tools")
|
|
77
|
+
if tools:
|
|
78
|
+
lines.append(f" tools: {', '.join(tools)}")
|
|
79
|
+
|
|
80
|
+
network = declared.get("network")
|
|
81
|
+
if network:
|
|
82
|
+
domains = network.get("allow_domains")
|
|
83
|
+
if domains:
|
|
84
|
+
lines.append(f" network.allow_domains: {', '.join(domains)}")
|
|
85
|
+
|
|
86
|
+
filesystem = declared.get("filesystem")
|
|
87
|
+
if filesystem:
|
|
88
|
+
write_paths = filesystem.get("write")
|
|
89
|
+
if write_paths:
|
|
90
|
+
lines.append(f" filesystem.write: {', '.join(write_paths)}")
|
|
91
|
+
|
|
92
|
+
secrets = declared.get("secrets")
|
|
93
|
+
if secrets:
|
|
94
|
+
env_vars = secrets.get("env")
|
|
95
|
+
if env_vars:
|
|
96
|
+
lines.append(f" secrets.env: {', '.join(env_vars)}")
|
|
97
|
+
|
|
98
|
+
human_approval = declared.get("human_approval")
|
|
99
|
+
if human_approval:
|
|
100
|
+
required = human_approval.get("required")
|
|
101
|
+
if required is not None:
|
|
102
|
+
lines.append(f" human_approval.required: {str(required).lower()}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _append_observed_lines(
|
|
106
|
+
lines: list[str],
|
|
107
|
+
observed: dict[str, Any],
|
|
108
|
+
) -> None:
|
|
109
|
+
tools = observed.get("tools")
|
|
110
|
+
if tools:
|
|
111
|
+
lines.append(f" tools: {', '.join(tools)}")
|
|
112
|
+
|
|
113
|
+
hosts = observed.get("network_hosts")
|
|
114
|
+
if hosts:
|
|
115
|
+
lines.append(f" network_hosts: {', '.join(hosts)}")
|
|
116
|
+
|
|
117
|
+
fs_write = observed.get("filesystem_write")
|
|
118
|
+
if fs_write:
|
|
119
|
+
lines.append(f" filesystem_write: {', '.join(fs_write)}")
|
|
120
|
+
|
|
121
|
+
env_vars = observed.get("secrets_env")
|
|
122
|
+
if env_vars:
|
|
123
|
+
lines.append(f" secrets_env: {', '.join(env_vars)}")
|
|
124
|
+
|
|
125
|
+
dangerous = observed.get("dangerous_commands_detected")
|
|
126
|
+
if dangerous is not None:
|
|
127
|
+
lines.append(
|
|
128
|
+
f" dangerous_commands_detected: {str(dangerous).lower()}"
|
|
129
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Handlers for course-reviews commands."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from cli._config import resolve_config_from_args
|
|
13
|
+
from cli._confirm import require_yes
|
|
14
|
+
from cli._context import make_client
|
|
15
|
+
from cli._errors import handle_error, print_err, validate_uuid_id
|
|
16
|
+
from cli._output import emit
|
|
17
|
+
from cli._utils import only_not_none
|
|
18
|
+
from cli.commands.course_reviews._render import (
|
|
19
|
+
append_queue_capability_summary_lines,
|
|
20
|
+
append_review_capability_evidence_lines,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
REQUIRE_NON_EMPTY_MSG = "Error: {} must not be empty."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handle_list(args: argparse.Namespace) -> int:
|
|
27
|
+
"""Execute course-reviews list."""
|
|
28
|
+
config = resolve_config_from_args(args)
|
|
29
|
+
client = make_client(config)
|
|
30
|
+
try:
|
|
31
|
+
kwargs = only_not_none({}, limit=args.limit, cursor=args.cursor)
|
|
32
|
+
result = client.v1.course_reviews.list(**kwargs)
|
|
33
|
+
if config.json_output:
|
|
34
|
+
emit(result, json_output=True)
|
|
35
|
+
else:
|
|
36
|
+
_render_list(result)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
return handle_error(exc)
|
|
39
|
+
else:
|
|
40
|
+
return 0
|
|
41
|
+
finally:
|
|
42
|
+
client.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def handle_get(args: argparse.Namespace) -> int:
|
|
46
|
+
"""Execute course-reviews get."""
|
|
47
|
+
bad_id = validate_uuid_id(args.review_id, "REVIEW_ID")
|
|
48
|
+
if bad_id is not None:
|
|
49
|
+
return bad_id
|
|
50
|
+
config = resolve_config_from_args(args)
|
|
51
|
+
client = make_client(config)
|
|
52
|
+
try:
|
|
53
|
+
result = client.v1.course_reviews.get(review_id=args.review_id)
|
|
54
|
+
if config.json_output:
|
|
55
|
+
emit(result, json_output=True)
|
|
56
|
+
else:
|
|
57
|
+
_render_get(result)
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
return handle_error(exc)
|
|
60
|
+
else:
|
|
61
|
+
return 0
|
|
62
|
+
finally:
|
|
63
|
+
client.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def handle_approve(args: argparse.Namespace) -> int:
|
|
67
|
+
"""Execute course-reviews approve."""
|
|
68
|
+
bad_id = validate_uuid_id(args.review_id, "REVIEW_ID")
|
|
69
|
+
if bad_id is not None:
|
|
70
|
+
return bad_id
|
|
71
|
+
if args.reviewer_notes is not None and not args.reviewer_notes.strip():
|
|
72
|
+
print_err(REQUIRE_NON_EMPTY_MSG.format("--reviewer-notes"))
|
|
73
|
+
return 2
|
|
74
|
+
refusal = require_yes(args.yes, "approve this review")
|
|
75
|
+
if refusal is not None:
|
|
76
|
+
return refusal
|
|
77
|
+
config = resolve_config_from_args(args)
|
|
78
|
+
client = make_client(config)
|
|
79
|
+
try:
|
|
80
|
+
kwargs = only_not_none(
|
|
81
|
+
{"review_id": args.review_id},
|
|
82
|
+
reviewer_notes=args.reviewer_notes,
|
|
83
|
+
acknowledge_capability_mismatches=(
|
|
84
|
+
args.acknowledge_capability_mismatches or None
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
result = client.v1.course_reviews.approve(**kwargs)
|
|
88
|
+
emit(result, json_output=config.json_output)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
return handle_error(exc)
|
|
91
|
+
else:
|
|
92
|
+
return 0
|
|
93
|
+
finally:
|
|
94
|
+
client.close()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def handle_reject(args: argparse.Namespace) -> int:
|
|
98
|
+
"""Execute course-reviews reject."""
|
|
99
|
+
bad_id = validate_uuid_id(args.review_id, "REVIEW_ID")
|
|
100
|
+
if bad_id is not None:
|
|
101
|
+
return bad_id
|
|
102
|
+
if not args.decision_reason.strip():
|
|
103
|
+
print_err(REQUIRE_NON_EMPTY_MSG.format("--decision-reason"))
|
|
104
|
+
return 2
|
|
105
|
+
if not args.reviewer_notes.strip():
|
|
106
|
+
print_err(REQUIRE_NON_EMPTY_MSG.format("--reviewer-notes"))
|
|
107
|
+
return 2
|
|
108
|
+
refusal = require_yes(args.yes, "reject this review")
|
|
109
|
+
if refusal is not None:
|
|
110
|
+
return refusal
|
|
111
|
+
config = resolve_config_from_args(args)
|
|
112
|
+
client = make_client(config)
|
|
113
|
+
try:
|
|
114
|
+
result = client.v1.course_reviews.reject(
|
|
115
|
+
review_id=args.review_id,
|
|
116
|
+
decision_reason=args.decision_reason,
|
|
117
|
+
reviewer_notes=args.reviewer_notes,
|
|
118
|
+
capability_reason_code=args.capability_reason_code,
|
|
119
|
+
)
|
|
120
|
+
emit(result, json_output=config.json_output)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
return handle_error(exc)
|
|
123
|
+
else:
|
|
124
|
+
return 0
|
|
125
|
+
finally:
|
|
126
|
+
client.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── Human-readable rendering helpers ────────────────────────────
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _to_data(value: object) -> dict:
|
|
133
|
+
"""Convert a Pydantic model or dict to a plain dict."""
|
|
134
|
+
if isinstance(value, BaseModel):
|
|
135
|
+
return value.model_dump(mode="json")
|
|
136
|
+
if isinstance(value, dict):
|
|
137
|
+
return value
|
|
138
|
+
return json.loads(json.dumps(value))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _render_list(result: object) -> None:
|
|
142
|
+
"""Render queue list with human-readable capability summary."""
|
|
143
|
+
data = _to_data(result)
|
|
144
|
+
items = data.get("items", [])
|
|
145
|
+
if not items:
|
|
146
|
+
sys.stdout.write("No reviews in the queue.\n")
|
|
147
|
+
return
|
|
148
|
+
for item in items:
|
|
149
|
+
course = (
|
|
150
|
+
f"{item.get('course_title', '')} ({item.get('course_id', '')})"
|
|
151
|
+
)
|
|
152
|
+
lines: list[str] = [
|
|
153
|
+
f"review_id: {item.get('review_id', '')}",
|
|
154
|
+
f"course: {course}",
|
|
155
|
+
f"status: {item.get('review_status', '')}",
|
|
156
|
+
f"findings: {item.get('finding_count', 0)}",
|
|
157
|
+
]
|
|
158
|
+
append_queue_capability_summary_lines(lines, item)
|
|
159
|
+
sys.stdout.write("\n".join(lines) + "\n\n")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _render_get(result: object) -> None:
|
|
163
|
+
"""Render review detail with human-readable capability evidence."""
|
|
164
|
+
data = _to_data(result)
|
|
165
|
+
course = f"{data.get('course_title', '')} ({data.get('course_id', '')})"
|
|
166
|
+
lines: list[str] = [
|
|
167
|
+
f"review_id: {data.get('review_id', '')}",
|
|
168
|
+
f"course: {course}",
|
|
169
|
+
f"version_id: {data.get('version_id', '')}",
|
|
170
|
+
f"status: {data.get('review_status', '')}",
|
|
171
|
+
f"owner_agent_id: {data.get('owner_agent_id', '')}",
|
|
172
|
+
f"submitted_at: {data.get('submitted_at', '')}",
|
|
173
|
+
]
|
|
174
|
+
append_review_capability_evidence_lines(lines, data)
|
|
175
|
+
|
|
176
|
+
findings_by_layer = data.get("findings_by_layer", {})
|
|
177
|
+
if findings_by_layer:
|
|
178
|
+
lines.append("findings:")
|
|
179
|
+
for layer, findings in findings_by_layer.items():
|
|
180
|
+
lines.append(f" {layer}:")
|
|
181
|
+
for f in findings:
|
|
182
|
+
severity = f.get("severity", "unknown")
|
|
183
|
+
rule_id = f.get("rule_id", "unknown")
|
|
184
|
+
desc = f.get("description", "")
|
|
185
|
+
pass_marker = " [PASS]" if f.get("is_pass") else ""
|
|
186
|
+
lines.append(f" - {severity}: {rule_id}{pass_marker}")
|
|
187
|
+
if desc:
|
|
188
|
+
lines.append(f" {desc}")
|
|
189
|
+
|
|
190
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# Re-export from _download_handler so cli.commands.course_reviews.parser
|
|
194
|
+
# can keep importing handle_download from this module.
|
|
195
|
+
from cli.commands.course_reviews._download_handler import ( # noqa: E402, F401
|
|
196
|
+
handle_download,
|
|
197
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Parser registration for course-reviews commands."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from cli._options import COMMON_PARSER
|
|
9
|
+
|
|
10
|
+
from .handlers import (
|
|
11
|
+
handle_approve,
|
|
12
|
+
handle_download,
|
|
13
|
+
handle_get,
|
|
14
|
+
handle_list,
|
|
15
|
+
handle_reject,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
20
|
+
"""Register the ``course-reviews`` subcommand group."""
|
|
21
|
+
parser = subparsers.add_parser(
|
|
22
|
+
"course-reviews",
|
|
23
|
+
help="Manage course publication review queue",
|
|
24
|
+
)
|
|
25
|
+
sub = parser.add_subparsers(
|
|
26
|
+
dest="course_reviews_command",
|
|
27
|
+
required=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
list_parser = sub.add_parser(
|
|
31
|
+
"list",
|
|
32
|
+
help="List actionable review queue items",
|
|
33
|
+
parents=[COMMON_PARSER],
|
|
34
|
+
)
|
|
35
|
+
list_parser.add_argument("--limit", type=int)
|
|
36
|
+
list_parser.add_argument("--cursor")
|
|
37
|
+
list_parser.set_defaults(handler=handle_list)
|
|
38
|
+
|
|
39
|
+
get = sub.add_parser(
|
|
40
|
+
"get",
|
|
41
|
+
help="Get review queue item details",
|
|
42
|
+
parents=[COMMON_PARSER],
|
|
43
|
+
)
|
|
44
|
+
get.add_argument("review_id", metavar="REVIEW_ID")
|
|
45
|
+
get.set_defaults(handler=handle_get)
|
|
46
|
+
|
|
47
|
+
approve = sub.add_parser(
|
|
48
|
+
"approve",
|
|
49
|
+
help="Approve a publication review",
|
|
50
|
+
parents=[COMMON_PARSER],
|
|
51
|
+
)
|
|
52
|
+
approve.add_argument("review_id", metavar="REVIEW_ID")
|
|
53
|
+
approve.add_argument("--reviewer-notes")
|
|
54
|
+
approve.add_argument(
|
|
55
|
+
"--acknowledge-capability-mismatches",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Acknowledge capability mismatches on the review",
|
|
58
|
+
)
|
|
59
|
+
approve.add_argument("--yes", action="store_true")
|
|
60
|
+
approve.set_defaults(handler=handle_approve)
|
|
61
|
+
|
|
62
|
+
reject = sub.add_parser(
|
|
63
|
+
"reject",
|
|
64
|
+
help="Reject a publication review",
|
|
65
|
+
parents=[COMMON_PARSER],
|
|
66
|
+
)
|
|
67
|
+
reject.add_argument("review_id", metavar="REVIEW_ID")
|
|
68
|
+
reject.add_argument("--decision-reason", required=True)
|
|
69
|
+
reject.add_argument("--reviewer-notes", required=True)
|
|
70
|
+
reject.add_argument(
|
|
71
|
+
"--capability-reason-code",
|
|
72
|
+
help="Code from the review's capability mismatches",
|
|
73
|
+
)
|
|
74
|
+
reject.add_argument("--yes", action="store_true")
|
|
75
|
+
reject.set_defaults(handler=handle_reject)
|
|
76
|
+
|
|
77
|
+
download = sub.add_parser(
|
|
78
|
+
"download",
|
|
79
|
+
help=(
|
|
80
|
+
"Download the bundle under review to a local directory "
|
|
81
|
+
"so SKILL.md and references can be read before deciding"
|
|
82
|
+
),
|
|
83
|
+
parents=[COMMON_PARSER],
|
|
84
|
+
)
|
|
85
|
+
download.add_argument("review_id", metavar="REVIEW_ID")
|
|
86
|
+
download.add_argument(
|
|
87
|
+
"--target",
|
|
88
|
+
help=(
|
|
89
|
+
"Directory to write the bundle into "
|
|
90
|
+
"(default: ./review-bundles/<review-id>/)"
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
download.set_defaults(handler=handle_download)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Shared capability summary rendering for CLI commands."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _append_meta_fields(
|
|
10
|
+
lines: list[str],
|
|
11
|
+
payload: dict[str, Any],
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Append capability metadata fields."""
|
|
14
|
+
status = payload.get("capabilities_status")
|
|
15
|
+
if status:
|
|
16
|
+
lines.append(f"capabilities_status: {status}")
|
|
17
|
+
schema_version = payload.get("capabilities_schema_version")
|
|
18
|
+
if schema_version is not None:
|
|
19
|
+
lines.append(f"capabilities_schema_version: {schema_version}")
|
|
20
|
+
manifest_path = payload.get("capabilities_manifest_path")
|
|
21
|
+
if manifest_path is not None:
|
|
22
|
+
lines.append(f"capabilities_manifest_path: {manifest_path}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _append_summary_fields(
|
|
26
|
+
lines: list[str],
|
|
27
|
+
summary: dict[str, Any],
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Append capability summary detail fields (tools, permissions, paths)."""
|
|
30
|
+
for tool in summary.get("tools") or []:
|
|
31
|
+
lines.append(f"tools: {tool}")
|
|
32
|
+
allows_shell = summary.get("allows_shell")
|
|
33
|
+
if allows_shell is not None:
|
|
34
|
+
lines.append(f"allows_shell: {str(allows_shell).lower()}")
|
|
35
|
+
allows_network = summary.get("allows_network")
|
|
36
|
+
if allows_network is not None:
|
|
37
|
+
lines.append(f"allows_network: {str(allows_network).lower()}")
|
|
38
|
+
for domain in summary.get("allowed_domains") or []:
|
|
39
|
+
lines.append(f"allowed_domains: {domain}")
|
|
40
|
+
for rpath in summary.get("filesystem_read") or []:
|
|
41
|
+
lines.append(f"filesystem_read: {rpath}")
|
|
42
|
+
for wpath in summary.get("filesystem_write") or []:
|
|
43
|
+
lines.append(f"filesystem_write: {wpath}")
|
|
44
|
+
for env_var in summary.get("secrets_env") or []:
|
|
45
|
+
lines.append(f"secrets_env: {env_var}")
|
|
46
|
+
human_approval = summary.get("human_approval_required")
|
|
47
|
+
if human_approval is not None:
|
|
48
|
+
lines.append(f"human_approval_required: {str(human_approval).lower()}")
|
|
49
|
+
_append_runtime_fields(lines, summary)
|
|
50
|
+
_append_runtime_warnings(lines, summary)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _render_install_steps(
|
|
54
|
+
lines: list[str],
|
|
55
|
+
install: list[dict[str, Any]],
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Append install-step detail lines."""
|
|
58
|
+
lines.append(" install_steps:")
|
|
59
|
+
for step in install:
|
|
60
|
+
req = "required" if step.get("required") else "optional"
|
|
61
|
+
kind = step.get("kind", "")
|
|
62
|
+
command = step.get("command", "")
|
|
63
|
+
notes = step.get("notes", "")
|
|
64
|
+
cmd_part = f": {command}" if command else ""
|
|
65
|
+
note_part = f": {notes}" if notes else ""
|
|
66
|
+
lines.append(f" - {kind}{cmd_part} ({req}){note_part}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _append_runtime_fields(
|
|
70
|
+
lines: list[str],
|
|
71
|
+
summary: dict[str, Any],
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Append runtime requirement and install-step detail fields.
|
|
74
|
+
|
|
75
|
+
Only rendered when any runtime field is non-empty, so minimal
|
|
76
|
+
manifests stay compact.
|
|
77
|
+
"""
|
|
78
|
+
env = summary.get("runtime_requires_env") or []
|
|
79
|
+
bins = summary.get("runtime_requires_bins") or []
|
|
80
|
+
any_bins = summary.get("runtime_requires_any_bins") or []
|
|
81
|
+
config = summary.get("runtime_requires_config") or []
|
|
82
|
+
os_vals = summary.get("runtime_requires_os") or []
|
|
83
|
+
software = summary.get("runtime_requires_software") or []
|
|
84
|
+
install = summary.get("runtime_install") or []
|
|
85
|
+
if not any([env, bins, any_bins, config, os_vals, software, install]):
|
|
86
|
+
return
|
|
87
|
+
lines.append("runtime_requirements:")
|
|
88
|
+
if env:
|
|
89
|
+
lines.append(f" env: {', '.join(env)}")
|
|
90
|
+
if bins:
|
|
91
|
+
lines.append(f" bins: {', '.join(bins)}")
|
|
92
|
+
if any_bins:
|
|
93
|
+
groups = [" or ".join(g) for g in any_bins]
|
|
94
|
+
lines.append(f" any_bins: {', '.join(groups)}")
|
|
95
|
+
if config:
|
|
96
|
+
lines.append(f" config: {', '.join(config)}")
|
|
97
|
+
if os_vals:
|
|
98
|
+
lines.append(f" os: {', '.join(os_vals)}")
|
|
99
|
+
if software:
|
|
100
|
+
lines.append(" software:")
|
|
101
|
+
for sw in software:
|
|
102
|
+
req = "required" if sw.get("required") else "optional"
|
|
103
|
+
install_kind = sw.get("install", "external")
|
|
104
|
+
name = sw.get("name", "")
|
|
105
|
+
notes = sw.get("notes", "")
|
|
106
|
+
suffix = f": {notes}" if notes else ""
|
|
107
|
+
lines.append(f" - {name} ({install_kind}, {req}){suffix}")
|
|
108
|
+
if install:
|
|
109
|
+
_render_install_steps(lines, install)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _append_runtime_warnings(
|
|
113
|
+
lines: list[str],
|
|
114
|
+
summary: dict[str, Any],
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Append runtime cross-field warning codes."""
|
|
117
|
+
codes = summary.get("runtime_warning_codes") or []
|
|
118
|
+
if not codes:
|
|
119
|
+
return
|
|
120
|
+
lines.append("runtime_warnings:")
|
|
121
|
+
for code in codes:
|
|
122
|
+
lines.append(f" - {code}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def append_capability_summary_lines(
|
|
126
|
+
lines: list[str],
|
|
127
|
+
payload: dict[str, Any],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Append human-readable capability summary lines to *lines*."""
|
|
130
|
+
_append_meta_fields(lines, payload)
|
|
131
|
+
summary = payload.get("capabilities_summary")
|
|
132
|
+
if summary:
|
|
133
|
+
_append_summary_fields(lines, summary)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def append_approved_capability_summary_lines(
|
|
137
|
+
lines: list[str],
|
|
138
|
+
payload: dict[str, Any],
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Append human-readable approved capability summary lines.
|
|
141
|
+
|
|
142
|
+
Only renders the buyer-safe subset: tools, allows_shell,
|
|
143
|
+
allows_network, allowed_domains, human_approval_required.
|
|
144
|
+
Does not expose review-only evidence.
|
|
145
|
+
"""
|
|
146
|
+
approved = payload.get("approved_capabilities_summary")
|
|
147
|
+
if not approved or not isinstance(approved, dict):
|
|
148
|
+
return
|
|
149
|
+
lines.append("approved_capabilities_summary:")
|
|
150
|
+
for tool in approved.get("tools") or []:
|
|
151
|
+
lines.append(f" tools: {tool}")
|
|
152
|
+
allows_shell = approved.get("allows_shell")
|
|
153
|
+
if allows_shell is not None:
|
|
154
|
+
lines.append(f" allows_shell: {str(allows_shell).lower()}")
|
|
155
|
+
allows_network = approved.get("allows_network")
|
|
156
|
+
if allows_network is not None:
|
|
157
|
+
lines.append(f" allows_network: {str(allows_network).lower()}")
|
|
158
|
+
for domain in approved.get("allowed_domains") or []:
|
|
159
|
+
lines.append(f" allowed_domains: {domain}")
|
|
160
|
+
human_approval = approved.get("human_approval_required")
|
|
161
|
+
if human_approval is not None:
|
|
162
|
+
val = str(human_approval).lower()
|
|
163
|
+
lines.append(f" human_approval_required: {val}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def append_capability_feedback_lines(
|
|
167
|
+
lines: list[str],
|
|
168
|
+
payload: dict[str, Any],
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Append human-readable capability feedback items."""
|
|
171
|
+
feedback_items = payload.get("capability_feedback")
|
|
172
|
+
if not feedback_items:
|
|
173
|
+
return
|
|
174
|
+
lines.append("capability_feedback:")
|
|
175
|
+
for item in feedback_items:
|
|
176
|
+
code = item.get("reason_code", "unknown")
|
|
177
|
+
msg = item.get("message", "")
|
|
178
|
+
lines.append(f" - reason_code: {code}")
|
|
179
|
+
if msg:
|
|
180
|
+
lines.append(f" message: {msg}")
|
|
181
|
+
file_path = item.get("file_path")
|
|
182
|
+
if file_path:
|
|
183
|
+
lines.append(f" file_path: {file_path}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Help strings for courses subcommands."""
|
|
3
|
+
|
|
4
|
+
CMD_HELP = {
|
|
5
|
+
"create": "Create a new course",
|
|
6
|
+
"get": "Get course details",
|
|
7
|
+
"purchase": "Purchase a course using credits",
|
|
8
|
+
"update": "Update an existing course",
|
|
9
|
+
"uploads": "Manage course version uploads",
|
|
10
|
+
"publication": "Manage course publication review",
|
|
11
|
+
"reviews": "Manage marketplace course reviews",
|
|
12
|
+
"feedback": "Get review feedback for a course",
|
|
13
|
+
"versions": "Manage course versions",
|
|
14
|
+
"capabilities": "Validate and inspect local capability manifests",
|
|
15
|
+
"report-usage": (
|
|
16
|
+
"File a usage review after completing a course-driven task"
|
|
17
|
+
),
|
|
18
|
+
}
|