SRouter
AGENTS.md24 starsChanged 16 days agoMITView on GitHub
SRouter — a self-hosted, multi-provider LLM gateway ("Multi-Provider OpenAI & Anthropic Compatible LLM Gateway", server/src/app.rs:54). It exposes OpenAI-style (/v1/chat/completions, /v1/chat), Anthropic-style (/v1/messages), and image (/v1/images/generations) endpoints, authenticates callers, routes each model id onto a per-vendor provider driver, streams SSE back, logs every request to SQLite, and serves an operator API plus an optional SPA dashboard. Two independently-toolchained halves in one repo, sharing only generated TypeScript bindings: There is no workspace root manifest. The root carries the container entry points (Dockerfile,…
- Reads credentials
We split this AGENTS.md into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 32
Read this before touching a file.
- Distinctive: line 36
Do the requested change, completely, and nothing else: No unrequested features, refactors, dependencies, or "while I'm here" tidy-ups.
- Very specific: line 56
Verify by running, not by reasoning: Every behavioural change gets exercised on the real path — the handler, the CLI, or the browser.
- Distinctive: line 57
State exactly what you ran: Never claim coverage you did not exercise, never describe an untested path as working, and never report success …
- Distinctive: line 58
Report a blocker; do not route around it: If the requested change cannot be made safely, say which invariant it breaks and what you tried — …
- Very specific: line 129
Run from the repo root. Server (verbatim from CONTRIBUTING.md:12-37):
- Very specific: line 165
All client-visible strings/codes/headers come from server/src/constants.rs — never inline literals in handlers.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Repository Guidelines
Project Overview
SRouter — a self-hosted, multi-provider LLM gateway ("Multi-Provider OpenAI & Anthropic Compatible LLM Gateway", server/src/app.rs:54). It exposes OpenAI-style (/v1/chat/completions, /v1/chat), Anthropic-style (/v1/messages), and image (/v1/images/generations) endpoints, authenticates callers, routes each model id onto a per-vendor provider driver, streams SSE back, logs every request to SQLite, and serves an operator API plus an optional SPA dashboard.
Two independently-toolchained halves in one repo, sharing only generated TypeScript bindings:
| Half | Stack | Notes |
server/ | Rust edition 2024, axum 0.8, tokio, sqlx 0.9 (SQLite only), specta | The real product; behaviorally ported from a deleted Node apps/api |
client/ | Bun + Vite + React 19, TanStack Router/Query, Tailwind v4, shadcn (base-mira on @base-ui/react) | Early-stage SPA: auth spine (login/first-run setup), provider management, request logs, and usage/quota are live; keys and settings are still static mockups |
There is no workspace root manifest. The root carries the container entry points (Dockerfile, .dockerignore, docker-compose.yml, Makefile), and .github/workflows/ holds the CI plus the manual GHCR publish. cargo is the whole server build system.
Architecture & Data Flow
Boot (server/src/main.rs): dotenvy::dotenv() → telemetry::init() → APIConfig::from_env_map → AppDatabase::connect (runs migrations to SCHEMA_VERSION = 7) → bootstrap_admin_account_from_env → ProviderRegistry::with_database + register_custom_providers → AppState::with_security(...).with_database(...) → spawns catalog warm-up and a 60 s OAuth token sweeper → listeners::serve_main(create_router(state), 0.0.0.0:PORT).
Request: axum router (server/src/app.rs create_router) → per-mount guards → feature handler → model resolution → provider driver → UpstreamClient (reqwest) → SSE/JSON back out; interception/logging.rs writes the request log and settles quota best-effort.
Composition root / DI: manual, no container. AppState + SecurityState (server/src/state.rs) hold Arc<dyn Trait> stores; middleware attaches Extension<APIPrincipal>; handlers extract State, Extension, Path/Query, Request. Empty* store impls + SecurityState::unconfigured() keep a DB-less boot coherent.
Gateway pipeline (server/src/features/gateway/): routes.rs → chat.rs / messages.rs / images.rs → body parse (protocol/model.rs) → ensure_model_allowed_any (api_keys/access.rs) → token_saver → reserve_api_key_quota → ProviderRegistry::resolve(model) → ProviderAdapter/ProviderExecutor → upstream call. Streaming uses mpsc + ReceiverStream; provider failures become in-stream SSE error events (adapter.rs), never stream errors. interceptor.rs intercepts built-in search tool calls the client did not declare.
Router layering rules (critical, in app.rs): guards are applied per mount — read surfaces get api_key_auth, operator surfaces get require_admin_session + CSRF, OAuth callbacks are public, admin-auth routes enforce session per handler. /v1 and /v1/v1 nests each own a JSON 404 fallback so they never fall through to the SPA. Global layers, last-added = outermost: log_access → log_failed_requests → security_headers → cors. Root route + SPA fallback must be registered before the layers (Router::layer only wraps existing routes).
Bindings flow: Rust wire types derive Serialize + specta::Type → server/src/bindings.rs registry → cargo run --bin export_ts → writes both server/bindings.ts and client/src/generated/typed.ts. The client imports them through client/src/api/types.ts.
Non-Negotiable Rules
Read this before touching a file. Nothing here is enforced by tooling; it is enforced by review. Standard is a senior engineer who owns the code for the next six months, not one who ships the fastest diff.
Scope
- Do the requested change, completely, and nothing else. No unrequested features, refactors, dependencies, or "while I'm here" tidy-ups. Reduce scope only with explicit approval; never silently.
- A diff is the unit of work. It compiles, runs, and is verifiable on its own. Rename a symbol → every callsite lands in the same change, and the obsolete version leaves with it. No shims, re-exports, or deprecation aliases.
- Leave no scaffolding. No
TODO/FIXMEmarker, stub, mock, placeholder, or unreachable branch in shipped code.server/srcandclient/srccontain zero today — keep it that way. (Four comments infeatures/provider_auth/cite aTODO.mddeleted in233b7a6; that is a stale pointer, not a marker — the same class as the deaddocs/*.mdcitations, and not a pattern to copy.)
Design
- No over-engineering. The entire server defines 5 traits (
APIKeyStore,APIKeyRepository,SessionStore,AdminAuthRepository,ProviderExecutor) and no DI container; the client has 19 runtime dependencies. Do not add an abstraction, generic parameter, trait, registry, plugin point, feature flag, or dependency until a second concrete caller exists. Three repeated lines beat a premature helper. - Reuse the vocabulary that exists.
client/src/components/ui/(shadcn/Base UI),APIError+constants.rson the server,TestDatabasein tests,create_*_routerfor mounts. A second convention for a job that already has one is the defect — even when the new one is nicer. - Match the file you are in. Same naming, module layout, error style, and comment style as its neighbours. Read the surrounding 50 lines before adding to anything.
- Comments explain why, never what. Record the trap that motivated the line (see
client/vite.config.tsonchangeOrigin,state.rsonunconfigured). No narration, no section banners, no restating the next line.
Correctness
- No
unwrap()on a production path. The codebase has exactly one (providers/registry.rs:282) and clippy will not flag yours. Propagate with?intoAPIError; useexpect("...")only where failure is provably impossible (static parse, literal) and always with a message. Lock poisoning is recovered, not unwrapped:.unwrap_or_else(|e| e.into_inner()). - One error type.
APIErrorwith builders, propagated by?. Noanyhow,thiserror,Box<dyn Error>outsidemain, and no per-module error enums. - Every client-visible string, code, and header name comes from
server/src/constants.rs. Never inline a message in a handler; interpolated messages become functions. - Frozen behaviour stays frozen. Node-parity semantics, generated files,
[profile.dev] debug = 1, per-mount guard placement, global layer order, and the frozen test contracts are all deliberate. If a change seems to require altering one, stop and report it rather than "fixing" it.
Evidence
- Verify by running, not by reasoning. Every behavioural change gets exercised on the real path — the handler, the CLI, or the browser. Tests alone are not proof. A green
cargo checkortscproves only that it compiles. - State exactly what you ran. Never claim coverage you did not exercise, never describe an untested path as working, and never report success from a command you skipped. Unverified is a valid answer; "probably fine" is not.
- Report a blocker; do not route around it. If the requested change cannot be made safely, say which invariant it breaks and what you tried — do not silently weaken a guard, widen a scope, or delete a test to make the change fit.
For server work, skills/srouter-server/SKILL.md is the detailed companion to the rules above; for client work, the companion is skills/srouter-frontend/SKILL.md.
Workflow: Superpowers Skills (Mandatory)
All code work in this repo runs through the superpowers set installed at ~/.agents/skills (15 skills, symlinked into the Hermes skills root as ~/.hermes/skills/<name>). Load the skill with skill_view(name=...) when its trigger fires. Do not paraphrase the process from memory, and do not skip a phase because the diff "looks small".
The Hermes-side router for this file is the skill seaavey-project-workflow: it maps phase → skill and project → repo skill. This section is the in-repo copy of the same mandate, so a non-Hermes agent reads the same rules.
| Trigger | Skill |
| Starting a task, or unsure which process applies | using-superpowers |
| Any work that produces or changes something — feature, behaviour, doc, config | brainstorming (load it first, every time; it decides whether a conversation is needed) |
Any *.tsx file, created, edited, reviewed, or refactored | vercel-react-best-practices (filename trigger, before the first edit, no exception) |
client/src/api/**, queryOptions, useQuery, useMutation, QueryClient | tanstack-query |
client/src/routes/**, routeTree.gen.ts, createFileRoute, validateSearch, useSearch | tanstack-router |
| Data grid, form, long-list virtualization, debounce, devtools panel | tanstack-table / tanstack-form / tanstack-virtual / tanstack-pacer / tanstack-devtools (only the one in play) |
| Multi-step work, before implementing | writing-plans |
| Executing a written plan | executing-plans |
| Plan has independent tasks worth parallelising | dispatching-parallel-agents |
| Plan tasks handed to fresh subagents | subagent-driven-development |
| Any implementation or bugfix, before product code | test-driven-development |
| Any bug, test failure, or unexpected behaviour, before proposing a fix | systematic-debugging |
| A superpowers run itself went wrong | diagnosing-superpowers |
| Work complete, before merge | requesting-code-review |
| Review feedback arrived | receiving-code-review |
| About to claim "done", "fixed", or "passing" | verification-before-completion |
| Branch ready to merge or open a PR | finishing-a-development-branch |
Long or risky work that needs isolation from main | using-git-worktrees |
| Authoring or editing a skill | writing-skills |
Hard gates, in order:
brainstormingis loaded before any work that produces or changes something. Loading is unconditional and comes before the first edit or the first reply that commits to an approach; the skill itself decides whether the request is complete enough to just build (make the icon cornflower blue) or needs a conversation. The only exits are the ones it defines: the human opts out of the questions, or asks for a spike. "It's a small change" is not an exit.- No production code before a failing test from
test-driven-development. Where this repo genuinely has no runner for the surface (the client has none), say so explicitly and substitutebun run typecheck && bun run lint && bun run buildplus a browser pass — never a silent skip. - No fix before
systematic-debuggingPhase 1 has a root cause. Three failed fixes mean the architecture is questioned (Phase 4.5), not a fourth attempt. - No "done" claim before
verification-before-completion. Rule 12 (verify by running) is the evidence; the skill is the checklist. - No merge without
requesting-code-reviewon the exact SHA range, with Critical and Important findings resolved. - Passing a superpowers phase is not passing this repo's gates:
cargo fmt --check,clippy -D warnings, the full suite, and the client checks still apply.
The bundled Hermes copies of requesting-code-review, systematic-debugging, and test-driven-development are parked in /root/.hermes/skills-disabled/bundled-superpowers-duplicates/. Leave them disabled: a second skill with the same name makes skill_view refuse to resolve the name at all.
<!-- antislop:start -->antislop (UI, copy, comments, mobile)
Interface, copy, accessibility, mobile-layout, or code-comment work also loads the antislop filter. On Hermes Agent the skills are antislop (core, always loaded with one of the below) plus antislop-ui, antislop-copywriting, antislop-human, antislop-layoutmobile, antislop-code. ui-ux-pro-max adds design intelligence and never replaces the filter.
Resolve the mode before the first edit: an explicit choice in the conversation, else ~/.config/antislop/settings.json, else ask DURING or AFTER and stop. Announce antislop active: <mode> (<source>). once. Run the Delivery Gate before handing anything over: a PASS/FAIL line per rule, each PASS backed by evidence, and a report containing a FAIL is never shipped.
<!-- antislop:end -->Key Directories
| Path | Contents |
server/src/app.rs | The only router assembly; ApiInfo, /health, route_not_found |
server/src/state.rs | AppState, SecurityState, builders |
server/src/error.rs | APIError + IntoResponse → OpenAI ErrorEnvelope |
server/src/constants.rs | Every client-visible message, code, error_type, header name |
server/src/features/ | One dir per feature: gateway/ (chat, messages, images, translation, interception, search), providers/ (registry, executors, per-vendor drivers, custom/, management/, rotation), catalog/, api_keys/, admin_auth/, provider_auth/, logs.rs, settings.rs, database_transfer/ |
server/src/http/ | middleware/ (api_key_auth, admin_session, rate_limit, csrf, cors, body_limit, access_log, failure_log, security_headers), listeners.rs, static_files.rs |
server/src/infrastructure/ | database/ (sqlite handle, migrations, stores), upstream/ (client + SSRF guards), telemetry.rs |
server/src/protocol/ | Leaf wire types: model, usage, sse, image |
server/migrations/ | NNNN snake_case.sql, DDL only, embedded via include_str!; no sqlx-cli |
server/tests/ | 46 integration suites + the 3.5k-line support/ harness |
client/src/routes/ | TanStack file routes; protected screens belong under _authenticated/ |
client/src/api/ | The only network layer (client.ts wrapper + queryOptions factories) |
server/src/benchmark/ | k6 scenarios — external k6 binary, not part of cargo test |
Development Commands
Run from the repo root. Server (verbatim from CONTRIBUTING.md:12-37):
cargo run --manifest-path server/Cargo.toml # listens on :3000
cargo test --manifest-path server/Cargo.toml --test chat_completions # one suite
cargo test --manifest-path server/Cargo.toml --locked # everything
cargo fmt --manifest-path server/Cargo.toml -- --check
cargo clippy --manifest-path server/Cargo.toml --all-targets --all-features --locked -- -D warnings
cargo run --manifest-path server/Cargo.toml --bin export_ts # regenerate bindings (commit both files)
cargo test --manifest-path server/Cargo.toml --test opencode_live -- --ignored --nocapture # network testClient, with Bun from client/:
bun run dev # vite; proxies /v1 → SROUTER_API_URL ?? http://127.0.0.1:3000, changeOrigin:false
bun run typecheck # tsr generate && tsc --noEmit
bun run lint # eslint .
bun run format # prettier --write "**/*.{ts,tsx}"
bun run build # tsr generate && tsc -b && vite buildTwo runtime gotchas:
dotenvyreads.envrelative to the current working directory, so run the server with cwdserver/(or export vars) forserver/.envto apply.resolve_web_distdoes not searchclient/dist; serving the built SPA needsWEB_DIST_PATH=client/dist(dev builds proxy instead, so this only matters for production-ish runs).
Pricing snapshot maintenance (Python 3 stdlib, not a required dev step): python3 server/scripts/update_models_dev_pricing.py --update|--check.
Code Conventions & Common Patterns
Rust (server/) — 4-space indent, rustfmt defaults; no rustfmt.toml, so cargo fmt output is the law.
- Errors: one custom
APIError { status, message, error_type, code, param }with builders andimpl IntoResponse; propagate with?inResult<_, APIError>. Noanyhow/thiserror(zero hits).Box<dyn Error>only inmain. - Wire fields are
snake_case, both directions. All 248 fields inclient/src/generated/typed.tsare snake_case and the client depends on it. Do not add#[serde(rename_all = "camelCase")], and use#[serde(rename)]only for a name that is not a valid Rust identifier (error.type). Enum values mirror their protocol instead (UPPERCASEmethod,lowercaseobject kind,kebab-caseSSE tags) — follow the enum next to yours. Regenerate withexport_tsandgit diffthe generated file so a camelCase field cannot slip through: a Rust type change is a wire contract change. - Request bodies are wire shapes too. A
POST/PATCHbody type derivesspecta::Typeon the server and reaches the client throughexport_ts(CreateAPIKeyInput,AdminSetupInput,AdminLoginInput); the client never declares its own request shape. A body that carries a secret derives noDebug/PartialEq, so it cannot be printed by accident. - All client-visible strings/codes/headers come from
server/src/constants.rs— never inline literals in handlers; interpolated messages become functions.error.codeis theErrorCodeenum there, soAPIError::with_codetakes a variant and the generated client sees a closed union instead ofstring. - Async: tokio multi-thread. Traits stay dyn-compatible by returning
BoxFuture<'_, …>instead ofasync fn(see the rationale inproviders/executor.rs). Shared state isArc<..>+RwLock/Mutexwith poison recovery; streams flow throughmpsc/broadcast. - Provider drivers: implement
ProviderExecutorin the vendor module, register one constructor — no central dispatch match. File set per vendor:mod.rs+executor.rs/translate.rs/types.rs/auth.rs/catalog.rs/tests.rs. Runtime-registered drivers areArc<dyn>-based; catalog order lives inSEED_PROVIDERS. - Naming:
snake_casemodules grouped by feature withmod.rsre-exports; handlers are verbs (create_completion,list_quota); router factories arecreate_*_router; stores areSQLx*Storewith traits instore.rs/repository.rsandEmpty*fallbacks. - Docs: module-level
//!comments explain why and cite the Nodeapps/api/src/...:lineoracle. Match that style; adocs/directory does not exist — comments citingdocs/api-v1-contract.mdetc. are stale pointers from the Node port, and the contract really lives in the Rust types plus the route tests. - JSON shapes: pair
#[serde(skip_serializing_if)]with#[specta(optional)];bindings.rsrenders a customWireShapesformat that strips the skip attributes so no_Serialize/_Deserializesplits appear.
TypeScript (client/) — prettier: semi: false, double quotes, printWidth: 80, trailingComma: es5, tailwind class sorting with tailwindFunctions: [cn, cva].
strict+noUnusedLocals/noUnusedParameters+verbatimModuleSyntax+erasableSyntaxOnly→ useimport type/ inlinetypeimports.- Path alias
@/*→client/src/*(Vite + tsconfig). Files kebab-case; components PascalCase, named exports. - Type discipline. A client shape is a view of a generated wire type —
Pick/Omit/Partial/Exclude/Record, never a re-declared field list. Lookup tables are keyed by their union (Record<FontFamily, string>), not bystring. Noenum:erasableSyntaxOnlybans it, so use a literal union plus a const object joined withsatisfies Record<Union, V>(the generatedErrorCodeis exactly that). Aswitchover a union ends in aneverassignment. Full table:skills/srouter-frontend/references/conventions.md. - No
anyand nounknowninclient/src.anydisables the checker;unknowndefers every decision to a laterinstanceof. Take the narrowest true type instead —Error | nullfor a TanStack Query error,objectfor a JSON request body, the generated wire type for a response.neveris exempt: it is the exhaustiveness mechanism, not an escape hatch. The fourunknowns that existed were removed (validateSearchinfers its parameter;describetakesError | null;bodyisobject; the parsed response isobject | null). - All network access goes through
request<T>()inclient/src/api/client.ts(credentials: 'include', verbs limited to GET/POST/PATCH/DELETE, no PUT). The verb union isRequestMethod, derived from the generatedHTTPMethodviaExtract— the log type also carries PUT/HEAD/OPTIONS/CONNECT/TRACE/OTHER, none of which the operator API accepts. Never add a cross-origin API base URL or strip theOriginheader: the game is same-origin + HttpOnlySameSite=Laxcookie + server-sideOriginCSRF guard. - Server state via TanStack Query
queryOptionsfactories declared next to their endpoint (client/src/api/<domain>.ts); the shared['admin','status']cache entry is load-bearing for the auth guard and login page. Local state viauseState; no Redux/Zustand/immer, no form library, no toast library. cnis re-exported fromclient/src/lib/utils.ts(thecnpackage, not clsx+tailwind-merge); UI primitives live inclient/src/components/ui/(shadcn generated over@base-ui/react).
Never hand-edit generated files: client/src/generated/**, server/bindings.ts (regenerate via export_ts; server/tests/bindings.rs fails on drift) and client/src/routeTree.gen.ts (tsr generate / the Vite plugin; committed on purpose).
Important Files
server/src/main.rs— composition root;server/src/lib.rs— module list + flat re-exports.server/src/app.rs—create_router(); the single place route guards and layer order are decided.server/src/config.rs—APIConfig::from_env_map, the only env parser; itsDebugredacts secrets.server/src/bindings.rs+server/src/bin/export_ts.rs— specta registry and writer.server/src/infrastructure/database/migrations.rs— hand-written runner,SCHEMA_VERSION = 7,PRAGMA user_versioncarrier, refuses newer versions.server/tests/support/mod.rs—TestDatabase, fake upstreams, fixture stores, request builders; every new suite should start here.client/src/api/client.ts,client/src/api/admin.ts,client/src/routes/__root.tsx(queryClient singleton),client/src/routes/_authenticated.tsx(auth gate).server/.env.example— the authoritative env surface;CONTRIBUTING.md,SECURITY.md— process and security rules.
Runtime/Tooling Preferences
- Server: Rust
stable(edition 2024 ⇒ ≥1.85), pinned only to the channel byserver/rust-toolchain.toml; commitserver/Cargo.lockand use--lockedfor checks. No cargo features table; all dep features are inline.[profile.dev]/[profile.test] debug = 1with dependencies atdebug = falseis a deliberate disk-footprint decision — do not "fix" it to 2. - Client: Bun is the package manager (
client/bun.lockis the only lockfile); noengines/packageManagerpins, noVITE_*vars — the only client env var isSROUTER_API_URL(Vite config). Never add npm/pnpm/yarn lockfiles. - Database: SQLite is the only supported backend.
DATABASE_URLis refused at boot (owner ruling 2026-10-08) and must stay unset. All SQL is runtimesqlx::query(...).bind(...)— noquery!macros, no.sqlxoffline cache, so no database is needed to build or test. - Server env vars (names only; parsed once at boot, grouped by concern): listener/storage
PORT,DATABASE_PATH(default$HOME/.srouter/srouter.db),WEB_DIST_PATH; URLs/authSROUTER_PUBLIC_URL,SROUTER_CORS_ORIGINS,SROUTER_SECURE_COOKIES,SROUTER_ADMIN_PASSWORD,SROUTER_TRUST_CF_CONNECTING_IP; runtime/loggingNODE_ENV(developmentis refused at boot: production is the only mode the binary boots),SROUTER_ACCESS_LOG,SROUTER_FILE_LOG,RUST_LOG,SROUTER_UPSTREAM_CONCURRENCY; search fallbacksBRAVE_API_KEY,TAVILY_API_KEY,SERPER_API_KEY,SEARXNG_URL; read outsideAPIConfig:CLAUDE_OAUTH_CLIENT_ID,CODEBUDDY_PRODUCT_JSON,HOME. - Runtimes needed anyway: Python 3 only for the pricing script; Node is not needed at all; k6 only for the optional benchmark scenarios.
- History lives on branches
backup/pre-packages-removalandbackup/pre-apps-api-removal(the removedapps/,packages/*, CLI, docs site);CONTRIBUTING.mdstill claims "no Node toolchain" — stale,client/is live.
Testing & QA
- Framework: plain
cargo testonly. Single dev-dependency:tower(util, foroneshot). No nextest, serial_test, testcontainers, wiremock, insta, or coverage tooling anywhere. - Shape: 46 integration suites in
server/tests/*.rs(552 test fns) + 78 in-crate#[cfg(test)]modules for provider translation/middleware/db units. Requests are driven either throughRouter::oneshotwith an injectedConnectInfo(SocketAddr)peer (most suites) or over a real127.0.0.1:0socket withreqwest(logs.rs,pricing.rs). - Isolation: every suite builds its own temp SQLite file via
TestDatabaseand fakes upstreams (Fake{Upstream,Qoder,CodeBuddy,Cline,Grok,Antigravity,Claude,Codex}intests/support/, all bound to random loopback ports and aborted onDrop). Tests must never open~/.srouter/srouter.dbor reach a real provider. The only global lock isEVENT_TEST_LOCKinlogs.rsaround process-global log-event broadcasts; prefer dependency injection over new globals. - Expectations: no coverage thresholds; CI (
.github/workflows/ci.yml) re-runsfmt,clippy, and the suite onmainpushes and pull requests into it, but run the full suite yourself before pushing (cargo test --manifest-path server/Cargo.toml --locked), thenfmt --checkandclippy -D warnings. Client-side verification isbun run typecheck && bun run lint && bun run build, plus the Playwright suite inclient/e2e(bun run e2e: four viewport projects against a throwaway gateway built bye2e/support/stack.ts). - Frozen contracts a change must keep green:
server/tests/bindings.rs(generated TS),server/tests/schema.rs(user_version = 7, newer refused),server/tests/http_runtime.rs(six frozen response headers incl.X-Version),server/tests/database.rs(DATABASE_URLrefused before any SQLite file is touched). Route tests pin status codes and error bodies, so the contract is the Rust types + tests — there is no separate contract document to update. - Stated policy (
CONTRIBUTING.md): Conventional Commits (feat:,fix:,docs:,refactor:,test:,perf:,chore:), imperative subject, and say what you ran in the PR description. - Security constraints to respect (
SECURITY.md): provider credentials are cleartext JSON inside the SQLite file and in exports — treat the DB like a.env; gateway keys store only sha256 + prefix (unrecoverable); request logs keep metadata only, never prompt/completion text; report vulnerabilities privately tosecurity@srouter.web.id, never via a public issue.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- llms.txtllms.txt
- srouter-frontendSkill
- srouter-serverSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

