dmint-cli 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. dmint_cli-0.2.0/MANIFEST.in +1 -0
  2. dmint_cli-0.2.0/PKG-INFO +97 -0
  3. dmint_cli-0.2.0/README.md +86 -0
  4. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/pyproject.toml +5 -2
  5. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli/__main__.py +20 -8
  6. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli/api.py +50 -0
  7. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli/compile_policy.py +14 -31
  8. dmint_cli-0.2.0/src/dmint_cli/create_policy.py +379 -0
  9. dmint_cli-0.2.0/src/dmint_cli/prompts/__init__.py +1 -0
  10. dmint_cli-0.2.0/src/dmint_cli/prompts/policy_skill.md +49 -0
  11. dmint_cli-0.2.0/src/dmint_cli/verify_policy.py +92 -0
  12. dmint_cli-0.2.0/src/dmint_cli.egg-info/PKG-INFO +97 -0
  13. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli.egg-info/SOURCES.txt +6 -1
  14. dmint_cli-0.2.0/src/dmint_cli.egg-info/requires.txt +1 -0
  15. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/tests/test_cli_api.py +32 -0
  16. dmint_cli-0.2.0/tests/test_create_policy.py +214 -0
  17. dmint_cli-0.2.0/tests/test_verify_policy.py +68 -0
  18. dmint_cli-0.1.0/PKG-INFO +0 -79
  19. dmint_cli-0.1.0/README.md +0 -67
  20. dmint_cli-0.1.0/src/dmint_cli/create_policy.py +0 -230
  21. dmint_cli-0.1.0/src/dmint_cli.egg-info/PKG-INFO +0 -79
  22. dmint_cli-0.1.0/src/dmint_cli.egg-info/requires.txt +0 -2
  23. dmint_cli-0.1.0/tests/test_create_policy.py +0 -95
  24. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/LICENSE +0 -0
  25. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/setup.cfg +0 -0
  26. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli/__init__.py +0 -0
  27. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli.egg-info/dependency_links.txt +0 -0
  28. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli.egg-info/entry_points.txt +0 -0
  29. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/src/dmint_cli.egg-info/top_level.txt +0 -0
  30. {dmint_cli-0.1.0 → dmint_cli-0.2.0}/tests/test_compile_policy.py +0 -0
