@claudexor/harness-cursor 1.0.0 → 2.1.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/dist/auth.d.ts +55 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +137 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts +8 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +193 -183
- package/dist/index.js.map +1 -1
- package/dist/parse.d.ts +3 -2
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +126 -14
- package/dist/parse.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -3,10 +3,13 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { ConformanceReport as ConformanceReportSchema, HarnessCapabilityProfile as HarnessCapabilityProfileSchema, HarnessManifest as HarnessManifestSchema, } from "@claudexor/schema";
|
|
6
|
-
import { HarnessUnavailableError, needsScopedHomeKeychainBridge, providerScrubEnv, runCapture, runCliHarness as runCliHarnessDefault, } from "@claudexor/core";
|
|
6
|
+
import { abortSignalFromSpec, HarnessUnavailableError, needsScopedHomeKeychainBridge, promptWithInstructions, providerScrubEnv, runCapture, runCliHarness as runCliHarnessDefault, } from "@claudexor/core";
|
|
7
7
|
import { resolveSecret } from "@claudexor/secrets";
|
|
8
8
|
import { CLAUDEXOR_VERSION, nowIso, redactSecrets } from "@claudexor/util";
|
|
9
|
-
import { createCursorParser } from "./parse.js";
|
|
9
|
+
import { createCursorParser, parseCursorModelList } from "./parse.js";
|
|
10
|
+
export { parseCursorModelList } from "./parse.js";
|
|
11
|
+
import { cursorProfileKeyOrRefusal, probeCursorCredentialProfile, probeCursorNativeAuth, selectCursorAuthRoute, shouldDiscloseCursorAutoApiRoute, shouldSmokeCursorApiKey, } from "./auth.js";
|
|
12
|
+
export { cursorStatusAuthenticated, cursorStatusLoggedOut, selectCursorAuthRoute, shouldDiscloseCursorAutoApiRoute, } from "./auth.js";
|
|
10
13
|
const BIN = process.env.CLAUDEXOR_CURSOR_BIN || "cursor-agent";
|
|
11
14
|
// Long enough for one sequential reviewer panel pass; still bounded so revoked
|
|
12
15
|
// keys do not remain smoke-proven for a whole daemon lifetime.
|
|
@@ -25,7 +28,7 @@ const CURSOR_CAPABILITY_PROFILE = HarnessCapabilityProfileSchema.parse({
|
|
|
25
28
|
isolation: {
|
|
26
29
|
supported_containment: ["scoped_home_keychain_bridge", "env_or_file_injection"],
|
|
27
30
|
},
|
|
28
|
-
|
|
31
|
+
attachment_inputs: [],
|
|
29
32
|
});
|
|
30
33
|
function accessArgs(access) {
|
|
31
34
|
switch (access) {
|
|
@@ -43,34 +46,20 @@ function accessArgs(access) {
|
|
|
43
46
|
return ["--trust"];
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
|
-
async function detectVersion() {
|
|
49
|
+
async function detectVersion(abortSignal) {
|
|
47
50
|
try {
|
|
48
|
-
const r = await runCapture(BIN, ["--version"], {
|
|
51
|
+
const r = await runCapture(BIN, ["--version"], {
|
|
52
|
+
timeoutMs: 10_000,
|
|
53
|
+
abortSignal,
|
|
54
|
+
cancelSignal: "SIGTERM",
|
|
55
|
+
cancelKillDelayMs: 0,
|
|
56
|
+
});
|
|
49
57
|
return r.stdout.trim() || `${BIN} (version unknown)`;
|
|
50
58
|
}
|
|
51
59
|
catch {
|
|
52
60
|
return null;
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
|
-
async function nativeAuthOk(env) {
|
|
56
|
-
try {
|
|
57
|
-
const r = await runCapture(BIN, ["status"], { env, timeoutMs: 10_000 });
|
|
58
|
-
return cursorStatusAuthenticated(r.code, `${r.stdout}\n${r.stderr}`);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export function cursorStatusAuthenticated(code, text) {
|
|
65
|
-
if (code !== 0)
|
|
66
|
-
return false;
|
|
67
|
-
const normalized = text.toLowerCase();
|
|
68
|
-
if (normalized.includes("not logged in") || normalized.includes("authentication required"))
|
|
69
|
-
return false;
|
|
70
|
-
return (normalized.includes("logged in") ||
|
|
71
|
-
normalized.includes("authenticated") ||
|
|
72
|
-
normalized.includes("account"));
|
|
73
|
-
}
|
|
74
63
|
function cursorApiKey(env) {
|
|
75
64
|
if (env && Object.prototype.hasOwnProperty.call(env, "CLAUDEXOR_CURSOR_API_KEY"))
|
|
76
65
|
return env["CLAUDEXOR_CURSOR_API_KEY"] || null;
|
|
@@ -81,78 +70,6 @@ function cursorApiKey(env) {
|
|
|
81
70
|
process.env.CURSOR_API_KEY ||
|
|
82
71
|
null);
|
|
83
72
|
}
|
|
84
|
-
export function selectCursorAuthRoute(input) {
|
|
85
|
-
const keyRouteReady = input.hasKey && input.apiKeyReady;
|
|
86
|
-
if (input.authPreference === "api_key") {
|
|
87
|
-
if (keyRouteReady)
|
|
88
|
-
return "api_key";
|
|
89
|
-
if (input.nativeAuthed)
|
|
90
|
-
return "local_session";
|
|
91
|
-
return "unavailable";
|
|
92
|
-
}
|
|
93
|
-
if (input.authPreference === "subscription") {
|
|
94
|
-
if (input.nativeAuthed)
|
|
95
|
-
return "local_session";
|
|
96
|
-
return "unavailable";
|
|
97
|
-
}
|
|
98
|
-
if (input.scopedHome && keyRouteReady)
|
|
99
|
-
return "api_key";
|
|
100
|
-
if (input.nativeAuthed)
|
|
101
|
-
return "local_session";
|
|
102
|
-
if (keyRouteReady)
|
|
103
|
-
return "api_key";
|
|
104
|
-
return "unavailable";
|
|
105
|
-
}
|
|
106
|
-
export function shouldDiscloseCursorAutoApiRoute(input) {
|
|
107
|
-
return input.authPreference === "auto" && input.route === "api_key" && input.nativeAuthed;
|
|
108
|
-
}
|
|
109
|
-
function shouldSmokeCursorApiKey(input) {
|
|
110
|
-
if (!input.hasKey)
|
|
111
|
-
return false;
|
|
112
|
-
if (input.authPreference === "subscription")
|
|
113
|
-
return false;
|
|
114
|
-
if (input.authPreference === "api_key")
|
|
115
|
-
return true;
|
|
116
|
-
return !input.nativeAuthed || input.scopedHome;
|
|
117
|
-
}
|
|
118
|
-
function isCursorModelId(id) {
|
|
119
|
-
if (!id)
|
|
120
|
-
return false;
|
|
121
|
-
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(id))
|
|
122
|
-
return false;
|
|
123
|
-
for (const ch of id) {
|
|
124
|
-
if ((ch >= "a" && ch <= "z") ||
|
|
125
|
-
(ch >= "A" && ch <= "Z") ||
|
|
126
|
-
(ch >= "0" && ch <= "9") ||
|
|
127
|
-
ch === "-" ||
|
|
128
|
-
ch === "." ||
|
|
129
|
-
ch === "_" ||
|
|
130
|
-
ch === "/" ||
|
|
131
|
-
ch === ":")
|
|
132
|
-
continue;
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
export function parseCursorModelList(text) {
|
|
138
|
-
const out = [];
|
|
139
|
-
const seen = new Set();
|
|
140
|
-
for (const rawLine of text.replaceAll("\r", "").split("\n")) {
|
|
141
|
-
const line = rawLine.trim();
|
|
142
|
-
if (!line || line === "Available models" || line.startsWith("Tip:"))
|
|
143
|
-
continue;
|
|
144
|
-
const sep = line.indexOf(" - ");
|
|
145
|
-
if (sep <= 0)
|
|
146
|
-
continue;
|
|
147
|
-
const id = line.slice(0, sep).trim();
|
|
148
|
-
const label = line.slice(sep + 3).trim() || null;
|
|
149
|
-
if (!isCursorModelId(id) || seen.has(id))
|
|
150
|
-
continue;
|
|
151
|
-
seen.add(id);
|
|
152
|
-
out.push({ id, label, context_window: null });
|
|
153
|
-
}
|
|
154
|
-
return out;
|
|
155
|
-
}
|
|
156
73
|
export function cursorApiSmokeFinalText(stdout) {
|
|
157
74
|
const replies = [];
|
|
158
75
|
for (const rawLine of stdout.replaceAll("\r", "").split("\n")) {
|
|
@@ -291,17 +208,17 @@ export async function smokeIsolatedApiKey(key = cursorApiKey(), options = {}) {
|
|
|
291
208
|
function cursorApiSmokeCacheKey(key) {
|
|
292
209
|
return createHash("sha256").update(key).digest("hex");
|
|
293
210
|
}
|
|
294
|
-
async function smokeCursorApiKey(deps, key) {
|
|
211
|
+
async function smokeCursorApiKey(deps, key, fresh = false) {
|
|
295
212
|
const cacheKey = cursorApiSmokeCacheKey(key);
|
|
296
213
|
const now = deps.nowMs();
|
|
297
|
-
const cached = deps.apiSmokeCache.get(cacheKey);
|
|
214
|
+
const cached = fresh ? undefined : deps.apiSmokeCache.get(cacheKey);
|
|
298
215
|
if (cached && cached.expiresAtMs > now)
|
|
299
216
|
return cached.result;
|
|
300
217
|
if (cached)
|
|
301
218
|
deps.apiSmokeCache.delete(cacheKey);
|
|
302
219
|
const result = await deps.smokeIsolatedApiKey(key);
|
|
303
220
|
const ttlMs = result.ok ? deps.apiSmokeCacheTtlMs : deps.apiSmokeFailureCacheTtlMs;
|
|
304
|
-
if (ttlMs > 0)
|
|
221
|
+
if (!fresh && ttlMs > 0)
|
|
305
222
|
deps.apiSmokeCache.set(cacheKey, { result, expiresAtMs: now + ttlMs });
|
|
306
223
|
return result;
|
|
307
224
|
}
|
|
@@ -338,17 +255,20 @@ async function resolveCursorAuthRoute(deps, input) {
|
|
|
338
255
|
const scopedHome = Boolean(input.env?.["HOME"]);
|
|
339
256
|
if (scopedHome)
|
|
340
257
|
maybeBridgeScopedHome(env);
|
|
341
|
-
const key = deps.cursorApiKey(input.env);
|
|
342
258
|
const authPreference = input.authPreference ?? "auto";
|
|
343
|
-
const
|
|
259
|
+
const key = authPreference === "subscription" ? null : deps.cursorApiKey(input.env);
|
|
260
|
+
const nativeProbe = authPreference === "api_key"
|
|
261
|
+
? { authed: false, probeError: null }
|
|
262
|
+
: await deps.nativeAuthOk(env, input.abortSignal);
|
|
263
|
+
const nativeAuthed = nativeProbe.authed;
|
|
344
264
|
const shouldSmokeApiKey = shouldSmokeCursorApiKey({
|
|
345
265
|
hasKey: Boolean(key),
|
|
346
266
|
authPreference,
|
|
347
267
|
nativeAuthed,
|
|
348
|
-
|
|
268
|
+
nativeProbeError: nativeProbe.probeError,
|
|
349
269
|
});
|
|
350
270
|
const apiSmoke = shouldSmokeApiKey && key
|
|
351
|
-
? await smokeCursorApiKey(deps, key)
|
|
271
|
+
? await smokeCursorApiKey(deps, key, input.fresh === true)
|
|
352
272
|
: {
|
|
353
273
|
ok: false,
|
|
354
274
|
detail: key
|
|
@@ -362,7 +282,15 @@ async function resolveCursorAuthRoute(deps, input) {
|
|
|
362
282
|
nativeAuthed,
|
|
363
283
|
scopedHome,
|
|
364
284
|
});
|
|
365
|
-
return {
|
|
285
|
+
return {
|
|
286
|
+
route,
|
|
287
|
+
env,
|
|
288
|
+
key,
|
|
289
|
+
nativeAuthed,
|
|
290
|
+
nativeProbeError: nativeProbe.probeError,
|
|
291
|
+
apiSmoke,
|
|
292
|
+
scopedHome,
|
|
293
|
+
};
|
|
366
294
|
}
|
|
367
295
|
async function listCursorModelsFromReadyRoute(deps, spec) {
|
|
368
296
|
const catalogOnly = () => {
|
|
@@ -372,11 +300,13 @@ async function listCursorModelsFromReadyRoute(deps, spec) {
|
|
|
372
300
|
CURSOR_API_KEY: key ?? null,
|
|
373
301
|
});
|
|
374
302
|
};
|
|
375
|
-
if (spec?.env || spec?.authPreference) {
|
|
303
|
+
if (spec?.env || spec?.authPreference || spec?.fresh) {
|
|
376
304
|
const authPreference = spec.authPreference ?? "auto";
|
|
377
305
|
const resolved = await resolveCursorAuthRoute(deps, {
|
|
378
306
|
env: spec.env,
|
|
379
307
|
authPreference,
|
|
308
|
+
fresh: spec?.fresh,
|
|
309
|
+
abortSignal: spec?.abortSignal,
|
|
380
310
|
});
|
|
381
311
|
if (resolved.route === "local_session") {
|
|
382
312
|
const models = await deps.listCursorModels({ ...resolved.env, CURSOR_API_KEY: null });
|
|
@@ -393,15 +323,18 @@ async function listCursorModelsFromReadyRoute(deps, spec) {
|
|
|
393
323
|
return [];
|
|
394
324
|
}
|
|
395
325
|
const nativeEnv = cursorNativeEnv();
|
|
396
|
-
|
|
326
|
+
const nativeProbe = await deps.nativeAuthOk(nativeEnv, spec?.abortSignal);
|
|
327
|
+
if (nativeProbe.authed) {
|
|
397
328
|
const nativeModels = await deps.listCursorModels(nativeEnv);
|
|
398
329
|
if (nativeModels.length > 0)
|
|
399
330
|
return nativeModels;
|
|
400
331
|
}
|
|
332
|
+
if (nativeProbe.probeError)
|
|
333
|
+
return [];
|
|
401
334
|
const key = deps.cursorApiKey();
|
|
402
335
|
if (!key)
|
|
403
336
|
return catalogOnly();
|
|
404
|
-
const apiSmoke = await smokeCursorApiKey(deps, key);
|
|
337
|
+
const apiSmoke = await smokeCursorApiKey(deps, key, spec?.fresh === true);
|
|
405
338
|
if (apiSmoke.ok) {
|
|
406
339
|
const models = await deps.listCursorModels({ ...providerScrubEnv(), CURSOR_API_KEY: key });
|
|
407
340
|
if (models.length > 0)
|
|
@@ -412,7 +345,7 @@ async function listCursorModelsFromReadyRoute(deps, spec) {
|
|
|
412
345
|
export function createCursorAdapter(deps = {}) {
|
|
413
346
|
const runtime = {
|
|
414
347
|
detectVersion,
|
|
415
|
-
nativeAuthOk,
|
|
348
|
+
nativeAuthOk: probeCursorNativeAuth,
|
|
416
349
|
cursorApiKey,
|
|
417
350
|
listCursorModels,
|
|
418
351
|
smokeIsolatedApiKey,
|
|
@@ -421,6 +354,7 @@ export function createCursorAdapter(deps = {}) {
|
|
|
421
354
|
apiSmokeFailureCacheTtlMs: CURSOR_API_SMOKE_FAILURE_CACHE_TTL_MS,
|
|
422
355
|
nowMs: () => Date.now(),
|
|
423
356
|
runCliHarness: runCliHarnessDefault,
|
|
357
|
+
resolveProfileSecret: (ref) => resolveSecret(ref),
|
|
424
358
|
...deps,
|
|
425
359
|
};
|
|
426
360
|
return {
|
|
@@ -430,7 +364,8 @@ export function createCursorAdapter(deps = {}) {
|
|
|
430
364
|
if (version === null) {
|
|
431
365
|
throw new HarnessUnavailableError("cursor-agent not found on PATH (set CLAUDEXOR_CURSOR_BIN)");
|
|
432
366
|
}
|
|
433
|
-
const
|
|
367
|
+
const nativeProbe = await runtime.nativeAuthOk(cursorNativeEnv());
|
|
368
|
+
const nativeAuthed = nativeProbe.authed;
|
|
434
369
|
const apiKey = runtime.cursorApiKey() !== null;
|
|
435
370
|
return HarnessManifestSchema.parse({
|
|
436
371
|
id: "cursor",
|
|
@@ -458,9 +393,8 @@ export function createCursorAdapter(deps = {}) {
|
|
|
458
393
|
...CURSOR_CAPABILITY_PROFILE,
|
|
459
394
|
auth: {
|
|
460
395
|
...CURSOR_CAPABILITY_PROFILE.auth,
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// runtime because that decision depends on the run environment.
|
|
396
|
+
// Native-first is invariant across host and scoped environments;
|
|
397
|
+
// a key becomes auto fallback only after native is unavailable.
|
|
464
398
|
preferred_source: nativeAuthed ? "native_session" : apiKey ? "api_key_env" : null,
|
|
465
399
|
},
|
|
466
400
|
},
|
|
@@ -474,7 +408,7 @@ export function createCursorAdapter(deps = {}) {
|
|
|
474
408
|
});
|
|
475
409
|
},
|
|
476
410
|
async doctor(_spec) {
|
|
477
|
-
const version = await runtime.detectVersion();
|
|
411
|
+
const version = await runtime.detectVersion(_spec.abortSignal);
|
|
478
412
|
if (version === null) {
|
|
479
413
|
return ConformanceReportSchema.parse({
|
|
480
414
|
harness_id: "cursor",
|
|
@@ -483,22 +417,32 @@ export function createCursorAdapter(deps = {}) {
|
|
|
483
417
|
reasons: ["cursor-agent not found (install Cursor CLI or set CLAUDEXOR_CURSOR_BIN)"],
|
|
484
418
|
});
|
|
485
419
|
}
|
|
420
|
+
const requestedSource = _spec.authSource;
|
|
421
|
+
const probeNative = requestedSource === undefined || requestedSource === "native_session";
|
|
422
|
+
const probeApi = requestedSource === undefined || requestedSource === "api_key_env";
|
|
486
423
|
const env = cursorNativeEnv(_spec.env);
|
|
487
424
|
const scopedHome = Boolean(_spec.env?.["HOME"]);
|
|
488
|
-
const authPreference =
|
|
489
|
-
|
|
425
|
+
const authPreference = requestedSource === "native_session"
|
|
426
|
+
? "subscription"
|
|
427
|
+
: requestedSource === "api_key_env"
|
|
428
|
+
? "api_key"
|
|
429
|
+
: (_spec.authPreference ?? "auto");
|
|
430
|
+
if (probeNative && scopedHome)
|
|
490
431
|
maybeBridgeScopedHome(env);
|
|
491
|
-
const
|
|
492
|
-
|
|
432
|
+
const nativeProbe = probeNative
|
|
433
|
+
? await runtime.nativeAuthOk(env, _spec.abortSignal)
|
|
434
|
+
: { authed: false, probeError: null };
|
|
435
|
+
const nativeAuthed = nativeProbe.authed;
|
|
436
|
+
const key = probeApi ? runtime.cursorApiKey(_spec.env) : null;
|
|
493
437
|
const apiKey = key !== null;
|
|
494
438
|
const shouldSmokeApiKey = shouldSmokeCursorApiKey({
|
|
495
439
|
hasKey: apiKey,
|
|
496
440
|
authPreference,
|
|
497
441
|
nativeAuthed,
|
|
498
|
-
|
|
442
|
+
nativeProbeError: nativeProbe.probeError,
|
|
499
443
|
});
|
|
500
444
|
const apiSmoke = key && shouldSmokeApiKey
|
|
501
|
-
? await smokeCursorApiKey(runtime, key)
|
|
445
|
+
? await smokeCursorApiKey(runtime, key, _spec.fresh === true)
|
|
502
446
|
: {
|
|
503
447
|
ok: false,
|
|
504
448
|
detail: key
|
|
@@ -506,8 +450,8 @@ export function createCursorAdapter(deps = {}) {
|
|
|
506
450
|
: "no Cursor API key",
|
|
507
451
|
};
|
|
508
452
|
// Readiness doctrine: a key string alone is source availability, not
|
|
509
|
-
// proven readiness.
|
|
510
|
-
// API
|
|
453
|
+
// proven readiness. A bridged native status probe proves the exact scoped
|
|
454
|
+
// environment; API fallback still requires its isolated smoke.
|
|
511
455
|
const routeableIntents = [
|
|
512
456
|
"plan",
|
|
513
457
|
"spec",
|
|
@@ -520,24 +464,7 @@ export function createCursorAdapter(deps = {}) {
|
|
|
520
464
|
"explain",
|
|
521
465
|
"audit",
|
|
522
466
|
];
|
|
523
|
-
const allIntents = [
|
|
524
|
-
...routeableIntents,
|
|
525
|
-
"orchestrate",
|
|
526
|
-
];
|
|
527
|
-
// Write-class intents run in isolated envelopes with a scoped HOME where
|
|
528
|
-
// the native cursor session is unreachable — they REQUIRE the key
|
|
529
|
-
// fallback. Native-only auth honestly enables only the non-envelope
|
|
530
|
-
// (read-only) intents so doctor-ok can never precede a guaranteed run
|
|
531
|
-
// failure (readiness/routing contract).
|
|
532
|
-
const readOnlyIntents = [
|
|
533
|
-
"plan",
|
|
534
|
-
"spec",
|
|
535
|
-
"review",
|
|
536
|
-
"verify",
|
|
537
|
-
"synthesize",
|
|
538
|
-
"explain",
|
|
539
|
-
"audit",
|
|
540
|
-
];
|
|
467
|
+
const allIntents = [...routeableIntents, "orchestrate"];
|
|
541
468
|
const route = selectCursorAuthRoute({
|
|
542
469
|
authPreference,
|
|
543
470
|
hasKey: apiKey,
|
|
@@ -545,38 +472,101 @@ export function createCursorAdapter(deps = {}) {
|
|
|
545
472
|
nativeAuthed,
|
|
546
473
|
scopedHome,
|
|
547
474
|
});
|
|
548
|
-
const enabled = route === "
|
|
475
|
+
const enabled = route === "unavailable" ? [] : routeableIntents;
|
|
549
476
|
const ok = route !== "unavailable";
|
|
477
|
+
const selectedAvailable = authPreference === "subscription"
|
|
478
|
+
? nativeAuthed
|
|
479
|
+
: authPreference === "api_key"
|
|
480
|
+
? apiKey
|
|
481
|
+
: nativeAuthed || apiKey;
|
|
482
|
+
const probeUnknown = authPreference !== "api_key" && nativeProbe.probeError !== null;
|
|
483
|
+
const nativeSource = nativeProbe.probeError
|
|
484
|
+
? {
|
|
485
|
+
source: "native_session",
|
|
486
|
+
availability: "unknown",
|
|
487
|
+
verification: "not_run",
|
|
488
|
+
detail: `Cursor status probe failed: ${nativeProbe.probeError}`,
|
|
489
|
+
}
|
|
490
|
+
: nativeAuthed
|
|
491
|
+
? {
|
|
492
|
+
source: "native_session",
|
|
493
|
+
availability: "available",
|
|
494
|
+
verification: "passed",
|
|
495
|
+
detail: "native Cursor session passed the status probe in the exact run environment",
|
|
496
|
+
}
|
|
497
|
+
: {
|
|
498
|
+
source: "native_session",
|
|
499
|
+
availability: "unavailable",
|
|
500
|
+
verification: "not_run",
|
|
501
|
+
detail: "native Cursor session is not authenticated",
|
|
502
|
+
};
|
|
503
|
+
const apiSource = {
|
|
504
|
+
source: "api_key_env",
|
|
505
|
+
availability: apiKey ? "available" : "unavailable",
|
|
506
|
+
verification: apiSmoke.ok ? "passed" : shouldSmokeApiKey ? "failed" : "not_run",
|
|
507
|
+
detail: apiSmoke.detail,
|
|
508
|
+
};
|
|
509
|
+
const authSources = requestedSource === "native_session"
|
|
510
|
+
? [nativeSource]
|
|
511
|
+
: requestedSource === "api_key_env"
|
|
512
|
+
? [apiSource]
|
|
513
|
+
: requestedSource !== undefined
|
|
514
|
+
? [
|
|
515
|
+
{
|
|
516
|
+
source: requestedSource,
|
|
517
|
+
availability: "unavailable",
|
|
518
|
+
verification: "not_run",
|
|
519
|
+
detail: `Cursor does not support ${requestedSource}`,
|
|
520
|
+
},
|
|
521
|
+
]
|
|
522
|
+
: [nativeSource, apiSource];
|
|
550
523
|
return ConformanceReportSchema.parse({
|
|
551
524
|
harness_id: "cursor",
|
|
552
|
-
status: ok ? "ok" :
|
|
525
|
+
status: ok ? "ok" : selectedAvailable || probeUnknown ? "degraded" : "unavailable",
|
|
553
526
|
checks: [
|
|
554
527
|
{ id: "installed", status: "pass", detail: version },
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
528
|
+
...(probeNative
|
|
529
|
+
? [
|
|
530
|
+
{
|
|
531
|
+
id: "auth",
|
|
532
|
+
status: nativeAuthed ? "pass" : "fail",
|
|
533
|
+
detail: nativeProbe.probeError ?? nativeSource.detail,
|
|
534
|
+
},
|
|
535
|
+
]
|
|
536
|
+
: []),
|
|
537
|
+
...(probeApi
|
|
538
|
+
? [
|
|
539
|
+
{
|
|
540
|
+
id: "stored_key",
|
|
541
|
+
status: apiKey ? "pass" : "fail",
|
|
542
|
+
detail: apiKey ? "cursor secret/env available" : "no Cursor API-key fallback",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: "isolated_api_smoke",
|
|
546
|
+
status: apiSmoke.ok ? "pass" : shouldSmokeApiKey ? "fail" : "skip",
|
|
547
|
+
detail: apiSmoke.detail,
|
|
548
|
+
},
|
|
549
|
+
]
|
|
550
|
+
: []),
|
|
568
551
|
],
|
|
552
|
+
auth_sources: authSources,
|
|
569
553
|
enabled_intents: enabled,
|
|
570
554
|
disabled_intents: allIntents.filter((i) => !enabled.includes(i)),
|
|
571
555
|
reasons: ok
|
|
572
|
-
?
|
|
573
|
-
|
|
574
|
-
:
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
556
|
+
? []
|
|
557
|
+
: nativeProbe.probeError && authPreference !== "api_key"
|
|
558
|
+
? [`Cursor native-session probe failed: ${nativeProbe.probeError}`]
|
|
559
|
+
: authPreference === "subscription"
|
|
560
|
+
? ["Cursor subscription route is not ready (run `cursor-agent login`)"]
|
|
561
|
+
: authPreference === "api_key"
|
|
562
|
+
? [
|
|
563
|
+
apiKey
|
|
564
|
+
? `cursor key present but route unproven: ${apiSmoke.detail}`
|
|
565
|
+
: "Cursor API-key route is not configured",
|
|
566
|
+
]
|
|
567
|
+
: apiKey
|
|
568
|
+
? [`cursor key present but route unproven: ${apiSmoke.detail}`]
|
|
569
|
+
: ["not authenticated (cursor-agent login or set CURSOR_API_KEY)"],
|
|
580
570
|
});
|
|
581
571
|
},
|
|
582
572
|
run(spec) {
|
|
@@ -588,6 +578,12 @@ export function createCursorAdapter(deps = {}) {
|
|
|
588
578
|
async models(spec) {
|
|
589
579
|
return listCursorModelsFromReadyRoute(runtime, spec);
|
|
590
580
|
},
|
|
581
|
+
// INV-135 (release wave round-15 #1): a valid cursor profile must admit
|
|
582
|
+
// the route even when the default store is logged out — the orchestrator
|
|
583
|
+
// consults THIS probe to override the default auth verdict.
|
|
584
|
+
async probeCredentialProfile(profile) {
|
|
585
|
+
return probeCursorCredentialProfile(profile, runtime.resolveProfileSecret);
|
|
586
|
+
},
|
|
591
587
|
};
|
|
592
588
|
}
|
|
593
589
|
async function* runCursor(spec, deps) {
|
|
@@ -602,17 +598,35 @@ async function* runCursor(spec, deps) {
|
|
|
602
598
|
return;
|
|
603
599
|
}
|
|
604
600
|
const args = ["-p", "--output-format", "stream-json", ...accessArgs(spec.access)];
|
|
601
|
+
// W-C4 live deltas (engine-gated; the parser applies the documented taxonomy).
|
|
602
|
+
if (spec.stream_deltas)
|
|
603
|
+
args.push("--stream-partial-output");
|
|
605
604
|
if (spec.model_hint)
|
|
606
605
|
args.push("--model", spec.model_hint);
|
|
607
606
|
// Resume the thread's native cursor chat as a follow-up turn.
|
|
608
607
|
if (spec.resume_session_id)
|
|
609
608
|
args.push("--resume", spec.resume_session_id);
|
|
610
|
-
|
|
611
|
-
|
|
609
|
+
// Cursor has no native system-prompt flag; layer instructions as a delimited
|
|
610
|
+
// prompt prefix (the engine already withheld them from synthesis/reviewers).
|
|
611
|
+
args.push(promptWithInstructions(spec));
|
|
612
|
+
// INV-135 strict profile routing: cursor's native login is a singleton, so
|
|
613
|
+
// a profile is exactly its secret-ref API key (smoked before use like the
|
|
614
|
+
// default key route); other kinds refuse typed.
|
|
615
|
+
const profile = spec.credential_profile;
|
|
616
|
+
const profileGate = profile
|
|
617
|
+
? cursorProfileKeyOrRefusal(profile, deps.resolveProfileSecret)
|
|
618
|
+
: null;
|
|
619
|
+
if (profileGate && "refusal" in profileGate) {
|
|
620
|
+
yield { type: "error", session_id: spec.session_id, ts: nowIso(), error: profileGate.refusal };
|
|
621
|
+
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const routeDeps = profileGate ? { ...deps, cursorApiKey: () => profileGate.key } : deps;
|
|
625
|
+
const { route, env, key, nativeAuthed, scopedHome } = await resolveCursorAuthRoute(routeDeps, {
|
|
612
626
|
env: spec.env,
|
|
613
|
-
authPreference: spec.auth_preference,
|
|
627
|
+
authPreference: profile ? "api_key" : spec.auth_preference,
|
|
628
|
+
abortSignal: abortSignalFromSpec(spec),
|
|
614
629
|
});
|
|
615
|
-
const preferApi = spec.auth_preference === "api_key";
|
|
616
630
|
if (route === "api_key" && key) {
|
|
617
631
|
env.CURSOR_API_KEY = key;
|
|
618
632
|
if (shouldDiscloseCursorAutoApiRoute({
|
|
@@ -635,32 +649,22 @@ async function* runCursor(spec, deps) {
|
|
|
635
649
|
}
|
|
636
650
|
else if (route === "local_session") {
|
|
637
651
|
env.CURSOR_API_KEY = null;
|
|
638
|
-
if (preferApi) {
|
|
639
|
-
yield {
|
|
640
|
-
type: "message",
|
|
641
|
-
session_id: spec.session_id,
|
|
642
|
-
ts: nowIso(),
|
|
643
|
-
payload: {
|
|
644
|
-
auth_switched: true,
|
|
645
|
-
from_auth_mode: "api_key",
|
|
646
|
-
to_auth_mode: "local_session",
|
|
647
|
-
reason: "auth_unavailable",
|
|
648
|
-
},
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
652
|
}
|
|
652
653
|
else {
|
|
653
654
|
yield {
|
|
654
655
|
type: "error",
|
|
655
656
|
session_id: spec.session_id,
|
|
656
657
|
ts: nowIso(),
|
|
657
|
-
error:
|
|
658
|
-
?
|
|
659
|
-
:
|
|
658
|
+
error: profile
|
|
659
|
+
? `credential profile "${profile.profile_id}": the Cursor API-key smoke did not pass for its stored key`
|
|
660
|
+
: scopedHome
|
|
661
|
+
? "scoped Cursor HOME requires either a bridged native session or a smoke-proven Cursor API key fallback"
|
|
662
|
+
: "Cursor requires either a native session or a smoke-proven Cursor API key fallback",
|
|
660
663
|
};
|
|
661
664
|
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
662
665
|
return;
|
|
663
666
|
}
|
|
667
|
+
const cursorParser = createCursorParser(route === "local_session" ? "vendor_native" : "managed_api_key", route === "local_session" ? "native_session" : "api_key_env");
|
|
664
668
|
yield* deps.runCliHarness({
|
|
665
669
|
bin: BIN,
|
|
666
670
|
args,
|
|
@@ -668,7 +672,13 @@ async function* runCursor(spec, deps) {
|
|
|
668
672
|
env,
|
|
669
673
|
label: "cursor-agent",
|
|
670
674
|
redact: redactSecrets,
|
|
671
|
-
parseEvent:
|
|
675
|
+
parseEvent: (obj, sessionId) => {
|
|
676
|
+
const out = cursorParser(obj, sessionId);
|
|
677
|
+
if (out && profile)
|
|
678
|
+
for (const ev of out)
|
|
679
|
+
ev.credential_profile_id = profile.profile_id;
|
|
680
|
+
return out;
|
|
681
|
+
},
|
|
672
682
|
});
|
|
673
683
|
}
|
|
674
684
|
//# sourceMappingURL=index.js.map
|