@cursor/july 0.1.43 → 0.1.45

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.
Files changed (136) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/channels/github/github-channel.d.ts.map +1 -1
  3. package/dist/channels/github/github-channel.js +13 -5
  4. package/dist/channels/github/types.d.ts +8 -0
  5. package/dist/channels/github/types.d.ts.map +1 -1
  6. package/dist/channels.d.ts +17 -0
  7. package/dist/channels.d.ts.map +1 -1
  8. package/dist/channels.js +31 -0
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.DsTLo2j1.js → app.fHnNXEJ6.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.BMT0xM4I.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.DoVfutcV.js → VPLocalSearchBox.Cf6ghfFY.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.DtuVtKkc.js → theme.B_5-VLe8.js} +2 -2
  15. package/dist/docs/assets/guides_opentelemetry.md.Csn7ZI25.js +27 -0
  16. package/dist/docs/assets/guides_opentelemetry.md.Csn7ZI25.lean.js +1 -0
  17. package/dist/docs/assets/index.md.COeoSkcF.js +5 -0
  18. package/dist/docs/assets/index.md.COeoSkcF.lean.js +1 -0
  19. package/dist/docs/assets/{quickstart.md.rIbfahJM.js → quickstart.md.DZxBu44y.js} +18 -49
  20. package/dist/docs/assets/quickstart.md.DZxBu44y.lean.js +1 -0
  21. package/dist/docs/assets/{reference_agent-config.md.CRmkoxd6.js → reference_agent-config.md.XRpxctsA.js} +9 -4
  22. package/dist/docs/assets/{reference_agent-config.md.CRmkoxd6.lean.js → reference_agent-config.md.XRpxctsA.lean.js} +1 -1
  23. package/dist/docs/assets/{reference_cli.md.DqEqQpGn.js → reference_cli.md.BDmjPPAm.js} +2 -2
  24. package/dist/docs/assets/{reference_tools.md.lSrsTxYJ.js → reference_tools.md.kJfJAP2m.js} +1 -1
  25. package/dist/docs/building-with-agents.html +3 -3
  26. package/dist/docs/concepts.html +3 -3
  27. package/dist/docs/deployment.html +3 -3
  28. package/dist/docs/evals.html +3 -3
  29. package/dist/docs/example-agents/approval-buddy.html +3 -3
  30. package/dist/docs/example-agents/benny.html +3 -3
  31. package/dist/docs/example-agents/bugbot.html +3 -3
  32. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  33. package/dist/docs/example-agents/codeowners-review.html +3 -3
  34. package/dist/docs/example-agents/concierge.html +3 -3
  35. package/dist/docs/example-agents/fsd.html +3 -3
  36. package/dist/docs/example-agents/index.html +3 -3
  37. package/dist/docs/example-agents/knowledge-base.html +3 -3
  38. package/dist/docs/example-agents/oncall.html +3 -3
  39. package/dist/docs/example-agents/security-reviewer.html +3 -3
  40. package/dist/docs/example-agents/slack-agent.html +3 -3
  41. package/dist/docs/example-agents/weather-agent.html +3 -3
  42. package/dist/docs/guides/agent-to-agent.html +3 -3
  43. package/dist/docs/guides/cloud-runtime.html +3 -3
  44. package/dist/docs/guides/github.html +3 -3
  45. package/dist/docs/guides/human-in-the-loop.html +3 -3
  46. package/dist/docs/guides/mcp-oauth.html +3 -3
  47. package/dist/docs/guides/opentelemetry.html +12 -5
  48. package/dist/docs/guides/slack.html +3 -3
  49. package/dist/docs/guides/webhooks.html +3 -3
  50. package/dist/docs/hashmap.json +1 -1
  51. package/dist/docs/hillclimbing.html +3 -3
  52. package/dist/docs/index.html +8 -28
  53. package/dist/docs/quickstart.html +24 -55
  54. package/dist/docs/reference/agent-config.html +11 -6
  55. package/dist/docs/reference/artifacts.html +3 -3
  56. package/dist/docs/reference/channels.html +3 -3
  57. package/dist/docs/reference/cli.html +6 -6
  58. package/dist/docs/reference/connections.html +3 -3
  59. package/dist/docs/reference/hooks.html +3 -3
  60. package/dist/docs/reference/http-api.html +3 -3
  61. package/dist/docs/reference/instructions.html +3 -3
  62. package/dist/docs/reference/playground.html +3 -3
  63. package/dist/docs/reference/project-layout.html +3 -3
  64. package/dist/docs/reference/prompt.html +3 -3
  65. package/dist/docs/reference/schedules.html +3 -3
  66. package/dist/docs/reference/sessions.html +3 -3
  67. package/dist/docs/reference/skills.html +3 -3
  68. package/dist/docs/reference/subagents.html +3 -3
  69. package/dist/docs/reference/tools.html +5 -5
  70. package/dist/docs/scaffolding-agents.html +3 -3
  71. package/dist/docs/storage.html +3 -3
  72. package/dist/docs/templates/triage.html +3 -3
  73. package/dist/docs/troubleshooting.html +3 -3
  74. package/dist/index.d.ts +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/internal/cli-ax.js +2 -2
  78. package/dist/internal/cli-deploy.d.ts.map +1 -1
  79. package/dist/internal/cli-deploy.js +38 -7
  80. package/dist/internal/deploy-client.d.ts +8 -0
  81. package/dist/internal/deploy-client.d.ts.map +1 -1
  82. package/dist/internal/deploy-client.js +5 -2
  83. package/dist/internal/discovery.d.ts.map +1 -1
  84. package/dist/internal/discovery.js +46 -9
  85. package/dist/internal/host-otel.d.ts +25 -0
  86. package/dist/internal/host-otel.d.ts.map +1 -0
  87. package/dist/internal/host-otel.js +162 -0
  88. package/dist/internal/init-project.js +10 -10
  89. package/dist/internal/otel/collector.d.ts +5 -0
  90. package/dist/internal/otel/collector.d.ts.map +1 -1
  91. package/dist/internal/otel/collector.js +68 -27
  92. package/dist/internal/sdk-runner.d.ts +1 -1
  93. package/dist/internal/sdk-runner.d.ts.map +1 -1
  94. package/dist/internal/sdk-runner.js +1 -1
  95. package/dist/internal/server.d.ts.map +1 -1
  96. package/dist/internal/server.js +1 -0
  97. package/dist/internal/session-engine.d.ts +5 -1
  98. package/dist/internal/session-engine.d.ts.map +1 -1
  99. package/dist/internal/session-engine.js +23 -5
  100. package/dist/playground/assets/index-CZH9EFvO.css +1 -0
  101. package/dist/playground/assets/{index-D-DIYQzw.js → index-Dces8BDp.js} +26 -26
  102. package/dist/playground/index.html +2 -2
  103. package/dist/types.d.ts +81 -2
  104. package/dist/types.d.ts.map +1 -1
  105. package/docs/README.md +33 -106
  106. package/docs/guides/opentelemetry.md +30 -0
  107. package/docs/quickstart.md +89 -227
  108. package/docs/reference/agent-config.md +52 -0
  109. package/docs/reference/cli.md +2 -2
  110. package/docs/reference/tools.md +5 -4
  111. package/package.json +2 -2
  112. package/skills/otel/SKILL.md +20 -2
  113. package/src/channels/github/github-channel.ts +9 -0
  114. package/src/channels/github/types.ts +4 -0
  115. package/src/channels.ts +38 -0
  116. package/src/index.ts +1 -0
  117. package/src/internal/cli-ax.ts +2 -2
  118. package/src/internal/cli-deploy.ts +58 -3
  119. package/src/internal/deploy-client.ts +11 -0
  120. package/src/internal/discovery.ts +49 -0
  121. package/src/internal/host-otel.ts +252 -0
  122. package/src/internal/init-project.ts +10 -10
  123. package/src/internal/otel/collector.ts +135 -50
  124. package/src/internal/sdk-runner.ts +6 -0
  125. package/src/internal/server.ts +1 -0
  126. package/src/internal/session-engine.ts +32 -2
  127. package/src/types.ts +89 -2
  128. package/dist/docs/assets/chunks/@localSearchIndexroot.BZx4pmZL.js +0 -1
  129. package/dist/docs/assets/guides_opentelemetry.md.DmyUwC5R.js +0 -20
  130. package/dist/docs/assets/guides_opentelemetry.md.DmyUwC5R.lean.js +0 -1
  131. package/dist/docs/assets/index.md.CMc5FP7E.js +0 -25
  132. package/dist/docs/assets/index.md.CMc5FP7E.lean.js +0 -1
  133. package/dist/docs/assets/quickstart.md.rIbfahJM.lean.js +0 -1
  134. package/dist/playground/assets/index-DQTH622f.css +0 -1
  135. /package/dist/docs/assets/{reference_cli.md.DqEqQpGn.lean.js → reference_cli.md.BDmjPPAm.lean.js} +0 -0
  136. /package/dist/docs/assets/{reference_tools.md.lSrsTxYJ.lean.js → reference_tools.md.kJfJAP2m.lean.js} +0 -0
