Repository navigation
Address issues encountered during new provider turn up - #722
Merged
Merged
Conversation
A lane took its via cell from the row's pooled match types and upstreams but its result from the first match detail, so binutils' exact-direct match showed "via source package binutils". With a raw match, every cell now reads its first detail, and "via" shows only for an exact-indirect detail that searched by another package's name. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A run whose config gives grype by path records spec {name, path}, no
version, so the gate's setup lines raised KeyError. They now show the
version, else the path.
Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Research on a CLEANSTART-* id ran the whole agent, then propose refused its call because CLEANSTART is in neither advisory-ids nor vulnerability-namespaces. POST /api/research now runs the same check against each job's gate vocabulary and refuses the request before any job is queued. The refusal now says which list to add the namespace to. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Phase 4 kept openQuestions empty unless the verdict was unclear, so a tp or fp report's open questions never reached the finding page, which reads only proposal.json (CLEANSTART-2026-IC12241 lost two). Every open question the report lists now goes into openQuestions, on any verdict. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A set read one source's vocabulary.yaml whole: the writable source's, else the first that had one. So a provider's own claims couldn't list a new namespace without copying the published vocabulary. Now every source's vocabulary.yaml is optional and they merge in order: the lists are unioned, and a later source may add justifications and rules. A key two sources define differently is an error naming both sources, as is a namespace in vulnerability-namespaces that an advisory-ids pattern matches. claims validate and vex import, which take one corpus directory, still read that directory's own vocabulary. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Refusing an id whose namespace is in neither list only said to add it somewhere. Where the gate's rows are at hand (POST /api/research and claims agent propose), the refusal now counts the vulnerabilities each of the scan's ids in that namespace names: an id naming at most one is a disclosure, so the namespace goes in vulnerability-namespaces; one naming several is an advisory, so it goes in advisory-ids. The refusal gives the count, one example, and the writable source's vocabulary.yaml to edit. An id's same-namespace related ids are siblings, not names, and its CVEs count when it has any, so a CVE and its GHSA count once. With no scan, the refusal names both lists and an additional-claims vocabulary.yaml. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
The research refusal for a namespace no vocabulary lists was plain text,
and only POST /api/research checked it: the per-gate research route,
which the finding page's Research button uses, started the job anyway.
Both routes now refuse with an UnlistedNamespace body (code
unlisted-namespace): the namespace, the list the scan suggests, how many
of its ids were checked, an example, and the vocabulary.yaml to edit,
relative to the workspace. A gate's state lists the same for its open
rows' namespaces.
POST /api/gates/{gate}/vocabulary/namespace adds a namespace to a list
of the writable claims source's vocabulary.yaml, under the label-write
lock. It creates the file when the source has none, and otherwise edits
the YAML tree in place, keeping comments, order and styles. It refuses
a namespace that would be listed as both kinds, and a gate with no
writable source. The next request's view reads the change, since a view
is rebuilt when a local source's files change.
Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Research refused for a namespace the claims vocabulary lacks showed as a red toast that disappeared before it could be read, with nothing to click. Every research start (the finding's Research, a table's, the research page's, the unlabeled tab's and a job's Retry) now opens a dialog that stays until answered. It says what the gate's scan shows (e.g. CLEANSTART-2026-GU36052 → CVE-2025-11082), offers the suggested list as the primary button and the other beside it, and names the vocabulary.yaml it writes. Adding writes the file and starts the same research again, with errors shown in the dialog. A gate's page and the gates page's work banner show the same notice and add buttons for open rows in an unlisted namespace, before anyone starts research. A toast longer than a line now stays until its close button is clicked. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
The footer's three buttons overflowed the dialog, pushing Cancel past its left edge. Wrap them, right-aligned, and shorten the add buttons to "Add as disclosure" / "Add as advisory"; the body already says namespace. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A research job whose command exited 0 was done, whether or not the agent recorded a call. The CleanStart turnup's jobs read "researched, no hint" after propose refused the namespace, or after headless claude was denied every write, with nothing saying either. A job that exits 0 is now done only when a proposal or cause stamped with its id was recorded since it started; otherwise it is not-recorded. Its error says why: propose's refusal, which propose now leaves in the job's directory (propose-error.txt), else the tool uses the agent was refused (the result line's permission_denials), else that it never proposed. The job keeps the agent's last message. A job recorded as done before this, which recorded nothing, reads as not-recorded the same way. A row's research summary carries the job's cost. The row's detail carries what the job's agent wrote under .yardstick/research/<vuln>/<pkg>/: its report, and its proposal when that holds a verdict. POST research/record records that proposal through propose, in the job's environment, without researching again. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
The finding page offered only "Research again", with nothing saying research had run or why it left no hint. Its new research section heads with when the job ran, on what model and for how much, then says what came of it: recorded; concluded tp · fix-newer · high but not recorded, and why; ran but wrote no result, and why; running; failed. It shows REPORT.md, the open questions and the agent's last message. When the research wrote a proposal it didn't record, Record as hint records it without researching again, and a refusal stays in the section rather than a toast. Tables say "researched, not recorded", with why on hover, in place of "researched, no hint", and the jobs page says not recorded with why. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A not-recorded job's reason listed the agent's first three refused tool uses. In the CleanStart turnup those were gh api calls, which left the denied writes to REPORT.md and proposal.json under "and 2 more". Writes and propose calls now lead. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
The finding page looked for a job's report and proposal under a package directory named by packageurl-go. The plugin names it with its own parse_purl regexp, and the two disagree on Go modules: for golang.org/x/net the plugin writes x_net, packageurl-go says net. They also disagree on a subpath holding a '/'. OutputDir now uses the plugin's regexp and unquote, so it names the same directory: one optional namespace segment, then the name up to @, ? or #, falling back to the whole purl when it doesn't parse, and to "all" when there is none. A table test pins the plugin's own outputs for apk, deb, scoped npm, maven, Go modules, subpaths and qualifiers. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Research jobs from the label UI did their research and then were refused every write (friction log entry 10: CLEANSTART-2026-GU36052 and -IC12241, about $0.29 each, nothing recorded). The cause, reproduced with `claude -p` on haiku: Claude Code resolves a relative path rule such as Edit(.yardstick/research/**) against the session's current directory, and that moves when the agent runs `cd` in Bash (allowed without a rule, as a read-only command). The CleanStart runs did `cd .yardstick/research/<vuln>/<pkg> && cat SUMMARY.md`, and every Write after that was refused. Without the cd the same rule allows the write. It is not the guess in the log (rules resolved against the repo root), and not the user settings rewrite: neither changes the result. So the rule is absolute: Edit(/$YARDSTICK_ROOT/.yardstick/research/**), which expands to Claude Code's "//<path>" form. Verified: it allows the write after a cd, still refuses a write outside .yardstick/research, and works with a space in the path. Because the workspace path now lands inside the --allowedTools string, Start refuses a path with characters that could end the rule early, widen it as a glob, or break the shell quoting (a workspace named "x),Bash(*" would otherwise grant a shell). The workspace is the user's own cwd, so this is defence in depth rather than a reachable injection. Deliberately not done: running research without the user's settings (--setting-sources/--restricted). That would also drop the user's own deny rules and the plugin's enablement. The grants yardstick passes now suffice on their own, so the user's settings can only add to them. gh and curl stay ungranted: the agent reads pages anyone can write, and WebFetch covers what they were used for. The skill now says to write only with Write/Edit by absolute path and never to cd, so an interactive run (whose skill rule is still relative) doesn't hit the same trap. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A branch build of yardstick is stamped like v0.17.0-my-branch, and the plugin it ships can only be plain 0.17.0 (a plugin version is plain semver). The exact-match check turned research off for every branch build, and said why only in one line on the serve terminal, while the UI just offered no research (friction log entry 8). The workaround was to stamp the branch build as the release, which hides which build runs. Now a build whose base version (without the -/+ suffix) matches the installed plugin runs research, with a note on the terminal and the gates page that research runs the installed plugin, which may not be this build's. A build of another release is still refused: its plugin's skills and the binary's CLI and policy may disagree, which is what the check exists for. A dev build without a version still takes any plugin. Why accept rather than warn-and-refuse: refusing left no supported way to try a branch's research at all, and the risk (a plugin a patch behind its binary) is the same one a dev build has always been allowed. When research is off, the gates page now shows the reason and the command to run, since the tables then have no research control to hover. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Trying a branch's research skills meant re-pointing the user's global Claude Code plugin marketplace, which changes every session, and (as the plugin's version can't differ from the release's) only an uninstall and reinstall picked the change up (friction log entry 9). claims serve --research-plugin-dir <dir> now gives research jobs `--plugin-dir <dir>`, which Claude Code loads for that run only and which wins over an installed plugin of the same name (checked with claude -p: the directory's /yardstick:research-vuln ran, not the installed one). The directory reaches the command through YARDSTICK_PLUGIN_DIR, quoted, never through the command text, like every other job value. The version check reads the directory's .claude-plugin/plugin.json instead of `claude plugin list`, and refuses a directory that isn't a yardstick plugin. DEVELOPING.md now says how to try a branch in another repo's gate (entry 7): build it with its UI via the existing make snapshot:single-target (go-make's goreleaser task, rather than a new build task), copy it over .tool/yardstick, and point research at the clone's plugin. Changing binny or vunnel's make targets is vunnel's side and not done here. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
golangci-lint (v2.14.0, the repo's pin) found six issues on the branch: - gosec G703 on propose's refusal note: the path comes from YARDSTICK_RESEARCH_JOB_DIR. The agent can't set that variable (its Bash rule only matches commands that start with `yardstick claims agent propose`), but a stray value would have let propose write or delete propose-error.txt anywhere. It now acts only on a directory shaped like .yardstick/research/jobs/<id>, and the nolint says why. - funlen on Runner.Start: the request checks moved to Runner.check. - goconst: the stream-json "result" line type is a constant. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
With --research-plugin-dir the startup note read "runs the yardstick plugin in <dir> 0.17.0, which may not be this build's": a garbled string replace, and a caveat that doesn't fit a directory someone named on purpose. It now says which version runs from where, and the gates page's notice title fits both cases. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A real headless run on CLEANSTART-2026-GU36052 (the branch build, the branch's plugin, a copy of vunnel's workspace) now records its proposal: Write and Edit under .yardstick/research go through. Its only refusals were two curl calls, which are deliberately not granted (the agent reads pages anyone can write). Saying so up front saves the turns spent on them. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
These Playwright specs were written on this branch while Chromium couldn't
launch on the host; make e2e:container (docker) runs them. Three fixes:
- namespace-dialog: Basecoat fixes a dialog's child in place, so the
<dialog> itself has no box and Playwright calls it hidden. The testid
moves to the box, as the command palette's already is.
- The e2e reset deleted the workspace while the previous test's research
job was still writing into it ("restore workspace" 500). The seed now
stops the old server and waits for its jobs first; testkit.NewServer
returns the runner to make that possible.
- research-everywhere: a queued job's finding page polls, so a faked
state route could be mid-flight as a test ended. Routes are dropped
with ignoreErrors after each test, as Playwright advises.
Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Jobs() kept the id written inside job.json, and that id becomes a path: the job's log, its saves, and Record. A record naming another id, or a path, could point them elsewhere. A job's id is now its directory's name, whatever its record says. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
propose filled author.researchJob from YARDSTICK_RESEARCH_JOB only when the call left it empty. A job counts as having recorded something by that stamp, so an agent could name another job and mark it recorded. The stamp is now always the job propose runs in, and empty outside one. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A not-recorded job counted as researched (its run set LastDone), so batch research and a table's Research skipped its rows. After someone adds CLEANSTART to the vocabulary, "research all" has to pick up the rows propose refused; the price shows before it starts. LastDone now marks only a run that recorded a call, so a not-recorded row is researched again, with the rest. Jobs() read the agent store, and an old job's log, on every call, every 2s poll, to settle done jobs from before yardstick told done from not recorded. Each job is now settled once and saved: a done job that recorded a call is marked recorded, and one that didn't becomes not-recorded, dropping the LastDone its own run set. Later reads ask nothing and read no log. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
…'s way Record could not record a job from before yardstick told done from not recorded: it loaded the record from disk, which still said done, while the page showed the reconciled status. It now judges the job as Jobs reads it. It recorded whatever proposal-*.json the folder held, else proposal.json. The folder is the vulnerability's and package's, with no subject, so another subject's job shares it, and an agent could plant a proposal naming another row. Record now sends proposal.json alone, and only when it is about one of the job's rows and written since this run started. The page shows the same: a verdict, a report or Record as hint only from this run, and a proposal on another row as another finding's research. It took no lock and didn't check for a live job, so a rerun queued while propose ran could be saved over as done. It now refuses a live job, and checks again under the runner's lock before saving. The route also checks that the job researched under the gate asked about. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
With no refusal left by propose and no refused tool use, a not-recorded job said "the agent ended without proposing". For a job from before propose left its refusal, that is false: the CleanStart runs did propose, and propose refused them. It now says "nothing was recorded; the log says what happened", which is true either way. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
AddToList rebuilt a comment-only vocabulary.yaml from nothing, dropping its comments: yaml.v3 keeps no comments for a document with no node. It also refused a file that is only "---" as not a mapping, though loading reads it as an empty vocabulary. Comments alone are now kept as written with the list after them, and an empty document becomes a mapping holding the list. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
9fa5048 made a lane read one match detail, so binutils' CleanStart match no longer says "via source package binutils". Without a raw match the lane falls back to the row's upstreams, and that path still took the first one, the package's own name (found by the adversarial review). It now takes the first upstream that names another package. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Two findings from an adversarial review of this branch: 1. The agent could write job records. The Edit grant covers .yardstick/research/**, which includes jobs/<id>/: job.json, which the server trusts for a job's status, rows and the paths it reads and writes, and propose-error.txt, which the UI shows as yardstick's own words. A prompt-injected agent could forge a "not recorded" job over any rows, offer a planted proposal to Record, or show a person instructions as if from yardstick. The command now also passes --disallowedTools "Edit(/$YARDSTICK_ROOT/.yardstick/research/jobs/**)". Checked with claude -p: a deny wins over the allow, also after a cd, and writes elsewhere under research still go through. propose-error.txt is still written by yardstick itself (propose), not the agent's tools. 2. The agent's propose didn't see claims sources given to claims serve by --claims. The UI adds a namespace to the last source, so with `claims serve --claims ./.claims` the dialog would add CLEANSTART, the server would accept it, and the agent's propose would refuse it. Jobs now get YARDSTICK_CLAIMS, the server's own list (clio reads it, comma-separated; checked against the CleanStart workspace's merged vocabulary), with local directories made absolute. A source with a comma is refused rather than split. Without sources nothing is set, so the workspace's .yardstick.yaml applies as before. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
The label-writing specs (hints, lists, refresh) flaked with ENOENT on a claims file that readdir had just listed: 4 failures in 6 runs of those three specs on origin/main (2e92f9d), and more on this branch. The server replaces a file by renaming a new copy over it (corpus.WriteFile), and through make e2e:container's bind mount the file can briefly look absent. That last part is an inference: ENOENT on a file that is only ever renamed over, in both trees, and only under docker. diskLabels now waits up to half a second for such a file to come back (and skips it if it really was removed), rather than turning the poll into a failure. After the change: 4/4 clean runs of the three specs, and 2/2 full runs of 246. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Two regressions from the first review's fixes, found by a second adversarial review: 1. Record passed only proposal.json to propose. For a finding over several binary packages the skill writes a copy per row (proposal-<row id>.json), so every row but the first got no hint, and the job counted all of them researched. Record now adds the copies too, but only this run's, about another of the job's rows, and with the verdict and justification the page shows, so it still records nothing the person didn't see (the first review's concern). 2. Record refused a request from a gate other than the job's. A row id is the same in every gate that scans its image, and the finding page shows the row's newest job whatever gate it ran under, so the button failed there. The check protected nothing: Record always proposes under the job's own gate. Dropped. Checked and not changed: a case variant (.../research/JOBS/...) does not get past the jobs deny rule; claude -p on macOS refused the Write. Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
A double click on Record as hint ran propose twice, and the second came back refused (the job was already done), showing an error over a record that went through. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
The job id and job directory were bare os.Getenv calls beside clio's options, with the names split between the server and propose. Both are trust anchors: the agent runs propose with whatever arguments it likes, so if either could come from a flag, it could name its own job. - they are now one options struct on propose (and propose-cause, explain propose) with no flag: fangs reads a flagless field from YARDSTICK_<key> alone, no config file is read, and there is no --config - the server writes YARDSTICK_RESEARCH_JOB through research.JobEnv, beside research.JobDirEnv Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
propose kept a record's own timestamp and author kind. In a research job both come from an agent that reads pages anyone can write, and the timestamp is the other half of the recorded check: a job counts as recorded by a call stamped with it since the job started. - a call dated ahead would mark every later run of the job recorded, so research-all would skip its rows for good; one dated back would have a job that did record re-run, and re-charged - a call claiming to be a person's isn't one In a job, timestamp is now and author.kind is agent, as the job id already is. Outside a job nothing changes. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
The agent may run `yardstick claims agent propose` with any arguments, and propose only checked that a row's package and vulnerability match it. So a prompt-injected agent could propose under another --gate (and its vocabulary), on rows the job wasn't asked about, or under another subject, and each call would carry this job's id and show as requested in the label UI. It could also skip Record and write that directly. In a research job propose now refuses: 1. a gate other than the job's (--gate naming the job's gate is fine, as Record passes it) 2. a proposal with no row, or a row not in YARDSTICK_ROWS 3. a subject other than the row's (left off, it is the row's as before) The job's gate comes from a new YARDSTICK_RESEARCH_GATE, the same value as YARDSTICK_GATE under a name no flag reads, since --gate overrides YARDSTICK_GATE. Rows and gate are read env only, with the job id. Outside a job nothing changes. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Job records (job.json, log.txt, propose-error.txt) lived under .yardstick/research/jobs/, inside the agent's Edit grant on .yardstick/research/**. Only a deny rule kept the agent's Edit and Write off them, and the plugin's helper script, which writes under .yardstick/research/<vuln>/<pkg>/, could reach jobs/<id>/ with a vuln named "jobs". That held only because none of its file names matched a record's. - records now live under .yardstick/jobs/<id>/, outside every write the agent is granted. A workspace's old .yardstick/research/jobs/ is moved there once, when the server starts and finds no .yardstick/jobs/ - the --disallowedTools rule stays, pointed at the new place. It no longer guards against yardstick's own allow rule, but the user's settings still merge their allow rules into research, and a broad one (Edit(**), say) would otherwise reach the records - propose's refusal note follows: it writes only into a directory shaped like .yardstick/jobs/<id> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
The agent wrote its report and proposal under .yardstick/research/<vuln>/<pkg>/, with no job or subject in the path, so two jobs on the same CVE and package (another image, another gate) shared REPORT.md and proposal.json, and research-all runs two at a time. The server stood in for ownership with heuristics: a file counted only when written since the run started, and a proposal naming another row showed as "another finding's research". - a job now gets YARDSTICK_RESEARCH_OUT, .yardstick/research/job-<id>/, and the helper script and the skill write there. The helper refuses a value outside the workspace's .yardstick/research/. A run outside a job keeps the <vuln>/<pkg> layout - a run clears its directory as it starts, so what is there is the last run's, and the mtime checks go - a job that never started (cancelled while queued) shows no output, rather than the run before it - the Go copy of the plugin's purl-to-directory naming goes too: the server names the directory now, instead of guessing the plugin's - a proposal is still shown and recordable only when it names one of the job's rows Another job's agent can still write into this directory, as it can anywhere under .yardstick/research/; the directory is per job for ownership, not as a boundary. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
… were The move from .yardstick/research/jobs/ ran on any start that found no .yardstick/jobs/, and that directory only appeared once a job was saved. The old place is inside the agent's write grant, so a record an agent left there would be taken in as a trusted job on a later start. - the start that checks now makes .yardstick/jobs/ right after the move, found or not, so it happens at most once - .yardstick/research/jobs/** is denied to the agent alongside the new place, absolute like the allow rule - the helper script refuses to write under research/jobs/, in any case, since a vuln named "jobs" (or "JOBS" on a case-insensitive filesystem) would otherwise land there Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Record checked proposal.json through Output, then gave propose its path, and propose read it again. Anything that rewrote the file in between (a rerun, or another job's agent, which can write anywhere under .yardstick/research/) got recorded under this job's id instead. The per-row copies were matched to the shown proposal only on verdict and justification, so their confidence, note or evidence could differ from what the person saw. - Record now pipes the proposal.json bytes Output read to propose on stdin, nothing by path - a row's copy decides only which rows get the call (it has to be one of the job's rows, with the shown verdict and justification). What is recorded for that row is proposal.json with its row swapped in Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Research keeps the user's Claude Code settings so their deny rules still apply. That also carried their defaultMode and MCP servers into a run whose agent reads pages anyone can write: acceptEdits would hand it writes anywhere, bypassPermissions everything, and an MCP server with an allow rule a tool yardstick never granted. - --permission-mode default: a headless run then refuses whatever isn't allowed, as yardstick's rules assume. On current Claude Code the mode is listed as "manual"; "default" is still accepted - --strict-mcp-config with no --mcp-config: no MCP servers. The plugin ships none and the skill uses none Not covered: the user's (and the workspace's) allow rules still merge with yardstick's, so a user who allows `gh` or a broad Edit allows it here too. Dropping them means --setting-sources without user and project, and passing the user's deny rules through by hand, which is a bigger change. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
The skill's allowed-tools grants Edit(.yardstick/research/**), which Claude Code resolves against the session's current directory, so it means the workspace only in an interactive run started there. Frontmatter can't name $YARDSTICK_ROOT, so the frontmatter now says that a job's absolute rule (and its deny on job records) on the command line is the one that holds. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
A refused tool use's input and the agent's last message were cut at a byte offset, so text with non-ASCII in it could end in half a rune and show U+FFFD. They now back up to the start of the rune, as the log tail does. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
readCapped took a single Read, which may return less than the file holds (and a pipe or network filesystem often does). It now reads until the cap or the end of the file. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Merging vocabulary.yaml across sources refused any difference in a justification or rule, prose included. So the usual github.com/anchore/claims@main plus a local checkout broke every gate the day a corpus PR reworded a rule's title, for everyone whose checkout was behind. - a justification's description and a rule's title, basis and decision: the later source wins, with a warning naming both - a rule's verdict, justification or ceiling differing is still an error, as is a namespace on both lists Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
A vocabulary.yaml that only lists a namespace adds to another source's. Merged with nothing under it, it turned strict checking on with nothing allowed, so every label was refused, and the label UI would write one when a gate had no vocabulary at all. - `Vocabulary.CheckWritable` is the one rule: no vocabulary, or only fragments, is no vocabulary to write labels against, with a message saying a fragment needs a full one under it - `FindVocabulary` and `Set.Writer` refuse on it, and adding a namespace is refused rather than writing a fragment with nothing under it - a gate on fragments alone offers no namespace adds Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
import --to read only that directory's vocabulary.yaml, so a --to holding one that only adds a namespace (as the label UI writes) refused every label. It now takes --claims and opens the app config's sources, --claims and --to in that order, writing to --to as a gate's writer does, so labels are checked against the same merged vocabulary every other command uses. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
claims validate checks one directory as a whole corpus, the way corpus CI does (catalog.yaml, SBOMs, the layout), so it doesn't take other sources to merge onto: a directory whose vocabulary.yaml lists no justifications isn't one. It now says so once, rather than refusing each label for a justification nothing lists. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
A refusal for an unlisted namespace that named no file said to add it to "an additional-claims vocabulary.yaml", so validating the published corpus pointed contributors at the wrong place. - `CheckVulnerability` takes the file: validate passes the corpus's own vocabulary.yaml, and a write the writing source's - with no file, the refusal says "a claims source's vocabulary.yaml" Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Adding a namespace read the file with os.ReadFile, which follows a symlink out of the claims source, while the set's loader and the write both stay inside it through os.Root. All three now read by the same rule. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
POST /api/gates/{gate}/vocabulary/namespace reads like a per-gate change,
but it edits the writable source's vocabulary.yaml, which every gate on that
source picks up. The route's doc says so.
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
The namespace check rebuilt pairing.RelatedIDs over every row on each research request and each GET state, which the finding page polls during research, even when every namespace was listed. A view now builds it on first use and keeps it until the view is rebuilt. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
AddToList re-encoded the whole file as a YAML tree, which keeps comments and order but drops blank lines, so on the published corpus's file adding CLEANSTART also removed the blank line after the list. - an item is now written after the list's last one, inline as ", NS" or as a line indented like the one above, and a missing list is appended at the end, so the diff is the one line - the result is parsed back and checked; anything laid out otherwise (an empty list key, an inline mapping, an escaped last item) still goes through the tree - adding a namespace that's already listed returns the file untouched - an empty document (`---`) keeps its marker, with the list after it Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
An unlisted namespace whose ids in the scan each named one vulnerability was called a disclosure namespace, and the dialog made "Add as disclosure" the primary button. A single-CVE advisory (a DSA, an ALAS) looks the same, and picking the wrong list changes pairing, since advisories never join alias groups. - `Unlisted.Suggest` is now only `advisory-ids`, when some id names several vulnerabilities; otherwise it's empty and the refusal says the scan can't tell, naming both lists - the dialog and the gate's notice say the same and promote neither button Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
…t start A research-all dry run skipped the namespace check, so the estimate looked fine and Start then refused the whole batch on the first unlisted namespace, leaving the dialog to find the rest one at a time. - a dry run's response lists every namespace a start would be refused for (`unlisted`), and the research page shows them with their add buttons - a refused start still answers with one namespace for the dialog, and its error names the others Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
not-recorded is already a justification (the reason most ported labels carry), and both show on the finding page and ride the same wire types. The job status is unrecorded now, so the two don't read as one. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
wagoodman
approved these changes
Oct 29, 2026
The panel fades in from `visibility: hidden`, so a `focus()` right after the cog's click could miss and leave Esc to walk up a crumb. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch was tested out on anchore/vunnel#1114 to see whether it has enough features that it can be used to turn up a new provider. It addresses a few issues:
Long description:
proposerefused thecleanstartIDs as an unknown namespace, and the UI only flashed a toast.I verified the fix with the real
claudeon a copy of the CleanStart workspace: two findings, both recorded, about $0.20 each.Changes
vocabulary.yamlmerges across claims sources. For an unlisted namespace, the UI offers to add it and writes the file. It suggests a list by counting the vulnerabilities each ID names:vulnerability-namespacesoradvisory-ids.Edit(...)rule against the session's current directory, so onecdbroke every later write. The rule is now absolute, and the skill says never tocd.--research-plugin-dirloads a branch's plugin for research runs only. DEVELOPING.md says how to run a branch in another repo's gate.Trust decisions
The research agent reads pages anyone can write, so it's treated as untrusted. Each commit message has the full reasoning.
cd. The job ID comes from the job's directory, andauthor.researchJobfrom the job's environment.curlandghstay blocked. The agent uses WebFetch.Known gaps (low)
subjectinstead of arowcan't be recorded from the UI.---invocabulary.yamlmay be lost when a namespace is added.Testing
go test, golangci-lint,bun testandtscpass.task e2e:containerpasses 246 of 246.