Skip to content
hamr0Public

About

Small local classifier that scopes r/w/x for API operations on the fly: Jev-shaped JSON answers, a drop-in companion to rwxmap. [WIP]

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

   rwxlite
   three questions in ──→ JSON out ──→ or no answer
   a small, local gut check for what an API call can do

CI version (auto from package.json) classifier: rwxlite-0.2.0+8ad0a182 license: Apache 2.0

A small local classifier that says whether an API operation reads (r), writes (w) or can't be taken back (x). Jev-shaped questions in, JSON out, no answer if it isn't confident.

Status: module 1 (the Node answerer) is built, version 0.2.0; the npm package is not published yet (the name is reserved). The rest of the repo is module 0 research. Parts of this page describe the plan. They are marked planned.

Agentic automation is on the rise, and there's no solution in sight for controlling API access with guardrails. It's impossible to account for every possibility. Today's answers don't scale and don't adapt to different shapes: scoped MCPs, policies built on MCP hints that aren't widely adopted or accurate, accountability moved into rigid sandboxes, or pushed back to the consumer through fine-grained IAM or APIs scoped at creation. Meanwhile we're creating new open standards for agents (MCP, WebMCP, headless browsing) faster than we can guardrail them.

rwxlite is meant to solve that on the fly. Jev-shaped three questions in, JSON out, no answer if it isn't confident. Use it on its own, or couple it with rwxmap to replace Jev. Free, local, responds in milliseconds, quantized to int8 at about 23 MB (built, see below).

The three letters

  • r — nothing changes.
  • w — changes something a later call can set back.
  • x — can't be taken back.

Two modes

  • a. With rwxmap. Mechanical rules lean first. The model only moves the contested rows. It answers in Jev's JSON, so rwxmap only swaps the client. Planned (M2).
  • b. Standalone. One operation in, {letter, p} out. A low top probability is the "ask a human" signal. The threshold is yours. Built (M1).
// request (same as Jev's)
{ "state": { "method": "POST", "path": "/v1/search", "operationId": "search",
             "summary": "Search records", "description": "" },
  "model": "rwxlite-0.2.0",
  "questions": { "isX": "<the a7bc855 question text>" } }

// answer (same shape as Jev's)
{ "answers": { "isX": { "noul": 0.04 } },
  "model": "rwxlite-0.2.0+<weights-hash>" }

// standalone (mode b)
{ "letter": "r", "p": { "r": 0.91, "w": 0.07, "x": 0.02 }, "model": "rwxlite-0.2.0+<hash>" }

The model outputs three probabilities. Every Jev question is arithmetic on them: isX is p(x), changes is p(w) + p(x).

Start here

