roteiro 2.0.2

Roteiro: a provenance-tagged knowledge graph for your codebase — structure, intent, and context in one queryable store
[package]
name = "roteiro"
description = "Roteiro: a provenance-tagged knowledge graph for your codebase — structure, intent, and context in one queryable store"
readme = "README.md"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
authors.workspace = true

[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
# The analyzer contract and the ingest path behind it (`roteiro security …`).
# Optional so the seam has a name that Stages 22/24 can hang their
# `exec-subprocess` / `exec-boxlite` backends off — but enabled by default,
# because ingest is the zero-install path (ADR-0014) and pulls no third-party
# dependency the workspace does not already have.
rto-exec = { workspace = true, optional = true }
rto-graph = { workspace = true }
rto-llama = { workspace = true, optional = true }
# `remote` only: the remote model tier's consent gate, payload allow-list and
# egress record (ADR-0019). Optional and **off by default** — see the `remote`
# feature below, and do not "complete the job" by adding it to `default`.
rto-remote = { workspace = true, optional = true }
rto-render = { workspace = true }
rto-serve = { workspace = true, optional = true }
rto-spec = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
# Shared by `serve`/`mcp` (a SIGHUP handler that reloads the workspace registry
# without a restart, ADR-0008) and `explorer` (the standalone axum server binds a
# `tokio::net::TcpListener` and runs on a small current-thread runtime).
# `unsafe_code = "forbid"` rules out a raw signal handler, so use tokio's signal
# support; `net` pulls tokio's I/O driver, which both the signal handling and the
# TCP listener require. Declared here so this crate is self-contained, not reliant
# on another crate's features. Pure Rust — no C/C++ toolchain.
tokio = { version = "1", features = ["rt", "signal", "net", "time"], optional = true }
# `explorer` only: the read-only `/v1/graph/*` JSON API router (axum 0.8). Pure
# Rust — no llama.cpp, no `rto-serve` — so the explorer builds and tests without a
# C/C++ toolchain.
axum = { version = "0.8", optional = true }
# Layered project/user configuration (ADR-0007). TOML only — `serde_yaml` is
# unmaintained. Permissive, `cargo deny`-clean.
toml = "1.1"
# Structured logging/telemetry groundwork (ADR-0011). `tracing` is the event
# facade; `tracing-subscriber` builds the layered subscriber (the human-text
# stdout layer, unchanged and default, plus the opt-in structured file layer);
# `tracing-appender` provides the time-based rolling file appender and the
# non-blocking writer (whose worker guard is held for the process lifetime so a
# slow disk can never stall the app). No OTLP/network exporter yet — the file
# layer emits OpenTelemetry-shaped JSON lines a future collector can ingest.
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "registry"] }
tracing-appender = "0.2"
# `inference-local-models` only: consent-gated model download (rustls, no
# platform TLS). Blocking + pure-Rust.
ureq = { version = "3", optional = true }
# Restore the default SIGPIPE disposition (`SIG_DFL`) at startup so the CLI dies
# quietly on a closed stdout pipe (`roteiro query … | head`) instead of panicking
# with a broken-pipe backtrace — the classic Rust-CLI footgun (Rust sets SIGPIPE
# to `SIG_IGN`, so `println!` hits EPIPE and panics). We forbid `unsafe_code`
# workspace-wide, which rules out a raw `libc::signal` call here, so we lean on
# this tiny, contained wrapper (one `libc::signal(SIGPIPE, SIG_DFL)`, a no-op off
# Unix). libc is already in the tree; MIT-licensed. See `main`'s first line.
sigpipe = "0.1.3"

[dev-dependencies]
# Write a codegraph-schema SQLite fixture in the `import --from codegraph` test.
rusqlite = { workspace = true }
# `explorer` route tests: drive the axum router in-memory (no TCP bind) with
# `tower::ServiceExt::oneshot`, mirroring `rto-serve`'s server tests.
tokio = { version = "1", features = ["rt", "macros"] }
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"
# `tests/ci_release_pr_parity.rs` reads `.github/workflows/ci.yml` structurally —
# job by job, step by step — rather than grepping it, because the thing it
# asserts is the *shape* of a job and a `contains` over the text would be
# satisfied by the comments that discuss that shape at length. Adds no crate to
# the tree: `rto-graph` already depends on `yaml-rust2` non-optionally (for
# `config_keys`), so it is in the lockfile and in every build of this binary
# already, and neither `cargo deny` nor `cargo audit` gains a new surface. Keep
# the version in step with `crates/rto-graph/Cargo.toml`.
yaml-rust2 = "0.11.0"

