[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81819":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":8,"htmlUrl":8,"language":9,"languages":8,"totalLinesOfCode":8,"stars":10,"forks":11,"watchers":12,"openIssues":13,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":13,"stars7d":12,"stars30d":12,"stars90d":14,"forks30d":14,"starsTrendScore":15,"compositeScore":16,"rankGlobal":8,"rankLanguage":8,"license":8,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":17,"hasPages":17,"topics":19,"createdAt":8,"pushedAt":8,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":14,"starSnapshotCount":14,"syncStatus":13,"lastSyncTime":23,"discoverSource":24},81819,"loupe","project-loupe\u002Floupe","project-loupe",null,"Rust",37,8,4,2,0,6,43.26,false,"main",[],"2026-06-12 04:01:35","# loupe\n\nA security-scanning harness for source repositories. `loupe` runs LLM\nagents (and, in future milestones, fuzzers and other tooling) over a\ncodebase, lets each agent self-validate its findings (write a\nregression-test PoC, check it applies), and dispatches confirmed\nfindings to the configured reporter so they show up where the rest of\nthe team's bugs live.\n\nThe system is split into three components that talk to each other over\nmTLS:\n\n- **`loupe-server`** — long-running daemon. Holds the SQLite database\n  (registered repos, jobs, findings, secrets), runs the scheduler, hands\n  out leases, accepts findings + verdicts, and dispatches confirmed\n  findings to the configured reporter — today: GitHub issues, email\n  via sendmail, or no reporter at all (manual triage via `loupectl`).\n- **`loupe-worker`** — fleet of stateless workers. Authenticate with the\n  server using a client cert minted at registration time, lease a job,\n  clone the repo into a local cache, run the configured scanners, and\n  submit findings back. A worker can also serve cross-model verification\n  jobs by advertising a `verify:*` capability.\n- **`loupectl`** — operator CLI. Authenticates with the admin client\n  cert produced by `loupe-server init` and exposes the things you'd\n  otherwise be doing by hand: register repos, mint worker certs, trigger\n  scans, inspect findings.\n\nFor the architecture in one page (component diagram, data lifecycle,\nmTLS topology), see `ARCH.md`.\n\nJoin the Project Loupe Discord: https:\u002F\u002Fdiscord.gg\u002Fd4Z58kTZF4\n\n## Prerequisites\n\nBefore installing, the host needs:\n\n- **Rust** (stable toolchain). Nightly is only required if you intend to\n  run `cargo fmt` — `rustfmt.toml` uses nightly-only options. CI runs\n  `fmt` on nightly and `clippy`\u002F`test` on stable.\n- **`git`** on PATH. `loupe-worker` shells out to `git` for repo\n  cloning into the local cache.\n- **`bubblewrap`** (`bwrap`) on PATH on every machine running\n  `loupe-worker` *with the LLM scanner enabled*. The worker\n  hard-fatals at startup if the LLM scanner is on but `bwrap` is\n  missing — set `LOUPE_DISABLE_SANDBOX=1` to override on dev\n  machines that genuinely cannot install it. Debian\u002FUbuntu:\n  `sudo apt-get install bubblewrap`. Fedora\u002FRHEL: `sudo dnf install\n  bubblewrap`. macOS does not have a port; LLM scanning runs on Linux\n  workers only.\n- **`claude` CLI** on PATH on every machine running `loupe-worker`\n  *with the LLM scanner enabled*. The discovery backend shells out to\n  `claude --dangerously-skip-permissions -p`, with the worker's\n  bubblewrap mount keeping each invocation's `\u002Ftmp` and `$HOME` fresh.\n  See https:\u002F\u002Fgithub.com\u002Fanthropics\u002Fclaude-code for install\n  instructions.\n- **`codex` CLI** (optional) on PATH on every machine running\n  `loupe-worker` *with the LLM verifier enabled*. The verifier prefers\n  codex so the cross-model second opinion comes from a different model\n  family than discovery; falls back to `claude` if `codex` isn't\n  installed. The verifier shells out to `codex exec\n  --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check`.\n  For API-key auth, set `CODEX_API_KEY`; `OPENAI_API_KEY` remains a\n  compatibility alias in the Docker deploy helper.\n  See https:\u002F\u002Fgithub.com\u002Fopenai\u002Fcodex for install instructions.\n- **`bkb-mcp`** (optional) on PATH on workers scanning bitcoin \u002F\n  lightning \u002F cashu codebases. When the binary is present at startup,\n  the discovery agent's per-call MCP config gets a second server\n  entry exposing the bkb tool surface (`bkb_search`, `bkb_lookup_bip`,\n  `bkb_lookup_bolt`, `bkb_lookup_lud`, `bkb_lookup_nut`,\n  `bkb_lookup_blip`, `bkb_find_commit`, `bkb_get_document`,\n  `bkb_get_references`, `bkb_timeline`) so the agent can pull spec +\n  historical context the worktree alone won't carry. Install with\n  `cargo install bkb-mcp`. The worker sets `BKB_API_URL` to\n  `https:\u002F\u002Fbitcoinknowledge.dev` (the public hosted instance) for\n  every spawn by default; operators pointing at a self-hosted BKB\n  instance can override `[bkb].api_url` in the worker config. Absence\n  is silent: workers without bkb-mcp run normally and the agent's\n  prompt doesn't mention bkb at all.\n- **A GitHub personal access token** for each target tracker repo,\n  only if you intend to use the GitHub-issue reporter (skip this\n  prereq when registering repos with `--no-reporting` for manual\n  triage). The GitHub-issue reporter has no extra prereq beyond\n  outbound HTTPS to `api.github.com`. The token is\n  used by the server to call `POST \u002Frepos\u002F{owner}\u002F{repo}\u002Fissues`, so\n  it needs scope to file issues on the *tracker* repo (not the source\n  repo being scanned — those can be different). Required scopes:\n  - **Fine-grained PAT** (recommended): repository access scoped to\n    the tracker repo, with the **Issues** permission set to\n    *Read and write*.\n  - **Classic PAT**: the `repo` scope. (`public_repo` is enough if\n    the tracker repo is public.)\n  PATs are stored in the `secrets` table inside an\n  SQLCipher-encrypted SQLite file. The whole database — secrets,\n  findings (descriptions, PoCs, suggested fixes), repo metadata,\n  audit trails — is sealed with AES-256 + HMAC-SHA512 under\n  `loupe-server`'s master key, so an attacker reading\n  `loupe.sqlite` off disk gets ciphertext for every row. The master\n  key is mandatory (the server refuses to start without one);\n  `loupe-server init` mints it the first time you bootstrap a data\n  dir.\n- **A sendmail-compatible local mailer** on the server host, only if\n  you intend to use the email reporter. The built-in reporter shells\n  out to `\u002Fusr\u002Fsbin\u002Fsendmail -t -i` and writes an RFC 5322 message on\n  stdin; a local MTA or wrapper such as postfix, msmtp, or nullmailer\n  needs to own delivery.\n\n## Building\n\n```\ncargo build --workspace --release\n```\n\nThe binaries land in `target\u002Frelease\u002F`:\n\n- `target\u002Frelease\u002Floupe-server` (daemon)\n- `target\u002Frelease\u002Floupe-worker` (worker)\n- `target\u002Frelease\u002Floupectl` (admin CLI)\n\n`cargo test --workspace --all-targets` runs the unit and integration\ntest suites; the LLM-backend live test skips automatically when\n`claude` is not on PATH, and the bubblewrap integration tests skip\nwhen `bwrap` is missing.\n\n## Quickstart\n\nThe walkthrough below assumes a single host running both the server\nand one worker, talking to `127.0.0.1:8443`. Multi-host deployments\nfollow the same shape — copy the worker's cert bundle to the worker\nhost, set `LOUPE_SERVER_URL` to the server's hostname, and make sure\nthe server cert's SAN list (`--hostname` at init time) covers it.\n\n### 1. Bootstrap the data directory\n\n```\nloupe-server init --data-dir \u002Fvar\u002Flib\u002Floupe --hostname loupe.example.internal\n```\n\nThis mints the internal CA, the server cert, the admin client cert,\n**and** the database master key (32 random bytes, hex-encoded);\nwrites `ca.pem`, `ca.key`, `server.pem`, `server.key`, `admin.pem`,\n`admin.key`, and `master.key` under the data dir with `0600` perms;\nand prints the admin client cert + key on stdout. Save the admin\nbundle somewhere you can reach with `loupectl` — `init` is the only\ntime the admin key leaves the machine.\n\nIf `LOUPE_MASTER_KEY` is already set in the environment when you run\n`init` (e.g. you're managing the key in a secret store \u002F systemd\ncredentials \u002F vault), `init` uses it as-is and does **not** write a\n`master.key` file. That keeps the env var the source of truth for\noperators who don't want the key on disk at all.\n\n`init` refuses to run against an already-initialised data dir.\n\n### 2. Run the server\n\n```\n# Source the master key. Either point the server at the on-disk file:\nexport LOUPE_MASTER_KEY=\"$(cat \u002Fvar\u002Flib\u002Floupe\u002Fmaster.key)\"\n# …or load from a secret manager and skip persisting to disk:\n# export LOUPE_MASTER_KEY=\"$(systemd-creds cat loupe-master)\"\n\nloupe-server serve \\\n  --bind 127.0.0.1:8443 \\\n  --db \u002Fvar\u002Flib\u002Floupe\u002Floupe.sqlite \\\n  --server-cert \u002Fvar\u002Flib\u002Floupe\u002Fserver.pem \\\n  --server-key  \u002Fvar\u002Flib\u002Floupe\u002Fserver.key \\\n  --ca-cert     \u002Fvar\u002Flib\u002Floupe\u002Fca.pem \\\n  --ca-key      \u002Fvar\u002Flib\u002Floupe\u002Fca.key\n```\n\nIf you'd rather have the server read the key from the on-disk file\nitself, drop `LOUPE_MASTER_KEY` and pass `--master-key-file\n\u002Fvar\u002Flib\u002Floupe\u002Fmaster.key` (also `LOUPE_MASTER_KEY_FILE`) instead.\nThe env var still takes precedence when both are set. The server\nrefuses to start if neither source supplies a key — there's no\nplaintext-mode fallback because the database itself is sealed.\n\nAll flags also accept the matching `LOUPE_*` env vars (`LOUPE_BIND`,\n`LOUPE_DB`, `LOUPE_SERVER_CERT`, etc.).\n\n#### Or: keep settings in `config.toml`\n\nAnything you'd otherwise pass on the command line can live in a TOML\nconfig file (a sample ships in `contrib\u002Fconfig.toml`). Drop it next to\nthe data directory and point the server at it:\n\n```\ncp contrib\u002Fconfig.toml \u002Fvar\u002Flib\u002Floupe\u002Fconfig.toml\n$EDITOR \u002Fvar\u002Flib\u002Floupe\u002Fconfig.toml      # adjust to taste\n\nloupe-server serve --config \u002Fvar\u002Flib\u002Floupe\u002Fconfig.toml\n```\n\nPath-typed fields under `[paths]` are interpreted relative to the\nconfig file's directory, so a single file can ship next to the certs\nand database without absolute paths. The master key path can also\nlive under `[paths] master_key`; the env var still wins on conflict\nso `LOUPE_MASTER_KEY` overrides the file. CLI flags and `LOUPE_*`\nenv vars override anything the file supplies, so a typical deploy\nkeeps stable settings in `config.toml` and uses the env to flip\nper-environment knobs.\n\n### 3. Point `loupectl` at the server\n\n```\nexport LOUPE_SERVER_URL=https:\u002F\u002F127.0.0.1:8443\nexport LOUPE_CA_CERT=\u002Fvar\u002Flib\u002Floupe\u002Fca.pem\nexport LOUPE_ADMIN_CERT=\u002Fvar\u002Flib\u002Floupe\u002Fadmin.pem\nexport LOUPE_ADMIN_KEY=\u002Fvar\u002Flib\u002Floupe\u002Fadmin.key\n\nloupectl repo list   # sanity check — empty list, no error\n```\n\n### 4. Mint a worker bundle\n\n```\nloupectl worker register --name worker-01 --out \u002Fetc\u002Floupe\u002Fworker-01.json\n```\n\nThe output JSON carries a fresh client cert + key + the CA cert. The\nkey is **only** ever returned here — the server doesn't keep a copy.\n\nPull the three PEMs out for the worker process:\n\n```\njq -r .client_cert_pem \u002Fetc\u002Floupe\u002Fworker-01.json > \u002Fetc\u002Floupe\u002Fworker.pem\njq -r .client_key_pem  \u002Fetc\u002Floupe\u002Fworker-01.json > \u002Fetc\u002Floupe\u002Fworker.key\njq -r .ca_cert_pem     \u002Fetc\u002Floupe\u002Fworker-01.json > \u002Fetc\u002Floupe\u002Fca.pem\nchmod 600 \u002Fetc\u002Floupe\u002Fworker.key\n```\n\n### 5. Run a worker\n\n```\nloupe-worker \\\n  --server-url https:\u002F\u002F127.0.0.1:8443 \\\n  --ca-cert    \u002Fetc\u002Floupe\u002Fca.pem \\\n  --cert       \u002Fetc\u002Floupe\u002Fworker.pem \\\n  --key        \u002Fetc\u002Floupe\u002Fworker.key \\\n  --cache-dir  \u002Fvar\u002Flib\u002Floupe\u002Fcache\n```\n\nWorker settings can also live in TOML:\n\n```bash\ncp contrib\u002Fworker-config.toml \u002Fetc\u002Floupe\u002Fworker.config.toml\n$EDITOR \u002Fetc\u002Floupe\u002Fworker.config.toml\nloupe-worker run --config \u002Fetc\u002Floupe\u002Fworker.config.toml\n```\n\nThe worker config owns non-secret runtime settings: server URL, TLS\nfile paths, cache settings, logging, Claude\u002FCodex model + effort,\nscanner defaults, and BKB API URL. CLI flags and matching env vars\noverride the config. API keys and PEM contents still belong in env or\nsecret files.\n\nThe worker auto-detects authenticated `claude` and `codex` CLIs at\nstartup and wires the LLM scanners accordingly:\n\n- **authenticated `claude`** → discovery scanner advertises `scan:llm`\n  (claude owns submission via the loupe MCP server's\n  `submit_finding` tool).\n- **authenticated `claude` or `codex`** → verifier scanner advertises\n  `verify:llm`. Codex is preferred when both are ready so the second\n  opinion comes from a different model family than discovery; claude\n  is the fallback when codex is not ready.\n- **No authenticated agent CLI** → worker refuses to start. A\n  \"regex-only\" loupe-worker isn't a deployment we want operators to\n  fall into by accident; install at least one agent CLI and provide\n  its API key or login state.\n\n> **Note:** scan jobs use LLM providers and may count against paid,\n> metered, or rate-limited usage. The discovery scanner currently uses\n> `claude-cli` and can launch one Claude agent session per discovered\n> source file, so large repositories may trigger hundreds or thousands\n> of Claude CLI invocations. `codex` is used for verifier jobs after a\n> finding already exists. Actual cost or quota impact depends on the\n> provider, model, account plan, retries, failed-call accounting, and\n> token usage. Run a small test repository or narrow scanner\n> configuration first if usage limits matter.\n\nThe worker also probes for `bwrap` at startup and exits 1 if it is\nmissing (set `LOUPE_DISABLE_SANDBOX=1` to bypass for dev work).\nCache size defaults to 40 GB and evicts LRU clones above the cap.\n\nVerifier jobs only get queued when a repo resolves to\n`verification_enabled = true`, either because it was registered with\n`--verification-enabled` or because the server's verification default\nis on.\n\n#### Deploy with containers\n\nProduction deployment now lives under `contrib\u002Fdocker\u002F`. The supported\npath is rootful Podman managed by systemd, with server\u002Fworker secrets\npersisted in one protected env file per host and mounted read-only into\nthe containers. Secrets are not written into systemd units or Podman env\nmetadata, so normal systemd restarts and host reboots keep working.\n\nSee `contrib\u002Fdocker\u002FREADME.md` for fresh Debian host prerequisites,\nimage builds, two-host deployment, restart behaviour, and the exact\nsecret-handling model.\n\n### 6. Register a repo and trigger a scan\n\nThe `--pat` value here is the GitHub PAT you minted in the\nprerequisites: a fine-grained token with **Issues: Read and write**\non the *tracker* repo, or a classic token with the `repo` scope.\nPass it via the `LOUPE_TRACKER_PAT` env var rather than as a\npositional flag so it doesn't end up in shell history. The server\nencrypts it at rest with the master key (see prerequisites) before\npersisting; the plaintext PAT never travels back out of the server in\nany response.\n\n```\nexport LOUPE_TRACKER_PAT=ghp_xxx_with_issues_write_scope\n\nloupectl repo add \\\n  --clone-url     https:\u002F\u002Fgithub.com\u002Facme\u002Fwidget.git \\\n  --target-owner  acme \\\n  --target-repo   widget-security \\\n  --pat           \"$LOUPE_TRACKER_PAT\" \\\n  --scan-interval-seconds 86400      # optional; daily\n\nloupectl repo list\nloupectl repo scan 1                 # one-shot scan of repo id 1\n```\n\nAdd `--verification-enabled` if this repo should route scan findings\nthrough verifier jobs before reporting. If the server-wide verification\ndefault is on, omit it to inherit the default, or pass\n`--no-verification` to opt this repo out.\n\nConfirmed findings dispatch automatically — the GitHub reporter\nreads the PAT out of the secrets table (transparently decrypted by\nSQLCipher when the row is fetched) and posts to\n`https:\u002F\u002Fapi.github.com\u002Frepos\u002Facme\u002Fwidget-security\u002Fissues`, stamping\n`reported_at` on the finding row.\n\n#### Or: email reporting\n\nThe server also has an email reporting destination on the wire:\n`ReportingSetup::Email { to, from, subject_prefix }`. It sends\nconfirmed findings through the server host's sendmail-compatible\nbinary and does not require a PAT or other reporter secret.\n\n`loupectl repo add` does not expose email flags yet, so registering an\nemail-backed repo currently means calling `POST \u002Fv1\u002Frepos` with an\nadmin mTLS client or using a small client built on `loupe-proto`.\nOnce registered, the scan, verification, approval, and dispatch flow\nis the same as the GitHub reporter.\n\n#### Or: scan-only mode (no tracker)\n\nIf you want to use loupe purely as a \"find issues, queue them for me\"\nsystem — no tracker repo, no automatic GitHub issue creation, just\na queue you triage with `loupectl finding ...` and act on\nout-of-band — pass `--no-reporting`:\n\n```\nloupectl repo add \\\n  --clone-url https:\u002F\u002Fgithub.com\u002Facme\u002Fwidget.git \\\n  --no-reporting\n```\n\nThe full pipeline (scan → optional verify → approval gate) runs as\nusual, but with no reporter configured the dispatcher leaves confirmed\nfindings in state `confirmed`. You can either handle them out-of-band,\nor configure reporting later and retry delivery:\n\n```\nloupectl repo set-github-reporting \u003Crepo-id> \\\n  --target-owner acme \\\n  --target-repo widget-security\n\nloupectl finding retry-report \u003Cfinding-id>\n```\n\nReject still moves a held finding to terminal `dismissed`.\n\n### 7. Inspect what happened\n\n```\nloupectl job list\nloupectl job get  \u003Cjob-id>\nloupectl job retry \u003Cjob-id>                # requeue a failed job\nloupectl finding list \u003Crepo-id>\nloupectl finding show \u003Cfinding-id>          # pretty-printed for human review\nloupectl finding show \u003Cfinding-id> --json   # raw FindingDetail DTO\nloupectl finding search \u003Crepo-id> \"\u003Ckeywords>\"  # FTS5 keyword search\n```\n\n`finding search` is also reachable from inside the LLM scanner — the\nMCP tool `query_prior_findings` calls the same endpoint, so the\nagent can ask \"have we seen anything like this before?\" mid-scan.\n\n#### Continuous scans\n\nWhen you set `--scan-interval-seconds`, loupe runs the scan periodically\nwithout operator intervention. Two complementary dedup mechanisms\nkeep re-scans cheap:\n\n- **Semantic dedup (agent-driven):** every discovery session has the\n  `query_prior_findings` and `get_finding_by_id` MCP tools. The\n  prompt asks the agent to enumerate *every* exploitable bug in the\n  file (severity-ordered) and search for prior reports before\n  submitting each — a duplicate hit suppresses *that one* candidate\n  and the agent moves on to the next, so a re-scan still surfaces\n  bugs ranked below an already-reported finding. Catches paraphrases,\n  refactor-shifted bugs (function moved to a different file), and\n  renamed functions. Conservative — only suppresses on a clear match.\n- **Hash dedup (free, server-side):** every finding carries a\n  `blake3(scanner_id | file | normalized_content_window)`\n  fingerprint. The `findings` table has `UNIQUE(repo_id,\n  fingerprint)`, so any submission that hash-matches an existing\n  row is silently dropped at insert (`INSERT OR IGNORE`). Survives\n  `cargo fmt`-style cosmetic edits because the hash normalises\n  whitespace and case. This is the deterministic floor under the\n  agent's semantic decisions.\n\nTo verify dedup is working: run `loupectl repo scan \u003Cid>` twice in\na row and compare the new-finding counts in `loupectl finding list\n\u003Crepo-id>` between the two jobs — the second run shouldn't add rows\nthe first one already covered.\n\n### 8. Adjust an existing repo\n\n```\nloupectl repo update \u003Cid> --disable                  # pause scheduler\nloupectl repo update \u003Cid> --enable\nloupectl repo update \u003Cid> --interval 3600            # hourly\nloupectl repo update \u003Cid> --verification-enabled     # route via verify flow\nloupectl repo update \u003Cid> --no-verification          # skip verify; dispatch on insert\nloupectl repo update \u003Cid> --require-approval         # hold for human sign-off\nloupectl repo update \u003Cid> --no-require-approval      # opt out of the approval gate\nloupectl repo update \u003Cid> --inherit-approval         # fall back to the server default\n```\n\nThe clone URL and reporting destination are deliberately *not*\npatchable: silently re-pointing where new findings get filed is too\neasy a footgun. Re-register the repo if you need to change either.\n\n## Human-in-the-loop approval\n\nBy default, confirmed findings dispatch immediately. For repos where\nyou want a human to read the finding before an issue is filed, turn on\nthe approval gate. Two layers compose:\n\n- **Per-repo `require_approval`** (`loupectl repo add --require-approval`,\n  or `loupectl repo update \u003Cid> --require-approval`). Pinning it\n  `true` always holds; pinning it `false` always dispatches; leaving\n  it unpinned (`--inherit-approval` clears the override) falls back\n  to the server default.\n- **Server-wide default `require_approval_default`** in\n  `config.toml`'s `[policy]` section, or via the\n  `--require-approval-default` flag \u002F `LOUPE_REQUIRE_APPROVAL_DEFAULT`\n  env. Off by default. Per-repo overrides win.\n\nWhen the gate is active, a confirmed finding (auto-pass or\nverifier-confirmed) parks in state `awaiting_approval` instead of\nhitting the reporter. The operator handles it with:\n\n```\nloupectl finding list \u003Crepo-id>                 # state=awaiting_approval\nloupectl finding show \u003Cfinding-id>              # pretty: title, severity,\n                                                #   location, description,\n                                                #   PoC diff (regression test\n                                                #   that fails on HEAD)\nloupectl finding show \u003Cfinding-id> --json       # raw DTO for scripting\nloupectl finding approve \u003Cfinding-id>           # → confirmed → dispatched\nloupectl finding retry-report \u003Cfinding-id>      # retry a confirmed finding\nloupectl finding reject  \u003Cfinding-id>           # → terminal dismissed\n```\n\n`finding show` is the review surface: it renders the model's\ndescription, the location of the suspect code, and — most\nimportantly — the **proof-of-concept regression test** as a unified\ndiff (with `+`\u002F`-` colored like `git diff` when stdout is a TTY). The\nPoC is the strongest evidence the finding is real: applying the diff\nagainst a fresh worktree and running the test should fail on HEAD.\n`--json` falls back to the raw `FindingDetail` DTO when you need\nmachine-readable output. `NO_COLOR=1` (or piping into a non-TTY)\nsuppresses ANSI escapes.\n\n`approve` runs the dispatcher synchronously when a reporter is\nconfigured. Without a reporter, the finding stays `confirmed`; add\nreporting with `repo set-github-reporting`, then run\n`finding retry-report`. `reject` is terminal; the audit columns\n`approved_by_cn` \u002F `rejected_by_cn` record the admin client cert's\n`workers.name` so dashboards can later answer \"who clicked what\". A\nverifier-issued `dismiss` and a human `reject` both land on\n`state = 'dismissed'`, but only the human path stamps `rejected_*`.\n\n## Verification flow (cross-model second opinion)\n\nSetting `verification_enabled = true` on a repo causes scan-time\nfindings to land in `validating` state with one `kind=verify` job\nenqueued per finding. You can set it per repo with\n`loupectl repo add --verification-enabled` or\n`loupectl repo update \u003Cid> --verification-enabled`.\n\nFor verifier-first deployments, set the server-wide\n`verification_default` in `config.toml`'s `[policy]` section,\nor pass `--verification-default` \u002F set\n`LOUPE_VERIFICATION_DEFAULT=true`. New repo registrations that\ndo not pass either `--verification-enabled` or `--no-verification`\ninherit that default. Existing repos keep their stored value; update\nthem explicitly if you change the server default later.\n\nThe verify job is leased by a worker advertising a `verify:*`\ncapability, which runs an independent LLM pass over the finding and\nsubmits a `confirm | dismiss | inconclusive` verdict. The server\napplies a rollup policy in-transaction (any `dismissed` → finding\n`dismissed`; else any `confirmed` → `confirmed` + dispatch; else stay\nin `validating`). The full state machine + reaper details are in\n`ARCH.md` and the `submit_verdict` \u002F `complete` handlers in\n`crates\u002Floupe-server\u002Fsrc\u002Froutes\u002Fjobs.rs`.\n\nA worker with `codex` (or just `claude`) on PATH advertises\n`verify:llm` automatically — see step 5 for backend selection. A\ndeployment can run discovery and verifier on the same worker, on\nseparate workers, or share a single worker with both — the lease\nloop matches by capability, not by binary. To force role separation,\ninstall only `claude` on the discovery hosts and only `codex` on the\nverifier hosts; the auto-detect picks the matching capability tags.\n\n## Continuous integration\n\nGitHub Actions (`.github\u002Fworkflows\u002Fci.yml`) runs three jobs on every\npush and pull request:\n\n- **fmt** — `cargo fmt --all -- --check` on a nightly toolchain.\n- **clippy** — `cargo clippy --workspace --all-targets --all-features\n  -- -D warnings` on stable.\n- **test** — `cargo test --workspace --all-targets` on stable.\n\n## Layout\n\n```\ncrates\u002F\n  loupe-core      shared types: Finding, Verdict, ReportingDestination\n  loupe-proto     wire-format DTOs (versioned protocol, X-Loupe-Protocol)\n  loupe-tls       internal CA + cert minting + fingerprint helpers\n  loupe-storage   SQLCipher DAO surface, FTS5 index, schema-versioned migrations\n  loupe-server    daemon binary + mTLS routes + reporters + scheduler\u002Freaper\n  loupe-worker    worker binary (`run` + `mcp-serve` subcommands) +\n                  scanner trait + LLM backend + versioned MCP tool surface +\n                  bwrap sandbox\n  loupe-cli       loupectl admin CLI\n```\n\nSee each crate's module-level docs for the design intent, and\n`ARCH.md` for the cross-crate flow at a glance.\n","loupe 是一个用于源代码仓库的安全扫描工具。它通过运行LLM代理（未来版本将支持模糊测试和其他工具）来扫描代码库，并让每个代理自行验证其发现的问题（如编写回归测试PoC并检查其适用性），然后将确认的问题发送给配置的报告器，以便开发团队能在他们通常处理bug的地方看到这些安全问题。该项目使用Rust语言开发，由三个主要组件构成：`loupe-server`作为长期运行的守护进程管理数据库、调度任务和分发结果；`loupe-worker`是无状态的工作节点，负责执行实际的扫描任务；`loupectl`则为操作员提供了命令行界面以进行管理和控制。此项目适用于需要对代码库进行全面安全审查的企业或组织，特别是那些希望自动化这一过程并集成到现有工作流中的团队。","2026-06-11 04:06:50","CREATED_QUERY"]