Commit Graph
100 Commits
Author SHA1 Message Date
Marius CiepluchandClaude Opus 5.5 45d02ecf20 feat(plane): honest coverage in alerts; PHP alerts from the engine on the host
Quick mode has rules only for c/cpp, groovy, java, javascript/typescript and
python, so every PHP case's "0 alerts" read as clean. The `alerts` task now
records per language of the files read whether quick mode covers it
(derived from the bundled ruleset, not a list): coverage quick | none, with
file counts per pin, and `uncovered`. `remember` turns that into `coverage`
memory rows (a new kind; the Run passes the alerts summary.json as input),
loop-measure reports coverage per language, and improve/memory.py's M1/M2
skip a repository+pin whose coverage for the rule's language is only
`none` (counted under `coverage_none` in the proposal's excluded).

`ousast plane alerts-engine <Run.yaml> --source <frozen export>`
(plane/engine_alerts.py) does the alerts task on the host for cases whose
files include a language the engine covers but quick mode does not (PHP):
exports both pins from the case cache, runs the quick scan and the
openultrasast:dev engine container (finding_dump.py, --network none,
--memory 3g, one at a time, per-pin deadline), writes engine findings in
the alerts.jsonl row shape (rule_id engine:<family>, source engine,
in_fix_range from case.json) with a summary (files read, questions,
completed, seconds, degradations per pin), and marks the task done via
reconciler.mark_done. No result, no files read, or questions == 0 fails
loudly and leaves the run untouched. Refuses a git checkout as source.

Runner image rebuilt and re-pinned (sha256:ebb40a9a...); validation-46
regenerated (digest only). Live, recorded in
benchmarks/measurements/2026-09-30-php-engine-alerts/: froxlor 366 files,
130/130 questions per pin, 6 alerts per pin, 0 in the fix range; wp-gopay
19 files, 101/101 questions, 0 alerts. The loop: 32 run rows (coverage 8,
php=engine), 0 proposals, gate no_proposals, $0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 13:44:32 +02:00
Marius CiepluchandClaude Opus 5.5 f4bd984fda docs(harnessx-removal): the plane replaces HarnessX in README, docs and specs (task 8)
README: "Configuring the HarnessX agentic plane" becomes "The agentic
plane (ax)" (ousast plane run|status|doctor|remember, per-task Model,
budget and egress, pointer to ops/ax/README.md, one tagged retirement
line); "The HarnessX self-improving cycle" becomes "The self-improving
cycle", and its MetaAgent paragraph is replaced by the memory proposer
(`ousast improve --memory`, provenance, train-on-test guard). The fusion
panel_model/decider_model lines and the [harnessx] spend caps are gone.
The README reference test now passes without its strict xfail.

docs/examples.md section 6 runs the plane; docs/threat-model.md describes
the per-task EgressPolicy, the usd/calls budgets and the start-request
credential path. RELEASE_NOTES.md gets an Unreleased entry: HarnessX
removed, the retired keys and their errors, the silent loss of the
second hunter for users who set [models] hunter, and the plane as the
agentic path.

Dated retirement notes (2026-09-30) head the requirements, design and
brief of harnessx-self-improving-rulesets, learning-harness,
three-stage-scan and openrouter-sast-harness, and follow the spec table
in steering/overview.md; history is unchanged. All tasks are checked, so
spec.json moves to implementation-complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 12:45:29 +02:00
Marius CiepluchandClaude Opus 5.5 800204de87 bench(harnessx-removal): deterministic outputs byte-identical after the removal (task 7)
Reran benchmarks/harnessx_removal_equality.py record from a git archive
export of b9afb66 (tasks 1-6 landed; same venv freeze hash as the
baseline, harnessx not importable, no API keys, empty config) and
compared it against 2026-09-30-harnessx-removal-baseline:

  equal=True differences=0

All 13 output files (pairs.json, benchmark, standard scan, improve
--dry-run) are raw-byte identical to the baseline. The suite map
(suite-map.json) accounts for 29 deleted ids, 2 renamed ids and 83
added ids; each deleted or renamed id cites the design's Tests table and
commit cbadc38 or b9afb66. Every surviving test id keeps its outcome
(1740 passed, 38 skipped incl. the README xfail).

Instrument note: a first record failed test_push_hook twice because the
recording shell's PATH lacked .venv/bin (the hook execs `ousast`); the
same two tests fail at b9afb66 without it and pass with it. The
committed record has .venv/bin on PATH.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 12:40:57 +02:00
Marius CiepluchandClaude Opus 5.5 b9afb66250 chore(harnessx-removal): delete HarnessX code and packaging (task 6)
Deletes harness_ext.py and hunter_harness.py with their HarnessX-only
tests (the judge path, verify_judge.py, went in task 5). The harnessx
extra, its mypy override and its uv.lock entries are removed (uv lock
drops only packages; no remaining version changes). The conftest
fixture becomes assert_cold_import; test_gate's two never-import checks
become one allow-list (stdlib, openultrasast, yaml) over the governance
planes' module-level imports, and the skipped live-extra tolerance test
is deleted. Audit rows for the two modules are removed, stage_processors
is reworded, and the module-audit manifest is regenerated (no orphan).

test_removed_plane_references implements Req 2.4 over src, tests and
pyproject.toml: a match must carry a `retired 2026-..` tag (the spec's
own slug is not counted). The README part is a strict xfail until task 8
rewrites the README's plane sections.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 12:18:42 +02:00
Marius CiepluchandClaude Opus 5.5 cbadc38d5e refactor(harnessx-removal): retire the HarnessX scan paths (task 5)
The standard scan now runs run_hunter_pool, verify_findings and the
deterministic fuse_findings directly; the HarnessX hunter orchestrator,
the LLM judge dispatcher (verify_judge.py, deleted with its test) and the
LLM fusion panels are gone, and so are their extra-absent degradations.
host_under_harnessx is dropped from stage_processors.

config: [models] verifier and [fusion] panel_model/decider_model raise
RetiredConfigError naming the plane replacement (the CLI prints it and
exits 2); a [harnessx] section loads with one warning and is otherwise
ignored. harness.json keeps "verifier": null. Tests that asserted the
fallback are renamed to the behaviour they assert; the module-audit
manifest drops verify_judge. Nothing outside the two modules deleted in
task 6 imports harness_ext or hunter_harness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 12:13:26 +02:00
Marius CiepluchandClaude Opus 5.5 b68fe27c74 feat(plane): the improvement loop as a plane Run (harnessx-removal task 4)
`ousast plane workspaces --validation-set ... --loop` appends the loop to a Run:
per case `alerts` (plane/tasks/alerts.py: the quick-mode scan on the vulnerable
and the fixed pin, a fixed-pin Workspace per case, alerts attributed to their
enclosing function by the repo-facts patterns, `in_fix_range` from the fix's
old-side and new-side ranges, failing on an unread tree) feeding `remember`,
then `memory-snapshot` -> `loop-measure` -> `loop-propose` -> `loop-improve`
(plane/tasks/loop.py). Every new task binds no Model and has budget
{usd: 0, calls: 0}. The snapshot is the store's rows after the train-on-test
guard, written on the host by memory.seed (a sandboxed run of it fails loudly);
propose runs M1/M2 against this repository at the commit under test; improve
runs one unchanged evolve.run_round and delivers gate.json, journal.json, the
sidecar, rule_policy.json only when accepted, and proposal_outcome rows that
the host ingests. Adoption stays a maintainer commit.

`remember` is now a Run entry per case (the image contains it), with the facts
row keyed by the image the facts were computed on (repo-facts records it in
its summary; unknown -> the row is omitted, never guessed). Alert rows carry
their own pin; improve/memory.py counts a repeated alert once.

Runner image rebuilt (+0.39 MB: only the new modules; the quick scan needs no
extra and the rulesets were already in the package), pushed, plane/ re-pinned
and regenerated (validation-46: 105 entries). Fixture test runs the whole loop
graph through the fake ax, whose new `exec` mode runs the real task modules.
Live: one loop Run over three validation-46 cases on the local ax, recorded in
benchmarks/measurements/2026-09-30-harnessx-removal-loop/ (all done, $0 new
spend, 0 proposals, gate verdict no_proposals).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 11:59:50 +02:00
Marius CiepluchandClaude Opus 5.5 d104166697 feat(improve): proposals from plane memory through the unchanged gate (harnessx-removal task 3)
improve/memory.py: rules M1 (enabled->shadow on repeated false alerts across
>=2 repositories, no agreed or site_match hit) and M2 (shadow->enabled on
>=2 agreed declared sites no enabled rule catches); M3 disputed families are
advisory (signals.json) and never reach the validator. The train-on-test
guard drops rows of --qualify-population, gated benchmark cases and holdout
pair repositories before any rule sees a row. run_round(proposals=...) adds
memory edits after the benchmark's through the unchanged validator and gate;
provenance (row ids, object keys and versions, counts, exclusions, index
digest) goes to memory_proposals.jsonl and the journal's evidence field;
proposal_outcome rows are written back after a non-dry run.
ousast improve --memory is opt-in; default improve output is byte-identical
to the task-1 baseline.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 11:25:52 +02:00
Marius CiepluchandClaude Opus 5.5 52d28b5121 feat(plane): memory store (file and MinIO) and the remember task (harnessx-removal task 2)
plane/memory.py: the MemoryStore interface with FileStore (index.jsonl, facts by
content hash, repos/<host>__<owner>__<name>/<pin>.jsonl, idempotent ingest,
deterministic row ids, refusal below 1 GiB free naming the path) and MinioStore
(object metadata and tags, S3 Select pushdown probed once per process with a
logged local fallback, object version ids on every row read, versioning and
runs/ lifecycle helper, presigned URLs). OUSAST_MEMORY selects the backend;
MINIO_* settings come from .env; the minio SDK is the optional extra `minio`.

