This plan is aligned with:
quay/enhancements#42, which assigns classifier configuration, training examples, model generation, scan execution, run history, match records, quarantine state, review workflows, remediation history, and redaction history toquay-service-tool.quay/quay#6154, which limits Quay runtime behavior to repository description ingress evaluation using a local JSON Bayesian classifier artifact.michaelalang/spam-classifications, which is useful as seed-data and training-flow prior art but must not become a Quay runtime dependency.
Important boundary decisions for this repository:
- Quay must not call
quay-service-toolon the request path. quay-service-toolgenerates a versioned JSON artifact for the Quay image build to bake into the image.- Broad scans read directly from the Quay database through a read-only user or replica, with a read-only session enabled where the database supports it.
- Approved quarantine, restore, and redaction use explicit direct Quay database writes, not user-scoped Quay repository APIs.
- Remediation must use conditional writes and retry-safe service-tool state updates because the Quay DB and service-tool state DB do not share one ACID transaction.
- Initial auditability is through service-tool logs and service-tool state, not a new Quay audit API.
The service tool currently has a small Flask-RESTful backend and PatternFly frontend:
- Backend tasks live under
backend/tasks/and are registered inbackend/app.py. - Existing endpoints use
@login_required,@verify_admin_permissionsor related decorators, and@log_responsefrombackend/utils.py. - The backend configures Quay's Peewee database from
DB_URIat startup. - There is no service-tool-owned durable state database or migration framework in the repository today.
- Frontend routes are declared in
frontend/src/app/routes.tsxand use role-gated navigation infrontend/src/app/AppLayout/AppLayout.tsx. - API calls use
frontend/src/services/HttpService.tsx.
The implementation therefore needs to add service-tool state and migrations before adding scan history, classifier history, or quarantine workflows.
The generated classifier artifact must be JSON and compatible with the Quay
local reader from quay/quay#6154. The initial service-tool trainer should
emit at least:
{
"version": "2026-06-20.1",
"spam_prior": 0.5,
"ham_prior": 0.5,
"token_spam_counts": {},
"token_ham_counts": {},
"spam_token_total": 0,
"ham_token_total": 0,
"vocabulary_size": 1,
"smoothing": 1.0,
"ingress_threshold": 0.9,
"ingress_thresholds": {
"public": 0.9,
"private": 0.98
},
"feature_config": {
"token_pattern": "[a-z0-9][a-z0-9_-]*",
"include_repository_name": false
},
"training_corpus_version": "..."
}The trainer also writes a sidecar checksum file or API response field containing
the SHA256 of the exact JSON bytes. Quay loads the baked JSON artifact from
/conf/spam-detection/classifier.json by default and verifies
SPAM_DETECTION_CLASSIFIER_VERSION and optionally
SPAM_DETECTION_CLASSIFIER_SHA256.
The active service-tool policy is the source of truth for the ingress threshold embedded in the artifact. Training and artifact export must write a new versioned artifact when the active policy threshold changes, so Quay never has to consult service-tool on the request path.
For production handoff, service-tool must support exporting an additional copy
of the artifact to an explicit build output path. The Quay image build copies
that JSON file and its .sha256 sidecar into the image at
/conf/spam-detection/classifier.json. The initial implementation should not
require runtime artifact downloads, shared mutable volumes, or calls from Quay
pods to service-tool.
Add these backend modules:
backend/spam_detection/__init__.pymodels.pyfor service-tool-owned Peewee models.database.pyfor the service-tool state DB connection and migration helpers.migrations.pyfor idempotent schema creation or a small migration runner.classifier.pyfor tokenization, training, artifact serialization, artifact loading, scoring, and explanations.training_import.pyfor reviewed-label and seed CSV import.quay_db.pyfor explicit read-only and write-capable Quay DB connection helpers, including session-level read-only protection for scan/preview connections where supported.scanner.pyfor cursor-paginated repository scans.remediation.pyfor quarantine, restore, dismiss, and redact state transitions and Quay DB mutations.schemas.pyfor request validation and response shaping.
backend/tasks/spam_detection.pyfor Flask-RESTful resources.backend/cli.pyorbackend/spam_detection_cli.pyfor command-line entry points used by CronJobs and operators.
Register resources in backend/app.py:
GET /spam-detection/healthGET /spam-detection/classifiersPOST /spam-detection/classifiersPUT /spam-detection/classifiers/<uuid>POST /spam-detection/classifiers/<uuid>/training-examplesPOST /spam-detection/classifiers/<uuid>/import-csvPOST /spam-detection/classifiers/<uuid>/trainPOST /spam-detection/classifiers/<uuid>/export-artifactGET /spam-detection/policyPUT /spam-detection/policyPOST /spam-detection/previewPOST /spam-detection/runsGET /spam-detection/runsGET /spam-detection/runs/<uuid>/matchesGET /spam-detection/reviewGET /spam-detection/auditPOST /spam-detection/review/<uuid>/quarantinePOST /spam-detection/review/<uuid>/restorePOST /spam-detection/review/<uuid>/reopenPOST /spam-detection/review/<uuid>/dismissPOST /spam-detection/review/<uuid>/redact
Extend backend/config/config.yaml and app startup handling with:
SPAM_DETECTION_STATE_DB_URI: service-tool-owned state database.SPAM_DETECTION_READONLY_DB_URI: read-only Quay user or replica for preview, scans, run-history enrichment, and training candidates.SPAM_DETECTION_WRITE_DB_URI: write-capable Quay DB path for approved quarantine, restore, and redaction.SPAM_DETECTION_S3_BUCKET: bucket for generated JSON artifacts.SPAM_DETECTION_S3_PREFIX: key prefix for generated JSON artifacts.SPAM_DETECTION_BATCH_SIZE: default200.SPAM_DETECTION_SLEEP_BETWEEN_BATCHES: default0.5.SPAM_DETECTION_SCAN_DRY_RUN: defaulttrue.SPAM_DETECTION_RESCAN_TERMINAL_RECORDS: defaultfalse; unchanged dismissed, restored, and redacted records remain closed until their description or active classifier artifact changes.SPAM_DETECTION_MAX_REPOS: default0for unlimited.SPAM_DETECTION_INCLUDE_PRIVATE: defaultfalse.SPAM_DETECTION_QUARANTINE_DESCRIPTION: standard quarantine notice that tells repository owners spam detection removed the description, gives the restore contact path, states owner remediation expectations, and names the expected review timeline.SPAM_DETECTION_ROLE: read/report/preview access.SPAM_DETECTION_REMEDIATION_ROLE: write/remediation access.
Do not swap Quay's global Peewee connection between replica and primary during requests. Use separate connection objects or direct SQL helpers for spam detection read/write paths.
Implement service-tool-owned tables. These are not Quay application tables.
iduuidnameenabledtraining_corpus_versionartifact_versionartifact_sha256artifact_pathmodel_snapshot_jsonfeature_config_jsonscan_thresholdingress_threshold: default threshold used when training/exporting outside the active policy.created_atupdated_atcreated_byupdated_by
Indexes:
enabled, updated_at- unique
uuid - unique nullable
artifact_version
iduuidclassifier_idrepository_idnamespace_namerepository_nametextlabel:spamorhamsource:manual_review,review_action,csv_import, orseed_importsource_ref: action-history UUID for review-derived examplescreated_bycreated_atinvalidated_atinvalidated_byinvalidation_reason
Indexes:
classifier_id, label, created_atsource, created_atrepository_id, created_at
iduuidactive_classifier_idscan_thresholdingress_threshold: source of truth for active Quay ingress artifacts.include_privatepublic_only_defaultscan_empty_repositories_only: always enforced for scan matches and review eligibility.scan_filters_jsonquarantine_descriptionscan_dry_runmax_reposbatch_sizesleep_between_batchesrescan_terminal_recordscreated_atupdated_atupdated_by
Keep a single active policy initially. Store complete snapshots on runs and actions so historical decisions remain explainable.
iduuidsource:manual,cronjob, orclidry_runstatus:running,completed, orfailedstarted_atcompleted_atclassifier_snapshot_jsonpolicy_snapshot_jsonrepos_scannedrepos_matchedrepos_flaggedrepos_quarantinedrepos_skipped_terminalerrorcreated_by
Indexes:
started_atstatus, started_at
iduuidrun_idrepository_idnamespace_namerepository_namevisibilitydescription_excerptclassifier_scoreexplanation_jsonis_emptyhard_filter_resultsquarantine_record_idcreated_at
Indexes:
run_id, classifier_score, idrepository_id, created_at
iduuidrepository_idnamespace_namerepository_namevisibilitystatus:flagged,quarantined,restored,dismissed, orredactedoriginal_descriptionquarantine_descriptionredacted_descriptionclassifier_scoreclassifier_snapshot_jsondescription_fingerprintterminal_classifier_snapshot_jsonterminal_description_fingerprintrun_idmatch_idcreated_atupdated_atactioned_byactioned_at
Indexes:
status, classifier_score, idrepository_id, status
Application validation must prevent more than one active flagged or
quarantined record for the same repository.
iduuidquarantine_record_idaction:flag,quarantine,restore,reopen,dismiss,redact,train,import,policy_update,artifact_exportfrom_statusto_statusoperatorcreated_atdetails_json
Indexes:
quarantine_record_id, created_ataction, created_at
Use an in-repository lightweight multinomial naive Bayes trainer rather than adding a runtime classifier service. Avoid adding scikit-learn to Quay or to the generated artifact contract.
Training flow:
- Load approved
spam_training_examplerows for the selected classifier. - Optionally import seed CSV files shaped as
text,label, includingmichaelalang/spam-classificationsCSV exports. - Validate labels as
spamorham. - Tokenize with the same configurable regex that Quay uses by default.
- Count spam and ham tokens.
- Compute spam and ham priors from example counts.
- Store model identity, S3 URI, and checksum metadata in service-tool state.
- Write canonical JSON with deterministic key ordering to S3.
- Compute SHA256 over the exact bytes written.
- Record an
artifact_exportaction-history row.
The first implementation should include repository description text only by
default. Optional repository-name tokens can be controlled by
feature_config.include_repository_name. The tokenizer regex should remain the
fixed reviewed default until a regex safety strategy suitable for Quay's
request path is available.
Scanning must read from the Quay DB directly, not from Quay APIs.
Query shape:
SELECT ...
FROM repository ...
WHERE repository.id > :last_seen_id
ORDER BY repository.id
LIMIT :batch_sizeRules:
- Default scan scope is public repository descriptions only.
- Private repositories are excluded unless
SPAM_DETECTION_INCLUDE_PRIVATEor the policy draft explicitly enables private scanning. - Repositories with pushed image content are always excluded from preview results, scan match history, and flagged review records.
- Use cursor-based pagination over repository IDs.
- Avoid offset pagination.
- Avoid per-repository tag queries; prefetch emptiness/tag-existence for the current batch.
- Persist hard-filter results for each match so operators can see the objective eligibility checks that allowed the classifier decision to matter.
- Persist
spam_scan_runandspam_scan_matchrows for scans. - Preview uses the same read path and classifier but does not persist run, match, or quarantine rows.
- Dry-run scans persist run and match history but do not open quarantine records and do not mutate Quay data.
- Non-dry-run scans may open
flaggedreview records but still require human approval for quarantine, restore, dismiss, and redaction. - A matching terminal
dismissed,restored, orredactedrecord suppresses a new review record when its description fingerprint and terminal classifier artifact version/checksum still match. A changed description, changed artifact, orrescan_terminal_recordspolicy override reopens eligibility.
The exact repository visibility join must be validated against the Quay models
available through the pinned quay dependency before implementation. If model
APIs are awkward for separate read/write connections, use narrow parameterized
SQL and keep it isolated in backend/spam_detection/quay_db.py.
Remediation is state-machine driven and transactional.
Allowed lifecycle:
flagged->quarantinedflagged->dismissedquarantined->restoredrestored->flaggedquarantined->dismissedquarantined->redacted
Quarantine:
- Open a service-tool state transaction.
- Lock or refresh the quarantine record.
- Validate status is
flagged. - Open a write-capable Quay DB transaction.
- Re-read the Quay repository row by
repository_id. - Preserve the latest original description if not already preserved.
- Write the configured quarantine description directly to the Quay repository row.
- Commit Quay write, then update service-tool status and action history.
- Log operator, repository ID, namespace/name, previous status, new status, and classifier score.
- Persist the reviewed original description as a
spamtraining example linked to the action-history UUID.
Restore:
- Require status
quarantined. - Write
original_descriptionback to the Quay repository row. - Mark service-tool state
restored. - Add action history and log entry.
- Persist the restored description as a
hamtraining example.
Reopen:
- Require status
restored, remediation permission, and an operator reason. - Recheck that the Quay repository exists, is active, and has no pushed image content.
- Mark the existing record
flaggedso normal quarantine confirmation remains required. - Add
restored->flaggedaction history with the operator reason. - Invalidate the
hamtraining example created by the mistaken restore so the next training run does not consume contradictory feedback.
Dismiss:
- Allow from
flaggedorquarantined. - Do not mutate Quay data.
- Mark service-tool state
dismissed. - Add action history and log entry.
- Persist the reviewed description as a
hamtraining example.
Redact:
- Require status
quarantined. - Write an explicit redacted description or
NULL, depending on policy. - Mark service-tool state
redacted. - Preserve action history but treat content restoration as intentionally no longer available through the normal restore action.
- Persist the original description as a
spamtraining example.
Review actions do not train, export, or deploy a classifier. The normal train operation consumes all stored review-derived examples for that classifier on the next operator-initiated model build.
Because a single ACID transaction cannot reliably span two different database connections, implementation should make operations idempotent and record enough state to safely retry or reconcile if the service-tool state update fails after the Quay write succeeds.
Add command-line entry points that can run inside the service-tool image:
uv run python -m spam_detection_cli init-state-dbuv run python -m spam_detection_cli import-csv --classifier <uuid> --path <csv> --source seed_importuv run python -m spam_detection_cli train --classifier <uuid> --artifact-version <version>uv run python -m spam_detection_cli export-artifact --classifier <uuid>uv run python -m spam_detection_cli export-artifact --classifier <uuid> --output-path <quay-build-context>/spam-classifier.jsonuv run python -m spam_detection_cli scan --source cronjob --dry-runuv run python -m spam_detection_cli scan --source manual --max-repos <n>uv run python -m spam_detection_cli healthcheck
The scheduled scan path should be an OpenShift CronJob invoking the CLI scan entry point, not a Quay worker and not an always-running scanner in each Quay pod.
Manual scans started through the service-tool API must be bounded by
SPAM_DETECTION_API_SCAN_LIMIT; unbounded production scans should use the CLI
or scheduled CronJob path.
Add:
frontend/src/app/SpamDetection/SpamDetection.tsxfrontend/src/app/SpamDetection/SpamDetection.test.tsx- optional small child components for classifier, policy, preview, runs, and review queue sections.
Register a route in frontend/src/app/routes.tsx:
- label:
Spam Detection - path:
/spam-detection - permission:
window.SPAM_DETECTION_ROLE || process.env.SPAM_DETECTION_ROLE
Render SPAM_DETECTION_ROLE and SPAM_DETECTION_REMEDIATION_ROLE through
backend/app.py and backend/templates/index.html.
Initial UI sections:
- Classifier: list classifiers, thresholds, artifact version, SHA, train/export actions.
- Policy: edit scan threshold, ingress threshold, public/private handling, dry-run, max repos, batch size, and quarantine notice.
- Preview: run a read-only preview with filters and paginated matches.
- Runs: list scan runs and drill into matches.
- Review Queue: show flagged, quarantined, and restored records and run quarantine, restore, reopen, dismiss, or redact actions with confirmation.
- Audit: list review transitions with repository, operator, and timestamp.
Backend tests:
- state DB migration/schema creation and indexes;
- classifier training from manual examples;
- seed CSV import with
text,labelrows; - deterministic artifact JSON and SHA256;
- artifact compatibility with Quay #6154 expected fields;
- preview does not write run, match, quarantine, or Quay repository rows;
- scans use cursor pagination and public-only default;
- private repositories are skipped unless explicitly enabled;
- dry-run scan persists run/match rows without quarantine records;
- non-dry-run scan opens flagged records but does not mutate Quay content;
- unchanged terminal records are suppressed unless policy, description, or active artifact identity changes;
- invalid review lifecycle transitions fail;
- quarantine/restore/reopen/redact use Quay DB helpers and update state history;
- review actions persist linked training feedback used by the next train run;
- reopening requires a reason, rechecks empty-repository eligibility, and invalidates mistaken restore feedback;
- remediation role gating differs from read/preview role gating;
- healthcheck reports state DB, read-only Quay DB, and write DB status.
Frontend tests:
- route visibility by spam detection role;
- classifier list and train/export states;
- policy editing and validation;
- preview loading, error, empty, and result states;
- run history and match drilldown;
- review action confirmation, restored-record reopen, and API errors.
- Add service-tool spam detection config keys, roles, and health surface.
- Add service-tool state DB connection and migration runner.
- Add service-tool state models and unit tests.
- Add classifier training, CSV import, artifact export, and tests.
- Add read-only Quay DB scan query helpers and preview API.
- Add scan runner, scan CLI, run/match persistence, and tests.
- Add remediation state machine and direct write DB helpers.
- Add review APIs and role gates.
- Add frontend route and the initial classifier/policy/preview/runs/review views.
- Add deployment notes for the CronJob and artifact distribution to Quay.
- Service-tool-owned spam detection state uses
SPAM_DETECTION_STATE_DB_URI. - Approved quarantine replaces
Repository.descriptionwith the configured standard quarantine notice. - The first implementation includes backend APIs, CLI commands, and the PatternFly operator UI.
- Public/private scanning uses Quay's
repository.visibility_idtovisibility.namerelationship. - Generated classifier artifacts are exported as JSON plus
.sha256sidecar files for the Quay image build to bake into the image.