Files
Open_Ultra_SAST/.kiro/specs/ai-service-plane/requirements.md
T
Marius CiepluchandClaude Opus 4.8 39a1873e5b docs: plain-English pass over the active specs
Ran the humanizer rules over 79 prose files in the active specs (brief,
design, requirements, research and narrative docs). Removed em dashes,
significance inflation, forced rule-of-three, bold-label list items and
filler. Skipped tasks.md checklists and the retired specs (harnessx-*,
openrouter-sast-harness, unseen-repo-evaluation).

Every number, path, command, hash and identifier was preserved: checked
each file against HEAD for drift in numbers, inline code and code fences,
zero drift across all 79 files.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 18:16:41 +02:00

158 lines
10 KiB
Markdown

# Requirements Document
## Introduction
The model-driven pipeline (classify sinks, triage, verify, judge, recheck, score) is a set of scripts run from
one long conversation, each with its own resume, budget and pass logic. Measured 2026-09-29: 90% of development
tokens are tool I/O around those scripts, and verification re-derives the same per-repository facts in every
hunt and every pass. This feature restructures the pipeline as a Kubernetes-style service plane at the model
level, in the shape of google/ax: declarative `Task`, `Workspace` and `Model` manifests, a reconciler that
executes them in isolation with resume, and a runner contract. The plane runs on ax itself: a `kind` cluster
with Agent Substrate and the ax controller on this host (maintainer decision 2026-09-29: "switch to ax
completely now"). What ax does not yet carry -- task dependencies, artifacts, token budgets (on ax's roadmap),
a DeepSeek provider -- is carried by our thin `Run` layer and dissolves into ax as ax gains it.
## Boundary Context
- In scope: the manifest kinds and a `Run` kind for dependencies, artifacts and budgets; bring-up of ax on
this host (kind + Agent Substrate + ax controller) with a doctor command; an ax-backed reconciler that
submits Tasks to ax (one engine-bearing task at a time) and is the only executor; the runner contract and a
runner image; the first two tasks, `repo-facts` and
`verify`, with the existing scripts as reference behaviour; measurement on the 46-candidate validation set.
- Out of scope: a remote or multi-node cluster; contributing providers or budgets to ax upstream (a
follow-on once the increment is measured); changing what the model is asked (prompts, families, pass rules)
except as required to consume `repo-facts`; the judge stage, the config-family question and population v3
(they become tasks later, under their own specs); any change to `evaluate.py`'s protocol scoring.
- Adjacent expectations: the independent-population protocols and records stay as they are; `finding_dump`
and the engine path are untouched; `vulnerabilities-over-plumbing` applies -- this is plumbing that must pay
for itself in measured tokens and unchanged or better recall.
- Target state (maintainer, 2026-09-29): this plane replaces HarnessX as the agentic plane; HarnessX is
removed by its own spec (`harnessx-removal`) once Requirement 6's measurement shows value; the `Run` layer
shrinks as ax gains dependencies, budgets and artifacts. No new HarnessX feature is added meanwhile.
## Requirements
### Requirement 1: Manifests are ax's kinds plus a Run kind
**User Story:** As the maintainer, I want the pipeline declared in manifests that ax would accept, so that
moving to a cluster later changes the reconciler and not the pipeline.
#### Acceptance Criteria
1. `Task`, `Workspace` and `Model` manifests use `apiVersion: ax.io/v1alpha1` and only fields ax documents
(`spec.image`, `spec.command`, `spec.env`, `spec.resources`, `spec.workspaces`; `spec.git`, `spec.files`;
`spec.provider`, `spec.model`, `spec.secretKey`, `spec.parameters`).
2. A `Run` manifest (our kind) lists tasks with `dependsOn`, the artifacts each task produces and consumes, and a
per-task budget (`usd`, `calls`); it contains nothing a task needs to execute.
3. A manifest that violates its schema is rejected before anything runs, with the field named.
4. A DeepSeek model is expressed as a `Model` with a provider adapter noted in the manifest; no ax field is
repurposed to carry it.
### Requirement 2: A task is isolated, idempotent and resumable
**User Story:** As the maintainer, I want a stopped or failed run to resume without repeating finished work and
without ever recording partial work as a result.
#### Acceptance Criteria
1. A task reads only its declared inputs and the bound workspace, and writes only its declared artifacts.
2. Work is recorded per unit (file, candidate) as it completes; a restarted task skips completed units and
repeats unverified ones.
3. A task whose model calls fail on account or authentication errors stops at once with a distinct status;
its artifacts are marked unfinished, never complete.
4. A task's spend is metered from the provider's usage fields and stops the task at its budget; a task with no
priced model reports "unpriced", not zero.
### Requirement 3: An ax-backed reconciler executes a Run on this host's ax
**User Story:** As the maintainer, I want to run a whole pipeline with one command on the laptop, with every
task executed by ax on the local cluster, so that the same manifests move to a bigger cluster unchanged.
#### Acceptance Criteria
1. The reconciler submits each task to ax as a `Task` (with its bound `Workspace`s and `Model`) in dependency
order, at most one engine-bearing task at a time, reads task outcome from ax's status plus the task's
delivered `summary.json`, and passes artifacts between tasks by placing a producer's outputs where the
consumer's manifest expects them. It never executes a task as a local subprocess; ax is the only executor.
2. Task status (`pending`, `running`, `suspended`, `done`, `failed`, `unfinished`) is persisted and shown by
one status command.
3. A suspended or interrupted run resumes from persisted status (Requirement 2.2) with the same command.
4. The reconciler is under 500 lines and holds no pipeline logic: no prompts, no scoring, no model calls.
5. `ousast plane doctor` verifies the host's ax in one command: kind cluster reachable, Agent Substrate control
API up, ax controller ready, runner image loaded; each check names what is missing and how to bring it up.
6. Bring-up on this host (kind + Agent Substrate + ax) is scripted, documented with its measured memory and
CPU footprint, and smoke-tested with one trivial Task before any pipeline task runs.
### Requirement 4: The runner contract is ax's
**User Story:** As the maintainer, I want each task entrypoint to run unchanged inside an ax task container
later.
#### Acceptance Criteria
1. An entrypoint receives its task and workspace manifests as YAML in `AX_TASK_YAML` and `AX_WORKSPACES_YAML`
and takes nothing else from the command line except what the manifest's `command` states.
2. The runner image serves `/healthz` and `/readyz` on port 80, `/readyz` returning 503 until workspaces are
materialised, as ax's runner contract requires; the image contains this package and is loaded into kind.
3. One fixture test per task type runs the entrypoint against a small workspace and checks its artifacts.
4. Because ax has no artifact channel, the runner delivers the task's output directory (including
`summary.json` and `units.jsonl`) to the reconciler's artifact endpoint named in `OUSAST_ARTIFACT_URL`
before it reports completion; a delivery failure is reported as `failed`, never as done.
5. The command never runs in Agent Substrate's golden-snapshot boot, which executes the same image with the same
task environment (observed on the live cluster, 2026-09-29): the runner prepares workspaces, reports ready and
waits; the reconciler starts the command with one request to the task's actor through Substrate's router
(`ate-target-actor: <atespace>/<task>`), which never addresses the golden actor. A second start request for
the same task is refused.
6. Provider credentials reach a task only in that start request, held in the runner's memory and passed to the
command's environment; they never appear in a manifest, a rendered Task, an ax template or any file.
### Requirement 5: `repo-facts` is the memory layer
**User Story:** As the maintainer, I want per-repository facts computed once and reused by every hunt and pass,
so that verification stops paying tool turns for the same lookups.
#### Acceptance Criteria
1. `repo-facts` produces, without model calls, per product file: the functions defined, and for each candidate
function the call sites in other product files (path and line) -- from source text only, never from git.
2. The artifact is deterministic for a given workspace and is reused across tasks and passes unchanged.
3. `verify` includes each candidate's callers from `repo-facts` in its prompt and reports the number of tool
turns per hunt.
### Requirement 6: The first increment is measured before the rest moves
**User Story:** As the maintainer, I want proof that the restructuring saves tokens without losing recall
before the remaining concerns are converted.
#### Acceptance Criteria
1. `repo-facts` + `verify` run on the 46-candidate validation set through the reconciler on ax, two passes, and
report cost per candidate, tool turns per hunt and declared sites agreed.
2. Acceptance: declared sites agreed at least 16 of 20 (the current value) and cost per candidate below the
current $0.022 for two passes; otherwise the increment is reported as not met and the design revisited.
3. The development transcript measure (`benchmarks/dev/token_report.py`) is run at the end of the increment and
recorded next to the result.
4. After the first measurement (plane-increment-1.json: 14/20 agreed, 19/20 in either pass), agreement is decided
2-of-3 with a third pass run only on candidates passes a and b dispute; the gate of 6.2 is re-evaluated on that
decision, and the result is recorded as plane-increment-2.json next to the first.
### Requirement 7: Routing is a declared concern of the plane
**User Story:** As the maintainer, I want which model serves which concern to be a manifest decision with a
budget, not a prompt trick inside one conversation, so that token spend is structured by component.
#### Acceptance Criteria
1. Every `Task` in a `Run` binds exactly one `Model`; a Run may bind different Models to different tasks
(a cheap model for reads and triage, a stronger one for verification and judgement), and the reconciler
passes the bound Model's identity and prices to the task.
2. A task never chooses or switches its model at runtime; the Model it was bound to is recorded in its
`summary.json`.
3. `ousast plane status <run>` prints a token attribution table: one row per task and Model binding with
calls, input tokens, cache-hit tokens, output tokens and USD, a total row, and a per-Model subtotal; every
number comes from the provider's usage fields recorded in the tasks' `summary.json`, never estimated. A
`--units` flag breaks a task down per unit (file or candidate) from `units.jsonl`. The same table is
written to `attribution.json` in the run directory so runs can be compared.
4. A task that needs a second model for a sub-step (a judge over a verifier's output) is a separate task with
its own Model binding, never a second client inside one task.