@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/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
- image_input: "none",
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"], { timeoutMs: 10_000 });
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 nativeAuthed = await deps.nativeAuthOk(env);
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
- scopedHome,
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 { route, env, key, nativeAuthed, apiSmoke, scopedHome };
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
- if (await deps.nativeAuthOk(nativeEnv)) {
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 nativeAuthed = await runtime.nativeAuthOk(cursorNativeEnv());
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
- // Static source preference mirrors the normal non-scoped route.
462
- // Scoped/envelope runs may still choose a smoke-proven API key at
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 = _spec.authPreference ?? "auto";
489
- if (scopedHome)
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 nativeAuthed = await runtime.nativeAuthOk(env);
492
- const key = runtime.cursorApiKey(_spec.env);
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
- scopedHome,
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. Native auth proves read-only/session reuse; an isolated
510
- // API-key smoke proves envelope/write routes.
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 === "api_key" ? routeableIntents : route === "local_session" ? readOnlyIntents : [];
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" : apiKey ? "degraded" : "unavailable",
525
+ status: ok ? "ok" : selectedAvailable || probeUnknown ? "degraded" : "unavailable",
553
526
  checks: [
554
527
  { id: "installed", status: "pass", detail: version },
555
- { id: "auth", status: nativeAuthed ? "pass" : "fail" },
556
- {
557
- id: "stored_key",
558
- status: apiKey ? "pass" : "fail",
559
- detail: apiKey
560
- ? "cursor secret/env available"
561
- : "no cursor key fallback (write/envelope intents disabled)",
562
- },
563
- {
564
- id: "isolated_api_smoke",
565
- status: apiSmoke.ok ? "pass" : apiKey ? "fail" : "skip",
566
- detail: apiSmoke.detail,
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
- ? apiSmoke.ok
573
- ? []
574
- : [
575
- "native session only: isolated envelope (write) intents need a passing Cursor API-key smoke",
576
- ]
577
- : apiKey
578
- ? [`cursor key present but route unproven: ${apiSmoke.detail}`]
579
- : ["not authenticated (cursor-agent login or set CURSOR_API_KEY)"],
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
- args.push(spec.prompt);
611
- const { route, env, key, nativeAuthed, scopedHome } = await resolveCursorAuthRoute(deps, {
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: scopedHome
658
- ? "scoped Cursor HOME requires either a bridged native session or a smoke-proven Cursor API key fallback"
659
- : "Cursor requires either a native session or a smoke-proven Cursor API key fallback",
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: createCursorParser(),
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