[features]
# Ingest is available in a stock install — ADR-0014 lists it as "always, no
# feature" — so `execution` is on by default. It stays a *named* feature because
# the subprocess and sandboxed backends land behind subfeatures of it, and so
# `--no-default-features` still yields a binary with no analyzer surface at all.
#
# `models` is on by default because `roteiro model pull` is the **prerequisite
# for working offline**, and without it that command does not exist: the clap
# variant is `#[cfg(feature = "models")]`, so a stock install answered
# `unrecognized subcommand` rather than degrading gracefully. "Prepare once, then
# work offline" — the story `docs/OFFLINE_SETUP.md` tells — was unreachable from
# the shipped default. It is cheap to carry: measured on an unloaded 18-core
# machine, a cold release build is within noise of the old default (28.0–30.5 s
# before, 29.2–31.1 s after) and the binary grows 2.17 MiB over 20 extra crates
# (209 → 229). It adds **no new host-toolchain class**: `ureq`'s rustls stack
# does reach C and pregenerated assembly via `ring`, but the default build
# already compiles C for bundled SQLite and 18 tree-sitter grammars, so this
# needs the same `cc` and no more — never C++, cmake or libclang. Do not restate
# this as "pure Rust"; it is one `cc` away from that and the distinction is the
# whole point. `cargo deny check` is clean at this set; every licence it
# moves into the default binary (`ISC`, `BSD-3-Clause`, and
# `CDLA-Permissive-2.0` via `webpki-roots`) was already allow-listed with a
# recorded rationale — see `deny.toml` and ADR-0017.
#
# **`serve` deliberately stays off**, and this is the record so that nobody
# "completes" the job later:
#   * it transitively builds llama.cpp from source — measured +24 s cold on 18
#     cores, realistically **3–12 minutes on 2–4 cores**;
#   * without `cmake` and `libclang` it does not degrade, it **panics in a build
#     script** and `cargo install` fails. On Debian/Ubuntu `build-essential`
#     supplies neither, so it turns zero extra packages into two;
#   * `docs/VENDORED_DEPENDENCIES.md` records that the vendored llama.cpp has
#     **13 published upstream advisories invisible to `cargo audit`**, which
#     reads crate metadata and cannot see C++ vendored by a build script.
#     Defaulting it on would ship that unmonitored surface to everyone rather
#     than to the people who asked for inference.
# `exec-subprocess` is on by default so that the *other* half of "prepare once,
# then work offline" ships too. Enabling it compiles the capability to run an
# analyzer as a child process; it does not exercise it, and three things keep
# that true:
#   * `--allow-unsandboxed` is still required **per run**. See below — with the
#     build-time gate gone from the default path, that flag is now the only thing
#     between a stock install and an unsandboxed execution, so it got *more*
#     load-bearing here, not less.
#   * Roteiro never installs an analyzer. `semgrep` and `osv-scanner` must
#     already be on `PATH`, so the host owner has already opted into having them;
#     a missing one is a named error, not a fetch.
#   * A run records `isolation=none` in its evidence either way.
# `serve` is the one thing that stays off, for the reasons above.
default = ["execution", "models", "exec-subprocess"]
# The `AnalyzerRunner` seam, `roteiro security ingest|list`, and — since the
# provisioning/execution split — `security prefetch|status` (ADR-0012, ADR-0014).
# It also carries `roteiro lint`'s command surface, because that reads types from
# `rto-exec`; the linter itself needs `exec-subprocess` below, and in a build
# without it the command parses and refuses by name rather than vanishing.
#
# Provisioning lives here rather than behind a backend feature because it
# executes nothing: it downloads, digests, pins and reports. Every `Command::new`
# in `rto-exec` is in `subprocess.rs` or `boxlite.rs`, both still gated. Gating it
# on a backend was also circular — `exec-boxlite`'s build script demands a
# verified runtime archive at compile time, and `prefetch` is what obtains it, so
# provisioning had to be reachable from a build with no backend at all.
#
# It takes `ureq` for that reason and no other: `prefetch --allow-download`
# fetches `osv-scanner`'s per-ecosystem OSV databases. `rto-exec` deliberately
# does **not** gain a network dependency — it takes the fetcher as a function
# argument, so the code that can open a socket lives in the CLI and is reachable
# only from `prefetch`. In the default set `ureq` is already present for
# `roteiro model pull`, so this costs nothing there; in a
# `--no-default-features --features execution` build it is the one crate the
# split adds.
# It also forwards `rto-render/execution`, which is what puts `security list` and
# `security status` on the **MCP** tool surface (issue #435). Forwarded from here
# rather than from `mcp` because this is the feature that decides whether this
# binary has an analyzer surface at all: the served-chat registry's two
# `security_*` tools are gated on `all(serve, execution)` and MCP's on
# `all(mcp, execution)`, so both surfaces gain and lose the pair together and
# `both_tool_surfaces_offer_the_same_tools` cannot be satisfied by only one of
# them. Cargo cannot express "only when `mcp` is also on", so a non-`mcp` default
# build carries the dependency edge and none of the code — and `rto-exec` is
# already in that build, so the edge costs nothing.
execution = ["dep:rto-exec", "dep:ureq", "rto-render/execution"]
# The subprocess analyzer *backend*: `roteiro security run` (ADR-0014). **On by
# default**, which is a change — it used to have to be asked for at build time as
# well as consented to per run.
#
# Rewriting rather than deleting that caution, because only half of it stopped
# being true. What the build-time default grants is the *presence* of the runner;
# what it does not grant is a run. `security run` still refuses without
# `--allow-unsandboxed` on every single invocation, still records
# `isolation=none` in the run's evidence, and still cannot fetch an asset — a
# cold cache fails and names the `prefetch` command. And it cannot install an
# analyzer: if `semgrep` is not on `PATH` the run stops with a message saying so
# and pointing at `roteiro security ingest`.
#
# So `--allow-unsandboxed` is now the *only* gate between a stock install and
# executing a third-party binary on this host with no isolation boundary. Do not
# weaken it "for consistency" because the build-time gate is gone; it is carrying
# the load the build-time gate used to share. ADR-0014 v1.1 records this.
#
# This feature also compiles **`roteiro lint`**, which under ADR-0020 v1.3 is
# sandboxed by default and — since no sandboxed builder exists yet — **refuses**
# unless host execution has been granted. An earlier revision of this note said
# `lint` ran here with no equivalent gate, on the reasoning that it is your own
# toolchain over the tree you are standing in. The owner overruled that, and
# correctly: that the *toolchain* is yours does not make the *code* yours when
# you are linting a branch you are reviewing, and `cargo clippy` has `cargo
# check` semantics, so it runs that branch's build scripts here.
#
# So both commands now gate host execution, and they gate it differently on
# purpose. `security run --allow-unsandboxed` is invocation-only: there is no
# config key, and a run files its verdict into the store as a fact about the
# repository. `roteiro lint` also accepts a standing grant — `[lint]
# allow_unsandboxed` in the *user's own* config, never a committed `roteiro.toml`
# — because it stores nothing and a person may reasonably say once that builds on
# their own machine are fine. Do not collapse the two into one key without
# deciding, in an ADR, whether a standing grant should also reach `security run`;
# that would make an invocation-only gate reachable without the invocation.
#
# Keeping it a named feature is still worth it: `--no-default-features --features
# execution` builds a Roteiro that provisions and ingests but **cannot execute**,
# which is the shape a locked-down environment wants.
exec-subprocess = ["execution", "rto-exec/exec-subprocess"]
# The sandboxed analyzer backend: `roteiro security run` inside a digest-pinned
# OCI image in a microVM (ADR-0014). **Off by default.**
#
# Build requirements, which are real and not optional: `protoc >= 3.12` on the
# host, and a sandbox runtime provisioned and verified beforehand —
# `roteiro security prefetch --analyzer sandbox --allow-download`, then build
# with `BOXLITE_RUNTIME_URL` pointing at the verified copy. `rto-exec`'s build
# script refuses to proceed otherwise, and prints exactly what to run. See
# `crates/rto-exec/NOTICE-boxlite-runtime.md` for the third-party binaries this
# embeds and the licence duties they carry.
#
# It pulls in `exec-subprocess` for a reason that is CLI plumbing, not policy:
# `security run` is gated on that feature, and the sandboxed backend needs the
# same command. (`prefetch` and `status` are **not** — they moved to `execution`
# in the provisioning/execution split, and this comment kept saying all three
# afterwards. That gating is load-bearing now: `rto-exec/build.rs` tells a
# first-time installer that `cargo install roteiro` alone can provision the
# runtime *because* `prefetch` sits behind a default feature, so the two must not
# drift apart again.) Enabling it does **not** enable unsandboxed runs — those
# still require `--allow-unsandboxed` per invocation, and still record
# `isolation=none`. Worth untangling when the `security` command's feature gates
# are next revisited.
exec-boxlite = ["exec-subprocess", "rto-exec/exec-boxlite"]
mcp = ["rto-render/mcp", "dep:tokio"]
# The interactive workspace explorer's read-only `/v1/graph/*` JSON API, off by
# default. Pure Rust: adds only axum + tokio, never `rto-serve`/llama.cpp, so it
# builds, serves (`roteiro explorer`), and tests without a C/C++ toolchain. Also
# merged onto `/v1` when combined with `serve` (see `serve_v1_tail`).
explorer = ["dep:axum", "dep:tokio"]
# Offline similarity inference (`roteiro infer`) — the `inferred` provenance
# class. Adds no dependencies (pure-Rust hashing embedding; ADR-0003).
inference = ["rto-graph/inference"]
# The shared model machinery: the `roteiro model` list/pull commands and the
# consent-gated downloader (`ureq`), over rto-graph's candle-free registry.
# Enabled by every model tier so `roteiro model pull` works without candle, and
# **on by default** — see the `default` entry above for why. Being present is not
# being active: `pull` still fetches nothing without an explicit `[y/N]` yes (or
# `--yes`), and no other command opens a socket, so the default build remains
# offline-by-default in behaviour as well as in name.
models = ["rto-graph/models", "dep:ureq"]
# Pluggable local embedding/generative models (`roteiro infer --model`, `spec
# draft`) via the shared **llama.cpp** engine — GGUF embedding + generative
# models, no candle (ADR-0003 v1.1). Pulls `rto-llama` (a C/C++ toolchain).
inference-local-models = [
    "inference",
    "models",
    "dep:rto-llama",
    "rto-llama/llama",
]
# The remote model tier (ADR-0019) — Roteiro's **one explicitly-consented egress
# path**, and the first capability in the project that can send repository
# content off the machine.
#
# **Off by default, and it stays off.** No release may flip it. That is not a
# preference about defaults: ADR-0019 exempts this capability from Principle 10
# (a remote call is fetching by definition and can be neither digest-pinned nor
# prefetched), and the exemption is only tolerable because the capability is
# absent unless someone asked for it at build time *and* again in their own user
# config *and* again per invocation. `remote_is_not_a_default_feature` in
# `tests/remote_consent_cli.rs` reads this file and fails if `default` grows it.
#
# Enabling it adds **no third-party dependency**: `rto-remote` is a workspace
# crate over `rto-graph`, `serde` and `thiserror`, and it deliberately contains
# no HTTP client. `ureq` is already in the tree for `roteiro model pull` and
# `security prefetch`, and `rto-exec` is a workspace crate — so `cargo deny` and
# ADR-0017 have nothing new to look at, which is what ADR-0019 anticipated when
# it recorded that `ureq` "is already in the tree, so this need not add a new
# dependency closure".
#
# **Part 2 is what made a build with this feature able to send.** Part 1
# compiled the policy and no backend at all — the consent gate, the payload
# allow-list, the dry-run and the egress ledger — so that build could not send
# anything and the guard was reviewable on its own. `src/remote_transport.rs` is
# the socket, and it is still handed to `rto_remote::call_with` as a closure:
# the crate holding the policy has no HTTP client and must never acquire one,
# which stays checkable from `crates/rto-remote/Cargo.toml` rather than promised
# in prose.
#
# Three dependencies, each for one named reason:
#   * `rto-remote` — the policy: who consented, what may be sent, what came
#     back, what was recorded.
#   * `ureq` — the transport, in `remote_transport::call` and nowhere else.
#   * `rto-exec` — for `rfc3339_utc` alone, which stamps the ledger's entries
#     through `rto_remote::Clock`. Part 1 deliberately left this undeclared
#     because nothing then called `call_with`; that call site now exists. It
#     does **not** enable `execution`, so `--features remote` still yields a
#     binary with no analyzer surface.
#
# Part 2b adds a fourth, and it is a **resolver** dependency rather than a model
# one. The tier does not choose itself: `ModelSource::Remote` is a variant of the
# *shared* `rto_graph::model_choice` resolver, which is gated on
# `rto-graph/models`, and `spec draft` / Ask ask that resolver which backend
# serves them before either can send. Without it a `--features remote` build
# would hold a consent gate and a transport and nothing that decides when to use
# them.
#   * `models` — the shared resolver (and, incidentally, the registry and the
#     `roteiro model` commands, both already in `default`). It pulls no
#     llama.cpp, and it does **not** enable the tier: the user layer and the
#     invocation still have to grant, every run.
#
# Part 2c adds a fifth, and it adds **no toolchain**: `rto-llama` without its
# `llama` feature is the pure-Rust `Engine` trait and its request/result types,
# with no `build.rs` and no vendored C++. What this tier needs from it is
# `rto_llama::thinking` — the rule for reading a reasoning model's `<think>`
# block, which moved there in #582 because `rto-serve` needs it too and cannot
# depend on `roteiro`. A hosted reasoning model emits the same block as a local
# one, so `spec_draft_remote` reads it with the same function every other
# consumer does, rather than keeping a copy for the one build that could not see
# the original. Do **not** write this as `rto-llama/llama`: that would put
# cmake/libclang between a user and the remote tier, which is one of the costs
# this tier exists to avoid.
#   * `rto-llama` — the `<think>` rule and `FinishReason`. No C/C++ toolchain.
remote = ["dep:rto-remote", "dep:rto-exec", "dep:ureq", "dep:rto-llama", "models"]
# Ingest text from PDF documents during `roteiro sync` (off by default; pulls a
# large PDF/font-parsing tree). See [`rto_graph::extract`].
pdf-text = ["rto-graph/pdf-text"]
# OCR text from images during `roteiro sync` (ADR-0005 Tier A; off by default;
# pulls the pure-Rust ocrs/rten tree). Implies `models` so `roteiro model pull
# ocrs-text` can fetch the OCR models with consent.
image-ocr = ["models", "rto-graph/image-ocr"]
# Describe images with a local GGUF vision-language model during `roteiro sync`
# (ADR-0005 Tier B; off by default). Uses the shared llama.cpp engine via
# `rto-graph/image-vision` (smolvlm + mmproj) — no candle.
image-vision = ["models", "rto-graph/image-vision"]
# Transcribe spoken-word audio (wav/mp3/flac) during `roteiro sync` (Stage 18;
# off by default). Uses the shared llama.cpp engine via
# `rto-graph/audio-transcribe` (voxtral + audio mmproj) — no candle. Implies
# `models` so `roteiro model pull voxtral-mini-3b` can fetch it.
audio-transcribe = ["models", "rto-graph/audio-transcribe"]
# Extract audio metadata — codec, sample rate, bit depth, channels, duration, tags
# — as `derived` facts during `roteiro sync` (ADR-0016; off by default). Reads the
# container with `symphonia`; **no model, no network and no C/C++ toolchain**, so
# unlike `audio-transcribe` this one costs a compile and nothing else. Implies
# neither `models` nor `audio-transcribe`: extraction and generation are separate
# stories (ADR-0015 / ADR-0016).
audio-metadata = ["rto-graph/audio-metadata"]
# Local model serving (`roteiro serve --models`): the opt-in, loopback,
# OpenAI-compatible `/v1` endpoint over installed models (ADR-0006). Pulls
# `rto-serve` with its llama.cpp engine; also enables the internal llama.cpp uses
# (`infer --model`, `spec draft`) via `inference-local-models`.
serve = ["dep:rto-serve", "rto-serve/llama", "rto-serve/tls", "inference-local-models", "dep:tokio"]

[lints]
workspace = true