@@ -0,0 +1 @@
1
+ recursive-include src/dmint_cli/prompts *.md
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: dmint-cli
3
+ Version: 0.2.0
4
+ Summary: CLI tooling for Dmint policy creation, compilation, and management
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: dmint<0.2.0,>=0.1.0
10
+ Dynamic: license-file
11
+
12
+ # dmint-cli (v0.2.0)
13
+
14
+ > **Developer tooling for interactive creation, compilation, and validation of Dmint security policies.**
15
+
16
+ `dmint-cli` provides command-line utilities for discovering tools via static AST analysis, interactive multi-turn policy authoring via LLM model selection and clarification loops, and deterministic schema/semantic policy verification (`policy.json`).
17
+
18
+ ```text
19
+ access.md (Human Security Intent)
20
+ ↓
21
+ dmint create-policy (Multi-turn Wizard & Model Discovery)
22
+ ↓
23
+ Self-Correcting LLM Loop (clarification_needed / policy_ready)
24
+ ↓
25
+ Dmint Core Validation (Policy.from_mapping)
26
+ ↓
27
+ dmint verify-policy (Pure Deterministic Verification)
28
+ ↓
29
+ policy.json (Authoritative Policy)
30
+ ```
31
+
32
+ > **Note:** The CLI helps developers *author* and *validate* policies during development. `dmint` core remains the sole trusted runtime authorization engine.
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install dmint-cli
40
+ ```
41
+
42
+ ---
43
+
44
+ ## CLI Commands
45
+
46
+ ### 1. Interactive Policy Wizard (`dmint create-policy`)
47
+
48
+ Interactive, multi-turn wizard to create and validate a Dmint policy:
49
+ - Interactive provider and model discovery (`list_models`).
50
+ - AST-based static tool discovery via `--tools` parameter.
51
+ - Multi-turn envelope output contract (`clarification_needed` & `policy_ready`).
52
+ - Self-correcting validation retry loop on `PolicyError`.
53
+ - Atomic file write and immediate verification.
54
+
55
+ ```bash
56
+ dmint create-policy -f access.md -o policy.json --tools my_tools.py
57
+ ```
58
+
59
+ ### 2. Standalone Policy Verification (`dmint verify-policy`)
60
+
61
+ Pure, deterministic schema and semantic validation of `policy.json` without LLM involvement. Returns exit code `0` on success or non-zero on validation failure:
62
+
63
+ ```bash
64
+ dmint verify-policy policy.json
65
+ # or
66
+ dmint verify-policy -f policy.json
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Environment Configuration
72
+
73
+ Set API credentials via environment variables for LLM policy compilation:
74
+
75
+ ```bash
76
+ export OPENAI_API_KEY="sk-..."
77
+ # Optional custom providers (openai, gemini, groq, openrouter, ollama)
78
+ export GEMINI_API_KEY="..."
79
+ export GROQ_API_KEY="..."
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Testing
85
+
86
+ Run the CLI test suite:
87
+
88
+ ```bash
89
+ python3 -m unittest discover -s tests -p "test_*.py"
90
+ ```
91
+
92
+ ---
93
+
94
+ ## License
95
+
96
+ Licensed under the [Apache License, Version 2.0](LICENSE).
97
+ See the [`LICENSE`](LICENSE) file for the complete license text.
@@ -0,0 +1,86 @@
1
+ # dmint-cli (v0.2.0)
2
+
3
+ > **Developer tooling for interactive creation, compilation, and validation of Dmint security policies.**
4
+
5
+ `dmint-cli` provides command-line utilities for discovering tools via static AST analysis, interactive multi-turn policy authoring via LLM model selection and clarification loops, and deterministic schema/semantic policy verification (`policy.json`).
6
+
7
+ ```text
8
+ access.md (Human Security Intent)
9
+ ↓
10
+ dmint create-policy (Multi-turn Wizard & Model Discovery)
11
+ ↓
12
+ Self-Correcting LLM Loop (clarification_needed / policy_ready)
13
+ ↓
14
+ Dmint Core Validation (Policy.from_mapping)
15
+ ↓
16
+ dmint verify-policy (Pure Deterministic Verification)
17
+ ↓
18
+ policy.json (Authoritative Policy)
19
+ ```
20
+
21
+ > **Note:** The CLI helps developers *author* and *validate* policies during development. `dmint` core remains the sole trusted runtime authorization engine.
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install dmint-cli
29
+ ```
30
+
31
+ ---
32
+
33
+ ## CLI Commands
34
+
35
+ ### 1. Interactive Policy Wizard (`dmint create-policy`)
36
+
37
+ Interactive, multi-turn wizard to create and validate a Dmint policy:
38
+ - Interactive provider and model discovery (`list_models`).
39
+ - AST-based static tool discovery via `--tools` parameter.
40
+ - Multi-turn envelope output contract (`clarification_needed` & `policy_ready`).
41
+ - Self-correcting validation retry loop on `PolicyError`.
42
+ - Atomic file write and immediate verification.
43
+
44
+ ```bash
45
+ dmint create-policy -f access.md -o policy.json --tools my_tools.py
46
+ ```
47
+
48
+ ### 2. Standalone Policy Verification (`dmint verify-policy`)
49
+
50
+ Pure, deterministic schema and semantic validation of `policy.json` without LLM involvement. Returns exit code `0` on success or non-zero on validation failure:
51
+
52
+ ```bash
53
+ dmint verify-policy policy.json
54
+ # or
55
+ dmint verify-policy -f policy.json
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Environment Configuration
61
+
62
+ Set API credentials via environment variables for LLM policy compilation:
63
+
64
+ ```bash
65
+ export OPENAI_API_KEY="sk-..."
66
+ # Optional custom providers (openai, gemini, groq, openrouter, ollama)
67
+ export GEMINI_API_KEY="..."
68
+ export GROQ_API_KEY="..."
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Testing
74
+
75
+ Run the CLI test suite:
76
+
77
+ ```bash
78
+ python3 -m unittest discover -s tests -p "test_*.py"
79
+ ```
80
+
81
+ ---
82
+
83
+ ## License
84
+
85
+ Licensed under the [Apache License, Version 2.0](LICENSE).
86
+ See the [`LICENSE`](LICENSE) file for the complete license text.
@@ -4,16 +4,19 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dmint-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "CLI tooling for Dmint policy creation, compilation, and management"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.10"
11
11
  readme = "README.md"
12
12
  dependencies = [
13
13
  "dmint>=0.1.0,<0.2.0",
14
- "dmint-skills>=0.1.0,<0.2.0",
15
14
  ]
16
15
 
16
+ [tool.setuptools.package-data]
17
+ "dmint_cli" = ["prompts/*.md"]
18
+ "dmint_cli.prompts" = ["*.md"]
19
+
17
20
  [project.scripts]
18
21
  dmint = "dmint_cli.__main__:main"
19
22
 
