getclaros 0.1.1 → 0.3.0
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/README.md +97 -7
- package/dist/cli.js +60 -52
- package/dist/hooks.js +348 -0
- package/dist/projects.js +179 -0
- package/dist/setup.js +347 -48
- package/dist/ui.js +1 -1
- package/dist/verdict.js +54 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# getclaros guard
|
|
2
2
|
|
|
3
|
-
A local stdio MCP proxy. It wraps an app's MCP server and checks every write tool
|
|
4
|
-
expert-approved Work Maps (`POST /v1/check`) before the call reaches the app
|
|
3
|
+
A local stdio MCP proxy, plus native hooks for coding agents. It wraps an app's MCP server and checks every write tool
|
|
4
|
+
call against the company's expert-approved Work Maps (`POST /v1/check`) before the call reaches the app; the hooks
|
|
5
|
+
check a coding agent's shell commands and file edits the same way ([Coding agents](#coding-agents-claude-code-codex-cursor-shell-commands-and-file-edits)). A call that breaks a rule is never
|
|
5
6
|
forwarded; the agent gets a tool error with the rule, the expert's words and who to ask.
|
|
6
7
|
|
|
7
8
|
The package name (`getclaros`) is a placeholder and lives only in `package.json`.
|
|
@@ -15,8 +16,13 @@ npx getclaros init
|
|
|
15
16
|
To try a local build, run `node dist/cli.js init` with `GETCLAROS_LAUNCHER="node /abs/path/to/dist/cli.js"` so the
|
|
16
17
|
configs launch your build instead of `npx -y getclaros`.
|
|
17
18
|
|
|
19
|
+
One command for every kind of agent: it guards the MCP servers your agent clients run, and, when run in a Playwright
|
|
20
|
+
or browser-use project, installs the browser hook with that project's own package manager (see
|
|
21
|
+
[Browser agents](#browser-agents-playwright-browser-use)). `npx getclaros doctor` then says, per agent type, whether it
|
|
22
|
+
is guarded and can reach Claros.
|
|
23
|
+
|
|
18
24
|
```
|
|
19
|
-
claros” getclaros 0.
|
|
25
|
+
claros” getclaros 0.3.0
|
|
20
26
|
|
|
21
27
|
Sign in opening your browser
|
|
22
28
|
› https://claros-eta.vercel.app/device?code=ZQRC-ZCTP
|
|
@@ -25,7 +31,7 @@ configs launch your build instead of `npx -y getclaros`.
|
|
|
25
31
|
✓ Signed in to workspace "Acme" key clr_7f3a91c2_…
|
|
26
32
|
<claros server> · key saved in ~/.config/getclaros/config.json, readable only by you
|
|
27
33
|
|
|
28
|
-
MCP
|
|
34
|
+
MCP agents
|
|
29
35
|
|
|
30
36
|
Claude Code ~/.claude.json
|
|
31
37
|
1 ● erpnext npx -y frappe-mcp-server Work Map "Process a supplier invoice"
|
|
@@ -81,9 +87,70 @@ What it changes, in files that already exist (it never creates a client config):
|
|
|
81
87
|
up front with `get_workflow`; in each client's user config (or its project config when it has no user config).
|
|
82
88
|
- Every file is backed up before it changes (`~/.config/getclaros/backups/<time>/`) and written atomically.
|
|
83
89
|
|
|
90
|
+
### Coding agents (Claude Code, Codex, Cursor): shell commands and file edits
|
|
91
|
+
|
|
92
|
+
The MCP guard sees MCP tool calls only. A coding agent also runs shell commands and edits files with its own built-in
|
|
93
|
+
tools, so `init` adds a native hook to each coding agent it finds (its directory exists in your home or the project):
|
|
94
|
+
the agent runs `npx -y getclaros hook <agent>` before each shell command or file edit, the hook asks Claros
|
|
95
|
+
(`POST /v1/check`) and answers in the agent's own protocol. MCP calls stay with the MCP guard, so nothing is checked
|
|
96
|
+
twice.
|
|
97
|
+
|
|
98
|
+
| Agent | Hooks file (project scope; `--user`: all projects) | Checked | stop | ask | nudge |
|
|
99
|
+
|---|---|---|---|---|---|
|
|
100
|
+
| Claude Code | `.claude/settings.json` (`~/.claude/settings.json`), `PreToolUse` on `Bash\|PowerShell\|Edit\|Write\|NotebookEdit` | shell, edits | denied; the rule, the expert's words and who to ask go to Claude | Claude Code's own ask: you confirm, with the owner and check id | allowed, the rule and fix added to Claude's context |
|
|
101
|
+
| Codex | `.codex/hooks.json` (`$CODEX_HOME/hooks.json`), `PreToolUse` on `Bash` and `apply_patch` | shell, edits (the files named in the patch) | denied, with the rule | denied with "ask <owner>" and the check id (Codex has no ask on `PreToolUse`) | allowed, added as context |
|
|
102
|
+
| Cursor | `.cursor/hooks.json` (`~/.cursor/hooks.json`), `beforeShellExecution` + `preToolUse` on `Write\|Delete` | shell, edits | denied; the rule goes to the agent | shell: Cursor's own ask; edits: denied with the owner (Cursor doesn't enforce ask on `preToolUse` yet) | allowed, the rule and fix go to the agent |
|
|
103
|
+
|
|
104
|
+
- The action Claros sees: `tool` `shell` with `{command, cwd, project}`, or `write_file` / `edit_file` /
|
|
105
|
+
`delete_file` with `{file_path (relative to the project), content or diff, project}`; `actor` is the agent. The
|
|
106
|
+
project is the agent's project directory, else the git root, else the working directory. Content over 20,000
|
|
107
|
+
characters is cut.
|
|
108
|
+
- Merged into what the file holds: your own hooks and settings stay, ours is appended once, and `init` again changes
|
|
109
|
+
nothing. `undo` restores the file from its backup, or, if you changed it since, removes only our entries.
|
|
110
|
+
- Fail-open by default: when Claros can't be reached, times out (`--timeout`, 5000 ms) or rejects the key, the action
|
|
111
|
+
runs and the agent shows a warning ("Claros couldn't check … it ran unchecked"). `init --fail-closed` installs
|
|
112
|
+
`hook <agent> --fail-closed`, which blocks instead.
|
|
113
|
+
- The hook reads the key from `$CLAROS_KEY`, the saved config or the project's `.env`, and the server from the saved
|
|
114
|
+
config (asked from the web app once, then cached). Nothing secret goes into the hooks file, so a project
|
|
115
|
+
`.claude/settings.json` can be committed; a teammate without a key gets the fail-open warning until they run `init`.
|
|
116
|
+
- Codex runs a new or changed hook only after you trust it in `/hooks`, and project hooks only in a trusted project.
|
|
117
|
+
- Each call starts `npx`; for less latency install the package (`npm i -g getclaros`) and run `init` with
|
|
118
|
+
`GETCLAROS_LAUNCHER=getclaros`.
|
|
119
|
+
- `--no-hooks` skips this part of `init`; `doctor` lists which agents are hooked and where.
|
|
120
|
+
|
|
121
|
+
### Browser agents (Playwright, browser-use)
|
|
122
|
+
|
|
123
|
+
`init` also looks at the project directory (the current one, or `--project <dir>`):
|
|
124
|
+
|
|
125
|
+
| Found | Install (never global) | Hook |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `@playwright/test` in package.json | `pnpm add -D` / `yarn add -D` / `bun add -d` / `npm install -D getclaros-browser`, by lockfile | writes `claros.fixture.ts` next to `playwright.config.*`; you change each spec's `from "@playwright/test"` to `from "<rel>/claros.fixture"` (init prints the exact line) |
|
|
128
|
+
| `playwright` (the library) in package.json | the same, as a dependency | printed: `import { guard } from "getclaros-browser"; await guard(page);` |
|
|
129
|
+
| `browser-use` in pyproject.toml / requirements*.txt / uv.lock / poetry.lock | `uv add` / `poetry add claros-browser`, else `pip install` into the project's virtualenv (none: printed, not run) | printed: `from claros_browser import guard_tools; guard_tools(tools)`, with the file that builds the `Agent` |
|
|
130
|
+
|
|
131
|
+
Each project gets its plan (the exact install command, the file it writes, where the key goes, the hook) and one
|
|
132
|
+
question, `Set up Playwright? Enter = yes`. `--yes` says yes without asking; `--no-browser-hooks` shows the plan and
|
|
133
|
+
changes nothing. User source files are never edited: the hook is printed.
|
|
134
|
+
|
|
135
|
+
Why a fixture file for Playwright Test: Playwright has no config-level page hook, and a fixture is its supported way to
|
|
136
|
+
run code for every test's `page`. The file lives in your repo (you can read it, compose it with your own fixtures), it
|
|
137
|
+
imports `getclaros-browser` and `@playwright/test` from your own `node_modules` (works with pnpm's strict layout), and
|
|
138
|
+
it re-exports everything from `@playwright/test`, so swapping the import never loses a type or helper. It also reads
|
|
139
|
+
`CLAROS_*` from `.env` when the shell doesn't set them, because Playwright doesn't read `.env` itself.
|
|
140
|
+
|
|
141
|
+
The browser hooks read `CLAROS_KEY` (and `CLAROS_URL` / `CLAROS_WEB_URL` when you aren't on the hosted Claros) from the
|
|
142
|
+
environment. `init` writes them to the project's `.env` only when it already exists, git ignores it and git doesn't
|
|
143
|
+
track it (no repo: the `.gitignore` next to it decides). Otherwise it prints an export line that reads the saved key
|
|
144
|
+
without showing it: `export CLAROS_KEY="$(node -p "require('$HOME/.config/getclaros/config.json').key")"`.
|
|
145
|
+
browser-use loads `.env` itself.
|
|
146
|
+
|
|
147
|
+
The fixture file is new (undo deletes it, unless you changed it) and `.env` is backed up like any config (undo restores
|
|
148
|
+
it, or, if you edited it since, removes only the `CLAROS_*` lines). Undo doesn't uninstall the package.
|
|
149
|
+
|
|
84
150
|
```
|
|
85
|
-
npx getclaros test #
|
|
86
|
-
npx getclaros
|
|
151
|
+
npx getclaros test # made-up records checked against your rules → prints the stop (and, in the demo, a nudge with its fix)
|
|
152
|
+
npx getclaros doctor # per agent type: guarded? reachable? (server, key, MCP wrapping, browser package + hook + key), with the fix
|
|
153
|
+
npx getclaros status # the same as doctor
|
|
87
154
|
npx getclaros undo # undo the last init; --all: every init, plus any guard wrapping it finds
|
|
88
155
|
npx getclaros export <workflow_id> [--format agents|design|skill] > AGENTS.md # a map's rules as markdown
|
|
89
156
|
```
|
|
@@ -99,7 +166,30 @@ rules page. Exit codes: 0 sent, 1 no such check / not valid, 2 bad usage, 3 unre
|
|
|
99
166
|
|
|
100
167
|
`test` asks Claros for a record that breaks a precise rule of a Work Map for one of your guarded servers
|
|
101
168
|
(`GET /v1/check/example?app=…`), checks it with `POST /v1/check` and prints the stop. Nothing is sent to the app.
|
|
102
|
-
|
|
169
|
+
|
|
170
|
+
With no guarded server matching a map it runs a demo on two Starter maps (added to your workspace the first time, as
|
|
171
|
+
drafts that are never enforced; delete them on the web and they stay deleted): a production deploy during a release
|
|
172
|
+
freeze, which is stopped, and a coding agent's stylesheet with a raw hex colour, which the judge nudges with a fix.
|
|
173
|
+
Both are checked side by side and it ends with a `Demo:` note:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
◉ Stopped · ✎ Nudged 2 checks in 827 ms
|
|
177
|
+
|
|
178
|
+
◉ stop No production deploys during the release freeze.
|
|
179
|
+
“Nothing goes to prod during the freeze. Not even a typo fix.” — Release manager
|
|
180
|
+
Who to ask: Release manager
|
|
181
|
+
Test action: deploy deploy.environment = production, deploy.freeze_active = true
|
|
182
|
+
|
|
183
|
+
✎ nudge No hand-picked hex values in UI code; use the design tokens (var(--primary) and the rest).
|
|
184
|
+
“If it's not a token, it's a bug. No hand-picked hex values.” — Design systems lead
|
|
185
|
+
→ Replace #7C3AED and #FFFFFF with design tokens like var(--primary)
|
|
186
|
+
Test action: write_file file_path = src/components/Button.css, content = .button { background: #7C3AED; color: #FFFFFF; border-radius: 6px; }
|
|
187
|
+
|
|
188
|
+
Your agent gets a stop as the tool's error; a nudge lets the call through, with the fix. Nothing reached any app.
|
|
189
|
+
Demo: no server is guarded yet, so this used the Starter maps (examples, not enforced; delete them on the web).
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
For a guarded server whose app has a Work Map:
|
|
103
193
|
|
|
104
194
|
```
|
|
105
195
|
◉ Stopped in 1.3 s Work Map "Process a supplier invoice" · check chk_0b14cb5eef
|
package/dist/cli.js
CHANGED
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
// When Claros needs the record a call targets (a submit by name, a partial record, an item's group), it answers
|
|
8
8
|
// `need`; the guard reads those records itself through the wrapped server's read tools (ids the client never sees),
|
|
9
9
|
// or through another guarded server's guard when the need names its app (peers.ts), and asks again. stdout carries the protocol: every log line goes to stderr.
|
|
10
|
+
// An "ask" opens a question to the rule's owner in Claros: the guard waits (--ask-wait, default 120 s) on the decision
|
|
11
|
+
// URL and forwards the call only when it is approved for this exact call (sha256 of {app, tool, arguments}); a denial
|
|
12
|
+
// or no answer comes back as the stop, with the reason.
|
|
10
13
|
import { spawn } from "node:child_process";
|
|
11
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
15
|
import { readFileSync } from "node:fs";
|
|
13
16
|
import { parseArgs } from "node:util";
|
|
14
17
|
import { reduceRows, rowsOf } from "./query.js";
|
|
18
|
+
import { nudgesOf, nudgeText, stopText } from "./verdict.js";
|
|
15
19
|
import { ask, serve } from "./peers.js";
|
|
16
20
|
import { DEFAULT_WEB, loadConfig, relocate, resolve } from "./config.js";
|
|
17
21
|
// the package name lives in package.json only (placeholder until decided)
|
|
@@ -20,8 +24,11 @@ const NAME = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)
|
|
|
20
24
|
const READ = /^(get|list|search|read|find|describe|fetch)|^(check|run|export)_|^(version|ping)$/i;
|
|
21
25
|
const LOOKUP_TTL = 60_000, KINDS_TTL = 30_000;
|
|
22
26
|
const GUARD_USAGE = ` ${NAME} guard --server <name> [--url <claros server> | --web <claros web app>] [--key <key> | env CLAROS_KEY]
|
|
23
|
-
[--timeout <ms>] [--fail-open]
|
|
27
|
+
[--timeout <ms>] [--fail-open] [--ask-wait <seconds, 0 = don't wait>]
|
|
28
|
+
(-- <command> [args...] | --remote <mcp url> [--header "K: V" ...])
|
|
24
29
|
${NAME} mcp (the Claros MCP server over stdio, with the saved key)
|
|
30
|
+
${NAME} hook <claude-code|codex|cursor> [--fail-closed] [--timeout <ms>] (run by the agent per shell
|
|
31
|
+
command or file edit; init installs it)
|
|
25
32
|
Server: --url / CLAROS_URL, else the saved one, else asked from the web app (--web / CLAROS_WEB, default
|
|
26
33
|
https://claros-eta.vercel.app, at /.well-known/claros.json). Help: ${NAME} <command> --help`;
|
|
27
34
|
const log = (s) => process.stderr.write(`[${NAME} guard] ${s}\n`);
|
|
@@ -60,52 +67,11 @@ function parse(b) {
|
|
|
60
67
|
return undefined;
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
? `Claros held this action: ${hits.length === 1 ? "a rule" : "rules"} from the ${from} ${hits.length === 1 ? "says" : "say"} to ask first. Do not run it without that answer.`
|
|
69
|
-
: `Claros stopped this action: it breaks ${hits.length === 1 ? "a rule" : "rules"} from the ${from}. Do not retry it as is.`];
|
|
70
|
-
for (const v of hits) {
|
|
71
|
-
const who = v.who_to_ask || v.owner || v.expert || "the process owner";
|
|
72
|
-
out.push("", `Rule: ${v.rule}`);
|
|
73
|
-
if (v.reason)
|
|
74
|
-
out.push(`Why Claros stopped it: ${v.reason}`); // fuzzy rules: the judge's one-line reason
|
|
75
|
-
if (v.quote)
|
|
76
|
-
out.push(`${v.source === "doc" ? "In the written policy" : "In the expert's words"}${v.expert ? ` (${v.expert})` : ""}: "${v.quote}"`);
|
|
77
|
-
out.push(`Who to ask: ${who}`);
|
|
78
|
-
out.push(v.action === "ask"
|
|
79
|
-
? `Next: stop and ask ${who} before doing this; continue only with their answer.`
|
|
80
|
-
: `Next: change the record so the rule holds, or ask ${who} to approve an exception. Tell the user why.`);
|
|
81
|
-
}
|
|
82
|
-
// an override goes to the rule's expert as a report; the rule stays as it is until they confirm a change
|
|
83
|
-
if (r.check_id) {
|
|
84
|
-
out.push("", "If this stop is wrong, tell the rule's expert why (the rule stays until they change it):");
|
|
85
|
-
for (const v of hits)
|
|
86
|
-
out.push(`npx ${NAME} override ${r.check_id} --rule ${v.guardrail_id} --reason "<why it was fine>"`);
|
|
87
|
-
}
|
|
88
|
-
return out.join("\n");
|
|
89
|
-
}
|
|
90
|
-
// a nudge lets the call through; "warn" is its old name (older Claros servers)
|
|
91
|
-
const nudgesOf = (r) => (r.violations || []).filter((v) => v.action === "nudge" || v.action === "warn");
|
|
92
|
-
/** The extra text item on a nudged call's result: the rule, the expert's words and the fix, so the agent can
|
|
93
|
-
* correct this record (if it still can) and the next ones. */
|
|
94
|
-
function nudgeText(r) {
|
|
95
|
-
const hits = nudgesOf(r);
|
|
96
|
-
const out = [`Claros nudge: the call went through, but it doesn't follow ${hits.length === 1 ? "a rule" : "rules"} ` +
|
|
97
|
-
`your team holds. Fix it if you still can, and follow ${hits.length === 1 ? "it" : "them"} from now on.`];
|
|
98
|
-
for (const v of hits) {
|
|
99
|
-
out.push("", `Rule: ${v.rule}`);
|
|
100
|
-
if (v.reason)
|
|
101
|
-
out.push(`Why: ${v.reason}`);
|
|
102
|
-
if (v.quote)
|
|
103
|
-
out.push(`${v.source === "doc" ? "In the written policy" : "In the expert's words"}${v.expert ? ` (${v.expert})` : ""}: "${v.quote}"`);
|
|
104
|
-
if (v.fix)
|
|
105
|
-
out.push(`Suggested fix: ${v.fix}`);
|
|
106
|
-
}
|
|
107
|
-
return out.join("\n");
|
|
108
|
-
}
|
|
70
|
+
/** JSON with sorted keys, as the server's approvals.payload_hash canonicalises it. */
|
|
71
|
+
const canon = (v) => Array.isArray(v) ? v.map(canon) : v !== null && typeof v === "object"
|
|
72
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, canon(v[k])])) : v;
|
|
73
|
+
/** sha256 of the exact action: an approval in Claros carries the hash of the call it was given for. */
|
|
74
|
+
const payloadHash = (app, tool, args) => createHash("sha256").update(JSON.stringify(canon({ app, tool, arguments: args }))).digest("hex");
|
|
109
75
|
/** fetch, and once more at the web app's current server address if this one didn't answer (it moved). */
|
|
110
76
|
async function reach(o, path, init = {}) {
|
|
111
77
|
const go = async () => {
|
|
@@ -128,12 +94,34 @@ async function check(o, tool, args, fetched = {}) {
|
|
|
128
94
|
method: "POST",
|
|
129
95
|
headers: { "content-type": "application/json", ...(o.key ? { authorization: `Bearer ${o.key}` } : {}) },
|
|
130
96
|
body: JSON.stringify({ app: o.server, tool, action: tool, state: flatten(args ?? {}), arguments: args ?? {},
|
|
131
|
-
...(Object.keys(fetched).length ? { fetched } : {}) }),
|
|
97
|
+
...(Object.keys(fetched).length ? { fetched } : {}), ...(o.askWait > 0 ? { ask_wait: o.askWait } : {}) }),
|
|
132
98
|
});
|
|
133
99
|
if (!res.ok)
|
|
134
100
|
throw new Error(`HTTP ${res.status}`);
|
|
135
101
|
return res.json();
|
|
136
102
|
}
|
|
103
|
+
/** An ask's answer: long-polls the decision URL until the owner decides or Claros times the question out (at
|
|
104
|
+
* ask_wait, plus a few seconds' grace). null = still open (Claros unreachable or late). */
|
|
105
|
+
async function decision(o, r, hash) {
|
|
106
|
+
const end = Date.now() + o.askWait * 1000 + 5000;
|
|
107
|
+
const headers = o.key ? { authorization: `Bearer ${o.key}` } : {};
|
|
108
|
+
while (Date.now() < end) {
|
|
109
|
+
const wait = Math.max(1, Math.min(25, Math.ceil((end - Date.now()) / 1000)));
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`${o.url}${r.question.decision_url}?wait=${wait}&payload_sha256=${hash}`, { headers, signal: AbortSignal.timeout(wait * 1000 + o.timeout) });
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
throw new Error(`HTTP ${res.status}`);
|
|
114
|
+
const v = await res.json();
|
|
115
|
+
if (v.status !== "open")
|
|
116
|
+
return v;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
log(`decision poll failed (${e.message}); retrying`);
|
|
120
|
+
await new Promise((ok) => setTimeout(ok, 1000));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
137
125
|
/** A read tool's result → the record: structuredContent, else JSON in the text (whole, or its outermost {...}). */
|
|
138
126
|
function recordOf(tool, m) {
|
|
139
127
|
if (m?.error || m?.result?.isError)
|
|
@@ -369,7 +357,20 @@ function guard(o, connect) {
|
|
|
369
357
|
return toServer(line);
|
|
370
358
|
}
|
|
371
359
|
log(`${o.server} ${name} → ${r.result === "ask" ? "ask" : "stop"} ${wf} ${ids} ${ms} ${r.check_id ?? ""}`);
|
|
372
|
-
|
|
360
|
+
if (r.result !== "ask" || !r.question?.decision_url || o.askWait <= 0)
|
|
361
|
+
return reply(stopText(r));
|
|
362
|
+
const hash = payloadHash(o.server, name, args ?? {});
|
|
363
|
+
const who = r.violations?.find((v) => v.action === "ask")?.who_to_ask || "the rule's owner";
|
|
364
|
+
log(` waiting up to ${o.askWait}s for ${who}'s answer`);
|
|
365
|
+
const v = await decision(o, r, hash);
|
|
366
|
+
if (v?.decision === "approve" && v.payload_sha256 === hash) {
|
|
367
|
+
log(`${o.server} ${name} → approved by ${v.approver} (${v.authority}) ${r.check_id}`);
|
|
368
|
+
return toServer(line);
|
|
369
|
+
}
|
|
370
|
+
log(`${o.server} ${name} → ${v?.decision ?? "no answer"} ${r.check_id}`);
|
|
371
|
+
reply(stopText(r, v?.decision === "approve" ? "Claros stopped this action: the approval was for a different action than this call. Do not run it."
|
|
372
|
+
: v?.decision === "deny" ? `Claros stopped this action: ${v.approver ?? who} said no${v.reason ? `: ${v.reason}` : "."} Do not retry it as is.`
|
|
373
|
+
: `Claros stopped this action: no answer from ${who} within ${o.askWait} s. Do not run it without their answer.`));
|
|
373
374
|
}
|
|
374
375
|
lines(process.stdin, (line) => {
|
|
375
376
|
const msg = parse(line);
|
|
@@ -404,7 +405,11 @@ async function main(argv) {
|
|
|
404
405
|
}
|
|
405
406
|
if (argv[0] === "mcp")
|
|
406
407
|
return void import("./bridge.js").then((m) => m.bridge());
|
|
407
|
-
if ([
|
|
408
|
+
if (argv[0] === "hook") { // a coding agent's per-tool-call hook (hooks.ts)
|
|
409
|
+
process.exitCode = await (await import("./hooks.js")).main(argv.slice(1));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (["init", "test", "status", "doctor", "undo", "export", "override"].includes(argv[0])) {
|
|
408
413
|
process.exitCode = await (await import("./setup.js")).run(argv);
|
|
409
414
|
return;
|
|
410
415
|
}
|
|
@@ -421,7 +426,7 @@ async function main(argv) {
|
|
|
421
426
|
options: {
|
|
422
427
|
server: { type: "string" }, url: { type: "string" }, web: { type: "string" }, key: { type: "string" },
|
|
423
428
|
timeout: { type: "string" }, "fail-open": { type: "boolean" }, remote: { type: "string" },
|
|
424
|
-
header: { type: "string", multiple: true },
|
|
429
|
+
header: { type: "string", multiple: true }, "ask-wait": { type: "string" },
|
|
425
430
|
},
|
|
426
431
|
}).values;
|
|
427
432
|
}
|
|
@@ -432,6 +437,9 @@ async function main(argv) {
|
|
|
432
437
|
return fail(); // a command or a remote URL, not both
|
|
433
438
|
if (v.remote && !/^https?:\/\/\S+$/.test(v.remote))
|
|
434
439
|
return fail("--remote wants an http(s) URL\n");
|
|
440
|
+
const askWait = Number(v["ask-wait"] ?? 120);
|
|
441
|
+
if (!(askWait >= 0))
|
|
442
|
+
return fail("--ask-wait wants seconds (0 = don't wait)\n");
|
|
435
443
|
const headers = {};
|
|
436
444
|
for (const h of v.header ?? []) {
|
|
437
445
|
const i = h.indexOf(":");
|
|
@@ -451,6 +459,6 @@ async function main(argv) {
|
|
|
451
459
|
}
|
|
452
460
|
if (!at.key)
|
|
453
461
|
log("no key (set CLAROS_KEY): every write check will fail" + (v["fail-open"] ? " open" : " closed"));
|
|
454
|
-
guard({ ...at, server: v.server, failOpen: !!v["fail-open"], timeout: Number(v.timeout ?? 5000) }, v.remote ? http(v.remote, headers) : stdio(cmd[0], cmd.slice(1)));
|
|
462
|
+
guard({ ...at, server: v.server, failOpen: !!v["fail-open"], timeout: Number(v.timeout ?? 5000), askWait }, v.remote ? http(v.remote, headers) : stdio(cmd[0], cmd.slice(1)));
|
|
455
463
|
}
|
|
456
464
|
main(process.argv.slice(2));
|