jithox
v0.3.0
Published
Jithox Connect Kit — OAuth-first: connect the AI client you already use to Jithox; the client signs in itself, no credential in any file. Preview first, explicit consent, backup, atomic write, test, undo.
Readme
Jithox Connect Kit
The jithox CLI connects an AI client to Jithox's MCP servers, and checks that
the connection is real before you trust it.
Install
npm install -g jithoxWhich platforms this is tested on
Honestly, and only from what CI actually runs:
| | how it is tested |
| --- | --- |
| Linux | the whole suite, plus the packed tarball installed into an empty project |
| Windows | the whole suite on windows-latest, plus the packed tarball installed into an empty project and the first journey walked through it. Eighteen tests are skipped there, each naming the POSIX capability Windows lacks (file mode bits, FIFOs, /proc/self/fd, path separators) |
| macOS | not tested. No CI lane runs it. It is a POSIX system and is expected to work, but expected is not tested, and this table will say so until a lane exists |
One difference worth knowing about on Windows: O_NOFOLLOW does not exist
there, so the kit checks with lstat before it opens. That closes the case
that happens — a symbolic link already sitting where one of the kit's own files
belongs — but not the race of a link planted in the instant between the check
and the open. On Linux the kernel closes both.
First run
jithox init # prepare this machine (no credential, no network)
jithox discover --json # what is reachable, and what may it do?
jithox call verify_iban --input '{"iban":"BE68 5390 0754 7034"}'
# ONE real, free tool call — no credential, a receipt
jithox doctor --local --json # verify the evidence that real call left behind
jithox test --sandbox --client generic_mcp # a local simulation — nothing leaves your machineinit writes up to two small local files after showing you exactly what they
will contain: ~/.jithox/config.json and an inert sandbox marker, both mode
600 inside a 700 directory. It asks nothing secret, makes no network call, and
activates nothing in the cloud. Answering n writes nothing at all. In a
script or container, pass --non-interactive — without a terminal to ask,
init refuses rather than silently doing nothing.
doctor --local is strict: it passes only for a real connection. Use
doctor --local --sandbox to check the local simulation; a sandbox pass is
never a cloud connection and never means authenticated.
Undo
init is reversible on the machine, not just in the repository.
jithox init --undo --dry-run # show exactly what would go; changes nothing
jithox init --undo # remove it, after showing the list and asking
jithox init --undo --json # the same, as a machine-readable envelope
jithox init --undo --verify-receiptIt removes only what init itself created, checked by content rather than by
name: init records every path it writes with the SHA-256 of the bytes it
wrote, and undo deletes a file only when the bytes on disk still match. A
config you edited is kept and reported MODIFIED_BY_USER; a file that predates
init is kept as NOT_CREATED_BY_INIT. If there is no record, nothing is
removed at all — this command will not delete a file it cannot prove it wrote.
auth.json, connection, last-execution.json and receipts/ belong to
auth login, connect and test, and are never touched. A symlink, hard
link, FIFO or device where a regular file was recorded is refused rather than
followed.
The directory itself is never removed. rmdir takes a name in the parent
directory, and no descriptor can bind that call to the directory the command
validated; when an earlier version lost that race it could re-create only a
mode — not the inode, owner, ACLs or extended attributes of what it had just
destroyed. So ~/.jithox is left standing even when empty, directoryRemoved
and the receipt's directory_removed are always false, and removing the
directory is yours to do. The closing message lists what was still in the
directory when the command last looked at it; it deliberately does not print a
rm -r line, because the path could name a different directory than the one
that listing came from.
An empty ~/.jithox that init created keeps its init-manifest.json, listing
no files. That file records that init made the directory, and it is what lets
you tell init's leftovers from your own.
For the same reason, a leftover temp file (.<name>.jithox-tmp-<pid>-<ts>, from
a write that was killed) is kept, not swept, unless the manifest records
that exact name and hash. Matching the name, having a dead pid, even opening
with the same bytes a Jithox document starts with — none of that identifies the
writer, and each of those tests, in turn, deleted a user's file. Anything
unproven is reported in kept[] as NOT_PROVEN_OURS and is yours to delete.
Two limits stated plainly. The manifest is bookkeeping, not an authenticated record: anyone who can write it can name a file in it, and the temp-name pattern is what keeps that from reaching arbitrary basenames. And re-reading a file immediately before removing it narrows the window in which it could be swapped — it does not close it, because POSIX offers no unlink-if-unchanged.
kept[].reason is an open set. NOT_PROVEN_OURS was added in 0.2.2, and
more values may follow; a program reading --json must treat an unrecognised
reason as "kept, for a reason this version does not know" rather than failing.
A real run writes ~/.jithox/undo-receipt.json (mode 600) whose digest anyone
can verify, and which a later undo removes in turn. Exit codes: 0 removed or
dry run, 2 nothing to undo.
Discovery
jithox discover reads only public sources and needs no login. It lists
capabilities; it never installs anything and never calls a discovered tool.
Anything a source did not state stays UNKNOWN — a price or a client
compatibility is never guessed. If a source cannot be reached, discover says
so and exits 2 rather than reporting "no tools".
jithox discover --read-only # only capabilities proven not to write
jithox discover --use-case invoice # filter on real text
jithox discover --config-for <id> # a copy-pasteable client config
jithox discover --offline # a dated local snapshot onlyAn online run where every source answered records the result to
~/.jithox/discovery-snapshot.json (mode 600), which is what --offline reads
back. An incomplete run never overwrites that file: a source outage would
otherwise shrink the offline answer silently. Capabilities read from a snapshot
are reported as SNAPSHOT, never as live evidence, and an undated or unparseable
snapshot is refused rather than trusted.
The first real call
jithox call <tool> --input '<json>' makes one real tool call over the
public endpoint discovery found and writes a receipt. The CLI sends no
Authorization header for these requests; what the server checks is its own. It
runs discovery itself, online, in the same run: only evidence read now may
authorise a free call (a snapshot cannot, so there is no --offline).
Five free examples, also printed by jithox --help. Each line is a separate
call; the JSON quoting is for POSIX shells. In Windows PowerShell or cmd, put
the JSON in a file and use --input-file, or use field=value pairs for flat
inputs (for example, iban=BE68539007547034 after the tool name).
jithox call verify_iban --input '{"iban":"BE68 5390 0754 7034"}'
jithox call check_payment_change --input '{"newIban":"DE89 3704 0044 0532 0130 00","ibanOnFile":"BE68 5390 0754 7034","supplierCountry":"BE"}'
jithox call lookup_peppol_participant --input '{"identifier":"BE0403170701"}'
jithox call check_peppol_ready --input '{"invoiceNumber":"CLI-EXAMPLE-001","issueDate":"2026-10-02","currency":"EUR","buyerReference":"EXAMPLE","supplier":{"name":"Example Supplier","countryCode":"BE","endpointId":"0403170701","endpointScheme":"0208"},"customer":{"name":"Example Customer","countryCode":"BE","endpointId":"0417497106","endpointScheme":"0208"},"lines":[{"description":"Example service","quantity":1,"unitPrice":100,"vatPercent":21}],"documentAllowances":[],"documentCharges":[],"totalWithoutVat":100,"totalVat":21,"totalWithVat":121}'
jithox call preflight_payment --input '{"rail":"invoice_bank","approved":{"amount":"121.00","currency":"EUR","payee":{"name":"Example Supplier","iban":"BE68 5390 0754 7034"}},"payment":{"iban":"BE68 5390 0754 7034","amount":"121.00","currency":"EUR","payeeName":"Example Supplier","supplierCountry":"BE"},"ibanOnFile":"BE68 5390 0754 7034","instructionSource":"human"}'These are example inputs, not payment instructions or proof that an account belongs to a supplier. The Peppol checks can report unknown or unverified when a register does not answer; a completed call is not a passing check.
Read the result and the receipt from your own run: initialize and tools/list
succeeding do not prove the tool ran; only the tools/call step does. Then use
jithox doctor --local --json to verify the evidence it left behind.
What it refuses, before the endpoint is touched (discovery has read the
public manifest by then; nothing else has been sent): a tool discovery does
not know, a tool with any price other than included, a tool the manifest
does not declare side-effect free, evidence that is only a snapshot, and an
endpoint that is not on the --base origin (https, no port, no userinfo, no
query — a look-alike host in a manifest is refused, and a redirect is never
followed; "no port" here means "the same origin as --base", so a --base
you give with a port is honoured as given). "Free" and "read-only" are the
source's declarations; executing the call confirms neither, and the output
says so. A refusal exits 3 and writes no execution record, so the record of
your last real run is not overwritten. --sandbox and --offline are not
modes of this command: they exit 2 with a KitError, like every other
invalid invocation (test --sandbox is the simulation).
What the outcome words mean:
| Outcome | Meaning | Exit |
|---|---|---|
| EXECUTED | initialize, tools/list and the one tools/call all succeeded, the answer parsed as a JSON-RPC 2.0 MCP result whose isError is false or absent (the output says which), and a receipt verifies | 0 |
| REFUSED | the gate said no; nothing was called | 3 |
| NOT_REACHED | initialize or tools/list did not succeed, or the live endpoint does not list the tool; no tools/call was sent | 4 |
| UNKNOWN_RESULT | a tools/call was attempted and the tool answered isError, the answer was unreadable or not a JSON-RPC 2.0 result, the endpoint answered 5xx, the transport failed, or the tool answered without a reported error and the receipt could not be stored. Delivery, execution, effects and cost are unknown; the output says to check first and never to retry blindly (in the last case: repair the evidence store, do not run the call again) | 4 |
The receipt records step names and outcomes only — never your input, the
tool's output or a host name — through the same writer test and connect
use, so doctor --local reads it and receipt-integrity verifies it. The
digest is self-contained: a verified receipt is internally consistent, which
catches truncation and accidental edits, not an author who rewrites file and
digest together — and it proves nothing about who answered on the wire (TLS
to the endpoint is the only trust). The kit never retries a call by itself:
one call is one call.
A call through your connection
jithox call never sends a credential, so it cannot reach anything that needs
your connection. jithox run <tool> is that route: one tool call through
the connection gateway (/api/agent-connection/v1/mcp) — the same
authorisation, the same tools and the same rules the website and your MCP
client get. Nothing is decided in the kit.
jithox run <tool> [field=value ...] [--input-file <path>] [--input '<json>']
[--max-cost <minor units>] [--credential-file <path>] [--sandbox] [--json]What it does, in this order:
- The credential. From
--credential-file <path>when you give one, otherwise your OAuth login (jithox auth login), otherwise~/.jithox/connection. It is never accepted as an argument, never printed, and never written to a receipt, a log or--json. No credential: refused before any request, with the next step — create a connection on https://jithox.com/mcp/account ("Connect another device or agent"), save the credential it shows once in a private file, pass--credential-file. A file that does not hold ajxc_live_…credential is never sent anywhere on a guess.--sandboxrefuses a production credential, as everywhere else. - Discovery, with the credential. The gateway's discovery document says what this connection may do. An unknown tool, a tool that is not on offer, or a right the connection does not carry is refused before the call. The missing right is named in words and as its scope string. A connection's rights are fixed when it is created, so a missing one takes a new connection with that option ticked; trying again never helps.
- Cost is never implicit. When discovery says one attempt can cost money,
the call is refused unless you pass
--max-cost <minor units>of at least that amount (EUR 0.10is--max-cost 10). It is a ceiling for one attempt, not an amount to spend. A cost that cannot be read is refused.--yesis refused by the whole kit. - It says what will happen, then calls once. One block: tool, capability,
mode (read / prepare / act after a person's approval), the right used, the
maximum cost, the endpoint — and, for a tool that acts, discovery's approval
rule verbatim. Then exactly one
tools/callwith an idempotency key made for this attempt. It never retries, and it follows no redirect. - The result: the tool's structured result (redacted), and the gateway's
own account of the call —
executed(true / false / "uncertain"), what it charged for the call, and its receipt reference.
The quote desk, from the terminal
A connection needs two options that are off by default and are ticked when
the connection is created: "Draft quotes, show me the exact e-mail, and ask for
my approval" (actions:prepare:quote-desk) and, for the fourth line, "Carry out
a send I have approved" (actions:prepare:quote-desk-send).
request.json — the same file works from PowerShell 5.1, cmd.exe and a POSIX
shell, because nothing is quoted on the command line:
{
"customer": { "name": "Peters BV" },
"lines": [
{ "description": "Painting the living room", "quantity": 2, "unitPrice": 85, "vatPercent": 21 }
],
"quoteDate": "2026-09-18"
}QUOTE_ID and ACTION_ID are what the step before printed:
jithox run quote_desk_prepare --input-file request.json
jithox run quote_desk_preview_email quoteId=QUOTE_ID [email protected]
jithox run quote_desk_request_approval quoteId=QUOTE_ID
jithox run quote_desk_execute_approved actionId=ACTION_ID --max-cost 10
jithox run quote_desk_status quoteId=QUOTE_IDBetween the third and the fourth line a person approves on
https://jithox.com/quote-desk. This command can never approve, and the
approval is bound to the exact e-mail (snapshotSha256): change the quote, the
recipient or the text and it takes a new preview and a new approval. The fourth
line costs at most EUR 0.10 — the call is free; the approved e-mail it carries
out is metered — which is why it needs --max-cost 10.
What the fourth line can answer, and what each answer means:
| Outcome | Meaning | Exit |
|---|---|---|
| SENT | the service reports that the e-mail provider confirmed this send | 0 |
| WAITING_FOR_APPROVAL | no person has approved yet; nothing was sent and the provider was not contacted. Run the same command again afterwards (IN_PROGRESS, another delivery of the same e-mail is running, exits 10 as well) | 10 |
| REJECTED | the request was rejected or withdrawn; nothing was sent and nothing will be | 11 |
| NOT_SENT | confirmed: nothing left, nothing is pending. A new preview and a new approval start again | 12 |
| UNCERTAIN | unknown: the message may or may not have left. This command never retries it, and calling again sends nothing. A person settles it on the quote desk (delivered / not delivered) | 13 |
NOT_SENT and UNCERTAIN are different things and are never merged: the first
is an answer, the second is the absence of one. About a second e-mail, only
what the service itself documents: when a person approves the same e-mail again
after an uncertain outcome, the e-mail provider's idempotency key keeps that
re-approved send from becoming a second message for 24 hours — not for
ever. After that window the provider has forgotten the key, which is why the
quote desk asks the owner to check with the customer first.
Exit codes of run
| Exit | Meaning |
|---|---|
| 0 | the tool answered (a tool that acts after approval: SENT) |
| 2 | usage error (KitError): bad input, unreadable --input-file, --offline, a production credential under --sandbox |
| 3 | refused before the tool call: no credential (before any request at all), unknown tool, missing right, a cost you did not allow. No execution record is written |
| 4 | discovery could not be reached, or the outcome is unknown: 5xx, timeout, an unreadable answer, isError, a tool that did not finish, or HTTP 409 (the key was fresh, so a copy of this request arrived before). Check first; never retry blindly |
| 5 | a request was made and the gateway or the tool refused it before anything ran (401, 403, 402, 404, 429, or the tool's own refusal such as package_stale); the reason is printed |
| 10–13 | only for a tool whose capability acts after approval — see the table above |
The kit's receipt (~/.jithox/receipts/…, read by doctor --local) records step
names and outcomes only — never the input, the output, a recipient, an amount or
the credential. The tool's own receipt (quote_desk_status → receipt) is the
service's, and what it is worth is stated in discovery.
Rehearse it offline
Add --sandbox to every line. It is a local simulation: nothing leaves the
machine, nothing is charged, every line says LOCAL_SIMULATION, quote numbers
start with OFF-SBX- and identifiers with sbx_. It needs the sandbox marker
jithox init writes, and never takes a real credential. The person who approves
is simulated by a tool that exists only there — jithox.com does not know the
name, and jithox run without --sandbox refuses it before any call:
jithox run sandbox_person_decides actionId=ACTION_ID decision=approve --sandboxdecision=reject rejects; delivery=not_sent or delivery=uncertain makes the
simulated provider answer that way, so every exit code above can be rehearsed.
The simulated desk is kept between commands in ~/.jithox/sandbox-state.json
(mode 600; simulation data only — delete it whenever you like).
Building from a checkout
If you have the repository instead of the published package:
npm ci
npm --prefix packages/jithox-cli run build
node packages/jithox-cli/dist/cli.js --helpFor a checkout whose path contains spaces, quote both paths:
"$(command -v node)" "/path with spaces/jithox/packages/jithox-cli/dist/cli.js" --helpIn PowerShell, use the call operator with single-quoted paths:
& 'C:\path with spaces\node.exe' 'C:\path with spaces\jithox\packages\jithox-cli\dist\cli.js' --helpThe kit prints recovery, rollback, and next-step commands in the form that suits the copy you are running. From a checkout that is the running Node executable plus the compiled entrypoint, so build first or the referenced dist/cli.js will not exist. From an installed package it is plain jithox <command> — an installed user has no checkout, and a command naming an interpreter path stops working as soon as that path moves (an nvm install plus a version cleanup, a reinstalled runtime, another machine).
Two limits worth knowing. jithox <command> assumes the package's bin is on your PATH; if it is not, use npx jithox <command>, which the help banner also names. And "installed" is decided from whether the running entrypoint sits under a node_modules directory — a heuristic, not a guarantee: a checkout placed under such a directory is treated as installed, and a layout that resolves the entrypoint elsewhere (some pnpm/Yarn PnP setups) is treated as a checkout. Either way the command is still a printed instruction, never something the kit executes for you.
Safe local checks
node packages/jithox-cli/dist/cli.js doctor --sandbox --json
node packages/jithox-cli/dist/cli.js connect --prepare --sandbox --client generic_mcp --json--sandbox is local simulation only. --prepare writes nothing and does not request a credential. A real connection always shows its plan and waits for an interactive confirmation; OAuth sign-in happens in the selected client or browser, never through a secret passed on the command line.
Checkout workflow
Use the package scripts from this repository:
npm --prefix packages/jithox-cli run build
node packages/jithox-cli/dist/cli.js doctor
node packages/jithox-cli/dist/cli.js connect --prepare --client <client> --jsonSupported client ids are claude, claude_desktop, chatgpt, gemini, grok_cursor, generic_mcp, and generic_http.
The CLI never accepts credentials or tokens as command-line arguments. It does not publish, install, purchase, or call a provider during --help, doctor --sandbox, or connect --prepare --sandbox.
