pretext-sim 0.3.2__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.
- pretext/__init__.py +34 -0
- pretext/cli.py +555 -0
- pretext/client.py +220 -0
- pretext/data/example-profile.yaml +12 -0
- pretext/models.py +326 -0
- pretext/prompt.py +126 -0
- pretext/render.py +247 -0
- pretext_sim-0.3.2.dist-info/METADATA +259 -0
- pretext_sim-0.3.2.dist-info/RECORD +12 -0
- pretext_sim-0.3.2.dist-info/WHEEL +4 -0
- pretext_sim-0.3.2.dist-info/entry_points.txt +2 -0
- pretext_sim-0.3.2.dist-info/licenses/LICENSE +21 -0
pretext/render.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Rich rendering and plain-text export of simulations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from rich.console import Console, Group, RenderableType
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.rule import Rule
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from .models import Profile, Settings, Simulation
|
|
16
|
+
|
|
17
|
+
DISCLAIMER = (
|
|
18
|
+
"Fictional exercise. Domains, names, and systems are invented. "
|
|
19
|
+
"Use against authorized targets only."
|
|
20
|
+
)
|
|
21
|
+
DIFFICULTY_LABELS = {
|
|
22
|
+
1: "broad, high volume",
|
|
23
|
+
2: "lightly targeted",
|
|
24
|
+
3: "targeted",
|
|
25
|
+
4: "researched, multi-step",
|
|
26
|
+
5: "bespoke, multi-day",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def console(*, no_color: bool = False, width: int | None = None, stderr: bool = False) -> Console:
|
|
31
|
+
return Console(
|
|
32
|
+
no_color=no_color,
|
|
33
|
+
width=width,
|
|
34
|
+
highlight=False,
|
|
35
|
+
soft_wrap=False,
|
|
36
|
+
stderr=stderr,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _difficulty(value: int) -> Text:
|
|
41
|
+
filled = "●" * value + "○" * (5 - value)
|
|
42
|
+
label = DIFFICULTY_LABELS.get(value, "targeted")
|
|
43
|
+
return Text(f"{filled} {value}/5 · {label}", style="magenta")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
SECTIONS = ("pretext", "opening", "escalation", "red_flags")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def render_simulation(
|
|
50
|
+
simulation: Simulation,
|
|
51
|
+
profile: Profile,
|
|
52
|
+
out: Console,
|
|
53
|
+
*,
|
|
54
|
+
sections: tuple[str, ...] = SECTIONS,
|
|
55
|
+
) -> None:
|
|
56
|
+
header = Table.grid(padding=(0, 2))
|
|
57
|
+
header.add_column(style="bold cyan", justify="right")
|
|
58
|
+
header.add_column()
|
|
59
|
+
header.add_row("Persona", profile.name)
|
|
60
|
+
header.add_row("Role", f"{profile.role} · {profile.company}")
|
|
61
|
+
header.add_row("Style", profile.communication_style)
|
|
62
|
+
header.add_row("Difficulty", _difficulty(simulation.difficulty))
|
|
63
|
+
if simulation.objective:
|
|
64
|
+
header.add_row("Objective", simulation.objective)
|
|
65
|
+
|
|
66
|
+
out.print(
|
|
67
|
+
Panel(
|
|
68
|
+
header,
|
|
69
|
+
title="[bold]Simulation[/bold]",
|
|
70
|
+
subtitle=f"[dim]{profile.name}[/dim]",
|
|
71
|
+
border_style="cyan",
|
|
72
|
+
expand=False,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if "pretext" in sections:
|
|
77
|
+
out.print(Rule("[bold]Pretext narrative[/bold]", style="cyan"))
|
|
78
|
+
out.print(Markdown(simulation.pretext))
|
|
79
|
+
|
|
80
|
+
if "opening" not in sections:
|
|
81
|
+
return _render_tail(simulation, out, sections)
|
|
82
|
+
|
|
83
|
+
message_body: list[RenderableType] = []
|
|
84
|
+
if simulation.subject_line:
|
|
85
|
+
message_body.append(Text(f"Subject: {simulation.subject_line}", style="bold"))
|
|
86
|
+
message_body.append(Text(""))
|
|
87
|
+
message_body.append(Markdown(simulation.opening_message))
|
|
88
|
+
out.print(
|
|
89
|
+
Panel(
|
|
90
|
+
Group(*message_body),
|
|
91
|
+
title="[bold]Opening message[/bold]",
|
|
92
|
+
border_style="green",
|
|
93
|
+
expand=False,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_render_tail(simulation, out, sections)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _render_tail(simulation: Simulation, out: Console, sections: tuple[str, ...]) -> None:
|
|
101
|
+
if "escalation" in sections:
|
|
102
|
+
_render_escalation(simulation, out)
|
|
103
|
+
if "red_flags" in sections:
|
|
104
|
+
_render_red_flags(simulation, out)
|
|
105
|
+
out.print(Rule(style="dim"))
|
|
106
|
+
out.print(Text(DISCLAIMER, style="dim italic"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_escalation(simulation: Simulation, out: Console) -> None:
|
|
110
|
+
out.print(Rule("[bold]Escalation path[/bold]", style="cyan"))
|
|
111
|
+
steps = Table(show_header=True, header_style="bold cyan", expand=True, box=None)
|
|
112
|
+
steps.add_column("#", width=3, justify="right")
|
|
113
|
+
steps.add_column("Channel", width=11)
|
|
114
|
+
steps.add_column("Action", ratio=3)
|
|
115
|
+
steps.add_column("Goal", ratio=2)
|
|
116
|
+
for step in simulation.escalation_path:
|
|
117
|
+
steps.add_row(
|
|
118
|
+
str(step.stage),
|
|
119
|
+
Text(step.channel.value, style="yellow"),
|
|
120
|
+
Text(step.action),
|
|
121
|
+
Text(step.goal, style="dim"),
|
|
122
|
+
)
|
|
123
|
+
if step.artifact:
|
|
124
|
+
steps.add_row("", "", Text(f"↳ {step.artifact}", style="dim italic"), "")
|
|
125
|
+
out.print(steps)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _render_red_flags(simulation: Simulation, out: Console) -> None:
|
|
129
|
+
out.print(Rule("[bold]Red flags for defenders[/bold]", style="cyan"))
|
|
130
|
+
for index, flag in enumerate(simulation.red_flags, start=1):
|
|
131
|
+
out.print(
|
|
132
|
+
Panel(
|
|
133
|
+
Group(
|
|
134
|
+
Markdown(flag.why_it_matters),
|
|
135
|
+
Text(""),
|
|
136
|
+
Text(f"Defender action: {flag.defender_action}", style="bold green"),
|
|
137
|
+
),
|
|
138
|
+
title=f"[bold]{index}. {flag.title}[/bold]",
|
|
139
|
+
border_style="red",
|
|
140
|
+
expand=False,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def render_settings(settings: Settings, out: Console) -> None:
|
|
146
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
147
|
+
table.add_column(style="bold cyan", justify="right")
|
|
148
|
+
table.add_column()
|
|
149
|
+
table.add_row("Endpoint", settings.endpoint)
|
|
150
|
+
table.add_row("Model", settings.model)
|
|
151
|
+
table.add_row("Key", "set" if settings.api_key else "[red]missing[/red]")
|
|
152
|
+
table.add_row("Temperature", f"{settings.temperature:g}")
|
|
153
|
+
table.add_row("Max tokens", str(settings.max_tokens))
|
|
154
|
+
table.add_row("Timeout", f"{settings.timeout:g}s")
|
|
155
|
+
table.add_row("Retries", str(settings.retries))
|
|
156
|
+
if settings.seed is not None:
|
|
157
|
+
table.add_row("Seed", str(settings.seed))
|
|
158
|
+
out.print(
|
|
159
|
+
Panel(
|
|
160
|
+
table,
|
|
161
|
+
title="[bold]Resolved configuration[/bold]",
|
|
162
|
+
border_style="cyan",
|
|
163
|
+
expand=False,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_profile(profile: Profile, out: Console) -> None:
|
|
169
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
170
|
+
table.add_column(style="bold cyan", justify="right")
|
|
171
|
+
table.add_column()
|
|
172
|
+
for label, value in (
|
|
173
|
+
("Name", profile.name),
|
|
174
|
+
("Role", profile.role),
|
|
175
|
+
("Company", profile.company),
|
|
176
|
+
("Style", profile.communication_style),
|
|
177
|
+
("Department", profile.department),
|
|
178
|
+
("Tenure", profile.tenure),
|
|
179
|
+
("Reports to", profile.reports_to),
|
|
180
|
+
("Systems", ", ".join(profile.systems) or None),
|
|
181
|
+
("Notes", profile.notes),
|
|
182
|
+
):
|
|
183
|
+
if value:
|
|
184
|
+
table.add_row(label, value)
|
|
185
|
+
out.print(Panel(table, title="[bold]Profile OK[/bold]", border_style="green", expand=False))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def to_markdown(
|
|
189
|
+
simulation: Simulation,
|
|
190
|
+
profile: Profile,
|
|
191
|
+
*,
|
|
192
|
+
sections: tuple[str, ...] = SECTIONS,
|
|
193
|
+
) -> str:
|
|
194
|
+
lines = [
|
|
195
|
+
f"# Social-engineering simulation: {profile.name}",
|
|
196
|
+
"",
|
|
197
|
+
f"- **Role:** {profile.role} at {profile.company}",
|
|
198
|
+
f"- **Communication style:** {profile.communication_style}",
|
|
199
|
+
f"- **Difficulty:** {simulation.difficulty}/5",
|
|
200
|
+
]
|
|
201
|
+
if simulation.objective:
|
|
202
|
+
lines.append(f"- **Objective:** {simulation.objective}")
|
|
203
|
+
if "pretext" in sections:
|
|
204
|
+
lines += ["", "## Pretext narrative", "", simulation.pretext]
|
|
205
|
+
if "opening" in sections:
|
|
206
|
+
lines += ["", "## Opening message", ""]
|
|
207
|
+
if simulation.subject_line:
|
|
208
|
+
lines += [f"**Subject:** {simulation.subject_line}", ""]
|
|
209
|
+
lines += [simulation.opening_message]
|
|
210
|
+
if "escalation" in sections:
|
|
211
|
+
lines += ["", "## Escalation path", ""]
|
|
212
|
+
for step in simulation.escalation_path:
|
|
213
|
+
lines.append(
|
|
214
|
+
f"{step.stage}. **{step.channel.value}** · {step.action} _(goal: {step.goal})_"
|
|
215
|
+
)
|
|
216
|
+
if step.artifact:
|
|
217
|
+
lines.append(f" - Artifact: {step.artifact}")
|
|
218
|
+
if "red_flags" in sections:
|
|
219
|
+
lines += ["", "## Red flags for defenders", ""]
|
|
220
|
+
for index, flag in enumerate(simulation.red_flags, start=1):
|
|
221
|
+
lines += [
|
|
222
|
+
f"{index}. **{flag.title}**",
|
|
223
|
+
f" - Why it matters: {flag.why_it_matters}",
|
|
224
|
+
f" - Defender action: {flag.defender_action}",
|
|
225
|
+
]
|
|
226
|
+
lines += ["", "---", "", f"_{DISCLAIMER}_", ""]
|
|
227
|
+
return "\n".join(lines)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def to_json(
|
|
231
|
+
simulation: Simulation,
|
|
232
|
+
profile: Profile,
|
|
233
|
+
*,
|
|
234
|
+
sections: tuple[str, ...] = SECTIONS,
|
|
235
|
+
) -> str:
|
|
236
|
+
payload: dict[str, object] = {
|
|
237
|
+
"profile": profile.model_dump(mode="json"),
|
|
238
|
+
"simulation": simulation.model_dump(mode="json"),
|
|
239
|
+
"sections": list(sections),
|
|
240
|
+
"disclaimer": DISCLAIMER,
|
|
241
|
+
}
|
|
242
|
+
return json.dumps(payload, indent=2, ensure_ascii=False)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def render_warnings(warnings: Iterable[str], out: Console) -> None:
|
|
246
|
+
for warning in warnings:
|
|
247
|
+
out.print(Text(f"! {warning}", style="yellow"))
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pretext-sim
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Generate labeled social-engineering simulations for security training
|
|
5
|
+
Author: pretext contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,phishing,red-team,security,security-awareness,training
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: pydantic>=2.6
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: rich>=13.7
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<div align="center">
|
|
34
|
+
|
|
35
|
+
# pretext
|
|
36
|
+
|
|
37
|
+
**Turn a one-page persona into a runnable social-engineering exercise.**
|
|
38
|
+
|
|
39
|
+
Narrative, opening message, escalation path, and the red flags defenders should
|
|
40
|
+
have caught, generated, validated, then rendered where you need it.
|
|
41
|
+
|
|
42
|
+
[](https://github.com/sjdsjadjajdjsa/pretext/actions/workflows/ci.yml)
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
<img src="docs/demo.png" alt="pretext rendering a simulation in the terminal" width="820">
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
> **Authorized training only.** Everything produced is a fictional exercise.
|
|
51
|
+
> Domains, names, and systems are invented unless you say otherwise. pretext never
|
|
52
|
+
> sends anything to a target; it writes the material and the analysis. Run an
|
|
53
|
+
> exercise against your own people only after your security team agrees to it, and
|
|
54
|
+
> check the lures before sending.
|
|
55
|
+
|
|
56
|
+
## Why
|
|
57
|
+
|
|
58
|
+
Security-awareness decks are generic ("you receive an email with a typo"), so staff
|
|
59
|
+
learn to spot a template rather than a technique. A useful exercise is grounded in
|
|
60
|
+
one role at one company: what that person controls, who they trust, and what an
|
|
61
|
+
operator would do next.
|
|
62
|
+
|
|
63
|
+
Doing that by hand costs an afternoon per persona. `pretext` takes a YAML profile
|
|
64
|
+
and produces the exercise in one call, defender's side included, so training value
|
|
65
|
+
does not depend on whoever wrote the scenario remembering to add it.
|
|
66
|
+
|
|
67
|
+
## What you get
|
|
68
|
+
|
|
69
|
+
| Section | Content |
|
|
70
|
+
| ------- | ------- |
|
|
71
|
+
| Pretext narrative | The operation, attacker-side: harvested detail, forged artifact, why contact lands today |
|
|
72
|
+
| Opening message | Usable verbatim, in the sender's voice, carrying the sending address and one lure |
|
|
73
|
+
| Escalation path | 3-6 stages across channels, each handing over a new prop or introducing a new person |
|
|
74
|
+
| Red flags | Observable tells tied to this exercise, each with a defender action, including one only clear in hindsight |
|
|
75
|
+
|
|
76
|
+
`docs/running-an-exercise.md` covers authorization, delivery, scoring and the
|
|
77
|
+
debrief, which is the half the tool cannot do.
|
|
78
|
+
|
|
79
|
+
Every output carries a disclaimer. [`sample-output.md`](sample-output.md) is a
|
|
80
|
+
complete Markdown export; [`docs/demo.png`](docs/demo.png) is the terminal view.
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
uvx pretext-sim generate persona.yaml # no install
|
|
86
|
+
pipx install pretext-sim # or a permanent command
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Python 3.10+. The distribution is `pretext-sim`; the command is `pretext`. For the
|
|
90
|
+
source tree: `uv sync --extra dev` then `uv run pretext`.
|
|
91
|
+
|
|
92
|
+
## Quickstart
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
pretext example -o persona.yaml # fictional profile to edit
|
|
96
|
+
pretext validate persona.yaml # schema check, no API call
|
|
97
|
+
pretext generate persona.yaml --dry-run # resolved config + exact prompt, no call
|
|
98
|
+
pretext generate persona.yaml -f markdown -o exercise.md
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Point it at any OpenAI-compatible chat completions endpoint with a flag, env var, or
|
|
102
|
+
`.env`:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
export PRETEXT_API_KEY=sk-...
|
|
106
|
+
export PRETEXT_BASE_URL=http://localhost:11434/v1 # local proxy, if you prefer
|
|
107
|
+
export PRETEXT_MODEL=gpt-oss:20b
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or name a known host instead of the URL: `--provider openai|openrouter|venice|ollama|lmstudio`
|
|
111
|
+
(`PRETEXT_PROVIDER`), which fills the base URL. An explicit `--base-url` wins. Local
|
|
112
|
+
providers need no key.
|
|
113
|
+
|
|
114
|
+
`pretext config` shows what resolves, with the key masked.
|
|
115
|
+
|
|
116
|
+
## Profile
|
|
117
|
+
|
|
118
|
+
Only four fields are required (`name`, `role`, `company`, `communication_style`).
|
|
119
|
+
The rest sharpen the result.
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
name: Dana Iyer
|
|
123
|
+
role: Senior Payroll Analyst
|
|
124
|
+
company: Northwind Logistics
|
|
125
|
+
communication_style: Brisk and formal, mixes payroll shorthand with chatty asides
|
|
126
|
+
department: People Operations
|
|
127
|
+
tenure: 3 months
|
|
128
|
+
reports_to: Marta Kwan
|
|
129
|
+
systems: [Workday, the payroll mailbox]
|
|
130
|
+
notes: New enough to the team to be unsure of process.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Optional: `department`, `tenure`, `reports_to`, `systems`, `notes`. Unknown keys are
|
|
134
|
+
rejected so typos surface early. Fixture personas live in
|
|
135
|
+
[`tests/fixtures/profiles`](tests/fixtures/profiles).
|
|
136
|
+
|
|
137
|
+
## Command reference
|
|
138
|
+
|
|
139
|
+
| Command | Purpose |
|
|
140
|
+
| ------- | ------- |
|
|
141
|
+
| `generate PROFILE` | Produce one exercise |
|
|
142
|
+
| `validate PROFILE` | Validate without calling the API |
|
|
143
|
+
| `example` | Print the bundled fictional profile |
|
|
144
|
+
| `batch DIR` | One exercise per `*.yaml` in DIR (files starting `_` are skipped) |
|
|
145
|
+
| `doctor` | Probe the endpoint: config, TLS, reachability, model, JSON mode |
|
|
146
|
+
| `schema` | Print the JSON Schema model output must satisfy (`--all` adds the profile) |
|
|
147
|
+
| `config` | Show resolved settings, key masked |
|
|
148
|
+
|
|
149
|
+
Flags on `generate`:
|
|
150
|
+
|
|
151
|
+
| Flag | Effect |
|
|
152
|
+
| ---- | ------ |
|
|
153
|
+
| `--channel` | Force the opening channel (email, phone, sms, chat, in_person, linkedin) |
|
|
154
|
+
| `--extra` | Extra direction for the model |
|
|
155
|
+
| `--format` | `rich` (default), `markdown`, `json` |
|
|
156
|
+
| `--out` | Write to a file; plain text unless a format is chosen |
|
|
157
|
+
| `--sections` | Render only some of `pretext,opening,escalation,red_flags` |
|
|
158
|
+
| `--from-file` | Replay a saved response; no API call, no key |
|
|
159
|
+
| `--dry-run` | Print resolved config and prompt, send nothing; `--out` writes the prompt |
|
|
160
|
+
| `--usage` | Token counts on stderr |
|
|
161
|
+
| `--base-url`, `--model`, `--api-key` | Override endpoint settings |
|
|
162
|
+
| `--temperature`, `--max-tokens`, `--timeout`, `--retries`, `--seed` | Sampling and transport |
|
|
163
|
+
| `--no-color` | Plain terminal output |
|
|
164
|
+
|
|
165
|
+
Exit codes: `0` success, `1` bad profile/config/API/schema failure, `2` usage error,
|
|
166
|
+
`130` interrupted.
|
|
167
|
+
|
|
168
|
+
`--from-file` accepts a `--format json` export, a bare simulation object, or a raw
|
|
169
|
+
chat completion body, so formatting work stays free and offline.
|
|
170
|
+
|
|
171
|
+
## Diagnose first
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pretext doctor # config, TLS, /models, JSON mode
|
|
175
|
+
pretext doctor --full # also runs one real simulation through the schema
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Exits `1` on any failure and prints the provider's own error, so a wrong model name
|
|
179
|
+
or a blocked key shows up before a batch run wastes retries.
|
|
180
|
+
|
|
181
|
+
## Batch runs
|
|
182
|
+
|
|
183
|
+
For a whole team, point `batch` at a directory and it writes one file per profile,
|
|
184
|
+
continuing past failures:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
pretext batch profiles/ --out-dir packet/ --format markdown
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Progress and a `wrote 7/9` summary go to stderr, the failure reasons are listed, and
|
|
191
|
+
the exit code is `1` if anything failed. Endpoint flags are the same as `generate`;
|
|
192
|
+
`--sections`, `--format` and `--usage` apply to the run as a whole.
|
|
193
|
+
|
|
194
|
+
## Configuration
|
|
195
|
+
|
|
196
|
+
First hit wins:
|
|
197
|
+
|
|
198
|
+
1. Flags (`--base-url`, `--model`, `--api-key`, `--provider`)
|
|
199
|
+
2. Environment: the same names under `PRETEXT_` (`PRETEXT_API_KEY`, `PRETEXT_MODEL`, ...)
|
|
200
|
+
3. `.env` in the working directory
|
|
201
|
+
4. `OPENAI_API_KEY` for the key, then `https://api.openai.com/v1` and `gpt-4o-mini`
|
|
202
|
+
|
|
203
|
+
The key is never printed and never written to output.
|
|
204
|
+
|
|
205
|
+
## How it works
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
profile.yaml ─> Profile ─> prompt ─> POST /chat/completions ─> extract JSON
|
|
209
|
+
│
|
|
210
|
+
rich | markdown | json <─ render <─ validate <────────────────┘
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Model output is untrusted. It is parsed out of fences or prose, validated against a
|
|
214
|
+
strict pydantic schema, and only then rendered. Escalation stages must run 1..n with
|
|
215
|
+
no gaps, `red_flags` needs at least two entries, and unknown keys are rejected, so a
|
|
216
|
+
loose model fails loudly instead of producing a half-filled exercise.
|
|
217
|
+
|
|
218
|
+
Diagnostics go to stderr, keeping stdout pipeable: `pretext generate p.yaml -f json | jq`.
|
|
219
|
+
|
|
220
|
+
## Releasing
|
|
221
|
+
|
|
222
|
+
Tagging `vX.Y.Z` runs `release.yml`: it checks the tag against `__version__`, gates
|
|
223
|
+
on lint, format, types and tests, builds, then publishes a release whose notes come
|
|
224
|
+
from `CHANGELOG.md`. Version bumps and changelog sections land before the tag.
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
uv run pytest --cov=pretext # offline: MockTransport and a stubbed client
|
|
230
|
+
uv run ruff check src tests
|
|
231
|
+
uv run mypy
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The suite needs no key. `conftest.py` chdirs each test into a temp directory so your
|
|
235
|
+
own `.env` cannot leak in, and pins the terminal width so Rich reflow stays stable.
|
|
236
|
+
|
|
237
|
+
`scripts/capture_corpus.py` refreshes `tests/fixtures/corpus`, which
|
|
238
|
+
`tests/test_corpus.py` checks for structural promises.
|
|
239
|
+
|
|
240
|
+
`AGENTS.md` documents the layout, the hardening rules, and the traps for agent
|
|
241
|
+
contributors. `CONTRIBUTING.md` covers process; security reports follow `SECURITY.md`.
|
|
242
|
+
|
|
243
|
+
## Status
|
|
244
|
+
|
|
245
|
+
Pre-1.0: the CLI surface and JSON schema can still move between minor versions;
|
|
246
|
+
`CHANGELOG.md` records what changed.
|
|
247
|
+
|
|
248
|
+
Output has been checked against gpt-4o-mini, gemini-2.5-flash, claude-haiku-4.5 and
|
|
249
|
+
llama-3.3-70b through OpenRouter, plus venice-uncensored-1-2 and
|
|
250
|
+
qwen3-235b-a22b-instruct on Venice. All six held the JSON contract and used distinct
|
|
251
|
+
channels. Weak models will still need prompt tuning, and the suite covers the
|
|
252
|
+
pipeline rather than any one model's prose.
|
|
253
|
+
|
|
254
|
+
Not planned: delivery to targets, click tracking, an SMTP client. pretext writes the
|
|
255
|
+
exercise; sending stays your job.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pretext/__init__.py,sha256=b2SsiOk-VyOt7PxcuorCfQ7JSP5_geKBwAbSlWbyQfk,590
|
|
2
|
+
pretext/cli.py,sha256=wp07Fg_DkeFIHnNfkbtfHbF0NWQAKGYH0_enK9WFZZg,20199
|
|
3
|
+
pretext/client.py,sha256=sNFeBZLUzKg5AcVrwGscBE6lwS6e73e1Kbb12164Yrw,8143
|
|
4
|
+
pretext/models.py,sha256=tmjPrqbswtxoybeOZQ9mI1W90gl9SdYA6QeuPvTu-5s,10474
|
|
5
|
+
pretext/prompt.py,sha256=yQ49GF3F9jD_-xW_FiHCfW04Aq6bWNnVcHxVPztdb30,5559
|
|
6
|
+
pretext/render.py,sha256=IYT6h6mO4TWKYJBHAOMjWAS1IqcFH6upHHZqoHL0730,8302
|
|
7
|
+
pretext/data/example-profile.yaml,sha256=4Lspy5Wh0NH4XMUAo05Pd0RHbBWPxfVoVY5svqMd4Ac,419
|
|
8
|
+
pretext_sim-0.3.2.dist-info/METADATA,sha256=uWhfeJOP6S_dRou4SVoy22OfdvVpJDbzWZV6yhOFbKE,10195
|
|
9
|
+
pretext_sim-0.3.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
pretext_sim-0.3.2.dist-info/entry_points.txt,sha256=hbPonwsPJ7IZoLDhG_VEcLa2kvWNe7ZYpE9pjCvz4L8,44
|
|
11
|
+
pretext_sim-0.3.2.dist-info/licenses/LICENSE,sha256=ax7hQ3Ou5brcygpbq86G4ndIiwolij1Fhs1RHVBdfZ0,1077
|
|
12
|
+
pretext_sim-0.3.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pretext contributors
|
|
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.
|