@@ -7,13 +7,14 @@ import sys
7
7
 
8
8
  from dmint_cli.compile_policy import main_compile
9
9
  from dmint_cli.create_policy import main_create
10
+ from dmint_cli.verify_policy import main_verify
10
11
 
11
12
 
12
13
  def main(args: list[str] | None = None) -> int:
13
14
  if args is None:
14
15
  args = sys.argv[1:]
15
16
 
16
- # Normalize subcommands like "create policy" -> "create-policy" and "compile policy" -> "compile-policy"
17
+ # Normalize two-word subcommands: "create policy" -> "create-policy", "verify policy" -> "verify-policy", etc.
17
18
  normalized_args: list[str] = []
18
19
  i = 0
19
20
  while i < len(args):
@@ -22,6 +23,10 @@ def main(args: list[str] | None = None) -> int:
22
23
  normalized_args.append("create-policy")
23
24
  i += 2
24
25
  continue
26
+ if arg == "verify" and i + 1 < len(args) and args[i + 1] == "policy":
27
+ normalized_args.append("verify-policy")
28
+ i += 2
29
+ continue
25
30
  if arg == "compile" and i + 1 < len(args) and args[i + 1] == "policy":
26
31
  normalized_args.append("compile-policy")
27
32
  i += 2
@@ -36,13 +41,18 @@ def main(args: list[str] | None = None) -> int:
36
41
  subparsers = parser.add_subparsers(dest="command", help="Command to run")
37
42
 
38
43
  subparsers.add_parser(
39
- "compile-policy",
40
- help="Compile natural language policy requirements into a validated policy.json file",
44
+ "create-policy",
45
+ help="Interactive wizard to create and validate Dmint policy files",
41
46
  add_help=False,
42
47
  )
43
48
  subparsers.add_parser(
44
- "create-policy",
45
- help="Interactive wizard to discover tools, ask security questions, and create policy files",
49
+ "verify-policy",
50
+ help="Verify a policy.json file against Dmint schema and semantic invariants",
51
+ add_help=False,
52
+ )
53
+ subparsers.add_parser(
54
+ "compile-policy",
55
+ help="Compile natural language policy requirements into a validated policy.json file",
46
56
  add_help=False,
47
57
  )
48
58
 
@@ -53,10 +63,12 @@ def main(args: list[str] | None = None) -> int:
53
63
  cmd = normalized_args[0]
54
64
  cmd_args = normalized_args[1:]
55
65
 
56
- if cmd == "compile-policy":
57
- return main_compile(cmd_args)
58
- elif cmd == "create-policy":
66
+ if cmd == "create-policy":
59
67
  return main_create(cmd_args)
68
+ elif cmd == "verify-policy":
69
+ return main_verify(cmd_args)
70
+ elif cmd == "compile-policy":
71
+ return main_compile(cmd_args)
60
72
  else:
61
73
  parser.print_help()
62
74
  return 2
@@ -154,6 +154,56 @@ class OpenAICompatClient:
154
154
 
155
155
  return extract_json_text(content)
156
156
 
157
+ def list_models(self) -> list[str]:
158
+ """Call GET {base_url}/models and return a list of available model IDs."""
159
+ endpoint = f"{self.base_url}/models"
160
+ headers = {
161
+ "User-Agent": "dmint-policy-author/0.1.0",
162
+ }
163
+ if self.api_key:
164
+ headers["Authorization"] = f"Bearer {self.api_key}"
165
+
166
+ req = urllib.request.Request(
167
+ endpoint,
168
+ headers=headers,
169
+ method="GET",
170
+ )
171
+
172
+ try:
173
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
174
+ resp_bytes = resp.read()
175
+ try:
176
+ resp_data = json.loads(resp_bytes.decode("utf-8"))
177
+ except Exception as exc:
178
+ raise RuntimeError("API response is not valid JSON") from exc
179
+ except urllib.error.HTTPError as exc:
180
+ try:
181
+ error_body = exc.read().decode("utf-8", errors="ignore")
182
+ error_json = json.loads(error_body)
183
+ msg = error_json.get("error", {}).get("message", error_body)
184
+ except Exception:
185
+ msg = f"HTTP Error {exc.code}"
186
+ masked_key = mask_secret(self.api_key)
187
+ raise RuntimeError(f"OpenAI-compatible API HTTP {exc.code}: {msg} (key: {masked_key})") from exc
188
+ except urllib.error.URLError as exc:
189
+ raise RuntimeError(f"API connection failed ({self.base_url}): {exc.reason}") from exc
190
+ except Exception as exc:
191
+ raise RuntimeError(f"API request failed: {exc}") from exc
192
+
193
+ data = resp_data.get("data")
194
+ if not isinstance(data, list):
195
+ raise RuntimeError("API response missing 'data' array in models response")
196
+
197
+ models: list[str] = []
198
+ for item in data:
199
+ if isinstance(item, dict) and "id" in item and isinstance(item["id"], str):
200
+ models.append(item["id"])
201
+
202
+ if not models:
203
+ raise RuntimeError("No model IDs found in /models response")
204
+
205
+ return models
206
+
157
207
 
