@cursor/july 0.1.24 → 0.1.26

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 (205) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/channels/github/instrument.d.ts +20 -0
  3. package/dist/channels/github/instrument.d.ts.map +1 -0
  4. package/dist/docs/404.html +1 -1
  5. package/dist/docs/ab.html +2 -2
  6. package/dist/docs/assets/{app.CvSGaAxk.js → app.DYcC9FY-.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.J1jJbCvs.js → VPLocalSearchBox.o4N_knTV.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.C7vfzr7h.js → theme.DQ-njyo0.js} +2 -2
  10. package/dist/docs/building-with-agents.html +2 -2
  11. package/dist/docs/concepts.html +2 -2
  12. package/dist/docs/deployment.html +2 -2
  13. package/dist/docs/evals.html +2 -2
  14. package/dist/docs/example-agents/approval-buddy.html +2 -2
  15. package/dist/docs/example-agents/benny.html +2 -2
  16. package/dist/docs/example-agents/bugbot.html +2 -2
  17. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  18. package/dist/docs/example-agents/codeowners-review.html +2 -2
  19. package/dist/docs/example-agents/concierge.html +2 -2
  20. package/dist/docs/example-agents/fsd.html +2 -2
  21. package/dist/docs/example-agents/index.html +2 -2
  22. package/dist/docs/example-agents/knowledge-base.html +2 -2
  23. package/dist/docs/example-agents/oncall.html +2 -2
  24. package/dist/docs/example-agents/security-reviewer.html +2 -2
  25. package/dist/docs/example-agents/slack-agent.html +2 -2
  26. package/dist/docs/example-agents/weather-agent.html +2 -2
  27. package/dist/docs/guides/agent-to-agent.html +2 -2
  28. package/dist/docs/guides/cloud-runtime.html +2 -2
  29. package/dist/docs/guides/github.html +2 -2
  30. package/dist/docs/guides/human-in-the-loop.html +2 -2
  31. package/dist/docs/guides/mcp-oauth.html +2 -2
  32. package/dist/docs/guides/slack.html +2 -2
  33. package/dist/docs/guides/webhooks.html +2 -2
  34. package/dist/docs/hillclimbing.html +2 -2
  35. package/dist/docs/index.html +2 -2
  36. package/dist/docs/quickstart.html +2 -2
  37. package/dist/docs/reference/agent-config.html +2 -2
  38. package/dist/docs/reference/channels.html +2 -2
  39. package/dist/docs/reference/cli.html +2 -2
  40. package/dist/docs/reference/connections.html +2 -2
  41. package/dist/docs/reference/hooks.html +2 -2
  42. package/dist/docs/reference/http-api.html +2 -2
  43. package/dist/docs/reference/instructions.html +2 -2
  44. package/dist/docs/reference/playground.html +2 -2
  45. package/dist/docs/reference/project-layout.html +2 -2
  46. package/dist/docs/reference/prompt.html +2 -2
  47. package/dist/docs/reference/schedules.html +2 -2
  48. package/dist/docs/reference/sessions.html +2 -2
  49. package/dist/docs/reference/skills.html +2 -2
  50. package/dist/docs/reference/subagents.html +2 -2
  51. package/dist/docs/reference/tools.html +2 -2
  52. package/dist/docs/scaffolding-agents.html +2 -2
  53. package/dist/docs/storage.html +2 -2
  54. package/dist/docs/troubleshooting.html +2 -2
  55. package/dist/internal/json-dir-store.d.ts +32 -0
  56. package/dist/internal/json-dir-store.d.ts.map +1 -0
  57. package/dist/internal/json-dir-store.js +100 -0
  58. package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
  59. package/dist/playground/assets/{index-DTG9OsPV.js → index-DqXdAFGa.js} +41 -41
  60. package/dist/playground/index.html +2 -2
  61. package/package.json +31 -24
  62. package/src/artifacts.ts +78 -0
  63. package/src/bin/agent-serve.version.test.ts +64 -0
  64. package/src/channels/github/api.test.ts +64 -0
  65. package/src/channels/github/auth.test.ts +105 -0
  66. package/src/channels/github/cursor-account.test.ts +204 -0
  67. package/src/channels/github/forward.test.ts +457 -0
  68. package/src/channels/github/github-channel.ts +19 -2
  69. package/src/channels/github/github.test.ts +985 -0
  70. package/src/channels/github/replay.test.ts +179 -0
  71. package/src/channels/github/types.ts +6 -0
  72. package/src/channels/slack/api.post-message.test.ts +148 -0
  73. package/src/channels/slack/api.ts +3 -1
  74. package/src/channels/slack/approvals.test.ts +328 -0
  75. package/src/channels/slack/block-actions.test.ts +469 -0
  76. package/src/channels/slack/bot-mentions.test.ts +267 -0
  77. package/src/channels/slack/channel-watch.test.ts +363 -0
  78. package/src/channels/slack/cursor-account.test.ts +301 -0
  79. package/src/channels/slack/cursor-account.ts +3 -1
  80. package/src/channels/slack/defaults.final-post.test.ts +182 -0
  81. package/src/channels/slack/dispatch.test.ts +875 -0
  82. package/src/channels/slack/dispatch.ts +12 -1
  83. package/src/channels/slack/eval-directive.test.ts +273 -0
  84. package/src/channels/slack/interactive.ts +3 -0
  85. package/src/channels/slack/message-body.test.ts +54 -0
  86. package/src/channels/slack/nudge-store.test.ts +143 -0
  87. package/src/channels/slack/slack-channel.ts +6 -0
  88. package/src/channels/slack/slack.test.ts +391 -0
  89. package/src/channels/slack/stop.test.ts +23 -0
  90. package/src/channels/slack/thread-context.test.ts +202 -0
  91. package/src/channels/slack/types.ts +6 -0
  92. package/src/evals/assertions.test.ts +617 -0
  93. package/src/evals/assertions.ts +37 -1
  94. package/src/evals/expect.test.ts +144 -0
  95. package/src/evals/judge.test.ts +181 -0
  96. package/src/evals/loaders.test.ts +132 -0
  97. package/src/evals/matchers.test.ts +95 -0
  98. package/src/evals/reporters.test.ts +303 -0
  99. package/src/evals/run-facts.test.ts +259 -0
  100. package/src/evals.ts +1 -0
  101. package/src/index.ts +1 -0
  102. package/src/internal/ab-snapshot.test.ts +325 -0
  103. package/src/internal/approval-gate.test.ts +49 -0
  104. package/src/internal/approvals.integration.test.ts +383 -0
  105. package/src/internal/artifacts-store.test.ts +283 -0
  106. package/src/internal/artifacts-store.ts +290 -0
  107. package/src/internal/authored-loaders.test.ts +31 -0
  108. package/src/internal/builtin-tools/artifacts.test.ts +247 -0
  109. package/src/internal/builtin-tools/artifacts.ts +74 -0
  110. package/src/internal/builtin-tools/index.ts +21 -0
  111. package/src/internal/builtin-tools/reminders.test.ts +201 -0
  112. package/src/internal/channel-route-schema.test.ts +294 -0
  113. package/src/internal/chat-attach.test.ts +262 -0
  114. package/src/internal/cli-deploy.test.ts +1991 -0
  115. package/src/internal/cli-docs.test.ts +161 -0
  116. package/src/internal/cli-mcp.test.ts +789 -0
  117. package/src/internal/cli-skills.test.ts +133 -0
  118. package/src/internal/cli-slack.test.ts +1654 -0
  119. package/src/internal/cli-slack.ts +13 -4
  120. package/src/internal/cloud-merge.test.ts +74 -0
  121. package/src/internal/cron.test.ts +22 -0
  122. package/src/internal/cursor/account-mcp.test.ts +807 -0
  123. package/src/internal/cursor/backend-client.test.ts +591 -0
  124. package/src/internal/cursor/credentials.test.ts +351 -0
  125. package/src/internal/cursor/github-credentials.test.ts +136 -0
  126. package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
  127. package/src/internal/cursor-account.integration.test.ts +441 -0
  128. package/src/internal/cursor-event-relay.test.ts +746 -0
  129. package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
  130. package/src/internal/cursor-slack-relay.test.ts +525 -0
  131. package/src/internal/deploy-source.test.ts +111 -0
  132. package/src/internal/discovery.artifact-tool.test.ts +136 -0
  133. package/src/internal/discovery.artifacts.test.ts +119 -0
  134. package/src/internal/discovery.builtin-tools.test.ts +94 -0
  135. package/src/internal/discovery.concurrency.test.ts +60 -0
  136. package/src/internal/discovery.cursor-account.test.ts +133 -0
  137. package/src/internal/discovery.cwd.test.ts +83 -0
  138. package/src/internal/discovery.hosting.test.ts +80 -0
  139. package/src/internal/discovery.identity.test.ts +44 -0
  140. package/src/internal/discovery.ts +102 -1
  141. package/src/internal/distribution.ts +1 -0
  142. package/src/internal/docs-site.test.ts +66 -0
  143. package/src/internal/duration.test.ts +29 -0
  144. package/src/internal/eval-judge-model.test.ts +187 -0
  145. package/src/internal/eval-run-store.cancel.test.ts +142 -0
  146. package/src/internal/eval-run-store.storage.test.ts +211 -0
  147. package/src/internal/eval-runner.http.test.ts +403 -0
  148. package/src/internal/eval-runner.run.test.ts +928 -0
  149. package/src/internal/evals-client.test.ts +307 -0
  150. package/src/internal/event-mapper.test.ts +243 -0
  151. package/src/internal/github-fanout.test.ts +213 -0
  152. package/src/internal/handleAgentServeTrigger.test.ts +185 -0
  153. package/src/internal/host-kv.test.ts +82 -0
  154. package/src/internal/host-platforms.test.ts +126 -0
  155. package/src/internal/http-channel.test.ts +402 -0
  156. package/src/internal/init-project.test.ts +270 -0
  157. package/src/internal/install-cursor-skills.test.ts +262 -0
  158. package/src/internal/local-env.test.ts +120 -0
  159. package/src/internal/log-ring.test.ts +31 -0
  160. package/src/internal/logs-client.test.ts +350 -0
  161. package/src/internal/mcp-endpoint.test.ts +436 -0
  162. package/src/internal/mcp-host.test.ts +298 -0
  163. package/src/internal/mcp-oauth.test.ts +148 -0
  164. package/src/internal/net.test.ts +17 -0
  165. package/src/internal/peer-connections.test.ts +128 -0
  166. package/src/internal/peer-mcp.integration.test.ts +289 -0
  167. package/src/internal/playground/toolchain.test.ts +53 -0
  168. package/src/internal/playground-cli.test.ts +187 -0
  169. package/src/internal/playground-proxy.test.ts +376 -0
  170. package/src/internal/prompt-context.integration.test.ts +232 -0
  171. package/src/internal/prompt-context.test.ts +127 -0
  172. package/src/internal/reminder-runner.test.ts +432 -0
  173. package/src/internal/reminder-runner.ts +1 -0
  174. package/src/internal/reminder-store.test.ts +53 -0
  175. package/src/internal/request-headers.test.ts +27 -0
  176. package/src/internal/resolve-prod-target.test.ts +787 -0
  177. package/src/internal/resolved-connections.test.ts +295 -0
  178. package/src/internal/router.test.ts +57 -0
  179. package/src/internal/schedule-runner.ts +1 -0
  180. package/src/internal/sdk-runner.test.ts +290 -0
  181. package/src/internal/server.artifacts.test.ts +334 -0
  182. package/src/internal/server.ts +45 -0
  183. package/src/internal/session-engine.artifacts.test.ts +293 -0
  184. package/src/internal/session-engine.coalesce.test.ts +169 -0
  185. package/src/internal/session-engine.concurrency.test.ts +250 -0
  186. package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
  187. package/src/internal/session-engine.interrupt.test.ts +577 -0
  188. package/src/internal/session-engine.storage.test.ts +547 -0
  189. package/src/internal/session-engine.ts +129 -1
  190. package/src/internal/session-urls.test.ts +28 -0
  191. package/src/internal/sessions-client.test.ts +518 -0
  192. package/src/internal/storage-coordinator.test.ts +517 -0
  193. package/src/internal/storage-coordinator.ts +62 -0
  194. package/src/internal/tool-call.test.ts +458 -0
  195. package/src/internal/tool-result.test.ts +52 -0
  196. package/src/internal/trajectory.approvals.test.ts +83 -0
  197. package/src/internal/trajectory.subagents.test.ts +198 -0
  198. package/src/internal/turn-governor.test.ts +137 -0
  199. package/src/internal/update-check.test.ts +485 -0
  200. package/src/internal/workspace.test.ts +207 -0
  201. package/src/storage-backends/cursor-hosted.test.ts +121 -0
  202. package/src/storage.ts +6 -0
  203. package/src/types.ts +152 -2
  204. package/dist/docs/assets/chunks/@localSearchIndexroot.CL2Y0Zmh.js +0 -1
  205. package/dist/playground/assets/index-CidizGZv.css +0 -1
@@ -0,0 +1,80 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { defineAgent } from "../index.js";
6
+ import type { AgentHostingOptions } from "../types.js";
7
+ import { loadAgentProject, projectInfo } from "./discovery.js";
8
+
9
+ const cleanups: Array<() => Promise<void>> = [];
10
+
11
+ afterEach(async () => {
12
+ while (cleanups.length > 0) {
13
+ await cleanups.pop()?.();
14
+ }
15
+ });
16
+
17
+ async function loadWithHosting(hosting: AgentHostingOptions) {
18
+ const rootDir = await mkdtemp(join(tmpdir(), "agent-serve-hosting-disc-"));
19
+ cleanups.push(() => rm(rootDir, { recursive: true, force: true }));
20
+ const agentDir = join(rootDir, "agent");
21
+ await mkdir(agentDir, { recursive: true });
22
+ await writeFile(join(agentDir, "agent.ts"), "export default {};\n");
23
+ await writeFile(join(agentDir, "instructions.md"), "hi\n");
24
+ return loadAgentProject(rootDir, {
25
+ importModule: async () => ({
26
+ default: defineAgent({ model: "composer-2.5", hosting }),
27
+ }),
28
+ });
29
+ }
30
+
31
+ describe("loadAgentProject hosting", () => {
32
+ it("resolves the hosting block and surfaces it in projectInfo", async () => {
33
+ const project = await loadWithHosting({
34
+ egressDomains: ["api.example.com", "*.internal.example.com"],
35
+ secretNames: ["OPENAI_API_KEY"],
36
+ });
37
+ expect(project.diagnostics).toEqual([]);
38
+ expect(project.agent.hosting).toEqual({
39
+ egressDomains: ["api.example.com", "*.internal.example.com"],
40
+ secretNames: ["OPENAI_API_KEY"],
41
+ });
42
+ expect(projectInfo(project).hosting).toEqual(project.agent.hosting);
43
+ });
44
+
45
+ it("emits warning diagnostics (not errors) for grammar violations", async () => {
46
+ const project = await loadWithHosting({
47
+ // Uppercase, single-label, numeric TLD, double wildcard: all invalid.
48
+ egressDomains: ["API.example.com", "localhost", "a.b.123", "*.*.x.com"],
49
+ secretNames: ["lower_case", "CURSOR_API_KEY", "A".repeat(65)],
50
+ });
51
+ const warnings = project.diagnostics.filter(
52
+ (d) => d.severity === "warning"
53
+ );
54
+ expect(project.diagnostics).toHaveLength(warnings.length);
55
+ const messages = warnings.map((d) => d.message).join("\n");
56
+ expect(messages).toContain('Invalid egress domain "API.example.com"');
57
+ expect(messages).toContain('Invalid egress domain "localhost"');
58
+ expect(messages).toContain('Invalid egress domain "a.b.123"');
59
+ expect(messages).toContain('Invalid egress domain "*.*.x.com"');
60
+ expect(messages).toContain('Invalid secret name "lower_case"');
61
+ expect(messages).toContain("platform-managed");
62
+ expect(messages).toContain("too long");
63
+ // Invalid entries are still kept — the backend stays authoritative.
64
+ expect(project.agent.hosting?.egressDomains).toHaveLength(4);
65
+ });
66
+
67
+ it("warns when egressDomains exceeds the deploy cap", async () => {
68
+ const project = await loadWithHosting({
69
+ egressDomains: Array.from(
70
+ { length: 21 },
71
+ (_, i) => `svc${i}.example.com`
72
+ ),
73
+ });
74
+ expect(
75
+ project.diagnostics.some(
76
+ (d) => d.severity === "warning" && d.message.includes("capped at 20")
77
+ )
78
+ ).toBe(true);
79
+ });
80
+ });
@@ -0,0 +1,44 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { defineAgent } from "../index.js";
6
+ import { loadAgentProject } from "./discovery.js";
7
+
8
+ const cleanups: Array<() => Promise<void>> = [];
9
+
10
+ afterEach(async () => {
11
+ while (cleanups.length > 0) {
12
+ await cleanups.pop()?.();
13
+ }
14
+ });
15
+
16
+ describe("loadAgentProject identity preamble", () => {
17
+ it("prepends the resolved name into instructions", async () => {
18
+ const rootDir = await mkdtemp(join(tmpdir(), "agent-serve-identity-"));
19
+ cleanups.push(() => rm(rootDir, { recursive: true, force: true }));
20
+ const agentDir = join(rootDir, "agent");
21
+ await mkdir(agentDir, { recursive: true });
22
+ await writeFile(join(agentDir, "agent.ts"), "export default {};\n");
23
+ await writeFile(
24
+ join(agentDir, "instructions.md"),
25
+ "Be brief and specific.\n"
26
+ );
27
+
28
+ const project = await loadAgentProject(rootDir, {
29
+ importModule: async () => ({
30
+ default: defineAgent({
31
+ name: "Benny",
32
+ model: "composer-2.5",
33
+ }),
34
+ }),
35
+ });
36
+
37
+ expect(project.agent.instructions).toBe(
38
+ [
39
+ "You are Benny. That is your name; use it when people ask who you are.",
40
+ "Be brief and specific.",
41
+ ].join("\n\n")
42
+ );
43
+ });
44
+ });
@@ -18,6 +18,7 @@ import {
18
18
  } from "node:path";
