diff-sentry 0.4.3__tar.gz → 0.5.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.
- diff_sentry-0.5.0/.claude/rules/detect-setup.md +23 -0
- diff_sentry-0.5.0/.claude/rules/handoff.md +45 -0
- diff_sentry-0.5.0/.claude/rules/indicators.md +42 -0
- diff_sentry-0.5.0/.claude/rules/rubric-and-eval.md +42 -0
- diff_sentry-0.5.0/.claude/skills/release/SKILL.md +32 -0
- diff_sentry-0.5.0/.env.example +55 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/.github/workflows/ci.yml +17 -13
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/.github/workflows/release.yml +4 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/.gitignore +1 -1
- diff_sentry-0.5.0/AGENTS.md +126 -0
- diff_sentry-0.5.0/CHANGELOG.md +171 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/PKG-INFO +20 -20
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/README.md +17 -17
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/VENDOR.md +5 -6
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/action.yml +4 -4
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/__init__.py +5 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/assemble.py +4 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/config.py +9 -12
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/deep_classify.py +7 -8
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/detect.py +2 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/indicators.py +28 -39
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/ingest.py +3 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/normalize.py +4 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/response.py +8 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/rl_export.py +37 -8
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/rubric.py +3 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/schema.py +9 -7
- diff_sentry-0.5.0/eval/README.md +113 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/cli.py +3 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/judge.py +1 -1
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/schema.py +1 -1
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/score.py +14 -16
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/pyproject.toml +2 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/conftest.py +5 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/test_score.py +4 -4
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/pyproject.toml +17 -20
- diff_sentry-0.5.0/studio/DESIGN.md +227 -0
- diff_sentry-0.5.0/studio/README.md +209 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/diff_sentry_studio/app.py +34 -13
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/diff_sentry_studio/iterations.py +10 -12
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/diff_sentry_studio/live.py +4 -6
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/diff_sentry_studio/mapper.py +5 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/pyproject.toml +1 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/app.js +6 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/replay-core.js +1 -1
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/run-core.js +14 -4
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/style.css +3 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/replay-core.test.js +3 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/run-core.test.js +18 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/static-contract.test.js +3 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/test_app.py +42 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/test_iterations.py +3 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/test_mapper.py +9 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_assemble.py +2 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_cli.py +3 -4
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_deep_classify.py +3 -3
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_detect.py +1 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_export.py +21 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_forward_offline.py +5 -5
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_incident_hackerbot_claw.py +8 -8
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_indicators.py +9 -9
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_ingest.py +2 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_normalize.py +1 -1
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_response.py +2 -2
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_subscription.py +1 -1
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/uv.lock +9 -9
- diff_sentry-0.4.3/.claude/rules/handoff.md +0 -56
- diff_sentry-0.4.3/.env.example +0 -50
- diff_sentry-0.4.3/CHANGELOG.md +0 -296
- diff_sentry-0.4.3/CLAUDE.md +0 -226
- diff_sentry-0.4.3/eval/README.md +0 -105
- diff_sentry-0.4.3/studio/DESIGN.md +0 -206
- diff_sentry-0.4.3/studio/README.md +0 -208
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/.python-version +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/LICENSE +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/__main__.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/cli.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/emit.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/fetch_tool.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/skills/ci-injection-patterns.md +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/skills/prompt-injection-catalog.md +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/diff_sentry/skills/triage-a-change.md +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/__init__.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/__main__.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/diff_sentry_eval/taskset.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/taskset.example.json +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/test_boundary.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/test_cli.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/test_judge.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/test_schema.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/eval/tests/test_taskset.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/diff_sentry_studio/__init__.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/index.html +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/trajectory.js +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/vendor/fonts/jetbrains-mono-400.woff2 +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/static/vendor/fonts/jetbrains-mono-700.woff2 +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/test_live.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/studio/tests/trajectory.test.js +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/__init__.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/conftest.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/corpus/change_corpus.json +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/corpus/hackerbot_claw_incident.json +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/fixtures/known-malicious.diff +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_config.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_detection_quality.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_emit.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_fetch_tool.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_injection_offline.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_provenance.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_rubric.py +0 -0
- {diff_sentry-0.4.3 → diff_sentry-0.5.0}/tests/test_schema.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "diff_sentry/detect.py"
|
|
4
|
+
- "diff_sentry/config.py"
|
|
5
|
+
- "diff_sentry/cli.py"
|
|
6
|
+
- "diff_sentry/deep_classify.py"
|
|
7
|
+
- "diff_sentry/__init__.py"
|
|
8
|
+
- ".env.example"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Model roles, auth, and run failures
|
|
12
|
+
|
|
13
|
+
- **Failures name themselves.** With `max_retries=1` (set in `detect.setup`), a non-retryable LM error
|
|
14
|
+
(auth / billing / configuration / unsupported model) escapes after one attempt as the raw
|
|
15
|
+
`dspy.LMError` subclass. An `RLMTaskError` means the single attempt ended in an adapter parse failure,
|
|
16
|
+
an invalid result, or a retryable endpoint fault (timeout / 5xx / transport). `cli.run` turns both into
|
|
17
|
+
a `status=failed` response. Check the endpoint before suspecting the schema.
|
|
18
|
+
- **Subscription auth is a lazy, opt-in branch.** A role whose model is `claude-agent-sdk/<id>` runs on
|
|
19
|
+
rlm-harness's `ClaudeAgentLM`, imported only inside `detect._maybe_subscription_lm` and never from
|
|
20
|
+
`__init__.py`, so `import diff_sentry` stays dspy-free and a proxy-only install never touches the SDK.
|
|
21
|
+
- **The classifier never runs on the subscription.** It is a separate OpenAI-compatible client
|
|
22
|
+
(`deep_classify._selfclassify_chat`), so `config.from_env` rejects a `claude-agent-sdk/` classifier
|
|
23
|
+
model, whether set explicitly or inherited from `DS_SUB_LM`. Mixed auth across roles is by design.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Context preservation (read before auto-compacting)
|
|
2
|
+
|
|
3
|
+
Durable knowledge belongs in tracked files; a handoff summary carries only the in-flight session state
|
|
4
|
+
they do not hold yet.
|
|
5
|
+
|
|
6
|
+
- **Stable invariants** → `AGENTS.md` **Hard invariants**, or the topic rule under `.claude/rules/`.
|
|
7
|
+
- **Shipped changes** → the commit message, and a `CHANGELOG.md` entry at release.
|
|
8
|
+
- **Open / proposed work** → the issue tracker, or `AGENTS.md` **Layout** (next increments).
|
|
9
|
+
|
|
10
|
+
A handoff summary covers, in order:
|
|
11
|
+
|
|
12
|
+
1. **Decisions agreed this session** that are not yet in those files, each with its reason. Promote
|
|
13
|
+
durable ones into `AGENTS.md` or a topic rule before they fade.
|
|
14
|
+
2. **Files / symbols changed**, as `path:symbol — final shape` one-liners (e.g.
|
|
15
|
+
`assemble.py:assemble_verdict — signal = verdict∈emit_on OR severity≥floor`). No diffs, no
|
|
16
|
+
intermediate revisions.
|
|
17
|
+
3. **Current status**: what passes (`uv run pytest`, the eval suite, `uvx ruff check .`) with counts,
|
|
18
|
+
what is broken, the last command and its result. One paragraph.
|
|
19
|
+
4. **Open items** not yet tracked, each marked `proposed`, `accepted-not-done`, or `rejected`.
|
|
20
|
+
5. **Seams touched**: `classify_backend` (`"self"` or a dedicated backend in progress), the host-side
|
|
21
|
+
pre-filter / live GitHub+SIEM wiring, and the rlm-harness pin. A resumed session must not re-open a
|
|
22
|
+
seam that moved.
|
|
23
|
+
6. **The user's in-flight intent and acceptance criteria.**
|
|
24
|
+
|
|
25
|
+
Do not preserve anything already in `AGENTS.md`, `README.md`, `.env.example`, or `pyproject.toml`; tool
|
|
26
|
+
output, file listings, or file contents readable from disk; or exploration narration that led to no
|
|
27
|
+
decision.
|
|
28
|
+
|
|
29
|
+
Format (keep it under ~40 lines):
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
## Session state
|
|
33
|
+
- Goal: <one sentence>
|
|
34
|
+
- Status: <what passes, what doesn't, last command + result>
|
|
35
|
+
- Seams: <only the ones touched, one line each>
|
|
36
|
+
|
|
37
|
+
## Decisions
|
|
38
|
+
- <decision> — <why> (→ AGENTS.md / topic rule / commit message)
|
|
39
|
+
|
|
40
|
+
## Changed
|
|
41
|
+
- <path:symbol> — <what & why>
|
|
42
|
+
|
|
43
|
+
## Open
|
|
44
|
+
- [proposed|accepted-not-done|rejected] <item>
|
|
45
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "diff_sentry/indicators.py"
|
|
4
|
+
- "diff_sentry/normalize.py"
|
|
5
|
+
- "tests/corpus/**"
|
|
6
|
+
- "tests/test_indicators.py"
|
|
7
|
+
- "tests/test_detection_quality.py"
|
|
8
|
+
- "tests/test_incident_*.py"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Indicator rules
|
|
12
|
+
|
|
13
|
+
- **Deterministic, pure Python, no subprocess.** A subprocess spawned inside the live dspy.RLM/asyncio
|
|
14
|
+
process hangs; any heavier scanner belongs host-side, after the run. `mint_id` is a sha1 over
|
|
15
|
+
rule+evidence (no time, no randomness), so a baseline hit and an in-loop re-scan of the same content
|
|
16
|
+
dedupe to ONE union member. Hit evidence is a bounded snippet (`_MAX_EVIDENCE`), never the whole diff.
|
|
17
|
+
- **Severities are tuned, not monotone-paranoid.** A plain workflow-file edit is `medium`, below the
|
|
18
|
+
signal floor on purpose: a benign workflow PR must not force a SIEM signal, and a real payload inside
|
|
19
|
+
the workflow fires the high/critical shell/obfuscation rules itself. A CODEOWNERS reassignment is
|
|
20
|
+
`high`. A bare `pull_request_target` (the ordinary label-bot shape) is `medium`; the same trigger
|
|
21
|
+
checking out the PR head is `pwn-request` / `critical` (untrusted code with the base repo's secrets).
|
|
22
|
+
`dev-tunnel-endpoint`, `content-addressed-host` (IPFS) and `dynamic-code-eval` are sub-floor
|
|
23
|
+
corroborators; pairs with no innocent reading (`child_process` + `detached`, network-fetch +
|
|
24
|
+
file-write) are `high`. Requiring both halves is how a common primitive becomes evidence without
|
|
25
|
+
becoming noise.
|
|
26
|
+
- **Every rule ships with its negative case**, and `tests/corpus/` pins the suite's hit/miss behavior.
|
|
27
|
+
- **`pwn-request` has three checkout forms, each requiring the privileged trigger:** the head expression
|
|
28
|
+
on a `ref:` (directly or one binding hop away), a hand-rolled `git fetch` / `gh pr checkout`, and
|
|
29
|
+
`allow-unsafe-pr-checkout: true` (the actions/checkout v5.1+ opt-in to the fork-PR checkout it blocks
|
|
30
|
+
by default; the line states the intent, so no dataflow is needed). Each form has its own title: calling
|
|
31
|
+
an opt-in "checks out the PR HEAD" sends a reader hunting for a `ref:` that is not there.
|
|
32
|
+
- **A paired rule only pairs within one file.** `scan_diff` splits a unified diff on its file headers;
|
|
33
|
+
`scan_content` does the same for an event, whose files `raw_content` would otherwise join into one
|
|
34
|
+
header-less blob. `scan` and the in-loop tool use `scan_diff`, the host-side baseline uses
|
|
35
|
+
`scan_content`, and `normalize.content_segments` is the single definition both `raw_content` and the
|
|
36
|
+
baseline build on. Per-file scoping stops two halves from different files composing into a `critical`
|
|
37
|
+
no file contains, and keeps a hit id byte-stable between a whole-diff and a single-file scan, which is
|
|
38
|
+
what lets the baseline and an in-loop scan dedupe (MF3). Non-diff input falls through to a whole-text
|
|
39
|
+
scan; keep that fallback.
|
|
40
|
+
- **The in-loop tool registers as exactly `scan_indicators`.** dspy registers a tool under its
|
|
41
|
+
`__name__` and the prompt calls `scan_indicators(region)`; any other name makes every sandbox call a
|
|
42
|
+
NameError. The inner callable is renamed on purpose (`make_indicator_tool`); keep it.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "diff_sentry/rubric.py"
|
|
4
|
+
- "diff_sentry/rl_export.py"
|
|
5
|
+
- "diff_sentry/response.py"
|
|
6
|
+
- "diff_sentry/assemble.py"
|
|
7
|
+
- "eval/**"
|
|
8
|
+
- "studio/**"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ATLAS rubric labels and the eval member
|
|
12
|
+
|
|
13
|
+
## The rubric is a reward-free label surface
|
|
14
|
+
|
|
15
|
+
- `rl_export.rubric_signal` attaches the ATLAS 4-category (TF/TA/TG/PA) decomposition as LABELS: a fixed,
|
|
16
|
+
no-LLM skeleton in `run_start` meta (`rubric_to_meta(default_rubric())`, one criterion per category,
|
|
17
|
+
since the task is constant; there is no `generate_rubric` path) plus deterministic per-criterion
|
|
18
|
+
`criteria_facts`.
|
|
19
|
+
- The facts are a re-lens over `run_labels` / `run_metrics`, not a second derivation: `rubric.trace_facts`
|
|
20
|
+
reuses them, so a criterion's `observed` cannot drift from what a trainer reads. `_CATEGORY_LENS` maps
|
|
21
|
+
TF↔(verdict / signal / hit_iteration_cap), TA↔(scan / deep_classify / analyst / fetch / skill counts +
|
|
22
|
+
circuit-breaks + analyst failures), TG↔(indicator_count / max_indicator_severity / signal /
|
|
23
|
+
cited_unknown), PA↔(verdict / cited_unknown).
|
|
24
|
+
- `CriterionFact` has no score/met field; the trainer scores. Never add a reward/score/met field to any
|
|
25
|
+
rubric type.
|
|
26
|
+
- `rubric.py` is a dspy-free leaf: it imports only `.schema` at top, and its `rl_export` reuse is a
|
|
27
|
+
function-level import (that path is dspy-free too), so `response.py` can import it at top.
|
|
28
|
+
- `response._rubric` surfaces it as `DetectionResponse.rubric` (`RubricReport`); the studio shows it as a
|
|
29
|
+
"labels — not a score" card that adds no judgement. Keep the card legacy-guarded: a response without a
|
|
30
|
+
`rubric` renders nothing.
|
|
31
|
+
|
|
32
|
+
## The eval member measures; it never rewards
|
|
33
|
+
|
|
34
|
+
- `eval/` (`diff-sentry-eval`) scores a recorded run's assembled verdict with a fixed external ATLAS
|
|
35
|
+
LLM-as-judge: TF/TA/TG/PA on 0–10, TF primary, per-category means only, no composite, no threshold.
|
|
36
|
+
- It is a one-way reader: it reaches diff-sentry only through `verdict_from_events` / `run_labels` /
|
|
37
|
+
`run_metrics` / `AssembledVerdict`, and `diff_sentry` never imports `diff_sentry_eval`
|
|
38
|
+
(`eval/tests/test_boundary.py` enforces it).
|
|
39
|
+
- The judge is rubric-free (it never reads `rubric_signal`, which would bias the measure) and holds
|
|
40
|
+
read-never-execute: it assesses the classification statically and treats the change as untrusted.
|
|
41
|
+
- A run it cannot score (never finalized, no usable verdict, judge failed) is `unscored`, never a fake 0.
|
|
42
|
+
See `eval/README.md`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release
|
|
3
|
+
description: Cut a diff-sentry release — bump the five version sites together, write the CHANGELOG entry, and publish the GitHub Release that ships PyPI + the Marketplace Action. Use when asked to release, bump the version, or tag vX.Y.Z.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cut a diff-sentry release
|
|
7
|
+
|
|
8
|
+
A release moves FIVE version sites together, plus a `CHANGELOG.md` entry:
|
|
9
|
+
|
|
10
|
+
1. `pyproject.toml` `[project].version`
|
|
11
|
+
2. `diff_sentry/__init__.py` `__version__`
|
|
12
|
+
3. `action.yml` `inputs.version.default` (decides which PyPI release a consumer's `uses: …@vX.Y.Z` installs)
|
|
13
|
+
4. `README.md` quickstart `uses: qazbnm456/diff-sentry@vX.Y.Z`
|
|
14
|
+
5. `uv.lock`, via `uv lock`
|
|
15
|
+
|
|
16
|
+
`release.yml` fails the build unless the tag, the built wheel's version, the installed `__version__`, and
|
|
17
|
+
the `action.yml` default all agree, and it checks that the installed console script flags a known-malicious
|
|
18
|
+
diff. Nothing gates the README `uses:` line: a stale one sends new adopters to the previous release, so
|
|
19
|
+
check it by hand.
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. Freeze the boundary: `git log v<last>..HEAD`. Every `feat:` / `fix:` / `deps:` commit inside it is a
|
|
24
|
+
CHANGELOG candidate.
|
|
25
|
+
2. Write the `CHANGELOG.md` entry in the file's existing style: one sentence per user-visible outcome,
|
|
26
|
+
breaking changes and required actions explicit, no debugging story.
|
|
27
|
+
3. Bump sites 1–4, run `uv lock`, then verify: `uv run pytest`, the eval suite
|
|
28
|
+
(`uv run --package diff-sentry-eval --extra dev python -m pytest eval/tests`), `uvx ruff check .`.
|
|
29
|
+
4. Commit (`chore: release X.Y.Z`) and push to `main` once the user approves.
|
|
30
|
+
5. Publishing a GitHub Release tagged `vX.Y.Z` (with "Publish this Action to the GitHub Marketplace"
|
|
31
|
+
ticked) is what builds, verifies, and uploads to PyPI via Trusted Publishing. The upload is
|
|
32
|
+
irreversible; confirm with the user before publishing.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# diff-sentry: model roles and runtime knobs. Copy to .env; a live run needs these plus a Deno sandbox.
|
|
2
|
+
|
|
3
|
+
# --- Model roles (required for a live `pr`/`issue`/`classify` run) ---
|
|
4
|
+
# planner: a cheap, injection-resistant orchestrator that holds the diff in the REPL and triages.
|
|
5
|
+
DS_ROOT_LM=your-planner-model
|
|
6
|
+
# analyst: an expensive model reached via llm_query for a subtle source->sink (short questions only).
|
|
7
|
+
DS_SUB_LM=your-analyst-model
|
|
8
|
+
# One OpenAI-compatible proxy serves all roles. When DS_BASE_URL is set the model names are the plain
|
|
9
|
+
# ids your endpoint serves (e.g. qwen/qwen3-next), not litellm-prefixed.
|
|
10
|
+
DS_BASE_URL=https://your-openai-compatible-endpoint/v1
|
|
11
|
+
DS_API_KEY=sk-...
|
|
12
|
+
|
|
13
|
+
# --- OR: run planner + analyst on a Claude Pro/Max subscription (no API key) ---
|
|
14
|
+
# A role with the `claude-agent-sdk/<id>` prefix runs on your Claude subscription through the Claude
|
|
15
|
+
# Agent SDK (`rlm_harness.ClaudeAgentLM`, the `subscription` extra). Each call is a pure completion:
|
|
16
|
+
# no tools, no filesystem, no settings leakage. Prerequisites:
|
|
17
|
+
# - `uv sync --extra subscription`
|
|
18
|
+
# - the Claude Code CLI installed and logged in (`claude` then /login, or `claude setup-token` and
|
|
19
|
+
# export CLAUDE_CODE_OAUTH_TOKEN)
|
|
20
|
+
# - `unset ANTHROPIC_API_KEY`: the CLI prefers it over the subscription and would bill API credit,
|
|
21
|
+
# so the adapter refuses to start while it is set
|
|
22
|
+
# Prefer full model ids over aliases, which drift.
|
|
23
|
+
# DS_ROOT_LM=claude-agent-sdk/claude-sonnet-5 # planner on Sonnet 5
|
|
24
|
+
# DS_SUB_LM=claude-agent-sdk/claude-fable-5 # analyst on Fable 5
|
|
25
|
+
# DS_BASE_URL/DS_API_KEY then serve only the classifier, which always needs an OpenAI-compatible
|
|
26
|
+
# endpoint (a subscription-only run is not supported). Set DS_CLASSIFIER_LM to a real model id (plus
|
|
27
|
+
# DS_CLASSIFIER_BASE_URL/DS_CLASSIFIER_API_KEY if it is a separate box). Left unset, it inherits the
|
|
28
|
+
# `claude-agent-sdk/…` DS_SUB_LM and config loading fails with an actionable error.
|
|
29
|
+
# Budgets use the same env vars as proxy mode. Each call adds ~2-5s of CLI-spawn overhead, so keep
|
|
30
|
+
# these runs interactive, not batch. DS_PLANNER_MAX_TOKENS has no effect here: the Agent SDK exposes
|
|
31
|
+
# no token or sampling controls.
|
|
32
|
+
|
|
33
|
+
# --- Second-stage classifier (the swappable seam) ---
|
|
34
|
+
# Defaults to the analyst model on DS_BASE_URL/DS_API_KEY. Required when the analyst runs on the
|
|
35
|
+
# subscription (see above).
|
|
36
|
+
# DS_CLASSIFIER_LM=your-strong-classifier
|
|
37
|
+
# DS_CLASSIFIER_BASE_URL=https://your-classifier-endpoint/v1
|
|
38
|
+
# DS_CLASSIFIER_API_KEY=sk-...
|
|
39
|
+
# DS_CLASSIFIER_CIRCUIT_BREAK=4
|
|
40
|
+
|
|
41
|
+
# --- RLM runtime ---
|
|
42
|
+
# DS_INTERPRETER=pyodide # read-not-execute: the sandbox never runs the change. `deno` is an alias.
|
|
43
|
+
# DS_MAX_ITERATIONS=25
|
|
44
|
+
# DS_MAX_LLM_CALLS=8 # caps only analyst (llm_query) escalations
|
|
45
|
+
# DS_ADAPTER=json # portable structured output (OpenAI + vLLM/NIM)
|
|
46
|
+
# DS_PLANNER_MAX_TOKENS=16384 # generous, so a reasoning planner isn't truncated to empty content
|
|
47
|
+
|
|
48
|
+
# --- Enrichment fetch (off by default: an exfil surface on attacker-authored input; GitHub-allowlisted) ---
|
|
49
|
+
# DS_ENABLE_FETCH=false
|
|
50
|
+
# DS_FETCH_ALLOW_CIDRS=198.18.0.0/16 # only if behind a fake-IP proxy / split-DNS VPN
|
|
51
|
+
|
|
52
|
+
# --- Host-side SIEM emitter (opt-in; deterministic POST after the run) ---
|
|
53
|
+
# DS_SIEM_WEBHOOK_URL=https://your-siem/intake
|
|
54
|
+
# DS_SIEM_TOKEN=...
|
|
55
|
+
# DS_EMIT_ON=suspicious,malicious
|
|
@@ -2,8 +2,8 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
# Offline suite only: no live LLM, no network calls in tests, no Deno sandbox — dspy-bearing paths use
|
|
4
4
|
# DummyLM / rlm-harness's ScriptedInterpreter, transports are injected fakes, and the corpus pins the
|
|
5
|
-
# indicator suite's hit/miss behavior (see
|
|
6
|
-
#
|
|
5
|
+
# indicator suite's hit/miss behavior (see AGENTS.md "Verify"). `uv sync` resolves rlm-harness from PyPI
|
|
6
|
+
# at the exact version pinned in pyproject.toml / uv.lock.
|
|
7
7
|
on:
|
|
8
8
|
push:
|
|
9
9
|
branches: [main]
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
with:
|
|
40
40
|
enable-cache: true
|
|
41
41
|
|
|
42
|
-
# --all-packages brings in the studio workspace
|
|
42
|
+
# --all-packages brings in the studio and eval workspace members, --all-extras every optional surface
|
|
43
43
|
# (classify/observe/subscription + the studio dev extra) so both suites resolve in one env.
|
|
44
44
|
- name: Sync (core + studio workspace)
|
|
45
45
|
run: uv sync --all-extras --all-packages --python ${{ matrix.python-version }}
|
|
@@ -50,8 +50,8 @@ jobs:
|
|
|
50
50
|
- name: Studio console tests
|
|
51
51
|
run: uv run --python ${{ matrix.python-version }} python -m pytest studio/tests
|
|
52
52
|
|
|
53
|
-
# The eval workspace member (diff-sentry-eval)
|
|
54
|
-
# scorecard, stub judge, synthetic traces, no creds.
|
|
53
|
+
# The eval workspace member (diff-sentry-eval) has its OWN offline suite: the reward-free ATLAS
|
|
54
|
+
# scorecard, stub judge, synthetic traces, no creds.
|
|
55
55
|
- name: Eval scorecard tests
|
|
56
56
|
run: uv run --package diff-sentry-eval --python ${{ matrix.python-version }} python -m pytest eval/tests
|
|
57
57
|
|
|
@@ -89,7 +89,7 @@ jobs:
|
|
|
89
89
|
# tabulate all of the above. Scanning those against our own rules is a guaranteed self-hit
|
|
90
90
|
# (the pipe-to-shell example inside detect.INSTRUCTIONS and the field-separator token quoted
|
|
91
91
|
# in README's rule table both fire), so they are reported without gating while the machinery
|
|
92
|
-
# and the build/CI/dependency surface stay enforced.
|
|
92
|
+
# and the build/CI/dependency surface stay enforced. This file is in the GATED half:
|
|
93
93
|
# describe a payload shape here, never quote one.
|
|
94
94
|
report-only-paths: |
|
|
95
95
|
diff_sentry/indicators.py
|
|
@@ -121,9 +121,14 @@ jobs:
|
|
|
121
121
|
ver=$(uv run --no-project --with pyyaml python -c \
|
|
122
122
|
"import yaml;print(yaml.safe_load(open('action.yml'))['inputs']['version']['default'])")
|
|
123
123
|
echo "action.yml default version: $ver"
|
|
124
|
-
# The fixture lives under tests/ (a report-only path) rather than inline here
|
|
125
|
-
# the GATED half, so a payload written into it fails our own scan
|
|
126
|
-
#
|
|
124
|
+
# The fixture lives under tests/ (a report-only path) rather than inline here: this file is in
|
|
125
|
+
# the GATED half, so a payload written into it fails our own scan.
|
|
126
|
+
# FIRST prove the install. `scan` exits 1 when it flags, and uvx ALSO exits 1 when it cannot
|
|
127
|
+
# resolve `diff-sentry==${ver}` -- so the negative check below cannot tell "flagged" from "never
|
|
128
|
+
# installed" (e.g. a release commit whose version is not on PyPI yet).
|
|
129
|
+
got=$(uvx "diff-sentry==${ver}" --version)
|
|
130
|
+
echo "installed: $got"
|
|
131
|
+
case "$got" in *" ${ver}") ;; *) echo "::error::expected version ${ver}, got '$got'"; exit 1;; esac
|
|
127
132
|
# Must exit non-zero: a package that installs but scans nothing would otherwise pass silently.
|
|
128
133
|
if uvx "diff-sentry==${ver}" scan tests/fixtures/known-malicious.diff; then
|
|
129
134
|
echo "::error::the published package did not flag a known-malicious diff"
|
|
@@ -141,8 +146,7 @@ jobs:
|
|
|
141
146
|
enable-cache: true
|
|
142
147
|
- name: Lint
|
|
143
148
|
# PINNED on purpose. `uvx ruff` resolves the latest release at run time, and ruff's DEFAULT
|
|
144
|
-
# rule set is not a stable contract
|
|
145
|
-
# baseline,
|
|
146
|
-
#
|
|
147
|
-
# release do it for you.
|
|
149
|
+
# rule set is not a stable contract (0.16 widened it well past the pycodestyle/pyflakes
|
|
150
|
+
# baseline), so an unpinned run can turn this job red with no code change. Bump it
|
|
151
|
+
# deliberately, with the resulting fixes in the same commit.
|
|
148
152
|
run: uvx ruff@0.16.0 check .
|
|
@@ -34,11 +34,10 @@ jobs:
|
|
|
34
34
|
- name: Build sdist + wheel
|
|
35
35
|
run: uv build
|
|
36
36
|
|
|
37
|
-
# Verify the ARTIFACT, not the checkout — a wheel can build cleanly and still be unusable.
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# catches that.
|
|
37
|
+
# Verify the ARTIFACT, not the checkout — a wheel can build cleanly and still be unusable. Without
|
|
38
|
+
# a `[project.scripts]` entry point, for example, `uvx diff-sentry` (the exact command the GitHub
|
|
39
|
+
# Action runs) cannot work, and nothing in the source tree says so. Installing the built wheel into
|
|
40
|
+
# a clean environment and running it is what catches that.
|
|
42
41
|
#
|
|
43
42
|
# It also settles the version question against the thing being uploaded rather than against a
|
|
44
43
|
# regex over the source. Three identities have to agree: the git tag people pin, the version
|
|
@@ -23,6 +23,6 @@ output/
|
|
|
23
23
|
# Claude Code local overrides (never commit)
|
|
24
24
|
.claude/settings.local.json
|
|
25
25
|
|
|
26
|
-
# Build artifacts. `uv build` is
|
|
26
|
+
# Build artifacts. `uv build` is a normal local step (release.yml runs it), so an
|
|
27
27
|
# accidental `git add .` must not sweep a wheel into the repo.
|
|
28
28
|
dist/
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# diff-sentry — agent guide
|
|
2
|
+
|
|
3
|
+
A BewAIre-style malicious-change detector built on `rlm-harness`. It classifies ONE GitHub change
|
|
4
|
+
(PR / issue / push) for malicious intent: the change is UNTRUSTED DATA held in a sandboxed REPL, the
|
|
5
|
+
planner SUBMITs a judgement-only verdict, and the deterministic indicator EVIDENCE is unioned on read into
|
|
6
|
+
a SIEM signal. `README.md` has the pipeline table and the honest caveats; rlm-harness's "Building a
|
|
7
|
+
consumer" is the extension contract this project lives within.
|
|
8
|
+
|
|
9
|
+
rlm-harness is an exact PyPI pin (`rlm-harness==X.Y.Z` in `pyproject.toml`, locked in `uv.lock`). Overlay
|
|
10
|
+
`uv pip install -e ../rlm-harness` only while co-developing the kit, and bump the pin once the fix ships.
|
|
11
|
+
|
|
12
|
+
## Verify
|
|
13
|
+
|
|
14
|
+
- `uv run pytest` — the full offline suite (`uv sync --group dev` first). No live LLM, network, or Deno:
|
|
15
|
+
dspy paths use DummyLM / rlm-harness's `ScriptedInterpreter`, transports are injected fakes, and
|
|
16
|
+
`tests/corpus/` pins the indicator suite's hit/miss behavior.
|
|
17
|
+
- `uv run --package diff-sentry-eval --extra dev python -m pytest eval/tests` — the eval member's own suite
|
|
18
|
+
(the `--package` is required; a root `uv run` does not install the member). Run it when you touch
|
|
19
|
+
`assemble.py`, `rl_export.py`, `rubric.py`, `schema.py`, or the trace payloads it scores.
|
|
20
|
+
- `uvx ruff check .` — lint (line-length 110). CI runs it as its own job, next to both suites and the
|
|
21
|
+
node tests (`.github/workflows/ci.yml`).
|
|
22
|
+
- A live run needs role creds (`DS_*`, see `.env.example`), Deno (`brew install deno`), and `gh` for
|
|
23
|
+
`pr`/`issue` ingest. `render`/`export` are offline; `classify` ingests offline but classifies live.
|
|
24
|
+
|
|
25
|
+
## Running — always through the CLI
|
|
26
|
+
|
|
27
|
+
- Run via `cli` (`pr` / `issue` / `classify`), never an ad-hoc script. `cli.run(event, …)` is the
|
|
28
|
+
programmatic entry: it records `<out>/traces/{run_id}.jsonl` (dropping a stale one first, since
|
|
29
|
+
TraceRecorder appends), writes `<out>/responses/{run_id}.json`, and emits the SIEM signal host-side
|
|
30
|
+
after the run. It never raises: a crash still writes a `status=failed` response that can signal off the
|
|
31
|
+
evidence floor. Extend `cli.py` rather than driving `detect_from_event` / `build_response` directly.
|
|
32
|
+
- Offline: `python -m diff_sentry render <trace> <run_id>` re-renders a response;
|
|
33
|
+
`python -m diff_sentry export "output/traces/*.jsonl" ds.json` exports the reward-free dataset.
|
|
34
|
+
|
|
35
|
+
## Hard invariants — do not break
|
|
36
|
+
|
|
37
|
+
- **The change is DATA: read, never execute.** It is a REPL variable under the default `pyodide`
|
|
38
|
+
interpreter; nothing in it is run, built, or fetched-and-run. An embedded instruction is a
|
|
39
|
+
`prompt-injection` signal, never a command. Never route the interpreter to `local`.
|
|
40
|
+
- **Judgement-only SUBMIT; evidence is unioned on read (MF3).** `ChangeVerdict` has no hits field; the
|
|
41
|
+
planner may only CITE ids (`indicator_ids`). `assemble.assemble_verdict` unions every hit in the trace
|
|
42
|
+
(run_start `baseline_indicators` ∪ each `scan_indicators` tool_call) and derives
|
|
43
|
+
`signal = verdict ∈ emit_on OR max severity ≥ SIGNAL_SEVERITY_FLOOR`. A false-benign verdict cannot
|
|
44
|
+
suppress evidence; a cited id with no recorded hit lands in `cited_unknown_ids`. The same assembly runs
|
|
45
|
+
on every read path (live, re-render, `rl_export`). Never add a hits/severity/signal field to the SUBMIT
|
|
46
|
+
type or a second signal derivation.
|
|
47
|
+
- **An ungroundable input yields `inconclusive`, never a confident verdict.** `inconclusive` is a
|
|
48
|
+
sanctioned 4th SUBMIT value (`schema.INCONCLUSIVE_VERDICT`) mapped to `status="inconclusive"` +
|
|
49
|
+
`RefusalInfo(reason="insufficient_evidence")`. The host-side backstop `normalize.has_groundable_content`
|
|
50
|
+
downgrades even a confident verdict when no groundable content exists. It is a reward-free negative
|
|
51
|
+
outcome, not an escape hatch for a hard-but-real change, and not in `emit_on`, so hard indicator
|
|
52
|
+
evidence still signals on its own.
|
|
53
|
+
- **MF1: the metadata sandwich in `normalize_event`.** dspy previews ~1000 chars of head+tail, so an
|
|
54
|
+
identical derived-metadata header and footer deny the attacker those edges, and attacker-authored
|
|
55
|
+
metadata fields are capped (`_MAX_META_*`). The title/author ride in `raw_content` so the host-side
|
|
56
|
+
baseline catches a title-borne injection that skews the planner. Don't flatten the sandwich or lift
|
|
57
|
+
the caps.
|
|
58
|
+
- **MF2: enrichment fetch is GitHub-allowlisted and off by default** (`enable_fetch=False`). An injected
|
|
59
|
+
instruction could steer a fetcher to `https://attacker.tld/?leak=…`: the kit's SSRF guard blocks
|
|
60
|
+
internal targets, and the `github_hosts` allowlist blocks external ones, re-checked on every redirect
|
|
61
|
+
hop. Never add a general-purpose fetch or fetch a URL the change names. `fetch_allow_cidrs` only
|
|
62
|
+
relaxes the resolved-IP layer (fake-IP proxy / split DNS); the syntactic guard still refuses
|
|
63
|
+
localhost/metadata.
|
|
64
|
+
- **Indicators are deterministic, pure Python, and in-loop safe.** Rule-level invariants (severity
|
|
65
|
+
tuning, per-file pairing, `pwn-request`, the tool name) live in `.claude/rules/indicators.md`.
|
|
66
|
+
- **Models are ROLES configured by env**: `DS_ROOT_LM` planner, `DS_SUB_LM` analyst, `DS_CLASSIFIER_LM`
|
|
67
|
+
classifier (defaults to the analyst). Refer to them by role; no hardcoded model name.
|
|
68
|
+
- **The budget is hard: `max_retries=1`, no whole-RLM retry.** One change = one trajectory, so the trace
|
|
69
|
+
stays valid training data. A failed run is infra before it is a schema bug; see
|
|
70
|
+
`.claude/rules/detect-setup.md` for how the failure names itself.
|
|
71
|
+
- **A second model-judgement is a TOOL, never the sub-LM.** `deep_classify` (on rlm-harness's
|
|
72
|
+
`make_model_tool`) is the swappable second-stage seam the planner chooses to call, so the decision is a
|
|
73
|
+
`tool_call` in the trajectory. The analyst intercept (`intercept_sub_lm`) is tracing-only, zero
|
|
74
|
+
transforms. Swapping the backend touches only `classify_backend` / `_selfclassify_chat`. The planner
|
|
75
|
+
passes distilled findings, never the whole diff.
|
|
76
|
+
- **GitHub ingest and SIEM emission are host-side plumbing, never planner tools.** `ingest.py` shells out
|
|
77
|
+
to `gh` host-side (transport injectable); `emit.emit_signal` POSTs after the run, stays out of the
|
|
78
|
+
trajectory, and never raises. Only choices the policy makes are tools; delivering a finished result is
|
|
79
|
+
plumbing.
|
|
80
|
+
- **This is a ROLLOUT source: trajectories, not reward.** `rl_export` passes `reward=None`; labels read
|
|
81
|
+
the assembled verdict and metrics are objective effort counters. `deep_classify` tool_calls train the
|
|
82
|
+
classifier; every other action is the planner's. A metric keeps ONE meaning across rlm-harness
|
|
83
|
+
versions (the corpus mixes them; `run_start.payload.rlm_harness` names the writer): a quantity a newer
|
|
84
|
+
kit makes observable gets a NEW key that is None where the trace cannot say (e.g. `analyst_failures`).
|
|
85
|
+
The ATLAS rubric and the eval member stay reward-free too; see `.claude/rules/rubric-and-eval.md`.
|
|
86
|
+
- **Attack knowledge lives in `diff_sentry/skills/`, not the prompt.** The skill catalog is injected
|
|
87
|
+
(`load_skills_as_tools(discovery="inject")`) and `read_skill(name)` pulls a body on demand. Fix a wrong
|
|
88
|
+
convention by editing or adding a skill; `detect.INSTRUCTIONS` holds only identity, the MISSION frame,
|
|
89
|
+
the tool cost model, the triage loop, and output-gating rules. Skills ship via
|
|
90
|
+
`packages = ["diff_sentry"]`; a force-include duplicates them and breaks the build.
|
|
91
|
+
- **Keep the dspy-free modules dspy-free.** `config`, `schema`, `normalize`, `indicators`, `assemble`,
|
|
92
|
+
`response`, `emit`, `ingest`, `rl_export`, and `rubric` must not import dspy at module top, and
|
|
93
|
+
`import diff_sentry` must not import dspy (`ClassifyChange` / `setup` / `run` / `detect_from_event` are
|
|
94
|
+
lazy PEP 562 re-exports).
|
|
95
|
+
- **The trace self-describes the run.** `run_start` meta carries the normalized event, instructions,
|
|
96
|
+
source echo, `baseline_indicators`, `emit_on`, role→model names, budgets, and the rubric skeleton, so
|
|
97
|
+
an offline re-render/export re-derives the same `signal` (reading `emit_on` from meta, never current
|
|
98
|
+
config). Any new per-run config that affects read-time derivation must ride in meta.
|
|
99
|
+
|
|
100
|
+
## Topic guides
|
|
101
|
+
|
|
102
|
+
Claude Code loads each rule automatically when you touch a matching file; other agents should read the
|
|
103
|
+
file before working in that area.
|
|
104
|
+
|
|
105
|
+
| Area | File |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Indicator rules, severities, corpus | `.claude/rules/indicators.md` |
|
|
108
|
+
| Model roles, subscription auth, run failures | `.claude/rules/detect-setup.md` |
|
|
109
|
+
| ATLAS rubric labels, eval member, studio rubric card | `.claude/rules/rubric-and-eval.md` |
|
|
110
|
+
| Cutting a release (five version sites) | `.claude/skills/release/SKILL.md` |
|
|
111
|
+
| What to keep across context compaction | `.claude/rules/handoff.md` |
|
|
112
|
+
|
|
113
|
+
## Layout
|
|
114
|
+
|
|
115
|
+
- rlm-harness owns the generic cores: `make_model_tool` (chat → retry → validate → circuit-break) under
|
|
116
|
+
`deep_classify`, and the SSRF primitives (`is_safe_url`, `resolved_host_is_safe`, `parse_cidrs`) under
|
|
117
|
+
`fetch_tool`, which adds the GitHub allowlist, the httpx provider, and tracing.
|
|
118
|
+
- When this consumer needs a workaround, fix the reusable gap in rlm-harness generically; never
|
|
119
|
+
special-case diff-sentry there. Consumer values (`DS_*` roles, the verdict schema, indicator rules, the
|
|
120
|
+
SIEM payload shape) stay here.
|
|
121
|
+
- Next increments: a cheap pre-filter tier (deterministic indicators + one small single-shot call) as
|
|
122
|
+
host-side plumbing in front of `cli.run`, not extra RLM turns; and live GitHub/SIEM wiring.
|
|
123
|
+
- Two workspace members, never in the `diff_sentry` wheel: `studio/` (`diff-sentry-studio`, the detection
|
|
124
|
+
console, behind its `live` extra) and `eval/` (`diff-sentry-eval`, the reward-free scorecard). Both read
|
|
125
|
+
the trace / `DetectionResponse` contract through the public surface only. Keep the root wheel at
|
|
126
|
+
`packages = ["diff_sentry"]` so `uv build` never sweeps them in.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to diff-sentry, a BewAIre-style detector that classifies one GitHub change
|
|
4
|
+
(PR, issue or push) for malicious intent on [`rlm-harness`](https://github.com/qazbnm456/rlm-harness).
|
|
5
|
+
|
|
6
|
+
## 0.5.0
|
|
7
|
+
|
|
8
|
+
Upgrade from 0.4.3: dspy 3.4.0 removed the interface rlm-harness 1.10.1 hands its sandbox through, so a
|
|
9
|
+
fresh 0.4.3 install resolves dspy 3.4.0 and every `pr` / `issue` / `classify` run fails with a
|
|
10
|
+
`TypeError`. The Action's deterministic `scan` is unaffected. 0.5.0 pins `rlm-harness==1.14.0`, which
|
|
11
|
+
caps dspy below 3.5.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **`rlm-harness==1.14.0`, which requires `dspy>=3.4.0,<3.5.0` and `pydantic>=2.11.0`** (from 1.10.1,
|
|
15
|
+
dspy 3.3.1). diff-sentry's own `pydantic` floor rises to match.
|
|
16
|
+
- **Each exported action's `state` lists its prior actions in causal order** (interleaved by time)
|
|
17
|
+
instead of write order; labels, rubric facts and the SIEM signal are unchanged.
|
|
18
|
+
- **A failed analyst escalation is recorded**: the trace gains a `sub_call` carrying the provider's
|
|
19
|
+
error and `cause: "endpoint"`. The new `analyst_failures` metric counts them (None for a trace
|
|
20
|
+
written before rlm-harness 1.13.0, which could not record them), and `analyst_calls` keeps counting
|
|
21
|
+
only escalations that got a response, so it means the same on traces from either side of the upgrade.
|
|
22
|
+
- **The agent guide is `AGENTS.md` (formerly `CLAUDE.md`)**, which Claude Code 2.1.277+ and other coding
|
|
23
|
+
agents read natively. Area-specific invariants moved to path-scoped `.claude/rules/`, and the release
|
|
24
|
+
procedure to `.claude/skills/release`.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **The studio's run telemetry flags refused analyst escalations**: the analyst tile turns amber and an
|
|
28
|
+
"N analyst escalations refused" chip appears; a trace too old to record refusals shows neither.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **The studio feed shows a refused analyst escalation as "Ask analyst failed"** with the provider's
|
|
32
|
+
error, instead of rendering it like one that answered.
|
|
33
|
+
- **The studio replays a run in causal order**: a turn's reasoning streams before the tool calls it
|
|
34
|
+
made, and `run_end` always streams last, even when a timestamp is missing, NaN or infinite.
|
|
35
|
+
- **CI's consumer-install job fails when the published version cannot be installed**, instead of
|
|
36
|
+
passing green because the negative scan check also exits 1.
|
|
37
|
+
|
|
38
|
+
### Docs
|
|
39
|
+
- **Docs, comments and test docstrings state current facts only**, and claims that had drifted from the
|
|
40
|
+
code are corrected (studio replay order, `RefusalInfo` reasons, where CI resolves rlm-harness).
|
|
41
|
+
|
|
42
|
+
## 0.4.3
|
|
43
|
+
|
|
44
|
+
Dependencies only: the `diff_sentry` package itself is unchanged.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- **`rlm-harness==1.10.1` and `dspy` 3.3.1** (from 1.0.0 and 3.2.1); the pin stays exact, and a fresh
|
|
48
|
+
install resolves fewer packages because dspy 3.3 drops several exact pins.
|
|
49
|
+
- **Traces record more of each run** (the kit version in `run_start`; applied `budgets`, per-attempt
|
|
50
|
+
`usage` and the failed run's `error_chain` in `run_end`; `duration_s` on every tool call and
|
|
51
|
+
`sub_call`), while labels, metrics, rubric facts and the SIEM signal are derived exactly as before.
|
|
52
|
+
- **A non-retryable LM error (auth, billing, configuration, unsupported model) fails after one attempt
|
|
53
|
+
and the `status=failed` response names the dspy error class**, so a credential problem reads
|
|
54
|
+
differently from a model that kept producing invalid output (`RLMTaskError`).
|
|
55
|
+
- **The studio's "took" label on a tool call shows the recorded `duration_s`**, falling back to the
|
|
56
|
+
event gap only for older traces.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- **An analyst escalation appears in the trace and the exported datasets exactly once.**
|
|
60
|
+
- **The analyst intercept handles dspy 3.3's typed `LMResponse`.**
|
|
61
|
+
- **A payload containing an unpaired surrogate is recorded** instead of being dropped from the trace.
|
|
62
|
+
|
|
63
|
+
### Docs
|
|
64
|
+
- **`CLAUDE.md` lists all five version sites a release moves**, noting that
|
|
65
|
+
`release.yml` does not gate the README's `uses:` line.
|
|
66
|
+
|
|
67
|
+
## 0.4.2
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- **Paired rules (`pwn-request`, `detached-process-spawn`) only pair signals from the same file**, in both
|
|
71
|
+
the in-loop `scan_indicators` tool and the host-side baseline, so two unrelated files can no longer
|
|
72
|
+
compose a `critical` that neither contains.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
- **`astral-sh/setup-uv` v10.0.1 and `actions/checkout` v7**, for the Node 24 runtime; the setup-uv bump
|
|
76
|
+
also applies inside `action.yml`, so repos running the action stop seeing the Node 20 deprecation
|
|
77
|
+
warning.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
- **`pwn-request` flags `allow-unsafe-pr-checkout: true` under a privileged trigger** as its third
|
|
81
|
+
checkout form, with its own title; an explicit `false`, a removed opt-in, or a plain `pull_request`
|
|
82
|
+
trigger does not fire.
|
|
83
|
+
- **Each hit names the file it came from** instead of the whole diff.
|
|
84
|
+
- **A hit id is the same whether the diff was scanned whole or one file at a time**, so the baseline and
|
|
85
|
+
an in-loop re-scan de-duplicate to one piece of evidence.
|
|
86
|
+
|
|
87
|
+
## 0.4.1
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
- **`pypa/gh-action-pypi-publish` v1.14.2**, whose Twine 7 accepts the core metadata 2.5 that `uv build`
|
|
91
|
+
now emits.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
- **`pwn-request` fires only when the PR-head expression reaches a checkout** (an `actions/checkout`
|
|
95
|
+
`ref:` or a `git checkout` / `git fetch` / `gh pr checkout` command), so the safe `workflow_run`
|
|
96
|
+
publisher shape the README recommends stays at the sub-floor `privileged-fork-trigger`.
|
|
97
|
+
- **`pwn-request` follows one binding hop**, catching a head SHA bound to a name and then used as
|
|
98
|
+
`ref: ${{ env.HEAD }}`.
|
|
99
|
+
- **A hand-rolled fetch of `refs/pull/${{ github.event.number }}/head` is detected.**
|
|
100
|
+
|
|
101
|
+
## 0.4.0
|
|
102
|
+
|
|
103
|
+
The first release, led by a GitHub Action anyone can install; the local model pipeline, studio and
|
|
104
|
+
trajectory export remain for teams that run that infrastructure.
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- **diff-sentry ships as a GitHub Action** (`action.yml`, with inputs `fail-on`, `report-only-paths`,
|
|
108
|
+
`base-sha`, `version` and outputs `failed`, `hit-count`, `max-severity`, `report`) that runs only the
|
|
109
|
+
deterministic scan, so it needs no credentials, network or Deno and runs safely on fork PRs under the
|
|
110
|
+
read-only token.
|
|
111
|
+
- **A `scan` subcommand and a `diff-sentry` console entry point** run the deterministic scan standalone;
|
|
112
|
+
`--fail-on` defaults to the SIEM signal floor, with `--json` and `--include-deletions` options.
|
|
113
|
+
- **Ten indicator rules for the Miasma attack families**: `pwn-request`, `privileged-fork-trigger`,
|
|
114
|
+
`diff-viewport-evasion`, `invisible-unicode`, `detached-process-spawn`, `inline-code-exec`,
|
|
115
|
+
`remote-fetch-to-disk`, `obfuscated-identifiers`, and the sub-floor corroborators `dynamic-code-eval`
|
|
116
|
+
and `content-addressed-host`, each with a negative case in the corpus.
|
|
117
|
+
- **A `self-scan` CI job** runs this repo's own Action on every PR.
|
|
118
|
+
- **`release.yml` publishes to PyPI through OIDC Trusted Publishing** after installing the built wheel
|
|
119
|
+
cleanly and checking that the tag, wheel filename and `__version__` agree.
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
- **`rlm-kit` is replaced by `rlm-harness==1.0.0` from PyPI**; imports move to `rlm_harness`.
|
|
123
|
+
- **The rubric uses `rlm_harness.rubric`**, and `schema` still re-exports `Criterion`, `CriterionFact`
|
|
124
|
+
and `RubricCriteria`.
|
|
125
|
+
- **The README and package description lead with the Action.**
|
|
126
|
+
- **A bare `uv sync` installs the `subscription-sdk` dev group**, keeping the Claude Agent SDK in the dev
|
|
127
|
+
environment.
|
|
128
|
+
- **CI pins ruff to `0.16.0`.**
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
- **`scan` ignores lines a diff deletes by default**, so a commit that removes a payload is not flagged
|
|
132
|
+
for it.
|
|
133
|
+
|
|
134
|
+
## 0.3.0
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
- **An input with no assessable content yields `inconclusive`** (`status="inconclusive"`,
|
|
138
|
+
`reason="insufficient_evidence"`) instead of a confident verdict, enforced by a host-side backstop and
|
|
139
|
+
exported as a reward-free `inconclusive` label; hard indicator evidence still forces a SIEM signal.
|
|
140
|
+
- **The studio shows the short fields of an unrecognized tool call** in the drawer, the live feed and the
|
|
141
|
+
trajectory view instead of an empty step, and `deep_classify` keeps a child-rollout link when its
|
|
142
|
+
backend returns one.
|
|
143
|
+
|
|
144
|
+
## 0.2.1
|
|
145
|
+
|
|
146
|
+
### Fixed
|
|
147
|
+
- **The studio starts on a subscription with the same command as the sibling harnesses**, through a new
|
|
148
|
+
`subscription` extra on `diff-sentry-studio`.
|
|
149
|
+
- **`/v1/config` no longer shows a subscription analyst as the classifier**, since no run can use one.
|
|
150
|
+
|
|
151
|
+
## 0.2.0
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
- **The planner and analyst can run on a Claude Pro/Max subscription** by setting `DS_ROOT_LM` /
|
|
155
|
+
`DS_SUB_LM` to `claude-agent-sdk/<model>`; this needs `uv sync --extra subscription`, a logged-in
|
|
156
|
+
Claude Code CLI and no `ANTHROPIC_API_KEY`.
|
|
157
|
+
- **The studio has a page-height, three-column layout with a Verdict / Indicators / Change switch**,
|
|
158
|
+
and the Change view reads the normalized content from the run's own trace.
|
|
159
|
+
|
|
160
|
+
### Changed
|
|
161
|
+
- **The classifier must stay on an OpenAI-compatible endpoint**: `config.from_env` rejects a
|
|
162
|
+
`claude-agent-sdk/…` classifier, set directly or inherited from `DS_SUB_LM`, so a subscription-only
|
|
163
|
+
run is not supported.
|
|
164
|
+
|
|
165
|
+
## 0.1.0
|
|
166
|
+
|
|
167
|
+
The initial release: classify a change into a judgement-only verdict, union the deterministic indicator
|
|
168
|
+
evidence on read (MF3), build the response, emit the SIEM signal host-side, and export reward-free
|
|
169
|
+
trajectories, all testable offline. Includes the metadata sandwich (MF1), the GitHub-allowlisted opt-in
|
|
170
|
+
enrichment fetch (MF2), the pure-Python indicator suite, the `deep_classify` second-stage seam,
|
|
171
|
+
progressive-disclosure attack skills, an offline hackerbot-claw reproduction, and the studio console.
|