codeloop-mcp-server 0.1.63 → 0.1.64
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/dist/auth/critical_floors.d.ts.map +1 -1
- package/dist/auth/critical_floors.js +4 -0
- package/dist/auth/critical_floors.js.map +1 -1
- package/dist/auth/key_resolver.d.ts +55 -0
- package/dist/auth/key_resolver.d.ts.map +1 -0
- package/dist/auth/key_resolver.js +41 -0
- package/dist/auth/key_resolver.js.map +1 -0
- package/dist/auth/key_source.d.ts +60 -4
- package/dist/auth/key_source.d.ts.map +1 -1
- package/dist/auth/key_source.js +281 -48
- package/dist/auth/key_source.js.map +1 -1
- package/dist/evidence/observability.d.ts +67 -0
- package/dist/evidence/observability.d.ts.map +1 -0
- package/dist/evidence/observability.js +57 -0
- package/dist/evidence/observability.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -4
- package/dist/index.js.map +1 -1
- package/dist/runners/app_logger.d.ts.map +1 -1
- package/dist/runners/app_logger.js +31 -3
- package/dist/runners/app_logger.js.map +1 -1
- package/dist/runners/logging_readiness.d.ts +72 -0
- package/dist/runners/logging_readiness.d.ts.map +1 -0
- package/dist/runners/logging_readiness.js +419 -0
- package/dist/runners/logging_readiness.js.map +1 -0
- package/dist/runners/remote_logs.d.ts +105 -0
- package/dist/runners/remote_logs.d.ts.map +1 -0
- package/dist/runners/remote_logs.js +336 -0
- package/dist/runners/remote_logs.js.map +1 -0
- package/dist/tools/gate_check.d.ts.map +1 -1
- package/dist/tools/gate_check.js +25 -0
- package/dist/tools/gate_check.js.map +1 -1
- package/dist/tools/interaction_replay.d.ts +18 -0
- package/dist/tools/interaction_replay.d.ts.map +1 -1
- package/dist/tools/interaction_replay.js +102 -1
- package/dist/tools/interaction_replay.js.map +1 -1
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +61 -5
- package/dist/tools/verify.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"critical_floors.d.ts","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"critical_floors.d.ts","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,aAAa,EAoF1C,CAAC"}
|
|
@@ -100,6 +100,10 @@ export const CRITICAL_FLOORS = [
|
|
|
100
100
|
min_version: "0.1.61",
|
|
101
101
|
reason: "0.1.60 could CRASH THE ENTIRE MCP SERVER on a warm npx cache — backend_runtime.js (new in 0.1.60) statically imported @codelooptech/shared/init/detect-backend, a submodule first shipped in shared@0.1.30, but both codeloop-mcp-server and codeloop only pinned `@codelooptech/shared: ^0.1.22`. So npx could pair mcp-server 0.1.60 with an OLDER cached shared (0.1.22–0.1.29) that lacks detect-backend.js, and the static import threw ERR_MODULE_NOT_FOUND at module-load time, taking down index -> verify -> backend_runtime before any tool could run (the same class of stale-cache skew the 0.1.53 floor first patched for rules-version.js, reintroduced when the dep floor wasn't raised alongside the new submodule). 0.1.61 fixes it durably two ways: (1) raises the @codelooptech/shared dependency floor to ^0.1.30 in BOTH codeloop-mcp-server and codeloop so a fresh resolve can never pick a shared missing the file, and (2) makes detect-backend a LAZY, GUARDED dynamic import — if the submodule can't be loaded for any reason, backend auto-detection degrades to n/a (backend gates skip) instead of crashing the server, so no future shared-submodule addition can ever hard-crash CodeLoop again. Users on a broken 0.1.60 install must clear the npx cache (delete %LocalAppData%/npm-cache/_npx on Windows or ~/.npm/_npx on macOS/Linux, or run `npx clear-npx-cache`) and relaunch so npx fetches 0.1.61.",
|
|
102
102
|
},
|
|
103
|
+
{
|
|
104
|
+
min_version: "0.1.64",
|
|
105
|
+
reason: "Observability blind spot — pre-0.1.64 CodeLoop could only SCAN whatever logs an app happened to emit (runtime_log_clean_evidence over backend.log + app log + browser console); it never checked whether the app HAD a logging system, and it never pulled production / hosting / third-party logs. So an app with no structured logger, no request logging, and no global error handler swallowed exceptions invisibly and the runtime-log gate passed as n/a (nothing to scan) — root causes stayed unfindable. 0.1.64 ships an observability-enforcement layer: (1) logging_observability_evidence — a per-stack (node/web/python/dotnet/flutter/go/rust) applicable-or-n/a BLOCKER gate that detects whether the project has a structured logger, request/HTTP logging (servers), and a global error handler / React error boundary, classifies the project (frontend/backend/fullstack/cli/library) to decide which dimensions are required, and BLOCKS ready_for_review with stack-specific 'build this' instructions when a required dimension is missing (severity + per-dimension requirements tunable via config.observability); (2) remote-log collection — auto-detects deploy targets + SaaS providers from project markers (Vercel/Netlify/Heroku/Fly/Railway/Render/Cloudflare/Firebase/Google Cloud/AWS/Supabase/Stripe/Convex + a config-extensible long tail) and, when the provider CLI is installed, pulls read-only logs into the run's logs/ dir so production / 3rd-party errors flow through the same runtime_log_clean_evidence blocker as local logs — when a detected provider's CLI is missing or unauthenticated, CodeLoop pushes the agent to install / log in; (3) per-interaction log correlation — codeloop_interaction_replay now ties each recorded codeloop_interact to the backend/app/remote log lines emitted in a window around it (errorsNear), so a click that triggers a server-side error is bound directly to the offending log line. ALSO in 0.1.64: revoked-key diagnostics no longer mislead users with an active key. Cursor/Claude inject CODELOOP_API_KEY from the mcp.json `env` block, which OVERRIDES the shell — so a stale hardcoded key in .cursor/mcp.json kept losing to the dashboard-rotated key in ~/.zshrc, yet the old diagnostic scanned ONLY rc files and (finding the live key there) pointed users at ~/.zshrc, making the revoked-key error look like a CodeLoop bug ('but my key is active!'). identifyKeySource now scans the mcp.json env blocks (project + global Cursor, .mcp.json, .vscode, Claude) FIRST, reports the exact hardcoded file+line, and detects SHADOWING (a different active key sitting in the shell/config that the hardcoded key is silently overriding) so the fix instructions name the real file to edit and tell the user to reload the MCP server.",
|
|
106
|
+
},
|
|
103
107
|
{
|
|
104
108
|
min_version: "0.1.63",
|
|
105
109
|
reason: "Out-of-app confirm/alert modal was NEVER detected (recurrence) — 0.1.54/0.1.55/0.1.57 only hardened FILE dialogs; a plain Yes/No confirmation MessageBox raised by clicking a button (e.g. Photometry-DB 'Delete TEMP PCB Test Range') stayed invisible to the detector and blocked the journey. detectWindowsModal only flagged a window when UIA reported IsModal=true AND the dialog TITLE contained the app name, OR the title matched a file-dialog pattern — but a WPF/WinForms MessageBox is a SEPARATE top-level #32770 window whose caption is 'Confirm'/'Delete'/the app name and whose UIA IsModal flag is frequently FALSE, so it slipped through every pass (is_modal_present:false). No F4 directive fired and the agent kept clicking the now-blocked button beneath it, then mis-read 'click missed' and looped. A second bug compounded it: win_ui_inspect / win_ui_automate located the app window with an EXACT NameProperty match, so app_name 'Photometry DB' never matched the live title 'PhotometryDB - Professional…' (no space) and every inspect returned 'App window not found', forcing coordinate guessing. 0.1.63 ships H12: (1) the Windows modal probe now collects each window's ProcessId and resolves the app's own pid(s) + main-window handle(s), and flags any app-OWNED secondary top-level window carrying a dialog signal (modal flag / #32770 or dialog class / confirm-or-alert title) as the modal regardless of its caption — the title- and IsModal-independent ownership signal that finally catches in-process MessageBox confirms; (2) classifyModalKind now treats an ambiguous #32770 as `confirm` (routes to codeloop_handle_modal for an agent decision) instead of `file_dialog` (auto-Escape), while a file-TITLED #32770 stays file_dialog; (3) codeloop_handle_modal with decision confirm/cancel now INVOKES the matching Yes/OK/Save/Delete or No/Cancel button on the dialog directly via UIA and re-detects to confirm it cleared, so a destructive Yes/No box no longer depends on which button is the Enter/Escape default; (4) buildUiaAppElementLookup + a whitespace-insensitive process-lookup fallback make win_ui_inspect / win_ui_automate / screenshot / window-bounds resolve the app window even when app_name differs from the live title by spaces or a suffix, or doesn't embed it at all (owning-process match).",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"critical_floors.js","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAoB;IAC9C;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,ufAAuf;KAC1f;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,4hBAA4hB;KACriB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,yvBAAyvB;KAClwB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,kxBAAkxB;KACrxB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0/BAA0/B;KAC7/B;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0iCAA0iC;KAC7iC;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,gqDAAgqD;KACnqD;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,uqDAAuqD;KAC1qD;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,w+EAAw+E;KAC3+E;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,88EAA88E;KACj9E;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,uiEAAuiE;KAC1iE;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,u/DAAu/D;KAC1/D;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,k3DAAk3D;KACr3D;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,oiDAAoiD;KACviD;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,g3CAAg3C;KACn3C;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,gwEAAgwE;KACnwE;CACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"critical_floors.js","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAoB;IAC9C;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,ufAAuf;KAC1f;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,4hBAA4hB;KACriB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,yvBAAyvB;KAClwB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,kxBAAkxB;KACrxB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0/BAA0/B;KAC7/B;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0iCAA0iC;KAC7iC;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,gqDAAgqD;KACnqD;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,uqDAAuqD;KAC1qD;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,w+EAAw+E;KAC3+E;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,88EAA88E;KACj9E;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,uiEAAuiE;KAC1iE;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,u/DAAu/D;KAC1/D;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,k3DAAk3D;KACr3D;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,oiDAAoiD;KACviD;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,g3CAAg3C;KACn3C;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,srFAAsrF;KACzrF;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,gwEAAgwE;KACnwE;CACF,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-healing API-key resolver.
|
|
3
|
+
*
|
|
4
|
+
* The failure mode we're closing: Cursor / Claude Code launch the MCP
|
|
5
|
+
* server with a `CODELOOP_API_KEY` injected from an mcp.json `env` block.
|
|
6
|
+
* That value OVERRIDES the user's shell. When the user rotates their key
|
|
7
|
+
* on the dashboard and updates `~/.zshrc` (or the dashboard rotates it for
|
|
8
|
+
* them) but forgets the hardcoded mcp.json copy, the server keeps
|
|
9
|
+
* presenting a now-REVOKED key and every tool call hard-blocks — even
|
|
10
|
+
* though the user has a perfectly active key sitting in another file.
|
|
11
|
+
*
|
|
12
|
+
* `resolveActiveApiKey` makes that impossible to get stuck on: if the
|
|
13
|
+
* configured key validates, we use it (zero extra work, no disk scan). If
|
|
14
|
+
* it's revoked/invalid, we scan the other on-disk keys (shell, PowerShell,
|
|
15
|
+
* every mcp.json, config.json) and use the FIRST one that validates,
|
|
16
|
+
* reporting which stale file the user should clean up. As long as ANY
|
|
17
|
+
* active key exists on the machine, CodeLoop keeps working.
|
|
18
|
+
*
|
|
19
|
+
* We only ever fall back when the primary key is genuinely invalid — never
|
|
20
|
+
* on `activation_required` (no key at all) and never on a transient
|
|
21
|
+
* backend/offline result (validateApiKey already returns `valid: true` in
|
|
22
|
+
* offline mode). Full key values are read only to attempt validation and
|
|
23
|
+
* are never logged.
|
|
24
|
+
*/
|
|
25
|
+
import type { ApiKeyValidation, ActivationResponse } from "@codelooptech/shared";
|
|
26
|
+
import { type OnDiskKey } from "./key_source.js";
|
|
27
|
+
export interface ResolvedApiKey {
|
|
28
|
+
/** The key that should actually be used for this run (may differ from the input). */
|
|
29
|
+
key: string | undefined;
|
|
30
|
+
/** Validation result for `key`. */
|
|
31
|
+
result: ApiKeyValidation | ActivationResponse;
|
|
32
|
+
/**
|
|
33
|
+
* Present only when we fell back from a revoked configured key to an
|
|
34
|
+
* active on-disk key. Drives the user-facing recovery notice.
|
|
35
|
+
*/
|
|
36
|
+
recovered?: {
|
|
37
|
+
/** Prefix of the revoked key that was configured (e.g. injected by mcp.json). */
|
|
38
|
+
stale_prefix: string;
|
|
39
|
+
/** Prefix of the active key we recovered to. */
|
|
40
|
+
active_prefix: string;
|
|
41
|
+
/** Where the active key was found. */
|
|
42
|
+
origin: OnDiskKey["origin"];
|
|
43
|
+
file: string;
|
|
44
|
+
line: number | null;
|
|
45
|
+
};
|
|
46
|
+
/** How many distinct on-disk keys we tried (diagnostics only). */
|
|
47
|
+
tried_count?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate `configuredKey`; if it's revoked/invalid, attempt to recover by
|
|
51
|
+
* trying every other distinct key found on disk. Returns the first key that
|
|
52
|
+
* validates (or the original failing result if none do).
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveActiveApiKey(configuredKey: string | undefined, projectDir: string): Promise<ResolvedApiKey>;
|
|
55
|
+
//# sourceMappingURL=key_resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key_resolver.d.ts","sourceRoot":"","sources":["../../src/auth/key_resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEjF,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,qFAAqF;IACrF,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,mCAAmC;IACnC,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,CAAC;IAC9C;;;OAGG;IACH,SAAS,CAAC,EAAE;QACV,iFAAiF;QACjF,YAAY,EAAE,MAAM,CAAC;QACrB,gDAAgD;QAChD,aAAa,EAAE,MAAM,CAAC;QACtB,sCAAsC;QACtC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC5B,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,CAAC;IACF,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,cAAc,CAAC,CAmCzB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { validateApiKey, isActivationRequired } from "./api_key.js";
|
|
2
|
+
import { collectOnDiskKeys } from "./key_source.js";
|
|
3
|
+
function isValid(r) {
|
|
4
|
+
return !isActivationRequired(r) && r.valid === true;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Validate `configuredKey`; if it's revoked/invalid, attempt to recover by
|
|
8
|
+
* trying every other distinct key found on disk. Returns the first key that
|
|
9
|
+
* validates (or the original failing result if none do).
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveActiveApiKey(configuredKey, projectDir) {
|
|
12
|
+
const primary = await validateApiKey(configuredKey);
|
|
13
|
+
// No key configured at all, or a valid key, or an offline/transient
|
|
14
|
+
// success — use as-is. (validateApiKey returns valid:true when the
|
|
15
|
+
// backend is unreachable, so we don't scan in that case.)
|
|
16
|
+
if (isActivationRequired(primary) || isValid(primary)) {
|
|
17
|
+
return { key: configuredKey, result: primary };
|
|
18
|
+
}
|
|
19
|
+
// Primary key is genuinely invalid/revoked. Try the other on-disk keys.
|
|
20
|
+
const candidates = collectOnDiskKeys(projectDir).filter((c) => c.value && c.value !== configuredKey);
|
|
21
|
+
for (const cand of candidates) {
|
|
22
|
+
const r = await validateApiKey(cand.value);
|
|
23
|
+
if (isValid(r)) {
|
|
24
|
+
return {
|
|
25
|
+
key: cand.value,
|
|
26
|
+
result: r,
|
|
27
|
+
recovered: {
|
|
28
|
+
stale_prefix: (configuredKey ?? "").slice(0, 12),
|
|
29
|
+
active_prefix: cand.key_prefix,
|
|
30
|
+
origin: cand.origin,
|
|
31
|
+
file: cand.file,
|
|
32
|
+
line: cand.line,
|
|
33
|
+
},
|
|
34
|
+
tried_count: candidates.length,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Nothing on disk validated — surface the original failure.
|
|
39
|
+
return { key: configuredKey, result: primary, tried_count: candidates.length };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=key_resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key_resolver.js","sourceRoot":"","sources":["../../src/auth/key_resolver.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAkB,MAAM,iBAAiB,CAAC;AAyBpE,SAAS,OAAO,CAAC,CAAwC;IACvD,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAK,CAAsB,CAAC,KAAK,KAAK,IAAI,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,aAAiC,EACjC,UAAkB;IAElB,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,CAAC;IAEpD,oEAAoE;IACpE,mEAAmE;IACnE,0DAA0D;IAC1D,IAAI,oBAAoB,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACtD,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IACjD,CAAC;IAED,wEAAwE;IACxE,MAAM,UAAU,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC,MAAM,CACrD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,aAAa,CAC5C,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACf,OAAO;gBACL,GAAG,EAAE,IAAI,CAAC,KAAK;gBACf,MAAM,EAAE,CAAC;gBACT,SAAS,EAAE;oBACT,YAAY,EAAE,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;oBAChD,aAAa,EAAE,IAAI,CAAC,UAAU;oBAC9B,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,IAAI,EAAE,IAAI,CAAC,IAAI;iBAChB;gBACD,WAAW,EAAE,UAAU,CAAC,MAAM;aAC/B,CAAC;QACJ,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;AACjF,CAAC"}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export interface ConfigHit {
|
|
2
|
+
/** Absolute path of the file that owns the value. */
|
|
3
|
+
file: string;
|
|
4
|
+
/** 1-based line number of the match, when locatable. */
|
|
5
|
+
line: number | null;
|
|
6
|
+
}
|
|
1
7
|
export type KeySource = {
|
|
2
8
|
kind: "env";
|
|
3
9
|
env_var: "CODELOOP_API_KEY";
|
|
@@ -7,22 +13,72 @@ export type KeySource = {
|
|
|
7
13
|
line: number | null;
|
|
8
14
|
/** First 12 chars of the key (safe to log). */
|
|
9
15
|
key_prefix: string;
|
|
16
|
+
/**
|
|
17
|
+
* mcp.json `env` blocks that hardcode THIS exact key. When present
|
|
18
|
+
* these are the EFFECTIVE source — the IDE injects them and they
|
|
19
|
+
* override the shell — so the diagnostic points here first.
|
|
20
|
+
*/
|
|
21
|
+
mcp_config_files?: ConfigHit[];
|
|
22
|
+
/**
|
|
23
|
+
* A DIFFERENT key found in the shell rc / config.json that the
|
|
24
|
+
* effective (hardcoded) key is silently overriding. Explains the
|
|
25
|
+
* "but my key is active!" case: the active key is there, just
|
|
26
|
+
* shadowed.
|
|
27
|
+
*/
|
|
28
|
+
shadow?: {
|
|
29
|
+
key_prefix: string;
|
|
30
|
+
file: string;
|
|
31
|
+
line: number | null;
|
|
32
|
+
};
|
|
10
33
|
} | {
|
|
11
34
|
kind: "config";
|
|
12
35
|
file: string;
|
|
13
36
|
key_prefix: string;
|
|
37
|
+
mcp_config_files?: ConfigHit[];
|
|
38
|
+
shadow?: {
|
|
39
|
+
key_prefix: string;
|
|
40
|
+
file: string;
|
|
41
|
+
line: number | null;
|
|
42
|
+
};
|
|
14
43
|
} | {
|
|
15
44
|
kind: "none";
|
|
16
45
|
};
|
|
17
46
|
/**
|
|
18
47
|
* Resolve the source of the API key currently being presented to the
|
|
19
|
-
* MCP server. Pass the same `apiKey` value the server uses
|
|
20
|
-
*
|
|
48
|
+
* MCP server. Pass the same `apiKey` value the server uses
|
|
49
|
+
* (`process.env.CODELOOP_API_KEY || config.api_key`) and the
|
|
50
|
+
* `projectDir` it discovered.
|
|
21
51
|
*
|
|
22
|
-
* Safe to call on every revoked-key error: scanning a handful of
|
|
23
|
-
*
|
|
52
|
+
* Safe to call on every revoked-key error: scanning a handful of files
|
|
53
|
+
* is microseconds and we cache nothing.
|
|
24
54
|
*/
|
|
25
55
|
export declare function identifyKeySource(apiKey: string | undefined, projectDir: string): KeySource;
|
|
56
|
+
/** An API key value discovered on disk, with where it came from. */
|
|
57
|
+
export interface OnDiskKey {
|
|
58
|
+
/** Full key value (used only to attempt validation; never logged). */
|
|
59
|
+
value: string;
|
|
60
|
+
/** First 12 chars — safe to surface. */
|
|
61
|
+
key_prefix: string;
|
|
62
|
+
/** Human-readable origin: "shell", "powershell", "mcp_config", or "config_json". */
|
|
63
|
+
origin: "shell" | "powershell" | "mcp_config" | "config_json";
|
|
64
|
+
/** Absolute path of the owning file. */
|
|
65
|
+
file: string;
|
|
66
|
+
/** 1-based line number, when locatable. */
|
|
67
|
+
line: number | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Gather EVERY CodeLoop API key written anywhere on disk that could plausibly
|
|
71
|
+
* be the user's real key — shell rc files, PowerShell profiles, every
|
|
72
|
+
* mcp.json `env` block (all clients × OS), and the project's
|
|
73
|
+
* `.codeloop/config.json`. De-duplicated by full value, preserving the
|
|
74
|
+
* priority order (mcp.json first — it's what the IDE injects — then shell,
|
|
75
|
+
* then config).
|
|
76
|
+
*
|
|
77
|
+
* Used by the self-heal path: when the configured key is REVOKED we try each
|
|
78
|
+
* of these in turn so that as long as the user has ANY active key on disk,
|
|
79
|
+
* CodeLoop keeps working instead of hard-blocking.
|
|
80
|
+
*/
|
|
81
|
+
export declare function collectOnDiskKeys(projectDir: string): OnDiskKey[];
|
|
26
82
|
/**
|
|
27
83
|
* Build the user-facing fix instructions for a revoked-key situation.
|
|
28
84
|
* Returned as a structured object so callers can embed it in the JSON
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key_source.d.ts","sourceRoot":"","sources":["../../src/auth/key_source.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"key_source.d.ts","sourceRoot":"","sources":["../../src/auth/key_source.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,SAAS;IACxB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,MAAM,SAAS,GACjB;IACE,IAAI,EAAE,KAAK,CAAC;IACZ,OAAO,EAAE,kBAAkB,CAAC;IAC5B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,SAAS,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACpE,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,SAAS,EAAE,CAAC;IAC/B,MAAM,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACpE,GACD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAqMrB;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,UAAU,EAAE,MAAM,GACjB,SAAS,CA8CX;AAED,oEAAoE;AACpE,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,MAAM,EAAE,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,aAAa,CAAC;IAC9D,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAwBjE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,GAAG;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAqFA"}
|
package/dist/auth/key_source.js
CHANGED
|
@@ -7,23 +7,32 @@
|
|
|
7
7
|
* We never log or return the key value itself, only its prefix and the
|
|
8
8
|
* file/line that owns it.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Why this is more than an rc-file scan
|
|
11
|
+
* -------------------------------------
|
|
12
|
+
* Cursor / Claude Code launch the MCP server with an `env` block from
|
|
13
|
+
* `.cursor/mcp.json` (project) and `~/.cursor/mcp.json` (global). That
|
|
14
|
+
* block is injected straight into the child process's environment and
|
|
15
|
+
* therefore OVERRIDES anything the user exported in `~/.zshrc`. So the
|
|
16
|
+
* value the server reads from `process.env.CODELOOP_API_KEY` is, in the
|
|
17
|
+
* common Cursor setup, the hardcoded mcp.json key — NOT the shell one.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* The original implementation only scanned shell rc files. When the
|
|
20
|
+
* effective key was a stale mcp.json hardcode, the scan found nothing
|
|
21
|
+
* matching there and fell back to "likely ~/.zshrc" — pointing the user
|
|
22
|
+
* at the very file where their ACTIVE (rotated) key lives, which made
|
|
23
|
+
* the revoked-key error look like a CodeLoop bug ("but my key is
|
|
24
|
+
* active!"). We now scan the mcp.json `env` blocks FIRST (they win at
|
|
25
|
+
* runtime) and also detect SHADOWING — a different key sitting in the
|
|
26
|
+
* shell / config that the hardcoded mcp.json key is silently overriding
|
|
27
|
+
* — so the message explains exactly what happened.
|
|
19
28
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
29
|
+
* Source ranking (matches the read order in `index.ts` —
|
|
30
|
+
* `process.env.CODELOOP_API_KEY || config.api_key`):
|
|
22
31
|
*
|
|
32
|
+
* 1. `process.env.CODELOOP_API_KEY` (wins) — scan mcp.json env blocks
|
|
33
|
+
* AND shell rc files for the line that owns the live value.
|
|
34
|
+
* 2. `config.api_key` from the project's `.codeloop/config.json`.
|
|
23
35
|
* 3. neither — `{ source: "none" }` (activation required).
|
|
24
|
-
*
|
|
25
|
-
* The returned `fix_steps` are deliberately copy-paste shell commands
|
|
26
|
-
* the user can run to rotate, so the agent can present them verbatim.
|
|
27
36
|
*/
|
|
28
37
|
import { existsSync, readFileSync } from "node:fs";
|
|
29
38
|
import { join } from "node:path";
|
|
@@ -37,28 +46,72 @@ const SHELL_RC_CANDIDATES = [
|
|
|
37
46
|
".bashrc",
|
|
38
47
|
".bash_profile",
|
|
39
48
|
".profile",
|
|
40
|
-
// Fish
|
|
41
|
-
//
|
|
42
|
-
// a user
|
|
49
|
+
// Fish: env vars set here don't usually leak into Cursor's MCP child,
|
|
50
|
+
// but they're cheap to scan and help the diagnostic point at the right
|
|
51
|
+
// file when a user does export from there.
|
|
52
|
+
join(".config", "fish", "config.fish"),
|
|
43
53
|
];
|
|
44
54
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* We deliberately match by prefix — not by full string — so:
|
|
52
|
-
* - We never store the full key in this process's memory longer
|
|
53
|
-
* than the comparison.
|
|
54
|
-
* - Stale `export` lines for an older key don't masquerade as the
|
|
55
|
-
* current source.
|
|
55
|
+
* PowerShell profile scripts — the Windows equivalent of the shell rc
|
|
56
|
+
* files. A user who runs `$env:CODELOOP_API_KEY = "…"` (or `setx`) here
|
|
57
|
+
* is the Windows analogue of an `export` line in `~/.zshrc`, so we scan
|
|
58
|
+
* them for parity. Covers Windows PowerShell 5.1, PowerShell 7 (Windows
|
|
59
|
+
* + macOS/Linux), and the OneDrive-redirected Documents folder Windows
|
|
60
|
+
* often uses.
|
|
56
61
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
function powerShellProfileCandidates() {
|
|
63
|
+
const home = userHome();
|
|
64
|
+
const out = [
|
|
65
|
+
join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"),
|
|
66
|
+
join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"),
|
|
67
|
+
join(home, "Documents", "WindowsPowerShell", "profile.ps1"),
|
|
68
|
+
join(home, "Documents", "PowerShell", "profile.ps1"),
|
|
69
|
+
// OneDrive-redirected Documents (very common on managed Windows).
|
|
70
|
+
join(home, "OneDrive", "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"),
|
|
71
|
+
join(home, "OneDrive", "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"),
|
|
72
|
+
// PowerShell 7 on macOS / Linux.
|
|
73
|
+
join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
|
|
74
|
+
];
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* mcp.json-style config files whose `env` block can inject
|
|
79
|
+
* CODELOOP_API_KEY into the MCP server process. This is the list that
|
|
80
|
+
* matters most: Cursor / Claude Code launch the server with this `env`
|
|
81
|
+
* block, which OVERRIDES the shell. Covers every client × OS combination
|
|
82
|
+
* we support so a stale hardcoded key is always locatable.
|
|
83
|
+
*/
|
|
84
|
+
function mcpConfigCandidates(projectDir) {
|
|
85
|
+
const home = userHome();
|
|
86
|
+
const appData = process.env.APPDATA; // Windows roaming app data
|
|
87
|
+
const out = [
|
|
88
|
+
// Cursor — project + global (same path on macOS/Linux/Windows via homedir).
|
|
89
|
+
join(projectDir, ".cursor", "mcp.json"),
|
|
90
|
+
join(home, ".cursor", "mcp.json"),
|
|
91
|
+
// Generic / VS Code project conventions.
|
|
92
|
+
join(projectDir, ".mcp.json"),
|
|
93
|
+
join(projectDir, ".vscode", "mcp.json"),
|
|
94
|
+
// VS Code user-level mcp.json (per OS).
|
|
95
|
+
join(home, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
96
|
+
join(home, ".config", "Code", "User", "mcp.json"),
|
|
97
|
+
// Claude Code (CLI) — global + nested project configs live in ~/.claude.json.
|
|
98
|
+
join(home, ".claude.json"),
|
|
99
|
+
// Claude Desktop — macOS, Linux, and Windows (%APPDATA%) locations.
|
|
100
|
+
join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
101
|
+
join(home, ".config", "claude", "claude_desktop_config.json"),
|
|
102
|
+
// Windsurf / Codeium.
|
|
103
|
+
join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
104
|
+
];
|
|
105
|
+
if (appData) {
|
|
106
|
+
out.push(join(appData, "Claude", "claude_desktop_config.json"));
|
|
107
|
+
out.push(join(appData, "Code", "User", "mcp.json"));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** Scan shell rc files for `export CODELOOP_API_KEY=…` lines. */
|
|
112
|
+
function scanRcFiles() {
|
|
61
113
|
const home = userHome();
|
|
114
|
+
const out = [];
|
|
62
115
|
for (const name of SHELL_RC_CANDIDATES) {
|
|
63
116
|
const path = join(home, name);
|
|
64
117
|
if (!existsSync(path))
|
|
@@ -73,45 +126,148 @@ function findExportLine(keyPrefix) {
|
|
|
73
126
|
const lines = body.split("\n");
|
|
74
127
|
for (let i = 0; i < lines.length; i += 1) {
|
|
75
128
|
const line = lines[i] ?? "";
|
|
76
|
-
// Cheap pre-filter — most rc files never mention us.
|
|
77
129
|
if (!line.includes("CODELOOP_API_KEY"))
|
|
78
130
|
continue;
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
// `setenv FOO bar` csh form for completeness.
|
|
82
|
-
const m = /(?:export\s+|setenv\s+)CODELOOP_API_KEY\s*[= ]\s*["']?([^"'\s]+)["']?/.exec(line);
|
|
131
|
+
// bash/zsh `export FOO=…` / csh `setenv FOO …` / fish `set -x FOO …`.
|
|
132
|
+
const m = /(?:export\s+|setenv\s+|set\s+(?:-x\s+|--export\s+)?)CODELOOP_API_KEY\s*[= ]\s*["']?([^"'\s]+)["']?/.exec(line);
|
|
83
133
|
if (!m)
|
|
84
134
|
continue;
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
out.push({ file: path, line: i + 1, value: m[1] ?? "" });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** Scan PowerShell profiles for `$env:CODELOOP_API_KEY = "…"` / `setx` lines. */
|
|
141
|
+
function scanPowerShellProfiles() {
|
|
142
|
+
const out = [];
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
for (const path of powerShellProfileCandidates()) {
|
|
145
|
+
if (seen.has(path))
|
|
146
|
+
continue;
|
|
147
|
+
seen.add(path);
|
|
148
|
+
if (!existsSync(path))
|
|
149
|
+
continue;
|
|
150
|
+
let body;
|
|
151
|
+
try {
|
|
152
|
+
body = readFileSync(path, "utf-8");
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const lines = body.split("\n");
|
|
158
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
159
|
+
const line = lines[i] ?? "";
|
|
160
|
+
if (!line.includes("CODELOOP_API_KEY"))
|
|
161
|
+
continue;
|
|
162
|
+
// `$env:CODELOOP_API_KEY = "…"` or `setx CODELOOP_API_KEY "…"`.
|
|
163
|
+
const m = /(?:\$env:CODELOOP_API_KEY\s*=\s*|setx\s+CODELOOP_API_KEY\s+)["']?([^"'\s]+)["']?/.exec(line);
|
|
164
|
+
if (!m)
|
|
87
165
|
continue;
|
|
88
|
-
|
|
166
|
+
out.push({ file: path, line: i + 1, value: m[1] ?? "" });
|
|
89
167
|
}
|
|
90
168
|
}
|
|
91
|
-
return
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/** Combined shell + PowerShell scan — the cross-platform rc-file equivalent. */
|
|
172
|
+
function scanShellKeyFiles() {
|
|
173
|
+
return [...scanRcFiles(), ...scanPowerShellProfiles()];
|
|
174
|
+
}
|
|
175
|
+
/** Read the `api_key` value out of a project's `.codeloop/config.json`. */
|
|
176
|
+
function scanProjectConfig(projectDir) {
|
|
177
|
+
const path = join(projectDir, ".codeloop", "config.json");
|
|
178
|
+
if (!existsSync(path))
|
|
179
|
+
return [];
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = readFileSync(path, "utf-8");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const lines = body.split("\n");
|
|
188
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
189
|
+
const line = lines[i] ?? "";
|
|
190
|
+
if (!line.includes("api_key"))
|
|
191
|
+
continue;
|
|
192
|
+
const m = /"api_key"\s*:\s*"([^"]+)"/.exec(line);
|
|
193
|
+
if (!m)
|
|
194
|
+
continue;
|
|
195
|
+
return [{ file: path, line: i + 1, value: m[1] ?? "" }];
|
|
196
|
+
}
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
/** Scan mcp.json-style config files for a hardcoded `"CODELOOP_API_KEY": "…"`. */
|
|
200
|
+
function scanMcpConfigs(projectDir) {
|
|
201
|
+
const out = [];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const path of mcpConfigCandidates(projectDir)) {
|
|
204
|
+
if (seen.has(path))
|
|
205
|
+
continue;
|
|
206
|
+
seen.add(path);
|
|
207
|
+
if (!existsSync(path))
|
|
208
|
+
continue;
|
|
209
|
+
let body;
|
|
210
|
+
try {
|
|
211
|
+
body = readFileSync(path, "utf-8");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const lines = body.split("\n");
|
|
217
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
218
|
+
const line = lines[i] ?? "";
|
|
219
|
+
if (!line.includes("CODELOOP_API_KEY"))
|
|
220
|
+
continue;
|
|
221
|
+
// JSON form: "CODELOOP_API_KEY": "cl_live_…"
|
|
222
|
+
const m = /"CODELOOP_API_KEY"\s*:\s*"([^"]+)"/.exec(line);
|
|
223
|
+
if (!m)
|
|
224
|
+
continue;
|
|
225
|
+
out.push({ file: path, line: i + 1, value: m[1] ?? "" });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
function prefixOf(value) {
|
|
231
|
+
return value.slice(0, 12);
|
|
92
232
|
}
|
|
93
233
|
/**
|
|
94
234
|
* Resolve the source of the API key currently being presented to the
|
|
95
|
-
* MCP server. Pass the same `apiKey` value the server uses
|
|
96
|
-
*
|
|
235
|
+
* MCP server. Pass the same `apiKey` value the server uses
|
|
236
|
+
* (`process.env.CODELOOP_API_KEY || config.api_key`) and the
|
|
237
|
+
* `projectDir` it discovered.
|
|
97
238
|
*
|
|
98
|
-
* Safe to call on every revoked-key error: scanning a handful of
|
|
99
|
-
*
|
|
239
|
+
* Safe to call on every revoked-key error: scanning a handful of files
|
|
240
|
+
* is microseconds and we cache nothing.
|
|
100
241
|
*/
|
|
101
242
|
export function identifyKeySource(apiKey, projectDir) {
|
|
102
243
|
if (!apiKey || apiKey.trim() === "") {
|
|
103
244
|
return { kind: "none" };
|
|
104
245
|
}
|
|
105
|
-
const prefix = apiKey
|
|
246
|
+
const prefix = prefixOf(apiKey);
|
|
247
|
+
const rcLines = scanShellKeyFiles();
|
|
248
|
+
const mcpLines = scanMcpConfigs(projectDir);
|
|
249
|
+
// mcp.json env blocks that hardcode THIS exact key — the effective
|
|
250
|
+
// override at runtime under Cursor / Claude Code.
|
|
251
|
+
const mcpHits = mcpLines
|
|
252
|
+
.filter((l) => l.value.slice(0, prefix.length) === prefix)
|
|
253
|
+
.map((l) => ({ file: l.file, line: l.line }));
|
|
254
|
+
// A DIFFERENT key sitting anywhere on disk — the one the user likely
|
|
255
|
+
// thinks is active but which the effective key is shadowing.
|
|
256
|
+
const shadowLine = [...mcpLines, ...rcLines].find((l) => l.value && l.value.slice(0, prefix.length) !== prefix);
|
|
257
|
+
const shadow = shadowLine
|
|
258
|
+
? { key_prefix: prefixOf(shadowLine.value), file: shadowLine.file, line: shadowLine.line }
|
|
259
|
+
: undefined;
|
|
106
260
|
// Branch 1: env var wins per the read order in index.ts.
|
|
107
261
|
if (process.env.CODELOOP_API_KEY && process.env.CODELOOP_API_KEY === apiKey) {
|
|
108
|
-
const
|
|
262
|
+
const rcHit = rcLines.find((l) => l.value.slice(0, prefix.length) === prefix);
|
|
109
263
|
return {
|
|
110
264
|
kind: "env",
|
|
111
265
|
env_var: "CODELOOP_API_KEY",
|
|
112
|
-
file:
|
|
113
|
-
line:
|
|
266
|
+
file: rcHit?.file ?? null,
|
|
267
|
+
line: rcHit?.line ?? null,
|
|
114
268
|
key_prefix: prefix,
|
|
269
|
+
mcp_config_files: mcpHits.length > 0 ? mcpHits : undefined,
|
|
270
|
+
shadow,
|
|
115
271
|
};
|
|
116
272
|
}
|
|
117
273
|
// Branch 2: came from the project config.
|
|
@@ -119,14 +275,91 @@ export function identifyKeySource(apiKey, projectDir) {
|
|
|
119
275
|
kind: "config",
|
|
120
276
|
file: join(projectDir, ".codeloop", "config.json"),
|
|
121
277
|
key_prefix: prefix,
|
|
278
|
+
mcp_config_files: mcpHits.length > 0 ? mcpHits : undefined,
|
|
279
|
+
shadow,
|
|
122
280
|
};
|
|
123
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Gather EVERY CodeLoop API key written anywhere on disk that could plausibly
|
|
284
|
+
* be the user's real key — shell rc files, PowerShell profiles, every
|
|
285
|
+
* mcp.json `env` block (all clients × OS), and the project's
|
|
286
|
+
* `.codeloop/config.json`. De-duplicated by full value, preserving the
|
|
287
|
+
* priority order (mcp.json first — it's what the IDE injects — then shell,
|
|
288
|
+
* then config).
|
|
289
|
+
*
|
|
290
|
+
* Used by the self-heal path: when the configured key is REVOKED we try each
|
|
291
|
+
* of these in turn so that as long as the user has ANY active key on disk,
|
|
292
|
+
* CodeLoop keeps working instead of hard-blocking.
|
|
293
|
+
*/
|
|
294
|
+
export function collectOnDiskKeys(projectDir) {
|
|
295
|
+
const groups = [
|
|
296
|
+
{ origin: "mcp_config", lines: scanMcpConfigs(projectDir) },
|
|
297
|
+
{ origin: "shell", lines: scanRcFiles() },
|
|
298
|
+
{ origin: "powershell", lines: scanPowerShellProfiles() },
|
|
299
|
+
{ origin: "config_json", lines: scanProjectConfig(projectDir) },
|
|
300
|
+
];
|
|
301
|
+
const out = [];
|
|
302
|
+
const seenValues = new Set();
|
|
303
|
+
for (const g of groups) {
|
|
304
|
+
for (const l of g.lines) {
|
|
305
|
+
const value = (l.value ?? "").trim();
|
|
306
|
+
if (!value || seenValues.has(value))
|
|
307
|
+
continue;
|
|
308
|
+
seenValues.add(value);
|
|
309
|
+
out.push({
|
|
310
|
+
value,
|
|
311
|
+
key_prefix: prefixOf(value),
|
|
312
|
+
origin: g.origin,
|
|
313
|
+
file: l.file,
|
|
314
|
+
line: l.line,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
124
320
|
/**
|
|
125
321
|
* Build the user-facing fix instructions for a revoked-key situation.
|
|
126
322
|
* Returned as a structured object so callers can embed it in the JSON
|
|
127
323
|
* error envelope; rendering is up to the agent.
|
|
128
324
|
*/
|
|
129
325
|
export function buildRevokedKeyDiagnostic(source) {
|
|
326
|
+
// The single highest-value case: the revoked key is hardcoded in one
|
|
327
|
+
// or more mcp.json env blocks. Those override the shell, so this is
|
|
328
|
+
// the file the user must edit — even though their shell may hold a
|
|
329
|
+
// perfectly active key.
|
|
330
|
+
const mcpFiles = source.mcp_config_files;
|
|
331
|
+
const shadow = source.shadow;
|
|
332
|
+
if ((source.kind === "env" || source.kind === "config") && mcpFiles && mcpFiles.length > 0) {
|
|
333
|
+
const fileList = mcpFiles
|
|
334
|
+
.map((h) => (h.line ? `${h.file} (line ${h.line})` : h.file))
|
|
335
|
+
.join(" and ");
|
|
336
|
+
const shadowNote = shadow
|
|
337
|
+
? ` Your shell/config has a DIFFERENT key (${shadow.key_prefix}…) in ${shadow.file}, ` +
|
|
338
|
+
`but it is being IGNORED because the hardcoded mcp.json key takes precedence — ` +
|
|
339
|
+
`this is why your key looks active but isn't the one CodeLoop is using.`
|
|
340
|
+
: "";
|
|
341
|
+
const prefix = source.key_prefix ?? "your key";
|
|
342
|
+
return {
|
|
343
|
+
message: `Your API key (${prefix}…) is revoked. It is HARDCODED in the \`env\` block of ${fileList}, ` +
|
|
344
|
+
`which your IDE injects into the CodeLoop MCP server and which OVERRIDES any key exported in your shell ` +
|
|
345
|
+
`(~/.zshrc) or stored in .codeloop/config.json.${shadowNote} ` +
|
|
346
|
+
`Fix: replace the CODELOOP_API_KEY value in ${fileList} with an active key from ` +
|
|
347
|
+
`https://codeloop.tech/dashboard/keys (or DELETE that line to fall back to your shell/config key), ` +
|
|
348
|
+
`then fully reload the CodeLoop MCP server — toggle it off/on in your IDE's MCP settings, or restart the IDE.`,
|
|
349
|
+
source,
|
|
350
|
+
fix_steps: [
|
|
351
|
+
`# 1. Edit the mcp.json file(s) that hardcode the revoked key:`,
|
|
352
|
+
...mcpFiles.map((h) => `open -e "${h.file}"`),
|
|
353
|
+
`# 2. In each "mcpServers" -> "codeloop" -> "env" block, set CODELOOP_API_KEY to an`,
|
|
354
|
+
`# active key from https://codeloop.tech/dashboard/keys — OR delete the`,
|
|
355
|
+
`# CODELOOP_API_KEY line entirely so the server uses your shell / .codeloop/config.json key.`,
|
|
356
|
+
...(shadow
|
|
357
|
+
? [`# (An active-looking key ${shadow.key_prefix}… already lives in ${shadow.file}.)`]
|
|
358
|
+
: []),
|
|
359
|
+
`# 3. Reload the MCP server: toggle CodeLoop off/on in your IDE's MCP settings, or restart the IDE.`,
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
130
363
|
if (source.kind === "env") {
|
|
131
364
|
const location = source.file && source.line
|
|
132
365
|
? `${source.file} line ${source.line}`
|