@@ -27,6 +27,15 @@ const CONTENT_MAX = 2_048;
27
27
  export interface OtelCollector {
28
28
  onEvent(event: SessionEvent, session: SessionInfo): void;
29
29
  close(): void;
30
+ /** Merge author tags onto the session; paints open spans immediately. */
31
+ setSessionAttributes(
32
+ sessionId: string,
33
+ attributes: Record<string, AttrValue>
34
+ ): void;
35
+ /** Session tags previously set by {@link setSessionAttributes}. */
36
+ getSessionAttributes(
37
+ sessionId: string
38
+ ): Record<string, AttrValue> | undefined;
30
39
  }
31
40
 
32
41
  interface TurnState {
@@ -49,6 +58,7 @@ export function createOtelCollector(
49
58
  log: (line: string) => void
50
59
  ): OtelCollector {
51
60
  const sessions = new Map<string, SessionState>();
61
+ const extraBySession = new Map<string, Record<string, AttrValue>>();
52
62
  const metrics = runtime.meter ? buildMetrics(runtime.meter) : undefined;
53
63
  const logOpts =
54
64
  runtime.config.logs === false ? undefined : runtime.config.logs;
@@ -71,6 +81,17 @@ export function createOtelCollector(
71
81
  state.span.end();
72
82
  sessions.delete(sessionId);
73
83
  }
84
+ extraBySession.clear();
85
+ },
86
+ setSessionAttributes(sessionId, attributes) {
87
+ const bag = extraBySession.get(sessionId) ?? {};
88
+ Object.assign(bag, attributes);
89
+ extraBySession.set(sessionId, bag);
90
+ paintOpenSpans(sessionId, attributes);
91
+ },
92
+ getSessionAttributes(sessionId) {
93
+ const bag = extraBySession.get(sessionId);
94
+ return bag === undefined ? undefined : { ...bag };
74
95
  },
75
96
  };
