Repository navigation
·
3 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Added
- Publisher-authored Sigma passthrough with enforceable provenance (2026-09-24): a deterministic, LLM-free scanner now finds complete Sigma mappings directly in
articles.content, preserves the exact source substring and records article/source IDs, SHA-256, extraction offsets, license evidence and attribution. Source rules bypass generation and batch deduplication, receive reviewer-only similarity context, and remain immutable; edits happen on a generated copy. The queue adds a first-classlocal_review_onlystate and prominent provenance UI. Server-side single/bulk approval and the final PR preflight fail closed unless the destination repository explicitly allowlists the source license or a reviewer records source-specific permission; mixed PR batches are rejected before any Git/GitHub side effect. The schema migration and existing-article backfill are explicit, dry-run-by-default operator steps rather than startup mutations. - Catalog-driven model parameter compatibility and per-agent reasoning effort (2026-09-04): the per-agent Temperature / Top_P sliders rendered for every provider and model, so an operator could move a control that the OpenAI reasoning path dropped server-side and that the Codex adapter never sent -- and the lever that matters on current models, reasoning effort, was not configurable at all. Compatibility was a hardcoded name-prefix list that misclassified new families silently and did not know that Anthropic Opus 4.7+/4.8/5, Sonnet 5 and Fable 5 reject
temperatureoutright (the Anthropic client sent it unconditionally). A newconfig/model_capabilities.json(keyed by model id:supports_temperature,supports_top_p,effort_levels,default_effort; tiers verified against the OpenAI per-model pages and the Anthropic effort doc on 2026-09-04) is now the source of truth, resolved byget_model_capabilities(provider, model)inprovider_model_catalog.py;model_supports_variable_temperature()is a thin wrapper over it and the old prefix heuristics survive only as the fallback for unlisted ids, which get "sampling as before, no effort control" instead of an error. The v2 config contract gains an optional per-agentEffort(flat key{Agent}_effort,None= provider default; the twelve quickstart presets and the live DB row validate unchanged), the migrator lifts it, andPUT /api/workflow/configrejects a tier the resolved model does not list -- Codex tiers are live and have shipped names outside the documented vocabulary (ultra), so the contract pins the token shape and membership is checked against the catalog.llm_client.pysendsreasoning_effort(OpenAI) oroutput_config.effort(Anthropic) only when configured and listed, omitstemperaturefor models that reject it, and records the field in the forensic_provider_payload;LLMServiceresolves{Agent}_effort(extractors inheritExtractAgent_effort) so existing call sites needed no signature changes; the Codex adapter keepssupportedReasoningEfforts/defaultReasoningEffortfrommodel/listvialist_model_details()and passes the configured tier as theturn/start.effortoverride. The OpenAI temperature-rejected retry now logsCAPABILITY DRIFTnaming the model whenever the file claimed support, the production alarm for a stale value.GET /api/workflow/provider-optionsreturnsmodel_capabilitiesper provider, and the Agents page disables (never hides) an unsupported slider with a "Not supported by this model" note and renders an Effort select listing exactly the model's tiers after "Provider default", hidden when the model has none; the autosave carries{Agent}_effortand the server clears the override on the blank option. The dailyupdate_provider_model_catalogsjob diffs fetched ids against the capabilities file, prints a⚠️per gap, and the Celery task logs a WARNING and returnsunclassified_modelsso the gap shows in the Scheduled Jobs UI; therefresh-model-context-windowsskill now verifies existing entries' tier values, the part no API exposes. Docs:docs/llm/cloud-model-reference.md(new capabilities section, corrected Anthropic sampling claim),docs/reference/api.md, the scheduled-jobs report. Tests:tests/unit/test_model_capabilities_resolver.py,tests/config/test_workflow_config_effort.py,tests/unit/test_workflow_config_effort_validation.py,tests/services/test_llm_client_effort.py,tests/worker/test_update_provider_model_catalogs_task.py, additions to the Codex client, provider-options and catalog-script suites, andtests/playwright/agent_config_effort_capabilities.spec.ts. - Eval runs can be launched and polled over MCP (2026-09-03): the MCP server could read eval evidence and save diagnoses but could not start a run, so the edit-prompt -> run -> diagnose loop still needed the operator on the Agent Evals page. The launcher behind
POST /api/evaluations/run-subagent-evalnow lives insrc/services/subagent_eval_launch_service.py:plan_subagent_evalresolves the active config, canonical subagent, committed URL set, server-side replicates, fixtures, DB rows and the extractor's provider and model, and writes nothing;launch_subagent_evalperforms the writes, commit and staggered Celery dispatch the route always performed, withinitiated_byin the snapshot extras (hash-excluded) and the audit payload. The route is a thin wrapper with an unchanged path and response shape, its deaduse_active_configfield is dropped and ignored on input, and the response'seval_record_idis now populated instead of null. A newMAX_EVAL_EXECUTIONS_PER_LAUNCHcap (default 100) is enforced before any write from both callers. Two MCP tools land on top.run_subagent_evalis a caller-attested write: withconfirmed_by_userfalse it returns the plan with a billing line and writes nothing, it launches only on a freshconfirmed_by_user=trueper call, checks the broker before writing, reports fixture-less and row-less URLs as skipped rather than running an extractor inside the MCP process, and auditsevaluation.run_requestedasservice:mcp.get_subagent_eval_statusis a read-only poll keyed by run label (v5139,v5139a) that returns the same progress and metrics as the HTTP status endpoint without an eval record id. The sharedSUBAGENT_TO_EXTRACT_AGENTmap insubagent_utils.pyreplaces three private CamelCase copies.docs/reference/mcp-tools.mdno longer says that no MCP eval path bills tokens: diagnosis still spends nothing, launches bill the extractor's configured provider unless it islmstudio. The live end-to-end launch also exposed that a desktop client runs the MCP server on the host with no broker URL, where Celery's config falls back to the compose-internalredishostname;stdio_server.pynow defaultsREDIS_URLtoredis://localhost:6379/0for such runs, the same way it already assemblesDATABASE_URL, so the broker preflight passes instead of failing closed. - CI checks became CI gates (2026-09-02): eleven workflows ran on every push and PR, and none was required --
mainhadenforce_adminsset with zero required status checks,europa-devhad no protection at all, andtests.ymlskipspushevents onmain, so a direct push tomainran no tests and nothing stopped it. Branch protection onmainnow requires all fifteen existing check contexts (the six test tiers, Security Audit, Ruff, pinned versions, UX contract, dead code, mypy, CodeQL, Semgrep, secret scanning, MkDocs build) withstrictup-to-date enforcement, admins included, force-push and deletion blocked. Four new gates land with this change. (1)Lockfile Up To Daterunsuv lock --check: every other job installs with--frozen, which trustsuv.lockas-is and never compares it topyproject.toml, so a stale lock -- easy to produce, since a bareuv runre-locks silently -- passed CI. (2)Paired Surfaces(scripts/check_paired_surfaces.py, pull requests only) fails when one side of a documented pair moves without the other:src/database/models.pywithout ascripts/migrate_*.py(migrations are never auto-applied, so the runtime falls back silently and the schema change ships inert), andsrc/prompts/orworkflow_config_schema.pywithoutconfig/presets/(the runtime reads the presets, not the prompt files). Escape labelsno-migration-neededandno-preset-sync-neededcover the genuine exceptions; PR labels and the base ref are passed throughenv:, not interpolated into the shell. (3) Every committed Sigma rule must parse (tests/unit/test_committed_sigma_parses.py): the standalone fixture rules, and any complete literal example rule fenced indocs/contracts/*.md(today the contracts hold only output templates and fragments, which are skipped -- the scan exists so a literal example added later is checked automatically), go through the sameSigmaCollection.from_yaml+ condition parse the runtime applies, with a non-empty-corpus guard on the fixtures so the gate cannot go vacuous if their paths move. Motivated by a rule carrying the nonexistent|anymodifier reaching the Huntable-SIGMA-Rules repo through PR #5 -- parse-level only, deliberately: a 2026-09-02 measurement of all 838 queue rules found zero that parse but fail to compile on Splunk, ES Lucene, ES|QL, or Kusto, so backend conversion was not added. (4) Coverage ratchet forsrc.web,src.workflows, andsrc.core, which had no floor at all (the existing gates coversrc.servicesandsrc.utilsonly); floors are set at measured coverage less a small margin and are meant only to rise. The release scripts had to change with this, because they ownedmain's protection:release_lock.shwroterequired_status_checks: nullwithlock_branch: true, andrelease_unlock.shdeleted protection outright -- so the release merge, the one PR intomainthe checks exist to gate, ran with none, and the next lock would have erased the required list. Both scripts now apply a single committed payload,scripts/main_branch_protection.json, toggling onlylock_branch; unlock keeps every required check and the force-push/deletion blocks in force, with a--allow-force-pushflag reserved for the documented rollback path.tests/unit/test_main_branch_protection_contexts.pypins every listed context to a job name that still exists in.github/workflowsand every PR-eligible job to the list, because GitHub matches required checks by context string and a renamed job silently un-gates itself -- or, on a locked-then-unlockedmain, blocks every PR waiting for a check that no longer reports. The committed list carries 18 contexts (the 15 live ones plusDependency Review,Lockfile Up To Date, andPaired Surfaces); the live protection picks them up the next timerelease_unlock.shruns.europa-devgets force-push/deletion protection without required checks, since direct pushes to the integration branch are the working model. - Real MITRE ATT&CK validation for Sigma
attack.*tags (2026-09-02): the Huntable policy pass insigma_validator.pyonly checked that tags were non-empty strings with an allowed character set, so a generated rule could cite a technique ID that does not exist, or one MITRE revoked years ago (attack.t1086for PowerShell), and sail through. Everyattack.*tag is now checked against a cached ATT&CK taxonomy: an unknown, revoked, or deprecated technique, sub-technique, group, software, mitigation, or campaign ID is a validation error with an actionable message (revoked IDs name their replacement, e.g. "replaced by T1059.001 (PowerShell) -- use 'attack.t1059.001'"; an unknown sub-technique names its existing parent), so the Phase 3 repair loop receives a concrete fix rather than silently accepting the tag. Unknown tactic names are warnings only, because ATT&CK v19 renamed Enterprisedefense-evasion(nowstealth/defense-impairment) while SigmaHQ and the Sigma spec still use the old name everywhere -- hard-failing that would reject correct community-style rules. The taxonomy lives inconfig/attack_taxonomy.json(2,535 objects across the Enterprise, Mobile, and ICS domains, ATT&CK v19.2) and is regenerated by the newscripts/build_attack_taxonomy.py, which reuses the STIX fetch frombuild_attack_platform_map.pyand is run by the operator when MITRE ships a release (roughly twice a year), matching the existing platform-map cadence; it is deliberately not a worker job because the output is a committed config artifact. A missing or corrupt taxonomy file disables the check with one logged warning instead of failing every rule. Covered bytests/services/test_attack_taxonomy.py,tests/scripts/test_build_attack_taxonomy.py, and a newTestAttackTagTaxonomyValidationclass intests/test_sigma_validator.pythat drives bothSigmaValidator.validate_ruleand the pySigma-backedvalidate_sigma_ruleentry point with valid, unknown, revoked, and deprecated IDs.
Changed
- Docs drift fixes from the 2026-09-25 doc true-up (2026-09-25):
docs/llm/cloud-model-reference.mdgains theclaude-fable-5-1row (added to the catalog 2026-09-02) and records the 128K max output for the Claude 5 models, verified against platform.claude.com, which clears that TODO marker.docs/development/ui-test-tiers.mdnow lists WF-CFG-PHANTOM-003 in thequarantinePlaywright project instead of calling it empty.docs/development/testing.mddrops the claim that CI reports quarantine counts, since no workflow does.mkdocs.ymladds four 2026-08solutions/records tonot_in_nav, matching their siblings. - HuntQueriesExtract now has an explicit source-authority boundary (2026-09-24): LLM-captured query text remains available for analyst display, traceability, and evaluation, but every HuntQueries observable is marked
content_fidelity: llm_best_effortandsource_text_authoritative: false. Captured items withtype: sigmaare now always display-only, even if future metadata supplies an otherwise eligible logsource; non-Sigma backend queries with explicit target telemetry retain their existing generation path. The contract now enumerates every text consumer and requires the plannedsource_providedimporter to scanarticles.contentdirectly for exact rule bytes rather than trusting LLM transcription. - GitHub PR submission no longer persists PATs in Git remote configuration (2026-09-24):
SigmaPRServicepreviously rewrote the customer rules repository's HTTPSoriginto includex-access-token:<PAT>, leaving the credential in.git/configafter every submission and across token rotation. HTTPS fetch, pull, and push operations now receive the PAT only through an invocation-scopedGIT_ASKPASSenvironment whose temporary script contains no secret. Before network operations, legacy credential-bearing HTTPS origins are rewritten to the same clean repository URL; SSH origins are untouched. Focused tests use temporary Git repositories to prove clean HTTPS remains clean, embedded credentials are removed, SSH remains unchanged, and neither the command arguments nor askpass script contain the PAT. - Backup freshness is visible in Settings (2026-09-03): the backup status API now exposes the newest backup timestamp, age, and a three-day stale threshold, and the Settings backup panel shows a distinct warning when the latest system backup is stale. The backup schedule and retention policy are unchanged.
- Cloud model reference data is current and catalog-sourced (2026-09-03): verified the OpenAI and Anthropic model output limits against their live documentation, dated the OpenAI source capture, and added
o1-proando3-proto the application model catalogs so their documented context windows are sourced consistently. - Quickstart presets gain the four short rules they were missing from the standard (2026-09-03): the first preset pass applied only a four-item delta, so every variant still lacked the every-tactic rule,
|windash,OriginalFileName, and the instruction boundary for article text (the LMStudio pair also lacked field order and the title rule). Because a preset's system text is the whole standard on that path, those gaps were live for every preset user. Each variant gained about 1,300 characters in its own structure (LMStudio 5.9k -> 7.2k, gpt-5/codex 8.0k -> 9.1k, main variants 10.0k -> 11.2k). The per-logsource field-name table was deliberately left out: it is 1,300 characters on its own, and the SigmaHQ validator layer now fails a wrong field name with the field named, which the repair prompt then fixes. The system message is not subject to the LMStudio user-prompt cap, so local runs are unaffected by the growth. - The in-app Sigma validator now enforces the rules repository's blocking SigmaHQ validator set (2026-09-03):
validate_sigma_ruleran pySigma parsing plus the Huntable policy pass, neither of which checks field names, logsource membership, custom top-level keys, or the requiredid/status/level/descriptionfields, so a rule withurl|containsorServerNameundernetwork_connection,category: proxywithproduct: windows, orservice: sysmonwithout an EventID parsed cleanly, was approved in the queue, and failed the blocking CI in Huntable-SIGMA-Rules.pySigma-validators-sigmahq==0.21.0(the version the rules repo pins; compatible with the app's pysigma 1.3.3) is now a dependency, andsrc/services/sigma_validation_blocking.ymlmirrors the repository's.sigma/validation-blocking.yml(36 validators). After pySigma accepts a rule the set runs over it; every issue becomes an error prefixedSigmaHQ <Issue>(so the repair prompt sees it), the result carriesmetadata["sigmahq"], and a rule is only valid when the list is empty. Three details matter: the pipeline's own grounding keys (observables_used,platform,observable_attribution, ...; now one sharedSIGMA_GROUNDING_METADATA_FIELDSused by the validator, the generation service and the workflow) never tripsigmahq_unknown_field, because the pipeline strips them before publication; validator instances are rebuilt per call, becauseduplicate_titleandidentifier_uniquenessremember every rule they have seen and a shared instance reported collisions between unrelated calls; and each validator runs in isolation, so one that crashes (pySigma 1.3.3's sigmahq logsource checks raise on list-of-map selections underservice:) is reported as a HIGH issue instead of aborting or passing the rule. A missing plugin disables the whole layer loudly (metadata["sigmahq"]["available"] == False) rather than narrowing it. The Huntable policy pass also stopped rejecting list-of-map selections (selection_img:as- Image|endswith:/- OriginalFileName:), a valid Sigma form that the generation prompt's own example uses. Test fixtures that predated the standard (noid/status/level,network_connectionwithout a product) were updated; the repair prompt's error map gained entries for the SigmaHQ issue names. The live images need a rebuild for the new package; until then the layer reports itself unavailable and validation behaves as before. - Sigma generation gives non-reasoning models a 4,000-token completion budget (2026-09-03):
_call_provider_for_sigmacapped every non-reasoning provider (LMStudio, Anthropic, local models) atmax_tokens=800. A complete rule is roughly 250-400 tokens, so any article with more than two detectable behaviors came back truncated withfinish_reason=lengthand the tail rules were lost or half-written. The budgets are now the named constantsSIGMA_MAX_TOKENS_REASONING(10,000, unchanged) andSIGMA_MAX_TOKENS_STANDARD(4,000), with a regression test for the standard path. A new unit test (tests/unit/test_sigma_prompt_files.py) also pins each Sigma prompt file to the exact kwargs its caller formats it with, checks that a single format pass leaves no doubled braces, and fails if the bootstrap seedsigma_generation.txtdrifts fromsigma_generate_multi.txt. - Sigma generate, validate and enrich prompts share one rule standard (2026-09-03): the three prompt surfaces that shape what reaches Huntable-SIGMA-Rules disagreed with each other and with the rules repository's CI. Findings and evidence are in
docs/development/sigma-prompt-audit-2026-09-03.md; the headline defects were (1) the generation prompt's network-abstraction section taught field names that do not exist in the SigmaHQ taxonomy (url,UserAgent,HttpMethod,ServerName,ALPN), which pySigma and the in-app validator accept and the repository's blockingsigmahq_invalid_fieldname/sigmahq_unknown_fieldvalidators reject, so such a rule could be approved in the queue and never merge; (2) underscored ATT&CK tactic tags (attack.defense_evasion) in the contract, the presets and 8 of the 18 published rules, which pySigma's ATT&CK validator flags; (3) the queue validate route's first-attempt prompt was an inline f-string with its own weaker rules, and both it and the repair prompt handed the model only the first 500 characters of the rule to fix, so the model repaired a fragment and invented the rest; (4) the enrichment prompt escaped its JSON schema for two format passes but is formatted once, so the model saw{{ ... }}, and the modal's default system prompt duplicated the file's directive block with a contradictoryd5(author) rule while the default instruction asked for "more comprehensive conditions" that directived4forbids.src/prompts/sigma_generate_multi.txt(and its byte-identical bootstrap seedsigma_generation.txt) now carries the full standard -- SigmaHQ field names per logsource, category-first logsource withserviceonly alongside anEventID, hyphenated tactics with every tactic a technique belongs to,|windashandOriginalFileNameresilience, ISOdate, title/description/level/false-positive conventions, a grounded example in canonical field order, and an instruction boundary for article text. The newsigma_validate_single.txt(first attempt ofPOST /api/sigma-queue/{id}/validate), the rewrittensigma_repair_single.txt, and the restructuredsigma_enrichment.txtmirror that standard instead of introducing rules of their own; the validate and enrich system prompts are now short constants insigma_queue.py(SIGMA_QUEUE_VALIDATION_SYSTEM_PROMPT,DEFAULT_SIGMA_ENRICHMENT_SYSTEM_PROMPT, mirrored inqueue.js), the retry preview cap is 8,000 characters in both the route andSigmaGenerationService._repair_rules,_sigma_rule_date()emitsYYYY-MM-DD, and the 12 quickstart presets had their tactic underscores anddate:format corrected inside theSigmaAgentprompt. The example rules embedded in the new prompts pass the rules repository's blocking validator set (pinned pysigma 1.5.0 / pySigma-validators-sigmahq 0.21.0). The three preset variants' network-abstraction block (about 12,000 characters ofurl/UserAgent/HttpMethod/ServerName/ALPNexamples per preset) was then replaced with a 2,500-character block that uses only SigmaHQ proxy, dns_query, network_connection and firewall fields, and each preset's user scaffold now passes{author}and{date}so the model no longer has to guess them; that matters because with a preset applied the preset text is the whole standard and the prompt file is never read. Still not done: the in-appSigmaValidatorstill has no field-name or custom-key check, so queue validation remains advisory where the repository CI is blocking. The contract-guardian pass over this change also corrected the contract's description of the preset path (a quickstart preset's ownsystemtext is the entire rule standard and itsuserscaffold is the user message, so the prompt file is not used on that path and preset drift is live drift), replaced the stale 150-line copy of the old enrichment system prompt inworkflow.htmlwith the short constant, and switched the rule export insrc/web/routes/ai.pyfromYYYY/MM/DDto ISO dates so exported rules match what generation now emits.
Removed
- Legacy manual
/generate-sigmaendpoint, and the starvation it left behind (2026-09-03):POST /api/articles/{article_id}/generate-sigma(383 lines insrc/web/routes/ai.py) had no caller anywhere insrc/web/-- no template, no JS, no dynamically built path -- and returned its rules to the caller instead of queueing them, so anything it generated billed a provider run and could never be reviewed. It was already half-removed: the 2026-08-10 dormant-subsystem cleanup droppedarticle_sigma_matchesand named this endpoint as that table's sole writer, but left the generator standing. It was also the only writer ofarticle_metadata["sigma_rules"], which starved its two remaining readers.POST /api/articles/{article_id}/sigma-matches-- still called from the article page -- read that key and therefore answered "No generated SIGMA rules found. Please generate rules first." for every article the workflow had actually produced rules for; it now sources this article's rows fromsigma_rule_queue(rejected excluded, oldest first, unparseable YAML skipped with a warning) and assesses them for real. Verified against live data: article 481 yields its 7 queued rules with intact logsource and detection. The#sigmaURL-fragment modal inarticle_detail.html, the other reader, is still metadata-backed and therefore still dead; it has no entry point in the UI either, and is tracked separately. Note for whoever picks that up:sigma_ruleswas empty (0 rows) when this landed, so novelty assessment returned 0 candidates regardless of source; the corpus is populated now (3,757 rules, fully embedded) -- see thegit/runtime-osfix below for why./setup.shhad left it empty. - Dead observables-mode "mark-reviewed" surface removed (2026-09-04): the observables annotation mode has been disabled since 2026-01 (its page ships with the feature flag off), leaving a half-implementation behind.
POST /api/articles/{article_id}/mark-reviewedinsrc/web/routes/articles.pydrove an agent-writablearticle.reviewedflag that no UI ever saved, and had zero callers; the same mode left a commented-out "mark observables reviewed" modal inarticle_detail.htmlwith an orphanedmarkObservablesReviewed()JS function. Removed the route, themark_article_reviewedMCP tool and its tool-list entry insrc/huntable_mcp/tools/articles.py, theACTION_ARTICLE_REVIEWEDaudit constant insrc/services/audit_service.py, the route's classification inroute_manifest.py, and the dead template block plus orphaned JS.docs/reference/mcp-tools.mdrenumbered its tool table from 37 to 36 entries. Regression guards intests/api/test_removed_dead_endpoints.pyandtests/unit/test_mcp_write_tools.pyprove both the endpoint and the MCP tool stay gone. - Remaining observables-mode machinery and its training subsystem removed (2026-09-05): the previous day's cleanup removed the mode's "mark-reviewed" surface but left the mode itself and an entire dormant training pipeline standing. In
article_detail.html,OBSERVABLE_MODEand its constants, the#article-content-plainsurface and itsapplyContentVisibility()/updateModeUI()branching, the[data-annotation-mode]CSS gate,showObservableModal(),validateAndExtractObservableSelection(),submitObservableAnnotation(), and the three mobile touch-handler branches that called them are gone;setAnnotationMode()now only accepts huntability, and huntability's selection, modal, and mobile-touch paths are unchanged and re-verified in the browser. Deleted the subsystem this fed:src/services/observable_training.py,src/services/model_training.py(its only caller, shelling out to aWorkshop/training script tree that does not exist in this repo),src/web/routes/observable_training.py(GET /api/observables/training/summary,POST /api/observables/training/run),src/worker/tasks/observable_training.pyand its explicit import incelery_app.py,src/web/templates/observable_training.html, and the/observables-trainingpage route -- all four now 404.route_manifest.pydrops both/api/observables/training/*entries plus a phantom/api/observables/evaluation/*entry orphaned since the 2026-08-10observable_evaluationremoval (docs/features/observable-evaluation.mdupdated to match: its "still-live" training section is now a removal note). The live DB had zeroCMD/PROC_LINEAGEannotations and zero calls to either removed endpoint, confirming neither had a real caller. Eighttests/worker/test_*.pyfiles carried asrc.worker.tasks.observable_trainingsys.modulesmock that predated this removal and are now trimmed to what still exists;tests/unit/test_target_blank_rel_noopener.pydrops itsobservable_training.htmlexemption and lowers its known-links floor from 7 to 6. - Unreferenced mobile annotation manager removed (2026-09-16):
src/web/static/js/annotation-manager-mobile.js(1,103 lines) still carriedOBSERVABLE_MODE,PROC_LINEAGEand the observables touch handlers after the 2026-09-05 cleanup, and no template or script loaded it (the article page shipsannotation-manager.jsandmobile-simple.js; this is a desktop-only tool). Deleted, and the three mobile-only cases intests/playwright/annotation_xss_regression.spec.tswent with it; the desktopescapeHtmlregressions stay.
Fixed
- Generated Sigma rules are no longer attributed to the article's publisher (2026-09-17): the SigmaAgent "AUTHOR PRESERVATION" instruction told the model to copy the author of a complete Sigma rule whenever the article itself published one. It keyed on what the article contained, not on whether the generated rule reproduced anything, so every newly written rule from a detection publisher was eligible to come back as
author: The Hunters Ledger-- measured 2026-09-04 on queue #14, #15, #16 and #21, while sibling rules from the same execution said Huntable CTI Studio. The carve-out is removed from all four prompt sinks: both seed prompt files (still byte-identical), the ten quickstart presets that carried it (the two LMStudio presets never did), and the live active workflow config, saved throughPUT /api/workflow/config/promptsas v66 with only that sentence changed (v65 had it; 14 older config versions still do). Because those older versions and any model can still emit a copied author,SigmaGenerationServicenow stampsSIGMA_RULE_AUTHORon every generated rule after carrying the optional Sigma fields. Two generation-service tests pinned the old behaviour (a copied publisher author surviving, and no author when the model omitted one) and now assert the stamped author; newtests/unit/test_sigma_author_attribution_contract.pyfails if any seed prompt or preset reintroduces the carve-out.docs/contracts/sigma-generate.mdupdated. Publisher-authored rules keep their attribution only through the planned source_provided import path. - Intra-batch Sigma dedup compares matched values, not condition syntax (2026-09-17):
_detection_leaf_valuestried to drop Sigma condition expressions with a four-string denylist (selection,condition,all of them,any of them) instead of skipping theconditionkey, so real conditions such asselection_image and selection_commandwere counted as detection values and inflated the union of every rule pair. Execution 25 (article 989) queued #19 and #21 -- the same wscript.exe-from-Windows-Temp detection from two generation groups -- at 0.50 overlap against a 0.8 threshold. The collector now skips the reservedconditionandtimeframekeys at the top of the detection block. That alone lifts #19/#21 only to 0.75 (#19 addsOriginalFileName), so the duplicate test (_batch_rules_are_duplicates) gains a containment arm: Jaccard >= 0.8, or the smaller value set fully contained in the larger with at least 3 values and Jaccard >= 0.6. Measured before choosing it: of the live queue's same-execution pairs it catches all three true duplicates that share tokens (#2/#7 and #23/#26, which the condition leak had also hidden at 0.67, plus #19/#21) and keeps the distinct #9/#10; across 1,187,088 SigmaHQ same-logsource pairs of distinct published rules it flags 94 versus 77 for Jaccard >= 0.8 alone, 135 for Jaccard >= 0.7 and 411 for pure containment. Near-duplicates spelled with different tokens (#24/#27urlcachevs-urlcache) remain separate by design.docs/architecture/workflow-data-flow.mddescribed the old rule (and adetection.selection-only scope the code never had) and is updated. Newtests/workflows/test_batch_rule_dedup.pyreproduces #19/#21 from the live YAML; its correctness cases fail against the previous code. - Submitted Sigma queue rules keep their YAML, stop matching themselves, and show their real title (2026-09-16): three defects surfaced on queue rows #962 and #984. (1) After a rule's PR merged and the customer repository synced, the corpus held its byte-identical
cust-<yaml id>copy, so re-scoring the queued rule reported a 1.0 match against itself and hid genuine duplicates. Candidate retrieval (SigmaNoveltyService.retrieve_candidates) now skips that row on the exact-hash shortcut, the canonical-class path and the logsource-key fallback;customer_copy_rule_id()derives the id, and the queue's list, similar-rules and compare-similarity endpoints now pass the YAMLidinto scoring (they previously rebuilt the rule without it). Workflow generation was unaffected: a new rule has no customer copy yet. (2)rule_yamlwas rewritten days aftersubmitted_at: in view mode the queue's Validate and Similar Rules buttons re-save the modal text whenever it differs from the stored YAML, and no endpoint checked submission state._apply_rule_yamlnow guards the approve, reject andPUT /api/sigma-queue/{queue_id}/yamlpaths: a YAML change on a row withpr_submittedorsubmitted_atreturns409and nothing is committed (identical YAML stays a no-op); the reject path applies it outside its broad JSON-body fallback so the 409 is not swallowed. The queue's apply-validated and apply-enriched actions now show the server's reason instead of a generic error. (3) YAML edits never updatedrule_metadata.title, so the queue header kept the generated title; every accepted edit now refreshes it from the new YAML when it parses with a title. Also correcteddocs/features/sigma-rules.md: customer rules use thefile_pathprefixcust/, notcustomer/. Newtests/unit/test_sigma_queue_submitted_rule_integrity.pyandtests/integration/test_sigma_self_match_exclusion.py(real test database) fail against the previous behavior. - A manual trigger no longer discards a staggered eval run's pending execution (2026-09-16): the eval launcher commits every execution row up front and schedules their Celery tasks with increasing countdowns (up to
EVAL_STAGGER_SECONDS+ a 60 s throttle per step), so the tail of a large launch sitspendingwith a live task well past five minutes. The trigger eligibility check treated any such row as stuck, marked itfailedand started a fresh execution; when the eval row's task then arrived, the worker's atomic claim (below) found it no longerpendingand skipped it, so the eval data point was lost behind a misleading "stuck in pending" error.is_stuck_pending_executionnow gives rows whose snapshot carrieseval_runa window of the 5-minute base plus the latest countdown the launcher can schedule (eval_pending_stuck_after(), read fromMAX_EVAL_EXECUTIONS_PER_LAUNCHat call time: about 1 h 44 min at the default cap). Inside that window the trigger is blocked with the existing "already has an active workflow execution" reason andPOST /api/workflow/executions/trigger-stuckcounts the row as skipped; a row that outlives it is still recovered by both paths, and the retention reaper still fails anything idle pastRETENTION_STALE_EXECUTION_HOURS. Non-eval rows keep the 5-minute rule. Ingest auto-triggers are not exposed: they dispatch only for newly inserted articles, and eval rows point at pre-existing fixture articles. The trigger-stuck response wording now describes a start window rather than a fixed age. New tests intests/unit/test_trigger_stuck_dispatch.py,tests/services/test_workflow_trigger_service.pyandtests/integration/test_execution_claim.pyfail against the previous predicate; the integration case reproduces the loss end to end on the test database. - The workflow worker claims its execution row atomically (2026-09-16):
run_workflowlogged the existing execution and then wrotestatus = "running"unconditionally, so two Celery tasks carrying oneexecution_id-- a trigger-stuck re-dispatch racing the original, a retry of a merely delayed row, worker prefetch across a restart -- both ran the graph and both wrote results into the same row (measured 2026-09-04 on execution 19: two fork-pool workers, one sawpending, the next sawrunningand proceeded; with 2 x 2 worker concurrency up to four can collide, and on a non-junk article that is double provider spend). The blind write is now a conditionalUPDATE ... WHERE id = :id AND status = 'pending'with a rowcount check: exactly one caller moves the row torunning; every other caller logs a skip and returns{success: false, skipped: true}without invoking the graph or touching the row. Gating onpendingis safe because the two otherstatus = "running"writes in the module (os_detection and rank_article nodes) re-affirm a row the function already owns, nothing resumes arunningrow, and restart-orphanedrunningrows are still failed byrecover_orphaned_running_executionbefore a retrigger creates a new row.STUCK_PENDING_AFTER(5 min) stays as a dispatch heuristic forPOST /api/workflow/executions/trigger-stuckand as the trigger path's definition of a lost row; the decision and its reasoning (the staggered eval tail can sit pending for minutes with a live task) are recorded next to the constant. Newtests/integration/test_execution_claim.pyruns two threads with separate sessions into one pending row against the test database and asserts exactly one graph invocation. - Two web routes that imported workflow code no longer fail at runtime (2026-09-04): the web image's venv is built with
uv sync --frozen --no-default-groups(Dockerfile stagebuilder-web), solanggraphis not installed incti_web-- and two routes importedsrc.workflows.agentic_workflowanyway, raisingModuleNotFoundErroron the first request.POST /api/workflow/executions/trigger-stuckwas also running the LangGraph pipeline in-process; it now re-dispatches each pending execution onto Celery viatrigger_agentic_workflow.delay, so the worker runs the graph, and it audits each re-dispatch.summarize_rule_noveltymoved out ofagentic_workflowintosrc/services/sigma_novelty_service.pyso the sigma-queue routes can reach it without pulling in the workflow dependency group;agentic_workflowre-exports it, so its own callers are unchanged. Behavior of the novelty classification itself is unchanged -- inconclusive still yieldsmax_similarity=Nonerather than a confident0.0. - Sigma sync, indexing and embeddings work again on a fresh setup (2026-09-04): the role-specific runtime split (
f9a236b3) moved every service off the monolithic image, and the sharedruntime-osbase it introduced did not installgit.sigma_sync_serviceshells out togit clone/git pullfor the SigmaHQ corpus andsigma_pr_servicedrives the customer rules repo, sosigma syncfailed inside theclicontainer on every setup after that commit -- and becausestartup_sigma_sync_and_indexgates metadata indexing and embedding generation behind the sync's exit status, both were skipped. The call's2>/dev/nulldiscarded the real error, leaving only "Sigma sync failed (run manually...)", and the suggested manual command failed identically. A completed./setup.shtherefore leftdata/sigma-repoempty, zero rows insigma_rules, and no semantic search, rule similarity or MCP Sigma retrieval; Sigma PR submission from the web container was broken by the same missing binary.gitis now installed inruntime-os(one shared layer for all seven role targets) and in theDockerfile.prodruntime stage._startup_sigma_clireplaces the three suppressions: stderr is appended tologs/sigma_index.logand its tail printed on failure, while stdout stays on the terminal so the long embedding job still shows progress.startup_ensure_runtime_directoriesnow also createstest-resultsanddata/diagnoses, two gitignored bind-mount sources whose absence on a fresh clone makesdocker compose upfail with an OCI mount error. Contract tests pingitin both runtime images and require every relative bind-mount source to be tracked or scaffolded. - Codex subscription login uses the device-auth flow, and the status message says what's actually wrong (2026-09-03):
docker compose exec workflow_worker codex loginstarts a browser-callback listener bound inside the container, which a host browser cannot reach, so the documented login command hung with no explanation..env.example, both Codex docs pages, and the Settings panel now point atcodex login --device-auth, which prints a URL and code to open in the operator's own browser; the Settings panel shows the command in a copy-able block instead of hiding it behind an "ask an administrator" line.POST /api/settings/codex/test(test_codex_subscription) stopped collapsing every failure into that same "ask an administrator" message: a logged-out app-server ({"account": null}) now reports "Codex is not logged in. Run: ..."; a login on the wrong auth mode reports "...not with a ChatGPT subscription. Run: ..."; an unreachable app-server keeps its own message -- each names the runnable command.o1-proando3-prowere also dropped back out ofconfig/provider_model_catalog.json: the project's OpenAI allowlist (filter_openai_models_project_allowlist) already excludes both from every Settings/Workflow dropdown, so listing them in the raw catalog was inert. - Restore snapshots, CLI collection, workflow recovery, and Dev2 initialization are safer (2026-09-03):
--forcenow skips only restore confirmation; both database restore paths create a pre-restore snapshot unless the operator explicitly passes--no-snapshot, including uploaded backups through the maintenance API. The lightweight CLI can now start collection without Playwright installed; a JavaScript-rendered source reports a clear instruction to use the Playwright-enabled ingestion worker instead of failing during import. A workflow row leftrunningby a worker restart is marked failed on the next trigger attempt only after 30 minutes without anupdated_atchange, so a healthy slow run is not interrupted and a forced retrigger can proceed. Dev2 now mounts the standardinit-scriptsdirectory rather than a missinginit.sqlpath; its deliberately limited service boundary is documented. - Sigma generation no longer truncates the prompt for cloud providers, and trims article content rather than instructions for local ones (2026-09-03):
generate_sigma_rulesapplied the LMStudio context-window cap whenever the caller passedai_model="lmstudio", and the workflow passes exactly that as a placeholder meaning "use the configured provider". So every workflow run on Codex, OpenAI or Anthropic cut the user prompt at 8,000-12,000 characters (Truncating prompt from 21850 to 12000 charson every run in the worker log), and because the article sits at the top of the template the cut removed the tail of the standard: level calibration, false positives, metadata, output format, the example and the final check. Execution 3900 showed the effect once the persona bug stopped masking it: no---separators, no author, date, references or false positives, technique-only tags. The cap now keys on the resolved provider (_resolve_sigma_provider, shared with the multi-rule call), and when a real local model needs trimming the article content is shortened and the prompt rebuilt ([Article content truncated ...]), so the instructions always survive. Known limit: the 13.5k template alone exceeds the local budgets (8,000-12,000 characters), so on LMStudio the content is trimmed to almost nothing; either raise the caps for modern local models or shorten the template. - A raw-text SigmaAgent record is the user template, not the persona (2026-09-03):
parse_sigma_agent_prompt_datatreated any plain-text prompt saved next to a siblingmodelkey as a system persona, and that is exactly the shapereset-to-defaultsandbootstrapwrite fromsigma_generation.txt({"prompt": <file>, "instructions": "", "model": "Not configured"}). The runtime therefore sent the whole 13.5k template unformatted as the system message, with literal{author}/{date}/{title}placeholders, and then the file again, formatted, as the user message. Execution 3899 showed the symptom: three of four rules came back withauthor: '{author}'. Now the placeholder check decides: text with at least one{identifier}is the user template (system falls back toDEFAULT_SIGMA_SYSTEM_PROMPT), text without placeholders is the persona, regardless of themodelkey. Every run on a raw-text record before this, including today's 3895, 3898 and 3899, was on the doubled prompt. - Out-of-class Sigma rules are re-homed to the group whose logsource they match instead of being dropped (2026-09-03): every generation group's call sees the whole article, so the model regularly emits a rule that belongs to a sibling group.
_rule_logsource_matches_grouptreated that as an error and dropped the rule; on execution 3898 thenetwork_connectioncall produced the article's best detection (a wscript.exe -> node.exe process_creation rule) and it was discarded while theprocess_creationgroup, whose observables did not include that command line, never saw it._find_rehome_groupnow keeps the emitting group when the logsource fits, otherwise moves the rule to the first sibling group (never the full-content fallback) whose logsource hint matches, and drops it only when no group can own it.observables_usedare still rebased against the emitting group (the indices are positions in that group's observable list); attribution, platform, telemetry andsigma_generation_groupcome from the home group, which recordsrehomed_from. Group summaries gainrehomed_rulesanddropped_rulesno longer counts re-homed rules. Regression tests mirror execution 3898. - Queued Sigma YAML no longer leaks
generation_phase, and is written in SigmaHQ field order (2026-09-03):promote_to_queuekept a third private copy of the pipeline-key list, and that copy lackedgeneration_phase, so every queued rule carried it as a top-level key; the PR service commits queue YAML verbatim, so the rules repo's blockingcustom_attributes/sigmahq_unknown_fieldcheck would have rejected each one (the in-app SigmaHQ layer showed it first: all 12 rules from the day's two runs failed on exactly that key). Promotion now uses the sharedSIGMA_GROUNDING_METADATA_FIELDS(which gainsgeneration_phase) throughcanonical_sigma_rule_dict, which also orders the remaining keys title, id, status, description, references, author, date, tags, logsource, detection, falsepositives, level instead of the service's construction order. - Generation prompt no longer credits rules to the article's publisher (2026-09-03): the first live run on the new prompt (execution 3898, Codex gpt-5.6-terra) set
author: Microsoft Security Blog - Defenderon rules from an article that contains no Sigma rule at all; the model read the author-preservation clause ("if the article contains a verbatim Sigma rule with a non-blank author") as "credit the source". The clause insigma_generate_multi.txt, its seed, and the 10 preset variants that carry it now says the supplied author is used unless the article itself contains a complete Sigma rule in YAML with its own author, and that the article's author, publisher, blog or vendor name is never the rule author. The liveSigmaAgentrecord was re-seeded. - The Workflow Config effective-prompt preview shows the SigmaAgent messages the backend really sends (2026-09-03):
config.jscarried a hand-copiedLOCKED_SIGMA_USER_TEMPLATE(the pre-auditsigma_generate_multi.txt) and always showed it for SigmaAgent, and_effReadCurrentSystemfell back to the user text when no persona was saved, so a raw-text SigmaAgent record was rendered as both the system and the user message and a preset's own user scaffold was never shown. NewGET /api/workflow/config/prompts/defaults/{agent_name}serves the on-disk user template (sigma_generate_multi.txt, stripped the wayPromptLoaderdoes) plusDEFAULT_SIGMA_SYSTEM_PROMPT; the preview fetches it once on first use, shows the DB user template when the record carries one (preset / raw-text, badge "editable · from DB") and the file otherwise ("LOCKED · code-owned"), shows the code default as the system message with a "default · code-owned" badge when no persona is saved, and documents the{author}/{date}placeholders. The old JS mirror is gone, so the preview cannot drift from the file again. - Evals2 item scorer reads RegistryExtract's real
key/value_namefields (2026-09-03):item_candidates()forregistry_artifactsreadregistry_hive/registry_key_path/registry_value_name, which RegistryExtract has not emitted since the 2026-05-12 schema change, so every registry item was silently dropped from matching and rows showedmatched_count=0with a passingactual_count-- a false green checkmark on the Agent Evals table despite correct extraction. Found via the first realmcp_agenteval diagnosis (execution 3866). Covered by updated cases intests/services/test_eval_item_scorer.pyandtests/api/test_subagent_eval_rescore.py. - Generated Sigma rules keep
author,date,references, andfalsepositives(2026-09-02):SigmaGenerationService.generate_sigma_rulesrebuilt every validated rule into a dict with a fixed key list (title, description, id, tags, level, status, logsource, detection) and the review queue dumps its YAML from that dict, so those four standard fields were silently dropped from every rule on every source. The effect was invisible until the Hunter's Ledger detections pages were ingested: the workflow trace for execution 3865 shows the model emitted anauthor:line on all 12 raw rules (7 attempting the publisher's name from the embedded rules, 5 with the template defaultHuntable CTI Studio), yet all 4 queued rules carried none, so the SigmaGenerate AUTHOR PRESERVATION directive could never reach the queue. The rebuilt dict now carriesauthor,date,modified,references,falsepositives, andfieldswhen the model emitted them (SIGMA_OPTIONAL_RULE_FIELDS), coerced to JSON-safe values:yaml.safe_loadturns an ISOdate: 2026-08-03into a Pythondate, which the JSONBsigma_rulescolumn cannot store (execution 3889 failed on exactly that during verification), so dates become ISO strings and other scalars become strings. Queue promotion already keeps every non-grounding key, so these appear inrule_yamlwith no further change. - Eval diagnosis UI now shows run signals, the help button says what it does, and the skill copies match (2026-09-02): the v2 diagnosis schema requires
run_signalsand thehuntable-eval-diagnosisskill checks them first, butrenderDiagnosisPanelinagent_evals.htmlnever rendered them, so truncation, context pressure, contract compliance, finish reason, and token utilization were invisible in the Agent Evals execution modal. They now render as a Run Signals row ahead of root causes with the bad values highlighted (truncation detected,finish_reasonoflength/error, high context pressure,violated/partialcompliance, utilization at or above 80%), and the "What it returns" table indocs/features/agent-evals.mdlists the field. The execution-modal button was labeled Diagnose via MCP but only opened a help modal; it is now How to Diagnose, and the help copy is rewritten for the operator (the prompt first, with a Copy button, then what the agent does and when the one approved save happens) with the file-path pointers folded into a maintainers-only disclosure. The saved-diagnosis views (GET .../evals/{id}/diagnosis,.../diagnoses, and the MCPlist_eval_diagnoses/export_diagnosed_eval_bundlesloaders) now share oneload_saved_diagnoseshelper ineval_diagnosis_service.pythat orders runs by the persistedcreated_atstamp instead of file mtime, so a restore or copy ofdata/diagnoses/no longer reorders run history; the MCP loaders report_source_fileas a bare filename instead of an absolute host path; andget_eval_diagnosis_contextreportsexecution_id/article_idas integers to match the save and list tools..agents/skills/huntable-eval-diagnosis/SKILL.md(the canonical copy) had drifted to a shorter version than its.claude/skills/compatibility copy and is synced to the fuller text. Two stale doc lines fixed:docs/guides/authentication.mdstill listed "LLM-powered bundle diagnosis" as an audited HTTP route, anddocs/brand-style-guide.mdstill described source healing as automatic repair. Covered bytests/unit/test_eval_diagnosis_run_signals_render.py(template structure), acreated_at-over-mtime ordering test intests/api/test_eval_diagnosis_api.py, a bare-filename test intests/unit/test_mcp_eval_tools.py, and an id-coercion test intests/services/test_eval_diagnosis_service.py. Also removed the MLOps Control Center's "System Online" pulse indicator and its four-cell stats bar (mlops.html) -- all four cells were hardcoded (3 / 3,Ready,Monitoring,--), never wired to a real status source, and unrelated to this fix beyond sharing the file. - The Hunter's Ledger source now ingests its
/hunting-detections/pages, and discovery-path articles keep their author (2026-09-02): the source only ingested/reports/posts from the Atom feed, but the publisher's validated Sigma rules live on 56/hunting-detections/<slug>-detections/pages the feed never lists (a sample page carries 18 rules, every one withauthor: The Hunters Ledger; the matching report page carries none), so the SigmaGenerate AUTHOR PRESERVATION directive never fired on this source. Wideningpost_url_regexalone does nothing because the fetcher returns after a successful RSS tier unlessarchive_pages/max_archive_pagesare set, and with nodiscovery.strategiesURL discovery falls back to the homepage, which links to none of them (the site has no sitemap). Thehunters_ledgerstanza inconfig/sources.yamlnow widens the regex, enables the archive supplement, and adds a single-pagelistingstrategy over the detections index witha[href^="/hunting-detections/"][href!="/hunting-detections/"](verified against the live index: 56 matches, no pagination). Index links omit the trailing slash while the page canonical includes it; the storedcanonical_urlcomes from JSON-LD with the slash, and both the fetcher's RSS/archive merge and the processor's URL dedup strip trailing slashes, so no slash/no-slash duplicates are created. Two supporting fixes:ModernScraper._extract_articledroppedauthors,tags, andsummary-- it extracted them from JSON-LD and selectors and then built theArticleCreatewithout them, so every discovery-path article on every source was stored with no author; they are now carried through. And because the detections pages carry no author signal anywhere (JSON-LDBlogPostingwithoutauthor, no meta author), the stanza addsmeta[property='og:site_name']::attr(content)as a trailingauthor_selectorsfallback so those articles match the feed-path attribution instead of being blank; report pages still get the person-level author from theirTechArticleblock. The livesources.id=42row was updated with a targeted JSON merge rather thansync-sources, which rewrites the whole config and would have clobbered the operator's customrobots.user_agent. Known limit, unchanged by this work: the archive supplement re-fetches up to 50 discovered pages on every check (the_use_conditionalflag is unused), which is the same behavior everyarchive_pagessource already has. - ContentCleaner keeps indentation inside
<pre>blocks, and the processor no longer re-cleans already-clean text (2026-09-02):html_to_textfolded block boundaries correctly, butnormalize_whitespace_keep_newlinesthen left-trimmed every line, so an indented Sigma rule or code sample published in<pre>came out flush-left and stopped being parseable YAML (on The Hunter's Ledger detections pages all 18author: The Hunters Ledgerlines survived whileselection:landed at column 0 underdetection:). Downstream, the SigmaGenerate AUTHOR PRESERVATION directive assumes a verbatim rule in the content, so the model had to re-serialize instead of reproduce.html_to_textnow stashes each<pre>block verbatim before normalization and restores it afterwards; block-level descendants (<br>, per-line<div>) still break lines, tabs and highlighter<span>wrappers are preserved, and prose outside<pre>is byte-identical to before (pinned by a captured-output test). Fixing the cleaner alone would have been inert:ContentProcessorranclean_htmla second time on the text every producer had already cleaned, and with no<pre>left to protect, that pass stripped the indentation again before the row was stored. The processor now skips that redundant pass when the content carries no HTML markup (ContentCleaner.looks_like_html), and still cleans content that really is HTML. Forward-only: existing rows keep their old content, and eval-article rows andconfig/eval_articles_datafixtures are untouched. Verified against the live Hunter's Ledger page: 18 embedded rules now come out ofclean_htmlas parseable Sigma YAML with nested detection blocks. Covered by 8 new cleaner tests intests/test_content_cleaner.pyand 2 processor tests intests/test_content_processor.py. - JSON-LD article picker accepts
TechArticleand prefers the block that names an author (2026-09-02):StructuredDataExtractor.find_article_jsonld(src/core/modern_scraper.py) returned the first JSON-LD block typedArticle,BlogPosting, orNewsArticleand ignored every other block. The Hunter's Ledger report pages emit a bareBlogPostingfollowed by aTechArticlecarryingauthor.name, so the person-level author was dropped on the scrape path (feed-path articles were unaffected becauserss_parseruses the feed author). The picker now also acceptsTechArticle,ScholarlyArticle, andReport, and when several article-typed blocks exist it prefers the one with a usable author name, then the one witharticleBody, then document order -- so single-block pages behave exactly as before. Anauthorfield with no usable name (empty list, empty object, organization without a name) does not count.extract_from_jsonldis unchanged. Covered by 11 new cases intests/test_modern_scraper.py, including the two-block Hunter's Ledger shape and the single-block regression guard. - WF-CFG-PHANTOM-003 un-quarantined: autosave dropped a stored
agent_modelskey whenever its own last-known value was blank (2026-09-02):performAutoSave()inconfig.jsonly fell back to the stored value for a key that read back as''(a provider/model round-trip miss, or a catalog fetch still in flight) when that stored value was itself a non-empty string -- so a key whose currently-known value happened to be''(a genuinely unconfigured agent, or one whose real value hadn't reached this page'scurrentConfigsnapshot yet) was dropped from the PUT payload entirely rather than sent as''. The backend'sagent_modelsmerge masked this in practice (an omitted key keeps whatever the DB already has), but it meant the save payload could not stand on its own -- exactly what[WF-CFG-PHANTOM-003]asserts. Fixed by keying the fallback on whethercurrentConfig.agent_modelshas the key at all, not on the stored value being non-empty. Root-caused via a from-scratch Postgres+Redis+uvicorn environment mirroringtests.yml'suijob: a freshly-seeded config has every*_model/*_providerkey stored as'', which reproduced the exact "stored key missing from the sent payload" failure deterministically (no concurrency needed) and is also the condition an in-flight catalog/provider fetch reduces a real value to at collection time. Verified with the restored spec run 2-up alongside every other non-excludedworkflowspec (expanded_prompt_editor_save.spec.tsincluded) and repeated within-file at--workers=2. Un-quarantined:quarantined_workflow_config_phantom_003.spec.tsmerged back intoworkflow_config_no_phantom_save.spec.ts, andplaywright.config.ts'squarantineProject.testMatchrestored to empty.
Security
- Dependency pins bumped for pip-audit findings (2026-09-25):
pip-audit -r requirements.txt --no-deps --disable-pip(a full resolve fails on a langchain-core conflict) flagged three pins.GitPython3.1.58 -> 3.1.60 (PYSEC-2026-3785/3786/3787/3788/3837 fixed in 3.1.59, PYSEC-2026-3982/3984 fixed in 3.1.60; still zero import sites).mkdocs-material9.7.6 -> 9.7.7 (PYSEC-2026-3864), in bothrequirements.txtandpyproject.toml.accelerate1.13.0 -> 1.15.0 (PYSEC-2026-3804 / CVE-2026-69112 / GHSA-4j2p-28q2-5m79, path traversal via sharded-checkpointweight_mapentries inload_checkpoint_in_model/load_checkpoint_and_dispatch): the advisory lists no fixed version, but its text scopes it to "through 1.14.0" and PyPI reports no advisories against 1.15.0. A patched release existed, so the pin moved and no accepted-risk ignore was added.uv.lockregenerated withuv lock; only those three packages changed.