llm-cli-gateway 2.13.0 → 2.13.2
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/.agents/skills/multi-llm-review/SKILL.md +111 -16
- package/CHANGELOG.md +69 -0
- package/dist/acp/errors.js +59 -3
- package/dist/api-http.js +16 -2
- package/dist/config.d.ts +8 -0
- package/dist/config.js +58 -19
- package/dist/connector-setup.d.ts +39 -0
- package/dist/connector-setup.js +86 -0
- package/dist/doctor.d.ts +39 -1
- package/dist/doctor.js +182 -3
- package/dist/http-transport.js +27 -3
- package/dist/index.d.ts +6 -5
- package/dist/index.js +199 -31
- package/dist/oauth.js +20 -15
- package/dist/remote-url.d.ts +28 -0
- package/dist/remote-url.js +61 -0
- package/dist/resources.js +3 -1
- package/dist/session-manager.d.ts +2 -0
- package/dist/session-manager.js +20 -1
- package/dist/workspace-registry.d.ts +9 -0
- package/dist/workspace-registry.js +24 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +75 -0
|
@@ -3,7 +3,7 @@ name: multi-llm-review
|
|
|
3
3
|
description: Parallel code reviews across Claude, Codex, Gemini, Grok, and Mistral. Use for quality analysis, bug finding, or security audit.
|
|
4
4
|
metadata:
|
|
5
5
|
author: verivus-oss
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.7"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Multi-LLM Code Review
|
|
@@ -24,10 +24,89 @@ Parallel reviews using gateway MCP tools. Each LLM has different strengths — c
|
|
|
24
24
|
|
|
25
25
|
Apply these on every dispatch unless the caller has explicitly overridden a rule in the current turn:
|
|
26
26
|
|
|
27
|
-
1. **
|
|
28
|
-
2.
|
|
29
|
-
3.
|
|
30
|
-
4. **
|
|
27
|
+
1. **Use the stdio gateway MCP surface only** — call the host's `mcp__gtwy__*` tools (or the equivalent stdio `gtwy` namespace exposed to the current agent). Do not use connector/shadow gateway tools when the user asked for stdio gateway validation.
|
|
28
|
+
2. **Omit `model`** — let the gateway use its configured default per CLI. Nominating a model risks deprecated IDs (`o3`, `o3-pro`, `gpt-4o`, ...) and capability mismatches. Call `list_models` only when the caller has asked for a specific variant.
|
|
29
|
+
3. **`approvalStrategy:"mcp_managed"`** is the skill dispatch default (the gateway schema default is `"legacy"`). It gates the request before execution. Grant full non-interactive verification permissions on every review round because permission grants are not assumed durable.
|
|
30
|
+
4. **Full verification access for reviewers is required** — reviewers need read, test/build, code-search, docs lookup, web/search, and provider-safe gateway introspection access where appropriate. Do not suppress tools or pass empty allowlists. If a provider-specific permission or MCP server required for verification is rejected, treat that as a review dispatch failure and fix the gateway/permission setup before proceeding, unless the user explicitly authorizes a degraded review.
|
|
31
|
+
5. **No wallclock timeout; poll every 90 s** — use `*_request_async`. Poll `llm_job_status` no more than once every 90 seconds. Do **not** cancel reviewer jobs for taking too long; cancel only on explicit user instruction or a terminal provider/runtime failure. `idleTimeoutMs` (no-output safeguard) is separate.
|
|
32
|
+
6. **Iterate until unconditional APPROVED** (review dispatches only) — every review prompt must end with a strict verdict requirement. On `NOT APPROVED`, `CHANGES_REQUIRED`, `BLOCKER`, or conditional approval, consolidate findings, apply fixes, refresh the verification report and exact diff/commit evidence, then re-dispatch the same reviewers. Repeat until unconditional approval or a concrete blocker remains after evidence-based rebuttal. This rule does **not** apply to pure implementation or non-review analysis dispatches.
|
|
33
|
+
|
|
34
|
+
## Standard Validation Gate
|
|
35
|
+
|
|
36
|
+
Use this gate whenever the user asks for "the other LLMs", "cross review",
|
|
37
|
+
"red team", "validation", or similar review work.
|
|
38
|
+
|
|
39
|
+
### Evidence Packet
|
|
40
|
+
|
|
41
|
+
Before dispatch, prepare a stable packet and pass the same packet to every
|
|
42
|
+
reviewer:
|
|
43
|
+
|
|
44
|
+
- **Corrective-program verification report**: the local verification report used
|
|
45
|
+
as the spec for the review. It must list claims, commands run, test names,
|
|
46
|
+
command-result digests, and code/doc evidence.
|
|
47
|
+
- **Exact change set**: commit SHA(s) when available, the diff range or explicit
|
|
48
|
+
`git diff` command, and the changed-file list. If the work is uncommitted,
|
|
49
|
+
state that clearly and include the exact dirty-file list.
|
|
50
|
+
- **Scope and invariants**: DAG step(s), issue/PR references, security
|
|
51
|
+
invariants, docs that define intended behavior, and out-of-scope files.
|
|
52
|
+
- **Review log from prior rounds**: verbatim reviewer findings plus per-finding
|
|
53
|
+
responses marked `FIXED`, `DISAGREE`, or `BLOCKER`, each with file:line, test,
|
|
54
|
+
command, or doc evidence.
|
|
55
|
+
|
|
56
|
+
The packet is not a substitute for inspection. The prompt must explicitly say
|
|
57
|
+
that the verification report and summary are claims, not evidence.
|
|
58
|
+
|
|
59
|
+
### Reviewer Contract
|
|
60
|
+
|
|
61
|
+
Every reviewer prompt must require:
|
|
62
|
+
|
|
63
|
+
- Verify claims against actual code, tests, docs, and upstream documentation.
|
|
64
|
+
- Open/read the changed files and relevant neighboring code directly.
|
|
65
|
+
- Run or inspect the cited tests/builds; approval cannot be based on a summary,
|
|
66
|
+
intent, plan-compliance, or "should be fixed" language.
|
|
67
|
+
- Cite concrete evidence for each finding: `file:line`, test name + command, or
|
|
68
|
+
upstream doc URL. If a claim cannot be verified, report it as at least a major
|
|
69
|
+
finding.
|
|
70
|
+
- Return strict structured output with `verdict`, `findings`, `inspected`, and
|
|
71
|
+
`unconditional_approval_blockers`.
|
|
72
|
+
|
|
73
|
+
Recommended final verdict enum:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"verdict": "APPROVED | CHANGES_REQUIRED | BLOCKER",
|
|
78
|
+
"findings": [
|
|
79
|
+
{
|
|
80
|
+
"id": "F1",
|
|
81
|
+
"severity": "blocker | major | minor | nit",
|
|
82
|
+
"file": "src/example.ts",
|
|
83
|
+
"line": 123,
|
|
84
|
+
"claim": "Exact claim reviewed",
|
|
85
|
+
"issue": "What is wrong",
|
|
86
|
+
"evidence": "file:line, command/test output digest, URL, or unable to verify: reason",
|
|
87
|
+
"suggested_fix": "Concrete fix"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"inspected": ["files, tests, commands, docs actually inspected"],
|
|
91
|
+
"unconditional_approval_blockers": ["F1"]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Evidence-Based Triage
|
|
96
|
+
|
|
97
|
+
For every finding:
|
|
98
|
+
|
|
99
|
+
- If correct: fix it, run the relevant local gates, update the verification
|
|
100
|
+
report, and start a new review round with a fresh exact change set.
|
|
101
|
+
- If disputed: respond to that reviewer through the gateway with code/doc/test
|
|
102
|
+
evidence. Do not rebut with assertion, intent, "by design", or "the code should
|
|
103
|
+
already do that" unless accompanied by the exact evidence. The reviewer must
|
|
104
|
+
withdraw the finding or provide counter-evidence.
|
|
105
|
+
- If unresolvable in scope: record it as a concrete `BLOCKER` with evidence and
|
|
106
|
+
stop advancing the change until the user decides scope.
|
|
107
|
+
|
|
108
|
+
"Approved with nits" is not unconditional approval. Fix the nits or get the
|
|
109
|
+
reviewer to withdraw them as non-blocking with evidence.
|
|
31
110
|
|
|
32
111
|
## Workflow
|
|
33
112
|
|
|
@@ -61,28 +140,30 @@ CLIs share Claude's tool names or MCP semantics. The same data is available as
|
|
|
61
140
|
|
|
62
141
|
### 2. Send Parallel Reviews
|
|
63
142
|
|
|
64
|
-
Sync tools auto-defer at 45s
|
|
143
|
+
Sync tools auto-defer at 45s, but review gates should prefer async tools. Poll
|
|
144
|
+
`jobId` via `llm_job_status` no more than once every 90s, fetch with
|
|
145
|
+
`llm_job_result` only after a terminal status.
|
|
65
146
|
|
|
66
147
|
**Tip — share the stable prefix across reviewers:** when the same long brief / file dump is sent to every reviewer, switch from `prompt` to the structured `promptParts` field. The gateway concatenates in canonical order `system → tools → context → task`, so every reviewer sees byte-identical stable prefix bytes, raising implicit cache hit rate at each provider. `prompt` and `promptParts` are mutually exclusive — the runtime returns `provide exactly one of \`prompt\` or \`promptParts\`` if both are supplied. After the round, read `cache-state://prefix/{hash}` (tokens/hashes only, no prompt text) to confirm reviewers actually shared the prefix.
|
|
67
148
|
|
|
68
149
|
**Claude — Quality & Architecture:**
|
|
69
150
|
```
|
|
70
|
-
|
|
151
|
+
claude_request_async({prompt:"Review the attached evidence packet and exact change set for architecture, design patterns, maintainability, and documentation gaps. The packet is a claim, not evidence. Verify against the changed files, neighboring source, tests, and docs directly. Cite file:line/test/doc evidence. Return strict JSON and end with APPROVED, CHANGES_REQUIRED, or BLOCKER.",approvalStrategy:"mcp_managed",optimizePrompt:true})
|
|
71
152
|
```
|
|
72
153
|
|
|
73
154
|
**Codex — Logic & Correctness:**
|
|
74
155
|
```
|
|
75
|
-
|
|
156
|
+
codex_request_async({prompt:"Review the attached evidence packet and exact change set for logic bugs, off-by-one errors, missing error handling, races, and test gaps. The packet is a claim, not evidence. Verify against code/tests/docs directly. Cite file:line/test/doc evidence. Return strict JSON and end with APPROVED, CHANGES_REQUIRED, or BLOCKER.",fullAuto:true,approvalStrategy:"mcp_managed",optimizePrompt:true})
|
|
76
157
|
```
|
|
77
158
|
|
|
78
159
|
**Gemini — Security & Edge Cases:**
|
|
79
160
|
```
|
|
80
|
-
|
|
161
|
+
gemini_request_async({prompt:"Review the attached evidence packet and exact change set for security issues: injection, auth bypasses, data leaks, OWASP Top 10, and crash-causing edge cases. The packet is a claim, not evidence. Verify against code/tests/docs/upstream docs directly. Cite evidence. Return strict JSON and end with APPROVED, CHANGES_REQUIRED, or BLOCKER.",approvalStrategy:"mcp_managed",optimizePrompt:true})
|
|
81
162
|
```
|
|
82
163
|
|
|
83
164
|
**Grok — Independent Diversity (optional 4th reviewer):**
|
|
84
165
|
```
|
|
85
|
-
|
|
166
|
+
grok_request_async({prompt:"Independent review of the attached evidence packet and exact change set. The packet is a claim, not evidence. Verify against code/tests/docs directly, flag blind spots, and contradict weak findings with evidence. Return strict JSON and end with APPROVED, CHANGES_REQUIRED, or BLOCKER.",approvalStrategy:"mcp_managed",optimizePrompt:true})
|
|
86
167
|
```
|
|
87
168
|
|
|
88
169
|
Add Grok when consensus matters (high-stakes changes, security-critical paths) or to break ties between the other three. Auth must already be set up (`grok login` OAuth or `GROK_CODE_XAI_API_KEY`).
|
|
@@ -122,7 +203,10 @@ grok_request_async({prompt:"Independent diversity review of all TS files in src/
|
|
|
122
203
|
mistral_request_async({prompt:"Independent Vibe review of all TS files in src/... End with APPROVED or NOT APPROVED with findings.",approvalStrategy:"mcp_managed",correlationId:"review-mistral"})
|
|
123
204
|
```
|
|
124
205
|
|
|
125
|
-
Poll with `llm_job_status`
|
|
206
|
+
Poll with `llm_job_status` no more than once every 90s, retrieve with
|
|
207
|
+
`llm_job_result` when terminal. Jobs are durable — if your polling wrapper times
|
|
208
|
+
out, re-issue the same call (the gateway auto-dedups onto the live job) or fetch
|
|
209
|
+
by `jobId` later (default 30-day retention).
|
|
126
210
|
|
|
127
211
|
For iterative Gemini reviews, pass `sessionId` for resumability:
|
|
128
212
|
|
|
@@ -135,23 +219,34 @@ gemini_request_async({prompt:"Deep security audit of src/... End with APPROVED o
|
|
|
135
219
|
|
|
136
220
|
These patterns undermine review quality and trigger review integrity warnings:
|
|
137
221
|
|
|
138
|
-
- **Don't
|
|
222
|
+
- **Don't use non-stdio gateway surfaces** when the requested validation path is
|
|
223
|
+
the local stdio gateway.
|
|
224
|
+
- **Don't provide only a summary** — provide the verification report and exact
|
|
225
|
+
change set, while requiring reviewers to verify against files/tests/docs
|
|
226
|
+
directly.
|
|
227
|
+
- **Don't under-grant review access** — full read, test/build, and MCP
|
|
228
|
+
verification access is the default. A partial-access review is not a valid
|
|
229
|
+
approval unless the user explicitly accepts that limitation.
|
|
139
230
|
- **Don't suppress tools** — never include "do not run tools" or "respond only based on code provided" in review prompts
|
|
140
231
|
- **Don't use `allowedTools:[]`** for reviews — reviewers need tool access to
|
|
141
232
|
verify claims
|
|
142
233
|
- **Don't copy tool names between providers** — Claude's `Read` / `Grep` /
|
|
143
234
|
`Glob` names are not Grok, Codex, Gemini, or Vibe allowlist names. Check
|
|
144
235
|
`provider_tool_capabilities` first, or omit provider tool allowlists.
|
|
145
|
-
- **
|
|
236
|
+
- **Don't cancel reviewer jobs** just because they are slow. Let them reach a
|
|
237
|
+
terminal status unless the user explicitly says to stop them.
|
|
238
|
+
- **Do provide file paths and exact diff identifiers** — `"Review commit
|
|
239
|
+
abc123, diff range base..head, changed files src/auth.ts ..."` instead of
|
|
240
|
+
vague summaries.
|
|
146
241
|
|
|
147
242
|
## Iteration Loop (mandatory)
|
|
148
243
|
|
|
149
244
|
Reviews are not one-shot. The caller runs this loop:
|
|
150
245
|
|
|
151
246
|
1. Dispatch reviewer(s) with the verdict clause in the prompt
|
|
152
|
-
2. Poll every
|
|
247
|
+
2. Poll every 90s if deferred; fetch result only after terminal status
|
|
153
248
|
3. Parse verdict from each reviewer — APPROVED / NOT APPROVED / conditional
|
|
154
|
-
4. **Any NOT APPROVED or conditional** →
|
|
249
|
+
4. **Any NOT APPROVED, CHANGES_REQUIRED, BLOCKER, or conditional** → triage each finding with evidence → fix or rebut with code/doc/test citations → refresh verification report and exact diff → re-dispatch same review → goto 2
|
|
155
250
|
5. **All APPROVED (unconditional)** → done
|
|
156
251
|
6. After 3 rounds without convergence, escalate to the user
|
|
157
252
|
|
|
@@ -169,6 +264,6 @@ Reviews are not one-shot. The caller runs this loop:
|
|
|
169
264
|
- Use all five async variants for true parallel reviews when you want Grok's
|
|
170
265
|
independent perspective and Mistral Vibe's fifth review
|
|
171
266
|
- Pass `sessionId` to `gemini_request_async` / `grok_request_async` for resumable follow-up
|
|
172
|
-
- Check for `status:"deferred"` in sync responses — poll `jobId` every
|
|
267
|
+
- Check for `status:"deferred"` in sync responses — poll `jobId` no more than once every 90s if present
|
|
173
268
|
- Gateway `mcpServers` default to the host's configured Claude MCP set; pass explicit server names only when the review needs those capabilities
|
|
174
269
|
- **Re-issuing after a polling timeout is safe** — auto-dedup (default 1 h window, `LLM_GATEWAY_DEDUP_WINDOW_MS`) reattaches the new call to the existing job. Use `forceRefresh:true` only when inputs genuinely changed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the llm-cli-gateway project.
|
|
4
4
|
|
|
5
|
+
## [2.13.2] - 2026-07-01: remote HTTP + OAuth connector UX and hardening
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`remote_http_oauth` readiness projection in `doctor --json`.** A stable,
|
|
10
|
+
ordered 8-stage decision tree (`not_started`, `missing_public_url`,
|
|
11
|
+
`endpoint_unreachable`, `oauth_disabled`, `unsafe_oauth_config`,
|
|
12
|
+
`missing_oauth_client`, `missing_workspace`, `ready`) with deterministic,
|
|
13
|
+
secret-free `next_actions`, the copy-safe connector URLs, `oauth.consent_required`,
|
|
14
|
+
and a path-free workspace summary. Validated by `setup/status.schema.json`.
|
|
15
|
+
- **`llm-cli-gateway connector setup` command.** Emits a copy-safe JSON connector
|
|
16
|
+
packet (MCP URL, authorization URL, token URL, client id, workspace guidance)
|
|
17
|
+
plus a human summary, reusing the readiness projection. The deprecated no-auth
|
|
18
|
+
connector URL is omitted unless `--include-legacy-no-auth` is passed.
|
|
19
|
+
- **Centralized remote URL construction (`src/remote-url.ts`).** doctor, the
|
|
20
|
+
connector packet, the CLI OAuth output, the installer, and the runtime OAuth
|
|
21
|
+
well-known metadata + `WWW-Authenticate` challenge all derive URLs from one
|
|
22
|
+
helper so they cannot drift.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **OAuth-first remote connector docs and setup UI.** The ChatGPT guide,
|
|
27
|
+
endpoint-exposure runbook, `ENDPOINT_EXPOSURE.md`, and setup UI lead with the
|
|
28
|
+
OAuth path keyed on `remote_http_oauth.stage`; the no-auth connector is
|
|
29
|
+
compatibility-only. `oauth client add` validates the redirect URI and client
|
|
30
|
+
id before writing, refuses duplicate ids, and labels copy-once secret output.
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
|
|
34
|
+
- **F17 hardening.** The unsafe-OAuth (public clients / `open_dev`) start gate
|
|
35
|
+
now fails closed when the gateway is publicly exposed via a public URL or
|
|
36
|
+
tunnel, not only on a non-loopback bind.
|
|
37
|
+
- **No local-path disclosure to remote clients.** Remote MCP `workspace_*`
|
|
38
|
+
tools, `session_get`, `sessions://*` resources, and worktree response paths
|
|
39
|
+
are path-redacted for remote HTTP/OAuth callers (local operators keep absolute
|
|
40
|
+
paths; resume metadata is unaffected). `workspace_register_existing_repo` no
|
|
41
|
+
longer acts as a filesystem existence oracle.
|
|
42
|
+
- **Installer URL redaction.** The Go installer derives OAuth/MCP URLs from a
|
|
43
|
+
clean origin and strips userinfo/query/fragment from the stored public URL, so
|
|
44
|
+
no credential-bearing URL is emitted.
|
|
45
|
+
- **gitleaks allowlist** now covers the `gateway-logger` redaction test fixtures
|
|
46
|
+
(deliberate fake tokens), restoring a green secret-scan gate.
|
|
47
|
+
|
|
48
|
+
## [2.13.1] - 2026-07-01: code-scanning hardening and review-gate discipline
|
|
49
|
+
|
|
50
|
+
### Security
|
|
51
|
+
|
|
52
|
+
- **CodeQL remediation for URL, ReDoS, logging, and OAuth cookie alerts.**
|
|
53
|
+
API endpoint joining no longer uses slash-trimming regexes; ACP message
|
|
54
|
+
redaction now uses a stack-based single-pass JSON payload scanner with
|
|
55
|
+
truncated structured-payload fallback; central gateway stderr logging redacts
|
|
56
|
+
message strings, structured args, and nested `Error` name/message/stack
|
|
57
|
+
values; the xAI missing-key error no longer leaks the configured env var name;
|
|
58
|
+
and OAuth consent CSRF cookies now include `HttpOnly`, `Secure`,
|
|
59
|
+
`SameSite=Lax`, `Path=/oauth`, and `Max-Age=300`.
|
|
60
|
+
- **Regression coverage for the fixed alert surfaces.** Added focused tests for
|
|
61
|
+
endpoint slash normalization, nested and truncated ACP payload redaction,
|
|
62
|
+
logger redaction before stderr output, generic xAI missing-key messaging, and
|
|
63
|
+
complete OAuth CSRF cookie attributes.
|
|
64
|
+
|
|
65
|
+
### Docs
|
|
66
|
+
|
|
67
|
+
- **Standardized cross-LLM review validation.** The shipped multi-LLM review
|
|
68
|
+
skill and best-practices guide now require the stdio gateway surface, no
|
|
69
|
+
provider model nomination, full non-interactive verification access,
|
|
70
|
+
90-second polling, exact evidence packets, no cancellation of slow reviewers,
|
|
71
|
+
evidence-based rebuttals, and iteration until unconditional approval or a
|
|
72
|
+
concrete blocker.
|
|
73
|
+
|
|
5
74
|
## [2.13.0] - 2026-07-01: Cursor Agent CLI provider
|
|
6
75
|
|
|
7
76
|
### Added
|
package/dist/acp/errors.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export function redactAcpMessage(input) {
|
|
2
|
-
let out = input;
|
|
3
|
-
out = out.replace(/\{[\s\S]*?\}/g, "<redacted-json>");
|
|
4
|
-
out = out.replace(/\[[\s\S]*?\]/g, "<redacted-json>");
|
|
2
|
+
let out = redactJsonLikeBodies(input);
|
|
5
3
|
out = out.replace(/\b(bearer|token|api[_-]?key|secret)\b\s*[:=]?\s*\S+/gi, "$1 <redacted>");
|
|
6
4
|
out = out.replace(/\b(sk|xai|gsk|key)-[A-Za-z0-9_-]{8,}\b/gi, "<redacted-token>");
|
|
7
5
|
out = out.replace(/(^|[^A-Za-z0-9._~\\/-])[A-Za-z]:\\[^\s"')\]}>]*/g, "$1<redacted-path>");
|
|
@@ -11,6 +9,64 @@ export function redactAcpMessage(input) {
|
|
|
11
9
|
out = out.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "<redacted-email>");
|
|
12
10
|
return out;
|
|
13
11
|
}
|
|
12
|
+
function redactJsonLikeBodies(input) {
|
|
13
|
+
let out = "";
|
|
14
|
+
let cursor = 0;
|
|
15
|
+
let bodyStart = -1;
|
|
16
|
+
let stack = [];
|
|
17
|
+
let inString = false;
|
|
18
|
+
let escaped = false;
|
|
19
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
20
|
+
const char = input.charAt(index);
|
|
21
|
+
if (bodyStart === -1) {
|
|
22
|
+
const close = char === "{" ? "}" : char === "[" ? "]" : "";
|
|
23
|
+
if (!close)
|
|
24
|
+
continue;
|
|
25
|
+
bodyStart = index;
|
|
26
|
+
stack = [close];
|
|
27
|
+
inString = false;
|
|
28
|
+
escaped = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (escaped) {
|
|
32
|
+
escaped = false;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (char === "\\") {
|
|
36
|
+
escaped = inString;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char === '"') {
|
|
40
|
+
inString = !inString;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (inString)
|
|
44
|
+
continue;
|
|
45
|
+
const nestedClose = char === "{" ? "}" : char === "[" ? "]" : "";
|
|
46
|
+
if (nestedClose) {
|
|
47
|
+
stack.push(nestedClose);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char !== stack[stack.length - 1])
|
|
51
|
+
continue;
|
|
52
|
+
stack.pop();
|
|
53
|
+
if (stack.length > 0)
|
|
54
|
+
continue;
|
|
55
|
+
out += input.slice(cursor, bodyStart);
|
|
56
|
+
out += "<redacted-json>";
|
|
57
|
+
cursor = index + 1;
|
|
58
|
+
bodyStart = -1;
|
|
59
|
+
}
|
|
60
|
+
if (bodyStart !== -1 && isLikelyJsonPayload(input.slice(bodyStart))) {
|
|
61
|
+
out += input.slice(cursor, bodyStart);
|
|
62
|
+
out += "<redacted-json>";
|
|
63
|
+
cursor = input.length;
|
|
64
|
+
}
|
|
65
|
+
return cursor === 0 ? input : out + input.slice(cursor);
|
|
66
|
+
}
|
|
67
|
+
function isLikelyJsonPayload(span) {
|
|
68
|
+
return /"(?:jsonrpc|method|params|prompt|content|body|token|secret|api[_-]?key|credential|auth|cwd|path|[A-Za-z0-9_-]+)"\s*:/i.test(span);
|
|
69
|
+
}
|
|
14
70
|
export function redactAcpDebug(value) {
|
|
15
71
|
const sensitiveKey = /(payload|body|prompt|content|token|secret|api[_-]?key|credential|auth|cwd|path)/i;
|
|
16
72
|
if (typeof value === "string") {
|
package/dist/api-http.js
CHANGED
|
@@ -40,8 +40,8 @@ export function isLoopbackUrl(value) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
export function buildEndpointUrl(baseUrl, path) {
|
|
43
|
-
const trimmedBase = baseUrl
|
|
44
|
-
const trimmedPath = path
|
|
43
|
+
const trimmedBase = stripTrailingSlashChars(baseUrl);
|
|
44
|
+
const trimmedPath = stripLeadingSlashChars(path);
|
|
45
45
|
const url = new URL(`${trimmedBase}/${trimmedPath}`);
|
|
46
46
|
if (url.protocol !== "https:" &&
|
|
47
47
|
!(url.protocol === "http:" && LOOPBACK_HOSTS.includes(url.hostname))) {
|
|
@@ -49,6 +49,20 @@ export function buildEndpointUrl(baseUrl, path) {
|
|
|
49
49
|
}
|
|
50
50
|
return url;
|
|
51
51
|
}
|
|
52
|
+
function stripTrailingSlashChars(value) {
|
|
53
|
+
let end = value.length;
|
|
54
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
55
|
+
end -= 1;
|
|
56
|
+
}
|
|
57
|
+
return value.slice(0, end);
|
|
58
|
+
}
|
|
59
|
+
function stripLeadingSlashChars(value) {
|
|
60
|
+
let start = 0;
|
|
61
|
+
while (start < value.length && value.charCodeAt(start) === 47) {
|
|
62
|
+
start += 1;
|
|
63
|
+
}
|
|
64
|
+
return value.slice(start);
|
|
65
|
+
}
|
|
52
66
|
export function isHttpTransient(error) {
|
|
53
67
|
const status = typeof error?.status === "number" ? error.status : null;
|
|
54
68
|
if (status === 429 || (status !== null && status >= 500))
|
package/dist/config.d.ts
CHANGED
|
@@ -162,4 +162,12 @@ export interface AcpConfig {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
export declare function loadAcpConfig(logger?: Logger): AcpConfig;
|
|
165
|
+
export type RemoteOAuthConfigStatus = "absent" | "disabled" | "enabled" | "malformed";
|
|
166
|
+
export interface RemoteOAuthConfigDiagnostics {
|
|
167
|
+
config: RemoteOAuthConfig;
|
|
168
|
+
status: RemoteOAuthConfigStatus;
|
|
169
|
+
configured: boolean;
|
|
170
|
+
issues: string[];
|
|
171
|
+
}
|
|
172
|
+
export declare function diagnoseRemoteOAuthConfig(logger?: Logger, env?: NodeJS.ProcessEnv): RemoteOAuthConfigDiagnostics;
|
|
165
173
|
export declare function loadRemoteOAuthConfig(logger?: Logger, env?: NodeJS.ProcessEnv): RemoteOAuthConfig;
|
package/dist/config.js
CHANGED
|
@@ -663,11 +663,7 @@ function disabledOAuthConfig(sourcePath = null, envOverrides = []) {
|
|
|
663
663
|
function isSafeRedirectUri(uri) {
|
|
664
664
|
return isHttpsOrLoopbackUrl(uri);
|
|
665
665
|
}
|
|
666
|
-
|
|
667
|
-
const configPath = defaultGatewayConfigPath();
|
|
668
|
-
const { parsed: configFile, sourcePath } = readGatewayTomlFile(configPath, logger, "OAuth");
|
|
669
|
-
const rawHttp = configFile?.http ?? {};
|
|
670
|
-
const rawOAuth = rawHttp.oauth ?? {};
|
|
666
|
+
function mergeOAuthEnvOverrides(rawOAuth, env) {
|
|
671
667
|
const envOverrides = [];
|
|
672
668
|
const merged = { ...rawOAuth };
|
|
673
669
|
if (env.LLM_GATEWAY_OAUTH_ENABLED !== undefined) {
|
|
@@ -695,43 +691,43 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
|
695
691
|
merged.require_consent = merged.require_consent ?? true;
|
|
696
692
|
envOverrides.push("LLM_GATEWAY_OAUTH_CONSENT_SECRET");
|
|
697
693
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
error: parsed.error.message,
|
|
702
|
-
});
|
|
703
|
-
return disabledOAuthConfig(sourcePath, envOverrides);
|
|
704
|
-
}
|
|
705
|
-
const data = parsed.data;
|
|
694
|
+
return { merged, envOverrides };
|
|
695
|
+
}
|
|
696
|
+
function validateParsedOAuthConfig(data, env, sourcePath, envOverrides, logger, issues) {
|
|
706
697
|
if (data.issuer !== "auto" && !isHttpsOrLoopbackUrl(data.issuer)) {
|
|
707
698
|
logWarn(logger, "Invalid [http.oauth].issuer; remote OAuth disabled");
|
|
708
|
-
|
|
699
|
+
issues.push("OAuth issuer must be an https:// URL (or a loopback URL for local testing).");
|
|
700
|
+
return null;
|
|
709
701
|
}
|
|
710
702
|
for (const client of data.clients) {
|
|
711
703
|
if (!data.allow_public_clients && !client.client_secret_hash) {
|
|
712
704
|
logWarn(logger, "OAuth client secret hash is required when public clients are disabled", {
|
|
713
705
|
client_id: client.client_id,
|
|
714
706
|
});
|
|
715
|
-
|
|
707
|
+
issues.push("An OAuth client is missing a client_secret_hash (required when public clients are disabled). Recreate it with `llm-cli-gateway oauth client add`.");
|
|
708
|
+
return null;
|
|
716
709
|
}
|
|
717
710
|
if (client.client_secret_hash && !isSecretHash(client.client_secret_hash)) {
|
|
718
711
|
logWarn(logger, "Invalid OAuth client secret hash; remote OAuth disabled", {
|
|
719
712
|
client_id: client.client_id,
|
|
720
713
|
});
|
|
721
|
-
|
|
714
|
+
issues.push("An OAuth client secret hash is not a valid scrypt hash. Rotate it with `llm-cli-gateway oauth client rotate`.");
|
|
715
|
+
return null;
|
|
722
716
|
}
|
|
723
717
|
if (client.allowed_redirect_uris.length === 0 ||
|
|
724
718
|
client.allowed_redirect_uris.some(uri => !isSafeRedirectUri(uri))) {
|
|
725
719
|
logWarn(logger, "Invalid OAuth client redirect URI; remote OAuth disabled", {
|
|
726
720
|
client_id: client.client_id,
|
|
727
721
|
});
|
|
728
|
-
|
|
722
|
+
issues.push("An OAuth client has a missing or non-https/loopback redirect URI. Re-add the client with a valid --redirect-uri.");
|
|
723
|
+
return null;
|
|
729
724
|
}
|
|
730
725
|
}
|
|
731
726
|
if (data.shared_secret?.enabled) {
|
|
732
727
|
if (!data.shared_secret.secret_hash || !isSecretHash(data.shared_secret.secret_hash)) {
|
|
733
728
|
logWarn(logger, "Invalid [http.oauth.shared_secret] secret_hash; remote OAuth disabled");
|
|
734
|
-
|
|
729
|
+
issues.push("The OAuth shared-secret hash is missing or invalid. Reset it with `llm-cli-gateway oauth shared-secret set`.");
|
|
730
|
+
return null;
|
|
735
731
|
}
|
|
736
732
|
}
|
|
737
733
|
if (data.registration_policy === "open_dev" && env.LLM_GATEWAY_OAUTH_OPEN_DEV !== "1") {
|
|
@@ -740,7 +736,8 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
|
740
736
|
if (data.require_consent) {
|
|
741
737
|
if (!data.consent_secret_hash || !isSecretHash(data.consent_secret_hash)) {
|
|
742
738
|
logWarn(logger, "[http.oauth].require_consent is set but consent_secret_hash is missing/invalid; remote OAuth disabled");
|
|
743
|
-
|
|
739
|
+
issues.push("require_consent is set but the consent secret hash is missing or invalid. Set it with `llm-cli-gateway oauth shared-secret set` or LLM_GATEWAY_OAUTH_CONSENT_SECRET.");
|
|
740
|
+
return null;
|
|
744
741
|
}
|
|
745
742
|
}
|
|
746
743
|
return {
|
|
@@ -769,3 +766,45 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
|
769
766
|
sources: { configFile: sourcePath, envOverrides },
|
|
770
767
|
};
|
|
771
768
|
}
|
|
769
|
+
export function diagnoseRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
770
|
+
const configPath = defaultGatewayConfigPath();
|
|
771
|
+
const { parsed: configFile, sourcePath } = readGatewayTomlFile(configPath, logger, "OAuth");
|
|
772
|
+
const rawHttp = configFile?.http ?? {};
|
|
773
|
+
const rawOAuth = rawHttp.oauth ?? {};
|
|
774
|
+
const { merged, envOverrides } = mergeOAuthEnvOverrides(rawOAuth, env);
|
|
775
|
+
const configured = Object.keys(rawOAuth).length > 0 || envOverrides.length > 0;
|
|
776
|
+
const parsed = OAuthConfigSchema.safeParse(merged);
|
|
777
|
+
if (!parsed.success) {
|
|
778
|
+
logWarn(logger, "Invalid [http.oauth] config; remote OAuth disabled", {
|
|
779
|
+
error: parsed.error.message,
|
|
780
|
+
});
|
|
781
|
+
return {
|
|
782
|
+
config: disabledOAuthConfig(sourcePath, envOverrides),
|
|
783
|
+
status: "malformed",
|
|
784
|
+
configured: true,
|
|
785
|
+
issues: [
|
|
786
|
+
"The [http.oauth] config is invalid (schema validation failed); remote OAuth is disabled.",
|
|
787
|
+
],
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const data = parsed.data;
|
|
791
|
+
const issues = [];
|
|
792
|
+
const built = validateParsedOAuthConfig(data, env, sourcePath, envOverrides, logger, issues);
|
|
793
|
+
if (!built) {
|
|
794
|
+
return {
|
|
795
|
+
config: disabledOAuthConfig(sourcePath, envOverrides),
|
|
796
|
+
status: data.enabled ? "malformed" : configured ? "disabled" : "absent",
|
|
797
|
+
configured,
|
|
798
|
+
issues: data.enabled ? issues : [],
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const status = built.enabled
|
|
802
|
+
? "enabled"
|
|
803
|
+
: configured
|
|
804
|
+
? "disabled"
|
|
805
|
+
: "absent";
|
|
806
|
+
return { config: built, status, configured, issues: [] };
|
|
807
|
+
}
|
|
808
|
+
export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
809
|
+
return diagnoseRemoteOAuthConfig(logger, env).config;
|
|
810
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type RemoteHttpOAuthReadiness } from "./doctor.js";
|
|
2
|
+
import type { RemoteOAuthConfig } from "./auth.js";
|
|
3
|
+
export declare const CONNECTOR_SETUP_SECRET_WARNING = "Never paste gateway bearer tokens, OAuth client secrets, OAuth access tokens, consent/shared secrets, tunnel tokens, or provider credentials into a remote chat transcript. Only the fields in this packet are safe to paste into a connector UI.";
|
|
4
|
+
export interface ConnectorSetupOptions {
|
|
5
|
+
clientId?: string;
|
|
6
|
+
includeLegacyNoAuth?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectorSetupPacket {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
schema: "remote-connector-setup.v1";
|
|
11
|
+
ready: boolean;
|
|
12
|
+
stage: RemoteHttpOAuthReadiness["stage"];
|
|
13
|
+
auth_mode: RemoteHttpOAuthReadiness["auth_mode"];
|
|
14
|
+
connector: {
|
|
15
|
+
mcp_url: string | null;
|
|
16
|
+
authorization_url: string | null;
|
|
17
|
+
token_url: string | null;
|
|
18
|
+
client_id: string | null;
|
|
19
|
+
client_secret_required: boolean;
|
|
20
|
+
client_secret_source: string | null;
|
|
21
|
+
};
|
|
22
|
+
workspace: RemoteHttpOAuthReadiness["workspace"];
|
|
23
|
+
next_actions: string[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
legacy_no_auth?: {
|
|
26
|
+
deprecated: true;
|
|
27
|
+
connector_url: string | null;
|
|
28
|
+
note: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare function buildConnectorSetupPacket(input: {
|
|
32
|
+
readiness: RemoteHttpOAuthReadiness;
|
|
33
|
+
oauth: RemoteOAuthConfig;
|
|
34
|
+
options?: ConnectorSetupOptions;
|
|
35
|
+
legacyNoAuthUrl?: string | null;
|
|
36
|
+
}): ConnectorSetupPacket;
|
|
37
|
+
export declare function legacyNoAuthConnectorUrl(env?: NodeJS.ProcessEnv): string | null;
|
|
38
|
+
export declare function gatherConnectorSetupPacket(options?: ConnectorSetupOptions, env?: NodeJS.ProcessEnv): ConnectorSetupPacket;
|
|
39
|
+
export declare function renderConnectorSetupSummary(packet: ConnectorSetupPacket): string;
|