mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 17:58:18 +00:00
Compare commits
232
Commits
@@ -21,3 +21,10 @@ OPENROUTER_API_KEY=
|
|||||||
# AWS_SESSION_TOKEN=
|
# AWS_SESSION_TOKEN=
|
||||||
# The bucket must have versioning enabled, a lifecycle rule expiring runs/, S3 Select and readable object tags
|
# 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.
|
# (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,43 @@
|
|||||||
|
name: Docs
|
||||||
|
|
||||||
|
# Builds the MkDocs site and publishes it to GitHub Pages on every push to main that touches it.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ["docs/**", "mkdocs.yml", "README.md", ".github/workflows/docs.yml"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: astral-sh/setup-uv@v7
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version-file: .python-version
|
||||||
|
- run: uv sync --extra docs
|
||||||
|
- run: uv run mkdocs build --strict
|
||||||
|
- uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: site
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
name: Engine image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
paths:
|
||||||
|
- 'plane/Dockerfile.*'
|
||||||
|
- 'plane/task-base.digest'
|
||||||
|
- 'Dockerfile'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
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
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- package: openultrasast
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
- package: ousast-search-task
|
||||||
|
dockerfile: plane/Dockerfile.search-task
|
||||||
|
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: ${{ matrix.package }}
|
||||||
|
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: ${{ matrix.dockerfile }}
|
||||||
|
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
|
||||||
|
- 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"
|
||||||
|
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"
|
||||||
@@ -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"
|
||||||
@@ -36,3 +36,9 @@ workspace/
|
|||||||
|
|
||||||
# MkDocs build output (mkdocs build)
|
# MkDocs build output (mkdocs build)
|
||||||
site/
|
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,140 @@
|
|||||||
|
# Engine change-scoped analysis (G2 workstream)
|
||||||
|
|
||||||
|
Status: **proposed / in progress** (opened 2026-10-11). Owner: engine. Tracks a concrete lever for the
|
||||||
|
engine-detection gap found while driving the G0 baseline. 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)
|
||||||
|
|
||||||
|
On real unseen vulnerability changes the engine produces **no finding** — but the diagnosis (live, 300 s long-deadline
|
||||||
|
replays) shows it 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`. **Zero analyses ran.** And 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`. Multi-causal; correspondence was one part (fixed separately,
|
||||||
|
commit c39fbb8), capability registry is Task 8.3.
|
||||||
|
|
||||||
|
## Hypothesis
|
||||||
|
|
||||||
|
A pre-push hook should vet **the code this push changed**, not re-scan the whole repository. Seeding/scoping the
|
||||||
|
engine's analysis to the **changed declarations and their dataflow neighborhood** (the `change_sites` are already in
|
||||||
|
the replay spec) should let analysis **actually run within budget** instead of being deferred — which is where
|
||||||
|
detection is being lost, not in Joern's capability.
|
||||||
|
|
||||||
|
## Anchors (confirm in increment 1)
|
||||||
|
|
||||||
|
- `src/openultrasast/rank.py` — 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`, `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 CPG build is NOT the bottleneck; the over-broad candidate set
|
||||||
|
is. Change-scoping is confirmed as the right lever, and because per-candidate prep includes the line-correspondence
|
||||||
|
work, fewer candidates also cuts that cost (the two fixes compound).
|
||||||
|
- [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.
|
||||||
|
- **Fix locus (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 a reverted first attempt — two things the naive filter got 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` — which locations to itemise vs
|
||||||
|
summarise — NOT to shrink 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 pathologically slow on a cold large
|
||||||
|
CPG — a separate optimisation from cutting the pair count.
|
||||||
|
- **So increment 2's single target is the evidence pass** (`scan.py:338-359`, `_evidence_pass`). The fork is HOW
|
||||||
|
to cut it without deleting reach-into-unchanged:
|
||||||
|
- (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, tier those in a second small pass — exact reach-into-unchanged, more code.
|
||||||
|
- (iii) **Per-pair speedup:** attack the ~235 ms/pair (warm server / batching) rather than the pair count —
|
||||||
|
helps every scan but is a deeper engine change and does not by itself bound 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 correctly 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 is also large and swings 52→127 s — two engines thrashing the 7 GB host (the record-07 baseline shares it).
|
||||||
|
The real levers are now (iii)-shaped, not (i): **warm/persistent query engine or a cached census** to kill the
|
||||||
|
~150 s per-call fixed cost, and/or a **bigger engine-arm budget** than 300 s; plus a **clean (uncontended)
|
||||||
|
measurement**, because contention alone may be inflating 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, which would free ~90 s — possibly enough for analysis,
|
||||||
|
but marginal.
|
||||||
|
- **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 is doubly wrong for 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; the win
|
||||||
|
here is findings moving 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 operator-owned infra, not assumptions to make here.
|
||||||
|
</content>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Plan: learn fix mechanisms into the engine (draft for review, 2026-10-04)
|
||||||
|
|
||||||
|
Status: approved 2026-10-04 by the maintainer. Increment 1 = discharge facts only; coverage additions (Req 7) come later.
|
||||||
|
Decisions: minimum 2 proposing repositories before a candidate is tried; admission per language (not pooled by callee name).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The Joern trace run over exp-005's 501 units shows the engine cannot tell a vulnerable function from its
|
||||||
|
fix. In the 105 pairs where both sides were analysed, a taint path appears on both sides in 12, on the
|
||||||
|
vulnerable side only in 2, on the fixed side only in 0, and on neither in 91
|
||||||
|
(`benchmarks/measurements/2026-10-04-joern-slice-leak-audit/record.json`).
|
||||||
|
|
||||||
|
So the fix is invisible to the engine. Either the flow survives and the engine does not know the fix's
|
||||||
|
mechanism (an `allowed_classes` argument, a path normalisation check, an escaping call), or the engine's
|
||||||
|
sources and sinks miss the function on both sides. A classifier fed this evidence cannot improve on it.
|
||||||
|
|
||||||
|
The corpus now holds 710 complete fix pairs (`2026-10-04-decision-engine-harvest-3/record.json`). Each fix
|
||||||
|
diff shows what was added. That is the material the engine lacks.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Grow the engine's discharge knowledge (sanitizers, guards, safe-argument forms) and, second, its source and
|
||||||
|
sink coverage, from the fix diffs. Admit a fact only when it improves held-out separation. Spend no model
|
||||||
|
money.
|
||||||
|
|
||||||
|
## Requirements (proposed)
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
(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
|
||||||
|
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
|
||||||
|
where neither side has a path (91 of 105 today), admitted under the same held-out rule.
|
||||||
|
|
||||||
|
## How it relates to what exists
|
||||||
|
|
||||||
|
- `corpus-seeded-mechanisms` already has a closed loop that admits and retracts mechanisms by leave-one-out.
|
||||||
|
This plan reuses its lever and store, and points it at discharge facts mined from real fix diffs instead
|
||||||
|
of variant search.
|
||||||
|
- `benchmarks/pairs/mechanisms.toml` (23 mechanisms) labels what kind of bug a pair is. The new candidates
|
||||||
|
describe what a fix does. They live in the fact tables, not in the mechanism vocabulary.
|
||||||
|
|
||||||
|
## Measures of success
|
||||||
|
|
||||||
|
- Pair asymmetry on held-out folds: vulnerable-only paths rise from 2 of 105 toward a substantial share,
|
||||||
|
with zero fixed-only paths.
|
||||||
|
- No held-out recall loss on vulnerable sides.
|
||||||
|
- Then the decision engine is re-run with the new engine evidence (exp-005 design), and the pooled block gate
|
||||||
|
(exp-004 design) is re-measured.
|
||||||
|
|
||||||
|
## Cost and time
|
||||||
|
|
||||||
|
- Model cost: $0 (mining and admission are deterministic; the classifier re-run afterwards is a separate,
|
||||||
|
approved spend).
|
||||||
|
- Engine time: one pass over affected pins per admission round; about 2 hours per full pass on k3s plus the
|
||||||
|
VM, about 6 hours on the VM alone.
|
||||||
|
|
||||||
|
## Open questions for the maintainer
|
||||||
|
|
||||||
|
1. Should coverage additions (Req 7) be in the same increment or a later one?
|
||||||
|
2. Minimum proposing repositories: 2 is the proposal. Higher is safer, lower finds more.
|
||||||
|
3. Admission per family and language, or pooled across languages for the same callee name?
|
||||||
@@ -119,6 +119,26 @@ rule hits.
|
|||||||
2. Without a trained model available the tools fall back to today's behaviour and say so in the report.
|
2. Without a trained model available the tools fall back to today's behaviour and say so in the report.
|
||||||
3. Hand-written rules remain as signal producers; changing their enabled/shadow status no longer changes what is
|
3. Hand-written rules remain as signal producers; changing their enabled/shadow status no longer changes what is
|
||||||
reported unless the engine's decision changes.
|
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
|
||||||
|
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
|
||||||
|
one whose Wilson 95% lower bound on precision, over the training flags of all families together, is >= 0.95.
|
||||||
|
It is reported by the Wilson 95% lower bound over all held-out flags of all families, before the floor.
|
||||||
|
- **Family floor**: a family may block only if its own held-out flags have zero errors and number at least F
|
||||||
|
(F = 10, reasoned in the exp-004 registration); otherwise that family stays ADVISORY.
|
||||||
|
- **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
|
### Requirement 7: Built for unseen repositories -- a repo safety net
|
||||||
|
|
||||||
@@ -136,7 +156,8 @@ rule hits.
|
|||||||
ADVISORY, trading precision for recall; both reported with out-of-repository intervals. The decision unit is a
|
ADVISORY, trading precision for recall; both reported with out-of-repository intervals. The decision unit is a
|
||||||
candidate in changed code (`pre-push` delta), with whole-repository scans as a secondary mode.
|
candidate in changed code (`pre-push` delta), with whole-repository scans as a secondary mode.
|
||||||
4. Calibration is checked on held-out repositories; where it does not hold, only ADVISORY is offered for that
|
4. Calibration is checked on held-out repositories; where it does not hold, only ADVISORY is offered for that
|
||||||
family.
|
family. *(Amended 2026-10-03: once exp-004 adopts the pooled gate of 6.4, BLOCK precision is shown by its held-out
|
||||||
|
bound and family floor, and calibration is reported, not required.)*
|
||||||
5. Any adaptation to a user's own repository (facts reuse, their dismissals) is opt-in, separate from the
|
5. Any adaptation to a user's own repository (facts reuse, their dismissals) is opt-in, separate from the
|
||||||
generalisation model, and never contributes to its reported numbers.
|
generalisation model, and never contributes to its reported numbers.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"feature_name": "learned-decision-engine",
|
"feature_name": "learned-decision-engine",
|
||||||
"created_at": "2026-09-30T14:00:00Z",
|
"created_at": "2026-09-30T14:00:00Z",
|
||||||
"updated_at": "2026-10-01T10:20:00Z",
|
"updated_at": "2026-10-04T18:00:00Z",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"phase": "implementation",
|
"phase": "implementation",
|
||||||
"approvals": {
|
"approvals": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ memory, compiled DSPy-style in-house); tasks 1-5 are unaffected, tasks 6-10 are
|
|||||||
against the provider balance read just before it, and none starts with a ceiling above that balance. No test makes
|
against the provider balance read just before it, and none starts with a ceiling above that balance. No test makes
|
||||||
a model or embedding call.
|
a model or embedding call.
|
||||||
|
|
||||||
|
Direction 2026-10-04: `.kiro/steering/roadmap.md` (section of 2026-10-04, phases G0-G5) is the authority. Every
|
||||||
|
change is judged on the unseen-repository pool (G0, spec `unseen-repo-evaluation`) by catch rate and false-alarm
|
||||||
|
rate per push. Within-pair AUC stays a development signal. Tasks 11-14 below follow that roadmap.
|
||||||
|
|
||||||
- [x] 1. Schema and features
|
- [x] 1. Schema and features
|
||||||
- `learn/schema.py` (closed, typed allow-list; no identities; missing != zero), `learn/features.py` (host
|
- `learn/schema.py` (closed, typed allow-list; no identities; missing != zero), `learn/features.py` (host
|
||||||
builder), new memory `KINDS`, `plane/tasks/features.py` and the `remember` input; tests enforcing the
|
builder), new memory `KINDS`, `plane/tasks/features.py` and the `remember` input; tests enforcing the
|
||||||
@@ -45,6 +49,11 @@ a model or embedding call.
|
|||||||
- _Requirements: 1.1_
|
- _Requirements: 1.1_
|
||||||
|
|
||||||
- [ ] 6. Program, local memory and compilation (design sections 4-5)
|
- [ ] 6. Program, local memory and compilation (design sections 4-5)
|
||||||
|
- Status 2026-10-04: 6.1-6.6 are done. 6.7 is partial: leave-one-source-out, the 0/25/50% memory-size points and
|
||||||
|
the source-count curve are not run. Around this task the experiment machinery grew: rule arms
|
||||||
|
(`precision_bound_block`, `pooled_block_gate` with a family floor) in `learn/experiments.py`, the response-cache
|
||||||
|
fill with a hard ceiling and a dry-run estimate in `learn/fill.py`, and the in-function slice in `learn/slice.py`.
|
||||||
|
These ran exp-003, exp-004 and exp-005 (7.5-7.7). Memory now holds 710 complete fix pairs (7.9).
|
||||||
- [x] 6.1 Excerpts and the example store (no model)
|
- [x] 6.1 Excerpts and the example store (no model)
|
||||||
- `learn/excerpt.py` (function span at the label's pin, bounds 80/4,000 for candidates and 40/2,000 for
|
- `learn/excerpt.py` (function span at the label's pin, bounds 80/4,000 for candidates and 40/2,000 for
|
||||||
examples, numbered lines, no path/repository/commit, advisory ids and security wording in comments
|
examples, numbered lines, no path/repository/commit, advisory ids and security wording in comments
|
||||||
@@ -151,6 +160,28 @@ a model or embedding call.
|
|||||||
rows' null plane-only features (1f525ff), the 788 embedding vectors absent from RustFS (copied from the file
|
rows' null plane-only features (1f525ff), the 788 embedding vectors absent from RustFS (copied from the file
|
||||||
store), the result row's column-type collision under S3 Select (780caaa). Further program variants (`k = 5` vs
|
store), the result row's column-type collision under S3 Select (780caaa). Further program variants (`k = 5` vs
|
||||||
`k = 1`, the confidence source) as new manifests.
|
`k = 1`, the confidence source) as new manifests.
|
||||||
|
- [x] 7.5 exp-003 run and recorded 2026-10-03 ($0, replay only): **inconclusive**. Selective blocking (a per-family
|
||||||
|
raw-score threshold chosen on training folds by its Wilson lower bound) offers no BLOCK on held-out paired units
|
||||||
|
in any family; the incumbent stays. 211 of 353 frozen units evaluated (142 replay misses). Proving 0.95 needs at
|
||||||
|
least 73 clean flags. Result: `benchmarks/experiments/exp-003-precision-bound-blocking/result.json`.
|
||||||
|
- [x] 7.6 exp-004 run and recorded 2026-10-03: **inconclusive**. One pooled BLOCK threshold over all families with a
|
||||||
|
family floor (Req 6.4). No outer fold found a threshold, pooled or per family; B offers no BLOCK. 501 units,
|
||||||
|
replay at $0 after a cache fill of $1.24 (ceiling $2; the dry-run estimate over-priced 4.5x). Result and record:
|
||||||
|
`benchmarks/experiments/exp-004-pooled-block-gate/result.json`, `record.json`.
|
||||||
|
- [x] 7.7 exp-005 run and recorded 2026-10-03: **inconclusive**. The in-function def-use slice in the prompt of the
|
||||||
|
six slice-2 programs. Within-pair AUC 0.762 -> 0.746 (CI [-0.063, 0.032], spans 0); unsure rate 0.29 -> 0.24
|
||||||
|
(favours B); usd per candidate slightly higher. A $0 leak audit came first
|
||||||
|
(`benchmarks/measurements/2026-10-03-graph-slice-leak-audit/record.json`). Result:
|
||||||
|
`benchmarks/experiments/exp-005-graph-slice/result.json`.
|
||||||
|
- [x] 7.8 Negatives harvest 2026-10-03 ($0.0014 embeddings, no chat-model call): 98 real fix pairs whose function
|
||||||
|
survives the fix, from reviewed advisories, as pointer-pair corpus `advisory-fixes-2`; memory 937 -> 1133
|
||||||
|
examples. Record: `benchmarks/measurements/2026-10-03-decision-engine-negatives-harvest/record.json`.
|
||||||
|
- [x] 7.9 Harvest 3, 2026-10-04 ($0.003 embeddings, no chat-model call): 251 pairs as `advisory-fixes-3`; complete
|
||||||
|
pairs in memory 459 -> 710. Record: `benchmarks/measurements/2026-10-04-decision-engine-harvest-3/record.json`.
|
||||||
|
- [x] 7.10 Joern trace run over exp-005's 501 units and its leak audit, 2026-10-04 ($0): no leak flagged. In the 105
|
||||||
|
pairs where both sides were asked, a path appears on both sides in 12, on the vulnerable side only in 2, on the
|
||||||
|
fixed side only in 0, and on neither in 91. The engine cannot see what a fix changed. Record:
|
||||||
|
`benchmarks/measurements/2026-10-04-joern-slice-leak-audit/record.json`.
|
||||||
- _Requirements: 3.6, 4.1-4.4_
|
- _Requirements: 3.6, 4.1-4.4_
|
||||||
|
|
||||||
- [ ] 8. Integration
|
- [ ] 8. Integration
|
||||||
@@ -176,3 +207,60 @@ a model or embedding call.
|
|||||||
`result-v3-engine.json` is committed. Nothing between the two commits may touch the program, memory snapshot,
|
`result-v3-engine.json` is committed. Nothing between the two commits may touch the program, memory snapshot,
|
||||||
features, priors or thresholds. This spec's code never opens v3 files.
|
features, priors or thresholds. This spec's code never opens v3 files.
|
||||||
- _Requirements: 5.3_
|
- _Requirements: 5.3_
|
||||||
|
|
||||||
|
- [ ] 11. Fix mechanisms (plan approved 2026-10-04; see `fix-mechanisms-plan.md`)
|
||||||
|
- Roadmap G2 arm (`.kiro/steering/roadmap.md`, section of 2026-10-04): one candidate improvement, not the spine.
|
||||||
|
It is judged on the unseen-repository pool (G0, spec `unseen-repo-evaluation`) by catch rate and false-alarm
|
||||||
|
rate per push. **Paused after 11.1 until the G0 pool exists.**
|
||||||
|
- [ ] 11.1 Mine candidate discharge facts from the 710 fix diffs (sanitizer calls, guarding conditions, safe sink
|
||||||
|
arguments, safe replacements); deterministic, $0. In progress.
|
||||||
|
- [ ] 11.2 Admission by held-out separation per outer fold: a candidate turns "path on both sides" into "path on the
|
||||||
|
vulnerable side only" on folds it was not mined from, removes no held-out vulnerable path, has at least 2
|
||||||
|
proposing repositories, and is admitted per language.
|
||||||
|
- [ ] 11.3 Engine re-run on the affected pins (kube-ax lanes plus the VM lane).
|
||||||
|
- [ ] 11.4 Registered experiment, with the detection gates green, before `ruleset/semantic/*.toml` changes.
|
||||||
|
- [ ] 11.5 Coverage increment: source and sink additions for pairs where neither side has a path (later).
|
||||||
|
- [ ] 11.6 TypeScript admission in the engine (branch `engine-typescript`).
|
||||||
|
- _Requirements: 4.1-4.4, 7.1, 8.1_
|
||||||
|
|
||||||
|
- [ ] 12. exp-006: Joern witness slice in the prompt **(budget go-ahead, about $2-4)**
|
||||||
|
- Planned, roadmap G1/G2. Against the current prompt, after a $0 leak audit, on re-frozen units. Judged on the
|
||||||
|
unseen-repository pool, not on within-pair AUC alone.
|
||||||
|
- _Requirements: 4.1-4.4, 7.1_
|
||||||
|
|
||||||
|
- [ ] 13. exp-007: the combiner ($0 replay)
|
||||||
|
- Planned, roadmap G1/G2. Three or four weights (score, engine entailment, neighbour agreement) over cached
|
||||||
|
responses. Judged on the unseen-repository pool.
|
||||||
|
- _Requirements: 4.1-4.4, 7.1_
|
||||||
|
|
||||||
|
- [ ] 14. Re-run the pooled block gate on re-frozen units
|
||||||
|
- Planned, roadmap G3. The exp-004 design with the family floor (Req 6.4), together with the pool's false-alarm
|
||||||
|
rate per family.
|
||||||
|
- _Requirements: 6.4, 7.3_
|
||||||
|
|
||||||
|
- [ ] 15. Trained classifier arm (maintainer decision 2026-10-04: train a model, not hand-set profiles; judged on the
|
||||||
|
unseen-repo pool, roadmap G1)
|
||||||
|
- [ ] 15.1 Features: code embedding of the excerpt (stored vectors) + every allow-listed signal; no hand-picked
|
||||||
|
profile. Automated screening: a signal that separates pair sides by itself on training folds above the leak
|
||||||
|
threshold is dropped for that fold (the leak audit as a filter, fitted per fold).
|
||||||
|
- [ ] 15.2 Models: regularised logistic regression and gradient-boosted trees, fitted per repository-held-out outer
|
||||||
|
fold with inner-fold hyper-parameter search; per-family and pooled variants.
|
||||||
|
- [ ] 15.3 Scores on held-out folds: within-pair AUC, pooled AUC, top-k precision, Wilson bound at the pooled
|
||||||
|
threshold; compared with the current LLM programs on the same units.
|
||||||
|
- [ ] 15.4 Scored on the unseen-repo pool when G0 exists: catch rate at fixed false-alarm budgets.
|
||||||
|
- _Requirements: 3 (revised 2026-10-04 by maintainer decision), 6, 7_
|
||||||
|
|
||||||
|
- [ ] 16. Jointly optimised LLM program arm (judged on the unseen-repo pool)
|
||||||
|
- 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_
|
||||||
|
|
||||||
|
|||||||
@@ -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, not only at
|
||||||
|
push. And 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."
|
||||||
|
}
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
# Design Document
|
# Design Document
|
||||||
|
|
||||||
|
> **Status 2026-10-04.** Groups 1-2 are done on kind (records `benchmarks/measurements/2026-10-02-k8s-01-profile/`
|
||||||
|
> and `2026-10-02-k8s-02-store-delivery/`). Task 2.6 is paused. The production target changed. The operator runs a
|
||||||
|
> kube-ax cluster (k3s, Agent Substrate v0.3.0, AX v0.3.1, 2 nodes). There we may only submit AX Tasks: no k8s
|
||||||
|
> object creation, no secrets, no access to `ate-system`. The engine now runs there as AX Tasks through
|
||||||
|
> `benchmarks/learn/engine_trace_ax.py`. The engine-task image (`plane/Dockerfile.engine-task`) is built FROM
|
||||||
|
> `ghcr.io/norandom/ax-task-runner:v0.3.1`. Tasks read and write through presigned HTTPS links, are named
|
||||||
|
> `ousast-engine-*`, and run a 1.7 GB heap with `ActiveProcessorCount=2` and one resident Joern session per pin. A
|
||||||
|
> pin whose sandbox dies is retried once under a new name, then handed to the VM lane (2.5 GB heap). Egress is
|
||||||
|
> operator-managed: HTTPS 443 to public hosts is open; LAN and cluster-internal addresses are blocked. Archive
|
||||||
|
> names (`.tar`, `.gz`, `.zip`, `.bin`) failed through the store host's Cloudflare cache until the operator added a
|
||||||
|
> cache-bypass rule; presigned objects are staged as `.dat` as a safeguard. Groups 3-8 are **to re-plan** against
|
||||||
|
> that cluster under roadmap G4 (`.kiro/steering/roadmap.md`, section of 2026-10-04). Their text below is kept
|
||||||
|
> as written. G4 (detection on push for any repository: `ousast plane scan --base --head`, engine pins on kube-ax,
|
||||||
|
> model calls on the VM or CI) is the new target for group 7, sized by what G1-G3 keep.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The plane keeps its shape: `ousast plane run` is the reconciler, ax is the only executor, every task is one
|
The plane keeps its shape: `ousast plane run` is the reconciler, ax is the only executor, every task is one
|
||||||
@@ -254,6 +269,13 @@ containerd; `Dockerfile:1-7` already argues against it).
|
|||||||
a 3 Gi actor lands only on `engine-pool`. If Substrate does not place by resource fit, the fallback is a
|
a 3 Gi actor lands only on `engine-pool`. If Substrate does not place by resource fit, the fallback is a
|
||||||
separate atespace with its own pool if Substrate scopes pools that way; failing that, the engine stays
|
separate atespace with its own pool if Substrate scopes pools that way; failing that, the engine stays
|
||||||
`alerts-engine` on a Docker host and the k3s engine becomes a plain Kubernetes Job outside ax, recorded as such.
|
`alerts-engine` on a Docker host and the k3s engine becomes a plain Kubernetes Job outside ax, recorded as such.
|
||||||
|
**Probe outcome (task 1.5, 2026-10-02, `benchmarks/measurements/2026-10-02-k8s-01-profile/`): `placement: any`.**
|
||||||
|
With `ousast-pool` at one 1536Mi worker and a `probe-pool` of one 3Gi worker both ACTIVE, a `repo-facts` actor
|
||||||
|
requesting 2560Mi (limit 3Gi) was placed on the 1536Mi `ousast-pool` worker (`kubectl ate get workers`: ACTORS
|
||||||
|
1/1000 there, 0/1000 on `probe-pool`), and so was the 256Mi actor. Substrate does not place by resource fit, so
|
||||||
|
group 5 takes the fallback: the engine pool must be the only pool its actors can land on. Before 5.2, check whether
|
||||||
|
Substrate scopes pools by atespace (an `engine` atespace with `engine-pool` as its only pool); if it does not, the
|
||||||
|
k3s engine runs as a plain Kubernetes Job outside ax and `alerts-engine` stays the kind path.
|
||||||
- **`alerts-engine` stays** for development on a host with Docker (kind profile), its help text and
|
- **`alerts-engine` stays** for development on a host with Docker (kind profile), its help text and
|
||||||
`docs/plane.md` row marked "development only; the plane task is `engine`" (Req 4.3). Generated Runs choose
|
`docs/plane.md` row marked "development only; the plane task is `engine`" (Req 4.3). Generated Runs choose
|
||||||
`engine` when the profile's `images` file has an engine image, else leave `alerts` for `alerts-engine`.
|
`engine` when the profile's `images` file has an engine image, else leave `alerts` for `alerts-engine`.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"feature_name": "plane-on-kubernetes",
|
"feature_name": "plane-on-kubernetes",
|
||||||
"created_at": "2026-10-02T20:00:00Z",
|
"created_at": "2026-10-02T20:00:00Z",
|
||||||
"updated_at": "2026-10-02T22:00:00Z",
|
"updated_at": "2026-10-04T18:00:00Z",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"phase": "implementation",
|
"phase": "implementation",
|
||||||
"approvals": {
|
"approvals": {
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# Implementation Plan
|
# Implementation Plan
|
||||||
|
|
||||||
|
> **Status 2026-10-04.** Groups 1-2 are done on kind (records `benchmarks/measurements/2026-10-02-k8s-01-profile/`
|
||||||
|
> and `2026-10-02-k8s-02-store-delivery/`). Task 2.6 is paused. The production target changed. The operator runs a
|
||||||
|
> kube-ax cluster (k3s, Agent Substrate v0.3.0, AX v0.3.1, 2 nodes). There we may only submit AX Tasks: no k8s
|
||||||
|
> object creation, no secrets, no access to `ate-system`. The engine now runs there as AX Tasks through
|
||||||
|
> `benchmarks/learn/engine_trace_ax.py`. The engine-task image (`plane/Dockerfile.engine-task`) is built FROM
|
||||||
|
> `ghcr.io/norandom/ax-task-runner:v0.3.1`. Tasks read and write through presigned HTTPS links, are named
|
||||||
|
> `ousast-engine-*`, and run a 1.7 GB heap with `ActiveProcessorCount=2` and one resident Joern session per pin. A
|
||||||
|
> pin whose sandbox dies is retried once under a new name, then handed to the VM lane (2.5 GB heap). Egress is
|
||||||
|
> operator-managed: HTTPS 443 to public hosts is open; LAN and cluster-internal addresses are blocked. Archive
|
||||||
|
> names (`.tar`, `.gz`, `.zip`, `.bin`) failed through the store host's Cloudflare cache until the operator added a
|
||||||
|
> cache-bypass rule; presigned objects are staged as `.dat` as a safeguard. Groups 3-8 are **to re-plan** against
|
||||||
|
> that cluster under roadmap G4 (`.kiro/steering/roadmap.md`, section of 2026-10-04). Their text below is kept
|
||||||
|
> as written. G4 (detection on push for any repository: `ousast plane scan --base --head`, engine pins on kube-ax,
|
||||||
|
> model calls on the VM or CI) is the new target for group 7, sized by what G1-G3 keep.
|
||||||
|
|
||||||
One task group per increment of `design.md` section 7, in that order; a later group never starts before the
|
One task group per increment of `design.md` section 7, in that order; a later group never starts before the
|
||||||
earlier group's kind proof is recorded. Every task ships with its tests; a task is done when its tests pass in
|
earlier group's kind proof is recorded. Every task ships with its tests; a task is done when its tests pass in
|
||||||
the host suite, ruff and mypy are clean, and nothing in `src/`, `plane/`, `ops/` (minus `ops/ax/up.sh` and
|
the host suite, ruff and mypy are clean, and nothing in `src/`, `plane/`, `ops/` (minus `ops/ax/up.sh` and
|
||||||
@@ -9,8 +24,8 @@ its kind proof and the record it writes under `benchmarks/measurements/<date>-k8
|
|||||||
(commit, images, the instrument checks, the numbers). Steps that call a model are marked **paid: needs a budget
|
(commit, images, the instrument checks, the numbers). Steps that call a model are marked **paid: needs a budget
|
||||||
go-ahead** with a ceiling; steps that need a hand on a cluster that does not exist yet are marked **manual**.
|
go-ahead** with a ceiling; steps that need a hand on a cluster that does not exist yet are marked **manual**.
|
||||||
|
|
||||||
- [ ] 1. Profile and literals (design section 1; open-question checks 2, 3, 7)
|
- [x] 1. Profile and literals (design section 1; open-question checks 2, 3, 7)
|
||||||
- [ ] 1.1 `PlaneProfile`
|
- [x] 1.1 `PlaneProfile`
|
||||||
- `src/openultrasast/plane/profile.py`: frozen dataclass with the fields of design section 1; `load_profile()`
|
- `src/openultrasast/plane/profile.py`: frozen dataclass with the fields of design section 1; `load_profile()`
|
||||||
reads `ops/k8s/profiles/<OUSAST_PLANE_PROFILE>.toml` (a name or a path; default `kind`), then applies
|
reads `ops/k8s/profiles/<OUSAST_PLANE_PROFILE>.toml` (a name or a path; default `kind`), then applies
|
||||||
`OUSAST_<FIELD>` overrides; rejects an unknown key, a secret-looking key (`*_KEY`, `*_SECRET`), an `exec`
|
`OUSAST_<FIELD>` overrides; rejects an unknown key, a secret-looking key (`*_KEY`, `*_SECRET`), an `exec`
|
||||||
@@ -19,7 +34,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
- `tests/test_plane_profile.py`: defaults; file then env precedence; each rejection names the field; both
|
- `tests/test_plane_profile.py`: defaults; file then env precedence; each rejection names the field; both
|
||||||
committed profiles load; no field holds a value matching a credential pattern.
|
committed profiles load; no field holds a value matching a credential pattern.
|
||||||
- _Requirements: 1.1_
|
- _Requirements: 1.1_
|
||||||
- [ ] 1.2 Literals out of `doctor.py`, `egress.py`, `router.py`, `generate.py`
|
- [x] 1.2 Literals out of `doctor.py`, `egress.py`, `router.py`, `generate.py`
|
||||||
- `doctor(profile)`: context from `profile.kube_context`; the registry check resolves the `images` file's runner
|
- `doctor(profile)`: context from `profile.kube_context`; the registry check resolves the `images` file's runner
|
||||||
reference (`docker manifest inspect` for a remote registry, `/v2/_catalog` for a local one); `open_store`
|
reference (`docker manifest inspect` for a remote registry, `/v2/_catalog` for a local one); `open_store`
|
||||||
verifies `profile.memory`; prints every configured address (Req 1.2). `Egress(context=...)`,
|
verifies `profile.memory`; prints every configured address (Req 1.2). `Egress(context=...)`,
|
||||||
@@ -29,46 +44,46 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
profile's context, never a built one.
|
profile's context, never a built one.
|
||||||
- Evidence: `grep -rn "kind-" src/openultrasast/plane` matches only comments citing the kind profile.
|
- Evidence: `grep -rn "kind-" src/openultrasast/plane` matches only comments citing the kind profile.
|
||||||
- _Requirements: 1.1, 1.2_
|
- _Requirements: 1.1, 1.2_
|
||||||
- [ ] 1.3 Host-literal scan test
|
- [x] 1.3 Host-literal scan test
|
||||||
- `tests/test_plane_literals.py`: pattern built at runtime (as `tests/test_removed_plane_references.py` does)
|
- `tests/test_plane_literals.py`: pattern built at runtime (as `tests/test_removed_plane_references.py` does)
|
||||||
over `src/`, `plane/`, `ops/` minus `ops/ax/up.sh` and `ops/k8s/profiles/`, and `docs/`, for `kind-ousast`,
|
over `src/`, `plane/`, `ops/` minus `ops/ax/up.sh` and `ops/k8s/profiles/`, and `docs/`, for `kind-ousast`,
|
||||||
`localhost:5001`, `172.19.`, `127.0.0.1:` and `port-forward` outside `router.py` and lines tagged
|
`localhost:5001`, `172.19.`, `127.0.0.1:` and `port-forward` outside `router.py` and lines tagged
|
||||||
`ax-tunnel`; fails listing `path:line`.
|
`ax-tunnel`; fails listing `path:line`.
|
||||||
- Evidence: the test passes on this commit and fails when a literal is reinserted (shown once in the record).
|
- Evidence: the test passes on this commit and fails when a literal is reinserted (shown once in the record).
|
||||||
- _Requirements: 1.3_
|
- _Requirements: 1.3_
|
||||||
- [ ] 1.4 `up.sh` by profile
|
- [x] 1.4 `up.sh` by profile
|
||||||
- `ops/ax/up.sh`: `KO_DOCKER_REPO` and the context from `kind.toml` (read with `python -m
|
- `ops/ax/up.sh`: `KO_DOCKER_REPO` and the context from `kind.toml` (read with `python -m
|
||||||
openultrasast.plane.profile --print registry|kube_context`); writes `kind-images.json` instead of
|
openultrasast.plane.profile --print registry|kube_context`); writes `kind-images.json` instead of
|
||||||
`runner-image`; `generate.repin_templates` accepts that file (runner key only until task 4.3).
|
`runner-image`; `generate.repin_templates` accepts that file (runner key only until task 4.3).
|
||||||
- Evidence: `ops/ax/up.sh` reruns idempotently on kind; `ops/ax/smoke-run.sh` passes.
|
- Evidence: `ops/ax/up.sh` reruns idempotently on kind; `ops/ax/smoke-run.sh` passes.
|
||||||
- _Requirements: 1.1, 2.4_
|
- _Requirements: 1.1, 2.4_
|
||||||
- [ ] 1.5 Placement probe (open question 2, decided before the engine work)
|
- [x] 1.5 Placement probe (open question 2, decided before the engine work) -- result: `placement: any`
|
||||||
- `ops/ax/probe-placement.sh`: applies a second WorkerPool `probe-pool` (1 replica, 3 Gi) from the design's
|
- `ops/ax/probe-placement.sh`: applies a second WorkerPool `probe-pool` (1 replica, 3 Gi) from the design's
|
||||||
engine template, then one model-free `repo-facts` Task with `resources.requests.memory: 2560Mi`, and records
|
engine template, then one model-free `repo-facts` Task with `resources.requests.memory: 2560Mi`, and records
|
||||||
with `kubectl ate get workers` which worker took the actor; then the same Task with 256Mi. Removed after.
|
with `kubectl ate get workers` which worker took the actor; then the same Task with 256Mi. Removed after.
|
||||||
- Evidence: `record.json` says whether Substrate places by resource fit (`placement: fit|any`). `any` selects
|
- Evidence: `record.json` says whether Substrate places by resource fit (`placement: fit|any`). `any` selects
|
||||||
the design's fallback for group 5 and is written into `design.md` section 4 before group 5 starts.
|
the design's fallback for group 5 and is written into `design.md` section 4 before group 5 starts.
|
||||||
- _Requirements: 4.1_
|
- _Requirements: 4.1_
|
||||||
- [ ] 1.6 Reconciler bound (open question 7)
|
- [x] 1.6 Reconciler bound (open question 7) -- 489 lines after 1.2; projected 514 during group 2: the bound moves to 550 in 2.4
|
||||||
- Record `wc -l src/openultrasast/plane/reconciler.py` (477 at 17ac7ad) and the lines task 2.4 may add
|
- Record `wc -l src/openultrasast/plane/reconciler.py` (477 at 17ac7ad) and the lines task 2.4 may add
|
||||||
(receiver wiring removed: `-12`, `Delivery` calls: `+8` estimated). If the estimate crosses 500, the bound
|
(receiver wiring removed: `-12`, `Delivery` calls: `+8` estimated). If the estimate crosses 500, the bound
|
||||||
in `tests/test_plane_reconciler.py:803` moves to 550 in task 2.4 with the reason in the assertion message.
|
in `tests/test_plane_reconciler.py:803` moves to 550 in task 2.4 with the reason in the assertion message.
|
||||||
- Evidence: the numbers in `record.json`.
|
- Evidence: the numbers in `record.json`.
|
||||||
- _Requirements: 2.1_
|
- _Requirements: 2.1_
|
||||||
- [ ] 1.7 gvisor tarball reachability (open question 3) **manual**
|
- [x] 1.7 gvisor tarball reachability (open question 3) **manual** -- documented as planned in `docs/deployment.md` B.1; the live check waits for k3s
|
||||||
- `docs/deployment.md` (B.1, planned): the k3s node must fetch
|
- `docs/deployment.md` (B.1, planned): the k3s node must fetch
|
||||||
`gs://gvisor/releases/nightly/2026-09-02/<arch>/gvisor.tar.zstd` (`substrate/manifests/ate-install/sandboxconfig-gvisor.yaml`)
|
`gs://gvisor/releases/nightly/2026-09-02/<arch>/gvisor.tar.zstd` (`substrate/manifests/ate-install/sandboxconfig-gvisor.yaml`)
|
||||||
or a mirrored copy named in a copied SandboxConfig; the check is `kubectl -n ate-system logs` of the worker
|
or a mirrored copy named in a copied SandboxConfig; the check is `kubectl -n ate-system logs` of the worker
|
||||||
pod's atelet fetch line, run by hand on the day k3s exists. Labelled planned until then.
|
pod's atelet fetch line, run by hand on the day k3s exists. Labelled planned until then.
|
||||||
- _Requirements: 7.2_
|
- _Requirements: 7.2_
|
||||||
- [ ] 1.8 Kind proof and record 01
|
- [x] 1.8 Kind proof and record 01
|
||||||
- `ousast plane doctor --profile kind` passes every line; `ousast plane doctor --profile k3s` fails only on the
|
- `ousast plane doctor --profile kind` passes every line; `ousast plane doctor --profile k3s` fails only on the
|
||||||
unreachable context (the expected line); `test_plane_literals` 0 offenders; the probe of 1.5; the bound of 1.6.
|
unreachable context (the expected line); `test_plane_literals` 0 offenders; the probe of 1.5; the bound of 1.6.
|
||||||
- `benchmarks/measurements/<date>-k8s-01-profile/record.json`.
|
- `benchmarks/measurements/<date>-k8s-01-profile/record.json`.
|
||||||
- _Requirements: 1.1, 1.2, 1.3, 7.1_
|
- _Requirements: 1.1, 1.2, 1.3, 7.1_
|
||||||
|
|
||||||
- [ ] 2. Store delivery, receiver-less egress (design section 3)
|
- [ ] 2. Store delivery, receiver-less egress (design section 3)
|
||||||
- [ ] 2.1 Presigned URLs and the store run directory
|
- [x] 2.1 Presigned URLs and the store run directory
|
||||||
- `src/openultrasast/plane/delivery.py`: `Delivery(store, run)` mints presigned PUT for
|
- `src/openultrasast/plane/delivery.py`: `Delivery(store, run)` mints presigned PUT for
|
||||||
`runs/<run>/<task>/output.tar` and GET for `runs/<run>/<producer>/<artifact>` (boto3 `generate_presigned_url`
|
`runs/<run>/<task>/output.tar` and GET for `runs/<run>/<producer>/<artifact>` (boto3 `generate_presigned_url`
|
||||||
on `S3Client.sdk`, expiry `OUSAST_TASK_TIMEOUT + 600`); `delivered(task)` HEADs the tar; `collect(task)`
|
on `S3Client.sdk`, expiry `OUSAST_TASK_TIMEOUT + 600`); `delivered(task)` HEADs the tar; `collect(task)`
|
||||||
@@ -78,7 +93,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
- `tests/test_plane_delivery.py`: against a local S3-shaped fake (PUT/GET/HEAD by key); URLs never appear in
|
- `tests/test_plane_delivery.py`: against a local S3-shaped fake (PUT/GET/HEAD by key); URLs never appear in
|
||||||
logs; expiry; member rejection; declared-only re-put; state seeding.
|
logs; expiry; member rejection; declared-only re-put; state seeding.
|
||||||
- _Requirements: 3.1, 2.1_
|
- _Requirements: 3.1, 2.1_
|
||||||
- [ ] 2.2 Runner: `delivery` in the start body
|
- [x] 2.2 Runner: `delivery` in the start body
|
||||||
- `src/openultrasast/plane/runner.py`: `StartGate.offer` validates an optional `delivery {put, inputs}` (https,
|
- `src/openultrasast/plane/runner.py`: `StartGate.offer` validates an optional `delivery {put, inputs}` (https,
|
||||||
hostname, no echo); `_deliver_output` PUTs the tar to `delivery.put` (same retry and `DeliveryError`
|
hostname, no echo); `_deliver_output` PUTs the tar to `delivery.put` (same retry and `DeliveryError`
|
||||||
semantics); `fetch_inputs` GETs from `delivery.inputs[name]`; `OUSAST_ARTIFACT_URL` and `OUSAST_ARTIFACT_DIAL`
|
semantics); `fetch_inputs` GETs from `delivery.inputs[name]`; `OUSAST_ARTIFACT_URL` and `OUSAST_ARTIFACT_DIAL`
|
||||||
@@ -86,14 +101,14 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
- `tests/test_plane_runner.py`: accepted body with `delivery`; 400 on an `http` or IP URL; PUT to a local
|
- `tests/test_plane_runner.py`: accepted body with `delivery`; 400 on an `http` or IP URL; PUT to a local
|
||||||
server; inputs fetched from presigned-shaped URLs; the marker and retry-only boot unchanged.
|
server; inputs fetched from presigned-shaped URLs; the marker and retry-only boot unchanged.
|
||||||
- _Requirements: 3.1_
|
- _Requirements: 3.1_
|
||||||
- [ ] 2.3 Egress without the receiver
|
- [x] 2.3 Egress without the receiver
|
||||||
- `src/openultrasast/plane/egress.py`: `policy_for(task, workspaces, model, store_host)` emits one
|
- `src/openultrasast/plane/egress.py`: `policy_for(task, workspaces, model, store_host)` emits one
|
||||||
`tlsPassthrough` rule on 443 over the sorted union of store host, Git hosts, Model hosts; no `http` rule; an
|
`tlsPassthrough` rule on 443 over the sorted union of store host, Git hosts, Model hosts; no `http` rule; an
|
||||||
IP store host raises naming `S3_ENDPOINT`.
|
IP store host raises naming `S3_ENDPOINT`.
|
||||||
- `tests/test_plane_egress.py`: for every committed template under `plane/tasks/` the rendered policy is the
|
- `tests/test_plane_egress.py`: for every committed template under `plane/tasks/` the rendered policy is the
|
||||||
expected set; the IP refusal.
|
expected set; the IP refusal.
|
||||||
- _Requirements: 3.2_
|
- _Requirements: 3.2_
|
||||||
- [ ] 2.4 Reconciler on `Delivery`
|
- [x] 2.4 Reconciler on `Delivery`
|
||||||
- `src/openultrasast/plane/reconciler.py`: `run()` opens the store first (no fallback), builds `Delivery`,
|
- `src/openultrasast/plane/reconciler.py`: `run()` opens the store first (no fallback), builds `Delivery`,
|
||||||
passes `delivery` to `router.starter`/`start_task` (body extended in `router.py`), `_await` on
|
passes `delivery` to `router.starter`/`start_task` (body extended in `router.py`), `_await` on
|
||||||
`Delivery.delivered`, outcome from the collected `summary.json`; the receiver thread is kept behind
|
`Delivery.delivered`, outcome from the collected `summary.json`; the receiver thread is kept behind
|
||||||
@@ -101,13 +116,13 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
- `tests/test_plane_reconciler.py`: the ax fake run completes through the delivery fake; a rerun from an empty
|
- `tests/test_plane_reconciler.py`: the ax fake run completes through the delivery fake; a rerun from an empty
|
||||||
results root resumes from the store's `state.json`.
|
results root resumes from the store's `state.json`.
|
||||||
- _Requirements: 2.1, 3.1_
|
- _Requirements: 2.1, 3.1_
|
||||||
- [ ] 2.5 Kind proof and record 02
|
- [x] 2.5 Kind proof and record 02
|
||||||
- `ops/ax/smoke-run.sh` with `OUSAST_DELIVERY=store`: `facts.json` and `summary.json` arrive through the store;
|
- `ops/ax/smoke-run.sh` with `OUSAST_DELIVERY=store`: `facts.json` and `summary.json` arrive through the store;
|
||||||
the actor's policy read back has one TLS rule naming the store host; `ousast plane status ax-e2e-smoke`
|
the actor's policy read back has one TLS rule naming the store host; `ousast plane status ax-e2e-smoke`
|
||||||
prints the table. Instrument: the tar's byte size from the store equals the extracted files' total.
|
prints the table. Instrument: the tar's byte size from the store equals the extracted files' total.
|
||||||
- `benchmarks/measurements/<date>-k8s-02-store-delivery/record.json`: bytes, PUT-to-poll seconds, rules.
|
- `benchmarks/measurements/<date>-k8s-02-store-delivery/record.json`: bytes, PUT-to-poll seconds, rules.
|
||||||
- _Requirements: 3.1, 3.2, 7.1_
|
- _Requirements: 3.1, 3.2, 7.1_
|
||||||
- [ ] 2.6 Remove the receiver (last; after 2.5 is recorded)
|
- [ ] 2.6 Remove the receiver (last; after 2.5 is recorded) -- paused 2026-10-04
|
||||||
- Delete `Receiver`, `_Handler`, `receiver_address`, `receiver_cluster_ip`, `RECEIVER_HOST`, `RECEIVER_PORT`
|
- Delete `Receiver`, `_Handler`, `receiver_address`, `receiver_cluster_ip`, `RECEIVER_HOST`, `RECEIVER_PORT`
|
||||||
from `egress.py`; `OUSAST_ARTIFACT_URL/DIAL/HOST/PORT` from `runner.py` and `reconciler.py`; the
|
from `egress.py`; `OUSAST_ARTIFACT_URL/DIAL/HOST/PORT` from `runner.py` and `reconciler.py`; the
|
||||||
`OUSAST_DELIVERY` switch; `ops/ax/receiver-service.yaml.tmpl` and the `up.sh` receiver step; the EndpointSlice
|
`OUSAST_DELIVERY` switch; `ops/ax/receiver-service.yaml.tmpl` and the `up.sh` receiver step; the EndpointSlice
|
||||||
@@ -117,7 +132,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
`receiver_removed: <commit>`.
|
`receiver_removed: <commit>`.
|
||||||
- _Requirements: 3.1, 1.3_
|
- _Requirements: 3.1, 1.3_
|
||||||
|
|
||||||
- [ ] 3. `--exec remote` from a CI-shaped principal (design section 2; open-question check 1)
|
- [ ] 3. `--exec remote` from a CI-shaped principal (design section 2; open-question check 1) -- to re-plan (kube-ax, roadmap G4)
|
||||||
- [ ] 3.1 Execution mode and RBAC manifests
|
- [ ] 3.1 Execution mode and RBAC manifests
|
||||||
- `cli.py`: `ousast plane run --exec local|remote` (default `profile.exec`); `reconciler.Ax` passes the
|
- `cli.py`: `ousast plane run --exec local|remote` (default `profile.exec`); `reconciler.Ax` passes the
|
||||||
profile's context to `ax` (its context flag, read from `ax --help` and pinned in a test); `remote` refuses a
|
profile's context to `ax` (its context flag, read from `ax --help` and pinned in a test); `remote` refuses a
|
||||||
@@ -150,7 +165,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
status, usd spent.
|
status, usd spent.
|
||||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 7.1_
|
- _Requirements: 2.1, 2.2, 2.3, 2.4, 7.1_
|
||||||
|
|
||||||
- [ ] 4. Images on GHCR (design section 8)
|
- [ ] 4. Images on GHCR (design section 8) -- to re-plan (kube-ax, roadmap G4)
|
||||||
- [ ] 4.1 `images.yml`
|
- [ ] 4.1 `images.yml`
|
||||||
- `.github/workflows/images.yml` on tags `v*`: `docker/build-push-action` for `plane/Dockerfile.runner` ->
|
- `.github/workflows/images.yml` on tags `v*`: `docker/build-push-action` for `plane/Dockerfile.runner` ->
|
||||||
`ghcr.io/norandom/ousast-runner:<tag>` and the root `Dockerfile` -> `ghcr.io/norandom/ousast-engine:<tag>`;
|
`ghcr.io/norandom/ousast-runner:<tag>` and the root `Dockerfile` -> `ghcr.io/norandom/ousast-engine:<tag>`;
|
||||||
@@ -178,7 +193,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
- `benchmarks/measurements/<date>-k8s-04-ghcr-images/record.json`: image sizes, pull seconds, visibility.
|
- `benchmarks/measurements/<date>-k8s-04-ghcr-images/record.json`: image sizes, pull seconds, visibility.
|
||||||
- _Requirements: 1.1, 4.2, 7.1_
|
- _Requirements: 1.1, 4.2, 7.1_
|
||||||
|
|
||||||
- [ ] 5. The engine as a plane task (design section 4)
|
- [ ] 5. The engine as a plane task (design section 4) -- to re-plan (kube-ax, roadmap G4)
|
||||||
- [ ] 5.1 `cpg/dump.py` and `tasks/engine.py`
|
- [ ] 5.1 `cpg/dump.py` and `tasks/engine.py`
|
||||||
- Lift `benchmarks/push/finding_dump.py`'s scan into `src/openultrasast/cpg/dump.py` (the script becomes a
|
- Lift `benchmarks/push/finding_dump.py`'s scan into `src/openultrasast/cpg/dump.py` (the script becomes a
|
||||||
thin caller; `ops/README.md:78` command unchanged). `src/openultrasast/plane/tasks/engine.py`: env
|
thin caller; `ops/README.md:78` command unchanged). `src/openultrasast/plane/tasks/engine.py`: env
|
||||||
@@ -208,7 +223,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
placement, image size and pull seconds.
|
placement, image size and pull seconds.
|
||||||
- _Requirements: 4.1, 4.2, 7.1_
|
- _Requirements: 4.1, 4.2, 7.1_
|
||||||
|
|
||||||
- [ ] 6. Pools, autoscaling, node checklist (design section 5)
|
- [ ] 6. Pools, autoscaling, node checklist (design section 5) -- to re-plan (kube-ax, roadmap G4)
|
||||||
- [ ] 6.1 `plane/k8s.py` and the manifests
|
- [ ] 6.1 `plane/k8s.py` and the manifests
|
||||||
- `src/openultrasast/plane/k8s.py`: `render(profile, out)` writes `workerpool-default.yaml`,
|
- `src/openultrasast/plane/k8s.py`: `render(profile, out)` writes `workerpool-default.yaml`,
|
||||||
`workerpool-engine.yaml`, `ax-server-snapshots.yaml` (patch with `profile.snapshots_bucket`), `rbac-ci.yaml`;
|
`workerpool-engine.yaml`, `ax-server-snapshots.yaml` (patch with `profile.snapshots_bucket`), `rbac-ci.yaml`;
|
||||||
@@ -232,7 +247,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
- `benchmarks/measurements/<date>-k8s-06-pools/record.json`: footprint table, securityContext, dry-run result.
|
- `benchmarks/measurements/<date>-k8s-06-pools/record.json`: footprint table, securityContext, dry-run result.
|
||||||
- _Requirements: 5.1, 5.2, 7.1_
|
- _Requirements: 5.1, 5.2, 7.1_
|
||||||
|
|
||||||
- [ ] 7. `ousast plane scan` (design section 6)
|
- [ ] 7. `ousast plane scan` (design section 6) -- to re-plan (kube-ax, roadmap G4)
|
||||||
- [ ] 7.1 Workspaces and candidates from URL + commits
|
- [ ] 7.1 Workspaces and candidates from URL + commits
|
||||||
- `generate.scan_cases(url, base, head, cache, *, all=False)`: shallow clone into the case cache, two pinned
|
- `generate.scan_cases(url, base, head, cache, *, all=False)`: shallow clone into the case cache, two pinned
|
||||||
Workspaces, head-side hunks via `fix_ranges(side="new")`, functions via `repo_facts.enclosing` over
|
Workspaces, head-side hunks via `fix_ranges(side="new")`, functions via `repo_facts.enclosing` over
|
||||||
@@ -265,7 +280,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
|
|||||||
seconds from submit to verdict, whether the known CVE site is among the agreed.
|
seconds from submit to verdict, whether the known CVE site is among the agreed.
|
||||||
- _Requirements: 6.1, 6.2, 6.3, 7.1_
|
- _Requirements: 6.1, 6.2, 6.3, 7.1_
|
||||||
|
|
||||||
- [ ] 8. Runbook (design section 7, Req 7)
|
- [ ] 8. Runbook (design section 7, Req 7) -- to re-plan (kube-ax, roadmap G4)
|
||||||
- [ ] 8.1 `docs/deployment.md` as the runbook
|
- [ ] 8.1 `docs/deployment.md` as the runbook
|
||||||
- Sections: prerequisites for k3s (Substrate, ax with the snapshots patch, GHCR and the pull secret, buckets,
|
- Sections: prerequisites for k3s (Substrate, ax with the snapshots patch, GHCR and the pull secret, buckets,
|
||||||
the CI SA and its grant from 3.2, the node checklist of 6.2), the profile switch, `--exec remote` from a laptop
|
the CI SA and its grant from 3.2, the node checklist of 6.2), the profile switch, `--exec remote` from a laptop
|
||||||
|
|||||||
@@ -147,6 +147,43 @@ with Express-style routes and middleware, including vulnerable, fixed and benign
|
|||||||
frontend/framework adaptations and new abstract capabilities, and shall report C/C++ envelope or
|
frontend/framework adaptations and new abstract capabilities, and shall report C/C++ envelope or
|
||||||
unsupported-property results separately from demonstrated vulnerability detection.
|
unsupported-property results separately from demonstrated vulnerability detection.
|
||||||
|
|
||||||
|
## Requirement 9: Operate as an advisory safety net on any repository
|
||||||
|
|
||||||
|
Added 2026-10-02, maintainer-approved, from the operational-readiness audit. The maintainer's
|
||||||
|
definition: a user installs the hook on ANY repository and gets honest, advisory-only results;
|
||||||
|
nothing ever blocks a push unless blocking was explicitly chosen.
|
||||||
|
|
||||||
|
9.1 (added 2026-10-02, maintainer-approved) When the installed hook cannot run the check, because the
|
||||||
|
`ousast` executable is missing or an argument or setting is invalid, the hook shall allow the push and
|
||||||
|
print one line saying the check did not run and how to fix it; only an explicit `--mode blocking`
|
||||||
|
opt-in may turn an analysis result into a rejected push.
|
||||||
|
|
||||||
|
9.2 (added 2026-10-02, maintainer-approved) When a check is skipped or incomplete, the terminal output
|
||||||
|
shall name each distinct reason in plain words, one line per reason, with repository paths decoded and
|
||||||
|
no opaque hash tokens; a language with no engine frontend and no quick rules shall be named with its
|
||||||
|
file count; a first push of a new branch shall say there was no base to compare against and what was
|
||||||
|
checked instead. The JSON artifact keeps the raw reason values.
|
||||||
|
|
||||||
|
9.3 (added 2026-10-02, maintainer-approved) Before the engine, within the same deadline, the Safety Net
|
||||||
|
shall run the shipped quick pattern rules over the head side of the changed lines and show their
|
||||||
|
findings as advisory pattern matches, labeled separately from admitted alerts. This amends 3.2 for this
|
||||||
|
tier only: quick-rule findings are shown, never admitted, and never block.
|
||||||
|
|
||||||
|
9.4 (added 2026-10-02, maintainer-approved) When a finding's operation lies in a file that is new in the
|
||||||
|
head revision, the comparison shall classify it as new rather than unknown; an engine finding whose
|
||||||
|
novelty is established but whose capability is not yet qualified shall be shown as an advisory engine
|
||||||
|
finding, not an admitted alert. Genuinely unknown comparisons keep their vetoes.
|
||||||
|
|
||||||
|
9.5 (added 2026-10-02, maintainer-approved) When the engine cannot finish within the deadline, one line
|
||||||
|
shall say so; on explicit request (`--engine background`) the Safety Net shall start the engine
|
||||||
|
detached after the push, record its result in the artifact directory, and show it on the next run as
|
||||||
|
the engine result for that head from the previous push.
|
||||||
|
|
||||||
|
9.6 (added 2026-10-02, maintainer-approved) The hook installer and remover shall ship in the wheel
|
||||||
|
(`ousast pre-push install|uninstall`), refuse to overwrite an existing hook unless forced, and a
|
||||||
|
hook-callable container wrapper shall pass the push input and artifact path through; the documentation
|
||||||
|
shall give a first-run guide, a glossary of skip reasons, a CI recipe and help text for every flag.
|
||||||
|
|
||||||
## Accepted experimental acceptance profile
|
## Accepted experimental acceptance profile
|
||||||
|
|
||||||
These values are the initial implementation/evaluation profile accepted for task generation, not
|
These values are the initial implementation/evaluation profile accepted for task generation, not
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ Preserve all completed task evidence below.
|
|||||||
- _Depends: 13.1_
|
- _Depends: 13.1_
|
||||||
- _Requirements: 6.2, 6.3, 8.1, 8.2_
|
- _Requirements: 6.2, 6.3, 8.1, 8.2_
|
||||||
|
|
||||||
- [ ] 14. Ask every declared family whether it establishes anything (precondition for M4)
|
- [x] 14. Ask every declared family whether it establishes anything (precondition for M4)
|
||||||
- Seven families are declared and two were proven end to end. A declared family that has never been asked whether it finds anything is advertised coverage, and M4 cannot qualify a capability that establishes nothing.
|
- Seven families are declared and two were proven end to end. A declared family that has never been asked whether it finds anything is advertised coverage, and M4 cannot qualify a capability that establishes nothing.
|
||||||
- [x] 14.1 Census the seven declared families on NodeGoat and VAmPI
|
- [x] 14.1 Census the seven declared families on NodeGoat and VAmPI
|
||||||
- Run production discovery and scan over each whole checkout and report, per family, the regions that offered it, the questions asked, the questions completed, and what was found with the witness and rung. No comparison, no novelty, no admission: detection only.
|
- Run production discovery and scan over each whole checkout and report, per family, the regions that offered it, the questions asked, the questions completed, and what was found with the witness and rung. No comparison, no novelty, no admission: detection only.
|
||||||
@@ -575,7 +575,7 @@ Preserve all completed task evidence below.
|
|||||||
- _Boundary: Evaluation harness, full-run integration_
|
- _Boundary: Evaluation harness, full-run integration_
|
||||||
- _Depends: 13.2, contributor-scan 7.3, contributor-scan 7.8_
|
- _Depends: 13.2, contributor-scan 7.3, contributor-scan 7.8_
|
||||||
- _Requirements: 6.2, 6.3, 8.1, 8.2_
|
- _Requirements: 6.2, 6.3, 8.1, 8.2_
|
||||||
- [ ] 13.4 Let a question inside one partition complete, whatever else the repository holds
|
- [x] 13.4 Let a question inside one partition complete, whatever else the repository holds
|
||||||
- **Diagnosed 2026-09-21, and PHP was never the variable.** A repository holding TWO languages can never complete a question, so it can never admit a finding. `partitions.py` raises `cross_partition_semantics_unresolved` whenever more than one language partition exists; that reason is a member of `GRAPH_INTEGRITY_GAPS`; and a graph-integrity gap demotes every completed outcome in the scan.
|
- **Diagnosed 2026-09-21, and PHP was never the variable.** A repository holding TWO languages can never complete a question, so it can never admit a finding. `partitions.py` raises `cross_partition_semantics_unresolved` whenever more than one language partition exists; that reason is a member of `GRAPH_INTEGRITY_GAPS`; and a graph-integrity gap demotes every completed outcome in the scan.
|
||||||
- The evidence is one column wide: NodeGoat is JavaScript alone and completes 299 of 299, VAmPI is Python alone and completes 35 of 35, PMPro is PHP with 36 regions of bundled JavaScript and completes 0 of 127 and 0 of 520 while establishing 19 and 43 findings. NodeGoat's own `vendor_semantics_unresolved` is a DECLARED exclusion rather than an integrity gap, which is why it does not demote.
|
- The evidence is one column wide: NodeGoat is JavaScript alone and completes 299 of 299, VAmPI is Python alone and completes 35 of 35, PMPro is PHP with 36 regions of bundled JavaScript and completes 0 of 127 and 0 of 520 while establishing 19 and 43 findings. NodeGoat's own `vendor_semantics_unresolved` is a DECLARED exclusion rather than an integrity gap, which is why it does not demote.
|
||||||
- **This is larger than the PHP runtime limit and it lands on the declared v0.1 target.** A WordPress plugin is PHP plus its admin JavaScript, so every one of them is multi-language, so on the target this tool was built for it can detect defects and never admit one. Neither subject that currently passes could reveal it.
|
- **This is larger than the PHP runtime limit and it lands on the declared v0.1 target.** A WordPress plugin is PHP plus its admin JavaScript, so every one of them is multi-language, so on the target this tool was built for it can detect defects and never admit one. Neither subject that currently passes could reveal it.
|
||||||
@@ -587,6 +587,7 @@ Preserve all completed task evidence below.
|
|||||||
- _Three causes, not one, and the third was hidden by the record rather than by the code._ The same census gap is raised in TWO passes, arbitration and evidence, and carrying the partition in one but not the other left an unscoped copy that kept demoting everything, so the fix looked applied and was not. It was found by making the census print each outcome's reason instead of only its status, after which three separate causes stopped looking identical.
|
- _Three causes, not one, and the third was hidden by the record rather than by the code._ The same census gap is raised in TWO passes, arbitration and evidence, and carrying the partition in one but not the other left an unscoped copy that kept demoting everything, so the fix looked applied and was not. It was found by making the census print each outcome's reason instead of only its status, after which three separate causes stopped looking identical.
|
||||||
- _Admission is scoped too._ `_blocking_degradations` already exempted declared exclusions; it now also ignores a gap another language partition raised, so a JavaScript build artefact cannot block a PHP finding. A gap naming no partition still reaches everything, and so does every gap when the claim's own language is unknown, because unknown scope is not evidence of independence. Evidence: `benchmarks/measurements/2026-09-21-partition-gap-blast-radius.json`.
|
- _Admission is scoped too._ `_blocking_degradations` already exempted declared exclusions; it now also ignores a gap another language partition raised, so a JavaScript build artefact cannot block a PHP finding. A gap naming no partition still reaches everything, and so does every gap when the claim's own language is unknown, because unknown scope is not evidence of independence. Evidence: `benchmarks/measurements/2026-09-21-partition-gap-blast-radius.json`.
|
||||||
- _Boundary: contributor-scan query/context evidence, Ranker Scope Contract_
|
- _Boundary: contributor-scan query/context evidence, Ranker Scope Contract_
|
||||||
|
- _Checked 2026-10-02 per the operational-readiness audit:_ implemented and verified on PMPro (120 of 127, above). 13.3 stays open on its own blocker; this box was left unchecked only because it waits on it.
|
||||||
- _Depends: 13.3_
|
- _Depends: 13.3_
|
||||||
- _Requirements: 2.1, 2.2, 6.3, 8.1_
|
- _Requirements: 2.1, 2.2, 6.3, 8.1_
|
||||||
|
|
||||||
@@ -619,6 +620,55 @@ Preserve all completed task evidence below.
|
|||||||
- _Requirements: 8.1, 8.2_
|
- _Requirements: 8.1, 8.2_
|
||||||
|
|
||||||
|
|
||||||
|
- [x] 17. Operational readiness (2026-10-02)
|
||||||
|
- **Named by the 2026-10-02 operational-readiness audit**, maintainer-approved as the first increment. The maintainer's definition: a user installs the hook on ANY repository and gets honest, advisory-only results; nothing ever blocks a push. The audit found the plumbing sound (snapshots, resolution, deadline accounting, advisory exit 0, the JSON artifact) and five things in the way: the hook can block a push, quick rules are not part of pre-push, the engine cannot finish a 26-file repository inside 30 s, the terminal hides every skip reason, and there is no supported install path without the source checkout. Before numbers for every item are the audit's live runs on records (Python), serve-static (JavaScript) and unrolled/secure (Go), each with one planted commit.
|
||||||
|
- This group is plumbing and honesty. It enables no capability, edits no rule for a planted sample, and leaves graph reuse (the engine speed work decided in 11.6) as a later task.
|
||||||
|
- Record: `benchmarks/measurements/2026-10-02-pre-push-operational-1/record.json` (see 17.7).
|
||||||
|
- _Done 2026-10-02:_ all seven subtasks done. The engine still does not fit the 30 s hook deadline (graph reuse, the 11.6 direction, stays a later task), and plane verdicts after the push were not in this increment.
|
||||||
|
- _Requirements: 9.1–9.6_
|
||||||
|
- [x] 17.1 Never block a push the user did not choose to block
|
||||||
|
- The installed hook exits 0 with one line when `ousast` is missing (the check did not run, and how to install). In hook (stdin) mode, an argument or usage error (an invalid `OUSAST_PUSH_DEADLINE`, a cache directory inside the repository) is advisory: one line and exit 0. `--mode blocking` stays the explicit opt-in that can reject a push, and keeps its usage errors. The prior-hook input-retention failure keeps exit 1, because it is the chained hook's verdict that cannot be obtained, not ours.
|
||||||
|
- Files: `ops/pre-push`, `src/openultrasast/cli.py`. Tests: `tests/test_push_hook.py` (a push with `ousast` absent from PATH succeeds and prints the line; a bad deadline in hook mode exits 0; blocking mode keeps exit 2).
|
||||||
|
- Evidence: the audit's `hook.sh` push 1 (rejected before) and `fail.sh` (rc=2 before) rerun with exit 0.
|
||||||
|
- _Done 2026-10-02:_ the hook prints one line and exits 0 (chaining a prior hook if present) when `ousast` (or `OUSAST_COMMAND`) is missing; in hook mode without `--mode blocking`, argparse/usage errors and unexpected exceptions print one line and exit 0. Tests: `test_missing_ousast_never_blocks_the_push`, `test_hook_mode_usage_errors_are_advisory_unless_blocking` (blocking keeps exit 2), and `test_cli_refuses_the_flag_outside_explicit_replay` updated (the experimental flag is still refused in hook mode, now advisory). Gates: 1987 passed, 42 skipped; ruff, format, mypy clean.
|
||||||
|
- _Requirements: 9.1, 5.3, 5.4_
|
||||||
|
- [x] 17.2 Name every skipped check in plain words
|
||||||
|
- One terminal line per distinct coverage reason: hex-encoded paths decoded, sha256-suffixed reasons grouped with a count, JSON unchanged. "language not covered: <lang> (<n> files)" for a language with no engine frontend and no quick rules, in both `pre-push` and `scan`. A first push of a new branch says there is no base and what was checked instead. `reports.py` gains lines for the eight scan degradations `cli.py` records and the report did not render (`learning_endpoint_unavailable`, `cpg_unavailable`, `facts_unavailable`, `policy_invalid`, `model_withheld_execution_budget`, `max_findings_exceeded`, `hunter_model_unavailable`, `sandbox_unavailable`).
|
||||||
|
- Files: `src/openultrasast/push/report.py`, `src/openultrasast/push/explain.py` (new), `src/openultrasast/reports.py`, `src/openultrasast/cli.py`. Tests: `tests/test_push_report.py`, `tests/test_reports.py`.
|
||||||
|
- Evidence: the three repositories' terminal output names engine-missing, Go not covered and the new branch.
|
||||||
|
- _Done 2026-10-02:_ new `push/explain.py` turns every raw coverage reason into one plain `Skipped:` line per distinct kind (hex paths decoded, sha256 tokens dropped and counted, unknown reasons still printed); the existing one-line `Notice:` keeps its next action and gains an engine-missing variant. `language_not_covered:<lang>:<changed>:<total>` is recorded for a changed language with no engine frontend and no quick rules; `ousast scan` prints `notice: language not covered: go (N files)` and its report says so. `reports.py` renders the eight previously dropped degradations and prints any reason it has no sentence for. The hook passes `OUSAST_COMPARISON_BASE`. The new-branch line names the base it lacks; what was checked instead lands with the quick tier (17.3). Tests: `tests/test_push_explain.py` (6), two in `tests/test_reports.py`; module-audit manifest regenerated for the new module. Gates: 1995 passed, 42 skipped; ruff, format, mypy clean.
|
||||||
|
- _Requirements: 9.2, 5.2, 8.2_
|
||||||
|
- [x] 17.3 Run the quick rules as the fast first tier
|
||||||
|
- Inside the deadline and before the engine, the shipped quick rules run over the head side of the changed files and keep the findings on changed lines or in changed functions. They print as advisory pattern matches, labeled apart from admitted alerts, recorded in the artifact's `quick_tier`, and never block.
|
||||||
|
- Files: `src/openultrasast/push/quick.py` (new), `src/openultrasast/push/runner.py`, `src/openultrasast/push/report.py`. Tests: `tests/test_push_quick.py` (new).
|
||||||
|
- Evidence: serve-static's planted `eval` in the new `debug.js` is shown; whether records' planted `%`-formatted SQL is shown is recorded as is, with no rule edits for it.
|
||||||
|
- _Done 2026-10-02:_ new `push/quick.py` runs the shipped quick rules (no shadow rules, no model) over the changed head files inside `_prepare` under the shared deadline, before the engine, keeping matches on a changed line or inside a changed function (entry-point ranges); it records `files`/`bytes_read` as proof of input. The terminal shows them under "Quick rules: N pattern match(es) on changed code ... Advisory: not verified by the engine."; the artifact keeps `quick_tier`; admission and push disposition are untouched. A new branch runs the quick tier over its last commit's changes and says so. `_analyze` gained `engine="off"`. On the audit's serve-static planted commit the `eval` in the new `debug.js` is now shown (js-eval, js-xss-response at debug.js:2, ~1.1 s on the host); the planted `%`-formatted SQL in records is NOT matched by any quick rule (coverage gap, no rule edited). Tests: `tests/test_push_quick.py` (3). Gates: 1998 passed, 42 skipped; ruff, format, mypy clean.
|
||||||
|
- _Requirements: 9.3_
|
||||||
|
- [x] 17.4 Classify a file new in head as new, not unknown
|
||||||
|
- `ChangeContext` records the paths Git reports as added. An operation in an added file, when no add/delete pairing is unresolved and its head evidence is complete, is `new` with reason `file_added_in_head`, and admission accepts that reason as change evidence. An engine finding with established novelty whose only vetoes are capability vetoes renders as an advisory engine finding, not an alert. Every other unknown comparison keeps its veto.
|
||||||
|
- Files: `src/openultrasast/model/contracts.py`, `src/openultrasast/push/snapshot.py`, `src/openultrasast/push/policy.py`, `src/openultrasast/push/report.py`. Tests: `tests/test_push_policy.py`, `tests/test_push_snapshot.py`, `tests/test_push_report.py`.
|
||||||
|
- Evidence: unit tests on the comparison and admission; a live engine run if the host allows one (free memory >= 3 GB).
|
||||||
|
- _Done 2026-10-02:_ `ChangeContext.added_paths` records Git status-A paths (rename targets excluded). In `_compare_evidence`, after every existing context/head/deadline check, an operation in an added file is `new`/`file_added_in_head` when the claim is entailed, undischarged and has change evidence, and no base answer holds the same operation (else `operation_moved_within_change`); an unpaired add/delete still blocks as `change_context_unresolved`. Admission accepts `(new, file_added_in_head)`; with the empty registry such a candidate's only veto is `capability_unavailable`, and the terminal now shows it under "Engine: N finding(s) this push introduced. Advisory ... not an alert." Any comparison, evidence or location veto keeps a candidate out of that line. Tests: `tests/test_push_new_file.py` (9). Live engine evidence depends on host memory (17.7). Gates: 2007 passed, 42 skipped; ruff, format, mypy clean.
|
||||||
|
- _Requirements: 9.4, 3.4_
|
||||||
|
- [x] 17.5 Say when the engine cannot finish, and offer it in the background
|
||||||
|
- One line when the engine did not finish (deadline, engine not installed). `--engine background` starts the engine detached after the quick tier, writes its result under the artifact directory and, on the next run, prints "engine result for <head> from the previous push". One background engine at a time; `--engine off` skips it with a stated line. No speed work.
|
||||||
|
- Files: `src/openultrasast/push/background.py` (new), `src/openultrasast/push/runner.py`, `src/openultrasast/cli.py`, `ops/pre-push`. Tests: `tests/test_push_background.py` (new).
|
||||||
|
- Evidence: a hook push with `OUSAST_PUSH_ENGINE=background` followed by a second push showing the previous result.
|
||||||
|
- _Done 2026-10-02:_ `--engine inline|background|off` (hook: `OUSAST_PUSH_ENGINE`) and `--background-deadline` (default 900 s). `off` and `background` run the quick tier and record `engine_off`/`engine_background` with a plain skip line; the deadline line names the deadline and the background option. New `push/background.py` starts one detached explicit replay (`start_new_session`, stdin closed, output to a log) per artifact directory and repository (lock file with a live-pid check: a second start reports busy), writes its artifact under `<artifact dir>/engine/<repo key>/<head>.json`, and the next run prints "Engine result for <head> from the previous push: ..." once, then marks it shown. Not usable through the Docker wrapper (the container exits with the hook); documented in 17.6. Tests: `tests/test_push_background.py` (4, one launching a real detached run). Gates: 2011 passed, 42 skipped; ruff, format, mypy clean.
|
||||||
|
- _Requirements: 9.5, 4.1, 4.2_
|
||||||
|
- [x] 17.6 Install, Docker and documentation for a first-time user
|
||||||
|
- `ousast pre-push install|uninstall [--force] [--docker] [REPO]` ships the hook as package data, honours `core.hooksPath`, refuses an existing hook unless `--force` (which chains it as `pre-push.before-ousast`), and works from a pip install. `ops/ousast-docker` is a hook-callable script (not a shell function) that passes stdin and the artifact path through. The `openultrasast:dev` image is rebuilt only if `df -h /` shows at least 4 GB free; otherwise the rebuild command is documented and the record says it was not rebuilt. `docs/scanning.md` gains a first-run guide, a glossary of skip reasons, a GitHub Actions recipe and full `--help` text; README tiers 1 and 2 updated, pulling `ghcr.io/norandom/openultrasast:2.0.1` where the wrapper allows.
|
||||||
|
- Files: `src/openultrasast/push/hook/pre-push`, `src/openultrasast/push/install.py` (new), `src/openultrasast/cli.py`, `ops/ousast-docker`, `docs/scanning.md`, `README.md`, `ops/README.md`. Tests: `tests/test_push_install.py` (new), `mkdocs build --strict`.
|
||||||
|
- Evidence: install from the package into a scratch clone, push through it, uninstall restores the prior hook.
|
||||||
|
- _Done 2026-10-02:_ `ousast pre-push install|uninstall [REPO] [--force] [--docker]` (`push/install.py`) installs the hook shipped as package data (`openultrasast/push/hook/pre-push`, byte-identical to `ops/pre-push`, verified in the built wheel), honours `core.hooksPath` without editing it, refuses an existing hook unless `--force` (ours is replaced; a foreign hook moves to `pre-push.before-ousast` and is chained), and uninstall removes only a hook it wrote and restores the chained one. `ops/ousast-docker` (also package data; `--docker` installs it beside the hook, which then uses it) is a script that mounts the repository read-only and the artifact directory at their own absolute paths, passes stdin with `-i`, runs as the user with no network and `--memory 3g`, defaults to `ghcr.io/norandom/openultrasast:2.0.1`, never pulls inside a hook, and exits 0 unless `OUSAST_PUSH_MODE=blocking`. The hook passes `--engine`/`--comparison-base` only when set, so an older ousast or image still runs. Every pre-push flag has help text; `docs/scanning.md` has the first-run guide, settings table, new-branch and background notes, the artifact keys, a skip-reason glossary (a test keeps it in sync with `push/explain.py`), a GitHub Actions recipe and the full `--help` text; README tiers 1 and 2 and `ops/README.md` updated (pull and tag the GHCR image instead of building). **Image not rebuilt:** `df -h /` showed 1.1 GB free (< 4 GB); rebuild with `docker build -t openultrasast:dev .`. Tests: `tests/test_push_install.py` (11) and the glossary test. Gates: 2022 passed, 42 skipped; ruff, format, mypy clean; `mkdocs build --strict` exit 0.
|
||||||
|
- _Requirements: 9.6, 7.4_
|
||||||
|
- [x] 17.7 Prove it on the audit's three repositories
|
||||||
|
- Rerun records, serve-static and unrolled/secure (same planted commits) with the new build in quick mode and with the hook installed, plus one Docker engine run on serve-static if free memory >= 3 GB and free disk >= 1 GB. Prove each run read its input (files, bytes).
|
||||||
|
- Files: `benchmarks/measurements/2026-10-02-pre-push-operational-1/record.json`. Evidence: before (the audit) and after: exit codes, wall time against the deadline, findings shown, every skip line printed. A planted sample still missed is a coverage gap, recorded, never a rule edit.
|
||||||
|
- _Done 2026-10-02:_ `benchmarks/measurements/2026-10-02-pre-push-operational-1/record.json` (before = the audit, after = this build installed from its wheel into a scratch venv). 10 hook pushes and 6 replays, all exit 0, 0.69-1.21 s against 30 s on the host; the quick tier read exactly the changed file and every head snapshot read the full tree (records 43,577 B, serve-static 61,525 B, secure 103,023 B). The planted JS `eval` in the new `debug.js` surfaces: quick tier on the host, and as two advisory engine findings classified `new`/`file_added_in_head` in one Docker engine run (190.9 s against a 300 s deadline; MemAvailable 3.28 GB, 1.16 GB disk). **The planted Python `%`-formatted SQL is still missed**: no quick rule matches it and the host has no engine. Recorded as a coverage gap, no rule edited. Go is named as not covered. The proof caught a wrong install hint (`pip install openultrasast`, not on PyPI), corrected in the hook in this commit.
|
||||||
|
- _Requirements: 9.1–9.6, 6.3_
|
||||||
|
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
- Task5.2 measured PMPro timing out after both snapshots were materialized but before change context/graph analysis. Group6 must account for compatible snapshot/discovery reuse as well as graph reuse; identical-tip hits cannot substitute for the later representative changed-code latency gate. No new detector, scope heuristic or incremental CPG mutation is implied.
|
- Task5.2 measured PMPro timing out after both snapshots were materialized but before change context/graph analysis. Group6 must account for compatible snapshot/discovery reuse as well as graph reuse; identical-tip hits cannot substitute for the later representative changed-code latency gate. No new detector, scope heuristic or incremental CPG mutation is implied.
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Brief: OpenCode-MCP proof arm (follow-up to search-with-proof)
|
||||||
|
|
||||||
|
Status: brief / not yet specced. Proposed 2026-10-07 by the maintainer. Builds on
|
||||||
|
`search-with-proof` (see `../search-with-proof/verification-architecture.md`).
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
The search arm (directed, Cairn-inspired) works. The **proof-authoring** last mile is the drag:
|
||||||
|
|
||||||
|
1. **Safety-classifier refusals.** Authoring exploit demos repeatedly tripped a safety classifier
|
||||||
|
this session when exploit payloads or step-by-step attack prose passed through the model or a
|
||||||
|
tool prompt. The mitigation so far (keep payloads on disk, treat demos as opaque) is a workaround,
|
||||||
|
not a fix, and it limits how directly the proof model can reason about the exploit.
|
||||||
|
2. **Nondeterministic wiring.** The agent authors a demo but intermittently mis-wires the app to
|
||||||
|
the oracle fixture (served directory for path, database URL for SQL), so a demonstrable pair
|
||||||
|
comes back inconclusive until a retry. Each miss has cost a paid run.
|
||||||
|
|
||||||
|
Proposal: drive the proof with an **agentic coding tool (OpenCode MCP)** running **inside gVisor**,
|
||||||
|
using a model chosen for exploit authoring rather than one that refuses it, and **learn** the
|
||||||
|
proof-authoring policy with DSPy from **traces logged in OpenObserve**.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
- **Search model:** DeepSeek (current). Good at the directed fact/intent search.
|
||||||
|
- **Proof model:** GLM 5.3 (Anthropic-recommended) and/or an abliterated model via
|
||||||
|
`ABLITERATION_BASE_URL` (`abliterated large v2`). The abliterated model authors exploit **demos**
|
||||||
|
without the refusals that block a safety-trained model on this defensive task.
|
||||||
|
- **Proof driver:** OpenCode MCP (github.com/AlaeddineMessadi/opencode-mcp) as the agentic
|
||||||
|
demo-author, running inside the gVisor verification task so the exploit-authoring is sandboxed.
|
||||||
|
- **Learning:** DSPy compiles/optimizes the proof-authoring program (demo schema, wiring, steps)
|
||||||
|
from past traces, instead of the per-family hand-guidance we keep adding (which is now hitting
|
||||||
|
the prompt character budget — a signal the hand-rule approach is near its limit). Matches the
|
||||||
|
maintainer's "learned decisions, not hand-tuning" direction.
|
||||||
|
- **Tracing:** proof runs (reason/explore/author/verify, timings, outcomes, error classes) log to
|
||||||
|
**OpenObserve AI** at `OPENOBSERVE_URL` (org `OPENOBSERVE_ORG`, stream `OPENOBSERVE_TRACES_STREAM`).
|
||||||
|
DSPy consumes these traces as its training signal.
|
||||||
|
- **Credentials:** stored in `.env` like the existing keys, referenced by name only:
|
||||||
|
`ABLITERATION_API_KEY`, `ABLITERATION_BASE_URL`, `ABLITERATION_MODEL`, `OPENOBSERVE_URL`,
|
||||||
|
`OPENOBSERVE_ORG`, `OPENOBSERVE_TRACES_STREAM`, `OPENOBSERVE_TOKEN`. No secret value ever enters a
|
||||||
|
tracked file, a prompt, a log stream, or a commit.
|
||||||
|
|
||||||
|
## The trust-domain decision (must be resolved first)
|
||||||
|
|
||||||
|
The current design keeps the **model key on the VM only**; cluster tasks carry no secrets and have
|
||||||
|
restricted egress (verify tasks reach the files host only). Running the proof **model** inside gVisor
|
||||||
|
(as OpenCode MCP would) breaks both: the task would hold a model key and need egress to the model
|
||||||
|
API host. Two options:
|
||||||
|
|
||||||
|
- **A. Scoped key + egress in the cluster (operator-managed).** Inject a spend-limited proof-model
|
||||||
|
key into the proof task (like the existing `gemini-api-secret` slot) and add a per-host egress
|
||||||
|
allowlist for the proof-model API. Keeps the fully-sandboxed agentic authoring the maintainer
|
||||||
|
wants; requires an operator change and widens the cluster's secret/egress surface.
|
||||||
|
- **B. Brain on the VM, execution in gVisor (current split preserved).** OpenCode MCP and the proof
|
||||||
|
model run on the VM; only the DAST execution stays in gVisor. No new cluster secret or egress;
|
||||||
|
but the agentic authoring loop is not itself sandboxed.
|
||||||
|
|
||||||
|
Recommendation: start with **B** (no trust-domain change, fastest to try the abliterated proof model
|
||||||
|
and DSPy loop), and move to **A** only if sandboxing the authoring loop itself proves necessary.
|
||||||
|
|
||||||
|
## Security scope (why an abliterated model is in-scope here)
|
||||||
|
|
||||||
|
This is authorized **defensive** work: authoring proof-of-concept demos for **known CVE fix-pairs**
|
||||||
|
in a **gVisor-isolated** corpus, to demonstrate a regression before push. Guardrails to keep:
|
||||||
|
|
||||||
|
- Demos are **data**, not agent code; they stay in the gitignored private area, never echoed.
|
||||||
|
- No real-world or third-party live targets; only the frozen fix-pair corpus and owned oracles.
|
||||||
|
- gVisor is the execution boundary; the abliterated model never runs code itself, it only writes a
|
||||||
|
declarative demo the trusted verifier executes.
|
||||||
|
- The abliterated model is confined to proof-authoring; the search and all reporting stay on the
|
||||||
|
normal models.
|
||||||
|
|
||||||
|
## Open questions for the spec
|
||||||
|
|
||||||
|
1. A or B for the trust domain (above).
|
||||||
|
2. Does the abliterated proof model actually raise the demonstrated rate and remove the wiring
|
||||||
|
misses, measured on the pairs already shown demonstrable (xiaomusic, glance)? Qualify before scaling.
|
||||||
|
3. DSPy program shape: what is the proof-authoring signature (inputs: candidate + oracle contract;
|
||||||
|
output: a valid, wired demo), and what metric does it optimize (demonstrated rate at zero false
|
||||||
|
proofs) from OpenObserve traces?
|
||||||
|
4. Does OpenCode MCP fit the existing executor/verifier task contract (no-secret, presigned I/O,
|
||||||
|
scratch/egress limits), or does it need a new task kind?
|
||||||
|
5. Cost envelope per proof vs the current DeepSeek authoring.
|
||||||
|
|
||||||
|
## OpenCode MCP as the endpoint for both PoC and fix
|
||||||
|
|
||||||
|
Maintainer note (2026-10-07): OpenCode MCP is a good server endpoint for the PoC **and** the fix,
|
||||||
|
not only proof authoring. The same agentic endpoint can:
|
||||||
|
|
||||||
|
1. Author the PoC (the demonstrated proof) for a caught finding.
|
||||||
|
2. Write a candidate fix against the finding's repair direction and the failing PoC.
|
||||||
|
3. Hand the fix back to the differential verifier, which re-runs the PoC and confirms absence
|
||||||
|
(the proof flips from failing to passing).
|
||||||
|
|
||||||
|
This turns the "no auto-patch" gap in `../../../docs/use-case-fix-and-reverify.md` into "the proof
|
||||||
|
endpoint also proposes the fix, and the fix is accepted only when the proof it generated goes green."
|
||||||
|
The PoC is the acceptance test for the fix, so a wrong fix is caught, not trusted. The fix stays a
|
||||||
|
proposal for the developer to review; the verifier's green proof is the objective signal, not the
|
||||||
|
model's say-so. This fix capability is specced alongside the proof authoring when this arm is built.
|
||||||
|
|
||||||
|
## Not in scope
|
||||||
|
|
||||||
|
Changing the search arm, the oracle differential, or the abstract-interpretation triage. This brief
|
||||||
|
is about the **proof-authoring and fix** driver and its learning loop, planned as a full spec after
|
||||||
|
the safety net is qualified.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"feature_name": "proof-arm-opencode",
|
||||||
|
"created_at": "2026-10-07T14:40:00Z",
|
||||||
|
"updated_at": "2026-10-07T14:40: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": "OpenCode-MCP proof-authoring arm for search-with-proof: abliterated/GLM proof model, DSPy-optimized from OpenObserve traces; trust-domain decision (A scoped-key-in-cluster vs B brain-on-VM) must be resolved first. Creds in .env by name."
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Search-with-proof — handoff (2026-10-06)
|
||||||
|
|
||||||
|
State at handoff, for resuming on the search problem and abstract interpretation.
|
||||||
|
|
||||||
|
## RESOLVED 2026-10-07 — the one open question is CLOSED: first cluster demonstration achieved
|
||||||
|
|
||||||
|
The "first `demonstrated`" question below is answered. search-with-proof produced its first autonomous
|
||||||
|
`demonstrated` finding end to end on kube-ax, and a blind negative confirmed no false proof.
|
||||||
|
|
||||||
|
- **Pair:** hanxi/xiaomusic (pilot pair 13), family `path`, vulnerability class **prefix-check bypass**
|
||||||
|
(`startswith(root)` without a trailing separator; the fix adds `os.sep`). Image `e2c8b460`.
|
||||||
|
- **Vulnerable side:** `demonstrated` — owned effect in 3/3 affected runs, 0/3 safe runs. $0.37, 55 calls.
|
||||||
|
- **Fixed side (blind negative, Req 6.3):** `inconclusive`, **0 false proofs**. $0.56, 85 calls.
|
||||||
|
- **Pair total:** $1.22. Record: `benchmarks/measurements/2026-10-07-search-first-cluster-demonstration/`.
|
||||||
|
|
||||||
|
**The original pair-6 both-sides-false was a TWO-part diagnosis, both now fixed:** the demo did not reach the
|
||||||
|
observable (demo/agent), and — the decisive one — the path oracle only modelled **classic parent traversal** while
|
||||||
|
many real path CVEs (xiaomusic included) are **prefix-bypass**, which reaches a sibling that string-extends the served
|
||||||
|
dir, not the parent. The oracle now also plants a prefix-sibling canary.
|
||||||
|
|
||||||
|
**Fix chain that enabled the demonstration (each merged to main and validated by a paid run):**
|
||||||
|
1. Parity — agent is told the observable's location (path oracle reached parity with command/ssrf).
|
||||||
|
2. Verify-time-only — the observable is injected at verify time and absent during exploration; stop the explore-time
|
||||||
|
reachability trap that burned budget without authoring a demo.
|
||||||
|
3. Fair-share pilot budget — each search gets a fair share of `--ceiling-usd` with rollover, so one expensive search
|
||||||
|
no longer starves the rest.
|
||||||
|
4. Served-root guidance — the demo MUST set the app's served directory to the `${served_root}` placeholder.
|
||||||
|
5. Oracle prefix-bypass coverage — plant a sibling canary for the `startswith`-without-separator class.
|
||||||
|
6. Control-signal feedback — the keystone control (does the app serve `${served_root}`?) is fed back to the agent as a
|
||||||
|
`served-root` hint so it self-corrects its wiring. This was the decisive last step.
|
||||||
|
|
||||||
|
**Keystone (honest measurement), merged earlier:** the verifier runs a benign positive control; a broken or mis-wired
|
||||||
|
app yields `could_not_run`/annotation, never a silent clean-looking `inconclusive`.
|
||||||
|
|
||||||
|
**Next toward the pilot exit (Req 6.2):** fan out the path family to ≥5 demonstrations with 0 false proofs, then the
|
||||||
|
≥73-negative BLOCK qualification (Req 6.3). The glance pair (classic traversal) also demonstrates once its npm
|
||||||
|
dependency build-break (`filed`→`mime@4`) is pinned.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The one open question (HISTORICAL — resolved above)
|
||||||
|
|
||||||
|
The full chain runs end to end on kube-ax. Every execution layer is cleared. The **first `demonstrated` has not
|
||||||
|
happened yet**, and the remaining gap is narrow and reproducible at **$0 model cost** via `demo_replay`.
|
||||||
|
|
||||||
|
No-model replay of pilot pair 6 (path / Node / npm build), both sides, image `aa54880`:
|
||||||
|
- Both sides **build, start, and run** (ready ~18–20 s, "fresh task run completed", no `could_not_run`).
|
||||||
|
- Oracle observed **no effect on either side** → `inconclusive`, "no consistent differential".
|
||||||
|
|
||||||
|
Both-sides-false means it is **not the harness** any more. It is one of:
|
||||||
|
1. The demo's attack step as written does not trigger the effect even on the vulnerable revision, or
|
||||||
|
2. The oracle does not capture the app's output where the effect would show.
|
||||||
|
|
||||||
|
## How to close it (no model spend)
|
||||||
|
|
||||||
|
- Control: the **probe path app** (`src/openultrasast/search/probe_apps/`) is known-good and *does* observe through the
|
||||||
|
same verifier code path. Diff its behaviour against the real app: how the served directory is wired
|
||||||
|
(`${served_root}` substitution in `start.arguments`), and where the oracle reads the nonce from (app stdout/response
|
||||||
|
capture in `oracles.py` `PathOracle.observe`).
|
||||||
|
- Re-run with `python -m benchmarks.search.demo_replay --both --demo <stored demo.json> --family path --lane ax
|
||||||
|
--image <digest> ...`. Stored demos live under `benchmarks/search/private/runs/*/<pair>/demo/demo.json` (gitignored).
|
||||||
|
- **Classifier note:** keep exploit payloads on disk. Hand Codex file-path tasks that treat demo contents as opaque;
|
||||||
|
print outcomes / phase timings / error *classes*, never request strings, env values, or step bodies. See memory
|
||||||
|
`search-with-proof-direction` and the classifier false-positive note. Code changes never tripped it; echoing
|
||||||
|
payloads or step-by-step attack prose did.
|
||||||
|
|
||||||
|
## What is done and merged (main)
|
||||||
|
|
||||||
|
- Trust-domain split: model key on the VM only; executor and verifier tasks carry no secrets; exchange via presigned
|
||||||
|
objects. Declarative demos (data, not agent code). gVisor is the cluster boundary.
|
||||||
|
- Verifier + 5 oracles pass inside a gVisor task (probe: 5/5 demonstrated, gaming demos inconclusive).
|
||||||
|
- Shared task base image, code-last layers; `plane/task-base.digest` pins it; `engine-image.yml` builds only on
|
||||||
|
dispatch / tags / Dockerfile / pin changes. Measured unpacked: search 2.07 GB, engine 3.61 GB.
|
||||||
|
- Pilot driver (`benchmarks/search/pilot_run.py`), budgets with reservation, named exhaustion limits, `demo_replay`.
|
||||||
|
- Per-host egress live: exec → any 443; verify/probe/pass → files host; replay → files + github. Task names carry
|
||||||
|
the right prefixes.
|
||||||
|
|
||||||
|
## Plumbing defects fixed along the way (each cost one run)
|
||||||
|
|
||||||
|
step allowance → family-vs-oracle mapping → build memory (256 MB SIGTRAP) → manifest path appended positionally →
|
||||||
|
startup env + data files → dead-end intent dedupe → archive size (edge ~100 MB, now split+gzip) → two-interpreter
|
||||||
|
image (pip 3.13 vs venv 3.12 → use `sys.executable`) → checkout root on sys.path → `node_modules` path.
|
||||||
|
|
||||||
|
## Next, once a demo demonstrates
|
||||||
|
|
||||||
|
1. Fan out the pilot across all 20 pairs, both sides (feasibility: demonstrated rate per family, zero false proofs on
|
||||||
|
fixed sides). Exit: ≥1 family with ≥5 demonstrations and zero false proofs (Req 6.2).
|
||||||
|
2. BLOCK qualification per family: 0 false proofs over ≥73 negatives from ≥73 repos (Req 6.3).
|
||||||
|
3. G1 arm on the unseen pool vs the triage/decision arms at equal false-alarm budgets (Req 6.5).
|
||||||
|
|
||||||
|
## Cluster / spend facts
|
||||||
|
|
||||||
|
- kube-ax: **2 tasks in flight, one per worker** (operator `--max-actors=1`). Dispatcher caps AX concurrency at 2;
|
||||||
|
raise it only when the operator raises worker count. "no free workers available" = retry, not failure.
|
||||||
|
- `/workspace` ~3 GiB free per node until the volume move; scratch guard 2 GiB/task.
|
||||||
|
- Pilot spend to date ~$11 across 9 paid runs; the decisive replay was $0.
|
||||||
|
|
||||||
|
## Decision model vs search — current read
|
||||||
|
|
||||||
|
- Decision/classification: measured, **promising as triage** (within-pair AUC 0.762; trained logistic ties at 0.721),
|
||||||
|
**capped as a blocker** (cannot prove 95% precision). Reframed as triage (`learned-decision-engine` Req 6.5).
|
||||||
|
- Search-with-proof: **not yet demonstrated**, but no observation so far argues against the idea — every failure was
|
||||||
|
our execution, now cleared. The pair-6 demo/oracle question is the first verdict on the method itself.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Brief: search with proof
|
||||||
|
|
||||||
|
Maintainer, 2026-10-05, after reviewing Cairn (github.com/oritera/Cairn): "we are still not progressing through the
|
||||||
|
state space search / not finding what we need and the progress is sort of slow ... i think something is missing."
|
||||||
|
And: "I'd move to primarily executing in the cluster."
|
||||||
|
|
||||||
|
## What was missing
|
||||||
|
|
||||||
|
Every detection step so far classifies: it scores one function excerpt and then tries to make the score precise
|
||||||
|
enough to block. Calibration, Wilson bounds, pooled block gates, prompt variants and trained models all hit the same
|
||||||
|
wall: ranking is decent (within-pair AUC about 0.76), but precision at the top cannot be proven to 95% on our data.
|
||||||
|
|
||||||
|
Cairn treats a security problem as a directed search with an objectively checkable goal. A shared board holds
|
||||||
|
confirmed facts, open intents and human hints. A reasoner proposes independent intents; explorers with real tools
|
||||||
|
each confirm one fact; the loop ends when the goal is demonstrably met.
|
||||||
|
|
||||||
|
Three things follow for OpenUltraSAST:
|
||||||
|
|
||||||
|
1. **An objective success condition.** A finding is demonstrated when a test or proof of concept fails on the
|
||||||
|
vulnerable code and passes on the fixed code (or, for a push, fails on head and passes on base where applicable).
|
||||||
|
A demonstrated finding is right by construction; it needs no calibration and no training data, so it generalises
|
||||||
|
to unseen repositories by construction.
|
||||||
|
2. **Exploration with tools.** Agents that read other files, follow callers, build and run the code, instead of a
|
||||||
|
single-shot judgement of one excerpt.
|
||||||
|
3. **Search state per repository and push.** Facts and intents accumulate on a board instead of every question
|
||||||
|
starting from zero.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
- The cheap layers (quick rules, engine, classifier) become triage: they rank candidates.
|
||||||
|
- Agentic search runs only on the top-ranked candidates of a push, under a per-push budget.
|
||||||
|
- BLOCK only on a demonstrated finding. Advisory for plausible, unproven ones. "Could not demonstrate" is recorded as
|
||||||
|
such, never as "not vulnerable".
|
||||||
|
- Execution is primarily in the kube-ax cluster: reason and explore steps are AX Tasks; the VM coordinates.
|
||||||
|
- Model access: a scoped, budget-limited key in the cluster, managed by the operator.
|
||||||
|
|
||||||
|
## Not in scope
|
||||||
|
|
||||||
|
Copying Cairn code (AGPL-3.0; this repository is Apache-2.0). Offensive use against systems we do not own: all
|
||||||
|
execution is against code under analysis, inside gVisor sandboxes.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Design: search with proof (v2, after two Codex reviews; the Claude Doc is the readable version)
|
||||||
|
|
||||||
|
## Why: from scoring excerpts to proving findings
|
||||||
|
BLOCK will mean a vulnerability was demonstrated and re-run by the system, not that a score passed a threshold. Every detection step so far classified one function excerpt, then tried to make the score precise enough to block. Same wall in every experiment: LLM programs (exp-005 A) within-pair AUC 0.762, pooled gate needs >=73 clean flags; trained logistic 0.721 (tie, CI spans zero); gradient boosting 0.597 (worse). Cairn (AGPL, design only) treats a security problem as directed search with a checkable goal. Three things follow: (1) objective goal: test/PoC fails on vulnerable, passes on fixed; needs no calibration or training data, so it carries over to unseen repos; (2) exploration with tools (read other files, follow callers, build, run); (3) search state: facts and intents accumulate on a board per search.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
VM coordinates and is the only writer to the board (memory store); every model call and execution runs as an AX Task on kube-ax. Triage (quick rules, engine, classifier) -> top K candidates -> first facts on the board. Reason task (reads snapshot, returns complete or <=3 intents) <-> board; Explore tasks (one per intent; read, grep, run, write demo/; return one fact) <-> board; Verify task (fresh sandbox, planted canary, vulnerable vs fixed / head vs base) -> outcome: BLOCK only if demonstrated, else advisory or silent. Scheduler and budgets on the VM check caps before every dispatch. Tasks read/write via presigned links; store credentials stay on the VM. VM is fallback lane, kind local fallback, same image.
|
||||||
|
|
||||||
|
## The demonstration contract (v2)
|
||||||
|
The verifier, not the agent, observes the effect, on one side and not the other, consistently, in fresh sandboxes. Artefact: demo/ with build.sh and attack.sh (or a request file) touching only the documented external interface; its exit code and output are ignored. Oracles owned by the verifier per family: injection SQL = canary row seen in verifier's query log/response capture; path = canary file outside root read, seen by file-access watch; XSS = script executes in verifier's headless browser; SSRF = request arrives at verifier's in-sandbox listener; injection (command) = canary side effect seen by process watch. Deserialisation, access control, config/secrets: no owned oracle yet -> plausible_unproven at most. Attacker boundary excludes source tree, dependencies, runtime, fixtures; checkout and dependencies read-only during run; untracked files, dependency swaps, runtime patching refused. Differential: fix pair vulnerable-vs-fixed; push head-vs-base. Both sides must build and pass readiness independently; each side 3 runs from fresh state; inconsistent runs rejected; a side that fails to build or start => inconclusive, never a pass. Outcomes: demonstrated (BLOCK once the family is qualified), plausible_unproven (advisory, a visible finding), could_not_build/could_not_run/inconclusive (advisory if facts support, else silent; never "not vulnerable"), no_oracle (as plausible_unproven), no_evidence (silent). REGRESS's nonzero-exit-is-triggerable rule (regress/verdict.py) is not reused for proof; its sandbox plumbing is.
|
||||||
|
|
||||||
|
## Agent worker and sandbox tools
|
||||||
|
Own small tool loop in src/openultrasast/search/ via the existing OpenAI-compatible client (provider swappable, spend metered, Apache-2.0). Image ousast-search-task from the AX task runner, toolchains for Python, Node and TypeScript, PHP, Java; repo fetched over HTTPS. Tools: list_files, read_file (400 lines/call), grep (200 matches), run (per-call timeout, truncated output), write_demo (only write, under demo/), finish (one fact or recorded failure). Engine evidence (taint path, sinks, missing guards) is in the explore prompt. Reason step uses no tools.
|
||||||
|
|
||||||
|
## Triage, budgets and model access (v2)
|
||||||
|
Top K per push (K=3); candidate carries family hint + engine evidence as first facts. Caps (ceilings, not estimates): reason rounds 4/search, intents 3/round, explore 15 min/intent, $0.50/search, $1.50/push, 3 demos x 2 sides x 3 runs. Spend reserved before each model call at max cost and settled after (plane/budget.py gets a reservation step); per-task memory/disk/cleanup limits sized to workers (~3 vCPU, 5-5.75 GiB, two heavy lanes); total run wall-time and retry cap. Worst case 120 task-hours for 40 searches (~60 h on two lanes); a 1,300-change slice at K=3 could reach $1,950. First 3 pilot searches measure real cost; pilot ceiling set from that and asked for. Model key: operator scoped secret (AX injects only GEMINI_API_KEY; operator decides mechanism); never in manifest/log/board/record; repository code cannot read it: model client runs in a separate process as its own user.
|
||||||
|
|
||||||
|
## Pilot, qualification, then the unseen pool (v2)
|
||||||
|
1. Feasibility pilot: 20 dev-corpus pairs (never v3/pool), the four pool families with an owned oracle (injection, path, output encoding, untrusted destination), across the pool's five languages (Python, JS, TS, PHP, Java). Candidate = labelled function (measures proof, not triage); advantages recorded; fixed-side search never sees the vulnerable revision or its demo. Reported: demonstrated rate per family/language, false proofs, could_not_build/run/inconclusive/no_oracle rates, spend/tasks/wall. Exit: >=1 family with >=5 verified demonstrations and zero false proofs, else stop.
|
||||||
|
2. BLOCK qualification per family: 0/20 still allows 16.1% (Wilson 95%); a family BLOCKs only after >=73 negative-side searches (fixed revisions + ordinary changes) with zero demonstrated (upper bound 5%, same bar as the pooled gate). Until then demonstrations show as advisories.
|
||||||
|
3. G1 arm on the unseen pool: triage top K on every replayed change in a fresh slice; scored with the pool's rules on every visible finding incl. advisories, family + location match; 1/5/10% FA budgets beside classifier arms; adopted only under the no-regression rule. No evaluation label, fixing revision or evaluation board is ever an input to unseen search.
|
||||||
|
Every run reports the coverage ceiling (true candidate x owned oracle x both sides runnable x genuine differential, build failures in the denominator) next to catch. Delayed results go via push/background.py and are scored separately from push-time results.
|
||||||
|
|
||||||
|
## Decisions, risks and open questions
|
||||||
|
Largest risk: environment failure caps the demonstrated rate. Risks: builds fail; agent games oracle (mitigated by re-run, canary, differential, tracked-file check); repo code misuses egress (gVisor; ask operator for no-egress verify class); cost (hard caps); minutes per search too slow for synchronous hook (decide product path after pilot). Open: operator key mechanism + no-egress class; own tool loop vs CLI agent; pilot go-ahead $20; how a minutes-long search reaches the pre-push user.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Declarative verification and separate execution domains
|
||||||
|
|
||||||
|
Implemented on `search-with-proof`, 2026-10-05, under the maintainer's explicit instructions.
|
||||||
|
`git merge -q main` failed because the shared Git metadata is read-only. The amended requirements
|
||||||
|
were read from the main checkout, including Requirements 3.1 and 5.2. No commits or network operations.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Strict, bounded JSON demonstration schema, also supplied to the model's `write_demo` tool.
|
||||||
|
Verifier-owned recipes cover pip, npm, composer, Maven, Gradle and no build. Runtime entrypoints,
|
||||||
|
CLI/HTTP sequences and earlier-response captures are validated; private fixtures, environment
|
||||||
|
substitutions, canary captures and script artefacts are rejected. Injection/traversal strings remain
|
||||||
|
legitimate request payload data, never executable verifier instructions.
|
||||||
|
- The verifier owns build, readiness and request execution. Local runs retain Bubblewrap (`userns`
|
||||||
|
on this host). HTTP requests require an acknowledged readiness gate; incomplete observations fail
|
||||||
|
closed. Build-generated internal symlinks are supported without accepting external or cyclic links.
|
||||||
|
- A supplied AX side dispatcher selects `task-boundary` and creates three separate fresh tasks per
|
||||||
|
side. Failure of local isolation without a dispatcher fails closed.
|
||||||
|
- The model-holding worker delegates list/read/grep/run to an executor interface. Production uses
|
||||||
|
presigned command/result objects; the in-process adapter is for tests. Commands have sequence
|
||||||
|
numbers, identical-retry caching, deadlines, stop and bounded results. Uncertain outcomes prevent
|
||||||
|
later commands from reusing that executor session.
|
||||||
|
- AX executor tasks use the `ousast-engine-search-exec-` prefix. Task entrypoints reject inherited
|
||||||
|
credentials before reading checkout input, then use an explicit environment allowlist.
|
||||||
|
|
||||||
|
## Changed files
|
||||||
|
|
||||||
|
Production:
|
||||||
|
- `src/openultrasast/search/demo.py`
|
||||||
|
- `src/openultrasast/search/executor.py`
|
||||||
|
- `src/openultrasast/search/worker.py`
|
||||||
|
- `src/openultrasast/search/verify.py`
|
||||||
|
- `src/openultrasast/search/verify_task.py`
|
||||||
|
- `src/openultrasast/search/_sandbox.py`
|
||||||
|
- `src/openultrasast/search/oracles.py`
|
||||||
|
- `src/openultrasast/search/probe.py`
|
||||||
|
- `src/openultrasast/search/probe_apps/app.py.txt`
|
||||||
|
- `benchmarks/ax/batch.py`
|
||||||
|
- `benchmarks/ax/search_verify.py`
|
||||||
|
- `plane/Dockerfile.search-task`
|
||||||
|
|
||||||
|
Tests and audit:
|
||||||
|
- `tests/test_search_demo.py`
|
||||||
|
- `tests/test_search_executor.py`
|
||||||
|
- `tests/test_search_verify_transport.py`
|
||||||
|
- `tests/test_search_verify.py`
|
||||||
|
- `tests/test_search_worker.py`
|
||||||
|
- `tests/test_search_isolation.py`
|
||||||
|
- `tests/test_search_probe.py`
|
||||||
|
- `tests/test_ax_batch.py`
|
||||||
|
- `benchmarks/measurements/2026-09-08-module-audit.json`
|
||||||
|
|
||||||
|
This report is the additional documentation file.
|
||||||
|
|
||||||
|
## Fresh validation
|
||||||
|
|
||||||
|
`/home/mc/Source/OpenUltraSAST/.venv/bin/pytest -q tests/test_search_*.py tests/test_ax_batch.py tests/test_plane_literals.py tests/test_module_audit.py -rs`
|
||||||
|
|
||||||
|
Result: **171 passed, 3 skipped, exit 0**, in 35.46 seconds.
|
||||||
|
The three skips are Chromium isolation/network setup, SSRF localhost sockets and HTTP localhost sockets,
|
||||||
|
all prohibited by this host's policy. Host verification mode was explicitly checked: `userns`.
|
||||||
|
|
||||||
|
Ruff passes for search sources, AX changes and affected tests. Targeted strict mypy passes for the seven
|
||||||
|
changed Python search implementation modules checked. Independent review approved the implementation,
|
||||||
|
including follow-up timeout, readiness, capture, credential and generated-link regressions.
|
||||||
|
|
||||||
|
Module audit: **171 modules**, **165 load-bearing**, **6 standalone capabilities**, **0 orphaned**.
|
||||||
|
The audit read the worktree source explicitly with `PYTHONPATH=src`.
|
||||||
|
|
||||||
|
## Remaining validation gaps
|
||||||
|
|
||||||
|
- No live AX/gVisor/kind dispatch, image build/pull/publish, or paid model calls were performed.
|
||||||
|
- Actual ecosystem dependency installations remain unqualified. Python dependency loading was tested
|
||||||
|
with a controlled offline build output and real isolated execution.
|
||||||
|
- The three socket/browser integrations need a host or task where those operations are permitted.
|
||||||
|
- Hard aggregate disk isolation in `task-boundary` mode requires an outer task storage quota. The
|
||||||
|
executor records this requirement and additionally bounds individual files and monitors checkout
|
||||||
|
growth; it cannot enforce a quota across every writable task path without that outer resource limit.
|
||||||
|
- AX tasks configured to inject a model credential will fail closed; executor and verifier task classes
|
||||||
|
must be provisioned without that injection.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Host launcher repair and kube-ax operator constraints
|
||||||
|
|
||||||
|
2026-10-05, branch `search-with-proof`. Implemented under the maintainer's explicit
|
||||||
|
instructions. No commits or network operations. Existing uncommitted work was preserved.
|
||||||
|
This supersedes the build/transport and disk-bound statements in
|
||||||
|
`implementation-trust-domains.md` where they differ.
|
||||||
|
|
||||||
|
## Root cause and repair
|
||||||
|
|
||||||
|
The HTTP driver calls `subprocess.Popen(..., stdin=DEVNULL, stdout=DEVNULL,
|
||||||
|
stderr=DEVNULL)` inside bubblewrap. Its filesystem had no `/dev/null`; Python failed
|
||||||
|
in `_get_handles` before starting the app, binding port 18765, or applying captures.
|
||||||
|
Other probe apps do not use this nested HTTP launcher path.
|
||||||
|
|
||||||
|
A socket-free before/after reproduction read a 19-byte instrument input. Removing
|
||||||
|
`--dev /dev` reproduced exit 1 and the reported `subprocess.py:992 → _get_handles`
|
||||||
|
stack, ending with `FileNotFoundError: [Errno 2] No such file or directory: '/dev/null'`.
|
||||||
|
With the private device mount, the identical launcher exited 0 with empty stderr.
|
||||||
|
The original HTTP capture test's fixed port, token exchange, and differential
|
||||||
|
assertion remain unchanged. Reasons now retain the last 2,000 stderr characters;
|
||||||
|
executor result truncation retains stderr tails too.
|
||||||
|
|
||||||
|
## Operator contract
|
||||||
|
|
||||||
|
- Shared two-slot AX reservations cover workload lane instances and long-lived
|
||||||
|
search executors. Capacity rejection retries the identical manifest with 15–30
|
||||||
|
seconds of jitter until the deadline. Exhaustion is `capacity_timeout`, never
|
||||||
|
`sandbox_failure`, and does not trigger another execution attempt or VM fallback.
|
||||||
|
Uncertain task cleanup retains the reservation and halts dispatch.
|
||||||
|
- Task entrypoints configure temporary files and the specified package caches under
|
||||||
|
`/workspace`. The aggregate guard defaults to 2 GiB, configurable through
|
||||||
|
`OUSAST_SCRATCH_BYTES`. It checks workspace files, caches, and unlinked output logs
|
||||||
|
during child execution and after exit. Exceeding it returns `could_not_build` with
|
||||||
|
`scratch limit`; unreadable scratch is not interpreted as zero usage.
|
||||||
|
- The default AX side dispatcher first builds/packages each side in an executor,
|
||||||
|
retrieves its digest-checked archive, and deletes the executor before verification.
|
||||||
|
One archive contains the checkout, products, and spec. Three fresh verifier tasks
|
||||||
|
consume that archive through `VERIFY_INPUT_URL`; results use `RESULT_URL`.
|
||||||
|
Verifiers reject dependency builds. Git metadata is omitted from source archives;
|
||||||
|
safe internal generated links are materialized before export.
|
||||||
|
- Search tasks receive source archives. Executor tool guidance and project/operator
|
||||||
|
documentation require HTTPS GitHub git clone or codeload, never `api.github.com`.
|
||||||
|
First-download retries tolerate delayed egress activation for up to 60 seconds.
|
||||||
|
|
||||||
|
## Files changed in this follow-up
|
||||||
|
|
||||||
|
Production:
|
||||||
|
- `src/openultrasast/search/_sandbox.py`
|
||||||
|
- `src/openultrasast/search/verify.py`
|
||||||
|
- `src/openultrasast/search/executor.py`
|
||||||
|
- `src/openultrasast/search/verify_task.py`
|
||||||
|
- `src/openultrasast/search/task_storage.py` (new)
|
||||||
|
- `src/openultrasast/search/worker.py`
|
||||||
|
- `benchmarks/ax/batch.py`
|
||||||
|
- `benchmarks/ax/search_verify.py`
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- `tests/test_search_isolation.py`
|
||||||
|
- `tests/test_search_verify.py`
|
||||||
|
- `tests/test_search_verify_transport.py`
|
||||||
|
- `tests/test_search_task_storage.py` (new)
|
||||||
|
- `tests/test_ax_batch.py`
|
||||||
|
|
||||||
|
Documentation/evidence:
|
||||||
|
- `.kiro/steering/kube-ax.md` (new)
|
||||||
|
- `ops/ax/README.md`
|
||||||
|
- `benchmarks/measurements/2026-09-08-module-audit.json`
|
||||||
|
- This report (new).
|
||||||
|
|
||||||
|
Other files already modified/untracked at session start remain uncommitted.
|
||||||
|
|
||||||
|
## Fresh verification
|
||||||
|
|
||||||
|
- Requested suite, with `-rs` for skip reasons:
|
||||||
|
`/home/mc/Source/OpenUltraSAST/.venv/bin/pytest -q tests/test_search_*.py tests/test_ax_batch.py tests/test_plane_literals.py -rs`
|
||||||
|
— **181 passed, 3 skipped, exit 0**, 24.94 seconds.
|
||||||
|
- Final executor/storage regression and module audit tests after tightening stderr
|
||||||
|
truncation: **34 passed, exit 0**, including all **6 module audit tests**.
|
||||||
|
- Shared dispatcher consumer `tests/test_unseen_calibrate.py`: **10 passed, exit 0**.
|
||||||
|
- Module audit regenerated from the worktree using `PYTHONPATH=src`, with an explicit
|
||||||
|
source read/byte count: **172 modules, 166 load-bearing, 6 standalone capabilities,
|
||||||
|
0 orphaned**.
|
||||||
|
- Ruff passes across search, AX, and changed tests; strict mypy passes for the six
|
||||||
|
changed typed implementation modules. `git diff --check` passes.
|
||||||
|
|
||||||
|
## Gaps
|
||||||
|
|
||||||
|
- Host policy prohibits Chromium network/isolation setup, SSRF localhost sockets,
|
||||||
|
and the full HTTP capture test. These are the three skips. The exact missing-device
|
||||||
|
failure is reproduced and repaired without sockets; full HTTP host confirmation
|
||||||
|
remains necessary.
|
||||||
|
- No live AX tasks, image builds/pulls, registry installs, or external downloads were
|
||||||
|
run. Mocked transport plus local real child processes establish code contracts,
|
||||||
|
not deployed cluster readiness. Verifier egress to
|
||||||
|
`files.because-security.com:443` is requested from the operator and still pending.
|
||||||
|
- Scratch enforcement is a polling guard, not an instantaneous filesystem quota.
|
||||||
|
Programs that hardcode RAM-backed paths can still consume the worker memory limit.
|
||||||
|
Archive bounds remain 64 MiB compressed / 256 MiB file contents / 20,000 entries;
|
||||||
|
larger built projects fail explicitly and require a separately qualified envelope.
|
||||||
|
- Multi-ecosystem dependency build/runtime qualification remains open. The offline
|
||||||
|
preparation tests cover no-build projects and controlled Python build products.
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Add a search-with-proof stage: for the top-ranked candidates of a push or a labelled pair, sandboxed tool-using agents
|
||||||
|
search for a demonstration of the vulnerability, coordinated through a fact/intent board, executing primarily in the
|
||||||
|
kube-ax cluster. BLOCK becomes "demonstrated", not "scored above a threshold". Context: `brief.md`, roadmap G1-G2,
|
||||||
|
`unseen-repo-evaluation` (the pool and metric this arm is judged by).
|
||||||
|
|
||||||
|
## Boundary Context
|
||||||
|
|
||||||
|
- **In scope**: the board (facts, intents, hints) in the memory store; reason and explore steps as AX Tasks; the
|
||||||
|
demonstration contract and its verification; triage-to-search budgeting; a pilot on known fix pairs; a G1 arm on the
|
||||||
|
unseen pool; model access through an operator-managed scoped key.
|
||||||
|
- **Out of scope**: copying Cairn code (AGPL-3.0); network actions against anything but the code under analysis;
|
||||||
|
changing the existing quick rules, engine or classifier (they are inputs here).
|
||||||
|
- **Adjacent expectations**: `unseen-repo-evaluation` scores this arm; the AX batch dispatcher (`benchmarks/ax/batch.py`)
|
||||||
|
and the kube-ax constraints (2 heavy lanes, `ousast-engine-` names, egress to public HTTPS) apply;
|
||||||
|
`learned-decision-engine` Req 6.4 BLOCK rule is superseded for this arm by "demonstrated".
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: The board
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want each search to keep its state, so exploration builds on what is already
|
||||||
|
known instead of restarting.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. Per search (one candidate or one push), the board holds facts (confirmed, objective, with the evidence that
|
||||||
|
confirmed them), intents (declared directions, open or concluded, with the facts they start from) and hints (human
|
||||||
|
input, absorbed on the next read).
|
||||||
|
2. The board lives in the memory store, versioned, and every fact and intent carries the step and the task that wrote
|
||||||
|
it.
|
||||||
|
3. A search ends when a fact meets the goal (Requirement 3), when its budget is spent, or when the reasoner proposes no
|
||||||
|
new intent and none is open; the end reason is recorded.
|
||||||
|
|
||||||
|
### Requirement 2: Reason and explore steps
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want the search driven by the board's state, not by fixed roles.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. A reason step reads the board and returns: the goal is met (citing facts), or at most N new, independent,
|
||||||
|
non-overlapping intents, or no new intent.
|
||||||
|
2. An explore step claims one intent, works it with tools inside the sandbox (read files, search the repository,
|
||||||
|
follow callers, build, run tests and scripts against the code under analysis), and returns exactly one new fact or
|
||||||
|
a recorded failure.
|
||||||
|
3. Both run as AX Tasks in the kube-ax cluster by default; the VM coordinates (board, dispatch, budgets) and is a
|
||||||
|
fallback lane only.
|
||||||
|
4. Parallel explores of one search never race on the board (claims as in the batch dispatcher).
|
||||||
|
|
||||||
|
### Requirement 3: The demonstration contract
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want BLOCK to mean "shown", so precision does not depend on calibration.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. *(Amended 2026-10-05, maintainer decision after the kube-ax probe: AX tasks have no capabilities, so gVisor and one
|
||||||
|
fresh task per side are the only isolation.)* A demonstration is **declarative data, never agent-written code**: a
|
||||||
|
build recipe chosen from the verifier's fixed per-ecosystem recipes, a start command of the form runtime + path +
|
||||||
|
arguments (no shell), and a request sequence (HTTP requests or CLI invocations, with values captured from earlier
|
||||||
|
responses) that the verifier itself sends. The system, not the agent, runs it in fresh sandboxes. The **verifier observes the effect itself** through a per-family oracle it owns (for
|
||||||
|
example a request log, a planted resource it watches, a browser execution check for XSS); the artefact's own exit
|
||||||
|
code or output is never the oracle.
|
||||||
|
2. Each family's oracle states the attacker boundary (what the artefact may touch: only the documented external
|
||||||
|
inputs of the code under analysis, never the verifier's fixtures, the source tree, dependencies or the runtime).
|
||||||
|
Inputs are immutable during a run; untracked files, dependency replacement and runtime patching are refused.
|
||||||
|
3. Differential: for a labelled pair the effect is observed on the vulnerable revision and not on the fixed one; for a
|
||||||
|
push, on head and not on base. Both sides must build and pass a readiness check independently; a side that fails
|
||||||
|
to build or start makes the result **inconclusive**, never a pass. Each side is run at least three times from fresh
|
||||||
|
state; inconsistent runs are rejected.
|
||||||
|
4. Only a verified demonstration counts as `demonstrated`. Outcomes: `demonstrated`, `plausible_unproven`,
|
||||||
|
`could_not_build`, `could_not_run`, `inconclusive`, `no_oracle` (family without an owned oracle), `no_evidence`;
|
||||||
|
none of the failure outcomes is ever reported as "not vulnerable".
|
||||||
|
5. Families without an owned oracle (for now: deserialisation, access control, configuration and secrets) can reach
|
||||||
|
`plausible_unproven` at most until an oracle is specified and reviewed.
|
||||||
|
|
||||||
|
### Requirement 4: Triage and budget
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want search spent where it matters, at a known cost.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. Candidates come from the existing layers (quick rules, engine, classifier scores); only the top K per push or per
|
||||||
|
pair side are searched; K and the per-search and per-push budgets (model spend, task count, wall time) are
|
||||||
|
configuration, enforced by the coordinator.
|
||||||
|
2. Every search records its model spend, task count and wall time. Spend is **reserved before each model call** (the
|
||||||
|
call's maximum cost) and released after; wall time, memory, disk and retries are capped per task and in total; a
|
||||||
|
budget is never exceeded, including by calls in flight.
|
||||||
|
3. Paid runs need the maintainer's budget go-ahead with a ceiling, as all paid steps do.
|
||||||
|
|
||||||
|
### Requirement 5: Model access in the cluster
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want agents in the cluster to reach a model without spreading keys.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The agents' model key is a scoped, budget-limited key provided by the operator inside the cluster (AX v0.3.1
|
||||||
|
injects only `gemini-api-secret` as `GEMINI_API_KEY`; the design names the exact mechanism agreed with the
|
||||||
|
operator). Store credentials stay out of the cluster (presigned links, as today). For this arm, model calls inside
|
||||||
|
the cluster supersede `unseen-repo-evaluation` Req 4.4 (model calls on the VM); that boundary is unchanged for all
|
||||||
|
other arms. *(maintainer decision 2026-10-05: scoped key in the cluster)*
|
||||||
|
2. The key never appears in a manifest, a log, a board entry or a record, and never shares a sandbox with repository
|
||||||
|
code. *(Amended 2026-10-05, maintainer decision: trust domains are separated instead of isolated inside one task.)*
|
||||||
|
The agent loop (the "brain", holding the key) runs on the VM or in its own AX task and executes no repository code;
|
||||||
|
every tool call that touches the repository runs in a separate executor AX task without the key, exchanging
|
||||||
|
commands and results through presigned store objects. A Firecracker executor runtime is considered only if the
|
||||||
|
pilot's `could_not_build` rate shows gVisor cannot build the projects.
|
||||||
|
|
||||||
|
### Requirement 6: Feasibility pilot, false-proof qualification, then the unseen pool
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want to know quickly whether search with proof works, and to admit BLOCK only on
|
||||||
|
evidence that meets the 95% precision bar.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. **Feasibility pilot** (amended 2026-10-05 after review): about 20 known fix pairs from development corpora. It
|
||||||
|
measures feasibility only and is reported as such: demonstrated rate on vulnerable sides, false proofs on fixed
|
||||||
|
sides, `could_not_build` / `could_not_run` / `inconclusive` / `no_oracle` rates, spend, tasks and wall time. Its
|
||||||
|
advantages (known location, labelled family, runnable-project bias) are stated in the record. Fixed-side searches
|
||||||
|
never receive the vulnerable revision or its demo.
|
||||||
|
2. Before any paid search, an infrastructure probe without model calls proves each owned oracle works inside a gVisor
|
||||||
|
AX Task (database fixture for SQL, in-sandbox listener for SSRF, headless browser for XSS, file and process
|
||||||
|
watches), that the artefact cannot reach the observers directly, and records image size, RAM and the full
|
||||||
|
verification cost (up to 3 demos x 2 sides x 3 runs per search). Pilot exit: at least one family with at least 5 verified demonstrations on vulnerable sides and zero false proofs;
|
||||||
|
otherwise the result is recorded and the arm is not scaled.
|
||||||
|
3. **BLOCK admission per family** (deferred until the pilot passes; not part of the first tasks) requires a false-proof
|
||||||
|
qualification. A **negative** is a search on code that should be safe: a fixed revision searched on its own (the
|
||||||
|
search never sees the vulnerable revision), or an ordinary change. A **false proof** is the verifier observing the
|
||||||
|
effect on that safe code, confirmed by adjudication (an ordinary-change alarm may be a real new bug and is
|
||||||
|
adjudicated before it counts either way). Qualification needs zero false proofs over at least 73 negatives drawn
|
||||||
|
from at least 73 distinct repositories, so the Wilson 95% upper bound on the false-proof rate per negative is at
|
||||||
|
most 5%. This bounds the false-proof rate, not precision among flags; precision is reported separately from the
|
||||||
|
pool. Repeated runs and repeated searches of one repository are one negative.
|
||||||
|
4. **Coverage ceilings**: every run reports, separately, (a) the demonstrated ceiling: the share of vulnerable changes
|
||||||
|
with a true candidate selected, an owned oracle, both sides runnable and a genuine differential, build and run
|
||||||
|
failures in the denominator; and (b) the advisory ceiling: the share with a true candidate selected. Families
|
||||||
|
without an owned oracle (access control, deserialisation, configuration and secrets) have a demonstrated and BLOCK
|
||||||
|
ceiling of zero today; their advisory ceiling need not be zero.
|
||||||
|
5. **G1 arm on the unseen pool**: scored with the pool's rules on **every visible finding** (BLOCK and advisory), with
|
||||||
|
its family and location match; catch at the 1%, 5% and 10% false-alarm budgets; compared with the classifier arms;
|
||||||
|
adopted only under the pool's no-regression rule on a fresh slice. No evaluation label, fixing revision or board
|
||||||
|
from evaluation searches is ever an input to unseen search.
|
||||||
|
6. Delayed results (a search finishing after the push) reach the user through the existing background delivery and
|
||||||
|
are scored separately from results available at push time.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"feature_name": "search-with-proof",
|
||||||
|
"created_at": "2026-10-05T12:00:00Z",
|
||||||
|
"updated_at": "2026-10-07T13:30:00Z",
|
||||||
|
"language": "en",
|
||||||
|
"phase": "tasks-generated",
|
||||||
|
"approvals": {
|
||||||
|
"requirements": {
|
||||||
|
"generated": true,
|
||||||
|
"approved": true
|
||||||
|
},
|
||||||
|
"design": {
|
||||||
|
"generated": true,
|
||||||
|
"approved": true
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"generated": true,
|
||||||
|
"approved": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ready_for_implementation": false,
|
||||||
|
"pilot": {
|
||||||
|
"status": "in_progress",
|
||||||
|
"first_cluster_demonstration": "2026-10-07",
|
||||||
|
"pair": "xiaomusic (path, prefix-bypass)",
|
||||||
|
"false_proofs": 0,
|
||||||
|
"exit_remaining": "path family >=5 demonstrations / 0 false proofs (Req 6.2), then >=73-negative BLOCK qualification (Req 6.3)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
Scope: infrastructure probe and feasibility pilot only (Req 6.1-6.2). BLOCK qualification (6.3), the unseen-pool arm
|
||||||
|
(6.5) and delivery (6.6) are deferred until the pilot passes. Implementation via Codex in a worktree; Claude reviews,
|
||||||
|
gates, runs every networked or paid step.
|
||||||
|
|
||||||
|
- [ ] 1. Operator prerequisites (Claude, no code)
|
||||||
|
- [ ] 1.1 Ask the operator: scoped spend-limited model key mechanism (reuse `gemini-api-secret` slot or a new
|
||||||
|
injection), and whether a no-egress task class exists for verify runs. _Requirements: 5.1, 3.2_
|
||||||
|
- [ ] 1.2 Record the answers in `kube-ax-cluster` memory and design.md; no key value anywhere. _Requirements: 5.2_
|
||||||
|
|
||||||
|
- [x] 2. Verifier and oracles (no model calls)
|
||||||
|
- [x] 2.1 `src/openultrasast/search/verify.py`: fresh-sandbox runner per side; build, readiness check, read-only
|
||||||
|
checkout and dependencies, untracked-file and runtime-patch refusal, 3 runs per side, consistency check,
|
||||||
|
outcomes `demonstrated|inconclusive|could_not_build|could_not_run|no_oracle`. Artefact exit code ignored.
|
||||||
|
_Requirements: 3.1-3.4_
|
||||||
|
- [x] 2.2 Oracles owned by the verifier: SQL (database fixture + query log), path (file-access watch on a planted
|
||||||
|
canary), XSS (headless browser execution check), SSRF (in-sandbox listener), command injection (process watch).
|
||||||
|
Observers unreachable from the artefact. _Requirements: 3.1, 3.2, 3.5_
|
||||||
|
- [x] 2.3 Unit tests with hand-written vulnerable/fixed toy apps per oracle, including gaming attempts (reading the
|
||||||
|
canary, revision sniffing, patching a dependency, contacting the listener directly): each must not yield
|
||||||
|
`demonstrated`. _Requirements: 3.2, 3.3_
|
||||||
|
|
||||||
|
- [ ] 3. Image and infrastructure probe (no model calls)
|
||||||
|
- [ ] 3.1 `ousast-search-task` image from the AX task runner: Python, Node/TypeScript, PHP, Java toolchains, the
|
||||||
|
oracle fixtures, headless browser; built by CI to GHCR, pinned by digest. _Requirements: 2.3_
|
||||||
|
- [ ] 3.2 Probe: the toy apps from 2.3 verified as AX Tasks on kube-ax (gVisor) and on kind. Record image size,
|
||||||
|
RAM peak, disk, wall time per verification and per 18-run search, oracle success per family, and that observers
|
||||||
|
are unreachable. Record `benchmarks/measurements/2026-10-xx-search-probe/record.json`. _Requirements: 6.2_
|
||||||
|
- [ ] 3.3 Gate: every oracle works under gVisor, or the family is marked `no_oracle` for the pilot.
|
||||||
|
|
||||||
|
- [x] 4. Board, coordinator, budgets
|
||||||
|
- [x] 4.1 Board in the memory store as immutable versioned blobs (`plane/memory.py`); facts, intents, hints with
|
||||||
|
writer step and task; coordinator is the only writer; tasks read and write through presigned links.
|
||||||
|
_Requirements: 1.1-1.3_
|
||||||
|
- [x] 4.2 Spend reservation before each model call in `plane/budget.py`, settled after; per-task memory, disk,
|
||||||
|
cleanup caps; total wall-time and retry caps; tests that in-flight calls cannot overrun. _Requirements: 4.1, 4.2_
|
||||||
|
- [x] 4.3 Dispatch reason, explore and verify as AX Tasks through `benchmarks/ax/batch.py` (extend its environment
|
||||||
|
contract beyond numeric deadlines), VM lane as fallback, resume from checkpoints. _Requirements: 2.3, 2.4_
|
||||||
|
|
||||||
|
- [ ] 5. Agent worker
|
||||||
|
- [x] 5.1 Reason step (no tools): board snapshot in, `complete` / up to 3 intents / nothing out. _Requirements: 2.1_
|
||||||
|
- [x] 5.2 Explore step: tool loop (`list_files`, `read_file`, `grep`, `run`, `write_demo`, `finish`) via the
|
||||||
|
existing OpenAI-compatible client; model client in a separate process and user from executed code; one fact
|
||||||
|
or a recorded failure. _Requirements: 2.2, 5.2_
|
||||||
|
- [ ] 5.3 Smoke test with a stub model (no spend) end to end on one toy pair on kube-ax.
|
||||||
|
|
||||||
|
- [ ] 6. Feasibility pilot (paid, after go-ahead)
|
||||||
|
- [x] 6.1 Select 20 development-corpus pairs (never v3 or the unseen pool) in the four pool families with an owned
|
||||||
|
oracle, across the five languages; record selection advantages. _Requirements: 6.1_
|
||||||
|
- [ ] 6.2 Run 3 searches, measure real cost per search, ask the maintainer for the pilot ceiling. _Requirements: 4.3_
|
||||||
|
- [ ] 6.3 Run the pilot: both sides per pair, fixed side blind to the vulnerable revision; record demonstrated
|
||||||
|
rate, false proofs, failure-outcome rates, both coverage ceilings, spend, tasks, wall time.
|
||||||
|
_Requirements: 6.1, 6.4_
|
||||||
|
- [ ] 6.4 Exit decision against 6.2 and report to the maintainer; on pass, spec tasks for qualification and the
|
||||||
|
unseen-pool arm. _Requirements: 6.2_
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- Group 2 (2026-10-05): independently reviewed; `tests/test_search_verify.py` has 32 passing tests and one
|
||||||
|
skipped SSRF integration test because this host forbids even localhost sockets. The namespace handoff and
|
||||||
|
listener request parsing are tested separately; the full SSRF effect still needs the task 3.2 probe before
|
||||||
|
claiming that oracle operational. The module audit and neighbouring sandbox/regression tests pass.
|
||||||
|
- The local lane requires Linux Bubblewrap and util-linux, reuses `SandboxJob`/`SandboxResult`, and fails closed
|
||||||
|
without isolation. Artefacts receive a batch CLI `TARGET` bridge and private scratch; HTTP apps need a trusted
|
||||||
|
CLI adapter. Only trusted `Side.build_command` outputs can enter the app runtime (read-only `/build`);
|
||||||
|
artefact `build.sh` writes scratch only. XSS requires a trusted browser executor, otherwise `no_oracle`.
|
||||||
|
|
||||||
|
- Tasks 3.1 (files only) and group 4 (2026-10-05): independent review approved; final offline validation:
|
||||||
|
107 passed, 2 skipped (Chromium namespace/socket restrictions and the existing SSRF localhost restriction).
|
||||||
|
Ruff and changed-source mypy pass; module audit: 160 load-bearing, 6 standalone, zero orphaned. No network
|
||||||
|
actions, model calls, image builds, pushes or commits were performed. Task 3.1 remains unchecked until the
|
||||||
|
image is built/published and a real digest is available; CI now includes `plane/Dockerfile.search-task`.
|
||||||
|
- The coordinator accepts an injected executor. `BatchExecutor` uses the repository's `benchmarks.ax.batch`
|
||||||
|
transport with one attempt per admission; coordinator owns the global retry budget and VM fallback. It requires
|
||||||
|
a repository checkout import path and a trusted worker command; reason/explore workers remain group 5.
|
||||||
|
Per-task memory, disk and cleanup ceilings are passed in the worker contract; worker enforcement and gVisor
|
||||||
|
compatibility still require group 5 and the 3.2 probe. Only trusted verification results can meet the goal.
|
||||||
|
- Board heads are immutable memory-store hashes; retain the returned head with the host run to resume. Unknown
|
||||||
|
interrupted model calls conservatively consume their full reservation; settled failures with zero spend release
|
||||||
|
it. Cleanup exceptions stop dispatch. Chromium uses a fresh profile in a private network namespace; injected
|
||||||
|
script must call `alert("ousast-xss")`, while escaped/reflected payloads cannot produce the verifier DOM marker.
|
||||||
|
|
||||||
|
- Tasks 3.2 and 5.1–5.3 offline implementation (2026-10-05, explicitly authorized): packaged five-family
|
||||||
|
toy pairs and real/two-gaming demos in `search/probe_apps/`; `python -m openultrasast.search.probe` reports
|
||||||
|
per-side stage times, image versions, child/self peak RSS, retained scratch usage and sampled verification
|
||||||
|
scratch peak. Generic isolation and later oracle namespace failures are instrument failures, never misses.
|
||||||
|
Optional RESULT_URL uses an object-scoped urllib PUT. `benchmarks.ax.search_probe` reuses AX/Docker lanes
|
||||||
|
(kind via its kubeconfig), with `ousast-engine-search-probe-` task names.
|
||||||
|
- Worker reason/explore uses the existing OpenAI-compatible client with retries disabled and SpendBudget
|
||||||
|
reservation before each call. Repository commands run only in the scrubbed sandbox with bounded tmpfs;
|
||||||
|
the key-absence, timeout, output limit and disk limit tests execute real isolated processes. The scripted
|
||||||
|
model/fake coordinator executor smoke reaches `demonstrated` through the real path oracle verifier.
|
||||||
|
- Independent review APPROVED the requested offline scope. Fresh requested search/AX/plane tests plus module
|
||||||
|
audit: 87 passed, 2 skipped, exit 0; focused new tests: 15 passed; Ruff and changed-source mypy pass.
|
||||||
|
Audit: 162 load-bearing, 6 standalone, zero orphaned. Tasks 3.2 and 5.3 remain unchecked for their original
|
||||||
|
live-cluster criteria; their code/local smoke is implemented. No network, paid calls, image builds, pushes
|
||||||
|
or commits. Offline wheel build could not resolve the uncached uv-build backend, so installed-wheel and
|
||||||
|
real AX/gVisor/kind/Docker image validation remain gaps.
|
||||||
|
|
||||||
|
- Task 6.1 and executor smoke (2026-10-05, explicitly authorized): added
|
||||||
|
`benchmarks/search/executor_smoke.py` and `pilot_select.py`, with offline tests and usage documentation.
|
||||||
|
The smoke drives one managed, key-free executor mailbox and records per-command timing/output measurements
|
||||||
|
and sampled workspace peak; AX and Docker lifecycle cleanup is covered by fake-lane tests. No live task ran.
|
||||||
|
Pilot selection read 10 development catalogs (873 rows, 904,050 bytes), retained 192 eligible pairs and selected
|
||||||
|
20 distinct repositories: four per language and five per family. Identities live only in the gitignored local
|
||||||
|
manifest; the aggregate record is `benchmarks/measurements/2026-10-05-search-pilot-selection/record.json`.
|
||||||
|
All 20 selections have unknown manifest/test-suite evidence; no matching local cache or frozen unseen-pool
|
||||||
|
metadata was present. These are feasibility inputs, not proof of buildability or qualification.
|
||||||
|
Both task-local independent reviews approved after a reserved-cache-symlink regression was fixed.
|
||||||
|
Fresh requested search/AX/plane tests, module audit, independent/reserved and unseen guards, and label guards:
|
||||||
|
287 passed, four skipped, exit 0. Skips: Chromium isolation, two localhost-socket cases, and no frozen pool.
|
||||||
|
Ruff and changed-source mypy pass; audit: 166 load-bearing, six standalone, zero orphaned.
|
||||||
|
No network or commits. The requested main merge could not update read-only Git metadata; HEAD and main
|
||||||
|
had identical file trees. Live AX/Docker executor smoke and oracle probes remain unmeasured.
|
||||||
|
|
||||||
|
- **FIRST CLUSTER DEMONSTRATION (2026-10-07) — pilot is live (6.2/6.3 in progress).** search-with-proof produced its
|
||||||
|
first autonomous `demonstrated` finding end to end on kube-ax: pair 13 (hanxi/xiaomusic, path, prefix-check-bypass
|
||||||
|
class), vulnerable side `demonstrated` (3/3 affected, 0/3 safe, $0.37), fixed side blind negative `inconclusive`
|
||||||
|
with 0 false proofs ($0.56); pair total $1.22, image e2c8b460. Record:
|
||||||
|
`benchmarks/measurements/2026-10-07-search-first-cluster-demonstration/`. This closes the handoff's open question.
|
||||||
|
The infra probe (3.2) passed earlier; the pilot (6.x) has run many times. Enabling fix chain, each merged + paid-run
|
||||||
|
validated: (1) parity (name the observable to the agent), (2) verify-time-only (observable injected at verify time,
|
||||||
|
absent in exploration — ends the explore reachability trap), (3) fair-share pilot budget (no starvation across
|
||||||
|
searches), (4) served-root guidance (demo MUST serve `${served_root}`), (5) oracle prefix-bypass coverage (sibling
|
||||||
|
canary for `startswith`-without-separator path traversal), (6) control-signal feedback (served-root hint so the
|
||||||
|
agent self-corrects wiring). Plus the keystone (positive control → `could_not_run`, never a silent clean-looking
|
||||||
|
`inconclusive`). NEXT: fan out the path family to ≥5 demonstrations / 0 false proofs (6.2 exit), then the
|
||||||
|
≥73-negative BLOCK qualification (6.3). glance (classic traversal) also demonstrates once its npm `filed`→`mime@4`
|
||||||
|
build-break is pinned.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Search-with-proof: where each stage runs (verification architecture)
|
||||||
|
|
||||||
|
Recorded 2026-10-07. search-with-proof has two stages across a trust boundary: a Cairn-inspired
|
||||||
|
directed state-space **search**, then a DAST-style **proof (verification)**.
|
||||||
|
|
||||||
|
## The two stages
|
||||||
|
|
||||||
|
1. **Search (Cairn-inspired).** A blackboard of facts, intents and hints. A reasoner proposes
|
||||||
|
independent, non-overlapping intents; tool-using explorers each confirm one fact against the
|
||||||
|
code. The goal is a declarative demo (a candidate PoC), never proof by itself.
|
||||||
|
2. **Proof (DAST verification).** The verifier builds and starts the app, drives the demo's steps
|
||||||
|
through the app's real interface, and watches a verifier-owned canary for a **differential**
|
||||||
|
between the vulnerable and fixed revisions: the owned effect appears on the vulnerable side and
|
||||||
|
not on the fixed side (3/3 vs 0/3). Only this differential earns `demonstrated`.
|
||||||
|
|
||||||
|
## Trust boundary — what runs where
|
||||||
|
|
||||||
|
The brain runs on the VM; the agent's tool executions and the DAST proof run as isolated tasks in
|
||||||
|
the kube-ax cluster. Secrets never enter the cluster.
|
||||||
|
|
||||||
|
| Component | Location | Notes |
|
||||||
|
|-----------|----------|-------|
|
||||||
|
| Coordinator, board, budgets | VM (host) | Orchestrates the search; owns the global budget. |
|
||||||
|
| Reasoner + explorer **model calls** | VM (host) | The model key lives only on the VM. This is the brain. |
|
||||||
|
| Explorer **tool runs** (`list_files`, `read_file`, `grep`, `run`, build) | kube-ax AX Task (gVisor) | Carry no secrets; inputs/results via presigned object links. |
|
||||||
|
| **DAST proof / verification** | kube-ax AX Task (gVisor) | Builds, starts, attacks and observes the app. One task per revision. |
|
||||||
|
|
||||||
|
- **gVisor is the isolation boundary.** The proof runs untrusted exploit code against a running
|
||||||
|
app, so it is sandboxed per-task in the cluster.
|
||||||
|
- **Secrets stay on the VM.** Executor and verifier tasks carry no credentials; they exchange
|
||||||
|
checkout archives, demos and results through presigned object-store links. Per-host egress is
|
||||||
|
restricted (verify tasks reach the files host only).
|
||||||
|
|
||||||
|
## Verification lanes (the `--verify-lane` choice)
|
||||||
|
|
||||||
|
- **`ax` (default for paid pilot runs):** verification runs **inside kube-ax** as gVisor AX Tasks,
|
||||||
|
one per side. This is how every paid pilot run this session executed the proof.
|
||||||
|
- **`vm` (local):** verification runs in **Bubblewrap on the host**, no cluster. Used for the
|
||||||
|
zero-cost ceiling proofs (a hand-authored demo verified locally with no model and no cluster),
|
||||||
|
which is how glance and xiaomusic were confirmed demonstrable before any spend.
|
||||||
|
- **`docker` (replay):** `benchmarks.search.demo_replay` runs a stored demo with no model calls in
|
||||||
|
a local Docker lane.
|
||||||
|
|
||||||
|
## Why the proof is dynamic, not abstract interpretation
|
||||||
|
|
||||||
|
Abstract interpretation (the static engine and the decision model) is the cheap, generalizable
|
||||||
|
**triage/ranker**: it selects and orders candidate regions across any repo without executing. It
|
||||||
|
plateaued as a **blocking** gate because it could not prove the required precision. search-with-proof
|
||||||
|
is the **dynamic** confirmation: a demonstrated finding is ground truth by execution, so it earns a
|
||||||
|
block without calibration. The per-app harness cost (building and running the app, wiring its served
|
||||||
|
directory or database to the oracle fixture) is the price of the dynamic proof and is why proof pays
|
||||||
|
off where a runnable harness exists, with abstract interpretation remaining the static backbone.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Brief: does the safety net work on repositories it has never seen?
|
||||||
|
|
||||||
|
Maintainer, 2026-10-04: "we want to spread the safety net across repos in general, unseen code. that was the purpose
|
||||||
|
of the AB tests to see if our approach is generalizeable." And: "we should run our tests / runs in kube. now we can
|
||||||
|
speed it up and design the workloads for ax directly. kind is only a fallback."
|
||||||
|
|
||||||
|
## Why this spec exists
|
||||||
|
|
||||||
|
The A/B experiments so far (exp-003, exp-004, exp-005) score the decision engine on repository-held-out folds of known
|
||||||
|
fix pairs. That measures whether the vulnerable side of a pair scores above its fixed side. It does not measure what a
|
||||||
|
safety net on an arbitrary repository lives on:
|
||||||
|
|
||||||
|
- on an ordinary push, does it stay quiet? (false-alarm rate)
|
||||||
|
- on a push that introduces a real vulnerability, does it say so? (catch rate)
|
||||||
|
|
||||||
|
Populations v1 and v2 measured the engine scan on fresh cases once (1 of 11, 0 of 17) and are spent. Nothing since has
|
||||||
|
measured push-level behaviour on unseen repositories.
|
||||||
|
|
||||||
|
## What this spec delivers
|
||||||
|
|
||||||
|
1. A frozen pool of repositories never used anywhere in development, with two kinds of change each: real
|
||||||
|
vulnerability-introducing commits and ordinary commits.
|
||||||
|
2. A push-level metric: each change replayed through the hook exactly as a user would run it, and through other
|
||||||
|
arms (decision engine, engine evidence) where an experiment asks.
|
||||||
|
3. Execution designed for the kube-ax cluster from the start: one push replay is one AX Task; kind and the VM are
|
||||||
|
fallbacks.
|
||||||
|
4. A baseline for today's hook, then a table of arms scored on the same pool.
|
||||||
|
|
||||||
|
## Not in scope
|
||||||
|
|
||||||
|
Changing detection. This spec only builds the measurement. Improvements are separate, registered experiments judged
|
||||||
|
by it.
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
# Design Document
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This spec builds the instrument for roadmap G0: a frozen pool of repositories that no development step has seen,
|
||||||
|
and a push-level score on it. The score is what a user of the hook sees. On a vulnerability-introducing push, did
|
||||||
|
the hook say so (catch rate)? On an ordinary push, did it say anything (false-alarm rate)? Replays run as AX Tasks
|
||||||
|
on kube-ax. kind and the VM run the same tasks as fallbacks.
|
||||||
|
|
||||||
|
Four facts from the code shape the design:
|
||||||
|
|
||||||
|
- **Populations already have a freeze-and-guard pattern.** `benchmarks/independent/freeze.py` verifies cases by
|
||||||
|
reading git only, `evaluate.py` scores with fixed rules and Wilson intervals, and
|
||||||
|
`tests/test_independent_population.py` fails when a reserved repository is named anywhere in `benchmarks/`,
|
||||||
|
`src/` or `tests/` outside its directory. Harvest batch 3 used that guard as a black box: candidates written,
|
||||||
|
guard run with output discarded, subsets bisected on red. That is how v3 stays unread.
|
||||||
|
- **The hook can already replay a change.** `ousast pre-push PATH --base B --head H --artifact F` resolves two
|
||||||
|
local commits (`snapshot.py`, `resolve_replay`: no fetch, no merge-base), runs quick rules and the engine inside
|
||||||
|
one deadline (default 30 s, engine inline) and writes a JSON artifact. The terminal text has one `Skipped:` line
|
||||||
|
per skipped check. The artifact holds three finding streams: admitted defects, advisory engine findings
|
||||||
|
(new or worsened, vetoed only by capability) and quick-rule matches with their CWE.
|
||||||
|
- **The AX pattern works but lives in one script.** `engine_trace_ax.py` (manifest, egress checks, presigned
|
||||||
|
objects, retry once under a new name, cleanup) and the lane scheduler in `engine_trace.py` (`run_plan`: fixed
|
||||||
|
lanes, fallback queue, `progress.json`, `STOP`, resume by input digest) are engine-specific.
|
||||||
|
- **Placement on kube-ax is random.** One worker per node, shared by all actors. Two heavy tasks fit per worker,
|
||||||
|
but only two concurrent tasks in all guarantee at most two on one worker.
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- A pool frozen by code against every used source (Req 1), with two kinds of change per repository (Req 2).
|
||||||
|
- A scorer whose numbers mean what the hook shows, with silent zeros visible (Req 3).
|
||||||
|
- One replay is one AX Task via a dispatcher shared with the engine (Req 4); a baseline and fresh slices (Req 5).
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
Detection changes, population v3, model calls in the cluster, languages the hook does not analyse.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
M[pool manifest slice] --> D[batch dispatcher on VM]
|
||||||
|
D --> P[presign spec GET and record PUT]
|
||||||
|
P --> T[AX Task ousast-engine-replay]
|
||||||
|
T --> G[shallow fetch base and head from GitHub]
|
||||||
|
G --> H[ousast pre-push --base --head]
|
||||||
|
H --> R[record tar: artifact, text, instrument]
|
||||||
|
R --> S[(store via presigned PUT)]
|
||||||
|
D -->|sandbox died twice or OOM| V[VM docker lane]
|
||||||
|
D -->|cluster down| K[kind lane, same tasks]
|
||||||
|
V --> S
|
||||||
|
K --> S
|
||||||
|
S --> C[scorer: catch, false alarm, coverage]
|
||||||
|
S --> A[model arms on VM, budgeted]
|
||||||
|
A --> C
|
||||||
|
C --> O[record: counts and digests]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components and Interfaces
|
||||||
|
|
||||||
|
### 1. Pool sourcing and eligibility (Req 1)
|
||||||
|
|
||||||
|
**Decision 1.1: sources and ecosystems.** Candidates come from GitHub reviewed advisories (`gh api /advisories`,
|
||||||
|
the enumeration harvest-3 used) for pip, npm, maven and composer, published 2019 or later, whose CWE maps to one of
|
||||||
|
the seven families through the project's family CWE table. Each needs a linked fix commit with one parent in one
|
||||||
|
repository. OSV records for the same advisory give the first affected version. That version is a cross-check for
|
||||||
|
the introducing commit (section 2). Reason: these four ecosystems are the five languages the hook analyses
|
||||||
|
(Python, JavaScript, TypeScript, PHP, Java). GHSA links commits and CWEs; OSV adds version brackets. Rejected:
|
||||||
|
drawing from every ecosystem (Go, Ruby, C#). That would mostly measure missing frontends, which is already known.
|
||||||
|
Coverage still reports uncovered files inside the drawn repositories. Rejected: NVD and published fix datasets.
|
||||||
|
NVD links commits weakly, and the datasets are mined corpora that development or model training has likely seen.
|
||||||
|
|
||||||
|
**Decision 1.2: eligibility by code against every used source.** `benchmarks/unseen/eligibility.py` builds the
|
||||||
|
used set before the draw. It never prints names; it records counts per source and a digest of the set.
|
||||||
|
|
||||||
|
| Source (Req 1.1) | Where it is read |
|
||||||
|
| --- | --- |
|
||||||
|
| Pair corpora | every catalog, recipe and dataset file under `benchmarks/pairs/`, plus the local pointer-pair manifests |
|
||||||
|
| Populations v1, v2 | `population-v1.toml`, `population-v2.toml`, read directly |
|
||||||
|
| Population v3 | never opened; the existing guard test only (below) |
|
||||||
|
| Harvest records | the batch catalogs under `benchmarks/pairs/advisory-fixes*` (the records hold counts only) |
|
||||||
|
| Memory examples | `example` rows of the local FileStore and the S3 store (`repo` field) |
|
||||||
|
| Experiment units | `plane/experiments/*.units.jsonl` and registered units files |
|
||||||
|
| Measurement records | everything under `benchmarks/measurements/` and `benchmarks/experiments/` |
|
||||||
|
| Anything else | a text sweep for `github.com/<owner>/<name>` over `benchmarks/`, `src/`, `tests/`, `plane/`, `.kiro/` |
|
||||||
|
|
||||||
|
Names are normalised (lower case, no `.git`, no trailing slash). Each candidate is resolved through the GitHub API
|
||||||
|
to its canonical `full_name`, its former names (redirects) and its fork network (`parent`, `source`). A candidate
|
||||||
|
is out if any of these is in the used set. A fork shares code with what it was forked from.
|
||||||
|
|
||||||
|
v3 is checked like harvest-3 did. The draft manifest is written into `benchmarks/unseen/`. Then
|
||||||
|
`pytest -q tests/test_independent_population.py -k referenced` runs with its output discarded. The guard already
|
||||||
|
sweeps `benchmarks/` outside the population directory. On red, the candidate list is bisected by rewriting the
|
||||||
|
draft until green, and the dropped count is recorded. No v3 name is ever printed or read by a person.
|
||||||
|
|
||||||
|
**Decision 1.3: post-cutoff preference.** An advisory published after 2026-06-01 (harvest-3's cutoff for the
|
||||||
|
decision engine's model) is flagged `post_cutoff`. Within each ecosystem stratum post-cutoff advisories are drawn
|
||||||
|
first. Results are reported for both groups. Reason: a post-cutoff advisory cannot be in the model's training
|
||||||
|
data as a known vulnerability. Rejected: post-cutoff only. Harvest-3 kept 12 post-cutoff pairs out of 251, and
|
||||||
|
three slices of 100 repositories cannot be filled from that window.
|
||||||
|
|
||||||
|
**Decision 1.4: licences.** The licence is read at the pinned head commit (LICENSE file, then the API's SPDX id).
|
||||||
|
Only permissive licences are published by name: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD, Unlicense,
|
||||||
|
Zlib, PostgreSQL, BSL-1.0. Everything else (copyleft, source-available, none) goes to the gitignored
|
||||||
|
`benchmarks/unseen/private/pool-p1.toml`. The committed manifest then holds an opaque id and the sha256 of
|
||||||
|
(URL, commits) for that entry, so the freeze digest still covers it. Reason: Req 1.4 read literally. Rejected:
|
||||||
|
"OSI-approved" as the bar, as population v2 used. It is wider than the requirement says (open question 1,
|
||||||
|
resolved 2026-10-04: copyleft stays private).
|
||||||
|
|
||||||
|
Also required: anonymous HTTPS, at most 500 MB, at least 40 eligible ordinary commits, one per fork network.
|
||||||
|
|
||||||
|
### 2. Change extraction (Req 2.1, 2.2)
|
||||||
|
|
||||||
|
Extraction runs on the VM over a full `--filter=blob:none` clone in a scratch cache. It uses git only, like
|
||||||
|
`freeze.py`. No scanner, engine or model touches a pool repository before the freeze.
|
||||||
|
|
||||||
|
**Decision 2.1: the vulnerability-introducing range.** Let F be the fix and P its parent. The vulnerable lines are
|
||||||
|
the old-side lines of F's hunks in the vulnerable function. For a pure-addition fix (an absence bug such as a
|
||||||
|
missing guard), they are the vulnerable function's own lines.
|
||||||
|
|
||||||
|
1. **`introducing` (preferred).** `git blame -w -M -C --porcelain P -L <lines> -- <file>`. The candidate V is the
|
||||||
|
commit that last wrote the sink line (for absence bugs, the function's declaration). V is accepted if all hold:
|
||||||
|
at V's first parent the sink line's normalised text is absent from the file's vulnerable function; V is not a
|
||||||
|
merge or root commit; V changes at most 50 files and 2,000 lines; when OSV names a first affected version, V is
|
||||||
|
reachable from that tag and not from the previous release tag. If the text is present at V's parent, blame
|
||||||
|
walks back from there, at most five steps. The replay is `base = V^`, `head = V`.
|
||||||
|
2. **`last_touch` (fallback).** When no V passes, `git log -L :<function>:<file> P` gives the newest commit L
|
||||||
|
before F that changed the vulnerable function. The replay is `base = L^`, `head = P`. The vulnerable code is in
|
||||||
|
head, and the range contains a change to the vulnerable function.
|
||||||
|
|
||||||
|
Each change records its method, family, vulnerable function (resolved in head), head-side changed lines,
|
||||||
|
`post_cutoff` and the advisory id.
|
||||||
|
|
||||||
|
Bias of each, stated in the record. `introducing` inherits SZZ noise: blame finds the last writer, not the author
|
||||||
|
of the flaw. A refactor, a move the heuristics miss or a reformat can stand in for the real introduction. The
|
||||||
|
absence check, the walk-back and the version bracket cut this down, but do not remove it. The bias runs both
|
||||||
|
ways: a wrong V may introduce nothing (a miss is not the hook's fault) or may move a flaw (the delta-based hook is
|
||||||
|
right to call it existing). `last_touch` is pessimistic for a delta hook. The flaw is often already in base, so
|
||||||
|
the hook's novelty rule correctly stays quiet and the change counts as a miss. Catch is therefore reported per
|
||||||
|
method. The headline is `introducing`; `last_touch` is a lower bound. Rejected: fix-parent against a fixed old
|
||||||
|
base (for example 50 commits back). The range often misses the vulnerable function entirely and grows without
|
||||||
|
bound on busy repositories.
|
||||||
|
|
||||||
|
Label check: every third `introducing` change is read by an agent before the freeze, with no model call, as
|
||||||
|
harvest-3 reviewed diffs. It is marked `confirmed`, `refactor` or `unclear`. The confirmed share is the label
|
||||||
|
noise estimate. The scorer also reports catch on the confirmed subset.
|
||||||
|
|
||||||
|
**Decision 2.2: ordinary commits.** The candidates are non-merge commits on the first-parent line of the default
|
||||||
|
branch that change at least one source file of an analysed language. Excluded:
|
||||||
|
|
||||||
|
- explicit security signals in the message: `secur|cve|ghsa|vuln|inject|xss|csrf|ssrf|exploit|sanitiz|escap`;
|
||||||
|
- reverts (`^Revert`, `This reverts commit`);
|
||||||
|
- dependency bumps (bot authors such as dependabot and renovate, `bump|upgrade .* from`, or only manifest and lock
|
||||||
|
files changed);
|
||||||
|
- commits whose source changes are all generated or vendored (`vendor/`, `node_modules/`, `dist/`, `*.min.js`,
|
||||||
|
lock files, `linguist-generated` in `.gitattributes`, "generated" headers);
|
||||||
|
- any fix commit of any advisory of the repository, and any commit touching a known vulnerable function of it;
|
||||||
|
- commits over 1,000 changed source lines (counted, reported).
|
||||||
|
|
||||||
|
Twelve are drawn per repository with a seeded random draw. Strata are three time thirds of the repository's
|
||||||
|
history crossed with four size buckets of changed source lines (1-10, 11-50, 51-200, 201-1,000). Allocation is
|
||||||
|
proportional to the repository's own commits per cell, with at least one per time third. Each change records base
|
||||||
|
(its first parent), head, changed files and line ranges. Reason: proportional allocation keeps the false-alarm
|
||||||
|
rate a per-push rate as a user meets it. Rejected: `freeze.py`'s broad `SECURITY_WORDS`. It also drops commits
|
||||||
|
saying "query", "token", "session" or "valid", which removes the ordinary commits where false alarms are most
|
||||||
|
likely, and the rate would look better than it is. Rejected: docs-only commits. They are trivially quiet and
|
||||||
|
would dilute the rate.
|
||||||
|
|
||||||
|
Label noise: an ordinary commit may itself introduce an unknown vulnerability, so a flag counted as a false alarm
|
||||||
|
may be true. The false-alarm rate stays as Req 3.2 defines it. Flagged ordinary changes are adjudicated beside it
|
||||||
|
(all if 30 or fewer, else every third) against protocol-v2's standard, and the adjudicated share is reported.
|
||||||
|
|
||||||
|
### 3. Sizing and slices (Req 2.3, 5.2)
|
||||||
|
|
||||||
|
Wilson 95% interval, z = 1.96: centre (p + z²/2n)/(1 + z²/n), half width z·sqrt(p(1-p)/n + z²/4n²)/(1 + z²/n).
|
||||||
|
Taking the larger distance from p as the width, the smallest n that meets the bar, computed exactly:
|
||||||
|
|
||||||
|
| Rate | Assumed p | Bar | n needed |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| false alarm | 0.05 | +/- 3 points | 315 |
|
||||||
|
| false alarm | 0.10 | +/- 3 points | 483 |
|
||||||
|
| false alarm | 0.20 | +/- 3 points | 756 |
|
||||||
|
| catch | 0.10 | +/- 10 points | 62 |
|
||||||
|
| catch | 0.25 | +/- 10 points | 88 |
|
||||||
|
| catch | 0.40 | +/- 10 points | 97 |
|
||||||
|
|
||||||
|
Catch: 100 vulnerability-introducing changes per slice, one per repository. At p = 0.4, Wilson gives
|
||||||
|
[0.309, 0.498], within 10 points. One per repository keeps them independent.
|
||||||
|
|
||||||
|
False alarm: ordinary changes of one repository are correlated (noisy code flags often). Wilson assumes
|
||||||
|
independence, so the count is inflated by a design effect 1 + (m - 1)·ICC. With m = 12 per repository and an
|
||||||
|
assumed ICC of 0.05, that is 1.55. 100 repositories × 12 = 1,200 changes, effective n 774 ≥ 756. The plain Wilson
|
||||||
|
interval at p = 0.2, n = 1,200 is about +/- 2.3 points. A repository-cluster bootstrap interval is reported beside
|
||||||
|
it, and the ICC measured on slice 1 is recorded. If it exceeds 0.05, the next pool sizes up. A frozen slice is
|
||||||
|
never changed.
|
||||||
|
|
||||||
|
**Decision 3.1: slices.** Pool p1 has three slices of 100 repositories each (100 + 1,200 changes per slice; 300
|
||||||
|
repositories and 3,900 changes in all). Repositories are assigned by a seeded shuffle, stratified by ecosystem and
|
||||||
|
`post_cutoff`. Slice 1 serves the baseline and the G1 arms table. Slices 2 and 3 serve the first two adoption
|
||||||
|
decisions. When they are spent, pool p2 is drawn by the same code, and it excludes p1. Reason: each slice alone
|
||||||
|
meets the Req 2.3 widths, so any decision can stand on one slice. Rejected: one large pool scored whole. Every
|
||||||
|
decision would then see every repository, which Req 5.2 forbids. Rejected: ten small slices. None would meet the
|
||||||
|
widths. Per family, 100 changes split over seven families give intervals of about +/- 20 to 25 points. They are
|
||||||
|
reported, not sized (open question 3).
|
||||||
|
|
||||||
|
Use ledger: `benchmarks/unseen/usage-p1.jsonl` is append-only, one row per use (slice, decision or experiment id,
|
||||||
|
date, result digest). A slice with a row whose purpose is `informed` cannot appear again as `qualifies` for that
|
||||||
|
change. A test checks this.
|
||||||
|
|
||||||
|
### 4. Scoring rules (Req 3.1, 3.2, 3.3)
|
||||||
|
|
||||||
|
Each replay uses the hook's defaults: `--deadline 30`, `--engine inline`, `--max-regions 500`, `--mode advisory`.
|
||||||
|
That is what a user's push gets. The text output is kept with every `Skipped:` line, and so is the artifact.
|
||||||
|
|
||||||
|
**Decision 4.0: a registered long-deadline arm** (maintainer decision 2026-10-04, open question 5). Beside the
|
||||||
|
default replay, slice 1 is replayed once more with `--deadline 300` and every other flag unchanged. The arm is
|
||||||
|
registered (manifest committed, `unseen: {pool: p1, slice: 1}`) before its first replay. G1 then reports both
|
||||||
|
what the user sees (30 s) and what the engine could catch given time (300 s), so a miss caused by the deadline is
|
||||||
|
told apart from a miss by the engine. It is a measurement arm, not a proposed default: adopting a longer hook
|
||||||
|
deadline would be a separate decision under 7.2.
|
||||||
|
|
||||||
|
**Decision 4.0a: calibrated cluster deadline and native check** (maintainer, 2026-10-05).
|
||||||
|
The gVisor proof exhausted 30 seconds in snapshot preparation on a 1,725-file repository.
|
||||||
|
Cluster replays therefore scale the 30/300-second arms by the median paired cluster/native
|
||||||
|
time through the quick tier, rounded to integer seconds. Native runc replays retain 30/300 seconds.
|
||||||
|
A deterministic sample (default 20, or all of a smaller proof manifest) runs both lanes at
|
||||||
|
600 seconds for calibration. Report per-change stage times and median slowdown with bootstrap
|
||||||
|
95% intervals for time through quick, engine build and engine queries separately. Incomplete
|
||||||
|
timings or degraded scans invalidate calibration. Records contain counts, times and opaque digests.
|
||||||
|
|
||||||
|
A deterministic native sample accompanies the cluster slice. Compare whether each push flags
|
||||||
|
anything, with a Wilson 95% interval. If its lower bound is below 95%, or any sample pair is
|
||||||
|
missing/failed, flag the run and exclude cluster results from the baseline. Disagreements name
|
||||||
|
change IDs only. The scorer reports applied deadlines and factors. Docker pulls explicitly after
|
||||||
|
checking its data-root free space against an unpacked image size upper bound plus 1 GiB; failures
|
||||||
|
retain sanitized stderr tails and exit codes. See the unseen README for coordinator commands.
|
||||||
|
|
||||||
|
**Decision 4.1: catch, per family.** A finding counts if its family is the change's family. Engine streams carry a
|
||||||
|
family; a quick-rule match maps its CWE through the family CWE table. Its head location must also be in the
|
||||||
|
vulnerable function or within 10 lines of the change's head-side hunks (protocol v1's window).
|
||||||
|
|
||||||
|
| Family | Location rule |
|
||||||
|
| --- | --- |
|
||||||
|
| injection, path, deserialization, output_encoding, untrusted_destination | sink line in the vulnerable function, or the changed lines +/- 10 |
|
||||||
|
| access_control | the vulnerable function itself (the handler that lacks the check), or the changed lines +/- 10 |
|
||||||
|
| config_secrets | the changed file and lines +/- 10 (file scope; function `<global>`) |
|
||||||
|
|
||||||
|
All three streams count: admitted defects, advisory engine findings and quick-rule matches. Each catch also names
|
||||||
|
the stream that made it. Reason: the hook shows all three to the user. Rejected: admitted defects only. That
|
||||||
|
scores the blocking path, which G3 decides separately, and would hide what users read.
|
||||||
|
|
||||||
|
**Decision 4.2: false alarm.** An ordinary change is a false alarm when any of the three streams holds at least one
|
||||||
|
finding, of any family, anywhere. The alerts-only rate (admitted defects) is reported beside it.
|
||||||
|
|
||||||
|
**Decision 4.3: coverage, so zeros cannot hide.** Every change gets exactly one state:
|
||||||
|
|
||||||
|
- `instrument_failure`: no record, a failed fetch, the head commit not verified, or zero bytes of the changed files
|
||||||
|
read. It is re-run once, then reported. It never counts as quiet or missed.
|
||||||
|
- `unanalysable`: no changed file is in a language the hook covers (the hook's `language_not_covered` notice).
|
||||||
|
- `quick_only`: the quick tier read the changed files, but no engine question completed on them.
|
||||||
|
- `engine_covered`: at least one engine question completed on a changed file.
|
||||||
|
|
||||||
|
The task entry proves its input as AGENTS.md requires. It records files and bytes in the checkout, bytes of each
|
||||||
|
changed file, the JVM's wall time and the hook's exit code. A replay faster than a JVM can start is flagged
|
||||||
|
`suspect_fast` and goes to the failure path. Catch and false alarm are reported over all changes and over each
|
||||||
|
coverage state.
|
||||||
|
|
||||||
|
### 5. The AX workload (Req 4)
|
||||||
|
|
||||||
|
**Decision 5.1: the image.** `plane/Dockerfile.engine-task` gains a second stage, `FROM engine AS replay`, built
|
||||||
|
with `--target replay` and pinned by digest. It adds `git`, the package with its runtime dependencies and
|
||||||
|
`benchmarks/unseen/replay_entry.py`. Joern, php-cli, Java 21 and the frontend-retention patch come from the engine
|
||||||
|
stage, so both lanes run the same Joern. The import smoke runs `ousast pre-push --help`. The root image stays
|
||||||
|
`ghcr.io/norandom/ax-task-runner:v0.3.1`. Rejected: a separate Dockerfile. It would duplicate the Joern install,
|
||||||
|
and two Joern copies are two instruments that drift. Rejected: one stage for both. The engine image's isolated
|
||||||
|
venv without yaml and boto3 is proven, and replay dependencies would change its digest on every bump.
|
||||||
|
|
||||||
|
**Decision 5.2: the task entry.** `replay_entry.py` is standard library only, like `engine_task_entry.py`, and
|
||||||
|
reuses its `transfer` (egress readiness wait, three attempts, no URL text in errors).
|
||||||
|
|
||||||
|
1. GET `SPEC_URL` (presigned, `.json`): repository URL, base, head, change id, hook flags.
|
||||||
|
2. `git init`, then `git fetch --depth 1 origin <head>` and `git fetch --depth 1 origin <base>` over HTTPS
|
||||||
|
(GitHub serves commits by SHA). Verify both with `git cat-file -e`. The fetch is bounded at 180 s; the 500 MB
|
||||||
|
repository cap bounds its size.
|
||||||
|
3. Run `ousast pre-push <checkout> --base <base> --head <head> --artifact /tmp/out/push.json` with defaults, the
|
||||||
|
backend heap at 1700 MB and the cluster JVM flags of `engine_trace_worker.py`. Capture stdout.
|
||||||
|
4. Pack `push.json`, `push.txt`, `engine.json` (the head and base scan records and witness rows from the artifact,
|
||||||
|
keyed by repository and revision, for engine-evidence arms) and `instrument.json` (section 4.3, image digest,
|
||||||
|
step timings). PUT to `RESULT_URL` as `.dat`. The PUT is completion.
|
||||||
|
|
||||||
|
On any error it PUTs a failure record naming the step and HTTP status, as the engine entry does. Task names are
|
||||||
|
`ousast-engine-replay-<slug>-<digest>`, the prefix the operator's egress watcher matches. No key or store
|
||||||
|
credential enters the manifest. Only the two presigned URLs do, and the spec object keeps repository names out of
|
||||||
|
AX status and cluster telemetry.
|
||||||
|
|
||||||
|
**Decision 5.3: the generic dispatcher.** `benchmarks/ax/batch.py` takes over from `engine_trace_ax.AX` and
|
||||||
|
`engine_trace.run_plan`:
|
||||||
|
|
||||||
|
- `Item`: id, input objects (name to bytes), the result object name, command, extra env, input digest.
|
||||||
|
- `Workload`: image digest, allowed URL env names, deadline, how to validate a received result.
|
||||||
|
- Lanes: `ax` (manifest, apply, poll the result object and AX phase, delete, clean objects), `kind` (the same
|
||||||
|
manifest against the kind AX), `docker` (the VM image, one container at a time).
|
||||||
|
- Scheduler: two AX lanes plus one VM lane by default. A sandbox death is retried once under a new name; a second
|
||||||
|
death or a JVM OOM goes to the VM fallback queue, which beats overflow on the VM lane. `progress.json` is written
|
||||||
|
only by the scheduler. A `STOP` file halts new launches. Resume skips items whose stored record has the same
|
||||||
|
input digest. Cleanup failure stops dispatching.
|
||||||
|
|
||||||
|
The engine pass becomes a workload over it, with the same tests. Reason: the egress, secrecy and cleanup logic took
|
||||||
|
several fixes and must exist once. Rejected: a replay-specific copy of `engine_trace_ax.py`. Rejected: putting it in
|
||||||
|
`src/` now. G4's `ousast plane scan` may need it there, and it moves when G4 is planned.
|
||||||
|
|
||||||
|
Concurrency is two AX tasks. Placement is random, and only two guarantees two per worker at most. Four would need
|
||||||
|
the operator to add placement (open question 2, resolved 2026-10-04: stays at two, no placement change
|
||||||
|
requested). Estimated wall time per slice: about 75 s per replay (fetch, 30 s hook, setup), 1,300 replays over
|
||||||
|
three lanes, about 9 hours. The long-deadline arm (4.0) is bounded at about 345 s per replay; at that bound 1,300
|
||||||
|
replays over three lanes take about 42 hours, less where the hook finishes early. It runs with `STOP` and resume
|
||||||
|
across sessions.
|
||||||
|
|
||||||
|
**Decision 5.4: model arms on the VM.** Arms that need DeepSeek (the decision engine, the combiner) read the stored
|
||||||
|
records on the VM. They build candidates from the head side's changed functions, score them, and take the change
|
||||||
|
score as the maximum. Each runs under its experiment manifest's `budget_usd` through the metered client, dry run
|
||||||
|
first, with responses cached. Nothing in the cluster calls a model.
|
||||||
|
|
||||||
|
**kind (Req 4.5)** runs the identical manifest and image through the `kind` lane. Every new task is proven there
|
||||||
|
before kube-ax, as `plane-on-kubernetes` requires.
|
||||||
|
|
||||||
|
### 6. Records and the guard (Req 1.2, 1.3, 5.1)
|
||||||
|
|
||||||
|
`benchmarks/unseen/` holds `pool-p1.toml` (repositories, licences, slices, changes with base, head, method,
|
||||||
|
family, function, lines), `freeze-p1.json` (used-set digests and counts per source, guard result, label-check
|
||||||
|
counts, freeze digest = sha256 of the canonical manifest), `protocol-p1.md` (the scoring rules of section 4,
|
||||||
|
committed before the first replay) and `usage-p1.jsonl`. The `private/` directory is gitignored.
|
||||||
|
|
||||||
|
The guard grows by one parametrised reservation. The existing sweep becomes a list of (reservation directory,
|
||||||
|
names): `benchmarks/independent/` for populations and `benchmarks/unseen/` for pools, including the private
|
||||||
|
manifest when present. It also sweeps `plane/`. A second test asserts that pool and population names are disjoint,
|
||||||
|
and prints only a count on failure. A third asserts that a frozen pool's digest matches `freeze-p1.json`, so the
|
||||||
|
pool is never edited. Raw per-change records stay outside the repository (`~/ousast-results/unseen/` and the
|
||||||
|
store). Committed result records under `benchmarks/measurements/` carry counts, rates, intervals and digests,
|
||||||
|
never names.
|
||||||
|
|
||||||
|
### 7. Metrics and reporting (Req 3.3, 3.4, 5.3)
|
||||||
|
|
||||||
|
`benchmarks/unseen/score.py` reports per family and overall: catch and false-alarm rate with Wilson 95%
|
||||||
|
intervals, the cluster bootstrap interval for false alarms, coverage counts per state, catch per method,
|
||||||
|
post-cutoff and per stream, and the alerts-only rate.
|
||||||
|
|
||||||
|
**Decision 7.1: arms at fixed budgets.** An arm with a score is thresholded per budget (1%, 5%, 10%). The
|
||||||
|
threshold is the lowest score whose false-alarm rate on the slice's ordinary changes stays within the budget.
|
||||||
|
Catch at that threshold is reported with its Wilson interval and the realised false-alarm rate. That in-slice
|
||||||
|
threshold is labelled exploratory. For a qualification, the threshold is fixed beforehand on an earlier slice or
|
||||||
|
on held-out folds, and it is applied unchanged. A binary arm (today's hook) has one operating point, reported with
|
||||||
|
the budgets it meets. Rejected: AUC over changes (it averages operating points no user runs).
|
||||||
|
|
||||||
|
**Decision 7.2: next to held-out folds.** Experiment manifests registered after this spec lands carry
|
||||||
|
`unseen: {pool, slice}`. `analyse` writes an `unseen` block into `result.json` next to the fold estimates: catch at
|
||||||
|
each budget for each arm, and the paired difference B - A with an exact McNemar test on the 100 changes. The
|
||||||
|
fold rule decides as today. The slice is recorded in the usage ledger.
|
||||||
|
|
||||||
|
**Decision 7.3: adoption gate on the unseen slice** (amended 2026-10-04, maintainer decision; Req 5.4; open
|
||||||
|
question 4 resolved). Reporting is not enough. An arm B is adopted over the production arm A only if, on the
|
||||||
|
untouched slice that qualifies it, both hold:
|
||||||
|
|
||||||
|
- catch at the 5% budget, with B's threshold fixed beforehand (7.1), is not significantly lower than A's: exact
|
||||||
|
two-sided McNemar on the paired vulnerability-introducing changes, alpha 0.05;
|
||||||
|
- for an arm with one operating point (a binary arm such as a hook setting), its false-alarm rate is not
|
||||||
|
significantly higher than A's: exact two-sided McNemar on the paired ordinary changes, alpha 0.05, with the
|
||||||
|
repository-cluster bootstrap interval of the difference reported beside it, since those changes are clustered.
|
||||||
|
|
||||||
|
`analyse` writes `unseen.gate: pass | regression | not_run` into `result.json`. `not_run` (no slice, slice
|
||||||
|
already `informed` for this change, or more than 5% `instrument_failure`) blocks adoption like `regression`. The
|
||||||
|
fold rule and this gate must both pass.
|
||||||
|
|
||||||
|
### 8. Increments
|
||||||
|
|
||||||
|
1. **Used set and guard.** `eligibility.py`, the extended guard and the usage-ledger test. Exit: guard green on an
|
||||||
|
empty pool; a planted used repository, a renamed one and a fork are each rejected in tests.
|
||||||
|
2. **Sourcing and extraction.** Candidates, both change kinds and the label check, on the VM. Exit: a draft for 300
|
||||||
|
repositories with method counts, the confirmed share and exclusion counts.
|
||||||
|
3. **Freeze p1.** Guard bisection for v3, then manifest, freeze record and protocol committed. Exit: freeze digest
|
||||||
|
committed and guard green.
|
||||||
|
4. **Dispatcher.** `benchmarks/ax/batch.py`, with the engine pass moved onto it. Exit: existing tests green and a
|
||||||
|
two-pin engine smoke on kube-ax matching its stored records.
|
||||||
|
5. **Replay task.** Image stage and entry, proven on kind, then kube-ax on five changes from this repository's own
|
||||||
|
history (never pool changes). Exit: instrument fields show bytes read and realistic JVM time, and quick-tier
|
||||||
|
output matches the VM lane.
|
||||||
|
6. **Scorer.** `score.py` on synthetic records, including every coverage state and an instrument failure. Exit:
|
||||||
|
unit tests green.
|
||||||
|
7. **Baseline.** Today's hook, from a frozen export of HEAD, on slice 1 (1,300 replays). Exit: the record committed
|
||||||
|
(counts and digests only) and a `baseline` row in the usage ledger. That is G0's exit.
|
||||||
|
8. **Arms on slice 1 (G1)** (added 2026-10-04 with decisions 4.0 and 7.3). The registered long-deadline arm,
|
||||||
|
the engine-evidence arm from the stored records, the model arms on the VM (5.4) under their budgets, and the
|
||||||
|
`unseen` block with the adoption gate in `analyse`. Exit: one committed table of arms on slice 1 at the 1/5/10%
|
||||||
|
budgets (counts, rates, intervals, digests), and `exploratory` rows in the usage ledger.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
A failed fetch, a missing commit or a hook crash gives a failure record, never an empty success. A dead sandbox is
|
||||||
|
retried once, then the VM runs it. A store or AX cleanup error stops the dispatcher. A slice with more than 5%
|
||||||
|
`instrument_failure` after re-runs is not scored; the cause is fixed first.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
Unit tests: used-set normalisation (case, `.git`, renames, forks), blame acceptance on a synthetic repository,
|
||||||
|
ordinary-commit exclusions, Wilson values against section 3, budget thresholds, coverage states, the task manifest
|
||||||
|
and dispatcher resume and fallback with a fake lane. Guard tests run in CI; cluster runs stay manual.
|
||||||
|
|
||||||
|
## Requirement Traceability
|
||||||
|
|
||||||
|
| Requirement | Design |
|
||||||
|
| --- | --- |
|
||||||
|
| 1.1 eligibility against every source, v3 via the guard | 1.2 |
|
||||||
|
| 1.2 frozen manifest, never edited | 6, increment 3 |
|
||||||
|
| 1.3 pool directory, guard extended | 6 |
|
||||||
|
| 1.4 licences, private manifest | 1.4 |
|
||||||
|
| 2.1 introducing changes, family, function, post-cutoff | 1.1, 1.3, 2.1 |
|
||||||
|
| 2.2 ordinary changes across time and size | 2.2 |
|
||||||
|
| 2.3 sizes for interval widths | 3 |
|
||||||
|
| 3.1 default replay, skipped checks kept | 4, 5.2 |
|
||||||
|
| 3.2 catch and false-alarm rules | 4.1, 4.2 |
|
||||||
|
| 3.3 per family, Wilson, coverage | 4.3, 7 |
|
||||||
|
| 3.4 arms at 1/5/10% budgets | 4.0, 7.1, increment 8 |
|
||||||
|
| 4.1 one change one Task, prefix, image by digest | 5.1, 5.2 |
|
||||||
|
| 4.2 HTTPS fetch, presigned record, no keys | 5.2 |
|
||||||
|
| 4.3 generic dispatcher, concurrency, retry, fallback, resume | 5.3 |
|
||||||
|
| 4.4 model arms on the VM within budgets | 5.4 |
|
||||||
|
| 4.5 kind runs the same tasks | 5.3, 5 (kind paragraph) |
|
||||||
|
| 5.1 baseline on slice 1, counts and digests | 6, increment 7 |
|
||||||
|
| 5.2 fresh slice per decision | 3.1, usage ledger |
|
||||||
|
| 5.3 experiments report unseen next to folds | 7.2 |
|
||||||
|
| 5.4 adoption needs no significant unseen regression | 7.3 |
|
||||||
|
|
||||||
|
## Amendments (2026-10-11)
|
||||||
|
|
||||||
|
Folded in after the G0 pool was frozen; the original sections above stay as the design of record, these refine it
|
||||||
|
where reality differed.
|
||||||
|
|
||||||
|
- **Persistence is bucket-first, not local disk.** The draw streams every stage to the `ousast-corpus` RustFS
|
||||||
|
bucket and resumes from it, so a restart (even after a code change) is cheap. Layout: `unseen/<seed>/decisions/`
|
||||||
|
(accept/reject per repo, key `sha256(repo+fix)`, idempotent), `unseen/<seed>/resolved/` (per-advisory
|
||||||
|
resolution), `unseen/<seed>/advisories/list.json`, `unseen/repos/` (get_repo cache, **not** seed-scoped, shared
|
||||||
|
across seeds), and `unseen/<seed>/pool/` (the frozen pool + provenance — the "don't redo sourcing" archive). The
|
||||||
|
local journal is a within-process cursor only. Implementation: `benchmarks/unseen/bank.py` (`CorpusBank`). The
|
||||||
|
VM's local disk is the scarce resource; run artifacts and replay outputs belong on RustFS (see steering
|
||||||
|
`storage-streaming.md`). Credentials: `.env` `CORPUS_S3_*`; boto3 needs path-style + SigV4.
|
||||||
|
- **Harvest cost is GitHub-rate-limit bound, measured yield ~2.2%.** Through the strict change criteria (1 vuln +
|
||||||
|
12 stratified ordinary, >=40 eligible commits) ~2.2% of candidates are accepted, so "100 repos" is ~4,500
|
||||||
|
candidate attempts at ~4 GitHub calls each — the bottleneck is the 5,000/hr API limit, not CPU or cores. Pure
|
||||||
|
GitHub-API harvesting cannot replace the clone: the positive is the SZZ introducing commit and the negatives are
|
||||||
|
stratified over full history, both needing repo history. The convergence fix was a full-objects clone (dropping
|
||||||
|
`--filter=blob:none`, which caused ~600 per-blob lazy fetches) plus the bucket caches above.
|
||||||
|
- **Record 07 is a machine-label baseline.** The pool's 100 vulnerability labels are SZZ-derived and unchecked
|
||||||
|
(`freeze-p1.json` `label_check_counts`: 100 unchecked). The G0 baseline is reported as a machine-label number
|
||||||
|
with that caveat; a human label pass is a later quality refinement, required before any published precision
|
||||||
|
claim, not before record 07.
|
||||||
|
- **Replay image provenance is pinned.** CI `engine-image.yml` builds `ghcr.io/norandom/ousast-replay-task` (the
|
||||||
|
replay harness, which `COPY`s `/app/benchmarks/unseen/replay_entry.py`) and `ousast-engine-task` (the Joern
|
||||||
|
hook), publicly pullable by digest. `ousast-runner` (the 248 MB ax-task-runner, no `/app/benchmarks`, no Joern)
|
||||||
|
is **not** the replay image. The AX lane's store check (`benchmarks/ax/batch.py`) requires a public-HTTPS DNS
|
||||||
|
presign-capable endpoint; how that and the cluster's store access are provided is operator-owned infra, not
|
||||||
|
assumed here.
|
||||||
|
|
||||||
|
- **AX lane blocker (2026-10-11) — kube is the target lane, so this is tracked, not dismissed.** The replay
|
||||||
|
*dispatches* correctly but tasks do not *complete*: `pins_done` stayed 0 for both the 5-change proof and the
|
||||||
|
1,300-change slice 1. Sub-issues resolved along the way: (a) the replay image is `ousast-replay-task` (public);
|
||||||
|
(b) the ax CLI must be `~/go/bin/ax` — the `~/.local/ax-v0.3.1/ax` build calls an `UpdateTask` RPC the deployed
|
||||||
|
ax-server answers `Unimplemented` (client/server version mismatch); (c) the store endpoint
|
||||||
|
`files.because-security.com` / `sast-memory` passes the egress check. **Remaining blocker (AX/cluster-internal,
|
||||||
|
operator-owned):** dispatched tasks reach the `ousast-pool` workers and the replay actors execute and checkpoint,
|
||||||
|
but the result never returns to the coordinator and no completion is recorded. `ousast-pool` logs show a gVisor
|
||||||
|
cleanup failure: `runsc delete ... connecting to control server at PID NNN: connection refused` and `failed to
|
||||||
|
terminate workload after checkpoint ... runsc delete: exit status 128`. Until the completion / checkpoint-cleanup
|
||||||
|
path is fixed, the ax lane cannot produce results; the G0 baseline (7.2) was taken on the **docker lane**
|
||||||
|
instead. The ax lane must be restored before it is the baseline/G1 lane (the stated target).
|
||||||
|
|
||||||
|
- **Engine coverage gap is comparison, not deadline (2026-10-11, preliminary n=1).** The cold-cache default arm
|
||||||
|
(30s) times out the Joern CPG build → `quick_only`. But a long-deadline sample (`--arm long_deadline`, 300s) on a
|
||||||
|
vulnerability change ran **206s with no timeout and still did not cover** (`coverage_status: incomplete`,
|
||||||
|
`quick_only`): the reasons were `ambiguous_line_correspondence`, `unmatched_added_deleted_path_correspondence`,
|
||||||
|
`eligibility_provenance_mismatch`, `witness_unresolved`, `comparison_unknown` — i.e. the engine could not
|
||||||
|
correspond the replayed diff's lines between base and head. So **calibration / a longer deadline alone will not
|
||||||
|
give engine coverage**; the blocker is the engine's base↔head comparison on real replayed diffs (consistent with
|
||||||
|
the earlier "engine near-blind to fixes" slice-leak audit). This is G2 engine-fix territory (comparison /
|
||||||
|
line-correspondence), not a G0 deadline knob. To confirm beyond n=1, the long-deadline arm must run at scale
|
||||||
|
(needs the kube lane). Implication: the record-07 baseline is measured on the **quick-rule tier** (the default
|
||||||
|
arm), and the engine arm's value is gated on fixing the comparison path, not on calibration.
|
||||||
|
|
||||||
|
- **Line-correspondence fix made, but it is necessary-not-sufficient (2026-10-11).** Root cause confirmed in
|
||||||
|
`src/openultrasast/push/snapshot.py`: equal-region base↔head line pairs were mapped only when the line content
|
||||||
|
was *globally unique in the file*, so common lines (`}`, blanks, imports) tripped `ambiguous_line_correspondence`
|
||||||
|
and the finding's anchor was discarded → `comparison_unknown` → finding suppressed. Fixed to **run-aware
|
||||||
|
positional anchoring**: a pair maps when its content occupies exactly one contiguous equal run per side (intra-run
|
||||||
|
duplicates are forced/safe; cross-run duplicates stay withheld — soundness preserved, `test_push_snapshot.py`
|
||||||
|
moved-operation guard still passes). 163 push tests pass. BUT live-testing the fix (mounted over the installed
|
||||||
|
package, 300s) on a real messy change (a 14-file Java diff) did **not** flip coverage: it stayed `incomplete`
|
||||||
|
because the binding constraints are elsewhere — `capability_unavailable` (empty capability registry always blocks
|
||||||
|
`coverage_status=complete` until Task 8.3), the engine produced **no actionable defect at all**
|
||||||
|
(`actionable_defect_ids: []`, a detection gap, not correspondence), plus `eligibility_provenance_mismatch` and
|
||||||
|
`contributor-scan ... projection_unavailable`. So "near-blind to fixes" is **multi-causal**; the correspondence
|
||||||
|
fix removes one real cause but engine coverage on unseen diffs also needs the capability registry (Task 8.3), the
|
||||||
|
engine's actual vuln detection, and the eligibility/projection paths. Validating the correspondence fix's benefit
|
||||||
|
needs a change where correspondence is the sole blocker (a small single-file diff), or the engine arm at scale.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Maintainer decisions of 2026-10-04 resolve questions 1, 2, 4 and 5; question 3 stays open for G3.
|
||||||
|
|
||||||
|
1. **Resolved 2026-10-04.** Licences: only permissively licensed repositories are published by name. Copyleft and
|
||||||
|
all other licences go to the gitignored private manifest, covered in the committed manifest by an opaque id and
|
||||||
|
a digest (Req 1.4 as written; decision 1.4). Population v2's OSI-approved bar is not used.
|
||||||
|
2. **Resolved 2026-10-04.** Concurrency stays at two AX lanes plus the VM lane. No operator placement change is
|
||||||
|
requested (5.3).
|
||||||
|
3. Per-family BLOCK (G3) needs about 100 changes per family. Pool slices together, or draw a family-enriched pool
|
||||||
|
for G3?
|
||||||
|
4. **Resolved 2026-10-04.** An adoption requires no significant regression on the unseen slice, not only a report
|
||||||
|
(Req 5.4, amended; decision 7.3).
|
||||||
|
5. **Resolved 2026-10-04.** G1 adds a registered long-deadline arm (`--deadline 300`) next to the default 30 s, so
|
||||||
|
the table shows both what the user sees and what the engine could catch (decision 4.0, increment 8).
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The safety net is meant for repositories it has never seen. This spec builds the instrument that measures that: a
|
||||||
|
frozen pool of unseen repositories, a push-level metric (catch rate and false-alarm rate as the hook reports them),
|
||||||
|
and an execution path that runs the replays as AX Tasks on the kube-ax cluster. Context: `brief.md`, the roadmap's
|
||||||
|
G0-G1 (`.kiro/steering/roadmap.md`).
|
||||||
|
|
||||||
|
## Boundary Context
|
||||||
|
|
||||||
|
- **In scope**: pool selection and freezing; change extraction (vulnerability-introducing and ordinary commits);
|
||||||
|
replay of each change through `ousast pre-push` and through registered experiment arms; the metric and its
|
||||||
|
confidence intervals; AX Task execution with kind/VM fallback; the baseline for today's hook; rotation of pool
|
||||||
|
slices.
|
||||||
|
- **Out of scope**: any detection change; population v3 (stays reserved and unread); model calls inside the cluster.
|
||||||
|
- **Adjacent expectations**: `learned-decision-engine` experiments use this pool as their primary unseen-repo score;
|
||||||
|
`pre-push-safety-net` owns the hook being replayed; `plane-on-kubernetes` and the AX executor
|
||||||
|
(`benchmarks/learn/engine_trace_ax.py`) supply the execution pattern; the reserved-repository guard covers every
|
||||||
|
population and this pool.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: A pool no development step has touched
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want the evaluation repositories to be ones nothing in development has seen, so a
|
||||||
|
good number means the approach generalises.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. A repository is eligible only if it appears in no pair corpus, no population (v1, v2, v3), no harvest record, no
|
||||||
|
memory example, no experiment unit and no measurement record. Eligibility is checked by code against every source
|
||||||
|
before the pool is frozen; v3 is checked through the existing guard without being opened.
|
||||||
|
2. The pool is frozen (a committed manifest with repository URLs, commit SHAs, licences and the freeze digest) before
|
||||||
|
any change in it is replayed. A frozen pool is never edited; a correction is a new pool.
|
||||||
|
3. Pool repositories are referenced only inside the pool's own directory (like `benchmarks/independent/`), and the
|
||||||
|
reserved-repository guard is extended to them.
|
||||||
|
4. Only permissively licensed repositories are published by name; others may be used with their names kept out of
|
||||||
|
the repository (pointers in a local, gitignored manifest), recorded as such.
|
||||||
|
|
||||||
|
### Requirement 2: Two kinds of change per repository
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want both vulnerable and ordinary pushes from the same repositories, so the
|
||||||
|
false-alarm rate is measured on the code the catch rate is measured on.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. Vulnerability-introducing changes: from public advisories with a linked fix, the commit range that introduced the
|
||||||
|
vulnerable code where it can be determined, else the fix's parent compared with an earlier base; each labelled
|
||||||
|
with family and the vulnerable function. Post-model-cutoff advisories are preferred and flagged.
|
||||||
|
2. Ordinary changes: commits from the same repositories' histories with no security fix, revert or advisory link,
|
||||||
|
sampled across time and size; each recorded with its changed files and lines.
|
||||||
|
3. Per repository, at least one vulnerability-introducing change and several ordinary changes; the pool's totals are
|
||||||
|
sized so the false-alarm rate's 95% interval is no wider than +/- 3 points and the catch rate's no wider than
|
||||||
|
+/- 10 points (sizes computed in the design).
|
||||||
|
|
||||||
|
### Requirement 3: The push-level metric
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want the score to be what a user would see, so the number means something.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. Each change is replayed as `ousast pre-push --base <base> --head <head>` with the hook's default settings, in a
|
||||||
|
clean checkout, with the record of every skipped check kept.
|
||||||
|
2. Catch: a vulnerability-introducing change counts as caught when the hook (or the arm) reports a finding of the
|
||||||
|
right family on the vulnerable function or its changed lines. False alarm: an ordinary change counts as a false
|
||||||
|
alarm when the hook reports any advisory finding.
|
||||||
|
3. Reported per family and overall: catch rate, false-alarm rate, and both with 95% Wilson intervals; plus coverage
|
||||||
|
(changes where the hook could analyse the changed language) so a silent zero is visible.
|
||||||
|
4. Arms (decision engine, combiner, engine evidence, fix mechanisms) are scored on the same replays: catch rate at
|
||||||
|
fixed false-alarm budgets (1%, 5%, 10%), so arms are compared at equal noise.
|
||||||
|
|
||||||
|
### Requirement 4: Execution on kube-ax, designed for AX
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want the replays to run on the cluster, so measurements are fast and the VM is
|
||||||
|
free.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. One replayed change is one AX Task, named with the `ousast-engine-` prefix the operator's egress watcher matches,
|
||||||
|
using an image built `FROM ghcr.io/norandom/ax-task-runner:v0.3.1` with this package, Joern and php-cli, pinned by
|
||||||
|
digest.
|
||||||
|
2. The task fetches the repository at the needed commits itself over HTTPS (public hosts are reachable from actors)
|
||||||
|
and writes its record through a presigned link; no API keys or store credentials enter the cluster.
|
||||||
|
3. A generic AX batch dispatcher serves both this replay and the existing engine passes: items to Tasks, at most the
|
||||||
|
cluster's safe concurrency (two heavy tasks per worker), retry once on sandbox death, then a VM or kind fallback
|
||||||
|
lane; progress and resume as in the engine runner.
|
||||||
|
4. Arms that need model calls take the cluster's records and make their calls on the VM, within registered budgets.
|
||||||
|
5. kind runs the same tasks unchanged as the local fallback.
|
||||||
|
|
||||||
|
### Requirement 5: Baseline, rotation and use
|
||||||
|
|
||||||
|
**User Story:** As the maintainer, I want a baseline now and a fresh slice for each decision, so the pool is not
|
||||||
|
worn out by tuning.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The first use is a baseline of today's hook on the first slice; its record is committed (counts and digests only,
|
||||||
|
no repository names outside the pool directory).
|
||||||
|
2. A slice that informed a change can no longer qualify that change; each adoption decision draws a new, untouched
|
||||||
|
slice, recorded with its freeze digest.
|
||||||
|
3. Every learned-decision-engine experiment from now on reports its unseen-repo score from this pool next to its
|
||||||
|
held-out-fold score.
|
||||||
|
4. An arm is adopted only if, on the untouched slice that qualifies it, it shows no significant regression against
|
||||||
|
the current production arm: catch at the 5% false-alarm budget not significantly lower, and for an arm with one
|
||||||
|
operating point the false-alarm rate not significantly higher (exact paired tests, two-sided, alpha 0.05).
|
||||||
|
Reporting the unseen score alone is not enough to adopt. *(amended 2026-10-04, maintainer decision)*
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"feature_name": "unseen-repo-evaluation",
|
||||||
|
"created_at": "2026-10-04T18:00:00Z",
|
||||||
|
"updated_at": "2026-10-04T20: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
|
||||||
|
}
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
One task group per increment of `design.md` section 8, in that order. A group starts only after the previous
|
||||||
|
group's exit is recorded. Maintainer decisions of 2026-10-04 are folded in: a registered long-deadline arm
|
||||||
|
(`--deadline 300`) beside the hook's default 30 s (design 4.0, group 8); adoption needs no significant regression
|
||||||
|
on the unseen slice (Req 5.4 as amended, design 7.3, tasks 6.4 and 8.2); only permissively licensed repositories
|
||||||
|
are published by name, everything else goes to the gitignored private manifest under a digest (Req 1.4, design
|
||||||
|
1.4); concurrency stays at two AX lanes plus the VM lane (design 5.3).
|
||||||
|
|
||||||
|
**2026-10-11 amendments folded in** (see design.md "Amendments (2026-10-11)"):
|
||||||
|
|
||||||
|
- **Pool p1 is frozen.** `benchmarks/unseen/pool-p1.toml` = 100 repos, slice 1; `freeze-p1.json` guard=0,
|
||||||
|
dropped=0, deterministic (manifest `6bd2fa5d`); archived to `ousast-corpus/unseen/20260601/pool/`. Groups 1-3 are
|
||||||
|
effectively done (the draw, draft and freeze ran end to end); formal records 02/03 were not written as separate
|
||||||
|
`record.json` files — the freeze was run directly (`bisect_guard` then `freeze`). Freeze recipe: `bisect_guard
|
||||||
|
--root $PWD --draft $CACHE/journal/draft-p1.toml` then `freeze --root $PWD --used-report <FILE>` where the file
|
||||||
|
is the draw report's `used_set` object (not the whole report).
|
||||||
|
- **Persistence is bucket-first** (`ousast-corpus`, `benchmarks/unseen/bank.py`); the local journal is a
|
||||||
|
within-process cursor only. Raw per-change replay records go to **RustFS**, not `~/ousast-results/` on local disk
|
||||||
|
— amend tasks 5.3/7.2's `--out` accordingly.
|
||||||
|
- **Group 5 image note:** the replay image is the CI-built `ghcr.io/norandom/ousast-replay-task` (+ engine-task),
|
||||||
|
publicly pullable by digest — not a local export-HEAD build and not `ousast-runner`. Task 7.1's "build and push
|
||||||
|
the replay image" is satisfied by the CI `engine-image.yml` run; use that digest.
|
||||||
|
- **Record 07 is a machine-label baseline** (labels SZZ-derived, 100 unchecked); state the caveat in the record.
|
||||||
|
- **Baseline lane (7.2):** run on kube (`--lane ax`). The `ax` binary and the AX-lane store endpoint are
|
||||||
|
operator-owned infra to stand up before 7.2; do not assume the cluster/network state.
|
||||||
|
|
||||||
|
Execution rules:
|
||||||
|
|
||||||
|
- **Who does what.** Codex implements every coding task offline: no network, no model, no store, no cluster. The
|
||||||
|
coordinator runs every networked step (GitHub and OSV API calls, git fetches and clones, the S3 store, image
|
||||||
|
pushes, kind and kube-ax). Those tasks are marked **(coordinator, network)**. Steps that need the coordinator but
|
||||||
|
no network (agent label reads, adjudication, the v3 guard bisection, commits) are marked **(coordinator)**.
|
||||||
|
Steps that spend model money are marked **paid: needs a budget go-ahead**. Each starts only after the maintainer
|
||||||
|
confirms a ceiling against the provider balance read just before it.
|
||||||
|
- **Done means.** A coding task is done when its tests pass in the host suite, `ruff check`, `ruff format --check`
|
||||||
|
and `mypy` are clean, and the reserved-repository guard is green. No test makes a network, model or cluster call.
|
||||||
|
- **Names stay in the pool directory.** Pool repository names and URLs appear only under `benchmarks/unseen/`
|
||||||
|
(copyleft and other non-permissive ones only under the gitignored `benchmarks/unseen/private/`). Raw per-change
|
||||||
|
records live in `~/ousast-results/unseen/` and the store, never in the repository.
|
||||||
|
- **Population v3 is never opened.** It is checked only through the guard test, run as a black box with its
|
||||||
|
output discarded.
|
||||||
|
- **Every group ends with a measured exit** and a record at
|
||||||
|
`benchmarks/measurements/<date>-unseen-NN-<slug>/record.json`: commit, image digests where used, the instrument
|
||||||
|
checks (bytes read, JVM time, exit codes), counts and digests. No repository names. The guard sweeps these
|
||||||
|
records.
|
||||||
|
- **Instrument first** (AGENTS.md). A zero, an empty draft or a fast run is treated as an instrument failure until
|
||||||
|
bytes read and wall time prove otherwise.
|
||||||
|
|
||||||
|
- [x] 1. Used set and guard (design 1.2, 6; increment 1)
|
||||||
|
- [x] 1.1 Used-set builder
|
||||||
|
- `benchmarks/unseen/eligibility.py`: normalises names (lower case, no `.git`, no trailing slash) and builds the
|
||||||
|
used set from every source of design table 1.2: pair catalogs, recipes and datasets under `benchmarks/pairs/`
|
||||||
|
plus the local pointer-pair manifests as `benchmarks/pairs/catalog_gen.py` reads them; `population-v1.toml`
|
||||||
|
and `population-v2.toml`; the advisory-fix batch catalogs; memory `example` rows (`repo` field) through an
|
||||||
|
injected `MemoryStore` (`FileStore` locally, the S3 store when the coordinator passes one); experiment units
|
||||||
|
under `plane/experiments/`; `benchmarks/measurements/` and `benchmarks/experiments/`; a text sweep for
|
||||||
|
`github.com/<owner>/<name>` over `benchmarks/`, `src/`, `tests/`, `plane/` and `.kiro/`. It never reads
|
||||||
|
population v3 and never prints a name. Output: counts per source and the sha256 of the sorted set.
|
||||||
|
- `tests/test_unseen_eligibility.py`: a synthetic tree with one repository per source, each found; case, `.git`
|
||||||
|
and trailing-slash variants collapse; stdout and the output JSON contain no name; a v3-shaped file planted in
|
||||||
|
the synthetic tree is not opened (open is patched to fail on it).
|
||||||
|
- _Requirements: 1.1_
|
||||||
|
- [x] 1.2 Canonical resolution, renames and forks
|
||||||
|
- `eligibility.py`: `resolve(candidate, api)` with an injected GitHub client returns the canonical `full_name`,
|
||||||
|
former names (redirects) and the fork network (`parent`, `source`). A candidate is ineligible when any of them
|
||||||
|
is in the used set; one candidate per fork network is kept.
|
||||||
|
- Tests with a fake client: a planted used repository, a renamed one and a fork of a used one are each
|
||||||
|
rejected; two forks of one network keep one; the rejection reason is a source label, not a name.
|
||||||
|
- _Requirements: 1.1_
|
||||||
|
- [x] 1.3 Guard extended to pools
|
||||||
|
- `tests/test_independent_population.py`: the sweep becomes a list of (reservation directory, names):
|
||||||
|
`benchmarks/independent/` for populations, `benchmarks/unseen/` for pools (public manifests, plus
|
||||||
|
`benchmarks/unseen/private/*.toml` when present). The sweep also covers `plane/`. New tests: pool and
|
||||||
|
population names are disjoint (failure prints a count only); a frozen pool's manifest digest equals its
|
||||||
|
`freeze-pN.json` (skipped while no pool is frozen). The existing v3 handling is unchanged.
|
||||||
|
- Each reservation is exempt only inside its own directory: population names (v3 included) are still swept in
|
||||||
|
`benchmarks/unseen/`, the gitignored `private/` files included. Task 3.1 depends on this.
|
||||||
|
- `.gitignore`: `benchmarks/unseen/private/`.
|
||||||
|
- Evidence: the guard is green with an empty `benchmarks/unseen/`, and red when a test fixture names a pool
|
||||||
|
repository in `benchmarks/measurements/` (shown once in the record).
|
||||||
|
- _Requirements: 1.2, 1.3, 1.4_
|
||||||
|
- [x] 1.4 Usage ledger
|
||||||
|
- `benchmarks/unseen/ledger.py`: append-only `usage-pN.jsonl`, one row per use (slice, decision or experiment
|
||||||
|
id, purpose `baseline | exploratory | informed | qualifies`, date, result digest, freeze digest). It refuses a
|
||||||
|
`qualifies` row for a change whose slice already has an `informed` row for it, and refuses to rewrite rows.
|
||||||
|
- `tests/test_unseen_ledger.py`: append, the refusal, a rewritten row detected by a digest chain.
|
||||||
|
- _Requirements: 5.2_
|
||||||
|
- [x] 1.5 Used-set measurement and record 01 **(coordinator, network)**
|
||||||
|
- Run `eligibility.py` against the real sources, the S3 memory store included (read only).
|
||||||
|
- Instrument: each source reports files read and bytes; a source with zero rows fails the run unless it is
|
||||||
|
known empty.
|
||||||
|
- `benchmarks/measurements/<date>-unseen-01-used-set/record.json`: counts per source, used-set digest, guard
|
||||||
|
result (green on the empty pool), test counts of 1.1-1.4.
|
||||||
|
- _Requirements: 1.1, 1.3_
|
||||||
|
|
||||||
|
- [ ] 2. Sourcing and extraction (design 1.1, 1.3, 1.4, 2, 3; increment 2)
|
||||||
|
- [x] 2.1 Advisory candidates and repository filters
|
||||||
|
- `benchmarks/unseen/source.py`: from GHSA advisory JSON (pip, npm, maven, composer; published 2019 or later)
|
||||||
|
keep those whose CWE maps to a family through `src/openultrasast/model/taxonomy.py` and that link one fix
|
||||||
|
commit with one parent in one repository; attach the OSV first affected version; flag `post_cutoff` when
|
||||||
|
published after 2026-06-01. Repository filters: anonymous HTTPS, at most 500 MB, licence at the pinned head
|
||||||
|
(LICENSE file, then the API's SPDX id) classed `permissive` (the design 1.4 list) or `private`.
|
||||||
|
- `tests/test_unseen_source.py` on recorded fixture JSON (no network): CWE mapping, multi-parent and
|
||||||
|
multi-repository fixes dropped, cutoff flag, licence classing (GPL, AGPL, LGPL, none and source-available go
|
||||||
|
private).
|
||||||
|
- _Requirements: 1.4, 2.1_
|
||||||
|
- [x] 2.2 Vulnerability-introducing range
|
||||||
|
- `benchmarks/unseen/extract.py`: the `introducing` method of design 2.1 (blame of the sink line, or the
|
||||||
|
declaration for an absence bug; absence at V's first parent; no merge or root; at most 50 files and 2,000
|
||||||
|
lines; OSV version bracket when present; walk-back of at most five steps) and the `last_touch` fallback. Each
|
||||||
|
change records method, family, vulnerable function resolved in head, head-side lines, `post_cutoff`, advisory
|
||||||
|
id. Git only; no scanner or engine.
|
||||||
|
- `tests/test_unseen_extract.py` on a synthetic repository built in a temp dir: accepted V; a reformat walked
|
||||||
|
back; a merge rejected; size cap; bracket rejection; fallback chosen when no V passes.
|
||||||
|
- _Requirements: 2.1_
|
||||||
|
- [x] 2.3 Ordinary-commit draw
|
||||||
|
- `extract.py`: first-parent, non-merge commits touching an analysed language; the exclusions of design 2.2
|
||||||
|
(security words of the narrow pattern, reverts, bot and bump commits, manifest- or lock-only, generated or
|
||||||
|
vendored, advisory fix commits, commits touching a known vulnerable function, more than 1,000 source lines,
|
||||||
|
counted). Seeded draw of 12 per repository over three time thirds by four size buckets, proportional, at least
|
||||||
|
one per third; a repository with fewer than 40 eligible commits is dropped.
|
||||||
|
- Tests: each exclusion; the narrow pattern keeps "query", "token", "session"; allocation and the seed
|
||||||
|
reproduce the same draw; the 40-commit floor.
|
||||||
|
- _Requirements: 2.2, 2.3_
|
||||||
|
- [x] 2.4 Draft manifest and slices
|
||||||
|
- `benchmarks/unseen/draft.py`: writes `benchmarks/unseen/draft-p1.toml` (permissive entries by name) and
|
||||||
|
`benchmarks/unseen/private/draft-p1.toml` (the rest); the public file holds an opaque id and
|
||||||
|
sha256(URL, commits) per private entry. Seeded shuffle into three slices of 100 repositories, stratified by
|
||||||
|
ecosystem and `post_cutoff`; each repository has one introducing change and 12 ordinary changes.
|
||||||
|
- Tests: the digest covers private entries; no private URL in the public file; the same seed gives the same
|
||||||
|
slices; per-slice strata within one repository of proportional.
|
||||||
|
- _Requirements: 1.2, 1.4, 2.3, 5.2_
|
||||||
|
- [ ] 2.5 Sourcing run **(coordinator, network)**
|
||||||
|
- `gh api /advisories` enumeration, OSV lookups, canonical resolution against the used set (1.2), blobless
|
||||||
|
clones into a scratch cache, extraction (2.2, 2.3), draft (2.4).
|
||||||
|
- Instrument: clones counted with bytes on disk; a repository whose clone reads zero source files is an
|
||||||
|
instrument failure, not a drop. Fewer than 300 eligible repositories stops the run with counts; the
|
||||||
|
eligibility bars are not relaxed to fill it.
|
||||||
|
- Observable: a draft of 300 repositories (3,900 changes) on disk.
|
||||||
|
- _Requirements: 1.1, 1.4, 2.1, 2.2, 2.3_
|
||||||
|
- [ ] 2.6 Label check and record 02 **(coordinator)**
|
||||||
|
- Every third `introducing` change read by an agent, no model call, marked `confirmed | refactor | unclear`
|
||||||
|
in the draft.
|
||||||
|
- `benchmarks/measurements/<date>-unseen-02-draft/record.json`: candidates per ecosystem, rejections per used
|
||||||
|
source, licence split (public, private), method counts, post-cutoff counts, confirmed share, ordinary-commit
|
||||||
|
exclusion counts. No names.
|
||||||
|
- _Requirements: 2.1, 2.2_
|
||||||
|
|
||||||
|
- [ ] 3. Freeze p1 (design 1.2, 6; increment 3)
|
||||||
|
- [x] 3.1 Guard bisection tool
|
||||||
|
- `benchmarks/unseen/bisect_guard.py`: rewrites the draft with a candidate subset, runs
|
||||||
|
`.venv/bin/pytest -q -p no:cacheprovider tests/test_independent_population.py -k referenced` with stdout and
|
||||||
|
stderr discarded, reads only the exit code, and bisects until green. Reports the dropped count only.
|
||||||
|
- `tests/test_unseen_bisect.py` with an injected fake guard (a hidden set): finds and drops exactly the hidden
|
||||||
|
members; never logs a candidate; an exit code other than 0 or 1 aborts (instrument failure).
|
||||||
|
- _Requirements: 1.1_
|
||||||
|
- [x] 3.2 Freeze writer
|
||||||
|
- `benchmarks/unseen/freeze.py`: canonical `pool-p1.toml` (and the private counterpart) from the bisected draft;
|
||||||
|
`freeze-p1.json` with used-set digest and per-source counts, guard result, dropped count, label-check counts
|
||||||
|
and the freeze digest (sha256 of the canonical manifest). Refuses to write over a frozen pool; a correction
|
||||||
|
is `p2`.
|
||||||
|
- Tests: digest stable under key order; overwrite refused; the guard's digest test (1.3) passes on a fixture
|
||||||
|
pool.
|
||||||
|
- _Requirements: 1.2_
|
||||||
|
- [x] 3.3 Protocol p1
|
||||||
|
- `benchmarks/unseen/protocol-p1.md`: the scoring rules of design section 4 (default flags, decision 4.0's
|
||||||
|
long-deadline arm, catch and location rules, false alarm, coverage states), the statistics of section 3 and
|
||||||
|
7, and the adoption gate of 7.3, stated before any replay.
|
||||||
|
- Observable: the file exists and its sha256 is listed in `freeze-p1.json`.
|
||||||
|
- _Requirements: 3.1, 3.2, 3.3, 3.4, 5.4_
|
||||||
|
- [ ] 3.4 Freeze run and record 03 **(coordinator)**
|
||||||
|
- Run the bisection (3.1), then the freeze (3.2). Guard green, digest test active.
|
||||||
|
- `benchmarks/measurements/<date>-unseen-03-freeze/record.json`: freeze digest, protocol digest, dropped
|
||||||
|
count, final counts per slice and kind.
|
||||||
|
- _Requirements: 1.1, 1.2, 1.3_
|
||||||
|
|
||||||
|
- [ ] 4. Dispatcher (design 5.3; increment 4)
|
||||||
|
- [x] 4.1 Items, workloads and the AX lane
|
||||||
|
- `benchmarks/ax/batch.py`: `Item` (id, input objects, result object name, command, extra env, input digest),
|
||||||
|
`Workload` (image digest, allowed URL env names, deadline, result validator) and the `ax` lane, moved from
|
||||||
|
`benchmarks/learn/engine_trace_ax.py`: manifest render, egress readiness, presigned GET and PUT as `.dat`,
|
||||||
|
apply, poll result object and AX phase, delete, object cleanup. The `kind` lane is the same lane with the
|
||||||
|
kind context.
|
||||||
|
- `tests/test_ax_batch.py` with a fake `ax` CLI and a fake store: manifest holds only the allowed URL env
|
||||||
|
names and no credential; names carry the `ousast-engine-` prefix; cleanup on success and failure.
|
||||||
|
- _Requirements: 4.1, 4.2, 4.5_
|
||||||
|
- [x] 4.2 Scheduler, fallback and resume
|
||||||
|
- `batch.py`: two AX lanes and one VM `docker` lane by default (one container at a time); sandbox death retried
|
||||||
|
once under a new name, a second death or a JVM OOM goes to the VM fallback queue ahead of overflow;
|
||||||
|
`progress.json` written by the scheduler only; a `STOP` file halts launches; resume skips items whose stored
|
||||||
|
record has the same input digest; a cleanup failure stops dispatching.
|
||||||
|
- Tests with fake lanes: retry then fallback; OOM straight to fallback; resume skips; `STOP`; cleanup failure
|
||||||
|
halts; never more than two AX tasks in flight.
|
||||||
|
- _Requirements: 4.3_
|
||||||
|
- [x] 4.3 Engine pass on the dispatcher
|
||||||
|
- `benchmarks/learn/engine_trace_ax.py` and `engine_trace.run_plan` become an engine `Workload` over `batch.py`;
|
||||||
|
no behaviour change.
|
||||||
|
- Observable: `tests/test_ax_executor.py` and `tests/test_engine_trace.py` pass with their assertions unchanged
|
||||||
|
(moved imports only).
|
||||||
|
- _Requirements: 4.3_
|
||||||
|
- [ ] 4.4 Engine smoke on kube-ax and record 04 **(coordinator, network)**
|
||||||
|
- Two pins through the new dispatcher on kube-ax, compared with their stored records.
|
||||||
|
- `benchmarks/measurements/<date>-unseen-04-dispatcher/record.json`: image digest, per-pin wall time and JVM
|
||||||
|
time, record digests equal to the stored ones (or the diff counted), cleanup result.
|
||||||
|
- _Requirements: 4.3_
|
||||||
|
|
||||||
|
- [ ] 5. Replay task (design 5.1, 5.2; increment 5)
|
||||||
|
- [x] 5.1 Image stage
|
||||||
|
- `plane/Dockerfile.engine-task`: second stage `FROM engine AS replay` adding `git`, the package with its
|
||||||
|
runtime dependencies and `benchmarks/unseen/replay_entry.py`; smoke `ousast pre-push --help`. The root image
|
||||||
|
stays `ghcr.io/norandom/ax-task-runner:v0.3.1`; the engine stage is unchanged.
|
||||||
|
- `tests/test_unseen_image.py`: parses the Dockerfile; the `replay` stage builds from `engine`; the root image
|
||||||
|
pin; the engine stage's text is byte-identical to before.
|
||||||
|
- _Requirements: 4.1_
|
||||||
|
- [x] 5.2 Task entry
|
||||||
|
- `benchmarks/unseen/replay_entry.py`, standard library only, reusing `engine_task_entry.transfer`: GET the spec;
|
||||||
|
`git init` and depth-1 fetches of head and base over HTTPS (180 s bound), verified with `git cat-file -e`; run
|
||||||
|
`ousast pre-push` with the spec's hook flags (defaults, or `--deadline 300` for the long-deadline arm), heap
|
||||||
|
1700 MB and the cluster JVM flags; pack `push.json`, `push.txt`, `engine.json`, `instrument.json` (files and
|
||||||
|
bytes in the checkout, bytes of each changed file, JVM wall time, hook exit code, step timings, image
|
||||||
|
digest); PUT as `.dat`. Any error PUTs a failure record naming the step and status, never URL text.
|
||||||
|
- `tests/test_unseen_replay_entry.py`: a local bare repository and a localhost presign stub; success record
|
||||||
|
fields; a missing commit, a failed fetch and a hook crash each give a failure record; zero bytes read and a
|
||||||
|
replay faster than JVM start are flagged `suspect_fast`.
|
||||||
|
- _Requirements: 3.1, 4.1, 4.2_
|
||||||
|
- [x] 5.3 Replay workload
|
||||||
|
- `benchmarks/unseen/replay.py`: builds `Item`s from a frozen slice and an arm (`default` or `long_deadline`);
|
||||||
|
the spec object carries the repository URL so names stay out of AX status and telemetry; task names
|
||||||
|
`ousast-engine-replay-<slug>-<digest>` with an opaque slug; the validator rejects a record without
|
||||||
|
`instrument.json`; records go to `~/ousast-results/unseen/<pool>/<slice>/<arm>/` and the store.
|
||||||
|
- Tests: no repository name in any task name or manifest; only the two presigned URLs in the env; input
|
||||||
|
digest changes with the arm's flags; `instrument_failure` items queued for one re-run.
|
||||||
|
- _Requirements: 3.1, 4.1, 4.2, 4.3_
|
||||||
|
- [ ] 5.4 Proof on kind, then kube-ax, and record 05 **(coordinator, network)**
|
||||||
|
- Build and push the `replay` target, pinned by digest. Five changes from this repository's own history (never
|
||||||
|
pool changes) on kind, then the same five on kube-ax, then on the VM lane.
|
||||||
|
- Exit: instrument fields show bytes read and realistic JVM time on every lane; quick-tier findings equal
|
||||||
|
across lanes.
|
||||||
|
- `benchmarks/measurements/<date>-unseen-05-replay-task/record.json`: image digest, per-lane timings, bytes,
|
||||||
|
exit codes, the quick-tier comparison.
|
||||||
|
- _Requirements: 4.1, 4.2, 4.5_
|
||||||
|
|
||||||
|
- [x] 6. Scorer (design 3, 4, 7.1, 7.3; increment 6)
|
||||||
|
- [x] 6.1 Coverage states
|
||||||
|
- `benchmarks/unseen/score.py`: each record gets one state (`instrument_failure`, `unanalysable`, `quick_only`,
|
||||||
|
`engine_covered`) from `instrument.json` and the artifact; `suspect_fast` goes to failure; a slice above 5%
|
||||||
|
`instrument_failure` after re-runs is refused.
|
||||||
|
- `tests/test_unseen_score.py` on synthetic records: every state; the refusal; a failure never counts as quiet
|
||||||
|
or missed.
|
||||||
|
- _Requirements: 3.3_
|
||||||
|
- [x] 6.2 Catch and false-alarm rules
|
||||||
|
- `score.py`: family match (engine family, quick-rule CWE through `taxonomy.py`), the location table of design
|
||||||
|
4.1, all three streams with the stream named per catch; false alarm on any finding of any stream; the
|
||||||
|
alerts-only rate.
|
||||||
|
- Tests: right family wrong place misses; `access_control` on the handler; `config_secrets` file scope; a
|
||||||
|
quick-rule-only catch; one advisory finding on an ordinary change is a false alarm.
|
||||||
|
- _Requirements: 3.2_
|
||||||
|
- [x] 6.3 Rates, intervals and breakdowns
|
||||||
|
- `score.py`: Wilson 95% intervals; repository-cluster bootstrap for false alarms; ICC; breakdowns per family,
|
||||||
|
method, `post_cutoff`, stream, coverage state and the confirmed subset.
|
||||||
|
- Tests: the n values of design section 3 (315, 483, 756, 62, 88, 97) reproduced; bootstrap seeded; ICC on a
|
||||||
|
synthetic clustered set.
|
||||||
|
- _Requirements: 2.3, 3.3_
|
||||||
|
- [x] 6.4 Budgets, paired tests, the gate and record 06
|
||||||
|
- `score.py`: per-budget thresholds (1%, 5%, 10%), labelled `exploratory` in-slice and `fixed` when supplied;
|
||||||
|
a binary arm's single operating point with the budgets it meets; exact two-sided McNemar for catch and for
|
||||||
|
false alarm; `gate(A, B)` returning `pass | regression | not_run` per design 7.3.
|
||||||
|
- Tests: thresholds on a synthetic score set; McNemar exact values; a binary arm with a significant
|
||||||
|
false-alarm increase gives `regression`; missing slice or excess failures give `not_run`.
|
||||||
|
- `benchmarks/measurements/<date>-unseen-06-scorer/record.json`: test counts and the digest of the scorer's
|
||||||
|
output on the committed synthetic fixture.
|
||||||
|
- _Requirements: 3.4, 5.4_
|
||||||
|
|
||||||
|
- [ ] 7. Baseline (design 4, 6; increment 7; G0 exit)
|
||||||
|
- [ ] 7.1 Frozen source and dry run **(coordinator, network)**
|
||||||
|
- Export HEAD to a frozen tree; build and push the `replay` image from it, pinned by digest.
|
||||||
|
- `benchmarks/push/python_overhead.py` run with the stub engine to size per-replay Python cost; dispatcher dry
|
||||||
|
run on slice 1 lists 1,300 items without launching.
|
||||||
|
- _Requirements: 5.1, 4.1_
|
||||||
|
- [ ] 7.2 Baseline replay of slice 1 **(coordinator, network)**
|
||||||
|
- Default arm, 1,300 replays over two AX lanes and the VM lane (about 9 hours); `instrument_failure` items
|
||||||
|
re-run once; the run stops for a cause above 5% failures.
|
||||||
|
- Observable: 1,300 records in `~/ousast-results/unseen/p1/1/default/` and the store, each with
|
||||||
|
`instrument.json`.
|
||||||
|
- _Requirements: 3.1, 4.3, 5.1_
|
||||||
|
- [ ] 7.3 Adjudication **(coordinator)**
|
||||||
|
- Flagged ordinary changes adjudicated against protocol-v2's standard (all if 30 or fewer, else every third);
|
||||||
|
the adjudicated share kept beside the false-alarm rate. ICC measured; above 0.05 noted as "next pool sizes
|
||||||
|
up".
|
||||||
|
- _Requirements: 3.2, 2.3_
|
||||||
|
- [ ] 7.4 Record 07 and the ledger **(coordinator)**
|
||||||
|
- `benchmarks/measurements/<date>-unseen-07-baseline/record.json`: `score.py` output (counts, rates, Wilson and
|
||||||
|
bootstrap intervals, coverage per state, per family, method, stream), freeze digest, image digest, commit.
|
||||||
|
`usage-p1.jsonl` row `baseline`.
|
||||||
|
- Observable: guard green over the record; G0's exit met.
|
||||||
|
- _Requirements: 5.1, 5.2, 3.3_
|
||||||
|
|
||||||
|
- [ ] 8. Arms on slice 1 (design 4.0, 5.4, 7.1-7.3; increment 8; G1)
|
||||||
|
- [ ] 8.1 Registered long-deadline arm
|
||||||
|
- `plane/experiments/` manifest for the long-deadline arm: hook flags with `--deadline 300`, budget 0,
|
||||||
|
`unseen: {pool: p1, slice: 1}`, committed before its first replay. `src/openultrasast/learn/experiments.py`
|
||||||
|
accepts `unseen` and a hook-flag arm.
|
||||||
|
- `tests/test_learn_experiments.py`: the manifest loads; an `unseen` block without pool or slice is refused.
|
||||||
|
- _Requirements: 3.4, 5.3_
|
||||||
|
- [ ] 8.2 `unseen` block and adoption gate in `analyse`
|
||||||
|
- `experiments.py` `analyse`: writes `unseen` into `result.json` (catch at each budget per arm, paired B - A,
|
||||||
|
McNemar, `gate`) from `score.py`; checks the usage ledger; adoption requires the fold rule and
|
||||||
|
`gate == pass`.
|
||||||
|
- Tests: `regression` and `not_run` each block adoption; an `informed` slice cannot qualify; result JSON shape.
|
||||||
|
- _Requirements: 5.2, 5.3, 5.4_
|
||||||
|
- [ ] 8.3 Engine-evidence arm
|
||||||
|
- `benchmarks/unseen/arms.py`: a score per change from `engine.json` (witness rows on head-side changed
|
||||||
|
functions), no model; plugs into the budget thresholds of 6.4.
|
||||||
|
- Tests on synthetic records: score monotone in witness strength; a change without engine coverage gets no
|
||||||
|
score and is reported, not counted quiet.
|
||||||
|
- _Requirements: 3.4_
|
||||||
|
- [ ] 8.4 Model-arm adapter
|
||||||
|
- `arms.py`: candidates from head-side changed functions of stored records, scored by the current decision
|
||||||
|
engine program (and the combiner when its program exists; else its row reads `not_built`); change score is the
|
||||||
|
maximum; metered client, dry run first, response cache, the manifest's `budget_usd` as the hard ceiling.
|
||||||
|
- Tests with a scripted client: dry-run estimate, ceiling stop, cache replay; no model call.
|
||||||
|
- _Requirements: 4.4, 3.4_
|
||||||
|
- [ ] 8.5 Long-deadline replay of slice 1 **(coordinator, network)**
|
||||||
|
- 1,300 replays of the `long_deadline` arm, bounded at about 345 s each (up to about 42 hours over three
|
||||||
|
lanes), with `STOP` and resume across sessions; failures re-run once.
|
||||||
|
- Observable: 1,300 records under `~/ousast-results/unseen/p1/1/long_deadline/`, JVM time in each.
|
||||||
|
- _Requirements: 3.1, 3.4, 4.3_
|
||||||
|
- [ ] 8.6 Model arms on slice 1 **(coordinator, network; paid: needs a budget go-ahead)**
|
||||||
|
- Dry run gives the candidate count; estimate at the injection slice's measured cost (about $0.008 per
|
||||||
|
candidate at k = 5); the maintainer sets the ceiling against the balance read just before. Then the decision
|
||||||
|
engine arm (and the combiner if built) on the VM.
|
||||||
|
- _Requirements: 4.4, 3.4_
|
||||||
|
- [ ] 8.7 Record 08: the arms table **(coordinator)**
|
||||||
|
- `benchmarks/measurements/<date>-unseen-08-arms/record.json`: per arm (hook 30 s, hook 300 s, engine evidence,
|
||||||
|
decision engine, combiner or `not_built`) catch at 1/5/10% budgets with Wilson intervals and realised
|
||||||
|
false-alarm rates, coverage per state, spend; `exploratory` rows in `usage-p1.jsonl`.
|
||||||
|
- Observable: one committed table of arms on slice 1; G1's table exists.
|
||||||
|
- _Requirements: 3.4, 5.2, 5.3_
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- 2026-10-04: Tasks 1.1–1.4 implemented offline and independently reviewed. Task 1.5 remains for the
|
||||||
|
coordinator; no networked measurement or commit was performed. Command and API contracts:
|
||||||
|
`benchmarks/unseen/README.md`. Offline evidence:
|
||||||
|
`benchmarks/measurements/2026-10-04-unseen-01-offline/record.json`.
|
||||||
|
- Population guard scope is preserved under the maintainer's hard rule: benchmarks/src/tests for
|
||||||
|
populations, plus plane for pools. Each directory exemption is reservation-specific, including private
|
||||||
|
pool manifests. Extending population scope to plane exposed 30 existing references; no historical
|
||||||
|
records were changed or exempted inside the existing population scope.
|
||||||
|
- S3 inventory opens through `open_store(read_only=True)` because ordinary store construction writes a
|
||||||
|
Select capability probe. Default store behavior is unchanged; inventory uses LIST/GET only.
|
||||||
|
- The freeze writer must match the canonical TOML digest encoding documented in the pool README and
|
||||||
|
exercised by `test_independent_population.py`; final pool discovery does not trust mutable status.
|
||||||
|
|
||||||
|
### Group 2 implementation evidence (2026-10-04)
|
||||||
|
|
||||||
|
Tasks 2.1–2.4 are implemented offline and independently reviewed. Task 2.5 is
|
||||||
|
`python -m benchmarks.unseen.draft --root "$PWD" --s3-store s3:// --seed 20260601`
|
||||||
|
with the documented PATH/PYTHONPATH and GH_TOKEN environment. No networked draw,
|
||||||
|
label check, group-2 measurement record, guard bisection, or commit was performed.
|
||||||
|
The group header remains pending its coordinator exits 2.5/2.6.
|
||||||
|
|
||||||
|
The command rebuilds the group-1 used set, resolves renamed/fork repositories,
|
||||||
|
uses disposable external blobless clones, re-reads licences at extracted heads,
|
||||||
|
and emits counts/digests only. The README documents injected clients, method bias,
|
||||||
|
the hard 300-repository floor, and an explicitly unmeasured resource estimate.
|
||||||
|
Independent review caught and fixed ambiguous parent-function absence and stale
|
||||||
|
candidate-404 handling; regression tests cover both.
|
||||||
|
|
||||||
|
Validation: requested pytest selection 103 passed, 1 skipped, exit 0 (population-test
|
||||||
|
output discarded); `ruff check .` exit 0; `ruff format --check .` exit 0 (411 files);
|
||||||
|
`mypy src/openultrasast` exit 0 (160 source files). The CLI `--help` smoke exited 0.
|
||||||
|
Source-read instrumentation printed 20,101 bytes for the extraction implementation.
|
||||||
|
|
||||||
|
### Groups 4–6 offline implementation (2026-10-04)
|
||||||
|
|
||||||
|
Tasks 4.1–4.3, 5.1–5.3 and 6.1–6.4 are implemented and independently reviewed.
|
||||||
|
Tasks 4.4 and 5.4 remain coordinator-owned network proofs; group headers 4 and 5 stay
|
||||||
|
pending those exits. No commit, network request, Docker, AX or kubectl command was run.
|
||||||
|
The sourcing job's draft.py, source.py, extract.py and journal.py are unchanged.
|
||||||
|
|
||||||
|
The shared dispatcher retains the engine adapter's historical questions.json object
|
||||||
|
name to preserve its unchanged assertions; replay uses spec.dat and result.dat.
|
||||||
|
The engine Dockerfile body is unchanged except its required AS engine alias; CI builds
|
||||||
|
engine and replay separately. S3Store's read_only class default preserves existing
|
||||||
|
constructor-bypassing transport fixtures; initialized read-only stores still refuse writes.
|
||||||
|
|
||||||
|
Exact coordinator commands and input contracts are in benchmarks/unseen/README.md,
|
||||||
|
including frozen TOML/private pointer verification, scoring joins, own-history proof
|
||||||
|
preparation and anonymous measurement-record generation. JVM wall time is the hook's
|
||||||
|
aggregate scan build/query timing and is labelled as such. Unsupported-only partitions
|
||||||
|
with explicit language_not_covered evidence do not pretend to have launched a JVM.
|
||||||
|
|
||||||
|
Record 06 contains the committed synthetic fixture and scorer-output digests. The
|
||||||
|
reserved-repository guard was run as a black box with both output streams discarded.
|
||||||
|
Independent review: APPROVED; all reported integration and coverage defects resolved.
|
||||||
|
|
||||||
|
Final verification: requested pytest command 279 passed, 8 skipped, exit 0; separate
|
||||||
|
`tests/test_ax_batch.py` 5 passed, exit 0; `ruff check .`, `ruff format --check .`,
|
||||||
|
`mypy src/openultrasast`, and the output-discarded reserved-repository guard all exit 0.
|
||||||
|
The own-history proof preparer and replay dry run read five items and exit 0.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# kube-ax operator constraints
|
||||||
|
|
||||||
|
Verified by the operator on 2026-10-05. These constraints apply across all workloads.
|
||||||
|
|
||||||
|
- Two workers, one task per worker, at most two tasks in flight cluster-wide. AX has no
|
||||||
|
queue: a third apply returns `no free workers available`. Treat this as back-pressure,
|
||||||
|
retry the same task/lane with 15–30 seconds of jitter until its deadline, and never
|
||||||
|
count it as a sandbox failure or trigger VM fallback. The host dispatcher shares a
|
||||||
|
two-slot reservation across workload instances and long-lived search executors.
|
||||||
|
- `/` and `/tmp` are RAM-backed and consume the worker's 3 GiB. `/workspace` is the only
|
||||||
|
disk-backed path, with about 2 GiB usable per task. Executor and verifier entrypoints
|
||||||
|
use `/workspace/tmp`, `/workspace/.npm`, `/workspace/.m2`, `/workspace/.composer`,
|
||||||
|
`/workspace/.pip`, and `/workspace/.gradle` through the respective tool environment
|
||||||
|
variables. Builds and extracted checkouts live under `/workspace` too. The default
|
||||||
|
aggregate workspace scratch guard is 2 GiB; `OUSAST_SCRATCH_BYTES` configures it.
|
||||||
|
Exceeding it returns `could_not_build`, reason `scratch limit`. The guard polls
|
||||||
|
during child execution and checks after exit; it is not an instantaneous filesystem
|
||||||
|
quota or a replacement for the worker memory limit.
|
||||||
|
- `ousast-engine-search-verify-*` tasks have no egress. The requested exception for
|
||||||
|
`files.because-security.com:443` is **pending**, not operationally verified. They
|
||||||
|
receive a presigned manifest for a gzip level-6 archive containing `checkout/`,
|
||||||
|
`products/`, and `spec.json`, built/exported by an executor. Compressed archives
|
||||||
|
retain the 1 GiB cap and travel in parts of at most 90 MB, each digest-checked
|
||||||
|
before assembly and extraction under `/workspace`. Only `VERIFY_INPUT_URL` and `RESULT_URL` cross the
|
||||||
|
verifier boundary. Verifiers reject build recipes and do not install dependencies
|
||||||
|
or fetch repositories. Executors finish and release their slots before verifier
|
||||||
|
repetitions start. Both task types must be provisioned without model credentials.
|
||||||
|
- All tasks share one public IP. Repository acquisition uses HTTPS git clone from
|
||||||
|
`github.com` or archives from `codeload.github.com`, never `api.github.com`. Host-side
|
||||||
|
metadata queries are a separate concern. Search tasks receive checkout archives;
|
||||||
|
executor tool guidance carries the same acquisition rule. Retain the 60-second
|
||||||
|
first-download retry for delayed task egress policy activation.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Operator notifications (Discord)
|
||||||
|
|
||||||
|
Recorded 2026-10-09 from the maintainer's direction. OpenUltraSAST notifies the operator on
|
||||||
|
Discord about the status and results of its own processes, and especially when a process is
|
||||||
|
**stuck or otherwise impaired**. This is an operator channel, not a developer or infrastructure
|
||||||
|
one.
|
||||||
|
|
||||||
|
## Audience
|
||||||
|
|
||||||
|
The **system operator** and the **AI operator** who run and oversee the tool. Not developers
|
||||||
|
debugging the code, and not an infrastructure on-call. The messages answer "is the work moving,
|
||||||
|
did it finish, what did it produce, is it stuck" for someone supervising a run, not "why does this
|
||||||
|
function throw".
|
||||||
|
|
||||||
|
## Scope: OpenUltraSAST processes and their results
|
||||||
|
|
||||||
|
Notify on the actual OpenUltraSAST workloads and what they produce:
|
||||||
|
|
||||||
|
- scans and pre-push checks (a completed check and its verdict/degradations),
|
||||||
|
- unseen-repo sourcing / draft, freeze, and replay / baseline runs,
|
||||||
|
- learning and evaluation runs (compile, evaluate, experiments) and their recorded results,
|
||||||
|
- fusion / adjudication and any long-running job.
|
||||||
|
|
||||||
|
**Not** k3s or ax cluster infrastructure. Cluster, node, and ax-task health is a separate
|
||||||
|
infrastructure concern monitored elsewhere; this channel does not carry it. The line is: a message
|
||||||
|
here is about an OpenUltraSAST process or result, never about the plane's plumbing.
|
||||||
|
|
||||||
|
## When to notify
|
||||||
|
|
||||||
|
- **Completion:** a process finished, with a one-line result summary (counts, status, digest).
|
||||||
|
- **Stuck or impaired (the important case):** a stall, a clean-but-short result
|
||||||
|
(`insufficient_*`), an abort or `instrument_failure`, a timeout or deadline hit, budget
|
||||||
|
exhaustion, or a resource limit (disk, memory). These @mention the operator so a supervised run
|
||||||
|
does not sit silently broken.
|
||||||
|
|
||||||
|
Advisory and low-volume by design: a start/finish and the impaired cases, not per-step chatter.
|
||||||
|
|
||||||
|
## Transport and secrets
|
||||||
|
|
||||||
|
A Discord webhook, with credentials referenced by name only and stored in gitignored `.env`, like
|
||||||
|
every other credential (`.env.example` lists the names, blank):
|
||||||
|
|
||||||
|
- `DISCORD_WEBHOOK_URL` — the operator channel webhook.
|
||||||
|
- `DISCORD_OPERATOR_ID` — the account to @mention on a stuck/impaired alert.
|
||||||
|
|
||||||
|
The webhook URL is a secret (anyone holding it can post to the channel): it never enters a tracked
|
||||||
|
file, a prompt, a log, or a commit, and it is rotated if exposed. Notifications are **off** when the
|
||||||
|
webhook is unset, so the tool runs the same with or without it.
|
||||||
|
|
||||||
|
## Content discipline
|
||||||
|
|
||||||
|
Messages carry status, counts, and digests only, under the same redaction as the hardening and
|
||||||
|
measurement-record conventions: no exploit payloads or request strings, no secrets, no customer
|
||||||
|
content, no repository contents. A notification is safe to read in a shared channel.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Policy recorded; the notifier (reading the env-named webhook, quiet when unset, honoring the
|
||||||
|
redaction above) is to be wired into the process entry points and may be specced separately. Until
|
||||||
|
then this steering states the intended behavior for any process that adds notifications.
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
# Overview — what this is, and what is actually true of it
|
# Overview — what this is, and what is actually true of it
|
||||||
|
|
||||||
## Current state, 2026-10-02
|
## Current state, 2026-10-04
|
||||||
|
|
||||||
|
Everything below this section is dated history; where it disagrees with this section, this section wins. The
|
||||||
|
direction lives in `roadmap.md`, section of 2026-10-04 (phases G0-G5).
|
||||||
|
|
||||||
|
- **Goal.** A pre-push safety net that works on repositories it has never seen. Two numbers per push decide it:
|
||||||
|
catch rate on real vulnerability-introducing changes, and false-alarm rate on ordinary changes. Neither has been
|
||||||
|
measured yet; the unseen-repository pool (G0, spec `unseen-repo-evaluation`) comes first.
|
||||||
|
- **Correction, 2026-10-04.** Infrastructure and engine work had drifted from that goal. It is now done only when
|
||||||
|
it removes a bottleneck for those measurements.
|
||||||
|
- **Engine.** Joern runs on the operator's kube-ax cluster as AX Tasks, with a VM fallback lane; kind stays the
|
||||||
|
development profile. It is almost blind to fixes: paths differ between sides in 2 of 105 both-asked pairs.
|
||||||
|
- **Decision engine.** Ranks but cannot block (exp-003 to exp-005 inconclusive; 710 fix pairs in memory). Fix
|
||||||
|
mechanisms is one G2 arm, paused after mining until the pool exists.
|
||||||
|
- **Hook.** `ousast pre-push` is operational and advisory on any repository; rollout stays NO-GO.
|
||||||
|
|
||||||
|
## State, 2026-10-02 (superseded by the section above)
|
||||||
|
|
||||||
Everything below this section is dated history; where it disagrees with this section, this section wins.
|
|
||||||
User-facing documentation: `README.md`, `docs/architecture.md`, `docs/evaluation.md`, `docs/decision-engine.md`,
|
User-facing documentation: `README.md`, `docs/architecture.md`, `docs/evaluation.md`, `docs/decision-engine.md`,
|
||||||
`ops/ax/README.md`.
|
`ops/ax/README.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,143 @@
|
|||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
|
## Roadmap to detection with the decision engine on kube-ax (2026-10-04)
|
||||||
|
|
||||||
|
This section supersedes the ordering below it. The older sections stay as history.
|
||||||
|
|
||||||
|
### Destination
|
||||||
|
|
||||||
|
On every push, from a hook or CI, OpenUltraSAST answers for the changed code. The answer is advisory for every
|
||||||
|
family, and BLOCK only for families whose precision has been proven on repositories the system never saw. The work
|
||||||
|
is split by what each place is good at:
|
||||||
|
|
||||||
|
- **kube-ax cluster** (operator k3s, Agent Substrate v0.3.0, AX v0.3.1, 2 nodes): the Joern engine, as AX Tasks.
|
||||||
|
No API keys and no store credentials in the cluster; tasks read and write through short-lived presigned links.
|
||||||
|
- **The VM or CI runner**: candidate selection, the decision engine's model calls (DeepSeek), the memory store,
|
||||||
|
and the fallback engine lane (2.5 GB heap) for pins that do not fit the cluster's 1.7 GB heap.
|
||||||
|
- **kind** stays the local development profile, with the same code and manifests.
|
||||||
|
|
||||||
|
### The question everything answers
|
||||||
|
|
||||||
|
Does the safety net work on repositories it has never seen? That is the purpose of the A/B experiments, and every
|
||||||
|
phase below is judged by it. On an unseen repository almost every push is an ordinary change, so two numbers decide
|
||||||
|
whether the safety net is useful, measured on the hook's advisory output exactly as a user would see it:
|
||||||
|
|
||||||
|
1. **Catch rate:** on real vulnerability-introducing changes in unseen repositories, how often it flags the change.
|
||||||
|
2. **False-alarm rate:** on ordinary changes in the same unseen repositories, how often it flags anything.
|
||||||
|
|
||||||
|
Within-pair AUC on known fix pairs answers neither. It stays a development signal, not the goal.
|
||||||
|
|
||||||
|
### Where we are (2026-10-04)
|
||||||
|
|
||||||
|
| Area | State | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Generalisation measured so far | The classifier ranks above chance on repository-held-out folds (within-pair AUC about 0.76) but cannot reach the block bar; the engine-based scan failed fresh populations (v1 1/11, v2 0/17); push-level catch and false-alarm rates on unseen repositories have never been measured | exp-003/004/005, `results-v1.json`, `results-v2.json` |
|
||||||
|
| Pre-push hook | Operational and advisory on any repository | `2026-10-02-pre-push-operational-1/record.json` |
|
||||||
|
| Labelled data | 710 complete real fix pairs | `2026-10-04-decision-engine-harvest-3/record.json` |
|
||||||
|
| Engine | Almost blind to fixes (paths differ between sides in 2 of 105 both-asked pairs) | `2026-10-04-joern-slice-leak-audit/record.json` |
|
||||||
|
| Infrastructure | Engine runs on kube-ax as AX Tasks with a VM fallback; kind for development | memory `kube-ax-cluster` |
|
||||||
|
|
||||||
|
Correction recorded 2026-10-04: the previous two days went mostly into infrastructure and engine internals that did
|
||||||
|
not move a generalisation number. From here, infrastructure and engine work is done only when it removes a
|
||||||
|
bottleneck for the measurements below.
|
||||||
|
|
||||||
|
### Where we are (2026-10-11)
|
||||||
|
|
||||||
|
The G0 measurement is now runnable: the unseen pool is **frozen** and the whole pipeline is hardened and
|
||||||
|
**bucket-first**. The baseline replay is the one remaining step, to run on kube.
|
||||||
|
|
||||||
|
| Area | State | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Unseen pool (G0.1) | **Frozen.** `pool-p1.toml` = 100 repos (slice 1); `freeze-p1.json` guard=0, dropped=0; deterministic (manifest `6bd2fa5d`). License 49 permissive / 51 private; methods 81 introducing / 19 last_touch. | `benchmarks/unseen/pool-p1.toml`; archived `ousast-corpus/unseen/20260601/pool/` |
|
||||||
|
| Labels | 100 machine-derived (SZZ), **unchecked**. Record 07 is a **machine-label baseline** (caveat stated); a human label pass is a later refinement, before any published claim. | `freeze-p1.json` label_check_counts |
|
||||||
|
| Harvest | Rebuilt to converge. Pure GitHub-API harvesting is a dead end for this benchmark (it needs the SZZ introducing commit + 12 stratified history negatives, not one commit). The fix: a full-objects clone (drop `--filter=blob:none`, which caused ~600 per-blob lazy fetches) + bucket-first durability + a get_repo cache that paid the 4,499-name used-set canonicalization once. Yield ~2.2%; **GitHub-rate-limit bound, not CPU**. | 95bed70, 857263d, 21aebe8, 7b711a6 |
|
||||||
|
| Persistence | **Bucket-first, not local disk.** `ousast-corpus` is the source of truth: enumeration, resolution, get_repo, and accept/reject decisions all stream to and resume from it. The host disk (~9-13 GB free) is the scarce resource; RustFS/S3 has the room, so run artifacts and **replay outputs go to RustFS too**, not `~/ousast-results` on local disk. | `storage-streaming.md`, `bank.py` |
|
||||||
|
| Replay image | Provenance pinned. CI `engine-image.yml` builds `ousast-replay-task` (harness with `/app/benchmarks/unseen/replay_entry.py`) + `ousast-engine-task` (Joern hook), publicly pullable by digest. The old `ousast-runner:dev` (248 MB ax-runner, no `/app/benchmarks`, no Joern) is the **wrong** image for replay. | CI run 38116281676 |
|
||||||
|
| Baseline (G0.2) | **Not yet.** Replay reads the frozen pool cleanly (dry-run = 1,300 changes, `freeze_digest` verified). Remaining: mirror CI's passing `replay` job wiring, and stand up the kube lane (`ax` binary + a public-HTTPS presign-capable store endpoint; GHCR pull already public). | CI `replay` job (passed) |
|
||||||
|
|
||||||
|
Lesson, recorded so it is not re-paid: sourcing and engine-image work cost real time. The durable takeaway now lives
|
||||||
|
in the pipeline and these docs — bucket-first persistence, pinned image provenance, a measured yield — so the next
|
||||||
|
pool or arm is cheap, not a rediscovery.
|
||||||
|
|
||||||
|
### Readiness plan (to G0 exit = record 07)
|
||||||
|
|
||||||
|
1. **Kube replay lane (infra, operator-owned):** install the `ax` binary; provide the store endpoint the AX lane
|
||||||
|
needs. The only thing settled in-repo is a code fact: `benchmarks/ax/batch.py` currently expects a public-HTTPS
|
||||||
|
DNS, presign-capable S3 endpoint. How that endpoint and the cluster's access to the store are provided is an
|
||||||
|
operator/AX decision, not an assumption to make here. GHCR pull of the images is verified public.
|
||||||
|
2. **Unblock the replay** by mirroring CI's passing `replay` job (replay-task + engine-task wired, store + network).
|
||||||
|
3. **Run slice-1 baseline on kube:** `--lane ax --arm default`, 1,300 changes, no model cost, resumable; outputs to
|
||||||
|
RustFS.
|
||||||
|
4. **Adjudicate (7.3) -> record 07 (7.4)** = the measured machine-label unseen baseline = **G0 exit**.
|
||||||
|
5. Then **G1 arms** (DeepSeek decision engine) on the frozen pool.
|
||||||
|
|
||||||
|
### Phases
|
||||||
|
|
||||||
|
**G0. The unseen-repository pool and the push-level metric** (new spec `unseen-repo-evaluation`)
|
||||||
|
|
||||||
|
1. A frozen pool of repositories never used for training, tuning, mining or harvesting, and not in any population.
|
||||||
|
Two kinds of change per repository: real vulnerability-introducing commits (the parent of a fix, post-cutoff where
|
||||||
|
possible) and ordinary commits from the same history.
|
||||||
|
2. The metric: replay each change through `ousast pre-push` (and the decision engine where an arm uses it) and
|
||||||
|
record catch rate and false-alarm rate, with confidence intervals, per family and overall.
|
||||||
|
3. Rotation: the pool is spent per decision, like the populations; a new slice is drawn for the next one.
|
||||||
|
|
||||||
|
Exit: the pool is frozen and the current production hook has a measured baseline on it.
|
||||||
|
|
||||||
|
**G1. Re-score the arms we have on the pool**
|
||||||
|
|
||||||
|
Current hook (quick rules + engine), decision-engine advisory (current programs), combiner, engine evidence. Each is
|
||||||
|
an arm with a registered decision rule, scored by catch rate at a fixed false-alarm budget.
|
||||||
|
|
||||||
|
Exit: one measured table of arms on unseen code; keep only what generalises.
|
||||||
|
|
||||||
|
**G2. Improve what moves the unseen-repo numbers**
|
||||||
|
|
||||||
|
Candidates, each admitted only if it improves the pool metric on a fresh slice: fix mechanisms in the engine
|
||||||
|
(approved plan, now one arm, not the spine), engine coverage and TypeScript, decision-engine prompt and retrieval
|
||||||
|
changes, more labelled pairs from more distinct repositories.
|
||||||
|
|
||||||
|
Active G2 workstreams (2026-10-11, opened while driving G0; progress tracked in their briefs):
|
||||||
|
|
||||||
|
- **Change-scoped engine analysis** (`.kiro/specs/engine-change-scoped-analysis/brief.md`). Live diagnosis on the
|
||||||
|
frozen pool showed the engine's "no finding" on unseen changes is usually *analysis-never-runs*: the engine ranks
|
||||||
|
repo-wide candidates and `deferred: deadline_exhausted` before analyzing anything (even at 300 s), and the ranked
|
||||||
|
sites are unrelated to the pushed change. Lever: seed analysis from the change's declarations + dataflow
|
||||||
|
neighbourhood (`rank.py`/`mapping.py`) so it runs within budget. Increment 0 (confirm CPG-build-vs-analysis cost
|
||||||
|
split) gates the rest. Scale measurement needs the kube lane.
|
||||||
|
- **Line-correspondence (done, necessary-not-sufficient).** `snapshot.py` discarded base↔head anchors on
|
||||||
|
non-globally-unique lines → `comparison_unknown` → suppressed findings; fixed to run-aware anchoring
|
||||||
|
(commit c39fbb8, 163 tests). Removes one cause; coverage still needs change-scoped analysis + the capability
|
||||||
|
registry (Task 8.3).
|
||||||
|
|
||||||
|
**G3. Decide BLOCK per family** on the pool's false-alarm rate and the pooled precision bound (Req 6.4).
|
||||||
|
|
||||||
|
**G4. Detection on push for any repository** (`plane-on-kubernetes` P4 work: `ousast plane scan --base --head`,
|
||||||
|
engine pins on kube-ax, model calls on the VM or CI), sized by what G1-G3 kept.
|
||||||
|
|
||||||
|
**G5. Qualification and rollout:** adopt, commit `prediction-v3.json`, spend population v3 once, advisory rollout,
|
||||||
|
BLOCK only for families that passed.
|
||||||
|
|
||||||
|
### Cross-cutting rules
|
||||||
|
|
||||||
|
- No hand-tuning per miss. Every detection change is a registered experiment or an admission by held-out
|
||||||
|
separation.
|
||||||
|
- Records carry counts, digests and pattern names, never repository names: the reserved-repository guard covers
|
||||||
|
every population.
|
||||||
|
- Instrument first: a zero from an unread input is a failed run (AGENTS.md).
|
||||||
|
- Cluster stability: sandbox deaths are retried once, then sent to the VM lane; the operator owns workers, egress
|
||||||
|
and Substrate.
|
||||||
|
|
||||||
|
### Spec map
|
||||||
|
|
||||||
|
| Spec | Role in this roadmap | State |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `unseen-repo-evaluation` (new) | G0, G1: the pool and the push-level metric | To specify |
|
||||||
|
| `learned-decision-engine` | G1-G3, G5; fix-mechanisms plan is one G2 arm | Implementation; experiments 3-5 recorded |
|
||||||
|
| `plane-on-kubernetes` | G4; the engine executors for kube-ax | Groups 1-2 done (kind); production path re-scoped to the operator's kube-ax (AX Tasks), groups 3-8 to re-plan |
|
||||||
|
| `pre-push-safety-net` | the hook replayed in G0-G1; M4-M5 in G5 | Operational increment 1 done; M4-M5 open |
|
||||||
|
| `ai-service-plane` | The plane the above runs on | Complete |
|
||||||
|
|
||||||
## Status, 2026-10-02
|
## Status, 2026-10-02
|
||||||
|
|
||||||
The milestone table below dates from 2026-09-14. Since then (`.kiro/specs/pre-push-safety-net/release-milestones.md`):
|
The milestone table below dates from 2026-09-14. Since then (`.kiro/specs/pre-push-safety-net/release-milestones.md`):
|
||||||
|
|||||||
@@ -142,3 +142,19 @@ promising but does not qualify security queries or hook latency. The contributor
|
|||||||
vendor/correspondence completeness vetoes, and query-to-operation identity as the next
|
vendor/correspondence completeness vetoes, and query-to-operation identity as the next
|
||||||
shared-contract work. Resolving a boundary requires evidence of its relevance and scope;
|
shared-contract work. Resolving a boundary requires evidence of its relevance and scope;
|
||||||
do not simply drop completeness checks to increase admissions. Rollout remains NO-GO.
|
do not simply drop completeness checks to increase admissions. Rollout remains NO-GO.
|
||||||
|
|
||||||
|
On 2026-10-07, the **search-with-proof** arm reached its first milestone: an autonomous,
|
||||||
|
execution-backed `demonstrated` finding end to end on the kube-ax cluster. On a real CVE
|
||||||
|
fix-pair (hanxi/xiaomusic, path traversal of the prefix-check-bypass class), the agent
|
||||||
|
found the vulnerability, authored a declarative demo, and the differential verifier
|
||||||
|
confirmed the owned effect on the vulnerable revision and its absence on the fixed one;
|
||||||
|
a blind search of the fixed revision produced zero false proofs. This is the proof-based
|
||||||
|
BLOCK mechanism the maintainer directed after classification hit a precision wall: a BLOCK
|
||||||
|
rests on a demonstrated finding with a direct execution trace, not a confidence score.
|
||||||
|
Scope note: this is feasibility-pilot evidence on a development-corpus pair (known location,
|
||||||
|
labelled family, runnable-project selection bias), not release readiness. The pilot exit
|
||||||
|
(≥5 demonstrations in one family with 0 false proofs, Req 6.2) and the ≥73-negative BLOCK
|
||||||
|
qualification (Req 6.3) remain open, and no capability is enabled. The decision/classification
|
||||||
|
engine is retained as a triage ranker (it could not prove 95% precision as a gatekeeper);
|
||||||
|
proof is the BLOCK gate. See `search-with-proof/HANDOFF.md` and
|
||||||
|
`benchmarks/measurements/2026-10-07-search-first-cluster-demonstration/`.
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Stream run artifacts to RustFS, one bucket per purpose
|
||||||
|
|
||||||
|
Recorded 2026-10-09 from the maintainer's direction. The host's local disk is the scarce
|
||||||
|
resource (it runs near full, and the draw stops itself at under 2 GB free). Runs should stream
|
||||||
|
their durable artifacts to RustFS instead of accumulating them on local disk, and should spread
|
||||||
|
them across **purpose-specific buckets** rather than one shared bucket. This relieves the disk and
|
||||||
|
simplifies runs: local state becomes mostly a scratch cache that can be rebuilt or resumed from the
|
||||||
|
object store.
|
||||||
|
|
||||||
|
## Why per-purpose buckets
|
||||||
|
|
||||||
|
One shared bucket (`sast-memory`) already bit us: an append-rewritten key on a versioned bucket
|
||||||
|
with no noncurrent-version expiry ballooned to >10 GB (see the `engine-queue` command-upload fix and
|
||||||
|
the lifecycle rules added 2026-10-09). Separate buckets let each kind of artifact carry the
|
||||||
|
lifecycle it actually needs, and keep a transient firehose from polluting durable data.
|
||||||
|
|
||||||
|
Proposed split (names illustrative; the admin creates them and grants the store key):
|
||||||
|
|
||||||
|
- `sast-memory` — the plane memory store (existing): facts, excerpts, embeddings, index. Durable,
|
||||||
|
versioned, with the noncurrent-version lifecycle now in place.
|
||||||
|
- `sast-pools` — frozen unseen pools and their provenance: `pool-pN.toml`, `freeze-pN.json`, and the
|
||||||
|
draw journal needed to resume/reproduce. Durable, versioned. This is also the "don't redo the
|
||||||
|
sourcing" archive.
|
||||||
|
- `sast-runs` (or `sast-journals`) — ephemeral run and queue state (engine-queue, replay results,
|
||||||
|
per-run journals). Aggressive lifecycle: short current expiry + 1-day noncurrent expiry, since it
|
||||||
|
is a transient firehose.
|
||||||
|
- optionally `sast-traces` — engine/replay traces, if kept.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
- **Stream, don't hoard.** Write a run's durable outputs to its bucket as they are produced; keep
|
||||||
|
only a working scratch (e.g. the live git clone, which cannot be streamed) on local disk, and let
|
||||||
|
the disk guard stay as the backstop.
|
||||||
|
- **Lifecycle per bucket**, tuned to the artifact: durable+versioned for pools/memory;
|
||||||
|
short+noncurrent-expiry for transient run/queue state. Never a versioned bucket without a
|
||||||
|
noncurrent-version rule.
|
||||||
|
- **Resume from the object store**, so a wiped or small local disk does not lose a long run.
|
||||||
|
- **Least privilege per bucket.** A run gets a key scoped to the bucket it writes; the store never
|
||||||
|
creates or configures a bucket (admin does that once), consistent with the current S3Store.
|
||||||
|
- Content discipline unchanged: no secrets, no exploit payloads; pool manifests keep the existing
|
||||||
|
public/private (licence) split.
|
||||||
|
|
||||||
|
## Foundation and dependency
|
||||||
|
|
||||||
|
`src/openultrasast/plane/memory.py` already speaks boto3 to RustFS (S3Store) with startup
|
||||||
|
verification of versioning/lifecycle/Select/tags; this direction extends that pattern to more
|
||||||
|
buckets rather than inventing a new transport. New buckets and their grants are an **operator
|
||||||
|
admin** action — the store credentials cannot create buckets or set lifecycle (confirmed by the
|
||||||
|
`AccessDenied` on the 2026-10-09 lifecycle apply).
|
||||||
|
|
||||||
|
## Default: RustFS over local disk
|
||||||
|
|
||||||
|
Local disk on the VM is the scarce resource (it runs near full; the draw stops itself under 2 GB free).
|
||||||
|
RustFS/S3 has far more room. So the default for any run's durable state and large artifacts is the bucket,
|
||||||
|
not local disk. Local files are a rebuildable scratch cache only (the live git clone, a within-process
|
||||||
|
journal). A long run must be resumable purely from the bucket, and its outputs — including replay result
|
||||||
|
records — are written to RustFS, not `~/ousast-results` on local disk.
|
||||||
|
|
||||||
|
## Status (implemented 2026-10-10/11)
|
||||||
|
|
||||||
|
Bucket-first persistence is live, not a future spec. The unseen draw streams every stage to the
|
||||||
|
`ousast-corpus` bucket (`http://10.0.0.54:9001`, creds in `.env` as `CORPUS_S3_*`; boto3 needs path-style +
|
||||||
|
SigV4) and resumes from it:
|
||||||
|
|
||||||
|
- `unseen/<seed>/decisions/<sha256(repo+fix)>.json` — accept/reject per repository (idempotent; content dedup).
|
||||||
|
- `unseen/<seed>/resolved/<sha256(ghsa)>.json` — per-advisory resolution.
|
||||||
|
- `unseen/<seed>/advisories/list.json` — the enumerated advisory list.
|
||||||
|
- `unseen/repos/<sha256(name)>.json` — get_repo cache, **not** seed-scoped (shared across seeds/runs), so the
|
||||||
|
thousands of used-set canonicalization lookups are paid once ever.
|
||||||
|
- `unseen/<seed>/pool/` — the frozen pool and its provenance (`pool-p1.toml`, `private-pool-p1.toml`,
|
||||||
|
`freeze-p1.json`, `bisect-p1.json`, `protocol-p1.md`): the durable "don't redo the sourcing" archive.
|
||||||
|
|
||||||
|
Implementation: `benchmarks/unseen/bank.py` (`CorpusBank`, `corpus_settings`). The old local journal is a
|
||||||
|
within-process cursor only; the bucket is the cross-run source of truth, so a restart (even after a code
|
||||||
|
change) resumes cheaply. Still to do and consistent with the default above: replay result records
|
||||||
|
(`replay.py --out`) currently default to `~/ousast-results/unseen/...` on local disk — point them at RustFS
|
||||||
|
for the slice-1 baseline and after. The AX (kube) lane already uses presigned object transfer, which is the
|
||||||
|
same principle. One in-repo code fact: `benchmarks/ax/batch.py` currently expects a public-HTTPS DNS,
|
||||||
|
presign-capable endpoint. How the store endpoint and the cluster's access to it are provided is operator/AX-owned
|
||||||
|
infra, not something this doc should presume.
|
||||||
+19
-42
@@ -1,26 +1,14 @@
|
|||||||
# OpenUltraSAST with its CPG engine, in one image.
|
# syntax=docker/dockerfile:1
|
||||||
#
|
# Supply digests from plane/task-base.digest; deliberately no floating fallback.
|
||||||
# The model layer's arbiter is Joern, and `openultrasast.cpg.backend` reaches it by subprocess. Shipping both
|
ARG TASK_BASE_TAG=base-unpublished
|
||||||
# in one image is what makes that simple: Joern is on PATH inside, so there is no container-to-container call,
|
ARG TASK_BASE_DIGEST
|
||||||
# no docker socket, no path translation between host and container, and no second code path in the backend to
|
FROM ghcr.io/norandom/ousast-task-base:${TASK_BASE_TAG}@${TASK_BASE_DIGEST} AS joern-build
|
||||||
# get wrong. The alternative -- tool and engine as sibling services -- needs the tool to exec into the other
|
ENV PATH="/opt/joern-cli:${PATH}"
|
||||||
# container, which means mounting the docker socket, and that is a large amount of privilege for a linter.
|
|
||||||
#
|
|
||||||
# The image is big (a JRE plus ~2 GB of Joern). That is the honest cost of bundling an engine, and it buys a
|
|
||||||
# contributor a working tool from `docker compose run` with nothing installed on their machine.
|
|
||||||
|
|
||||||
FROM eclipse-temurin:21-jre-noble
|
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0" \
|
|
||||||
org.opencontainers.image.source="https://github.com/norandom/OpenUltraSAST"
|
|
||||||
|
|
||||||
ARG JOERN_VERSION=v4.0.625
|
ARG JOERN_VERSION=v4.0.625
|
||||||
ARG ARCHIVE=joern-cli-linux-x86_64.zip
|
ARG ARCHIVE=joern-cli-linux-x86_64.zip
|
||||||
|
|
||||||
# php-cli is required by php2cpg, which drives PHP-Parser and shells out to a real interpreter. Without it a
|
|
||||||
# PHP tree fails with an opaque "Process exited with code 1" -- measured on this project, not assumed.
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends curl unzip \
|
||||||
python3 python3-venv python3-pip unzip curl ca-certificates git php-cli \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
@@ -32,39 +20,28 @@ RUN set -eux; \
|
|||||||
unzip -q "${ARCHIVE}" -d /opt; \
|
unzip -q "${ARCHIVE}" -d /opt; \
|
||||||
rm -f "${ARCHIVE}" "${ARCHIVE}.sha512" checksum.sha512; \
|
rm -f "${ARCHIVE}" "${ARCHIVE}.sha512" checksum.sha512; \
|
||||||
/opt/joern-cli/joern-parse --help > /dev/null
|
/opt/joern-cli/joern-parse --help > /dev/null
|
||||||
ENV PATH="/opt/joern-cli:${PATH}"
|
|
||||||
|
|
||||||
# This release ships the JS launcher with a .sh suffix; the backend's direct fallback uses
|
|
||||||
# the same extensionless command convention as the other frontends.
|
|
||||||
RUN if [ ! -e /opt/joern-cli/jssrc2cpg ] && [ -x /opt/joern-cli/jssrc2cpg.sh ]; then \
|
RUN if [ ! -e /opt/joern-cli/jssrc2cpg ] && [ -x /opt/joern-cli/jssrc2cpg.sh ]; then \
|
||||||
ln -s jssrc2cpg.sh /opt/joern-cli/jssrc2cpg; \
|
ln -s jssrc2cpg.sh /opt/joern-cli/jssrc2cpg; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COPY ops/frontend-retention/install.py /tmp/frontend-retention-install.py
|
COPY ops/frontend-retention/install.py /tmp/frontend-retention-install.py
|
||||||
RUN python3 /tmp/frontend-retention-install.py /opt/joern-cli \
|
RUN python3 /tmp/frontend-retention-install.py /opt/joern-cli \
|
||||||
&& rm /tmp/frontend-retention-install.py
|
&& rm /tmp/frontend-retention-install.py
|
||||||
|
|
||||||
WORKDIR /app
|
FROM ghcr.io/norandom/ousast-task-base:${TASK_BASE_TAG}@${TASK_BASE_DIGEST} AS engine
|
||||||
COPY pyproject.toml README.md ./
|
ENV PATH="/opt/joern-cli:${PATH}"
|
||||||
COPY src ./src
|
COPY --from=joern-build /opt/joern-cli /opt/joern-cli
|
||||||
COPY benchmarks ./benchmarks
|
# Create runtime identity before the application layers.
|
||||||
|
|
||||||
# The core install is zero-dependency by design; `semantic` adds the tree-sitter grammars the candidate
|
|
||||||
# enumerator uses. The LLM endpoint stays optional and is configured by environment, never baked in.
|
|
||||||
RUN python3 -m venv /venv \
|
|
||||||
&& /venv/bin/pip install --no-cache-dir -e ".[semantic]"
|
|
||||||
ENV PATH="/venv/bin:${PATH}"
|
|
||||||
|
|
||||||
# Analysis never needs root.
|
|
||||||
# Noble may already provide the ubuntu account at UID 1000. Reuse that identity rather than
|
|
||||||
# failing installation or creating a second account with the same UID.
|
|
||||||
RUN if getent passwd 1000 > /dev/null; then \
|
RUN if getent passwd 1000 > /dev/null; then \
|
||||||
usermod --login ousast --home /home/ousast --move-home "$(getent passwd 1000 | cut -d: -f1)"; \
|
usermod --login ousast --home /home/ousast --move-home "$(getent passwd 1000 | cut -d: -f1)"; \
|
||||||
else \
|
else useradd --create-home --uid 1000 ousast; fi
|
||||||
useradd --create-home --uid 1000 ousast; \
|
WORKDIR /app
|
||||||
fi \
|
COPY pyproject.toml README.md LICENSE ./
|
||||||
&& chown -R ousast /app
|
COPY src ./src
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
/venv/bin/pip install --no-deps --no-cache-dir .
|
||||||
|
COPY --chown=1000 benchmarks ./benchmarks
|
||||||
|
RUN chown -R ousast /app
|
||||||
USER ousast
|
USER ousast
|
||||||
|
|
||||||
ENTRYPOINT ["python", "-m", "openultrasast.cli"]
|
ENTRYPOINT ["python", "-m", "openultrasast.cli"]
|
||||||
CMD ["--help"]
|
CMD ["--help"]
|
||||||
|
|||||||
@@ -1,38 +1,71 @@
|
|||||||
# OpenUltraSAST
|
# OpenUltraSAST
|
||||||
|
|
||||||
OpenUltraSAST is an experimental static security analyser meant to work as a safety net on
|
OpenUltraSAST is an experimental static security analyser. It is meant to be a safety net on
|
||||||
repositories it has never seen: it scans a checkout, or the commits a `git push` would publish,
|
repositories it has never seen. It scans a checkout, or the commits a `git push` would publish. It
|
||||||
and separates what it can prove from what it merely suspects. A scan combines language-scoped
|
keeps what it can prove apart from what it only suspects.
|
||||||
pattern rules, a tree-sitter overlay, a Joern code property graph and, optionally, a model that is
|
|
||||||
asked only the question the graph cannot settle; every finding carries the evidence that earned
|
|
||||||
its place. A scan that could not look at something records a degradation instead of reporting a
|
|
||||||
clean result. `ousast` is the interface; model work over whole repositories runs on a separate
|
|
||||||
agentic plane (google/ax), and a learned decision engine is in development. Neither is needed
|
|
||||||
for a scan.
|
|
||||||
|
|
||||||
This file is the front door; each section links to its page of the documentation site (`docs/`,
|
A scan combines up to four parts:
|
||||||
`mkdocs.yml`; `uv sync --extra docs && uv run mkdocs serve`).
|
|
||||||
|
- language-scoped pattern rules;
|
||||||
|
- a tree-sitter overlay (a syntax tree of each file, built by the tree-sitter parser);
|
||||||
|
- a Joern code property graph (a graph of the program's syntax, control flow and data flow);
|
||||||
|
- optionally, a model. It is asked only the question the graph cannot settle.
|
||||||
|
|
||||||
|
Every finding carries the evidence that earned its place. When a scan could not look at
|
||||||
|
something, it records a degradation. It does not report a clean result.
|
||||||
|
|
||||||
|
Where a repository's application can be built and run, an experimental proof arm goes further. It
|
||||||
|
demonstrates a finding by executing it: the issue triggers on the vulnerable revision and not on
|
||||||
|
the fixed one, so a demonstrated finding is a reproducible fact, not a score. This arm is in pilot
|
||||||
|
and being qualified on repositories the tool has never seen; see [Status](#status).
|
||||||
|
|
||||||
|
`ousast` is the interface. Model work over whole repositories runs on a separate agentic plane
|
||||||
|
(google/ax). A learned decision engine is in development. A scan needs neither of them.
|
||||||
|
|
||||||
|
This file is the front door. Each section links to its page of the documentation site.
|
||||||
|
The site is published at <https://norandom.github.io/OpenUltraSAST/>. Its sources are in `docs/` and
|
||||||
|
`mkdocs.yml`; to preview it locally, run `uv sync --extra docs && uv run mkdocs serve`.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**v2.0.0, state as of 2026-10-02.** The pre-push safety net is **NO-GO** against every M4 gate:
|
**v2.0.1, state as of 2026-10-08.** The pre-push safety net is **not yet released**. Blocking on
|
||||||
no hook capability is enabled, and the two independent populations spent so far gave engine recall
|
the static analyser by hand is **NO-GO** against every M4 gate, and the project no longer tries to
|
||||||
1 of 11 and then 0 of 17. The learned decision engine ranks but may not block: BLOCK is offered for
|
reach them that way.
|
||||||
no family. The project has stopped trying to reach the gates by hand; every detection change now
|
|
||||||
has to win a controlled measurement on held-out repositories. The one page that states all of this
|
- No blocking hook capability is enabled; the pre-push hook is advisory only.
|
||||||
with the record behind each figure is [docs/where-we-stand.md](docs/where-we-stand.md); the
|
- Two independent populations (case sets frozen before any scan) have been spent on the static
|
||||||
release history is in [RELEASE_NOTES.md](RELEASE_NOTES.md).
|
engine: recall was 1 of 11, then 0 of 17. The learned decision engine ranks but blocks for no
|
||||||
|
family.
|
||||||
|
- Why blocking is hard: static abstract interpretation could not prove the precision a block
|
||||||
|
needs. So the current work is a **dynamic proof arm**. It builds and runs a repository's app and
|
||||||
|
demonstrates a finding by a differential: the issue triggers on the vulnerable revision and not
|
||||||
|
on the fixed one. A demonstrated finding is ground truth by execution, so it can earn a block
|
||||||
|
without calibration. Pilot demonstrations exist, including one executed on the cluster; the arm
|
||||||
|
is being qualified, not shipped.
|
||||||
|
- The release gate is a **measured** catch-rate and false-alarm number on repositories the tool
|
||||||
|
has never seen (the unseen-repo evaluation), produced rather than asserted. Until that number
|
||||||
|
exists, treat all output as advisory.
|
||||||
|
|
||||||
|
Every detection change has to win a controlled measurement on held-out repositories. One page
|
||||||
|
states where things stand, with the record behind each figure:
|
||||||
|
[docs/where-we-stand.md](docs/where-we-stand.md). For developers, the pre-push check and its
|
||||||
|
honest limits: [docs/pre-push-for-developers.md](docs/pre-push-for-developers.md); the
|
||||||
|
catch–prove–fix–re-verify loop: [docs/use-case-fix-and-reverify.md](docs/use-case-fix-and-reverify.md).
|
||||||
|
The release history is in [RELEASE_NOTES.md](RELEASE_NOTES.md).
|
||||||
|
|
||||||
## Install and first scan
|
## Install and first scan
|
||||||
|
|
||||||
Which tier you need: a deterministic scan of a checkout or a CI gate is tier 1; the Joern engine
|
Pick the tier you need:
|
||||||
(`standard`, `deep`) is tier 2; running model work over whole repositories on the plane is tier 3,
|
|
||||||
for maintainers; tier 4 does not exist yet.
|
1. Tier 1: a deterministic scan of a checkout, or a CI gate.
|
||||||
|
2. Tier 2: the Joern engine (`standard`, `deep`).
|
||||||
|
3. Tier 3, for maintainers: model work over whole repositories on the plane.
|
||||||
|
4. Tier 4 does not exist yet.
|
||||||
|
|
||||||
### Tier 1: quick scan, no Docker
|
### Tier 1: quick scan, no Docker
|
||||||
|
|
||||||
The core install depends only on PyYAML. From a checkout, `uv sync` (or `pip install .`) installs
|
The core install depends only on PyYAML. From a checkout, `uv sync` (or `pip install .`) installs
|
||||||
the `ousast` entry point; `uv sync --group dev` adds the test and lint tools.
|
the `ousast` entry point. `uv sync --group dev` adds the test and lint tools.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
uv sync
|
||||||
@@ -41,19 +74,41 @@ uv run ousast scan . --mode quick --fail-on verified # CI: exit 1 on any evide
|
|||||||
```
|
```
|
||||||
|
|
||||||
This runs the language-scoped pattern rules, entry-point reachability hints, ranking,
|
This runs the language-scoped pattern rules, entry-point reachability hints, ranking,
|
||||||
verification and scoring: deterministic, reproducible, no network, no keys. What it cannot do:
|
verification and scoring. It is deterministic and reproducible. It needs no network and no keys.
|
||||||
there is no engine, so nothing follows a value across statements or files, and the PHP quick rules
|
|
||||||
are a low-precision, in-sample development set (per-rule precision lower bounds from 0.03 to 1.0 on
|
The same install gives you the advisory pre-push hook for any repository. It runs the quick rules
|
||||||
the real pins, `benchmarks/measurements/2026-09-30-php-quick-rules/measurement.json`). The optional
|
on the lines a push changes, names every check it could not run, and never blocks a push:
|
||||||
`semantic` extra (`uv sync --extra semantic`) installs the tree-sitter grammars for the overlay.
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/your/repository && ousast pre-push install # remove: ousast pre-push uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
What it cannot do:
|
||||||
|
|
||||||
|
- There is no engine. Nothing follows a value across statements or files.
|
||||||
|
- The PHP quick rules are a low-precision, in-sample development set. Per-rule precision lower
|
||||||
|
bounds run from 0.03 to 1.0 on the real pins
|
||||||
|
(`benchmarks/measurements/2026-09-30-php-quick-rules/measurement.json`).
|
||||||
|
|
||||||
|
The optional `semantic` extra (`uv sync --extra semantic`) installs the tree-sitter grammars for
|
||||||
|
the overlay.
|
||||||
|
|
||||||
### Tier 2: standard and deep scans (Docker)
|
### Tier 2: standard and deep scans (Docker)
|
||||||
|
|
||||||
`standard` and `deep` need Joern. The Docker image `openultrasast:dev` ships the tool, Joern
|
`standard` and `deep` need Joern. The Docker image ships three things in one image: the tool,
|
||||||
v4.0.625 and `php-cli` (needed by the PHP frontend) in one image; it is built locally from the
|
Joern v4.0.625 and `php-cli` (the PHP frontend needs it). Each release publishes it to GHCR. It is
|
||||||
`Dockerfile` (no published image yet), and it is big: a JRE plus about 2 GB of Joern. The shell
|
big: a JRE plus about 2 GB of Joern. Pull it instead of building, and tag it with the name the
|
||||||
wrapper builds it on first use and then runs `ousast` in the container with your directory mounted
|
shell wrapper uses:
|
||||||
read-only and the container network off:
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/norandom/openultrasast:2.0.1
|
||||||
|
docker tag ghcr.io/norandom/openultrasast:2.0.1 openultrasast:dev # or build: docker compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
The shell wrapper runs `ousast` in the container. It builds the image only if `openultrasast:dev`
|
||||||
|
is missing. Your directory is mounted read-only and the container network is off. For the pre-push
|
||||||
|
hook, use `ousast pre-push install --docker`: Git cannot call a shell function, so it installs the
|
||||||
|
script `ousast-docker` beside the hook.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source /path/to/OpenUltraSAST/ops/shell/ousast.sh # PowerShell: ops/shell/ousast.ps1
|
source /path/to/OpenUltraSAST/ops/shell/ousast.sh # PowerShell: ops/shell/ousast.ps1
|
||||||
@@ -62,90 +117,132 @@ ousast-with-judge scan . --mode standard # network on, the model is a
|
|||||||
```
|
```
|
||||||
|
|
||||||
`docker compose build` and `TARGET=/path/to/repo docker compose run --rm ousast scan /target` do
|
`docker compose build` and `TARGET=/path/to/repo docker compose run --rm ousast scan /target` do
|
||||||
the same without the wrapper. `deep` adds REGRESS, which runs each reproduction snippet with
|
the same without the wrapper.
|
||||||
`docker run` from wherever the scan runs; the engine container has no Docker socket, so run `deep`
|
|
||||||
from a host install with Joern on the PATH (`pyinfra @local ops/joern.py`, pinned and checksum-
|
`deep` adds REGRESS. REGRESS runs each reproduction snippet with `docker run` from wherever the
|
||||||
verified) and a working `docker`, or accept the recorded `sandbox_unavailable` degradation.
|
scan runs. The engine container has no Docker socket. So run `deep` in one of two ways:
|
||||||
|
|
||||||
|
- from a host install with Joern on the PATH (`pyinfra @local ops/joern.py`, pinned and checksum-
|
||||||
|
verified) and a working `docker`;
|
||||||
|
- or accept the recorded `sandbox_unavailable` degradation.
|
||||||
|
|
||||||
Details, including the host install, are in [ops/README.md](ops/README.md).
|
Details, including the host install, are in [ops/README.md](ops/README.md).
|
||||||
|
|
||||||
Keys and store settings live in `.env` in the working directory (gitignored; `.env.example` lists
|
Keys and store settings live in `.env` in the working directory. The file is gitignored;
|
||||||
the names). `DEEPSEEK_API_KEY` serves every LLM call; `OPENROUTER_API_KEY` is used for embeddings
|
`.env.example` lists the names.
|
||||||
only (`ousast learn memory embed`). `.env` never overrides a variable already exported in your
|
|
||||||
shell, so `unset` a stale export when a key change seems to have no effect. The plane's memory
|
- `DEEPSEEK_API_KEY` serves every LLM call.
|
||||||
store defaults to a local file store under `~/ousast-results/plane/memory`; an S3-compatible bucket
|
- `OPENROUTER_API_KEY` is used for embeddings only (`ousast learn memory embed`).
|
||||||
is optional (`uv sync --extra s3`, then `OUSAST_MEMORY=s3://<bucket>` with `S3_ENDPOINT` and the
|
- `.env` never overrides a variable already exported in your shell. If a key change seems to have
|
||||||
AWS credentials in `.env`). Nothing in a scan needs it.
|
no effect, `unset` the stale export.
|
||||||
|
- The plane's memory store defaults to a local file store under `~/ousast-results/plane/memory`.
|
||||||
|
- An S3-compatible bucket is optional: `uv sync --extra s3`, then `OUSAST_MEMORY=s3://<bucket>`
|
||||||
|
with `S3_ENDPOINT` and the AWS credentials in `.env`. Nothing in a scan needs it.
|
||||||
|
|
||||||
### Tier 3: the plane on ax (maintainers)
|
### Tier 3: the plane on ax (maintainers)
|
||||||
|
|
||||||
None of this is needed for tier 1 or 2. Today's profile is a single-node kind cluster on the
|
Tier 1 and 2 need none of this. Today's profile is a single-node kind cluster on the maintainer's
|
||||||
maintainer's host. `ops/ax/up.sh` needs `docker`, `go`, `kubectl`, `kind`, `ko` and `ax` on the
|
host.
|
||||||
PATH (it refuses to start when one is missing) and builds Agent Substrate and ax from their
|
|
||||||
checkouts under `~/.cache/ousast/ax-src/`. Measured idle footprint with two workers: the kind node
|
- `ops/ax/up.sh` needs `docker`, `go`, `kubectl`, `kind`, `ko` and `ax` on the PATH. It refuses to
|
||||||
takes 1.7 GiB of memory (27% of the 7.7 GiB host) and 4.9 GB of disk for images, with several GB
|
start when one is missing.
|
||||||
more of Go caches during the build; the worker pool is sized for that 7 GB host. The verify and
|
- It builds Agent Substrate and ax from their checkouts under `~/.cache/ousast/ax-src/`.
|
||||||
roles tasks need `DEEPSEEK_API_KEY`, and memory lives in the local file store or the S3 store
|
- Measured idle footprint with two workers: the kind node takes 1.7 GiB of memory (27% of the
|
||||||
(RustFS is the tested server). Bring-up, doctor and the smoke Run: [ops/ax/README.md](ops/ax/README.md).
|
7.7 GiB host) and 4.9 GB of disk for images. The build uses several GB more for Go caches.
|
||||||
|
- The worker pool is sized for that 7 GB host.
|
||||||
|
- The verify and roles tasks need `DEEPSEEK_API_KEY`.
|
||||||
|
- Memory lives in the local file store or the S3 store. RustFS is the tested server.
|
||||||
|
|
||||||
|
Bring-up, doctor and the smoke Run: [ops/ax/README.md](ops/ax/README.md).
|
||||||
|
|
||||||
### Tier 4: Kubernetes (k3s), planned
|
### Tier 4: Kubernetes (k3s), planned
|
||||||
|
|
||||||
No deployment to any cluster other than that kind cluster has been made. The target is two
|
Nothing has been deployed to any cluster other than that kind cluster. The target is two profiles
|
||||||
profiles of one code base, `kind` locally and `k3s` in production, where the same `ousast plane run`
|
of one code base: `kind` locally and `k3s` in production. The same `ousast plane run` would
|
||||||
submits a Run in a remote execution mode: it needs a kubeconfig context or the cluster's ingress
|
submit a Run in a remote execution mode. It needs:
|
||||||
(and an `imagePullSecret` on the worker ServiceAccount for a private package), runner and engine
|
|
||||||
images published to GHCR and pinned by digest, and the S3 store as the artifact path, with no
|
- a kubeconfig context or the cluster's ingress (and an `imagePullSecret` on the worker
|
||||||
receiver on a laptop. The plan, and the code changes it needs with file paths, are in
|
ServiceAccount for a private package);
|
||||||
|
- runner and engine images published to GHCR and pinned by digest;
|
||||||
|
- the S3 store as the artifact path, with no receiver on a laptop.
|
||||||
|
|
||||||
|
The plan, and the code changes it needs with file paths, are in
|
||||||
[docs/deployment.md](docs/deployment.md#production-topology-ax-on-a-kubernetes-cluster).
|
[docs/deployment.md](docs/deployment.md#production-topology-ax-on-a-kubernetes-cluster).
|
||||||
|
|
||||||
## Scan modes
|
## Scan modes
|
||||||
|
|
||||||
`quick` runs the pattern rules, hints, ranking, verification and score and needs nothing.
|
There are three modes. Each adds to the one before.
|
||||||
`standard` adds the MAP stage: complexity map, semantic overlay, authorization obligations and the
|
|
||||||
Joern model layer, which builds one code property graph per repository and decides taint,
|
- `quick` runs the pattern rules, hints, ranking, verification and score. It needs nothing.
|
||||||
guard-dominance and configuration questions on it; a model is asked only the residual question
|
- `standard` adds the MAP stage: complexity map, semantic overlay, authorization obligations and
|
||||||
(without a key it is skipped and only the graph's entailed findings are reported). `deep` adds
|
the Joern model layer. The model layer builds one code property graph per repository. It decides
|
||||||
REGRESS: promoted candidates are loaded by a small snippet in a Docker sandbox and get a
|
taint, guard-dominance and configuration questions on that graph. A model is asked only the
|
||||||
`triggerable` or `not_triggerable` verdict. A missing engine is a recorded degradation
|
residual question. Without a key, that question is skipped and only the graph's entailed
|
||||||
(`cpg_unavailable`, `sandbox_unavailable`), never a clean result.
|
findings are reported.
|
||||||
|
- `deep` adds REGRESS. Promoted candidates are loaded by a small snippet in a Docker sandbox. Each
|
||||||
|
gets a `triggerable` or `not_triggerable` verdict.
|
||||||
|
|
||||||
|
A missing engine is a recorded degradation (`cpg_unavailable`, `sandbox_unavailable`), never a
|
||||||
|
clean result.
|
||||||
|
|
||||||
|
`--fail-on never|findings|verified|worth-fixing` sets the exit code. `--config openultrasast.toml`
|
||||||
|
loads settings. Every run writes these files under `<target>/.openultrasast/runs/<scan-id>/`:
|
||||||
|
`manifest.json`, `findings.json`, `verification.json`, `score.json`, `report.md`, `report.sarif`
|
||||||
|
and `trace/events.jsonl`.
|
||||||
|
|
||||||
|
`ousast pre-push` (experimental) analyses the commits a push would publish against their base.
|
||||||
|
First, the quick rules run on the changed lines. Then the engine compares head with base, and only
|
||||||
|
new or worsened defects become candidates. It is advisory by default, and it prints one line per
|
||||||
|
check it could not run. Its capability registry is empty, so it emits no normal alert today. An
|
||||||
|
engine finding the push introduced is shown as advisory. Install, settings, skip reasons and a CI
|
||||||
|
recipe: [docs/scanning.md](docs/scanning.md#first-run-install-the-hook-on-any-repository).
|
||||||
|
|
||||||
`--fail-on never|findings|verified|worth-fixing` sets the exit code and `--config openultrasast.toml`
|
|
||||||
loads settings. Every run writes `manifest.json`, `findings.json`, `verification.json`,
|
|
||||||
`score.json`, `report.md`, `report.sarif` and `trace/events.jsonl` under
|
|
||||||
`<target>/.openultrasast/runs/<scan-id>/`. `ousast pre-push` (experimental) analyses the commits a
|
|
||||||
push would publish against their base so that only new or worsened defects are candidates; it is
|
|
||||||
advisory by default and its capability registry is empty, so no normal alert is emitted today.
|
|
||||||
Flows and the per-language rule table: [docs/scanning.md](docs/scanning.md).
|
Flows and the per-language rule table: [docs/scanning.md](docs/scanning.md).
|
||||||
|
|
||||||
## How detection works
|
## How detection works
|
||||||
|
|
||||||
Three representations, from cheap to expensive: text (regex quick rules, one statement at a time),
|
The tool uses three representations of code, from cheap to expensive:
|
||||||
tree (the tree-sitter CST and an IR per function, for the overlay and the obligations) and graph
|
|
||||||
(the Joern CPG, the only one that follows a value across statements, calls and files). Taint runs
|
|
||||||
as fact tables, queries and verdicts on the graph; absence bugs (an operation that should have
|
|
||||||
been guarded) are found as obligations, dominance and configuration census; a model answers only
|
|
||||||
the residual question. [docs/detection-techniques.md](docs/detection-techniques.md) explains each
|
|
||||||
technique with the code it comes from and what it misses;
|
|
||||||
[docs/architecture.md](docs/architecture.md) gives the stage order, the evidence ladder (a state
|
|
||||||
machine, not a label a model may assign), the central CWE policy and the project score, and the
|
|
||||||
self-improving loops (`ousast improve`, with `--memory` for proposals from plane rows).
|
|
||||||
|
|
||||||
Quick rules cover C/C++, Python, JavaScript/TypeScript, Java with Groovy templates, and PHP, and
|
1. Text: regex quick rules, one statement at a time.
|
||||||
run only against files of their language. The detection gate (`uv run python -m openultrasast.gate`)
|
2. Tree: the tree-sitter CST (concrete syntax tree) and an IR (intermediate representation) per
|
||||||
enforces at least 90% recall and under 10% false positives per language on the bundled cheat-sheet
|
function. The overlay and the obligations use it.
|
||||||
corpora; on this tree it reports 93.62% recall (44/47) and 0.0% false positives. Those corpora show
|
3. Graph: the Joern CPG. It is the only one that follows a value across statements, calls and
|
||||||
that a rule change did not break a known detection; they are not a recall estimate on real code,
|
files.
|
||||||
and PHP is not in the gate. Framework knowledge (WordPress, Flask, Django, Express, Spring) is
|
|
||||||
tagged as an optional prior in `src/openultrasast/ruleset/frameworks.toml`, so its contribution can
|
Taint runs as fact tables, queries and verdicts on the graph. Absence bugs are operations that
|
||||||
be measured and switched off.
|
should have been guarded. They are found as obligations, dominance and configuration census. A
|
||||||
|
model answers only the residual question.
|
||||||
|
|
||||||
|
- [docs/detection-techniques.md](docs/detection-techniques.md) explains each technique, with the
|
||||||
|
code it comes from and what it misses.
|
||||||
|
- [docs/architecture.md](docs/architecture.md) gives the stage order and the evidence ladder (a
|
||||||
|
state machine, not a label a model may assign). It also covers the central CWE policy, the
|
||||||
|
project score and the self-improving loops (`ousast improve`, with `--memory` for proposals from
|
||||||
|
plane rows).
|
||||||
|
|
||||||
|
Quick rules cover C/C++, Python, JavaScript/TypeScript, Java with Groovy templates, and PHP. Each
|
||||||
|
rule runs only against files of its language.
|
||||||
|
|
||||||
|
The detection gate (`uv run python -m openultrasast.gate`) enforces at least 90% recall and under
|
||||||
|
10% false positives per language on the bundled cheat-sheet corpora. On this tree it reports
|
||||||
|
93.62% recall (44/47) and 0.0% false positives. Those corpora show that a rule change did not
|
||||||
|
break a known detection. They are not a recall estimate on real code, and PHP is not in the gate.
|
||||||
|
|
||||||
|
Framework knowledge (WordPress, Flask, Django, Express, Spring) is tagged as an optional prior in
|
||||||
|
`src/openultrasast/ruleset/frameworks.toml`. So its contribution can be measured and switched off.
|
||||||
|
|
||||||
## The plane on ax
|
## The plane on ax
|
||||||
|
|
||||||
Model-driven work over whole repositories runs as a Run manifest, a DAG of ax Tasks, each in its
|
Model-driven work over whole repositories runs as a Run manifest. A Run is a DAG of ax Tasks. Each
|
||||||
own gVisor actor on Agent Substrate. ax is the only agentic executor; there is no local
|
Task runs in its own gVisor actor (an isolated sandbox) on Agent Substrate. ax is the only agentic
|
||||||
subprocess path. Each Task binds at most one Model and its own `usd`/`calls` budget, egress is
|
executor; there is no local subprocess path.
|
||||||
deny-by-default per Task (hostnames only), and the provider key travels only in the Task's start
|
|
||||||
request, never in a manifest or an image. Measured on the first increment: the plane agreed on 16
|
- Each Task binds at most one Model and its own `usd`/`calls` budget.
|
||||||
of 20 declared sites at $0.0205 per candidate.
|
- Egress is deny-by-default per Task, by hostname only.
|
||||||
|
- The provider key travels only in the Task's start request, never in a manifest or an image.
|
||||||
|
|
||||||
|
Measured on the first increment: the plane agreed on 16 of 20 declared sites at $0.0205 per
|
||||||
|
candidate.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run ousast plane doctor # kind, Agent Substrate, ax controller, runner image
|
uv run ousast plane doctor # kind, Agent Substrate, ax controller, runner image
|
||||||
@@ -155,42 +252,61 @@ uv run ousast plane remember validation-46 # ingest the run's rows
|
|||||||
```
|
```
|
||||||
|
|
||||||
`workspaces`, `harvest`, `alerts-engine` and `memory-normalise` generate Runs, their inputs and
|
`workspaces`, `harvest`, `alerts-engine` and `memory-normalise` generate Runs, their inputs and
|
||||||
the store's rewrites. The flow and the task catalogue: [docs/plane.md](docs/plane.md); what runs
|
the store's rewrites. The flow and the task catalogue: [docs/plane.md](docs/plane.md). What runs
|
||||||
where today and what a separate cluster would take: [docs/deployment.md](docs/deployment.md).
|
where today, and what a separate cluster would take: [docs/deployment.md](docs/deployment.md).
|
||||||
|
|
||||||
HarnessX, the earlier agentic extra, was retired 2026-09-30: a `[harnessx]` section is ignored
|
HarnessX, the earlier agentic extra, was retired 2026-09-30. A `[harnessx]` section is ignored
|
||||||
with one warning, and `[models] verifier`, `[fusion] panel_model` and `[fusion] decider_model` fail
|
with one warning. `[models] verifier`, `[fusion] panel_model` and `[fusion] decider_model` fail and
|
||||||
naming the plane replacement, as do config keys nothing ever read. Fusion is deterministic.
|
name the plane replacement. Config keys that nothing ever read fail the same way. Fusion is
|
||||||
|
deterministic.
|
||||||
|
|
||||||
## Memory
|
## Memory
|
||||||
|
|
||||||
One store, keyed by repository and pin, holds what plane runs and the decision engine learned:
|
One store holds what plane runs and the decision engine learned. It is keyed by repository and pin.
|
||||||
rows (facts, verdicts, unit costs, alerts, coverage, feature records) and content-addressed blobs
|
It holds two kinds of data:
|
||||||
(excerpts, embeddings, cached responses, compiled programs). `OUSAST_MEMORY` selects the backend:
|
|
||||||
a local file store, or `s3://<bucket>` on any S3-compatible server with versioning, lifecycle
|
- rows: facts, verdicts, unit costs, alerts, coverage and feature records;
|
||||||
rules, object tags and S3 Select; RustFS is the tested server. An admin sets the bucket up once;
|
- content-addressed blobs: excerpts, embeddings, cached responses and compiled programs.
|
||||||
the store verifies it at every start and refuses to run, naming each missing piece, rather than
|
|
||||||
fall back. Layout and the read and write paths: [docs/memory.md](docs/memory.md); the bucket
|
`OUSAST_MEMORY` selects the backend. It is either a local file store or `s3://<bucket>` on any
|
||||||
policy and admin steps: [docs/rustfs.md](docs/rustfs.md); how stored rows are meant to turn into
|
S3-compatible server. The S3 server must support versioning, lifecycle rules, object tags and S3
|
||||||
better decisions: [docs/memory-and-detection.md](docs/memory-and-detection.md).
|
Select. RustFS is the tested server.
|
||||||
|
|
||||||
|
An admin sets the bucket up once. The store verifies it at every start. If anything is missing, it
|
||||||
|
refuses to run and names each missing piece. It does not fall back.
|
||||||
|
|
||||||
|
- Layout and the read and write paths: [docs/memory.md](docs/memory.md).
|
||||||
|
- The bucket policy and admin steps: [docs/rustfs.md](docs/rustfs.md).
|
||||||
|
- How stored rows are meant to turn into better decisions:
|
||||||
|
[docs/memory-and-detection.md](docs/memory-and-detection.md).
|
||||||
|
|
||||||
## Decision engine and evaluation
|
## Decision engine and evaluation
|
||||||
|
|
||||||
A learned decision engine is being built to replace hand-tuned detection edits: every instrument
|
A learned decision engine is being built. It is meant to replace hand-tuned detection edits. It
|
||||||
produces signals, never verdicts; labels come from ground truth only; a compiled AI classifier with
|
follows three rules:
|
||||||
local memory turns retrieved similar cases and the candidate's signals into a calibrated
|
|
||||||
probability with BLOCK and ADVISORY operating points. It is **in development, not adopted**: no
|
|
||||||
scan, pre-push check or report uses it. Measured on six families, on repositories the compiled
|
|
||||||
program was not shown, the pooled AUC of its score runs from 0.77 (injection) to 0.96 (access
|
|
||||||
control); the stricter within-pair AUC is 0.72 to 0.93; BLOCK is offered for no family
|
|
||||||
(`benchmarks/measurements/2026-10-02-decision-engine-slice-2/record.json`). Maintainer surface:
|
|
||||||
`ousast learn labels|memory|compile|evaluate|curve|experiment|audit-leaks`. Design, slices and
|
|
||||||
the A/B protocol: [docs/decision-engine.md](docs/decision-engine.md).
|
|
||||||
|
|
||||||
How anything is measured on code the tool was not tuned on, the pair corpora, the independent
|
- Every instrument produces signals, never verdicts.
|
||||||
populations and the M4 gates, and the fold discipline that keeps learned decisions out of their
|
- Labels come from ground truth only.
|
||||||
own training data: [docs/evaluation.md](docs/evaluation.md). Population v3 (PHP, 15 cases) is
|
- A compiled AI classifier with local memory turns retrieved similar cases and the candidate's
|
||||||
frozen as the one-time final check and nothing in the tree may read it.
|
signals into a calibrated probability. It has BLOCK and ADVISORY operating points.
|
||||||
|
|
||||||
|
It is **in development, not adopted**. No scan, pre-push check or report uses it.
|
||||||
|
|
||||||
|
It was measured on six families, on repositories the compiled program was not shown:
|
||||||
|
|
||||||
|
- The pooled AUC (area under the ROC curve) of its score runs from 0.77 (injection) to 0.96
|
||||||
|
(access control).
|
||||||
|
- The stricter within-pair AUC is 0.72 to 0.93 across the same families.
|
||||||
|
- BLOCK is offered for no family
|
||||||
|
(`benchmarks/measurements/2026-10-02-decision-engine-slice-2/record.json`).
|
||||||
|
|
||||||
|
Maintainer surface: `ousast learn labels|memory|compile|evaluate|curve|experiment|audit-leaks`.
|
||||||
|
Design, slices and the A/B protocol: [docs/decision-engine.md](docs/decision-engine.md).
|
||||||
|
|
||||||
|
[docs/evaluation.md](docs/evaluation.md) explains how anything is measured on code the tool was not
|
||||||
|
tuned on. It covers the pair corpora, the independent populations and the M4 gates. It also covers
|
||||||
|
the fold discipline that keeps learned decisions out of their own training data. Population v3
|
||||||
|
(PHP, 15 cases) is frozen as the one-time final check. Nothing in the tree may read it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run ousast benchmark benchmarks/manifests/python-vulnerable.toml --mode quick
|
uv run ousast benchmark benchmarks/manifests/python-vulnerable.toml --mode quick
|
||||||
@@ -201,19 +317,26 @@ uv run ousast improve benchmarks/manifests/java-spring-boot-vulnerable.toml --dr
|
|||||||
|
|
||||||
## Token economics
|
## Token economics
|
||||||
|
|
||||||
Model tokens are the one running cost. At runtime the plane routes them: one Model and one budget
|
Model tokens are the one running cost. At runtime the plane routes them:
|
||||||
per Task, a metered client that refuses the next call at the ceiling, repository facts computed
|
|
||||||
model-free and reused, blobs replayed at $0, triage before the hunt and a tie-break only on
|
- one Model and one budget per Task;
|
||||||
disputed candidates. In development, 85.5% of the maintainers' coding-session tokens were tool
|
- a metered client that refuses the next call at the ceiling;
|
||||||
input and output rather than reasoning (down from 90%), and the remedies in `benchmarks/dev/`
|
- repository facts computed without a model and reused;
|
||||||
target that. Mechanisms,
|
- blobs replayed at $0, with no new model call;
|
||||||
the measured costs and the before-and-after table: [docs/token-ergonomics.md](docs/token-ergonomics.md).
|
- triage before the hunt;
|
||||||
|
- a tie-break only on disputed candidates.
|
||||||
|
|
||||||
|
In development, 85.5% of the maintainers' coding-session tokens were tool input and output rather
|
||||||
|
than reasoning. That is down from 90%. The remedies in `benchmarks/dev/` target that share.
|
||||||
|
Mechanisms, the measured costs and the before-and-after table:
|
||||||
|
[docs/token-ergonomics.md](docs/token-ergonomics.md).
|
||||||
|
|
||||||
## Examples, threat model
|
## Examples, threat model
|
||||||
|
|
||||||
End-to-end walkthroughs, from a quick scan to an MCP client: [docs/examples.md](docs/examples.md).
|
- End-to-end walkthroughs, from a quick scan to an MCP client: [docs/examples.md](docs/examples.md).
|
||||||
What the tool trusts and what it does not (scanned code is untrusted input, secret redaction,
|
- What the tool trusts and what it does not: [docs/threat-model.md](docs/threat-model.md). This
|
||||||
per-task egress, budgets, the `deep` sandbox): [docs/threat-model.md](docs/threat-model.md).
|
covers scanned code as untrusted input, secret redaction, per-task egress, budgets and the
|
||||||
|
`deep` sandbox.
|
||||||
|
|
||||||
## Contributing and maintainer tooling
|
## Contributing and maintainer tooling
|
||||||
|
|
||||||
@@ -228,25 +351,111 @@ uv run python dagger/ci.py # containerized CI pipeline
|
|||||||
```
|
```
|
||||||
|
|
||||||
An agent that can run shell commands needs nothing but the CLI. For [OpenCode](https://opencode.ai)
|
An agent that can run shell commands needs nothing but the CLI. For [OpenCode](https://opencode.ai)
|
||||||
there are two project skills in `.agents/skills/`: `openultrasast-scan` (modes and their
|
there are two project skills in `.agents/skills/`:
|
||||||
prerequisites, `--fail-on`, the evidence ladder, recorded degradations, run artifacts) and
|
|
||||||
`openultrasast-triage` (false-positive reasons, what an adjudication may feed, fixing and
|
- `openultrasast-scan`: modes and their prerequisites, `--fail-on`, the evidence ladder, recorded
|
||||||
re-scanning). `ousast mcp` runs a narrow MCP server over stdio with ten tools (`openultrasast.scan`,
|
degradations and run artifacts.
|
||||||
`status`, `findings`, `get_finding`, `evidence`, `artifacts`, `benchmark`, `explain`,
|
- `openultrasast-triage`: false-positive reasons, what an adjudication may feed, and fixing and
|
||||||
`propose_patch`, `export_report`); no tool runs a shell, Docker or a free-form command:
|
re-scanning.
|
||||||
|
|
||||||
|
`ousast mcp` runs a narrow MCP (Model Context Protocol) server over stdio. It has ten tools:
|
||||||
|
`openultrasast.scan`, `status`, `findings`, `get_finding`, `evidence`, `artifacts`, `benchmark`,
|
||||||
|
`explain`, `propose_patch` and `export_report`. No tool runs a shell, Docker or a free-form
|
||||||
|
command.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{ "command": "uv", "args": ["run", "ousast", "mcp"] }
|
{ "command": "uv", "args": ["run", "ousast", "mcp"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
The `kiro-*` skills, `AGENTS.md` and the Kiro-style specifications under `.kiro/` are the
|
The `kiro-*` skills, `AGENTS.md` and the Kiro-style specifications under `.kiro/` are the
|
||||||
maintainer's development tooling and can be ignored by users. Other maintainer commands:
|
maintainer's development tooling. Users can ignore them. Other maintainer commands:
|
||||||
`ousast repos` (pinned known-vulnerable checkouts) and `ousast model candidates` (what the
|
|
||||||
candidate enumerator can reach).
|
- `ousast repos`: pinned known-vulnerable checkouts.
|
||||||
|
- `ousast model candidates`: what the candidate enumerator can reach.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
OpenUltraSAST is licensed under the [Apache License 2.0](LICENSE). Third-party material keeps its
|
OpenUltraSAST is licensed under the [Apache License 2.0](LICENSE). Third-party material keeps its
|
||||||
own terms: Joern (Apache-2.0) is downloaded at image build time, the tree-sitter wheels of the
|
own terms:
|
||||||
`semantic` extra are MIT, and the published benchmark cases were licence-checked one by one (v1's
|
|
||||||
OpenCVE case is BUSL-1.1, see [RELEASE_NOTES.md](RELEASE_NOTES.md)).
|
- Joern (Apache-2.0) is downloaded at image build time.
|
||||||
|
- The tree-sitter wheels of the `semantic` extra are MIT.
|
||||||
|
- The published benchmark cases were licence-checked one by one. v1's OpenCVE case is BUSL-1.1, see [RELEASE_NOTES.md](RELEASE_NOTES.md).
|
||||||
|
|
||||||
|
## Engine traces on kube-ax
|
||||||
|
|
||||||
|
`benchmarks/learn/engine_trace.py --executor ax` submits AX v0.3.1 Tasks with
|
||||||
|
`ax apply -f -` and removes them with `ax delete task NAME`. The operator supplies
|
||||||
|
kubeconfig and AX access; the dispatcher creates no Kubernetes resources or secrets.
|
||||||
|
The `ousast-engine-task` GHCR package **must be made Public once** in its package
|
||||||
|
settings so the actors can pull it without registry credentials.
|
||||||
|
|
||||||
|
The task image contains the current engine and worker code. Rebuild it after code
|
||||||
|
changes and pass its digest with `--image`; no analyzer download is needed. The host
|
||||||
|
materializes each pin as usual and uploads source and prepared questions under
|
||||||
|
`engine-queue/<run>/<pin>/`. Only object-scoped presigned URLs and deadline parameters
|
||||||
|
enter Task env. Credentials stay on the coordinator. Configure `OUSAST_MEMORY=s3://…`
|
||||||
|
and the store credentials in the coordinator's `.env` or environment, with
|
||||||
|
`S3_ENDPOINT=https://files.because-security.com`. Presigned links expire after the
|
||||||
|
pin deadline plus ten minutes. IP and HTTP endpoints are rejected: the operator's
|
||||||
|
actor EgressPolicy must allow **files.because-security.com:443**.
|
||||||
|
|
||||||
|
Completion is the result object, even while AX reports Running. Failed phases and
|
||||||
|
host deadlines produce explicit failure/timeout rows. Task and staging-object cleanup
|
||||||
|
runs on success and failure; cleanup failure stops further dispatch. Per-pin files,
|
||||||
|
`--rerun-status`, resume and `progress.json` retain the existing trace-runner behavior.
|
||||||
|
AX results include `executor: "ax"`, `worker_ip`, and the image digest. Placement is
|
||||||
|
random; `--parallel` defaults to two AX tasks and cannot exceed two.
|
||||||
|
|
||||||
|
AX workers use `--heap-profile cluster` with
|
||||||
|
`-Xms1700m -Xmx1700m -XX:MaxMetaspaceSize=256m -XX:ReservedCodeCacheSize=128m -XX:MaxDirectMemorySize=256m -Xss512k -XX:ActiveProcessorCount=1 -XX:+UseG1GC`
|
||||||
|
on every frontend/query JVM; instruments record the flags. A `JVM out of memory`
|
||||||
|
result is automatically re-queued to one local Docker lane with `--heap-profile vm`
|
||||||
|
(2560m), recording `fallback: "vm-oom"` and the AX attempt. `mixed-ax` also uses this
|
||||||
|
one Docker lane for overflow, prioritizing pending OOM fallbacks. Thus a mixed run has
|
||||||
|
up to two AX tasks plus one Docker container. Use `--docker-image` for the locally
|
||||||
|
available VM image; Docker never pulls it implicitly. A STOP file prevents new
|
||||||
|
launches; interrupted fallback queues are recovered on resume.
|
||||||
|
|
||||||
|
Coordinator commands, from this worktree (the AX binary and kubeconfig remain
|
||||||
|
configurable). Build/push the actual uncommitted tree and read its digest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/mc/Source/OpenUltraSAST/.claude/worktrees/engine-ax
|
||||||
|
export PATH=/home/mc/Source/OpenUltraSAST/.venv/bin:$PATH
|
||||||
|
export PYTHONPATH="$PWD/src"
|
||||||
|
TASK_TAG="ghcr.io/norandom/ousast-engine-task:sha-$(git rev-parse --short=7 HEAD)"
|
||||||
|
docker buildx build --platform linux/amd64 -f plane/Dockerfile.engine-task \
|
||||||
|
-t "$TASK_TAG" -t ghcr.io/norandom/ousast-engine-task:main \
|
||||||
|
--push --metadata-file /tmp/ousast-engine-task-build.json .
|
||||||
|
export TASK_IMAGE="ghcr.io/norandom/ousast-engine-task@$(python -c 'import json; print(json.load(open("/tmp/ousast-engine-task-build.json"))["containerimage.digest"])')"
|
||||||
|
printf '%s\n' "$TASK_IMAGE"
|
||||||
|
docker build -f Dockerfile -t openultrasast:engine-ax .
|
||||||
|
```
|
||||||
|
|
||||||
|
The Engine image workflow also builds/pushes both images and prints each digest;
|
||||||
|
CI needs the changes published before dispatch. The local build above includes
|
||||||
|
uncommitted changes without publishing a git commit.
|
||||||
|
|
||||||
|
With `KUBECONFIG` pointing to the operator's kube-ax config and store settings in
|
||||||
|
`.env`, run a one-pin smoke using the default experiment units and local example
|
||||||
|
memory (or add `--examples /path/to/frozen-examples.jsonl`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
: "${KUBECONFIG:?Set KUBECONFIG to the operator kube-ax configuration}"
|
||||||
|
python benchmarks/learn/engine_trace.py --executor ax \
|
||||||
|
--ax-bin "$HOME/.local/ax-v0.3.1/ax" --kubeconfig "$KUBECONFIG" \
|
||||||
|
--atespace default --image "$TASK_IMAGE" --docker-image openultrasast:engine-ax \
|
||||||
|
--parallel 1 --limit 1 --deadline 900 \
|
||||||
|
--out "$HOME/ousast-results/plane/engine-ax-smoke"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the mixed lanes (repeat to resume; defaults re-run failed/timeout pins):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python benchmarks/learn/engine_trace.py --executor mixed-ax \
|
||||||
|
--ax-bin "$HOME/.local/ax-v0.3.1/ax" --kubeconfig "$KUBECONFIG" \
|
||||||
|
--atespace default --image "$TASK_IMAGE" --docker-image openultrasast:engine-ax \
|
||||||
|
--parallel 2 --deadline 900 --rerun-status failed,timeout \
|
||||||
|
--out "$HOME/ousast-results/plane/engine-ax-mixed"
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,809 @@
|
|||||||
|
"""Host-only AX Task transport; credentials never enter task manifests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from collections import Counter, deque
|
||||||
|
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from openultrasast.config import load_dotenv
|
||||||
|
from openultrasast.plane.memory import PRESIGNED_ARCHIVE_SUFFIX, S3Store, open_store
|
||||||
|
from openultrasast.search.task_storage import diagnostic, exception_reason
|
||||||
|
|
||||||
|
|
||||||
|
class EgressError(ValueError):
|
||||||
|
"""Safe diagnostic without presigned URL contents."""
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxFailure(Exception):
|
||||||
|
"""A terminal AX task without an authoritative result object."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoFreeWorkers(Exception):
|
||||||
|
"""Capacity back-pressure, never a sandbox or instrument failure."""
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterUnavailable(Exception):
|
||||||
|
"""No running workers were observed in the worker namespace."""
|
||||||
|
|
||||||
|
|
||||||
|
class CapacityDeadline(Exception):
|
||||||
|
"""No worker became available before the task deadline."""
|
||||||
|
|
||||||
|
|
||||||
|
# Shared by every workload/lane instance in this dispatcher process, including
|
||||||
|
# long-lived search executors. The operator runs one task per worker, two workers.
|
||||||
|
AX_SLOTS = threading.BoundedSemaphore(2)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_when_available(lane, document, end):
|
||||||
|
started = lane.clock()
|
||||||
|
next_health_check = started + 60
|
||||||
|
while lane.clock() < end:
|
||||||
|
try:
|
||||||
|
lane.ax("apply", "-f", "-", manifest=document, timeout=min(30, end - lane.clock()))
|
||||||
|
return
|
||||||
|
except NoFreeWorkers:
|
||||||
|
# A failed apply is not proof that the server did not create a task.
|
||||||
|
# Never send another create while the preceding task can still run.
|
||||||
|
try:
|
||||||
|
lane.ax("delete", "task", document["metadata"]["name"])
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError("capacity cleanup failed: " + exception_reason(exc)) from None
|
||||||
|
if lane.clock() >= next_health_check:
|
||||||
|
lane.check_cluster_health(timeout=min(10, max(0.01, end - lane.clock())))
|
||||||
|
next_health_check = lane.clock() + 60
|
||||||
|
# Substrate's golden-snapshot copy holds the other worker for about a second after each new task
|
||||||
|
# (operator, 2026-10-05): retry quickly for the first minute, then back off while real tasks run.
|
||||||
|
delay = random.uniform(2, 5) if lane.clock() - started < 60 else random.uniform(15, 30)
|
||||||
|
lane.pause(min(delay, max(0, end - lane.clock())))
|
||||||
|
raise CapacityDeadline("no free workers available before deadline")
|
||||||
|
|
||||||
|
|
||||||
|
def public_endpoint(url):
|
||||||
|
"""Reject endpoints the actor's hostname-based HTTPS egress policy cannot reach."""
|
||||||
|
parts = urlsplit(url)
|
||||||
|
host = parts.hostname or ""
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(host)
|
||||||
|
is_ip = True
|
||||||
|
except ValueError:
|
||||||
|
is_ip = False
|
||||||
|
if (
|
||||||
|
parts.scheme != "https"
|
||||||
|
or not host
|
||||||
|
or is_ip
|
||||||
|
or "." not in host
|
||||||
|
or host.endswith((".localhost", ".local", ".internal"))
|
||||||
|
or parts.port not in (None, 443)
|
||||||
|
or parts.username
|
||||||
|
or parts.password
|
||||||
|
):
|
||||||
|
raise EgressError(
|
||||||
|
"AX requires a public HTTPS DNS hostname in S3_ENDPOINT and presigned URLs: "
|
||||||
|
"the egress policy allows the store hostname on port 443 only (no IP or plain HTTP)"
|
||||||
|
)
|
||||||
|
return host.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def task_state(text, name):
|
||||||
|
"""Accept AX YAML/JSON output and its human-readable tasks table."""
|
||||||
|
try:
|
||||||
|
doc = yaml.safe_load(text)
|
||||||
|
except yaml.YAMLError:
|
||||||
|
doc = None
|
||||||
|
rows = doc.get("items", doc.get("tasks", [doc])) if isinstance(doc, dict) else doc if isinstance(doc, list) else []
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict) or row.get("metadata", {}).get("name", row.get("name")) != name:
|
||||||
|
continue
|
||||||
|
status = row.get("status", {})
|
||||||
|
ip = next(
|
||||||
|
(status.get(key) or row.get(key) for key in ("workerIP", "workerIp", "worker_ip", "ip") if status.get(key) or row.get(key)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
worker = status.get("worker")
|
||||||
|
if not ip and isinstance(worker, dict):
|
||||||
|
ip = worker.get("ip") or worker.get("address")
|
||||||
|
return str(status.get("phase", row.get("phase", ""))), ip
|
||||||
|
lines = [line.split() for line in text.splitlines() if line.strip()]
|
||||||
|
if lines:
|
||||||
|
headers = [re.sub(r"[^a-z]", "", cell.lower()) for cell in lines[0]]
|
||||||
|
for cells in lines[1:]:
|
||||||
|
if name not in cells:
|
||||||
|
continue
|
||||||
|
row = dict(zip(headers, cells, strict=False))
|
||||||
|
phase = row.get("phase", row.get("status", ""))
|
||||||
|
ip = row.get("workerip") or row.get("ip")
|
||||||
|
if not ip:
|
||||||
|
for cell in cells:
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(cell)
|
||||||
|
ip = cell
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return phase, None if ip in {"<none>", "-", ""} else ip
|
||||||
|
listed = isinstance(doc, (dict, list)) or bool(lines and "name" in headers and {"phase", "status"}.intersection(headers))
|
||||||
|
return "Missing" if listed else "", None
|
||||||
|
|
||||||
|
|
||||||
|
def bounded_read(read, timeout):
|
||||||
|
"""Do not let SDK retries postpone task teardown beyond the pin deadline.
|
||||||
|
|
||||||
|
Only the read runs in a daemon thread; it cannot publish a late result to disk.
|
||||||
|
No executor shutdown waits for an SDK call whose socket has stopped responding.
|
||||||
|
"""
|
||||||
|
if timeout <= 0:
|
||||||
|
raise TimeoutError("AX result deadline")
|
||||||
|
replies = queue.Queue(maxsize=1)
|
||||||
|
|
||||||
|
def fetch():
|
||||||
|
try:
|
||||||
|
replies.put((True, read()))
|
||||||
|
except Exception as exc:
|
||||||
|
replies.put((False, exc))
|
||||||
|
|
||||||
|
threading.Thread(target=fetch, daemon=True, name="engine-ax-result-read").start()
|
||||||
|
try:
|
||||||
|
ok, result = replies.get(timeout=timeout)
|
||||||
|
except queue.Empty:
|
||||||
|
raise TimeoutError("AX result deadline") from None
|
||||||
|
if not ok:
|
||||||
|
raise result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class AXLane:
|
||||||
|
def __init__(self, args, workload, store=None, pause=time.sleep, clock=time.monotonic, runner=None):
|
||||||
|
self.runner = runner or subprocess.run
|
||||||
|
self.workload = workload
|
||||||
|
self.args = args
|
||||||
|
self.pause, self.clock = pause, clock
|
||||||
|
self.run = uuid.uuid4().hex
|
||||||
|
load_dotenv()
|
||||||
|
self.host = public_endpoint(os.environ.get("S3_ENDPOINT", ""))
|
||||||
|
# Validate the image before opening a store, whose constructor probes the bucket.
|
||||||
|
validate_image(workload.image)
|
||||||
|
self.store = store if store is not None else open_store()
|
||||||
|
if store is None and not isinstance(self.store, S3Store):
|
||||||
|
raise ValueError("AX requires OUSAST_MEMORY=s3://bucket[/prefix]")
|
||||||
|
|
||||||
|
def __call__(self, item, output, attempt):
|
||||||
|
attempts = []
|
||||||
|
# Docker has its own capacity and must not hold an AX worker slot.
|
||||||
|
slot = not isinstance(self, DockerLane)
|
||||||
|
if slot and not AX_SLOTS.acquire(timeout=self.workload.deadline):
|
||||||
|
return {"status": "capacity_timeout", "ax_attempts": attempts}
|
||||||
|
try:
|
||||||
|
done, worker = self._execute_attempt(item, output, attempt, attempts)
|
||||||
|
except CapacityDeadline:
|
||||||
|
return {"status": "capacity_timeout", "ax_attempts": attempts}
|
||||||
|
except SandboxFailure:
|
||||||
|
return {"status": "sandbox_failure", "ax_attempts": attempts}
|
||||||
|
except RuntimeError:
|
||||||
|
# Teardown did not prove the worker free; retain its reservation.
|
||||||
|
slot = False
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if slot:
|
||||||
|
AX_SLOTS.release()
|
||||||
|
if done is None or done.returncode:
|
||||||
|
return {
|
||||||
|
"status": "instrument_failure",
|
||||||
|
"reason": diagnostic(done.stderr) if done is not None else "TimeoutError: task result deadline",
|
||||||
|
"ax_attempts": attempts,
|
||||||
|
}
|
||||||
|
record = json.loads((output / "result.json").read_text())
|
||||||
|
return {**record, "worker_ip": worker, "ax_attempts": attempts}
|
||||||
|
|
||||||
|
def check_cluster_health(self, *, timeout=10):
|
||||||
|
env = dict(os.environ)
|
||||||
|
if self.args.kubeconfig:
|
||||||
|
env["KUBECONFIG"] = str(Path(self.args.kubeconfig).expanduser())
|
||||||
|
done = self.runner(
|
||||||
|
["kubectl", "-n", "ax-workers", "get", "pods", "-o", "json"],
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
if done.returncode:
|
||||||
|
raise RuntimeError(f"worker pod health check failed (exit {done.returncode})")
|
||||||
|
try:
|
||||||
|
pods = json.loads(done.stdout)["items"]
|
||||||
|
if not isinstance(pods, list):
|
||||||
|
raise ValueError("invalid pod list")
|
||||||
|
phases = [pod["status"]["phase"] for pod in pods]
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
raise RuntimeError("worker pod health check returned invalid pod data") from None
|
||||||
|
running = phases.count("Running")
|
||||||
|
if not running:
|
||||||
|
raise ClusterUnavailable(f"worker pods: total={len(pods)}, running=0, pending={phases.count('Pending')}")
|
||||||
|
|
||||||
|
def ax(self, *args, manifest=None, timeout=30):
|
||||||
|
env = dict(os.environ)
|
||||||
|
if self.args.kubeconfig:
|
||||||
|
env["KUBECONFIG"] = str(Path(self.args.kubeconfig).expanduser())
|
||||||
|
done = self.runner(
|
||||||
|
[str(Path(self.args.ax_bin).expanduser()), *args, *(["--atespace", self.args.atespace] if args[0] != "apply" else [])],
|
||||||
|
input=json.dumps(manifest) if manifest is not None else None,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=max(0.01, timeout),
|
||||||
|
)
|
||||||
|
if done.returncode:
|
||||||
|
if args[0] == "apply" and "no free workers available" in (done.stdout + done.stderr).lower():
|
||||||
|
raise NoFreeWorkers("no free workers available")
|
||||||
|
if args[0] == "delete" and "notfound" in (done.stdout + done.stderr).lower().replace(" ", ""):
|
||||||
|
return done
|
||||||
|
# AX errors may echo manifest URLs; sanitize before recording.
|
||||||
|
raise ValueError(f"ax {args[0]} failed (exit {done.returncode}): " + diagnostic(done.stderr))
|
||||||
|
return done
|
||||||
|
|
||||||
|
def _execute_attempt(self, item, output, attempt, attempts):
|
||||||
|
pin_id = item.id
|
||||||
|
digest = hashlib.sha256(f"{self.run}/{pin_id}/attempt-{attempt}".encode()).hexdigest()[:12]
|
||||||
|
kind = re.sub(r"[^a-z0-9]+", "-", self.workload.kind.lower()).strip("-") or "pin"
|
||||||
|
# Names are capped at 49 bytes: 14 (prefix) + kind + 1 + slug + 1 + 12 (digest).
|
||||||
|
slug = re.sub(r"[^a-z0-9]+", "-", str(pin_id).lower()).strip("-")[: max(4, 21 - len(kind))]
|
||||||
|
name = f"ousast-engine-{kind}-{slug or 'pin'}-{digest}"
|
||||||
|
key_id = hashlib.sha256(str(pin_id).encode()).hexdigest()
|
||||||
|
prefix = f"engine-queue/{self.run}/{key_id}/attempt-{attempt}/"
|
||||||
|
keys = [prefix + item.object_names.get(key, key.lower() + PRESIGNED_ARCHIVE_SUFFIX) for key in item.inputs]
|
||||||
|
keys.append(prefix + item.result_object)
|
||||||
|
part_keys = []
|
||||||
|
expires = timedelta(seconds=self.workload.deadline + 600)
|
||||||
|
worker_ip = None
|
||||||
|
submitted = False
|
||||||
|
started = self.clock()
|
||||||
|
phase = ""
|
||||||
|
try:
|
||||||
|
for env, key, data in zip(item.inputs, keys, item.inputs.values(), strict=False):
|
||||||
|
if env == "VERIFY_INPUT_URL":
|
||||||
|
import io
|
||||||
|
|
||||||
|
from openultrasast.search.verify_task import MAX_PART_BYTES, upload_parts
|
||||||
|
|
||||||
|
count = (len(data) + MAX_PART_BYTES - 1) // MAX_PART_BYTES
|
||||||
|
destinations = [key + f".part-{index}" for index in range(count)]
|
||||||
|
part_keys.extend(destinations)
|
||||||
|
put_urls = [self.store.presign_put(part, expires) for part in destinations]
|
||||||
|
get_urls = [self.store.presign_get(part, expires) for part in destinations]
|
||||||
|
if any(public_endpoint(url) != self.host for url in put_urls + get_urls):
|
||||||
|
raise EgressError("archive part hostname must match store egress policy")
|
||||||
|
from openultrasast.search.executor import URLTransport
|
||||||
|
|
||||||
|
data = upload_parts(
|
||||||
|
io.BytesIO(data), lambda url, block: URLTransport().put(url, block, self.workload.deadline), put_urls
|
||||||
|
)
|
||||||
|
data = json.loads(data)
|
||||||
|
for part, url in zip(data["parts"], get_urls, strict=True):
|
||||||
|
part["url"] = url
|
||||||
|
data = json.dumps(data).encode()
|
||||||
|
self.store._put(key, data)
|
||||||
|
urls = {env: self.store.presign_get(key, expires) for env, key in zip(item.inputs, keys, strict=False)}
|
||||||
|
urls["RESULT_URL"] = self.store.presign_put(keys[-1], expires)
|
||||||
|
if any(public_endpoint(url) != self.host for url in urls.values()):
|
||||||
|
raise EgressError("AX presign hostname must match S3_ENDPOINT for the store egress policy")
|
||||||
|
document = manifest(name, self.workload, item, urls, self.args.atespace)
|
||||||
|
end = self.clock() + self.workload.deadline
|
||||||
|
# An apply timeout can still have created the task; cleanup must cover it.
|
||||||
|
submitted = True
|
||||||
|
try:
|
||||||
|
apply_when_available(self, document, end)
|
||||||
|
except CapacityDeadline:
|
||||||
|
submitted = False # All applies were explicitly rejected.
|
||||||
|
raise
|
||||||
|
while self.clock() < end:
|
||||||
|
result = bounded_read(lambda: self.store._get(keys[-1]), end - self.clock())
|
||||||
|
if self.clock() >= end:
|
||||||
|
return None, worker_ip
|
||||||
|
phase = ""
|
||||||
|
try:
|
||||||
|
state = self.ax("get", "tasks", timeout=min(30, end - self.clock())).stdout
|
||||||
|
phase, ip = task_state(state, name)
|
||||||
|
worker_ip = ip or worker_ip
|
||||||
|
except (ValueError, OSError, subprocess.SubprocessError):
|
||||||
|
# The object is authoritative even when AX status is temporarily unavailable.
|
||||||
|
pass
|
||||||
|
if self.clock() >= end:
|
||||||
|
return None, worker_ip
|
||||||
|
if result is not None:
|
||||||
|
self.workload.validate(result[0], output)
|
||||||
|
return subprocess.CompletedProcess([], 0, "", ""), worker_ip
|
||||||
|
if phase.lower() in {"failed", "missing"}:
|
||||||
|
# The worker may have uploaded while we were reading AX status.
|
||||||
|
result = bounded_read(lambda: self.store._get(keys[-1]), end - self.clock())
|
||||||
|
if self.clock() >= end:
|
||||||
|
return None, worker_ip
|
||||||
|
if result is not None:
|
||||||
|
self.workload.validate(result[0], output)
|
||||||
|
return subprocess.CompletedProcess([], 0, "", ""), worker_ip
|
||||||
|
raise SandboxFailure
|
||||||
|
self.pause(min(2, max(0, end - self.clock())))
|
||||||
|
return None, worker_ip
|
||||||
|
except (subprocess.TimeoutExpired, TimeoutError) as exc:
|
||||||
|
return subprocess.CompletedProcess([], 1, "", exception_reason(exc)), worker_ip
|
||||||
|
except (SandboxFailure, CapacityDeadline):
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
# Never expose exception strings which may contain object-scoped bearer URLs.
|
||||||
|
detail = exception_reason(exc)
|
||||||
|
return subprocess.CompletedProcess([], 1, "", "AX transport failed: " + detail), worker_ip
|
||||||
|
finally:
|
||||||
|
attempts.append({"name": name, "phase": phase, "seconds": self.clock() - started})
|
||||||
|
errors = []
|
||||||
|
if submitted:
|
||||||
|
try:
|
||||||
|
self.ax("delete", "task", name)
|
||||||
|
except Exception as exc:
|
||||||
|
# A successful list already confirmed a disappeared task is gone.
|
||||||
|
if phase != "Missing":
|
||||||
|
errors.append("task: " + exception_reason(exc))
|
||||||
|
for key in [*keys, *part_keys]:
|
||||||
|
try:
|
||||||
|
self.store._delete(key)
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append("object: " + exception_reason(exc))
|
||||||
|
if errors:
|
||||||
|
raise RuntimeError("AX cleanup failed; stop dispatching: " + ", ".join(errors))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Item:
|
||||||
|
id: str
|
||||||
|
inputs: dict[str, bytes]
|
||||||
|
result_object: str
|
||||||
|
command: tuple[str, ...]
|
||||||
|
extra_env: dict[str, str]
|
||||||
|
input_digest: str
|
||||||
|
object_names: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Workload:
|
||||||
|
image: str
|
||||||
|
url_env: frozenset[str]
|
||||||
|
deadline: float
|
||||||
|
validate: object
|
||||||
|
# Task-name prefix after "ousast-engine-"; the operator's per-host egress rules match on it.
|
||||||
|
kind: str = "pin"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_image(image):
|
||||||
|
if not re.fullmatch(r"[^\s@]+@sha256:[0-9a-f]{64}", image):
|
||||||
|
raise ValueError("AX --image must be pinned by sha256 digest")
|
||||||
|
|
||||||
|
|
||||||
|
def manifest(name, workload, item, urls, atespace="default", *, validate_name=True):
|
||||||
|
validate_image(workload.image)
|
||||||
|
if validate_name and (not re.fullmatch(r"ousast-engine-[a-z0-9-]+", name) or len(name.encode()) > 49):
|
||||||
|
raise ValueError("invalid AX task name")
|
||||||
|
if set(urls) != workload.url_env:
|
||||||
|
raise ValueError("unexpected URL environment names")
|
||||||
|
# Fixed names and semantic values, never inherited environment or credentials.
|
||||||
|
patterns = {
|
||||||
|
"OUSAST_SCRATCH_BYTES": r"[1-9][0-9]*",
|
||||||
|
"DEADLINE": r"[0-9]+(?:\.[0-9]+)?",
|
||||||
|
"QUESTION_DEADLINE": r"[0-9]+(?:\.[0-9]+)?",
|
||||||
|
"SEARCH_STEP": r"reason|explore|verify",
|
||||||
|
"SEARCH_ID": r"[a-z0-9][a-z0-9-]{0,63}",
|
||||||
|
"SEARCH_TASK_ID": r"[a-z0-9][a-z0-9-]{0,63}",
|
||||||
|
}
|
||||||
|
if any(key not in patterns or not re.fullmatch(patterns[key], str(value)) for key, value in item.extra_env.items()):
|
||||||
|
raise ValueError("unexpected task environment")
|
||||||
|
if set(item.extra_env) & set(urls):
|
||||||
|
raise ValueError("overlapping task environment")
|
||||||
|
for key, value in urls.items():
|
||||||
|
if key == "PREPARED_PART_URLS":
|
||||||
|
from openultrasast.search.verify_task import MAX_PARTS
|
||||||
|
|
||||||
|
parts = json.loads(value)
|
||||||
|
if not isinstance(parts, list) or len(parts) != MAX_PARTS:
|
||||||
|
raise ValueError("invalid prepared part URLs")
|
||||||
|
for url in parts:
|
||||||
|
public_endpoint(url)
|
||||||
|
else:
|
||||||
|
public_endpoint(value)
|
||||||
|
env = [{"name": key, "value": str(value)} for key, value in {**urls, **item.extra_env}.items()]
|
||||||
|
if len(json.dumps(env).encode()) >= 32768:
|
||||||
|
raise ValueError("AX Task env exceeds the 32 KB limit")
|
||||||
|
return {
|
||||||
|
"apiVersion": "ax.io/v1alpha1",
|
||||||
|
"kind": "Task",
|
||||||
|
"metadata": {"name": name, "atespace": atespace},
|
||||||
|
"spec": {"image": workload.image, "env": env, "command": list(item.command)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path, record):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_suffix(".tmp")
|
||||||
|
temporary.write_text(json.dumps(record, sort_keys=True) + "\n")
|
||||||
|
temporary.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def schedule(pending, lanes, out, execute, complete, *, fallbacks=(), overflow=True, progress=None):
|
||||||
|
"""The sole progress writer; callbacks carry workload-specific records, never transport."""
|
||||||
|
if sum(lane in {"ax", "kind"} for lane in lanes) > 2 or lanes.count("docker") > 1:
|
||||||
|
raise ValueError("at most two AX lanes and one VM lane")
|
||||||
|
pending, fallbacks = deque(pending), deque(fallbacks)
|
||||||
|
progress = progress if progress is not None else {"pins_done": 0, "pins_total": len(pending) + len(fallbacks)}
|
||||||
|
progress["lanes"] = dict(Counter(lanes))
|
||||||
|
active = {}
|
||||||
|
failures = False
|
||||||
|
|
||||||
|
def persist():
|
||||||
|
progress.update(pins_in_flight=len(active), active_lanes=dict(Counter(lane for _, lane in active.values())))
|
||||||
|
if any(lane in {"ax", "kind"} for lane in lanes):
|
||||||
|
progress["fallbacks_pending"] = len(fallbacks)
|
||||||
|
write_json(out / "progress.json", progress)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=len(lanes)) as pool:
|
||||||
|
|
||||||
|
def launch(lane):
|
||||||
|
if (out / "STOP").exists():
|
||||||
|
return
|
||||||
|
if lane == "docker" and fallbacks:
|
||||||
|
item, previous = fallbacks.popleft()
|
||||||
|
elif pending and (lane != "docker" or overflow):
|
||||||
|
item, previous = pending.popleft(), None
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
active[pool.submit(execute, item, lane, previous)] = item, lane
|
||||||
|
|
||||||
|
for lane in lanes:
|
||||||
|
launch(lane)
|
||||||
|
persist()
|
||||||
|
try:
|
||||||
|
while active:
|
||||||
|
finished, _ = wait(active, return_when=FIRST_COMPLETED)
|
||||||
|
# Resolve ALL completed futures before launching; cleanup exceptions stop launches.
|
||||||
|
results = [(active.pop(future), future.result()) for future in finished]
|
||||||
|
for (item, lane), result in results:
|
||||||
|
failed, fallback = complete(item, lane, result, progress)
|
||||||
|
if fallback is not None:
|
||||||
|
fallbacks.append((item, fallback))
|
||||||
|
else:
|
||||||
|
failures |= failed
|
||||||
|
progress["pins_done"] += 1
|
||||||
|
for lane, count in (Counter(lanes) - Counter(lane for _, lane in active.values())).items():
|
||||||
|
for _ in range(count):
|
||||||
|
launch(lane)
|
||||||
|
persist()
|
||||||
|
finally:
|
||||||
|
persist()
|
||||||
|
return failures
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch(items, workload, out, executors, *, lanes=("ax", "ax", "docker"), overflow=True, max_attempts=2):
|
||||||
|
"""Digest checkpoints, one instrument rerun, sandbox retry, priority VM fallback."""
|
||||||
|
if not isinstance(max_attempts, int) or not 1 <= max_attempts <= 2:
|
||||||
|
raise ValueError("batch attempts must be one or two")
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
items = list(items)
|
||||||
|
if len({item.id for item in items}) != len(items):
|
||||||
|
raise ValueError("duplicate dispatch item id")
|
||||||
|
pending, fallbacks = [], []
|
||||||
|
|
||||||
|
def path(item):
|
||||||
|
return out / (hashlib.sha256(item.id.encode()).hexdigest() + ".json")
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
previous = json.loads(path(item).read_text()) if path(item).exists() else {}
|
||||||
|
if previous.get("input_digest") == item.input_digest:
|
||||||
|
if previous.get("fallback_pending"):
|
||||||
|
fallbacks.append((item, previous))
|
||||||
|
continue
|
||||||
|
if previous.get("done"):
|
||||||
|
continue
|
||||||
|
pending.append(item)
|
||||||
|
|
||||||
|
def execute(item, lane, previous):
|
||||||
|
output = out / hashlib.sha256(item.id.encode()).hexdigest()
|
||||||
|
output.mkdir(exist_ok=True)
|
||||||
|
attempts = []
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
attempt_output = output / ("attempt-" + uuid.uuid4().hex)
|
||||||
|
attempt_output.mkdir()
|
||||||
|
record = executors[lane](item, attempt_output, attempt)
|
||||||
|
for artifact in attempt_output.iterdir():
|
||||||
|
if artifact.is_file():
|
||||||
|
shutil.copyfile(artifact, output / artifact.name)
|
||||||
|
attempts.append(
|
||||||
|
{
|
||||||
|
"lane": lane,
|
||||||
|
"status": record.get("status"),
|
||||||
|
"attempt": attempt,
|
||||||
|
"transport": record.get("ax_attempts", []),
|
||||||
|
"docker_errors": record.get("docker_errors", []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if record.get("status") not in {"sandbox_failure", "instrument_failure"}:
|
||||||
|
break
|
||||||
|
return {**record, "attempts": (previous or {}).get("attempts", []) + attempts, "input_digest": item.input_digest, "done": True}
|
||||||
|
|
||||||
|
def complete(item, lane, record, progress):
|
||||||
|
fallback = lane in {"ax", "kind"} and record.get("status") in {"sandbox_failure", "oom"} and "docker" in lanes
|
||||||
|
record["fallback_pending"] = fallback
|
||||||
|
write_json(path(item), record)
|
||||||
|
progress["last_item"] = hashlib.sha256(item.id.encode()).hexdigest()
|
||||||
|
return record.get("status") not in {"ok", "unanalysable", "quick_only", "engine_covered"}, record if fallback else None
|
||||||
|
|
||||||
|
return schedule(pending, lanes, out, execute, complete, fallbacks=fallbacks, overflow=overflow)
|
||||||
|
|
||||||
|
|
||||||
|
def docker_diagnostic(text):
|
||||||
|
"""Bound diagnostics and remove bearer URLs, environment secrets and assignments."""
|
||||||
|
return diagnostic(text or "", maximum=4000)
|
||||||
|
|
||||||
|
|
||||||
|
class DockerLane(AXLane):
|
||||||
|
"""VM lane uses the same presigned-object protocol and pinned replay image."""
|
||||||
|
|
||||||
|
def __init__(self, *args, runner=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.runner = runner or subprocess.run
|
||||||
|
self._prepared = False
|
||||||
|
|
||||||
|
def __call__(self, item, output, attempt):
|
||||||
|
self._diagnostics = []
|
||||||
|
try:
|
||||||
|
if not self._prepared:
|
||||||
|
self._prepare_image()
|
||||||
|
self._prepared = True
|
||||||
|
except (ValueError, OSError, subprocess.SubprocessError) as exc:
|
||||||
|
if not self._diagnostics:
|
||||||
|
self._diagnostics.append({"step": "preflight", "exit_code": None, "stderr": docker_diagnostic(str(exc))})
|
||||||
|
record = {"status": "instrument_failure", "docker_errors": self._diagnostics}
|
||||||
|
write_json(output / "docker-error.json", record)
|
||||||
|
return record
|
||||||
|
try:
|
||||||
|
record = super().__call__(item, output, attempt)
|
||||||
|
finally:
|
||||||
|
if self._diagnostics:
|
||||||
|
write_json(output / "docker-error.json", {"docker_errors": self._diagnostics})
|
||||||
|
if self._diagnostics:
|
||||||
|
record["docker_errors"] = self._diagnostics
|
||||||
|
return record
|
||||||
|
|
||||||
|
def _run_docker(self, command, timeout):
|
||||||
|
try:
|
||||||
|
done = self.runner(command, capture_output=True, text=True, timeout=timeout)
|
||||||
|
except (OSError, subprocess.SubprocessError) as exc:
|
||||||
|
self._diagnostics.append(
|
||||||
|
{"step": command[1], "exit_code": None, "stderr": docker_diagnostic(getattr(exc, "stderr", "") or exception_reason(exc))}
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
if done.returncode:
|
||||||
|
self._diagnostics.append({"step": command[1], "exit_code": done.returncode, "stderr": docker_diagnostic(done.stderr)})
|
||||||
|
raise ValueError(f"docker {command[1]} failed (exit {done.returncode}): " + diagnostic(done.stderr))
|
||||||
|
return done
|
||||||
|
|
||||||
|
def _prepare_image(self):
|
||||||
|
# Docker's data root, not the coordinator checkout, is where layers consume space.
|
||||||
|
root = self._run_docker(["docker", "info", "--format", "{{.DockerRootDir}}"], 30).stdout.strip()
|
||||||
|
size = getattr(self.args, "docker_image_bytes", None)
|
||||||
|
if not isinstance(size, int) or size <= 0:
|
||||||
|
raise ValueError("set --docker-image-bytes to the unpacked image size upper bound before pulling")
|
||||||
|
free = self.runner(["df", "-B1", "--output=avail", root], capture_output=True, text=True, timeout=30)
|
||||||
|
if free.returncode:
|
||||||
|
raise ValueError("cannot check free space on Docker data root")
|
||||||
|
available = int(free.stdout.splitlines()[-1].strip())
|
||||||
|
required = size + 1024**3
|
||||||
|
if available < required:
|
||||||
|
raise ValueError(f"refusing Docker pull: free bytes {available} below image + 1 GiB ({required})")
|
||||||
|
self._run_docker(["docker", "pull", self.workload.image], 900)
|
||||||
|
|
||||||
|
def _execute_attempt(self, item, output, attempt, attempts):
|
||||||
|
# Reuse all object/validation/cleanup logic, replacing only the task runner.
|
||||||
|
import copy
|
||||||
|
|
||||||
|
lane = copy.copy(self)
|
||||||
|
lane.ax = lane._docker_command
|
||||||
|
lane._docker_name = None
|
||||||
|
return AXLane._execute_attempt(lane, item, output, attempt, attempts)
|
||||||
|
|
||||||
|
def _docker_command(self, *args, manifest=None, timeout=30):
|
||||||
|
if args[0] == "apply":
|
||||||
|
self._docker_name = manifest["metadata"]["name"]
|
||||||
|
self._docker_created = True # Even a failed run can leave a created container.
|
||||||
|
command = [
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"-d",
|
||||||
|
"--runtime",
|
||||||
|
"runc",
|
||||||
|
"--pull",
|
||||||
|
"never",
|
||||||
|
"--name",
|
||||||
|
self._docker_name,
|
||||||
|
"--entrypoint",
|
||||||
|
manifest["spec"]["command"][0],
|
||||||
|
]
|
||||||
|
for env in manifest["spec"]["env"]:
|
||||||
|
command += ["-e", env["name"] + "=" + env["value"]]
|
||||||
|
command += [manifest["spec"]["image"], *manifest["spec"]["command"][1:]]
|
||||||
|
if not {"openultrasast.search.executor", "openultrasast.search.verify_task"}.intersection(manifest["spec"]["command"]):
|
||||||
|
command += ["--heap-profile", "vm"]
|
||||||
|
elif args[0] == "get":
|
||||||
|
command = ["docker", "inspect", "--format", "{{.State.Running}} {{.State.ExitCode}}", self._docker_name]
|
||||||
|
else:
|
||||||
|
if not getattr(self, "_docker_created", False):
|
||||||
|
return subprocess.CompletedProcess([], 0, "", "")
|
||||||
|
command = ["docker", "rm", "-f", self._docker_name or args[2]]
|
||||||
|
try:
|
||||||
|
done = self._run_docker(command, timeout)
|
||||||
|
except ValueError:
|
||||||
|
if args[0] == "delete" and "No such container" in self._diagnostics[-1]["stderr"]:
|
||||||
|
self._diagnostics.pop()
|
||||||
|
return subprocess.CompletedProcess(command, 0, "", "")
|
||||||
|
raise
|
||||||
|
if args[0] == "apply":
|
||||||
|
self._docker_created = True
|
||||||
|
if args[0] == "get":
|
||||||
|
phase = "Running" if done.stdout.startswith("true") else "Failed"
|
||||||
|
if phase == "Failed":
|
||||||
|
logs = self._run_docker(["docker", "logs", "--tail", "10", self._docker_name], timeout)
|
||||||
|
self._diagnostics.append(
|
||||||
|
{"step": "container", "exit_code": int(done.stdout.split()[-1]), "stderr": docker_diagnostic(logs.stderr)}
|
||||||
|
)
|
||||||
|
done.stdout = json.dumps({"metadata": {"name": self._docker_name}, "status": {"phase": phase}})
|
||||||
|
return done
|
||||||
|
|
||||||
|
|
||||||
|
class SearchExecutorTask:
|
||||||
|
"""Host-owned executor lifecycle using the shared AX/presigning transport.
|
||||||
|
|
||||||
|
Supply a bounded tar archive of the checkout. The brain receives only the
|
||||||
|
command PUT/result GET pair; the task receives their opposite permissions.
|
||||||
|
Closing the context deletes the task even after an uncertain command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, lane, checkout_archive, *, deadline=900):
|
||||||
|
if not 0 < deadline <= 3600 or len(checkout_archive) > 128 * 1024**2:
|
||||||
|
raise ValueError("executor task input limit")
|
||||||
|
if isinstance(lane, DockerLane):
|
||||||
|
import copy
|
||||||
|
|
||||||
|
lane = copy.copy(lane)
|
||||||
|
lane._diagnostics = []
|
||||||
|
lane.ax = lane._docker_command
|
||||||
|
self.lane, self.archive, self.deadline = lane, checkout_archive, deadline
|
||||||
|
token = uuid.uuid4().hex[:20]
|
||||||
|
self.name = "ousast-engine-search-exec-" + token
|
||||||
|
self.keys = [f"engine-queue/search-exec/{token}/{name}" for name in ("repo.tar", "command.json", "result.json", "prepared.json")]
|
||||||
|
from openultrasast.search.verify_task import MAX_PARTS
|
||||||
|
|
||||||
|
self.keys.extend(self.keys[3] + f".part-{index}" for index in range(MAX_PARTS))
|
||||||
|
self.submitted = False
|
||||||
|
self.slot = False
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
from openultrasast.search.executor import ObjectStoreExecutor
|
||||||
|
|
||||||
|
self.end = time.monotonic() + self.deadline
|
||||||
|
if not isinstance(self.lane, DockerLane):
|
||||||
|
if not AX_SLOTS.acquire(timeout=self.deadline):
|
||||||
|
raise CapacityDeadline("no free workers available before deadline")
|
||||||
|
self.slot = True
|
||||||
|
expires = timedelta(seconds=self.deadline + 600)
|
||||||
|
store = self.lane.store
|
||||||
|
try:
|
||||||
|
if isinstance(self.lane, DockerLane):
|
||||||
|
self.lane._run_docker(["docker", "image", "inspect", self.lane.workload.image], min(30, self.deadline))
|
||||||
|
store._put(self.keys[0], self.archive)
|
||||||
|
urls = {
|
||||||
|
"REPO_URL": store.presign_get(self.keys[0], expires),
|
||||||
|
"COMMAND_GET_URL": store.presign_get(self.keys[1], expires),
|
||||||
|
"EXECUTOR_RESULT_PUT_URL": store.presign_put(self.keys[2], expires),
|
||||||
|
"PREPARED_PUT_URL": store.presign_put(self.keys[3], expires),
|
||||||
|
}
|
||||||
|
part_urls = [store.presign_put(key, expires) for key in self.keys[4:]]
|
||||||
|
command_put = store.presign_put(self.keys[1], expires)
|
||||||
|
result_get = store.presign_get(self.keys[2], expires)
|
||||||
|
if any(public_endpoint(url) != self.lane.host for url in [*urls.values(), *part_urls, command_put, result_get]):
|
||||||
|
raise EgressError("executor presign hostname must match store egress policy")
|
||||||
|
urls["PREPARED_PART_URLS"] = json.dumps(part_urls)
|
||||||
|
work = Workload(self.lane.workload.image, frozenset(urls), self.deadline, None)
|
||||||
|
item = Item(
|
||||||
|
self.name,
|
||||||
|
{},
|
||||||
|
"result.json",
|
||||||
|
(
|
||||||
|
"python3",
|
||||||
|
"-m",
|
||||||
|
"openultrasast.search.executor",
|
||||||
|
"--repo",
|
||||||
|
"/workspace/checkout",
|
||||||
|
"--task-boundary",
|
||||||
|
"--deadline",
|
||||||
|
str(self.deadline),
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
document = manifest(self.name, work, item, urls, self.lane.args.atespace)
|
||||||
|
self.submitted = True
|
||||||
|
try:
|
||||||
|
apply_when_available(self.lane, document, self.end)
|
||||||
|
except CapacityDeadline:
|
||||||
|
self.submitted = False
|
||||||
|
raise
|
||||||
|
self.client = ObjectStoreExecutor(command_put, result_get)
|
||||||
|
return self
|
||||||
|
except BaseException as exc:
|
||||||
|
self.__exit__(type(exc), exc, exc.__traceback__)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def prepare(self, spec):
|
||||||
|
"""Check the export manifest and every fragment before handing off the archive."""
|
||||||
|
remaining = min(300, self.end - time.monotonic())
|
||||||
|
result = self.client.submit("prepare_verification", spec, timeout_seconds=remaining)
|
||||||
|
self.preparation_result = result
|
||||||
|
if result.get("status") != "ok":
|
||||||
|
from openultrasast.search.task_storage import CommandFailure
|
||||||
|
|
||||||
|
raise CommandFailure(
|
||||||
|
result.get("phase", "build"), result.get("exit_code"), result.get("stderr", ""), result.get("reason", "could_not_build")
|
||||||
|
)
|
||||||
|
stored = bounded_read(lambda: self.lane.store._get(self.keys[3]), self.end - time.monotonic())
|
||||||
|
if stored is None or hashlib.sha256(stored[0]).hexdigest() != result.get("sha256"):
|
||||||
|
raise ValueError("executor archive missing or digest mismatch")
|
||||||
|
import io
|
||||||
|
|
||||||
|
from openultrasast.search.verify_task import assemble_parts
|
||||||
|
|
||||||
|
def get(part):
|
||||||
|
value = bounded_read(lambda: self.lane.store._get(self.keys[4 + part["index"]]), self.end - time.monotonic())
|
||||||
|
if value is None:
|
||||||
|
raise ValueError("executor archive part missing")
|
||||||
|
return value[0]
|
||||||
|
|
||||||
|
archive = io.BytesIO()
|
||||||
|
assemble_parts(stored[0], archive, get)
|
||||||
|
return archive.getvalue()
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, traceback):
|
||||||
|
errors = []
|
||||||
|
if self.submitted:
|
||||||
|
try:
|
||||||
|
self.lane.ax("delete", "task", self.name)
|
||||||
|
self.submitted = False
|
||||||
|
except Exception as error:
|
||||||
|
errors.append(exception_reason(error))
|
||||||
|
for key in self.keys:
|
||||||
|
try:
|
||||||
|
self.lane.store._delete(key)
|
||||||
|
except Exception as error:
|
||||||
|
errors.append(exception_reason(error))
|
||||||
|
if self.slot and not errors:
|
||||||
|
AX_SLOTS.release()
|
||||||
|
self.slot = False
|
||||||
|
if errors:
|
||||||
|
reason = "executor cleanup failed: " + ", ".join(errors)
|
||||||
|
logging.getLogger(__name__).error("%s", reason)
|
||||||
|
if exc is not None:
|
||||||
|
exc.executor_cleanup_reason = reason
|
||||||
|
exc.args = (str(exc) + "; " + reason,)
|
||||||
|
return False
|
||||||
|
raise RuntimeError(reason)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Dispatch the packaged no-model probe using the shared engine/replay transport.
|
||||||
|
|
||||||
|
python -m benchmarks.ax.search_probe --lane ax --image IMAGE@sha256:DIGEST --out /tmp/probe
|
||||||
|
For kind, select --lane kind and supply its --kubeconfig; Docker uses the same PUT protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from benchmarks.ax.batch import AXLane, DockerLane, Item, Workload, dispatch, validate_image
|
||||||
|
|
||||||
|
|
||||||
|
def validate_result(data: bytes, output: Path) -> None:
|
||||||
|
if len(data) > 1024 * 1024:
|
||||||
|
raise ValueError("oversized probe result")
|
||||||
|
record = json.loads(data)
|
||||||
|
if not isinstance(record, dict) or record.get("status") not in ("ok", "instrument_failure"):
|
||||||
|
raise ValueError("invalid probe result")
|
||||||
|
if record["status"] == "ok" and (
|
||||||
|
set(record.get("oracles", {})) != {"sql", "path", "command", "ssrf", "xss"}
|
||||||
|
or record.get("isolation_check", {}).get("status") not in ("ok", "skipped")
|
||||||
|
):
|
||||||
|
raise ValueError("incomplete probe")
|
||||||
|
(output / "result.json").write_text(json.dumps(record))
|
||||||
|
|
||||||
|
|
||||||
|
def probe_workload(image: str, deadline: float = 900) -> Workload:
|
||||||
|
validate_image(image)
|
||||||
|
return Workload(image, frozenset({"RESULT_URL"}), deadline, validate_result, kind="search-probe")
|
||||||
|
|
||||||
|
|
||||||
|
def probe_item(image: str) -> Item:
|
||||||
|
return Item(
|
||||||
|
"v1",
|
||||||
|
{},
|
||||||
|
"result.json",
|
||||||
|
("python3", "-m", "openultrasast.search.probe"),
|
||||||
|
{},
|
||||||
|
hashlib.sha256(("search-probe-v1:" + image).encode()).hexdigest(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_probe(args) -> bool:
|
||||||
|
workload = probe_workload(args.image, args.deadline)
|
||||||
|
transport = (DockerLane if args.lane == "docker" else AXLane)(args, workload)
|
||||||
|
return dispatch([probe_item(args.image)], workload, args.out, {args.lane: transport}, lanes=(args.lane,), max_attempts=1)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--image", required=True)
|
||||||
|
parser.add_argument("--lane", choices=("ax", "kind", "docker"), default="ax")
|
||||||
|
parser.add_argument("--out", required=True, type=Path)
|
||||||
|
parser.add_argument("--deadline", type=float, default=900)
|
||||||
|
parser.add_argument("--ax-bin", default="ax")
|
||||||
|
parser.add_argument("--atespace", default="default")
|
||||||
|
parser.add_argument("--kubeconfig")
|
||||||
|
parser.add_argument("--docker-image-bytes", type=int)
|
||||||
|
return 1 if run_probe(parser.parse_args()) else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Dispatch three fresh key-free AX tasks for each verification side."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from benchmarks.ax.batch import AXLane, CapacityDeadline, Item, SearchExecutorTask, Workload, validate_image
|
||||||
|
from openultrasast.search.budget import verification_run_seconds
|
||||||
|
from openultrasast.search.verify import RunObservation, SideRecord
|
||||||
|
from openultrasast.search.verify_task import MAX_RESULT_BYTES, pack_checkout
|
||||||
|
|
||||||
|
|
||||||
|
def side_record(value):
|
||||||
|
if not isinstance(value, dict) or value.get("isolation_mode") != "task-boundary":
|
||||||
|
raise ValueError("missing task-boundary attestation")
|
||||||
|
fields = SideRecord.__dataclass_fields__
|
||||||
|
# AXLane adds transport metadata outside the task record.
|
||||||
|
data = {key: value[key] for key in fields if key in value}
|
||||||
|
if data.get("outcome") not in {"observed", "could_not_run", "could_not_build", "no_oracle", "inconclusive"}:
|
||||||
|
raise ValueError("invalid side outcome")
|
||||||
|
runs = data.get("runs")
|
||||||
|
if not isinstance(runs, list) or len(runs) > 1:
|
||||||
|
raise ValueError("task must return one repetition")
|
||||||
|
for run in runs:
|
||||||
|
if not isinstance(run, dict):
|
||||||
|
raise ValueError("invalid observation")
|
||||||
|
if type(run.get("observed")) is not bool or not isinstance(run.get("evidence"), str):
|
||||||
|
raise ValueError("invalid observation")
|
||||||
|
if data["outcome"] == "observed" and len(runs) != 1:
|
||||||
|
raise ValueError("missing completed observation")
|
||||||
|
for number in [
|
||||||
|
data.get("elapsed_seconds"),
|
||||||
|
*(data.get(k, 0) for k in ("build_seconds", "ready_seconds", "run_seconds", "scratch_peak_bytes")),
|
||||||
|
*(run.get("elapsed_seconds") for run in runs),
|
||||||
|
]:
|
||||||
|
if type(number) not in (int, float) or not math.isfinite(number) or number < 0:
|
||||||
|
raise ValueError("invalid observation measurement")
|
||||||
|
if not isinstance(data.get("reason"), str):
|
||||||
|
raise ValueError("invalid side reason")
|
||||||
|
data["runs"] = tuple(RunObservation(**run) for run in runs)
|
||||||
|
return SideRecord(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_result(data, output):
|
||||||
|
if len(data) > MAX_RESULT_BYTES:
|
||||||
|
raise ValueError("oversized side result")
|
||||||
|
side_record(json.loads(data))
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
(output / "result.json").write_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
|
class AXSideDispatcher:
|
||||||
|
"""Host adapter passed as verify(task_dispatcher=...); never caches runs."""
|
||||||
|
|
||||||
|
def __init__(self, args, image, output, *, lane=None, prepare=None):
|
||||||
|
validate_image(image)
|
||||||
|
self.output = Path(output)
|
||||||
|
self.args, self.image, self.lane = args, image, lane
|
||||||
|
self.prepare = prepare
|
||||||
|
|
||||||
|
def __call__(self, *, side, demo, family, timeout_seconds, fresh_task):
|
||||||
|
if fresh_task is not True or side.command or side.build_command or side.dependencies:
|
||||||
|
raise ValueError("verification accepts checkout and declarative inputs only")
|
||||||
|
# The trusted host selects an executor for this side. Build/package work
|
||||||
|
# finishes there before any verifier worker slot is acquired.
|
||||||
|
workload = Workload(
|
||||||
|
self.image,
|
||||||
|
frozenset({"VERIFY_INPUT_URL", "RESULT_URL"}),
|
||||||
|
verification_run_seconds(demo, timeout_seconds) + 60,
|
||||||
|
validate_result,
|
||||||
|
kind="search-verify",
|
||||||
|
)
|
||||||
|
lane = self.lane or AXLane(self.args, workload)
|
||||||
|
spec = {"demo": demo, "family": family, "timeout_seconds": timeout_seconds}
|
||||||
|
try:
|
||||||
|
if self.prepare is not None:
|
||||||
|
archive = self.prepare(side, spec)
|
||||||
|
else:
|
||||||
|
with SearchExecutorTask(lane, pack_checkout(side.checkout)) as executor:
|
||||||
|
archive = executor.prepare(spec)
|
||||||
|
except (ValueError, CapacityDeadline) as exc:
|
||||||
|
return SideRecord("could_not_build", (), 0, str(exc), isolation_mode="task-boundary")
|
||||||
|
records = []
|
||||||
|
for _ in range(3):
|
||||||
|
identity = uuid.uuid4().hex
|
||||||
|
item = Item(
|
||||||
|
identity,
|
||||||
|
{"VERIFY_INPUT_URL": archive},
|
||||||
|
"result.json",
|
||||||
|
("python3", "-m", "openultrasast.search.verify_task"),
|
||||||
|
{},
|
||||||
|
hashlib.sha256(archive).hexdigest(),
|
||||||
|
)
|
||||||
|
output = self.output / identity
|
||||||
|
output.mkdir(parents=True)
|
||||||
|
raw = lane(item, output, 1)
|
||||||
|
# Roundtrip enforces JSON types for test adapters as for remote tasks.
|
||||||
|
if raw.get("status") == "capacity_timeout":
|
||||||
|
return SideRecord("could_not_run", (), 0, "AX capacity deadline", isolation_mode="task-boundary")
|
||||||
|
result = side_record(json.loads(json.dumps(raw)))
|
||||||
|
records.append(result)
|
||||||
|
if result.outcome != "observed":
|
||||||
|
break
|
||||||
|
last = records[-1]
|
||||||
|
return SideRecord(
|
||||||
|
last.outcome,
|
||||||
|
tuple(run for r in records for run in r.runs),
|
||||||
|
sum(r.elapsed_seconds for r in records),
|
||||||
|
last.reason,
|
||||||
|
sum(r.build_seconds for r in records),
|
||||||
|
sum(r.ready_seconds for r in records),
|
||||||
|
sum(r.run_seconds for r in records),
|
||||||
|
max(r.scratch_peak_bytes for r in records),
|
||||||
|
isolation_mode="task-boundary",
|
||||||
|
chromium_no_sandbox=any(r.chromium_no_sandbox for r in records),
|
||||||
|
phase=last.phase,
|
||||||
|
exit_code=last.exit_code,
|
||||||
|
stderr=last.stderr,
|
||||||
|
stdout=last.stdout,
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"experiment": "exp-003-precision-bound-blocking",
|
||||||
|
"registered": "2026-10-03",
|
||||||
|
"store": "s3://sast-memory",
|
||||||
|
"manifest": "plane/experiments/exp-003-precision-bound-blocking.yaml",
|
||||||
|
"manifest_digest": "5917f9a12035f09eae76978f3b5827b9fe59c8a9a5275f9ab54c3c1b55e8db3b",
|
||||||
|
"pin": "dd7ebc119b5ad8e77c05d418eed9cbd46e193587",
|
||||||
|
"units_status": "frozen",
|
||||||
|
"units_file": "plane/experiments/exp-003-precision-bound-blocking.units.jsonl",
|
||||||
|
"units_digest": "338de998b1cfa36857cd793dbc4cf7fd0a13ab517a91f6790335881e941b3006",
|
||||||
|
"units": {
|
||||||
|
"total": 353,
|
||||||
|
"paired": 244,
|
||||||
|
"groups": 146,
|
||||||
|
"by_family": {
|
||||||
|
"access_control": {"units": 41, "positives": 25, "paired": 32, "paired_positives": 16, "groups": 24},
|
||||||
|
"deserialization": {"units": 48, "positives": 31, "paired": 34, "paired_positives": 17, "groups": 31},
|
||||||
|
"injection": {"units": 119, "positives": 63, "paired": 76, "paired_positives": 38, "groups": 34},
|
||||||
|
"output_encoding": {"units": 45, "positives": 29, "paired": 32, "paired_positives": 16, "groups": 27},
|
||||||
|
"path": {"units": 39, "positives": 23, "paired": 32, "paired_positives": 16, "groups": 23},
|
||||||
|
"untrusted_destination": {"units": 61, "positives": 37, "paired": 38, "paired_positives": 19, "groups": 36}
|
||||||
|
},
|
||||||
|
"note": "frozen model-free on the 937-example static memory; the outer folds (k 5, seed 0, compile fraction 0.25) are the incumbent evaluation's; injection and access_control equal exp-002's 160 units"
|
||||||
|
},
|
||||||
|
"kind": "rule: one zero-cost scoring pass of the six slice-2 incumbent programs; arms differ only in the BLOCK rule",
|
||||||
|
"zero_cost": "responses replayed from the response cache only, behind a meter with zero USD and call ceilings around a refusing client; a unit with any uncached response is excluded and counted; the run is void unless the meter reads 0 calls and $0",
|
||||||
|
"arms": {
|
||||||
|
"A": "today's rule: Platt map cross-fitted per family; BLOCK only when the calibration check holds (slope CI contains 1, intercept CI contains 0, ECE <= 0.05); BLOCK point nested on the other folds' calibrated p (Wilson LB >= 0.95)",
|
||||||
|
"B": "selective blocking: per family and outer fold, threshold chosen on the family's paired units in the other four folds as the lowest raw score t with Wilson 95% LB of flagged precision >= 0.95 (unsure verdicts never flag); none -> no BLOCK on that fold",
|
||||||
|
"C": "secondary, non-deciding: as B with point precision >= 0.95 over at least 10 training flags"
|
||||||
|
},
|
||||||
|
"primary_metric": "held-out (outer-fold) Wilson 95% lower bound of flagged precision, per family, arm B, paired population",
|
||||||
|
"populations": {"primary": "paired (both sides of the candidate present; excludes every positives-only group and any unpaired unit)", "secondary": ["pooled (every unit)"]},
|
||||||
|
"secondary_metrics": ["flagged_precision", "coverage (flagged true positives / positives)", "flagged", "repository-cluster bootstrap 95% CI of flagged precision"],
|
||||||
|
"decision_rule": "REJECT B if in any family B flags held-out paired units at a point precision < 0.95; else ADOPT B if in some family B's held-out paired Wilson LB >= 0.95; else INCONCLUSIVE (the incumbent rule stays)",
|
||||||
|
"m4_reading": "a family reaches the M4 precision bar on held-out repositories when its held-out paired Wilson LB >= 0.95; point precision reported beside it",
|
||||||
|
"power": "a Wilson LB of 0.95 needs >= 73 flagged units with no false flag; the largest family holds 38 paired positives over all five folds, so arm B cannot offer a BLOCK in any family by construction; expected verdict inconclusive",
|
||||||
|
"status": "run and analysed 2026-10-03: inconclusive -- no arm flags any held-out unit (result.json); meter 0 client calls, $0.0; 142 of 353 units excluded for an uncached response"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,981 @@
|
|||||||
|
{
|
||||||
|
"arms": {
|
||||||
|
"A": {
|
||||||
|
"label": "today's rule -- Platt map cross-fitted per family, BLOCK only when the calibration check holds, BLOCK point nested on the other folds' calibrated p",
|
||||||
|
"rule": {
|
||||||
|
"name": "calibrated_block"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"label": "selective blocking -- per-family raw-score threshold from the other folds' paired units, lowest t with Wilson 95% LB of flagged precision >= 0.95",
|
||||||
|
"rule": {
|
||||||
|
"bound": "wilson",
|
||||||
|
"name": "precision_bound_block",
|
||||||
|
"precision": 0.95,
|
||||||
|
"select_on": "paired"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"label": "secondary, non-deciding -- as B with point precision >= 0.95 over at least 10 training flags",
|
||||||
|
"rule": {
|
||||||
|
"bound": "point",
|
||||||
|
"min_flagged": 10,
|
||||||
|
"name": "precision_bound_block",
|
||||||
|
"precision": 0.95,
|
||||||
|
"select_on": "paired"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code_commit": "7bb97ad0bca62b5f986c5768297d8b8fca50fae4",
|
||||||
|
"decision": "inconclusive",
|
||||||
|
"experiment": "exp-003-precision-bound-blocking",
|
||||||
|
"families": {
|
||||||
|
"access_control": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.077,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 16,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 16,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 32
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 24,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 25,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 41
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 16,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 16,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 32
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 24,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 25,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 41
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 16,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 16,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 32
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 24,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 25,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 41
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 41,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 12,
|
||||||
|
"outer-1": 13,
|
||||||
|
"outer-2": 13,
|
||||||
|
"outer-3": 13,
|
||||||
|
"outer-4": 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deserialization": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": null,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 3,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 2,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 5
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 12,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 11,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 14
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 3,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 2,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 5
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 12,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 11,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 14
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 3,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 2,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 5
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 12,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 11,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 14
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 14,
|
||||||
|
"excluded_replay_miss": 34,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 0,
|
||||||
|
"outer-2": 2,
|
||||||
|
"outer-3": 2,
|
||||||
|
"outer-4": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"injection": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.1067,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 26,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 38,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 76
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 34,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 63,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 119
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 26,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 38,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 76
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 34,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 63,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 119
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 26,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 38,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 76
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 34,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 63,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 119
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 119,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 29,
|
||||||
|
"outer-1": 33,
|
||||||
|
"outer-2": 33,
|
||||||
|
"outer-3": 26,
|
||||||
|
"outer-4": 31
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_encoding": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": null,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 8,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 8,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 16
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 15,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 16,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 24
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 8,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 8,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 16
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 15,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 16,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 24
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 8,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 8,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 16
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 15,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 16,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 24
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 24,
|
||||||
|
"excluded_replay_miss": 21,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 7,
|
||||||
|
"outer-1": 6,
|
||||||
|
"outer-2": 6,
|
||||||
|
"outer-3": 7,
|
||||||
|
"outer-4": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"A": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": null,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 0,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 0,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 0
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": null,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 0,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 0,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": null,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 0,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 0,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 0
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": null,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 0,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 0,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": null,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 0,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 0,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 0
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": null,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 0,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 0,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 0,
|
||||||
|
"excluded_replay_miss": 39,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"untrusted_destination": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": null,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 6,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 4,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 9
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 9,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 7,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 13
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 6,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 4,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 9
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 9,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 7,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 13
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 6,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 4,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 9
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 9,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 7,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 13
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 13,
|
||||||
|
"excluded_replay_miss": 48,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 4,
|
||||||
|
"outer-1": 3,
|
||||||
|
"outer-2": 3,
|
||||||
|
"outer-3": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hypothesis": "The calibrated-probability BLOCK rule (Platt map + calibration gate, ECE <= 0.05) is the wrong instrument for M4, which asks for precision on what is flagged. Selective blocking -- a per-family raw-score threshold chosen on training folds only as the lowest one whose Wilson 95% lower bound on flagged precision is >= 0.95 -- reaches the M4 precision bar on held-out repositories where the calibrated rule offers no BLOCK.\n",
|
||||||
|
"instrument_check": {
|
||||||
|
"families": {
|
||||||
|
"access_control": {
|
||||||
|
"distinct_s": 30,
|
||||||
|
"paired": {
|
||||||
|
"blockable": 28,
|
||||||
|
"positives_before_first_negative": 10
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"blockable": 37,
|
||||||
|
"positives_before_first_negative": 19
|
||||||
|
},
|
||||||
|
"verdicts": {
|
||||||
|
"not_vulnerable": 19,
|
||||||
|
"unsure": 4,
|
||||||
|
"vulnerable": 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deserialization": {
|
||||||
|
"distinct_s": 5,
|
||||||
|
"paired": {
|
||||||
|
"blockable": 4,
|
||||||
|
"positives_before_first_negative": 1
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"blockable": 13,
|
||||||
|
"positives_before_first_negative": 10
|
||||||
|
},
|
||||||
|
"verdicts": {
|
||||||
|
"not_vulnerable": 2,
|
||||||
|
"unsure": 1,
|
||||||
|
"vulnerable": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"injection": {
|
||||||
|
"distinct_s": 48,
|
||||||
|
"paired": {
|
||||||
|
"blockable": 50,
|
||||||
|
"positives_before_first_negative": 3
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"blockable": 77,
|
||||||
|
"positives_before_first_negative": 11
|
||||||
|
},
|
||||||
|
"verdicts": {
|
||||||
|
"not_vulnerable": 46,
|
||||||
|
"unsure": 42,
|
||||||
|
"vulnerable": 31
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_encoding": {
|
||||||
|
"distinct_s": 14,
|
||||||
|
"paired": {
|
||||||
|
"blockable": 11,
|
||||||
|
"positives_before_first_negative": 0
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"blockable": 19,
|
||||||
|
"positives_before_first_negative": 8
|
||||||
|
},
|
||||||
|
"verdicts": {
|
||||||
|
"not_vulnerable": 9,
|
||||||
|
"unsure": 5,
|
||||||
|
"vulnerable": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"untrusted_destination": {
|
||||||
|
"distinct_s": 9,
|
||||||
|
"paired": {
|
||||||
|
"blockable": 4,
|
||||||
|
"positives_before_first_negative": 1
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"blockable": 8,
|
||||||
|
"positives_before_first_negative": 4
|
||||||
|
},
|
||||||
|
"verdicts": {
|
||||||
|
"not_vulnerable": 3,
|
||||||
|
"unsure": 5,
|
||||||
|
"vulnerable": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"note": "descriptive, all folds together, ranking by raw score s among non-unsure verdicts; not nested, chooses nothing. It shows the scores are real and the zero is the rule's, not an unread input."
|
||||||
|
},
|
||||||
|
"manifest_digest": "5917f9a12035f09eae76978f3b5827b9fe59c8a9a5275f9ab54c3c1b55e8db3b",
|
||||||
|
"meter": {
|
||||||
|
"client_calls": 0,
|
||||||
|
"decided": 211,
|
||||||
|
"excluded_replay_miss": 142,
|
||||||
|
"folds_digest": "a1f78e141a572a4adcc9cd57eb8a784c440c177f0b60d5ef5bed9e94c8753c5f",
|
||||||
|
"metered_usd": 0.0,
|
||||||
|
"replay_priced_usd_not_spent": 0.582876,
|
||||||
|
"replayed_responses": 1056
|
||||||
|
},
|
||||||
|
"min_flagged_for_bound": 73,
|
||||||
|
"overall": {
|
||||||
|
"A": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 59,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 68,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 138
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 89,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 122,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 211
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 59,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 68,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 138
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 89,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 122,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 211
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 59,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 68,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 138
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 89,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 122,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 211
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"populations": {
|
||||||
|
"primary": "paired",
|
||||||
|
"secondary": [
|
||||||
|
"pooled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": [
|
||||||
|
{
|
||||||
|
"name": "flagged_precision_lb",
|
||||||
|
"target": 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reason": "B offers no BLOCK on held-out paired units in any family; the incumbent stays",
|
||||||
|
"resamples": 2000,
|
||||||
|
"secondary": [
|
||||||
|
{
|
||||||
|
"name": "flagged_precision"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coverage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flagged"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spend_usd": {
|
||||||
|
"score": 0.0
|
||||||
|
},
|
||||||
|
"target_precision": 0.95,
|
||||||
|
"units": {
|
||||||
|
"by_label": {
|
||||||
|
"0": 89,
|
||||||
|
"1": 122
|
||||||
|
},
|
||||||
|
"evaluated": 211,
|
||||||
|
"excluded_replay_miss": 142,
|
||||||
|
"frozen": 353,
|
||||||
|
"groups": 89,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_evaluated": 138,
|
||||||
|
"unsure": 57
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Instrument check for exp-004: are the scores real, and how clean is the pooled top of the ranking?
|
||||||
|
|
||||||
|
Reads the run's arm_outcome rows from the memory store (OUSAST_MEMORY) and the frozen units file; makes no model call.
|
||||||
|
usage: python benchmarks/experiments/exp-004-pooled-block-gate/instrument_check.py (from the repository root)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openultrasast.config import load_dotenv
|
||||||
|
from openultrasast.learn import experiments as ex
|
||||||
|
from openultrasast.plane.memory import open_store
|
||||||
|
|
||||||
|
EXPERIMENT = "exp-004-pooled-block-gate"
|
||||||
|
UNITS = Path(f"plane/experiments/{EXPERIMENT}.units.jsonl")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
load_dotenv(Path(".env"))
|
||||||
|
rows = [r.row for r in open_store(None).rows(repo=ex.experiment_repo(EXPERIMENT), kind="arm_outcome")]
|
||||||
|
with UNITS.open() as handle:
|
||||||
|
units = {u["unit"]: u for u in (json.loads(line) for line in handle)}
|
||||||
|
print("arm_outcome rows", len(rows), "units file", len(units))
|
||||||
|
by_family: dict[str, list[tuple[float | None, int, str | None, str]]] = collections.defaultdict(list)
|
||||||
|
for row in rows:
|
||||||
|
unit = units.get(row.get("unit"))
|
||||||
|
if unit:
|
||||||
|
by_family[unit["family"]].append((row.get("s"), unit["label"], unit.get("pair"), str(row.get("verdict") or "")))
|
||||||
|
for family, xs in sorted(by_family.items()):
|
||||||
|
scored = [x for x in xs if x[0] is not None]
|
||||||
|
pairs: dict[str, dict[int, float]] = collections.defaultdict(dict)
|
||||||
|
for score, label, pair, _ in scored:
|
||||||
|
if pair:
|
||||||
|
pairs[pair][label] = score
|
||||||
|
full = [p for p in pairs.values() if 0 in p and 1 in p]
|
||||||
|
wins = sum(1 for p in full if p[1] > p[0])
|
||||||
|
ties = sum(1 for p in full if p[1] == p[0])
|
||||||
|
top = sorted((x for x in scored if x[2]), key=lambda x: -(x[0] or 0.0))
|
||||||
|
first_negative = next((i for i, x in enumerate(top) if x[1] == 0), len(top))
|
||||||
|
print(
|
||||||
|
f"{family}: n={len(xs)} distinct_scores={len({round(x[0] or 0.0, 4) for x in scored})} "
|
||||||
|
f"unsure={sum(1 for x in xs if x[3] == 'unsure')} pairs={len(full)} within_pair_win={wins} tie={ties} "
|
||||||
|
f"pos_above_first_neg={first_negative}"
|
||||||
|
)
|
||||||
|
pooled = sorted((x for xs in by_family.values() for x in xs if x[0] is not None and x[2]), key=lambda x: -(x[0] or 0.0))
|
||||||
|
print("pooled raw top-30 labels:", "".join(str(x[1]) for x in pooled[:30]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"experiment": "exp-004-pooled-block-gate",
|
||||||
|
"date": "2026-10-03",
|
||||||
|
"fill": {
|
||||||
|
"client_calls": 2073,
|
||||||
|
"usd": 1.238223,
|
||||||
|
"ceiling_usd": 2.0,
|
||||||
|
"still_missing": 0,
|
||||||
|
"dry_run_estimate_usd": 5.582,
|
||||||
|
"note": "the dry-run estimator over-priced 4.5x (no prompt-cache pricing); measured spend is the meter's"
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"units": 501,
|
||||||
|
"client_calls": 0,
|
||||||
|
"usd": 0.0,
|
||||||
|
"replayed": 2509,
|
||||||
|
"replay_miss": 0
|
||||||
|
},
|
||||||
|
"verdict": "inconclusive",
|
||||||
|
"reason": "no t_block on any outer fold, pooled or per family; B offers no BLOCK",
|
||||||
|
"instrument_check": {
|
||||||
|
"per_family": {
|
||||||
|
"access_control": {
|
||||||
|
"n": 66,
|
||||||
|
"distinct_scores": 41,
|
||||||
|
"unsure": 5,
|
||||||
|
"pairs": 28,
|
||||||
|
"within_pair_wins": 22,
|
||||||
|
"ties": 1,
|
||||||
|
"positives_above_first_negative": 12
|
||||||
|
},
|
||||||
|
"deserialization": {
|
||||||
|
"n": 64,
|
||||||
|
"distinct_scores": 38,
|
||||||
|
"unsure": 7,
|
||||||
|
"pairs": 25,
|
||||||
|
"within_pair_wins": 18,
|
||||||
|
"ties": 4,
|
||||||
|
"positives_above_first_negative": 5
|
||||||
|
},
|
||||||
|
"injection": {
|
||||||
|
"n": 149,
|
||||||
|
"distinct_scores": 52,
|
||||||
|
"unsure": 54,
|
||||||
|
"pairs": 53,
|
||||||
|
"within_pair_wins": 30,
|
||||||
|
"ties": 17,
|
||||||
|
"positives_above_first_negative": 6
|
||||||
|
},
|
||||||
|
"output_encoding": {
|
||||||
|
"n": 69,
|
||||||
|
"distinct_scores": 31,
|
||||||
|
"unsure": 32,
|
||||||
|
"pairs": 28,
|
||||||
|
"within_pair_wins": 18,
|
||||||
|
"ties": 6,
|
||||||
|
"positives_above_first_negative": 2
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"n": 66,
|
||||||
|
"distinct_scores": 43,
|
||||||
|
"unsure": 15,
|
||||||
|
"pairs": 29,
|
||||||
|
"within_pair_wins": 25,
|
||||||
|
"ties": 3,
|
||||||
|
"positives_above_first_negative": 13
|
||||||
|
},
|
||||||
|
"untrusted_destination": {
|
||||||
|
"n": 87,
|
||||||
|
"distinct_scores": 36,
|
||||||
|
"unsure": 33,
|
||||||
|
"pairs": 32,
|
||||||
|
"within_pair_wins": 18,
|
||||||
|
"ties": 4,
|
||||||
|
"positives_above_first_negative": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pooled_raw_top30_labels": "111111111110111110111111110111",
|
||||||
|
"reading": "scores are real (31-52 distinct values per family); pooled raw top 30 holds 27 positives (0.90), first negative at rank 12; the Wilson bound needs >= 73 flags with no error, so precision at the top, not data volume alone, now binds"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"experiment": "exp-004-pooled-block-gate",
|
||||||
|
"registered": "2026-10-03",
|
||||||
|
"store": "s3://sast-memory",
|
||||||
|
"manifest": "plane/experiments/exp-004-pooled-block-gate.yaml",
|
||||||
|
"manifest_digest": "87fceb6580c6de629d6a6089eb5888cdfc4df0f791576184f987c14c192d85f5",
|
||||||
|
"pin": "2ad9c1db7ae673c2a2e48a0afadcaa795dfb1ced",
|
||||||
|
"units_status": "pending",
|
||||||
|
"units_file": "plane/experiments/exp-004-pooled-block-gate.units.jsonl",
|
||||||
|
"units_digest": "",
|
||||||
|
"units": "frozen at run time by `ousast learn experiment units` from the memory after the negatives harvest (merged in 5b2bd9b, 1133 static examples), committed, and registered again before the run; until then the run refuses (check_registered: the units file does not exist)",
|
||||||
|
"requirement": "learned-decision-engine Req 6.4, amended 2026-10-03 by maintainer decision (refines the BLOCK rule of Req 7.3 and 7.4)",
|
||||||
|
"kind": "rule: one zero-cost scoring pass of the six slice-2 incumbent programs; arms differ only in the BLOCK rule",
|
||||||
|
"zero_cost": "the run replays the response cache behind a zero-ceiling meter; a unit with any uncached response is excluded and counted. The harvest changed the memory the programs retrieve from, so a paid cache fill, approved separately with its own ceiling, must precede the run",
|
||||||
|
"normalisation": {
|
||||||
|
"choice": "per-family Platt scaling: learn.calibrate.fit_map with its existing floors (own slope and intercept per family at >= 20 positive and negative repository groups, a pooled slope with a per-family intercept at >= 10, no map below that and so no BLOCK for that family on that fold), fitted on the training folds' paired units only",
|
||||||
|
"why": "a rank-to-quantile map gives every family the same flag rate whatever its precision, so one pooled threshold would flag the top slice of a weak family as readily as of a strong one; Platt puts each family on the scale of its own training precision, which a pooled precision bound needs. The map is a normaliser only: precision is shown by the held-out bound, not by calibration (exp-003: ECE <= 0.05 does not hold per family)"
|
||||||
|
},
|
||||||
|
"arms": {
|
||||||
|
"A": "today's rule: Platt map cross-fitted per family; BLOCK only when the calibration check holds; BLOCK point nested on the other folds' calibrated p (Wilson LB >= 0.95)",
|
||||||
|
"B": "pooled gate: per outer fold j, training units normalised by an inner cross-fit; t_block = the lowest normalised score whose Wilson 95% LB of precision over the training paired flags of all families together is >= 0.95 (unsure never flags); t_quiet per family = the highest normalised score keeping >= 90% of that family's training paired positives; fold j's units BLOCK at p >= t_block, ADVISORY at p >= t_quiet (or without a map or t_quiet), silent below"
|
||||||
|
},
|
||||||
|
"family_floor": {
|
||||||
|
"F": 10,
|
||||||
|
"rule": "a family may block only if its own held-out paired flags (all five folds) number >= 10 with zero errors; otherwise its flags are reported as ADVISORY. The floor uses held-out labels to decide which families block and moves no threshold",
|
||||||
|
"why": "zero errors in 10 flags is unlikely for a family much worse than the bar (probability 0.056 at precision 0.75, 0.107 at 0.80); a larger F is out of reach of the smaller families (16-19 paired positives in exp-003's units over all folds); 10 is exp-003 arm C's training minimum"
|
||||||
|
},
|
||||||
|
"primary_metric": "held-out (outer-fold) Wilson 95% lower bound of flagged precision over arm B's paired flags of all families together, before the family floor (so dropping a failing family cannot raise it)",
|
||||||
|
"populations": {"primary": "paired (both sides of the candidate present)", "secondary": ["pooled (every unit)"]},
|
||||||
|
"secondary_metrics": ["per-family flagged precision and Wilson LB", "the family-floor table", "quiet_recall per family (held-out positives at or above t_quiet / positives) against 0.90", "after-floor totals (what users would get)", "repository-cluster bootstrap 95% CI of flagged precision", "coverage", "flagged"],
|
||||||
|
"decision_rule": "on arm B's held-out paired flags of all families together, before the floor: REJECT B if their point precision < 0.95; else ADOPT B if their Wilson 95% LB >= 0.95 and at least one family passes the floor (>= 10 held-out paired flags, zero errors) -- those families block, every other family stays advisory; else INCONCLUSIVE (the incumbent rule stays)",
|
||||||
|
"power": "the pooled bound needs >= 73 flags with no error (110 with one, 142 with two). On exp-003's units (all 353 scored, result-complete-cache.json) few paired positives rank above the first negative per family on the raw score (access control 10, deserialization 7, injection 2, output encoding 0, path 8, untrusted destination 5); normalisation keeps each family's order, so the pooled top set reaches 73 only if the harvest's pairs separate better. An inconclusive verdict is a plausible outcome",
|
||||||
|
"tests": "tests/test_learn_rules.py: test_one_pooled_threshold_blocks_where_no_family_alone_reaches_the_bound, test_no_held_out_label_moves_a_threshold_or_the_map, test_a_family_with_one_held_out_error_cannot_block, test_quiet_threshold_keeps_ninety_percent_of_positives_and_nothing_is_silenced_blind, test_the_pooled_verdict_follows_the_registered_rule, test_the_exp004_manifest_registers_the_procedure_before_its_units",
|
||||||
|
"status": "registered 2026-10-03, units pending, not run"
|
||||||
|
}
|
||||||
@@ -0,0 +1,822 @@
|
|||||||
|
{
|
||||||
|
"arms": {
|
||||||
|
"A": {
|
||||||
|
"label": "today's rule -- Platt map cross-fitted per family, BLOCK only when the calibration check holds, BLOCK point nested on the other folds' calibrated p",
|
||||||
|
"rule": {
|
||||||
|
"name": "calibrated_block"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"label": "pooled gate -- one t_block over all families on a per-family Platt score (training folds, inner cross-fit), t_quiet per family at 90% recall, family floor 10 clean held-out flags",
|
||||||
|
"rule": {
|
||||||
|
"floor": 10,
|
||||||
|
"name": "pooled_block_gate",
|
||||||
|
"normalise": "platt",
|
||||||
|
"precision": 0.95,
|
||||||
|
"recall": 0.9,
|
||||||
|
"select_on": "paired"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code_commit": "90a2642019df414cb67b5dcea6e4ec235f35650a",
|
||||||
|
"decision": "inconclusive",
|
||||||
|
"experiment": "exp-004-pooled-block-gate",
|
||||||
|
"families": {
|
||||||
|
"access_control": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.0715,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 28,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 28,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 56
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 37,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 38,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 66
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"floor": {
|
||||||
|
"errors": 0,
|
||||||
|
"flagged": 0,
|
||||||
|
"floor": 10,
|
||||||
|
"may_block": false
|
||||||
|
},
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 28,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 28,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 56
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 37,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 38,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 66
|
||||||
|
},
|
||||||
|
"quiet_recall": {
|
||||||
|
"paired": 0.8929,
|
||||||
|
"pooled": 0.9211
|
||||||
|
},
|
||||||
|
"t_block_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
},
|
||||||
|
"t_quiet_by_fold": {
|
||||||
|
"outer-0": 0.44900641988143913,
|
||||||
|
"outer-1": 0.3723384028044276,
|
||||||
|
"outer-2": 0.4202772526614184,
|
||||||
|
"outer-3": 0.4481849709797749,
|
||||||
|
"outer-4": 0.36346174463707154
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 66,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 22,
|
||||||
|
"outer-1": 22,
|
||||||
|
"outer-2": 22,
|
||||||
|
"outer-3": 23,
|
||||||
|
"outer-4": 23
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deserialization": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.0725,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 25,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 25,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 50
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 39,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 39,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 64
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"floor": {
|
||||||
|
"errors": 0,
|
||||||
|
"flagged": 0,
|
||||||
|
"floor": 10,
|
||||||
|
"may_block": false
|
||||||
|
},
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 25,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 25,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 50
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 39,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 39,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 64
|
||||||
|
},
|
||||||
|
"quiet_recall": {
|
||||||
|
"paired": 0.88,
|
||||||
|
"pooled": 0.9231
|
||||||
|
},
|
||||||
|
"t_block_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
},
|
||||||
|
"t_quiet_by_fold": {
|
||||||
|
"outer-0": 0.17507194490500488,
|
||||||
|
"outer-1": 0.22766488322270723,
|
||||||
|
"outer-2": 0.2163745870743959,
|
||||||
|
"outer-3": 0.3112201878893598,
|
||||||
|
"outer-4": 0.363243722721776
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 64,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 20,
|
||||||
|
"outer-1": 20,
|
||||||
|
"outer-2": 20,
|
||||||
|
"outer-3": 20,
|
||||||
|
"outer-4": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"injection": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.1042,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 41,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 53,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 106
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 49,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 78,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 149
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"floor": {
|
||||||
|
"errors": 0,
|
||||||
|
"flagged": 0,
|
||||||
|
"floor": 10,
|
||||||
|
"may_block": false
|
||||||
|
},
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 41,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 53,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 106
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 49,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 78,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 149
|
||||||
|
},
|
||||||
|
"quiet_recall": {
|
||||||
|
"paired": 0.8679,
|
||||||
|
"pooled": 0.8974
|
||||||
|
},
|
||||||
|
"t_block_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
},
|
||||||
|
"t_quiet_by_fold": {
|
||||||
|
"outer-0": 0.287983504097999,
|
||||||
|
"outer-1": 0.17524866355153343,
|
||||||
|
"outer-2": 0.12918182665453967,
|
||||||
|
"outer-3": 0.14521040470628938,
|
||||||
|
"outer-4": 0.23005714687697673
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 149,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 38,
|
||||||
|
"outer-1": 44,
|
||||||
|
"outer-2": 45,
|
||||||
|
"outer-3": 43,
|
||||||
|
"outer-4": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_encoding": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.0736,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 28,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 28,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 56
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 39,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 41,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 69
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"floor": {
|
||||||
|
"errors": 0,
|
||||||
|
"flagged": 0,
|
||||||
|
"floor": 10,
|
||||||
|
"may_block": false
|
||||||
|
},
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 28,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 28,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 56
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 39,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 41,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 69
|
||||||
|
},
|
||||||
|
"quiet_recall": {
|
||||||
|
"paired": 0.8571,
|
||||||
|
"pooled": 0.9024
|
||||||
|
},
|
||||||
|
"t_block_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
},
|
||||||
|
"t_quiet_by_fold": {
|
||||||
|
"outer-0": 0.3723166587726901,
|
||||||
|
"outer-1": 0.5329072757249385,
|
||||||
|
"outer-2": 0.3785066578507705,
|
||||||
|
"outer-3": 0.4389011844848602,
|
||||||
|
"outer-4": 0.43855966643440264
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 69,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 23,
|
||||||
|
"outer-1": 22,
|
||||||
|
"outer-2": 22,
|
||||||
|
"outer-3": 22,
|
||||||
|
"outer-4": 23
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.0621,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 29,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 29,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 58
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 37,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 37,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 66
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"floor": {
|
||||||
|
"errors": 0,
|
||||||
|
"flagged": 0,
|
||||||
|
"floor": 10,
|
||||||
|
"may_block": false
|
||||||
|
},
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 29,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 29,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 58
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 37,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 37,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 66
|
||||||
|
},
|
||||||
|
"quiet_recall": {
|
||||||
|
"paired": 0.8621,
|
||||||
|
"pooled": 0.8919
|
||||||
|
},
|
||||||
|
"t_block_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
},
|
||||||
|
"t_quiet_by_fold": {
|
||||||
|
"outer-0": 0.4680507207548809,
|
||||||
|
"outer-1": 0.3955717127313657,
|
||||||
|
"outer-2": 0.43694703358760895,
|
||||||
|
"outer-3": 0.4450583743716199,
|
||||||
|
"outer-4": 0.46396657833309796
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 66,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 23,
|
||||||
|
"outer-1": 23,
|
||||||
|
"outer-2": 24,
|
||||||
|
"outer-3": 23,
|
||||||
|
"outer-4": 23
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"untrusted_destination": {
|
||||||
|
"A": {
|
||||||
|
"calibration_holds": false,
|
||||||
|
"ece": 0.1143,
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 31,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 32,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 64
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 49,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 50,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 87
|
||||||
|
},
|
||||||
|
"reason_by_fold": {
|
||||||
|
"outer-0": "calibration",
|
||||||
|
"outer-1": "calibration",
|
||||||
|
"outer-2": "calibration",
|
||||||
|
"outer-3": "calibration",
|
||||||
|
"outer-4": "calibration"
|
||||||
|
},
|
||||||
|
"thresholds_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"floor": {
|
||||||
|
"errors": 0,
|
||||||
|
"flagged": 0,
|
||||||
|
"floor": 10,
|
||||||
|
"may_block": false
|
||||||
|
},
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 31,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 32,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 64
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 49,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 50,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 87
|
||||||
|
},
|
||||||
|
"quiet_recall": {
|
||||||
|
"paired": 1.0,
|
||||||
|
"pooled": 1.0
|
||||||
|
},
|
||||||
|
"t_block_by_fold": {
|
||||||
|
"outer-0": null,
|
||||||
|
"outer-1": null,
|
||||||
|
"outer-2": null,
|
||||||
|
"outer-3": null,
|
||||||
|
"outer-4": null
|
||||||
|
},
|
||||||
|
"t_quiet_by_fold": {
|
||||||
|
"outer-0": 0.13676746287025585,
|
||||||
|
"outer-1": 0.11255775703980551,
|
||||||
|
"outer-2": 0.13763698150768486,
|
||||||
|
"outer-3": 0.09787938647374639,
|
||||||
|
"outer-4": 0.13763698150768486
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"evaluated": 87,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_positives_by_training_folds": {
|
||||||
|
"outer-0": 26,
|
||||||
|
"outer-1": 26,
|
||||||
|
"outer-2": 24,
|
||||||
|
"outer-3": 26,
|
||||||
|
"outer-4": 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hypothesis": "One BLOCK threshold pooled across families on a per-family Platt-normalised score, chosen on training folds only as the lowest one whose Wilson 95% lower bound on flagged precision is >= 0.95, reaches that bound on held-out repositories' paired units, where per-family thresholds (exp-003) could not for lack of flags; a per-family floor (>= 10 held-out flags, none wrong) keeps a family that the pooling masks from blocking.\n",
|
||||||
|
"manifest_digest": "87fceb6580c6de629d6a6089eb5888cdfc4df0f791576184f987c14c192d85f5",
|
||||||
|
"min_flagged_for_bound": 73,
|
||||||
|
"overall": {
|
||||||
|
"A": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 182,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 195,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 390
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 220,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 283,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 501
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 182,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 195,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 390
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 220,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 283,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 501
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overall_after_floor": {
|
||||||
|
"B": {
|
||||||
|
"paired": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 182,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 195,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 390
|
||||||
|
},
|
||||||
|
"pooled": {
|
||||||
|
"coverage": 0.0,
|
||||||
|
"flagged": 0,
|
||||||
|
"flagged_precision": null,
|
||||||
|
"flagged_precision_ci": null,
|
||||||
|
"flagged_precision_lb": null,
|
||||||
|
"groups": 220,
|
||||||
|
"meets_target_lb": false,
|
||||||
|
"meets_target_point": false,
|
||||||
|
"positives": 283,
|
||||||
|
"true_flagged": 0,
|
||||||
|
"units": 501
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"populations": {
|
||||||
|
"primary": "paired",
|
||||||
|
"secondary": [
|
||||||
|
"pooled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": [
|
||||||
|
{
|
||||||
|
"name": "flagged_precision_lb",
|
||||||
|
"target": 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reason": "B offers no BLOCK on held-out paired units; the incumbent stays",
|
||||||
|
"resamples": 2000,
|
||||||
|
"secondary": [
|
||||||
|
{
|
||||||
|
"name": "flagged_precision"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coverage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flagged"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quiet_recall"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spend_usd": {
|
||||||
|
"score": 0.0
|
||||||
|
},
|
||||||
|
"target_precision": 0.95,
|
||||||
|
"units": {
|
||||||
|
"by_label": {
|
||||||
|
"0": 218,
|
||||||
|
"1": 283
|
||||||
|
},
|
||||||
|
"evaluated": 501,
|
||||||
|
"excluded_replay_miss": 0,
|
||||||
|
"frozen": 501,
|
||||||
|
"groups": 220,
|
||||||
|
"not_run": 0,
|
||||||
|
"paired_evaluated": 390,
|
||||||
|
"unsure": 146
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"arms": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"client_calls": 2394,
|
||||||
|
"experiment": "exp-005-graph-slice",
|
||||||
|
"families": {
|
||||||
|
"access_control": {
|
||||||
|
"cached": 0,
|
||||||
|
"conditional_retries": 330,
|
||||||
|
"estimate_method": "estimated",
|
||||||
|
"estimated_output_tokens": 300,
|
||||||
|
"estimated_usd": 0.9956892000000007,
|
||||||
|
"filled": 330,
|
||||||
|
"missing": 330,
|
||||||
|
"requests": 330,
|
||||||
|
"still_missing": 0,
|
||||||
|
"units": 66
|
||||||
|
},
|
||||||
|
"deserialization": {
|
||||||
|
"cached": 0,
|
||||||
|
"conditional_retries": 310,
|
||||||
|
"estimate_method": "estimated",
|
||||||
|
"estimated_output_tokens": 300,
|
||||||
|
"estimated_usd": 0.8843295999999988,
|
||||||
|
"filled": 310,
|
||||||
|
"missing": 310,
|
||||||
|
"requests": 310,
|
||||||
|
"still_missing": 0,
|
||||||
|
"units": 64
|
||||||
|
},
|
||||||
|
"injection": {
|
||||||
|
"cached": 0,
|
||||||
|
"conditional_retries": 675,
|
||||||
|
"estimate_method": "estimated",
|
||||||
|
"estimated_output_tokens": 300,
|
||||||
|
"estimated_usd": 2.270278999999999,
|
||||||
|
"filled": 675,
|
||||||
|
"missing": 675,
|
||||||
|
"requests": 675,
|
||||||
|
"still_missing": 0,
|
||||||
|
"units": 149
|
||||||
|
},
|
||||||
|
"output_encoding": {
|
||||||
|
"cached": 0,
|
||||||
|
"conditional_retries": 325,
|
||||||
|
"estimate_method": "estimated",
|
||||||
|
"estimated_output_tokens": 300,
|
||||||
|
"estimated_usd": 1.0014180000000004,
|
||||||
|
"filled": 328,
|
||||||
|
"missing": 325,
|
||||||
|
"requests": 328,
|
||||||
|
"still_missing": 0,
|
||||||
|
"units": 69
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"cached": 0,
|
||||||
|
"conditional_retries": 320,
|
||||||
|
"estimate_method": "estimated",
|
||||||
|
"estimated_output_tokens": 300,
|
||||||
|
"estimated_usd": 0.9399192000000005,
|
||||||
|
"filled": 325,
|
||||||
|
"missing": 320,
|
||||||
|
"requests": 325,
|
||||||
|
"still_missing": 0,
|
||||||
|
"units": 66
|
||||||
|
},
|
||||||
|
"untrusted_destination": {
|
||||||
|
"cached": 0,
|
||||||
|
"conditional_retries": 425,
|
||||||
|
"estimate_method": "estimated",
|
||||||
|
"estimated_output_tokens": 300,
|
||||||
|
"estimated_usd": 1.174316,
|
||||||
|
"filled": 426,
|
||||||
|
"missing": 425,
|
||||||
|
"requests": 426,
|
||||||
|
"still_missing": 0,
|
||||||
|
"units": 87
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reason": "",
|
||||||
|
"status": "done",
|
||||||
|
"usd": 1.5890382240000016
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
{
|
||||||
|
"alpha": 0.05,
|
||||||
|
"arms": {
|
||||||
|
"A": {
|
||||||
|
"budget_usd": 0.0,
|
||||||
|
"label": "six slice-2 incumbents unchanged, replaying exp-004's response cache",
|
||||||
|
"repeats": 1,
|
||||||
|
"replay_only": true
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"budget_usd": 6.0,
|
||||||
|
"label": "the same six programs with the in-function slice added to their prompts",
|
||||||
|
"repeats": 1,
|
||||||
|
"replay_only": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code_commit": "0acbc7ec41dcaeca805c4720bc3f6d02cb940ee7",
|
||||||
|
"decision": "inconclusive",
|
||||||
|
"experiment": "exp-005-graph-slice",
|
||||||
|
"hypothesis": "Adding the in-function def-use slice to the prompt improves within-pair ranking of the six slice-2 incumbent programs on held-out repositories, at no more than 1.5 times the cost per candidate.\n",
|
||||||
|
"looks": [
|
||||||
|
{
|
||||||
|
"boundary": 1.959964,
|
||||||
|
"crossed": false,
|
||||||
|
"estimate": {
|
||||||
|
"a": 0.7615384615384615,
|
||||||
|
"b": 0.7461538461538462,
|
||||||
|
"ci": [
|
||||||
|
-0.0628019323671497,
|
||||||
|
0.03155339805825241
|
||||||
|
],
|
||||||
|
"diff": -0.01538461538461533,
|
||||||
|
"groups": 220,
|
||||||
|
"metric": "within_pair_auc",
|
||||||
|
"resamples": 10000,
|
||||||
|
"se": 0.024030780235707972,
|
||||||
|
"units": 501
|
||||||
|
},
|
||||||
|
"fraction": 1.0,
|
||||||
|
"groups": 220,
|
||||||
|
"z": -0.6402045723740141
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manifest_digest": "1b1ec814cde12801491376d2086c8c2d6db70f4a86fc308c0adc5a530398e554",
|
||||||
|
"mde": 0.0,
|
||||||
|
"primary": [
|
||||||
|
{
|
||||||
|
"name": "within_pair_auc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reason": "the CI of within_pair_auc spans 0; the incumbent stays",
|
||||||
|
"resamples": 10000,
|
||||||
|
"secondary": [
|
||||||
|
{
|
||||||
|
"name": "top_30_precision"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unsure_rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "usd_per_candidate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spend_usd": {
|
||||||
|
"A": 1.444503,
|
||||||
|
"B": 1.589038
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"top_30_precision": {
|
||||||
|
"ci_vs_zero": "spans",
|
||||||
|
"direction": 1,
|
||||||
|
"estimate": {
|
||||||
|
"a": 1.0,
|
||||||
|
"b": 1.0,
|
||||||
|
"ci": [
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"diff": 0.0,
|
||||||
|
"groups": 220,
|
||||||
|
"metric": "top_30_precision",
|
||||||
|
"resamples": 10000,
|
||||||
|
"se": 0.0006666666666666664,
|
||||||
|
"units": 501
|
||||||
|
},
|
||||||
|
"mcnemar": null,
|
||||||
|
"target": null
|
||||||
|
},
|
||||||
|
"unsure_rate": {
|
||||||
|
"ci_vs_zero": "favour",
|
||||||
|
"direction": -1,
|
||||||
|
"estimate": {
|
||||||
|
"a": 0.29141716566866266,
|
||||||
|
"b": 0.23952095808383234,
|
||||||
|
"ci": [
|
||||||
|
-0.08583690987124465,
|
||||||
|
-0.01996007984031939
|
||||||
|
],
|
||||||
|
"diff": -0.05189620758483032,
|
||||||
|
"groups": 220,
|
||||||
|
"metric": "unsure_rate",
|
||||||
|
"resamples": 10000,
|
||||||
|
"se": 0.01685991406098136,
|
||||||
|
"units": 501
|
||||||
|
},
|
||||||
|
"mcnemar": {
|
||||||
|
"b": 47,
|
||||||
|
"c": 21,
|
||||||
|
"concordant": 433,
|
||||||
|
"p_value": 0.0021862431776861705
|
||||||
|
},
|
||||||
|
"target": null
|
||||||
|
},
|
||||||
|
"usd_per_candidate": {
|
||||||
|
"ci_vs_zero": "against",
|
||||||
|
"direction": -1,
|
||||||
|
"estimate": {
|
||||||
|
"a": 0.002883239520958083,
|
||||||
|
"b": 0.0031717325349301397,
|
||||||
|
"ci": [
|
||||||
|
0.0002458205128205199,
|
||||||
|
0.0003319800399201574
|
||||||
|
],
|
||||||
|
"diff": 0.0002884930139720565,
|
||||||
|
"groups": 220,
|
||||||
|
"metric": "usd_per_candidate",
|
||||||
|
"resamples": 10000,
|
||||||
|
"se": 2.1746161270931252e-05,
|
||||||
|
"units": 501
|
||||||
|
},
|
||||||
|
"mcnemar": null,
|
||||||
|
"target": null
|
||||||
|
},
|
||||||
|
"within_pair_auc": {
|
||||||
|
"ci_vs_zero": "spans",
|
||||||
|
"direction": 1,
|
||||||
|
"estimate": {
|
||||||
|
"a": 0.7615384615384615,
|
||||||
|
"b": 0.7461538461538462,
|
||||||
|
"ci": [
|
||||||
|
-0.0628019323671497,
|
||||||
|
0.03155339805825241
|
||||||
|
],
|
||||||
|
"diff": -0.01538461538461533,
|
||||||
|
"groups": 220,
|
||||||
|
"metric": "within_pair_auc",
|
||||||
|
"resamples": 10000,
|
||||||
|
"se": 0.024030780235707972,
|
||||||
|
"units": 501
|
||||||
|
},
|
||||||
|
"mcnemar": null,
|
||||||
|
"target": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tost": null,
|
||||||
|
"units": {
|
||||||
|
"by_label": {
|
||||||
|
"0": 218,
|
||||||
|
"1": 283
|
||||||
|
},
|
||||||
|
"groups": 220,
|
||||||
|
"outcomes": 1002,
|
||||||
|
"paired": 501
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ from:
|
|||||||
- quick rules and deterministic roles, run here on the unit's tree: a pair side materialised from its excerpt, a
|
- quick rules and deterministic roles, run here on the unit's tree: a pair side materialised from its excerpt, a
|
||||||
population or recipe pin exported from its clone or checkout (one at a time, deleted after, disk-guarded);
|
population or recipe pin exported from its clone or checkout (one at a time, deleted after, disk-guarded);
|
||||||
- the engine: ``benchmarks/learn/engine_pairs.py`` records for pairs, the recorded protocol scans
|
- the engine: ``benchmarks/learn/engine_pairs.py`` records for pairs, the recorded protocol scans
|
||||||
(``~/ousast-results/independent-v{1,2}/scans/<case>--<pin role>.json``) for populations;
|
(``~/ousast-results/independent-v{1,2}/scans/<case>--<pin role>[_a|_b].json``) for populations;
|
||||||
- verify passes a/b and ``agree`` from the harvest Run (``harvest-verify``) and, for population v2, the recorded
|
- verify passes a/b and ``agree`` from the harvest Run (``harvest-verify``) and, for population v2, the recorded
|
||||||
``validation-46`` Run; model roles from ``harvest-roles``;
|
``validation-46`` Run; model roles from ``harvest-roles``;
|
||||||
- callers and entry-point distance on a repository (``not_applicable`` on a pair excerpt: no repository exists).
|
- callers and entry-point distance on a repository (``not_applicable`` on a pair excerpt: no repository exists).
|
||||||
@@ -21,6 +21,11 @@ An instrument that has not run for a unit yet (the engine job still running, a R
|
|||||||
memory store (``OUSAST_MEMORY``, default ``~/ousast-results/plane/memory``) as kind ``features``; a rerun replaces
|
memory store (``OUSAST_MEMORY``, default ``~/ousast-results/plane/memory``) as kind ``features``; a rerun replaces
|
||||||
them by id. ``--counts`` writes the counts-only coverage record (no candidate, path or repository name).
|
them by id. ``--counts`` writes the counts-only coverage record (no candidate, path or repository name).
|
||||||
|
|
||||||
|
``--dry-run`` instead compares existing local store examples with the corrected engine features, printing
|
||||||
|
per-source transitions from not-run/none to ran (and recorded failures separately). It reads only the spent
|
||||||
|
v1/v2 manifests, saved scans and local Git objects; it writes neither the store nor ``--counts`` and does not
|
||||||
|
rebuild other instruments. No label snapshot, harvest units or scratch directory is needed in this mode.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
python benchmarks/learn/build_features.py --labels labels-<sha>.jsonl --units <harvest plane>/units.json --counts counts.json
|
python benchmarks/learn/build_features.py --labels labels-<sha>.jsonl --units <harvest plane>/units.json --counts counts.json
|
||||||
@@ -30,9 +35,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import tomllib
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -61,6 +68,7 @@ from openultrasast.learn.features import (
|
|||||||
source_part,
|
source_part,
|
||||||
verify_part,
|
verify_part,
|
||||||
)
|
)
|
||||||
|
from openultrasast.learn.labels import Clone
|
||||||
from openultrasast.learn.roles import RoleSet, from_model_roles, infer_for_checkout
|
from openultrasast.learn.roles import RoleSet, from_model_roles, infer_for_checkout
|
||||||
from openultrasast.pairs import DEFAULT_CATALOG, _materialize_side, _targets, load_pair_catalog
|
from openultrasast.pairs import DEFAULT_CATALOG, _materialize_side, _targets, load_pair_catalog
|
||||||
from openultrasast.plane import memory
|
from openultrasast.plane import memory
|
||||||
@@ -89,6 +97,110 @@ def read_json(path: Path) -> Any:
|
|||||||
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
|
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_scan(scans: Path, case: str, side: str) -> dict[str, Any] | None:
|
||||||
|
"""Read the v1 single scan, otherwise combine the v2 repeats for this side."""
|
||||||
|
single = read_json(scans / f"{case}--{side}.json")
|
||||||
|
if single is not None:
|
||||||
|
return dict(single)
|
||||||
|
repeats = [read_json(scans / f"{case}--{side}_{repeat}.json") for repeat in ("a", "b")]
|
||||||
|
available = [r for r in repeats if r is not None]
|
||||||
|
if not available:
|
||||||
|
return None
|
||||||
|
# Identical rule for vulnerable and fixed: a finding in either repeat counts;
|
||||||
|
# exact duplicates count once. engine_part takes the highest rung and minimum
|
||||||
|
# witness steps across the union. Completion is sum(completed)/sum(questions).
|
||||||
|
# A successful repeat survives a failed/missing peer, which marks degradation.
|
||||||
|
ran = [r for r in available if engine_for(r) is None]
|
||||||
|
findings = {json.dumps(f, sort_keys=True): f for r in ran for f in r.get("findings", [])}
|
||||||
|
degradations = sorted({d for r in available for d in r.get("degradations", [])})
|
||||||
|
if len(ran) != len(repeats):
|
||||||
|
degradations.append("repeat_missing_or_failed")
|
||||||
|
return {
|
||||||
|
"state": "ran" if ran else "none" if all(engine_for(r) == NONE for r in available) else "failed",
|
||||||
|
"questions": sum(int(r.get("questions") or 0) for r in available),
|
||||||
|
"completed": sum(int(r.get("completed") or 0) for r in available),
|
||||||
|
"findings": list(findings.values()),
|
||||||
|
"degradations": degradations,
|
||||||
|
"image": "+".join(sorted({str(r.get("image") or "recorded") for r in available})),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def recorded_engine(language: str, findings: list, result: Mapping | None, repository: bool) -> Part:
|
||||||
|
# The language list describes standalone scans. Repository scans also analyze
|
||||||
|
# TypeScript; recorded function findings take precedence over that list.
|
||||||
|
if language not in ENGINE_LANGUAGES and not findings and not (repository and language == "typescript" and result is not None):
|
||||||
|
return NONE
|
||||||
|
state = engine_for(result)
|
||||||
|
if state is not None:
|
||||||
|
return state
|
||||||
|
result = result or {}
|
||||||
|
questions, completed = int(result.get("questions") or 0), int(result.get("completed") or 0)
|
||||||
|
return engine_part(
|
||||||
|
findings, completion=completed / questions if questions else None, degraded=bool(result.get("degradations")),
|
||||||
|
vocabulary=Vocabulary.load(language), version=str(result.get("image") or "recorded"),
|
||||||
|
) # fmt: skip
|
||||||
|
|
||||||
|
|
||||||
|
class PinSourceIndex(SourceIndex):
|
||||||
|
"""Resolve findings with the same matcher as a checkout, without exporting it."""
|
||||||
|
|
||||||
|
def __init__(self, clone: Path, pin: str) -> None:
|
||||||
|
super().__init__(clone)
|
||||||
|
self.clone, self.pin = Clone(clone), pin
|
||||||
|
|
||||||
|
def lines(self, path: str) -> list[str]:
|
||||||
|
if path not in self._lines:
|
||||||
|
self._lines[path] = self.clone.git("show", f"{self.pin}:{path}").splitlines()
|
||||||
|
return self._lines[path] or []
|
||||||
|
|
||||||
|
|
||||||
|
def dry_run(store: memory.FileStore, results: Path, cache: Path) -> dict[str, Any]:
|
||||||
|
"""Count existing examples whose engine becomes available; never write rows or blobs.
|
||||||
|
|
||||||
|
Only spent v1/v2 populations are read. Counts are examples, not the two
|
||||||
|
static/plane feature records. Other sources are reported with zero changes.
|
||||||
|
"""
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
cases = {}
|
||||||
|
for population in ("population-v1", "population-v2"):
|
||||||
|
manifest = root / "benchmarks" / "independent" / f"{population}.toml"
|
||||||
|
for case in tomllib.loads(manifest.read_text(encoding="utf-8"))["case"]:
|
||||||
|
for side in ("vulnerable", "fixed"):
|
||||||
|
cases[(population, memory.repo_key(case["repo"]), case[side], side)] = case["id"]
|
||||||
|
rows = [r.row for r in store.rows(kind="example")]
|
||||||
|
if not rows:
|
||||||
|
raise ValueError("dry run read no examples from the store")
|
||||||
|
counts: dict[str, Counter[str]] = defaultdict(Counter)
|
||||||
|
for row in rows:
|
||||||
|
count = counts[row["source"]]
|
||||||
|
count["examples"] += 1
|
||||||
|
old = row["instruments"]["engine"]
|
||||||
|
reason = "not_run" if old.get("version") == "missing:not-run" else "none" if old["state"] == "none" else None
|
||||||
|
if reason is None:
|
||||||
|
continue
|
||||||
|
key = (row["split"], row["repo"], row["pin"], row["pin_role"])
|
||||||
|
case_id = cases.get(key)
|
||||||
|
if case_id is None:
|
||||||
|
continue
|
||||||
|
scans = results / row["split"].replace("population", "independent") / "scans"
|
||||||
|
result = resolve_scan(scans, case_id, row["pin_role"])
|
||||||
|
path, _, function = row["candidate"].partition("::")
|
||||||
|
engine = {}
|
||||||
|
if engine_for(result) is None:
|
||||||
|
index = PinSourceIndex(cache / "independent" / case_id, row["pin"])
|
||||||
|
lines = index.lines(path) # fail loudly if the candidate's input cannot be read
|
||||||
|
count["source_bytes_read"] += len("\n".join(lines).encode("utf-8"))
|
||||||
|
selected = dict(result or {})
|
||||||
|
selected["findings"] = [f for f in selected.get("findings", []) if f["site"].partition(":")[0] == path]
|
||||||
|
_, engine = _signals(index, [], selected, {})
|
||||||
|
part = recorded_engine(row["language"], engine.get((path, function, row["family"]), []), result, True)
|
||||||
|
if part.state == "ran":
|
||||||
|
count[f"{reason}_to_ran"] += 1
|
||||||
|
elif result is not None and part.state == "failed":
|
||||||
|
count[f"{reason}_to_recorded_failed"] += 1
|
||||||
|
return {source: {**{"not_run_to_ran": 0, "none_to_ran": 0}, **dict(count)} for source, count in sorted(counts.items())}
|
||||||
|
|
||||||
|
|
||||||
def done(run: Path, entry: str) -> bool:
|
def done(run: Path, entry: str) -> bool:
|
||||||
state = (read_json(run / "state.json") or {}).get("tasks", {})
|
state = (read_json(run / "state.json") or {}).get("tasks", {})
|
||||||
return state.get(entry, {}).get("status") == "done"
|
return state.get(entry, {}).get("status") == "done"
|
||||||
@@ -188,17 +300,7 @@ def static_parts(root: Path, index: SourceIndex, quick: Mapping, engine: Mapping
|
|||||||
key = (path, function, family)
|
key = (path, function, family)
|
||||||
parts: dict[str, Part] = {}
|
parts: dict[str, Part] = {}
|
||||||
parts["quick"] = quick_part(quick.get(key, []), rules, quick_version) if covers(QUICK_LANGUAGES, language) else NONE
|
parts["quick"] = quick_part(quick.get(key, []), rules, quick_version) if covers(QUICK_LANGUAGES, language) else NONE
|
||||||
if language not in ENGINE_LANGUAGES:
|
parts["engine"] = recorded_engine(language, engine.get(key, []), engine_result, repository)
|
||||||
parts["engine"] = NONE
|
|
||||||
elif engine_state is not None:
|
|
||||||
parts["engine"] = engine_state
|
|
||||||
else:
|
|
||||||
result = engine_result or {}
|
|
||||||
questions, completed = int(result.get("questions") or 0), int(result.get("completed") or 0)
|
|
||||||
parts["engine"] = engine_part(
|
|
||||||
engine.get(key, []), completion=completed / questions if questions else None, degraded=bool(result.get("degradations")),
|
|
||||||
vocabulary=Vocabulary.load(language), version=str(result.get("image") or "recorded"),
|
|
||||||
) # fmt: skip
|
|
||||||
lines = index.lines(path)
|
lines = index.lines(path)
|
||||||
parts["source"] = source_part(lines, language, function)
|
parts["source"] = source_part(lines, language, function)
|
||||||
parts["roles"] = roles_part(function_of(lines, path, language, function), inferred, language)
|
parts["roles"] = roles_part(function_of(lines, path, language, function), inferred, language)
|
||||||
@@ -237,21 +339,34 @@ def export(clone: Path, pin: str, target: Path) -> bool:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument("--labels", type=Path, required=True)
|
parser.add_argument("--labels", type=Path)
|
||||||
parser.add_argument("--units", type=Path, required=True, help="units.json of the harvest plane")
|
parser.add_argument("--units", type=Path, help="units.json of the harvest plane")
|
||||||
parser.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG)
|
parser.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG)
|
||||||
parser.add_argument("--cache", type=Path, default=Path.home() / ".cache" / "openultrasast")
|
parser.add_argument("--cache", type=Path, default=Path.home() / ".cache" / "openultrasast")
|
||||||
parser.add_argument("--engine", type=Path, default=RESULTS / "plane" / "harvest-engine")
|
parser.add_argument("--engine", type=Path, default=RESULTS / "plane" / "harvest-engine")
|
||||||
parser.add_argument("--verify-run", type=Path, default=RESULTS / "plane" / "harvest-verify")
|
parser.add_argument("--verify-run", type=Path, default=RESULTS / "plane" / "harvest-verify")
|
||||||
parser.add_argument("--roles-run", type=Path, default=RESULTS / "plane" / "harvest-roles")
|
parser.add_argument("--roles-run", type=Path, default=RESULTS / "plane" / "harvest-roles")
|
||||||
parser.add_argument("--v46", type=Path, default=RESULTS / "plane" / "validation-46")
|
parser.add_argument("--v46", type=Path, default=RESULTS / "plane" / "validation-46")
|
||||||
parser.add_argument("--scratch", type=Path, required=True)
|
parser.add_argument("--scratch", type=Path)
|
||||||
parser.add_argument("--min-free-gb", type=float, default=1.2)
|
parser.add_argument("--min-free-gb", type=float, default=1.2)
|
||||||
parser.add_argument("--counts", type=Path, required=True)
|
parser.add_argument("--counts", type=Path)
|
||||||
parser.add_argument("--store", help="OUSAST_MEMORY spec (default: the environment's)")
|
parser.add_argument("--store", help="OUSAST_MEMORY spec (default: the environment's)")
|
||||||
parser.add_argument("--limit", type=int, default=0, help="stop after this many targets (a test)")
|
parser.add_argument("--limit", type=int, default=0, help="stop after this many targets (a test)")
|
||||||
parser.add_argument("--kinds", default="pairs,pin", help="which targets: pairs, pin (population and recipe pins)")
|
parser.add_argument("--kinds", default="pairs,pin", help="which targets: pairs, pin (population and recipe pins)")
|
||||||
|
parser.add_argument("--results-root", type=Path, default=RESULTS)
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="read local stored examples and print engine changes by source; no writes")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.dry_run:
|
||||||
|
spec = args.store or os.environ.get("OUSAST_MEMORY") or ""
|
||||||
|
if spec and not spec.startswith("file://"):
|
||||||
|
parser.error("--dry-run requires a local file:// store (no network)")
|
||||||
|
store = memory.open_store(spec)
|
||||||
|
if not isinstance(store, memory.FileStore):
|
||||||
|
parser.error("--dry-run requires a local file:// store (no network)")
|
||||||
|
print(json.dumps({"dry_run": True, "by_source": dry_run(store, args.results_root, args.cache)}, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if any(getattr(args, name) is None for name in ("labels", "units", "scratch", "counts")):
|
||||||
|
parser.error("--labels, --units, --scratch and --counts are required unless --dry-run is used")
|
||||||
|
|
||||||
labels = [r for r in jsonl(args.labels) if r["unit"] == "pin" and r["source"] not in CONDITIONAL_SOURCES and not r.get("conditional")]
|
labels = [r for r in jsonl(args.labels) if r["unit"] == "pin" and r["source"] not in CONDITIONAL_SOURCES and not r.get("conditional")]
|
||||||
units = json.loads(args.units.read_text(encoding="utf-8"))
|
units = json.loads(args.units.read_text(encoding="utf-8"))
|
||||||
@@ -314,7 +429,7 @@ def main() -> int:
|
|||||||
continue
|
continue
|
||||||
split = {r["split"] for r in rows} | {unit["source"]}
|
split = {r["split"] for r in rows} | {unit["source"]}
|
||||||
population = "independent-v1" if "population-v1" in split else "independent-v2"
|
population = "independent-v1" if "population-v1" in split else "independent-v2"
|
||||||
engine_result = read_json(RESULTS / population / "scans" / f"{case_id}--{rows[0]['pin_role']}.json")
|
engine_result = resolve_scan(args.results_root / population / "scans", case_id, rows[0]["pin_role"])
|
||||||
stems = [(args.verify_run, n) for n in names] + [(args.v46, unit["ref"])]
|
stems = [(args.verify_run, n) for n in names] + [(args.v46, unit["ref"])]
|
||||||
owner = next(
|
owner = next(
|
||||||
(n for n in names if f"{n}-roles" in {e for e in (read_json(args.roles_run / "state.json") or {}).get("tasks", {})}),
|
(n for n in names if f"{n}-roles" in {e for e in (read_json(args.roles_run / "state.json") or {}).get("tasks", {})}),
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
"""AX task entry: object-scoped transfers and stdlib only; result PUT is completion."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class TaskEntryError(RuntimeError):
|
||||||
|
"""A task-entry diagnostic containing only locally controlled text."""
|
||||||
|
|
||||||
|
|
||||||
|
def transfer(request, destination=None, *, readiness_budget=None):
|
||||||
|
"""Wait for actor egress on startup; bound later transfers to three attempts."""
|
||||||
|
deadline = time.monotonic() + readiness_budget if readiness_budget is not None else None
|
||||||
|
delay = 0.5
|
||||||
|
attempt = 0
|
||||||
|
while True:
|
||||||
|
remaining = deadline - time.monotonic() if deadline is not None else 60
|
||||||
|
if remaining <= 0:
|
||||||
|
raise TaskEntryError("egress not open after 60 s")
|
||||||
|
attempt += 1
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=min(60, remaining)) as response:
|
||||||
|
if destination is not None:
|
||||||
|
with destination.open("wb") as output:
|
||||||
|
shutil.copyfileobj(response, output)
|
||||||
|
return
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
exc.close()
|
||||||
|
if exc.code != 403 and not 500 <= exc.code < 600:
|
||||||
|
raise
|
||||||
|
if deadline is None and attempt >= 3:
|
||||||
|
raise
|
||||||
|
except (urllib.error.URLError, OSError, http.client.HTTPException):
|
||||||
|
if deadline is None and attempt >= 3:
|
||||||
|
raise
|
||||||
|
if deadline is not None:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
raise TaskEntryError("egress not open after 60 s") from None
|
||||||
|
time.sleep(min(delay, remaining))
|
||||||
|
delay = min(delay * 2, 8)
|
||||||
|
else:
|
||||||
|
time.sleep(attempt)
|
||||||
|
|
||||||
|
|
||||||
|
def download(url, destination, *, readiness_budget=None):
|
||||||
|
transfer(url, destination, readiness_budget=readiness_budget)
|
||||||
|
|
||||||
|
|
||||||
|
def unpack(source, destination):
|
||||||
|
"""Reject links and traversal; source archives contain only regular files/directories."""
|
||||||
|
with tarfile.open(source) as archive:
|
||||||
|
for member in archive.getmembers():
|
||||||
|
target = (destination / member.name).resolve()
|
||||||
|
if not target.is_relative_to(destination.resolve()) or not (member.isfile() or member.isdir()):
|
||||||
|
raise ValueError("unsafe source archive member")
|
||||||
|
archive.extractall(destination, filter="data")
|
||||||
|
|
||||||
|
|
||||||
|
def failure_record(pin, reason):
|
||||||
|
return {
|
||||||
|
**pin,
|
||||||
|
"done": True,
|
||||||
|
"status": "failed",
|
||||||
|
"reason": reason,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
**unit,
|
||||||
|
"status": "failed" if unit.get("supported", True) else "unsupported",
|
||||||
|
"reason": reason,
|
||||||
|
"instrument": {"files": [], "bytes": 0, "jvm": []},
|
||||||
|
"questions": {},
|
||||||
|
"questions_asked": [],
|
||||||
|
"questions_completed": [],
|
||||||
|
"witness_rows": [],
|
||||||
|
"traces": [],
|
||||||
|
}
|
||||||
|
for unit in pin.get("units", [])
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main(*, work_root=Path("/tmp"), worker_script=None, readiness_budget=60):
|
||||||
|
pin = {}
|
||||||
|
failed = False
|
||||||
|
output = work_root / "out"
|
||||||
|
result = output / "result.json"
|
||||||
|
step = "prepare output"
|
||||||
|
try:
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
result.unlink(missing_ok=True)
|
||||||
|
questions = work_root / "questions.json"
|
||||||
|
step = "GET questions"
|
||||||
|
download(os.environ["QUESTIONS_URL"], questions, readiness_budget=readiness_budget)
|
||||||
|
step = "parse questions"
|
||||||
|
pin = json.loads(questions.read_text())
|
||||||
|
if not isinstance(pin, dict):
|
||||||
|
pin = {}
|
||||||
|
raise ValueError("questions object must contain a prepared pin")
|
||||||
|
source = work_root / "source.tar"
|
||||||
|
step = "GET source"
|
||||||
|
download(os.environ["SOURCE_URL"], source)
|
||||||
|
step = "unpack source"
|
||||||
|
case = work_root / "case"
|
||||||
|
case.mkdir(parents=True, exist_ok=True)
|
||||||
|
unpack(source, case)
|
||||||
|
step = "configure worker"
|
||||||
|
deadline = int(os.environ.get("DEADLINE", "900"))
|
||||||
|
command = [
|
||||||
|
sys.executable,
|
||||||
|
str(worker_script or Path(__file__).with_name("engine_trace_worker.py")),
|
||||||
|
"--worker",
|
||||||
|
str(questions),
|
||||||
|
"--root",
|
||||||
|
str(case),
|
||||||
|
"--output",
|
||||||
|
str(result),
|
||||||
|
"--heap-profile",
|
||||||
|
"cluster",
|
||||||
|
"--deadline",
|
||||||
|
str(deadline),
|
||||||
|
"--question-deadline",
|
||||||
|
os.environ.get("QUESTION_DEADLINE", "120"),
|
||||||
|
]
|
||||||
|
step = "run worker"
|
||||||
|
completed = subprocess.run(command, timeout=deadline + 5, check=False)
|
||||||
|
if completed.returncode:
|
||||||
|
raise TaskEntryError(f"worker exited {completed.returncode}")
|
||||||
|
step = "read worker result"
|
||||||
|
if not result.is_file() or not json.loads(result.read_text()).get("done"):
|
||||||
|
raise TaskEntryError("worker did not write a completed result")
|
||||||
|
except Exception as exc:
|
||||||
|
failed = True
|
||||||
|
# Avoid including exception text containing a presigned URL in durable output.
|
||||||
|
if isinstance(exc, urllib.error.HTTPError):
|
||||||
|
detail = f"HTTP {exc.code}"
|
||||||
|
else:
|
||||||
|
detail = str(exc) if isinstance(exc, TaskEntryError) else type(exc).__name__
|
||||||
|
reason = f"{step}: {detail}"
|
||||||
|
try:
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
result.write_text(json.dumps(failure_record(pin, reason)) + "\n")
|
||||||
|
except Exception:
|
||||||
|
print("task entry could not write failure result", file=sys.stderr)
|
||||||
|
step = "pack result"
|
||||||
|
try:
|
||||||
|
archive = work_root / "result.tar"
|
||||||
|
with tarfile.open(archive, "w") as tar:
|
||||||
|
for path in sorted(output.iterdir()):
|
||||||
|
tar.add(path, arcname=path.name)
|
||||||
|
data = archive.read_bytes()
|
||||||
|
step = "PUT result"
|
||||||
|
request = urllib.request.Request(os.environ["RESULT_URL"], data=data, method="PUT", headers={"Content-Type": "application/x-tar"})
|
||||||
|
transfer(request, readiness_budget=readiness_budget if failed else None)
|
||||||
|
except Exception as exc:
|
||||||
|
detail = f"HTTP {exc.code}" if isinstance(exc, urllib.error.HTTPError) else type(exc).__name__
|
||||||
|
print(f"{step}: {detail}; dispatcher deadline remains authoritative", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
"""Engine workload adapter for the shared AX transport."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||||
|
from engine_trace_k8s import pack
|
||||||
|
|
||||||
|
from benchmarks.ax.batch import AXLane, Item, SandboxFailure, Workload, manifest
|
||||||
|
from benchmarks.ax.batch import task_state as task_state
|
||||||
|
from openultrasast.plane.memory import open_store as open_store
|
||||||
|
|
||||||
|
|
||||||
|
def task_manifest(name, args, urls):
|
||||||
|
return manifest(
|
||||||
|
name, engine_workload(args, lambda *_: None), engine_item({}, "pin", b"", args), urls, args.atespace, validate_name=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def engine_workload(args, validate):
|
||||||
|
return Workload(args.image, frozenset({"SOURCE_URL", "QUESTIONS_URL", "RESULT_URL"}), args.deadline, validate, kind="pass")
|
||||||
|
|
||||||
|
|
||||||
|
def engine_item(pin, pin_id, source, args):
|
||||||
|
return Item(
|
||||||
|
pin_id,
|
||||||
|
{"SOURCE_URL": source, "QUESTIONS_URL": json.dumps(pin).encode()},
|
||||||
|
"result.dat",
|
||||||
|
("python3", "/app/benchmarks/learn/engine_task_entry.py"),
|
||||||
|
{"DEADLINE": str(args.deadline), "QUESTION_DEADLINE": str(args.question_deadline)},
|
||||||
|
"",
|
||||||
|
{"SOURCE_URL": "source.dat", "QUESTIONS_URL": "questions.json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AX(AXLane):
|
||||||
|
def __init__(self, args, store=None, pause=time.sleep, clock=time.monotonic):
|
||||||
|
super().__init__(args, engine_workload(args, lambda *_: None), store, pause, clock)
|
||||||
|
|
||||||
|
def receive(self, data, output, pin):
|
||||||
|
# Validate in isolation: malformed results must never become usable checkpoints.
|
||||||
|
with tempfile.TemporaryDirectory(prefix="engine-ax-result-") as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||||
|
archive.extractall(root, filter="data")
|
||||||
|
record = json.loads((root / "result.json").read_text())
|
||||||
|
units = record.get("units", [])
|
||||||
|
if record.get("done") and record.get("status") == "failed" and not units:
|
||||||
|
# Questions download can fail before the actor learns any unit identities.
|
||||||
|
# Keep that explicit failure, filling identities only from the host input.
|
||||||
|
record = {
|
||||||
|
**pin,
|
||||||
|
**record,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
**unit,
|
||||||
|
"status": "failed" if unit["supported"] else "unsupported",
|
||||||
|
"reason": record.get("reason", "AX task entry failed"),
|
||||||
|
"instrument": {"files": [], "bytes": 0, "cpg_path": None, "cpg_bytes": 0, "jvm": []},
|
||||||
|
"questions": {},
|
||||||
|
"questions_asked": [],
|
||||||
|
"questions_completed": [],
|
||||||
|
"witness_rows": [],
|
||||||
|
"traces": [],
|
||||||
|
}
|
||||||
|
for unit in pin["units"]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
units = record["units"]
|
||||||
|
(root / "result.json").write_text(json.dumps(record))
|
||||||
|
expected = [u["unit"] for u in pin["units"]]
|
||||||
|
actual = [u["unit"] for u in units]
|
||||||
|
if not record.get("done") or sorted(actual) != sorted(expected):
|
||||||
|
raise ValueError("AX Task returned incomplete or mismatched units")
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Keep the artifacts without interpreting any remote paths as host destinations.
|
||||||
|
shutil.copytree(root, output, dirs_exist_ok=True)
|
||||||
|
|
||||||
|
def execute(self, checkout, output, pin, pin_id):
|
||||||
|
import copy
|
||||||
|
|
||||||
|
self = copy.copy(self)
|
||||||
|
self.workload = engine_workload(self.args, lambda data, target: self.receive(data, target, pin))
|
||||||
|
attempts = []
|
||||||
|
try:
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
return self._execute_attempt(engine_item(pin, pin_id, pack(checkout), self.args), output, attempt, attempts)
|
||||||
|
except SandboxFailure:
|
||||||
|
if attempt == 0:
|
||||||
|
self.pause(10)
|
||||||
|
return subprocess.CompletedProcess([], 1, "", "AX sandbox failed twice"), None
|
||||||
|
finally:
|
||||||
|
(output / "ax_attempts.json").write_text(json.dumps(attempts))
|
||||||
|
result_path = output / "result.json"
|
||||||
|
if result_path.exists():
|
||||||
|
record = json.loads(result_path.read_text())
|
||||||
|
record["ax_attempts"] = attempts
|
||||||
|
result_path.write_text(json.dumps(record))
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
"""Host Kubernetes transport; the embedded bootstrap uses only the standard library."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from openultrasast.plane.memory import PRESIGNED_ARCHIVE_SUFFIX, S3Store, open_store
|
||||||
|
|
||||||
|
IMAGE = "ghcr.io/norandom/openultrasast:2.0.1"
|
||||||
|
BOOTSTRAP = r"""
|
||||||
|
import io, json, os, pathlib, subprocess, sys, tarfile, urllib.request
|
||||||
|
|
||||||
|
def get(name):
|
||||||
|
with urllib.request.urlopen(os.environ[name], timeout=120) as response:
|
||||||
|
return response.read()
|
||||||
|
|
||||||
|
def put(data):
|
||||||
|
request = urllib.request.Request(os.environ['RESULT_URL'], data=data, method='PUT')
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
response.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.environ.get('CHECK') == '1':
|
||||||
|
data = get('SOURCE_URL')
|
||||||
|
assert len(data) == 1024
|
||||||
|
subprocess.run([sys.executable, '-c', 'print(42)'], check=True)
|
||||||
|
put(data)
|
||||||
|
else:
|
||||||
|
for name, target in [('SOURCE_URL', '/case'), ('ANALYZER_URL', '/frozen')]:
|
||||||
|
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(get(name))) as archive:
|
||||||
|
archive.extractall(target, filter='data')
|
||||||
|
out = pathlib.Path('/out')
|
||||||
|
out.mkdir(exist_ok=True)
|
||||||
|
(out / 'pin.json').write_text(os.environ['PIN'])
|
||||||
|
env = dict(os.environ, PYTHONPATH='/frozen/src')
|
||||||
|
for key in ['SOURCE_URL', 'ANALYZER_URL', 'RESULT_URL']:
|
||||||
|
env.pop(key, None)
|
||||||
|
with (out / 'container.log').open('w') as log:
|
||||||
|
done = subprocess.run([sys.executable, '/frozen/benchmarks/learn/engine_trace_worker.py',
|
||||||
|
'--worker', '/out/pin.json', '--deadline', os.environ['DEADLINE'],
|
||||||
|
'--question-deadline', os.environ['QUESTION_DEADLINE']], env=env, stdout=log, stderr=log)
|
||||||
|
(out / 'exit.json').write_text(json.dumps(done.returncode))
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=buffer, mode='w') as archive:
|
||||||
|
for path in out.iterdir():
|
||||||
|
archive.add(path, arcname=path.name)
|
||||||
|
put(buffer.getvalue())
|
||||||
|
sys.exit(done.returncode)
|
||||||
|
except Exception as exc:
|
||||||
|
# urllib exceptions can contain bearer URLs. Never print exception text.
|
||||||
|
print('engine bootstrap failed: ' + type(exc).__name__, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def pack(path):
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=buffer, mode="w") as archive:
|
||||||
|
for child in sorted(path.iterdir()):
|
||||||
|
archive.add(child, arcname=child.name)
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def manifests(name, run, args, urls, pin, check=False):
|
||||||
|
labels = {"engine-trace-run": run, "engine-trace-job": name, "engine-trace": "true"}
|
||||||
|
secret = {
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": {"name": name, "namespace": args.kube_namespace, "labels": labels},
|
||||||
|
"type": "Opaque",
|
||||||
|
"stringData": urls,
|
||||||
|
}
|
||||||
|
env = [{"name": key, "valueFrom": {"secretKeyRef": {"name": name, "key": key}}} for key in urls]
|
||||||
|
env += [
|
||||||
|
{"name": key, "value": value}
|
||||||
|
for key, value in {
|
||||||
|
"PIN": json.dumps(pin),
|
||||||
|
"DEADLINE": str(args.deadline),
|
||||||
|
"QUESTION_DEADLINE": str(args.question_deadline),
|
||||||
|
"CHECK": "1" if check else "0",
|
||||||
|
}.items()
|
||||||
|
]
|
||||||
|
job = {
|
||||||
|
"apiVersion": "batch/v1",
|
||||||
|
"kind": "Job",
|
||||||
|
"metadata": {"name": name, "namespace": args.kube_namespace, "labels": labels},
|
||||||
|
"spec": {
|
||||||
|
"activeDeadlineSeconds": args.deadline,
|
||||||
|
"backoffLimit": 0,
|
||||||
|
"ttlSecondsAfterFinished": 86400,
|
||||||
|
"template": {
|
||||||
|
"metadata": {"labels": labels},
|
||||||
|
"spec": {
|
||||||
|
"restartPolicy": "Never",
|
||||||
|
"automountServiceAccountToken": False,
|
||||||
|
"securityContext": {"runAsUser": 1000, "runAsGroup": 1000, "fsGroup": 1000},
|
||||||
|
"volumes": [{"name": name, "emptyDir": {}} for name in ("case", "frozen", "out")],
|
||||||
|
"affinity": {
|
||||||
|
"podAntiAffinity": {
|
||||||
|
"requiredDuringSchedulingIgnoredDuringExecution": [
|
||||||
|
{
|
||||||
|
"labelSelector": {"matchLabels": {"engine-trace": "true"}},
|
||||||
|
"topologyKey": "kubernetes.io/hostname",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"containers": [
|
||||||
|
{
|
||||||
|
"name": "engine",
|
||||||
|
"image": args.image,
|
||||||
|
"command": ["python", "-c", BOOTSTRAP],
|
||||||
|
"env": env,
|
||||||
|
"volumeMounts": [{"name": name, "mountPath": "/" + name} for name in ("case", "frozen", "out")],
|
||||||
|
"resources": {"requests": {"memory": "3Gi", "cpu": "1"}, "limits": {"memory": "3Gi"}},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return job, secret
|
||||||
|
|
||||||
|
|
||||||
|
def redact(text):
|
||||||
|
return re.sub(r"https?://[^\s\"'<>]+", "[redacted URL]", text)
|
||||||
|
|
||||||
|
|
||||||
|
class Kubernetes:
|
||||||
|
def __init__(self, args, store=None):
|
||||||
|
self.args = args
|
||||||
|
self.run = uuid.uuid4().hex
|
||||||
|
self.store = store if store is not None else open_store()
|
||||||
|
if not isinstance(self.store, S3Store):
|
||||||
|
raise ValueError("Kubernetes requires OUSAST_MEMORY=s3://bucket[/prefix]")
|
||||||
|
|
||||||
|
def kubectl(self, *args, manifest=None, check=True):
|
||||||
|
result = subprocess.run(
|
||||||
|
["kubectl", "--context", self.args.kube_context, "--namespace", self.args.kube_namespace, *args],
|
||||||
|
input=json.dumps(manifest) if manifest is not None else None,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if check and result.returncode:
|
||||||
|
# kubectl can echo submitted Secret data on errors; never expose stderr.
|
||||||
|
raise ValueError(f"kubectl {args[0]} failed (exit {result.returncode})")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def namespace(self):
|
||||||
|
self.kubectl("get", "nodes", "-o", "json")
|
||||||
|
self.kubectl("apply", "-f", "-", manifest={"apiVersion": "v1", "kind": "Namespace", "metadata": {"name": self.args.kube_namespace}})
|
||||||
|
|
||||||
|
def execute(self, source, checkout, output, pin, check=False):
|
||||||
|
name = "engine-" + uuid.uuid4().hex[:24]
|
||||||
|
pin_key = hashlib.sha256(json.dumps([pin.get("repo"), pin.get("pin")]).encode()).hexdigest()
|
||||||
|
prefix = f"engine-jobs/{self.run}/{pin_key}/"
|
||||||
|
keys = [prefix + name + PRESIGNED_ARCHIVE_SUFFIX for name in ("source", "analyzer", "result")]
|
||||||
|
expiry = timedelta(seconds=self.args.deadline + 900)
|
||||||
|
node = None
|
||||||
|
try:
|
||||||
|
self.store._put(keys[0], b"x" * 1024 if check else pack(checkout))
|
||||||
|
self.store._put(keys[1], b"" if check else pack(source))
|
||||||
|
urls = {
|
||||||
|
"SOURCE_URL": self.store.presign_get(keys[0], expiry),
|
||||||
|
"ANALYZER_URL": self.store.presign_get(keys[1], expiry),
|
||||||
|
"RESULT_URL": self.store.presign_put(keys[2], expiry),
|
||||||
|
}
|
||||||
|
job, secret = manifests(name, self.run, self.args, urls, pin, check)
|
||||||
|
# create avoids kubectl's last-applied annotation duplicating Secret values.
|
||||||
|
created = json.loads(self.kubectl("create", "-f", "-", "-o", "json", manifest=job).stdout)
|
||||||
|
secret["metadata"]["ownerReferences"] = [
|
||||||
|
{
|
||||||
|
"apiVersion": "batch/v1",
|
||||||
|
"kind": "Job",
|
||||||
|
"name": name,
|
||||||
|
"uid": created["metadata"]["uid"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
self.kubectl("create", "-f", "-", manifest=secret)
|
||||||
|
end = time.monotonic() + self.args.deadline + 60
|
||||||
|
reason = "host deadline waiting for Job"
|
||||||
|
while time.monotonic() < end:
|
||||||
|
state = json.loads(self.kubectl("get", "job", name, "-o", "json").stdout).get("status", {})
|
||||||
|
pods = json.loads(self.kubectl("get", "pods", "-l", f"engine-trace-job={name}", "-o", "json").stdout)
|
||||||
|
node = next((p.get("spec", {}).get("nodeName") for p in pods.get("items", []) if p.get("spec", {}).get("nodeName")), node)
|
||||||
|
result = self.store._get(keys[2])
|
||||||
|
failed = next((c for c in state.get("conditions", []) if c.get("type") == "Failed" and c.get("status") == "True"), None)
|
||||||
|
if failed or state.get("failed"):
|
||||||
|
reason = (failed or {}).get("reason", "Job failed") + ": " + (failed or {}).get("message", "")
|
||||||
|
break
|
||||||
|
if state.get("succeeded") and result is not None:
|
||||||
|
if check:
|
||||||
|
if result[0] != b"x" * 1024:
|
||||||
|
raise ValueError("round-trip bytes mismatch")
|
||||||
|
else:
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(result[0])) as archive:
|
||||||
|
archive.extractall(output, filter="data")
|
||||||
|
if not (output / "result.json").is_file():
|
||||||
|
raise ValueError("Job produced no result.json")
|
||||||
|
record = json.loads((output / "result.json").read_text())
|
||||||
|
if not record.get("done") or {u["unit"] for u in record["units"]} != {u["unit"] for u in pin["units"]}:
|
||||||
|
raise ValueError("Job returned incomplete or mismatched units")
|
||||||
|
return subprocess.CompletedProcess([], 0, "", ""), node
|
||||||
|
time.sleep(2)
|
||||||
|
logs = self.kubectl("logs", f"job/{name}", "--tail=40", check=False)
|
||||||
|
reason = redact(reason + "\n" + logs.stdout + logs.stderr)
|
||||||
|
return subprocess.CompletedProcess([], 1, "", reason), node
|
||||||
|
except Exception as exc:
|
||||||
|
reason = "Kubernetes transport failed: " + type(exc).__name__ + ": " + redact(str(exc))
|
||||||
|
try:
|
||||||
|
logs = self.kubectl("logs", f"job/{name}", "--tail=40", check=False)
|
||||||
|
reason += "\n" + redact(logs.stdout + logs.stderr)
|
||||||
|
except (OSError, ValueError, subprocess.SubprocessError):
|
||||||
|
pass
|
||||||
|
return subprocess.CompletedProcess([], 1, "", reason), node
|
||||||
|
finally:
|
||||||
|
if not self.args.keep_jobs:
|
||||||
|
# Attempt every cleanup even if an earlier deletion fails.
|
||||||
|
errors = []
|
||||||
|
for kind in ("job", "secret"):
|
||||||
|
try:
|
||||||
|
self.kubectl("delete", kind, name, "--ignore-not-found", "--wait=true", "--timeout=30s")
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append(type(exc).__name__)
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
self.store._delete(key)
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append(type(exc).__name__)
|
||||||
|
if errors:
|
||||||
|
raise RuntimeError("engine Job cleanup failed: " + ", ".join(errors))
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
self.namespace()
|
||||||
|
nodes = json.loads(self.kubectl("get", "nodes", "-o", "json").stdout)
|
||||||
|
for node in nodes["items"]:
|
||||||
|
print(json.dumps({"node": node["metadata"]["name"], "allocatable_memory": node["status"]["allocatable"]["memory"]}))
|
||||||
|
done, node = self.execute(None, None, None, {}, check=True)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"k8s_check": "passed" if done.returncode == 0 else "failed",
|
||||||
|
"node": node,
|
||||||
|
"reason": done.stderr,
|
||||||
|
"round_trip_bytes": 1024 if done.returncode == 0 else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return done.returncode
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Lossless parsing of the optional shortest-path trace, with explicit evidence limits."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_STEP = re.compile(r"^(.*):(-?\d+) \[([^\]]+)\] ([\s\S]*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_trace(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Preserve row-level discharge evidence; never guess which node sanitized a path.
|
||||||
|
|
||||||
|
The query emits basename-only files and truncated code. A bound can be off-path;
|
||||||
|
it remains in ``guard`` even when no trace step exactly matches it. Invalid steps
|
||||||
|
raise instead of turning an unreadable trace into an empty path.
|
||||||
|
"""
|
||||||
|
raw = row.get("trace", [])
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
raise ValueError("trace must be an array")
|
||||||
|
steps = []
|
||||||
|
bound = str(row.get("bound") or "")
|
||||||
|
for index, text in enumerate(raw):
|
||||||
|
match = _STEP.fullmatch(text) if isinstance(text, str) else None
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"invalid trace step: {text!r}")
|
||||||
|
location, line, node_label, code = match.groups()
|
||||||
|
file, separator, method = location.partition(":")
|
||||||
|
roles = ["source"] if index == 0 else ["step"]
|
||||||
|
if index == len(raw) - 1:
|
||||||
|
roles = ["source", "sink"] if index == 0 else ["sink"]
|
||||||
|
if bound and code == bound:
|
||||||
|
roles.append("guard")
|
||||||
|
steps.append(
|
||||||
|
{
|
||||||
|
"file": file,
|
||||||
|
"method": method if separator else "?",
|
||||||
|
"line": int(line),
|
||||||
|
"node_label": node_label,
|
||||||
|
"code": code,
|
||||||
|
"roles": roles,
|
||||||
|
"raw": text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"steps": steps,
|
||||||
|
"sanitized": bool(row.get("sanitized", False)),
|
||||||
|
"sanitizer": {"on_path": bool(row.get("sanitized", False)), "step": None},
|
||||||
|
"guard": {"text": bound, "bounded": bool(row.get("bounded", False))},
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Host queue transport: source and task metadata only, never analyzer code."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from engine_worker_service import PENDING, client_from_env, pack, put, read, result_key, status
|
||||||
|
|
||||||
|
from openultrasast.plane.memory import PRESIGNED_ARCHIVE_SUFFIX
|
||||||
|
|
||||||
|
|
||||||
|
class Dispatcher:
|
||||||
|
def __init__(self, args, client=None, *, pause=time.sleep, clock=time.monotonic):
|
||||||
|
self.client = client if client is not None else client_from_env()
|
||||||
|
self.run = uuid.uuid4().hex
|
||||||
|
self.args, self.pause, self.clock = args, pause, clock
|
||||||
|
|
||||||
|
def execute(self, checkout, output, pin, pin_id):
|
||||||
|
task_id = f"{self.run}-{pin_id}"
|
||||||
|
task = dict(
|
||||||
|
run=self.run,
|
||||||
|
id=pin_id,
|
||||||
|
task_id=task_id,
|
||||||
|
pin=pin,
|
||||||
|
input=f"engine-queue/inputs/{self.run}/{pin_id}/source{PRESIGNED_ARCHIVE_SUFFIX}",
|
||||||
|
image=self.args.image,
|
||||||
|
deadline=self.args.deadline,
|
||||||
|
question_deadline=self.args.question_deadline,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.client.put(task["input"], pack(checkout), {})
|
||||||
|
put(self.client, PENDING + task_id, task)
|
||||||
|
end = self.clock() + self.args.queue_timeout
|
||||||
|
while self.clock() < end:
|
||||||
|
result = read(self.client, result_key(task))
|
||||||
|
if result is not None:
|
||||||
|
(output / "result.json").write_text(json.dumps(result))
|
||||||
|
self.client.delete(task["input"])
|
||||||
|
return subprocess.CompletedProcess([], 0, "", ""), result.get("worker")
|
||||||
|
self.pause(2)
|
||||||
|
# Removing pending cancels work that has not been claimed; retain source for a live lease.
|
||||||
|
self.client.delete(PENDING + task_id)
|
||||||
|
return None, None
|
||||||
|
except Exception as exc:
|
||||||
|
# SDK errors can contain endpoint URLs or credentials. Do not persist their text.
|
||||||
|
raise OSError("queue transport failed") from exc
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
return status(self.client)
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
"""Container-only Joern trace worker; host prepares all corpus and question metadata.
|
||||||
|
|
||||||
|
Keep this module independent of learn/plane and their optional dependencies. The
|
||||||
|
frozen host invokes this file directly in the shipped, offline analyzer image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import contextlib
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import selectors
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine_trace_parse import parse_trace
|
||||||
|
|
||||||
|
from openultrasast.cpg.backend import DATAFLOW_OVERLAY, JoernBackend, _render, extract_payload
|
||||||
|
from openultrasast.cpg.session import EngineSession
|
||||||
|
from openultrasast.model.contracts import ExecutionBudget
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path, value):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = path.with_suffix(".tmp")
|
||||||
|
tmp.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n")
|
||||||
|
tmp.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_path(root, name):
|
||||||
|
path = (root / name).resolve()
|
||||||
|
if not path.is_relative_to(root.resolve()):
|
||||||
|
raise ValueError(f"path escapes input root: {name}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
FRONTENDS = {"php2cpg", "pysrc2cpg", "jssrc2cpg", "javasrc2cpg", "c2cpg", "joern-parse"}
|
||||||
|
MIN_CPG_BYTES = 1024
|
||||||
|
HEAP_MB = 2560
|
||||||
|
CLUSTER_JVM_FLAGS = (
|
||||||
|
"-Xms1700m -Xmx1700m -XX:MaxMetaspaceSize=256m -XX:ReservedCodeCacheSize=128m "
|
||||||
|
"-XX:MaxDirectMemorySize=256m -Xss512k -XX:ActiveProcessorCount=2 -XX:+UseG1GC"
|
||||||
|
)
|
||||||
|
ENGINE_BUG = "engine bug: jssrc2cpg ObjectPropertyCallLinker"
|
||||||
|
EXCLUDED_DIRS = frozenset({"tests", "test", "docs", "node_modules", "vendor", "dist", "build", "templates", "migrations"})
|
||||||
|
|
||||||
|
|
||||||
|
def instrument_failure(proof):
|
||||||
|
if any(stage.get("oom") for stage in proof.get("jvm", [])):
|
||||||
|
return "JVM out of memory"
|
||||||
|
if not proof.get("files") or not proof.get("bytes"):
|
||||||
|
return "instrument read zero files or zero bytes"
|
||||||
|
if proof.get("cpg_bytes", 0) <= MIN_CPG_BYTES:
|
||||||
|
return "frontend wrote no CPG"
|
||||||
|
for stage in proof.get("jvm", []):
|
||||||
|
if stage["command"] in FRONTENDS:
|
||||||
|
if stage.get("cpg_bytes", 0) <= MIN_CPG_BYTES:
|
||||||
|
return "frontend wrote no CPG"
|
||||||
|
elif (
|
||||||
|
stage["command"] == "joern" or (stage["command"] == "joern-session" and stage.get("step") == "start" and stage.get("success"))
|
||||||
|
) and stage["seconds"] < 5:
|
||||||
|
return f"implausibly fast JVM step: {stage['command']} in {stage['seconds']:.3f}s (<5s)"
|
||||||
|
if not proof.get("jvm"):
|
||||||
|
return "no JVM step recorded"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class MeasuredBackend(JoernBackend):
|
||||||
|
"""Use the shipped backend with process and resident-session watchdogs."""
|
||||||
|
|
||||||
|
def __init__(self, deadline, question_deadline, checkpoint, heap_profile="vm"):
|
||||||
|
if heap_profile not in {"vm", "cluster"}:
|
||||||
|
raise ValueError(f"unknown heap profile: {heap_profile}")
|
||||||
|
self.heap_profile = heap_profile
|
||||||
|
super().__init__(
|
||||||
|
build_timeout=deadline,
|
||||||
|
query_timeout=deadline,
|
||||||
|
session_transport=False,
|
||||||
|
heap_mb=1700 if heap_profile == "cluster" else HEAP_MB,
|
||||||
|
)
|
||||||
|
self.deadline = time.monotonic() + deadline
|
||||||
|
self.question_deadline = question_deadline
|
||||||
|
self.checkpoint = checkpoint
|
||||||
|
self.stages = []
|
||||||
|
self.asked = []
|
||||||
|
self.answers = {}
|
||||||
|
self.completion_counts = Counter()
|
||||||
|
self.timed_out = False
|
||||||
|
self.fatal = ""
|
||||||
|
self.labelled_files = []
|
||||||
|
self.retry_pending = False
|
||||||
|
self.retries = []
|
||||||
|
self.session_fallback = ""
|
||||||
|
|
||||||
|
def _jvm_env(self):
|
||||||
|
env = super()._jvm_env()
|
||||||
|
if self.heap_profile == "cluster":
|
||||||
|
# Bound every JVM, including the child spawned by the Joern launcher.
|
||||||
|
env["JAVA_TOOL_OPTIONS"] = CLUSTER_JVM_FLAGS
|
||||||
|
env.pop("_JAVA_OPTIONS", None)
|
||||||
|
env.pop("JDK_JAVA_OPTIONS", None)
|
||||||
|
return env
|
||||||
|
|
||||||
|
def _session_stage(self, name, action):
|
||||||
|
started = time.monotonic()
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
result = action()
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
self.stages.append(
|
||||||
|
{
|
||||||
|
"command": "joern-session",
|
||||||
|
"step": name,
|
||||||
|
"success": result is not None and result is not False,
|
||||||
|
"seconds": time.monotonic() - started,
|
||||||
|
"heap_mb": self._heap_mb(),
|
||||||
|
"java_tool_options": self._jvm_env()["JAVA_TOOL_OPTIONS"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.checkpoint()
|
||||||
|
|
||||||
|
def _fallback_session(self, reason):
|
||||||
|
self.session_fallback = reason
|
||||||
|
if self._session is not None:
|
||||||
|
detail = self._session.log_tail()
|
||||||
|
if "OutOfMemoryError" in detail:
|
||||||
|
self.fatal = "JVM out of memory"
|
||||||
|
self.stages[-1]["oom"] = True
|
||||||
|
self.stages[-1]["output_tail"] = detail
|
||||||
|
self.close_session()
|
||||||
|
self.checkpoint()
|
||||||
|
|
||||||
|
def _session_for(self, cpg_path):
|
||||||
|
if self.session_fallback or self.fatal or self.timed_out:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if self._session is None:
|
||||||
|
self._session = EngineSession(
|
||||||
|
cpg_path.parent / "engine-session",
|
||||||
|
ExecutionBudget(self.deadline, 5),
|
||||||
|
self._jvm_env(),
|
||||||
|
startup_allowance_seconds=min(180, max(20, (self.deadline - time.monotonic()) / 3)),
|
||||||
|
)
|
||||||
|
if not self._session_stage("start", self._session.start):
|
||||||
|
self._fallback_session(self._session.failure or "session_start_failed")
|
||||||
|
return None
|
||||||
|
if self._session.loaded_graph != str(cpg_path):
|
||||||
|
if not self._session_stage("load", lambda: self._session.load(cpg_path, timeout=self.query_timeout)):
|
||||||
|
self._fallback_session(self._session.failure or "session_load_failed")
|
||||||
|
return None
|
||||||
|
self._defined.clear()
|
||||||
|
return self._session
|
||||||
|
except (OSError, RuntimeError) as exc:
|
||||||
|
self._fallback_session(f"session_start_or_load:{exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _session_payload(self, cpg_path, query, params):
|
||||||
|
session = self._session_for(cpg_path)
|
||||||
|
if session is None:
|
||||||
|
return None
|
||||||
|
end = min(self.deadline, time.monotonic() + self.query_timeout)
|
||||||
|
question_end = end
|
||||||
|
prior_asked = list(self.asked)
|
||||||
|
prior_answers = dict(self.answers)
|
||||||
|
prior_counts = self.completion_counts.copy()
|
||||||
|
consumed = 0
|
||||||
|
streamed = ""
|
||||||
|
|
||||||
|
def observe(output):
|
||||||
|
nonlocal consumed, question_end, streamed
|
||||||
|
streamed = output
|
||||||
|
lines = output[consumed:].splitlines(keepends=True)
|
||||||
|
for line in lines:
|
||||||
|
if not line.endswith("\n"):
|
||||||
|
break
|
||||||
|
consumed += len(line)
|
||||||
|
try:
|
||||||
|
event = json.loads(line)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if not isinstance(event, dict):
|
||||||
|
continue
|
||||||
|
if "__question__" in event:
|
||||||
|
self.asked.append(event["__question__"])
|
||||||
|
question_end = min(end, time.monotonic() + self.question_deadline)
|
||||||
|
if "id" in event and "rows" in event:
|
||||||
|
self.answers[event["id"]] = event["rows"]
|
||||||
|
self.completion_counts[event["id"]] += 1
|
||||||
|
question_end = end
|
||||||
|
self.checkpoint()
|
||||||
|
if time.monotonic() >= min(end, question_end):
|
||||||
|
self.timed_out = True
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run():
|
||||||
|
source = (self.queries_dir / f"{query}.sc").read_text()
|
||||||
|
# Same shipped script/parameters as JoernBackend, using the graph loaded above.
|
||||||
|
if source.count("importCpg(cpgFile)") != 1:
|
||||||
|
return None
|
||||||
|
symbol = f"ousast_{query}"
|
||||||
|
if query not in self._defined:
|
||||||
|
source = source.replace("importCpg(cpgFile)", "()", 1)
|
||||||
|
source = source.replace("@main def exec(", f"def {symbol}(", 1)
|
||||||
|
if session.define(source, timeout=max(0, end - time.monotonic())) is None:
|
||||||
|
return None
|
||||||
|
self._defined.add(query)
|
||||||
|
arguments = ", ".join(f"{name} = {json.dumps(_render(value))}" for name, value in sorted(params.items()))
|
||||||
|
session.output_observer = observe
|
||||||
|
try:
|
||||||
|
answer = session.evaluate(f"{symbol}({arguments})", timeout=max(0, end - time.monotonic()))
|
||||||
|
finally:
|
||||||
|
session.output_observer = None
|
||||||
|
if answer is not None:
|
||||||
|
observe(answer.stdout)
|
||||||
|
return answer.stdout
|
||||||
|
return None
|
||||||
|
|
||||||
|
failure = ""
|
||||||
|
try:
|
||||||
|
body = self._session_stage(query, run)
|
||||||
|
except (OSError, RuntimeError) as exc:
|
||||||
|
body = None
|
||||||
|
failure = f"session_{query}:{exc}"
|
||||||
|
if self.timed_out:
|
||||||
|
self.close_session()
|
||||||
|
return streamed
|
||||||
|
parsed = extract_payload(body) if body is not None else None
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
# Discard markers from an unverified attempt before replaying the script.
|
||||||
|
self.asked[:] = prior_asked
|
||||||
|
self.answers = prior_answers
|
||||||
|
self.completion_counts = prior_counts
|
||||||
|
self._fallback_session(failure or session.failure or f"session_{query}_payload_missing")
|
||||||
|
return None
|
||||||
|
if query == "census":
|
||||||
|
self.stages[-1]["files"] = int(parsed.get("files", 0))
|
||||||
|
if self.stages[-1]["files"] <= 0:
|
||||||
|
self.fatal = "query census read zero files"
|
||||||
|
return body
|
||||||
|
|
||||||
|
def _apply_overlays(self, cpg_path, scratch):
|
||||||
|
session = self._session_for(cpg_path)
|
||||||
|
result = False
|
||||||
|
if session is not None:
|
||||||
|
body = self._session_payload(cpg_path, "overlay", {"cpgFile": str(cpg_path)})
|
||||||
|
saved = session.scratch / "workspace" / cpg_path.name / "cpg.bin"
|
||||||
|
if body is not None and saved.is_file():
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.copyfile(saved, cpg_path)
|
||||||
|
result = True
|
||||||
|
elif body is not None:
|
||||||
|
self._fallback_session("session_overlay_saved_graph_missing")
|
||||||
|
if not result:
|
||||||
|
result = super()._apply_overlays(cpg_path, scratch)
|
||||||
|
# An optional overlay failure must not prevent the taint-only attempt.
|
||||||
|
if self.fatal == ENGINE_BUG and not self.timed_out:
|
||||||
|
self.retry_pending = True
|
||||||
|
self.fatal = ""
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _batch_once(self, cpg_path, query, requests):
|
||||||
|
if self.fatal or self.timed_out:
|
||||||
|
return None
|
||||||
|
if not self.retry_pending:
|
||||||
|
answers = super()._batch_once(cpg_path, query, requests)
|
||||||
|
if self.fatal != ENGINE_BUG:
|
||||||
|
return answers
|
||||||
|
if query != "taint" or self.timed_out or self.retries:
|
||||||
|
self.fatal = ENGINE_BUG
|
||||||
|
return None
|
||||||
|
self.retry_pending = False
|
||||||
|
self.fatal = ""
|
||||||
|
retry = {"reason": ENGINE_BUG, "mode": "taint-only", "completed": False}
|
||||||
|
self.retries.append(retry)
|
||||||
|
# Do not reuse rows/markers from the failed attempt as completion evidence.
|
||||||
|
self.asked.clear()
|
||||||
|
self.answers.clear()
|
||||||
|
self.completion_counts.clear()
|
||||||
|
original = self.queries_dir
|
||||||
|
with tempfile.TemporaryDirectory(prefix="trace-taint-", dir=cpg_path.parent) as scratch:
|
||||||
|
script = (original / "taint.sc").read_text()
|
||||||
|
marker = "importCpg(cpgFile)"
|
||||||
|
if script.count(marker) != 1:
|
||||||
|
self.fatal = ENGINE_BUG
|
||||||
|
return None
|
||||||
|
# Loading without enhancement skips the failing JS postprocessing passes.
|
||||||
|
# A raw graph without dataflow cannot establish a negative answer.
|
||||||
|
script = script.replace(
|
||||||
|
marker,
|
||||||
|
"importCpg(cpgFile, enhance = false)\n"
|
||||||
|
f' require(cpg.metaData.overlays.l.contains("{DATAFLOW_OVERLAY}"), '
|
||||||
|
'"taint-only retry requires saved dataflowOss overlay")',
|
||||||
|
)
|
||||||
|
self.queries_dir = Path(scratch)
|
||||||
|
(self.queries_dir / "taint.sc").write_text(script)
|
||||||
|
try:
|
||||||
|
answers = super()._batch_once(cpg_path, query, requests)
|
||||||
|
finally:
|
||||||
|
self.queries_dir = original
|
||||||
|
retry["completed"] = answers is not None and not self.fatal and not self.timed_out
|
||||||
|
if not retry["completed"]:
|
||||||
|
self.fatal = ENGINE_BUG
|
||||||
|
return answers
|
||||||
|
|
||||||
|
def _run(self, command, *, timeout, cwd=None):
|
||||||
|
if self.fatal or self.timed_out:
|
||||||
|
return None
|
||||||
|
if time.monotonic() >= self.deadline:
|
||||||
|
self.timed_out = True
|
||||||
|
return None
|
||||||
|
started = time.monotonic()
|
||||||
|
end = min(self.deadline, started + timeout)
|
||||||
|
question_end = end
|
||||||
|
binary = Path(command[0]).name
|
||||||
|
frontend = binary in FRONTENDS
|
||||||
|
query = binary == "joern" and "--script" in command
|
||||||
|
output = bytearray()
|
||||||
|
pending = b""
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, env=self._jvm_env(), start_new_session=True
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with selectors.DefaultSelector() as selector:
|
||||||
|
selector.register(process.stdout, selectors.EVENT_READ)
|
||||||
|
while selector.get_map():
|
||||||
|
if time.monotonic() >= min(end, question_end):
|
||||||
|
self.timed_out = True
|
||||||
|
break
|
||||||
|
for key, _ in selector.select(min(0.2, max(0, min(end, question_end) - time.monotonic()))):
|
||||||
|
chunk = os.read(key.fileobj.fileno(), 65536)
|
||||||
|
if not chunk:
|
||||||
|
selector.unregister(key.fileobj)
|
||||||
|
continue
|
||||||
|
output.extend(chunk)
|
||||||
|
pending += chunk
|
||||||
|
while b"\n" in pending:
|
||||||
|
line, pending = pending.split(b"\n", 1)
|
||||||
|
try:
|
||||||
|
event = json.loads(line)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if not isinstance(event, dict):
|
||||||
|
continue
|
||||||
|
if "__question__" in event:
|
||||||
|
self.asked.append(event["__question__"])
|
||||||
|
question_end = min(end, time.monotonic() + self.question_deadline)
|
||||||
|
if "id" in event and "rows" in event:
|
||||||
|
self.answers[event["id"]] = event["rows"]
|
||||||
|
self.completion_counts[event["id"]] += 1
|
||||||
|
question_end = end
|
||||||
|
self.checkpoint()
|
||||||
|
if not self.timed_out:
|
||||||
|
process.wait(timeout=max(0.01, end - time.monotonic()))
|
||||||
|
finally:
|
||||||
|
with contextlib.suppress(ProcessLookupError):
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
process.wait(timeout=5)
|
||||||
|
process.stdout.close()
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
if frontend or query:
|
||||||
|
stage = {
|
||||||
|
"command": binary,
|
||||||
|
"argv": command,
|
||||||
|
"heap_mb": self._heap_mb(),
|
||||||
|
"java_tool_options": self._jvm_env()["JAVA_TOOL_OPTIONS"],
|
||||||
|
"seconds": elapsed,
|
||||||
|
"exit": process.returncode,
|
||||||
|
"oom": b"OutOfMemoryError" in output,
|
||||||
|
}
|
||||||
|
lines = output.decode(errors="replace").splitlines()
|
||||||
|
stage["labelled_file_logs"] = {
|
||||||
|
file: [line for line in lines if file in line or Path(file).name in line]
|
||||||
|
for file in self.labelled_files
|
||||||
|
if any(file in line or Path(file).name in line for line in lines)
|
||||||
|
}
|
||||||
|
self.stages.append(stage)
|
||||||
|
if query and process.returncode != 0:
|
||||||
|
# stderr is merged into stdout above; drop JVM frames before taking
|
||||||
|
# the tail so they cannot crowd out the exception's message.
|
||||||
|
lines = [
|
||||||
|
line.strip()
|
||||||
|
for line in output.decode(errors="replace").splitlines()
|
||||||
|
if line.strip() and not line.strip().startswith(("at ", "... "))
|
||||||
|
]
|
||||||
|
stage["output_tail"] = "\n".join(lines[-20:])
|
||||||
|
if not self.timed_out:
|
||||||
|
self.fatal = f"joern query exited {process.returncode}"
|
||||||
|
if frontend:
|
||||||
|
# Capture the exact frontend output before build failure cleanup or overlays.
|
||||||
|
output_arg = next((i + 1 for i, arg in enumerate(command[:-1]) if arg in {"-o", "--output"}), None)
|
||||||
|
cpg_path = (Path(cwd or Path.cwd()) / command[output_arg]).resolve() if output_arg is not None else None
|
||||||
|
stage["cpg_path"] = str(cpg_path) if cpg_path is not None else None
|
||||||
|
stage["cpg_bytes"] = cpg_path.stat().st_size if cpg_path is not None and cpg_path.is_file() else 0
|
||||||
|
if stage["cpg_bytes"] <= MIN_CPG_BYTES:
|
||||||
|
self.fatal = "frontend wrote no CPG"
|
||||||
|
elif elapsed < 5 and not self.fatal:
|
||||||
|
self.fatal = f"implausibly fast JVM step: {command[0]} in {elapsed:.3f}s (<5s)"
|
||||||
|
if b"ObjectPropertyCallLinker failed" in output and b"Assignment statement with 3 arguments" in output:
|
||||||
|
self.fatal = ENGINE_BUG
|
||||||
|
stage["engine_bug"] = ENGINE_BUG
|
||||||
|
if b"OutOfMemoryError" in output:
|
||||||
|
self.fatal = "JVM out of memory"
|
||||||
|
self.checkpoint()
|
||||||
|
return subprocess.CompletedProcess(command, process.returncode if not self.fatal else 1, output.decode(errors="replace"), "")
|
||||||
|
|
||||||
|
|
||||||
|
def unit_record(unit, status, reason="", **extra):
|
||||||
|
return {
|
||||||
|
**unit,
|
||||||
|
"status": status,
|
||||||
|
"reason": reason,
|
||||||
|
"witness_rows": [],
|
||||||
|
"traces": [],
|
||||||
|
"instrument": {"files": [], "bytes": 0, "cpg_path": None, "cpg_bytes": 0, "jvm": []},
|
||||||
|
"questions": {},
|
||||||
|
"questions_asked": [],
|
||||||
|
"questions_completed": [],
|
||||||
|
**extra,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def worker_language(pin, root, out, deadline, question_deadline, persist=write_json, heap_profile="vm"):
|
||||||
|
proof = {
|
||||||
|
"files": [],
|
||||||
|
"bytes": 0,
|
||||||
|
"cpg_path": None,
|
||||||
|
"cpg_bytes": 0,
|
||||||
|
"jvm": [],
|
||||||
|
"heap_mb": 1700 if heap_profile == "cluster" else HEAP_MB,
|
||||||
|
"heap_profile": heap_profile,
|
||||||
|
"java_tool_options": CLUSTER_JVM_FLAGS
|
||||||
|
if heap_profile == "cluster"
|
||||||
|
else f"{os.environ.get('JAVA_TOOL_OPTIONS', '').strip()} -Xmx{HEAP_MB}m".strip(),
|
||||||
|
"excludes": sorted(EXCLUDED_DIRS),
|
||||||
|
"materialization": pin.get("materialization", {}),
|
||||||
|
}
|
||||||
|
record = {
|
||||||
|
**pin,
|
||||||
|
"done": False,
|
||||||
|
"instrument": proof,
|
||||||
|
"units": [unit_record(u, "unsupported" if not u["supported"] else "timeout", u["reason"]) for u in pin["units"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkpoint():
|
||||||
|
# Checkpoints retain raw completed rows even if the outer container deadline fires.
|
||||||
|
# They remain timeout until the finished JVM's instrument proof is validated.
|
||||||
|
for stage in backend.stages:
|
||||||
|
if "cpg_path" in stage:
|
||||||
|
proof["cpg_path"] = stage["cpg_path"]
|
||||||
|
proof["cpg_bytes"] = stage["cpg_bytes"]
|
||||||
|
if getattr(backend, "session_fallback", ""):
|
||||||
|
proof["session_fallback"] = backend.session_fallback
|
||||||
|
for row in record["units"]:
|
||||||
|
rid = row["unit"]
|
||||||
|
row["instrument"] = proof
|
||||||
|
row["questions_asked"] = [rid] if rid in backend.asked else []
|
||||||
|
if rid in backend.answers:
|
||||||
|
row["questions_completed"] = [rid]
|
||||||
|
row["witness_rows"] = [r for r in backend.answers[rid] if isinstance(r, dict) and "kind" not in r and "sink" in r]
|
||||||
|
persist(out, record)
|
||||||
|
|
||||||
|
backend = MeasuredBackend(deadline, question_deadline, checkpoint, **({"heap_profile": heap_profile} if heap_profile != "vm" else {}))
|
||||||
|
proof["jvm"] = backend.stages
|
||||||
|
proof["retries"] = getattr(backend, "retries", [])
|
||||||
|
backend.labelled_files = sorted({u["file"] for u in pin["units"] if u["supported"]})
|
||||||
|
cpg = None
|
||||||
|
try:
|
||||||
|
active = [u for u in pin["units"] if u["supported"]]
|
||||||
|
for file in sorted({u["file"] for u in active}):
|
||||||
|
data = safe_path(root, file).read_bytes()
|
||||||
|
proof["files"].append({"file": file, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
||||||
|
proof["bytes"] += len(data)
|
||||||
|
print(f"read {file}: {len(data)} bytes", flush=True)
|
||||||
|
if not proof["bytes"]:
|
||||||
|
raise ValueError("instrument read zero files or zero bytes")
|
||||||
|
languages = {"javascript" if u["language"] == "typescript" else u["language"] for u in active}
|
||||||
|
if len(languages) != 1:
|
||||||
|
raise ValueError(f"one pin has multiple frontend languages: {sorted(languages)}")
|
||||||
|
language = next(iter(languages))
|
||||||
|
requests = {rid: {key: _render(value) for key, value in params.items()} for rid, params in pin["questions"].items()}
|
||||||
|
if set(requests) != {u["unit"] for u in active}:
|
||||||
|
raise ValueError("prepared questions do not match supported units")
|
||||||
|
record["questions"] = requests
|
||||||
|
for unit_row in record["units"]:
|
||||||
|
unit_row["questions"] = {unit_row["unit"]: requests[unit_row["unit"]]} if unit_row["unit"] in requests else {}
|
||||||
|
persist(out, record)
|
||||||
|
cpg = backend.build(root, language=language)
|
||||||
|
if cpg is None:
|
||||||
|
raise ValueError(backend.fatal or backend.last_failure or ("pin deadline" if backend.timed_out else "CPG build failed"))
|
||||||
|
if proof["cpg_path"] is None:
|
||||||
|
proof["cpg_path"] = str(cpg.cpg_path)
|
||||||
|
proof["cpg_bytes"] = cpg.cpg_path.stat().st_size if cpg.cpg_path.is_file() else 0
|
||||||
|
failure = instrument_failure(proof)
|
||||||
|
if failure:
|
||||||
|
raise ValueError(failure)
|
||||||
|
if cpg.run_batch_once is None:
|
||||||
|
raise ValueError("backend offers no batch operation")
|
||||||
|
answers = cpg.run_batch_once("taint", requests) or {}
|
||||||
|
record["census"] = answers.get("__census__", [])
|
||||||
|
failure = backend.fatal or instrument_failure(proof)
|
||||||
|
if not backend.asked and not backend.timed_out:
|
||||||
|
failure = failure or "query produced no markers"
|
||||||
|
census = record["census"]
|
||||||
|
if census and int(census[0].get("files", 0)) <= 0:
|
||||||
|
failure = failure or "query census read zero files"
|
||||||
|
elif not census and not backend.timed_out:
|
||||||
|
failure = failure or "query census is missing"
|
||||||
|
for index, unit in enumerate(pin["units"]):
|
||||||
|
if not unit["supported"]:
|
||||||
|
continue
|
||||||
|
rid = unit["unit"]
|
||||||
|
extra = {
|
||||||
|
"questions": {rid: requests[rid]},
|
||||||
|
"questions_asked": [rid] if rid in backend.asked else [],
|
||||||
|
"questions_completed": [rid] if rid in answers else [],
|
||||||
|
"instrument": proof,
|
||||||
|
}
|
||||||
|
rows = answers.get(rid)
|
||||||
|
# A batch across backend shards is complete only when every shard answered.
|
||||||
|
# The legacy merger can return a union even when one shard was killed.
|
||||||
|
shards = int(census[0].get("shards", 1)) if census else 1
|
||||||
|
shard_incomplete = shards > 1 and getattr(backend, "completion_counts", {}).get(rid, 0) < shards
|
||||||
|
if rows is not None:
|
||||||
|
raw_flows = [r for r in rows if isinstance(r, dict) and "kind" not in r and "sink" in r]
|
||||||
|
extra.update(witness_rows=raw_flows, traces=[parse_trace(r) for r in raw_flows])
|
||||||
|
if shard_incomplete:
|
||||||
|
extra["questions_completed"] = []
|
||||||
|
names = census[0].get("file_names", []) if census else []
|
||||||
|
file_missing = not any(str(p).removeprefix("./") == unit["file"] or str(p).endswith("/" + unit["file"]) for p in names)
|
||||||
|
dropped = file_missing and rows is not None
|
||||||
|
if failure or dropped:
|
||||||
|
status, reason = "failed", failure or "labelled file was dropped or absent from query census"
|
||||||
|
if dropped:
|
||||||
|
logs = [line for stage in backend.stages for line in stage.get("labelled_file_logs", {}).get(unit["file"], [])]
|
||||||
|
reason += "\n" + ("\n".join(dict.fromkeys(logs)) if logs else "frontend emitted no log lines mentioning labelled file")
|
||||||
|
elif rows is None or shard_incomplete:
|
||||||
|
status, reason = ("timeout" if backend.timed_out else "failed"), "question did not complete"
|
||||||
|
elif not rows and rid not in backend.asked:
|
||||||
|
status, reason = "failed", "query produced no markers"
|
||||||
|
else:
|
||||||
|
flows = [r for r in rows if isinstance(r, dict) and "kind" not in r and "sink" in r]
|
||||||
|
if any(not row.get("trace") for row in flows):
|
||||||
|
raise ValueError("taint witness returned without requested trace")
|
||||||
|
extra.update(witness_rows=flows, traces=[parse_trace(r) for r in flows])
|
||||||
|
status, reason = ("path" if flows else "asked-nothing"), ""
|
||||||
|
record["units"][index] = unit_record(unit, status, reason, **extra)
|
||||||
|
except (OSError, ValueError, RuntimeError) as exc:
|
||||||
|
for index, unit in enumerate(pin["units"]):
|
||||||
|
if unit["supported"]:
|
||||||
|
record["units"][index] = unit_record(
|
||||||
|
unit,
|
||||||
|
"timeout" if backend.timed_out and not backend.fatal else "failed",
|
||||||
|
str(exc),
|
||||||
|
instrument=proof,
|
||||||
|
questions={unit["unit"]: record["questions"][unit["unit"]]} if unit["unit"] in record.get("questions", {}) else {},
|
||||||
|
witness_rows=[r for r in backend.answers.get(unit["unit"], []) if isinstance(r, dict) and "sink" in r],
|
||||||
|
questions_completed=[unit["unit"]] if unit["unit"] in backend.answers else [],
|
||||||
|
questions_asked=[unit["unit"]] if unit["unit"] in backend.asked else [],
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if hasattr(backend, "close_session"):
|
||||||
|
backend.close_session()
|
||||||
|
if cpg and cpg.cleanup:
|
||||||
|
cpg.cleanup()
|
||||||
|
for stage in backend.stages:
|
||||||
|
if stage["command"] == "joern" and stage.get("exit") and "output_tail" in stage:
|
||||||
|
detail = f"joern query exited {stage['exit']}: {stage['output_tail'] or 'no output'}"
|
||||||
|
for row in record["units"]:
|
||||||
|
if row["supported"] and row["status"] in {"failed", "timeout"}:
|
||||||
|
row["reason"] = f"{row['reason']}\n{detail}".strip()
|
||||||
|
record["questions_asked"] = list(dict.fromkeys(backend.asked))
|
||||||
|
record["questions_completed"] = [rid for u in record["units"] for rid in u["questions_completed"]]
|
||||||
|
record["done"] = True
|
||||||
|
persist(out, record)
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def worker(pin, root, out, deadline, question_deadline, heap_profile="vm"):
|
||||||
|
"""Run language partitions sequentially under one shared container deadline."""
|
||||||
|
languages = sorted({"javascript" if u["language"] == "typescript" else u["language"] for u in pin["units"] if u["supported"]})
|
||||||
|
if len(languages) <= 1:
|
||||||
|
return worker_language(pin, root, out, deadline, question_deadline, heap_profile=heap_profile)
|
||||||
|
end = time.monotonic() + deadline
|
||||||
|
record = {
|
||||||
|
**pin,
|
||||||
|
"done": False,
|
||||||
|
"units": [
|
||||||
|
unit_record(u, "timeout" if u["supported"] else "unsupported", "pin deadline" if u["supported"] else u["reason"])
|
||||||
|
for u in pin["units"]
|
||||||
|
],
|
||||||
|
"instruments": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def persist(_, partial):
|
||||||
|
updates = {u["unit"]: u for u in partial["units"]}
|
||||||
|
record["units"] = [updates.get(u["unit"], u) for u in record["units"]]
|
||||||
|
record["instruments"][language] = partial["instrument"]
|
||||||
|
record["questions_asked"] = [rid for u in record["units"] for rid in u["questions_asked"]]
|
||||||
|
record["questions_completed"] = [rid for u in record["units"] for rid in u["questions_completed"]]
|
||||||
|
write_json(out, record)
|
||||||
|
|
||||||
|
write_json(out, record)
|
||||||
|
for language in languages:
|
||||||
|
remaining = end - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
units = [
|
||||||
|
u for u in pin["units"] if u["supported"] and ("javascript" if u["language"] == "typescript" else u["language"]) == language
|
||||||
|
]
|
||||||
|
questions = {u["unit"]: pin["questions"][u["unit"]] for u in units}
|
||||||
|
worker_language(
|
||||||
|
{**pin, "units": units, "questions": questions}, root, out, remaining, question_deadline, persist, heap_profile=heap_profile
|
||||||
|
)
|
||||||
|
record["done"] = True
|
||||||
|
write_json(out, record)
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--worker", type=Path, required=True)
|
||||||
|
parser.add_argument("--deadline", type=int, default=900)
|
||||||
|
parser.add_argument("--question-deadline", type=int, default=120)
|
||||||
|
parser.add_argument("--heap-profile", choices=("cluster", "vm"), default="vm")
|
||||||
|
parser.add_argument("--root", type=Path, default=Path("/case"))
|
||||||
|
parser.add_argument("--output", type=Path, default=Path("/out/result.json"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if min(args.deadline, args.question_deadline) <= 0:
|
||||||
|
parser.error("deadlines must be positive")
|
||||||
|
worker(json.loads(args.worker.read_text()), args.root, args.output, args.deadline, args.question_deadline, args.heap_profile)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
"""Image-resident queue workers. Requires strongly consistent S3 GET/PUT/LIST.
|
||||||
|
|
||||||
|
Per-task Lamport bakery tickets use only ordinary object operations. A choosing
|
||||||
|
marker precedes ticket selection; contenders wait for choosing peers and lower
|
||||||
|
(ticket, worker) pairs. Late arrivals cannot displace a running owner. Leases
|
||||||
|
assume bounded clock skew (less than MARGIN / 2) and a worker killed by its deadline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import contextlib
|
||||||
|
import ctypes
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine_trace_worker import unit_record, worker
|
||||||
|
|
||||||
|
from openultrasast.plane.memory import S3Client, s3_settings
|
||||||
|
|
||||||
|
PENDING = "engine-queue/pending/"
|
||||||
|
CLAIMED = "engine-queue/claimed/"
|
||||||
|
RESULTS = "engine-results/"
|
||||||
|
MARGIN = 60
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def client_from_env():
|
||||||
|
settings = s3_settings(os.environ)
|
||||||
|
bucket = os.environ.get("S3_BUCKET")
|
||||||
|
if not bucket:
|
||||||
|
raise ValueError("S3_BUCKET is required")
|
||||||
|
return S3Client(bucket=bucket, **settings)
|
||||||
|
|
||||||
|
|
||||||
|
def read(client, key):
|
||||||
|
value = client.get(key)
|
||||||
|
return json.loads(value[0]) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
def put(client, key, value):
|
||||||
|
client.put(key, json.dumps(value).encode(), {})
|
||||||
|
|
||||||
|
|
||||||
|
def result_key(task):
|
||||||
|
return f"{RESULTS}{task['run']}/{task['id']}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def pack(root):
|
||||||
|
stream = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=stream, mode="w") as archive:
|
||||||
|
archive.add(root, arcname=".")
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def unpack(data, root):
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||||
|
# Source snapshots need only regular files and directories, never links/devices.
|
||||||
|
for member in archive.getmembers():
|
||||||
|
target = (root / member.name).resolve()
|
||||||
|
if not target.is_relative_to(root.resolve()) or not (member.isfile() or member.isdir()):
|
||||||
|
raise ValueError("unsafe source archive")
|
||||||
|
archive.extractall(root, filter="data")
|
||||||
|
|
||||||
|
|
||||||
|
class Queue:
|
||||||
|
def __init__(self, client, identity=None, *, now=time.time, pause=time.sleep, stop=None):
|
||||||
|
self.client = client
|
||||||
|
self.identity = identity or f"{socket.gethostname()}-{uuid.uuid4().hex}"
|
||||||
|
self.now, self.pause = now, pause
|
||||||
|
self.stop = stop or threading.Event()
|
||||||
|
|
||||||
|
def markers(self, task_id=None):
|
||||||
|
result = []
|
||||||
|
for key in self.client.keys(CLAIMED):
|
||||||
|
if task_id is None or key.rsplit("/", 1)[-1] == task_id:
|
||||||
|
marker = read(self.client, key)
|
||||||
|
if marker:
|
||||||
|
result.append((key, marker))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def tasks(self):
|
||||||
|
tasks = {}
|
||||||
|
for key in self.client.keys(PENDING):
|
||||||
|
task = read(self.client, key)
|
||||||
|
if task:
|
||||||
|
tasks[task["task_id"]] = task
|
||||||
|
# A dead worker may have removed pending already. Its task remains in the lease.
|
||||||
|
for _, marker in self.markers():
|
||||||
|
if marker["expires"] < self.now():
|
||||||
|
task = marker["task"]
|
||||||
|
tasks.setdefault(task["task_id"], task)
|
||||||
|
return list(tasks.values())
|
||||||
|
|
||||||
|
def claim(self, task):
|
||||||
|
if read(self.client, result_key(task)) is not None or self.stop.is_set():
|
||||||
|
return None
|
||||||
|
key = f"{CLAIMED}{self.identity}/{task['task_id']}"
|
||||||
|
marker = dict(task=task, ticket=0, expires=self.now() + task["deadline"] + MARGIN)
|
||||||
|
put(self.client, key, marker)
|
||||||
|
try:
|
||||||
|
live = [m for _, m in self.markers(task["task_id"]) if m["expires"] > self.now()]
|
||||||
|
marker["ticket"] = 1 + max((m["ticket"] for m in live), default=0)
|
||||||
|
put(self.client, key, marker)
|
||||||
|
while not self.stop.is_set():
|
||||||
|
# Do not start once the execution budget no longer fits within our lease.
|
||||||
|
if self.now() >= marker["expires"] - task["deadline"] - MARGIN / 2:
|
||||||
|
break
|
||||||
|
peers = [(k, m) for k, m in self.markers(task["task_id"]) if k != key and m["expires"] > self.now()]
|
||||||
|
if any(m["ticket"] == 0 for _, m in peers):
|
||||||
|
self.pause(0.1)
|
||||||
|
continue
|
||||||
|
if any((m["ticket"], k) < (marker["ticket"], key) for k, m in peers):
|
||||||
|
break # loser backs off and can try another pending task
|
||||||
|
if read(self.client, key) != marker or read(self.client, result_key(task)) is not None:
|
||||||
|
break
|
||||||
|
self.client.delete(PENDING + task["task_id"])
|
||||||
|
return key
|
||||||
|
except Exception:
|
||||||
|
self.client.delete(key)
|
||||||
|
raise
|
||||||
|
self.client.delete(key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_one(self, execute=None):
|
||||||
|
for task in self.tasks():
|
||||||
|
key = self.claim(task)
|
||||||
|
if key is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
lease = read(self.client, key)
|
||||||
|
if lease is None:
|
||||||
|
continue
|
||||||
|
budget = min(task["deadline"], lease["expires"] - self.now() - MARGIN / 2)
|
||||||
|
if budget <= 0:
|
||||||
|
continue # Leave the expired lease's task available to recovery.
|
||||||
|
result = (execute or execute_task)(self.client, {**task, "runtime_budget": budget})
|
||||||
|
result.update(executor="queue", worker=self.identity, pod=socket.gethostname(), image=os.environ.get("ENGINE_IMAGE", ""))
|
||||||
|
put(self.client, result_key(task), result)
|
||||||
|
# Publish the result before removing either the input or the claim.
|
||||||
|
self.client.delete(task["input"])
|
||||||
|
for old_key, old in self.markers(task["task_id"]):
|
||||||
|
if old_key == key or old["expires"] < self.now():
|
||||||
|
self.client.delete(old_key)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
# Keep the lease and task for recovery, without logging credential-bearing errors.
|
||||||
|
LOG.error("task failed; lease retained for recovery")
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _child(pin, root, out, deadline, question_deadline):
|
||||||
|
os.setsid()
|
||||||
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||||
|
worker(pin, root, out, deadline, question_deadline)
|
||||||
|
|
||||||
|
|
||||||
|
def adopt_orphans():
|
||||||
|
# Linux PR_SET_CHILD_SUBREAPER: escaped Joern sessions reparent here, not
|
||||||
|
# outside the service. There is exactly one task process tree per worker.
|
||||||
|
if ctypes.CDLL(None, use_errno=True).prctl(36, 1, 0, 0, 0) != 0:
|
||||||
|
raise OSError("cannot enable task descendant cleanup")
|
||||||
|
|
||||||
|
|
||||||
|
def kill_children():
|
||||||
|
"""Freeze parents before walking children, including new-session JVMs."""
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def freeze(parent):
|
||||||
|
for entry in Path("/proc").iterdir():
|
||||||
|
if not entry.name.isdigit() or int(entry.name) in seen:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
# comm can contain spaces or parentheses. Fields after it begin at state.
|
||||||
|
fields = (entry / "stat").read_text().rsplit(")", 1)[1].split()
|
||||||
|
if int(fields[1]) != parent:
|
||||||
|
continue
|
||||||
|
pid = int(entry.name)
|
||||||
|
os.kill(pid, signal.SIGSTOP)
|
||||||
|
seen.add(pid)
|
||||||
|
freeze(pid)
|
||||||
|
except (FileNotFoundError, ProcessLookupError, PermissionError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# A child that exited during discovery can reparent descendants to this
|
||||||
|
# subreaper. Repeat until every remaining descendant is frozen.
|
||||||
|
while True:
|
||||||
|
count = len(seen)
|
||||||
|
freeze(os.getpid())
|
||||||
|
if len(seen) == count:
|
||||||
|
break
|
||||||
|
for pid in seen:
|
||||||
|
with contextlib.suppress(ProcessLookupError):
|
||||||
|
os.kill(pid, signal.SIGKILL)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_task(client, task):
|
||||||
|
adopt_orphans()
|
||||||
|
pin = task["pin"]
|
||||||
|
started = time.monotonic()
|
||||||
|
budget = task.get("runtime_budget", task["deadline"])
|
||||||
|
status, reason, code = "failed", "worker produced no result", None
|
||||||
|
with tempfile.TemporaryDirectory(prefix="engine-task-") as scratch:
|
||||||
|
root, out = Path(scratch) / "case", Path(scratch) / "result.json"
|
||||||
|
root.mkdir()
|
||||||
|
try:
|
||||||
|
if task["deadline"] > 900 or task["deadline"] <= 0:
|
||||||
|
raise ValueError("deadline exceeds deployment grace")
|
||||||
|
if task.get("image") != os.environ.get("ENGINE_IMAGE"):
|
||||||
|
raise ValueError("task image differs from deployed image")
|
||||||
|
source = client.get(task["input"])
|
||||||
|
if source is None:
|
||||||
|
raise ValueError("missing source")
|
||||||
|
unpack(source[0], root)
|
||||||
|
# The shipped worker also checks frontend input and graph output for every language.
|
||||||
|
files = [p for p in root.rglob("*") if p.is_file()]
|
||||||
|
if not files:
|
||||||
|
raise ValueError("empty source")
|
||||||
|
LOG.info("input proof: first file bytes=%d", len(files[0].read_bytes()))
|
||||||
|
if time.monotonic() - started >= budget:
|
||||||
|
status, reason = "timeout", "task input deadline"
|
||||||
|
raise TimeoutError(reason)
|
||||||
|
process = multiprocessing.get_context("fork").Process(target=_child, args=(pin, root, out, budget, task["question_deadline"]))
|
||||||
|
process.start()
|
||||||
|
try:
|
||||||
|
process.join(max(0, budget - (time.monotonic() - started)))
|
||||||
|
if process.is_alive():
|
||||||
|
status, reason = "timeout", "task deadline"
|
||||||
|
else:
|
||||||
|
reason = f"worker exit {process.exitcode}"
|
||||||
|
code = process.exitcode
|
||||||
|
finally:
|
||||||
|
# Joern creates separate sessions. Kill the whole descendant tree,
|
||||||
|
# not merely the Python wrapper's process group.
|
||||||
|
kill_children()
|
||||||
|
process.join()
|
||||||
|
# Reap adopted JVMs and their launchers after the direct child.
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
os.waitpid(-1, 0)
|
||||||
|
except ChildProcessError:
|
||||||
|
break
|
||||||
|
record = json.loads(out.read_text()) if out.exists() else {**pin, "units": []}
|
||||||
|
except Exception:
|
||||||
|
record = {**pin, "units": []}
|
||||||
|
reason = "worker input or execution failed"
|
||||||
|
if not record.get("done") or code != 0:
|
||||||
|
previous = {u["unit"]: u for u in record["units"]}
|
||||||
|
record["units"] = [
|
||||||
|
previous[u["unit"]]
|
||||||
|
if code is None and previous.get(u["unit"], {}).get("status") in {"path", "asked-nothing"}
|
||||||
|
else unit_record(u, status if u["supported"] else "unsupported", reason if u["supported"] else u["reason"])
|
||||||
|
for u in pin["units"]
|
||||||
|
]
|
||||||
|
record.update(done=True, container_exit=code, seconds=time.monotonic() - started)
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def status(client):
|
||||||
|
return {
|
||||||
|
"pending": len(client.keys(PENDING)),
|
||||||
|
"claimed": len(client.keys(CLAIMED)),
|
||||||
|
"done": len([k for k in client.keys(RESULTS) if k.endswith(".json")]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--port", type=int, default=8080)
|
||||||
|
args = parser.parse_args()
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
stop = threading.Event()
|
||||||
|
signal.signal(signal.SIGTERM, lambda *_: stop.set())
|
||||||
|
signal.signal(signal.SIGINT, lambda *_: stop.set())
|
||||||
|
client = client_from_env()
|
||||||
|
queue = Queue(client, stop=stop)
|
||||||
|
|
||||||
|
class Health(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_response(200 if self.path == "/healthz" and not stop.is_set() else 503)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, *_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
server = ThreadingHTTPServer(("0.0.0.0", args.port), Health)
|
||||||
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||||
|
try:
|
||||||
|
while not stop.is_set():
|
||||||
|
try:
|
||||||
|
if not queue.process_one():
|
||||||
|
stop.wait(2)
|
||||||
|
except Exception:
|
||||||
|
LOG.error("queue unavailable; retrying")
|
||||||
|
stop.wait(5)
|
||||||
|
finally:
|
||||||
|
server.shutdown()
|
||||||
|
server.server_close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
"""Offline audit of completed Joern results; never launches an analyzer or reads corpora.
|
||||||
|
|
||||||
|
Missing results stay in coverage, not signal denominators. Observed non-path rows
|
||||||
|
score zero for path features. Sink kinds are one-hot indicators, never ordinal
|
||||||
|
category IDs. Both signed and best-direction metrics expose reversed shortcuts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from slice_leak_audit import THRESHOLD, complete_pairs, metrics
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
UNITS = ROOT / "plane/experiments/exp-005-graph-slice.units.jsonl"
|
||||||
|
RESULTS = Path.home() / "ousast-results/plane/engine-trace"
|
||||||
|
OUTPUT = ROOT / "benchmarks/measurements/2026-10-04-joern-slice-leak-audit/record.json"
|
||||||
|
STATUSES = ("path", "asked-nothing", "failed", "timeout", "unsupported", "missing")
|
||||||
|
ASKED = {"path", "asked-nothing"}
|
||||||
|
SIGNALS = ("has_path", "shortest_path_steps", "sanitized", "guard_present", "asked")
|
||||||
|
ASYMMETRY = ("vulnerable_only", "fixed_only", "both", "neither", "not_both_asked")
|
||||||
|
|
||||||
|
|
||||||
|
def materialization_version():
|
||||||
|
# Read the producer's literal without importing its execution dependencies.
|
||||||
|
tree = ast.parse(Path(__file__).with_name("engine_trace.py").read_bytes())
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "MATERIALIZATION_VERSION" for t in node.targets):
|
||||||
|
return ast.literal_eval(node.value)
|
||||||
|
raise ValueError("Producer has no materialization version")
|
||||||
|
|
||||||
|
|
||||||
|
def features(row):
|
||||||
|
path = row["status"] == "path"
|
||||||
|
traces = row.get("traces", []) if path else []
|
||||||
|
candidates = [(i, t) for i, t in enumerate(traces) if t.get("steps")]
|
||||||
|
index, trace = min(candidates, key=lambda it: len(it[1]["steps"])) if candidates else (None, {})
|
||||||
|
witnesses = row.get("witness_rows", [])
|
||||||
|
witness = witnesses[index] if index is not None and index < len(witnesses) else {}
|
||||||
|
guard = trace.get("guard", {})
|
||||||
|
sink = witness.get("sinkKind")
|
||||||
|
if not sink:
|
||||||
|
# Retain only the call target: arguments, literals and code are not kinds.
|
||||||
|
match = re.search(r"([\w:$>.\\-]+)\s*\(", str(witness.get("sink", "")))
|
||||||
|
sink = match[1] if match else "unknown"
|
||||||
|
return {
|
||||||
|
"has_path": int(path),
|
||||||
|
"shortest_path_steps": len(trace["steps"]) if trace else None if path else 0,
|
||||||
|
"sanitized": int(bool(trace.get("sanitized", witness.get("sanitized", False)))) if trace else None if path else 0,
|
||||||
|
"guard_present": int(
|
||||||
|
bool(guard.get("text") or guard.get("bounded") or witness.get("bound") or witness.get("bounded"))
|
||||||
|
or any("guard" in s.get("roles", []) for s in trace.get("steps", []))
|
||||||
|
)
|
||||||
|
if trace
|
||||||
|
else None
|
||||||
|
if path
|
||||||
|
else 0,
|
||||||
|
"sink_kind": str(sink) if path else "no-path",
|
||||||
|
"asked": int(row["status"] in ASKED),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scored(rows, signal):
|
||||||
|
eligible = [r for r in rows if r[signal] is not None]
|
||||||
|
result = metrics(eligible, signal)
|
||||||
|
result["excluded_rows"] = len(rows) - len(eligible)
|
||||||
|
result["flagged"] = (result["pairs"] >= 10 and (result["best_direction_within_pair_accuracy"] or 0) >= THRESHOLD) or (
|
||||||
|
result["rows"] >= 10 and (result["best_direction_pooled_auc"] or 0) >= THRESHOLD
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def signal_metrics(rows, kinds):
|
||||||
|
return {
|
||||||
|
**{s: scored(rows, s) for s in SIGNALS},
|
||||||
|
"sink_kind": {kind: scored([{**r, "is_kind": int(r["sink_kind"] == kind)} for r in rows], "is_kind") for kind in kinds},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scope_report(rows, kinds):
|
||||||
|
pairs = complete_pairs(rows)
|
||||||
|
asymmetry = Counter()
|
||||||
|
honest = []
|
||||||
|
for v, f in pairs:
|
||||||
|
if not (v["asked"] and f["asked"]):
|
||||||
|
asymmetry["not_both_asked"] += 1
|
||||||
|
else:
|
||||||
|
honest.extend((v, f))
|
||||||
|
asymmetry[
|
||||||
|
"both"
|
||||||
|
if v["has_path"] and f["has_path"]
|
||||||
|
else "vulnerable_only"
|
||||||
|
if v["has_path"]
|
||||||
|
else "fixed_only"
|
||||||
|
if f["has_path"]
|
||||||
|
else "neither"
|
||||||
|
] += 1
|
||||||
|
observed = [r for r in rows if r["status"] != "missing"]
|
||||||
|
return {
|
||||||
|
"rows": len(rows),
|
||||||
|
"coverage": {s: sum(r["status"] == s for r in rows) for s in STATUSES},
|
||||||
|
"complete_pairs": len(pairs),
|
||||||
|
"pair_asymmetry": {s: asymmetry[s] for s in ASYMMETRY},
|
||||||
|
"both_asked_pairs": len(honest) // 2,
|
||||||
|
"signals": signal_metrics(observed, kinds),
|
||||||
|
"both_asked_signals": signal_metrics(honest, kinds),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def audit(units, results, *, expected_rows=501, expected_pairs=195):
|
||||||
|
if len(units) != expected_rows or len({u["unit"] for u in units}) != expected_rows:
|
||||||
|
raise ValueError(f"Expected {expected_rows} unique units")
|
||||||
|
if len(complete_pairs(units)) != expected_pairs:
|
||||||
|
raise ValueError(f"Expected {expected_pairs} complete pairs")
|
||||||
|
if not results.is_dir():
|
||||||
|
raise ValueError(f"Results directory is unreadable: {results}")
|
||||||
|
version = materialization_version()
|
||||||
|
unit_index = {u["unit"]: u for u in units}
|
||||||
|
matched = {}
|
||||||
|
files = []
|
||||||
|
counts = Counter()
|
||||||
|
for path in sorted(results.glob("*.json")):
|
||||||
|
raw = path.read_bytes()
|
||||||
|
record = json.loads(raw) # Corruption fails loudly; producer uses atomic replace.
|
||||||
|
if not isinstance(record, dict) or "repo" not in record or "pin" not in record:
|
||||||
|
continue
|
||||||
|
stale = record.get("materialization_version") != version
|
||||||
|
unfinished = record.get("done") is not True
|
||||||
|
counts["stale"] += stale
|
||||||
|
counts["unfinished"] += unfinished
|
||||||
|
counts["skipped"] += stale or unfinished
|
||||||
|
files.append((path.name, len(raw)))
|
||||||
|
if stale or unfinished:
|
||||||
|
continue
|
||||||
|
counts["accepted_files"] += 1
|
||||||
|
counts["result_rows_read"] += len(record["units"])
|
||||||
|
for row in record["units"]:
|
||||||
|
if row["unit"] not in unit_index:
|
||||||
|
continue
|
||||||
|
unit = unit_index[row["unit"]]
|
||||||
|
if any(row.get(k) != unit.get(k) for k in ("family", "label", "pair")):
|
||||||
|
raise ValueError(f"Result metadata mismatch: {row['unit']}")
|
||||||
|
if row["status"] not in STATUSES[:-1] or not row.get("source"):
|
||||||
|
raise ValueError(f"Invalid result status/source: {row['unit']}")
|
||||||
|
if row["unit"] in matched:
|
||||||
|
raise ValueError(f"Duplicate current result: {row['unit']}")
|
||||||
|
matched[row["unit"]] = {
|
||||||
|
**unit,
|
||||||
|
"source": row["source"],
|
||||||
|
"status": row["status"],
|
||||||
|
"pin_role": row.get("pin_role"),
|
||||||
|
**features(row),
|
||||||
|
}
|
||||||
|
rows = [
|
||||||
|
matched.get(u["unit"], {**u, "source": u.get("source", "unknown"), "status": "missing", **features({"status": "missing"})})
|
||||||
|
for u in units
|
||||||
|
]
|
||||||
|
kinds = sorted({r["sink_kind"] for r in rows if r["status"] != "missing"})
|
||||||
|
scopes = {"overall": scope_report(rows, kinds)}
|
||||||
|
for field in ("family", "source"):
|
||||||
|
scopes[f"by_{field}"] = {
|
||||||
|
value: scope_report([r for r in rows if r[field] == value], kinds) for value in sorted({r[field] for r in rows})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"cost_usd": 0,
|
||||||
|
"materialization_version": version,
|
||||||
|
"threshold": THRESHOLD,
|
||||||
|
"minimum_samples": 10,
|
||||||
|
"unit_rows_read": len(units),
|
||||||
|
"matched_rows": len(matched),
|
||||||
|
"inputs": {
|
||||||
|
**{k: counts[k] for k in ("accepted_files", "result_rows_read", "stale", "unfinished", "skipped")},
|
||||||
|
"files_read": len(files),
|
||||||
|
"bytes_read": sum(size for _, size in files),
|
||||||
|
# Hash a canonical inventory; filenames and paths may identify repositories.
|
||||||
|
"files_sha256": hashlib.sha256(json.dumps(sorted(files), separators=(",", ":")).encode()).hexdigest(),
|
||||||
|
},
|
||||||
|
"semantics": {
|
||||||
|
"signals": "Shortest nonempty trace (first on ties); its sanitizer, guard and corresponding witness sink call target.",
|
||||||
|
"missing": "Coverage only; absent source metadata is unknown. Skipped result units are never consumed.",
|
||||||
|
"non_path": "Observed non-path rows score zero; path rows lacking trace evidence have null trace features.",
|
||||||
|
"asked": "path or asked-nothing=1; failed, timeout, unsupported=0; missing excluded from metrics.",
|
||||||
|
"asymmetry": "Disjoint categories; not_both_asked takes precedence over path asymmetry.",
|
||||||
|
"flags": "Best of both directions, >=0.70 with >=10 pairs for pair accuracy or >=10 rows for AUC.",
|
||||||
|
"sink_kind": "One binary indicator per observed call target, including no-path/unknown; no ordinal encoding.",
|
||||||
|
"limits": "Exploratory associations, not proof of leakage; running input snapshot, no multiple-testing correction.",
|
||||||
|
},
|
||||||
|
**scopes,
|
||||||
|
"units": [{k: r[k] for k in ("unit", "pair", "family", "label", "fold", "source", "status", *SIGNALS, "sink_kind")} for r in rows],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--results", type=Path, default=RESULTS)
|
||||||
|
parser.add_argument("--units", type=Path, default=UNITS)
|
||||||
|
parser.add_argument("--output", type=Path, default=OUTPUT)
|
||||||
|
args = parser.parse_args()
|
||||||
|
raw = args.units.read_bytes()
|
||||||
|
units = [json.loads(line) for line in raw.splitlines() if line.strip()]
|
||||||
|
report = audit(units, args.results)
|
||||||
|
report["units_input"] = {"bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest()}
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||||
|
overall, inputs = report["overall"], report["inputs"]
|
||||||
|
lines = [
|
||||||
|
f"Joern leak audit: $0; materialization_version={report['materialization_version']}",
|
||||||
|
f"Units read: {len(units)}; bytes={len(raw)}; file={args.units}",
|
||||||
|
f"Result files read: {inputs['files_read']}; bytes={inputs['bytes_read']}; accepted={inputs['accepted_files']}",
|
||||||
|
f"Skipped: {inputs['skipped']} (stale={inputs['stale']}, unfinished={inputs['unfinished']}; counts overlap)",
|
||||||
|
f"Result rows read: {inputs['result_rows_read']}; matched={report['matched_rows']}",
|
||||||
|
f"Coverage: {overall['coverage']}",
|
||||||
|
f"Pairs: {overall['complete_pairs']}; both asked={overall['both_asked_pairs']}",
|
||||||
|
f"Asymmetry: {overall['pair_asymmetry']}",
|
||||||
|
]
|
||||||
|
for signal in (*SIGNALS, "sink_kind"):
|
||||||
|
|
||||||
|
def brief(value, signal=signal):
|
||||||
|
if signal == "sink_kind":
|
||||||
|
return {k: brief_metric(m) for k, m in value.items()}
|
||||||
|
return brief_metric(value)
|
||||||
|
|
||||||
|
lines.append(f"{signal}: all={brief(overall['signals'][signal])}; both asked={brief(overall['both_asked_signals'][signal])}")
|
||||||
|
lines.append(f"Family/source metrics and input inventory digest: {args.output}")
|
||||||
|
print("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
def brief_metric(metric):
|
||||||
|
return {k: metric[k] for k in ("rows", "pairs", "within_pair_accuracy", "pooled_auc", "flagged")}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""Read-only, zero-model slice audit. Run: env -u OPENROUTER_API_KEY python benchmarks/learn/slice_leak_audit.py.
|
||||||
|
|
||||||
|
Only sources.toml itself is read, never its referenced corpora. Stored source IDs
|
||||||
|
are the reporting strata (the store merges advisory pair catalogs into `pairs`).
|
||||||
|
Ties get half credit. Both signed and best-direction scores are recorded; like
|
||||||
|
learn.leaks, a reversed label shortcut is still a leak. Missing classes/pairs
|
||||||
|
produce null metrics, not plausible zeros. Unsupported slices score zero for both
|
||||||
|
indicators and remain separately counted in coverage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from openultrasast.config import load_dotenv
|
||||||
|
from openultrasast.learn.examples import load_examples
|
||||||
|
from openultrasast.learn.labels import load_sources
|
||||||
|
from openultrasast.learn.slice import function_slice
|
||||||
|
from openultrasast.plane.memory import MemoryStore, open_store
|
||||||
|
|
||||||
|
UNITS = Path("plane/experiments/exp-004-pooled-block-gate.units.jsonl")
|
||||||
|
OUTPUT = Path("benchmarks/measurements/2026-10-03-graph-slice-leak-audit/record.json")
|
||||||
|
THRESHOLD = 0.70
|
||||||
|
|
||||||
|
|
||||||
|
def complete_pairs(rows: Sequence[Mapping[str, Any]]) -> list[tuple[Mapping[str, Any], Mapping[str, Any]]]:
|
||||||
|
groups: dict[tuple[str, str], list[Mapping[str, Any]]] = defaultdict(list)
|
||||||
|
for row in rows:
|
||||||
|
if row.get("pair"):
|
||||||
|
groups[(row["family"], row["pair"])].append(row)
|
||||||
|
pairs = []
|
||||||
|
for members in groups.values():
|
||||||
|
if len(members) == 2 and {m["label"] for m in members} == {0, 1}:
|
||||||
|
v, f = sorted(members, key=lambda m: -m["label"])
|
||||||
|
if v["group"] != f["group"] or v["fold"] != f["fold"]:
|
||||||
|
raise ValueError("Pair crosses group or fold")
|
||||||
|
pairs.append((v, f))
|
||||||
|
elif len(members) != 1:
|
||||||
|
raise ValueError("Ambiguous pair")
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
def metrics(rows: Sequence[Mapping[str, Any]], signal: str) -> dict[str, Any]:
|
||||||
|
pairs = complete_pairs(rows)
|
||||||
|
higher = sum(v[signal] > f[signal] for v, f in pairs)
|
||||||
|
lower = sum(v[signal] < f[signal] for v, f in pairs)
|
||||||
|
ties = len(pairs) - higher - lower
|
||||||
|
pair_score = (higher + 0.5 * ties) / len(pairs) if pairs else None
|
||||||
|
positive = [r[signal] for r in rows if r["label"] == 1]
|
||||||
|
negative = [r[signal] for r in rows if r["label"] == 0]
|
||||||
|
auc = (
|
||||||
|
sum((v > f) + 0.5 * (v == f) for v in positive for f in negative) / (len(positive) * len(negative))
|
||||||
|
if positive and negative
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
best_pair = max(pair_score, 1 - pair_score) if pair_score is not None else None
|
||||||
|
best_auc = max(auc, 1 - auc) if auc is not None else None
|
||||||
|
return {
|
||||||
|
"rows": len(rows),
|
||||||
|
"pairs": len(pairs),
|
||||||
|
"positive": len(positive),
|
||||||
|
"negative": len(negative),
|
||||||
|
"higher_on_vulnerable": higher,
|
||||||
|
"higher_on_fixed": lower,
|
||||||
|
"ties": ties,
|
||||||
|
"within_pair_accuracy": pair_score,
|
||||||
|
"pooled_auc": auc,
|
||||||
|
"best_direction_within_pair_accuracy": best_pair,
|
||||||
|
"best_direction_pooled_auc": best_auc,
|
||||||
|
"flagged": any(v is not None and v >= THRESHOLD for v in (best_pair, best_auc)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def audit(store: MemoryStore, units: Sequence[Mapping[str, Any]], *, expected_rows: int = 501, expected_pairs: int = 195) -> dict[str, Any]:
|
||||||
|
if len(units) != expected_rows or len({u["unit"] for u in units}) != expected_rows:
|
||||||
|
raise ValueError(f"Expected {expected_rows} unique units, read {len(units)}")
|
||||||
|
if len(complete_pairs(units)) != expected_pairs:
|
||||||
|
raise ValueError(f"Expected {expected_pairs} complete pairs")
|
||||||
|
examples = load_examples(store)
|
||||||
|
index = {e.id: e for e in examples}
|
||||||
|
if len(index) != len(examples):
|
||||||
|
raise ValueError("Duplicate example IDs in memory")
|
||||||
|
sources = {s.id for s in load_sources().sources}
|
||||||
|
coverage: dict[str, Counter[str]] = defaultdict(Counter)
|
||||||
|
rows = []
|
||||||
|
excerpt_bytes = 0
|
||||||
|
blobs: dict[str, str] = {}
|
||||||
|
for unit in units:
|
||||||
|
e = index[unit["unit"]] # missing input is fatal, never counted as no-flow
|
||||||
|
if (e.family, e.label, e.group) != (unit["family"], unit["label"], unit["group"]):
|
||||||
|
raise ValueError(f"Unit metadata differs from store: {e.id}")
|
||||||
|
if e.source not in sources:
|
||||||
|
raise ValueError(f"Source absent from sources.toml: {e.source}")
|
||||||
|
if e.excerpt_sha not in blobs:
|
||||||
|
# MemoryStore.get_blob(verify=True) DELETES corrupt blobs: do not use it.
|
||||||
|
data = store.get_blob("excerpts", e.excerpt_sha, verify=False)
|
||||||
|
if not data or hashlib.sha256(data).hexdigest() != e.excerpt_sha:
|
||||||
|
raise ValueError(f"Missing, empty or corrupt excerpt: {e.id}")
|
||||||
|
excerpt_bytes += len(data)
|
||||||
|
blobs[e.excerpt_sha] = data.decode("utf-8")
|
||||||
|
result = function_slice(blobs[e.excerpt_sha], e.language, e.family)
|
||||||
|
status = "not-analysable" if result is None else "flow" if result.steps else "no-flow"
|
||||||
|
coverage[e.family][status] += 1
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
**unit,
|
||||||
|
"source": e.source,
|
||||||
|
"status": status,
|
||||||
|
"has_flow": int(status == "flow"),
|
||||||
|
"slice_length": len(result.steps) if result else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pairs = complete_pairs(rows)
|
||||||
|
asymmetry = Counter(
|
||||||
|
"both" if v["has_flow"] and f["has_flow"] else "vulnerable_only" if v["has_flow"] else "fixed_only" if f["has_flow"] else "neither"
|
||||||
|
for v, f in pairs
|
||||||
|
)
|
||||||
|
scopes = {"all": rows, **{f"source:{s}": [r for r in rows if r["source"] == s] for s in sorted({r["source"] for r in rows})}}
|
||||||
|
return {
|
||||||
|
"cost_usd": 0,
|
||||||
|
"threshold": THRESHOLD,
|
||||||
|
"unit_rows_read": len(units),
|
||||||
|
"example_rows_read": len(examples),
|
||||||
|
"matched_rows": len(rows),
|
||||||
|
"excerpts_read": len(blobs),
|
||||||
|
"excerpt_units": len(rows),
|
||||||
|
"excerpt_bytes": excerpt_bytes,
|
||||||
|
"complete_pairs": len(pairs),
|
||||||
|
"coverage": {f: {s: c[s] for s in ("flow", "no-flow", "not-analysable")} for f, c in sorted(coverage.items())},
|
||||||
|
"pair_asymmetry": {k: asymmetry[k] for k in ("vulnerable_only", "fixed_only", "both", "neither")},
|
||||||
|
"signals": {
|
||||||
|
signal: {scope: metrics(members, signal) for scope, members in scopes.items()} for signal in ("has_flow", "slice_length")
|
||||||
|
},
|
||||||
|
"source_strata": "Stored source IDs from sources.toml; advisory pair catalogs are merged into pairs by the example builder.",
|
||||||
|
"limitations": (
|
||||||
|
"Flat IR: no dominance/guards, textual branch order, potential parameter inputs, conservative call returns; no alias analysis."
|
||||||
|
),
|
||||||
|
"units": [
|
||||||
|
{k: r[k] for k in ("unit", "pair", "family", "label", "fold", "source", "status", "has_flow", "slice_length")} for r in rows
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
load_dotenv(Path(".env"))
|
||||||
|
os.environ.pop("OPENROUTER_API_KEY", None) # dotenv must not restore the removed key
|
||||||
|
raw = UNITS.read_bytes()
|
||||||
|
units = [json.loads(line) for line in raw.decode().splitlines() if line.strip()]
|
||||||
|
report = audit(open_store(), units)
|
||||||
|
report["units_bytes"] = len(raw)
|
||||||
|
report["units_sha256"] = hashlib.sha256(raw).hexdigest()
|
||||||
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||||
|
totals = Counter({s: sum(c[s] for c in report["coverage"].values()) for s in ("flow", "no-flow", "not-analysable")})
|
||||||
|
lines = [
|
||||||
|
f"Slice leak audit: $0; units bytes={len(raw)}",
|
||||||
|
f"Rows read: units={len(units)}, examples={report['example_rows_read']}, matched={report['matched_rows']}",
|
||||||
|
f"Excerpts read: {report['excerpts_read']}; units={report['excerpt_units']}; bytes={report['excerpt_bytes']}",
|
||||||
|
f"Coverage: {dict(totals)}",
|
||||||
|
f"Complete pairs: {report['complete_pairs']}",
|
||||||
|
f"Pair asymmetry: {report['pair_asymmetry']}",
|
||||||
|
f"Has flow: {report['signals']['has_flow']['all']}",
|
||||||
|
f"Slice length: {report['signals']['slice_length']['all']}",
|
||||||
|
f"Source strata: {', '.join(report['signals']['has_flow'])}; threshold={THRESHOLD}",
|
||||||
|
f"Report: {OUTPUT}",
|
||||||
|
]
|
||||||
|
print("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"modules": 151,
|
"modules": 172,
|
||||||
"counts": {
|
"counts": {
|
||||||
"load_bearing": 146,
|
"load_bearing": 166,
|
||||||
"standalone_capability": 5
|
"standalone_capability": 6
|
||||||
},
|
},
|
||||||
"orphaned": [],
|
"orphaned": [],
|
||||||
"standalone": {
|
"standalone": {
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
|
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
|
||||||
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
|
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
|
||||||
"push_scoring": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring",
|
"push_scoring": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring",
|
||||||
|
"search.coordinator": "search-with-proof Req 1/2/4: injected search executor and offline board checkpoint inspector; agent worker pending",
|
||||||
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path"
|
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path"
|
||||||
},
|
},
|
||||||
"rows": [
|
"rows": [
|
||||||
@@ -229,6 +230,8 @@
|
|||||||
"model.report",
|
"model.report",
|
||||||
"pairs",
|
"pairs",
|
||||||
"plane.tasks.alerts",
|
"plane.tasks.alerts",
|
||||||
|
"push.quick",
|
||||||
|
"push.runner",
|
||||||
"rank",
|
"rank",
|
||||||
"regress.candidate",
|
"regress.candidate",
|
||||||
"reports",
|
"reports",
|
||||||
@@ -356,12 +359,14 @@
|
|||||||
"learn.excerpt",
|
"learn.excerpt",
|
||||||
"learn.experiments",
|
"learn.experiments",
|
||||||
"learn.features",
|
"learn.features",
|
||||||
|
"learn.fill",
|
||||||
"learn.folds",
|
"learn.folds",
|
||||||
"learn.labels",
|
"learn.labels",
|
||||||
"learn.leaks",
|
"learn.leaks",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
"learn.retrieve",
|
"learn.retrieve",
|
||||||
"learn.roles",
|
"learn.roles",
|
||||||
|
"learn.slice",
|
||||||
"plane.harvest",
|
"plane.harvest",
|
||||||
"plane.tasks.features",
|
"plane.tasks.features",
|
||||||
"plane.tasks.remember",
|
"plane.tasks.remember",
|
||||||
@@ -410,7 +415,8 @@
|
|||||||
"importers": [
|
"importers": [
|
||||||
"learn.cli",
|
"learn.cli",
|
||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.experiments"
|
"learn.experiments",
|
||||||
|
"learn.fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -422,6 +428,7 @@
|
|||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.evaluate",
|
"learn.evaluate",
|
||||||
"learn.experiments",
|
"learn.experiments",
|
||||||
|
"learn.fill",
|
||||||
"learn.folds",
|
"learn.folds",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
"learn.retrieve"
|
"learn.retrieve"
|
||||||
@@ -435,6 +442,7 @@
|
|||||||
"learn.embeddings",
|
"learn.embeddings",
|
||||||
"learn.examples",
|
"learn.examples",
|
||||||
"learn.labels",
|
"learn.labels",
|
||||||
|
"learn.slice",
|
||||||
"plane.harvest"
|
"plane.harvest"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -443,7 +451,8 @@
|
|||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
"importers": [
|
"importers": [
|
||||||
"learn.cli"
|
"learn.cli",
|
||||||
|
"learn.fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -455,6 +464,14 @@
|
|||||||
"plane.tasks.features"
|
"plane.tasks.features"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "learn.fill",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"learn.cli"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "learn.folds",
|
"module": "learn.folds",
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
@@ -500,7 +517,8 @@
|
|||||||
"learn.cli",
|
"learn.cli",
|
||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.evaluate",
|
"learn.evaluate",
|
||||||
"learn.experiments"
|
"learn.experiments",
|
||||||
|
"learn.fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -539,6 +557,14 @@
|
|||||||
"plane.tasks.roles"
|
"plane.tasks.roles"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "learn.slice",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"learn.program"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "map_gate",
|
"module": "map_gate",
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
@@ -560,6 +586,7 @@
|
|||||||
"pairs",
|
"pairs",
|
||||||
"plane.tasks.alerts",
|
"plane.tasks.alerts",
|
||||||
"preprocess",
|
"preprocess",
|
||||||
|
"push.quick",
|
||||||
"push.runner"
|
"push.runner"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -579,9 +606,12 @@
|
|||||||
"cli",
|
"cli",
|
||||||
"cpg.backend",
|
"cpg.backend",
|
||||||
"cpg.session",
|
"cpg.session",
|
||||||
|
"findings",
|
||||||
"learn.cli",
|
"learn.cli",
|
||||||
"learn.features",
|
"learn.features",
|
||||||
|
"learn.fill",
|
||||||
"learn.labels",
|
"learn.labels",
|
||||||
|
"learn.slice",
|
||||||
"model.candidates",
|
"model.candidates",
|
||||||
"model.config_value",
|
"model.config_value",
|
||||||
"model.dominance",
|
"model.dominance",
|
||||||
@@ -602,11 +632,13 @@
|
|||||||
"push.cache",
|
"push.cache",
|
||||||
"push.contracts",
|
"push.contracts",
|
||||||
"push.policy",
|
"push.policy",
|
||||||
|
"push.quick",
|
||||||
"push.report",
|
"push.report",
|
||||||
"push.reuse",
|
"push.reuse",
|
||||||
"push.runner",
|
"push.runner",
|
||||||
"push.snapshot",
|
"push.snapshot",
|
||||||
"push.vetoes",
|
"push.vetoes",
|
||||||
|
"search.worker",
|
||||||
"tool_hunter"
|
"tool_hunter"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -649,6 +681,7 @@
|
|||||||
"push.cache",
|
"push.cache",
|
||||||
"push.contracts",
|
"push.contracts",
|
||||||
"push.policy",
|
"push.policy",
|
||||||
|
"push.quick",
|
||||||
"push.report",
|
"push.report",
|
||||||
"push.reuse",
|
"push.reuse",
|
||||||
"push.runner",
|
"push.runner",
|
||||||
@@ -672,9 +705,11 @@
|
|||||||
"importers": [
|
"importers": [
|
||||||
"cli",
|
"cli",
|
||||||
"learn.cli",
|
"learn.cli",
|
||||||
|
"learn.fill",
|
||||||
"plane.budget",
|
"plane.budget",
|
||||||
"plane.generate",
|
"plane.generate",
|
||||||
"push.assistance",
|
"push.assistance",
|
||||||
|
"search.worker",
|
||||||
"tool_hunter"
|
"tool_hunter"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -716,7 +751,9 @@
|
|||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
"importers": [
|
"importers": [
|
||||||
"model.scan"
|
"findings",
|
||||||
|
"model.scan",
|
||||||
|
"push.runner"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -775,6 +812,7 @@
|
|||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"reason": "model-grounded-detection Req 7.2-7.4: the security vocabularies the CPG queries are parameterised by",
|
"reason": "model-grounded-detection Req 7.2-7.4: the security vocabularies the CPG queries are parameterised by",
|
||||||
"importers": [
|
"importers": [
|
||||||
|
"learn.slice",
|
||||||
"model.config_value",
|
"model.config_value",
|
||||||
"model.dominance",
|
"model.dominance",
|
||||||
"model.pipeline",
|
"model.pipeline",
|
||||||
@@ -846,7 +884,10 @@
|
|||||||
"learn.embeddings",
|
"learn.embeddings",
|
||||||
"learn.examples",
|
"learn.examples",
|
||||||
"learn.experiments",
|
"learn.experiments",
|
||||||
|
"learn.fill",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
|
"plane.delivery",
|
||||||
|
"plane.doctor",
|
||||||
"plane.egress",
|
"plane.egress",
|
||||||
"plane.engine_alerts",
|
"plane.engine_alerts",
|
||||||
"plane.generate",
|
"plane.generate",
|
||||||
@@ -859,7 +900,10 @@
|
|||||||
"plane.tasks.remember",
|
"plane.tasks.remember",
|
||||||
"plane.tasks.roles",
|
"plane.tasks.roles",
|
||||||
"plane.tasks.verify",
|
"plane.tasks.verify",
|
||||||
"plane.workspaces"
|
"plane.workspaces",
|
||||||
|
"search.board",
|
||||||
|
"search.coordinator",
|
||||||
|
"search.worker"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -871,10 +915,21 @@
|
|||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.embeddings",
|
"learn.embeddings",
|
||||||
"learn.experiments",
|
"learn.experiments",
|
||||||
|
"learn.fill",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
"plane.generate",
|
"plane.generate",
|
||||||
"plane.tasks.roles",
|
"plane.tasks.roles",
|
||||||
"plane.tasks.verify"
|
"plane.tasks.verify",
|
||||||
|
"search.coordinator",
|
||||||
|
"search.worker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "plane.delivery",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"plane.reconciler"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -924,6 +979,7 @@
|
|||||||
"reason": "ai-service-plane Req 1.1-1.4: ax-shaped Task/Workspace/Model and the Run kind, validated before anything runs",
|
"reason": "ai-service-plane Req 1.1-1.4: ax-shaped Task/Workspace/Model and the Run kind, validated before anything runs",
|
||||||
"importers": [
|
"importers": [
|
||||||
"learn.cli",
|
"learn.cli",
|
||||||
|
"plane.delivery",
|
||||||
"plane.egress",
|
"plane.egress",
|
||||||
"plane.generate",
|
"plane.generate",
|
||||||
"plane.harvest",
|
"plane.harvest",
|
||||||
@@ -945,10 +1001,27 @@
|
|||||||
"learn.embeddings",
|
"learn.embeddings",
|
||||||
"learn.examples",
|
"learn.examples",
|
||||||
"learn.experiments",
|
"learn.experiments",
|
||||||
|
"learn.fill",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
|
"plane.delivery",
|
||||||
|
"plane.doctor",
|
||||||
"plane.generate",
|
"plane.generate",
|
||||||
|
"plane.reconciler",
|
||||||
"plane.tasks.loop",
|
"plane.tasks.loop",
|
||||||
"plane.tasks.remember"
|
"plane.tasks.remember",
|
||||||
|
"search.board",
|
||||||
|
"search.coordinator"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "plane.profile",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.plane.profile",
|
||||||
|
"importers": [
|
||||||
|
"cli",
|
||||||
|
"plane.doctor",
|
||||||
|
"plane.generate",
|
||||||
|
"plane.reconciler"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1093,6 +1166,7 @@
|
|||||||
"improve.evolve",
|
"improve.evolve",
|
||||||
"improve.validator",
|
"improve.validator",
|
||||||
"plane.tasks.loop",
|
"plane.tasks.loop",
|
||||||
|
"push.quick",
|
||||||
"scoring.project_score",
|
"scoring.project_score",
|
||||||
"semantic.overlay"
|
"semantic.overlay"
|
||||||
]
|
]
|
||||||
@@ -1132,6 +1206,7 @@
|
|||||||
"plane.tasks.alerts",
|
"plane.tasks.alerts",
|
||||||
"plane.tasks.features",
|
"plane.tasks.features",
|
||||||
"plane.tasks.repo_facts",
|
"plane.tasks.repo_facts",
|
||||||
|
"push.quick",
|
||||||
"push.runner",
|
"push.runner",
|
||||||
"rank",
|
"rank",
|
||||||
"regress.candidate",
|
"regress.candidate",
|
||||||
@@ -1164,6 +1239,7 @@
|
|||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
"model.endpoint",
|
"model.endpoint",
|
||||||
|
"search.worker",
|
||||||
"tool_hunter"
|
"tool_hunter"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1176,6 +1252,7 @@
|
|||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
"model.endpoint",
|
"model.endpoint",
|
||||||
|
"search.worker",
|
||||||
"tool_hunter"
|
"tool_hunter"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1186,6 +1263,7 @@
|
|||||||
"importers": [
|
"importers": [
|
||||||
"cli",
|
"cli",
|
||||||
"push.assistance",
|
"push.assistance",
|
||||||
|
"push.background",
|
||||||
"push.eligibility",
|
"push.eligibility",
|
||||||
"push.policy",
|
"push.policy",
|
||||||
"push.report",
|
"push.report",
|
||||||
@@ -1203,6 +1281,14 @@
|
|||||||
"push.runner"
|
"push.runner"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "push.background",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"push.runner"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "push.cache",
|
"module": "push.cache",
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
@@ -1234,6 +1320,23 @@
|
|||||||
"push.runner"
|
"push.runner"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "push.explain",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"push.report",
|
||||||
|
"push.runner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "push.install",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"cli"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "push.policy",
|
"module": "push.policy",
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
@@ -1245,12 +1348,21 @@
|
|||||||
"push.vetoes"
|
"push.vetoes"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "push.quick",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"push.runner"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "push.report",
|
"module": "push.report",
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"reason": "pre-push-safety-net Req 5.1/5.2/5.5/7.3: compact output and complete artifacts; runner integration pending",
|
"reason": "pre-push-safety-net Req 5.1/5.2/5.5/7.3: compact output and complete artifacts; runner integration pending",
|
||||||
"importers": [
|
"importers": [
|
||||||
"push.assistance",
|
"push.assistance",
|
||||||
|
"push.background",
|
||||||
"push.runner"
|
"push.runner"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1315,7 +1427,8 @@
|
|||||||
"improve.evolve",
|
"improve.evolve",
|
||||||
"map_gate",
|
"map_gate",
|
||||||
"pairs",
|
"pairs",
|
||||||
"plane.tasks.alerts"
|
"plane.tasks.alerts",
|
||||||
|
"push.quick"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1452,7 +1565,10 @@
|
|||||||
"regress.candidate",
|
"regress.candidate",
|
||||||
"regress.recipes",
|
"regress.recipes",
|
||||||
"regress.safety",
|
"regress.safety",
|
||||||
"regress.verdict"
|
"regress.verdict",
|
||||||
|
"search._sandbox",
|
||||||
|
"search.executor",
|
||||||
|
"search.verify"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1491,6 +1607,125 @@
|
|||||||
"scoring"
|
"scoring"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "search",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"search.coordinator",
|
||||||
|
"search.executor",
|
||||||
|
"search.oracles",
|
||||||
|
"search.probe",
|
||||||
|
"search.verify",
|
||||||
|
"search.verify_task",
|
||||||
|
"search.worker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search._sandbox",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"search.executor",
|
||||||
|
"search.oracles",
|
||||||
|
"search.probe",
|
||||||
|
"search.verify",
|
||||||
|
"search.worker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.board",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"search.coordinator"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.budget",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"search.coordinator"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.coordinator",
|
||||||
|
"classification": "standalone_capability",
|
||||||
|
"reason": "search-with-proof Req 1/2/4: injected search executor and offline board checkpoint inspector; agent worker pending",
|
||||||
|
"importers": [
|
||||||
|
"search.worker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.demo",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"search.executor",
|
||||||
|
"search.verify",
|
||||||
|
"search.verify_task",
|
||||||
|
"search.worker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.executor",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.search.executor",
|
||||||
|
"importers": [
|
||||||
|
"search.verify_task",
|
||||||
|
"search.worker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.oracles",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.search.oracles",
|
||||||
|
"importers": [
|
||||||
|
"search.probe",
|
||||||
|
"search.verify"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.probe",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.search.probe",
|
||||||
|
"importers": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.task_storage",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
|
"importers": [
|
||||||
|
"search.executor",
|
||||||
|
"search.verify",
|
||||||
|
"search.verify_task"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.verify",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.search.verify",
|
||||||
|
"importers": [
|
||||||
|
"search.executor",
|
||||||
|
"search.probe",
|
||||||
|
"search.verify_task"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.verify_task",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.search.verify_task",
|
||||||
|
"importers": [
|
||||||
|
"search.executor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "search.worker",
|
||||||
|
"classification": "load_bearing",
|
||||||
|
"reason": "python -m openultrasast.search.worker",
|
||||||
|
"importers": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "semantic",
|
"module": "semantic",
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
@@ -1501,6 +1736,7 @@
|
|||||||
"hunter_tools",
|
"hunter_tools",
|
||||||
"learn.features",
|
"learn.features",
|
||||||
"learn.roles",
|
"learn.roles",
|
||||||
|
"learn.slice",
|
||||||
"mapping",
|
"mapping",
|
||||||
"model.candidates",
|
"model.candidates",
|
||||||
"model.layout",
|
"model.layout",
|
||||||
@@ -1557,6 +1793,7 @@
|
|||||||
"hunter_tools",
|
"hunter_tools",
|
||||||
"learn.features",
|
"learn.features",
|
||||||
"learn.roles",
|
"learn.roles",
|
||||||
|
"learn.slice",
|
||||||
"mapping",
|
"mapping",
|
||||||
"model.candidates",
|
"model.candidates",
|
||||||
"model.layout",
|
"model.layout",
|
||||||
@@ -1590,6 +1827,7 @@
|
|||||||
"importers": [
|
"importers": [
|
||||||
"cli",
|
"cli",
|
||||||
"hunter_tools",
|
"hunter_tools",
|
||||||
|
"learn.slice",
|
||||||
"model.candidates",
|
"model.candidates",
|
||||||
"pairs",
|
"pairs",
|
||||||
"semantic.cst",
|
"semantic.cst",
|
||||||
@@ -1723,6 +1961,7 @@
|
|||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
"importers": [
|
"importers": [
|
||||||
|
"learn.slice",
|
||||||
"semantic.overlay"
|
"semantic.overlay"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1748,6 +1987,8 @@
|
|||||||
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
"importers": [
|
"importers": [
|
||||||
"cli",
|
"cli",
|
||||||
|
"learn.cli",
|
||||||
|
"learn.fill",
|
||||||
"model.endpoint",
|
"model.endpoint",
|
||||||
"plane.budget",
|
"plane.budget",
|
||||||
"plane.tasks.roles",
|
"plane.tasks.roles",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[ok] profile k3s: exec=remote kube_context=ousast-k3s registry=ghcr.io/norandom images=/home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/images.json router_url=(tunnel through the kube context) ax_server=(tunnel through the kube context) memory=s3://sast-memory atespace=default snapshots_bucket=s3://ax-snapshots image_pull_secret=ghcr-pull
|
||||||
|
[FAIL] cluster ousast-k3s: context ousast-k3s unreachable (error: context "ousast-k3s" does not exist); docs/deployment.md lists the prerequisites
|
||||||
|
[FAIL] runner image in ghcr.io/norandom: images file /home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/images.json: No such file or directory; docs/deployment.md lists the prerequisites
|
||||||
|
[ok] memory store s3://sast-memory: s3://sast-memory opens (an S3 bucket is verified)
|
||||||
|
exit=1 seconds=1.1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[ok] profile kind: exec=local kube_context=kind-ousast registry=localhost:5001 images=/home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/kind-images.json router_url=(tunnel through the kube context) ax_server=(tunnel through the kube context) memory=s3://sast-memory atespace=default snapshots_bucket=s3://ax-snapshots image_pull_secret=(none)
|
||||||
|
[ok] cluster kind-ousast: reachable
|
||||||
|
[ok] agent substrate (ate-system): 9 pods ready
|
||||||
|
[ok] ax controller (ax-system): 4 pods ready
|
||||||
|
[ok] runner image in localhost:5001: ousast-runner@sha256:95edc7ed5621... present in localhost:5001
|
||||||
|
[ok] memory store s3://sast-memory: s3://sast-memory opens (an S3 bucket is verified)
|
||||||
|
exit=0 seconds=1.4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
F.. [100%]
|
||||||
|
=================================== FAILURES ===================================
|
||||||
|
________ test_no_host_literal_outside_the_profile_and_the_kind_bring_up ________
|
||||||
|
|
||||||
|
def test_no_host_literal_outside_the_profile_and_the_kind_bring_up() -> None:
|
||||||
|
kind = load_profile("kind", environ={})
|
||||||
|
found = offenders(_files(), kind.registry)
|
||||||
|
> assert found == [], "host-only literals (Req 1.3):\n" + "\n".join(found)
|
||||||
|
E AssertionError: host-only literals (Req 1.3):
|
||||||
|
E src/openultrasast/plane/workspaces.py:37: [kind context] CONTEXT = "kind-ousast" # reinserted for the record 01 demonstration
|
||||||
|
E assert ['src/openult...emonstration'] == []
|
||||||
|
E
|
||||||
|
E Left contains one more item: 'src/openultrasast/plane/workspaces.py:37: [kind context] CONTEXT = "kind-ousast" # reinserted for the record 01 demonstration'
|
||||||
|
E Use -v to get more diff
|
||||||
|
|
||||||
|
tests/test_plane_literals.py:81: AssertionError
|
||||||
|
=========================== short test summary info ============================
|
||||||
|
FAILED tests/test_plane_literals.py::test_no_host_literal_outside_the_profile_and_the_kind_bring_up
|
||||||
|
1 failed, 2 passed in 0.26s
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
... [100%]
|
||||||
|
3 passed in 0.23s
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: ax.io/v1alpha1
|
||||||
|
kind: Workspace
|
||||||
|
metadata: {name: ousast-probe-big-ws, atespace: default}
|
||||||
|
spec:
|
||||||
|
files:
|
||||||
|
- {path: "hello.py", content: "def hello():\n return greet()\n"}
|
||||||
|
---
|
||||||
|
apiVersion: ax.io/v1alpha1
|
||||||
|
kind: Task
|
||||||
|
metadata: {name: ousast-probe-big, atespace: default}
|
||||||
|
spec:
|
||||||
|
image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884"
|
||||||
|
command: ["repo-facts"]
|
||||||
|
env: [{name: OUSAST_OUTPUT_DIR, value: "/workspace/.ousast-out/probe"}]
|
||||||
|
resources: {requests: {cpu: "250m", memory: "2560Mi"}, limits: {cpu: "1", memory: "3Gi"}}
|
||||||
|
workspaces: [{name: ousast-probe-big-ws, path: "/workspace"}]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: ax.io/v1alpha1
|
||||||
|
kind: Workspace
|
||||||
|
metadata: {name: ousast-probe-small-ws, atespace: default}
|
||||||
|
spec:
|
||||||
|
files:
|
||||||
|
- {path: "hello.py", content: "def hello():\n return greet()\n"}
|
||||||
|
---
|
||||||
|
apiVersion: ax.io/v1alpha1
|
||||||
|
kind: Task
|
||||||
|
metadata: {name: ousast-probe-small, atespace: default}
|
||||||
|
spec:
|
||||||
|
image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884"
|
||||||
|
command: ["repo-facts"]
|
||||||
|
env: [{name: OUSAST_OUTPUT_DIR, value: "/workspace/.ousast-out/probe"}]
|
||||||
|
resources: {requests: {cpu: "250m", memory: "256Mi"}, limits: {cpu: "1", memory: "512Mi"}}
|
||||||
|
workspaces: [{name: ousast-probe-small-ws, path: "/workspace"}]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Warm gVisor workers for ax tasks (Agent Substrate WorkerPool), rendered by ops/ax/up.sh (and by
|
||||||
|
# ops/ax/probe-placement.sh for the placement probe) from the profile's pool: POOL_NAME, POOL_REPLICAS, POOL_CPU,
|
||||||
|
# POOL_MEMORY (ops/k8s/profiles/<profile>.toml, [pools.default]) and WORKER_IMAGE (ko:// for a build, or a
|
||||||
|
# digest-pinned reference of a built worker image). ax's per-task ActorTemplates set no workerSelector, so every
|
||||||
|
# pool of the matching sandbox class is eligible. An actor occupies a whole worker. Replaced by the renderer of
|
||||||
|
# plane-on-kubernetes task 6.1 (ops/k8s/base/workerpool-*.yaml.tmpl).
|
||||||
|
apiVersion: ate.dev/v1alpha1
|
||||||
|
kind: WorkerPool
|
||||||
|
metadata:
|
||||||
|
name: probe-pool
|
||||||
|
namespace: ax-system
|
||||||
|
labels:
|
||||||
|
workload: ousast
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
workerImage: localhost:5001/ateom-gvisor-715889664656de67e44382a8d6ab981d@sha256:220f1cf13ea485b73a2e4270fa544901a18ae826d8f22bd54d42761944a41c5c
|
||||||
|
template:
|
||||||
|
nodeSelector:
|
||||||
|
ate.dev/substrate-version: "4f9c293"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: 3Gi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 3Gi
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"workers": [
|
||||||
|
{
|
||||||
|
"ip": "10.244.0.22",
|
||||||
|
"labels": {
|
||||||
|
"workload": "ousast"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"createTime": "2026-09-29T16:32:08.594663703Z",
|
||||||
|
"name": "769f7d8b-142a-4ed5-b896-08d6a04bedb6",
|
||||||
|
"uid": "152cf7c0-3f4d-4634-88c5-9fe501bb403e",
|
||||||
|
"updateTime": "2026-10-02T05:24:59.069304031Z",
|
||||||
|
"version": "1758"
|
||||||
|
},
|
||||||
|
"nodeName": "ousast-control-plane",
|
||||||
|
"sandboxClass": "gvisor",
|
||||||
|
"status": {
|
||||||
|
"capacity": {
|
||||||
|
"actors": 1000,
|
||||||
|
"resources": {
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"quantity": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory",
|
||||||
|
"quantity": "1536Mi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": "WORKER_STATE_ACTIVE"
|
||||||
|
},
|
||||||
|
"workerNamespace": "ax-system",
|
||||||
|
"workerPod": "ousast-pool-6656b55f5b-9wbrw",
|
||||||
|
"workerPodUid": "769f7d8b-142a-4ed5-b896-08d6a04bedb6",
|
||||||
|
"workerPool": "ousast-pool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "10.244.0.28",
|
||||||
|
"labels": {
|
||||||
|
"workload": "ousast"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"createTime": "2026-10-02T18:29:30.486026311Z",
|
||||||
|
"name": "1407a421-7dcf-4ad7-b999-70a4624bf890",
|
||||||
|
"uid": "2d496d7a-4f32-459d-9b92-256a2c5b4800",
|
||||||
|
"updateTime": "2026-10-02T18:29:30.551668391Z",
|
||||||
|
"version": "2"
|
||||||
|
},
|
||||||
|
"nodeName": "ousast-control-plane",
|
||||||
|
"sandboxClass": "gvisor",
|
||||||
|
"status": {
|
||||||
|
"capacity": {
|
||||||
|
"actors": 1000,
|
||||||
|
"resources": {
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"quantity": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory",
|
||||||
|
"quantity": "3Gi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": "WORKER_STATE_ACTIVE"
|
||||||
|
},
|
||||||
|
"workerNamespace": "ax-system",
|
||||||
|
"workerPod": "probe-pool-5dffd74bdc-dqlww",
|
||||||
|
"workerPodUid": "1407a421-7dcf-4ad7-b999-70a4624bf890",
|
||||||
|
"workerPool": "probe-pool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"workers": [
|
||||||
|
{
|
||||||
|
"ip": "10.244.0.22",
|
||||||
|
"labels": {
|
||||||
|
"workload": "ousast"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"createTime": "2026-09-29T16:32:08.594663703Z",
|
||||||
|
"name": "769f7d8b-142a-4ed5-b896-08d6a04bedb6",
|
||||||
|
"uid": "152cf7c0-3f4d-4634-88c5-9fe501bb403e",
|
||||||
|
"updateTime": "2026-10-02T18:29:45.281958347Z",
|
||||||
|
"version": "1761"
|
||||||
|
},
|
||||||
|
"nodeName": "ousast-control-plane",
|
||||||
|
"sandboxClass": "gvisor",
|
||||||
|
"status": {
|
||||||
|
"allocated": {
|
||||||
|
"actors": 1
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"actors": 1000,
|
||||||
|
"resources": {
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"quantity": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory",
|
||||||
|
"quantity": "1536Mi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": "WORKER_STATE_ACTIVE"
|
||||||
|
},
|
||||||
|
"workerNamespace": "ax-system",
|
||||||
|
"workerPod": "ousast-pool-6656b55f5b-9wbrw",
|
||||||
|
"workerPodUid": "769f7d8b-142a-4ed5-b896-08d6a04bedb6",
|
||||||
|
"workerPool": "ousast-pool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "10.244.0.28",
|
||||||
|
"labels": {
|
||||||
|
"workload": "ousast"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"createTime": "2026-10-02T18:29:30.486026311Z",
|
||||||
|
"name": "1407a421-7dcf-4ad7-b999-70a4624bf890",
|
||||||
|
"uid": "2d496d7a-4f32-459d-9b92-256a2c5b4800",
|
||||||
|
"updateTime": "2026-10-02T18:29:30.551668391Z",
|
||||||
|
"version": "2"
|
||||||
|
},
|
||||||
|
"nodeName": "ousast-control-plane",
|
||||||
|
"sandboxClass": "gvisor",
|
||||||
|
"status": {
|
||||||
|
"capacity": {
|
||||||
|
"actors": 1000,
|
||||||
|
"resources": {
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"quantity": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory",
|
||||||
|
"quantity": "3Gi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": "WORKER_STATE_ACTIVE"
|
||||||
|
},
|
||||||
|
"workerNamespace": "ax-system",
|
||||||
|
"workerPod": "probe-pool-5dffd74bdc-dqlww",
|
||||||
|
"workerPodUid": "1407a421-7dcf-4ad7-b999-70a4624bf890",
|
||||||
|
"workerPool": "probe-pool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"workers": [
|
||||||
|
{
|
||||||
|
"ip": "10.244.0.22",
|
||||||
|
"labels": {
|
||||||
|
"workload": "ousast"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"createTime": "2026-09-29T16:32:08.594663703Z",
|
||||||
|
"name": "769f7d8b-142a-4ed5-b896-08d6a04bedb6",
|
||||||
|
"uid": "152cf7c0-3f4d-4634-88c5-9fe501bb403e",
|
||||||
|
"updateTime": "2026-10-02T18:29:53.001457323Z",
|
||||||
|
"version": "1763"
|
||||||
|
},
|
||||||
|
"nodeName": "ousast-control-plane",
|
||||||
|
"sandboxClass": "gvisor",
|
||||||
|
"status": {
|
||||||
|
"allocated": {
|
||||||
|
"actors": 1
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"actors": 1000,
|
||||||
|
"resources": {
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"quantity": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory",
|
||||||
|
"quantity": "1536Mi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": "WORKER_STATE_ACTIVE"
|
||||||
|
},
|
||||||
|
"workerNamespace": "ax-system",
|
||||||
|
"workerPod": "ousast-pool-6656b55f5b-9wbrw",
|
||||||
|
"workerPodUid": "769f7d8b-142a-4ed5-b896-08d6a04bedb6",
|
||||||
|
"workerPool": "ousast-pool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "10.244.0.28",
|
||||||
|
"labels": {
|
||||||
|
"workload": "ousast"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"createTime": "2026-10-02T18:29:30.486026311Z",
|
||||||
|
"name": "1407a421-7dcf-4ad7-b999-70a4624bf890",
|
||||||
|
"uid": "2d496d7a-4f32-459d-9b92-256a2c5b4800",
|
||||||
|
"updateTime": "2026-10-02T18:30:04.825058843Z",
|
||||||
|
"version": "4"
|
||||||
|
},
|
||||||
|
"nodeName": "ousast-control-plane",
|
||||||
|
"sandboxClass": "gvisor",
|
||||||
|
"status": {
|
||||||
|
"capacity": {
|
||||||
|
"actors": 1000,
|
||||||
|
"resources": {
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"quantity": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory",
|
||||||
|
"quantity": "3Gi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": "WORKER_STATE_ACTIVE"
|
||||||
|
},
|
||||||
|
"workerNamespace": "ax-system",
|
||||||
|
"workerPod": "probe-pool-5dffd74bdc-dqlww",
|
||||||
|
"workerPodUid": "1407a421-7dcf-4ad7-b999-70a4624bf890",
|
||||||
|
"workerPool": "probe-pool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"record": "k8s-01-profile",
|
||||||
|
"spec": "plane-on-kubernetes",
|
||||||
|
"tasks": ["1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8"],
|
||||||
|
"date": "2026-10-02",
|
||||||
|
"host": "the maintainer's laptop: 7.9 GiB, single-node kind cluster, RustFS store reachable over https",
|
||||||
|
"commits": {
|
||||||
|
"1.1": "8eac1b2",
|
||||||
|
"1.2": "77a03ba",
|
||||||
|
"1.3": "36f5927",
|
||||||
|
"1.4": "8099b29",
|
||||||
|
"1.5": "86ec237",
|
||||||
|
"1.6": "2afa20e",
|
||||||
|
"1.7": "2cca03c",
|
||||||
|
"1.8": "the commit that adds this line (git log: plane(1.8))"
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"runner_before": "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884",
|
||||||
|
"runner_after_up_sh": "localhost:5001/ousast-runner@sha256:95edc7ed562189180a472df8115e6a90c8b4fcea47c06593395634bd56041bcb",
|
||||||
|
"pin_file": "ops/k8s/profiles/kind-images.json (written by ops/ax/up.sh; the templates re-pinned to it, 128 image lines)"
|
||||||
|
},
|
||||||
|
"instrument_checks": {
|
||||||
|
"code_under_test": "PYTHONPATH=<worktree>/src first; `python -c 'import openultrasast; print(openultrasast.__file__)'` printed the worktree path before every gate",
|
||||||
|
"store_reached": {
|
||||||
|
"how": "scratch store_probe.py: open_store('s3://sast-memory') through the worktree code (verify_bucket runs), then read the probe object",
|
||||||
|
"open_and_verify_seconds": 0.62,
|
||||||
|
"probe_key": "_probe/select.jsonl",
|
||||||
|
"probe_bytes": 24,
|
||||||
|
"probe_version_id_present": true
|
||||||
|
},
|
||||||
|
"smoke_delivery_sizes_bytes": {"facts.json": 451, "summary.json": 238, "units.jsonl": 291, "task.yaml": 1148},
|
||||||
|
"registry_resolution": "doctor HEADs /v2/ousast-runner/manifests/<digest> on the kind registry: HTTP 2xx for the new digest"
|
||||||
|
},
|
||||||
|
"doctor": {
|
||||||
|
"kind": {
|
||||||
|
"command": "ousast plane doctor --profile kind",
|
||||||
|
"exit": 0,
|
||||||
|
"seconds": 1.4,
|
||||||
|
"lines": [
|
||||||
|
"[ok] profile kind: exec=local kube_context=... registry=... images=... router_url=(tunnel) ax_server=(tunnel) memory=s3://sast-memory atespace=default snapshots_bucket=s3://ax-snapshots image_pull_secret=(none)",
|
||||||
|
"[ok] cluster <kind context>: reachable",
|
||||||
|
"[ok] agent substrate (ate-system): 9 pods ready",
|
||||||
|
"[ok] ax controller (ax-system): 4 pods ready",
|
||||||
|
"[ok] runner image in <kind registry>: ousast-runner@sha256:95edc7ed5621... present",
|
||||||
|
"[ok] memory store s3://sast-memory: opens (an S3 bucket is verified)"
|
||||||
|
],
|
||||||
|
"file": "doctor-kind.txt"
|
||||||
|
},
|
||||||
|
"k3s": {
|
||||||
|
"command": "ousast plane doctor --profile k3s",
|
||||||
|
"exit": 1,
|
||||||
|
"seconds": 1.1,
|
||||||
|
"failing_lines": [
|
||||||
|
"cluster ousast-k3s: context does not exist (expected: the cluster does not exist yet)",
|
||||||
|
"runner image in ghcr.io/norandom: images file ops/k8s/profiles/images.json missing (expected: the release asset is published by task 4.1)"
|
||||||
|
],
|
||||||
|
"passing_lines": ["profile k3s: every address reported", "memory store s3://sast-memory: opens"],
|
||||||
|
"note": "two expected failures, not one: the images file is the release asset and does not exist before group 4; the namespaces are not checked behind an unreachable context, so no third line fails for the same cause",
|
||||||
|
"file": "doctor-k3s.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"literal_scan": {
|
||||||
|
"test": "tests/test_plane_literals.py",
|
||||||
|
"scope": "src/, plane/, ops/ (minus ops/ax/up.sh, ops/k8s/profiles/), docs/",
|
||||||
|
"patterns": ["kind context", "kind registry", "kind gateway 172.19.", "loopback with port", "port-forward"],
|
||||||
|
"exemptions": [
|
||||||
|
"router.py for loopback-with-port and port-forward",
|
||||||
|
"lines tagged ax-tunnel or loopback (two: the runner's own health URL, the Joern session's query URL)",
|
||||||
|
"digest-pinned image: lines under plane/tasks/ whose registry equals the kind profile's (a third test asserts every one does)"
|
||||||
|
],
|
||||||
|
"offenders_at_1_3": 0,
|
||||||
|
"offenders_before_1_3": 23,
|
||||||
|
"reinsertion_demo": {
|
||||||
|
"inserted": "a kind-context literal appended to src/openultrasast/plane/workspaces.py",
|
||||||
|
"pytest_exit_with_literal": 1,
|
||||||
|
"named": "src/openultrasast/plane/workspaces.py:37: [kind context]",
|
||||||
|
"restored": "byte for byte (cmp)",
|
||||||
|
"pytest_exit_restored": 0,
|
||||||
|
"files": ["literal-reinserted-fail.txt", "literal-restored-pass.txt"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"up_sh_rerun": {
|
||||||
|
"command": "OUSAST_PYTHON=<main venv python> ops/ax/up.sh",
|
||||||
|
"exit": 0,
|
||||||
|
"seconds": 38,
|
||||||
|
"kept": ["cluster", "substrate", "egress gateway", "ax control plane", "worker pool ousast-pool (2/2 ready)", "ax CLI (newer than the checkout)"],
|
||||||
|
"done": ["receiver Service re-applied", "runner image rebuilt and pushed (new digest above)", "kind-images.json written", "smoke Task applied and resumed"],
|
||||||
|
"first_attempt_failure": "`profile --print images` answered `no image named ''`; fixed in profile._lookup (an images path with no sub-key is the file path) and covered by test_plane_profile",
|
||||||
|
"disk_gb_free_before_after": [1.7, 1.5],
|
||||||
|
"file": "up-sh-rerun.txt"
|
||||||
|
},
|
||||||
|
"smoke_run_1_4": {
|
||||||
|
"command": "ops/ax/smoke-run.sh (receiver delivery: the path group 2 replaces)",
|
||||||
|
"exit": 0,
|
||||||
|
"seconds": 24,
|
||||||
|
"status": "done",
|
||||||
|
"delivered": "facts.json 451 B, summary.json 238 B, units.jsonl 291 B; summary.image = the new digest, units 2/2",
|
||||||
|
"file": "smoke-run-1.4.txt"
|
||||||
|
},
|
||||||
|
"placement_probe_1_5": {
|
||||||
|
"script": "ops/ax/probe-placement.sh",
|
||||||
|
"pools": {"ousast-pool": "scaled to 1 worker, 1 CPU / 1536Mi", "probe-pool": "1 worker, 2 CPU / 3Gi, the already-built ateom-gvisor image (no ko build)"},
|
||||||
|
"big_actor": {"requests_memory": "2560Mi", "limits_memory": "3Gi", "landed_on": "ousast-pool (1536Mi worker): status.allocated.actors=1; probe-pool 0"},
|
||||||
|
"small_actor": {"requests_memory": "256Mi", "limits_memory": "512Mi", "landed_on": "ousast-pool: allocated.actors=1; probe-pool 0"},
|
||||||
|
"placement": "any",
|
||||||
|
"consequence": "design.md section 4: group 5 takes the fallback (an engine atespace with engine-pool as its only pool if Substrate scopes pools by atespace; else a plain Kubernetes Job on k3s and alerts-engine on kind)",
|
||||||
|
"resume_latency_seconds": "first resume of each new template succeeded at once (golden snapshot built within the resume deadline); whole probe 46 s",
|
||||||
|
"cleanup": "probe tasks deleted, probe-pool deleted, ousast-pool back to 2/2 (verified by kubectl get workerpool)",
|
||||||
|
"files": ["probe/probe.log", "probe/workers-before.json", "probe/workers-ousast-probe-big.json", "probe/workers-ousast-probe-small.json", "probe/ousast-probe-big.yaml", "probe/ousast-probe-small.yaml", "probe/probe-pool.yaml"]
|
||||||
|
},
|
||||||
|
"reconciler_bound_1_6": {
|
||||||
|
"lines_at_spec_commit_6bd44ca": 477,
|
||||||
|
"lines_after_1_2": 489,
|
||||||
|
"added_by_1_2": "the profile parameter of run() and its docstring (+12)",
|
||||||
|
"estimate_2_4": "Delivery wiring: open the store, build Delivery, pass the delivery body to the starter, delivered() and collect() in the await/execute path, state mirroring, the OUSAST_DELIVERY=receiver switch: about +25 while the receiver is kept (group 2 keeps both paths), about -20 when 2.6 removes the receiver wiring",
|
||||||
|
"projected_during_group_2": 514,
|
||||||
|
"projected_after_2_6": 494,
|
||||||
|
"decision": "the estimate crosses 500 while both paths coexist: task 2.4 moves the bound in tests/test_plane_reconciler.py to 550 with the reason in the assertion message, and 2.6 records the final count"
|
||||||
|
},
|
||||||
|
"gvisor_tarball_1_7": {
|
||||||
|
"status": "planned, manual",
|
||||||
|
"where": "docs/deployment.md B.1",
|
||||||
|
"url": "gs://gvisor/releases/nightly/2026-09-02/<arch>/gvisor.tar.zstd (sha256-pinned in substrate/manifests/ate-install/sandboxconfig-gvisor.yaml)",
|
||||||
|
"check": "the atelet's fetch line in `kubectl -n ate-system logs` of a worker pod on the k3s node; labelled planned until read there"
|
||||||
|
},
|
||||||
|
"gates": {
|
||||||
|
"1.1": "pytest 2002 passed + module-audit manifest regenerated (test_module_audit 6 passed), ruff 0, format 0, mypy 0",
|
||||||
|
"1.2": "pytest 2006 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0",
|
||||||
|
"1.3": "pytest 2009 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0; mkdocs --strict 0",
|
||||||
|
"1.4": "see the commit message"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
run finished: done
|
||||||
|
memory s3://sast-memory: ax-e2e-smoke: 0 tasks ingested, 0 already in the index; rows none
|
||||||
|
run ax-e2e-smoke: done (/tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/k8s-impl/results/plane/ax-e2e-smoke)
|
||||||
|
|
||||||
|
TASK MODEL CALLS PROMPT CACHE-HIT OUTPUT USD
|
||||||
|
facts - 0 n/a n/a n/a n/a [done]
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
total - n/a n/a n/a n/a n/a
|
||||||
|
smoke exit=0 seconds=24
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
== kind cluster 'ousast' (context kind-ousast) + registry localhost:5001 (20:35:35)
|
||||||
|
cluster exists; kept
|
||||||
|
|
||||||
|
== agent substrate (ate-system) (20:35:35)
|
||||||
|
substrate ready; kept
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
ate-api-server-647bb6f8b6-c9k5k 1/1 Running 2 (12h ago) 3d
|
||||||
|
ate-api-server-647bb6f8b6-dznjn 1/1 Running 1 (12h ago) 3d
|
||||||
|
ate-controller-c95586684-g7nvn 1/1 Running 0 3d2h
|
||||||
|
atelet-4f9c293-9zpfg 1/1 Running 0 3d2h
|
||||||
|
atenet-egress-d74b8f6bf-8zmxj 1/1 Running 0 3d
|
||||||
|
atenet-router-54887bc8f9-rqs6g 2/2 Running 2 (12h ago) 3d2h
|
||||||
|
postgres-0 2/2 Running 3 (3d2h ago) 3d2h
|
||||||
|
rustfs-6c4c677cb6-cqvhc 1/1 Running 3 (3d2h ago) 3d2h
|
||||||
|
rustfs-bucket-init-nvfr6 0/1 Completed 0 3d2h
|
||||||
|
|
||||||
|
== egress gateway (atenet-egress, agentgateway variant: one prebuilt image, nothing built) (20:35:35)
|
||||||
|
gateway present; kept
|
||||||
|
|
||||||
|
== ax control plane (ax-system) (20:35:35)
|
||||||
|
ax ready; kept
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
ax-redis-86df786c87-pz5tp 1/1 Running 0 3d2h
|
||||||
|
ax-server-b479bdc76-hwkqb 1/1 Running 0 3d2h
|
||||||
|
ousast-pool-6656b55f5b-5brkn 1/1 Running 0 5m23s
|
||||||
|
ousast-pool-6656b55f5b-9wbrw 1/1 Running 0 3d2h
|
||||||
|
|
||||||
|
== artifact receiver Service (ousast-receiver.ax-system -> this host) (20:35:35)
|
||||||
|
service/ousast-receiver unchanged
|
||||||
|
endpointslice.discovery.k8s.io/ousast-receiver-host configured
|
||||||
|
|
||||||
|
== worker pool (gVisor): pools.default of the profile (20:35:36)
|
||||||
|
pool ousast-pool exists (2/2 ready); kept
|
||||||
|
pod/ousast-pool-6656b55f5b-5brkn condition met
|
||||||
|
pod/ousast-pool-6656b55f5b-9wbrw condition met
|
||||||
|
|
||||||
|
== runner image localhost:5001/ousast-runner (20:35:37)
|
||||||
|
sha256:f004a5ef8f71b1cb8a5880c509389c0569181af7b19170b574958cc3ef75cb1c
|
||||||
|
localhost:5001/ousast-runner:dev
|
||||||
|
runner image localhost:5001/ousast-runner@sha256:95edc7ed562189180a472df8115e6a90c8b4fcea47c06593395634bd56041bcb -> /home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/kind-images.json
|
||||||
|
re-pin the templates with: ousast plane workspaces ... --runner-image /home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/kind-images.json (or ousast plane repin, task 4.3)
|
||||||
|
|
||||||
|
== ax CLI from the deployed checkout (a release CLI skews from the server) (20:36:05)
|
||||||
|
ax CLI newer than the checkout; kept
|
||||||
|
|
||||||
|
== smoke task (20:36:05)
|
||||||
|
task.ax.io/ousast-smoke created
|
||||||
|
workspace.ax.io/ousast-smoke-ws unchanged
|
||||||
|
task.ax.io/ousast-smoke resumed
|
||||||
|
NAME ATESPACE PHASE ACTOR WORKER-IP AGE
|
||||||
|
ousast-smoke default Running ousast-smoke 10.244.0.29 5s
|
||||||
|
|
||||||
|
== footprint (20:36:11)
|
||||||
|
total used free shared buff/cache available
|
||||||
|
Mem: 7877 4797 225 63 3464 3079
|
||||||
|
kind-registry 53.77MiB / 7.693GiB 0.00%
|
||||||
|
up.sh exit 0
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"atespace": "default",
|
||||||
|
"createTime": "2026-10-02T19:39:19.428045847Z",
|
||||||
|
"name": "default",
|
||||||
|
"uid": "741e52b7-aaf1-4a91-b4bf-fc3b20ba4a36",
|
||||||
|
"updateTime": "2026-10-02T19:39:19.428045847Z",
|
||||||
|
"version": "1"
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"tlsPassthrough": {
|
||||||
|
"hostnames": [
|
||||||
|
"files.because-security.com"
|
||||||
|
],
|
||||||
|
"ports": {
|
||||||
|
"numbers": [
|
||||||
|
443
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"record": "k8s-02-store-delivery",
|
||||||
|
"spec": "plane-on-kubernetes",
|
||||||
|
"tasks": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"],
|
||||||
|
"date": "2026-10-02",
|
||||||
|
"commits": {
|
||||||
|
"2.1": "35d8032",
|
||||||
|
"2.2": "9506a18",
|
||||||
|
"2.3": "c49eae4",
|
||||||
|
"2.4": "605b7af",
|
||||||
|
"2.5": "the commit that adds this file (git log: plane(2.5))",
|
||||||
|
"receiver_removed": "pending: task 2.6 (paused 2026-10-03 by the maintainer's reprioritisation; the receiver path still exists behind OUSAST_DELIVERY=receiver)"
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"runner": "localhost:5001/ousast-runner@sha256:d2a55293999cc34581ef1f02a1ffb0c2dd8fc0cbe001ac952df81600e868f019",
|
||||||
|
"built_by": "ops/ax/up.sh rerun (file up-sh-rebuild.txt): every step kept but the runner build; the image carries the 2.2 runner",
|
||||||
|
"disk_gb_free_after_build": 1.3
|
||||||
|
},
|
||||||
|
"instrument_checks": {
|
||||||
|
"code_under_test": "the worktree's src first on PYTHONPATH; `ousast` run as `python -m openultrasast.cli` from the worktree (ops/ax/smoke-run.sh with OUSAST_PYTHON)",
|
||||||
|
"store_read_back": {
|
||||||
|
"how": "scratch store_tar_check.py opens the store through the worktree code (verify_bucket runs), lists runs/ax-e2e-smoke/, downloads output.tar, compares its members with the files the reconciler extracted (file store-check.json)",
|
||||||
|
"tar_bytes": 10240,
|
||||||
|
"tar_members_bytes": {"facts.json": 451, "summary.json": 238, "units.jsonl": 291},
|
||||||
|
"extracted_files_bytes": {"facts.json": 451, "summary.json": 238, "units.jsonl": 291},
|
||||||
|
"members_equal_extracted": true,
|
||||||
|
"member_bytes_total": 980,
|
||||||
|
"note": "a tar is 512-byte blocks: 10240 B of tar carry 980 B of files; done.json records the tar's byte size"
|
||||||
|
},
|
||||||
|
"no_receiver_in_the_task": "the rendered task.yaml is 996 B against 1148 B on the receiver path: no OUSAST_ARTIFACT_URL/DIAL in the Task's env; the start body carries the URLs"
|
||||||
|
},
|
||||||
|
"smoke_runs": {
|
||||||
|
"store_1": {
|
||||||
|
"command": "ops/ax/smoke-run.sh (store delivery, the default after 2.4; OUSAST_DELIVERY unset)",
|
||||||
|
"exit": 0,
|
||||||
|
"seconds_submit_to_done": 26,
|
||||||
|
"tar_put_at": "2026-10-02T19:37:24+00:00 (the object's LastModified)",
|
||||||
|
"collected_at": "2026-10-02T19:37:26+00:00 (done.json finished)",
|
||||||
|
"put_to_collect_seconds": 2,
|
||||||
|
"poll_interval_seconds": 3,
|
||||||
|
"file": "smoke-run-store-1.txt"
|
||||||
|
},
|
||||||
|
"seeded_rerun": {
|
||||||
|
"command": "the same, with the local results root deleted and the store's runs/ax-e2e-smoke/ intact",
|
||||||
|
"exit": 0,
|
||||||
|
"seconds": 2,
|
||||||
|
"what_happened": "state.json seeded from the store: every task done, nothing submitted to ax (plane-on-kubernetes 2.1, design section 2: a rerun from another machine resumes from the store)",
|
||||||
|
"file": "smoke-run-seeded-rerun.txt"
|
||||||
|
},
|
||||||
|
"store_2": {
|
||||||
|
"command": "the same, after deleting runs/ax-e2e-smoke/ from the store (5 objects) so the task executes again, with the egress policy polled",
|
||||||
|
"exit": 0,
|
||||||
|
"seconds_submit_to_done": 24,
|
||||||
|
"tar_put_at": "2026-10-02T19:39:38+00:00",
|
||||||
|
"collected_at": "2026-10-02T19:39:40+00:00",
|
||||||
|
"put_to_collect_seconds": 2,
|
||||||
|
"file": "smoke-run-store-2.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects_delivered_to_the_store": {
|
||||||
|
"runs/ax-e2e-smoke/facts/output.tar": 10240,
|
||||||
|
"runs/ax-e2e-smoke/facts/facts.json": 451,
|
||||||
|
"runs/ax-e2e-smoke/facts/summary.json": 238,
|
||||||
|
"runs/ax-e2e-smoke/facts/done.json": 149,
|
||||||
|
"runs/ax-e2e-smoke/state.json": 350,
|
||||||
|
"note": "units.jsonl is extracted locally but not a declared output of the smoke Run, so it is not re-put"
|
||||||
|
},
|
||||||
|
"egress_policy_read_back": {
|
||||||
|
"how": "kubectl ate get egress-policy ax-e2e-smoke-facts -a default -o json, polled while the actor lived (file egress-policy-read-back.json)",
|
||||||
|
"rules": [{"tlsPassthrough": {"hostnames": ["files.because-security.com"], "ports": {"numbers": [443]}}}],
|
||||||
|
"reading": "one TLS rule naming the store host, no http rule; the model-free smoke task names no Git host and no Model host",
|
||||||
|
"also": "Egress.apply reads every policy back and raises on a mismatch, so each completed Run is itself the equality check"
|
||||||
|
},
|
||||||
|
"status_table": "ousast plane status ax-e2e-smoke printed the attribution table (facts: done, calls 0) in every run; the table is in the smoke-run-*.txt files",
|
||||||
|
"reconciler_lines": {"after_2_4": 549, "bound": 550, "note": "both paths coexist until 2.6; the receiver wiring goes then"},
|
||||||
|
"gates": {
|
||||||
|
"2.1": "pytest 2020 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0",
|
||||||
|
"2.2": "pytest 2024 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0",
|
||||||
|
"2.3": "pytest 2027 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0",
|
||||||
|
"2.4": "pytest 2031 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0",
|
||||||
|
"2.5": "pytest 2031 passed, 42 skipped, exit 0; ruff 0; format 0; mypy 0 (first attempt 2026-10-02: 11 failed + 5 errors, all the FileStore 1 GiB free-space refusal at 0.97 GiB free; rerun after the disk was freed)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
run finished: done
|
||||||
|
memory s3://sast-memory: ax-e2e-smoke: 0 tasks ingested, 0 already in the index; rows none
|
||||||
|
run ax-e2e-smoke: done (/tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/k8s-impl/results/plane/ax-e2e-smoke)
|
||||||
|
|
||||||
|
TASK MODEL CALLS PROMPT CACHE-HIT OUTPUT USD
|
||||||
|
facts - n/a n/a n/a n/a n/a [done]
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
total - n/a n/a n/a n/a n/a
|
||||||
|
smoke exit=0 seconds=2
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
run finished: done
|
||||||
|
memory s3://sast-memory: ax-e2e-smoke: 0 tasks ingested, 0 already in the index; rows none
|
||||||
|
run ax-e2e-smoke: done (/tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/k8s-impl/results/plane/ax-e2e-smoke)
|
||||||
|
|
||||||
|
TASK MODEL CALLS PROMPT CACHE-HIT OUTPUT USD
|
||||||
|
facts - 0 n/a n/a n/a n/a [done]
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
total - n/a n/a n/a n/a n/a
|
||||||
|
smoke exit=0 seconds=26
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
run finished: done
|
||||||
|
memory s3://sast-memory: ax-e2e-smoke: 0 tasks ingested, 0 already in the index; rows none
|
||||||
|
run ax-e2e-smoke: done (/tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/k8s-impl/results/plane/ax-e2e-smoke)
|
||||||
|
|
||||||
|
TASK MODEL CALLS PROMPT CACHE-HIT OUTPUT USD
|
||||||
|
facts - 0 n/a n/a n/a n/a [done]
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
total - n/a n/a n/a n/a n/a
|
||||||
|
smoke exit=0 seconds=24
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"store": "s3://sast-memory",
|
||||||
|
"objects_under_run": {
|
||||||
|
"runs/ax-e2e-smoke/facts/done.json": 149,
|
||||||
|
"runs/ax-e2e-smoke/facts/facts.json": 451,
|
||||||
|
"runs/ax-e2e-smoke/facts/output.tar": 10240,
|
||||||
|
"runs/ax-e2e-smoke/facts/summary.json": 238,
|
||||||
|
"runs/ax-e2e-smoke/state.json": 350
|
||||||
|
},
|
||||||
|
"tar_bytes": 10240,
|
||||||
|
"tar_members": {
|
||||||
|
"facts.json": 451,
|
||||||
|
"summary.json": 238,
|
||||||
|
"units.jsonl": 291
|
||||||
|
},
|
||||||
|
"extracted_files": {
|
||||||
|
"facts.json": 451,
|
||||||
|
"units.jsonl": 291,
|
||||||
|
"summary.json": 238
|
||||||
|
},
|
||||||
|
"members_equal_extracted": true,
|
||||||
|
"member_bytes_total": 980,
|
||||||
|
"extracted_bytes_total": 980,
|
||||||
|
"done_json": {
|
||||||
|
"bytes": 10240,
|
||||||
|
"files": 3,
|
||||||
|
"finished": "2026-10-02T19:39:40+00:00",
|
||||||
|
"published": [
|
||||||
|
"facts.json",
|
||||||
|
"summary.json"
|
||||||
|
],
|
||||||
|
"status": "done",
|
||||||
|
"task": "facts"
|
||||||
|
},
|
||||||
|
"tar_put_at_last_modified": "2026-10-02T19:39:38+00:00"
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
== kind cluster 'ousast' (context kind-ousast) + registry localhost:5001 (21:36:18)
|
||||||
|
cluster exists; kept
|
||||||
|
|
||||||
|
== agent substrate (ate-system) (21:36:18)
|
||||||
|
substrate ready; kept
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
ate-api-server-647bb6f8b6-c9k5k 1/1 Running 2 (13h ago) 3d1h
|
||||||
|
ate-api-server-647bb6f8b6-dznjn 1/1 Running 1 (13h ago) 3d1h
|
||||||
|
ate-controller-c95586684-g7nvn 1/1 Running 0 3d3h
|
||||||
|
atelet-4f9c293-9zpfg 1/1 Running 0 3d3h
|
||||||
|
atenet-egress-d74b8f6bf-8zmxj 1/1 Running 0 3d1h
|
||||||
|
atenet-router-54887bc8f9-rqs6g 2/2 Running 2 (13h ago) 3d3h
|
||||||
|
postgres-0 2/2 Running 3 (3d3h ago) 3d3h
|
||||||
|
rustfs-6c4c677cb6-cqvhc 1/1 Running 3 (3d3h ago) 3d3h
|
||||||
|
rustfs-bucket-init-nvfr6 0/1 Completed 0 3d3h
|
||||||
|
|
||||||
|
== egress gateway (atenet-egress, agentgateway variant: one prebuilt image, nothing built) (21:36:18)
|
||||||
|
gateway present; kept
|
||||||
|
|
||||||
|
== ax control plane (ax-system) (21:36:19)
|
||||||
|
ax ready; kept
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
ax-redis-86df786c87-pz5tp 1/1 Running 0 3d3h
|
||||||
|
ax-server-b479bdc76-hwkqb 1/1 Running 0 3d3h
|
||||||
|
ousast-pool-6656b55f5b-5brkn 1/1 Running 0 66m
|
||||||
|
ousast-pool-6656b55f5b-9wbrw 1/1 Running 0 3d3h
|
||||||
|
|
||||||
|
== artifact receiver Service (ousast-receiver.ax-system -> this host) (21:36:19)
|
||||||
|
service/ousast-receiver unchanged
|
||||||
|
endpointslice.discovery.k8s.io/ousast-receiver-host configured
|
||||||
|
|
||||||
|
== worker pool (gVisor): pools.default of the profile (21:36:20)
|
||||||
|
pool ousast-pool exists (2/2 ready); kept
|
||||||
|
pod/ousast-pool-6656b55f5b-5brkn condition met
|
||||||
|
pod/ousast-pool-6656b55f5b-9wbrw condition met
|
||||||
|
|
||||||
|
== runner image localhost:5001/ousast-runner (21:36:21)
|
||||||
|
sha256:0501baf180b29f7aa7bbb8888bebabfc5d62429b7bdaa7f5a62b5b188f8ad5f6
|
||||||
|
localhost:5001/ousast-runner:dev
|
||||||
|
runner image localhost:5001/ousast-runner@sha256:d2a55293999cc34581ef1f02a1ffb0c2dd8fc0cbe001ac952df81600e868f019 -> /home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/kind-images.json
|
||||||
|
re-pin the templates with: ousast plane workspaces ... --runner-image /home/mc/Source/OpenUltraSAST/.claude/worktrees/agent-a7d4724e0c0c407a8/ops/k8s/profiles/kind-images.json (or ousast plane repin, task 4.3)
|
||||||
|
|
||||||
|
== ax CLI from the deployed checkout (a release CLI skews from the server) (21:36:32)
|
||||||
|
ax CLI newer than the checkout; kept
|
||||||
|
|
||||||
|
== smoke task (21:36:32)
|
||||||
|
task.ax.io/ousast-smoke created
|
||||||
|
workspace.ax.io/ousast-smoke-ws unchanged
|
||||||
|
task.ax.io/ousast-smoke resumed
|
||||||
|
NAME ATESPACE PHASE ACTOR WORKER-IP AGE
|
||||||
|
ousast-smoke default Running ousast-smoke 10.244.0.29 4s
|
||||||
|
|
||||||
|
== footprint (21:36:38)
|
||||||
|
total used free shared buff/cache available
|
||||||
|
Mem: 7877 5202 382 63 2903 2674
|
||||||
|
kind-registry 32.45MiB / 7.693GiB 0.00%
|
||||||
|
up.sh exit 0
|
||||||
@@ -0,0 +1,792 @@
|
|||||||
|
{
|
||||||
|
"id": "2026-10-02-pre-push-operational-1",
|
||||||
|
"task": "pre-push-safety-net 17.7",
|
||||||
|
"requirements": [
|
||||||
|
"9.1",
|
||||||
|
"9.2",
|
||||||
|
"9.3",
|
||||||
|
"9.4",
|
||||||
|
"9.5",
|
||||||
|
"9.6",
|
||||||
|
"6.3"
|
||||||
|
],
|
||||||
|
"question": "Does the hook installed on any repository give honest, advisory-only results, and never block a push?",
|
||||||
|
"subjects": "The audit's three shallow clones (kennethreitz/records, expressjs/serve-static, unrolled/secure), each with the same planted commit c2 on c1, copied fresh from the audit's scratch clones.",
|
||||||
|
"instrument_proof": "Every replay and hook push records snapshots[].bytes_read and quick_tier files/bytes_read. The head snapshot's bytes equal `git ls-files | xargs cat | wc -c` at head (records 43,577; serve-static 61,525; secure 103,023), and the quick tier reads exactly the changed source files.",
|
||||||
|
"host": "Joern is not on the host, so host runs cannot run the engine; that is the case a first-time pip user hits, and the output must say so. The Docker engine run uses the 2026-09-14 openultrasast:dev image with the worktree src mounted read-only, as the audit did; the image was not rebuilt (1.1 GB disk free, below the 4 GB threshold).",
|
||||||
|
"before": {
|
||||||
|
"source": "/tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready/gap-report.md (audit at main 49e11e0)",
|
||||||
|
"replay_host": {
|
||||||
|
"records": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 1.06,
|
||||||
|
"files_bytes_read": [
|
||||||
|
21,
|
||||||
|
43577
|
||||||
|
],
|
||||||
|
"findings_shown": 0,
|
||||||
|
"terminal": "generic: Some checks could not be completed. Review the comparison details...",
|
||||||
|
"skip_reasons_named_in_terminal": 0
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 1.1,
|
||||||
|
"files_bytes_read": [
|
||||||
|
26,
|
||||||
|
61525
|
||||||
|
],
|
||||||
|
"findings_shown": 0,
|
||||||
|
"terminal": "same generic line",
|
||||||
|
"skip_reasons_named_in_terminal": 0
|
||||||
|
},
|
||||||
|
"secure": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 1.04,
|
||||||
|
"files_bytes_read": [
|
||||||
|
19,
|
||||||
|
103023
|
||||||
|
],
|
||||||
|
"findings_shown": 0,
|
||||||
|
"terminal": "same generic line; Go not named",
|
||||||
|
"skip_reasons_named_in_terminal": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"docker_engine_serve_static": {
|
||||||
|
"deadline_30": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 31.3,
|
||||||
|
"terminal": "Analysis did not finish. Rerun with a longer analysis deadline."
|
||||||
|
},
|
||||||
|
"deadline_300": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 261.7,
|
||||||
|
"raw_engine_findings": 2,
|
||||||
|
"admitted": 0,
|
||||||
|
"dispositions": "injection and output_encoding at debug.js:2: comparison_unknown/base_incomplete + capability_unavailable",
|
||||||
|
"terminal": "No actionable defects reported; Details: /out/... (container path)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quick_scan": {
|
||||||
|
"serve-static": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.46,
|
||||||
|
"findings": "js-eval, js-xss-response at debug.js:2"
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.49,
|
||||||
|
"findings": "2 pre-existing (setup.py os-command); planted %-formatted SQL missed"
|
||||||
|
},
|
||||||
|
"secure": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.48,
|
||||||
|
"findings": 0,
|
||||||
|
"terminal": "No quick-mode findings were emitted (silent zero for Go)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hook": {
|
||||||
|
"ousast_not_on_path": {
|
||||||
|
"push_exit": "rejected",
|
||||||
|
"terminal": ".git/hooks/pre-push: 13: exec: ousast: not found"
|
||||||
|
},
|
||||||
|
"first_push_new_branch": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.58,
|
||||||
|
"terminal": "No analysis result is available. (does not say new branch)"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 1.36,
|
||||||
|
"terminal": "No actionable defects reported; analysis is incomplete."
|
||||||
|
},
|
||||||
|
"bad_OUSAST_PUSH_DEADLINE_or_cache_inside_repo": {
|
||||||
|
"exit": 2,
|
||||||
|
"effect": "would block the push"
|
||||||
|
},
|
||||||
|
"install": "ops/install-pre-push only (checkout); not in the wheel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"verdict": {
|
||||||
|
"never_blocks": "PASS in this sample: 10 hook pushes across the three repositories all exited 0, including `ousast` absent from PATH (rejected before) and OUSAST_PUSH_DEADLINE=abc (exit 2 before). Blocking stays an explicit opt-in (unit-tested, not exercised here).",
|
||||||
|
"never_hides": "PASS in this sample: every incomplete run printed one plain `Skipped:` line per reason (2-5 per run), with no hex path or sha256 token. Go is named as not covered (`language not covered: go (1 changed file(s), 10 in the repository)`, and `notice: language not covered: go (10 files)` in `scan --mode quick`). The first push of a new branch says there is no base and what it checked instead.",
|
||||||
|
"quick_tier_within_deadline": "On the host, every replay and hook push finished in 0.69-1.21 s against the 30 s deadline. The quick tier read exactly the changed source file (records 1 file / 17,449 B; serve-static 1 / 87 B; secure 1 / 193 B).",
|
||||||
|
"planted_js_eval": "SURFACES. Quick tier: js-eval (CWE-95) and js-xss-response (CWE-79) at debug.js:2 on the host, 0.9 s, in replay and in the hook push. Engine (Docker, deadline 300): injection and output_encoding at debug.js:2, now novelty `new` / `file_added_in_head` (before: `unknown` / `base_incomplete`). The only veto is `capability_unavailable`, so it is shown as an advisory engine finding, not an alert.",
|
||||||
|
"planted_python_sql": "STILL MISSED. No quick rule matches the %-formatted db.query string in records.py, and the host has no Joern, so nothing in the pre-push path reports it. Recorded as a coverage gap, with no rule edit.",
|
||||||
|
"planted_go_command": "NOT DETECTED, NOW NAMED: Go has no engine frontend and no quick rules.",
|
||||||
|
"engine_deadline": "Not solved. The Docker engine took 190.9 s wall time (head 107.6 s, base 80.7 s) against a 300 s deadline. The audit measured 261.7 s on a different image/src combination. A 30 s hook deadline still cannot hold it. --engine background starts it after the push, and the next push showed the earlier result. On the host that result says 'the engine did not run (Joern unavailable)', which is honest."
|
||||||
|
},
|
||||||
|
"coverage_gaps": [
|
||||||
|
"records: the planted Python SQL injection (`db.query(\"... '%s'\" % name)`) is matched by no quick rule, before or after. Recorded as a quick-rule coverage gap; no rule was edited for a planted sample (that would be hand-tuning). On the host the engine cannot run, so nothing in the pre-push path reports it.",
|
||||||
|
"secure: Go has no engine frontend and no quick rules. It is now named in the terminal (language not covered: go), not detected.",
|
||||||
|
"The engine still cannot finish inside the 30 s hook deadline (the audit measured 260 s on 26 files); --engine background moves it after the push, graph reuse remains the later speed task."
|
||||||
|
],
|
||||||
|
"after": {
|
||||||
|
"replay_host": {
|
||||||
|
"records": {
|
||||||
|
"pre-push --engine inline": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 1.206878771,
|
||||||
|
"deadline_seconds": 30,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"changed lines could not be matched one-to-one to base lines in records.py",
|
||||||
|
"the changed code has no counterpart in the base revision to compare against",
|
||||||
|
"engine did not run: the code graph could not be built (is Joern installed? 'joern-parse' must be on PATH, or run the hook through ops/ousast-docker)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 17449 bytes read).",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
21,
|
||||||
|
43577
|
||||||
|
],
|
||||||
|
[
|
||||||
|
21,
|
||||||
|
43482
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
17449
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.041
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"7265636f7264732e7079:ambiguous_line_correspondence",
|
||||||
|
"base_counterpart_unresolved",
|
||||||
|
"cpg_build_failed",
|
||||||
|
"analysis_incomplete",
|
||||||
|
"base_comparison_unavailable"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 0.483
|
||||||
|
},
|
||||||
|
"pre-push --engine off": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.954010589,
|
||||||
|
"deadline_seconds": 30,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"changed lines could not be matched one-to-one to base lines in records.py",
|
||||||
|
"engine checks were switched off for this run (--engine off)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 17449 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
21,
|
||||||
|
43577
|
||||||
|
],
|
||||||
|
[
|
||||||
|
21,
|
||||||
|
43482
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
17449
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.042
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"7265636f7264732e7079:ambiguous_line_correspondence",
|
||||||
|
"engine_off",
|
||||||
|
"analysis_incomplete",
|
||||||
|
"base_comparison_unavailable"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 0.498
|
||||||
|
},
|
||||||
|
"scan --mode quick": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.407089313,
|
||||||
|
"terminal": [
|
||||||
|
"file_targets=9",
|
||||||
|
"findings=2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"pre-push --engine inline": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.892543629,
|
||||||
|
"deadline_seconds": 30,
|
||||||
|
"quick_findings_shown": [
|
||||||
|
"js-eval (CWE-95, critical) at debug.js:2: Dynamic JavaScript evaluation needs review",
|
||||||
|
"js-xss-response (CWE-79, medium) at debug.js:2: Unsanitized HTTP response reflection needs review"
|
||||||
|
],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"the changed code has no counterpart in the base revision to compare against",
|
||||||
|
"engine did not run: the code graph could not be built (is Joern installed? 'joern-parse' must be on PATH, or run the hook through ops/ousast-docker)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: 2 pattern match(es) on changed code (1 file(s), 87 bytes read). Advisory: not verified by the engine.",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
26,
|
||||||
|
61525
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25,
|
||||||
|
61438
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
87
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.017
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"base_counterpart_unresolved",
|
||||||
|
"cpg_build_failed",
|
||||||
|
"analysis_incomplete",
|
||||||
|
"base_comparison_unavailable"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 0.496
|
||||||
|
},
|
||||||
|
"pre-push --engine off": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.878791036,
|
||||||
|
"deadline_seconds": 30,
|
||||||
|
"quick_findings_shown": [
|
||||||
|
"js-eval (CWE-95, critical) at debug.js:2: Dynamic JavaScript evaluation needs review",
|
||||||
|
"js-xss-response (CWE-79, medium) at debug.js:2: Unsanitized HTTP response reflection needs review"
|
||||||
|
],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"engine checks were switched off for this run (--engine off)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: 2 pattern match(es) on changed code (1 file(s), 87 bytes read). Advisory: not verified by the engine.",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
26,
|
||||||
|
61525
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25,
|
||||||
|
61438
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
87
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.016
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"engine_off",
|
||||||
|
"analysis_incomplete",
|
||||||
|
"base_comparison_unavailable"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 0.471
|
||||||
|
},
|
||||||
|
"scan --mode quick": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.394269229,
|
||||||
|
"terminal": [
|
||||||
|
"file_targets=4",
|
||||||
|
"findings=2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"secure": {
|
||||||
|
"pre-push --engine inline": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.762186729,
|
||||||
|
"deadline_seconds": 30,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"language not covered: go (1 changed file(s), 10 in the repository): no engine frontend and no quick rules, so silence here is not a clean result",
|
||||||
|
"the changed code has no counterpart in the base revision to compare against",
|
||||||
|
"engine did not run: the code graph could not be built (is Joern installed? 'joern-parse' must be on PATH, or run the hook through ops/ousast-docker)",
|
||||||
|
"engine has no frontend for at least one language in this push",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 193 bytes read).",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
19,
|
||||||
|
103023
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18,
|
||||||
|
102830
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
193
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.015
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"language_not_covered:go:1:10",
|
||||||
|
"base_counterpart_unresolved",
|
||||||
|
"cpg_build_failed",
|
||||||
|
"frontend_unsupported",
|
||||||
|
"analysis_incomplete",
|
||||||
|
"base_comparison_unavailable"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 0.352
|
||||||
|
},
|
||||||
|
"pre-push --engine off": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.694374205,
|
||||||
|
"deadline_seconds": 30,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"language not covered: go (1 changed file(s), 10 in the repository): no engine frontend and no quick rules, so silence here is not a clean result",
|
||||||
|
"engine checks were switched off for this run (--engine off)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 193 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
19,
|
||||||
|
103023
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18,
|
||||||
|
102830
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
193
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.016
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"language_not_covered:go:1:10",
|
||||||
|
"engine_off",
|
||||||
|
"analysis_incomplete",
|
||||||
|
"base_comparison_unavailable"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 0.311
|
||||||
|
},
|
||||||
|
"scan --mode quick": {
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 0.372512766,
|
||||||
|
"terminal": [
|
||||||
|
"file_targets=10",
|
||||||
|
"findings=0",
|
||||||
|
"notice: language not covered: go (10 files)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hook_host": {
|
||||||
|
"records": {
|
||||||
|
"new-branch": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.873912782,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"new branch: the remote has no base to compare against, so the engine comparison did not run (OUSAST_COMPARISON_BASE=<local branch> compares against a local branch instead)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"New branch: instead, quick rules checked the files its last commit changed (compared with c925575).",
|
||||||
|
"Quick rules: no pattern match on changed code (2 file(s), 18416 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-records.git",
|
||||||
|
" * [new branch] HEAD~1 -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.890046656,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"changed lines could not be matched one-to-one to base lines in records.py",
|
||||||
|
"the changed code has no counterpart in the base revision to compare against",
|
||||||
|
"engine did not run: the code graph could not be built (is Joern installed? 'joern-parse' must be on PATH, or run the hook through ops/ousast-docker)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 17449 bytes read).",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-records.git",
|
||||||
|
" ea42736..ad3d7db HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"install_uninstall": [
|
||||||
|
"install rc=0",
|
||||||
|
"uninstall rc=0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"new-branch": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.904302643,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"new branch: the remote has no base to compare against, so the engine comparison did not run (OUSAST_COMPARISON_BASE=<local branch> compares against a local branch instead)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"New branch: instead, quick rules checked the files its last commit changed (compared with 76312e1).",
|
||||||
|
"Quick rules: no pattern match on changed code (0 file(s), 0 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-serve-static.git",
|
||||||
|
" * [new branch] HEAD~1 -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.92386555,
|
||||||
|
"quick_findings_shown": [
|
||||||
|
"js-eval (CWE-95, critical) at debug.js:2: Dynamic JavaScript evaluation needs review",
|
||||||
|
"js-xss-response (CWE-79, medium) at debug.js:2: Unsanitized HTTP response reflection needs review"
|
||||||
|
],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"the changed code has no counterpart in the base revision to compare against",
|
||||||
|
"engine did not run: the code graph could not be built (is Joern installed? 'joern-parse' must be on PATH, or run the hook through ops/ousast-docker)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: 2 pattern match(es) on changed code (1 file(s), 87 bytes read). Advisory: not verified by the engine.",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-serve-static.git",
|
||||||
|
" 74be78a..468213d HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no-ousast": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.017937758,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [],
|
||||||
|
"other_lines": [
|
||||||
|
"ousast: pre-push check did not run: 'ousast' is not on PATH. Install it (pip install git+https://github.com/norandom/OpenUltraSAST) or set OUSAST_COMMAND. Push continues.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-serve-static.git",
|
||||||
|
" 468213d..aad3dfe HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bad-deadline": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.300776,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [],
|
||||||
|
"other_lines": [
|
||||||
|
"usage: ousast pre-push [-h] [--base BASE] [--head HEAD] [--remote NAME URL]",
|
||||||
|
" [--comparison-base COMPARISON_BASE]",
|
||||||
|
" [--model-config MODEL_CONFIG] [--prior-hook PRIOR_HOOK]",
|
||||||
|
" --artifact ARTIFACT [--cache-dir CACHE_DIR]",
|
||||||
|
" [--deadline DEADLINE]",
|
||||||
|
" [--cancellation-allowance CANCELLATION_ALLOWANCE]",
|
||||||
|
" [--max-regions MAX_REGIONS]",
|
||||||
|
" [--mode {advisory,blocking}]",
|
||||||
|
" [--incomplete-coverage {allow,block}]",
|
||||||
|
" [--engine {inline,background,off}]",
|
||||||
|
" [--background-deadline BACKGROUND_DEADLINE]",
|
||||||
|
" [--experimental-declarations EXPERIMENTAL_DECLARATIONS]",
|
||||||
|
" [--experimental-record-vetoes]",
|
||||||
|
" path",
|
||||||
|
"ousast pre-push: error: argument --deadline: invalid float value: 'abc'",
|
||||||
|
"ousast: pre-push check did not run: invalid arguments or settings (message above). Push continues.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-serve-static.git",
|
||||||
|
" aad3dfe..6fcfec7 HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"background-1": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.915950565,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [
|
||||||
|
"Engine: running in the background for c5b6c4d7060f (deadline 900.0 s); the next push shows its result."
|
||||||
|
],
|
||||||
|
"skip_lines": [
|
||||||
|
"engine checks run in the background after this push; the next push shows their result",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 93 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Engine: running in the background for c5b6c4d7060f (deadline 900.0 s); the next push shows its result.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-serve-static.git",
|
||||||
|
" 6fcfec7..c5b6c4d HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"background-2": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.879868436,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [
|
||||||
|
"Engine result for c5b6c4d7060f from the previous push: the engine did not run (Joern unavailable)."
|
||||||
|
],
|
||||||
|
"skip_lines": [
|
||||||
|
"engine checks were switched off for this run (--engine off)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Engine result for c5b6c4d7060f from the previous push: the engine did not run (Joern unavailable).",
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 99 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-serve-static.git",
|
||||||
|
" c5b6c4d..3eee5ef HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"install_uninstall": [
|
||||||
|
"install rc=0",
|
||||||
|
"uninstall rc=0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secure": {
|
||||||
|
"new-branch": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.96295861,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"new branch: the remote has no base to compare against, so the engine comparison did not run (OUSAST_COMPARISON_BASE=<local branch> compares against a local branch instead)",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"New branch: instead, quick rules checked the files its last commit changed (compared with ba42bc3).",
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 41228 bytes read).",
|
||||||
|
"No analysis result is available.",
|
||||||
|
"Notice: Some checks could not be completed. Review the comparison details and resolve the recorded gaps before retrying.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-secure.git",
|
||||||
|
" * [new branch] HEAD~1 -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"push_exit": 0,
|
||||||
|
"wall_seconds": 0.747049238,
|
||||||
|
"quick_findings_shown": [],
|
||||||
|
"engine_findings_shown": [],
|
||||||
|
"skip_lines": [
|
||||||
|
"language not covered: go (1 changed file(s), 10 in the repository): no engine frontend and no quick rules, so silence here is not a clean result",
|
||||||
|
"the changed code has no counterpart in the base revision to compare against",
|
||||||
|
"engine did not run: the code graph could not be built (is Joern installed? 'joern-parse' must be on PATH, or run the hook through ops/ousast-docker)",
|
||||||
|
"engine has no frontend for at least one language in this push",
|
||||||
|
"no base revision was analysed, so no engine finding can be called new or worsened"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Quick rules: no pattern match on changed code (1 file(s), 193 bytes read).",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details.",
|
||||||
|
"To /tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/ready-impl/bare-secure.git",
|
||||||
|
" 740abc8..64739c4 HEAD -> main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"install_uninstall": [
|
||||||
|
"install rc=0",
|
||||||
|
"uninstall rc=0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"docker_engine_serve_static": {
|
||||||
|
"log": [
|
||||||
|
"MemAvailable=3283MB disk_free=1159MiB",
|
||||||
|
"base=74be78a8ffad679edfe135c457ce141114d96fcc head=468213d8a737ad03a1f712c792c1db8476e154df",
|
||||||
|
"exit=0 elapsed=190.895648822s"
|
||||||
|
],
|
||||||
|
"exit": 0,
|
||||||
|
"wall_seconds": 190.895648822,
|
||||||
|
"deadline_seconds": 300,
|
||||||
|
"quick_findings_shown": [
|
||||||
|
"js-eval (CWE-95, critical) at debug.js:2: Dynamic JavaScript evaluation needs review",
|
||||||
|
"js-xss-response (CWE-79, medium) at debug.js:2: Unsanitized HTTP response reflection needs review"
|
||||||
|
],
|
||||||
|
"engine_findings_shown": [
|
||||||
|
"Engine: 2 finding(s) this push introduced. Advisory: the capability is not yet qualified, so not an alert."
|
||||||
|
],
|
||||||
|
"skip_lines": [
|
||||||
|
"engine findings are not qualified as alerts yet: no capability has passed independent evaluation",
|
||||||
|
"the capability registry does not match this build (eligibility provenance mismatch), so no capability can be used",
|
||||||
|
"20 engine check(s) depend on dynamic or external code the engine cannot follow"
|
||||||
|
],
|
||||||
|
"other_lines": [
|
||||||
|
"Engine: 2 finding(s) this push introduced. Advisory: the capability is not yet qualified, so not an alert.",
|
||||||
|
"Quick rules: 2 pattern match(es) on changed code (1 file(s), 87 bytes read). Advisory: not verified by the engine.",
|
||||||
|
"No actionable defects reported; analysis is incomplete.",
|
||||||
|
"Notice: Some checks are not enabled for normal alerts. Review capability coverage in the details."
|
||||||
|
],
|
||||||
|
"snapshots_files_bytes": [
|
||||||
|
[
|
||||||
|
26,
|
||||||
|
61525
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25,
|
||||||
|
61438
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_files_bytes": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
87
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quick_tier_seconds": [
|
||||||
|
0.038
|
||||||
|
],
|
||||||
|
"coverage_reasons_raw": [
|
||||||
|
"capability_unavailable",
|
||||||
|
"eligibility:eligibility_provenance_mismatch",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:7ba72698b15c888c0835cad443fc1f177f1d2e13ed2658b17e816cc0b81415de",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:392beff95d0e245cbaef2676813d7f69d35599fb32977c8f197e6df46738fc75",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:ec16a67e984a2f82c3ce9a04dbc44eae1e950393b1c1c4fae88485446f1e2f95",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:0a3bd3f145260792e9a5d4af2546020689cfd08d0b1e82264d7fe31dda7210e7",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:1551c000ba35ba4e5488013f51171b27ab10df8b80f29509610670192cff8a30",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:5b687fa7647cefd89d064b3e1b5e5a62842f8fd23104900e8af8d5b14fc800fd",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:f122da538137e0a3a3e598c24e6af57c5e0b90ff48dacb4fd1b9d82ac20c35ef",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:8ab9ddcb1f4431fa3a3c1cce4cc3d289bcb5d363d47aac1bcf35bc7db86ce866",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:19f1031c2c010d91fb41e23955cd5f18eaa5ede355e68fab9e18c8249c21c2ee",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:d31a861124274a09e3cb8f3398831ca299cc0cfad72ea385083c1fd88b7a2c45",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:c8a1c8042befd3ff6f296921666949850a1c54d63c77dd44296e7be96e621127",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:3bb50c50f77027f1e0347f490e35fdd0a75ae586fdf92775c753877833a9d099",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:c32cb93cad379fee7b814900e6673aa5ae470c27acd55d33f855987bccb1ab8e",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:300fba1acc39cb0ab32ebbd66a8674daecf496b3d8c675d27f7f71cf9f788c9e",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:40c99dfe6d923602dc3aa6353f3b5bc017c0d1a67595c19ddf8206ef431db23d",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:0e99666c9be7c4704f6c6f858844d721ed8f0b4e3978d53073bc2c1b64d3f075",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:da903214d018e0b1bd7a3a3b635b3e0fb2a23fd44527f41a9483c0bf3913f2df",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:79d89b41b86aadcc0423baa786e0518ccff2a5b7853ff05986682ecbd636af52",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:7d3331ca3c06d1013c758d3648bfebb1223385955e77df977266fc621f62e733",
|
||||||
|
"dynamic_external_or_depth_context_unresolved:contributor-scan:54fd93450c8db7e6daef06fe795babb26726402211ca8f6d0a8679a375786041",
|
||||||
|
"analysis_incomplete"
|
||||||
|
],
|
||||||
|
"admitted_alerts": 0,
|
||||||
|
"push_disposition": "allow",
|
||||||
|
"total_seconds_internal": 189.327,
|
||||||
|
"dispositions": [
|
||||||
|
{
|
||||||
|
"family": "injection",
|
||||||
|
"site": "debug.js:2:debug",
|
||||||
|
"novelty": "new",
|
||||||
|
"reason": "file_added_in_head",
|
||||||
|
"vetoes": [
|
||||||
|
"capability_unavailable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "output_encoding",
|
||||||
|
"site": "debug.js:2:debug",
|
||||||
|
"novelty": "new",
|
||||||
|
"reason": "file_added_in_head",
|
||||||
|
"vetoes": [
|
||||||
|
"capability_unavailable"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timings": {
|
||||||
|
"deadline_seconds": 300.0,
|
||||||
|
"cancellation_allowance_seconds": 2.0,
|
||||||
|
"resolution_seconds": 0.0,
|
||||||
|
"provenance_seconds": 0.3,
|
||||||
|
"snapshot_seconds": 0.3,
|
||||||
|
"discovery_seconds": 0.3,
|
||||||
|
"preparation_seconds": 0.6,
|
||||||
|
"quick_seconds": 0.0,
|
||||||
|
"head_seconds": 107.6,
|
||||||
|
"base_seconds": 80.7,
|
||||||
|
"admission_seconds": 0.0,
|
||||||
|
"cleanup_seconds": 0.0,
|
||||||
|
"total_seconds": 189.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"worktree_head": "591818d + hook install-hint fix (this commit)",
|
||||||
|
"wheel_sha256": "5b725adb052239c2afaec237fecf582f69fc5ea61799dc67f0c07e09e10908c9",
|
||||||
|
"installed_with": "uv pip install <wheel> into a scratch venv"
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"records": {
|
||||||
|
"language": "python",
|
||||||
|
"tracked_files": 21,
|
||||||
|
"tracked_bytes": 43577,
|
||||||
|
"planted": "records.py: db.query(\"... '%s'\" % name)"
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"language": "javascript",
|
||||||
|
"tracked_files": 26,
|
||||||
|
"tracked_bytes": 61525,
|
||||||
|
"planted": "new debug.js: res.end(String(eval(req.query.code)))"
|
||||||
|
},
|
||||||
|
"secure": {
|
||||||
|
"language": "go",
|
||||||
|
"tracked_files": 19,
|
||||||
|
"tracked_bytes": 103023,
|
||||||
|
"planted": "new run.go: exec.Command(\"sh\", \"-c\", r.URL.Query().Get(\"cmd\"))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"The Docker engine run used the worktree src at 591818d mounted over the stale 2026-09-14 openultrasast:dev image (not rebuilt: 1.1 GB disk free, below the 4 GB threshold). `eligibility_provenance_mismatch` still appears; the audit already suspected src/image skew, and it is not investigated here.",
|
||||||
|
"The run shows host paths after `Details:` because ops/ousast-docker mounts the artifact directory at its own path. Before, it showed the container path /out/...",
|
||||||
|
"The hook's 'not on PATH' hint first said 'pip install openultrasast', which is not on PyPI. The first proof run caught it. It was corrected to the git URL, and the replay and hook proofs were rerun on the rebuilt wheel recorded under build. The Docker engine run predates the hint fix and does not involve the hook script.",
|
||||||
|
"Host runs read their input. Each head snapshot's bytes equal `git ls-files | xargs cat | wc -c` at head, and the quick tier lists the files it read."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"created": "2026-10-03",
|
||||||
|
"note": "COUNTS ONLY. Real (vulnerable function, fixed function) pairs where the function survives the fix, mined at zero model cost from reviewed GitHub advisories for the six compiled families, added as pointer-pair corpus benchmarks/pairs/advisory-fixes-2 (98 pairs, source `advisory-fixes-2` in learn/sources.toml), labelled, given static feature records and built into the memory store; their excerpts embedded. No chat-model call. Candidate, path, function and repository names stay in the catalog; the two pairs the reserved-population test flagged are withheld (never printed: found by bisection with the test's output discarded).",
|
||||||
|
"method": {
|
||||||
|
"enumeration": "gh api /advisories?type=reviewed&ecosystem=<eco>&cwes=<family CWEs>&published=>2025-06-01, per ecosystem (pip, npm, maven, composer, go, rubygems) x family; newest first",
|
||||||
|
"advisories_enumerated": 6594,
|
||||||
|
"advisories_published_after_2026_06_01": 2400,
|
||||||
|
"automated_screen": "single family by CWE; a linked fix commit (at most 3, one repository); repository not named anywhere under benchmarks/ (populations v1/v2, every pair corpus; excluded v3 file names never opened), no WordPress plugin, one pair per repository; SPDX licence permissive; one-parent commit with <= 4 non-test source files and <= 120 changed source lines; the function holding most changed CODE lines (comments and blanks excluded) inside its own matcher span, declared exactly once on both sides, body changed, >= 25% of the file's changed code lines (second pass; the first pass counted lines by nearest declaration above)",
|
||||||
|
"file_reads": "the two versions of each changed source file through the contents API (metadata + two raw files per file; no clone, no scanner, engine, plane task or model on any of these repositories)",
|
||||||
|
"manual_review": "every automated candidate's function diff was read (agent, no model call) and kept only when the named function itself holds the fix",
|
||||||
|
"reserved_population_guard": "tests/test_independent_population.py::test_no_reserved_repository_is_referenced_by_the_development_tree, run with its output discarded; bisection over catalog subsets, 23 runs",
|
||||||
|
"quotas": "20 per family on the second pass (14 on the first); advisories past a met quota were not examined",
|
||||||
|
"mechanism": "family default (injection/output_encoding/deserialization source_reaches_sink, path path_join_user_input, access_control missing_auth_guard, untrusted_destination validation_strength for CWE-601 else source_reaches_sink); provenance = agent"
|
||||||
|
},
|
||||||
|
"screened": {
|
||||||
|
"advisories_examined_second_pass": 2983,
|
||||||
|
"outcomes_second_pass": {
|
||||||
|
"no fix commit linked": 1247,
|
||||||
|
"repository already in benchmarks/ (populations v1/v2 or a pair corpus)": 520,
|
||||||
|
"language outside the feature vocabulary (go/ruby)": 332,
|
||||||
|
"CWEs span several families": 253,
|
||||||
|
"repository already kept (one pair per repository)": 140,
|
||||||
|
"licence not permissive or unrecorded": 135,
|
||||||
|
"large commit (refactor or release, not the fix)": 125,
|
||||||
|
"kept": 110,
|
||||||
|
"no function declared on both sides with a changed body": 51,
|
||||||
|
"fix spread over several commits or repositories": 25,
|
||||||
|
"manual review: the chosen function does not hold the fix (comment-only, misattributed, partial or family mismatch)": 13,
|
||||||
|
"no source file changed": 11,
|
||||||
|
"merge or root commit": 11,
|
||||||
|
"language outside the feature vocabulary (go/ruby/c#/...)": 9,
|
||||||
|
"WordPress plugin (population v3 safety)": 1
|
||||||
|
},
|
||||||
|
"candidates_manually_reviewed": 145,
|
||||||
|
"kept_after_review": 100,
|
||||||
|
"review_drops": {
|
||||||
|
"total": 45,
|
||||||
|
"function misattributed (matcher span overrun or an undetected enclosing declaration)": 13,
|
||||||
|
"change in the function unrelated to the advisory (refactor, rename, formatting, another advisory)": 11,
|
||||||
|
"partial: the function only calls or configures the real fix": 7,
|
||||||
|
"comment or documentation change only": 6,
|
||||||
|
"one pair per repository (a reviewed pair of the same repository kept)": 4,
|
||||||
|
"second pass chose another function of an advisory already reviewed": 2,
|
||||||
|
"family mismatch": 1,
|
||||||
|
"unsure": 1
|
||||||
|
},
|
||||||
|
"reserved_population_drops": 2,
|
||||||
|
"kept": 98
|
||||||
|
},
|
||||||
|
"kept": {
|
||||||
|
"total": 98,
|
||||||
|
"by_family": {
|
||||||
|
"access_control": 17,
|
||||||
|
"deserialization": 11,
|
||||||
|
"injection": 20,
|
||||||
|
"output_encoding": 16,
|
||||||
|
"path": 17,
|
||||||
|
"untrusted_destination": 17
|
||||||
|
},
|
||||||
|
"post_cutoff_by_family": {
|
||||||
|
"access_control": 13,
|
||||||
|
"deserialization": 2,
|
||||||
|
"injection": 20,
|
||||||
|
"output_encoding": 16,
|
||||||
|
"path": 17,
|
||||||
|
"untrusted_destination": 11
|
||||||
|
},
|
||||||
|
"post_cutoff_total": 79,
|
||||||
|
"by_language": {
|
||||||
|
"typescript": 34,
|
||||||
|
"python": 30,
|
||||||
|
"javascript": 15,
|
||||||
|
"java": 10,
|
||||||
|
"php": 9
|
||||||
|
},
|
||||||
|
"by_ecosystem": {
|
||||||
|
"npm": 48,
|
||||||
|
"pip": 32,
|
||||||
|
"maven": 9,
|
||||||
|
"composer": 9
|
||||||
|
},
|
||||||
|
"licences": {
|
||||||
|
"MIT": 67,
|
||||||
|
"Apache-2.0": 24,
|
||||||
|
"BSD-3-Clause": 5,
|
||||||
|
"ISC": 1,
|
||||||
|
"Unlicense": 1
|
||||||
|
},
|
||||||
|
"split": {
|
||||||
|
"train": 51,
|
||||||
|
"holdout": 47
|
||||||
|
},
|
||||||
|
"published_range": [
|
||||||
|
"2025-06-06",
|
||||||
|
"2026-10-02"
|
||||||
|
],
|
||||||
|
"verification": {
|
||||||
|
"verified": 98,
|
||||||
|
"excerpt_bytes_read": 526134
|
||||||
|
},
|
||||||
|
"known_limit": "excerpts inherit the label builder's matcher: some spans run past the function's end into the next declaration; the label is path::function and the changed lines were checked inside the span"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"command": "ousast learn labels --root . --out <scratch> --snapshot <scratch>",
|
||||||
|
"rows": [
|
||||||
|
6589,
|
||||||
|
6981
|
||||||
|
],
|
||||||
|
"new_pair_rows": 392,
|
||||||
|
"new_pin_rows_by_label": {
|
||||||
|
"positive": 98,
|
||||||
|
"negative": 98
|
||||||
|
},
|
||||||
|
"fixed_side_negative": "98 of 98 pairs (absent_fixed_side 0)"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"command": "benchmarks/learn/build_features.py --labels <the 392 new rows> --units <units.json of `ousast plane harvest --catalog benchmarks/pairs/advisory-fixes-2/catalog.toml` (manifests written, no Run started)> --kinds pairs",
|
||||||
|
"records": 392,
|
||||||
|
"instrument_states_plane_all_families": {
|
||||||
|
"agree:missing": 74,
|
||||||
|
"agree:none": 122,
|
||||||
|
"delta:not_applicable": 196,
|
||||||
|
"engine:missing": 128,
|
||||||
|
"engine:none": 68,
|
||||||
|
"entry_points:not_applicable": 196,
|
||||||
|
"facts:not_applicable": 196,
|
||||||
|
"language:ran": 196,
|
||||||
|
"model_sinks:missing": 196,
|
||||||
|
"quick:ran": 196,
|
||||||
|
"roles:ran": 196,
|
||||||
|
"source:ran": 196,
|
||||||
|
"verify:missing": 74,
|
||||||
|
"verify:none": 122
|
||||||
|
},
|
||||||
|
"note": "quick rules and deterministic roles ran on each side; engine, verify, agree and model sinks never ran for these pairs (missing/none: null features, never zeros)"
|
||||||
|
},
|
||||||
|
"memory_build": {
|
||||||
|
"command": "ousast learn memory build --labels <labels> --units <merged units.json> --profile static --root . (exit 2: adjudications 9 of 43 anonymous functions, unchanged since 2026-10-01; rows kept)",
|
||||||
|
"examples": [
|
||||||
|
937,
|
||||||
|
1133
|
||||||
|
],
|
||||||
|
"pairs_examples": [
|
||||||
|
772,
|
||||||
|
968
|
||||||
|
],
|
||||||
|
"pairs_counts_after": {
|
||||||
|
"absent_fixed_side": 0,
|
||||||
|
"examples": 968,
|
||||||
|
"labels": 1936,
|
||||||
|
"no_excerpt": 0,
|
||||||
|
"no_record": 968,
|
||||||
|
"recorded": 968,
|
||||||
|
"unread": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positives_only_groups": {
|
||||||
|
"how": "the 2026-10-02 gap record's count: static example rows of the store, per family, repository groups with a positive and no negative",
|
||||||
|
"before": 99,
|
||||||
|
"after": 99,
|
||||||
|
"before_note": "the store before this harvest held 937 examples (the 10-02 gap record counted 904 and 98; the 10-02 rebuild added one population-v2 positive without a negative)",
|
||||||
|
"why_unchanged": "the 99 are data gaps of other corpora (94 vibe-py pairs whose fixed side lost the function, 3 dev-php, 2 population-v2); a new repository group arrives with both labels, so it adds paired groups and leaves those counts as they were",
|
||||||
|
"paired_groups_total": [
|
||||||
|
151,
|
||||||
|
249
|
||||||
|
],
|
||||||
|
"positives_only_share_of_positive_groups_six_families": [
|
||||||
|
0.4034,
|
||||||
|
0.2857
|
||||||
|
],
|
||||||
|
"by_family_before_after": {
|
||||||
|
"access_control": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
12,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
32,
|
||||||
|
49
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
20,
|
||||||
|
37
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
20,
|
||||||
|
37
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config_secrets": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
3,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
13,
|
||||||
|
13
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deserialization": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
20,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
41,
|
||||||
|
52
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
21,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
21,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"injection": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
11,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
44,
|
||||||
|
64
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
33,
|
||||||
|
53
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
70,
|
||||||
|
90
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
173,
|
||||||
|
173
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"output_encoding": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
14,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
36,
|
||||||
|
52
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
22,
|
||||||
|
38
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
61,
|
||||||
|
77
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
12,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
33,
|
||||||
|
50
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
21,
|
||||||
|
38
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
21,
|
||||||
|
38
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"untrusted_destination": {
|
||||||
|
"groups_pos_only": [
|
||||||
|
27,
|
||||||
|
27
|
||||||
|
],
|
||||||
|
"groups_pos": [
|
||||||
|
52,
|
||||||
|
69
|
||||||
|
],
|
||||||
|
"groups_both": [
|
||||||
|
25,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
"examples_neg": [
|
||||||
|
36,
|
||||||
|
53
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embeddings": {
|
||||||
|
"command": "ousast learn memory embed --budget-usd 0.20 (dry run first: 66,005 estimated tokens, $0.0013)",
|
||||||
|
"requested": 1004,
|
||||||
|
"cached": 781,
|
||||||
|
"embedded": 223,
|
||||||
|
"calls": 7,
|
||||||
|
"tokens": 71163,
|
||||||
|
"usd": 0.001423,
|
||||||
|
"note": "embedded = the new pairs' excerpts plus the excerpts the 10-02 rebuild left without a vector; OpenRouter, the embedding model .env names"
|
||||||
|
},
|
||||||
|
"spend": {
|
||||||
|
"chat_model_calls": 0,
|
||||||
|
"embedding_usd": 0.001423,
|
||||||
|
"github_api": "metadata and raw contents only"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
|||||||
|
{
|
||||||
|
"created": "2026-10-04",
|
||||||
|
"note": "COUNTS ONLY. Batch 3 of real (vulnerable function, fixed function) pairs whose function survives the fix, mined at zero model cost from reviewed GitHub advisories (pip, npm, maven, composer) and added as pointer-pair corpus benchmarks/pairs/advisory-fixes-3 (source `advisory-fixes-3` in learn/sources.toml); labelled, given static feature records, built into the memory store and embedded. No chat-model call, no Docker, no Joern. Candidate, path, function and repository names stay in the catalog; the pairs the reserved-population guard flagged are withheld (found by bisection with the test output discarded; names never printed).",
|
||||||
|
"method": {
|
||||||
|
"enumeration": "gh api /advisories?type=reviewed&ecosystem=<eco>&cwes=<family CWEs>&published=>2023-01-01 per ecosystem (pip, npm, maven, composer) x six compiled families; Go, Ruby and C# not enumerated; newest first",
|
||||||
|
"advisories_enumerated": 8531,
|
||||||
|
"advisories_published_after_2026_06_01": 1855,
|
||||||
|
"automated_screen": "as batch 2 (single family by CWE; linked fix commit, at most 3, one repository; repository not named anywhere under benchmarks/ incl. batches 1-2; excluded v3 file names never opened; no WordPress plugin; one pair per repository; SPDX licence permissive; one-parent commit; the function holding most changed code lines inside its own matcher span, declared once on both sides, body changed, >= 25% of the file's changed code lines) plus three widenings: (a) an advisory linking only a merged pull request uses its single commit or its squash/rebase merge commit; (b) commits of up to 6 non-test source files / 240 changed lines (batch 2: 4 / 120); (c) a second pass over advisories whose CWEs span two families, the family then chosen from the function's diff on review",
|
||||||
|
"file_reads": "commit metadata and the two versions of each changed source file through the contents API into scratch (no clone, no scanner, engine, plane Run or model on these repositories); scratch deleted after",
|
||||||
|
"manual_review": "every automated candidate's in-function diff read (agent, no model call); kept only when the named function itself holds the fix; a weak inline check replaced by a stronger helper call counts as the fix, a function that only forwards a flag or calls a fix made elsewhere does not",
|
||||||
|
"reserved_population_guard": "pytest -q tests/ -k 'reserved or v3' with output discarded, bisected over catalog subsets (18 runs; baseline with an empty batch green); final catalog green",
|
||||||
|
"rename_check": "canonical full_name of every kept repository from the API: no two kept pairs share a repository, none renamed into a name the benchmarks use",
|
||||||
|
"mechanism": "family default as batch 2 (injection/output_encoding/deserialization source_reaches_sink, path path_join_user_input, access_control missing_auth_guard, untrusted_destination validation_strength for CWE-601 else source_reaches_sink); provenance = agent"
|
||||||
|
},
|
||||||
|
"screened": {
|
||||||
|
"advisories_examined": 8805,
|
||||||
|
"outcomes": {
|
||||||
|
"no fix commit linked": 3480,
|
||||||
|
"repository already in benchmarks/ (populations v1/v2 or a pair corpus)": 1993,
|
||||||
|
"licence not permissive or unrecorded": 1249,
|
||||||
|
"repository already kept (one pair per repository)": 657,
|
||||||
|
"kept": 398,
|
||||||
|
"CWEs span several families": 274,
|
||||||
|
"no function declared on both sides with a changed body": 191,
|
||||||
|
"large commit (refactor or release, not the fix)": 173,
|
||||||
|
"fix spread over several commits or repositories": 167,
|
||||||
|
"merge or root commit": 115,
|
||||||
|
"no source file changed": 42,
|
||||||
|
"language outside the feature vocabulary (go/ruby/c#/...)": 32,
|
||||||
|
"manual review: the chosen function does not hold the fix (comment-only, misattributed, partial or family mismatch)": 19,
|
||||||
|
"repository unavailable": 9,
|
||||||
|
"WordPress plugin (population v3 safety)": 4,
|
||||||
|
"fix commit unavailable": 2
|
||||||
|
},
|
||||||
|
"outcomes_note": "single-family pass (screen 2, every advisory examined, no quota met) plus the two-family pass; `kept` = automated candidates; `manual review: the chosen function does not hold the fix (comment-only, misattributed, partial or family mismatch)` = the screen re-meeting a function the 2026-10-03 review had dropped",
|
||||||
|
"candidates_manually_reviewed": 412,
|
||||||
|
"kept_after_review": 255,
|
||||||
|
"review_drops": {
|
||||||
|
"total": 157,
|
||||||
|
"change in the function unrelated to the advisory (refactor, rename, formatting, another advisory)": 33,
|
||||||
|
"function misattributed (matcher span overrun or an undetected enclosing declaration)": 33,
|
||||||
|
"partial: the function only calls or configures the real fix": 29,
|
||||||
|
"function diff not small (rewrite or refactor around the fix)": 28,
|
||||||
|
"unsure": 17,
|
||||||
|
"family mismatch": 11,
|
||||||
|
"same fix as a kept pair in a fork or sibling repository (would leak across repository folds)": 3,
|
||||||
|
"WordPress plugin (population v3 safety; missed by the automated screen)": 2,
|
||||||
|
"reserved-population guard (flagged by the 2026-10-03 guard run; name not printed)": 1
|
||||||
|
},
|
||||||
|
"one_pair_per_repository_drops": 3,
|
||||||
|
"reserved_population_drops": 2,
|
||||||
|
"kept": 251
|
||||||
|
},
|
||||||
|
"kept": {
|
||||||
|
"total": 251,
|
||||||
|
"by_family": {
|
||||||
|
"access_control": 20,
|
||||||
|
"deserialization": 9,
|
||||||
|
"injection": 56,
|
||||||
|
"output_encoding": 69,
|
||||||
|
"path": 71,
|
||||||
|
"untrusted_destination": 26
|
||||||
|
},
|
||||||
|
"post_cutoff_by_family": {
|
||||||
|
"injection": 5,
|
||||||
|
"output_encoding": 5,
|
||||||
|
"path": 2
|
||||||
|
},
|
||||||
|
"post_cutoff_total": 12,
|
||||||
|
"by_language": {
|
||||||
|
"java": 37,
|
||||||
|
"javascript": 37,
|
||||||
|
"php": 46,
|
||||||
|
"python": 83,
|
||||||
|
"typescript": 48
|
||||||
|
},
|
||||||
|
"post_cutoff_by_language": {
|
||||||
|
"python": 6,
|
||||||
|
"typescript": 6
|
||||||
|
},
|
||||||
|
"by_family_language": {
|
||||||
|
"access_control:java": 5,
|
||||||
|
"access_control:javascript": 3,
|
||||||
|
"access_control:php": 7,
|
||||||
|
"access_control:python": 2,
|
||||||
|
"access_control:typescript": 3,
|
||||||
|
"deserialization:java": 2,
|
||||||
|
"deserialization:php": 1,
|
||||||
|
"deserialization:python": 6,
|
||||||
|
"injection:java": 5,
|
||||||
|
"injection:javascript": 7,
|
||||||
|
"injection:php": 9,
|
||||||
|
"injection:python": 24,
|
||||||
|
"injection:typescript": 11,
|
||||||
|
"output_encoding:java": 7,
|
||||||
|
"output_encoding:javascript": 20,
|
||||||
|
"output_encoding:php": 12,
|
||||||
|
"output_encoding:python": 14,
|
||||||
|
"output_encoding:typescript": 16,
|
||||||
|
"path:java": 10,
|
||||||
|
"path:javascript": 4,
|
||||||
|
"path:php": 10,
|
||||||
|
"path:python": 34,
|
||||||
|
"path:typescript": 13,
|
||||||
|
"untrusted_destination:java": 8,
|
||||||
|
"untrusted_destination:javascript": 3,
|
||||||
|
"untrusted_destination:php": 7,
|
||||||
|
"untrusted_destination:python": 3,
|
||||||
|
"untrusted_destination:typescript": 5
|
||||||
|
},
|
||||||
|
"by_ecosystem": {
|
||||||
|
"composer": 46,
|
||||||
|
"maven": 37,
|
||||||
|
"npm": 81,
|
||||||
|
"pip": 87
|
||||||
|
},
|
||||||
|
"licences": {
|
||||||
|
"Apache-2.0": 80,
|
||||||
|
"BSD-2-Clause": 3,
|
||||||
|
"BSD-3-Clause": 18,
|
||||||
|
"ISC": 1,
|
||||||
|
"MIT": 147,
|
||||||
|
"MPL-2.0": 1,
|
||||||
|
"Unlicense": 1
|
||||||
|
},
|
||||||
|
"split": {
|
||||||
|
"holdout": 124,
|
||||||
|
"train": 127
|
||||||
|
},
|
||||||
|
"published_by_year": {
|
||||||
|
"2023": 50,
|
||||||
|
"2024": 52,
|
||||||
|
"2025": 66,
|
||||||
|
"2026": 83
|
||||||
|
},
|
||||||
|
"published_range": [
|
||||||
|
"2023-01-04",
|
||||||
|
"2026-10-01"
|
||||||
|
],
|
||||||
|
"via_pull_request": 13,
|
||||||
|
"commit_over_120_source_lines": 10,
|
||||||
|
"two_family_advisory_family_chosen_on_review": 3,
|
||||||
|
"verification": {
|
||||||
|
"verified": 251,
|
||||||
|
"excerpt_bytes_read": 1297516
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"asked": "injection and untrusted_destination >= 60 each; path, access_control, output_encoding, deserialization >= 30 where advisories allow; >= 250 total",
|
||||||
|
"met": {
|
||||||
|
"access_control": false,
|
||||||
|
"deserialization": false,
|
||||||
|
"injection": false,
|
||||||
|
"output_encoding": true,
|
||||||
|
"path": true,
|
||||||
|
"untrusted_destination": false
|
||||||
|
},
|
||||||
|
"limit": "the reviewed-advisory pool, not the review: every advisory since 2023-01-01 in the four ecosystems was examined; the post-cutoff pool was largely spent by batch 2 (its repositories are now excluded), and untrusted_destination/access_control/deserialization advisories mostly lack a linked single fix commit or sit in repositories the benchmarks already use"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"command": "ousast learn labels --root . --out <scratch> --snapshot <scratch>",
|
||||||
|
"rows": [
|
||||||
|
6981,
|
||||||
|
7985
|
||||||
|
],
|
||||||
|
"pairs_rows": [
|
||||||
|
1936,
|
||||||
|
2940
|
||||||
|
],
|
||||||
|
"new_pair_rows": 1004
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"command": "benchmarks/learn/build_features.py --labels <the 1004 new rows> --units <merged units.json of `ousast plane harvest --catalog benchmarks/pairs/advisory-fixes-3/catalog.toml` (manifests written, no Run started)> --catalog benchmarks/pairs/advisory-fixes-3/catalog.toml --kinds pairs",
|
||||||
|
"records": {
|
||||||
|
"plane:access_control": 40,
|
||||||
|
"plane:deserialization": 18,
|
||||||
|
"plane:injection": 112,
|
||||||
|
"plane:output_encoding": 138,
|
||||||
|
"plane:path": 142,
|
||||||
|
"plane:untrusted_destination": 52,
|
||||||
|
"static:access_control": 40,
|
||||||
|
"static:deserialization": 18,
|
||||||
|
"static:injection": 112,
|
||||||
|
"static:output_encoding": 138,
|
||||||
|
"static:path": 142,
|
||||||
|
"static:untrusted_destination": 52
|
||||||
|
},
|
||||||
|
"instrument_states_plane_all_families": {
|
||||||
|
"agree:missing": 164,
|
||||||
|
"agree:none": 338,
|
||||||
|
"delta:not_applicable": 502,
|
||||||
|
"engine:missing": 406,
|
||||||
|
"engine:none": 96,
|
||||||
|
"entry_points:not_applicable": 502,
|
||||||
|
"facts:not_applicable": 502,
|
||||||
|
"language:ran": 502,
|
||||||
|
"model_sinks:missing": 502,
|
||||||
|
"quick:ran": 502,
|
||||||
|
"roles:ran": 502,
|
||||||
|
"source:ran": 502,
|
||||||
|
"verify:missing": 164,
|
||||||
|
"verify:none": 338
|
||||||
|
},
|
||||||
|
"note": "quick rules and deterministic roles ran on each side; engine, verify, agree and model sinks never ran for these pairs (missing/none: null features, never zeros)"
|
||||||
|
},
|
||||||
|
"memory_build": {
|
||||||
|
"command": "ousast learn memory build --labels <labels> --units <merged units.json> --profile static --root . (exit 2: adjudications 9 of 43 anonymous functions, unchanged since 2026-10-01; rows kept)",
|
||||||
|
"examples": [
|
||||||
|
1133,
|
||||||
|
1635
|
||||||
|
],
|
||||||
|
"pairs_examples": [
|
||||||
|
968,
|
||||||
|
1470
|
||||||
|
],
|
||||||
|
"pairs_counts_after": {
|
||||||
|
"absent_fixed_side": 0,
|
||||||
|
"examples": 1470,
|
||||||
|
"labels": 2940,
|
||||||
|
"no_excerpt": 0,
|
||||||
|
"no_record": 1470,
|
||||||
|
"recorded": 1470,
|
||||||
|
"unread": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"complete_pairs": {
|
||||||
|
"how": "store example rows (profile static) keyed as learn.experiments.freeze_units pairs them: (repo, candidate, family) holding both labels; whole memory, every family",
|
||||||
|
"before": 459,
|
||||||
|
"after": 710,
|
||||||
|
"by_family": {
|
||||||
|
"access_control": [
|
||||||
|
37,
|
||||||
|
57
|
||||||
|
],
|
||||||
|
"config_secrets": [
|
||||||
|
12,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"deserialization": [
|
||||||
|
32,
|
||||||
|
41
|
||||||
|
],
|
||||||
|
"injection": [
|
||||||
|
68,
|
||||||
|
124
|
||||||
|
],
|
||||||
|
"memory": [
|
||||||
|
149,
|
||||||
|
149
|
||||||
|
],
|
||||||
|
"output_encoding": [
|
||||||
|
77,
|
||||||
|
146
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
38,
|
||||||
|
109
|
||||||
|
],
|
||||||
|
"unknown": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"untrusted_destination": [
|
||||||
|
44,
|
||||||
|
70
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exp_004_005_units": "195 complete pairs inside exp-004/005's frozen 501 units (six families, evaluation folds); those units are frozen and were not regenerated here"
|
||||||
|
},
|
||||||
|
"positives_only_groups": {
|
||||||
|
"before": 99,
|
||||||
|
"after": 99,
|
||||||
|
"groups_both_by_family": {
|
||||||
|
"access_control": [
|
||||||
|
37,
|
||||||
|
57
|
||||||
|
],
|
||||||
|
"config_secrets": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"deserialization": [
|
||||||
|
32,
|
||||||
|
41
|
||||||
|
],
|
||||||
|
"injection": [
|
||||||
|
53,
|
||||||
|
109
|
||||||
|
],
|
||||||
|
"memory": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"output_encoding": [
|
||||||
|
38,
|
||||||
|
107
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
38,
|
||||||
|
109
|
||||||
|
],
|
||||||
|
"unknown": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"untrusted_destination": [
|
||||||
|
42,
|
||||||
|
68
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embeddings": {
|
||||||
|
"command": "ousast learn memory embed --budget-usd 0.20 (dry run first: 144,806 estimated tokens, $0.0029)",
|
||||||
|
"requested": 1479,
|
||||||
|
"cached": 1004,
|
||||||
|
"embedded": 475,
|
||||||
|
"calls": 15,
|
||||||
|
"tokens": 150339,
|
||||||
|
"usd": 0.003007
|
||||||
|
},
|
||||||
|
"spend": {
|
||||||
|
"chat_model_calls": 0,
|
||||||
|
"embedding_usd": 0.003007,
|
||||||
|
"github_api": "metadata and raw contents only"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"count": 1029,
|
||||||
|
"guard": "green",
|
||||||
|
"sha256": "5a644a1e93c68a0a35c794e653286d0532b7eb8d757123ccce5f08b11bf9a4a2",
|
||||||
|
"sources": {
|
||||||
|
"harvest": {
|
||||||
|
"bytes": 501031,
|
||||||
|
"count": 467,
|
||||||
|
"files": 3,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 446,
|
||||||
|
"skipped_values": 446
|
||||||
|
},
|
||||||
|
"measurements": {
|
||||||
|
"bytes": 21169903,
|
||||||
|
"count": 278,
|
||||||
|
"files": 221,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 376,
|
||||||
|
"skipped_values": 681
|
||||||
|
},
|
||||||
|
"memory_local": {
|
||||||
|
"bytes": 8084523,
|
||||||
|
"count": 192,
|
||||||
|
"files": 776,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 759,
|
||||||
|
"skipped_values": 2
|
||||||
|
},
|
||||||
|
"memory_s3": {
|
||||||
|
"bytes": 15928676,
|
||||||
|
"count": 572,
|
||||||
|
"files": 1499,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 1490,
|
||||||
|
"skipped_values": 2
|
||||||
|
},
|
||||||
|
"pairs": {
|
||||||
|
"bytes": 1680480,
|
||||||
|
"count": 728,
|
||||||
|
"files": 28,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 900,
|
||||||
|
"skipped_values": 1260
|
||||||
|
},
|
||||||
|
"pointers": {
|
||||||
|
"bytes": 892448,
|
||||||
|
"count": 537,
|
||||||
|
"files": 7,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 586,
|
||||||
|
"skipped_values": 666
|
||||||
|
},
|
||||||
|
"populations": {
|
||||||
|
"bytes": 32431,
|
||||||
|
"count": 28,
|
||||||
|
"files": 2,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 28,
|
||||||
|
"skipped_values": 0
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"bytes": 34006439,
|
||||||
|
"count": 1029,
|
||||||
|
"files": 1619,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 2155,
|
||||||
|
"skipped_values": 0
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"bytes": 319641,
|
||||||
|
"count": 229,
|
||||||
|
"files": 9,
|
||||||
|
"known_empty": false,
|
||||||
|
"rows": 642,
|
||||||
|
"skipped_values": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test_exit_code": 0,
|
||||||
|
"tests": {
|
||||||
|
"errors": 0,
|
||||||
|
"failures": 0,
|
||||||
|
"skipped": 1,
|
||||||
|
"tests": 41
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"cleanup": "completed-before-checkpoint",
|
||||||
|
"commit": "5dc86c2f3f54b4dfb59060c88fb3ab5b307ef448",
|
||||||
|
"differences": 0,
|
||||||
|
"image": "ghcr.io/norandom/ousast-engine-task@sha256:b73c9bbd48fbf39c14ac013e5b0ff0a9d536b3d6388b50b1bd4cddfa22264b91",
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"baseline_digest": "c12c675f816a24ab33a63336a21efdc93db10a37dcf83437d91bec6f728cef1f",
|
||||||
|
"equal": true,
|
||||||
|
"instrument": [
|
||||||
|
{
|
||||||
|
"bytes": 1761,
|
||||||
|
"cpg_bytes": 138723,
|
||||||
|
"jvm": [
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 11.88592994,
|
||||||
|
"success": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 35.821929360999995,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 7.4493522819999995,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 2.9803447330000026,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 0.5151375319999971,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 9.691500790999996,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pin_digest": "63c992b500590a9d4b643c9458df1d6cf71d770b06cc70594acbdba99b06c00b",
|
||||||
|
"record_digest": "c12c675f816a24ab33a63336a21efdc93db10a37dcf83437d91bec6f728cef1f",
|
||||||
|
"seconds": 158.85258329089265
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseline_digest": "5a60ed069d99f9325a1d3f3a7612fcf0d853d4fabfd53add81151b051132a5ee",
|
||||||
|
"equal": true,
|
||||||
|
"instrument": [
|
||||||
|
{
|
||||||
|
"bytes": 1791,
|
||||||
|
"cpg_bytes": 138886,
|
||||||
|
"jvm": [
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 7.656527941999999,
|
||||||
|
"success": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 27.532885242,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 9.524187851,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 5.0155780199999995,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 0.6809096819999993,
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exit_code": null,
|
||||||
|
"heap_mb": 1700,
|
||||||
|
"seconds": 13.824248304000001,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pin_digest": "8bd2f48dfb2d00cfc5b60a5dee3a8c87e6970671121caa17221f2449c1254fb2",
|
||||||
|
"record_digest": "5a60ed069d99f9325a1d3f3a7612fcf0d853d4fabfd53add81151b051132a5ee",
|
||||||
|
"seconds": 142.5083134449087
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"commit": "48ae918dec25119142ae781607ba99306ae5ec17",
|
||||||
|
"fixture_bytes": 15374,
|
||||||
|
"fixture_sha256": "dd3c774a47d553105ed85b9cea8810736c233f870e5a015559a1524d5db5a14d",
|
||||||
|
"implementation_sha256": {
|
||||||
|
"benchmarks/ax/batch.py": "37aecac93beb25c02826e81eacaf064fbebb447a998fe139f6d94a07035b8685",
|
||||||
|
"benchmarks/unseen/replay.py": "32efb3e0b9e9d247f2780b5f3ceeb01b4c2cbda3656c75a741f9925621cfdfbc",
|
||||||
|
"benchmarks/unseen/replay_entry.py": "994d8e239eb505dc2c5bad4cdb8b5d45967a8a2aa9ef48fb4c2f20ba45736280",
|
||||||
|
"benchmarks/unseen/score.py": "56e0a7b34482b5d84df39824b8ee447c283e35bd932f38cfe692cf84cb52cef7"
|
||||||
|
},
|
||||||
|
"network_proofs": {
|
||||||
|
"4.4": "coordinator_pending",
|
||||||
|
"5.4": "coordinator_pending"
|
||||||
|
},
|
||||||
|
"scope": "offline synthetic scorer only",
|
||||||
|
"score_command_exit": 0,
|
||||||
|
"scorer_output_sha256": "77f154eedb6a4acd098ab63a0b17434291f0d226dabe8ac66f2e0b362154d002",
|
||||||
|
"scorer_test_exit": 0,
|
||||||
|
"scorer_tests": 14,
|
||||||
|
"synthetic_records": 20,
|
||||||
|
"uncommitted": true,
|
||||||
|
"validation": {
|
||||||
|
"generic_dispatcher_tests": {
|
||||||
|
"exit_code": 0,
|
||||||
|
"passed": 5
|
||||||
|
},
|
||||||
|
"mypy": 0,
|
||||||
|
"proof_and_scorer_tests": {
|
||||||
|
"exit_code": 0,
|
||||||
|
"passed": 16
|
||||||
|
},
|
||||||
|
"requested_pytest": {
|
||||||
|
"exit_code": 0,
|
||||||
|
"passed": 279,
|
||||||
|
"skipped": 8
|
||||||
|
},
|
||||||
|
"reserved_repository_guard": 0,
|
||||||
|
"ruff_check": 0,
|
||||||
|
"ruff_format_check": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"biases": [
|
||||||
|
"known_vulnerable_location",
|
||||||
|
"labelled_family",
|
||||||
|
"runnable_project_preference"
|
||||||
|
],
|
||||||
|
"catalogs": {
|
||||||
|
"bytes": 904050,
|
||||||
|
"files": 10,
|
||||||
|
"rows": 873
|
||||||
|
},
|
||||||
|
"eligible_pairs": 192,
|
||||||
|
"eligible_repositories": 192,
|
||||||
|
"exclusions": {
|
||||||
|
"bytes": 0,
|
||||||
|
"count": 0,
|
||||||
|
"files": 0,
|
||||||
|
"ledger_rows": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"gap_to_target": 0,
|
||||||
|
"guard": {
|
||||||
|
"guard": "green",
|
||||||
|
"test_exit_code": 0,
|
||||||
|
"tests": {
|
||||||
|
"errors": 0,
|
||||||
|
"failures": 0,
|
||||||
|
"skipped": 1,
|
||||||
|
"tests": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language_floor_gaps": {
|
||||||
|
"java": 0,
|
||||||
|
"javascript": 0,
|
||||||
|
"php": 0,
|
||||||
|
"python": 0,
|
||||||
|
"typescript": 0
|
||||||
|
},
|
||||||
|
"local_metadata": {
|
||||||
|
"config_bytes": 0,
|
||||||
|
"manifest_bytes": 0,
|
||||||
|
"matched_checkouts": 0
|
||||||
|
},
|
||||||
|
"manifest_sha256": "b593d2760f525c38b205fb33866c383fead4666102a84a805150b32c55ceeb62",
|
||||||
|
"per_family": {
|
||||||
|
"injection": 5,
|
||||||
|
"output_encoding": 5,
|
||||||
|
"path": 5,
|
||||||
|
"untrusted_destination": 5
|
||||||
|
},
|
||||||
|
"per_language": {
|
||||||
|
"java": 4,
|
||||||
|
"javascript": 4,
|
||||||
|
"php": 4,
|
||||||
|
"python": 4,
|
||||||
|
"typescript": 4
|
||||||
|
},
|
||||||
|
"purpose": "feasibility_only",
|
||||||
|
"qualification": "not_an_unseen_evaluation_or_block_admission",
|
||||||
|
"rejected": {
|
||||||
|
"commits": 60,
|
||||||
|
"family": 62,
|
||||||
|
"language": 100,
|
||||||
|
"location": 1,
|
||||||
|
"not_development_fix": 453,
|
||||||
|
"repository": 5
|
||||||
|
},
|
||||||
|
"seed": 20261005,
|
||||||
|
"selected": 20,
|
||||||
|
"status": "selected",
|
||||||
|
"target": 20,
|
||||||
|
"unknown_manifest": 20,
|
||||||
|
"unknown_test_suite": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Search-with-proof: first `demonstrated` (ceiling proof), 2026-10-06
|
||||||
|
|
||||||
|
First end-to-end `demonstrated` verdict for the search-with-proof verifier. No model, no cluster, $0.
|
||||||
|
Run locally in-process via `openultrasast.search.verify.verify` (non-task bubblewrap lane, userns, node v18.19.1).
|
||||||
|
|
||||||
|
- Pair: glance (jarofghosts/glance), family **path**, index.js `Glance$serveRequest`.
|
||||||
|
- Verdict: **demonstrated** — owned effect in 3/3 affected runs, 0/3 safe runs (zero false proof on the fixed side).
|
||||||
|
- Vulnerable side: canary observed 3/3 (parent traversal reaches the out-of-root nonce file; app reflects it).
|
||||||
|
- Fixed side: 0/3 (the fix commit's parent-escape guard returns 403; the traversal cannot escape).
|
||||||
|
- Wall ~65 s for both sides x 3 runs; ready ~2 s/side.
|
||||||
|
|
||||||
|
## What this proves
|
||||||
|
The execution harness, the path oracle, and the differential verifier are correct and CAN produce a
|
||||||
|
`demonstrated` verdict on a real third-party app. The method's ceiling is reachable. Every prior pilot
|
||||||
|
`inconclusive` on this pair was an instrument artifact, not a method verdict.
|
||||||
|
|
||||||
|
## Two defects that had masked this (both isolated, neither is the method)
|
||||||
|
1. **Broken build.** `[email protected]` depends on an unpinned `mime >=1.2.6`; the checkout's regenerated
|
||||||
|
lockfileVersion-3 pins `[email protected]`, where `.lookup()` was removed. glance crashes serving ANY file
|
||||||
|
(`TypeError: mime.lookup is not a function`). Directory listing (readiness GET /) survives; the first
|
||||||
|
file serve kills the process, so the nonce never reaches the response. Fixed here by installing a
|
||||||
|
compatible `[email protected]` before the run.
|
||||||
|
2. **Wrong demo target.** The stored agent demo traversed to a filename that is not the canary, so it
|
||||||
|
404s even on the vulnerable side. The path oracle is the only owned oracle that hides its target from
|
||||||
|
the demo author. The corrected demo targets the conventional `canary` file one level above the served
|
||||||
|
root (same convention the known-good probe uses).
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
Demo schema + driver stored (gitignored) at `benchmarks/search/private/ceiling-proof-2026-10-06/`.
|
||||||
|
Prepared checkouts need their npm symlinks stripped (the verifier refuses links) and a compatible
|
||||||
|
`mime@1`. Payloads stay on disk per the classifier-safety note; never echoed.
|
||||||
|
|
||||||
|
## Follow-on for honest qualification at scale (not covered by this $0 proof)
|
||||||
|
- Keystone: positive-control in `verify.py` so a broken app yields `could_not_run`, never `inconclusive`.
|
||||||
|
- Build-repro: distrust generated npm locks; pin/override incompatible transitive deps; audit all 20 pairs.
|
||||||
|
- Demo-authoring: document the path-oracle target convention so the search AGENT can author a hitting demo.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"record": "2026-10-06 search-with-proof feasibility pilot (path family, vulnerable side, cluster verify lane)",
|
||||||
|
"image": "ghcr.io/norandom/ousast-search-task@sha256:60bfd73f...",
|
||||||
|
"note": "First pilot after the verify-side keystone and the agent-side path-oracle target-parity fix. Feasibility only; payloads/boards live in the gitignored private area.",
|
||||||
|
"ceiling_usd": 3.0,
|
||||||
|
"searches": [
|
||||||
|
{
|
||||||
|
"pair_index": 3,
|
||||||
|
"language": "php",
|
||||||
|
"family": "path",
|
||||||
|
"outcome": "inconclusive",
|
||||||
|
"end_reason": "budget_spent",
|
||||||
|
"end_detail": "demonstrations",
|
||||||
|
"model_calls": 78,
|
||||||
|
"executor_tasks": 12,
|
||||||
|
"verify_tasks": 6,
|
||||||
|
"settled_usd": 0.5779,
|
||||||
|
"wall_seconds": 439,
|
||||||
|
"failure_classes": [
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "no cli entrypoint exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify",
|
||||||
|
"class": "noconsistentdifferential"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify",
|
||||||
|
"class": "noconsistentdifferential"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify",
|
||||||
|
"class": "noconsistentdifferential"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pair_index": 9,
|
||||||
|
"language": "typescript",
|
||||||
|
"family": "path",
|
||||||
|
"outcome": "plausible_unproven",
|
||||||
|
"end_reason": "budget_spent",
|
||||||
|
"end_detail": "reason_rounds",
|
||||||
|
"model_calls": 154,
|
||||||
|
"executor_tasks": 12,
|
||||||
|
"verify_tasks": 0,
|
||||||
|
"settled_usd": 1.1979,
|
||||||
|
"wall_seconds": 2300,
|
||||||
|
"failure_classes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pair_index": 13,
|
||||||
|
"language": "python",
|
||||||
|
"family": "path",
|
||||||
|
"outcome": "plausible_unproven",
|
||||||
|
"end_reason": "budget_spent",
|
||||||
|
"end_detail": "run_ceiling",
|
||||||
|
"model_calls": 118,
|
||||||
|
"executor_tasks": 9,
|
||||||
|
"verify_tasks": 0,
|
||||||
|
"settled_usd": 1.1739,
|
||||||
|
"wall_seconds": 564,
|
||||||
|
"failure_classes": [
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "as predicted the parent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "passfalse canary workspacecheckoutcanary passfalse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "with the servedroot prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "h check at filepy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "are plain attribute reads"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aggregate": {
|
||||||
|
"outcomes": {
|
||||||
|
"inconclusive": 1,
|
||||||
|
"plausible_unproven": 2
|
||||||
|
},
|
||||||
|
"demonstrations": 0,
|
||||||
|
"false_proofs_fixed_side": 0,
|
||||||
|
"settled_usd": 2.9496,
|
||||||
|
"searches": 3
|
||||||
|
},
|
||||||
|
"finding": "Parity fix validated: the agent now runs directed, target-aware search toward the named observable (pair 13 reasoned explicitly about reaching it and inspected the app's file-serving guard). No demonstration: all three exhausted their per-pair allowance (demonstrations/reason_rounds/run_ceiling) before a differential; pairs 9 and 13 wrote 0 verify demos (budget consumed in reason/explore). Pair 3 (php) hit entrypoint-discovery failures (no CLI entrypoint / runtime entry missing). Cost ~$0.98/search, higher than the $0.2-0.5 estimate."
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-06",
|
||||||
|
"image": "ghcr.io/norandom/ousast-search-task@sha256:32c7d23a41936c841b91edf27ded2d1fef5df9d656171e3ae199e7ed51ba0264",
|
||||||
|
"image_tools": {
|
||||||
|
"bwrap": "bubblewrap 0.12.0",
|
||||||
|
"chromium": "Chromium 154.0.8037.92 built on Debian G",
|
||||||
|
"java": "openjdk version \"21.0.12.1\" 2026-08-18\nO",
|
||||||
|
"node": "v20.19.2",
|
||||||
|
"php": "PHP 8.4.26 (cli) (built: Sep 24 2026 17:",
|
||||||
|
"python": "Python 3.13.5",
|
||||||
|
"sqlite": "3.46.1 2024-08-13 09:16:08 c9c2ab54ba1f5",
|
||||||
|
"tsc": "Version 4.9.5"
|
||||||
|
},
|
||||||
|
"isolation_check": {
|
||||||
|
"reason": "task-boundary",
|
||||||
|
"status": "skipped"
|
||||||
|
},
|
||||||
|
"isolation_mode": "task-boundary",
|
||||||
|
"lane": "kube-ax (AX task, gVisor, task-boundary)",
|
||||||
|
"namespaces": {
|
||||||
|
"cgroup": 1,
|
||||||
|
"ipc": 1,
|
||||||
|
"mount": 1,
|
||||||
|
"net": 1,
|
||||||
|
"pid": 1,
|
||||||
|
"setpriv": 127,
|
||||||
|
"user": 1,
|
||||||
|
"uts": 1
|
||||||
|
},
|
||||||
|
"oracles": {
|
||||||
|
"command": {
|
||||||
|
"observer_access": "inconclusive",
|
||||||
|
"real": "demonstrated",
|
||||||
|
"revision_patch": "inconclusive"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"observer_access": "inconclusive",
|
||||||
|
"real": "demonstrated",
|
||||||
|
"revision_patch": "inconclusive"
|
||||||
|
},
|
||||||
|
"sql": {
|
||||||
|
"observer_access": "inconclusive",
|
||||||
|
"real": "demonstrated",
|
||||||
|
"revision_patch": "inconclusive"
|
||||||
|
},
|
||||||
|
"ssrf": {
|
||||||
|
"observer_access": "inconclusive",
|
||||||
|
"real": "demonstrated",
|
||||||
|
"revision_patch": "inconclusive"
|
||||||
|
},
|
||||||
|
"xss": {
|
||||||
|
"observer_access": "inconclusive",
|
||||||
|
"real": "demonstrated",
|
||||||
|
"revision_patch": "inconclusive"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peak_rss_kib": {
|
||||||
|
"children": 460152,
|
||||||
|
"self": 42172,
|
||||||
|
"sum": 502324
|
||||||
|
},
|
||||||
|
"scratch_disk_bytes": 23040,
|
||||||
|
"status": "ok",
|
||||||
|
"task": "search-with-proof 3.2",
|
||||||
|
"wall_seconds": 38.368433234
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"record": "2026-10-07 FIRST CLUSTER DEMONSTRATION for search-with-proof",
|
||||||
|
"pair": {
|
||||||
|
"index": 13,
|
||||||
|
"repo": "hanxi/xiaomusic",
|
||||||
|
"family": "path",
|
||||||
|
"class": "prefix-check-bypass (startswith without separator)",
|
||||||
|
"language": "python"
|
||||||
|
},
|
||||||
|
"image": "ghcr.io/norandom/ousast-search-task@sha256:e2c8b460...",
|
||||||
|
"side": "vulnerable",
|
||||||
|
"outcome": "demonstrated",
|
||||||
|
"verify_tasks": 18,
|
||||||
|
"model_calls": 55,
|
||||||
|
"settled_usd": 0.3686,
|
||||||
|
"wall_seconds": 2170,
|
||||||
|
"false_proofs_fixed_side": 0,
|
||||||
|
"demo_wires_served_root": true,
|
||||||
|
"finding": "The agent autonomously produced a demonstrated finding on a real CVE fix-pair end to end on the kube-ax cluster: it found the prefix-bypass sink, wired the app's served directory to ${served_root} (prompted by the new served-root control-signal hint after an earlier mis-wire), targeted the oracle's prefix-sibling canary, and the differential verifier confirmed the owned effect on the vulnerable revision and its absence on the fixed revision. Cost $0.37, 55 model calls.",
|
||||||
|
"fix_chain_that_enabled_it": [
|
||||||
|
"parity: agent targets the observable",
|
||||||
|
"verify-time-only: no explore reachability trap",
|
||||||
|
"fair-share budget: authors+verifies within budget",
|
||||||
|
"served-root guidance: wire the served dir to ${served_root}",
|
||||||
|
"oracle prefix-bypass coverage: sibling canary for startswith-without-separator vulns",
|
||||||
|
"control-signal feedback: served-root hint so the agent self-corrects wiring"
|
||||||
|
],
|
||||||
|
"both_sides": {
|
||||||
|
"vulnerable": {
|
||||||
|
"outcome": "demonstrated",
|
||||||
|
"settled_usd": 0.3686,
|
||||||
|
"verify_tasks": 18,
|
||||||
|
"model_calls": 55
|
||||||
|
},
|
||||||
|
"fixed_blind_negative": {
|
||||||
|
"outcome": "inconclusive",
|
||||||
|
"false_proof": false,
|
||||||
|
"settled_usd": 0.5632,
|
||||||
|
"verify_tasks": 18,
|
||||||
|
"model_calls": 85,
|
||||||
|
"note": "Agent searched the FIXED (safe) revision blind (no vuln knowledge); authored demos but the verifier found no effect -> no false proof (Req 6.3 negative)."
|
||||||
|
},
|
||||||
|
"pair_total_settled_usd": 1.224,
|
||||||
|
"verdict": "True positive on the vulnerable revision, zero false proof on the fixed revision."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"record": "2026-10-07 search-with-proof validation of the explore-time reachability-trap fix (path family, vulnerable, cluster)",
|
||||||
|
"compares_against": "benchmarks/measurements/2026-10-06-search-pilot-path",
|
||||||
|
"ceiling_usd": 2.0,
|
||||||
|
"searches": [
|
||||||
|
{
|
||||||
|
"pair_index": 9,
|
||||||
|
"language": "typescript",
|
||||||
|
"outcome": "inconclusive",
|
||||||
|
"end_reason": "budget_spent",
|
||||||
|
"end_detail": "reason_rounds",
|
||||||
|
"model_calls": 170,
|
||||||
|
"verify_tasks": 2,
|
||||||
|
"settled_usd": 1.6032,
|
||||||
|
"wall_seconds": 2281,
|
||||||
|
"failure_classes": [
|
||||||
|
{
|
||||||
|
"phase": "explore",
|
||||||
|
"class": "valueerror fact requires tool evidence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from checkout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify/could_not_build",
|
||||||
|
"class": "runtime entry missing from checkout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "verify",
|
||||||
|
"class": "no consistent differential"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pair_index": 13,
|
||||||
|
"language": "python",
|
||||||
|
"outcome": "plausible_unproven",
|
||||||
|
"end_reason": "budget_spent",
|
||||||
|
"end_detail": "run_ceiling",
|
||||||
|
"model_calls": 36,
|
||||||
|
"verify_tasks": 0,
|
||||||
|
"settled_usd": 0.3418,
|
||||||
|
"wall_seconds": 254,
|
||||||
|
"failure_classes": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aggregate": {
|
||||||
|
"outcomes": {
|
||||||
|
"inconclusive": 1,
|
||||||
|
"plausible_unproven": 1
|
||||||
|
},
|
||||||
|
"demonstrations": 0,
|
||||||
|
"false_proofs_fixed_side": 0,
|
||||||
|
"settled_usd": 1.945
|
||||||
|
},
|
||||||
|
"finding": "Trap fix validated behaviourally. TypeScript pair 9: now AUTHORS a demo (verify_tasks 0->2) where it wrote none before; the demo fails could_not_build ('runtime entry missing from checkout') -- an entrypoint/build issue, same class as the php pair. Python pair 13: no longer burns budget on the reachability dead-end (calls 118->36, $1.17->$0.34); its board shows efficient directed analysis that traced the full route-to-sink path and the app entrypoint, but it hit the per-run budget ceiling (run_ceiling) before authoring/verifying a demo. Still 0 demonstrations, 0 false proofs. Remaining blockers: (1) per-run budget/step allocation leaves no room to author+verify after analysis; (2) demo entrypoint/build (start.path) for compiled/served apps (ts, php)."
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"record": "2026-10-07 search-with-proof: Python pair re-validation of the per-search fair-share budget fix",
|
||||||
|
"ceiling_usd": 1.5,
|
||||||
|
"pair": {"index": 13, "language": "python", "family": "path"},
|
||||||
|
"outcome": "inconclusive",
|
||||||
|
"end_detail": "demonstrations",
|
||||||
|
"verify_tasks": 18,
|
||||||
|
"model_calls": 98,
|
||||||
|
"settled_usd": 0.7411,
|
||||||
|
"wall_seconds": 2722,
|
||||||
|
"false_proofs_fixed_side": 0,
|
||||||
|
"finding": "Budget fix VALIDATED: with a fair per-search share the Python agent completed the full loop and ran 18 verify tasks (0 before), authoring and verifying demos within budget. Still inconclusive ('no consistent differential'), zero false proofs. Structural root cause confirmed from the authored demo: it does NOT reference ${served_root} anywhere; the agent set the app's served-directory config (an app env var) to a value of its own rather than to the oracle placeholder ${served_root}, so the app served the wrong tree and the planted observable (which lives relative to ${served_root}) was never reachable. Next guidance lever: require the agent to point the application's served/root directory at ${served_root} (via arg, env, or config) for the path oracle, so the observable is in the served tree.",
|
||||||
|
"progression": "parity fix -> agent targets observable; verify-time-only fix -> agent stops the explore reachability trap; budget fair-share -> agent authors+verifies within budget; remaining -> wire served dir to ${served_root}."
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user