Repository navigation
0.14.0 — scoped signal entities, a migration command, and a generation hot path
0.14.0 — scoped signal entities, a migration command, and a generation hot path
14 October 2026. Minor on 0.13.0.
Two things need action after upgrading. objective.signals[].entities values are now stage-scoped, so Hostname becomes Host::Hostname — and opentide migrate vocab-scope does that for you. response.responders becomes a configuration enum you declare in deployment.toml, so a value that no instance could make valid under 0.13.0 now validates.
Documentation and export generation are dramatically faster. A corpus that previously exceeded a one-hour CI job timeout now completes, which makes the deploy stage reachable again.
Install
pip install opentide==0.14.0
export OPENTIDE_REPO_ROOT=/path/to/detection-repo
opentide migrate vocab-scope --apply
opentide generate schemas templates vocabs snippets
opentide validate --strictBreaking
-
objective.signals[].entitiesis stage-scoped. An authored value is<stage>::<key>. Seven of the 28 entity keys are defined under two stages —Account,User,Authentication,Token,Permissions,EmailandAPI Call— so a bare key name could not say whether a signal keyed on a host account or a cloud account. That is a real detection distinction: different telemetry, a different data contract, often a different platform. The enum carried 28 values where the vocabulary defines 35; it now carries all 35. Stage identifiers are capitalised toHost,NetworkandCloud, matching the conventionthreat.surfacealready uses. Vocabulary entry keys are unchanged, so thesignal.entities::1.0pin is unaffected. See RFC 0005. -
response.respondersis a configuration enum, not a vocabulary. The shippedrespondersvocabulary hadkeys = []while both rule revisions pinned the field to it, so every authored value failed validation and no instance could clear it — vocabularies are canonical and not locally extensible. One corpus reported 483 unclearable findings from two values.The alert handling team is per-organisation, so it belongs in configuration. Declare your teams in
deployment.toml:default_responders = "CSIRC" # must stay ABOVE every [[...]] block [[responders]] name = "CSIRC" description = "Computer Security Incident Response Capability — first line" [[responders]] name = "CATCH" description = "Cyber Threat Analysis and Hunting — escalation"
The generated schema offers those names in the editor with their descriptions, and
validaterejects anything else. Declaring none leaves the field unconstrained — a fresh install must never carry a constraint it cannot satisfy, which is the defect being fixed.[deployment] default_responderssupplies the team for a rule that names none; it shipped in the template and in the spec but was read by no code at all, so the documented fallback never actually happened. The vocabulary file is removed.
Changed
pip install opentidenow ships the MCP server.mcpmoves from the optional[mcp]extra into base dependencies. A bare install previously omitted it, soopentide mcp— a first-class CLI command — opened by telling you to install an extra first. The[mcp]extra is kept as an empty alias, sopip install opentide[mcp]and any editor or agent configuration naming it keep working unchanged.
Added
-
opentide migrate vocab-scopere-prefixes authored values for every stage-scoped field, not just the new one. Three properties make it safe on a real corpus:- Nothing is guessed. A bare key defined under several stages is reported with its candidates and left untouched; only an implied single-stage prefix or a stage-casing fix is applied. Exits 2 while anything is left for a human, so CI cannot mistake a partial migration for success.
- Only the declared field is touched. Values are located by path through the document, not by leaf key name — a Sentinel platform block also carries a key called
entities, holding entity mappings, and a name-based rewrite would corrupt it. - Formatting survives. Each value is replaced over the exact span its scalar occupies, so comments, block scalars, key order, quoting style and flow sequences are preserved. Every file is re-parsed and compared against the plan before it is written.
-
generate --helpnames the four positional phases.schemas,templates,vocabsandsnippetsare accepted, but Typer renders the sub-app's help string rather than the callback docstring — which already named them — so the only place the list appeared was an error string you had to trigger deliberately. Naming the cheap phases is the documented remedy for a stale editor schema after a contract change, so the right command was previously unguessable. -
generatereports what each phase costs, and names the slowest. A run that overran a CI job timeout emitted no phase boundary and no timing, so a killed job gave no way to tell whether it was 10% or 90% through — which is why the 0.13.0 report could not narrow its own blocker down. Timing is recorded even when a phase raises.
Fixed
- Documentation and export generation no longer rescan the catalogue per object.
childs()walked every model on every call, while all of its callers are per-object: the table exporter per row, the documentation catalogue per signal, andrelations_downstream()recursively at every level. Cost therefore grew with the square of the corpus. It is now a reverse index built once per catalogue generation — 68× faster on a 1497-object corpus. This one function is the root cause behind both agenerate docsphase that exceeded a one-hour CI job timeout and agenerate exportsphase measured at 395s. - A relation could be attributed to the wrong object.
childs()matched a model id against reference values with a substring test, so a short non-UUID id matched a longer one containing it:o-1matched a signal referencingo-186, returning 113 spurious children. Comparison is now by equality. UUID-shaped ids could not trigger this; any other id scheme could. validateandgenerateagreed to disagree.get_vocab_entry()stripped the stage prefix unconditionally before lookup whileis_valid()checked the raw value first, so a repository could be green onvalidateand still produce degraded documentation. Both now resolve through one raw-first helper. ThreeSecurity::*surface keys resolved to a different entry under the stripping path and three more missed entirely; all six are reachable now.check_statusresolved a status by name, not by strategy, so renaming a configured deployment status silently took the wrong branch. It now resolves the strategy, and raises typed errors in place of a bareException.opentide mcpadvised an install that could not help. Withmcpunimportable, bothmcp startandsetup mcptold you to runpip install 'opentide[mcp]'— but that extra is now an empty alias, so following the advice installed nothing. They name the real remedy, and tell an incomplete environment apart from an incompatiblemcp2.x.- The docs advertised a release they did not install. Four pages named 0.12.1 as the latest release beside their own
pip install 'opentide==0.14.0'block. That is #406, closed at 0.8.0 and silently regressed through 0.13.0: the release tests covered the release commands, and nothing covered the version claims. Every claim, pin and PyPI link outside the historical release log is now checked against the newest changelog entry on every test run. - The test suite could rewrite the repository under test. Git tests ran
gitwithcwd=set to a temporary repository but inherited the environment, andGIT_DIRoverridescwd. Whengit pushexportedGIT_DIRto its hooks and a hook ran the suite, the fixtures committed and checked out branches in the real repository — producing a commit that deleted 890 files. The environment is now sanitised for every test, with a collection-time guard so the hazard cannot return silently. This affects contributors, not released behaviour.
Verify
opentide migrate vocab-scope # dry run; names every rewrite and every ambiguity
opentide migrate vocab-scope --apply
opentide generate schemas templates vocabs snippets
opentide validate --strictThe generated objective schema should carry 35 stage-prefixed entities values, with Host::Account and Cloud::Account both present and distinct:
python - <<'PY'
import json, pathlib
schema = json.loads(pathlib.Path(".opentide/schemas/objective.1.0.schema.json").read_text())
enum = schema["$defs"]["DetectionSignal"]["properties"]["entities"]["items"]["enum"]
print(len(enum), "values;", sum("::" in v for v in enum), "stage-prefixed")
print("Host::Account" in enum, "Cloud::Account" in enum)
PYThanks
The defect report behind most of this release was measured end to end on a 1276-object corpus with the full pipeline run on a throwaway project. Two of its items were retracted on review — one by the reporter, one by us — and that is exactly what a good report looks like.