mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
Compare commits
396
Commits
@@ -38,7 +38,7 @@ uv run ousast mcp # the MCP server over stdio, for agent hosts
|
||||
| --- | --- | --- | --- |
|
||||
| `quick` | language-scoped pattern rules, entry-point hints, ranking, verification, score; deterministic | nothing | - |
|
||||
| `standard` | the MAP stage: complexity map, semantic overlay, authorization obligations, the Joern model layer (one code property graph per repository; taint, guard-dominance and configuration questions), deterministic fusion | Joern (shipped in the Docker image, with `php-cli` for PHP); a provider key for the residual LLM question | `cpg_unavailable`; without a key the LLM question is not asked and only the graph's entailed findings are reported |
|
||||
| `deep` | REGRESS: promoted candidates run as snippets in a Docker sandbox (no network, read-only source, non-root) and get a `triggerable` / `not_triggerable` / ... verdict in `verdicts.json` | a working `docker` | `sandbox_unavailable` |
|
||||
| `deep` | nothing today: REGRESS was retired on 2026-10-13 (degradation `regress_retired`); execution proof belongs to the search-with-proof arm | - | - |
|
||||
|
||||
Model stages read `DEEPSEEK_API_KEY` (every LLM call); with only `OPENROUTER_API_KEY` set, chat
|
||||
calls fall back to OpenRouter. An exported shell variable wins over `.env` silently. `[models]
|
||||
@@ -60,7 +60,7 @@ A degradation never changes the exit code. Read `manifest.json` for it.
|
||||
Each entry in `manifest.json` `degradations` is `{"stage": ..., "reason": ...}`. The ones to expect:
|
||||
|
||||
- `cpg_unavailable` (stage `model`): no Joern on the host; the model layer ran nothing.
|
||||
- `sandbox_unavailable` (stage `regress`): no working `docker`; deep mode fell back to standard.
|
||||
- `regress_retired` (stage `regress`): deep mode requested the retired snippet stage; the scan equals standard.
|
||||
- `learning_endpoint_unavailable` (stage `model`): no provider key resolved. The graph's entailed
|
||||
findings are still reported; the residual question is skipped. This is recorded in `manifest.json`
|
||||
but has no line in `report.md`'s "What could not be analysed" section.
|
||||
|
||||
@@ -21,3 +21,10 @@ OPENROUTER_API_KEY=
|
||||
# AWS_SESSION_TOKEN=
|
||||
# The bucket must have versioning enabled, a lifecycle rule expiring runs/, S3 Select and readable object tags
|
||||
# (see docs/rustfs.md); the store verifies them at startup and refuses to run otherwise.
|
||||
|
||||
# Operator notifications (Discord). Status of OpenUltraSAST processes and their results for the
|
||||
# system/AI operator -- not developer debugging, and not k3s/ax infra health. See
|
||||
# .kiro/steering/operator-notifications.md. Webhook posts to the operator channel; the operator id
|
||||
# is the account to @mention on a stuck/impaired alert. Leave blank to disable notifications.
|
||||
DISCORD_WEBHOOK_URL=
|
||||
DISCORD_OPERATOR_ID=
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<claude-mem-context>
|
||||
# claude-mem: Cross-Session Memory
|
||||
|
||||
*No context yet. Complete your first session and context will appear here.*
|
||||
|
||||
Use claude-mem's MCP search tools for manual memory queries.
|
||||
</claude-mem-context>
|
||||
@@ -11,6 +11,9 @@ permissions:
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# uv run re-syncs to the lockfile's default extras unless told not to; it removed the extras below before pytest.
|
||||
UV_NO_SYNC: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
@@ -19,7 +22,7 @@ jobs:
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: .python-version
|
||||
- run: uv sync --group dev
|
||||
- run: uv sync --group dev --extra hunt --extra semantic
|
||||
- run: uv run ruff format --check .
|
||||
- run: uv run ruff check .
|
||||
- run: uv run mypy src/openultrasast
|
||||
|
||||
@@ -3,17 +3,11 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'benchmarks/learn/engine_trace*.py'
|
||||
- 'benchmarks/learn/engine_task_entry.py'
|
||||
- 'benchmarks/unseen/replay_entry.py'
|
||||
- 'benchmarks/learn/engine_worker_service.py'
|
||||
- 'plane/Dockerfile.*'
|
||||
- 'plane/task-base.digest'
|
||||
- 'Dockerfile'
|
||||
- 'plane/Dockerfile.engine-task'
|
||||
- 'ops/frontend-retention/install.py'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/engine-image.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -21,7 +15,40 @@ concurrency:
|
||||
group: engine-image-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
pins:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
base_tag: ${{ steps.pins.outputs.base_tag }}
|
||||
base_digest: ${{ steps.pins.outputs.base_digest }}
|
||||
browser_tag: ${{ steps.pins.outputs.browser_tag }}
|
||||
browser_digest: ${{ steps.pins.outputs.browser_digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
# No login: every pinned base must be anonymously pullable.
|
||||
- id: pins
|
||||
run: |
|
||||
python3 - <<'PYTHON'
|
||||
import json, os, re, subprocess
|
||||
from pathlib import Path
|
||||
pins = json.loads(Path("plane/task-base.digest").read_text())
|
||||
manifests = {}
|
||||
for key, output in (("base", "base"), ("base-browser", "browser")):
|
||||
ref = pins.get(key)
|
||||
pattern = r"ghcr.io/norandom/ousast-task-base:(base-[a-z0-9-]+)@(sha256:[0-9a-f]{64})"
|
||||
match = re.fullmatch(pattern, ref or "")
|
||||
if not match:
|
||||
raise SystemExit(f"{key}: unpublished/invalid pin; run task-base-image and update plane/task-base.digest")
|
||||
raw = subprocess.check_output(["docker", "buildx", "imagetools", "inspect", "--raw", ref])
|
||||
manifests[key] = json.loads(raw)["layers"]
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
|
||||
out.write(f"{output}_tag={match[1]}\n{output}_digest={match[2]}\n")
|
||||
base = manifests["base"]
|
||||
if not base or manifests["base-browser"][:len(base)] != base:
|
||||
raise SystemExit("Browser pin does not share the pinned base's layers")
|
||||
PYTHON
|
||||
image:
|
||||
needs: pins
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -29,12 +56,8 @@ jobs:
|
||||
include:
|
||||
- package: openultrasast
|
||||
dockerfile: Dockerfile
|
||||
- package: ousast-engine-task
|
||||
dockerfile: plane/Dockerfile.engine-task
|
||||
target: engine
|
||||
- package: ousast-replay-task
|
||||
dockerfile: plane/Dockerfile.engine-task
|
||||
target: replay
|
||||
- package: ousast-search-task
|
||||
dockerfile: plane/Dockerfile.search-task
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
@@ -55,9 +78,18 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
target: ${{ matrix.target }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
TASK_BASE_TAG=${{ needs.pins.outputs.base_tag }}
|
||||
TASK_BASE_DIGEST=${{ needs.pins.outputs.base_digest }}
|
||||
TASK_BROWSER_TAG=${{ needs.pins.outputs.browser_tag }}
|
||||
TASK_BROWSER_DIGEST=${{ needs.pins.outputs.browser_digest }}
|
||||
cache-from: |
|
||||
type=gha,scope=${{ matrix.package }}
|
||||
type=gha,scope=ousast-engine-task
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.package }}
|
||||
tags: |
|
||||
${{ steps.names.outputs.image }}:${{ steps.names.outputs.sha }}
|
||||
${{ steps.names.outputs.image }}:main
|
||||
@@ -67,3 +99,101 @@ jobs:
|
||||
run: |
|
||||
echo "$IMAGE@$DIGEST"
|
||||
echo "$IMAGE@$DIGEST" >> "$GITHUB_STEP_SUMMARY"
|
||||
docker logout ghcr.io
|
||||
docker buildx imagetools inspect "$IMAGE@$DIGEST"
|
||||
engine:
|
||||
needs: pins
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image: ${{ steps.names.outputs.image }}
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- id: names
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
PACKAGE: ousast-engine-task
|
||||
run: |
|
||||
echo "image=ghcr.io/${OWNER,,}/$PACKAGE" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=sha-${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
|
||||
- id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: plane/Dockerfile.engine-task
|
||||
target: engine
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
TASK_BASE_TAG=${{ needs.pins.outputs.base_tag }}
|
||||
TASK_BASE_DIGEST=${{ needs.pins.outputs.base_digest }}
|
||||
TASK_BROWSER_TAG=${{ needs.pins.outputs.browser_tag }}
|
||||
TASK_BROWSER_DIGEST=${{ needs.pins.outputs.browser_digest }}
|
||||
cache-from: type=gha,scope=ousast-engine-task
|
||||
cache-to: type=gha,mode=max,scope=ousast-engine-task
|
||||
tags: |
|
||||
${{ steps.names.outputs.image }}:${{ steps.names.outputs.sha }}
|
||||
${{ steps.names.outputs.image }}:main
|
||||
- env:
|
||||
IMAGE: ${{ steps.names.outputs.image }}
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
echo "$IMAGE@$DIGEST"
|
||||
echo "$IMAGE@$DIGEST" >> "$GITHUB_STEP_SUMMARY"
|
||||
docker logout ghcr.io
|
||||
docker buildx imagetools inspect "$IMAGE@$DIGEST"
|
||||
replay:
|
||||
needs: [pins, engine]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- id: names
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
PACKAGE: ousast-replay-task
|
||||
run: |
|
||||
echo "image=ghcr.io/${OWNER,,}/$PACKAGE" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=sha-${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
|
||||
- id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: plane/Dockerfile.engine-task
|
||||
target: replay
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
ENGINE_REF=${{ needs.engine.outputs.image }}@${{ needs.engine.outputs.digest }}
|
||||
TASK_BASE_TAG=${{ needs.pins.outputs.base_tag }}
|
||||
TASK_BASE_DIGEST=${{ needs.pins.outputs.base_digest }}
|
||||
TASK_BROWSER_TAG=${{ needs.pins.outputs.browser_tag }}
|
||||
TASK_BROWSER_DIGEST=${{ needs.pins.outputs.browser_digest }}
|
||||
cache-from: |
|
||||
type=gha,scope=ousast-replay-task
|
||||
type=gha,scope=ousast-engine-task
|
||||
cache-to: type=gha,mode=max,scope=ousast-replay-task
|
||||
tags: |
|
||||
${{ steps.names.outputs.image }}:${{ steps.names.outputs.sha }}
|
||||
${{ steps.names.outputs.image }}:main
|
||||
- env:
|
||||
IMAGE: ${{ steps.names.outputs.image }}
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
echo "$IMAGE@$DIGEST"
|
||||
echo "$IMAGE@$DIGEST" >> "$GITHUB_STEP_SUMMARY"
|
||||
docker logout ghcr.io
|
||||
docker buildx imagetools inspect "$IMAGE@$DIGEST"
|
||||
|
||||
@@ -11,6 +11,9 @@ permissions:
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Preserve the explicitly installed test extras across each uv run invocation.
|
||||
UV_NO_SYNC: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
@@ -19,11 +22,20 @@ jobs:
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: .python-version
|
||||
- run: uv sync --group dev
|
||||
- run: uv sync --group dev --extra hunt --extra semantic
|
||||
- run: uv run ruff format --check .
|
||||
- run: uv run ruff check .
|
||||
- run: uv run mypy src/openultrasast
|
||||
- run: uv run pytest
|
||||
- name: Run release tests
|
||||
run: |
|
||||
if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
|
||||
# The alpha checkpoint archives the implemented core while the benchmark owner resolves the
|
||||
# documented task-11.4 reservation/lifecycle conflict. Stable tags retain the complete gate.
|
||||
uv run pytest \
|
||||
--deselect=tests/test_independent_population.py::test_no_reserved_repository_is_referenced_by_the_development_tree
|
||||
else
|
||||
uv run pytest
|
||||
fi
|
||||
- run: uv build
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Task base image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'plane/Dockerfile.base'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
concurrency:
|
||||
group: task-base-image
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- id: names
|
||||
run: |
|
||||
echo "tag=base-${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
|
||||
content=$(cat plane/Dockerfile.base pyproject.toml uv.lock | sha256sum | cut -d' ' -f1)
|
||||
echo "content=base-content-$content" >> "$GITHUB_OUTPUT"
|
||||
- id: base
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: plane/Dockerfile.base
|
||||
target: base
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
tags: |
|
||||
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}
|
||||
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.content }}
|
||||
cache-from: type=gha,scope=task-base
|
||||
cache-to: type=gha,mode=max,scope=task-base
|
||||
- id: browser
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: plane/Dockerfile.base
|
||||
target: base-browser
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
build-args: |
|
||||
TASK_BASE_REF=ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}@${{ steps.base.outputs.digest }}
|
||||
tags: |
|
||||
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}-browser
|
||||
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.content }}-browser
|
||||
cache-from: type=gha,scope=task-base-browser
|
||||
cache-to: type=gha,mode=max,scope=task-base-browser
|
||||
- name: Print manual pin update and verify public access
|
||||
env:
|
||||
BASE: ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}@${{ steps.base.outputs.digest }}
|
||||
BROWSER: ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}-browser@${{ steps.browser.outputs.digest }}
|
||||
run: |
|
||||
python3 - <<'PYTHON' >> "$GITHUB_STEP_SUMMARY"
|
||||
import json, os
|
||||
print("Replace plane/task-base.digest with this JSON after both public checks pass:")
|
||||
print("```json")
|
||||
print(json.dumps({"base": os.environ["BASE"], "base-browser": os.environ["BROWSER"]}, indent=2))
|
||||
print("```")
|
||||
PYTHON
|
||||
docker logout ghcr.io
|
||||
docker buildx imagetools inspect "$BASE"
|
||||
docker buildx imagetools inspect "$BROWSER"
|
||||
@@ -22,6 +22,9 @@ build/
|
||||
.cache/openultrasast/
|
||||
**/.cache/openultrasast/
|
||||
artifacts/
|
||||
!src/openultrasast/ruleset/entries/artifacts/
|
||||
!src/openultrasast/ruleset/entries/artifacts/sha256/
|
||||
!src/openultrasast/ruleset/entries/artifacts/sha256/*
|
||||
reports/
|
||||
vector-store/
|
||||
*.sarif
|
||||
@@ -39,3 +42,6 @@ site/
|
||||
|
||||
# Unseen evaluation pointers for non-permissive repositories.
|
||||
benchmarks/unseen/private/
|
||||
|
||||
# Feasibility pilot identities remain local; only aggregate records are tracked.
|
||||
benchmarks/search/private/
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# Breadth ledger: every spec and every top-level capability, with a status and the deletion that closes it.
|
||||
# Draft 2026-10-13 from the 22 spec.json files and the Pathfinder feature inventory (PATHFINDER-2026-10-13/).
|
||||
# Nothing here changes code; it records decisions so a gate can hold the line.
|
||||
#
|
||||
# status values
|
||||
# active = on one of the four lines in .kiro/steering/overview.md (2026-10-13); work may land
|
||||
# done = implemented and closed; the code it produced is owned by a capability below
|
||||
# parked = no work lands; scaffolding (stubs, unused flags, dead task modules) is removed; spec stays as history
|
||||
# retiring = the "closes" deletion is scheduled; the spec directory goes with it
|
||||
# review = Pathfinder 2026-10-13 decides between parked and retiring
|
||||
# removed = the "closes" deletion has run; the entry stays one release as the record of what went
|
||||
#
|
||||
# open_fronts (the weekly number) = specs with status active or review
|
||||
# + capabilities with status review or retiring whose "closes" has not run.
|
||||
#
|
||||
# gate (to implement after the map is approved):
|
||||
# fail when a spec.json reaches phase "tasks-generated" without an entry here;
|
||||
# fail when a new directory under src/openultrasast/ lands without a [capability.*] entry;
|
||||
# fail when an entry has status "retiring" for more than 14 days with its "closes" unexecuted.
|
||||
|
||||
[meta]
|
||||
drafted = "2026-10-13"
|
||||
spec_count = 22
|
||||
open_fronts = 0 # filled by the gate script once it exists
|
||||
|
||||
# ---------------------------------------------------------------- specs
|
||||
|
||||
[spec.hunter-learning-loop]
|
||||
status = "active"
|
||||
line = "hunter-learning-loop"
|
||||
phase = "requirements-generated"
|
||||
owner = ["src/openultrasast/learn", "src/openultrasast/tool_hunter.py", "src/openultrasast/hunter_tools.py"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.pre-push-safety-net]
|
||||
status = "active"
|
||||
line = "pre-push-safety-net"
|
||||
phase = "implementation (77 of 87 tasks)"
|
||||
owner = ["src/openultrasast/push"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.search-with-proof]
|
||||
status = "active"
|
||||
line = "search-with-proof"
|
||||
phase = "tasks-generated (11 of 24 done)"
|
||||
owner = ["src/openultrasast/search", "src/openultrasast/sandbox"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.proof-arm-opencode]
|
||||
status = "active"
|
||||
line = "search-with-proof"
|
||||
phase = "brief"
|
||||
owner = ["src/openultrasast/search"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.engine-change-scoped-analysis]
|
||||
status = "active"
|
||||
line = "engine-change-scoped-analysis"
|
||||
phase = "brief"
|
||||
owner = ["src/openultrasast/cpg", "src/openultrasast/model", "src/openultrasast/semantic"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.valid-detection-benchmark]
|
||||
status = "active"
|
||||
line = "hunter-learning-loop" # the benchmark every line is measured on
|
||||
phase = "tasks (18 of 24 done)"
|
||||
owner = ["benchmarks/valid", "benchmarks/unseen/replay.py", "benchmarks/unseen/score.py"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.learned-decision-engine]
|
||||
status = "parked"
|
||||
phase = "implementation (20 of 47 tasks)"
|
||||
owner = ["src/openultrasast/learn"]
|
||||
note = "the classifier it built is the second stage of the hunter arm and stays; new learn work goes through hunter-learning-loop, not here"
|
||||
closes = "mark remaining 27 tasks superseded by hunter-learning-loop; no code deletion"
|
||||
|
||||
[spec.plane-on-kubernetes]
|
||||
status = "parked"
|
||||
phase = "implementation (14 of 43 tasks)"
|
||||
owner = ["src/openultrasast/plane"]
|
||||
note = "kube move paused after groups 1-2 (2026-10-02); the ax lane is blocked on the gVisor checkpoint path (operator-owned)"
|
||||
closes = "when the ax lane is restored: finish; otherwise delete plane/runner.py container path and ops/k8s"
|
||||
|
||||
[spec.contributor-scan]
|
||||
status = "parked"
|
||||
phase = "implementation (27 of 51 tasks), paused since 2026-09-23"
|
||||
owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
|
||||
closes = "fold the open census/family-context tasks into engine-change-scoped-analysis; close this spec"
|
||||
|
||||
[spec.mcp-shift-left-agent]
|
||||
status = "parked"
|
||||
phase = "brief"
|
||||
owner = ["src/openultrasast/mcp.py", "src/openultrasast/skills.py", "src/openultrasast/index.py"]
|
||||
note = "follow-up to the pre-push hook; waits for a hook that catches something"
|
||||
closes = "if not started by 2026-12-01: delete mcp.py, skills.py, index.py and their tests"
|
||||
|
||||
[spec.authorization-obligations]
|
||||
status = "review"
|
||||
phase = "tasks-generated (0 of 24)"
|
||||
owner = ["src/openultrasast/semantic/obligations"]
|
||||
note = "overview.md calls it superseded in practice by the dominance arbiter; obligations code exists and is called from scan (cli.py:1388)"
|
||||
closes = "Pathfinder U5 decides: keep obligations as an annotation producer for the hunter, or delete semantic/obligations and its tests"
|
||||
|
||||
[spec.finding-feedback-loop]
|
||||
status = "retiring"
|
||||
phase = "design, dormant since 2026-09-09"
|
||||
owner = []
|
||||
closes = "delete .kiro/specs/finding-feedback-loop (no code was written)"
|
||||
|
||||
[spec.reachability-flow-model]
|
||||
status = "retiring"
|
||||
phase = "tasks-generated (0 of 23), dormant"
|
||||
owner = []
|
||||
note = "largely delivered by adopting Joern (overview.md)"
|
||||
closes = "delete .kiro/specs/reachability-flow-model"
|
||||
|
||||
[spec.flow-aware-ranking]
|
||||
status = "retiring"
|
||||
phase = "brief"
|
||||
owner = []
|
||||
closes = "delete .kiro/specs/flow-aware-ranking; keep the one sentence it owns in safety-net.md"
|
||||
|
||||
[spec.zero-with-a-reason]
|
||||
status = "retiring"
|
||||
phase = "brief"
|
||||
owner = []
|
||||
note = "its idea (coverage gaps are reported, never a clean verdict) is already a product principle in safety-net.md"
|
||||
closes = "delete .kiro/specs/zero-with-a-reason"
|
||||
|
||||
[spec.ir-consolidation]
|
||||
status = "retiring"
|
||||
phase = "brief"
|
||||
owner = []
|
||||
closes = "delete .kiro/specs/ir-consolidation"
|
||||
|
||||
[spec.ai-service-plane]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/plane"]
|
||||
|
||||
[spec.model-grounded-detection]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
|
||||
|
||||
[spec.pair-corpus-honesty]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/pairs.py", "benchmarks/pairs"]
|
||||
|
||||
[spec.corpus-seeded-mechanisms]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/learn"]
|
||||
|
||||
[spec.propose-adjudicate-prove]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/semantic", "src/openultrasast/ruleset"]
|
||||
|
||||
[spec.tree-sitter-overlay-extra]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/semantic"]
|
||||
|
||||
# ---------------------------------------------------------------- capabilities (top-level code)
|
||||
|
||||
[capability.push]
|
||||
status = "active"
|
||||
spec = "pre-push-safety-net"
|
||||
lines = 5074
|
||||
|
||||
[capability.engine]
|
||||
status = "active"
|
||||
spec = "engine-change-scoped-analysis"
|
||||
modules = ["cpg", "model (minus endpoint.py)"]
|
||||
lines = 7764
|
||||
|
||||
[capability.semantic]
|
||||
status = "active"
|
||||
spec = "engine-change-scoped-analysis"
|
||||
modules = ["semantic", "complexity"]
|
||||
lines = 4301
|
||||
note = "complexity/ hotspots feed the hunter; obligations under review (see spec.authorization-obligations)"
|
||||
|
||||
[capability.llm-hunter-and-client]
|
||||
status = "active"
|
||||
spec = "hunter-learning-loop"
|
||||
modules = ["tool_hunter.py", "hunter_tools.py", "provider/openrouter.py", "model/endpoint.py", "plane/budget.py"]
|
||||
lines = 1700
|
||||
note = "four never-enabled hunter tools removed 2026-10-13 (P0); P2 collapses the client stack to one client, one meter, one resolver"
|
||||
|
||||
[capability.learn]
|
||||
status = "active"
|
||||
spec = "hunter-learning-loop"
|
||||
modules = ["learn", "plane/memory.py", "plane/tasks (fact producers)"]
|
||||
lines = 11350
|
||||
|
||||
[capability.search-with-proof]
|
||||
status = "active"
|
||||
spec = "search-with-proof"
|
||||
modules = ["search", "sandbox"]
|
||||
lines = 4803
|
||||
|
||||
[capability.core]
|
||||
status = "active"
|
||||
modules = ["cli.py", "config.py", "contracts.py", "preprocess.py", "source.py", "findings.py", "rank.py", "mapping.py", "ruleset", "policy", "reports.py", "stages.py", "run.py", "provenance.py", "profiling.py"]
|
||||
# source.py (P1, 2026-10-13) owns function ranges, enclosing function, bounds, call sites, product files, declaration
|
||||
# tables and language aliases; learn no longer imports plane.tasks
|
||||
note = "hunter.py is the regex quick-rule pool and should be renamed"
|
||||
|
||||
[capability.pairs-and-benchmarks]
|
||||
status = "active"
|
||||
spec = "valid-detection-benchmark"
|
||||
modules = ["pairs.py", "benchmark.py", "repos.py"]
|
||||
note = "pairs.py label functions feed learn/labels.py; the `ousast pairs` CLI surface is under review (U10)"
|
||||
|
||||
[capability.plane-executor]
|
||||
status = "review"
|
||||
spec = "plane-on-kubernetes"
|
||||
modules = ["plane (reconciler, runner, generate, manifests, harvest, egress, delivery, router, doctor, profile, engine_alerts, workspaces)"]
|
||||
lines = 4885
|
||||
closes = "Pathfinder U8 lists which modules only the retired HarnessX loop used (engine_alerts, harvest, workspaces, profile are the suspects); delete those"
|
||||
|
||||
[capability.regress]
|
||||
status = "removed" # f5a904b, 2026-10-13: deep mode records regress_retired; proof is the search line
|
||||
modules = ["regress"]
|
||||
lines = 549
|
||||
closes = "Pathfinder U9: if search/ covers the triggerable verdict, delete regress/ and the deep-mode REGRESS branch in cli.py:839-880"
|
||||
|
||||
[capability.improve]
|
||||
status = "removed" # 94e96c6, 2026-10-13
|
||||
modules = ["improve"]
|
||||
lines = 1175
|
||||
note = "HarnessX-era improve lever; no active spec names it"
|
||||
closes = "delete improve/, `ousast improve` (cli.py:237, 433, 1106), plane/tasks/loop.py lazy imports, tests/test_cli_improve.py, tests/test_improve_*.py"
|
||||
|
||||
[capability.scan-extras]
|
||||
status = "review"
|
||||
modules = ["fusion.py", "verification.py", "calibration.py", "scoring"] # harness.py processor machinery removed 2026-10-13
|
||||
closes = "U1 found fusion decisions never reach findings or score, verification runs before MAP, processors unreachable; decide keep-and-fix or delete per module"
|
||||
|
||||
[capability.mcp-and-skills]
|
||||
status = "parked"
|
||||
spec = "mcp-shift-left-agent"
|
||||
modules = ["mcp.py"] # skills.py and index.py removed f5a904b, 2026-10-13
|
||||
lines = 830
|
||||
closes = "see spec.mcp-shift-left-agent"
|
||||
|
||||
[capability.gates]
|
||||
status = "review"
|
||||
modules = ["gate.py", "map_gate.py", "pair_gate.py"]
|
||||
lines = 428
|
||||
closes = "U10: keep gate.py (CI + plane loop); delete map_gate.py and pair_gate.py if CI does not need them"
|
||||
|
||||
[capability.push-evaluation-instruments]
|
||||
status = "review"
|
||||
modules = ["push_scoring.py"] # push_inputs.py moved to benchmarks/push 2026-10-13; push_scoring stays (eligibility.py:18)
|
||||
lines = 462
|
||||
closes = "U3: move under benchmarks/push or delete if only tests use them"
|
||||
|
||||
[capability.quick-rules-and-overlay]
|
||||
status = "review"
|
||||
modules = ["findings.py", "ruleset", "semantic (taint, overlay, prove_filter)"]
|
||||
note = "maintainer 2026-10-13: 'semgrep oss is a good idea'; brief in benchmarks/measurements/2026-10-13-semgrep-brief/ says registry rules are not shippable, our converted rules are; 4.7 s startup floor"
|
||||
closes = "run the Semgrep arm on the valid pilot (benchmarks/valid/semgrep_arm.py) as a comparison; it may replace the regex quick tier only (same job: pattern matches), never act as a filter on other tiers. A Semgrep miss is never evidence (maintainer 2026-10-13). If it matches the quick tier at least as well, retire findings.py and ruleset/*.py and amend safety-net.md with the measurement"
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
# Design Document
|
||||
|
||||
## Overview
|
||||
The access-control prover remains a deterministic rule arm over the existing obligation checker. The amendment
|
||||
changes the semantic contract: resolution, resource matching, authority matching and dominance return abstract
|
||||
relations rather than booleans, and every examined path receives one of three judgments: `unguarded`,
|
||||
`discharged`, or `indeterminate`. A degraded instrument may reduce precision to `indeterminate`; it cannot
|
||||
manufacture either final verdict.
|
||||
|
||||
Target knowledge remains outside the engine. Entry shapes live in registered recognition strategies, while
|
||||
operations, dischargers, resource rules and authority implications live in promoted fact data. A label-blind
|
||||
initialization job produces that knowledge, and evaluation freezes its digest before admission labels are opened.
|
||||
|
||||
### Goals
|
||||
- Produce an unguarded finding only from a complete, sufficiently resolved evidence path.
|
||||
- Make call, path, resource, authority and control-flow uncertainty explicit and monotone.
|
||||
- Carry target knowledge as verified data or recogniser strategies, never engine branches.
|
||||
- Attribute development gains across analyzer and fact changes before measuring an untouched admission population.
|
||||
- Fail the runtime and instrument gates rather than summarize partial work as a result.
|
||||
|
||||
### Non-Goals
|
||||
- Taint analysis, Joern integration, model calls inside the prover, or vulnerability families beyond access control.
|
||||
- Client-side presentation gating, proof execution, pre-push wiring, or admission-policy thresholds.
|
||||
- Replacing the existing order-based checker mode for consumers that do not opt into prover semantics.
|
||||
|
||||
## Boundary Commitments
|
||||
|
||||
### This Spec Owns
|
||||
- The abstract evidence domain and three-way pair/path judgment.
|
||||
- Stable program identities, conservative call-target sets and bounded interprocedural summaries.
|
||||
- Resource- and authority-sensitive operation/witness matching.
|
||||
- Prover-mode control-flow dominance without textual-order fallback.
|
||||
- Acquired entry, operation, discharger and authority knowledge needed by this family.
|
||||
- The production access-control detection-core family contract, independently reviewed
|
||||
target-knowledge authority, structural matcher, and canonical observation/match writer.
|
||||
- Deterministic traces, failure-visible coverage, development arms and the admission-run contract.
|
||||
|
||||
### Out of Boundary
|
||||
- The legacy checker behavior used by existing callers; default APIs remain backward compatible.
|
||||
- Target labels and benchmark curation, owned by `valid-detection-benchmark`.
|
||||
- Runtime demonstrations, owned by `search-with-proof`.
|
||||
- Hook admission and rollout, owned by `pre-push-safety-net`.
|
||||
- Continuous fact evolution after the one-time initialization pass.
|
||||
- Family-neutral fact, match, evidence, readiness and admission validation, owned by
|
||||
`detection-core`.
|
||||
|
||||
### Allowed Dependencies
|
||||
- `openultrasast.source`, `openultrasast.mapping`, the existing semantic IR and CFG, and the existing obligation
|
||||
facts/checker contracts.
|
||||
- The hunter `Context` caches, valid-benchmark metrics, replay record envelopes and reserved-population guard.
|
||||
- Public detection-core v2 contracts and validators through an adapter dependency from this
|
||||
spec to core; detection-core never imports access-control or target-aware modules.
|
||||
- No new third-party dependency.
|
||||
|
||||
### Revalidation Triggers
|
||||
- A change to `EntryPointRecord`, `Operation`, `Discharge`, `Trace`, hunt-defect or replay-record contracts.
|
||||
- A change to call-site receiver/signature capture, target-knowledge schemas or authority implication semantics.
|
||||
- A change to path aggregation, judgment ordering, admission population isolation or the 120-second budget.
|
||||
- A change to detection-core family, fact-pack, fact-match, provenance, custody or canonical
|
||||
serialization contracts; the adapter must re-run downstream conformance before use.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Existing Architecture Analysis
|
||||
The delivered baseline already recognises entries, composes bounded paths, discovers operations and witnesses,
|
||||
builds structural CFGs, emits traces and scores replay records. Its decision interfaces are boolean: unresolved
|
||||
receivers fall back to a bare name, unavailable CFGs fall back to textual order, and a witness either discharges or
|
||||
does not. The amendment preserves the existing checker as the single verdict owner while inserting typed abstract
|
||||
values before that boolean boundary and adding an opt-in three-way judgment path.
|
||||
|
||||
### Architecture Pattern & Boundary Map
|
||||
~~~mermaid
|
||||
flowchart LR
|
||||
TK[Target knowledge] --> MR[Mapping registry]
|
||||
TK --> FI[Fact interpreter]
|
||||
TK --> FA[Reviewed fact authority]
|
||||
FA --> FM[Structural matcher and artifact writer]
|
||||
FM --> DC[Detection core validation]
|
||||
MR --> CTX[Per change context]
|
||||
FI --> OP[Operations and witnesses]
|
||||
CTX --> SI[Symbol index]
|
||||
SI --> CR[Call resolution]
|
||||
CR --> FS[Function summaries]
|
||||
OP --> FS
|
||||
FS --> PM[Concrete path materializer]
|
||||
CTX --> CFG[Control flow relations]
|
||||
PM --> J[Three way judgment]
|
||||
CFG --> J
|
||||
OP --> J
|
||||
J --> TR[Canonical traces]
|
||||
J --> PR[Prover findings and coverage]
|
||||
TR --> RUN[Development and admission runners]
|
||||
PR --> RUN
|
||||
~~~
|
||||
|
||||
Dependency direction is:
|
||||
`abstract domain -> fact and recogniser adapters -> symbol resolution -> summaries and CFG -> checker judgment
|
||||
-> prover integration -> runners`. Engine modules never import target-named strategy modules. Target-aware
|
||||
adapters may import engine contracts.
|
||||
|
||||
- **Strategy + Registry**: entry recognisers are registered per recognition concern.
|
||||
- **Interpreter**: promoted fact rows describe operations, dischargers, resource rules and authority implications.
|
||||
- **Adapter**: the prover converts repository IR, target knowledge and context caches into engine contracts.
|
||||
- **Abstract interpretation**: finite domains and monotone joins propagate evidence and incompleteness.
|
||||
|
||||
### Technology Stack
|
||||
| Layer | Choice | Role |
|
||||
|---|---|---|
|
||||
| Analysis | Python 3.11, existing semantic IR and structural CFG | symbols, calls, summaries, dominance |
|
||||
| Knowledge | Existing TOML fact files and mapping strategies | target-specific vocabulary |
|
||||
| Core contract | detection-core v2 typed records and validators | family-neutral conformance |
|
||||
| Scoring | Existing valid-benchmark metrics and replay records | development and admission results |
|
||||
|
||||
## File Structure Plan
|
||||
~~~text
|
||||
src/openultrasast/
|
||||
├── mapping/
|
||||
│ ├── registry.py # strategy registration and dispatch
|
||||
│ ├── registration_sites.py # generic call-site registration adapter
|
||||
│ ├── fact_authority.py # authenticate installed reviewed authority
|
||||
│ ├── installed_fact_authority.py # closed JSON decoders and checked-in artifact resolver
|
||||
│ ├── entry_packs.py # compatibility projection and v2 structural adapter
|
||||
│ ├── fact_match_store.py # persist canonical observations and match artifacts
|
||||
│ ├── legacy.py # production caller migrates to authority-bearing adapter
|
||||
│ └── <target>.py # one target-aware strategy per migrated concern
|
||||
├── semantic/
|
||||
│ ├── ir.py # CallSite receiver and signature evidence
|
||||
│ ├── cst.py # receiver, arity and signature capture
|
||||
│ └── obligations/
|
||||
│ ├── abstract_domain.py # relations, completeness, resources, authorities, judgments
|
||||
│ ├── core_contract.py # production access-control FamilyContract
|
||||
│ ├── resolution.py # SymbolId, symbol index and CallTargetSet
|
||||
│ ├── summaries.py # monotone FunctionSummary computation over call components
|
||||
│ ├── paths.py # concrete evidence paths and cutoff records
|
||||
│ ├── operations.py # operation/witness abstract identities
|
||||
│ ├── dominance.py # legacy order mode, unchanged by default
|
||||
│ ├── cfg_dominance.py # prover-mode control-flow relation, no order fallback
|
||||
│ ├── check.py # opt-in abstract matching and PairJudgment
|
||||
│ ├── enforcement.py # target-aware enforcement adapter
|
||||
│ └── trace.py # canonical trace with abstract relations and completeness
|
||||
├── hunt/
|
||||
│ ├── producers.py # cached symbol index and summaries
|
||||
│ └── prover.py # integration, path aggregation, findings and coverage
|
||||
└── tools/
|
||||
└── fact_init.py # label-blind extract/propose/verify/promote workflow
|
||||
ruleset/obligations/*.toml # promoted operation, witness and authority knowledge
|
||||
src/openultrasast/ruleset/entries/
|
||||
├── bootstrap.json # host trust policy, external to authenticated bundle
|
||||
├── authority.json # reviewed bundle payload and immutable references
|
||||
├── express.javascript.v2.json # reviewed JavaScript FactPack
|
||||
├── express.typescript.v2.json # reviewed TypeScript FactPack
|
||||
├── express.json # authenticated non-authoritative compatibility projection
|
||||
├── identities/
|
||||
│ ├── generator.json # producer identity manifest
|
||||
│ ├── maintainer-reviewer.json # maintainer approval identity manifest
|
||||
│ └── custodian.json # installed-artifact custody identity manifest
|
||||
└── artifacts/sha256/<content-digest> # canonical review/disclosure/evidence bytes
|
||||
benchmarks/valid/
|
||||
├── prover_arm.py # records, stage funnel, gates and four-arm comparison
|
||||
└── prover_admission.py # frozen-manifest admission orchestration
|
||||
tests/
|
||||
├── test_obligation_abstract_domain.py
|
||||
├── test_obligation_core_contract.py
|
||||
├── test_obligation_resolution.py
|
||||
├── test_obligation_summaries.py
|
||||
├── test_obligation_judgment.py
|
||||
├── test_obligation_trace.py
|
||||
├── test_mapping_registry.py
|
||||
├── test_fact_init.py
|
||||
├── test_hunt_prover.py
|
||||
└── test_prover_arm.py
|
||||
~~~
|
||||
|
||||
### Modified Files
|
||||
- Existing facts, IR, checker, dominance, trace, prover and arm modules receive additive or opt-in contracts.
|
||||
- Existing callers retain legacy defaults; only the prover selects three-way judgment and strict CFG semantics.
|
||||
- Existing completed benchmark records remain immutable development baselines.
|
||||
- `mapping/entry_packs.py` keeps legacy pattern output byte-stable while adding an explicit
|
||||
authority-bearing v2 path; no-context legacy loading cannot emit detection-core evidence.
|
||||
|
||||
## System Flows
|
||||
|
||||
### Analysis
|
||||
~~~mermaid
|
||||
flowchart TD
|
||||
A[Read cached source] --> B[Recognise entries and facts]
|
||||
B --> C[Build symbol index]
|
||||
C --> D[Resolve call target sets]
|
||||
D --> E[Compute function summaries]
|
||||
E --> F[Materialise evidence paths]
|
||||
F --> G[Match witnesses to obligations]
|
||||
G --> H[Evaluate control flow]
|
||||
H --> I{Path state}
|
||||
I -->|complete and missing| U[Unguarded]
|
||||
I -->|complete and governed| DCH[Discharged]
|
||||
I -->|possible unknown or cut| IND[Indeterminate]
|
||||
U --> FIND[Finding and trace]
|
||||
DCH --> TRACE[Trace only]
|
||||
IND --> TRACE
|
||||
~~~
|
||||
|
||||
### Evaluation
|
||||
~~~mermaid
|
||||
flowchart LR
|
||||
ACQ[Label blind acquisition] --> FREEZE[Freeze candidate bundle]
|
||||
BASE[Materialize baseline bundle] --> DEV[Four arm development comparison]
|
||||
FREEZE --> DEV
|
||||
DEV --> AUDIT[Review ordinary labels]
|
||||
AUDIT --> HOLD[Untouched paired admission run]
|
||||
HOLD --> GATE{Instrument budget and quality gates}
|
||||
GATE -->|pass| GO[G1 eligible]
|
||||
GATE -->|fail| NOGO[No go]
|
||||
~~~
|
||||
|
||||
### Reviewed fact adaptation
|
||||
~~~mermaid
|
||||
flowchart LR
|
||||
BP[Host bootstrap policy] --> AUTH[Authenticate authority bundle]
|
||||
DATA[Reviewed contract packs and projection] --> AUTH
|
||||
AUTH --> MATCH[Match typed repository structure]
|
||||
MATCH --> OBS[Persist canonical observations]
|
||||
OBS --> EVID[Build and persist FactMatchEvidence]
|
||||
EVID --> CORE[Detection core validate and apply]
|
||||
LEGACY[Legacy pattern projection] --> RECOG[Compatibility recognition only]
|
||||
LEGACY -. forbidden .-> CORE
|
||||
~~~
|
||||
|
||||
The access-control adapter writes each observation before constructing the match, writes the
|
||||
canonical match bytes next, and only then calls core with the immutable references. The
|
||||
resolver exposed to core is read-only. A regex or name hit can preserve recognition during
|
||||
migration but cannot substitute for typed structural observations.
|
||||
|
||||
## Requirements Traceability
|
||||
| Requirement | Components |
|
||||
|---|---|
|
||||
| 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 | mapping registry, registration adapter, fact initialization, coverage |
|
||||
| 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10 | fact interpreter, abstract domain, operations, enforcement, witness matcher |
|
||||
| 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10 | symbol resolution, summaries, path materializer |
|
||||
| 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7 | pair judgment, checker integration, prover findings |
|
||||
| 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7 | prover arm, four-arm development comparison |
|
||||
| 6.1, 6.2, 6.3 | context caches, deadline state, runner budget gate |
|
||||
| 7.1, 7.2, 7.3, 7.4, 7.5, 7.6 | completeness, prover coverage, runner funnel |
|
||||
| 8.1, 8.2, 8.3 | finding contract, trace references, proof-arm filter |
|
||||
| 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7 | canonical trace and record provenance |
|
||||
| 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7 | strict CFG dominance and legacy order compatibility |
|
||||
| 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8 | abstract domain, path and operation aggregation, transition report |
|
||||
| 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9 | acquisition record, freeze manifest, admission runner |
|
||||
| 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8 | access-control family contract, reviewed fact authority, structural matcher, artifact writer, detection-core conformance, caller cutover |
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### Abstract evidence domain
|
||||
~~~python
|
||||
class EvidenceRelation(Enum):
|
||||
MUST = "must"
|
||||
MAY = "may"
|
||||
NO = "no"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
class PairJudgment(Enum):
|
||||
UNGUARDED = "unguarded"
|
||||
DISCHARGED = "discharged"
|
||||
INDETERMINATE = "indeterminate"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Completeness:
|
||||
is_complete: bool
|
||||
reasons: tuple[str, ...] = ()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResourceRef:
|
||||
kind: Literal["exact", "aliases", "request_global", "unknown"]
|
||||
values: tuple[str, ...] = ()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuthorityRef:
|
||||
values: tuple[str, ...]
|
||||
is_unknown: bool = False
|
||||
~~~
|
||||
|
||||
The partial order places definite evidence below possible or unknown evidence. Join never converts `MAY` or
|
||||
`UNKNOWN` into `MUST` or `NO`. The precision order is:
|
||||
|
||||
~~~text
|
||||
MUST <= MAY <= UNKNOWN
|
||||
NO <= MAY <= UNKNOWN
|
||||
~~~
|
||||
|
||||
`MUST` and `NO` are incomparable. The alternative-path join is fixed by this table:
|
||||
|
||||
| join | MUST | NO | MAY | UNKNOWN |
|
||||
|---|---|---|---|---|
|
||||
| MUST | MUST | MAY | MAY | UNKNOWN |
|
||||
| NO | MAY | NO | MAY | UNKNOWN |
|
||||
| MAY | MAY | MAY | MAY | UNKNOWN |
|
||||
| UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
|
||||
|
||||
Three separate operations prevent one table from being used for the wrong quantifier:
|
||||
|
||||
1. `join_alternatives` uses the table above when alternative call targets or control paths contribute evidence.
|
||||
2. `conjoin_dimensions` combines kind, scope, resource, authority and dominance for one witness: any `NO`
|
||||
produces `NO`; otherwise any `UNKNOWN` produces `UNKNOWN`; otherwise any `MAY` produces `MAY`; only
|
||||
all-`MUST` produces `MUST`.
|
||||
3. `aggregate_witnesses` asks whether any witness definitely governs one obligation: any `MUST` produces
|
||||
`MUST`; otherwise `UNKNOWN` takes precedence over `MAY`, and `MAY` takes precedence over `NO`.
|
||||
An empty witness set is `NO` only when discovery and facts are complete; otherwise it is `UNKNOWN`.
|
||||
|
||||
For one complete path, all required obligations at `MUST` yield `discharged`; any `MAY` or `UNKNOWN` yields
|
||||
`indeterminate`; otherwise any `NO` yields `unguarded` and every `NO` obligation is reported missing.
|
||||
Thus a mixed `[MUST, NO]` obligation vector is unguarded for the `NO` obligation. Resource
|
||||
alias sets join by finite union; an unbounded or unresolvable value joins to `unknown`. Authority implication is
|
||||
read from promoted facts and contributes its fact identifier to the match evidence.
|
||||
|
||||
### Symbol resolution
|
||||
~~~python
|
||||
@dataclass(frozen=True, order=True)
|
||||
class SymbolId:
|
||||
module: str
|
||||
enclosing_type: str | None
|
||||
name: str
|
||||
signature: tuple[str, ...] | None
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CallTargetSet:
|
||||
targets: tuple[SymbolId, ...]
|
||||
completeness: Completeness
|
||||
~~~
|
||||
|
||||
The symbol index covers repository-defined declarations and inheritance. Exact receiver and full signature produce
|
||||
one complete target. Partial type or signature information produces a finite candidate set with an incompleteness
|
||||
reason. No call falls back to a global bare-name key. Base declarations attach through explicit descendant edges.
|
||||
|
||||
### Function summaries and paths
|
||||
~~~python
|
||||
@dataclass(frozen=True)
|
||||
class FunctionSummary:
|
||||
symbol: SymbolId
|
||||
operations: tuple[Operation, ...]
|
||||
witnesses: tuple[Discharge, ...]
|
||||
calls: tuple[CallTargetSet, ...]
|
||||
completeness: Completeness
|
||||
~~~
|
||||
|
||||
Summaries are computed by a monotone worklist over strongly connected call components. Their finite contents grow
|
||||
by union until stable. A budget cutoff stops growth, records the cutoff and leaves affected summaries incomplete.
|
||||
Concrete paths are then materialised from recognised entries through exact targets for evidence. Candidate edges
|
||||
may contribute diagnostic traces but cannot complete a final path.
|
||||
|
||||
### Witness matching and judgment
|
||||
~~~python
|
||||
@dataclass(frozen=True)
|
||||
class WitnessMatch:
|
||||
relation: EvidenceRelation
|
||||
resource_relation: EvidenceRelation
|
||||
authority_relation: EvidenceRelation
|
||||
evidence: tuple[str, ...]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PathJudgment:
|
||||
judgment: PairJudgment
|
||||
missing: tuple[str, ...]
|
||||
completeness: Completeness
|
||||
matches: tuple[WitnessMatch, ...]
|
||||
~~~
|
||||
|
||||
A witness governs an obligation only when kind, scope, resource, authority and dominance are all `MUST`.
|
||||
Any `MAY` or `UNKNOWN` required relation makes the path indeterminate. With complete evidence and no governing
|
||||
witness for a required kind, the path is unguarded. Multiple paths aggregate as follows:
|
||||
|
||||
- any complete unguarded path -> `unguarded`;
|
||||
- complete enumeration and every path discharged -> `discharged`;
|
||||
- otherwise -> `indeterminate`.
|
||||
|
||||
The checker gains an opt-in abstract mode. Legacy callers keep the current boolean result and order-based default.
|
||||
|
||||
### Strict CFG dominance
|
||||
`CfgDominance.relation` returns an evidence relation and a guard effect. Prover mode never calls
|
||||
`OrderDominance` when a graph, line mapping or construct is unavailable; it returns `UNKNOWN` with
|
||||
`cfg_unavailable` or `line_without_node`. Route and declaration scopes return `MUST` only after exact handler
|
||||
attachment. The legacy dominance class remains unchanged.
|
||||
|
||||
### Target knowledge and acquisition
|
||||
The mapping registry and fact interpreter are target-aware adapters. The initialization job has:
|
||||
|
||||
- extraction mode for existing strategies and fact rows;
|
||||
- proposal mode for source- or documentation-derived candidate knowledge;
|
||||
- verification that checks source, version, region and concrete matches;
|
||||
- promotion that writes only verified rows or enabled strategies;
|
||||
- an acquisition record containing input revision, evidence digest, accepted/rejected proposals and output digest.
|
||||
|
||||
Admission mode prevents the acquisition process from receiving benchmark labels or fix locations. A changed output
|
||||
digest after labels are opened spends the population.
|
||||
|
||||
### Reviewed detection-core fact adapter
|
||||
|
||||
The production contract is family-specific but target-neutral. Its initial reviewed
|
||||
identity is `family_id=access_control`, `family_version=2.0.0`,
|
||||
`schema_version=openultrasast.detection-core.evidence-packet.v2`, and
|
||||
`ecosystems=(express,)`; later ecosystems require a reviewed contract-version change. It
|
||||
uses the security object `entry_operation_authorization` with required text identity fields
|
||||
`operation` and `boundary`. Its closed vocabulary is:
|
||||
|
||||
| Role | Contract |
|
||||
|---|---|
|
||||
| `operation` | no fields; `OPERATION_EFFECT`; `product_or_reviewed_fact` |
|
||||
| `constraint` | no fields; `CONSTRAINT_EFFECT`; `product_or_reviewed_fact` |
|
||||
| `security_object` | required text fields `operation` and `boundary`; no relation kinds; `product_or_reviewed_fact` |
|
||||
| `framework_factory` | no fields; `REGISTRATION`; `product_or_reviewed_fact` |
|
||||
| `framework_instance` | no fields; `REGISTRATION`; `product_or_reviewed_fact` |
|
||||
| `entry_handler` | no fields; `INVOCATION`; `product_or_reviewed_fact` |
|
||||
| `entry_registration` | no fields; `REGISTRATION`; `product_or_reviewed_fact` |
|
||||
|
||||
The decision rules are exactly `unguarded -> violation`, `discharged -> satisfied`, and
|
||||
`indeterminate -> indeterminate`. The two determinate decisions require `operation`,
|
||||
`constraint`, and `security_object` with allowed statuses `(COMPLETE,)`; the indeterminate
|
||||
decision requires `constraint` with allowed statuses `(INCOMPLETE, UNRESOLVED)`. Residual question
|
||||
`is_path_authorized` requires `operation`, `constraint`, and `security_object` and permits
|
||||
all three decisions. The initial indeterminate-reason set includes
|
||||
`missing_constraint_relation` and `ambiguous_constraint_relation`; additions require a
|
||||
contract-version change and downstream revalidation.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class EntryFactAuthorityBundle:
|
||||
contract: FamilyContract
|
||||
packs: tuple[FactPack, ...]
|
||||
recognition_projection: ContentRef
|
||||
lineages: tuple[LineageManifest, ...]
|
||||
custody_policy: PopulationCustodyPolicy
|
||||
review_policy: ReviewIndependencePolicy
|
||||
matcher_revision: str
|
||||
matcher_behavior_digest: str
|
||||
matcher_lineage_manifest_digest: str
|
||||
authority_review: IndependenceAttestation
|
||||
bundle_digest: str
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntryAuthorityBootstrapPolicy:
|
||||
reviewed_bundle_reviewer_digests: tuple[str, ...]
|
||||
forbidden_identity_pairs: tuple[tuple[str, str], ...]
|
||||
trusted_store_identities: tuple[str, ...]
|
||||
maximum_bundle_bytes: int
|
||||
policy_digest: str
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntryFactAuthorityContext:
|
||||
bundle: EntryFactAuthorityBundle
|
||||
custody: PopulationCustodyPort
|
||||
resolver: ContentResolver
|
||||
|
||||
class FactMatchArtifactWriter(Protocol):
|
||||
def persist_observation(self, value: StructuralObservation) -> ContentRef: ...
|
||||
def persist_match(self, value: FactMatchEvidence) -> ContentRef: ...
|
||||
```
|
||||
|
||||
The authority review subject is the canonical bundle payload digest with
|
||||
`authority_review` omitted. The host supplies the bootstrap policy separately; neither the
|
||||
bundle nor mapping code may choose its own trusted reviewers or stores. Validation
|
||||
re-derives policy, attestation, disclosure, pack, projection, lineage, and bundle digests;
|
||||
requires producer-reviewer separation; applies forbidden identity pairs; resolves every
|
||||
referenced byte from an admitted store; and refuses missing, duplicate, or substituted
|
||||
language packs. Because `FactPack.language` is singular, a multi-language target has one
|
||||
reviewed pack per language with shared semantic intent but distinct canonical identities.
|
||||
The shipped host trust policy is loaded from `bootstrap.json`, never referenced as authority
|
||||
by the bundle it authenticates, and is replaceable by host configuration. The artifact
|
||||
writer receives an explicit store root, store identity, and retention-policy digest; it
|
||||
writes content-addressed canonical bytes and returns a read-only resolver plus re-derivable
|
||||
storage receipts admitted by that bootstrap policy.
|
||||
|
||||
#### Installed authority wire and review ceremony
|
||||
|
||||
The checked-in authority uses closed canonical JSON records; decoders reject unknown or
|
||||
missing fields before constructing typed objects. `bootstrap.json` has schema
|
||||
`openultrasast.access-control.entry-authority-bootstrap.v2` and exactly the five
|
||||
`EntryAuthorityBootstrapPolicy` fields. `authority.json` has schema
|
||||
`openultrasast.access-control.installed-entry-authority.v1` and exactly:
|
||||
|
||||
- `pack_files`: the ordered pair `express.javascript.v2.json`,
|
||||
`express.typescript.v2.json`;
|
||||
- `recognition_projection`, lineages, custody policy, review policy, matcher revision,
|
||||
matcher behavior digest, matcher lineage digest, authority review, and bundle digest;
|
||||
- no trust-root, reviewer-admission, or store-admission fields.
|
||||
|
||||
Each v2 pack file is canonical JSON over every public `FactPack` field and the complete
|
||||
nested `SemanticFact`, structural-requirement, relation-effect, knowledge-evidence,
|
||||
attestation, and `ContentRef` values. Enum values use their public string values. The
|
||||
installed decoder accepts only `FACT_PACK_SCHEMA`, re-encodes the typed value, and requires
|
||||
byte equality so alternate/unknown encodings cannot share authority. `authority.json`
|
||||
embeds policies and lineages because those values participate directly in the typed bundle;
|
||||
review evidence and disclosures remain immutable `ContentRef` values.
|
||||
|
||||
All referenced bytes live under `artifacts/sha256/<content_digest>`. The installed resolver
|
||||
validates the lowercase SHA-256 digest first and derives the filesystem basename solely from
|
||||
`ContentRef.content_digest`; `artifact_id` is diagnostic metadata and is never interpreted as
|
||||
a path. The resolver verifies byte length and digest, and derives
|
||||
the `StorageReceipt` from the bootstrap-admitted store identity
|
||||
`openultrasast-installed-entry-artifacts-v1`, the reference retention-policy digest, content
|
||||
digest, and length. Receipts are derived, not separately mutable files. Pack filenames are
|
||||
fixed by the closed authority schema; their typed normalized identities are covered by the
|
||||
bundle digest, so substituting either pack changes authority even though the filenames are
|
||||
not trust roots.
|
||||
|
||||
The approved repo-local ceremony records three distinct canonical identity manifests under
|
||||
schema `openultrasast.access-control.authority-identity.v1`. Every manifest has exactly
|
||||
`schema_version`, `role`, `name`, and `approval_basis`; its identity is the digest of its
|
||||
canonical bytes, and the loader rejects unknown fields or noncanonical bytes. The fixed rows
|
||||
are:
|
||||
|
||||
| File | `role` | `name` | `approval_basis` |
|
||||
|---|---|---|---|
|
||||
| `generator.json` | `producer` | `openultrasast-entry-pack-generator-v1` | `generated-from-reviewed-entry-data` |
|
||||
| `maintainer-reviewer.json` | `reviewer` | `openultrasast-maintainer` | `maintainer-session-approval:2026-11-01` |
|
||||
| `custodian.json` | `custodian` | `openultrasast-installed-entry-custodian-v1` | `checked-in-artifact-custody` |
|
||||
|
||||
The ceremony loader requires bootstrap reviewer admission to equal the reviewer-manifest
|
||||
digest, custody-policy admission to equal the custodian-manifest digest, authority review
|
||||
producer/reviewer identities to equal the generator/reviewer digests, and pack evidence to
|
||||
use the same producer/reviewer identities. The maintainer explicitly approved this ceremony
|
||||
on 2026-11-01; that approval authorizes recording the reviewer identity but does not let the
|
||||
generator choose a different reviewer. Bootstrap admits only that reviewer digest and the
|
||||
installed store identity. Producer and reviewer must differ, the pair remains subject to the
|
||||
forbidden-pair policy, conflict disclosures must be canonical and resolved, and any identity
|
||||
field or approval-basis change creates a new digest and invalidates these bindings until a
|
||||
new human review.
|
||||
|
||||
The structural adapter consumes promoted recognition output and repository structure. For
|
||||
each fact it emits the complete typed requirement set (qualified target, AST shape,
|
||||
receiver, call target, argument shape, or configuration as declared), persists every
|
||||
canonical `StructuralObservation`, builds `FactMatchEvidence`, persists
|
||||
`fact_match_bytes(match)`, and then calls detection-core with the match plus its content
|
||||
reference. Match construction never accepts a future self-reference: the reference is an
|
||||
input only to core application after persistence.
|
||||
|
||||
#### Initial compatibility migration evidence
|
||||
|
||||
The first reviewed migration preserves the existing Express recognition rows as data, not
|
||||
engine vocabulary. `express.javascript.v2.json` and `express.typescript.v2.json` carry the
|
||||
same semantic expansion under distinct language-scoped pack identities. The authenticated
|
||||
projection uses schema `openultrasast.entry-recognition-projection.v1` and closed row fields
|
||||
`legacy_fact_id`, `languages`, `defines_handler`, `handler_kind`, and `pattern`; its content
|
||||
reference is covered by the authority bundle digest. The projection keeps legacy regular
|
||||
expressions and IDs byte-stable while the v2 packs use these exact facts:
|
||||
|
||||
| Legacy data row | Reviewed v2 fact | Required observations | Effect |
|
||||
|---|---|---|---|
|
||||
| `entry:express:app-route` | for each `method` in `get`, `post`, `put`, `delete`, `patch`, `all`, `use`: ID `entry:express:app-route:<method>`, fact role `entry_registration` | `qualified_target`: `QUALIFIED_NAME`, field `qualified_target`, expected `app.<method>`; `call_shape`: `AST_SHAPE`, field `call_shape`, expected `member_call`; `receiver`: `RECEIVER_TYPE`, field `receiver`, expected `express_application`; `arguments`: `ARGUMENT_SHAPE`, field `arguments`, expected `route_path_and_handler` | kind `REGISTRATION`, role `entry_registration`, source `framework_instance`, target `entry_handler`, conditions `()` |
|
||||
| `entry:express:router-route` | for the same methods: ID `entry:express:router-route:<method>`, fact role `entry_registration` | `qualified_target`: `QUALIFIED_NAME`, field `qualified_target`, expected `router.<method>`; `call_shape`: `AST_SHAPE`, field `call_shape`, expected `member_call`; `receiver`: `RECEIVER_TYPE`, field `receiver`, expected `express_router`; `arguments`: `ARGUMENT_SHAPE`, field `arguments`, expected `route_path_and_handler` | kind `REGISTRATION`, role `entry_registration`, source `framework_instance`, target `entry_handler`, conditions `()` |
|
||||
| `entry:express:express-app` | ID `entry:express:express-app`, fact role `framework_instance` | `qualified_target`: `QUALIFIED_NAME`, field `qualified_target`, expected `express`; `call_shape`: `AST_SHAPE`, field `call_shape`, expected `factory_call`; `binding`: `ARGUMENT_SHAPE`, field `binding`, expected `application_instance_binding` | kind `REGISTRATION`, role `framework_instance`, source `framework_factory`, target `framework_instance`, conditions `()` |
|
||||
|
||||
The promoted route-method values are read from reviewed projection data rather than copied
|
||||
into engine branches. A pattern hit alone is `INCOMPLETE`; it cannot produce
|
||||
`FactMatchEvidence`, enter readiness, or support admission. The no-context legacy loader is
|
||||
removed only after `mapping/legacy.py` uses the authority-bearing path, a repository census
|
||||
finds no caller that treats the projection as v2 evidence or bypasses authority, and the
|
||||
core conformance handoff passes. `analyze_entry_points` remains the stable public entry;
|
||||
bootstrap and adapter construction occur at its target-aware mapping boundary rather than
|
||||
being pushed into neutral engine callers.
|
||||
|
||||
### Prover integration
|
||||
`prove_change` orchestrates cached reads, recognition, resolution, summaries, paths, strict dominance and
|
||||
judgment. It emits findings only for `unguarded` pairs and preserves all three judgments as traces. Coverage
|
||||
contains stage counts, reason counts, pair judgments and exploration cutoffs. Exceptions are recorded at the stage
|
||||
that raised them.
|
||||
|
||||
### Development and admission runners
|
||||
Before comparison, the development runner materialises and validates two immutable bundles:
|
||||
|
||||
~~~python
|
||||
@dataclass(frozen=True)
|
||||
class AnalyzerBundle:
|
||||
analyzer_revision: str
|
||||
fact_set_digest: str
|
||||
acquisition_record_digest: str
|
||||
configuration_digest: str
|
||||
~~~
|
||||
|
||||
The baseline bundle points to the analyzer revision and fact set that produced the immutable 2026-10-28 records;
|
||||
the candidate bundle points to the amended equivalents. The runner refuses a four-arm comparison if either
|
||||
revision or fact artifact cannot be materialised and digest-verified. It then registers baseline
|
||||
analyzer/baseline facts, baseline analyzer/candidate facts, candidate analyzer/baseline facts and candidate
|
||||
analyzer/candidate facts. This separates knowledge coverage from engine behavior.
|
||||
|
||||
The admission runner accepts only a frozen manifest naming analyzer, facts, acquisition, configuration and
|
||||
population digests. The population manifest is produced and reviewed by `valid-detection-benchmark`; this spec
|
||||
only consumes and validates it. The runner verifies its review and isolation fields, scores its paired
|
||||
vulnerable/fixed cases and reviewed ordinary labels, refuses zero-byte or stale-anchor records, fails any change
|
||||
over 120 seconds, and never converts indeterminate pairs to clean results.
|
||||
|
||||
## Data Models
|
||||
|
||||
### Canonical trace
|
||||
Each trace carries:
|
||||
- entry identity and target-knowledge evidence;
|
||||
- exact hops and call-target completeness;
|
||||
- operation kind, resource and authority;
|
||||
- witnesses with kind, scope, provenance, resource, authority, dominance and match relations;
|
||||
- control context and guard effects;
|
||||
- completeness reasons and final path judgment;
|
||||
- analyzer, fact-set and input digests.
|
||||
|
||||
Repository identity, repository size and function-size features remain outside decision input.
|
||||
|
||||
### Run manifest
|
||||
The manifest contains population role (`development` or `admission`), immutable input digest, analyzer revision,
|
||||
fact-set digest, acquisition-record digest, configuration digest, label-review state and spent-state marker.
|
||||
The analyzer/fact bundle is a separate immutable input whose artifacts must be materialisable by revision and
|
||||
digest; a manifest cannot redefine or synthesize the baseline.
|
||||
|
||||
## Error Handling
|
||||
- Parse, recognition, resolution, summary, CFG and judgment exceptions become stage-specific incompleteness reasons.
|
||||
- Deadline, fan-out, depth and recursion limits preserve accumulated evidence and return indeterminate affected
|
||||
pairs.
|
||||
- Zero-byte reads and stale anchors are instrument failures that invalidate the run.
|
||||
- Missing or mismatched manifest digests invalidate admission rather than falling back to development mode.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit: the complete join table, dimension conjunction, witness aggregation and monotonicity laws; symbol
|
||||
collisions and overload candidates; resource aliases and authority
|
||||
implication; SCC termination; CFG unknown behavior; three-way aggregation.
|
||||
- Metamorphic: renaming a type preserves judgment; adding an unresolved overload can only preserve a judgment or
|
||||
move it to indeterminate; removing semantic evidence cannot switch directly between discharged and unguarded.
|
||||
- Integration: exact unguarded and discharged paths; a guard in a called helper; registration-site entries; base
|
||||
attachment; resource mismatch; authority insufficiency; ambiguous calls; CFG unavailable; budget cutoff.
|
||||
- Runner: baseline/candidate bundle materialisation, four-arm attribution, paired vulnerable/fixed scoring,
|
||||
reviewed-manifest validation, freeze-digest guard, zero-byte and stale-anchor refusal, indeterminate rate, and
|
||||
hard 120-second gate.
|
||||
- Fact authority: bootstrap-policy independence, canonical bundle identity, reviewer/store
|
||||
admission, forbidden identity pairs, disclosure resolution, one pack per language, and
|
||||
refusal of substituted or self-authored artifacts.
|
||||
- Fact matching: canonical observation and match persistence, complete structural matches,
|
||||
name-only/partial ambiguity, lineage/custody contamination, and read-only core resolution.
|
||||
- Compatibility: byte-stable legacy recognition projection and proof that it cannot construct
|
||||
v2 evidence, readiness, or admission; core imports remain target-neutral.
|
||||
|
||||
## Migration Strategy
|
||||
1. Add abstract types and adapters without changing legacy checker defaults.
|
||||
2. Add strict resolution, summaries and judgment behind the prover's opt-in path.
|
||||
3. Migrate traces and coverage, then enable the amended development arms.
|
||||
4. Freeze facts and analyzer before running a separate admission population.
|
||||
5. Authenticate the existing entry projection, publish reviewed language-scoped v2 packs,
|
||||
and run the authority-bearing structural adapter beside the legacy loader.
|
||||
6. Pass detection-core downstream conformance, migrate production callers, then remove the
|
||||
no-context compatibility path at the single-writer cutover.
|
||||
|
||||
Rollback is the previous prover arm and fact-set digest; baseline records remain immutable and comparable.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Requirements Document
|
||||
|
||||
## Project Description (Input)
|
||||
An access-control prover: a deterministic check that a protected operation is reachable from an attacker-facing
|
||||
entry point without a required authorization check governing that path. The prover consumes acquired entry,
|
||||
operation and enforcement facts, resolves calls and resources conservatively, emits an ordered trace for every
|
||||
examined pair, and is measured separately on development and admission populations.
|
||||
|
||||
## Introduction
|
||||
The security object for access control is not the shape of a changed function. It is an ordered path from an
|
||||
attacker-facing entry to a protected operation, together with the authorization evidence that governs every
|
||||
relevant control-flow and call-flow step. The 2026-10-28 development measurement established a useful baseline:
|
||||
15 of 23 fix changes detected, 15.0 flagged ordinary pushes per 100, and no instrument failures. The subsequent
|
||||
ablation showed that unresolved calls, over-broad resource matching, missing registration facts and silent
|
||||
fallbacks account for both missed detections and false alarms.
|
||||
|
||||
This amendment makes uncertainty part of the result rather than a side counter. A pair is `unguarded`,
|
||||
`discharged`, or `indeterminate`; degraded resolution, missing control-flow semantics, incomplete facts and
|
||||
budget cutoffs cannot create a final security verdict. It also separates label-blind fact acquisition and
|
||||
development regression from admission measurement on untouched repositories.
|
||||
|
||||
Concrete frameworks and repositories are benchmark evidence, not acceptance-criteria inventories. Target
|
||||
vocabulary is acquired as reviewed data or recogniser strategies and is verified before promotion. The abstract
|
||||
interpretation engine remains target-agnostic.
|
||||
|
||||
## Boundary Context
|
||||
- **In scope**: acquired entry, operation and enforcement knowledge; exact and conservative call resolution;
|
||||
resource- and authority-sensitive witness matching; bounded interprocedural summaries and concrete evidence
|
||||
paths; control-flow-sensitive dominance; three-way pair judgments; failure-visible coverage; development and
|
||||
admission runners.
|
||||
- **Out of scope**: taint flow; the Joern engine; model calls inside the prover; vulnerability families other than
|
||||
access control; client-side presentation gating; pre-push hook wiring; proof execution; hand-tuning vocabulary
|
||||
from evaluation labels.
|
||||
- **Adjacent expectations**: `authorization-obligations` supplies the existing checker and fact contracts;
|
||||
`valid-detection-benchmark` supplies reviewed populations; `search-with-proof` consumes unguarded traces;
|
||||
`pre-push-safety-net` may consume the arm only after admission passes.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Acquired attacker-facing entry points
|
||||
**Objective:** As a maintainer, I want entry recognition to consume verified target knowledge, so that new targets
|
||||
can be covered without adding target vocabulary to the analysis engine.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When verified target knowledge describes a declarative entry shape, the mapper shall record every matching
|
||||
repository-defined handler as an entry with its source range, trust boundary and recognition evidence.
|
||||
2. When verified target knowledge describes a call-site registration, the mapper shall resolve the registered
|
||||
repository-defined handler and record its request methods as entries with the registration site and resolved
|
||||
handler identity as evidence.
|
||||
3. If a registration target cannot be resolved to repository-defined code, the mapper shall emit no entry for it
|
||||
and shall record `registration_unresolved` coverage.
|
||||
4. When acquired knowledge proves that an entry is intentionally public, the mapper shall record access `public`;
|
||||
otherwise, the mapper shall record access `unknown` rather than infer public access.
|
||||
5. The mapper shall preserve the promoted fact or strategy identifier and the concrete source evidence on every
|
||||
entry record.
|
||||
6. If no promoted entry knowledge covers a file, the prover shall record `framework_unrecognised` and shall not
|
||||
interpret the absence of entries as a clean result.
|
||||
7. When target entry knowledge is added or updated, the system shall apply it through data or a registered
|
||||
recognition strategy without changing target-agnostic analysis modules.
|
||||
|
||||
### Requirement 2: Acquired protected operations and enforcement evidence
|
||||
**Objective:** As a maintainer, I want operations and authorization evidence represented as verified knowledge,
|
||||
so that the prover compares the correct action, resource and authority.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When promoted operation knowledge matches a call or recognised handler, the prover shall record the protected
|
||||
operation, its required authorization kinds, its resource identity and any required authority.
|
||||
2. When promoted enforcement knowledge matches a statement, decorator, route, policy decision or equivalent
|
||||
construct, the prover shall record a witness with its scope, resource identity, authority and provenance.
|
||||
3. When a handler name itself denotes a protected action, the prover shall apply that operation only to an entry
|
||||
recognised for the same promoted target knowledge.
|
||||
4. When a route or declared policy marks an entry public, the prover shall waive only a request-level path guard
|
||||
and shall preserve resource, ownership and authority obligations.
|
||||
5. When a witness binds a different resource or an insufficient authority, the prover shall not use that witness
|
||||
to discharge the operation.
|
||||
6. When a witness is request-global, the prover shall use it without resource equality only when the operation
|
||||
fact explicitly declares that witness kind sufficient at request scope.
|
||||
7. If resource identity or authority compatibility cannot be decided, the prover shall treat the witness match as
|
||||
unknown rather than matching it broadly or rejecting it as definitely unrelated.
|
||||
8. When one authority implies another, the prover shall apply that implication only from promoted target
|
||||
knowledge and shall record the implication used in the trace.
|
||||
9. When target knowledge is extracted or proposed, the acquisition workflow shall verify its source, version,
|
||||
region and match evidence before promoting it to closed data or an enabled strategy.
|
||||
10. If proposed target knowledge contradicts the inspected source or lacks verification evidence, the acquisition
|
||||
workflow shall reject it with a reason and shall not modify promoted knowledge.
|
||||
|
||||
### Requirement 3: Conservative interprocedural reachability
|
||||
**Objective:** As a maintainer, I want calls resolved to stable program identities and paths carried through
|
||||
helpers, so that unrelated same-name code neither creates nor suppresses findings.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a touched function or reachable helper contains a protected operation, the prover shall construct caller
|
||||
chains from recognised entries to that operation within the configured work budget.
|
||||
2. When a call target can be identified by module, enclosing type, method and signature, the prover shall keep
|
||||
that identity distinct from every other same-name target.
|
||||
3. When more than one call target remains feasible, the prover shall preserve the finite candidate set and shall
|
||||
mark the call incomplete rather than merge the candidates under a bare name.
|
||||
4. If no bounded candidate set can be established, the prover shall mark the call unresolved and shall not use it
|
||||
to create a final discharged or unguarded judgment.
|
||||
5. When an operation or entry is declared on a base type, the prover shall attach it only to repository-defined
|
||||
concrete descendants whose resolved identities support that attachment.
|
||||
6. When a helper calls another helper that contains an operation or governing authorization evidence, the prover
|
||||
shall continue the analysis through that call while budget remains.
|
||||
7. When recursion is present, the prover shall terminate and shall preserve the accumulated operations,
|
||||
authorization effects and incompleteness reasons for the recursive component.
|
||||
8. If depth, fan-out, time or another work bound truncates exploration, the prover shall record the exact cutoff
|
||||
reason and shall treat affected pairs as incomplete.
|
||||
9. When a complete chain is materialised, the prover shall emit its hops from entry to operation with the call
|
||||
site of every transition.
|
||||
10. When multiple complete chains reach the same operation, the prover shall evaluate each chain independently so
|
||||
that a guarded chain cannot hide an unguarded chain.
|
||||
|
||||
### Requirement 4: Access-control verdicts
|
||||
**Objective:** As a developer about to push, I want a finding only for a demonstrated unguarded path, so that every
|
||||
alarm names a concrete security obligation and repair location.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a complete path reaches a protected operation, at least one required authorization kind is proven absent,
|
||||
and every other required kind is proven either governing or absent, the prover shall judge the pair
|
||||
`unguarded` and shall report each absent kind.
|
||||
2. When every required authorization kind is proven to govern the operation on a complete path, the prover shall
|
||||
judge the pair `discharged`.
|
||||
3. If the evidence is insufficient for either final judgment, the prover shall judge the pair `indeterminate`.
|
||||
4. When a pair is `unguarded`, the prover shall emit one finding per operation and missing authorization kind with
|
||||
family `access_control`, exact source anchor, entry, hops and evidence.
|
||||
5. When a pair is `discharged` or `indeterminate`, the prover shall emit no vulnerability finding for that pair.
|
||||
6. The prover shall apply the recognised-entry-is-protected premise only in its explicit opt-in mode and shall
|
||||
identify that premise in the evidence.
|
||||
7. The prover shall make no model call and shall return findings in the existing hunt-defect contract.
|
||||
|
||||
### Requirement 5: Development measurement
|
||||
**Objective:** As a maintainer, I want repeatable development measurements, so that changes to facts and analysis
|
||||
can be attributed without being mistaken for admission evidence.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a development population is run, the runner shall write one resumable record per change with analyzer,
|
||||
fact-set, population and input digests.
|
||||
2. When development records are summarised, the runner shall report recall on fix changes and verified locations,
|
||||
flagged ordinary pushes per 100, precision, vulnerable/fixed pair correctness, instrument failures and runtime
|
||||
distributions with uncertainty intervals where defined.
|
||||
3. The runner shall report a stage funnel from entry recognition through call resolution, operation discovery,
|
||||
control-flow evaluation, witness matching and pair judgment.
|
||||
4. The runner shall report per-target-family and per-repository coverage without disclosing reserved repository
|
||||
identities in shared records.
|
||||
5. When comparing an amended analyzer and amended facts, the runner shall support the four registered arms formed
|
||||
by old/new analyzer crossed with old/new fact set.
|
||||
6. If any record reads zero bytes for a file present in the analysed tree or anchors a finding outside files read,
|
||||
the runner shall mark an instrument failure and shall refuse the summary.
|
||||
7. The runner shall label results from populations used to choose facts or analyzer behavior as development or
|
||||
regression evidence and shall not present them as admission evidence.
|
||||
|
||||
### Requirement 6: Pre-push budget
|
||||
**Objective:** As a developer, I want the prover to finish inside the pre-push window, so that eligible detection
|
||||
can run on every push.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a measured change exceeds 120 seconds, the runner shall fail the budget gate and shall identify the
|
||||
change, stage and elapsed time.
|
||||
2. The prover shall reuse per-change file, text, syntax, call, entry, symbol and summary caches and shall not read
|
||||
the product tree more than once per change.
|
||||
3. If analysis stops because of a deadline, the prover shall return incomplete coverage and indeterminate affected
|
||||
pairs rather than a partial clean verdict.
|
||||
|
||||
### Requirement 7: Failure-visible coverage
|
||||
**Objective:** As a developer, I want the tool to state what it could not decide, so that silence never reads as a
|
||||
clean bill of health.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The prover shall report files by language, recognised entry families, entries, reached and unreached targets,
|
||||
operations, discharged pairs, unguarded pairs and indeterminate pairs.
|
||||
2. The prover shall count unchecked or incomplete work by specific reason, including parse failure, unsupported
|
||||
language, unrecognised target, absent facts, unresolved registration, ambiguous or unresolved call, exploration
|
||||
cutoff, unavailable control flow, unknown resource, unknown authority and analysis exception.
|
||||
3. If no target family is recognised for a repository, the runner shall mark the repository `uncovered` and shall
|
||||
report its recall separately from covered recall.
|
||||
4. When a recognition, resolution, summary or judgment pass raises an exception, the prover shall record the
|
||||
exception class at that stage and shall not fold it into a quiet coverage reason.
|
||||
5. When the same pair has multiple incompleteness reasons, the prover shall preserve every reason in the trace and
|
||||
aggregate each reason in coverage.
|
||||
6. The runner shall report the indeterminate rate separately and shall not include indeterminate pairs in either
|
||||
clean or discharged counts.
|
||||
|
||||
### Requirement 8: Proof-arm feed
|
||||
**Objective:** As the proof arm, I want each unguarded finding to carry a complete analysis witness, so that a
|
||||
demonstration can be attempted without re-deriving the decision.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a finding is emitted, it shall carry the entry record, exact hop list, operation, missing authorization
|
||||
kind, resource and authority relations, pair judgment, trace identifier and completeness evidence.
|
||||
2. The finding identifier shall remain stable for the same operation, source anchor and missing authorization
|
||||
kind across deterministic replays.
|
||||
3. If the pair is indeterminate, the prover shall expose its trace only as diagnostic coverage and shall not send
|
||||
it to the proof arm as a vulnerability finding.
|
||||
|
||||
### Requirement 9: Ordered traces for every examined pair
|
||||
**Objective:** As a maintainer, I want every examined pair represented by the same deterministic trace, so that
|
||||
decisions and degradations can be inspected and compared.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When the prover examines an entry-operation path, it shall emit one trace containing the entry, ordered hops,
|
||||
operation, authorization witnesses, resource and authority relations, control-flow relations, completeness
|
||||
state and pair judgment.
|
||||
2. The prover shall emit traces for unguarded, discharged and indeterminate pairs alike.
|
||||
3. When an argument or resource is forwarded through a call, the trace shall preserve its origin through the
|
||||
resolved hop; if origin cannot be followed, the trace shall record `unknown`.
|
||||
4. The trace shall carry target knowledge identifiers and source evidence needed to reproduce the judgment but
|
||||
shall omit repository identity, file-size and repository-size features from decision input.
|
||||
5. The trace shall serialise to one deterministic canonical text form and one deterministic canonical JSON form.
|
||||
6. When a work bound or fallback would have been required, the trace shall record the cutoff or unavailable
|
||||
semantic fact rather than silently substituting a weaker analysis.
|
||||
7. The runner shall persist traces and their analyzer, fact-set and input digests with the corresponding record.
|
||||
|
||||
### Requirement 10: Control-flow-sensitive dominance
|
||||
**Objective:** As a maintainer, I want authorization evidence to count only when control flow guarantees it governs
|
||||
the operation, so that avoidable or ignored checks do not discharge obligations.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When authorization evidence appears before an operation, the prover shall determine whether every feasible
|
||||
path to the operation passes that evidence.
|
||||
2. When a guard's failing branch terminates or the operation exists only in the passing branch, the prover shall
|
||||
record the guard effect and may treat it as governing the operation when resource and authority also match.
|
||||
3. When a predicate result is ignored or appears only on some paths, the prover shall not treat it as a governing
|
||||
authorization check.
|
||||
4. When an operation is inside a loop, exception region or conditional branch, the trace shall record that control
|
||||
context and any exception handling that can bypass the guard.
|
||||
5. When route- or declaration-scoped authorization evidence covers the whole handler, the prover shall preserve
|
||||
that declared scope without inventing a statement location.
|
||||
6. If control-flow semantics or source-to-control-flow correspondence is unavailable, the prover shall mark the
|
||||
affected relation unknown and the pair indeterminate rather than decide it by textual order.
|
||||
7. The existing order-based checker mode shall remain available to callers that do not opt into prover semantics.
|
||||
|
||||
### Requirement 11: Three-way abstract judgment
|
||||
**Objective:** As a maintainer, I want uncertainty to propagate monotonically through the analysis, so that losing
|
||||
precision cannot manufacture a vulnerability or a discharge.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The prover shall represent call resolution, path completeness, resource compatibility, authority compatibility
|
||||
and dominance with values that distinguish definite, possible, absent and unknown evidence.
|
||||
2. When all required evidence on a complete path is definite and at least one required obligation has no governing
|
||||
witness, the prover shall produce `unguarded` and shall report every such obligation, including when other
|
||||
required obligations are definitely governed.
|
||||
3. When every required obligation has a definite governing witness on a complete path, the prover shall produce
|
||||
`discharged` for that path.
|
||||
4. If any required relation is possible or unknown, or path enumeration is incomplete, the prover shall produce
|
||||
`indeterminate` unless another complete path already proves `unguarded`.
|
||||
5. When multiple paths reach one operation, the prover shall aggregate them as `unguarded` if any complete path
|
||||
is unguarded, `discharged` only if enumeration is complete and every path is discharged, and `indeterminate`
|
||||
otherwise.
|
||||
6. When analysis precision is reduced by adding an unresolved candidate or removing semantic information, a final
|
||||
judgment shall remain unchanged or move to `indeterminate`; it shall not switch directly between `unguarded`
|
||||
and `discharged`.
|
||||
7. The prover shall keep abstract-domain operations deterministic and independent of target names.
|
||||
8. The runner shall expose judgment counts and transitions between compared arms so that precision loss cannot be
|
||||
hidden inside aggregate recall or false-alarm metrics.
|
||||
|
||||
### Requirement 12: Label-isolated admission workflow
|
||||
**Objective:** As a maintainer, I want admission measured on untouched, reviewed evidence after the analyzer and
|
||||
facts are frozen, so that a better development score is not mistaken for generalisation.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When an admission run is prepared, the workflow shall freeze the analyzer revision, promoted fact-set digest,
|
||||
acquisition record, configuration and population manifest before opening admission labels.
|
||||
2. While acquiring target knowledge for an admission repository, the workflow shall use repository source and
|
||||
approved documentation without vulnerability, fix-location or ordinary-change labels.
|
||||
3. If acquisition observes evaluation labels or is changed in response to an admission result, the workflow shall
|
||||
mark that population spent and shall not use it for admission.
|
||||
4. When ordinary changes are prepared for a false-alarm denominator, the benchmark workflow shall review and
|
||||
remove or relabel changes that contain real missing-authorization defects before scoring.
|
||||
5. When a vulnerable case has a fixed twin, the admission run shall require the expected unguarded trace on the
|
||||
vulnerable side and quiet or discharged behavior at the corresponding fixed operation.
|
||||
6. When an amended prover is considered for the G1 arm, the workflow shall measure it on untouched repositories
|
||||
that were not used to choose facts, recognition strategies, abstract-domain behavior or thresholds.
|
||||
7. The admission report shall include recall, verified-location recall, flagged ordinary pushes per 100,
|
||||
precision, paired vulnerable/fixed correctness, indeterminate rate, coverage, runtime and instrument failures
|
||||
with sample counts and uncertainty intervals.
|
||||
8. If the budget gate, instrument gate or pre-registered quality criteria fail, the workflow shall return NO-GO
|
||||
and shall not enable the arm in pre-push admission.
|
||||
9. The workflow shall retain the current 23-repository population as development regression evidence and shall
|
||||
require a separate untouched population for admission claims.
|
||||
|
||||
### Requirement 13: Reviewed detection-core fact authority and migration
|
||||
**Objective:** As a maintainer, I want access-control target knowledge translated into independently reviewed,
|
||||
persisted detection-core evidence, so that entry recognition can support later vulnerability-class specs without
|
||||
making the neutral core target-aware or allowing a pattern match to authorize itself.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The access-control adapter shall define a versioned production family contract whose security object, roles,
|
||||
decisions, residual questions and incompleteness reasons exactly represent access-control operation,
|
||||
constraint and entry-registration semantics.
|
||||
2. For each promoted target-knowledge set, the adapter shall provide reviewed language-scoped fact packs and an
|
||||
independently authenticated authority bundle naming the contract, pack references, review and custody
|
||||
policies, semantic-fact and matcher lineages, matcher identity and every immutable evidence reference.
|
||||
3. The host bootstrap policy shall be external to the bundle it authenticates, shall constrain admitted reviewer
|
||||
and store identities, shall enforce producer-reviewer separation and forbidden identity pairs, and shall
|
||||
refuse unresolved conflict disclosures or a non-canonical bundle digest.
|
||||
4. When legacy recognition knowledge is migrated, the adapter shall preserve its recognition behavior as a
|
||||
canonical compatibility projection while mapping each promoted row to explicit contract roles, typed
|
||||
structural requirements and declared relation effects. The projection alone shall remain non-authoritative.
|
||||
5. For every structural match, the adapter shall persist canonical bytes for each `StructuralObservation` and
|
||||
the resulting `FactMatchEvidence` before invoking detection-core, then supply the immutable content references
|
||||
through a read-only resolver. Detection-core shall not write or synthesize those artifacts.
|
||||
6. The adapter shall bind fact and matcher lineage, custody and evaluation-population identity so that label
|
||||
access for the same admission population makes the match incomplete or refused; result labels shall not
|
||||
influence recognition or matching for that admission run.
|
||||
7. The adapter shall pass the detection-core downstream conformance gate for complete evidence and shall remain
|
||||
incomplete or refused for name-only, partial, substituted, self-authored, legacy-only or contaminated inputs,
|
||||
without detection-core importing access-control or target modules.
|
||||
8. Before the compatibility path is removed, the production entry-mapping caller shall load the externally
|
||||
bootstrapped authority-bearing adapter, and a repository census shall report zero callers that use the legacy
|
||||
projection as v2 evidence or bypass the reviewed authority context.
|
||||
@@ -0,0 +1,350 @@
|
||||
# Research & Design Decisions
|
||||
|
||||
## Summary
|
||||
- **Feature**: `access-control-prover`
|
||||
- **Discovery Scope**: Extension of `authorization-obligations` (checker, facts, dominance) and the hunter's
|
||||
per-change context; gap analysis against the code on 2026-10-14 (commit 9feb8f1).
|
||||
- **Key Findings**:
|
||||
- The checker already takes `paths` and `entries` and already decides reached-ness and along-path dominance
|
||||
(`semantic/obligations/check.py::_reached`, `dominance.py::_dominates_along`); nothing supplies paths today,
|
||||
so every operation outside an entry handler is invisible to it.
|
||||
- The entry-point mapper (`mapping.py`) recognises Flask, Django, Express, Laravel routes, WordPress hooks,
|
||||
Next and Remix route modules partially, and nothing for Java: no Stapler `doXxx` web method, no Servlet or
|
||||
Jetty handler, no JAX-RS resource. Seventeen of the pool's 23 fixes are Stapler web methods.
|
||||
- The hunter's `Context` (hunt/producers.py) caches product files, texts, file IR, a call index and the
|
||||
repository's entry points per change; `source.callers_of` gives one hop of callers from the index. A bounded
|
||||
upward walk from a function to an entry point is a loop over that, no new index.
|
||||
- Enforcement outside the function fits the existing `Discharge` scopes `router` and `decorator`, which
|
||||
`OrderDominance.dominates` accepts without a line, through the checker's `extra_witnesses` parameter. No
|
||||
schema change to the facts loader is needed for them; the facts files gain operation rows only.
|
||||
- `benchmarks/valid/metrics.py` scores replay-shaped records with `benchmarks.unseen.score`; the hunter arm
|
||||
(`benchmarks/valid/hunter_arm.py`) writes that shape. A prover arm writing the same shape is scored by the
|
||||
same code, so its recall and false alarms per 100 stand beside the hunter arms.
|
||||
- The pool's fixes by shape: 17 Jenkins web methods (`doTestConnection`, `doFill...Items`, `doCheck...`,
|
||||
`doValidate...`, `doChangeSubmit`, `doNotifyCommit`, `doPython`, `persistValue`, `create`) missing
|
||||
`checkPermission`; one Jetty service without authentication (CWE-306); one Tornado terminal websocket open
|
||||
without authentication (CWE-306); one Laravel Livewire `store` without authorization (CWE-285); one Symfony
|
||||
controller action (CWE-284); one Remix action deactivating a schedule by id without an ownership check
|
||||
(CWE-639); one Flarum JavaScript extension (CWE-863, out of reach for this prover: client-side permission
|
||||
gating, counted as uncovered).
|
||||
|
||||
## Research Log
|
||||
|
||||
### Which checker parameters exist and what they expect
|
||||
- **Context**: avoid a second checker.
|
||||
- **Sources Consulted**: `src/openultrasast/semantic/obligations/check.py` (`check_obligations`, `_reached`,
|
||||
`_relevant`), `dominance.py` (`OrderDominance`, `_dominates_along`, `_hop_index`), the approved design of
|
||||
`authorization-obligations` (Dominance, Checker, EntryPoints sections).
|
||||
- **Findings**: `paths` is a sequence of objects with a `hops` attribute; each hop is `"path::function"` or a
|
||||
`(node, line)` pair; a path reaches an operation when its last hop equals `f"{op.path}::{op.function}"`. With
|
||||
no paths, only operations inside entry handlers (`entries`) are reached. A witness of scope `hop` on an
|
||||
earlier hop dominates along the path. Witness scopes `decorator` and `router` dominate without a line.
|
||||
- **Implications**: the prover produces `PathRecord(hops=(...))` objects ending at the operation's function and
|
||||
passes them as `paths`; it produces router and decorator witnesses as `extra_witnesses`; it needs no change to
|
||||
the checker's logic. One gap: the checker builds `operations` from the IRs it is given, so the prover passes the
|
||||
IR of every file on a path, not only the touched file.
|
||||
|
||||
### Where entry points come from
|
||||
- **Sources Consulted**: `src/openultrasast/mapping.py` (`analyze_entry_points`, `_entry`, the per-framework
|
||||
recognisers around lines 260-930), `EntryPointRecord` fields.
|
||||
- **Findings**: recognisers are code, one block per framework, each calling `_entry(target, start, end, handler,
|
||||
name, kind, access, trust_boundary, evidence, conditions, ...)`. No Java recogniser exists. `frameworks.toml` is
|
||||
a lexicon for priors and stratification, not for entry recognition.
|
||||
- **Implications**: add recognisers in `mapping.py` following the existing block pattern (Stapler, Jetty,
|
||||
Symfony attributes, Livewire, Tornado, Remix exports where missing). Each names its evidence.
|
||||
|
||||
### How a rule arm is scored
|
||||
- **Sources Consulted**: `benchmarks/valid/metrics.py` (`summarize`, `scoring_records`), `benchmarks/unseen/score.py`
|
||||
(`catches`, `location_match`, `false_alarm`, `coverage`), `benchmarks/valid/hunter_arm.py` (`run`, record
|
||||
writing), `src/openultrasast/hunt/metric.py::search_outcome`.
|
||||
- **Findings**: a record needs `id`, `kind`, `repo`, `family`, the change's `path`/`function_lines` (copied from
|
||||
the pool), `findings` with `path`, `line`, `family`, and `coverage` fields the scorer reads; a push is caught
|
||||
when a finding of the change's family lands within the tolerance of a verified location.
|
||||
- **Implications**: the prover arm copies the hunter arm's record envelope and instrument fields (bytes read per
|
||||
changed file, head verified), writes per-change files, and calls `benchmarks.valid.metrics.summarize`.
|
||||
|
||||
### Budget
|
||||
- **Findings**: the second screen's feature table took 53 minutes for 483 changes with every producer, 7 seconds
|
||||
per change on average and 117 seconds for the slowest seatunnel change; the caller index is the dominant cost
|
||||
and is already cached per change. A depth-3 upward walk adds a few index lookups per function.
|
||||
- **Implications**: the 120-second bound of Requirement 6 is realistic; the runner measures it per change.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
| Option | Description | Strengths | Risks / Limitations | Notes |
|
||||
|---|---|---|---|---|
|
||||
| Extend the checker's inputs (chosen) | Supply paths, entries and witnesses to `check_obligations` from the hunter's context | One checker, one dominance rule, facts stay closed data | Order dominance is textual inside a function; nesting-aware dominance (`guard-dominance-regime`) never existed | Honest for "check present before operation"; the Jenkins shape |
|
||||
| New prover module with its own graph | Build a call graph and CFG per change | Precise dominance | Weeks of work, duplicates the checker, no CFG for Java or PHP in the tree | Rejected for this increment |
|
||||
| Joern engine | Dataflow and call graph from the engine | Precise | 225 s per batch, pilot produced no findings, no PHP in the budget | Rejected as the gate; usable later as a proof backend |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Decision: paths from the caller index, not a call graph
|
||||
- **Context**: Requirement 3.
|
||||
- **Alternatives Considered**: a per-change call graph over all product files; the engine's call graph.
|
||||
- **Selected Approach**: an upward walk from the function through `source.callers_of` on the cached index,
|
||||
bounded by depth and fan-out, stopping at the first entry point of each chain.
|
||||
- **Rationale**: the index exists, is cached per change, and the pool's shapes are shallow (the operation is in the
|
||||
web method or one helper below it).
|
||||
- **Trade-offs**: name-based caller resolution conflates same-named functions across files; chains are kept per
|
||||
file but a caller of `save` in another class may be a false hop. Accepted and visible in the evidence.
|
||||
- **Follow-up**: measure how many positives are reached at depth 1, 2 and 3 on the pool.
|
||||
|
||||
### Decision: framework enforcement as router and decorator witnesses
|
||||
- **Context**: Requirement 2.
|
||||
- **Selected Approach**: a small recogniser per framework reads route registrations and attributes and emits
|
||||
`Discharge` witnesses with scope `router` or `decorator` for the handlers they cover; passed through
|
||||
`extra_witnesses`.
|
||||
- **Rationale**: the dominance rule already accepts those scopes; the facts files stay about calls.
|
||||
- **Trade-offs**: coverage is as wide as the recognisers; an unrecognised middleware name means a finding on a
|
||||
guarded route. Every recogniser lists its names in the facts header and the coverage statement counts routes
|
||||
with unrecognised middleware.
|
||||
|
||||
### Decision: an opt-in premise in the checker, not a synthesized policy
|
||||
- **Context**: Codex's first attempt stopped on a real conflict: with path records and neither a declared clause
|
||||
nor a sibling norm the checker returns no finding by design (gating 3), so the prover's core verdict could not
|
||||
be emitted without a change.
|
||||
- **Alternatives Considered**: synthesize a `DeclaredPolicy` in memory marking every recognised entry
|
||||
`authenticated` (dishonest: the finding would read "route declared authenticated" when nothing was declared);
|
||||
a second checker in the prover (duplicates the rules); an opt-in keyword and a distinct label (chosen).
|
||||
- **Selected Approach**: `assume_entries_protected=False` keyword, label `unguarded_path`, evidence naming the
|
||||
premise. Default behaviour and tests unchanged.
|
||||
- **Trade-offs**: the label makes the premise visible in every finding and in the static tags; consumers that
|
||||
grouped by label need the new value (revalidation trigger for `authorization-obligations`).
|
||||
|
||||
### Decision: no model call inside the prover
|
||||
- **Context**: Requirement 4.4; the maintainer's direction after the engine pilot: the model's job is the trust
|
||||
boundary question the static side cannot answer, asked after a static result exists.
|
||||
- **Selected Approach**: the prover is deterministic; the intent adjudication hook of the checker stays off in
|
||||
the arm; a later increment may wire it as a separate, measured arm.
|
||||
|
||||
## Risks & Mitigations
|
||||
- Jenkins web methods whose protected operation is an outbound connection test with stored secrets
|
||||
(`doTestConnection`) call no listed operation name — mitigate by adding operation rows for credential
|
||||
lookups and HTTP client calls used in those handlers, checked in the clones, and by counting the handlers the
|
||||
facts do not cover.
|
||||
- Textual order dominance inside a function misses a check in an `else` branch — accepted for this increment;
|
||||
recorded as a degradation when a witness exists but does not dominate.
|
||||
- Same-name conflation in callers — chains kept per file; evidence names the hop files.
|
||||
- Over-wide entry recognition (every `doXxx`) raises false alarms on helper methods — the Stapler recogniser
|
||||
requires a public method on an exposed type or a Stapler-typed parameter; the pool's ordinary pushes measure
|
||||
the rate.
|
||||
|
||||
## References
|
||||
- Jenkins AccessControlled: https://javadoc.jenkins.io/hudson/security/AccessControlled.html
|
||||
- Stapler web methods: https://www.jenkins.io/doc/developer/security/form-validation/ and
|
||||
https://www.jenkins.io/doc/developer/security/secrets-and-credentials/
|
||||
- Laravel authorization and route middleware: https://laravel.com/docs/authorization, https://laravel.com/docs/middleware
|
||||
- Symfony security: https://symfony.com/doc/current/security.html
|
||||
- Remix route module API: https://remix.run/docs/en/main/route/action
|
||||
- Tornado authentication: https://www.tornadoweb.org/en/stable/guide/security.html
|
||||
|
||||
---
|
||||
|
||||
## Design amendment synthesis -- 2026-10-29
|
||||
|
||||
Context: gates 1-3 (measurements `2026-10-28-access-control-{prover,judge,ablation}`) found one cross-cutting
|
||||
lever -- interprocedural resolution -- behind both the 8 recall misses (B: operations never linked; C: coarse
|
||||
guards discharge) and the kept false alarms (guards present but unmodeled, name-collision resolution, base-class
|
||||
projection). Requirements amendment `[2026-10-29]` (AC 1.9, 2.9, 2.10, 3.7-3.10, 5.6, 7.3); this section
|
||||
records the code survey behind the design amendment (same date tag).
|
||||
|
||||
### Research Log: resolution seams (verified against current code)
|
||||
|
||||
- Caller index keys on bare function name only: `src/openultrasast/source.py:447` `callers_of` (regex over
|
||||
`CodeLine` indexes); `CallSite(number, name)` at `source.py:22` carries no receiver. The prover adapter
|
||||
`hunt/prover.py:161-172` re-qualifies hits to `file::enclosing_fn` by IR span containment.
|
||||
- Tree-sitter IR drops receiver text: `semantic/ir.py:21-29` `CallSite` (callee is trailing name only;
|
||||
`semantic/cst.py:220` `_callee_name` strips `$obj->method` / `x.y.z`); receiver must be captured in
|
||||
`cst.py:203` `_call` and added to `ir.CallSite` (python-ast path `ir.py:100+` likewise).
|
||||
- Target enumeration is one-hop trailing-name join: `hunt/prover.py:174-185` (`call_names` from touched
|
||||
functions' IR calls; any `(path,name)` with that name and operations). Second receiver-blind seam.
|
||||
- Guards on hops: `Hop.guards_before_call` populated only at `semantic/obligations/trace.py:224-231`
|
||||
(serialization); the verdict path uses `semantic/obligations/check.py:312-323` `_on_caller_hop`, which today
|
||||
re-judges only `path_guard`/`ownership_check` (router/decorator => True; other kinds => False). Dominance
|
||||
order rule `dominance.py:52-60`; CFG deferral `cfg_dominance.py:63-65`.
|
||||
- Discharge has no resource identity: `semantic/obligations/operations.py:35-44` (`detail` is identifier-only).
|
||||
The only resource token today is `Operation.resource` (`operations.py:31`, `_resource_token` at 229-243:
|
||||
receiver first segment, SQL table, or `resource_arg`). Suppression loop `check.py:117-122`; relevance
|
||||
`check.py:326-327`; scope matching is `(path, function)` equality (`operations.py:124`, `dominance.py:38`).
|
||||
- Enforcement witnesses: `semantic/obligations/enforcement.py:18` covers Tornado decorators, Symfony
|
||||
`IsGranted`/`Security`/`access_control`, Laravel middleware + `authorizeResource`; called per language at
|
||||
`hunt/prover.py:223-227`. In-function recognizer `_call_witnesses` (`operations.py:163-183`) matches
|
||||
discharger rows by trailing name -- `denyAccessUnlessGranted`/`isGranted` already match existing rows
|
||||
(`ruleset/obligations/php.toml:68-78`); `voteOnAttribute` needs no row (interface method, trailing-name
|
||||
match). Jenkins guard rows exist (`java.toml:68-76`).
|
||||
- Mapping: `_jetty_entries` (`mapping.py:562`) anchors at method declarations only; call-site registration
|
||||
precedent exists (`_python_registered_routes` `mapping.py:945-1015`, WordPress `_PHP_REGISTRATION`
|
||||
`mapping.py:657-680` with `registered_by` field on `EntryPointRecord`, `mapping.py:53-74`); java dispatch at
|
||||
`mapping.py:185-198`; framework tag table `entry_framework` `mapping.py:472-488`.
|
||||
- prover_arm refusal seams: per-file zero-byte loop `benchmarks/valid/prover_arm.py:86-113`; stale-anchor
|
||||
validation belongs in `_prove` between lines 114 and 125 (before `record.update(findings=...)`), mirrored in
|
||||
`summarize()` (184-187); type-name-only catch at `_prove` 129-133.
|
||||
- Checker boundary: `authorization-obligations` owns check.py/facts.py/operations.py/paths.py/dominance.py.
|
||||
The design's third extension (resource field + `resource_bound` keyword + all-kind `_on_caller_hop`) touches
|
||||
`operations.py` (dataclass field) and `check.py` (keyword + relevance compare), default-off. Other
|
||||
`check_obligations` callers (cli.py, hunt/producers.py, pairs.py) keep unchanged defaults.
|
||||
|
||||
### Decisions
|
||||
|
||||
1. New module `semantic/obligations/resolution.py` + additive `CallSite.receiver` (captured in `cst.py`)
|
||||
rather than widening the source.py index: the index is shared with hunt search; a per-change class table
|
||||
keeps resolution local, cacheable on the Context, and testable in isolation. Bare-name fallback +
|
||||
`receiver_unresolved` coverage counter keeps dynamic languages (TS/PHP dynamic receivers) working as today.
|
||||
2. Resource binding as checker keyword (`resource_bound`) with emission-side tokens rather than prover-side
|
||||
filtering: the suppression decision is per (witness, operation) pair inside `check_obligations`; filtering
|
||||
outside would need a restructured call. Default-off keeps `authorization-obligations` tests untouched.
|
||||
3. Programmatic registration follows the existing call-site registration pattern (python routes, WordPress)
|
||||
with class-literal resolution through `resolution.class_table` -- one owner for class resolution, reused
|
||||
by the mapping recogniser and the callers adapter.
|
||||
|
||||
### Risks
|
||||
|
||||
- Dynamic receivers (TS `this.handler.get`, PHP `$container->get(...)`) stay name-keyed; the counter makes the
|
||||
fallback visible instead of silent. If the pool shows large `receiver_unresolved` rates, the fact-proposal
|
||||
lane (deferred injection spec) is the designated place to grow type facts, not this spec.
|
||||
- `down_depth` widens target enumeration; the fan-out bound and the 120 s budget cap it. Per-repository
|
||||
seconds-per-change in the runner summary will show any cost.
|
||||
|
||||
---
|
||||
|
||||
## Abstract judgment and admission amendment -- 2026-10-29
|
||||
|
||||
### Context
|
||||
|
||||
The design review after the capability-level lift found that the proposed amendment still allowed two degraded
|
||||
instruments to participate in binary decisions: unresolved receivers fell back to a bare-name call key, and
|
||||
unavailable CFGs fell back to source order. It also found that the fact-initialization task used known development
|
||||
misses and then remeasured the same population, which is valid regression work but cannot qualify an admission arm.
|
||||
|
||||
### Synthesis
|
||||
|
||||
1. **Generalization**: call certainty, path completeness, resource compatibility, authority compatibility and
|
||||
dominance are instances of one evidence-relation problem. They share a finite domain
|
||||
(`MUST | MAY | NO | UNKNOWN`) and one incompleteness record instead of separate booleans and counters.
|
||||
2. **Build vs adopt**: the repository already owns the semantic IR, structural CFG, obligation checker and path
|
||||
records. A small abstract-domain layer and monotone function summaries extend those contracts; adopting another
|
||||
graph or abstract-interpretation engine would duplicate the current architecture and violate steering.
|
||||
3. **Simplification**: the prover keeps one checker and one concrete trace. Function summaries accelerate and
|
||||
stabilize interprocedural discovery, but final evidence remains a materialised path. No second verdict engine is
|
||||
introduced.
|
||||
|
||||
### Decisions
|
||||
|
||||
#### Decision: three-way judgment
|
||||
- **Selected approach**: every path and aggregate pair is `unguarded`, `discharged`, or `indeterminate`.
|
||||
- **Invariant**: less precise evidence may preserve a verdict or move it to `indeterminate`; it may not switch
|
||||
directly between the two final verdicts.
|
||||
- **Algebra**: `MUST` and `NO` are incomparable, both below `MAY`, which is below `UNKNOWN`. Alternative
|
||||
evidence uses the least-upper-bound table; dimensions of one witness use conjunction; witnesses for one
|
||||
obligation use existential aggregation. These operations are distinct so `MUST + NO` on alternative paths
|
||||
becomes `MAY`, while one definite governing witness remains sufficient for the obligation it covers.
|
||||
- **Consequence**: prover mode does not use textual order as a semantic fallback. Legacy checker callers retain it.
|
||||
|
||||
#### Decision: stable symbols and finite candidate sets
|
||||
- **Selected approach**: identify repository declarations by module, enclosing type, method and available signature.
|
||||
Partial resolution returns a finite candidate set plus incompleteness; unbounded resolution returns unresolved.
|
||||
- **Rejected approach**: global bare-name fallback, because it conflates unrelated methods and can both invent paths
|
||||
and borrow unrelated guards.
|
||||
|
||||
#### Decision: resource and authority are separate abstract values
|
||||
- **Selected approach**: match witness kind, scope, resource and authority independently, joining each relation into
|
||||
the pair judgment. Request-global guards are valid only when the operation fact explicitly allows that scope.
|
||||
- **Rejected approach**: `None` as a resource wildcard. It cannot distinguish intentionally global authentication
|
||||
from missing resource evidence.
|
||||
|
||||
#### Decision: summaries discover; concrete paths prove
|
||||
- **Selected approach**: monotone summaries over strongly connected call components collect operations, witnesses,
|
||||
call target sets and incompleteness. Concrete paths are materialised from exact edges for trace evidence.
|
||||
- **Trade-off**: summary and path code add one engine boundary, but they remove recursion and depth handling from
|
||||
ad-hoc traversal and make every cutoff explicit.
|
||||
|
||||
#### Decision: development attribution before admission
|
||||
- **Selected approach**: compare old/new analyzer crossed with old/new facts on the current development population,
|
||||
then freeze analyzer, facts, acquisition record, configuration and a separate untouched population for admission.
|
||||
- **Consequence**: the 2026-10-28 numbers remain the immutable baseline. A better rerun on the same repositories is
|
||||
regression evidence, not a G1 admission claim.
|
||||
|
||||
### Risks and mitigations
|
||||
- **Indeterminate inflation**: strict uncertainty may initially lower apparent coverage. Report the stage funnel and
|
||||
reason distribution; improve the dominant reason rather than weakening the lattice.
|
||||
- **Summary cost**: a repository-wide symbol graph may threaten the pre-push budget. Reuse Context caches, use finite
|
||||
summary sets, and fail the budget gate when any measured change exceeds 120 seconds.
|
||||
- **Authority vocabulary growth**: permission implication can become target-specific code. Keep implications in
|
||||
promoted facts and include the fact identifier in every match.
|
||||
- **Admission scarcity**: the current pool is already development data. Source and review a separate population
|
||||
through `valid-detection-benchmark`; do not relabel the current pool as untouched.
|
||||
|
||||
## Admission outcome (2026-10-30)
|
||||
|
||||
The first admission run on the untouched pilot population returned **NO-GO** (0/12 pairs,
|
||||
in-family 0/3; 9/12 repositories with zero framework recognition; 0 instrument failures;
|
||||
determinism 4/4). See `benchmarks/measurements/2026-10-30-access-control-admission/`. The
|
||||
next lever is entry-recognition and call-resolution breadth for the valid pool's
|
||||
ecosystems, gated by the same admission workflow. The pilot pool remains unspent: no
|
||||
acquisition observed its labels; only admission scoring read them. Standing rules:
|
||||
`steering/detection-quality.md`.
|
||||
|
||||
---
|
||||
|
||||
## Detection-core fact-authority ownership amendment (2026-11-01)
|
||||
|
||||
### Context
|
||||
|
||||
Detection-core task 9.4 reached a real boundary gap while migrating the installed entry
|
||||
pack. The generic core already validates fact packs, structural matches, lineages, custody,
|
||||
and relation application, but no owner supplied the production access-control contract,
|
||||
reviewed target authority, structural matcher, or persisted match artifacts. Putting those
|
||||
objects into core would make a vulnerability-neutral package own access-control and target
|
||||
vocabulary.
|
||||
|
||||
The instrument was verified before diagnosis: the installed entry file was readable at 963
|
||||
bytes, and the focused legacy/v2 fact suites passed. The blocker was therefore ownership and
|
||||
missing authority, not an unread input or broken validator.
|
||||
|
||||
### Synthesis and decision
|
||||
|
||||
1. **Dependency direction:** `access-control-prover` imports detection-core contracts and
|
||||
supplies family-owned objects. Detection-core never imports access-control or target code.
|
||||
2. **Authority:** a checked-in immutable bundle references the production family contract,
|
||||
one reviewed fact pack per language, recognition projection, policies, lineages, matcher
|
||||
identity, and canonical evidence. A separately configured bootstrap policy authenticates
|
||||
the bundle's reviewer and store; the bundle cannot trust itself.
|
||||
3. **Evidence custody:** the downstream adapter persists canonical structural observations
|
||||
and match bytes before calling core. Core receives only typed objects, immutable content
|
||||
references, and read-only custody/resolver ports.
|
||||
4. **Compatibility:** existing regex rows remain a byte-stable recognition projection during
|
||||
migration. They are explicitly non-authoritative; exact typed observations are required
|
||||
for complete fact evidence.
|
||||
5. **Task routing:** access-control-prover tasks 14.1–14.8 define the family contract,
|
||||
authenticate authority, implement installed codecs/resolution, record the approved review
|
||||
ceremony, migrate reviewed data, persist evidence, match structure, and publish the
|
||||
conformance handoff. Detection-core task 9.4 then runs the target-neutral handoff-acceptance gate. Access-control-prover task 14.9 subsequently migrates the
|
||||
production caller and proves zero bypasses before detection-core task 10.2 cuts over.
|
||||
|
||||
### Risks and mitigations
|
||||
|
||||
- **Self-authorized trust:** keep bootstrap policy outside the bundle, re-derive every digest,
|
||||
require producer-reviewer separation, and enforce forbidden identity pairs.
|
||||
- **Pattern authority inflation:** preserve recognition behavior separately and require the
|
||||
complete typed predicate set before core application.
|
||||
- **Circular match references:** persist the completed canonical match first, then pass its
|
||||
reference to core; the reference is never part of match-digest construction.
|
||||
- **Admission contamination:** bind fact and matcher lineages to current custody and the
|
||||
evaluation population so label access refuses the same run.
|
||||
|
||||
### Approved repo-local review ceremony (2026-11-01)
|
||||
|
||||
- The maintainer approved a repo-local ceremony after Task 14.3 correctly refused to invent
|
||||
its own independent reviewer. Generator, maintainer reviewer, and custodian are separate
|
||||
canonical identity manifests; the maintainer-reviewer manifest records the explicit
|
||||
session approval as its basis.
|
||||
- The bootstrap file admits the maintainer reviewer and installed content-addressed store;
|
||||
the authenticated authority file cannot modify either admission list.
|
||||
- Closed canonical decoders, fixed pack filenames, embedded policies/lineages, and a
|
||||
digest-addressed artifact directory make installed bytes reproducible. Storage receipts
|
||||
are derived from verified bytes and bootstrap-admitted store identity rather than stored
|
||||
as independently mutable claims.
|
||||
- This resolves the external identity decision but adds two prerequisite implementation
|
||||
units: wire decoders/resolver first, then the closed identity/bootstrap manifests, before
|
||||
the Express data migration.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"feature_name": "access-control-prover",
|
||||
"created_at": "2026-10-14T13:48:18Z",
|
||||
"updated_at": "2026-11-01T15:44:59Z",
|
||||
"language": "en",
|
||||
"phase": "tasks-generated",
|
||||
"approvals": {
|
||||
"requirements": {"generated": true, "approved": true},
|
||||
"design": {"generated": true, "approved": true},
|
||||
"tasks": {"generated": true, "approved": true}
|
||||
},
|
||||
"ready_for_implementation": true,
|
||||
"depends_on": ["authorization-obligations", "valid-detection-benchmark", "hunter-learning-loop", "detection-core"],
|
||||
"approval_note": "2026-10-14 maintainer directive in session: spec the prover and queue implementation with Codex (fast-track; the three gates were passed on that instruction, review the documents before the second delivery). 2026-10-29: requirements amended after the 2026-10-28 gates 1-5 measurements (trace resolution, programmatic entry facts, failure visibility). 2026-10-29 (later): maintainer instruction in session ('can you address the issues?') treated as requirements approval and as the directive to regenerate the design. 2026-10-29 (capability lift): per-target vocabulary became acquired data; engine/target split, pattern discipline and complexity caps were recorded in steering; all phases reopened. 2026-10-29 (abstract-interpretation amendment): the maintainer accepted the design-review recommendations by asking to make the amendments. Requirements and design were regenerated with a three-way judgment, explicit evidence algebra, stable symbols, resource/authority domains, label-blind acquisition and separate admission measurement; that directive is recorded as approval to regenerate tasks. The merged task plan passed an independent task-graph sanity review after two design repairs. Tasks await human approval before implementation. 2026-10-29 (implementation start): the maintainer invoked /kiro-impl access-control-prover in session; that directive is recorded as tasks approval. 2026-11-01: maintainer approved moving the production fact authority, matcher and artifact writer from detection-core into access-control-prover; Requirement 13, design ownership and the repaired tasks are approved for implementation. 2026-11-01 (repo-local review ceremony): maintainer approved recording the generator as producer, maintainer session approval as reviewer basis, and the checked-in digest-addressed directory as trusted store; Tasks 14.3-14.4 implement the wire/resolver and ceremony records before data migration in Task 14.5."
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
# Implementation Plan
|
||||
|
||||
## Completed baseline
|
||||
|
||||
The completed work below produced the immutable 2026-10-28 development baseline. Amendment tasks 7-13 replace
|
||||
its final resolution, dominance, fact-acquisition and measurement semantics without erasing the evidence.
|
||||
|
||||
- [x] 1. Baseline paths and checker premise
|
||||
- [x] 1.1 Add bounded caller-index paths with concrete hop records
|
||||
- Delivered depth/fan-out traversal, entry stopping, file-qualified hops and unreached targets.
|
||||
- Baseline fixtures cover depth, fan-out, deduplication and multiple same-name files.
|
||||
- The final call semantics are superseded by tasks 8-9.
|
||||
- _Requirements: 3.1, 3.9, 3.10_
|
||||
- _Boundary: Concrete Paths_
|
||||
- [x] 1.2 Add the opt-in recognised-entry-is-protected premise
|
||||
- Existing checker defaults remain unchanged; prover evidence identifies the premise.
|
||||
- _Requirements: 4.6_
|
||||
- _Boundary: Obligation Checker_
|
||||
|
||||
- [x] 2. Baseline entry recognition
|
||||
- [x] 2.1 Add the first Java entry recognisers and evidence
|
||||
- Delivered development cases for declared web methods, servlets, resources and handlers.
|
||||
- _Requirements: 1.1, 1.5_
|
||||
- _Boundary: Mapping_
|
||||
- [x] 2.2 Add the initial PHP, JavaScript, TypeScript and Python entry recognisers
|
||||
- Delivered positive and negative fixture coverage with entry evidence.
|
||||
- _Requirements: 1.1, 1.5_
|
||||
- _Boundary: Mapping_
|
||||
|
||||
- [x] 3. Baseline enforcement and operation facts
|
||||
- [x] 3.1 Add route-, declaration- and policy-scoped enforcement witnesses
|
||||
- Delivered initial external-handler witnesses and integration fixtures.
|
||||
- _Requirements: 2.2, 2.4_
|
||||
- _Boundary: Enforcement Adapter_
|
||||
- [x] 3.2 Close the unfinished per-target fact inventory as superseded
|
||||
- The partially completed inventory is not treated as complete implementation.
|
||||
- Tasks 11.3-11.7 replace it with label-blind acquisition and promoted target knowledge.
|
||||
- _Requirements: 2.1, 2.2, 2.9, 2.10_
|
||||
- _Boundary: Target Knowledge_
|
||||
- [x] 3.3 Add handler-named operation facts
|
||||
- Delivered opt-in handler operations at declaration lines for recognised entries.
|
||||
- _Requirements: 2.3_
|
||||
- _Boundary: Fact Interpreter_
|
||||
|
||||
- [x] 4. Baseline prover, traces and structural control flow
|
||||
- [x] 4.1 Integrate entries, paths, operations and witnesses in the deterministic prover
|
||||
- Delivered hunt-defect findings and failure-visible baseline coverage with no model calls.
|
||||
- Final three-way judgment is superseded by task 10.6.
|
||||
- _Requirements: 4.7_
|
||||
- _Boundary: Prover Integration_
|
||||
- [x] 4.2 Emit deterministic ordered traces and local argument provenance
|
||||
- Delivered canonical text/JSON traces for discharged and unguarded baseline pairs.
|
||||
- _Requirements: 9.1, 9.2, 9.3, 9.5_
|
||||
- _Boundary: Canonical Trace_
|
||||
- [x] 4.3 Attach traces to prover records and findings
|
||||
- Delivered trace references through the replay envelope.
|
||||
- _Requirements: 8.1, 9.7_
|
||||
- _Boundary: Prover Integration_
|
||||
- [x] 4.4 Build structural control-flow graphs
|
||||
- Delivered branches, loops, exits, exception regions, dominators and control context.
|
||||
- _Requirements: 10.1, 10.2, 10.3, 10.4_
|
||||
- _Boundary: Structural CFG_
|
||||
- [x] 4.5 Add baseline CFG dominance and guard effects
|
||||
- Delivered value-only rejection and control-context trace fields.
|
||||
- Strict unknown semantics are superseded by task 10.1.
|
||||
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
|
||||
- _Boundary: CFG Dominance_
|
||||
|
||||
- [x] 5. Baseline replay arm
|
||||
- Delivered checkpointed records, standard metrics, coverage tables, zero-byte refusal and immutable records.
|
||||
- _Requirements: 5.1, 5.2, 5.4, 5.6_
|
||||
- _Boundary: Development Runner_
|
||||
|
||||
- [x] 6. Baseline development measurement
|
||||
- Measured 23 repositories and 483 changes: 65.2% fix-push recall, 15.0 flagged ordinary pushes per 100,
|
||||
0 instrument failures; recorded at benchmarks/measurements/2026-10-28-access-control-prover/.
|
||||
- The population is development evidence only and remains immutable.
|
||||
- _Requirements: 5.2, 5.7, 12.9_
|
||||
- _Boundary: Development Measurement_
|
||||
|
||||
## Amendment implementation
|
||||
|
||||
- [ ] 7. Abstract evidence foundation
|
||||
- [x] 7.1 Implement the evidence-relation algebra and completeness value
|
||||
- Encode the specified precision order, complete join table, dimension conjunction and witness aggregation.
|
||||
- Preserve every incompleteness reason deterministically and deduplicate reasons without losing stage identity.
|
||||
- Done when exhaustive table tests and algebraic law tests pass for every relation pair.
|
||||
- _Requirements: 11.1, 11.6, 11.7_
|
||||
- _Boundary: Abstract Evidence Domain_
|
||||
- [x] 7.2 Add resource abstract values and operation-fact fields
|
||||
- Represent exact resources, finite alias sets, request-global scope and unknown resources.
|
||||
- Extend operation facts to state when a request-global guard is sufficient.
|
||||
- Done when resource parsing and joins distinguish exact, alias, global and unknown fixtures.
|
||||
- _Requirements: 2.1, 2.5, 2.6, 2.7, 11.1_
|
||||
- _Boundary: Resource Domain_
|
||||
- _Depends: 7.1_
|
||||
- [x] 7.3 Add authority abstract values and promoted implication facts
|
||||
- Represent exact, finite and unknown authorities separately from resources.
|
||||
- Load authority implications from promoted facts and retain the supporting fact identifier.
|
||||
- Done when sufficient, insufficient and unknown authority fixtures produce the specified relations.
|
||||
- _Requirements: 2.1, 2.2, 2.5, 2.7, 2.8, 11.1_
|
||||
- _Boundary: Authority Domain_
|
||||
- _Depends: 7.1_
|
||||
- [x] 7.4 Extend judgment, trace and coverage contracts with abstract state
|
||||
- Add three-way judgments, completeness and evidence relations without changing legacy defaults.
|
||||
- Add canonical serialization for resource and authority relations and all incompleteness reasons.
|
||||
- Done when old trace fixtures remain stable in legacy mode and new fixtures round-trip all abstract fields.
|
||||
- _Requirements: 7.1, 7.2, 7.5, 7.6, 9.1, 9.2, 9.6_
|
||||
- _Boundary: Judgment Contracts, Canonical Trace_
|
||||
- _Depends: 7.1, 7.2, 7.3_
|
||||
|
||||
- [ ] 8. Stable symbols and conservative call resolution
|
||||
- [x] 8.1 Capture call receiver and arity evidence in the semantic IR
|
||||
- Preserve receiver expression and argument count for supported syntax while keeping existing constructors valid.
|
||||
- Done when language fixtures retain receiver and arity evidence without regressing existing IR consumers.
|
||||
- _Requirements: 3.2, 3.3_
|
||||
- _Boundary: Semantic IR_
|
||||
- [x] 8.2 Build stable declaration identities and the per-change symbol index
|
||||
- Identify declarations by module, enclosing type, name and available signature.
|
||||
- Cache the symbol and inheritance indexes on the per-change context.
|
||||
- Done when duplicate type names, same-name methods and overloads receive stable distinct identities.
|
||||
- _Requirements: 3.2, 3.5, 6.2_
|
||||
- _Boundary: Symbol Index_
|
||||
- _Depends: 8.1_
|
||||
- [x] 8.3 Resolve exact, finite-candidate and unresolved call targets
|
||||
- Return complete exact targets, incomplete finite candidate sets, or unresolved results with reasons.
|
||||
- Remove global bare-name fallback from prover-mode resolution.
|
||||
- Done when ambiguity never merges methods and unresolved calls cannot produce complete paths.
|
||||
- _Requirements: 3.3, 3.4, 11.1, 11.6_
|
||||
- _Boundary: Call Resolution_
|
||||
- _Depends: 8.2_
|
||||
- [x] 8.4 Resolve inheritance and base-declaration attachment
|
||||
- Attach base operations and entries only through repository-defined descendant relations.
|
||||
- Preserve ambiguous inheritance as incomplete rather than projecting to unrelated routes.
|
||||
- Done when subclass and unrelated-class fixtures produce distinct attachments and judgments.
|
||||
- _Requirements: 3.5_
|
||||
- _Boundary: Call Resolution_
|
||||
- _Depends: 8.2, 8.3_
|
||||
|
||||
- [ ] 9. Interprocedural summaries and concrete paths
|
||||
- [x] 9.1 Extract local function summaries
|
||||
- Collect each function's operations, witnesses, resolved calls and local completeness without traversing callers.
|
||||
- Done when summaries are deterministic and contain no target names outside fact identifiers.
|
||||
- _Requirements: 3.1, 3.6, 11.7_
|
||||
- _Boundary: Function Summaries_
|
||||
- _Depends: 7.4, 8.3_
|
||||
- [x] 9.2 Compute monotone summaries over recursive call components
|
||||
- Iterate finite summary sets over strongly connected components until stable.
|
||||
- Done when direct recursion and mutual recursion terminate with stable accumulated evidence.
|
||||
- _Requirements: 3.7, 11.6_
|
||||
- _Boundary: Function Summaries_
|
||||
- _Depends: 9.1_
|
||||
- [x] 9.3 Add summary caches and explicit exploration cutoffs
|
||||
- Reuse summaries per change and carry depth, fan-out, deadline and other cutoff reasons into completeness.
|
||||
- Done when each cutoff fixture returns accumulated evidence plus the exact incomplete reason.
|
||||
- _Requirements: 3.8, 6.2, 6.3, 7.2_
|
||||
- _Boundary: Function Summaries, Context Cache_
|
||||
- _Depends: 9.2_
|
||||
- [x] 9.4 Materialise exact evidence paths from summaries
|
||||
- Emit concrete entry-to-operation hops only through exact target edges.
|
||||
- Preserve every complete chain independently and retain candidate-edge diagnostics separately.
|
||||
- Done when guarded and unguarded paths to one operation remain separate and candidate edges never complete a path.
|
||||
- _Requirements: 3.9, 3.10, 11.4, 11.5_
|
||||
- _Boundary: Concrete Paths_
|
||||
- _Depends: 9.3_
|
||||
|
||||
- [ ] 10. Strict authorization judgment
|
||||
- [x] 10.1 Return abstract dominance relations without prover fallback
|
||||
- Return MUST, MAY, NO or UNKNOWN with guard effect and control context.
|
||||
- Keep legacy order dominance unchanged, but return UNKNOWN in prover mode for unavailable graphs or line mappings.
|
||||
- Done when no prover fixture is discharged or flagged solely through textual-order fallback.
|
||||
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7_
|
||||
- _Boundary: CFG Dominance_
|
||||
- _Depends: 7.1_
|
||||
- [x] 10.2 Match witness kind, scope and resource
|
||||
- Apply request-global sufficiency only when the operation fact declares it.
|
||||
- Return NO for definite mismatch and UNKNOWN for unresolved resource compatibility.
|
||||
- Done when same-resource, alias, global, mismatch and unknown fixtures match the design algebra.
|
||||
- _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 11.1_
|
||||
- _Boundary: Witness Matcher_
|
||||
- _Depends: 7.2, 10.1_
|
||||
- [x] 10.3 Match authority and promoted implications
|
||||
- Apply only promoted authority implications and preserve their evidence.
|
||||
- Return NO for insufficient authority and UNKNOWN when compatibility cannot be decided.
|
||||
- Done when stronger, weaker, unrelated and unknown authorities produce the expected relations.
|
||||
- _Requirements: 2.2, 2.5, 2.7, 2.8, 11.1_
|
||||
- _Boundary: Witness Matcher_
|
||||
- _Depends: 7.3, 10.1_
|
||||
- [x] 10.4 Compute one judgment per complete or diagnostic path
|
||||
- Conjoin witness dimensions, aggregate witnesses per obligation and apply the mixed-obligation rule.
|
||||
- Emit discharged only for all-MUST, indeterminate for any MAY/UNKNOWN, and unguarded for at least one NO.
|
||||
- Done when MUST/NO, MAY/NO, UNKNOWN and empty-witness fixtures match the specified truth table.
|
||||
- _Requirements: 4.1, 4.2, 4.3, 11.2, 11.3, 11.4_
|
||||
- _Boundary: Pair Judgment_
|
||||
- _Depends: 10.2, 10.3_
|
||||
- [x] 10.5 Aggregate multiple paths in the opt-in checker mode
|
||||
- Return unguarded if any complete path is unguarded, discharged only for complete all-discharged enumeration,
|
||||
and indeterminate otherwise.
|
||||
- Keep the legacy checker API and defaults unchanged.
|
||||
- Done when mixed guarded, unguarded and incomplete path sets produce the specified aggregate.
|
||||
- _Requirements: 3.10, 4.1, 4.2, 4.3, 4.4, 4.5, 11.5_
|
||||
- _Boundary: Obligation Checker_
|
||||
- _Depends: 9.4, 10.4_
|
||||
- [x] 10.6 Integrate three-way judgments into the prover
|
||||
- Emit hunt findings only for unguarded pairs and send no indeterminate pair to the proof arm.
|
||||
- Carry all judgments, completeness reasons, resources, authorities and stable identifiers in traces and coverage.
|
||||
- Record stage-specific exceptions without folding them into quiet coverage.
|
||||
- Done when end-to-end fixtures distinguish unguarded, discharged and indeterminate records and findings.
|
||||
- _Requirements: 4.4, 4.5, 4.6, 4.7, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 8.1, 8.2, 8.3, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7_
|
||||
- _Boundary: Prover Integration, Canonical Trace_
|
||||
- _Depends: 7.4, 9.4, 10.5_
|
||||
|
||||
- [ ] 11. Target knowledge registry and acquisition
|
||||
- [x] 11.1 (P) Add the entry-recognition strategy registry
|
||||
- Route mapping dispatch through one registry without embedding target names in engine modules.
|
||||
- Done when a fixture strategy registers, runs and records its strategy identifier and evidence.
|
||||
- _Requirements: 1.1, 1.5, 1.7_
|
||||
- _Boundary: Mapping Registry_
|
||||
- _Depends: 7.1_
|
||||
- [x] 11.2 Add the generic call-site registration strategy
|
||||
- Resolve repository-defined registered handlers through stable symbols and return unresolved registration coverage.
|
||||
- Done when exact and unresolved registration fixtures produce entries or coverage respectively.
|
||||
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6_
|
||||
- _Boundary: Registration Adapter_
|
||||
- _Depends: 8.4, 11.1_
|
||||
- [x] 11.3 (P) Extract existing target knowledge into candidate records
|
||||
- Read legacy recognition strategies and current facts without changing promoted outputs.
|
||||
- Done when extraction produces deterministic candidate records with source/version/region evidence.
|
||||
- _Requirements: 2.9_
|
||||
- _Boundary: Fact Initialization_
|
||||
- _Depends: 7.1_
|
||||
- [x] 11.4 Verify and promote proposed target knowledge
|
||||
- Accept source- or approved-documentation proposals, reject contradictions, and write only verified outputs.
|
||||
- Done when matching proposals promote and contradicted or evidence-free proposals leave promoted knowledge unchanged.
|
||||
- _Requirements: 2.9, 2.10_
|
||||
- _Boundary: Fact Initialization_
|
||||
- _Depends: 11.3_
|
||||
- [x] 11.5 Record acquisition provenance and enforce label-isolated mode
|
||||
- Persist input revision, evidence digest, accepted/rejected proposals and output digest.
|
||||
- Reject admission acquisition inputs containing vulnerability, fix-location or ordinary-change labels.
|
||||
- Done when changed output after labels are opened marks the population spent.
|
||||
- _Requirements: 12.1, 12.2, 12.3_
|
||||
- _Boundary: Fact Initialization, Admission Guard_
|
||||
- _Depends: 11.4_
|
||||
- [x] 11.6 Migrate one legacy recogniser through the registry
|
||||
- Move one development-evidence family out of inline dispatch without changing its entries.
|
||||
- Done when old fixtures stay stable and inline target-specific matching is removed for that family.
|
||||
- _Requirements: 1.1, 1.7_
|
||||
- _Boundary: Mapping Registry_
|
||||
- _Depends: 11.1_
|
||||
- [x] 11.7 Run the first label-blind development acquisition
|
||||
- Produce registration, handler-operation, enforcement, resource and authority knowledge through the gate.
|
||||
- Treat the output as development data and record its acquisition digest; do not encode names in engine modules.
|
||||
- Done when promoted outputs load, rejected proposals have reasons, and the old unfinished task 3.2 has no residue.
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 5.7, 12.9_
|
||||
- _Boundary: Target Knowledge_
|
||||
- _Depends: 10.3, 11.2, 11.5, 11.6_
|
||||
|
||||
- [ ] 12. Development and admission runners
|
||||
- [x] 12.1 Materialise and verify baseline and candidate analyzer bundles
|
||||
- Resolve analyzer revisions, fact sets, acquisition records and configurations to immutable digest-verified inputs.
|
||||
- Refuse comparison when any bundle artifact is unavailable or mismatched.
|
||||
- Done when the immutable baseline and a fixture candidate bundle can be reconstructed exactly.
|
||||
- _Requirements: 5.1, 5.5, 12.1_
|
||||
- _Boundary: Run Manifests_
|
||||
- [x] 12.2 Add record provenance, stage funnel and judgment transitions
|
||||
- Persist analyzer, fact, acquisition, configuration, population and input digests per record.
|
||||
- Summarise every analysis stage, completeness reason and transition between compared judgments.
|
||||
- Done when a fixture run accounts for every record from entry recognition through final judgment.
|
||||
- _Requirements: 5.1, 5.3, 5.4, 7.1, 7.6, 9.7, 11.8_
|
||||
- _Boundary: Development Runner_
|
||||
- _Depends: 10.6, 12.1_
|
||||
- [x] 12.3 Enforce instrument and hard runtime gates
|
||||
- Preserve zero-byte and stale-anchor refusal and add stage-aware deadline records.
|
||||
- Fail the run if any measured change exceeds 120 seconds; never summarize the overrun as a passing result.
|
||||
- Done when zero-byte, stale-anchor, deadline and 121-second fixtures all refuse the summary.
|
||||
- _Requirements: 5.6, 6.1, 6.3_
|
||||
- _Boundary: Runner Gates_
|
||||
- _Depends: 12.2_
|
||||
- [x] 12.4 Register the four-arm development comparison
|
||||
- Run baseline/candidate analyzer crossed with baseline/candidate facts over identical inputs.
|
||||
- Report metric deltas, stage-funnel deltas and judgment transitions without admission language.
|
||||
- Done when fixture arms isolate a fact-only lift from an analyzer-only lift.
|
||||
- _Requirements: 5.2, 5.5, 5.7, 11.8_
|
||||
- _Boundary: Development Runner_
|
||||
- _Depends: 12.1, 12.2, 12.3_
|
||||
- [x] 12.5 Consume and validate an externally reviewed admission manifest
|
||||
- Accept only a manifest produced by valid-detection-benchmark with review, isolation and immutable digest fields.
|
||||
- Reject label exposure, changed acquisition output or a spent population.
|
||||
- Done when valid, malformed, exposed and spent manifests produce deterministic accept/refuse results.
|
||||
- _Requirements: 12.1, 12.2, 12.3, 12.6, 12.8_
|
||||
- _Boundary: Admission Runner_
|
||||
- _Depends: 11.5, 12.3_
|
||||
- [x] 12.6 Add paired scoring and the admission report
|
||||
- Score vulnerable/fixed twins, reviewed ordinary changes, indeterminate rate, coverage, runtime and instruments.
|
||||
- Preserve sample counts and uncertainty intervals and return NO-GO on any pre-registered gate failure.
|
||||
- Done when fixture pairs detect vulnerable-only, fixed-side regression and mislabeled-negative cases.
|
||||
- _Requirements: 5.2, 12.4, 12.5, 12.7, 12.8, 12.9_
|
||||
- _Boundary: Admission Runner_
|
||||
- _Depends: 12.5_
|
||||
|
||||
- [ ] 13. Cross-cutting validation and measurements
|
||||
- [x] 13.1 Validate abstract-domain and metamorphic laws
|
||||
- Cover the full join table, conjunction, witness aggregation, monotonicity and mixed-obligation rule.
|
||||
- Prove that type renaming preserves a judgment and adding ambiguity can only preserve it or make it indeterminate.
|
||||
- Done when property and fixture tests pass deterministically across repeated runs.
|
||||
- _Requirements: 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_
|
||||
- _Boundary: Abstract Evidence Validation_
|
||||
- _Depends: 10.5_
|
||||
- [x] 13.2 (P) Validate cross-component analysis scenarios
|
||||
- Exercise helper guards, registration entries, base attachment, resource mismatch, authority insufficiency,
|
||||
ambiguous calls, unavailable CFGs, recursion and budget cutoff.
|
||||
- Done when every scenario yields the expected trace, coverage reasons and three-way judgment.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.5, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 4.1, 4.2, 4.3, 10.6_
|
||||
- _Boundary: Prover Integration Validation_
|
||||
- _Depends: 10.6, 11.7_
|
||||
- [x] 13.3 Run the four-arm development regression
|
||||
- Run all four registered arms on the immutable current development population after proving nonzero input reads.
|
||||
- Publish analyzer/fact attribution, stage funnel, three-way judgments, budget result and comparison to the
|
||||
2026-10-28 baseline; label the result development-only.
|
||||
- Done when all arm records and summary digests are complete and no admission claim appears.
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 6.1, 7.3, 11.8, 12.9_
|
||||
- _Boundary: Development Measurement_
|
||||
- _Depends: 12.4, 13.1, 13.2_
|
||||
- [x] 13.4 Run admission from an externally prepared untouched manifest
|
||||
- Consume a reviewed manifest from valid-detection-benchmark after analyzer and facts are frozen.
|
||||
- Verify label isolation, paired vulnerable/fixed behavior, reviewed ordinary labels and every admission gate.
|
||||
- Done when the report returns an evidence-backed GO or NO-GO without modifying facts, strategies or thresholds.
|
||||
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9_
|
||||
- _Boundary: Admission Measurement_
|
||||
- _Depends: 12.6, 13.3_
|
||||
|
||||
- [ ] 14. Reviewed detection-core fact adapter
|
||||
- [x] 14.1 Define the production access-control family contract
|
||||
- Encode the versioned access-control security object, roles, three decision rules, residual question and closed incompleteness reasons through public detection-core types.
|
||||
- Keep target names confined to the reviewed ecosystem declaration while pinning every digest-affecting field, status, origin policy and decision semantic declared by the design.
|
||||
- Done when the canonical production contract validates, round-trips deterministically, and rejects every altered or undeclared role, decision, field and reason fixture.
|
||||
- _Requirements: 13.1_
|
||||
- _Boundary: Access-Control Family Contract_
|
||||
- _Depends: 11.5_
|
||||
- [x] 14.2 Implement externally bootstrapped fact authority
|
||||
- Define the immutable authority bundle and separate host bootstrap policy over reviewed pack/projection references, policies, lineages, matcher identity, reviewer/store admission, forbidden identity pairs and conflict disclosure.
|
||||
- Re-derive canonical policy, attestation, disclosure and bundle identities before any target data is consumed; never let the bundle choose its own trust roots.
|
||||
- Done when a complete independently reviewed authority fixture authenticates and every missing, duplicate, substituted, self-authored or non-canonical variant fails before pack loading.
|
||||
- _Requirements: 13.2, 13.3_
|
||||
- _Boundary: Fact Authority_
|
||||
- _Depends: 14.1_
|
||||
- [x] 14.3 Implement installed authority codecs and resolver
|
||||
- Implement closed canonical JSON decoders for the bootstrap policy, installed authority, v2 fact packs, nested policies/lineages/evidence and immutable content references.
|
||||
- Implement the checked-in digest-addressed resolver using only validated `content_digest` for filesystem addressing and re-derivable storage receipts under an injected bootstrap-admitted store identity.
|
||||
- Done when canonical synthetic installed fixtures round-trip to Task 14.2 typed values and unknown/missing fields, noncanonical bytes, traversal-like artifact IDs, digest paths or byte substitutions fail before authentication.
|
||||
- _Requirements: 13.2, 13.3_
|
||||
- _Boundary: Installed Fact Authority Wire_
|
||||
- _Depends: 14.2_
|
||||
- [x] 14.4 Record the approved repo-local review ceremony
|
||||
- Write the three closed canonical identity manifests with the exact schema, role, name and approval-basis values declared by the design.
|
||||
- Bind bootstrap reviewer admission, custody policy, authority review and pack evidence identities to those manifest digests while keeping all trust/store admission outside `authority.json`.
|
||||
- Done when the approved manifests and bootstrap load through Task 14.3, all identity bindings validate, and changing any field or approval basis invalidates authentication.
|
||||
- _Requirements: 13.2, 13.3_
|
||||
- _Boundary: Installed Authority Review Ceremony_
|
||||
- _Depends: 14.3_
|
||||
- [x] 14.5 Migrate the installed entry knowledge into reviewed v2 packs
|
||||
- Produce one language-scoped v2 pack per promoted language and a separately authenticated canonical compatibility projection from the existing reviewed entry data.
|
||||
- Map every migrated row to declared roles, typed structural requirements and relation effects without adding target vocabulary to engine modules.
|
||||
- Preserve legacy pattern recognition byte-for-byte while preventing the projection from constructing detection-core evidence or readiness.
|
||||
- Done when legacy recognition fixtures are unchanged, both reviewed language packs validate under task 14.2 authority, and projection-only input remains non-authoritative.
|
||||
- _Requirements: 1.1, 1.5, 1.7, 2.9, 2.10, 13.2, 13.4_
|
||||
- _Boundary: Entry Knowledge Migration_
|
||||
- _Depends: 14.1, 14.2, 14.3, 14.4_
|
||||
- [x] 14.6 Add canonical structural-observation and fact-match persistence
|
||||
- Provide the downstream write boundary that persists canonical `StructuralObservation` bytes and returns immutable references through a read-only resolver.
|
||||
- Persist completed `fact_match_bytes` only after its observation references exist, then return the match reference without inserting a circular self-reference into the match digest.
|
||||
- Done when stored observation and match bytes resolve canonically and byte or reference substitution is refused deterministically.
|
||||
- _Requirements: 13.5_
|
||||
- _Boundary: Fact-Match Artifact Writer_
|
||||
- [x] 14.7 Implement the authority-bearing structural matcher
|
||||
- Build typed observations from repository structure for the requirements declared by the selected reviewed fact; do not infer missing structure from a regex or name hit.
|
||||
- Use task 14.6 to persist every observation and completed `FactMatchEvidence`, then pass the immutable match reference with family contract, policies, lineages, custody and read-only resolver to detection-core.
|
||||
- Bind matcher behavior and fact lineage to current custody and the evaluation population so same-population label influence is incomplete or refused.
|
||||
- Done when complete fixtures apply the declared relations, while name-only, partial, ambiguous, substituted and contaminated fixtures cannot produce complete evidence.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.5, 7.2, 9.4, 13.5, 13.6_
|
||||
- _Boundary: Structural Matcher_
|
||||
- _Depends: 14.2, 14.5, 14.6_
|
||||
- [x] 14.8 Produce the downstream conformance handoff
|
||||
- Run detection-core fact-pack, fact-match, application and full-evidence validators over target-owned fixtures after proving nonzero fixture reads.
|
||||
- Publish an immutable manifest naming every fixture, artifact digest, validator result and negative-case reason for detection-core task 9.4 to accept; do not add target fixtures to core.
|
||||
- Exercise authority substitution, producer/reviewer conflict, resolver byte substitution, lineage omission, custody contamination and deterministic replay.
|
||||
- Done when the complete target-owned fixture passes, every incomplete/self-authored/substituted/contaminated/legacy-only variant is refused with a stable reason, and the immutable handoff manifest is reproducible.
|
||||
- _Requirements: 7.2, 7.4, 12.2, 12.3, 13.7_
|
||||
- _Boundary: Downstream Adapter Conformance Handoff_
|
||||
- _Depends: 14.7_
|
||||
- [x] 14.9 Migrate the production entry caller and prove zero bypasses
|
||||
- Wire the externally bootstrapped authority-bearing adapter into the target-aware `mapping/legacy.py` entry path while keeping `analyze_entry_points` stable for its callers.
|
||||
- Retain the legacy projection only for compatibility recognition; prohibit it from supplying v2 evidence, readiness or admission.
|
||||
- Run import/caller and artifact-writer censuses proving no production path bypasses authority or treats legacy projection output as v2 evidence.
|
||||
- Done when existing entry recognition remains byte-stable, explicit v2 matching uses the reviewed persisted path, and both production bypass censuses are zero.
|
||||
- _Requirements: 1.1, 1.5, 1.7, 13.4, 13.7, 13.8_
|
||||
- _Boundary: Production Mapping Integration_
|
||||
- _Depends: 14.8_
|
||||
- _Blocked: detection-core task 9.4 must accept the immutable handoff from task 14.8 before production caller migration_
|
||||
|
||||
## Task constraints
|
||||
|
||||
- Complexity: McCabe at most 10 per touched function, at most 50 logical lines per function and 400 per new module.
|
||||
- Engine/target split: abstract-domain, resolution, summary, judgment and trace modules contain no target names.
|
||||
- Target knowledge: names enter only through promoted facts or registered target-aware strategies.
|
||||
- Compatibility: legacy checker defaults and completed baseline records remain unchanged.
|
||||
- Instrument rule: validation must prove nonzero input reads before interpreting any clean or zero result.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Task 14.3 planning: the 963-byte legacy entry file was readable, but typed authority validation was not an installed-artifact format. The maintainer approved the repo-local review ceremony; Tasks 14.3–14.4 now separate wire/resolver implementation from the identity/bootstrap records before Task 14.5 migrates data.
|
||||
|
||||
## Baseline implementation record
|
||||
|
||||
- Deliveries A-C implemented the baseline paths, recognisers, witnesses, handler facts, prover, traces and CFG with
|
||||
static checks and focused tests passing at delivery time.
|
||||
- The full development arm then processed all 483 changes with zero instrument failures. Its maximum runtime exceeded
|
||||
the amended hard budget, so the baseline is evidence, not an eligible arm.
|
||||
- The 2026-10-28 ablation identified call resolution, resource/authority precision and failure visibility as the
|
||||
amendment levers. Those observations inform tasks 7-13 but are not admission labels.
|
||||
@@ -141,7 +141,7 @@ before `ax apply`, and a task without a Model gets none. The value is never rend
|
||||
Every actor connection except DNS goes through Agent Substrate's egress gateway (`atenet-egress`), which denies
|
||||
an actor without an `EgressPolicy`. Three pieces make the plane work under that rule:
|
||||
|
||||
- **Receiver by name.** `ops/ax/receiver-service.yaml.tmpl` (applied by `up.sh`) is a selector-less Service
|
||||
- Receiver by name. `ops/ax/receiver-service.yaml.tmpl` (applied by `up.sh`) is a selector-less Service
|
||||
`ousast-receiver` in `ax-system`, port 80, with an EndpointSlice on the kind gateway at `OUSAST_ARTIFACT_PORT`
|
||||
(fixed, 18090), where the reconciler binds its receiver. Tasks get
|
||||
`OUSAST_ARTIFACT_URL=http://ousast-receiver.ax-system.svc.cluster.local/` plus `OUSAST_ARTIFACT_DIAL=<the
|
||||
@@ -150,12 +150,12 @@ an actor without an `EgressPolicy`. Three pieces make the plane work under that
|
||||
`Host` but connects to the address the actor dialled (measured: dialling the kind gateway on port 80 was allowed
|
||||
and then refused upstream), so the dial address is the ClusterIP, which kube-proxy maps to the host.
|
||||
`OUSAST_ARTIFACT_HOST` set keeps the old direct URL (tests).
|
||||
- **Per-task policy.** `policy_for` grants `http` on port 80 to the receiver name, and `tls_passthrough` on 443
|
||||
- Per-task policy. `policy_for` grants `http` on port 80 to the receiver name, and `tls_passthrough` on 443
|
||||
to the Git hosts of the bound Workspaces' `https` repos and to the bound Model's
|
||||
`openultrasast.io/egress-hosts` (`api.deepseek.com` for `deepseek-flash`, validated as a plain DNS name).
|
||||
Nothing else: no IP address or wildcard, and passthrough only -- an `https` rule intercepts TLS and the actor
|
||||
would need the gateway CA, which ax templates do not mount.
|
||||
- **Order.** `ax apply` creates the actor; the reconciler writes the policy with `kubectl ate create
|
||||
- Order. `ax apply` creates the actor; the reconciler writes the policy with `kubectl ate create
|
||||
egress-policy` (update when one exists), reads it back, waits `OUSAST_EGRESS_SETTLE` (11 s: the gateway caches a
|
||||
missing policy as deny for 10 s), then resumes. kubectl-ate has no delete verb; Substrate's store deletes the
|
||||
policy with its actor, so after `ax delete task` the reconciler confirms it is gone.
|
||||
|
||||
@@ -14,19 +14,19 @@ a DeepSeek provider -- is carried by our thin `Run` layer and dissolves into ax
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: the manifest kinds and a `Run` kind for dependencies, artifacts and budgets; bring-up of ax on
|
||||
- In scope: the manifest kinds and a `Run` kind for dependencies, artifacts and budgets; bring-up of ax on
|
||||
this host (kind + Agent Substrate + ax controller) with a doctor command; an ax-backed reconciler that
|
||||
submits Tasks to ax (one engine-bearing task at a time) and is the only executor; the runner contract and a
|
||||
runner image; the first two tasks, `repo-facts` and
|
||||
`verify`, with the existing scripts as reference behaviour; measurement on the 46-candidate validation set.
|
||||
- **Out of scope**: a remote or multi-node cluster; contributing providers or budgets to ax upstream (a
|
||||
- Out of scope: a remote or multi-node cluster; contributing providers or budgets to ax upstream (a
|
||||
follow-on once the increment is measured); changing what the model is asked (prompts, families, pass rules)
|
||||
except as required to consume `repo-facts`; the judge stage, the config-family question and population v3
|
||||
(they become tasks later, under their own specs); any change to `evaluate.py`'s protocol scoring.
|
||||
- **Adjacent expectations**: the independent-population protocols and records stay as they are; `finding_dump`
|
||||
- Adjacent expectations: the independent-population protocols and records stay as they are; `finding_dump`
|
||||
and the engine path are untouched; `vulnerabilities-over-plumbing` applies -- this is plumbing that must pay
|
||||
for itself in measured tokens and unchanged or better recall.
|
||||
- **Target state (maintainer, 2026-09-29)**: this plane replaces HarnessX as the agentic plane; HarnessX is
|
||||
- Target state (maintainer, 2026-09-29): this plane replaces HarnessX as the agentic plane; HarnessX is
|
||||
removed by its own spec (`harnessx-removal`) once Requirement 6's measurement shows value; the `Run` layer
|
||||
shrinks as ax gains dependencies, budgets and artifacts. No new HarnessX feature is added meanwhile.
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ Nothing in the pipeline can represent "this operation should have been guarded"
|
||||
|
||||
## Desired Outcome
|
||||
|
||||
Absence bugs become first-class, measured, and honest. An obligated operation (protected data access, privileged action, security configuration) reached from an entry point without a discharger (a dominating auth guard, an identity constraint bound from the authenticated context, a non-permissive value) is reported as an obligation violation, with the missing discharger kind, the evidence for why the obligation exists (sibling handlers that discharge it, or a declared policy clause), and the known fix shape the corpus taught. Leave-one-out on the absence pairs is the objective, per obligation kind and per provenance profile, so the number rises as the corpus grows. `agent-vfc` and `vibe-py` absence pairs move from 0 detected to a measured baseline that the engine specs can raise.
|
||||
Absence bugs become first-class and are measured without overstating results. An obligated operation (protected data access, privileged action, security configuration) reached from an entry point without a discharger (a dominating auth guard, an identity constraint bound from the authenticated context, a non-permissive value) is reported as an obligation violation, with the missing discharger kind, the evidence for why the obligation exists (sibling handlers that discharge it, or a declared policy clause), and the known fix shape the corpus taught. Leave-one-out on the absence pairs is the objective, per obligation kind and per provenance profile, so the number rises as the corpus grows. `agent-vfc` and `vibe-py` absence pairs move from 0 detected to a measured baseline that the engine specs can raise.
|
||||
|
||||
Principle written into the roadmap: **an operation carries obligations, not only data; the application's own consistency and its declared policy say which, and the tool reports the missing discharger rather than a phantom flow.**
|
||||
Principle written into the roadmap: **an operation carries data and obligations; the application's own consistency and its declared policy say which, and the tool reports the missing discharger rather than a phantom flow.**
|
||||
|
||||
## Approach
|
||||
|
||||
@@ -18,8 +18,8 @@ Obligated operations and discharger kinds are closed facts data, like sinks and
|
||||
|
||||
## Scope
|
||||
|
||||
- **In**: obligated-operation and discharger facts; obligation shapes derived from pairs; consistency detection across sibling handlers; optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; new evidence rungs and report wording for obligations; worth-fixing weighting by resource sensitivity; corpus recipe shape for absence pairs (handler plus registration context); leave-one-out per obligation kind; `obligations` improve lever; offline tests.
|
||||
- **Out**: taint or facts changes for flow bugs; the call graph and path records themselves (reachability spec); guard-dominance IR (guard-dominance-regime); LLM-authored policy or shapes; any merge gate; vendoring unlicensed code; sandbox proof of absence bugs (they do not crash).
|
||||
- In: obligated-operation and discharger facts; obligation shapes derived from pairs; consistency detection across sibling handlers; optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; new evidence rungs and report wording for obligations; worth-fixing weighting by resource sensitivity; corpus recipe shape for absence pairs (handler plus registration context); leave-one-out per obligation kind; `obligations` improve lever; offline tests.
|
||||
- Out: taint or facts changes for flow bugs; the call graph and path records themselves (reachability spec); guard-dominance IR (guard-dominance-regime); LLM-authored policy or shapes; any merge gate; vendoring unlicensed code; sandbox proof of absence bugs (they do not crash).
|
||||
|
||||
## Boundary Candidates
|
||||
|
||||
@@ -40,13 +40,13 @@ Obligated operations and discharger kinds are closed facts data, like sinks and
|
||||
|
||||
## Upstream / Downstream
|
||||
|
||||
- **Upstream**: `EntryPointRecord` (route, access level, decorators, conditions); `FileIR`; path records and dominance from the two engine specs; `PairCase` with `review_tier`; `MechanismStore` and the lever machinery; facts loader.
|
||||
- **Downstream**: reports and SARIF; worth-fixing ranking; the roadmap's leave-one-out table gains obligation kinds; `guard-dominance-regime` consumes obligation discharger kinds as guard candidates for web code.
|
||||
- Upstream: `EntryPointRecord` (route, access level, decorators, conditions); `FileIR`; path records and dominance from the two engine specs; `PairCase` with `review_tier`; `MechanismStore` and the lever machinery; facts loader.
|
||||
- Downstream: reports and SARIF; worth-fixing ranking; the roadmap's leave-one-out table gains obligation kinds; `guard-dominance-regime` consumes obligation discharger kinds as guard candidates for web code.
|
||||
|
||||
## Existing Spec Touchpoints
|
||||
|
||||
- **Extends**: `corpus-seeded-mechanisms` (second shape family, same store, same lever contract), `pair-corpus-honesty` (registration-context excerpt mode, obligation label), `three-stage-scan` (one more proposer in MAP, new rungs below proof).
|
||||
- **Depends**: `reachability-flow-model` (entry point to operation paths); `guard-dominance-regime` (a guard dominates the path). Reachability is now on the critical path for two consumers.
|
||||
- Extends: `corpus-seeded-mechanisms` (second shape family, same store, same lever contract), `pair-corpus-honesty` (registration-context excerpt mode, obligation label), `three-stage-scan` (one more proposer in MAP, new rungs below proof).
|
||||
- Depends: `reachability-flow-model` (entry point to operation paths); `guard-dominance-regime` (a guard dominates the path). Reachability is now on the critical path for two consumers.
|
||||
|
||||
## Constraints
|
||||
|
||||
|
||||
@@ -152,9 +152,9 @@ tests/
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `src/openultrasast/cli.py` — `_check_obligations` after `_search_variants` in MAP (standard/deep); manifest `obligations` block; `mechanisms export` writes obligation rows too.
|
||||
- `src/openultrasast/pairs.py` — `ExpectedFinding.obligation: str | None`; `_detections`-side additive rule: for a row with `obligation`, a finding with tag `obligation:<kind>` inside the labeled function counts.
|
||||
- `src/openultrasast/semantic/mechanisms.py` — no schema change; `corpus_mechanisms()` unchanged; a helper `obligation_mechanisms(records)` selects `shape["family"] == "obligation"` (added in `obligations/shapes.py`, not in the store module).
|
||||
- `src/openultrasast/cli.py`: `_check_obligations` after `_search_variants` in MAP (standard/deep); manifest `obligations` block; `mechanisms export` writes obligation rows too.
|
||||
- `src/openultrasast/pairs.py`: `ExpectedFinding.obligation: str | None`; `_detections`-side additive rule: for a row with `obligation`, a finding with tag `obligation:<kind>` inside the labeled function counts.
|
||||
- `src/openultrasast/semantic/mechanisms.py`: no schema change; `corpus_mechanisms()` unchanged; a helper `obligation_mechanisms(records)` selects `shape["family"] == "obligation"` (added in `obligations/shapes.py`, not in the store module).
|
||||
- `src/openultrasast/reports.py`, `rank.py`, `config.py`, `improve/*`, `semantic/seed.py`, `semantic/loo.py`, `benchmarks/pairs/harvest.py`, `benchmarks/pairs/mechanisms.toml` as listed above.
|
||||
|
||||
## System Flows
|
||||
@@ -177,7 +177,7 @@ flowchart LR
|
||||
I --> J[StaticFinding suspicion; tags obligation:*, discharger:*, obligation_evidence:*]
|
||||
```
|
||||
|
||||
Gating: the checker emits a finding only when (1) the operation is reached — from an entry point along a `PathRecord`, or, without paths, when the operation sits in a handler that is itself an entry point — and (2) no discharger of the required kind dominates, and (3) the obligation is justified by a sibling set (`consistency_violation`), a policy clause (`declared_policy_violation`) or, in degraded mode without either, by the operation fact alone (`function_local`). A declared public route suppresses path-guard obligations for that route only.
|
||||
Gating: the checker emits a finding only when (1) the operation is reached (from an entry point along a `PathRecord`, or, without paths, when the operation sits in a handler that is itself an entry point) and (2) no discharger of the required kind dominates, and (3) the obligation is justified by a sibling set (`consistency_violation`), a policy clause (`declared_policy_violation`) or, in degraded mode without either, by the operation fact alone (`function_local`). A declared public route suppresses path-guard obligations for that route only.
|
||||
|
||||
### Teaching and measuring
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ AppSec engineers and maintainers of OpenUltraSAST scan web code, increasingly vi
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: obligated operations and discharger kinds as closed facts data; obligation shapes learned from trusted pairs; consistency detection across sibling handlers; an optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; obligation evidence rungs, report wording and worth-fixing weighting; a corpus recipe shape that keeps the handler's registration context; leave-one-out per obligation kind; an `obligations` improve lever; offline tests.
|
||||
- **Out of scope**: flow taint, walker or sink facts; the call graph, path records and dominance computation (owned by `reachability-flow-model` and `guard-dominance-regime`); model-authored policy or shapes; sandbox proof of absence bugs; any merge gate; vendoring unlicensed code.
|
||||
- **Adjacent expectations**: the entry-point mapper keeps providing routes with an access level and their decorators; the reachability spec provides entry-point-to-operation paths and the guard-dominance spec provides "a guard dominates the path"; the mechanism store, its deterministic ids and the lever contract from `corpus-seeded-mechanisms` are reused unchanged; the pair scorer accepts one additive label field; `local` remains the only slice that can fail CI.
|
||||
- In scope: obligated operations and discharger kinds as closed facts data; obligation shapes learned from trusted pairs; consistency detection across sibling handlers; an optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; obligation evidence rungs, report wording and worth-fixing weighting; a corpus recipe shape that keeps the handler's registration context; leave-one-out per obligation kind; an `obligations` improve lever; offline tests.
|
||||
- Out of scope: flow taint, walker or sink facts; the call graph, path records and dominance computation (owned by `reachability-flow-model` and `guard-dominance-regime`); model-authored policy or shapes; sandbox proof of absence bugs; any merge gate; vendoring unlicensed code.
|
||||
- Adjacent expectations: the entry-point mapper keeps providing routes with an access level and their decorators; the reachability spec provides entry-point-to-operation paths and the guard-dominance spec provides "a guard dominates the path"; the mechanism store, its deterministic ids and the lever contract from `corpus-seeded-mechanisms` are reused unchanged; the pair scorer accepts one additive label field; `local` remains the only slice that can fail CI.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Summary
|
||||
|
||||
- **Feature**: `authorization-obligations`
|
||||
- **Discovery Scope**: Extension of the semantic layer (facts, shapes, store, MAP proposers), the entry-point mapper, reports, the pair corpus, leave-one-out and the improve lever; complex integration because it consumes two unimplemented upstream specs.
|
||||
- **Key Findings**:
|
||||
- Feature: `authorization-obligations`
|
||||
- Discovery Scope: Extension of the semantic layer (facts, shapes, store, MAP proposers), the entry-point mapper, reports, the pair corpus, leave-one-out and the improve lever; complex integration because it consumes two unimplemented upstream specs.
|
||||
- Key Findings:
|
||||
- The absence pairs are real and structured. `vampi-books-get-by-title` has identical code on both sides except a constraint `user=user` bound from `token_validator(request.headers.get('Authorization'))['sub']`; the vulnerable branch queries `Book` by title alone. The difference is the provenance of a constraint, not a flow. 15 of 29 `agent-vfc` rows and 6 of the 19 non-teaching `vibe-py` pairs carry absence labels (`missing_auth_guard`, `permissive_default`, `identity_from_request_body`, `secret_in_client`, `validation_strength`).
|
||||
- The entry-point mapper already classifies every route as `public`, `authenticated` or `role-restricted` from its decorators (`mapping._python_route_access`), records the decorators as `access_evidence`, and lists `conditions` (`if` tests that look like checks). That is the sibling detector's upstream for path guards and needs no change.
|
||||
- `FileIR` binds carry `value_text`, `names` and `call_name`; `variants.source_kind_of_name` already traces a name through binds to a fact source, a parameter or a container read. Provenance of a constraint's value (authenticated context versus request input versus constant) is the same trace with one more source class.
|
||||
@@ -17,29 +17,29 @@
|
||||
|
||||
### What the absence pairs actually differ by
|
||||
|
||||
- **Context**: Decide whether absence bugs can be a shape family at all.
|
||||
- **Sources Consulted**: `benchmarks/pairs/vibe-py` excerpts for `missing_auth_guard` and `permissive_default` rows; `benchmarks/pairs/agent-vfc/recipes.toml` mechanisms; `mechanisms.toml` definitions.
|
||||
- **Findings**: three recurring differences: (a) a constraint added to a data access whose value comes from the authenticated context; (b) a guard call or decorator added before the operation (early return on a failed token check, `login_required`); (c) a literal changed from permissive to restrictive (`CORS(app)` → origins list, `DEBUG=True` → `False`, RLS enabled). A fourth, rarer: identity read from the body replaced by identity from the token (same as (a) seen from the other side).
|
||||
- **Implications**: discharger kinds `identity_constraint`, `path_guard`, `ownership_check`, `non_permissive_value`, `validated_input`; the value provenance is the discriminating feature for (a) and (d).
|
||||
- Context: Decide whether absence bugs can be a shape family at all.
|
||||
- Sources Consulted: `benchmarks/pairs/vibe-py` excerpts for `missing_auth_guard` and `permissive_default` rows; `benchmarks/pairs/agent-vfc/recipes.toml` mechanisms; `mechanisms.toml` definitions.
|
||||
- Findings: three recurring differences: (a) a constraint added to a data access whose value comes from the authenticated context; (b) a guard call or decorator added before the operation (early return on a failed token check, `login_required`); (c) a literal changed from permissive to restrictive (`CORS(app)` → origins list, `DEBUG=True` → `False`, RLS enabled). A fourth, rarer: identity read from the body replaced by identity from the token (same as (a) seen from the other side).
|
||||
- Implications: discharger kinds `identity_constraint`, `path_guard`, `ownership_check`, `non_permissive_value`, `validated_input`; the value provenance is the discriminating feature for (a) and (d).
|
||||
|
||||
### Where the policy comes from
|
||||
|
||||
- **Context**: Absence is relative to intent; the tool must not invent intent.
|
||||
- **Sources Consulted**: `mapping.analyze_entry_points` (routes, access level, evidence), Real-Vuln repositories (many handlers per resource), Clearwing's sibling-comparison practice.
|
||||
- **Findings**: in every Real-Vuln web app the handlers on one resource share a router and a model; the vulnerable handler is the odd one out. Route access classification already exists. A declared policy is the only way to say "this route is meant to be public"; the hunter can only guess it.
|
||||
- **Implications**: consistency first (no declaration needed, measurable now), declared policy second (stronger rung), hunter last (records intent, never raises).
|
||||
- Context: Absence is relative to intent; the tool must not invent intent.
|
||||
- Sources Consulted: `mapping.analyze_entry_points` (routes, access level, evidence), Real-Vuln repositories (many handlers per resource), Clearwing's sibling-comparison practice.
|
||||
- Findings: in every Real-Vuln web app the handlers on one resource share a router and a model; the vulnerable handler is the odd one out. Route access classification already exists. A declared policy is the only way to say "this route is meant to be public"; the hunter can only guess it.
|
||||
- Implications: consistency first (no declaration needed, measurable now), declared policy second (stronger rung), hunter last (records intent, never raises).
|
||||
|
||||
### Dominance before guard-dominance-regime exists
|
||||
|
||||
- **Context**: Req 5 needs "a discharger dominates the path"; the spec that owns dominance is not written.
|
||||
- **Findings**: within one handler, the existing `FunctionIR` order of binds and calls gives statement order; a guard call followed by an early return before the operation line is dominance in the common case; decorators dominate the whole handler; middleware attached to the router dominates every route under it. Across calls, `PathRecord.hops` gives the sequence of nodes; a discharger on any hop before the operation node dominates the path when the hop's function returns early on failure.
|
||||
- **Implications**: define `Dominance` as a narrow protocol (`dominates(witness, operation) -> bool`) with a built-in `OrderDominance` implementation (statement order in the handler, decorators, router middleware, hop order) that `guard-dominance-regime` may replace with nesting-aware dominance. The degraded mode (`function_local`) is `OrderDominance` without path records.
|
||||
- Context: Req 5 needs "a discharger dominates the path"; the spec that owns dominance is not written.
|
||||
- Findings: within one handler, the existing `FunctionIR` order of binds and calls gives statement order; a guard call followed by an early return before the operation line is dominance in the common case; decorators dominate the whole handler; middleware attached to the router dominates every route under it. Across calls, `PathRecord.hops` gives the sequence of nodes; a discharger on any hop before the operation node dominates the path when the hop's function returns early on failure.
|
||||
- Implications: define `Dominance` as a narrow protocol (`dominates(witness, operation) -> bool`) with a built-in `OrderDominance` implementation (statement order in the handler, decorators, router middleware, hop order) that `guard-dominance-regime` may replace with nesting-aware dominance. The degraded mode (`function_local`) is `OrderDominance` without path records.
|
||||
|
||||
### Store and lever reuse
|
||||
|
||||
- **Context**: Avoid a second store and a second lever machinery.
|
||||
- **Findings**: `Mechanism.shape` is a dict validated where used; `corpus_mechanism_id` hashes `shape.key()`; `validate_mechanism` checks guard and source kinds against `variants` constants. A second family needs its own closed kinds check and a `family` field in the shape dict; the tombstone, snapshot and profile clause need nothing.
|
||||
- **Implications**: `ObligationShape.to_dict()` carries `family = "obligation"`; the lever validator dispatches on `family`; `search_tree` ignores obligation rows (they are not sink shapes) and the obligation checker ignores sink rows.
|
||||
- Context: Avoid a second store and a second lever machinery.
|
||||
- Findings: `Mechanism.shape` is a dict validated where used; `corpus_mechanism_id` hashes `shape.key()`; `validate_mechanism` checks guard and source kinds against `variants` constants. A second family needs its own closed kinds check and a `family` field in the shape dict; the tombstone, snapshot and profile clause need nothing.
|
||||
- Implications: `ObligationShape.to_dict()` carries `family = "obligation"`; the lever validator dispatches on `family`; `search_tree` ignores obligation rows (they are not sink shapes) and the obligation checker ignores sink rows.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
@@ -54,23 +54,23 @@
|
||||
|
||||
### Decision: Obligation findings are `suspicion` with a data label, not new ladder rungs
|
||||
|
||||
- **Context**: Req 6.1 names three labels; the ladder is closed and owned elsewhere.
|
||||
- **Selected Approach**: `StaticFinding.evidence_level = "suspicion"`, tags `obligation:<kind>`, `discharger:<kind>`, `obligation_evidence:<label>`; reports render the label. Dispositions and `ALLOWED_EVIDENCE` unchanged.
|
||||
- **Trade-offs**: SARIF consumers see `suspicion` plus properties; honest and non-invasive.
|
||||
- Context: Req 6.1 names three labels; the ladder is closed and owned elsewhere.
|
||||
- Selected Approach: `StaticFinding.evidence_level = "suspicion"`, tags `obligation:<kind>`, `discharger:<kind>`, `obligation_evidence:<label>`; reports render the label. Dispositions and `ALLOWED_EVIDENCE` unchanged.
|
||||
- Trade-offs: SARIF consumers see `suspicion` plus properties; honest and non-invasive.
|
||||
|
||||
### Decision: Dominance is a protocol with an order-based default
|
||||
|
||||
- **Context**: `guard-dominance-regime` is unwritten; this spec cannot wait for it to be useful.
|
||||
- **Selected Approach**: `Dominance` protocol; `OrderDominance` default (decorator, router middleware, statement order before the operation, hop order along a path). The guard spec replaces it without touching callers.
|
||||
- **Trade-offs**: order-based dominance misses guards inside conditionals; recorded as the known coarseness and measured on the fixed twins.
|
||||
- Context: `guard-dominance-regime` is unwritten; this spec cannot wait for it to be useful.
|
||||
- Selected Approach: `Dominance` protocol; `OrderDominance` default (decorator, router middleware, statement order before the operation, hop order along a path). The guard spec replaces it without touching callers.
|
||||
- Trade-offs: order-based dominance misses guards inside conditionals; recorded as the known coarseness and measured on the fixed twins.
|
||||
|
||||
### Decision: Policy is never written by the tool
|
||||
|
||||
- **Selected Approach**: `declared policy` is a hand-maintained file with a closed schema; the lever may only *propose* a clause, journaled as pending until a human accepts; scans never read pending clauses.
|
||||
- Selected Approach: `declared policy` is a hand-maintained file with a closed schema; the lever may only *propose* a clause, journaled as pending until a human accepts; scans never read pending clauses.
|
||||
|
||||
### Decision: Resource identity for sibling grouping
|
||||
|
||||
- **Selected Approach**: a handler's resource is the trailing segment of its route path with parameters stripped, joined with the operation's target token (model, table, collection name from the operation fact match); router or module is the second key. Coarse but deterministic and text-free.
|
||||
- Selected Approach: a handler's resource is the trailing segment of its route path with parameters stripped, joined with the operation's target token (model, table, collection name from the operation fact match); router or module is the second key. Coarse but deterministic and text-free.
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
# Brief: the detector optimizer
|
||||
|
||||
## Problem
|
||||
|
||||
`learning-harness` built the measurement layer honestly — a closed family taxonomy, a class-aware pair
|
||||
scorer, per-family detector configurations, a split enforced by construction, verifier-gated reporting,
|
||||
and a journal with attribution. What it did **not** build is a search. `run_learning_round` is a
|
||||
hand-rolled hill-climber: one proposal, one minibatch, one holdout check, a threshold rule, accept or
|
||||
revert. Every defect four review rounds found in it was a defect of that improvised loop rather than of
|
||||
the measurement underneath it.
|
||||
|
||||
The maintainer's judgement, and the reason for this spec: the remaining work is not a sequence of
|
||||
patches to that loop. It is adopting an interface and an algorithm that already exist, are published,
|
||||
and are better than what we would arrive at by fixing ours one finding at a time.
|
||||
|
||||
## What the current loop gets wrong, measured
|
||||
|
||||
Round zero on vibe-py, `deepseek-v4-flash`, K = 5, $0.69, 23/30 pair-correct:
|
||||
|
||||
| family | scorable | recall | silence | Youden | noise floor |
|
||||
|---|---|---|---|---|---|
|
||||
| injection | 20 | 0.75 | 0.90 | +0.65 | **11** |
|
||||
| access_control | 3 | 1.00 | 1.00 | +1.00 | 3 |
|
||||
| config_secrets | 3 | 1.00 | 1.00 | +1.00 | 1 |
|
||||
| deserialization | 2 | 1.00 | 1.00 | +1.00 | 0 |
|
||||
| output_encoding | 1 | 1.00 | 1.00 | +1.00 | 1 |
|
||||
| path | 1 | 1.00 | 1.00 | +1.00 | 1 |
|
||||
|
||||
Four defects follow from the loop's shape, not from its bugs:
|
||||
|
||||
1. **The acceptance rule cannot discriminate.** `decide` compares raw deltas: any regression on train or
|
||||
holdout rejects, a tie rejects. The measured floor says 11 of 20 injection pairs disagree with
|
||||
*themselves* across five runs, so a delta over a minibatch of 8 is mostly noise. The spec built
|
||||
`sign_test` and `reliable_change` for exactly this and the rule calls neither.
|
||||
2. **The holdout is an input to the decision.** `decide` reads `holdout_delta`. Req 5.4 keeps the
|
||||
*proposer* blind to the holdout, and then the *selector* reads it every round. Repeated rounds
|
||||
selecting on holdout performance is search-time overfitting of the test set — the same leak class
|
||||
this project has already measured once, one level up.
|
||||
3. **Noise is fought with repetition instead of diversity.** K = 5 spends five rollouts per pair per
|
||||
side estimating variance. The published alternative spends that budget on more candidates and lets a
|
||||
frontier keep whatever wins somewhere.
|
||||
4. **The proposer is told a score, not what went wrong.** `FailureFacts` carries outcomes, run strings
|
||||
and counts. The single largest lever in the published work is textual feedback from the *evaluation
|
||||
trace* — and our evaluator produces exactly the right material (which finding landed on which line,
|
||||
whether it fell inside the labeled span, whether the fixed side leaked, which family was claimed)
|
||||
and then throws all of it away.
|
||||
|
||||
## What others built
|
||||
|
||||
**GEPA — Reflective Prompt Evolution (Agrawal et al., arXiv:2507.19457, ICLR 2026 oral).** Algorithm 1,
|
||||
read from the paper rather than from a summary:
|
||||
|
||||
- Split `D_train` into `D_feedback` and `D_pareto`. Initialise the candidate pool `P = [Φ]` and score
|
||||
the seed on every instance of `D_pareto`, giving a score **matrix** `S[candidate][instance]`.
|
||||
- Each iteration: select a candidate from the Pareto frontier; select a module round-robin; run it on a
|
||||
minibatch drawn from `D_feedback`; collect scores, traces and **textual feedback** via a feedback
|
||||
function `μ_f`; have a reflection LM rewrite that module's prompt; re-run the mutated system on the
|
||||
*same* minibatch.
|
||||
- **Accept if and only if the minibatch average improved.** Only then is the candidate scored on all of
|
||||
`D_pareto` and added to the pool with its parent recorded.
|
||||
- Return the candidate with the best average on `D_pareto`.
|
||||
|
||||
Algorithm 2, the selection: for each instance take the best score any candidate achieved; keep the
|
||||
candidates that achieve it; drop strictly dominated ones; sample a survivor with probability
|
||||
proportional to the number of instances it leads on.
|
||||
|
||||
Three things about that are directly relevant to us. The acceptance gate is **cheap and noisy on
|
||||
purpose** — a single minibatch comparison — because the frontier, not the gate, is what protects
|
||||
against a bad accept: anything that wins on one instance is retained. Selection is **stochastic and
|
||||
diversity-preserving**, which is what stops the search collapsing onto one lineage. And the expensive
|
||||
full evaluation is spent only on candidates that already passed the cheap gate.
|
||||
|
||||
Reported: 93% on MATH with a DSPy `ChainOfThought` program against 67% unoptimised, +10% over MIPROv2
|
||||
and +20% over GRPO with **35× fewer rollouts**; on IFBench, optimal prompts after 678 rollouts against
|
||||
GRPO's 24,000. Sample efficiency is the claim that matters for us, because our rollouts cost money.
|
||||
|
||||
**DSPy's optimizer interface.** `MIPROv2` proposes instruction and demonstration candidates and searches
|
||||
them with Bayesian optimisation (Optuna TPE), scoring on minibatches and running a full validation pass
|
||||
on the incumbent every `minibatch_full_eval_steps` — an explicit two-fidelity design for a noisy metric.
|
||||
`dspy.GEPA` takes `metric`, `reflection_lm`, a budget (`auto` / `max_full_evals` / `max_metric_calls`),
|
||||
`reflection_minibatch_size`, `candidate_selection_strategy` (`pareto` | `current_best`) and
|
||||
`track_stats`, and its metric returns `Prediction(score, feedback)` rather than a float.
|
||||
|
||||
**SIMBA — Stochastic Introspective Mini-Batch Ascent (`dspy.SIMBA`).** Two of its ideas fit our corpus
|
||||
better than GEPA's do. It runs `num_candidates` (6 by default) program variants per example, buckets the
|
||||
trajectories, and ranks examples by **max-to-min score gap** — deliberately spending the budget on the
|
||||
instances where variants disagree most. On our corpus that is not a nuisance, it is the training signal:
|
||||
the 11 injection pairs that disagree with themselves are precisely the pairs where a rule change can flip
|
||||
an outcome, and uniform minibatch sampling would spend most of its budget on the 9 that never move. It
|
||||
also alternates two strategies rather than round-robining blindly — `append_a_demo` (a successful
|
||||
trajectory becomes a demonstration) and `append_a_rule` (the model introspects on the bucket and writes a
|
||||
rule) — which is exactly the split our levers already have between `counterexamples`/`hard_negatives` and
|
||||
`checklist`/`prompt`. Candidates are sampled by softmax over historical scores at temperature 0.2, a
|
||||
cheaper third option beside GEPA's `pareto` and `current_best`.
|
||||
|
||||
**The reflection meta-prompt** (GEPA Appendix C) is short and tells us what the feedback function must
|
||||
produce. It shows the reflector the current instruction, then "inputs, outputs and feedback" for the
|
||||
minibatch, and asks it to infer the task, extract "all niche and domain specific factual information ...
|
||||
as a lot of it may not be available to the assistant in the future", and name any generalizable strategy.
|
||||
That is a specification for `μ_f`, not just a prompt: feedback has to carry the domain facts a detector
|
||||
cannot re-derive — which sink, which span, which side leaked — rather than a verdict.
|
||||
|
||||
**GEPA's merge** is gated strictly: two candidates are crossed only when they share a common ancestor,
|
||||
optimised *disjoint* sets of prompts, are both Pareto-optimal, and both beat the ancestor's aggregate.
|
||||
Worth having once we have per-lever candidates, and worth not having before then.
|
||||
|
||||
**The standalone `gepa` library** (MIT) exposes `gepa.optimize(seed_candidate, trainset, valset,
|
||||
task_lm, reflection_lm, max_metric_calls)` over a `GEPAAdapter` protocol of three methods —
|
||||
`evaluate`, `make_reflective_dataset`, `propose_new_texts` — with a seed candidate that is just a dict
|
||||
of component name to text. That adapter shape is the interface we want whether or not we take the
|
||||
dependency: it is the smallest surface that separates *what a detector is* from *how it is searched*.
|
||||
|
||||
## What this spec proposes
|
||||
|
||||
Restate the detector as an optimisable program and put a published optimiser behind it.
|
||||
|
||||
- **A detector is a candidate**: a dict of named text components (`prompt`, `checklist`,
|
||||
`hard_negatives`, `counterexamples`) plus its frozen non-text settings (tools, budgets). That is
|
||||
already what `FamilyConfig` holds; it becomes the seed candidate.
|
||||
- **The metric returns a score and a reason.** `score_pair_family` gains a feedback string built from
|
||||
what the evaluation already knows. This is the highest-value change in the spec and it is independent
|
||||
of which optimiser sits on top.
|
||||
- **`D_pareto` is carved out of the train split.** The holdout leaves the acceptance rule entirely and
|
||||
is scored once, for reporting, when the search is over. This is a requirements change to Req 9.5 and
|
||||
the most important honesty consequence of the redesign.
|
||||
- **An adapter protocol** (`evaluate`, `make_reflective_dataset`, `propose_new_texts`) with two backends:
|
||||
our own implementation of Algorithms 1 and 2 by default, so the core install keeps
|
||||
`dependencies = []`, and `gepa` behind an optional extra so its improvements arrive for free.
|
||||
- **The Pareto pool replaces the accept/revert tree.** `Archive` already records per-pair winners; it
|
||||
becomes the frontier, and the journal's parent records become the ancestry GEPA needs.
|
||||
|
||||
## Where the noise actually is, and why the architecture may be wrong
|
||||
|
||||
The maintainer's objection, and I think it is correct: the harness treats the detector as a black box
|
||||
emitting a noisy scalar, and everything follows from that — K runs, majority vote, a measured floor, a
|
||||
regression budget, a sign test. That is the stance you take when you cannot see inside the thing you are
|
||||
measuring. We can see inside it.
|
||||
|
||||
The variance is not in the judgement. `temperature: 0` is sent and thinking is disabled, and 11 of 20
|
||||
injection pairs still disagree with themselves. A traced run shows why: over four steps the model chose
|
||||
different files to read and different grep patterns, and reached its conclusion from different context
|
||||
each time. The freedom is in the **trajectory**, and we built statistics to average over it instead of
|
||||
removing it.
|
||||
|
||||
This is exactly what DSPy exists to argue about, and three of its constructs attack the source rather
|
||||
than the symptom.
|
||||
|
||||
**Constraints as predicates, enforced at generation time** (`dspy.Assert`/`Suggest`, arXiv:2312.13382).
|
||||
A constraint is a boolean function of the output; on failure the past output *and the error* are injected
|
||||
into a retry. We already have the predicates — is the reported line inside a parsed function, does the
|
||||
named sink occur at that line, is the claimed family in the taxonomy, did the run cite a tool result —
|
||||
and we apply every one of them **after the fact, at scoring time, to decide whether to count a finding**.
|
||||
Applying them at generation time to decide whether to *accept* one is free, needs no dependency, and is
|
||||
the single highest-value change available. The paper reports near-total intrinsic constraint satisfaction
|
||||
and 5–15% downstream gains.
|
||||
|
||||
**Typed decomposition instead of one free-running loop.** Our detector is a single ReAct loop that decides
|
||||
which files to read, what to grep, when to stop and what to report; every one of those is a variance
|
||||
source. DSPy's thesis is that each step gets a signature — a typed input/output contract — and a little
|
||||
control flow composes them. Ours would be roughly: locate candidate sink sites; trace whether an untrusted
|
||||
value reaches each; check for a guard between source and sink; judge. **Several of those steps need no
|
||||
model at all** — the CST, the entry-point mapper, `flows` and `obligations` already answer them
|
||||
deterministically, and we are currently paying a model to re-derive facts we hold.
|
||||
|
||||
**Best-of-N against a checkable reward, instead of a majority vote** (`dspy.Refine`, `BestOfN`). Both
|
||||
spend K samples. A majority vote assumes the modal answer is right and discards the disagreement; Refine
|
||||
ranks the N attempts by an explicit reward and feeds a hint from the failures into the next attempt. We
|
||||
have checkable rewards for free. The same budget, used as evidence rather than as averaging.
|
||||
|
||||
**Why this matters more for our models, not less.** GEPA and SIMBA are demonstrated with a strong
|
||||
reflection model over a weaker task model, and their gains come from evolving instructions. A cheap model
|
||||
fails on an under-specified task and does well on a narrow typed one. So the cheaper the detector model,
|
||||
the more of the return sits in decomposition and constraints and the less in prompt evolution — which is
|
||||
the opposite of the order this spec originally proposed.
|
||||
|
||||
### The experiment, run (2026-09-06, `benchmarks/measurements/2026-09-06-trajectory-divergence.json`)
|
||||
|
||||
Twelve injection pairs, both sides, K = 5, temperature 0, thinking disabled, recording the tool-call
|
||||
sequence of every run. $0.23.
|
||||
|
||||
**The intended statistic could not be computed. Across 120 runs there was not one repeated trajectory** —
|
||||
4.67 distinct trajectories per vulnerable side and 4.92 per fixed side, out of 5, and 78 distinct tool-*name*
|
||||
sequences ignoring arguments entirely. The detector is not sampling one measurement five times. It is running
|
||||
five different investigations and voting on the results.
|
||||
|
||||
Asked as a gradient instead, the relationship is monotone and clean:
|
||||
|
||||
| trajectory similarity (Jaccard over tool calls) | run pairs | disagreed on the outcome |
|
||||
|---|---|---|
|
||||
| high (≥ 0.75) | 2 | 0 (0.0%) |
|
||||
| mid (0.4–0.75) | 24 | 1 (4.2%) |
|
||||
| low (< 0.4) | 94 | 29 (30.9%) |
|
||||
|
||||
Three more facts from the same run:
|
||||
|
||||
- **The variance is a search problem, and the search is `grep_repo`.** 555 grep calls against 181 `read_file`,
|
||||
71 `entry_points`, 43 `find_refs`, 13 `read_definition`. The model spends its budget guessing patterns to
|
||||
locate a sink — while the CST, the entry-point mapper, `flows` and `obligations` already know where the
|
||||
sinks are.
|
||||
- **All of the instability is on the vulnerable side.** Every one of the twelve pairs produced exactly one
|
||||
distinct finding set on the fixed side. The detector is reliably silent on the fix and unreliably vocal on
|
||||
the bug, so the noise is entirely in detection and none of it in false positives.
|
||||
- **Findings converge far more than trajectories do.** 78 distinct trajectories, but one to three distinct
|
||||
finding sets per pair. Many different investigations reach the same answer; six of twelve pairs are stable
|
||||
despite never once repeating a trajectory.
|
||||
|
||||
**What this does to the noise architecture.** The floor is not measuring the detector's uncertainty about a
|
||||
question. It is measuring how often an unconstrained search happens to reach the sink. A majority vote over
|
||||
five runs is therefore not variance reduction — it is an unweighted ensemble of five arbitrary search
|
||||
procedures — and a regression budget derived from it is a budget for the search's luck. K runs, the floor,
|
||||
the budget and the sign test are all statistical treatment for a structural problem, and the 0% and 4.2%
|
||||
rows of that table say what happens when the structure is fixed instead.
|
||||
|
||||
## Reflection on the approved specification
|
||||
|
||||
Read against that measurement, `learning-harness` divides cleanly into a part that stands and a part that
|
||||
was answering the wrong question.
|
||||
|
||||
**Stands, and is worth more now than before.** The closed taxonomy; class-aware pair scoring; the split
|
||||
enforced by construction; the unscorable vocabulary and its denominators; verifier-gated reporting; the
|
||||
corpus repairs; the publication machinery; the per-pair archive. None of it depends on the noise framing,
|
||||
and all of it is what made this experiment possible to run and cost $0.23. A measurement layer that can
|
||||
answer "is our own instability structural?" in twenty minutes is the thing that was missing before.
|
||||
|
||||
**Was answering the wrong question.**
|
||||
|
||||
- **Req 3.5, K ≥ 3 runs per pair.** The premise is variance reduction over a repeated measurement. There is
|
||||
no repeated measurement: 120 runs, 120 trajectories. K runs is an *ensemble of distinct search
|
||||
procedures*, and a majority vote over it is an unweighted ensemble, not an estimate. It should either be
|
||||
renamed and published as an ensemble — which changes what every family number means — or dropped once the
|
||||
search is constrained.
|
||||
- **Req 8.2, the noise floor as the regression budget.** The floor measures how often an unconstrained
|
||||
search happens to reach the sink. Deriving a regression budget from it means the worse the search wanders,
|
||||
the more collateral damage a round is licensed to do. That is backwards, and it is why injection's budget
|
||||
is 11 of 20.
|
||||
- **Req 9.5, the acceptance rule.** Already flagged for reading the holdout every round. Now also: it
|
||||
compares deltas between two ensemble votes, and calls the difference an improvement.
|
||||
- **The sign test and `reliable_change`.** Added this session in good faith; they test whether an ensemble's
|
||||
vote moved, which is not the question either.
|
||||
|
||||
**What the data adds that neither spec anticipated.**
|
||||
|
||||
- **The instability is entirely one-sided.** All twelve pairs produced exactly one finding set on the fixed
|
||||
side. Specificity is solid and recall is the noisy half, so the leak ceiling and the false-positive
|
||||
machinery are sound and it is the recall numbers that carry the caveat.
|
||||
- **`grep_repo` is the variance engine** — 555 calls against 181 `read_file` and 71 `entry_points`. The
|
||||
single highest-leverage change in either spec is not a prompt, a lever or an optimiser: **stop asking the
|
||||
model to locate the sink.** The CST, the entry-point mapper, `flows` and `obligations` already know. Give
|
||||
the detector the candidate sites and ask it the question only a model can answer — does untrusted input
|
||||
reach this one, and is there a guard.
|
||||
- **The loop is partly self-correcting**, so this should be a large win rather than a marginal one: 78
|
||||
distinct trajectories collapse to one to three finding sets per pair, and half the pairs are already
|
||||
stable despite never repeating an investigation.
|
||||
|
||||
**Limits of the experiment, stated so the conclusion is not over-read.** Twelve pairs, one family, one
|
||||
slice, one model. The high-similarity bucket holds only two run pairs, so its 0% is decoration; the
|
||||
defensible number is the mid bucket's 4.2% over 24 run pairs against 30.9% over 94. And the direction of
|
||||
causation is inferred, not established — similar trajectories may be a symptom of easy pairs rather than a
|
||||
cause of agreement. The way to settle that is the constrained detector itself: fix the investigation, and
|
||||
if the floor does not collapse, this reading was wrong.
|
||||
|
||||
## The constraint that outranks the algorithm
|
||||
|
||||
Both algorithms assume a dataset. GEPA splits `D_train` into `D_feedback` and `D_pareto`; SIMBA's default
|
||||
batch size is 32. **Our injection family has 12 train teachers** — fewer than one SIMBA batch, and after a
|
||||
GEPA split roughly six instances on each side. Every other family has one to three.
|
||||
|
||||
So the honest statement of what this spec can and cannot buy: GEPA and SIMBA are sample-efficient in
|
||||
*rollouts*, not in *instances*. They will not shortcut a corpus of twelve. The optimiser is worth building
|
||||
because the current loop is the wrong shape and will stay wrong at any corpus size, but the binding
|
||||
constraint on results is the train split, and it will remain so after this spec ships. Two things move it,
|
||||
neither of which is an algorithm: reviewing agent-vfc rows above tier `title` (29 rows, currently unable to
|
||||
teach anything), and harvesting more pairs per family.
|
||||
|
||||
## Scope
|
||||
|
||||
In: the search over a family's text components, the feedback function, the candidate pool and its
|
||||
selection, the budget accounting, and the artifacts that make a run reproducible.
|
||||
|
||||
Out, and deliberately: the taxonomy, the classifier, the pair scorer's *outcome* rules, the verifier
|
||||
boundary, the write roots, redaction, the three gates. Those are the measurement, and a search whose
|
||||
objective is not trustworthy is worse than no search. Nothing here relaxes them — in particular the
|
||||
proposer stays blind to the holdout, verifier code and gold labels stay outside every write root, and
|
||||
no pattern or policy is written by a model.
|
||||
|
||||
## Decisions for the maintainer
|
||||
|
||||
0. **Is the optimiser even the next thing to build?** Proposed no, and this reverses the order above.
|
||||
Constraints at generation time, typed decomposition and best-of-N attack the variance at its source; an
|
||||
optimiser only tunes text around a program whose freedom is the problem. Proposed sequence: measure where
|
||||
the variance is, constrain generation, decompose the loop, then optimise — and at a trainset of twelve the
|
||||
optimiser that fits is `BootstrapFewShot` (collect traces that scored well, install them as demonstrations
|
||||
in a lever we already have) rather than an evolutionary search with nothing to search over.
|
||||
|
||||
0b. **Adopt DSPy itself?** Proposed no. Our detector is not a DSPy program — it is an agentic tool loop over
|
||||
a repository with curated tools, redaction on every prompt, path clamping, a "no findings unless a tool
|
||||
was called" rule and a final-answer turn, several of which are security properties rather than
|
||||
conveniences. Handing that loop to `dspy.ReAct` and the calls to litellm would put them behind someone
|
||||
else's abstraction, and DSPy could only ever be an optional extra under `dependencies = []`, which means
|
||||
two code paths for the thing that *is* the product. What DSPy would genuinely buy — signatures, adapters,
|
||||
multi-module composition — we barely need at one module per family. The optimisers are available without
|
||||
the framework: `gepa` ships standalone under MIT, and SIMBA's two ideas are a hundred lines of logic.
|
||||
Proposed instead: take the interface shape and the algorithms, not the framework.
|
||||
|
||||
1. **Reimplement Algorithms 1 and 2, take `gepa` as an extra, or both?** Both is proposed: ours by
|
||||
default (zero-dependency, and the security properties are ours to own), `gepa` behind an extra for
|
||||
comparison. The cost is one adapter and one conformance test that both backends must pass.
|
||||
2. **Drop the holdout from the acceptance rule?** Proposed yes, and it amends approved Req 9.5. The
|
||||
holdout becomes what its name says.
|
||||
3. **Sample instances by disagreement (SIMBA) or uniformly (GEPA)?** Proposed SIMBA's ranking. Our measured
|
||||
floor makes it the better fit, and it turns the noise floor from an obstacle into an instance sampler.
|
||||
|
||||
4. **K = 1 inside the search, K ≥ 3 only for published numbers?** Proposed yes. That is roughly a 5×
|
||||
budget reclaim spent on more candidates, which is where the sample efficiency comes from.
|
||||
5. **Does the reflection model differ from the detector model?** The published work uses a stronger
|
||||
reflector than the task model. We have `deepseek-v4-pro` configured as the judge; proposed to reuse it.
|
||||
6. **Is `access_control` still the family we care most about?** It has one train teacher. Until agent-vfc
|
||||
rows are reviewed above tier `title`, the optimiser cannot work on it, whatever its algorithm.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does `gepa` run against an OpenAI-shaped base URL that is not OpenAI (our DeepSeek endpoint), and what
|
||||
does it pull in? Both must be checked before the extra is offered, under the no-undeclared-dependency rule.
|
||||
- Our "module" granularity: is a module a family, or a lever within a family? Round-robin over levers is
|
||||
the closer analogue, but the levers are not independent — a checklist edit and a prompt edit interact.
|
||||
- The feedback function's failure mode is a reflection model that writes plausible prose about a trace it
|
||||
misread. What check makes a feedback string answerable rather than persuasive?
|
||||
- GEPA's acceptance is a single noisy comparison. Our floor says a single comparison is close to a coin
|
||||
flip on this corpus. Does the frontier really absorb that at n ≈ 20 instances, or is our corpus simply
|
||||
too small for the algorithm's assumptions?
|
||||
|
||||
Sources: [GEPA (arXiv:2507.19457)](https://arxiv.org/abs/2507.19457) ·
|
||||
[DSPy GEPA overview](https://dspy.ai/api/optimizers/GEPA/overview/) ·
|
||||
[DSPy MIPROv2](https://dspy.ai/api/optimizers/MIPROv2/) ·
|
||||
[gepa-ai/gepa](https://github.com/gepa-ai/gepa) ·
|
||||
[DSPy SIMBA](https://dspy.ai/api/optimizers/SIMBA/) ·
|
||||
[dspy/teleprompt/simba.py](https://github.com/stanfordnlp/dspy/blob/main/dspy/teleprompt/simba.py) ·
|
||||
[DSPy Assertions (arXiv:2312.13382)](https://arxiv.org/pdf/2312.13382) ·
|
||||
[dspy.Refine](https://dspy.ai/api/modules/Refine/) ·
|
||||
[DSPy (arXiv:2310.03714)](https://arxiv.org/pdf/2310.03714)
|
||||
@@ -1,294 +0,0 @@
|
||||
# Design Document: constrained-detector
|
||||
|
||||
## Overview
|
||||
|
||||
The detector stops searching. A deterministic pass enumerates the candidate sites inside a labeled region from the
|
||||
semantic IR; the model answers one bounded, typed question per candidate with no tools and no choice of what to look
|
||||
at; the predicates that decide whether a finding counts are applied while it is being made, with one retry. The
|
||||
unconstrained tool hunter stays in place as the generalist and as the baseline this is measured against.
|
||||
|
||||
Two measurements shape every decision below and neither was a guess. **120 runs produced 120 distinct trajectories**,
|
||||
so the variance is the loop's freedom rather than the model's judgement. And the candidate generator was measured
|
||||
before anything was built: the **pattern ruleset caps recall at 12.1%**, below the 64.9% the unconstrained loop
|
||||
already reaches, while the **semantic IR caps it at 96.6%** with a median of ten sites per labeled function.
|
||||
|
||||
### Goals
|
||||
|
||||
- Two runs of the same detector on the same input make an identical sequence of model calls.
|
||||
- The limit on what the detector can find is a published number, not an assumption.
|
||||
- Real-project slices decide whether a change is kept; hand-written teaching code cannot justify one alone.
|
||||
- The K-run and noise-floor machinery is re-measured and deleted if a constrained detector does not need it.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Prompt optimisation, reflective evolution, any optimiser. Deferred to a later spec, on this feature's evidence.
|
||||
- Changing the taxonomy, the classifier, the scorer's outcome rules, the split, the verifier boundary or the gates.
|
||||
- Adopting DSPy as a dependency. The interface shape is borrowed; the framework is not.
|
||||
|
||||
## Boundary Commitments
|
||||
|
||||
### This Spec Owns
|
||||
|
||||
- `learning/candidates.py` — what a candidate is, how the IR enumerates one per family shape, and the ceiling report.
|
||||
- `learning/judgments.py` — the typed answer, and the toolless call that produces it.
|
||||
- `learning/predicates.py` — the checks that constrain a claim at generation time and score it afterwards, defined once.
|
||||
- `learning/slices.py` — deciding versus development slices, the overfitting gap, and the cross-slice rejection rule.
|
||||
- `run_constrained_detector` in `learning/detectors.py`, beside the existing `run_family_detector`.
|
||||
- `ousast learning candidates`, and a detector selector on `score` and `baseline`.
|
||||
|
||||
### Out of Boundary
|
||||
|
||||
- `learning/families.py`, `classify.py`, `split.py`, `verifiers.py`, `canaries.py`, `journal.py`, `acceptance.py`,
|
||||
`rounds.py`, `publish.py` keep their contracts. `scoring.py` is touched only to import predicates it already
|
||||
implements inline, and a test asserts its behaviour is unchanged.
|
||||
- `tool_hunter.run_tool_hunter` keeps its contract and its callers. It is not removed while it scores higher on any
|
||||
deciding slice.
|
||||
- The pair corpus, its labels, tiers and splits. The three gates.
|
||||
|
||||
### Allowed Dependencies
|
||||
|
||||
- Core: standard library only. `dependencies = []` is unchanged.
|
||||
- Optional: the semantic extra for the IR. Without it, every family reports `unsupported_language` and scores nothing.
|
||||
- The chat endpoint already resolved by `learning/endpoint.py`. No new provider, no new key.
|
||||
|
||||
### Revalidation Triggers
|
||||
|
||||
- The candidate ceiling on a deciding slice falling below the unconstrained loop's detection rate on that slice.
|
||||
- The re-measured trajectory count per pair exceeding one.
|
||||
- Any change to `scoring.py`'s outcome rules, which would invalidate the baseline this is compared against.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Existing Architecture Analysis
|
||||
|
||||
`run_family_detector` composes `run_tool_hunter` with a family's prompt, checklist, curated tools and context files.
|
||||
The hunter loop is a ReAct loop: the model calls tools until it stops, then emits findings, and a final turn asks for
|
||||
JSON when it ended in prose. `_overlay_scan` already parses both sides of a pair and returns function ranges;
|
||||
`semantic/ir.py` already exposes `FunctionIR` with `calls: tuple[CallSite, ...]` and `binds: tuple[Bind, ...]`,
|
||||
each carrying argument texts, the names they mention and whether they are constant. `hunter_tools.obligations` and
|
||||
`flows` already answer the guard and reachability questions deterministically. Nothing new has to be parsed.
|
||||
|
||||
### Architecture Pattern & Boundary Map
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
R[Region: path, function, admitted families] --> E[candidates.enumerate_candidates]
|
||||
IR[(semantic IR: calls, binds)] --> E
|
||||
OB[(obligations, flows, entry_points)] --> E
|
||||
E -->|"Candidate[] sorted, bounded"| J[judgments.judge_candidates]
|
||||
J -->|"one typed call per batch, no tools"| M[[chat endpoint]]
|
||||
M --> P[predicates.check]
|
||||
P -->|violation| J
|
||||
P -->|clean| F[StaticFinding: family, detector, verifier tags]
|
||||
F --> S[learning.scoring.score_pair_family]
|
||||
P -.->|same definition| S
|
||||
E --> C[CandidateReport: ceiling per family per slice]
|
||||
```
|
||||
|
||||
The dotted edge is the point of Requirement 4: one definition of the predicates, used to constrain and to score.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Concern | Choice | Why |
|
||||
|---|---|---|
|
||||
| Candidate enumeration | `semantic/ir.py` `FunctionIR.calls` and `.binds` | 96.6% ceiling measured; the ruleset's 12.1% is below the current baseline |
|
||||
| Guards in scope | `hunter_tools.obligations`, `mapping.analyze_entry_points` | already deterministic, already used by the obligations checker |
|
||||
| Typed answer | `json_object` on the DeepSeek adapter, one array per batch | the provider has no schema mode; the predicates are the schema |
|
||||
| Retry on violation | one bounded retry with the violation injected | DSPy Assertions' backtracking, without the framework |
|
||||
| Determinism | candidates sorted, batches fixed, no tools | Req 2.4; the model's text may vary, the call sequence may not |
|
||||
|
||||
## File Structure Plan
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/openultrasast/learning/
|
||||
├── candidates.py # NEW Candidate, CandidateKind, enumerate_candidates, ceiling, CandidateReport
|
||||
├── judgments.py # NEW Judgment, judge_candidates, batching, the typed prompt
|
||||
├── predicates.py # NEW PREDICATES, check_claim, violation reasons
|
||||
├── slices.py # NEW DECIDING_SLICES, DEVELOPMENT_SLICES, overfitting_gap, cross_slice_verdict
|
||||
├── detectors.py # MOD run_constrained_detector beside run_family_detector
|
||||
└── scoring.py # MOD imports predicates; outcome rules unchanged, asserted by the recorded baseline
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `src/openultrasast/learning/detectors.py` — `run_constrained_detector(root, region, config, *, client, model,
|
||||
taxonomy, ranges)`; `FamilyConfig` gains nothing, because a constrained detector is configured by the same
|
||||
prompt/checklist/limits and reads no tools.
|
||||
- `src/openultrasast/learning/scoring.py` — `_hits`, `_others` and `_fabricated` delegate to `predicates`; the
|
||||
family metrics gain nothing. A test asserts the recorded baseline numbers are reproduced exactly.
|
||||
- `src/openultrasast/learning/publish.py` — the overfitting gap and the per-slice table (Req 3.1, 3.4).
|
||||
- `src/openultrasast/config.py` — `[learning] detector = "hunter" | "constrained"`, default `hunter` until the
|
||||
comparison says otherwise.
|
||||
- `src/openultrasast/cli.py` — `learning candidates`; `--detector` on `score` and `baseline`.
|
||||
- `pyproject.toml` — unchanged.
|
||||
|
||||
## System Flows
|
||||
|
||||
### One region, constrained
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant D as run_constrained_detector
|
||||
participant E as enumerate_candidates
|
||||
participant J as judge_candidates
|
||||
participant P as predicates
|
||||
D->>E: root, region, family, ranges
|
||||
E-->>D: Candidate[] (sorted by path, line, name; bounded)
|
||||
alt no candidates
|
||||
D-->>D: record no_candidate, return []
|
||||
end
|
||||
loop each fixed-size batch
|
||||
D->>J: batch, config, client
|
||||
J-->>D: Judgment[] (typed)
|
||||
D->>P: judgment, candidate, ranges, taxonomy
|
||||
alt violation
|
||||
D->>J: retry once, violation injected
|
||||
J-->>D: Judgment[]
|
||||
D->>P: re-check
|
||||
end
|
||||
P-->>D: clean claims or a recorded discard
|
||||
end
|
||||
D-->>D: StaticFinding[] tagged family, detector, verifier
|
||||
```
|
||||
|
||||
## Requirements Traceability
|
||||
|
||||
| Criteria | Delivered by |
|
||||
|---|---|
|
||||
| 1.1, 1.2 | `candidates.enumerate_candidates`, `CandidateKind` per family shape |
|
||||
| 1.3 | `candidates.ceiling`, `ousast learning candidates`, `benchmarks/measurements/*-candidate-ceiling.json` |
|
||||
| 1.4 | `enumerate_candidates` returns `Unsupported` when `parse_file(...).parse_ok` is false |
|
||||
| 1.5 | `no_candidate` joins the unscorable vocabulary in `scoring.unscorable_reason` |
|
||||
| 1.6 | `MAX_CANDIDATES_PER_CALL`, `CandidateReport.discarded` |
|
||||
| 2.1, 2.2, 2.3 | `judgments.judge_candidates`, `Judgment`, `_typed_prompt`; findings built from `Judgment` only |
|
||||
| 2.4 | `test_two_runs_make_an_identical_sequence_of_calls` over a recording client |
|
||||
| 2.5 | `StaticFinding.tags` gains `candidate:<path>:<line>:<name>` |
|
||||
| 3.1, 3.4 | `slices.overfitting_gap`, `publish._slice_table` |
|
||||
| 3.2, 3.3 | `slices.DECIDING_SLICES`, `slices.cross_slice_verdict` |
|
||||
| 3.5 | `learning candidates --slice` over every slice; the artifact carries per-slice ceilings |
|
||||
| 3.6 | `slices.teachable(slice)` and a note emitted with every published number |
|
||||
| 3.7 | `NON_GATING_FAMILIES` unchanged; `publish` reports memory separately, never inside a web aggregate |
|
||||
| 4.1, 4.2, 4.3 | `predicates.PREDICATES`, `check_claim`, one retry in `run_constrained_detector`, `discarded` tally |
|
||||
| 4.4 | `scoring._hits`/`_fabricated` call `predicates`; the baseline-reproduction test pins no drift |
|
||||
| 5.1, 5.2 | the trajectory script and `run_baseline` re-run against the constrained detector |
|
||||
| 5.3, 5.4 | the published floor names the `learning-harness` requirements it would amend; K is labelled |
|
||||
| 6.1 | `score_pair_family` unchanged; the constrained detector only produces findings |
|
||||
| 6.2, 6.3 | the comparison artifact, per slice, against 64.9% / 12.3% / 52.6% and $0.030 / $0.136 |
|
||||
| 6.4, 6.5 | the comparison's headline rule; `[learning] detector` keeps `hunter` selectable |
|
||||
| 7.1 | `resolve_chat_endpoint` unchanged; `learning_endpoint_unavailable` recorded |
|
||||
| 7.2 | the client's `cost_usd` meter, already wired |
|
||||
| 7.3 | the committed gate baseline test |
|
||||
| 7.4 | `has_semantic_extra` guard; `unsupported_language` without it |
|
||||
| 7.5 | `redact_secrets` on every prompt and persisted trajectory |
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### candidates.py
|
||||
|
||||
```python
|
||||
CandidateKind = Literal["call", "bind", "operation", "config"]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Candidate:
|
||||
path: str
|
||||
line: int
|
||||
kind: CandidateKind
|
||||
name: str # the callee, the bound name, or the operation
|
||||
text: str # the site as written, clamped
|
||||
arg_texts: tuple[str, ...]
|
||||
arg_names: tuple[tuple[str, ...], ...]
|
||||
binding_chain: tuple[str, ...] # what the IR resolved for the arguments, nearest first
|
||||
guards: tuple[str, ...] # decorators and in-body guards in scope, from obligations
|
||||
function: str
|
||||
|
||||
@property
|
||||
def id(self) -> str: ... # f"{path}:{line}:{name}", the tag a finding carries
|
||||
|
||||
FAMILY_SHAPE: dict[str, tuple[CandidateKind, ...]] = {
|
||||
"injection": ("call",), "path": ("call",), "deserialization": ("call",),
|
||||
"untrusted_destination": ("call",), "prototype": ("call", "bind"),
|
||||
"access_control": ("operation",), "config_secrets": ("bind", "config"),
|
||||
"output_encoding": ("call",), "memory": ("call",), "unknown": ("call", "bind"),
|
||||
}
|
||||
MAX_CANDIDATES_PER_CALL = 12 # p90 of the measured distribution is 22; two batches covers all but the tail
|
||||
|
||||
def enumerate_candidates(root, region, family, *, taxonomy, limit=MAX_CANDIDATES_PER_CALL) -> CandidateSet
|
||||
def ceiling(cases, *, taxonomy) -> CandidateReport # offline, no model
|
||||
```
|
||||
|
||||
`CandidateSet` carries `candidates`, `discarded`, and `reason` (`""`, `unsupported_language`, `no_candidate`).
|
||||
Ordering is `(path, line, name)`, which makes the batches and therefore the call sequence deterministic.
|
||||
|
||||
### judgments.py
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Judgment:
|
||||
candidate_id: str
|
||||
verdict: bool
|
||||
source: str # the expression carrying untrusted input, or ""
|
||||
guard: str # the guard found, or "" meaning none
|
||||
family: str
|
||||
rationale: str
|
||||
|
||||
def judge_candidates(candidates, config, *, client, model, taxonomy, violation=None) -> tuple[Judgment, ...]
|
||||
```
|
||||
|
||||
One call per batch, `tools=[]`, `json_object=True`. The prompt is the family's prompt and checklist plus the batch
|
||||
rendered as data; `violation` is the single retry's injected text. Answers that name a candidate id not in the batch
|
||||
are dropped before the predicates run, because a fabricated id is not a claim about anything.
|
||||
|
||||
### predicates.py
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Violation:
|
||||
predicate: str
|
||||
detail: str
|
||||
|
||||
def check_claim(judgment, candidate, *, ranges, taxonomy) -> Violation | None
|
||||
def inside_labeled_function(finding, spans) -> bool # used by scoring._hits
|
||||
def is_known_family(tag, taxonomy) -> bool # used by scoring._fabricated
|
||||
```
|
||||
|
||||
Named predicates: `line_in_parsed_function`, `sink_at_line`, `family_in_taxonomy`, `source_in_function`. The last
|
||||
two of the three helpers are the ones `scoring.py` already implements inline; moving them here is a refactor whose
|
||||
only observable effect must be none, which the recorded baseline pins.
|
||||
|
||||
### slices.py
|
||||
|
||||
```python
|
||||
DECIDING_SLICES = ("agent-vfc", "vfc-js", "vfc", "github") # harvested from real projects
|
||||
DEVELOPMENT_SLICES = ("vibe-py",) # hand-written; cannot justify a change alone
|
||||
|
||||
def overfitting_gap(per_slice: Mapping[str, FamilyMetrics]) -> float
|
||||
def cross_slice_verdict(before, after) -> Verdict # rejects an improvement that regresses a deciding slice, by name
|
||||
def teachable(slice_name: str, cases) -> bool # False while every row of a slice is below a gating tier
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Condition | Behaviour | Recorded as |
|
||||
|---|---|---|
|
||||
| IR cannot parse the side | no candidates, no model call | `unsupported_language` |
|
||||
| Labeled function has no call or bind | no model call | `no_candidate` |
|
||||
| Region exceeds the batch bound | judge the first `limit`, count the rest | `CandidateReport.discarded` |
|
||||
| Predicate fails twice | claim discarded | `predicates.discarded[<name>]`, published |
|
||||
| Model names a candidate id not in the batch | answer dropped before predicates | `fabricated_candidate` tally |
|
||||
| Endpoint unavailable | score nothing | `learning_endpoint_unavailable` |
|
||||
| Cost cap crossed | stop, keep what was measured | `cost_cap_exceeded` |
|
||||
|
||||
## Migration and Phases
|
||||
|
||||
1. **Candidates and the ceiling, offline.** `candidates.py`, `learning candidates`, the per-slice ceiling artifact
|
||||
including the real-world slices (Req 3.5). No model call, so the phase is free and its result can still cancel
|
||||
the design — if the ceiling on a deciding slice is below that slice's current detection rate, stop here.
|
||||
2. **Predicates extracted.** `predicates.py`, `scoring.py` delegating, the baseline-reproduction test.
|
||||
3. **Judgment and the constrained detector.** `judgments.py`, `run_constrained_detector`, the determinism test.
|
||||
4. **Slices and reporting.** `slices.py`, the overfitting gap, `publish` per slice.
|
||||
5. **The comparison.** Constrained against unconstrained on every slice, the re-measured trajectory count and noise
|
||||
floor, and the named `learning-harness` amendments if the floor collapses.
|
||||
|
||||
Phase 1 is deliberately able to kill the design before any money is spent on it.
|
||||
@@ -1,172 +0,0 @@
|
||||
# Requirements Document
|
||||
|
||||
> **Status 2026-10-02:** superseded by `model-grounded-detection` (spec.json `superseded_by`). 4 of 20 tasks were done and the tasks were never approved; none will be. Kept as decision history; nothing in src, tests or CI points here.
|
||||
|
||||
## Introduction
|
||||
|
||||
The detector's instability is structural, not statistical. Measured on 2026-09-06 over twelve injection pairs at
|
||||
temperature 0 with thinking disabled, **120 runs produced 120 distinct tool-call trajectories** — not one repeat —
|
||||
and outcome disagreement tracks trajectory divergence (30.9% at low similarity against 4.2% at mid). The model
|
||||
spends its budget searching: 555 `grep_repo` calls against 71 `entry_points`, guessing where sinks are while the
|
||||
semantic IR already knows. So `learning-harness`'s K runs are an ensemble of distinct search procedures rather than
|
||||
repeated measurements, and the noise floor it derives a regression budget from measures how often an unconstrained
|
||||
search happens to reach the sink.
|
||||
|
||||
This feature removes the freedom instead of averaging over it. A deterministic pass enumerates the candidate sites
|
||||
inside the labeled region from the IR; the model is asked one bounded, typed question per candidate with no tools
|
||||
and no choice of what to look at; and the predicates the scorer already applies after the fact are enforced at
|
||||
generation time with a bounded retry. The candidate generator was measured before anything was built: the pattern
|
||||
ruleset caps recall at 12.1%, below the 64.9% the unconstrained loop already reaches, while the semantic IR caps it
|
||||
at 96.6% with a median of ten sites per labeled function.
|
||||
|
||||
Baseline to beat, `deepseek-v4-flash`, K = 5, 57 scorable web pairs: **detection 64.9%, leak 12.3%, pair-correct
|
||||
52.6%** — and 83.3% detection on hand-written vibe-py against 44.4% on agent-written agent-vfc, which is the gap
|
||||
this feature must close rather than widen. Context and measurements: `brief.md`,
|
||||
`benchmarks/measurements/2026-09-06-trajectory-divergence.json`, `2026-09-06-candidate-ceiling.json`.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: candidate enumeration from the semantic IR, per family shape; the typed, toolless judgment step;
|
||||
constraint predicates enforced at generation time with a bounded retry; the evidence that decides whether the
|
||||
K-run and noise-floor machinery still earns its place; per-slice reporting that keeps real-world code in front.
|
||||
- **Out of scope**: the family taxonomy, the classifier, the pair scorer's outcome rules, the split, the verifier
|
||||
boundary and write roots, redaction, the three gates, and the publication machinery — all of which stand and are
|
||||
what made this feature's measurements possible. Also out: prompt optimisation, reflective evolution and any
|
||||
optimiser (deferred to a later spec, on this feature's evidence); finding bugs in JavaScript interpreters, WebGL
|
||||
renderers or browser engines; memory-safety fuzzing for C and C++; fine-tuning model weights; adopting DSPy as a
|
||||
dependency.
|
||||
- **Adjacent expectations**: `learning-harness` keeps owning the taxonomy, the scorer, the split and publication;
|
||||
its Req 3.5 (K runs), 8.2 (noise floor) and 9.5 (acceptance) are amended by this feature only on evidence it
|
||||
produces, never on argument. The pair corpus keeps owning labels, tiers and splits. The unconstrained tool hunter
|
||||
stays as the generalist and as the baseline this feature is measured against.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Candidates come from the semantic IR, and the enumerator's ceiling is published
|
||||
|
||||
**Objective:** As a maintainer, I want the sites a detector judges to be enumerated deterministically, so that the
|
||||
model stops searching and the limit of what it can possibly find is a number rather than a hope.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall enumerate candidate sites for a region from the semantic IR — call sites and
|
||||
bindings — and shall not use the pattern ruleset as its candidate generator.
|
||||
2. The Constrained Detector shall shape candidates by family: a call site for injection, path, deserialization,
|
||||
untrusted destination and prototype; an operation on a protected resource with the guards in scope for access
|
||||
control; a configuration assignment or literal for permissive configuration and secrets.
|
||||
3. The Constrained Detector shall publish, per family and per slice, the fraction of labeled functions for which at
|
||||
least one candidate was enumerated, and shall name that fraction as the ceiling on its recall.
|
||||
4. If the semantic IR cannot parse a side, then the Constrained Detector shall record `unsupported_language` and
|
||||
shall score nothing for that pair rather than falling back to a search.
|
||||
5. If a labeled function yields no candidate site, then the Constrained Detector shall record `no_candidate` and
|
||||
report that pair as unscorable with that reason rather than as a miss.
|
||||
6. While enumerating, the Constrained Detector shall bound the candidates handed to one judgment call and shall
|
||||
record the bound and the number discarded when a region exceeds it.
|
||||
|
||||
### Requirement 2: One bounded, typed question per candidate, with no search
|
||||
|
||||
**Objective:** As a maintainer, I want the model to answer only what a model is needed for, so that two runs of the
|
||||
same detector on the same input take the same path.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall call the model with the candidate site, its enclosing function, the binding chain
|
||||
the IR resolved for its arguments, and the guards in scope, and shall offer it no tools.
|
||||
2. The Constrained Detector shall require a typed answer per candidate: a verdict, the expression it believes
|
||||
carries untrusted input, the guard it found or the statement that there is none, and the family.
|
||||
3. The Constrained Detector shall derive every finding from a typed answer and shall never parse a finding out of
|
||||
free prose.
|
||||
4. When the detector is run twice on the same input with the same configuration, the sequence of model calls shall
|
||||
be identical, and a test shall assert this.
|
||||
5. The Constrained Detector shall record, for every claim, the candidate that produced it, so that a claim is
|
||||
traceable to the site it was asked about.
|
||||
|
||||
### Requirement 3: Real-world code decides, and the gap to teaching code is published
|
||||
|
||||
**Objective:** As a maintainer, I want changes judged on code from real projects rather than on hand-written
|
||||
teaching examples, so that the tool is not tuned for cases nobody has.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall report every number per slice and shall never publish an aggregate across slices
|
||||
without the per-slice numbers beside it.
|
||||
2. The Constrained Detector shall designate the slices harvested from real projects — agent-vfc, vfc-js, vfc and
|
||||
github — as the deciding slices, and the hand-written vibe-py slice as a development slice that cannot on its
|
||||
own justify accepting a change.
|
||||
3. If a change improves the development slice and regresses any deciding slice, then the Constrained Detector shall
|
||||
reject it, and the rejection shall name the slice that regressed.
|
||||
4. The Constrained Detector shall publish the **overfitting gap** — development-slice detection minus deciding-slice
|
||||
detection — beside every headline number, and shall state that the baseline gap is 83.3% against 44.4%.
|
||||
5. The Constrained Detector shall measure and publish the candidate ceiling separately for the real-world slices,
|
||||
because the functions there are larger than the teaching corpus's and the enumerator's behaviour on them is not
|
||||
implied by its behaviour on vibe-py.
|
||||
6. While the deciding slices carry no pair a learning path may teach from, the Constrained Detector shall state that
|
||||
with every number it publishes rather than presenting the development slice as if it were the corpus.
|
||||
7. The Constrained Detector shall keep memory safety measured and non-gating, and shall not let the 176 vfc rows
|
||||
dominate an aggregate that a reader would take as the tool's web-taxonomy performance.
|
||||
|
||||
### Requirement 4: The predicates that score a finding also constrain it
|
||||
|
||||
**Objective:** As a maintainer, I want the checks that decide whether a finding counts applied while it is being
|
||||
made, so that a violation is corrected rather than silently discarded.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall express as predicates the checks the scorer already applies: the reported line
|
||||
falls inside a parsed function, the named sink occurs at that line, the claimed family is in the taxonomy, and
|
||||
the claimed source expression appears in the enclosing function.
|
||||
2. When a predicate fails, the Constrained Detector shall retry the judgment once with the failed answer and the
|
||||
predicate that rejected it, and shall not retry more than once per candidate.
|
||||
3. If a retry also fails a predicate, then the Constrained Detector shall discard the claim and record the predicate
|
||||
that rejected it, and shall count that discard in a published tally.
|
||||
4. The Constrained Detector shall apply the same predicates at generation time and at scoring time from one
|
||||
definition, so the two cannot drift apart.
|
||||
|
||||
### Requirement 5: The noise machinery is re-measured, and kept only if it earns its place
|
||||
|
||||
**Objective:** As a maintainer, I want the K runs, the noise floor and the regression budget re-measured against a
|
||||
constrained detector, so that machinery built for an unconstrained one is deleted rather than maintained.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall re-run the trajectory measurement against itself and shall publish the resulting
|
||||
distinct-trajectory count per pair.
|
||||
2. The Constrained Detector shall re-measure the per-family noise floor at the same K as the baseline and shall
|
||||
publish it beside the baseline's floor.
|
||||
3. If the re-measured floor is zero for a family, then the Constrained Detector shall report that family's K-run
|
||||
requirement and its regression budget as unnecessary and shall name the `learning-harness` requirements that
|
||||
would be amended.
|
||||
4. The Constrained Detector shall state, wherever a K-run number is published, whether the runs were repeated
|
||||
measurements or an ensemble of distinct procedures.
|
||||
|
||||
### Requirement 6: The constrained detector is measured against the unconstrained one, on the same corpus
|
||||
|
||||
**Objective:** As a maintainer, I want one comparison that says whether this was worth building.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall be scored by the existing class-aware pair scorer, with the same denominators,
|
||||
the same unscorable vocabulary and the same split, and shall introduce no scoring rule of its own.
|
||||
2. The Constrained Detector shall publish detection, leak and pair-correct rates against the recorded baseline of
|
||||
64.9%, 12.3% and 52.6%, per slice and in total.
|
||||
3. The Constrained Detector shall publish its cost per correct pair against the baseline's $0.030 on vibe-py and
|
||||
$0.136 on agent-vfc.
|
||||
4. If the constrained detector's detection rate is below the unconstrained loop's on any deciding slice, then that
|
||||
shall be published as the headline of the comparison rather than as a footnote.
|
||||
5. The Constrained Detector shall keep the unconstrained tool hunter available and shall not remove it while it
|
||||
scores higher on any deciding slice.
|
||||
|
||||
### Requirement 7: Degradation, budget and boundaries
|
||||
|
||||
**Objective:** As a maintainer, I want the same operational contract the rest of the tool has.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Constrained Detector shall run behind the existing chat endpoint resolution and shall record
|
||||
`learning_endpoint_unavailable` and score nothing rather than fall back to another provider.
|
||||
2. The Constrained Detector shall meter its own spend through the client's meter and shall stop when a configured
|
||||
cap is crossed, recording what it had spent.
|
||||
3. The Constrained Detector shall leave the detection, map and local pair gate outputs byte-identical.
|
||||
4. The Constrained Detector shall keep the core install at zero dependencies, and every optional capability it uses
|
||||
shall degrade to a recorded reason without its extra.
|
||||
5. The Constrained Detector shall pass every prompt and every persisted trajectory through redaction.
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"feature_name": "constrained-detector",
|
||||
"created_at": "2026-09-06T14:30:00Z",
|
||||
"updated_at": "2026-09-08T00:00:00Z",
|
||||
"language": "en",
|
||||
"phase": "tasks",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": false
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": false,
|
||||
"depends_on": [
|
||||
"learning-harness"
|
||||
],
|
||||
"approval_note": "2026-09-06 brief drafted at the maintainer's direction ('I'd rather spend time to design the exact DSPy style interface and evolutionary / statistical prompt optimization than fixing piece by piece'). Not approved; requirements not generated.; 2026-09-06 renamed from detector-optimizer to constrained-detector after the trajectory measurement: the brief's own conclusion is that the optimiser is not the next thing to build, and the spec should be named for what it delivers; 2026-09-06 requirements generated at the maintainer's direction, with the maintainer's emphasis recorded as Requirement 3 ('don't forget the emphasis on the real world bugs from the larger projects to avoid overfitting. we otherwise benchmark for cases that are not relevant'); 2026-09-06 requirements approved by the maintainer in session ('approve the requirements and run the design phase'); 2026-09-06 design approved by the maintainer in session ('approve the design and generate the tasks'); 2026-09-08 SUPERSEDED by execution-grounded-detection: this session's measurements showed the noise architecture (K-runs, per-family floor, regression budget, sign test, evolve loop) is a statistical treatment for a structural problem, and that the field (Big Sleep, AIxCC finals, Clearwing) grounds detection in execution rather than in scoring against a labeled corpus. The taxonomy, the IR candidate enumerator (96.6% ceiling) and the obligation checker survive as the web/logic detection path to contribute to a Clearwing-based substrate.",
|
||||
"superseded_by": "model-grounded-detection"
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
# Implementation Plan
|
||||
|
||||
- [x] 1. The ceiling, offline: what a candidate detector could possibly find
|
||||
- [x] 1.1 Candidate enumeration from the semantic IR, shaped by family
|
||||
- `learning/candidates.py`: `Candidate` (path, line, kind, name, text, argument texts and names, the binding chain the IR resolved, the guards in scope, the enclosing function, and an `id` of `path:line:name`); `CandidateKind` of call, bind, operation, config; `FAMILY_SHAPE` mapping each family to the kinds that can carry its bug; `enumerate_candidates` returning a `CandidateSet` with the candidates, the count discarded past the bound, and a reason of empty, `unsupported_language` or `no_candidate`.
|
||||
- Call and bind candidates come from `FunctionIR.calls` and `.binds`; operation candidates for access control come from the obligations facts with the decorators and in-body guards in scope; config candidates are bindings whose value is a literal in a configuration position. The pattern ruleset is never the generator.
|
||||
- Ordering is `(path, line, name)` and the bound is a constant, so the batches a later task forms are determined by the input alone.
|
||||
- Observable: on the shared obligations fixture every family shape yields the sites its docstring names; a file the IR cannot parse yields `unsupported_language` and no candidates; a labeled function with no calls or binds yields `no_candidate`; enumerating twice returns an identical sequence.
|
||||
- _Requirements: 1.1, 1.2, 1.4, 1.5, 1.6_
|
||||
|
||||
- [x] 1.2 The ceiling report and the `learning candidates` command
|
||||
- `candidates.ceiling(cases, taxonomy)` returns, per family and per slice, the number of labeled functions for which at least one candidate was enumerated, the sites-per-function distribution, and the rows that yielded nothing with their reason; `ousast learning candidates --catalog --slice --json` prints it and writes the artifact. No model is called at any point.
|
||||
- Observable: the command runs offline with no endpoint configured; the artifact reproduces the recorded web-slice figures (ruleset 12.1%, IR 96.6%, median 10 sites) and adds the deciding slices; running it twice produces a byte-identical artifact.
|
||||
- _Requirements: 1.3, 3.5_
|
||||
- _Depends: 1.1_
|
||||
|
||||
- [x] 1.3 The stop-or-go reading of the ceiling
|
||||
- The artifact states, per deciding slice, the ceiling against that slice's recorded detection rate from round zero, and names the comparison as the condition on which this design continues. The reading is written into the spec's Implementation Notes with the numbers, whichever way it goes.
|
||||
- Observable: the artifact carries agent-vfc, vfc-js, vfc and github ceilings beside 44.4% and the other recorded rates, and the notes state whether any deciding slice fails the condition.
|
||||
- _Requirements: 1.3, 3.5, 3.7_
|
||||
- _Depends: 1.2_
|
||||
|
||||
- [ ] 2. One definition of the checks, used to constrain and to score
|
||||
- [ ] 2.1 (P) Predicates extracted from the scorer without changing it
|
||||
- `learning/predicates.py`: `Violation`, the named predicates `line_in_parsed_function`, `sink_at_line`, `family_in_taxonomy` and `source_in_function`, and `check_claim`; `scoring._hits`, `_others` and `_fabricated` delegate to them.
|
||||
- Observable: the scorer's unit tests pass unchanged, and a test reproduces the recorded round-zero numbers for vibe-py and agent-vfc from the committed artifacts exactly, so the refactor's observable effect is none.
|
||||
- _Requirements: 4.1, 4.4, 6.1_
|
||||
|
||||
- [ ] 2.2 The unscorable vocabulary gains `no_candidate`
|
||||
- `no_candidate` joins the reasons `unscorable_reason` can return, distinct from `unresolved_label` (the corpus named a function nothing parses) and from `unsupported_language` (the tooling could not parse at all); it leaves every denominator and is listed by reason.
|
||||
- Observable: a pair whose labeled function has no call or bind is reported unscorable with `no_candidate` and appears in the per-family unscorable tally, not as a miss.
|
||||
- _Requirements: 1.5, 6.1_
|
||||
- _Depends: 1.1, 2.1_
|
||||
|
||||
- [ ] 3. The detector
|
||||
- [ ] 3.1 Typed judgment, one bounded question per candidate, no tools
|
||||
- `learning/judgments.py`: `Judgment` (candidate id, verdict, source expression, guard or empty, family, rationale) and `judge_candidates(candidates, config, client, model, taxonomy, violation=None)` making one call per fixed-size batch with `tools=[]` and JSON-object mode, the family's prompt and checklist as the system message, and the batch rendered as data. An answer naming a candidate id absent from the batch is dropped before any predicate runs.
|
||||
- Observable: a scripted client shows one call per batch and no tool schema offered; a reply with a fabricated candidate id yields no judgment and increments the `fabricated_candidate` tally; a reply that is prose yields no judgment rather than a parsed guess.
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
- _Depends: 1.1_
|
||||
|
||||
- [ ] 3.2 `run_constrained_detector`, with the predicates enforced and one retry
|
||||
- `detectors.run_constrained_detector` enumerates, batches, judges, checks each judgment with `check_claim`, retries a violating batch exactly once with the violation injected, discards what still violates, and returns `StaticFinding`s tagged `family:`, `detector:` and `candidate:<id>`. `run_family_detector` is untouched and stays selectable.
|
||||
- Observable: a judgment naming a line outside the parsed function is retried once and then discarded, and the discard is counted by predicate name; every finding carries the candidate id it came from; the unconstrained hunter still runs from the same configuration.
|
||||
- _Requirements: 2.5, 4.1, 4.2, 4.3, 6.5_
|
||||
- _Depends: 2.1, 3.1_
|
||||
|
||||
- [ ] 3.3 Two runs make the same calls
|
||||
- A recording client captures the full sequence of `(model, messages, tools)` for a run; two runs over the same region and configuration produce an identical sequence.
|
||||
- Observable: the test fails if candidate ordering, batching or the retry condition becomes input-dependent in a way the input does not determine; it is run repeatedly in the suite to catch order instability.
|
||||
- _Requirements: 2.4_
|
||||
- _Depends: 3.2_
|
||||
|
||||
- [ ] 3.4 Selecting the detector, and its degradations
|
||||
- `[learning] detector = "hunter" | "constrained"` defaulting to `hunter`; `--detector` on `learning score` and `learning baseline`; without the semantic extra the constrained detector reports `unsupported_language` and scores nothing; without an endpoint it records `learning_endpoint_unavailable`; its spend goes through the client meter and stops at the cap.
|
||||
- Observable: both detectors run from the same command over the same catalog; the extra-free matrix reports the recorded reason rather than failing; a run with no endpoint still writes its artifact.
|
||||
- _Requirements: 7.1, 7.2, 7.4, 7.5_
|
||||
- _Depends: 3.2_
|
||||
|
||||
- [ ] 4. Real-world code decides
|
||||
- [ ] 4.1 (P) Deciding and development slices, and the overfitting gap
|
||||
- `learning/slices.py`: `DECIDING_SLICES` of agent-vfc, vfc-js, vfc and github; `DEVELOPMENT_SLICES` of vibe-py; `overfitting_gap` over per-slice metrics; `cross_slice_verdict(before, after)` rejecting by name any change that improves a development slice and regresses a deciding one; `teachable(slice, cases)` false while every row of a slice sits below a gating tier.
|
||||
- Observable: a change that improves vibe-py and regresses agent-vfc is rejected with agent-vfc named; the gap over the recorded baseline reads 83.3% against 44.4%; `teachable("agent-vfc", …)` is false today and the reason is the tier.
|
||||
- _Requirements: 3.2, 3.3, 3.4, 3.6_
|
||||
|
||||
- [ ] 4.2 Per-slice publication, and memory kept out of the web aggregate
|
||||
- `publish` prints a per-slice table, the overfitting gap beside every headline number, the statement that the deciding slices teach nothing today, and the memory family separately from any figure a reader would take as web-taxonomy performance; no aggregate is printed without its per-slice rows.
|
||||
- Observable: the published section carries a row per slice and refuses to print a cross-slice aggregate alone; the 176 vfc rows appear under their own heading; the regenerated roadmap section is byte-identical on a second run.
|
||||
- _Requirements: 3.1, 3.4, 3.6, 3.7_
|
||||
- _Depends: 4.1_
|
||||
|
||||
- [ ] 5. The comparison, and what it retires
|
||||
- [ ] 5.1 Constrained against unconstrained, per slice
|
||||
- Both detectors scored by the same scorer over the same catalog at the same K, on every slice; detection, leak and pair-correct rates published against the recorded 64.9%, 12.3% and 52.6%, with cost per correct pair against $0.030 and $0.136. If the constrained detector is behind on any deciding slice, that is the headline of the artifact.
|
||||
- Observable: one artifact holds both detectors' numbers per slice with the deltas, and its summary line names the deciding-slice result rather than the aggregate.
|
||||
- _Requirements: 6.2, 6.3, 6.4_
|
||||
- _Depends: 3.4, 4.2_
|
||||
|
||||
- [ ] 5.2 The trajectory and the floor, re-measured
|
||||
- The trajectory experiment re-run against the constrained detector, publishing distinct trajectories per pair; the per-family noise floor re-measured at the same K as the baseline and published beside it; every published K-run number labelled as repeated measurements or as an ensemble of distinct procedures.
|
||||
- Observable: the artifact reports the distinct-trajectory count per pair for both detectors on the same twelve pairs, and the floors side by side.
|
||||
- _Requirements: 5.1, 5.2, 5.4_
|
||||
- _Depends: 5.1_
|
||||
|
||||
- [ ] 5.3 What the floor retires, named
|
||||
- If a family's re-measured floor is zero, the artifact states that its K-run requirement and its regression budget are unnecessary and names the `learning-harness` requirements that would be amended — 3.5, 8.2 and 9.5 — without amending them here.
|
||||
- Observable: the artifact carries the per-family verdict and the named requirements, and the spec's Implementation Notes record the escalation to the maintainer.
|
||||
- _Requirements: 5.3_
|
||||
- _Depends: 5.2_
|
||||
|
||||
- [ ] 5.4 Gates, matrices and the boundary
|
||||
- Detection, map and local pair gate outputs byte-identical to the committed baseline; both test matrices green; the core install still resolving with no dependencies; every prompt and persisted trajectory redacted.
|
||||
- Observable: the committed gate-baseline test passes in both matrices, and a planted key in a judgment prompt does not survive into the artifact.
|
||||
- _Requirements: 7.3, 7.4, 7.5_
|
||||
- _Depends: 5.1_
|
||||
|
||||
## Task-plan review notes
|
||||
|
||||
- Phase 1 is deliberately able to end the feature. Tasks 1.1 to 1.3 call no model, so the condition in 1.3 — a deciding slice whose ceiling is below its recorded detection rate — is reachable before any budget is spent. The ruleset variant of this design would have failed that condition at 12.1% against 64.9%.
|
||||
- Task 2.1 is the only task that touches `scoring.py`, and its observable is that nothing changes. The recorded round-zero artifacts are the pin; if they cannot be reproduced exactly, the refactor is wrong and not the numbers.
|
||||
- `(P)` marks the two tasks with no dependency on a model call: 2.1 and 4.1 can be built and reviewed while phase 1 runs.
|
||||
- Task 3.3 is the one that would have caught the defect this whole spec exists for. It belongs in the suite, run repeatedly, not in a one-off measurement.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Task 1.1 (2026-09-06, RED first: 13 failing tests in `tests/test_candidate_enumeration.py`; then green). `learning/candidates.py`. Two things the fixtures taught: the IR emits a chained call twice at one line, once bare and once carrying the keyword arguments, so deduplication keeps the richest — a bare `filter_by` and `filter_by(user_id=owner)` are the same site and only one of them says what the constraint was. And `leaky` and `constrained` in the shared obligations fixture **both** discharge through `filter_by`; what separates them is that one binding chain ends at `request.args.get('owner')` and the other at `request.user.id`. The test asserts that contrast, which is the judgement the model is being narrowed to.
|
||||
- Task 1.2 (2026-09-06, RED first: 3 failing tests; then green). `candidates.ceiling(cases, taxonomy, generator="ir"|"ruleset")` and `ousast learning candidates`. The `ruleset` arm exists so the comparison that rejected it stays a measurement rather than a remembered number. The command calls no model and its artifact is byte-identical on a second run.
|
||||
- Task 1.3 (2026-09-06). **Verdict: go.** Measured offline across every slice, `benchmarks/measurements/2026-09-06-candidate-ceiling-all-slices.json`:
|
||||
|
||||
| slice | ceiling | round-zero detection | headroom |
|
||||
|---|---|---|---|
|
||||
| agent-vfc (deciding) | **89.7%** | 44.4% | +45.3 |
|
||||
| vfc (deciding, non-gating) | 84.7% | pending | — |
|
||||
| vfc-js (deciding) | 52.9% | none recorded | see below |
|
||||
| github (deciding) | 100.0% | none recorded | — |
|
||||
| vibe-py (development) | 100.0% | 83.3% | +16.7 |
|
||||
|
||||
The margin is largest on agent-vfc, which is the slice that matters most and the one the tool is actually for. Per family: config_secrets, deserialization, output_encoding and untrusted_destination 100%; access_control 90%; path 87.5%; memory 86.4%; injection 84.6%; prototype 71.4%; `unknown` 0% of two rows, which is what abstention looks like.
|
||||
|
||||
- **A shape correction the measurement forced, before the detector existed.** `config_secrets` first read 63.6% with a bind-and-literal shape. A permissive default is as often an argument to a call — `cors({origin: '*'})`, `app.use(session({secure: false}))` — as it is a binding; adding call sites to that family's shape took it to 100% and recovered `new-erp-final-app-6e9644`, `zoonk-origin-bfbf67`, `melodix-playerprovider-089931` and `registry-proxy-worker-426847`.
|
||||
- **Carry-forward, and it is not this feature's to fix.** vfc-js sits at 52.9% because eight of seventeen rows label a function the JavaScript CST names `<anon>`: `module.exports.publish = function () {}` and its relatives. The gap is in function *naming*, not in candidate generation, and it caps the existing hunter on that slice exactly as much. Same class as the `_declarator_start` defects found during learning-harness task 4.1.
|
||||
- vfc's 21 `unsupported_language` rows are C and C++ files the IR declines; they are the non-gating memory slice and are already excluded from every web number.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Brief: contributor-scan — make the model layer a tool someone runs
|
||||
# Brief: contributor-scan, make the model layer a tool someone runs
|
||||
|
||||
**Status:** brief only, unapproved. Successor to `model-grounded-detection`, which is complete (16/16 tasks)
|
||||
and is not reopened by this.
|
||||
@@ -22,7 +22,7 @@ is wired. The spec is complete and the feature is unshipped, both at once.
|
||||
|
||||
> "a real world tool that helps developers when they want to contribute"
|
||||
|
||||
That reframes the work. A contributor does not have a labelled function and a known weakness class — they
|
||||
That reframes the work. A contributor does not have a labelled function and a known weakness class; they
|
||||
have a repository they are about to touch and a question about what is risky in it. Everything measured so
|
||||
far is function-level pairs: a labelled function, a declared family, a known target. None of it exercises the
|
||||
questions a whole repository asks.
|
||||
@@ -33,11 +33,11 @@ questions a whole repository asks.
|
||||
Without this nothing else matters.
|
||||
2. **Whole-repo scanning.** Region selection from entry points instead of a labelled function; ranking across
|
||||
thousands of candidates rather than one; a budget policy (the enumerator caps at 8 candidates per region,
|
||||
which is meaningless at repo scale); and the performance envelope — a CPG build on a 100k-line tree, not a
|
||||
which is meaningless at repo scale); and the performance envelope: a CPG build on a 100k-line tree, not a
|
||||
40-line excerpt. **This has never been run.**
|
||||
3. **A known-vulnerable repository as the target.** The 176 real-world CVE pairs already in the corpus are
|
||||
single-function excerpts and do not exercise any of the above. Running against a real checkout — curl,
|
||||
libpng, a WordPress plugin — is the only way to find what breaks at scale.
|
||||
single-function excerpts and do not exercise any of the above. Running against a real checkout (curl,
|
||||
libpng, a WordPress plugin) is the only way to find what breaks at scale.
|
||||
4. **The memory decision.** 173 of those 176 pairs are `memory` family, routed to a deferred execution tier
|
||||
that is a stub. Either adopt Clearwing for them or scope C memory-safety out explicitly. Right now it is
|
||||
neither, and a sixth of the corpus sits idle.
|
||||
@@ -49,12 +49,12 @@ questions a whole repository asks.
|
||||
These were delivered during `model-grounded-detection` without being in any spec. They belong here so the
|
||||
record is not scattered:
|
||||
|
||||
- **The pipeline itself** (`model/pipeline.py`) — Req 8 existed, task 3.2 built `judge()`, and nothing ever
|
||||
- The pipeline itself (`model/pipeline.py`): Req 8 existed, task 3.2 built `judge()`, and nothing ever
|
||||
said to connect an enumerator to it. Built, tested, measured, unspecified.
|
||||
- **Ops** — a single image carrying tool and engine, a compose file with read-only target and tmpfs scratch,
|
||||
- Ops: a single image carrying tool and engine, a compose file with read-only target and tmpfs scratch,
|
||||
bash/zsh/PowerShell wrappers that make the container invisible, and a pyinfra deploy for host installs.
|
||||
The Docker path is written and syntax-validated but **the image has never been built**.
|
||||
- **Corpus** — the `owasp` slice (40 validated xss pairs; `output_encoding` went from 1 pair to 41) and
|
||||
- Corpus: the `owasp` slice (40 validated xss pairs; `output_encoding` went from 1 pair to 41) and
|
||||
authored sink/sanitizer facts for Java and JavaScript.
|
||||
|
||||
## Known limits to carry forward
|
||||
@@ -63,7 +63,7 @@ record is not scattered:
|
||||
`debug=True` is dangerous where `httponly=True` is safe and the model cannot tell them apart.
|
||||
- Interprocedural resolution is the most repeated miss cause across every experiment: OWASP's helper classes,
|
||||
`angular-http-server`'s `resolveUrl`. Joern supports it; our queries may not be configured for it.
|
||||
- Five bugs in `model-grounded-detection` shared one shape — a taint-specific assumption applied to every
|
||||
- Five bugs in `model-grounded-detection` shared one shape: a taint-specific assumption applied to every
|
||||
family, each found by a measurement rather than a check. Any new family-specific path should be audited for
|
||||
siblings before it is measured, not after.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Task 2.19 — measured input retention and census completeness
|
||||
# Task 2.19, measured input retention and census completeness
|
||||
|
||||
The maintainer accepted the proposed contributor-scan follow-up after the pre-push
|
||||
NO-GO report. The Kiro root-cause-first workflow identified a concrete shared-boundary
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Design Document: contributor-scan
|
||||
|
||||
## Integration amendment — 2026-09-12 (draft, unapproved)
|
||||
## Integration amendment, 2026-09-12 (draft, unapproved)
|
||||
|
||||
The maintainer has clarified the product as an actionable pre-push safety net across supported
|
||||
ecosystems. The proposed successor `../pre-push-safety-net/design.md` consumes this driver/backend
|
||||
@@ -9,7 +9,7 @@ of both targets and graphs. Existing approvals below apply to the prior design,
|
||||
|
||||
Proposed shared seams needing requirements/design review before implementation:
|
||||
|
||||
- Accept an absolute deadline across graph build, overlays, retries and queries, not only region/call caps.
|
||||
- Accept an absolute deadline across graph build, overlays, retries and queries as well as region/call caps.
|
||||
- Accept an optional validated reusable graph lease while preserving ordinary fresh-build scans;
|
||||
backend remains authoritative for artifact identity, census, compatibility and cleanup.
|
||||
- Return exact selected/deferred question IDs and context boundaries from ranker decisions;
|
||||
@@ -45,13 +45,13 @@ for someone who does not have Joern.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- New arbiters or families — the three forms exist and are measured.
|
||||
- New arbiters or families; the three forms exist and are measured.
|
||||
- Any optimiser, prompt search, or K-run averaging. The predecessor deleted that machinery; this does not
|
||||
reintroduce it by another name.
|
||||
- Building a sandbox. Req 10 of the predecessor stands: adopt, never build.
|
||||
- Changing the gates, the taxonomy, or the fact-table schema.
|
||||
|
||||
## The fact tables are an ontology, and it is general-purpose — 2026-09-21
|
||||
## The fact tables are an ontology, and it is general-purpose, 2026-09-21
|
||||
|
||||
The `[[source]]`, `[[sink]]`, `[[sanitizer]]`, `[[operation]]`, `[[discharger]]` and `[[layout]]` rows
|
||||
are not a pattern list. They are an ontology of program elements and their security relations: what
|
||||
@@ -87,18 +87,18 @@ repository is in front of the maintainer, which is what task 7.1 exists to make
|
||||
|
||||
### This Spec Owns
|
||||
|
||||
- `model/regions.py` — entry points and file fallbacks to regions, with the family routing for each.
|
||||
- `model/scan.py` — the repository-level driver: rank, budget, dispatch to `model/pipeline`, collect.
|
||||
- `model/regions.py`: entry points and file fallbacks to regions, with the family routing for each.
|
||||
- `model/scan.py`: the repository-level driver: rank, budget, dispatch to `model/pipeline`, collect.
|
||||
- The `Stage.MAP` wiring in `cli.py` and the scan's model-layer payload and report section.
|
||||
- `benchmarks/repos/` — pinned known-vulnerable checkouts and their baselines.
|
||||
- `benchmarks/repos/`: pinned known-vulnerable checkouts and their baselines.
|
||||
- The decisions in Req 7 and their record.
|
||||
|
||||
### Out of Boundary
|
||||
|
||||
- `model/pipeline`, `taint`, `dominance`, `config_value`, `ladder`, `specs`, `candidates` — built, measured,
|
||||
- `model/pipeline`, `taint`, `dominance`, `config_value`, `ladder`, `specs`, `candidates`: built, measured,
|
||||
used unchanged. If this feature needs to change one, that is a signal the change belongs in its own commit
|
||||
with its own measurement.
|
||||
- `mapping.analyze_entry_points`, the overlay, the obligations checker, the gates — untouched.
|
||||
- `mapping.analyze_entry_points`, the overlay, the obligations checker, the gates: untouched.
|
||||
|
||||
### Allowed Dependencies
|
||||
|
||||
@@ -117,8 +117,8 @@ repository is in front of the maintainer, which is what task 7.1 exists to make
|
||||
### Existing Architecture Analysis
|
||||
|
||||
`ousast scan` runs `policy_load → ruleset_load → preprocess → entry_point_mapping → rank → calibrate →
|
||||
STATIC → MAP → REGRESS → report`. Inside MAP it already builds a complexity map, runs the overlay, and — the
|
||||
closest analogue to what we are adding — runs `_check_obligations(root, targets, entries, facts, settings,
|
||||
STATIC → MAP → REGRESS → report`. Inside MAP it already builds a complexity map, runs the overlay, and (the
|
||||
closest analogue to what we are adding) runs `_check_obligations(root, targets, entries, facts, settings,
|
||||
runtime, hunter_model)`, which takes entry points, produces findings, and merges them by re-ranking.
|
||||
|
||||
The model layer slots in exactly there, with the same shape and the same failure discipline. `entry_points`
|
||||
@@ -167,10 +167,10 @@ benchmarks/repos/
|
||||
|
||||
### Changed
|
||||
|
||||
- `cli.py` — a `model` stage inside MAP, mirroring `_check_obligations`; its payload into the manifest and a
|
||||
- `cli.py`: a `model` stage inside MAP, mirroring `_check_obligations`; its payload into the manifest and a
|
||||
report section.
|
||||
- `model/report.py` — a per-rung summary block for a scan (it currently reports per-slice for corpus runs).
|
||||
- `config.py` — a `ModelConfig`-style block: enabled, budget, max regions. Zero-dep, defaults conservative.
|
||||
- `model/report.py`: a per-rung summary block for a scan (it currently reports per-slice for corpus runs).
|
||||
- `config.py`: a `ModelConfig`-style block: enabled, budget, max regions. Zero-dep, defaults conservative.
|
||||
|
||||
### Unchanged and load-bearing
|
||||
|
||||
@@ -194,8 +194,8 @@ class ScanRegion:
|
||||
def regions_for(entries, targets, *, taxonomy, language_of) -> tuple[ScanRegion, ...]
|
||||
```
|
||||
|
||||
Ranking uses what `EntryPointRecord` already carries — an externally-reachable entry point at a trust
|
||||
boundary outranks an internal one — rather than inventing a new signal.
|
||||
Ranking uses what `EntryPointRecord` already carries (an externally-reachable entry point at a trust
|
||||
boundary outranks an internal one) rather than inventing a new signal.
|
||||
|
||||
### model/scan.py
|
||||
|
||||
@@ -264,10 +264,10 @@ each; everything past exhaustion counted into `regions_unjudged`.
|
||||
1. **Wire it (Req 1, 2).** Regions, the MAP stage, findings with rungs reaching a report. Gate-protected.
|
||||
Nothing else matters until a user can see a rung.
|
||||
2. **Scale it (Req 3, 4).** Budget and ranking, then the first run against a pinned known-vulnerable
|
||||
checkout — the measurement that has never been taken and can reshape the rest.
|
||||
checkout, the measurement that has never been taken and can reshape the rest.
|
||||
3. **Ship it (Req 5, 6).** Contributor output; build and verify the image that is currently only defined.
|
||||
4. **Close the deferred (Req 7).** The memory decision; retire `evidence_level`.
|
||||
5. **Grow the corpus (Req 9).** PHP/WordPress first — the families the model arbitrates best. C CVEs only
|
||||
5. **Grow the corpus (Req 9).** PHP/WordPress first: the families the model arbitrates best. C CVEs only
|
||||
after 7.1.
|
||||
6. **Regression baselines (Req 10).**
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Release remediation research — 2026-09-14
|
||||
# Release remediation research, 2026-09-14
|
||||
|
||||
Status: investigation under the maintainer's instruction to continue towards release.
|
||||
This record does not approve rollout, replace the existing design, or mark a production
|
||||
|
||||
@@ -12,7 +12,7 @@ hunter's 76.7% / 12.3%, at a fifth of the model calls. All 16 of its tasks are c
|
||||
from a measurement harness, not from the tool. This feature closes that gap and answers the question the
|
||||
maintainer actually posed: *a real world tool that helps developers when they want to contribute.*
|
||||
|
||||
That framing changes the work. A contributor has no labelled function and no declared weakness class — they
|
||||
That framing changes the work. A contributor has no labelled function and no declared weakness class; they
|
||||
have a repository they are about to touch. Everything measured so far is function-level pairs, which
|
||||
exercise none of the questions a whole repository asks: which regions to look at, how to rank thousands of
|
||||
candidates, what to spend, and whether a CPG build on a 100k-line tree finishes at all.
|
||||
@@ -22,13 +22,13 @@ Context: `brief.md`, `.kiro/specs/model-grounded-detection/`, and the committed
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: wiring `model/pipeline` into `ousast scan`; region selection from entry points; ranking and
|
||||
- In scope: wiring `model/pipeline` into `ousast scan`; region selection from entry points; ranking and
|
||||
budget at repository scale; the performance envelope; running against known-vulnerable checkouts;
|
||||
contributor-facing output; the packaging already built (image, compose, shell wrappers, pyinfra); the
|
||||
deferred memory-family decision; retiring `evidence_level`.
|
||||
- **Out of scope**: new arbiters or families (the three forms exist); prompt optimisation of any kind;
|
||||
building a sandbox (Req 10 of the predecessor stands — adopt, never build); changing the gates.
|
||||
- **Carried forward, not re-litigated**: the evidence ladder, the taxonomy, the fact tables, and the finding
|
||||
- Out of scope: new arbiters or families (the three forms exist); prompt optimisation of any kind;
|
||||
building a sandbox (Req 10 of the predecessor stands: adopt, never build); changing the gates.
|
||||
- Carried forward, not re-litigated: the evidence ladder, the taxonomy, the fact tables, and the finding
|
||||
that the model supplies precision while the LLM supplies recall.
|
||||
|
||||
## Requirements
|
||||
@@ -45,7 +45,7 @@ built is something I can run rather than something a harness demonstrates.
|
||||
2. The system shall report model findings alongside the existing pattern and overlay findings, without
|
||||
removing either.
|
||||
3. When no CPG engine is available, the scan shall behave exactly as it does today and record
|
||||
`cpg_unavailable` — the model layer is additive, never a precondition.
|
||||
`cpg_unavailable`; the model layer is additive, never a precondition.
|
||||
4. The detection, map and local pair gates shall remain byte-identical to the committed baseline.
|
||||
5. The system shall expose the model layer's per-rung counts in the scan's own output, so a user can see how
|
||||
much of the result the model arbitrated rather than inferring it.
|
||||
@@ -109,7 +109,7 @@ in the corpus exercises it.
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. Every finding shall carry its rung, its witness, and the file and line it concerns.
|
||||
2. The system shall state plainly what a `suspicion` means — that the model could not confirm it — so a
|
||||
2. The system shall state plainly what a `suspicion` means (that the model could not confirm it) so a
|
||||
reader does not treat it as established.
|
||||
3. The system shall write findings to the writable output location, never into the repository under analysis.
|
||||
4. The system shall summarise a scan in a form readable without opening a JSON file.
|
||||
@@ -121,7 +121,7 @@ in the corpus exercises it.
|
||||
throughout, and no criterion here forbade it.)*
|
||||
6. Where a family's abstraction cannot decide a weakness class, the system shall state that limit in the
|
||||
report, and no scan shall present an absence of findings as evidence of absence. *(Added 2026-09-09.
|
||||
`c/memory` scored zero true positives across 89k lines of C for a structural reason — it models string
|
||||
`c/memory` scored zero true positives across 89k lines of C for a structural reason: it models string
|
||||
functions and libpng overflows through arithmetic. A silent scan reading as a clean bill of health is
|
||||
worse than noise because it is quiet, and it is the one failure mode the rung ladder does not guard.)*
|
||||
|
||||
@@ -148,7 +148,7 @@ silently caps what the corpus can tell us.
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall either reach the `memory` family through an adopted execution tier, or record explicitly
|
||||
that C memory-safety is out of scope — 173 of the 176 real-world CVE pairs currently sit idle behind a
|
||||
that C memory-safety is out of scope; 173 of the 176 real-world CVE pairs currently sit idle behind a
|
||||
stub.
|
||||
2. The system shall retire `evidence_level` in favour of `rung`, or record why both are kept.
|
||||
3. Each decision shall be recorded with its reason where a later reader will find it.
|
||||
@@ -167,7 +167,7 @@ this one, because each exists due to a specific past error.
|
||||
4. The core install shall remain zero-dependency, with the CPG engine, the LLM endpoint and any execution
|
||||
tier optional and degrading to a recorded reason.
|
||||
5. Any new family-specific code path shall be checked for sibling paths that need the same change, before it
|
||||
is measured — five bugs in the predecessor shared exactly that shape.
|
||||
is measured; five bugs in the predecessor shared exactly that shape.
|
||||
|
||||
### Requirement 9: Grow the corpus where the engine can use it
|
||||
|
||||
@@ -177,14 +177,14 @@ what developers actually contribute to rather than against 30 pairs of one slice
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall add a **PHP/WordPress** slice covering plugin and core weaknesses. WordPress plugin CVEs
|
||||
are overwhelmingly injection, XSS and authorization bypass — the families the model layer already
|
||||
arbitrates well — and `php2cpg` works once a PHP interpreter is present, which the shipped image now
|
||||
are overwhelmingly injection, XSS and authorization bypass (the families the model layer already
|
||||
arbitrates well) and `php2cpg` works once a PHP interpreter is present, which the shipped image now
|
||||
provides.
|
||||
2. The system shall add **real-world C library CVEs** beyond the existing curl and OpenSSL rows — libpng and
|
||||
2. The system shall add **real-world C library CVEs** beyond the existing curl and OpenSSL rows: libpng and
|
||||
the image libraries, OpenSSH, libpam, and vulnerable kernel modules were named. These are gated on Req 7.1:
|
||||
until the memory-family decision is closed they would join 173 pairs already sitting idle, so the decision
|
||||
comes first and the harvest follows it.
|
||||
3. The system shall add further **vibe-code** pairs — agent-written and human-written web application bugs —
|
||||
3. The system shall add further **vibe-code** pairs (agent-written and human-written web application bugs)
|
||||
because that slice is where the tool's stated audience actually works, and it is currently 35 pairs.
|
||||
4. Before any slice is vendored at scale, the system shall verify on a sample that the model can **read** its
|
||||
pairs, and shall vendor only the categories that a measurement shows it can arbitrate. The OWASP harvest
|
||||
@@ -194,7 +194,7 @@ what developers actually contribute to rather than against 30 pairs of one slice
|
||||
contiguous key-shaped literal.
|
||||
6. Each new slice shall report as its own row and shall never be blended into a headline, and a synthetic
|
||||
slice shall say in its catalog that it measures breadth rather than real-world performance.
|
||||
7. The system shall record, per slice, how many pairs the model can arbitrate — so corpus growth is measured
|
||||
7. The system shall record, per slice, how many pairs the model can arbitrate, so corpus growth is measured
|
||||
by what it teaches rather than by how many rows it adds.
|
||||
|
||||
### Requirement 10: Regression tuning over a large codebase
|
||||
@@ -206,7 +206,7 @@ tables and queries can be checked for regressions that pair scoring cannot see.
|
||||
|
||||
1. The system shall support a repeatable scan over a pinned checkout of a large repository, recorded as a
|
||||
committed baseline.
|
||||
2. When the fact tables or queries change, the system shall report the delta against that baseline —
|
||||
2. When the fact tables or queries change, the system shall report the delta against that baseline:
|
||||
findings gained, findings lost, and rung movements.
|
||||
3. The system shall treat an unexplained loss of a previously-entailed finding as a regression to be
|
||||
investigated, not a number to be accepted.
|
||||
@@ -214,7 +214,7 @@ tables and queries can be checked for regressions that pair scoring cannot see.
|
||||
|
||||
---
|
||||
|
||||
## Amendment record — group 2 (approved 2026-09-09)
|
||||
## Amendment record, group 2 (approved 2026-09-09)
|
||||
|
||||
Group 2 was specified as the phase that can reshape the rest, and it did. Three amendments were drafted from
|
||||
what it measured and **approved by the maintainer on 2026-09-09**; they are folded into the numbered
|
||||
@@ -227,7 +227,7 @@ requirements above and marked there with the evidence that produced them.
|
||||
| C — a checkout that can demonstrate detection | Req 4.6 | libpng satisfies 4.1 and cannot demonstrate detection |
|
||||
|
||||
**A is only half implemented, deliberately.** Task 2.11 made pattern matches proportionate. The same argument
|
||||
applies to the model layer's own suspicion band — 38 of vampi's 60 findings — which is an enumerated
|
||||
applies to the model layer's own suspicion band (38 of vampi's 60 findings), which is an enumerated
|
||||
candidate the graph could not decide and an LLM affirmed: a pattern match with an opinion attached, not a
|
||||
reasoned claim. Task 2.17 carries that half.
|
||||
|
||||
@@ -238,5 +238,5 @@ reasoned claim. Task 2.17 carries that half.
|
||||
(2.6), where the leak fell 42.9% → 0.0% with pair-correct unchanged.
|
||||
* **Req 3.3's honesty counters were the load-bearing part.** `regions_unjudged` is what made a budget that
|
||||
stopped free work visible at all.
|
||||
* **Req 11.2's byte-identical gates held through every one of nine defect fixes** — which says the same thing
|
||||
* **Req 11.2's byte-identical gates held through every one of nine defect fixes**, which says the same thing
|
||||
from the other side: the corpus could not have caught any of them.
|
||||
|
||||
@@ -18,8 +18,8 @@ Derive a mechanism record from a pair by parsing the vulnerable excerpt with the
|
||||
|
||||
## Scope
|
||||
|
||||
- **In**: mechanism export from pairs; shape normalization; variant search as a proposer in MAP; leave-one-out measurement command and payload; `mechanisms` improve lever with validator rules; store hygiene (tier, provenance, dedupe, version); offline tests.
|
||||
- **Out**: LLM-authored shapes; changing dispositions or evidence rungs; embedding retrieval changes (existing prove-order reuse stays); inter-file flow (reachability spec); any merge gate; vendoring more pairs (corpus spec).
|
||||
- In: mechanism export from pairs; shape normalization; variant search as a proposer in MAP; leave-one-out measurement command and payload; `mechanisms` improve lever with validator rules; store hygiene (tier, provenance, dedupe, version); offline tests.
|
||||
- Out: LLM-authored shapes; changing dispositions or evidence rungs; embedding retrieval changes (existing prove-order reuse stays); inter-file flow (reachability spec); any merge gate; vendoring more pairs (corpus spec).
|
||||
|
||||
## Boundary Candidates
|
||||
|
||||
@@ -35,13 +35,13 @@ Derive a mechanism record from a pair by parsing the vulnerable excerpt with the
|
||||
|
||||
## Upstream / Downstream
|
||||
|
||||
- **Upstream**: `PairCase`/`load_pair_catalog` with `review_tier`; `FileIR`; `MechanismStore`; `adjudicate`; `run_improvement` gate with per-profile holdout.
|
||||
- **Downstream**: `reachability-flow-model` attaches paths to variant findings; `guard-dominance-regime` consumes the guard shapes as sanitizer candidates; reports show mechanism ids on findings.
|
||||
- Upstream: `PairCase`/`load_pair_catalog` with `review_tier`; `FileIR`; `MechanismStore`; `adjudicate`; `run_improvement` gate with per-profile holdout.
|
||||
- Downstream: `reachability-flow-model` attaches paths to variant findings; `guard-dominance-regime` consumes the guard shapes as sanitizer candidates; reports show mechanism ids on findings.
|
||||
|
||||
## Existing Spec Touchpoints
|
||||
|
||||
- **Extends**: `propose-adjudicate-prove` mechanism memory (additive fields, second write path), `three-stage-scan` MAP stage (one more proposer), `harnessx-self-improving-rulesets` improve loop (one more bounded lever).
|
||||
- **Depends**: `pair-corpus-honesty` Req 9 (tiers) for which pairs may seed.
|
||||
- Extends: `propose-adjudicate-prove` mechanism memory (additive fields, second write path), `three-stage-scan` MAP stage (one more proposer), `harnessx-self-improving-rulesets` improve loop (one more bounded lever).
|
||||
- Depends: `pair-corpus-honesty` Req 9 (tiers) for which pairs may seed.
|
||||
|
||||
## Constraints
|
||||
|
||||
|
||||
@@ -107,9 +107,9 @@ tests/
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `semantic/overlay.py` — `OverlayRecord.mechanism_id: str | None = None` (additive).
|
||||
- `regress/candidate.py` — exposes `hotspot_from_finding`; variant findings enter the ranked candidate slice as zero-score hotspots from `cli.py` (after every scored promotion, inside the cap, through the safety check).
|
||||
- `config.py` — `[variants] enabled=true, max_mechanisms=500`.
|
||||
- `semantic/overlay.py`: `OverlayRecord.mechanism_id: str | None = None` (additive).
|
||||
- `regress/candidate.py`: exposes `hotspot_from_finding`; variant findings enter the ranked candidate slice as zero-score hotspots from `cli.py` (after every scored promotion, inside the cap, through the safety check).
|
||||
- `config.py`: `[variants] enabled=true, max_mechanisms=500`.
|
||||
|
||||
## System Flows
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ OpenUltraSAST's labeled pair corpus measures the detector but does not improve i
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: mechanism export from pairs; shape normalization per language; variant search proposer in MAP; leave-one-out evaluation; `mechanisms` improve lever; store schema additions; reports showing mechanism ids; offline tests.
|
||||
- **Out of scope**: taint, facts, walker, dispositions, evidence rungs; embedding retrieval changes; inter-file flow; LLM-authored shapes; catalogs and scorer changes beyond reading `PairCase`; any merge gate.
|
||||
- **Adjacent expectations**: `Mechanism` gains additive fields only; `order_promotions` keeps working on old records; the improve loop's existing clauses and the per-profile holdout clause apply unchanged; `local` remains the only slice that can fail CI.
|
||||
- In scope: mechanism export from pairs; shape normalization per language; variant search proposer in MAP; leave-one-out evaluation; `mechanisms` improve lever; store schema additions; reports showing mechanism ids; offline tests.
|
||||
- Out of scope: taint, facts, walker, dispositions, evidence rungs; embedding retrieval changes; inter-file flow; LLM-authored shapes; catalogs and scorer changes beyond reading `PairCase`; any merge gate.
|
||||
- Adjacent expectations: `Mechanism` gains additive fields only; `order_promotions` keeps working on old records; the improve loop's existing clauses and the per-profile holdout clause apply unchanged; `local` remains the only slice that can fail CI.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Mechanisms seeded from trusted pairs
|
||||
|
||||
**Objective:** As a maintainer, I want each reviewed pair to become a mechanism record, so that the corpus is a library the detector can search, not only a ruler.
|
||||
**Objective:** As a maintainer, I want each reviewed pair to become a mechanism record, so that the corpus is both a library the detector can search and a ruler.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
@@ -64,7 +64,7 @@ OpenUltraSAST's labeled pair corpus measures the detector but does not improve i
|
||||
|
||||
### Requirement 5: A recall-raising lever in the improve loop
|
||||
|
||||
**Objective:** As a maintainer, I want the improve loop to be able to admit mechanisms, so that it can raise recall and not only shadow noisy rules.
|
||||
**Objective:** As a maintainer, I want the improve loop to be able to admit mechanisms, so that it can raise recall as well as shadow noisy rules.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Summary
|
||||
|
||||
- **Feature**: `corpus-seeded-mechanisms`
|
||||
- **Discovery Scope**: Extension of mechanism memory, MAP stage, pair eval, and the improve loop
|
||||
- **Key Findings**:
|
||||
- Feature: `corpus-seeded-mechanisms`
|
||||
- Discovery Scope: Extension of mechanism memory, MAP stage, pair eval, and the improve loop
|
||||
- Key Findings:
|
||||
- `semantic/mechanisms.py` already has `Mechanism`, `MechanismStore` (append-only JSONL), `append_mechanism`, embedding cache, hard filters by language/CWE/tags, and `order_promotions`. It is written only by `cli._append_proven_mechanisms` after a sandbox trigger, so it is empty on every real tree.
|
||||
- Clearwing's recall lever is the variant loop: a proven mechanism becomes a structural search over the messy tree. OpenUltraSAST copied the store and the retrieval, not the loop.
|
||||
- `FileIR` call sites already carry callee name, argument texts, argument identifier lists, and constant flags; binds carry source text and names. That is enough for a shape without new parsing.
|
||||
@@ -16,30 +16,30 @@
|
||||
|
||||
### Mechanism store today
|
||||
|
||||
- **Context**: Can the existing store carry corpus-derived records?
|
||||
- **Sources Consulted**: `semantic/mechanisms.py`, `cli.py` `_append_proven_mechanisms`, `propose-adjudicate-prove` design §Mechanism.
|
||||
- **Findings**: `Mechanism` fields are summary, cwe, language, tags, keywords, what_made_it_exploitable, source_finding_id, source_repo. Identity is the pattern. JSONL is authoritative; vectors are a rebuildable cache keyed by id and model.
|
||||
- **Implications**: add `origin` (`sandbox` | `corpus`), `review_tier`, `pairs`, `shape` (structured), `guard` as additive fields with defaults so old rows load. A second writer (`append_from_pair`) beside the sandbox writer.
|
||||
- Context: Can the existing store carry corpus-derived records?
|
||||
- Sources Consulted: `semantic/mechanisms.py`, `cli.py` `_append_proven_mechanisms`, `propose-adjudicate-prove` design §Mechanism.
|
||||
- Findings: `Mechanism` fields are summary, cwe, language, tags, keywords, what_made_it_exploitable, source_finding_id, source_repo. Identity is the pattern. JSONL is authoritative; vectors are a rebuildable cache keyed by id and model.
|
||||
- Implications: add `origin` (`sandbox` | `corpus`), `review_tier`, `pairs`, `shape` (structured), `guard` as additive fields with defaults so old rows load. A second writer (`append_from_pair`) beside the sandbox writer.
|
||||
|
||||
### Shape derivation from FileIR
|
||||
|
||||
- **Context**: Avoid regex and LLM authoring.
|
||||
- **Sources Consulted**: `semantic/ir.py` (`CallSite.arg_names`, `arg_is_constant`, `Bind.names`, `FunctionIR.params`), `semantic/taint.py` (`_source_in`, parameter seeding).
|
||||
- **Findings**: for the labeled function, the sink call site is the call whose trailing name equals the label's sink or whose line equals the label's line; source positions are argument indexes whose identifiers trace by binds to a fact source, a parameter, or a container read. Guard kind comes from statements on the fixed side absent on the vulnerable side: `if` with `== NULL`/`is None`/`!x` → `null_test`; comparison with `<`, `>`, `len(` → `bounds_test`; membership in a literal collection or `startswith`/regex → `allowlist_test`; identity read from an auth context (`request.state`, `req.user`, `session`) → `auth_check`; parameterized second argument → `parameterized_call`; changed declared type → `type_change`.
|
||||
- **Implications**: `semantic/variants.py` implements `derive_shape(pair_ir_vuln, pair_ir_fixed, label)` and `match_shape(file_ir, shape)`. Guard classification is heuristic and closed; unknown → `none`.
|
||||
- Context: Avoid regex and LLM authoring.
|
||||
- Sources Consulted: `semantic/ir.py` (`CallSite.arg_names`, `arg_is_constant`, `Bind.names`, `FunctionIR.params`), `semantic/taint.py` (`_source_in`, parameter seeding).
|
||||
- Findings: for the labeled function, the sink call site is the call whose trailing name equals the label's sink or whose line equals the label's line; source positions are argument indexes whose identifiers trace by binds to a fact source, a parameter, or a container read. Guard kind comes from statements on the fixed side absent on the vulnerable side: `if` with `== NULL`/`is None`/`!x` → `null_test`; comparison with `<`, `>`, `len(` → `bounds_test`; membership in a literal collection or `startswith`/regex → `allowlist_test`; identity read from an auth context (`request.state`, `req.user`, `session`) → `auth_check`; parameterized second argument → `parameterized_call`; changed declared type → `type_change`.
|
||||
- Implications: `semantic/variants.py` implements `derive_shape(pair_ir_vuln, pair_ir_fixed, label)` and `match_shape(file_ir, shape)`. Guard classification is heuristic and closed; unknown → `none`.
|
||||
|
||||
### Where variant search sits
|
||||
|
||||
- **Context**: Propose, adjudicate, prove roles must stay separate.
|
||||
- **Sources Consulted**: `three-stage-scan` design (MAP stage, hunter suspicion), `propose-adjudicate-prove` (overlay records, coverage), `regress/candidate.py`.
|
||||
- **Findings**: MAP already runs overlay and the tool hunter; variant search is a third proposer that reads `FileIR` and the store. Findings at `suspicion`, origin `variant`; when the overlay has a flow at that call site, merge (attach mechanism id to the overlay record). Candidate eligibility follows the hunter-eligibility path being added in `reachability-flow-model` or the sev-5 forced-hotspot path today.
|
||||
- **Implications**: no disposition change; one new finding origin; manifest counters.
|
||||
- Context: Propose, adjudicate, prove roles must stay separate.
|
||||
- Sources Consulted: `three-stage-scan` design (MAP stage, hunter suspicion), `propose-adjudicate-prove` (overlay records, coverage), `regress/candidate.py`.
|
||||
- Findings: MAP already runs overlay and the tool hunter; variant search is a third proposer that reads `FileIR` and the store. Findings at `suspicion`, origin `variant`; when the overlay has a flow at that call site, merge (attach mechanism id to the overlay record). Candidate eligibility follows the hunter-eligibility path being added in `reachability-flow-model` or the sev-5 forced-hotspot path today.
|
||||
- Implications: no disposition change; one new finding origin; manifest counters.
|
||||
|
||||
### Leave-one-out
|
||||
|
||||
- **Context**: Prove that corpus growth raises recall, honestly.
|
||||
- **Findings**: pair eval materializes each side into a temp tree already; a leave-one-out runner seeds a temp store from the other trusted pairs and runs variant search on both sides, scoring with the pair scorer's detection and leak rules.
|
||||
- **Implications**: `pairs.py` gains a read-only consumer path (`evaluate_loo`) or a sibling module `semantic/loo.py` to keep the scorer's boundary; report per slice, profile, mechanism; artifact only.
|
||||
- Context: Prove that corpus growth raises recall, honestly.
|
||||
- Findings: pair eval materializes each side into a temp tree already; a leave-one-out runner seeds a temp store from the other trusted pairs and runs variant search on both sides, scoring with the pair scorer's detection and leak rules.
|
||||
- Implications: `pairs.py` gains a read-only consumer path (`evaluate_loo`) or a sibling module `semantic/loo.py` to keep the scorer's boundary; report per slice, profile, mechanism; artifact only.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
@@ -54,27 +54,27 @@
|
||||
|
||||
### Decision: Seed only from seeded and reviewed tiers
|
||||
|
||||
- **Context**: `title` pairs are unreviewed guesses; `advisory` pairs have correct fixes but unlabeled mechanisms.
|
||||
- **Selected Approach**: export reads `review_tier`; `advisory`/`title` may be scored leave-one-out as held-out targets but never seed.
|
||||
- **Trade-offs**: fewer seeds at first; honest about what the detector was taught.
|
||||
- Context: `title` pairs are unreviewed guesses; `advisory` pairs have correct fixes but unlabeled mechanisms.
|
||||
- Selected Approach: export reads `review_tier`; `advisory`/`title` may be scored leave-one-out as held-out targets but never seed.
|
||||
- Trade-offs: fewer seeds at first; honest about what the detector was taught.
|
||||
|
||||
### Decision: Variant is a proposer at suspicion
|
||||
|
||||
- **Context**: Evidence honesty.
|
||||
- **Selected Approach**: origin `variant`, `suspicion`; merge with overlay flow when present; sandbox may raise.
|
||||
- **Trade-offs**: variant-only hits do not fail CI; correct.
|
||||
- Context: Evidence honesty.
|
||||
- Selected Approach: origin `variant`, `suspicion`; merge with overlay flow when present; sandbox may raise.
|
||||
- Trade-offs: variant-only hits do not fail CI; correct.
|
||||
|
||||
### Decision: Lever admits records, not shapes
|
||||
|
||||
- **Context**: Keep the validator closed.
|
||||
- **Selected Approach**: `mechanisms` lever edits reference record ids produced by the exporter; free-form shapes are rejected.
|
||||
- Context: Keep the validator closed.
|
||||
- Selected Approach: `mechanisms` lever edits reference record ids produced by the exporter; free-form shapes are rejected.
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
- Coarse shapes over-match (`exec(x)` everywhere) — source-position requirement plus fact/parameter/container check; report leak rate on fixed sides leave-one-out; the per-profile gate rejects admissions that leak.
|
||||
- Guard heuristics misclassify — closed set with `none`; reported, not used for demotion.
|
||||
- Store growth — dedupe by shape; one record per shape with pair provenance list.
|
||||
- Leave-one-out cost — one temp store per pair over tens of pairs; fine; cache parsed IR per excerpt.
|
||||
- Coarse shapes over-match (`exec(x)` everywhere): source-position requirement plus fact/parameter/container check; report leak rate on fixed sides leave-one-out; the per-profile gate rejects admissions that leak.
|
||||
- Guard heuristics misclassify: closed set with `none`; reported, not used for demotion.
|
||||
- Store growth: dedupe by shape; one record per shape with pair provenance list.
|
||||
- Leave-one-out cost: one temp store per pair over tens of pairs; fine; cache parsed IR per excerpt.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Brief: detection-core
|
||||
|
||||
## Problem
|
||||
|
||||
Detection in this project is specified as a set of per-family, per-arm runners (prover,
|
||||
hunter, hook, proof arm). Corrections patch one arm and do not compose: v1→v2 retunes
|
||||
transferred 0/17 on untouched cases, and the 2026-10-30 admission run returned NO-GO
|
||||
(0/12 pairs, in-family 0/3, 9/12 pilot repositories with zero framework recognition).
|
||||
Learned components have nothing constraining them, so DSPy fitted a shape shortcut
|
||||
(deletion count separates reversed fixes at 0.90). The root cause: the core security
|
||||
object — one deterministic, checkable artifact per change that makes a defect claim
|
||||
provable (with witness) and a non-claim bounded (with coverage) — was implicit, never
|
||||
specified.
|
||||
|
||||
## Current State
|
||||
|
||||
- The ordered trace exists for one family: `access-control-prover` implements entry →
|
||||
hops → operation → witnesses → dominance → three-way judgment, with digests and the
|
||||
Req 12 admission workflow (dev pool + untouched admission pool). 13.3's four-arm run
|
||||
proved attribution works around the trace (analyzer_only −246 unguarded).
|
||||
- Measured ceilings of the chain: entry recognition (9/12 pilot repos unrecognized),
|
||||
call resolution (11.9% complete), guard dominance coarseness, 120s pre-push budget
|
||||
(whole-repo CPGs do not fit: libpng 446s, 637-file PMPro does not load).
|
||||
- `valid-detection-benchmark` provides the measurement instrument; `detection-quality.md`
|
||||
(steering, 2026-10-30) holds standing rules and pre-registered stop conditions.
|
||||
|
||||
## Desired Outcome
|
||||
|
||||
One specified core object — the security trace — with a contract that every detection arm,
|
||||
fact pack, and consumer reuses: given a push (base, head) within a fixed budget, output
|
||||
proven defect claims (each with its witness) plus explicit coverage of what was not
|
||||
decided, on repositories never seen. No fitted component ever sees pool labels.
|
||||
|
||||
## Approach
|
||||
|
||||
Specify the trace as the core: scope (change regions, hard budget) → recognize (counted
|
||||
entry/operation coverage) → resolve (every call in one completeness bucket) → trace
|
||||
(entry → hops → operation → witnesses → dominance → judgment → digests) → verdict (proven
|
||||
claim | bounded silence). Models are bounded to residuals: zero-shot judge on the trace,
|
||||
hunter proposes trust boundaries, proof arm demonstrates (only BLOCK evidence). Admission
|
||||
on an untouched population gates the trace itself.
|
||||
|
||||
## Scope
|
||||
|
||||
- **In**: trace schema and witness algebra; three-way judgment; budget/scoping contract;
|
||||
coverage accounting (funnel, resolution buckets, named incompleteness reasons); digest
|
||||
and provenance fields; admission-gate contract (Req 12 workflow generalized); the
|
||||
anti-shortcut rule for any fitted component (reversed-pair probes, per-repo holdout,
|
||||
untouched-population admission); re-parenting map of existing specs onto the core.
|
||||
- **Out**: new vulnerability families; per-framework fact content (that is fact-pack work,
|
||||
e.g. recognition-coverage table task 7.1 in valid-detection-benchmark); the pre-push
|
||||
transaction itself (pre-push-safety-net consumes the core); engine internals.
|
||||
|
||||
## Boundary Candidates
|
||||
|
||||
- Core trace object and verdict contract (this spec).
|
||||
- Fact packs: per-family recognition/operation/guard facts, admission-gated (extension
|
||||
point, starting with access_control from access-control-prover).
|
||||
- Consumers: pre-push-safety-net (push transaction), search-with-proof (BLOCK evidence),
|
||||
hunter-learning-loop (proposal lane only).
|
||||
|
||||
## Out of Boundary
|
||||
|
||||
- The pre-push hook transaction and rollout policy.
|
||||
- New detection families beyond what the trace contract enables.
|
||||
- Fitted models of any kind in the core path.
|
||||
- Engine/CPG internals (contributor-scan owns those).
|
||||
|
||||
## Upstream / Downstream
|
||||
|
||||
- **Upstream**: `valid-detection-benchmark` (measurement instrument, dev + admission
|
||||
pools), `access-control-prover` (trace implemented for access_control), steering
|
||||
`detection-quality.md` (standing rules and stop conditions).
|
||||
- **Downstream**: `pre-push-safety-net` (M-series milestones), `search-with-proof`,
|
||||
`hunter-learning-loop`, future fact-pack specs per family/ecosystem.
|
||||
|
||||
## Existing Spec Touchpoints
|
||||
|
||||
- **Extends**: `access-control-prover` — its trace schema, judgment vocabulary and
|
||||
admission workflow become the core contract, generalized across families.
|
||||
- **Adjacent**: `pre-push-safety-net`, `search-with-proof`, `hunter-learning-loop`,
|
||||
`valid-detection-benchmark`, `contributor-scan` — consumers/instruments; must not
|
||||
duplicate the trace contract.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Pool measures, never trains: no fitted prompt/threshold/model on pool labels
|
||||
(detection-quality.md rules 4-5, 13-proposal).
|
||||
- Fixed measurement schema: population + freeze digests, judgment vocabulary, rates with
|
||||
n and Wilson 95%, coverage, instrument failures, runtime.
|
||||
- Budget is measured, not static: per-change reply time and complexity are recorded, and
|
||||
the budget is derived from the measured distribution per workload class (sample, percentile,
|
||||
hardware disclosed); scoping to the change is part of the core contract.
|
||||
- Instrument rule: prove nonzero input reads before interpreting any zero.
|
||||
- Plain English in all documents; standard metrics only.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,392 @@
|
||||
# Requirements Document
|
||||
|
||||
## Project Description (Input)
|
||||
|
||||
**Who has the problem:** the maintainer of OpenUltraSAST. The intended detection loop is
|
||||
to augment family-specific model programs with deterministic program-analysis evidence.
|
||||
Today that loop cannot be extended safely because the shared core embeds authorization
|
||||
vocabulary, semantic gaps surface only during repository admission, and a clean-looking
|
||||
zero can still hide a failure before the model receives useful evidence.
|
||||
|
||||
**Current situation:** the instrument-verified 2026-10-31 authorization diagnosis showed
|
||||
two different failures. Framework-mediated reachability stopped before protected
|
||||
operations, and paths that survived still lacked the constraint semantics needed to
|
||||
distinguish vulnerable and fixed code. These are useful limits of the deterministic
|
||||
evidence producer, not a reason to require it to become a complete vulnerability prover.
|
||||
The core must expose such limits early and give each vulnerability family a stable way to
|
||||
interpret the evidence without translating its semantics into authorization terms.
|
||||
|
||||
**What should change:** the detection core shall provide a family-neutral evidence and
|
||||
verdict envelope. Deterministic analysis shall contribute typed transitions, effects,
|
||||
provenance, and explicit completeness. Each vulnerability family shall define its security
|
||||
object and decision vocabulary and may use a bounded model program to answer residual
|
||||
questions over that evidence. A staged readiness contract shall stop a family at the first
|
||||
failing semantic layer, before population-level admission is attempted.
|
||||
|
||||
**Evaluation-admission V3 amendment, 2026-11-06:** the validated version-two admission
|
||||
contract pools all claimed scopes and represents one arm. The approved replacement benchmark
|
||||
requires a new contract that evaluates multiple component and combined arms under one population
|
||||
spend while preserving an independently interpretable result for every exact vulnerability-family
|
||||
and ecosystem scope. Existing version-two evidence remains historical and unchanged.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: the family-neutral evidence, outcome, trace, coverage, provenance, budget,
|
||||
staged-readiness, admission, measurement, fact-pack, model-handoff, and consumer
|
||||
contracts; family conformance requirements; failure-visible semantic-stage accounting;
|
||||
and the versioned exact-scope, multi-arm evaluation-admission validation and replay contract.
|
||||
- **Out of scope**: vulnerability-family detection algorithms and prompt content;
|
||||
framework-specific fact content; model optimization; analysis of vendored dependency
|
||||
source; the pre-push transaction and rollout policy; proof execution; enabling a new
|
||||
vulnerability family for users; population sourcing, curation, reservation, licensing,
|
||||
and lifecycle; durable object-store or compare-and-swap adapters and bucket administration;
|
||||
detector execution or isolation; observation storage, access enforcement, and retry
|
||||
orchestration; workflow heartbeat and status presentation; and operator notifications.
|
||||
- **Adjacent expectations**: the existing deterministic analysis is an evidence producer,
|
||||
not a required complete prover; family-specific DSPy programs may consume the evidence
|
||||
packet under the model-handoff contract; proof-based blocking remains a separate
|
||||
consumer. Benchmark-owned workflows supply the reviewed population, lifecycle, custody,
|
||||
sealed-observation, access-control, and post-spend release evidence that the core validates;
|
||||
the core does not create or operate those workflows. A parked arm remains parked unless a
|
||||
separately reviewed strategy creates a new arm, baseline, and admission population.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Family-neutral evidence record
|
||||
The detection core shall define one canonical evidence record for every examined security
|
||||
question within an analyzed change.
|
||||
1. When a security question is analyzed, the detection core shall produce a canonical
|
||||
evidence record carrying the family identity, security-object identity, analyzed scope,
|
||||
ordered semantic transitions, observations with provenance, completeness reasons,
|
||||
family decision, neutral outcome, evidence basis, and input digests.
|
||||
2. When a transition or observation is derived from product code or reviewed semantic
|
||||
knowledge, the detection core shall record its semantic role, source location,
|
||||
provenance, and completeness independently of every other transition or observation.
|
||||
3. When a family supplies family-specific evidence fields, the detection core shall
|
||||
preserve those fields under the declared family schema without requiring them to use
|
||||
another family's vocabulary.
|
||||
4. The detection core shall serialize every evidence record deterministically, so that
|
||||
identical inputs produce byte-identical records.
|
||||
|
||||
### Requirement 2: Neutral outcome and evidence basis
|
||||
The detection core shall separate a family-specific decision from the neutral outcome and
|
||||
the basis supporting that outcome.
|
||||
1. When a family reaches a decision, the detection core shall record exactly one neutral
|
||||
outcome of violation, satisfied, or indeterminate and shall retain the family's own
|
||||
decision code separately.
|
||||
2. When an outcome is satisfied, the detection core shall report the exact bounded scope
|
||||
over which satisfaction was established and shall not present it as repository-wide
|
||||
safety.
|
||||
3. When evidence is incomplete for the family decision, the detection core shall return
|
||||
indeterminate with every named incompleteness reason.
|
||||
4. If a required semantic relation is unavailable or ambiguous, the detection core shall
|
||||
prevent that missing relation from creating a violation or satisfied outcome.
|
||||
5. When an outcome is reported, the detection core shall identify whether its basis is
|
||||
deterministic analysis, model corroboration, or execution-backed proof.
|
||||
|
||||
### Requirement 3: Vulnerability-family conformance contract
|
||||
The detection core shall require every vulnerability family to declare the meaning and
|
||||
limits of the evidence it consumes and the decisions it produces.
|
||||
1. When a family is registered, the detection core shall require its family and schema
|
||||
versions, claimed ecosystems, security-object definition, accepted semantic roles,
|
||||
decision vocabulary, neutral-outcome mapping, completeness conditions, and named
|
||||
indeterminate reasons.
|
||||
2. When a family claims a violation or satisfied outcome, the detection core shall require
|
||||
the family contract to identify the minimum evidence and completeness conditions for
|
||||
that outcome.
|
||||
3. When a family receives evidence outside its declared ecosystem or schema scope, the
|
||||
detection core shall report the question as unsupported rather than applying a
|
||||
different family's interpretation.
|
||||
4. If a family decision code, evidence field, or semantic role is not declared by the
|
||||
family contract, the detection core shall refuse the result.
|
||||
5. The detection core shall demonstrate conformance with at least two reference families
|
||||
that use materially different security objects before the family-neutral contract is
|
||||
declared ready for downstream family specifications.
|
||||
|
||||
### Requirement 4: Typed semantic transitions and effects
|
||||
The detection core shall preserve reviewed semantic knowledge needed to connect product
|
||||
code across framework and dependency boundaries without analyzing vendored source.
|
||||
1. When reviewed semantic knowledge supplies a registration, invocation, call, return,
|
||||
value transformation, operation effect, or constraint effect, the detection core shall
|
||||
record the typed relation and the supplying fact identity in the evidence record.
|
||||
2. When a typed relation is complete only under stated conditions, the detection core shall
|
||||
retain those conditions and shall mark the relation incomplete when they are not met.
|
||||
3. If multiple reviewed relations conflict or match ambiguously, the detection core shall
|
||||
report the ambiguity and shall not choose a complete relation silently.
|
||||
4. When vendored source is excluded, the detection core shall require a reviewed semantic
|
||||
relation for any dependency behavior needed by a determinate family decision.
|
||||
|
||||
### Requirement 5: Coverage and instrument accounting
|
||||
The detection core shall account for every input and semantic stage it did or did not use,
|
||||
so a zero result is never indistinguishable from unread or unmodeled input.
|
||||
1. When a change is analyzed, the detection core shall report counts for inputs read,
|
||||
security questions formed, semantic sites recognized, transitions materialized,
|
||||
security objects constructed, constraints evaluated, determinate outcomes, and findings
|
||||
emitted.
|
||||
2. When product-code or fact-derived relations are considered, the detection core shall
|
||||
assign each relation exactly one complete, incomplete, or unresolved outcome.
|
||||
3. If a semantic site, transition, security object, or constraint cannot be modeled, the
|
||||
detection core shall report the specific missing relation rather than interpreting its
|
||||
absence as safe.
|
||||
4. If any tracked input file is read as zero bytes or cannot be opened, the detection core
|
||||
shall fail the run as an instrument failure rather than reporting a clean result.
|
||||
5. When a stage produces zero outputs, the detection core shall identify the last stage
|
||||
with verified nonzero input and the first stage that failed to produce its expected
|
||||
output.
|
||||
|
||||
### Requirement 6: Measured budget and change scoping
|
||||
The detection core shall derive its time budget from measurement, not from a fixed constant.
|
||||
1. When a change is analyzed, the detection core shall measure and record reply time and
|
||||
change complexity, including changed regions, transition count, and changed size.
|
||||
2. When a workload-class budget is published, the detection core shall disclose the sample
|
||||
count, percentile, reference workload, reference hardware, and derived budget.
|
||||
3. When a change exceeds its measured workload-class budget, the detection core shall fail
|
||||
the budget gate naming the change, elapsed time, and budget derivation exceeded.
|
||||
4. While a push is analyzed, the detection core shall limit analysis to regions affected
|
||||
by the change and shall record the scope it used.
|
||||
|
||||
### Requirement 7: Verdict contract
|
||||
The detection core shall distinguish supported defect claims, bounded satisfaction, and
|
||||
bounded silence.
|
||||
1. When the detection core reports a defect, the claim shall carry its evidence basis, an
|
||||
exact location tied to the change, and the complete evidence record supporting the
|
||||
claim.
|
||||
2. When the detection core reports satisfaction, the claim shall carry the completed
|
||||
security questions and the bounded scope over which they were decided.
|
||||
3. When the detection core cannot decide a question, it shall report that question as
|
||||
indeterminate with its coverage reasons and shall never present it as clean.
|
||||
4. If a claim depends on an incomplete or unresolved semantic relation, the detection core
|
||||
shall expose the affected question as bounded silence unless another complete path
|
||||
independently supports the claim.
|
||||
|
||||
### Requirement 8: Staged readiness and promotion
|
||||
The detection core shall require a vulnerability-family arm to pass each semantic readiness
|
||||
stage before it can advance to broader evaluation.
|
||||
1. When an arm begins development, the detection core shall require separate readiness
|
||||
results for instrument validity, semantic recognition, transition construction,
|
||||
security-object and constraint evaluation, vulnerable/fixed pair behavior, development
|
||||
population behavior, fresh admission, and consumer integration.
|
||||
2. When a readiness stage is evaluated, the detection core shall require a positive case,
|
||||
a negative case, and an ambiguity or missing-evidence case appropriate to that stage.
|
||||
3. If a readiness stage fails, the detection core shall stop promotion at that stage and
|
||||
shall report the failing semantic layer and named correction lever.
|
||||
4. If any prerequisite readiness result is missing, stale, or failed, the detection core
|
||||
shall refuse to run a later population or integration gate as evidence of readiness.
|
||||
5. When a semantic change invalidates an earlier readiness result, the detection core shall
|
||||
mark every dependent later result stale.
|
||||
6. When a family passes a stage, the detection core shall preserve the fixtures and results
|
||||
as regression evidence without treating them as fresh admission evidence.
|
||||
|
||||
### Requirement 9: Model-augmentation contract
|
||||
The detection core shall support bounded family-specific model reasoning over deterministic
|
||||
evidence without allowing model output to rewrite that evidence.
|
||||
1. Where a family uses a model program, the detection core shall present the canonical
|
||||
evidence record, its completeness state, the family contract, and the residual question
|
||||
to be answered.
|
||||
2. When a model program returns a family decision, the detection core shall preserve the
|
||||
model response, program and configuration identities, evidence-record digest, and
|
||||
declared decision mapping.
|
||||
3. If deterministic evidence is incomplete, the detection core shall prevent a model
|
||||
response from converting the affected question into a satisfied outcome or a
|
||||
proof-backed violation.
|
||||
4. When model output supports an advisory claim, the detection core shall label the claim
|
||||
as model-corroborated and shall keep it distinct from deterministic and execution-backed
|
||||
claims.
|
||||
5. The detection core shall not allow a model to propose and independently adjudicate the
|
||||
same claim within one evidence path.
|
||||
6. The detection core shall reserve blocking verdicts for execution-backed proof with a
|
||||
differential witness.
|
||||
|
||||
### Requirement 10: Provenance and digests
|
||||
The detection core shall stamp every evidence record and verdict with the identities of its
|
||||
inputs.
|
||||
1. When an analysis is recorded, the detection core shall include the analyzer revision,
|
||||
family-contract digest, fact-set digest, acquisition-record digest, configuration
|
||||
digest, population digest, and per-change input digest.
|
||||
2. When an evidence record uses reviewed semantic knowledge, the detection core shall
|
||||
include the semantic pack and fact identities needed to reproduce each derived relation
|
||||
or effect.
|
||||
3. When a model program contributes a decision, the detection core shall include the model,
|
||||
program, prompt, and configuration identities needed to reproduce the call.
|
||||
4. If any recorded digest or semantic fact identity cannot be re-derived from the named
|
||||
inputs, the detection core shall refuse the result rather than accept it.
|
||||
|
||||
### Requirement 11: Reviewed semantic fact packs as the extension point
|
||||
The detection core shall admit family and ecosystem knowledge only through reviewed,
|
||||
versioned semantic fact packs over the family-neutral contract.
|
||||
1. When a semantic fact pack is admitted, the detection core shall validate its schema
|
||||
version, claimed family and ecosystem scope, stable fact identities, declared semantic
|
||||
roles, and review evidence.
|
||||
2. When a semantic fact matches analyzed product code, the detection core shall permit the
|
||||
consuming family to record the corresponding transition or effect only with the matching
|
||||
evidence and fact provenance attached.
|
||||
3. The detection core shall not allow a name-only match without the fact's required
|
||||
structural evidence to create a complete semantic relation or determinate outcome.
|
||||
4. If a semantic fact has an undeclared role, unknown field, invalid scope, missing review
|
||||
evidence, or inconsistent digest, the detection core shall refuse the pack.
|
||||
5. When admitted knowledge is insufficient to resolve a semantic relation, the detection
|
||||
core shall preserve the partial evidence record and report the relation as incomplete or
|
||||
unresolved.
|
||||
6. The detection core shall report, per arm, the vulnerability families and ecosystems the
|
||||
arm claims, so admission and rollout numbers are never read across unsupported scopes.
|
||||
|
||||
### Requirement 12: Admission gate contract
|
||||
The detection core shall gate every generalization claim on completed readiness stages and
|
||||
a reviewed untouched population whose denominator matches the arm's declared scope.
|
||||
1. When an arm is proposed for consumer use, the detection core shall require a reviewed
|
||||
manifest naming the frozen analyzer, family contract, model program where applicable,
|
||||
facts, acquisition, configuration, claimed families, claimed ecosystems, denominator
|
||||
policy, readiness results, and untouched admission population before any admission
|
||||
label is opened.
|
||||
2. The detection core shall consume admission manifests without producing, mutating, or
|
||||
re-labeling them and shall refuse manifests that are unreviewed, label-exposing,
|
||||
acquisition-changed, or spent.
|
||||
3. When an admission manifest is validated before label opening, the detection core shall
|
||||
verify that every pre-registered success threshold is arithmetically reachable under the
|
||||
declared family, ecosystem, and denominator policy.
|
||||
4. If a threshold cannot be reached even when every in-scope case succeeds, the detection
|
||||
core shall refuse the admission configuration rather than run or report a misleading
|
||||
no-go result.
|
||||
5. When a population contains cases outside an arm's declared scope, the detection core
|
||||
shall score quality gates on the pre-registered in-scope denominator and shall report
|
||||
whole-population coverage separately.
|
||||
6. When admission evidence is scored, the detection core shall report paired
|
||||
vulnerable/fixed behavior, ordinary false alarms, semantic-stage coverage, model-call
|
||||
outcomes where applicable, and every budget, instrument, and quality gate as an
|
||||
evidence-backed go or no-go result.
|
||||
|
||||
### Requirement 13: Separation of development and evaluation
|
||||
The detection core shall prevent evaluation labels from influencing an arm before its
|
||||
generalization claim is scored.
|
||||
1. The detection core shall prohibit prompts, examples, thresholds, fitted models, semantic
|
||||
facts, or decision mappings whose preparation data includes admission labels.
|
||||
2. When a fitted component is proposed, the detection core shall require reversed-pair
|
||||
probes, repository-held-out development evaluation, and admission on an untouched
|
||||
population before any generalization claim is made.
|
||||
3. When semantic knowledge is acquired, the detection core shall require source,
|
||||
documentation, or independently reviewed behavioral evidence that is isolated from
|
||||
vulnerability, fix-location, and ordinary-change labels.
|
||||
4. If evaluation labels influence semantic fact content, matching behavior, prompt
|
||||
content, examples, thresholds, or promotion, the detection core shall mark the affected
|
||||
population spent and shall refuse it for admission.
|
||||
|
||||
### Requirement 14: Consumer and re-parenting contract
|
||||
The detection core shall be the single contract that existing and future detection specs
|
||||
reuse.
|
||||
1. The detection core shall define the interface through which the pre-push transaction
|
||||
consumes evidence records and verdicts within its deadline.
|
||||
2. The detection core shall define the interface through which proof-based blocking
|
||||
evidence attaches to an existing claim.
|
||||
3. When an existing arm's behavior is described, the detection core shall require that
|
||||
description to be expressed over the family-neutral contract rather than as a separate
|
||||
runner contract.
|
||||
4. When a consumer receives bounded silence caused by missing semantic knowledge, the
|
||||
detection core shall require the consumer to preserve the named coverage reasons.
|
||||
5. When a family specification is created, the detection core shall require it to identify
|
||||
its family contract, readiness fixtures, development population, and admission boundary.
|
||||
|
||||
### Requirement 15: Fixed measurement schema
|
||||
The detection core shall require one fixed measurement schema for every detection number.
|
||||
1. When any detection result is reported, the detection core shall require population and
|
||||
freeze digests, claimed family and ecosystem scope, denominator policy, family decision
|
||||
vocabulary, neutral outcomes, evidence bases, every rate with its sample count and
|
||||
uncertainty interval, coverage, instrument failures, model-call failures, and runtime.
|
||||
2. When semantic relations participate in an arm, the detection core shall require the
|
||||
report to include sites considered, sites recognized, transitions completed, security
|
||||
objects constructed, constraints evaluated, determinate outcomes, and unresolved counts
|
||||
by named reason.
|
||||
3. When a measurement changes population, denominator policy, claimed scope, family
|
||||
contract, fact schema, model program, or outcome mapping, the detection core shall treat
|
||||
the result as a new baseline and shall not present it as a trend against the previous
|
||||
baseline.
|
||||
4. When a zero or clean-looking result is reported, the detection core shall require proof
|
||||
that every tracked input was read and that every prerequisite semantic stage executed.
|
||||
5. When a later-stage metric is reported, the detection core shall include the results and
|
||||
digests of every prerequisite readiness stage.
|
||||
|
||||
## Evaluation-Admission V3 Amendment
|
||||
|
||||
Requirements 16–20 extend Requirements 12 and 15 for a new evaluation-admission contract. Requirements 1–15 and completed version-two evidence retain their existing meaning, identities, and validation behavior; the amendment does not reinterpret or upgrade them.
|
||||
|
||||
### Requirement 16: Versioned evaluation-admission compatibility
|
||||
The detection core shall provide a new evaluation-admission contract for exact-scope, multi-arm qualification without changing the meaning or validation of historical admission artifacts.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a new evaluation admission is requested, the detection core shall validate it under a distinct contract version and shall keep every earlier admission contract independently replayable.
|
||||
2. The detection core shall preserve the accepted bytes, digests, decisions, and replay behavior of existing version-two admission artifacts.
|
||||
3. If pooled version-two evidence is presented as exact-scope version-three evidence, the detection core shall refuse the conversion and shall require a new baseline evaluated under the new contract.
|
||||
4. If an admission artifact names an unknown, mixed, or incompatible contract version, the detection core shall refuse it rather than infer a compatible shape.
|
||||
5. When a population, evaluation plan, arm identity, claimed scope, denominator policy, threshold set, judgment vocabulary, analyzer identity, fact identity, configuration identity, registered budget, execution identity, outcome mapping, or combination rule changes, the detection core shall require a new baseline identity.
|
||||
6. The detection core shall prevent an insufficient result, impaired result, aggregate result, incomplete arm result, raw report, or other partial artifact from authorizing consumer use.
|
||||
|
||||
### Requirement 17: Reviewed multi-arm plan and atomic spend authority
|
||||
The detection core shall bind every arm evaluated against one untouched population to a reviewed label-blind plan and one atomic admission spend.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When an evaluation plan is registered before label access, the detection core shall require a reviewed label-blind plan naming the population, input-set identity, every component arm, every combined arm, and every expected observation slot.
|
||||
2. When an arm enters an evaluation plan, the detection core shall require its immutable baseline identity, exact claimed family-and-ecosystem scopes, denominator policy, thresholds, sufficiency rules, outcome mapping, and combination lineage where applicable.
|
||||
3. When the label-blind plan is reviewed, the detection core shall evaluate threshold reachability and evidence sufficiency independently for every planned arm and exact claimed scope.
|
||||
4. If any planned arm or claimed scope cannot meet its registered minimum-size, threshold-reachability, or interval-precision rule, the detection core shall report that arm and scope as insufficient and shall refuse the shared population spend.
|
||||
5. When every planned observation is complete and before label access, the detection core shall require an admission authority reviewed independently from its producer that binds the feasible plan to the exact completed observation identities and the current population custody state.
|
||||
6. If an expected observation is absent, unreadable, changed, associated with another arm, input, or plan, or inconsistent with the feasible plan, the detection core shall refuse admission authorization.
|
||||
7. When shared labels are released, the detection core shall require one admission preparation, one exact monotonic population transition, and one resulting label-release receipt binding every planned arm.
|
||||
8. If the custody head is stale, the transition is not the exact available-to-spent successor, the receipt is absent or mismatched, or availability is later restored, the detection core shall refuse label release and scoring.
|
||||
9. The detection core shall not authorize a partial spend for a subset of planned arms or scopes.
|
||||
10. While the population remains unspent, the detection core shall permit admission reuse only when raw observations and population-specific diagnostics remain unavailable to detector development and only closed process status, counts, and digests are exposed.
|
||||
11. If raw observations or population-specific diagnostics are inspected, or an arm, plan, configuration, registered budget, or execution identity changes after an attempt, the detection core shall require a development or spent disposition and shall refuse that population for admission.
|
||||
12. When a pre-spend attempt is retried, the detection core shall accept the retry only under the exact unchanged population, plan, arms, inputs, configuration, registered budgets, and execution identities.
|
||||
|
||||
### Requirement 18: Exact-scope denominators and whole-population coverage
|
||||
The detection core shall derive qualification independently for each arm's exact vulnerability-family and ecosystem scope while preserving separate whole-population coverage.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When quality is evaluated, the detection core shall derive one complete standard metric set for every exact arm, vulnerability-family, and ecosystem scope declared in the reviewed plan.
|
||||
2. When recall or pair correctness is derived for a scope, the detection core shall use only vulnerable and fixed cases independently verified for that exact family and ecosystem.
|
||||
3. When ordinary false alarms are derived for an arm, the detection core shall count each eligible ordinary push once for every family claimed by that arm in the push's reviewed ecosystem and shall not project it into another ecosystem.
|
||||
4. When precision is derived for a scope, the detection core shall use only findings and cases assigned to that exact arm, family, and ecosystem under the reviewed denominator policy.
|
||||
5. When one underlying push contributes to multiple verified scoped denominators, the detection core shall count it once in whole-population coverage.
|
||||
6. When a scoped metric is reported, the detection core shall include its numerator, denominator, value, sample count, and Wilson 95% interval under the fixed metric algebra.
|
||||
7. Where an overall aggregate is reported, the detection core shall identify its constituent arms and scopes, sample counts, and weighting and shall not attach a gate or authorization decision to the aggregate.
|
||||
8. If a post-spend scoped denominator is zero or differs from the reviewed label-blind census and feasible plan, the detection core shall refuse the evaluation as invalid rather than report insufficient, go, or no-go.
|
||||
|
||||
### Requirement 19: Version-three case release, arm results, and proof binding
|
||||
The detection core shall validate post-spend evidence separately for every planned arm while preserving shared population custody and combination lineage.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When post-spend cases are released, the detection core shall require one version-three case-release graph reviewed independently from its producer and bound to the reviewed evaluation plan, shared spend, and exact completed observations.
|
||||
2. When a case is released for an arm, the detection core shall bind its arm identity, exact scope, role, pair identity, result, evidence, change inputs, reviewed input manifest, and release review.
|
||||
3. If a version-two case release is supplied to a version-three evaluation, the detection core shall refuse it rather than reinterpret it under the new contract.
|
||||
4. When a combined arm is evaluated, the detection core shall preserve every component arm's observations, metrics, gates, and decision and shall report the registered combination lineage.
|
||||
5. If a combined result omits, replaces, or contradicts a required component result, the detection core shall refuse the combined evaluation.
|
||||
6. While ground-truth roles remain sealed, the detection core shall treat every label-blind proof candidate as non-authoritative for demonstrated status.
|
||||
7. When a proof-backed status is derived after case release, the detection core shall require an exact validated proof attachment matching the released family, scope, pair roles, change inputs, finding location, differential record, control binding, producer, verification receipt, and trust policy.
|
||||
8. When a combined arm retains proof evidence, the detection core shall require the identical validated proof candidate from the matching component observation and shall prevent the combined arm from creating or transferring proof evidence.
|
||||
9. If proof is absent or inconclusive, the detection core shall retain the finding as not demonstrated.
|
||||
10. If a proof attachment is role-swapped, cross-family, cross-scope, location-mismatched, or bound to unrelated inputs or controls, the detection core shall refuse the attachment as invalid and shall not count it as demonstrated.
|
||||
11. The detection core shall not allow one planned arm's valid result to hide another planned arm's missing, invalid, or failed result.
|
||||
|
||||
### Requirement 20: Complete evaluation reports and replay authority
|
||||
The detection core shall make the complete multi-arm evaluation replay the only admission artifact capable of authorizing downstream consumption.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a planned arm completes evaluation, the detection core shall report one go or no-go decision for every exact claimed scope from that arm's validated instrument, budget, readiness, and quality gates.
|
||||
2. The detection core shall reserve go and no-go for completed quality evaluations and shall keep pre-spend insufficiency, unread input, skipped stages, incomplete arms, and other instrument failures distinct from quality decisions.
|
||||
3. When an evaluation report is produced, the detection core shall preserve per-arm scoped metrics separately from whole-population coverage, completed and deferred work, coverage funnels, instrument and model failures, runtime, and every degradation reason.
|
||||
4. When every planned arm has a valid report, the detection core shall produce one complete evaluation replay binding the reviewed plans, feasibility results, admission authority, shared custody transition and receipt, the version-three case-release graph, independently valid per-arm scoped reports, and combination lineage.
|
||||
5. If any planned arm report, mandatory gate, custody binding, case release, or replay component is missing or invalid, the detection core shall refuse evaluation completion.
|
||||
6. When an evaluation replay is validated, the detection core shall rederive identities, counts, metrics, gates, and decisions from the canonical source artifacts rather than trust caller-supplied summaries.
|
||||
7. When a consumer requests admission evidence, the detection core shall provide authorization only from a complete validated evaluation replay and shall not authorize from a per-arm report or aggregate alone.
|
||||
8. When a zero or clean-looking arm result is included, the detection core shall require nonzero input-read evidence and execution evidence for every required stage before accepting the replay.
|
||||
|
||||
## Out of Scope
|
||||
- The pre-push hook transaction, rollout policy, and capability registry.
|
||||
- Vulnerability-family algorithms, prompts, examples, and optimization programs.
|
||||
- Framework- or repository-specific semantic fact content.
|
||||
- Analysis or indexing of vendored dependency source.
|
||||
- Proof execution and sandbox orchestration.
|
||||
- Enabling a vulnerability family for advisory or blocking use.
|
||||
- Analysis-engine, frontend, graph, and family-internal implementation details.
|
||||
- Population sourcing, human curation, permanent reservation, licensing, and lifecycle operations.
|
||||
- Durable object-store or compare-and-swap adapter implementation, bucket administration, and retention configuration.
|
||||
- Detector execution and isolation, observation storage and access enforcement, and retry orchestration.
|
||||
- Workflow heartbeat and operator-status presentation, notification transport, and product rollout.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"feature_name": "detection-core",
|
||||
"created_at": "2026-10-30T13:55:30Z",
|
||||
"updated_at": "2026-11-06T12:21:47Z",
|
||||
"language": "en",
|
||||
"phase": "tasks-generated",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": true,
|
||||
"approval_note": "2026-11-06: Requirements and the merged V3 design were auto-approved by the maintainer's explicit -y request. Additive V3 task groups 12-18 passed the task-plan review and an independent graph sanity review; existing groups 1-11 remain historical V2 context, and blocked task 11.4 is not a V3 prerequisite. Tasks are approved for implementation."
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
# Implementation Plan
|
||||
|
||||
## Merge record
|
||||
|
||||
| Previous v1 item | Disposition in this plan |
|
||||
|---|---|
|
||||
| Completed groups 1–7 and 8.1 | Retained as regression substrate; their former requirement mappings do not count toward v2 coverage |
|
||||
| Previous task 8.2 | Permanently parked after admission NO-GO #3; no task below revives that arm |
|
||||
| Previous tasks 8.3–8.4 | Superseded by v2 consumer and proof conformance work; no rollout or live capability enablement is included |
|
||||
| Existing task 11.4 | Retained as blocked historical V2 validation; it is not a prerequisite for additive V3 tasks 12–18, which have explicit dependencies and their own compatibility gate |
|
||||
|
||||
## Task constraints
|
||||
|
||||
- New and amended code keeps McCabe complexity at most 10, functions at most 50 logical lines, and new modules at most 400 logical lines.
|
||||
- Core owns contracts and pure validation only; analyzers, model execution, proof execution, persistence, acquisition, and rollout remain with adjacent owners.
|
||||
- Every zero-result test first proves nonzero readable input; an unread input is an instrument failure.
|
||||
- Historical v1 artifacts remain byte-identical. V2 creates a new baseline and is never trended against v1.
|
||||
- The parked authorization arm remains parked. Admission orchestration uses synthetic/reference inputs only and does not consume a live population.
|
||||
- Existing target facts may be schema-migrated without expanding vocabulary or behavior.
|
||||
- Tasks 12–18 are the current additive V3 implementation scope; completed groups 1–11 remain historical V2 implementation context.
|
||||
- V2 aggregate roots and public routes remain frozen; V3 uses a distinct schema and adds no converter or inferred projection.
|
||||
- Benchmark-owned storage, compare-and-swap, execution, access, retry, and workflow orchestration remain outside core and are exercised only through synthetic or in-memory boundaries.
|
||||
|
||||
- [ ] 1. Establish migration-safe foundations
|
||||
- [x] 1.1 Freeze historical v1 artifacts and the reviewed migration inventory
|
||||
- Pin representative v1 trace and report bytes and hashes as immutable regression fixtures.
|
||||
- Provide an independent schema-routed read-only decoder that validates committed v1 artifacts without importing the compatibility namespace, constructing v2 determinate outcomes, or exposing a writer or converter.
|
||||
- Freeze the reviewed initial set of v1 readers and writers with stable entry identities, paths, symbols or capabilities, owner boundaries, and open migration status.
|
||||
- Preserve every inventory identity permanently and allow only monotone transitions from open to `v2_migrated` or `retired`; fail unreadable or zero-byte sources, additions, deletions, and rekeying.
|
||||
- Preserve v1 as migration input only and never treat it as v2 conformance or a comparable trend baseline.
|
||||
- Done when fixture tests detect any byte or hash change, the decoder validates both historical schemas read-only, every inventory entry resolves to a nonzero source and valid target, and schema, status, or addition mutations fail.
|
||||
- _Requirements: 14.3, 15.3_
|
||||
- [x] 1.2 Build the strict canonical codec and resolver-backed artifact boundary
|
||||
- Produce deterministic UTF-8 JSON with full SHA-256 identities, stable mapping order, and retained sequence order.
|
||||
- Refuse non-finite numbers, unsupported values, unknown schemas, duplicate logical identities, and malformed digests.
|
||||
- Validate resolved bytes, length, storage receipt, retention identity, trusted store, and configured size limits.
|
||||
- Provide deterministic in-memory resolver and receipt fixtures for later contract tests.
|
||||
- Done when identical values produce byte-identical output and each content or receipt mismatch fails before parsing.
|
||||
- _Requirements: 1.4, 10.4_
|
||||
|
||||
- [ ] 2. Implement canonical semantic, provenance, and evidence contracts
|
||||
- [x] 2.1 (P) Represent family-neutral semantic relations and observations
|
||||
- Cover registration, invocation, call, return, value transformation, operation effect, and constraint effect.
|
||||
- Preserve role, location, conditions, provenance, and independent complete, incomplete, or unresolved status.
|
||||
- Refuse conflicting complete candidates by producing explicit ambiguity rather than selecting one.
|
||||
- Done when contract tests exercise every relation kind and all three relation statuses with canonical identities.
|
||||
- _Requirements: 1.2, 4.1, 4.2, 4.3, 5.2_
|
||||
- _Boundary: Semantic Evidence_
|
||||
- _Depends: 1.2_
|
||||
- [x] 2.2 (P) Validate complete input and decision provenance
|
||||
- Stamp analyzer, family contract, fact set, acquisition, configuration, population, and change-input identities.
|
||||
- Require the closed named-input role set and resolve every referenced input through the trusted artifact boundary.
|
||||
- Keep model and proof identities outside the immutable deterministic packet.
|
||||
- Done when every stamp field has a tamper test and unreadable or substituted named input is refused.
|
||||
- _Requirements: 10.1, 10.4_
|
||||
- _Boundary: Provenance and Lineage_
|
||||
- _Depends: 1.2_
|
||||
- [x] 2.3 Enforce append-only population custody
|
||||
- Construct and validate contiguous reviewed-custodian receipt chains against the complete current log.
|
||||
- Require compare-and-swap requests to name the expected log head and derive the next sequence and predecessor.
|
||||
- Make spent status monotonic and reject old prefixes, omitted receipts, broken chains, and later availability claims.
|
||||
- Provide deterministic in-memory custody-port and custodian fixtures for lineage and admission tests.
|
||||
- Done when concurrent requests have one valid successor and no fixture can restore a spent population.
|
||||
- _Requirements: 12.2, 13.1, 13.4_
|
||||
- [x] 2.4 Bind lineage purposes to exact influenced artifacts
|
||||
- Require each lineage use to name its purpose, influenced artifact, source population, label access, and current-chain receipt.
|
||||
- Enforce required and optional use counts for prompts, examples, thresholds, fitted models, facts, matching behavior, mappings, and promotion.
|
||||
- Distinguish fact ancestry that forbids all label access from development lineage that may use a different spent population.
|
||||
- Refuse protected-population influence, redirected artifact identities, wrong purposes, and incomplete receipt history.
|
||||
- Done when matching subjects cannot mask a wrong influenced artifact and contaminated populations are deterministically refused.
|
||||
- _Requirements: 13.1, 13.3, 13.4_
|
||||
- [x] 2.5 Bind immutable evidence packets to neutral decisions
|
||||
- Preserve family identity, security object, analyzed scope, ordered semantics, observations, family fields, completeness, and provenance.
|
||||
- Keep family decision codes separate from violation, satisfied, and indeterminate outcomes.
|
||||
- Require bounded scope for satisfaction, named reasons for indeterminate outcomes, and a valid deterministic, model-corroborated, or execution-backed basis.
|
||||
- Prevent incomplete or ambiguous required relations from producing violation or satisfaction.
|
||||
- Done when positive, satisfied, and incomplete records round-trip canonically and every invalid packet/decision combination is refused.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 2.4, 2.5, 10.1_
|
||||
|
||||
- [ ] 3. Add vulnerability-family contracts and unlike reference families
|
||||
- [x] 3.1 Validate a closed vulnerability-family declaration
|
||||
- Require family/schema versions, ecosystems, security-object fields, roles, decisions, outcome rules, completeness conditions, residual questions, and indeterminate reasons.
|
||||
- Enforce typed family fields and exact ecosystem/schema scope.
|
||||
- Declare whether each role accepts product evidence or requires reviewed excluded-dependency facts.
|
||||
- Refuse undeclared fields, roles, decisions, reasons, or determinate outcomes without their minimum evidence.
|
||||
- Done when the family registration matrix accepts only declared combinations and reports unsupported scope without cross-family fallback.
|
||||
- _Requirements: 1.3, 2.1, 2.4, 3.1, 3.2, 3.3, 3.4, 4.4, 9.1_
|
||||
- [x] 3.2 Build exact family catalog resolution and capability declarations
|
||||
- Re-derive every contract and catalog digest and require canonical ordering and unique family/version/schema identities.
|
||||
- Resolve a contract only when family, digest, and ecosystem identify exactly one entry.
|
||||
- Validate that each family capability names its contract, readiness fixtures, development population, and admission boundary.
|
||||
- Done when duplicate/substituted catalog entries and each missing or corrupted capability field are refused.
|
||||
- _Requirements: 3.1, 3.3, 3.4, 14.5_
|
||||
- [x] 3.3 Create obligation and taint-flow reference conformance fixtures
|
||||
- Model an operation-plus-constraint family and an ordered source-to-sink family without target vocabulary.
|
||||
- Provide positive, negative, and ambiguity or missing-evidence cases for both semantic shapes.
|
||||
- Require each family to produce violation, bounded satisfaction, and indeterminate output through the same validators.
|
||||
- Done when both fixture suites pass without adding a family-specific field to any shared core contract.
|
||||
- _Requirements: 3.5, 8.2_
|
||||
|
||||
- [ ] 4. Expose operational coverage, budget, and readiness gates
|
||||
- [x] 4.1 (P) Account for instrument reads and every semantic-stage outcome
|
||||
- Count reads, questions, sites considered and recognized, transitions, security objects, constraints, determinate outcomes, and findings.
|
||||
- Put every considered relation in exactly one complete, incomplete, or unresolved bucket.
|
||||
- Fail unopened or zero-byte tracked input and diagnose the last verified nonzero stage and first missing expected output.
|
||||
- Done when stage-by-stage fixtures distinguish a valid zero from unread input and identify every injected first failure.
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 15.2, 15.4_
|
||||
- _Boundary: Coverage Funnel_
|
||||
- _Depends: 2.1, 2.5_
|
||||
- [x] 4.2 (P) Derive and enforce measured workload budgets and change scope
|
||||
- Record changed regions, semantic-transition count, changed bytes, and total reply time.
|
||||
- Derive workload-class budgets with sample count, percentile, reference workload, reference hardware, and canonical identity.
|
||||
- Resolve the changed-byte measurement and refuse missing, zero, non-finite, or incompatible complexity data.
|
||||
- Reject evidence and claim anchors outside the supplied affected scope and name the exceeded derivation on budget failure.
|
||||
- Done when class derivation is deterministic and scope escape or elapsed-budget excess yields a named gate failure.
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4_
|
||||
- _Boundary: Budget and Scope_
|
||||
- _Depends: 1.2, 2.5_
|
||||
- [x] 4.3 Record ordered readiness evidence with explicit evidence-use roles
|
||||
- Cover instrument, recognition, transitions, constraints, pairs, development, admission, and consumer integration in fixed order.
|
||||
- Bind each result to arm/family/ecosystem, analysis identity, population use, fixtures, predecessors, measurement, and correction lever.
|
||||
- Keep regression, development, repository-holdout, fresh-admission, and integration evidence uses non-interchangeable.
|
||||
- Done when each stage accepts the correct positive/negative/ambiguity triplet and rejects the wrong evidence-use role.
|
||||
- _Requirements: 8.1, 8.2, 8.6_
|
||||
- [x] 4.4 Stop promotion on failed, absent, or stale prerequisites
|
||||
- Authorize a stage only when every ordered predecessor is current and passed.
|
||||
- Derive transitive staleness from core schema, semantic vocabulary, family, facts, acquisition, configuration, model, and outcome mapping.
|
||||
- Return the first failing layer and named correction lever without mutating historical results.
|
||||
- Done when one changed identity makes all dependent later results stale and later population/integration gates refuse to run.
|
||||
- _Requirements: 8.3, 8.4, 8.5, 15.5_
|
||||
|
||||
- [ ] 5. Replace semantic extension points with reviewed fact packs
|
||||
- [x] 5.1 Validate versioned packs and independent review evidence
|
||||
- Bind schema, family, ecosystem, language, stable facts, declared roles, normalized content, and review evidence.
|
||||
- Accept source, official-documentation, or independently reviewed behavioral evidence under a trusted review policy.
|
||||
- Resolve conflict disclosures and require producer/reviewer independence where behavioral evidence is used.
|
||||
- Reject label-derived semantic-fact ancestry, unknown scope/roles, missing review, and inconsistent digests.
|
||||
- Done when the schema/scope/review matrix accepts only fully reviewed label-isolated packs.
|
||||
- _Requirements: 11.1, 11.4, 13.3_
|
||||
- [x] 5.2 Validate complete structural observations and matcher lineage
|
||||
- Match the declared requirement set, including qualified name, shape, receiver, target, argument, and configuration evidence as applicable.
|
||||
- Resolve every observation artifact and require its identifier set to equal the fact’s complete structural requirements.
|
||||
- Bind matcher revision, behavior digest, and current lineage to the exact evaluated product location.
|
||||
- Prevent name-only, partial, fabricated, ambiguous, or protected-population-contaminated matches from becoming complete.
|
||||
- Done when only a fully observed structural match with valid matcher lineage is eligible for effect application.
|
||||
- _Requirements: 10.2, 11.2, 11.3, 13.4_
|
||||
- [x] 5.3 Apply declared fact effects with reproducible provenance
|
||||
- Materialize only relation kinds, roles, source/target roles, conditions, and effects declared by the matched fact and family.
|
||||
- Attach exact pack, fact, product location, structural observation, matcher, and match-reference identities to every derived relation.
|
||||
- Preserve unmet conditions as incomplete evidence and conflicting eligible matches as unresolved ambiguity.
|
||||
- Mark label-influenced matching behavior or promotion as spent before the relation can participate in evaluation.
|
||||
- Done when valid matches yield canonical effects while partial and ambiguous cases remain non-determinate with named reasons.
|
||||
- _Requirements: 4.1, 4.2, 4.3, 10.2, 11.2, 11.3, 11.5, 13.4_
|
||||
- [x] 5.4 Revalidate fact-derived relations and enforce the dependency boundary
|
||||
- Re-open referenced packs, facts, match evidence, review policy, lineage, custody, and resolved bytes for every fact-derived relation.
|
||||
- Reject undeclared roles, invalid scope, inconsistent content, and fact provenance that cannot be reproduced.
|
||||
- Require reviewed semantic facts for determinate behavior attributed to excluded dependencies.
|
||||
- Preserve missing dependency knowledge as named incomplete evidence rather than product-derived safety.
|
||||
- Done when product-only dependency behavior is refused, a missing fact stays indeterminate, and the reviewed structurally matched equivalent is accepted.
|
||||
- _Requirements: 2.4, 4.4, 10.4, 11.4, 11.5_
|
||||
|
||||
- [ ] 6. Add bounded model handoff and semantic shortcut probes
|
||||
- [x] 6.1 (P) Preserve reproducible model requests, results, and responses
|
||||
- Present the canonical evidence, completeness snapshot, family declaration, and declared residual question without importing a model runtime.
|
||||
- Freeze provider, model, program, prompt, examples, fitted component, configuration, adapter, decision mapping, and input-bundle identities before execution.
|
||||
- Preserve normalized responses plus one bounded redacted inline value or trusted immutable artifact, including failures.
|
||||
- Done when matching success/failure fixtures round-trip and substituted request, packet, response, program, or mapping identities are refused.
|
||||
- _Requirements: 9.1, 9.2, 10.3_
|
||||
- _Boundary: Model Handoff_
|
||||
- _Depends: 2.4, 3.1_
|
||||
- [x] 6.2 Apply model results without rewriting deterministic evidence
|
||||
- Validate current prompt/example/fitted-model/mapping lineage against the evaluated population and declared artifacts.
|
||||
- Reject proposer/adjudicator role collision and results for another packet, family, residual, or mapping.
|
||||
- Allow model-corroborated output only when deterministic residual prerequisites are complete.
|
||||
- Prevent model output from creating semantic relations, satisfaction over missing evidence, or execution-backed basis.
|
||||
- Done when accepted derivation leaves the evidence packet byte-identical and every incomplete or contaminated path remains indeterminate.
|
||||
- _Requirements: 2.4, 2.5, 9.3, 9.4, 9.5, 13.1_
|
||||
- [x] 6.3 (P) Evaluate independently reviewed equal-semantics reversed-surface probes
|
||||
- Derive semantic projections from records and family contracts rather than trusting caller summaries.
|
||||
- Resolve distinct surface artifacts and independently reviewed semantic-equivalence evidence.
|
||||
- Reject unequal semantics, digest-only review, non-finite tolerances, and fabricated passed summaries.
|
||||
- Detect a surface-shape component while accepting a relation-based component within its pre-registered tolerance.
|
||||
- Done when probe results are reproducible solely from canonical probe inputs and review policy.
|
||||
- _Requirements: 13.2_
|
||||
- _Boundary: Probes_
|
||||
- _Depends: 3.3, 5.1_
|
||||
|
||||
- [ ] 7. Compose validated evidence into bounded decisions and consumer output
|
||||
- [x] 7.1 Revalidate deterministic and fact-derived records through one shared context
|
||||
- Bind the exact catalog, packs, model declarations, lineage set, review policies, provenance trust, and custody policy.
|
||||
- Resolve the family contract and all seven named provenance inputs for each raw record.
|
||||
- Re-run family and fact validation and reject embedded pack, lineage, or named-input substitutes.
|
||||
- Done when raw, forged, and context-mismatched records cannot reach admission or consumer decision code.
|
||||
- _Requirements: 4.4, 10.1, 10.2, 10.4, 12.6, 14.1_
|
||||
- [x] 7.2 Replay model derivation at the shared validation seam
|
||||
- Require deterministic records to omit model derivation and model-corroborated records to include both request and result.
|
||||
- Re-run model application from the pre-model record and compare the derived record canonically.
|
||||
- Refuse constructible “validated” markers or model programs outside the context.
|
||||
- Done when admission and consumer tests exercise the same validator and neither can bypass it with a raw record.
|
||||
- _Requirements: 9.2, 9.3, 9.4, 9.5, 10.3, 10.4, 12.6, 14.1_
|
||||
- [x] 7.3 (P) Derive defect claims, bounded satisfaction, and bounded silence
|
||||
- Require exact change-tied locations and complete evidence for defect claims.
|
||||
- Carry completed question identities and exact scope for satisfaction.
|
||||
- Preserve named coverage reasons and record identities for every undecided question.
|
||||
- Keep incomplete paths silent unless a separate complete path independently supports a claim.
|
||||
- Done when mixed complete/incomplete fixtures produce supported claims plus bounded silence without any unbounded clean result.
|
||||
- _Requirements: 2.2, 2.3, 7.1, 7.2, 7.3, 7.4, 14.4_
|
||||
- _Boundary: Verdict and Proof_
|
||||
- _Depends: 2.5, 3.1_
|
||||
- [x] 7.4 Validate differential proof attachment without enabling rollout
|
||||
- Re-derive reviewed producer, sandbox, oracle, affected/safe side, control, witness, and verification identities.
|
||||
- Resolve all proof artifacts under the supplied producer, reviewer, store, and byte-limit trust policy.
|
||||
- Upgrade basis only for demonstrated affected-only behavior tied to the same family, packet, location, and change/control.
|
||||
- Reject opaque, self-asserted, inconclusive, same-side, or mismatched proof records.
|
||||
- Done when only the valid differential fixture yields execution-backed basis and no attachment changes rollout policy.
|
||||
- _Requirements: 2.5, 9.6, 14.2_
|
||||
- [x] 7.5 Integrate validated records, proof, scope, and budget at the core consumer edge
|
||||
- Accept raw validation envelopes and call the shared validator before creating any output.
|
||||
- Preserve every bounded-silence reason and reject records or anchors outside the supplied change scope.
|
||||
- Apply optional proof attachments through the proof validator and include all processing in total elapsed time.
|
||||
- Return defects, satisfactions, silence, input identities, complexity, and measured budget identity.
|
||||
- Done when a synthetic push returns a bounded verdict and scope, trust, or deadline violations fail explicitly.
|
||||
- _Requirements: 6.4, 14.1, 14.4_
|
||||
- _Depends: 4.2, 7.2, 7.4_
|
||||
|
||||
- [ ] 8. Implement fixed measurement and leakage-safe admission
|
||||
- [x] 8.1 (P) Define comparable baseline identities and fixed metric algebra
|
||||
- Bind arm, population/freeze, claimed scopes, exact contract/fact/model sets, acquisition, analyzer, configuration, denominator, threshold set, and outcome mapping.
|
||||
- Define closed numerator/denominator rules for pair correctness, recall, fixed and ordinary false alarms, precision, semantic coverage, and model success.
|
||||
- Require Wilson 95 intervals with numerator and denominator; serialize zero denominators as null and prevent them from passing.
|
||||
- Treat any identity mutation as a new baseline and refuse cross-arm or cross-scope aggregation.
|
||||
- Done when a baseline mutation matrix and two-arm isolation fixture distinguish every non-comparable result.
|
||||
- _Requirements: 11.6, 15.1, 15.3_
|
||||
- _Boundary: Measurement_
|
||||
- _Depends: 4.1, 4.4_
|
||||
- [x] 8.2 Build complete measurement records and typed repository-holdout reports
|
||||
- Record family decision vocabulary, neutral outcomes, evidence bases, fixed metrics, semantic funnel, model outcomes/failures, instrument failures, runtime, and readiness ancestry.
|
||||
- Require every later-stage record to carry current prerequisite readiness stage results and digests.
|
||||
- Define a typed repository-holdout report with an explicit evidence-use role, training and holdout manifest references, repository identities, fitting lineage, and distinct measurement/report identities.
|
||||
- Refuse zero or clean-looking results unless tracked input reads and prerequisite semantic stages passed.
|
||||
- Done when missing metrics, counts, failure fields, readiness ancestry, or holdout-report identity fields invalidate the corresponding record.
|
||||
- _Requirements: 13.2, 15.1, 15.2, 15.4, 15.5_
|
||||
- [x] 8.3 Validate independently reviewed census and manifest payloads
|
||||
- Canonically bind every substantive census and manifest field into separate review-subject digests.
|
||||
- Require producer/reviewer separation, admitted reviewer identity, allowed identity pairing, and resolver-validated conflict disclosure.
|
||||
- Reject self-review, copied attestations, unknown reviewers, malformed review digests, and unreadable disclosures.
|
||||
- Preserve aggregate census cardinalities without exposing case identities or per-case labels.
|
||||
- Done when only independently attested label-blind payloads can proceed to manifest validation.
|
||||
- _Requirements: 12.1, 12.2_
|
||||
- [x] 8.4 Validate exact admission context, readiness, scope, sets, and threshold lineage
|
||||
- Bind the exact evidence-validation context, reviewed census, denominator, thresholds, readiness results, untouched population, and current custody head.
|
||||
- Re-derive claimed scopes and exact contract, fact, model, threshold, and outcome-mapping sets from canonical objects.
|
||||
- Require current instrument-through-development readiness with matching analysis identity and evidence-use roles.
|
||||
- Validate prompt, example, fitted-model, fact, matcher, mapping, promotion, and threshold lineage before labels open.
|
||||
- Reject label exposure, acquisition drift, context substitution, stale readiness, scope mismatch, or prior spend.
|
||||
- Done when each refusal branch fails before feasibility and no digest-only set or lineage claim is accepted.
|
||||
- _Requirements: 12.1, 12.2, 13.1_
|
||||
- [x] 8.5 Refuse arithmetically impossible threshold configurations
|
||||
- Enumerate reachable counts from label-blind pair, case, semantic-site, and model-call capacities.
|
||||
- Evaluate count, rate, per-100, and Wilson-bound thresholds with the fixed comparator direction for each metric.
|
||||
- Require every grouped threshold to have at least one jointly attainable count assignment.
|
||||
- Done when the historical impossible-denominator case is a configuration refusal rather than a measured no-go.
|
||||
- _Requirements: 12.3, 12.4_
|
||||
- [x] 8.6 Validate the pre-label population-spend protocol inside core
|
||||
- Construct a label-release request bound to the exact validated authorization and current custody-log head.
|
||||
- Validate the returned receipt as the exact current-chain direct successor that changes the named population from available to spent.
|
||||
- Reject missing, stale, mismatched, non-successor, or availability-restoring receipts before scoring authorization.
|
||||
- Keep durable append, compare-and-swap execution, label release, persistence, crash handling, and concurrency outside core.
|
||||
- Done when only the exact spent successor authorizes scoring and every other receipt shape is refused without an external side effect.
|
||||
- _Requirements: 12.2, 13.4_
|
||||
- [x] 8.7 Validate the authenticated post-spend case release
|
||||
- Define canonical released-case bindings over case identity, scope, role, scoped pair identity, exact result digest, evidence-input digests, change-input digests, and input-manifest digest.
|
||||
- Bind the release to the population, manifest, authorization, preparation, exact current-chain spend receipt, custodian identity, and an independent review attestation.
|
||||
- Validate the release only after the exact spend successor and require one canonical binding for every supplied case and no others.
|
||||
- Reject missing, extra, duplicate, cross-scope, role-swapped, pair-swapped, result-swapped, or rehashed operational-payload substitutions before metric production.
|
||||
- Keep case identities and labels out of the pre-label census and make the external custodian, not the runner, authoritative for their post-spend association.
|
||||
- Done when the reproduced vulnerable/fixed payload swap and every release-field mutation fail before any admission metric is returned.
|
||||
- _Requirements: 12.2, 12.5, 12.6, 13.4, 15.1_
|
||||
- _Boundary: Admission Case Release_
|
||||
- [x] 8.8 Derive scoped admission metrics from canonical case evidence
|
||||
- Re-run shared evidence validation, instrument, budget, verdict, and proof validation for every case.
|
||||
- Consume only cases that exactly match the authenticated post-spend release; use its roles and scoped pairs while reconciling aggregate counts to the reviewed census.
|
||||
- Score quality only over pre-registered family/ecosystem scopes while retaining whole-population coverage.
|
||||
- Compute paired behavior, ordinary false alarms, semantic coverage, model outcomes, and all standard metrics without trusting runner aggregates.
|
||||
- Done when forged decisions, substituted contracts, invalid fact review, contaminated lineage, or case-count drift fail before metric production.
|
||||
- _Requirements: 12.5, 12.6, 15.1, 15.2_
|
||||
- _Depends: 8.7_
|
||||
- [x] 8.9 Assemble evidence-backed gates and the final admission report
|
||||
- Emit instrument, budget, readiness, and quality gates with reasons and supporting identities.
|
||||
- Bind the report to the validation context, authorization, pre-label spend receipt, authenticated case release, census, and complete case-result set.
|
||||
- Require every mandatory metric and gate before allowing GO; make model success mandatory when a model is baseline-configured or validated claimed-scope calls occur. Refuse undeclared claimed-scope calls before report production; make configured zero-call, missing-threshold, and failed declared-call cases NO-GO while ignoring out-of-scope-only calls.
|
||||
- Return an immutable admission replay bundle carrying the exact manifest, denominator, readiness, preparation, spend receipt, case release, cases, and derived report; give the bundle a canonical transport digest without treating it as a validation token.
|
||||
- Revalidate the replay against independently supplied current context, custody, and content resolution by re-running the complete derivation and requiring exact report and replay equality.
|
||||
- Keep model-corroborated, deterministic, and execution-backed counts separate.
|
||||
- Done when any missing/failed mandatory gate forces NO-GO, coordinated report/readiness/threshold/case substitutions are refused, and a complete passing synthetic fixture yields a reproducible replayed GO report.
|
||||
- _Requirements: 12.6, 15.1, 15.2, 15.4, 15.5_
|
||||
- _Depends: 8.8_
|
||||
- [x] 8.10 Authorize fitted components only from replayed probes and disjoint evaluation
|
||||
- Accept only the typed admission replay bundle from task 8.9, validate it first against current authoritative context/custody/content, and refuse raw admission reports or coordinated replay substitutions.
|
||||
- Resolve the component’s unique model declaration, family contract, review policy, and custody policy from the admission-bound context.
|
||||
- Re-evaluate every canonical probe input and require the derived result set to match the component evidence.
|
||||
- Consume the typed repository-holdout report from task 8.2 and validate its evidence-use role, training/holdout references, report identity, and fitting lineage.
|
||||
- Prove the report’s training and repository-held-out repository sets are disjoint.
|
||||
- Require an admission GO that was untouched before its bound label-release spend.
|
||||
- Done when fabricated summaries, caller-selected policy substitutes, overlapping repositories, contaminated lineage, or mismatched admission are refused.
|
||||
- _Requirements: 13.1, 13.2, 13.4_
|
||||
- _Depends: 8.2, 8.9_
|
||||
|
||||
- [ ] 9. Integrate adjacent producers and benchmark-owned orchestration
|
||||
- [x] 9.1 (P) Translate the parked authorization trace conservatively into v2 evidence
|
||||
- Move conversion out of the family trace domain and keep authorization vocabulary outside shared core modules.
|
||||
- Map missing per-hop knowledge to incomplete relations and indeterminate outcomes without behavior lifting.
|
||||
- Preserve existing parked-arm regression behavior and avoid any admission rerun or reactivation.
|
||||
- Done when adapter conformance matches the frozen v1 behavior while the authorization domain no longer imports v2 core types directly.
|
||||
- _Requirements: 3.5, 14.3_
|
||||
- _Boundary: Authorization Adapter_
|
||||
- _Depends: 3.3, 7.2_
|
||||
- [x] 9.2 (P) Translate hunt-owned model execution through the bounded handoff
|
||||
- Convert structured executor inputs and outputs to the canonical request/result contracts.
|
||||
- Keep prompt creation, provider calls, caching, parsing, and redaction in the hunt subsystem.
|
||||
- Reject mismatched declared results through core validation without importing DSPy into core.
|
||||
- Done when a fake executor success/failure pair crosses the adapter and core’s import census contains no model-runtime dependency.
|
||||
- _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 14.3_
|
||||
- _Boundary: Model Adapter_
|
||||
- _Depends: 6.2, 7.2_
|
||||
- [x] 9.3 (P) Translate demonstrated search results into trusted proof attachments
|
||||
- Map existing typed affected/safe verification records without moving sandbox or oracle execution into core.
|
||||
- Preserve producer, control, witness, storage, and verification identities required by proof validation.
|
||||
- Refuse inconclusive and mismatched fixtures at the adapter/core seam.
|
||||
- Done when a canonical demonstrated fixture attaches successfully and every negative proof shape remains at its prior evidence basis.
|
||||
- _Requirements: 9.6, 14.2, 14.3_
|
||||
- _Boundary: Proof Adapter_
|
||||
- _Depends: 7.4_
|
||||
- [x] 9.4 Accept downstream reviewed fact-adapter conformance evidence
|
||||
- Consume the immutable conformance fixture and manifest produced by the downstream family adapter without constructing its contract, packs, matches, authority, or target data in core.
|
||||
- Re-run only the family-neutral fact-pack, fact-match, relation-application, full-evidence, canonical-reference, and import-boundary checks named by that handoff.
|
||||
- Verify the manifest accounts for complete, name-only, partial, substituted, self-authored, protected-population-contaminated, and legacy-projection-only cases.
|
||||
- Done when the downstream handoff is accepted by generic validators and core remains free of target imports; adapter implementation and target-specific fixtures remain downstream-owned.
|
||||
- _Requirements: 10.2, 10.4, 11.1, 11.2, 11.3, 11.4, 11.5, 13.4, 14.3_
|
||||
- _Boundary: Fact Pack Adapter Conformance_
|
||||
- _Depends: 5.4_
|
||||
- [x] 9.5 (P) Produce readiness and development records through benchmark-owned adapters
|
||||
- Construct canonical readiness results while leaving sequencing and persistence with benchmark workflows.
|
||||
- Emit development observations with v2 identities, evidence-use roles, instrument receipts, and prerequisite ancestry.
|
||||
- Stop a runner before population work when readiness is failed, absent, or stale.
|
||||
- Done when development fixtures pass through v2 and an injected semantic identity change prevents the later run.
|
||||
- _Requirements: 8.1, 8.3, 8.4, 8.5, 8.6, 14.3, 15.5_
|
||||
- _Boundary: Benchmark Readiness Adapter_
|
||||
- _Depends: 4.4, 8.2_
|
||||
- [x] 9.6 Integrate benchmark-owned admission scoring and guarded custody
|
||||
- Delegate neutral scoring to core while retaining population persistence and run orchestration in the benchmark.
|
||||
- Make the custodian the sole append-only writer and combine durable compare-and-swap spend plus label release as one guarded operation.
|
||||
- After the winning spend, produce and independently attest the canonical case release over the exact immutable result, evidence, change, and input identities supplied to scoring.
|
||||
- Supply expanded manifest, lineage, resolver, and validation-context identities without exposing labels early.
|
||||
- Exercise durable append, label release, case-release production, crash-before-append, crash-after-append, and concurrent-winner behavior at this external boundary.
|
||||
- Done when synthetic admission completes end to end, injected crash/concurrency cases prove labels reach at most the successful compare-and-swap winner, and swapped operational payloads are refused by core.
|
||||
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 13.4_
|
||||
- _Depends: 8.9, 9.5_
|
||||
|
||||
- [ ] 10. Complete the single-writer v2 migration
|
||||
- [x] 10.1 Isolate every live v1 contract and writer behind the closed compatibility namespace
|
||||
- Move all authorization-shaped v1 values into one compatibility contract owner and all live v1 serialization into its sole writer facade.
|
||||
- Limit the compatibility namespace to the canonical primitive needed for byte stability and prevent every v2 module from importing it.
|
||||
- Preserve the frozen v1 wire representation while eliminating independent contract or serializer implementations elsewhere.
|
||||
- Done when old writes remain byte-identical, the compatibility namespace is the only live v1 contract and write owner, and ownership/import tests refuse any duplicate implementation.
|
||||
- _Requirements: 14.3, 15.3_
|
||||
- _Depends: 1.1_
|
||||
- [x] 10.2 Convert the temporary public and module compatibility surfaces into explicit shims
|
||||
- Keep the old named top-level API and designated compatibility modules as explicit imports from the isolated namespace during migration.
|
||||
- Remove legacy definitions and re-exports from mixed v2 modules so each compatibility name has exactly one owner.
|
||||
- Keep the canonical v2 paths private until the final atomic cutover.
|
||||
- Done when the old named API remains compatible before cutover, direct imports of moved legacy symbols from mixed modules fail, and no shim owns an independent implementation.
|
||||
- _Requirements: 14.3, 15.3_
|
||||
- [x] 10.3 Build the syntax-only migration-boundary instrument
|
||||
- Prove every scoped source file is readable and nonzero before interpreting a zero result.
|
||||
- Report explicit compatibility-namespace imports, designated shim imports, named top-level v1 symbols, writer-facade references, and module-valued or dynamic core access without evaluating alias or data flow.
|
||||
- Validate immutable inventory identities and their monotone open, `v2_migrated`, or `retired` status transitions.
|
||||
- During migration, distinguish the designated compatibility implementation from the production-consumer scope; reserve repository-wide zero for post-deletion validation.
|
||||
- Done when an adversarial syntax matrix reports the exact forbidden sites and `try`/`finally`, comprehension, assignment, and generic-call alias shapes cannot turn an explicit forbidden reference into a false zero.
|
||||
- _Requirements: 14.3, 15.3_
|
||||
- [x] 10.4 Migrate or retire remaining registered production source-tree consumers
|
||||
- Move each registered non-benchmark production reader and writer to the canonical v2 contract or retire it within its owning boundary.
|
||||
- Update the matching stable inventory entry to `v2_migrated` or `retired` in the same change without removing or rekeying it.
|
||||
- Keep authorization analysis, model execution, proof execution, and other adjacent behavior in their existing owner boundaries while changing only their contract seam.
|
||||
- Done when every source-tree entry owned by core or an adjacent adapter is terminal and the verified nonzero production-consumer scope outside the designated compatibility implementation has zero forbidden references.
|
||||
- _Requirements: 14.1, 14.3, 15.3_
|
||||
- _Boundary: Core and Adjacent Adapter Migration_
|
||||
- _Depends: 10.3_
|
||||
- [x] 10.5 Migrate benchmark readiness and admission consumers
|
||||
- Move every registered benchmark reader and writer to v2 while retaining readiness sequencing, persistence, population custody, and admission orchestration in the benchmark boundary.
|
||||
- Update each matching stable inventory entry to `v2_migrated` or `retired` without changing its identity.
|
||||
- Preserve guarded spend, authenticated case release, fixed measurement, and bounded model behavior through the migration.
|
||||
- Done when benchmark production scope has zero forbidden consumer references outside the designated compatibility implementation, every benchmark entry is terminal, and synthetic v2 readiness and admission complete end to end.
|
||||
- _Requirements: 8.1, 12.6, 14.3, 15.3_
|
||||
- _Boundary: Benchmark Readiness and Admission Adapters_
|
||||
- _Depends: 9.5, 9.6, 10.3_
|
||||
- [x] 10.6 Migrate or retire registered test and operational-tool consumers
|
||||
- Move authorization assumptions into legacy or adapter tests, keep family-neutral contract tests on v2, and retain frozen v1 bytes only for decoder regression.
|
||||
- Move or retire registered operational-tool consumers without adding a second compatibility or write path.
|
||||
- Mark the corresponding stable inventory entries `v2_migrated` or `retired` and require every original entry to reach a terminal status.
|
||||
- Done when the complete original inventory is terminal and the boundary instrument proves a nonzero repository scope with no forbidden consumer reference outside the designated compatibility implementation.
|
||||
- _Requirements: 14.3, 15.3_
|
||||
- _Boundary: Migration Tests and Operational Tooling_
|
||||
- _Depends: 10.4, 10.5_
|
||||
- [x] 10.7 Delete v1 write capability and publish the single v2 surface atomically
|
||||
- Require every inventory entry to be terminal and the migration-time production-consumer boundary to be zero before deletion begins.
|
||||
- Delete the compatibility namespace, v1 writer facade, and temporary shims while publishing the canonical v2 modules and top-level exports in the same integration change.
|
||||
- Prove old top-level and submodule imports and writes fail and that no v1 serializer or converter remains.
|
||||
- Retain the independent read-only decoder for frozen historical bytes and refuse comparison between v1 and v2 baseline identities.
|
||||
- After deletion, require repository-wide zero with no compatibility exclusion.
|
||||
- Done when isolated-tree and repository tests show old imports and writes fail, v2 imports and writes succeed, historical bytes still validate, and the final nonzero boundary scan remains zero.
|
||||
- _Requirements: 14.3, 15.3_
|
||||
- _Boundary: Single-Writer Cutover Integration_
|
||||
- _Depends: 10.6_
|
||||
|
||||
- [ ] 11. Validate readiness, failure visibility, and implementation boundaries
|
||||
- [x] 11.1 Run both reference families through the complete synthetic readiness path
|
||||
- Start from a verified nonzero input and run recognition, transitions, security object/constraints, pair, development, reviewed synthetic admission, guarded spend, and consumer verdict.
|
||||
- Produce violation, bounded satisfaction, and bounded silence for both unlike families.
|
||||
- Require current prerequisite readiness identities at every later stage.
|
||||
- Done when both authored repositories complete the path without target-specific core fields or a real admission population.
|
||||
- _Requirements: 3.5, 8.1, 8.2, 8.4, 8.6, 12.1, 14.1, 15.5_
|
||||
- [x] 11.2 Inject one failure at every semantic and governance stage
|
||||
- Exercise unread/zero input, missing recognition, incomplete transition, missing constraint, pair mismatch, failed development, contaminated admission, and refused integration.
|
||||
- Verify each run stops at the first failing stage with a named correction lever and no later metric or clean result.
|
||||
- Mutate a semantic identity and verify transitive staleness through all dependent readiness and measurement records.
|
||||
- Done when every injected failure is diagnosed at its source and no late admission-only surprise remains.
|
||||
- _Requirements: 5.3, 5.4, 5.5, 8.3, 8.4, 8.5, 15.4, 15.5_
|
||||
- [x] 11.3 Verify bounded validation cost and total consumer deadline accounting
|
||||
- Measure canonicalization and validation across increasing relation, observation, receipt, and case counts.
|
||||
- Exercise large transition sets for linear behavior and bounded memory without weakening semantic checks.
|
||||
- Include optional model and proof processing in the same measured consumer budget.
|
||||
- Publish sample count, percentile, workload, hardware, and derived budget through the fixed measurement contract.
|
||||
- Done when performance fixtures satisfy the derived workload envelope and any excess fails the named budget gate.
|
||||
- _Requirements: 1.4, 6.1, 6.2, 6.3, 6.4_
|
||||
- [ ] 11.4 Run the full migration, security, and regression gate
|
||||
- Verify core imports only standard-library and lower core layers and contains no target, DSPy, benchmark, proof-execution, or analyzer dependencies.
|
||||
- Run all unit, adapter, migration, reference-family, end-to-end, admission, and historical compatibility suites.
|
||||
- Enforce function/module complexity caps and scan for remaining v1 imports or writers.
|
||||
- Prove each test instrument read nonzero fixture bytes before interpreting zero findings or failures.
|
||||
- Done when the complete suite passes, boundary/import censuses are clean, historical bytes are unchanged, and all declared adapter and consumer seams pass conformance.
|
||||
- _Requirements: 1.4, 10.4, 14.3, 15.3, 15.4_
|
||||
- _Depends: 10.7_
|
||||
- _Blocked: Benchmark population governance conflict: the permanent unseen-pool reservation guard and the later spent/retired training-only pilot policy disagree. The benchmark owner must approve the authoritative lifecycle and replacement/retirement plan before the full suite can pass._
|
||||
|
||||
## Evaluation-admission V3 amendment
|
||||
|
||||
Tasks 12–18 implement Requirements 16–20 as an additive predecessor for the approved
|
||||
valid-detection-benchmark design. Existing groups 1–11 remain the historical V2
|
||||
implementation record. The unresolved downstream-governance item 11.4 is not reopened and
|
||||
is not a prerequisite for this V3 stream; V3 tasks depend explicitly on completed V2
|
||||
contract work and use synthetic/in-memory workflow boundaries only.
|
||||
|
||||
- [ ] 12. Preserve V2 admission and establish V3 measurement foundations
|
||||
- [ ] 12.1 (P) Freeze representative V2 admission behavior
|
||||
- Pin accepted canonical bytes, digests, decisions, validation failures, report output, and authoritative replay output for representative V2 admissions.
|
||||
- Prove every frozen fixture is readable and nonzero before treating a stable result as compatibility evidence.
|
||||
- Keep pooled V2 evidence replayable only under V2 and add no converter or inferred V3 projection.
|
||||
- Done when a byte, digest, decision, failure-order, or replay mutation fails the compatibility suite while the current V2 fixtures pass unchanged.
|
||||
- _Requirements: 16.1, 16.2, 16.3, 16.4_
|
||||
- _Boundary: V2 Admission Compatibility_
|
||||
- _Depends: 8.9, 8.10_
|
||||
- [ ] 12.2 Extract version-neutral metric arithmetic without changing V2
|
||||
- Isolate only the closed standard-metric algebra and Wilson calculation; keep every serialized V2 identity and validator in its existing owner.
|
||||
- Route V2 through the shared arithmetic and compare all frozen V2 outputs byte-for-byte.
|
||||
- Expose the same pure arithmetic to V3 without accepting a V2 policy, result, or report as a V3 value.
|
||||
- Done when both versions use one arithmetic implementation and the complete V2 compatibility fixture remains unchanged.
|
||||
- _Requirements: 16.2, 18.6_
|
||||
- _Boundary: Version-Neutral Metric Algebra_
|
||||
- _Depends: 12.1_
|
||||
- [ ] 12.3 Define exact arm-scope measurement and sufficiency contracts
|
||||
- Represent one canonical arm-and-scope key for every policy, threshold, sufficiency rule, metric, gate, and decision.
|
||||
- Bind exact denominator algebra, fixed Wilson intervals, sample counts, minimum sizes, interval precision, and rational aggregate weights.
|
||||
- Include every arm identity input that must start a new baseline when changed, including judgment vocabulary, analyzer, facts, configuration, registered budget, execution, outcome mapping, and combination rule.
|
||||
- Refuse duplicate, unsorted, unknown, cross-key, non-finite, or incomplete values through exact closed validation.
|
||||
- Done when canonical contract tests accept one complete component and combined-arm configuration and reject every single-field identity or arm-scope mismatch.
|
||||
- _Requirements: 16.1, 16.4, 16.5, 18.1, 18.6, 18.7_
|
||||
- _Boundary: Evaluation Measurement_
|
||||
- _Depends: 12.2_
|
||||
- [ ] 12.4 Establish distinct V3 version-boundary primitives
|
||||
- Reserve the exact evaluation-admission V3 schema identity and version-routing primitives without changing any V2 public name or route.
|
||||
- Make V3 contracts exact-type only and prohibit V2-to-V3 conversion, subclass acceptance, and mixed-version child relationships as each later root is introduced.
|
||||
- Define the shared non-authorizing marker for feasibility, insufficiency, raw reports, aggregates, incomplete arm results, and other partial values; leave complete root decoding to facade integration.
|
||||
- Done when boundary tests preserve exact V2 routing, recognize only the reserved V3 identity on the V3 path, and prove that a representative partial V3 value has no authorization capability.
|
||||
- _Requirements: 16.1, 16.3, 16.4, 16.6_
|
||||
- _Boundary: Evaluation Version Compatibility_
|
||||
- _Depends: 12.1, 12.3_
|
||||
|
||||
- [ ] 13. Register reviewed multi-arm plans and exact preflight
|
||||
- [ ] 13.1 Define component, combined-arm, and declarative combination configurations
|
||||
- Represent component and combined arm configurations with exact scopes, policies, thresholds, sufficiency rules, outcome mapping, and a closed declarative combination rule.
|
||||
- Require component identities and a finite normalized decision table for every combined arm, with identical-component-candidate-only proof retention.
|
||||
- Refuse missing components, duplicate arms, cross-key policies, and arbitrary executable combination behavior.
|
||||
- Done when canonical component and combined configurations validate and every component, rule-row, policy, threshold, sufficiency, or outcome-mapping mutation changes the configuration identity.
|
||||
- _Requirements: 16.5, 17.1, 17.2, 19.4_
|
||||
- _Boundary: Evaluation Arm Configuration_
|
||||
- _Depends: 12.3_
|
||||
- [ ] 13.2 Build the non-circular plan declaration, baselines, and expected slots
|
||||
- Derive the complete label-blind plan declaration before constructing per-arm baselines so the plan and baselines cannot form a digest cycle.
|
||||
- Bind each baseline to the population, freeze, input set, declaration, and exact arm configuration so any listed change creates a new baseline.
|
||||
- Define expected observation slots for every planned arm and input without including ground-truth roles or labels.
|
||||
- Done when plan reordering is canonical, every substantive mutation changes the declaration and affected baselines, and a combined arm cannot name an absent component.
|
||||
- _Requirements: 16.5, 17.1, 17.2_
|
||||
- _Boundary: Evaluation Plan_
|
||||
- _Depends: 13.1_
|
||||
- [ ] 13.3 Validate the reviewed label-blind census and plan manifest
|
||||
- Represent exact vulnerable/fixed scope capacities, ecosystem-specific ordinary-push capacities, semantic/model capacities, and deduplicated whole-population size without case labels.
|
||||
- Require an independently reviewed plan payload that binds the population, input set, arm configurations, baselines, readiness, custody head, census, and expected slots.
|
||||
- Resolve review evidence under the configured trust and byte limits and refuse self-review, copied attestations, exposed labels, missing scopes, or inconsistent population identities.
|
||||
- Done when one reviewed multi-arm manifest validates with labels sealed and every review, census, scope, ordinary-ecosystem, or slot substitution is refused before feasibility.
|
||||
- _Requirements: 17.1, 17.2, 17.3, 18.1, 18.3_
|
||||
- _Boundary: Evaluation Plan Review_
|
||||
- [ ] 13.4 Derive feasibility independently for every planned arm and exact scope
|
||||
- Produce exactly one feasibility row for the canonical product of planned arms and their claimed scopes.
|
||||
- Recompute minimum sample size, threshold reachability, and Wilson interval precision from the reviewed label-blind census for each row.
|
||||
- Derive shared spend eligibility only when every row is feasible; keep insufficiency and its reasons distinct from GO and NO-GO.
|
||||
- Refuse missing, extra, duplicated, or caller-summarized rows and prevent preparation when one row is insufficient.
|
||||
- Done when a single insufficient scope blocks the shared spend without opening labels, while the complete feasible plan yields one reproducible preflight digest.
|
||||
- _Requirements: 17.3, 17.4, 20.2_
|
||||
- _Boundary: Evaluation Feasibility_
|
||||
|
||||
- [ ] 14. Seal planned observations and authorize one atomic spend
|
||||
- [ ] 14.1 Validate exact nonzero observations against every expected slot
|
||||
- Represent label-blind observation headers that bind slot, arm, input, execution, registered budget, configuration, input-read evidence, stage-execution evidence, cases, and proof candidates.
|
||||
- Resolve every sealed observation through trusted content references and prove its bytes are nonzero before accepting its header.
|
||||
- Require one-to-one equality between expected slots and sealed observations; refuse missing, extra, changed, unreadable, cross-arm, cross-input, or duplicate values.
|
||||
- Done when the complete slot set validates and each single-slot removal, swap, truncation, zero-byte value, or identity mutation is refused before authority.
|
||||
- _Requirements: 17.5, 17.6, 20.8_
|
||||
- _Boundary: Evaluation Observation_
|
||||
- _Depends: 13.3_
|
||||
- [ ] 14.2 Validate reviewed attempt history, sealed access, and exact retries
|
||||
- Bind each attempt event to the complete retry identity, closed access policy, raw-observation and diagnostic access counts, public status projection, disposition, review, and predecessor.
|
||||
- Resolve the reviewed predecessor chain from its declared head and require every unspent attempt to have the identical population, plan, arms, inputs, configurations, budgets, and execution identities.
|
||||
- Permit public pre-spend projection only for closed status codes, counts, and digests; any raw/diagnostic access or identity change produces a terminal development-opened or spent disposition.
|
||||
- Enforce terminal monotonicity so a later sealed event cannot restore admission eligibility.
|
||||
- Done when an unchanged all-sealed retry chain validates and broken, forked, changed, exposed, self-reviewed, or terminal-then-sealed histories cannot authorize.
|
||||
- _Requirements: 17.10, 17.11, 17.12_
|
||||
- _Boundary: Evaluation Attempt History_
|
||||
- _Depends: 13.3_
|
||||
- [ ] 14.3 Validate the independently reviewed admission authority
|
||||
- Bind the feasible plan to the exact sealed observation set, attempt-history head, current readiness, validation context, and current complete custody head.
|
||||
- Require the admission-authority payload to be reviewed independently from its producer and resolve its review evidence before authorization.
|
||||
- Revalidate feasibility, all observation bytes, attempt history, readiness, and custody immediately before producing authority.
|
||||
- Refuse stale or restored availability, changed observations, missing review, and any difference from the reviewed plan rather than returning a partial authority.
|
||||
- Done when one canonical authority is derived only from the complete reviewed graph and every stale, substituted, or incomplete source fails before spend.
|
||||
- _Requirements: 17.5, 17.6, 17.8, 17.10, 17.11, 17.12_
|
||||
- _Boundary: Evaluation Authority_
|
||||
- _Depends: 13.4, 14.1, 14.2_
|
||||
- [ ] 14.4 Bind every planned arm to one preparation and custody receipt
|
||||
- Construct one head-bound label-release request whose influenced artifact transitively commits to every planned baseline and exact authority.
|
||||
- Validate only the exact monotonic available-to-spent successor and derive an explicit all-arm binding between preparation, receipt, and planned baselines.
|
||||
- Reject subset preparations, missing or mismatched receipts, stale concurrent attempts, broken custody chains, and any later restoration of availability.
|
||||
- Keep durable compare-and-swap and label release outside core while exercising the boundary with an in-memory custody port.
|
||||
- Done when one receipt authorizes all and only the planned arms, concurrent/stale losers fail, and no partial spend is constructible.
|
||||
- _Requirements: 17.7, 17.8, 17.9_
|
||||
- _Boundary: Evaluation Shared Spend_
|
||||
- _Depends: 14.3_
|
||||
|
||||
- [ ] 15. Authenticate V3 case release, combinations, and proof continuity
|
||||
- [ ] 15.1 Build the arm-aware V3 case and independently reviewed release graph
|
||||
- Bind each post-spend case to its arm-scope key, baseline, observation, underlying push, role, pair, result, evidence inputs, change inputs, input manifest, and proof-candidate identity.
|
||||
- Bind one independently reviewed release to the plan, admission authority, preparation, exact shared spend, and complete canonical case set.
|
||||
- Require exact planned-arm and released-case cardinality and exact V3 root types; refuse V2 releases and subclasses.
|
||||
- Require nonzero read and required-stage execution evidence in every released case before it can enter scoring.
|
||||
- Done when the complete V3 release validates and V2 substitution, missing arms, duplicate cases, wrong spend, or copied review fail before metric production.
|
||||
- _Requirements: 19.1, 19.2, 19.3, 19.11, 20.8_
|
||||
- _Boundary: Evaluation Release_
|
||||
- _Depends: 14.4_
|
||||
- [ ] 15.2 Revalidate exact role, pair, scope, result, and input associations
|
||||
- Recompute every case and binding digest from canonical evidence rather than accepting caller-supplied summaries.
|
||||
- Require vulnerable/fixed roles and scoped pair identities to form exact pairs; prohibit pair identities on ordinary cases.
|
||||
- Preserve the separately reviewed underlying-push identity for scoped projection and later whole-population deduplication.
|
||||
- Refuse arm, scope, role, pair, observation, result, evidence, location, input, or manifest swaps before any count is derived.
|
||||
- Done when a canonical multi-scope release passes and a mutation matrix proves every association mismatch is invalid rather than a failed quality gate.
|
||||
- _Requirements: 18.2, 18.5, 19.2, 19.10_
|
||||
- _Boundary: Released Case Validation_
|
||||
- [ ] 15.3 Replay combined-arm rules and preserve component observation lineage
|
||||
- Recompute combined observation outcomes from the registered finite decision table and exact component observation digests.
|
||||
- Preserve each component arm's observation identity and outcome independently of the combined result; defer completed metric, gate, decision, and report preservation to replay assembly.
|
||||
- Refuse missing, extra, replaced, reordered, or contradictory component material and any combined arm whose lineage differs from the reviewed plan.
|
||||
- Done when the registered combined arm replays deterministically from exact component observations and no component observation can hide another component's absence, contradiction, or lineage mismatch.
|
||||
- _Requirements: 19.4, 19.5, 19.11_
|
||||
- _Boundary: Combination Validation_
|
||||
- _Depends: 15.1, 15.2_
|
||||
- [ ] 15.4 Enforce proof-candidate identity and prevent proof transfer
|
||||
- Keep every label-blind proof candidate non-authoritative until the V3 release establishes its exact arm, family, scope, roles, inputs, and location.
|
||||
- Apply proof-backed status only through the existing trusted differential-proof validator and the identical released candidate.
|
||||
- Permit a combined arm to retain only a candidate already present in its named component observation; prevent creation, substitution, and cross-arm transfer.
|
||||
- Preserve absent or inconclusive proof as not demonstrated and refuse role-swapped, cross-family, cross-scope, location, input, control, producer, receipt, or trust-policy mismatches.
|
||||
- Done when valid released proof attaches once, absent proof remains non-demonstrated, and the complete transfer/mismatch matrix is refused.
|
||||
- _Requirements: 19.6, 19.7, 19.8, 19.9, 19.10_
|
||||
- _Boundary: Proof Continuity_
|
||||
- _Depends: 15.3_
|
||||
|
||||
- [ ] 16. Derive exact scoped metrics and completed reports
|
||||
- [ ] 16.1 Partition quality evidence by exact arm, family, and ecosystem
|
||||
- Select vulnerable and fixed cases only from the independently released exact scope and derive pair correctness without cross-scope reuse.
|
||||
- Derive precision only from findings and cases assigned to the same arm-scope key under its reviewed denominator policy.
|
||||
- Preserve family decision vocabulary, neutral outcomes, evidence bases, model outcomes, and semantic coverage independently for each key.
|
||||
- Done when a multi-arm, multi-scope case set yields isolated partitions and any cross-arm, cross-family, or cross-ecosystem contribution is refused.
|
||||
- _Requirements: 18.1, 18.2, 18.4_
|
||||
- _Boundary: Evaluation Scoped Metrics_
|
||||
- _Depends: 15.2, 15.4_
|
||||
- [ ] 16.2 Project ordinary pushes by ecosystem and deduplicate whole coverage
|
||||
- Count each eligible ordinary push once for every family claimed by the arm in the push's reviewed ecosystem and never in another ecosystem.
|
||||
- Deduplicate one underlying push once in whole-population coverage even when it contributes to several verified scope denominators.
|
||||
- Reconcile projected ordinary counts and global unique-push counts back to the reviewed census and released identities.
|
||||
- Done when same-ecosystem multi-family projection produces the expected scoped counts, cross-ecosystem projection is absent, and whole coverage counts each push once.
|
||||
- _Requirements: 18.3, 18.5_
|
||||
- _Boundary: Evaluation Ordinary Projection_
|
||||
- [ ] 16.3 Produce the complete scoped metric set and reject post-spend invalidity
|
||||
- Derive every standard metric for every arm-scope key using the fixed algebra and Wilson 95% calculation.
|
||||
- Publish numerator, denominator, sample count, value, and interval bounds for each scoped metric.
|
||||
- Treat a post-spend zero denominator, census difference, missing standard metric, or inconsistent projection as invalid evaluation evidence rather than insufficiency, GO, or NO-GO.
|
||||
- Done when complete nonzero scoped metrics match independent count fixtures and every zero/mismatch mutation fails before report construction.
|
||||
- _Requirements: 18.1, 18.6, 18.8_
|
||||
- _Boundary: Evaluation Metric Derivation_
|
||||
- [ ] 16.4 Derive completed per-scope decisions and per-arm operational evidence
|
||||
- Derive instrument, budget, readiness, and quality gates for every exact arm-scope result from canonical evidence.
|
||||
- Emit GO or NO-GO only when the scope evaluation is complete; keep insufficiency, unread input, skipped stage, incomplete arm, and instrument impairment outside quality decisions.
|
||||
- Preserve per-arm completed and deferred case identities, stage funnel, instrument/model failures, runtime, and closed degradation reasons separately from shared whole coverage.
|
||||
- Require nonzero input reads and required-stage execution before accepting a clean-looking result.
|
||||
- Done when every completed scope receives one reproducible decision and every incomplete or impaired fixture is refused without a quality decision.
|
||||
- _Requirements: 20.1, 20.2, 20.3, 20.8_
|
||||
- _Boundary: Evaluation Report_
|
||||
- [ ] 16.5 Derive optional aggregates that can never authorize
|
||||
- Bind aggregate constituents, sample counts, canonical rational weights, and derived metric values to exact arm-scope metric identities.
|
||||
- Require weights to be non-negative, canonical, one-to-one with constituents, and sum exactly to one through integer arithmetic.
|
||||
- Exclude gates, GO/NO-GO, proof promotion, and every consumer-authorization capability from the aggregate contract.
|
||||
- Done when a valid aggregate reproduces from its constituents and raw, incomplete, gated, decision-bearing, or differently weighted aggregates are refused as authorization evidence.
|
||||
- _Requirements: 16.6, 18.7, 20.7_
|
||||
- _Boundary: Evaluation Aggregate_
|
||||
|
||||
- [ ] 17. Close complete replay and replay-only consumer authorization
|
||||
- [ ] 17.1 Assemble one complete replay over the shared evaluation graph
|
||||
- Bind the reviewed plan, exact feasibility, reviewed admission authority, preparation, receipt, all-arm spend binding, attempt-history head, V3 release, cases, per-arm reports, combination lineages, shared whole coverage, and optional aggregate.
|
||||
- Require exact equality between planned arms/scopes and case/report identities, including every component required by a combined arm.
|
||||
- Prevent replay construction when any arm, scope, mandatory gate, custody binding, release, case, component, or lineage is missing or invalid.
|
||||
- Done when the complete graph produces one deterministic replay digest and every omitted or substituted component prevents construction.
|
||||
- _Requirements: 19.4, 19.5, 19.11, 20.4, 20.5_
|
||||
- _Boundary: Evaluation Replay_
|
||||
- _Depends: 15.3, 16.4, 16.5_
|
||||
- [ ] 17.2 Revalidate replay from canonical source artifacts and current trust state
|
||||
- Resolve the complete attempt chain and observation/review content again, revalidate current custody, and rederive every identity, count, metric, gate, decision, report, lineage, and replay digest.
|
||||
- Require exact equality with the carried values and refuse coordinated rehashing, stale external state, unknown/mixed versions, and partial roots.
|
||||
- Make the validated complete replay the only value that may cross a V3 authorization boundary.
|
||||
- Done when independent replay validation reproduces the original reports exactly and every source, summary, or trust-state mutation is refused.
|
||||
- _Requirements: 16.6, 20.5, 20.6, 20.7, 20.8_
|
||||
- _Boundary: Evaluation Replay Validation_
|
||||
- [ ] 17.3 Publish the integrated V3 preflight, authorization, scoring, and replay facade
|
||||
- Wire the exact plan preflight, reviewed authority, shared-spend validation, score/replay, strict decoding, and replay validation through one public V3 contract surface.
|
||||
- Preserve every V2 facade and import unchanged and expose no benchmark storage, execution, lifecycle, or presentation implementation from core.
|
||||
- Reject wrong-phase values and make each returned artifact usable only by the next declared phase.
|
||||
- Done when a client can execute the complete synthetic contract through the public V3 schema and V2/unknown/mixed artifacts fail at the first boundary.
|
||||
- _Requirements: 16.1, 16.4, 20.4, 20.6_
|
||||
- _Boundary: Evaluation Admission Facade Integration_
|
||||
- _Depends: 17.1, 17.2_
|
||||
- [ ] 17.4 (P) Authorize fitted components only from one exact replayed arm-scope result
|
||||
- Validate the complete V3 replay before selecting exactly one planned arm-scope result and require that completed result to be GO.
|
||||
- Preserve existing reversed-pair probe replay, fitting lineage, exact model declaration, and disjoint resolved repository-holdout checks.
|
||||
- Refuse V2 replay, raw arm report, aggregate, incomplete replay, missing/ambiguous key, NO-GO scope, or substituted fitted evidence.
|
||||
- Done when a matching fitted component authorizes only from its exact GO scope and every partial, aggregate, cross-arm, or probe/holdout mutation fails.
|
||||
- _Requirements: 13.2, 16.6, 20.7_
|
||||
- _Boundary: Fitted Consumer V3_
|
||||
- _Depends: 17.2_
|
||||
|
||||
- [ ] 18. Validate the V3 predecessor end to end
|
||||
- [ ] 18.1 Run a synthetic multi-arm, multi-family evaluation through one shared spend
|
||||
- Use at least two component arms and one combined arm across at least two families and two ecosystems with multi-scope fixes and representative ordinary pushes.
|
||||
- Prove nonzero reads, reviewed plan, exact preflight, sealed observations, reviewed attempt history, one preparation/receipt, V3 release, scoped reports, complete replay, and exact fitted selection.
|
||||
- Preserve component observations/results and verify ordinary projection plus shared whole-population deduplication in the completed replay.
|
||||
- Done when the public V3 facade produces one validated replay with every planned arm/scope and no V2 or benchmark-owned implementation is used as a substitute.
|
||||
- _Requirements: 16.1, 16.5, 17.1, 17.3, 17.5, 17.7, 18.1, 18.3, 18.5, 19.1, 19.4, 20.1, 20.4, 20.7, 20.8_
|
||||
- _Boundary: V3 End-to-End Conformance_
|
||||
- _Depends: 17.3, 17.4_
|
||||
- [ ] 18.2 (P) Run the adversarial pre-spend and compatibility matrix
|
||||
- Exercise V2 byte/digest/replay stability, V2-as-V3, unknown/mixed/subclass schemas, baseline mutations, impossible scopes, missing slots, zero/unreadable observations, self-review, forked attempt history, identity changes, raw access, stale custody, and subset spend.
|
||||
- Prove every measurement instrument reads nonzero fixture bytes and fails loudly on unread input.
|
||||
- Verify insufficiency leaves custody available and no refused case reaches detector execution or label release in the synthetic boundary.
|
||||
- Done when every pre-spend mutation fails at its owning layer, V2 remains unchanged, and no failure produces a clean-looking result or quality decision.
|
||||
- _Requirements: 16.1, 16.2, 16.3, 16.4, 16.5, 16.6, 17.2, 17.4, 17.6, 17.8, 17.9, 17.10, 17.11, 17.12, 20.2, 20.8_
|
||||
- _Boundary: V3 Pre-Spend and Compatibility Validation_
|
||||
- _Depends: 17.3_
|
||||
- [ ] 18.3 (P) Run the adversarial post-spend, metric, combination, and replay matrix
|
||||
- Exercise V2 release substitution, arm/scope/role/pair/result/input swaps, missing components, contradictory combinations, proof transfer, absent/inconclusive proof, cross-ecosystem ordinary projection, duplicate whole coverage, zero/mismatched denominators, missing gates, and incomplete reports/replay.
|
||||
- Verify invalid post-spend evidence never becomes insufficiency, GO, NO-GO, aggregate authority, or consumer eligibility.
|
||||
- Revalidate every complete replay from source artifacts and current custody rather than trusting carried summaries.
|
||||
- Done when the valid control replay reproduces exactly and every post-spend mutation is refused before authorization without hiding another arm's status.
|
||||
- _Requirements: 18.2, 18.4, 18.6, 18.7, 18.8, 19.2, 19.3, 19.5, 19.6, 19.7, 19.8, 19.9, 19.10, 19.11, 20.3, 20.5, 20.6, 20.7_
|
||||
- _Boundary: V3 Post-Spend and Replay Validation_
|
||||
- _Depends: 17.2, 17.4_
|
||||
- [ ] 18.4 Verify dependency boundaries, complexity caps, and bounded validation growth
|
||||
- Verify dependency direction, no V2/V3 aggregate imports across the version boundary, standard-library-only core runtime, and absence of benchmark, analyzer, model-runtime, or proof-execution ownership in V3 modules.
|
||||
- Enforce function and module complexity caps and measure validation growth over increasing arms, scopes, observations, attempt events, cases, and reports.
|
||||
- Fail each measurement fixture unless its scoped inputs are readable and nonzero before interpreting timing, count, or clean-result output.
|
||||
- Done when import/boundary checks are clean, amended code stays within complexity caps, and validation cost remains bounded by supplied artifact counts without weakening checks.
|
||||
- _Requirements: 16.2, 20.6, 20.8_
|
||||
- _Boundary: V3 Boundary and Performance Validation_
|
||||
- _Depends: 18.1, 18.2, 18.3_
|
||||
- [ ] 18.5 Run the final compatibility, regression, and schema-export gate
|
||||
- Run the V2 golden suite, every V3 unit/integration/adversarial suite, and the existing core regression suite without consuming a real admission population.
|
||||
- Prove all fixtures are readable and nonzero and the exact `openultrasast.detection-core.evaluation-admission.v3` schema is publicly exported.
|
||||
- Done when all gates pass, V2 outputs remain frozen, validation stays bounded by supplied artifacts, and the validated predecessor is ready for downstream benchmark task generation.
|
||||
- _Requirements: 16.1, 16.2, 16.4, 20.6, 20.8_
|
||||
- _Boundary: V3 Final Validation Integration_
|
||||
- _Depends: 18.4_
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The frozen migration inventory is a reviewed worklist with stable identities, not a semantic-use census. It may only close entries as `v2_migrated` or `retired`; it never grows, deletes entries, or proves absence by interpreting Python flow.
|
||||
- Cutover is proved by explicit syntax checks plus deletion of the compatibility and v1 write capability. No task extends the discarded alias/data-flow interpreter.
|
||||
- Task 10.5 debug established that executable owner probes cannot compensate for blanket source-closure acceptance; resumed hunt attempts also require collision-resistant attempt identity and create-only persistence before handoff validation is authoritative.
|
||||
- Task 10.6 debug established that exact-byte successor authority must require canonical repository-relative paths and strict resolved-root containment, including rejection of outside-root symlink targets, before module digests can prove closure.
|
||||
- Human review reopened Task 10.6 for one focused remediation cycle covering canonical path syntax, resolved-root containment/symlink refusal, and the task-local formatting gate.
|
||||
- Human review reopened Task 10.5 for one focused remediation cycle covering exact source-closure evidence, collision-resistant create-only attempt persistence, and complexity-cap conformance.
|
||||
- Task 11.4 debug established that the 73 reserved-repository references are a benchmark-governance specification conflict, not a detection-core or instrument defect; historical artifacts and the reservation guard must remain unchanged pending an owning-spec decision.
|
||||
@@ -0,0 +1,490 @@
|
||||
# Engine change-scoped analysis (G2 workstream)
|
||||
|
||||
Status: **proposed / in progress** (opened 2026-10-11). Owner: engine. Tracks work to address the
|
||||
engine-detection gap found during the G0 baseline run. This is G2 ("improve what moves the unseen-repo
|
||||
numbers"), gated on the pool metric; it is not part of the G0 record-07 baseline (that runs on the quick-rule tier).
|
||||
|
||||
## Problem (measured, not assumed)
|
||||
|
||||
The engine produces **no finding** on real unseen vulnerability changes. Live 300 s long-deadline replays show
|
||||
that the cause is usually not a detection-capability gap:
|
||||
|
||||
- Clean Python injection change (`aws/bedrock-agentcore-sdk-python`, `install_packages`, introducing, 3-file
|
||||
diff): `wall=303 s`, hit the deadline. The ranker found candidates (`static_rank=1.0`) but **`selected: []`** and
|
||||
every candidate `deferred` with `reason: deadline_exhausted`, `analysis_not_started=deadline_exhausted`,
|
||||
`change_context_incomplete`. No analyses ran. The ranked candidates were **repo-wide and unrelated to
|
||||
the push** (`auth.py` access_control/config_secrets), not the changed `install_packages`. The engine is trying to
|
||||
re-audit the whole repo and runs out of budget before analyzing the change.
|
||||
- Noisy last_touch config_secrets change (`jenkinsci/metrics-plugin`, 14-file diff): finished in 206 s but
|
||||
`comparison_unknown` + `capability_unavailable` + `eligibility_provenance_mismatch` +
|
||||
`contributor-scan ... projection_unavailable`. This has several causes. Correspondence was one (fixed separately,
|
||||
commit c39fbb8); the capability registry is Task 8.3.
|
||||
|
||||
## Hypothesis
|
||||
|
||||
A pre-push hook should check **the code this push changed**. Starting the engine's analysis from the
|
||||
**changed declarations and their dataflow neighborhood** should let it run within budget instead of being
|
||||
deferred. The `change_sites` are already in the replay spec. The missed detection comes from deferral,
|
||||
rather than a limit in Joern's capability.
|
||||
|
||||
## Anchors (confirm in increment 1)
|
||||
|
||||
- `src/openultrasast/rank.py` is the static-evidence ranker (`ranking_mode="evidence"`, `existing-static-evidence-v1`).
|
||||
- `src/openultrasast/mapping.py` builds the `selected`/`deferred` scope + the budget allocation that defers on
|
||||
`deadline_exhausted`.
|
||||
- `src/openultrasast/push/policy.py` and `push/runner.py` consume the scope; change_sites arrive via the push input.
|
||||
- `benchmarks/unseen/replay_entry.py` carries `changed_paths` / `declaration_paths` in the spec.
|
||||
|
||||
## Increments (progress tracking)
|
||||
|
||||
- [x] **0. Cost split confirmed (2026-10-11, timed live replay).** `build_seconds=66.69` (CPG build is cheap),
|
||||
`query_seconds=0.0` (no query ever executed), total 303 s (hit the deadline), **4,095** candidate questions all
|
||||
`analysis_not_started=deadline_exhausted`, `selected: []`, `regions_scanned=0`. So of the 303 s, ~67 s built the
|
||||
CPG and ~236 s went to enumerating/ranking/preparing 4,095 repo-wide candidate questions for a **3-file** change.
|
||||
The budget was spent before a single query ran. The over-broad candidate set is the bottleneck, rather than the CPG build.
|
||||
This confirms change-scoping as the fix. Preparing each candidate includes line-correspondence work,
|
||||
so fewer candidates also reduce that cost.
|
||||
- [x] **1. Located (2026-10-11).** The over-enumeration is upstream of the scan, not inside it:
|
||||
- `push/runner.py:345-348`, `head_regions = discovery(tip.root, …)` discovers regions across the **whole head
|
||||
tree** (the ~4,095), independent of the change.
|
||||
- `push/runner.py:397-407`, those regions are passed wholesale to `scan_repository(..., change_context=context)`;
|
||||
`change_context` is threaded in but **never scopes the region set**.
|
||||
- `model/scan.py:167` and `:364`, `[_identity(unit, r, f) for r in regions for f in r.families]` enumerates all
|
||||
regions × families; with `order_by_evidence=True` (`runner.py:288/403`) the scan evidence-tiers **every** pair
|
||||
before analysis. That pre-work is the ~236 s measured in increment 0, so the deadline hits at `query_seconds=0`.
|
||||
- Primitive already present: `model/regions.py:279` computes the changed-path set from the context;
|
||||
`compare_targeted_base` (`runner.py:411`) is a precedent for change-targeted work.
|
||||
- For increment 2, restrict the *analyzed* region set to the change's declarations + dataflow
|
||||
neighbourhood (filter `head_regions` by `context.changed_paths` before the scan, or scope the enumeration inside
|
||||
`scan_repository`). Keep the **CPG whole-repo** so taint queries can still trace sources from unchanged code into
|
||||
the changed regions. Only the question set is change-scoped.
|
||||
|
||||
Correction (2026-10-11), after reverting the first attempt: the filter got two things wrong.
|
||||
- **The engine already reaches into unchanged code by design, and it is tested.** A changed `source` that reaches
|
||||
an unchanged `sink` must still select the unchanged sink's question
|
||||
(`tests/test_model_change_context.py::test_changed_dependency_reaches_unchanged_ranked_question`;
|
||||
`test_change_does_not_replace_ranker_selection` pins that change context must not alter the ranker's selection).
|
||||
`_changed_files` feeds `_evidence_pass` only as `contextFilter`/`contextPaths`. This controls which locations to itemise vs
|
||||
summarise, rather than shrinking the tier population. A "changed files only" scope deletes the reach-into-unchanged
|
||||
behaviour. Any scope must keep changed regions **plus the regions they reach** (the affected/`relationships`
|
||||
neighbourhood), not changed files alone.
|
||||
- **The 236 s is now split (2026-10-11, saved artifacts).** `total=291s build=52s query=0s arbitrate=238.75s`,
|
||||
and `query_seconds_by_kind["evidence"]=238.54s`, the evidence-tier pass is **99.9 %** of the arbitrate phase;
|
||||
`affected_context` + `_scope_work` are **0.21 s**. The pass tiered 1,015 pairs (`tier_counts 0:1009, 1:6`) at
|
||||
**~235 ms/pair** (contended; ~120 ms uncontended) and that alone blew the budget, `selected=0`,
|
||||
`deferred=1365` all `deadline_exhausted`. Secondary anomaly: the design assumes ~2.5 ms/pair
|
||||
("17,700 in 44 s"); here it is 50–100× that, i.e. the evidence query is unusually slow on a cold large
|
||||
CPG. Fixing that is separate from cutting the pair count.
|
||||
- Increment 2 targets the evidence pass (`scan.py:338-359`, `_evidence_pass`). The options must
|
||||
reduce its cost while preserving flows into unchanged code:
|
||||
- (i) Scope + bound: order the evidence candidates change-first and bound the pass to a fraction of the
|
||||
budget so analysis queries still run; reached unchanged regions are tiered only while budget lasts.
|
||||
- (ii) Neighbourhood pass: tier the changed regions, read the reached regions from the context rows the
|
||||
pass already returns, then tier those in a second small pass. This preserves exact reach-into-unchanged behaviour but needs more code.
|
||||
- (iii) Per-pair speedup: reduce the ~235 ms/pair (warm server / batching) rather than the pair count.
|
||||
This helps every scan but needs a deeper engine change and does not by itself bound work on a huge repo.
|
||||
|
||||
**CORRECTION (2026-10-11, after implementing (i) + four live re-verifies, the cost model was wrong).**
|
||||
Scope + bound was built and tested (change-first ordering, adaptive budget-fitted portioning, `_context_pass`
|
||||
scoped to changed files, honest `evidence_pass_bounded` degradation; branch `engine-change-scoped-evidence-pass`).
|
||||
It cut the tiered pair count, but `query_seconds` stayed **0** every time. The live numbers show why:
|
||||
|
||||
| pairs tiered | run_batch calls | evidence s | build s |
|
||||
|---|---|---|---|
|
||||
| 1015 | 1 | 238 | 52 |
|
||||
| 512 | 1 | 219 | 68 |
|
||||
| 64 | 2 | 220 | 68 |
|
||||
| 4 (probe) | 1 | 160 | 127 |
|
||||
|
||||
Evidence time is **flat regardless of pair count** → the cost is **per-*call* fixed** (~150–220 s: loading the CPG
|
||||
into the query engine + the census the evidenceOnly batch computes), ~85 ms/pair on top. Pair-count scoping
|
||||
therefore cannot free budget, and portioning re-pays the fixed cost per call (so (i) can be counterproductive).
|
||||
Build time also varies from 52→127 s. Two engines compete for the 7 GB host, which the record-07 baseline shares.
|
||||
The fixes now resemble (iii), rather than (i): a **warm/persistent query engine or a cached census** to remove the
|
||||
~150 s per-call fixed cost, and/or a **bigger engine-arm budget** than 300 s. A **clean (uncontended)
|
||||
measurement** is also needed because contention may inflate build + fixed cost 2–3×. Scope+bound is not merged.
|
||||
|
||||
**UNCONTENDED measurement (2026-10-11, baseline paused via SIGSTOP + container kill, then resumed).** Shipped
|
||||
engine, one evidence call over 1,015 pairs: `build=48 s`, `evidence=243.79 s`, `query=0`. The evidence pass is
|
||||
**the same ~244 s uncontended as ~238 s contended** → the cost is **intrinsic, not contention**; contention only
|
||||
inflated `build` (48 → 127 s) and added run-to-run noise. The model is `evidence ≈ 150 s fixed (CPG load into the
|
||||
query engine + the census the evidenceOnly batch computes) + ~93 ms/pair` (244 = 150 + 1015×0.093). Consequences:
|
||||
- **~200 s of fixed overhead** (`build` + evidence-load) precedes any analysis, so a 300 s budget leaves only
|
||||
~100 s for everything else. This is why `query=0` even on a clean host.
|
||||
- **Change-scoping still helps, but only by cutting the per-pair tail** (~1,015×93 ms ≈ 95 s): one evidence call
|
||||
over ~30 changed pairs is ~150 s + 3 s instead of 244 s. This would free ~90 s, which might leave enough time
|
||||
for analysis, but only just.
|
||||
- **It must be ONE call, not portioned.** The ~150 s is a first-call load, so the adaptive probe attributed it to
|
||||
its 4 probe pairs (per-pair≈37 s), concluded the budget fit ~0 more, and tiered only 4, the implemented
|
||||
scope+bound has both problems under this cost model. The correct shape: change-first candidates, count-capped, ONE
|
||||
evidence call, `deadline=None`.
|
||||
- Pre-push viability: the ~200 s fixed floor means the engine arm cannot be a fast (≤30 s) pre-push check on a
|
||||
repo this size without a warm/persistent session or a cached census. A larger budget (e.g. 600 s) makes the
|
||||
benchmark arm run but is not a realistic pre-push deadline. The lever that matters for the product is killing
|
||||
the ~150 s fixed cost, not scoping pairs.
|
||||
- [ ] **2. Seed analysis from the change.** Prioritize (or restrict first-pass to) the changed declarations plus
|
||||
their call/data neighborhood over repo-wide static rank, within budget.
|
||||
- [ ] **3. Soundness.** Keep cross-file dataflow into the changed function (don't scope to literal changed lines);
|
||||
add tests; keep existing push tests green.
|
||||
- [ ] **4. Live re-test.** Re-run the bedrock change: does analysis now run and produce a finding on
|
||||
`install_packages` (vs `deferred`)? Re-run metrics-plugin.
|
||||
- [ ] **5. Measure the delta.** Engine-arm sample on the pool (catch/false-alarm + engine-coverage rate), needs the
|
||||
kube lane (blocked on the AX completion path) for scale.
|
||||
|
||||
## Dependencies / notes
|
||||
|
||||
- Coverage `complete_within_scope` also needs the capability registry (Task 8.3) regardless of this work; this work
|
||||
moves findings from `comparison_unknown`/deferred to a decided novelty → advisory engine finding.
|
||||
- Implement directly and verify on the live instrument (a fake-client/unit pass is not evidence the live engine
|
||||
changed); the kube-lane and store endpoints are infrastructure owned by the operator.
|
||||
|
||||
## Efficiency barrier analysis (post-record-07 priority)
|
||||
|
||||
The records confirm that the record-07 baseline measured regex only. The default and
|
||||
long_deadline replay arms both hardcode `model="disabled"`, `max_model_calls=0`, `cost_usd=0`. No LLM is
|
||||
called, and the engine never completed (`engine_covered=0`, the ~200 s wall). The measured `catch≈2.5%` covers
|
||||
only the quick pattern-rule tier, a structured pattern search. This measures the floor, rather than the product.
|
||||
It confirms the maintainer's point that pattern rules alone are insufficient; otherwise `grep` would do.
|
||||
The product's two detection tiers did not run:
|
||||
|
||||
| Tier | Mechanism | Measured cost (uncontended, mid-size repo) | Ran? | Catch |
|
||||
|---|---|---|---|---|
|
||||
| 1 quick rules | regex/pattern over changed lines | ~seconds | yes | 2.5% (floor) |
|
||||
| 2 engine (Joern) | CPG build + taint/dataflow | build 48 s + evidence (150 s fixed + 93 ms/pair) + analysis (unmeasured) | no (walled) | unknown |
|
||||
| 3 LLM adjudication | model judgment / search-with-proof (hunter, fusion, decision engine) | model latency + $/call (unmeasured in the push path) | no (disabled) | unknown |
|
||||
|
||||
To be releasable, the net needs tier 2 and/or tier 3 to run and catch within a realistic pre-push window.
|
||||
The target is **≤60–120 s** (developer patience), hard ceiling 300 s. The analysis records each tier's budget against
|
||||
that window and identifies the measurements needed:
|
||||
|
||||
- Engine barrier (tier 2). Fixed floor `build(48) + evidence_fixed(~150) ≈ 198 s` precedes any dataflow answer.
|
||||
Measure (clean host): **E1** split the ~150 s into JVM-start / graph-load / census; **E2** does a warm session
|
||||
with the graph **preloaded at build** eliminate `evidence_fixed` on the first query (the architectural
|
||||
test)?; **E3** analysis (`reachableByFlows`) per-pair cost + warm reuse; **E4** build cost vs repo size.
|
||||
Levers in priority order: warm session + graph preload → cached census → change-scope the per-pair tail.
|
||||
- LLM barrier (tier 3). Unmeasured in the push path. Measure: **L1** model round-trip latency per call
|
||||
(DeepSeek) and calls needed per push for the decision/proof stage; **L2** the recall lift of tier 3 over tiers
|
||||
1+2 on the pool, does the LLM catch what regex and dataflow miss, and at what $ and latency per push?
|
||||
- Produce a **barrier table** for budget ∈ {60, 120, 300, 600} s × each tier combination. Record whether it fits
|
||||
and its measured recall/false-alarm on the pool. Use this to identify the minimal viable config for a releasable
|
||||
net. 600 s unblocks the benchmark arm but is not a pre-push deadline; the product target is tiers 2+3 under ~120 s.
|
||||
|
||||
Sequence: finish G0 (record 07) → **E1/E2 first** (they decide whether the warm session alone crosses the engine
|
||||
barrier) → E3/E4 → L1/L2. Each on a clean host (the session times out contended). The change-scoped branch
|
||||
(`engine-change-scoped-evidence-pass`, unmerged) only trims the tier-2 per-pair tail and is a late add-on, not the
|
||||
lever.
|
||||
|
||||
## E1/E2 RESULTS (measured 2026-10-12, CLEAN host, bedrock-agentcore, 3-file change, 300 s budget, head side)
|
||||
|
||||
Four clean-host replays **correct the earlier "~200 s graph-load wall"** diagnosis. It had confused graph-load
|
||||
cost with host contention from the running baseline replay:
|
||||
|
||||
| Run | build | evidence (taint batch) | dominance | query_s | selected | deferred |
|
||||
|---|---|---|---|---|---|---|
|
||||
| contended (prior) | 48–127 | 243.8 | — | **0** | **0** | 1365 |
|
||||
| disposable (shipped, clean) | 27.9 | 225.1 | 34.8 | 34.8 | **356** | 1009 |
|
||||
| warm session `SESSION=1` (clean) | 32.9 | 228.6 | 25.9 | 25.9 | **356** | 1009 |
|
||||
| session + graph preload (clean) | 30.4 | 223.2 | 32.5 | 32.5 | **356** | 1009 |
|
||||
|
||||
The measurements show:
|
||||
|
||||
1. Host contention prevented the engine arm from running. The prior `query=0 / selected=0 / all
|
||||
1365 deferred` was measured while the slice-1 baseline replay contended for the 7 GB host. On a clean host the
|
||||
shipped (disposable) engine already runs dataflow: `query_seconds` 0 → ~35 s, `selected` 0 → **356 regions**,
|
||||
dominance flow queries complete. This is the first time the engine arm produced analysis on this change.
|
||||
2. **The evidence/taint batch (~225 s) is invariant to session and to graph preload** (225.1 / 228.6 / 223.2 across
|
||||
the three clean runs). Warm session and preload-at-build do **not** reduce it. So the dominant cost is intrinsic
|
||||
to the taint batch, not graph I/O.
|
||||
3. Graph-load is cheap and does not account for the delay. The `dominance` query is a full cold query *including*
|
||||
`importCpg`, and it costs only ~35 s. So `importCpg` is tens of seconds, not ~150 s. **This corrects the earlier
|
||||
note** that attributed the ~150 s fixed to "first query JVM-start + graph-load + census." The ~160 s fixed lives
|
||||
inside `taint.sc`'s own per-batch flow work (seed/sink memo fill over the distinct `(family, file)` combinations
|
||||
it walks). It is paid lazily as the first pairs are processed, independent of the raw pair count.
|
||||
4. **The warm session is not the lever.** It starts fine on a clean host (the contended `session_startup_timeout`
|
||||
was contention-only), but gives ~no benefit here: subsequent queries (dominance) are already cheap disposable,
|
||||
and the evidence pass is a single batch call, so there is nothing for the session to amortise.
|
||||
5. The barrier, restated: `build(~30) + evidence(~225) ≈ 255 s` of a 300 s budget, so 1009/1365 regions defer
|
||||
as `deadline_exhausted`; only 356 are analysed. The fix is to **reduce the taint batch's work** by scoping
|
||||
the pair/family set to the change so the batch fits the budget. Session reuse, preload, or a bigger JVM will not do this. The parked branch
|
||||
(`engine-change-scoped-evidence-pass`) is therefore **relevant after all, on a clean host**, provided it scopes
|
||||
the family/file set `taint.sc` walks (not merely the pair list); its adaptive portioning still needs replacing
|
||||
with a fixed cap (per-call-fixed cost, not per-pair).
|
||||
6. Side note: the `base`-side CPG build failed (`cpg_build_failed`, 0.9 s) in the preload run, a separate
|
||||
intermittent base-revision build issue; the `head` side built and analysed normally. Track separately.
|
||||
|
||||
E1/E2 did not answer the G2 question: does the engine catch vulnerabilities regex misses when it selects 356 regions
|
||||
on a clean host? That needs an **engine-on unseen pilot** (a handful of pool repos replayed with the
|
||||
engine arm enabled on a clean host) scored against record 07's regex-only floor (catch 3/92). That is the
|
||||
next experiment, ahead of E3/E4/L1/L2.
|
||||
|
||||
## ENGINE-ON PILOT RESULTS (measured 2026-10-12, CLEAN host, 5 public pool repos, vuln change each, 300 s)
|
||||
|
||||
Five public repos were picked for flow-shaped families and small size, giving the engine its best chance to finish and catch:
|
||||
|
||||
| repo | fam | size | REGEX on vuln | ENGINE finding | cov | vuln region ranked | best taint | selected/deferred | wall |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| masci/banks | path | 2f | ✗ | **none** | incomplete | yes | sink found, no source (score 0) | 281 / **0** | 236 s |
|
||||
| vitejs/launch-editor | injection | 1f | ✗ | **none** | incomplete | yes | tier1 score2.0 sink (wrong fam) | 13 / 38 | 261 s |
|
||||
| getzep/graphiti | injection | 5f | ✗ | **none** | incomplete | yes | injection score 0 | 882 / **0** | 304 s |
|
||||
| aws/bedrock-agentcore | injection | 3f | ✗ | **none** | incomplete | yes | — | 356 / 1009 | 305 s |
|
||||
| miguelgrinberg/python-socketio | deser | 10f | **✓ (n=2)** | **none** | incomplete | yes | **tier1 score2.0 sink found** | 136 / **0** | 257 s |
|
||||
|
||||
Result: regex 1/5, engine 0/5. On a clean host, with budget to spare on most, the shipped engine **added no
|
||||
catches over the regex floor.**
|
||||
|
||||
The diagnosis was corrected after inspecting `head.findings` rather than ranker evidence:
|
||||
1. The budget hypothesis is refuted. banks, graphiti and socketio **finished their region queue** (deferred=0)
|
||||
and still emitted no finding. Change-scoping to fit budget would not have changed the outcome.
|
||||
2. The engine produced no vuln-family findings (path/injection/deser) on any of the 5. Actual `head.findings`:
|
||||
banks 1 (config_secrets), socketio 2 (config_secrets), launch-editor / graphiti / bedrock **0 findings each**.
|
||||
The dataflow/taint tier traced **no source→sink flow** for the vuln families (`taint_evidence_not_applicable`;
|
||||
e.g. banks found the `path.open` sink but **no source reaches it**). Correction: the tier=1 / score=2.0
|
||||
"sink" seen in the ranker's `normalized_evidence` is the ranker's static sink candidate, not a confirmed taint
|
||||
finding. An earlier draft of this section misread it as detection partially working. It was not.
|
||||
3. `ambiguous_line_correspondence` does not explain these missed vulnerabilities. It fires on all 5, but there
|
||||
are **no vuln-family findings to anchor**, so a region-level witness fallback (the proposed "Option A") would
|
||||
change nothing here. Witness/anchoring only touched the config_secrets findings that did appear.
|
||||
4. **The LLM adjudication tier was disabled** (`model_calls=0`, `cost_usd=0`, `model_assistance={}` on all 5). The
|
||||
rungs reached: `suspicion=0, model_corroborated=0, model_entailed=0–2, execution_confirmed=0`. The tier designed
|
||||
to confirm a vuln the static dataflow cannot trace **never ran**.
|
||||
5. **Ranking also under-promotes the vuln region** (tier 0 for most families; banks path family stuck low). So even
|
||||
where a sink exists, the region is not analysed deeply enough to attempt a flow.
|
||||
6. The pool's SZZ last-writer change definition yields large/moved diffs; a cleaner probe
|
||||
uses tight vuln-introducing diffs. Secondary to (2)/(4).
|
||||
|
||||
Reprioritised engine levers (corrected, anchoring is not the lever):
|
||||
- #1: the LLM adjudication tier (disabled in the push path) has the highest priority. regex+dataflow = 1/5 with
|
||||
the model OFF; the static tiers produce no vuln-family findings on these shapes. The hypothesis
|
||||
([[search-with-proof]]) is that the LLM tier catches what regex+dataflow miss. The pilot supports
|
||||
testing it: **L1** model latency + calls/push, **L2** recall lift on the same 5 (and beyond). These calls cost money, so stage
|
||||
them (5–10 sizing → ~10–20 pilot → full only on lift).
|
||||
- #2 taint source/sink coverage + region ranking so the dataflow recognises these vuln sources and promotes the
|
||||
vuln region (banks path: sink found, source unrecognised; graphiti injection: score 0).
|
||||
- #3 change-anchoring robustness matters only after the static/LLM tiers produce vuln-family
|
||||
findings to anchor. Deprioritised until (1)/(2) yield findings.
|
||||
|
||||
Detection does not meet the pre-push net's release requirements. Clean host, generous budget, LLM off: regex 1/5,
|
||||
engine 0/5. The static tiers (regex + Joern dataflow) do not detect these unseen vulns; the untested lever
|
||||
is the **LLM adjudication tier**. Next, run an **LLM-tier pilot (L1/L2)** on these 5 repos to test whether the model
|
||||
produces vuln-family findings regex+dataflow miss, and measure its $/latency.
|
||||
|
||||
## ROOT CAUSE of engine 0/5 + the "#2 guided by #1" workstream (2026-10-12)
|
||||
|
||||
Code and evidence show why the taint tier traced no flow. The pilot repos are **libraries**, whose
|
||||
untrusted input arrives as **public-API function parameters**, not HTTP requests. The Python taint **source**
|
||||
vocabulary (`ruleset/semantic/python.toml`) is entirely web/CLI: Flask/Django `request.*`, `sys.argv`, `sys.stdin`,
|
||||
`input(`. There is no source for a library's own API parameter. The engine *does* have a parameter-as-source mode
|
||||
(`_parameters_are_input`, `scan.py`), but it fires **only for recognised entry points** (`_is_entry_point` and
|
||||
`entry_kind != "function"`). The pilot vuln regions carry `entry:false`, so their parameters are **deliberately
|
||||
not** sources. That restriction is intentional: treating every function parameter as a source caused a flood of
|
||||
false SQL-injections on WordPress/PMPro (documented in the `_parameters_are_input` docstring). The static
|
||||
engine cannot safely widen its sources to library APIs without reintroducing those false alarms.
|
||||
For `banks` path: sink `path.open` found, `source_local/near=false`; `graphiti`/`bedrock` injection: no source.
|
||||
|
||||
The maintainer's "#2 guided by #1" directive addresses this gap. Static taint cannot *safely*
|
||||
decide which library parameters are trust boundaries; the **LLM can** reason about which public-API surface is
|
||||
attacker-reachable (intent, not pattern). So:
|
||||
- #1 (LLM) guides: on a changed library region the engine ranks but cannot source, the LLM adjudicates whether a
|
||||
parameter is an untrusted trust boundary and names the source→sink it believes exists (search-with-proof).
|
||||
- #2 (taint) traces + proves: the engine then runs the flow from the LLM-named source to the known sink,
|
||||
confirming or refuting it with a traced path. The trace provides proof beyond the LLM's judgment. This would avoid the static FP
|
||||
increase while giving every catch a dataflow witness.
|
||||
- Where the LLM repeatedly names the same source shape (e.g. a registry-key → path family), promote it
|
||||
to a static fact in `python.toml` guarded by the entry/shape conditions that keep WordPress quiet.
|
||||
|
||||
Proposed first increment (pending budget confirmation, LLM calls cost money): wire the disabled LLM tier on the
|
||||
5 pilot regions in *LLM-guided-source* mode and measure: (a) does it name the right trust-boundary source per vuln,
|
||||
(b) does the taint engine then trace source→sink and emit a vuln-family finding, (c) catch lift over regex (1/5) and
|
||||
false-alarm cost on the ordinary changes. Stage it (5–10 sizing → ~10–20 → full on lift). This makes "#2 guided by
|
||||
#1" the concrete G2 detection workstream; budget/change-scoping and change-anchoring stay deprioritised until it
|
||||
yields findings.
|
||||
|
||||
## BANKS SIZING + "2 guided by 1" validated (2026-10-12, budget-approved, DeepSeek)
|
||||
|
||||
Sized the LLM tier on banks (the clearest pilot miss) with the project's own DeepSeek client (`deepseek-flash`).
|
||||
|
||||
DeepSeek is reachable, and model id `deepseek-flash` is valid. **~$0.00016–0.00025 per
|
||||
call, ~1.1 s latency.** This costs less than a cent per push even at 8 candidates × a few regions. The GLM/abliteration fallback
|
||||
(`ABLITERATION_*`) currently errors `HTTP 422` on the `thinking` request param. A client fix (don't send
|
||||
`thinking` to that endpoint) is needed before it can be used; DeepSeek works today so it is not blocking.
|
||||
|
||||
"2 guided by 1" works in this case: the LLM gives the correct verdict when given the real sink + source
|
||||
context:
|
||||
- For `_load` (the pool's SZZ-labelled site, 135–141), the model returned `vulnerable=false`. This is correct: `_load` reconstructs
|
||||
paths from its own index, not the sink.
|
||||
- Asked about the real sink `from_prompt_path`/`set()` line 44 (`path / f"{prompt.name}…"; write_text`) with the
|
||||
context that `prompt.name` can be request-derived: **`vulnerable=true`**, *"prompt.name is joined into a
|
||||
filesystem path and written without sanitization… path traversal or arbitrary file write."* This is a correct catch.
|
||||
|
||||
Two blockers to realising it in the live pipeline (both now concrete):
|
||||
1. **Context, not single-shot.** The model only catches with the real sink + caller/source context. A single-shot
|
||||
question on the ranker's region (which pointed at `_load`, and at the benign local-glob `_scan`) does not. This
|
||||
requires the **agentic search-with-proof hunter** (explores callers/entry points to establish the trust
|
||||
boundary), not the single CANDIDATE_QUESTION path. That needs more calls → their cost is the next measurement.
|
||||
2. **Corpus-label soundness.** The pool's banks label is mislocated: `head_lines=[[30],[104],[138–141]]`,
|
||||
`function=_load`. The real sink is line **44**, outside head_lines±10 and a different function. So (a) even a
|
||||
correct finding at line 44 would not score as a catch under `location_match`, and (b) the sink pre-exists in base
|
||||
(the change did not touch it), so a change-scoped pre-push hook **correctly stays quiet**. The pool even
|
||||
annotates this bias ("delta hook can correctly stay quiet"). **Part of the pilot's engine 0/5 is this SZZ
|
||||
mislocation, not a detection failure.**
|
||||
|
||||
The budget is approved. Keep the revised next steps staged:
|
||||
- Validate the 5 pilot labels first (cheap, LLM-assisted corpus audit): for each, is the real sink inside the
|
||||
changed lines, and is it change-introduced vs pre-existing-in-base? Only sound, change-located vulns are fair
|
||||
targets. This separates true detection gaps from SZZ artifacts before spending on detection runs.
|
||||
- Then run the agentic hunter (search-with-proof) on the sound targets and size its per-vuln call count/cost;
|
||||
measure catch + false-alarm (on ordinary changes) vs regex 1/5. The single-shot path is insufficient; the hunter
|
||||
is the real "#1".
|
||||
- Feed confirmed source shapes (e.g. library param → path join) back as guarded static facts (#2).
|
||||
|
||||
## Prompt evolution is a CORE lever for the LLM arm (maintainer directive 2026-10-12)
|
||||
|
||||
**"What is key is that we evolve the prompts as well"**, and **"GEPA or comparable; this is an evolving field."**
|
||||
The bedrock hunter ran (64 model calls, $0.0076), but its **current prompts** led it to flag adjacent concerns
|
||||
(network-PUBLIC default, download_file). It never named the `install_packages` command injection. This was a
|
||||
*prompting/targeting* miss, rather than a cost or capability limit. So the LLM arm needs a **prompt-optimisation loop** that reflects on misses/low-confidence
|
||||
outputs, updates the prompt, and retries. Build it on **DSPy** (its GEPA / MIPRO optimisers). Keep it **pluggable**
|
||||
behind an interface because the field changes quickly (GEPA, MIPRO, TextGrad, successors). This aligns with
|
||||
[[learned-decisions-not-hand-tuning]] (learned decisions + local memory, not hand rules). It applies to both LLM
|
||||
uses here: the search-with-proof detector and the corpus-label auditor.
|
||||
|
||||
The optimisation must learn which hints/annotations the model needs to reach the real sink. Rewording the
|
||||
prompt alone is insufficient. bedrock needed the hint "`install_packages` runs `pip install` with caller-supplied package
|
||||
names → command injection"; banks needed "`prompt.name` is request-derived and joined into a filesystem path →
|
||||
traversal". DSPy optimises which evidence/hints to feed the signature so the model targets the true sink instead of
|
||||
adjacent noise.
|
||||
|
||||
Use the project's existing framework knowledge for those hints. The semantic facts taxonomy
|
||||
(`ruleset/semantic/*.toml`, `ruleset/frameworks.toml`) encodes sources/sinks/sanitizers per framework without
|
||||
user annotations ([[php-detection-is-engine-only]] zero-annotation value). Where framework knowledge covers the
|
||||
API, feed it as a hint. Where it does not (library APIs like banks/bedrock), the LLM proposes the trust boundary,
|
||||
which becomes a fact after repeated confirmation (the #2 feedback loop). No new annotation burden on the
|
||||
user. This is now part of the "#2 guided by #1" workstream and must not be deferred. (DeepSeek
|
||||
`deepseek-flash` is the working default; GLM 5.3 "cyber" model is future-spec.)
|
||||
|
||||
## POOL-P1 LABEL AUDIT (LLM, 2026-10-12): 47% of labels are sound
|
||||
|
||||
The banks label-location check was extended to all **49 public** pool vulns via DeepSeek (advisory + head code +
|
||||
pool label → sound/unsound). Full artifact: `benchmarks/measurements/2026-10-12-pool-p1-label-audit/`.
|
||||
- **label_sound = 23/49 (47%)**; 53% unsound (25 pre-exist in base, 16 sink outside changed lines). Cost $0.038.
|
||||
- By family (sound/total): injection 10/13, access_control 5/8, untrusted_destination 4/7, deserialization 1/2,
|
||||
config_secrets 1/4, **path 1/7, output_encoding 1/8**. Injection labels mostly sound; path/output_encoding mostly
|
||||
mislocate or pre-exist.
|
||||
- Record 07's floor (catch 3/92) is scored against a corpus ~half unfair to a delta hook. On the ~53%
|
||||
pre-existing/mislocated vulns a change-scoped hook correctly stays quiet. The honest catchable denominator is the
|
||||
sound subset; record 07 needs this caveat + a sound-subset recomputation.
|
||||
- Only bedrock + graphiti (injection) of the 5 pilot cases have sound labels. The engine missed both, so these
|
||||
are detection gaps. The 23 sound targets are the fair local eval set for "#2 guided by #1".
|
||||
- Next: run the agentic search-with-proof hunter on the sound targets (start bedrock + graphiti), size its
|
||||
per-vuln call count/cost, measure catch + false-alarm. Then recompute record 07 on the sound subset.
|
||||
|
||||
## MODEL HUNTER on the 2 sound targets (2026-10-12): first LLM catch the static tiers missed
|
||||
|
||||
Ran `ousast scan --mode deep` (DeepSeek `deepseek-flash`, model layer + tool_hunter, 60-call/8-hotspot bound) on
|
||||
bedrock + graphiti, the two sound pilot targets the static engine missed. Artifact:
|
||||
`benchmarks/measurements/2026-10-12-hunter-sound-targets/`.
|
||||
- **hunter 1/2 vs static engine 0/2.** graphiti: **HIT**, `falkordb_driver.py:207`, tag `family:cypher-injection`,
|
||||
rationale traces `build_fulltext_query` concatenating user input into a Cypher string at line 278 (inside the
|
||||
labelled function, location-matches). bedrock: **miss**, 12 suspicions on adjacent concerns, never named the
|
||||
`install_packages`→`pip` command injection.
|
||||
- Cost is low: ~$0.007/repo, 64 model calls for a whole-repo deep scan.
|
||||
- Everything stopped at `suspicion`. The proof arm did not elevate to corroborated/entailed/execution_confirmed.
|
||||
Release-grade BLOCK needs *proven* findings; the next step is to measure the proof arm's lift.
|
||||
- bedrock's miss is the concrete case for the DSPy prompt/hints loop above.
|
||||
|
||||
Next: (a) measure the proof arm (can it elevate graphiti's suspicion to proven?); (b) stand up the DSPy hint loop
|
||||
and re-run bedrock; (c) widen the hunter to the 23 sound targets for a catch rate; (d) then a change-scoped + proven
|
||||
run for the release-relevant shape.
|
||||
|
||||
## STRATEGIC PIVOT: core problem = unprovable reachability; FIX THE EVALUATION TARGET FIRST (2026-10-12)
|
||||
|
||||
The maintainer and team identified the problem: the tool can find dangerous operations but cannot prove
|
||||
an attacker can reach them. The trust boundary is outside the changed library code, in a caller/application
|
||||
that a change-scoped scan does not see. The project will not emit unproven alarms. Recall → ~0 from unprovable
|
||||
reachability, not blindness. Cost/latency/engine-wall/corpus-labels did not explain this problem.
|
||||
|
||||
The chosen bet is to fix the evaluation target first. The three options were fix-target / advisory-with-conditions / build-prover.
|
||||
A pre-push hook runs inside a developer's application (entry points present, request→sink in-repo), but the pool is
|
||||
library-heavy, so we measured the hardest/wrong case.
|
||||
|
||||
Shape classification (LLM, 49 public vulns, $0.033, `benchmarks/measurements/2026-10-12-pool-p1-shape-classification/`):
|
||||
reachability_in_repo True only 12/49 (24%); 37/49 are pure libraries. A valid catch needs a sound label and
|
||||
in-repo reachability → 5/49 (~10%): graphiti, desktopcommandermcp, malla, document-merge-service, mcp-remote.
|
||||
Repository shape predicted the hunter result (graphiti app-shaped→caught; bedrock library→missed). So record-07's 3/92 is
|
||||
scored against a ~90%-illegitimate denominator. The near-zero catch largely comes from a mismatch between the targets and the tool.
|
||||
|
||||
Next: deep scan (regex/engine/hunter) on the 5 fair targets → per-tier catch rate on the corrected set; then
|
||||
stand up an app-shaped eval set from known-vulnerable apps (VAmPI/NodeGoat/DjanGoat/vibe-py) and recompute the
|
||||
floor. Revisit the deferred bets (advisory-with-conditions, reachability prover) once the fair-target results are available.
|
||||
|
||||
## FAIR-TARGET RESULT: detection 3/5 on app-shaped+sound (vs ~0 library-heavy), target-fix validated (2026-10-12)
|
||||
|
||||
Deep scan on the 5 app-shaped+sound targets (`benchmarks/measurements/2026-10-12-hunter-fair-targets/`):
|
||||
- 3/5 detected: graphiti (hunter, Cypher injection), mcp-remote (hunter, url-scheme injection), dcmcp (ENGINE
|
||||
overlay, "outbound.request reached from parameter" on readFileFromUrl; the param-as-source mode fired because an
|
||||
entry point is present). 2/5 missed: malla (DOM/template XSS) and dms (Jinja SSTI), both in families
|
||||
documented as limited in families.toml (template-mediated escaping).
|
||||
- The library-heavy pilot found ~0 → this confirms that the target mismatch affected detection. On an app-shaped repo the engine
|
||||
itself detects (parameter is a recognized source once the entry point is in-repo).
|
||||
- Everything stops at `suspicion`/`static_corroboration`. None are proven. Cost ~$0.007/repo.
|
||||
|
||||
Bet #1 (fix the eval target) is necessary and improves detection, but is insufficient on its own.
|
||||
Detection appears on fair targets; release-grade BLOCK still needs (a) proof-arm elevation (suspicion→proven) and
|
||||
(b) family coverage (template XSS/SSTI) + the DSPy hint loop for misses. Next: recompute a floor on an app-shaped
|
||||
sound subset / real app corpus (VAmPI, NodeGoat, DjanGoat, vibe-py), and measure the proof arm's suspicion→proven lift.
|
||||
|
||||
## PROOF-ARM MEASUREMENT on the 3 fair-target hits (2026-10-12): push-time lift 0/3, structural
|
||||
|
||||
Measured whether the proof arm can elevate the 3 fair-target suspicions (graphiti, mcp-remote, dcmcp) to
|
||||
`exploit_demonstrated`.
|
||||
- The search arm's `FAMILY_ORACLES` (search/demo.py) differs from families.toml's `verifier`.
|
||||
All 3 hits have a runtime oracle: injection→(sql,command), untrusted_destination→(ssrf). (deserialization/
|
||||
access_control/config_secrets have none, empty tuple → `no_oracle`.) So dcmcp is provable in principle
|
||||
(earlier "unprovable" was a families.toml misread).
|
||||
- Runtime differential proof through `search/verify.verify()` requires (1) an authored declarative demo
|
||||
(start the app on a port, run exploit steps, fill oracle placeholders: sql→database_path, command→marker_dir,
|
||||
ssrf→callback_url), (2) both head and base runnable with offline deps (`pip --no-index`, `npm ci --offline`),
|
||||
(3) a sandbox differential "owned effect in 3/3 affected runs and 0/3 safe runs". Local host has a sandbox
|
||||
(`isolation_mode: userns`); the plane supplies a task-boundary dispatcher.
|
||||
- The scan/push path reaches only `suspicion`/`static_corroboration` (verification.py levels). There is no code
|
||||
path from a scan suspicion to `exploit_demonstrated`, and no caller wires the search Coordinator from the push
|
||||
flow. The vulns are real, but proof-arm lift at push time = 0/3 because proving is a
|
||||
runtime differential operation with an offline build for each target that cannot run inside a pre-push hook. (Prior
|
||||
per-pair demo attempts were mostly inconclusive on this exact wiring, app startup, offline deps, oracle plumbing.)
|
||||
|
||||
This resolves the three-bet choice. Proof/BLOCK runs asynchronously (CI / plane / triage
|
||||
with authored demos + a runnable sandbox). At push time, the strongest available evidence is corroborated
|
||||
suspicion:
|
||||
- Bet #2 (advisory-with-conditions) is the honest PRE-PUSH product: surface corroborated suspicions ("change
|
||||
introduces a flow that looks like X; reachable if untrusted Y reaches it"), let the dev / coding-agent (who has
|
||||
the app context) judge. This matches the "oversee coding agents" purpose and the evidence available at the suspicion rung.
|
||||
- Bet #3 (the proof arm) is the ASYNC BLOCK lane: run `search/verify` on the plane/CI for the suspicions worth
|
||||
proving, with authored (eventually DSPy-authored) demos. Run a bounded, separate test on one tractable target
|
||||
(mcp-remote) to confirm that the arm demonstrates a real unseen vuln.
|
||||
|
||||
## BANKS FIXED-SEARCH MVP (2026-10-12): "no wrong targets, only wrong searching" demonstrated
|
||||
|
||||
Record: `benchmarks/measurements/2026-10-12-banks-fixed-search-mvp/`. Target: banks (the "library" that gave
|
||||
no engine path findings in the pilot). The target and CPG are unchanged; only the search changes.
|
||||
|
||||
- Injected the two annotations a per-target LLM steer would supply: **parameter-as-source**
|
||||
(`parameter_sources=True`, so `prompt.name` counts as untrusted) + the **missing file-write sink**
|
||||
(`write_text`/`read_text` added to path sinks).
|
||||
- The shipped search baseline resolved 0 `prompt → write_text` flows. Its 5 flow rows are all census/
|
||||
evidence (`sink=None`), matching the pilot's zero path findings.
|
||||
- Fixed search: the engine resolves `prompt → path-join → prompt_file.write_text(prompt.raw)` in
|
||||
`from_prompt_path` (`source=prompt`), plus a second `write_text` sink in `_scan`. These appear only with
|
||||
the annotations.
|
||||
- The banks result supports the hypothesis: the target was valid, but the search lacked annotations. The
|
||||
fix needed exactly **two** inputs: the param-source and the file-write sink. These are what the
|
||||
per-target LLM steer (DSPy-optimised, framework-seeded) has to learn to supply. This is the first measured
|
||||
instance of a complete "#2 guided by #1" loop on an unseen target.
|
||||
- Caveats: (1) one row shows `source=path` not `prompt`, the param-source fires broadly, the steering loop needs to control that breadth to improve
|
||||
precision; (2) this is a trace, not the runtime differential proof (proof stays the async BLOCK lane).
|
||||
|
||||
The first MVP used the single-query `cpg.run("taint", …)` path and returned
|
||||
0/0 because of an instrument failure, rather than producing a valid result (`cpg query taint failed` on every query). Root cause filed in
|
||||
`bugs/taint-sc-single-query-signature-skew.md`: `request_params()` emits 5 keys (`guards`, `fixedOrigin`,
|
||||
`originAnchors`, `quotedSanitizers`, `fieldParameterSources`) that `taint.sc`'s `exec` signature does not
|
||||
declare, so both single-query sub-routes (CLI `--param` and session named-arg) reject them. The batch path
|
||||
(`run_batch` → `requestsFile` JSON) tolerates them, which is why the pilot scans ran and this MVP used it.
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# BUG: `taint.sc` single-query path broken by exec-signature skew
|
||||
|
||||
Filed: 2026-10-12 · Repo HEAD at filing: `7ab5c2a` · Severity: **high (silent zero)** · Status: **FIXED (2026-10-12)**
|
||||
|
||||
## Resolution (2026-10-12)
|
||||
|
||||
Fixed via **option 1** (filter params to the declared signature) in `src/openultrasast/cpg/backend.py`:
|
||||
|
||||
- New `_declared_exec_params(source)` parses the `@main def exec(...)` parameter names from a shipped script
|
||||
(brace-aware, tolerant of an unfindable signature → empty set → "do not filter").
|
||||
- New memoised `JoernBackend._declared_params(query)` reads+caches a script's declared names.
|
||||
- `JoernBackend.query()` now drops any param the script does not declare before building the `--param` list
|
||||
(disposable route) and the in-session named-arg call (session route). This is faithful, not lossy: the
|
||||
single-request branch of `taint.sc` (line 1542) calls `rowsFor(...)` with top-level params only and lets
|
||||
`fixedOrigin`/`originAnchors`/`quotedSanitizers`/`guards`/`fieldParameterSources` default; it never could
|
||||
consume them. The batch path keeps carrying them as JSON the script parses itself.
|
||||
- No-op for `config`/`dominance`: their own `request_params` emit only params their `.sc` declares (verified),
|
||||
so nothing is dropped. Only `taint` sheds the 5 extras.
|
||||
|
||||
**Verified end-to-end** (patched backend mounted into the replay image, single-query path on banks): the
|
||||
queries now RUN (no `cpg query taint failed`); baseline = 0 findings, fixed search (annotation) = 2 scored
|
||||
`path`/`model_entailed` findings (`from_prompt_path` → `prompt_file.write_text(prompt.raw)` L45; `_scan` →
|
||||
`self._index_path.write_text(...)` L155). Regression test:
|
||||
`tests/test_cpg_backend.py::test_a_single_query_passes_only_params_the_script_declares` (fake runner, no Joern;
|
||||
asserts the 5 undeclared params never reach the command line and the query parses).
|
||||
|
||||
## Summary
|
||||
|
||||
Any call on the **single-query** taint path (`cpg.run("taint", request_params(...))`) fails on **both**
|
||||
of its sub-routes, returns `None`, and is read upstream as "no flows / clean". `request_params()` emits five
|
||||
keys that `taint.sc`'s `@main def exec(...)` does not declare, and the single-query dispatch passes every
|
||||
param straight to the script. The **batch** path (`run_batch` → `requestsFile` JSON) is unaffected, which is
|
||||
why the engine-on scans ran while the single-query MVP did not.
|
||||
|
||||
This is exactly the failure shape AGENTS.md warns about: a tool that could not read/execute its input reports
|
||||
a plausible zero that is indistinguishable from a clean result.
|
||||
|
||||
## Mechanism
|
||||
|
||||
`request_params()` (`src/openultrasast/model/taint.py`) always emits, among others:
|
||||
|
||||
- `fieldParameterSources` (line 54)
|
||||
- `fixedOrigin` (line 65)
|
||||
- `originAnchors` (line 66)
|
||||
- `quotedSanitizers` (line 67)
|
||||
- `guards` (line 68)
|
||||
|
||||
`taint.sc`'s signature (`src/openultrasast/cpg/queries/taint.sc:41` `@main def exec(...)`) declares **none**
|
||||
of those five. Its 19 params are: `cpgFile, sources, sinks, sanitizers, function, file, callDepth,
|
||||
boundedSinks, parameterSources, fieldSources, evidenceOnly, hookCallbacks, dispatchApply, dispatchValue,
|
||||
contextFilter, contextPaths, requests, requestsFile, trace`.
|
||||
|
||||
`backend.query()` (`src/openultrasast/cpg/backend.py:1266`) dispatches a single query two ways, **both** of
|
||||
which choke on the undeclared keys:
|
||||
|
||||
- Disposable / CLI route (lines 1276–1278): appends `--param {key}={value}` for every param. joern errors
|
||||
`Unknown arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources`
|
||||
(and, as a knock-on, `Missing argument: --cpgFile`). Observed verbatim via `banks_debug.py` on 2026-10-12.
|
||||
- Session route (lines 1321–1326): defines `def ousast_taint(...)` by renaming `@main def exec(` and then
|
||||
calls `ousast_taint(key = value, ...)` with named args. The same five keys are not parameters of that
|
||||
method → the Scala named-arg call fails to compile. (By construction; the rename preserves the exact
|
||||
`exec` parameter list.)
|
||||
|
||||
Why the batch path survives: `run_batch` writes the per-request params **inside** the `requestsFile` JSON
|
||||
object, and `taint.sc` parses that JSON itself (`requestsJson`, taint.sc:67). The extra fields ride as JSON
|
||||
keys, never as `exec` arguments, so they are tolerated.
|
||||
|
||||
## Blast radius
|
||||
|
||||
Broken (single-query taint):
|
||||
|
||||
- `taint._usable_flows` (`taint.py:124`) → `verdicts()` (`taint.py:131`) → `verdict()` (`taint.py:99`).
|
||||
- `pipeline.scan_region` dispatches taint families to `taint_verdict` (`pipeline.py:121`) → so **per-region
|
||||
single-query taint via `scan_region` is dead** whenever the disposable or session route is taken.
|
||||
|
||||
Unaffected:
|
||||
|
||||
- The push/scan **batch** path: `scan.py` uses `run_batch("taint", …)` (scan.py:1259, 1264, 1387). This is
|
||||
why the engine-on pilot produced results.
|
||||
- `config` and `dominance` single-queries (`config_value.py:46`, `dominance.py:54`): their `.sc` signatures
|
||||
are presumed to match what their specs emit (confirm separately; not exercised here).
|
||||
|
||||
Open question: **CLOSED by call-graph trace (2026-10-12).** No shipped product path and no in-repo
|
||||
benchmark/test reached the real single-query `JoernBackend.query("taint")`; the bug was latent but unreached
|
||||
by the product. Traced:
|
||||
|
||||
- Shipped push/scan/replay (`cli.py:1334` → `scan_repository` → `_scan_repository_impl`) is DOUBLE
|
||||
shielded: Phase 2 fetches taint via the **batch route** (`run_batch`, always wired by `build()`), and
|
||||
Phase 3's `scan_region` (scan.py:559) runs against a **`_prefetched` CpgResult** whose `run` ignores the
|
||||
query and returns the already-fetched batch rows (scan.py:1472). So `scan_region → taint_verdict →
|
||||
_usable_flows → cpg.run("taint", …)` is a no-op replay that never reaches `JoernBackend.query()`.
|
||||
- The non-batching fallback (`scan.py:519 cpg.run(kind, params)`) only runs for a backend without
|
||||
`run_batch`; `JoernBackend` always provides it (backend.py:625/658), so it is dead for Joern.
|
||||
- In-repo real-backend benchmarks both use the batch path: `benchmarks/ranking/position.py` via
|
||||
`scan_repository`; `benchmarks/learn/engine_trace_worker.py` via `cpg.run_batch_once("taint", …)`.
|
||||
- `tests/test_model_taint.py` / `tests/test_model_pipeline.py` drive `verdict`/`scan_region` against FAKE
|
||||
canned-row `CpgResult`s, not a real `JoernBackend`, so they never exercised `query()` with these params.
|
||||
- The only callers that reached the real single-query path were **out-of-product ad-hoc harnesses** (pair /
|
||||
measurement scripts, the banks MVP). Regression onset: **562b670 (2026-09-25)**, when `request_params`
|
||||
began emitting `fixedOrigin`/`originAnchors` (taint.sc never declared them on `exec`; they are batch-JSON
|
||||
only by design). Residual unknown: any EXTERNAL/historical pair harness that drove single-query taint
|
||||
against a real backend after that date would have silently zeroed; none is present in-repo to confirm.
|
||||
|
||||
The fix restores the single-query path for pair/direct use without altering push behaviour.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `benchmarks/measurements/2026-10-12-banks-fixed-search-mvp/`: the batch path works (11 flow rows,
|
||||
`write_text` sinks); the v1 single-query attempt returned 0/0 on `cpg query taint failed`.
|
||||
- Raw joern error captured by `banks_debug.py` (scratchpad): `Missing argument: --cpgFile; Unknown
|
||||
arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources`.
|
||||
|
||||
## Suggested fix (not yet applied)
|
||||
|
||||
Pick one, lowest-risk first:
|
||||
|
||||
1. **Make `backend.query()` filter params to the script's declared signature** before building `--param` /
|
||||
named-arg lists (parse the `@main def exec(` arg names once, intersect). Fixes every current and future
|
||||
signature skew for all single-query scripts, symmetrically with how the batch path already tolerates
|
||||
extras. Preferred.
|
||||
2. Add the five params to `taint.sc`'s `exec` signature as ignored passthroughs (mirrors batch semantics but
|
||||
duplicates the JSON parsing already in the script; narrower).
|
||||
3. Route `_usable_flows` through `run_batch` with a one-element request (makes single-query taint share the
|
||||
tolerant path; changes the call shape upstream).
|
||||
|
||||
Add a regression test under the joern-gated suite: `cpg.run("taint", request_params(path_spec, …))` must
|
||||
return a parsed payload (not `None`) on a fixture CPG, asserting the instrument actually executed.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Brief: finding-feedback-loop — what happens after a finding is judged wrong
|
||||
# Brief: finding-feedback-loop, what happens after a finding is judged wrong
|
||||
|
||||
**Status:** brief only, unapproved. Successor scope lifted out of `contributor-scan`, which is mid-implementation
|
||||
and does not cover this.
|
||||
@@ -7,13 +7,13 @@ and does not cover this.
|
||||
|
||||
Two task groups were drafted into `contributor-scan/tasks.md` on 2026-09-09 without passing through
|
||||
requirements or design. That was a workflow error: they are not refinements of scan integration, they are a
|
||||
different feature — a loop that learns from findings a human rejected — and writing them as tasks skipped the
|
||||
different feature (a loop that learns from findings a human rejected) and writing them as tasks skipped the
|
||||
two phases that decide whether the feature is shaped right. They are lifted here to be specified properly.
|
||||
|
||||
## The gap this exists to close
|
||||
|
||||
Nine engine defects were found in one session against two repositories. **Three of them were the same bug in
|
||||
three different matchers**: a spec token matched as a substring rather than as a word — `resolve` inside
|
||||
three different matchers**: a spec token matched as a substring rather than as a word: `resolve` inside
|
||||
`resolveUrl` in the sanitizer matcher, `user` inside `users` in the discharger matcher, `gets` inside `fgets`
|
||||
in the sink matcher. Each was found by a measurement, fixed locally, and left no trace. The third survived
|
||||
until a 89k-line C library made it visible as 26 identical false positives.
|
||||
@@ -60,17 +60,17 @@ sound. The same machinery writing sink tables is the thing that produced the lea
|
||||
|
||||
## What the shape looks like
|
||||
|
||||
* **Per-repository, declarative, committed by the developer.** A dismissal file in the *target* repository —
|
||||
site, rung, reason, author — read by the tool and never written by it, the same rule the declared policy
|
||||
* **Per-repository, declarative, committed by the developer.** A dismissal file in the *target* repository (
|
||||
site, rung, reason, author), read by the tool and never written by it, the same rule the declared policy
|
||||
file already follows. Reviewed like code, diffable, and a dismissal without a reason is how a suppression
|
||||
list becomes a graveyard nobody can audit.
|
||||
* **Dismissals are labels, not suppressions.** With the known vulnerabilities a `benchmarks/repos/` recipe
|
||||
already declares, a dismissal file gives a per-repository evaluation set with *both* classes in it — which
|
||||
already declares, a dismissal file gives a per-repository evaluation set with *both* classes in it, which
|
||||
is what an optimiser needs and what a suppression list normally throws away.
|
||||
* **A ledger for the maintainer.** Counter-examples grouped by what they share (same token, same matcher
|
||||
clause, same witness shape), so a recurring cause is visible as a cause rather than as five separate bugs.
|
||||
* **Split-respecting adoption.** Train split only, holdout never read, the three gates byte-identical, and
|
||||
precision *and* recall reported on both sides — an adoption that quietly trades recall is worse than none.
|
||||
precision *and* recall reported on both sides; an adoption that quietly trades recall is worse than none.
|
||||
|
||||
## The acceptance test for the model's role
|
||||
|
||||
@@ -80,7 +80,7 @@ recover a cause we already know should not be trusted to propose one we do not.
|
||||
## Known limits to carry forward
|
||||
|
||||
* The pair corpus could not see any of the nine defects; whole-repository measurement is what found them, and
|
||||
this loop inherits that. Its evaluation set has to include repositories, not only pairs.
|
||||
this loop inherits that. Its evaluation set has to include repositories as well as pairs.
|
||||
* Only two repositories exist today (one Python, one C), and the C one has never produced a true positive.
|
||||
An optimiser tuned on that evidence base would be tuned on very little.
|
||||
* `contributor-scan` Req 8 already warns about the +16% overfitting gap between the slice a change was
|
||||
|
||||
@@ -10,7 +10,7 @@ anything.
|
||||
|
||||
The design turns on one asymmetry: **two of those three bugs needed no judgement at all.** Once the first was
|
||||
understood, the other two were the same fix carried across. So the automation that pays is *propagation and
|
||||
memory*, and the model is confined to the one step that genuinely needs inference — naming what a group has in
|
||||
memory*, and the model is confined to the one step that genuinely needs inference: naming what a group has in
|
||||
common.
|
||||
|
||||
### Goals
|
||||
@@ -22,7 +22,7 @@ common.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- **Automated fact-table edits.** Not deferred — prohibited. See Boundary Commitments.
|
||||
- **Automated fact-table edits.** Prohibited rather than deferred. See Boundary Commitments.
|
||||
- Suppressing findings silently. A dismissal makes a finding *labelled*, never invisible.
|
||||
- A general-purpose optimiser. The only surfaces evolved are two prompt constants.
|
||||
- Replacing the pair corpus. It is the holdout that keeps per-repository tuning honest.
|
||||
@@ -38,10 +38,10 @@ common.
|
||||
|
||||
### Out of Boundary
|
||||
|
||||
- `semantic/facts`, `semantic/obligations`, `ruleset/`, the declared policy file — **no automated writer, by
|
||||
- `semantic/facts`, `semantic/obligations`, `ruleset/`, the declared policy file: **no automated writer, by
|
||||
any path, at any time.** This is the boundary the whole design exists to respect.
|
||||
- The three gates. They are the invariant an adoption is measured against, not a thing this spec changes.
|
||||
- `model/taint.py`, `dominance.py`, `config_value.py` — arbiters are fixed here; a proposal that would change
|
||||
- `model/taint.py`, `dominance.py`, `config_value.py`: arbiters are fixed here; a proposal that would change
|
||||
one is a proposal for a human, not an edit.
|
||||
|
||||
### Allowed Dependencies
|
||||
@@ -64,7 +64,7 @@ Three pieces already exist and are reused rather than rebuilt:
|
||||
- **`benchmarks/repos/*.toml`** declares known vulnerabilities per checkout, with `in_scope` stating whether a
|
||||
family can decide one at all. That is the positive half of an evaluation set, already committed.
|
||||
- **`pairs.py`** already carries a train/holdout split and the machinery that respects it. The leak of
|
||||
2026-09-05 was in the *lever*, not the splitter — `semantic/loo.evaluate_loo` was honest throughout.
|
||||
2026-09-05 was in the *lever*, not the splitter; `semantic/loo.evaluate_loo` was honest throughout.
|
||||
- **`redaction.py`** is applied to every prompt and persisted trajectory, and applies here unchanged.
|
||||
|
||||
### Boundary Map
|
||||
@@ -87,21 +87,21 @@ benchmarks/repos/*.toml ── read ────▶ evaluation set
|
||||
|
||||
**A dismissal is a label, not a suppression.** A suppressed finding disappears and its reason rots; a
|
||||
labelled one is reported as dismissed with its reason, and a dismissal that stops matching is reported too.
|
||||
This is what turns a suppression list into the negative half of an evaluation set — the thing that makes the
|
||||
This is what turns a suppression list into the negative half of an evaluation set, the thing that makes the
|
||||
rest of the feature possible.
|
||||
|
||||
**The tool never writes the dismissal file.** Same rule as the declared policy file, for the same reason: a
|
||||
file the tool edits is a file no reviewer trusts. It is committed by a developer and reviewed like code.
|
||||
|
||||
**Grouping is deterministic; only the hypothesis is not.** Entries are grouped by shared spec token, matcher
|
||||
clause, family and witness shape — all mechanical. The model is asked one question about a group it did not
|
||||
clause, family and witness shape; all mechanical. The model is asked one question about a group it did not
|
||||
choose, and its answer goes to a queue. This keeps the inference confined to the step that needs it.
|
||||
|
||||
**Evolution targets the question, never the facts.** A prompt is how we ask; a fact table is what we claim.
|
||||
`CANDIDATE_QUESTION` and `residual_question` are already the LLM-facing surface and are the only mutable
|
||||
targets. Enforced by a test that fails if any writer touches the fact modules, not by convention.
|
||||
|
||||
**The holdout is never read.** Not "should not" — the evaluation-set API has no path to it during
|
||||
**The holdout is never read.** The evaluation-set API has no path to it during
|
||||
optimisation, and a test asserts the absence. The 2026-09-05 leak was possible because reading it was merely
|
||||
discouraged.
|
||||
|
||||
@@ -123,9 +123,9 @@ benchmarks/ledger/
|
||||
|
||||
### Changed
|
||||
|
||||
- `cli.py` — a `feedback` subcommand: `ledger`, `groups`, `propose`, `evolve`. Maintainer surface; `scan`
|
||||
- `cli.py`: a `feedback` subcommand: `ledger`, `groups`, `propose`, `evolve`. Maintainer surface; `scan`
|
||||
gains only the dismissal read.
|
||||
- `reports.py` — a dismissed finding is rendered as dismissed with its reason.
|
||||
- `reports.py`: a dismissed finding is rendered as dismissed with its reason.
|
||||
|
||||
### Unchanged and load-bearing
|
||||
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
## Introduction
|
||||
|
||||
Nine engine defects were found in one session against two repositories. **Three were the same bug in three
|
||||
matchers** — a spec token matched as a substring rather than a word: `resolve` inside `resolveUrl` in the
|
||||
matchers**: a spec token matched as a substring rather than a word: `resolve` inside `resolveUrl` in the
|
||||
sanitizer matcher, `user` inside `users` in the discharger matcher, `gets` inside `fgets` in the sink matcher.
|
||||
Each was found by a measurement, fixed locally, and left no trace; the third survived until an 89k-line C
|
||||
library made it visible as 26 identical false positives.
|
||||
|
||||
Nothing in the system remembers that a finding was wrong. There is no artifact saying *this site, at this
|
||||
rung, is not a defect, and here is why* — so a class recurs, and each recurrence costs another measurement.
|
||||
rung, is not a defect, and here is why*, so a class recurs, and each recurrence costs another measurement.
|
||||
|
||||
Two audiences want that artifact for different reasons. A maintainer wants the *propagation* automated: two of
|
||||
the three matcher bugs needed no judgement at all, only the fix carried across. A developer wants to dismiss a
|
||||
finding in their own repository, have it stay dismissed, and have the reason reviewable beside the code — a
|
||||
finding in their own repository, have it stay dismissed, and have the reason reviewable beside the code; a
|
||||
tool that cannot be told it is wrong is one they stop running.
|
||||
|
||||
This feature is bounded by a rule the project has held from the start and a leak that shows why. An optimiser
|
||||
may evolve **the question** — the judge and residual prompts, which are how we ask. It may never author **the
|
||||
facts** — tables, rulesets, the declared policy file, which are what we claim. On 2026-09-05 the improve lever
|
||||
may evolve **the question**: the judge and residual prompts, which are how we ask. It may never author **the
|
||||
facts**: tables, rulesets, the declared policy file, which are what we claim. On 2026-09-05 the improve lever
|
||||
admitted candidate shapes because they "recover a currently missed holdout pair"; five of eleven were taught by
|
||||
the holdout pairs they then recovered, and a holdout Youden of **−0.059 was reported as +0.059**.
|
||||
|
||||
@@ -38,11 +38,11 @@ recorded and my colleagues can see why.
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall read dismissals from a declarative file in the **target** repository, and shall never
|
||||
write to it — the same rule the declared policy file already follows.
|
||||
write to it, the same rule the declared policy file already follows.
|
||||
2. A dismissal shall carry the site it concerns, the rung it was dismissed at, a reason, and an author.
|
||||
3. The system shall reject a dismissal that carries no reason, because a suppression list without reasons is
|
||||
a graveyard nobody can audit.
|
||||
4. The system shall report a dismissed finding as dismissed, with its reason, rather than omitting it — so a
|
||||
4. The system shall report a dismissed finding as dismissed, with its reason, rather than omitting it, so a
|
||||
stale dismissal is visible rather than silent.
|
||||
5. When a dismissal no longer matches any finding, the system shall say so, because a dismissal that has
|
||||
stopped applying is either a fixed bug or a moved target and both are worth knowing.
|
||||
@@ -54,20 +54,20 @@ cause is visible as a cause rather than as five separate bugs.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When a finding is contradicted — by a dismissal, by the judge with coverage, or by a declared contract —
|
||||
1. When a finding is contradicted (by a dismissal, by the judge with coverage, or by a declared contract)
|
||||
the system shall record the site, the rung, the witness, the spec tokens that produced it, and the commit.
|
||||
2. Each ledger entry shall be runnable as a regression case: this site, at this commit, shall not be reported
|
||||
at that rung.
|
||||
3. The ledger's regression cases shall run offline in CI, so that re-introducing a fixed cause fails a test
|
||||
rather than a repository scan four weeks later.
|
||||
4. The system shall group ledger entries by what they share — spec token, matcher clause, family, witness
|
||||
shape — and report the groups by size.
|
||||
4. The system shall group ledger entries by what they share (spec token, matcher clause, family, witness
|
||||
shape) and report the groups by size.
|
||||
5. The ledger shall be committed evidence, not a local artifact.
|
||||
|
||||
### Requirement 3: A model may propose a cause, and only propose it
|
||||
|
||||
**Objective:** As a maintainer, I want the model to do the part that needs judgement — naming what a group of
|
||||
counter-examples has in common — without it deciding anything.
|
||||
**Objective:** As a maintainer, I want the model to do the part that needs judgement (naming what a group of
|
||||
counter-examples has in common) without it deciding anything.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
@@ -132,7 +132,7 @@ quietly loses recall is worse than none.
|
||||
|
||||
1. The evaluation set shall include whole repositories, not pairs alone: the pair corpus could not see any of
|
||||
the nine defects that motivate this feature, and the gates stayed byte-identical through every fix.
|
||||
2. The system shall state how many repositories its evidence base contains, because two — one of which has
|
||||
never produced a true positive — is very little to tune against.
|
||||
2. The system shall state how many repositories its evidence base contains, because two (one of which has
|
||||
never produced a true positive) is very little to tune against.
|
||||
3. Where an adoption is measured on one repository only, the system shall label it as such rather than
|
||||
reporting it as a general improvement.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Brief: flow-aware-ranking — spend the expensive query where the cheap evidence is strongest
|
||||
# Brief: flow-aware-ranking, spend the expensive query where the cheap evidence is strongest
|
||||
|
||||
> **Status 2026-10-02:** dormant. Brief only and unapproved; its 2026-09-12 amendment placed ranking inside the pre-push safety net, where the ranker now lives.
|
||||
|
||||
**Status:** brief only, unapproved.
|
||||
|
||||
## Current amendment — 2026-09-12
|
||||
## Current amendment, 2026-09-12
|
||||
|
||||
This section supersedes forward-looking interpretations below; the iteration history is retained.
|
||||
The product goal is a scalable, actionable pre-push security safety net as AI accelerates code changes.
|
||||
@@ -23,7 +23,7 @@ need explicit summaries or unresolved coverage, never automatic vendor reintrodu
|
||||
- Latest recorded phase-2 budgets: PMPro **82**, WP Statistics **91**, MW WP Form **54**, VAmPI **16**.
|
||||
The `<50` gate remains unmet. The evidence passes recorded no unanswered pairs. These are stored
|
||||
ranking measurements, not fresh detection/precision results or a pre-push latency demonstration.
|
||||
- **Equal-score ties are not the whole remaining problem.** At fixed scores, perfect tie-breaking
|
||||
- Equal-score ties are not the whole remaining problem. At fixed scores, perfect tie-breaking
|
||||
still needs at least **68** regions for PMPro and **86** for WP Statistics (67/85 strictly
|
||||
higher-scored same-tier regions precede the targets). An LLM restricted to equal-score ties
|
||||
cannot pass `<50`. Within-tier reordering across scores or a new feature is a distinct experiment.
|
||||
@@ -95,7 +95,7 @@ Three regions from one plugin, with what the ranker sees today and what is actua
|
||||
| `pmpro_rest_api_get_membership_level` | **0.80** | none — no SQL sink at all |
|
||||
|
||||
A region with no sink outranks two holding raw concatenated SQL. The discriminating signal is in the sink
|
||||
call's own source text — interpolated versus bound — and nothing in the ranker reads it.
|
||||
call's own source text (interpolated versus bound), and nothing in the ranker reads it.
|
||||
|
||||
One detail decides whether a keyword rule suffices, and it does not: `esc_sql` IS in scope for the CVE's
|
||||
function, applied to a different statement. "A sanitizer is present, rank it lower" demotes precisely the
|
||||
@@ -122,8 +122,8 @@ finding**, it decides what is looked at, and `regions_truncated` already reports
|
||||
Each phase ships only if it beats the previous one **on a repository the change was not developed against**.
|
||||
A phase that cannot show that is not an improvement, it is a story.
|
||||
|
||||
- **Phase 0 — the oracle and the baseline.** The evidence extractor (no model) and a scoring harness over the
|
||||
pinned CVEs, reporting each one's rank position. Gate: reproduces today's numbers — CVE-2023-23488 at
|
||||
- Phase 0, the oracle and the baseline. The evidence extractor (no model) and a scoring harness over the
|
||||
pinned CVEs, reporting each one's rank position. Gate: reproduces today's numbers, CVE-2023-23488 at
|
||||
position 390 of 4,463, 83.8% of regions in one tier. Without a baseline nothing later is measurable.
|
||||
|
||||
**Done 2026-09-10.** `benchmarks/ranking/position.py`, committed as
|
||||
@@ -138,36 +138,36 @@ A phase that cannot show that is not an improvement, it is a story.
|
||||
|
||||
**And this corrects the cross-assessment above.** "Ranking's job is concentration, not inclusion" was
|
||||
measured on `pmpro` alone, where the CVE is inside the budget. On the other two plugins the CVE sits at
|
||||
512 and 503 — **outside a 500-region budget** — so the current ranker never examines it at all, and the
|
||||
512 and 503, **outside a 500-region budget**, so the current ranker never examines it at all, and the
|
||||
slices found those CVEs only because a slice has too few regions to exclude anything. Both jobs are
|
||||
real: inclusion on two repositories, concentration on all three. The single metric `budget_at_recall`
|
||||
covers both, which is why it is the one that matters.
|
||||
|
||||
The instrument is `evidenceOnly` mode on the taint query, recorded on every scan as `ModelScanResult.tiers`
|
||||
and `tier_counts`, and acted on by nothing — so every later phase is measured against a record made by
|
||||
and `tier_counts`, and acted on by nothing, so every later phase is measured against a record made by
|
||||
scans that did not know they were being measured.
|
||||
|
||||
**Measured cost of the instrument**, 637-file graph, the scan's own 500-region request set:
|
||||
|
||||
2,500 pairs in 98.9 s including JVM load — about 40 ms per pair
|
||||
2,500 pairs in 98.9 s including JVM load, about 40 ms per pair
|
||||
tier 0 (no sink of that family in reach): 2,161 of 2,500 = 86%
|
||||
|
||||
Two attempts before that ran past an hour each without producing a payload, and the cause was mine both
|
||||
times: repository-wide walks — the sink scan, the reachable-method BFS, `familyInRepo` — repeated per
|
||||
times: repository-wide walks (the sink scan, the reachable-method BFS, `familyInRepo`) repeated per
|
||||
request. Memoising them across the batch is what made "milliseconds per pair" true rather than claimed.
|
||||
|
||||
**And the same memoisation reaches the dataflow mode.** The identical 250-request set that cost 2,026.3 s
|
||||
(8,105 ms per request) with both joins on now costs **287.0 s — 1,148 ms per request — for the same 5,779
|
||||
(8,105 ms per request) with both joins on now costs **287.0 s (1,148 ms per request) for the same 5,779
|
||||
rows.** Most of what contributor-scan 5.13 measured as "the base taint query" was the per-request scans,
|
||||
not the dataflow. That correction is recorded there.
|
||||
- **Phase 1 — tier 0 only, no model.** Exact pruning. Gate: **53% fewer taint requests issued, measured** —
|
||||
- Phase 1, tier 0 only, no model. Exact pruning. Gate: **53% fewer taint requests issued, measured**,
|
||||
and nothing more. 1,177 requests at 6.63 s is still 2.2 hours, so this phase does NOT make the scan finish;
|
||||
it halves the problem. Claiming completion here would send someone looking for a bug where there is none.
|
||||
|
||||
**Done 2026-09-10, gate exceeded, scan still does not finish — and the reason corrects phase 0's cost
|
||||
**Done 2026-09-10, gate exceeded, scan still does not finish, and the reason corrects phase 0's cost
|
||||
figure.** On the 637-file plugin: `tier_counts = {0: 2161, 1: 60, 2: 20, 3: 211, 4: 48}`, **2,161 of 2,500
|
||||
requests pruned (86%)**, evidence pass 94.5 s. The remaining **339 requests still hit the 2,400 s ceiling**
|
||||
— more than 7 s each.
|
||||
requests pruned (86%)**, evidence pass 94.5 s. The remaining **339 requests still hit the 2,400 s ceiling**,
|
||||
more than 7 s each.
|
||||
|
||||
**The scan finishes, 2026-09-11.** Not because of anything in this spec: contributor-scan 5.13 found
|
||||
that every query batch had been recomputing Joern's dataflow overlay at load since PHP builds went
|
||||
@@ -188,12 +188,12 @@ A phase that cannot show that is not an improvement, it is a story.
|
||||
which 88% were tier 0 and returned instantly; the ~30 real requests in it cost about 9.5 s each. Pruning
|
||||
removes exactly the cheap questions and keeps the expensive ones, so it cannot make those faster. The
|
||||
honest per-request cost of a taint question that *can* have an answer is **7–10 s at `callDepth = 3`**,
|
||||
and that is the number phase 2 and 5.13 must work against — not 1.1 s, and not 6.6 s either.
|
||||
- **Phase 2 — heuristic tiers, still no model.** The vector and tiers defined below, ordered within a tier
|
||||
by a published weighted sum. Gate: `budget_at_recall` — the smallest K holding every pinned CVE — **below
|
||||
and that is the number phase 2 and 5.13 must work against, not 1.1 s, and not 6.6 s either.
|
||||
- Phase 2, heuristic tiers, still no model. The vector and tiers defined below, ordered within a tier
|
||||
by a published weighted sum. Gate: `budget_at_recall` (the smallest K holding every pinned CVE) **below
|
||||
50 on every pinned repository**, and `pmpro`'s taint query completing at that K. This is the phase that
|
||||
makes a scan finish, because the job is concentration: the CVE is already inside 500, the question is
|
||||
whether it is inside 50. **If this passes, the model is a refinement, not the plan** — and knowing that is
|
||||
whether it is inside 50. **If this passes, the model is a refinement, not the plan**, and knowing that is
|
||||
worth more than assuming it.
|
||||
**Implemented 2026-09-11, measurement in flight.** `Evidence.score` is the published weighted sum, and
|
||||
every weight is in one table in `model/evidence.py`: per open sink +1.0 (capped at 5), per cleansed or
|
||||
@@ -304,17 +304,17 @@ A phase that cannot show that is not an improvement, it is a story.
|
||||
facts and a weight table can do; the ties are phase 3's. After that, within-tier order is phase 3's -- a model reading the code the ties cannot --
|
||||
or one more exact path fact (`distance`), and the gate stays unmet until one of them moves it.
|
||||
|
||||
- **Phase 3 — the LLM ranker.** Same evidence, ordered by a model. Gate: beats phase 2 leave-one-repository-out.
|
||||
- Phase 3, the LLM ranker. Same evidence, ordered by a model. Gate: beats phase 2 leave-one-repository-out.
|
||||
If it does not, phase 2 ships and phase 3 does not.
|
||||
- **Phase 4 — break the closed loop.** An exploration slice (~10% of budget sampled outside the top-K) and
|
||||
- Phase 4, break the closed loop. An exploration slice (~10% of budget sampled outside the top-K) and
|
||||
full labelling on repositories small enough to arbitrate every region. Gate: a measured estimate of the
|
||||
selection bias in phase 3's numbers.
|
||||
- **Phase 5 — prompt optimisation (GEPA / SIMBA / DSPy).** Only after 3 and 4. An optimiser run against a
|
||||
- Phase 5, prompt optimisation (GEPA / SIMBA / DSPy). Only after 3 and 4. An optimiser run against a
|
||||
censored metric optimises the censorship.
|
||||
|
||||
## What the prompt has to carry
|
||||
|
||||
The question put to the model is **not** "is this vulnerable" — that is adjudication, it is the arbiter's job,
|
||||
The question put to the model is **not** "is this vulnerable". That is adjudication, the arbiter's job,
|
||||
and it is the one thing a ranker must never be asked. It is:
|
||||
|
||||
> Given a fixed budget of expensive dataflow queries, which of these sites are worth spending it on?
|
||||
@@ -326,14 +326,14 @@ reachable from a request handler.
|
||||
|
||||
## Constraints, from measurement and from prior scars
|
||||
|
||||
- **Cost.** One model call per region is 4,463 calls a scan. Batch over a prefilter, or it does not ship.
|
||||
- **The loop is closed.** A region the ranker excludes is never arbitrated, never labelled, so the ranker
|
||||
- Cost. One model call per region is 4,463 calls a scan. Batch over a prefilter, or it does not ship.
|
||||
- The loop is closed. A region the ranker excludes is never arbitrated, never labelled, so the ranker
|
||||
never learns it was wrong. Every run confirms the ordering it already had.
|
||||
- **N projects.** Four pinned CVEs is not a training set. This project already has the closed-loop
|
||||
- N projects. Four pinned CVEs is not a training set. This project already has the closed-loop
|
||||
train-on-test scar; the reported figure must come from a repository the optimiser never saw.
|
||||
- **Determinism.** Two runs of one repository must agree or a baseline diff is meaningless. The ranking is
|
||||
- Determinism. Two runs of one repository must agree or a baseline diff is meaningless. The ranking is
|
||||
recorded in the manifest.
|
||||
- **Degradation.** With no model configured, phases 0–2 still rank, and the report says which ranker ordered
|
||||
- Degradation. With no model configured, phases 0–2 still rank, and the report says which ranker ordered
|
||||
the scan.
|
||||
|
||||
## Cross-assessment against the measurements
|
||||
@@ -342,7 +342,7 @@ Read against what was actually measured today, the plan above has two errors and
|
||||
here, corrected, rather than silently rewritten, because the corrections are themselves results.
|
||||
|
||||
**Phase 1's gate is wrong.** It says pruning makes `pmpro`'s taint query complete. It does not. Pruning
|
||||
removes 53% of requests, and 1,177 × 6.63 s is still **2.2 hours** — nowhere near the 300 s default or the
|
||||
removes 53% of requests, and 1,177 × 6.63 s is still **2.2 hours**, nowhere near the 300 s default or the
|
||||
2,400 s that was tried. Pruning halves the problem; it does not solve it. The honest gate is "53% fewer
|
||||
requests issued, measured", and completion belongs to phase 2. Stating otherwise would have sent someone
|
||||
looking for a bug in phase 1 when phase 1 had done exactly what it can.
|
||||
@@ -356,8 +356,8 @@ the numbers it had; the numbers were the instrument's.
|
||||
**Ranking's job is concentration, not inclusion.** The brief implies the CVE is missed because the ranker
|
||||
excluded it. It did not: CVE-2023-23488's region sits at position 390 of 4,463, inside the 500-region budget,
|
||||
and was examined. It goes unreported because the budget is spent on 500 regions when 50 would do, and 500
|
||||
is unaffordable at 6.63 s per family per region. So the ranker's success metric is not "is the CVE in the
|
||||
budget" — it already is — but **"how small can the budget be while the CVE stays in it"**. That is a
|
||||
is unaffordable at 6.63 s per family per region. So the ranker's success metric is
|
||||
**"how small can the budget be while the CVE stays in it"**; the CVE is already in the budget. That is a
|
||||
different objective, and it is the one that makes a scan finish.
|
||||
|
||||
**The gap: nothing above says what the evidence IS.** "Sink shape" and "cleansing on the call" are named,
|
||||
@@ -366,24 +366,24 @@ check for language leakage. The rest of this brief defines it.
|
||||
|
||||
Two things the measurements confirm and the plan should lean on harder:
|
||||
|
||||
- **The joins are cheap and must survive any ranker.** 22% of query cost, two of three CVEs. A ranker that
|
||||
- The joins are cheap and must survive any ranker. 22% of query cost, two of three CVEs. A ranker that
|
||||
demotes field- or hook-carried regions because they show no direct source is a ranker that loses those
|
||||
CVEs. The evidence vector below therefore carries the join's own stage-one result as a field.
|
||||
- **84% of regions in one tier means any separation is progress**, and the separation does not need to be
|
||||
- 84% of regions in one tier means any separation is progress, and the separation does not need to be
|
||||
clever to be large. A tier 0 that is merely exact already reorders the budget more than the current ranker
|
||||
ever has.
|
||||
|
||||
## The evidence vector, defined
|
||||
|
||||
The unit is a **(region, family) pair**. For each, a fixed vector of fields is computed. Every field is
|
||||
defined in terms of **fact-table categories** — source, sink, sanitizer, dispatch, access — never in terms of
|
||||
defined in terms of **fact-table categories** (source, sink, sanitizer, dispatch, access), never in terms of
|
||||
a language's syntax, which is what makes the definition language-agnostic: any language with a fact table
|
||||
gets the same vector, and a heuristic that mentions `$wpdb` or `wp_ajax_` has by construction left the
|
||||
vector and must be rejected in review.
|
||||
|
||||
**Where it is computed matters as much as what it is.** The graph is already built for the arbiter, at 76 s
|
||||
for 637 files. Extracting these fields is a CPG query **without dataflow** — call nodes, their arguments,
|
||||
their enclosing methods, the call graph one hop out — which is milliseconds per call rather than 6.63 s per
|
||||
for 637 files. Extracting these fields is a CPG query **without dataflow** (call nodes, their arguments,
|
||||
their enclosing methods, the call graph one hop out), which is milliseconds per call rather than 6.63 s per
|
||||
request. The extractor is therefore a sibling of `taint.sc` that never calls `reachableByFlows`:
|
||||
|
||||
> **Tiering is the CPG query without dataflow. Arbitration is the CPG query with it. Same graph, same
|
||||
@@ -393,7 +393,7 @@ That is the whole architecture in one line, and it is why the evidence is langua
|
||||
merely language-neutral-in-intent: the CPG normalises syntax across frontends, so "the argument of this call
|
||||
is a literal" means the same thing in PHP, Python and C.
|
||||
|
||||
### Sink evidence — from `[[sink]]` facts of family F
|
||||
### Sink evidence, from `[[sink]]` facts of family F
|
||||
|
||||
| field | definition | exactness |
|
||||
|---|---|---|
|
||||
@@ -401,7 +401,7 @@ is a literal" means the same thing in PHP, Python and C.
|
||||
| `shape` | per sink call, one of: **`literal`** (the vulnerable-position argument is a constant), **`bound`** (the call is a declared-safe form — a `parameterized` sanitizer fact wraps it, or its arity/shape matches the bound form the taint query already recognises), **`computed`** (the argument is an expression: concatenation, interpolation, a variable, a call result) | exact for `literal` and `bound`; `computed` is the residue |
|
||||
| `cleansed_on_call` | per sink call, whether a sanitizer of F is an **ancestor of the vulnerable-position argument in the AST** — not merely present in the same function | exact, and this is the field a text scan gets wrong: `esc_sql` in scope on a different statement is `false` here |
|
||||
|
||||
### Source evidence — from `[[source]]` facts and the entry-point contract
|
||||
### Source evidence, from `[[source]]` facts and the entry-point contract
|
||||
|
||||
| field | definition | exactness |
|
||||
|---|---|---|
|
||||
@@ -411,7 +411,7 @@ is a literal" means the same thing in PHP, Python and C.
|
||||
| `access` | the declared access level, **with provenance**: `declared` (a registration or route contract says so) or `inferred` (nothing found) — because "public" meaning "no decorator found" is the false positive the project has already paid for | exact |
|
||||
| `carried` | the region reads a **field** whose assignment is fed by a source anywhere in the file, or a **dispatch** value whose registered producer is — i.e. stage one of the two-stage join, which is already computed and memoised, reused here as evidence | exact where computed; absent (not false) where the join was not run |
|
||||
|
||||
### Path evidence — cheap structural proxies for what dataflow would establish
|
||||
### Path evidence, cheap structural proxies for what dataflow would establish
|
||||
|
||||
| field | definition |
|
||||
|---|---|
|
||||
@@ -438,14 +438,14 @@ is a literal" means the same thing in PHP, Python and C.
|
||||
|
||||
Two properties are load-bearing and must hold in every implementation:
|
||||
|
||||
1. **Only tier 0 excludes; every other tier orders.** So the only place exactness is *required* is tier 0,
|
||||
1. Only tier 0 excludes; every other tier orders. So the only place exactness is *required* is tier 0,
|
||||
and tier 0's predicate has a proof: a family with no sink in reach cannot produce a flow, in any language,
|
||||
under any dataflow model. Above tier 0, being wrong costs position, and `regions_truncated` reports what
|
||||
position cost.
|
||||
2. **Tier 0's scope must match the arbiter's.** At `callDepth = 0` the scope is the region; at depth k it is
|
||||
2. Tier 0's scope must match the arbiter's. At `callDepth = 0` the scope is the region; at depth k it is
|
||||
the region plus its reachable methods. A tier 0 computed on the file when the arbiter will look k hops
|
||||
out is not exact and must not be called so. The repository-wide form (`family_in_repo == false`) is exact
|
||||
at every depth and is the cheap first cut — it alone removes all 487 `deserialization` requests on `pmpro`.
|
||||
at every depth and is the cheap first cut. It alone removes all 487 `deserialization` requests on `pmpro`.
|
||||
|
||||
**Within a tier**, order by a score. In phase 2 that score is a fixed weighted sum over the vector, published
|
||||
in the manifest so two runs agree. In phase 3 it is the model's ordering **of the same vector**, which is
|
||||
@@ -456,23 +456,23 @@ what makes the two comparable: same input, different ordering function, one held
|
||||
The vector above is exact wherever exactness was cheap. What remains genuinely uncertain is narrower than
|
||||
"ranking":
|
||||
|
||||
- **Ambiguous cleansing.** `cleansed_on_call` is exact when the sanitizer wraps the argument directly. It is
|
||||
`false` — correctly, but unhelpfully — when cleansing happens two statements earlier into a variable that is
|
||||
- Ambiguous cleansing. `cleansed_on_call` is exact when the sanitizer wraps the argument directly. It is
|
||||
`false` (correctly, but unhelpfully) when cleansing happens two statements earlier into a variable that is
|
||||
then interpolated. Deciding whether *that* cleansing reaches *this* call is reading the code, and it is the
|
||||
single largest source of tier-2/tier-3 misplacement.
|
||||
- **Unfamiliar shapes.** A sink call whose form the `bound` rule does not recognise, in a framework with no
|
||||
- Unfamiliar shapes. A sink call whose form the `bound` rule does not recognise, in a framework with no
|
||||
facts yet. The model can say "this looks bound" as an *ordering* opinion; it cannot add a `bound` fact,
|
||||
because a fact is policy.
|
||||
- **Within-tier priority** when the fixed score ties, which on a real repository it will, constantly.
|
||||
- Within-tier priority when the fixed score ties, which on a real repository it will, constantly.
|
||||
|
||||
And the boundaries:
|
||||
|
||||
- **The model never computes tier 0.** Exclusion is exact or it is a silent miss, and a model's "no sink
|
||||
- The model never computes tier 0. Exclusion is exact or it is a silent miss, and a model's "no sink
|
||||
here" is not a proof.
|
||||
- **The model never sees the question "is this vulnerable".** It sees "which of these is worth an expensive
|
||||
- The model never sees the question "is this vulnerable". It sees "which of these is worth an expensive
|
||||
query", with the vector and the sink call's source text. Adjudication is the arbiter's, and keeping it
|
||||
there is what keeps the gates byte-identical.
|
||||
- **The model's input is the vector plus text, never the arbiter's verdict** on the same run — that is the
|
||||
- The model's input is the vector plus text, never the arbiter's verdict on the same run. That is the
|
||||
closed loop feeding on itself.
|
||||
|
||||
## Metrics, so every phase can fail
|
||||
@@ -498,8 +498,8 @@ answer is visible:
|
||||
1. ~~Where does the evidence extractor live?~~ **It is a CPG query without dataflow, a sibling of `taint.sc`
|
||||
in `cpg/queries/`.** Not the mapper: the mapper reads text and would have to re-derive per language what
|
||||
the graph already normalises. The vector is language-agnostic *because* the graph is.
|
||||
2. ~~Per file or per reachable set?~~ **Both, in that order.** `family_in_repo` first — exact at any depth,
|
||||
free, a fifth of the budget on `pmpro` — then the reachable set at the arbiter's own `callDepth`. Never the
|
||||
2. ~~Per file or per reachable set?~~ **Both, in that order.** `family_in_repo` first (exact at any depth,
|
||||
free, a fifth of the budget on `pmpro`), then the reachable set at the arbiter's own `callDepth`. Never the
|
||||
file alone above depth zero, because that is not exact and must not be called tier 0.
|
||||
3. How many pinned repositories before leave-one-out means anything? Four is not enough. This is the
|
||||
corpus ask, and it is the same ask contributor-scan 5.8 made: plugins chosen by installed base, CVEs read
|
||||
@@ -507,7 +507,7 @@ answer is visible:
|
||||
4. Does the arbiter's verdict feed back as a label? **Only through the exploration slice.** A verdict on a
|
||||
region the ranker chose is a label the policy selected, and training on it closes the loop. A verdict on
|
||||
an exploration region is the only unbiased label the scan produces, and it is the one worth keeping.
|
||||
5. **New:** the weighted sum in phase 2 has weights. Where do they come from before any model exists?
|
||||
5. New: the weighted sum in phase 2 has weights. Where do they come from before any model exists?
|
||||
Proposed: set by hand from the tier semantics, published in the manifest, and treated as a baseline for
|
||||
phase 3 to beat — not tuned, because tuning four weights on four CVEs is the overfit the plan exists to
|
||||
phase 3 to beat, not tuned, because tuning four weights on four CVEs is the overfit the plan exists to
|
||||
avoid.
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Brief: remove HarnessX after the plane proves value
|
||||
|
||||
Status: brief only, gated. Not started until `ai-service-plane` Requirement 6 records a met gate.
|
||||
|
||||
## Inventory (read-only, 2026-09-29)
|
||||
|
||||
- Code: `src/openultrasast/harness_ext.py` (97 lines) and `hunter_harness.py` (197) exist only for HarnessX;
|
||||
guarded branches in `cli.py` (372-512), `fusion.py` (LLM panels, ~100 lines), `verify_judge.py` (judge path,
|
||||
~90), `stage_processors.py` (`host_under_harnessx`, ~40), `config.py` (`HarnessxConfig`). About 575 of
|
||||
30,243 lines (1.9%). Every path falls back deterministically when the extra is absent and records a
|
||||
degradation (`harnessx_extra_unavailable`; fallbacks `run_hunter_pool`, `structural_verifier`,
|
||||
`deterministic_panels`). `evolve`, `regress`, `benchmark`, `gate` and quick mode do not depend on it.
|
||||
- Comment-only mentions (re-verified 2026-09-30): `gate.py:174`, `improve/evolve.py:9`, `slot_contract.py:7-13`.
|
||||
- Packaging: `pyproject.toml` extra `harnessx` pinned to a git commit; mypy override; `uv.lock` entry.
|
||||
- Tests: 14 files touch it; dedicated `test_harness_ext.py`, `test_hunter_harness.py`, `test_cli_hx_dispatch.py`,
|
||||
`test_verify_judge.py`; `tests/conftest.py` fixture `assert_cold_of_harnessx`.
|
||||
- Docs: README sections "Configuring the HarnessX agentic plane" and "The HarnessX self-improving cycle";
|
||||
`docs/examples.md` section 6; `docs/threat-model.md` egress and spend caps.
|
||||
- Specs: `harnessx-self-improving-rulesets` (approved, 43/43 done, Requirement 1 is direct adoption);
|
||||
`learning-harness` (30/33); `three-stage-scan` framed as its follow-up; `openrouter-sast-harness` (no
|
||||
spec.json); mentions in four more; steering `overview.md:175`.
|
||||
|
||||
## Shape of the removal
|
||||
|
||||
1. Replace the three guarded paths with plane tasks (hunter pool -> `verify`; judge -> `judge`; panels ->
|
||||
`judge`/`agree`) so their fallbacks become the only path, then delete the two modules, the config block,
|
||||
the extra, the override and the fixture.
|
||||
2. Retire the four specs' HarnessX requirements with a dated note, not a rewrite; update README and docs.
|
||||
3. Gate: the plane increment met (recall and cost), and the replaced features measured no worse on the
|
||||
development pins before deletion.
|
||||
@@ -1,507 +0,0 @@
|
||||
# Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
HarnessX leaves the product in one reviewable sequence, and the plane's memory layer takes over what HarnessX was
|
||||
meant to drive. Three facts from the code shape the design:
|
||||
|
||||
- **No HarnessX capability is measured, and one of them never existed.** The LLM paths are reached only through
|
||||
config keys, not CLI flags (`scan` has `path`, `--mode`, `--config`, `--fail-on`; `cli.py:163-167`), and
|
||||
`benchmarks/` holds no run of any of them (no `hx-hunter:` finding, no `llm-panels` decision). The
|
||||
`MetaAgent.evolve` proposer is only a docstring (`improve/evolve.py:8-9`, `harness_ext.py:7-8`). No code calls it.
|
||||
- **The plane already does the LLM work better, and outside `ousast scan`.** `verify` (two passes, known callers
|
||||
from `repo-facts`) plus `agree` (2-of-3 with the tie-break) met the ai-service-plane gate
|
||||
(`benchmarks/independent/plane-increment-2.json`: 16/20 declared sites agreed, $0.0205 per candidate). The
|
||||
standard-mode LLM hunter that users actually reach, the tool hunter at the MAP stage (`cli.py:631-640`,
|
||||
`tool_hunter.resolve_hunter_client`), is not HarnessX and stays.
|
||||
- **Everything deterministic already runs without the extra.** Every guarded branch falls back to a deterministic
|
||||
function (`run_hunter_pool`, `verify_findings`, `fuse_finding`). Removal therefore makes the fallback the only
|
||||
path. Quick mode, `benchmark`, `pairs`, `improve` and the gates never enter a guarded branch.
|
||||
|
||||
So every HarnessX capability in `ousast scan` is **retired** with its deterministic path kept. The plane's `verify`
|
||||
and `agree` tasks are the documented agentic path (`ousast plane run <Run>`). The proposer is **carried by the
|
||||
memory layer**: a persistent store, keyed by repository and pin, that plane Runs write through a model-free
|
||||
`remember` task. From that store a deterministic generator emits `RuleStatusEdit` proposals into the unchanged
|
||||
`EvolveValidator` and gate. The improvement loop becomes a plane Run whose only Model-bound tasks are the verify
|
||||
passes.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Before After
|
||||
ousast scan --mode standard ousast scan --mode standard
|
||||
hunter_pool: HxScanOrchestrator | run_hunter_pool hunter_pool: run_hunter_pool (only path)
|
||||
verify: verify_judge (llm-judge) | verify_findings verify: verification.verify_findings
|
||||
fusion: llm panels | deterministic panels fusion: deterministic panels (fuse_findings)
|
||||
MAP: tool hunter ([models] hunter, DeepSeek) MAP: tool hunter (unchanged)
|
||||
harness_ext.py, hunter_harness.py, verify_judge.py deleted
|
||||
stage_processors.host_under_harnessx deleted; deterministic pipeline kept
|
||||
[harnessx] config, extra, mypy override, lock one load warning; extra/override/lock gone
|
||||
improve: benchmark per-rule proposer (+ MetaAgent improve: benchmark proposer + memory proposer (--memory)
|
||||
"plugs in" -- never implemented)
|
||||
plane Run (per case)
|
||||
facts -> va, vb -> agree -> vc -> final (existing)
|
||||
alerts (new, no model) ------------------+
|
||||
remember (new, no model) <- facts, final, alerts, va/vb/vc
|
||||
plane Run (loop tail, once)
|
||||
measure -> propose -> improve(validate+gate) (no model)
|
||||
~/ousast-results/plane/memory/ the store: facts by content, rows by repo + pin
|
||||
```
|
||||
|
||||
New code: `src/openultrasast/plane/memory.py` (store, ingest, seed; host side),
|
||||
`src/openultrasast/plane/tasks/{alerts,remember,measure,propose,improve}.py`,
|
||||
`src/openultrasast/improve/memory.py` (deterministic proposal rules). None of it is in the reconciler. The
|
||||
reconciler gains only `mark_done` (below). It is 465 lines today, and `tests/test_plane_reconciler.py:759-761`
|
||||
keeps it under 500.
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. Fate of each HarnessX capability (Requirement 1)
|
||||
|
||||
| Capability | Where today | Fate | What the user loses |
|
||||
| --- | --- | --- | --- |
|
||||
| LLM hunter pool (`HxScanOrchestrator`) | `hunter_harness.py:49-185`; gated in `cli.py:400-456` (`hx_hunter` at :405, orchestrator at :438-448, degradation at :452-455) | **Retired.** `run_hunter_pool` becomes the only standard-mode hunter pool. LLM hunting of a repository stays available in two places: the MAP-stage tool hunter in `ousast scan --mode standard/deep` with `[models] hunter` (`cli.py:631-640`, not HarnessX), and plane `verify` via `ousast plane run plane/runs/validation-46.yaml` | `hx-hunter:<path>:<line>` findings from an anthropic/openai/litellm agent (`hunter_harness.py:155`). No run of this path is recorded under `benchmarks/`, so no measured TP is lost. |
|
||||
| LLM judge | `verify_judge.py:22-63`; gated in `cli.py:406, 499-511` | **Retired.** `verification.verify_findings` is called directly and `verify_judge.py` is deleted (it is only the dispatcher and the judge). The plane's LLM verification is `verify` + `agree` (+ `vc`/`final` tie-break) | Per-finding `plausible/unsupported` verdicts that set `ACCEPTED/REJECTED` (`verify_judge.py:83-88`). Unmeasured. `[models] judge` (model-grounded-detection, `model/endpoint.py:195`) is a different capability and stays. |
|
||||
| LLM fusion panels | `fusion.py:194-326` (`_fuse_with_llm_panels`, `_run_llm_panels`); gated in `cli.py:519-541` | **Retired.** The deterministic panels (`_panel_a`, `_panel_b`, `_reconcile`, `fusion.py:101-167`) become the only path. `fuse_findings_dispatch` is renamed `fuse_findings(findings, verifications, *, high_assurance, mitigated_ids)`. The plane counterpart of independent cross-examination is `agree` across independent passes with the 2-of-3 tie-break | Steel-man LLM panels (`decision_source: "llm-panels"`). Unmeasured. `fusion.json` for deterministic decisions is unchanged. |
|
||||
| Deterministic stages hosted under HarnessX | `stage_processors.py:129-156` (`host_under_harnessx`); only caller is the skipped test `test_slot_contract.py:119-133` | **Retired, no replacement.** ax runs whole tasks, so a model-disabled in-loop host has nothing to host. `stage_processors`/`slot_contract` stay as the standalone deterministic pipeline that `test_gate.py:147-148` pins | Nothing. The function has no production caller. |
|
||||
| LLM evolution proposer (`MetaAgent.evolve`) | docstring only: `improve/evolve.py:8-9`, `harness_ext.py:7-8` | **Carried by the memory layer** (Requirement 6): `improve/memory.py` rules over the store, fed into `run_round` as extra proposals | Nothing implemented is lost. |
|
||||
|
||||
No CLI flag is HarnessX-specific, so Requirement 1.3 applies to config keys. A key that asked for a retired
|
||||
capability makes `load_config` raise `RetiredConfigError(ValueError)`, and the CLI prints the message and exits 2.
|
||||
A silent downgrade is not allowed:
|
||||
|
||||
| Key | Message (abridged) |
|
||||
| --- | --- |
|
||||
| `[models] verifier` | "`[models] verifier` was retired with HarnessX on <date>: LLM verification runs on the plane (`verify` + `agree`, `ousast plane run`, see ops/ax/README.md). Remove the key." |
|
||||
| `[fusion] panel_model`, `[fusion] decider_model` | "... LLM fusion panels were retired; fusion is deterministic; independent LLM agreement is the plane's `agree` task ..." |
|
||||
|
||||
`[harnessx]` is different (Requirement 3.3). It only tuned budgets and never requested a capability, so it loads
|
||||
with **one** `logging.warning` on logger `openultrasast.config` that names the plane ("`[harnessx]` is ignored:
|
||||
HarnessX was removed; agentic work runs on the ax plane with per-task budgets, see ops/ax/README.md"). The section
|
||||
is otherwise ignored. `[models] hunter` keeps its meaning (tool hunter, obligations intent adjudication at
|
||||
`cli.py:1237`), so it gets no message. With the extra installed it no longer starts a second, HarnessX hunter. The
|
||||
release notes say so.
|
||||
|
||||
Requirement 1.2 (moved capabilities have a CLI path, a Model per task and a fixture test) is met by the existing
|
||||
`verify`/`agree` tasks (`ousast plane run`; Model `deepseek-flash` bound per Task; `tests/test_plane_verify.py`,
|
||||
`tests/test_plane_scoring_parity.py`, `tests/test_plane_tiebreak.py`), and by the new memory and loop tasks below, each with its own fixture test. No
|
||||
capability is moved into a new Model-bound task.
|
||||
|
||||
### 2. Removal mechanics (Requirement 2)
|
||||
|
||||
**Source.**
|
||||
|
||||
- Delete `harness_ext.py` (97 lines), `hunter_harness.py` (197) and `verify_judge.py` (130).
|
||||
- `cli.py`: drop the imports at :44, :46 and :94. Drop `harnessx_present`, `hx_hunter` and `hx_verify` (:400-406),
|
||||
the orchestrator branch and its degradation (:438-456), and the verify degradation (:499-502). Call
|
||||
`verify_findings(findings)` at :503-511. Drop `hx_fusion` and its degradation (:521-527). Call
|
||||
`fuse_findings(findings, verifications, high_assurance=config.fusion.high_assurance)`. The `verifier_model`
|
||||
local goes away.
|
||||
- `fusion.py`: drop the import at :24, the `panel_model`/`decider_model`/`provider`/`use_harnessx` parameters, and
|
||||
`_with_degradation`, `_fuse_with_llm_panels`, `_run_llm_panels` and `_text` (:229-338). Rewrite the module
|
||||
docstring (:10-14) and update `__all__`.
|
||||
- `stage_processors.py`: drop `host_under_harnessx` (:129-156) and its `__all__` entry, and rewrite the module
|
||||
docstring (:10-11). `slot_contract.py:7-14` becomes "the zero-dependency substrate for the deterministic
|
||||
pipeline".
|
||||
- `config.py`: drop `HarnessxConfig` (:99-107), `ResolvedConfig.harnessx` (:201), `_load_harnessx` (:345-351) and
|
||||
its call (:230). Drop `ModelConfig.verifier` (:37), `FusionConfig.panel_model/decider_model` (:123-124) and
|
||||
their loaders (:266, :366-367). Add `RETIRED_KEYS` and the one-warning rule above. `harness.py:183` keeps the
|
||||
`"verifier": None` literal in `model_roles`, so `harness.json` keeps its schema byte for byte (the key has always
|
||||
been `null` in the baseline config).
|
||||
- Comment-only mentions: `gate.py:174` ("a model-backed stage"), and `improve/evolve.py:8-9`, which now names the
|
||||
memory proposer.
|
||||
- `plane/Dockerfile.runner:23` loses "no HarnessX".
|
||||
|
||||
**Packaging.**
|
||||
|
||||
- Delete the `harnessx` extra and its comment (`pyproject.toml:14-22`) and the `harnessx.*` mypy override and its
|
||||
comment (:67-71).
|
||||
- `uv lock` regenerates `uv.lock`. The `harnessx` package (:1062), the extra (:2145-2146), the requirement
|
||||
(:2169) and `provides-extras` (:2178) go.
|
||||
- Extras that remain: `semantic`. There is no `all` extra. `pip install .` and `pip install '.[semantic]'` in a
|
||||
fresh venv are the Requirement 2.2 check.
|
||||
- No source or test imports a HarnessX transitive dependency. The only third-party imports are `yaml`,
|
||||
`tree_sitter` and `pytest` (checked with an import scan), so dropping the lock entry cannot break an import.
|
||||
|
||||
**Tests** (14 files plus `conftest.py`):
|
||||
|
||||
| File | Action |
|
||||
| --- | --- |
|
||||
| `test_harness_ext.py`, `test_hunter_harness.py`, `test_verify_judge.py`, `test_cli_hx_dispatch.py` | **Delete.** Each exercises only HarnessX or its dispatcher. |
|
||||
| `conftest.py` | Rename fixture `assert_cold_of_harnessx` to `assert_cold_import(code)`: a fresh interpreter runs `code`, with no module-name guard. `test_semantic_extra.py:28-35` is its remaining user (the tree-sitter check brings its own assertion). |
|
||||
| `test_gate.py` | Delete `test_harnessx_stage_stays_within_tolerance_of_baseline` (:124-150) and the `has_harnessx` import (:23). Replace the two "never import" tests (:155-175) with `test_governance_planes_import_only_stdlib_and_declared_dependencies`, an allow-list (stdlib, `openultrasast`, `yaml`) over module-level imports of `GOVERNANCE_SCORING_BENCHMARK_PLANES`. It covers any extra without naming one. Update the docstring at :5-6. |
|
||||
| `test_scan_pipeline_integrity.py` | Rename `test_standard_scan_extra_absent_matches_deterministic_baseline` to `test_standard_scan_with_a_hunter_model_keeps_deterministic_findings` (config `hunter` only). Rename `test_quick_scan_is_unaffected_by_the_extra` to `test_quick_scan_records_no_degradation`. Drop the `has_harnessx` monkeypatch and the `harnessx` parameter. Recompute the expected degradation set from the post-removal run, since `hunter_pool`/`verify` entries no longer exist. |
|
||||
| `test_slot_contract.py` | Delete `test_host_under_harnessx_disables_the_model` and the imports at :16 and :22. Keep the rest. |
|
||||
| `test_config.py` | Replace `test_harnessx_defaults_when_section_absent` and `test_load_config_reads_harnessx_section` with `test_retired_agentic_section_loads_with_one_warning` (caplog: exactly one record naming the plane) and `test_retired_llm_keys_fail_naming_the_replacement` (verifier, panel_model, decider_model). |
|
||||
| `test_fusion.py` | Delete `test_dispatch_records_degradation_when_llm_requested_but_extra_absent` (:142-146) and the import at :16. Rename the dispatch calls to `fuse_findings`. |
|
||||
| `test_tool_hunter.py` (:229, :266), `test_scan_deep.py` (:144) | Drop the `has_harnessx` monkeypatch line. |
|
||||
| `test_semantic_extra.py` | Use `assert_cold_import`. |
|
||||
| `test_module_audit.py` | The subsystem tuple at :31 becomes `("fusion", "mcp", "skills")`. A new assertion checks that the three deleted modules are absent from the audit. |
|
||||
|
||||
**Module audit.** `model/audit.py:76-77` (`harness_ext`, `hunter_harness`) are deleted, and `:78`
|
||||
(`stage_processors`) is reworded to "deterministic slot-contracted scan stages; pinned by the zero-dependency
|
||||
guard in test_gate". Regenerate `benchmarks/measurements/2026-09-08-module-audit.json` in place with `to_dict(audit(SRC))`.
|
||||
`test_the_manifest_matches_the_tree` pins that path. The diff shows three rows removed and no orphan.
|
||||
|
||||
**Search (Requirement 2.4).** `grep -rni harnessx src tests pyproject.toml README.md` may match only lines tagged
|
||||
`retired 2026-..`:
|
||||
|
||||
- the `RETIRED_KEYS` entry and warning text in `config.py`
|
||||
- the two retirement tests in `test_config.py`
|
||||
- the one README line in the plane section
|
||||
|
||||
Requirement 3.3 cannot be implemented or tested without naming the section, so these lines count as retirement
|
||||
notes. The check is a test (`test_no_harnessx_outside_retirement_notes`) that fails on any untagged match. It
|
||||
builds its pattern at runtime, so it does not match itself.
|
||||
|
||||
### 3. Equality proof (Requirement 3)
|
||||
|
||||
The spec's "`ousast regress`" is not a subcommand (`cli.py:133-251`). Regress exists only as the deep-mode
|
||||
sandbox stage. On this project the regression command over the development pins is `ousast pairs`
|
||||
(README.md:38): the vuln-vs-fixed replay over local fixtures and vendored pinned VFC pairs, offline by default.
|
||||
The benchmark replay is README.md:35's manifest. The baseline also covers the two paths the removal touches
|
||||
directly: one standard-mode scan (verify and fusion) and one `improve --dry-run` (evolve). Everything is
|
||||
quick or standard mode with no Joern on the host, so each command takes minutes, not hours, on this 7 GB host.
|
||||
|
||||
**Environment (instrument first).**
|
||||
|
||||
- Run from a `git archive HEAD` export in the scratchpad (the source is frozen, per the long-runs rule).
|
||||
- `uv sync --frozen --extra semantic`, which drops the installed `harnessx`. It is installed in `.venv` today.
|
||||
- `OPENULTRASAST_SKIP_DOTENV=1` and `env -u DEEPSEEK_API_KEY -u OPENROUTER_API_KEY -u ANTHROPIC_API_KEY -u OPENAI_API_KEY`,
|
||||
plus an empty `--config`, so no command can reach a model.
|
||||
|
||||
The harness asserts `find_spec("harnessx") is None`, `find_spec("tree_sitter") is not None`, and a
|
||||
`uv pip freeze` hash. It also prints the byte size of each input manifest, and fails on any non-zero exit code.
|
||||
|
||||
**Commands** (one script, `benchmarks/harnessx_removal_equality.py record|compare <dir>`):
|
||||
|
||||
1. `pytest -q -p no:cacheprovider --junitxml=suite.xml`, which gives outcome per test id.
|
||||
2. `ousast pairs --json > pairs.json`. Instrument check: pair count > 0 and at least one TP.
|
||||
3. `ousast benchmark benchmarks/manifests/python-vulnerable.toml --mode quick`. Keep
|
||||
`benchmark_result.json`, `calibration_records.json` and `external_baseline_deltas.json`. Instrument check:
|
||||
`expected > 0`.
|
||||
4. `ousast scan benchmarks/fixtures/python-vulnerable --mode standard --config empty.toml`. Keep
|
||||
`findings.json`, `verification.json`, `fusion.json`, `report.sarif`, `harness.json` and `manifest.json`.
|
||||
5. `ousast improve benchmarks/manifests/java-spring-boot-vulnerable.toml --dry-run --no-pair-gate`, stdout plus
|
||||
the dry-run journal.
|
||||
|
||||
**Stored.** `benchmarks/measurements/<date>-harnessx-removal-baseline/`: the normalized outputs, and `env.json`
|
||||
(commit, freeze hash, instrument checks). The script's exclusion list is written into that directory's
|
||||
`README` line.
|
||||
|
||||
**Comparison.** JSON is parsed, normalized and re-serialized with sorted keys. Everything else is compared as
|
||||
raw bytes. Excluded:
|
||||
|
||||
- keys `runtime_seconds`, `scan_id`, `timestamp`, `started`, `finished`, `created_at`, `generated_at`, and any
|
||||
key ending `_seconds` or `_ms`
|
||||
- run directory names (timestamped, `benchmark.py:186`)
|
||||
- `trace/events.jsonl` as a whole (timings)
|
||||
- the export's absolute root path, replaced by `<ROOT>`
|
||||
|
||||
For the suite, every test id that survives must keep its outcome. Deleted and renamed ids are compared through
|
||||
the map in the Tests table above, and any other difference is a failure. Any difference in 2-5 blocks the merge.
|
||||
|
||||
### 4. Memory layer (Requirement 6.1, 6.4)
|
||||
|
||||
**Store** (`plane/memory.py`). The store lives at `results_root()/memory/` (`~/ousast-results/plane/memory/`,
|
||||
honouring `OUSAST_RESULTS`, `reconciler.py:58-59`). It is JSON files, not SQLite: the rest of the plane is JSON,
|
||||
diffable and greppable, and one run at a time holds the run lock, so SQLite's concurrency buys nothing.
|
||||
|
||||
```
|
||||
memory/
|
||||
index.jsonl one row per ingested (run, task): sha256 of its memory.jsonl, row count
|
||||
facts/<sha256>.json facts.json by content (identical facts stored once)
|
||||
repos/<host>__<owner>__<name>/<pin>.jsonl the rows of one repository + 40-hex pin
|
||||
```
|
||||
|
||||
Ingest is idempotent: an `index.jsonl` hit skips the delivery, and rows carry a deterministic `id`. Ingest also
|
||||
refuses to write when `shutil.disk_usage` reports less than 1 GiB free. The disk is at 98% today, so it names
|
||||
the store path in the error.
|
||||
|
||||
**Storage backends (maintainer, 2026-09-30: asked to use the object-store features of the S3 server he runs,
|
||||
including its JSON support).** The store is written against one interface, `MemoryStore` (`put_row`, `put_facts`, `get_facts`,
|
||||
`rows(repo=..., pin=..., kind=..., where=...)`, `index`), with two backends of identical behaviour:
|
||||
|
||||
- `FileStore`: the layout above, for this laptop and the unit tests.
|
||||
- `S3Store`: an S3-compatible bucket (boto3; RustFS is the tested server), and the store of record once the plane runs on a separate
|
||||
Kubernetes cluster. The layout maps onto one bucket with the same key paths:
|
||||
- **Object metadata and tags** on every row object: `repo`, `pin`, `kind`, `family`, `run`, `population`,
|
||||
`split`. Rules list by prefix (`repos/<repo>/`) and filter by tag instead of scanning the bucket.
|
||||
- **Server-side JSON queries**: `rows(where=...)` pushes the filter down with S3 Select
|
||||
(`SelectObjectContent`, JSON Lines input, JSON output), so a proposal rule reads matching rows, not whole
|
||||
files. Support is probed once per process against the server (a small Select on a known object); when the
|
||||
server lacks it, the backend fetches and filters locally, logs the
|
||||
fallback once, and returns the same rows. A query never silently returns nothing because a feature is
|
||||
missing.
|
||||
- **Versioning** on the bucket: each proposal's provenance records the object key and version id of every
|
||||
memory row it cites, so its evidence cannot change underneath it (Req 6.4). Object lock (governance mode) is
|
||||
optional for the `proposals/` prefix.
|
||||
- **Lifecycle rules**: raw run outputs under `runs/` expire after a configurable number of days; `repos/`,
|
||||
`facts/` and `proposals/` are kept. The near-full laptop disk stops being the store's limit.
|
||||
- **Presigned URLs** (the step that removes the host receiver for the Kubernetes move, not built in this
|
||||
spec): the reconciler can hand a task presigned PUT URLs for its outputs and GET URLs for its inputs in the
|
||||
start request, so tasks write to the bucket directly and never hold credentials. The interface keeps
|
||||
`presign_put`/`presign_get` so that follow-on changes the reconciler, not the store.
|
||||
- **Bucket notifications** (follow-on): an event on new `repos/` rows can trigger the loop Run
|
||||
(event-driven loop engineering).
|
||||
- **Configuration**: `OUSAST_MEMORY` selects the backend (`file:///path` or `s3://<bucket>[/<prefix>]`); the S3
|
||||
endpoint and credentials come from `.env` (`S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
|
||||
optional `S3_REGION`), never from a manifest, never printed. Client: boto3 as an optional extra
|
||||
(`openultrasast[s3]`); the core install stays PyYAML-only.
|
||||
- **Tests**: the same contract tests run against `FileStore` and against `S3Store` pointed at an S3-compatible
|
||||
endpoint (skipped unless `OUSAST_MEMORY_TEST_S3` is set), including the Select probe with its fallback
|
||||
forced, versioned provenance, and metadata filters. The disk-space refusal applies to `FileStore` only.
|
||||
|
||||
**`remember` task** (`plane/tasks/remember.py`, no Model, budget `{usd: 0, calls: 0}`, one per case). It reads the
|
||||
case's delivered artifacts as inputs: `facts.json`, the `units.jsonl` of `va`/`vb`/`vc`, `final/agreed.json` and
|
||||
`final/disputed.json` with the per-candidate rows, and `alerts.jsonl`. It takes the repository URL and pin from
|
||||
the bound Workspace (`AX_WORKSPACES_YAML`, `OUSAST_GIT_PINS`) and the population from the Run annotation
|
||||
`openultrasast.io/population`. It writes `memory.jsonl` and passes `facts.json` through. Task code interprets
|
||||
the artifacts, so the reconciler and runner stay content-blind (ai-service-plane Req 3.4). After `reconciler.run`
|
||||
returns, `cli._plane` calls `memory.ingest(run_dir)`. `ousast plane remember <run>` does the same by hand.
|
||||
|
||||
**Row kinds** (every row has `id`, `kind`, `repo`, `pin`, `run`, `task`, `population`, `split`, `image`):
|
||||
|
||||
- `facts`: `sha256`, `candidates_digest` (`repo_facts._digest`, `repo_facts.py:188`), `files`.
|
||||
- `verdict`: `candidate` (`path::function`), `family`, `site`, `passes {a, b, c}` (true/false/null), `final`
|
||||
(`agreed | rejected | disputed`), `tiebreak` (bool), `site_match` (bool/null), `usd`, `turns`.
|
||||
- `unit_cost`: `pass`, `path`, `usd`, `calls`, `usage`, `turns`, `model`.
|
||||
- `alert`: `rule_id`, `rule_status` (`enabled | shadow`), `path`, `line`, `function`, `pin_role`
|
||||
(`vulnerable | fixed`).
|
||||
- `proposal_outcome`: `edit_key`, `evidence_digest`, `outcome` (`accepted | reverted | rejected`), `gate_run`.
|
||||
|
||||
**Fact reuse.** A facts entry is valid for the key (repo, pin, candidates digest, runner image digest). The image
|
||||
is in the key because the host's source and the image's source can differ, and facts computed by other code are
|
||||
not the same facts. `ousast plane workspaces --validation-set` writes the key as the Task annotation
|
||||
`openultrasast.io/memory-key`. Annotations are already stripped before ax (`manifests.py:4-6`). Before
|
||||
`reconciler.run`, `cli._plane` calls `memory.seed(run_manifest)`. For each Run entry whose Task carries a key with
|
||||
stored facts and no state yet, seed writes `<run>/<task>/facts.json` and a `summary.json`
|
||||
(`status: done, units_done == units_total == 1, reused: {"sha256", "from_run"}`). It then calls
|
||||
`reconciler.mark_done(run, task, reused=...)`, a public helper of about 10 lines that takes the run lock, sets
|
||||
the state and releases it. The reconciler already skips `done` tasks (`reconciler.py:342`). The receiver already
|
||||
serves any regular file under the run directory as a declared input (`egress.py:242-246`). A changed pin, a
|
||||
changed candidate set or a rebuilt image recomputes.
|
||||
|
||||
### 5. Proposals from memory (Requirement 6.2, 6.4)
|
||||
|
||||
**Interface.** The proposal surface HarnessX would have used is `run_round` (`improve/evolve.py:149-247`). It
|
||||
builds `RuleStatusEdit`s (`improve/validator.py:36-47`), filters them through the journal's novelty gate
|
||||
(`journal.reverted_edit_keys`, `journal.py:26-38`), validates each with `EvolveValidator.validate`
|
||||
(`validator.py:75-81`, which accepts only `RuleStatusEdit` and `PolicyConstantEdit`), smoke-replays and gates.
|
||||
|
||||
`improve/memory.py` exposes:
|
||||
|
||||
```
|
||||
propose_from_memory(rows, ruleset_by_id, current_ledger, journal_rounds, *, excluded) -> list[MemoryProposal]
|
||||
MemoryProposal = (edit: RuleStatusEdit, provenance: dict)
|
||||
```
|
||||
|
||||
`run_round` gains `proposals: Sequence[MemoryProposal] = ()`. It appends their edits after the benchmark's edits
|
||||
and keeps the first edit per `key()`. The validator and the gate are untouched. `ousast improve` gains
|
||||
`--memory [DIR]` (off by default, so the deterministic `improve` output is byte-identical) and
|
||||
`--qualify-population NAME` (repeatable).
|
||||
|
||||
**Rules** (the thresholds are module constants and part of the provenance record). Join key: an `alert` and a
|
||||
`verdict` match when repo, pin and `path::function` are equal. A `disputed` final counts as no evidence in
|
||||
either direction.
|
||||
|
||||
- **M1, repeated false alerts** (`enabled -> shadow`). Rule R qualifies when both hold:
|
||||
- at least 3 R alerts sit at candidates whose final is `rejected`, or on a `fixed` pin inside the case's declared
|
||||
fix range, across at least 2 distinct repositories;
|
||||
- no R alert sits at an `agreed` candidate or a `site_match` site.
|
||||
- **M2, missed declared sites** (`shadow -> enabled`). Shadow rule R qualifies when all three hold:
|
||||
- its shadow alerts hit at least 2 `agreed` candidates with `site_match == true`, across at least 2
|
||||
repositories;
|
||||
- no enabled rule alerts at those candidates;
|
||||
- R has no alert on a `fixed` pin at the same function and at most 1 alert at a `rejected` candidate.
|
||||
- **M3, frequently disputed families.** Families with at least 5 candidates and a dispute rate of at least 0.3 go
|
||||
to `signals.json` as advisory input for plane routing (for example, scheduling pass `c` up front). They are not
|
||||
an edit and never reach the validator.
|
||||
|
||||
**Provenance.** Each proposal's `rationale` is `memory:<proposal_id>`, where `proposal_id` = sha256 of
|
||||
(rule, edit key, sorted evidence row ids, thresholds). The sidecar `memory_proposals.jsonl`, next to the journal,
|
||||
holds:
|
||||
|
||||
- `proposal_id`, `edit_key`, `rule_id`
|
||||
- `evidence` (the row ids plus their repo, pin, run and task)
|
||||
- the counts that met each threshold
|
||||
- `excluded` (populations and pins removed by the guard, with counts)
|
||||
- the store's `index.jsonl` digest at proposal time
|
||||
|
||||
`evolve._record` (`evolve.py:366-396`) adds `"evidence": proposal_id` to an edit's journal entry only when the
|
||||
edit came from memory. Otherwise the journal is unchanged byte for byte. A reader follows journal, then sidecar,
|
||||
then store rows, then the run directory.
|
||||
|
||||
**Novelty.** `journal.reverted_edit_keys` does not block a rule edit that carries a rationale (`journal.py:36`), so
|
||||
a memory proposal would otherwise come back every round. The proposer itself skips any key whose reverted journal
|
||||
entry has the same evidence digest. New evidence is the "new rationale" that allows a retry.
|
||||
|
||||
**Train-on-test guard.** A row is excluded from proposals when any of these holds:
|
||||
|
||||
- (a) its `population` is one of `--qualify-population`, which is also where the proposal will be gated or
|
||||
qualified;
|
||||
- (b) its (repo, pin) is a case of the benchmark manifest being gated;
|
||||
- (c) its (repo, pin) is a `split = "holdout"` pair of the pair catalog, which the per-profile holdout clause
|
||||
(`evolve.py:204-214`) measures.
|
||||
|
||||
This is the rule the closed-loop leak broke, where a holdout pair taught the shape that "recovered" it. Here the
|
||||
filter runs before any rule sees a row. Concretely, rows from `validation-46` (population
|
||||
`independent-v2/validation`) never propose edits that are then qualified on `independent-v2/validation`. The
|
||||
tie-break was designed on that set, so it is development evidence only.
|
||||
|
||||
### 6. The loop as a plane Run (Requirement 6.3)
|
||||
|
||||
`ousast plane workspaces --validation-set ... --loop` extends the generated Run. It also emits a Workspace for each
|
||||
case's fixed pin (the population's `fixed` sha, already read by `generate.fix_ranges`, `generate.py:89`):
|
||||
|
||||
| Task (per case unless noted) | Model | Budget | Inputs -> outputs |
|
||||
| --- | --- | --- | --- |
|
||||
| `<case>-facts`, `-va`, `-vb`, `-agree`, `-vc`, `-final` | verify passes: `deepseek-flash`; others none | as generated today (`generate.verify_budget`) | unchanged |
|
||||
| `<case>-alerts` (new) | none | `{0, 0}`, `serialize: engine` | vulnerable + fixed Workspaces -> `alerts.jsonl` (quick-mode `quick_scan_findings` with enabled and shadow rules, function-attributed) |
|
||||
| `<case>-remember` (new) | none | `{0, 0}` | facts, va, vb, vc, final, alerts -> `memory.jsonl`, `facts.json` |
|
||||
| `loop-measure` (once) | none | `{0, 0}` | every `remember` + `memory-snapshot/rows.jsonl` -> `measure.json` (the increment metrics: declared sites agreed, cost per candidate, dispute rates) |
|
||||
| `loop-propose` (once) | none | `{0, 0}` | measure, snapshot, journal -> `proposals.jsonl`, `memory_proposals.jsonl`, `signals.json` |
|
||||
| `loop-improve` (once) | none | `{0, 0}`, `serialize: engine` | proposals; Workspace = this repository at the commit under test -> `gate.json` (the `RoundOutcome`), `journal.json`, `rule_policy.json` (only when accepted) |
|
||||
|
||||
Before the Run starts, `memory-snapshot` is seeded into the run directory like reused facts. It holds the store
|
||||
rows after the guard, since a sandboxed task cannot read the host store. A `{usd: 0, calls: 0}` budget makes
|
||||
`budget.py` refuse any call, so a model-free task that tries one fails loudly rather than spending. Attribution
|
||||
per task is the existing `ousast plane status` table.
|
||||
|
||||
**The gate's verdict** lands in `~/ousast-results/plane/<run>/loop-improve/gate.json` and is ingested as
|
||||
`proposal_outcome` rows. An accepted `rule_policy.json` is adopted into the repository only by a maintainer
|
||||
commit, as any ledger change is today (README "Rule-level loop").
|
||||
|
||||
### 7. Documentation and specs (Requirement 4)
|
||||
|
||||
- **README.** Replace "Configuring the HarnessX agentic plane" (README.md:293-329) with "The agentic plane (ax)":
|
||||
what runs there (repo-facts, verify, agree, memory), `ousast plane run|status|remember`, a pointer to
|
||||
`ops/ax/README.md`, and one tagged line saying HarnessX was retired. Rename "The HarnessX self-improving cycle"
|
||||
(:330) to "The self-improving cycle". Its calibration ledger and `ousast improve` content is not HarnessX and
|
||||
stays. The MetaAgent paragraph (:393-400) is replaced by the memory proposer (`--memory`, provenance, guard).
|
||||
Drop the `panel_model`/`decider_model` lines at :227-228.
|
||||
- **`docs/examples.md` section 6** becomes "Running the agentic plane", with the plane doctor, run and status
|
||||
commands.
|
||||
- **`docs/threat-model.md:17-20, 32-34`**: egress is the per-task EgressPolicy (receiver, Git hosts, Model
|
||||
`egress-hosts`, passthrough only). Credentials travel only in the start request. Spend is the per-task
|
||||
`usd`/`calls` budget in the Run.
|
||||
- **Retirement notes.** A dated note ("Retired 2026-..: HarnessX was removed by `harnessx-removal`; the agentic
|
||||
plane is ax (`ai-service-plane`), proposals come from the plane memory. History below is unchanged.") goes at
|
||||
the top of the requirements (and design, where present) of `harnessx-self-improving-rulesets`,
|
||||
`learning-harness`, `three-stage-scan` and `openrouter-sast-harness`, and in `.kiro/steering/overview.md` near
|
||||
:175. The other specs that mention HarnessX in passing keep their history untouched.
|
||||
- `RELEASE_NOTES.md` gets one entry listing the retired keys. Kiro tooling stays out of user docs.
|
||||
|
||||
## Data Models
|
||||
|
||||
- `memory.jsonl` (remember output) and `repos/<repo>/<pin>.jsonl`: the rows above, one JSON object per line,
|
||||
sorted keys, stable `id` = sha256(kind, run, task, candidate|path|rule_id+line).
|
||||
- `index.jsonl`: `{"run", "task", "sha256", "rows", "ingested"}`.
|
||||
- `memory_proposals.jsonl`: `{"proposal_id", "edit_key", "rule_id", "rule": "M1|M2", "evidence": [...],
|
||||
"counts": {...}, "thresholds": {...}, "excluded": {...}, "store_digest"}`.
|
||||
- Journal edit entry: the existing fields (`evolve.py:381-384`) plus `"evidence"` only for memory edits.
|
||||
- Equality record: `env.json` `{"commit", "freeze_sha256", "harnessx_importable": false, "tree_sitter": true,
|
||||
"inputs": {path: bytes}, "exclusions": [...]}`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- `RetiredConfigError`: exit 2 with the key and its replacement, before any stage runs.
|
||||
- A `[harnessx]` section produces exactly one warning, and loading continues.
|
||||
- `memory.ingest` on a run without `remember` outputs is a no-op that says so. A malformed row fails the ingest
|
||||
and names the file and line, and nothing partial is written: rows are staged in a temporary file and renamed.
|
||||
- A low-disk refusal names the store and the free space. The plane run's result is unaffected, and ingest can be
|
||||
repeated with `ousast plane remember`.
|
||||
- `seed` never overwrites an existing task directory or a non-pending state. A facts file whose sha256 no longer
|
||||
matches is dropped from the store with a warning, and that task recomputes.
|
||||
- `propose_from_memory` with an empty store after the guard returns no proposals, and `run_round` reports
|
||||
`no_proposals` as today.
|
||||
- The equality script exits non-zero on the first failed instrument check. It also exits non-zero on any
|
||||
difference outside the exclusion list and prints the first differing path and key.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- `tests/test_config.py`: the one-warning section and the retired keys (above).
|
||||
- `tests/test_no_harnessx_references.py`: the tagged-only search of Requirement 2.4.
|
||||
- `tests/test_plane_memory.py`: ingest is idempotent and content-addresses identical facts. `seed` marks a facts
|
||||
task done and serves its file, and does not for a changed pin, candidate digest or image. Low-disk refusal
|
||||
(monkeypatched `disk_usage`).
|
||||
- `tests/test_plane_remember.py`, `test_plane_alerts.py`, `test_plane_loop.py`: fixture run directories built from
|
||||
the recorded validation-46 shapes. `remember` rows match `agree`'s per-candidate rows. `alerts` attributes
|
||||
functions. `measure` reproduces the increment's recorded metrics on the recorded artifacts. `propose` and
|
||||
`improve` work end to end on a fixture manifest.
|
||||
- `tests/test_improve_memory.py`:
|
||||
- M1 and M2 fire at their thresholds and not below.
|
||||
- A disputed candidate counts neither way.
|
||||
- The guard removes rows by population, manifest case and holdout pair, so a proposal that exists only because
|
||||
of a holdout row disappears.
|
||||
- Provenance ids are stable across runs.
|
||||
- A reverted memory edit is not re-proposed on the same evidence, and is re-proposed on new evidence.
|
||||
- `run_round` without proposals writes a journal byte-identical to today's.
|
||||
- `tests/test_plane_reconciler.py`: `mark_done` under the lock. The existing under-500-lines test stays.
|
||||
- The existing gate and pair tests, `test_benchmark_output_is_byte_identical_to_golden_baseline`
|
||||
(`test_gate.py:104`) and the equality script (section 3) are the regression net.
|
||||
|
||||
## Requirement Traceability
|
||||
|
||||
| Requirement | Design section | Verified by |
|
||||
| --- | --- | --- |
|
||||
| 1.1 fate of every capability | 1 (table) | this document; commit 5 |
|
||||
| 1.2 moved capabilities reachable, Model per task, fixture test | 1 (existing `verify`/`agree`), 4-6 (new tasks are Model-free) | `test_plane_verify.py`, `test_plane_tiebreak.py`, new plane task tests |
|
||||
| 1.3 retired keys fail naming the replacement | 1 (`RetiredConfigError`) | `test_config.py` |
|
||||
| 2.1 modules and guards gone | 2 Source | commit 6; audit test |
|
||||
| 2.2 extra, override, lock gone; installs succeed | 2 Packaging | fresh-venv `pip install .` / `'.[semantic]'` |
|
||||
| 2.3 tests deleted or renamed; audit regenerated | 2 Tests, Module audit | `test_module_audit.py` |
|
||||
| 2.4 search returns only retirement notes | 2 Search | `test_no_harnessx_references.py` |
|
||||
| 3.1 baseline without the extra | 3 | commit 1 record |
|
||||
| 3.2 byte-identical after, exclusions written | 3 Comparison | commit 7 record |
|
||||
| 3.3 `[harnessx]` loads with one warning | 1 | `test_config.py` |
|
||||
| 4.1-4.3 docs, retirement notes, Kiro out of user docs | 7 | commit 8 review |
|
||||
| 5.1-5.2 order, green steps, no deletion before replacement | Commit sequence | per-commit gates |
|
||||
| 6.1 store keyed by repo + pin, fact reuse | 4 | `test_plane_memory.py`, `test_plane_remember.py` |
|
||||
| 6.2 deterministic proposals through validator and gate | 5 | `test_improve_memory.py` |
|
||||
| 6.3 loop as a plane Run with per-task Models and budgets | 6 | `test_plane_loop.py`; `ousast plane status` attribution |
|
||||
| 6.4 provenance and train-on-test guard | 5 | `test_improve_memory.py` |
|
||||
|
||||
## Commit sequence (Requirement 5), mapped to future tasks
|
||||
|
||||
Each commit is gated on the full suite's own exit code, `ruff` and `mypy`.
|
||||
|
||||
1. **Baseline** (3.1): the equality script and `benchmarks/measurements/<date>-harnessx-removal-baseline/`,
|
||||
recorded with the extra uninstalled.
|
||||
2. **Memory store and `remember`** (6.1): `plane/memory.py`, `tasks/remember.py`, `reconciler.mark_done`, seed and
|
||||
ingest wiring in `cli._plane`, generator annotations, fixture tests.
|
||||
3. **Memory proposer** (6.2, 6.4): `improve/memory.py`, `run_round(proposals=...)`, `ousast improve --memory`,
|
||||
sidecar and journal evidence, guard tests.
|
||||
4. **Loop Run** (6.3): `alerts`, `measure`, `propose` and `improve` tasks, `--loop` generation, fixed-pin
|
||||
Workspaces, fixture tests.
|
||||
5. **Retire the scan paths** (1, 3.3): the `cli.py`, `fusion.py`, `verify_judge.py` and `stage_processors.py`
|
||||
changes, `RetiredConfigError`, the one warning, and the test renames that follow. At this point nothing imports
|
||||
`harness_ext`/`hunter_harness`. Their replacements (plane tasks, and the memory proposer from 2-4) have landed.
|
||||
6. **Delete code and packaging** (2): the two modules, the extra, the mypy override, `uv.lock`, the deleted tests,
|
||||
the fixture rename, audit rows and the manifest regeneration, and the reference search test.
|
||||
7. **Equality proof** (3.2): rerun the script, commit the comparison record next to the baseline, and link it in
|
||||
the commit message.
|
||||
8. **Docs and specs** (4): the README, `docs/examples.md`, `docs/threat-model.md`, retirement notes, steering and
|
||||
release notes.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Existing users of the LLM scan paths.** There is no `--llm` flag. Users who set `[models] verifier` or
|
||||
`[fusion] panel_model` get a hard error, which is intended (1.3). Users who installed the extra and set
|
||||
`[models] hunter` lose the second (HarnessX) hunter without an error. Only the release note covers that,
|
||||
because the key is still valid. No measurement suggests either path found anything the remaining paths miss.
|
||||
The absence of a measurement is itself the risk.
|
||||
- **Plane precision is unsolved.** `plane-increment-2.json` agrees 28 candidates, of which 16 are declared
|
||||
sites. The other 12 are unadjudicated and several may be false. M1 treats a `rejected` final as negative
|
||||
evidence, while plane recall is 16/20, so "rejected" is not "safe". The thresholds (2 or more repositories, zero
|
||||
agreed hits) and the unchanged recall/FP gate are the guard. Precision work (judge design) remains out of scope.
|
||||
- **Evidence is thin.** Today's store would hold 15 cases from one validation population, which is exactly the
|
||||
population the guard excludes for its own qualification. Expect `no_proposals` until a second population has
|
||||
run through the loop.
|
||||
- **Disk.** The home volume is 98% full (2.4 GB free). One validation-46-sized Run adds about 3 MB of facts
|
||||
(content-addressed; about 0.1 MB when pins are unchanged) plus about 0.5 MB of rows. Ingest refuses below 1 GiB
|
||||
free. The store has no eviction in this spec.
|
||||
- **The baseline measures the environment as well as the code.** `.venv` has `harnessx` installed today. A
|
||||
baseline taken without re-syncing would include it. The script's `find_spec` and freeze checks exist for that.
|
||||
- **Seeding writes reconciler state from outside the reconciler.** It is confined to `mark_done` under the run
|
||||
lock. The reconciler's line budget (465 of 500) leaves no room for fact reuse inside the reconciler.
|
||||
@@ -1,125 +0,0 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
HarnessX is the optional agentic plane (`openultrasast[harnessx]`): an LLM hunter pool, an LLM judge and LLM
|
||||
fusion panels, each behind a capability guard with a deterministic fallback, plus a lazy bridge that hosts
|
||||
deterministic stages under a HarnessX processor. The ai-service-plane increment met its gate on google/ax
|
||||
(`benchmarks/independent/plane-increment-2.json`), which is the condition the maintainer set on 2026-09-29:
|
||||
"goal is to remove harnessx and to shift to ax fully once we establish we have value." This feature removes
|
||||
HarnessX from the product and moves every LLM capability it carried either onto the plane or into a recorded
|
||||
retirement. Inventory: `brief.md` (verified 2026-09-30).
|
||||
|
||||
Maintainer, 2026-09-30: "Please remove harnessx completely. the memory layer that saves tokens takes over the
|
||||
evolutionary improvements as well as loop engineering." So nothing of HarnessX stays, not even as an optional
|
||||
path, and the self-improving cycle HarnessX was meant to drive (the LLM `MetaAgent.evolve` proposer plugged into
|
||||
`improve/evolve.py`) is carried by the plane's memory layer instead: what runs record (repository facts,
|
||||
verdicts, disputes, costs) becomes the input to proposals and to the run-measure-propose-gate loop.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: the HarnessX-only modules (`harness_ext.py`, `hunter_harness.py`); the guarded branches in
|
||||
`cli.py` (scan `--llm` path, `HxScanOrchestrator`), `verify_judge.py` (judge path), `fusion.py` (LLM panels),
|
||||
`stage_processors.py` (`host_under_harnessx`), `config.py` (`HarnessxConfig`); the `harnessx` extra, its mypy
|
||||
override and lock entry; the tests that exercise HarnessX; README and docs sections; dated retirement notes in
|
||||
the specs and steering that name HarnessX.
|
||||
- **Also in scope**: the plane memory layer as the source of evolutionary proposals and the loop (Requirement 6),
|
||||
replacing the HarnessX proposer.
|
||||
- **Out of scope**: changing the deterministic engine, rulesets or `evolve`'s validator and gate (proposals change
|
||||
source, not how they are validated and gated); new detection logic; qualification on an independent population; the plane's own precision work (judge design) --
|
||||
where a capability moves to the plane, this spec wires it, it does not improve it.
|
||||
- **Adjacent expectations**: every deterministic path (quick mode, `regress`, `benchmark`, `gate`, `evolve`)
|
||||
produces byte-identical output before and after; `vulnerabilities-over-plumbing` applies, so capabilities are
|
||||
kept or retired by what they detect, reported in TP/FP terms where a measurement exists.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: HarnessX is removed completely; the plane is the only agentic path
|
||||
|
||||
**User Story:** As the maintainer, I want no HarnessX path left, optional or not, and every LLM capability either
|
||||
on the plane or explicitly retired, so that nothing is lost silently.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. No HarnessX code path remains, optional or guarded. The design lists each capability -- LLM hunter pool
|
||||
(`ousast scan --llm`), LLM judge (`verify_judge`), LLM fusion panels (`fusion`), deterministic stages hosted
|
||||
under HarnessX (`host_under_harnessx`), the LLM evolution proposer -- with its fate: carried by a plane task
|
||||
(named), carried by the memory layer (Requirement 6), or retired with the deterministic path kept.
|
||||
2. A capability moved to the plane is reachable from the CLI through the plane (`ousast plane run` with a
|
||||
documented Run, or a CLI flag that submits one), binds its own Model per task (ai-service-plane Req 7), and
|
||||
has a fixture test like the other plane tasks.
|
||||
3. A retired capability's CLI flags and config keys fail with a message that names the replacement or says it
|
||||
was retired, never a silent no-op.
|
||||
|
||||
### Requirement 2: HarnessX code, packaging and tests are gone
|
||||
|
||||
**User Story:** As the maintainer, I want no HarnessX code path left to maintain.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. `harness_ext.py` and `hunter_harness.py` are deleted; no module imports `harnessx`; `use_harnessx`
|
||||
parameters, `has_harnessx` checks and `host_under_harnessx` are removed; the deterministic fallbacks become the
|
||||
only code path where a capability is retired.
|
||||
2. The `harnessx` extra, the mypy override for `harnessx.*` and the lock entry are removed; `pip install .` and
|
||||
`pip install '.[all]'` (or whatever extras remain) succeed.
|
||||
3. Tests that only exercise HarnessX are deleted; tests that assert fallback behaviour are kept and renamed to
|
||||
describe the behaviour, not the absence of HarnessX; the module audit is regenerated with no orphan.
|
||||
4. A search for `harnessx` (case-insensitive) in `src/`, `tests/`, `pyproject.toml` and the README returns only
|
||||
the retirement notes of Requirement 4.
|
||||
|
||||
### Requirement 3: Deterministic behaviour is unchanged, and proven so
|
||||
|
||||
**User Story:** As the maintainer, I want evidence, not assurance, that removing HarnessX changed nothing a
|
||||
deterministic run produces.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. Before any deletion, a baseline is recorded without the extra installed: the full host suite, and the outputs
|
||||
of `ousast regress` and one `ousast benchmark` replay on the development pins, stored under
|
||||
`benchmarks/measurements/`.
|
||||
2. After the removal the same commands produce byte-identical outputs (timestamps and durations excluded, and
|
||||
the exclusion list is written down); any difference blocks the merge.
|
||||
3. A config file that still carries a `[harnessx]` section loads with one warning that names the plane, and the
|
||||
section is otherwise ignored.
|
||||
|
||||
### Requirement 4: Documentation and specs tell the new story
|
||||
|
||||
**User Story:** As a reader of the README or a spec, I want to learn that the agentic plane is ax, not HarnessX.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The README sections "Configuring the HarnessX agentic plane" and "The HarnessX self-improving cycle" are
|
||||
replaced by a short section on the ax plane that points at `ops/ax/README.md`; `docs/examples.md` section 6 and
|
||||
`docs/threat-model.md` (egress and spend caps) describe the plane's egress policy and budgets instead.
|
||||
2. Specs that name HarnessX (`harnessx-self-improving-rulesets`, `learning-harness`, `three-stage-scan`,
|
||||
`openrouter-sast-harness`) and `.kiro/steering/overview.md` get a dated retirement note at the top; their
|
||||
history is not rewritten.
|
||||
3. Maintainer-only Kiro tooling stays out of user docs (standing rule).
|
||||
|
||||
### Requirement 5: The removal lands in one reviewable sequence
|
||||
|
||||
**User Story:** As the maintainer, I want to review the removal in steps that each leave main green.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. Order: baseline (3.1) -> capabilities moved or retired (1) -> code and packaging removed (2) -> equality proof
|
||||
(3.2) -> docs and specs (4). Each step is a commit gated on the full suite's own exit code, ruff and mypy.
|
||||
2. No step deletes code whose replacement (Requirement 1) has not landed and passed its fixture test.
|
||||
|
||||
### Requirement 6: The memory layer carries evolutionary improvement and loop engineering
|
||||
|
||||
**User Story:** As the maintainer, I want the token-saving memory layer to be what the system learns from, so
|
||||
that improvement proposals and the improvement loop come from recorded evidence instead of a HarnessX agent.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. Plane runs persist their memory beyond the run directory in one store keyed by repository and pin: repository
|
||||
facts (`repo-facts`), per-candidate verdicts per pass with the final decision, disputes and tie-breaks, and cost
|
||||
and tool turns per unit; later runs reuse facts for an unchanged pin instead of recomputing them.
|
||||
2. `evolve`'s proposals are generated from that store (for example: families or rules whose candidates are often
|
||||
disputed, missed declared sites, repeated false alerts), deterministically and without a HarnessX agent; they
|
||||
pass through the existing validator and gate unchanged.
|
||||
3. The improvement loop -- run, measure, propose, validate, gate -- is expressible as a plane Run whose tasks bind
|
||||
their own Models and budgets (ai-service-plane Req 7), so each iteration's token spend is attributed per task.
|
||||
4. A proposal records which memory entries it came from, so a later reader can trace every accepted change to the
|
||||
evidence behind it; proposals never train on the population used to qualify them (the train-on-test rule).
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"feature_name": "harnessx-removal",
|
||||
"created_at": "2026-09-30T08:00:00Z",
|
||||
"updated_at": "2026-09-30T16:00:00Z",
|
||||
"language": "en",
|
||||
"phase": "implementation-complete",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": true
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
# Implementation Plan
|
||||
|
||||
Order follows the design's commit sequence (Requirement 5): nothing is deleted before its replacement has landed
|
||||
and passed its fixture test. Every task is a commit gated on the full suite's own exit code, `ruff check`,
|
||||
`ruff format --check` and `mypy`; the reconciler stays under 500 lines.
|
||||
|
||||
- [x] 1. Baseline without the extra
|
||||
- Equality script (design §3) with its `find_spec("harnessx")` and freeze checks; re-sync `.venv` without the
|
||||
`harnessx` extra first.
|
||||
- Record `benchmarks/measurements/<date>-harnessx-removal-baseline/`: full suite, `ousast pairs --json`, the
|
||||
`python-vulnerable` quick benchmark, one standard-mode scan, `ousast improve --dry-run`; the exclusion list
|
||||
written next to it.
|
||||
- _Requirements: 3.1, 5.1_
|
||||
|
||||
- [x] 2. Memory store and the `remember` task
|
||||
- `plane/memory.py`: the `MemoryStore` interface with `FileStore` and `S3Store` (object metadata/tags,
|
||||
S3 Select JSON queries probed with a local fallback, versioned provenance, lifecycle, presign stubs;
|
||||
`OUSAST_MEMORY` selects; S3 settings from `.env`; boto3 as an optional extra); JSON rows per
|
||||
repository + pin, facts by content hash, `FileStore` ingest refuses below 1 GiB free,
|
||||
`plane/tasks/remember.py` (model-free), `reconciler.mark_done` under the run lock, seed and ingest wiring in
|
||||
`cli._plane`, generator annotations; fact reuse when repository, pin, candidate set and runner image match.
|
||||
- Fixture tests: rows from a recorded Run, reuse on an unchanged pin, recompute on a changed one, disk refusal.
|
||||
- _Requirements: 6.1, 6.4_
|
||||
|
||||
- [x] 3. Proposals from memory
|
||||
- `improve/memory.py` with rules M1 (demote) and M2 (promote) and the advisory disputed-family signal;
|
||||
`run_round(proposals=...)`; `ousast improve --memory` (opt-in; default output byte-identical); provenance per
|
||||
proposal in the sidecar and journal; the train-on-test guard (qualification population, gated benchmark cases,
|
||||
holdout pairs).
|
||||
- Tests: each rule on crafted rows, the guard drops excluded rows, provenance round-trips, default `improve`
|
||||
unchanged.
|
||||
- _Requirements: 6.2, 6.4_
|
||||
|
||||
- [x] 4. The loop as a plane Run
|
||||
- Tasks `alerts`, `measure`, `propose`, `improve` (no Model, zero budget); `--loop` Run generation; fixed-pin
|
||||
Workspaces; the gate's verdict in `<run>/loop-improve/gate.json`; adoption stays a maintainer commit.
|
||||
- Fixture tests with the fake ax; one live loop Run on the local ax recorded (no model spend beyond the verify
|
||||
passes it reuses from memory).
|
||||
- _Requirements: 6.3_
|
||||
|
||||
- [x] 5. Retire the HarnessX scan paths
|
||||
- `cli.py`, `fusion.py`, `verify_judge.py`, `stage_processors.py` changes; `RetiredConfigError` for
|
||||
`[models] verifier`, `[fusion] panel_model`/`decider_model`; one warning for a `[harnessx]` section (the only
|
||||
tagged retirement text allowed in `src`, enforced by a test); tests renamed to the behaviour they assert.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 3.3_
|
||||
|
||||
- [x] 6. Delete HarnessX code and packaging
|
||||
- Delete `harness_ext.py`, `hunter_harness.py`, `verify_judge.py`'s judge path, the HarnessX-only tests; remove
|
||||
the extra, the mypy override and the `uv.lock` entry; rename the conftest fixture; audit rows and manifest; the
|
||||
reference search test (Req 2.4).
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
||||
|
||||
- [x] 7. Equality proof
|
||||
- Rerun the baseline script; commit the comparison record next to the baseline; any difference outside the
|
||||
exclusion list blocks the merge.
|
||||
- _Requirements: 3.2_
|
||||
|
||||
- [x] 8. Documentation and specs
|
||||
- README ax-plane section pointing at `ops/ax/README.md`; `docs/examples.md` §6 and `docs/threat-model.md`
|
||||
(egress policy, budgets); dated retirement notes in the four specs and `.kiro/steering/overview.md`; release
|
||||
note naming the silent loss of the second hunter for users who set `[models] hunter`.
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
@@ -1,96 +0,0 @@
|
||||
# DocuHarnessX ↔ OpenUltraSAST — Blueprint Alignment
|
||||
|
||||
How OpenUltraSAST's HarnessX integration (Phase 3+) aligns with **DocuHarnessX**
|
||||
(`/home/mc/Source/DocuHarnessX`), the author's other HarnessX project. Both are HarnessX
|
||||
bundles steered from claude-code/opencode via Kiro spec-driven development. Goal: one shared
|
||||
architecture the steering layer drives identically.
|
||||
|
||||
## Orientation
|
||||
|
||||
DocuHarnessX is **agent-first** — an 8-stage pipeline (`ingest → analyze → classify → plan →
|
||||
write → review → assemble → deploy`) that *is* the agent loop. OpenUltraSAST is a
|
||||
**deterministic-pipeline + agentic-plane hybrid** — a sync `HarnessRuntime` driving 13 stages,
|
||||
with HarnessX confined to the one stage that is genuinely an agent loop (the hunter pool). They
|
||||
converge on the same load-bearing patterns and diverge only where SAST's determinism and
|
||||
supply-chain posture require it.
|
||||
|
||||
**What "self-improving multi-agent system" means in DocuHarnessX (important):** it does **not**
|
||||
use `meta_harness` / `MetaAgent.evolve` at all (the `Train` loop is explicit future / out of
|
||||
scope). Its self-improvement is realized two ways: (1) an in-product **fail-closed LLM-judge
|
||||
quality firewall** (COBESY: fixed criteria, threshold-re-derived verdicts, unavailable → fail),
|
||||
and (2) the **claude-code/opencode Kiro steering loop** (fresh implementer → adversarial
|
||||
reviewer → debugger subagents per task). OpenUltraSAST inherits both substrates and *additionally*
|
||||
builds the `MetaAgent.evolve` loop DocuHarnessX leaves unbuilt.
|
||||
|
||||
## Shared blueprint (the spine)
|
||||
|
||||
| Concern | DocuHarnessX | OpenUltraSAST | Verdict |
|
||||
|---|---|---|---|
|
||||
| Composition seam | `make_docgen` in `docuharnessx/bundle.py` — the only HarnessX import site; `control \| stages_builder()`; returns model-less `HarnessConfig` | `build_sast()` in `harness/build_sast.py` (mirrors `examples/coding/build_coding`), lazy behind `_has_harnessx()` | **shared** |
|
||||
| Model binding | CLI `prepare_run()`: `model_config.agentic(make_docgen(...))`; model on `harness.model_config.main`, never in config | `HxScanOrchestrator`: `asyncio.run(model_config.agentic(cfg).run(ScanTask(...)))` | **shared** |
|
||||
| Processor base | `NoOpStage(MultiHookProcessor)`, content-neutral on `step_end`, real module-level classes for `_target_` resolution | `MultiHookProcessor(skip_model=True)` + `SlotContractMixin`, real module-level classes | **shared** |
|
||||
| Inter-stage seam | `RunContext` over append-only slot constants; frozen versioned handoff dataclasses | typed `sast.*` slots + `SlotContractMixin` (`reads_slots`/`writes_slots`) | **shared** |
|
||||
| Domain / "ontology" seam | `.docuharnessx/ontology.yaml` → `Vocabulary` (Role×Subject×Intent), loaded at run start | central **CWE policy + ruleset** (`policy/CWE_Score.tsv` + `ruleset/*.toml` + `rule_policy.json`), loaded at run start | **analogous** |
|
||||
| Acceptance firewall | COBESY all-of gate; judge `passed` untrusted/re-derived; unavailable → fail | `VerifyProcessor` (llm-judge + structural fallback, rejects unknown reachability, fail-closed) | **analogous** |
|
||||
| Steering | Kiro SDD; `CLAUDE.md`; `.kiro/{steering,specs}`; `kiro-impl` fresh-subagent + adversarial review; `kiro-verify-completion` | same Kiro convention; `.kiro/specs/harnessx-self-improving-rulesets/*`; OpenCode steers, kiro-* maintainer-only | **shared** |
|
||||
| Self-improvement (`evolve`) | **none** — future seam | `improve/evolve.py → MetaAgent.evolve`, bounded `rule`/`policy` levers, `rule_policy.json` ledger | **diverges (OUS builds it)** |
|
||||
| Dependency posture | `harnessx` direct git dep + pyyaml | core zero-dep; `harnessx` optional SHA-pinned extra, lazy | **diverges (by design)** |
|
||||
|
||||
## Three principled divergences (and reconciliation)
|
||||
|
||||
1. **Agent-first vs hybrid.** OUS keeps the sync `HarnessRuntime` through Phases 0–4 and confines
|
||||
HarnessX to the hunter pool (the runtimes are "incompatible at every load-bearing point —
|
||||
bridge, do not map"). Reconcile by treating `build_sast()` as building **only the hunter
|
||||
sub-harness**, applying DocuHarnessX's `make_docgen` pattern *at the hunter-pool boundary*.
|
||||
Optional Phase 5 converges OUS toward DocuHarnessX's agent-first end-state (retire
|
||||
`HarnessRuntime`) — the divergence is temporal, and DocuHarnessX is the north star.
|
||||
2. **Editable ontology vs authoritative severity.** DocuHarnessX's whole vocabulary is editable
|
||||
presets. OUS splits its domain seam into a **mutable detection vocabulary** (`ruleset/*.toml`,
|
||||
loop- and human-editable) and an **immutable severity/scope authority** (`CWE_Score.tsv`,
|
||||
verycode-upstream, the loop must never edit). Where DocuHarnessX relies on convention ("never a
|
||||
hardcoded literal"), OUS relies on a **validator** (`EvolveValidator._policy` forbids editing the
|
||||
0–5 severity) because a loop, not just a human, edits it.
|
||||
3. **Firewall-as-reward vs reward/feasibility split.** DocuHarnessX's firewall *is* the signal
|
||||
(binary pass/fail). OUS must feed `MetaAgent.evolve`, so it keeps a **scalar reward**
|
||||
(`project_score`) **separate from** a **hard feasibility constraint** (≥90% recall / <10% FP),
|
||||
un-folded so the loop can't game the number. Reuse DocuHarnessX's fail-closed firewall verbatim
|
||||
at the *per-finding* `VerifyProcessor` granularity; layer the reward/feasibility split on top.
|
||||
|
||||
## Phase 3 blueprint for OpenUltraSAST (mirror DocuHarnessX)
|
||||
|
||||
- **`build_sast()` factory** = the single HarnessX composition import site (DocuHarnessX's "blast
|
||||
radius is this file" rule on `bundle.py`); `control | hunter_stages_builder()`; raised loop
|
||||
thresholds (large-repo scanning revisits shapes, like large-repo docs); model bound later in
|
||||
`HxScanOrchestrator`. OUS delta: the `_has_harnessx()` guard + raise-then-fallback to
|
||||
`run_hunter_pool()`.
|
||||
- **Ontology seam** = CWE policy + ruleset as OUS's "ontology": `load_ruleset(path, ledger)` +
|
||||
`load_policy(tsv)` loaded into `sast.*` slots at run start; `resolve_severity()` is the single
|
||||
severity authority (mirrors DocuHarnessX's "every transform reads the loaded store, never a
|
||||
literal"); absent ledger → byte-identical baseline; unmapped enabled-rule CWE → fail-loud
|
||||
(stricter than DocuHarnessX, because severity is authoritative).
|
||||
- **Processor set**: register HarnessX classes directly **only inside the hunter sub-harness**
|
||||
(LoopDetection, CostGuard/TokenBudget, ToolFailureGuard, ToolFilter, SelfVerify, EpisodeMetrics/
|
||||
OTel/Checkpoint); everything deterministic is a `skip_model=True` `MultiHookProcessor` over slots
|
||||
(SarifIngest/Preprocess/EntryPoint/Rank/Calibrate/QuickFindings/PolicyScoring/Verify/Score) with
|
||||
stable `make_<stage>_processor()` factories so a spec replaces exactly one stub in place.
|
||||
- **`MetaAgent.evolve` loop** (Phase 4, on the same substrate): two bounded levers (`rule`,
|
||||
`policy`); reward = `project_score`; feasibility = recall/FP gate (un-folded); signal path
|
||||
benchmark per-rule precision/recall + `rule_policy_delta` → `rule_signals.json` →
|
||||
`MetaAgent.evolve`; gate = validity (canonicalize → contract → `_rules`/`_policy` → replay) →
|
||||
novelty/evidence → re-benchmark → attribution → accept or **byte-for-byte revert** (the
|
||||
structural analog of DocuHarnessX's `DEFAULT_UNAVAILABLE_VERDICT="fail"`); ledger =
|
||||
`rule_policy.json` (OUS's resume-safe analog of DocuHarnessX's markdown round journal).
|
||||
- **Steering surface**: reuse DocuHarnessX's `kiro-*` skills verbatim (maintainer-only); add
|
||||
SAST-domain `.kiro/steering/` rules pinning the detection gate as a hard invariant, the zero-dep
|
||||
guard, and the `pip --dry-run` audit gate; traceable per-module docstrings citing spec Req/Decision
|
||||
numbers.
|
||||
|
||||
## Net
|
||||
|
||||
The two projects already share the spine — one composition factory, model bound in the CLI via
|
||||
`ModelConfig.agentic`, content-neutral processors over typed slots, a project-loaded domain seam,
|
||||
a fail-closed acceptance firewall, and the Kiro fresh-subagent steering loop. OpenUltraSAST diverges
|
||||
in exactly three principled places (hybrid runtime confining HarnessX to the hunter pool; it *builds*
|
||||
the `MetaAgent.evolve` loop with a reward/feasibility split + `rule_policy.json` ledger; zero-dep core
|
||||
with HarnessX opt-in). Adopting DocuHarnessX's load-bearing patterns at the points above gives the
|
||||
two projects one architecture that opencode/claude-code steers identically.
|
||||
@@ -1,116 +0,0 @@
|
||||
# HarnessX Dependency Dry-Run Audit (Task 5.1)
|
||||
|
||||
Recorded realized transitive dependency graph and the accept-vs-vendor decision for
|
||||
adopting HarnessX (`git+https://github.com/Darwin-Agent/HarnessX.git`) in Phase 3.
|
||||
|
||||
## Method
|
||||
|
||||
```bash
|
||||
uv venv /tmp/hx-audit --python 3.11
|
||||
uv pip install --dry-run --python /tmp/hx-audit/bin/python <HarnessX checkout>
|
||||
```
|
||||
|
||||
- **118 transitive packages** resolved from HarnessX's **30 direct** `[project.dependencies]`
|
||||
(all mandatory upstream; the only extra is `dev`).
|
||||
- Pinned versions observed: `harnessx 0.1.0`, and engine image `registry.dagger.io`/SDK
|
||||
unrelated; LLM SDKs `anthropic 0.111.0`, `openai 2.43.0`, `litellm 1.89.3`.
|
||||
|
||||
### Realized transitive graph (118)
|
||||
|
||||
```
|
||||
aiofiles aiohappyeyeballs aiohttp aiosignal annotated-doc annotated-types anthropic
|
||||
antlr4-python3-runtime anyio attrs bracex certifi cffi charset-normalizer click cryptography
|
||||
distro docker dockerfile-parse docstring-parser e2b et-xmlfile fastapi fastuuid filelock
|
||||
frozenlist fsspec googleapis-common-protos greenlet grpcio h11 h2 harnessx hf-xet hpack
|
||||
html2text httpcore httptools httpx httpx-sse huggingface-hub hydra-core hyperframe idna
|
||||
importlib-metadata jinja2 jiter jsonschema jsonschema-specifications litellm loguru lxml
|
||||
markdown-it-py markupsafe mcp mdurl multidict omegaconf openai openpyxl opentelemetry-api
|
||||
opentelemetry-exporter-otlp opentelemetry-exporter-otlp-proto-common
|
||||
opentelemetry-exporter-otlp-proto-grpc opentelemetry-exporter-otlp-proto-http opentelemetry-proto
|
||||
opentelemetry-sdk opentelemetry-semantic-conventions packaging pdfminer-six pdfplumber pillow
|
||||
playwright prompt-toolkit propcache protobuf pycparser pydantic pydantic-core pydantic-settings
|
||||
pyee pygments pyjwt pypdfium2 python-dateutil python-docx python-dotenv python-multipart
|
||||
python-pptx pyyaml referencing regex reportlab requests rich rpds-py shellingham six sniffio
|
||||
sse-starlette starlette structlog tiktoken tokenizers tqdm typer typing-extensions
|
||||
typing-inspection urllib3 uvicorn uvloop watchfiles wcmatch wcwidth websockets xlsxwriter yarl zipp
|
||||
```
|
||||
|
||||
## Key finding: heavy backends are lazy; ~half the tree is dead weight
|
||||
|
||||
Every heavy backend is **lazily imported** — `docker`/`e2b` are `TYPE_CHECKING`-guarded and
|
||||
imported inside provider factories; `playwright` is imported inside `_get_local_page()`;
|
||||
office/PDF libs inside `tools/builtin/read.py` parse functions; `sandbox/__init__.py` registers
|
||||
docker/e2b via `try/except`. The `gateway/` is a **separate distribution**. So the 118-package
|
||||
tree is an **install-time** cost; at **runtime only what a given agent path touches is loaded**.
|
||||
|
||||
This is the answer to "I don't need most of these": neither OpenUltraSAST (SAST) nor the
|
||||
author's DocuHarnessX (docs) loads the bulk of it — HarnessX simply declares every capability as
|
||||
a *mandatory* dependency.
|
||||
|
||||
| Category | Packages | SAST verdict |
|
||||
|---|---|---|
|
||||
| LLM SDKs | anthropic, openai, litellm, tiktoken, tokenizers, huggingface-hub, hf-xet, jiter, regex, tqdm, fsspec | **needed** |
|
||||
| HTTP / async / pydantic core | httpx(+core/h11/h2/hpack/hyperframe), anyio, aiohttp(+glue), pydantic(+core/settings), certifi, urllib3, requests, sniffio | **needed** |
|
||||
| Config / CLI / logging | hydra-core, omegaconf, antlr4-runtime, typer, click, rich, prompt-toolkit, structlog, loguru, jsonschema(+glue) | **needed** |
|
||||
| Browser automation | **playwright, pyee, greenlet, html2text** | **dead weight** (+ out-of-band Chromium binary download via `playwright install`) |
|
||||
| Sandbox backends | docker, dockerfile-parse, **e2b** | docker = useful (isolate hunter); **e2b = dead weight** |
|
||||
| Web gateway / Lab UI | **fastapi, uvicorn, starlette, sse-starlette, httptools, websockets, uvloop, watchfiles, python-multipart, aiofiles** | **dead weight** (server surface; never started by a headless embed) |
|
||||
| gRPC + OpenTelemetry | grpcio, protobuf, googleapis-common-protos, opentelemetry-* (8) | **optional** (tracing only) |
|
||||
| Office / PDF | **python-pptx, python-docx, openpyxl, reportlab, pdfplumber, pdfminer-six, pypdfium2, pillow, xlsxwriter, lxml, et-xmlfile** | **dead weight** (SAST reads source, not documents) |
|
||||
| MCP | mcp, httpx-sse | optional (only if the agent calls MCP tools) |
|
||||
|
||||
Roughly **55–60 packages are opt-in capability weight** pulled in mandatorily, including a
|
||||
browser binary and a Docker SDK — an awkward footprint for a supply-chain auditing tool.
|
||||
|
||||
## Decision
|
||||
|
||||
**ACCEPT HarnessX as the optional, SHA-pinned extra `openultrasast[harnessx]`, lazy-imported
|
||||
behind `_has_harnessx()`. Keep a pre-specified vendored lean subset as a documented, triggered
|
||||
fallback. Pursue upstream dependency-extras as the preferred long-term slimming path.**
|
||||
|
||||
Rationale:
|
||||
1. **Footprint is fully opt-in.** Core `dependencies = []` stays zero-dep; the governance /
|
||||
scoring / benchmark / detection planes never pull HarnessX. Only `openultrasast[harnessx]`
|
||||
installs the tree, and lazy imports keep heavy backends cold unless the path runs.
|
||||
2. **Author precedent.** DocuHarnessX already ships `harnessx` as a direct git dependency — the
|
||||
`ModelConfig.agentic` integration is proven and maintained. OpenUltraSAST reuses the
|
||||
mechanism but tightens the posture (opt-in, not direct).
|
||||
3. **Lower maintenance than vendoring.** Accept + SHA-pin gives reproducibility without a fork;
|
||||
vendoring is paid only if the *installed* graph is itself judged unacceptable.
|
||||
|
||||
### Preferred long-term: upstream dependency-extras (helps both projects)
|
||||
|
||||
The clean fix for "I don't need most of these" is to modularize HarnessX's own dependencies into
|
||||
extras — e.g. `harnessx[browser]`, `harnessx[sandbox-e2b]`, `harnessx[gateway]`, `harnessx[office]`,
|
||||
`harnessx[otel]` — leaving a lean agent-runtime core (`anthropic`/`openai`/`litellm`/`pydantic`/
|
||||
`httpx`/`hydra`/`structlog`/`mcp`). Since the author owns the `Darwin-Agent/HarnessX` line, this is
|
||||
actionable and benefits DocuHarnessX equally. OpenUltraSAST would then depend on
|
||||
`harnessx` (lean core) under its `[harnessx]` extra and add only the subsets it needs.
|
||||
|
||||
### Vendored lean subset (fallback, fully specified)
|
||||
|
||||
If triggered, vendor `harnessx/` only:
|
||||
|
||||
- **KEEP**: `core/`, `processors/{control,evaluation,observability,tools,context,memory,multi_model}`,
|
||||
`meta_harness/`, `rl/`, `providers/`, `tools/builtin/` (minus `browser.py`/`web_*`),
|
||||
`sandbox/{base,local,docker}.py`, plus `config`/`tracing`/`plugins`/`bundles`/`workspace` as needed.
|
||||
- **REMOVE**: `frontend/`, `gateway/`, `harnessx/api/` (drops the web-server stack), `examples/`,
|
||||
`docs/`, `benchmarks/`, `container/`, `scripts/`, `recipe/` (incl. the `recipe/verl_harnessX/verl`
|
||||
submodule — clone non-recursive regardless), `extensions/skills/{docx,pdf,pptx,xlsx}` (drops
|
||||
office/PDF), `harnessx/sandbox/e2b.py` (drops e2b), `harnessx/tools/builtin/browser.py`
|
||||
(drops playwright + browser binary). Net ≈ 40–60% fewer packages, no browser binary.
|
||||
|
||||
## Residual risks (security-tool-specific) and mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| `playwright install` fetches a Chromium binary out-of-band (unaudited, breaks hermetic builds) | Browser path is lazy and never invoked; document that `[harnessx]` does not trigger a browser download; lean subset deletes `browser.py` |
|
||||
| `docker` SDK can reach the Docker daemon socket (privilege boundary) | Docker sandbox **default-off**, `LocalSandbox` default; run untrusted target code only inside the sandbox; SHA-pin docker/cryptography/cffi |
|
||||
| FastAPI/uvicorn listening-socket surface in a security tool | `api/` server only starts on the explicit `serve` path; add a guard test asserting no server starts on import; lean subset removes `api/` |
|
||||
| 118-package CVE surface in a supply-chain tool | SHA-pin the entire extra; keep core **zero-dep**; add a **zero-dep CI guard test** (importing `openultrasast` without the extra pulls nothing outside stdlib) |
|
||||
| `recipe/verl` git submodule recursion | Init non-recursively; never in the accept-as-extra install path |
|
||||
|
||||
> Decision date 2026-06-22. The `pip install --dry-run` audit is itself the Phase-3 gate: it is
|
||||
> re-run before the packaging task (5.2), and if the realized graph pulls a browser binary /
|
||||
> Docker SDK / FastAPI stack as *mandatory* (non-lazy) deps judged unacceptable, the spec switches
|
||||
> to the vendored lean subset under the same extra name.
|
||||
@@ -1,614 +0,0 @@
|
||||
# Design Document
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Overview
|
||||
|
||||
**Purpose.** This feature splits OpenUltraSAST's single overloaded regex layer — which today is detector, severity authority, and volume knob at once — into three governed owners: **rules detect**, a **central CWE policy decides severity and scope**, and a **single project score** becomes the optimization target. On top of that governance plane it adopts the HarnessX runtime exactly where it pays off — the LLM hunter loop, the LLM-judge verifier, and a meta-harness self-improvement loop that adapts rules and policy gates under hard bounds.
|
||||
|
||||
**Users.** OpenUltraSAST maintainers gain a staged, reversible HarnessX adoption and an automatic, gated rule/policy tuning loop. Security policy owners gain a versioned, central CWE-to-severity policy. Security reviewers and CI consumers gain a bounded 0-100 AppSec project score with a two-condition gate.
|
||||
|
||||
**Impact.** Today `findings.PatternRule` carries a string `severity`, `quick_scan_findings` reads a module-level `PATTERN_RULES`, `calibration.py` only demotes ranking (floored at `MIN_CALIBRATED_PRIORITY = 0.1`, keyed on `tags[0]`), `benchmark.py:368` writes an inert `next_improvement_candidate="rules_or_language_hunter"`, and `harness.HarnessRuntime.run_stage` is a sync dict-state driver. This design turns rules into TOML data, removes `PatternRule.severity`, sources severity from a vendored verycode `CWE_Score.tsv`, computes a project score, makes the benchmark actionable, and confines HarnessX behind an optional, SHA-pinned, lazy-imported extra — while `dependencies = []` and `requires-python = ">=3.11"` stay intact and the **≥90% recall / <10% FP** detection gate holds at every phase.
|
||||
|
||||
### Goals
|
||||
|
||||
- Rules become versioned data (`ruleset/<lang>/*.toml`) with a loop-controlled `status` and no rule-local severity.
|
||||
- One CWE = one governed severity, sourced from `policy/CWE_Score.tsv` via `resolve_severity()`.
|
||||
- A single 0-100 `project_score` from `policy.severity × reachability`, with a two-condition CI gate.
|
||||
- HarnessX adopted at the hunter loop, the verifier, and the meta-harness, behind `openultrasast[harnessx]`.
|
||||
- A bounded self-improvement loop (`rule`, `policy` levers) that auto-reverts byte-for-byte under the gate.
|
||||
- The governance + scoring + benchmark planes stay **zero-dependency** and **byte-identical by default** through Phases 0-2.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- The loop authoring or rewriting detection pattern text (human PR only).
|
||||
- The loop editing the authoritative 0-5 CWE severity (verycode-upstream authority).
|
||||
- A full single-step HarnessX rewrite of all deterministic stages and weight-level RL (deferred to optional Phase 5+).
|
||||
- Changing the reachability vocabulary, which stays exactly `reachable | inferred-file-surface | unknown`.
|
||||
- Dynamic/DAST scoring — report-only for a static tool.
|
||||
|
||||
## Boundary Commitments
|
||||
|
||||
### This Spec Owns
|
||||
|
||||
- The **RulesetStore** (`ruleset/store.py` + `ruleset/<lang>/*.toml`): `load_ruleset()`, the `PatternRule` schema (detection-only), and the `enabled|shadow|disabled` status vocabulary.
|
||||
- The **PolicyStore** (`policy/verycode.py` + `policy/CWE_Score.tsv`): `load_policy()`, `CwePolicy`, `resolve_severity()`, and the CWE-120→CWE-121 remap.
|
||||
- The **ScoreModel** (`scoring/project_score.py`): `SEV_WEIGHT`, `REACH_MULT`, `finding_penalty`, `project_score`, the score artifact, and the two-condition CI gate.
|
||||
- The **loop ledger** (`.openultrasast/calibration/rule_policy.json`): the loop-owned overlay of `status`/evidence-floor/threshold/precision.
|
||||
- The **HarnessX seam**: `harness/build_sast.py`, `HxScanOrchestrator`, the `MultiHookProcessor` SAST processors, the `SlotContractMixin`, and the `improve/` meta-harness driver.
|
||||
- The **benchmark→signal** conversion: `rule_id` attribution, per-rule precision/recall, `rule_policy_delta`, and `rule_signals.json`.
|
||||
|
||||
### Out of Boundary
|
||||
|
||||
- The detection-pattern **content** of rules (authored by humans via PR; the loop may only toggle status / raise floors).
|
||||
- The authoritative **0-5 severity** numbers in `CWE_Score.tsv` (verycode-upstream; PR gaps there).
|
||||
- Upstream **HarnessX internals** beyond the minimal meta-harness patch (`_VALID_LEVERS`, `_render_task_brief`, `EvolveValidator`, changeset buckets, `contract_autocheck`).
|
||||
- The DAST/dynamic execution engine; dynamic-only CWEs are report-only here.
|
||||
- Retiring `HarnessRuntime` and adopting `rl/` — explicitly Phase 5+, optional.
|
||||
|
||||
### Allowed Dependencies
|
||||
|
||||
- **Stdlib only** for the governance/scoring/benchmark planes: `tomllib`, `csv`, `json`, `math`, `dataclasses`.
|
||||
- **HarnessX** only behind the `[harnessx]` extra, pinned to a commit SHA, imported lazily behind `_has_harnessx()`.
|
||||
- The existing `HarnessRuntime` as the top-level sync driver through Phases 0-4 (not removed by this spec until Phase 5).
|
||||
- The existing reachability vocabulary emitted by `findings.py` (consumed, never redefined).
|
||||
|
||||
### Revalidation Triggers
|
||||
|
||||
- **Contract shape changes** to `PatternRule` (removing `severity`), `CwePolicy`, `VerificationResult`, `BenchmarkMiss`/`BenchmarkFalsePositive` (adding `rule_id`), or the `project_score` artifact JSON.
|
||||
- **Data ownership change**: severity moving from rule → policy; calibration re-keying from `tags[0]` → `rule_id`.
|
||||
- **Dependency direction change**: any new import of HarnessX outside a lazy `_has_harnessx()` guard; any addition to core `dependencies`.
|
||||
- **Runtime prerequisite change**: the startup CWE-resolution invariant (fail-loud if an enabled rule's CWE is unmapped); the `pip install --dry-run` audit gate before Phase 3.
|
||||
- **Lever surface change**: any addition to `journal._VALID_LEVERS` or the `EvolveValidator` bounds.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Existing Architecture Analysis
|
||||
|
||||
| Existing element | File | Constraint to respect / debt to fix |
|
||||
|---|---|---|
|
||||
| `HarnessRuntime.run_stage(name, callback)` | `harness.py:74` | Sync, dict `state` (`{"degradations": []}`), single `HarnessEvent` (`scan_id`), `ProcessorSpec.reads/writes` allow-list. Incompatible with HarnessX async-generator + message-window contract — **bridge, do not map**. Stays as the top-level driver through Phase 4. |
|
||||
| `PatternRule.severity: str` + module-level `PATTERN_RULES` | `findings.py:31,34` | The fused defect: regex carries its own severity string ("high"/"medium"/"low") and is the only volume control. **Remove `severity`; externalize patterns to TOML.** |
|
||||
| `quick_scan_findings(root, targets, rankings)` | `findings.py` / `cli.py:126` | Reads `PATTERN_RULES` implicitly. Becomes `quick_scan_findings(..., ruleset)` wired like `static_hints`. |
|
||||
| `calibrate_rankings` demotion | `calibration.py:146` | `max(MIN_CALIBRATED_PRIORITY, calibrated - 0.5)`, keyed on `finding.tags[0]` (`calibration.py:262`). Demotes ranking, never the rule. **Re-key to `rule_id`; write status flips to ledger.** |
|
||||
| `verify_findings` rejects on `reachability_status == "unknown"` | `verification.py:91` | Structural verifier. Keep as **zero-dep fallback**; add LLM-judge path. |
|
||||
| `benchmark.py:368` `next_improvement_candidate="rules_or_language_hunter"` | `benchmark.py` | Inert stub written to `calibration_records.json`. **Make actionable** via `rule_id` attribution + `rule_policy_delta`. |
|
||||
| `cli.py` `run_stage(...)` call chain | `cli.py:104-145` | The 13-stage sync pipeline. Stages migrate to HarnessX hooks incrementally; the hunter pool first. |
|
||||
|
||||
### Architecture Pattern & Boundary Map
|
||||
|
||||
**Selected pattern.** Three planes with one-directional authority: **governance data drives execution; execution emits signals; the improvement loop edits governance under a gate.** Nothing in execution mutates governance directly. The governance + scoring planes are **pure stdlib, zero HarnessX** (non-negotiable, so CI gating runs in the default install). HarnessX lives only in the execution and improvement planes, behind the extra.
|
||||
|
||||
**Boundaries.** RulesetStore detects, PolicyStore decides severity/scope, ScoreModel optimizes — three owners, three change channels (human PR for patterns/severity; loop-via-`EvolveValidator` for status/floors/score-constants).
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph GOV["GOVERNANCE PLANE — stdlib, versioned, audited, ZERO-DEP"]
|
||||
direction LR
|
||||
POL["PolicyStore<br/>policy/verycode.py<br/>CWE_Score.tsv -> CwePolicy<br/>severity 0-5, static/dynamic"]
|
||||
RUL["RulesetStore<br/>ruleset/store.py<br/>*.toml: pattern, cwe, status"]
|
||||
LED["rule_policy.json<br/>loop-owned: status/floor/precision"]
|
||||
SCO["ScoreModel<br/>scoring/project_score.py<br/>policy-weighted -> 0-100 + gate"]
|
||||
FPL["FP ledger<br/>false_positive_learnings.json"]
|
||||
end
|
||||
subgraph EXE["EXECUTION PLANE — HarnessX runtime, OPTIONAL EXTRA"]
|
||||
direction LR
|
||||
DET["Deterministic stages<br/>skip_model MultiHookProcessors"]
|
||||
HUNT["hunter_pool<br/>HxScanOrchestrator (real LLM loop)"]
|
||||
VER["VerifyProcessor<br/>llm_judge helpers (+ structural fallback)"]
|
||||
PSP["PolicyScoringProcessor + ScoreProcessor"]
|
||||
end
|
||||
subgraph IMP["IMPROVEMENT PLANE — meta_harness, OPTIONAL EXTRA"]
|
||||
EVO["improve/evolve.py -> MetaAgent.evolve()<br/>levers: rule | policy"]
|
||||
BEN["BenchmarkHarness<br/>recall/FP + per-rule attribution"]
|
||||
end
|
||||
POL --> DET & PSP & SCO
|
||||
RUL --> DET & HUNT
|
||||
LED --> RUL
|
||||
DET --> HUNT --> VER --> PSP --> SCO
|
||||
VER --> FPL
|
||||
EXE -->|trajectories + verdicts| BEN
|
||||
BEN -->|rule_signals.json + deltas| EVO
|
||||
EVO -->|gated edits| RUL & POL & LED
|
||||
SCO -->|gate verdict| CI[("CI gate")]
|
||||
BEN -->|>=90% recall / <10% FP| CI
|
||||
```
|
||||
|
||||
**New components rationale.** PolicyStore/ScoreModel/RulesetStore exist to break the fused regex defect; `HxScanOrchestrator`/`VerifyProcessor` exist to host the agent loop and judge; `improve/evolve.py`+`EvolveValidator` exist to make adaptation safe (bounded, gated, reversible). **Steering compliance:** zero-dep default install preserved; detection gate kept as a hard constraint separate from the score reward.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Layer | Choice / Version | Role in Feature | Notes |
|
||||
|-------|------------------|-----------------|-------|
|
||||
| CLI | `cli.py` (`_run_scan`) | Wires ruleset/policy; Phase 3 routes hunter pool to `HxScanOrchestrator` | Unchanged entrypoints when extra absent |
|
||||
| Governance | Python 3.11 stdlib (`tomllib`, `csv`, `dataclasses`) | RulesetStore + PolicyStore load | Zero-dep, fail-loud on unmapped CWE |
|
||||
| Scoring | Python stdlib `math` | `project_score` exponential decay | Always on, zero-dep |
|
||||
| Execution runtime | HarnessX `@ git+...@<SHA>` (optional extra) | Hunter loop, judge verifier, processors | Lazy import behind `_has_harnessx()` |
|
||||
| Self-improvement | HarnessX `meta_harness` (extra/vendored) | `MetaAgent.evolve` + `EvolveValidator` + journal | 5/6-hook minimal patch |
|
||||
| Data / Storage | TOML rulesets, TSV policy, JSON ledger/artifacts | Versioned governance + loop overlay | All `_target_`/JSON-serializable for resume |
|
||||
| Events | HarnessX lifecycle hooks (`on_task_start`…`on_task_end`) | Stage→hook mapping | `HarnessRuntime` sync driver retained Phases 0-4 |
|
||||
| Packaging | `pyproject.toml` `[project.optional-dependencies]` | `harnessx` extra, pinned SHA | Core `dependencies = []` unchanged |
|
||||
|
||||
## File Structure Plan
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/openultrasast/
|
||||
├── ruleset/
|
||||
│ ├── store.py # load_ruleset()/write_ruleset(); PatternRule (NO severity); status/min_evidence_level/precision_estimate/version
|
||||
│ ├── python/*.toml # rules-as-data, one [[rule]] per detector
|
||||
│ ├── c/*.toml # incl. CWE-120 -> CWE-121 remapped buffer rules
|
||||
│ └── <lang>/*.toml # same pattern per language
|
||||
├── policy/
|
||||
│ ├── verycode.py # load_policy() -> dict[str, CwePolicy]; resolve_severity(); CwePolicy dataclass
|
||||
│ └── CWE_Score.tsv # vendored verycode data (167 CWEs; header "Flaw Severity " has trailing space)
|
||||
├── scoring/
|
||||
│ └── project_score.py # SEV_WEIGHT, REACH_MULT, finding_penalty(), project_score(), gate()
|
||||
├── harness/ # Phase 3 (extra)
|
||||
│ ├── build_sast.py # build_sast() factory mirrors examples/coding/build_coding
|
||||
│ ├── task.py # ScanTask(BaseTask)
|
||||
│ ├── run.py # HxScanOrchestrator: asyncio.run around model_config.agentic(cfg).run(...)
|
||||
│ ├── slot_contract.py # SlotContractMixin: reads_slots/writes_slots on MultiHookProcessor
|
||||
│ └── processors.py # SarifIngest/Preprocess/EntryPoint/Rank/Calibrate/QuickFindings/PolicyScoring/Verify/Score processors
|
||||
└── improve/ # Phase 4 (extra)
|
||||
├── evolve.py # orchestrator: benchmark -> delta -> rule_signals.json -> MetaAgent.evolve() -> re-benchmark -> accept/revert
|
||||
├── rule_validator.py # EvolveValidator._rules / ._policy bound checks
|
||||
└── rule_autocheck.py # ContractAutoCheckProcessor _check_rule/_check_policy branches
|
||||
|
||||
.openultrasast/calibration/
|
||||
└── rule_policy.json # loop-owned ledger: status/threshold/evidence-floor/precision per rule_id
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `findings.py` — extract `PATTERN_RULES` to TOML; **delete `PatternRule.severity`**; `quick_scan_findings(root, targets, rankings, ruleset)` takes ruleset arg.
|
||||
- `calibration.py` — re-key from `tags[0]` (line 262) to `rule_id`; write `rule_policy.json` status flips, not only ranking demotion (line 146).
|
||||
- `benchmark.py` — add `rule_id` to `BenchmarkMiss`/`BenchmarkFalsePositive`; per-rule precision/recall in `BenchmarkMetrics`; emit `rule_policy_delta`; replace the inert stub at line 368; add `BenchEvaluator` subclass.
|
||||
- `verification.py` — add LLM-judge path (`VerifyProcessor` over `llm_judge` helpers); keep structural verifier (`reachability_status == "unknown"` reject, line 91) as zero-dep fallback.
|
||||
- `config.py` — ruleset/policy/score paths + gate thresholds (`K`, `MIN_SCORE`, `min_emit_priority`, `min_emit_precision`).
|
||||
- `cli.py` — `_run_scan` wires ruleset/policy; Phase 3 routes the `hunter_pool` `run_stage` call (line 122) to `HxScanOrchestrator`.
|
||||
- `harness.py` — retired in Phase 5 only.
|
||||
- `pyproject.toml` — add `[project.optional-dependencies] harnessx = ["harnessx @ git+https://github.com/Darwin-Agent/HarnessX.git@<SHA>"]`; core `dependencies = []` unchanged.
|
||||
- `harnessx/meta_harness/{journal.py, agent.py, validate_workflow.py, processors/contract_autocheck.py}` — vendored/patched seam (Phase 4).
|
||||
|
||||
## System Flows
|
||||
|
||||
### Scan pipeline on HarnessX (Phase 3 seam)
|
||||
|
||||
The top-level driver stays sync (`HarnessRuntime`); only the `hunter_pool` stage opens an async surface via `asyncio.run`. Deterministic stages run as `skip_model=True` processors carrying work in typed slots; the `SlotContractMixin` validates `reads_slots`/`writes_slots` on every hook (preserving the `ProcessorSpec.reads/writes` discipline). If a HarnessX capability is unavailable at runtime, the stage falls back to its existing equivalent and records a degradation in the manifest.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["on_task_start: SarifIngestProcessor<br/>-> slot sast.static_hints"] --> B["Preprocess -> sast.targets"]
|
||||
B --> C["EntryPoint -> sast.entry_points"]
|
||||
C --> D["on_step_start: Rank -> sast.rankings"]
|
||||
D --> E["Calibrate (MemoryRetrieval pattern over FP ledger)<br/>demotes sast.rankings"]
|
||||
E --> F{"HarnessX extra<br/>present & capable?"}
|
||||
F -->|yes| G["hunter_pool: HxScanOrchestrator<br/>asyncio.run(model_config.agentic(cfg).run(ScanTask))<br/>control/tools/obs processors -> StatefulTrajectory"]
|
||||
F -->|no / capability missing| G2["fallback run_hunter_pool()<br/>record degradation in manifest"]
|
||||
G --> H["QuickFindings: regex over load_ruleset()<br/>-> sast.findings (NO severity yet)"]
|
||||
G2 --> H
|
||||
H --> I["PolicyScoringProcessor: resolve_severity(policy, rule.cwe)<br/>+ emission decision; drop shadow findings"]
|
||||
I --> J{"reachability_status<br/>== unknown?"}
|
||||
J -->|yes| K["VerifyProcessor: REJECTED (structural fallback)<br/>or llm_judge verdict"]
|
||||
J -->|no| K2["VerifyProcessor: llm_judge build/parse/call<br/>(structural fallback if extra absent)"]
|
||||
K --> L["record_calibration: lesson/missing -> FalsePositiveLearning<br/>+ per-rule outcome -> rule_policy.json"]
|
||||
K2 --> L
|
||||
L --> M["on_task_end: ScoreProcessor -> project_score + gate"]
|
||||
M --> N["report / sarif / manifest (incl. score block)"]
|
||||
```
|
||||
|
||||
### Self-improvement loop
|
||||
|
||||
The benchmark is the **regression suite governing the data stores**. The objective is `maximize project_score subject to recall ≥ 90% AND FP < 10%` as a hard feasibility constraint — kept separate so the loop cannot "improve" the number by shadowing rules that hurt real security. Every change is `shadow`-staged first, novelty/evidence/replay-gated, and reverted byte-for-byte on any failure.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as BenchmarkHarness
|
||||
participant O as improve/evolve.py
|
||||
participant M as MetaAgent.evolve()
|
||||
participant V as EvolveValidator (_rules/_policy)
|
||||
participant J as journal (compute_attribution/fill_gating)
|
||||
participant L as rule_policy.json
|
||||
B->>O: misses(recall) + FPs/verifier-rejects(precision) + per-rule metrics
|
||||
O->>O: write rule_signals.json [{rule_id, signal, cwe, evidence, suggested_action}]
|
||||
O->>M: evolve(brief + rule_signals.json)
|
||||
M->>M: write ruleset/*.toml + policy/overrides.toml + candidates.md + journal Round N
|
||||
M->>V: validity(canon -> contract -> _rules/_policy -> replay) + policy(novelty + evidence)
|
||||
alt validation fails
|
||||
V-->>M: StrictValidationError(kind="rule_change") -> round rejected
|
||||
else passes
|
||||
V->>B: re-run benchmark on candidate
|
||||
B->>J: passed_now vs passed_before
|
||||
J->>J: compute_attribution(predicted): flipped/still_F/regressed/still_T/absent
|
||||
alt recall<90% OR FP>=10% OR score regressed OR unpredicted regression
|
||||
J->>L: fill_gating(reverted) -> restore config byte-for-byte
|
||||
else
|
||||
J->>L: fill_gating(accepted) -> persist status/floor/precision
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Requirements Traceability
|
||||
|
||||
| Requirement | Summary | Components | Interfaces | Flows |
|
||||
|-------------|---------|------------|------------|-------|
|
||||
| 1.1, 1.4 | Hunter pool on real `Harness.run()`; async confined to one stage | `HxScanOrchestrator` | `run.py:HxScanOrchestrator.run()` | Scan pipeline |
|
||||
| 1.2 | Verify via llm-judge + structural fallback | `VerifyProcessor` | `processors.py:VerifyProcessor` | Scan pipeline |
|
||||
| 1.3 | Extra absent → unchanged quick/standard | `cli._run_scan`, `_has_harnessx()` | capability guard | Scan pipeline (`F` branch) |
|
||||
| 1.5 | Slot allow-list validated per hook | `SlotContractMixin` | `slot_contract.py` | Scan pipeline |
|
||||
| 1.6 | Deterministic stages `skip_model` | SAST processors | `MultiHookProcessor(skip_model=True)` | Scan pipeline |
|
||||
| 1.7 | Event/`scan_id` rename to HarnessX hooks/`run_id` | `build_sast`, journal | hook map table | Scan pipeline |
|
||||
| 1.8 | Capability-missing fallback + manifest degradation | `HxScanOrchestrator` | manifest `degradations` | Scan pipeline (`G2`) |
|
||||
| 2.1, 2.4 | Rules-as-data; 3 statuses; no rule severity | RulesetStore | `load_ruleset()`, `PatternRule` | — |
|
||||
| 2.2, 2.3 | Severity from policy; legacy strings discarded | PolicyStore, `PolicyScoringProcessor` | `resolve_severity()` | Scan pipeline (`I`) |
|
||||
| 2.5 | Shadow excluded from report+score | `PolicyScoringProcessor`, ScoreModel | emission filter | Scan pipeline (`I`,`M`) |
|
||||
| 2.6 | CwePolicy 0-5 + static/dynamic; dynamic report-only | PolicyStore | `CwePolicy` | — |
|
||||
| 2.7 | Fail-loud on unmapped enabled-rule CWE | PolicyStore startup invariant | `load_policy()` check | — |
|
||||
| 2.8 | CWE-120 → CWE-121 remap | RulesetStore | TOML data | — |
|
||||
| 2.9, 5.9, 7.5 | Governance/score/benchmark zero-dep | all GOV components | stdlib-only | — |
|
||||
| 3.1 | Two levers `rule`/`policy` | `improve/evolve.py` | `_VALID_LEVERS` | Self-improvement |
|
||||
| 3.2, 3.3 | No pattern text / no 0-5 severity edits | `EvolveValidator` | `_rules`/`_policy` | Self-improvement |
|
||||
| 3.4 | No enabled→disabled jump (shadow mandatory) | `EvolveValidator._rules` | bound check | Self-improvement |
|
||||
| 3.5 | Parse/status/bounds/dup/CWE-resolve checks | `EvolveValidator._rules` | validity phase | Self-improvement |
|
||||
| 3.6 | Novelty gate on reverted re-proposals | `journal.check_novelty` | novelty gate | Self-improvement |
|
||||
| 3.7 | Replay smoke gate (ruleset boots) | `run_synthetic_task_smoke_gate` | replay phase | Self-improvement |
|
||||
| 3.8, 3.9 | `StrictValidationError`; journal each edit | `EvolveValidator`, journal | `fill_gating` | Self-improvement |
|
||||
| 4.1, 4.2 | `rule_id` attribution; per-rule metrics; delta artifact | BenchmarkHarness | `rule_policy_delta` | Self-improvement |
|
||||
| 4.3 | Delta → `rule_signals.json` (miss/fp) | `improve/evolve.py` | signal writer | Self-improvement |
|
||||
| 4.4, 4.5 | Hard accept gate; byte-for-byte revert | `improve/evolve.py`, journal | `fill_gating(reverted)` | Self-improvement |
|
||||
| 4.6 | Auto-shadow precision-draggers | `improve/evolve.py` | status flip | Self-improvement |
|
||||
| 4.7 | Coverage-gap nominates rule (human PR) | `improve/evolve.py` | `rule_signals.json` | Self-improvement |
|
||||
| 4.8, 6.5 | Score=reward, gate=feasibility (separate) | `improve/evolve.py` | objective split | Self-improvement |
|
||||
| 5.1, 5.2 | 0-100 score; reachability multipliers | ScoreModel | `project_score()` | Scan pipeline (`M`) |
|
||||
| 5.3 | Dynamic-only/unmapped → 0 penalty, report-only | ScoreModel | `finding_penalty()` | Scan pipeline (`M`) |
|
||||
| 5.4 | Shadow excluded from score input | ScoreModel | pre-call filter | Scan pipeline (`M`) |
|
||||
| 5.5 | Score artifact + manifest block | ScoreModel | score JSON | Scan pipeline (`N`) |
|
||||
| 5.6 | FP lowers effective reachability, not delete | `calibration.py` | reachability knob | Self-improvement |
|
||||
| 5.7 | Hard gate: sev5 + reachable → fail | CI Score Gate | `gate()` | — |
|
||||
| 5.8 | Score gate advisory-first, then blocking | CI Score Gate | `gate()` | — |
|
||||
| 6.1, 6.4, 6.6 | Per-merge recall/FP gate + HX tolerance | CI Pipeline | benchmark gate | — |
|
||||
| 6.2 | Byte-identical baseline Phases 0-2 | BenchmarkHarness | golden diff | Migration |
|
||||
| 6.3 | Gate must pass on re-benchmarked candidate | `improve/evolve.py` | accept gate | Self-improvement |
|
||||
| 7.1, 7.6 | Empty core deps; pinned extra; non-recursive submodule | Packaging Config | `pyproject.toml` | Migration |
|
||||
| 7.2 | Lazy import behind capability check | `_has_harnessx()` | guard | — |
|
||||
| 7.3, 7.4, 7.7 | Dry-run audit; vendored fallback trigger; pre-specified | Packaging Config | audit gate | Migration (Phase 3 gate) |
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|
||||
|-----------|--------------|--------|--------------|--------------------------|-----------|
|
||||
| RulesetStore | Governance | Rules-as-data + status | 2.1,2.4,2.8 | tomllib (P0), rule_policy.json (P1) | State |
|
||||
| PolicyStore | Governance | CWE→severity/scope authority | 2.2,2.6,2.7 | CWE_Score.tsv (P0) | State |
|
||||
| ScoreModel | Scoring | 0-100 score + gate | 5.1-5.9 | PolicyStore (P0) | Batch, State |
|
||||
| HxScanOrchestrator | Execution | Hunter loop on HarnessX | 1.1,1.4,1.8 | HarnessX (P0), RulesetStore (P1) | Service, Event |
|
||||
| PolicyScoringProcessor | Execution | Resolve severity + emission | 2.2,2.3,2.5 | PolicyStore (P0) | Event |
|
||||
| VerifyProcessor | Execution | LLM-judge + structural fallback | 1.2 | llm_judge (P1) | Event |
|
||||
| EvolveValidator (`_rules`/`_policy`) | Improvement | Bound-check loop edits | 3.2-3.8 | journal (P0), PolicyStore (P1) | Service |
|
||||
| improve/evolve.py | Improvement | Benchmark→signal→evolve→gate | 4.1-4.8,6.3 | BenchmarkHarness (P0), MetaAgent (P0) | Batch |
|
||||
|
||||
### Governance Layer
|
||||
|
||||
#### RulesetStore
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Load detection rules from versioned TOML; carry loop-controlled status; carry no severity |
|
||||
| Requirements | 2.1, 2.4, 2.8, 2.9 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- `load_ruleset(path) -> tuple[PatternRule, ...]` replaces module-level `PATTERN_RULES`; loaded with `tomllib` (zero-dep).
|
||||
- `PatternRule` carries `rule_id`, `languages`, `cwe`, `tags`, `pattern`, `status`, `min_evidence_level`, `precision_estimate`, `version` — **no `severity` field**.
|
||||
- `status ∈ {enabled, shadow, disabled}` is the volume control; the loop overlays `status`/`min_evidence_level`/`precision_estimate` from `rule_policy.json`.
|
||||
- C/C++ buffer rules (`c-unsafe-copy`, `c-unsafe-memory-copy`, `c-unsafe-scanf`) declare `cwe = "CWE-121"` (remapped from unmapped CWE-120).
|
||||
|
||||
**Dependencies** — Inbound: `quick_scan_findings` (P0). Outbound: `rule_policy.json` overlay (P1). External: `tomllib` (P0).
|
||||
|
||||
**Contracts**: State [x]
|
||||
|
||||
##### Service Interface
|
||||
```python
|
||||
def load_ruleset(path: Path, ledger: Path | None = None) -> tuple[PatternRule, ...]: ...
|
||||
def write_ruleset(path: Path, rules: Iterable[PatternRule]) -> None: ...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatternRule:
|
||||
rule_id: str
|
||||
languages: tuple[str, ...]
|
||||
cwe: str # the ONLY policy join key
|
||||
tags: tuple[str, ...]
|
||||
pattern: str # human-PR-only; loop must never edit
|
||||
status: str = "enabled" # enabled | shadow | disabled (loop-controlled)
|
||||
min_evidence_level: str = "static_corroboration"
|
||||
precision_estimate: float = 0.0
|
||||
version: str = "1"
|
||||
```
|
||||
- **Preconditions:** every loaded rule has a non-empty `pattern` and a `CWE-NNN` `cwe`.
|
||||
- **Postconditions:** ledger overlay applied; `status`/floors reflect `rule_policy.json`.
|
||||
- **Invariants:** no `severity` attribute exists; `status` is one of the three literals.
|
||||
|
||||
#### PolicyStore
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Authoritative CWE→severity/scope; resolve finding severity |
|
||||
| Requirements | 2.2, 2.3, 2.6, 2.7, 2.8 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- `load_policy() -> dict[str, CwePolicy]` keyed `"CWE-NNN"` from `CWE_Score.tsv` (167 CWEs; severity column header is literally `"Flaw Severity "` with a trailing space — parse defensively).
|
||||
- `resolve_severity(policy, rule.cwe, target, ranking)` replaces every literal `rule.severity` read. **Policy always wins**; any legacy rule severity string is discarded.
|
||||
- **Startup invariant (fail-loud):** every `enabled` rule's `cwe` must resolve in PolicyStore, else the scan aborts. `dynamic`-only CWEs are report-only.
|
||||
|
||||
**Dependencies** — Inbound: `PolicyScoringProcessor`, ScoreModel (P0). External: `csv`, `dataclasses` (P0).
|
||||
|
||||
**Contracts**: State [x]
|
||||
|
||||
##### Service Interface
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class CwePolicy:
|
||||
flaw_category: str
|
||||
severity: int # 0..5 — AUTHORITATIVE; overrides any rule string; loop must never edit
|
||||
static: bool # SAST scope gate
|
||||
dynamic: bool # DAST scope — report out-of-scope, never scored by static tool
|
||||
|
||||
def load_policy(tsv: Path = CWE_SCORE_TSV) -> dict[str, CwePolicy]: ...
|
||||
def resolve_severity(policy: Mapping[str, CwePolicy], cwe: str, target, ranking) -> int: ...
|
||||
def assert_rules_resolve(rules, policy) -> None: # raises at startup if an enabled rule's CWE is unmapped
|
||||
...
|
||||
```
|
||||
- **Invariants:** one CWE ⇒ one severity; two rules for the same CWE always receive identical severity.
|
||||
|
||||
### Scoring Layer
|
||||
|
||||
#### ScoreModel
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Compute a 0-100 project score and the two-condition gate verdict |
|
||||
| Requirements | 5.1-5.9 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Pure stdlib (`math`), always on, zero-dep — runs in the default install with no HarnessX.
|
||||
- Excludes `shadow`-status findings before computing; assigns 0 penalty to dynamic-only/unmapped CWEs (reports them instead).
|
||||
- The `REACH_MULT` keys are exactly the three values `findings.py` emits; the reachability multiplier is the **FP-calibration knob** (a confirmed FP lowers effective reachability, never deletes a rule).
|
||||
- Emits the scan-level artifact and the manifest score block; computes the hard gate (sev5 + reachable) and the score gate (advisory-first).
|
||||
|
||||
**Contracts**: Batch [x] / State [x]
|
||||
|
||||
##### Service Interface
|
||||
```python
|
||||
SEV_WEIGHT = {5: 50, 4: 25, 3: 10, 2: 2, 1: 1, 0: 0}
|
||||
REACH_MULT = {"reachable": 1.0, "inferred-file-surface": 0.6, "unknown": 0.4}
|
||||
|
||||
def finding_penalty(f, rule_cwe: str, policy) -> float:
|
||||
pol = policy.get(rule_cwe)
|
||||
if pol is None or not pol.static: # dynamic-only / unmapped -> report, don't score
|
||||
return 0.0
|
||||
return SEV_WEIGHT[pol.severity] * REACH_MULT.get(f.reachability_status, 0.4)
|
||||
|
||||
def project_score(findings, rule_cwe_by_id, policy, K: float = 60.0) -> int:
|
||||
# shadow-status findings excluded before this call
|
||||
P = sum(finding_penalty(f, rule_cwe_by_id[f.finding_id], policy) for f in findings)
|
||||
return round(100 * math.exp(-P / K)) # P=0 -> 100; one critical-reachable -> ~43
|
||||
|
||||
def gate(findings, score, *, min_score: int, block_severity_reachable: int = 5,
|
||||
blocking: bool) -> dict: ...
|
||||
```
|
||||
- **Postconditions:** artifact JSON (see Data Models) written next to per-finding output and merged into the manifest.
|
||||
|
||||
### Execution Layer
|
||||
|
||||
#### HxScanOrchestrator
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Run the hunter pool on a real HarnessX agent loop, confining async to one stage |
|
||||
| Requirements | 1.1, 1.4, 1.7, 1.8 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Invoked from the existing `hunter_pool` `run_stage` callback (`cli.py:122`); wraps `asyncio.run(model_config.agentic(harness_config).run(ScanTask(...)))`.
|
||||
- `build_sast()` mirrors `examples/coding/build_coding`; `ModelConfig(main=hunter, judge=…, ranker=…, patcher=…).agentic(cfg)` keeps model info out of `HarnessConfig`.
|
||||
- Each hunter sub-harness registers `LoopDetectionProcessor`, `CostGuardProcessor`/`TokenBudgetProcessor`, `ToolFailureGuard`, `ToolFilterProcessor`, `SelfVerifyProcessor`, `EpisodeMetricsProcessor`/`OTelProcessor`/`CheckpointProcessor`.
|
||||
- Produces a `StatefulTrajectory` per target; rewards backfilled from verifier verdicts. On capability-missing, falls back to `run_hunter_pool()` and appends to manifest `degradations`.
|
||||
|
||||
**Dependencies** — External: HarnessX core + processors (P0, lazy). Inbound: `cli._run_scan` (P0).
|
||||
|
||||
**Contracts**: Service [x] / Event [x]
|
||||
|
||||
##### Event Contract
|
||||
- Hook mapping: `stage_start`→`on_step_start`, `stage_end`→`on_step_end`, `scan_id`→`session_id`/`run_id`; `before/after_model|tool` direct. `HARNESSX_CONTRACT_MODE` (message-window) + `OUS_SLOT_CONTRACT` (key-level via `SlotContractMixin`) split the old single `contract_mode`.
|
||||
|
||||
#### PolicyScoringProcessor
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Resolve policy severity per finding and make the emission decision |
|
||||
| Requirements | 2.2, 2.3, 2.5 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Runs `on_after_tool`/`on_step_end`; reads `sast.rankings` + PolicyStore; writes resolved severity + emit-decision.
|
||||
- Drops `shadow` findings from the reported set and the score input while recording their outcome for precision tracking.
|
||||
- Applies `min_emit_priority`/`min_emit_precision` thresholds — the new volume controls that replace "every regex match becomes a finding".
|
||||
|
||||
**Contracts**: Event [x]
|
||||
|
||||
#### VerifyProcessor
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | LLM-judge verification with a zero-dep structural fallback |
|
||||
| Requirements | 1.2 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Wraps `build_judge_prompt`/`parse_judge_response`/`_call_judge` (`llm_judge.py`) as a `judge`-role sub-harness; maps the strict-JSON verdict (`plausible|unsupported|hedging|…` + `lesson`/`missing`) to `VerificationResult`.
|
||||
- **Fallback:** the existing structural verifier (`verification.py:91`, rejects on `reachability_status == "unknown"`) is used verbatim when the extra is absent or the judge is unavailable.
|
||||
|
||||
**Contracts**: Event [x]
|
||||
|
||||
### Improvement Layer
|
||||
|
||||
#### EvolveValidator (`_rules` / `_policy`)
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Enforce the bounded-lever safety contract on every loop edit |
|
||||
| Requirements | 3.2-3.8 |
|
||||
|
||||
**Responsibilities & Constraints** (the gate — runs in the *validity* phase beside `_contract`/`_replay`)
|
||||
- `_rules`: edit parses; `status ∈ {enabled,shadow,disabled}`; `tighten`/`loosen` adjusts only threshold/evidence-floor **within configured bounds**; `add` doesn't duplicate a `rule_id`; **every `cwe` resolves in PolicyStore**; **pattern text unchanged** (reject otherwise); **no `enabled→disabled` jump** (require `shadow`).
|
||||
- `_policy`: touches only evidence floor, scope, `K`/`MIN_SCORE` within bounds; **never the 0-5 severity**.
|
||||
- On any failure: `raise StrictValidationError(kind="rule_change")` → the whole round is rejected.
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
##### Service Interface
|
||||
```python
|
||||
_VALID_LEVERS = frozenset({
|
||||
"configuration", "control", "action", "instruction",
|
||||
"rule", # status/evidence-floor/threshold in ruleset/ — NEVER pattern text
|
||||
"policy", # evidence floor + scope + K/MIN_SCORE — NEVER the 0-5 CWE severity
|
||||
})
|
||||
|
||||
class EvolveValidator:
|
||||
def _rules(self, edit) -> None: ... # raises StrictValidationError(kind="rule_change")
|
||||
def _policy(self, edit) -> None: ...
|
||||
```
|
||||
|
||||
#### improve/evolve.py (orchestrator)
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Drive benchmark→signal→evolve→re-benchmark→accept/revert under the gate |
|
||||
| Requirements | 4.1-4.8, 6.3 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Converts `rule_policy_delta` + FP learnings → `rule_signals.json` (recall side `signal="miss"`; precision side `signal="verifier_reject"|"fp"`); calls `MetaAgent.evolve()`.
|
||||
- **Acceptance gate (hard):** round accepted **only if** aggregate recall ≥ 90% **AND** FP < 10% **AND** `project_score` did not regress on the held set **AND** no unpredicted regression; otherwise `fill_gating(outcome="reverted")` restores config byte-for-byte.
|
||||
- Precision-dragging rules are **auto-shadowed**, never deleted; coverage gaps nominate a rule via `rule_signals.json` (pattern authoring stays human-PR).
|
||||
- Objective: `maximize project_score s.t. recall/FP gate` — score is the reward, the gate is the feasibility region; never folded together.
|
||||
|
||||
**Contracts**: Batch [x]
|
||||
|
||||
## Data Models
|
||||
|
||||
### Ruleset policy schema (detection only — TOML)
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
rule_id = "python-sql-injection"
|
||||
languages = ["python"]
|
||||
cwe = "CWE-89" # the ONLY policy join key
|
||||
tags = ["injection"]
|
||||
pattern = '(\.execute)\s*\(\s*[^,)]*%' # human-PR-only
|
||||
status = "enabled" # enabled | shadow | disabled (loop-controlled)
|
||||
min_evidence_level = "static_corroboration" # loop may raise within bounds
|
||||
precision_estimate = 0.62 # backfilled from benchmarks
|
||||
version = "3"
|
||||
```
|
||||
|
||||
Status semantics: `enabled` fires/scores/reports; `shadow` fires and records outcomes but is excluded from report and score (mandatory staging state for any loop-proposed change); `disabled` does not fire.
|
||||
|
||||
Loop ledger (`rule_policy.json`) — loop-owned overlay, re-keyed on `rule_id`:
|
||||
```json
|
||||
{ "python-sql-injection": { "status": "shadow", "min_evidence_level": "dynamic_corroboration",
|
||||
"precision_estimate": 0.41, "outcome": "accepted", "round": 7 } }
|
||||
```
|
||||
|
||||
### Project-score schema (artifact + manifest block)
|
||||
|
||||
```json
|
||||
{ "project_score": 73, "max_severity": 5, "penalty_total": 30.0,
|
||||
"by_category": {"Command or Argument Injection": 1, "Cryptographic Issues": 2},
|
||||
"out_of_scope_dynamic_only": 0,
|
||||
"unmapped_cwe": [],
|
||||
"gate": {"min_score": 80, "block_severity_reachable": 5, "blocking": false, "passed": false} }
|
||||
```
|
||||
|
||||
### Consistency & Integrity
|
||||
|
||||
- **Authority direction:** governance data is the source of truth; execution reads it, emits findings/signals; only the loop (via `EvolveValidator`) writes `status`/floors/score-constants back, never the pattern or the 0-5 severity.
|
||||
- **Temporal/versioning:** `PatternRule.version` and `rule_policy.json.round` give per-rule audit; the journal records each lever edit; `fill_gating(reverted)` restores byte-for-byte.
|
||||
- **Serialization (resume-safe):** all scan slots (`sast.targets`, `sast.findings`, `sast.rankings`, verifications) use plain dicts/dataclasses with `_target_` registration so they survive `wake()`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Strategy
|
||||
|
||||
| Error type | Condition | Response |
|
||||
|---|---|---|
|
||||
| **Startup config error** | An `enabled` rule's `cwe` is unmapped in PolicyStore | `assert_rules_resolve` raises; **fail loud**, abort scan before any work (Req 2.7) |
|
||||
| **Malformed ruleset/policy TOML** | `tomllib` parse failure | Fail loud at load; CI catches before merge |
|
||||
| **Loop edit violates bounds** | Pattern edit / severity edit / enabled→disabled / duplicate `rule_id` / unmapped CWE | `EvolveValidator` raises `StrictValidationError(kind="rule_change")`; entire round rejected (Req 3.8) |
|
||||
| **Reverted re-proposal w/o rationale** | Novelty gate hit | `journal.check_novelty` blocks; loop continues with other candidates (Req 3.6) |
|
||||
| **Replay smoke gate failure** | Edited ruleset/policy won't boot | Round rejected before benchmark re-run (Req 3.7) |
|
||||
| **Acceptance-gate failure** | recall<90% OR FP≥10% OR score regressed OR unpredicted regression | `fill_gating(reverted)` restores config byte-for-byte (Req 4.5) |
|
||||
| **HarnessX capability missing at runtime** | Extra absent or sub-harness unavailable | Stage falls back to existing equivalent; record degradation in manifest (Req 1.8) |
|
||||
| **Slot contract violation** | Stage reads/writes an undeclared slot | `SlotContractMixin` raises under `OUS_SLOT_CONTRACT=strict`; warns under `warn` (Req 1.5) |
|
||||
| **Slot serialization loss on resume** | Non-`_target_` slot restores as `None` | Per-slot-type round-trip test guards; `_target_` registration required |
|
||||
|
||||
### Monitoring
|
||||
|
||||
- `EpisodeMetricsProcessor.episode_summary` → manifest metrics block; `OTelProcessor` OTLP export when configured.
|
||||
- `HarnessJournal` writes per-run `{run_id}.jsonl` / `_trace.jsonl` / `_state.json`, rotated on `SegmentBoundaryEvent`.
|
||||
- The score artifact `unmapped_cwe` list surfaces policy-completeness drift; the journal changeset table surfaces every rule/policy lever edit per round.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- `load_policy()` parses the `"Flaw Severity "` trailing-space header and yields 167 `CwePolicy` entries with correct `static`/`dynamic` flags.
|
||||
- `project_score()` boundary cases: P=0 → 100; one severity-5 reachable finding → ~43; one low finding → ~97; shadow findings excluded.
|
||||
- `resolve_severity()` returns policy severity (not the legacy rule string) and is identical for two rules sharing a CWE.
|
||||
- `EvolveValidator._rules` rejects: pattern edit, severity edit, `enabled→disabled` jump, duplicate `rule_id`, unmapped CWE.
|
||||
- `finding_penalty()` returns 0.0 for dynamic-only and unmapped CWEs.
|
||||
|
||||
### Integration Tests
|
||||
- **Byte-identical baseline (Req 6.2):** with the default ruleset and an empty `rule_policy.json`, `benchmark.py` output matches the pre-feature golden corpus exactly.
|
||||
- **Startup invariant (Req 2.7):** an enabled rule with an unmapped CWE aborts the scan with a loud error.
|
||||
- **Self-improvement round (Req 4.4/4.5):** a deliberately noisy rule is auto-shadowed when it drags precision; a round that regresses recall is reverted byte-for-byte.
|
||||
- **Capability fallback (Req 1.8):** extra absent → `quick`/`standard` run through the sync driver unchanged; degradation recorded when a HX stage is requested but unavailable.
|
||||
- **Slot round-trip (Req on resume):** each scan slot type serializes and restores without loss after `wake()`.
|
||||
|
||||
### E2E / Gate Tests
|
||||
- **CI detection gate (Req 6.1):** the pipeline fails the merge if recall<90% or FP≥10% on the corpus.
|
||||
- **Two-condition score gate (Req 5.7/5.8):** hard gate fails on a sev5+reachable finding; score gate is advisory until `K`/`MIN_SCORE` are calibrated, then blocking.
|
||||
- **Zero-dep guard (Req 7.5):** with HarnessX absent, governance/scoring/benchmark planes operate and CI gating runs in the default install.
|
||||
|
||||
### Performance / Tolerance
|
||||
- **HX tolerance (Req 6.4):** an enabled HarnessX-backed stage keeps recall/FP within the configured tolerance of the zero-dep baseline.
|
||||
- Hunter-loop cost/token budgets enforced by `CostGuardProcessor`/`TokenBudgetProcessor`.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Supply-chain posture (Req 7):** core `dependencies = []` is preserved; HarnessX ships only as a SHA-pinned optional extra, lazy-imported behind `_has_harnessx()`. A `pip install --dry-run` audit runs before Phase 3; if the realized transitive graph pulls browser binaries / Docker SDK / a FastAPI web stack as *mandatory* deps and maintainers judge it unacceptable for a supply-chain auditing tool, the pre-specified **vendored lean subset** (Option C) is switched in under the same extra name — a packaging change, not a redesign. Submodule init is **non-recursive** so the large `verl` RL fork is never pulled.
|
||||
- **Loop containment:** the meta-agent can never invent or rewrite detectors (no pattern edits) or rewrite the severity authority (no 0-5 edits); `shadow`-first staging, novelty/evidence/replay gates, and byte-for-byte revert bound its blast radius — directly preventing the "too influential" defect from re-entering via the loop.
|
||||
- **Gate integrity:** the recall/FP gate is a hard constraint that is never folded into the score reward, so the loop cannot "improve" the number by shadowing rules that hurt real security.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
Every phase is independently shippable, test-gated, and reversible. Phases 0-2 deliver governance value at **zero dependency cost** and stay byte-identical by default. Phases 3-4 are the HarnessX adoption behind the extra. Phase 5 is the optional native end-state.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P0["Phase 0<br/>Policy + Score<br/>zero-dep, gate advisory"] --> P1["Phase 1<br/>Rules-as-data<br/>zero-dep, byte-identical"]
|
||||
P1 --> P2["Phase 2<br/>Benchmark->signal + ledger<br/>zero-dep"]
|
||||
P2 --> GATE{"pip --dry-run audit<br/>(D2 decision)"}
|
||||
GATE -->|graph acceptable| P3B["Phase 3<br/>[harnessx] extra (Option B)<br/>hunter loop + verify"]
|
||||
GATE -->|graph unacceptable| P3C["Phase 3<br/>vendored lean subset (Option C)<br/>same extra name"]
|
||||
P3B --> P4["Phase 4<br/>Self-improvement loop live"]
|
||||
P3C --> P4
|
||||
P4 --> P5["Phase 5 (optional)<br/>full single-step harness + RL<br/>retire HarnessRuntime"]
|
||||
```
|
||||
|
||||
| Phase | Scope | Dep cost | Merge gate | Reversibility |
|
||||
|---|---|---|---|---|
|
||||
| **0 — Policy & score** | Vendor `CWE_Score.tsv`; `policy/verycode.py` + `scoring/project_score.py`; CWE-120→121 remap; startup CWE-resolution invariant; emit score artifact + advisory gate. `HarnessRuntime`/`PATTERN_RULES` unchanged. | **zero** | benchmark byte-identical; score is additive output | gate disable flag |
|
||||
| **1 — Rules as data** | Extract `PATTERN_RULES`→`ruleset/<lang>/*.toml`; delete `PatternRule.severity`; `load_ruleset()`; `resolve_severity`; add `status` + emit thresholds. | **zero** | benchmark **byte-identical** to Phase 0 | keep `PATTERN_RULES` fallback |
|
||||
| **2 — Benchmark→signal + ledger** | `rule_id` on misses/FPs; per-rule precision/recall; emit `rule_policy_delta`; `rule_policy.json` ledger; calibration writes status flips re-keyed on `rule_id`. | **zero** | empty ledger == current behavior | empty ledger = no-op |
|
||||
| **3 — HX execution adoption** | Run `pip --dry-run` audit (D2 gate). Add `[harnessx]` extra (pinned SHA, lazy, zero-dep CI guard); `build_sast()` + `HxScanOrchestrator`; hunter pool on real `Harness.run()`; `VerifyProcessor`; `PolicyScoring`/`ScoreProcessor`. `HarnessRuntime` stays. | **opt-in heavy** | extra-absent: unchanged; extra-present: recall/FP within tolerance | feature flag off |
|
||||
| **4 — Self-improvement live** | Vendor/patch the meta-harness seam; orchestrator benchmark→delta→signal→`evolve()`→re-benchmark→accept/revert. | **opt-in** | ≥90% recall / <10% FP per-round; auto-revert | don't run `evolve` → ledger-only changes |
|
||||
| **5 — Full harness + RL (optional)** | Migrate deterministic stages to single-step harness + `SlotContractMixin`; retire `HarnessRuntime`; promote score gate to blocking; optional `rl/`. | **opt-in** | golden-diff vs Phase 3 empty; gate held | per-stage revert |
|
||||
|
||||
**Rollback triggers.** Any phase that drops recall<90% or raises FP≥10% on the corpus blocks the merge; any self-improvement round failing the acceptance gate reverts byte-for-byte; an unacceptable `pip --dry-run` graph switches Phase 3 to the vendored subset.
|
||||
@@ -1,120 +0,0 @@
|
||||
# Requirements Document
|
||||
|
||||
> **Status 2026-10-02:** retired with HarnessX (see the note below). Its task 6.3 modules `stage_processors.py` and `slot_contract.py` were deleted on 2026-10-02: nothing imported them. Kept as decision history; nothing in src, tests or CI points here.
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Introduction
|
||||
|
||||
OpenUltraSAST today fuses three responsibilities into one regex layer: detection patterns also carry their own severity strings and act as the sole volume control, so a noisy rule can dominate scan output, set its own criticality, and fire forever with no central governance. This feature splits those responsibilities into three governed owners — rules detect, a central CWE policy decides severity and scope, and a single project score becomes the optimization target — and adopts the HarnessX runtime exactly where it pays off: the LLM hunter loop, the LLM-judge verifier, and a meta-harness self-improvement loop that adapts rules and policy gates under hard bounds.
|
||||
|
||||
The work spans five user asks. First, OpenUltraSAST shall adopt HarnessX directly at the hunter loop and migrate the existing scan flow stage-by-stage onto HarnessX processors. Second, detection rules shall become versioned data governed centrally, with severity sourced from the verycode CWE policy rather than from the rule itself. Third, the HarnessX meta-harness shall adapt rules and policy gates within bounded levers. Fourth, benchmark results shall drive that adaptation as actionable signals with a hard acceptance gate. Fifth, a Veracode-style project score shall be computed from policy-weighted, reachability-adjusted findings and gated in CI.
|
||||
|
||||
Two cross-cutting constraints govern every part of this feature. The existing detection gate of at least 90% recall and under 10% false-positive rate on the benchmark corpus must never regress, at any phase, for any change. And the HarnessX runtime must be adopted in a way that preserves the project's zero-dependency posture for the governance and scoring planes, so an explicit dependency-weight decision controls whether HarnessX ships as an optional extra or as a vendored lean subset.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: detection rules as versioned data; a central CWE-to-severity policy with static/dynamic scope; a single project score (0-100) with a two-condition CI gate; HarnessX adoption at the hunter loop, the verifier, and the self-improvement meta-harness; benchmark-to-signal conversion with per-rule attribution; bounded loop authority over rule status, evidence floors, thresholds, and score constants; a dependency-weight audit and decision; preservation of the recall/FP detection gate.
|
||||
- **Out of scope**: authoring new detection pattern text by the loop (human pull-request only); editing the authoritative 0-5 CWE severity by the loop (verycode-upstream authority); a full single-step HarnessX rewrite of all deterministic stages and weight-level reinforcement learning (deferred, optional, later phase); changing the reachability vocabulary, which remains exactly `reachable | inferred-file-surface | unknown`; dynamic/DAST scoring, which is report-only for a static tool.
|
||||
- **Adjacent expectations**: the governance and scoring planes shall run with no HarnessX present and `dependencies = []`; CI gating (score plus benchmark) shall run in the zero-dependency default install; HarnessX, when present, shall be confined behind a capability check and lazy imports so heavy code paths stay cold in the default install.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: HarnessX Direct Adoption and Scan-Flow Migration
|
||||
|
||||
**Objective:** As an OpenUltraSAST maintainer, I want the scan flow to adopt the HarnessX runtime directly at the LLM hunter loop and migrate the existing stages onto HarnessX processors in a staged way, so that the project gains the HarnessX agent loop, journal, and trajectory machinery without a high-risk big-bang rewrite.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. Where the HarnessX extra is installed, the Scan Orchestrator shall run the hunter pool stage on a real HarnessX `Harness.run()` loop with the HarnessX control, tool, and observability processors registered in the hunter sub-harness.
|
||||
2. Where the HarnessX extra is installed, the Verify Processor shall produce verification verdicts using the HarnessX llm-judge helpers and shall retain the structural verifier as a zero-dependency fallback.
|
||||
3. While the HarnessX extra is absent, the Scan Runtime shall execute `quick` and `standard` scans through the existing sync driver with behavior unchanged from the pre-adoption baseline.
|
||||
4. When the hunter pool stage runs under HarnessX, the Scan Orchestrator shall confine all asynchronous execution to that single stage so that the deterministic stages remain synchronous.
|
||||
5. The Scan Runtime shall preserve each stage's declared read and write slot allow-list and shall validate slot access on every lifecycle hook, so that the existing provenance and audit discipline is retained under the HarnessX runtime.
|
||||
6. Where a deterministic stage runs under HarnessX, the Scan Runtime shall execute it with model invocation disabled so that no deterministic stage incurs a model call.
|
||||
7. When migrating event names, the Scan Runtime shall map the existing `stage_start`/`stage_end` to the HarnessX step-level hooks and `scan_id` to the HarnessX run identifier without losing trace continuity.
|
||||
8. If a HarnessX-backed stage is enabled and the corresponding HarnessX capability is unavailable at runtime, then the Scan Runtime shall fall back to the existing equivalent stage and shall record the degradation in the scan manifest.
|
||||
|
||||
### Requirement 2: Central Security-Affine Ruleset Governance
|
||||
|
||||
**Objective:** As a security policy owner, I want detection rules to be versioned data whose severity and scope come from a central CWE policy rather than from the rule itself, so that one CWE maps to one governed severity and no rule can be too influential by setting its own criticality or volume.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Ruleset Store shall load detection rules from versioned data files in which each rule declares a pattern, a CWE, tags, and a loop-controlled `status`, and shall not carry any rule-local severity field.
|
||||
2. When resolving the severity of a finding, the Policy Scoring Processor shall read severity exclusively from the Policy Store keyed on the rule's CWE and shall discard any severity string carried in legacy rule data.
|
||||
3. While two enabled rules map to the same CWE, the Policy Scoring Processor shall assign both findings the identical policy-governed severity so that two rules for one CWE can no longer disagree.
|
||||
4. The Ruleset Store shall support exactly three rule statuses — `enabled`, `shadow`, and `disabled` — where `enabled` fires and scores and reports, `shadow` fires and records outcomes but is excluded from the report and the score, and `disabled` does not fire.
|
||||
5. When a finding's rule status is `shadow`, the Policy Scoring Processor shall exclude that finding from the reported findings and from the project-score input while still recording its outcome for precision tracking.
|
||||
6. The Policy Store shall expose, per CWE, an authoritative 0-5 severity plus a static scope flag and a dynamic scope flag, and shall mark dynamic-only CWEs as report-only and never scored by the static tool.
|
||||
7. If an enabled rule's CWE does not resolve in the Policy Store at startup, then the Scan Runtime shall fail loud and shall not proceed with the scan.
|
||||
8. The Ruleset Store shall remap the C/C++ buffer-handling rules that reference the unmapped CWE-120 to CWE-121 (severity 5) so that those rules contribute a nonzero penalty to the project score.
|
||||
9. The Ruleset Store and the Policy Store shall be loaded with the standard library only and shall require no third-party dependency.
|
||||
|
||||
### Requirement 3: Bounded Rule and Policy Adaptation in the Self-Improvement Loop
|
||||
|
||||
**Objective:** As an OpenUltraSAST maintainer, I want the HarnessX meta-harness to adapt rules and policy gates only through two bounded levers, so that the loop can suppress, stage, or raise the bar on noisy rules without ever inventing detectors or rewriting the severity authority.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Self-Improvement Loop shall expose exactly two new edit levers: a `rule` lever covering rule status, evidence floor, and threshold, and a `policy` lever covering evidence floor, scope, and the score constants `K` and `MIN_SCORE`.
|
||||
2. The Rule Edit Validator shall reject any loop edit that modifies detection pattern text, so that pattern authoring remains human-pull-request only.
|
||||
3. The Policy Edit Validator shall reject any loop edit that modifies the authoritative 0-5 CWE severity, so that the severity authority remains upstream-governed.
|
||||
4. If the loop proposes moving a rule directly from `enabled` to `disabled`, then the Rule Edit Validator shall reject the edit and require `shadow` as a mandatory intermediate staging state.
|
||||
5. When validating a proposed edit, the Rule Edit Validator shall assert that the edit parses, that `status` is one of `enabled`/`shadow`/`disabled`, that any threshold or evidence-floor tightening or loosening stays within configured bounds, that an added rule does not duplicate an existing rule identifier, and that every referenced CWE resolves in the Policy Store.
|
||||
6. If the loop re-proposes an edit that was previously reverted without supplying a retry rationale, then the Self-Improvement Loop shall block the re-proposal via the novelty gate.
|
||||
7. Before accepting any round, the Self-Improvement Loop shall run a replay smoke gate that proves the edited ruleset and policy still load and boot.
|
||||
8. When any loop edit is rejected during validation, the Self-Improvement Loop shall raise a rule-change validation error and reject the entire round.
|
||||
9. The Self-Improvement Loop shall record each rule and policy edit, its lever, and its outcome in the journal so that every change is attributable and reversible.
|
||||
|
||||
### Requirement 4: Benchmark-Driven Iteration with a Hard Acceptance Gate
|
||||
|
||||
**Objective:** As an OpenUltraSAST maintainer, I want benchmark results to become actionable per-rule signals that drive the self-improvement loop under a hard acceptance gate, so that detection quality improves automatically while regressions are reverted byte-for-byte.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Benchmark Harness shall attribute every recorded miss and every recorded false positive to a `rule_id` and shall aggregate per-rule precision and recall in its metrics.
|
||||
2. The Benchmark Harness shall emit a per-rule recommendation artifact alongside its records, recommending enable, disable, shadow, threshold, or evidence-floor changes per rule.
|
||||
3. When the orchestrator converts benchmark output to loop input, it shall write recall-side items as miss signals and precision-side items as verifier-reject or false-positive signals into the loop's signal file.
|
||||
4. After the meta-agent edits rules or policy, the Self-Improvement Loop shall re-run the benchmark and shall accept the round only if aggregate recall is at least 90% AND aggregate false-positive rate is under 10% AND the project score did not regress on the held set AND no unpredicted regression occurred.
|
||||
5. If a re-run round fails any acceptance condition, then the Self-Improvement Loop shall revert the round and restore the rule and policy configuration byte-for-byte.
|
||||
6. If a rule drags aggregate precision below the gate, then the Self-Improvement Loop shall auto-shadow that rule rather than delete it, so that recall is preserved while the false-positive cost is removed.
|
||||
7. When a coverage gap surfaces as a persistent miss, the Self-Improvement Loop shall nominate a new or loosened rule through the signal file while leaving pattern authoring to a human pull request.
|
||||
8. The Self-Improvement Loop shall treat maximizing the project score as the optimization objective and the recall/FP gate as a separate hard feasibility constraint, so that the loop cannot improve the score by shadowing rules in a way that harms real security.
|
||||
|
||||
### Requirement 5: Project Scores via verycode Policies
|
||||
|
||||
**Objective:** As a security reviewer, I want a single 0-100 project score computed from policy-weighted, reachability-adjusted findings and gated in CI, so that a scan produces a bounded AppSec quality signal alongside its findings.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Score Model shall compute a single 0-100 project score from policy-governed severity weights multiplied by a reachability multiplier over the reported findings, using the standard library only.
|
||||
2. The Score Model shall use reachability multipliers keyed exactly on the three emitted reachability values `reachable`, `inferred-file-surface`, and `unknown`.
|
||||
3. When a finding maps to a CWE that is dynamic-only or unmapped in the Policy Store, the Score Model shall assign that finding a zero score penalty and shall report it as out-of-scope or unmapped rather than scoring it.
|
||||
4. The Score Model shall exclude findings whose rule status is `shadow` from the score input before computing the project score.
|
||||
5. The Score Model shall emit a scan-level score artifact containing the project score, the maximum severity, the total penalty, a by-category breakdown, the out-of-scope dynamic-only count, the unmapped CWE list, and the gate verdict, and shall include the score block in the scan manifest.
|
||||
6. When confirming a false positive, the Calibration subsystem shall lower the finding's effective reachability multiplier rather than delete the rule, so that recall stays high while the score stops over-penalizing.
|
||||
7. If any reported finding maps to policy severity 5 AND has `reachability_status` equal to `reachable`, then the CI Score Gate shall fail the build.
|
||||
8. Where the score gate is configured as blocking, the CI Score Gate shall fail the build when the project score is below `MIN_SCORE`; until the score constants are calibrated against the corpus, the CI Score Gate shall compute and report the score advisory-first without blocking.
|
||||
9. The Score Model and the CI Score Gate shall run in the zero-dependency default install with no HarnessX present.
|
||||
|
||||
### Requirement 6: Detection Gate Non-Regression
|
||||
|
||||
**Objective:** As an OpenUltraSAST maintainer, I want the existing detection gate of at least 90% recall and under 10% false positives on the benchmark corpus to hold across every phase and every change, so that no governance, scoring, runtime, or self-improvement change can silently degrade detection quality.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The CI Pipeline shall evaluate aggregate recall and aggregate false-positive rate on the benchmark corpus on every merge and shall fail the merge if recall is below 90% or the false-positive rate is at or above 10%.
|
||||
2. While the default ruleset and an empty loop ledger are in effect, the Benchmark Harness shall produce results byte-identical to the pre-feature baseline for the governance and rules-as-data phases.
|
||||
3. When the self-improvement loop proposes any rule or policy change, the Self-Improvement Loop shall require the recall/FP gate to pass on the re-benchmarked candidate before the change can be accepted.
|
||||
4. If a HarnessX-backed stage is enabled, then the CI Pipeline shall confirm recall and false-positive rate stay within the configured tolerance of the equivalent zero-dependency baseline.
|
||||
5. The CI Pipeline shall treat the recall/FP gate as a hard constraint that is never folded into the project-score reward, so that improving the score can never substitute for meeting the detection gate.
|
||||
6. If any change would reduce recall below 90% or raise the false-positive rate to 10% or above, then the CI Pipeline shall block the change regardless of any project-score improvement.
|
||||
|
||||
### Requirement 7: HarnessX Dependency-Weight Decision
|
||||
|
||||
**Objective:** As an OpenUltraSAST maintainer, I want an explicit dependency-weight decision that keeps the core install zero-dependency and chooses between an optional HarnessX extra and a vendored lean subset based on an audited dependency graph, so that a supply-chain auditing tool never silently inherits an unacceptable transitive dependency footprint.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Packaging Configuration shall keep the core `dependencies` list empty and shall expose HarnessX only as an optional extra pinned to a specific commit revision.
|
||||
2. The Runtime shall guard every HarnessX import behind a capability check and shall import HarnessX lazily so that heavy code paths stay cold in the default install.
|
||||
3. Before adopting the HarnessX execution stages, the maintainer shall run a dependency dry-run audit and shall record the realized transitive dependency graph.
|
||||
4. If the audited transitive graph pulls a browser-binary, container-SDK, or web-server stack as a mandatory dependency and the maintainers judge it unacceptable for a supply-chain auditing tool, then the Packaging Configuration shall switch to a vendored lean subset of HarnessX under the same extra name with the heavy subtrees removed.
|
||||
5. While HarnessX is absent, the CI Pipeline shall run a zero-dependency guard test confirming the governance, scoring, and benchmark planes operate with no HarnessX present.
|
||||
6. Where the project is developed via a submodule, the Runtime setup shall initialize HarnessX non-recursively so that the large reinforcement-learning fork subtree is not pulled.
|
||||
7. The Packaging Configuration shall keep the vendored lean subset pre-specified so that switching from the optional extra to the vendored subset is a packaging change and not a redesign.
|
||||
@@ -1,191 +0,0 @@
|
||||
# Research & Design Decisions
|
||||
|
||||
---
|
||||
**Purpose**: Capture the discovery findings and architectural investigation behind the `harnessx-self-improving-rulesets` spec. This is the background-notes companion to `design.md` — it records *what was read*, *what was found in the source*, and *why* the three resolved decisions came out the way they did. `design.md` carries the committed architecture; this file carries the evidence and the rejected alternatives.
|
||||
---
|
||||
|
||||
## Summary
|
||||
- **Feature**: `harnessx-self-improving-rulesets`
|
||||
- **Discovery Scope**: Complex Integration (adopt an external agent-runtime library + re-architect the in-house rule/policy/scoring planes)
|
||||
- **Key Findings**:
|
||||
- HarnessX and OpenUltraSAST (OUS) already both have a thing called a "harness" with "processors" and "events", but they are **incompatible at every load-bearing point**: HarnessX is async-generator / message-window-contract / transcript-state; OUS is sync / declared-`reads`/`writes`-contract / dict-state. There is no drop-in.
|
||||
- HarnessX's real, reusable value for OUS is concentrated in three places: the **LLM hunter agent loop** (control/tools/observability processors), the **llm-judge verifier** (`build_judge_prompt`/`parse_judge_response`/`_call_judge`), and the **meta-harness self-improvement engine** (journal / novelty / evidence / replay / attribution). Everything else (lab web UI, browser tools, sandboxes, office skills) is unused weight.
|
||||
- OUS today fuses **detector + severity authority + volume control** into one regex layer (`PATTERN_RULES` in `findings.py`); its self-improvement loop only demotes *ranking* (floored at `0.1`, keyed on `tags[0]`), never the rules; and its benchmark already names the ruleset as the thing to fix (`next_improvement_candidate="rules_or_language_hunter"`) but writes it to an inert JSON. The signals exist; the loop back onto the rules does not.
|
||||
- The `verycode` policy data (`CWE_Score.tsv`, 167 CWEs, severity 0-5 + static/dynamic scope) is **complete and authoritative**, but the `verycode-score` scoring *algorithm was never implemented* (`// scorer code here` placeholder). The score formula in the design is a labeled, defensible reconstruction.
|
||||
- OUS today has **zero runtime dependencies** (`dependencies = []`); HarnessX's `dependencies` list is ~30 heavyweight packages (Playwright, Docker SDK, FastAPI/uvicorn, multi-LLM SDKs, office-doc suite) with **no lean extra** to pip-install a subset — a direct hard dependency would invert the project's supply-chain posture for a security tool.
|
||||
|
||||
## Research Log
|
||||
|
||||
### HarnessX core composition & runtime model
|
||||
- **Context**: We need to know whether OUS's `harness.HarnessRuntime` can be replaced by, or interoperate with, HarnessX's `Harness`, and how an external project builds and runs a HarnessX harness.
|
||||
- **Sources Consulted**: `/tmp/HarnessX/harnessx/core/{harness,builder,processor,events,runloop,state,contract_check,config_schema,config_store,model_config,trajectory}.py` (read in full); `/home/mc/Source/OpenUltraSAST/src/openultrasast/harness.py`.
|
||||
- **Findings**:
|
||||
- Public composition is three objects + one method: `HarnessBuilder.build() -> HarnessConfig`; `ModelConfig.agentic(HarnessConfig) -> Harness`; `await Harness.run(task) -> HarnessResult`. **`HarnessConfig` carries NO model info** — model is always supplied separately via `ModelConfig` (key→provider map, `main` required, non-`main` keys auto-get sub-harnesses).
|
||||
- `HarnessBuilder` is an immutable fluent factory: `add(proc)`, `slot(**kw)`, `add_tool`, `plugin(source)`, `__or__`/`merge` (collects conflicts → `HarnessConflictError`), `build()` (topo-sorts processors per hook by `_order` then Kahn within a bucket by `_after`). Declarative alt: `build_from_config(dict)` from a flat `processors:` list of `_target_` dicts.
|
||||
- The run loop (`run_loop`) is a fixed async agent loop: `task_start` → `while True:` (step: `step_start` → `before_model` → call provider → `after_model` → per-tool `before_tool`/exec/`after_tool` → `step_end`) → `task_end`. `get_procs(key)` runs `"*"` (MultiHook) processors on every hook, then hook-specific lists.
|
||||
- State is **two message tracks** (`raw_messages` append-only, `messages` effective context) + typed `slots: dict[str, StateSlot]` (the nearest analogue to OUS's flat `state` dict, but typed and `_target_`-serializable). `StatefulTrajectory` is a first-class product (`s_t = (z_t, Δz_t, a_t, o_t, e_t, r_t)`), built inline during `run_loop`, with `backfill_rewards` and `to_training_records`/`to_rl_records`.
|
||||
- Hook-name alignment vs OUS: 5/8 match exactly (`task_start`, `before_model`, `after_model`, `before_tool`, `after_tool`, `task_end`); the gap is `stage_start`/`stage_end` ↔ `step_start`/`step_end`. Semantically an OUS "stage" is a coarse pipeline phase wrapping a callback (`run_stage(name, callback)`), whereas a HarnessX "step" is one model-call iteration — not the same concept.
|
||||
- **Implications**: A whole-pipeline swap is high risk. The cheapest correct adoption confines HarnessX's async loop to the one OUS stage that *is* an agent loop (the hunter pool) and keeps the sync driver for deterministic stages. This directly motivates Decision D1 (phased seam, not big-bang).
|
||||
|
||||
### The two incompatible processor contracts
|
||||
- **Context**: OUS's defining feature is a declared `reads`/`writes` per-key state contract. We must know if HarnessX preserves it.
|
||||
- **Sources Consulted**: `/tmp/HarnessX/harnessx/core/{processor,contract_check}.py`; `/home/mc/Source/OpenUltraSAST/src/openultrasast/harness.py:49-118`.
|
||||
- **Findings**:
|
||||
- HarnessX `Processor` is `async def process(event) -> AsyncIterator[Event]` — an async generator that yields transformed events. The recommended base is `MultiHookProcessor` with `on_*` hook methods, an `@on(EventClass)` decorator, and class-attr ordering hints (`_order`, `_after`, `_singleton_group`, `_hook`). There is **no `spec`, no `handles`, no `reads`/`writes`.**
|
||||
- HarnessX's contract governs **message-window mutation**, not state keys: enforced in `ProcessorChain.process` via `_validate_messages_contract` (system stays at index 0 on `step_start`; `before_model` net length ∈ {0,+1}; appended message must be role `user`; no out-of-window edits) and per-chain `check_post_hook_invariants`. Mode is the env var `HARNESSX_CONTRACT_MODE ∈ {warn(default), strict}` — not a constructor arg.
|
||||
- OUS `Processor` is **synchronous**, `def handle(event, state: dict) -> dict | None`, with `spec: ProcessorSpec(name, version, handles, reads, writes)`; `_run_processor` enforces *state-key* discipline (reads must pre-exist; writes must be declared; return must be a dict). `ContractMode` is a `"strict"|"warn"` constructor arg.
|
||||
- **Implications**: The two contracts do not map onto each other. OUS's per-key allow-list (its provenance/audit discipline) has **no home in HarnessX**. To keep that discipline we must reimplement it as a `SlotContractMixin` on our HarnessX processors (`reads_slots`/`writes_slots` class attrs validated in a base `on_*` wrapper), gated by our own `OUS_SLOT_CONTRACT` env. This is Decision D7.
|
||||
|
||||
### HarnessX built-in processors — reuse map
|
||||
- **Context**: Which HarnessX processors can OUS register directly, which are reusable as patterns/helpers, and which are irrelevant?
|
||||
- **Sources Consulted**: `/tmp/HarnessX/harnessx/processors/` (control, tools, multi_model, observability, evaluation, memory, context); `/home/mc/Source/OpenUltraSAST/src/openultrasast/cli.py` (`_run_scan`).
|
||||
- **Findings**:
|
||||
- **Register-direct only inside the hunter loop** (the one real agent loop): `LoopDetectionProcessor`, `CostGuardProcessor`/`TokenBudgetProcessor`, `ToolFailureGuard`, `SelfVerifyProcessor`, `ToolFilterProcessor`/`ToolWhitelistProcessor`, `ModelRouterProcessor`, `EpisodeMetricsProcessor`/`OTelProcessor`/`CheckpointProcessor`.
|
||||
- **Reuse as helper functions** for `verify`: the `llm_judge` code (`build_judge_prompt`, `parse_judge_response` with the verdict whitelist, the two-attempt repair loop `_call_judge`) plus the strict-JSON verdict schema (`verdict/confidence/cause/missing_capability`/`lesson`/`missing`). The judge is explicitly told "ground truth is intentionally withheld" — matching OUS's no-oracle scanning.
|
||||
- **Reuse as pattern** for `calibrate`/`record_calibration`: `MemoryRetrievalProcessor` + `MemoryPolicy` (retrieve/add, should_retrieve/should_store) is the clean interface to refactor the FP ledger toward; `MemoryExtractionProcessor` mirrors distilling verifier outcomes into durable learnings. Storage stays a custom JSON ledger.
|
||||
- **Keep custom** for every SAST-domain stage: `static_mapping`, `preprocess`, `entry_point_mapping`, `quick_findings`, `report`, `sarif`, `manifest`, and the benchmark recall/FP scoring (HarnessX `EvalResult.score` is a single float — too coarse for the recall/FP gate).
|
||||
- **Implications**: Adoption is bimodal — register HarnessX classes directly only in the hunter sub-harness; everywhere else port logic as helpers or patterns. This is the backbone of the §3 flow-migration table in `design.md`.
|
||||
|
||||
### HarnessX self-improvement (meta_harness) and the rule-adaptation seam
|
||||
- **Context**: Can an external signal (a verifier rejection or a benchmark miss) drive a *detection-rule* change through HarnessX's self-improvement loop, and how does it compare to OUS's current calibration?
|
||||
- **Sources Consulted**: `/tmp/HarnessX/harnessx/meta_harness/{agent,journal,replay,validate_workflow}.py`, `processors/contract_autocheck.py`, `workers/trajectory_digester.py`, `rl/{builder,config,task}.py`; `/home/mc/Source/OpenUltraSAST/src/openultrasast/{calibration,benchmark,findings,rank,cli}.py`.
|
||||
- **Findings**:
|
||||
- HarnessX self-improves a *harness* round by round. The unit of change is a `HarnessConfig` (the meta-agent literally rewrites the inner agent's config/processors/templates as files); the unit of memory is a markdown **journal** with per-`## Round N` YAML frontmatter; the unit of safety is a multi-phase **validation gate**.
|
||||
- The journal classifies every change by a **lever** from `_VALID_LEVERS = {configuration, control, action, instruction}` and tracks `predicted_affected` task_ids plus a `gating_attribution` (`flipped/still_F/regressed/still_T/absent`). `build_context` renders a per-lever **time-decayed Beta posterior** scoreboard (`_DECAY=0.9`) and an explicit "Reverted hypotheses — do not re-propose" list.
|
||||
- `EvolveValidator.run` enforces validity (canonicalize → contract → diff → replay smoke gate), policy (novelty + evidence, only when the diff is non-empty), advisory. `check_novelty` hard-blocks re-proposing a reverted hypothesis or reverted `(levers, predicted_affected)` signature unless a `retry_rationale` is present. `_evidence` requires a `candidates.md` `## Candidate C-N` cross-referenced by the journal. `run_synthetic_task_smoke_gate` proves the evolved config boots end-to-end. `ContractAutoCheckProcessor` gives in-session `[AUTO-CHECK]` feedback on every authored `processors/*.py`/`tools/*.py` edit.
|
||||
- **The concrete seam to drive a rule change**: add a lever value to `journal._VALID_LEVERS`; carry the external signal in via a `rule_signals.json` read-context artifact + `_render_task_brief` deliverable; add a validity-phase check (`EvolveValidator._rules`) that raises `StrictValidationError(kind="rule_change")`; add changeset buckets (`compute_changeset` + `journal._CHANGESET_KEYS_ORDER`); extend `contract_autocheck` to classify rule files. The novelty/evidence/replay/attribution machinery then applies for free.
|
||||
- **OUS by comparison**: detection rules in `findings.py` are static and never modified. Calibration (`calibration.py`) only does `calibrated = max(0.1, calibrated - 0.5)` on a *file's* ranking priority (keyed on `tags[0]`, not `rule_id`) plus a prompt-constraint string — it never disables/tightens/adds a rule, has no novelty gate, no attribution. Benchmark misses produce a `BenchmarkCalibrationRecord` whose `next_improvement_candidate` is hard-coded to `"rules_or_language_hunter"` and written to an **inert** `calibration_records.json` that nothing consumes. The recall side is a fully open loop.
|
||||
- **Implications**: OUS has exactly the *signals* HarnessX needs (verifier rejections = precision side; benchmark misses = recall side) but no loop closing back onto the rules. The meta_harness is the engine that closes it. This motivates Decision D3 (which levers, and their hard bounds) and the §5/§6 design.
|
||||
|
||||
### The verycode policy & scoring model
|
||||
- **Context**: The design needs a central CWE→severity authority and a project score. What does `verycode` actually provide?
|
||||
- **Sources Consulted**: `/tmp/verycode-policies/{README.md,CWE_Score.tsv}`; `gh repo view norandom/verycode-score` (incl. `action.yaml`, `main.go`); `gh repo view norandom/verycode-lib`; `/home/mc/Source/OpenUltraSAST/src/openultrasast/findings.py`.
|
||||
- **Findings**:
|
||||
- `CWE_Score.tsv` is the real artifact: 167 data rows, one per CWE, columns `Flaw Category | CWE ID | CWE Name | Flaw Severity (0-5) | Static (X/empty) | Dynamic (X/empty)`. Severity distribution: 12×5, 15×4, 97×3, 31×2, 0×1, 12×0. Scope: 154 Static, 45 Dynamic, 13 Dynamic-only, none neither. (Header column 4 is literally `"Flaw Severity "` with a trailing space.)
|
||||
- **The scoring algorithm does not exist.** `norandom/verycode-score` is the unmodified `the-gophers/go-action` "Tweeter" template with `// scorer code here` and a `--policy policies/standard.json` flag; `norandom/verycode-lib` is a one-line stub README. The data shape + the "GitHub Advanced Security findings, weighted by CWE severity" descriptions imply a Veracode-style 0-100 pipeline; the design reconstructs the formula and labels it as such.
|
||||
- **Real coverage gap**: CWE-120 (Classic Buffer Overflow) is **absent from the TSV**, but 3 high-value C/C++ rules use it (`c-unsafe-copy`, `c-unsafe-memory-copy`, `c-unsafe-scanf`). Unmapped → those rules would contribute 0 to the score. CWE-121 (Stack Buffer Overflow) is present at severity 5 and is the precise classic-overflow CWE.
|
||||
- **Severity mismatch**: OUS rule strings disagree with policy (rules label CWE-78/95/134 "high"; policy rates them 5/critical; rules label CWE-94/502 "high"; policy rates them 3/medium). The policy is the authoritative source.
|
||||
- The OUS reachability vocabulary is exactly `reachable | inferred-file-surface | unknown` (verified in `findings.py`), which maps cleanly onto a `REACH_MULT = {reachable:1.0, inferred-file-surface:0.6, unknown:0.4}` exploitability multiplier — and that multiplier is the natural FP-calibration knob (lower effective reachability instead of deleting a rule).
|
||||
- **Implications**: Severity must flow from PolicyStore (`CWE_Score.tsv`), not from the rule. Day-one fixes: remap the 3 CWE-120 rules to CWE-121 (Decision D5); discard rule string-severities (Decision D4). The score is a stdlib-only reconstruction (Decision D8 ships it advisory-first until `K`/`MIN_SCORE` are calibrated).
|
||||
|
||||
### Installability & dependency weight
|
||||
- **Context**: How does an external project depend on HarnessX, and what does it cost a zero-dependency security tool?
|
||||
- **Sources Consulted**: `/tmp/HarnessX/pyproject.toml`, `harnessx/__init__.py`, `core/builder.py`, `plugins/{base,loader,discovery,convert}.py`, `examples/{coding,research,assistant}/harness.py`, `extensions/plugins/workflow/plugin.py`, `.gitmodules`; `/home/mc/Source/OpenUltraSAST/pyproject.toml`.
|
||||
- **Findings**:
|
||||
- HarnessX is **library-first** and **effectively not on PyPI** (`name="harnessx"`, `version="0.1.0"`, Beta; consumed from Git). `requires-python = ">=3.11"` — exact match with OUS, no floor conflict. Depend via a SHA-pinned Git URL (the version string won't move).
|
||||
- Custom stages map cleanly onto HarnessX's plugin model: each SAST stage is a `MultiHookProcessor`; group stages + tools + skills into a `HarnessPlugin` mounted via `builder.plugin(...)`; load in-process (instance/class) rather than via directory/`plugin.json`.
|
||||
- **Dependency-weight is the headline risk.** HarnessX's `dependencies` (always-installed, not extras) is ~30 heavyweight packages: `anthropic`/`openai`/`litellm`/`tiktoken`, `fastapi`/`uvicorn`/`websockets`, `playwright` (ships browser binaries), `docker`/`e2b`, `hydra-core`/`omegaconf`/`structlog`/`loguru`, `opentelemetry-*`, `mcp`, `python-pptx`/`python-docx`/`openpyxl`/`reportlab`/`pdfplumber`. **There is no lean extra** — `pip install harnessx` pulls everything. OUS currently has `dependencies = []`.
|
||||
- **Mitigating fact**: HarnessX uses **pervasive lazy imports** (heavy modules imported inside functions). So the heavy deps are *installed* but only *imported on demand* — which makes both an optional-extra opt-in and a vendored subset genuinely viable.
|
||||
- **Submodule caveat**: `.gitmodules` declares a nested `recipe/verl_harnessX/verl` → a large verl RL fork OUS does not need; never `--recursive` init.
|
||||
- **Implications**: A hard dependency is off the table (it inverts the zero-dep stance and pins to an unmovable Beta). Options narrow to (B) optional extra and (C) vendored lean subset. The lazy-import architecture is what makes B keep heavy code cold by default and makes C a surgical packaging switch. This is Decision D2.
|
||||
|
||||
### Current OUS rule-system problem analysis
|
||||
- **Context**: Establish, in code terms, why the user's complaint ("rules are too influential and not centrally managed") is true.
|
||||
- **Sources Consulted**: `/home/mc/Source/OpenUltraSAST/src/openultrasast/{findings,rank,calibration,benchmark,verification,harness,config,cli}.py`.
|
||||
- **Findings** (six code-level defects):
|
||||
1. **Pattern matches dominate output.** In quick mode, every non-comment regex match becomes a finding (`quick_scan_findings`); the only gate is language. Ranking only sorts; verification rejects only on `reachability_status == "unknown"`. A noisy regex sets output volume and FP rate directly — the rule *is* the result.
|
||||
2. **Severity is rule-local.** `PatternRule.severity` is a string literal copied straight onto the finding (`_finding_from_match`). Two rules for the same CWE can disagree; nothing reconciles them.
|
||||
3. **No central store.** The ruleset is a Python tuple compiled into the package; `config.py` has no rule/policy section. You cannot enable/disable/tune a rule without editing source and cutting a release.
|
||||
4. **Calibration demotes ranking, not rules.** `calibrate_ranking` only subtracts `learning.demotion` (default 0.5) from a *file's* `priority`, floored at `MIN_CALIBRATED_PRIORITY = 0.1`, keyed on `tags[0]`. A chronically wrong rule fires at full severity forever; learning targets the wrong object.
|
||||
5. **No CWE-policy linkage.** `cwe` is free-text used only for rationale text and loose benchmark matching. No table maps CWE → governed severity/enablement/evidence floor.
|
||||
6. **No project-score weighting.** Severity and emission are global constants; the ranking signal (`composite_priority`) only re-sorts. A low-severity crypto finding in a fuzzable network parser is never promoted; a high-severity finding in dead code is never demoted.
|
||||
- **Implications**: The regex layer is simultaneously **detector, severity authority, and volume knob**, while ranking/verification/calibration/benchmark can only act *around* the rules, never *on* them. The fix is to split those three roles into three owners (rules detect / policy decides severity & scope / score is the optimization target) with a one-directional authority model (governance drives execution; execution emits signals; the loop edits governance under a gate). This is the organizing principle of `design.md`.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
These are the three "architect angles" that were considered for the overall shape. Each is internally coherent; the synthesis in `design.md` borrows from all three and resolves their three disagreements (D1/D2/D3).
|
||||
|
||||
| Option | Description | Strengths | Risks / Limitations | Notes |
|
||||
|--------|-------------|-----------|---------------------|-------|
|
||||
| **Incremental seam** | Keep the OUS sync driver; run HarnessX only inside the `hunter_pool` callback via `asyncio.run`. Rules→data + policy + score added around the existing pipeline. Dependency = optional extra (B) with vendored (C) as escape hatch. Loop edits a `"rule"` lever (status/threshold). | Smallest blast radius; confines the async surface to one stage; keeps the recall/FP gate green per-merge; preserves zero-dep default. | Two harness contracts coexist for a while; `HarnessRuntime` lingers; deterministic stages don't gain HarnessX trajectories yet. | Chosen as the base trajectory (Phases 0-4). |
|
||||
| **Native rewrite** | Delete `HarnessRuntime` entirely; the *whole* scan becomes a single-step HarnessX harness with `skip_model=True` + slot dataflow; vendored lean subset (C) as the committed default. Loop edits a `"rule"` lever; patterns stay human-PR'd. | Cleanest end-state; one runtime, one contract; full trajectory/journal coverage of every stage; smallest auditable dep graph. | Touches every stage at once — too much blast radius to keep the gate green per-merge; vendoring is permanent fork-maintenance burden. | Demoted to optional Phase 5+; its end-state (single-step harness, retire `HarnessRuntime`) is the long-term target. |
|
||||
| **Policy-first** | The whole scan runs as processors on a fixed loop; central CWE policy is the centerpiece; loop edits `"rule"` + `"policy"` levers (policy = evidence floors/scope, *not* the 0-5 CWE severity). Dependency = optional extra (B) default, vendored (C) fallback. | Strongest governance story; two levers give the loop the right surface to "stop rules being too influential" without inventing detectors or rewriting severity. | Same whole-pipeline-at-once risk as native if taken literally; needs careful bounds so the loop can't corrupt the ruleset. | Contributed the two-lever model (D3) and the bounded-authority safety rules. |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
The three architect angles agreed on the destination (rules→governed data; severity from a central CWE policy; a gated project score; HarnessX hosting the hunter loop and a rule-adapting self-improvement loop). They disagreed on three load-bearing choices. Each is resolved below with the recall/FP gate as the deciding tie-breaker. (D4-D9 are smaller decisions that follow.)
|
||||
|
||||
### Decision D1: Scope of the HarnessX migration
|
||||
- **Context**: How much of the scan moves onto HarnessX, and how fast?
|
||||
- **Alternatives Considered**:
|
||||
1. Incremental seam — sync driver stays; HarnessX only in the hunter callback.
|
||||
2. Native rewrite — delete `HarnessRuntime`; whole scan becomes a single-step harness immediately.
|
||||
3. Policy-first whole-loop — whole scan as processors on a fixed loop.
|
||||
- **Selected Approach**: **Phased.** Adopt HarnessX directly *at the hunter loop* first (Phase 3); migrate deterministic stages onto a single-step harness only later and optionally (Phase 5+), retiring `HarnessRuntime` only after the seam is proven and the gate has held.
|
||||
- **Rationale**: The sync→async + contract mismatch is the single highest-risk item in the whole effort. The native rewrite touches every stage at once — too much blast radius to keep ≥90% recall / <10% FP green per-merge. The incremental seam ships the *actual* HarnessX value (agent loop, journal, `StatefulTrajectory`) at exactly the one stage that is already an agent loop.
|
||||
- **Trade-offs**: Two harness contracts coexist through Phases 3-4 and `HarnessRuntime` lingers; in exchange every phase is independently shippable, test-gated, and reversible.
|
||||
- **Follow-up**: Golden-output diff gate at each phase; confine `asyncio.run` to the hunter callback; `SlotContractMixin` to preserve key-level discipline.
|
||||
|
||||
### Decision D2: Dependency strategy
|
||||
- **Context**: A zero-dependency security tool considering a ~30-package agent runtime with no lean extra.
|
||||
- **Alternatives Considered**:
|
||||
1. (A) Hard dependency in main deps.
|
||||
2. (B) Optional extra `openultrasast[harnessx]`, SHA-pinned, lazy-imported.
|
||||
3. (C) Vendored lean subset under `src/openultrasast/_vendor/harnessx/`.
|
||||
- **Selected Approach**: **(B) is the shipping default; (C) is a pre-committed, triggered fallback** gated by an explicit `pip install --dry-run` audit before Phase 3. Core `dependencies = []` stays; all HarnessX imports are lazy behind a `_has_harnessx()` check.
|
||||
- **Rationale**: HarnessX's pervasive lazy imports mean the heavy deps are *installed* but only *imported on demand*, so a clean opt-in extra already keeps heavy code paths cold in the default install — with the least maintenance burden, while tracking upstream. (A) permanently inverts the project's stance and pins to an unmovable Beta `0.1.0`. (C) adds permanent fork-maintenance and should only be paid for when the *installed* graph (not the *imported* graph) is itself unacceptable.
|
||||
- **Trade-offs**: With B, enabling the extra still inherits the full HarnessX dep set (no lean upstream extra exists); every HarnessX import must be guarded.
|
||||
- **Follow-up**: Run the dry-run audit and record the realized transitive graph. **Decision rule**: if it pulls Playwright browser binaries / Docker SDK / a FastAPI web stack as *mandatory* transitive deps and maintainers judge that unacceptable, switch to (C) — vendor `harnessx/{core,plugins,processors,bundles,meta_harness,workspace,tools/base}` + the `llm_judge`/`journal`/`validate_workflow`/`agent`/`contract_autocheck` modules, delete `lab/`/browser/sandbox/office subtrees, re-pin a minimal set. The lazy-import design makes the deletion surgical. Never use the recursive submodule path (`recipe/verl_harnessX/verl`).
|
||||
|
||||
### Decision D3: What the self-improvement loop is allowed to edit
|
||||
- **Context**: The user's core complaint is that rules are "too influential." The loop must be able to *govern* rules without becoming a new way for them to run wild.
|
||||
- **Alternatives Considered**:
|
||||
1. Single `"rule"` lever (status/threshold).
|
||||
2. `"rule"` lever; patterns stay human-PR'd (native angle).
|
||||
3. `"rule"` + `"policy"` levers, where policy = evidence floors/scope, *not* CWE severity (policy-first angle).
|
||||
- **Selected Approach**: **Two bounded levers.** `"rule"` = `status` (enabled/shadow/disabled) + evidence-floor + threshold, **never pattern text**. `"policy"` = evidence floor + scope + score constants `K`/`MIN_SCORE`, **never the authoritative 0-5 CWE severity**. Patterns and CWE severities are human-PR-only.
|
||||
- **Rationale**: This is the precise answer to "stop rules being too influential" — the loop may *suppress / stage / raise-the-bar*, never *invent detectors* or *rewrite the severity authority*. Bounds + mandatory `shadow` staging + auto-revert keep loop authority safe; the meta_harness novelty/evidence/replay/attribution gates apply for free.
|
||||
- **Trade-offs**: The loop cannot fix a bad pattern itself (it can only `shadow` it and nominate a human PR), and cannot re-weight a CWE — both are deliberate guardrails.
|
||||
- **Follow-up**: Implement `EvolveValidator._rules`/`._policy` raising `StrictValidationError(kind="rule_change")`; enforce the four "may NOT" bounds (no pattern edits, no 0-5 severity edits, no `enabled→disabled` without `shadow`, no reverted re-proposal without `retry_rationale`).
|
||||
|
||||
### Decision D4: Severity authority
|
||||
- **Context**: Two rules for the same CWE can disagree on severity today.
|
||||
- **Selected Approach**: PolicyStore (`CWE_Score.tsv`) is authoritative; `resolve_severity(policy, rule.cwe, target, ranking)` replaces the literal `rule.severity` read; `PatternRule.severity` is removed. Policy always wins; rule string-severities are discarded.
|
||||
- **Rationale**: One CWE = one governed severity, reconciled centrally; ends rule disagreement.
|
||||
- **Trade-offs**: Loses the per-rule severity hint (acceptable — it was an authoring accident, not a governed decision).
|
||||
|
||||
### Decision D5: The CWE-120 coverage gap
|
||||
- **Context**: 3 C/C++ buffer rules use CWE-120, which is absent from `CWE_Score.tsv`.
|
||||
- **Selected Approach**: Remap `c-unsafe-copy`/`c-unsafe-memory-copy`/`c-unsafe-scanf` to **CWE-121** (Stack Buffer Overflow, severity 5) in the ruleset TOML.
|
||||
- **Rationale**: CWE-121 is the precise classic-overflow CWE and is already at severity 5; this avoids forking the upstream policy file. Otherwise those high-value rules contribute 0 to the score.
|
||||
- **Follow-up**: Add a hard CI invariant — every `enabled` rule's `cwe` must resolve in PolicyStore (fail loud at startup); log `unmapped_cwe` in the score artifact; PR genuine gaps upstream to verycode-policies.
|
||||
|
||||
### Decision D6: Response to a confirmed false positive
|
||||
- **Context**: How does the system react to an FP without hurting recall?
|
||||
- **Selected Approach**: Lower the finding's effective **reachability multiplier** and/or `shadow` the rule — **never delete it**.
|
||||
- **Rationale**: The score stops over-penalizing while the rule keeps firing (tracked, just not scored/reported), preserving ≥90% recall and pushing FP <10%. This is exactly the recall/FP balance the project gates on.
|
||||
|
||||
### Decision D7: Where OUS's `reads`/`writes` contract lives
|
||||
- **Context**: HarnessX has no per-key state contract; OUS's defining discipline must survive.
|
||||
- **Selected Approach**: Reimplement as a `SlotContractMixin` on our `MultiHookProcessor`s (`reads_slots`/`writes_slots` class attrs validated in a base `on_*` wrapper), gated by `OUS_SLOT_CONTRACT=strict|warn`.
|
||||
- **Rationale**: The two contracts are incompatible; the mixin preserves provenance/audit discipline inside HarnessX's runtime without trying to bend the message-window contract.
|
||||
|
||||
### Decision D8: Project-score gate rollout
|
||||
- **Context**: The verycode score formula is a reconstruction (no upstream implementation).
|
||||
- **Selected Approach**: Ship the score gate **advisory-first** (computed and reported, not blocking); promote to blocking only after `K`/`MIN_SCORE` are calibrated against the benchmark corpus. Score computation is stdlib-only and always on (zero-dep), so CI gating runs in the default install with no HarnessX present.
|
||||
- **Rationale**: The formula needs corpus calibration before it can safely block a merge; keeping it zero-dep keeps the CI gate independent of the optional harness.
|
||||
|
||||
### Decision D9: RL adoption
|
||||
- **Context**: HarnessX has an `rl/` subsystem (weight-level improvement) alongside the config-level meta_harness.
|
||||
- **Selected Approach**: Optional, Phase 5+.
|
||||
- **Rationale**: Config-level adaptation via the meta_harness is sufficient to close the rule/policy loop; weight-level RL over hunter trajectories is purely additive and not on the critical path.
|
||||
|
||||
## Risks & Mitigations
|
||||
- **Sync↔async + contract incompatibility (High)** — confine the async surface to the hunter stage via `asyncio.run` in its callback (Phase 3); `SlotContractMixin` for key-level discipline; deterministic stages run `skip_model`; golden-output diff gate at each phase.
|
||||
- **Dependency posture inversion (High)** — governance/scoring planes stay zero-dep; HarnessX strictly behind the extra + lazy imports + pinned SHA; CI zero-dep guard test; `pip --dry-run` audit before Phase 3; vendored-subset fallback pre-specified.
|
||||
- **Benchmark-gate regression from rule auto-tuning (High)** — the recall/FP gate is the per-round acceptance test (aggregate + per-rule, hard constraint); `shadow` before `disabled`; novelty + attribution block thrash and reverted re-proposals; auto-revert byte-for-byte.
|
||||
- **Score gaming the gate (High)** — keep the gate as a *hard constraint*, never part of the reward; track unpredicted regressions; add a score-regression check to acceptance.
|
||||
- **Meta-agent corrupts the ruleset, re-introducing "too influential" (Medium)** — bounded levers (D3): no pattern rewrites, no severity edits, mandatory `shadow` staging, no `enabled→disabled` jump; recall gate auto-reverts; every change journaled and novelty-checked.
|
||||
- **Policy completeness / CWE drift (Medium)** — day-one CWE-120→CWE-121 remap; hard CI invariant that every enabled rule's CWE resolves; `unmapped_cwe` logged; treat verycode-policies as authoritative-upstream and PR gaps there.
|
||||
- **Frozen Beta upstream (Medium)** — pin SHA; document in `VENDOR.md`; keep the meta_harness patch minimal/isolated; rule lever degrades gracefully (absent patch = no rule lever, loop still does ranking demotion); vendored subset removes upstream-instability exposure for depended modules.
|
||||
- **Slot serialization loss on resume (Medium)** — all scan slots use plain dicts/dataclasses with `_target_` registration; per-slot-type round-trip serialization test (non-serializable content restores as `None` on `wake()`).
|
||||
- **Two-store coordination drift (Low-Med)** — a single `resolve_severity` owns the rule-status + policy-gate combination; the changeset records both lever edits per round; the benchmark regression suite catches interaction regressions.
|
||||
|
||||
## References
|
||||
- HarnessX core — `/tmp/HarnessX/harnessx/core/{harness,builder,processor,events,runloop,state,contract_check,model_config,trajectory}.py` — composition/run-loop/contract/state model.
|
||||
- HarnessX processors — `/tmp/HarnessX/harnessx/processors/{evaluation/llm_judge.py, evaluation/strategies/evaluators/prm.py, observability/{otel_proc,episode_metrics,checkpoint}.py, memory/memory_retrieval.py, control/loop_detection.py}` — reuse map (verify/calibrate/observability).
|
||||
- HarnessX meta_harness — `/tmp/HarnessX/harnessx/meta_harness/{agent,journal,replay,validate_workflow}.py`, `processors/contract_autocheck.py`, `workers/trajectory_digester.py`; `rl/{builder,config,task}.py` — self-improvement engine + rule-adaptation seam.
|
||||
- HarnessX packaging — `/tmp/HarnessX/pyproject.toml`, `harnessx/__init__.py`, `plugins/{base,loader,discovery,convert}.py`, `examples/{coding,research,assistant}/harness.py`, `extensions/plugins/workflow/plugin.py`, `.gitmodules` — installability + dependency weight.
|
||||
- verycode policy — `/tmp/verycode-policies/{CWE_Score.tsv,README.md}`; `gh repo view norandom/verycode-score` (`action.yaml`, `main.go` placeholder); `gh repo view norandom/verycode-lib` (stub) — policy data (complete) vs scoring algorithm (never implemented).
|
||||
- OUS current system — `/home/mc/Source/OpenUltraSAST/src/openultrasast/{findings,rank,calibration,benchmark,verification,harness,config,cli}.py` — `PATTERN_RULES`, ranking-only calibration (`MIN_CALIBRATED_PRIORITY=0.1`), inert `next_improvement_candidate`, sync `HarnessRuntime`, no rule/policy config section.
|
||||
- Companion design — `.kiro/specs/harnessx-self-improving-rulesets/design.md` (committed architecture); detection gate per `MEMORY.md`: ≥90% recall / <10% FP on the benchmark corpus.
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"feature_name": "harnessx-self-improving-rulesets",
|
||||
"created_at": "2026-06-22T10:07:18Z",
|
||||
"updated_at": "2026-06-22T16:45:00Z",
|
||||
"language": "en",
|
||||
"phase": "implementation",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": true
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
# Implementation Plan
|
||||
|
||||
This plan splits the fused regex layer into three governed owners (rules detect, CWE policy decides severity/scope, project score is the optimization target) and adopts HarnessX behind an optional extra. Tasks are grouped into four phases. Each phase is independently shippable and keeps the ≥90% recall / <10% FP detection gate green. Phases 1-2 (Foundation) and the early Core work stay zero-dependency and byte-identical by default; the HarnessX execution and self-improvement work (Integration) lives behind a lazy, SHA-pinned extra; Validation locks the cross-cutting gates and the dependency-weight decision.
|
||||
|
||||
## Phase 1 — Foundation: Governance & Scoring Planes (zero-dep)
|
||||
|
||||
- [x] 1. Stand up the central CWE policy authority
|
||||
- [x] 1.1 Vendor the verycode CWE severity data and build the Policy Store loader
|
||||
- Vendor the verycode CWE-to-severity dataset into the governance plane and parse it with the standard library only, tolerating the trailing-space severity column header.
|
||||
- Expose, per CWE, an authoritative 0-5 severity, a static scope flag, and a dynamic scope flag; mark dynamic-only CWEs as report-only.
|
||||
- Observable completion: loading the policy yields the full set of CWE policy entries with correct static/dynamic flags and zero third-party imports, verified by a unit test.
|
||||
- _Requirements: 2.6, 2.9_
|
||||
- _Boundary: PolicyStore_
|
||||
|
||||
- [x] 1.2 Expose policy-governed severity resolution
|
||||
- Resolve a finding's severity exclusively from the Policy Store keyed on the rule's CWE, discarding any severity string carried in legacy rule data.
|
||||
- Guarantee that two findings whose rules map to the same CWE receive identical severity.
|
||||
- Observable completion: a resolution call returns the policy severity (never a legacy string) and is identical for two rules sharing a CWE, proven by a unit test.
|
||||
- _Requirements: 2.2, 2.3_
|
||||
- _Boundary: PolicyStore_
|
||||
|
||||
- [x] 1.3 Enforce the fail-loud startup CWE-resolution invariant
|
||||
- Assert at startup that every enabled rule's CWE resolves in the Policy Store; abort the scan loudly before any work when an enabled rule's CWE is unmapped.
|
||||
- Observable completion: an enabled rule with an unmapped CWE aborts the scan with a loud error and no findings are produced, proven by an integration test.
|
||||
- _Requirements: 2.7_
|
||||
- _Boundary: PolicyStore_
|
||||
- _Depends: 1.1_
|
||||
|
||||
- [x] 2. Build the project Score Model and CI gates (zero-dep)
|
||||
- [x] 2.1 Compute the 0-100 policy-weighted, reachability-adjusted project score
|
||||
- Compute a single 0-100 project score from policy-governed severity weights multiplied by a reachability multiplier over reported findings, using the standard library only.
|
||||
- Key the reachability multipliers exactly on the three emitted reachability values (`reachable`, `inferred-file-surface`, `unknown`) without redefining the vocabulary.
|
||||
- Assign a zero score penalty to findings whose CWE is dynamic-only or unmapped, reporting them as out-of-scope or unmapped instead of scoring them.
|
||||
- Observable completion: a no-penalty corpus scores 100, a single severity-5 reachable finding scores roughly 43, and dynamic-only/unmapped findings contribute zero penalty, proven by boundary unit tests.
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.9_
|
||||
- _Boundary: ScoreModel_
|
||||
- _Depends: 1.1_
|
||||
|
||||
- [x] 2.2 Emit the scan-level score artifact and manifest score block
|
||||
- Emit a score artifact containing the project score, the maximum severity, the total penalty, a by-category breakdown, the out-of-scope dynamic-only count, the unmapped-CWE list, and the gate verdict; include the score block in the scan manifest.
|
||||
- Observable completion: a scan writes a score artifact with every required field and merges the score block into the manifest, verified against the documented artifact schema.
|
||||
- _Requirements: 5.5_
|
||||
- _Boundary: ScoreModel_
|
||||
- _Depends: 2.1_
|
||||
|
||||
- [x] 2.3 Implement the two-condition CI score gate (advisory-first)
|
||||
- Fail the build when any reported finding maps to policy severity 5 and has reachability `reachable`.
|
||||
- When configured as blocking, fail the build when the project score is below the minimum; until the score constants are calibrated against the corpus, compute and report the score advisory-first without blocking.
|
||||
- Observable completion: the hard gate fails on a severity-5 reachable finding while the score gate reports advisory-only by default, proven by gate tests.
|
||||
- _Requirements: 5.7, 5.8, 5.9_
|
||||
- _Boundary: CI Score Gate_
|
||||
- _Depends: 2.1_
|
||||
|
||||
- [x] 2.4 Centralize ruleset/policy/score configuration
|
||||
- Add configuration entries for the ruleset, policy, and score data locations plus the gate thresholds and emission floors used by later phases.
|
||||
- Observable completion: the scan reads the score constants and emission thresholds from configuration rather than hardcoded literals, confirmed by a config-driven test.
|
||||
- _Requirements: 5.8_
|
||||
- _Boundary: Packaging Configuration, ScoreModel_
|
||||
|
||||
## Phase 2 — Core: Rules-as-Data and Benchmark Signals (zero-dep, byte-identical)
|
||||
|
||||
- [x] 3. Convert detection rules into versioned, governed data
|
||||
- [x] 3.1 Externalize detection patterns into a versioned Ruleset Store
|
||||
- Load detection rules from versioned data files in which each rule declares a pattern, a CWE, tags, and a loop-controlled status, carrying no rule-local severity field.
|
||||
- Remove the rule-local severity field from the in-memory rule model so severity can only come from policy.
|
||||
- Observable completion: the existing pattern rules load from versioned data files with no severity attribute present, proven by a unit test asserting the absence of the field.
|
||||
- _Requirements: 2.1, 2.9_
|
||||
- _Boundary: RulesetStore_
|
||||
|
||||
- [x] 3.2 Implement the three-status volume control
|
||||
- Support exactly three rule statuses — `enabled`, `shadow`, `disabled` — where enabled fires/scores/reports, shadow fires and records outcomes but is excluded from report and score, and disabled does not fire.
|
||||
- Exclude shadow-status findings from the reported set and from the score input while still recording their outcome for precision tracking.
|
||||
- Observable completion: an enabled rule appears in report and score, a shadow rule is excluded from both yet records an outcome, and a disabled rule never fires, proven by a unit test across all three statuses.
|
||||
- _Requirements: 2.4, 2.5, 5.4_
|
||||
- _Boundary: RulesetStore, ScoreModel_
|
||||
- _Depends: 3.1, 2.1_
|
||||
|
||||
- [x] 3.3 Remap the unmapped C/C++ buffer rules to a scored CWE
|
||||
- Remap the C/C++ buffer-handling rules that reference the unmapped CWE-120 to CWE-121 so they resolve in policy and contribute a nonzero penalty.
|
||||
- Observable completion: the remapped buffer rules resolve to a severity-5 policy entry and produce a nonzero score penalty, verified by a unit test.
|
||||
- _Requirements: 2.8_
|
||||
- _Boundary: RulesetStore_
|
||||
- _Depends: 3.1, 1.1_
|
||||
|
||||
- [x] 3.4 Wire findings and severity resolution through the governance plane
|
||||
- Pass the loaded ruleset into the finding-detection stage instead of reading an implicit module-level rule list, and resolve every finding's severity through the Policy Store.
|
||||
- Apply emission thresholds so not every regex match becomes a reported finding, replacing the old "rule is its own volume control" behavior.
|
||||
- Observable completion: a scan produces findings with policy-resolved severity and shadow findings filtered out, while the default ruleset keeps benchmark output byte-identical to the pre-feature baseline.
|
||||
- _Requirements: 2.2, 2.3, 2.5, 6.2_
|
||||
- _Boundary: PolicyScoringProcessor, RulesetStore, PolicyStore_
|
||||
- _Depends: 3.1, 3.2, 1.2_
|
||||
|
||||
- [x] 4. Make benchmark output an actionable per-rule signal
|
||||
- [x] 4.1 Attribute misses and false positives to rules
|
||||
- Attribute every recorded miss and every recorded false positive to a rule identifier and aggregate per-rule precision and recall in the benchmark metrics.
|
||||
- Observable completion: a benchmark run reports per-rule precision/recall and every miss and false positive carries a rule identifier, verified by a metrics test.
|
||||
- _Requirements: 4.1_
|
||||
- _Boundary: Benchmark Harness_
|
||||
- _Depends: 3.1_
|
||||
|
||||
- [x] 4.2 Emit a per-rule recommendation delta artifact
|
||||
- Replace the inert improvement-candidate stub with a per-rule recommendation artifact recommending enable, disable, shadow, threshold, or evidence-floor changes per rule.
|
||||
- Observable completion: a benchmark run writes a recommendation artifact with per-rule suggested actions alongside its records, verified against the documented delta schema.
|
||||
- _Requirements: 4.2_
|
||||
- _Boundary: Benchmark Harness_
|
||||
- _Depends: 4.1_
|
||||
|
||||
- [x] 4.3 Introduce the loop-owned ledger and re-key calibration to rules
|
||||
- Add the loop-owned overlay ledger that records per-rule status, evidence floor, threshold, and precision, applied over the ruleset at load time.
|
||||
- Re-key calibration from the first-tag key to the rule identifier and write status flips into the ledger rather than only demoting ranking.
|
||||
- Observable completion: an empty ledger leaves behavior byte-identical to the prior phase while a ledger entry overlays a rule's status at load, proven by an integration test.
|
||||
- _Requirements: 2.4, 5.6, 6.2_
|
||||
- _Boundary: RulesetStore, Calibration subsystem_
|
||||
- _Depends: 3.1, 3.2_
|
||||
|
||||
- [x] 4.4 Convert false-positive confirmation into a reachability knob
|
||||
- When confirming a false positive, lower the finding's effective reachability multiplier rather than delete the rule, so recall stays high while the score stops over-penalizing.
|
||||
- Observable completion: confirming a false positive lowers the finding's effective reachability multiplier and the rule still fires, proven by a calibration test.
|
||||
- _Requirements: 5.6_
|
||||
- _Boundary: Calibration subsystem, ScoreModel_
|
||||
- _Depends: 4.3, 2.1_
|
||||
|
||||
## Phase 3 — Integration: HarnessX Execution & Self-Improvement (optional extra)
|
||||
|
||||
- [x] 5. Gate and package the HarnessX dependency-weight decision
|
||||
- [x] 5.1 Run the dependency dry-run audit and record the transitive graph
|
||||
- Before adopting the HarnessX execution stages, run a dependency dry-run audit and record the realized transitive dependency graph.
|
||||
- If the audited graph pulls a browser-binary, container-SDK, or web-server stack as a mandatory dependency and it is judged unacceptable, select the pre-specified vendored lean subset under the same extra name with the heavy subtrees removed.
|
||||
- Observable completion: the audit produces a recorded transitive-dependency graph and a documented accept-extra-or-vendor-subset decision before any HarnessX execution code is wired in.
|
||||
- _Requirements: 7.3, 7.4, 7.7_
|
||||
- _Boundary: Packaging Configuration_
|
||||
|
||||
- [x] 5.2 Configure HarnessX as a SHA-pinned optional extra with a capability guard
|
||||
- Keep the core dependency list empty and expose HarnessX only as an optional extra pinned to a specific commit revision; initialize any submodule non-recursively so the large RL fork subtree is not pulled.
|
||||
- Guard every HarnessX import behind a capability check and import it lazily so heavy code paths stay cold in the default install.
|
||||
- Observable completion: the default install resolves with an empty core dependency list and HarnessX symbols are only imported after the capability check passes, verified by an import-cold test.
|
||||
- _Requirements: 7.1, 7.2, 7.6_
|
||||
- _Boundary: Packaging Configuration_
|
||||
- _Depends: 5.1_
|
||||
|
||||
- [x] 6. Adopt HarnessX at the hunter loop and verifier
|
||||
- [x] 6.1 Run the hunter pool stage on a real HarnessX agent loop
|
||||
- Run the hunter pool stage on a real HarnessX run loop with the control, tool, and observability processors registered in the hunter sub-harness, invoked from the existing hunter-pool stage callback.
|
||||
- Confine all asynchronous execution to this single stage so the deterministic stages remain synchronous.
|
||||
- Observable completion: with the extra installed, the hunter pool produces per-target trajectories from a real agent loop while all other stages stay synchronous, verified by an integration test.
|
||||
- _Requirements: 1.1, 1.4_
|
||||
- _Boundary: HxScanOrchestrator_
|
||||
- _Depends: 5.2_
|
||||
|
||||
- [x] 6.2 Map stage events and identifiers onto HarnessX hooks
|
||||
- Map the existing stage-start/stage-end events to the HarnessX step-level hooks and the scan identifier to the HarnessX run identifier without losing trace continuity.
|
||||
- Observable completion: a HarnessX-backed run preserves trace continuity with stage events mapped to step hooks and the scan id mapped to the run id, verified by inspecting the emitted trace.
|
||||
- _Requirements: 1.7_
|
||||
- _Boundary: HxScanOrchestrator_
|
||||
- _Depends: 6.1_
|
||||
|
||||
- [x] 6.3 (P) Run deterministic stages as model-disabled HarnessX processors with slot contracts
|
||||
- Run each deterministic stage under HarnessX with model invocation disabled so no deterministic stage incurs a model call.
|
||||
- Preserve each stage's declared read/write slot allow-list and validate slot access on every lifecycle hook.
|
||||
- Observable completion: deterministic stages execute under HarnessX with no model calls and a slot-contract violation is raised on undeclared slot access, proven by a slot-contract test.
|
||||
- Implemented zero-dep in `slot_contract.py` (`SlotContractMixin`/`SlotPipeline`, validates reads/writes per lifecycle hook, enforces `skip_model`) + `stage_processors.py` (the deterministic stages as `skip_model` slot processors; `run_quick_pipeline` is byte-identical to the quick path). Per "bridge, do not map", the sync `HarnessRuntime` stays the top-level driver and these slot processors are the model-disabled representation; `host_under_harnessx` is the lazy, capability-gated bridge that disables the model on HarnessX's `BeforeModelEvent.skip_model`. Module names differ from the design's `harness/{slot_contract,processors}.py` because `harness` is a module, not a package. Tests: `tests/test_slot_contract.py`.
|
||||
- _Requirements: 1.5, 1.6_
|
||||
- _Boundary: HxScanOrchestrator_
|
||||
- _Depends: 6.1_
|
||||
|
||||
- [x] 6.4 (P) Add the LLM-judge verifier with a structural fallback
|
||||
- Produce verification verdicts using the HarnessX llm-judge helpers when the extra is present, mapping the strict-JSON verdict to a verification result.
|
||||
- Retain the existing structural verifier (which rejects on unknown reachability) verbatim as the zero-dependency fallback.
|
||||
- Observable completion: with the extra present the judge produces verdicts and with it absent the structural verifier produces equivalent verdicts, proven by a paired verifier test.
|
||||
- _Requirements: 1.2_
|
||||
- _Boundary: VerifyProcessor_
|
||||
- _Depends: 5.2_
|
||||
|
||||
- [x] 6.5 Fall back gracefully when a HarnessX capability is unavailable
|
||||
- When a HarnessX-backed stage is enabled but its capability is unavailable at runtime, fall back to the existing equivalent stage and record the degradation in the scan manifest.
|
||||
- Observable completion: requesting a HarnessX stage whose capability is missing runs the existing equivalent and records a degradation entry in the manifest, proven by a fallback test.
|
||||
- _Requirements: 1.8_
|
||||
- _Boundary: HxScanOrchestrator_
|
||||
- _Depends: 6.1, 6.4_
|
||||
|
||||
- [x] 7. Stand up the bounded self-improvement loop
|
||||
- [x] 7.1 Add the two bounded edit levers
|
||||
- Expose exactly two new edit levers: a rule lever covering rule status, evidence floor, and threshold, and a policy lever covering evidence floor, scope, and the score constants.
|
||||
- Observable completion: the loop's valid-lever set includes exactly the rule and policy levers and rejects any edit outside their declared fields, verified by a lever test.
|
||||
- _Requirements: 3.1_
|
||||
- _Boundary: EvolveValidator, improve/evolve.py_
|
||||
- _Depends: 5.2_
|
||||
|
||||
- [x] 7.2 Enforce the rule/policy edit safety bounds in the validator
|
||||
- Reject any loop edit that modifies detection pattern text and any edit that modifies the authoritative 0-5 CWE severity.
|
||||
- Reject moving a rule directly from enabled to disabled, requiring shadow as a mandatory intermediate staging state.
|
||||
- Assert that each edit parses, that status is one of the three literals, that threshold/evidence-floor changes stay within configured bounds, that an added rule does not duplicate an existing identifier, and that every referenced CWE resolves in policy; raise a rule-change validation error and reject the whole round on any failure.
|
||||
- Observable completion: each prohibited edit (pattern change, severity change, enabled-to-disabled jump, duplicate identifier, unmapped CWE, out-of-bounds threshold) is rejected and rejects the round, proven by validator unit tests.
|
||||
- _Requirements: 3.2, 3.3, 3.4, 3.5, 3.8_
|
||||
- _Boundary: EvolveValidator_
|
||||
- _Depends: 7.1, 1.1_
|
||||
|
||||
- [x] 7.3 Add the novelty, replay, and journal gates
|
||||
- Block re-proposing a previously reverted edit that lacks a retry rationale via the novelty gate.
|
||||
- Run a replay smoke gate before any benchmark re-run proving the edited ruleset and policy still load and boot.
|
||||
- Record each rule and policy edit, its lever, and its outcome in the journal so every change is attributable and reversible.
|
||||
- Observable completion: a reverted re-proposal without rationale is blocked, a non-booting edited ruleset is rejected before re-benchmark, and every edit appears in the journal with its lever and outcome, proven by gate tests.
|
||||
- _Requirements: 3.6, 3.7, 3.9_
|
||||
- _Boundary: EvolveValidator, improve/evolve.py_
|
||||
- _Depends: 7.2_
|
||||
|
||||
- [x] 7.4 Convert benchmark deltas into loop signals
|
||||
- Convert the per-rule recommendation delta and false-positive learnings into a loop signal file, writing recall-side items as miss signals and precision-side items as verifier-reject or false-positive signals.
|
||||
- Observable completion: a benchmark run produces a signal file whose recall-side entries are miss signals and precision-side entries are verifier-reject/false-positive signals, verified against the documented signal schema.
|
||||
- _Requirements: 4.3_
|
||||
- _Boundary: improve/evolve.py_
|
||||
- _Depends: 4.2, 7.1_
|
||||
|
||||
- [x] 7.5 Implement the hard acceptance gate with byte-for-byte revert
|
||||
- After the meta-agent edits rules or policy, re-run the benchmark and accept the round only if aggregate recall is at least 90% AND false-positive rate is under 10% AND the project score did not regress on the held set AND no unpredicted regression occurred.
|
||||
- On any failed acceptance condition, revert the round and restore the rule and policy configuration byte-for-byte.
|
||||
- Observable completion: a round that regresses recall is reverted and the configuration is restored byte-for-byte, while a compliant round is accepted and persisted, proven by a self-improvement integration test.
|
||||
- _Requirements: 4.4, 4.5, 6.3_
|
||||
- _Boundary: improve/evolve.py_
|
||||
- _Depends: 7.3, 7.4, 4.1_
|
||||
|
||||
- [x] 7.6 Auto-shadow precision-draggers, nominate coverage gaps, and separate the objective
|
||||
- Auto-shadow a rule that drags aggregate precision below the gate rather than delete it, preserving recall while removing the false-positive cost.
|
||||
- Nominate a new or loosened rule through the signal file when a coverage gap surfaces as a persistent miss, leaving pattern authoring to a human pull request.
|
||||
- Treat maximizing the project score as the optimization objective and the recall/FP gate as a separate hard feasibility constraint that is never folded into the score reward.
|
||||
- Observable completion: a deliberately noisy rule is auto-shadowed (not deleted) when it drags precision, a persistent miss emits a rule nomination, and the loop cannot raise the score by shadowing rules that breach the gate, proven by integration tests.
|
||||
- _Requirements: 4.6, 4.7, 4.8_
|
||||
- _Boundary: improve/evolve.py_
|
||||
- _Depends: 7.5_
|
||||
|
||||
## Phase 4 — Validation: Cross-Cutting Gates & Non-Regression
|
||||
|
||||
- [x] 8. Lock the detection-gate non-regression checks into CI
|
||||
- [x] 8.1 Enforce the per-merge recall/FP detection gate
|
||||
- Evaluate aggregate recall and aggregate false-positive rate on the benchmark corpus on every merge and fail the merge if recall is below 90% or the false-positive rate is at or above 10%.
|
||||
- Treat the recall/FP gate as a hard constraint that is never folded into the project-score reward, blocking any change that breaches the gate regardless of score improvement.
|
||||
- Observable completion: a CI run fails the merge when recall drops below 90% or false-positive rate reaches 10%, and a score improvement cannot unblock a gate-breaching change, proven by pipeline tests.
|
||||
- _Requirements: 6.1, 6.5, 6.6_
|
||||
- _Boundary: CI Pipeline_
|
||||
- _Depends: 4.1_
|
||||
|
||||
- [x] 8.2 (P) Assert byte-identical benchmark baseline for the zero-dep phases
|
||||
- With the default ruleset and an empty loop ledger, assert the benchmark output is byte-identical to the pre-feature baseline for the governance and rules-as-data phases.
|
||||
- Observable completion: a golden-diff test confirms byte-identical benchmark output with the default ruleset and empty ledger.
|
||||
- _Requirements: 6.2_
|
||||
- _Boundary: Benchmark Harness, CI Pipeline_
|
||||
- _Depends: 3.4, 4.3_
|
||||
|
||||
- [x] 8.3 (P) Confirm HarnessX-backed stages stay within detection tolerance
|
||||
- When a HarnessX-backed stage is enabled, confirm recall and false-positive rate stay within the configured tolerance of the equivalent zero-dependency baseline.
|
||||
- Observable completion: a CI run with a HarnessX-backed stage enabled keeps recall/FP within the configured tolerance of the baseline, proven by a tolerance test.
|
||||
- _Requirements: 6.4_
|
||||
- _Boundary: CI Pipeline, HxScanOrchestrator_
|
||||
- _Depends: 6.1, 8.1_
|
||||
|
||||
- [x] 8.4 (P) Add the zero-dependency guard test
|
||||
- Run a guard test confirming that with HarnessX absent the governance, scoring, and benchmark planes operate and CI gating runs in the default install.
|
||||
- Observable completion: the guard test passes with HarnessX uninstalled and fails if any governance/scoring/benchmark path imports HarnessX.
|
||||
- _Requirements: 7.5_
|
||||
- _Boundary: CI Pipeline_
|
||||
- _Depends: 8.1_
|
||||
|
||||
- [x] 9. Validate scan-pipeline integrity and serialization
|
||||
- [x] 9.1 (P) Verify capability fallback and unchanged sync-driver behavior
|
||||
- Confirm that with the extra absent, quick and standard scans execute through the existing sync driver with behavior unchanged from the pre-adoption baseline, and that a requested-but-unavailable HarnessX stage records a degradation.
|
||||
- Observable completion: an extra-absent scan matches the pre-adoption baseline and a missing-capability request records a manifest degradation, proven by an end-to-end fallback test.
|
||||
- _Requirements: 1.3, 1.8_
|
||||
- _Boundary: HxScanOrchestrator, CI Pipeline_
|
||||
- _Depends: 6.5_
|
||||
|
||||
- [x] 9.2 (P) Verify slot serialization round-trips on resume
|
||||
- Confirm each scan slot type serializes and restores without loss after a resume, guarding against non-serializable slots restoring as empty.
|
||||
- Observable completion: a per-slot-type round-trip test passes for every scan slot after a resume cycle.
|
||||
- Done for the current artifact-persistence model (findings/ranking/file-target/verification/score round-trip + the real findings.json reload path in `tests/test_scan_pipeline_integrity.py`). The HarnessX processor slot-contract serialization variant remains deferred with task 6.3.
|
||||
- _Requirements: 1.5_
|
||||
- _Boundary: HxScanOrchestrator_
|
||||
- _Depends: 6.3_
|
||||
@@ -0,0 +1,218 @@
|
||||
# Design: hunter-learning-loop
|
||||
|
||||
> **Amendment 2026-10-14.** The feature table below is the shape of the first round and is superseded as the
|
||||
> model's input by the ordered trace of `access-control-prover` (design sections "trace: the ordered
|
||||
> serialisation" and "cfg and CfgDominance"). The feature table remains the screen's instrument and the
|
||||
> ablation's bookkeeping; the decision strategy is the one stated in requirements.md, "Decision strategy,
|
||||
> corrected 2026-10-14": a rule over the trace, a zero-shot judge for the residual question, the pool for
|
||||
> measurement only.
|
||||
|
||||
## Overview
|
||||
**Purpose**: turn the LLM hunter from a hand-written prompt into a measured search stage whose inputs are an
|
||||
engineered feature table and whose prompt is optimised by a program runtime, judged on held-out folds.
|
||||
**Users**: the maintainer runs the loop locally on the research host; the pre-push safety net consumes its output
|
||||
later (not in this spec).
|
||||
**Impact**: adds a research package `src/openultrasast/hunt/` and one research dependency (DSPy); reuses the
|
||||
learn harness for labels, folds, leak audit and paired analysis; reuses `benchmarks/valid/hunter_arm.py` as the
|
||||
runner and `benchmarks/unseen/score.py` plus `benchmarks/valid/metrics.py` as the only scorekeepers. The hook,
|
||||
its urllib client and the core install are untouched.
|
||||
|
||||
### Goals
|
||||
- A feature table with named, switchable columns is the contract between producers, ablation and the program (3, 4).
|
||||
- The hunter is a DSPy program over that table plus the changed hunks; its instruction and demonstrations are
|
||||
proposed by DSPy optimisers and accepted only by our rule on held-out folds (5, 6).
|
||||
- Every annotation's worth is a paired, pre-registered ablation with intervals; the importance table is the output
|
||||
the maintainer reads (4).
|
||||
- Search recall and suspicions per push improve on the held-out pilot, reported with the pilot's own scorer (7, 8).
|
||||
|
||||
### Non-Goals
|
||||
- Thresholds or operating points (4.7); the hook's LLM tier (pre-push-safety-net); the proof arm (search-with-proof);
|
||||
retraining the classifier (7.5); replacing the quick-rule engine with Semgrep (separate decision, brief pending);
|
||||
migrating `learn/labels.py` to pandas (statistics item, separate).
|
||||
|
||||
## Boundary Commitments
|
||||
### This spec owns
|
||||
- `src/openultrasast/hunt/`: the feature table schema, the producer list, the hunt program, the ablation runner,
|
||||
the acceptance rule, the run artifact.
|
||||
- The training-set recipe (which advisories, folds, digests) and its records under `benchmarks/measurements/`.
|
||||
- The importance table and the before/after report on the pilot.
|
||||
### Out of boundary
|
||||
- Label semantics (`benchmarks/valid/catalog.py`, `verify.py`, `learn/labels.py`): consumed as they are.
|
||||
- The scorer and metrics (`benchmarks/unseen/score.py`, `benchmarks/valid/metrics.py`): never edited to fit.
|
||||
- The hook, `tool_hunter.py`'s shipped prompt, the classifier program, Semgrep adoption, lizard adoption (each a
|
||||
producer candidate that enters only through the producer contract after its own evaluation).
|
||||
### Allowed dependencies
|
||||
- `openultrasast.source`, `semantic` facts, `mapping` entry points, `learn.folds`, `learn.leaks`,
|
||||
`learn.experiments` (paired bootstrap, McNemar), `learn.labels` readers, `benchmarks.valid.hunter_arm` (runner),
|
||||
`benchmarks.unseen.score`; `dspy` (research extra `[hunt]`), `pandas` (table), `scipy`/`statsmodels` once the
|
||||
statistics item lands (until then `learn.experiments`).
|
||||
- Dependency direction: data (manifests, labels) → producers → feature table → program → runner → scorer → report.
|
||||
Nothing to the right imports to the left; producers never import `hunt.program`; nothing in `hunt/` imports
|
||||
`push/` or `cli.py`.
|
||||
### Revalidation triggers
|
||||
- The feature table schema version changes; a producer's output shape changes; the scorer's tolerance changes;
|
||||
DSPy minor version changes; the pilot manifest digest changes.
|
||||
|
||||
## Architecture
|
||||
### Existing architecture analysis
|
||||
The hunter (`tool_hunter.run_tool_hunter`) is a tool loop with a four-sentence prompt and no diff
|
||||
(01-flowcharts/U6-hunter-client.md). The runner `hunter_arm.run` already walks a frozen manifest, checks out
|
||||
each head, proves bytes read, runs the hunter, classifies each finding with the decision-engine classifier and
|
||||
writes one record per change; `combine` joins with the replay. The harness has grouped folds, a leak audit and
|
||||
paired analysis. The missing pieces are the table, the switchable inputs and the optimisable program.
|
||||
|
||||
### Pattern and boundary map
|
||||
```mermaid
|
||||
flowchart LR
|
||||
M[(frozen manifests<br/>training set, pilot held out)] --> P
|
||||
L[(labels<br/>fix sites, ordinary pushes)] --> T
|
||||
subgraph producers
|
||||
P1[source.facts_for]
|
||||
P2[semantic facts<br/>roles, def-use, obligations]
|
||||
P3[mapping entry points]
|
||||
P4[diff hunks]
|
||||
P5[candidates: lizard, Semgrep<br/>after evaluation]
|
||||
end
|
||||
M --> P[hunt.producers.compute]
|
||||
P --> P1 & P2 & P3 & P4 & P5
|
||||
P1 & P2 & P3 & P4 & P5 --> T[(feature table<br/>hunt.table)]
|
||||
T --> A[hunt.ablation<br/>column mask per arm]
|
||||
A --> G[hunt.program<br/>dspy.ReAct + HuntSignature]
|
||||
G --> R[hunter_arm.run<br/>records per change]
|
||||
R --> S[unseen.score + valid.metrics]
|
||||
S --> I[importance table,<br/>acceptance decision, report]
|
||||
O[dspy optimisers<br/>GEPA, MIPROv2] --> G
|
||||
I --> O
|
||||
```
|
||||
- Selected pattern: a table-centred pipeline; the program is a replaceable consumer of the table.
|
||||
- Existing patterns preserved: the runner's record shape and resumability; the scorer; folds by repository; the
|
||||
instrument proof (bytes read, base and head verified).
|
||||
|
||||
### Technology stack (changed layers only)
|
||||
| Layer | Tool | Role |
|
||||
|---|---|---|
|
||||
| Program runtime | `dspy==3.4.*` (pulls litellm, openai, gepa, diskcache) as extra `[hunt]` | signature, ReAct tool loop, optimisers, cache, usage |
|
||||
| Model access | `dspy.LM("openai/deepseek-flash", api_base=DEEPSEEK_BASE_URL, api_key=env)` | one client for the research side; prices registered from `model/endpoint.py:55` |
|
||||
| Table | `pandas` as extra `[hunt]` | the feature table, masks, joins with labels and scores |
|
||||
| Statistics | `learn.experiments` now; `scipy`/`statsmodels` after the statistics item | paired bootstrap, McNemar |
|
||||
|
||||
## File structure plan
|
||||
```
|
||||
src/openultrasast/hunt/
|
||||
__init__.py
|
||||
table.py FeatureTable: schema v1, columns, masks, read/write (jsonl + parquet), digest
|
||||
producers.py PRODUCERS: fixed list of (name, fn, columns, needs); compute(root, change) -> rows
|
||||
program.py HuntSignature, HuntProgram(dspy.Module: ReAct + tools), render_row, parse_output
|
||||
metric.py search_metric(example, prediction) -> score, feedback text (GEPA), per-change outcome
|
||||
ablation.py Manifest (pre-registered), arms as masks, run(arm) -> records, analyse -> importance table
|
||||
accept.py the acceptance rule (6.1-6.8) over compile folds and held-out folds
|
||||
artifact.py RunArtifact: program id, schema version, masks, data digests, usage, versions
|
||||
benchmarks/valid/hunter_arm.py --program <artifact> and --mask <arm> options; everything else unchanged
|
||||
benchmarks/valid/training_set.py sourcing recipe apart from the pilot (catalog + verify + assemble, pilot excluded)
|
||||
tests/test_hunt_*.py table, producers (planted-advisory test), program with a scripted LM, metric,
|
||||
ablation masks, acceptance rule, artifact
|
||||
```
|
||||
|
||||
### Rule: a pattern engine's miss is never evidence
|
||||
Maintainer, 2026-10-13: "what does not work is that we dismiss things just because semgrep cannot find the bug.
|
||||
if semgrep was enough we'd use it." A Semgrep (or quick-rule, or engine) match is positive evidence and may enter
|
||||
the table as a feature ("matched here, rule X"). The absence of a match is not recorded as a feature, is never a
|
||||
filter, a demotion, a down-weight or a gate on a hunter suspicion, and no acceptance rule may count it. The
|
||||
Semgrep arm on the pilot is a comparison arm only.
|
||||
|
||||
## Components and interfaces
|
||||
| Component | Intent | Requirements | Depends on | Contract |
|
||||
|---|---|---|---|---|
|
||||
| FeatureTable | one row per candidate location with named columns | 3.1, 3.5, 4.2 | pandas | `columns: feature.<producer>.<name>`, `label`, `provenance.*`, `fold`, `schema_version` |
|
||||
| producers | compute facts from the repository only | 3.2, 3.3, 3.4, 3.7 | source, semantic, mapping | `compute(root, change) -> list[dict]`; absent feature = `None` + `provenance.<name>.reason` |
|
||||
| HuntProgram | the search | 3.5, 3.6, 5.1 | dspy, producers (rendered row) | input: rendered row + hunks; output: list of `{path, line, family ∈ FAMILY_IDS, confidence, rationale, cited_lines}` |
|
||||
| search_metric | per-change score and feedback | 4.3, 7.1 | unseen.score tolerance | hit on fix site with right family = 1; ordinary push suspicions counted; feedback text names misses |
|
||||
| ablation | arms as masks, paired runs | 4.1 to 4.6, 4.8 | table, program, runner, experiments | manifest: table digest, masks, folds, seed, budget, rule |
|
||||
| accept | train and holdout rule | 6.1 to 6.8 | ablation results | returns accepted|rejected with both fold results and the regression budget check |
|
||||
| RunArtifact | the record of a run | 5.1, 5.2, 5.5, 9.5 | dspy save | content-addressed JSON + the saved program + the DSPy cache directory copied |
|
||||
|
||||
### FeatureTable schema v1 (one row per candidate location)
|
||||
| Column group | Columns | Producer |
|
||||
|---|---|---|
|
||||
| key | `change_id`, `repo`, `head`, `path`, `line`, `kind` (vulnerability or ordinary), `fold` | manifest |
|
||||
| label | `label` (1 fix site, 0 ordinary, null unknown), `family`, `fix_span` | labels |
|
||||
| diff | `feature.diff.hunks` (list of (start, end) at head), `feature.diff.on_changed_line`, `feature.diff.distance` | producers.diff |
|
||||
| source | `feature.source.function`, `.span`, `.callers` (list of (path, line)), `.entry_points`, `.language` | source.facts_for |
|
||||
| semantic | `feature.semantic.roles` (source/sink/sanitizer per identifier), `.def_use` (binds and calls in the function), `.obligations` | semantic facts |
|
||||
| taxonomy | `feature.taxonomy.families` (closed list with one-line definitions) | ruleset/families.toml |
|
||||
| provenance | `provenance.<producer>.{version, ok, reason, bytes_read}` | each producer |
|
||||
A candidate location is every function the change touched at head (ordinary pushes) or the labelled fix site plus
|
||||
every other touched function (positives; the non-site rows carry `label` null, 1.3).
|
||||
|
||||
### HuntSignature
|
||||
Inputs (each an `InputField`, present only when its column is unmasked): `change_summary`, `hunks`,
|
||||
`function_text`, `callers`, `entry_points`, `roles`, `def_use`, `obligations`, `families`. Output: `findings`
|
||||
(JSON list, the classifier's answer structure). Tools (ReAct, `max_iters` bounded by the budget): `read_file`,
|
||||
`grep_repo`, `find_refs` from `hunter_tools`, path-clamped. An answer with a family outside `FAMILY_IDS` or a
|
||||
cited line outside the shown text is recorded `unknown` and scores against the program (3.6, 3.7).
|
||||
|
||||
### Optimisation and acceptance (one change per round, 6.8)
|
||||
1. Compile folds and held-out folds come from `learn.folds` by repository (2.5); the pilot is refused by digest.
|
||||
2. A round changes one thing: a producer mask (ablation), an instruction (GEPA or MIPROv2 proposal), or a
|
||||
demonstration set (BootstrapFewShot). DSPy proposes; `search_metric` scores on compile folds.
|
||||
3. `accept` scores the candidate on held-out folds, per family, with intervals from the paired bootstrap; keeps
|
||||
it only if compile and held-out are both non-negative, one strictly positive, no family below its regression
|
||||
budget, instruction under the length cap; ties rejected; rejected candidates kept visible to the next round;
|
||||
held-out never shown to the proposer (6.2 to 6.7).
|
||||
4. The artifact records both fold results and the rule (6.7).
|
||||
|
||||
## System flows
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as maintainer
|
||||
participant A as hunt.ablation
|
||||
participant T as FeatureTable
|
||||
participant G as HuntProgram (dspy)
|
||||
participant R as hunter_arm.run
|
||||
participant S as score + metrics
|
||||
M->>A: register manifest (table digest, masks, folds, budget, rule)
|
||||
A->>T: load table, apply mask for arm
|
||||
A->>G: build program (artifact or baseline)
|
||||
A->>R: run arm over compile folds (resumable records)
|
||||
R->>S: score per change
|
||||
A->>A: paired analysis vs comparison arm (bootstrap by repo, McNemar)
|
||||
A->>M: importance table, decision (accept.py), artifact id
|
||||
```
|
||||
|
||||
## Data flow and storage
|
||||
- Tables, records and DSPy caches live outside the checkout under `$OUSAST_RESULTS/hunt/<run>/` (9.4); the run
|
||||
artifact and the importance table are copied into `benchmarks/measurements/<date>-hunt-<run>/`.
|
||||
- Budgets: money and calls per run through `dspy.track_usage()` and litellm prices (9.1, 9.5); errors recorded by
|
||||
type (9.2); records resumable by `(change_id, input_digest, program_id, mask)` (9.3).
|
||||
|
||||
## Error handling
|
||||
- A producer that cannot compute writes `provenance.<name>.ok=false` with a reason; the row stays (3.4).
|
||||
- A change with nothing readable is an instrument failure, never a clean result (8.4); more than 5% invalidates
|
||||
the run (8.5).
|
||||
- A parse rejection in `source.ranges` is recorded as a provenance reason, not swallowed.
|
||||
|
||||
## Testing strategy
|
||||
- Unit: table masks and digests; each producer on a fixture repository; the planted-advisory test (3.7); the metric
|
||||
on scorer fixtures; the acceptance rule on synthetic fold results (every clause of 6.1 to 6.8).
|
||||
- Program: a scripted `dspy.LM` (DSPy's dummy LM) drives ReAct through the three tools; no network in tests.
|
||||
- Integration: one ablation round on a five-change fixture manifest with the scripted LM; the artifact round-trips.
|
||||
- Live, budgeted: the pilot run once per accepted program (8.6), recorded under `benchmarks/measurements/`.
|
||||
|
||||
## Requirements traceability
|
||||
| Requirement | Components |
|
||||
|---|---|
|
||||
| 1.1, 1.2, 1.3, 1.4, 1.5, 1.6 | FeatureTable label columns, search_metric, training_set.py (verification record next to the label) |
|
||||
| 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7 | training_set.py, learn.folds, learn.leaks audit over table columns, RunArtifact digests, pilot refusal in ablation |
|
||||
| 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8 | FeatureTable schema v1, producers (planted-advisory test for 3.7), HuntSignature, program output check |
|
||||
| 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8 | ablation (manifest, masks, paired runs, importance table, curve), search_metric |
|
||||
| 5.1, 5.2, 5.3, 5.4, 5.5, 5.6 | HuntProgram, RunArtifact, DSPy cache per run, scorer and labels outside the editable surface, length cap in accept |
|
||||
| 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8 | accept |
|
||||
| 7.1, 7.2, 7.3, 7.4, 7.5 | search_metric (search stage), hunter_arm classification (second stage, unchanged), report |
|
||||
| 8.1, 8.2, 8.3, 8.4, 8.5, 8.6 | hunter_arm `--program`, measurements record, instrument proof in records |
|
||||
| 9.1, 9.2, 9.3, 9.4, 9.5 | RunArtifact, dspy.track_usage, error types, results directory, resumable records |
|
||||
|
||||
## First increment (what task group 1 builds)
|
||||
`hunt/table.py`, `hunt/producers.py` with the four existing producers, `hunt/program.py` with the baseline
|
||||
signature equal to today's hunter prompt plus the hunks, `hunt/metric.py`, `--program` in `hunter_arm.py`, the
|
||||
scripted-LM tests, and one baseline run over the 12 pilot positives compared with the pilot's hunter records to
|
||||
prove the plumbing before any optimisation.
|
||||
@@ -0,0 +1,300 @@
|
||||
# Requirements Document
|
||||
|
||||
## Project Description (Input)
|
||||
|
||||
Maintainer, 2026-10-13: "the hunter would then need a dspy process now, so that we iteratively refine our
|
||||
llm integration. we can distinct positives from false positives. and dspy can mutate prompts with its
|
||||
statistical tools. the key is to produce enough information for the llm to use it correctly." Then:
|
||||
"threshold based assessments come later. right now the search is too bad. if I cut a threshold at 50:50
|
||||
that is coinflip optimization. what we need rather than that is an annotate model for the llm, with
|
||||
statistical ablation." And: "the answer to our problem is feature engineering + math." Before that:
|
||||
"proof should be a resort to face with certainty. before our search problem needs to be solved to reduce
|
||||
what we have to proof."
|
||||
|
||||
Who has the problem: the pre-push safety net. The valid benchmark pilot
|
||||
(`benchmarks/measurements/2026-10-12-valid-benchmark-pilot/NOTES.md`) measured the LLM tool hunter at
|
||||
5 of 12 verified positives found and 54 false alarms per 100 ordinary pushes after the decision-engine
|
||||
classifier. The hook as shipped found 0 of 12.
|
||||
|
||||
Current situation: the hunter's search prompt is a hand-written string, its family answer is free text,
|
||||
and it never sees the diff. Of 343 ordinary-push suspicions the classifier called `vulnerable`, 250 sit
|
||||
more than 10 lines from any changed line: the hunter re-reports the repo's old code on every push. Every
|
||||
precision gain so far was a hand rule or a threshold picked after looking at the misses, in-sample. A
|
||||
threshold on today's output is a coin flip dressed as a decision. The learning harness that compiles the
|
||||
decision-engine classifier (bootstrapped demonstrations, instruction proposals, grouped folds, a leak
|
||||
audit, paired A/B experiments with cluster bootstrap, a cached and replayable model caller) exists, but
|
||||
the hunter does not run on it, and the compile step accepts the best score on one validation split with
|
||||
no train-against-holdout check, no per-family regression check, and no prompt length cap.
|
||||
|
||||
What should change: the search gets better before anything is thresholded. The hunter receives an
|
||||
annotation model: a named, versioned set of facts about the push computed from the repository (changed
|
||||
hunks with line numbers, the enclosing function and its callers, entry-point evidence, source, sink and
|
||||
sanitizer roles, framework facts, def-use inside the function, guards and obligations, closed taxonomy
|
||||
families with the classifier's answer format). Which facts the model needs is decided by statistical
|
||||
ablation: each annotation is switched on and off in pre-registered, paired experiments over the same
|
||||
changes, and its effect on search recall over fix sites and on suspicions per ordinary push is reported
|
||||
per family with intervals. An annotation or a prompt change is kept only when it helps on held-out folds.
|
||||
Labels come from the benchmark's rule: a suspicion is a true positive when it lands on a verified fix
|
||||
site, a false positive when it sits on an ordinary push. Training data is sourced separately (about 100
|
||||
advisories through the fix-site catalog and verifier, about 40 accepted positives) with repository-level
|
||||
folds and the leak audit; the 252-change pilot stays held out and is only used to report the gain.
|
||||
Thresholds and operating points come after the search is good; this spec reports the whole recall
|
||||
against false-alarm curve and sets none. The proof arm runs only on what survives.
|
||||
|
||||
## Introduction
|
||||
|
||||
This spec turns the LLM hunter from a hand-written prompt into a measured search stage. It owns four
|
||||
things: the labelled data the loop learns from, the annotation model the hunter is given about a push,
|
||||
the ablation method that decides which annotations stay, and the rule by which a changed prompt or
|
||||
annotation set is kept or thrown away. It reports in the benchmark's own terms: recall, false alarms per
|
||||
100 pushes, precision, per family, with intervals. It sets no threshold.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: labelling hunter suspicions from fix-site positives and ordinary pushes; sourcing a
|
||||
training set separate from the held-out pilot; the annotation model and its producers; statistical
|
||||
ablation of annotations; the hunter as a versioned, replayable compiled program; the acceptance rule
|
||||
for a prompt, demonstration or annotation-set change; re-measuring the pilot and reporting the gain.
|
||||
- **Out of scope**: choosing confidence or scope thresholds and operating points (later, once the search
|
||||
is measured good); wiring the compiled hunter into the pre-push hook (`pre-push-safety-net`); the proof
|
||||
arm itself (`search-with-proof`, `proof-arm-opencode`); building new deterministic analyses such as a
|
||||
whole-repo call graph or taint engine (`engine-change-scoped-analysis` owns those; this spec consumes
|
||||
what exists); growing the benchmark beyond what this loop needs (`valid-detection-benchmark`).
|
||||
- **Adjacent expectations**: the fix-site catalog and verifier produce the labels; the benchmark scorer
|
||||
and metrics are the only scorekeepers; the decision-engine classifier stays as the second stage and is
|
||||
not retrained here; the earlier freeze on hunter changes ("no hunter prompt, tool or configuration may
|
||||
change before that measurement") is lifted by the pilot measurement, which is the baseline.
|
||||
|
||||
## Decision strategy, corrected 2026-10-14
|
||||
|
||||
The maintainer's finding after the second access-control screen: the annotations this loop selected and
|
||||
screened were software features, not security features. Counts of callers, entry points, calls, binds,
|
||||
function length, diff shape, language and framework describe the code around a change; none of them
|
||||
determines that the code is unsafe, and a decision model fitted on them learns the pool (the deletion count
|
||||
separates reversed fixes at 0.90) and generalises to nothing. "If you build a safety net and only send
|
||||
information unrelated to security, the expected result is nothing."
|
||||
|
||||
What decides safety for the access-control family is a property of a path, in order: an entry an attacker
|
||||
can reach, request-derived data reaching a protected operation along that path, and no required check
|
||||
dominating the operation by control flow. That object, the ordered trace with argument provenance and
|
||||
control-flow dominance, is specified and built in `access-control-prover` (requirements 9 and 10). From
|
||||
this date:
|
||||
|
||||
1. The hunter's input is the trace, not the feature table. Shape annotations may be rendered for context but
|
||||
are not decision features and the ablation shall not credit them as such.
|
||||
2. The pool measures; it never trains. No model is fitted on the pool's labels, no prompt is compiled on the
|
||||
pool, no threshold is tuned on it. The verdict is a rule over the trace, the same on every repository;
|
||||
the facts naming entries, operations and checks are closed data with a stated source.
|
||||
3. Where a judgement remains (is this entry meant to be public; is this operation protected somewhere the
|
||||
facts do not model), the judge reads the trace zero-shot with general security knowledge. The pool
|
||||
reports how often it is right; the unseen pilot says whether that holds elsewhere.
|
||||
4. The ablation's remaining use is selection, not fitting: which trace components the judge needs to see,
|
||||
with intervals, read with the same suspicion as any in-sample number.
|
||||
5. Requirements 3.3 and 4 below stand as written for the record of the first round; where they name shape
|
||||
annotations as the model's input, this section overrides them.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Labels from fix sites and ordinary pushes
|
||||
|
||||
**Objective:** As the maintainer, I want every hunter suspicion labelled true or false by the benchmark's
|
||||
own rule, so that the loop learns from facts and not from my reading of the misses.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a hunter suspicion names a path and line inside a verified fix-site function of a positive change,
|
||||
or within the benchmark scorer's location tolerance of it, and its family equals the change's family,
|
||||
the Learning Loop shall label it a true positive.
|
||||
2. When a hunter suspicion is raised on an ordinary push, the Learning Loop shall label it a false positive.
|
||||
3. When a hunter suspicion is raised on a positive change but outside every verified fix site of that
|
||||
change, the Learning Loop shall label it unknown and shall exclude it from training and scoring.
|
||||
4. The Learning Loop shall label at the level of the suspicion (path, line, family), and shall also derive
|
||||
the push-level outcome (caught, alarmed) with the benchmark scorer, so that both the search stage and
|
||||
the push-level metric are measurable from the same records.
|
||||
5. When a positive change is used for training, the Learning Loop shall record its fix-site verification
|
||||
record (model checks and human decision) next to the label, so that a label can be traced to its
|
||||
evidence.
|
||||
6. If a label's source cannot be traced to a verified fix site or a frozen ordinary draw, the Learning
|
||||
Loop shall reject it.
|
||||
|
||||
### Requirement 2: A training set apart from the held-out pilot
|
||||
|
||||
**Objective:** As the maintainer, I want the loop to learn on data the benchmark never reports on, so that
|
||||
the pilot number stays a measurement.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Learning Loop shall source its training positives through the fix-site catalog and verifier from
|
||||
advisories and repositories that are not in the held-out pilot, and shall record the sourcing run.
|
||||
2. The Learning Loop shall draw its training negatives as ordinary pushes from the training repositories
|
||||
only, at the benchmark's ratio of at least 20 ordinary pushes per positive change.
|
||||
3. The training set shall contain at least 40 accepted positive changes across at least two languages
|
||||
before the first ablation result is reported; a run on fewer is marked a dry run.
|
||||
4. While an ablation, compile or evaluation runs, the Learning Loop shall never read the held-out pilot's
|
||||
changes, labels or records, and every artifact shall record which data digests it did read.
|
||||
5. The Learning Loop shall split the training set by repository group into folds, so that no repository
|
||||
appears on both sides of a split.
|
||||
6. Before the first ablation, the Learning Loop shall run the leak audit over every annotation and shall
|
||||
report any annotation that separates positives from negatives on its own above the audit's threshold,
|
||||
so that an annotation that encodes the label is found before it is credited.
|
||||
7. If a training repository is later found in the held-out pilot or in any benchmark report, the Learning
|
||||
Loop shall mark every run that used it as contaminated and shall exclude those runs from any reported
|
||||
gain.
|
||||
|
||||
### Requirement 3: The annotation model
|
||||
|
||||
**Objective:** As the maintainer, I want the facts the hunter sees about a push to be a named, versioned
|
||||
set that each can be switched on or off, so that "enough information for the model" is something we can
|
||||
measure rather than guess.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Learning Loop shall define the annotation model as a versioned schema of named annotations, each
|
||||
with a producer, a bound on its size, and a digest, and shall record the schema version on every
|
||||
hunter run.
|
||||
2. The Learning Loop shall compute every annotation from the repository at the push's revisions and from
|
||||
the tool's own framework knowledge only; no annotation shall read a label, an advisory, a CVE or GHSA
|
||||
identifier, a fix commit, or security-worded commit text.
|
||||
3. The annotation model shall include at least: the changed hunks with head-side line numbers; the
|
||||
enclosing function's full text at head; the names and locations of that function's callers; entry-point
|
||||
evidence (an in-repo route, handler, command or public API that reaches the function); source, sink and
|
||||
sanitizer roles for the identifiers in the function; the framework facts that apply to the file;
|
||||
def-use evidence inside the function; the guards and obligations on the changed path; and the closed
|
||||
list of taxonomy families with a one-line definition each.
|
||||
4. When an annotation's producer cannot compute it for a push, the Learning Loop shall record the
|
||||
annotation as absent with the reason, and shall not substitute a default that could be read as a fact.
|
||||
5. The Learning Loop shall render the enabled annotations into the hunter's input in a fixed order with
|
||||
fixed labels, so that two runs with the same annotation set and the same push produce the same input.
|
||||
6. The Learning Loop shall require the hunter's family answer to come from the closed list; an answer
|
||||
outside the list is recorded as `unknown` and counts against the program.
|
||||
7. The Learning Loop shall require the hunter's answer in the same fixed structure the classifier uses
|
||||
(verdict, family, confidence, rationale, cited lines), and shall reject an answer whose cited lines
|
||||
fall outside the text it was shown.
|
||||
8. When the hunter reports a suspicion, the Learning Loop shall record whether it lies on a changed line,
|
||||
within 10 lines of one, or farther away, so that scope is a measured property of every suspicion and
|
||||
not a filter.
|
||||
|
||||
### Requirement 4: Statistical ablation decides what the model needs
|
||||
|
||||
**Objective:** As the maintainer, I want each annotation's worth measured by switching it off, so that the
|
||||
input the hunter gets is built from effects with intervals, not from intuition.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. Before an ablation runs, the Learning Loop shall register it: the annotation set under test, the
|
||||
annotations switched off, the folds, the metric, the sample size, the budget and the decision rule.
|
||||
2. The Learning Loop shall run every ablation arm over the same changes as its comparison arm, paired by
|
||||
change, with the same program, model and sampling, so that the only difference between arms is the
|
||||
annotation set.
|
||||
3. For each ablation, the Learning Loop shall report the paired effect on search recall over fix sites
|
||||
and on suspicions per ordinary push, per family and pooled, with a 95% interval from a bootstrap
|
||||
clustered by repository and a paired test on the per-change outcomes.
|
||||
4. The Learning Loop shall report, after a full ablation round, an importance table: for every
|
||||
annotation, its effect on recall and on suspicions per ordinary push when removed from the full set and
|
||||
when added to the baseline, with intervals and sample counts.
|
||||
5. The Learning Loop shall keep an annotation in the hunter's input only when its removal lowers search
|
||||
recall or raises suspicions per ordinary push on held-out folds with an interval that excludes zero,
|
||||
or when its removal has no measurable effect and it is needed by an annotation that does.
|
||||
6. If an annotation raises suspicions per ordinary push without raising recall, the Learning Loop shall
|
||||
drop it and shall record the result where the next round can see it.
|
||||
7. The Learning Loop shall not use a confidence cut, a scope cut or any other threshold on the hunter's
|
||||
output as the quantity being optimised; confidence is reported, not tuned, in this spec.
|
||||
8. The Learning Loop shall report the full recall against suspicions-per-push curve of each annotation
|
||||
set, so that a later operating-point decision has the whole curve and not one point of it.
|
||||
|
||||
### Requirement 5: The hunter as a compiled, replayable program
|
||||
|
||||
**Objective:** As the maintainer, I want every version of the hunter's prompt, demonstrations and
|
||||
annotation set to be an artifact I can name, diff, replay and roll back, so that an improvement is a fact
|
||||
and not a memory.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Learning Loop shall store each compiled hunter as a single content-addressed artifact holding the
|
||||
instruction, the demonstrations, the annotation schema version and enabled set, the model, the data
|
||||
digests, every scored configuration and the spend.
|
||||
2. When the hunter runs, the Learning Loop shall record the program id and the annotation set on every
|
||||
suspicion it emits.
|
||||
3. The Learning Loop shall route every hunter model call through a cached, metered caller, so that a
|
||||
rerun of the same program on the same inputs reproduces the same answers at no cost.
|
||||
4. The Learning Loop shall keep the scorer, the labels and the held-out data outside every surface the
|
||||
compile step may edit, so that a proposal cannot read its own answer key.
|
||||
5. If a run exhausts its budget before finishing, the Learning Loop shall write no program artifact and
|
||||
shall report the run as unfinished with the spend so far.
|
||||
6. The Learning Loop shall cap the instruction length per family, and shall reject a proposal that
|
||||
exceeds the cap regardless of its score.
|
||||
|
||||
### Requirement 6: Accepting or rejecting a change
|
||||
|
||||
**Objective:** As the maintainer, I want a prompt, demonstration or annotation-set change kept only when
|
||||
it helps on data it was not tuned on, so that the loop cannot overfit its way to a better number.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a candidate program is scored, the Learning Loop shall score it on the compile folds and on the
|
||||
held-out folds separately, per family, and shall report both.
|
||||
2. The Learning Loop shall accept a candidate only when its compile-fold and held-out-fold results are
|
||||
both non-negative against the current program and at least one is strictly positive.
|
||||
3. If any family's held-out result falls below its regression budget, the Learning Loop shall reject the
|
||||
candidate, whatever the aggregate shows.
|
||||
4. If a candidate ties the current program within the pre-registered tolerance, the Learning Loop shall
|
||||
reject it.
|
||||
5. The Learning Loop shall report every result with its sample count and a 95% interval, and shall not
|
||||
accept a candidate on a point estimate alone.
|
||||
6. The Learning Loop shall keep rejected candidates and their scores where the next proposal round can
|
||||
see them, and shall never show the held-out results to the proposer.
|
||||
7. When a candidate is accepted, the Learning Loop shall record the comparison (both programs' ids, both
|
||||
fold results, the decision rule) in the new program's artifact.
|
||||
8. The Learning Loop shall change one thing per round (one annotation, or one instruction, or one
|
||||
demonstration set), so that an accepted gain is attributable.
|
||||
|
||||
### Requirement 7: Two stages, reported for what each owns
|
||||
|
||||
**Objective:** As the maintainer, I want the search stage and the classifier stage measured separately,
|
||||
so that we fix the search problem before paying for proof.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Learning Loop shall report the search stage on recall over verified fix sites (did a suspicion land
|
||||
on the fix site with the right family) and on suspicions per ordinary push, per family.
|
||||
2. The Learning Loop shall report the classifier stage on precision over the suspicions the search stage
|
||||
emitted, and on the push-level false alarms per 100 ordinary pushes after it, per family.
|
||||
3. The Learning Loop shall report the combined push-level result as recall, false alarms per 100 pushes
|
||||
and precision with 95% intervals, in the benchmark's names and no coined ones.
|
||||
4. The Learning Loop shall report the gate distance on every run: the recall reached at 10 false alarms
|
||||
per 100 pushes and the false alarms at 90% recall, read off the curve, as a report and not as a tuned
|
||||
operating point.
|
||||
5. The Learning Loop shall not retrain or recompile the classifier stage within this spec; a classifier
|
||||
change is a separate experiment.
|
||||
|
||||
### Requirement 8: Measuring the gain on the pilot
|
||||
|
||||
**Objective:** As the maintainer, I want one number that says what the loop bought, measured on the data
|
||||
it never saw, so that the decision to put the hunter on pre-push rests on evidence.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When a compiled hunter is accepted, the Learning Loop shall run it once over the held-out pilot's 252
|
||||
changes with the same scorer and metrics as the pilot, and shall write the result next to the pilot's
|
||||
measurement.
|
||||
2. The Learning Loop shall report the gain as the before-and-after of search recall, suspicions per
|
||||
ordinary push, push-level recall, false alarms per 100 pushes and precision, per family, with
|
||||
intervals, against the pilot's recorded hunter numbers.
|
||||
3. The Learning Loop shall report, for the same run, the number of suspicions the proof arm would have to
|
||||
handle per 100 pushes, so that proof's workload is a measured count.
|
||||
4. The Learning Loop shall prove its instrument in every pilot run: for each change it shall record bytes
|
||||
read per changed file and the verified base and head, and shall mark a change with nothing readable as
|
||||
an instrument failure rather than a clean result.
|
||||
5. If a pilot run's instrument failures exceed 5% of changes, the Learning Loop shall report the run as
|
||||
invalid and shall not quote its numbers.
|
||||
6. The Learning Loop shall run the pilot at most once per accepted program, and shall record each run, so
|
||||
that the pilot is not used as a tuning surface.
|
||||
|
||||
### Requirement 9: Cost, secrets and unattended runs
|
||||
|
||||
**Objective:** As the operator, I want ablation and compile runs to stay inside a budget, keep the model
|
||||
key out of every record, and finish without me, so that the loop can run overnight on the research host.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The Learning Loop shall take a pre-registered budget in money and calls per run and shall stop at the
|
||||
first exceeded bound.
|
||||
2. The Learning Loop shall never write a model key, or an exception message that could carry one, into a
|
||||
record, a log or an artifact; an error is recorded by its type.
|
||||
3. When a run is interrupted, the Learning Loop shall resume from its recorded progress without repeating
|
||||
finished model calls.
|
||||
4. The Learning Loop shall write its outputs outside the repository checkout and shall keep each record
|
||||
once, so that an overnight run cannot fill the host's disk with duplicates.
|
||||
5. The Learning Loop shall report, at the end of every run, the spend, the calls, the replay hits and the
|
||||
wall time.
|
||||
@@ -0,0 +1,143 @@
|
||||
# Research and design decisions: hunter-learning-loop
|
||||
|
||||
## Summary
|
||||
- **Feature**: `hunter-learning-loop`
|
||||
- **Discovery scope**: Extension of the learn harness and the valid benchmark; one new runtime dependency (DSPy).
|
||||
- **Maintainer direction, 2026-10-13**: "the core we need to evolve is feature engineering ... that is the deciding
|
||||
factor here"; "labels for me are a pandas task"; DSPy for the hunter program first, statistics libraries second,
|
||||
no OpenAI SDK on its own ("unless dspy pulls it in"; it does, through litellm); lizard against its alternatives is worth
|
||||
pursuing; labels and folds are a pandas and scikit-learn task; Semgrep OSS is a good idea. Each of those is a
|
||||
feature producer or a feature consumer, and the feature table decides between them.
|
||||
- **Key findings**:
|
||||
- The pilot (benchmarks/measurements/2026-10-12-valid-benchmark-pilot) measured the hunter at 5 to 6 of 12 and 54
|
||||
false alarms per 100 pushes after the classifier; 250 of 343 "vulnerable" suspicions on ordinary pushes sit more
|
||||
than 10 lines from the diff. The hunter never sees the diff (01-flowcharts/U6-hunter-client.md).
|
||||
- The harness already has the pieces the loop needs: labels (`learn/labels.py`), repository folds and leak audit
|
||||
(`learn/folds.py`, `learn/leaks.py`), paired experiments with cluster bootstrap and McNemar
|
||||
(`learn/experiments.py`), the cached caller (`learn/program.py`), and the runner that joins hunter output to
|
||||
frozen changes (`benchmarks/valid/hunter_arm.py`). What it lacks is a feature table and a program runtime that
|
||||
can be optimised rather than hand-edited.
|
||||
- `source.facts_for` (P1, commit 062a7b7) is the first feature producer: language, enclosing function, span,
|
||||
callers, entry points, from the repository only.
|
||||
|
||||
## Research log
|
||||
|
||||
### DSPy as the program runtime
|
||||
- **Context**: requirements R3 to R6 need a signature with switchable inputs, demonstrations, instruction
|
||||
proposals, a scorer-driven compile, and metered, replayable model calls.
|
||||
- **Sources**: local install, `dspy==3.4.1` into the research venv (`uv pip install --dry-run dspy`: 42 packages,
|
||||
among them `litellm 1.105`, `openai 2.54`, `gepa 0.1.4`, `diskcache`, `json_repair`, `tenacity`, `pydantic`);
|
||||
API introspection of the installed package (`dspy.LM(model, cache=True, num_retries=3, ...)`,
|
||||
`dspy.ReAct(signature, tools, max_iters=20)`, `dspy.Predict`, `dspy.Signature` with `InputField`/`OutputField`,
|
||||
`dspy.Evaluate(devset, metric, num_threads)`, `dspy.BootstrapFewShot`, `dspy.MIPROv2(metric, auto=...)`,
|
||||
`dspy.GEPA(metric, auto, reflection_lm, candidate_selection_strategy="pareto")`, `dspy.track_usage()`,
|
||||
`dspy.Module.save(path)`).
|
||||
- **Findings**: `dspy.LM` reaches DeepSeek and OpenRouter through litellm with an OpenAI-compatible base URL and the
|
||||
key from the environment; the project's model id `deepseek-flash` is not in litellm's price table, so prices are
|
||||
registered from our table (`model/endpoint.py:55`). DSPy caches responses on disk (`diskcache`) keyed by the
|
||||
request; a run's cache directory is settable, so a run can be replayed from its own directory. `dspy.ReAct` is
|
||||
the tool-calling loop the hunter already is (read_file, grep_repo, find_refs). GEPA is the optimiser the
|
||||
learning-harness brief described (per-instance Pareto archive, reflective edits); MIPROv2 is the instruction and
|
||||
demo search. Neither applies our acceptance rule; that stays outside DSPy (R6).
|
||||
- **Implications**: the hunter program is written as a DSPy module from the start; the classifier
|
||||
(`learn/program.py`) stays as is for this spec (R7.5). The hook's urllib client is untouched: the hook has no LLM
|
||||
tier yet, and the core install stays PyYAML-only; DSPy is a research extra.
|
||||
|
||||
### The feature table (labels and features as a pandas task)
|
||||
- **Context**: R1, R3, R4 need one row per candidate location with named feature columns, a label, provenance, and
|
||||
the ability to switch columns off per ablation arm.
|
||||
- **Findings**: `learn/features.py` builds feature records as nested dicts and parts; `learn/labels.py` (979 lines)
|
||||
joins sources by hand; `benchmarks/unseen/score.py` joins records by id. All of it is group-by, merge and
|
||||
dedup work. pandas and scikit-learn (grouped folds, calibration, metrics) are what the maintainer expects here and are pulled
|
||||
by none of the current extras.
|
||||
- **Implications**: the loop's contract is a DataFrame (persisted as JSONL or parquet) with a fixed column schema;
|
||||
producers add columns, ablation arms drop columns, the program renders a row into its prompt. Existing dict-based
|
||||
builders are consumed, not rewritten, in this spec; migrating labels to pandas is part of the statistics item,
|
||||
not this one.
|
||||
|
||||
### Feature producers and the tools under evaluation
|
||||
- **Context**: the annotation model (R3) lists the facts; the maintainer wants the producers chosen by what gives
|
||||
the best features.
|
||||
- **Candidates**: `source.facts_for` (ours; P1), the semantic overlay facts (roles, def-use, obligations;
|
||||
`semantic/`), `mapping.analyze_entry_points`, lizard (function spans, nesting, cyclomatic complexity; pure
|
||||
Python), universal-ctags (spans across many languages; a binary), Semgrep OSS or Opengrep (pattern
|
||||
and taint findings as features; also a candidate to replace the quick-rule engine, a separate decision).
|
||||
- **Result, lizard (benchmarks/measurements/2026-10-13-span-engines/REPORT.md)**: on 610 pair files and 570 labelled
|
||||
functions, lizard matches our parser engine line for line on C, Python and Java (end-line difference 0.00,
|
||||
name-set Jaccard 0.97 to 1.00) and survives the 56 excerpts the cpp grammar rejects, but loses 6 of 44 labelled
|
||||
JavaScript functions to naming (`$` identifiers, `function anonymous`, nested named functions, assigned function
|
||||
expressions), gets TypeScript end lines wrong on 22% of labels (one by 81 lines), has no Perl reader, and is the
|
||||
slowest of the four engines. Verdict: keep ours; lizard's per-function cyclomatic complexity and nesting are a
|
||||
candidate feature column, not a span engine (its nesting correlates with `measure_nesting` at Spearman 0.6, so
|
||||
it is not a drop-in there either).
|
||||
- **Result, tree-sitter-language-pack**: the same tree-sitter, 503 of 503 spans identical with our parser engine,
|
||||
1.4x faster (it caches parsers), one 6 MB MIT wheel in place of six grammar wheels, and it adds Go, Ruby, PHP
|
||||
and Perl grammars that ours handles by regex today. Adopt it as the grammar packaging in `semantic/extra.py`;
|
||||
map `.c` to the c grammar first (20 of the 56 rejections clear); cache the parser. `tree-sitter-languages` is
|
||||
ABI-broken under tree-sitter 0.26; universal-ctags has no pip build.
|
||||
- **Result, Semgrep CE and Opengrep (benchmarks/measurements/2026-10-13-semgrep-brief/BRIEF.md)**: the engine is
|
||||
LGPL-2.1 and shelling out carries no obligation, but every Semgrep-maintained registry rule has been under the
|
||||
Semgrep Rules License since 2024-12-13 (internal use only, no distribution, not in competing products), so
|
||||
nothing but our own converted rules could ship. Measured here: 4.7 s startup floor and about 5 to 10 ms per
|
||||
file after it (the regex tier runs in-process under a second); converted regex and taint rules fire with
|
||||
traces for Python, PHP and JavaScript; C propagation through `strcpy` does not (propagators are Pro); Groovy has
|
||||
no parser; a Semgrep sanitizer silenced the `ORDER BY` shape our `quoted_only` flag refuses on purpose; guards,
|
||||
dispatch facts, obligations, shadow status and emit floors have no equivalent. A swap would retire about 1,250
|
||||
lines. Recommendation taken from the brief: option C first, a Semgrep arm on the valid pilot
|
||||
(`benchmarks/valid/semgrep_arm.py`, our rules converted, scored by the same scorer) before any decision on the
|
||||
quick tier; adopting it later means amending the "no second engine" rule in safety-net.md with that measurement.
|
||||
For this spec, Semgrep findings are a candidate feature column like any other producer.
|
||||
- **Research needed**: Semgrep registry rule licence terms for an open-source tool.
|
||||
|
||||
### Training data apart from the pilot
|
||||
- **Context**: R2. The pilot (252 changes, 12 positives) is held out.
|
||||
- **Findings**: `benchmarks/valid/catalog.py` and `verify.py` produce fix-site positives from OSV advisories in
|
||||
about 12 accepted per 32 advisories; the used-set guard (`benchmarks/unseen/*`) excludes training-only sources.
|
||||
`learn/folds.py` groups by repository; `learn/leaks.py` audits per-signal separation.
|
||||
- **Implications**: about 100 advisories give about 40 positives; negatives at 20 per positive from the same
|
||||
repositories; folds by repository; the pilot is never read by the loop.
|
||||
|
||||
## Architecture pattern evaluation
|
||||
| Option | Description | Strengths | Risks | Notes |
|
||||
|---|---|---|---|---|
|
||||
| A. Extend `learn/program.py` with a hunter signature | one harness, our caller and cache | no new dependency | the compile loop has no train-vs-holdout rule, no tool loop, no optimiser beyond demos+instructions; everything the maintainer asked for (statistical mutation of prompts) is to build | rejected |
|
||||
| B. New `hunt/` package on DSPy, feature table as the contract, harness reused for labels, folds, leaks, paired analysis | the optimisers, the tool loop, caching and usage come with the library; our code is the feature producers, the table, the acceptance rule, the scoring | one heavy research dependency; two caches (DSPy's and `Caller`'s) until the classifier migrates | selected |
|
||||
| C. Rewrite the classifier onto DSPy too | one runtime | large rewrite of a parked line | later, if B earns it |
|
||||
|
||||
## Design decisions
|
||||
### Decision: DSPy runs the hunter program; the acceptance rule stays ours
|
||||
- Alternatives: A above; a hand-written optimiser.
|
||||
- Selected: `dspy.ReAct` over a `HuntSignature` whose inputs are the rendered feature row plus the changed hunks;
|
||||
`dspy.GEPA` or `MIPROv2` propose candidates; `dspy.Evaluate` scores them on compile folds with our metric; the
|
||||
candidate is accepted only by our rule on held-out folds (R6), implemented outside DSPy.
|
||||
- Trade-offs: DSPy's cache is request-keyed, not content-addressed; a run pins its cache directory and copies it
|
||||
with the run record. Cost comes from `dspy.track_usage()` with our prices registered in litellm.
|
||||
|
||||
### Decision: the feature table is the contract between producers, ablation and the program
|
||||
- Selected: one row per (change, path, line) candidate with columns `feature.<producer>.<name>`, `label`,
|
||||
`provenance.*`, `fold`; producers are functions `(root, change, path, line) -> dict`; an ablation arm is a
|
||||
column mask recorded in the manifest; the program sees only the unmasked columns, rendered in a fixed order.
|
||||
- Trade-offs: a table per run is larger than nested records; it is also what pandas, scipy and the maintainer read.
|
||||
|
||||
### Decision: producers are a fixed list, switched by name, no registry
|
||||
- `source.facts_for`, `semantic` facts (roles, def-use, obligations), `mapping` entry points, `diff` hunks; lizard and
|
||||
Semgrep/Opengrep join the list only after their evaluation reports.
|
||||
|
||||
### Decision: objective and metric
|
||||
- Search stage: recall over verified fix sites (a suspicion within the scorer's tolerance of the fix site with the
|
||||
right family) and suspicions per ordinary push; the DSPy metric returns the per-change score and, for GEPA, a
|
||||
text feedback naming the missed site or the off-diff suspicions. No threshold is tuned (R4.7); the curve is
|
||||
reported (R4.8).
|
||||
|
||||
## Risks and mitigations
|
||||
- DSPy or litellm upgrades change behaviour: pin `dspy==3.4.*`, record versions in every run artifact.
|
||||
- A producer reads a label by accident: producers take the repository and a location only; a test feeds a
|
||||
repository with a planted advisory id and asserts no feature carries it (R3.7).
|
||||
- Train-on-test: the pilot path is denied to the loop by construction (the runner refuses the pilot manifest
|
||||
digest), and every artifact lists the data digests it read (R2.4).
|
||||
- Cost: budgets per run (R9.1); DSPy cache on; `--replay-only` reruns at no cost.
|
||||
|
||||
## References
|
||||
- DSPy 3.4.1 (installed; https://dspy.ai), GEPA (https://github.com/gepa-ai/gepa), litellm pricing registration.
|
||||
- PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md, U7-learn-memory-tasks.md; 02-duplication-report.md D1, D4.
|
||||
- benchmarks/measurements/2026-10-12-valid-benchmark-pilot/NOTES.md (numbers and the filter sweep).
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"feature_name": "hunter-learning-loop",
|
||||
"created_at": "2026-10-13T09:05:00+02:00",
|
||||
"updated_at": "2026-10-13T12:20:00+02:00",
|
||||
"language": "en",
|
||||
"phase": "tasks-generated",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true,
|
||||
"note": "maintainer 2026-10-13: '4 first' (DSPy for the hunter program)"
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": false
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": false
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": false
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# Implementation Plan
|
||||
|
||||
- [x] 1. Foundation: the feature table and its producers
|
||||
- [x] 1.1 Create the feature table with a versioned column schema, masks and digests
|
||||
- One row per candidate location (change, path, line) with key, label, feature, provenance and fold columns
|
||||
- Rows read and write as JSONL and parquet; a table digest covers rows and schema version
|
||||
- A mask drops named feature columns and is recorded with the table it was applied to
|
||||
- Done when a round-trip test shows the same digest after write and read, and a masked table lists its mask
|
||||
- _Requirements: 3.1, 3.5, 4.2, 5.1_
|
||||
- [x] 1.2 Produce the four existing annotations from the repository only (P)
|
||||
- Diff hunks at head with on-changed-line and distance; enclosing function, span, callers, entry points from the source facts; roles, def-use and obligations from the semantic facts; the closed family list with one-line definitions
|
||||
- An annotation a producer cannot compute is recorded as absent with a reason, never defaulted
|
||||
- Done when a fixture repository yields every column, and a repository with a planted advisory id and a security-worded commit message yields no feature carrying either
|
||||
- _Requirements: 3.2, 3.3, 3.4, 3.7, 3.8_
|
||||
- _Boundary: producers_
|
||||
- [x] 1.3 Label the rows by the benchmark's rule (P)
|
||||
- Fix-site rows of positives label 1 with the change's family and fix span; ordinary-push rows label 0; other rows on a positive label null and are excluded from scoring
|
||||
- Each positive row carries the verification record reference next to the label; a row whose source cannot be traced is rejected
|
||||
- Done when a fixture manifest with one positive and two ordinary pushes produces the expected labels and the null rows are excluded
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
|
||||
- _Boundary: FeatureTable, training_set.py_
|
||||
- [x] 1.4 Install the research extra and pin the program runtime
|
||||
- Optional dependency group for the loop with the program runtime and the table library, pinned to a minor version; the core install stays unchanged
|
||||
- Done when the extra installs into the research environment and the core test suite collects without it
|
||||
- _Requirements: 5.1, 9.5_
|
||||
|
||||
- [x] 2. Core: the hunter as a program
|
||||
- [x] 2.1 Define the hunt signature and the program with the three repository tools
|
||||
- Inputs are the rendered unmasked feature columns plus the hunks; the output is the classifier's answer structure with a family from the closed list and cited lines inside the shown text
|
||||
- The tool loop is bounded by the run budget; an answer outside the vocabulary or citing unseen lines is recorded unknown
|
||||
- Done when a scripted model drives the program through the tools on a fixture and the parsed findings match the fixture's expectation
|
||||
- _Requirements: 3.5, 3.6, 5.1, 5.2, 5.4_
|
||||
- [x] 2.2 Score a change: the search metric with feedback (P)
|
||||
- A suspicion within the scorer's tolerance of the fix site with the right family counts as a hit; ordinary-push suspicions are counted per push; the feedback text names the missed site or the off-diff suspicions
|
||||
- Done when the metric reproduces the pilot's hit list for the 12 positives from their recorded suspicions
|
||||
- _Requirements: 1.4, 4.3, 7.1, 7.2_
|
||||
- _Boundary: search_metric_
|
||||
- [x] 2.3 Record a run as a content-addressed artifact (P)
|
||||
- Program id, schema version, mask, data digests, every scored configuration, spend, calls, replay hits, wall time, library versions; the saved program and its cache directory travel with it
|
||||
- A run that exhausts its budget writes no artifact and reports the spend so far
|
||||
- Done when two identical runs produce the same artifact id and a budget-exhausted run leaves only the partial report
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.5, 9.1, 9.2, 9.5_
|
||||
- _Boundary: RunArtifact_
|
||||
|
||||
- [x] 3. Integration: the runner and the baseline
|
||||
- [x] 3.1 Run the program through the existing hunter-arm runner
|
||||
- The runner takes a program artifact and a mask, computes the table rows for each change at head, renders the inputs, runs the program and writes the same record shape as today, resumable by change id, input digest, program id and mask
|
||||
- The pilot manifest is refused by digest unless the run is explicitly a pilot report run
|
||||
- Done when a run over a five-change fixture manifest with the scripted model writes records the existing scorer accepts
|
||||
- _Requirements: 2.4, 8.4, 9.3, 9.4_
|
||||
- [x] 3.2 Prove the plumbing: baseline program on the 12 pilot positives
|
||||
- The baseline instruction equals today's hunter prompt plus the hunks; one live, budgeted run over the 12 positives only, as a pilot report run
|
||||
- Done when the search metric over the new records is compared with the pilot's recorded hunter hits and the difference is explained in the measurement record
|
||||
- _Requirements: 8.1, 8.4, 8.5, 8.6_
|
||||
- _Depends: 2.2, 2.3_
|
||||
|
||||
- [ ] 4. Core: the training set, folds and ablation
|
||||
- [ ] 4.1 Source the training set apart from the pilot
|
||||
- About one hundred advisories through the fix-site catalog and verifier, pilot repositories excluded, at least 40 accepted positives across two languages, 20 ordinary pushes per positive from the same repositories; the sourcing run is recorded
|
||||
- Done when the frozen training manifest lists its positives, negatives, languages and the pilot exclusion check
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
- [x] 4.2 Fold by repository and audit every annotation for leaks
|
||||
- Compile folds and held-out folds by repository group; the leak audit runs over every feature column and reports any that separates positives from negatives on its own
|
||||
- Done when the folds are disjoint by repository and the audit report lists each column with its separation score
|
||||
- _Requirements: 2.5, 2.6_
|
||||
- [x] 4.3 Register and run a paired ablation
|
||||
- A manifest fixes the table digest, the masks, the folds, the metric, the sample size, the budget and the decision rule before anything runs; each arm runs over the same changes with the same program and sampling
|
||||
- Results per arm carry the paired effect on search recall and suspicions per ordinary push, per family and pooled, with intervals from the repository-clustered bootstrap and a paired test
|
||||
- Done when an ablation over a fixture manifest with the scripted model yields two arms, the paired statistics, and the full recall-against-suspicions curve
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.7, 4.8_
|
||||
- [x] 4.4 Build the importance table and apply the keep/drop rule
|
||||
- For every annotation, its effect when removed from the full set and when added to the baseline, with intervals and counts; an annotation stays only on a held-out effect whose interval excludes zero or when a kept annotation needs it; one that raises suspicions without raising recall is dropped and recorded
|
||||
- Done when the table is written for a fixture ablation and the rule's decisions match the fixture's expectations
|
||||
- _Requirements: 4.4, 4.5, 4.6_
|
||||
- [x] 4.5 Contamination check
|
||||
- A training repository later found in the pilot or in a benchmark report marks every run that used it as contaminated and excludes those runs from any gain
|
||||
- Done when a fixture run is marked contaminated after its repository is added to a report list
|
||||
- _Requirements: 2.7_
|
||||
|
||||
- [x] 5. Core: proposals and acceptance
|
||||
- [x] 5.1 Propose candidates with the program runtime's optimisers
|
||||
- One change per round: an instruction proposal, a demonstration set, or a mask; proposals score on compile folds with the search metric; the proposer never sees held-out results; rejected candidates stay visible to the next round
|
||||
- Done when a round on a fixture produces a candidate with its compile-fold score and the proposer's inputs are shown to exclude held-out data
|
||||
- _Requirements: 5.4, 6.6, 6.8_
|
||||
- [x] 5.2 Accept or reject by the rule
|
||||
- Compile-fold and held-out results per family; accepted only when both are non-negative, one strictly positive, no family below its regression budget, instruction under the length cap; ties rejected; the comparison is recorded in the new artifact
|
||||
- Done when every clause of the rule has a synthetic fold-result test that flips the decision
|
||||
- _Requirements: 5.6, 6.1, 6.2, 6.3, 6.4, 6.5, 6.7_
|
||||
|
||||
- [ ] 6. Validation: the pilot gain
|
||||
- [x] 6.1 Run an accepted program once over the held-out pilot and report the gain
|
||||
- Same scorer and metrics as the pilot; before-and-after of search recall, suspicions per push, push-level recall, false alarms per 100 and precision, per family, with intervals; the proof arm's workload per 100 pushes; the gate distance read off the curve; the run recorded next to the pilot's measurement, at most once per accepted program
|
||||
- Done when the measurement record exists with the before-and-after table and the instrument proof for every change
|
||||
- _Requirements: 7.3, 7.4, 7.5, 8.1, 8.2, 8.3, 8.6_
|
||||
@@ -1,4 +1,4 @@
|
||||
# Brief: ir-consolidation — stop maintaining an IR we did not need, and adopt one we do
|
||||
# Brief: ir-consolidation, stop maintaining an IR we did not need, and adopt one we do
|
||||
|
||||
> **Status 2026-10-02:** dormant. Brief only and unapproved. The subtraction it asks for still needs a measured recall comparison before the flat IR goes.
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
Two different questions live inside that, and they have opposite answers. Keeping them apart is most of the
|
||||
value of this brief.
|
||||
|
||||
## Question 1 — the source IR we already replaced and never retired
|
||||
## Question 1, the source IR we already replaced and never retired
|
||||
|
||||
`semantic/` is **3,218 lines, 16% of the codebase**: a hand-written IR (`ir.py`, `cst.py`) and a flat taint
|
||||
engine (`taint.py`), plus the overlay that runs them. `model-grounded-detection` measured that IR against
|
||||
Joern's CPG and the result was decisive — **2.2% entailment against 41.3%** — which is why the CPG was
|
||||
Joern's CPG and the result was decisive, **2.2% entailment against 41.3%**, which is why the CPG was
|
||||
adopted for the model layer. The flat IR was never removed, and still runs as a proposer in every scan.
|
||||
|
||||
So the outgrowth is not something we are about to add. It is something already carried.
|
||||
|
||||
**Vine and P-Code do not help here.** Both are *binary* IRs — Vine lifts executables (BitBlaze, UC Berkeley,
|
||||
**Vine and P-Code do not help here.** Both are *binary* IRs. Vine lifts executables (BitBlaze, UC Berkeley,
|
||||
GPL-2.0, last significant activity 2009), and P-Code lifts machine code. Our source languages are Python,
|
||||
PHP, JavaScript and Java, and the source IR we should adopt is the one we already have. The work is
|
||||
subtraction, not substitution:
|
||||
@@ -32,16 +32,16 @@ subtraction, not substitution:
|
||||
|
||||
A candidate ~1,000-line deletion with a number attached, which is the only kind worth doing.
|
||||
|
||||
## Question 2 — the abstraction we are missing, where an existing IR is exactly right
|
||||
## Question 2, the abstraction we are missing, where an existing IR is exactly right
|
||||
|
||||
`c/memory` has produced **zero true positives** and three distinct classes of false one. The reason is
|
||||
structural and now stated in the report (Req 5.6): it models unbounded string CALLS, while real C overflows
|
||||
are *arithmetic* — an index, a pointer offset, a `memcpy` whose size is computed. Every one of libpng's is,
|
||||
are *arithmetic* (an index, a pointer offset, a `memcpy` whose size is computed). Every one of libpng's is,
|
||||
and its library code never calls a modelled string API at all.
|
||||
|
||||
"Can this expression exceed this allocation" is a value question, not a reachability one. **Rolf Rolles'
|
||||
GhidraPAL carries a three-valued-logic abstract interpreter over Ghidra P-Code** — each bit definite 0,
|
||||
definite 1, or unknown, with abstract transformers for `INT_ADD`, `INT_MULT`, `INT_AND`, the comparisons —
|
||||
GhidraPAL carries a three-valued-logic abstract interpreter over Ghidra P-Code**, with each bit definite 0,
|
||||
definite 1, or unknown, and with abstract transformers for `INT_ADD`, `INT_MULT`, `INT_AND`, the comparisons,
|
||||
validated against concrete execution in its own tests. That is precisely the shape of analysis the C family
|
||||
lacks, and it exists.
|
||||
|
||||
@@ -55,13 +55,13 @@ lacks, and it exists.
|
||||
|
||||
**angr is the strongest candidate and Vine is not one.** Three things decide it:
|
||||
|
||||
* **It is Python.** This codebase is Python with `dependencies = []` in the core, and every heavyweight
|
||||
* It is Python. This codebase is Python with `dependencies = []` in the core, and every heavyweight
|
||||
capability so far has been reached across a subprocess boundary rather than imported. angr fits that
|
||||
pattern as an extra, the way `harnessx` and the tree-sitter grammars already do.
|
||||
* **Value-set analysis is the abstraction we are missing**, by name. "Can this index exceed this allocation"
|
||||
* Value-set analysis is the abstraction we are missing, by name. "Can this index exceed this allocation"
|
||||
is a VSA question, and VSA is abstract interpretation rather than SMT -- which is the choice
|
||||
`model-grounded-detection` already made and gave its reasons for.
|
||||
* **BSD-2-Clause**, against Vine's GPL-2.0. This project has a standing rule about never vendoring code
|
||||
* BSD-2-Clause, against Vine's GPL-2.0. This project has a standing rule about never vendoring code
|
||||
without a stated licence, and a GPL dependency is a decision about the whole tool rather than about one
|
||||
analysis.
|
||||
|
||||
@@ -71,19 +71,19 @@ thing to read before writing any abstract domain here, whether the platform unde
|
||||
|
||||
It is also the honest answer to a decision this project already took. `model-grounded-detection` chose
|
||||
abstract interpretation over SMT, and chose to build the model from code because execution cannot be
|
||||
assumed. A bit-level abstract interpreter over P-Code is *still abstract interpretation* and still static —
|
||||
what changes is the artifact: it needs a **built** library rather than its source.
|
||||
assumed. A bit-level abstract interpreter over P-Code is *still abstract interpretation* and still static.
|
||||
What changes is the artifact: it needs a **built** library rather than its source.
|
||||
|
||||
## What this brief must decide
|
||||
|
||||
1. Whether the source-IR subtraction is safe, measured rather than assumed.
|
||||
2. Whether the C arithmetic class is worth a second pipeline at all, or whether `c/memory` stays scoped to
|
||||
string misuse and says so — which is where task 2.16 left it, deliberately.
|
||||
string misuse and says so, which is where task 2.16 left it, deliberately.
|
||||
3. If it is worth it: **angr** first, on language, licence and the fact that value-set analysis is the
|
||||
named abstraction -- with GhidraPAL read as the worked example of a bit-precise domain rather than as a
|
||||
platform. Vine is dormant since 2009 and is not a candidate.
|
||||
4. What a second pipeline costs in the one currency this project has been strict about — **not lines, but
|
||||
the number of things that can silently return nothing.** This session found three separate quiet failures
|
||||
4. What a second pipeline costs in the one currency this project has been strict about,
|
||||
**the number of things that can silently return nothing rather than lines.** This session found three separate quiet failures
|
||||
in one PHP scan. A binary pipeline adds a build step, a lifter, and a domain, each of which can produce
|
||||
an empty result that looks like a clean one.
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ experiments.
|
||||
|
||||
Six facts from the repository and the recorded results shape the design:
|
||||
|
||||
- **The deployment target is a repository nobody trained on.** Maintainer: "the tool will never run against the
|
||||
- The deployment target is a repository nobody trained on. Maintainer: "the tool will never run against the
|
||||
test cases it's being trained with. it's purpose is a repo safety net." (Req 7). The primary unit is a candidate
|
||||
in changed code (`ousast pre-push`, base..head), and whole-repository `scan` is the secondary mode. Every
|
||||
reported number therefore comes from folds grouped by repository across all corpora. Features are
|
||||
repository-agnostic by an allow-list. Fit to a user's own repository is an opt-in layer outside the reported
|
||||
numbers.
|
||||
- **Labels are scarce, and full plane signals exist on 43 rows.** The only recorded plane run with
|
||||
- Labels are scarce, and full plane signals exist on 43 rows. The only recorded plane run with
|
||||
verify/agree/tie-break is `validation-46`: 15 cases, 43 candidates after triage, 19 of them with
|
||||
`site_match` true, all from population v2
|
||||
(`~/ousast-results/plane/validation-46/*-final/agreed.json`). The pair corpus is larger (427 pairs and 452
|
||||
@@ -30,19 +30,19 @@ Six facts from the repository and the recorded results shape the design:
|
||||
classifier over verify signals cannot be evaluated today. The design starts from the signals every labelled row
|
||||
can have, harvests the rest at a stated cost, and accepts that data will stay small: the engine learns by
|
||||
memory and compilation, not by fitting parameters.
|
||||
- **The core install is PyYAML only** (`pyproject.toml:11`). numpy, scipy and scikit-learn are not installed in
|
||||
- The core install is PyYAML only (`pyproject.toml:11`). numpy, scipy and scikit-learn are not installed in
|
||||
`.venv`. Retrieval (a Gower distance and a cosine over at most 40 vectors) and calibration (two-parameter Platt)
|
||||
stay pure Python; the model calls go through the plane's metered client (section 4).
|
||||
- **`pre-push` already has the decision seam.** `admit_candidates` (`push/policy.py:879`) applies one evidence
|
||||
- `pre-push` already has the decision seam. `admit_candidates` (`push/policy.py:879`) applies one evidence
|
||||
bar to `CandidateDelta`s with `novelty` (`push/policy.py:61-74`). Its capability registry is empty by design
|
||||
(`push/policy.py:802-823`: "No shipped declaration is enabled"), so `pre-push` blocks nothing today. The
|
||||
engine's operating point replaces the capability-eligibility reasons, and the structural reasons stay.
|
||||
- **The memory store is the system of record and has the needed shape.** Rows are keyed by repository and pin,
|
||||
- The memory store is the system of record and has the needed shape. Rows are keyed by repository and pin,
|
||||
with a closed `KINDS` tuple (`plane/memory.py:52`). There are two backends (FileStore, S3Store) behind one
|
||||
interface (`plane/memory.py:176-291`), content-addressed blobs (`put_facts`, `plane/memory.py:249-255`), and a
|
||||
train-on-test `Guard` (`improve/memory.py:95-133`). Features, labels, decisions, model artifacts and experiment
|
||||
outcomes become new row kinds and blob prefixes. No second store is added.
|
||||
- **Hand-written framework knowledge is embedded in the detectors** (Req 8). Examples are WordPress `$wpdb`,
|
||||
- Hand-written framework knowledge is embedded in the detectors (Req 8). Examples are WordPress `$wpdb`,
|
||||
hooks and `maybe_unserialize` in `ruleset/semantic/php.toml` and `ruleset/php/rules.toml`, and Flask and
|
||||
Django names in `ruleset/semantic/python.toml`. Section 2 tags each entry. Priors become optional features
|
||||
that are off by default. Framework roles are inferred per repository instead.
|
||||
@@ -548,7 +548,7 @@ If the pool cannot satisfy a minimum, take what exists and record `balance: "sho
|
||||
**The evaluation boundary (Req 3.4, 7.1).** `learn/retrieve.py: eligible(example, target, fold)` is the only way
|
||||
an example enters a prompt, for retrieved examples and for the compiled demonstrations alike:
|
||||
|
||||
- `example.group` is not in the fold's evaluation groups (all of them, not just the target's), and not the
|
||||
- `example.group` is not in the fold's evaluation groups (every evaluation group, including the target's), and not the
|
||||
target's group after the CVE/alias merge (`Groups`, `learn/labels.py:182`);
|
||||
- leave-one-source-out: `example.source` is not the held-out source;
|
||||
- leave-one-framework-out on F: `F not in example.frameworks`; a compiled demonstration tagged F is swapped for its
|
||||
@@ -669,23 +669,23 @@ million. Token sizes are estimates to be replaced by the smoke run's measured `u
|
||||
| candidate (80-line excerpt ~1,100, signals and roles ~300) | ~1,400 | no |
|
||||
| output (JSON, rationale <= 3 sentences) | ~200 | -- |
|
||||
|
||||
- **Per candidate, `k = 1`**: 2,750 x 0.014 + 5,950 x 0.44 + 200 x 1.32 per million = **$0.0029**.
|
||||
- **Each extra sample** (same prompt, fully cached): 8,700 x 0.014 + 200 x 1.32 per million = **$0.0004**. So
|
||||
- Per candidate, `k = 1`: 2,750 x 0.014 + 5,950 x 0.44 + 200 x 1.32 per million = **$0.0029**.
|
||||
- Each extra sample (same prompt, fully cached): 8,700 x 0.014 + 200 x 1.32 per million = **$0.0004**. So
|
||||
`k = 5` is **$0.0045** with cache hits, and $0.0146 if the cache does not hit (the smoke run decides the default).
|
||||
- **Embedding**: $0.000024 per candidate; the whole store about $0.10 once.
|
||||
- **Per compile**: bootstrap 150 x $0.0029 = $0.44; proposals ~$0.01; scoring 600 x $0.0029 = $1.74; total
|
||||
- Embedding: $0.000024 per candidate; the whole store about $0.10 once.
|
||||
- Per compile: bootstrap 150 x $0.0029 = $0.44; proposals ~$0.01; scoring 600 x $0.0029 = $1.74; total
|
||||
**~$2.2, ceiling $3.00**.
|
||||
- **Per full evaluation** of one program, evaluated families only (475 verified rows outside memory,
|
||||
- Per full evaluation of one program, evaluated families only (475 verified rows outside memory,
|
||||
config_secrets and unknown) plus a 200-unit benign-delta sample for false BLOCKs:
|
||||
- outer folds: 675 candidates, **$2.0 at k = 1, $3.0 at k = 5** (cached);
|
||||
- leave-one-source-out: 475 more, **$1.4 / $2.1**;
|
||||
- leave-one-framework-out: $0 today (no framework at the floor);
|
||||
- total **~$3.4 (k = 1) to $5.2 (k = 5)**, ceiling $7.
|
||||
- **Learning curves** (section 7): 6 memory-size points + 3 source-count points on a fixed 200-candidate subset at
|
||||
- Learning curves (section 7): 6 memory-size points + 3 source-count points on a fixed 200-candidate subset at
|
||||
`k = 1`: 9 x 200 x $0.0029 = **~$5.2**, ceiling $7.
|
||||
- **A program-variant A/B** on 300 paired candidates: arm A replays cached responses from the evaluation ($0);
|
||||
- A program-variant A/B on 300 paired candidates: arm A replays cached responses from the evaluation ($0);
|
||||
arm B costs **$0.9 (k = 1) to $1.35 (k = 5)**.
|
||||
- **Deployment**: `pre-push` with ~5 candidates is ~$0.02 per push at `k = 5`; a plane Run case with ~30 candidates
|
||||
- Deployment: `pre-push` with ~5 candidates is ~$0.02 per push at `k = 5`; a plane Run case with ~30 candidates
|
||||
~$0.14.
|
||||
|
||||
**Against the balance.** DeepSeek was ~$19.6 before the harvest; the harvest's verify passes are ~$6 expected and
|
||||
@@ -807,10 +807,10 @@ flagged in any pass as 17 for the plane vs 19 for the reference, of 20. The plan
|
||||
(`plane/tasks/verify.py:1-9, 164`, `CALLERS_CAP = 8` at :34), and the comparison is confounded (other changes,
|
||||
stochastic passes, v2 is where it was seen).
|
||||
|
||||
- **Arms.** A is `verify` as today. B is `verify` with `OUSAST_VERIFY_CALLERS=off`, a new switch (about 3 lines)
|
||||
- Arms. A is `verify` as today. B is `verify` with `OUSAST_VERIFY_CALLERS=off`, a new switch (about 3 lines)
|
||||
that makes `callers_line` return empty. Same model (`deepseek-flash`), `PER_HUNT = 6`, `MAX_STEPS = 6`, same
|
||||
candidates, same `facts.json`, same runner image digest, two passes (a, b) per arm.
|
||||
- **Units: the development corpus, not v2** (v2 generated the hypothesis):
|
||||
- Units: the development corpus, not v2 (v2 generated the hypothesis):
|
||||
- population v1, 11 cases, whose engine scans are recorded under `~/ousast-results/independent-v1/scans/` (44
|
||||
scans);
|
||||
- pmpro, MediaWiki, WP Statistics (dev-php);
|
||||
@@ -821,15 +821,15 @@ stochastic passes, v2 is where it was seen).
|
||||
model-free and frozen in the units file: per pin, the fix-changed functions, plus the top engine- and
|
||||
quick-alert functions, up to 8 candidates per pin, on the vulnerable and the fixed pin. Only units where arm A's
|
||||
callers line is non-empty are *treated*. The primary analysis is restricted to them, and the rest are reported.
|
||||
- **Metric.** Primary: the per-positive-unit rate of "flagged in any pass" (the 17-vs-19 metric), with positives
|
||||
- Metric. Primary: the per-positive-unit rate of "flagged in any pass" (the 17-vs-19 metric), with positives
|
||||
from the label builder (fix-range functions on the vulnerable pin). Secondary: the flag rate on fixed-pin
|
||||
negatives (false flags), and usd per candidate.
|
||||
- **Sample size and MDE.** About 55 positive units across 41 repositories (about 28 dev + 27 pointer pairs). With
|
||||
- Sample size and MDE. About 55 positive units across 41 repositories (about 28 dev + 27 pointer pairs). With
|
||||
a discordance rate of about 0.15, alpha 0.05 and power 0.8, the paired MDE is `(1.96 + 0.84) * sqrt(0.15) /
|
||||
sqrt(55)`, about **0.15**. The observed gap of 0.10 needs about 115 positive units, so an inconclusive result is
|
||||
the likely and honest outcome. It is registered as such in advance, and a follow-up needs new units, not a
|
||||
continuation.
|
||||
- **Budget.** Hunts per pass per arm: 14 dev repositories x 2 pins x up to 3 files (84) + 27 pairs x 2 sides (54)
|
||||
- Budget. Hunts per pass per arm: 14 dev repositories x 2 pins x up to 3 files (84) + 27 pairs x 2 sides (54)
|
||||
= 138. Four arm-passes give 552 hunts x $0.014, **about $7.7 expected**. The ceiling is **$15 total, $7.5 per
|
||||
arm**, enforced by the task budgets (`plane/budget.py`). Candidate generation reuses the recorded v1 engine
|
||||
scans. For dev-php it runs the engine once per pin on the host, one container at a time, from a frozen source
|
||||
@@ -1111,30 +1111,30 @@ Each commit is gated on the full suite's own exit code, `ruff` and `mypy`. None
|
||||
commit 9 adopts a program, and without an adopted program every path is today's. Steps that spend model money
|
||||
start only after the maintainer confirms their budget against the balance read just before.
|
||||
|
||||
1. **Schema and features** (1.1-1.3, 7.2) -- done.
|
||||
2. **Framework tags** (8.2, 8.4) -- done.
|
||||
3. **Roles** (8.1) -- done.
|
||||
4. **Labels** (2.1-2.3, 7.1) -- done.
|
||||
5. **Harvest** (1.1, data) -- in progress: verify a/b on the verify-family pairs and the development cases (about
|
||||
1. Schema and features (1.1-1.3, 7.2) -- done.
|
||||
2. Framework tags (8.2, 8.4) -- done.
|
||||
3. Roles (8.1) -- done.
|
||||
4. Labels (2.1-2.3, 7.1) -- done.
|
||||
5. Harvest (1.1, data) -- in progress: verify a/b on the verify-family pairs and the development cases (about
|
||||
$6, ceiling $10), model roles on the candidate files (ceiling $10), engine features on the host.
|
||||
6. **Program, memory, compile, first evaluation** (3.1-3.6, 5.1-5.2, 7.3-7.4, 8.3): excerpts and the example
|
||||
6. Program, memory, compile, first evaluation (3.1-3.6, 5.1-5.2, 7.3-7.4, 8.3): excerpts and the example
|
||||
store, metered embeddings and the cache, retrieval with the boundary test, the program and response cache, folds,
|
||||
compile, cross-fitted calibration; then the paid slice (smoke, embed, compile, outer evaluation, ~$5, ceiling $7)
|
||||
and, after a second go-ahead, leave-one-source-out and the memory-size curves (~$7, ceiling $10), committed under
|
||||
`benchmarks/measurements/<date>-decision-engine-v1/`.
|
||||
7. **Experiments** (4.1-4.4): `learn/experiments.py`, `register`, `experiment-outcome`, the Run annotation checks,
|
||||
7. Experiments (4.1-4.4): `learn/experiments.py`, `register`, `experiment-outcome`, the Run annotation checks,
|
||||
`OUSAST_VERIFY_CALLERS`, exp-001 registered, run and recorded, and the program-variant experiment machinery.
|
||||
8. **Integration** (6.1-6.3, 7.3, 7.5): the `embed` and `decide` tasks with the seeded memory, the `scan` stage,
|
||||
8. Integration (6.1-6.3, 7.3, 7.5): the `embed` and `decide` tasks with the seeded memory, the `scan` stage,
|
||||
`pre-push` admission with the engine, report fields, the fallback line, and the opt-in adaptation layer.
|
||||
9. **Adopt** (3.3, 8.2): `ousast learn adopt`, the packaged program with the licence filter, the priors-on/off and
|
||||
9. Adopt (3.3, 8.2): `ousast learn adopt`, the packaged program with the licence filter, the priors-on/off and
|
||||
`k` experiments with recorded decisions.
|
||||
10. **v3 check** (5.3): `prediction-v3.json` is committed first, then v3 is run by its owner under protocol v3, and
|
||||
10. v3 check (5.3): `prediction-v3.json` is committed first, then v3 is run by its owner under protocol v3, and
|
||||
`result-v3-engine.json` is committed. Nothing between the two commits may touch the program, memory snapshot,
|
||||
features, priors or thresholds.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Label scarcity decides what can be evaluated.** The status of each family today:
|
||||
- Label scarcity decides what can be evaluated. The status of each family today:
|
||||
- memory (5 repository groups, 173/173 rows, almost all openssl and curl), config_secrets (5) and prototype are
|
||||
**insufficient data** (`labels.json` `verified_pin_labels`). Users get today's behaviour there, labelled as
|
||||
such, and no money is spent evaluating them.
|
||||
@@ -1143,16 +1143,16 @@ start only after the maintainer confirms their budget against the balance read j
|
||||
- The plane profile has 43 rows from one population until the harvest Run.
|
||||
- BLOCK at 95% precision is likely **not reachable** for most families on this data. The design reports "BLOCK
|
||||
not offered" instead of lowering the target.
|
||||
- **Weak signals, strong code.** The pair baseline has quick-rule labelled recall 0.24 and Youden -0.013 overall
|
||||
- Weak signals, strong code. The pair baseline has quick-rule labelled recall 0.24 and Youden -0.013 overall
|
||||
(`pairs.json` `overall`); the v2 engine recall is 0/17 (`results-v2.json`). The program reads the code, so it
|
||||
can beat the signals, but the verify history says the model over-flags (v1 1/12, v2 0/17 adjudicated true). A
|
||||
calibrated classifier that mostly says "advisory" is honest; recall at ADVISORY 0.90 may need a threshold with
|
||||
poor precision. That is reported, not tuned away on outer folds.
|
||||
- **LLM nondeterminism.** The same prompt can answer differently, and temperature 0 is not a provider guarantee.
|
||||
- LLM nondeterminism. The same prompt can answer differently, and temperature 0 is not a provider guarantee.
|
||||
Guards: every response cached and replayable (numbers reproduce at $0); sample 1 at temperature 0; the vote mean
|
||||
over `k` samples as the score; the A/B unit is paired and both arms' caches are recorded; a re-ask agreement rate
|
||||
on a fixed 30-candidate canary set is reported with each evaluation.
|
||||
- **Label leakage into prompts.**
|
||||
- Label leakage into prompts.
|
||||
- Through rationales: memory examples carry no rationale; demonstration rationales are produced without the label
|
||||
and scanned for identities and advisory ids; the candidate's prompt never holds `EXCLUDED_FIELDS` (test).
|
||||
- Through code: fixed-side excerpts can carry fix comments ("prevent XSS", a CVE id). Advisory ids and security
|
||||
@@ -1160,11 +1160,11 @@ start only after the maintainer confirms their budget against the balance read j
|
||||
the change under test, never the later fix.
|
||||
- Through memorisation: the chat model may have seen public CVE fixes in pre-training. Not removable; the v3
|
||||
check (unpublished fixes are rarer there) and per-source numbers expose it, and the report names it.
|
||||
- **Retrieval leakage.** Same repository under another name or corpus: group normalisation, CVE merge, the
|
||||
- Retrieval leakage. Same repository under another name or corpus: group normalisation, CVE merge, the
|
||||
near-duplicate drop at cosine >= 0.98 in evaluation, and `test_boundary`. Demonstrations from the compile split
|
||||
are, by construction, from repositories never evaluated. The seed for plane Runs filters on the host, where the
|
||||
labels are; the sandbox never receives an example of its own case's group.
|
||||
- **Label-tracking signals (maintainer decision 2026-10-01: "Neutralise both for v1").** The harvest found two
|
||||
- Label-tracking signals (maintainer decision 2026-10-01: "Neutralise both for v1"). The harvest found two
|
||||
signals that follow the pair label instead of the code: verify candidates on fixed sides named functions the side
|
||||
no longer declares (now never produced: a side's candidates are only the functions it declares, the rest counted
|
||||
`absent_on_side`), and the engine's "read the file, asked no question" state, which a fixed side reaches by losing
|
||||
@@ -1173,40 +1173,40 @@ start only after the maintainer confirms their budget against the balance read j
|
||||
the stored record keeps it. The engine returns only through a pre-registered A/B evaluated on assumed-benign and
|
||||
benign-control negatives, where it cannot track a fix. `ousast learn audit-leaks` reports how well every signal
|
||||
alone separates the sides of a pair and flags any above 0.20 (counts under `benchmarks/measurements/`).
|
||||
- **Earlier leakage paths still apply.** Hand rules and framework priors written after seeing a training repository
|
||||
- Earlier leakage paths still apply. Hand rules and framework priors written after seeing a training repository
|
||||
(the `$wpdb` rule from PMPro, the hook source from WP Statistics, `taint.sc:911-949`): priors off by default,
|
||||
leave-one-framework-out, best-effort `taught_by = [repo]`. The mechanism exporter's train-on-test leak
|
||||
(`closed-loop-train-on-test-leak.md`): no exporter output is a feature. Verify and tie-break fitted on v2: the v2
|
||||
fold is design-informed. Thresholds and instructions chosen on reported folds: cross-fitting and the compile
|
||||
split. v3: fail-closed sources, never opened. User dismissals: refused by both builders.
|
||||
- **Cost.** Model spend is now per candidate and per evaluation, not one-off. Measured (section 4.6) the first
|
||||
- Cost. Model spend is now per candidate and per evaluation, not one-off. Measured (section 4.6) the first
|
||||
slice fits today's balance; everything after it needs a top-up. Guards: `MeteredClient` ceilings on every task
|
||||
and command, a smoke check that measures real tokens and cache hits before any paid run, cached replay for
|
||||
re-analysis, unevaluable families skipped, `k = 1` if the prefix cache does not hit. Deployment cost (~$0.02 per
|
||||
push) is a user-facing change: `pre-push` without a key falls back, and the report states the spend.
|
||||
- **Model and version drift.** `deepseek-flash` is an alias whose weights can change, and so can OpenRouter's
|
||||
- Model and version drift. `deepseek-flash` is an alias whose weights can change, and so can OpenRouter's
|
||||
embedding route. The artifact records model ids, the Model parameters digest and the response `model` field;
|
||||
before adoption and before the v3 prediction the canary set is re-asked, and agreement below 0.9 with the
|
||||
cached answers, or an embedding dimension change, invalidates the evaluation (re-embed, re-evaluate) rather than
|
||||
being averaged in. Prices are data in the Model manifest; a price change changes only cost reports.
|
||||
- **Latency.** A model call per candidate is slower than a dot product. The 30 s `pre-push` deadline is met by
|
||||
- Latency. A model call per candidate is slower than a dot product. The 30 s `pre-push` deadline is met by
|
||||
concurrency and the `undecided: deadline` state; a push with many candidates gets partial decisions, stated.
|
||||
- **Packaging third-party code.** Demonstrations and memory hold excerpts of corpus repositories. Only
|
||||
- Packaging third-party code. Demonstrations and memory hold excerpts of corpus repositories. Only
|
||||
permissively licensed excerpts may ship in the package; the store is never shipped. A user without a store gets
|
||||
the demonstrations-only program, whose own curve point (memory 0%) and calibration are what they are told.
|
||||
- **Framework inference is itself a model.** Model roles cost money and can be wrong. `scan` without `[models]`
|
||||
- Framework inference is itself a model. Model roles cost money and can be wrong. `scan` without `[models]`
|
||||
gets only wrapper inference, whose reach on framework-heavy code without dependency source is poor. The report
|
||||
says which role source ran. Both are measured by the roles-on/off experiment. Nothing assumes they help.
|
||||
- **Host time on the 7 GB host.** Engine features remain the time cost (7-14 hours serial for the pair corpus,
|
||||
- Host time on the 7 GB host. Engine features remain the time cost (7-14 hours serial for the pair corpus,
|
||||
dev-php scans of 200-2,000 s each), run once per engine image from a frozen source export, one container at
|
||||
`--memory 3g` at a time, never beside the kind cluster's verify pods, cached by (content sha, image id). Model
|
||||
calls add wall time but no memory pressure; excerpts and embeddings are small (~8 KB per example with a 1,536-float
|
||||
vector, ~45 MB for 5,600 examples).
|
||||
- **Experiments will often be inconclusive.** With 41 development repositories the MDE is about 15 points.
|
||||
- Experiments will often be inconclusive. With 41 development repositories the MDE is about 15 points.
|
||||
Pre-registering that and recording "inconclusive" is the intended behaviour. The failure mode to avoid is
|
||||
extending an experiment after looking.
|
||||
|
||||
- **First paid slice, 2026-10-01 (tasks 6.2, 6.6; injection only).** Four instrument defects surfaced before any
|
||||
- First paid slice, 2026-10-01 (tasks 6.2, 6.6; injection only). Four instrument defects surfaced before any
|
||||
number was believed, each fixed with a test:
|
||||
- *The memory build joined no pair.* The harvest stores a pair side's record under its unit pin (the blob sha1 of
|
||||
the side's excerpt), not under `label_pin`; `ousast learn memory build --units <harvest units.json>` joins them.
|
||||
@@ -1227,7 +1227,7 @@ start only after the maintainer confirms their budget against the balance read j
|
||||
0.90 has precision 0.54 (prevalence 0.53); the majority `vulnerable` verdict alone has precision 0.86, recall 0.43.
|
||||
Canary re-ask agreement at temperature 0 was 0.83, below the 0.9 the adoption check requires.
|
||||
|
||||
- **Slice 2, 2026-10-02 (six families, after the advisory-fix harvest).** Two instrument defects stopped runs before any
|
||||
- Slice 2, 2026-10-02 (six families, after the advisory-fix harvest). Two instrument defects stopped runs before any
|
||||
number: the memory build's side reader knew only the first pair-kind source (all 194 advisory sides unread), and
|
||||
compiled demonstrations skipped the near-duplicate rule that retrieval and the pre-call assertion apply (the
|
||||
assertion stopped the untrusted_destination evaluation). Both fixed with tests. A confound to read with the new
|
||||
|
||||
@@ -25,29 +25,29 @@ money.
|
||||
|
||||
## Requirements (proposed)
|
||||
|
||||
1. **Mining.** From each fix pair, extract what the fixed function adds on the vulnerable flow: new calls,
|
||||
1. Mining. From each fix pair, extract what the fixed function adds on the vulnerable flow: new calls,
|
||||
new conditions guarding the sink, new or changed sink arguments. Record each as a candidate fact with its
|
||||
language, family, form (sanitizer call, guard predicate, safe argument, sink restriction) and the pairs
|
||||
that propose it. Mining is deterministic (tree-sitter diff of the two function bodies). No model calls.
|
||||
2. **Generalisation is explicit.** A candidate is a pattern, not a repository name: a callee name, an
|
||||
2. Generalisation is explicit. A candidate is a pattern, not a repository name: a callee name, an
|
||||
argument key, a predicate shape. Candidates naming project-specific identifiers are rejected (Req 8: no
|
||||
application-specific trees).
|
||||
3. **Admission by held-out separation.** Folds are by repository, as in the decision engine. A candidate is
|
||||
3. Admission by held-out separation. Folds are by repository, as in the decision engine. A candidate is
|
||||
admitted for a fold only if, on that fold's held-out pairs, adding it to the fact tables:
|
||||
- increases pairs where the vulnerable side keeps a path and the fixed side loses it or becomes
|
||||
sanitized;
|
||||
- and does not remove any held-out vulnerable-side path (no recall loss).
|
||||
A candidate proposed by fewer than 2 distinct repositories in the training folds is not tried.
|
||||
4. **No train-on-test.** Mining and admission never read held-out pairs; the closed-loop leak recorded in
|
||||
4. No train-on-test. Mining and admission never read held-out pairs; the closed-loop leak recorded in
|
||||
memory (`closed-loop-train-on-test-leak`) is the failure this rule exists to prevent. Population v3 is
|
||||
never read.
|
||||
5. **Measurement by the trace runner.** Each admission round re-runs the engine on the affected pins only
|
||||
5. Measurement by the trace runner. Each admission round re-runs the engine on the affected pins only
|
||||
(the runner's resume and selection already support this; on k3s the worker Deployment runs them in
|
||||
parallel). The outcome is the pair-asymmetry table per family, before and after, on held-out folds.
|
||||
6. **Gate before production.** An admitted fact enters `src/openultrasast/ruleset/semantic/*.toml` only after
|
||||
6. Gate before production. An admitted fact enters `src/openultrasast/ruleset/semantic/*.toml` only after
|
||||
the detection gates (`gate`, `map_gate`, `pair_gate`) show no regression, and only as a registered
|
||||
experiment whose decision rule was fixed before the run.
|
||||
7. **Coverage second.** After discharge facts, the same loop proposes source and sink additions from pairs
|
||||
7. Coverage second. After discharge facts, the same loop proposes source and sink additions from pairs
|
||||
where neither side has a path (91 of 105 today), admitted under the same held-out rule.
|
||||
|
||||
## How it relates to what exists
|
||||
|
||||
@@ -9,13 +9,13 @@ Maintainer direction and context: `brief.md`.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: a feature record per candidate built from every instrument and the memory store; a labelled
|
||||
- In scope: a feature record per candidate built from every instrument and the memory store; a labelled
|
||||
dataset with provenance; training, calibration and versioning of the decision engine; A/B experiments as plane
|
||||
Runs; learning curves and leave-one-population-out estimates; the engine's use in `ousast scan`, `ousast
|
||||
pre-push` and plane Runs; a final one-time check on frozen population v3.
|
||||
- **Out of scope**: new instruments (they plug in as signal producers later); deep or large models; any change to
|
||||
- Out of scope: new instruments (they plug in as signal producers later); deep or large models; any change to
|
||||
population v3 or its protocol; publishing results (licences still gate that).
|
||||
- **Adjacent expectations**: instruments keep working standalone; the memory store (harnessx-removal Req 6) is
|
||||
- Adjacent expectations: instruments keep working standalone; the memory store (harnessx-removal Req 6) is
|
||||
the system of record; the plane attributes every experiment's token spend per task.
|
||||
|
||||
## Requirements
|
||||
@@ -121,7 +121,7 @@ rule hits.
|
||||
reported unless the engine's decision changes.
|
||||
4. *(Amended 2026-10-03, maintainer decision; refines the BLOCK rule of 7.3 and 7.4. Tested by exp-004.)* BLOCK
|
||||
precision is demonstrated pooled across families, not per family:
|
||||
- **Normalisation**, declared before any run: each family's raw score is put on one scale by a per-family Platt
|
||||
- Normalisation, declared before any run: each family's raw score is put on one scale by a per-family Platt
|
||||
map (`learn.calibrate.fit_map` with its floors) fitted on training folds only. A family the training folds
|
||||
cannot map does not block on that fold.
|
||||
- **t_block**: one threshold on the normalised score for all families, chosen on training folds as the lowest
|
||||
@@ -132,6 +132,13 @@ rule hits.
|
||||
- **t_quiet**: per family, the highest threshold keeping >= 90% of the paired positives at or above it (chosen
|
||||
on training folds, its held-out recall reported); findings below it are silent.
|
||||
- No held-out label moves a threshold or a normalisation map; candidates whose verdict is `unsure` never block.
|
||||
5. *(Amended 2026-10-05, maintainer decision, with `search-with-proof`.)* Where a family has an owned demonstration
|
||||
oracle, the engine is **triage, not the gate**: BLOCK comes only from a verified demonstration (`search-with-proof`
|
||||
Req 3, 6.3), and the engine's job is to put the true vulnerable function among the top K candidates of a push.
|
||||
For those families the engine is judged by (a) top-K recall per push (K = 3 to start) on the unseen-repo pool and
|
||||
(b) its advisory false-alarm rate there; the pooled block gate of 6.4 then governs only families without an
|
||||
oracle (access control, deserialisation, configuration and secrets), where the engine remains the only signal.
|
||||
Every arm reports top-K recall next to its catch and false-alarm rates.
|
||||
|
||||
### Requirement 7: Built for unseen repositories -- a repo safety net
|
||||
|
||||
|
||||
@@ -254,3 +254,13 @@ rate per push. Within-pair AUC stays a development signal. Tasks 11-14 below fol
|
||||
- Search input fields, retrieval parameters, k, instruction and demonstrations together (DSPy MIPRO-style) on
|
||||
training folds; no hand-set input profile. Budget go-ahead before paid search.
|
||||
- _Requirements: 3, 6, 7_
|
||||
|
||||
- [ ] 17. Triage objective (Req 6.5, amended 2026-10-05 with `search-with-proof`)
|
||||
- [ ] 17.1 Metric: top-K recall per push (K = 1, 3, 5) of the true vulnerable function among changed functions,
|
||||
on held-out folds and on the unseen-repo pool; added to the pool scorer next to catch and false alarm.
|
||||
- [ ] 17.2 Score the existing arms on it ($0 replay): trained logistic (cheapest per candidate, likely default),
|
||||
LLM programs (exp-005 A), engine-evidence ordering, and an averaged ensemble.
|
||||
- [ ] 17.3 Re-scope 14 (pooled block gate) to families without a demonstration oracle; 15.4 and 16 report top-K
|
||||
recall as their primary unseen-pool number for families with an oracle.
|
||||
- _Requirements: 6.5, 7_
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# Brief: learning-harness
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Problem
|
||||
|
||||
The tool is a hand-authored static analyzer with a learning loop bolted on the side, and the loop cannot learn. Three deliberate choices cap it: closed vocabularies (sinks, sources, obligation kinds) that grow only by writing a new spec; a corpus that is the only teacher and a shape that is the only lesson; and a ban on retaining anything the model discovers. Results have ceilings by construction, the loop trains on its own test set, and there is no failure analysis feeding the next round. Detection logic is ~16% of the source and the other 84% cannot say whether the 16% works.
|
||||
|
||||
Measured 2026-09-06 on the vibe-py holdout (17 vendored pairs), the existing tool hunter (Sonnet 5 through OpenRouter, 4 steps, tools `read_file`, `grep_repo`, `find_refs`, prompted from file-level hotspots, no learning of any kind) against the overlay:
|
||||
|
||||
| scorer | published number | class-aware, identical twins excluded (n = 14) |
|
||||
|---|---|---|
|
||||
| tool hunter | 0/17 correct, Youden −0.588 | **12/14 correct, Youden +0.857, 0 leaks** |
|
||||
| overlay (facts + shapes) | 5/17 correct, Youden +0.294 | 5/14 correct, Youden +0.357 |
|
||||
|
||||
The published hunter number is a scoring artifact: the pair scorer counts a hunter finding only when the finding text contains the literal CWE id and the prompt never asks for one, so 24 of 25 vulnerable-side findings that sit inside the labeled function and name the labeled bug were thrown away. The 24 "leaks" split into 3 findings on byte-identical twins and 21 findings outside the labeled function on the trap twin, most of them real defects in a deliberately vulnerable repository. Raw runs: `benchmarks/measurements/2026-09-06-vibe-py-holdout-hunter-*.json`.
|
||||
|
||||
Three consequences follow.
|
||||
|
||||
1. **The model with tools is already the best detector we own, without any learning.** The roadmap thesis ("recall from the model with tools plus proof; precision and silence from static checks") is confirmed, and the harness has been optimizing the wrong component.
|
||||
2. **Detection, silence and leak are meaningless without a class.** Every scorer, lever and gate uses text tokens (CWE strings, sink names) as a proxy for class; the corpus's own labels are 32 free-text strings across 7 slices, and 130 of 176 vfc rows say `other`.
|
||||
3. **Improvements without a class dimension oscillate.** A change that helps one class and hurts another reads as no change on an aggregate Youden; the loop keeps or reverts it for the wrong reason and the system degrades in ways nobody predicted. This is the maintainer's observation and it is what the corpus-seeded lever does today.
|
||||
|
||||
Repairs already measured and required regardless: the mechanism lever seeds candidates from holdout pairs (holdout Youden +0.059 with every pair teaching, −0.059 with the train split only); five vibe-py twins are byte-identical; TypeScript has no grammar; absence excerpts carry no registration context; unit fixtures are easier than the corpus rows they stand in for.
|
||||
|
||||
## What others learned
|
||||
|
||||
Full sourced notes in `research.md` (four threads, 2026-09-06). The findings that change this design:
|
||||
|
||||
**Cyber reasoning systems (AIxCC finals, Big Sleep, XBOW, Glasswing).** The routing key that worked in every finalist was mode × language × sanitizer or CWE family, aligned to what the oracle can confirm; nobody routed on fine CWE. Only proof survives: every top system gates a report on an executed oracle, the accuracy multiplier decided the ranking (Atlantis 0.9999 versus 0.9044), and XBOW keys a non-AI validator per vulnerability class that alone decides success. RoboDuck's cheap logprob classifier (~$0.001) sending the top 20% to a $0.50 agent is the closest public analogue of an auto-classifier front end. Ensembles beat selection when the winner is unpredictable, with attribution done per agent after the fact. Validated is not correct: 38–46% of automatically accepted patches were semantically wrong. Hallucination controls are structural (forced tool calls, curated tools instead of bash, symbol resolution). Cross-run learning is nearly absent in the field, and the one trained component reported catastrophic forgetting across projects. Stability and cost decided more than cleverness.
|
||||
|
||||
**Self-improving harnesses (GEPA, DSPy, AlphaEvolve, DGM, Self-Harness, SkillOpt, AHE, Meta-Harness).** Track wins per instance or class, never the aggregate: GEPA's per-instance Pareto archive doubled the gain of greedy selection. Accept a change only when train and holdout are both non-negative and one is strictly positive; reject ties. Meta-agents predict the class they help at 33.7% precision and the class they hurt at 11.8% ("regression blindness"), so a cross-class sweep cannot be optional. Two-stage evaluation is where sample efficiency comes from. Prompt bloat is the overfit signature; cap length. Structural edits (tools, checklists, memory) transfer across model families, prose edits regress. Self-diagnosis confabulates (0 of 121 reflections named the real cause; structured trajectory facts reached 86%). Hide the checker and the gold labels from the editable surface. One change, one class per round. Harness-edit quality is flat across model tiers while detector quality is not: a cheap meta-agent, spend on the detector.
|
||||
|
||||
**Datasets and taxonomies.** Deduplicate by normalized hash before splitting and split by repository, chronologically. Report pair-wise outcomes (pair-correct, both-vulnerable, both-benign, reversed) and a directional-bias index, not F1; models that flag both sides win F1 while losing pair accuracy. Match on location and class with hierarchical partial credit and the maximum penalty for a hallucinated class. Fine CWE classification from code is unsolved (top-1 ≤ 14.7%, most errors lateral siblings); coarse families are workable (88 CWEs collapsed to 12 gave per-class experts +12.8% F1, with a router right 63.8% of the time and F1 0.18 when wrong). Specialists collapse without hard negatives. Verdicts flip on renames and whitespace; single-shot evaluation missed 42% of real changes, so evaluate with K ≥ 3 runs and a reliable-change test. Function-level pairs lose context, the same defect as our context-free absence excerpts.
|
||||
|
||||
**Routing and regression control.** Learned routers plateau 10–20 points below an oracle and can lose to random; measure router accuracy on its own against random and oracle. Route to several experts with an explicit `unknown`, never argmax. Slice everything; aggregate gains of +2 points have masked 28–39% of items getting worse. Freeze per-class configurations to prevent interference; skill loops driven by self-feedback alone drift, external feedback does not. Production gates are conjunctive per slice with relative thresholds against the last blessed baseline; no public numeric negative-flip budget exists, so the defensible budget is the measured seed-noise floor per class.
|
||||
|
||||
## Desired Outcome
|
||||
|
||||
A harness that improves a **set of classified detectors** round by round, where every change is attributed to the class it was meant to move, every unpredicted regression in another class counts against it, only what a verifier can confirm is reported above suspicion, and nothing is published without its unscorable denominator and its noise floor.
|
||||
|
||||
- **Auto-classifier as routing key.** A closed, versioned taxonomy of roughly 8–12 families derived from the CWE hierarchy and aligned to what our verifiers can confirm (sandbox recipe, static corroboration, entry-point and flow tools). Multi-label with an explicit `unknown`. Three tiers: deterministic static signals first, a cheap model second with logprob averaging, `unknown` third, each answer recording its tier. Measured on its own: agreement with maintainer labels on reviewed pairs with hierarchical partial credit, accuracy against a random router and an oracle, confusion matrix per round. Disagreements are the review queue.
|
||||
- **Per-class detectors, frozen between rounds.** One hunter configuration per family: length-capped prompt, skills, curated tools with symbol resolution (never bash), step and cost budget, evidence checklist, hard negatives, known counterexamples. A region routes to every family the classifier admits plus a generalist. Round zero clones the current hunter into every family so the baseline is controlled; K ≥ 3 runs per class establish the seed-noise floor that becomes that class's regression budget.
|
||||
- **Class-aware, pair-wise scorer.** Detection = a finding in the labeled function whose class matches the label, with hierarchical credit and the maximum penalty for a hallucinated class. Silence = no finding of the labeled class in the labeled function on the fixed side. Outcomes as pair-correct, both-vulnerable, both-benign, reversed, plus a directional-bias index, negative flips per item against the last blessed round, and a fixed-FPR view. Unscorable rows (identical twin, unsupported language, unresolved label, missing context) listed by reason and removed from the denominator, never deleted. Dedup by normalized hash before splitting; split by repository, chronologically.
|
||||
- **Only proof is reported above suspicion.** Class-keyed verifiers decide: sandbox recipes, static corroboration, entry-point, flow and obligation tools. A hunter claim without a verifier stays `suspicion` and is scored as such. Verifier code and gold labels sit outside every editable surface, and anything reported as proven gets a second independent judge.
|
||||
- **Learning unit = structural change, journaled.** A round's output is a tool, a checklist, a counterexample, a memory entry or a bounded prompt edit for one class, drafted from structured failure facts (which sink, which line, which check fired), never from free-form self-critique. Accepted only when train and holdout are both non-negative with one strictly positive and no other class falls below its budget; ties rejected; rejected edits kept in a buffer the proposer sees; the proposer never sees the holdout. HarnessX's `MetaAgent.evolve` and journal (`hypothesis_id`, `levers`, `predicted_affected`, precision discounted by `regressed_unpredicted`) is the round primitive; the tool supplies tasks keyed by class, trajectories, the evaluator and the routing.
|
||||
- **Honest fitness.** Split enforced by construction; two-stage evaluation (minibatch gate, the class's full holdout, the cross-class sweep); a per-class archive of which candidate wins which instance rather than one champion; the roadmap rewritten from one command with the unscorable denominator and the noise floor beside every number; a per-round cost cap where an over-budget round is reverted and recorded.
|
||||
|
||||
Principle for the roadmap: **classify first, verify before reporting, improve one class at a time under a fitness that punishes what it did not predict.**
|
||||
|
||||
## Approach
|
||||
|
||||
1. **Class-aware pair scorer and unscorable rows.** Pair-wise outcome vocabulary, location-and-class matching with hierarchical credit, directional-bias index, per-item negative flips, K-run reliable-change test, unscorable reasons out of the denominator, normalized-hash dedup, repository-level split. Re-publish every slice from one command. Smallest change, biggest number.
|
||||
2. **Taxonomy and auto-classifier.** Family taxonomy versioned in one file with its CWE mapping and its verifier per family; three-tier classifier; its own measurement report; the vfc slice's 130 `other` rows and the 32 free-text classes are its first job, disagreements queued for review.
|
||||
3. **Per-class hunter configurations.** Clone the current hunter into every family; curated tools with symbol resolution; length-capped prompts; per-family evidence checklist and hard negatives; the classifier routes a region to every admitted family plus the generalist; findings carry their class, detector and verifier.
|
||||
4. **Verifiers keyed by class.** Sandbox recipes, static corroboration, entry-point, flow and obligation tools exposed to the hunter and to the scorer; a claim rises above `suspicion` only through a verifier; a second independent judge for anything proven.
|
||||
5. **Split enforcement and lever refusal.** Every exporter and lever takes the train split by construction and refuses candidates touching a holdout pair; re-measure `corpus-seeded-mechanisms` and rewrite its roadmap row.
|
||||
6. **Evolve loop on HarnessX.** Tasks = classified pairs (`RLTask.task_type` = family); trajectories from journaled hunter runs; evaluator = the class-aware scorer; one structural change for one class per round with `predicted_affected` set; the acceptance rule and cross-class sweep above; per-class Pareto archive; rejected-edit buffer; reflector blind to the holdout; per-round cost cap; the journal's attribution table is the round report and the daily benchmark.
|
||||
7. **Corpus repairs the loop needs.** TypeScript grammar in the semantic extra; absence rows re-harvested with `handler_context`; identical twins marked `known_limit:identical_twin`; a fixture-difficulty check that fails when a unit fixture is strictly easier than the corpus rows its test claims to cover; a post-cutoff slice for contamination.
|
||||
8. **Operational floor.** Persisted logs and trajectories, budget caps tested on the failure path, redaction on every prompt and journaled trajectory, cost per point published per round.
|
||||
|
||||
## Scope
|
||||
|
||||
- **In**: items 1–8; roadmap rewrite from one command; a closure decision on `authorization-obligations` 4.3, 5 and 6.
|
||||
- **Out**: new static shape families; hand-written facts beyond what a round proposes and validates; changing the evidence ladder or dispositions; any merge gate on non-local slices; vendoring new pairs beyond re-harvesting existing recipes; fine-tuning model weights.
|
||||
|
||||
## Boundary Candidates
|
||||
|
||||
- Taxonomy and classifier (new, owns class assignment and its measurement) vs pair labels (maintainer-owned reference)
|
||||
- Per-class detector configurations (new, evolved, frozen between rounds) vs static analyzers (unchanged internally, exposed as tools and verifiers)
|
||||
- Class-aware scorer (replaces the text-token rule) vs catalog schema (one additive `unscorable` reason, one `class` field)
|
||||
- Evolve loop (new, HarnessX) vs existing bounded levers (rules, policy, mechanisms; kept, subordinated to split, class and budget)
|
||||
|
||||
## Out of Boundary
|
||||
|
||||
- Taint semantics, walker, dominance internals (`guard-dominance-regime`, `overlay-ir-completeness`)
|
||||
- Call graph and path records (`reachability-flow-model`), except as a future hunter tool
|
||||
- Obligation checker internals (`authorization-obligations`); its corpus tasks move here, its lever waits for the split
|
||||
|
||||
## Upstream / Downstream
|
||||
|
||||
- **Upstream**: `pair-corpus-honesty` (labels, tiers, splits, `known_limit`), `corpus-seeded-mechanisms` (exporter, LOO, lever), `tool_hunter` and `provider/openrouter`, `harness_ext`, HarnessX 0.1.0 (`meta_harness`, `rl`, `tracing.journal`), `tree-sitter-overlay-extra`, the sandbox and regress recipes.
|
||||
- **Downstream**: every roadmap number; `harnessx-self-improving-rulesets` (its evolve step is this spec); `authorization-obligations` 4.3/5/6; scan reports (findings carry class, detector and verifier).
|
||||
|
||||
## Existing Spec Touchpoints
|
||||
|
||||
- **Extends**: `pair-corpus-honesty` Req 1 (scorer) and Req 9/10 (row classification); `corpus-seeded-mechanisms` Req 4/5 (split by construction); `three-stage-scan` MAP (per-class hunters as proposers); `propose-adjudicate-prove` (verifiers keyed by class); `harnessx-self-improving-rulesets` (the evolve loop lands here).
|
||||
- **Folds in**: the retired `closed-loop-integrity` brief.
|
||||
- **Re-homes**: `authorization-obligations` 4.3 and the corpus half of 6.1; holds 5.
|
||||
|
||||
## Constraints
|
||||
|
||||
Core `dependencies = []`; HarnessX and grammars stay optional extras; every loop step degrades to a recorded reason without them. No row deleted; unscorable rows keep a reason. Labels stay maintainer-owned: the classifier proposes, disagreements queue for review, and a class change on a reviewed pair is a reviewed edit. Model-authored skills, tools and prompt edits are retained only after held-out validation under the acceptance rule and are journaled with attribution; nothing a round proposes reaches a scan without its class holdout and the cross-class sweep. Split enforced by construction. Verifier code and gold labels outside every editable surface. Prompt length capped per class. Gates (`gate`, `map_gate`, `pair_gate`) stay byte-identical. Redaction on every prompt and trajectory. Per-round cost cap with revert.
|
||||
|
||||
## What this project is for (maintainer, 2026-09-06)
|
||||
|
||||
The target is to catch the taxonomy families below **reliably** on real-world application code, including absence bugs in vibe-coded services (an authorization check that was never written), with an emphasis on dangerous, low-hanging fruit. It is not a bug-finder for JavaScript interpreters, WebGL renderers or browser engines, and it is not a memory-safety fuzzer: memory bugs in C and C++ are a different game played by specialized systems (AIxCC-class CRSs, Big Sleep) that this project learns from and does not replicate or compete with. The balance to hold is between two failure modes, being useless on actual code and being blind to forgotten checks in generated code; both are in scope, engine and renderer bugs are not. The vfc C slice therefore stays a measured, non-gating slice with one coarse memory-safety family and no crash oracle of our own.
|
||||
|
||||
## Decisions (maintainer, 2026-09-06)
|
||||
|
||||
1. **Taxonomy spine.** Ten families keyed to danger and to a verifier we can run, with CWE and the mechanism vocabulary as attributes, never the routing key: injection into a query or command (SQL, command, code, template); file path from user input; deserialization of untrusted data; access control (IDOR, missing guard, identity from request body; the obligations checker is its verifier); output encoding (XSS, template to HTML); untrusted destination (server-side request forgery and open redirect, which share the shape of untrusted input choosing where a request goes and account for 34 corpus rows); prototype pollution and unsafe object merge; permissive configuration and secrets (CORS, debug, cookie flags, weak crypto, hardcoded credentials); memory safety as one coarse family for the vfc slice only; unknown. Multi-label routing.
|
||||
2. **Regression budget.** Measured, not chosen: round zero runs the cloned hunter K = 5 times per family at temperature 0 over the vendored corpus; the per-family negative-flip rate between seeds is the budget. A round that exceeds it in any family is rejected regardless of its target gain.
|
||||
3. **Models.** Detectors and meta-agent on `deepseek-v4-flash` through DeepSeek's own OpenAI-compatible endpoint (`https://api.deepseek.com`); `deepseek-v4-pro` only as the second independent judge above suspicion and for classifier disagreements queued for review; embeddings stay on OpenRouter. The chat endpoint and the embedding endpoint are configured separately (today one base-URL variable serves both). The 1M context lets the detector read whole files instead of hotspot excerpts.
|
||||
4. **Round-zero verifiers.** Snippet templates with an oracle per family, on the existing per-language sandbox recipes: canary row in an in-memory database for injection, canary file for command execution and traversal, canary object for deserialization. Access control gets the obligations checker as static corroboration. Output encoding, SSRF, prototype pollution and configuration report at `suspicion` only until they have an oracle. A family without a verifier never reports above suspicion in a scan but is still scored class-aware on the corpus: the corpus is the verifier for learning, the sandbox is the verifier for reporting.
|
||||
5. **Obligations lever.** `authorization-obligations` task 5 is retired as a spec task and becomes the access-control family's first evolve round; obligation shapes are that family's memory entries under the split rule.
|
||||
6. **Order.** Round zero on the web families over vibe-py and agent-vfc, where real-world code, dangerous fruit and a working hunter coincide; the vfc C slice later, as a measured slice only.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `benchmarks/measurements/2026-09-06-vibe-py-holdout-hunter-sonnet5.json`: `ousast pairs --slice vibe-py --split holdout --hunter --hunter-model anthropic/claude-sonnet-5 --json`.
|
||||
- `benchmarks/measurements/2026-09-06-vibe-py-holdout-hunter-findings.json`: every hunter finding per side with `in_labeled_function` and the scorer's verdict; the class-aware re-score used a keyword classifier written for the measurement only, which is why approach item 2 exists.
|
||||
- `research.md`: the four research threads with sources.
|
||||
@@ -1,651 +0,0 @@
|
||||
# Design Document: learning-harness
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Overview
|
||||
|
||||
**Purpose**: Turn OpenUltraSAST from a hand-authored analyzer with a leaking improvement loop into a learning harness: an auto-classifier routes every pair, finding and code region to one of ten verifier-aligned families; one detector configuration per family is evolved one change per round from structured failure facts; a class-aware pair-wise scorer with unscorable denominators and a measured per-family noise floor decides acceptance; only verifier-confirmed claims are reported above suspicion; the split is enforced by construction.
|
||||
|
||||
**Users**: the maintainer runs round zero, evolve rounds and the publish command; operators read scan findings that now carry a family, a detector version and a verifier outcome; reviewers work the classifier disagreement queue.
|
||||
|
||||
**Impact**: the tool hunter becomes the primary detector and the static machinery becomes tools and verifiers. The pair scorer's hunter path, the exporter, leave-one-out, the mechanism lever and the improve journal change behavior; findings, catalogs and the manifest gain additive fields. Gates stay byte-identical.
|
||||
|
||||
### Goals
|
||||
- One trustworthy number per family per slice, with its denominator, taxonomy version, noise floor and cost.
|
||||
- A round can only be accepted when it helps its target family on held-out pairs and hurts no other family beyond its measured budget.
|
||||
- Nothing above `suspicion` without a verifier; verifier code and gold labels outside every editable surface.
|
||||
- Round zero on the web families over vibe-py and agent-vfc before any learning.
|
||||
|
||||
### Non-Goals
|
||||
- Browser-engine, interpreter or renderer bugs; crash oracles for C and C++ (the memory-safety family is measured, never gating).
|
||||
- New static shape families, new hand-written facts, evidence-ladder or disposition changes, merge gates on non-local slices, model fine-tuning.
|
||||
- A general router framework or a generic agent platform; the classifier is a function and a detector configuration is a directory.
|
||||
|
||||
## Boundary Commitments
|
||||
|
||||
### This Spec Owns
|
||||
- The family taxonomy file, its loader and its version (1.x).
|
||||
- Classification of pairs, findings and regions, and the classifier's measurement report and review queue (2.x).
|
||||
- Class-aware pair scoring: outcomes, directional bias, fixed-FPR view, K-run reliable-change test, negative flips (3.x).
|
||||
- Unscorable accounting and its reasons; the additive `unscorable` and `family` fields on catalog rows (4.x).
|
||||
- The single teacher-selection rule every learning path calls, and holdout refusal (5.x).
|
||||
- Per-family detector configurations, their loader, the family detector runner and finding tags (6.1–6.5); the chat-endpoint resolution independent of embeddings (6.6).
|
||||
- The verifier registry keyed by family, canary oracles, the second-judge step (7.x).
|
||||
- Round zero, the noise floor, evolve rounds, acceptance, attribution, the archive, the round journal, cost caps (8.x, 9.x).
|
||||
- The corpus repairs listed in 10.x and the publish command with committed artifacts (11.x).
|
||||
|
||||
### Out of Boundary
|
||||
- Pair labels, tiers, splits and `known_limit` semantics beyond the two additive fields (`pair-corpus-honesty`).
|
||||
- Facts, walker, dominance, obligations checker internals; the mechanism store schema; the rule and policy levers' validators.
|
||||
- The sandbox runner, snippet safety rules and Docker images; this spec adds an oracle to the verdict, not a runner.
|
||||
- HarnessX internals; this spec composes `MetaAgent.evolve` and the journal behind `harness_ext`.
|
||||
- Harvesting new pairs; only re-harvesting existing absence recipes.
|
||||
|
||||
### Allowed Dependencies
|
||||
- `pairs`, `benchmark`, `semantic.loo`, `semantic.seed`, `semantic.mechanisms`, `improve.evolve`, `improve.validator`, `tool_hunter`, `hunter_tools`, `provider.openrouter`, `regress.*`, `sandbox.*`, `redaction`, `harness_ext`, `config`.
|
||||
- Optional extras only through `harness_ext` (HarnessX) and `semantic.extra` (tree-sitter, now including TypeScript); every step degrades to a recorded reason without them.
|
||||
- Direction: `learning.families` → `learning.classify` → `learning.scoring` → `learning.split` → `learning.detectors` and `learning.verifiers` → `learning.rounds` → `learning.publish` and `cli`. `learning.*` imports the packages above; nothing outside `learning` imports `learning.rounds` or `learning.publish`. `pairs` and `benchmark` never import `learning`.
|
||||
|
||||
### Revalidation Triggers
|
||||
- A change to the family list, a family's CWE set or its verifier (taxonomy version bump).
|
||||
- A change to the pair outcome vocabulary or the reliable-change test.
|
||||
- A change to the acceptance rule, the regression-budget definition or the journal schema.
|
||||
- A change to the chat-endpoint resolution order or the tool-hunter finding shape.
|
||||
- A change to the sandbox verdict contract (oracle callback) or snippet safety rules.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Existing Architecture Analysis
|
||||
- `tool_hunter.run_tool_hunter` is a 4-step loop over three curated tools with findings parsed into `StaticFinding` at `suspicion` (`finding_id = tool-hunter:<path>:<line>`, tag `tool-hunter`). It refuses to emit findings when no tool was called. It has no temperature, budget or context knobs beyond `max_steps`.
|
||||
- `pairs._hunter_matches_expected` keys detection on text tokens and `_evaluate_hunter_pair` counts every fixed-side finding as a leak under `fix_policy = silent`; both are replaced on the hunter path only. The overlay and inventory paths and `pair_gate` are untouched.
|
||||
- `evaluate_profiles`, `evaluate_mechanism_profiles`, `propose_mechanism_edits`, `evaluate_loo` and `build_pair_signals` filter by tier and vendored status but not by split; `export_mechanisms` seeds from every case it is given.
|
||||
- `OpenRouterChatClient` and `OpenRouterEmbeddingClient` read the same `OPENROUTER_BASE_URL`; `complete_chat` always sends `temperature: 0` and drops `reasoning_content` from replies. DeepSeek requires thinking disabled for temperature to apply, `reasoning_content` replayed on tool-call turns, no `/v1` suffix, `json_object` only, no seed.
|
||||
- `regress.candidate` derives a verdict from the exit code; snippets are compile-only templates or the model's `proposed_snippet`; `check_snippet_safety` bans sockets, curl and workspace writes, which a canary file in `/scratch` respects.
|
||||
- `MechanismStore` is an append-only JSONL with tombstones and deterministic ids, the pattern reused for the archive and the journal.
|
||||
- HarnessX 0.1.0 provides `MetaAgent.evolve(current_config, trajectories_dir, output_dir)` with post-flight checks, `JournalEntrySpec(round, label, hypothesis_id, levers, predicted_affected, prose)`, `build_context` with precision discounted by unpredicted regressions, and `RLTask.task_type` routing.
|
||||
|
||||
### Architecture Pattern & Boundary Map
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Corpus
|
||||
Catalog[pair catalogs]
|
||||
Loader[pairs loader with unscorable and family fields]
|
||||
end
|
||||
subgraph Learning
|
||||
Families[families taxonomy]
|
||||
Classify[classifier three tiers]
|
||||
Scoring[class aware pair scorer]
|
||||
Split[teachers and holdout refusal]
|
||||
Detectors[family detector configs and runner]
|
||||
Verifiers[verifier registry canary static none]
|
||||
Rounds[round zero and evolve rounds]
|
||||
Journal[round journal and archive]
|
||||
Publish[publish command]
|
||||
end
|
||||
subgraph Existing
|
||||
Hunter[tool hunter loop]
|
||||
Chat[chat endpoint client]
|
||||
Regress[regress verdicts and sandbox]
|
||||
Levers[exporter loo mechanism lever]
|
||||
Meta[HarnessX MetaAgent optional]
|
||||
end
|
||||
Catalog --> Loader --> Scoring
|
||||
Families --> Classify --> Scoring
|
||||
Families --> Detectors
|
||||
Families --> Verifiers
|
||||
Classify --> Detectors
|
||||
Split --> Levers
|
||||
Split --> Rounds
|
||||
Detectors --> Hunter --> Chat
|
||||
Verifiers --> Regress
|
||||
Scoring --> Rounds
|
||||
Detectors --> Rounds
|
||||
Verifiers --> Rounds
|
||||
Rounds --> Journal
|
||||
Rounds --> Meta
|
||||
Journal --> Publish
|
||||
Scoring --> Publish
|
||||
```
|
||||
|
||||
**Architecture Integration**:
|
||||
- Selected pattern: a `learning` package layered left to right (families → classify → scoring → split → detectors/verifiers → rounds → publish), composing existing seams rather than replacing them.
|
||||
- Domain boundaries: the corpus owns labels and rows; `learning` owns class assignment, scoring, teaching rules, detector configurations, verifier keys and rounds; `regress` owns execution; `harness_ext` owns the HarnessX seam.
|
||||
- Existing patterns preserved: append-only JSONL with tombstones for the archive and journal; `HarnessRuntime.run_stage` and degradations for every step; `ScriptedChatClient` for offline tests; `FakeSandboxRunner` for verifier tests.
|
||||
- New components rationale: each is one requirement area with one owner; the classifier and scorer are functions, the detector configuration is a directory, the journal is a file.
|
||||
- Steering compliance: core `dependencies = []`; recall first under a false-positive ceiling, now per family; gates byte-identical; no model-authored artifact retained without held-out validation.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Layer | Choice / Version | Role in Feature | Notes |
|
||||
|-------|------------------|-----------------|-------|
|
||||
| Chat endpoint | DeepSeek platform API, OpenAI-compatible, `https://api.deepseek.com` (no `/v1`); models `deepseek-v4-flash` (default), `deepseek-v4-pro` (second judge, review queue) | Detectors, classifier model tier, proposer, judge | Thinking on by default: the client sends `thinking: {"type": "disabled"}` for detectors and the classifier so `temperature` applies; `reasoning_content` replayed on tool-call turns; `response_format json_object` with empty-content retry; `logprobs` requested by the classifier tier only. No seed: determinism is best-effort and absorbed by the noise floor. Pricing (peak, USD/1M): flash 0.44 in / 1.32 out, cache hit 0.014; pro 1.32 / 3.96. Concurrency 2500 (flash). |
|
||||
| Embedding endpoint | OpenRouter, `openai/text-embedding-3-small`, unchanged | Prove-order retrieval | Reads `OPENROUTER_*` only; DeepSeek has no embeddings endpoint. |
|
||||
| Evolve loop | HarnessX 0.1.0 (`meta_harness.agent.MetaAgent`, `meta_harness.journal`), optional extra | Proposal generation and attribution context | Behind `harness_ext.require_harnessx`; a scripted proposer stands in without it. |
|
||||
| Parsing | tree-sitter extra + `tree-sitter-typescript>=0.23` | TypeScript rows become scorable | Optional extra only. |
|
||||
| Sandbox | existing Docker CLI runner, images `python:3.12-alpine`, `node:22-alpine` | Canary verifiers | Oracle callback added to the verdict; safety rules unchanged. |
|
||||
| Storage | JSONL under `.openultrasast/learning/` and `benchmarks/measurements/` | Journal, archive, noise floors, published artifacts | Append-only, committed when published. |
|
||||
|
||||
## File Structure Plan
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/openultrasast/learning/
|
||||
├── __init__.py # public surface: load_families, classify_*, score_pair_family, teachers, run_baseline, run_learning_round, publish
|
||||
├── families.py # Family, FamilyTaxonomy, load_families(ruleset/families.toml), version, verifier kind per family
|
||||
├── classify.py # Classification, classify_pair/finding/region (tiers), measure_classifier, review queue entries
|
||||
├── scoring.py # FamilyOutcome, score_pair_family, FamilyMetrics, directional_bias, negative_flips, reliable_change, fixed_fpr_view, unscorable_reason
|
||||
├── split.py # teachers(cases), refuse_if_holdout(candidate_pairs, cases) -> Refusal | None
|
||||
├── detectors.py # FamilyConfig, load_family_configs, run_family_detector (whole-file context, curated tools), finding tags
|
||||
├── endpoint.py # ChatEndpoint, resolve_chat_endpoint (DeepSeek first, OpenRouter fallback, scripted), DeepSeekChatClient adapter
|
||||
├── verifiers.py # Verifier protocol, CanaryVerifier, StaticVerifier, NoVerifier, verifier_for(family), second_judge
|
||||
├── rounds.py # run_baseline (round zero, noise floor), run_learning_round (propose, minibatch, holdout, sweep, accept/revert), Attribution
|
||||
├── proposer.py # Proposer protocol, ScriptedProposer, HarnessXProposer (MetaAgent.evolve behind harness_ext), failure facts, rejected buffer
|
||||
├── journal.py # RoundRecord, LearningJournal (append-only JSONL), Archive (family -> winning config per pair)
|
||||
└── publish.py # publish(): regenerate numbers from one command, write roadmap table and measurement artifacts
|
||||
src/openultrasast/ruleset/families.toml # closed, versioned taxonomy: id, cwes, mechanisms, verifier, description
|
||||
src/openultrasast/learning/configs/<family>/ # round-zero detector configuration per family (prompt.md, checklist.md, tools.toml, budget.toml, hard_negatives.jsonl, counterexamples.jsonl)
|
||||
tests/test_learning_families.py, test_learning_classify.py, test_learning_scoring.py, test_learning_split.py, test_learning_detectors.py, test_learning_endpoint.py, test_learning_verifiers.py, test_learning_rounds.py, test_learning_journal.py, test_learning_publish.py, test_fixture_difficulty.py
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
- `src/openultrasast/pairs.py` — `PairCase.unscorable: str | None` computed at load (identical twin via normalized body hash, `known_limit` passthrough); duplicate report across pairs before split; `split_by_repository` helper honoring existing `split` fields and reporting straddles; `_evaluate_hunter_pair` delegates matching and leaks to `learning.scoring` through an injected callable so `pairs` does not import `learning`; `build_pair_signals` gains a `split` filter.
|
||||
- `src/openultrasast/benchmark.py` — `ExpectedFinding.family: str | None = None` (additive, stored as an opaque string; the Classifier validates loaded labels against the taxonomy so `pairs` and `benchmark` never import `learning`).
|
||||
- `src/openultrasast/tool_hunter.py` — `run_tool_hunter` gains `system_prompt`, `context_files`, `tools`, `max_chars` and `tags` parameters with today's values as defaults; finding parsing accepts an optional `family` field; the reply loop replays `reasoning_content`.
|
||||
- `src/openultrasast/hunter_tools.py` — four new tools beside `read_file`, `grep_repo`, `find_refs`: `read_definition(symbol)`, `entry_points(path)`, `flows(path, function)`, `obligations(path, function)`, each clamped to the repository and returning identifiers and line spans only; `HUNTER_TOOLS` schemas extended.
|
||||
- `src/openultrasast/provider/openrouter.py` — `OpenRouterChatClient` gains `extra_body` (thinking, response_format, logprobs) and retains the `reasoning_content` field on assistant messages; replaying it across tool turns is the hunter loop's job (`tool_hunter`), not the client's. No behavior change for existing callers.
|
||||
- `src/openultrasast/config.py` — `[models]` gains `chat_base_url`, `chat_api_key_env`, `judge`; `[learning]` section (`families_path`, `configs_dir`, `k_runs = 5`, `round_cost_cap_usd`, `minibatch`, `cutoff_date`); `load_dotenv` unchanged, tests inject endpoints explicitly.
|
||||
- `src/openultrasast/semantic/seed.py`, `semantic/loo.py`, `improve/evolve.py` — teacher selection through `learning.split.teachers`; refusals recorded; `evaluate_loo` teaches from train only.
|
||||
- `src/openultrasast/regress/candidate.py`, `regress/verdict.py` — `run_regression` accepts an optional `oracle: Callable[[SandboxResult], bool]`; `verdict_from_result` uses the oracle when given, exit code otherwise.
|
||||
- `src/openultrasast/cli.py` — `ousast learning classify|score|baseline|round|publish`; `pairs --hunter` uses the family scorer and `--k-runs`; `[models].hunter` resolution through `learning.endpoint`.
|
||||
- `pyproject.toml` — `tree-sitter-typescript>=0.23` in the semantic extra; `src/openultrasast/semantic/extra.py` maps `typescript`/`tsx`.
|
||||
- `benchmarks/pairs/vibe-py/catalog.toml` and `recipes.toml` — the five identical twins gain `known_limit = "identical_twin"`; absence rows gain `family` and `mode = "handler_context"` (re-harvest is a maintainer step).
|
||||
- **Cross-file registration context (amended during task 4.1).** Some frameworks keep the route table out of the handler module: connexion registers by `operationId` in an OpenAPI document, an Express application registers in a router file. A single-file excerpt of such a handler can never contain its registration, so Req 10.2 is unreachable for those rows without a second file. A recipe may therefore name `context = ["<repo-relative path>"]`; `benchmarks/pairs/harvest.py` fetches each document at the same two commits, extracts only the stanza that names the handler (`extract_registration_context`), and writes it under `<name>/context/<side>/<relpath>` with its own provenance header and the same redaction. The catalog carries `[[pair.context]]` rows, `PairCase.context_files` holds `(relpath, vuln, fixed)`, and `_materialize_side` copies them beside the excerpt at their repository-relative paths so the entry-point mapper sees the same layout the project has. `src/openultrasast/mapping.py` gains `_route_manifest_entry_points`, which reads `operationId` and the operation's `security` block out of a YAML or JSON document and attributes the record to the module the operation id points at. Trap pairs whose fixed side is a different, correctly guarded handler carry `fix_function` so the fixed side gets *its* registration rather than the vulnerable side's.
|
||||
- `benchmarks/pairs/mechanisms.toml` — unchanged; families reference mechanism ids.
|
||||
- `.kiro/steering/roadmap.md` — rewritten by `publish` from artifacts.
|
||||
|
||||
## System Flows
|
||||
|
||||
### Class-aware scoring of one pair (hunter path)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Scorer
|
||||
participant C as Classifier
|
||||
participant D as FamilyDetectors
|
||||
participant V as Verifier
|
||||
S->>C: classify_pair(case)
|
||||
C-->>S: families or unknown, tier
|
||||
loop K runs
|
||||
S->>D: run on vuln side and fixed side
|
||||
D-->>S: findings with family tags
|
||||
end
|
||||
S->>S: detection = family match inside labeled function
|
||||
S->>S: silence = no labeled family inside function on fixed side
|
||||
S->>V: verify claims that a verifier exists for
|
||||
V-->>S: suspicion, static corroboration, or proven
|
||||
S-->>S: FamilyOutcome, negative flips, reliable change
|
||||
```
|
||||
|
||||
Decisions: unscorable pairs short-circuit before any run; K runs share one classification; verifier outcomes never change detection or silence, they change the reported rung.
|
||||
|
||||
### One evolve round
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Start[failure facts for target family] --> Propose[proposer drafts one structural change]
|
||||
Propose --> Refuse{touches holdout or exceeds length cap}
|
||||
Refuse -->|yes| Reject[reject and record]
|
||||
Refuse -->|no| Mini[train minibatch of target family]
|
||||
Mini --> MiniOk{no decrease}
|
||||
MiniOk -->|no| Reject
|
||||
MiniOk -->|yes| Hold[target family full holdout]
|
||||
Hold --> HoldOk{train and holdout non negative and one strictly positive}
|
||||
HoldOk -->|no| Reject
|
||||
HoldOk -->|yes| Sweep[every other family holdout]
|
||||
Sweep --> SweepOk{negative flips within each budget}
|
||||
SweepOk -->|no| Reject
|
||||
SweepOk -->|yes| Accept[accept freeze config bump version]
|
||||
Accept --> Journal[journal hypothesis predicted attribution cost]
|
||||
Reject --> Journal
|
||||
Journal --> Buffer[rejected edit buffer updated]
|
||||
```
|
||||
|
||||
Decisions: the proposer never sees holdout inputs, outputs or scores; cost is metered per stage and the round is reverted when the cap is crossed at any stage; ties reject.
|
||||
|
||||
## Requirements Traceability
|
||||
|
||||
| Requirement | Summary | Components | Interfaces | Flows |
|
||||
|---|---|---|---|---|
|
||||
| 1.1–1.4 | closed taxonomy, verifier per family, version, attributes not keys | Families | `load_families`, `FamilyTaxonomy` | — |
|
||||
| 2.1–2.5 | three-tier multi-label classifier, measured, review queue, whole corpus | Classifier | `classify_*`, `measure_classifier` | scoring |
|
||||
| 3.1–3.6 | family-in-function detection and silence, outcomes, bias, FPR, K runs, no text tokens | Scorer | `score_pair_family`, `FamilyMetrics` | scoring |
|
||||
| 4.1–4.5 | unscorable reasons, never delete, dedup, repository split, denominators shown | Loader (pairs), Scorer, Publish | `PairCase.unscorable`, `unscorable_reason`, `split_by_repository` | — |
|
||||
| 5.1–5.4 | train-only teachers, refusal, re-measure lever, proposer blind | Split, Levers, Proposer | `teachers`, `refuse_if_holdout` | round |
|
||||
| 6.1–6.6 | per-family config, frozen, routing plus generalist, whole file, curated tools, independent chat endpoint | Detectors, Endpoint | `FamilyConfig`, `run_family_detector`, `resolve_chat_endpoint` | scoring |
|
||||
| 7.1–7.6 | suspicion unless verified, canary oracles, static corroboration, none, second judge, sealed surface | Verifiers, Regress | `Verifier`, `verifier_for`, `second_judge`, `run_regression(oracle=)` | scoring |
|
||||
| 8.1–8.4 | clone, K = 5 noise floor, publish round zero, web families first | Rounds | `run_baseline`, `NoiseFloor` | — |
|
||||
| 9.1–9.9 | failure facts, one change, journal, staged evaluation, acceptance, attribution, archive, cost cap, retention | Rounds, Proposer, Journal | `run_learning_round`, `Proposer`, `RoundRecord`, `Archive` | round |
|
||||
| 10.1–10.5 | TypeScript, re-harvest, identical twins, fixture difficulty, post-cutoff slice | Extra, Corpus, Tests | `semantic.extra`, catalog fields, `test_fixture_difficulty` | — |
|
||||
| 11.1–11.5 | one command, artifacts committed, trajectories persisted, degrade with reason, gates identical | Publish, Journal, Runtime | `publish`, `LearningJournal` | — |
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
| Component | Layer | Intent | Req | Key Dependencies | Contracts |
|
||||
|---|---|---|---|---|---|
|
||||
| Families | data | closed taxonomy with verifier per family | 1.x | ruleset file (P0) | Service |
|
||||
| Classifier | logic | assign families with tier, measure itself | 2.x | Families (P0), Endpoint (P1) | Service, State |
|
||||
| Scorer | logic | class-aware pair outcomes and statistics | 3.x, 4.x | Families (P0); findings from Detectors and rungs from Verifiers are inputs, not imports (P1) | Service |
|
||||
| Split | logic | the one teacher rule and holdout refusal | 5.x | pairs (P0) | Service |
|
||||
| Detectors | logic | per-family configuration and runner | 6.1–6.5 | tool_hunter (P0), Endpoint (P0), Classifier (P0) | Service, State |
|
||||
| Endpoint | integration | independent chat endpoint resolution and DeepSeek adapter | 6.6 | provider.openrouter (P0), config (P0) | Service |
|
||||
| Verifiers | integration | family-keyed oracles and the second judge | 7.x | regress, sandbox (P0), obligations (P1) | Service |
|
||||
| Rounds | orchestration | round zero, noise floor, evolve rounds, acceptance | 8.x, 9.x | Scorer, Detectors, Proposer, Journal (P0) | Batch, State |
|
||||
| Proposer | integration | structural change proposals from failure facts | 9.1–9.2 | harness_ext (P1), Endpoint (P1) | Service |
|
||||
| Journal | data | round records, attribution, archive | 9.3, 9.6, 9.7, 11.3 | JSONL (P0) | State |
|
||||
| Publish | orchestration | numbers from one command, artifacts, roadmap | 11.x | Scorer, Journal (P0) | Batch |
|
||||
|
||||
### Data layer
|
||||
|
||||
#### Families
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Load and version the closed family taxonomy with its CWE set, mechanism set and verifier kind |
|
||||
| Requirements | 1.1, 1.2, 1.3, 1.4 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Exactly ten families as decided; ids are stable identifiers; the file carries `version`.
|
||||
- Verifier kind per family is one of `canary`, `static`, `none`; memory safety is `none`.
|
||||
- No component may route or score on a CWE or mechanism id; they are looked up only through a family.
|
||||
|
||||
**Dependencies**: Inbound: Classifier, Scorer, Detectors, Verifiers (P0). External: `ruleset/families.toml` (P0).
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Family:
|
||||
id: str # e.g. "injection", "path", "deserialization", "access_control", "output_encoding", "untrusted_destination", "prototype", "config_secrets", "memory", "unknown"
|
||||
cwes: frozenset[str]
|
||||
mechanisms: frozenset[str]
|
||||
verifier: Literal["canary", "static", "none"]
|
||||
description: str
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FamilyTaxonomy:
|
||||
version: str
|
||||
families: tuple[Family, ...]
|
||||
def by_id(self, family_id: str) -> Family: ...
|
||||
def family_of_cwe(self, cwe: str) -> Family | None: ...
|
||||
def family_of_mechanism(self, mechanism: str) -> Family | None: ...
|
||||
def related(self, a: str, b: str) -> Literal["same", "parent_child", "lateral", "fabricated"]: ...
|
||||
|
||||
def load_families(path: Path | None = None) -> FamilyTaxonomy # raises FamiliesError naming the field on any deviation
|
||||
```
|
||||
- Invariant: `unknown` is always present and never has a verifier.
|
||||
|
||||
#### Journal
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Append-only record of rounds with attribution, and the archive of which configuration wins which pair |
|
||||
| Requirements | 9.3, 9.6, 9.7, 11.3 |
|
||||
|
||||
**Contracts**: State [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class RoundRecord:
|
||||
round: int
|
||||
family: str
|
||||
hypothesis: str
|
||||
levers: tuple[str, ...] # "tool" | "checklist" | "counterexample" | "memory" | "prompt"
|
||||
predicted_affected: tuple[str, ...]
|
||||
predicted_at_risk: tuple[str, ...]
|
||||
outcome: Literal["accepted", "rejected", "reverted_cost", "refused_holdout"]
|
||||
reason: str
|
||||
target_train_delta: float
|
||||
target_holdout_delta: float
|
||||
sweep: dict[str, int] # family -> negative flips
|
||||
attribution: Attribution # flipped_predicted, flipped_unpredicted, precision
|
||||
cost_usd: float
|
||||
taxonomy_version: str
|
||||
config_version: str
|
||||
|
||||
class LearningJournal: # .openultrasast/learning/journal.jsonl
|
||||
def append(self, record: RoundRecord) -> None: ...
|
||||
def rounds(self) -> tuple[RoundRecord, ...]: ...
|
||||
def rejected_buffer(self, family: str) -> tuple[str, ...]: ... # hypotheses rejected for this family
|
||||
|
||||
class Archive: # .openultrasast/learning/archive.jsonl
|
||||
def record(self, family: str, config_version: str, pair: str, outcome: FamilyOutcome) -> None: ...
|
||||
def winners(self, family: str) -> dict[str, str]: ... # pair -> config version that scores it best
|
||||
```
|
||||
- Persistence: JSONL, one record per line, never rewritten; the HarnessX journal is mirrored when the extra is present, never the source of truth.
|
||||
|
||||
### Logic layer
|
||||
|
||||
#### Classifier
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Assign zero or more families with the deciding tier; measure itself; queue disagreements |
|
||||
| Requirements | 2.1, 2.2, 2.3, 2.4, 2.5 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Tier 1 (deterministic): rule id, sink family from facts, obligation kind, mechanism id, labeled CWE; any hit decides without a model.
|
||||
- Tier 2 (model): `json_object` question with the taxonomy definitions, `thinking` disabled, `logprobs` requested; averaged log-probability under a threshold yields `unknown`.
|
||||
- Tier 3: `unknown`.
|
||||
- Never writes a label; disagreements go to `.openultrasast/learning/review-queue.jsonl`.
|
||||
- Validates every loaded catalog `family` label against the taxonomy at classification time and rejects a fabricated id by name; the corpus loader stores the label as an opaque string.
|
||||
|
||||
**Dependencies**: Inbound: Scorer, Detectors (P0). Outbound: Families (P0), Endpoint (P1, degrades to tier 1 + unknown without a client).
|
||||
|
||||
**Contracts**: Service [x] State [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Classification:
|
||||
families: tuple[str, ...] # () means unknown
|
||||
tier: Literal["static", "model", "none"]
|
||||
confidence: float | None
|
||||
|
||||
def classify_pair(case: PairCase, taxonomy: FamilyTaxonomy, *, client: ChatClient | None) -> Classification
|
||||
def classify_finding(finding: StaticFinding, taxonomy: FamilyTaxonomy, *, client: ChatClient | None) -> Classification
|
||||
def classify_region(path: str, text: str, function: str | None, taxonomy: FamilyTaxonomy, *, facts, client: ChatClient | None) -> Classification
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassifierReport:
|
||||
taxonomy_version: str
|
||||
agreement: float # hierarchical credit vs maintainer labels on reviewed pairs
|
||||
confusion: dict[str, dict[str, int]]
|
||||
random_router: float
|
||||
oracle_router: float
|
||||
per_tier: dict[str, int]
|
||||
unknown: int
|
||||
queued: int
|
||||
|
||||
def measure_classifier(cases: Sequence[PairCase], taxonomy: FamilyTaxonomy, *, client: ChatClient | None) -> ClassifierReport
|
||||
```
|
||||
- Postcondition: every reviewed pair whose label disagrees with the classification appears once in the review queue with both answers and the tier.
|
||||
|
||||
#### Scorer
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Score one pair per family from detector findings, aggregate per family and slice, decide reliable change |
|
||||
| Requirements | 3.1–3.6, 4.1, 4.5 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Detection: a finding whose `family:` tag relates `same` or `parent_child` to the labeled family and whose line lies in the labeled function; `fabricated` scores the maximum penalty; text is never inspected.
|
||||
- Silence: no `same`/`parent_child` finding of the labeled family inside the labeled function on the fixed side; other findings are counted in `other_findings`, never as leaks.
|
||||
- K runs per pair at every evaluation (baseline K = 5, rounds K ≥ 3 as 3.5 requires); reliable change against the last blessed round via a paired test on per-pair outcomes; negative flips per family.
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
```python
|
||||
FamilyOutcome = Literal["pair_correct", "both_flagged", "both_silent", "reversed", "unscorable"]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PairFamilyScore:
|
||||
pair: str
|
||||
family: str
|
||||
runs: tuple[FamilyOutcome, ...] # length K
|
||||
outcome: FamilyOutcome # majority of runs
|
||||
other_findings_vuln: int
|
||||
other_findings_fixed: int
|
||||
rung: Literal["suspicion", "static_corroboration", "proven"]
|
||||
unscorable_reason: str | None
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FamilyMetrics:
|
||||
family: str
|
||||
scorable: int
|
||||
unscorable: dict[str, int] # reason -> count
|
||||
outcomes: dict[FamilyOutcome, int]
|
||||
recall: float
|
||||
silence: float
|
||||
youden: float
|
||||
directional_bias: float # (both_flagged - both_silent) / scorable
|
||||
fixed_fpr_recall: float # recall at the family's FPR ceiling
|
||||
negative_flips: int # vs last blessed round
|
||||
reliable_change: bool
|
||||
taxonomy_version: str
|
||||
|
||||
def score_pair_family(case: PairCase, family: str, vuln_runs: Sequence[Sequence[StaticFinding]], fixed_runs: Sequence[Sequence[StaticFinding]], *, ranges, taxonomy) -> PairFamilyScore
|
||||
def aggregate(scores: Sequence[PairFamilyScore], *, baseline: Sequence[PairFamilyScore] | None) -> dict[str, FamilyMetrics]
|
||||
def unscorable_reason(case: PairCase, *, parse_ok: bool, ranges) -> str | None # identical_twin | unsupported_language | unresolved_label | missing_context | known_limit:<x>
|
||||
```
|
||||
|
||||
#### Split
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | The one rule that selects teachers, and the refusal for anything touching holdout |
|
||||
| Requirements | 5.1, 5.2, 5.3, 5.4 |
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
```python
|
||||
def teachers(cases: Sequence[PairCase]) -> tuple[PairCase, ...] # vendored, gating tier, split == "train", scorable
|
||||
def refuse_if_holdout(taught_by: Iterable[str], cases: Sequence[PairCase]) -> Refusal | None # Refusal(pairs=..., reason="holdout_pair")
|
||||
```
|
||||
- `export_mechanisms`, `evaluate_loo`, `evaluate_profiles`, `evaluate_mechanism_profiles`, `propose_mechanism_edits` and `build_pair_signals` obtain teachers only through `teachers`; refusals are recorded as degradations `{"stage": "learning", "reason": "holdout_pair_refused", "pairs": [...]}`.
|
||||
|
||||
#### Detectors
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Load frozen per-family configurations and run the family detector with whole-file context and curated tools |
|
||||
| Requirements | 6.1, 6.2, 6.3, 6.4, 6.5 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- A configuration is a directory `learning/configs/<family>/` with `prompt.md` (length cap enforced at load), `checklist.md`, `tools.toml` (subset of `read_file`, `grep_repo`, `find_refs`, `read_definition`, `entry_points`, `flows`, `obligations`), `budget.toml` (`max_steps`, `max_cost_usd`, `max_chars`), `hard_negatives.jsonl`, `counterexamples.jsonl`, `version`.
|
||||
- Round zero writes every family's directory from the current hunter prompt; a round rewrites only its target family and bumps that `version`.
|
||||
- The runner builds the user message from the whole file plus resolved definitions, runs `run_tool_hunter` with the family's tools and prompt, and tags findings `family:<id>`, `detector:<id>@<version>`; the generalist is the `unknown` family's configuration.
|
||||
|
||||
**Contracts**: Service [x] State [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class FamilyConfig:
|
||||
family: str
|
||||
version: str
|
||||
prompt: str
|
||||
checklist: str
|
||||
tools: tuple[str, ...]
|
||||
max_steps: int
|
||||
max_cost_usd: float
|
||||
max_chars: int
|
||||
hard_negatives: tuple[dict[str, object], ...]
|
||||
counterexamples: tuple[dict[str, object], ...]
|
||||
|
||||
def load_family_configs(configs_dir: Path, taxonomy: FamilyTaxonomy) -> dict[str, FamilyConfig] # raises ConfigError on cap or tool violations
|
||||
def run_family_detector(root: Path, region: Region, config: FamilyConfig, *, client: ChatClient, model: str) -> list[StaticFinding]
|
||||
```
|
||||
- Precondition: `region.families` from the classifier; postcondition: every finding carries exactly one `family:` tag and one `detector:` tag.
|
||||
|
||||
#### Endpoint
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Resolve the chat endpoint independently of the embedding endpoint and adapt DeepSeek quirks |
|
||||
| Requirements | 6.6 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Resolution order: explicit injection (tests) → `OPENULTRASAST_HUNTER_CLIENT` scripted modes → `DEEPSEEK_API_KEY` with `DEEPSEEK_BASE_URL` (default `https://api.deepseek.com`) → `OPENROUTER_API_KEY` with `OPENROUTER_BASE_URL`. Embeddings keep reading `OPENROUTER_*` only.
|
||||
- `[models]`: `hunter` (default `deepseek-v4.1-flash`), `judge` (default `deepseek-v4.1-flash`), `chat_base_url`, `chat_api_key_env`.
|
||||
V4 Pro was discontinued on 2026-09-10 and its requests route to V4.1 Flash from 2026-09-14, billed at Flash's
|
||||
price. Detector and judge are now the SAME model, so any claim that the judge is a stronger second opinion is
|
||||
a claim about the prompt and the evidence put to it, not about model class.
|
||||
- Adapter: `thinking` disabled and `temperature 0` for detectors and classifier; `reasoning_content` replayed; `response_format json_object` with one retry on empty content; `logprobs` on request; usage fields normalized (`prompt_cache_hit_tokens`) into cost accounting.
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class ChatEndpoint:
|
||||
base_url: str
|
||||
api_key: str
|
||||
provider: Literal["deepseek", "openrouter", "scripted"]
|
||||
thinking: bool
|
||||
price_in_per_m: float
|
||||
price_out_per_m: float
|
||||
|
||||
def resolve_chat_endpoint(config: ResolvedConfig, *, override: ChatClient | None = None) -> tuple[ChatClient, ChatEndpoint] | None
|
||||
class DeepSeekChatClient: # implements ChatClient; wraps OpenRouterChatClient with extra_body and reasoning_content replay
|
||||
def complete(self, *, model, messages, tools, timeout_seconds=60, json_object=False, logprobs=False) -> ChatResponse
|
||||
```
|
||||
|
||||
#### Verifiers
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Confirm a claim through the family's oracle, or refuse to raise it |
|
||||
| Requirements | 7.1–7.6 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- `CanaryVerifier` (injection, path, deserialization, and command execution inside injection): writes a snippet from a family template that imports the target function, plants a canary (row in an in-memory database, file under `/scratch`, object with a marker method) and prints `CANARY:<token>` only when the vulnerability fires; the oracle is the token in stdout, never the exit code. The snippet passes `check_snippet_safety` unchanged.
|
||||
- `StaticVerifier` (access control): the obligations checker's finding on the same function raises to `static_corroboration`.
|
||||
- `NoVerifier`: stays at `suspicion`.
|
||||
- `second_judge`: a `deepseek-v4.1-flash` call with the snippet, the oracle output and the claim, answering `json_object` `{"confirms": bool, "reason": str}`; a proven claim is published only when it confirms.
|
||||
- Templates, oracle code and gold labels live under `learning/verifiers/` and `benchmarks/`, both excluded from every proposer write root.
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
```python
|
||||
class Verifier(Protocol):
|
||||
kind: Literal["canary", "static", "none"]
|
||||
def verify(self, claim: StaticFinding, *, root: Path, language: str, runner: SandboxRunner, limits: SandboxConfig) -> Verification
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Verification:
|
||||
rung: Literal["suspicion", "static_corroboration", "proven"]
|
||||
oracle_output: str
|
||||
reason: str
|
||||
|
||||
def verifier_for(family: Family) -> Verifier
|
||||
def second_judge(verification: Verification, claim: StaticFinding, *, client: ChatClient, model: str) -> bool
|
||||
```
|
||||
- `regress.run_regression(..., oracle=Callable[[SandboxResult], bool] | None)`: when given, `TRIGGERABLE` iff the oracle returns true; exit-code behavior unchanged otherwise.
|
||||
|
||||
### Orchestration layer
|
||||
|
||||
#### Rounds
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Round zero with noise floors; evolve rounds with staged evaluation, acceptance, attribution and revert |
|
||||
| Requirements | 8.1–8.4, 9.1–9.9 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- `run_baseline`: clone the current hunter prompt into every family directory, run K = 5 over scorable vendored pairs of the selected slices, compute per-family negative-flip rate between runs as `NoiseFloor`, persist `noise-floors.json`, publish round zero. Artifacts and floors are keyed by detector model (`baseline/<model>/`), so a second model is baselined by changing `[models].hunter` and re-running; `publish` renders a model comparison table from every baseline present. This is the maintainer's reconsideration path if the default model underperforms; configurations are never touched by a model swap.
|
||||
- `run_learning_round`: failure facts for the target family → proposer → refusal checks (holdout, length cap, allowed levers) → minibatch → holdout → sweep → accept (freeze, bump version, archive) or revert (restore the directory snapshot) → journal → rejected buffer.
|
||||
- Acceptance: `train_delta >= 0 and holdout_delta >= 0 and (train_delta > 0 or holdout_delta > 0) and all(sweep[f] <= floor[f])`; ties reject.
|
||||
- Cost metered per stage from usage; crossing `round_cost_cap_usd` at any stage reverts and records `reverted_cost`.
|
||||
- The proposer receives no holdout material; scoring of holdout happens after the proposal is frozen.
|
||||
|
||||
**Contracts**: Batch [x] State [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class NoiseFloor:
|
||||
family: str
|
||||
k_runs: int
|
||||
negative_flip_rate: float
|
||||
budget_flips: int # ceil(rate * scorable pairs)
|
||||
|
||||
def run_baseline(cases, *, slices, taxonomy, configs_dir, client, model, k_runs=5, out_dir) -> BaselineReport
|
||||
def run_learning_round(cases, *, family, taxonomy, configs_dir, proposer, client, model, journal, archive, floors, cost_cap_usd, minibatch) -> RoundRecord
|
||||
```
|
||||
- Idempotency: a round directory is written under `.openultrasast/learning/rounds/<n>/` with the snapshot, proposal, scores and trajectories; rerunning a round number refuses.
|
||||
- Every stage scores with K runs as the scorer requires; the noise floor is the tolerance for the cross-family sweep, not a substitute for repeated runs.
|
||||
|
||||
#### Proposer
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Draft one structural change for one family from structured failure facts |
|
||||
| Requirements | 9.1, 9.2, 5.4 |
|
||||
|
||||
**Contracts**: Service [x]
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class FailureFacts:
|
||||
family: str
|
||||
misses: tuple[MissFact, ...] # pair, function, sink_or_check, line, verifier_outcome, runs
|
||||
leaks: tuple[LeakFact, ...]
|
||||
rejected: tuple[str, ...] # hypotheses from the buffer
|
||||
config: FamilyConfig
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Proposal:
|
||||
hypothesis: str
|
||||
lever: Literal["tool", "checklist", "counterexample", "memory", "prompt"]
|
||||
change: dict[str, object] # file-level edit inside the family directory
|
||||
predicted_affected: tuple[str, ...]
|
||||
predicted_at_risk: tuple[str, ...]
|
||||
|
||||
class Proposer(Protocol):
|
||||
def propose(self, facts: FailureFacts) -> Proposal | None
|
||||
|
||||
class ScriptedProposer: ... # tests and offline runs
|
||||
class HarnessXProposer: ... # MetaAgent.evolve with allowed_write_roots = the family directory only; degrades to None with a recorded reason without the extra
|
||||
```
|
||||
|
||||
#### Publish
|
||||
|
||||
| Field | Detail |
|
||||
|---|---|
|
||||
| Intent | Regenerate every published number from one command and commit its artifacts |
|
||||
| Requirements | 11.1, 11.2, 4.5 |
|
||||
|
||||
**Contracts**: Batch [x]
|
||||
|
||||
```python
|
||||
def publish(cases, *, taxonomy, journal, floors, out_dir=Path("benchmarks/measurements"), roadmap=Path(".kiro/steering/roadmap.md")) -> PublishReport
|
||||
```
|
||||
- Writes `<date>-<slice>-<family>.json` artifacts, a markdown table with scorable/unscorable counts, taxonomy version, noise floor and cost per point beside every number, and replaces the roadmap's measured section between markers.
|
||||
|
||||
## Data Models
|
||||
|
||||
### Domain Model
|
||||
- Aggregates: `FamilyTaxonomy` (versioned, immutable per version); `FamilyConfig` (per family, versioned, frozen between rounds); `RoundRecord` (append-only); `PairCase` (owned by the corpus, gains two additive fields).
|
||||
- Invariants: a finding carries exactly one `family:` tag; a pair has at most one `unscorable` reason; a round targets exactly one family; the archive maps a pair to one winning configuration per family.
|
||||
|
||||
### Data Contracts
|
||||
- Catalog row additions: `[[pair.expected]] family = "<id>"` (opaque at load, validated by the Classifier), `known_limit = "identical_twin"` for the five twins, `fix_date = "YYYY-MM-DD"` populated by the catalog generator from recipe metadata where available (rows without it are counted as undated in the post-cutoff slice); `unscorable` is computed, never stored.
|
||||
- Manifest block `learning = {taxonomy_version, families: {id: FamilyMetrics}, classifier: ClassifierReport, round: n | null}`; findings carry tags `family:`, `detector:`, `verifier:<rung>`.
|
||||
- Journal and archive JSONL as above; `noise-floors.json`; `review-queue.jsonl` entries `{pair, label_family, classified, tier, confidence}`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Taxonomy or configuration deviations fail fast at load with the field named (`FamiliesError`, `ConfigError`).
|
||||
- Missing extras degrade with a recorded reason: no HarnessX → `ScriptedProposer` or `proposer_unavailable`; no TypeScript grammar → rows `unsupported_language`; no sandbox → `NoVerifier` for canary families with `sandbox_unavailable`.
|
||||
- Endpoint errors: transient retries as today; empty `json_object` content retried once then recorded; a round whose endpoint fails mid-stage is reverted with `endpoint_failed`.
|
||||
- Holdout refusal, cost overrun and safety rejection are outcomes, not exceptions, and always land in the journal.
|
||||
- Monitoring: every stage runs under `HarnessRuntime.run_stage`; cost per stage and per round is written to the round directory.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Unit: taxonomy loader rejects an eleventh family and a missing verifier kind (1.1, 1.2); `related` returns `parent_child` for a CWE child and `fabricated` for an unknown id (3.1); classifier tier 1 decides from a rule id without a client and tier 2 falls to `unknown` under the threshold with a scripted client (2.1, 2.2); scorer counts detection only inside the labeled function and never from text (3.1, 3.6); silence ignores other-family findings (3.2); directional bias and negative flips on synthetic run sets (3.3, 3.5); `unscorable_reason` on an identical twin and an unsupported language (4.1); `teachers` excludes holdout and `refuse_if_holdout` names the pair (5.1, 5.2); configuration loader rejects a prompt over the cap and a tool outside the allowed set (6.1, 6.5); endpoint resolution prefers DeepSeek over OpenRouter and never touches the embedding variables (6.6); canary snippet passes safety and the oracle keys on the token (7.2); acceptance rule rejects ties and budget breaches (9.5); journal round-trips a record and the rejected buffer (9.3, 9.6).
|
||||
- Integration: `pairs --hunter` with a scripted client on the shared handler fixture yields family outcomes and the manifest block (3.x, 6.3); `export_mechanisms` and `evaluate_loo` teach from train only and record refusals (5.1, 5.3); `run_baseline` with a scripted client writes noise floors and round-zero artifacts (8.x); `run_learning_round` with `ScriptedProposer` accepts a helpful change, rejects a tie, reverts on cost and journals attribution (9.x); `run_regression(oracle=)` returns `TRIGGERABLE` on the token with `FakeSandboxRunner` (7.2).
|
||||
- Corpus: TypeScript rows parse under the extra (10.1); the five twins load as `known_limit:identical_twin` and are excluded from denominators (10.3, 4.1); `test_fixture_difficulty` fails on a fixture easier than its named rows (10.4); post-cutoff slice reported separately (10.5).
|
||||
- Gates: `gate`, `map_gate`, `pair_gate` stdout byte-identical to the pre-feature commit; extra-free suite green with semantic and harnessx tests skipping (11.5).
|
||||
- Live (maintainer, not CI): round zero on vibe-py and agent-vfc web families through DeepSeek, artifacts committed under `benchmarks/measurements/`.
|
||||
|
||||
## Security Considerations
|
||||
- Every prompt and every persisted trajectory passes `redact_secrets`; verifier templates, oracle code and gold labels are outside the proposer's write roots; snippets keep today's safety bans; the sandbox stays network-off and workspace read-only; the second judge sees the snippet and oracle output, never the labels.
|
||||
|
||||
## Performance & Cost
|
||||
- Round zero: K = 5 over ~250 vendored pairs, both sides, whole-file context ≈ 25M input tokens; at DeepSeek flash peak pricing under $15, off-peak half. A round: minibatch + holdout + sweep ≈ one baseline pass; `round_cost_cap_usd` default 10. Concurrency bounded by the runtime's existing budget controls.
|
||||
|
||||
## Migration Strategy
|
||||
- Phase 1 (offline, no model): taxonomy, classifier tier 1, scorer, split, unscorable rows, endpoint resolution, TypeScript grammar, identical twins, fixture-difficulty check; existing numbers re-published with denominators.
|
||||
- Phase 2 (live): round zero on web families; noise floors; classifier tier 2 measured; review queue worked.
|
||||
- Phase 3: verifiers for the four canary families and the static verifier; second judge.
|
||||
- Phase 4: evolve rounds with `ScriptedProposer` first, then `HarnessXProposer`; obligations lever as the access-control family's first round.
|
||||
- Rollback: every round is a directory snapshot; `publish` regenerates from artifacts, so a bad round is reverted by restoring the family directory and re-publishing.
|
||||
@@ -1,156 +0,0 @@
|
||||
# Requirements Document
|
||||
|
||||
> **Status 2026-10-02:** retired, superseded by `model-grounded-detection` (spec.json `superseded_by`). 30 of 33 tasks were done; the open three will not be. Kept as decision history; nothing in src, tests or CI points here.
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Introduction
|
||||
|
||||
OpenUltraSAST's detection numbers cannot be trusted and cannot be improved. The static machinery is treated as the detector while the model with tools already outperforms it (12/14 versus 5/14 pair-correct on the vibe-py holdout once scoring is keyed on class and function), the pair scorer keys detection on text tokens rather than on a vulnerability class, the improve loop learns from its own holdout pairs, and no change is attributed to the class it targets, so an improvement in one class silently degrades another. This feature turns the tool into a learning harness: an auto-classifier assigns every labeled pair, finding and code region to one of ten verifier-aligned families; one detector configuration per family is evolved round by round from structured failure facts; a class-aware, pair-wise scorer with unscorable denominators and a measured per-family noise floor decides acceptance; only verifier-confirmed claims are reported above suspicion; and the train/holdout split is enforced by construction. Context, measurements and sources: `brief.md`, `research.md`, `benchmarks/measurements/2026-09-06-*`.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: the class taxonomy and its classifier; the class-aware pair scorer and unscorable-row accounting; split enforcement for every learning path; per-family detector configurations and their routing; class-keyed verifiers and reporting rungs; the round-based evolve loop with attribution; the corpus repairs the loop needs; publishing every number with its denominator and noise floor.
|
||||
- **Out of scope**: finding bugs in JavaScript interpreters, WebGL renderers or browser engines; memory-safety fuzzing or crash oracles for C and C++ (the vfc slice stays a measured, non-gating slice under one coarse family); new hand-written static shape families; changes to the evidence ladder or dispositions; merge gates on non-local slices; vendoring new pairs beyond re-harvesting existing recipes; fine-tuning model weights.
|
||||
- **Adjacent expectations**: the pair corpus (`pair-corpus-honesty`) keeps owning labels, tiers, splits and `known_limit`; `corpus-seeded-mechanisms` keeps its exporter, leave-one-out and lever, which this feature subordinates to the split and the class attribution; `authorization-obligations` keeps its checker, whose corpus tasks 4.3 and 6.1 move here and whose lever task 5 becomes the access-control family's first round; the local detection, map and pair gates stay byte-identical; HarnessX and tree-sitter grammars remain optional extras and every step that needs them degrades to a recorded reason without them.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: A closed, versioned class taxonomy aligned to verifiers
|
||||
|
||||
**Objective:** As a maintainer, I want every unit of detection work to belong to one of a small set of named families, so that detection, silence, leaks and improvements have a definite meaning.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Learning Harness shall define a closed, versioned taxonomy of exactly these families: injection into a query or command; file path from user input; deserialization of untrusted data; access control; output encoding; untrusted destination (server-side request forgery and open redirect); prototype pollution and unsafe object merge; permissive configuration and secrets; memory safety; unknown.
|
||||
2. The Learning Harness shall record for every family the CWE identifiers it covers, the mechanism ids it covers, and the verifier that can confirm a claim in that family or the statement that none exists.
|
||||
3. When the taxonomy changes, the Learning Harness shall bump its version and every published number shall name the taxonomy version it was scored under.
|
||||
4. The Learning Harness shall treat CWE identifiers and mechanism ids as attributes of a family and shall never use them as the key that routes work or scores a pair.
|
||||
|
||||
### Requirement 2: An auto-classifier that is measured on its own
|
||||
|
||||
**Objective:** As a maintainer, I want a classifier that assigns families to pairs, findings and code regions and reports its own accuracy, so that routing errors are visible instead of hidden inside detector numbers.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When given a labeled pair, a finding or a code region, the Learning Harness shall return zero or more families plus, when nothing is admitted, the `unknown` family, and shall record which tier decided each answer: deterministic static signals, a model judgment, or none.
|
||||
2. The Learning Harness shall attempt deterministic classification from static signals before consulting a model, and shall consult a model only for what static signals cannot place.
|
||||
3. When the classifier is evaluated, the Learning Harness shall report agreement with maintainer labels on reviewed pairs with partial credit for a parent–child relation and no credit for a lateral or fabricated family, a confusion matrix per family, and its accuracy against both a random router and an oracle router.
|
||||
4. When the classifier disagrees with a maintainer label, the Learning Harness shall queue the disagreement for review and shall not change the label.
|
||||
5. The Learning Harness shall classify every labeled row in the corpus, including rows whose current class is free text or `other`, and shall report the count classified per family and the count left `unknown`.
|
||||
6. While no family in the taxonomy declares a parent, the Learning Harness shall state beside every agreement figure that hierarchical partial credit could not apply.
|
||||
|
||||
### Requirement 3: A class-aware, pair-wise scorer
|
||||
|
||||
**Objective:** As a maintainer, I want pairs scored on whether the labeled family was found in the labeled function, so that a detector is credited for the bug it was asked about and nothing else.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When scoring the vulnerable side, the Learning Harness shall count a detection only for a finding inside the labeled function whose family matches the labeled family, with partial credit for a parent–child relation and the maximum penalty for a fabricated family.
|
||||
2. When scoring the fixed side, the Learning Harness shall count silence only when no finding of the labeled family lies inside the labeled function, and shall report findings of other families or outside the function separately, never as leaks.
|
||||
3. The Learning Harness shall report every pair as exactly one of pair-correct, both-sides-flagged, both-sides-silent, or reversed, and shall report per family and per slice the counts of each outcome, recall, silence, Youden and a directional-bias index.
|
||||
4. The Learning Harness shall report a fixed-false-positive-rate view per family beside Youden.
|
||||
5. When a detector is evaluated, the Learning Harness shall run it at least three times per pair and shall report a change against the last blessed round only when a reliable-change test passes, together with the negative-flip count per family.
|
||||
6. The Learning Harness shall never count a text token, a CWE string or a sink name in a finding's text as evidence of detection.
|
||||
7. While no family in the taxonomy declares a parent, the Learning Harness shall state beside every published family figure that hierarchical partial credit could not apply.
|
||||
|
||||
### Requirement 4: Unscorable rows and corpus hygiene
|
||||
|
||||
**Objective:** As a maintainer, I want rows that cannot be scored removed from every denominator with a stated reason, so that no number carries a permanent loss that no detector could avoid.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Learning Harness shall mark a pair unscorable with one of these reasons: identical twin, unsupported language, unresolved function label, missing registration context, or a maintainer-declared known limit; and shall exclude it from recall, silence, Youden and negative-flip denominators while still listing it.
|
||||
2. The Learning Harness shall never delete a catalog row; an unscorable row shall keep its reason in the catalog.
|
||||
3. When pairs are loaded, the Learning Harness shall detect identical twins by comparing normalized excerpt bodies with headers stripped, and shall report duplicates across pairs before any split is applied.
|
||||
4. The Learning Harness shall assign train and holdout membership by repository and in date order, so that no two pairs from one repository straddle the split.
|
||||
5. When a published number is shown, the Learning Harness shall show the scorable count, the unscorable count by reason, and the taxonomy version beside it.
|
||||
|
||||
### Requirement 5: The split is enforced by construction
|
||||
|
||||
**Objective:** As a maintainer, I want it impossible for any learning path to see a holdout pair, so that a holdout number means what it says.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When any exporter, lever or evolve round seeds, proposes or validates from pairs, the Learning Harness shall use only train-split pairs as teachers.
|
||||
2. If a candidate change was taught by, or touches, any holdout pair, then the Learning Harness shall refuse it and record the refusal with the pair names.
|
||||
3. The Learning Harness shall re-measure the existing mechanism lever under this rule and shall publish the corrected holdout number in place of the earlier one.
|
||||
4. While an evolve round proposes changes, the Learning Harness shall not expose holdout inputs, outputs or scores to the proposer.
|
||||
|
||||
### Requirement 6: One detector configuration per family
|
||||
|
||||
**Objective:** As a maintainer, I want each family to have its own detector configuration that other families' changes cannot alter, so that improvements do not interfere.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Learning Harness shall hold one detector configuration per family consisting of a prompt under a stated length cap, a curated tool set, a step budget, a cost budget, an evidence checklist and a set of hard negatives and counterexamples.
|
||||
2. While a round evolves one family, the Learning Harness shall keep every other family's configuration unchanged.
|
||||
3. When a code region is classified, the Learning Harness shall run the detector of every admitted family plus a generalist detector, and shall record on each finding the family, the detector configuration version and the verifier outcome.
|
||||
4. The Learning Harness shall give a detector the whole file of a region and the files its curated tools resolve, not only an excerpt around a hotspot.
|
||||
5. The Learning Harness shall expose to a detector only curated tools that read, search and resolve symbols within the scanned repository, and no general shell.
|
||||
6. The Learning Harness shall run detectors, the classifier's model tier and the evolve proposer on a configurable chat endpoint that is independent of the embedding endpoint, with a cheap model as the default and a stronger model reserved for the second judge and for review-queue disagreements.
|
||||
|
||||
### Requirement 7: Only a verifier raises a claim above suspicion
|
||||
|
||||
**Objective:** As an operator, I want to know that anything reported above suspicion was confirmed by something other than the model, so that reports stay honest.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Learning Harness shall report a detector claim at `suspicion` unless a verifier keyed to the claim's family confirms it.
|
||||
2. Where a family has a sandbox oracle, the Learning Harness shall confirm a claim by executing a family-specific snippet whose oracle is a canary observable only through the vulnerability (a canary row for injection, a canary file for command execution and traversal, a canary object for deserialization).
|
||||
3. Where a family has only static corroboration (access control through the obligations checker), the Learning Harness shall raise a claim no higher than static corroboration.
|
||||
4. Where a family has no verifier, the Learning Harness shall report at `suspicion` only and shall still score that family class-aware on the corpus.
|
||||
5. When a claim is reported as proven, the Learning Harness shall obtain a second, independent judgment before publishing it as proven.
|
||||
6. The Learning Harness shall keep verifier code, oracles and gold labels outside every surface an evolve round can edit.
|
||||
|
||||
### Requirement 8: Round zero establishes the baseline and the noise floor
|
||||
|
||||
**Objective:** As a maintainer, I want a controlled baseline and a measured noise floor per family before any learning, so that later gains and regressions are distinguishable from noise.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When round zero runs, the Learning Harness shall clone the current hunter into every family's configuration unchanged.
|
||||
2. When round zero runs, the Learning Harness shall run each family's detector five times over the scorable vendored pairs at deterministic settings and shall record the per-family negative-flip rate between runs as that family's regression budget.
|
||||
3. The Learning Harness shall publish round zero's per-family outcomes, noise floors and cost per pair before the first evolve round.
|
||||
4. The Learning Harness shall run round zero first on the web families over the vibe-py and agent-vfc slices; the memory-safety family over the vfc slice is measured but never gates a round.
|
||||
|
||||
### Requirement 9: An evolve round improves one family under attribution
|
||||
|
||||
**Objective:** As a maintainer, I want each round to change one thing for one family and to be accepted only when the change helps that family without hurting any other, so that the system cannot degrade in ways nobody predicted.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When a round starts, the Learning Harness shall give the proposer structured failure facts for the target family (which pair, which function, which sink or check, which line, which verifier outcome) and the buffer of previously rejected proposals, and shall not give it free-form self-critique or holdout material.
|
||||
2. The Learning Harness shall accept at most one change to one family per round, and the change shall be a tool, a checklist, a counterexample, a memory entry, or a prompt edit within the family's length cap.
|
||||
3. When a round records its proposal, the Learning Harness shall journal a hypothesis, the levers changed, the family predicted to improve, and the families predicted to be at risk.
|
||||
4. When a round is evaluated, the Learning Harness shall score the target family on a train minibatch, then on the target family's full holdout, then sweep every other family's holdout.
|
||||
5. If the target family's train score or holdout score decreases, or neither strictly increases, or any other family's negative flips exceed its regression budget, then the Learning Harness shall reject the round, revert the change and record the reason.
|
||||
6. When a round is accepted or rejected, the Learning Harness shall record attribution: which pairs flipped in the predicted family, which flipped in unpredicted families, and the resulting precision of the prediction.
|
||||
7. The Learning Harness shall keep, per family, an archive of which configuration wins which pair, rather than one champion configuration.
|
||||
8. If a round exceeds its cost cap, then the Learning Harness shall stop it, revert it and record the overrun.
|
||||
9. The Learning Harness shall retain a model-authored skill, tool or prompt edit only after it passed the acceptance rule and was journaled with its attribution.
|
||||
|
||||
### Requirement 10: Corpus repairs the loop depends on
|
||||
|
||||
**Objective:** As a maintainer, I want the corpus defects that cap every number fixed before detectors are tuned against it, so that a detector change and a corpus artifact are distinguishable.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The Learning Harness shall parse TypeScript rows so that they are scorable rather than counted as unsupported.
|
||||
2. When absence-labeled rows are re-harvested, the Learning Harness shall keep each handler with its decorators and registration statements, and every re-harvested excerpt shall keep its license line and pass redaction.
|
||||
3. The Learning Harness shall mark the five byte-identical vibe-py twins unscorable with the reason identical twin.
|
||||
4. If a unit-test fixture is strictly easier than the corpus rows its test names (fewer binding hops to the identity source, registration present when the corpus rows lack it), then the Learning Harness shall fail that check.
|
||||
5. The Learning Harness shall carry a slice of pairs dated after the detector model's training cutoff and shall report it separately.
|
||||
|
||||
### Requirement 11: Publishing and operations
|
||||
|
||||
**Objective:** As a maintainer, I want every number regenerated from one command with its denominator, noise floor and cost, so that the roadmap cannot drift from the measurement.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. When the roadmap numbers are regenerated, the Learning Harness shall produce them from one command and shall write the command, the taxonomy version, the scorable and unscorable counts, the noise floor and the cost per point beside every number.
|
||||
2. The Learning Harness shall commit the raw measurement artifacts of every published number.
|
||||
3. The Learning Harness shall persist every detector trajectory and every round's journal, redacted, so that a round can be replayed.
|
||||
4. If a budget cap, a failing endpoint or a missing optional extra interrupts a run, then the Learning Harness shall record the reason and complete the remaining steps that do not depend on it.
|
||||
5. The Learning Harness shall leave the local detection, map and pair gate outputs byte-identical to the pre-feature commit.
|
||||
@@ -1,137 +0,0 @@
|
||||
# Research & Design Decisions: learning-harness
|
||||
|
||||
## Summary
|
||||
|
||||
- **Feature**: `learning-harness`
|
||||
- **Discovery Scope**: Complex Integration (extension of pairs, tool hunter, provider, regress, improve; adoption of HarnessX meta-harness; corpus repairs)
|
||||
- **Key Findings**:
|
||||
- The tool hunter already beats the overlay 12/14 vs 5/14 on the class-aware, scorable vibe-py holdout; the published 0/17 was the text-token scorer.
|
||||
- Every learning path ignores the split (`evaluate_profiles`, `evaluate_mechanism_profiles`, `propose_mechanism_edits`, `evaluate_loo`, `build_pair_signals`, `export_mechanisms`); one teacher rule fixes all of them.
|
||||
- The sandbox verdict is exit-code only and snippets are compile-only templates; a canary oracle is an additive callback that respects the existing safety bans.
|
||||
- The chat and embedding clients share one base-URL variable; DeepSeek needs thinking disabled, `reasoning_content` replay, no `/v1`, `json_object` only, and offers no seed and no embeddings.
|
||||
- HarnessX 0.1.0 already provides the round primitive (`MetaAgent.evolve`, journal with `predicted_affected` and precision discounted by unpredicted regressions); the tool supplies tasks, trajectories, the evaluator and routing.
|
||||
- The field routes on coarse verifier-aligned families, gates reports on proof, and reports regression blindness in meta-agents; per-instance archives and no-regression acceptance rules are the published remedies.
|
||||
|
||||
## Research Log
|
||||
|
||||
### Codebase seams (2026-09-06, Explore subagent over `src/openultrasast`)
|
||||
- **Context**: design an extension without re-reading every module.
|
||||
- **Findings**: `run_tool_hunter(root, hotspots, *, client, model, max_steps)` with three curated tools and `suspicion` findings tagged `tool-hunter`; `ChatClient.complete(*, model, messages, tools, timeout_seconds)`; `OpenRouterChatClient.complete_chat` always sends `temperature: 0` and drops `reasoning_content`; `_hunter_matches_expected` is text-token based and every fixed-side finding leaks under `fix_policy = silent`; `run_regression(..., sandbox, sandbox_limits, images)` with `verdict_from_result` on exit code; `check_snippet_safety` bans sockets, curl and workspace writes; `MechanismStore` append-only JSONL with tombstones; `improve.run_round` journal has no hypothesis, attribution or cost fields and its regression check is per provenance profile; `load_dotenv` no-ops under pytest; `pyproject` semantic extra lacks TypeScript; `pair_gate` scores only vendored `local` pairs.
|
||||
- **Implications**: the design adds parameters and additive fields instead of replacing modules; the family scorer is injected into the hunter path so `pairs` never imports `learning`; the oracle is an optional callback on `run_regression`; endpoint resolution takes an explicit override for tests.
|
||||
|
||||
### DeepSeek platform API (2026-09-06, api-docs.deepseek.com)
|
||||
- **Findings**: base `https://api.deepseek.com` (no `/v1` in current docs), Anthropic-shape endpoint at `/anthropic`; models `deepseek-v4-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-vision-exp`; 1M context, 384K output; thinking on by default (`thinking: {"type": "disabled"}` to turn off; `temperature` ignored while thinking); tools with `tool_choice`, `reasoning_content` must be replayed on tool turns; `response_format json_object` only, occasional empty content; `logprobs`/`top_logprobs` documented, unverified in thinking mode; no `seed`, `n`, `logit_bias`; flash 0.44/1.32 USD per M in/out peak, cache hit 0.014, pro 1.32/3.96, off-peak half; concurrency 2500 flash / 500 pro; no embeddings endpoint.
|
||||
- **Implications**: detectors and classifier run with thinking disabled and temperature 0; determinism is best-effort and the K-run noise floor absorbs it; the classifier's model tier uses `logprobs` averaged and falls to `unknown`; embeddings stay on OpenRouter; cost accounting reads DeepSeek's cache-hit usage fields.
|
||||
|
||||
### Measurement that triggered the spec (2026-09-06)
|
||||
- See `brief.md` Problem section and `benchmarks/measurements/2026-09-06-*`.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
| Option | Description | Strengths | Risks / Limitations | Notes |
|
||||
|--------|-------------|-----------|---------------------|-------|
|
||||
| Keep static machinery as the detector, add a class field | Minimal change | Cheap | Ceiling by construction; measured 5/14 vs 12/14 | Rejected |
|
||||
| One generalist hunter, evolved as a whole | Single config | Simple loop | Aggregate fitness hides per-class regressions; the maintainer's oscillation observation; regression blindness in the literature | Rejected |
|
||||
| Per-family detector set with auto-classifier, per-family attribution, verifier-gated reporting | This design | Matches AIxCC routing practice, GEPA per-instance archives, slice-based gating | Router error (63.8% in MoEVD); mitigated by multi-label routing plus a generalist and by measuring the router separately | **Selected** |
|
||||
| Fine-tune a model per class | Weights | Potentially strong | Out of scope; label noise 25–60% in public sets; not reproducible offline | Rejected |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Decision: families are the only routing and scoring key
|
||||
- **Context**: 1.4, 3.6; text tokens and CWE ids were the proxy.
|
||||
- **Alternatives**: fine CWE routing; mechanism ids as key.
|
||||
- **Selected**: ten coarse families with CWE and mechanism as attributes; hierarchical relation for partial credit.
|
||||
- **Rationale**: fine CWE from code is unsolved (≤14.7% top-1) and coarse families are what worked in AIxCC and MoEVD.
|
||||
- **Trade-offs**: less granular reporting; compensated by attributes on findings.
|
||||
|
||||
### Decision: the scorer is injected into the hunter path, not imported by `pairs`
|
||||
- **Context**: dependency direction; `pairs` is owned by `pair-corpus-honesty`.
|
||||
- **Selected**: `_evaluate_hunter_pair` accepts a matcher/leak callable; the CLI wires `learning.scoring`.
|
||||
- **Trade-offs**: one more parameter; keeps `pairs` free of `learning` and the overlay path unchanged.
|
||||
|
||||
### Decision: canary oracle as an optional callback on `run_regression`
|
||||
- **Context**: 7.2; exit code cannot express "the vulnerability fired".
|
||||
- **Alternatives**: a parallel verifier runner; parsing stderr.
|
||||
- **Selected**: `oracle: Callable[[SandboxResult], bool] | None`; token in stdout.
|
||||
- **Trade-offs**: minimal change to regress; templates per family live in `learning/verifiers/`.
|
||||
|
||||
### Decision: HarnessX proposer behind the seam, scripted proposer first
|
||||
- **Context**: 9.x with core `dependencies = []`.
|
||||
- **Selected**: `Proposer` protocol; `HarnessXProposer` composes `MetaAgent.evolve` with `allowed_write_roots` = the family directory; `ScriptedProposer` for tests and phase 4 start.
|
||||
- **Trade-offs**: two proposers; the loop is testable offline and the extra stays optional.
|
||||
|
||||
### Decision: noise floor from K = 5 baseline runs is the regression budget
|
||||
- **Context**: 8.2, 9.5; no public numeric negative-flip budget exists.
|
||||
- **Selected**: per-family negative-flip rate between seed runs; `budget_flips = ceil(rate × scorable)`.
|
||||
- **Trade-offs**: a noisy family gets a loose budget; reported beside the number so it is visible.
|
||||
|
||||
### Decision: DeepSeek adapter wraps the existing OpenRouter client
|
||||
- **Context**: 6.6; both clients share one base URL today.
|
||||
- **Selected**: `ChatEndpoint` resolution order DeepSeek → OpenRouter → scripted; adapter adds `extra_body` and `reasoning_content` replay; embeddings untouched.
|
||||
- **Trade-offs**: one adapter class; no new dependency.
|
||||
|
||||
## Risks & Mitigations
|
||||
- Router error routes a region to the wrong family → multi-label routing plus the generalist; router measured separately against random and oracle.
|
||||
- No seed on DeepSeek → K runs and the reliable-change test; determinism recorded as best-effort.
|
||||
- Canary templates cannot express some families (XSS, SSRF) → those report at `suspicion` only, stated in the taxonomy.
|
||||
- Proposer confabulation → structured failure facts only; rejected buffer; length cap; structural levers preferred.
|
||||
- Cost drift → per-stage metering, per-round cap with revert, cost per point published.
|
||||
- Re-harvest needs network and licenses → maintainer step; excerpts re-redacted; failures leave the old excerpt and a recorded reason.
|
||||
|
||||
## References
|
||||
- Codebase seams and DeepSeek facts: this file, sections above.
|
||||
- External literature: the four threads below.
|
||||
|
||||
---
|
||||
|
||||
## External research (2026-09-06)
|
||||
|
||||
Web research conducted 2026-09-06 in four threads by research subagents; summaries are theirs, condensed, with primary sources. Speculation is marked. This section backs the "What others learned" section of `brief.md`.
|
||||
|
||||
## 1. Cyber reasoning systems (AIxCC finals 2025, Big Sleep, XBOW, Glasswing)
|
||||
|
||||
Primary cross-team source: USENIX Security '26 SoK on AIxCC, arXiv 2602.07666. 42-b3yond-6ug and Lacrosse have no first-party write-up.
|
||||
|
||||
| System | Decomposition / routing key | LLM vs tools |
|
||||
|---|---|---|
|
||||
| Atlantis (1st), arXiv 2509.14589, team-atlanta.github.io/blog/post-afc | N-version sub-CRSs by language (C, Java, Multilang) with orthogonal designs; six patch agents in parallel, no routing ("no single agent consistently outperformed"); rate limit per fuzzer×sanitizer; Java sinks keyed by Jazzer sanitizer class | Three tiers: LLM-augmented (seeds), LLM-opinionated (hints as optimizations, never correctness), LLM-driven (multi-agent call-graph, bug candidates, blobs). Fuzzers/concolic are ground truth |
|
||||
| Buttercup (2nd), blog.trailofbits.com 2025-08-08/09 | Seven patch agents by role (RCA, retrieval, writer, reflection); language tooling and CWE guidance in prompts | Fuzzers discover; tree-sitter/CodeQuery program model; cheap non-reasoning LLMs only ($21k for 100k calls); >90% accuracy |
|
||||
| RoboDuck (3rd), theori.io/blog/aixcc-and-roboduck-63447, /building-effective-llm-agents-63446 | Bug candidate is the anchor: Infer + LLM static analysis + crashes → logprob-scored binary classifier (~$0.001/report) → top 20% to a source-browsing agent (~$0.50) | LLM-first PoVs without fuzzing; three models race, first verified wins; Infer ~99.9% FP |
|
||||
| FuzzingBrain (4th), arXiv 2509.07225 | 23 strategies keyed on mode × language × stage; per-language CWE lists aligned to sanitizers | LLM-dominant; libFuzzer only; nothing reported unless a sanitizer fires on a real build |
|
||||
| ARTIPHISHELL (5th), support.shellphish.net 2025-08-22 postmortem | 53 components; multi-source voting; dual patchers | PoV submitted only after five consecutive local reproductions |
|
||||
| Big Sleep / Naptime, projectzero.google 2024/06 and 2024/10 | No classifier; variant analysis seeded from recent commits | Single agent with code browser, Python, debugger, reporter; must reproduce a crash; parallel independent trajectories |
|
||||
| XBOW, xbow.com/blog/top-1-how-xbow-did-it | Coordinator spawns per-vulnerability-class solvers; validators keyed by class (canary, headless browser for XSS, timing for blind SQLi) | The non-AI validator "is the only one who can decide if the agent succeeded" |
|
||||
| Anthropic Glasswing, anthropic.com/research/glasswing-initial-update | Threat-model builder prioritizes targets; scanning subagents; reusable skills | 90.6% of 1,752 findings valid after human reproduction; internals unpublished |
|
||||
|
||||
Lessons: (1) route on mode × language × sanitizer/CWE family, never fine CWE; route to what the verifier can check. (2) Only proof survives; the accuracy multiplier (Atlantis 0.9999 vs 0.9044) decided the ranking; RoboDuck lost 16.3 points to FPs. (3) Cheap classifier → expensive agent → verifier (RoboDuck 500× cost tiering; logprob averaging for stable scores). (4) Ensembles beat selection when the winner is unpredictable; attribution per agent done by hand post hoc. (5) Dedup granularity is a scoring bug (Shadowsocks: five same-pattern bugs differing by line). (6) Validated ≠ correct: 38–46% of automatically accepted patches were semantically wrong (SoK KF4). (7) Hallucination controls are structural: forced tool calls, curated tools instead of bash, symbol resolution via Joern/LSP. (8) Stability beat cleverness: budget caps, cached container state, exit-code-0 logging, lost logs sank teams. (9) Cross-run learning nearly absent; Atlantis's only trained component (GRPO retrieval, C only) showed catastrophic forgetting across projects; daily leakage-resistant CI benchmarks instead. (10) Cost is a control: >$1,000/hour unbounded (arXiv 2603.08566); Buttercup $181/point.
|
||||
|
||||
## 2. Self-improving harness methods
|
||||
|
||||
| Method | Evolved unit | Search operator | Validation |
|
||||
|---|---|---|---|
|
||||
| DSPy MIPROv2 (dspy.ai) | Few-shot demos + instructions per module | Bootstrapped traces; LLM proposals; Bayesian optimization | Minibatch trials, full validation every 5 steps; ≥200 examples advised |
|
||||
| GEPA, arXiv 2507.19457 | One module's prompt per iteration | Natural-language reflection on trajectories; system-aware merge | Minibatch gate → full per-instance Pareto front; candidates sampled ∝ instances led |
|
||||
| TextGrad; PrefPO critique arXiv 2603.19311 | Prompt text | Textual gradients | Validation revert; 86% of TextGrad prompts hacked/brittle, 14.7× length inflation |
|
||||
| OPRO 2309.03409 / EvoPrompt 2309.08532 / PromptBreeder 2309.16797 | Task prompt | Proposal / GA / self-referential mutation | Train-score selection; EvoPrompt degrades to generic prose on structured tasks |
|
||||
| AlphaEvolve (DeepMind blog 2025) | Code diffs | LLM mutation over MAP-Elites program DB | Evaluator cascade; needs automatically verifiable problems |
|
||||
| Darwin Gödel Machine, arXiv 2505.22954, sakana.ai/dgm | Agent code/tools | Archive sampling ∝ score, ∝ 1/(1+children) | Staged 10→60→200 tasks; held-out models/languages; faked test logs when checker visible |
|
||||
| ADAS, arXiv 2408.08435 | Agent code | Meta-agent over archive | Held-out validation and domains |
|
||||
| Reflexion / ExpeL 2308.10144 / AWM 2409.07429 / Voyager 2305.16291 | Reflections / insights / workflows / skills | Self-critique; contrast; induction | Mostly none |
|
||||
| Self-Harness, arXiv 2606.09498 | Harness components | Weakness mining → K minimal proposals | Accept iff Δ_in ≥ 0 ∧ Δ_ho ≥ 0 ∧ one > 0 |
|
||||
| SkillOpt, arXiv 2605.23904 | SKILL.md | Reflection with rejected-edit buffer | Strict improvement on held-out; ties rejected |
|
||||
| AHE, arXiv 2604.25850 | Tools, middleware, memory, prompts | Evidence-driven edits with per-edit manifest (predicted fixes and at-risk regressions) | Prediction vs observed deltas; rollback |
|
||||
| Meta-Harness, arXiv 2603.28052 | Harness code | Agentic proposer over prior candidates and traces | Held-out models and tasks |
|
||||
|
||||
Lessons: (1) per-instance/per-class tracking doubled gains vs greedy best-overall (GEPA +12.44% vs +6.05%). (2) No-regression acceptance rule; reject ties (Self-Harness, SkillOpt). (3) Regression blindness: AHE fix-prediction precision 33.7% but regression prediction 11.8%; the cross-class sweep is mandatory. (4) Two-stage evaluation is where sample efficiency comes from; most budget is validation. (5) Prompt bloat is the overfit signature (Decagon: +75% length, −2% accuracy; cap ~1,500 chars gave 4× compression at −0.8%). (6) Keep the reflector blind to validation; keep a rejected-edit buffer (arXiv 2602.22483; SkillOpt). (7) Structural edits transfer (+5.1–10.1 points across model families), system-prompt-only edits regressed −2.3 (AHE ablation). (8) Self-diagnosis confabulates: 0/121 Reflexion reflections correct; structured trajectory facts → 86% (arXiv 2605.29463). (9) Hide the checker (DGM). (10) One objective per round (arXiv 2605.26046: joint criteria −59% task focus). (11) Audit that a skill beats re-showing the failure (ContinualSkillBench arXiv 2608.03874). (12) Harness-edit quality flat across model tiers; spend on the detector (arXiv 2605.30621). Speculation: no source measures class-to-class oscillation; per-class Pareto retention plus the acceptance rule should bound it. Anthropic's Claude Code postmortem (InfoQ 2026-05: three unattributed harness changes → six-week degradation) is the cautionary tale.
|
||||
|
||||
## 3. Dataset pitfalls and class taxonomies
|
||||
|
||||
Scoring: (1) dedup by normalized body hash before splitting; split by commit, chronologically (PrimeVul arXiv 2403.18624; test-set copies 12.7–18.9% in older sets). (2) Automatic labels 25–60% correct (BigVul 25%, CVEfixes 51.7%, DiverseVul 60%; Croft et al. ICSE'23 20–71% wrong, 17–99% duplicates; CleanVul arXiv 2411.17274). (3) Pair-wise outcomes P-C/P-V/P-B/P-R, not F1 (GPT-4 CoT P-C 12.94% vs P-V 54.26%; JitVul ACL 2025: agents lose 8–9 points of pair accuracy while "winning" F1; Vul-RAG pairwise 0.21 vs plain 0.61). (4) Directional-bias index (CWE-Trace arXiv 2606.20502: −85.5 to +94.8 points at ~50% accuracy). (5) Score at fixed FPR (PrimeVul VD-S; VFC benchmark arXiv 2605.13138). (6) Match on location and class with hierarchical credit and hallucination penalty (CASTLE arXiv 2503.09433; ALPHA arXiv 2601.01320). (7) Robustness to renames/whitespace/dead code (SecLLMHolmes arXiv 2312.12575; patched-code FPs dominate). (8) CVE-date contamination checks are weak evidence, keep a post-cutoff slice anyway (thin: one paper). (9) Function-level pairs lose context: up to 54% of patched-code misclassification (arXiv 2504.13474). (10) Run-to-run consistency 8.3–81.9% by model (ALPHA).
|
||||
|
||||
Taxonomy: (1) fine CWE from code unsolved (top-1 ≤ 14.7% over 74 CWEs; 86.5% lateral errors; VulDetectBench arXiv 2406.07595 >80% coarse, <30% detailed). (2) From CVE text ~85–90% ceiling (arXiv 2603.14911; CweAgent arXiv 2608.21977). (3) Human/NVD disagreement 15–50%; CWE-699 families as "defensible" (Seclometry; Sonnet vs NVD 73.1% exact, 84.5% with parent–child, LLM right in 72/100 disagreements). (4) Score hierarchically. (5) Few aggregated classes: MoEVD arXiv 2501.16454 collapsed 88 → 12 families, per-class experts +12.8% F1, router 63.8% right, F1 0.18 when wrong; three-class OOB/UAF/other 87.4% (arXiv 2509.22796). (6) Specialists need hard negatives (arXiv 2408.02329: F1 0.89 in class → 0.12–0.19 on full corpus). (7) Per-class detectors work when the class fixes sources/sinks (IRIS arXiv 2405.17238: 55 vs 27 CodeQL detections, FDR 46–85%). (8) VFC typing immature (F1 0.53–0.70). Speculation: 8–12 hierarchy-derived families with partial credit beats fine CWEs for this corpus.
|
||||
|
||||
## 4. Routing and regression control
|
||||
|
||||
Router: (1) learned routers plateau 10–20 points below oracle, sometimes lose to random (RouteLLM arXiv 2406.18665; RouterBench arXiv 2403.12031; arXiv 2606.07587). (2) Evaluate router AUROC separately from expert strength (arXiv 2602.11877). (3) Small embedding classifier fine-tuned on own labels: 80.4% → 98.5% (Red Hat vLLM Semantic Router, 2026-06). (4) Cascades tolerate router error until judge error > 0.2 (FrugalGPT arXiv 2305.05176; RouterBench). (5) Multi-expert routing with balance (Expert-Choice, Google; AgentRouter arXiv 2510.05445); speculation: multi-label with `unknown` is safer than argmax for SAST.
|
||||
|
||||
Attribution: (6) slice everything (Oakden-Rayner arXiv 1909.12475; Slice-based learning 1909.06349; Overton 1909.05372; Model Cards 1810.03993). (7) Negative flips per item (PCT arXiv 2011.09161: 5.2% NFR at equal accuracy; MUSCLE 2407.09435: 8–49% NFR across Llama versions; arXiv 2604.27405: +1.6–2.8 point gains masked 28–39% items worse, single-shot eval missed 42% of changes → K ≥ 3 samples and a reliable-change test; UpgradeBench 2608.20918: neutral retraining breaks 1–5%). (8) Frozen per-class configurations (+20%, Progressive Prompts arXiv 2301.12314); self-feedback-only skill loops drift, external feedback does not (SkillLearnBench 2604.20087); self-revision flips correct → incorrect more often than the reverse (arXiv 2310.01798). (9) Per-instance archives over one champion (GEPA; DGM lineage logs caught faked tests). (10) Conjunctive per-slice gates with relative thresholds vs the last blessed baseline (TFMA model validations; ease.ml/ci arXiv 1903.00278; Amazon Alexa regression-free updates). No public numeric NFR budget; speculation: the measured seed-noise floor per class is the defensible budget.
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"feature_name": "learning-harness",
|
||||
"created_at": "2026-09-06T06:32:41Z",
|
||||
"updated_at": "2026-09-08T00:00:00Z",
|
||||
"language": "en",
|
||||
"phase": "implementation",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": true,
|
||||
"depends_on": [
|
||||
"pair-corpus-honesty",
|
||||
"corpus-seeded-mechanisms",
|
||||
"authorization-obligations",
|
||||
"tree-sitter-overlay-extra",
|
||||
"harnessx-self-improving-rulesets"
|
||||
],
|
||||
"approval_note": "2026-09-06 requirements approved by the maintainer in session ('approve the requirements and run the design phase'); 2026-09-06 design approved by the maintainer in session ('approve the design and generate the tasks'); 2026-09-06 tasks approved by the maintainer in session ('approve the tasks and go on with the implementation'); 2026-09-06 requirements amended by the maintainer in session ('amend the requirements to untrusted_destination and note the flat taxonomy'): Req 1.1 names untrusted destination instead of server-side request forgery, and Req 2.7 and 3.7 require every agreement and family figure to state that hierarchical partial credit could not apply while the taxonomy is flat; 2026-09-06 Req 6.1 amended by the maintainer in session ('drop the skills set from the requirement'): a detector configuration has no skills set, because the skill router belongs to the tool's earlier shape and a family's prompt, checklist, tools and counterexamples already carry what a round evolves; 2026-09-06 design amended during task 4.1 (recorded in design.md): a recipe may name cross-file registration documents, because connexion registers by operationId in an OpenAPI file and express in a router module, so Req 10.2 is unreachable from a single-file excerpt for those rows; 2026-09-06 task 5.3 amended by the maintainer in session (asked and answered): the first evolve rounds target `injection` rather than `access_control`, because under the split and review-tier rules access_control has one train teacher and four holdout pairs and teaches zero obligation shapes, while injection has twelve teachers and ten holdout pairs; 2026-09-08 SUPERSEDED by execution-grounded-detection: this session's measurements showed the noise architecture (K-runs, per-family floor, regression budget, sign test, evolve loop) is a statistical treatment for a structural problem, and that the field (Big Sleep, AIxCC finals, Clearwing) grounds detection in execution rather than in scoring against a labeled corpus. The taxonomy, the IR candidate enumerator (96.6% ceiling) and the obligation checker survive as the web/logic detection path to contribute to a Clearwing-based substrate.",
|
||||
"superseded_by": "model-grounded-detection"
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
# Implementation Plan
|
||||
|
||||
- [x] 1. Foundation: taxonomy, endpoint, corpus fields, grammar
|
||||
- [x] 1.1 Closed family taxonomy with a verifier per family
|
||||
- Ten families exactly as decided, each with its CWE set, mechanism set, verifier kind (canary, static, none) and description; `unknown` always present with no verifier; a version string.
|
||||
- Loader rejects an unknown field, an eleventh family, a missing verifier kind or a CWE claimed by two families, naming the offender; relation lookup answers same, parent–child, lateral or fabricated for any two ids.
|
||||
- Observable: the taxonomy loads from the ruleset directory, a fixture with a fabricated family fails by name, and every mechanism id in the mechanism vocabulary resolves to exactly one family.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4_
|
||||
|
||||
- [x] 1.2 Independent chat endpoint with the DeepSeek adapter
|
||||
- Endpoint resolution order: explicit override, scripted client modes, DeepSeek key and base URL, then OpenRouter; the embedding client keeps reading only its own variables; `[models]` gains the hunter and judge defaults and the endpoint keys, `[learning]` gains its section.
|
||||
- Adapter disables thinking and sends temperature zero for detector and classifier calls, keeps the provider's reasoning field on assistant messages so a caller can replay it, supports JSON-object mode with one retry on empty content and optional log-probabilities, and reports normalized usage and cost from the provider's cache-hit fields.
|
||||
- Observable: with both provider keys set the hunter client targets the DeepSeek base URL without a version suffix while the embedding client still targets OpenRouter; a scripted client is chosen under the existing environment flag; a recorded request shows thinking disabled and the reasoning field retained on the reply.
|
||||
- _Requirements: 6.6, 11.4_
|
||||
|
||||
- [x] 1.3 Additive corpus fields and hygiene at load
|
||||
- Expected rows accept an opaque family id (validated later by the classifier task, so the corpus module never imports the learning package); a pair carries a computed unscorable reason (identical twin by normalized body comparison with headers stripped, computed only when the catalog does not already declare a known limit) and is never dropped; duplicates across pairs are reported before any split; a repository-and-date split helper assigns train or holdout to rows that lack a split, keeps declared splits, and reports rows from one repository that straddle the split; pair signals take a split filter.
|
||||
- Observable: the five identical vibe-py twins load with the identical-twin reason and the existing catalogs load with unchanged row counts; the straddle report on the current catalogs is produced and committed.
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||
|
||||
- [x] 1.4 TypeScript rows parse under the extra
|
||||
- The semantic extra gains the TypeScript grammar and the parser maps TypeScript and TSX to it; the extra-free suite still skips them with the existing marker.
|
||||
- Observable: every vendored TypeScript pair parses on both sides under the extra, and the parser reports no unsupported-language result for TypeScript or TSX.
|
||||
- _Requirements: 10.1_
|
||||
|
||||
- [x] 1.5 Curated hunter tools and hunter-loop parameters
|
||||
- Four new repository tools beside the existing three: resolve a definition by symbol, list entry points, list flows for a function, list obligations for a function, each clamped to the scanned repository and returning identifiers and line spans only; the hunter loop gains prompt, context files, tool set, character limit and tag parameters with today's behavior as defaults and replays the provider's reasoning field across tool turns.
|
||||
- Observable: the existing hunter tests pass unchanged with default parameters; each new tool answers on the shared handler fixture and refuses a path outside the repository; a recorded tool-turn conversation shows the reasoning field replayed.
|
||||
- _Requirements: 6.4, 6.5_
|
||||
|
||||
- [x] 2. Core: classifier, scorer, detectors, verifiers, journal
|
||||
- [x] 2.1 (P) Classifier tiers
|
||||
- Tier one decides from rule id, sink family, obligation kind, mechanism id or labeled CWE without a model; tier two asks the chat endpoint in JSON-object mode with thinking disabled and averages log-probabilities, falling to unknown under a threshold; tier three is unknown; every answer records its tier; results are multi-label; loaded catalog family labels are validated against the taxonomy here and a fabricated family is rejected by name.
|
||||
- Observable: on the shared handler fixture tier one classifies the leaky handler as access control without any client; with a scripted client an off-vocabulary answer becomes unknown; a catalog with a fabricated family is rejected by name.
|
||||
- _Requirements: 2.1, 2.2_
|
||||
- _Boundary: Classifier_
|
||||
|
||||
- [x] 2.2 Classifier measurement and review queue
|
||||
- Report: agreement with maintainer labels on reviewed pairs with parent–child partial credit and no credit for lateral or fabricated ids, confusion matrix per family, accuracy against a random router and an oracle router, counts per tier and unknown; disagreements append to a review queue and never change a label; the whole corpus, including free-text and `other` rows, is classified with per-family counts.
|
||||
- Observable: the report on the vendored corpus lists the vfc `other` rows by resulting family and the queue holds every disagreement once with both answers and the tier.
|
||||
- Observable: the report also states whether hierarchical partial credit could apply.
|
||||
- _Requirements: 2.3, 2.4, 2.5, 2.6_
|
||||
|
||||
- [x] 2.3 (P) Per-pair family scoring
|
||||
- Detection only for a finding whose family tag relates same or parent–child to the labeled family and whose line lies in the labeled function, with the maximum penalty for a fabricated family; silence only when no such finding lies in the labeled function on the fixed side; other findings counted separately, never as leaks; text is never inspected; outcomes pair-correct, both-flagged, both-silent, reversed and unscorable; the unscorable reason also covers unsupported language, unresolved label and missing context.
|
||||
- Observable: a synthetic run set where every fixed side carries an other-family finding scores full silence; a finding with a fabricated family never counts; a pair in an unsupported language scores unscorable with that reason.
|
||||
- _Requirements: 3.1, 3.2, 3.6, 4.1_
|
||||
- _Boundary: Scorer_
|
||||
|
||||
- [x] 2.4 Aggregation, noise handling and denominators
|
||||
- Per family and slice: outcome counts, recall, silence, Youden, directional-bias index, a fixed-false-positive-rate view; K runs per pair with the majority outcome, negative flips per family against a baseline set, and a paired reliable-change test; unscorable rows listed by reason and excluded from every denominator; every metrics block carries the taxonomy version and the scorable and unscorable counts.
|
||||
- Observable: a single-run difference below the reliable-change threshold reports no change while a consistent flip across runs does; the unscorable count and taxonomy version appear in every metrics block.
|
||||
- Observable: every metrics block states whether hierarchical partial credit could apply.
|
||||
- _Requirements: 3.3, 3.4, 3.5, 3.7, 4.5_
|
||||
|
||||
- [x] 2.5 (P) Per-family detector configurations and the family detector runner
|
||||
- A configuration directory per family: prompt under a stated length cap, checklist, tool list from the curated set only, step and cost budget, maximum characters, hard negatives and counterexamples, version; the loader rejects an over-cap prompt, an unknown tool or a missing family; this task ships the loader and a test fixture configuration, the shipped per-family directories are written by round zero.
|
||||
- The runner takes the families admitted by the classifier as input, builds the detector's user message from the whole file plus definitions its tools resolve, runs the hunter loop with the family's prompt, tools and budgets, and tags each finding with exactly one family tag and one detector-version tag; the generalist is the unknown family's configuration.
|
||||
- Observable: a family run on the shared fixture with a scripted client yields findings tagged with that family and version and uses only the family's tool list; a configuration whose prompt exceeds the cap fails to load by name.
|
||||
- _Depends: 2.1_
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
- _Boundary: Detectors_
|
||||
|
||||
- [x] 2.6 (P) Verifier registry and the oracle callback
|
||||
- The regress verdict accepts an optional oracle callback and keeps exit-code behavior otherwise; a registry keyed by the taxonomy's verifier kind returns a canary, static or no-op verifier; the static verifier raises access-control claims to static corroboration when the obligations checker reports the same function; the no-op verifier leaves suspicion.
|
||||
- Observable: with the fake sandbox runner a result whose output carries the token yields triggerable while a nonzero exit without the token does not; a family with no verifier keeps suspicion on the shared fixture; the access-control claim on the leaky handler reaches static corroboration.
|
||||
- _Requirements: 7.1, 7.3, 7.4_
|
||||
- _Boundary: Verifiers, Regress_
|
||||
|
||||
- [x] 2.7 Canary templates and the second judge
|
||||
- Family snippet templates for injection, file path and deserialization plant a canary (row in an in-memory database, file under the scratch mount, marked object) and print the token only when the vulnerability fires; the second judge asks the judge model with the snippet, oracle output and claim and answers confirm or not in JSON-object mode; a claim is published as proven only when it confirms; templates, oracle code and gold labels live outside every proposer write root.
|
||||
- Observable: every canary template passes the existing snippet safety check; a scripted judge that declines keeps the claim below proven; templates and oracle code live under the verifier directory and gold labels under the corpus directory.
|
||||
- _Requirements: 7.2, 7.5_
|
||||
|
||||
- [x] 2.8 (P) Round journal, archive and rejected buffer
|
||||
- Append-only round records with hypothesis, levers, predicted affected and at-risk families, outcome, reason, target deltas, per-family sweep flips, attribution (flipped predicted, flipped unpredicted, precision), cost, taxonomy and configuration versions; a per-family archive of which configuration version wins which pair; a rejected-hypothesis buffer per family; every persisted text passes redaction.
|
||||
- Observable: a record round-trips through the file; the buffer returns only the target family's rejected hypotheses; the archive names one winner per pair per family; a secret literal written into a record is stored redacted.
|
||||
- _Requirements: 9.3, 9.6, 9.7, 11.3_
|
||||
- _Boundary: Journal_
|
||||
|
||||
- [x] 3. Integration: split, scoring paths, rounds, proposer, publish, CLI
|
||||
- [x] 3.1 One teacher rule across every learning path
|
||||
- Teachers are vendored, gating-tier, train-split, scorable pairs; a refusal names any candidate taught by or touching a holdout pair; the exporter, leave-one-out, per-profile evaluation, mechanism-profile evaluation, mechanism admission and pair signals all obtain teachers through the rule and record refusals as degradations; the mechanism lever's holdout number is re-measured under the rule and its raw artifact committed for the publish task.
|
||||
- Observable: leave-one-out on the toy slice seeds from train pairs only; a candidate shape taught by a holdout pair is refused by name; the re-measured artifact is in the measurements directory.
|
||||
- _Depends: 1.3_
|
||||
- _Requirements: 5.1, 5.2, 5.3_
|
||||
|
||||
- [x] 3.2 Family scoring on the pairs hunter path
|
||||
- The pair scorer's hunter path takes an injected family matcher and leak rule from the learning package, classifies each pair once, runs the admitted family detectors plus the generalist K times per side, and emits family outcomes and metrics; the overlay and inventory paths stay untouched.
|
||||
- Observable: the pairs command with a scripted client on the shared fixture reports a family outcome per pair; no text-token matching remains on the hunter path; the pair gate output is byte-identical to before.
|
||||
- _Depends: 2.1, 2.4, 2.5, 2.6_
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.5, 11.5_
|
||||
|
||||
- [x] 3.3 Round zero and the noise floor
|
||||
- Clone the current hunter prompt into every family configuration; run each family K = 5 times over the scorable vendored pairs of the selected slices at deterministic settings; record the per-family negative-flip rate between runs as that family's regression budget; persist noise floors and round-zero artifacts keyed by detector model so another model can be baselined and compared without touching configurations; the web families over vibe-py and agent-vfc are the default selection and the memory-safety family over vfc is selectable but never gates.
|
||||
- Observable: with a scripted client round zero writes a noise-floor file with one budget per family and a round-zero report per slice; a second baseline under another model name writes a separate artifact and the comparison table lists both.
|
||||
- _Depends: 3.2_
|
||||
- _Requirements: 8.1, 8.2, 8.3, 8.4_
|
||||
|
||||
- [x] 3.4 Family detectors, verifiers and tags in scans
|
||||
- In a standard scan each classified region runs its admitted family detectors plus the generalist from the directories round zero wrote (a missing family directory is a recorded degradation and that family's detector is skipped), claims pass through the family's verifier, findings carry family, detector and verifier tags in reports and SARIF, and the manifest gains a learning block with taxonomy version, per-family counts, the classifier report and the current round; the detection and map gates stay byte-identical.
|
||||
- Observable: a standard scan of the shared fixture with a scripted client writes findings tagged with a family, a detector version and a verifier rung, and the manifest block; the detection and map gate outputs diff empty.
|
||||
- _Depends: 3.3, 2.6_
|
||||
- _Requirements: 6.3, 7.1, 11.5_
|
||||
|
||||
- [x] 3.5 Proposer protocol, scripted proposer and the HarnessX proposer
|
||||
- Failure facts for one family: misses and leaks with pair, function, sink or check, line, verifier outcome and run outcomes, plus the rejected buffer and the current configuration; a proposal is one hypothesis, one lever (tool, checklist, counterexample, memory, prompt), one file-level change inside the family directory, and predicted affected and at-risk families; the proposer never receives holdout inputs, outputs or scores.
|
||||
- A scripted proposer for tests and offline rounds; a HarnessX proposer composing the meta-agent with write roots limited to the family directory, degrading to a recorded reason without the extra.
|
||||
- Observable: the scripted proposer returns a proposal from a facts fixture; the HarnessX proposer is skipped with the reason recorded when the extra is absent and, when present, writes only inside the family directory; the write-root list excludes the verifier and corpus directories.
|
||||
- _Depends: 2.5, 2.8_
|
||||
- _Requirements: 9.1, 9.2, 5.4, 7.6_
|
||||
|
||||
- [x] 3.6 Acceptance rule and directory revert
|
||||
- Acceptance requires train and holdout non-negative with one strictly positive and every other family within its budget; ties reject; a family directory snapshot restores byte for byte on rejection or overrun; refusals for holdout contact, length cap and lever set are outcomes with reasons.
|
||||
- Observable: a helpful change is accepted, a tie is rejected, a change that breaches another family's budget is rejected with the family named, and a revert restores the directory byte for byte.
|
||||
- Observable: every family other than the target has a byte-identical directory before and after the round, whatever the outcome.
|
||||
- _Depends: 3.3_
|
||||
- _Requirements: 6.2, 9.5_
|
||||
|
||||
- [x] 3.7 Evolve round orchestration, cost and journal
|
||||
- One round: facts, proposal, refusals, train minibatch, target-family holdout, sweep over every other family's holdout, K-run scoring at every stage as the scorer requires, accept (freeze, bump version, archive) or revert; cost metered per stage with revert and a recorded reason on overrun; a journal record and the rejected buffer written for every outcome; a round directory holds the snapshot, proposal, scores and trajectories and a repeated round number is refused.
|
||||
- Observable: with the scripted proposer each of accepted, rejected, budget-breach and cost-overrun leaves one journal record with attribution and a round directory; a repeated round number is refused.
|
||||
- The round directory holds the pair identities behind the attribution counts and every detector trajectory, redacted, so a round can be replayed and its attribution audited; every stage scores at least three runs per pair.
|
||||
- _Depends: 3.5, 3.6_
|
||||
- _Requirements: 3.5, 9.4, 9.6, 9.7, 9.8, 9.9, 11.3_
|
||||
|
||||
- [x] 3.8 Publish from artifacts
|
||||
- One entry point regenerates every family and slice number from committed artifacts, writes a markdown table with scorable and unscorable counts, taxonomy version, noise floor and cost per point beside every number, replaces the roadmap's measured section between markers, and writes the raw artifacts under the measurements directory.
|
||||
- Observable: running the publish entry point twice without new artifacts produces byte-identical output; the corrected mechanism-lever number from the re-measurement artifact replaces the earlier roadmap value.
|
||||
- _Depends: 3.1, 3.3_
|
||||
- _Requirements: 11.1, 11.2, 4.5, 5.3_
|
||||
|
||||
- [x] 3.9 Learning subcommands
|
||||
- Subcommands for classify, score, baseline, round and publish with the flags the design names; the pairs hunter path gains the K-runs flag; every step runs under the harness runtime with degradations for missing extras, endpoints or sandbox.
|
||||
- Observable: each subcommand exits zero on the shared fixture with a scripted client and writes its artifact; the roadmap section written by the publish subcommand carries the command line that produced it; a missing endpoint records the reason and the remaining steps complete.
|
||||
- _Depends: 3.7, 3.8_
|
||||
- _Requirements: 11.1, 11.4_
|
||||
|
||||
- [x] 4. Corpus repairs
|
||||
- [x] 4.1 Re-harvest absence rows with registration context
|
||||
- Absence rows in vibe-py and agent-vfc gain their family and the handler-context mode in the recipes; the maintainer re-harvests them over the network; every re-harvested excerpt keeps its license line, passes redaction and keeps the decorators and registration statements; failed fetches keep the old excerpt with a recorded reason; catalogs regenerate idempotently.
|
||||
- Observable: the vampi and threatbyte excerpts contain their registration lines, the entry-point mapper names their handlers, and the catalogs load with unchanged vendored counts.
|
||||
- _Depends: 1.3_
|
||||
- _Requirements: 10.2_
|
||||
|
||||
- [x] 4.2 Identical twins and the fixture-difficulty check
|
||||
- The five identical vibe-py twins carry the identical-twin known-limit reason in the catalog; a test derives a difficulty vector for a unit fixture (binding hops to the identity source, registration present, cross-file registration) and for the corpus rows its docstring names, and fails when the fixture is strictly easier.
|
||||
- Observable: the twins are excluded from every denominator with their reason listed; the shared obligations fixture is compared against the re-harvested vampi rows and the check's verdict is recorded.
|
||||
- _Depends: 4.1_
|
||||
- _Requirements: 10.3, 10.4_
|
||||
|
||||
- [x] 4.3 Fix dates and the post-cutoff slice
|
||||
- Catalog rows gain a fix date populated by the catalog generator from recipe metadata where available; rows without a date are counted separately; rows dated after the configured detector cutoff form a separately reported slice in every scoreboard.
|
||||
- Observable: the scoreboard shows the post-cutoff slice with its own counts, the cutoff date it used and the undated count.
|
||||
- _Depends: 1.3, 2.4_
|
||||
- _Requirements: 10.5_
|
||||
|
||||
- [ ] 5. Validation
|
||||
- [x] 5.1 Gates, matrices and degradation paths
|
||||
- Detection, map and local pair gate outputs byte-identical to the pre-feature commit; the extra-free suite green with semantic and HarnessX tests skipping; each optional dependency's absence exercised once with its recorded reason.
|
||||
- Observable: the three gate outputs diff empty against the recorded baseline and both test matrices pass.
|
||||
- _Requirements: 11.4, 11.5_
|
||||
|
||||
- [ ] 5.2 Live round zero
|
||||
- Round zero through the DeepSeek endpoint on the vibe-py and agent-vfc web families with K = 5, plus the memory-safety family over the vfc slice as a measured, non-gating run; artifacts, noise floors and cost per pair committed; the classifier report and review queue produced on the same run; a second baseline with the judge model on one slice for the model comparison table.
|
||||
- Observable: the published table shows per-family outcomes, noise floors and cost for both models and for the vfc memory-safety family, and the roadmap section is regenerated from it.
|
||||
- **Blocked 2026-09-06: no `DEEPSEEK_API_KEY` in `.env` or the environment.** Everything else is ready; the run is one command per baseline. The maintainer chose to add the key and have the full run go ahead (web families with `deepseek-v4-flash`, the vfc memory slice as a measured non-gating run, and a second baseline with `deepseek-v4-pro` on one slice). Scale, measured on the corpus: 58 scorable web pairs plus 176 vfc pairs, both sides, K=5 = 2,340 detector runs at up to four model calls each; the design estimated under $15 at peak flash pricing.
|
||||
- Dress-rehearsed offline 2026-09-06 with the scripted client over the whole vibe-py slice at K=3: round zero produced six families with per-slice blocks, correct denominators (the twins out by reason), and a floor each; a round then read the recorded proposals, applied one, swept the other five families for negative flips (0 each), rejected on `tie`, reverted the family directory byte for byte and archived the rejected version's per-pair results. The only thing the live run adds is a detector that can actually see.
|
||||
- _Depends: 3.9, 4.1_
|
||||
- _Requirements: 8.3, 8.4, 11.1, 11.2_
|
||||
|
||||
- [ ] 5.3 First evolve rounds
|
||||
- **Amended 2026-09-06 (maintainer, in session): the target family is `injection`, not `access_control`.** Measured on the corpus under the split and review-tier rules: access_control has **1 train teacher and 4 holdout pairs**, six of its rows unusable because every agent-vfc row sits at tier `title`, and the vibe-py absence rows that could teach are Real-Vuln-Benchmark trap pairs whose two sides are different handlers — so the train split teaches **zero** obligation shapes and any accepted change would rest on one pair. `injection` has 12 train teachers and 10 holdout pairs, the only family in the corpus with enough train evidence to tell a real change from noise. The first memory entries are therefore the mechanism shapes the train split really teaches rather than obligation shapes, which do not exist yet. access_control follows when its corpus can teach: either a maintainer reviews agent-vfc rows up to `reviewed`, or the trap rows gain non-trap twins.
|
||||
- Three rounds with the scripted proposer on the injection family using the shapes the train split teaches as the first memory entries, then one round with the HarnessX proposer; every round journaled with attribution; the roadmap regenerated.
|
||||
- Observable: the journal holds four records with outcomes and attribution precision, at least one accepted change is versioned in its family directory, and no other family exceeded its budget in any accepted round.
|
||||
- _Depends: 5.2_
|
||||
- _Requirements: 9.9_
|
||||
|
||||
## Task-plan review notes
|
||||
|
||||
- Sanity review round 1 (2026-09-06): NEEDS_FIXES, eleven findings, all repaired (K-run policy honors Req 3.5; per-model baselines added to the design; family validation moved out of the corpus loader; reasoning replay owned by the hunter loop; Req 5.3 split into re-measure and publish; scoring path split into pairs and scan tasks; oversized tasks split; fix-date prerequisite added; fixture-difficulty check depends on the re-harvest; memory-safety family measured on vfc; detector task depends on the classifier and round zero owns the shipped directories; observables re-anchored).
|
||||
- Sanity review round 2 (2026-09-06): NEEDS_FIXES with three local items, repaired without a third review under the bounded loop: task 1.5 delivers the four curated tools and the hunter-loop parameters that 2.5 relied on; the scan-path task now follows round zero and degrades when a family directory is missing; the write-root observable moved to the HarnessX proposer task with 7.6. Non-blocking notes applied: the split helper assigns as well as reports (4.4), 3.2 depends on 2.1 rather than 2.2.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Requirements approved 2026-09-06, design approved 2026-09-06, tasks approved 2026-09-06 (maintainer, in session).
|
||||
- Task 1.1 (2026-09-06, RED first: 9 failing tests in `tests/test_learning_families.py`, `ModuleNotFoundError: openultrasast.learning`; then green): `learning/families.py` and `ruleset/families.toml` with the ten decided families, each carrying its CWE set, mechanism set, verifier kind and description. Decisions taken while writing the data: (a) `unknown` carries the six mechanisms that span families (`source_reaches_sink`, `container_taint`, `validation_strength`, `redos`, `cross_artifact`, `other`) and the two weaknesses we deliberately do not target (CWE-20 generic validation, CWE-835 infinite loop), so a row we considered and declined is visible instead of unresolved; an earlier draft said `unknown` had none. (b) The maintainer's "server-side request forgery" family is named `untrusted_destination` and also covers open redirect (CWE-601), because both are untrusted input choosing a request destination and the corpus carries 34 open-redirect rows that would otherwise be unclassifiable. (c) The taxonomy is flat: `parent` is an optional field no shipped family uses, exercised by a fixture, because Req 2.3 and 3.1 need a parent-child arm and inventing a hierarchy would be worse than leaving it empty. (d) Abstention earns no partial credit: `related("unknown", X)` is `lateral`. All 33 labeled corpus weaknesses resolve, none is claimed twice, and every mechanism id belongs to exactly one family.
|
||||
- Task 1.2 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_endpoint.py`; then green): `learning/endpoint.py` resolves override, then the scripted flag, then a configured endpoint, then DeepSeek, then OpenRouter; the embedding client is untouched. `provider/openrouter.py` gains `complete_chat_raw` (whole payload, for `usage`) and an `extra_body` passthrough; `ChatResponse` gains `reasoning` and `mean_logprob`. Design deviation: `ChatEndpoint` deliberately holds no `api_key` (the design listed one) so the record is safe to journal; the key stays on the client. `[models]` gains `judge`, `chat_base_url`, `chat_api_key_env`; `[learning]` is new. `ModelConfig.hunter` keeps its `None` default on purpose: defaulting it would silently enable LLM stages in existing scans, so the DeepSeek model defaults live in `resolve_models`.
|
||||
- Task 1.3 (2026-09-06, RED first: 7 failing tests in `tests/test_pair_hygiene.py`; then green): `ExpectedFinding.family` (opaque; the classifier validates it), `PairCase.unscorable`, `PairCase.split_declared`, `PairCase.fix_date` (the generator fills it in 4.3), `duplicate_groups`, `split_by_repository` with `SplitReport`, and a split filter on `build_pair_signals`. Measured and committed as `benchmarks/measurements/2026-09-06-corpus-hygiene.json`: **19 repositories straddle the train/holdout split** (a learning path can be taught by one file of a repository and evaluated on another) and **10 groups of pairs share a vulnerable excerpt byte for byte**, mostly the same fix harvested twice at different granularity. Only three vendored rows (the `local` slice) declare no split.
|
||||
- Task 1.4 (2026-09-06, RED first: 2 failing tests in `tests/test_typescript_grammar.py`; then green): the semantic extra gains `tree-sitter-typescript`; `grammar_for` maps a language to (module, attribute) because the wheel ships two grammars, and `parse_with_cst` picks the JSX-aware one for a `.tsx` path while the parsed file keeps the language it was given. Eighteen agent-vfc rows plus the TypeScript absence rows now parse on both sides.
|
||||
- Task 1.5 (2026-09-06, RED first: 5 of 6 tests in `tests/test_hunter_curated_tools.py` failing; then green): `hunter_tools` gains `read_definition`, `entry_points`, `flows`, `obligations`; `run_tool_hunter` gains `system_prompt`, `user_prompt`, `context_files`, `tools`, `max_chars` and `tags`, all defaulting to today's behaviour, and replays the provider's reasoning field on the next turn. Deviations: `read_definition` returns the definition text as well as its span, and includes preceding decorator lines, because a resolver that returns only line numbers forces another `read_file` and a handler's decorators are exactly what the obligations work showed to matter; the three list tools return identifiers and spans only. No caching: a stale view of a tree a round may have rewritten is worse than a reparse.
|
||||
- Review round 1 (2026-09-06) APPROVED 1.1, 1.2, 1.4, 1.5; REJECTED 1.3. The reviewer ran every check in a detached worktree at the group-1 tip and confirmed RED-first by running each new test file against the pre-feature commit (37 failures there, all passing after), the three gate outputs byte-identical, and `pairs`/`benchmark` free of any learning import.
|
||||
- **1.3 rejection, remediated RED-first**: `_body_digest` stripped every leading comment line, which in C and C++ swallows `#include` and `#define`, and in any language swallows an ordinary leading comment. Two files differing only in a buffer size on a `#define` hashed equal and were marked `identical_twin`, removing a real pair from every denominator. Measured before the fix: twelve file sides across six vendored pairs had directives inside the stripped prefix, including the Juliet stack-overflow pair whose whole point is that constant. The strip is now anchored to the grammar `harvest.provenance_header` writes: a leading comment counts as header only when it carries one of the eleven provenance keys followed by a colon, and the scan stops at the first line of code. Four regression tests pin both directions plus a corpus-wide assertion that no vendored excerpt loses a directive. The five twins, ten duplicate groups and nineteen straddling repositories are unchanged, so the committed hygiene artifact still holds.
|
||||
- Suggestions applied: a mechanism claimed by two families now fails loud by name; the obligations tool's path-escape refusal and the endpoint's degrade-to-None on a provider error are now asserted; the TypeScript parse assertion is exact instead of a 25% allowance, which surfaced one real corpus defect (`lensops-kubernetesmanagement-fe4910` was harvested as a truncated destructuring parameter list and is not valid TypeScript on its own; it belongs to the 4.1 re-harvest).
|
||||
- **Two findings escalated to the maintainer, both blocking task 3.8 rather than group 1.** (a) `untrusted_destination` contradicts the approved requirement sentence, which says "server-side request forgery": the family id is the key every published number carries, so `requirements.md` Req 1.1 and the brief's Decision 1 must be amended to the widened name, or the family renamed back and CWE-601's 34 rows re-homed. (b) The shipped taxonomy is flat, so `parent_child` is unreachable in production and the "partial credit for a parent-child relation" in Req 2.3 and 3.1 is inert at version 1; either declare real parents or record the inertness beside every published agreement number.
|
||||
- Carry-forward: the nineteen straddling repositories cross the split by declaration, so `split_by_repository` cannot fix them and task 3.1's teacher rule inherits the leak until a maintainer re-declares those splits. The `preprocess_repository` cost of the three list tools should be measured in 2.5 before round zero.
|
||||
- Task 2.1 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_classify.py`; then green): `learning/classify.py` with three tiers, multi-label answers in taxonomy order, and `validate_family_labels` owning the taxonomy check the corpus loader deliberately does not do.
|
||||
- Task 2.2 (2026-09-06, RED first: 5 failing tests in `tests/test_learning_classifier_report.py`; then green): `measure_classifier` and `write_review_queue`. Bug caught by the RED test: the classifier read the declared label it was being scored against, making agreement trivially 1.0; `classify_pair(..., use_declared=False)` is what the measurement uses. Measured on the 277 vendored pairs: tier one places 275 without a model call, 2 land in `unknown`, and the 176 memory-safety rows resolve to one family. `labeled` is 0 because no row carries a maintainer family label yet, and the report says so instead of inventing a score.
|
||||
- Task 2.3 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_scoring.py`; then green): `learning/scoring.py` `score_pair_family`. Detection is a family-tagged finding inside the labeled function; text is never read. Findings of other families or outside the function are counted separately and never called leaks; a fabricated family id is counted as noise. K runs, majority outcome, disagreeing runs recorded as flips.
|
||||
- Task 2.4 (2026-09-06, RED first: 7 failing tests in `tests/test_learning_aggregation.py`; then green): `aggregate` and `sign_test`. Recall at a fixed false-positive ceiling is forfeited entirely when the leak rate exceeds it; the directional-bias index separates "flags everything" from "silent on everything", which Youden cannot; change against a baseline is a two-sided exact sign test over per-pair flips, not a difference of means. `PairFamilyScore` gained a `slice` field (additive, filled from the case) so metrics can be reported per slice.
|
||||
- Task 2.5 (2026-09-06, RED first: 9 failing tests in `tests/test_learning_detectors.py`; then green): `learning/detectors.py` with `FamilyConfig`, `Region`, `load_family_configs`, `run_family_detector`, `run_region_detectors`, `write_default_configs`. Deviations: the parts live in one `family.toml` plus the markdown and JSON-lines files, because two TOML files with three keys between them was more surface than value and `version` had no home in the design's list; configurations live under the project's learning directory, not inside the package, because a round writes them. `CURATED_TOOLS` is derived from `HUNTER_TOOLS` so the closed set cannot drift.
|
||||
- Task 2.6 (2026-09-06, RED first: 6 failing tests in `tests/test_learning_verifiers.py`; then green): the optional oracle callback on `verdict_from_result`, inserted after the inconclusive guards and before the exit-code branch so a call without an oracle is the old decision tree, plus `verifier_for`, `StaticVerifier`, `NoVerifier` and `apply_verification`.
|
||||
- Task 2.7 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_canaries.py`; then green): `learning/canaries.py` with per-family templates, `CanaryVerifier` and `confirm_proven`. Deviation: templates live in `learning/canaries.py` rather than the design's `learning/verifiers/` directory; the property that matters, being outside every proposer write root, holds and is tested.
|
||||
- Task 2.8 (2026-09-06, RED first: 6 failing tests in `tests/test_learning_journal.py`; then green): `learning/journal.py` with `RoundRecord`, `Attribution`, `LearningJournal`, `Archive` and `summarize`.
|
||||
- Review round 2 (2026-09-06) APPROVED 2.1, 2.2, 2.4, 2.5, 2.8; REJECTED 2.3, 2.6, 2.7. The reviewer ran every check in a detached worktree at the group tip, re-derived the RED phase per task against each task's parent commit (8/5/8/7/9/6/8/6 failures, the four recorded claims exact), killed 15 mutants against the new tests, and hand-verified `sign_test` against the closed form for every (better, worse) with n <= 20.
|
||||
- **2.3 rejection, remediated RED-first**: two of Req 4.1's five unscorable reasons had no code path, so a row whose language could not be parsed was reported as `unresolved_label`, a corpus labelling defect, when the cause was a tooling gap. The design's `unscorable_reason(case, *, parse_ok, ranges)` is reinstated as a public function, `score_pair_family` takes `parse_ok` and `entry_points`, and `missing_context` names a handler that arrived without the registration that makes it reachable, for the families whose bug shape is a missing check.
|
||||
- **2.6 rejection, remediated RED-first**: `apply_verification` wrote `evidence_level="proven"`, which is not a member of the project's `EvidenceLevel`, so `verify_finding`, `is_report_verified` and `verify_judge` all raise `ValueError` on it. Requirements put the evidence ladder out of scope and the design's data contract specifies only the `verifier:<rung>` tag. Nothing in production called it yet, so the suite stayed green while task 3.4's wiring would have broken a scan. `apply_verification` now writes only the tag; `evidence_level_for` and `raise_to` are the single explicit bridge onto the ladder, and a test round-trips a proven claim through the project's own helpers.
|
||||
- **2.7 rejection, remediated RED-first**: the injection canary opened its own in-memory database, inserted the token there and called the handler without ever giving it that connection, so the oracle could never fire; the fake sandbox runner returned the token regardless of the snippet, which is why no test noticed. It is now an execution canary whose marker only an interpreter could write, and each template is exercised end to end by running the rendered snippet against a vulnerable fixture and its fixed twin. That test immediately caught a second flaw: a fixed twin that echoes its input printed the oracle token and satisfied the oracle with no vulnerability at all, so an injected payload now carries a value unrelated to the token the oracle looks for. Recorded scope: the injection canary confirms command, code and template execution; a query-language injection whose payload never reaches an interpreter stays a suspicion.
|
||||
- Suggestions applied: a configuration with an empty tool list is refused by name instead of producing a detector that silently reports nothing; the classifier abstains when its no-adapter fallback call also fails; the archive docstring states that the earliest winning version keeps its claim to a pair.
|
||||
- Task-plan updates from the review: 2.2 and 2.4 claim the two criteria the maintainer added (2.6 and 3.7); 3.6 claims Req 6.2 with an observable that every non-target family directory is byte-identical across a round; 3.7 claims Req 3.5's K floor, 9.6's pair identities and 11.3's trajectories.
|
||||
- **Escalated to the maintainer**: Req 6.1 lists "a skills set" among a detector configuration's parts, and neither the approved design nor the code has one, although the repository ships a skill router. Either Req 6.1 drops the skills set or a task wires it; the implementation followed the approved design, so this is a spec-internal conflict rather than a deviation.
|
||||
- Carry-forward: the nineteen straddling repositories still cross the split by declaration and cap every holdout number these tasks produce; `unknown` serves as both the classifier's abstention and the generalist detector, which deserves a sentence in the design before a round ever targets it.
|
||||
- Task 4.1 (2026-09-06, RED first: 21 of 23 failing tests in `tests/test_pair_registration_context.py`; then green, 26 tests). Measured before the change: **8 of the 10 vendored access-control rows had no named handler at all**, so every one of them would have scored `missing_context` forever rather than as a detector miss. After the re-harvest 9 of 10 are named and the tenth states its reason. What the repair needed, in order of how much it cost:
|
||||
- **Class registration.** A flask-restx handler is a method; `@ns.route('/profile')` sits on the class. `extract_handler_context` now carries the enclosing class declaration and its decorators, so the excerpt is both reachable and parsable — a bare indented `def get` is neither.
|
||||
- **Multi-line decorators and commented-out guards.** `@ns.doc(...)` wraps over four lines, and stopping at a continuation dropped the `@ns.route(...)` above it — the registration itself. `#@token_required` above a handler is not noise; in the two ThreatByte rows it is the entire bug, and the evidence field says so. `_decorator_start` now walks both.
|
||||
- **Cross-file registration (design amendment, recorded in `design.md`).** connexion registers VAmPI's handlers by `operationId` in `openapi_specs/openapi3.yml`; Overleaf registers `exportProject` in `router.mjs`. No single-file excerpt can contain that, so a recipe may name `context = [...]`, the harvester writes the stanza per side under `<name>/context/<side>/<relpath>`, the catalog carries `[[pair.context]]`, and `_materialize_side` lays the documents out beside the excerpt at their repository-relative paths. `mapping._route_manifest_entry_points` reads `operationId` plus the operation's `security` block; `_js_statements` joins a registration that spans lines, because reading `webRouter.post(` alone finds neither the handler nor the middleware that guards it.
|
||||
- **Anchors.** Real-Vuln-Benchmark trap rows take their fixed side from a different, correctly guarded handler of the same snapshot, so `handler_context` honours `line`/`fix_line` and a new `fix_function` gives the fixed side *its* registration instead of the vulnerable side's.
|
||||
- **`innermost_function_bounds`** for brace languages, used only by this mode: `function_bounds_at` returns the outermost top-level group, which for a handler declared inside a React component is the whole 6000-line component. `hunk` and `enclosing` rows keep the bounds they were harvested with.
|
||||
- **Fetch-then-write.** `harvest_recipe` fetches every blob before the first write, and the driver takes `--report` so a failed fetch leaves the reviewed excerpt exactly as it was and says why. A half-written re-harvest is indistinguishable from a real upstream change.
|
||||
- Bugs the re-harvest exposed and fixed on the way: `.mjs` and `.cjs` were not in `LANGUAGE_BY_EXTENSION`, so the one pair vendored as an ES module had **no file target at all** — no entry point, no finding, a permanent miss; and `_python_route_access` matched auth words anywhere in a decorator, so `@ns.doc(description='... without proper authorization checks')` read the unguarded ThreatByte endpoint as `authenticated`, silencing every obligation on it. The guard is now the decorator's name, never its prose, and `@token_required` — the commonest Flask guard — is recognised at all.
|
||||
- Corpus corrections: `lensops-kubernetesmanagement-fe4910` was labeled `setErrorMessage`, a React state setter, because the hunk landed in the wrong bounds; it is now `loadNamespaces` and the pair diff is exactly the fix. Its bug is a browser-side call omitting the header its siblings carry, not a missing guard on an inbound handler, so it carries `known_limit = "client_side_caller"` and leaves the denominators with a stated reason.
|
||||
- Three existing tests encoded the old corpus and now encode the new one: the TypeScript parse pin (the lensops excerpt parses now), the classifier report (`labeled` 0 -> 4, the four seeded rows; the six `title`-tier agent rows carry a family but are not labels a maintainer stands behind), and the hygiene report ordering.
|
||||
- **New honesty finding, recorded in the hygiene artifact as `cross_split_duplicates`: nine of the ten duplicate groups sit on both sides of the split.** Two forks of one project (`openniw-jobs-0fc947` train, `openniw-jobs-b74de3` holdout) and eight CVEs harvested twice under different names are byte-identical, so the train row teaches the holdout row exactly. `split_by_repository` cannot see it — the duplicates are different repositories. `duplicate_groups` is now ordered by name so the committed report does not reshuffle when one excerpt is re-harvested.
|
||||
- Pre-existing drift the regeneration corrected, unrelated to this task: `catalog.toml` in git disagreed with a fresh `catalog_gen` run at the group-3 tip on two derived labels (`origin` -> `createApplication`, `normalizeDomain` -> `getGoogleProvider`). Verified in a detached worktree at `7a33756` before touching anything.
|
||||
- Group 3 (2026-09-06). Tasks 3.1-3.9 implemented in `86ed54c`, `fcda1ee`, `e8e6baf`, `127dbef`, `ad7c98e`, `346cc70`, `16d9bc2`, `b955251`, `b8ad42c`, `7a33756`. RED counts, derived by the reviewer against each task's parent commit: 3.1 4/6, 3.2 6/6, 3.3 6/6, 3.4 3/5, 3.5 10/10, 3.6 12/12, 3.7 9/9, 3.8 7/7, 3.9 7/7. Deviations from the design, recorded here because they were not written down at the time: `learning.split` duck-types `PairCase` through `getattr` rather than importing it; `pairs.py` reaches `learning` through function-local imports rather than the design's injected callable; `run_baseline` takes a scan factory where the design named a client. One flaky test found and fixed in `346cc70` (`_is_vulnerable_side` keyed on `"-v" in str(root)`, which random temporary directory names matched about once in eight; harness noise recorded as detector noise would inflate every family's regression budget).
|
||||
- Review round 3 (2026-09-06) APPROVED 3.4; REJECTED 3.1, 3.2, 3.3, 3.5, 3.6, 3.7, 3.8, 3.9. The reviewer ran every check in detached worktrees at the group tip against `71efc9c`, derived the RED phase per task, mutation-tested `decide`, the teacher rule, the snapshot restore and the round's revert paths (7 mutants on the acceptance rule, 6 killed and 1 equivalent), ran the three flakiness-prone files 61 times with varied `PYTHONHASHSEED` without a failure, and confirmed the three gate outputs byte-identical and the extra-free matrix unchanged. Every rejection was remediated RED-first; the numbers each defect was hiding are recorded below because they are the point of the spec.
|
||||
- **3.2, the fixed side was judged against the vulnerable side's line ranges.** A fix that adds a guard above the handler moves its first line, and the reviewer measured that **191 of 227 pairs with a resolvable labeled span (84%) have different spans on the two sides**. A finding inside the fixed handler was therefore read as outside it: a leak recorded as silence, always in the direction that flatters Youden. `score_pair_family` now takes `fixed_ranges`; the fixed side's parse was already being computed and thrown away.
|
||||
- **3.2 and 3.9, K runs was declared and never threaded.** `_evaluate_hunter_pair(..., runs=1)` had one call site that passed nothing, `evaluate_catalog` had no parameter at all, and `pairs --k-runs` was parsed and discarded; the only test grepped `--help`. K now flows `pairs --k-runs` -> `evaluate_catalog` -> `evaluate_pair` -> `_evaluate_hunter_pair`, defaults to three and refuses less, and the test counts scans per side.
|
||||
- **3.2, the generalist could never be scored.** `pairs --hunter` fed the class-aware scorer findings with no `family:` tag, so it reported recall 0 by construction. `run_tool_hunter` now reads an optional `family` field into the tag (the design said it would) and `make_hunter_scan(taxonomy=...)` asks for one from the closed list. A generalist that names no family is unanswerable; one that names a wrong family is counted as noise, which is the honest treatment.
|
||||
- **3.3 and 3.9, round zero measured every family with the generalist.** `run_baseline` took one `scan`, and the CLI handed it the `unknown` configuration; since `related(X, "unknown")` is `lateral` by the taxonomy's own decision that abstention earns no credit, **every family except `unknown` baselined at a structural zero** and every noise floor with it. `run_baseline` now takes a `scan_factory` and runs each family's own configuration, reports per slice as well as per family, and refuses fewer than three runs. This is the decision the group-2 review asked for on `unknown` serving as both abstention and generalist: it is the generalist for *scanning*, and it is never credited as a family for *scoring*.
|
||||
- **3.5, the meta-agent proposer was a stub** that returned None after confirming the extra was present, so `ousast learning round` could never propose anything at any time. It now composes `MetaAgent.evolve` behind `harness_ext` against a scratch **copy** of the family directory — the live tree is never touched, because the round snapshots only after a proposal exists — and builds the proposal from the diff of that copy, so the one-file rule and the lever whitelist are enforced on what the agent did rather than on what it claimed. `learning round --proposals FILE` runs a recorded sequence offline, which is what makes the command exercisable without the extra at all.
|
||||
- **3.6 and 3.1, the holdout refusal existed only as a vocabulary word.** `journal.py` documented `refused_holdout` and no code path could produce it; the covering test asserted `outcome == "refused_holdout" or outcome == "rejected"`, which passes whichever way the code behaves. The round now runs `refuse_if_holdout` over every corpus pair the proposal names — in its predictions and in the file it writes — and journals `refused_holdout` with the pair names. A memory entry that quotes a held-out pair looks exactly like a legitimate hard negative until the holdout number is read as if it meant something.
|
||||
- **3.7, the sweep counted absolute failures, not negative flips.** Req 9.5 compares against the pre-change state; the code counted every pair of another family that was not `pair_correct` and compared that to a budget derived from the run-to-run flip rate. On the real corpus, where most pairs fail at round zero, that rejects **every** round for damage it did not do — a harness that then reports it cannot improve. Unscorable rows voted too, against Req 4.1. The sweep is now scored before and after the change, `_negative_flips` counts only pairs that were right and are not, unscorable rows leave the count, and attribution is derived from the same comparison instead of from absolute failures.
|
||||
- **3.7, cost was metered once**, after the minibatch, the holdout and the whole cross-family sweep had all been paid for; Req 9.8 says stop, revert and record. The cap is now read after every stage. A stage that raises is journalled as `reverted`/`stage_failed` with its round directory before the exception leaves, because an exception that loses the record also loses the money the round already spent. A rejected round's version is archived: Req 9.7 exists so a configuration that lost overall but is the only one that ever won some pair is not forgotten.
|
||||
- **3.8, the corrected lever number was published beside the earlier one, not in place of it** (Req 5.3), and the test named for that property never checked it. `publish` now rewrites the superseded claim to point at the regenerated section, idempotently, and the test asserts the old sentence is gone. Run against the real roadmap: the 2026-09-05 claim "human holdout profile pair_correct 0 -> 6 of 17, Youden 0 -> +0.353" now says it was measured with holdout pairs teaching and names the artifact that corrects it.
|
||||
- **3.1, refusals were `continue` statements.** `Refusal.degradation()` existed and nothing called it. The mechanism lever now records every candidate it declined with the holdout pairs that taught it, leave-one-out records the pairs it kept out of the teaching set, and `improve` carries both into the round outcome. `semantic.seed` reported `split:train` for a train-split identical twin, which is simply untrue; an unscorable row now reports its own reason. `evaluate_profiles` and `evaluate_mechanism_profiles` are deliberately split-blind and now say so in their docstrings: they measure a ruleset and never learn from a pair, so a filter there would only shrink the evidence.
|
||||
- Test-quality repairs the review asked for, each of which changed a number: two split tests were measuring an `x = 1` fixture that teaches nothing whatever the rule says, so both arms now use a pair that really carries a shape and the lever test asserts the same shape *is* admitted when a train pair taught it; the two CLI scoreboard tests asserted against output that was all zeros, and now use a side-aware detector stub (the shipped `scripted` stub answers twice, then goes quiet, and puts its finding outside the labeled function — three separate ways to make a family number zero); `ScriptedChatClient` gained an opt-in `cycle` so a stub cannot silently run out of answers mid-run; the sandbox-candidate assertion in `tests/test_learning_scan.py` compared verdict paths against a prefix no implementation would produce and now compares finding ids; the manifest's classifier block reported `measure_classifier([])`, all zeros, and now reports what this scan actually routed.
|
||||
- Carry-forward, unchanged and still capping every holdout number: the nineteen straddling repositories, and — found during task 4.1 — **nine of the ten duplicate groups sit on both sides of the split**, which the repository-level split cannot see because the duplicates are different repositories.
|
||||
- Task 4.2 (2026-09-06, RED first: 2 failing tests in `tests/test_fixture_difficulty.py`, and the check's own arm — `test_the_check_catches_a_fixture_that_is_easier_on_one_axis` — fails if the fixture hardening is reverted). Two parts:
|
||||
- **The five twins declare their limit.** A content hash is evidence; the catalog row is the maintainer standing behind it, and without one a re-harvest that changes a byte turns a known-unscorable row back into a scored one with no review. A declared limit that names one of the vocabulary's own reasons keeps that spelling, so `identical_twin` declared and `identical_twin` computed stay one fact instead of splitting across two buckets in every aggregation. `semantic.seed._skip_reason` was reordered for the same reason: an unscorable row reports `unscorable:<reason>` before either the known-limit or the split branch can claim it. Consequence, recorded because it moves a published number: leave-one-out over vibe-py now runs on **30 rows, not 35** — a pair whose two sides are the same code cannot be held out against itself.
|
||||
- **The fixture-difficulty check** (`learning/difficulty.py`), on three axes the corpus really varies on: binding hops from the identity source to the constrained use, registration distance (0 decorator on the handler, 1 statement elsewhere in the file, 2 another document), and guard distance (0 decorator, 1 call in the body, 2 nothing visible). The comparison is per axis over the whole set, because a suite covers a bug shape with several handlers between them. **Measured first, and it failed: the shared obligations fixture reached (1, 0, 2) where the vibe-py absence rows reach (5, 2, 2).** Every shared handler carried its own route decorator, so nothing in the suite exercised a handler registered from another document and no chain was longer than one binding — the checker was passing on a simpler world than the one it meets. `from_spec` was added to the fixture: the connexion shape those rows really have, a token validated by a call rather than a decorator and five bindings between the token and the constraint. The verdict is committed as `benchmarks/measurements/2026-09-06-fixture-difficulty.json` with both reaches and the history of the first measurement. One existing expectation moved with it (`from_spec` reads the user row and then the book: two real operations).
|
||||
- Task 4.3 (2026-09-06, RED first: 10 failing tests in `tests/test_learning_cutoff.py`; then green, 11). The catalog generator fills `fix_date` from the recipe at the precision the recipe has: a full ISO day when one was recorded, otherwise the year. Every harvested recipe carries a year and almost none carries a day, and writing `-01-01` would turn a year into a claim about a month. A row therefore counts as post-cutoff only when its **earliest possible** date is after the cutoff, so a coarse date can under-claim the clean slice but never over-claim it — and over-claiming is the direction that would turn contamination into a result. `PairFamilyScore` carries the date, `aggregate(cutoff=...)` reports the same numbers over the post-cutoff rows beside the whole number and never instead of it, undated rows are counted on their own, and `publish` prints the slice with the cutoff it used. Without a configured cutoff every scoreboard says so rather than inventing one; `[learning].cutoff_date` has no default because guessing a model's training cutoff and publishing a contamination number from the guess would be worse than reporting none. Corpus dates as harvested: vibe-py all 2026, agent-vfc 67 of 69 in 2026, vfc spread 2014-2026, vfc-js 2014-2021.
|
||||
- Task 5.1 (2026-09-06, RED first: 3 of 9 tests in `tests/test_degradation_matrix.py`; then green, 8 after one test was deleted rather than kept). The three gate outputs are captured at `71efc9c` — the commit before the first line of this feature — as `benchmarks/measurements/2026-09-06-gate-baseline.txt`, and a test reproduces them byte for byte on every run, in both matrices. Until now byte-identity was something a reviewer checked by hand against a worktree; it is now mechanical and runs in CI, which is what makes it a gate rather than a habit. Six absences are each exercised once with the reason they record: HarnessX (`harnessx_unavailable`), the chat endpoint (`learning_endpoint_unavailable`, and the run still writes its artifact), the family configurations (`learning_configs_unavailable`), the network for pointer pairs (`pointer_pair_skipped`), the semantic extra (`language_unsupported` rather than a guess), and the sandbox (`sandbox_unavailable`, so a claim nothing could run is not confused with a claim that failed). One test written for this task was **deleted before the commit**: it asserted that this file contained a `without_<capability>` name per capability, which measures the file's own naming and nothing about the code.
|
||||
- Review round 4 (2026-09-06) APPROVED 4.1, 4.2, 4.3; REJECTED the round-3 remediation on three counts. The reviewer re-derived the RED phase for group 4 (24 of 26 for 4.1, 3 for 4.2, 11 of 11 for 4.3 — **every count in the notes above was understated**, corrected here), killed 18 of 19 mutants, verified the 4.1 observable independently (2 of 10 access-control rows named at `7a33756`, 9 of 10 at `3e71e9e`, the tenth stating its reason), confirmed `_python_route_access` changed no access level anywhere in the corpus and `.mjs` changed exactly one snapshot, and reproduced the fixture-difficulty artifact number for number including its failing arm. Remediated RED-first:
|
||||
- **The meta-agent proposer still could not propose.** `MetaAgent(inner_model=...)` takes a `ModelConfig`; it was handed a model name, a `# type: ignore` hid that from mypy, and a bare `except Exception` turned the resulting `AttributeError` into the degradation reason `meta_agent_failed` — which reads exactly like the model declining. Every other HarnessX composition site in the repository builds the config properly. `_meta_agent_class` and `_model_config` are now seams a test can pin without the extra, and a `TypeError`/`AttributeError`/`ImportError` from the call is re-raised: "we called the agent wrong" is not "the agent had nothing to propose".
|
||||
- **`ousast pairs --hunter` crashed in its documented form.** The K floor landed with `--k-runs` still defaulting to 1, so the flag's absence raised `ValueError` from inside `evaluate_catalog`. The default is `None` (three), and a breach exits naming the flag. The covering test always passed `--k-runs 4`, which is why nothing noticed.
|
||||
- **28 rows resolved a span on the vulnerable side and none on the fixed side** (12% of the corpus), so every fixed-side finding read as outside the labeled function and the pair scored as silence whatever the detector said — the same flattering direction as the range defect, one-seventh the size. `fix_function` reached only the harvester; it now reaches the catalog and `PairCase`, `_spans` uses it for the fixed side, and the catalog generator derives it from the fixed excerpt itself for the 25 vibe-py trap rows that never declared one. A row that still resolves no fixed-side span reports `unresolved_label:fixed` instead of scoring silent. Measured after: **0 rows**.
|
||||
- Suggestions applied: one spelling per corpus fact everywhere (`PairCase.unscorable` verbatim, no invented prefix); leave-one-out records non-teachers withheld for tier or an unscorable reason, not only holdout refusals; the hygiene test asserts the declared twins still hash equal, so a declaration that stopped being true would not be invisible; `is_post_cutoff` refuses a cutoff coarser than a day, which was the one over-claim path that existed, and month precision is pinned; `UNSCORABLE_REASONS` covers the whole vocabulary rather than `identical_twin` alone; the zero-run score carries its date; `_pairs_named_by` filters by substring before the regex.
|
||||
- Carry-forward, measured 2026-09-06 after group 4 (`benchmarks/measurements/2026-09-06-trajectory-divergence.json`): **the K-run numbers this spec publishes are ensemble votes, not repeated measurements.** Across 120 runs of the injection family at temperature 0 with thinking disabled there was not one repeated tool-call trajectory, and outcome disagreement tracks trajectory divergence (30.9% at low similarity, 4.2% at mid). Consequences for anyone reading these numbers: Req 3.5's K is an ensemble over distinct search procedures; Req 8.2's noise floor measures how often an unconstrained search reaches the sink rather than the detector's uncertainty, so Req 9.5's regression budget grows with the search's own wandering; and the instability is entirely one-sided — every pair produced a single finding set on the fixed side, so specificity is solid and recall carries the caveat. `.kiro/specs/detector-optimizer/brief.md` proposes the repair, and it is a program change rather than a prompt change.
|
||||
- Task 5.3 (2026-09-07, five rounds on `injection`, $2.17, `benchmarks/measurements/2026-09-07-evolve-rounds-injection.json`). The task was amended to target injection because access_control has one train teacher.
|
||||
|
||||
| round | lever | outcome | train | p | holdout | p | distinguishable from noise |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | memory | rejected `train_regressed` | 0+/3− | 0.250 | 1+/4− | 0.375 | no |
|
||||
| 2 | checklist | rejected `train_regressed` | 0+/1− | 1.000 | 1+/1− | 1.000 | no |
|
||||
| 3 | prompt | **accepted `improved` → v1** | 2+/0− | 0.500 | 2+/1− | 1.000 | no |
|
||||
| 4 | meta-agent | rejected `no_proposal` | — | — | — | — | never ran |
|
||||
| 5 | counterexample | rejected `train_regressed` | 0+/1− | 1.000 | 1+/3− | 0.625 | no |
|
||||
|
||||
**Nothing the loop decided carried evidence, in either direction — five rounds, zero reliable decisions.** Round 2 rejected a change because **one pair of eight** moved, at p = 1.000 — the maximum the sign test can return. Round 3 accepted one because two of eight improved and none regressed, at p = 0.500, and that acceptance is now versioned as v1 and will be built on. On a family whose measured floor says **11 of 20 pairs disagree with themselves between runs of the unchanged detector**, both readings are what changing nothing looks like.
|
||||
|
||||
I overstated this once and correct it here: after rounds 1 and 2 I wrote that the rule was "close to structurally incapable of accepting anything". Round 3 accepted. The accurate statement is symmetric and worse — the rule accepts on noise as readily as it rejects on it.
|
||||
|
||||
- **Escalation to the maintainer, not applied here.** Req 9.5's threshold rule (reject if either side regresses, accept if one improves and neither regresses, tie rejects) should be replaced by an acceptance that survives the measured variance. `Archive` already holds the per-pair Pareto frontier and `acceptance.reliability` already computes the test; neither decides anything. The honest caveat is that at a minibatch of 8 and a holdout of 10 a reliability-gated rule would accept almost nothing either: **the corpus is the binding constraint, not the rule**, and no acceptance rule fixes a train split of twelve.
|
||||
- **The v1 configuration rests on p = 0.500** and should probably be reverted before any further round builds on it. Left in place, and flagged, because reverting an accepted round is the maintainer's call.
|
||||
- Round 1's rejection was correct but for my error, not the loop's insight: the `memory` lever writes `hard_negatives.jsonl`, which the detector renders as *"Known non-issues, do not report these"*, and I fed it the four vulnerability shapes the train split teaches. Round 5 re-ran the same content through `counterexamples` (*"Worked examples"*), which is the lever I meant — and was rejected because **one pair of eight** moved, at p = 1.000. **The experiment I designed to give the demo lever a fair test could not give it one.** The rule cannot distinguish a lever that helps from a lever that does nothing, so correcting my authoring error changed the reason for the verdict and not the verdict.
|
||||
- What did work: the cross-family sweep caught a genuine negative flip in `access_control` in round 2 — a real regression found by the before/after comparison that replaced the absolute-failure count, which under the old rule would have rejected rounds for families that were already failing.
|
||||
|
||||
- **The finding that outranks the rounds (maintainer, 2026-09-07: "the error is not the model, the error is the elsewhere").** Eighteen defects were found in this session's measurement and round work. Every one was in this repository; none was the model's. The list is kept because it should change how the next bad number is triaged: the prose parser that discarded findings the model had already reached; the CLI that never read the cost meter it published; the baseline that lost everything on an exception, and again on a timeout; two keys that collapsed a slice into another, in two different modules; the pattern ruleset at a 12.1% ceiling; a harvester that truncated an excerpt at both ends; a family shape that could not see a permissive default inside a call argument; a lever wired to the file that says *do not report these*; a provider seam hard-coded to a vendor with no key; a proposer reason the journal threw away; a retry that did not cover a body which stopped mid-read; an exception path wrapped around half a round; log-probabilities read from the wrong object; `.mjs` absent from the language table; and an access-level check that matched prose inside a decorator. Three of them were the same shape — fixing an instance and missing the class. **The default assumption for an unexplained number is that we broke it.** `deepseek-v4-pro` scoring 46.7% against flash's 83.3% is unexplained and is therefore, by this prior, ours until shown otherwise.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Brief: MCP shift-left agent + skills
|
||||
|
||||
Status: brief / not yet specced. Proposed 2026-10-07. Delivery surface for the
|
||||
qualified safety net; builds on the existing `ousast mcp` server and the product
|
||||
skills. Related: `proof-arm-opencode` (the proof model), `pre-push-safety-net`
|
||||
(the gate), `unseen-repo-evaluation` (the qualification).
|
||||
|
||||
## Motivation
|
||||
|
||||
The pre-push hook is the gate. Developers and their coding agents want the same
|
||||
security feedback **earlier**, while the code is still being written, before
|
||||
push. In an agent-driven workflow, a mandatory security check is how a human
|
||||
oversees agents: the agent's output has to clear it before it ships.
|
||||
|
||||
Proposal: expose the safety net through an MCP agent and a small set of skills so
|
||||
an editor or a coding agent can ask for a security read of the changed code on
|
||||
demand, shift-left, and so a coding agent can be required to pass it before it
|
||||
proposes a push.
|
||||
|
||||
## Shape
|
||||
|
||||
- Transport: extend the existing narrow MCP server (`ousast mcp`, stdio) that
|
||||
OpenCode and other agents already speak. No new network surface; same
|
||||
no-secret, bounded contract.
|
||||
- What the agent offers:
|
||||
1. Scan the current diff with the static arm (fast, no model, no cluster) and
|
||||
return findings scoped to the change, with location and a concrete repair
|
||||
direction, not a confidence label.
|
||||
2. Triage a finding: explain it in plain terms and whether it is reachable.
|
||||
3. Prove on demand: hand the top candidate to the proof arm when a runnable
|
||||
harness exists, and return the demonstrated proof-of-concept.
|
||||
- Skills: the existing `openultrasast-scan` and `openultrasast-triage`
|
||||
product skills, plus shift-left additions (scan-the-diff, explain-a-finding,
|
||||
suggest-a-fix-direction) that a coding agent invokes mid-task.
|
||||
- Oversight mode: a coding agent can be configured to call the scan as a
|
||||
required step before it finalizes a change, so security review is automatic
|
||||
rather than remembered.
|
||||
|
||||
## Control (same model as the hook)
|
||||
|
||||
Advisory by default; blocking is opt-in. Budget and a push/call deadline bound
|
||||
the work. The inline path runs the fast static arm only; heavy proof is explicit
|
||||
and lane-selected. Models and spend caps stay in `.env`.
|
||||
|
||||
## Open questions for the spec
|
||||
|
||||
1. Inline latency budget: in-editor feedback must return in seconds, so the
|
||||
static arm must be fast on a single diff. What is the time contract, and does
|
||||
the large-repo latency problem block it?
|
||||
2. Return contract: exactly what a coding agent gets back (findings, locations,
|
||||
repair direction, optional proof) and how it decides pass/fail in oversight
|
||||
mode. The return should be ticket-ready evidence a busy developer can paste
|
||||
into the issue tracker to win capacity in a prioritization discussion with a
|
||||
product owner, since a reproducible proof is what moves a security fix out of
|
||||
the backlog.
|
||||
3. Does oversight mode need a signed/attestable result so a reviewer can trust
|
||||
that the agent actually ran the check?
|
||||
4. Reuse: the `ousast mcp` tool set already exists; which tools are enough and
|
||||
what is genuinely new.
|
||||
|
||||
## Scope and sequencing
|
||||
|
||||
This is a delivery and adoption surface, correctly **after** the safety net is
|
||||
qualified on unseen repositories (G0 through G5). It is not a detection change;
|
||||
it is how developers and agents consume the detection early. The proof model
|
||||
improvement lives in `proof-arm-opencode`; this brief is the dev- and
|
||||
agent-facing feedback channel.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"feature_name": "mcp-shift-left-agent",
|
||||
"created_at": "2026-10-07T15:10:00Z",
|
||||
"updated_at": "2026-10-07T15:10:00Z",
|
||||
"language": "en",
|
||||
"phase": "brief",
|
||||
"approvals": {"requirements": {"generated": false, "approved": false}, "design": {"generated": false, "approved": false}, "tasks": {"generated": false, "approved": false}},
|
||||
"ready_for_implementation": false,
|
||||
"summary": "MCP agent + skills so developers and coding agents get the safety net's feedback early (shift-left, in-editor) and so a coding agent can be required to pass a security check before it ships. Delivery surface after G0-G5 qualification; builds on the existing ousast mcp server."
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
> deterministically from code**; execution is a later, conditional tier. The correction is integrated below.
|
||||
|
||||
**Status:** course-setting. Supersedes `learning-harness` and `constrained-detector`. Not a requirements
|
||||
document — a decision about what to build, what to delete, and what to adopt, written after the
|
||||
document, a decision about what to build, what to delete, and what to adopt, written after the
|
||||
learning-harness line produced numbers its own measurements say to discard.
|
||||
|
||||
## 1. The premise we got wrong
|
||||
@@ -17,19 +17,19 @@ averaged (K runs), floored (per-family noise budget), gated (sign test), and slo
|
||||
Every mechanism in `learning-harness` follows from that assumption. This session measured all of it and it
|
||||
does not hold:
|
||||
|
||||
- Five evolve rounds, $2.17, **zero decisions distinguishable from chance** — because the acceptance rule
|
||||
- Five evolve rounds, $2.17, **zero decisions distinguishable from chance**, because the acceptance rule
|
||||
compares two ensemble votes over a detector that never runs the same investigation twice.
|
||||
- 120 runs of one detector produced **120 distinct trajectories**. The variance is the loop's freedom to
|
||||
search, not the model's uncertainty about an answer.
|
||||
- The noise floor a regression budget is derived from measures *how often an unconstrained search happens
|
||||
to reach the sink* — so the worse the search wanders, the more collateral damage a round is licensed to do.
|
||||
to reach the sink*, so the worse the search wanders, the more collateral damage a round is licensed to do.
|
||||
|
||||
The assumption is also empirically false as a claim about LLMs, and the evidence is now overwhelming:
|
||||
|
||||
- **Google Big Sleep** (Nov 2024): the first LLM-discovered zero-day in widely-used real-world software — a
|
||||
stack buffer underflow in SQLite — found by *variant analysis*, a sandboxed test case, and an
|
||||
- Google Big Sleep (Nov 2024): the first LLM-discovered zero-day in widely-used real-world software, a
|
||||
stack buffer underflow in SQLite, found by *variant analysis*, a sandboxed test case, and an
|
||||
auto-generated root-cause report. Not pattern-matching; reproduction.
|
||||
- **DARPA AIxCC finals** (DEF CON 33, Aug 2025): seven autonomous systems processed 54M lines of code,
|
||||
- DARPA AIxCC finals (DEF CON 33, Aug 2025): seven autonomous systems processed 54M lines of code,
|
||||
patched 43 of 54 synthetic vulnerabilities and **uncovered 18 previously-unknown real 0-days**. Buttercup
|
||||
(Trail of Bits, 2nd place) found **28 vulnerabilities across 20 CWEs at 90% accuracy, $181 per point,
|
||||
using non-reasoning LLMs only**, ~$152 per task, 45 minutes each.
|
||||
@@ -47,11 +47,11 @@ Lazarus-AI, MIT, an open reproduction of Anthropic's Glasswing) share one shape:
|
||||
> **The LLM is a judgement layer on top of a deterministic substrate that arbitrates its claims. Truth comes
|
||||
> from that substrate, not from a score against a labeled corpus.**
|
||||
|
||||
In the memory-safety systems above the substrate is *execution* — a crash is the arbiter. But execution is
|
||||
In the memory-safety systems above the substrate is *execution*. A crash is the arbiter. But execution is
|
||||
only the arbiter you reach for when the target builds in isolation and the bug crashes, which is the C world,
|
||||
not ours. The general form of the substrate is a **program model built from the code**: a deterministic
|
||||
abstraction rich enough to *check* a claim — does the source actually reach the sink through resolved
|
||||
dataflow; is there a guard on the path or isn't there; is the entry point attacker-reachable. Execution is one
|
||||
abstraction rich enough to *check* a claim (does the source actually reach the sink through resolved
|
||||
dataflow; is there a guard on the path or isn't there; is the entry point attacker-reachable). Execution is one
|
||||
way to arbitrate; a model is the way that works for a SAST project and for bugs that never crash.
|
||||
|
||||
Clearwing's evidence ladder is the whole thesis in one type:
|
||||
@@ -81,24 +81,24 @@ real. **We built K-runs and noise floors because we had no ground truth. They ge
|
||||
This is not a total loss, and the course is not "throw everything away." Two results here are real,
|
||||
measured, and *absent from Clearwing*:
|
||||
|
||||
1. **IR-driven candidate enumeration with a published ceiling.** We measured that the pattern ruleset caps
|
||||
1. IR-driven candidate enumeration with a published ceiling. We measured that the pattern ruleset caps
|
||||
recall at 12.1% while call-sites-and-bindings from the semantic IR reach **96.6%** across the web slices,
|
||||
a median of ten sites per function. Clearwing's candidate feeder is C-first (taint tables for
|
||||
`memcpy`-shaped bugs); its `web_framework` specialist is a *single unconstrained prompt* with no
|
||||
equivalent candidate enumerator for logic/web classes.
|
||||
|
||||
2. **Absence-bug detection.** Our `obligations` work — an access-control bug is the *absence* of a guard
|
||||
around an operation its siblings protect — has no analogue in Clearwing, whose ladder is built around a
|
||||
2. Absence-bug detection. Our `obligations` work (an access-control bug is the *absence* of a guard
|
||||
around an operation its siblings protect) has no analogue in Clearwing, whose ladder is built around a
|
||||
*crash*. An IDOR or a missing authorization check never crashes a sandbox. Clearwing's execution
|
||||
ground-truth model does not reach these classes at all, and they are exactly the "dangerous but
|
||||
low-hanging" web bugs this project was chartered to catch.
|
||||
|
||||
So the gap Clearwing leaves open is **the web/logic taxonomy without a crash oracle** — which is where our
|
||||
So the gap Clearwing leaves open is **the web/logic taxonomy without a crash oracle**, which is where our
|
||||
IR candidate enumerator, our family taxonomy, and our obligation checker are genuinely ahead.
|
||||
|
||||
## 4b. The model layer, and the evidence ladder it produces
|
||||
|
||||
The arbiter is a program model constructed deterministically from the code — the thing an LLM claim is
|
||||
The arbiter is a program model constructed deterministically from the code, the thing an LLM claim is
|
||||
*checked against*, so that a finding's truth does not rest on the LLM that proposed it. We already hold the
|
||||
first rungs of it; they were buried under a noise layer instead of being made the arbiter.
|
||||
|
||||
@@ -113,16 +113,16 @@ The evidence ladder is the model layer made legible. Each rung is a stronger, st
|
||||
|
||||
Two properties are the whole point:
|
||||
|
||||
- **`model_corroborated` and above are deterministic.** Run the checker twice over the same model and it
|
||||
returns the same verdict. That removes the run-to-run noise the same way a crash would — no K-runs, no
|
||||
floor, no sign test — *without needing a runnable target*. The noise this session measured was the LLM
|
||||
- `model_corroborated` and above are deterministic. Run the checker twice over the same model and it
|
||||
returns the same verdict. That removes the run-to-run noise the same way a crash would (no K-runs, no
|
||||
floor, no sign test), *without needing a runnable target*. The noise this session measured was the LLM
|
||||
free-searching; a model check does not search.
|
||||
- **The LLM's job shrinks to the residual the model cannot decide** — is this string genuinely
|
||||
attacker-controlled, is this guard semantically sufficient — and even that answer is checked against the
|
||||
- The LLM's job shrinks to the residual the model cannot decide (is this string genuinely
|
||||
attacker-controlled, is this guard semantically sufficient), and even that answer is checked against the
|
||||
model rather than averaged over runs. The LLM stops being the oracle and becomes a proposer the model
|
||||
audits.
|
||||
|
||||
**Measured, 2026-09-08 — the entailment ceiling** (`benchmarks/measurements/2026-09-08-model-entailment-ceiling.json`,
|
||||
**Measured, 2026-09-08, the entailment ceiling** (`benchmarks/measurements/2026-09-08-model-entailment-ceiling.json`,
|
||||
offline, no LLM):
|
||||
|
||||
| rung | web/logic taxonomy | for comparison |
|
||||
@@ -134,17 +134,17 @@ offline, no LLM):
|
||||
|
||||
The gap between 96.6% (a site exists) and 2.2% (the model *decides* the vulnerability) is the gap between
|
||||
enumerating and arbitrating, and it is the whole finding. The model cannot be a *pure* arbiter at v1. But it
|
||||
is not a wall — the measurement diagnoses depth, not impossibility: one-hop source resolution (real taint sits
|
||||
is not a wall. The measurement diagnoses depth, not impossibility: one-hop source resolution (real taint sits
|
||||
1–3 binds upstream: `q = "…" + id; execute(q)`), a closed literal sink table (misses project wrappers and the
|
||||
path/xss/prototype sinks), and the function-naming gap that eats 20/50 injection pairs as `no_function`.
|
||||
Deepening the taint to reuse the candidate enumerator's depth-4 chain already moved injection 24% → 48%.
|
||||
|
||||
So the architecture the numbers support is a **layered arbiter**, not a pure one:
|
||||
|
||||
- where the model **entails** — report `model_entailed`, deterministic, the LLM not needed;
|
||||
- where the model **corroborates a taint path** (~35% and rising) — the LLM judges, but its claim is *checked*
|
||||
- where the model **entails**, report `model_entailed`, deterministic, the LLM not needed;
|
||||
- where the model **corroborates a taint path** (~35% and rising), the LLM judges, but its claim is *checked*
|
||||
against the resolved path, so run-to-run noise is removed on this band;
|
||||
- where the model can only **enumerate** (the rest, up to 96.6%) — honest `suspicion`, the LLM proposes with
|
||||
- where the model can only **enumerate** (the rest, up to 96.6%), honest `suspicion`, the LLM proposes with
|
||||
no deterministic backstop, and this is exactly the band where the deferred execution / differential tier
|
||||
earns its place.
|
||||
|
||||
@@ -152,32 +152,32 @@ Pure `model_entailed`-as-arbiter is the north star. The v1 is the layered arbite
|
||||
named by the measurement: fix function naming, build per-family sink/sanitizer tables, and lift the flow
|
||||
model onto the multi-hop binding chain we already have.
|
||||
|
||||
This is the classical SAST arbiter — dataflow / abstract interpretation, the CodeQL and Infer lineage —
|
||||
This is the classical SAST arbiter (dataflow / abstract interpretation, the CodeQL and Infer lineage),
|
||||
with the LLM supplying the semantic judgement the model is weakest at (aliasing intent, whether a value is
|
||||
really untrusted, whether a guard really covers the case). It does not need soundness. A partial model that
|
||||
can confirm *some* claims and honestly returns "could not confirm — stays suspicion" for the rest is already
|
||||
can confirm *some* claims and honestly returns "could not confirm, stays suspicion" for the rest is already
|
||||
the independent arbiter we never had.
|
||||
|
||||
## 4c. IL or AST, build or adopt — the substrate decision
|
||||
## 4c. IL or AST, build or adopt, the substrate decision
|
||||
|
||||
**IL, and adopt one.** Both halves are forced by the entailment measurement.
|
||||
|
||||
*Why an IL, not the AST we have.* Our `semantic/ir.py` is not an IL — it is a lossy AST projection: per-function
|
||||
*Why an IL, not the AST we have.* Our `semantic/ir.py` is a lossy AST projection rather than an IL: per-function
|
||||
flat lists of calls and binds, no control-flow graph, no basic blocks, no data-dependence edges. That is
|
||||
*why* entailment sits at 2.2%. Real abstract interpretation is a fixpoint over a **CFG**: guard *dominance*
|
||||
(is the check on every path to the operation) is a dominator-tree question, and path-sensitive taint (a
|
||||
sanitizer on the taint path specifically) needs the branch structure. A flat bind list cannot express either;
|
||||
it can only over-approximate taint flow-insensitively by name, which is the one-hop ceiling we measured. The
|
||||
model layer wants the classical IL that merges AST + CFG + PDG + call graph into one graph — a **Code Property
|
||||
Graph (CPG)** — over which taint is a reachability query (source → sink minus sanitizer) and absence is a
|
||||
model layer wants the classical IL that merges AST + CFG + PDG + call graph into one graph (a **Code Property
|
||||
Graph (CPG)**), over which taint is a reachability query (source → sink minus sanitizer) and absence is a
|
||||
dominance query.
|
||||
|
||||
*Why adopt, not build.* Building a multi-language CFG/PDG/taint engine on top of tree-sitter is precisely
|
||||
reinventing CodeQL, Semgrep and Joern — a multi-year effort, and the maximal case of the failure mode this
|
||||
reinventing CodeQL, Semgrep and Joern, a multi-year effort, and the maximal case of the failure mode this
|
||||
session logged eighteen times. **Joern** is the adoptable answer: Apache-2.0, a CPG engine for exactly our
|
||||
corpus (C/C++/Java/JavaScript/Python/Kotlin/binary), a built-in taint-propagation engine, embeddable, and
|
||||
there is already a published CPG-plus-LLM bridge (Codebadger, an MCP server over Joern) and an arXiv line on
|
||||
CPGs as the substrate for LLM program analysis. Joern *is* "a model built up from code" as a graph — the exact
|
||||
CPGs as the substrate for LLM program analysis. Joern *is* "a model built up from code" as a graph, the exact
|
||||
thing the maintainer described. The alternatives: CodeQL is more powerful but least adoptable (restrictive
|
||||
license, query-only, GitHub-owned); Semgrep's taint engine is lighter and we already have integration
|
||||
precedent through Clearwing's sidecar, but interprocedural taint is behind the paid Pro engine.
|
||||
@@ -192,14 +192,14 @@ source ──▶ Joern: compile to CPG (one IL: AST + CFG + PDG + call graph, la
|
||||
```
|
||||
|
||||
The CPG is language-agnostic once its frontend exists, so the taint and dominance logic is written *once*, not
|
||||
per language — which is the whole reason our per-language tree-sitter approach kept hitting naming and grammar
|
||||
per language, which is the whole reason our per-language tree-sitter approach kept hitting naming and grammar
|
||||
gaps.
|
||||
|
||||
*What stays ours, and is the contribution.* Joern is a workbench, not an opinionated web/logic detector with
|
||||
an LLM judge. What we own and port onto it: the closed **family taxonomy**; the **source / sink / sanitizer /
|
||||
guard models** for the web/logic CWEs — our obligation facts and closed guard vocabulary become CPG queries
|
||||
and taint specifications; and the **LLM-adjudication-checked-against-the-CPG** orchestration. That security
|
||||
modelling — expressed as queries over an adopted CPG — is where our work is genuinely ahead of both Joern
|
||||
guard models** for the web/logic CWEs (our obligation facts and closed guard vocabulary become CPG queries
|
||||
and taint specifications); and the **LLM-adjudication-checked-against-the-CPG** orchestration. That security
|
||||
modelling, expressed as queries over an adopted CPG, is where our work is ahead of both Joern
|
||||
(no opinion on these classes) and Clearwing (crash-oracle, no arbiter for bugs that do not crash).
|
||||
|
||||
*The one real cost.* Joern is JVM/Scala; embedding it in a Python tool means its server mode or the MCP bridge,
|
||||
@@ -212,38 +212,38 @@ noise architecture; keep execution as a later, conditional tier.**
|
||||
|
||||
Concretely, superseding both prior specs:
|
||||
|
||||
1. **Delete, do not fix.** The K-run machinery, the per-family noise floor, the regression budget, the sign
|
||||
1. Delete, do not fix. The K-run machinery, the per-family noise floor, the regression budget, the sign
|
||||
test, the evolve loop, the acceptance rule, the mechanism-lever optimizer. All of it existed to average an
|
||||
LLM oracle we could not otherwise trust. A deterministic model arbiter removes the need for every piece.
|
||||
`learning-harness` Req 3.5, 8.2, 9.5 and the evolve tasks are retired, not amended.
|
||||
|
||||
2. **Adopt a CPG engine (Joern) as the arbiter substrate; port our security models onto it.** We do not lift
|
||||
2. Adopt a CPG engine (Joern) as the arbiter substrate; port our security models onto it. We do not lift
|
||||
our flat IR to a CFG ourselves (§4c). The taxonomy, the source/sink/sanitizer/guard models, and the
|
||||
obligation/absence logic move from prompt context to **CPG taint specifications and dominance queries** —
|
||||
obligation/absence logic move from prompt context to **CPG taint specifications and dominance queries**,
|
||||
deterministic `model_corroborated` and `model_entailed` verdicts over an engine that already has the CFG
|
||||
and PDG our IR lacks. Our tree-sitter IR retains one job: cheap candidate enumeration for the `suspicion`
|
||||
band where the CPG has no verdict.
|
||||
|
||||
3. **The LLM proposes; the model disposes.** One bounded, typed question per candidate — the constrained
|
||||
detector we already started — but the answer is not scored against a corpus, it is *checked against the
|
||||
model*. A claim the model corroborates advances; a claim it contradicts is dropped with the contradiction
|
||||
3. The LLM proposes; the model disposes. One bounded, typed question per candidate (the constrained
|
||||
detector we already started), with the answer *checked against the
|
||||
model* rather than scored against a corpus. A claim the model corroborates advances; a claim it contradicts is dropped with the contradiction
|
||||
recorded; a claim it can neither confirm nor deny stays `suspicion` and is reported as such. No K-runs, no
|
||||
evolve loop.
|
||||
|
||||
4. **Corpus is the model's calibration set, not the LLM's training signal.** Keep the vulnerable-parent /
|
||||
fix-commit pairs — but their role changes. The pair is ground truth for *the model's precision*: the fix
|
||||
4. Corpus is the model's calibration set, not the LLM's training signal. Keep the vulnerable-parent /
|
||||
fix-commit pairs, but their role changes. The pair is ground truth for *the model's precision*: the fix
|
||||
is what the model should be able to see (a guard added, a sanitizer inserted, a bound checked), and a pair
|
||||
the model cannot distinguish is a gap in the model, named and measured, not a detector miss to average.
|
||||
Harvest more pairs at repository scale from CVE history, as Clearwing does; truth is the fix diff read by
|
||||
the model, not a label we hand-assign.
|
||||
|
||||
5. **Execution is a deferred, conditional tier.** For the classes a static model genuinely cannot arbitrate —
|
||||
memory-safety-in-C, where reachability and exploitability turn on runtime layout — a sandbox crash oracle
|
||||
5. Execution is a deferred, conditional tier. For the classes a static model genuinely cannot arbitrate
|
||||
(memory-safety-in-C, where reachability and exploitability turn on runtime layout), a sandbox crash oracle
|
||||
is the right arbiter, and Clearwing already is that system. Adopt it *there*, later, only when a class
|
||||
needs it. It is the `execution_confirmed` rung, not the foundation.
|
||||
|
||||
6. **Reporting keeps the honesty disciplines that stand:** per-slice numbers with the real-world slice
|
||||
deciding, the overfitting gap published, and the evidence rung on every finding — now meaning a model
|
||||
6. Reporting keeps the honesty disciplines that stand: per-slice numbers with the real-world slice
|
||||
deciding, the overfitting gap published, and the evidence rung on every finding, now meaning a model
|
||||
verdict, not a sandbox result.
|
||||
|
||||
## 6. Build vs. adopt, revised
|
||||
@@ -252,14 +252,14 @@ The earlier draft of this brief over-rotated to "adopt Clearwing wholesale," bec
|
||||
foregrounds execution. The maintainer's correction re-centres it, and the honest split is cleaner than either
|
||||
extreme:
|
||||
|
||||
- **The model layer is adopt-plus-contribute, not build.** The arbiter *engine* is Joern's CPG (Apache-2.0,
|
||||
our languages, taint + dominance built in); the *contribution* is our security modelling on top of it — the
|
||||
- The model layer is adopt-plus-contribute, not build. The arbiter *engine* is Joern's CPG (Apache-2.0,
|
||||
our languages, taint + dominance built in); the *contribution* is our security modelling on top of it, the
|
||||
taxonomy and the source/sink/sanitizer/guard specs for the web/logic classes, plus the LLM adjudication
|
||||
checked against the CPG. The entailment measurement (2.2% on our flat IR) is what rules out building the
|
||||
engine ourselves: that number is the distance between our AST projection and a real CPG, and closing it by
|
||||
hand is reimplementing Joern.
|
||||
- **The execution tier is Clearwing's to adopt**, later, for the memory-safety classes where it is strong and
|
||||
we are weak — via the fork the maintainer already maintains. We do not reimplement its sandbox lifecycle,
|
||||
- The execution tier is Clearwing's to adopt, later, for the memory-safety classes where it is strong and
|
||||
we are weak, via the fork the maintainer already maintains. We do not reimplement its sandbox lifecycle,
|
||||
container pooling, sanitizer images or PoC stability; those are exactly the parts this session proved we get
|
||||
wrong.
|
||||
|
||||
@@ -268,24 +268,24 @@ arbiter we cannot, when a class demands it.
|
||||
|
||||
## Open questions for the requirements phase
|
||||
|
||||
- **How far up the ladder can a partial model reach without soundness?** `model_corroborated` needs only a
|
||||
- How far up the ladder can a partial model reach without soundness? `model_corroborated` needs only a
|
||||
consistency check; `model_entailed` needs a real interprocedural dataflow/guard-dominance result. What
|
||||
fraction of our corpus can the model *entail* today, and where does it honestly fall back to `suspicion`?
|
||||
This is the first measurement to run — the model analogue of the 96.6% candidate ceiling — and it can end
|
||||
This is the first measurement to run (the model analogue of the 96.6% candidate ceiling), and it can end
|
||||
or reshape the design before any LLM cost, exactly as the ceiling did.
|
||||
- **What is the model's arbiter for an absence bug?** A missing guard has no dataflow path to confirm. The
|
||||
- What is the model's arbiter for an absence bug? A missing guard has no dataflow path to confirm. The
|
||||
obligation/sibling-dominance model is the candidate arbiter; does it distinguish the vulnerable-parent from
|
||||
the fixed twin across the corpus on its own, deterministically?
|
||||
- **Where does the LLM's residual judgement resist checking?** Some claims — "this value is attacker-
|
||||
controlled by convention", "this guard is semantically insufficient" — the model cannot audit. How large is
|
||||
- Where does the LLM's residual judgement resist checking? Some claims ("this value is attacker-
|
||||
controlled by convention", "this guard is semantically insufficient") the model cannot audit. How large is
|
||||
that residual, and is `suspicion` an acceptable home for it or does it need the execution tier sooner?
|
||||
- **Which classes cross into execution, and when.** Memory-safety-in-C is the clear case for the deferred
|
||||
- Which classes cross into execution, and when. Memory-safety-in-C is the clear case for the deferred
|
||||
tier. Is anything in the web/logic taxonomy genuinely un-arbitrable by a model, forcing the differential
|
||||
request oracle earlier than "later, if needed"?
|
||||
- **What of the existing corpus survives** as the model's calibration set, given the twin/straddle/tier
|
||||
- What of the existing corpus survives as the model's calibration set, given the twin/straddle/tier
|
||||
defects the learning-harness reviews surfaced?
|
||||
|
||||
Sources: [Big Sleep / SQLite zero-day](https://thehackernews.com/2024/11/googles-ai-tool-big-sleep-finds-zero.html) ·
|
||||
[AIxCC finals — Buttercup, 28 vulns / 20 CWEs / non-reasoning LLMs](https://blog.trailofbits.com/2025/08/09/trail-of-bits-buttercup-wins-2nd-place-in-aixcc-challenge/) ·
|
||||
[AIxCC results — 18 real 0-days, 43/54 patched](https://semiengineering.com/aixcc-2025-what-it-means-for-device-security/) ·
|
||||
[AIxCC finals, Buttercup, 28 vulns / 20 CWEs / non-reasoning LLMs](https://blog.trailofbits.com/2025/08/09/trail-of-bits-buttercup-wins-2nd-place-in-aixcc-challenge/) ·
|
||||
[AIxCC results, 18 real 0-days, 43/54 patched](https://semiengineering.com/aixcc-2025-what-it-means-for-device-security/) ·
|
||||
[Clearwing](https://github.com/Lazarus-AI/clearwing) · local: `/home/mc/Source/clearwing`, fork `norandom/deepwing`.
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
The detector stops being a noisy oracle and becomes a proposer that a deterministic program model audits. The
|
||||
model is a **Code Property Graph** (Joern, Apache-2.0): one graph merging AST, CFG, PDG and call graph, over
|
||||
which taint reachability and guard dominance produce verdicts on the evidence ladder — `suspicion`,
|
||||
which taint reachability and guard dominance produce verdicts on the evidence ladder, `suspicion`,
|
||||
`model_corroborated`, `model_entailed`, `execution_confirmed`. The LLM is asked one bounded question per
|
||||
candidate; its answer is checked against the CPG, not averaged over runs.
|
||||
|
||||
Two disciplines shape every decision below. **Subtraction first:** the noise architecture and the outgrown
|
||||
test surface are removed and audited before the CPG arbiter is adopted, because the session that produced this
|
||||
spec logged eighteen defects building on an overgrown surface. **Adopt, don't build:** the CFG/PDG our flat IR
|
||||
lacks comes from Joern, not from a hand-built engine — the 2.2% entailment ceiling is the measured distance
|
||||
lacks comes from Joern, not from a hand-built engine. The 2.2% entailment ceiling is the measured distance
|
||||
between our AST projection and a real CPG, and closing it by hand is reimplementing Joern.
|
||||
|
||||
### Goals
|
||||
@@ -19,12 +19,12 @@ between our AST projection and a real CPG, and closing it by hand is reimplement
|
||||
- Remove the noise architecture and reduce the test suite with it, gate-protected and reversible.
|
||||
- A deterministic model verdict on the band the model covers; honest `suspicion` on the rest.
|
||||
- The CPG engine, the LLM endpoint and any execution tier are optional; the core stays zero-dependency.
|
||||
- The security modelling — taxonomy, source/sink/sanitizer/guard specs — is ours, expressed as CPG queries.
|
||||
- The security modelling (taxonomy, source/sink/sanitizer/guard specs) is ours, expressed as CPG queries.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Building a control-flow or dataflow engine (adopted, Req 4). Prompt optimisation, evolve loops, acceptance
|
||||
rules (deleted, Req 8.5). The execution tier itself (deferred to Clearwing, Req 10). Soundness — the
|
||||
rules (deleted, Req 8.5). The execution tier itself (deferred to Clearwing, Req 10). Soundness, the
|
||||
north-star `model_entailed`-as-pure-arbiter is not v1.
|
||||
|
||||
## Boundary Commitments
|
||||
@@ -32,18 +32,18 @@ between our AST projection and a real CPG, and closing it by hand is reimplement
|
||||
### This Spec Owns
|
||||
|
||||
- The subtraction: which modules are removed, the dangling-import check, the module-audit manifest.
|
||||
- `cpg/` — the Joern seam (`CpgBackend`, the subprocess/query implementation, the null backend).
|
||||
- `model/` — `Verdict`/`Rung`, taint reachability, guard dominance, constant abstraction; the security specs
|
||||
- `cpg/`, the Joern seam (`CpgBackend`, the subprocess/query implementation, the null backend).
|
||||
- `model/`, `Verdict`/`Rung`, taint reachability, guard dominance, constant abstraction; the security specs
|
||||
ported from the deleted modules' vocabularies.
|
||||
- The proposer/judge orchestration where the LLM's claim is checked against the model.
|
||||
- The corpus-as-calibration harness and its model-gap report.
|
||||
|
||||
### Out of Boundary
|
||||
|
||||
- The family **taxonomy** (`learning/families.py`) — retained unchanged, re-homed under `model/`.
|
||||
- The corpus loader, per-slice split and gates — retained; the gates stay byte-identical.
|
||||
- Redaction, the LLM endpoint client (`learning/endpoint.py`) — retained.
|
||||
- Clearwing's sandbox lifecycle — never reimplemented; the execution tier adopts it.
|
||||
- The family **taxonomy** (`learning/families.py`), retained unchanged, re-homed under `model/`.
|
||||
- The corpus loader, per-slice split and gates, retained; the gates stay byte-identical.
|
||||
- Redaction, the LLM endpoint client (`learning/endpoint.py`), retained.
|
||||
- Clearwing's sandbox lifecycle, never reimplemented; the execution tier adopts it.
|
||||
|
||||
### Allowed Dependencies
|
||||
|
||||
@@ -63,7 +63,7 @@ between our AST projection and a real CPG, and closing it by hand is reimplement
|
||||
### Existing Architecture Analysis
|
||||
|
||||
The `learning/` package (4,091 lines, 16 modules) implements the noise architecture and is **absent from the
|
||||
gate path** — the gates import only from `pairs`, `benchmark`, `findings`, `mapping`, `preprocess`, `rank`,
|
||||
gate path**. The gates import only from `pairs`, `benchmark`, `findings`, `mapping`, `preprocess`, `rank`,
|
||||
`complexity.map`. So the subtraction is clean: removing `rounds`, `acceptance`, `proposer`, `journal`, the
|
||||
noise machinery in `scoring`, and the mechanism loop cannot move a gate. What survives and re-homes: the
|
||||
taxonomy (`families.py`), the candidate enumerator (`candidates.py`, the `suspicion`-band feeder), the LLM
|
||||
@@ -140,7 +140,7 @@ src/openultrasast/model/
|
||||
- `learning/candidates.py` → `model/candidates.py` (the suspicion-band enumerator, Req 8.4).
|
||||
- `learning/endpoint.py` → `model/endpoint.py` (the judge client).
|
||||
- `semantic/obligations/` retained; its operation/discharge facts feed `model/dominance.py`.
|
||||
- `pairs.py`, the gates, redaction — unchanged.
|
||||
- `pairs.py`, the gates, redaction, unchanged.
|
||||
|
||||
### The audit manifest (Req 3)
|
||||
|
||||
@@ -288,21 +288,21 @@ is dominated by no discharger and its siblings are, else `CORROBORATED`/`None`.
|
||||
|
||||
## Migration and Phases
|
||||
|
||||
1. **Subtract and audit (Req 1–3).** Remove the noise architecture; port the guard/sink vocabularies into
|
||||
1. Subtract and audit (Req 1–3). Remove the noise architecture; port the guard/sink vocabularies into
|
||||
`model/specs.py` before deleting their modules; run the import-graph check and the gate-baseline; reduce
|
||||
the tests in the same change; commit the audit manifest. Gate-protected and reversible; no CPG yet.
|
||||
2. **Joern spike on one family (Req 4, 6.1).** The `CpgBackend` seam and the injection `TaintSpec`, proven
|
||||
2. Joern spike on one family (Req 4, 6.1). The `CpgBackend` seam and the injection `TaintSpec`, proven
|
||||
end to end on the injection slice: build a CPG, run the taint query, produce entailed/corroborated
|
||||
verdicts, assert determinism. This phase can stop the design if Joern cannot deliver a deterministic
|
||||
verdict at acceptable cost — the analogue of the candidate-ceiling gate.
|
||||
3. **The ladder and the judge (Req 5, 8).** `Verdict`/`Rung`, and `judge.py` checking the LLM's residual
|
||||
verdict at acceptable cost, the analogue of the candidate-ceiling gate.
|
||||
3. The ladder and the judge (Req 5, 8). `Verdict`/`Rung`, and `judge.py` checking the LLM's residual
|
||||
answer against the verdict. No K-runs, nothing to average.
|
||||
4. **Absence and the rest of the taxonomy (Req 6.2, 7).** Dominance queries from the obligation facts; the
|
||||
4. Absence and the rest of the taxonomy (Req 6.2, 7). Dominance queries from the obligation facts; the
|
||||
remaining `TaintSpec`s. Regenerate the entailment ceiling (Req 6.5) and watch it rise as depth improves.
|
||||
5. **Corpus calibration and reporting (Req 9, 11).** The vulnerable-parent/fix differential, the model-gap
|
||||
5. Corpus calibration and reporting (Req 9, 11). The vulnerable-parent/fix differential, the model-gap
|
||||
report, per-slice + overfitting gap.
|
||||
6. **Execution tier stub (Req 10).** The `execution_confirmed` rung and the Clearwing adoption seam, built
|
||||
only when a class needs it — memory/C first, and not for web/logic v1.
|
||||
6. Execution tier stub (Req 10). The `execution_confirmed` rung and the Clearwing adoption seam, built
|
||||
only when a class needs it, memory/C first, and not for web/logic v1.
|
||||
|
||||
Phase 1 removes ~5,000 lines before a line of Joern is written. Phase 2 can end the design before the
|
||||
integration cost is paid, exactly as the candidate ceiling could have in the prior spec.
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
This feature supersedes `learning-harness` and `constrained-detector`. It replaces the assumption that the LLM
|
||||
is a noisy oracle to be averaged, floored and gated with an architecture in which a **deterministic program
|
||||
model arbitrates the LLM's claims**. The arbiter is a Code Property Graph (CPG) — an intermediate language
|
||||
merging AST, control-flow graph, program-dependence graph and call graph — over which taint reachability and
|
||||
model arbitrates the LLM's claims**. The arbiter is a Code Property Graph (CPG), an intermediate language
|
||||
merging AST, control-flow graph, program-dependence graph and call graph, over which taint reachability and
|
||||
guard dominance produce verdicts the LLM's assertion is no longer needed for. The measurements that force this
|
||||
design are committed: the candidate ceiling (96.6% of labeled functions have an enumerable site), and the
|
||||
entailment ceiling (a deterministic model *decides* only 2.2% today on our flat AST-projection IR, ~24-35%
|
||||
with deeper taint — the distance to a real CPG). Context: `brief.md`,
|
||||
with deeper taint, the distance to a real CPG). Context: `brief.md`,
|
||||
`benchmarks/measurements/2026-09-06-trajectory-divergence.json`, `2026-09-06-candidate-ceiling-all-slices.json`,
|
||||
`2026-09-08-model-entailment-ceiling.json`.
|
||||
|
||||
@@ -22,15 +22,15 @@ overgrown, superseded surface is how this session logged eighteen defects.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: removing the noise architecture and the superseded arbiter; reducing the test suite to
|
||||
- In scope: removing the noise architecture and the superseded arbiter; reducing the test suite to
|
||||
surviving behavior; auditing the whole tree for orphaned modules; adopting a CPG engine (Joern) as the
|
||||
model-layer substrate behind an optional extra; the evidence ladder as the verdict type; taint + dominance
|
||||
verdicts; porting our taxonomy and source/sink/sanitizer/guard models to the CPG; the LLM as a proposer
|
||||
checked against the model; the corpus as the model's calibration set; per-slice reporting.
|
||||
- **Out of scope**: building a dataflow/CFG engine by hand (the CPG is adopted, §Req 4); the execution tier
|
||||
- Out of scope: building a dataflow/CFG engine by hand (the CPG is adopted, §Req 4); the execution tier
|
||||
itself (deferred to Clearwing, `execution_confirmed` rung only, Req 10); prompt optimisation, reflective
|
||||
evolution, any optimiser (deleted, not deferred); fine-tuning weights.
|
||||
- **North star, not v1**: pure `model_entailed`-as-arbiter. The entailment ceiling says the model decides a
|
||||
- North star, not v1: pure `model_entailed`-as-arbiter. The entailment ceiling says the model decides a
|
||||
minority today; v1 is the *layered* arbiter (entail where it can, corroborate the taint band, honest
|
||||
suspicion for the rest). This is stated so the requirements are not read as promising a soundness the
|
||||
measurement denies.
|
||||
@@ -45,12 +45,12 @@ so that nothing is maintained, tested, or built upon that this feature's measure
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall remove the round loop, the per-family noise floor, the regression budget, the acceptance
|
||||
rule, the sign test, the evolve journal and the meta-agent proposer — concretely `learning/rounds.py`,
|
||||
rule, the sign test, the evolve journal and the meta-agent proposer, concretely `learning/rounds.py`,
|
||||
`learning/acceptance.py`, `learning/proposer.py`, `learning/journal.py`, and the K-run / floor / sign-test
|
||||
machinery in `learning/scoring.py`.
|
||||
2. The system shall remove the mechanism-optimisation loop — the mechanism store and lever in
|
||||
2. The system shall remove the mechanism-optimisation loop (the mechanism store and lever in
|
||||
`improve/evolve.py`, and the shape-search machinery in `semantic/mechanisms.py` and the optimisation
|
||||
surface of `semantic/variants.py` — while preserving the *security models* those modules carried (the
|
||||
surface of `semantic/variants.py`), while preserving the *security models* those modules carried (the
|
||||
guard vocabulary and source/sink classification) for porting under Req 7.
|
||||
2b. The system shall remove the per-family evolvable detector configuration and its canary/verifier surface
|
||||
(`learning/detectors.py` config-evolution parts, `learning/canaries.py`, `learning/verifiers.py`,
|
||||
@@ -70,7 +70,7 @@ ratchet that pins deleted mechanisms and the suite is once again a description o
|
||||
|
||||
1. When a mechanism is removed under Req 1, the tests that existed only to pin its behavior shall be removed
|
||||
with it, in the same change, not left skipped or xfailed.
|
||||
2. Every surviving test shall map to a surviving requirement — of this spec or of a still-shipping feature —
|
||||
2. Every surviving test shall map to a surviving requirement (of this spec or of a still-shipping feature)
|
||||
and a test that maps to nothing shall be removed.
|
||||
3. The reduced suite shall be smaller than the current 832 tests, and the change shall report the count before
|
||||
and after with the requirement each removed test cluster served.
|
||||
@@ -78,7 +78,7 @@ ratchet that pins deleted mechanisms and the suite is once again a description o
|
||||
determinism test (Req 6.4), the per-slice honesty tests, and the two ceiling measurements' regeneration
|
||||
tests.
|
||||
5. A test that asserts a shape "does nothing" or greps `--help`, or that passes with its capability absent,
|
||||
shall be removed rather than kept — the session logged several; none survive.
|
||||
shall be removed rather than kept. The session logged several; none survive.
|
||||
|
||||
### Requirement 3: Audit the whole tree for outgrowth
|
||||
|
||||
@@ -89,7 +89,7 @@ found and removed rather than carried indefinitely.
|
||||
|
||||
1. The system shall classify every source module as one of: load-bearing for the model-grounded architecture;
|
||||
a retained standalone capability with its own users; or orphaned.
|
||||
2. If a module is orphaned — no importer, no CLI entry, no gate dependency, no surviving requirement — then the
|
||||
2. If a module is orphaned (no importer, no CLI entry, no gate dependency, no surviving requirement), then the
|
||||
system shall remove it and record the classification.
|
||||
3. The system shall name, for the audit, the older-phase subsystems whose status is not obvious
|
||||
(`fusion.py`, `mcp.py`, `skills.py`, `harness_ext.py`, `hunter_harness.py`) and give each an explicit
|
||||
@@ -122,7 +122,7 @@ model result is never confused with an LLM suspicion.
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall assign every finding one rung: `suspicion` (a site was enumerated / the LLM flagged it),
|
||||
`model_corroborated` (the LLM's claim is consistent with the CPG — the source it names reaches the sink
|
||||
`model_corroborated` (the LLM's claim is consistent with the CPG, the source it names reaches the sink
|
||||
through resolved dataflow, or a guard its siblings carry is absent), `model_entailed` (the CPG alone
|
||||
establishes it, independent of trusting the LLM), or `execution_confirmed` (a sandbox reproduced it).
|
||||
2. The system shall establish `model_corroborated` and `model_entailed` from the CPG deterministically, with
|
||||
@@ -145,21 +145,21 @@ project measured is removed on the band the model covers.
|
||||
4. When the system runs the model layer twice over the same CPG, the sequence of verdicts shall be identical,
|
||||
and a test shall assert it.
|
||||
5. The system shall report, per family and per slice, the fraction of the corpus it can `model_entail` and
|
||||
`model_corroborate` — the regeneration of the committed entailment ceiling — and shall name where it falls
|
||||
`model_corroborate` (the regeneration of the committed entailment ceiling), and shall name where it falls
|
||||
back to `suspicion`.
|
||||
|
||||
### Requirement 7: Port the taxonomy and security models onto the CPG
|
||||
|
||||
**Objective:** As a maintainer, I want our security knowledge — the classes we target and how their bugs look
|
||||
— expressed as queries over the adopted CPG, so that the contribution is the modelling, not the engine.
|
||||
**Objective:** As a maintainer, I want our security knowledge (the classes we target and how their bugs look)
|
||||
expressed as queries over the adopted CPG, so that the contribution is the modelling, not the engine.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall retain the closed family taxonomy as the routing key, unchanged in meaning.
|
||||
2. The system shall express the source, sink and sanitizer models for the web/logic CWEs as CPG taint
|
||||
specifications, seeded from the models the deleted modules carried.
|
||||
3. The system shall express the obligation / absence model — an operation on a protected resource, and the
|
||||
guards that discharge it — as CPG dominance queries, as the arbiter for the classes that never crash.
|
||||
3. The system shall express the obligation / absence model (an operation on a protected resource, and the
|
||||
guards that discharge it) as CPG dominance queries, as the arbiter for the classes that never crash.
|
||||
4. The system shall keep the verifier boundary: the models and gold labels stay outside anything an LLM or a
|
||||
future optimiser may write.
|
||||
|
||||
@@ -177,7 +177,7 @@ with its claim checked against the model, so that there is no oracle to average.
|
||||
`model_corroborated`.
|
||||
4. When the model can neither confirm nor contradict, the system shall report the claim at `suspicion`, and
|
||||
this band is where a future execution tier earns its place.
|
||||
5. The system shall contain no K-run averaging, no noise floor, no acceptance rule, no evolve loop — the model
|
||||
5. The system shall contain no K-run averaging, no noise floor, no acceptance rule, no evolve loop. The model
|
||||
check replaces all of them.
|
||||
|
||||
### Requirement 9: The corpus is the model's calibration set
|
||||
@@ -188,7 +188,7 @@ vote, so that a gap is a gap in the model, named and fixable.
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall use the vulnerable-parent / fix-commit pair as ground truth for whether the model can
|
||||
distinguish the two — the fix is what the model should be able to see.
|
||||
distinguish the two. The fix is what the model should be able to see.
|
||||
2. If the model cannot distinguish a pair's two sides, then the system shall record it as a named model gap
|
||||
with the reason, not as a detector miss to be averaged away.
|
||||
3. The system shall keep the per-slice split with the real-world slices deciding, and shall not present the
|
||||
@@ -204,7 +204,7 @@ arbitrate, without building or requiring it at v1.
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. The system shall treat `execution_confirmed` as an optional rung reached only for classes where the model
|
||||
layer cannot decide — memory-safety in C first.
|
||||
layer cannot decide, memory-safety in C first.
|
||||
2. When execution confirmation is used, the system shall adopt it from Clearwing (the existing fork), not
|
||||
reimplement the sandbox lifecycle, container pooling, sanitizer images or PoC stability.
|
||||
3. The system shall not require the execution tier for any web/logic class at v1.
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
# Assessment: OpenUltraSAST Kickoff
|
||||
|
||||
## Executive Assessment
|
||||
|
||||
OpenUltraSAST should be built as a harness runtime specialized for security analysis, not as a direct Clearwing clone. The strongest design is to treat Clearwing's sourcehunt pipeline as a proven reference flow, HarnessX as the runtime abstraction, OpenUltraCode as the verification and fusion discipline, OpenRouter as the provider plane, and Trail of Bits skills as targeted domain expertise.
|
||||
|
||||
The minimum viable system is not deep fuzzing. The minimum viable system is an auditable quick scan that can rank files, generate structured findings, eliminate obvious false positives, enforce evidence levels, and emit reports. Deep Docker-backed validation should come after the evidence model, trace model, false-positive loop, and report model are stable.
|
||||
|
||||
## Why This Project Is Viable
|
||||
|
||||
Clearwing demonstrates that a SAST + LLM hybrid can be useful when it avoids treating model output as truth. Its best transferable ideas are:
|
||||
|
||||
- Static preprocessing before LLM analysis.
|
||||
- File ranking by surface, influence, and reachability.
|
||||
- Tiered hunter budgets instead of equal attention for every file.
|
||||
- Independent adversarial verification.
|
||||
- Evidence ladder that separates suspicion from crash reproduction and patch validation.
|
||||
- Docker isolation for builds, fuzzing, PoCs, and patches.
|
||||
- SARIF, Markdown, and JSON artifacts.
|
||||
|
||||
The most important product differentiator should be false-positive elimination. A scanner that finds many plausible issues but cannot separate evidence-backed findings from model hallucinations will not be trusted. Ranking, retrieval, verification, and HarnessX-style adjustment should all be optimized around reducing false positives without hiding the audit trail.
|
||||
|
||||
HarnessX provides the missing generalization: the scanner should be a composed harness with typed processors and traces, not a fixed pipeline that becomes hard to evolve. This matters because SAST workflows vary heavily by language, build system, vulnerability class, and available evidence.
|
||||
|
||||
OpenUltraCode provides the missing discipline: high-risk findings and proposed fixes need bounded plans, adversarial review, reconciliation, explicit model disclosure, and verification gates. This is directly applicable to false-positive control and to avoiding fixes that look plausible but introduce regressions.
|
||||
|
||||
Trail of Bits skills provide mapping discipline as much as they provide tools. CodeQL thinking maps interprocedural flows, Semgrep maps repeatable patterns and variants, differential review maps changed attack surface, and sharp-edge review maps APIs and defaults that make misuse likely. OpenUltraSAST should convert those disciplines into structured tasks and evidence, not only prompt inspiration.
|
||||
|
||||
## Recommended Initial Product Shape
|
||||
|
||||
The first release should support this workflow:
|
||||
|
||||
```text
|
||||
ousast scan /path/to/repo --mode quick
|
||||
```
|
||||
|
||||
The first realistic quality milestone should add benchmark-grounded evaluation before claiming language coverage:
|
||||
|
||||
```text
|
||||
ousast benchmark benchmarks/javascript-node.toml --mode standard
|
||||
ousast benchmark benchmarks/python-web.toml --mode standard
|
||||
ousast benchmark benchmarks/java-web.toml --mode standard
|
||||
ousast benchmark benchmarks/c-cpp.toml --mode quick
|
||||
```
|
||||
|
||||
It should produce:
|
||||
|
||||
```text
|
||||
.openultrasast/runs/<scan-id>/manifest.json
|
||||
.openultrasast/runs/<scan-id>/preprocess/file_targets.json
|
||||
.openultrasast/runs/<scan-id>/rank/ranking.json
|
||||
.openultrasast/runs/<scan-id>/findings.json
|
||||
.openultrasast/runs/<scan-id>/report.md
|
||||
```
|
||||
|
||||
The key product promise should be:
|
||||
|
||||
```text
|
||||
OpenUltraSAST tells you where security review should start, what it suspects, what evidence exists, what evidence is missing, and which findings have survived independent verification.
|
||||
```
|
||||
|
||||
For coder education, the complementary promise should be:
|
||||
|
||||
```text
|
||||
OpenUltraSAST explains why the pattern is dangerous, what evidence proves or weakens the finding, how to fix it safely, and how to avoid recreating the same bug class.
|
||||
```
|
||||
|
||||
## Core Architecture Recommendation
|
||||
|
||||
Use a Python implementation initially. Clearwing and HarnessX are both Python, Docker and SARIF libraries are mature, and Trail of Bits tooling integration will be simpler. OpenCode integration can happen through MCP and command files rather than requiring the core scanner to be TypeScript.
|
||||
|
||||
Recommended modules:
|
||||
|
||||
```text
|
||||
openultrasast/config.py
|
||||
openultrasast/cli.py
|
||||
openultrasast/models.py
|
||||
openultrasast/provider/openrouter.py
|
||||
openultrasast/preprocess/
|
||||
openultrasast/index/
|
||||
openultrasast/rank/
|
||||
openultrasast/harness/
|
||||
openultrasast/hunters/
|
||||
openultrasast/verify/
|
||||
openultrasast/fusion/
|
||||
openultrasast/sandbox/
|
||||
openultrasast/reports/
|
||||
openultrasast/mcp/
|
||||
```
|
||||
|
||||
## Build vs. Reuse Decisions
|
||||
|
||||
| Area | Recommendation | Reason |
|
||||
| --- | --- | --- |
|
||||
| Harness runtime | Build minimal HarnessX-inspired core first | Direct HarnessX dependency may add unstable surface before needs are proven |
|
||||
| OpenRouter chat | Build direct OpenAI-compatible client | Need explicit model IDs, cost metadata, and role controls |
|
||||
| OpenRouter embeddings | Build direct client | Embeddings are central and should not be hidden behind chat abstractions |
|
||||
| Vector store | Choose one local default | Avoid abstracting before retrieval needs are proven |
|
||||
| Docker sandbox | Build focused wrapper | Security defaults are project-specific |
|
||||
| Trail of Bits skills | Index descriptors, mapping tasks, and snippets | Avoid prompt bloat while preserving CodeQL/Semgrep/differential/sharp-edge discipline |
|
||||
| Clearwing | Reference only | Independence is a project requirement |
|
||||
|
||||
## Configuration Shape
|
||||
|
||||
Model names and the vector store should be selected before implementation. The config shape should look like this:
|
||||
|
||||
```toml
|
||||
[models]
|
||||
ranker = "<openrouter-ranker-model>"
|
||||
hunter = "<openrouter-hunter-model>"
|
||||
verifier = "<openrouter-verifier-model>"
|
||||
patcher = "<openrouter-patcher-model>"
|
||||
|
||||
[embeddings]
|
||||
model = "<openrouter-embedding-model>"
|
||||
store = "<selected-local-vector-store>"
|
||||
|
||||
[sandbox]
|
||||
network = false
|
||||
workspace_readonly = true
|
||||
memory_mb = 2048
|
||||
timeout_seconds = 300
|
||||
pids_limit = 512
|
||||
|
||||
[dynamic]
|
||||
enabled = false
|
||||
network_scope = []
|
||||
|
||||
[evidence]
|
||||
minimum_report_verified = "static_corroboration"
|
||||
minimum_exploit = "crash_reproduced"
|
||||
minimum_patch = "root_cause_explained"
|
||||
```
|
||||
|
||||
The actual model names should be explicit before implementation. Avoid baking in placeholder defaults that silently pick a moving target.
|
||||
|
||||
## Vector Store Decision
|
||||
|
||||
Choosing the vector store is a core design decision, not plumbing. It directly affects ranking quality and false-positive rates because poor retrieval contaminates hunter and verifier context.
|
||||
|
||||
Selection criteria:
|
||||
|
||||
- Strong metadata filters for path, language, symbol, repository snapshot, evidence level, and vulnerability class.
|
||||
- Incremental indexing for changed files.
|
||||
- Local persistence that works inside opencode workflows.
|
||||
- Deterministic export/import for reproducible scan artifacts.
|
||||
- Acceptable query latency on medium repositories.
|
||||
- Clean storage of OpenRouter embedding model metadata.
|
||||
- Ability to combine vector retrieval with lexical or structural filters.
|
||||
|
||||
The first milestone should include a short bakeoff on fixtures before locking the default store.
|
||||
|
||||
## Most Important Engineering Constraint
|
||||
|
||||
Evidence transitions must be enforced in code. A model can recommend a transition, but the harness should decide whether the required artifact exists.
|
||||
|
||||
Examples:
|
||||
|
||||
- `suspicion -> static_corroboration`: requires static evidence, independent verifier agreement, or a matching analyzer finding.
|
||||
- `static_corroboration -> crash_reproduced`: requires stored crash/reproducer artifact.
|
||||
- `crash_reproduced -> root_cause_explained`: requires verifier explanation tying crash to source and impact.
|
||||
- `root_cause_explained -> exploit_demonstrated`: requires exploit or trigger artifact beyond accidental crash.
|
||||
- `root_cause_explained -> patch_validated`: requires patch diff and passing validation command.
|
||||
|
||||
False-positive elimination should be handled the same way: a model can recommend demotion or suppression, but the harness should require a scoped reason and counter-evidence.
|
||||
|
||||
Benchmark grounding should be handled with the same discipline. A scanner that passes tiny fixtures but misses NodeGoat, PyGoat, OWASP Benchmark/WebGoat, Juliet, or small sanitizer-backed parser targets is not a realistic finder. Language support should be claimed only after benchmark runs show useful recall and auditable false-positive behavior on realistic vulnerable projects.
|
||||
|
||||
False-positive reasons should include:
|
||||
|
||||
- No attacker-controlled path.
|
||||
- Code is unreachable in supported configuration.
|
||||
- Static rule matched a safe wrapper or sanitizer.
|
||||
- Model confused similar symbols or files.
|
||||
- Finding duplicates an existing issue.
|
||||
- Impact is not security-relevant.
|
||||
- Dynamic probe or test disproved triggerability.
|
||||
|
||||
## OpenRouter Design Needs
|
||||
|
||||
OpenRouter should be treated as two capabilities:
|
||||
|
||||
1. Chat/completion routing for reasoning roles.
|
||||
2. Embedding endpoint for retrieval.
|
||||
|
||||
Every model call should record:
|
||||
|
||||
```text
|
||||
role
|
||||
model_id
|
||||
provider metadata
|
||||
prompt hash
|
||||
response hash
|
||||
cost estimate
|
||||
temperature and parameters
|
||||
fallback/degradation status
|
||||
```
|
||||
|
||||
The project should reject ambiguous model routing for reproducible scans. If a role uses auto routing, the manifest must record the actual model selected by OpenRouter if available.
|
||||
|
||||
## Docker Design Needs
|
||||
|
||||
Docker is not just convenience. It is the trust boundary between arbitrary target repositories and the user's host.
|
||||
|
||||
Minimum safety profile:
|
||||
|
||||
- No hidden network in analysis containers.
|
||||
- Read-only source mount.
|
||||
- Writable scratch only.
|
||||
- Timeouts around every command.
|
||||
- Memory, CPU, and pids limits.
|
||||
- Dropped capabilities by default.
|
||||
- Separate writable copy for patch validation.
|
||||
|
||||
Deep mode should not be implemented until this boundary exists.
|
||||
|
||||
## Dynamic Analysis Strategy
|
||||
|
||||
Network verification is not out of scope. It is just not the core idea of the harness. OpenUltraSAST should remain SAST-first, but it should use dynamic analysis when runtime evidence is the right way to confirm a finding.
|
||||
|
||||
Useful dynamic checks include:
|
||||
|
||||
- Starting a target service in a sandbox.
|
||||
- Using netcat-style TCP probes against declared local ports.
|
||||
- Running local HTTP or protocol smoke tests.
|
||||
- Executing a bounded PoC against a sandboxed service.
|
||||
- Using Clearwing-style network checks as validation patterns when a source finding has a runtime network surface.
|
||||
|
||||
The rule is not "no network ever." The rule is "no hidden or open-ended network activity." Dynamic probes must be explicit, scoped, logged, time-bounded, and tied to evidence advancement.
|
||||
|
||||
## Trail Of Bits Skill Strategy
|
||||
|
||||
The skills are best used as a routing corpus and checklist library. The harness should select compact snippets by target profile.
|
||||
|
||||
Good examples:
|
||||
|
||||
- C parser with pointer arithmetic: `c-review`, `address-sanitizer`, `libfuzzer`, `harness-writing`.
|
||||
- Python parser: `atheris`, `property-based-testing`.
|
||||
- Crypto code: `constant-time-analysis`, `wycheproof`, `vector-forge`, `zeroize-audit`.
|
||||
- SARIF flow: `sarif-parsing`, `semgrep`, `codeql`.
|
||||
- Changed code or proposed fixes: `differential-review`, `graph-evolution`.
|
||||
- Dangerous APIs, defaults, or configuration: `sharp-edges`, `insecure-defaults`.
|
||||
- Smart contracts: use chain-specific scanners only when the language and framework match.
|
||||
|
||||
Bad pattern:
|
||||
|
||||
```text
|
||||
Paste all security skill instructions into every hunter prompt.
|
||||
```
|
||||
|
||||
That would increase cost, reduce precision, and mix irrelevant assumptions into reviews.
|
||||
|
||||
## Fix Authoring And Audit Strategy
|
||||
|
||||
UltraCode should define the patch lifecycle. A patch is not complete when a model emits a diff. A patch is complete only after a bounded plan, minimal implementation, adversarial review, reconciliation, and fresh verification.
|
||||
|
||||
Fix stages:
|
||||
|
||||
```text
|
||||
verified finding -> bounded fix plan -> minimal patch -> sandbox validation -> adversarial fix audit -> reconciliation -> fresh verification -> patch artifact
|
||||
```
|
||||
|
||||
Trail of Bits disciplines should audit the fix:
|
||||
|
||||
- Differential review checks whether the patch changed more attack surface than intended.
|
||||
- Semgrep checks whether the vulnerable pattern or variants remain.
|
||||
- CodeQL checks whether taint/dataflow still reaches the sink when applicable.
|
||||
- Sharp-edge review checks whether the fix leaves a misleading or misuse-prone API behind.
|
||||
|
||||
Fusion should always be used to go deeper when the task requires it: risky fixes, contradictory evidence, difficult remediation tradeoffs, high-impact reports, unresolved verifier disagreement, or explicit high-assurance review. It should not be spent on routine low-risk patches when ordinary evidence is already sufficient.
|
||||
|
||||
## Kiro Spec Completion Criteria
|
||||
|
||||
The current spec is ready to drive implementation when these decisions are made:
|
||||
|
||||
1. Language/runtime: Python is recommended.
|
||||
2. Vector store: choose Chroma, LanceDB, Qdrant local, or SQLite vector extension.
|
||||
3. Initial OpenRouter chat models by role.
|
||||
4. Initial OpenRouter embedding model.
|
||||
5. First fixture repository or synthetic vulnerable examples.
|
||||
6. Whether to depend directly on HarnessX or implement a minimal compatible harness core.
|
||||
|
||||
## Recommended MVP Cut
|
||||
|
||||
MVP should include:
|
||||
|
||||
- Local path scan.
|
||||
- File enumeration and static tags.
|
||||
- OpenRouter ranker.
|
||||
- Quick-mode hunter.
|
||||
- Evidence ladder with enforced transitions.
|
||||
- Independent verifier.
|
||||
- JSON and Markdown reports.
|
||||
- Manifest with model IDs and prompt hashes.
|
||||
|
||||
MVP should exclude:
|
||||
|
||||
- Deep fuzzing and broad dynamic analysis.
|
||||
- Auto-patching.
|
||||
- Fusion panels for routine MVP findings.
|
||||
- Full MCP server.
|
||||
- Multi-repository campaigns.
|
||||
- Training bridge.
|
||||
|
||||
MVP should still model the future fix lifecycle in the data model, because evidence, reports, and patch artifacts need stable IDs before patching exists.
|
||||
|
||||
These exclusions keep the first milestone testable and prevent architecture from hardening around unverified assumptions.
|
||||
|
||||
## Implementation Order Rationale
|
||||
|
||||
Do not start with Docker or broad fusion. Those are expensive and only pay off after the data model and evidence model are correct. Start with the smallest useful loop:
|
||||
|
||||
```text
|
||||
preprocess -> rank -> quick finding -> verify -> report
|
||||
```
|
||||
|
||||
Once this loop works on fixtures, add embeddings. Once retrieval is stable, add sandboxed commands. Once sandboxing is stable, add deep fuzzing. Add fusion as soon as tasks require deeper adjudication than the normal verifier can provide.
|
||||
|
||||
## Immediate Next Questions
|
||||
|
||||
1. Which OpenRouter chat models should be used for ranker, hunter, verifier, and patcher roles?
|
||||
2. Which OpenRouter embedding model should be the default?
|
||||
3. Which vector store should be selected for the first implementation?
|
||||
4. Should the project start with synthetic fixtures or scan a real open-source target first?
|
||||
5. Should the first implementation include MCP, or should MCP wait until the CLI and artifacts stabilize?
|
||||
@@ -1,29 +0,0 @@
|
||||
# Decisions: OpenRouter SAST Harness
|
||||
|
||||
## Approved Initial Slice
|
||||
|
||||
The first implementation slice is approved for Phase 0 and Phase 1 only:
|
||||
|
||||
- Python package skeleton with `ousast` CLI.
|
||||
- Local configuration loading.
|
||||
- Scan run directory creation under `.openultrasast/runs/<scan-id>/`.
|
||||
- Repository snapshot collection.
|
||||
- Source file enumeration with basic ignore handling.
|
||||
- Language, LOC, and static tag heuristics.
|
||||
- `preprocess/file_targets.json` artifact.
|
||||
|
||||
## Current Decisions
|
||||
|
||||
| Decision | Value | Scope |
|
||||
| --- | --- | --- |
|
||||
| Runtime | Python 3.11+ | Initial implementation |
|
||||
| Harness dependency | Minimal HarnessX-compatible core first | Avoid direct dependency until contracts are proven |
|
||||
| Vector store | Deferred to Phase 4 bakeoff | Not needed for Phase 0/1 |
|
||||
| OpenRouter chat models | Deferred until ranker implementation | Not needed for Phase 0/1 |
|
||||
| OpenRouter embedding model | Deferred until embeddings implementation | Not needed for Phase 0/1 |
|
||||
| Fixtures | Synthetic fixture repositories first | Keeps tests deterministic |
|
||||
| MCP | Defer until CLI and artifacts stabilize | Phase 12 |
|
||||
|
||||
## Rationale
|
||||
|
||||
The first milestone should prove the local artifact contract before adding model calls, embeddings, Docker, MCP, or fusion. Later decisions must be recorded here before their implementation phases begin.
|
||||
@@ -1,762 +0,0 @@
|
||||
# Design: OpenRouter SAST Harness
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
OpenUltraSAST is a harness-oriented SAST system, not a monolithic scanner. The core runtime composes processors, model roles, retrieval, tools, sandbox policy, traces, verification gates, and reporting into a reproducible scan harness.
|
||||
|
||||
The central quality objective is false-positive elimination. The harness should not merely find more possible bugs; it should repeatedly narrow suspicion into evidence-backed findings and learn from rejected claims.
|
||||
|
||||
Conceptual lineage:
|
||||
|
||||
- HarnessX: harness primitives, event hooks, processor contracts, trace-driven evolution.
|
||||
- OpenUltraCode: bounded context, fix authoring discipline, adversarial review, reconciliation, fusion, verification evidence.
|
||||
- Clearwing sourcehunt: preprocess, rank, tiered hunters, evidence ladder, adversarial verifier, patch oracle, variant loop, reports.
|
||||
- Trail of Bits skills: domain-specific mapping, analysis, audit, and verification methods selected at the right stage, not embedded globally.
|
||||
|
||||
## High-Level Flow
|
||||
|
||||
```text
|
||||
repo path
|
||||
-> intake snapshot
|
||||
-> preprocess and static tags
|
||||
-> graph, SARIF ingest, and entry-point reachability mapping
|
||||
-> embeddings and retrieval index
|
||||
-> file/function ranking
|
||||
-> tiered hunter pool
|
||||
-> independent verifier
|
||||
-> optional fusion adjudication
|
||||
-> optional explain and guided fix workflow
|
||||
-> optional reproducer/exploit triage
|
||||
-> optional patch oracle
|
||||
-> reports and artifacts
|
||||
```
|
||||
|
||||
Benchmark runs wrap the same scan pipeline instead of using a separate detector path:
|
||||
|
||||
```text
|
||||
benchmark manifest
|
||||
-> acquire or locate vulnerable project
|
||||
-> run quick, standard, or deep scan
|
||||
-> normalize OpenUltraSAST findings and optional external baselines
|
||||
-> match expected vulnerabilities by CWE, file, function, sink, and evidence
|
||||
-> record recall, precision where possible, misses, false-positive reasons, and runtime
|
||||
-> feed misses and rejected findings into calibration artifacts
|
||||
```
|
||||
|
||||
## Main Components
|
||||
|
||||
### CLI
|
||||
|
||||
Initial command surface:
|
||||
|
||||
```text
|
||||
ousast scan <path> --mode quick|standard|deep --config openultrasast.toml
|
||||
ousast benchmark <manifest> --mode quick|standard|deep
|
||||
ousast explain <finding-id> --scan <scan-id> --audience concise|learner|reviewer
|
||||
ousast status <scan-id>
|
||||
ousast findings <scan-id>
|
||||
ousast report <scan-id> --format markdown|json|sarif
|
||||
ousast patch <finding-id> --propose
|
||||
ousast index <path> --rebuild
|
||||
```
|
||||
|
||||
The CLI should be thin. It loads config, creates a scan, starts the harness runtime, and writes artifacts.
|
||||
|
||||
### MCP Server
|
||||
|
||||
MCP should expose only stable project operations:
|
||||
|
||||
```text
|
||||
openultrasast.scan
|
||||
openultrasast.status
|
||||
openultrasast.findings
|
||||
openultrasast.get_finding
|
||||
openultrasast.evidence
|
||||
openultrasast.artifacts
|
||||
openultrasast.benchmark
|
||||
openultrasast.explain
|
||||
openultrasast.propose_patch
|
||||
openultrasast.export_report
|
||||
```
|
||||
|
||||
The MCP server must not expose arbitrary shell, Docker, or internal hunter tools. Those stay inside controlled harness stages.
|
||||
|
||||
### Harness Runtime
|
||||
|
||||
The runtime owns the scan lifecycle and event pipeline. It should follow the HarnessX separation:
|
||||
|
||||
```text
|
||||
ModelConfig: role -> OpenRouter model, parameters, budget, fallback policy
|
||||
HarnessConfig: processors, tools, memory, sandbox, trace, control policy
|
||||
ScanConfig: target path, mode, scope, excludes, report options
|
||||
```
|
||||
|
||||
Required event hooks:
|
||||
|
||||
```text
|
||||
task_start
|
||||
stage_start
|
||||
before_model
|
||||
after_model
|
||||
before_tool
|
||||
after_tool
|
||||
stage_end
|
||||
task_end
|
||||
```
|
||||
|
||||
Each processor declares the event types it handles and the state fields it reads or writes. Strict contract mode fails the scan when processors violate schemas. Warn mode records degraded state.
|
||||
|
||||
### OpenRouter Provider
|
||||
|
||||
The provider layer should implement two explicit clients:
|
||||
|
||||
```text
|
||||
OpenRouterChatClient
|
||||
OpenRouterEmbeddingClient
|
||||
```
|
||||
|
||||
Chat roles:
|
||||
|
||||
```text
|
||||
ranker
|
||||
hunter
|
||||
verifier
|
||||
fusion_panel_a
|
||||
fusion_panel_b
|
||||
fusion_decider
|
||||
patcher
|
||||
reporter
|
||||
```
|
||||
|
||||
Provider rules:
|
||||
|
||||
- No silent model substitution.
|
||||
- Every model response records model ID, provider metadata when available, token counts, cost estimate, prompt hash, and response hash.
|
||||
- Fallbacks must be configured per role and reported as degradations.
|
||||
- Embedding model ID must be stored in the vector index metadata.
|
||||
|
||||
### Retrieval Store
|
||||
|
||||
The retrieval layer maintains separate namespaces:
|
||||
|
||||
```text
|
||||
repo_code
|
||||
repo_docs
|
||||
static_findings
|
||||
mechanisms
|
||||
trailofbits_skills
|
||||
scan_traces
|
||||
false_positive_learnings
|
||||
```
|
||||
|
||||
Chunk metadata should include:
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_id": "...",
|
||||
"snapshot": "...",
|
||||
"path": "src/parser.c",
|
||||
"symbol": "parse_frame",
|
||||
"language": "c",
|
||||
"chunk_type": "function",
|
||||
"start_line": 120,
|
||||
"end_line": 220,
|
||||
"tags": ["parser", "memory_unsafe"],
|
||||
"embedding_model": "openrouter/..."
|
||||
}
|
||||
```
|
||||
|
||||
Retrieval packages must be bounded by role. A verifier gets the finding, relevant source, static corroboration, and counter-evidence candidates. It does not get hunter chain-of-thought.
|
||||
|
||||
#### Vector Store Selection Criteria
|
||||
|
||||
The vector store is a core ranking and false-positive-control dependency. The first implementation should choose one default after evaluating:
|
||||
|
||||
```text
|
||||
metadata filtering precision
|
||||
incremental reindexing by repository snapshot
|
||||
local persistence and portability
|
||||
query latency on medium repositories
|
||||
embedding model metadata support
|
||||
hybrid lexical/vector retrieval support or integration path
|
||||
deterministic export/import for scan reproducibility
|
||||
operational simplicity for opencode users
|
||||
```
|
||||
|
||||
Ranking depends on retrieval quality. Poor metadata filters will contaminate verifier context and increase false positives. The store must make it easy to retrieve "same symbol, same package, same vulnerability class, same evidence level" rather than semantically similar but irrelevant code.
|
||||
|
||||
### Preprocessor
|
||||
|
||||
The preprocessor produces `FileTarget` records. It should start simple and grow by plugins.
|
||||
|
||||
Initial fields:
|
||||
|
||||
```text
|
||||
path
|
||||
absolute_path
|
||||
language
|
||||
loc
|
||||
tags
|
||||
imports_by
|
||||
transitive_callers
|
||||
static_hints
|
||||
sarif_hints
|
||||
reachability_hints
|
||||
has_fuzz_entry_point
|
||||
defines_security_constants
|
||||
```
|
||||
|
||||
Initial tags:
|
||||
|
||||
```text
|
||||
memory_unsafe
|
||||
parser
|
||||
crypto
|
||||
deserialization
|
||||
syscall_entry
|
||||
network_entry
|
||||
filesystem_entry
|
||||
fuzzable
|
||||
privileged
|
||||
```
|
||||
|
||||
### Static Analysis Mapping Layer
|
||||
|
||||
The static analysis layer turns tool output and skill discipline into a map for the harness. It is not only a source of finished findings.
|
||||
|
||||
Primary mapping sources:
|
||||
|
||||
```text
|
||||
Semgrep: syntactic and lightweight semantic patterns, custom rule iteration, variant seeds
|
||||
CodeQL: interprocedural dataflow, taint paths, structural vulnerability queries
|
||||
Entry-point analysis: routes, CLI/process boundaries, parser inputs, smart-contract state-changing entry points, callbacks, privileged surfaces
|
||||
Differential review: changed attack surfaces, regression-prone patches, new trust-boundary crossings
|
||||
Sharp-edge review: unsafe APIs, insecure defaults, confusing configuration, misuse-prone interfaces
|
||||
SARIF parsing: normalized evidence, rule metadata, locations, severity, fingerprints
|
||||
```
|
||||
|
||||
Mapping outputs:
|
||||
|
||||
```text
|
||||
static_hints on FileTarget
|
||||
sarif_hints on FileTarget
|
||||
taint_paths and source/sink pairs
|
||||
variant_search_seeds
|
||||
changed_surface records
|
||||
sharp_edge records
|
||||
entry_point records
|
||||
reachability_hints on FileTarget
|
||||
evidence candidates for verifier
|
||||
```
|
||||
|
||||
This layer should use Trail of Bits skills as task templates and checklists. For example, the CodeQL discipline supplies query/dataflow thinking, Semgrep supplies pattern and variant loops, entry-point analysis supplies attacker-controlled surface and access classification, differential review supplies changed-code blast-radius analysis, and sharp-edge review supplies API misuse mapping.
|
||||
|
||||
### Benchmark Layer
|
||||
|
||||
The benchmark layer is the quality control system for a realistic grounded finder. It prevents the scanner from passing narrow fixtures while missing obvious bugs in vulnerable projects.
|
||||
|
||||
Benchmark manifest shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "nodegoat",
|
||||
"language": "javascript",
|
||||
"frameworks": ["node", "express"],
|
||||
"source": {"type": "local", "path": "../benchmarks/nodegoat"},
|
||||
"setup": {"commands": [], "requires_network": false},
|
||||
"modes": ["quick", "standard"],
|
||||
"expected": [
|
||||
{
|
||||
"cwe": "CWE-89",
|
||||
"class": "sql_injection",
|
||||
"path": "app/routes/*.js",
|
||||
"evidence": "user input reaches database query"
|
||||
}
|
||||
],
|
||||
"known_noise": [
|
||||
{"reason": "training-only route", "scope": "docs/**"}
|
||||
],
|
||||
"baselines": ["semgrep", "codeql", "clearwing"]
|
||||
}
|
||||
```
|
||||
|
||||
Initial benchmark families:
|
||||
|
||||
```text
|
||||
javascript_node_web: NodeGoat, DVNA, OWASP Juice Shop slices, vulnerable Express fixtures
|
||||
python_web: PyGoat, vulnerable Flask/Django apps, pickle/deserialization and command-injection fixtures
|
||||
java_web: OWASP Benchmark, WebGoat, Juliet Java, Spring MVC injection/auth fixtures
|
||||
c_cpp: Juliet C/C++, Damn Vulnerable C/C++ Program, vulnerable parsers, ASan/UBSan crash fixtures
|
||||
```
|
||||
|
||||
Benchmark metrics:
|
||||
|
||||
```text
|
||||
expected_findings_total
|
||||
matched_findings_total
|
||||
missed_findings_total
|
||||
unsupported_classes
|
||||
false_positive_total
|
||||
false_positive_reasons
|
||||
recall_by_cwe
|
||||
recall_by_language
|
||||
precision_when_ground_truth_allows
|
||||
runtime_seconds
|
||||
model_calls_and_cost
|
||||
external_baseline_delta
|
||||
```
|
||||
|
||||
Benchmark misses are first-class calibration inputs. A miss should record the source location, expected CWE, which stage failed to surface it, whether an external baseline found it, and the next candidate improvement: static rule, SARIF source, entry-point mapping, CodeQL/Semgrep discipline, LLM hunter prompt, retrieval package, dynamic reproducer, or skill routing.
|
||||
|
||||
#### Entry-Point Reachability Mapping
|
||||
|
||||
Entry-point mapping is a Phase 5A prerequisite for useful ranking and false-positive reduction. Pattern matches and static analyzer alerts are not enough; the harness must identify whether the affected function or symbol is plausibly reachable from attacker-controlled input.
|
||||
|
||||
Initial entry-point kinds:
|
||||
|
||||
```text
|
||||
route: HTTP/framework route handlers and request callbacks
|
||||
cli: process entry points, argparse/click handlers, argv consumers, main functions
|
||||
parser: parser or deserialization inputs that consume untrusted data
|
||||
fuzz: explicit fuzz entry points such as LLVMFuzzerTestOneInput
|
||||
smart_contract_state_change: externally callable non-view contract functions
|
||||
contract_callback: callbacks and hook implementations invoked by other contracts
|
||||
privileged_surface: auth boundaries, admin operations, filesystem/process/syscall boundaries
|
||||
```
|
||||
|
||||
Initial access levels:
|
||||
|
||||
```text
|
||||
public
|
||||
authenticated
|
||||
role-restricted
|
||||
contract-only/callback
|
||||
local-only
|
||||
review-required
|
||||
```
|
||||
|
||||
Each entry-point record should include path, function name when known, start line, end line, name, kind, access level, trust boundary, access evidence, condition evidence, provenance, and rationale. Access level distinguishes unauthenticated/public from authenticated, role-restricted, local-only, callback-only, and review-required surfaces. Condition evidence captures feature flags, rollout toggles, pause guards, experiment gates, and runtime guard expressions. The preprocessor attaches matching records to `FileTarget.reachability_hints`. Findings use the source line to inherit only function-level entry points whose range contains the finding. The ranker uses these hints to raise file review priority for files containing public routes, parser/fuzz inputs, authenticated surfaces, contract callbacks, and role-restricted privileged surfaces. A finding outside a reachable function should remain eligible for review, but should not inherit the entry point's priority without call-graph evidence.
|
||||
|
||||
### Ranker
|
||||
|
||||
Ranking should combine heuristics, graph features, static hints, and model judgment.
|
||||
|
||||
Scores:
|
||||
|
||||
```text
|
||||
surface: direct vulnerability likelihood, 1-5
|
||||
influence: downstream blast radius, 1-5
|
||||
reachability: attacker-controlled path likelihood, 1-5
|
||||
priority: surface * 0.5 + influence * 0.2 + reachability * 0.3
|
||||
```
|
||||
|
||||
The ranker should emit concise rationales:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "src/parser.c",
|
||||
"surface": 5,
|
||||
"influence": 4,
|
||||
"reachability": 4,
|
||||
"priority": 4.5,
|
||||
"rationale": "C parser with external input, pointer arithmetic, and many callers",
|
||||
"model_id": "openrouter/...",
|
||||
"static_boosts": ["memory_unsafe", "parser", "sarif_high"]
|
||||
}
|
||||
```
|
||||
|
||||
Ranking is not only prioritization; it is a false-positive filter. The ranker should use verifier outcomes and dynamic evidence outcomes to calibrate which surfaces deserve attention. Repeated rejected patterns should be demoted unless new evidence appears.
|
||||
|
||||
Ranking metrics:
|
||||
|
||||
```text
|
||||
verified findings per tier
|
||||
false positives per tier
|
||||
false-positive reason distribution
|
||||
time-to-verification per tier
|
||||
retrieval hit quality per role
|
||||
missed known-vulnerable fixture targets
|
||||
```
|
||||
|
||||
### False-Positive Elimination Loop
|
||||
|
||||
HarnessX-style adjustment is the mechanism that reduces false positives over time. Each scan produces traces, verifier decisions, rejected hypotheses, static analyzer deltas, retrieval packages, and evidence transitions. The harness should use those outputs to adjust future runs.
|
||||
|
||||
Adjustment targets:
|
||||
|
||||
```text
|
||||
ranker score calibration
|
||||
retrieval filters and namespace weights
|
||||
hunter prompt constraints
|
||||
skill routing rules
|
||||
verifier tie-breaker templates
|
||||
static mapping seeds
|
||||
variant search patterns
|
||||
finding deduplication rules
|
||||
```
|
||||
|
||||
False-positive records:
|
||||
|
||||
```text
|
||||
finding_id
|
||||
rejection_reason
|
||||
counter_evidence
|
||||
scope of suppression or demotion
|
||||
affected ranker/static/retrieval inputs
|
||||
verifier model_id
|
||||
human_override if present
|
||||
```
|
||||
|
||||
False-positive learning must be scoped. A rejected claim in one framework, version, path, or call pattern should not globally suppress a vulnerability class.
|
||||
|
||||
### Hunter Pool
|
||||
|
||||
The hunter pool schedules targets by priority tier and depth.
|
||||
|
||||
Suggested tiers:
|
||||
|
||||
```text
|
||||
A: priority >= 3.0, most budget
|
||||
B: priority >= 2.0, moderate budget
|
||||
C: priority < 2.0, opportunistic budget
|
||||
```
|
||||
|
||||
Hunter inputs:
|
||||
|
||||
```text
|
||||
target metadata
|
||||
bounded source context
|
||||
static hints
|
||||
retrieved related code
|
||||
selected Trail of Bits skill snippets
|
||||
sandbox capabilities
|
||||
budget
|
||||
evidence requirements
|
||||
```
|
||||
|
||||
Hunter outputs must be structured and evidence-oriented. They should not rely on hidden reasoning.
|
||||
|
||||
Hunters must be language- and framework-aware. The default hunter prompts should not ask one generic model to find every vulnerability type. Initial hunter families should cover:
|
||||
|
||||
```text
|
||||
javascript_node_web: injection, XSS, SSRF, prototype pollution, insecure deserialization, auth/session bugs, dependency misuse
|
||||
python_web: command injection, SQL/template injection, deserialization, path traversal, Flask/Django misconfiguration, auth/session bugs
|
||||
java_web: servlet/Spring injection, XXE, deserialization, path traversal, authorization bypass, crypto/API misuse
|
||||
c_cpp: memory corruption, integer overflow, format strings, command execution, parser bugs, lifetime and ownership mistakes
|
||||
```
|
||||
|
||||
### Verifier
|
||||
|
||||
The verifier runs independently from the hunter. It receives only auditable facts:
|
||||
|
||||
```text
|
||||
finding claim
|
||||
source excerpts
|
||||
static evidence
|
||||
retrieved relevant context
|
||||
reproducer or crash artifacts when available
|
||||
negative evidence candidates
|
||||
```
|
||||
|
||||
Verifier output:
|
||||
|
||||
```json
|
||||
{
|
||||
"is_real": true,
|
||||
"severity": "high",
|
||||
"confidence": 0.82,
|
||||
"evidence_level": "static_corroboration",
|
||||
"pro_case": "...",
|
||||
"counter_case": "...",
|
||||
"tie_breaker": "Build a reproducer that drives len past buffer capacity",
|
||||
"required_next_step": "attempt_crash_reproduction"
|
||||
}
|
||||
```
|
||||
|
||||
### Fusion Adjudication
|
||||
|
||||
Fusion is the mandatory deepening mechanism whenever the task requires more reasoning depth than the normal ranker, hunter, verifier, and static mapping loop can provide.
|
||||
|
||||
Fusion triggers:
|
||||
|
||||
- Critical or high severity findings.
|
||||
- Findings with verifier disagreement.
|
||||
- Findings that gate patching or disclosure.
|
||||
- Findings where static and semantic evidence conflict.
|
||||
- Findings where a patch could introduce regression risk.
|
||||
- Findings where multiple skills disagree on exploitability or remediation.
|
||||
- User or task requests for deeper analysis, ultrathink-style review, or high-assurance adjudication.
|
||||
|
||||
Fusion protocol:
|
||||
|
||||
```text
|
||||
panel A independently reviews bounded evidence
|
||||
panel B independently reviews bounded evidence
|
||||
panels critique each other
|
||||
panels revise
|
||||
panels vote/rank
|
||||
decider issues final disposition
|
||||
reconciler records accepted/rejected/blocked items
|
||||
verifier records fresh evidence
|
||||
```
|
||||
|
||||
Fusion should not be forced for routine low-severity findings when ordinary evidence is already sufficient. It must run when the task requires deeper analysis, when evidence does not converge, or when the decision gates a risky fix, exploit claim, disclosure, or high-impact report.
|
||||
|
||||
### Docker Sandbox
|
||||
|
||||
Sandbox defaults:
|
||||
|
||||
```text
|
||||
network: none
|
||||
workspace: read-only
|
||||
scratch: writable tmpfs or volume
|
||||
cap_drop: all
|
||||
cap_add: SYS_PTRACE only when sanitizer/debugger needs it
|
||||
memory: bounded
|
||||
cpu: bounded
|
||||
pids: bounded
|
||||
runtime: docker default, optional gVisor
|
||||
```
|
||||
|
||||
Sandbox use cases:
|
||||
|
||||
```text
|
||||
static tool execution
|
||||
build system probing
|
||||
sanitizer builds
|
||||
fuzz harness compilation
|
||||
PoC execution
|
||||
local service startup
|
||||
netcat-style local socket probes
|
||||
protocol smoke tests
|
||||
patch validation in writable copy
|
||||
```
|
||||
|
||||
Patch validation must use a writable copy, never the user's working tree by default.
|
||||
|
||||
### Dynamic Analysis Boundary
|
||||
|
||||
OpenUltraSAST is SAST-first, but dynamic analysis is in scope when it provides necessary evidence. The harness may run controlled runtime checks such as service startup, local HTTP/TCP probes, netcat-style reachability checks, PoC execution, and Clearwing-style network validation patterns.
|
||||
|
||||
Dynamic analysis rules:
|
||||
|
||||
```text
|
||||
disabled by default
|
||||
enabled only by explicit config or task-required evidence
|
||||
scoped to declared hosts, ports, containers, and commands
|
||||
logged as artifacts
|
||||
bounded by timeout and resource limits
|
||||
never treated as an open-ended network pentest loop
|
||||
```
|
||||
|
||||
Dynamic evidence can support transitions such as `static_corroboration -> crash_reproduced`, `static_corroboration -> root_cause_explained`, or `root_cause_explained -> exploit_demonstrated` when the artifact actually demonstrates the claim.
|
||||
|
||||
### Explain Mode And Interactive Fixing
|
||||
|
||||
Explain mode turns a finding or benchmark miss into a prevention-oriented explanation. It should be useful to a coder who wants to understand the mistake, a vibecoder who needs safe guidance, and a reviewer who needs evidence boundaries.
|
||||
|
||||
Explain input types:
|
||||
|
||||
```text
|
||||
finding_id within a scan
|
||||
benchmark miss ID
|
||||
vulnerability class plus source location
|
||||
SARIF result ID imported into a scan
|
||||
```
|
||||
|
||||
Explain output sections:
|
||||
|
||||
```text
|
||||
what was found
|
||||
why this pattern is dangerous
|
||||
what attacker-controlled path or reachability evidence exists
|
||||
what evidence is missing
|
||||
how to fix it safely
|
||||
how to avoid recreating it
|
||||
which validation command or artifact would prove the fix
|
||||
which security skills were used for guidance
|
||||
```
|
||||
|
||||
Explain mode must not inflate evidence. If a result is only a pattern match, it says so. If reachability is unknown, it names the missing call graph, route, CLI, parser, or dynamic artifact needed to prioritize the fix.
|
||||
|
||||
Interactive fixing builds on explain mode and the patch oracle. It should ask for confirmation before patch proposal, select skills by language and vulnerability class, produce a minimal diff, run or list the required validation checks, and preserve all decisions as artifacts. It must not silently apply patches to the user's repository by default.
|
||||
|
||||
### Trail Of Bits Skill Integration
|
||||
|
||||
Trail of Bits skills should be indexed and selected by routing metadata.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
C/C++ memory target -> c-review, address-sanitizer, libfuzzer, harness-writing
|
||||
Python parser target -> atheris, property-based-testing
|
||||
Rust target -> cargo-fuzz, zeroize-audit when secrets are present
|
||||
Crypto target -> constant-time-analysis, wycheproof, vector-forge
|
||||
SARIF processing -> sarif-parsing
|
||||
Semgrep mapping loop -> semgrep, semgrep-rule-creator, semgrep-rule-variant-creator
|
||||
CodeQL mapping loop -> codeql, sarif-parsing
|
||||
Changed-code review -> differential-review, graph-evolution
|
||||
API footgun review -> sharp-edges, insecure-defaults
|
||||
Smart contract target -> chain-specific scanner skill
|
||||
```
|
||||
|
||||
Do not paste entire skills into every prompt. Store descriptors, retrieval chunks, and short operating checklists. Pull only the relevant snippets into a role-specific context package.
|
||||
|
||||
### UltraCode Fix Workflow
|
||||
|
||||
OpenUltraCode discipline should govern fix authoring and fix audit. The patch oracle is not a single prompt that writes code and declares success.
|
||||
|
||||
Fix workflow:
|
||||
|
||||
```text
|
||||
intake verified finding and evidence
|
||||
create bounded fix plan
|
||||
generate minimal patch in writable sandbox or worktree
|
||||
run affected checks and reproducer
|
||||
run adversarial fix review
|
||||
reconcile accepted/rejected/deferred review items
|
||||
run fresh verification
|
||||
emit patch diff and validation report
|
||||
```
|
||||
|
||||
Fix audit inputs:
|
||||
|
||||
```text
|
||||
original finding evidence
|
||||
patch diff
|
||||
differential analysis of changed code
|
||||
Semgrep/CodeQL result delta when available
|
||||
tests and reproducer output
|
||||
sharp-edge regression checklist when API/config behavior changes
|
||||
```
|
||||
|
||||
The workflow can propose a fix without applying it. It can only mark a fix as ready when the adversarial review is reconciled and fresh verification evidence exists.
|
||||
|
||||
### Data Model
|
||||
|
||||
Core entities:
|
||||
|
||||
```text
|
||||
Scan
|
||||
RepositorySnapshot
|
||||
HarnessRun
|
||||
ProcessorTrace
|
||||
ModelCall
|
||||
FileTarget
|
||||
Finding
|
||||
Evidence
|
||||
Artifact
|
||||
PatchProposal
|
||||
VerificationDecision
|
||||
FusionDecision
|
||||
FalsePositiveLearning
|
||||
RankingCalibration
|
||||
```
|
||||
|
||||
Finding fields:
|
||||
|
||||
```text
|
||||
id
|
||||
scan_id
|
||||
title
|
||||
description
|
||||
severity
|
||||
confidence
|
||||
status
|
||||
evidence_level
|
||||
path
|
||||
start_line
|
||||
end_line
|
||||
symbol
|
||||
vulnerability_class
|
||||
attack_scenario
|
||||
impact
|
||||
remediation
|
||||
discovered_by
|
||||
verified_by
|
||||
model_ids
|
||||
artifact_refs
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
Evidence levels:
|
||||
|
||||
```text
|
||||
suspicion
|
||||
static_corroboration
|
||||
crash_reproduced
|
||||
root_cause_explained
|
||||
exploit_demonstrated
|
||||
patch_validated
|
||||
```
|
||||
|
||||
### Artifact Layout
|
||||
|
||||
Suggested scan directory:
|
||||
|
||||
```text
|
||||
.openultrasast/runs/<scan-id>/
|
||||
manifest.json
|
||||
config.resolved.json
|
||||
repo_snapshot.json
|
||||
preprocess/
|
||||
index/
|
||||
rank/
|
||||
traces/
|
||||
model_calls/
|
||||
sandbox/
|
||||
findings.json
|
||||
report.md
|
||||
report.sarif
|
||||
patches/
|
||||
```
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
Build the system in vertical slices:
|
||||
|
||||
1. Quick scan: intake, preprocess, rank, LLM/static finding generation, JSON/Markdown report.
|
||||
2. Verification: independent verifier, evidence ladder enforcement, SARIF export.
|
||||
3. Retrieval: OpenRouter embeddings, persistent index, bounded context packages.
|
||||
4. Static mapping: Semgrep/CodeQL SARIF ingest, differential mapping, sharp-edge records.
|
||||
5. False-positive loop: rejected finding taxonomy, ranking calibration, retrieval adjustment, and scoped suppressions.
|
||||
6. Sandbox: Docker command runner, static tools, read-only source, scratch volume.
|
||||
7. Dynamic evidence: controlled service startup, local probes, PoC execution, and artifact capture.
|
||||
8. Deep mode: sanitizer builds, fuzz harness generation, crash artifacts.
|
||||
9. Patch mode: UltraCode-style fix authoring, adversarial fix audit, validation loop.
|
||||
10. MCP/opencode integration: narrow tools and commands.
|
||||
11. Fusion: mandatory deepening for task-required hard findings, contradictory evidence, and risky fixes.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. Evidence level is a state machine, not a label the model can freely assign.
|
||||
2. Verifier context excludes hunter reasoning to reduce contamination.
|
||||
3. OpenRouter model selection is role-scoped and auditable.
|
||||
4. Embeddings and vector-store metadata filtering are part of the runtime, not an offline optional add-on.
|
||||
5. Docker isolation is mandatory for execution-heavy modes.
|
||||
6. Trail of Bits skills are retrieved as scoped expertise, not global prompt bloat.
|
||||
7. MCP surface is narrow and project-oriented.
|
||||
8. Fixes are authored and audited through an UltraCode-style workflow, not a one-shot patch prompt.
|
||||
9. Fusion is the required deepening path when the task demands high-assurance reasoning, not a replacement for routine analysis.
|
||||
10. Harness adjustment loops must reduce repeated false positives without hiding auditable evidence.
|
||||
|
||||
## Risks
|
||||
|
||||
1. OpenRouter embedding availability and model-specific response formats may vary.
|
||||
2. Deep mode can become expensive without strict budget controls.
|
||||
3. Docker sandbox escape risk requires conservative defaults and no-network execution.
|
||||
4. LLM findings can inflate false positives unless the evidence ladder is enforced in code.
|
||||
5. Multi-language callgraph quality will vary; ranking must tolerate partial graph data.
|
||||
6. Skill retrieval can overfit prompts if descriptors are too broad.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Which local vector store should be the default: Chroma, LanceDB, SQLite vector extension, or Qdrant local?
|
||||
2. Should the first implementation be pure Python like Clearwing/HarnessX, or TypeScript to align with more opencode extension tooling?
|
||||
3. Should OpenUltraSAST vendor a minimal HarnessX-inspired runtime or depend directly on HarnessX?
|
||||
4. What minimum OpenRouter embedding model should be required for acceptable code retrieval?
|
||||
5. Should patch proposals be applied only through git worktrees?
|
||||
@@ -1,242 +0,0 @@
|
||||
# Requirements: OpenRouter SAST Harness
|
||||
|
||||
> **Status 2026-10-02:** retired (see the note below). Its OpenCode skill `openultrasast-kiro-impl`, which still sent agents to this tasks.md, was deleted. Kept as decision history; nothing in src, tests or CI points here.
|
||||
|
||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
|
||||
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
|
||||
|
||||
## Overview
|
||||
|
||||
OpenUltraSAST shall be an independent OpenCode-driven security harness for finding, validating, reporting, and optionally fixing source-code vulnerabilities at scale. It shall use OpenRouter-selected chat models for reasoning, OpenRouter embeddings for semantic retrieval, Docker for isolated analysis, and an explicit evidence ladder to prevent AI suspicion from being reported as verified security impact.
|
||||
|
||||
The project shall use Clearwing only as an implementation oracle. It shall not import Clearwing internals, fork Clearwing, or expose Clearwing's broad MCP surface.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Provide a repeatable SAST harness that can scan local repositories and produce ranked, evidence-scored findings.
|
||||
2. Make false-positive elimination a primary product goal by separating suspicion, corroboration, verification, reproduction, and patch validation.
|
||||
3. Combine static analysis, semantic retrieval, LLM hunters, adversarial verification, and patch validation into one opencode-native workflow.
|
||||
4. Keep model routing explicit through OpenRouter, with per-role model configuration and no silent substitution.
|
||||
5. Preserve Docker isolation as a first-class security boundary for code execution, harness compilation, fuzzing, and patch validation.
|
||||
6. Integrate Trail of Bits skills as scoped security expertise selected by language, framework, vulnerability class, mapping need, or verification need.
|
||||
7. Emit machine-readable artifacts suitable for CI, triage, and later training or harness evolution.
|
||||
8. Build detector quality against grounded vulnerable-project benchmarks for JavaScript/Node/web, Python/web, Java, and C/C++ instead of relying on toy fixtures or pattern-only tests.
|
||||
9. Help coders and vibecoders understand and fix findings through an evidence-aware explain mode that teaches the secure pattern, not only flags the insecure one.
|
||||
|
||||
## Completeness Model
|
||||
|
||||
Implementation progress shall be reported against functional harness capability, not raw checklist percentage. A task may be complete as scaffold while the product capability remains incomplete.
|
||||
|
||||
Completion levels:
|
||||
|
||||
1. `scaffold`: schemas, CLI shape, local artifacts, tests, or isolated primitives exist.
|
||||
2. `usable`: the feature runs end-to-end for a narrow local fixture without manual wiring.
|
||||
3. `integrated`: the feature participates in the scan harness with traces, evidence, reports, and degradation handling.
|
||||
4. `production`: the feature is robust enough for real repositories, CI budgets, failure recovery, and analyst audit.
|
||||
|
||||
Progress reports shall identify both checklist progress and functional completeness. The project shall not describe OpenUltraSAST as a working harness until the `usable_harness_mvp` gate is satisfied.
|
||||
|
||||
Functional gates:
|
||||
|
||||
1. `scaffold_quick_scan`: quick scan can enumerate files, rank targets, emit basic static findings, and write JSON/Markdown artifacts. This gate does not constitute a security harness.
|
||||
2. `usable_harness_mvp`: harness runtime, event tracing, static mapping ingestion, evidence ladder enforcement, independent verification, and JSON/Markdown/SARIF reporting work together on fixture repositories.
|
||||
3. `standard_security_harness`: standard mode runs ranked hunters with retrieval packages, selected security skills, independent verification, false-positive learning, and auditable traces.
|
||||
4. `sandboxed_dynamic_harness`: Docker sandbox, scoped dynamic probes, sanitizer/build artifacts, and evidence transitions work without hidden network or host mutation.
|
||||
5. `fix_validation_harness`: patch proposals, adversarial fix audit, differential/static delta checks, and sandbox validation are integrated and cannot mark patches validated without passing evidence.
|
||||
6. `opencode_product`: MCP tools, opencode commands/skills, fusion deepening, CI-ready reports, cost controls, and hardening are complete enough for routine opencode-driven use.
|
||||
7. `benchmark_grounded_finder`: benchmark manifests, expected vulnerabilities, tool comparison baselines, recall/precision metrics, and missed-finding regression tracking exist for JavaScript, Python, Java, and C/C++ vulnerable projects.
|
||||
8. `explain_and_fix_workflow`: users can ask for an evidence-grounded explanation of a finding, receive prevention guidance, and enter an interactive fixing workflow that routes to the appropriate security skills without silently applying patches.
|
||||
|
||||
The current implementation state shall be documented as `usable_harness_mvp` until later gates are explicitly verified.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
1. Do not make network pentesting the core workflow.
|
||||
2. Do not reimplement all of Clearwing.
|
||||
3. Do not report LLM-only claims as verified vulnerabilities.
|
||||
4. Do not require a local embedding model.
|
||||
5. Do not depend on a single OpenRouter model.
|
||||
6. Do not mutate user repositories by default.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### Repository Intake
|
||||
|
||||
1. The system shall accept a local repository path as the primary scan target.
|
||||
2. The system should later support git URLs, but initial implementation may require local checkout.
|
||||
3. The system shall snapshot basic repository metadata: root path, commit hash when available, language mix, file count, and ignored paths.
|
||||
4. The system shall respect common ignore files such as `.gitignore`, `.ignore`, and explicit harness config excludes.
|
||||
|
||||
### Preprocessing
|
||||
|
||||
1. The system shall enumerate source files and assign initial language, LOC, and path metadata.
|
||||
2. The system shall tag files using lightweight static heuristics, including memory-unsafe code, parser surfaces, crypto surfaces, authentication boundaries, syscall or process boundaries, serialization boundaries, and fuzzable entry points.
|
||||
3. The system shall build a dependency or call graph where practical.
|
||||
4. The system shall ingest optional static analyzer output such as Semgrep SARIF or CodeQL SARIF.
|
||||
5. The system shall preserve preprocessing artifacts for later ranking, retrieval, and audit.
|
||||
|
||||
### Static Analysis And Mapping Discipline
|
||||
|
||||
1. The system shall treat Semgrep, CodeQL, differential review, and sharp-edge/API review as mapping disciplines, not just optional external scanners.
|
||||
2. The system shall ingest Semgrep and CodeQL output as SARIF and normalize it into static hints, evidence candidates, and variant-search seeds.
|
||||
3. The system shall support function-level entry-point and reachability analysis to identify attacker-controlled inputs, externally callable functions, public routes, CLI/process boundaries, parser entry points, smart-contract state-changing entry points, callbacks, and privileged/admin-only surfaces.
|
||||
4. The system shall classify entry points by access level and trust boundary, including public/unrestricted, authenticated, role-restricted, contract-only/callback, local-only, and unknown/review-required.
|
||||
5. The system shall preserve condition evidence for entry points, including feature flags, rollout toggles, pause guards, experiment gates, and runtime guard expressions that affect whether a threat actor can reach the code.
|
||||
6. The system shall use function-level reachability modeling to prioritize candidate bugs inside reachable entry-point functions and to demote pattern-only findings that lack a plausible attacker-controlled path.
|
||||
6. The system shall support differential analysis between a base and head revision to identify changed attack surfaces, newly reachable code, and regression-prone fixes.
|
||||
7. The system shall support sharp-edge mapping for APIs, configuration, defaults, cryptographic interfaces, and dangerous framework patterns that create predictable misuse.
|
||||
8. The system shall preserve analyzer provenance so each finding can cite which static tool, rule, diff, entry-point analysis, or skill-derived checklist contributed evidence.
|
||||
|
||||
### Benchmark-Grounded Detection
|
||||
|
||||
1. The system shall maintain benchmark manifests for realistic vulnerable projects, not only synthetic single-file fixtures.
|
||||
2. The initial benchmark matrix shall cover JavaScript/Node/web, Python/web, Java, and C/C++.
|
||||
3. Benchmarks shall include vulnerable training projects and curated CWE samples such as NodeGoat, DVNA or similar Node/Express apps, OWASP Juice Shop slices, PyGoat or vulnerable Flask/Django samples, OWASP Benchmark/WebGoat/Juliet Java, Juliet C/C++, vulnerable parser fixtures, and small sanitizer-backed C/C++ crash targets.
|
||||
4. Each benchmark manifest shall record repository path or source, language, framework, setup commands, scan modes to run, expected vulnerability classes, expected locations when known, and known noisy/non-security patterns.
|
||||
5. The benchmark runner shall emit recall, precision where ground truth allows it, false-positive reason distribution, missed expected findings, unsupported vulnerability classes, runtime, model usage, and artifact paths.
|
||||
6. Benchmark results shall compare quick, standard, and deep modes separately so rule coverage, LLM hunting, and dynamic validation are not conflated.
|
||||
7. Benchmark runs shall support comparison against external baselines such as Clearwing, Semgrep, CodeQL, clang-tidy, npm audit, Bandit, or language-appropriate tools when their outputs are available as SARIF or normalized findings.
|
||||
8. Missed benchmark vulnerabilities shall become regression records that feed rule expansion, ranking calibration, retrieval packages, hunter prompts, and skill routing.
|
||||
9. A detector shall not be considered realistic for a language until it demonstrates useful recall on at least one realistic benchmark project and one curated CWE corpus for that language.
|
||||
10. Benchmark artifacts shall be preserved under `.openultrasast/benchmarks/<benchmark-run-id>/` with links to the scan artifacts that produced each metric.
|
||||
|
||||
### OpenRouter Embeddings And Retrieval
|
||||
|
||||
1. The system shall use OpenRouter embedding endpoints for semantic indexing.
|
||||
2. The system shall chunk source code, docs, static findings, prior mechanisms, and Trail of Bits skill descriptors into separate collections or namespaces.
|
||||
3. The system shall store vectors in a local persistent vector database by default.
|
||||
4. The system shall support reusing an existing index when the repository snapshot has not changed.
|
||||
5. The system shall retrieve bounded context packages for rankers, hunters, verifiers, and patchers.
|
||||
6. The system shall evaluate vector-store candidates against ranking quality, metadata filtering, incremental indexing, local persistence, cost, and reproducibility before selecting a default.
|
||||
7. The vector store shall support precise metadata filters for path, language, symbol, vulnerability class, evidence level, scan ID, and repository snapshot.
|
||||
|
||||
### Ranking
|
||||
|
||||
1. The system shall score candidate files on at least three axes: surface, influence, and reachability.
|
||||
2. The system shall compute a composite priority using an explicit formula, initially `surface * 0.5 + influence * 0.2 + reachability * 0.3`.
|
||||
3. The system shall keep low-surface high-influence files eligible for analysis.
|
||||
4. The system shall combine static heuristics, dependency graph data, entry-point reachability, embeddings, and OpenRouter LLM scoring.
|
||||
5. The system shall record score rationales and model identity.
|
||||
6. The system shall measure ranking quality with fixture repositories and prior scan outcomes, including false-positive rate by tier and missed-true-positive rate where ground truth exists.
|
||||
7. The system shall feed verifier outcomes back into ranking calibration so repeated false-positive patterns lose priority over time.
|
||||
|
||||
### False-Positive Elimination Loop
|
||||
|
||||
1. The system shall track every rejected, contradicted, duplicate, unsupported, and unverified finding as false-positive learning data.
|
||||
2. The system shall record why a finding failed: unreachable path, missing attacker control, sanitizer disproved, static rule mismatch, incorrect model assumption, duplicate finding, or insufficient impact.
|
||||
3. The system shall use HarnessX-style adjustment loops to update prompts, retrieval filters, ranking calibration, skill routing, verifier tie-breakers, and static mapping seeds from false-positive outcomes.
|
||||
4. The system shall keep false-positive learnings auditable and reversible; learned suppressions shall cite evidence and scope.
|
||||
5. The system shall prefer evidence-gated demotion over deletion so analysts can inspect why a suspected issue was suppressed or downgraded.
|
||||
|
||||
### Harness Execution
|
||||
|
||||
1. The system shall model the scan as a HarnessX-style harness composed of processors, tools, memory, sandbox, trace, and model roles.
|
||||
2. The system shall expose stage hooks for task start, stage start, before model, after model, before tool, after tool, stage end, and task end.
|
||||
3. The system shall contract-check processor inputs and outputs in strict mode for CI and warn mode for local exploration.
|
||||
4. The system shall serialize harness configuration, processor versions, model roles, and prompt hashes into scan artifacts.
|
||||
|
||||
### Hunter Pool
|
||||
|
||||
1. The system shall fan out analysis across ranked targets using budgeted tiers.
|
||||
2. The system shall support at least three analysis depths: quick, standard, and deep.
|
||||
3. Quick mode shall rely on static analysis, ranking, and bounded LLM review without code execution.
|
||||
4. Standard mode shall use sandboxed tool-calling hunters and adversarial verification.
|
||||
5. Deep mode shall add harness generation, sanitizer builds, fuzzing, dynamic reproduction, crash reproduction, and patch validation where applicable.
|
||||
6. The system shall record every hunter trajectory as structured JSONL.
|
||||
7. Standard-mode hunters shall be evaluated against benchmark misses before being treated as production-quality detectors.
|
||||
8. Hunter prompts shall be language- and framework-aware for JavaScript/Node/web, Python/web, Java, and C/C++ instead of using one generic vulnerability prompt for all targets.
|
||||
|
||||
### Evidence Ladder
|
||||
|
||||
1. Every finding shall carry exactly one current evidence level.
|
||||
2. Evidence levels shall be ordered as: `suspicion`, `static_corroboration`, `crash_reproduced`, `root_cause_explained`, `exploit_demonstrated`, `patch_validated`.
|
||||
3. Findings below `static_corroboration` shall not be reported as verified.
|
||||
4. Exploit or PoC generation shall require at least `crash_reproduced` or equivalent trigger evidence.
|
||||
5. Auto-patching shall require at least `root_cause_explained` and shall only advance to `patch_validated` after tests or reproducer validation pass.
|
||||
|
||||
### Verification And Fusion
|
||||
|
||||
1. The system shall verify findings using independent context that excludes hunter chain-of-thought and raw hunter reasoning.
|
||||
2. The verifier shall steel-man both the vulnerability case and the false-positive case.
|
||||
3. The verifier shall output explicit tie-breaker evidence requirements.
|
||||
4. The system shall support OpenUltraCode-style fusion for task-required deepening, high-impact findings, uncertain findings, risky fixes, or unresolved disagreement, using two panels and an explicit decider.
|
||||
5. Fusion output shall disclose model IDs, panel roles, degradations, warnings, votes, and final decision source.
|
||||
6. Every adversarial finding shall receive a disposition: accepted, rejected, mitigated, deferred, or blocked.
|
||||
7. Fusion shall be mandatory whenever the task requires deeper reasoning than the normal ranker, hunter, verifier, and static mapping loop can provide.
|
||||
8. Fusion shall not be forced for routine low-risk issues unless the task explicitly asks for deeper analysis or the evidence remains unresolved.
|
||||
|
||||
### Docker Sandbox
|
||||
|
||||
1. The system shall run untrusted build, test, fuzz, and PoC commands inside Docker by default.
|
||||
2. Analysis containers shall have network disabled by default.
|
||||
3. Source mounts shall be read-only except in explicit patch workspaces.
|
||||
4. Containers shall use a writable scratch volume or tmpfs.
|
||||
5. Containers shall enforce CPU, memory, pids, timeout, and capability limits.
|
||||
6. The system shall support sanitizer variants, initially ASan and UBSan for C/C++ targets.
|
||||
7. The system should support gVisor or another hardened runtime when available.
|
||||
|
||||
### Dynamic Analysis
|
||||
|
||||
1. The system shall support optional dynamic analysis when runtime evidence is needed to verify, reproduce, or de-risk a finding.
|
||||
2. Dynamic analysis may include service startup, local socket checks, netcat-style probes, protocol smoke tests, PoC execution, or Clearwing-style network validation patterns.
|
||||
3. Dynamic network access shall be disabled by default and enabled only through explicit scan configuration or task-required evidence collection.
|
||||
4. Dynamic probes shall run in sandboxed environments with declared network scope, timeouts, command logs, and captured artifacts.
|
||||
5. Dynamic analysis shall contribute evidence but shall not turn OpenUltraSAST into a general network pentest agent.
|
||||
|
||||
### Patch Oracle
|
||||
|
||||
1. The system shall optionally generate minimal defensive patches for verified findings.
|
||||
2. The patch workflow shall follow OpenUltraCode discipline: intake, bounded plan, implementation, adversarial review, reconciliation, and fresh verification.
|
||||
3. The patch oracle shall target the smallest safe change.
|
||||
4. The patch oracle shall not rewrite unrelated code or change public signatures unless required and approved.
|
||||
5. Patch validation shall rerun the reproducer, affected tests, or relevant build checks in a writable sandbox.
|
||||
6. Patch artifacts shall be emitted as diffs, not silently applied to the user's repository by default.
|
||||
7. The system shall audit proposed fixes for regression risk using differential analysis and relevant Trail of Bits skills before marking them ready.
|
||||
|
||||
### Explain Mode And Interactive Fixing
|
||||
|
||||
1. The system shall expose an explain workflow for a finding, vulnerability class, or benchmark miss.
|
||||
2. Explain mode shall describe the vulnerable pattern, why it matters, what attacker-controlled path or evidence exists, what evidence is missing, and what secure coding habit would have prevented it.
|
||||
3. Explain mode shall distinguish education from verification; an explanation shall not upgrade evidence level unless the required evidence artifact already exists.
|
||||
4. Explain mode shall cite source lines, finding artifacts, verifier results, SARIF/static evidence, dynamic evidence, benchmark metadata, and relevant skill snippets where available.
|
||||
5. Explain mode shall support audience levels such as concise, learner, and reviewer so it can serve coders, vibecoders, and security reviewers without changing the underlying evidence.
|
||||
6. Interactive fixing shall guide the user through a bounded plan, relevant security skill selection, minimal patch proposal, adversarial review, and validation commands.
|
||||
7. Interactive fixing shall route by language and vulnerability class, for example C/C++ memory issues to `c-review`, `address-sanitizer`, `libfuzzer`, and `harness-writing`; JavaScript web issues to web/security mapping and dependency checks; Python web issues to injection/deserialization/auth/defaults checks; and Java issues to OWASP Benchmark/WebGoat-style dataflow and framework checks.
|
||||
8. Interactive fixing shall never silently apply patches to the user's repository by default. It shall produce a proposed diff and required validation plan first.
|
||||
9. Explain and fix outputs shall emphasize prevention patterns so users learn how not to recreate the same vulnerability class in future code.
|
||||
|
||||
### Reporting
|
||||
|
||||
1. The system shall emit JSON, SARIF, Markdown, and a manifest per scan.
|
||||
2. Reports shall sort findings by severity, evidence level, reachability, and confidence.
|
||||
3. Reports shall include model IDs, static evidence, verifier results, reproduction evidence, patch status, and artifact paths.
|
||||
4. Reports shall distinguish suspected, verified, exploited, and patch-validated findings.
|
||||
|
||||
### MCP And OpenCode Integration
|
||||
|
||||
1. The project shall expose a narrow MCP server for OpenCode integration.
|
||||
2. MCP tools shall focus on project-level operations: scan, status, findings, evidence, artifacts, patch proposal, and report export.
|
||||
3. The MCP server shall not expose arbitrary internal tools as public MCP methods.
|
||||
4. OpenCode commands shall invoke the harness with explicit model role configuration and visible assumptions.
|
||||
5. MCP and OpenCode integration shall include narrow operations for benchmark status, finding explanation, and interactive patch proposal without exposing arbitrary shell execution.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
1. The system shall be deterministic where possible: fixed config, artifact manifests, prompt hashes, and model identifiers.
|
||||
2. The system shall degrade visibly when optional tools, Docker, embeddings, or specific models are unavailable.
|
||||
3. The system shall avoid hidden network access from analysis containers; any dynamic network access shall be explicit, scoped, logged, and artifact-backed.
|
||||
4. The system shall support CI usage with bounded runtime and cost budgets.
|
||||
5. The system shall keep findings auditable from report back to source, trace, model call, verifier decision, and artifact.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `scaffold_quick_scan`: A user can run a local repository scan in quick mode and receive ranked static findings with JSON and Markdown reports. This may use heuristics and static patterns only.
|
||||
2. `usable_harness_mvp`: A user can run quick or standard fixture scans through the harness runtime and inspect event traces, normalized static mapping evidence, verifier decisions, and report artifacts.
|
||||
3. `standard_security_harness`: A user can run standard mode and receive adversarially verified findings with explicit evidence levels, selected retrieval context, skill routing, and false-positive learning records.
|
||||
4. `sandboxed_dynamic_harness`: A user can run deep mode on a small C/C++ parser target and receive sandboxed build/fuzz artifacts when the target is fuzzable.
|
||||
5. `fix_validation_harness`: A patch cannot be marked validated unless a sandboxed validation command passes and the fix audit has no accepted blocking findings.
|
||||
6. `opencode_product`: A user can drive scans, triage, evidence inspection, report export, fusion, and patch proposal through narrow MCP/OpenCode interfaces without exposing arbitrary shell execution.
|
||||
7. A finding cannot be marked verified unless it reaches at least `static_corroboration` through enforced evidence transitions.
|
||||
8. A scan manifest records repository snapshot, harness config, OpenRouter model IDs, embedding model, prompt hashes, processor versions, evidence transitions, and artifact paths.
|
||||
9. `benchmark_grounded_finder`: A user can run a benchmark manifest for JavaScript, Python, Java, or C/C++ and receive language-level recall, missed finding, false-positive, runtime, and artifact reports.
|
||||
10. `explain_and_fix_workflow`: A user can request an explanation for a finding or benchmark miss and receive evidence-cited prevention guidance plus an optional interactive fix plan without automatic repository mutation.
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"feature_name": "openrouter-sast-harness",
|
||||
"language": "en",
|
||||
"status": "retired",
|
||||
"updated_at": "2026-10-02T00:00:00Z",
|
||||
"retired_note": "2026-09-30: HarnessX was removed by harnessx-removal; this spec had no spec.json before 2026-10-02 and is kept as decision history."
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
# Tasks: OpenRouter SAST Harness
|
||||
|
||||
## Completion Gates
|
||||
|
||||
Checklist completion is not product completion. Each phase now carries a functional gate label:
|
||||
|
||||
- `scaffold`: isolated primitives, schemas, local artifacts, or tests exist.
|
||||
- `usable`: the feature works end-to-end for fixtures without manual wiring.
|
||||
- `integrated`: the feature participates in the harness runtime, traces, evidence, and reports.
|
||||
- `production`: the feature is robust for real repositories, CI budgets, and analyst audit.
|
||||
|
||||
Current verified gate: `usable_harness_mvp`.
|
||||
|
||||
Do not report OpenUltraSAST as a working security harness until `usable_harness_mvp` is checked.
|
||||
|
||||
## Phase 0: Project Skeleton (`scaffold`, complete)
|
||||
|
||||
- [x] Create Python package skeleton with CLI entry point `ousast`.
|
||||
- [x] Add config loading for `openultrasast.toml` and environment variables.
|
||||
- [x] Add resolved config artifact writer.
|
||||
- [x] Add scan run directory creation under `.openultrasast/runs/<scan-id>/`.
|
||||
- [x] Add basic tests for config loading and run directory creation.
|
||||
|
||||
## Phase 1: Intake And Preprocess
|
||||
|
||||
- [x] Implement repository snapshot collection.
|
||||
- [x] Implement source file enumeration with ignore handling.
|
||||
- [x] Implement language detection from extension and shebang.
|
||||
- [x] Implement LOC counting.
|
||||
- [x] Implement initial tag heuristics for parser, memory unsafe, crypto, auth boundary, deserialization, syscall, network, filesystem, and fuzzable targets.
|
||||
- [x] Emit `preprocess/file_targets.json`.
|
||||
- [x] Add tests for tag heuristics on small fixture repositories.
|
||||
|
||||
## Phase 2: Ranking
|
||||
|
||||
- [x] Define `FileTarget` and ranking schemas.
|
||||
- [x] Implement heuristic score floors and boosts.
|
||||
- [x] Implement OpenRouter chat client for ranker role.
|
||||
- [x] Implement chunked ranker calls with JSON response validation.
|
||||
- [x] Compute composite priority.
|
||||
- [x] Emit ranking rationales and model metadata.
|
||||
- [x] Add tests for composite priority and malformed model response handling.
|
||||
|
||||
## Phase 3: Quick Scan Reports
|
||||
|
||||
- [x] Implement static finding schema.
|
||||
- [x] Implement quick-mode hunter prompt and bounded file context.
|
||||
- [x] Implement structured finding extraction.
|
||||
- [x] Enforce initial evidence level `suspicion` unless static corroboration exists.
|
||||
- [x] Emit `findings.json`.
|
||||
- [x] Emit Markdown report.
|
||||
- [x] Add smoke test against a fixture repository with one known insecure pattern.
|
||||
|
||||
## Phase 4: OpenRouter Embeddings
|
||||
|
||||
- [x] Implement OpenRouter embedding client.
|
||||
- [x] Run a vector-store bakeoff for metadata filtering, local persistence, incremental indexing, export/import, and retrieval quality.
|
||||
- [x] Select default local vector store based on bakeoff results.
|
||||
- [x] Implement code chunking with path, symbol, and line metadata.
|
||||
- [x] Implement namespaces for repo code, docs, static findings, mechanisms, skills, and traces.
|
||||
- [x] Implement index reuse keyed by repository snapshot and embedding model.
|
||||
- [x] Implement bounded retrieval package builder per role.
|
||||
- [x] Add tests for chunk metadata and retrieval filters.
|
||||
|
||||
## Phase 4A: Ranking Quality And False-Positive Loop
|
||||
|
||||
- [x] Define false-positive reason taxonomy.
|
||||
- [x] Add false-positive learning records for rejected, duplicate, unsupported, contradicted, and unverified findings.
|
||||
- [x] Track ranking metrics by tier: verified findings, false positives, time to verification, and missed fixture vulnerabilities.
|
||||
- [x] Feed verifier outcomes into ranking calibration.
|
||||
- [x] Feed rejected findings into retrieval filter adjustment and prompt constraints.
|
||||
- [x] Add tests proving scoped false-positive demotion does not globally suppress the vulnerability class.
|
||||
|
||||
## Gate: `scaffold_quick_scan`
|
||||
|
||||
- [x] Quick scan can enumerate files, rank targets, emit basic static findings, and write JSON/Markdown artifacts.
|
||||
- [x] Quick scan is uv-managed and covered by unit tests.
|
||||
- [ ] Quick scan includes real LLM hunter execution. This is intentionally deferred to standard mode.
|
||||
- [x] Quick scan includes SARIF output.
|
||||
- [x] Quick scan includes enforced verifier decisions.
|
||||
|
||||
## Gate: `usable_harness_mvp`
|
||||
|
||||
- [x] Harness runtime drives scan stages instead of direct CLI orchestration.
|
||||
- [x] Event traces are emitted for every scan stage.
|
||||
- [x] Static mapping ingestion feeds FileTarget hints and verifier evidence candidates.
|
||||
- [x] Entry-point reachability feeds FileTarget reachability hints and ranking priority.
|
||||
- [x] Evidence ladder state machine prevents invalid verified statuses.
|
||||
- [x] Independent verifier runs on fixture findings without hunter reasoning.
|
||||
- [x] JSON, Markdown, SARIF, and manifest artifacts share finding IDs and evidence references.
|
||||
- [x] A fixture scan demonstrates one accepted static-corroborated finding and one rejected false positive.
|
||||
|
||||
## Phase 5: Harness Runtime (`usable_harness_mvp` prerequisite)
|
||||
|
||||
- [x] Define harness event model.
|
||||
- [x] Define processor interface and contract checks.
|
||||
- [x] Implement strict and warn modes.
|
||||
- [x] Add trace writer for events and processor state changes.
|
||||
- [x] Serialize harness config, prompt hashes, processor versions, and model roles.
|
||||
- [x] Add tests for processor contract violations.
|
||||
|
||||
## Phase 5A: Static Mapping Disciplines
|
||||
|
||||
- [x] Add SARIF ingestion for Semgrep and CodeQL outputs.
|
||||
- [x] Normalize SARIF rules, locations, severity, fingerprints, and provenance into static hints.
|
||||
- [x] Add Semgrep mapping task records for pattern, variant, and rule-authoring loops.
|
||||
- [x] Add CodeQL mapping task records for source, sink, sanitizer, and path explanations.
|
||||
- [x] Add entry-point mapping records for routes, CLI/process boundaries, parser entry points, smart-contract state-changing entry points, callbacks, and privileged surfaces.
|
||||
- [x] Classify entry points by access level: public, authenticated, role-restricted, contract-only/callback, local-only, or review-required.
|
||||
- [x] Map entry points to function-level source ranges and access evidence.
|
||||
- [x] Preserve conditional reachability evidence for feature flags, rollout toggles, pause guards, and runtime guard expressions.
|
||||
- [x] Attach reachability hints to FileTargets and ranking inputs.
|
||||
- [x] Annotate findings with function-level reachability status before prioritizing fixes.
|
||||
- [x] Add differential mapping records for changed files, changed functions, and changed trust-boundary crossings.
|
||||
- [x] Add sharp-edge records for insecure defaults, dangerous APIs, confusing config, and misuse-prone interfaces.
|
||||
- [x] Add tests for mapping analyzer output to `FileTarget` hints and verifier evidence candidates.
|
||||
|
||||
## Phase 6: Verification (`usable_harness_mvp` prerequisite)
|
||||
|
||||
- [x] Define evidence ladder state machine.
|
||||
- [x] Implement independent verifier prompt and schema.
|
||||
- [x] Ensure verifier context excludes hunter reasoning.
|
||||
- [x] Implement pro-case, counter-case, tie-breaker, and required-next-step fields.
|
||||
- [x] Enforce that verified report status requires at least `static_corroboration`.
|
||||
- [x] Add tests for invalid evidence transitions.
|
||||
|
||||
## Gate: `standard_security_harness`
|
||||
|
||||
- [x] Standard mode uses ranked hunter scheduling, retrieval packages, selected skill snippets, and independent verification.
|
||||
- [x] Every hunter trajectory is persisted and linked to finding artifacts.
|
||||
- [x] False-positive learning updates ranking calibration from verifier outcomes.
|
||||
- [ ] Fusion can be triggered for high-impact, contradictory, or difficult findings.
|
||||
- [ ] Standard mode uses real language-aware LLM hunters instead of reusing quick static findings.
|
||||
|
||||
## Gate: `benchmark_grounded_finder`
|
||||
|
||||
- [ ] Benchmark manifests exist for JavaScript/Node/web, Python/web, Java, and C/C++ vulnerable projects.
|
||||
- [ ] Benchmark runner executes quick, standard, and deep modes as supported and preserves benchmark artifacts.
|
||||
- [ ] Benchmark reports include recall, missed expected findings, false-positive reasons, runtime, and external baseline deltas.
|
||||
- [ ] Missed benchmark findings feed calibration records for rules, ranking, retrieval, hunters, SARIF ingestion, dynamic probes, or skill routing.
|
||||
- [ ] At least one realistic vulnerable project and one curated CWE corpus are tracked per supported language family.
|
||||
|
||||
## Phase 6A: Benchmark Corpus And Scoreboard (`benchmark_grounded_finder` prerequisite)
|
||||
|
||||
- [x] Define benchmark manifest schema with source, language, framework, setup, expected vulnerabilities, known noise, scan modes, and optional baselines.
|
||||
- [x] Implement benchmark run directory creation under `.openultrasast/benchmarks/<benchmark-run-id>/`.
|
||||
- [x] Implement finding-to-ground-truth matching by CWE, rule ID, file, function, sink, and evidence text.
|
||||
- [x] Implement benchmark metrics for recall, precision when possible, false-positive reasons, missed findings, runtime, model usage, and artifact links.
|
||||
- [x] Add benchmark fixtures or manifests for JavaScript/Node/web, Python/web, Java, and C/C++.
|
||||
- [x] Add external baseline ingestion for SARIF or normalized findings from Clearwing, Semgrep, CodeQL, clang-tidy, Bandit, npm audit, or language-appropriate tools.
|
||||
- [x] Add tests proving benchmark misses are recorded as calibration inputs without automatically suppressing unrelated findings.
|
||||
|
||||
## Gate: `explain_and_fix_workflow`
|
||||
|
||||
- [ ] Explain mode can explain a finding or benchmark miss with source evidence, reachability status, missing evidence, secure pattern, and prevention guidance.
|
||||
- [ ] Explain mode supports concise, learner, and reviewer audience levels without changing evidence levels.
|
||||
- [ ] Interactive fixing selects relevant security skills and proposes a minimal patch plan without silently mutating the target repository.
|
||||
- [ ] Fix guidance links to validation commands and required evidence transitions.
|
||||
|
||||
## Phase 6B: Explain Mode And Interactive Fixing (`explain_and_fix_workflow` prerequisite)
|
||||
|
||||
- [ ] Add explain artifact schema for finding ID, source refs, evidence refs, audience level, prevention guidance, selected skills, and validation advice.
|
||||
- [ ] Add `ousast explain <finding-id> --scan <scan-id>` CLI command.
|
||||
- [ ] Add explain support for benchmark misses and imported SARIF findings.
|
||||
- [ ] Implement language/vulnerability skill routing for JavaScript/Node/web, Python/web, Java, and C/C++.
|
||||
- [ ] Add interactive fixing plan schema with confirmation gates, selected skills, proposed patch scope, validation commands, and evidence requirements.
|
||||
- [ ] Ensure interactive fixing cannot mark patches validated unless the patch oracle and sandbox validation gates pass.
|
||||
- [ ] Add tests that explanation does not upgrade evidence level and patch guidance does not mutate repositories by default.
|
||||
|
||||
## Phase 7: Docker Sandbox (`sandboxed_dynamic_harness` prerequisite)
|
||||
|
||||
- [ ] Implement Docker availability probe.
|
||||
- [ ] Implement no-network read-only workspace sandbox runner.
|
||||
- [ ] Add explicit scoped-network sandbox mode for task-required dynamic probes.
|
||||
- [ ] Add scratch volume or tmpfs support.
|
||||
- [ ] Add CPU, memory, pids, timeout, and capability limits.
|
||||
- [ ] Add command execution artifact capture.
|
||||
- [ ] Add sanitizer build environment for C/C++ fixtures.
|
||||
- [ ] Add tests that verify workspace remains read-only.
|
||||
|
||||
## Phase 7A: Dynamic Evidence (`sandboxed_dynamic_harness` prerequisite)
|
||||
|
||||
- [ ] Implement dynamic analysis configuration with `enabled = false` by default.
|
||||
- [ ] Implement service startup command records with declared ports and timeouts.
|
||||
- [ ] Implement netcat-style local socket probe tool inside sandbox scope.
|
||||
- [ ] Implement HTTP or protocol smoke-test artifact capture.
|
||||
- [ ] Tie dynamic artifacts to evidence transitions without allowing model-only upgrades.
|
||||
- [ ] Add tests that reject undeclared network targets.
|
||||
|
||||
## Phase 8: Standard Mode Hunter Pool (`standard_security_harness` prerequisite)
|
||||
|
||||
- [x] Implement tier assignment by priority.
|
||||
- [x] Implement budget allocation and per-target limits.
|
||||
- [x] Implement parallel hunter scheduling.
|
||||
- [x] Persist hunter trajectories as JSONL.
|
||||
- [x] Integrate retrieval packages and selected skill snippets.
|
||||
- [x] Integrate verifier after hunter output.
|
||||
- [x] Add fixture scan demonstrating at least one verified static-corroborated finding.
|
||||
|
||||
## Phase 9: SARIF And CI Output (`usable_harness_mvp` prerequisite)
|
||||
|
||||
- [x] Implement SARIF report writer.
|
||||
- [x] Implement machine-readable manifest.
|
||||
- [x] Add severity, confidence, evidence level, model IDs, and artifact refs to reports.
|
||||
- [x] Add CI-friendly exit code policy.
|
||||
- [x] Add tests for SARIF schema validity.
|
||||
|
||||
## Gate: `sandboxed_dynamic_harness`
|
||||
|
||||
- [ ] Docker sandbox runs build/test/fuzz/PoC commands with no network by default.
|
||||
- [ ] Scoped dynamic probes require explicit declared targets and emit artifacts.
|
||||
- [ ] Sanitizer/fuzz artifacts can advance evidence only through valid transitions.
|
||||
|
||||
## Phase 10: Deep Mode (`sandboxed_dynamic_harness` prerequisite)
|
||||
|
||||
- [ ] Implement fuzzable target detection beyond existing fuzz entry points.
|
||||
- [ ] Implement fuzz harness generation prompt and schema.
|
||||
- [ ] Compile generated harnesses in sandbox.
|
||||
- [ ] Run libFuzzer or equivalent with sanitizer instrumentation.
|
||||
- [ ] Capture crashes and minimization artifacts.
|
||||
- [ ] Advance findings to `crash_reproduced` only with artifact evidence.
|
||||
- [ ] Add a small C/C++ parser fixture with reproducible sanitizer crash.
|
||||
|
||||
## Gate: `fix_validation_harness`
|
||||
|
||||
- [ ] Patch proposals are generated as diffs and not silently applied.
|
||||
- [ ] Fix lifecycle records intake, plan, implementation, adversarial review, reconciliation, verification, and ready states.
|
||||
- [ ] Patch validation requires sandboxed checks and accepted adversarial findings must be resolved.
|
||||
|
||||
## Phase 11: Patch Oracle (`fix_validation_harness` prerequisite)
|
||||
|
||||
- [ ] Implement patch proposal schema.
|
||||
- [ ] Implement OpenUltraCode-style fix lifecycle states: intake, plan, implement, adversarial_review, reconcile, verify, ready.
|
||||
- [ ] Run patching in a writable sandbox copy or git worktree.
|
||||
- [ ] Generate minimal diffs only.
|
||||
- [ ] Rerun reproducer and affected checks.
|
||||
- [ ] Run differential fix audit against the generated patch.
|
||||
- [ ] Run Semgrep or CodeQL delta checks when corresponding evidence exists.
|
||||
- [ ] Run sharp-edge fix audit when API, config, auth, crypto, or defaults are changed.
|
||||
- [ ] Advance findings to `patch_validated` only after validation passes.
|
||||
- [ ] Add tests that reject patches changing unrelated files.
|
||||
|
||||
## Gate: `opencode_product`
|
||||
|
||||
- [ ] Narrow MCP tools cover scan, status, findings, evidence, artifacts, patch proposal, and report export.
|
||||
- [ ] OpenCode workflows can drive scan, triage, fusion, and patch audit without arbitrary shell exposure.
|
||||
- [ ] Cost controls, retries, redaction, docs, examples, and CI gates are in place.
|
||||
- [ ] OpenCode workflows can drive benchmark runs, finding explanations, and interactive fix proposals.
|
||||
|
||||
## Phase 12: MCP And OpenCode Commands (`opencode_product` prerequisite)
|
||||
|
||||
Implemented in `mcp.py` (zero-dep stdio JSON-RPC server) + the `ousast mcp` CLI
|
||||
subcommand; tests in `tests/test_mcp.py`; README "MCP server" section documents it.
|
||||
|
||||
- [x] Add project-local opencode skills for scan, triage, fix audit, and implementation steering.
|
||||
- [x] Implement narrow MCP server.
|
||||
- [x] Add `openultrasast.scan`, `status`, `findings`, `get_finding`, `evidence`, `artifacts`, `benchmark`, `explain`, `propose_patch`, and `export_report` tools.
|
||||
- [x] Add opencode command docs for quick, standard, deep, verify, and patch workflows. (README; deep/patch noted as roadmap.)
|
||||
- [x] Ensure MCP tools never expose arbitrary shell execution. (Surface-safety test asserts no shell/exec/command tool.)
|
||||
- [x] Add integration test for MCP tool listing and scan status.
|
||||
|
||||
## Phase 13: Fusion (`standard_security_harness` and `opencode_product` prerequisite)
|
||||
|
||||
Implemented in `fusion.py` (zero-dep deterministic engine + lazy LLM panels), wired
|
||||
into the standard-mode CLI (`fusion.json` + manifest `fusion` block), config `[fusion]`.
|
||||
Tests in `tests/test_fusion.py`.
|
||||
|
||||
- [x] Implement fusion trigger policy.
|
||||
- [x] Trigger fusion whenever a task requires deeper reasoning, including difficult findings, contradictory evidence, high-impact decisions, and risky fixes.
|
||||
- [x] Implement panel A, panel B, and decider role config.
|
||||
- [x] Implement cross-critique and revised answer flow.
|
||||
- [x] Implement vote/rank output schema.
|
||||
- [x] Implement reconciliation dispositions.
|
||||
- [x] Record model IDs, degradations, warnings, and final decision source.
|
||||
- [x] Add tests for fusion output validation.
|
||||
|
||||
## Phase 14: Skill Index (`standard_security_harness` prerequisite)
|
||||
|
||||
Implemented in `skills.py` (descriptor inventory + scoring router + retrieval-namespace
|
||||
chunking + budget-capped context); the HarnessX hunter prompt consumes the routed
|
||||
skill context; tests in `tests/test_skills.py`.
|
||||
|
||||
- [x] Build Trail of Bits skill descriptor inventory.
|
||||
- [x] Chunk selected skill operating guidance into retrieval namespace.
|
||||
- [x] Implement skill router by language, tags, vulnerability class, and stage.
|
||||
- [x] Add prompt budget caps for skill snippets.
|
||||
- [x] Add tests for routing C parser targets to memory/fuzzing skills and crypto targets to constant-time/vector skills.
|
||||
|
||||
## Phase 15: Hardening And Release Readiness (`production` prerequisite)
|
||||
|
||||
Implemented: `redaction.py` (traces/reports), provider retry/backoff in
|
||||
`provider/openrouter.py`, `[hardening]` config (redact_secrets + max_findings budget;
|
||||
agentic spend bounded by `[harnessx]`), `docs/threat-model.md`, `docs/examples.md`.
|
||||
Tests in `tests/test_redaction.py`, `tests/test_provider_retry.py`,
|
||||
`tests/test_hardening_pipeline.py`. CI gates pre-existed (ruff/mypy/pytest/detection
|
||||
gate/dagger).
|
||||
|
||||
- [x] Add cost budget enforcement.
|
||||
- [x] Add model call retry and backoff policies.
|
||||
- [x] Add redaction for secrets in traces and reports.
|
||||
- [x] Add docs for threat model and sandbox limits.
|
||||
- [x] Add end-to-end examples.
|
||||
- [x] Add lint, typecheck, test, and CI gates.
|
||||
@@ -25,8 +25,8 @@ Fix the scorer and labels first (no engine change). Anchor harvest at the declar
|
||||
|
||||
## Scope
|
||||
|
||||
- **In**: scorer fidelity; label schema; harvest anchor and modes; corpus hygiene for sast; three new slices; dataset pointers; provenance fingerprint; per-profile and per-mechanism dashboard; per-profile improve-loop gate; offline tests.
|
||||
- **Out**: any change to taint, facts, CST walker, or dispositions (engine specs); tolerating tree-sitter ERROR nodes (engine); BaxBench generation and exploit-labelled slices (needs deep mode; later); profile-specific fact tables; making any dashboard a merge gate; vendoring SecBench.js test files (no license).
|
||||
- In: scorer fidelity; label schema; harvest anchor and modes; corpus hygiene for sast; three new slices; dataset pointers; provenance fingerprint; per-profile and per-mechanism dashboard; per-profile improve-loop gate; offline tests.
|
||||
- Out: any change to taint, facts, CST walker, or dispositions (engine specs); tolerating tree-sitter ERROR nodes (engine); BaxBench generation and exploit-labelled slices (needs deep mode; later); profile-specific fact tables; making any dashboard a merge gate; vendoring SecBench.js test files (no license).
|
||||
|
||||
## Boundary Candidates
|
||||
|
||||
@@ -42,14 +42,14 @@ Fix the scorer and labels first (no engine change). Anchor harvest at the declar
|
||||
|
||||
## Upstream / Downstream
|
||||
|
||||
- **Upstream**: pair catalog schema; overlay eval path; `build_pair_signals`; `run_improvement` accept gate; preprocess tags.
|
||||
- **Downstream**: `overlay-ir-completeness` and `guard-dominance-regime` measure against these slices and vocabularies; later `baxgen` slice reuses provenance and mechanism fields.
|
||||
- Upstream: pair catalog schema; overlay eval path; `build_pair_signals`; `run_improvement` accept gate; preprocess tags.
|
||||
- Downstream: `overlay-ir-completeness` and `guard-dominance-regime` measure against these slices and vocabularies; later `baxgen` slice reuses provenance and mechanism fields.
|
||||
|
||||
## Existing Spec Touchpoints
|
||||
|
||||
- **Supersedes**: `real-world-vfc-slice` (corpus/scorer plane). Its seed pairs, harvest script, and dual scoreboard are kept and extended.
|
||||
- **Extends**: roadmap direct candidate "record inventory vs overlay Youden side-by-side in sast JSON".
|
||||
- **Adjacent**: `propose-adjudicate-prove` (one additive field on `OverlayRecord`), `harnessx-self-improving-rulesets` (accept gate grows a per-profile clause, ledger format unchanged).
|
||||
- Supersedes: `real-world-vfc-slice` (corpus/scorer plane). Its seed pairs, harvest script, and dual scoreboard are kept and extended.
|
||||
- Extends: roadmap direct candidate "record inventory vs overlay Youden side-by-side in sast JSON".
|
||||
- Adjacent: `propose-adjudicate-prove` (one additive field on `OverlayRecord`), `harnessx-self-improving-rulesets` (accept gate grows a per-profile clause, ledger format unchanged).
|
||||
|
||||
## Constraints
|
||||
|
||||
|
||||
@@ -172,11 +172,11 @@ tests/
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `src/openultrasast/pairs.py` — see above; `OVERLAY_SLICES` grows; `PairCase` gains `provenance`, `mechanism` (on expected), `split`, `known_limit`.
|
||||
- `src/openultrasast/semantic/overlay.py` — fill `function` from the `FunctionIR` whose range contains the record line; default `None`.
|
||||
- `src/openultrasast/improve/evolve.py` — `_Eval` computes `per_profile` on holdout; new reject reason `profile_regression:<profile>`.
|
||||
- `src/openultrasast/preprocess.py`, `reports.py`, `cli.py` — wiring only.
|
||||
- `benchmarks/pairs/README.md`, `.kiro/steering/roadmap.md` — slice list and spec order.
|
||||
- `src/openultrasast/pairs.py`: see above; `OVERLAY_SLICES` grows; `PairCase` gains `provenance`, `mechanism` (on expected), `split`, `known_limit`.
|
||||
- `src/openultrasast/semantic/overlay.py`: fill `function` from the `FunctionIR` whose range contains the record line; default `None`.
|
||||
- `src/openultrasast/improve/evolve.py`: `_Eval` computes `per_profile` on holdout; new reject reason `profile_regression:<profile>`.
|
||||
- `src/openultrasast/preprocess.py`, `reports.py`, `cli.py`: wiring only.
|
||||
- `benchmarks/pairs/README.md`, `.kiro/steering/roadmap.md`: slice list and spec order.
|
||||
|
||||
## System Flows
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ Engineers evolving OpenUltraSAST's overlay cannot trust the pair scoreboard: cov
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: pair scorer fidelity; catalog label schema (function, mechanism, provenance, known limits, held-out split); harvest anchoring and extraction modes; sast corpus hygiene; `vibe-py`, `vfc-js`, `agent-vfc` slices; dataset pointers; deterministic provenance fingerprint at scan time; per-profile and per-mechanism dashboard; per-profile acceptance in the improve loop; offline tests.
|
||||
- **Out of scope**: taint, facts, CST walker, dispositions, or tolerance of parser ERROR nodes (engine specs); BaxBench generation slices; profile-specific fact tables or thresholds; any new merge gate; vendoring files without a stated license; LLM-based classification.
|
||||
- **Adjacent expectations**: `local` stays the 100% pair gate; the stage-1 smoke gate stays inventory-only; `OverlayRecord` may gain one optional additive field; the improve loop's existing accept gate (recall floor, FP ceiling, score, no matched regression) stays and grows one clause; ledger file formats do not change.
|
||||
- In scope: pair scorer fidelity; catalog label schema (function, mechanism, provenance, known limits, held-out split); harvest anchoring and extraction modes; sast corpus hygiene; `vibe-py`, `vfc-js`, `agent-vfc` slices; dataset pointers; deterministic provenance fingerprint at scan time; per-profile and per-mechanism dashboard; per-profile acceptance in the improve loop; offline tests.
|
||||
- Out of scope: taint, facts, CST walker, dispositions, or tolerance of parser ERROR nodes (engine specs); BaxBench generation slices; profile-specific fact tables or thresholds; any new merge gate; vendoring files without a stated license; LLM-based classification.
|
||||
- Adjacent expectations: `local` stays the 100% pair gate; the stage-1 smoke gate stays inventory-only; `OverlayRecord` may gain one optional additive field; the improve loop's existing accept gate (recall floor, FP ceiling, score, no matched regression) stays and grows one clause; ledger file formats do not change.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -23,7 +23,7 @@ Engineers evolving OpenUltraSAST's overlay cannot trust the pair scoreboard: cov
|
||||
1. When an overlay slice pair is scored, the system shall count a `coverage` record that carries a source-to-sink flow as a detection on the vulnerable side and as a leak on the fixed side, symmetrically with `promote`.
|
||||
2. When an expected row names a function, the system shall match a promotion or coverage record only if the record lies inside that function's line range.
|
||||
3. When an expected row names neither a rule id nor a sink nor a function, the system shall not count a CWE-only match as a detection and shall flag the row as `weak_label` in the payload.
|
||||
4. When any overlay slice is evaluated, the system shall report inventory and overlay metrics side by side for that slice, not only for `vfc`.
|
||||
4. When any overlay slice is evaluated, the system shall report inventory and overlay metrics side by side for that slice as well as for `vfc`.
|
||||
5. When a pair is evaluated, the system shall report how many files on each side were `parse_failed` and how many proposals were `unadjudicated`, so parser loss is visible on the dashboard.
|
||||
6. The system shall keep `local` as the only slice whose result can fail CI.
|
||||
7. Where a hunter model is configured, the system shall report a third scorer, `hunter`, that runs the tool hunter on each isolated pair function and scores it with the same pair rules, so the LLM plane is measured on the same corpus; absence of a model shall skip the scorer with a recorded degradation.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Summary
|
||||
|
||||
- **Feature**: `pair-corpus-honesty`
|
||||
- **Discovery Scope**: Extension of the pair catalog, scorer, and harvest; supersedes `real-world-vfc-slice`
|
||||
- **Key Findings**:
|
||||
- Feature: `pair-corpus-honesty`
|
||||
- Discovery Scope: Extension of the pair catalog, scorer, and harvest; supersedes `real-world-vfc-slice`
|
||||
- Key Findings:
|
||||
- The overlay scorer ignores `coverage`. The Java sqli vuln file already produces a coverage record on `prepareCall` from a servlet source; the scorer reports MISS.
|
||||
- The overlay column equals the inventory column on all 176 vfc pairs because C facts have no memory sink, so every proposal is `flow_incomplete` and the scorer falls back to inventory. The dashboard cannot show engine progress until the scorer reports parser and adjudication loss explicitly.
|
||||
- Harvest anchors on the first textual occurrence of the function name. For curl that is the doc comment mention (`Curl_follow()`), so 25 pairs start mid-comment and fail to parse.
|
||||
@@ -18,23 +18,23 @@
|
||||
|
||||
### Scorer blind spots
|
||||
|
||||
- **Context**: Why the sast overlay number did not move after the tree-sitter extra.
|
||||
- **Sources Consulted**: `src/openultrasast/pairs.py` (`_evaluate_overlay_pair`, `_overlay_matches_expected`, `_overlay_leaks`), `semantic/overlay.py`, direct runs of `adjudicate` on the sast files.
|
||||
- **Findings**: only `promote` is counted; `coverage` is excluded on both sides. `_overlay_matches_expected` accepts a CWE-only match. `ExpectedFinding` already has `function` and `line`, and `benchmark._matches_expected` honors them, but the overlay path does not. `OverlayRecord` has no function field; `FunctionIR` has line ranges.
|
||||
- **Implications**: count coverage symmetrically; add optional `function` to `OverlayRecord`; require function or sink or rule id for a match when present; flag CWE-only rows.
|
||||
- Context: Why the sast overlay number did not move after the tree-sitter extra.
|
||||
- Sources Consulted: `src/openultrasast/pairs.py` (`_evaluate_overlay_pair`, `_overlay_matches_expected`, `_overlay_leaks`), `semantic/overlay.py`, direct runs of `adjudicate` on the sast files.
|
||||
- Findings: only `promote` is counted; `coverage` is excluded on both sides. `_overlay_matches_expected` accepts a CWE-only match. `ExpectedFinding` already has `function` and `line`, and `benchmark._matches_expected` honors them, but the overlay path does not. `OverlayRecord` has no function field; `FunctionIR` has line ranges.
|
||||
- Implications: count coverage symmetrically; add optional `function` to `OverlayRecord`; require function or sink or rule id for a match when present; flag CWE-only rows.
|
||||
|
||||
### Harvest anchor defect
|
||||
|
||||
- **Context**: 50 vfc files `parse_failed`.
|
||||
- **Sources Consulted**: `benchmarks/pairs/vfc/harvest.py` `extract_function`; tree-sitter ERROR nodes on `curl-cve-2022-27774/vuln.c` and `curl-cve-2023-38545-do_socks5/vuln.c`.
|
||||
- **Findings**: the name search accepts a match followed by `(` even inside a comment, and the brace search then finds the real body. A second, smaller class is `#if`-split `else if` chains, which tree-sitter cannot attach; that is an engine tolerance question, not harvest.
|
||||
- **Implications**: skip comment and string regions before searching; anchor on a line that starts a declarator; add line-range and hunk modes so Python and JS/TS are covered; report parse loss on the dashboard so the engine spec can measure its tolerance work.
|
||||
- Context: 50 vfc files `parse_failed`.
|
||||
- Sources Consulted: `benchmarks/pairs/vfc/harvest.py` `extract_function`; tree-sitter ERROR nodes on `curl-cve-2022-27774/vuln.c` and `curl-cve-2023-38545-do_socks5/vuln.c`.
|
||||
- Findings: the name search accepts a match followed by `(` even inside a comment, and the brace search then finds the real body. A second, smaller class is `#if`-split `else if` chains, which tree-sitter cannot attach; that is an engine tolerance question, not harvest.
|
||||
- Implications: skip comment and string regions before searching; anchor on a line that starts a declarator; add line-range and hunk modes so Python and JS/TS are covered; report parse loss on the dashboard so the engine spec can measure its tolerance work.
|
||||
|
||||
### Candidate corpora
|
||||
|
||||
- **Context**: User asked for vibe-coded and Node/TypeScript/Python web corpora.
|
||||
- **Sources Consulted**: Real-Vuln-Benchmark repo and paper; SecBench.js repo and ICSE 2023 paper; AIDev dataset card and paper; BaxBench site, repo, and paper; SecurityEval, CodeSecEval, CWEval; CyberSecEval benchmarks README; VibeApps paper; TS-VulBench page; SecureVibeBench repo; GitHub commit search via `gh api search/commits`.
|
||||
- **Findings**:
|
||||
- Context: User asked for vibe-coded and Node/TypeScript/Python web corpora.
|
||||
- Sources Consulted: Real-Vuln-Benchmark repo and paper; SecBench.js repo and ICSE 2023 paper; AIDev dataset card and paper; BaxBench site, repo, and paper; SecurityEval, CodeSecEval, CWEval; CyberSecEval benchmarks README; VibeApps paper; TS-VulBench page; SecureVibeBench repo; GitHub commit search via `gh api search/commits`.
|
||||
- Findings:
|
||||
| Corpus | Languages | Labels | Fixed twin | License | Verdict |
|
||||
|---|---|---|---|---|---|
|
||||
| Real-Vuln-Benchmark | Python web | file, line range, function, CWEs, authorship, FP traps | traps | Apache-2.0 | `vibe-py` |
|
||||
@@ -46,14 +46,14 @@
|
||||
| VibeApps/VibeVulns | TS, JS, React | not released | no | n/a | method only |
|
||||
| TS-VulBench | TS synthetic | paywalled | no | unknown | skip |
|
||||
| SecureVibeBench | C/C++ | agent tasks | n/a | MIT | skip for web |
|
||||
- **Implications**: three slices now; BaxBench after deep mode is stable; CyberSecEval never as recall ground truth.
|
||||
- Implications: three slices now; BaxBench after deep mode is stable; CyberSecEval never as recall ground truth.
|
||||
|
||||
### Classification and branching
|
||||
|
||||
- **Context**: User expects tuning to split into branches per corpus class.
|
||||
- **Sources Consulted**: `improve/evolve.py` accept gate; `ruleset/store.py` ledger; `calibration.py` scope; `preprocess.detect_tags`; `build_pair_signals`.
|
||||
- **Findings**: the accept gate already ANDs four clauses and reverts byte-for-byte. Pair signals carry pair name but no profile. Ledger is one file per target. Preprocess tags are deterministic and per file.
|
||||
- **Implications**: classify on two axes (provenance for gating, mechanism for reporting). Branch the acceptance decision, not the ledger format: reject a change if any profile's holdout regresses. Profile-specific facts and thresholds are deferred to the engine specs where those data live.
|
||||
- Context: User expects tuning to split into branches per corpus class.
|
||||
- Sources Consulted: `improve/evolve.py` accept gate; `ruleset/store.py` ledger; `calibration.py` scope; `preprocess.detect_tags`; `build_pair_signals`.
|
||||
- Findings: the accept gate already ANDs four clauses and reverts byte-for-byte. Pair signals carry pair name but no profile. Ledger is one file per target. Preprocess tags are deterministic and per file.
|
||||
- Implications: classify on two axes (provenance for gating, mechanism for reporting). Branch the acceptance decision, not the ledger format: reject a change if any profile's holdout regresses. Profile-specific facts and thresholds are deferred to the engine specs where those data live.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
@@ -68,48 +68,48 @@
|
||||
|
||||
### Decision: Count coverage-with-flow as detection and as leak
|
||||
|
||||
- **Context**: Sink aliases will always land as coverage.
|
||||
- **Alternatives Considered**: promote coverage to `promote` in the engine; count coverage only on the vulnerable side.
|
||||
- **Selected Approach**: scorer treats `coverage` with a non-empty source the same as `promote` on both sides.
|
||||
- **Rationale**: `propose-adjudicate-prove` already states coverage enters the same state machine. Asymmetry would inflate recall.
|
||||
- **Trade-offs**: some pairs move from MISS to LEAK; that is the honest reading.
|
||||
- Context: Sink aliases will always land as coverage.
|
||||
- Alternatives Considered: promote coverage to `promote` in the engine; count coverage only on the vulnerable side.
|
||||
- Selected Approach: scorer treats `coverage` with a non-empty source the same as `promote` on both sides.
|
||||
- Rationale: `propose-adjudicate-prove` already states coverage enters the same state machine. Asymmetry would inflate recall.
|
||||
- Trade-offs: some pairs move from MISS to LEAK; that is the honest reading.
|
||||
|
||||
### Decision: Function-scoped matching with weak-label flagging
|
||||
|
||||
- **Context**: CWE-only matching over-counts.
|
||||
- **Selected Approach**: expected rows carry `function`; match requires the record inside that function's lines; rows with no function, sink, or rule id are `weak_label` and never match by CWE alone.
|
||||
- **Trade-offs**: vfc recall may drop on first run; the drop is real.
|
||||
- Context: CWE-only matching over-counts.
|
||||
- Selected Approach: expected rows carry `function`; match requires the record inside that function's lines; rows with no function, sink, or rule id are `weak_label` and never match by CWE alone.
|
||||
- Trade-offs: vfc recall may drop on first run; the drop is real.
|
||||
|
||||
### Decision: Provenance is deterministic and closed
|
||||
|
||||
- **Context**: No LLM in classification.
|
||||
- **Selected Approach**: signals from `.claude/`, `CLAUDE.md`, `AGENTS.md`, `.cursor/`, `.github/copilot-instructions.md`, Lovable meta tag, "Generated with Claude Code", and co-author trailers for Claude, Codex, Copilot, Cursor, Devin, Jules when a git log is readable. Threshold for `agent` on a git tree: at least 85% of commits or lines in the last N commits carry a signal, following the VibeApps method; otherwise `mixed` when any signal exists; `human` when none; `synthetic` only from catalog declaration.
|
||||
- **Trade-offs**: repos that scrub trailers read as human. Acceptable; the label is a profile, not a verdict.
|
||||
- Context: No LLM in classification.
|
||||
- Selected Approach: signals from `.claude/`, `CLAUDE.md`, `AGENTS.md`, `.cursor/`, `.github/copilot-instructions.md`, Lovable meta tag, "Generated with Claude Code", and co-author trailers for Claude, Codex, Copilot, Cursor, Devin, Jules when a git log is readable. Threshold for `agent` on a git tree: at least 85% of commits or lines in the last N commits carry a signal, following the VibeApps method; otherwise `mixed` when any signal exists; `human` when none; `synthetic` only from catalog declaration.
|
||||
- Trade-offs: repos that scrub trailers read as human. Acceptable; the label is a profile, not a verdict.
|
||||
|
||||
### Decision: Branch the accept decision, not the data
|
||||
|
||||
- **Context**: Overfitting risk with per-slice tuning.
|
||||
- **Selected Approach**: `run_improvement` evaluates holdout pairs per profile; any profile regression beyond tolerance rejects. Profiles capped at four. Minimum holdout size reported.
|
||||
- **Follow-up**: profile-keyed facts land with the engine specs, gated by this clause.
|
||||
- Context: Overfitting risk with per-slice tuning.
|
||||
- Selected Approach: `run_improvement` evaluates holdout pairs per profile; any profile regression beyond tolerance rejects. Profiles capped at four. Minimum holdout size reported.
|
||||
- Follow-up: profile-keyed facts land with the engine specs, gated by this clause.
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
- Recall numbers drop after function-scoped matching and weak-label flags — expected; record before and after in the roadmap.
|
||||
- SecBench.js has no license — never vendor its files; harvest from upstream package repos and record each package license.
|
||||
- Agent-authored commits may be false labels (a "fix" that was not a vuln) — human review required; keep `agent-vfc` small.
|
||||
- Real-Vuln-Benchmark apps are educational and CTF style — say so in the slice README; do not treat `vibe-py` as production.
|
||||
- Held-out halves are small on day one — report minimum-size warnings; do not gate on a profile under the minimum.
|
||||
- Recall numbers drop after function-scoped matching and weak-label flags; expected; record before and after in the roadmap.
|
||||
- SecBench.js has no license; never vendor its files; harvest from upstream package repos and record each package license.
|
||||
- Agent-authored commits may be false labels (a "fix" that was not a vuln); human review required; keep `agent-vfc` small.
|
||||
- Real-Vuln-Benchmark apps are educational and CTF style; say so in the slice README; do not treat `vibe-py` as production.
|
||||
- Held-out halves are small on day one; report minimum-size warnings; do not gate on a profile under the minimum.
|
||||
|
||||
## References
|
||||
|
||||
- [Real-Vuln-Benchmark](https://github.com/kolega-ai/Real-Vuln-Benchmark) — Apache-2.0, manifest with pinned SHAs, ground-truth schema
|
||||
- [RealVuln paper](https://arxiv.org/abs/2604.13764) — 26 human plus 40 LLM-generated Python repos, FP traps
|
||||
- [SecBench.js](https://github.com/cristianstaicu/SecBench.js) — 600 npm vulns, `fixCommit`, `sinkLocation`
|
||||
- [Real-Vuln-Benchmark](https://github.com/kolega-ai/Real-Vuln-Benchmark): Apache-2.0, manifest with pinned SHAs, ground-truth schema
|
||||
- [RealVuln paper](https://arxiv.org/abs/2604.13764): 26 human plus 40 LLM-generated Python repos, FP traps
|
||||
- [SecBench.js](https://github.com/cristianstaicu/SecBench.js): 600 npm vulns, `fixCommit`, `sinkLocation`
|
||||
- [SecBench.js paper](https://software-lab.org/publications/icse2023_SecBenchJS.pdf)
|
||||
- [AIDev dataset](https://huggingface.co/datasets/hao-li/AIDev) — agent-authored PRs, CC-BY-4.0 index
|
||||
- [BaxBench](https://github.com/logic-star-ai/baxbench) — MIT, scenarios and exploits, generation by CLI
|
||||
- [AIDev dataset](https://huggingface.co/datasets/hao-li/AIDev): agent-authored PRs, CC-BY-4.0 index
|
||||
- [BaxBench](https://github.com/logic-star-ai/baxbench): MIT, scenarios and exploits, generation by CLI
|
||||
- [SecurityEval](https://github.com/s2e-lab/SecurityEval), [CodeSecEval](https://arxiv.org/pdf/2407.02395), [CWEval](https://github.com/Co1lin/CWEval)
|
||||
- [CyberSecEval](https://github.com/meta-llama/PurpleLlama/tree/main/CybersecurityBenchmarks) — labels defined by ICD rules
|
||||
- [VibeApps paper](https://arxiv.org/html/2606.23130v1) — identification method
|
||||
- `.kiro/specs/real-world-vfc-slice/` — superseded corpus spec
|
||||
- Memory note `overlay-gaps-diagnosis-2026-09` — measured state and engine gaps
|
||||
- [CyberSecEval](https://github.com/meta-llama/PurpleLlama/tree/main/CybersecurityBenchmarks): labels defined by ICD rules
|
||||
- [VibeApps paper](https://arxiv.org/html/2606.23130v1): identification method
|
||||
- `.kiro/specs/real-world-vfc-slice/`: superseded corpus spec
|
||||
- Memory note `overlay-gaps-diagnosis-2026-09`: measured state and engine gaps
|
||||
|
||||
@@ -206,25 +206,25 @@ URL; the CLI mints them and polls the store. The URLs travel in the start body,
|
||||
"inputs": {"CANDIDATES": "<presigned GET runs/<run>/<producer>/<artifact>>"}}}
|
||||
```
|
||||
|
||||
- **Why the start body.** A presigned URL is a bearer capability, so the credentials-only-in-the-start-request
|
||||
- Why the start body. A presigned URL is a bearer capability, so the credentials-only-in-the-start-request
|
||||
rule applies; env would bake it into the ActorTemplate and hand it to the golden boot; and the body has no
|
||||
32 KB ceiling (Substrate rejects env values over 32768 characters, `reconciler.py:141`). `StartGate.offer`
|
||||
(`runner.py:250-275`) validates `delivery` the way it validates `credentials`: `https` URLs only, hostnames,
|
||||
no echo. Expiry = `OUSAST_TASK_TIMEOUT` (7200 s) + 600 s; a re-start after a re-resume re-mints.
|
||||
- **Why presigned rather than a scoped delivery credential.** A per-task S3 key needs a per-task policy on RustFS,
|
||||
- Why presigned rather than a scoped delivery credential. A per-task S3 key needs a per-task policy on RustFS,
|
||||
gives the sandbox `ListBucket` and reads beyond its inputs, and would travel in the same body. Presigned URLs
|
||||
name exactly the objects a task may write and read, which is what the receiver enforces today
|
||||
(`Receiver.input_file`, `egress.py:230-247`).
|
||||
- **Why not an in-cluster receiver Service.** It is a server to run, holds state in a process, and is reachable
|
||||
- Why not an in-cluster receiver Service. It is a server to run, holds state in a process, and is reachable
|
||||
only from a pod; that is the gap remote mode has. **Rejected.**
|
||||
- **Objects.** `runs/<run>/<task>/output.tar` (the runner's PUT); after the CLI extracts it, the declared
|
||||
- Objects. `runs/<run>/<task>/output.tar` (the runner's PUT); after the CLI extracts it, the declared
|
||||
`outputs[]` are re-put as `runs/<run>/<task>/<artifact>` so a consumer's presigned GET names one artifact, and
|
||||
`runs/<run>/<task>/done.json` carries the summary's status. The 30-day `runs/` expiry already applies; memory
|
||||
rows are ingested from the local extraction by `remember` as today (f: annotations and `memory_key` seeding
|
||||
unchanged, `generate.py:_chain_inputs`, `memory.seed`).
|
||||
- **Completion.** `_await`'s `delivered()` becomes `Delivery.delivered(task)`: a HEAD on `output.tar`, polled at
|
||||
- Completion. `_await`'s `delivered()` becomes `Delivery.delivered(task)`: a HEAD on `output.tar`, polled at
|
||||
`OUSAST_POLL_SECONDS` (3 s). The runner's completion marker logic (`runner.py`, `_run_once`) is unchanged.
|
||||
- **Inputs no task produces** (`candidates.json`, `functions.json`, `case.json`) may also be store objects
|
||||
- Inputs no task produces (`candidates.json`, `functions.json`, `case.json`) may also be store objects
|
||||
declared as inputs, which lifts the ~20 KB inline-files limit (`ops/ax/README.md:117`); Workspace `files`
|
||||
stay for the smoke Task and small cases.
|
||||
|
||||
@@ -251,7 +251,7 @@ shim and `/workspace`), so one *contract* in two images: `ousast-runner` (slim,
|
||||
2.8 GB and every golden snapshot carry it; the engine runs for a minority of cases); Docker-in-pod (no socket on
|
||||
containerd; `Dockerfile:1-7` already argues against it).
|
||||
|
||||
- **Task.** Template `plane/tasks/engine.yaml`: `command: ["engine"]`, env `OUSAST_WORKSPACE_DIR`,
|
||||
- Task. Template `plane/tasks/engine.yaml`: `command: ["engine"]`, env `OUSAST_WORKSPACE_DIR`,
|
||||
`OUSAST_FIXED_DIR`, `OUSAST_INPUT_CASE`, pins and `OUSAST_FAMILIES`, bound to the same two Workspaces the
|
||||
`alerts` task binds (`generate.py:342-353`). The module lifts `benchmarks/push/finding_dump.py`'s scan into
|
||||
`openultrasast.cpg.dump` (the image ships `src/`, not `benchmarks/`) and reuses `engine_alerts.engine_rows`,
|
||||
@@ -259,7 +259,7 @@ containerd; `Dockerfile:1-7` already argues against it).
|
||||
`alerts.jsonl` rows (`rule_id: engine:<family>`, `source: "engine"`) and `summary.json` (coverage with the
|
||||
engine's languages, files and bytes read, questions, seconds, degradations per pin) as `alerts-engine`, with
|
||||
`host: false`. The instrument rule stays: no `read N files` line or `questions == 0` fails the task loudly.
|
||||
- **Sizing from the record** (`benchmarks/measurements/2026-09-30-php-engine-alerts/record.json`): container
|
||||
- Sizing from the record (`benchmarks/measurements/2026-09-30-php-engine-alerts/record.json`): container
|
||||
peak 2.129 GiB under a 3 GiB limit, 789 s for both pins of froxlor, deadline 1800 s per pin. Task resources:
|
||||
requests 3 Gi / 1 CPU, limits 4 Gi / 2 CPU; `engine-pool` workers 4 Gi / 2 CPU, replicas 1 on kind (the 7 GB
|
||||
host holds it only with `ousast-pool` at one worker), production replicas in `k3s.toml`. Task timeout: two pins
|
||||
@@ -314,23 +314,23 @@ runbook: a Run with `--workers N` beyond the pool's replicas queues in ax as Sus
|
||||
|
||||
`plane/scan.py`, reusing `generate.py`'s render path with a new case source:
|
||||
|
||||
1. **Workspace generation** (`generate.scan_cases(url, base, head, cache)`): a shallow clone into the case cache
|
||||
1. Workspace generation (`generate.scan_cases(url, base, head, cache)`): a shallow clone into the case cache
|
||||
(`runner.find_cached_checkout` layout), two pinned Workspaces `<repo_dir>-<head7>` and `<repo_dir>-<base7>`
|
||||
(`workspaces.py` shape, pins in `openultrasast.io/git-commits`), repository key by `memory.repo_key`.
|
||||
2. **Candidates from changed functions**: `generate.fix_ranges(repo, base, head, side="new")` gives the head-side
|
||||
2. Candidates from changed functions: `generate.fix_ranges(repo, base, head, side="new")` gives the head-side
|
||||
hunks (the `push/` delta primitive `_diff_records` is the same `git diff -U0`), `repo_facts.enclosing` names
|
||||
the function per hunk line, filtered to `repo_facts.product_files`. Quick rules on the changed files
|
||||
(`tasks.alerts.scan`) add alerted functions; `roles` runs once per repository when memory has no `roles` rows
|
||||
for it; `repo-facts` supplies callers. `candidates.json` and `functions.json` are store inputs (section 3).
|
||||
3. **Run composition**: per changed-function set `facts -> va, vb -> agree -> vc -> final -> features ->
|
||||
3. Run composition: per changed-function set `facts -> va, vb -> agree -> vc -> final -> features ->
|
||||
remember`, plus `engine` when a changed file's language is engine-covered and not quick-covered, or PHP
|
||||
(`ENGINE_ALONGSIDE_QUICK`). Budgets from `generate.verify_budget` (0.022 USD per candidate, headroom 1.5);
|
||||
`--ceiling` caps the sum. `--all` (whole repository, secondary) takes every product function.
|
||||
4. **Results** land in memory under `repos/<host>__<owner>__<name>/<head>.jsonl` through `remember` and
|
||||
4. Results land in memory under `repos/<host>__<owner>__<name>/<head>.jsonl` through `remember` and
|
||||
`ousast plane remember` (rows already carry `repo` and `pin`, `memory.py:77`); `runs/<run>/` holds the raw
|
||||
artifacts for 30 days. The report prints the attribution table and the plane's agreement (`final`); the
|
||||
decision engine's verdict only when a model is adopted (learned-decision-engine Req 6 unchanged).
|
||||
5. **Latency split (e).** `ousast pre-push` stays the blocking hook: quick rules and the delta engine inside
|
||||
5. Latency split (e). `ousast pre-push` stays the blocking hook: quick rules and the delta engine inside
|
||||
`OUSAST_PUSH_DEADLINE` (30 s, `ops/README.md:322`). `ousast plane scan --exec remote --no-wait` submits and
|
||||
prints the Run name in the hook's report; `--wait SECONDS` blocks for CI. `ousast plane status <run>` reports
|
||||
later. The wrapper `ops/pre-push` gains an opt-in `OUSAST_PLANE_SCAN=1`; it never blocks on the plane.
|
||||
@@ -378,7 +378,7 @@ template names `imagePullSecret` from the profile.
|
||||
- `tests/test_plane_engine_task.py`: fixture record -> rows and summary equal `engine_alerts.produce`'s shape;
|
||||
the instrument failures.
|
||||
- `tests/test_plane_scan.py`: candidates from a two-commit fixture repository; Run composition; budgets.
|
||||
- **Only live on kind:** placement on `engine-pool`; Substrate accepting the SA token for `egress-policy`;
|
||||
- Only live on kind: placement on `engine-pool`; Substrate accepting the SA token for `egress-policy`;
|
||||
the gateway passing TLS to the store host; GHCR pulls; the HPA (k3s only). These are the kind proofs above.
|
||||
|
||||
## Data Models
|
||||
@@ -411,16 +411,16 @@ from `StartGate.offer`. The engine task fails loudly on an unread input, as `rea
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Substrate authorization for the CI token.** `kubectl-ate` sends a ServiceAccount token; which audience and
|
||||
1. Substrate authorization for the CI token. `kubectl-ate` sends a ServiceAccount token; which audience and
|
||||
which Substrate-side grant let `ousast-ci` create `egress-policy` in `default` is not in the checkouts read
|
||||
here. Proven or refuted in increment 3.
|
||||
2. **Engine placement.** Whether Substrate places a 3 Gi actor only on workers that fit it (section 4). The
|
||||
2. Engine placement. Whether Substrate places a 3 Gi actor only on workers that fit it (section 4). The
|
||||
fallback is named.
|
||||
3. **gVisor tarball reachability on k3s** (`gs://gvisor/...` in the SandboxConfig): mirror or egress needed.
|
||||
4. **Store endpoint hostname** for any future in-cluster RustFS; today's external endpoint is fine.
|
||||
5. **Two images under one contract** versus the maintainer's "one runner image for all tasks": the design
|
||||
3. gVisor tarball reachability on k3s (`gs://gvisor/...` in the SandboxConfig): mirror or egress needed.
|
||||
4. Store endpoint hostname for any future in-cluster RustFS; today's external endpoint is fine.
|
||||
5. Two images under one contract versus the maintainer's "one runner image for all tasks": the design
|
||||
reads it as one contract; confirm.
|
||||
6. **GHCR visibility** (public packages need no pull secret) and whether Substrate's and ax's `ko` images go to
|
||||
6. GHCR visibility (public packages need no pull secret) and whether Substrate's and ax's `ko` images go to
|
||||
the same registry namespace.
|
||||
7. **Reconciler size**: remote mode and `Delivery` must stay under the 500-line test; if they do not, the test's
|
||||
7. Reconciler size: remote mode and `Delivery` must stay under the 500-line test; if they do not, the test's
|
||||
bound moves with a reason, not silently.
|
||||
|
||||
@@ -9,24 +9,24 @@ production entry point (a delta Run for a push) is added. Context: `brief.md`, `
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: two profiles of one code base, `kind` (local, kept) and `k3s` (production); a remote execution mode of the CLI; artifacts delivered to the store; configurable
|
||||
- In scope: two profiles of one code base, `kind` (local, kept) and `k3s` (production); a remote execution mode of the CLI; artifacts delivered to the store; configurable
|
||||
cluster context, registry and image pins; our own snapshot and memory buckets; the Joern engine as a plane task
|
||||
on a larger worker pool; worker pool sizing and autoscaling; a delta Run command and Workspaces from URL +
|
||||
commit; a deployment runbook with a proof on kind and a checklist for the target cluster.
|
||||
- **Out of scope**: provisioning or operating the target cluster; modifying Agent Substrate or ax; any change to
|
||||
- Out of scope: provisioning or operating the target cluster; modifying Agent Substrate or ax; any change to
|
||||
detection, scoring, populations or the decision engine; replacing the `ax` CLI with a bespoke client beyond what
|
||||
remote submission needs.
|
||||
- **Adjacent expectations**: `ai-service-plane` requirements (one Model + budget per task, attribution,
|
||||
- Adjacent expectations: `ai-service-plane` requirements (one Model + budget per task, attribution,
|
||||
credentials only in the start request, egress policies, verify-at-startup memory store) keep holding;
|
||||
`harnessx-removal` Req 6 (memory on the S3 store) keeps holding; nothing is deleted before its replacement
|
||||
passes its test (the `harnessx-removal` sequencing rule).
|
||||
|
||||
## Target Environment (maintainer, 2026-10-02)
|
||||
|
||||
- **Cluster: k3s.** Consequences: containerd (no Docker socket; the engine's host-Docker path does not exist there),
|
||||
- Cluster: k3s. Consequences: containerd (no Docker socket; the engine's host-Docker path does not exist there),
|
||||
gVisor needs `runsc` and a `RuntimeClass` installed on the nodes Substrate's workers run on, k3s ships Traefik
|
||||
and local-path storage, and single-node k3s is close enough to kind that the kind proof transfers.
|
||||
- **Images and source: GitHub.** Images are published to GitHub Container Registry (`ghcr.io/norandom/...`),
|
||||
- Images and source: GitHub. Images are published to GitHub Container Registry (`ghcr.io/norandom/...`),
|
||||
built by GitHub Actions from tagged commits, digest-pinned in the manifests; private packages need an
|
||||
`imagePullSecret` on the worker ServiceAccounts and the CI principal. Substrate's and ax's own images are built with
|
||||
`ko` from their checkouts and pushed to the same registry.
|
||||
|
||||
@@ -69,7 +69,7 @@ family semantics remain with their existing specs rather than being reimplemente
|
||||
- No promise of all-language coverage; capabilities are admitted per language/framework/family.
|
||||
- Initial work must establish feasibility and noise before investing in an LLM ranker.
|
||||
|
||||
## Cross-language amendment — 2026-09-12
|
||||
## Cross-language amendment, 2026-09-12
|
||||
|
||||
Keep PMPro as the primary worked optimization example, and require a JavaScript/Express/Node API
|
||||
transfer experiment before claiming the core improvement generalizes. Abstract evidence, ranking,
|
||||
|
||||
@@ -24,7 +24,7 @@ an irrelevant old defect, or an unactionable witness can still produce noise.
|
||||
- Do not build a second ranker/scope heuristic, an incremental CPG engine, a daemon, an automatic fixer,
|
||||
or a mandatory model service. Do not scan intermediate pushed history in this feature.
|
||||
|
||||
## What this hook is, and is not — 2026-09-21
|
||||
## What this hook is, and is not, 2026-09-21
|
||||
|
||||
A general-purpose pre-commit hook, not an application-specific one. It may be measured on WordPress
|
||||
plugins and Node applications, and it may not become a tool that only works on them. The practical
|
||||
@@ -63,8 +63,8 @@ The fourth and fifth cost the declared v0.1 target its entire admissibility: a W
|
||||
PHP plus admin JavaScript, so the product could detect defects on its own target and never admit
|
||||
one. Neither passing subject could reveal it, because both hold one language.
|
||||
|
||||
**In review:** every new degradation reason must state the scope it reaches — one question, one file,
|
||||
one partition, or the whole read — and a reason that demotes an outcome must justify reaching that
|
||||
**In review:** every new degradation reason must state the scope it reaches (one question, one file,
|
||||
one partition, or the whole read), and a reason that demotes an outcome must justify reaching that
|
||||
far. A gap that costs coverage without threatening correctness is a declared exclusion: it stays
|
||||
published and it does not demote.
|
||||
|
||||
@@ -462,7 +462,7 @@ decision in the terminal; do not fabricate a successful cache entry or hang whil
|
||||
separate from a future C/C++ positive control. See `evaluation.md` for selection and labeling gates.
|
||||
|
||||
Measure the accepted experimental acceptance profile in requirements on declared hardware and fixed workloads.
|
||||
Warm means prior reusable work from the preceding state, not only repeating the identical tip.
|
||||
Warm means prior reusable work from the preceding state. Repeating the identical tip alone is insufficient.
|
||||
Report identical-tip hits separately from a one-function change and a dependency/configuration
|
||||
change. Include first installs, cold graphs, growing repository size, multi-ref pushes and incomplete
|
||||
outcomes. Freeze the applicable population before ranker selection; deferred questions count against
|
||||
|
||||
@@ -84,3 +84,15 @@ to remove Node from the evaluation or silently fall back to a PHP-only release c
|
||||
## Task 1.2 input preparation
|
||||
|
||||
The versioned push manifest records upstream PMPro vulnerable/fixed revisions, NodeGoat with an explicitly authored eval-removal repair, and comment-only benign changes that retain vulnerable backlog. Ghost is reserved independently for production Node module/async API evaluation; its reviewed supported-family pair population remains missing. The validator verifies immutable Git blob identities and source/license byte counts offline, retains every unresolved case, and keeps all capabilities experimental. Independent PHP and Python admission populations remain prerequisites; VAmPI regressions and the libpng unsupported-property envelope retain their separate roles. See `benchmarks/push/README.md` for the reproducible command and provenance boundaries.
|
||||
|
||||
## Measurement schema and admission separation (amended 2026-10-30)
|
||||
|
||||
- Every detection measurement on this workload matrix publishes one fixed schema: population
|
||||
and freeze digests, judgment vocabulary, recall / false alarms per 100 / precision / pair
|
||||
correctness each with sample counts and Wilson 95%, coverage, instrument failures, and
|
||||
runtime. A change of population or vocabulary starts a new baseline; cross-vocabulary
|
||||
trend claims are invalid.
|
||||
- M4 qualification reports per family scope (which families the capability claims) and per
|
||||
untouched population, through the Req 12 admission workflow. No tuning on admission labels.
|
||||
- Stop conditions per `steering/detection-quality.md`: two consecutive admission NO-GOs on
|
||||
the same capability park it for a strategy review; no incremental patching of parked arms.
|
||||
|
||||
@@ -68,7 +68,7 @@ real capability; task 8.3 still owns actual eligibility. Task 4.2 brings progres
|
||||
|
||||
The agent service rejected the task 4.3 dispatch with `agent thread limit reached`.
|
||||
The [kiro-impl skill](../../../.agents/skills/kiro-impl/SKILL.md) explicitly provides:
|
||||
“If multi-agent is not available, fall back to manual mode execution for all tasks.”
|
||||
"If multi-agent is not available, fall back to manual mode execution for all tasks."
|
||||
Implementation, review and completion checks therefore ran in the main context.
|
||||
This is a manual review record, not an independent-review claim.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Implementation verification: group 5
|
||||
|
||||
Group 5 was authorized by the maintainer's “approved” after group 4 completion.
|
||||
Group 5 was authorized by the maintainer's "approved" after group 4 completion.
|
||||
Tasks 5.1–5.2 are implemented and verified (16/27 total); the measured feasibility decision is NO-GO.
|
||||
Real capability eligibility and rollout readiness remain unproven.
|
||||
|
||||
@@ -8,7 +8,7 @@ Real capability eligibility and rollout readiness remain unproven.
|
||||
|
||||
Fresh implementer dispatch for 5.1 returned `agent thread limit reached`.
|
||||
The [Kiro implementation skill](../../../.agents/skills/kiro-impl/SKILL.md) explicitly
|
||||
states: “If multi-agent is not available, fall back to manual mode execution for all tasks.”
|
||||
states: "If multi-agent is not available, fall back to manual mode execution for all tasks."
|
||||
Its manual review rule permits review in the main context when a fresh reviewer is unavailable.
|
||||
This run therefore uses manual implementation and adversarial review, not independent review.
|
||||
|
||||
|
||||
@@ -300,15 +300,15 @@ and its [bundle](../../../benchmarks/measurements/2026-09-16-nodegoat-m1b-explai
|
||||
Three evaluation findings were rendered on the vulnerable revision, one per eval site. For
|
||||
line 32:
|
||||
|
||||
- **Location** app/routes/contributions.js:32, family injection, novelty new with
|
||||
- Location: app/routes/contributions.js:32, family injection, novelty new with
|
||||
`operation_absent_from_comparable_base`.
|
||||
- **Witness** `eval(req.body.preTax) -> const preTax = eval(req.body.preTax)`.
|
||||
- **Change attribution** `base:app/routes/contributions.js:32-34`.
|
||||
- **Provenance** `file_scope`: the question named no function, the engine reported `<lambda>2`.
|
||||
- **Consequence** "A request field reaches const preTax = eval(req.body.preTax), where it is
|
||||
- Witness: `eval(req.body.preTax) -> const preTax = eval(req.body.preTax)`.
|
||||
- Change attribution: `base:app/routes/contributions.js:32-34`.
|
||||
- Provenance: `file_scope`: the question named no function, the engine reported `<lambda>2`.
|
||||
- Consequence: "A request field reaches const preTax = eval(req.body.preTax), where it is
|
||||
evaluated as JavaScript, so an attacker controlling that field can execute arbitrary code in
|
||||
the server process."
|
||||
- **Repair** "Replace the evaluation at const preTax = eval(req.body.preTax) with a numeric
|
||||
- Repair: "Replace the evaluation at const preTax = eval(req.body.preTax) with a numeric
|
||||
conversion such as Number(...) or parseInt(...), then validate the result before use."
|
||||
|
||||
Both sentences come from the declaration's templates through the grounded-template path, not from
|
||||
|
||||
@@ -64,9 +64,9 @@ code the precision estimate will move, against a bar of 95% with zero false aler
|
||||
benign sides. Expect the first M4 evaluation to fail and budget for a second. That is the strongest
|
||||
argument for decision 2.
|
||||
|
||||
## M1 — One real security change works end to end
|
||||
## M1: One real security change works end to end
|
||||
|
||||
Status: **NEXT — not verified**. Dependencies: existing immutable replay and scan machinery.
|
||||
Status: **NEXT, not verified**. Dependencies: existing immutable replay and scan machinery.
|
||||
M1 is split into M1a (diagnose with vetoes recorded, not applied) and M1b (repair only
|
||||
the vetoes M1a showed to matter). M1 is verified only when M1b passes.
|
||||
|
||||
@@ -80,7 +80,7 @@ a global completion veto, line ambiguity inherited by every question, and query
|
||||
differing from witness identity. Requiring all four repaired before the first visible
|
||||
result is a full production pass, not one detection.
|
||||
|
||||
### M1a — Show the detection with every veto recorded
|
||||
### M1a: Show the detection with every veto recorded
|
||||
|
||||
Status: **MET 2026-09-16**, inside its one-week box. Evidence:
|
||||
[M1a record](m1a-recorded-vetoes.md) and
|
||||
@@ -112,9 +112,9 @@ source and census receipts. **Kill criterion:** if the fixed twin is not quiet u
|
||||
recorded-veto run, M1 has a detector problem, and M1b does not start until that is
|
||||
diagnosed and recorded.
|
||||
|
||||
### M1b — Repair only the vetoes M1a showed to matter
|
||||
### M1b: Repair only the vetoes M1a showed to matter
|
||||
|
||||
Status: **NEXT — not verified**. M1a supplied the veto list: per-family change context,
|
||||
Status: **NEXT, not verified**. M1a supplied the veto list: per-family change context,
|
||||
question-owned vendor relevance, operation correspondence for an operation on a changed line,
|
||||
file-wide correspondence ambiguity scope, and query-to-operation provenance. The owner
|
||||
diagnosis expected the first and the last two; operation correspondence is the addition M1a
|
||||
@@ -144,7 +144,7 @@ Owners: contributor-scan for query/operation/context evidence; pre-push-safety-n
|
||||
comparison, admission, the recorded-veto flag and reporting. Requirements: 1.1–1.4,
|
||||
2.1–2.4, 3.1–3.5, 6.3, 7.1–7.3.
|
||||
|
||||
## M2 — The same useful analysis fits the push budget
|
||||
## M2: The same useful analysis fits the push budget
|
||||
|
||||
Status: **MEASURED AND UNMET, 2026-09-17.** Two attacks on engine startup were measured and
|
||||
neither reaches the gate. A transaction-owned session is implemented and proven equivalent for
|
||||
@@ -179,7 +179,7 @@ completion at least 95%, under the predeclared profile. A fast census alone does
|
||||
Owner: contributor-scan runtime, with pre-push-safety-net deadline/cache integration.
|
||||
Requirements: 4.1–4.4, 6.2–6.5, 7.3.
|
||||
|
||||
## M3 — The shared design transfers across languages
|
||||
## M3: The shared design transfers across languages
|
||||
|
||||
Status: **PARTIAL, 2026-09-17. Not met.** Python is established and PHP is blocked by runtime.
|
||||
|
||||
@@ -218,7 +218,7 @@ remain a separate unsupported abstraction; adding a new engine is not a prerequi
|
||||
Owners: contributor-scan shared evidence and framework adapters; pre-push-safety-net evaluation.
|
||||
Requirements: 6.2–6.4, 8.1–8.4.
|
||||
|
||||
## M4 — Independent examples establish useful detection and low noise
|
||||
## M4: Independent examples establish useful detection and low noise
|
||||
|
||||
Status: **PENDING M3**. Population preparation may proceed earlier, but evaluation follows
|
||||
the frozen candidate. Reserve untouched cases before examining their scan results.
|
||||
@@ -241,7 +241,7 @@ do not require every possible language or family to become eligible for a narrow
|
||||
Owner: pre-push-safety-net task 8.3, consuming verified contributor-scan semantics.
|
||||
Requirements: 6.1–6.5, 8.1–8.4 and the accepted experimental acceptance profile.
|
||||
|
||||
## M5 — Enable a narrow advisory rollout
|
||||
## M5: Enable a narrow advisory rollout
|
||||
|
||||
Status: **PENDING M4**.
|
||||
|
||||
|
||||
@@ -196,6 +196,6 @@ failures without lowering the bar after inspecting holdout results.
|
||||
- >= 95% observed actionable-alert precision with counts and confidence interval; zero known false
|
||||
alerts on the fixed/benign regression suite. Small samples do not establish broad precision.
|
||||
- >= 90% recall on the predeclared supported security-change suite; unsupported classes remain in
|
||||
coverage reporting, not reclassified after a miss. Publish per-capability results, not only an aggregate.
|
||||
coverage reporting, not reclassified after a miss. Publish per-capability results; an aggregate alone is insufficient.
|
||||
- No capability is admitted with zero positive controls, zero reviewed emitted alerts, or no untouched
|
||||
evaluation repository. Failed profiles remain experimental and silent in the normal hook.
|
||||
|
||||
@@ -89,7 +89,7 @@ feasibility gate. [Official CPG slicing documentation](https://docs.joern.io/cpg
|
||||
2. **Separate claim from interruption.** Existing rungs describe analysis evidence. Hook admission
|
||||
additionally requires evaluated capability, change attribution and actionability. This meets the
|
||||
user's low-noise requirement without pretending an LLM confidence score proves a bug.
|
||||
3. **Compare evidence, not just finding lists.** A missing base finding can mean truncated analysis.
|
||||
3. **Compare evidence as well as finding lists.** A missing base finding can mean truncated analysis.
|
||||
Targeted comparable base checks are required for novelty, including removed-guard cases.
|
||||
4. **Adopt Git and the existing engine.** Build only the push transaction, cache and admission adapters.
|
||||
Do not add an IR, database or service merely to describe future scale.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user