getclaros 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timur Monasypov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,254 @@
1
- # Temporary Holding Version
1
+ # getclaros guard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A local stdio MCP proxy. It wraps an app's MCP server and checks every write tool call against the company's
4
+ expert-approved Work Maps (`POST /v1/check`) before the call reaches the app. A call that breaks a rule is never
5
+ forwarded; the agent gets a tool error with the rule, the expert's words and who to ask.
6
+
7
+ The package name (`getclaros`) is a placeholder and lives only in `package.json`.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ npx getclaros init
13
+ ```
14
+
15
+ To try a local build, run `node dist/cli.js init` with `GETCLAROS_LAUNCHER="node /abs/path/to/dist/cli.js"` so the
16
+ configs launch your build instead of `npx -y getclaros`.
17
+
18
+ ```
19
+ claros” getclaros 0.1.1
20
+
21
+ Sign in opening your browser
22
+ › https://claros-eta.vercel.app/device?code=ZQRC-ZCTP
23
+ Code ZQRC-ZCTP approve only if the page shows this code
24
+
25
+ ✓ Signed in to workspace "Acme" key clr_7f3a91c2_…
26
+ <claros server> · key saved in ~/.config/getclaros/config.json, readable only by you
27
+
28
+ MCP servers
29
+
30
+ Claude Code ~/.claude.json
31
+ 1 ● erpnext npx -y frappe-mcp-server Work Map "Process a supplier invoice"
32
+ 2 ○ github docker run -i …
33
+ 3 ○ playwright (project ~/proj) npx @playwright/mcp@latest
34
+
35
+ Cursor ~/.cursor/mcp.json
36
+ 4 ● erpnext npx frappe-mcp-server Work Map "Process a supplier invoice"
37
+
38
+ · 3 remote servers need OAuth — guard them later: linear, notion, figma
39
+
40
+ ● matches a Work Map: recommended
41
+ Guard which? Enter = 1,4 (recommended) · numbers like 1,3 · all · none ›
42
+
43
+ ✓ Guarded erpnext in Claude Code, Cursor
44
+ ✓ Added the Claros MCP server in Claude Code, Cursor
45
+ every file backed up first: ~/.config/getclaros/backups/2026-10-24T01-34-53-159Z
46
+
47
+ Next restart Claude Code, Cursor, then run npx getclaros test
48
+ undo anytime: npx getclaros undo
49
+ ```
50
+
51
+ On a terminal the output is coloured in Claros's inks (stop magenta, ask green, nudge vermilion) with a spinner while
52
+ it waits for the browser. Piped, it is plain text with no escape codes and no header; `NO_COLOR` turns colour off,
53
+ `FORCE_COLOR` turns it on. Old Windows consoles get ASCII marks.
54
+
55
+ `init` signs you in through the browser (RFC 8628 device flow): it prints a URL and a code (XXXX-XXXX), opens the
56
+ URL (`--no-browser` to skip), and once you approve on Claros's `/device` page and pick a workspace it receives an
57
+ `agent` key for that workspace. A key from `--key` / `$CLAROS_KEY` / the saved config wins; `--paste` (or a Claros
58
+ without device sign-in) asks for a hidden paste instead. It checks the key, finds your agent clients' MCP configs, lists their servers and
59
+ asks which to guard. Servers whose name matches an app with a Work Map are preselected. `--yes` takes that default,
60
+ `--servers a,b` picks by name, `--project <dir>` sets where project configs are read (default: the current
61
+ directory), `--no-mcp` skips the Claros MCP entry. Restart the agent client afterwards.
62
+
63
+ | Client | Files read |
64
+ |---|---|
65
+ | Claude Code | `<project>/.mcp.json`; `~/.claude.json` (`mcpServers` and each `projects[path].mcpServers`) |
66
+ | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json`, `%APPDATA%\Claude\…`, `~/.config/Claude/…` |
67
+ | Cursor | `~/.cursor/mcp.json`, `<project>/.cursor/mcp.json` |
68
+ | VS Code | the profile's `mcp.json` (`~/Library/Application Support/Code/User/mcp.json`, …), `<project>/.vscode/mcp.json` |
69
+ | Codex | `~/.codex/config.toml` (`$CODEX_HOME`), `<project>/.codex/config.toml`: `[mcp_servers.<name>]` tables |
70
+
71
+ What it changes, in files that already exist (it never creates a client config):
72
+
73
+ - Each picked stdio server: `command` becomes `npx`, and `-y getclaros guard --server <name> --` plus the original
74
+ command go in front of the original `args`. `env`, `cwd`, comments and everything else stay. Already-guarded
75
+ servers are left alone, so running `init` again changes nothing.
76
+ - Each picked remote (`url`) server with static headers: the entry becomes a stdio one running
77
+ `npx -y getclaros guard --server <name> --remote <url> --header "K: V"…`, which reaches the server over Streamable
78
+ HTTP. The headers stay in that file and are never sent to Claros. Servers that need OAuth (no static header), take
79
+ their token from an env var or helper, or speak legacy SSE are listed as not guarded yet.
80
+ - A `claros` server (`npx -y getclaros mcp`, a stdio bridge to Claros's `/mcp`) so agents can read the Work Map
81
+ up front with `get_workflow`; in each client's user config (or its project config when it has no user config).
82
+ - Every file is backed up before it changes (`~/.config/getclaros/backups/<time>/`) and written atomically.
83
+
84
+ ```
85
+ npx getclaros test # a made-up record that breaks one of your rules → prints the stop (rule, expert's words, who to ask)
86
+ npx getclaros status # which servers are guarded, Claros reachable, key valid
87
+ npx getclaros undo # undo the last init; --all: every init, plus any guard wrapping it finds
88
+ npx getclaros export <workflow_id> [--format agents|design|skill] > AGENTS.md # a map's rules as markdown
89
+ ```
90
+
91
+ `export` prints a Work Map's rules (`GET /v1/maps/<id>/export`) as an AGENTS.md section, a DESIGN.md-style file or a
92
+ SKILL.md: an advisory copy for tools that read rules files; Claros still enforces the rules when the agent acts.
93
+ Exit codes: 0 printed, 1 no such map, 2 not signed in / bad usage, 3 Claros unreachable or key rejected.
94
+
95
+ `override <check_id> --rule <id> --reason "…"` tells the rule's expert a check got it wrong
96
+ (`POST /v1/checks/<id>/override`; `--verdict wrong_stop` by default, or `wrong_nudge | missed | right`). Every stop
97
+ message ends with this command filled in. Nothing changes until the expert confirms a suggested change on the map's
98
+ rules page. Exit codes: 0 sent, 1 no such check / not valid, 2 bad usage, 3 unreachable or key rejected.
99
+
100
+ `test` asks Claros for a record that breaks a precise rule of a Work Map for one of your guarded servers
101
+ (`GET /v1/check/example?app=…`), checks it with `POST /v1/check` and prints the stop. Nothing is sent to the app.
102
+ With no guarded server matching a map it uses the workspace's newest map and ends with a `Demo:` note.
103
+
104
+ ```
105
+ ◉ Stopped in 1.3 s Work Map "Process a supplier invoice" · check chk_0b14cb5eef
106
+
107
+ ◉ stop Equipment over 5,000 is capex: book it to the capital equipment account, never to expense.
108
+ “Equipment above five thousand is an asset, it goes to capex.” — AP lead
109
+ Who to ask: Controller
110
+
111
+ Test action for erpnext, made up, only checked: items.0.item_group = Machinery, items.0.amount = 7200
112
+ Your agent gets the rule as the tool's error. Nothing reached erpnext.
113
+ ```
114
+
115
+ Exit codes: 0 stopped, 1 not stopped
116
+ or no map to test, 2 not signed in / bad usage, 3 Claros unreachable or key rejected (`status` uses 0 / 2 / 3 too).
117
+
118
+ `undo` restores a file from its backup when it is byte-for-byte what `init` wrote; if anything changed it since, it
119
+ unwraps the guarded servers and removes the `claros` entry in place instead, keeping the other edits, and says which
120
+ it did. What `undo` overwrites is backed up as well.
121
+
122
+ Security model:
123
+
124
+ - The key is stored once, in `~/.config/getclaros/config.json` (`$XDG_CONFIG_HOME`), mode 0600 in a 0700 directory,
125
+ never in a client config (project files such as `.mcp.json` are often committed). The guard and the bridge read it
126
+ from there (`--key` / `$CLAROS_KEY` still win). The key is never printed; output shows only its id
127
+ (`clr_<prefix>_…`).
128
+ - Backups hold the full previous config files (which can contain other servers' tokens): 0600 files in a 0700
129
+ directory. Delete `~/.config/getclaros/backups` once you're happy.
130
+ - The guard adds no credentials to the wrapped server: its `env` is passed through as before.
131
+
132
+ ## Usage
133
+
134
+ ```
135
+ export CLAROS_KEY=clr_... # a key with the `agent` scope
136
+ getclaros guard --server erpnext -- npx -y some-erpnext-mcp --site https://erp.example.com
137
+ ```
138
+
139
+ In an MCP client config, replace the server's `command`/`args` with the guard in front of them:
140
+
141
+ ```json
142
+ { "erpnext": { "command": "getclaros",
143
+ "args": ["guard", "--server", "erpnext", "--", "npx", "-y", "some-erpnext-mcp"],
144
+ "env": { "CLAROS_KEY": "clr_..." } } }
145
+ ```
146
+
147
+ | Flag | Default | |
148
+ |---|---|---|
149
+ | `--server <name>` | required | Sent as `app`; helps route the call to the right Work Map |
150
+ | `--url <base>` | `$CLAROS_URL`, else the URL `init` saved, else the one the web app names | Claros server base URL |
151
+ | `--web <origin>` | `$CLAROS_WEB`, else the web origin `init` saved, else `https://claros-eta.vercel.app` | Where to ask for the server address (`/.well-known/claros.json`) |
152
+ | `--key <key>` | `$CLAROS_KEY`, else the key `init` saved | Prefer the env var: flags show up in process lists |
153
+ | `--timeout <ms>` | `5000` | How long to wait for a check |
154
+ | `--fail-open` | off | Forward writes when Claros can't be reached (default: block them) |
155
+
156
+ Server address: `--url` / `$CLAROS_URL` win and are never changed. Otherwise the CLI uses the URL `init` saved, or asks
157
+ the web app (`GET <web>/.well-known/claros.json` → `{"api": "<server>"}`) and saves the answer with the web origin.
158
+ When the saved server doesn't answer at all (a connection error, not an HTTP error), it asks the web app once more and
159
+ follows a moved server, updating the saved config: no reinstall. A config made with `init --url` alone stays pinned to
160
+ that URL. `--help` / `-h` on any command prints the usage.
161
+
162
+ ## What it does
163
+
164
+ - Relays newline-delimited JSON-RPC both ways byte for byte; only `tools/call` is inspected.
165
+ - Every `tools/list` response is passed on unchanged, then its tools are registered with Claros
166
+ (`POST /v1/tools {app, tools:[{name, description, inputSchema, annotations}]}`) so Claros can map their arguments
167
+ to a Work Map's fields once per tool. Only new tools or tools whose definition changed are sent. Registration is
168
+ fire and forget: a failure is logged, retried on the next `tools/list`, and never delays or blocks a call.
169
+ - Read tools pass without a check. Claros's classification (`GET /v1/tools/kinds`: LLM-proposed, expert-overridable)
170
+ wins once known; until then `annotations.readOnlyHint === true`, or a name starting with get / list / search / read /
171
+ find / describe / fetch / check_ / run_ / export_, or exactly version / ping. Everything else is a write.
172
+ Kinds are refreshed after registration, on every `tools/list` and at most every 30 s on writes.
173
+ - Writes: `POST /v1/check` with `{app, tool, action: tool, state, arguments}`. `state` is the arguments flattened
174
+ (`{"items.0.amount": 7200, "supplier": "..."}`); the server checks each array row as its own record. When the
175
+ tool has a mapping (proposed by Claros, confirmed by the expert), the server reads `arguments` through it instead,
176
+ so argument names don't have to match the map (`debit_to`, `items[].amount`).
177
+ - Record resolution: when the tool's mapping references a record (a submit by name, a partial doc) or needs values the
178
+ app fills on save (an item's group), Claros answers `need: [{key, tool, arguments}]`. The guard calls those read
179
+ tools on the wrapped server itself, with ids under a per-process prefix that are never relayed to the agent, waits
180
+ at most `--timeout`, takes the record from `structuredContent` or the JSON in the text, and checks again with
181
+ `fetched`. A failed or missing read blocks the write (or forwards it with `--fail-open`). A `need` naming a tool the
182
+ guard doesn't consider a read is refused. Lookups (`cache: true`) are cached for 60 s; the record itself is read
183
+ every time.
184
+ - Cross-server lookups: a `need` with `app` (another guarded server's read tool) goes to that server's guard over a
185
+ Unix socket, `<config dir>/run/<hash of the server name>.sock` (0600, dir 0700). Each guard serves its own server's
186
+ read tools there to guards that present the same key (sha256), and only read tools. No guard for that app → the
187
+ read fails → the write is blocked. Not on Windows yet.
188
+ - Allowed → forwarded unchanged. Stopped (`stop` or `ask`) → an `isError` result under the same id; the app never sees the call.
189
+ Nudged (a rule broken whose response is `nudge`, e.g. a taste rule) → forwarded; the app's result comes back with
190
+ one more text item, "Claros nudge: …" (rule, the expert's words, the suggested fix), so the agent can correct itself.
191
+ A call several Work Maps govern is checked against each (milestone 7); the stop names every map whose rule it
192
+ breaks, and a rule taken from a written policy is quoted as "In the written policy (file § section)".
193
+ - Claros unreachable, timed out or rejecting the key → writes are blocked with "ask a person"; reads still pass.
194
+ - A JSON-RPC batch containing a write `tools/call` is rejected.
195
+ - One log line per decision on stderr (stdout carries the protocol).
196
+
197
+ ## How a stop looks
198
+
199
+ What the agent receives as the tool result (`isError: true`):
200
+
201
+ ```
202
+ Claros stopped this action: it breaks a rule from the expert-approved Work Map "wf_ap_invoice". Do not retry it as is.
203
+
204
+ Rule: Equipment over 5,000 is capex: book to the capital equipment account, never to expense.
205
+ In the expert's words (AP lead): "Equipment above five thousand is an asset, it goes to capex."
206
+ Who to ask: AP lead
207
+ Next: change the record so the rule holds, or ask AP lead to approve an exception. Tell the user why.
208
+ ```
209
+
210
+ An ask-first rule is held the same way (never run), but opens with "Claros held this action: a rule from the expert-approved Work Map … says to ask first." and its Next line says who to ask.
211
+
212
+ A nudge, appended to the app's own result (not an error):
213
+
214
+ ```
215
+ Claros nudge: the call went through, but it doesn't follow a rule your team holds. Fix it if you still can, and follow it from now on.
216
+
217
+ Rule: <the rule>
218
+ In the expert's words (<expert>): "<their words>"
219
+ Suggested fix: <the judge's fix, or the rule's fix template filled from the record>
220
+ ```
221
+
222
+ And on stderr:
223
+
224
+ ```
225
+ [getclaros guard] erpnext submit_purchase_invoice → stop wf_ap_invoice g1 31ms chk_0b14cb5eef
226
+ [getclaros guard] erpnext call_method → stop wf_ap_invoice g1 12ms (1 read) chk_8187eaa79f
227
+ [getclaros guard] erpnext create_purchase_invoice → nudge wf_ap_invoice g1 2ms chk_25cd79b34a
228
+ [getclaros guard] nudge: <the rule> (fix: <the fix>)
229
+ ```
230
+
231
+ ## Develop
232
+
233
+ ```
234
+ npm install
235
+ npm test # tsc + node:test (fake MCP server + fake Claros; init/test/undo against a temp HOME)
236
+ npm run smoke # starts its own Claros on a free port with a temp DB, seeds wf_ap_invoice; create_purchase_invoice
237
+ # (supplier_name, debit_to, items[].amount) passes unmapped, is stopped once the expert maps it;
238
+ # update_record {doctype, name, values: {docstatus: 1}} is stopped after the guard reads the record
239
+ # and the item (fetch recipe + derived amount + lookup)
240
+ # then smoke-init: empty temp HOME with a Claude Code config → init --yes → test (timed) → status,
241
+ # the Claros MCP bridge against the real /mcp, undo
242
+ cd e2e && npm run e2e # against a real ERPNext through frappe-mcp-server (docs/GATEWAY_E2E_ERPNEXT.md)
243
+ cd e2e && npm run horizontal # four maps made from written policies, guards around mcp-sqlite (helpdesk, crm),
244
+ # mcp-server-git, the filesystem server and (when it answers) the local Zammad; the real
245
+ # LLM mapper, router and judge; prints mapper / routing / per-case tables (docs/HORIZONTAL.md)
246
+ ```
247
+
248
+ The smoke runs the server with `CLAROS_TOOL_MAPPER=off` (no LLM proposals, offline) and maps the tool the way an
249
+ expert would, through `POST /v1/tools/{id}/mapping`.
250
+
251
+ Limits: until a tool is mapped, only argument names that match the map's field names or aliases are understood, and
252
+ an unmapped write nothing matches is allowed (logged `mapping_status: unmapped`). The record is read just before the
253
+ write, not locked: it can change in between. No Windows `.cmd` shims (spawn
254
+ without a shell). Fuzzy rules (`judge_yourself`) are not enforced yet (milestone 5).
package/dist/bridge.js ADDED
@@ -0,0 +1,72 @@
1
+ // getclaros mcp: the Claros MCP server (get_workflow, check_action, …) for any client, as a stdio server.
2
+ // Relays each newline-delimited JSON-RPC message to <url>/mcp (streamable HTTP) with the saved key, and the answer
3
+ // (JSON, or the `data:` events of an SSE answer) back on stdout. Every client speaks stdio, and the key stays in
4
+ // ~/.config/getclaros instead of each client's config.
5
+ import { relocate, resolve } from "./config.js";
6
+ export async function bridge() {
7
+ const log = (s) => process.stderr.write(`[getclaros mcp] ${s}\n`);
8
+ let at;
9
+ try {
10
+ at = await resolve();
11
+ }
12
+ catch (e) {
13
+ log(e.message);
14
+ return process.exit(2);
15
+ }
16
+ const key = at.key;
17
+ if (!key)
18
+ log("no key: run `npx getclaros init` (or set CLAROS_KEY)");
19
+ let session, version, chain = Promise.resolve();
20
+ const out = (m) => process.stdout.write(m.trim() + "\n");
21
+ async function send(line) {
22
+ let msg;
23
+ try {
24
+ msg = JSON.parse(line);
25
+ }
26
+ catch {
27
+ return;
28
+ }
29
+ const post = () => fetch(`${at.url}/mcp`, {
30
+ method: "POST", body: line, signal: AbortSignal.timeout(30_000),
31
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream",
32
+ ...(key ? { authorization: `Bearer ${key}` } : {}), ...(session ? { "mcp-session-id": session } : {}),
33
+ ...(version ? { "mcp-protocol-version": version } : {}) },
34
+ });
35
+ try {
36
+ // a server that doesn't answer may have moved: ask the web app once, then retry there
37
+ const r = await post().catch(async (e) => {
38
+ if (e.name === "TimeoutError" || !(await relocate(at)))
39
+ throw e;
40
+ log(`Claros moved: now ${at.url}`);
41
+ return post();
42
+ });
43
+ session = r.headers.get("mcp-session-id") ?? session;
44
+ const text = await r.text();
45
+ if (!r.ok)
46
+ throw new Error(`HTTP ${r.status}`);
47
+ const msgs = (r.headers.get("content-type") ?? "").includes("event-stream")
48
+ ? text.split(/\r?\n\r?\n/).map((ev) => ev.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("\n"))
49
+ : [text];
50
+ for (const m of msgs.filter((x) => x.trim())) {
51
+ version ??= JSON.parse(m)?.result?.protocolVersion;
52
+ out(m);
53
+ }
54
+ }
55
+ catch (e) {
56
+ if (msg?.id !== undefined) {
57
+ out(JSON.stringify({ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message: `Claros MCP at ${at.url}: ${e.message}` } }));
58
+ }
59
+ }
60
+ }
61
+ let buf = "";
62
+ process.stdin.setEncoding("utf8");
63
+ process.stdin.on("data", (c) => {
64
+ buf += c;
65
+ for (let i; (i = buf.indexOf("\n")) >= 0; buf = buf.slice(i + 1)) {
66
+ const line = buf.slice(0, i);
67
+ if (line.trim())
68
+ chain = chain.then(() => send(line)); // in order; the server is stateless, so this is just simple
69
+ }
70
+ });
71
+ process.stdin.on("end", () => chain.then(() => process.exit(0)));
72
+ }