plane/tasks/remember.py: model-free rows (facts, verdict, unit_cost, alert)
from a case's artifacts; remember_run computes them on the host for a recorded
run without writing into it. reconciler.mark_done (477 lines total), seed
before and ingest after `ousast plane run`, `ousast plane remember <run>`.
The generator annotates each repo-facts Task with openultrasast.io/memory-key
(repo, pin, candidates digest, image digest) and the Run with its population
and split; plane/ regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 11:10:28 +02:00
Marius CiepluchandClaude Opus 5.5 d64ab2817e merge(harnessx-removal): deterministic baseline without the extra (task 1)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:43:27 +02:00
Marius CiepluchandClaude Opus 5.5 ca5caf735f bench(harnessx-removal): deterministic baseline without the extra (task 1)
Equality script (design section 3) records suite outcomes, `ousast pairs --json`,
the python-vulnerable quick benchmark, one standard scan and `improve --dry-run`
from a frozen export with the harnessx extra absent and no model reachable.
Two independent records are byte-identical (determinism.json).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:42:57 +02:00
Marius CiepluchandClaude Opus 5.5 b3561b5a20 spec(harnessx-removal): memory store on MinIO -- metadata, S3 Select JSON queries, versioned provenance
Maintainer: use MinIO features including its JSON support; a server exists.
One MemoryStore interface with FileStore (laptop, tests) and MinioStore (the
store of record for the separate Kubernetes deployment): tags for indexing,
S3 Select pushed-down queries probed with a local fallback, versioning for
proposal provenance, lifecycle for raw outputs, presigned URLs and bucket
notifications kept for the follow-on that removes the host receiver.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:37:14 +02:00
Marius CiepluchandClaude Opus 5.5 e19687a379 ops(ax): list the host-specific assumptions to change for a separate Kubernetes cluster
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:25:56 +02:00
Marius CiepluchandClaude Opus 5.5 2ed6d9a036 spec(harnessx-removal): tasks approved; implementation phase
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:23:51 +02:00
Marius CiepluchandClaude Opus 5.5 5240a02de5 spec(harnessx-removal): implementation plan for review; design approved
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:23:20 +02:00
Marius CiepluchandClaude Opus 5.5 647e84cdba spec(harnessx-removal): design for review
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:22:39 +02:00
Marius CiepluchandClaude Opus 5.5 f0bc372769 spec(harnessx-removal): requirements approved
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:13:10 +02:00
Marius CiepluchandClaude Opus 5.5 6eee8e6b1d spec(harnessx-removal): remove completely; the memory layer carries evolution and the loop
Maintainer amendment: no HarnessX path stays, optional or not; the plane memory
layer replaces the HarnessX evolution proposer and carries loop engineering
(Requirement 6: persistent run memory, proposals from it through the existing
validator and gate, the loop as a plane Run, provenance per proposal).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:12:55 +02:00
Marius CiepluchandClaude Opus 5.5 095eeee12e spec(harnessx-removal): brief lists the comment-only mentions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:10:41 +02:00
Marius CiepluchandClaude Opus 5.5 9391a69f25 spec(harnessx-removal): requirements for review
The ai-service-plane increment met its gate on ax (plane-increment-2.json), the
condition for removing HarnessX. Requirements: an explicit fate per capability
(moved to a plane task or retired), code/packaging/tests removed, deterministic
outputs proven byte-identical against a recorded baseline, docs and specs
retold, one reviewable sequence. Brief inventory re-verified.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 10:10:33 +02:00
Marius CiepluchandClaude Opus 5.5 df1dfa3b7a docs(plane): handover -- ops README points at the ax runbook; replaced scripts marked reference-only (task 9)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 08:39:24 +02:00
Marius CiepluchandClaude Opus 5.5 67699e8737 bench(plane): increment gate met after the tie-break -- 16/20 agreed at $0.0205/candidate
A third verify pass on the 10 disputed candidates, 2-of-3 agreement (Req 6.4),
run on ax by extending the same Run: 16/20 declared sites agreed (reference 16),
$0.0205 per candidate with recorded triage (reference $0.022). Caveats recorded:
a design change on the same set; 17 vs 19 sites flagged in any pass; three
non-declared pgadmin candidates promoted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 08:35:41 +02:00
Marius CiepluchandClaude Opus 5.5 34d11c7af0 chore(plane): re-pin task manifests to the rebuilt runner image
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 08:26:17 +02:00
Marius CiepluchandClaude Opus 5.5 b797605eda merge(plane): tie-break pass on disputed candidates; 2-of-3 agreement
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 08:20:57 +02:00
Marius CiepluchandClaude Opus 5.5 2f1b852966 bench(plane): correct the increment reading -- 17/20 flagged in either pass, not 19
agree's declared_sites_matched counts candidate coverage, not detection. Recounted
from agreed.json rows, plane passes flagged 17 of 20 declared sites against the
reference's 19 (budibase read and fuxa getDaqValue missed by both passes). The
earlier "detection unchanged" reading was wrong.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 08:20:50 +02:00
Marius CiepluchandClaude Opus 5.5 6e6bcfc40c feat(plane): tie-break pass on disputed candidates; 2-of-3 agreement
The design revisit after plane-increment-1.json (14/20 agreed, 19/20 in either
pass): a third verify pass on the candidates passes a and b dispute, and agree
decides 2-of-3 where pass c asked (the a/b rule elsewhere; c alone never decides).

