Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
git clone --depth 1 https://github.com/Scopeo/draftnrunWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/rules/scopeo/draftnrun/engine)<a href="https://agentmods.dev/rules/scopeo/draftnrun/engine"><img src="https://agentmods.dev/badge/rules/scopeo/draftnrun/engine.svg" alt="Measured on agentmods" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.00000 | $0.02431 |
| Opus 5 | $0.00000 | $0.01215 |
| Sonnet 5 | $0.00000 | $0.00486 |
| Haiku 4.5 | $0.00000 | $0.00243 |
Grade A, and why
engine scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 44 lines — stays where its author put it; the contents beside it link to each section on GitHub.
Execution Engine Conventions
- Components implement the
Runnableprotocol with typed Pydantic I/O schemas. _run_without_io_trace(inputs: BaseModel, ctx: dict) -> BaseModelis the core execution method — this is what subclasses implement.- Port system:
PortDefinition(catalogue schema) →PortInstance(per-graph-instance) →FieldExpression(JSON AST wiring and transforms). There is noport_mappingstable; field expressions are the sole wiring mechanism. GraphRunnerusesnetworkx.DiGraph, topological scheduling,ExecutionDirectivefor control flow (CONTINUE,HALT,SELECTIVE_EDGE_INDICES). Skipped subgraphs useTaskState.HALTED(root of the skipped branch plus descendants), notCOMPLETED. If/Else uses selective edge orders0for true and1for false/else; theenable_false_pathcomponent parameter defaults tofalseand must betruefor theorder=1branch to run.- Field expressions: 5 AST node types (
literal,ref,var,concat,json_build). Key files:engine/field_expressions/ast.py(AST),parser.py,serializer.py,traversal.py. parse_expression_flexible()acceptsstr,dict, andlist. Dicts that match a serialized AST shape are deserialized viafrom_json(). Plain dicts/lists becomeLiteralNode(json.dumps(value)). The client is responsible for buildingjson_buildAST nodes for JSON-typed parameters containing@{{}}refs/vars.- Variable resolution: defaults → layered set overrides. See
ada_backend/services/variable_resolution_service.py. - Secret handling: runtime secrets use
SecretStr(pydantic.SecretStr) and are unwrapped only at explicit execution boundaries (unwrap_secrets()/get_secret_value()). - Observability boundary:
engine/trace/serializer.pymasksSecretStr;shared/log_redaction.pyis best-effort defense-in-depth for untyped payloads/events and is shared by API, scheduler, engine, and worker processes. - Default OpenAI chat-capable model is
gpt-5-mini; provider-qualified references and completion-model catalog defaults should useopenai:gpt-5-mini. GoogleProvidermust not replay prior assistanttool_callsin multi-turn function-calling requests; Gemini requires hiddenthought_signaturedata that the OpenAI-compatible response does not expose. Preserve tool outputs by convertingtoolrole messages into user-visible tool-result context.requires_tool_name = Truefor components that need atool_nameinput (MCP tools, API tools).- Legacy system: some components still use
AgentPayloadpattern (migrated = False). New components MUST use the multi-portNodeDatasystem (migrated = True). - Python Code Runner keeps the full E2B payload in
artifacts.execution_result; when a text result exists, it also exposesartifacts.textfor@{{instance.artifacts::text}}injection. New sandbox-root files with supported extensions are saved to the run temp folder and exposed asartifacts.files; this key must always be present and contain[]when no supported files were generated. The tool output lists generated filenames so AI Agents can pass them back throughinput_filepathson later Python Code Runner calls. When Python Code Runner is an AI Agent tool,AIAgentmust carryartifacts.filesinto the next iteration and add anAvailable generated filesinstruction to the system prompt. Generated PDFs must only be attached to the next LLM message after the model explicitly callsattach_generated_files_to_llmwith exact filenames;artifacts.attached_filesis one-shot metadata and must be cleared after that next request is built. Direct LLM file payload filenames must preserve the normalized run-relative path, not just the basename, so same-named files from different subdirectories remain distinguishable. Skip missing, oversized, or unreadable PDFs per file and keep sending any remaining attachments. If no user-role message is retained, add a fallback user message carrying the file payloads. Itsoutputis trace-friendly and must omit inlinepng/jpegbase64 payloads while preserving them in artifacts. Shared E2B sandboxes from tracing context must pass a current-loop async health check before reuse; stale or closed-loop clients are discarded and replaced. - DOCX Template render errors should preserve docxtpl diagnostics: validate undeclared template variables before render, validate image placeholders with python-docx before adding them to render context, identify the failing render substep (body XML, tables, body mapping, headers, footers, properties, footnotes), and include exception type/
reprplus Jinja DOCX context when available. SQLLocalServiceengine pools are cached perengine_url(process-level). Avoid patterns that instantiate many services for the same URL expecting independent pools.- In ingestion code paths, reuse one
SQLLocalServiceper job when possible, and ensureawait close()is called in afinallyblock. IS_CLOUD_S3controls folder-source ingestion URL strategy: keep itFalsein local/dev and customS3_ENDPOINT_URLsetups (direct file reads; presigned URL getter returnsNone), set itTruein cloud AWS S3 prod to require presigned URL generation and fail fast on missing URLs.- For DB-source ingestion worker flow (
ingestion_script/ingest_db_source.py), keep a single sourceSQLLocalServiceper run and share it across validation and fetch helpers. - Adding a new component: create class in
engine/components/, register inada_backend/services/registry.py, add DB seed data (Component, ComponentVersion, PortDefinition, ComponentParameterDefinition rows). Always setmigrated = True, define Pydantic I/O schemas, define canonical ports. - Branded OAuth components may reuse an existing runtime class when behavior is identical, but must have separate seed entries and a distinct
OAuthProvidervalue when they use a separate Nango provider config key (e.g.google-mail-neverdrop,google-calendar-neverdrop,google-contact-neverdrop). Gmail Neverdrop must remain send-only: nosave_as_draftcatalog or runtime parameter, recipients required, and no draft fallback. Neverdrop-branded Google tools should remain agent-available (is_agent=True) and function-callable; Google Contacts Neverdrop is read-only, lists regular contacts plus Other contacts by default, always requests People API sync tokens (callers passsync_token/other_contacts_sync_tokenfor deltas; expired tokens surfaceEXPIRED_SYNC_TOKEN), exposescontacts_search_contactsover both sources (30-result cap, empty-first-result = cache warmup -> wait ~2s + one retry, restricted Other-contacts search readMask), and requires bothcontacts.readonlyandcontacts.other.readonly. - Unified Mail Sender versions that expose a Gmail Neverdrop connection in seed data must resolve
gmail_oauth_connection_idwithOAuthProvider.GMAIL_NEVERDROPinada_backend/services/registry.py; keep older unified Mail Sender versions on regular Gmail when their catalog still exposesOAuthProvider.GMAIL. Standalone Gmail Neverdrop remains send-only with nosave_as_draftparameter, while unifiedmail_sender_v2exposessave_as_draftfor Gmail or Outlook and defaults it totrue. - Mail sender
email_attachmentsis a list whose items can each be either a string path/URL or an object withfilenameplus eitherurlorpath. Preserve both item shapes when changing Gmail, Gmail Neverdrop, Outlook, or unified Mail Sender components; object attachments must usefilenameas the displayed attachment name when downloading fromurlor reading frompath, and tool-description schemas must not useoneOf/anyOf. - Outlook Sender
ccaccepts either a list of emails or a comma-separated string; strings are split/stripped into a list before Graph payload construction. - Public mail attachment input contract changes need a new
ToolDescriptionrow wired only to the new component version; older mail sender versions should keep legacy string-item tool descriptions. - Attachment URL downloads must validate the initial URL and every redirect target before streaming; reject non-HTTP(S) schemes and private/reserved network addresses. When connecting to a resolved IP for SSRF protection, preserve the original HTTPS hostname for SNI/certificate verification.
- HubSpot MCP
crm_upsert_contact_by_emailreturnsid,operation, andremote_url; cache successful portal metadata per HubSpot client and keepremote_urlas an empty string if portal metadata cannot be resolved after a successful upsert, including HubSpot metadata API errors and network failures. - HubSpot MCP
notes_upsert_for_contactandtasks_createtreatproperties.hs_timestampas optional and auto-fill missing or empty values with the current UTC time in ISO-8601Zformat before calling HubSpot. - HubSpot Owner is an API-tool-style component, not an MCP wrapper. It keeps HubSpot headers as a JSON configuration value, injects
owner_idintohttps://api.hubapi.com/crm/v3/owners/{owner_id}, and exposes owner fields (id,email,firstName,lastName, etc.) at the root output level for field expressions. - Google API discovery services used from async MCP clients must be built inside each
asyncio.to_threadcall; do not share google-api-python-client service instances across worker threads. - Removing a component: delete the runtime class, registry entry, DB seed definitions, default tool description, and any wrapper components that only exist to invoke it in the same diff. Do not leave dead catalog entries that can still be instantiated through backend graphs or MCP.
SQLSpanExporter(engine/trace/sql_exporter.py): each span is parsed once (_parse_span_or_error→ dict passed to_export_span); do not re-serialize the same span in the export path.- Hybrid search:
QdrantServicesupports threeSearchModevalues:semantic(default, dense only),keyword(BM25 sparse only),hybrid(dense + sparse with RRF fusion). All collections are hybrid (named"dense"+"sparse"vectors). Existing component versions default tosemanticwithout exposing the parameter; new versions (RAG v40.3.0inseed_rag_v4.py, Retriever v20.0.2, Retriever Tool v20.0.2) addsearch_modeas a user-configurable parameter. Seeada_backend/docs/engine.mdfor details. - Qdrant metadata filters: keep technical identifiers (
chunk_id,source_id,file_id,url,sync_id) askeywordindexes queried withmatch.value. Never payload-index chunk body fields (content,chunk). Human text metadata (VARCHAR/TEXTnames, authors, titles, descriptions) uses Qdranttextindexes and should be queried withmatch.textso punctuation/case/accent differences are normalized by Qdrant. - See
ada_backend/docs/engine.mdandada_backend/docs/payload-and-data-flow.md.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 2d ago First seen · 44 lines · 0 tokens per session scan A ff5a65c6be77
engine is a cursor rule published in the GitHub repository Scopeo/draftnrun (30 stars, last pushed 4d ago), licensed Apache-2.0. It costs nothing until one of its globs matches a file; then it loads 2,431 tokens. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-09-04.
Other cursor rules, from other repositories
ponytail
Ponytail, lazy senior dev mode. Always pick the simplest solution that works.
angular-20
This rule provides comprehensive best practices and coding standards for Angular development, focusing on modern TypeScript, standalone components, signals, and performance optimizations.
dev-standard
Apache Superset development standards and guidelines for Cursor IDE.
cli-error-handling
CLI command error handling patterns.
prefer-direct-imports-over-module-mocks
Prefer extracting a testable core over vi.mock / vi.resetModules when unit tests need to reach production logic entangled with config, env, or singletons.
control-plane-descriptors
Control plane descriptor and instance implementation patterns.