76
97
 
@@ -215,7 +236,7 @@ export function createOtelCollector(
215
236
  {
216
237
  kind: SpanKind.INTERNAL,
217
238
  startTime,
218
- attributes: attrs({
239
+ attributes: join(event.sessionId, {
219
240
  "cursor.conversation.id": event.sessionId,
220
241
  "cursor.tool.name": call.toolName,
221
242
  "cursor.tool.kind": "builtin",
@@ -287,12 +308,15 @@ export function createOtelCollector(
287
308
  span.end(startTime);
288
309
  turn?.tools.delete(event.data.callId);
289
310
  }
290
- metrics?.toolCalls.add(1, {
291
- "cursor.tool.name": event.data.toolName,
292
- "cursor.tool.kind": "builtin",
293
- "cursor.tool.status": status,
294
- "agent_sdk.agent": agentName,
295
- });
311
+ metrics?.toolCalls.add(
312
+ 1,
313
+ join(event.sessionId, {
314
+ "cursor.tool.name": event.data.toolName,
315
+ "cursor.tool.kind": "builtin",
316
+ "cursor.tool.status": status,
317
+ "agent_sdk.agent": agentName,
318
+ })
319
+ );
296
320
  emitLog(
297
321
  "action.result",
298
322
  event.data.isError ? SeverityNumber.ERROR : SeverityNumber.INFO,
@@ -327,7 +351,7 @@ export function createOtelCollector(
327
351
  {
328
352
  kind: SpanKind.INTERNAL,
329
353
  startTime,
330
- attributes: attrs({
354
+ attributes: join(event.sessionId, {
331
355
  "cursor.conversation.id": event.sessionId,
332
356
  "agent_sdk.agent": agentName,
333
357
  "agent_sdk.turn_id": event.turnId,
@@ -343,10 +367,13 @@ export function createOtelCollector(
343
367
  trace.setSpan(context.active(), turn.span)
344
368
  );
345
369
  turn.subagents.set(event.data.callId, span);
346
- metrics?.subagents.add(1, {
347
- "agent_sdk.agent": agentName,
348
- "agent_sdk.subagent_name": event.data.name ?? "unknown",
349
- });
370
+ metrics?.subagents.add(
371
+ 1,
372
+ join(event.sessionId, {
373
+ "agent_sdk.agent": agentName,
374
+ "agent_sdk.subagent_name": event.data.name ?? "unknown",
375
+ })
376
+ );
350
377
  return;
351
378
  }
352
379
  case "subagent.completed": {
@@ -389,7 +416,7 @@ export function createOtelCollector(
389
416
  "agent_sdk.usage.reasoning_tokens": usage.reasoningTokens,
390
417
  })
391
418
  );
392
- recordTokens(usage);
419
+ recordTokens(event.sessionId, usage);
393
420
  }
394
421
  if (cost !== undefined) {
395
422
  turn.span.setAttributes(
@@ -399,18 +426,24 @@ export function createOtelCollector(
399
426
  "agent_sdk.cost_source": cost.source,
400
427
  })
401
428
  );
402
- metrics?.cost.add(cost.totalUsd, {
403
- "cursor.model.name": cost.model,
404
- "agent_sdk.agent": agentName,
405
- });
429
+ metrics?.cost.add(
430
+ cost.totalUsd,
431
+ join(event.sessionId, {
432
+ "cursor.model.name": cost.model,
433
+ "agent_sdk.agent": agentName,
434
+ })
435
+ );
406
436
  }
407
437
  turn.span.setStatus({ code: SpanStatusCode.OK });
408
438
  turn.span.end(startTime);
409
439
  sessions.get(event.sessionId)?.turns.delete(event.turnId);
410
- metrics?.turns.add(1, {
411
- "agent_sdk.agent": agentName,
412
- "agent_sdk.turn_status": "completed",
413
- });
440
+ metrics?.turns.add(
441
+ 1,
442
+ join(event.sessionId, {
443
+ "agent_sdk.agent": agentName,
444
+ "agent_sdk.turn_status": "completed",
445
+ })
446
+ );
414
447
  emitLog(
415
448
  "turn.completed",
416
449
  SeverityNumber.INFO,
@@ -441,10 +474,13 @@ export function createOtelCollector(
441
474
  }
442
475
  turn.span.end(startTime);
443
476
  sessions.get(event.sessionId)?.turns.delete(event.turnId);
444
- metrics?.turns.add(1, {
445
- "agent_sdk.agent": agentName,
446
- "agent_sdk.turn_status": "failed",
447
- });
477
+ metrics?.turns.add(
478
+ 1,
479
+ join(event.sessionId, {
480
+ "agent_sdk.agent": agentName,
481
+ "agent_sdk.turn_status": "failed",
482
+ })
483
+ );
448
484
  emitLog(
449
485
  "turn.failed",
450
486
  SeverityNumber.ERROR,
@@ -531,7 +567,7 @@ export function createOtelCollector(
531
567
  {
532
568
  kind: SpanKind.INTERNAL,
533
569
  startTime: args.startTime,
534
- attributes: attrs({
570
+ attributes: join(args.sessionId, {
535
571
  "cursor.conversation.id": args.sessionId,
536
572
  "agent_sdk.agent": agentName,
537
573
  "agent_sdk.channel": args.channelId,
@@ -549,10 +585,13 @@ export function createOtelCollector(
549
585
  };
550
586
  sessions.set(args.sessionId, state);
551
587
  if (!args.restored) {
552
- metrics?.sessions.add(1, {
553
- "agent_sdk.agent": agentName,
554
- "agent_sdk.channel": args.channelId,
555
- });
588
+ metrics?.sessions.add(
589
+ 1,
590
+ join(args.sessionId, {
591
+ "agent_sdk.agent": agentName,
592
+ "agent_sdk.channel": args.channelId,
593
+ })
594
+ );
556
595
  }
557
596
  return state;
558
597
  }
@@ -600,7 +639,7 @@ export function createOtelCollector(
600
639
  {
601
640
  kind: SpanKind.INTERNAL,
602
641
  startTime: args.startTime,
603
- attributes: attrs({
642
+ attributes: join(args.sessionId, {
604
643
  "cursor.conversation.id": args.sessionId,
605
644
  "agent_sdk.agent": agentName,
606
645
  "agent_sdk.turn_id": args.turnId,
@@ -666,34 +705,77 @@ export function createOtelCollector(
666
705
  );
667
706
  state.span.end(args.endTime);
668
707
  sessions.delete(args.sessionId);
708
+ extraBySession.delete(args.sessionId);
669
709
  }
670
710
 
671
- function recordTokens(usage: {
672
- inputTokens: number;
673
- outputTokens: number;
674
- cacheReadTokens: number;
675
- cacheWriteTokens: number;
676
- reasoningTokens?: number;
677
- }): void {
711
+ function extra(sessionId: string): Record<string, AttrValue> {
712
+ return extraBySession.get(sessionId) ?? {};
713
+ }
714
+
715
+ function join(
716
+ sessionId: string,
717
+ rest: Record<string, AttrValue | undefined>
718
+ ): Record<string, AttrValue> {
719
+ return attrs({ ...extra(sessionId), ...rest });
720
+ }
721
+
722
+ function paintOpenSpans(
723
+ sessionId: string,
724
+ incoming: Record<string, AttrValue>
725
+ ): void {
726
+ const state = sessions.get(sessionId);
727
+ if (state === undefined) {
728
+ return;
729
+ }
730
+ const painted = attrs(incoming);
731
+ if (Object.keys(painted).length === 0) {
732
+ return;
733
+ }
734
+ state.span.setAttributes(painted);
735
+ for (const turn of state.turns.values()) {
736
+ turn.span.setAttributes(painted);
737
+ for (const span of turn.tools.values()) {
738
+ span.setAttributes(painted);
739
+ }
740
+ for (const span of turn.subagents.values()) {
741
+ span.setAttributes(painted);
742
+ }
743
+ }
744
+ }
745
+
746
+ function recordTokens(
747
+ sessionId: string,
748
+ usage: {
749
+ inputTokens: number;
750
+ outputTokens: number;
751
+ cacheReadTokens: number;
752
+ cacheWriteTokens: number;
753
+ reasoningTokens?: number;
754
+ }
755
+ ): void {
678
756
  if (metrics === undefined) {
679
757
  return;
680
758
  }
681
759
  const agent = agentName;
682
760
  // Wire token types match enterprise export (`cache_creation`, not write).
683
- addTokens(usage.inputTokens, "input", agent);
684
- addTokens(usage.outputTokens, "output", agent);
685
- addTokens(usage.cacheReadTokens, "cache_read", agent);
686
- addTokens(usage.cacheWriteTokens, "cache_creation", agent);
761
+ addTokens(sessionId, usage.inputTokens, "input", agent);
762
+ addTokens(sessionId, usage.outputTokens, "output", agent);
763
+ addTokens(sessionId, usage.cacheReadTokens, "cache_read", agent);
764
+ addTokens(sessionId, usage.cacheWriteTokens, "cache_creation", agent);
687
765
  if (usage.reasoningTokens !== undefined) {
688
766
  // Reasoning is Agent-SDK-only; enterprise wire has no reasoning enum.
689
- metrics.tokens.add(usage.reasoningTokens, {
690
- "cursor.token.type": "reasoning",
691
- "agent_sdk.agent": agent,
692
- });
767
+ metrics.tokens.add(
768
+ usage.reasoningTokens,
769
+ join(sessionId, {
770
+ "cursor.token.type": "reasoning",
771
+ "agent_sdk.agent": agent,
772
+ })
773
+ );
693
774
  }
694
775
  }
695
776
 
696
777
  function addTokens(
778
+ sessionId: string,
697
779
  value: number,
698
780
  type: "input" | "output" | "cache_read" | "cache_creation",
699
781
  agent: string
@@ -701,10 +783,13 @@ export function createOtelCollector(
701
783
  if (metrics === undefined || value === 0) {
702
784
  return;
703
785
  }
704
- metrics.tokens.add(value, {
705
- "cursor.token.type": type,
706
- "agent_sdk.agent": agent,
707
- });
786
+ metrics.tokens.add(
787
+ value,
788
+ join(sessionId, {
789
+ "cursor.token.type": type,
790
+ "agent_sdk.agent": agent,
791
+ })
792
+ );
708
793
  }
709
794
 
710
795
  /** Content opt-in for failure text (same gate as tool payloads). */
@@ -52,6 +52,7 @@ export type RunnerPrewarmRequest = Pick<
52
52
  | "workspaceDir"
53
53
  | "runnerStateDir"
54
54
  | "sandbox"
55
+ | "tools"
55
56
  | "mcpServers"
56
57
  | "subagents"
57
58
  | "model"
@@ -373,6 +374,11 @@ export class CursorSdkRunner implements AgentRunner {
373
374
  this.options.store ?? (await this.getStore(request.runnerStateDir));
374
375
  return {
375
376
  ...this.buildSharedOptions(request),
377
+ // The built-in tool allowlist rides AgentOptions, not `local`, and is
378
+ // deliberately absent from openCloudAgent's options: the SDK rejects
379
+ // tool restrictions on cloud agents. Passed on create *and* resume
380
+ // (both flow through here) — the SDK does not persist it across runs.
381
+ ...(request.tools === undefined ? {} : { tools: request.tools }),
376
382
  local: {
377
383
  cwd: request.workspaceDir,
378
384
  settingSources: ["project"],
@@ -435,6 +435,7 @@ export async function startServer(
435
435
  logger,
436
436
  platforms: hostPlatforms,
437
437
  otel: otelCollector,
438
+ otelMeter: otelRuntime?.meter,
438
439
  });
439
440
  // Rehydrate sessions from the authored storage sink before any
440
441
  // channel or route can address them (continuation tokens must resolve).
@@ -10,6 +10,7 @@
10
10
  import { mkdir, rm, writeFile } from "node:fs/promises";
11
11
  import { dirname, join } from "node:path";
12
12
  import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
13
+ import type { Meter } from "@opentelemetry/api";
13
14
  import {
14
15
  type ABDefinition,
15
16
  mergeABInstructions,
@@ -95,6 +96,7 @@ import { TurnEventMapper } from "./event-mapper.js";
95
96
  import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
96
97
  import { createHostFilesApi } from "./host-files.js";
97
98
  import { createHostKvApi } from "./host-kv.js";
99
+ import { createHostOtelApi, type HostOtelSessionApi } from "./host-otel.js";
98
100
  import {
99
101
  createHostPlatforms,
100
102
  createHostSlackClient,
@@ -246,6 +248,8 @@ export interface SessionEngineOptions {
246
248
  durableSessionStore?: HostKvApi;
247
249
  /** Process-level OTel collector; observe-only, never fatal. */
248
250
  otel?: OtelCollector;
251
+ /** Process-level meter for {@link HostContext.otel}. Absent → no-op counters. */
252
+ otelMeter?: Meter;
249
253
  }
250
254
 
251
255
  /**
@@ -362,6 +366,7 @@ export class SessionEngine {
362
366
  private readonly approvals = new ApprovalGate();
363
367
  private readonly abCollector: ABCollector;
364
368
  private readonly otel: OtelCollector | undefined;
369
+ private readonly otelHost: HostOtelSessionApi;
365
370
  private reminderApi: HostContext["reminders"];
366
371
  private evalsApi: HostContext["evals"];
367
372
  private readonly kvApi: HostContext["kv"];
@@ -395,6 +400,11 @@ export class SessionEngine {
395
400
  this.getCloudUsage = options.getCloudUsage ?? defaultGetCloudUsage;
396
401
  this.estimateTurnCost = options.estimateTurnCost ?? estimateTurnCostFromApi;
397
402
  this.otel = options.otel;
403
+ this.otelHost = createHostOtelApi({
404
+ agentName: options.project.name,
405
+ meter: options.otelMeter,
406
+ collector: options.otel,
407
+ });
398
408
  this.turnGovernor = new TurnGovernor(options.project.agent.concurrency);
399
409
  this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
400
410
  this.nudgeStore = new SlackNudgeStore(
@@ -574,7 +584,7 @@ export class SessionEngine {
574
584
  this.cloudServerToolsConnection = connection;
575
585
  }
576
586
 
577
- /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / Slack nudges). */
587
+ /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
578
588
  get host(): HostContext {
579
589
  return {
580
590
  mcp: this.mcpHost,
@@ -584,6 +594,7 @@ export class SessionEngine {
584
594
  evals: this.evalsApi,
585
595
  kv: this.kvApi,
586
596
  files: this.filesApi,
597
+ otel: this.otelHost,
587
598
  slackNudges: this.slackNudgeHostApi(),
588
599
  };
589
600
  }
@@ -598,7 +609,11 @@ export class SessionEngine {
598
609
  const host = this.host;
599
610
  return sessionId === undefined
600
611
  ? host
601
- : { ...host, files: this.filesApi.forSession(sessionId) };
612
+ : {
613
+ ...host,
614
+ files: this.filesApi.forSession(sessionId),
615
+ otel: this.otelHost.forSession(sessionId),
616
+ };
602
617
  }
603
618
 
604
619
  /** Unbound `ctx.artifacts` facade (channel handlers, scratch tool calls). */
@@ -1359,6 +1374,9 @@ export class SessionEngine {
1359
1374
  workspaceDir,
1360
1375
  runnerStateDir: join(this.stateRoot, "runner"),
1361
1376
  sandbox: agent.local?.sandbox === true,
1377
+ ...(agent.allowedTools === undefined
1378
+ ? {}
1379
+ : { tools: agent.allowedTools }),
1362
1380
  mcpServers: this.buildMcpServers("local"),
1363
1381
  subagents: buildRunnerSubagents(agent),
1364
1382
  ...(agent.model === undefined ? {} : { model: agent.model }),
@@ -1614,6 +1632,15 @@ export class SessionEngine {
1614
1632
  agentRuntime: agent.runtime,
1615
1633
  cloudOverride: record.cloudOverride,
1616
1634
  });
1635
+ // Fail closed: the SDK cannot enforce the built-in tool allowlist on
1636
+ // cloud agents yet, and a restriction that silently does not apply is
1637
+ // worse than a refused turn. Cloud-runtime agents already fail
1638
+ // discovery; this guards hybrid agents' per-send cloud sessions.
1639
+ if (runtime === "cloud" && agent.allowedTools !== undefined) {
1640
+ throw new Error(
1641
+ `Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on cloud turns yet. Refusing the cloud session rather than running it unrestricted.`
1642
+ );
1643
+ }
1617
1644
  const mergedCloud =
1618
1645
  runtime === "cloud"
1619
1646
  ? mergeCloudOptions({
@@ -1668,6 +1695,9 @@ export class SessionEngine {
1668
1695
  : { sdkAgentId: record.sdkAgentId }),
1669
1696
  workspaceDir: this.harnessCwd(record),
1670
1697
  sandbox: this.project.agent.local?.sandbox === true,
1698
+ ...(runtime === "local" && agent.allowedTools !== undefined
1699
+ ? { tools: agent.allowedTools }
1700
+ : {}),
1671
1701
  runnerStateDir: join(this.stateRoot, "runner"),
1672
1702
  prompt: turnPrompt,
1673
1703
  isFirstTurn,
package/src/types.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * SDK as the execution harness.
9
9
  */
10
10
 
11
- import type { InteractionUpdate, SDKCustomTool } from "@cursor/sdk";
11
+ import type { InteractionUpdate, SDKCustomTool, ToolName } from "@cursor/sdk";
12
12
  import type { z } from "zod";
13
13
  import type { ABConfigFile } from "./ab.js";
14
14
  import type { OtelConfig, OtelDefinition } from "./otel.js";
@@ -206,6 +206,40 @@ export interface AgentConfig {
206
206
  * {@link BuiltinToolsConfig}.
207
207
  */
208
208
  builtinTools?: BuiltinToolsConfig;
209
+ /**
210
+ * Allowlist of built-in harness tools offered to the model
211
+ * (`AgentOptions.tools` on the SDK). Opt-in by design: unset keeps the
212
+ * model's full standard toolset, and once set the agent gets exactly
213
+ * what it names — nothing more, `[]` meaning no native built-in tools.
214
+ * There is deliberately no exclusion form: an agent that restricts
215
+ * tools states what it keeps, so tools the platform adds later stay
216
+ * off until someone opts in.
217
+ *
218
+ * `"mcp"` is always granted alongside a configured allowlist. It is
219
+ * transport, not capability: this kit's own server tools reach the
220
+ * model over the SDK custom-tool MCP path, so omitting it would
221
+ * silently strand `agent/tools/`. What MCP actually exposes stays
222
+ * governed by the second layer — the tools you author, the connections
223
+ * you declare, and the harness cwd's ambient `.cursor` config (point
224
+ * `local.cwd` outside checkouts whose MCP servers the agent must not
225
+ * see, and `local.sandbox` makes MCP calls fail closed).
226
+ *
227
+ * Names use the SDK's public tool vocabulary (`"shell"`, `"read"`,
228
+ * `"edit"`, `"grep"`, `"glob"`, `"ls"`, `"task"`, ...). `"shell"` is a
229
+ * capability group that also grants shell stdin writes — and is what
230
+ * `execution: "agent"` tool scripts run through, so an allowlist
231
+ * without it strands them (discovery warns). `"task"` gates subagents.
232
+ * Unknown names fail the turn (`ConfigurationError` at `Agent.create`).
233
+ *
234
+ * The allowlist states runtime-agnostic intent, but the SDK cannot
235
+ * enforce tool restrictions on cloud agents yet, and it must never
236
+ * silently not apply: a `runtime: "cloud"` agent that sets it fails at
237
+ * serve start (error diagnostic), and a hybrid agent's per-send cloud
238
+ * sessions are refused (the turn errors) rather than run unrestricted.
239
+ * Not a security boundary against the serve host: like `local.sandbox`,
240
+ * it only shapes what the model can call.
241
+ */
242
+ tools?: ToolName[];
209
243
  /**
210
244
  * How the serve host coordinates event delivery and turn execution. See
211
245
  * {@link AgentServingMode}. Defaults to `"resident"`; opt in to
@@ -506,6 +540,12 @@ export interface HostContext {
506
540
  * process.
507
541
  */
508
542
  files: HostFilesApi;
543
+ /**
544
+ * Author-facing OpenTelemetry metrics and session tags. Always present.
545
+ * Counters and histograms no-op when no meter is running. `setAttributes`
546
+ * still tags the open session when a collector is attached.
547
+ */
548
+ otel: HostOtelApi;
509
549
  }
510
550
 
511
551
  /** Host-facing JSON key-value API attached to {@link HostContext.kv}. */
@@ -706,6 +746,39 @@ export interface HostFilesApi {
706
746
  ): string | undefined;
707
747
  }
708
748
 
749
+ /** Attribute bag accepted by {@link HostOtelApi}. `undefined` values are dropped. */
750
+ export type HostOtelAttributes = Record<
751
+ string,
752
+ string | number | boolean | undefined
753
+ >;
754
+
755
+ /**
756
+ * Author-facing OpenTelemetry surface on {@link HostContext.otel}.
757
+ *
758
+ * Metrics and tags only — no custom spans, and no raw OTel `Meter` /
759
+ * `Tracer`. First-party names (`cursor.token.usage`, …) and join keys
760
+ * (`cursor.conversation.id`, `agent_sdk.agent`, …) are reserved.
761
+ */
762
+ export interface HostOtelApi {
763
+ /** True when a meter is running and `increment` / `record` export. */
764
+ readonly enabled: boolean;
765
+
766
+ /**
767
+ * Merge tags onto this host binding. On a session-bound host they join
768
+ * the open session span and later first-party metrics. Unbound tags
769
+ * stay on this host instance only.
770
+ */
771
+ setAttributes(attributes: HostOtelAttributes): void;
772
+
773
+ /** Add 1 to a counter. */
774
+ increment(name: string, attributes?: HostOtelAttributes): void;
775
+ /** Add `value` to a counter. */
776
+ increment(name: string, value: number, attributes?: HostOtelAttributes): void;
777
+
778
+ /** Record one histogram observation. */
779
+ record(name: string, value: number, attributes?: HostOtelAttributes): void;
780
+ }
781
+
709
782
  /** Runtime context handed to a tool's `execute`. */
710
783
  export interface ToolContext {
711
784
  /** Correlates with `actions.requested` / `action.result` stream events. */
@@ -1838,7 +1911,12 @@ export interface ChannelMeta {
1838
1911
  }
1839
1912
 
1840
1913
  export interface ChannelConfig<TState = JsonValue> {
1841
- /** Route auth chain. Defaults to `[localDevStrict()]`. */
1914
+ /**
1915
+ * Route auth chain. Defaults to `[localDevStrict()]`. Include
1916
+ * `publicEndpoint()` to serve this channel WITHOUT the platform alias
1917
+ * token on Cursor hosting (for webhook senders that cannot attach custom
1918
+ * headers — the handler must verify deliveries itself).
1919
+ */
1842
1920
  auth?: AuthPolicy[];
1843
1921
  routes?: ChannelRoute[];
1844
1922
  /**
@@ -2347,6 +2425,13 @@ export interface ResolvedAgent {
2347
2425
  * (`cwd` is absolute — relative paths resolved against the project root).
2348
2426
  */
2349
2427
  local?: AgentLocalOptions;
2428
+ /**
2429
+ * Built-in harness tool allowlist from {@link AgentConfig.tools}, with
2430
+ * the "mcp" transport grant injected. Named apart from {@link tools}
2431
+ * (the authored `agent/tools/` list). Applied to local turns; cloud
2432
+ * turns fail closed (cloud-runtime agents already fail discovery).
2433
+ */
2434
+ allowedTools?: ToolName[];
2350
2435
  /** Managed-hosting declarations from {@link AgentConfig.hosting}. */
2351
2436
  hosting?: AgentHostingOptions;
2352
2437
  /** Validated turn admission limits from {@link AgentConfig.concurrency}. */
@@ -2503,6 +2588,8 @@ export interface RunnerTurnRequest {
2503
2588
  workspaceDir: string;
2504
2589
  /** Run the local harness sandboxed ({@link AgentLocalOptions.sandbox}). */
2505
2590
  sandbox?: boolean;
2591
+ /** Built-in tool allowlist ({@link AgentConfig.tools}, resolved). */
2592
+ tools?: ToolName[];
2506
2593
  /** Absolute path for runner-owned durable state (conversation store). */
2507
2594
  runnerStateDir: string;
2508
2595
  prompt: string;