Rukh — Roadmap
Julien Béranger
+ Claude Opus 5
Covers both rukh (the API) and rukh-ui (the web app). The two ship in lockstep, so milestones are defined once and split into an API track and a UI track.
Status: proposal. Nothing here is committed to a release date.
Priority: the teachers & pupils profile. M0–M4 exist to serve it. The ZK API profile (M7) is explicitly not a priority and carries no date.
1. Guiding principle
The API stays generalist. Rukh must never learn what a school is, what a pupil is, or what a lesson plan is. It learns a small number of generic primitives, and every vertical is expressed as data filling those primitives — never as a branch in the API.
Concretely, that means the roadmap below is mostly one thing: growing the context schema from "a bag of files" into "a declarative description of an assistant", then implementing the runtime that reads it.
Three consequences worth stating up front, because they constrain everything else:
- A context is data, not code. Anything that defines behaviour must be serialisable and inspectable. No vertical-specific handlers.
- The same context definition must run on either backend. Rukh and zk-api are two deployments of the same idea, not two products. A context should move between them by changing one field.
- Capabilities are declared, resolved, and enforced per audience. The same context grants different things to its creator than to an anonymous visitor. This is the mechanism that makes a safe pupil surface possible without a "kids mode" flag in the API.
2. The context schema
Everything converges here. Today a context is name, description, an optional model override, a list of files, a list of links, and a queries log (see src/dto/context.dto.ts in the API, and docs/CONTEXT_MANAGEMENT.md).
Target shape:
{
"name": "maths-4e",
"description": "Maths homework help, French 4e level",
"schemaVersion": 2,
// How it behaves. Always injected, never subject to RAG selection.
"instructions": "You are a Socratic tutor for a 4e class. Never give the final answer; ask the question that moves the pupil forward one step...",
// What it can do. Whitelist, resolved per audience (see below).
"capabilities": ["skills", "document"],
// Who may talk to it, and how they authenticate.
"audience": {
"creator": { "capabilities": ["skills", "document", "web-reader"] },
"public": false,
"code": { "enabled": true, "capabilities": ["skills"] }
},
// What gets logged, and for how long.
"retention": { "storeMessages": false, "queries": "30d" },
// Which backend serves it. Resolved by the UI, not by the API.
"routing": "rukh", // | "zk-api"
// Who pays.
"billing": "creator", // | "end-user"
// Unchanged.
"model": "anthropic",
"files": [...],
"links": [...]
}Every field is optional and every default reproduces today's behaviour, so existing contexts keep working untouched. schemaVersion exists so the migration can be explicit rather than inferred.
3. The three profiles
A profile is a preset that fills the schema above. Profiles live in rukh-ui as plain JSON — the API has no notion of them. That is the whole point: adding a fourth vertical must require zero API changes.
| Generalist | Teachers & pupils | ZK API | |
|---|---|---|---|
| Priority | maintained | the priority | not a priority |
routing | rukh | rukh | zk-api |
billing | creator (Stripe) | creator (Stripe, school pays) | end-user (onchain credits) |
audience.public | true | false | true |
| Access for end users | open link | class code, no account | anonymous, proof-gated |
retention.storeMessages | creator's choice | false, enforced | operator's policy |
| Identity seen by operator | wallet / anonymous | teacher only | unlinkable by proof |
| RAG | server-side | server-side | server-side |
| Skills | yes | yes | yes |
| Server-executed tools | yes | teacher audience only | yes |
| Viable from | M0 | M2 | M7 |
The education profile is therefore not a feature — it is instructions + capabilities narrowed for the code audience + retention.storeMessages: false + audience.public: false. Nothing in the API changes to support it beyond the generic mechanisms.
4. Two backends, one context
zk-api is getting its own Rukh-style RAG, so the two backends converge on capability. What differs is identity and payment, not what the assistant can do.
- Rukh (
POST /ask) authenticates the creator by SIWE, meters cost server-side, and bills through Stripe. - zk-api (
POST /zk-api/request) authenticates a right to spend rather than a person: a Groth16 proof plus a one-time nullifier, so two requests from the same depositor cannot be correlated. Transport is ML-KEM-1024 + AES-256-GCM (src/encryption/mlkem-encryption.service.ts), with optional TEE attestation.
What zk-api does and does not buy you. Its own README is the honest statement: "The operator sees valid proofs and the requests it forwards. It does not see who you are or link your requests together." So the operator does see request content — which is precisely why it can run RAG. The guarantee is unlinkability of identity, not blindness to content. Do not describe it in the UI as end-to-end private; describe it as unlinkable. (An earlier draft of this document wrongly assumed the operator was blind to payloads and built a client-side prompt-compilation requirement on top of that. That requirement is dropped.)
Because both backends see plaintext and both can do RAG, a context needs no special preparation to move between them. routing is a deployment choice, not a capability boundary.
5. Request routing and PII redaction
Routing happens in rukh-ui, at the Next.js API route level — not in the browser and not in Rukh. This layer does not exist yet: src/utils/api.ts currently calls the Rukh API straight from the browser using the public NEXT_PUBLIC_RUKH_API_URL, and there is no route.ts anywhere under src/app/.
Target shape:
browser ──► /api/ask (Next.js route handler)
├─ redact PII
├─ resolve routing from the context
├──► Rukh POST /ask
└──► zk-api POST /zk-api/requestWhat the layer buys, beyond routing:
- Backend URLs stop being public.
NEXT_PUBLIC_RUKH_API_URLbecomes a server-onlyRUKH_API_URL, and the browser no longer needs to know where anything lives. - One place to redact, rate-limit, and enforce quotas, rather than one per call site.
- SIWE headers are forwarded rather than exposed; the signature is still produced client-side by W3PK, so the trust model is unchanged.
One caveat to accept knowingly. Proxying makes the Next.js server a party that sees every prompt and every IP. For the Rukh/teacher path that is fine — it is already a trusted first-party server. For the zk-api path it is a genuine regression: it reintroduces exactly the observer that the proofs exist to eliminate. So for routing: "zk-api", the Next.js route should return a routing decision plus the operator's public key and let the browser talk to zk-api directly, redacting client-side before it sends. The package is zero-dependency regex, so it runs in the browser despite the node in its name.
PII redaction
Use @redactpii/node (MIT, v1.0.17, zero dependencies, fully offline):
import { Redactor } from '@redactpii/node'
const redactor = new Redactor()
const clean = redactor.redact(message) // "Hi PERSON_NAME, call PHONE_NUMBER"It also offers redactObject() for whole payloads and hasPII() for detection without rewriting, and rules can be enabled selectively (new Redactor({ rules: { EMAIL: true } })).
Be honest about what this is worth. It is defence in depth, not a compliance guarantee, and it must never be sold to a school as "we remove personal data". Three concrete limits:
- The built-in patterns are US-centric. Emails and credit cards travel, but phone matching targets US formats and SSN is a US identifier. French mobile numbers (
06 12 34 56 78), the NIR, the INE, and French postal addresses are not covered. Extending the rule set for FR/EU identifiers is part of the work, not an afterthought. - Name detection is greeting-based. It catches
Hi David Johnson. It will not catchje m'appelle LéaorLéa, 4e B— which is exactly how a French pupil introduces themselves. Expect close to zero name coverage out of the box in French. - False positives are a real hazard in this product. A maths context is full of long digit strings that regexes happily mistake for phone numbers or card numbers. Redacting the numbers in a maths exercise is worse than not redacting at all. Since rules are global (below), the mitigations have to be global too.
Rules are global configuration
Redaction rules are configured once for the deployment, not per context. A teacher configures an assistant's behaviour; they do not configure the privacy floor, and they must not be able to lower it for everyone by editing a context. So the rule set lives in rukh-ui as deployment config — a src/config/redaction.ts module with environment overrides — read by the Next.js route layer on every request, with a read-only view in /settings so an admin can see what is actually active.
That decision puts the whole weight of the false-positive problem on the defaults, so they have to carry it:
- Rules are individually toggleable.
EMAILandCREDIT_CARDare high-value and low-noise;PHONEandSSNare the ones that misfire on numeric content. Being able to disable one rule without disabling redaction is the difference between a usable default and one every operator turns off wholesale. - Exclude fenced code blocks and maths expressions before matching. A global pre-pass that skips fenced code blocks and
$...$/\(...\)spans removes most of the maths damage without touching the rules themselves. - Ship a dry-run screen. Let an admin paste real lesson content and see exactly what would be redacted, before it is switched on. This is the cheapest possible safeguard and the one most likely to be skipped.
- Log redaction counts per rule, never the matched text. Counts make misfires visible and stay consistent with M0's rule that no message content is stored.
The actual privacy guarantee stays what it is in M0: the message is never stored. Redaction reduces what transits; not storing is what makes it unrecoverable. Also verify that the package's optional compliance-dashboard integration stays off, and pin that with a test — an outbound call from a redaction library would be a spectacular own goal.
6. Milestones
M0 — Schema and privacy foundations
The single most important milestone. Everything else fills in fields defined here.
API
- Add
instructionstoCreateContextDto/ContextMetadataDto, persist it inindex.json, and inject it at the head of the system prompt outside RAG selection (currently the only mechanism is the hardcodedREQUIRED_FILES = ['instruction-file.md']insrc/rag/rag.service.ts:21). Keep the magic filename as a deprecated fallback. - Stop writing the raw user message in
recordContextQuery(src/app.service.ts:283). It currently persists the full prompt verbatim intodata/contexts/<name>/index.json, under amessagefield thatContextQueryDtodoes not even declare, with no retention limit. Replace withtimestamp+contextFilesUsedonly. - Add
retention(storeMessages,queries) with a purge job. DefaultstoreMessages: false. - Declare
capabilities,audience,routing,billingandschemaVersionin the schema and in Swagger. They may be inert at this stage — declaring them early is what lets the UI and the migration be written once. - Write the migration for existing contexts in
data/contexts/.
UI
- Introduce the Next.js route layer (§5):
/api/askand the context calls, movingsrc/utils/api.tsoffNEXT_PUBLIC_RUKH_API_URL. - Wire
@redactpii/nodeinto that layer, reading the global rule config, with the FR/EU rule extensions, the code/maths exclusion pre-pass, and the dry-run screen. - Context edit page: a first-class "How should this assistant behave?" field, visually separate from Documents and Links.
- A retention control, defaulting to "don't store messages".
- Surface
schemaVersionmismatches instead of failing silently.
Done when a context can define behaviour without a magic filename, no user message is written to disk anywhere, and every outbound prompt passes through one redaction point.
M1 — Ingestion
Rukh accepts markdown. Teachers have PDFs, .docx, and photographs of textbook pages. Until a photo of a page becomes a context, nothing downstream matters.
API
- Accept PDF,
.docxand images onPOST /context/upload; convert to markdown on ingest and store the markdown (keeping the original is optional and costs privacy). - Images: use the vision path rather than OCR where the model is available.
- Extend
src/validators/file.validator.tsandsrc/config/file-upload.config.tsfor the new types and size limits.
UI
- Drag-and-drop upload, conversion progress, preview and edit of the converted markdown before it is saved. Never save a silent bad conversion.
Done when a teacher can photograph a page and get a usable context without touching markdown.
M2 — Audience and account-free access
Unlocks the education profile. Also the milestone with the clearest regulatory driver: the CNIL explicitly tells schools not to collect "les informations nécessitant la création d'un compte individuel lorsqu'un compte de classe suffit" — information requiring an individual account where a class account would do.
API
- Implement
audience:creator(SIWE, as today),public(open),code(a short shared token, rate-limited, revocable, rotatable). - Resolve capabilities per audience on every request — this is the enforcement point that makes a restricted surface real rather than cosmetic.
- Per-audience throttling on top of the existing
ThrottlerGuard.
UI
- Teacher: generate, display, rotate and revoke a class code; QR code for projection.
- Pupil: a route that takes a code and opens a chat with no authentication, no passkey, no account, nothing persisted. W3PK stays the teacher's alone — a class of thirty on shared tablets is the worst possible case for device-bound passkeys.
Done when a class can use a context without a single pupil account existing.
M3 — First capability: document export
The deliverable of a teacher's workflow is a worksheet, a quiz or a marking grid — printed or dropped in the ENT. Chat text is not the deliverable.
API
- A capability registry: each capability is a NestJS provider exposing a declaration and a handler, resolved against the context's
capabilitiesand the caller's audience. - Implement
document: markdown → PDF/docx. - Expose the existing
WebReaderServiceas theweb-readercapability (it is already written; it is simply not reachable as one).
UI
- Export action on any assistant response; format choice; a capability picker on the edit page that shows what each audience gets.
Done when a teacher can go from a photographed page to a printable differentiated worksheet.
M4 — Skills as files
The portable half of "capabilities", and the piece that works on all three profiles.
API
- A
skills/subfolder in a context; each file is a named procedure (make-a-quiz.md,grade-with-rubric.md,differentiate-three-levels.md). - Skills are declarative markdown, never code. Keeping them as data is what lets them be inspected by a teacher, shared as a file, and reused unchanged on either backend.
- Selection: named invocation first (deterministic, cheap); model-chosen selection later if it earns its keep.
UI
- Browse, edit and create skills as a distinct tab from Documents.
- Import/export a skill as a file. Teachers already trade lesson plans; a marketplace of content fits their practice far better than a marketplace of code, and it is a much cheaper network effect to build.
Done when a teacher can write a reusable procedure once, invoke it across contexts, and share it with a colleague as a file.
M5 — Stripe: creators pay
API
- Per-context, per-creator usage metering.
CostTracker(src/memory/cost-tracking.service.ts) already computes per-request cost — this is aggregation and persistence, not new measurement. - Stripe customer keyed to the creator's wallet address, metered subscription, quota enforcement with a clear over-quota error, and webhook handling.
- Quotas must be enforceable before the expensive capabilities land in M6.
UI
- Billing section in
/settings: plan, current usage, per-context breakdown, payment method. - Over-quota state on the context page that is honest about what stopped and why.
Note on the two payment rails. Stripe is identified, invoiced, recurring — right for creators, and the only thing a school's accounting department can process. zk-api credits are anonymous and prepaid — right for privacy-first end users. They are not competing; they serve opposite ends of the same product. A school pays by card while its pupils stay anonymous, and that is a coherent story, not a contradiction.
Done when a creator can exceed a free tier and be charged for it.
M6 — The tool loop
Deliberately late, and deliberately not universal.
API
- An agentic loop over the declared capabilities, gated by the audience resolution from M2.
- Depth, cost and wall-clock ceilings per request.
- Full trace of tool calls for the creator.
UI
- Render tool calls as they happen; let the creator inspect and replay a trace.
Deliberate non-goal: the pupil assistant is never agentic. "It cannot browse, it cannot act, it does not remember you after the lesson" is the sentence that gets the product past a headteacher and a DPO. That is the pitch, not a limitation. Agency belongs on the teacher's preparation surface, where the value is high and the risk is nil.
M7 — The ZK API profile
Not a priority. No date. Listed so the schema stays honest about where it is heading, and so nothing in M0–M6 forecloses it.
API
- Nothing, ideally. If
routing: "zk-api"requires an API change beyond serving the field, the schema is wrong.
UI
- Route
routing: "zk-api"contexts to zk-api from the Next.js layer, browser-direct for the reasons in §5. - Proof generation via W3PK,
POST /zk-api/estimate-costbefore sending,POST /zk-api/requestto send, refund redemption. - A deposit/credits screen, and an honest explanation of the boundary: unlinkable who, not private what.
Done when the same context definition runs on both backends with no change beyond one field.
7. Explicit non-goals
- MCP, for now. It is the right long-term interface and the wrong foundation today: arbitrary remote servers plus per-user credentials plus minors is an indefensible compliance surface, and no teacher will ever paste a server URL into a form. Revisit after M6 as one capability among others, teacher-only, gated by the same whitelist.
- A third-party plugin marketplace. First-party, audited capabilities only until the security model is proven.
- Vertical-specific API endpoints. If a milestone seems to need one, the schema is wrong.
- Autonomous-agent registries (ERC-8004 and similar). Interesting; out of scope for a classroom.
- Presenting redaction as compliance. See §5.
8. Open questions
- Should
instructionsbe a single field or a small set (persona / constraints / refusals)? A single field is simpler; a structured one is easier to validate and to preset per profile. - Where does the class-code session live so that "nothing persisted" stays true, while a lesson still survives a page reload?
- Does the
queriesaggregate (M0) give a teacher anything genuinely useful once messages are gone, or should the "what did the class struggle with" view be a derived skill instead? - The homepage previously grouped contexts into Free / ZK API / For kids / Agentic (see
CHANGELOG.md0.2.0) using acategoryfield that the API refactor dropped. Do categories come back as a first-class field, or are they derived fromrouting+billing+audience? Deriving them is more consistent with §1.
9. Related documents
CONTEXT_MANAGEMENT.md— how contexts work todayMODELS.md— provider and model support- Rukh : se spécialiser pour l'école — the reasoning behind the education profile, with the regulatory sources