Install the package: npm i rwxlite (set ONNXRUNTIME_NODE_INSTALL=skip to avoid a 285 MB GPU download; the CPU build is enough). The model files are not in the npm package; get them from the GitHub releases (see "Using the answerer"). Release order, because the package fetches those files at first load: 1. merge m1; 2. create the GitHub release v<version> with the assets from node m1/release-assets.mjs; 3. then "Run workflow" for publish.yml (it fails if the release or its assets are missing or empty, or if the release's manifest.json version is not the package version).

The rest of this repo is research (module 0): read on for the scripts.

  • Read the PRD first.
  • Mechanical labels: node m0/mechanical.mjs
  • Training: python3 train/build_dataset.py, train/baseline.py, train/finetune.py, train/tune_cuts.py
  • Gate scripts for the label exam live in m0/.
  • Exams: node m0/exam-inputs.mjs [fresh] builds the model inputs, then python3 train/exam_score.py predict|score (first exam) or python3 train/exam_fresh.py predict|score (fresh exam). The ask-rule measurements are train/abstain.py, train/fallback.py and train/lowerflag.py.
  • These scripts need a sibling checkout of rwxmap and the trained weights, which are local and not in the repo (data/train/); the committed exam inputs, predictions and results are under data/.

What's inside

Part State What it does
Label recipe built, measured rwxmap mechanical + Jev, per-method questions, two-step POST lower, credential rule
Baseline trained TF-IDF + logistic regression, 24 MB, about 1.5 ms/op
Fine-tuned MiniLM trained, exam failed (too loose) 22.7M params, 90.9 MB fp32, about 22-28 ms/op in fp32 PyTorch; 95.9% exact, 0.84% too loose on the exam
int8 ONNX export + Node answerer built (M1) the in-process answerer that speaks Jev's JSON
Abstain threshold measured, not built "no answer when not confident"; the contract rule asks on lowerings under 0.97 and trusts raises and keeps: fresh exam 92.6% right, 5 too loose, 7.8% asked
rwxmap integration planned (M2) mechanical leans first, model moves contested rows

Input is OpenAPI operations only: method, path template, operationId, summary, description.

It tells you the truth

As of 2026-11-04, module 0 (labels, training, exams) is scored and module 1 (the Node answerer) is built; the npm package is not published yet.

  • Labels are measured. The label recipe passed a fresh blind gate at 96.5% exact and 0.7% too loose, against 79.3% and 1.2% for mechanical alone. Two findings went back into rwxmap (D129 two-step POST, D131 credential rule).
  • The model failed its go/no-go exam, on one line. The fine-tuned MiniLM was scored once on 4,279 operations from vendors it never saw (cloudflare, pagerduty, sentry). It answered 95.9% exact, ahead of mechanical + Jev (93.0%) and mechanical alone (82.3%). But 0.84% of its answers were too loose (36 rows), over the 0.5% line set by Jev. Most of those are Cloudflare "create" calls that move money or start jobs, which it confidently called editable. Next step is open.
  • Asking helps, but wasn't the pass/fail check. When the model answers only at 95% confidence or above and asks on the rest, it answers 90.2% of rows at 98.7% exact and 0.47% too loose. That was reported, not pre-registered, so it doesn't count as a pass. Full table in the PRD.

How to read "ask". Every operation lands in one pile: right, too loose (called safer than it is, the dangerous one), too tight (called riskier, annoying but safe), or asked (the user decides). The confidence bar only moves rows into "asked". On the 4,279-row exam:

bar right too loose too tight asked what the asks held (right + loose + tight)
none 4104 36 139 0
70% 4064 36 69 110 40 + 0 + 70
90% 3961 21 37 260 143 + 15 + 102
95% 3812 18 31 418 292 + 18 + 108

At 70% all 36 dangerous mistakes are still answered: the model was sure of them. From 90% the asks start catching them, at the cost of asking on 6% to 10% of operations.

  • Training labels aren't the cause. Two blind labellers re-labelled 1,200 training rows of the failing shapes: the training labels were too loose on only 32 (2.7%). The exam misses are mostly one vendor's pattern (Cloudflare billing and jobs).
  • Fresh exam: missed by one row. On 949 operations from four vendors nobody had trained on (Coolify, WorkOS, Lago, Resend), with lowerings flagged and asked under 97% confidence: 92.6% right, 7.8% asked, 5 too-loose answers (0.57%) against a line of 4 (0.5%). The lowering flag caught every wrong lowering; the 5 misses are credential updates and logouts that the mechanical rules already call too safe. Mechanical alone: 77.6% right, 6 too loose. Details in the PRD.
  • "No answer if not confident" is a rule, not code in the answerer. The answerer returns probabilities only; the ask rule is measured on the exams and lives in the scoring scripts and the test reference (see "Using the answerer" and the PRD).
  • int8 size is measured: the per-channel int8 model is 23 MB (fp32 is 91 MB).
  • Limits. OpenAPI operations only. No request bodies, no query values, no hosts without a spec.

Using the answerer

import { loadAnswerer } from 'rwxlite';

const a = await loadAnswerer({ dir: '/path/to/model-dir' }); // threads: default min(4, cores)
const state = { method: 'POST', path: '/v1/search', operationId: 'search', summary: 'Search records', description: '' };
const req = { state, questions: { isX: '', changes: '' }, lean: { class: 'r', rule: 'path-verb' } };
const one = await a.answer(req);        // { model, answers: { isX: { noul }, changes: { noul } } }; throws on a no-answer
const many = await a.answerMany([req]); // each slot is { model, answers } or { model, error }; a bad request does not fail the rest

The HTTP QUERY method (RFC 10008) is read as GET: the model never saw QUERY in training. The lean rule credential-set (rwxmap D131) is read as cant-undo-verb in the model text, because the model never saw that name; the lean class is unchanged.

  • lean is required: the mechanical letter and rule ({class: 'r'|'w'|'x', rule}). The model was trained with it as the first words of its input.
  • Only the keys of questions are read. isX is p(x), changes is p(w) + p(x). rwxlite returns probabilities only; the caller decides.
  • Two shapes, both plain JSON: success {model, answers: {...}} and no answer {model, error: '<message>'} (no answers key). answerMany never returns Error objects (they serialise as {}); answer throws an Error with the same message. On a no-answer keep the mechanical letter.
  • rwxlite returns only p; turning p into a letter is rwxmap's call. The rule rwxlite measured (which rows move, the cuts, the credential rule, ask on lowerings under 97%), and the comparison with rwxmap's secondPass, are in the PRD. test/fixtures/decision.jsonl and test/decision.test.js are a parity and reference file for rwxlite's rule.
  • dir holds manifest.json, the model file and vocab.txt; every sha256 is checked and a mismatch throws. The manifest may carry temperature (the calibration temperature, a finite number > 0; anything else throws); without it the answerer uses 0.844, the 0.2.0 value.
  • onnxruntime-node is an optional dependency. Install it with ONNXRUNTIME_NODE_INSTALL=skip npm install onnxruntime-node to skip the 285 MB of GPU libraries.
  • Two model options, both on the GitHub releases: the package default, int8 per-channel (manifest.json, model-int8-pc.onnx, 23 MB) and "exact" fp32 (manifest-exact.json, model-fp32.onnx, 91 MB, no quantisation drift). rwxmap's own default is the exact fp32 model; the package default is int8-pc. Load the exact one with loadAnswerer({ dir, manifestFile: 'manifest-exact.json' }).

Docs

  • PRD — scope, milestones, open questions.
  • Findings — what was measured and what was decided.
  • Classifier training — how training works and how the model answers.

License

Apache License 2.0. See LICENSE.

About

Small local classifier that scopes r/w/x for API operations on the fly: Jev-shaped JSON answers, a drop-in companion to rwxmap. [WIP]

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages