@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
package/src/channels.ts CHANGED
@@ -383,3 +383,41 @@ export function allowAll(): AuthPolicy {
383
383
  principalType: "user",
384
384
  });
385
385
  }
386
+
387
+ /**
388
+ * `Symbol.for` so the marker survives duplicated copies of the framework in
389
+ * one process (e.g. a monorepo agent resolving its own `@cursor/july`).
390
+ */
391
+ const PUBLIC_ENDPOINT_MARKER = Symbol.for("agent-serve.public-endpoint");
392
+
393
+ /**
394
+ * Like {@link allowAll}, but ALSO declares the channel publicly reachable on
395
+ * Cursor's managed hosting: `deploy` collects every channel whose auth chain
396
+ * contains this policy and the platform serves that channel's routes
397
+ * (`/v1/channels/<id>/…`) through the stable alias WITHOUT
398
+ * `X-Agent-Alias-Token`.
399
+ *
400
+ * Use it for webhook senders that cannot attach custom headers (Linear,
401
+ * Stripe, …) — **the handler must authenticate deliveries itself**, e.g. by
402
+ * verifying the provider's HMAC signature over the raw body. Everything
403
+ * else (sessions, MCP, playground) stays behind platform auth.
404
+ *
405
+ * Locally this admits every caller, exactly like `allowAll()`.
406
+ */
407
+ export function publicEndpoint(): AuthPolicy {
408
+ const policy: AuthPolicy = () => ({
409
+ authenticator: "public-endpoint",
410
+ principalId: "anonymous",
411
+ principalType: "user",
412
+ });
413
+ return Object.assign(policy, { [PUBLIC_ENDPOINT_MARKER]: true as const });
414
+ }
415
+
416
+ /** True when a policy was created by {@link publicEndpoint}. */
417
+ export function isPublicEndpointPolicy(policy: AuthPolicy): boolean {
418
+ return (
419
+ (policy as { [PUBLIC_ENDPOINT_MARKER]?: unknown })[
420
+ PUBLIC_ENDPOINT_MARKER
421
+ ] === true
422
+ );
423
+ }
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export {
48
48
  PATCH,
49
49
  POST,
50
50
  PUT,
51
+ publicEndpoint,
51
52
  } from "./channels.js";
52
53
  export { defineConnection } from "./connections.js";
53
54
  export { defineHook } from "./hooks.js";
@@ -23,7 +23,7 @@ import { lookupSessionContinuation, runChat } from "./chat-client.js";
23
23
  import { cmdLogin } from "./cli-cursor.js";
24
24
  import { resolveDeployAuth } from "./cli-deploy.js";
25
25
  import { resolveApiKey } from "./cursor/credentials.js";
26
- import { CLI_COMMAND_NAME } from "./distribution.js";
26
+ import { CLI_COMMAND_NAME, PACKAGE_NAME } from "./distribution.js";
27
27
  import {
28
28
  discoverEvals,
29
29
  type EvalRunResult,
@@ -374,7 +374,7 @@ export async function cmdInit(options: AxCliOptions): Promise<number> {
374
374
  const cliOnPath = linked.ok;
375
375
  if (!cliOnPath) {
376
376
  process.stderr.write(
377
- `\nCould not put ${CLI_COMMAND_NAME} on PATH (${linked.stderr.trim() || `exit ${linked.exitCode}`}). Next steps include a PATH export for node_modules/.bin.\n\n`
377
+ `\nCould not put ${CLI_COMMAND_NAME} on PATH (${linked.stderr.trim() || `exit ${linked.exitCode}`}). Next steps use npx ${PACKAGE_NAME} instead.\n\n`
378
378
  );
379
379
  } else {
380
380
  process.stdout.write("\n");
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { basename, resolve } from "node:path";
13
+ import { isPublicEndpointPolicy } from "../channels.js";
13
14
  import {
14
15
  type AgentHostingOptions,
15
16
  isCursorAccountTransport,
@@ -633,7 +634,8 @@ async function deployOne(
633
634
  // truth for egress domains and expected secret names; flags override or
634
635
  // augment it. Directories without an agent project (static placeholder
635
636
  // deploys) simply have no manifest. The same load supplies the
636
- // cursorAccount connection refs for the post-deploy connector auth.
637
+ // cursorAccount connection refs for the post-deploy connector auth and
638
+ // the channels declared public via `publicEndpoint()`.
637
639
  const manifest = isAgent ? await readDeployManifest(dir) : undefined;
638
640
  const hosting = manifest?.hosting;
639
641
  const egressAllowedDomains = [
@@ -642,6 +644,21 @@ async function deployOne(
642
644
  ...(options.allowDomains ?? []),
643
645
  ]),
644
646
  ];
647
+ const publicRoutePaths = (manifest?.publicChannelIds ?? []).map(
648
+ (channelId) => `/v1/channels/${channelId}`
649
+ );
650
+ if (!ctx.json && manifest?.httpChannelPublicEndpoint === true) {
651
+ ctx.err(
652
+ `warning: publicEndpoint() on the built-in HTTP channel has no effect on hosting — only custom channels (/v1/channels/…) can be public; the session API always requires the alias token.\n`
653
+ );
654
+ }
655
+ if (!ctx.json && publicRoutePaths.length > 0) {
656
+ // Security-relevant surface change: say exactly what will be reachable
657
+ // without the alias token before the deploy is accepted.
658
+ ctx.err(
659
+ ` public: ${publicRoutePaths.join(", ")} (served WITHOUT the alias token — these channels authenticate requests themselves)\n`
660
+ );
661
+ }
645
662
 
646
663
  // Today's only source shape: the control plane builds from a git ref (or
647
664
  // serves the static placeholder). See DeploySource for the artifact seam.
@@ -655,6 +672,8 @@ async function deployOne(
655
672
  ? {}
656
673
  : { cursorEventRepos: options.cursorEventsRepos }),
657
674
  ...(egressAllowedDomains.length === 0 ? {} : { egressAllowedDomains }),
675
+ publicRoutePaths:
676
+ publicRoutePaths.length === 0 ? undefined : publicRoutePaths,
658
677
  };
659
678
 
660
679
  let accepted: DeployAccepted;
@@ -716,6 +735,7 @@ async function deployOne(
716
735
  ctx.err(`Deployment ${slug} ended ${detail.status}${errorDetail}\n`);
717
736
  return 1;
718
737
  }
738
+ const deployedPublicRoutes = detail.publicRoutePaths ?? [];
719
739
  ctx.out(
720
740
  `Deployed ${slug} (team ${ctx.teamId})\n` +
721
741
  ` status: running\n` +
@@ -723,7 +743,12 @@ async function deployOne(
723
743
  ` kind: ${detail.deploymentKind}\n` +
724
744
  ` alias: ${aliasUrl}\n` +
725
745
  ` playground: ${aliasUrl}/playground (sign in with the alias token)\n` +
726
- ` open: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
746
+ ` open: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n` +
747
+ // Paste-ready webhook URLs: these subtrees skip the alias token, so
748
+ // the channel handler must verify deliveries (webhook signatures).
749
+ deployedPublicRoutes
750
+ .map((path) => ` public: ${aliasUrl}${path} (no alias token)\n`)
751
+ .join("")
727
752
  );
728
753
 
729
754
  await runPostDeployConnectorAuth(ctx, options, {
@@ -828,11 +853,23 @@ interface DeployManifest {
828
853
  name: string;
829
854
  servers: string[] | undefined;
830
855
  }>;
856
+ /**
857
+ * Channels whose authored auth chain contains `publicEndpoint()`: their
858
+ * `/v1/channels/<id>` subtree is served through the stable alias WITHOUT
859
+ * `X-Agent-Alias-Token` (the channel authenticates deliveries itself).
860
+ */
861
+ publicChannelIds: string[];
862
+ /**
863
+ * `publicEndpoint()` on the built-in HTTP channel — a no-op on hosting
864
+ * (only `/v1/channels/…` subtrees can be public); surfaced as a warning.
865
+ */
866
+ httpChannelPublicEndpoint: boolean;
831
867
  }
832
868
 
833
869
  /**
834
870
  * Read the deploy-relevant bits of the project manifest at `dir` in one
835
- * load: the `hosting` block plus the agent's cursorAccount connections.
871
+ * load: the `hosting` block, the agent's cursorAccount connections, and the
872
+ * channels declared public via `publicEndpoint()`.
836
873
  */
837
874
  async function readDeployManifest(dir: string): Promise<DeployManifest> {
838
875
  const project = await loadAgentProject(dir);
@@ -843,6 +880,14 @@ async function readDeployManifest(dir: string): Promise<DeployManifest> {
843
880
  ? [{ name: connection.name, servers: connection.transport.servers }]
844
881
  : []
845
882
  ),
883
+ publicChannelIds: project.channels
884
+ .filter((channel) =>
885
+ (channel.definition.auth ?? []).some(isPublicEndpointPolicy)
886
+ )
887
+ .map((channel) => channel.id),
888
+ httpChannelPublicEndpoint: (project.httpChannel.auth ?? []).some(
889
+ isPublicEndpointPolicy
890
+ ),
846
891
  };
847
892
  }
848
893
 
@@ -985,6 +1030,16 @@ export async function cmdDeploymentStatus(
985
1030
  ) {
986
1031
  lines.push(` egress: ${detail.egressAllowedDomains.join(", ")}`);
987
1032
  }
1033
+ if (
1034
+ detail.publicRoutePaths !== undefined &&
1035
+ detail.publicRoutePaths.length > 0
1036
+ ) {
1037
+ // Served through the alias WITHOUT X-Agent-Alias-Token (the channel
1038
+ // authenticates deliveries itself).
1039
+ for (const path of detail.publicRoutePaths) {
1040
+ lines.push(` public: ${aliasUrl}${path} (no alias token)`);
1041
+ }
1042
+ }
988
1043
  if (detail.secretNames !== undefined && detail.secretNames.length > 0) {
989
1044
  lines.push(` secrets: ${detail.secretNames.join(", ")}`);
990
1045
  }
@@ -69,6 +69,12 @@ export type DeploySource = {
69
69
  * deploys — the backend 400s when set on a static deploy.
70
70
  */
71
71
  egressAllowedDomains?: string[];
72
+ /**
73
+ * `/v1/channels/…` subtrees the stable alias serves WITHOUT
74
+ * `X-Agent-Alias-Token` (channels authored with `publicEndpoint()`).
75
+ * Only valid on repo-backed deploys.
76
+ */
77
+ publicRoutePaths?: string[];
72
78
  };
73
79
 
74
80
  const SLUG_PATTERN = /^[a-z0-9_-]{1,64}$/;
@@ -158,6 +164,8 @@ export interface Deployment {
158
164
  cursorEventRepos?: string[];
159
165
  /** Normalized egress allowlist for the engine pod. */
160
166
  egressAllowedDomains?: string[];
167
+ /** Channel subtrees the alias serves without `X-Agent-Alias-Token`. */
168
+ publicRoutePaths?: string[];
161
169
  /** Service-account id (`sa_…`); null on legacy rows. */
162
170
  serviceAccountId?: string | number | null;
163
171
  /**
@@ -297,6 +305,7 @@ const deploymentSchema = z.object({
297
305
  lastReconciledAt: z.string().nullable().optional(),
298
306
  cursorEventRepos: z.array(z.string()).optional(),
299
307
  egressAllowedDomains: z.array(z.string()).optional(),
308
+ publicRoutePaths: z.array(z.string()).optional(),
300
309
  // String ids (`sa_…`) in prod; older mocks/fixtures used numbers. Null on
301
310
  // legacy rows that predate service-account-backed deploys.
302
311
  serviceAccountId: z.union([z.string(), z.number()]).nullable().optional(),
@@ -441,6 +450,8 @@ export class AgentServeDeployClient {
441
450
  ...(args.source.egressAllowedDomains === undefined
442
451
  ? {}
443
452
  : { egressAllowedDomains: args.source.egressAllowedDomains }),
453
+ // JSON.stringify drops undefined keys.
454
+ publicRoutePaths: args.source.publicRoutePaths,
444
455
  };
445
456
  const raw = await this.request({
446
457
  method: "POST",
@@ -376,6 +376,14 @@ async function loadAgent(
376
376
  message: "builtinTools is not supported on subagents; ignoring.",
377
377
  });
378
378
  }
379
+ if (config?.tools !== undefined) {
380
+ ctx.diagnostics.push({
381
+ severity: "warning",
382
+ path: displayPath(ctx, join(agentDir, "agent.ts")),
383
+ message:
384
+ "tools is not supported on subagents (they keep the harness task-tool toolset); ignoring.",
385
+ });
386
+ }
379
387
  const artifactsPath = await findModuleFile(agentDir, "artifacts");
380
388
  if (artifactsPath !== undefined) {
381
389
  ctx.diagnostics.push({
@@ -415,6 +423,46 @@ async function loadAgent(
415
423
  const localCwd = resolveAgentCwd(ctx, config?.local?.cwd);
416
424
  const localWorkspaceDir = resolveAgentCwd(ctx, config?.local?.workspaceDir);
417
425
  const localSandbox = config?.local?.sandbox === true;
426
+ // Built-in tool allowlist (top-level `tools`). `[]` is meaningful (no
427
+ // native built-in tools), so presence is keyed on the field, not on
428
+ // emptiness. "mcp" is always granted alongside a configured allowlist:
429
+ // it is transport, not capability — this kit's own server tools ride the
430
+ // SDK custom-tool MCP path, so omitting it silently breaks
431
+ // `agent/tools/`, and what MCP actually exposes stays controlled by the
432
+ // second layer (authored tools, mcp-connections, the harness cwd's
433
+ // ambient config).
434
+ const authoredAllowlist = config?.tools;
435
+ const allowedTools =
436
+ authoredAllowlist === undefined || authoredAllowlist.includes("mcp")
437
+ ? authoredAllowlist
438
+ : [...authoredAllowlist, "mcp" as const];
439
+ // The allowlist states runtime-agnostic intent, but the SDK cannot
440
+ // enforce tool restrictions on cloud agents yet — a cloud-runtime agent
441
+ // that declares one must fail at serve start, never run unrestricted.
442
+ // (Hybrid per-send cloud sessions fail closed in the session engine.)
443
+ if (allowedTools !== undefined && runtime === "cloud") {
444
+ ctx.diagnostics.push({
445
+ severity: "error",
446
+ path: displayPath(ctx, join(agentDir, "agent.ts")),
447
+ message:
448
+ 'tools cannot be enforced on runtime: "cloud" yet (the Cursor SDK rejects tool restrictions on cloud agents). Remove `tools` or run the agent on the local runtime.',
449
+ });
450
+ }
451
+ // `execution: "agent"` tool scripts run through the shell tool. Shell is
452
+ // a real capability, so it is never injected — but an allowlist that
453
+ // strands authored scripts deserves a loud diagnostic, not silence.
454
+ if (
455
+ allowedTools !== undefined &&
456
+ !allowedTools.includes("shell") &&
457
+ tools.some((tool) => tool.execution === "agent")
458
+ ) {
459
+ ctx.diagnostics.push({
460
+ severity: "warning",
461
+ path: displayPath(ctx, join(agentDir, "agent.ts")),
462
+ message:
463
+ '`tools` omits "shell" but this agent has execution: "agent" tools, which run as shell scripts; the model will not be able to run them.',
464
+ });
465
+ }
418
466
  if (localCwd !== undefined && localWorkspaceDir !== undefined) {
419
467
  ctx.diagnostics.push({
420
468
  severity: "warning",
@@ -455,6 +503,7 @@ async function loadAgent(
455
503
  runtime,
456
504
  cloud,
457
505
  local,
506
+ ...(allowedTools === undefined ? {} : { allowedTools }),
458
507
  hosting,
459
508
  concurrency,
460
509
  serving,
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Author-facing OpenTelemetry surface for {@link HostContext.otel}.
3
+ *
4
+ * Always present on `ctx.host`. Counters and histograms no-op when no meter
5
+ * is running. `setAttributes` still tags the open session when a collector
6
+ * is attached. `forSession` is engine-only so a tool cannot retarget another
7
+ * conversation's tags.
8
+ */
9
+
10
+ import type { Counter, Histogram, Meter } from "@opentelemetry/api";
11
+ import type { HostOtelApi, HostOtelAttributes } from "../types.js";
12
+ import type { AttrValue } from "./otel/attrs.js";
13
+ import { attrs } from "./otel/attrs.js";
14
+ import type { OtelCollector } from "./otel/collector.js";
15
+
16
+ const METRIC_NAME = /^[A-Za-z][A-Za-z0-9_./-]*$/;
17
+
18
+ const RESERVED_METRICS = new Set([
19
+ "cursor.token.usage",
20
+ "cursor.tool.calls",
21
+ "cursor.cost.usage",
22
+ "agent_sdk.session.count",
23
+ "agent_sdk.turn.count",
24
+ "agent_sdk.subagent.count",
25
+ "agent_sdk.http.duration",
26
+ ]);
27
+
28
+ const RESERVED_ATTRIBUTES = new Set([
29
+ "cursor.conversation.id",
30
+ "agent_sdk.agent",
31
+ "agent_sdk.turn_id",
32
+ "agent_sdk.framework",
33
+ ]);
34
+
35
+ export interface CreateHostOtelApiOptions {
36
+ agentName: string;
37
+ meter?: Meter;
38
+ collector?: OtelCollector;
39
+ /** Session bound for join tags; prefer {@link HostOtelSessionApi.forSession}. */
40
+ sessionId?: string;
41
+ }
42
+
43
+ /** Engine-only binding. Not part of the public {@link HostOtelApi}. */
44
+ export interface HostOtelSessionApi extends HostOtelApi {
45
+ forSession(sessionId: string): HostOtelSessionApi;
46
+ }
47
+
48
+ interface CachedInstrument {
49
+ kind: "counter" | "histogram";
50
+ instrument: Counter | Histogram;
51
+ }
52
+
53
+ interface SharedState {
54
+ agentName: string;
55
+ meter: Meter | undefined;
56
+ collector: OtelCollector | undefined;
57
+ instruments: Map<string, CachedInstrument>;
58
+ /** Used when no collector is attached so bound instances still share tags. */
59
+ sessionTags: Map<string, Record<string, AttrValue>>;
60
+ }
61
+
62
+ interface BoundState {
63
+ shared: SharedState;
64
+ sessionId: string | undefined;
65
+ localTags: Record<string, AttrValue>;
66
+ }
67
+
68
+ /** Build the host OpenTelemetry API for one agent process. */
69
+ export function createHostOtelApi(
70
+ options: CreateHostOtelApiOptions
71
+ ): HostOtelSessionApi {
72
+ return createBoundHostOtelApi({
73
+ shared: {
74
+ agentName: options.agentName,
75
+ meter: options.meter,
76
+ collector: options.collector,
77
+ instruments: new Map(),
78
+ sessionTags: new Map(),
79
+ },
80
+ sessionId: options.sessionId,
81
+ localTags: {},
82
+ });
83
+ }
84
+
85
+ function createBoundHostOtelApi(state: BoundState): HostOtelSessionApi {
86
+ const api: HostOtelSessionApi = {
87
+ get enabled(): boolean {
88
+ return state.shared.meter !== undefined;
89
+ },
90
+
91
+ setAttributes(attributes) {
92
+ const cleaned = validateAttributes(attributes);
93
+ if (state.sessionId === undefined) {
94
+ Object.assign(state.localTags, cleaned);
95
+ return;
96
+ }
97
+ mergeSessionTags(state, state.sessionId, cleaned);
98
+ state.shared.collector?.setSessionAttributes(state.sessionId, cleaned);
99
+ },
100
+
101
+ increment(
102
+ name: string,
103
+ valueOrAttributes?: number | HostOtelAttributes,
104
+ maybeAttributes?: HostOtelAttributes
105
+ ) {
106
+ const value =
107
+ typeof valueOrAttributes === "number" ? valueOrAttributes : 1;
108
+ const attributes =
109
+ typeof valueOrAttributes === "number"
110
+ ? maybeAttributes
111
+ : valueOrAttributes;
112
+ counter(state.shared, name)?.add(
113
+ value,
114
+ emitAttributes(state, attributes)
115
+ );
116
+ },
117
+
118
+ record(name, value, attributes) {
119
+ histogram(state.shared, name)?.record(
120
+ value,
121
+ emitAttributes(state, attributes)
122
+ );
123
+ },
124
+
125
+ forSession(sessionId) {
126
+ const trimmed = sessionId.trim();
127
+ if (trimmed === "") {
128
+ throw new Error("host.otel.forSession requires a non-empty session id");
129
+ }
130
+ return createBoundHostOtelApi({
131
+ shared: state.shared,
132
+ sessionId: trimmed,
133
+ localTags: {},
134
+ });
135
+ },
136
+ };
137
+ return api;
138
+ }
139
+
140
+ function mergeSessionTags(
141
+ state: BoundState,
142
+ sessionId: string,
143
+ incoming: Record<string, AttrValue>
144
+ ): void {
145
+ const bag = state.shared.sessionTags.get(sessionId) ?? {};
146
+ Object.assign(bag, incoming);
147
+ state.shared.sessionTags.set(sessionId, bag);
148
+ }
149
+
150
+ function emitAttributes(
151
+ state: BoundState,
152
+ callSite: HostOtelAttributes | undefined
153
+ ): Record<string, AttrValue> {
154
+ return attrs({
155
+ ...sessionBag(state),
156
+ ...validateAttributes(callSite),
157
+ "agent_sdk.agent": state.shared.agentName,
158
+ ...(state.sessionId === undefined
159
+ ? {}
160
+ : { "cursor.conversation.id": state.sessionId }),
161
+ });
162
+ }
163
+
164
+ function sessionBag(state: BoundState): Record<string, AttrValue> {
165
+ if (state.sessionId === undefined) {
166
+ return state.localTags;
167
+ }
168
+ const fromCollector = state.shared.collector?.getSessionAttributes(
169
+ state.sessionId
170
+ );
171
+ const fromShared = state.shared.sessionTags.get(state.sessionId) ?? {};
172
+ return {
173
+ ...fromShared,
174
+ ...(fromCollector ?? {}),
175
+ ...state.localTags,
176
+ };
177
+ }
178
+
179
+ function counter(shared: SharedState, name: string): Counter | undefined {
180
+ validateMetricName(name);
181
+ if (shared.meter === undefined) {
182
+ return undefined;
183
+ }
184
+ const existing = shared.instruments.get(name);
185
+ if (existing !== undefined) {
186
+ if (existing.kind !== "counter") {
187
+ throw new Error(`host.otel metric "${name}" is already a histogram`);
188
+ }
189
+ return existing.instrument as Counter;
190
+ }
191
+ const instrument = shared.meter.createCounter(name);
192
+ shared.instruments.set(name, { kind: "counter", instrument });
193
+ return instrument;
194
+ }
195
+
196
+ function histogram(shared: SharedState, name: string): Histogram | undefined {
197
+ validateMetricName(name);
198
+ if (shared.meter === undefined) {
199
+ return undefined;
200
+ }
201
+ const existing = shared.instruments.get(name);
202
+ if (existing !== undefined) {
203
+ if (existing.kind !== "histogram") {
204
+ throw new Error(`host.otel metric "${name}" is already a counter`);
205
+ }
206
+ return existing.instrument as Histogram;
207
+ }
208
+ const instrument = shared.meter.createHistogram(name);
209
+ shared.instruments.set(name, { kind: "histogram", instrument });
210
+ return instrument;
211
+ }
212
+
213
+ function validateMetricName(name: string): void {
214
+ if (!METRIC_NAME.test(name)) {
215
+ throw new Error(
216
+ `host.otel metric name "${name}" must match [A-Za-z][A-Za-z0-9_./-]*`
217
+ );
218
+ }
219
+ if (RESERVED_METRICS.has(name)) {
220
+ throw new Error(`host.otel metric name "${name}" is reserved`);
221
+ }
222
+ }
223
+
224
+ function validateAttributes(
225
+ attributes: HostOtelAttributes | undefined
226
+ ): Record<string, AttrValue> {
227
+ if (attributes === undefined) {
228
+ return {};
229
+ }
230
+ const out: Record<string, AttrValue> = {};
231
+ for (const [key, value] of Object.entries(attributes)) {
232
+ if (value === undefined) {
233
+ continue;
234
+ }
235
+ if (RESERVED_ATTRIBUTES.has(key)) {
236
+ throw new Error(
237
+ `host.otel attribute "${key}" is reserved; the runtime always sets it`
238
+ );
239
+ }
240
+ if (
241
+ typeof value !== "string" &&
242
+ typeof value !== "number" &&
243
+ typeof value !== "boolean"
244
+ ) {
245
+ throw new Error(
246
+ `host.otel attribute "${key}" must be a string, number, or boolean`
247
+ );
248
+ }
249
+ out[key] = value;
250
+ }
251
+ return out;
252
+ }
@@ -699,20 +699,20 @@ function initNextStepEntries(options: InitNextStepsOptions): Array<{
699
699
  if (cd !== undefined) {
700
700
  steps.push({ command: cd });
701
701
  }
702
- if (!options.cliOnPath) {
703
- steps.push({
704
- command: 'export PATH="$PWD/node_modules/.bin:$PATH"',
705
- note: "use the project-local agent-sdk bin",
706
- });
707
- }
708
702
  if (!options.signedIn) {
703
+ const login = options.cliOnPath
704
+ ? `${CLI_COMMAND_NAME} login`
705
+ : `npx ${PACKAGE_NAME} login`;
709
706
  steps.push({
710
- command: `${CLI_COMMAND_NAME} login`,
707
+ command: login,
711
708
  note: "connect your Cursor account (powers model turns)",
712
709
  });
713
710
  }
711
+ const dev = options.cliOnPath
712
+ ? `${CLI_COMMAND_NAME} dev`
713
+ : `npx ${PACKAGE_NAME} dev`;
714
714
  steps.push({
715
- command: `${CLI_COMMAND_NAME} dev`,
715
+ command: dev,
716
716
  note: "start the local playground",
717
717
  });
718
718
  return steps;
@@ -739,9 +739,9 @@ export function printInitGuide(): void {
739
739
  process.stdout.write(`${CLI_COMMAND_NAME} — coding agent setup
740
740
 
741
741
  Scaffold a project, then open the playground:
742
- npx ${PACKAGE_NAME} init ./my-agent # installs deps + puts agent-sdk on PATH
742
+ npx ${PACKAGE_NAME} init ./my-agent # installs deps + links agent-sdk onto PATH
743
743
  npx ${PACKAGE_NAME} init ./my-triage --template triage # start from a template (agent-sdk templates: triage)
744
- cd ./my-agent && ${CLI_COMMAND_NAME} dev
744
+ cd ./my-agent && agent-sdk dev # or: npx ${PACKAGE_NAME} dev
745
745
 
746
746
  Recommended loop once the project exists (from the project directory):
747
747
  1. Edit agent/ + tools under agent/tools/