158
208
  def extract_json_text(text: str) -> str:
159
209
  """Extract clean JSON text, removing markdown code fences if present."""
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import importlib.resources
6
7
  import json
7
8
  import os
8
9
  import sys
@@ -11,7 +12,6 @@ from pathlib import Path
11
12
 
12
13
  from dmint.policy import Policy, PolicyError
13
14
  from dmint_cli.api import OpenAICompatClient, mask_secret
14
- from dmint_skills.skill import DMINT_POLICY_SYSTEM_PROMPT
15
15
 
16
16
 
17
17
  class CLIError(Exception):
@@ -47,6 +47,15 @@ class OutputWriteError(CLIError):
47
47
  super().__init__(message, exit_code=7)
48
48
 
49
49
 
50
+ def load_system_prompt() -> str:
51
+ """Load system prompt from bundled resource file."""
52
+ try:
53
+ return importlib.resources.files("dmint_cli.prompts").joinpath("policy_skill.md").read_text(encoding="utf-8")
54
+ except Exception:
55
+ prompt_path = Path(__file__).parent / "prompts" / "policy_skill.md"
56
+ return prompt_path.read_text(encoding="utf-8")
57
+
58
+
50
59
  def atomic_write_json(output_path: Path, data: dict) -> None:
51
60
  """Atomically write JSON to destination using temp file replace to prevent partial writes."""
52
61
  output_path.parent.mkdir(parents=True, exist_ok=True)
@@ -75,7 +84,6 @@ def compile_policy_file(
75
84
  model: str | None = None,
76
85
  provider: str | None = None,
77
86
  timeout: float = 60.0,
78
- offline_fallback: bool = True,
79
87
  ) -> Policy:
80
88
  """Compile a natural language policy requirement file into a verified Dmint policy JSON file."""
81
89
  input_path = Path(input_file).resolve()
@@ -105,8 +113,10 @@ def compile_policy_file(
105
113
  timeout=timeout,
106
114
  )
107
115
 
116
+ system_prompt = load_system_prompt()
117
+
108
118
  messages = [
109
- {"role": "system", "content": DMINT_POLICY_SYSTEM_PROMPT},
119
+ {"role": "system", "content": system_prompt},
110
120
  {
111
121
  "role": "user",
112
122
  "content": f"Convert the following natural language policy requirements into Dmint JSON policy format:\n\n{policy_text}",
@@ -116,34 +126,7 @@ def compile_policy_file(
116
126
  try:
117
127
  raw_json = client.chat_completion(messages, temperature=0.0)
118
128
  except Exception as exc:
119
- if offline_fallback:
120
- print(f"[!] API unavailable or key unconfigured ({exc}). Using deterministic policy compilation fallback...\n")
121
- rules = []
122
- seen_rules = set()
123
- for line in policy_text.splitlines():
124
- l = line.lower()
125
- r = None
126
- if "list_services" in l or "list services" in l:
127
- r = ("allow", "deployment", "list_services", "*")
128
- elif "get_service_status" in l or "read service status" in l:
129
- r = ("allow", "deployment", "get_service_status", "*")
130
- elif "deploy_service" in l or "deploy" in l:
131
- eff = "approval_required" if "approval" in l or "production" in l else "allow"
132
- r = (eff, "deployment", "deploy_service", "*")
133
- elif "rollback_service" in l or "rollback" in l:
134
- r = ("approval_required", "deployment", "rollback_service", "*")
135
- elif "scale_service" in l or "scale" in l:
136
- r = ("approval_required", "deployment", "scale_service", "*")
137
- elif "delete_service" in l or "delete" in l:
138
- r = ("deny", "deployment", "delete_service", "*")
139
-
140
- if r and r not in seen_rules:
141
- seen_rules.add(r)
142
- rules.append({"effect": r[0], "tool": r[1], "action": r[2], "resource": r[3]})
143
-
144
- raw_json = json.dumps({"rules": rules})
145
- else:
146
- raise APIError(str(exc)) from exc
129
+ raise APIError(str(exc)) from exc
147
130
 
148
131
  try:
149
132
  policy_data = json.loads(raw_json)