killchain 0.1.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.
- killchain-0.1.0/.gitignore +9 -0
- killchain-0.1.0/LICENSE +21 -0
- killchain-0.1.0/PKG-INFO +268 -0
- killchain-0.1.0/README.md +244 -0
- killchain-0.1.0/killchain/__init__.py +10 -0
- killchain-0.1.0/killchain/__main__.py +5 -0
- killchain-0.1.0/killchain/config.toml +78 -0
- killchain-0.1.0/killchain/contract.py +154 -0
- killchain-0.1.0/killchain/detector.py +180 -0
- killchain-0.1.0/killchain/doctor.py +164 -0
- killchain-0.1.0/killchain/fakes.py +182 -0
- killchain-0.1.0/killchain/frontier.py +63 -0
- killchain-0.1.0/killchain/gate.py +99 -0
- killchain-0.1.0/killchain/harness.py +377 -0
- killchain-0.1.0/killchain/localmodel.py +74 -0
- killchain-0.1.0/killchain/paths.py +277 -0
- killchain-0.1.0/killchain/prompts/frontier_build.md +34 -0
- killchain-0.1.0/killchain/prompts/local_implement.md +36 -0
- killchain-0.1.0/killchain/prompts/partition.md +100 -0
- killchain-0.1.0/killchain/routing.py +310 -0
- killchain-0.1.0/killchain/stubgen.py +151 -0
- killchain-0.1.0/pyproject.toml +45 -0
killchain-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Josh Simmons
|
|
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.
|
killchain-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: killchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Split a build so a frontier coding agent never sees the risky half.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jcpsimmons/killchain
|
|
6
|
+
Project-URL: Repository, https://github.com/jcpsimmons/killchain
|
|
7
|
+
Project-URL: Issues, https://github.com/jcpsimmons/killchain/issues
|
|
8
|
+
Project-URL: YouTube, https://youtube.com/@drjoshcsimmons
|
|
9
|
+
Author: Josh Simmons
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,claude-code,codegen,codex,partitioning
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# killchain: refusal-first split build
|
|
26
|
+
|
|
27
|
+
An abliterated orchestrator model (refuseless by default, settings mirrored
|
|
28
|
+
from `~/.hermes/config.yaml`) is the front door. It is the only model that
|
|
29
|
+
sees the full prompt. It splits the job into a **public half** (built by a
|
|
30
|
+
frontier coding agent: Claude Code or Codex, which may fan out to its own
|
|
31
|
+
subagents) and a **private half** (implemented by the orchestrator itself),
|
|
32
|
+
joined by a contract.
|
|
33
|
+
|
|
34
|
+
The partition rule is **refusal risk, nothing else**. Claude and Codex are good
|
|
35
|
+
and fast, but they hedge, caveat, simplify, or refuse when a task looks
|
|
36
|
+
reverse-engineered, unlicensed, regulated, exploitative, or oversized. Those
|
|
37
|
+
parts go to the uncensored model. The frontier gets the rest of the app and
|
|
38
|
+
never learns how the interesting bits work, so it has no position to take.
|
|
39
|
+
|
|
40
|
+
It is not a cost trick and not only a quality trick: a brief that says "match
|
|
41
|
+
the behaviour of X" gets you `from memory` disclaimers and invented constants.
|
|
42
|
+
A brief that says "given these fields return a float in [0, 1], monotonic in
|
|
43
|
+
both" gets you working code.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Python 3.11+, no dependencies. Pick one:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv tool install killchain # installs the `killchain` command
|
|
51
|
+
pipx install killchain # same, if you use pipx
|
|
52
|
+
pip install killchain # into the current environment
|
|
53
|
+
uvx killchain "your task" # zero-install trial, no venv needed
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From a checkout, before it is on an index:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv tool install git+https://github.com/jcpsimmons/killchain
|
|
60
|
+
# or, for development:
|
|
61
|
+
uv venv && uv pip install -e ".[dev]"
|
|
62
|
+
python harness.py --version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then find out what is missing before you waste a run:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
killchain --doctor
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`doctor` checks your config, the `claude`/`codex` binary, both API keys, the
|
|
72
|
+
private directory mode, the denylist, and does one live round-trip to each
|
|
73
|
+
endpoint. It exits non-zero only if something fatal to a run is wrong.
|
|
74
|
+
|
|
75
|
+
## First run
|
|
76
|
+
|
|
77
|
+
Nothing is written into the directory you installed to. On first use killchain
|
|
78
|
+
creates:
|
|
79
|
+
|
|
80
|
+
| Path | What |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `~/.config/killchain/config.toml` | your config, seeded from the shipped default |
|
|
83
|
+
| `~/.config/killchain/denylist.txt` | literal terms and `re:` patterns the gate rejects |
|
|
84
|
+
| `~/.local/share/killchain/workspace/` | public halves, one directory per run |
|
|
85
|
+
| `~/killchain-private/` | mode 0700: raw prompt, contract, `private_impl/`, risk report |
|
|
86
|
+
|
|
87
|
+
Override with `$XDG_CONFIG_HOME`, `$XDG_DATA_HOME`, `$KILLCHAIN_CONFIG`,
|
|
88
|
+
`$KILLCHAIN_PRIVATE_ROOT`, or per run with `--private-root` / `--workspace`.
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
prompt ─► orchestrator ─► contract.json (brief + interfaces + decoy fixtures + private fixtures + notes)
|
|
94
|
+
│
|
|
95
|
+
├─ risk score (8 dimensions, per span) ┐ on the PUBLIC VIEW, plus
|
|
96
|
+
├─ whole-view pass (is the task itself scary?) ┘ flagged spans go back to orchestrator
|
|
97
|
+
├─ regex gate + framing hints
|
|
98
|
+
│
|
|
99
|
+
├─► public/ contracts/ stubs + tests/test_contracts.py ─► frontier agent builds app + tests
|
|
100
|
+
│ (HARNESS_MODE=stub: fixtures answer)
|
|
101
|
+
└─► private/ prompt, contract, private_impl/ ◄─ orchestrator implements to contract
|
|
102
|
+
(HARNESS_MODE=real: tests prove the fit)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Where to edit the routing
|
|
106
|
+
|
|
107
|
+
**[killchain/routing.py](https://github.com/jcpsimmons/killchain/blob/main/killchain/routing.py)** is the one file that decides what the frontier is
|
|
108
|
+
allowed to see. Everything else is plumbing that imports from it:
|
|
109
|
+
|
|
110
|
+
| In routing.py | What it controls |
|
|
111
|
+
|-------------------|-----------------------------------------------------------------------|
|
|
112
|
+
| `DIMENSIONS` | the refusal dimensions, and the argument for each. Their `question` text is what Jev is actually asked |
|
|
113
|
+
| `SAFE` | what a frontier agent builds without comment; the "false" criteria |
|
|
114
|
+
| `HARD_RULES` | regexes that block a send outright, no model involved |
|
|
115
|
+
| `FRAMING_HINTS` | non-blocking warnings for clone/hedge/licence wording |
|
|
116
|
+
| `THRESHOLDS` | risk probability needed to pull a span, per dimension and span kind |
|
|
117
|
+
| `classify()` | the final per-span decision; any dimension over its bar routes private |
|
|
118
|
+
| `pick_frontier()` | which coding agent builds the public half, by content if you like |
|
|
119
|
+
| `split_prompt()` | how the raw prompt is cut into spans for the risk map |
|
|
120
|
+
|
|
121
|
+
The denylist (`~/.config/killchain/denylist.txt`) adds literal and regex rules
|
|
122
|
+
without touching any code. It carries two things: secrets that must never
|
|
123
|
+
leave, and terms that tip the frontier off (product names, licence names,
|
|
124
|
+
codenames).
|
|
125
|
+
|
|
126
|
+
The shipped dimensions are a starting taxonomy, not a law: attribution and
|
|
127
|
+
clone framing, reverse engineering, offensive security, extraction and ToS,
|
|
128
|
+
regulated domains, another vendor's model or SDK internals, hedge wording, and
|
|
129
|
+
scope an agent will negotiate away. Add a dimension when you find a refusal
|
|
130
|
+
that none of them predict.
|
|
131
|
+
|
|
132
|
+
## Trust boundaries
|
|
133
|
+
|
|
134
|
+
To the **orchestrator (refuseless)**: the full prompt, the contract, and the
|
|
135
|
+
private implementation loop. This is the half that is supposed to know
|
|
136
|
+
everything.
|
|
137
|
+
|
|
138
|
+
To the **frontier agent**: only `contract.public.json`, the public brief,
|
|
139
|
+
interface signatures and descriptions, and decoy fixtures. It is told the
|
|
140
|
+
private functions are a black box it should not infer, reimplement, or ask
|
|
141
|
+
about.
|
|
142
|
+
|
|
143
|
+
To **OpenRouter/TypeSafe (Jev)**: the public view span by span, plus, when
|
|
144
|
+
`probe_raw_prompt = true` (default), raw prompt spans. The denylist terms ride
|
|
145
|
+
along in every criteria block either way. Turn scoring off entirely with
|
|
146
|
+
`--no-risk` if OpenRouter is out of bounds.
|
|
147
|
+
|
|
148
|
+
The raw prompt, `private_notes`, `private_plan`, and `private_impl/` live under
|
|
149
|
+
`paths.private_root` (default `~/killchain-private/`), never the frontier's cwd or
|
|
150
|
+
on its PYTHONPATH, and the frontier subprocess gets an environment scrubbed of
|
|
151
|
+
anything pointing there. The frontier runs with permissions skipped, so treat
|
|
152
|
+
that boundary as a convention, not a jail.
|
|
153
|
+
|
|
154
|
+
## Layers of protection on the outbound view
|
|
155
|
+
|
|
156
|
+
1. **Orchestrator partition** (semantic): told the rule is refusal risk, shown
|
|
157
|
+
the dimensions, told to rewrite the framing rather than only move content.
|
|
158
|
+
2. **Regex gate** (deterministic): denylist literals and regexes plus built-in
|
|
159
|
+
secret patterns. Any hit blocks the send.
|
|
160
|
+
3. **Risk scorer** (probabilistic): `typesafe/jev-1.14` via the OpenRouter
|
|
161
|
+
Decisions API, one `noul` question per dimension per span, criteria from
|
|
162
|
+
routing.py. Then a whole-view pass, because a refusal is triggered by the
|
|
163
|
+
shape of a task and every individual span can be innocent.
|
|
164
|
+
4. **Human review**: the outbound view, the decoy fixtures, and the span
|
|
165
|
+
scores are printed and you confirm, unless `--yes`.
|
|
166
|
+
|
|
167
|
+
Rejections from 2 and 3 are fed back to the orchestrator with the exact spans,
|
|
168
|
+
so it re-partitions and re-frames.
|
|
169
|
+
|
|
170
|
+
**Probes.** Before it partitions, the harness scores the raw prompt span by
|
|
171
|
+
span and puts the resulting risk map in the partition prompt. The orchestrator
|
|
172
|
+
can also reply `{"probes": ["exact text", ...]}` up to `max_probe_rounds`
|
|
173
|
+
times to score any phrasing it is unsure about before committing.
|
|
174
|
+
|
|
175
|
+
## Run
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
killchain "build X using our proprietary Y"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
killchain -f prompt.md --frontier codex --yes
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Flags: `--frontier claude|codex|fake`, `--orch-url`, `--risk-url` (alias
|
|
186
|
+
`--detector-url`), `--no-risk`/`--no-detector`, `--run-id`, `--skip-frontier`
|
|
187
|
+
(public already built, redo private), `--skip-private` (stop after frontier),
|
|
188
|
+
`--private-root`, `--workspace`.
|
|
189
|
+
|
|
190
|
+
Offline end-to-end with canned backends (exercises both feedback loops and the
|
|
191
|
+
whole-view pass):
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
killchain "rank leads" --orch-url fake --frontier fake --detector-url fake --yes
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Then run the result with both halves together:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cd <workspace>/<run>/public && HARNESS_MODE=real PYTHONPATH=~/killchain-private/<run> python -m app.<entry>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Config
|
|
204
|
+
|
|
205
|
+
Edit `~/.config/killchain/config.toml` (or pass `-c FILE`). `[orchestrator]` is
|
|
206
|
+
any OpenAI-compatible chat endpoint; default
|
|
207
|
+
is refuseless with the key read from `$REFUSELESS_API_KEY` or from
|
|
208
|
+
`~/.hermes/.env` at runtime (never stored in the repo). `[frontier]` picks
|
|
209
|
+
`claude` or `codex` and their flags (overridable per run by
|
|
210
|
+
`routing.pick_frontier`). `[risk]` holds the Jev endpoint and model and needs
|
|
211
|
+
`OPENROUTER_API_KEY`; dimensions and thresholds are in routing.py. `[paths]`
|
|
212
|
+
sets the workspace, the private root and an optional denylist location.
|
|
213
|
+
|
|
214
|
+
## Contract shape
|
|
215
|
+
|
|
216
|
+
Each interface is one Python function in `contracts/<module>.py`, generated
|
|
217
|
+
from the contract and never edited by either side. In stub mode it answers from
|
|
218
|
+
`_fixtures.json` (exact input match, else first fixture). In real mode it
|
|
219
|
+
delegates to `private_impl.<module>.<function>`.
|
|
220
|
+
|
|
221
|
+
Two fixture sets per interface. **Public fixtures** are decoys: right shape,
|
|
222
|
+
wrong values, never computed with the real logic, because a few correct
|
|
223
|
+
input/output points would let the frontier vendor fit the rule. They drive stub
|
|
224
|
+
mode. **Private fixtures** are computed with the real logic, live only in the
|
|
225
|
+
private run dir (`private_fixtures.json`), and drive `tests/test_contracts.py`
|
|
226
|
+
in real mode via `HARNESS_FIXTURES`. Passing there is the integration proof.
|
|
227
|
+
The harness rejects a public fixture that equals a private one.
|
|
228
|
+
|
|
229
|
+
## Limits
|
|
230
|
+
|
|
231
|
+
Jev predicts refusal; it does not measure it. Refusals are stochastic and
|
|
232
|
+
version-dependent, and a model can still balk at something no dimension
|
|
233
|
+
predicted. Two things mitigate that: the framing rules in `prompts/partition.md`
|
|
234
|
+
(describe behaviour, not lineage) and `NOTES.md` from the frontier, which
|
|
235
|
+
tells the private side the edge cases it expects. Re-run a partition when the
|
|
236
|
+
frontier ships a new model.
|
|
237
|
+
|
|
238
|
+
v1 limits: Python interfaces only; the orchestrator writes files by returning
|
|
239
|
+
JSON (no tool use needed on its side); one contract round, no renegotiation
|
|
240
|
+
mid-build.
|
|
241
|
+
|
|
242
|
+
## Why this exists
|
|
243
|
+
|
|
244
|
+
I keep handing the good model work it will not do. The build comes back with a
|
|
245
|
+
polite paragraph about what it cannot help with and a function that returns a
|
|
246
|
+
placeholder. So I drop to something weaker and looser, which writes the hard
|
|
247
|
+
part badly and the easy part badly too, and now I am debugging two models
|
|
248
|
+
instead of one.
|
|
249
|
+
|
|
250
|
+
The thing I actually wanted was to hand the boring 90% to the model that is
|
|
251
|
+
good at boring, and hand the part it flinches at to one that does not flinch.
|
|
252
|
+
That is all this does. No jailbreak, no prompt poem, no arguing with anything.
|
|
253
|
+
You know which half of your own project is the risky half. Say so once, and
|
|
254
|
+
each model gets the half it is best suited to.
|
|
255
|
+
|
|
256
|
+
Frame a task by what it does and Claude Code will build it. Frame it by what it
|
|
257
|
+
resembles and you get disclaimers and made-up numbers. That difference is not a
|
|
258
|
+
bug in the model. It is a difference in how you asked.
|
|
259
|
+
|
|
260
|
+
I write about building with these things, and about the parts of the industry
|
|
261
|
+
that deserve a harder look, over at
|
|
262
|
+
**[youtube.com/@drjoshcsimmons](https://youtube.com/@drjoshcsimmons)**. Issues
|
|
263
|
+
and war stories are welcome in the
|
|
264
|
+
[repo](https://github.com/jcpsimmons/killchain).
|
|
265
|
+
|
|
266
|
+
MIT licensed.
|
|
267
|
+
|
|
268
|
+
- Dr. J
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# killchain: refusal-first split build
|
|
2
|
+
|
|
3
|
+
An abliterated orchestrator model (refuseless by default, settings mirrored
|
|
4
|
+
from `~/.hermes/config.yaml`) is the front door. It is the only model that
|
|
5
|
+
sees the full prompt. It splits the job into a **public half** (built by a
|
|
6
|
+
frontier coding agent: Claude Code or Codex, which may fan out to its own
|
|
7
|
+
subagents) and a **private half** (implemented by the orchestrator itself),
|
|
8
|
+
joined by a contract.
|
|
9
|
+
|
|
10
|
+
The partition rule is **refusal risk, nothing else**. Claude and Codex are good
|
|
11
|
+
and fast, but they hedge, caveat, simplify, or refuse when a task looks
|
|
12
|
+
reverse-engineered, unlicensed, regulated, exploitative, or oversized. Those
|
|
13
|
+
parts go to the uncensored model. The frontier gets the rest of the app and
|
|
14
|
+
never learns how the interesting bits work, so it has no position to take.
|
|
15
|
+
|
|
16
|
+
It is not a cost trick and not only a quality trick: a brief that says "match
|
|
17
|
+
the behaviour of X" gets you `from memory` disclaimers and invented constants.
|
|
18
|
+
A brief that says "given these fields return a float in [0, 1], monotonic in
|
|
19
|
+
both" gets you working code.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
Python 3.11+, no dependencies. Pick one:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install killchain # installs the `killchain` command
|
|
27
|
+
pipx install killchain # same, if you use pipx
|
|
28
|
+
pip install killchain # into the current environment
|
|
29
|
+
uvx killchain "your task" # zero-install trial, no venv needed
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
From a checkout, before it is on an index:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv tool install git+https://github.com/jcpsimmons/killchain
|
|
36
|
+
# or, for development:
|
|
37
|
+
uv venv && uv pip install -e ".[dev]"
|
|
38
|
+
python harness.py --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then find out what is missing before you waste a run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
killchain --doctor
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`doctor` checks your config, the `claude`/`codex` binary, both API keys, the
|
|
48
|
+
private directory mode, the denylist, and does one live round-trip to each
|
|
49
|
+
endpoint. It exits non-zero only if something fatal to a run is wrong.
|
|
50
|
+
|
|
51
|
+
## First run
|
|
52
|
+
|
|
53
|
+
Nothing is written into the directory you installed to. On first use killchain
|
|
54
|
+
creates:
|
|
55
|
+
|
|
56
|
+
| Path | What |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `~/.config/killchain/config.toml` | your config, seeded from the shipped default |
|
|
59
|
+
| `~/.config/killchain/denylist.txt` | literal terms and `re:` patterns the gate rejects |
|
|
60
|
+
| `~/.local/share/killchain/workspace/` | public halves, one directory per run |
|
|
61
|
+
| `~/killchain-private/` | mode 0700: raw prompt, contract, `private_impl/`, risk report |
|
|
62
|
+
|
|
63
|
+
Override with `$XDG_CONFIG_HOME`, `$XDG_DATA_HOME`, `$KILLCHAIN_CONFIG`,
|
|
64
|
+
`$KILLCHAIN_PRIVATE_ROOT`, or per run with `--private-root` / `--workspace`.
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
prompt ─► orchestrator ─► contract.json (brief + interfaces + decoy fixtures + private fixtures + notes)
|
|
70
|
+
│
|
|
71
|
+
├─ risk score (8 dimensions, per span) ┐ on the PUBLIC VIEW, plus
|
|
72
|
+
├─ whole-view pass (is the task itself scary?) ┘ flagged spans go back to orchestrator
|
|
73
|
+
├─ regex gate + framing hints
|
|
74
|
+
│
|
|
75
|
+
├─► public/ contracts/ stubs + tests/test_contracts.py ─► frontier agent builds app + tests
|
|
76
|
+
│ (HARNESS_MODE=stub: fixtures answer)
|
|
77
|
+
└─► private/ prompt, contract, private_impl/ ◄─ orchestrator implements to contract
|
|
78
|
+
(HARNESS_MODE=real: tests prove the fit)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Where to edit the routing
|
|
82
|
+
|
|
83
|
+
**[killchain/routing.py](https://github.com/jcpsimmons/killchain/blob/main/killchain/routing.py)** is the one file that decides what the frontier is
|
|
84
|
+
allowed to see. Everything else is plumbing that imports from it:
|
|
85
|
+
|
|
86
|
+
| In routing.py | What it controls |
|
|
87
|
+
|-------------------|-----------------------------------------------------------------------|
|
|
88
|
+
| `DIMENSIONS` | the refusal dimensions, and the argument for each. Their `question` text is what Jev is actually asked |
|
|
89
|
+
| `SAFE` | what a frontier agent builds without comment; the "false" criteria |
|
|
90
|
+
| `HARD_RULES` | regexes that block a send outright, no model involved |
|
|
91
|
+
| `FRAMING_HINTS` | non-blocking warnings for clone/hedge/licence wording |
|
|
92
|
+
| `THRESHOLDS` | risk probability needed to pull a span, per dimension and span kind |
|
|
93
|
+
| `classify()` | the final per-span decision; any dimension over its bar routes private |
|
|
94
|
+
| `pick_frontier()` | which coding agent builds the public half, by content if you like |
|
|
95
|
+
| `split_prompt()` | how the raw prompt is cut into spans for the risk map |
|
|
96
|
+
|
|
97
|
+
The denylist (`~/.config/killchain/denylist.txt`) adds literal and regex rules
|
|
98
|
+
without touching any code. It carries two things: secrets that must never
|
|
99
|
+
leave, and terms that tip the frontier off (product names, licence names,
|
|
100
|
+
codenames).
|
|
101
|
+
|
|
102
|
+
The shipped dimensions are a starting taxonomy, not a law: attribution and
|
|
103
|
+
clone framing, reverse engineering, offensive security, extraction and ToS,
|
|
104
|
+
regulated domains, another vendor's model or SDK internals, hedge wording, and
|
|
105
|
+
scope an agent will negotiate away. Add a dimension when you find a refusal
|
|
106
|
+
that none of them predict.
|
|
107
|
+
|
|
108
|
+
## Trust boundaries
|
|
109
|
+
|
|
110
|
+
To the **orchestrator (refuseless)**: the full prompt, the contract, and the
|
|
111
|
+
private implementation loop. This is the half that is supposed to know
|
|
112
|
+
everything.
|
|
113
|
+
|
|
114
|
+
To the **frontier agent**: only `contract.public.json`, the public brief,
|
|
115
|
+
interface signatures and descriptions, and decoy fixtures. It is told the
|
|
116
|
+
private functions are a black box it should not infer, reimplement, or ask
|
|
117
|
+
about.
|
|
118
|
+
|
|
119
|
+
To **OpenRouter/TypeSafe (Jev)**: the public view span by span, plus, when
|
|
120
|
+
`probe_raw_prompt = true` (default), raw prompt spans. The denylist terms ride
|
|
121
|
+
along in every criteria block either way. Turn scoring off entirely with
|
|
122
|
+
`--no-risk` if OpenRouter is out of bounds.
|
|
123
|
+
|
|
124
|
+
The raw prompt, `private_notes`, `private_plan`, and `private_impl/` live under
|
|
125
|
+
`paths.private_root` (default `~/killchain-private/`), never the frontier's cwd or
|
|
126
|
+
on its PYTHONPATH, and the frontier subprocess gets an environment scrubbed of
|
|
127
|
+
anything pointing there. The frontier runs with permissions skipped, so treat
|
|
128
|
+
that boundary as a convention, not a jail.
|
|
129
|
+
|
|
130
|
+
## Layers of protection on the outbound view
|
|
131
|
+
|
|
132
|
+
1. **Orchestrator partition** (semantic): told the rule is refusal risk, shown
|
|
133
|
+
the dimensions, told to rewrite the framing rather than only move content.
|
|
134
|
+
2. **Regex gate** (deterministic): denylist literals and regexes plus built-in
|
|
135
|
+
secret patterns. Any hit blocks the send.
|
|
136
|
+
3. **Risk scorer** (probabilistic): `typesafe/jev-1.14` via the OpenRouter
|
|
137
|
+
Decisions API, one `noul` question per dimension per span, criteria from
|
|
138
|
+
routing.py. Then a whole-view pass, because a refusal is triggered by the
|
|
139
|
+
shape of a task and every individual span can be innocent.
|
|
140
|
+
4. **Human review**: the outbound view, the decoy fixtures, and the span
|
|
141
|
+
scores are printed and you confirm, unless `--yes`.
|
|
142
|
+
|
|
143
|
+
Rejections from 2 and 3 are fed back to the orchestrator with the exact spans,
|
|
144
|
+
so it re-partitions and re-frames.
|
|
145
|
+
|
|
146
|
+
**Probes.** Before it partitions, the harness scores the raw prompt span by
|
|
147
|
+
span and puts the resulting risk map in the partition prompt. The orchestrator
|
|
148
|
+
can also reply `{"probes": ["exact text", ...]}` up to `max_probe_rounds`
|
|
149
|
+
times to score any phrasing it is unsure about before committing.
|
|
150
|
+
|
|
151
|
+
## Run
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
killchain "build X using our proprietary Y"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
killchain -f prompt.md --frontier codex --yes
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Flags: `--frontier claude|codex|fake`, `--orch-url`, `--risk-url` (alias
|
|
162
|
+
`--detector-url`), `--no-risk`/`--no-detector`, `--run-id`, `--skip-frontier`
|
|
163
|
+
(public already built, redo private), `--skip-private` (stop after frontier),
|
|
164
|
+
`--private-root`, `--workspace`.
|
|
165
|
+
|
|
166
|
+
Offline end-to-end with canned backends (exercises both feedback loops and the
|
|
167
|
+
whole-view pass):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
killchain "rank leads" --orch-url fake --frontier fake --detector-url fake --yes
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Then run the result with both halves together:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
cd <workspace>/<run>/public && HARNESS_MODE=real PYTHONPATH=~/killchain-private/<run> python -m app.<entry>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Config
|
|
180
|
+
|
|
181
|
+
Edit `~/.config/killchain/config.toml` (or pass `-c FILE`). `[orchestrator]` is
|
|
182
|
+
any OpenAI-compatible chat endpoint; default
|
|
183
|
+
is refuseless with the key read from `$REFUSELESS_API_KEY` or from
|
|
184
|
+
`~/.hermes/.env` at runtime (never stored in the repo). `[frontier]` picks
|
|
185
|
+
`claude` or `codex` and their flags (overridable per run by
|
|
186
|
+
`routing.pick_frontier`). `[risk]` holds the Jev endpoint and model and needs
|
|
187
|
+
`OPENROUTER_API_KEY`; dimensions and thresholds are in routing.py. `[paths]`
|
|
188
|
+
sets the workspace, the private root and an optional denylist location.
|
|
189
|
+
|
|
190
|
+
## Contract shape
|
|
191
|
+
|
|
192
|
+
Each interface is one Python function in `contracts/<module>.py`, generated
|
|
193
|
+
from the contract and never edited by either side. In stub mode it answers from
|
|
194
|
+
`_fixtures.json` (exact input match, else first fixture). In real mode it
|
|
195
|
+
delegates to `private_impl.<module>.<function>`.
|
|
196
|
+
|
|
197
|
+
Two fixture sets per interface. **Public fixtures** are decoys: right shape,
|
|
198
|
+
wrong values, never computed with the real logic, because a few correct
|
|
199
|
+
input/output points would let the frontier vendor fit the rule. They drive stub
|
|
200
|
+
mode. **Private fixtures** are computed with the real logic, live only in the
|
|
201
|
+
private run dir (`private_fixtures.json`), and drive `tests/test_contracts.py`
|
|
202
|
+
in real mode via `HARNESS_FIXTURES`. Passing there is the integration proof.
|
|
203
|
+
The harness rejects a public fixture that equals a private one.
|
|
204
|
+
|
|
205
|
+
## Limits
|
|
206
|
+
|
|
207
|
+
Jev predicts refusal; it does not measure it. Refusals are stochastic and
|
|
208
|
+
version-dependent, and a model can still balk at something no dimension
|
|
209
|
+
predicted. Two things mitigate that: the framing rules in `prompts/partition.md`
|
|
210
|
+
(describe behaviour, not lineage) and `NOTES.md` from the frontier, which
|
|
211
|
+
tells the private side the edge cases it expects. Re-run a partition when the
|
|
212
|
+
frontier ships a new model.
|
|
213
|
+
|
|
214
|
+
v1 limits: Python interfaces only; the orchestrator writes files by returning
|
|
215
|
+
JSON (no tool use needed on its side); one contract round, no renegotiation
|
|
216
|
+
mid-build.
|
|
217
|
+
|
|
218
|
+
## Why this exists
|
|
219
|
+
|
|
220
|
+
I keep handing the good model work it will not do. The build comes back with a
|
|
221
|
+
polite paragraph about what it cannot help with and a function that returns a
|
|
222
|
+
placeholder. So I drop to something weaker and looser, which writes the hard
|
|
223
|
+
part badly and the easy part badly too, and now I am debugging two models
|
|
224
|
+
instead of one.
|
|
225
|
+
|
|
226
|
+
The thing I actually wanted was to hand the boring 90% to the model that is
|
|
227
|
+
good at boring, and hand the part it flinches at to one that does not flinch.
|
|
228
|
+
That is all this does. No jailbreak, no prompt poem, no arguing with anything.
|
|
229
|
+
You know which half of your own project is the risky half. Say so once, and
|
|
230
|
+
each model gets the half it is best suited to.
|
|
231
|
+
|
|
232
|
+
Frame a task by what it does and Claude Code will build it. Frame it by what it
|
|
233
|
+
resembles and you get disclaimers and made-up numbers. That difference is not a
|
|
234
|
+
bug in the model. It is a difference in how you asked.
|
|
235
|
+
|
|
236
|
+
I write about building with these things, and about the parts of the industry
|
|
237
|
+
that deserve a harder look, over at
|
|
238
|
+
**[youtube.com/@drjoshcsimmons](https://youtube.com/@drjoshcsimmons)**. Issues
|
|
239
|
+
and war stories are welcome in the
|
|
240
|
+
[repo](https://github.com/jcpsimmons/killchain).
|
|
241
|
+
|
|
242
|
+
MIT licensed.
|
|
243
|
+
|
|
244
|
+
- Dr. J
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""killchain: split a build so a frontier agent never sees the risky half.
|
|
2
|
+
|
|
3
|
+
The prompt is partitioned by *refusal risk*, not confidentiality. Anything a
|
|
4
|
+
commercial coding agent would refuse, hedge, simplify, or guess at is routed to
|
|
5
|
+
an abliterated orchestrator model, which then implements it locally. A frontier
|
|
6
|
+
agent (Claude Code, Codex) builds the rest against stubs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# killchain configuration. Paths may use ~.
|
|
2
|
+
#
|
|
3
|
+
# This file is the shipped default. On first run it is copied to
|
|
4
|
+
# ~/.config/killchain/config.toml, which is what you should edit.
|
|
5
|
+
|
|
6
|
+
[orchestrator]
|
|
7
|
+
# The abliterated orchestrator model: the only model that sees the full prompt.
|
|
8
|
+
# Any OpenAI-compatible chat endpoint. Default: refuseless, settings mirrored
|
|
9
|
+
# from ~/.hermes/config.yaml. The key is read from $REFUSELESS_API_KEY, or
|
|
10
|
+
# from env_file if that is unset. It is never written to disk by killchain.
|
|
11
|
+
base_url = "https://api.refuseless.com/v1"
|
|
12
|
+
model = "deepseek-v4.1-flash-abliterated" # also: glm-5.3-abliterated, glm-5.3-flash-abliterated
|
|
13
|
+
api_key_env = "REFUSELESS_API_KEY"
|
|
14
|
+
env_file = "~/.hermes/.env"
|
|
15
|
+
timeout = 900
|
|
16
|
+
max_partition_tries = 4
|
|
17
|
+
max_impl_tries = 4
|
|
18
|
+
|
|
19
|
+
[frontier]
|
|
20
|
+
backend = "claude" # claude | codex | fake
|
|
21
|
+
claude_model = "" # blank = CLI default
|
|
22
|
+
claude_extra_args = ["--dangerously-skip-permissions"] # it runs confined to the public dir
|
|
23
|
+
codex_model = ""
|
|
24
|
+
codex_extra_args = ["--full-auto"]
|
|
25
|
+
timeout = 3600
|
|
26
|
+
|
|
27
|
+
[risk]
|
|
28
|
+
# Jev (TypeSafe) via the OpenRouter Decisions API. Scores each span of the
|
|
29
|
+
# candidate public view on every refusal dimension in routing.py. Everything
|
|
30
|
+
# scored is sent to OpenRouter, so scoring happens on the public view and, if
|
|
31
|
+
# score_raw_prompt is on, on spans of the raw prompt.
|
|
32
|
+
enabled = true
|
|
33
|
+
base_url = "https://openrouter.ai/api/alpha/decisions"
|
|
34
|
+
model = "typesafe/jev-1.14"
|
|
35
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
36
|
+
# Dimensions, thresholds and the per-span decision live in routing.py.
|
|
37
|
+
timeout = 60
|
|
38
|
+
# Pre-score the RAW PROMPT span by span to build the risk map, and let the
|
|
39
|
+
# orchestrator ask for extra probes on subsets. Refusal risk is carried by the
|
|
40
|
+
# original phrasing, so this is the useful mode. All of it goes to
|
|
41
|
+
# OpenRouter/TypeSafe; turn off to score only the assembled public view.
|
|
42
|
+
probe_raw_prompt = true
|
|
43
|
+
max_probe_rounds = 3
|
|
44
|
+
|
|
45
|
+
[routing]
|
|
46
|
+
# Shipped defaults live in killchain/routing.py; anything set here wins. Read
|
|
47
|
+
# routing.py first - these change what the frontier model is allowed to build.
|
|
48
|
+
# Nothing here is required; the section exists so retuning needs no code edit.
|
|
49
|
+
|
|
50
|
+
# Drop a dimension entirely (it stops being asked and stops routing spans).
|
|
51
|
+
# disabled_dimensions = ["hedge_framing"]
|
|
52
|
+
|
|
53
|
+
# Per-dimension bar, 0..1: the risk probability needed to pull a span out of
|
|
54
|
+
# the public view. Lower = more paranoid (more goes private).
|
|
55
|
+
# Valid keys: attribution, reverse_engineering, offensive_security, extraction,
|
|
56
|
+
# regulated, proprietary_model, hedge_framing, scale_or_scope,
|
|
57
|
+
# overall, default
|
|
58
|
+
# [routing.thresholds]
|
|
59
|
+
# hedge_framing = 0.75
|
|
60
|
+
# attribution = 0.40
|
|
61
|
+
|
|
62
|
+
# Adjustment per span kind, added to the bar. Fixtures are bare JSON numbers
|
|
63
|
+
# and score near 0.5 whatever they contain, so they get a higher bar.
|
|
64
|
+
# [routing.kind_slack]
|
|
65
|
+
# fixture = 0.15
|
|
66
|
+
|
|
67
|
+
# Extra regexes that block an outbound view outright, no model involved.
|
|
68
|
+
# Prefix with re: for a regex, otherwise it is a literal. Prefer the denylist
|
|
69
|
+
# file for terms; use this for patterns that should ship with your team's config.
|
|
70
|
+
# hard_rules = ["TICKER-[A-Z]{4}", { pattern = "re:\\bsk-[a-z0-9]{20,}", why = "looks like an API key" }]
|
|
71
|
+
|
|
72
|
+
[paths]
|
|
73
|
+
# Defaults are user directories so an installed command never writes into
|
|
74
|
+
# site-packages. Override with env vars (KILLCHAIN_PRIVATE_ROOT) or per run
|
|
75
|
+
# with --workspace / --private-root. Relative paths resolve against the cwd.
|
|
76
|
+
workspace = "" # blank = <project>/.killchain/workspace, else ~/.local/share/killchain/workspace
|
|
77
|
+
private_root = "~/killchain-private" # mode 0700: prompts, contracts, private impls
|
|
78
|
+
denylist = "" # blank = ~/.config/killchain/denylist.txt
|