xone-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.
- xone_cli/__init__.py +4 -0
- xone_cli/__main__.py +8 -0
- xone_cli/cli.py +189 -0
- xone_cli/evidence.py +138 -0
- xone_cli/lab.py +38 -0
- xone_cli/model.py +43 -0
- xone_cli/release.py +111 -0
- xone_cli/risk.py +26 -0
- xone_cli/runbook.py +48 -0
- xone_cli/tooling.py +106 -0
- xone_cli-0.1.0.dist-info/METADATA +79 -0
- xone_cli-0.1.0.dist-info/RECORD +16 -0
- xone_cli-0.1.0.dist-info/WHEEL +5 -0
- xone_cli-0.1.0.dist-info/entry_points.txt +2 -0
- xone_cli-0.1.0.dist-info/licenses/LICENSE +22 -0
- xone_cli-0.1.0.dist-info/top_level.txt +1 -0
xone_cli/__init__.py
ADDED
xone_cli/__main__.py
ADDED
xone_cli/cli.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from xone_cli import __version__
|
|
10
|
+
from xone_cli.evidence import build_failure_packet, collect_evidence, collect_for_runbook, gate_evidence
|
|
11
|
+
from xone_cli.lab import render_evidence_loop_lab
|
|
12
|
+
from xone_cli.risk import render_risk_context
|
|
13
|
+
from xone_cli.runbook import write_runbook
|
|
14
|
+
from xone_cli.release import print_release_report, verify_release
|
|
15
|
+
from xone_cli.tooling import doctor_status
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
parser = argparse.ArgumentParser(prog="xone")
|
|
20
|
+
parser.add_argument("--version", action="store_true", help="show version and exit")
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
22
|
+
|
|
23
|
+
doctor = subparsers.add_parser("doctor", help="check local X-One tool availability")
|
|
24
|
+
doctor.add_argument("--json", action="store_true", help="write machine-readable status")
|
|
25
|
+
doctor.add_argument("--dry-run", action="store_true", help="show checks without running them")
|
|
26
|
+
|
|
27
|
+
evidence = subparsers.add_parser("evidence", help="work with PR evidence and failure packets")
|
|
28
|
+
evidence_subparsers = evidence.add_subparsers(dest="evidence_command")
|
|
29
|
+
collect = evidence_subparsers.add_parser("collect", help="collect PR evidence")
|
|
30
|
+
_add_evidence_collection_args(collect)
|
|
31
|
+
collect.add_argument("--output", type=Path, required=True, help="JSON evidence output path")
|
|
32
|
+
collect.add_argument("--dry-run", action="store_true")
|
|
33
|
+
|
|
34
|
+
gate = evidence_subparsers.add_parser("gate", help="run baseline gate")
|
|
35
|
+
_add_evidence_collection_args(gate)
|
|
36
|
+
gate.add_argument("--baseline", type=Path, required=True)
|
|
37
|
+
gate.add_argument("--dry-run", action="store_true")
|
|
38
|
+
|
|
39
|
+
packet = evidence_subparsers.add_parser("packet", help="build a redacted failure packet")
|
|
40
|
+
packet.add_argument("--input", type=Path, required=True)
|
|
41
|
+
packet.add_argument("--output", type=Path, required=True)
|
|
42
|
+
packet.add_argument("--profile", choices=("incident", "issue"), default="issue")
|
|
43
|
+
packet.add_argument("--dry-run", action="store_true")
|
|
44
|
+
|
|
45
|
+
risk = subparsers.add_parser("risk", help="work with MCP risk context")
|
|
46
|
+
risk_subparsers = risk.add_subparsers(dest="risk_command")
|
|
47
|
+
context = risk_subparsers.add_parser("context", help="render risk context")
|
|
48
|
+
context.add_argument("--catalog", type=Path, required=True)
|
|
49
|
+
context.add_argument("--output", type=Path, required=True)
|
|
50
|
+
context.add_argument("--dry-run", action="store_true")
|
|
51
|
+
|
|
52
|
+
lab = subparsers.add_parser("lab", help="work with safe-local lab scenarios")
|
|
53
|
+
lab_subparsers = lab.add_subparsers(dest="lab_command")
|
|
54
|
+
evidence_loop = lab_subparsers.add_parser("evidence-loop", help="render Agent Evidence Loop scenario")
|
|
55
|
+
evidence_loop.add_argument("--output", type=Path, required=True)
|
|
56
|
+
evidence_loop.add_argument("--dry-run", action="store_true")
|
|
57
|
+
|
|
58
|
+
release = subparsers.add_parser("release", help="verify local release readiness")
|
|
59
|
+
release_subparsers = release.add_subparsers(dest="release_command")
|
|
60
|
+
verify = release_subparsers.add_parser("verify", help="run release/package checks")
|
|
61
|
+
verify.add_argument("--project-root", type=Path, default=Path("."))
|
|
62
|
+
verify.add_argument("--build", action="store_true")
|
|
63
|
+
verify.add_argument("--install", action="store_true")
|
|
64
|
+
verify.add_argument("--smoke", action="store_true")
|
|
65
|
+
verify.add_argument("--json", action="store_true")
|
|
66
|
+
|
|
67
|
+
runbook = subparsers.add_parser("runbook", help="assemble a local X-One evidence runbook")
|
|
68
|
+
_add_evidence_collection_args(runbook)
|
|
69
|
+
runbook.add_argument("--output", type=Path)
|
|
70
|
+
runbook.add_argument("--dry-run", action="store_true", help="show underlying commands without running them")
|
|
71
|
+
runbook.add_argument("--json", action="store_true", help="write machine-readable summary")
|
|
72
|
+
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
77
|
+
parser = build_parser()
|
|
78
|
+
args = parser.parse_args(argv)
|
|
79
|
+
|
|
80
|
+
if args.version:
|
|
81
|
+
print(f"xone {__version__}")
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
if args.command == "doctor":
|
|
85
|
+
report = doctor_status()
|
|
86
|
+
payload = report.to_dict()
|
|
87
|
+
payload["dry_run"] = args.dry_run
|
|
88
|
+
if args.json:
|
|
89
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
90
|
+
else:
|
|
91
|
+
_print_doctor(report)
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
if args.command == "evidence":
|
|
95
|
+
if args.evidence_command == "collect":
|
|
96
|
+
return collect_evidence(
|
|
97
|
+
repo=args.repo,
|
|
98
|
+
base=args.base,
|
|
99
|
+
head=args.head,
|
|
100
|
+
output=args.output,
|
|
101
|
+
test_logs=args.test_log,
|
|
102
|
+
profile=args.profile,
|
|
103
|
+
dry_run=args.dry_run,
|
|
104
|
+
)
|
|
105
|
+
if args.evidence_command == "gate":
|
|
106
|
+
return gate_evidence(
|
|
107
|
+
repo=args.repo,
|
|
108
|
+
base=args.base,
|
|
109
|
+
head=args.head,
|
|
110
|
+
baseline=args.baseline,
|
|
111
|
+
profile=args.profile,
|
|
112
|
+
dry_run=args.dry_run,
|
|
113
|
+
)
|
|
114
|
+
if args.evidence_command == "packet":
|
|
115
|
+
return build_failure_packet(
|
|
116
|
+
input_path=args.input,
|
|
117
|
+
output=args.output,
|
|
118
|
+
profile=args.profile,
|
|
119
|
+
dry_run=args.dry_run,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if args.command == "risk" and args.risk_command == "context":
|
|
123
|
+
return render_risk_context(catalog=args.catalog, output=args.output, dry_run=args.dry_run)
|
|
124
|
+
|
|
125
|
+
if args.command == "lab" and args.lab_command == "evidence-loop":
|
|
126
|
+
return render_evidence_loop_lab(output=args.output, dry_run=args.dry_run)
|
|
127
|
+
|
|
128
|
+
if args.command == "release" and args.release_command == "verify":
|
|
129
|
+
report = verify_release(
|
|
130
|
+
project_root=args.project_root,
|
|
131
|
+
build=args.build,
|
|
132
|
+
install=args.install,
|
|
133
|
+
smoke=args.smoke,
|
|
134
|
+
)
|
|
135
|
+
print_release_report(report, as_json=args.json)
|
|
136
|
+
return 0 if report["ok"] else 1
|
|
137
|
+
|
|
138
|
+
if args.command == "runbook":
|
|
139
|
+
code, report, message = collect_for_runbook(
|
|
140
|
+
repo=args.repo,
|
|
141
|
+
base=args.base,
|
|
142
|
+
head=args.head,
|
|
143
|
+
test_logs=args.test_log,
|
|
144
|
+
profile=args.profile,
|
|
145
|
+
dry_run=args.dry_run,
|
|
146
|
+
)
|
|
147
|
+
if code != 0:
|
|
148
|
+
print(message)
|
|
149
|
+
return code
|
|
150
|
+
if args.dry_run:
|
|
151
|
+
print(message)
|
|
152
|
+
return 0
|
|
153
|
+
payload = {
|
|
154
|
+
"schema_version": "xone.runbook.v1",
|
|
155
|
+
"handoff_decision": (report or {}).get("handoff_decision"),
|
|
156
|
+
}
|
|
157
|
+
if args.json:
|
|
158
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
159
|
+
if args.output and report is not None:
|
|
160
|
+
write_runbook(report, args.output)
|
|
161
|
+
elif report is not None:
|
|
162
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
parser.print_help(sys.stderr)
|
|
166
|
+
return 2
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def entrypoint() -> None:
|
|
170
|
+
raise SystemExit(main())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _print_doctor(report) -> None:
|
|
174
|
+
print("X-One toolchain")
|
|
175
|
+
for tool in report.tools:
|
|
176
|
+
status = "ok" if tool.available else "missing"
|
|
177
|
+
print(f"- {tool.name}: {status}")
|
|
178
|
+
if tool.version:
|
|
179
|
+
print(f" version: {tool.version}")
|
|
180
|
+
if not tool.available:
|
|
181
|
+
print(f" install: {tool.install_hint}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _add_evidence_collection_args(parser: argparse.ArgumentParser) -> None:
|
|
185
|
+
parser.add_argument("--repo", default=".", help="git repository path")
|
|
186
|
+
parser.add_argument("--base", required=True, help="base git ref")
|
|
187
|
+
parser.add_argument("--head", required=True, help="head git ref")
|
|
188
|
+
parser.add_argument("--test-log", action="append", default=[], help="test log path; repeatable")
|
|
189
|
+
parser.add_argument("--profile", choices=("default", "strict"), default="strict")
|
xone_cli/evidence.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from xone_cli.tooling import run_command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def collect_evidence(
|
|
11
|
+
*,
|
|
12
|
+
repo: str,
|
|
13
|
+
base: str,
|
|
14
|
+
head: str,
|
|
15
|
+
output: Path | None,
|
|
16
|
+
test_logs: list[str],
|
|
17
|
+
profile: str,
|
|
18
|
+
dry_run: bool = False,
|
|
19
|
+
) -> int:
|
|
20
|
+
command = [
|
|
21
|
+
"agent-pr-evidence",
|
|
22
|
+
"collect",
|
|
23
|
+
"--repo",
|
|
24
|
+
repo,
|
|
25
|
+
"--base",
|
|
26
|
+
base,
|
|
27
|
+
"--head",
|
|
28
|
+
head,
|
|
29
|
+
"--profile",
|
|
30
|
+
profile,
|
|
31
|
+
"--format",
|
|
32
|
+
"json",
|
|
33
|
+
]
|
|
34
|
+
for test_log in test_logs:
|
|
35
|
+
command.extend(["--test-log", test_log])
|
|
36
|
+
if output:
|
|
37
|
+
command.extend(["--output", str(output)])
|
|
38
|
+
result = run_command(command, dry_run=dry_run)
|
|
39
|
+
print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
|
|
40
|
+
if result.stderr:
|
|
41
|
+
print(result.stderr, end="" if result.stderr.endswith("\n") else "\n")
|
|
42
|
+
return result.returncode
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def gate_evidence(
|
|
46
|
+
*,
|
|
47
|
+
repo: str,
|
|
48
|
+
base: str,
|
|
49
|
+
head: str,
|
|
50
|
+
baseline: Path,
|
|
51
|
+
profile: str,
|
|
52
|
+
dry_run: bool = False,
|
|
53
|
+
) -> int:
|
|
54
|
+
command = [
|
|
55
|
+
"agent-pr-evidence",
|
|
56
|
+
"gate",
|
|
57
|
+
"--repo",
|
|
58
|
+
repo,
|
|
59
|
+
"--base",
|
|
60
|
+
base,
|
|
61
|
+
"--head",
|
|
62
|
+
head,
|
|
63
|
+
"--profile",
|
|
64
|
+
profile,
|
|
65
|
+
"--baseline",
|
|
66
|
+
str(baseline),
|
|
67
|
+
"--format",
|
|
68
|
+
"json",
|
|
69
|
+
]
|
|
70
|
+
result = run_command(command, dry_run=dry_run)
|
|
71
|
+
print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
|
|
72
|
+
if result.stderr:
|
|
73
|
+
print(result.stderr, end="" if result.stderr.endswith("\n") else "\n")
|
|
74
|
+
return result.returncode
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_failure_packet(*, input_path: Path, output: Path, profile: str, dry_run: bool = False) -> int:
|
|
78
|
+
validate = run_command(["agent-failure-packet", "validate", "--input", str(input_path)], dry_run=dry_run)
|
|
79
|
+
print(validate.stdout, end="" if validate.stdout.endswith("\n") else "\n")
|
|
80
|
+
if validate.stderr:
|
|
81
|
+
print(validate.stderr, end="" if validate.stderr.endswith("\n") else "\n")
|
|
82
|
+
if validate.returncode != 0:
|
|
83
|
+
return validate.returncode
|
|
84
|
+
|
|
85
|
+
build = run_command(
|
|
86
|
+
[
|
|
87
|
+
"agent-failure-packet",
|
|
88
|
+
"build",
|
|
89
|
+
"--input",
|
|
90
|
+
str(input_path),
|
|
91
|
+
"--profile",
|
|
92
|
+
profile,
|
|
93
|
+
"--output",
|
|
94
|
+
str(output),
|
|
95
|
+
],
|
|
96
|
+
dry_run=dry_run,
|
|
97
|
+
)
|
|
98
|
+
print(build.stdout, end="" if build.stdout.endswith("\n") else "\n")
|
|
99
|
+
if build.stderr:
|
|
100
|
+
print(build.stderr, end="" if build.stderr.endswith("\n") else "\n")
|
|
101
|
+
return build.returncode
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def collect_for_runbook(
|
|
105
|
+
*,
|
|
106
|
+
repo: str,
|
|
107
|
+
base: str,
|
|
108
|
+
head: str,
|
|
109
|
+
test_logs: list[str],
|
|
110
|
+
profile: str,
|
|
111
|
+
dry_run: bool,
|
|
112
|
+
) -> tuple[int, dict | None, str]:
|
|
113
|
+
command = [
|
|
114
|
+
"agent-pr-evidence",
|
|
115
|
+
"collect",
|
|
116
|
+
"--repo",
|
|
117
|
+
repo,
|
|
118
|
+
"--base",
|
|
119
|
+
base,
|
|
120
|
+
"--head",
|
|
121
|
+
head,
|
|
122
|
+
"--profile",
|
|
123
|
+
profile,
|
|
124
|
+
"--format",
|
|
125
|
+
"json",
|
|
126
|
+
]
|
|
127
|
+
for test_log in test_logs:
|
|
128
|
+
command.extend(["--test-log", test_log])
|
|
129
|
+
if dry_run:
|
|
130
|
+
result = run_command(command, dry_run=True)
|
|
131
|
+
return result.returncode, None, result.stdout
|
|
132
|
+
with tempfile.TemporaryDirectory(prefix="xone-runbook-") as tmp:
|
|
133
|
+
output = Path(tmp) / "evidence.json"
|
|
134
|
+
result = run_command([*command, "--output", str(output)])
|
|
135
|
+
if result.returncode != 0:
|
|
136
|
+
return result.returncode, None, result.stderr or result.stdout
|
|
137
|
+
return 0, json.loads(output.read_text(encoding="utf-8")), ""
|
|
138
|
+
|
xone_cli/lab.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from xone_cli.tooling import run_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_evidence_loop_lab(*, output: Path, dry_run: bool = False) -> int:
|
|
10
|
+
with tempfile.TemporaryDirectory(prefix="xone-lab-") as tmp:
|
|
11
|
+
scenario_dir = Path(tmp) / "scenarios"
|
|
12
|
+
init = run_command(["ai-incident-lab", "init", "--output", str(scenario_dir)], dry_run=dry_run)
|
|
13
|
+
print(init.stdout, end="" if init.stdout.endswith("\n") else "\n")
|
|
14
|
+
if init.stderr:
|
|
15
|
+
print(init.stderr, end="" if init.stderr.endswith("\n") else "\n")
|
|
16
|
+
if init.returncode != 0:
|
|
17
|
+
return init.returncode
|
|
18
|
+
|
|
19
|
+
scenario = scenario_dir / "agent-evidence-loop.yml"
|
|
20
|
+
validate = run_command(["ai-incident-lab", "validate", "--scenarios", str(scenario_dir)], dry_run=dry_run)
|
|
21
|
+
print(validate.stdout, end="" if validate.stdout.endswith("\n") else "\n")
|
|
22
|
+
if validate.stderr:
|
|
23
|
+
print(validate.stderr, end="" if validate.stderr.endswith("\n") else "\n")
|
|
24
|
+
if validate.returncode != 0:
|
|
25
|
+
return validate.returncode
|
|
26
|
+
|
|
27
|
+
render = run_command(
|
|
28
|
+
["ai-incident-lab", "render", "--scenarios", str(scenario), "--format", "markdown"],
|
|
29
|
+
dry_run=dry_run,
|
|
30
|
+
)
|
|
31
|
+
if render.returncode == 0 and not dry_run:
|
|
32
|
+
output.write_text(render.stdout, encoding="utf-8")
|
|
33
|
+
else:
|
|
34
|
+
print(render.stdout, end="" if render.stdout.endswith("\n") else "\n")
|
|
35
|
+
if render.stderr:
|
|
36
|
+
print(render.stderr, end="" if render.stderr.endswith("\n") else "\n")
|
|
37
|
+
return render.returncode
|
|
38
|
+
|
xone_cli/model.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class CommandResult:
|
|
8
|
+
command: list[str]
|
|
9
|
+
returncode: int
|
|
10
|
+
stdout: str
|
|
11
|
+
stderr: str
|
|
12
|
+
dry_run: bool = False
|
|
13
|
+
|
|
14
|
+
def to_dict(self) -> dict:
|
|
15
|
+
return asdict(self)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ToolStatus:
|
|
20
|
+
name: str
|
|
21
|
+
executable: str | None
|
|
22
|
+
available: bool
|
|
23
|
+
version: str | None
|
|
24
|
+
install_hint: str
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict:
|
|
27
|
+
return asdict(self)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class DoctorReport:
|
|
32
|
+
schema_version: str
|
|
33
|
+
tools: list[ToolStatus]
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def ok(self) -> bool:
|
|
37
|
+
return all(tool.available for tool in self.tools)
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
data = asdict(self)
|
|
41
|
+
data["ok"] = self.ok
|
|
42
|
+
return data
|
|
43
|
+
|
xone_cli/release.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from xone_cli.tooling import run_command
|
|
9
|
+
|
|
10
|
+
RELEASE_VERIFY_SCHEMA_VERSION = "xone.release.verify.v1"
|
|
11
|
+
|
|
12
|
+
REQUIRED_RELEASE_FILES = (
|
|
13
|
+
"pyproject.toml",
|
|
14
|
+
"README.md",
|
|
15
|
+
"README.zh-CN.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
".github/workflows/ci.yml",
|
|
19
|
+
".github/workflows/publish.yml",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def required_files_status(project_root: Path) -> dict[str, bool]:
|
|
24
|
+
return {name: (project_root / name).exists() for name in REQUIRED_RELEASE_FILES}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def verify_release(
|
|
28
|
+
*,
|
|
29
|
+
project_root: Path,
|
|
30
|
+
build: bool,
|
|
31
|
+
install: bool,
|
|
32
|
+
smoke: bool,
|
|
33
|
+
) -> dict:
|
|
34
|
+
project_root = project_root.resolve()
|
|
35
|
+
required_files = required_files_status(project_root)
|
|
36
|
+
checks: list[dict] = []
|
|
37
|
+
release_env = _release_subprocess_env()
|
|
38
|
+
smoke_commands: list[list[str]] = [
|
|
39
|
+
["python", "-m", "xone_cli", "--version"],
|
|
40
|
+
["python", "-m", "xone_cli", "doctor", "--json"],
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
if build:
|
|
44
|
+
result = run_command(["python", "-m", "build"], dry_run=False, cwd=project_root, env=release_env)
|
|
45
|
+
checks.append(_check("build", result.returncode, result.stdout, result.stderr))
|
|
46
|
+
|
|
47
|
+
if install:
|
|
48
|
+
with tempfile.TemporaryDirectory(prefix="xone-release-venv-") as tmp:
|
|
49
|
+
venv_dir = Path(tmp) / "venv"
|
|
50
|
+
venv = run_command(["python", "-m", "venv", str(venv_dir)])
|
|
51
|
+
checks.append(_check("venv", venv.returncode, venv.stdout, venv.stderr))
|
|
52
|
+
if venv.returncode == 0:
|
|
53
|
+
wheel = _latest_wheel(project_root)
|
|
54
|
+
if wheel:
|
|
55
|
+
pip = venv_dir / "bin" / "pip"
|
|
56
|
+
installed = run_command([str(pip), "install", str(wheel)], env=release_env)
|
|
57
|
+
checks.append(_check("wheel-install", installed.returncode, installed.stdout, installed.stderr))
|
|
58
|
+
xone = venv_dir / "bin" / "xone"
|
|
59
|
+
smoke_commands = [
|
|
60
|
+
[str(xone), "--version"],
|
|
61
|
+
[str(xone), "doctor", "--json"],
|
|
62
|
+
]
|
|
63
|
+
else:
|
|
64
|
+
checks.append({"name": "wheel-install", "returncode": 1, "stdout": "", "stderr": "No wheel found in dist/"})
|
|
65
|
+
|
|
66
|
+
if smoke:
|
|
67
|
+
for name, command in (("cli-version", smoke_commands[0]), ("doctor-json", smoke_commands[1])):
|
|
68
|
+
result = run_command(command, cwd=project_root, env=release_env)
|
|
69
|
+
checks.append(_check(name, result.returncode, result.stdout, result.stderr))
|
|
70
|
+
elif smoke:
|
|
71
|
+
for name, command in (("cli-version", smoke_commands[0]), ("doctor-json", smoke_commands[1])):
|
|
72
|
+
result = run_command(command, cwd=project_root, env=release_env)
|
|
73
|
+
checks.append(_check(name, result.returncode, result.stdout, result.stderr))
|
|
74
|
+
|
|
75
|
+
ok = all(required_files.values()) and all(check["returncode"] == 0 for check in checks)
|
|
76
|
+
return {
|
|
77
|
+
"schema_version": RELEASE_VERIFY_SCHEMA_VERSION,
|
|
78
|
+
"ok": ok,
|
|
79
|
+
"required_files": required_files,
|
|
80
|
+
"checks": checks,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_release_report(report: dict, *, as_json: bool) -> None:
|
|
85
|
+
if as_json:
|
|
86
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
87
|
+
return
|
|
88
|
+
print("X-One release verification")
|
|
89
|
+
print(f"ok: {str(report['ok']).lower()}")
|
|
90
|
+
print("required files:")
|
|
91
|
+
for name, exists in report["required_files"].items():
|
|
92
|
+
print(f"- {name}: {'ok' if exists else 'missing'}")
|
|
93
|
+
if report["checks"]:
|
|
94
|
+
print("checks:")
|
|
95
|
+
for check in report["checks"]:
|
|
96
|
+
print(f"- {check['name']}: {'ok' if check['returncode'] == 0 else 'failed'}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _check(name: str, returncode: int, stdout: str, stderr: str) -> dict:
|
|
100
|
+
return {"name": name, "returncode": returncode, "stdout": stdout[-2000:], "stderr": stderr[-2000:]}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _latest_wheel(project_root: Path) -> Path | None:
|
|
104
|
+
wheels = sorted((project_root / "dist").glob("*.whl"), key=lambda path: path.stat().st_mtime, reverse=True)
|
|
105
|
+
return wheels[0] if wheels else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _release_subprocess_env() -> dict[str, str]:
|
|
109
|
+
env = dict(os.environ)
|
|
110
|
+
env.pop("PYTHONPATH", None)
|
|
111
|
+
return env
|
xone_cli/risk.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from xone_cli.tooling import run_command
|
|
6
|
+
|
|
7
|
+
RISK_CONTEXT_BOUNDARY = "This is review context, not an allow/deny decision."
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_risk_context(*, catalog: Path, output: Path, dry_run: bool = False) -> int:
|
|
11
|
+
validate = run_command(["mcp-risk-index", "validate", "--catalog", str(catalog), "--strict"], dry_run=dry_run)
|
|
12
|
+
print(validate.stdout, end="" if validate.stdout.endswith("\n") else "\n")
|
|
13
|
+
if validate.stderr:
|
|
14
|
+
print(validate.stderr, end="" if validate.stderr.endswith("\n") else "\n")
|
|
15
|
+
if validate.returncode != 0:
|
|
16
|
+
return validate.returncode
|
|
17
|
+
|
|
18
|
+
rendered = run_command(["mcp-risk-index", "render", "--catalog", str(catalog), "--format", "markdown"], dry_run=dry_run)
|
|
19
|
+
if rendered.returncode == 0 and not dry_run:
|
|
20
|
+
output.write_text(f"> {RISK_CONTEXT_BOUNDARY}\n\n{rendered.stdout}", encoding="utf-8")
|
|
21
|
+
else:
|
|
22
|
+
print(rendered.stdout, end="" if rendered.stdout.endswith("\n") else "\n")
|
|
23
|
+
if rendered.stderr:
|
|
24
|
+
print(rendered.stderr, end="" if rendered.stderr.endswith("\n") else "\n")
|
|
25
|
+
return rendered.returncode
|
|
26
|
+
|
xone_cli/runbook.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def render_runbook(report: dict) -> str:
|
|
8
|
+
decision = report.get("handoff_decision") or {}
|
|
9
|
+
lines = [
|
|
10
|
+
"# X-One Evidence Runbook",
|
|
11
|
+
"",
|
|
12
|
+
"## Handoff Decision",
|
|
13
|
+
"",
|
|
14
|
+
f"- Decision: `{decision.get('decision', 'unknown')}`",
|
|
15
|
+
f"- Reason: {decision.get('reason', 'not provided')}",
|
|
16
|
+
f"- Evidence source: {decision.get('evidence_source', 'not provided')}",
|
|
17
|
+
f"- Handoff target: {decision.get('handoff_target', 'not provided')}",
|
|
18
|
+
f"- Revisit trigger: {decision.get('revisit_trigger', 'not provided')}",
|
|
19
|
+
"",
|
|
20
|
+
"## Next Action",
|
|
21
|
+
"",
|
|
22
|
+
_next_action(decision.get("decision")),
|
|
23
|
+
"",
|
|
24
|
+
"## Raw Summary",
|
|
25
|
+
"",
|
|
26
|
+
"```json",
|
|
27
|
+
json.dumps(report.get("summary", {}), indent=2, sort_keys=True),
|
|
28
|
+
"```",
|
|
29
|
+
"",
|
|
30
|
+
]
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_runbook(report: dict, output: Path) -> None:
|
|
35
|
+
output.write_text(render_runbook(report), encoding="utf-8")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _next_action(decision: str | None) -> str:
|
|
39
|
+
if decision == "create-failure-packet":
|
|
40
|
+
return "- Build a redacted failure packet with `xone evidence packet`."
|
|
41
|
+
if decision == "block-before-merge":
|
|
42
|
+
return "- Add passing tests, an accountable owner, or a remediation plan before merge."
|
|
43
|
+
if decision == "baseline-review":
|
|
44
|
+
return "- Review the baseline before accepting new or existing risk."
|
|
45
|
+
if decision == "request-test-evidence":
|
|
46
|
+
return "- Provide test logs or explain why tests are not required."
|
|
47
|
+
return "- Continue normal review."
|
|
48
|
+
|
xone_cli/tooling.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from xone_cli.model import CommandResult, DoctorReport, ToolStatus
|
|
10
|
+
|
|
11
|
+
DOCTOR_SCHEMA_VERSION = "xone.doctor.v1"
|
|
12
|
+
|
|
13
|
+
REQUIRED_TOOLS = (
|
|
14
|
+
"agent-pr-evidence",
|
|
15
|
+
"agent-failure-packet",
|
|
16
|
+
"mcp-risk-index",
|
|
17
|
+
"ai-incident-lab",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
PACKAGE_BY_TOOL = {
|
|
21
|
+
"agent-pr-evidence": "xone-agent-pr-evidence",
|
|
22
|
+
"agent-failure-packet": "xone-agent-failure-packet",
|
|
23
|
+
"mcp-risk-index": "xone-mcp-risk-index",
|
|
24
|
+
"ai-incident-lab": "xone-ai-incident-lab",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_tool(name: str) -> str | None:
|
|
29
|
+
return shutil.which(name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def install_hint(name: str) -> str:
|
|
33
|
+
package = PACKAGE_BY_TOOL.get(name, name)
|
|
34
|
+
return f"python -m pip install {package}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_command(
|
|
38
|
+
command: Sequence[str],
|
|
39
|
+
*,
|
|
40
|
+
dry_run: bool = False,
|
|
41
|
+
cwd: Path | None = None,
|
|
42
|
+
env: Mapping[str, str] | None = None,
|
|
43
|
+
) -> CommandResult:
|
|
44
|
+
command_list = [str(part) for part in command]
|
|
45
|
+
if dry_run:
|
|
46
|
+
return CommandResult(
|
|
47
|
+
command=command_list,
|
|
48
|
+
returncode=0,
|
|
49
|
+
stdout=f"DRY RUN: {_format_command(command_list)}",
|
|
50
|
+
stderr="",
|
|
51
|
+
dry_run=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
completed = subprocess.run(
|
|
56
|
+
command_list,
|
|
57
|
+
check=False,
|
|
58
|
+
text=True,
|
|
59
|
+
stdout=subprocess.PIPE,
|
|
60
|
+
stderr=subprocess.PIPE,
|
|
61
|
+
cwd=cwd,
|
|
62
|
+
env=dict(env) if env is not None else None,
|
|
63
|
+
)
|
|
64
|
+
except FileNotFoundError as error:
|
|
65
|
+
return CommandResult(
|
|
66
|
+
command=command_list,
|
|
67
|
+
returncode=127,
|
|
68
|
+
stdout="",
|
|
69
|
+
stderr=str(error),
|
|
70
|
+
)
|
|
71
|
+
return CommandResult(
|
|
72
|
+
command=command_list,
|
|
73
|
+
returncode=completed.returncode,
|
|
74
|
+
stdout=completed.stdout,
|
|
75
|
+
stderr=completed.stderr,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def doctor_status() -> DoctorReport:
|
|
80
|
+
tools = [_tool_status(name) for name in REQUIRED_TOOLS]
|
|
81
|
+
return DoctorReport(schema_version=DOCTOR_SCHEMA_VERSION, tools=tools)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _tool_status(name: str) -> ToolStatus:
|
|
85
|
+
executable = find_tool(name)
|
|
86
|
+
if not executable:
|
|
87
|
+
return ToolStatus(
|
|
88
|
+
name=name,
|
|
89
|
+
executable=None,
|
|
90
|
+
available=False,
|
|
91
|
+
version=None,
|
|
92
|
+
install_hint=install_hint(name),
|
|
93
|
+
)
|
|
94
|
+
version_result = run_command([name, "--version"])
|
|
95
|
+
version = version_result.stdout.strip() if version_result.returncode == 0 else None
|
|
96
|
+
return ToolStatus(
|
|
97
|
+
name=name,
|
|
98
|
+
executable=executable,
|
|
99
|
+
available=True,
|
|
100
|
+
version=version,
|
|
101
|
+
install_hint=install_hint(name),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _format_command(command: Sequence[str]) -> str:
|
|
106
|
+
return " ".join(shlex.quote(part) for part in command)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xone-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified CLI entry point for X-One Agent Evidence Loop workflows.
|
|
5
|
+
Author: X-One-AI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/X-One-AI/xone-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/X-One-AI/xone-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/X-One-AI/xone-cli/issues
|
|
10
|
+
Keywords: ai,agents,devsecops,mcp,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# xone-cli
|
|
29
|
+
|
|
30
|
+
Languages: English | [中文](./README.zh-CN.md)
|
|
31
|
+
|
|
32
|
+
`xone-cli` is the unified local entry point for X-One Agent Evidence Loop workflows.
|
|
33
|
+
|
|
34
|
+
It helps developers move from scattered tools to one clear path:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
collect PR evidence
|
|
38
|
+
-> decide handoff
|
|
39
|
+
-> create failure packet when needed
|
|
40
|
+
-> attach MCP risk context when useful
|
|
41
|
+
-> run safe-local training scenarios
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m pip install xone-cli
|
|
48
|
+
xone --version
|
|
49
|
+
xone doctor
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## First Run
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
xone doctor
|
|
56
|
+
xone runbook --base main --head HEAD --dry-run
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`xone-cli` orchestrates these X-One tools:
|
|
60
|
+
|
|
61
|
+
- `agent-pr-evidence`
|
|
62
|
+
- `agent-failure-packet`
|
|
63
|
+
- `mcp-risk-index`
|
|
64
|
+
- `ai-incident-lab`
|
|
65
|
+
|
|
66
|
+
## Boundary
|
|
67
|
+
|
|
68
|
+
- It does not replace the underlying tools.
|
|
69
|
+
- It does not post GitHub comments.
|
|
70
|
+
- It does not modify repositories automatically.
|
|
71
|
+
- It does not make allow/deny runtime enforcement decisions.
|
|
72
|
+
- It does not make hidden network calls.
|
|
73
|
+
|
|
74
|
+
## Docs
|
|
75
|
+
|
|
76
|
+
- [Product Foundation](./docs/product-foundation.md)
|
|
77
|
+
- [Install](./docs/install.md)
|
|
78
|
+
- [Release](./docs/release.md)
|
|
79
|
+
- [Open-source Feedback Ledger](./docs/feedback/open-source-feedback-ledger.md)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
xone_cli/__init__.py,sha256=YosWCpCE15wFKb9PYPbECNR0HI0P7moX8U0yX319veg,59
|
|
2
|
+
xone_cli/__main__.py,sha256=J4Wq8cwjCVitF0nyuoNwPmGAthKhoaPiVobP6l3_e-w,119
|
|
3
|
+
xone_cli/cli.py,sha256=gek02J0PgTRYdnHOSdhCf5veIHzBTP4j73VFnFS1q1w,7760
|
|
4
|
+
xone_cli/evidence.py,sha256=9Dbzb-GTVxaf2RVrKgLwvlI8S9qeEaXJoW2LfmB4bIA,3571
|
|
5
|
+
xone_cli/lab.py,sha256=8KU7Pti_1Co9NY5wmR1zHcEgzOGMalA2zWu_3EWTtrk,1631
|
|
6
|
+
xone_cli/model.py,sha256=XRm1-8n8HXCkfj8Nx7vDDpKnCGDF11MlMO1_Zem2J9o,804
|
|
7
|
+
xone_cli/release.py,sha256=RcbjD-ydgOhzQfwagU1A3MqAtBOb9hFooEmMeJuBViY,4186
|
|
8
|
+
xone_cli/risk.py,sha256=-XV6drlXUwrolpfU5lxxsJTpu3y5BbcebWNRpgoERXI,1142
|
|
9
|
+
xone_cli/runbook.py,sha256=twUO0yjF4K8vpJA_dBMSa1HvF9-_vuzXDuInjtTpbBY,1649
|
|
10
|
+
xone_cli/tooling.py,sha256=c2BRssDHRWFQaX2jvHd5yDLXNcrsj4nbEShtEtV5Les,2790
|
|
11
|
+
xone_cli-0.1.0.dist-info/licenses/LICENSE,sha256=2AV3_k1rPe0sVkPEDXEeDKTvt3wS9rQm0Yu7wcRBCVw,1066
|
|
12
|
+
xone_cli-0.1.0.dist-info/METADATA,sha256=5YVycYbFK3AVZh8OoMIaEx1i2UHZO788W7mdEDdCoFs,2157
|
|
13
|
+
xone_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
xone_cli-0.1.0.dist-info/entry_points.txt,sha256=hH0QkGff99qEymxv5xd8lJE9zoXjVf7qsixgX4yMPMA,49
|
|
15
|
+
xone_cli-0.1.0.dist-info/top_level.txt,sha256=Q8zVoHnDEJIJHvSd8tvff5HWUXeQgqfq7o3SA7Ir-lk,9
|
|
16
|
+
xone_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 X-One-AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xone_cli
|