From c9090b6f5d5b9d957c7b378460b85844fe2dada7 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 16:06:15 -0600 Subject: [PATCH 01/53] docs: Use refreshed theme from community repo --- docs/book.toml | 5 +- docs/theme/css/content.css | 46 +++++ docs/theme/css/roost-hljs.css | 199 ++++++++++++++++++ docs/theme/css/roost.css | 142 +++++++++++++ docs/theme/fonts/fonts.css | 5 +- docs/theme/index.hbs | 367 ++++++++++++++++++++++++++++++++++ 6 files changed, 759 insertions(+), 5 deletions(-) create mode 100644 docs/theme/css/content.css create mode 100644 docs/theme/css/roost-hljs.css create mode 100644 docs/theme/css/roost.css create mode 100644 docs/theme/index.hbs diff --git a/docs/book.toml b/docs/book.toml index c663872b7..45aff97d1 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -8,9 +8,12 @@ src = "." build-dir = "./book" [output.html] -site-url = "/osprey/" +default-theme = "roost" +preferred-dark-theme = "roost" +additional-css = ["theme/css/roost.css", "theme/css/roost-hljs.css", "theme/css/content.css"] edit-url-template = "https://github.com/roostorg/osprey/edit/main/docs/{path}" git-repository-url = "https://github.com/roostorg/osprey" +site-url = "/osprey/" [output.html.print] enable = false diff --git a/docs/theme/css/content.css b/docs/theme/css/content.css new file mode 100644 index 000000000..632f61975 --- /dev/null +++ b/docs/theme/css/content.css @@ -0,0 +1,46 @@ +/* Content styling overrides */ + +.content img { + border: 0.5em solid color-mix(var(--bg) 80%, white); + box-shadow: 0 0.25em 0.5em 0.25em rgba(0 0 0 / 0.1 ); + box-sizing: border-box; + margin-block: 0.5em; +} + +.blockquote-tag { + background: var(--quote-bg); + border-radius: 0.25em; + padding: 1em; +} + +.menu-title { + font-weight: 600; +} + +table { + width: 100%; +} + +table td, +table thead th { + padding: 0.25em 0.5em; +} + +table td { + text-wrap: balance; +} + +dt { + margin-block: 1.5em 0; +} + +dd { + margin-inline-start: 1em; + margin-block: 0.25em; +} + +hr { + margin-block: 2em; + opacity: 0.25; +} + diff --git a/docs/theme/css/roost-hljs.css b/docs/theme/css/roost-hljs.css new file mode 100644 index 000000000..7fadf1fd5 --- /dev/null +++ b/docs/theme/css/roost-hljs.css @@ -0,0 +1,199 @@ +/*! + Theme: a11y-light + Author: @ericwbailey + Maintainer: @ericwbailey + + Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css +*/ + +.roost .hljs { + background: #fefefe; + color: #545454; +} + +/* Comment */ +.roost .hljs-comment, +.roost .hljs-quote { + color: #696969; +} + +/* Red */ +.roost .hljs-variable, +.roost .hljs-template-variable, +.roost .hljs-tag, +.roost .hljs-name, +.roost .hljs-selector-id, +.roost .hljs-selector-class, +.roost .hljs-regexp, +.roost .hljs-deletion { + color: #d91e18; +} + +/* Orange */ +.roost .hljs-number, +.roost .hljs-built_in, +.roost .hljs-literal, +.roost .hljs-type, +.roost .hljs-params, +.roost .hljs-meta, +.roost .hljs-link { + color: #aa5d00; +} + +/* Yellow */ +.roost .hljs-attribute { + color: #aa5d00; +} + +/* Green */ +.roost .hljs-string, +.roost .hljs-symbol, +.roost .hljs-bullet, +.roost .hljs-addition { + color: #008000; +} + +/* Blue */ +.roost .hljs-title, +.roost .hljs-section { + color: #007faa; +} + +/* Purple */ +.roost .hljs-keyword, +.roost .hljs-selector-tag { + color: #7928a1; +} + +.roost .hljs-emphasis { + font-style: italic; +} + +.roost .hljs-strong { + font-weight: bold; +} + +@media screen and (-ms-high-contrast: active) { + .roost .hljs-addition, + .roost .hljs-attribute, + .roost .hljs-built_in, + .roost .hljs-bullet, + .roost .hljs-comment, + .roost .hljs-link, + .roost .hljs-literal, + .roost .hljs-meta, + .roost .hljs-number, + .roost .hljs-params, + .roost .hljs-string, + .roost .hljs-symbol, + .roost .hljs-type, + .roost .hljs-quote { + color: highlight; + } + + .roost .hljs-keyword, + .roost .hljs-selector-tag { + font-weight: bold; + } +} + +@media (prefers-color-scheme: dark) { + /*! + Theme: a11y-dark + Author: @ericwbailey + Maintainer: @ericwbailey + + Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css + */ + + .roost .hljs { + background: #2b2b2b; + color: #f8f8f2; + } + + /* Comment */ + .roost .hljs-comment, + .roost .hljs-quote { + color: #d4d0ab; + } + + /* Red */ + .roost .hljs-variable, + .roost .hljs-template-variable, + .roost .hljs-tag, + .roost .hljs-name, + .roost .hljs-selector-id, + .roost .hljs-selector-class, + .roost .hljs-regexp, + .roost .hljs-deletion { + color: #ffa07a; + } + + /* Orange */ + .roost .hljs-number, + .roost .hljs-built_in, + .roost .hljs-literal, + .roost .hljs-type, + .roost .hljs-params, + .roost .hljs-meta, + .roost .hljs-link { + color: #f5ab35; + } + + /* Yellow */ + .roost .hljs-attribute { + color: #ffd700; + } + + /* Green */ + .roost .hljs-string, + .roost .hljs-symbol, + .roost .hljs-bullet, + .roost .hljs-addition { + color: #abe338; + } + + /* Blue */ + .roost .hljs-title, + .roost .hljs-section { + color: #00e0e0; + } + + /* Purple */ + .roost .hljs-keyword, + .roost .hljs-selector-tag { + color: #dcc6e0; + } + + .roost .hljs-emphasis { + font-style: italic; + } + + .roost .hljs-strong { + font-weight: bold; + } + + @media screen and (-ms-high-contrast: active) { + .roost .hljs-addition, + .roost .hljs-attribute, + .roost .hljs-built_in, + .roost .hljs-bullet, + .roost .hljs-comment, + .roost .hljs-link, + .roost .hljs-literal, + .roost .hljs-meta, + .roost .hljs-number, + .roost .hljs-params, + .roost .hljs-string, + .roost .hljs-symbol, + .roost .hljs-type, + .roost .hljs-quote { + color: highlight; + } + + .roost .hljs-keyword, + .roost .hljs-selector-tag { + font-weight: bold; + } + } +} diff --git a/docs/theme/css/roost.css b/docs/theme/css/roost.css new file mode 100644 index 000000000..2f3b6bc32 --- /dev/null +++ b/docs/theme/css/roost.css @@ -0,0 +1,142 @@ +.roost, +html:not(.js) { + color-scheme: light dark; + background-color: var(--roost-charcoal); + + --roost-gray: rgb(187 193 190); + --roost-charcoal: hsl(80, 22%, 13%); + --roost-yellow: rgb(238 238 0); + --roost-peach: rgb(247 187 128); + --roost-muddy: rgb(229 217 136); + + --color-scheme: light; + --bg: rgba(250 250 250); + --fg: var(--roost-charcoal); + --sidebar-header-border-color: oklch(from var(--roost-yellow) calc(l * 0.95) c h); + --sidebar-resize-indicator-space: 0px; + --sidebar-bg: var(--roost-charcoal); + --sidebar-fg: rgba(255 255 255 / 0.9); + --sidebar-non-existant: #5c6773; + --sidebar-active: var(--roost-muddy); + --sidebar-spacer: var(--roost-muddy); + + --icons: white; + --icons-hover: var(--roost-yellow); + --links: inherit; + + --theme-popup-bg: var(--bg); + --theme-popup-border: color-mix(var(--fg) 15%, transparent); + --theme-hover: color-mix(var(--fg) 4%, transparent); + + --quote-bg: color-mix(var(--fg) 3%, transparent); + --quote-border: color-mix(var(--fg) 10%, transparent); + + --searchbar-bg: color-mix(var(--fg) 3%, transparent); + --searchbar-fg: inherit; + --searchbar-border-color: color-mix(var(--fg) 10%, transparent); + --searchbar-shadow-color: var(--roost-peach); + --searchbar-margin-block-start: 2em; + + --warning-border: color-mix(orange, var(--fg)); + + --table-border-color: color-mix(var(--fg) 7%, var(--bg)); + --table-header-bg: color-mix(var(--fg) 10%, var(--bg)); + --table-alternate-bg: color-mix(var(--fg) 3%, var(--bg)); + + --scrollbar: color-mix(var(--fg), transparent); + + --inline-code-color: inherit; + + --searchresults-header-fg: color-mix(var(--roost-peach) 75%, var(--fg)); + --searchresults-border-color: color-mix(var(--fg), transparent); + --searchresults-li-bg: color-mix(var(--roost-yellow) 20%, transparent); + --search-mark-bg: color-mix(var(--roost-yellow), transparent); + --footnote-highlight: var(--search-mark-bg); + + --copy-button-filter: none; + --copy-button-filter-hover: invert(20%); + + --blockquote-note-color: color-mix(blue, var(--fg)); + --blockquote-tip-color: color-mix(lime, var(--fg)); + --blockquote-important-color: color-mix(fuchsia, var(--fg)); + --blockquote-warning-color: var(--warning-border); + --blockquote-caution-color: color-mix(red, var(--fg)); + + --overlay-bg: color-mix(var(--fg), transparent); +} + +html:not(.js) { + --sidebar-resize-indicator-width: 0px; +} + +@media (prefers-color-scheme: dark) { + .roost, + html:not(.js) { + --color-scheme: dark; + --bg: color-mix(var(--roost-charcoal) 90%, black); + --fg: rgba(255 255 255 / 0.9); + --copy-button-filter: invert(80%); + --copy-button-filter-hover: invert(100%); + } +} + +/* Theme overrides */ + +.roost body { + background-color: var(--bg); + min-height: calc(100vh + var(--menu-bar-height)); +} + +.roost #mdbook-menu-bar { + background-color: var(--roost-charcoal); + border: none; + box-shadow: 0 0.25em 1em rgba(0 0 0 / 0.25); +} + +.roost #mdbook-theme-toggle { + x-display: none; +} + +.roost #mdbook-searchbar { + border-radius: 999px; + transition: none; +} + +.roost #mdbook-searchbar:focus-visible { + outline: 1px solid color-mix(var(--searchbar-shadow-color), transparent); +} + +.roost ul#mdbook-searchresults li { + margin-block: 1.25em; +} + +.roost #mdbook-sidebar { + z-index: 102; +} + +.roost #mdbook-sidebar-resize-handle { + background-color: var(--sidebar-bg); +} + +.roost :not(pre):not(a) > .hljs { + background: color-mix(var(--fg) 10%, transparent); +} + +.roost .nav-wide-wrapper { + --icons: color-mix(var(--fg) 25%, transparent); + --icons-hover: var(--fg); +} + +.roost .content { + padding: 2em 1em 1em; +} + +.roost .content a { + font-weight: bold; + text-decoration: underline; +} + +.roost a:link.mobile-nav-chapters { + color: var(--roost-yellow); +} + diff --git a/docs/theme/fonts/fonts.css b/docs/theme/fonts/fonts.css index 5180f0016..3f6bd3755 100644 --- a/docs/theme/fonts/fonts.css +++ b/docs/theme/fonts/fonts.css @@ -7,13 +7,10 @@ font-style: normal; } + h1, h2, h3, h4, h5, h6 { font-family: "Funnel Display", sans-serif; font-optical-sizing: auto; font-weight: 700; font-style: normal; } - -.content a { - text-decoration: underline; -} diff --git a/docs/theme/index.hbs b/docs/theme/index.hbs new file mode 100644 index 000000000..1c790abe6 --- /dev/null +++ b/docs/theme/index.hbs @@ -0,0 +1,367 @@ + + + + + + {{ title }} + {{#if is_print }} + + {{/if}} + {{#if base_url}} + + {{/if}} + + + + {{> head}} + + + + + + {{#if favicon_svg}} + + {{/if}} + {{#if favicon_png}} + + {{/if}} + + + + {{#if print_enable}} + + {{/if}} + + + + + + + + + + + {{#each additional_css}} + + {{/each}} + + {{#if mathjax_support}} + + + {{/if}} + + + + + + + +
+
+

Keyboard shortcuts

+
+

Press or to navigate between chapters

+ {{#if search_enabled}} +

Press S or / to search in the book

+ {{/if}} +

Press ? to show this help

+

Press Esc to hide this help

+
+
+
+
+ + + + + + + + + + + + + +
+ +
+ {{> header}} +
+ + + {{#if search_enabled}} + + {{/if}} + + + + + +
+ + + +
+ + + + + + + + {{#if live_reload_endpoint}} + + + {{/if}} + + {{#if playground_line_numbers}} + + {{/if}} + + {{#if playground_copyable}} + + {{/if}} + + {{#if playground_js}} + + + + + + {{/if}} + + {{#if search_js}} + + + + {{/if}} + + + + + + + {{#each additional_js}} + + {{/each}} + + {{#if is_print}} + {{#if mathjax_support}} + + {{else}} + + {{/if}} + {{/if}} + + {{#if fragment_map}} + + {{/if}} + +
+ + From 19698197cca2ddd2ddd422886fa4ba8ab582d649 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 16:54:25 -0600 Subject: [PATCH 02/53] docs: fix broken link to research-personas.md in README The user research link pointed to docs/user_personas.md, which doesn't exist; the actual file is docs/research-personas.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a1ea1a30..71b56eb7d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Osprey is an event stream decisions engine and analysis UI designed to investiga Osprey is a library for processing actions through human-written rules and outputting verdicts & custom effects back to configurable output sinks. It evaluates events using structured rule logic (SML) that is extendable via user-defined functions (UDFs). Osprey can also track state across events by labelling entities if implementers provide a labels service backend (see [labels_service.py](./example_plugins/src/services/labels_service.py) for a Postgres-backed labels service example). -Osprey is built for engineers and Trust & Safety teams who want to explore, test, and integrate its core capabilities into their platform for incident response and Trust & Safety investigation. [Read more about user research and personas](docs/user_personas.md). +Osprey is built for engineers and Trust & Safety teams who want to explore, test, and integrate its core capabilities into their platform for incident response and Trust & Safety investigation. [Read more about user research and personas](docs/research-personas.md). ## Adopters From 2585b798b4f1131084d0079ca58f6510e12c55e2 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 16:55:35 -0600 Subject: [PATCH 03/53] docs: fix stale docs/DEVELOPMENT.md references in AGENTS.md docs/DEVELOPMENT.md doesn't exist; the development setup guide actually lives at docs/development/README.md. --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e7fae7b57..f196d905f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ Top-level modules: - `example_plugins/` — reference plugins (UDFs, output sinks, labels service) using the pluggy-based plugin system. Do not add production code here. - `example_rules/` — sample SML rules and YAML config. -Reference files: `docs/DEVELOPMENT.md` (setup), `example_plugins/src/register_plugins.py` (plugin patterns), `example_plugins/src/services/labels_service.py` (labels service example). +Reference files: `docs/development/README.md` (setup), `example_plugins/src/register_plugins.py` (plugin patterns), `example_plugins/src/services/labels_service.py` (labels service example). ## Design @@ -157,7 +157,7 @@ uv tool run fawltydeps --check-unused --pyenv .venv - Do not disable lint or type rules to silence errors. Fix the underlying issue, or use a narrowly-scoped `# noqa: ` / `# type: ignore[]` with a comment explaining why. - Before adding a new dependency, check it for known CVEs and confirm the license is compatible with `LICENSE.md`. - Do not commit generated protobuf files from an untrusted toolchain; always regenerate via `./gen-protos.sh`. -- Default Docker bindings are `127.0.0.1`; do not change bind addresses without explicit instruction (see `docs/DEVELOPMENT.md` §6). +- Default Docker bindings are `127.0.0.1`; do not change bind addresses without explicit instruction (see `docs/development/README.md` §6). ## Code review From d5c1967f778d7bec64fcb55b78525db6a2598f70 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 16:56:48 -0600 Subject: [PATCH 04/53] docs: match manage.md section headers to on-page "Registry" titles The Features and Rules pages in the UI are titled "Features Registry" and "Rules Registry" (added alongside the Registry pages), but the docs still called them "Features" and "Rules". Updated the headers and the anchor links pointing to them from the User Guide overview. --- docs/user/README.md | 4 ++-- docs/user/manage.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/user/README.md b/docs/user/README.md index b79ff014a..44474a01e 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -14,8 +14,8 @@ Osprey's tools are categorized into three primary functions: - [Rules Visualizer](manage.md#rules-visualizer): graph view of label/rule relationships - [UDF Registry](manage.md#udf-registry): API reference for all available functions -- [Features](manage.md#features): inventory of all features in the system -- [Rules](manage.md#rules): inventory of all rules in the system +- [Features Registry](manage.md#features-registry): inventory of all features in the system +- [Rules Registry](manage.md#rules-registry): inventory of all rules in the system **Operate**: run and review bulk operations diff --git a/docs/user/manage.md b/docs/user/manage.md index 36ed4225e..500e0d319 100644 --- a/docs/user/manage.md +++ b/docs/user/manage.md @@ -31,9 +31,9 @@ UDFs are organized by category and are searchable. Each entry shows: Use this page as your reference when writing queries or rules: especially to confirm a function's exact name and parameter order before using it. (Querying a UDF that doesn't exist causes a silent 500 error.) -## Features +## Features Registry -The Features page lists every feature defined in your Osprey deployment. Features are named variables extracted from events; they're what you query against and what rules operate on. +The Features Registry lists every feature defined in your Osprey deployment. Features are named variables extracted from events; they're what you query against and what rules operate on. The list is paginated (50 per page) and can be filtered and sorted: @@ -45,9 +45,9 @@ The list is paginated (50 per page) and can be filtered and sorted: Each row shows the feature's name, category, extraction function(s), reference count (how many rules use it), description, owner, and last modified date. -## Rules +## Rules Registry -The Rules page lists every rule loaded in your Osprey deployment. +The Rules Registry lists every rule loaded in your Osprey deployment. The list is paginated (50 per page) and can be filtered and sorted: From 2b6031fa3a960b25f6a1719e7485ac97a7730efb Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 16:59:49 -0600 Subject: [PATCH 05/53] docs: add CLI reference for osprey-cli, osprey-stress, osprey-async-cli None of Osprey's three console-script CLIs were documented anywhere. Flags and subcommands here are verified against the actual click/argparse definitions, and cross-checked against real --help output where the sandboxed environment allowed running them. --- docs/SUMMARY.md | 1 + docs/development/cli-reference.md | 111 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 docs/development/cli-reference.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7ad17d182..c093b3365 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -17,6 +17,7 @@ - [Development Guide](development/README.md) - [Workflow](development/workflow.md) - [Tools Overview](development/tools.md) + - [CLI Reference](development/cli-reference.md) - [Troubleshooting](development/troubleshooting.md) - [IDE Setup](development/ide.md) - [Release Process](development/releases.md) diff --git a/docs/development/cli-reference.md b/docs/development/cli-reference.md new file mode 100644 index 000000000..c96688133 --- /dev/null +++ b/docs/development/cli-reference.md @@ -0,0 +1,111 @@ +# CLI Reference + +Osprey ships three command-line tools, each installed as a console script by its package's `pyproject.toml`. Run any of them with `uv run --help` from the repo root, or ` --help` inside an environment where the package is installed. + +## osprey-cli + +Installed by `osprey_worker` (`osprey.worker.lib.cli:cli`). General-purpose worker administration: pushing rules, opening a debug shell, and applying labels manually. + +### push_rules + +```bash +osprey-cli push_rules RULES_PATH [--dry-run/--no-dry-run] [--suppress-warnings] +``` + +Validates the rules at `RULES_PATH` and pushes them. Use `--dry-run` to validate without pushing. Exits non-zero if validation fails. + +### compute_and_upload_dependencies_mapping + +```bash +osprey-cli compute_and_upload_dependencies_mapping RULES_PATH [--suppress-warnings] +``` + +Computes the dependency graph for the rules at `RULES_PATH` and uploads it — this is what powers the [Rules Visualizer](../user/manage.md#rules-visualizer). + +### shell + +```bash +osprey-cli shell [-i / --auto-import / --no-auto-import] +``` + +Opens an interactive shell (IPython if installed, otherwise a fallback `code.InteractiveConsole`) with `labels`, `access_audit_log`, and `stored_execution_result` storage modules pre-imported, plus `EntityT`, `EntityLabelMutation`, and `LabelStatus`. With `--auto-import` (the default), it also imports every model class it can find under `osprey_lib`. Useful for interactively inspecting stored data. + +### apply_label + +```bash +osprey-cli apply_label ENTITY_TYPE ENTITY_ID LABEL_NAME LABEL_STATUS \ + [--reason REASON] [--description DESCRIPTION] [--expire-instantly] +``` + +Manually applies a label to a single entity. Mainly intended for debugging or importing individual labels from an external source. `--reason` defaults to `CliLabelMutationWithoutEffects`; `--description` defaults to `"Manually changed from the command line for debugging."`. `--expire-instantly` makes the label expire almost immediately rather than persisting. + +This requires a labels provider to be configured for the Osprey instance — it fails with an assertion error otherwise. + +### bulk_apply_label + +```bash +osprey-cli bulk_apply_label ENTITY_TYPE ENTITY_IDS_FILE_PATH LABEL_NAME LABEL_STATUS \ + [--reason REASON] [--description DESCRIPTION] [--expire-instantly] +``` + +Same as `apply_label`, but reads entity IDs (one per line) from `ENTITY_IDS_FILE_PATH` and applies the label to all of them, printing progress as it goes. This is the CLI path for importing bulk label lists from external sources — see also the UI-driven [Bulk Actions](../user/operate.md#bulk-actions) workflow. + +## osprey-stress + +Installed by `osprey_worker` (`osprey.worker.stress.cli:main`). Runs an end-to-end stress test against a live Osprey worker: produces synthetic events, consumes the resulting execution results, and reports drop rate and latency. Useful for validating dependency bumps, measuring throughput regressions, and gating CI on pipeline health. + +### run + +```bash +osprey-stress run \ + --events 10000 --rate 1000 \ + --threshold-drop-rate 0.01 --threshold-p95-ms 500 \ + --report json +``` + +Produces `--events` synthetic events (default `1000`) at `--rate` events/second (default `100.0`) to the input topic, then waits up to `--drain-seconds` (default `30.0`) for the worker to finish processing before reporting. + +Common flags: +- `--report {human,json}` — output format (default `human`) +- `--verbose` — emit periodic progress lines to stderr, with `--verbose-interval-seconds` (default `2.0`) controlling frequency +- `--bootstrap-servers` (default `localhost:9092`), `--input-topic` (default `osprey.actions_input`), `--output-topic` (default `osprey.execution_results`) +- `--threshold-drop-rate` and `--threshold-p95-ms` — if set, the command exits non-zero when the observed drop rate or p95 latency breaches the threshold, so it can gate a CI job + +### measure + +```bash +osprey-stress measure [--duration SECONDS] [--report {human,json}] +``` + +Reserved for open-loop measurement against externally-produced events, once the jetstream input stream plugin (#236) lands. Today it prints a stub message and exits non-zero — use `run` for closed-loop synthetic testing in the meantime. + +## osprey-async-cli + +Installed by `osprey_async_worker` (`osprey.async_worker.cli.main:cli`). **Experimental** — this is the asyncio-native worker prototype (no gevent, no monkey-patching), for validating whether an asyncio-based executor can replace the gevent one. Not intended for production use yet. + +### run + +```bash +osprey-async-cli run --rules-path PATH [--input-file PATH] [--max-concurrent 12] \ + [--with-plugins] [--input-source {file,kafka}] \ + [--kafka-topic TOPIC] [--kafka-bootstrap-servers SERVERS] \ + [--kafka-group-id GROUP] [--kafka-offset-reset {latest,earliest}] +``` + +Runs the async worker against a static rules directory (`--rules-path`, required). By default it uses stdlib UDFs only and prints results to stdout; `--with-plugins` loads the full async plugin system (UDFs, validators, and output sinks registered via the `osprey_async_plugin` entry-point group), which requires external services to be available. + +Input source is controlled by `--input-source`: +- `file` (default) — reads JSONL actions from `--input-file`, or runs with no input if omitted (useful for just validating the worker boots) +- `kafka` — consumes from `--kafka-topic` (default `osprey.actions_input`) via `--kafka-bootstrap-servers` (default `localhost:9092`) + +### benchmark + +```bash +osprey-async-cli benchmark --rules-path PATH --input-file PATH \ + [--max-concurrent 12] [--iterations 1000] [--warmup 50] +``` + +Benchmarks the async executor against the gevent executor (if `gevent` is importable) using the same rules and input data, running `--warmup` iterations first, then `--iterations` timed iterations, and prints a throughput/latency comparison. + +> [!NOTE] +> Verify the exact `--help` output for each command against a running dev environment (e.g. via `docker compose` or `uv run --help`) before relying on it — the flags documented here come directly from the current source, but are worth a final sanity check since these are actively evolving tools. From 07af56e24eb6e7ec05098c2187366864584ae168 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 17:00:47 -0600 Subject: [PATCH 06/53] docs: link operate.md bulk actions to CLI reference Bulk labeling from the command line (apply_label/bulk_apply_label) was otherwise undiscoverable from the UI-focused Operate docs. --- docs/user/operate.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/operate.md b/docs/user/operate.md index 73884b327..6c1751058 100644 --- a/docs/user/operate.md +++ b/docs/user/operate.md @@ -20,6 +20,8 @@ There are two ways to start a bulk action: Before submitting, Osprey shows a count of how many unique entities will be labeled. Review this number carefully to understand the impact of your bulk action. +> For scripted or bulk labeling from the command line (e.g., importing label lists from external sources), see `apply_label` and `bulk_apply_label` in the [CLI Reference](../development/cli-reference.md#apply_label). + Each job requires: - The entity type and label to apply - A reason (required for all labeling operations) From dddf8dd846891d81723c381e6e4e986b5047a691 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 17:10:37 -0600 Subject: [PATCH 07/53] docs: explain why Osprey rules use SML instead of plain Python (closes #11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answers a recurring adopter question: SML's restrictions (no loops, assign-once names, static typing) aren't arbitrary — they're what let the engine run untrusted rule logic safely and evaluate it in parallel. Also honest about the current gap: there's no dedicated SML tooling yet. --- docs/rules.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/rules.md b/docs/rules.md index 53e1494d6..3f7ca121b 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -55,6 +55,17 @@ My_Rule_Name_v2 = Rule( ) ``` +## Why a bespoke language instead of plain Python? + +It's a fair question: why introduce a bespoke language at all, instead of writing rules in plain Python? The short answer is that SML's restrictions are load-bearing — they're what make Osprey's rule engine safe to run untrusted, third-party-authored logic in production, and fast enough to do it in real time. + +- **The grammar is deliberately small.** The SML parser (`osprey_worker/src/osprey/engine/ast/py_ast.py`) only understands a restricted set of Python AST nodes — assignments, calls, comparisons, boolean/binary operators, literals, and f-strings. There's no `for`/`while`, no `def`/`class`, no arbitrary `import`. Anything outside that set raises a syntax error explaining that "the Osprey rule language is a simplified subset of python." This isn't an accident of an unfinished parser; it rules out unbounded loops and recursion by construction, so a rule can't hang or DoS the worker. +- **Every name is assign-once.** The `UniqueStoredNames` validator rejects re-declaring the same name anywhere in the ruleset, and `NoUnusedLocals` rejects declaring a name that's never read. Combined, every rule and feature compiles down to a single, static, acyclic dependency graph (`execution_graph.py`) rather than an imperative script with mutable state. +- **That static graph is what makes the engine fast and introspectable.** Because nothing can have side effects or run in a loop, the executor can safely evaluate independent branches of the graph in parallel using gevent greenlets (`executor.py`), and the UI can render the exact dependency graph for any rule or feature — that's what powers the [Rules Visualizer](user/manage.md#rules-visualizer) and the cross-referencing in the [Rules](user/manage.md#rules-registry) and [Features Registry](user/manage.md#features-registry) pages. Plain Python doesn't give you that for free; you'd have to reconstruct it by statically analyzing arbitrary code, which is a much harder problem. +- **Types are checked before a rule ever runs against live traffic.** `validate_static_types.py` and `validate_call_rvalue.py` catch type errors and misused return values at validation/push time, not as a runtime `AttributeError` in production the first time a rare code path executes. + +None of that is free, and it's worth being honest about the cost: **there's no dedicated SML syntax highlighting or language server today.** Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable (if imperfect) highlighting as a stopgap, but proper tooling is a known gap rather than a solved problem — see the [Development Guide](development/README.md) for current editor setup. + ## Rule Structuring You will likely find it useful to maintain two subdirectories inside of your main rules directory - a `rules` directory where actual logic will be added and a `models` directory for defining the various features that occur in any or specific event types. For example, your structure may look something like this: From ff307b850fcf5ee8e85de51ae5cb71027f8606f8 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 17:19:51 -0600 Subject: [PATCH 08/53] =?UTF-8?q?docs:=20document=20Osprey's=20data=20flow?= =?UTF-8?q?=20=E2=80=94=20getting=20data=20in=20and=20out=20(closes=20#381?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New adopters had no single page explaining how events actually reach Osprey (Kafka/PubSub/coordinator gRPC), how results come back out (output sinks + execution result stores), or how to run against a custom platform via the register_input_stream/ register_action_proto_deserializer plugin hooks instead of forking. --- docs/SUMMARY.md | 1 + docs/data-flow.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 docs/data-flow.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c093b3365..5b44e3118 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -28,6 +28,7 @@ - [Writing Rules](rules.md) - [Integrations & Plugins](integrations.md) +- [Data Flow: Getting Data In and Out](data-flow.md) - [User Research & Personas](research-personas.md) --- diff --git a/docs/data-flow.md b/docs/data-flow.md new file mode 100644 index 000000000..026c3bca8 --- /dev/null +++ b/docs/data-flow.md @@ -0,0 +1,97 @@ +# Data Flow: Getting Data In and Out + +This page covers how events actually reach Osprey, how results come back out, and what to change (not fork) if your platform doesn't look like the default docker-compose setup. + +## Architecture at a glance + +At its core, Osprey is a pipeline: events come in, get evaluated against your SML rules, and verdicts/effects go out. + +``` + Kafka topic(s) / PubSub / gRPC + │ + ▼ + Osprey Coordinator (Rust, optional) + │ + ▼ + Osprey Worker (Python) + evaluates rules against the + event, produces an + ExecutionResult + │ + ▼ + Output sink(s) + execution result store + (stdout, Kafka, Postgres, GCS, MinIO, BigTable, or your own plugin) + │ + ▼ + Druid + UI API → Osprey UI +``` + +For the worker's own rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals — priority queues, bidirectional streaming vs. synchronous API — see the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md), which has a more detailed component diagram and a working example. + +## Getting data in + +There are three real ways to get an event into Osprey: + +### 1. Kafka (the default) + +The worker consumes from a Kafka topic — `osprey.actions_input` by default (`OSPREY_KAFKA_INPUT_STREAM_TOPIC` in `docker-compose.yaml`; see `KafkaInputStream` in `osprey_worker/src/osprey/worker/sinks/sink/input_stream.py`). Each message is a JSON blob shaped like: + +```json +{ + "send_time": "", + "data": { + "action_id": 123, + "action_name": "user_login", + "data": { "...": "your event's actual fields" } + } +} +``` + +`action_name` is what your rules match against; the inner `data` object is whatever fields your event has — those become the values your UDFs and feature extractors read. + +### 2. Google Cloud PubSub + +An alternate input source, selected via `InputStreamSource.PUBSUB` (`osprey_worker/src/osprey/worker/sinks/input_stream_chooser.py`). Configured with `PUBSUB_OSPREY_PROJECT_ID` and `PUBSUB_OSPREY_RULES_SINK_SUBSCRIPTION`. + +### 3. The Coordinator's synchronous gRPC API + +If you run Osprey with the optional Rust coordinator (`osprey_coordinator/`), external services can submit a single action directly and get an immediate response, without going through Kafka at all — useful when a caller needs a synchronous verdict rather than firing into a queue. This is the Sync Action API on port `19951` (bidirectional streaming for workers is a separate port, `19950`). See the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md) for a working `grpcurl` example and full setup. + +### Running against your own platform, without forking + +If your events don't come from Kafka/PubSub/the coordinator, or don't arrive as that JSON envelope, you don't need to fork Osprey — there are two plugin hooks for exactly this: + +- **`register_action_proto_deserializer`** — convert your own protobuf `Action` message into the JSON dict shape the engine expects. +- **`register_input_stream`** — swap in an entirely custom input source (a different queue system, a database poller, whatever you have). + +Both are `pluggy` hooks, registered the same way as UDFs and output sinks — see [Integrations & Plugins](development/integrations.md) for the plugin mechanics and `example_plugins/src/register_plugins.py` for a working example. + +## Getting data out + +Once a rule evaluates, the result flows out through two, largely independent mechanisms: + +### Output sinks + +An `ExecutionResult` is handed to every registered `BaseOutputSink` (`register_output_sinks`). Stdlib ships: +- `StdoutOutputSink` — prints to stdout (the default if nothing else is configured; useful for local dev) +- `KafkaOutputSink` — writes to `osprey.execution_results` by default (`OSPREY_KAFKA_OUTPUT_TOPIC`) +- `StoredExecutionResultOutputSink` — persists via whichever `ExecutionResultStore` is configured (see below) + +You can register your own via the same hook — this is the extension point for pushing results into a review queue, a webhook, or any other external system. + +### Execution result storage + +Separately, `ExecutionResultStore` backends persist full execution results for later querying. The active backend is chosen via `OSPREY_EXECUTION_RESULT_STORAGE_BACKEND` (`osprey_worker/src/osprey/worker/_stdlibplugin/execution_result_store_chooser.py`): + +| Value | Backend | +| --- | --- | +| `bigtable` | Google Cloud BigTable | +| `gcs` | Google Cloud Storage | +| `minio` | MinIO (S3-compatible; `OSPREY_MINIO_ENDPOINT`, `OSPREY_MINIO_ACCESS_KEY`, `OSPREY_MINIO_SECRET_KEY`, `OSPREY_MINIO_EXECUTION_RESULTS_BUCKET`) | +| `postgres` | Postgres | +| `plugin` | Your own `register_execution_result_store` implementation | +| `none` (default) | No persistence | + +### How results become queryable in the UI + +Druid consumes the worker's Kafka output and powers the UI API's real-time querying — this is what backs the [Investigate](user/investigate/README.md) query interface and the [Rules](user/manage.md#rules-registry)/[Features Registries](user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. From 88dc3e5bf728815396ac87df31c7b8660ced2518 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 17:20:29 -0600 Subject: [PATCH 09/53] docs: add missing plugin hooks to development README hooks table register_validation_exporter and register_label_output_sink exist in osprey_hooks.py but weren't listed alongside the other hookspecs. --- docs/development/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development/README.md b/docs/development/README.md index 80d4956eb..73f459951 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -138,6 +138,8 @@ Implement any subset of these in your plugin's `register_plugins.py`: | `register_input_stream` | `BaseInputStream` | Single-provider (`firstresult`). | | `register_execution_result_store` | `ExecutionResultStore` | Single-provider (`firstresult`). | | `register_labels_service_or_provider` | `LabelsServiceBase \| LabelsProvider` | Single-provider (`firstresult`). | +| `register_validation_exporter` | `BaseValidationResultExporter \| None` | Optional; publishes experiment/bucket metadata after validation. Single-provider (`firstresult`). | +| `register_label_output_sink` | `BaseOutputSink \| None` | Optional; custom label-mutation sink, replacing the default `LabelOutputSink`. Single-provider (`firstresult`). | ## Rules From d7f2c8913f0ec3ad7c02ba9442b411f33c82be3b Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 17:25:44 -0600 Subject: [PATCH 10/53] docs: add Integrations & Plugins guide covering UDFs, sinks, hashing, ML models, and known gaps (closes #383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates the plugin extension points adopters ask about most: UDFs, input/output sinks, the labels service, and three previously undocumented ones (hash-based lookups, ML model integration, review tool integration). The latter three don't map to dedicated features — documented honestly as compositions of existing primitives (or, for review tools, as an acknowledged gap) rather than inventing hooks that don't exist. --- docs/SUMMARY.md | 1 + docs/development/README.md | 2 +- docs/development/integrations.md | 49 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 docs/development/integrations.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 5b44e3118..6708469ff 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -18,6 +18,7 @@ - [Workflow](development/workflow.md) - [Tools Overview](development/tools.md) - [CLI Reference](development/cli-reference.md) + - [Integrations & Plugins](development/integrations.md) - [Troubleshooting](development/troubleshooting.md) - [IDE Setup](development/ide.md) - [Release Process](development/releases.md) diff --git a/docs/development/README.md b/docs/development/README.md index 73f459951..e5ceda1e9 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -108,7 +108,7 @@ The UI will automatically connect to the backend services running in Docker cont ## Plugins -In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/register_plugins.py`: +In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](integrations.md). ```python @hookimpl_osprey diff --git a/docs/development/integrations.md b/docs/development/integrations.md new file mode 100644 index 000000000..3fde76c1c --- /dev/null +++ b/docs/development/integrations.md @@ -0,0 +1,49 @@ +# Integrations & Plugins + +Osprey is extended through a [pluggy](https://pluggy.readthedocs.io/)-based plugin system. A plugin package implements any subset of the hooks in the [Available hooks](README.md#available-hooks) table and registers them via `@hookimpl_osprey`; see `example_plugins/src/register_plugins.py` and `example_plugins/src/register_async_plugins.py` for working examples. This page walks through the concrete integration points adopters ask about most. + +## User-defined functions (UDFs) + +UDFs are how you extend the functions available to rule authors — see [Writing Rules § User Defined Functions](../rules.md#user-defined-functions-udfs) for the language-level reference, including working examples (`TextContains`, `BanUser`) in `example_plugins/src/udfs/`. + +To register your own, implement a `UDFBase` subclass and return it from the `register_udfs` hook. Give it a category from `UdfCategories` (`osprey_worker/src/osprey/engine/stdlib/udfs/categories.py` — e.g. `STRING`, `HASH`, `ENTITY`, `HTTP`) so it's grouped sensibly in the [UDF Registry](../user/manage.md#udf-registry). + +## Input and output sinks + +See [Data Flow § Getting data in](../data-flow.md#getting-data-in) and [§ Getting data out](../data-flow.md#getting-data-out) for the conceptual picture. The relevant hooks: + +- `register_input_stream` / `register_action_proto_deserializer` — bring in events from a source other than Kafka/PubSub/the coordinator. +- `register_output_sinks` — send execution results somewhere other than stdout/Kafka/the configured result store. +- `register_execution_result_store` — persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options. + +## Hash-based lookups + +There's no dedicated lookup-table or lookup-service feature in Osprey today — no denylist/allowlist primitive, no bulk-import-a-set-of-values mechanism. What exists is the `Hash*` UDF family (`osprey_worker/src/osprey/engine/stdlib/udfs/string_hashes.py`: `HashMd5`, `HashSha1`, `HashSha256`, `HashSha512`), which you compose with SML's `in` operator or `HasLabel` to check membership: + +```python +# Check a hashed value against a small inline set +IsKnownBadHash = HashSha256(input=SomeValue) in ['abc123...', 'def456...'] + +# Or check membership via a label that was set by some other process +IsKnownBadActor = HasLabel(entity=SomeEntity, label='KnownBad') +``` + +If you need to check against a large external list (millions of hashes, updated frequently), that's not a built-in feature — write a custom UDF (see above) that queries your own store. + +## Plugging in your own ML models + +There's no dedicated hook for this either — it's the standard UDF pattern. Implement a `UDFBase` subclass whose `execute()` calls out (HTTP, gRPC, an SDK — whatever your model server expects) and returns a score, boolean, or other typed value, then register it via `register_udfs`. There's no built-in outbound-HTTP UDF to build on (`UdfCategories.HTTP` currently has one UDF, `extract_cookie.py`, which parses cookies rather than making requests). + +Because model calls are often slow or costly, gate them with `Require(..., require_if=...)` so they only run when relevant — `docs/rules.md` documents this pattern directly for "a call to an AI service": + +```python +Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register') +``` + +## Connecting to a review tool + +This isn't a shipped integration — "poor integration with review tools (e.g., build filtered review queues)" is listed as an unmet need in [User Research & Personas](../research-personas.md), not something Osprey currently connects to out of the box. The closest real extension points are the output-sink hooks: `register_output_sinks` for execution results generally, or `register_label_output_sink` for a custom sink specifically for label mutations (replacing the default `LabelOutputSink`). Either could push into a review queue, but there's no purpose-built connector today. + +## Labels service + +If you want Osprey to track state across events (e.g., "this user has 3 prior violations"), implement `register_labels_service_or_provider`. `example_plugins/src/services/labels_service.py` is a full reference implementation backed by Postgres — a good starting point if you're building your own. From ea44720988d1f9615cbf265fc888049cbca61664 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 18:18:45 -0600 Subject: [PATCH 11/53] docs: style pass on new pages, cut space-separated em-dashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced with colons, semicolons, commas, or restructured sentences depending on context, and varied a repeated "— this is..." construction that had crept into several pages. --- docs/data-flow.md | 26 +++++++++++++------------- docs/development/cli-reference.md | 22 +++++++++++----------- docs/development/integrations.md | 22 +++++++++++----------- docs/rules.md | 8 ++++---- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/data-flow.md b/docs/data-flow.md index 026c3bca8..886a9bcb4 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -26,7 +26,7 @@ At its core, Osprey is a pipeline: events come in, get evaluated against your SM Druid + UI API → Osprey UI ``` -For the worker's own rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals — priority queues, bidirectional streaming vs. synchronous API — see the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md), which has a more detailed component diagram and a working example. +For the worker's own rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md), which has a more detailed component diagram and a working example. ## Getting data in @@ -34,7 +34,7 @@ There are three real ways to get an event into Osprey: ### 1. Kafka (the default) -The worker consumes from a Kafka topic — `osprey.actions_input` by default (`OSPREY_KAFKA_INPUT_STREAM_TOPIC` in `docker-compose.yaml`; see `KafkaInputStream` in `osprey_worker/src/osprey/worker/sinks/sink/input_stream.py`). Each message is a JSON blob shaped like: +By default, the worker consumes from the Kafka topic `osprey.actions_input` (`OSPREY_KAFKA_INPUT_STREAM_TOPIC` in `docker-compose.yaml`; see `KafkaInputStream` in `osprey_worker/src/osprey/worker/sinks/sink/input_stream.py`). Each message is a JSON blob shaped like: ```json { @@ -47,7 +47,7 @@ The worker consumes from a Kafka topic — `osprey.actions_input` by default (`O } ``` -`action_name` is what your rules match against; the inner `data` object is whatever fields your event has — those become the values your UDFs and feature extractors read. +`action_name` is what your rules match against; the inner `data` object is whatever fields your event has, and those become the values your UDFs and feature extractors read. ### 2. Google Cloud PubSub @@ -55,16 +55,16 @@ An alternate input source, selected via `InputStreamSource.PUBSUB` (`osprey_work ### 3. The Coordinator's synchronous gRPC API -If you run Osprey with the optional Rust coordinator (`osprey_coordinator/`), external services can submit a single action directly and get an immediate response, without going through Kafka at all — useful when a caller needs a synchronous verdict rather than firing into a queue. This is the Sync Action API on port `19951` (bidirectional streaming for workers is a separate port, `19950`). See the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md) for a working `grpcurl` example and full setup. +If you run Osprey with the optional Rust coordinator (`osprey_coordinator/`), external services can submit a single action directly and get an immediate response, without going through Kafka at all, which is useful when a caller needs a synchronous verdict rather than firing into a queue. This is the Sync Action API on port `19951` (bidirectional streaming for workers is a separate port, `19950`). See the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md) for a working `grpcurl` example and full setup. ### Running against your own platform, without forking -If your events don't come from Kafka/PubSub/the coordinator, or don't arrive as that JSON envelope, you don't need to fork Osprey — there are two plugin hooks for exactly this: +If your events don't come from Kafka/PubSub/the coordinator, or don't arrive as that JSON envelope, you don't need to fork Osprey. Two plugin hooks handle exactly this: -- **`register_action_proto_deserializer`** — convert your own protobuf `Action` message into the JSON dict shape the engine expects. -- **`register_input_stream`** — swap in an entirely custom input source (a different queue system, a database poller, whatever you have). +- **`register_action_proto_deserializer`**: convert your own protobuf `Action` message into the JSON dict shape the engine expects. +- **`register_input_stream`**: swap in an entirely custom input source (a different queue system, a database poller, whatever you have). -Both are `pluggy` hooks, registered the same way as UDFs and output sinks — see [Integrations & Plugins](development/integrations.md) for the plugin mechanics and `example_plugins/src/register_plugins.py` for a working example. +Both are `pluggy` hooks, registered the same way as UDFs and output sinks. See [Integrations & Plugins](development/integrations.md) for the plugin mechanics and `example_plugins/src/register_plugins.py` for a working example. ## Getting data out @@ -73,11 +73,11 @@ Once a rule evaluates, the result flows out through two, largely independent mec ### Output sinks An `ExecutionResult` is handed to every registered `BaseOutputSink` (`register_output_sinks`). Stdlib ships: -- `StdoutOutputSink` — prints to stdout (the default if nothing else is configured; useful for local dev) -- `KafkaOutputSink` — writes to `osprey.execution_results` by default (`OSPREY_KAFKA_OUTPUT_TOPIC`) -- `StoredExecutionResultOutputSink` — persists via whichever `ExecutionResultStore` is configured (see below) +- `StdoutOutputSink`: prints to stdout (the default if nothing else is configured; useful for local dev) +- `KafkaOutputSink`: writes to `osprey.execution_results` by default (`OSPREY_KAFKA_OUTPUT_TOPIC`) +- `StoredExecutionResultOutputSink`: persists via whichever `ExecutionResultStore` is configured (see below) -You can register your own via the same hook — this is the extension point for pushing results into a review queue, a webhook, or any other external system. +You can register your own via the same hook; it's the extension point for pushing results into a review queue, a webhook, or any other external system. ### Execution result storage @@ -94,4 +94,4 @@ Separately, `ExecutionResultStore` backends persist full execution results for l ### How results become queryable in the UI -Druid consumes the worker's Kafka output and powers the UI API's real-time querying — this is what backs the [Investigate](user/investigate/README.md) query interface and the [Rules](user/manage.md#rules-registry)/[Features Registries](user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. +Druid consumes the worker's Kafka output and powers the UI API's real-time querying; this is what backs the [Investigate](user/investigate/README.md) query interface and the [Rules](user/manage.md#rules-registry)/[Features Registries](user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. diff --git a/docs/development/cli-reference.md b/docs/development/cli-reference.md index c96688133..48fad1c08 100644 --- a/docs/development/cli-reference.md +++ b/docs/development/cli-reference.md @@ -20,7 +20,7 @@ Validates the rules at `RULES_PATH` and pushes them. Use `--dry-run` to validate osprey-cli compute_and_upload_dependencies_mapping RULES_PATH [--suppress-warnings] ``` -Computes the dependency graph for the rules at `RULES_PATH` and uploads it — this is what powers the [Rules Visualizer](../user/manage.md#rules-visualizer). +Computes the dependency graph for the rules at `RULES_PATH` and uploads it; this is what powers the [Rules Visualizer](../user/manage.md#rules-visualizer). ### shell @@ -39,7 +39,7 @@ osprey-cli apply_label ENTITY_TYPE ENTITY_ID LABEL_NAME LABEL_STATUS \ Manually applies a label to a single entity. Mainly intended for debugging or importing individual labels from an external source. `--reason` defaults to `CliLabelMutationWithoutEffects`; `--description` defaults to `"Manually changed from the command line for debugging."`. `--expire-instantly` makes the label expire almost immediately rather than persisting. -This requires a labels provider to be configured for the Osprey instance — it fails with an assertion error otherwise. +This requires a labels provider to be configured for the Osprey instance; it fails with an assertion error otherwise. ### bulk_apply_label @@ -48,7 +48,7 @@ osprey-cli bulk_apply_label ENTITY_TYPE ENTITY_IDS_FILE_PATH LABEL_NAME LABEL_ST [--reason REASON] [--description DESCRIPTION] [--expire-instantly] ``` -Same as `apply_label`, but reads entity IDs (one per line) from `ENTITY_IDS_FILE_PATH` and applies the label to all of them, printing progress as it goes. This is the CLI path for importing bulk label lists from external sources — see also the UI-driven [Bulk Actions](../user/operate.md#bulk-actions) workflow. +Same as `apply_label`, but reads entity IDs (one per line) from `ENTITY_IDS_FILE_PATH` and applies the label to all of them, printing progress as it goes. This is the CLI path for importing bulk label lists from external sources; see also the UI-driven [Bulk Actions](../user/operate.md#bulk-actions) workflow. ## osprey-stress @@ -66,10 +66,10 @@ osprey-stress run \ Produces `--events` synthetic events (default `1000`) at `--rate` events/second (default `100.0`) to the input topic, then waits up to `--drain-seconds` (default `30.0`) for the worker to finish processing before reporting. Common flags: -- `--report {human,json}` — output format (default `human`) -- `--verbose` — emit periodic progress lines to stderr, with `--verbose-interval-seconds` (default `2.0`) controlling frequency +- `--report {human,json}`: output format (default `human`) +- `--verbose`: emit periodic progress lines to stderr, with `--verbose-interval-seconds` (default `2.0`) controlling frequency - `--bootstrap-servers` (default `localhost:9092`), `--input-topic` (default `osprey.actions_input`), `--output-topic` (default `osprey.execution_results`) -- `--threshold-drop-rate` and `--threshold-p95-ms` — if set, the command exits non-zero when the observed drop rate or p95 latency breaches the threshold, so it can gate a CI job +- `--threshold-drop-rate` and `--threshold-p95-ms`: if set, the command exits non-zero when the observed drop rate or p95 latency breaches the threshold, so it can gate a CI job ### measure @@ -77,11 +77,11 @@ Common flags: osprey-stress measure [--duration SECONDS] [--report {human,json}] ``` -Reserved for open-loop measurement against externally-produced events, once the jetstream input stream plugin (#236) lands. Today it prints a stub message and exits non-zero — use `run` for closed-loop synthetic testing in the meantime. +Reserved for open-loop measurement against externally-produced events, once the jetstream input stream plugin (#236) lands. Today it prints a stub message and exits non-zero; use `run` for closed-loop synthetic testing in the meantime. ## osprey-async-cli -Installed by `osprey_async_worker` (`osprey.async_worker.cli.main:cli`). **Experimental** — this is the asyncio-native worker prototype (no gevent, no monkey-patching), for validating whether an asyncio-based executor can replace the gevent one. Not intended for production use yet. +Installed by `osprey_async_worker` (`osprey.async_worker.cli.main:cli`). **Experimental**: the asyncio-native worker prototype (no gevent, no monkey-patching), for validating whether an asyncio-based executor can replace the gevent one. Not intended for production use yet. ### run @@ -95,8 +95,8 @@ osprey-async-cli run --rules-path PATH [--input-file PATH] [--max-concurrent 12] Runs the async worker against a static rules directory (`--rules-path`, required). By default it uses stdlib UDFs only and prints results to stdout; `--with-plugins` loads the full async plugin system (UDFs, validators, and output sinks registered via the `osprey_async_plugin` entry-point group), which requires external services to be available. Input source is controlled by `--input-source`: -- `file` (default) — reads JSONL actions from `--input-file`, or runs with no input if omitted (useful for just validating the worker boots) -- `kafka` — consumes from `--kafka-topic` (default `osprey.actions_input`) via `--kafka-bootstrap-servers` (default `localhost:9092`) +- `file` (default): reads JSONL actions from `--input-file`, or runs with no input if omitted (useful for just validating the worker boots) +- `kafka`: consumes from `--kafka-topic` (default `osprey.actions_input`) via `--kafka-bootstrap-servers` (default `localhost:9092`) ### benchmark @@ -108,4 +108,4 @@ osprey-async-cli benchmark --rules-path PATH --input-file PATH \ Benchmarks the async executor against the gevent executor (if `gevent` is importable) using the same rules and input data, running `--warmup` iterations first, then `--iterations` timed iterations, and prints a throughput/latency comparison. > [!NOTE] -> Verify the exact `--help` output for each command against a running dev environment (e.g. via `docker compose` or `uv run --help`) before relying on it — the flags documented here come directly from the current source, but are worth a final sanity check since these are actively evolving tools. +> Verify the exact `--help` output for each command against a running dev environment (e.g. via `docker compose` or `uv run --help`) before relying on it. The flags documented here come directly from the current source, but are worth a final sanity check since these are actively evolving tools. diff --git a/docs/development/integrations.md b/docs/development/integrations.md index 3fde76c1c..58cef0023 100644 --- a/docs/development/integrations.md +++ b/docs/development/integrations.md @@ -4,21 +4,21 @@ Osprey is extended through a [pluggy](https://pluggy.readthedocs.io/)-based plug ## User-defined functions (UDFs) -UDFs are how you extend the functions available to rule authors — see [Writing Rules § User Defined Functions](../rules.md#user-defined-functions-udfs) for the language-level reference, including working examples (`TextContains`, `BanUser`) in `example_plugins/src/udfs/`. +UDFs are how you extend the functions available to rule authors. See [Writing Rules § User Defined Functions](../rules.md#user-defined-functions-udfs) for the language-level reference, including working examples (`TextContains`, `BanUser`) in `example_plugins/src/udfs/`. -To register your own, implement a `UDFBase` subclass and return it from the `register_udfs` hook. Give it a category from `UdfCategories` (`osprey_worker/src/osprey/engine/stdlib/udfs/categories.py` — e.g. `STRING`, `HASH`, `ENTITY`, `HTTP`) so it's grouped sensibly in the [UDF Registry](../user/manage.md#udf-registry). +To register your own, implement a `UDFBase` subclass and return it from the `register_udfs` hook. Give it a category from `UdfCategories` (`osprey_worker/src/osprey/engine/stdlib/udfs/categories.py`, e.g. `STRING`, `HASH`, `ENTITY`, `HTTP`) so it's grouped sensibly in the [UDF Registry](../user/manage.md#udf-registry). ## Input and output sinks See [Data Flow § Getting data in](../data-flow.md#getting-data-in) and [§ Getting data out](../data-flow.md#getting-data-out) for the conceptual picture. The relevant hooks: -- `register_input_stream` / `register_action_proto_deserializer` — bring in events from a source other than Kafka/PubSub/the coordinator. -- `register_output_sinks` — send execution results somewhere other than stdout/Kafka/the configured result store. -- `register_execution_result_store` — persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options. +- `register_input_stream` / `register_action_proto_deserializer`: bring in events from a source other than Kafka/PubSub/the coordinator. +- `register_output_sinks`: send execution results somewhere other than stdout/Kafka/the configured result store. +- `register_execution_result_store`: persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options. ## Hash-based lookups -There's no dedicated lookup-table or lookup-service feature in Osprey today — no denylist/allowlist primitive, no bulk-import-a-set-of-values mechanism. What exists is the `Hash*` UDF family (`osprey_worker/src/osprey/engine/stdlib/udfs/string_hashes.py`: `HashMd5`, `HashSha1`, `HashSha256`, `HashSha512`), which you compose with SML's `in` operator or `HasLabel` to check membership: +There's no dedicated lookup-table or lookup-service feature in Osprey today: no denylist/allowlist primitive, no bulk-import-a-set-of-values mechanism. What exists is the `Hash*` UDF family (`osprey_worker/src/osprey/engine/stdlib/udfs/string_hashes.py`: `HashMd5`, `HashSha1`, `HashSha256`, `HashSha512`), which you compose with SML's `in` operator or `HasLabel` to check membership: ```python # Check a hashed value against a small inline set @@ -28,13 +28,13 @@ IsKnownBadHash = HashSha256(input=SomeValue) in ['abc123...', 'def456...'] IsKnownBadActor = HasLabel(entity=SomeEntity, label='KnownBad') ``` -If you need to check against a large external list (millions of hashes, updated frequently), that's not a built-in feature — write a custom UDF (see above) that queries your own store. +If you need to check against a large external list (millions of hashes, updated frequently), that's not a built-in feature: write a custom UDF (see above) that queries your own store. ## Plugging in your own ML models -There's no dedicated hook for this either — it's the standard UDF pattern. Implement a `UDFBase` subclass whose `execute()` calls out (HTTP, gRPC, an SDK — whatever your model server expects) and returns a score, boolean, or other typed value, then register it via `register_udfs`. There's no built-in outbound-HTTP UDF to build on (`UdfCategories.HTTP` currently has one UDF, `extract_cookie.py`, which parses cookies rather than making requests). +There's no dedicated hook for this either; it's the standard UDF pattern. Implement a `UDFBase` subclass whose `execute()` calls out (HTTP, gRPC, an SDK, whatever your model server expects) and returns a score, boolean, or other typed value, then register it via `register_udfs`. There's no built-in outbound-HTTP UDF to build on (`UdfCategories.HTTP` currently has one UDF, `extract_cookie.py`, which parses cookies rather than making requests). -Because model calls are often slow or costly, gate them with `Require(..., require_if=...)` so they only run when relevant — `docs/rules.md` documents this pattern directly for "a call to an AI service": +Because model calls are often slow or costly, gate them with `Require(..., require_if=...)` so they only run when relevant. `docs/rules.md` documents this pattern directly for "a call to an AI service": ```python Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register') @@ -42,8 +42,8 @@ Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register ## Connecting to a review tool -This isn't a shipped integration — "poor integration with review tools (e.g., build filtered review queues)" is listed as an unmet need in [User Research & Personas](../research-personas.md), not something Osprey currently connects to out of the box. The closest real extension points are the output-sink hooks: `register_output_sinks` for execution results generally, or `register_label_output_sink` for a custom sink specifically for label mutations (replacing the default `LabelOutputSink`). Either could push into a review queue, but there's no purpose-built connector today. +This isn't a shipped integration. "Poor integration with review tools (e.g., build filtered review queues)" is listed as an unmet need in [User Research & Personas](../research-personas.md), not something Osprey currently connects to out of the box. The closest real extension points are the output-sink hooks: `register_output_sinks` for execution results generally, or `register_label_output_sink` for a custom sink specifically for label mutations (replacing the default `LabelOutputSink`). Either could push into a review queue, but there's no purpose-built connector today. ## Labels service -If you want Osprey to track state across events (e.g., "this user has 3 prior violations"), implement `register_labels_service_or_provider`. `example_plugins/src/services/labels_service.py` is a full reference implementation backed by Postgres — a good starting point if you're building your own. +If you want Osprey to track state across events (e.g., "this user has 3 prior violations"), implement `register_labels_service_or_provider`. `example_plugins/src/services/labels_service.py` is a full reference implementation backed by Postgres, and a good starting point if you're building your own. diff --git a/docs/rules.md b/docs/rules.md index 3f7ca121b..006bbf26b 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -57,14 +57,14 @@ My_Rule_Name_v2 = Rule( ## Why a bespoke language instead of plain Python? -It's a fair question: why introduce a bespoke language at all, instead of writing rules in plain Python? The short answer is that SML's restrictions are load-bearing — they're what make Osprey's rule engine safe to run untrusted, third-party-authored logic in production, and fast enough to do it in real time. +It's a fair question: why introduce a bespoke language at all, instead of writing rules in plain Python? The short answer is that SML's restrictions are load-bearing: they're what make Osprey's rule engine safe to run untrusted, third-party-authored logic in production, and fast enough to do it in real time. -- **The grammar is deliberately small.** The SML parser (`osprey_worker/src/osprey/engine/ast/py_ast.py`) only understands a restricted set of Python AST nodes — assignments, calls, comparisons, boolean/binary operators, literals, and f-strings. There's no `for`/`while`, no `def`/`class`, no arbitrary `import`. Anything outside that set raises a syntax error explaining that "the Osprey rule language is a simplified subset of python." This isn't an accident of an unfinished parser; it rules out unbounded loops and recursion by construction, so a rule can't hang or DoS the worker. +- **The grammar is deliberately small.** The SML parser (`osprey_worker/src/osprey/engine/ast/py_ast.py`) only understands a restricted set of Python AST nodes: assignments, calls, comparisons, boolean/binary operators, literals, and f-strings. There's no `for`/`while`, no `def`/`class`, no arbitrary `import`. Anything outside that set raises a syntax error explaining that "the Osprey rule language is a simplified subset of python." This isn't an accident of an unfinished parser; it rules out unbounded loops and recursion by construction, so a rule can't hang or DoS the worker. - **Every name is assign-once.** The `UniqueStoredNames` validator rejects re-declaring the same name anywhere in the ruleset, and `NoUnusedLocals` rejects declaring a name that's never read. Combined, every rule and feature compiles down to a single, static, acyclic dependency graph (`execution_graph.py`) rather than an imperative script with mutable state. -- **That static graph is what makes the engine fast and introspectable.** Because nothing can have side effects or run in a loop, the executor can safely evaluate independent branches of the graph in parallel using gevent greenlets (`executor.py`), and the UI can render the exact dependency graph for any rule or feature — that's what powers the [Rules Visualizer](user/manage.md#rules-visualizer) and the cross-referencing in the [Rules](user/manage.md#rules-registry) and [Features Registry](user/manage.md#features-registry) pages. Plain Python doesn't give you that for free; you'd have to reconstruct it by statically analyzing arbitrary code, which is a much harder problem. +- **That static graph is what makes the engine fast and introspectable.** Because nothing can have side effects or run in a loop, the executor can safely evaluate independent branches of the graph in parallel using gevent greenlets (`executor.py`); the UI can also render the exact dependency graph for any rule or feature, which powers the [Rules Visualizer](user/manage.md#rules-visualizer) and the cross-referencing in the [Rules](user/manage.md#rules-registry) and [Features Registry](user/manage.md#features-registry) pages. Plain Python doesn't give you that for free; you'd have to reconstruct it by statically analyzing arbitrary code, which is a much harder problem. - **Types are checked before a rule ever runs against live traffic.** `validate_static_types.py` and `validate_call_rvalue.py` catch type errors and misused return values at validation/push time, not as a runtime `AttributeError` in production the first time a rare code path executes. -None of that is free, and it's worth being honest about the cost: **there's no dedicated SML syntax highlighting or language server today.** Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable (if imperfect) highlighting as a stopgap, but proper tooling is a known gap rather than a solved problem — see the [Development Guide](development/README.md) for current editor setup. +None of that is free, and it's worth being honest about the cost. **There's no dedicated SML syntax highlighting or language server today.** Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable (if imperfect) highlighting as a stopgap, but proper tooling is a known gap rather than a solved problem. See the [Development Guide](development/README.md) for current editor setup. ## Rule Structuring From 87110aa6c3391d0c8207f9b1bd2c60702dd2725f Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Mon, 13 Jul 2026 19:05:07 -0600 Subject: [PATCH 12/53] docs: clean up cli-reference for clarity --- docs/development/cli-reference.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/development/cli-reference.md b/docs/development/cli-reference.md index 48fad1c08..d793c1d35 100644 --- a/docs/development/cli-reference.md +++ b/docs/development/cli-reference.md @@ -1,28 +1,28 @@ # CLI Reference -Osprey ships three command-line tools, each installed as a console script by its package's `pyproject.toml`. Run any of them with `uv run --help` from the repo root, or ` --help` inside an environment where the package is installed. +Osprey includes the following command-line tools installed as console scripts; run them with `uv run ` from the repo root, or `` inside an environment where the package is installed. ## osprey-cli -Installed by `osprey_worker` (`osprey.worker.lib.cli:cli`). General-purpose worker administration: pushing rules, opening a debug shell, and applying labels manually. +General-purpose worker administration including pushing rules, opening a debug shell, and applying labels manually. -### push_rules +### osprey-cli push_rules -```bash +```sh osprey-cli push_rules RULES_PATH [--dry-run/--no-dry-run] [--suppress-warnings] ``` Validates the rules at `RULES_PATH` and pushes them. Use `--dry-run` to validate without pushing. Exits non-zero if validation fails. -### compute_and_upload_dependencies_mapping +### osprey-cli compute_and_upload_dependencies_mapping -```bash +```sh osprey-cli compute_and_upload_dependencies_mapping RULES_PATH [--suppress-warnings] ``` Computes the dependency graph for the rules at `RULES_PATH` and uploads it; this is what powers the [Rules Visualizer](../user/manage.md#rules-visualizer). -### shell +### osprey-cli shell ```bash osprey-cli shell [-i / --auto-import / --no-auto-import] @@ -30,7 +30,7 @@ osprey-cli shell [-i / --auto-import / --no-auto-import] Opens an interactive shell (IPython if installed, otherwise a fallback `code.InteractiveConsole`) with `labels`, `access_audit_log`, and `stored_execution_result` storage modules pre-imported, plus `EntityT`, `EntityLabelMutation`, and `LabelStatus`. With `--auto-import` (the default), it also imports every model class it can find under `osprey_lib`. Useful for interactively inspecting stored data. -### apply_label +### osprey-cli apply_label ```bash osprey-cli apply_label ENTITY_TYPE ENTITY_ID LABEL_NAME LABEL_STATUS \ @@ -41,7 +41,7 @@ Manually applies a label to a single entity. Mainly intended for debugging or im This requires a labels provider to be configured for the Osprey instance; it fails with an assertion error otherwise. -### bulk_apply_label +### osprey-cli bulk_apply_label ```bash osprey-cli bulk_apply_label ENTITY_TYPE ENTITY_IDS_FILE_PATH LABEL_NAME LABEL_STATUS \ @@ -54,7 +54,7 @@ Same as `apply_label`, but reads entity IDs (one per line) from `ENTITY_IDS_FILE Installed by `osprey_worker` (`osprey.worker.stress.cli:main`). Runs an end-to-end stress test against a live Osprey worker: produces synthetic events, consumes the resulting execution results, and reports drop rate and latency. Useful for validating dependency bumps, measuring throughput regressions, and gating CI on pipeline health. -### run +### osprey-stress run ```bash osprey-stress run \ @@ -71,7 +71,7 @@ Common flags: - `--bootstrap-servers` (default `localhost:9092`), `--input-topic` (default `osprey.actions_input`), `--output-topic` (default `osprey.execution_results`) - `--threshold-drop-rate` and `--threshold-p95-ms`: if set, the command exits non-zero when the observed drop rate or p95 latency breaches the threshold, so it can gate a CI job -### measure +### osprey-stress measure ```bash osprey-stress measure [--duration SECONDS] [--report {human,json}] @@ -83,7 +83,7 @@ Reserved for open-loop measurement against externally-produced events, once the Installed by `osprey_async_worker` (`osprey.async_worker.cli.main:cli`). **Experimental**: the asyncio-native worker prototype (no gevent, no monkey-patching), for validating whether an asyncio-based executor can replace the gevent one. Not intended for production use yet. -### run +### osprey-async-cli run ```bash osprey-async-cli run --rules-path PATH [--input-file PATH] [--max-concurrent 12] \ @@ -98,7 +98,7 @@ Input source is controlled by `--input-source`: - `file` (default): reads JSONL actions from `--input-file`, or runs with no input if omitted (useful for just validating the worker boots) - `kafka`: consumes from `--kafka-topic` (default `osprey.actions_input`) via `--kafka-bootstrap-servers` (default `localhost:9092`) -### benchmark +### osprey-async-cli benchmark ```bash osprey-async-cli benchmark --rules-path PATH --input-file PATH \ @@ -107,5 +107,3 @@ osprey-async-cli benchmark --rules-path PATH --input-file PATH \ Benchmarks the async executor against the gevent executor (if `gevent` is importable) using the same rules and input data, running `--warmup` iterations first, then `--iterations` timed iterations, and prints a throughput/latency comparison. -> [!NOTE] -> Verify the exact `--help` output for each command against a running dev environment (e.g. via `docker compose` or `uv run --help`) before relying on it. The flags documented here come directly from the current source, but are worth a final sanity check since these are actively evolving tools. From 792cd5a76daa9c9ce0a2693dc3a7b99daaf73405 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:13:33 -0400 Subject: [PATCH 13/53] CHANGELOG: Correct `Experiment` in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b732eb5b..a727bcb5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ For more information about each release including git tags and artifacts, see [R - `GetActionId()` stdlib UDF for retrieving the current action ID from execution context ([#327](https://github.com/roostorg/osprey/pull/327) by [@julietshen](https://github.com/julietshen)) - `ParseInt` UDF for converting numeric strings to integers ([#190](https://github.com/roostorg/osprey/pull/190) by [@bealsbe](https://github.com/bealsbe)) - `StringSlice` UDF for extracting substrings by index range ([#189](https://github.com/roostorg/osprey/pull/189) by [@bealsbe](https://github.com/bealsbe)) -- `InExperiment` UDF for checking experiment membership ([#203](https://github.com/roostorg/osprey/pull/203) by [@bealsbe](https://github.com/bealsbe)) +- `Experiment` and `ExperimentWhen` UDFs for checking experiment membership ([#203](https://github.com/roostorg/osprey/pull/203) by [@bealsbe](https://github.com/bealsbe)) - Per-action health metrics in the executor ([#191](https://github.com/roostorg/osprey/pull/191) by [@cmttt](https://github.com/cmttt)) - Option to suppress cached errors to reduce metric bloat ([#180](https://github.com/roostorg/osprey/pull/180) by [@lithium-powered](https://github.com/lithium-powered)) - Experimental asyncio-native worker with metrics and engine/coordinator improvements ([#341](https://github.com/roostorg/osprey/pull/341) by [@cmttt](https://github.com/cmttt)) From 106e80d2d93a7ed83b501fa33fc0954f85b370a4 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:14:06 -0400 Subject: [PATCH 14/53] docs: Fix some outdated commands and paths --- docs/development/README.md | 4 ++-- docs/development/workflow.md | 2 +- docs/rules.md | 13 +++++++------ docs/user/investigate/query-syntax.md | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/development/README.md b/docs/development/README.md index e5ceda1e9..4098405f4 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -108,7 +108,7 @@ The UI will automatically connect to the backend services running in Docker cont ## Plugins -In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](integrations.md). +In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](integrations.md). ```python @hookimpl_osprey @@ -146,7 +146,7 @@ Implement any subset of these in your plugin's `register_plugins.py`: Rules are written in SML, some examples are provided in `example_rules/` with YAML config, the rules are mounted to the worker processes when the containers start via environment variables. ex: ```bash -OSPREY_RULES=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink +OSPREY_RULES_PATH=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink ``` For more about rules, see [Writing Rules](../rules.md). diff --git a/docs/development/workflow.md b/docs/development/workflow.md index 23ed6baf6..eac351876 100644 --- a/docs/development/workflow.md +++ b/docs/development/workflow.md @@ -29,7 +29,7 @@ uv run ruff check uv run ruff format # Type checking (on specific files/modules) -uv run mypy osprey_worker/src/osprey_worker/lib +uv run mypy osprey_worker/src/osprey/worker/lib # Or you can type check every module (this will happen in CI) uv run mypy . diff --git a/docs/rules.md b/docs/rules.md index 006bbf26b..1cf02e77f 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -140,7 +140,8 @@ EmbedLink: Optional[str] = JsonData( required=False, ) -ReplyId: Entity[str] = JsonData( +ReplyId: Entity[str] = EntityJson( + type='PostId', path='$.replyId', required=False, ) @@ -254,7 +255,7 @@ User Defined Functions (UDFs) are plugins written in Python that enable users of as a plugin. They extend the `UDFBase` abstract base class with a set of arguments and an output. These will be executed whenever called in SML. ```python -# example_plugins/text_contains.py +# example_plugins/src/udfs/text_contains.py class TextContainsArguments(ArgumentsBase): text: str phrase: str @@ -268,7 +269,7 @@ class TextContains(UDFBase[TextContainsArguments, bool]): regex = re.compile(pattern, flags) return bool(regex.search(arguments.text)) -# example_plugins/register_plugins.py +# example_plugins/src/register_plugins.py @hookimpl_osprey def register_udfs(): return [TextContains] @@ -277,7 +278,7 @@ def register_udfs(): Usage in SML: ```python -# example_rules/post_contains_hello.sml +# example_rules/rules/post_contains_hello.sml ContainsHello = Rule( when_all=[ EventType == 'create_post', @@ -293,7 +294,7 @@ Plugins may also define external effects, which are useful for performing functi These UDFs have an output that extends `EffectBase`, and can be called as a result of a `WhenRules`. ```python -# example_plugins/src/ban_user.py +# example_plugins/src/udfs/ban_user.py class BanUser(UDFBase[BanUserArguments, BanUserEffect]): category = UdfCategories.ENGINE @@ -303,7 +304,7 @@ class BanUser(UDFBase[BanUserArguments, BanUserEffect]): comment=arguments.comment, ) -# example_rules/post_contains_hello.sml +# example_rules/rules/post_contains_hello.sml WhenRules( rules_any=[ContainsHello], then=[BanUser(entity=UserId, comment='User said "hello"')], diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index 9aca6cd2e..781b94a0d 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -10,7 +10,7 @@ Osprey uses SML (“Some Madeup Language,” a subset of Python with additional ```py UserId: Entity[int] = EntityJson(type='User', path='$.user.id', coerce_type=True) -UserEmail: str = JsonData(type='Email', path='$.user.email', required=False) +UserEmail: str = JsonData(path='$.user.email', required=False) ``` Both `UserId` and `UserEmail` above are features. From 189a289634acc50835f0fd5d835ddcb72c3f1b65 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:25:21 -0400 Subject: [PATCH 15/53] docs: link coordinator README via GitHub URL example_docker_compose/ lives outside the mdBook src root, so relative links to it 404 on the deployed site. Link to the GitHub directory instead, which renders the README. --- docs/data-flow.md | 4 ++-- docs/development/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data-flow.md b/docs/data-flow.md index 886a9bcb4..dacfa6291 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -26,7 +26,7 @@ At its core, Osprey is a pipeline: events come in, get evaluated against your SM Druid + UI API → Osprey UI ``` -For the worker's own rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md), which has a more detailed component diagram and a working example. +For the worker's own rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. ## Getting data in @@ -55,7 +55,7 @@ An alternate input source, selected via `InputStreamSource.PUBSUB` (`osprey_work ### 3. The Coordinator's synchronous gRPC API -If you run Osprey with the optional Rust coordinator (`osprey_coordinator/`), external services can submit a single action directly and get an immediate response, without going through Kafka at all, which is useful when a caller needs a synchronous verdict rather than firing into a queue. This is the Sync Action API on port `19951` (bidirectional streaming for workers is a separate port, `19950`). See the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md) for a working `grpcurl` example and full setup. +If you run Osprey with the optional Rust coordinator (`osprey_coordinator/`), external services can submit a single action directly and get an immediate response, without going through Kafka at all, which is useful when a caller needs a synchronous verdict rather than firing into a queue. This is the Sync Action API on port `19951` (bidirectional streaming for workers is a separate port, `19950`). See the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for a working `grpcurl` example and full setup. ### Running against your own platform, without forking diff --git a/docs/development/README.md b/docs/development/README.md index 4098405f4..1e5e82ee0 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -88,7 +88,7 @@ This starts up many services, including: - **Postgres**: A database that the Worker, UI API, and Druid use for various reasons, such as the Postgres-backed Labels Service (in the example plugins) - **Druid**: A database that consumes Osprey Worker outputs to power the UI API for real-time querying -Alternatively, you can start Osprey with `osprey-coordinator`, refer to the [Coordinator README](../example_docker_compose/run_osprey_with_coordinator/README.md) for more information +Alternatively, you can start Osprey with `osprey-coordinator`, refer to the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for more information ### 6. (Optional) Open ports for the UI/UI API From b3341afa61d99b95107ffc1a32ce7019a7c1ed90 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:29:15 -0400 Subject: [PATCH 16/53] docs: fix apply_label anchor in operate.md mdBook slugs the heading as #osprey-cli-apply_label, so the bare #apply_label anchor silently landed at the top of the page. --- docs/user/operate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/operate.md b/docs/user/operate.md index 6c1751058..9dc35850c 100644 --- a/docs/user/operate.md +++ b/docs/user/operate.md @@ -20,7 +20,7 @@ There are two ways to start a bulk action: Before submitting, Osprey shows a count of how many unique entities will be labeled. Review this number carefully to understand the impact of your bulk action. -> For scripted or bulk labeling from the command line (e.g., importing label lists from external sources), see `apply_label` and `bulk_apply_label` in the [CLI Reference](../development/cli-reference.md#apply_label). +> For scripted or bulk labeling from the command line (e.g., importing label lists from external sources), see `apply_label` and `bulk_apply_label` in the [CLI Reference](../development/cli-reference.md#osprey-cli-apply_label). Each job requires: - The entity type and label to apply From dfc92d524979040c2d5cc82be86c3f452c3901c2 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:29:36 -0400 Subject: [PATCH 17/53] docs: align sidebar titles with page headings "Tools Overview" vs "Development Tools" and "User Research Overview" vs "User Research & Personas" made the sidebar and pages disagree about what a page is called. --- docs/SUMMARY.md | 2 +- docs/research-personas.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 6708469ff..a33a2b764 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -16,7 +16,7 @@ - [Development Guide](development/README.md) - [Workflow](development/workflow.md) - - [Tools Overview](development/tools.md) + - [Development Tools](development/tools.md) - [CLI Reference](development/cli-reference.md) - [Integrations & Plugins](development/integrations.md) - [Troubleshooting](development/troubleshooting.md) diff --git a/docs/research-personas.md b/docs/research-personas.md index 4509130c9..3c2495bb9 100644 --- a/docs/research-personas.md +++ b/docs/research-personas.md @@ -1,4 +1,4 @@ -# User Research Overview +# User Research & Personas Osprey is designed for users who need to investigate and take automated action on events in real-time. By interviewing prospective Osprey users, ROOST has drafted the following user personas for developers to better understand who might be interested in using this tool. We hope this research helps the open source community prioritize features to build moving forward! From 1362648d7895b37010b3cb5d3d355b48ef04d45c Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:35:59 -0400 Subject: [PATCH 18/53] docs: fix pre-commit autoupdate command name The docs said "autoupgrade", which isn't a pre-commit subcommand. --- docs/development/tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/tools.md b/docs/development/tools.md index 037eb5bda..127bcc590 100644 --- a/docs/development/tools.md +++ b/docs/development/tools.md @@ -73,7 +73,7 @@ uv run pre-commit run --all-files uv run pre-commit run ruff # Update hook versions -uv run pre-commit autoupgrade +uv run pre-commit autoupdate # Bypass hooks (emergency only) git commit --no-verify From ba490d8548019530eba7dc9932f49b2ff03bf574 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:36:11 -0400 Subject: [PATCH 19/53] docs: describe ruff and mypy config in prose The per-tool "Key rules enabled:"/"Key features:" bullet lists read like generated filler; one sentence each carries the same facts. Claims verified against pyproject.toml. --- docs/development/tools.md | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/docs/development/tools.md b/docs/development/tools.md index 127bcc590..55313a27b 100644 --- a/docs/development/tools.md +++ b/docs/development/tools.md @@ -4,14 +4,7 @@ Familiarize yourself with these development tools to better understand how to wo ## Ruff -Linting and formatting. Replaces Black, isort, Flake8, and other tools. Configuration in `pyproject.toml` under `[tool.ruff]`. - -Key rules enabled: - -- `E`: pycodestyle errors -- `F`: pyflakes -- `I`: isort (import sorting) -- `B006`: flake8-bugbear (mutable default arguments) +Linting and formatting. Replaces Black, isort, Flake8, and other tools. Configuration lives in `pyproject.toml` under `[tool.ruff]`; the enabled rules are pycodestyle errors (`E`), pyflakes (`F`), import sorting (`I`), and flake8-bugbear's mutable-default-arguments check (`B006`). Commands: @@ -31,14 +24,7 @@ uv run ruff check path/to/file.py ## MyPy -Static type checking for Python. Configuration in `pyproject.toml` under `[tool.mypy]`. - -Key features: - -- Pydantic plugin support -- SQLAlchemy plugin support -- Relaxed strict mode (matching legacy codebase) -- Ignores protobuf generated files +Static type checking for Python. Configuration in `pyproject.toml` under `[tool.mypy]` loads the Pydantic and SQLAlchemy plugins, relaxes strict mode to match the older parts of the codebase, and skips generated protobuf files. Commands: From 434e107cedf389662f0068508bf00941a694c0d6 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:36:26 -0400 Subject: [PATCH 20/53] docs: Americanize appearance.md, drop emoji column This was the only British-spelled page in the book, and the icon column was decorative rather than informative. --- docs/user/appearance.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/user/appearance.md b/docs/user/appearance.md index 710f97694..7adef2c04 100644 --- a/docs/user/appearance.md +++ b/docs/user/appearance.md @@ -1,19 +1,19 @@ # Appearance -Osprey supports light and dark themes. By default, the interface follows your OS or browser colour-scheme preference. You can override this at any time using the theme toggle in the navigation bar. +Osprey supports light and dark themes. By default, the interface follows your OS or browser color scheme preference. You can override this at any time using the theme toggle in the navigation bar. ## Changing the theme The theme toggle is in the top-right area of the navigation bar. It offers three options: -| Option | Icon | Behaviour | -|--------|------|-----------| -| **Light** | ☀ Sun | Always use the light theme | -| **System** | 🖥 Desktop | Follow the OS/browser preference *(default)* | -| **Dark** | ☾ Moon | Always use the dark theme | +| Option | Behavior | +|--------|----------| +| **Light** | Always use the light theme | +| **System** | Follow the OS/browser preference *(default)* | +| **Dark** | Always use the dark theme | Your choice is saved in the browser and persists across sessions. Clearing browser storage resets it to **System**. ## System preference -When set to **System**, Osprey listens for OS colour-scheme changes in real time. Switching your OS between light and dark mode while Osprey is open takes effect immediately, no page reload required. \ No newline at end of file +When set to **System**, Osprey listens for OS color scheme changes in real time. Switching your OS between light and dark mode while Osprey is open takes effect immediately, no page reload required. \ No newline at end of file From abe15c3f4f61daadad56857450cec5f4805cf3ac Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:36:39 -0400 Subject: [PATCH 21/53] docs: drop cheerleading exclamation in research-personas --- docs/research-personas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/research-personas.md b/docs/research-personas.md index 3c2495bb9..5a1daee91 100644 --- a/docs/research-personas.md +++ b/docs/research-personas.md @@ -1,6 +1,6 @@ # User Research & Personas -Osprey is designed for users who need to investigate and take automated action on events in real-time. By interviewing prospective Osprey users, ROOST has drafted the following user personas for developers to better understand who might be interested in using this tool. We hope this research helps the open source community prioritize features to build moving forward! +Osprey is designed for users who need to investigate and take automated action on events in real-time. By interviewing prospective Osprey users, ROOST has drafted the following user personas for developers to better understand who might be interested in using this tool. We hope this research helps the open source community prioritize features to build moving forward. ## T&S Engineers From 7405dee1330373618427db149d6bbf36b26e1694 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:36:51 -0400 Subject: [PATCH 22/53] docs: name the Yeet buttons in Investigate "Yeet" is the literal button label in the UI (Chart.tsx, TopNFooter.tsx), so quote it as such instead of leaving it looking like slang, and document the Top N table's Yeet Table button, which was missing from the list. --- docs/user/investigate/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user/investigate/README.md b/docs/user/investigate/README.md index 9b25f0362..00cbd1a56 100644 --- a/docs/user/investigate/README.md +++ b/docs/user/investigate/README.md @@ -38,7 +38,7 @@ The center panels show two types of visualizations: ![Time Series Hover](../../images/hover-time-series.png) -You can add additional timeseries charts to compare different time granularities side-by-side. Charts you no longer need can be removed ("yeeted"). +You can add additional timeseries charts to compare different time granularities side-by-side. Charts you no longer need can be removed with the **Yeet** button. ![Multiple Time Series](../../images/multiple-time-series.png) @@ -47,6 +47,7 @@ You can add additional timeseries charts to compare different time granularities - Adjust the number of results shown (precision) - Enable **Period over Period (PoP)** to compare current results against a past time window and see the delta - Export the table as a CSV +- Remove the table with the **Yeet Table** button ![Top N Charts](../../images/top-n-charts.png) From 46f1de9f5e8e36bbb57dd212d0542767e20f0e4d Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 12:42:36 -0400 Subject: [PATCH 23/53] docs: shrink workflow.md to Osprey-specific facts The page was mostly a generic git tutorial with invented examples, and its pre-commit hook list didn't match .pre-commit-config.yaml (no trailing-whitespace, end-of-file, or YAML hooks exist). Keep branch naming, what the hooks actually run, the manual check commands, and two real commit messages from history. --- docs/development/workflow.md | 87 +++++++----------------------------- 1 file changed, 16 insertions(+), 71 deletions(-) diff --git a/docs/development/workflow.md b/docs/development/workflow.md index eac351876..aec5f0148 100644 --- a/docs/development/workflow.md +++ b/docs/development/workflow.md @@ -1,92 +1,37 @@ # Development Workflow -## Branch Management +Branch from `main` and name your branch `github_username/short-description` (e.g. `caidanw/fix-database-timeout`). -- **Branch naming convention**: Use `github_username/description` format (e.g., `caidanw/feature-auth`, `caidanw/fix-database-timeout`) -- **Base branch**: Always branch from `main` -- **Create new branch**: `git checkout -b username/feature-name` +## What runs on every commit -## Code Quality Standards +Pre-commit hooks run automatically when you commit; the config is `.pre-commit-config.yaml` at the repo root. The hooks check for filename case conflicts, leftover merge-conflict markers, and forgotten Python debugger calls; validate JSON and TOML; normalize line endings to LF; lint and format Python with Ruff; and type-check with mypy. Changes under `osprey_ui/` also go through Prettier and ESLint. -### Automated Checks +Hooks stop at the first failure, and several fix files in place — if a hook modifies a file, re-stage it and commit again. -Every commit automatically runs: +## Checking your work before you push -1. **Trailing whitespace removal** -2. **End-of-file fixing** -3. **YAML/JSON/TOML validation** -4. **Ruff linting and formatting** +CI runs the same hooks against the whole repo, so you can catch failures ahead of time with: -### Manual Checks +```bash +uv run pre-commit run --all-files +``` -Before pushing, run: +Or run the individual tools directly: ```bash -# Comprehensive linting check +# Lint and format uv run ruff check - -# Format all code uv run ruff format -# Type checking (on specific files/modules) +# Type check a specific module, or everything with `uv run mypy .` uv run mypy osprey_worker/src/osprey/worker/lib -# Or you can type check every module (this will happen in CI) -uv run mypy . - -# Run all pre-commit hooks -uv run pre-commit run --all-files ``` -## Commit Standards +## Commit messages -Follow [Conventional Commits](https://www.conventionalcommits.org/) format: +Follow the [Conventional Commits](https://www.conventionalcommits.org/) format. Two real examples from this repo's history: ``` -feat: add user authentication system -fix: resolve database connection timeout -docs: update API documentation -refactor: simplify rule evaluation logic +fix(ui): show the event-stream timezone once, not twice +build(deps): remove unused Discord-era Python dependencies ``` - -**Examples:** - -- `feat:` - New features -- `fix:` - Bug fixes -- `docs:` - Documentation changes -- `refactor:` - Code refactoring -- `test:` - Adding or updating tests -- `chore:` - Maintenance tasks - -## Making Changes - -1. **Create a new branch:** - - ```bash - git checkout -b username/feature-name - ``` - -2. **Make your changes** - -3. **Run quality checks:** - - ```bash - uv run ruff check --fix - uv run ruff format - ``` - -4. **Test your changes** (if tests exist) - -5. **Commit your changes:** - - ```bash - git add . - git commit -m "feat: descriptive commit message" - ``` - - Pre-commit hooks will run automatically and may fix formatting issues. - -6. **Push your branch:** - - ```bash - git push origin username/feature-name - ``` From be79d8f5b9846ad85278c78240f5e7c3c6a75b28 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 13:01:26 -0400 Subject: [PATCH 24/53] demo: advertise the rule that actually exists The ready banner listed LazyPostRule, QuickPostRule, and FoxPostRule, none of which are in example_rules/, and suggested a query that matches nothing. ContainsHello is the only demo rule; describe it accurately (ban + meow label) and fix the query hint. --- demo.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/demo.sh b/demo.sh index b00e45656..d833bcaf3 100755 --- a/demo.sh +++ b/demo.sh @@ -298,17 +298,14 @@ echo -e " • Osprey UI: ${GREEN}${UI_URL}${NC}" echo -e " • Druid Console: ${GREEN}http://localhost:8888${NC}" echo -e " • Osprey API: ${GREEN}http://localhost:5004${NC}" echo "" -echo -e "${BLUE}Demo Rules Active:${NC}" -echo -e " • ContainsHello - Bans users who say 'hello'" -echo -e " • LazyPostRule - Labels posts with 'lazy' as low_effort" -echo -e " • QuickPostRule - Labels posts with 'quick' as potential_bot" -echo -e " • FoxPostRule - Bans users who say 'fox' (spam pattern)" +echo -e "${BLUE}Demo Rule Active:${NC}" +echo -e " • ContainsHello - Bans users who post the word 'hello' and labels them 'meow'" echo "" echo -e "${BLUE}What to Demo:${NC}" echo -e " 1. Event Stream - See processed events with rule matches" echo -e " 2. TopN Panel - See users grouped by labels/bans" echo -e " 3. Timeseries - See event volume over time" -echo -e " 4. Query Filter - Try: LazyPostRule == True" +echo -e " 4. Query Filter - Try: ContainsHello == True" echo -e " 5. Rules Viz - View the rule dependency graph" echo "" echo -e "${YELLOW}Opening Osprey UI in browser...${NC}" From 0f8bac53fe7ac44b6bb7a14f013ba1c94c876999 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 13:02:17 -0400 Subject: [PATCH 25/53] docs: add Getting Started quickstart One-command demo path for the 1.1 analyst audience: prerequisites, what demo.sh actually does (including that it wipes prior demo volumes), what to try against the real ContainsHello ruleset, and how to stop it. Front of the book, before the guides. --- docs/SUMMARY.md | 1 + docs/getting-started.md | 63 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 docs/getting-started.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index a33a2b764..ced0eb938 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,6 +1,7 @@ # Summary [Welcome](README.md) +[Getting Started](getting-started.md) --- diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 000000000..8eb13b844 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,63 @@ +# Getting Started + +The fastest way to see Osprey working is the demo script: one command brings up the full stack with sample data and opens the UI on a pre-filled query. You don't need to write any code or configuration, and everything cleans up with a single command afterward. + +## Prerequisites + +- **Docker with the Compose v2 plugin.** The script checks for both, and for a running Docker daemon, before doing anything. +- **Free ports.** The stack needs a number of ports; the script checks them all up front and names any that are taken. The ones you'll actually visit are `5002` (Osprey UI) and `8888` (Druid console); the Osprey worker and UI API use `5001` and `5004`. +- **A few minutes on first run** while Docker pulls and builds images. Later runs are much faster. + +## Run the demo + +**1. Run the script.** From a clone of the repo: + +```sh +./demo.sh +``` + +Or without cloning anything first — this clones the repo into `./osprey-demo` for you: + +```sh +curl -sSL https://raw.githubusercontent.com/roostorg/osprey/main/demo.sh | bash +``` + +The script starts the whole stack — Kafka, Druid, PostgreSQL, MinIO, and Osprey's own worker, API, and UI, more than a dozen containers in all — plus a test-data producer that sends one synthetic post event per second. If it finds services from a previous run, it asks before stopping them; volumes from earlier demo runs are removed either way, so each demo starts from clean state. + +**2. Wait for the "Demo Ready!" banner.** The script waits for every service to report healthy, then for the first events to flow through, so this takes a couple of minutes. When it's done, it opens the UI in your browser (or prints the URL to open yourself), pre-filled with a query for the last day of events. + +## What to try + +The demo ruleset has exactly one rule, `ContainsHello`: any post containing the word "hello" gets its author banned and labeled `meow`. The producer builds five-word posts from a small word pool, so roughly a third of the generated posts trigger it. + +- **Watch the event stream.** The right panel shows events as they're processed. Click one that matched `ContainsHello` and look at its extracted features and effects. +- **Filter with a query.** Enter `ContainsHello == True` in the query bar to see only the posts that fired the rule; see [Query Syntax](user/investigate/query-syntax.md) for what else you can express. +- **Group with Top N.** The pre-filled query groups by `UserId`, showing which synthetic users have been banned the most. +- **Open the Rules Visualizer** from the navigation bar to see the dependency graph between the demo's features and rule. + +## Stopping the demo + +From the repo directory (or `osprey-demo/` if the script cloned for you): + +```sh +docker compose --profile test_data down -v +``` + +This stops every container and deletes the volumes, including all generated demo data. + +## If something goes wrong + +The script is a convenience wrapper; you can run the same stack directly and watch the logs: + +```sh +docker compose --profile test_data up -d +docker compose logs --follow osprey-worker +``` + +Then open once things settle. [Troubleshooting](development/troubleshooting.md) covers the common failure modes. + +## Where to go next + +- [User Guide](user/README.md) — the investigation workflow: queries, charts, labels, and bulk actions. +- [Writing Rules](rules.md) — how the SML rules that powered the demo work, and how to write your own. +- [Development Guide](development/README.md) — run Osprey against your own events and set up a development environment. From 55c04823bf284e112d19b769d75b55dbf5b359de Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 13:02:31 -0400 Subject: [PATCH 26/53] README: add Try it section, link the docs site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README never mentioned demo.sh or the published documentation — the two best entry points for someone evaluating Osprey. Also swap marketing adjectives for plain description. --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 71b56eb7d..43d2d41d2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Osprey -**Automate the obvious and investigate the ambiguous.** High-performance safety rules engine for real-time event processing at scale. +**Automate the obvious and investigate the ambiguous.** Osprey is a safety rules engine for real-time event processing at scale. - take action based on user behavior - combine actions with human written rules @@ -17,6 +17,22 @@ Osprey is a library for processing actions through human-written rules and outpu Osprey is built for engineers and Trust & Safety teams who want to explore, test, and integrate its core capabilities into their platform for incident response and Trust & Safety investigation. [Read more about user research and personas](docs/research-personas.md). +## Try it + +If you have Docker with Compose v2, one command brings up the full stack with sample data and opens the UI on a pre-filled query: + +```sh +./demo.sh +``` + +Or, without cloning the repo first: + +```sh +curl -sSL https://raw.githubusercontent.com/roostorg/osprey/main/demo.sh | bash +``` + +The [Getting Started guide](https://roostorg.github.io/osprey/latest/getting-started.html) explains what the demo starts, what to try in the UI, and how to shut it all down. The rest of the documentation lives at [roostorg.github.io/osprey/latest](https://roostorg.github.io/osprey/latest/). + ## Adopters Osprey is used by: @@ -28,7 +44,7 @@ Using Osprey and want to add your project/organization to this list? [Open a pul ## Development -- See the [development guide](./docs/development/) for comprehensive development setup and workflow documentation +- See the [development guide](./docs/development/) for development setup and workflow documentation - All code changes should pass linting (Ruff) and type checking (MyPy) - Pre-commit hooks automatically run on each commit to maintain code quality @@ -55,4 +71,4 @@ Your experimentation feedback will directly shape future priorities and help us ## Recognition -Discord uses Osprey to quickly detect and remove new types of harm that put users at risk. Rather than leaving other platforms to build similar tools from scratch, ROOST and Discord have open-sourced this powerful rule engine in collaboration with [internet.dev](https://internet.dev/) to make it available for anyone who needs it. +Discord uses Osprey to quickly detect and remove new types of harm that put users at risk. Rather than leaving other platforms to build similar tools from scratch, ROOST and Discord have open-sourced Osprey in collaboration with [internet.dev](https://internet.dev/) to make it available for anyone who needs it. From d41e50d3210e4361fd9a7d3028042ae3035f4470 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 13:02:55 -0400 Subject: [PATCH 27/53] docs: ship one integrations page, in the Development Guide PR #409 and this branch both added an integrations page closing #383. Keep #409's fuller walkthrough as the base at development/integrations.md, fold in the branch page's unique content (hash-lookup and review-tool extension points, UdfCategories tip, data-flow cross-links, extra hooks), and redirect the old top-level URL. Also set create-missing = false so bad SUMMARY paths fail the build instead of silently creating stub pages. --- docs/SUMMARY.md | 1 - docs/book.toml | 4 + docs/development/integrations.md | 317 +++++++++++++++++++++++++++++-- docs/integrations.md | 312 ------------------------------ 4 files changed, 303 insertions(+), 331 deletions(-) delete mode 100644 docs/integrations.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index ced0eb938..1604c694e 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -29,7 +29,6 @@ # Concepts & More - [Writing Rules](rules.md) -- [Integrations & Plugins](integrations.md) - [Data Flow: Getting Data In and Out](data-flow.md) - [User Research & Personas](research-personas.md) diff --git a/docs/book.toml b/docs/book.toml index 45aff97d1..0ceb5d60f 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -6,6 +6,7 @@ src = "." [build] build-dir = "./book" +create-missing = false [output.html] default-theme = "roost" @@ -17,3 +18,6 @@ site-url = "/osprey/" [output.html.print] enable = false + +[output.html.redirect] +"/integrations.html" = "development/integrations.html" diff --git a/docs/development/integrations.md b/docs/development/integrations.md index 58cef0023..638de6b76 100644 --- a/docs/development/integrations.md +++ b/docs/development/integrations.md @@ -1,24 +1,99 @@ # Integrations & Plugins -Osprey is extended through a [pluggy](https://pluggy.readthedocs.io/)-based plugin system. A plugin package implements any subset of the hooks in the [Available hooks](README.md#available-hooks) table and registers them via `@hookimpl_osprey`; see `example_plugins/src/register_plugins.py` and `example_plugins/src/register_async_plugins.py` for working examples. This page walks through the concrete integration points adopters ask about most. +Osprey is designed to be extended without modifying the core codebase; you can wire up your own logic such as detection functions, output destinations, entity state storage, and ML models through plugin packages that Osprey discovers at startup. A plugin package implements any subset of the hooks in the [Available hooks](README.md#available-hooks) table; this page walks through the integration points adopters ask about most. -## User-defined functions (UDFs) +See the [`example_plugins/` directory](https://github.com/roostorg/osprey/tree/main/example_plugins) for a working reference package. -UDFs are how you extend the functions available to rule authors. See [Writing Rules § User Defined Functions](../rules.md#user-defined-functions-udfs) for the language-level reference, including working examples (`TextContains`, `BanUser`) in `example_plugins/src/udfs/`. +## How plugins are loaded -To register your own, implement a `UDFBase` subclass and return it from the `register_udfs` hook. Give it a category from `UdfCategories` (`osprey_worker/src/osprey/engine/stdlib/udfs/categories.py`, e.g. `STRING`, `HASH`, `ENTITY`, `HTTP`) so it's grouped sensibly in the [UDF Registry](../user/manage.md#udf-registry). +Osprey uses [pluggy](https://pluggy.readthedocs.io/) for plugin discovery. Your plugin package declares one or both of these entry-point groups in its `pyproject.toml`: -## Input and output sinks +- `osprey_plugin`: loaded by the standard gevent worker +- `osprey_async_plugin`: loaded by the experimental asyncio worker -See [Data Flow § Getting data in](../data-flow.md#getting-data-in) and [§ Getting data out](../data-flow.md#getting-data-out) for the conceptual picture. The relevant hooks: +For example: -- `register_input_stream` / `register_action_proto_deserializer`: bring in events from a source other than Kafka/PubSub/the coordinator. -- `register_output_sinks`: send execution results somewhere other than stdout/Kafka/the configured result store. -- `register_execution_result_store`: persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options. +```toml +[project.entry-points.osprey_plugin] +register_plugins = "register_plugins" + +[project.entry-points.osprey_async_plugin] +register_async_plugins = "register_async_plugins" +``` + +Each entry point resolves to a module that contains hook functions decorated with `@hookimpl_osprey` or `@hookimpl_osprey_async`. Osprey calls each hook at startup to collect your registrations; see [`example_plugins/src/register_plugins.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/register_plugins.py) and [`register_async_plugins.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/register_async_plugins.py) for more. + +## Writing UDFs + +A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic such as text matching, DNS lookups, hash comparisons, or ML inference and make it available under a named function in the rules language. See [Writing Rules § User Defined Functions](../rules.md#user-defined-functions-udfs) for the language-level view. + +### Anatomy of a UDF + +UDFs require: + +1. An **arguments** class (subclass of `ArgumentsBase`) that declares the parameters the UDF accepts with types +2. A **UDF class** (subclass of `UDFBase[Arguments, ReturnType]`) with an `execute` method that contains the logic + +For example: + +```python +# example_plugins/src/udfs/text_contains.py +import re + +from osprey.engine.executor.execution_context import ExecutionContext +from osprey.engine.udf.arguments import ArgumentsBase +from osprey.engine.udf.base import UDFBase + + +class TextContainsArguments(ArgumentsBase): + text: str + phrase: str + case_sensitive = False + + +class TextContains(UDFBase[TextContainsArguments, bool]): + def execute(self, execution_context: ExecutionContext, arguments: TextContainsArguments) -> bool: + escaped = re.escape(arguments.phrase) + + pattern = rf'\b{escaped}\b' + + flags = 0 if arguments.case_sensitive else re.IGNORECASE + regex = re.compile(pattern, flags) + + return bool(regex.search(arguments.text)) +``` + +Once registered, `TextContains` is callable from rules as: + +```python +TextContains(text=SomeFeature, phrase="spam") +``` + +### UDFs with side effects + +UDFs can also produce **effects**: structured outputs that downstream systems act on, such as banning a user or flagging content. Effects are expressed using `EffectBase` as the return type. See [`example_plugins/src/udfs/ban_user.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/udfs/ban_user.py) for an example. + +### Async UDFs + +UDFs that perform I/O (e.g. network calls or database reads) should subclass `AsyncUDFBase` when used in the async worker; see [`osprey_async_worker/src/osprey/async_worker/stdlib_udfs/async_mx_lookup.py`](https://github.com/roostorg/osprey/blob/main/osprey_async_worker/src/osprey/async_worker/stdlib_udfs/async_mx_lookup.py) for an example. Pure-computation UDFs like `TextContains` can be reused in both workers without modification. + +### Registering UDFs + +Return your UDF classes from the `register_udfs` hook; for example: + +```python +from osprey.worker.adaptor.plugin_manager import hookimpl_osprey + +@hookimpl_osprey +def register_udfs(): + return [TextContains, BanUser] +``` + +Give each UDF a category from `UdfCategories` (`osprey_worker/src/osprey/engine/stdlib/udfs/categories.py`, e.g. `STRING`, `HASH`, `ENTITY`, `HTTP`) so it's grouped sensibly in the [UDF Registry](../user/manage.md#udf-registry). ## Hash-based lookups -There's no dedicated lookup-table or lookup-service feature in Osprey today: no denylist/allowlist primitive, no bulk-import-a-set-of-values mechanism. What exists is the `Hash*` UDF family (`osprey_worker/src/osprey/engine/stdlib/udfs/string_hashes.py`: `HashMd5`, `HashSha1`, `HashSha256`, `HashSha512`), which you compose with SML's `in` operator or `HasLabel` to check membership: +Osprey's standard library includes the `Hash*` UDF family (`HashMd5`, `HashSha1`, `HashSha256`, `HashSha512`, under the `HASH` category and available without registration), which takes a string `input` and returns the hex digest. Compose these with SML's `in` operator or `HasLabel` to check values against known-bad sets without storing raw data: ```python # Check a hashed value against a small inline set @@ -28,22 +103,228 @@ IsKnownBadHash = HashSha256(input=SomeValue) in ['abc123...', 'def456...'] IsKnownBadActor = HasLabel(entity=SomeEntity, label='KnownBad') ``` -If you need to check against a large external list (millions of hashes, updated frequently), that's not a built-in feature: write a custom UDF (see above) that queries your own store. +Inline sets suit small, stable lists. There's no bulk-import or lookup-table primitive, so for a large external list (millions of hashes, updated frequently), write a custom UDF (see above) that queries your own store. + +## Configuring input sinks + +An input sink is where events _enter_ Osprey. Osprey ships with built-in sources (Kafka, Google Pub/Sub, the Osprey Coordinator, and a synthetic generator for local testing) selected via the `InputStreamSource` config value. If none of those fit your platform, you can register a custom input stream as a plugin. For the conceptual picture, see [Data Flow § Getting data in](../data-flow.md#getting-data-in). + +### Built-in sources -## Plugging in your own ML models +The worker picks an input stream based on `InputStreamSource`: -There's no dedicated hook for this either; it's the standard UDF pattern. Implement a `UDFBase` subclass whose `execute()` calls out (HTTP, gRPC, an SDK, whatever your model server expects) and returns a score, boolean, or other typed value, then register it via `register_udfs`. There's no built-in outbound-HTTP UDF to build on (`UdfCategories.HTTP` currently has one UDF, `extract_cookie.py`, which parses cookies rather than making requests). +Source | Config | Use case +-------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ +`KAFKA` | `OSPREY_KAFKA_INPUT_STREAM_TOPIC`, `OSPREY_KAFKA_BOOTSTRAP_SERVERS` | Consume Action events from a Kafka topic +`PUBSUB` | `PUBSUB_OSPREY_PROJECT_ID`, `PUBSUB_OSPREY_RULES_SINK_SUBSCRIPTION` | Consume from Google Pub/Sub +`OSPREY_COORDINATOR` | `OSPREY_COORDINATOR_SERVICE_NAME` | Pull work from the Osprey Coordinator service +`SYNTHETIC` |   | Generates random fake events; useful for local dev without any upstream system +`PLUGIN` |   | Delegates to your registered `register_input_stream` hook -Because model calls are often slow or costly, gate them with `Require(..., require_if=...)` so they only run when relevant. `docs/rules.md` documents this pattern directly for "a call to an AI service": +Set `InputStreamSource.KAFKA` (or whichever fits your existing infrastructure) if you already have events flowing through Kafka or Pub/Sub. Otherwise, implement a custom input stream and set `InputStreamSource.PLUGIN` in your config. If your events arrive as protobuf rather than JSON, there's also a `register_action_proto_deserializer` hook for supplying your own deserializer. + +### Writing a custom input stream + +If your event source isn't Kafka or Pub/Sub (e.g. it's a webhook receiver, a different message queue, or a polling API), subclass `BaseInputStream` and implement `_gen`, a generator that yields one `Action` (wrapped in an `AckingContext`) per event. For example: ```python -Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register') +from collections.abc import Iterator + +from osprey.engine.executor.execution_context import Action +from osprey.worker.sinks.sink.input_stream import BaseInputStream +from osprey.worker.sinks.utils.acking_contexts import BaseAckingContext, NoopAckingContext + + +class MyInputStream(BaseInputStream[BaseAckingContext[Action]]): + def __init__(self, my_client): + super().__init__() + self._client = my_client + + def _gen(self) -> Iterator[BaseAckingContext[Action]]: + while True: + raw_event = self._client.poll() # block until the next event + action = Action( + action_id=int(raw_event['id']), + action_name=raw_event['type'], + data=raw_event['payload'], + timestamp=raw_event['timestamp'], + ) + yield NoopAckingContext(item=action) ``` -## Connecting to a review tool +`_gen` is called once and re-used. It should block and yield indefinitely rather than returning. Use `NoopAckingContext` unless your source needs explicit ack/nack (e.g. a queue with at-least-once delivery), in which case implement a custom `BaseAckingContext` that acks on success. + +Register it from the hook, and set `InputStreamSource.PLUGIN` in your config so the worker picks it up; for example: + +```python +@hookimpl_osprey +def register_input_stream(config): + return MyInputStream(my_client=build_client(config)) +``` + +## Configuring output sinks + +An output sink receives every `ExecutionResult` after rule evaluation and decides what to do with it, e.g. log it, forward it to a queue, call a webhook, or write to a database. For the conceptual picture, see [Data Flow § Getting data out](../data-flow.md#getting-data-out); if what you want is to persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options, the `register_execution_result_store` hook covers that instead. + +### Sync output sink -This isn't a shipped integration. "Poor integration with review tools (e.g., build filtered review queues)" is listed as an unmet need in [User Research & Personas](../research-personas.md), not something Osprey currently connects to out of the box. The closest real extension points are the output-sink hooks: `register_output_sinks` for execution results generally, or `register_label_output_sink` for a custom sink specifically for label mutations (replacing the default `LabelOutputSink`). Either could push into a review queue, but there's no purpose-built connector today. +Subclass `BaseOutputSink` and implement three methods; for example: + +```python +from osprey.worker.sinks.sink.output_sink import BaseOutputSink +from osprey.engine.executor.execution_context import ExecutionResult + + +class MyOutputSink(BaseOutputSink): + def will_do_work(self, result: ExecutionResult) -> bool: + # Return False to skip this result early (e.g. filter by rule hit) + return True + + def push(self, result: ExecutionResult) -> None: + # Do something with the result — send to a queue, call an API, etc. + pass + + def stop(self) -> None: + # Clean up connections, flush buffers + pass +``` + +Register it from the hook; for example: + +```python +@hookimpl_osprey +def register_output_sinks(config): + return [MyOutputSink()] +``` + +### Async output sink + +For the async worker, subclass `AsyncBaseOutputSink` and make `push` and `stop` coroutines. See [`example_plugins/src/async_sinks/example_async_output_sink.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/async_sinks/example_async_output_sink.py); for example: + +```python +from osprey.async_worker.adaptor.interfaces import AsyncBaseOutputSink +import logging + +logger = logging.getLogger(__name__) + +class ExampleAsyncOutputSink(AsyncBaseOutputSink): + def will_do_work(self, result: ExecutionResult) -> bool: + return True + + async def push(self, result: ExecutionResult) -> None: + logger.info( + 'example async output sink: features=%s verdicts=%s', + result.extracted_features_json, + result.verdicts, + ) + + async def stop(self) -> None: + pass +``` + +Register it with `@hookimpl_osprey_async` under the hook name `register_async_output_sinks`. This is a different hook from the sync `register_output_sinks` above, and goes in your `register_async_plugins.py` module (the one wired to the `osprey_async_plugin` entry point); for example: + +```python +from osprey.async_worker.adaptor.plugin_manager import hookimpl_osprey_async + +@hookimpl_osprey_async +def register_async_output_sinks(config): + return [ExampleAsyncOutputSink()] +``` ## Labels service -If you want Osprey to track state across events (e.g., "this user has 3 prior violations"), implement `register_labels_service_or_provider`. `example_plugins/src/services/labels_service.py` is a full reference implementation backed by Postgres, and a good starting point if you're building your own. +Osprey tracks state across events through entity labels: arbitrary tags attached to users, accounts, or other entities (e.g., "this user has 3 prior violations"). Labels are read during rule evaluation and written by rules with label effects. To persist labels across process restarts (and share them between workers), you provide a `LabelsServiceBase` implementation backed by your own storage via the `register_labels_service_or_provider` hook. + +The example implementation in [`example_plugins/src/services/labels_service.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/services/labels_service.py) uses PostgreSQL, e.g.: + +```python +from osprey.worker.lib.storage.labels import LabelsServiceBase + +class PostgresLabelsService(LabelsServiceBase): + def initialize(self) -> None: + # Called once at startup — open connections here + ... + + def read_labels(self, entity) -> EntityLabels: + # Return labels for this entity from your store + ... + + @contextmanager + def read_modify_write_labels_atomically(self, entity): + # Yield the current labels; caller mutates them in place; + # persist the result before the context manager exits + ... +``` + +Register it from the hook: + +```python +@hookimpl_osprey +def register_labels_service_or_provider(config): + return PostgresLabelsService() +``` + +## Connecting to a review tool + +Osprey doesn't currently support direct integration with a review tool ([a known unmet need](../research-personas.md)); however, these extension points can help you integrate: + +- `register_output_sinks` — push execution results into a review queue as they're produced. +- `register_label_output_sink` — a sink specifically for label mutations, replacing the default `LabelOutputSink`. +- A labels service backed by your existing datastore (previous section) — label an entity "flagged" from a rule, and your review queue queries your own store for that label. + +## Plugging in your own ML model + +ML models can be integrated as UDFs. For an in-process model, wrap your model's `predict` call in `execute`. Since a UDF's `__init__` receives `validation_context` and `arguments` from the framework, override it to accept and forward both, then do your model loading after the `super().__init__()` call; for example: + +```python +class Arguments(ArgumentsBase): + text: str + +class MySpamClassifier(UDFBase[Arguments, float]): + def __init__(self, validation_context, arguments): + super().__init__(validation_context, arguments) + self._model = load_model("/path/to/model.pkl") + + def execute(self, execution_context: ExecutionContext, arguments: Arguments) -> float: + return self._model.predict_proba([arguments.text])[0][1] +``` + +The returned score is then available in rules, e.g.: + +```python +MySpamClassifier(text=MessageContent) > 0.85 +``` + +Osprey constructs one UDF instance per call site when the rules are compiled, not per event, so the model isn't reloaded for every action processed. Keep in mind this means per _call site_, not per _class_: if you call the same UDF from multiple rules, each call site gets its own instance, and each one loads its own copy of the model. **For a large model, prefer calling the UDF from a single rule (or share the loaded weights via a module-level cache) rather than invoking it from many places.** + +For a model served remotely, the same pattern applies with `execute()` calling out over HTTP, gRPC, or your model server's SDK (you bring the client code). Because remote model calls are often slow or costly, gate them so they only run when relevant, using [Writing Rules' `Require(..., require_if=...)` pattern](../rules.md): + +```python +Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register') +``` + +## Packaging your plugin + +Your plugin package needs a `pyproject.toml` that declares the entry points; for example: + +```toml +[project] +name = "my-osprey-plugins" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = ["pluggy==1.5.0"] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[project.entry-points.osprey_plugin] +register_plugins = "register_plugins" +``` + +Install it into the same environment as Osprey and it will be discovered automatically on the next startup. + +See also: [Writing Rules](../rules.md) diff --git a/docs/integrations.md b/docs/integrations.md deleted file mode 100644 index 25f85e4d6..000000000 --- a/docs/integrations.md +++ /dev/null @@ -1,312 +0,0 @@ -# Integrations & Plugins - -Osprey is designed to be extended without modifying the core codebase; you can wire up your own logic such as detection functions, output destinations, entity state storage, and ML models through plugin packages that Osprey discovers at startup. - -See the [`example_plugins/` directory](https://github.com/roostorg/osprey/tree/main/example_plugins) for reference. - -## How plugins are loaded - -Osprey uses [pluggy](https://pluggy.readthedocs.io/) for plugin discovery. Your plugin package declares one or both of these entry-point groups in its `pyproject.toml`: - -- `osprey_plugin`: loaded by the standard gevent worker -- `osprey_async_plugin`: loaded by the experimental asyncio worker - -For example: - -```toml -[project.entry-points.osprey_plugin] -register_plugins = "register_plugins" - -[project.entry-points.osprey_async_plugin] -register_async_plugins = "register_async_plugins" -``` - -Each entry point resolves to a module that contains hook functions decorated with `@hookimpl_osprey` or `@hookimpl_osprey_async`. Osprey calls each hook at startup to collect your registrations; see [`example_plugins/src/register_plugins.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/register_plugins.py) and [`register_async_plugins.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/register_async_plugins.py) for more. - -## Writing UDFs - -A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic such as text matching, DNS lookups, hash comparisons, or ML inference and make it available under a named function in the rules language. - -### Anatomy of a UDF - -UDFs require: - -1. An **arguments** class (subclass of `ArgumentsBase`) that declares the parameters the UDF accepts with types -2. A **UDF class** (subclass of `UDFBase[Arguments, ReturnType]`) with an `execute` method that contains the logic - -For example: - -```python -# example_plugins/src/udfs/text_contains.py -import re - -from osprey.engine.executor.execution_context import ExecutionContext -from osprey.engine.udf.arguments import ArgumentsBase -from osprey.engine.udf.base import UDFBase - - -class TextContainsArguments(ArgumentsBase): - text: str - phrase: str - case_sensitive = False - - -class TextContains(UDFBase[TextContainsArguments, bool]): - def execute(self, execution_context: ExecutionContext, arguments: TextContainsArguments) -> bool: - escaped = re.escape(arguments.phrase) - - pattern = rf'\b{escaped}\b' - - flags = 0 if arguments.case_sensitive else re.IGNORECASE - regex = re.compile(pattern, flags) - - return bool(regex.search(arguments.text)) -``` - -Once registered, `TextContains` is callable from rules as: - -```python -TextContains(text=SomeFeature, phrase="spam") -``` - -### UDFs with side effects - -UDFs can also produce **effects**: structured outputs that downstream systems act on, such as banning a user or flagging content. Effects are expressed using `EffectBase` as the return type. See [`example_plugins/src/udfs/ban_user.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/udfs/ban_user.py) for an example. - -### Async UDFs - -UDFs that perform I/O (e.g. network calls or database reads) should subclass `AsyncUDFBase` when used in the async worker; see [`osprey_async_worker/src/osprey/async_worker/stdlib_udfs/async_mx_lookup.py`](https://github.com/roostorg/osprey/blob/main/osprey_async_worker/src/osprey/async_worker/stdlib_udfs/async_mx_lookup.py) for an example. Pure-computation UDFs like `TextContains` can be reused in both workers without modification. - -### Registering UDFs - -Return your UDF classes from the `register_udfs` hook; for example: - -```python -from osprey.worker.adaptor.plugin_manager import hookimpl_osprey - -@hookimpl_osprey -def register_udfs(): - return [TextContains, BanUser] -``` - -## Built-in UDFs: hash lookups - -Osprey's standard library includes hash UDFs (`HashMd5`, `HashSha1`, `HashSha256`, `HashSha512`) under the `HASH` category. They take a string `input` and return the hex digest. Use them in rules to compare hashed values against known-bad hash sets without storing raw data: - -```python -HashSha256(input=Username) == "e3b0c44298fc1c149afbf4c8996fb924..." -``` - -These are available without registration. - -## Configuring input sinks - -An input sink is where events _enter_ Osprey. Osprey ships with built-in sources (Kafka, Google Pub/Sub, the Osprey Coordinator, and a synthetic generator for local testing) selected via the `InputStreamSource` config value. If none of those fit your platform, you can register a custom input stream as a plugin. - -### Built-in sources - -The worker picks an input stream based on `InputStreamSource`: - -Source | Config | Use case --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ -`KAFKA` | `OSPREY_KAFKA_INPUT_STREAM_TOPIC`, `OSPREY_KAFKA_BOOTSTRAP_SERVERS` | Consume Action events from a Kafka topic -`PUBSUB` | `PUBSUB_OSPREY_PROJECT_ID`, `PUBSUB_OSPREY_RULES_SINK_SUBSCRIPTION` | Consume from Google Pub/Sub -`OSPREY_COORDINATOR` | `OSPREY_COORDINATOR_SERVICE_NAME` | Pull work from the Osprey Coordinator service -`SYNTHETIC` |   | Generates random fake events; useful for local dev without any upstream system -`PLUGIN` |   | Delegates to your registered `register_input_stream` hook - -Set `InputStreamSource.KAFKA` (or whichever fits your existing infrastructure) if you already have events flowing through Kafka or Pub/Sub. Otherwise, implement a custom input stream and set `InputStreamSource.PLUGIN` in your config. - -### Writing a custom input stream - -If your event source isn't Kafka or Pub/Sub (e.g. it's a webhook receiver, a different message queue, or a polling API), subclass `BaseInputStream` and implement `_gen`, a generator that yields one `Action` (wrapped in an `AckingContext`) per event. For example: - -```python -from collections.abc import Iterator - -from osprey.engine.executor.execution_context import Action -from osprey.worker.sinks.sink.input_stream import BaseInputStream -from osprey.worker.sinks.utils.acking_contexts import BaseAckingContext, NoopAckingContext - - -class MyInputStream(BaseInputStream[BaseAckingContext[Action]]): - def __init__(self, my_client): - super().__init__() - self._client = my_client - - def _gen(self) -> Iterator[BaseAckingContext[Action]]: - while True: - raw_event = self._client.poll() # block until the next event - action = Action( - action_id=int(raw_event['id']), - action_name=raw_event['type'], - data=raw_event['payload'], - timestamp=raw_event['timestamp'], - ) - yield NoopAckingContext(item=action) -``` - -`_gen` is called once and re-used. It should block and yield indefinitely rather than returning. Use `NoopAckingContext` unless your source needs explicit ack/nack (e.g. a queue with at-least-once delivery), in which case implement a custom `BaseAckingContext` that acks on success. - -Register it from the hook, and set `InputStreamSource.PLUGIN` in your config so the worker picks it up; for example: - -```python -@hookimpl_osprey -def register_input_stream(config): - return MyInputStream(my_client=build_client(config)) -``` - -## Configuring output sinks - -An output sink receives every `ExecutionResult` after rule evaluation and decides what to do with it, i.e. log it, forward it to a queue, call a webhook, or write to a database. - -### Sync output sink - -Subclass `BaseOutputSink` and implement three methods; for example: - -```python -from osprey.worker.sinks.sink.output_sink import BaseOutputSink -from osprey.engine.executor.execution_context import ExecutionResult - - -class MyOutputSink(BaseOutputSink): - def will_do_work(self, result: ExecutionResult) -> bool: - # Return False to skip this result early (e.g. filter by rule hit) - return True - - def push(self, result: ExecutionResult) -> None: - # Do something with the result — send to a queue, call an API, etc. - pass - - def stop(self) -> None: - # Clean up connections, flush buffers - pass -``` - -Register it from the hook; for example: - -```python -@hookimpl_osprey -def register_output_sinks(config): - return [MyOutputSink()] -``` - -### Async output sink - -For the async worker, subclass `AsyncBaseOutputSink` and make `push` and `stop` coroutines. See [`example_plugins/src/async_sinks/example_async_output_sink.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/async_sinks/example_async_output_sink.py); for example: - -```python -from osprey.async_worker.adaptor.interfaces import AsyncBaseOutputSink -import logging - -logger = logging.getLogger(__name__) - -class ExampleAsyncOutputSink(AsyncBaseOutputSink): - def will_do_work(self, result: ExecutionResult) -> bool: - return True - - async def push(self, result: ExecutionResult) -> None: - logger.info( - 'example async output sink: features=%s verdicts=%s', - result.extracted_features_json, - result.verdicts, - ) - - async def stop(self) -> None: - pass -``` - -Register it with `@hookimpl_osprey_async` under the hook name `register_async_output_sinks`. This is a different hook from the sync `register_output_sinks` above, and goes in your `register_async_plugins.py` module (the one wired to the `osprey_async_plugin` entry point); for example: - -```python -from osprey.async_worker.adaptor.plugin_manager import hookimpl_osprey_async - -@hookimpl_osprey_async -def register_async_output_sinks(config): - return [ExampleAsyncOutputSink()] -``` - -## Connecting to a review tool via a labels service - -Osprey tracks state across events through entity labels: arbitrary tags attached to users, accounts, or other entities. Labels are read during rule evaluation and written by rules with label effects. To persist labels across process restarts (and share them between workers), you provide a `LabelsServiceBase` implementation backed by your own storage. - -The example implementation in [`example_plugins/src/services/labels_service.py`](https://github.com/roostorg/osprey/blob/main/example_plugins/src/services/labels_service.py) uses PostgreSQL, e.g.: - -```python -from osprey.worker.lib.storage.labels import LabelsServiceBase - -class PostgresLabelsService(LabelsServiceBase): - def initialize(self) -> None: - # Called once at startup — open connections here - ... - - def read_labels(self, entity) -> EntityLabels: - # Return labels for this entity from your store - ... - - @contextmanager - def read_modify_write_labels_atomically(self, entity): - # Yield the current labels; caller mutates them in place; - # persist the result before the context manager exits - ... -``` - -Register it from the hook: - -```python -@hookimpl_osprey -def register_labels_service_or_provider(config): - return PostgresLabelsService() -``` - -A labels service backed by your existing datastore lets Osprey decisions feed directly into your review tool: label an entity "flagged", and your review queue queries for that label. - -## Plugging in your own ML model - -ML models integrate as UDFs. Wrap your model's `predict` call in `execute`. Since a UDF's `__init__` receives `validation_context` and `arguments` from the framework, override it to accept and forward both, then do your model loading after the `super().__init__()` call; for example: - -```python -class Arguments(ArgumentsBase): - text: str - -class MySpamClassifier(UDFBase[Arguments, float]): - def __init__(self, validation_context, arguments): - super().__init__(validation_context, arguments) - self._model = load_model("/path/to/model.pkl") - - def execute(self, execution_context: ExecutionContext, arguments: Arguments) -> float: - return self._model.predict_proba([arguments.text])[0][1] -``` - -The returned score is then available in rules, e.g.: - -```python -MySpamClassifier(text=MessageContent) > 0.85 -``` - -Osprey constructs one UDF instance per call site when the rules are compiled, not per event, so the model isn't reloaded for every action processed. Keep in mind this means per _call site_, not per _class_: if you call the same UDF from multiple rules, each call site gets its own instance, and each one loads its own copy of the model. **For a large model, prefer calling the UDF from a single rule (or share the loaded weights via a module-level cache) rather than invoking it from many places.** - -## Packaging your plugin - -Your plugin package needs a `pyproject.toml` that declares the entry points; for example: - -```toml -[project] -name = "my-osprey-plugins" -version = "0.1.0" -requires-python = ">=3.11" -dependencies = ["pluggy==1.5.0"] - -[tool.setuptools] -package-dir = {"" = "src"} - -[tool.setuptools.packages.find] -where = ["src"] - -[project.entry-points.osprey_plugin] -register_plugins = "register_plugins" -``` - -Install it into the same environment as Osprey and it will be discovered automatically on the next startup. - -See also: [Writing Rules](rules.md) From 73aece262076c9ceb206e2a808058d3680c63002 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 13:21:28 -0400 Subject: [PATCH 28/53] docs: restructure dev guide landing like Coop's The Development Guide now opens with the Getting Started quickstart (development/README.md), and the detailed setup reference moves to development/local.md; the two openers point at each other. Rename the setup page's "Getting Started" step to "Start the Services" to avoid colliding with the new page, and point rules.md's editor-setup claim at IDE Setup, which is where editor docs actually live. --- AGENTS.md | 4 +- README.md | 2 +- docs/SUMMARY.md | 2 +- docs/development/README.md | 172 +++++++------------------------ docs/development/integrations.md | 4 +- docs/development/local.md | 161 +++++++++++++++++++++++++++++ docs/getting-started.md | 63 ----------- docs/rules.md | 2 +- 8 files changed, 204 insertions(+), 206 deletions(-) create mode 100644 docs/development/local.md delete mode 100644 docs/getting-started.md diff --git a/AGENTS.md b/AGENTS.md index f196d905f..071a91763 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ Top-level modules: - `example_plugins/` — reference plugins (UDFs, output sinks, labels service) using the pluggy-based plugin system. Do not add production code here. - `example_rules/` — sample SML rules and YAML config. -Reference files: `docs/development/README.md` (setup), `example_plugins/src/register_plugins.py` (plugin patterns), `example_plugins/src/services/labels_service.py` (labels service example). +Reference files: `docs/development/local.md` (setup), `example_plugins/src/register_plugins.py` (plugin patterns), `example_plugins/src/services/labels_service.py` (labels service example). ## Design @@ -157,7 +157,7 @@ uv tool run fawltydeps --check-unused --pyenv .venv - Do not disable lint or type rules to silence errors. Fix the underlying issue, or use a narrowly-scoped `# noqa: ` / `# type: ignore[]` with a comment explaining why. - Before adding a new dependency, check it for known CVEs and confirm the license is compatible with `LICENSE.md`. - Do not commit generated protobuf files from an untrusted toolchain; always regenerate via `./gen-protos.sh`. -- Default Docker bindings are `127.0.0.1`; do not change bind addresses without explicit instruction (see `docs/development/README.md` §6). +- Default Docker bindings are `127.0.0.1`; do not change bind addresses without explicit instruction (see `docs/development/local.md` §6). ## Code review diff --git a/README.md b/README.md index 43d2d41d2..c26ae382f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Or, without cloning the repo first: curl -sSL https://raw.githubusercontent.com/roostorg/osprey/main/demo.sh | bash ``` -The [Getting Started guide](https://roostorg.github.io/osprey/latest/getting-started.html) explains what the demo starts, what to try in the UI, and how to shut it all down. The rest of the documentation lives at [roostorg.github.io/osprey/latest](https://roostorg.github.io/osprey/latest/). +The [Getting Started guide](https://roostorg.github.io/osprey/latest/development/) explains what the demo starts, what to try in the UI, and how to shut it all down. The rest of the documentation lives at [roostorg.github.io/osprey/latest](https://roostorg.github.io/osprey/latest/). ## Adopters diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 1604c694e..1c6d6d7b9 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,7 +1,6 @@ # Summary [Welcome](README.md) -[Getting Started](getting-started.md) --- @@ -16,6 +15,7 @@ --- - [Development Guide](development/README.md) + - [Local Development](development/local.md) - [Workflow](development/workflow.md) - [Development Tools](development/tools.md) - [CLI Reference](development/cli-reference.md) diff --git a/docs/development/README.md b/docs/development/README.md index 1e5e82ee0..fad52cb12 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -1,161 +1,61 @@ -# Development Guide +# Getting Started -Set up a development environment for Osprey. +Quickly see Osprey working with sample data and a real rule using the demo script. It needs no code or configuration, and everything cleans up with a single command afterward. For a full development environment—running services individually, making changes, and debugging—see [Local Development](local.md). -## Prerequisites +To run the demo: -- **Operating System**: macOS, Linux, or Windows (with WSL recommended) -- **[Python](https://www.python.org/) 3.11 or higher** (check with `python --version`) -- **[Git](https://git-scm.com/)** for version control -- **[uv](https://docs.astral.sh/uv/)** for Python package management -- **[Node.js](https://nodejs.org/en/download/) 22+** for the UI (Corepack ships with Node and auto-resolves pnpm from `osprey_ui/package.json`'s `packageManager` field; no separate pnpm install needed) +1. **Ensure you have prerequisites** installed: Docker with the Compose v2 plugin, and the Docker daemon running. The script checks these before doing anything. -## Project Setup + ```sh + docker --version && docker compose version + ``` -### 1. Clone the Repository + The stack also needs a number of free ports; the script checks them up front and names any that are taken. The ones you'll actually visit are `5002` (Osprey UI) and `8888` (Druid console); the worker and UI API use `5001` and `5004`. -```bash -git clone git@github.com:roostorg/osprey.git -cd osprey -``` - -### 2. Install Dependencies - -```bash -# Install all dependencies including development tools -uv sync -``` - -This command will: - -- Create a virtual environment automatically -- Install all production dependencies -- Install development dependencies (ruff, mypy, pre-commit) automatically -- Use the locked versions from `uv.lock` for reproducible builds - -**Note**: `uv sync` includes development dependencies by default. Use `uv sync --no-dev` if you only want production dependencies. - -### 3. Set Up Pre-commit Hooks - -```bash -uv run pre-commit install -``` - -This installs git hooks that automatically run code quality checks before each commit. - -### 4. Verify Setup - -Run these commands to ensure everything is working correctly: - -```bash -# Check linting configuration -uv run ruff check - -# Check formatting -uv run ruff format --diff - -# Run type checking -uv run mypy . - -# Test pre-commit hooks -uv run pre-commit run --all-files -``` - -**Expected Results:** - -- Ruff should report "All checks passed!" or show specific issues to fix -- MyPy should run without errors -- Pre-commit should run all hooks successfully - -### 5. Getting Started - -```bash -docker compose up -d -``` - -or using the wrapper script - -```bash -./start.sh -``` +2. **Run the script** from a clone of the repo: -This starts up many services, including: -- **Osprey Worker**: The main engine that processes input events given the rules and UDFs - - **Test Data Producer**: Optional with `--profile test_data` -- **Osprey UI**: Frontend service that hosts the react code for the web interface and communicates to the UI API -- **Osprey UI API**: Backend service that provides data and functionality to the web interface -- **Kafka** (KRaft mode): Message streaming for user generated events -- **Postgres**: A database that the Worker, UI API, and Druid use for various reasons, such as the Postgres-backed Labels Service (in the example plugins) -- **Druid**: A database that consumes Osprey Worker outputs to power the UI API for real-time querying + ```sh + ./demo.sh + ``` -Alternatively, you can start Osprey with `osprey-coordinator`, refer to the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for more information + Or without cloning anything first—this clones the repo into `./osprey-demo` for you: -### 6. (Optional) Open ports for the UI/UI API + ```sh + curl -sSL https://raw.githubusercontent.com/roostorg/osprey/main/demo.sh | bash + ``` -By default, the `docker-compose.yaml` binds running services to `127.0.0.1`. If you are running the docker compose on a headless machine, you may need to modify this configuration and/or make changes to your firewall, specifically for ports `5002` and `5004`. + The script starts the whole stack (Kafka, Druid, PostgreSQL, MinIO, and Osprey's own worker, API, and UI—more than a dozen containers in all) plus a test-data producer that sends one synthetic post event per second. If it finds services from a previous run, it asks before stopping them; volumes from earlier demo runs are removed either way, so each demo starts from clean state. Expect the first run to spend a few minutes pulling and building images. -For example, if you use Tailscale to access your Osprey instance, you may change `127.0.0.1:5002:5002` to `:5002:5002`. Alternatively, if you wish for your instance to be accessible from the public internet, you may set it simply to `5002:5002` to bind to `0.0.0.0`. +3. **Wait for the "Demo Ready!" banner.** The script waits for every service to report healthy, then for the first events to flow through, so this takes a couple of minutes. When it's done, it opens the UI in your browser (or prints the URL to open yourself), pre-filled with a query for the last day of events. -Be aware that some firewalls like iptables/UFW do _not_ prevent access to ports being used by Docker networking. Not explicitly setting a bind address with only UFW as a firewall will not prevent access from the public internet unless [properly configured](https://github.com/chaifeng/ufw-docker). +## What to try -### 7. Access the Application +The demo ruleset has one rule, `ContainsHello`: any post containing the word "hello" gets its author banned and labeled `meow`. The producer builds five-word posts from a small word pool, so roughly a third of the generated posts trigger it. -The UI will automatically connect to the backend services running in Docker containers. +- **Watch the event stream.** The right panel shows events as they're processed. Click one that matched `ContainsHello` and look at its extracted features and effects. +- **Filter with a query.** Enter `ContainsHello == True` in the query bar to see only the posts that fired the rule; see [Query Syntax](../user/investigate/query-syntax.md) for what else you can express. +- **Group with Top N.** The pre-filled query groups by `UserId`, showing which synthetic users have been banned the most. +- **Open the Rules Visualizer** from the navigation bar to see the dependency graph between the demo's features and rule. -- Osprey UI: [localhost:5002](http://localhost:5002) -- Backend API: [localhost:5004](http://localhost:5004) -- Worker Service: [localhost:5001](http://localhost:5001) +The [User Guide](../user/README.md) covers the full investigation workflow, and [Writing Rules](../rules.md) explains the SML behind `ContainsHello` and how to write your own rules. -## Plugins +## Stopping the demo -In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](integrations.md). +From the repo folder (or `osprey-demo/` if the script cloned for you): -```python -@hookimpl_osprey -def register_udfs() -> Sequence[Type[UDFBase[Any, Any]]]: - # Register custom user-defined functions - -@hookimpl_osprey -def register_output_sinks(config: Config) -> Sequence[BaseOutputSink]: - # Define output destinations - # By default it prints the execution results to the console - -@hookimpl_osprey -def register_ast_validators() -> None: - # Register AST validators +```sh +docker compose --profile test_data down -v ``` -### Available hooks - -Implement any subset of these in your plugin's `register_plugins.py`: - -| Hook | Returns | Notes | -| --- | --- | --- | -| `register_udfs` | `Sequence[Type[UDFBase]]` | Custom user-defined functions. | -| `register_output_sinks` | `Sequence[BaseOutputSink]` | Where execution results go. | -| `register_ast_validators` | `Sequence[Type[BaseValidator]]` | Extra SML validators. | -| `register_action_proto_deserializer` | `ActionProtoDeserializer \| None` | Custom action proto → JSON. | -| `register_input_stream` | `BaseInputStream` | Single-provider (`firstresult`). | -| `register_execution_result_store` | `ExecutionResultStore` | Single-provider (`firstresult`). | -| `register_labels_service_or_provider` | `LabelsServiceBase \| LabelsProvider` | Single-provider (`firstresult`). | -| `register_validation_exporter` | `BaseValidationResultExporter \| None` | Optional; publishes experiment/bucket metadata after validation. Single-provider (`firstresult`). | -| `register_label_output_sink` | `BaseOutputSink \| None` | Optional; custom label-mutation sink, replacing the default `LabelOutputSink`. Single-provider (`firstresult`). | - -## Rules - -Rules are written in SML, some examples are provided in `example_rules/` with YAML config, the rules are mounted to the worker processes when the containers start via environment variables. ex: - -```bash -OSPREY_RULES_PATH=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink -``` +This stops every container and deletes the volumes, including all generated demo data. -For more about rules, see [Writing Rules](../rules.md). +## If something goes wrong -## Test Data +The script is a convenience wrapper; you can run the same stack directly and watch the logs: -Generate sample JSON actions: -```bash -docker compose --profile test_data up osprey-kafka-test-data-producer -d +```sh +docker compose --profile test_data up -d +docker compose logs --follow osprey-worker ``` -Produces user login events with timestamps, user IDs, and IP addresses to `osprey.actions_input` topic. +Then open once things settle. [Troubleshooting](troubleshooting.md) covers common failure modes, and [Local Development](local.md) documents each service the demo starts. diff --git a/docs/development/integrations.md b/docs/development/integrations.md index 638de6b76..0f91a636c 100644 --- a/docs/development/integrations.md +++ b/docs/development/integrations.md @@ -1,6 +1,6 @@ # Integrations & Plugins -Osprey is designed to be extended without modifying the core codebase; you can wire up your own logic such as detection functions, output destinations, entity state storage, and ML models through plugin packages that Osprey discovers at startup. A plugin package implements any subset of the hooks in the [Available hooks](README.md#available-hooks) table; this page walks through the integration points adopters ask about most. +Osprey is designed to be extended without modifying the core codebase; you can wire up your own logic such as detection functions, output destinations, entity state storage, and ML models through plugin packages that Osprey discovers at startup. A plugin package implements any subset of the hooks in the [Available hooks](local.md#available-hooks) table; this page walks through the integration points adopters ask about most. See the [`example_plugins/` directory](https://github.com/roostorg/osprey/tree/main/example_plugins) for a working reference package. @@ -168,7 +168,7 @@ An output sink receives every `ExecutionResult` after rule evaluation and decide ### Sync output sink -Subclass `BaseOutputSink` and implement three methods; for example: +Subclass `BaseOutputSink` and implement its methods; for example: ```python from osprey.worker.sinks.sink.output_sink import BaseOutputSink diff --git a/docs/development/local.md b/docs/development/local.md new file mode 100644 index 000000000..b8a5506ae --- /dev/null +++ b/docs/development/local.md @@ -0,0 +1,161 @@ +# Local Development + +Set up a full local development environment for Osprey. To just get up and running quickly with sample data, see [Getting Started](README.md) instead. + +## Prerequisites + +- **Operating System**: macOS, Linux, or Windows (with WSL recommended) +- **[Python](https://www.python.org/) 3.11 or higher** (check with `python --version`) +- **[Git](https://git-scm.com/)** for version control +- **[uv](https://docs.astral.sh/uv/)** for Python package management +- **[Node.js](https://nodejs.org/en/download/) 22+** for the UI (Corepack ships with Node and auto-resolves pnpm from `osprey_ui/package.json`'s `packageManager` field; no separate pnpm install needed) + +## Project Setup + +### 1. Clone the Repository + +```bash +git clone git@github.com:roostorg/osprey.git +cd osprey +``` + +### 2. Install Dependencies + +```bash +# Install all dependencies including development tools +uv sync +``` + +This command will: + +- Create a virtual environment automatically +- Install all production dependencies +- Install development dependencies (ruff, mypy, pre-commit) automatically +- Use the locked versions from `uv.lock` for reproducible builds + +**Note**: `uv sync` includes development dependencies by default. Use `uv sync --no-dev` if you only want production dependencies. + +### 3. Set Up Pre-commit Hooks + +```bash +uv run pre-commit install +``` + +This installs git hooks that automatically run code quality checks before each commit. + +### 4. Verify Setup + +Run these commands to ensure everything is working correctly: + +```bash +# Check linting configuration +uv run ruff check + +# Check formatting +uv run ruff format --diff + +# Run type checking +uv run mypy . + +# Test pre-commit hooks +uv run pre-commit run --all-files +``` + +**Expected Results:** + +- Ruff should report "All checks passed!" or show specific issues to fix +- MyPy should run without errors +- Pre-commit should run all hooks successfully + +### 5. Start the Services + +```bash +docker compose up -d +``` + +or using the wrapper script + +```bash +./start.sh +``` + +This starts up many services, including: +- **Osprey Worker**: The main engine that processes input events given the rules and UDFs + - **Test Data Producer**: Optional with `--profile test_data` +- **Osprey UI**: Frontend service that hosts the react code for the web interface and communicates to the UI API +- **Osprey UI API**: Backend service that provides data and functionality to the web interface +- **Kafka** (KRaft mode): Message streaming for user generated events +- **Postgres**: A database that the Worker, UI API, and Druid use for various reasons, such as the Postgres-backed Labels Service (in the example plugins) +- **Druid**: A database that consumes Osprey Worker outputs to power the UI API for real-time querying + +Alternatively, you can start Osprey with `osprey-coordinator`, refer to the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for more information + +### 6. (Optional) Open ports for the UI/UI API + +By default, the `docker-compose.yaml` binds running services to `127.0.0.1`. If you are running the docker compose on a headless machine, you may need to modify this configuration and/or make changes to your firewall, specifically for ports `5002` and `5004`. + +For example, if you use Tailscale to access your Osprey instance, you may change `127.0.0.1:5002:5002` to `:5002:5002`. Alternatively, if you wish for your instance to be accessible from the public internet, you may set it simply to `5002:5002` to bind to `0.0.0.0`. + +Be aware that some firewalls like iptables/UFW do _not_ prevent access to ports being used by Docker networking. Not explicitly setting a bind address with only UFW as a firewall will not prevent access from the public internet unless [properly configured](https://github.com/chaifeng/ufw-docker). + +### 7. Access the Application + +The UI will automatically connect to the backend services running in Docker containers. + +- Osprey UI: [localhost:5002](http://localhost:5002) +- Backend API: [localhost:5004](http://localhost:5004) +- Worker Service: [localhost:5001](http://localhost:5001) + +## Plugins + +In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](integrations.md). + +```python +@hookimpl_osprey +def register_udfs() -> Sequence[Type[UDFBase[Any, Any]]]: + # Register custom user-defined functions + +@hookimpl_osprey +def register_output_sinks(config: Config) -> Sequence[BaseOutputSink]: + # Define output destinations + # By default it prints the execution results to the console + +@hookimpl_osprey +def register_ast_validators() -> None: + # Register AST validators +``` + +### Available hooks + +Implement any subset of these in your plugin's `register_plugins.py`: + +| Hook | Returns | Notes | +| --- | --- | --- | +| `register_udfs` | `Sequence[Type[UDFBase]]` | Custom user-defined functions. | +| `register_output_sinks` | `Sequence[BaseOutputSink]` | Where execution results go. | +| `register_ast_validators` | `Sequence[Type[BaseValidator]]` | Extra SML validators. | +| `register_action_proto_deserializer` | `ActionProtoDeserializer \| None` | Custom action proto → JSON. | +| `register_input_stream` | `BaseInputStream` | Single-provider (`firstresult`). | +| `register_execution_result_store` | `ExecutionResultStore` | Single-provider (`firstresult`). | +| `register_labels_service_or_provider` | `LabelsServiceBase \| LabelsProvider` | Single-provider (`firstresult`). | +| `register_validation_exporter` | `BaseValidationResultExporter \| None` | Optional; publishes experiment/bucket metadata after validation. Single-provider (`firstresult`). | +| `register_label_output_sink` | `BaseOutputSink \| None` | Optional; custom label-mutation sink, replacing the default `LabelOutputSink`. Single-provider (`firstresult`). | + +## Rules + +Rules are written in SML, some examples are provided in `example_rules/` with YAML config, the rules are mounted to the worker processes when the containers start via environment variables. ex: + +```bash +OSPREY_RULES_PATH=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink +``` + +For more about rules, see [Writing Rules](../rules.md). + +## Test Data + +Generate sample JSON actions: +```bash +docker compose --profile test_data up osprey-kafka-test-data-producer -d +``` + +Produces user login events with timestamps, user IDs, and IP addresses to `osprey.actions_input` topic. diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 8eb13b844..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,63 +0,0 @@ -# Getting Started - -The fastest way to see Osprey working is the demo script: one command brings up the full stack with sample data and opens the UI on a pre-filled query. You don't need to write any code or configuration, and everything cleans up with a single command afterward. - -## Prerequisites - -- **Docker with the Compose v2 plugin.** The script checks for both, and for a running Docker daemon, before doing anything. -- **Free ports.** The stack needs a number of ports; the script checks them all up front and names any that are taken. The ones you'll actually visit are `5002` (Osprey UI) and `8888` (Druid console); the Osprey worker and UI API use `5001` and `5004`. -- **A few minutes on first run** while Docker pulls and builds images. Later runs are much faster. - -## Run the demo - -**1. Run the script.** From a clone of the repo: - -```sh -./demo.sh -``` - -Or without cloning anything first — this clones the repo into `./osprey-demo` for you: - -```sh -curl -sSL https://raw.githubusercontent.com/roostorg/osprey/main/demo.sh | bash -``` - -The script starts the whole stack — Kafka, Druid, PostgreSQL, MinIO, and Osprey's own worker, API, and UI, more than a dozen containers in all — plus a test-data producer that sends one synthetic post event per second. If it finds services from a previous run, it asks before stopping them; volumes from earlier demo runs are removed either way, so each demo starts from clean state. - -**2. Wait for the "Demo Ready!" banner.** The script waits for every service to report healthy, then for the first events to flow through, so this takes a couple of minutes. When it's done, it opens the UI in your browser (or prints the URL to open yourself), pre-filled with a query for the last day of events. - -## What to try - -The demo ruleset has exactly one rule, `ContainsHello`: any post containing the word "hello" gets its author banned and labeled `meow`. The producer builds five-word posts from a small word pool, so roughly a third of the generated posts trigger it. - -- **Watch the event stream.** The right panel shows events as they're processed. Click one that matched `ContainsHello` and look at its extracted features and effects. -- **Filter with a query.** Enter `ContainsHello == True` in the query bar to see only the posts that fired the rule; see [Query Syntax](user/investigate/query-syntax.md) for what else you can express. -- **Group with Top N.** The pre-filled query groups by `UserId`, showing which synthetic users have been banned the most. -- **Open the Rules Visualizer** from the navigation bar to see the dependency graph between the demo's features and rule. - -## Stopping the demo - -From the repo directory (or `osprey-demo/` if the script cloned for you): - -```sh -docker compose --profile test_data down -v -``` - -This stops every container and deletes the volumes, including all generated demo data. - -## If something goes wrong - -The script is a convenience wrapper; you can run the same stack directly and watch the logs: - -```sh -docker compose --profile test_data up -d -docker compose logs --follow osprey-worker -``` - -Then open once things settle. [Troubleshooting](development/troubleshooting.md) covers the common failure modes. - -## Where to go next - -- [User Guide](user/README.md) — the investigation workflow: queries, charts, labels, and bulk actions. -- [Writing Rules](rules.md) — how the SML rules that powered the demo work, and how to write your own. -- [Development Guide](development/README.md) — run Osprey against your own events and set up a development environment. diff --git a/docs/rules.md b/docs/rules.md index 1cf02e77f..b8ece4a6f 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -64,7 +64,7 @@ It's a fair question: why introduce a bespoke language at all, instead of writin - **That static graph is what makes the engine fast and introspectable.** Because nothing can have side effects or run in a loop, the executor can safely evaluate independent branches of the graph in parallel using gevent greenlets (`executor.py`); the UI can also render the exact dependency graph for any rule or feature, which powers the [Rules Visualizer](user/manage.md#rules-visualizer) and the cross-referencing in the [Rules](user/manage.md#rules-registry) and [Features Registry](user/manage.md#features-registry) pages. Plain Python doesn't give you that for free; you'd have to reconstruct it by statically analyzing arbitrary code, which is a much harder problem. - **Types are checked before a rule ever runs against live traffic.** `validate_static_types.py` and `validate_call_rvalue.py` catch type errors and misused return values at validation/push time, not as a runtime `AttributeError` in production the first time a rare code path executes. -None of that is free, and it's worth being honest about the cost. **There's no dedicated SML syntax highlighting or language server today.** Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable (if imperfect) highlighting as a stopgap, but proper tooling is a known gap rather than a solved problem. See the [Development Guide](development/README.md) for current editor setup. +None of that is free, and it's worth being honest about the cost. **There's no dedicated SML syntax highlighting or language server today.** Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable (if imperfect) highlighting as a stopgap, but proper tooling is a known gap rather than a solved problem. See [IDE Setup](development/ide.md) for current editor setup. ## Rule Structuring From f23790fb824b21d220b1c8f092931facb939e278 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 13:21:41 -0400 Subject: [PATCH 29/53] docs: drop unnecessary exact count in workflow.md --- docs/development/workflow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/workflow.md b/docs/development/workflow.md index aec5f0148..c20fe9c5f 100644 --- a/docs/development/workflow.md +++ b/docs/development/workflow.md @@ -29,7 +29,7 @@ uv run mypy osprey_worker/src/osprey/worker/lib ## Commit messages -Follow the [Conventional Commits](https://www.conventionalcommits.org/) format. Two real examples from this repo's history: +Follow the [Conventional Commits](https://www.conventionalcommits.org/) format. Real examples from this repo's history: ``` fix(ui): show the event-stream timezone once, not twice From 715a7d6559bad0f237f86a3a8994287c0084721c Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 14:02:09 -0400 Subject: [PATCH 30/53] docs: catch Local Development up to the 1.1 stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MinIO is the default execution result store in docker-compose.yaml but was missing from the service list, and starting without the test_data profile leaves the UI empty with no explanation — friction log #96's top complaint. Also collapse the uv sync and "Expected Results" scaffolding into sentences and link the directory instead of README.md so links work on GitHub and the built site. --- docs/development/local.md | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/docs/development/local.md b/docs/development/local.md index b8a5506ae..0689c30b7 100644 --- a/docs/development/local.md +++ b/docs/development/local.md @@ -1,6 +1,6 @@ # Local Development -Set up a full local development environment for Osprey. To just get up and running quickly with sample data, see [Getting Started](README.md) instead. +Set up a full local development environment for Osprey. To just get up and running quickly with sample data, see [Getting Started](./) instead. ## Prerequisites @@ -26,14 +26,7 @@ cd osprey uv sync ``` -This command will: - -- Create a virtual environment automatically -- Install all production dependencies -- Install development dependencies (ruff, mypy, pre-commit) automatically -- Use the locked versions from `uv.lock` for reproducible builds - -**Note**: `uv sync` includes development dependencies by default. Use `uv sync --no-dev` if you only want production dependencies. +This creates a virtual environment and installs production and development dependencies (ruff, mypy, pre-commit) at the locked versions from `uv.lock`. Use `uv sync --no-dev` if you only want production dependencies. ### 3. Set Up Pre-commit Hooks @@ -61,32 +54,31 @@ uv run mypy . uv run pre-commit run --all-files ``` -**Expected Results:** - -- Ruff should report "All checks passed!" or show specific issues to fix -- MyPy should run without errors -- Pre-commit should run all hooks successfully +Ruff reports "All checks passed!" (or the specific issues to fix), and mypy and the pre-commit hooks run without errors. ### 5. Start the Services ```bash -docker compose up -d +docker compose --profile test_data up -d ``` or using the wrapper script ```bash -./start.sh +./start.sh --profile test_data up -d ``` +The `test_data` profile includes a producer that generates sample events; without it the stack still runs, but the UI shows no data until you send events yourself. [Getting Started](./) covers the one-command demo that wraps all of this. + This starts up many services, including: - **Osprey Worker**: The main engine that processes input events given the rules and UDFs - - **Test Data Producer**: Optional with `--profile test_data` + - **Test Data Producer**: The `--profile test_data` sample event generator - **Osprey UI**: Frontend service that hosts the react code for the web interface and communicates to the UI API - **Osprey UI API**: Backend service that provides data and functionality to the web interface - **Kafka** (KRaft mode): Message streaming for user generated events - **Postgres**: A database that the Worker, UI API, and Druid use for various reasons, such as the Postgres-backed Labels Service (in the example plugins) - **Druid**: A database that consumes Osprey Worker outputs to power the UI API for real-time querying +- **MinIO**: S3-compatible object storage; the default execution result store in this stack (`OSPREY_EXECUTION_RESULT_STORAGE_BACKEND=minio`) Alternatively, you can start Osprey with `osprey-coordinator`, refer to the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for more information From 1a821abceea406fa7693c7dba42ea134e9e6d1a2 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 14:02:27 -0400 Subject: [PATCH 31/53] docs: document event stream first-load defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #297 event cards fall back to showing all extracted features when nothing is configured or selected, so the stream isn't empty on first load — exactly the confusion friction log #96 flagged. Also quote the button by its verbatim label, Select Summary Features. --- docs/user/investigate/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/investigate/README.md b/docs/user/investigate/README.md index 00cbd1a56..7af5953e3 100644 --- a/docs/user/investigate/README.md +++ b/docs/user/investigate/README.md @@ -59,7 +59,7 @@ The right panel is Osprey's live feed. It shows individual events matching the c ![Event Stream](../../images/event-stream.png) -The stream can be displayed in card format or list format. You can customize which fields appear per event by selecting **Summary Features**, helpful when different team members care about different metadata. +The stream can be displayed in card format or list format. On first load, each event card shows all of its extracted features (or the summary features configured for that action type, if any), so the stream is useful before you've set anything up. You can customize which fields appear per event with **Select Summary Features**, helpful when different team members care about different metadata. ![Summary Features](../../images/summary-features.png) From 7579f45b208f80ebf0e16b0def26882380aec580 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 14:02:41 -0400 Subject: [PATCH 32/53] docs: tighten User Guide landing, add screenshots Replace the per-anchor nav lists with one line per section and a clickable screenshot each, following Coop's user guide index pattern. Puts two previously-unused images to work; left-side-menu.png stays out because it shows an obsolete flat nav. --- docs/user/README.md | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/docs/user/README.md b/docs/user/README.md index 44474a01e..7feb4a067 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -2,26 +2,20 @@ Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. -Osprey's tools are categorized into three primary functions: +[![The Osprey UI during an investigation: a query filtering events where SuspiciousDisplayName is True, timeseries charts of matching event volume, and the live event stream with one entity's label popup open showing a negative spam_display_name label](../images/osprey-suspicious-display-name.png)](../images/osprey-suspicious-display-name.png) -**Investigate**: query events and examine entity behavior +The sidebar groups Osprey's tools by task, and this guide follows the same sections: -- [Query](investigate/#query): the main investigation page -- [Query History](investigate/#query-history): browse and re-run past queries -- [Saved Queries](investigate/#saved-queries): manage frequently used queries +- **[Investigate](investigate/)**: query events in real time, chart the results, and drill into individual events and entities. Query history and saved queries let you revisit and share past investigations. -**Manage**: understand and navigate your rule and feature configuration + [![Two Top N tables for a query, grouping matching events by event type and by post text](../images/multiple-top-charts.png)](../images/multiple-top-charts.png) -- [Rules Visualizer](manage.md#rules-visualizer): graph view of label/rule relationships -- [UDF Registry](manage.md#udf-registry): API reference for all available functions -- [Features Registry](manage.md#features-registry): inventory of all features in the system -- [Rules Registry](manage.md#rules-registry): inventory of all rules in the system +- **[Manage](manage.md)**: browse the rules, features, and UDFs configured in your deployment, and visualize how rules and labels relate. -**Operate**: run and review bulk operations + [![The UDF Registry listing available functions with type signatures and descriptions, grouped by category](../images/udf-documentation.png)](../images/udf-documentation.png) -- [Bulk Actions](operate.md#bulk-actions): start and monitor bulk labeling jobs -- [Bulk Job History](operate.md#bulk-job-history): review past bulk jobs and their results +- **[Operate](operate.md)**: run bulk labeling jobs over query results and review past jobs. -The sidebar can be collapsed to a narrow icon-only strip using the toggle at the bottom. Its state persists between sessions. + [![The Bulk Edit Labels form for roughly 9,800 entities matching a query, with label name, status, reason, and expiration fields](../images/bulk-label.png)](../images/bulk-label.png) -The interface supports light and dark themes, see [Appearance](appearance.md) for details. \ No newline at end of file +The sidebar can be collapsed to an icon-only strip with the toggle at the bottom, and its state persists between sessions. The interface supports light and dark themes; see [Appearance](appearance.md) for details. From f556bed778bbe67319fbc8891655815d7cf29b5f Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 14:02:56 -0400 Subject: [PATCH 33/53] docs: link directories instead of README files Directory links render the README on the GitHub web UI and resolve to index.html on the built site; README.md links only work in one of the two. --- docs/data-flow.md | 2 +- docs/development/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-flow.md b/docs/data-flow.md index dacfa6291..2cc33a5bb 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -94,4 +94,4 @@ Separately, `ExecutionResultStore` backends persist full execution results for l ### How results become queryable in the UI -Druid consumes the worker's Kafka output and powers the UI API's real-time querying; this is what backs the [Investigate](user/investigate/README.md) query interface and the [Rules](user/manage.md#rules-registry)/[Features Registries](user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. +Druid consumes the worker's Kafka output and powers the UI API's real-time querying; this is what backs the [Investigate](user/investigate/) query interface and the [Rules](user/manage.md#rules-registry)/[Features Registries](user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. diff --git a/docs/development/README.md b/docs/development/README.md index fad52cb12..ee31d5f8d 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -37,7 +37,7 @@ The demo ruleset has one rule, `ContainsHello`: any post containing the word "he - **Group with Top N.** The pre-filled query groups by `UserId`, showing which synthetic users have been banned the most. - **Open the Rules Visualizer** from the navigation bar to see the dependency graph between the demo's features and rule. -The [User Guide](../user/README.md) covers the full investigation workflow, and [Writing Rules](../rules.md) explains the SML behind `ContainsHello` and how to write your own rules. +The [User Guide](../user/) covers the full investigation workflow, and [Writing Rules](../rules.md) explains the SML behind `ContainsHello` and how to write your own rules. ## Stopping the demo From e3ff411cb031ed86cafdc7c5fd930ff9663aa00c Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 14:06:48 -0400 Subject: [PATCH 34/53] docs: add Concepts page to the User Guide Defines actions vs. events, features, entities, rules, effects, labels, and verdicts (the last of which was never defined anywhere) in the order data flows through Osprey, with one consistent example that matches the demo ruleset. --- docs/SUMMARY.md | 1 + docs/user/README.md | 2 +- docs/user/concepts.md | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 docs/user/concepts.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 1c6d6d7b9..4457e1593 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -5,6 +5,7 @@ --- - [User Guide](user/README.md) + - [Concepts](user/concepts.md) - [Investigate](user/investigate/README.md) - [Labels](user/investigate/labels.md) - [Query Syntax](user/investigate/query-syntax.md) diff --git a/docs/user/README.md b/docs/user/README.md index 7feb4a067..7ddc6785a 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -1,6 +1,6 @@ # User Guide -Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. +Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. If terms like actions, features, and labels are new to you, start with [Concepts](concepts.md). [![The Osprey UI during an investigation: a query filtering events where SuspiciousDisplayName is True, timeseries charts of matching event volume, and the live event stream with one entity's label popup open showing a negative spam_display_name label](../images/osprey-suspicious-display-name.png)](../images/osprey-suspicious-display-name.png) diff --git a/docs/user/concepts.md b/docs/user/concepts.md new file mode 100644 index 000000000..9c40689df --- /dev/null +++ b/docs/user/concepts.md @@ -0,0 +1,55 @@ +# Concepts + +Osprey watches a stream of things happening on your platform, runs each one through your rules in real time, and records what it found so you can query and act on it. This page defines the terms the rest of these docs (and the UI itself) use. They're listed in the order data flows through Osprey, and later concepts build on earlier ones, so it's worth a read top to bottom. + +Throughout, imagine you're moderating a small social network where users create posts—the same scenario Osprey's example rules and [demo](../development/) use. + +## Actions and events + +Anything that happens on your platform—someone creates a post, sends a message, registers an account—can be sent to Osprey as it happens. Your platform likely calls these **events**; once inside Osprey, each one is called an **action**. The two words name the same thing, and you'll meet both: the query bar filters on `ActionName`, while the UI's live feed is the Event Stream. These docs say "event" when talking about your platform and "action" when talking about Osprey's view of one. + +An action arrives with a name, like `create_post`, and whatever JSON data your platform sent along—say, the author's user ID and the post text "hello world." + +## Features + +A **feature** is a named value that your rules extract from each action: `PostText`, `UserId`, `AccountAgeSeconds`. Features are what you query on—every feature your rules define is queryable by name, unless its name starts with an underscore, which keeps it private to the rule file that defined it. + +For the "hello world" post, your rules might extract `UserId`, `PostText`, and `EventType` as features. + +## Entities + +An **entity** is a feature that identifies a _thing_ on your platform rather than describing one—a user ID, an email address, an IP address. Entities are declared with a type (like `User`) so Osprey knows which values refer to the same thing across many actions. That identity is what makes labels (below) possible, and it's why clicking an entity in the UI opens a view of everything Osprey knows about it. + +In our example, `UserId` would be declared as an entity: the same user posts many times, and you want Osprey to remember them. + +## Rules + +A **rule** is a named condition over features, written in Osprey's rule language, SML: "the event is a post creation and the post text contains 'hello'." Rules are evaluated against every action as it arrives. On their own rules just produce a true or false—which is itself queryable, like any feature—but they can be wired to effects. + +Rules live in files your team reviews and versions like any other code; [Writing Rules](../rules.md) covers the language. + +## Effects + +An **effect** is what a matched rule does beyond evaluating to true: ban the user, add a label to an entity, declare a verdict. Effects are recorded on the action's result, so an investigator can always see afterward exactly what Osprey did and why. + +When the demo's `ContainsHello` rule matches our "hello world" post, it fires two effects: a ban for the author, and a `meow` label added to them. + +## Labels + +A **label** is a tag on an entity that persists across actions—Osprey's memory. Rules add and remove labels as an effect, and can also _check_ labels, so past decisions inform future ones: "flag this post if its author was previously labeled a spammer." You can also add or remove labels by hand from the UI, one entity at a time or in bulk. + +Labels have a name, the entity types they apply to, and a connotation (positive or negative). [Labels](investigate/labels.md) covers how they behave in investigations. + +## Verdicts + +A **verdict** is Osprey's answer when a caller is waiting for one. Most deployments feed Osprey from a queue and read results the same way, but a service can also submit an action synchronously and get a response; rules declare verdict strings, and those—along with any labels applied—are what the caller gets back. If your deployment only consumes results asynchronously, you may never handle a verdict directly, though you can spot them among an action's recorded features in the UI. + +## Results, and where they surface + +Every processed action produces an execution result: the extracted features, the rules that matched, and the effects that fired. Results are indexed for querying—that's what the whole [Investigate](investigate/) side of the UI reads. The query bar filters results by feature (`ContainsHello == True`), charts aggregate them over time, Top N groups them by any feature, and the Event Stream shows them one by one as they happen. Clicking any entity opens its details: current labels and its history on your platform. + +How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](../data-flow.md) page covers it. + +## Where to go from here + +Try these ideas out on live sample data by running the [demo](../development/), then head to [Investigate](investigate/) to learn the query workflow, or [Writing Rules](../rules.md) when you're ready to automate a decision of your own. From 8fddcc8d37bed16d56923599102439b5399da360 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jul 2026 14:08:25 -0400 Subject: [PATCH 35/53] docs: point query-syntax at Concepts instead of redefining The page opened with its own definitions of actions, features, entities, and effects; now that we cover that in Concepts, replace them with a sentence and a link to the Concepts page --- docs/user/investigate/query-syntax.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index 781b94a0d..4b9c7f051 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -2,22 +2,7 @@ Osprey uses SML (“Some Madeup Language,” a subset of Python with additional restrictions) for queries. Queries filter events by matching against features, actions, and labels. -## Core concepts - -**Actions** are events that happen in your system, like a user creating a post or sending a message. Your query filters which action types to look at. - -**Features** are named variables extracted from events. All features are in a global namespace; any feature exported by Osprey rules is queryable. Prefixing a variable with `_` keeps it local to a rule file and excludes it from querying. - -```py -UserId: Entity[int] = EntityJson(type='User', path='$.user.id', coerce_type=True) -UserEmail: str = JsonData(path='$.user.email', required=False) -``` - -Both `UserId` and `UserEmail` above are features. - -**Entities** are a special kind of feature. They can have effects applied to them: labels, classifications, signals. Clicking an entity in the UI navigates to its [Entity Details](labels.md#entity-details) view. - -**Effects** are outcomes triggered when one or more rules evaluate to true; for example, applying a `spammer` label to a `UserId` entity. +Queries match against **features**—the named values your rules extract from each action—including **entities** and **labels**. If those terms are new, read [Concepts](../concepts.md) first. ## Basic comparisons From d89bd2a92ec2cb5a0574e3f721cee83cd6f53697 Mon Sep 17 00:00:00 2001 From: Cassidy James Date: Tue, 14 Jul 2026 12:11:09 -0600 Subject: [PATCH 36/53] docs: remove link to personas from integrations page We don't need to call that out here imho --- docs/development/integrations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/integrations.md b/docs/development/integrations.md index 0f91a636c..209d72da5 100644 --- a/docs/development/integrations.md +++ b/docs/development/integrations.md @@ -267,7 +267,7 @@ def register_labels_service_or_provider(config): ## Connecting to a review tool -Osprey doesn't currently support direct integration with a review tool ([a known unmet need](../research-personas.md)); however, these extension points can help you integrate: +Osprey doesn't currently support direct integration with a review tool; however, these extension points can help you integrate: - `register_output_sinks` — push execution results into a review queue as they're produced. - `register_label_output_sink` — a sink specifically for label mutations, replacing the default `LabelOutputSink`. From 813ceac9426cb90628404865bf8b1ec72bb7bb07 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 10:11:17 +0200 Subject: [PATCH 37/53] docs: Consolodate appearance into user README --- docs/user/README.md | 4 ++-- docs/user/appearance.md | 19 ------------------- 2 files changed, 2 insertions(+), 21 deletions(-) delete mode 100644 docs/user/appearance.md diff --git a/docs/user/README.md b/docs/user/README.md index 7ddc6785a..2bb4ab167 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -1,6 +1,6 @@ # User Guide -Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. If terms like actions, features, and labels are new to you, start with [Concepts](concepts.md). +Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. If terms like events, features, and labels are new to you, start with [Concepts](concepts.md). [![The Osprey UI during an investigation: a query filtering events where SuspiciousDisplayName is True, timeseries charts of matching event volume, and the live event stream with one entity's label popup open showing a negative spam_display_name label](../images/osprey-suspicious-display-name.png)](../images/osprey-suspicious-display-name.png) @@ -18,4 +18,4 @@ The sidebar groups Osprey's tools by task, and this guide follows the same secti [![The Bulk Edit Labels form for roughly 9,800 entities matching a query, with label name, status, reason, and expiration fields](../images/bulk-label.png)](../images/bulk-label.png) -The sidebar can be collapsed to an icon-only strip with the toggle at the bottom, and its state persists between sessions. The interface supports light and dark themes; see [Appearance](appearance.md) for details. +The sidebar can be collapsed to an icon-only strip with the toggle at the bottom, and its state persists between sessions. The interface follows your OS or browser color scheme preference, or you can select a light or dark style using the toggle in the navigation bar. diff --git a/docs/user/appearance.md b/docs/user/appearance.md deleted file mode 100644 index 7adef2c04..000000000 --- a/docs/user/appearance.md +++ /dev/null @@ -1,19 +0,0 @@ -# Appearance - -Osprey supports light and dark themes. By default, the interface follows your OS or browser color scheme preference. You can override this at any time using the theme toggle in the navigation bar. - -## Changing the theme - -The theme toggle is in the top-right area of the navigation bar. It offers three options: - -| Option | Behavior | -|--------|----------| -| **Light** | Always use the light theme | -| **System** | Follow the OS/browser preference *(default)* | -| **Dark** | Always use the dark theme | - -Your choice is saved in the browser and persists across sessions. Clearing browser storage resets it to **System**. - -## System preference - -When set to **System**, Osprey listens for OS color scheme changes in real time. Switching your OS between light and dark mode while Osprey is open takes effect immediately, no page reload required. \ No newline at end of file From 2a602bb008b2f6e67093058b9c6c6fe983ecbaba Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 10:12:06 +0200 Subject: [PATCH 38/53] docs: Move concepts to concepts section; update terminology --- docs/SUMMARY.md | 4 +-- docs/concepts.md | 58 +++++++++++++++++++++++++++++++++++++++++++ docs/user/concepts.md | 55 ---------------------------------------- 3 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 docs/concepts.md delete mode 100644 docs/user/concepts.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4457e1593..6749e34c4 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -5,7 +5,6 @@ --- - [User Guide](user/README.md) - - [Concepts](user/concepts.md) - [Investigate](user/investigate/README.md) - [Labels](user/investigate/labels.md) - [Query Syntax](user/investigate/query-syntax.md) @@ -29,8 +28,9 @@ # Concepts & More +- [Concepts](concepts.md) +- [Data Flow](data-flow.md) - [Writing Rules](rules.md) -- [Data Flow: Getting Data In and Out](data-flow.md) - [User Research & Personas](research-personas.md) --- diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 000000000..3ddeeac31 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,58 @@ +# Concepts + +Osprey watches a stream of events happening on your platform, runs each one through your rules in real time, and records what it found so analysts can query and act on it. This page defines the terms the rest of these docs (and the UI itself) use. They're listed in the order data flows through Osprey, and later concepts build on earlier ones, so it's worth a read from start to finish. + +Throughout the docs are examples of moderating a small social network where users create posts; remember that Osprey is useful for platforms of all kinds, and its concepts may be adapted for any online platform where people interact. + +## Events (and actions) + +Anything that happens on your platform—e.g. someone registers an account, creates a post, sends a message, or reacts to content—can be sent to Osprey as it happens as an **event**. Each event arrives with a name, like `create_post`, and whatever JSON data your platform sent along, e.g. the author's user ID and the post text "hello world." + +> [!NOTE] +> You may also come across the term **action**: Osprey's submission API and rules engine historically call each incoming event an action, which is why the query bar filters on `ActionName` even though the UI's live feed is the Event Stream. In this context, they're the same: one event in, one `ActionName` recorded. + +## Features + +A **feature** is a named value that your rules extract from each event: `PostText`, `UserId`, `AccountAgeSeconds`. Features are what you query on; every feature your rules define is queryable by name, unless its name starts with an underscore, which keeps it private to the rule file that defined it. + +For a "hello world" post, your rules might extract `UserId`, `PostText`, and `EventType` as features. + +## Entities + +An **entity** is a feature that uniquely identifies something on your platform: a user ID, an email address, an IP address. Entities are declared with a type (like `User`) so Osprey knows which values refer to the same thing across many events. That identity is what makes [labels](#labels) possible, and it's how selecting an entity in the UI can open a view of everything Osprey knows about it. + +In our example, `UserId` would be declared as an entity: the same user posts many times, and you want Osprey to remember them. + +## Rules + +A **rule** is a named condition over features, written in Osprey's rule language, SML. Rules are evaluated against every event as it arrives. On their own, rules just return a boolean true/false (which is itself queryable, like any feature). Rules can be wired to [effects](#effets). + +In our example, a rule might cover "the event is a post creation and the post text contains 'hello'." + +Rules live as code in your Osprey deployment alongside the Osprey code itself. See [Writing Rules](../rules.md) for more detail. + +## Effects + +An **effect** is what a matched rule does beyond evaluating; e.g. ban the user, add a label to an entity, declare a verdict. Effects are recorded on the event's result, so an investigator can always see afterward exactly what Osprey did and why. + +When the demo's `ContainsHello` rule matches our "hello world" post, it fires two effects: a ban for the author, and a `meow` label added to them. + +## Labels + +A **label** is a tag on an entity that persists across events—Osprey's memory. Rules add and remove labels as an effect, and can also _check_ labels, so past decisions inform future ones: "flag this post if its author was previously labeled a spammer." You can also add or remove labels by hand from the UI, one entity at a time or in bulk. + +Labels have a name, the entity types they apply to, and a connotation (positive or negative). See [Labels](investigate/labels.md) for more information about how they're used for investigations. + +## Verdicts + +A **verdict** is Osprey's answer when a caller is waiting for one. Most deployments feed Osprey from a queue and read results the same way, but a service can also submit an event synchronously and get a response; rules declare verdict strings, and those—along with any labels applied—are what the caller gets back. If your deployment only consumes results asynchronously, you may never handle a verdict directly, though you can spot them among an event's recorded features in the UI. + +## Results, and where they surface + +Every processed event produces an execution result: the extracted features, the rules that matched, and the effects that fired. Results are indexed for querying—that's what the whole [Investigate](investigate/) side of the UI reads. The query bar filters results by feature (`ContainsHello == True`), charts aggregate them over time, Top N groups them by any feature, and the Event Stream shows them one by one as they happen. Clicking any entity opens its details: current labels and its history on your platform. + +How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](../data-flow.md) page covers it. + +## Where to go from here + +Try these ideas out on live sample data by running the [demo](../development/), then head to [Investigate](investigate/) to learn the query workflow, or [Writing Rules](../rules.md) when you're ready to automate a decision of your own. diff --git a/docs/user/concepts.md b/docs/user/concepts.md deleted file mode 100644 index 9c40689df..000000000 --- a/docs/user/concepts.md +++ /dev/null @@ -1,55 +0,0 @@ -# Concepts - -Osprey watches a stream of things happening on your platform, runs each one through your rules in real time, and records what it found so you can query and act on it. This page defines the terms the rest of these docs (and the UI itself) use. They're listed in the order data flows through Osprey, and later concepts build on earlier ones, so it's worth a read top to bottom. - -Throughout, imagine you're moderating a small social network where users create posts—the same scenario Osprey's example rules and [demo](../development/) use. - -## Actions and events - -Anything that happens on your platform—someone creates a post, sends a message, registers an account—can be sent to Osprey as it happens. Your platform likely calls these **events**; once inside Osprey, each one is called an **action**. The two words name the same thing, and you'll meet both: the query bar filters on `ActionName`, while the UI's live feed is the Event Stream. These docs say "event" when talking about your platform and "action" when talking about Osprey's view of one. - -An action arrives with a name, like `create_post`, and whatever JSON data your platform sent along—say, the author's user ID and the post text "hello world." - -## Features - -A **feature** is a named value that your rules extract from each action: `PostText`, `UserId`, `AccountAgeSeconds`. Features are what you query on—every feature your rules define is queryable by name, unless its name starts with an underscore, which keeps it private to the rule file that defined it. - -For the "hello world" post, your rules might extract `UserId`, `PostText`, and `EventType` as features. - -## Entities - -An **entity** is a feature that identifies a _thing_ on your platform rather than describing one—a user ID, an email address, an IP address. Entities are declared with a type (like `User`) so Osprey knows which values refer to the same thing across many actions. That identity is what makes labels (below) possible, and it's why clicking an entity in the UI opens a view of everything Osprey knows about it. - -In our example, `UserId` would be declared as an entity: the same user posts many times, and you want Osprey to remember them. - -## Rules - -A **rule** is a named condition over features, written in Osprey's rule language, SML: "the event is a post creation and the post text contains 'hello'." Rules are evaluated against every action as it arrives. On their own rules just produce a true or false—which is itself queryable, like any feature—but they can be wired to effects. - -Rules live in files your team reviews and versions like any other code; [Writing Rules](../rules.md) covers the language. - -## Effects - -An **effect** is what a matched rule does beyond evaluating to true: ban the user, add a label to an entity, declare a verdict. Effects are recorded on the action's result, so an investigator can always see afterward exactly what Osprey did and why. - -When the demo's `ContainsHello` rule matches our "hello world" post, it fires two effects: a ban for the author, and a `meow` label added to them. - -## Labels - -A **label** is a tag on an entity that persists across actions—Osprey's memory. Rules add and remove labels as an effect, and can also _check_ labels, so past decisions inform future ones: "flag this post if its author was previously labeled a spammer." You can also add or remove labels by hand from the UI, one entity at a time or in bulk. - -Labels have a name, the entity types they apply to, and a connotation (positive or negative). [Labels](investigate/labels.md) covers how they behave in investigations. - -## Verdicts - -A **verdict** is Osprey's answer when a caller is waiting for one. Most deployments feed Osprey from a queue and read results the same way, but a service can also submit an action synchronously and get a response; rules declare verdict strings, and those—along with any labels applied—are what the caller gets back. If your deployment only consumes results asynchronously, you may never handle a verdict directly, though you can spot them among an action's recorded features in the UI. - -## Results, and where they surface - -Every processed action produces an execution result: the extracted features, the rules that matched, and the effects that fired. Results are indexed for querying—that's what the whole [Investigate](investigate/) side of the UI reads. The query bar filters results by feature (`ContainsHello == True`), charts aggregate them over time, Top N groups them by any feature, and the Event Stream shows them one by one as they happen. Clicking any entity opens its details: current labels and its history on your platform. - -How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](../data-flow.md) page covers it. - -## Where to go from here - -Try these ideas out on live sample data by running the [demo](../development/), then head to [Investigate](investigate/) to learn the query workflow, or [Writing Rules](../rules.md) when you're ready to automate a decision of your own. From 14233bf1dbc824a6a8ede93d1d4698c916b83cd6 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 10:14:33 +0200 Subject: [PATCH 39/53] docs: Update Data Flow page terminology --- docs/data-flow.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data-flow.md b/docs/data-flow.md index 2cc33a5bb..a27df985b 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -1,6 +1,6 @@ -# Data Flow: Getting Data In and Out +# Data Flow -This page covers how events actually reach Osprey, how results come back out, and what to change (not fork) if your platform doesn't look like the default docker-compose setup. +How events actually reach Osprey, how results come back out, and what to change (not fork) if your platform doesn't look like the default docker-compose setup. ## Architecture at a glance @@ -26,11 +26,11 @@ At its core, Osprey is a pipeline: events come in, get evaluated against your SM Druid + UI API → Osprey UI ``` -For the worker's own rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. +For the worker's rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. ## Getting data in -There are three real ways to get an event into Osprey: +There are three ways to get an event into Osprey: ### 1. Kafka (the default) From 0e812e04e73e4b92d3ecdfc594d9079bbbd382ff Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 10:15:12 +0200 Subject: [PATCH 40/53] docs: Consistently use "event" language --- docs/user/investigate/README.md | 2 +- docs/user/investigate/query-syntax.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/investigate/README.md b/docs/user/investigate/README.md index 7af5953e3..8f154e2d5 100644 --- a/docs/user/investigate/README.md +++ b/docs/user/investigate/README.md @@ -59,7 +59,7 @@ The right panel is Osprey's live feed. It shows individual events matching the c ![Event Stream](../../images/event-stream.png) -The stream can be displayed in card format or list format. On first load, each event card shows all of its extracted features (or the summary features configured for that action type, if any), so the stream is useful before you've set anything up. You can customize which fields appear per event with **Select Summary Features**, helpful when different team members care about different metadata. +The stream can be displayed in card format or list format. On first load, each event card shows all of its extracted features (or the summary features configured for that event type, if any), so the stream is useful before you've set anything up. You can customize which fields appear per event with **Select Summary Features**, helpful when different team members care about different metadata. ![Summary Features](../../images/summary-features.png) diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index 4b9c7f051..138ea2179 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -2,7 +2,7 @@ Osprey uses SML (“Some Madeup Language,” a subset of Python with additional restrictions) for queries. Queries filter events by matching against features, actions, and labels. -Queries match against **features**—the named values your rules extract from each action—including **entities** and **labels**. If those terms are new, read [Concepts](../concepts.md) first. +Queries match against **features**—the named values your rules extract from each event—including **entities** and **labels**. If those terms are new, read [Concepts](../concepts.md) first. ## Basic comparisons @@ -43,7 +43,7 @@ ListLength(list=UserConnections) > 10 ## Label queries -The query UI searches across actions (events), not entity state, so `HasLabel()` won't work here. Use `DidAddLabel()` instead, which matches events where a label was added: +The query UI searches across events, not entity state, so `HasLabel()` won't work here. Use `DidAddLabel()` instead, which matches events where a label was added: ```py # Find events that added a specific label From e6e6561ba27d185f20828628b62331f70cafbac2 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 10:24:19 +0200 Subject: [PATCH 41/53] docs: Fix old reference to Appearance page It was folded into the user README --- docs/SUMMARY.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 6749e34c4..075b258e6 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -10,7 +10,6 @@ - [Query Syntax](user/investigate/query-syntax.md) - [Manage](user/manage.md) - [Operate](user/operate.md) - - [Appearance](user/appearance.md) --- From eefc9b924225ad568af7aaa940c31189bf504a9b Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 10:37:36 +0200 Subject: [PATCH 42/53] docs: rewrite Welcome as an onboarding page Open with what Osprey is and a screenshot instead of contributing boilerplate; route readers by role (analyst, rule author, developer); note how docs versioning works. Follows the style from Coop. --- docs/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index dd0ae1e59..2dac6e303 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,16 @@ # Welcome -Welcome to the Osprey documentation! Visit [Osprey on GitHub](https://github.com/roostorg/osprey#readme) for source code and project information. These docs are split into a few guides, depending on who you are and what you're looking for: +Osprey is an open source rules engine and investigation console for trust and safety teams: your platform streams events to it, your rules evaluate each one in real time, and your analysts query, chart, and act on the results. Originally built at Discord and running in production at Bluesky, Osprey is now developed in the open by [ROOST](https://roost.tools) and the community; visit [Osprey on GitHub](https://github.com/roostorg/osprey#readme) for source code and project information. -- [User Guide](user/): learn about Osprey, its functionality, and the user interface -- [Development Guide](development/): get Osprey running and learn how to navigate the code +[![The Query page with an SML filter for post-creation events and its history of past queries, beside a timeseries chart showing matching event volume in fifteen-minute buckets](images/query-and-charts.png)](images/query-and-charts.png) -See additional topics for reference: +The fastest way to get a feel for Osprey is the one-command demo in [Getting Started](development/), which brings up the full stack on live sample data in a few minutes. From there, these docs are split into a few guides, depending on who you are and what you're looking for: -- [Writing Rules](rules.md) -- [User Research & Personas](research-personas.md) +- [User Guide](user/): learn the investigation UI—querying events, labeling entities, and running bulk jobs. If terms like events, features, and labels are new to you, start with [Concepts](concepts.md). +- [Writing Rules](rules.md): learn SML, Osprey's rule language, and wire rules to real effects. +- [Development Guide](development/): run a local development environment, get data in and out of Osprey, and extend it with plugins. + +Note that the docs are versioned: `latest` tracks development on `main`, and other versions can be found at the [documentation site index](https://roostorg.github.io/osprey/). ## Contributing From c05f176b1ea43129e09c57bfcb8f70a320c491ef Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 11:08:31 +0200 Subject: [PATCH 43/53] docs: rework Welcome ordering a tiny bit --- docs/README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 2dac6e303..cabc72f23 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,16 +2,20 @@ Osprey is an open source rules engine and investigation console for trust and safety teams: your platform streams events to it, your rules evaluate each one in real time, and your analysts query, chart, and act on the results. Originally built at Discord and running in production at Bluesky, Osprey is now developed in the open by [ROOST](https://roost.tools) and the community; visit [Osprey on GitHub](https://github.com/roostorg/osprey#readme) for source code and project information. -[![The Query page with an SML filter for post-creation events and its history of past queries, beside a timeseries chart showing matching event volume in fifteen-minute buckets](images/query-and-charts.png)](images/query-and-charts.png) +These docs are split into a few guides, depending on who you are and what you're looking for: -The fastest way to get a feel for Osprey is the one-command demo in [Getting Started](development/), which brings up the full stack on live sample data in a few minutes. From there, these docs are split into a few guides, depending on who you are and what you're looking for: +- [User Guide](user/): learn the investigation UI—querying events, labeling entities, and running bulk jobs. -- [User Guide](user/): learn the investigation UI—querying events, labeling entities, and running bulk jobs. If terms like events, features, and labels are new to you, start with [Concepts](concepts.md). -- [Writing Rules](rules.md): learn SML, Osprey's rule language, and wire rules to real effects. - [Development Guide](development/): run a local development environment, get data in and out of Osprey, and extend it with plugins. +- [Concepts](concepts.md): learn about the basic concepts and terminology used in Osprey and these docs. + Note that the docs are versioned: `latest` tracks development on `main`, and other versions can be found at the [documentation site index](https://roostorg.github.io/osprey/). +[![The Query page with an SML filter for post-creation events and its history of past queries, beside a timeseries chart showing matching event volume in fifteen-minute buckets](images/query-and-charts.png)](images/query-and-charts.png) + +The fastest way to get a feel for Osprey is the one-command demo in [Getting Started](development/), which brings up the full stack on live sample data in a few minutes. + ## Contributing Osprey is an open source project from [ROOST](https://roost.tools) and the community. We welcome contributions and benefit from diverse perspectives and expertise in building safer online spaces. From bac8ba033ad5ef2c34d66734f2fabab42f1c61c5 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 11:38:41 +0200 Subject: [PATCH 44/53] docs: fix relative links broken by the Concepts move Concepts moved from user/ to the docs root, but links in and to it still assumed the old location; also fix a typo'd #effects anchor. --- docs/concepts.md | 12 ++++++------ docs/user/README.md | 2 +- docs/user/investigate/query-syntax.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/concepts.md b/docs/concepts.md index 3ddeeac31..b41d3e00c 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -25,11 +25,11 @@ In our example, `UserId` would be declared as an entity: the same user posts man ## Rules -A **rule** is a named condition over features, written in Osprey's rule language, SML. Rules are evaluated against every event as it arrives. On their own, rules just return a boolean true/false (which is itself queryable, like any feature). Rules can be wired to [effects](#effets). +A **rule** is a named condition over features, written in Osprey's rule language, SML. Rules are evaluated against every event as it arrives. On their own, rules just return a boolean true/false (which is itself queryable, like any feature). Rules can be wired to [effects](#effects). In our example, a rule might cover "the event is a post creation and the post text contains 'hello'." -Rules live as code in your Osprey deployment alongside the Osprey code itself. See [Writing Rules](../rules.md) for more detail. +Rules live as code in your Osprey deployment alongside the Osprey code itself. See [Writing Rules](rules/) for more detail. ## Effects @@ -41,7 +41,7 @@ When the demo's `ContainsHello` rule matches our "hello world" post, it fires tw A **label** is a tag on an entity that persists across events—Osprey's memory. Rules add and remove labels as an effect, and can also _check_ labels, so past decisions inform future ones: "flag this post if its author was previously labeled a spammer." You can also add or remove labels by hand from the UI, one entity at a time or in bulk. -Labels have a name, the entity types they apply to, and a connotation (positive or negative). See [Labels](investigate/labels.md) for more information about how they're used for investigations. +Labels have a name, the entity types they apply to, and a connotation (positive or negative). See [Labels](user/investigate/labels.md) for more information about how they're used for investigations. ## Verdicts @@ -49,10 +49,10 @@ A **verdict** is Osprey's answer when a caller is waiting for one. Most deployme ## Results, and where they surface -Every processed event produces an execution result: the extracted features, the rules that matched, and the effects that fired. Results are indexed for querying—that's what the whole [Investigate](investigate/) side of the UI reads. The query bar filters results by feature (`ContainsHello == True`), charts aggregate them over time, Top N groups them by any feature, and the Event Stream shows them one by one as they happen. Clicking any entity opens its details: current labels and its history on your platform. +Every processed event produces an execution result: the extracted features, the rules that matched, and the effects that fired. Results are indexed for querying—that's what the whole [Investigate](user/investigate/) side of the UI reads. The query bar filters results by feature (`ContainsHello == True`), charts aggregate them over time, Top N groups them by any feature, and the Event Stream shows them one by one as they happen. Clicking any entity opens its details: current labels and its history on your platform. -How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](../data-flow.md) page covers it. +How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](data-flow.md) page covers it. ## Where to go from here -Try these ideas out on live sample data by running the [demo](../development/), then head to [Investigate](investigate/) to learn the query workflow, or [Writing Rules](../rules.md) when you're ready to automate a decision of your own. +Try these ideas out on live sample data by running the [demo](development/), then head to [Investigate](user/investigate/) to learn the query workflow, or [Writing Rules](rules/) when you're ready to automate a decision of your own. diff --git a/docs/user/README.md b/docs/user/README.md index 2bb4ab167..4dcc376cd 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -1,6 +1,6 @@ # User Guide -Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. If terms like events, features, and labels are new to you, start with [Concepts](concepts.md). +Osprey is a web-based investigation and management console for safety teams. Query event data in real time, visualize trends, label entities, manage rules and features, and run bulk operations. If terms like events, features, and labels are new to you, start with [Concepts](../concepts.md). [![The Osprey UI during an investigation: a query filtering events where SuspiciousDisplayName is True, timeseries charts of matching event volume, and the live event stream with one entity's label popup open showing a negative spam_display_name label](../images/osprey-suspicious-display-name.png)](../images/osprey-suspicious-display-name.png) diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index 138ea2179..260ac6c79 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -2,7 +2,7 @@ Osprey uses SML (“Some Madeup Language,” a subset of Python with additional restrictions) for queries. Queries filter events by matching against features, actions, and labels. -Queries match against **features**—the named values your rules extract from each event—including **entities** and **labels**. If those terms are new, read [Concepts](../concepts.md) first. +Queries match against **features**—the named values your rules extract from each event—including **entities** and **labels**. If those terms are new, read [Concepts](../../concepts.md) first. ## Basic comparisons From 2249881601ee752316aa32cbc5a21ba6b4a282d0 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 11:39:18 +0200 Subject: [PATCH 45/53] docs: split Writing Rules into its own section rules.md was over a quarter of all doc text: tutorial, reference, rationale, and a worked example in one page. Move it to rules/ with Examples and Why a Bespoke Language? as subpages, slim the UDF section to the rule-author view (naming the new 1.1 stdlib UDFs, with the in-app UDF Registry as the authoritative list), and link Python implementation topics to Integrations & Plugins instead of duplicating them. Redirect the old rules.html URL. The Examples page walks the demo ruleset file by file and adds labels-as-state and multi-signal patterns. Fixes #382 --- docs/SUMMARY.md | 4 +- docs/book.toml | 1 + docs/data-flow.md | 2 +- docs/development/README.md | 2 +- docs/development/integrations.md | 6 +- docs/development/local.md | 2 +- docs/rules.md | 505 ------------------------------- docs/rules/README.md | 320 ++++++++++++++++++++ docs/rules/examples.md | 218 +++++++++++++ docs/rules/why-sml.md | 13 + docs/user/investigate/labels.md | 2 +- 11 files changed, 562 insertions(+), 513 deletions(-) delete mode 100644 docs/rules.md create mode 100644 docs/rules/README.md create mode 100644 docs/rules/examples.md create mode 100644 docs/rules/why-sml.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 075b258e6..9a71e1fe0 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -29,7 +29,9 @@ - [Concepts](concepts.md) - [Data Flow](data-flow.md) -- [Writing Rules](rules.md) +- [Writing Rules](rules/README.md) + - [Examples](rules/examples.md) + - [Why a Bespoke Language?](rules/why-sml.md) - [User Research & Personas](research-personas.md) --- diff --git a/docs/book.toml b/docs/book.toml index 0ceb5d60f..cd228a403 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -21,3 +21,4 @@ enable = false [output.html.redirect] "/integrations.html" = "development/integrations.html" +"/rules.html" = "rules/index.html" diff --git a/docs/data-flow.md b/docs/data-flow.md index a27df985b..bc76fb6e7 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -26,7 +26,7 @@ At its core, Osprey is a pipeline: events come in, get evaluated against your SM Druid + UI API → Osprey UI ``` -For the worker's rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules.md). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. +For the worker's rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules/). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. ## Getting data in diff --git a/docs/development/README.md b/docs/development/README.md index ee31d5f8d..d1986f016 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -37,7 +37,7 @@ The demo ruleset has one rule, `ContainsHello`: any post containing the word "he - **Group with Top N.** The pre-filled query groups by `UserId`, showing which synthetic users have been banned the most. - **Open the Rules Visualizer** from the navigation bar to see the dependency graph between the demo's features and rule. -The [User Guide](../user/) covers the full investigation workflow, and [Writing Rules](../rules.md) explains the SML behind `ContainsHello` and how to write your own rules. +The [User Guide](../user/) covers the full investigation workflow, and [Writing Rules](../rules/) explains the SML behind `ContainsHello` and how to write your own rules. ## Stopping the demo diff --git a/docs/development/integrations.md b/docs/development/integrations.md index 209d72da5..468cfad4b 100644 --- a/docs/development/integrations.md +++ b/docs/development/integrations.md @@ -25,7 +25,7 @@ Each entry point resolves to a module that contains hook functions decorated wit ## Writing UDFs -A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic such as text matching, DNS lookups, hash comparisons, or ML inference and make it available under a named function in the rules language. See [Writing Rules § User Defined Functions](../rules.md#user-defined-functions-udfs) for the language-level view. +A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic such as text matching, DNS lookups, hash comparisons, or ML inference and make it available under a named function in the rules language. See [Writing Rules § User Defined Functions](../rules/README.md#user-defined-functions-udfs) for the language-level view. ### Anatomy of a UDF @@ -298,7 +298,7 @@ MySpamClassifier(text=MessageContent) > 0.85 Osprey constructs one UDF instance per call site when the rules are compiled, not per event, so the model isn't reloaded for every action processed. Keep in mind this means per _call site_, not per _class_: if you call the same UDF from multiple rules, each call site gets its own instance, and each one loads its own copy of the model. **For a large model, prefer calling the UDF from a single rule (or share the loaded weights via a module-level cache) rather than invoking it from many places.** -For a model served remotely, the same pattern applies with `execute()` calling out over HTTP, gRPC, or your model server's SDK (you bring the client code). Because remote model calls are often slow or costly, gate them so they only run when relevant, using [Writing Rules' `Require(..., require_if=...)` pattern](../rules.md): +For a model served remotely, the same pattern applies with `execute()` calling out over HTTP, gRPC, or your model server's SDK (you bring the client code). Because remote model calls are often slow or costly, gate them so they only run when relevant, using [Writing Rules' `Require(..., require_if=...)` pattern](../rules/README.md#workflow-structure-and-file-placement): ```python Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register') @@ -327,4 +327,4 @@ register_plugins = "register_plugins" Install it into the same environment as Osprey and it will be discovered automatically on the next startup. -See also: [Writing Rules](../rules.md) +See also: [Writing Rules](../rules/) diff --git a/docs/development/local.md b/docs/development/local.md index 0689c30b7..51fec703a 100644 --- a/docs/development/local.md +++ b/docs/development/local.md @@ -141,7 +141,7 @@ Rules are written in SML, some examples are provided in `example_rules/` with YA OSPREY_RULES_PATH=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink ``` -For more about rules, see [Writing Rules](../rules.md). +For more about rules, see [Writing Rules](../rules/). ## Test Data diff --git a/docs/rules.md b/docs/rules.md deleted file mode 100644 index b8ece4a6f..000000000 --- a/docs/rules.md +++ /dev/null @@ -1,505 +0,0 @@ -# Osprey Rules - -![images/rules_architecture.png](images/rules_architecture.png) - -## Creating Rules - -Osprey rules are written in SML (“Some Madeup Language”) which is a subset of Python with additional restrictions to simplify rule writing. You may write rules that are specific to -single event types on a network, or ones that are applied to multiple event types. - -By themselves, rules only create variables; without a corresponding `WhenRules()` function call, the rule will have no effects outside of evaluation and query functionality. - -Rules currently support the following concepts through the `Rule(...)` function of the same name: - -- **Name** - - `Rule_Name = Rule(...)` - - The name of the rule also functions as a conventional "RuleId" and the name of the bool that can be used to query individual rule hits in the Osprey Query UI. As a result, changing the name of a rule after activation may affect historical query results in the UI if not logged externally. - -- **Logic** - - `when_all=[]` - - The actual logic that will be used to evaluate Osprey rules is all encompassed as single comma-delimited list of signals within the `when_all` parameter of the `Rule(...)` function and supports the use of Labels, Plugins, UDFs and other values to help enrich heuristics. - - At present, when evaluating UDFs or abstracted variables, any `NULL` evaluations in the series will cause the entire rule function to evaluate as `NULL`, which may be undesirable. - -- **Description** - - `description=f''` - - There is an additional string description field that is able to be emitted alongside the rule itself to external systems such as logging and ticketing systems to help enrich work-streams that may benefit from plain-language context on what the rule criteria is and what the rule may intend to do. - - It may be helpful to include dynamic variables as well to help enrich operational workflows that may need to identify specific values related to the trigger criteria. - - -Here's an example of a simple rule using various signal evaluations and out-of-the-box UDFs: - -```python -My_Rule_Name_v2 = Rule( - when_all=[ - # Primary Signal - MyFirstValue == True, - HasLabel(entity=MyEntityName, label='MyLabel'), - ListLength(list=UsersValues) == 5, - # Secondary Signal - RegexMatch(target=MyStringValue, pattern='(hello|world)'), - MySecondValue >= 3, - MyThirdValue != Null, - # Guardrail Signal - (_LocalValue in [1, 2, 3, 5]) or (GlobalValue in ['hello', 'howdy']), - not HasLabel(entity=MySecondEntityName, label='MySecondLabel'), - ], - description=f"{UserA} performed {ActionB} in this way. Emit warning", -) -``` - -## Why a bespoke language instead of plain Python? - -It's a fair question: why introduce a bespoke language at all, instead of writing rules in plain Python? The short answer is that SML's restrictions are load-bearing: they're what make Osprey's rule engine safe to run untrusted, third-party-authored logic in production, and fast enough to do it in real time. - -- **The grammar is deliberately small.** The SML parser (`osprey_worker/src/osprey/engine/ast/py_ast.py`) only understands a restricted set of Python AST nodes: assignments, calls, comparisons, boolean/binary operators, literals, and f-strings. There's no `for`/`while`, no `def`/`class`, no arbitrary `import`. Anything outside that set raises a syntax error explaining that "the Osprey rule language is a simplified subset of python." This isn't an accident of an unfinished parser; it rules out unbounded loops and recursion by construction, so a rule can't hang or DoS the worker. -- **Every name is assign-once.** The `UniqueStoredNames` validator rejects re-declaring the same name anywhere in the ruleset, and `NoUnusedLocals` rejects declaring a name that's never read. Combined, every rule and feature compiles down to a single, static, acyclic dependency graph (`execution_graph.py`) rather than an imperative script with mutable state. -- **That static graph is what makes the engine fast and introspectable.** Because nothing can have side effects or run in a loop, the executor can safely evaluate independent branches of the graph in parallel using gevent greenlets (`executor.py`); the UI can also render the exact dependency graph for any rule or feature, which powers the [Rules Visualizer](user/manage.md#rules-visualizer) and the cross-referencing in the [Rules](user/manage.md#rules-registry) and [Features Registry](user/manage.md#features-registry) pages. Plain Python doesn't give you that for free; you'd have to reconstruct it by statically analyzing arbitrary code, which is a much harder problem. -- **Types are checked before a rule ever runs against live traffic.** `validate_static_types.py` and `validate_call_rvalue.py` catch type errors and misused return values at validation/push time, not as a runtime `AttributeError` in production the first time a rare code path executes. - -None of that is free, and it's worth being honest about the cost. **There's no dedicated SML syntax highlighting or language server today.** Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable (if imperfect) highlighting as a stopgap, but proper tooling is a known gap rather than a solved problem. See [IDE Setup](development/ide.md) for current editor setup. - -## Rule Structuring - -You will likely find it useful to maintain two subdirectories inside of your main rules directory - a `rules` directory where actual logic will be added and a `models` directory for defining the various features that occur in any or specific event types. For example, your structure may look something like this: - -```bash -example-rules/ -| rules/ -| | record/ -| | | post/ -| | | | first_post_link.sml -| | | | index.sml -| | | like/ -| | | | like_own_post.sml -| | | | index.sml -| | account/ -| | | signup/ -| | | | high_risk_signup.sml -| | | | index.sml -| | index.sml -| models/ -| | record/ -| | | post.sml -| | | like.sml -| | account/ -| | | signup.sml -| main.sml -``` - -The `main.sml` file at the root of your rules directory serves as the entrypoint. It uses `Import` and `Require` statements to control which other files are loaded and when, allowing you to compose together logic across the project. This sort of structure lets you define rules and models that are specific to certain event types so that only the necessary rules are run for various event types. For example, you likely have some rules that should only be run on a `post` event, since only a `post` will have features like `text` or `mention_count`. - -Inside of each directory, you may maintain an `index.sml` file that will define the conditional logic in which the rules inside that directory are actually included for execution. Although you could handle all of this conditional logic inside of a single file, maintaining separate `index.sml`s per directory greatly helps with neat organization. See [Workflow Structure and File Placement](#workflow-structure-and-file-placement) for more on `Import` and `Require`. - -## Models - -Before you actually write a rule, you'll need to define a "model" for an event type. For this example, we will assume that you run a social media website that lets users create posts, either at the "top level" or as a reply to another top level post. Each post may include text, mentions of other users on your network, and an optional link embed in the post. Let's say that the event's JSON structure looks like this: - -```json -{ - "eventType": "userPost", - "user": { - "userId": "user_id_789", - "handle": "carol", - "postCount": 3, - "accountAgeSeconds": 9002 - }, - "postId": "abc123xyz", - "replyId": null, - "text": "Is anyone online right now? @alice or @bob, you there? If so check this video out", - "mentionIds": ["user_id_123", "user_id_456"], - "embedLink": "https://youtube.com/watch?id=1" -} -``` - -Inside of our `models/record` directory, we should now create a `post.sml` file where we will define the features for a post. - -```python -PostId: Entity[str] = EntityJson( - type='PostId', - path='$.postId', -) - -PostText: str = JsonData( - path='$.text', -) - -MentionIds: List[str] = JsonData( - path='$.mentionIds', -) - -EmbedLink: Optional[str] = JsonData( - path='$.embedLink', - required=False, -) - -ReplyId: Entity[str] = EntityJson( - type='PostId', - path='$.replyId', - required=False, -) -``` - -The [`JsonData` UDF](#user-defined-functions-udfs) lets us take the event's JSON and define features based on the contents of that JSON. These features can then be referenced in other rules that we import the `models/record/post.sml` model into. If you have any values inside your JSON object that may not always be present, you can set `required` to `False`, and these features will be `None` whenever the feature is not present. - -Note that we did not actually create any features for things like `userId` or `handle`. That is because these values will be present in *any* event. It wouldn't be very nice to have to copy these features into each event type's model. Therefore, we will actually create a `base.sml` model that defines these features which are always present. Inside of `models/base.sml`, let's define these. - -```python -EventType = JsonData( - path='$.eventType', -) - -UserId: Entity[str] = EntityJson( - type='UserId', - path='$.user.userId', -) - -Handle: Entity[str] = EntityJson( - type='Handle', - path='$.user.handle', -) - -PostCount: int = JsonData( - path='$.user.postCount', -) - -AccountAgeSeconds: int = JsonData( - path='$.user.accountAgeSeconds', -) -``` - -Here, instead of simply using `JsonData`, we instead use the `EntityJson` UDF for the `UserID`. This is covered in the [UDFs section](#user-defined-functions-udfs), but as a rule of thumb, you likely will want to have values for things like a user's ID set to be entities. This will help more later, such as when doing data explorations within the Osprey UI. - -### Model Hierarchy - -In practice, you may find it useful to create a hierarchy of base models: - -- `base.sml` for features present in every event (user IDs, handles, account stats, etc.) -- `account_base.sml` for features that appear only in account related events, but always appear in each account related event. Similarly, you may add one like `record_base.sml` for those features which appear in all record events. - -This type of hierarchy prevents duplication (which Osprey does not allow) and ensures features are defined at the appropriate level of abstraction. - -## Effects with WhenRules - -The `WhenRules()` function allows for creating effects that trigger external services, create declarations, or modify internal labels by listing `Rule` objects in sequence within the -`rules_any` parameter of `WhenRules()`. By default, operators and designers may utilize UDFs with predefined effects such as `DeclareVerdict()`, `LabelAdd()`, or `LabelRemove()` upon -positive rule evaluation. - -Below is an example of the use of a WhenRules() block to verify and email and reject a request. - -```python -WhenRules( - rules_any=[ - Enabled_Rule_1, - Enabled_Rule_2, - # Disabled_Rule_1, - ], - then=[ - # Verdicts - DeclareVerdict(verdict='reject'), - # Labels - LabelAdd(entity=UserId, label='recently_challenged', expires_after=TimeDelta(days=7)), - LabelAdd(entity=UserId, label='verify', apply_if=NotVerified), - LabelAdd(entity=Email, label='pending_verify'), - LabelAdd(entity=Domain, label='recently_seen', expires_after=TimeDelta(days=7)), - ], -) -``` - -`WhenRules()` must be placed after rule declaration within a file, and it may become difficult to interpret outcomes of rules that are too distributed. Therefore, it may be beneficial -to place any effects toward the bottom of workflows. - -## Output Sinks - -After all rules are evaluated for an input event, a set of output sinks takes the resulting `ExecutionResult` and performs additional work based on that data. These may be defined -as part of a plugin for performing domain specific work. - -Some default use cases include a `StdoutOutputSink` which simply outputs the result to the log, a `KafkaOutputSink` which pipes data to Kafka (used for Osprey UI), or the -`LabelOutputSink` which can add some stateful data to be used in future rules executions. - -```python -class StdoutOutputSink(BaseOutputSink): - """An output sink that prints to standard out!""" - - def __init__(self, log_sampler: Optional[DynamicLogSampler] = None): - pass - - def will_do_work(self, result: ExecutionResult) -> bool: - return True - - def push(self, result: ExecutionResult) -> None: - print(f'result: {result.extracted_features_json} {result.verdicts}') - - def stop(self) -> None: - pass -``` - -Passing data to these output sinks is standardized through the use of `Effects`, which are outputs of some functions, usually UDFs. - -```python -def push(self, result: ExecutionResult) -> None: - users_to_ban = result.effects[BanUserEffect] - ban_users(users_to_ban) -``` - -## User Defined Functions (UDFs) - -User Defined Functions (UDFs) are plugins written in Python that enable users of Osprey to extend and customize their use of the Osprey SML. UDFs are implemented as Python functions and are registered -as a plugin. They extend the `UDFBase` abstract base class with a set of arguments and an output. These will be executed whenever called in SML. - -```python -# example_plugins/src/udfs/text_contains.py -class TextContainsArguments(ArgumentsBase): - text: str - phrase: str - case_sensitive = False - -class TextContains(UDFBase[TextContainsArguments, bool]): - def execute(self, execution_context: ExecutionContext, arguments: TextContainsArguments) -> bool: - escaped = re.escape(arguments.phrase) - pattern = rf'\b{escaped}\b' - flags = 0 if arguments.case_sensitive else re.IGNORECASE - regex = re.compile(pattern, flags) - return bool(regex.search(arguments.text)) - -# example_plugins/src/register_plugins.py -@hookimpl_osprey -def register_udfs(): - return [TextContains] -``` - -Usage in SML: - -```python -# example_rules/rules/post_contains_hello.sml -ContainsHello = Rule( - when_all=[ - EventType == 'create_post', - TextContains(text=PostText, phrase='hello'), - ], - description='Post contains the word "hello"', -) -``` - -### Effect UDFs - -Plugins may also define external effects, which are useful for performing functionality in your primary service. Effects are simply passed to output sinks at the end of a rule run. -These UDFs have an output that extends `EffectBase`, and can be called as a result of a `WhenRules`. - -```python -# example_plugins/src/udfs/ban_user.py -class BanUser(UDFBase[BanUserArguments, BanUserEffect]): - category = UdfCategories.ENGINE - - def execute(self, execution_context: ExecutionContext, arguments: BanUserArguments) -> BanUserEffect: - return BanUserEffect( - entity=arguments.entity, - comment=arguments.comment, - ) - -# example_rules/rules/post_contains_hello.sml -WhenRules( - rules_any=[ContainsHello], - then=[BanUser(entity=UserId, comment='User said "hello"')], -) -``` - -UDF outputs can also implement the `CustomExtractedFeature` interface - which get persisted in the outputs for the UI. `EffectToCustomExtractedFeatureBase` can also be used when effects need additional processing for use in the UI. - -## Labels - -Labels are a standard plugin that enable stateful rules, and touch many parts of Osprey. They are effectively tags on various entities, which may be arbitrarily defined. For more about labels, see [User Guide → Investigate → Labels](user/investigate/labels.md). - -### Creating Entities - -Labels are applied to Entities, which are dynamically interpreted from outputs of the UDF `EntityJson`, usually applied to pieces of data that are generally consistent across actions such as User ID or email. - -```python -# user.sml -UserId: Entity[str] = EntityJson( - type='User', - path='$.user_id' -) -``` - -It is possible to create new UDFs that also create entities by having the output of UDF set to `EntityT`. - -### Adding Labels - -Labels may be added in a `WhenRules()` clause. This will cause the labels output sink to tag the given entity with the given label at the end of the rules run. - -```python -WhenRules( - rules_any=[ - Sent_Too_Many_DMs, - ], - then=[ - LabelAdd(entity=UserId, label='likely_spammer') - ], -) -``` - -### Using Labels - -Since labels may be retrieved during a rule run, they can be effectively used as state for your rules. - -```python -Should_Warn_User_Of_Spammer = Rule( - when_all=[ - HasLabel(entity=UserId, label='likely_spammer'), - This_Is_A_New_DM, - ], -) -``` - -Labels will also be shown in the UI for entities, and can also be set manually. Note that since the UI only searches across actions, `HasLabel()` will not work in the Query UI. -Instead, you may use `DidAddLabel`, which will be true when the given action added a label to a specific entity. - -```python -# UI Query -DidAddLabel(entity_type="UserId", label_name="likely_spammer") -``` - -## Notable Gotchas - -### Nulls - -Nulls are the case where a rule or variable in SML does not exist. This can occur for many reasons - either a piece of data is missing or a rule didn't run. Unlike many programming languages, generally rules with null valued variables will not evaluate that rule (and thus, downstream rules will not evaluate either). The exception cases are when nulls are explicitly checked in a rule. For example: - -```python -Thing: int = JsonData(path='$.property_that_doesnt_exist') - -# Evaluates to False -MyFirstRule = Rule(when_all=[ - Thing != Null, -]) - -# Skips evaluation and sets to Null -MySecondRule = Rule(when_all=[ - Thing > 1, -]) - -# Skips evaluation and sets to Null -MyThirdRule = Rule(when_all=[ - MySecondRule, -]) -``` - -### Workflow Structure and File Placement - -SML files can be composed to make your rules easier to understand. The `Import` statement allows you to include rules and variables found in other files. - -```python -# models/action_name.sml -ActionName = "foo" - -# main.sml -Import( - rules=[ - 'models/action_name.sml', - 'models/http_request.sml', - ] -) - -MyRule = Rule(when_all=[ActionName == "foo"]) -``` - -`Require` allows you to selectively run other SML scripts. Requires supports templating and conditionals, allowing scripts to be filtered out if necessary. This is important in situations where some rules or UDFs are particularly expensive to run (such as making a call to an AI service, for example). - -```python -# main.sml -Require(rule=f'actions/{ActionName}.sml') # will execute 'actions/foo.sml' - -Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == "register") -``` - -## Full Example - -The following is a complete walkthrough of writing a rule using the project structure described above. The goal is to flag accounts whose first post mentions at least one user and includes a link. - -### Writing the Rule - -We'll create `rules/record/post/first_post_link.sml` for the rule logic. This file defines both the conditions that cause the rule to evaluate to `True` and the actions to take when it does. - -```python -# First, import the models that you will need inside of this rule -Import( - rules=[ - 'models/base.sml', - 'models/record/post.sml', - ], -) - -# Next, define a variable that uses the `Rule` UDF -FirstPostLinkRule = Rule( - # Set the conditions in which this rule will be `True` - when_all=[ - PostCount == 1, # if this is the user's first post - EmbedLink != None, # if there is a link inside of the post - ListLength(list=MentionIds) >= 1, # if there is at least one mention in the post - ], - description='First post for user includes a link embed', -) - -# Finally, set which effect UDFs will be triggered -WhenRules( - rules_any=[FirstPostLinkRule], - then=[ - # This is a custom effect UDF that we have implemented - ReportRecord( - entity=PostId, - comment='This was the first post by a user and included a link', - severity=3, - ), - ], -) -``` - -### Wiring Up the Rule - -We want this rule to run *only* when the event is a post event. Using the project structure described above, this involves three files. - -First, `main.sml` at the project root includes a single `Require` statement pointing to the top-level rules index: - -```python -Require( - rule='rules/index.sml', -) -``` - -Next, `rules/index.sml` conditionally requires the post rules when the event type matches: - -```python -Import( - rules=[ - 'models/base.sml', - ], -) - -Require( - rule='rules/record/post/index.sml', - require_if=EventType == 'userPost', -) -``` - -Finally, `rules/record/post/index.sml` requires the new rule: - -```python -Import( - rules=[ - 'models/base.sml', - 'models/record/post.sml', - ], -) - -Require( - rule='rules/record/post/first_post_link.sml', -) -``` diff --git a/docs/rules/README.md b/docs/rules/README.md new file mode 100644 index 000000000..0edd6a971 --- /dev/null +++ b/docs/rules/README.md @@ -0,0 +1,320 @@ +# Writing Rules + +Rules are how you teach Osprey what to look for and what to do when it finds it. This page covers SML ("Some Madeup Language"), Osprey's rule language: defining features with models, writing rules over them, and wiring rules to effects. + +See also: + - [Examples](examples.md) to apply all the concepts to complete, runnable rulesets + - [Why a Bespoke Language?](why-sml.md) for more about SML and why it exists + +![A user's action flows from your production service into Osprey as an event, where rules call UDFs and produce execution output; output sinks return verdicts to your service—the user gets banned—and send features to the Osprey UI](../images/rules_architecture.png) + +## Creating Rules + +Osprey rules are written in SML, a subset of Python with additional restrictions to simplify rule writing. You may write rules that are specific to single event types on a network, or ones that are applied to multiple event types. + +By themselves, rules only create variables; without a corresponding `WhenRules()` function call, the rule will have no effects outside of evaluation and query functionality. + +Rules currently support the following concepts through the `Rule(...)` function of the same name: + +- **Name** + + `Rule_Name = Rule(...)` + + The name of the rule also functions as a conventional "RuleId" and the name of the bool that can be used to query individual rule hits in the Osprey Query UI. As a result, changing the name of a rule after activation may affect historical query results in the UI if not logged externally. + +- **Logic** + + `when_all=[]` + + The actual logic that will be used to evaluate Osprey rules is all encompassed as single comma-delimited list of signals within the `when_all` parameter of the `Rule(...)` function and supports the use of Labels, Plugins, UDFs and other values to help enrich heuristics. + + At present, when evaluating UDFs or abstracted variables, any `NULL` evaluations in the series will cause the entire rule function to evaluate as `NULL`, which may be undesirable. + +- **Description** + + `description=f''` + + There is an additional string description field that is able to be emitted alongside the rule itself to external systems such as logging and ticketing systems to help enrich work-streams that may benefit from plain-language context on what the rule criteria is and what the rule may intend to do. + + It may be helpful to include dynamic variables as well to help enrich operational workflows that may need to identify specific values related to the trigger criteria. + + +Here's an example of a simple rule using various signal evaluations and out-of-the-box UDFs: + +```python +My_Rule_Name_v2 = Rule( + when_all=[ + # Primary Signal + MyFirstValue == True, + HasLabel(entity=MyEntityName, label='MyLabel'), + ListLength(list=UsersValues) == 5, + # Secondary Signal + RegexMatch(target=MyStringValue, pattern='(hello|world)'), + MySecondValue >= 3, + MyThirdValue != Null, + # Guardrail Signal + (_LocalValue in [1, 2, 3, 5]) or (GlobalValue in ['hello', 'howdy']), + not HasLabel(entity=MySecondEntityName, label='MySecondLabel'), + ], + description=f"{UserA} performed {ActionB} in this way. Emit warning", +) +``` + +## Rule Structuring + +You will likely find it useful to maintain two subdirectories inside of your main rules directory: a `rules` directory where actual logic will be added, and a `models` directory for defining the various features that occur in any or specific event types. For example, your structure may look something like this: + +```bash +example-rules/ +| rules/ +| | record/ +| | | post/ +| | | | first_post_link.sml +| | | | index.sml +| | | like/ +| | | | like_own_post.sml +| | | | index.sml +| | account/ +| | | signup/ +| | | | high_risk_signup.sml +| | | | index.sml +| | index.sml +| models/ +| | record/ +| | | post.sml +| | | like.sml +| | account/ +| | | signup.sml +| main.sml +``` + +The `main.sml` file at the root of your rules directory serves as the entry point. It uses `Import` and `Require` statements to control which other files are loaded and when, allowing you to compose together logic across the project. This sort of structure lets you define rules and models that are specific to certain event types so that only the necessary rules are run for various event types. For example, you likely have some rules that should only be run on a `post` event, since only a `post` will have features like `text` or `mention_count`. + +Inside of each directory, you may maintain an `index.sml` file that will define the conditional logic in which the rules inside that directory are actually included for execution. Although you could handle all of this conditional logic inside of a single file, maintaining separate `index.sml`s per directory greatly helps with neat organization. + +See [Workflow Structure and File Placement](#workflow-structure-and-file-placement) for more on `Import` and `Require`. + +## Models + +Before you actually write a rule, you'll need to define a "model" for an event type. For this example, we will assume that you run a social media website that lets users create posts, either at the "top level" or as a reply to another top level post. Each post may include text, mentions of other users on your network, and an optional link embed in the post. Let's say that the event's JSON structure looks like this: + +```json +{ + "eventType": "userPost", + "user": { + "userId": "user_id_789", + "handle": "carol", + "postCount": 3, + "accountAgeSeconds": 9002 + }, + "postId": "abc123xyz", + "replyId": null, + "text": "Is anyone online right now? @alice or @bob, you there? If so check this video out", + "mentionIds": ["user_id_123", "user_id_456"], + "embedLink": "https://youtube.com/watch?id=1" +} +``` + +Inside of our `models/record` directory, we should now create a `post.sml` file where we will define the features for a post. + +```python +PostId: Entity[str] = EntityJson( + type='PostId', + path='$.postId', +) + +PostText: str = JsonData( + path='$.text', +) + +MentionIds: List[str] = JsonData( + path='$.mentionIds', +) + +EmbedLink: Optional[str] = JsonData( + path='$.embedLink', + required=False, +) + +ReplyId: Entity[str] = EntityJson( + type='PostId', + path='$.replyId', + required=False, +) +``` + +The [`JsonData` UDF](#user-defined-functions-udfs) lets us take the event's JSON and define features based on the contents of that JSON. These features can then be referenced in other rules that we import the `models/record/post.sml` model into. If you have any values inside your JSON object that may not always be present, you can set `required` to `False`, and these features will be `None` whenever the feature is not present. + +Note that we did not actually create any features for things like `userId` or `handle`. That is because these values will be present in *any* event. It wouldn't be very nice to have to copy these features into each event type's model. Therefore, we will actually create a `base.sml` model that defines these features which are always present. Inside of `models/base.sml`, let's define these. + +```python +EventType = JsonData( + path='$.eventType', +) + +UserId: Entity[str] = EntityJson( + type='UserId', + path='$.user.userId', +) + +Handle: Entity[str] = EntityJson( + type='Handle', + path='$.user.handle', +) + +PostCount: int = JsonData( + path='$.user.postCount', +) + +AccountAgeSeconds: int = JsonData( + path='$.user.accountAgeSeconds', +) +``` + +Here, instead of simply using `JsonData`, we instead use the `EntityJson` UDF for the `UserID`. This is covered in the [UDFs section](#user-defined-functions-udfs), but as a rule of thumb, you likely will want to have values for things like a user's ID set to be entities. This will help more later, such as when doing data explorations within the Osprey UI. + +### Model Hierarchy + +In practice, you may find it useful to create a hierarchy of base models: + +- `base.sml` for features present in every event (user IDs, handles, account stats, etc.) +- `account_base.sml` for features that appear only in account related events, but always appear in each account related event. Similarly, you may add one like `record_base.sml` for those features which appear in all record events. + +This type of hierarchy prevents duplication (which Osprey does not allow) and ensures features are defined at the appropriate level of abstraction. + +## Effects with WhenRules + +The `WhenRules()` function allows for creating effects that trigger external services, create declarations, or modify internal labels by listing `Rule` objects in sequence within the `rules_any` parameter of `WhenRules()`. By default, operators and designers may utilize UDFs with predefined effects such as `DeclareVerdict()`, `LabelAdd()`, or `LabelRemove()` upon positive rule evaluation. + +Below is an example of the use of a WhenRules() block to verify and email and reject a request. + +```python +WhenRules( + rules_any=[ + Enabled_Rule_1, + Enabled_Rule_2, + # Disabled_Rule_1, + ], + then=[ + # Verdicts + DeclareVerdict(verdict='reject'), + # Labels + LabelAdd(entity=UserId, label='recently_challenged', expires_after=TimeDelta(days=7)), + LabelAdd(entity=UserId, label='verify', apply_if=NotVerified), + LabelAdd(entity=Email, label='pending_verify'), + LabelAdd(entity=Domain, label='recently_seen', expires_after=TimeDelta(days=7)), + ], +) +``` + +`WhenRules()` must be placed after rule declaration within a file, and it may become difficult to interpret outcomes of rules that are too distributed. Therefore, it may be beneficial to place any effects toward the bottom of workflows. + +After evaluation, effects and the rest of the execution result are handed to your deployment's output sinks. [Data Flow](../data-flow.md) covers where results go from there, and [Integrations & Plugins](../development/integrations.md#configuring-output-sinks) covers adding output sinks of your own. + +## User Defined Functions (UDFs) + +Nearly every function on this page—`Rule`, `JsonData`, `EntityJson`—is a UDF: a function implemented in Python and made available to SML. Osprey ships a standard library of them, and your deployment's developers can register custom ones through plugins; writing a UDF in Python is covered in [Integrations & Plugins § Writing UDFs](../development/integrations.md#writing-udfs). + +The authoritative list of what's callable in _your_ deployment—with signatures, descriptions, and categories—is the in-app [UDF Registry](../user/manage.md#udf-registry). The standard library includes general-purpose helpers like `RegexMatch`, `ListLength`, `ParseInt` (numeric string to integer), `StringSlice` (substring by index range), and the `Hash*` family, plus engine functions like `GetActionName()` and `GetActionId()` for the name and ID of the event being processed, and `Experiment`/`ExperimentWhen` for bucketing entities so a rule can roll out to a slice of traffic. + +Custom UDFs are called from SML like any other function. The demo ruleset's one rule is built on a custom `TextContains` UDF: + +```python +# example_rules/rules/post_contains_hello.sml +ContainsHello = Rule( + when_all=[ + EventType == 'create_post', + TextContains(text=PostText, phrase='hello'), + ], + description='Post contains the word "hello"', +) +``` + +### Effect UDFs + +Some UDFs don't return a value to compare against; they produce an **effect**, a structured output that Osprey's output sinks act on after evaluation—banning a user, reporting a post. Effect UDFs are called in the `then=` list of a `WhenRules()` block: + +```python +# example_rules/rules/post_contains_hello.sml +WhenRules( + rules_any=[ContainsHello], + then=[BanUser(entity=UserId, comment='User said "hello"')], +) +``` + +Implementing an effect UDF (and the output sink that consumes it) is plugin work; see [Integrations & Plugins](../development/integrations.md#udfs-with-side-effects). + +## Labels + +Labels are a standard plugin that enable stateful rules, and touch many parts of Osprey. They are effectively tags on various entities, which may be arbitrarily defined. Rules can add and remove labels as effects and check them as conditions, so past decisions inform future ones; [Examples § Labels as state](examples.md#labels-as-state) walks through that pattern end to end. For how labels behave in the UI, see [User Guide → Investigate → Labels](../user/investigate/labels.md). + +### Creating Entities + +Labels are applied to Entities, which are dynamically interpreted from outputs of the UDF `EntityJson`, usually applied to pieces of data that are generally consistent across events such as User ID or email. + +```python +# user.sml +UserId: Entity[str] = EntityJson( + type='User', + path='$.user_id' +) +``` + +It is possible to create new UDFs that also create entities by having the output of UDF set to `EntityT`. + +## Notable Gotchas + +### Nulls + +Nulls are the case where a rule or variable in SML does not exist. This can occur for many reasons - either a piece of data is missing or a rule didn't run. Unlike many programming languages, generally rules with null valued variables will not evaluate that rule (and thus, downstream rules will not evaluate either). The exception cases are when nulls are explicitly checked in a rule. For example: + +```python +Thing: int = JsonData(path='$.property_that_doesnt_exist') + +# Evaluates to False +MyFirstRule = Rule(when_all=[ + Thing != Null, +]) + +# Skips evaluation and sets to Null +MySecondRule = Rule(when_all=[ + Thing > 1, +]) + +# Skips evaluation and sets to Null +MyThirdRule = Rule(when_all=[ + MySecondRule, +]) +``` + +### Workflow Structure and File Placement + +SML files can be composed to make your rules easier to understand. The `Import` statement allows you to include rules and variables found in other files. + +```python +# models/action_name.sml +ActionName = "foo" + +# main.sml +Import( + rules=[ + 'models/action_name.sml', + 'models/http_request.sml', + ] +) + +MyRule = Rule(when_all=[ActionName == "foo"]) +``` + +`Require` allows you to selectively run other SML scripts. Requires supports templating and conditionals, allowing scripts to be filtered out if necessary. This is important in situations where some rules or UDFs are particularly expensive to run (such as making a call to an AI service, for example). + +```python +# main.sml +Require(rule=f'actions/{ActionName}.sml') # will execute 'actions/foo.sml' + +Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == "register") +``` + +--- + +See [Examples](examples.md) to put these concepts together into complete rulesets you can run and adapt. diff --git a/docs/rules/examples.md b/docs/rules/examples.md new file mode 100644 index 000000000..f6b5ff25f --- /dev/null +++ b/docs/rules/examples.md @@ -0,0 +1,218 @@ +# Examples + +Everything [Writing Rules](./) covers, applied to complete rulesets: first the demo's real, runnable ruleset file by file, then two patterns you'll reach for as your own ruleset grows—labels as state, and a multi-signal rule organized across files. The examples use the same small social network as the rest of these docs. + +## The demo ruleset, file by file + +The [demo](../development/) runs the actual ruleset in [`example_rules/`](https://github.com/roostorg/osprey/tree/main/example_rules), which is small enough to read in one sitting: two models, one rule, and one label. The platform being moderated has a single (admittedly strict) policy: users may never say "hello." + +Events arrive as JSON like this—an event name (`action_name`), an ID, and whatever the platform sent: + +```json +{ + "action_id": 1, + "action_name": "create_post", + "data": { + "user_id": "user_1923", + "event_type": "create_post", + "post": { "text": "hello world" } + } +} +``` + +**`main.sml`** is the entrypoint. This ruleset is small enough to skip conditional `index.sml` files entirely: import the base model, require the one rule file. + +```python +Import(rules=['models/base.sml']) + +Require(rule='rules/post_contains_hello.sml') +``` + +**`models/base.sml`** defines the features present on every event. `UserId` and `EventType` are declared as entities, so Osprey tracks them as _things_ that persist across events—that's what lets a label stick to a user later. `coerce_type=True` (the default) converts a mismatched value, like a numeric user ID, to the declared type instead of erroring. + +```python +UserId: Entity[str] = EntityJson( + type='User', + path='$.user_id', + coerce_type=True +) + +EventType: Entity[str] = EntityJson( + type='EventType', + path='$.event_type', + coerce_type=True +) + +ActionName=GetActionName() + +ActionId=GetActionId() +``` + +The last two lines pull the event's name and ID from Osprey itself rather than the JSON payload: `GetActionName()` and `GetActionId()` are stdlib UDFs, and exposing them as features makes them queryable in the UI. + +**`models/post.sml`** adds the one feature specific to posts: + +```python +PostText: Entity[str] = EntityJson( + type='PostText', + path='$.post.text', + coerce_type=True +) +``` + +**`rules/post_contains_hello.sml`** imports both models, defines the rule, and wires it to effects—the whole loop in one file: + +```python +Import( + rules=[ + 'models/base.sml', + 'models/post.sml', + ] +) + +ContainsHello = Rule( + when_all=[ + EventType == 'create_post', + TextContains(text=PostText, phrase='hello') + ], + description='Post contains the word "hello"', +) + +WhenRules( + rules_any=[ContainsHello], + then=[ + BanUser(entity=UserId, comment='User said "hello"'), + LabelAdd(entity=UserId, label='meow'), + ], +) +``` + +`TextContains` and `BanUser` aren't stdlib—they're custom UDFs from [`example_plugins/`](https://github.com/roostorg/osprey/tree/main/example_plugins), which is the reference for [writing your own](../development/integrations.md#writing-udfs). `LabelAdd` is stdlib. + +**`config/labels.yaml`** declares the label the rule applies: which entity types it's valid for, and its connotation. + +```yaml +labels: + meow: + valid_for: [User] + connotation: positive + description: testing label +``` + +**What you'll see in the UI** once events flow: each processed post appears in the Event Stream with `UserId`, `EventType`, `PostText`, and `ContainsHello` among its extracted features; querying `ContainsHello == True` filters to the posts that matched; and clicking through to a matched post's author shows the `meow` label on their `User` entity, with the `BanUser` effect recorded on the event. [Getting Started](../development/) walks through this UI tour on live demo data. + +## Labels as state + +Rules can't see previous events directly, but labels persist on entities across events—so a label added by one rule becomes a condition another rule checks later. Say you've flagged users who send too many DMs: + +```python +WhenRules( + rules_any=[ + Sent_Too_Many_DMs, + ], + then=[ + LabelAdd(entity=UserId, label='likely_spammer') + ], +) +``` + +From then on, _every_ event by that user carries the state, and other rules can act on it—here, on a different event type entirely: + +```python +Should_Warn_User_Of_Spammer = Rule( + when_all=[ + HasLabel(entity=UserId, label='likely_spammer'), + This_Is_A_New_DM, + ], +) +``` + +Labels also show on the entity in the UI, where they can be added and removed by hand. One asymmetry to know about: `HasLabel()` works in rules but not in the query bar, because queries search events rather than current entity state. To find events where a label was applied, query `DidAddLabel(entity_type="UserId", label_name="likely_spammer")` instead; see [Query Syntax](../user/investigate/query-syntax.md) for details. + +## A multi-signal rule, organized across files + +The following is a complete walkthrough of writing a rule using the project structure described in [Writing Rules § Rule Structuring](./#rule-structuring). The goal is to flag accounts whose first post mentions at least one user and includes a link—three signals that are individually innocent but suspicious together. + +### Writing the rule + +We'll create `rules/record/post/first_post_link.sml` for the rule logic. This file defines both the conditions that cause the rule to evaluate to `True` and the actions to take when it does. + +```python +# First, import the models that you will need inside of this rule +Import( + rules=[ + 'models/base.sml', + 'models/record/post.sml', + ], +) + +# Next, define a variable that uses the `Rule` UDF +FirstPostLinkRule = Rule( + # Set the conditions in which this rule will be `True` + when_all=[ + PostCount == 1, # if this is the user's first post + EmbedLink != None, # if there is a link inside of the post + ListLength(list=MentionIds) >= 1, # if there is at least one mention in the post + ], + description='First post for user includes a link embed', +) + +# Finally, set which effect UDFs will be triggered +WhenRules( + rules_any=[FirstPostLinkRule], + then=[ + # This is a custom effect UDF that we have implemented + ReportRecord( + entity=PostId, + comment='This was the first post by a user and included a link', + severity=3, + ), + ], +) +``` + +### Wiring up the rule + +We want this rule to run _only_ when the event is a post event. Using the project structure described above, this involves three files. + +First, `main.sml` at the project root includes a single `Require` statement pointing to the top-level rules index: + +```python +Require( + rule='rules/index.sml', +) +``` + +Next, `rules/index.sml` conditionally requires the post rules when the event type matches: + +```python +Import( + rules=[ + 'models/base.sml', + ], +) + +Require( + rule='rules/record/post/index.sml', + require_if=EventType == 'userPost', +) +``` + +Finally, `rules/record/post/index.sml` requires the new rule: + +```python +Import( + rules=[ + 'models/base.sml', + 'models/record/post.sml', + ], +) + +Require( + rule='rules/record/post/first_post_link.sml', +) +``` + +--- + +From here, try modifying the demo ruleset while it's running—add a phrase to `TextContains`, or a second rule over `PostText`—or head to [Integrations & Plugins](../development/integrations.md) when a pattern needs a UDF that doesn't exist yet. diff --git a/docs/rules/why-sml.md b/docs/rules/why-sml.md new file mode 100644 index 000000000..54339882f --- /dev/null +++ b/docs/rules/why-sml.md @@ -0,0 +1,13 @@ +# Why a Bespoke Language? + +Why does Osprey use the bespoke SML ("Some Madeup Language") instead of supporting plain Python? The short answer is that SML's restrictions are what make Osprey's rule engine safe to run untrusted, third-party-authored logic in production—and fast enough to do it in real time. + +- **The grammar is intentionally small.** The SML parser (`osprey_worker/src/osprey/engine/ast/py_ast.py`) only understands a restricted set of Python AST nodes: assignments, calls, comparisons, boolean/binary operators, literals, and f-strings. Anything outside that set raises a syntax error; i.e. there's no `for`/`while`, `def`/`class`, or arbitrary `import`. This helps rule out unbounded loops and recursion by construction, meaning a rule can't hang or DoS the worker. + +- **Every name is assign-once.** The `UniqueStoredNames` validator rejects re-declaring the same name anywhere in the ruleset, and `NoUnusedLocals` rejects declaring a name that's never read. Combined, every rule and feature compiles down to a single, static, acyclic dependency graph (`execution_graph.py`) rather than an imperative script with mutable state. + +- **The static graph makes the engine fast and introspectable.** Because nothing can have side effects or run in a loop, the executor can safely evaluate independent branches of the graph in parallel using gevent greenlets (`executor.py`); the UI can also render the exact dependency graph for any rule or feature, which powers the [Rules Visualizer](../user/manage.md#rules-visualizer) and the cross-referencing in the [Rules](../user/manage.md#rules-registry) and [Features Registry](../user/manage.md#features-registry) pages. + +- **Types are checked before rules run.** `validate_static_types.py` and `validate_call_rvalue.py` catch type errors and misused return values at validation/push time, not as a runtime `AttributeError` in production the first time a rare code path executes. + +Since SML is a legal subset of Python syntax, pointing your editor at Python-mode gets you reasonable syntax highlighting. See [IDE Setup](../development/ide.md) for current editor setup. diff --git a/docs/user/investigate/labels.md b/docs/user/investigate/labels.md index 574e27645..b9330aded 100644 --- a/docs/user/investigate/labels.md +++ b/docs/user/investigate/labels.md @@ -34,4 +34,4 @@ From the event stream, select any entity to open its label drawer. ![Complete Label](../../images/complete-label.png) -For more about how labels are used, see [Writing Rules → Labels](../../rules.md#labels). +For more about how labels are used, see [Writing Rules → Labels](../../rules/README.md#labels). From 8d887f8a6de438ead7c4825549dbf0b969917a54 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 11:39:34 +0200 Subject: [PATCH 46/53] docs: give transparent images a solid backdrop Screenshots with transparency were hard to read against the dark theme. Reuse the frame color as a background-color, and add the required colorspace to color-mix() so both declarations actually apply; browsers drop color-mix() without it. --- docs/theme/css/content.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/theme/css/content.css b/docs/theme/css/content.css index 632f61975..ad0973e97 100644 --- a/docs/theme/css/content.css +++ b/docs/theme/css/content.css @@ -1,7 +1,8 @@ /* Content styling overrides */ .content img { - border: 0.5em solid color-mix(var(--bg) 80%, white); + background-color: color-mix(in srgb, var(--bg) 80%, white); + border: 0.5em solid color-mix(in srgb, var(--bg) 80%, white); box-shadow: 0 0.25em 0.5em 0.25em rgba(0 0 0 / 0.1 ); box-sizing: border-box; margin-block: 0.5em; From e829bc83d837336a1629261a00e5bfe96d9ad111 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 11:51:56 +0200 Subject: [PATCH 47/53] docs: split adopter-facing pages into an Integration Guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data Flow and Integrations & Plugins are for people connecting Osprey to their platform, not people changing Osprey itself—the same audience split Coop's docs draw. Give them their own guide with a short landing page, and retarget the /integrations.html redirect. --- docs/README.md | 6 ++++-- docs/SUMMARY.md | 8 ++++++-- docs/book.toml | 2 +- docs/concepts.md | 2 +- docs/development/local.md | 2 +- docs/integration/README.md | 8 ++++++++ docs/{ => integration}/data-flow.md | 6 +++--- docs/{development => integration}/integrations.md | 6 +++--- docs/rules/README.md | 6 +++--- docs/rules/examples.md | 4 ++-- 10 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 docs/integration/README.md rename docs/{ => integration}/data-flow.md (84%) rename docs/{development => integration}/integrations.md (97%) diff --git a/docs/README.md b/docs/README.md index cabc72f23..104276339 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,11 +6,13 @@ These docs are split into a few guides, depending on who you are and what you're - [User Guide](user/): learn the investigation UI—querying events, labeling entities, and running bulk jobs. -- [Development Guide](development/): run a local development environment, get data in and out of Osprey, and extend it with plugins. +- [Development Guide](development/): run a local development environment and change Osprey itself. + +- [Integration Guide](integration/): connect Osprey to your platform—get data in and out, and extend it with plugins. - [Concepts](concepts.md): learn about the basic concepts and terminology used in Osprey and these docs. -Note that the docs are versioned: `latest` tracks development on `main`, and other versions can be found at the [documentation site index](https://roostorg.github.io/osprey/). +Note that the docs are versioned; other versions can be found at the [documentation site index](https://roostorg.github.io/osprey/). [![The Query page with an SML filter for post-creation events and its history of past queries, beside a timeseries chart showing matching event volume in fifteen-minute buckets](images/query-and-charts.png)](images/query-and-charts.png) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 9a71e1fe0..153353759 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -18,17 +18,21 @@ - [Workflow](development/workflow.md) - [Development Tools](development/tools.md) - [CLI Reference](development/cli-reference.md) - - [Integrations & Plugins](development/integrations.md) - [Troubleshooting](development/troubleshooting.md) - [IDE Setup](development/ide.md) - [Release Process](development/releases.md) --- +- [Integration Guide](integration/README.md) + - [Data Flow](integration/data-flow.md) + - [Integrations & Plugins](integration/integrations.md) + +--- + # Concepts & More - [Concepts](concepts.md) -- [Data Flow](data-flow.md) - [Writing Rules](rules/README.md) - [Examples](rules/examples.md) - [Why a Bespoke Language?](rules/why-sml.md) diff --git a/docs/book.toml b/docs/book.toml index cd228a403..202fd41ff 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -20,5 +20,5 @@ site-url = "/osprey/" enable = false [output.html.redirect] -"/integrations.html" = "development/integrations.html" +"/integrations.html" = "integration/integrations.html" "/rules.html" = "rules/index.html" diff --git a/docs/concepts.md b/docs/concepts.md index b41d3e00c..6d3632d67 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -51,7 +51,7 @@ A **verdict** is Osprey's answer when a caller is waiting for one. Most deployme Every processed event produces an execution result: the extracted features, the rules that matched, and the effects that fired. Results are indexed for querying—that's what the whole [Investigate](user/investigate/) side of the UI reads. The query bar filters results by feature (`ContainsHello == True`), charts aggregate them over time, Top N groups them by any feature, and the Event Stream shows them one by one as they happen. Clicking any entity opens its details: current labels and its history on your platform. -How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](data-flow.md) page covers it. +How results get from the engine to those views (and into your own systems) is plumbing your developers control; the [Data Flow](integration/data-flow.md) page covers it. ## Where to go from here diff --git a/docs/development/local.md b/docs/development/local.md index 51fec703a..205547807 100644 --- a/docs/development/local.md +++ b/docs/development/local.md @@ -100,7 +100,7 @@ The UI will automatically connect to the backend services running in Docker cont ## Plugins -In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](integrations.md). +In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](../integration/integrations.md). ```python @hookimpl_osprey diff --git a/docs/integration/README.md b/docs/integration/README.md new file mode 100644 index 000000000..f09278f72 --- /dev/null +++ b/docs/integration/README.md @@ -0,0 +1,8 @@ +# Integration Guide + +Osprey is designed to plug into your platform rather than replace it: your systems send events in, your rules decide, and your systems act on what comes out. This guide covers that seam. + +- [Data Flow](data-flow.md): the conceptual picture—how events get into Osprey, and how results and effects get back out +- [Integrations & Plugins](integrations.md): the how-to—writing UDFs, input and output sinks, a labels service, and ML model integration through the plugin system + +If you're still evaluating Osprey, start with the one-command demo in [Getting Started](../development/); if you want to change Osprey itself rather than connect to it, see the [Development Guide](../development/). diff --git a/docs/data-flow.md b/docs/integration/data-flow.md similarity index 84% rename from docs/data-flow.md rename to docs/integration/data-flow.md index bc76fb6e7..a727eb94c 100644 --- a/docs/data-flow.md +++ b/docs/integration/data-flow.md @@ -26,7 +26,7 @@ At its core, Osprey is a pipeline: events come in, get evaluated against your SM Druid + UI API → Osprey UI ``` -For the worker's rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](rules/). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. +For the worker's rule-evaluation architecture (how a single event is scored against your rules), see the diagram at the top of [Writing Rules](../rules/). For the coordinator's internals (priority queues, bidirectional streaming vs. synchronous API), see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator), which has a more detailed component diagram and a working example. ## Getting data in @@ -64,7 +64,7 @@ If your events don't come from Kafka/PubSub/the coordinator, or don't arrive as - **`register_action_proto_deserializer`**: convert your own protobuf `Action` message into the JSON dict shape the engine expects. - **`register_input_stream`**: swap in an entirely custom input source (a different queue system, a database poller, whatever you have). -Both are `pluggy` hooks, registered the same way as UDFs and output sinks. See [Integrations & Plugins](development/integrations.md) for the plugin mechanics and `example_plugins/src/register_plugins.py` for a working example. +Both are `pluggy` hooks, registered the same way as UDFs and output sinks. See [Integrations & Plugins](integrations.md) for the plugin mechanics and `example_plugins/src/register_plugins.py` for a working example. ## Getting data out @@ -94,4 +94,4 @@ Separately, `ExecutionResultStore` backends persist full execution results for l ### How results become queryable in the UI -Druid consumes the worker's Kafka output and powers the UI API's real-time querying; this is what backs the [Investigate](user/investigate/) query interface and the [Rules](user/manage.md#rules-registry)/[Features Registries](user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. +Druid consumes the worker's Kafka output and powers the UI API's real-time querying; this is what backs the [Investigate](../user/investigate/) query interface and the [Rules](../user/manage.md#rules-registry)/[Features Registries](../user/manage.md#features-registry). If you're running without Kafka output enabled, Druid has nothing to index and the query UI will be empty even though rules are still evaluating correctly. diff --git a/docs/development/integrations.md b/docs/integration/integrations.md similarity index 97% rename from docs/development/integrations.md rename to docs/integration/integrations.md index 468cfad4b..bf20fa65a 100644 --- a/docs/development/integrations.md +++ b/docs/integration/integrations.md @@ -1,6 +1,6 @@ # Integrations & Plugins -Osprey is designed to be extended without modifying the core codebase; you can wire up your own logic such as detection functions, output destinations, entity state storage, and ML models through plugin packages that Osprey discovers at startup. A plugin package implements any subset of the hooks in the [Available hooks](local.md#available-hooks) table; this page walks through the integration points adopters ask about most. +Osprey is designed to be extended without modifying the core codebase; you can wire up your own logic such as detection functions, output destinations, entity state storage, and ML models through plugin packages that Osprey discovers at startup. A plugin package implements any subset of the hooks in the [Available hooks](../development/local.md#available-hooks) table; this page walks through the integration points adopters ask about most. See the [`example_plugins/` directory](https://github.com/roostorg/osprey/tree/main/example_plugins) for a working reference package. @@ -107,7 +107,7 @@ Inline sets suit small, stable lists. There's no bulk-import or lookup-table pri ## Configuring input sinks -An input sink is where events _enter_ Osprey. Osprey ships with built-in sources (Kafka, Google Pub/Sub, the Osprey Coordinator, and a synthetic generator for local testing) selected via the `InputStreamSource` config value. If none of those fit your platform, you can register a custom input stream as a plugin. For the conceptual picture, see [Data Flow § Getting data in](../data-flow.md#getting-data-in). +An input sink is where events _enter_ Osprey. Osprey ships with built-in sources (Kafka, Google Pub/Sub, the Osprey Coordinator, and a synthetic generator for local testing) selected via the `InputStreamSource` config value. If none of those fit your platform, you can register a custom input stream as a plugin. For the conceptual picture, see [Data Flow § Getting data in](data-flow.md#getting-data-in). ### Built-in sources @@ -164,7 +164,7 @@ def register_input_stream(config): ## Configuring output sinks -An output sink receives every `ExecutionResult` after rule evaluation and decides what to do with it, e.g. log it, forward it to a queue, call a webhook, or write to a database. For the conceptual picture, see [Data Flow § Getting data out](../data-flow.md#getting-data-out); if what you want is to persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options, the `register_execution_result_store` hook covers that instead. +An output sink receives every `ExecutionResult` after rule evaluation and decides what to do with it, e.g. log it, forward it to a queue, call a webhook, or write to a database. For the conceptual picture, see [Data Flow § Getting data out](data-flow.md#getting-data-out); if what you want is to persist results in a backend other than the built-in BigTable/GCS/MinIO/Postgres options, the `register_execution_result_store` hook covers that instead. ### Sync output sink diff --git a/docs/rules/README.md b/docs/rules/README.md index 0edd6a971..d7d8f7cca 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -209,11 +209,11 @@ WhenRules( `WhenRules()` must be placed after rule declaration within a file, and it may become difficult to interpret outcomes of rules that are too distributed. Therefore, it may be beneficial to place any effects toward the bottom of workflows. -After evaluation, effects and the rest of the execution result are handed to your deployment's output sinks. [Data Flow](../data-flow.md) covers where results go from there, and [Integrations & Plugins](../development/integrations.md#configuring-output-sinks) covers adding output sinks of your own. +After evaluation, effects and the rest of the execution result are handed to your deployment's output sinks. [Data Flow](../integration/data-flow.md) covers where results go from there, and [Integrations & Plugins](../integration/integrations.md#configuring-output-sinks) covers adding output sinks of your own. ## User Defined Functions (UDFs) -Nearly every function on this page—`Rule`, `JsonData`, `EntityJson`—is a UDF: a function implemented in Python and made available to SML. Osprey ships a standard library of them, and your deployment's developers can register custom ones through plugins; writing a UDF in Python is covered in [Integrations & Plugins § Writing UDFs](../development/integrations.md#writing-udfs). +Nearly every function on this page—`Rule`, `JsonData`, `EntityJson`—is a UDF: a function implemented in Python and made available to SML. Osprey ships a standard library of them, and your deployment's developers can register custom ones through plugins; writing a UDF in Python is covered in [Integrations & Plugins § Writing UDFs](../integration/integrations.md#writing-udfs). The authoritative list of what's callable in _your_ deployment—with signatures, descriptions, and categories—is the in-app [UDF Registry](../user/manage.md#udf-registry). The standard library includes general-purpose helpers like `RegexMatch`, `ListLength`, `ParseInt` (numeric string to integer), `StringSlice` (substring by index range), and the `Hash*` family, plus engine functions like `GetActionName()` and `GetActionId()` for the name and ID of the event being processed, and `Experiment`/`ExperimentWhen` for bucketing entities so a rule can roll out to a slice of traffic. @@ -242,7 +242,7 @@ WhenRules( ) ``` -Implementing an effect UDF (and the output sink that consumes it) is plugin work; see [Integrations & Plugins](../development/integrations.md#udfs-with-side-effects). +Implementing an effect UDF (and the output sink that consumes it) is plugin work; see [Integrations & Plugins](../integration/integrations.md#udfs-with-side-effects). ## Labels diff --git a/docs/rules/examples.md b/docs/rules/examples.md index f6b5ff25f..0b69b7506 100644 --- a/docs/rules/examples.md +++ b/docs/rules/examples.md @@ -87,7 +87,7 @@ WhenRules( ) ``` -`TextContains` and `BanUser` aren't stdlib—they're custom UDFs from [`example_plugins/`](https://github.com/roostorg/osprey/tree/main/example_plugins), which is the reference for [writing your own](../development/integrations.md#writing-udfs). `LabelAdd` is stdlib. +`TextContains` and `BanUser` aren't stdlib—they're custom UDFs from [`example_plugins/`](https://github.com/roostorg/osprey/tree/main/example_plugins), which is the reference for [writing your own](../integration/integrations.md#writing-udfs). `LabelAdd` is stdlib. **`config/labels.yaml`** declares the label the rule applies: which entity types it's valid for, and its connotation. @@ -215,4 +215,4 @@ Require( --- -From here, try modifying the demo ruleset while it's running—add a phrase to `TextContains`, or a second rule over `PostText`—or head to [Integrations & Plugins](../development/integrations.md) when a pattern needs a UDF that doesn't exist yet. +From here, try modifying the demo ruleset while it's running—add a phrase to `TextContains`, or a second rule over `PostText`—or head to [Integrations & Plugins](../integration/integrations.md) when a pattern needs a UDF that doesn't exist yet. From b991e09424f040489b7aa170bad10fffe53f376d Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 14:50:59 +0200 Subject: [PATCH 48/53] README: consolidate the intro and cut boilerplate The intro had three overlapping "Osprey is..." paragraphs plus a list of vague bullets accumulated over time; merge them into one description of the actual flow from events in to verdicts out. Also give the screenshot a real alt text, Americanize "labelling", and drop boilerplate that doesn't apply (mailing lists) or that the dev guide already covers. --- README.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c26ae382f..2ed28b3ce 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,15 @@ # Osprey -**Automate the obvious and investigate the ambiguous.** Osprey is a safety rules engine for real-time event processing at scale. +**Automate the obvious and investigate the ambiguous.** Osprey is a safety rules engine and investigation console for real-time event processing at scale. -- take action based on user behavior -- combine actions with human written rules -- let operators query human actions and past decisions -- perform investigations or write new rules based on decisions +Your platform streams events to Osprey, and human-written rules evaluate each one as it arrives—taking automatic action, applying labels to the entities involved, and sending verdicts and custom effects to your own systems. Analysts then query and chart the results to spot patterns, investigate, and turn what they find into new rules. Originally developed internally at [Discord](https://discord.com/) to combat spam, abuse, botting, and scripting across its platform, Osprey has been open-sourced to help other platforms facing similar challenges. -Osprey is an event stream decisions engine and analysis UI designed to investigate and take automatic action on events and their properties as they happen in real-time. Originally developed internally at [Discord](https://discord.com/) to combat spam, abuse, botting, and scripting across its platform, Osprey has been open-sourced to help other platforms facing similar challenges. +![The Query page with an SML filter for post-creation events and its history of past queries, beside a timeseries chart showing matching event volume in fifteen-minute buckets](docs/images/query-and-charts.png) -![Osprey UI sample](docs/images/query-and-charts.png) +Rules are written in SML, Osprey's structured rule language, and extended with user-defined functions (UDFs). Osprey tracks state across events by labeling entities when you provide a labels service backend; see [labels_service.py](./example_plugins/src/services/labels_service.py) for a Postgres-backed example. -Osprey is a library for processing actions through human-written rules and outputting verdicts & custom effects back to configurable output sinks. It evaluates events using structured rule logic (SML) that is extendable via user-defined functions (UDFs). Osprey can also track state across events by labelling entities if implementers provide a labels service backend (see [labels_service.py](./example_plugins/src/services/labels_service.py) for a Postgres-backed labels service example). - -Osprey is built for engineers and Trust & Safety teams who want to explore, test, and integrate its core capabilities into their platform for incident response and Trust & Safety investigation. [Read more about user research and personas](docs/research-personas.md). +Osprey is built for engineers and Trust & Safety teams who want to explore, test, and integrate it into their platform for incident response and investigations. [Read more about user research and personas](docs/research-personas.md). ## Try it @@ -44,13 +39,11 @@ Using Osprey and want to add your project/organization to this list? [Open a pul ## Development -- See the [development guide](./docs/development/) for development setup and workflow documentation -- All code changes should pass linting (Ruff) and type checking (MyPy) -- Pre-commit hooks automatically run on each commit to maintain code quality +See the [development guide](./docs/development/) for development setup and workflow documentation, including the linting, type checking, and pre-commit hooks that changes are expected to pass. -## Join Us +## Join us -Writing code is not the only way to help the project. Reviewing pull requests, answering questions to help others on mailing lists or issues, providing feedback from a domain expert perspective, organizing and teaching tutorials, working on the website, improving the documentation, are all priceless contributions. +Writing code is not the only way to help the project. Reviewing pull requests, answering questions in issues and discussions, providing feedback from a domain expert perspective, teaching tutorials, and improving the documentation are all priceless contributions. - Join us in [our Discord server](https://discord.gg/5Csqnw2FSQ) - Join our [newsletter](https://roost.tools/#get-started) for more announcements and information @@ -58,7 +51,7 @@ Writing code is not the only way to help the project. Reviewing pull requests, a _[ROOST](https://roost.tools) (Robust Open Online Safety Tools) is a non-profit organization that brings together expertise, resources, and investments from major technology companies and philanthropies to build scalable, interoperable safety infrastructure for the AI era._ -### Feedback Wanted +### Feedback wanted This is a working system, not a prototype. Try it locally, connect your data, write some rules, and tell us what's missing for your use case. We're particularly interested in: @@ -67,7 +60,7 @@ This is a working system, not a prototype. Try it locally, connect your data, wr - Missing detection capabilities or response actions you need - API improvements that would make adoption easier for your team -Your experimentation feedback will directly shape future priorities and help us build the most useful Trust & Safety tooling for the community. +Your feedback will directly shape future priorities and help us build the most useful Trust & Safety tooling for the community. ## Recognition From 81b0db779b95d83d4e474cc275b092864309a33f Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 14:51:18 +0200 Subject: [PATCH 49/53] docs: fix User Guide claims against the actual interface Verified the user guide against osprey_ui/src and the screenshots: labels have connotations (LabelConnotation), not "polarities"; the Create New Job modal creates jobs from an uploaded file of entities rather than collecting label fields; the Saved Queries menu options and the Rules Visualizer's action/label search now match the UI; and PoP and Precision wording follows the actual buttons. Documents the 100,000-entity default limit on bulk jobs (and its opt-out) from LabelForm. Also replaces every placeholder image alt text ("Osprey Home", "Query Box") with a full description of what each screenshot shows, and trims Query Syntax's opener that repeated the next paragraph. --- docs/concepts.md | 6 ++-- docs/user/investigate/README.md | 51 ++++++++++++++------------- docs/user/investigate/labels.md | 10 +++--- docs/user/investigate/query-syntax.md | 4 +-- docs/user/manage.md | 8 ++--- docs/user/operate.md | 14 ++++---- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/docs/concepts.md b/docs/concepts.md index 6d3632d67..d001a59de 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -2,11 +2,11 @@ Osprey watches a stream of events happening on your platform, runs each one through your rules in real time, and records what it found so analysts can query and act on it. This page defines the terms the rest of these docs (and the UI itself) use. They're listed in the order data flows through Osprey, and later concepts build on earlier ones, so it's worth a read from start to finish. -Throughout the docs are examples of moderating a small social network where users create posts; remember that Osprey is useful for platforms of all kinds, and its concepts may be adapted for any online platform where people interact. +Throughout the docs are examples of moderating a small social network where users create posts; the same concepts adapt to any platform where people interact. ## Events (and actions) -Anything that happens on your platform—e.g. someone registers an account, creates a post, sends a message, or reacts to content—can be sent to Osprey as it happens as an **event**. Each event arrives with a name, like `create_post`, and whatever JSON data your platform sent along, e.g. the author's user ID and the post text "hello world." +Anything that happens on your platform—e.g. someone registers an account, creates a post, sends a message, or reacts to content—can be sent to Osprey as an **event**, as it happens. Each event arrives with a name, like `create_post`, and whatever JSON data your platform sent along, e.g. the author's user ID and the post text "hello world." > [!NOTE] > You may also come across the term **action**: Osprey's submission API and rules engine historically call each incoming event an action, which is why the query bar filters on `ActionName` even though the UI's live feed is the Event Stream. In this context, they're the same: one event in, one `ActionName` recorded. @@ -41,7 +41,7 @@ When the demo's `ContainsHello` rule matches our "hello world" post, it fires tw A **label** is a tag on an entity that persists across events—Osprey's memory. Rules add and remove labels as an effect, and can also _check_ labels, so past decisions inform future ones: "flag this post if its author was previously labeled a spammer." You can also add or remove labels by hand from the UI, one entity at a time or in bulk. -Labels have a name, the entity types they apply to, and a connotation (positive or negative). See [Labels](user/investigate/labels.md) for more information about how they're used for investigations. +Labels have a name, the entity types they apply to, and a connotation (positive, negative, or neutral). See [Labels](user/investigate/labels.md) for more information about how they're used for investigations. ## Verdicts diff --git a/docs/user/investigate/README.md b/docs/user/investigate/README.md index 8f154e2d5..598ad05ae 100644 --- a/docs/user/investigate/README.md +++ b/docs/user/investigate/README.md @@ -6,17 +6,17 @@ The Investigate section is where most day-to-day work happens. It contains the m The Query page is the home page of Osprey. It's a live investigation workspace with three panels. -![Osprey Home](../../images/osprey-home.png) +![The Query page on first load: an empty query input with a Last Day time range on the left above the query history, and empty Timeseries Chart, Top N Results, and Event Stream panels awaiting data](../../images/osprey-home.png) ### Query input The left panel is where you write and run queries. Osprey uses SML syntax—the same language used to write rules—to filter and search event data. See [Query Syntax](query-syntax.md) for a full reference. -![Query Box](../../images/query-box.png) +![The query input with an SML filter for post-creation events by one user, a Last 2 Weeks time range, and a Submit Query button](../../images/query-box.png) As you type, the input offers autocomplete suggestions for feature names, action names, and UDFs. Hovering over any UDF name shows a tooltip with its description. -![Query UDF Hover](../../images/query-udf-hover.png) +![Autocomplete in the query input suggesting the ContainsHello UDF after typing "Contains", with a tooltip describing it as "Post contains the word hello"](../../images/query-udf-hover.png) The active query is reflected in the page URL, making it easy to share a specific investigation with a teammate, though be aware this may expose sensitive query parameters. @@ -24,7 +24,7 @@ The active query is reflected in the page URL, making it easy to share a specifi Every query runs against a time window. You can choose a preset interval—from the last second up to the last three months—or set a custom date range using the date picker. -![Query Time Range](../../images/query-time-range.png) +![The time range dropdown under the query input, open to presets ranging from Last 12 Hours to Last 3 Months](../../images/query-time-range.png) The entire page updates dynamically when the query or time range changes, and interacting with any other panel (charts, event stream) can also update the query in turn. @@ -32,36 +32,36 @@ The entire page updates dynamically when the query or time range changes, and in The center panels show two types of visualizations: -![Charts](../../images/charts.png) +![A Timeseries Chart of daily matching event counts above a Top N Results table grouping events by PostText, with Download CSV, Bulk Label, PoP, Yeet Table, and Precision controls along the table footer](../../images/charts.png) **Timeseries** displays how many matching events occurred over time. You can set the granularity—minute, fifteen minutes, half hour, hour, day, week, or month—and hover over individual bars to see the count for that period. -![Time Series Hover](../../images/hover-time-series.png) +![Hovering over a timeseries bar, showing a tooltip with that bucket's date, time, and event count](../../images/hover-time-series.png) -You can add additional timeseries charts to compare different time granularities side-by-side. Charts you no longer need can be removed with the **Yeet** button. +You can add additional timeseries charts to compare different time granularities side by side. Charts you no longer need can be removed with the **Yeet** button. -![Multiple Time Series](../../images/multiple-time-series.png) +![Two timeseries charts for the same query stacked vertically: daily granularity above, per-minute granularity below, with a Yeet button beside the second chart](../../images/multiple-time-series.png) **Top N** shows a table of the top results for the current query, grouped by a dimension you choose. You can: - Add or remove dimension columns -- Adjust the number of results shown (precision) -- Enable **Period over Period (PoP)** to compare current results against a past time window and see the delta +- Adjust the number of results shown with **Precision** +- Toggle **PoP** (period over period) to compare current results against a past time window and see the delta - Export the table as a CSV - Remove the table with the **Yeet Table** button -![Top N Charts](../../images/top-n-charts.png) +![A Top N Results table grouping matching events by UserId, sorted by count](../../images/top-n-charts.png) -![Period over Period](../../images/pop.png) +![A Top N Results table with PoP enabled, adding delta and percent-change columns comparing each count against the previous period, with values redacted](../../images/pop.png) ### Event stream -The right panel is Osprey's live feed. It shows individual events matching the current query in near-real time, and can also be used to search historical events. +The right panel is Osprey's live feed. It shows individual events matching the current query in near real time, and can also be used to search historical events. -![Event Stream](../../images/event-stream.png) +![The Event Stream filtered to one UserId, showing payment events as cards listing each extracted feature, with sensitive values redacted](../../images/event-stream.png) The stream can be displayed in card format or list format. On first load, each event card shows all of its extracted features (or the summary features configured for that event type, if any), so the stream is useful before you've set anything up. You can customize which fields appear per event with **Select Summary Features**, helpful when different team members care about different metadata. -![Summary Features](../../images/summary-features.png) +![The Set Features modal for the event stream, with a toggle for Set Custom Features and a filterable checklist of features grouped by model and rule](../../images/summary-features.png) Selecting an entity in the event stream (such as a user ID or IP address) opens the [Entity Details](labels.md#entity-details) view. Hold Ctrl (⌘ Cmd on macOS) while clicking to select multiple events for bulk labeling. @@ -71,27 +71,30 @@ Selecting an event opens a detail view at `/events/:eventId` showing all extract Every query you run is automatically saved to your history. -![Query History](../../images/query-history.png) +![A history entry highlighted in the sidebar's History tab, showing its query text and original time range beside the results it produced](../../images/query-history.png) Hovering over a query in the sidebar shows the Top N dimensions that were active during that session. The full Query History page (accessible from the sidebar) shows a searchable list of all queries run across your team. You can filter by user email, view the original query text, and re-run any past query using the same time range it was originally run with. -![Query History Page](../../images/query-history-page.png) +![The Query History page listing each query with its text and time range, buttons to view the original query or re-run it, a filter-by-user dropdown, and a Saved Queries sidebar](../../images/query-history-page.png) ## Saved Queries For queries you return to frequently, Osprey lets you save them by name. -![Save Query](../../images/query-history-save.png) +![The menu on a query history entry, with View Original Query and Save Query options](../../images/query-history-save.png) -The Saved Queries page (accessible from the sidebar) shows a grid of all saved queries with the query text, the user who saved it, and when it was saved. You can filter by user email. +The Saved Queries page (accessible from the sidebar) lists all saved queries with the query text, the user who saved it, and when it was saved. You can filter by user email. -![Saved Queries Page](../../images/saved-queries-page.png) +![The Saved Queries page listing a saved query with its text and metadata, and an open menu of Run Original Query, Run Query Using Interval, Show Saved Query History, and Delete Saved Query options](../../images/saved-queries-page.png) -From the grid, you can: -- **Run** a saved query to load it into the Query page -- **Rename** it via an edit modal -- **Delete** it (with a confirmation step) +From each saved query's menu, you can: +- **Run Original Query** to re-run it over its original time range +- **Run Query Using Interval** to run it over its saved interval (e.g. Last Day) relative to now +- **Show Saved Query History** to see past runs of the query +- **Delete Saved Query**, with a confirmation step + +To rename a saved query, run it and select the edit icon next to its name on the Query page. Saved queries also have a direct URL: `/saved-query/:savedQueryId/latest` automatically loads and executes the query. diff --git a/docs/user/investigate/labels.md b/docs/user/investigate/labels.md index b9330aded..003afc880 100644 --- a/docs/user/investigate/labels.md +++ b/docs/user/investigate/labels.md @@ -2,7 +2,7 @@ Labels are annotations you apply to entities: users, IP addresses, emails, and other tracked objects. They're the bridge between human judgment and Osprey's automated rule system; a label you apply manually can feed into rules that act on future events automatically. -Labels have three polarities: +Labels have one of three connotations: - **Negative**: harmful or problematic (e.g. `spammer`, `bot`, `banned`, `suspicious`) - **Positive**: trusted or verified (e.g. `verified`, `trusted`, `premium_user`) @@ -14,7 +14,7 @@ A reason is required whenever you apply a label. Selecting an entity anywhere in the UI navigates to an entity view showing every label that has ever been applied to that entity, grouped by label name. -![User Entity](../../images/osprey-user-entity.png) +![The entity view for a user, listing their negative labels—including identity_evasion with its description and an automatically added RapidHandleChange entry—beside the query page's charts and event stream](../../images/osprey-user-entity.png) Each label entry shows: - The label value and type @@ -26,12 +26,12 @@ Each label entry shows: From a Top N table, hover over an entity row and select **Edit Labels**: -![Add Labels](../../images/add-labels.png) +![The label popup for a PostText entity in a Top N table, with keyboard hints for adding the entity to the query and an Edit Labels button](../../images/add-labels.png) From the event stream, select any entity to open its label drawer. -![Empty Label](../../images/empty-label.png) +![The label drawer for a user entity, with a label name search, reason and expiration fields, and empty negative, positive, and neutral label sections](../../images/empty-label.png) -![Complete Label](../../images/complete-label.png) +![An entity view with negative labels applied, one expanded to show its description and the rule event that produced it, plus neutral labels below; identifying values are redacted](../../images/complete-label.png) For more about how labels are used, see [Writing Rules → Labels](../../rules/README.md#labels). diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index 260ac6c79..f8c183ec9 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -1,8 +1,6 @@ # Query Syntax -Osprey uses SML (“Some Madeup Language,” a subset of Python with additional restrictions) for queries. Queries filter events by matching against features, actions, and labels. - -Queries match against **features**—the named values your rules extract from each event—including **entities** and **labels**. If those terms are new, read [Concepts](../../concepts.md) first. +Osprey uses SML (“Some Madeup Language,” a subset of Python with additional restrictions) for queries. Queries match against **features**—the named values your rules extract from each event—including **entities** and **labels**. If those terms are new, read [Concepts](../../concepts.md) first. ## Basic comparisons diff --git a/docs/user/manage.md b/docs/user/manage.md index 500e0d319..ab6b92e07 100644 --- a/docs/user/manage.md +++ b/docs/user/manage.md @@ -4,11 +4,11 @@ The Manage section provides visibility into your Osprey configuration: the rules ## Rules Visualizer -The Rules Visualizer shows how labels, rules, and other labels relate to one another in a dependency graph. It's useful for understanding what will fire when a particular label is applied, or what conditions must be true for a label to be produced. +The Rules Visualizer shows how rules and labels relate to one another in a dependency graph. It's useful for understanding what will fire when a particular label is applied, or what conditions must be true for a label to be produced. -![Rules Visualizer](../images/rules-visualizer.png) +![The Rules Visualizer page with its search box open, offering Actions and Labels categories, before anything is selected](../images/rules-visualizer.png) -To use it, select a feature or label from the search interface. A graph appears showing the upstream and downstream relationships for your selection. You can toggle upstream and downstream visibility independently. +To use it, search for an action or label in the search box. A graph appears showing the upstream and downstream relationships for your selection. You can toggle upstream and downstream visibility independently. Node types in the graph: - **Red ellipse**: a label that is upstream of a rule (an input condition) @@ -21,7 +21,7 @@ Hovering over a node shows its source file path. The graph supports zoom and pan The UDF Registry is an auto-generated API reference for every user-defined function (UDF) available in Osprey. It updates dynamically as UDFs are added or modified in code, so it always reflects what's actually available. -![UDF Documentation](../images/udf-documentation.png) +![The UDF Registry listing available functions with type signatures and descriptions, grouped by category](../images/udf-documentation.png) UDFs are organized by category and are searchable. Each entry shows: - Function signature with syntax highlighting diff --git a/docs/user/operate.md b/docs/user/operate.md index 9dc35850c..dea46f1b3 100644 --- a/docs/user/operate.md +++ b/docs/user/operate.md @@ -13,19 +13,21 @@ Bulk Actions let you apply a label to every entity matching the current query; u There are two ways to start a bulk action: -1. **Bulk Actions** → **Create New Job** to open the job creation modal +1. **Bulk Actions** → **Create New Job** to create a job from an uploaded file of entities 2. **From the Query page**: use the bulk label drawer in the chart column, which pre-fills the query from your current session -![Bulk Label](../images/bulk-label.png) +![The Bulk Edit Labels form for roughly 9,800 entities matching a query, with label name, status, reason, and expiration fields](../images/bulk-label.png) Before submitting, Osprey shows a count of how many unique entities will be labeled. Review this number carefully to understand the impact of your bulk action. > For scripted or bulk labeling from the command line (e.g., importing label lists from external sources), see `apply_label` and `bulk_apply_label` in the [CLI Reference](../development/cli-reference.md#osprey-cli-apply_label). -Each job requires: -- The entity type and label to apply +Each labeling job takes: +- The label to apply and its status - A reason (required for all labeling operations) -- The label polarity (negative, positive, or neutral) +- An optional expiration + +Standard bulk jobs are limited to 100,000 entities; the form has a checkbox to remove the limit for larger jobs. ### Monitoring jobs @@ -37,7 +39,7 @@ The page polls for updates automatically; you don't need to refresh to see progr The Bulk Job History page provides a detailed view of past bulk jobs. -![Bulk Job History](../images/bulk-job-history.png) +![The empty Bulk Job History page: a task ID search field on the left and a Find Single Bulk Job panel on the right](../images/bulk-job-history.png) The page has two columns: - Recent bulk jobs with status, progress, and summary statistics; filterable by task ID From 5c5936fc0523dd77885c679ce3b0536ea9f8443d Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 14:51:36 +0200 Subject: [PATCH 50/53] docs: rewrite Writing Rules' stiff legacy passages The Name/Logic/Description blocks and the effects, labels, nulls, and Import/Require sections kept a stiff, formal register inherited from the original rules.md ("all encompassed as single comma-delimited list", "operators and designers may utilize"). Rewrite them in the page's tutorial voice without changing technical meaning, link the Null gotcha from where it first bites, and fix the "verify and email" typo. --- docs/rules/README.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index d7d8f7cca..0db4dea00 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -20,23 +20,21 @@ Rules currently support the following concepts through the `Rule(...)` function `Rule_Name = Rule(...)` - The name of the rule also functions as a conventional "RuleId" and the name of the bool that can be used to query individual rule hits in the Osprey Query UI. As a result, changing the name of a rule after activation may affect historical query results in the UI if not logged externally. + The rule's name doubles as its identifier and as the boolean feature you can query in the Osprey UI to see individual rule hits. Renaming a rule after it's active disconnects it from its historical query results, so name carefully. - **Logic** `when_all=[]` - The actual logic that will be used to evaluate Osprey rules is all encompassed as single comma-delimited list of signals within the `when_all` parameter of the `Rule(...)` function and supports the use of Labels, Plugins, UDFs and other values to help enrich heuristics. + The rule's logic is the list of signals in the `when_all` parameter; a signal can be a comparison over features, a label check, a UDF call, or another rule. - At present, when evaluating UDFs or abstracted variables, any `NULL` evaluations in the series will cause the entire rule function to evaluate as `NULL`, which may be undesirable. + If any signal in the list evaluates to `Null`, the whole rule evaluates to `Null`; see [Nulls](#nulls) below. - **Description** `description=f''` - There is an additional string description field that is able to be emitted alongside the rule itself to external systems such as logging and ticketing systems to help enrich work-streams that may benefit from plain-language context on what the rule criteria is and what the rule may intend to do. - - It may be helpful to include dynamic variables as well to help enrich operational workflows that may need to identify specific values related to the trigger criteria. + A plain-language description of what the rule looks for, emitted alongside the rule to external systems like logging and ticketing. It's an f-string, so you can interpolate feature values to show responders exactly what triggered the rule. Here's an example of a simple rule using various signal evaluations and out-of-the-box UDFs: @@ -145,7 +143,7 @@ ReplyId: Entity[str] = EntityJson( The [`JsonData` UDF](#user-defined-functions-udfs) lets us take the event's JSON and define features based on the contents of that JSON. These features can then be referenced in other rules that we import the `models/record/post.sml` model into. If you have any values inside your JSON object that may not always be present, you can set `required` to `False`, and these features will be `None` whenever the feature is not present. -Note that we did not actually create any features for things like `userId` or `handle`. That is because these values will be present in *any* event. It wouldn't be very nice to have to copy these features into each event type's model. Therefore, we will actually create a `base.sml` model that defines these features which are always present. Inside of `models/base.sml`, let's define these. +We did not create features for values like `userId` or `handle` because they're present in *any* event; copying them into every event type's model would get tedious. Instead, let's define them once in a `base.sml` model. Inside of `models/base.sml`: ```python EventType = JsonData( @@ -171,7 +169,7 @@ AccountAgeSeconds: int = JsonData( ) ``` -Here, instead of simply using `JsonData`, we instead use the `EntityJson` UDF for the `UserID`. This is covered in the [UDFs section](#user-defined-functions-udfs), but as a rule of thumb, you likely will want to have values for things like a user's ID set to be entities. This will help more later, such as when doing data explorations within the Osprey UI. +Here we use the `EntityJson` UDF for `UserId` rather than plain `JsonData`. UDFs are covered [below](#user-defined-functions-udfs), but as a rule of thumb, identifiers like user IDs should be entities; it pays off later when exploring data in the Osprey UI. ### Model Hierarchy @@ -184,9 +182,9 @@ This type of hierarchy prevents duplication (which Osprey does not allow) and en ## Effects with WhenRules -The `WhenRules()` function allows for creating effects that trigger external services, create declarations, or modify internal labels by listing `Rule` objects in sequence within the `rules_any` parameter of `WhenRules()`. By default, operators and designers may utilize UDFs with predefined effects such as `DeclareVerdict()`, `LabelAdd()`, or `LabelRemove()` upon positive rule evaluation. +`WhenRules()` wires rules to effects: list `Rule` objects in its `rules_any` parameter, and when any of them evaluates true, the effects in `then=` fire. Osprey ships effect UDFs like `DeclareVerdict()`, `LabelAdd()`, and `LabelRemove()`; effects can also trigger external services through your output sinks. -Below is an example of the use of a WhenRules() block to verify and email and reject a request. +Below is an example of a `WhenRules()` block that rejects the request and labels the user, email, and domain for verification follow-up: ```python WhenRules( @@ -207,7 +205,7 @@ WhenRules( ) ``` -`WhenRules()` must be placed after rule declaration within a file, and it may become difficult to interpret outcomes of rules that are too distributed. Therefore, it may be beneficial to place any effects toward the bottom of workflows. +`WhenRules()` must appear after the rules it references. Effects scattered throughout a file are hard to trace, so keep them together toward the bottom. After evaluation, effects and the rest of the execution result are handed to your deployment's output sinks. [Data Flow](../integration/data-flow.md) covers where results go from there, and [Integrations & Plugins](../integration/integrations.md#configuring-output-sinks) covers adding output sinks of your own. @@ -250,7 +248,7 @@ Labels are a standard plugin that enable stateful rules, and touch many parts of ### Creating Entities -Labels are applied to Entities, which are dynamically interpreted from outputs of the UDF `EntityJson`, usually applied to pieces of data that are generally consistent across events such as User ID or email. +Labels are applied to entities: features created with the `EntityJson` UDF, usually for values that stay consistent across events, like a user ID or email address. ```python # user.sml @@ -260,13 +258,13 @@ UserId: Entity[str] = EntityJson( ) ``` -It is possible to create new UDFs that also create entities by having the output of UDF set to `EntityT`. +A custom UDF can also create entities by declaring `EntityT` as its output type. ## Notable Gotchas ### Nulls -Nulls are the case where a rule or variable in SML does not exist. This can occur for many reasons - either a piece of data is missing or a rule didn't run. Unlike many programming languages, generally rules with null valued variables will not evaluate that rule (and thus, downstream rules will not evaluate either). The exception cases are when nulls are explicitly checked in a rule. For example: +A rule or variable in SML is `Null` when it doesn't exist; a piece of data was missing, or a rule didn't run. Unlike in many programming languages, a rule with a `Null` signal is skipped entirely (and so are rules downstream of it) unless the rule checks for `Null` explicitly. For example: ```python Thing: int = JsonData(path='$.property_that_doesnt_exist') @@ -289,7 +287,7 @@ MyThirdRule = Rule(when_all=[ ### Workflow Structure and File Placement -SML files can be composed to make your rules easier to understand. The `Import` statement allows you to include rules and variables found in other files. +SML files can be composed to make your rules easier to understand. The `Import` statement includes rules and variables from other files. ```python # models/action_name.sml @@ -306,7 +304,7 @@ Import( MyRule = Rule(when_all=[ActionName == "foo"]) ``` -`Require` allows you to selectively run other SML scripts. Requires supports templating and conditionals, allowing scripts to be filtered out if necessary. This is important in situations where some rules or UDFs are particularly expensive to run (such as making a call to an AI service, for example). +`Require` selectively runs other SML scripts. It supports templating and conditionals, so scripts can be skipped entirely; that matters when a rule or UDF is expensive to run, like a call to an AI service. ```python # main.sml From 46578badbf858bef598faefa3154d17c6aa92a05 Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Wed, 15 Jul 2026 14:51:56 +0200 Subject: [PATCH 51/53] docs: mechanical style fixes from the tone sweep Normalize dash spacing, drop "Note that"/"Keep in mind" hedges, align the sidebar Workflow entry with its H1, and fix comma splices and a typo in Local Development and the personas page. Corrects the test-data producer description (it sends create_post events, not user logins, per example_data/template.json) and gives IDE Setup the SML file-association note that Why a Bespoke Language? already points readers to. --- docs/README.md | 2 +- docs/SUMMARY.md | 2 +- docs/development/ide.md | 10 ++++++++++ docs/development/local.md | 12 ++++++------ docs/development/troubleshooting.md | 2 +- docs/development/workflow.md | 2 +- docs/integration/integrations.md | 14 +++++++------- docs/research-personas.md | 6 +++--- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/docs/README.md b/docs/README.md index 104276339..d34f98c82 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ These docs are split into a few guides, depending on who you are and what you're - [Concepts](concepts.md): learn about the basic concepts and terminology used in Osprey and these docs. -Note that the docs are versioned; other versions can be found at the [documentation site index](https://roostorg.github.io/osprey/). +The docs are versioned; other versions are available at the [documentation site index](https://roostorg.github.io/osprey/). [![The Query page with an SML filter for post-creation events and its history of past queries, beside a timeseries chart showing matching event volume in fifteen-minute buckets](images/query-and-charts.png)](images/query-and-charts.png) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 153353759..97d47845b 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -15,7 +15,7 @@ - [Development Guide](development/README.md) - [Local Development](development/local.md) - - [Workflow](development/workflow.md) + - [Development Workflow](development/workflow.md) - [Development Tools](development/tools.md) - [CLI Reference](development/cli-reference.md) - [Troubleshooting](development/troubleshooting.md) diff --git a/docs/development/ide.md b/docs/development/ide.md index 554da48e2..71132e953 100644 --- a/docs/development/ide.md +++ b/docs/development/ide.md @@ -20,3 +20,13 @@ If using VS Code, we recommend you install these extensions for the best develop "python.analysis.typeCheckingMode": "basic" } ``` + +## SML files + +There's no dedicated editor tooling for SML yet, but since SML is a legal subset of Python syntax, associating `.sml` files with Python gets you reasonable highlighting. In VS Code: + +```json +"files.associations": { + "*.sml": "python" +} +``` diff --git a/docs/development/local.md b/docs/development/local.md index 205547807..e8a83e5b2 100644 --- a/docs/development/local.md +++ b/docs/development/local.md @@ -73,14 +73,14 @@ The `test_data` profile includes a producer that generates sample events; withou This starts up many services, including: - **Osprey Worker**: The main engine that processes input events given the rules and UDFs - **Test Data Producer**: The `--profile test_data` sample event generator -- **Osprey UI**: Frontend service that hosts the react code for the web interface and communicates to the UI API +- **Osprey UI**: Frontend service that hosts the React code for the web interface and communicates with the UI API - **Osprey UI API**: Backend service that provides data and functionality to the web interface -- **Kafka** (KRaft mode): Message streaming for user generated events +- **Kafka** (KRaft mode): Message streaming for user-generated events - **Postgres**: A database that the Worker, UI API, and Druid use for various reasons, such as the Postgres-backed Labels Service (in the example plugins) - **Druid**: A database that consumes Osprey Worker outputs to power the UI API for real-time querying - **MinIO**: S3-compatible object storage; the default execution result store in this stack (`OSPREY_EXECUTION_RESULT_STORAGE_BACKEND=minio`) -Alternatively, you can start Osprey with `osprey-coordinator`, refer to the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for more information +Alternatively, you can start Osprey with `osprey-coordinator`; see the [Coordinator README](https://github.com/roostorg/osprey/tree/main/example_docker_compose/run_osprey_with_coordinator) for more information. ### 6. (Optional) Open ports for the UI/UI API @@ -100,7 +100,7 @@ The UI will automatically connect to the backend services running in Docker cont ## Plugins -In Osprey, UDFs and output sinks are designed to be easily portable. This is done through a plugin system based on pluggy. An example plugin package has been provided for reference, see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](../integration/integrations.md). +In Osprey, UDFs and output sinks are designed to be easily portable through a plugin system based on pluggy. An example plugin package is provided for reference; see `example_plugins/src/register_plugins.py`. For a full walkthrough with examples (UDFs, sinks, hash-based lookups, ML models, labels service), see [Integrations & Plugins](../integration/integrations.md). ```python @hookimpl_osprey @@ -135,7 +135,7 @@ Implement any subset of these in your plugin's `register_plugins.py`: ## Rules -Rules are written in SML, some examples are provided in `example_rules/` with YAML config, the rules are mounted to the worker processes when the containers start via environment variables. ex: +Rules are written in SML; examples with YAML config are provided in `example_rules/`. Rules are mounted into the worker processes when the containers start, controlled via environment variables; for example: ```bash OSPREY_RULES_PATH=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink @@ -150,4 +150,4 @@ Generate sample JSON actions: docker compose --profile test_data up osprey-kafka-test-data-producer -d ``` -Produces user login events with timestamps, user IDs, and IP addresses to `osprey.actions_input` topic. +This produces synthetic post-creation events with timestamps, user IDs, and IP addresses to the `osprey.actions_input` topic; it's the same producer the `test_data` profile starts. diff --git a/docs/development/troubleshooting.md b/docs/development/troubleshooting.md index e23c2b576..fdd3b928e 100644 --- a/docs/development/troubleshooting.md +++ b/docs/development/troubleshooting.md @@ -31,7 +31,7 @@ The UI defaults to querying the last 24 hours. If the selected time range is too - Narrow the time range to 1–4 hours centered on when you generated test data - Click the edit icon next to the displayed time range to switch to a custom date/time picker -- Note that Druid's Kafka consumer uses `auto.offset.reset: latest`; it only picks up events produced after `docker compose up` first ran, so events from before that point will not appear regardless of the time range +- Druid's Kafka consumer uses `auto.offset.reset: latest`; it only picks up events produced after `docker compose up` first ran, so events from before that point will not appear regardless of the time range ## Kafka topic disk growth diff --git a/docs/development/workflow.md b/docs/development/workflow.md index c20fe9c5f..ff0b2e99f 100644 --- a/docs/development/workflow.md +++ b/docs/development/workflow.md @@ -6,7 +6,7 @@ Branch from `main` and name your branch `github_username/short-description` (e.g Pre-commit hooks run automatically when you commit; the config is `.pre-commit-config.yaml` at the repo root. The hooks check for filename case conflicts, leftover merge-conflict markers, and forgotten Python debugger calls; validate JSON and TOML; normalize line endings to LF; lint and format Python with Ruff; and type-check with mypy. Changes under `osprey_ui/` also go through Prettier and ESLint. -Hooks stop at the first failure, and several fix files in place — if a hook modifies a file, re-stage it and commit again. +Hooks stop at the first failure, and several fix files in place—if a hook modifies a file, re-stage it and commit again. ## Checking your work before you push diff --git a/docs/integration/integrations.md b/docs/integration/integrations.md index bf20fa65a..a5cdd9100 100644 --- a/docs/integration/integrations.md +++ b/docs/integration/integrations.md @@ -25,7 +25,7 @@ Each entry point resolves to a module that contains hook functions decorated wit ## Writing UDFs -A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic such as text matching, DNS lookups, hash comparisons, or ML inference and make it available under a named function in the rules language. See [Writing Rules § User Defined Functions](../rules/README.md#user-defined-functions-udfs) for the language-level view. +A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic—text matching, DNS lookups, hash comparisons, ML inference—and expose it as a named function in the rules language. See [Writing Rules § User Defined Functions](../rules/README.md#user-defined-functions-udfs) for the language-level view. ### Anatomy of a UDF @@ -181,7 +181,7 @@ class MyOutputSink(BaseOutputSink): return True def push(self, result: ExecutionResult) -> None: - # Do something with the result — send to a queue, call an API, etc. + # Do something with the result—send to a queue, call an API, etc. pass def stop(self) -> None: @@ -243,7 +243,7 @@ from osprey.worker.lib.storage.labels import LabelsServiceBase class PostgresLabelsService(LabelsServiceBase): def initialize(self) -> None: - # Called once at startup — open connections here + # Called once at startup—open connections here ... def read_labels(self, entity) -> EntityLabels: @@ -269,9 +269,9 @@ def register_labels_service_or_provider(config): Osprey doesn't currently support direct integration with a review tool; however, these extension points can help you integrate: -- `register_output_sinks` — push execution results into a review queue as they're produced. -- `register_label_output_sink` — a sink specifically for label mutations, replacing the default `LabelOutputSink`. -- A labels service backed by your existing datastore (previous section) — label an entity "flagged" from a rule, and your review queue queries your own store for that label. +- `register_output_sinks`—push execution results into a review queue as they're produced. +- `register_label_output_sink`—a sink specifically for label mutations, replacing the default `LabelOutputSink`. +- A labels service backed by your existing datastore (previous section)—label an entity "flagged" from a rule, and your review queue queries your own store for that label. ## Plugging in your own ML model @@ -296,7 +296,7 @@ The returned score is then available in rules, e.g.: MySpamClassifier(text=MessageContent) > 0.85 ``` -Osprey constructs one UDF instance per call site when the rules are compiled, not per event, so the model isn't reloaded for every action processed. Keep in mind this means per _call site_, not per _class_: if you call the same UDF from multiple rules, each call site gets its own instance, and each one loads its own copy of the model. **For a large model, prefer calling the UDF from a single rule (or share the loaded weights via a module-level cache) rather than invoking it from many places.** +Osprey constructs one UDF instance per call site when the rules are compiled, not per event, so the model isn't reloaded for every event processed. That's one instance per _call site_, not per _class_: if you call the same UDF from multiple rules, each call site gets its own instance, and each one loads its own copy of the model. **For a large model, prefer calling the UDF from a single rule (or share the loaded weights via a module-level cache) rather than invoking it from many places.** For a model served remotely, the same pattern applies with `execute()` calling out over HTTP, gRPC, or your model server's SDK (you bring the client code). Because remote model calls are often slow or costly, gate them so they only run when relevant, using [Writing Rules' `Require(..., require_if=...)` pattern](../rules/README.md#workflow-structure-and-file-placement): diff --git a/docs/research-personas.md b/docs/research-personas.md index 5a1daee91..41fa2d90a 100644 --- a/docs/research-personas.md +++ b/docs/research-personas.md @@ -1,10 +1,10 @@ # User Research & Personas -Osprey is designed for users who need to investigate and take automated action on events in real-time. By interviewing prospective Osprey users, ROOST has drafted the following user personas for developers to better understand who might be interested in using this tool. We hope this research helps the open source community prioritize features to build moving forward. +Osprey is designed for users who need to investigate and take automated action on events in real time. By interviewing prospective Osprey users, ROOST has drafted the following user personas for developers to better understand who might be interested in using this tool. We hope this research helps the open source community prioritize features to build moving forward. ## T&S Engineers -Most engineers who are interested in potentially using Osprey are like Alice – they do not currently have a viable rules engine due to cost, lack of flexibility, or limited available options. However, they need a tool for real-time event processing to handle adverse events at scale. They also want to optimize how Osprey works with the rest of their tech stack. +Most engineers who are interested in potentially using Osprey are like Alice—they do not currently have a viable rules engine due to cost, lack of flexibility, or limited available options. However, they need a tool for real-time event processing to handle adverse events at scale. They also want to optimize how Osprey works with the rest of their tech stack. ![user_persona_engineer](images/user_persona_engineer.png) @@ -12,7 +12,7 @@ Most engineers who are interested in potentially using Osprey are like Alice – - When I manage rules, I want a flexible and intuitive developer experience (including intuitive logic and easy to learn programming syntax) so that I can make changes quickly and without being reliant on third party tools -- When I assess new tools, I clear info on ease of deployment and integration with common tools (e.g., Splunk), so that I know if the tool will work within our stack +- When I assess new tools, I want clear info on ease of deployment and integration with common tools (e.g., Splunk), so that I know if the tool will work within our stack - When I deploy changes, I want to ensure they will be as effective as possible, to avoid future duplication of work From 621a3b08daf10c8db36c532691c87c9c4c55388a Mon Sep 17 00:00:00 2001 From: Juliet Shen Date: Mon, 20 Jul 2026 08:10:04 -0700 Subject: [PATCH 52/53] docs: fix inaccuracies found in the 1.1 accuracy pass Cross-checked the docs against the engine and CLI and fixed several things that were wrong or would not actually work: - cli-reference: osprey-cli subcommands use dashes, not underscores (Click derives the command name from the function and converts _ to -), so `push_rules` etc. failed with "No such command". Also refreshed the osprey-stress `measure` wording now that #236 has merged. - query-syntax: SML uses `None`, not `Null`; the query box supports only four built-in functions (RegexMatch, DidAddLabel, DidRemoveLabel, DidDeclareVerdict), so the TextContains/ListLength query examples (both rules UDFs) would 500. Reworked the "Using UDFs" section and the note. - manage: clarified that the UDF Registry is mainly a rules reference; only those four functions work in the query box. - rules/README and rules/examples: Rule(...) requires a `description` argument (validation rejects it otherwise) and the null literal is `None`, not `Null`. Verified both against the real validator (validate_and_push). - local: use `uv run python` instead of a hardcoded python3.11 binary. Co-Authored-By: cassidyjames <611168+cassidyjames@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 --- docs/development/cli-reference.md | 20 ++++++------ docs/development/local.md | 2 +- docs/rules/README.md | 45 ++++++++++++++++----------- docs/rules/examples.md | 1 + docs/user/investigate/query-syntax.md | 22 ++++++------- docs/user/manage.md | 2 +- 6 files changed, 51 insertions(+), 41 deletions(-) diff --git a/docs/development/cli-reference.md b/docs/development/cli-reference.md index d793c1d35..8ad47c18e 100644 --- a/docs/development/cli-reference.md +++ b/docs/development/cli-reference.md @@ -6,18 +6,18 @@ Osprey includes the following command-line tools installed as console scripts; r General-purpose worker administration including pushing rules, opening a debug shell, and applying labels manually. -### osprey-cli push_rules +### osprey-cli push-rules ```sh -osprey-cli push_rules RULES_PATH [--dry-run/--no-dry-run] [--suppress-warnings] +osprey-cli push-rules RULES_PATH [--dry-run/--no-dry-run] [--suppress-warnings] ``` Validates the rules at `RULES_PATH` and pushes them. Use `--dry-run` to validate without pushing. Exits non-zero if validation fails. -### osprey-cli compute_and_upload_dependencies_mapping +### osprey-cli compute-and-upload-dependencies-mapping ```sh -osprey-cli compute_and_upload_dependencies_mapping RULES_PATH [--suppress-warnings] +osprey-cli compute-and-upload-dependencies-mapping RULES_PATH [--suppress-warnings] ``` Computes the dependency graph for the rules at `RULES_PATH` and uploads it; this is what powers the [Rules Visualizer](../user/manage.md#rules-visualizer). @@ -30,10 +30,10 @@ osprey-cli shell [-i / --auto-import / --no-auto-import] Opens an interactive shell (IPython if installed, otherwise a fallback `code.InteractiveConsole`) with `labels`, `access_audit_log`, and `stored_execution_result` storage modules pre-imported, plus `EntityT`, `EntityLabelMutation`, and `LabelStatus`. With `--auto-import` (the default), it also imports every model class it can find under `osprey_lib`. Useful for interactively inspecting stored data. -### osprey-cli apply_label +### osprey-cli apply-label ```bash -osprey-cli apply_label ENTITY_TYPE ENTITY_ID LABEL_NAME LABEL_STATUS \ +osprey-cli apply-label ENTITY_TYPE ENTITY_ID LABEL_NAME LABEL_STATUS \ [--reason REASON] [--description DESCRIPTION] [--expire-instantly] ``` @@ -41,14 +41,14 @@ Manually applies a label to a single entity. Mainly intended for debugging or im This requires a labels provider to be configured for the Osprey instance; it fails with an assertion error otherwise. -### osprey-cli bulk_apply_label +### osprey-cli bulk-apply-label ```bash -osprey-cli bulk_apply_label ENTITY_TYPE ENTITY_IDS_FILE_PATH LABEL_NAME LABEL_STATUS \ +osprey-cli bulk-apply-label ENTITY_TYPE ENTITY_IDS_FILE_PATH LABEL_NAME LABEL_STATUS \ [--reason REASON] [--description DESCRIPTION] [--expire-instantly] ``` -Same as `apply_label`, but reads entity IDs (one per line) from `ENTITY_IDS_FILE_PATH` and applies the label to all of them, printing progress as it goes. This is the CLI path for importing bulk label lists from external sources; see also the UI-driven [Bulk Actions](../user/operate.md#bulk-actions) workflow. +Same as `apply-label`, but reads entity IDs (one per line) from `ENTITY_IDS_FILE_PATH` and applies the label to all of them, printing progress as it goes. This is the CLI path for importing bulk label lists from external sources; see also the UI-driven [Bulk Actions](../user/operate.md#bulk-actions) workflow. ## osprey-stress @@ -77,7 +77,7 @@ Common flags: osprey-stress measure [--duration SECONDS] [--report {human,json}] ``` -Reserved for open-loop measurement against externally-produced events, once the jetstream input stream plugin (#236) lands. Today it prints a stub message and exits non-zero; use `run` for closed-loop synthetic testing in the meantime. +Reserved for open-loop measurement against externally-produced events, such as the JetStream input stream sample ([#236](https://github.com/roostorg/osprey/pull/236)). The measurement layer isn't implemented yet, so today it prints a stub message and exits non-zero; use `run` for closed-loop synthetic testing in the meantime. ## osprey-async-cli diff --git a/docs/development/local.md b/docs/development/local.md index e8a83e5b2..b705e7f99 100644 --- a/docs/development/local.md +++ b/docs/development/local.md @@ -138,7 +138,7 @@ Implement any subset of these in your plugin's `register_plugins.py`: Rules are written in SML; examples with YAML config are provided in `example_rules/`. Rules are mounted into the worker processes when the containers start, controlled via environment variables; for example: ```bash -OSPREY_RULES_PATH=./example_rules uv run python3.11 osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink +OSPREY_RULES_PATH=./example_rules uv run python osprey_worker/src/osprey/worker/cli/sinks.py run-rules-sink ``` For more about rules, see [Writing Rules](../rules/). diff --git a/docs/rules/README.md b/docs/rules/README.md index 0db4dea00..d33a036d9 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -28,7 +28,7 @@ Rules currently support the following concepts through the `Rule(...)` function The rule's logic is the list of signals in the `when_all` parameter; a signal can be a comparison over features, a label check, a UDF call, or another rule. - If any signal in the list evaluates to `Null`, the whole rule evaluates to `Null`; see [Nulls](#nulls) below. + If any signal in the list evaluates to `None`, the whole rule evaluates to `None`; see [None values](#none-values) below. - **Description** @@ -49,7 +49,7 @@ My_Rule_Name_v2 = Rule( # Secondary Signal RegexMatch(target=MyStringValue, pattern='(hello|world)'), MySecondValue >= 3, - MyThirdValue != Null, + MyThirdValue != None, # Guardrail Signal (_LocalValue in [1, 2, 3, 5]) or (GlobalValue in ['hello', 'howdy']), not HasLabel(entity=MySecondEntityName, label='MySecondLabel'), @@ -262,27 +262,36 @@ A custom UDF can also create entities by declaring `EntityT` as its output type. ## Notable Gotchas -### Nulls +### None values -A rule or variable in SML is `Null` when it doesn't exist; a piece of data was missing, or a rule didn't run. Unlike in many programming languages, a rule with a `Null` signal is skipped entirely (and so are rules downstream of it) unless the rule checks for `Null` explicitly. For example: +A rule or variable in SML is `None` when it doesn't exist; a piece of data was missing, or a rule didn't run. Unlike in many programming languages, a rule with a `None` signal is skipped entirely (and so are rules downstream of it) unless the rule checks for `None` explicitly. For example: ```python Thing: int = JsonData(path='$.property_that_doesnt_exist') # Evaluates to False -MyFirstRule = Rule(when_all=[ - Thing != Null, -]) - -# Skips evaluation and sets to Null -MySecondRule = Rule(when_all=[ - Thing > 1, -]) - -# Skips evaluation and sets to Null -MyThirdRule = Rule(when_all=[ - MySecondRule, -]) +MyFirstRule = Rule( + when_all=[ + Thing != None, + ], + description=f'Thing is present', +) + +# Skips evaluation and sets to None +MySecondRule = Rule( + when_all=[ + Thing > 1, + ], + description=f'Thing is greater than 1', +) + +# Skips evaluation and sets to None +MyThirdRule = Rule( + when_all=[ + MySecondRule, + ], + description=f'MySecondRule matched', +) ``` ### Workflow Structure and File Placement @@ -301,7 +310,7 @@ Import( ] ) -MyRule = Rule(when_all=[ActionName == "foo"]) +MyRule = Rule(when_all=[ActionName == "foo"], description=f'Action is foo') ``` `Require` selectively runs other SML scripts. It supports templating and conditionals, so scripts can be skipped entirely; that matters when a rule or UDF is expensive to run, like a call to an AI service. diff --git a/docs/rules/examples.md b/docs/rules/examples.md index 0b69b7506..bd2092975 100644 --- a/docs/rules/examples.md +++ b/docs/rules/examples.md @@ -124,6 +124,7 @@ Should_Warn_User_Of_Spammer = Rule( HasLabel(entity=UserId, label='likely_spammer'), This_Is_A_New_DM, ], + description=f'Likely spammer {UserId} started a new DM', ) ``` diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index f8c183ec9..9dc95d640 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -7,7 +7,7 @@ Osprey uses SML (“Some Madeup Language,” a subset of Python with additional ```py EventType == "create_post" UserId == 12345 -MessageText != Null +MessageText != None ``` ## Combining conditions @@ -23,21 +23,21 @@ EventType == "user_login" and LoginAttempts >= 3 EventType in ["create_post", "send_message"] ``` -## Using UDFs +## Query functions -UDFs (see [UDF Registry](../manage.md#udf-registry)) extend what you can express in a query: +Queries support a small, fixed set of built-in functions. Unlike the UDFs you use in rules (which are pluggable), this set does not grow when you add plugins: + +- `RegexMatch(target=..., pattern=...)`: regex match against a feature +- `DidAddLabel(...)` and `DidRemoveLabel(...)`: events where a label was added or removed (see [Label queries](#label-queries)) +- `DidDeclareVerdict(...)`: events where a verdict was declared ```py -# Text search -TextContains(text=PostContent, phrase="spam") +# Regex match against a feature RegexMatch(target=MessageText, pattern="(buy|sell|deal)") - -# List operations -ListLength(list=UserConnections) > 10 ``` > [!NOTE] -> If you query a UDF that doesn't exist, Osprey will silently fail with a 500 error. Use the UDF Registry to confirm a function name before using it. +> These are the only functions that work in queries. The [UDF Registry](../manage.md#udf-registry) lists every UDF, but most are rules-only and will fail with a silent 500 error if you use them in a query. ## Label queries @@ -55,8 +55,8 @@ DidAddLabel(entity_type="IpAddress", label_name="suspicious") # Suspicious login attempts EventType == "user_login" and LoginAttempts >= 5 -# Posts containing specific words -EventType == "create_post" and TextContains(text=PostContent, phrase="urgent") +# Posts matching a pattern +EventType == "create_post" and RegexMatch(target=PostContent, pattern="urgent") # Users who were flagged DidAddLabel(entity_type="UserId", label_name="flagged") diff --git a/docs/user/manage.md b/docs/user/manage.md index ab6b92e07..671d8e3bd 100644 --- a/docs/user/manage.md +++ b/docs/user/manage.md @@ -29,7 +29,7 @@ UDFs are organized by category and are searchable. Each entry shows: - Parameter names, types, and descriptions - Return type -Use this page as your reference when writing queries or rules: especially to confirm a function's exact name and parameter order before using it. (Querying a UDF that doesn't exist causes a silent 500 error.) +Use this page as your reference when writing rules, to confirm a function's exact name and parameter order. Note that most UDFs are for rules only: the query box supports just four functions (`RegexMatch`, `DidAddLabel`, `DidRemoveLabel`, `DidDeclareVerdict`), so using any other UDF in a query currently fails with a silent 500 error. ## Features Registry From ab5dca33d718b1994d6210d9fd60480a63d74c4b Mon Sep 17 00:00:00 2001 From: Juliet Shen Date: Mon, 20 Jul 2026 20:33:59 -0700 Subject: [PATCH 53/53] docs: fix broken rules links and verified review findings - Fix three cross-links to rules/README.md#... : mdBook renders that page as rules/index.html, so the inline links became rules/README.html and 404'd. Point them at the directory (rules/#anchor) instead. Affects labels.md, integrations.md (x2). - DidAddLabel entity_type should be the entity's declared type ("User"), not the feature name "UserId" (examples.md, query-syntax.md). - concepts.md: rules also evaluate to None (missing signal), not just True/False. - local.md: register_ast_validators returns Sequence[Type[BaseValidator]], not None (matches the hook spec and the hooks table). - integrations.md: use InputStreamSource.PUBSUB for Google Pub/Sub, not KAFKA (the surrounding table already maps it to PUBSUB). Co-Authored-By: cassidyjames <611168+cassidyjames@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 --- docs/concepts.md | 2 +- docs/development/local.md | 2 +- docs/integration/integrations.md | 6 +++--- docs/rules/examples.md | 2 +- docs/user/investigate/labels.md | 2 +- docs/user/investigate/query-syntax.md | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/concepts.md b/docs/concepts.md index d001a59de..89c502584 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -25,7 +25,7 @@ In our example, `UserId` would be declared as an entity: the same user posts man ## Rules -A **rule** is a named condition over features, written in Osprey's rule language, SML. Rules are evaluated against every event as it arrives. On their own, rules just return a boolean true/false (which is itself queryable, like any feature). Rules can be wired to [effects](#effects). +A **rule** is a named condition over features, written in Osprey's rule language, SML. Rules are evaluated against every event as it arrives. On their own, rules just return `True` or `False` (which is itself queryable, like any feature), or `None` when a signal it depends on is missing. Rules can be wired to [effects](#effects). In our example, a rule might cover "the event is a post creation and the post text contains 'hello'." diff --git a/docs/development/local.md b/docs/development/local.md index b705e7f99..2ae6caeae 100644 --- a/docs/development/local.md +++ b/docs/development/local.md @@ -113,7 +113,7 @@ def register_output_sinks(config: Config) -> Sequence[BaseOutputSink]: # By default it prints the execution results to the console @hookimpl_osprey -def register_ast_validators() -> None: +def register_ast_validators() -> Sequence[Type[BaseValidator]]: # Register AST validators ``` diff --git a/docs/integration/integrations.md b/docs/integration/integrations.md index a5cdd9100..4d906558c 100644 --- a/docs/integration/integrations.md +++ b/docs/integration/integrations.md @@ -25,7 +25,7 @@ Each entry point resolves to a module that contains hook functions decorated wit ## Writing UDFs -A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic—text matching, DNS lookups, hash comparisons, ML inference—and expose it as a named function in the rules language. See [Writing Rules § User Defined Functions](../rules/README.md#user-defined-functions-udfs) for the language-level view. +A user-defined function (UDF) is a Python class that can be called from your rules. UDFs encapsulate reusable detection logic—text matching, DNS lookups, hash comparisons, ML inference—and expose it as a named function in the rules language. See [Writing Rules § User Defined Functions](../rules/#user-defined-functions-udfs) for the language-level view. ### Anatomy of a UDF @@ -121,7 +121,7 @@ Source | Config `SYNTHETIC` |   | Generates random fake events; useful for local dev without any upstream system `PLUGIN` |   | Delegates to your registered `register_input_stream` hook -Set `InputStreamSource.KAFKA` (or whichever fits your existing infrastructure) if you already have events flowing through Kafka or Pub/Sub. Otherwise, implement a custom input stream and set `InputStreamSource.PLUGIN` in your config. If your events arrive as protobuf rather than JSON, there's also a `register_action_proto_deserializer` hook for supplying your own deserializer. +Set `InputStreamSource.KAFKA` if you already have events flowing through Kafka, or `InputStreamSource.PUBSUB` for Google Pub/Sub (see the table above). Otherwise, implement a custom input stream and set `InputStreamSource.PLUGIN` in your config. If your events arrive as protobuf rather than JSON, there's also a `register_action_proto_deserializer` hook for supplying your own deserializer. ### Writing a custom input stream @@ -298,7 +298,7 @@ MySpamClassifier(text=MessageContent) > 0.85 Osprey constructs one UDF instance per call site when the rules are compiled, not per event, so the model isn't reloaded for every event processed. That's one instance per _call site_, not per _class_: if you call the same UDF from multiple rules, each call site gets its own instance, and each one loads its own copy of the model. **For a large model, prefer calling the UDF from a single rule (or share the loaded weights via a module-level cache) rather than invoking it from many places.** -For a model served remotely, the same pattern applies with `execute()` calling out over HTTP, gRPC, or your model server's SDK (you bring the client code). Because remote model calls are often slow or costly, gate them so they only run when relevant, using [Writing Rules' `Require(..., require_if=...)` pattern](../rules/README.md#workflow-structure-and-file-placement): +For a model served remotely, the same pattern applies with `execute()` calling out over HTTP, gRPC, or your model server's SDK (you bring the client code). Because remote model calls are often slow or costly, gate them so they only run when relevant, using [Writing Rules' `Require(..., require_if=...)` pattern](../rules/#workflow-structure-and-file-placement): ```python Require(rule='ai_services/my_ai_service.sml', require_if=ActionName == 'register') diff --git a/docs/rules/examples.md b/docs/rules/examples.md index bd2092975..58dc48af1 100644 --- a/docs/rules/examples.md +++ b/docs/rules/examples.md @@ -128,7 +128,7 @@ Should_Warn_User_Of_Spammer = Rule( ) ``` -Labels also show on the entity in the UI, where they can be added and removed by hand. One asymmetry to know about: `HasLabel()` works in rules but not in the query bar, because queries search events rather than current entity state. To find events where a label was applied, query `DidAddLabel(entity_type="UserId", label_name="likely_spammer")` instead; see [Query Syntax](../user/investigate/query-syntax.md) for details. +Labels also show on the entity in the UI, where they can be added and removed by hand. One asymmetry to know about: `HasLabel()` works in rules but not in the query bar, because queries search events rather than current entity state. To find events where a label was applied, query `DidAddLabel(entity_type="User", label_name="likely_spammer")` instead; see [Query Syntax](../user/investigate/query-syntax.md) for details. ## A multi-signal rule, organized across files diff --git a/docs/user/investigate/labels.md b/docs/user/investigate/labels.md index 003afc880..a1d03b712 100644 --- a/docs/user/investigate/labels.md +++ b/docs/user/investigate/labels.md @@ -34,4 +34,4 @@ From the event stream, select any entity to open its label drawer. ![An entity view with negative labels applied, one expanded to show its description and the rule event that produced it, plus neutral labels below; identifying values are redacted](../../images/complete-label.png) -For more about how labels are used, see [Writing Rules → Labels](../../rules/README.md#labels). +For more about how labels are used, see [Writing Rules → Labels](../../rules/#labels). diff --git a/docs/user/investigate/query-syntax.md b/docs/user/investigate/query-syntax.md index 9dc95d640..d2ad2a808 100644 --- a/docs/user/investigate/query-syntax.md +++ b/docs/user/investigate/query-syntax.md @@ -45,7 +45,7 @@ The query UI searches across events, not entity state, so `HasLabel()` won't wor ```py # Find events that added a specific label -DidAddLabel(entity_type="UserId", label_name="likely_spammer") +DidAddLabel(entity_type="User", label_name="likely_spammer") DidAddLabel(entity_type="IpAddress", label_name="suspicious") ``` @@ -59,10 +59,10 @@ EventType == "user_login" and LoginAttempts >= 5 EventType == "create_post" and RegexMatch(target=PostContent, pattern="urgent") # Users who were flagged -DidAddLabel(entity_type="UserId", label_name="flagged") +DidAddLabel(entity_type="User", label_name="flagged") # Complex: messages matching a pattern, from users without a verified label EventType == "send_message" and RegexMatch(target=MessageText, pattern="(click|link|urgent)") and -not DidAddLabel(entity_type="UserId", label_name="verified") +not DidAddLabel(entity_type="User", label_name="verified") ```