19
19
  import { pathToFileURL } from "node:url";
20
20
  import { type ABConfigFile, resolveABMaxPlaygroundSessions } from "../ab.js";
21
+ import { resolveArtifactsMax } from "../artifacts.js";
21
22
  import { httpChannel } from "../channels.js";
22
23
  import type { StorageDefinition } from "../storage.js";
23
24
  import {
@@ -27,6 +28,7 @@ import {
27
28
  type AgentProject,
28
29
  type AgentProjectInfo,
29
30
  type AgentRuntime,
31
+ type ArtifactsDefinition,
30
32
  type BuiltinToolsConfig,
31
33
  type ChannelDefinition,
32
34
  type ConnectionDefinition,
@@ -58,7 +60,11 @@ import {
58
60
  import { registerAuthoredModuleLoaders } from "./authored-loaders.js";
59
61
  import { normalizePositiveInt } from "./bounded-int.js";
60
62
  import { getDefinitionKind } from "./brand.js";
61
- import { builtinTools } from "./builtin-tools/index.js";
63
+ import {
64
+ artifactBuiltinTool,
65
+ builtinTools,
66
+ TAG_ARTIFACT_TOOL_NAME,
67
+ } from "./builtin-tools/index.js";
62
68
  import { isValidCron } from "./cron.js";
63
69
  import { parseFrontmatter } from "./frontmatter.js";
64
70
  import {
@@ -280,6 +286,8 @@ export async function loadAgentProject(
280
286
  const abs = await loadABs(ctx, agentDir);
281
287
  const abConfig = await loadABConfig(ctx, agentDir);
282
288
  const storage = await loadStorage(ctx, agentDir);
289
+ const artifacts = await loadArtifacts(ctx, agentDir);
290
+ await appendArtifactTool(ctx, agentDir, agent, artifacts);
283
291
 
284
292
  if (agentDir !== rootDir) {
285
293
  await warnUnknownSlots(ctx, agentDir);
@@ -297,6 +305,7 @@ export async function loadAgentProject(
297
305
  abs,
298
306
  abConfig,
299
307
  storage,
308
+ artifacts,
300
309
  diagnostics,
301
310
  };
302
311
  }
@@ -351,6 +360,14 @@ async function loadAgent(
351
360
  message: "builtinTools is not supported on subagents; ignoring.",
352
361
  });
353
362
  }
363
+ const artifactsPath = await findModuleFile(agentDir, "artifacts");
364
+ if (artifactsPath !== undefined) {
365
+ ctx.diagnostics.push({
366
+ severity: "warning",
367
+ path: displayPath(ctx, artifactsPath),
368
+ message: "artifacts are not supported on subagents; ignoring.",
369
+ });
370
+ }
354
371
  return {
355
372
  name,
356
373
  ...(config?.description === undefined
@@ -1579,6 +1596,81 @@ async function loadStorage(
1579
1596
  return definition as StorageDefinition | undefined;
1580
1597
  }
1581
1598
 
1599
+ // ============================================================================
1600
+ // Artifacts (agent/artifacts.ts)
1601
+ // ============================================================================
1602
+
1603
+ async function loadArtifacts(
1604
+ ctx: DiscoveryContext,
1605
+ agentDir: string
1606
+ ): Promise<ArtifactsDefinition | undefined> {
1607
+ const path = await findModuleFile(agentDir, "artifacts");
1608
+ if (path === undefined) {
1609
+ return undefined;
1610
+ }
1611
+ const imported = await importDefault(ctx, path, "artifacts");
1612
+ if (imported === undefined) {
1613
+ return undefined;
1614
+ }
1615
+ const definition = imported as ArtifactsDefinition;
1616
+ if (
1617
+ definition.agentTool === true &&
1618
+ Object.keys(definition.kinds ?? {}).length === 0
1619
+ ) {
1620
+ ctx.diagnostics.push({
1621
+ severity: "error",
1622
+ path: displayPath(ctx, path),
1623
+ message:
1624
+ "artifacts.agentTool requires kinds: the tag_artifact tool needs at least one declared kind; its descriptions are the tool prompt.",
1625
+ });
1626
+ }
1627
+ return definition;
1628
+ }
1629
+
1630
+ /**
1631
+ * Materialize the `tag_artifact` built-in when `defineArtifacts` set
1632
+ * `agentTool: true`. Appended after `loadAgent` because the tool is
1633
+ * generated from the artifacts kinds registry, not `AgentConfig.builtinTools`.
1634
+ */
1635
+ async function appendArtifactTool(
1636
+ ctx: DiscoveryContext,
1637
+ agentDir: string,
1638
+ agent: ResolvedAgent,
1639
+ artifacts: ArtifactsDefinition | undefined
1640
+ ): Promise<void> {
1641
+ const tool = artifactBuiltinTool(artifacts);
1642
+ if (tool === undefined) {
1643
+ return;
1644
+ }
1645
+ const artifactsPath = displayPath(
1646
+ ctx,
1647
+ (await findModuleFile(agentDir, "artifacts")) ??
1648
+ join(agentDir, "artifacts.ts")
1649
+ );
1650
+ // TODO(artifacts-cloud): on cloud runtimes, ship tag_artifact as an
1651
+ // execution: "agent" script and have the host fold its action.result
1652
+ // stream events into artifact records — agent-tool calls already surface
1653
+ // durably in the session stream.
1654
+ if (agent.runtime === "cloud") {
1655
+ ctx.diagnostics.push({
1656
+ severity: "warning",
1657
+ path: artifactsPath,
1658
+ message:
1659
+ "tag_artifact is an in-process server tool (like builtinTools) and is not available on cloud agents; it is skipped.",
1660
+ });
1661
+ return;
1662
+ }
1663
+ if (agent.tools.some((t) => t.name === TAG_ARTIFACT_TOOL_NAME)) {
1664
+ ctx.diagnostics.push({
1665
+ severity: "warning",
1666
+ path: artifactsPath,
1667
+ message: `Authored tool "${TAG_ARTIFACT_TOOL_NAME}" shadows the built-in tool of the same name; the authored tool wins.`,
1668
+ });
1669
+ return;
1670
+ }
1671
+ agent.tools.push(tool);
1672
+ }
1673
+
1582
1674
  function resolveABName(definition: unknown, fallback: string): string {
1583
1675
  if (
1584
1676
  typeof definition === "object" &&
@@ -1759,6 +1851,14 @@ export function projectInfo(project: AgentProject): AgentProjectInfo {
1759
1851
  project.storage === undefined
1760
1852
  ? undefined
1761
1853
  : { name: project.storage.name },
1854
+ artifacts:
1855
+ project.artifacts === undefined
1856
+ ? undefined
1857
+ : {
1858
+ kinds: Object.keys(project.artifacts.kinds ?? {}),
1859
+ agentTool: project.artifacts.agentTool === true,
1860
+ max: resolveArtifactsMax(project.artifacts),
1861
+ },
1762
1862
  diagnostics: project.diagnostics,
1763
1863
  };
1764
1864
  }
@@ -1779,6 +1879,7 @@ async function importDefault(
1779
1879
  | "hook"
1780
1880
  | "ab"
1781
1881
  | "storage"
1882
+ | "artifacts"
1782
1883
  ): Promise<unknown> {
1783
1884
  let mod: Record<string, unknown>;
1784
1885
  try {
@@ -107,6 +107,7 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
107
107
  ["evals/reporters", "evals/reporters.js"],
108
108
  ["evals/loaders", "evals/loaders.js"],
109
109
  ["ab", "ab.js"],
110
+ ["artifacts", "artifacts.js"],
110
111
  ["storage", "storage.js"],
111
112
  ["storage/file-kv", "storage-backends/file-kv.js"],
112
113
  ["storage/postgres-kv", "storage-backends/postgres-kv.js"],
@@ -0,0 +1,66 @@
1
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { beforeAll, describe, expect, it } from "vitest";
5
+ import { resolveDocsFile } from "./docs-site.js";
6
+
7
+ describe("resolveDocsFile", () => {
8
+ let dist: string;
9
+
10
+ beforeAll(async () => {
11
+ dist = await mkdtemp(join(tmpdir(), "agent-serve-docs-"));
12
+ await mkdir(join(dist, "guides"), { recursive: true });
13
+ await mkdir(join(dist, "assets", "chunks"), { recursive: true });
14
+ await writeFile(join(dist, "index.html"), "<html>index</html>");
15
+ await writeFile(join(dist, "quickstart.html"), "<html>quickstart</html>");
16
+ await writeFile(join(dist, "guides", "slack.html"), "<html>slack</html>");
17
+ await writeFile(join(dist, "assets", "chunks", "app.js"), "js");
18
+ await writeFile(join(dist, "hashmap.json"), "{}");
19
+ });
20
+
21
+ it("serves the site index for the empty path", () => {
22
+ expect(resolveDocsFile("", dist)).toBe(join(dist, "index.html"));
23
+ expect(resolveDocsFile("/", dist)).toBe(join(dist, "index.html"));
24
+ });
25
+
26
+ it("resolves clean page URLs through the .html chain", () => {
27
+ expect(resolveDocsFile("quickstart", dist)).toBe(
28
+ join(dist, "quickstart.html")
29
+ );
30
+ expect(resolveDocsFile("guides/slack", dist)).toBe(
31
+ join(dist, "guides", "slack.html")
32
+ );
33
+ expect(resolveDocsFile("guides/slack/", dist)).toBe(
34
+ join(dist, "guides", "slack.html")
35
+ );
36
+ });
37
+
38
+ it("serves exact files (assets, json, explicit .html)", () => {
39
+ expect(resolveDocsFile("assets/chunks/app.js", dist)).toBe(
40
+ join(dist, "assets", "chunks", "app.js")
41
+ );
42
+ expect(resolveDocsFile("hashmap.json", dist)).toBe(
43
+ join(dist, "hashmap.json")
44
+ );
45
+ expect(resolveDocsFile("quickstart.html", dist)).toBe(
46
+ join(dist, "quickstart.html")
47
+ );
48
+ });
49
+
50
+ it("returns undefined for unknown pages", () => {
51
+ expect(resolveDocsFile("nope", dist)).toBeUndefined();
52
+ expect(resolveDocsFile("guides/nope", dist)).toBeUndefined();
53
+ });
54
+
55
+ it("rejects traversal out of the dist dir", () => {
56
+ expect(resolveDocsFile("../package.json", dist)).toBeUndefined();
57
+ expect(resolveDocsFile("..%2Fpackage.json", dist)).toBeUndefined();
58
+ expect(resolveDocsFile("guides/../../package.json", dist)).toBeUndefined();
59
+ expect(resolveDocsFile("..", dist)).toBeUndefined();
60
+ });
61
+
62
+ it("does not treat directories as files", () => {
63
+ // `guides` has no guides.html and no guides/index.html.
64
+ expect(resolveDocsFile("guides", dist)).toBeUndefined();
65
+ });
66
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isValidDuration, parseDuration } from "./duration.js";
3
+
4
+ describe("parseDuration", () => {
5
+ it("parses short units", () => {
6
+ expect(parseDuration("30s")).toBe(30_000);
7
+ expect(parseDuration("5m")).toBe(5 * 60_000);
8
+ expect(parseDuration("2h")).toBe(2 * 3_600_000);
9
+ expect(parseDuration("1d")).toBe(86_400_000);
10
+ });
11
+
12
+ it("parses word units", () => {
13
+ expect(parseDuration("10 minutes")).toBe(10 * 60_000);
14
+ expect(parseDuration("1.5h")).toBe(90 * 60_000);
15
+ });
16
+
17
+ it("accepts bare numbers as milliseconds", () => {
18
+ expect(parseDuration(1500)).toBe(1500);
19
+ expect(parseDuration("1500")).toBe(1500);
20
+ });
21
+
22
+ it("rejects invalid input", () => {
23
+ expect(() => parseDuration("")).toThrow();
24
+ expect(() => parseDuration("abc")).toThrow();
25
+ expect(() => parseDuration("5x")).toThrow();
26
+ expect(isValidDuration("5m")).toBe(true);
27
+ expect(isValidDuration("nope")).toBe(false);
28
+ });
29
+ });
@@ -0,0 +1,187 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { EvalJudgeUnavailableError } from "../evals/judge.js";
3
+ import {
4
+ assertJudgeBackendMatch,
5
+ callJudgeModel,
6
+ resolveJudgeModel,
7
+ } from "./eval-judge-model.js";
8
+
9
+ const create = vi.hoisted(() => vi.fn());
10
+ const resolveApiKey = vi.hoisted(() => vi.fn());
11
+
12
+ vi.mock("@cursor/sdk", () => ({ Agent: { create } }));
13
+
14
+ vi.mock("./cursor/credentials.js", async (importOriginal) => {
15
+ const actual =
16
+ await importOriginal<typeof import("./cursor/credentials.js")>();
17
+ return { ...actual, resolveApiKey };
18
+ });
19
+
20
+ beforeEach(() => {
21
+ create.mockReset();
22
+ resolveApiKey.mockReset();
23
+ resolveApiKey.mockResolvedValue({
24
+ apiKey: "key_1",
25
+ source: "env",
26
+ backendUrl: "https://api2.cursor.sh",
27
+ });
28
+ create.mockResolvedValue({
29
+ send: async () => ({
30
+ wait: async () => ({ status: "completed", result: "CHOICE: Y" }),
31
+ }),
32
+ close: () => undefined,
33
+ });
34
+ });
35
+
36
+ describe("resolveJudgeModel", () => {
37
+ it("resolves innermost-first and ignores a blank env value", () => {
38
+ expect(
39
+ resolveJudgeModel({
40
+ call: "call-model",
41
+ evalLevel: "eval-model",
42
+ config: "config-model",
43
+ env: "env-model",
44
+ })
45
+ ).toBe("call-model");
46
+ expect(
47
+ resolveJudgeModel({ evalLevel: "eval-model", config: "config-model" })
48
+ ).toBe("eval-model");
49
+ expect(resolveJudgeModel({ config: "config-model" })).toBe("config-model");
50
+ expect(resolveJudgeModel({ env: " env-model " })).toBe("env-model");
51
+ expect(resolveJudgeModel({ env: " " })).toBeUndefined();
52
+ expect(resolveJudgeModel({})).toBeUndefined();
53
+ });
54
+ });
55
+
56
+ describe("assertJudgeBackendMatch", () => {
57
+ it("allows a credential scoped to the backend the SDK will use", () => {
58
+ expect(() =>
59
+ assertJudgeBackendMatch(
60
+ "https://api2.cursor.sh",
61
+ "https://api2.cursor.sh"
62
+ )
63
+ ).not.toThrow();
64
+ });
65
+
66
+ it("ignores a trailing-slash difference", () => {
67
+ expect(() =>
68
+ assertJudgeBackendMatch(
69
+ "https://api2.cursor.sh/",
70
+ "https://api2.cursor.sh"
71
+ )
72
+ ).not.toThrow();
73
+ });
74
+
75
+ it("declines when the credential belongs to a different backend", () => {
76
+ // The SDK takes no backend option, so sending the key anyway would ship it
77
+ // (and the judge prompt) to a host it was not minted for.
78
+ expect(() =>
79
+ assertJudgeBackendMatch(
80
+ "https://staging.example.com",
81
+ "https://api2.cursor.sh"
82
+ )
83
+ ).toThrow(EvalJudgeUnavailableError);
84
+ expect(() =>
85
+ assertJudgeBackendMatch(
86
+ "https://staging.example.com",
87
+ "https://api2.cursor.sh"
88
+ )
89
+ ).toThrow(/does not read AGENT_SERVE_CURSOR_BACKEND_URL/);
90
+ });
91
+ });
92
+
93
+ describe("callJudgeModel", () => {
94
+ it("skips visibly when there are no credentials", async () => {
95
+ resolveApiKey.mockResolvedValue(undefined);
96
+ await expect(
97
+ callJudgeModel({ prompt: "grade this", model: "gpt-5.4-mini" })
98
+ ).rejects.toThrow(EvalJudgeUnavailableError);
99
+ expect(create).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("confines the grading session so an injected reply cannot reach the host", async () => {
103
+ await callJudgeModel({ prompt: "grade this", model: "gpt-5.4-mini" });
104
+
105
+ expect(create).toHaveBeenCalledTimes(1);
106
+ const options = create.mock.calls[0]![0] as {
107
+ mcpServers: Record<string, unknown>;
108
+ local: {
109
+ cwd: string;
110
+ settingSources: string[];
111
+ sandboxOptions: { enabled: boolean };
112
+ };
113
+ };
114
+ // Grading embeds untrusted agent output, so the judge gets no MCP surface,
115
+ // no ambient settings, an empty scratch cwd, and a sandbox.
116
+ expect(options.mcpServers).toEqual({});
117
+ expect(options.local.settingSources).toEqual([]);
118
+ expect(options.local.sandboxOptions).toEqual({ enabled: true });
119
+ expect(options.local.cwd).toMatch(/agentkit-eval-judge-/);
120
+ });
121
+
122
+ it("never opens a session when the credential's backend disagrees", async () => {
123
+ resolveApiKey.mockResolvedValue({
124
+ apiKey: "key_1",
125
+ source: "login",
126
+ backendUrl: "https://staging.example.com",
127
+ });
128
+ await expect(
129
+ callJudgeModel({ prompt: "p", model: "gpt-5.4-mini" })
130
+ ).rejects.toThrow(EvalJudgeUnavailableError);
131
+ // The point of failing closed: the key never reaches the SDK.
132
+ expect(create).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it("passes a bare model id through as a selection object", async () => {
136
+ await callJudgeModel({ prompt: "p", model: "gpt-5.4-mini" });
137
+ expect(create.mock.calls[0]![0]).toMatchObject({
138
+ apiKey: "key_1",
139
+ model: { id: "gpt-5.4-mini" },
140
+ });
141
+ });
142
+
143
+ it("preserves model params when given a selection object", async () => {
144
+ await callJudgeModel({
145
+ prompt: "p",
146
+ model: { id: "gpt-5.4-mini", params: [{ id: "effort", value: "low" }] },
147
+ });
148
+ expect(create.mock.calls[0]![0]).toMatchObject({
149
+ model: { id: "gpt-5.4-mini", params: [{ id: "effort", value: "low" }] },
150
+ });
151
+ });
152
+
153
+ it("returns the judge reply", async () => {
154
+ await expect(
155
+ callJudgeModel({ prompt: "p", model: "gpt-5.4-mini" })
156
+ ).resolves.toBe("CHOICE: Y");
157
+ });
158
+
159
+ it("surfaces a failed grading run as an error", async () => {
160
+ create.mockResolvedValue({
161
+ send: async () => ({
162
+ wait: async () => ({
163
+ status: "error",
164
+ error: { message: "provider unavailable" },
165
+ }),
166
+ }),
167
+ close: () => undefined,
168
+ });
169
+ await expect(
170
+ callJudgeModel({ prompt: "p", model: "gpt-5.4-mini" })
171
+ ).rejects.toThrow(/eval judge run failed: provider unavailable/);
172
+ });
173
+
174
+ it("closes the session even when grading throws", async () => {
175
+ const close = vi.fn();
176
+ create.mockResolvedValue({
177
+ send: async () => {
178
+ throw new Error("boom");
179
+ },
180
+ close,
181
+ });
182
+ await expect(
183
+ callJudgeModel({ prompt: "p", model: "gpt-5.4-mini" })
184
+ ).rejects.toThrow("boom");
185
+ expect(close).toHaveBeenCalledTimes(1);
186
+ });
187
+ });