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.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,6 @@
1
+ """Make `python -m devtorch_cli` equivalent to the `devtorch` entry point."""
2
+
3
+ from devtorch_cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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