devtorch-cli 3.0.1__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.
- devtorch_cli/__init__.py +1 -0
- devtorch_cli/__main__.py +6 -0
- devtorch_cli/audit_cmds.py +221 -0
- devtorch_cli/capability_role_cmds.py +94 -0
- devtorch_cli/main.py +4090 -0
- devtorch_cli/pr_reporter_cmd.py +41 -0
- devtorch_cli/pre_commit_cmds.py +40 -0
- devtorch_cli/session_cmds.py +282 -0
- devtorch_cli-3.0.1.dist-info/METADATA +60 -0
- devtorch_cli-3.0.1.dist-info/RECORD +13 -0
- devtorch_cli-3.0.1.dist-info/WHEEL +5 -0
- devtorch_cli-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_cli-3.0.1.dist-info/top_level.txt +1 -0
devtorch_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
devtorch_cli/__main__.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""CLI commands for Sprint 30: trust, compliance, and security audit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from devtorch_core.audit import (
|
|
12
|
+
AuditFilters,
|
|
13
|
+
AuditService,
|
|
14
|
+
PrivacyConfig,
|
|
15
|
+
ExportSigner,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _gcc_dir_from_args(args: argparse.Namespace) -> Path:
|
|
20
|
+
return Path(args.path).resolve() / ".GCC"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_iso(value: str | None) -> datetime.datetime | None:
|
|
24
|
+
if not value:
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
return datetime.datetime.fromisoformat(value)
|
|
28
|
+
except ValueError:
|
|
29
|
+
# Allow bare dates (YYYY-MM-DD).
|
|
30
|
+
try:
|
|
31
|
+
return datetime.datetime.strptime(value, "%Y-%m-%d")
|
|
32
|
+
except ValueError:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cmd_audit_export(args: argparse.Namespace) -> int:
|
|
37
|
+
gcc_dir = _gcc_dir_from_args(args)
|
|
38
|
+
if not gcc_dir.exists():
|
|
39
|
+
print(f"error: {gcc_dir} not found. Run `devtorch init` first.", file=sys.stderr)
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
service = AuditService(gcc_dir)
|
|
43
|
+
privacy = service.get_privacy_config()
|
|
44
|
+
|
|
45
|
+
if args.no_redaction:
|
|
46
|
+
privacy.redaction_enabled = False
|
|
47
|
+
|
|
48
|
+
filters = AuditFilters(
|
|
49
|
+
since=_parse_iso(args.since),
|
|
50
|
+
until=_parse_iso(args.until),
|
|
51
|
+
project=args.project,
|
|
52
|
+
workspace=args.workspace,
|
|
53
|
+
user=args.user,
|
|
54
|
+
event_type=args.event_type,
|
|
55
|
+
actor=args.actor,
|
|
56
|
+
limit=args.limit,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
output_path = Path(args.output).resolve()
|
|
60
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
service = AuditService(gcc_dir)
|
|
63
|
+
result = service.export(
|
|
64
|
+
output_path,
|
|
65
|
+
fmt=args.format,
|
|
66
|
+
filters=filters,
|
|
67
|
+
privacy_config=privacy,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(f"Exported {result['event_count']} event(s) to {result['output_path']} ({result['format']}).")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cmd_audit_sign(args: argparse.Namespace) -> int:
|
|
75
|
+
input_path = Path(args.input).resolve()
|
|
76
|
+
output_path = Path(args.output).resolve()
|
|
77
|
+
key_path = Path(args.key).resolve()
|
|
78
|
+
|
|
79
|
+
if not input_path.exists():
|
|
80
|
+
print(f"error: input file not found: {input_path}", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
if not key_path.exists():
|
|
83
|
+
print(f"error: private key not found: {key_path}", file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
signer = ExportSigner()
|
|
87
|
+
signer.sign_file(input_path, output_path, key_path)
|
|
88
|
+
print(f"Signed envelope written to {output_path}")
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def cmd_audit_verify(args: argparse.Namespace) -> int:
|
|
93
|
+
envelope_path = Path(args.input).resolve()
|
|
94
|
+
key_path = Path(args.key).resolve()
|
|
95
|
+
|
|
96
|
+
if not envelope_path.exists():
|
|
97
|
+
print(f"error: envelope file not found: {envelope_path}", file=sys.stderr)
|
|
98
|
+
return 1
|
|
99
|
+
if not key_path.exists():
|
|
100
|
+
print(f"error: public key not found: {key_path}", file=sys.stderr)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
signer = ExportSigner()
|
|
104
|
+
if signer.verify_file(envelope_path, key_path):
|
|
105
|
+
print("Signature verification: OK")
|
|
106
|
+
return 0
|
|
107
|
+
print("Signature verification: FAILED")
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _coerce_bool(value):
|
|
112
|
+
if isinstance(value, bool):
|
|
113
|
+
return value
|
|
114
|
+
if isinstance(value, str):
|
|
115
|
+
return value.lower() in ("true", "1", "yes", "on")
|
|
116
|
+
return bool(value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cmd_audit_privacy(args: argparse.Namespace) -> int:
|
|
120
|
+
gcc_dir = _gcc_dir_from_args(args)
|
|
121
|
+
if not gcc_dir.exists():
|
|
122
|
+
print(f"error: {gcc_dir} not found. Run `devtorch init` first.", file=sys.stderr)
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
service = AuditService(gcc_dir)
|
|
126
|
+
config = service.get_privacy_config()
|
|
127
|
+
|
|
128
|
+
updated = False
|
|
129
|
+
if args.telemetry is not None:
|
|
130
|
+
config.telemetry_enabled = _coerce_bool(args.telemetry)
|
|
131
|
+
updated = True
|
|
132
|
+
if args.prompt_telemetry is not None:
|
|
133
|
+
config.prompt_telemetry_enabled = _coerce_bool(args.prompt_telemetry)
|
|
134
|
+
updated = True
|
|
135
|
+
if args.redaction is not None:
|
|
136
|
+
config.redaction_enabled = _coerce_bool(args.redaction)
|
|
137
|
+
updated = True
|
|
138
|
+
if args.remote_endpoints is not None:
|
|
139
|
+
config.allow_remote_endpoints = _coerce_bool(args.remote_endpoints)
|
|
140
|
+
updated = True
|
|
141
|
+
if args.retention_days is not None:
|
|
142
|
+
config.retention_days = int(args.retention_days)
|
|
143
|
+
updated = True
|
|
144
|
+
|
|
145
|
+
if updated:
|
|
146
|
+
from devtorch_core.audit import save_privacy_config
|
|
147
|
+
save_privacy_config(gcc_dir, config)
|
|
148
|
+
print("Privacy settings updated.")
|
|
149
|
+
|
|
150
|
+
print(json.dumps(config.to_dict(), indent=2))
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cmd_audit_keygen(args: argparse.Namespace) -> int:
|
|
155
|
+
"""Generate an Ed25519 key pair for signing audit exports."""
|
|
156
|
+
output_dir = Path(args.output_dir).resolve()
|
|
157
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
private_path = output_dir / args.private_key
|
|
159
|
+
public_path = output_dir / args.public_key
|
|
160
|
+
|
|
161
|
+
signer = ExportSigner()
|
|
162
|
+
private_pem, public_pem = signer.generate_keypair()
|
|
163
|
+
signer.save_key(private_path, private_pem)
|
|
164
|
+
signer.save_key(public_path, public_pem)
|
|
165
|
+
|
|
166
|
+
print(f"Generated signing key pair:")
|
|
167
|
+
print(f" Private key: {private_path}")
|
|
168
|
+
print(f" Public key: {public_path}")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_audit_parser(subparsers):
|
|
173
|
+
audit_parser = subparsers.add_parser("audit", help="Trust, compliance, and security audit tools.")
|
|
174
|
+
audit_sub = audit_parser.add_subparsers(dest="audit_command", required=True)
|
|
175
|
+
|
|
176
|
+
p_export = audit_sub.add_parser("export", help="Export audit events to JSON or PDF.")
|
|
177
|
+
p_export.add_argument("--format", choices=["json", "pdf"], required=True, help="Export format.")
|
|
178
|
+
p_export.add_argument("--output", required=True, help="Output file path.")
|
|
179
|
+
p_export.add_argument("--since", default=None, help="Include events at or after this ISO timestamp.")
|
|
180
|
+
p_export.add_argument("--until", default=None, help="Include events before this ISO timestamp.")
|
|
181
|
+
p_export.add_argument("--project", default=None, help="Filter by payload.project.")
|
|
182
|
+
p_export.add_argument("--workspace", default=None, help="Filter by payload.workspace.")
|
|
183
|
+
p_export.add_argument("--user", default=None, help="Filter by payload.user.")
|
|
184
|
+
p_export.add_argument("--event-type", default=None, help="Filter by event_type.")
|
|
185
|
+
p_export.add_argument("--actor", default=None, help="Filter by actor.")
|
|
186
|
+
p_export.add_argument("--limit", type=int, default=None, help="Maximum number of events to include.")
|
|
187
|
+
p_export.add_argument("--no-redaction", action="store_true", help="Disable PII redaction for this export.")
|
|
188
|
+
p_export.set_defaults(func=cmd_audit_export)
|
|
189
|
+
|
|
190
|
+
p_sign = audit_sub.add_parser("sign", help="Sign an exported JSON audit report.")
|
|
191
|
+
p_sign.add_argument("input", help="Path to exported JSON file.")
|
|
192
|
+
p_sign.add_argument("--output", required=True, help="Path to write signed envelope.")
|
|
193
|
+
p_sign.add_argument("--key", required=True, help="Path to Ed25519 private key (PEM).")
|
|
194
|
+
p_sign.set_defaults(func=cmd_audit_sign)
|
|
195
|
+
|
|
196
|
+
p_verify = audit_sub.add_parser("verify", help="Verify a signed audit report envelope.")
|
|
197
|
+
p_verify.add_argument("input", help="Path to signed envelope.")
|
|
198
|
+
p_verify.add_argument("--key", required=True, help="Path to Ed25519 public key (PEM).")
|
|
199
|
+
p_verify.set_defaults(func=cmd_audit_verify)
|
|
200
|
+
|
|
201
|
+
p_privacy = audit_sub.add_parser("privacy", help="Show or update privacy policy settings.")
|
|
202
|
+
p_privacy.add_argument("--telemetry", type=_str_to_bool, default=None, help="Enable/disable telemetry.")
|
|
203
|
+
p_privacy.add_argument("--prompt-telemetry", type=_str_to_bool, default=None, dest="prompt_telemetry", help="Enable/disable prompt telemetry.")
|
|
204
|
+
p_privacy.add_argument("--redaction", type=_str_to_bool, default=None, help="Enable/disable PII redaction.")
|
|
205
|
+
p_privacy.add_argument("--remote-endpoints", type=_str_to_bool, default=None, dest="remote_endpoints", help="Allow remote endpoints.")
|
|
206
|
+
p_privacy.add_argument("--retention-days", type=int, default=None, dest="retention_days", help="Data retention period in days.")
|
|
207
|
+
p_privacy.set_defaults(func=cmd_audit_privacy)
|
|
208
|
+
|
|
209
|
+
p_keygen = audit_sub.add_parser("keygen", help="Generate an Ed25519 key pair for signing exports.")
|
|
210
|
+
p_keygen.add_argument("--output-dir", default=".", dest="output_dir", help="Directory for key files.")
|
|
211
|
+
p_keygen.add_argument("--private-key", default="audit_private.pem", dest="private_key", help="Private key filename.")
|
|
212
|
+
p_keygen.add_argument("--public-key", default="audit_public.pem", dest="public_key", help="Public key filename.")
|
|
213
|
+
p_keygen.set_defaults(func=cmd_audit_keygen)
|
|
214
|
+
|
|
215
|
+
return audit_parser
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _str_to_bool(value: str | None) -> bool | None:
|
|
219
|
+
if value is None:
|
|
220
|
+
return None
|
|
221
|
+
return value.lower() in ("true", "1", "yes", "on")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""S28 CLI handlers for `devtorch capability role`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from devtorch_core.capability import (
|
|
11
|
+
AgentRole,
|
|
12
|
+
CapabilityStore,
|
|
13
|
+
apply_default_policy,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _store(args: argparse.Namespace) -> CapabilityStore:
|
|
18
|
+
gcc_dir = Path(args.path).resolve() / ".GCC"
|
|
19
|
+
return CapabilityStore(gcc_dir / "capabilities")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _split_csv(value: str | None) -> List[str]:
|
|
23
|
+
if not value:
|
|
24
|
+
return []
|
|
25
|
+
return [p.strip() for p in value.split(",") if p.strip()]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_capability_role_list(args: argparse.Namespace) -> int:
|
|
29
|
+
store = _store(args)
|
|
30
|
+
role_ids = store.list_roles()
|
|
31
|
+
if not role_ids:
|
|
32
|
+
print("No roles defined.")
|
|
33
|
+
return 0
|
|
34
|
+
roles = [r for r in (store.load_role(rid) for rid in role_ids) if r is not None]
|
|
35
|
+
print(f"{'ID':<16} {'Name':<20} {'Allowed':<30} {'Disallowed'}")
|
|
36
|
+
print("-" * 80)
|
|
37
|
+
for r in roles:
|
|
38
|
+
allowed = ", ".join(r.allowed_concepts) or "(any)"
|
|
39
|
+
disallowed = ", ".join(r.disallowed_concepts) or "(none)"
|
|
40
|
+
print(f"{r.role_id:<16} {r.name:<20} {allowed:<30} {disallowed}")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_capability_role_show(args: argparse.Namespace) -> int:
|
|
45
|
+
store = _store(args)
|
|
46
|
+
role = store.load_role(args.role_id)
|
|
47
|
+
if role is None:
|
|
48
|
+
print(f"error: role '{args.role_id}' not found", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
print(f"Role {role.role_id}: {role.name}")
|
|
51
|
+
print(f" allowed_concepts: {role.allowed_concepts or '(any)'}")
|
|
52
|
+
print(f" disallowed_concepts: {role.disallowed_concepts or '(none)'}")
|
|
53
|
+
print(f" required_capabilities: {role.required_capabilities}")
|
|
54
|
+
print(f" max_temperature: {role.max_temperature}")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_capability_role_set(args: argparse.Namespace) -> int:
|
|
59
|
+
store = _store(args)
|
|
60
|
+
role = AgentRole(
|
|
61
|
+
role_id=args.role_id,
|
|
62
|
+
name=args.name or args.role_id,
|
|
63
|
+
allowed_concepts=_split_csv(args.allowed),
|
|
64
|
+
disallowed_concepts=_split_csv(args.disallowed),
|
|
65
|
+
required_capabilities=_split_csv(args.required),
|
|
66
|
+
max_temperature=args.max_temperature,
|
|
67
|
+
)
|
|
68
|
+
store.save_role(role)
|
|
69
|
+
print(f"Saved role {role.role_id}: {role.name}")
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_capability_role_policy(args: argparse.Namespace) -> int:
|
|
74
|
+
store = _store(args)
|
|
75
|
+
policy = store.load_policy()
|
|
76
|
+
if policy is None:
|
|
77
|
+
print("No role policy defined.")
|
|
78
|
+
return 0
|
|
79
|
+
print(f"Default role: {policy.default_role}")
|
|
80
|
+
print("Roles:")
|
|
81
|
+
for r in policy.roles:
|
|
82
|
+
print(f" - {r.role_id}: {r.name}")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_capability_role_apply_default_policy(args: argparse.Namespace) -> int:
|
|
87
|
+
store = _store(args)
|
|
88
|
+
apply_default_policy(store)
|
|
89
|
+
policy = store.load_policy()
|
|
90
|
+
print("Applied default role policy.")
|
|
91
|
+
if policy:
|
|
92
|
+
print(f" default_role: {policy.default_role}")
|
|
93
|
+
print(f" roles: {', '.join(r.role_id for r in policy.roles)}")
|
|
94
|
+
return 0
|