- verify: optional OUSAST_INPUT_ONLY restricts the hunt to the named candidates
  (agree's disputed.json); an empty list is done with 0 units and no model client.
- agree: optional OUSAST_INPUT_PASS_C; metrics keep every key and add tiebreak
  {asked, flipped_to_agreed, flipped_to_rejected, rule}; usd_total, hunts and
  declared-site metrics are on the final decision.
- generate: per case <case>-vc and <case>-final appended to validation-46; the
  first 60 entries stay byte-identical, so a rerun keeps their done state.
- Offline replay over the recorded outputs with an empty pass c reproduces the
  recorded agree summaries exactly (14/20 agreed, 23 agreed, 10 disputed).
- Spec: Requirement 6.4, design Measurement, task 8.1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-30 08:19:46 +02:00
Marius CiepluchandClaude Opus 5.5 c5e6ed3221 bench(plane): first increment measured on ax -- gate not met (14/20 agreed vs 16)
validation-46 ran done on google/ax: 60 tasks, 357 DeepSeek calls, $0.83 with
recorded triage ($0.0194 per candidate, reference $0.022: cost met). Declared
sites agreed 14/20 against the reference's 16/20: recall criterion not met.
Both runs find 19/20 sites in at least one pass; the two-pass agreement
flicker moved three sites. Design revisit before task 9.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 22:19:38 +02:00
Marius CiepluchandClaude Opus 5.5 c97e216d45 fix(plane): ax task names leave room for the template suffix
ax names a task's ActorTemplate <task>-tmpl-<8 hex> and Substrate caps names at
63 bytes; validation-46's first task (51 characters) fell back to a missing
default template on the live cluster. Task names are capped at 49 with a
stable hash suffix when longer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 21:51:51 +02:00
Marius CiepluchandClaude Opus 5.5 46f0492eed chore(plane): re-pin task manifests to the rebuilt runner image
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 21:40:39 +02:00
Marius CiepluchandClaude Opus 5.5 89d000c704 fix(plane): fetch task-produced inputs from the receiver after the start
A producer's artifact rendered as spec.files of a generated <task>-inputs
Workspace ends up in ax's AX_WORKSPACES_YAML env value; Agent Substrate
rejects values over 32768 characters, so a 182 KB facts.json broke ax apply
on the live cluster. The reconciler now passes OUSAST_INPUTS instead, the
receiver serves a running task its declared inputs over GET /inputs/..., and
the runner fetches them after the start (failed fetch -> failed summary).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 21:35:28 +02:00
Marius CiepluchandClaude Opus 5.5 554f00e2f7 chore(plane): re-pin task manifests to the rebuilt runner image
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 21:23:50 +02:00
Marius CiepluchandClaude Opus 5.5 3d0d83c585 fix(plane): prepare workspaces only after the start request
On the live cluster the case clone ran at boot: the golden boot had no egress
policy, and the real actor was still being restored, so both TLS handshakes to
GitHub were cut. After the start the actor is RUNNING under its policy (the
delivery the same minute went through). Adds the one-case check Run (a subset
of validation-46) and re-pins the task manifests to the current runner digest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 21:23:25 +02:00
Marius CiepluchandClaude Opus 5.5 344df61fc1 ops(ax): README with measured footprint and live lessons; tasks 0-7 done
The end-to-end smoke Run finished done on the local ax: submit, egress policy,
resume, router start, repo-facts in gVisor, artifacts delivered through the
egress gateway, attribution table printed, task and policy cleaned up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 20:56:14 +02:00
Marius CiepluchandClaude Opus 5.5 137c6c8ed5 feat(plane): per-task egress policy; receiver by name through the gateway
Agent Substrate's egress gateway denies an actor without an EgressPolicy, so
no task could deliver its artifacts or reach a model. egress.py grants each
task exactly: http/80 to ousast-receiver.ax-system.svc.cluster.local, and
TLS passthrough/443 to the bound Workspaces' https Git hosts and the bound
Model's openultrasast.io/egress-hosts (api.deepseek.com for deepseek-flash).
No IPs, no wildcards, no https (MITM) rules.

The reconciler writes the policy with kubectl-ate between ax apply and
resume, reads it back, waits OUSAST_EGRESS_SETTLE (11 s, the gateway's
cached deny), and after ax delete task confirms the policy went with its
actor (kubectl-ate has no delete verb; the store cascades).

The receiver is a selector-less Service (ops/ax/receiver-service.yaml.tmpl,
applied by up.sh together with the no-build agentgateway egress gateway)
on port 18090 of the host. Measured live: the gateway checks the policy on
Host but connects to the address the actor dialled, so the runner dials the
Service's ClusterIP (OUSAST_ARTIFACT_DIAL) with the receiver's name as Host.
The runner now labels its delivery with the Run's task name (OUSAST_TASK),
which is what the receiver accepts; the ax Task name was refused with 403.
The Receiver moved to egress.py to keep reconciler.py at 460 lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 20:51:21 +02:00
Marius CiepluchandClaude Opus 5.5 0744a157bf merge(plane): scoring parity with the reference -- sites, ranges, triage cost
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 20:30:45 +02:00
Marius CiepluchandClaude Opus 5.5 30e4778b11 fix(plane): apply a Task after the Workspaces it binds; end-to-end smoke Run
On the live cluster ax created the Task before its Workspace in the same apply
and the runner saw an empty AX_WORKSPACES_YAML. Tests now find rendered
documents by kind and name, not position. ops/ax/smoke-run.sh renders a
model-free repo-facts Run with the current runner digest and executes it
through the reconciler.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 20:26:30 +02:00
Marius CiepluchandClaude Opus 5.5 56b1717246 feat(plane): scoring parity with the reference -- sites, ranges, triage cost
agree matched declared sites through benchmarks/independent/evaluate.py,
which the runner image does not carry, so site matching would have been
"not assessed". The generator now resolves the fix's old-side ranges
(evaluate.hunks, line for line) into case.json, with the declared sites in
the set and the reference's cost, and agree.matches_declared reproduces
matches_v2; a parity test runs both over the recorded outputs of the 15
validation cases (200 probes, identical verdicts) and agree on those outputs
returns the reference's 16 of 20 and $0.022.

agree reports cost_per_candidate (plane spend) and
cost_per_candidate_with_recorded_triage; Requirement 6.2 is judged on the
latter (design.md). --runner-image re-pins the templates from ops/ax/up.sh's
pin file; the committed manifests are regenerated at the current digest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 19:49:28 +02:00
Marius CiepluchandClaude Opus 5.5 14ea48b1ea feat(plane): router-addressed start signal; credentials only in the start request
Agent Substrate's golden-snapshot boot runs the same image with the same task env,
so the runner no longer starts its command by itself: after the workspaces are
ready it waits for POST /ousast/v1/start (run/task checked, once per boot, refused
after a delivered run). The reconciler sends it through svc/atenet-router with
ate-target-actor, which never addresses the golden actor (plane/router.py), and the
bound Model's secretKey variable travels only in that body, into the child's env.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 19:41:35 +02:00
Marius CiepluchandClaude Opus 5.5 ac296ee3a4 spec(ai-service-plane): start signal through the router; credentials only in the start request
The live cluster ran the task command twice: in the actor and in Substrate's
golden-snapshot boot, which gets the same image and task env. Requirement 4.5
gates the command on a router-addressed start request; 4.6 carries provider
credentials only in that request.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 19:19:11 +02:00
Marius CiepluchandClaude Opus 5.5 2e7e0550b2 fix(plane): runner drops the fields ax's server sets before validating
On the live cluster ax hands AX_TASK_YAML with metadata.creationTimestamp and a
status block; the strict authoring schema rejected it, readiness never turned
green and every resume timed out. Other unknown fields still fail. up.sh retries
the first resume while the golden snapshot builds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 19:17:19 +02:00
Marius CiepluchandClaude Opus 5.5 8e6485e577 merge(plane): ax task lifecycle -- resume, stay-up runner, delivery as completion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 19:07:52 +02:00
Marius CiepluchandClaude Opus 5.5 bb5c534fb2 fix(plane): ax task lifecycle -- resume, stay-up runner, delivery as completion
The first live run on the local ax cluster and ax's source (docs/runner.md,
docs/sandbox.md, internal/controller/reconciler.go, internal/server/server.go)
showed a lifecycle the task-3/4 code did not model:

- `ax apply` creates a Task Suspended (Ready=False, TaskSuspended); it runs only
  after `ax resume task`. The first resume of a new image can fail with
  DeadlineExceeded while Agent Substrate builds the golden snapshot, and ax's
  server then stores phase Failed; a later resume succeeds.
- ax has no Completed phase and never reads a command's exit back.
- The runner is PID 1 and must keep serving after the command exits; stop and
  suspend deliver SIGTERM, to be forwarded to the command's process group.
  Our runner imported the task in-process and exited, which stopped the
  container ("Container guest is stopped") and made every resume time out.
- ax's controller probes /readyz?check=workspace; the query string made our
  handler answer 404.

Runner (Req 4.1-4.4): spec.command runs as a child
`python -m openultrasast.plane.tasks.<name>` in its own process group, cwd the
first workspace, AX_METADATA_URL set; ax's metadata paths are served. After the
child exits the runner reads summary.json (a crash without one becomes failed
with the exit code and stderr tail), delivers the output directory, writes
/workspace/.ousast-state/<task>.done.json and serves until SIGTERM (forwarded
to the group, SIGKILL after OUSAST_TERM_GRACE, exit 0). A boot with the marker
never reruns the command; a failed delivery is retried alone. Workspaces are
prepared on the first boot only. AX_RUNNER_EXIT_AFTER_COMMAND=1 is the
unit-test escape.

Reconciler (Req 2.2, 2.3, 3.1-3.3): `ax resume task` after apply, retrying
DeadlineExceeded/Unavailable with backoff up to OUSAST_RESUME_TIMEOUT (900 s);
completion is the artifact delivery; Failed after a resume went through, a
vanished task, or no delivery within OUSAST_TASK_TIMEOUT (7200 s) fail the
task with ax's condition message; `ax delete task` follows. Ax gains resume()
and phase() returns the condition message. doctor moves to plane/doctor.py
(tasks.md task 0 allows the sibling) to keep reconciler.py under 500 lines.

Tests: the fake ax follows the real lifecycle (Suspended, a failing first
resume that leaves Failed, Running forever, delivery only after a resume went
through), plus Failed-with-message, vanish, resume-timeout and task-timeout
scenarios; runner tests cover the child's cwd/group/metadata URL, stay-up and
SIGTERM in a real subprocess, SIGKILL of a SIGTERM-ignoring group, and the
marker. The opt-in live test now checks resume, Running, suspend and a second
resume instead of a Completed phase. design.md and tasks.md state the
lifecycle; the module audit manifest counts plane.doctor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 19:04:52 +02:00
Marius CiepluchandClaude Opus 5.5 56755f4fd6 merge(plane): increment manifests for the 46-candidate validation run (task 7)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:58:32 +02:00
Marius CiepluchandClaude Opus 5.5 a35397c0ec feat(plane): increment manifests for the 46-candidate validation run (task 7)
Model deepseek-flash (declared provider extension, endpoint prices in
parameters, secretKey naming the DEEPSEEK_API_KEY secret), digest-pinned
Task templates repo-facts / verify / agree, and a generator
(plane/generate.py, `ousast plane workspaces --validation-set ...`) that
emits per case the vulnerable-pin Workspace, a files-only inputs Workspace
(recorded candidates after the recorded triage, declared sites, triage),
per-case Tasks and one Run validation-46: facts -> verify a, verify b ->
agree for the 15 cases, 46 candidates (43 kept), budgets 1.5x the recorded
per-pass share. A Run entry cannot add a binding or env, and Run.inputs
names only task artifacts, so the case is fixed by per-case Tasks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:55:38 +02:00
Marius CiepluchandClaude Opus 5.5 1a69e50a68 merge(plane): ax field names verbatim; pins and bindings as stripped annotations
Live test made opt-in (OUSAST_AX_LIVE=1) against the digest-pinned manifest
up.sh renders, so the host gate no longer depends on cluster state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:40:19 +02:00
Marius CiepluchandClaude Opus 5.5 d579b724b3 ops(ax): worker pool, digest-pinned runner image, CLI from the deployed checkout
Substrate installs no WorkerPool (resume failed: no free workers) and rejects
images not pinned by digest; ax's default runner image on gcr.io needs
credentials, so the smoke task runs our runner (repo-facts over a one-file
workspace). A release ax CLI skewed from the server (UpdateTask unimplemented).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:37:25 +02:00
Marius CiepluchandClaude Opus 5.5 18b67cf942 fix(plane): ax field names verbatim; pins and bindings as stripped annotations
Requirement 1.1 (Task/Workspace/Model use only fields ax documents) and
Requirement 1.4 (no ax field repurposed) were violated: manifests accepted
metadata.namespace and a Workspace git `commit`, and the reconciler dumped
every git field into what `ax apply` receives.

