killchain 0.1.0__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.
killchain/fakes.py ADDED
@@ -0,0 +1,182 @@
1
+ """Canned backends so the whole pipeline can be exercised with no models.
2
+
3
+ Toy task: rank inbound leads from a CSV. Framing is the thing under test: the
4
+ fake orchestrator keeps leaking attribution into the public brief. It trips the
5
+ denylist gate on attempt 1 and a refusal dimension on attempt 2, so both
6
+ feedback loops (and the marker the detector keys on) are covered.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from killchain import routing
14
+
15
+ CONTRACT = {
16
+ "public_brief": (
17
+ "Build `leadrank`, a stdlib-only Python CLI that reads a CSV of inbound leads "
18
+ "and prints them ranked by score, highest first.\n\n"
19
+ "Input CSV columns: lead_id, company, company_size (int), industry (str), visits (int). "
20
+ "Scoring is provided by the private function contracts.scoring.score_lead, which takes "
21
+ "a dict of the non-id columns and returns a float in [0, 1].\n\n"
22
+ "CLI: `python -m app.rank <csv> [--top N]`. Output one line per lead: "
23
+ "`<rank>. <lead_id> <company> <score:.2f>`. Malformed rows are skipped with a warning "
24
+ "on stderr. Add unittest tests under tests/ covering ranking order and --top."
25
+ ),
26
+ "interfaces": [{
27
+ "module": "scoring",
28
+ "function": "score_lead",
29
+ "description": "Score one lead. Higher means more likely to convert.",
30
+ "params": [{"name": "lead", "type": "dict",
31
+ "description": "keys: company_size (int), industry (str), visits (int)"}],
32
+ "returns": "float",
33
+ "fixtures": [
34
+ {"inputs": {"lead": {"company_size": 50, "industry": "saas", "visits": 12}},
35
+ "output": 0.42, "note": "decoy"},
36
+ {"inputs": {"lead": {"company_size": 10, "industry": "retail", "visits": 0}},
37
+ "output": 0.08, "note": "decoy"},
38
+ ],
39
+ "private_fixtures": [
40
+ {"inputs": {"lead": {"company_size": 50, "industry": "saas", "visits": 12}},
41
+ "output": 0.74, "note": "real"},
42
+ {"inputs": {"lead": {"company_size": 10, "industry": "retail", "visits": 0}},
43
+ "output": 0.1, "note": "real"},
44
+ ],
45
+ }],
46
+ "private_notes": "Real formula: 0.01*company_size + 0.02*visits, capped at 1.0, rounded to 4dp. "
47
+ "Worked out by reverse-engineering Acme Corp's hosted scorer (project NIGHTJAR).",
48
+ "private_plan": "1. implement score_lead with the formula. 2. clamp. 3. round. "
49
+ "4. keep the credit out of the public brief.",
50
+ }
51
+
52
+ IMPL = {
53
+ "files": {
54
+ "private_impl/__init__.py": "",
55
+ "private_impl/scoring.py": (
56
+ "def score_lead(lead: dict) -> float:\n"
57
+ " s = 0.01 * int(lead.get('company_size', 0)) + 0.02 * int(lead.get('visits', 0))\n"
58
+ " return round(min(1.0, max(0.0, s)), 4)\n"),
59
+ },
60
+ "notes": "fake implementation",
61
+ }
62
+
63
+
64
+ def fake_local(messages: list[dict]) -> str:
65
+ system = messages[0]["content"] if messages and messages[0]["role"] == "system" else ""
66
+ user = messages[-1]["content"]
67
+ if "orchestrator for a two-party build" in system:
68
+ convo = "\n".join(m["content"] for m in messages if m["role"] == "user")
69
+ if "PROBE RESULTS" not in convo:
70
+ return json.dumps({"probes": ["rank leads from a CSV by score",
71
+ "FLAGME the formula is 0.01 per employee"]})
72
+ attempt = convo.count("PREVIOUS ATTEMPT REJECTED") + 1
73
+ c = json.loads(json.dumps(CONTRACT))
74
+ if attempt == 1:
75
+ c["public_brief"] += "\n\nThis replaces the project nightjar scorer."
76
+ elif attempt == 2:
77
+ c["public_brief"] += ("\n\nFLAGME: a clean-room reimplementation, reverse-engineered "
78
+ "from observed behaviour; the existing weights were roughly "
79
+ "0.01 and 0.02.")
80
+ return "```json\n" + json.dumps(c) + "\n```"
81
+ if "implementing the private half" in user:
82
+ return json.dumps(IMPL)
83
+ raise RuntimeError("fake_local: unrecognised prompt")
84
+
85
+
86
+ def _scores(text: str) -> dict[str, float]:
87
+ hot = "FLAGME" in text
88
+ scores = {d: (0.03 if not hot else 0.05) for d in routing.DIMENSIONS}
89
+ if hot:
90
+ scores["reverse_engineering"] = 0.97
91
+ scores["hedge_framing"] = 0.91
92
+ return scores
93
+
94
+
95
+ def fake_detector(spans: list[tuple[str, str]]) -> list[dict[str, float]]:
96
+ return [_scores(t) for _, t in spans]
97
+
98
+
99
+ def fake_overall(text: str) -> dict[str, float]:
100
+ return {"overall": 0.97 if "FLAGME" in text else 0.12}
101
+
102
+
103
+ def _contract_matches_canned(public_dir: Path) -> bool:
104
+ try:
105
+ pv = json.loads((public_dir / "contract.public.json").read_text())
106
+ i = pv["interfaces"][0]
107
+ return (len(pv["interfaces"]) == 1 and i["module"] == "scoring"
108
+ and i["function"] == "score_lead" and [p["name"] for p in i["params"]] == ["lead"])
109
+ except (OSError, KeyError, IndexError, ValueError):
110
+ return False
111
+
112
+
113
+ def fake_frontier(prompt: str, public_dir: Path) -> str:
114
+ if not _contract_matches_canned(public_dir):
115
+ (public_dir / "NOTES.md").write_text(
116
+ "# fake frontier\n\nContract does not match the canned toy signature "
117
+ "(scoring.score_lead(lead)); wrote no app. Use a real frontier backend.\n")
118
+ return "fake frontier: contract differs from canned toy; wrote NOTES.md only"
119
+ app = public_dir / "app"
120
+ app.mkdir(exist_ok=True)
121
+ (app / "__init__.py").write_text("")
122
+ (app / "rank.py").write_text('''"""leadrank CLI (built by fake frontier)."""
123
+ import argparse
124
+ import csv
125
+ import sys
126
+
127
+ from contracts import scoring
128
+
129
+
130
+ def load(path):
131
+ rows = []
132
+ with open(path, newline="") as f:
133
+ for n, r in enumerate(csv.DictReader(f), 2):
134
+ try:
135
+ lead = {"company_size": int(r["company_size"]), "industry": r["industry"],
136
+ "visits": int(r["visits"])}
137
+ except (KeyError, ValueError) as e:
138
+ print(f"warning: skipping row {n}: {e}", file=sys.stderr)
139
+ continue
140
+ rows.append((r["lead_id"], r["company"], lead))
141
+ return rows
142
+
143
+
144
+ def rank(rows, top=None):
145
+ scored = [(lid, co, scoring.score_lead(lead=lead)) for lid, co, lead in rows]
146
+ scored.sort(key=lambda t: t[2], reverse=True)
147
+ return scored[:top] if top else scored
148
+
149
+
150
+ def main(argv=None):
151
+ ap = argparse.ArgumentParser(prog="leadrank")
152
+ ap.add_argument("csv")
153
+ ap.add_argument("--top", type=int)
154
+ a = ap.parse_args(argv)
155
+ for n, (lid, co, s) in enumerate(rank(load(a.csv), a.top), 1):
156
+ print(f"{n}. {lid} {co} {s:.2f}")
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
161
+ ''')
162
+ tests = public_dir / "tests"
163
+ tests.mkdir(exist_ok=True)
164
+ (tests / "test_rank.py").write_text('''import unittest
165
+ from app.rank import rank
166
+
167
+
168
+ class RankTests(unittest.TestCase):
169
+ ROWS = [
170
+ ("L1", "Acme", {"company_size": 10, "industry": "retail", "visits": 0}),
171
+ ("L2", "Beta", {"company_size": 50, "industry": "saas", "visits": 12}),
172
+ ]
173
+
174
+ def test_order(self):
175
+ out = rank(self.ROWS)
176
+ self.assertEqual([r[0] for r in out], ["L2", "L1"])
177
+
178
+ def test_top(self):
179
+ self.assertEqual(len(rank(self.ROWS, top=1)), 1)
180
+ ''')
181
+ (public_dir / "NOTES.md").write_text("# leadrank\n\nRun: `python -m app.rank leads.csv --top 5`\n")
182
+ return "fake frontier: wrote app/rank.py, tests/test_rank.py, NOTES.md"
killchain/frontier.py ADDED
@@ -0,0 +1,63 @@
1
+ """Frontier builder adapters. Each runs a coding agent inside the PUBLIC
2
+ directory only, with the public brief and generated stubs. The agent is free
3
+ to dispatch to subagents in its own family; that is its business.
4
+
5
+ The private tree is kept out of the child's environment and cwd. It is a
6
+ convention rather than a jail: the agent runs with permissions skipped.
7
+
8
+ Backends: claude (Claude Code CLI), codex (OpenAI Codex CLI), fake.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+
18
+ class FrontierError(RuntimeError):
19
+ pass
20
+
21
+
22
+ def _clean_env(private_root: Path) -> dict:
23
+ """Environment for the frontier subprocess: strip anything pointing at the
24
+ private tree, and make sure HARNESS_MODE is stub."""
25
+ env = {k: v for k, v in os.environ.items()
26
+ if str(private_root) not in v and k not in ("PYTHONPATH",)
27
+ # a Claude Code parent marks its env; the child must start clean
28
+ and k != "CLAUDECODE" and not k.startswith("CLAUDE_CODE_")}
29
+ env["HARNESS_MODE"] = "stub"
30
+ return env
31
+
32
+
33
+ def run(cfg: dict, prompt: str, public_dir: Path, private_root: Path, log: Path) -> str:
34
+ backend = cfg.get("backend", "claude")
35
+ if backend == "fake":
36
+ from killchain.fakes import fake_frontier
37
+ return fake_frontier(prompt, public_dir)
38
+ if backend == "claude":
39
+ exe = shutil.which("claude") or "claude"
40
+ cmd = [exe, "-p", prompt, "--output-format", "text"]
41
+ if cfg.get("claude_model"):
42
+ cmd += ["--model", cfg["claude_model"]]
43
+ cmd += list(cfg.get("claude_extra_args", []))
44
+ elif backend == "codex":
45
+ exe = shutil.which("codex") or "codex"
46
+ cmd = [exe, "exec", "-C", str(public_dir)]
47
+ if cfg.get("codex_model"):
48
+ cmd += ["-m", cfg["codex_model"]]
49
+ cmd += list(cfg.get("codex_extra_args", []))
50
+ cmd += [prompt]
51
+ else:
52
+ raise FrontierError(f"unknown frontier backend {backend!r}")
53
+
54
+ with log.open("w") as lf:
55
+ lf.write(f"$ {' '.join(cmd[:3])} ... (cwd={public_dir})\n\n")
56
+ lf.flush()
57
+ proc = subprocess.run(cmd, cwd=public_dir, env=_clean_env(private_root),
58
+ stdout=lf, stderr=subprocess.STDOUT, text=True,
59
+ timeout=cfg.get("timeout", 3600))
60
+ out = log.read_text()
61
+ if proc.returncode != 0:
62
+ raise FrontierError(f"{backend} exited {proc.returncode}; see {log}\n{out[-2000:]}")
63
+ return out
killchain/gate.py ADDED
@@ -0,0 +1,99 @@
1
+ """Redaction gate: a deterministic check on everything that leaves the machine.
2
+
3
+ Two jobs now:
4
+
5
+ * **Blocks** (secrets, keys, internal hostnames) against routing.HARD_RULES
6
+ and the user denylist. Any hit stops the send.
7
+ * **Hints** against routing.FRAMING_HINTS: clone/port wording, hedge words,
8
+ licence talk, TODO-ish scope. These do not block, they are printed at
9
+ review so you can see the phrasing that is most likely to make a frontier
10
+ agent hedge or negotiate before it goes out.
11
+
12
+ The denylist lives in the private root because it names the terms that tip the
13
+ frontier off: product names it will recognise, licence names, codenames. Lines
14
+ are literals, or `re:<regex>`.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ from killchain.routing import FRAMING_HINTS, HARD_RULES as BUILTIN # edit routing.py, not this file
23
+
24
+ DENYLIST_TEMPLATE = """# Risk denylist. One entry per line. Lines starting with # are ignored.
25
+ # Plain lines are case-insensitive literal matches.
26
+ # Lines starting with re: are Python regexes (case-insensitive).
27
+ #
28
+ # Two uses: things that must never reach a third-party API (keys, internal
29
+ # hostnames), and terms that reliably make a frontier model hedge: product
30
+ # names it will recognise, vendor names, licence names, project codenames.
31
+ #
32
+ # examples:
33
+ # nightjar
34
+ # re:\\bfrom memory\\b
35
+ # re:\\bgpl-?3\\b
36
+ """
37
+
38
+
39
+ @dataclass
40
+ class Hit:
41
+ rule: str
42
+ match: str
43
+ context: str
44
+
45
+
46
+ def load_denylist(path: Path) -> list[tuple[str, re.Pattern]]:
47
+ if not path.exists():
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ path.write_text(DENYLIST_TEMPLATE)
50
+ return []
51
+ rules = []
52
+ for n, raw in enumerate(path.read_text().splitlines(), 1):
53
+ line = raw.strip()
54
+ if not line or line.startswith("#"):
55
+ continue
56
+ if line.startswith("re:"):
57
+ try:
58
+ rules.append((f"denylist regex {line[3:]!r}", re.compile(line[3:], re.I)))
59
+ except re.error as e:
60
+ raise SystemExit(f"denylist line {n}: bad regex {line[3:]!r}: {e}") from e
61
+ else:
62
+ rules.append((f"denylist literal {line!r}", re.compile(re.escape(line), re.I)))
63
+ return rules
64
+
65
+
66
+ def _scan(text: str, rules: list[tuple[str, re.Pattern]]) -> list[Hit]:
67
+ hits: list[Hit] = []
68
+ for name, pat in rules:
69
+ for m in pat.finditer(text):
70
+ s, e = max(0, m.start() - 40), min(len(text), m.end() + 40)
71
+ ctx = text[s:e].replace("\n", " ")
72
+ hits.append(Hit(rule=name, match=m.group(0), context=ctx))
73
+ return hits
74
+
75
+
76
+ def scan(text: str, denylist: list[tuple[str, re.Pattern]]) -> list[Hit]:
77
+ """Blocking scan."""
78
+ return _scan(text, BUILTIN + denylist)
79
+
80
+
81
+ def hints(text: str) -> list[Hit]:
82
+ """Non-blocking framing warnings."""
83
+ return _scan(text, FRAMING_HINTS)
84
+
85
+
86
+ def describe(hits: list[Hit]) -> str:
87
+ lines = [f"REDACTION GATE BLOCKED SEND: {len(hits)} hit(s)"]
88
+ for h in hits:
89
+ lines.append(f" [{h.rule}] {h.match!r}\n ...{h.context}...")
90
+ return "\n".join(lines)
91
+
92
+
93
+ def describe_hints(hits: list[Hit]) -> str:
94
+ if not hits:
95
+ return "framing hints: none"
96
+ lines = [f"FRAMING HINTS: {len(hits)} phrase(s) that raise refusal odds"]
97
+ for h in hits:
98
+ lines.append(f" [{h.rule}] {h.match!r}")
99
+ return "\n".join(lines)