Facts from ax's source (pkg/apis/v1alpha1/ax.proto, internal/workspace/setup.go):
- ObjectMeta is `name` and `atespace` (plus server-set creation_timestamp);
  no `namespace`, no `annotations`.
- GitRepo is `name`, `repo`, `branch`, `dir`, `depth`; no `commit`.
- The API server decodes with protojson.UnmarshalOptions{}: an unknown field
  is an error at `ax apply` (confirmed on the live cluster: a rendered
  Workspace+Model applied cleanly; the same Workspace with `commit` was
  refused with `unknown field "commit"`).
- ValidateModel checks only metadata; any spec.provider string is accepted.

Changes:
- manifests: metadata takes `name`, `atespace`, `annotations` (our extension,
  never sent to ax); git entries take exactly ax's five fields (depth int);
  `commit` in spec.git is rejected with a pointer to the new Workspace
  annotation `openultrasast.io/git-commits: "<git name>=<40-hex>,..."`,
  validated for unknown git names and non-hex shas.
- reconciler: rendered docs carry only ax fields; each bound Workspace's pins
  reach the Task as env OUSAST_GIT_PINS = {"<workspace>/<git name>": "<sha>"}.
  A test walks every rendered doc against ax's field lists.
- runner: pins come from OUSAST_GIT_PINS; git is fetched as setup.go does
  (init/remote/fetch with --depth when depth > 0/checkout FETCH_HEAD, default
  branch main, cloneDestination's dir and placeholder-name rules), then the
  pin is checked out. A pinned entry without a branch fetches the commit.
- workspaces: emits the annotation, `dir: repo` and `depth: 1` instead of
  `commit`, so generated case workspaces keep <path>/repo at depth 1.
- design.md: ax field names verbatim; pins and model binding as stripped
  openultrasast.io/* annotations carried to the task by env.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:36:29 +02:00
Marius CiepluchandClaude Opus 5.5 e6d5124ffc fix(plane): doctor no longer reads "No resources found" as a ready pod
kubectl prints the sentence as one whitespace-split row, which passed the
ready check and reported an absent ax controller as "1 pods ready". Rows must
carry a READY column; an empty namespace fails. Regression test added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:21:32 +02:00
Marius CiepluchandClaude Opus 5.5 ea9d7c7b7d merge(plane): runner contract with artifact delivery (task 3)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:18:12 +02:00
Marius CiepluchandClaude Opus 5.5 c250c5738c fix(audit): drop the duplicate plane.tasks row left by the task 6 merge
Merges ee44d3a (task 6, verify and agree) and b850a86 (task 4, ax-backed
reconciler) were committed before the gate; this commit is gated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:15:59 +02:00
Marius CiepluchandClaude Fable 5.1 86e7a61f83 feat(plane): runner contract with artifact delivery (task 3)
ai-service-plane task 3, Requirements 4.1-4.4 (with 2.2 and 2.3 honoured by the
exit-code contract):

- src/openultrasast/plane/runner.py: the ax-task-runner entrypoint. Reads the Task
  from AX_TASK_YAML and the bound Workspaces from AX_WORKSPACES_YAML (task-1 loaders),
  serves /healthz and /readyz on port 80 (AX_RUNNER_PORT; 0 picks a free port,
  AX_RUNNER_HTTP=0 disables it for unit tests), materialises each workspace at its
  bound path (spec.files verbatim; spec.git at <path>/<name>, exported from the
  OUSAST_CASE_CACHE checkout whose origin matches, else a shallow clone at the pin;
  a repo already present on a resumed volume is kept) and only then answers 200 on
  /readyz. Runs spec.command by importing openultrasast.plane.tasks.<name> with the
  remaining elements as argv; sets OUSAST_WORKSPACE_DIR from the first binding when
  the Task's env leaves it unset. A crash writes summary.json with status failed and
  the traceback. When OUSAST_ARTIFACT_URL is set, the whole OUSAST_OUTPUT_DIR is
  POSTed as an uncompressed tar (Content-Type application/x-tar, X-Ousast-Task,
  X-Ousast-Run) with three attempts and backoff; a failed delivery rewrites
  summary.json to failed and exits 2. Exit codes: done 0, failed 2, unfinished 3.
- plane/Dockerfile.runner: python:3.12-slim + git, `pip install .` (no extras), a
  shell wrapper at /usr/local/bin/ax-task-runner exec'ing the module; built and
  loaded into kind by ops/ax/up.sh, not by the tests.
- tests/test_plane_runner.py (+ tests/plane_stub_task.py registered as the task
  module): env contract on a files workspace, readyz 503 then 200, delivery to a
  local receiver with the headers and tar checked, delivery failure -> failed and
  exit 2 after three attempts, crash -> failed with traceback, exit codes per
  status, case-cache export at the pin, shallow-clone fallback, resume keeps the
  checkout.
- model/audit.py: SPEC_OWNED row for plane.runner; module-audit measurement
  regenerated (122 modules, no orphans, note kept).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:11:24 +02:00
Marius Ciepluch b850a86e2a Merge branch 'worktree-agent-ad03a357c98ed488d'
# Conflicts:
#	benchmarks/measurements/2026-09-08-module-audit.json
#	src/openultrasast/model/audit.py
2026-09-29 18:11:05 +02:00
Marius Ciepluch ee44d3a1f6 Merge branch 'worktree-agent-a75a91d22b62da581'
# Conflicts:
#	benchmarks/measurements/2026-09-08-module-audit.json
#	src/openultrasast/model/audit.py
2026-09-29 18:11:04 +02:00
Marius CiepluchandClaude Opus 5.5 a6998173c1 ops(ax): log the failing line and exit code of the bring-up
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:10:51 +02:00
Marius CiepluchandClaude Opus 5.5 6dfa5daa76 merge(plane): repo-facts memory task (task 5)
Audit registry conflict resolved by keeping every plane row; manifest regenerated from the merged tree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:10:51 +02:00
Marius CiepluchandClaude Fable 5.1 4866665e5e feat(plane): ax-backed reconciler, status attribution and doctor (task 4)
Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 2.2, 7.1, 7.3 of ai-service-plane.

- plane/reconciler.py (492 lines, test-counted): a Run's tasks submitted to ax
  in dependency order through the injectable `ax` CLI (apply / get task /
  delete task), serialize labels never overlapping, --workers, state.json with
  pending|running|suspended|done|failed|unfinished, a PID lock per run, done
  tasks skipped on rerun, unfinished tasks holding their dependants, a failed
  task halting the run. Budgets, inputs, output dir, artifact URL and the bound
  Model (annotation openultrasast.io/model; identity and priced parameters) go
  to the task as env in the rendered Task; consumed artifacts travel as files
  of a generated Workspace bound under /workspace. A threaded HTTP receiver
  takes each task's tar delivery, validating run/task and rejecting traversal.
  Outcome comes from the delivered summary.json (done only when every unit
  finished), never from ax's phase alone. status prints the token attribution
  table per task and Model from the summaries' usage fields (n/a, never an
  estimate) and writes attribution.json; doctor runs the four host checks.
- plane/workspaces.py: one Workspace manifest per case pin of a population.
- CLI: ousast plane run|status|doctor|workspaces.
- tests/test_plane_reconciler.py: a fake ax executable drives the DAG;
  tests/test_plane_ax_live.py (marker ax) applies ops/ax/smoke-task.yaml on a
  live cluster and skips unless doctor passes.
- Module audit rows and regenerated manifest.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:10:09 +02:00
Marius CiepluchandClaude Fable 5.1 0cbdc5d2ac feat(plane): verify and agree tasks with metered hunts (task 6)
Requirements 5.3, 2.1, 2.2, 2.3, 7.2 of ai-service-plane.

- plane/tasks/verify.py: the batched hunt of verify_batched.py moved into
  the package (system prompt, one hunt per file, up to 6 candidates, 6
  steps, candidate-anchored sites). New against the reference: each
  candidate's known callers from facts.json in the prompt (capped at 8),
  tool turns and usage per hunt in units.jsonl, per-unit resume (rows
  with "error" are re-asked), pass label from env, MeteredClient wrapper:
  BudgetExhausted -> unfinished (exit 3), AccountError -> failed with the
  provider message (exit 2); summary.json is written last, names the bound
  Model and the summed usage, and is done only when every unit finished.
- plane/tasks/agree.py: verify_batched.agreement over two pass outputs,
  agreed.json / disputed.json, per-candidate cost and turns, declared-site
  match via benchmarks/independent/evaluate.matches_v2 loaded by file;
  metrics block in summary.json; no model calls.
- tests/test_plane_verify.py: a scripted usage-recording hunter client;
  callers line, resume, 402, budget stop and resume, model and summed
  usage, unpriced usd None, both main() env contracts, agreement metrics.
- model/audit.py + module-audit manifest: SPEC_OWNED rows for plane.tasks,
  plane.tasks.verify, plane.tasks.agree; manifest regenerated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 18:05:08 +02:00
Marius CiepluchandClaude Fable 5.1 2a490eed59 feat(plane): repo-facts memory task (task 5)
plane/tasks/repo_facts.py computes, from source text only and without a
model or git, the product files of a workspace (tests, vendored trees,
node_modules, dist/build, minified bundles and dot-directories excluded via
the layout facts plus the model_sinks.py filter), the functions each file
declares (Python, PHP, JavaScript/TypeScript, Java, Go, Ruby declaration
patterns extended from sink_candidates.py) and, per candidate function, the
call sites in other product files with path, 1-based line and enclosing
function, capped at 40 and sorted (Requirement 5.1). Output is byte-identical
across runs over the same tree (Requirement 5.2).

Under the task contract the unit is one file: a row goes to units.jsonl as
each file finishes and a restart reuses the rows computed for the same
candidate set (Requirement 2.2); facts.json is assembled from the rows and
summary.json is written last, refusing `done` unless every unit finished.
The entrypoint reads OUSAST_WORKSPACE_DIR, OUSAST_OUTPUT_DIR and optional
OUSAST_INPUT_CANDIDATES only (Requirement 2.1) and exits 0 on done, 2 on
failed with the traceback recorded in summary.json.

tests/test_plane_repo_facts.py: a three-file tree with a tests/ file, a
vendored file, a node_modules bundle and a dot-directory; cross-file callers
with line and enclosing function, same-file and declaration lines excluded,
the 40 cap, byte-identical second run, resume that recomputes only the
missing unit, a crash that leaves `failed` with the unit counts and resumes
to `done`, and the declaration patterns per language.

The two new modules are registered as spec-owned in the module audit (their
importer, the runner, lands in task 3) and the audit manifest is regenerated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:55:34 +02:00
Marius CiepluchandClaude Fable 5.1 6fed361387 ops(ax): bring-up, tear-down and smoke Task for the local ax (task 0, in progress)
kind cluster + registry, Agent Substrate, ax control plane via ko, one files-only
smoke Task; helpers run from their own checkout because they resolve paths from
the git toplevel. Footprint and README follow once the cluster is up.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:50:28 +02:00
Marius CiepluchandClaude Fable 5.1 488e63951e merge(plane): manifest schemas and validation (task 1)
Audit registry conflict resolved: plane, plane.budget and plane.manifests rows kept; manifest regenerated from the merged tree.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:47:45 +02:00
Marius CiepluchandClaude Fable 5.1 c728808316 feat(plane): manifest schemas and validation (task 1)
Add openultrasast.plane.manifests: frozen dataclasses and loaders for ax's
Task, Workspace and Model kinds (ax.io/v1alpha1, ax's fields only) and the
Run kind (openultrasast.io/v1alpha1: tasks, dependsOn, inputs, outputs,
budget, serialize). Every violation raises ManifestError naming the kind,
metadata.name and field path: unknown fields at any depth, wrong apiVersion
or kind, a provider outside google/anthropic unless deepseek is declared via
the openultrasast.io/provider-extension annotation, a Run with a dependency
cycle, an input naming no task, and a Run task naming no loaded Task.
load_manifests reads multi-document YAML files and indexes by kind and name.

PyYAML becomes the one declared core dependency; the two zero-dependency
guards now pin exactly that, and the module audit registers the plane
package under the ai-service-plane spec.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:44:31 +02:00
Marius CiepluchandClaude Fable 5.1 3acd6c8822 merge(plane): budget metering (task 2)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:42:48 +02:00
Marius CiepluchandClaude Fable 5.1 6884bc8c2c spec(ai-service-plane): switch to ax completely; routing and token attribution requirements
Maintainer decisions 2026-09-29: ax (kind + Agent Substrate + ax controller on
this host) is the only executor; the Run layer carries dependencies, artifacts,
budgets and attribution until ax has them. Requirement 7 declares routing per
Task (one Model per Task, no runtime switching) and a token attribution table
from provider usage fields. Task 0 (bring-up, doctor), runner artifact delivery,
ax-backed reconciler with a fake-ax test seam. harnessx-removal brief records
the HarnessX inventory; removal is gated on the increment's measurement.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:42:39 +02:00
Marius CiepluchandClaude Fable 5.1 f460735a87 feat(plane): budget metering (task 2)
MeteredClient wraps a ChatClient, sums the provider's usage rows per call,
prices them from a Model manifest's parameters (cache-hit, input, output per
million), and raises BudgetExhausted before a call once the usd or calls
ceiling is reached. A 401/402 or "Insufficient Balance" failure becomes
AccountError with the provider's message. An unpriced model reports usd None,
never zero, and refuses a usd budget; summary() gives the spend block of a
task's summary.json.

The two plane modules are registered as spec-owned in the module audit (their
consumers land in tasks 3-4) and the audit manifest is regenerated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:41:09 +02:00
Marius CiepluchandClaude Fable 5.1 152728c052 spec(ai-service-plane): tasks approved; implementation phase
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:29:10 +02:00
Marius CiepluchandClaude Fable 5.1 c8ec677814 spec(ai-service-plane): implementation plan for review; design approved
Nine tasks in dependency order: manifests, budget metering, runner contract,
reconciler and CLI, repo-facts, verify and agree, the increment's manifests, the
measurement against the recorded batched check, handover.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:28:13 +02:00
Marius CiepluchandClaude Fable 5.1 d8f4443702 spec(ai-service-plane): design for review; requirements approved
Package src/openultrasast/plane/: manifests (ax kinds + Run), a reconciler under
500 lines with persisted status and resume, an ax-contract runner, budget metering,
and task entrypoints. First increment: repo-facts (source-only callers, no model),
verify (the batched hunt with callers in the prompt, per-unit resume), agree.
Measurement on the 46-candidate set against the recorded batched check.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:27:04 +02:00
Marius CiepluchandClaude Fable 5.1 dcc51d8a8d spec(ai-service-plane): requirements for review
Six requirements from the brief: ax kinds plus a Run kind; isolated, idempotent,
resumable tasks with metered budgets; a local reconciler under 500 lines with no
pipeline logic; ax's runner contract; repo-facts as the memory layer; and a
measured first increment (repo-facts + verify on the 46-candidate set: at least 16
of 20 declared sites, under $0.022 per candidate for two passes).

Phase: requirements, not yet approved.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:24:49 +02:00
Marius CiepluchandClaude Fable 5.1 27812499b5 chore(dev): token-lean workflow tools -- read guard, bulk reader, transcript report
Measured on the main session: 90% of development tokens were tool I/O. Three
tools from the Portal pattern:
- read_guard.py: PreToolUse hook that denies a whole-file Read over 350 lines and
  points to a ranged read or the bulk reader (pipe-tested: deny, range, small).
- bulk_read.py: answers a question about files with the cheap chat model, citing
  path:line, under a spend ceiling.
- token_report.py: where a session's tokens go, from its transcript.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:23:30 +02:00
Marius CiepluchandClaude Fable 5.1 38af0a86c2 spec(ai-service-plane): brief -- Kubernetes-style service design for the model pipeline
Adopt the design of google/ax (Task / Workspace / Model manifests, a reconciler,
isolated resumable execution, a runner contract), not yet its software: it needs
Kubernetes and Agent Substrate, lists only Google and Anthropic providers, and has
no pipelines, artifacts or budgets. We add a thin Run layer for those.

Each pipeline concern (classify, triage, repo-facts, verify, judge, recheck, score)
becomes a declared task with its own inputs, outputs and budget. That bounds the
tokens per task and lets each be developed alone. Decisions listed for the
maintainer.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 17:21:52 +02:00
Marius CiepluchandClaude Fable 5.1 ec7a5ed90d docs(pre-push-safety-net): record the model-driven pipeline result on v2 and stop
Exploratory, 7 of 17 cases, stopped when the account emptied:
- source-only scope: a model reading every product file puts the vulnerable
  function among the candidates in 15/17 (ranker 1/17, vocabulary 2/17);
- per-file triage + per-file 6-step hunts + two-pass agreement: 12/15 candidate
  cases on the validation set; 4/7 on the full run, two more in one pass of two;
- precision: 5 pgAdmin agreed findings read against source, 3 true (privileged),
  2 false with self-contradicting rationales; agreement cannot catch an error
  both passes make;
- cost: $0.009-0.022 per candidate for two passes.

Four instrument defects fixed on the way (model id, provider routing, JSON-mode
final turn, unbounded tool output) void the earlier hunter number. No gate is
met; qualification needs population v3 and a protocol that states the judge
stage, the pass rule and a config-family question.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 11:29:06 +02:00
Marius CiepluchandClaude Fable 5.1 6acd354311 feat(independent): fixed-pin recheck and an exploratory scorer for the batched verifier
- verify_batched.py --fixed asks the agreed candidates again at the fixed pin
  (no triage); a function the fix removed is recorded as removed.
- score_batched.py applies protocol v2's matching to the batched output
  (vulnerable pin, and the fixed pin where rechecked) and draws the precision
  sample.
- A finding's site is anchored to the candidate operation asked about; the
  model's own reported location is kept in `reported_at`. ReactPress's confirmed
  findAll was reported at the controller file and failed to match its declared
  site. The scorer anchors older records the same way.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 10:25:08 +02:00
Marius CiepluchandClaude Fable 5.1 072a102b20 measure(independent): triage + per-file batching + two-pass agreement -- 16/20 sites, flicker cut 26 -> 9, 34% cheaper
verify_batched.py replaces the per-candidate verifier's design:
- triage per file (no tools): where does the operation's data originate;
  internal/constant candidates are not verified;
- one 6-step hunt per file for up to 6 candidates, a verdict per operation;
- two independent passes; only agreement is reported, the rest is "disputed".

On the 46-candidate validation set ($1.01):
- declared sites: 16 of 20 agreed, 12 of 15 cases (the per-candidate design
  confirmed 11). Budibase and Froxlor are new; Unleash dropped to one pass of two.
  winml is triaged out as "constant": a CORS literal, which the reachability
  question does not fit.
- the 26 pgAdmin candidates that flickered between single runs: 2 triaged out,
  9 agreed in both passes, 4 disputed, 13 quiet.
- $0.022 per candidate for two passes vs $0.033 under the old design; on a file
  with several candidates, 2.3x cheaper per pass.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 10:22:11 +02:00
Marius CiepluchandClaude Fable 5.1 bad8c7ada4 measure(independent): known-answer check of the verifier -- 11/15 sites, unstable verdicts, escalation costs more
Declared vulnerable sites: 11 of the 15 that were candidates are confirmed (8 at 3
steps, 3 more at 6). Missed: winml (a CORS config; the reachability question does
not fit), FUXA, Budibase, Froxlor.

Restart-based escalation is strictly worse than a straight 6-step budget: 88% of
pgAdmin candidates used every step, so they paid 3 + 6 ($0.0201 per candidate vs
$0.0139 baseline). Defaults are now 6 steps, no escalation. The cache order change
did not lower the per-call cost ($0.002): the conversation prefix was already
cached; 56% of prompt tokens are served from cache.

The larger finding: on 180 pgAdmin candidates verified twice, 15 and 13 were
flagged with only 2 in common. Verdicts on borderline candidates flip run to run,
while the declared sites were found consistently. Precision needs agreement
across runs or a second judgement, not a single pass.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 10:10:33 +02:00
Marius CiepluchandClaude Fable 5.1 6df74cc3ae fix(independent): verifier checkouts live under the run's own results directory
Two runs exporting the same pin into one path deleted each other's tree.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 09:56:46 +02:00
Marius CiepluchandClaude Fable 5.1 7be1f18778 chore(independent): verifier flags for a known-answer check into a separate results directory
--declared-sites-only, --candidates-from <jsonl> and --results-suffix, so the
efficiency change can be validated on the 180 already-verified pgAdmin candidates
and the 15 declared sites without touching the main results.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 09:52:50 +02:00
Marius CiepluchandClaude Fable 5.1 041d87199a perf(hunter): cacheable message order; adaptive step budget in the sink verifier
Token efficiency, measured on 180 verified pgAdmin candidates: stage 2 costs
$0.0139 per candidate, 90% of the spend, because the conversation is re-sent on
every turn and grows with each tool result.

- Context files now precede the question in the hunter's first message. DeepSeek
  bills a cached prefix at 1/30 of a fresh one, by prefix; with the question first,
  every candidate asked about the same file (about 5 per file) broke the prefix on
  its first line and paid for the file again. A test pins the order.
- The verifier runs each candidate at 3 steps and repeats it once at 6 only when
  the hunt spent every step on tool calls without a verdict. On the seven declared
  v2 sites, 3 steps found 5 and 8 steps found 6; most candidates conclude in 3.
- Per-candidate token usage (prompt, cached, completion, calls) and escalation
  are recorded, and each case reports its cache share, so the next efficiency
  decision is made on numbers.

Validation on the 180 pgAdmin candidates plus the 22 known sites (about $1)
waits for account credit: DeepSeek returns 402 at the moment.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 09:52:23 +02:00
Marius CiepluchandClaude Opus 5.5 66fa08cdd0 fix(independent): the sink verifier stops on an account error and retries unverified candidates
The rerun hit DeepSeek HTTP 402 ("Insufficient Balance") after 180 pgAdmin
candidates. Every later call failed at $0, and the runner wrote sixteen cases as
complete with nothing asked, because an unverified candidate counted toward
completion. That is a clean-looking zero from an instrument that could not call
the model.

Now:
- 401/402 or "Insufficient Balance" stops the run at once, exit 2, nothing
  recorded;
- only verified candidates count as done, so unverified ones are asked again on
  resume;
- a case is complete only when every candidate was verified.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-29 07:00:59 +02:00
Marius CiepluchandClaude Opus 5.5 8794884a64 fix(hunter): bound every tool result; show the provider's reason for an HTTP error
FUXA failed every hunt with HTTP 400, and neither run could say why. The client
discarded the response body. With the body shown, the reason was "This model's
maximum context length...": FUXA ships minified bundles whose single lines run to
megabytes, and grep_repo returned a matched line whole.

- grep_repo and find_refs quote a matched line (240 characters), never the whole
  line.
- Every tool result is capped at 16 x max_chars, with a truncation marker.
- OpenRouterChatClient errors carry the provider's response body (first 500
  characters). That would also have exposed the rejected model id in one call
  instead of eighteen days.

After the fix, FUXA candidates verify normally at $0.004-0.013 each. A test covers
the megabyte-line case.

verify_sinks.py: skip futures cancelled at the spend ceiling instead of reading
them (the run crashed with CancelledError).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 20:15:14 +02:00
Marius CiepluchandClaude Opus 5.5 3ffb8b4c54 fix(hunter): ask for the final answer in plain text; DeepSeek's JSON mode lost it
The final-answer turn requested the provider's JSON mode with the tools still
attached. DeepSeek (deepseek-flash) then answered with the format itself
(`{"type": "json_object"}`), wrapped the array (`{"type": "json_object",
"value": [...]}`), or emitted its tool-call markup as text. A hunt that had found
the vulnerability reported nothing.

Measured on the v2 sink verifier with the declared vulnerable functions: 0 of 7
confirmed in JSON mode, 5 of 7 asked plainly (6 of 7 at 8 steps). The model named
Medplum's actual redirect-URI prefix-match bug, LMDeploy's redirect bypass of
_is_safe_url, and ContextForge's unsandboxed Jinja2.

The final turn is now plain text without tools. The parser also reads a
`{"value": [...]}` wrapper. Two tests that pinned JSON mode now pin the plain
turn, and a new test reproduces the wrapped answer.

Invalidates as measurements: the hunter experiment on v2 (c4a0bd8) and the first
sink-verifier run. Both used this turn.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 20:10:57 +02:00
Marius CiepluchandClaude Opus 5.5 267bca5232 perf(hunter): read a repository's source once per state, not once per tool call
Each grep_repo and find_refs call re-enumerated the tree (walk, ignore rules,
vendored checks) and re-read every file. The v2 sink verifier spent almost three
hours on its first case (pgAdmin, about 1,800 files) at 108% CPU with one open
connection. It was CPU-bound in these tools, not waiting on the model.

Source lines are now memoised per root, keyed by path and modification time. A hit
is re-validated file by file, because an edit inside a subdirectory does not touch
the root's mtime; a test covers one enumeration and a detected edit.

verify_sinks.py now runs candidates in worker processes, not threads, and appends
each candidate's result to a per-case log, so a stopped run resumes where it left
off. GoPay: 12 candidates in 14 s, the real site found.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 18:30:09 +02:00
Marius CiepluchandClaude Opus 5.5 023c24535f measure(independent): stage-2 verifier for model-classified sink candidates
Per candidate function, one bounded tool-hunter pass (custom prompt, the file as
context, 3 steps) asks whether attacker-controlled data reaches the operation
without a sufficient guard. It reports only with a stated source-to-sink path.
Hard spend ceiling, parallel workers; one run per pin; the fixed pin is rechecked
only for detected sites. Exploratory on v2.

Pilot: on GoPay, the one real site was reported in 3 of 4 runs and nothing else
out of 12 candidates.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 13:24:22 +02:00
Marius CiepluchandClaude Opus 5.5 32eb46f297 measure(independent): model-classified sinks put the vulnerable function in scope in 15/17
Source-only selection on the v2 vulnerable pins, compared on the same cases:
- ranker top-20 entry points: the vulnerable file was in scope in 1/17;
- shipped-vocabulary sink-first: the declared site was a candidate in 2/17;
- model-classified sinks (deepseek-flash, no vocabulary): the declared site was a
  candidate in 15/17, and a fix or site file in 16/17. Every chunk was classified;
  $14.42.
Misses: User Registration (file in scope, not the function) and Sulu (JCR-SQL2
query builder). The cost of this reach is volume: 3-1037 candidate functions per
repository, 4,762 in total. The next stage must decide which candidates are fed by
attacker data without a guard. v2 is spent for tuning; this qualifies nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 13:21:41 +02:00
Marius CiepluchandClaude Opus 5.5 05d56993fc measure(independent): model-classified sinks diagnostic (source only, no vocabulary)
This asks DeepSeek, per product source file (in chunks of 400 numbered lines),
which functions perform the family's dangerous operation on non-constant data. It
is scored like sink_candidates.py: is the declared vulnerable function among the
candidates? A failed chunk counts as unclassified, never as "no sinks". The spend
ceiling is enforced across worker threads.

Pilot: GoPay found its declared site for $0.03. Estimated full v2 run: ~$14-18.
The maintainer raised the ceiling to $20.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 12:43:03 +02:00
Marius CiepluchandClaude Opus 5.5 83cca9894f measure(independent): sink-first selection from source -- the vocabulary misses most real sinks
This is a source-only diagnostic: no engine and no model. At each v2 vulnerable
pin, every call matching the family's shipped sink vocabulary becomes a candidate,
attributed to its enclosing function.
- The declared vulnerable function is a candidate in 2/17 cases (Medplum, GoPay);
  the ranker's top 20 managed 1/17.
- A fix-changed or site file is among the candidates in 8/17.
- Candidates per repository range from 2 to 904 functions.

The misses are vocabulary, not direction. Examples: wp_redirect, download_url,
spawn(shell:true), ky(), requests.Session().get, sympy parse_expr, Jinja
from_string().render, Mongo collection.find, JCR-SQL2 createQuery, TypeORM
andWhere template strings, and CORSMiddleware(allow_origins=["*"]) are not named.
v2 is spent for tuning, so this informs design and qualifies nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 12:36:43 +02:00
Marius CiepluchandClaude Opus 5.5 c4a0bd8bd9 measure(independent): the unchanged tool hunter on population v2 -- recall 1/17, and why
The run follows hunter-protocol-v2.md: deepseek-flash, the ranker's top 20
regions per pin, protocol v2 scoring, $8.88 spent.
- Recall: 1/17 (Wilson 0.01-0.27).
- Precision: 2/14 sampled (both true positives need a privileged role, one of
  them GoPay's CVE).
- Fixed-side alerts: 1. Benign alerts: 16.
- Stability: 73% of findings differ between two identical runs.

The deciding observation: the ranker's top 20 contained the vulnerable file in 1
of 17 cases, and in that case (GoPay) the hunter detected it. With 4 tool steps
per hunt it cannot search its way to code it is not handed. The result therefore
says less about the model than about scope selection, which sits upstream of the
engine too.

Instrument failures, reported as failures: every FUXA pin and three Directorist
pins (DeepSeek HTTP 400 on every hunt; neither offered a region of its family).

Scorer fix: protocol v2 defines benign alerts and the precision pool over the
CASE FAMILY, but score_v2 pooled every family. The engine's dump emits only the
case family, so its recorded result (21adc40) is unchanged by this; rescoring
reproduces 3 benign alerts and 49 pooled findings.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 11:36:32 +02:00
Marius CiepluchandClaude Opus 5.5 6bfb05f952 spec(pre-push-safety-net): pre-register the hunter experiment on population v2
This is the deciding experiment of the strategy review, fixed before any v2 pin is
hunted.
- Hunter: the unchanged tool hunter (default prompt, tools and 4 steps) on
  deepseek-flash.
- Scope: the ranker's top 20 regions for the case family, in four hunts. The model
  never sees the fix diff or the declared sites.
- Families: a fixed keyword table maps the model's family to the protocol's three.
- Scoring: exactly protocol v2's scorer.
- Cost: a hard $10 ceiling.

hunt.py runs it and scores it (--score). A pilot on a v1 development case
(mongo-express) found the vulnerability at lib/bson.js:60, at $0.031 per pin,
alongside a lot of speculative noise.

Transport fix found by the pilot: the DeepSeek endpoint the hunter builds was
unpriced, so the cost meter read $0.00 for any number of calls. It is now priced.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 09:10:02 +02:00
Marius CiepluchandClaude Opus 5.5 cf579956d5 fix(model): the chat model is deepseek-flash; the hunter reaches DeepSeek
Every model call has failed since 2026-09-10 (83ab533):
- The default detector and judge model was `deepseek-v4.1-flash`, the product name.
  The API rejects it with HTTP 400 ("The supported API model names are
  deepseek-flash, deepseek-v4-pro"). The judge and pipeline record an endpoint
  failure as a degradation, never a verdict, so nothing failed loudly.
- `tool_hunter.resolve_hunter_client` resolved OpenRouter only. In this project
  DeepSeek is the chat provider and the OpenRouter key is scoped to embeddings, so
  the hunter could not reach a chat model at all; the call came back 401.

The defaults are now `deepseek-flash` (priced). The hunter resolves DeepSeek first
when its key is set, and OpenRouter stays as the explicit or fallback route.
Verified live: plain completion and a tool call through both the endpoint resolver
and the hunter client.

Not affected: the independent evaluations (finding_dump runs with
max_model_calls=0) and the 2026-09-06 hunter measurement, which predates the id.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 09:01:14 +02:00
Marius CiepluchandClaude Opus 5.5 5835239808 docs(pre-push-safety-net): strategy review after the two independent evaluations
v1 (1/11) and v2 (0/17 recall, 0/17 sampled precision, 12% completion) show that
fixes learned on one population do not transfer to the next. The causes are
breadth: framework-shaped entry points, module boundaries the frontends do not
resolve, name-matched sinks colliding, and scale.

Options:
- A: continue engine-first;
- B: the model proposes, the engine proves;
- C: narrow the product to one ecosystem.
Recommendation: first run the unchanged tool hunter on the v2 pins as the deciding
experiment.

For maintainer decision; no approved spec changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 08:55:07 +02:00
Marius CiepluchandClaude Opus 5.5 21adc408f1 measure(independent): evaluation of population v2 under protocol v2 -- fails every M4 gate
The analyzer was frozen at a75b9f6. 17 cases, 102 scans (vulnerable and fixed pins
twice each), scored exactly as pre-registered.
- Recall: 0/17 (Wilson 95% 0-0.18). No unstable detections.
- Precision: 0/17 sampled (0-0.18). Every sampled finding is false: regex
  match/exec read as sinks, int casts, ORM statements, client-SDK code, the
  process's own argv, a generated database name, a registered-URI check.
- Completion: 13206/108374 questions (12%).
- Fixed-side alerts: 0, trivially, since nothing was detected.
- Benign alerts: 3.
- Stability: 2 of 100 findings differ between runs.
The zero is not an instrument artefact: FUXA, GoPay, cct-studio, ReactPress, User
Registration and Directorist completed questions in the sink file itself and still
found nothing.
Instrument failures, reported as failures: LMDeploy (every taint batch failed on all
six pins) and Froxlor's two benign scans.

This record is committed before any analyzer change informed by v2.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-28 08:52:53 +02:00
Marius CiepluchandClaude Opus 5.5 a75b9f62f9 spec(pre-push-safety-net): pre-register evaluation protocol v2 and its runner
Committed before any v2 case is scanned. It has two changes from v1, both chosen by
the maintainer from lessons of the v1 run:
- declared sites: a finding at a `path::function` the advisory names is credited,
  as well as one on the fix's changed lines;
- two runs of the vulnerable and fixed pins: detection needs the finding in both
  runs, a fixed-side alert in either counts, and the difference between runs is
  reported as stability.

evaluate.py takes --population. v2 scans six pins per case and scores with
matches_v2 / score_v2. v1's scoring is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 18:02:03 +02:00
Marius CiepluchandClaude Opus 5.5 21634ccf58 spec(pre-push-safety-net): freeze the independent population v2
The analyzer was not run. freeze.py read each case's source at both pins:
- both pins are commits in the repository and in the right order;
- the declared sink site's file is present at the vulnerable pin, and its declared
  functions appear in it;
- the fix touches a declared site file (Budibase's sites reordered so the changed
  file, queries.ts, comes first).

Benign controls:
- 12 are reviewed automatic proposals.
- 3 automatic proposals were rejected as not benign: qwed's merged a ReDoS fix,
  ReactPress's corrected a SQL column whitelist, and Sulu's was a branch merge that
  can carry the fix.
- Those three, plus cct-studio and Directorist (no automatic proposal), were chosen
  by hand. Directorist's comes from before the vulnerable pin, because only 3
  commits follow its fix.
freeze.py now accepts a recorded benign control instead of reporting that none was
proposed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 18:02:03 +02:00
Marius CiepluchandClaude Opus 5.5 c64e37edbd spec(pre-push-safety-net): reserve the independent population v2 (17 cases, unscanned)
v1 was evaluated and then used to retune the analyzer. A retune can only be
qualified on a population that did not inform it.

The candidates came from web research alone (research-2026-09-26.md):
- advisories were taken newest-first per language under fixed criteria, never by
  how easy they look;
- 261 advisories were examined and 224 rejected, each with a reason;
- every SHA pair was verified through the GitHub API;
- no clone and no scan.
The maintainer selected the rule-based "balanced" set: 5 Python, 6 JS/TS, and 6 PHP
of which 4 are WordPress plugins; 10 injection, 6 destination and 1 config.
Each case declares the advisory's sink sites before any scan, for protocol v2.

freeze.py takes --population and writes freeze-vN.json. The reservation tests now
cover every population-v*.toml.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 17:53:51 +02:00
Marius CiepluchandClaude Opus 5.5 d14fd5e960 Revert "feat(taint): project sink wrappers, one level and confirmed by dataflow"
This reverts commit cc885f0. It failed the acceptance bar set before the run (each
recall pin within +2 findings, completion unchanged):
- WP Statistics: 16 -> 31.
- PMPro: 16 -> 26, and completion 378/382 -> 103/382. Six prior findings were lost
  to the budget; CVE line 936 survived.
- YesWiki: completion 2798 -> 1968, still not detected.

Two causes:
- Cost: wrapper confirmation runs a flow query per parameter of every sink-bearing
  method, repeated at each engine start of a portioned scan.
- Precision: a helper whose parameter reaches a sink under its own escaping (for
  example `getMemberOrderByPayPalToken(sanitize_text_field(...))`) is not a sink
  for its caller.
Doing this properly needs summaries that carry the wrapper's internal
sanitisation, computed once per scan, not per request batch.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 15:36:25 +02:00
Marius CiepluchandClaude Opus 5.5 cc885f025c feat(taint): project sink wrappers, one level and confirmed by dataflow
This is the second attempt at the YesWiki shape: SQL run through
`$this->dbService->query($query)`, whose body is the `mysqli_query` the facts name,
through a receiver the frontend cannot type. The first attempt was reverted in
c7e3a05.

A method is now a wrapper for a parameter only when the engine's own dataflow
inside the method carries that parameter to a fact sink's argument, on a plausible
path that no cleansing or guard discharges. The guard rails:
- one level only;
- nothing under a bundled library tree (lib/, vendor/, node_modules/, ...);
- a name counts only if every project method with that name is a wrapper;
- wrappers are asked in the flow pass only, never in the evidence pass, so no
  region's selection or pruning changes;
- at a wrapper call, only the confirmed argument is asked.

Three Joern-gated tests cover the direct shape (reported), a literal, an
intval-casting helper and a level-2 feeder (not reported), a harmless same-named
method, and a wrapper inside lib/. Each fails when its rule is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 13:05:12 +02:00
Marius CiepluchandClaude Opus 5.5 c7e3a05ba0 Revert "feat(taint): project-local sink wrappers are sinks where they are called"
This reverts commit 54c9dfc. The recall-pin check (p16) showed a large precision
and completion regression, although every recall target survived:
- MW WP Form: 3 -> 60 findings. Form-field renderers became sinks.
- WP Statistics: 16 -> 40.
- PMPro: 16 -> 65, 20 of them inside bundled Braintree/Stripe code. Completion fell
  to 390 of 1189 questions, because wrappers made many more regions look
  sink-bearing.
- YesWiki: completion fell from 2798/2807 to 216/2807.

The structural summary was too loose. Any identifier in a sink argument counted as
fed by a parameter if any assignment anywhere in the method mentioned it, with no
order or dominance. It also ignored vendored library code and cascaded three
levels by name. A redesign needs dataflow-confirmed, level-limited summaries that
leave the question count alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 10:14:30 +02:00
Marius CiepluchandClaude Opus 5.5 54c9dfcd44 feat(taint): project-local sink wrappers are sinks where they are called
Real code rarely calls the driver where it builds the query. YesWiki builds its
INSERT in `FormManager::create($data)` and runs it through
`$this->dbService->query($query)`, whose body is the `mysqli_query` the facts name.
The only sink the facts knew sat in a helper that no request reached directly.

A method whose parameter feeds a family sink is now a sink for that argument,
reported at the call site. The summary is structural, to stay cheap and
deterministic. It is not a flow query per parameter, which would repeat at every
engine start of a portioned scan. The parameter must reach the sink call's
argument directly or through at most two local assignments, with at least one
occurrence outside every cleansing call. It iterates up to three levels, so a
wrapper of a wrapper counts.

Calls resolve by name, because an injected receiver has no type the frontend can
see. A name therefore counts only if every project method with that name is a
wrapper. At a wrapper call, only the summarised arguments are asked.

Two Joern-gated tests cover:
- a request value into a second-level wrapper, reported at the controller's call;
- direct use, a literal, and an `intval`-casting helper, which are not wrappers;
- a harmless same-named method, which disqualifies the name.
Both fail with their part removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 07:55:24 +02:00
Marius CiepluchandClaude Opus 5.5 64ef488ce4 test(cpg-budget): a process vanishing mid-read is the success case too
Reading /proc/<pid>/stat for a process that is reaped during the read raises
ProcessLookupError (ESRCH), not FileNotFoundError. The previous race fix caught only
the latter, so the hanging-process-group test failed twice today under full-suite
load. It passed alone. The disappearance is exactly what the test asserts.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-26 07:44:33 +02:00
Marius CiepluchandClaude Opus 5.5 63460f8af4 fix(taint): a guard validates only the value it tests
p14 showed PMPro's functions.php:906 discharged by `elseif (in_array('E', $levels))`.
That checks whether a constant is in `$levels`; it says nothing about `$levels`. The
guard counted a carried variable in any argument. Only argument 1, the tested value
(`in_array`'s needle, the URL given to `url_has_allowed_host_and_scheme`), is
validated now. The allowlist test gains the haystack shape as a reported control.
The test fails when any argument counts.

The other p14 removals were right: WP Statistics export_data x2
(`if (!in_array($table, $allow_tbl)) $table = false;`) and PMPro memberorder.php:1633.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-25 18:34:15 +02:00
Marius CiepluchandClaude Opus 5.5 69d4a8626f fix(taint): a value stored while its guard held stays guarded
Ultimate Member's fixed pin kept one alert. The fix writes
`$this->sql_order = " ORDER BY u.{$sortby} "` inside
`elseif (in_array($sortby, $allowed, true))`, and after the if/elseif chain it
passes the field through `apply_filters(...)` into a second assignment. The guard
was judged only at the flow's final node, which is outside the branch.

A guard is now also judged at every point where the path stores the value (an
assignment target). A value stored while the check held carries the check forward,
because the rest of the path derives from that store. A mere read inside a branch
is not a candidate: the engine lists uses in branches the flow does not depend on.

The Joern-gated test gains the shape and its control: the same store in the
unguarded else branch, through the same filter, is still reported. The test fails
when only the end is judged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-25 17:24:49 +02:00
Marius CiepluchandClaude Opus 5.5 03f2ce7162 feat(taint): guards -- a validator or allowlist check makes a value safe where it held
Three of the four fixed-side alerts left in the independent population had one
cause: the fix was a check, and checks were not modelled.
- wger's fix is `url_has_allowed_host_and_scheme`: `if not ...(next_url):
  next_url = reverse(..)` and `if next_url and ...(next_url): return redirect`.
- Ultimate Member's fix is an allowlist: `in_array(strtoupper($order), [...],
  true) ? $order : 'ASC'` and `elseif (in_array($sortby, $allowed, true))`.
(The fourth, Signal K, is `assertAllowedHost`, a project-local function. It is not
covered.)

Sanitizer facts can now say `guard = true`. A guard is a check, never a cleansing
call on a path. A flow is discharged only when a guard tests a variable the path
carries, and the flow is in one of these places:
- inside the branch whose condition requires the check. The guard may be combined
  with `&&` only;
- past a failure test, a single `!` combined with `||` only, whose branch exits
  (return, exit, die, wp_die, throw) or overwrites the value, and whose condition
  dominates the flow's end;
- through a ternary whose condition requires the check, where the other arm cannot
  carry the value. The engine can enter through the condition's own copy of the
  value, which never becomes the result.

Facts: `in_array` (PHP) and Django's `url_has_allowed_host_and_scheme` /
`is_safe_url` (Python). A row now consults guards even when a request lists no
sanitizers.

Two Joern-gated tests cover the discharged shapes and, as controls, an unguarded
redirect, a guard on a different variable, a failure that is only logged, the
unguarded else branch, and the ternary arm the check does not select. Both tests
fail without the guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-25 16:56:15 +02:00