@cursor/july 0.1.31 → 0.1.32

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 (72) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  3. package/dist/channels/slack/post-update-delivery.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.DigB_9cQ.js → app.C2aVczmo.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.xgMnlUSO.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.BCMX25Bv.js → VPLocalSearchBox.DLEzetbD.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.BDDWeELx.js → theme.BkOH1oMp.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/files-backends/agent-store-presigned-url.d.ts +3 -100
  56. package/dist/files-backends/agent-store-presigned-url.d.ts.map +1 -1
  57. package/dist/files-backends/agent-store-presigned-url.js +990 -314
  58. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  59. package/dist/files-backends/cursor-hosted.js +25 -2
  60. package/dist/internal/json-dir-store.d.ts +32 -0
  61. package/dist/internal/json-dir-store.d.ts.map +1 -0
  62. package/dist/internal/persistence-coordinator.d.ts +127 -0
  63. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  64. package/dist/multi-tenant.d.ts +80 -0
  65. package/dist/multi-tenant.d.ts.map +1 -0
  66. package/dist/multi-tenant.js +69 -0
  67. package/dist/persistence.d.ts +184 -0
  68. package/dist/persistence.d.ts.map +1 -0
  69. package/package.json +3 -2
  70. package/src/files-backends/agent-store-presigned-url.ts +6 -400
  71. package/src/files-backends/cursor-hosted.ts +32 -6
  72. package/dist/docs/assets/chunks/@localSearchIndexroot.CnFFl07y.js +0 -1
@@ -1,402 +1,8 @@
1
1
  /**
2
- * Vendored from `@anysphere/agent-store-sync` `presigned-url.ts` so the
3
- * published `@cursor/july` tarball does not depend on that private workspace
4
- * package (not on npm). Keep in sync when the source validator changes.
2
+ * Re-export the agent-store SSRF pin used by `cursorHostedFiles`.
3
+ *
4
+ * Source keeps a single dependency on `@anysphere/agent-store-sync` (dev-only
5
+ * for publish). The build overwrites the emitted `dist` copy with an esbuild
6
+ * bundle so the npm tarball does not need that private package at runtime.
5
7
  */
6
-
7
- // Validators the engine runs against every server-returned presigned URL
8
- // before fetching, so a compromised transport can't turn inbound sync
9
- // into an SSRF gadget. Production wiring should pin the bucket host via
10
- // `createHostAllowlistValidator` on top of the strict default.
11
-
12
- export type PresignedUrlValidator = (url: URL) => void;
13
-
14
- export type PresignedUrlRejectionCode =
15
- | "scheme_not_allowed"
16
- | "userinfo_not_allowed"
17
- | "host_not_allowed"
18
- | "private_host_not_allowed";
19
-
20
- export interface PresignedUrlRejectionOptions {
21
- code: PresignedUrlRejectionCode;
22
- message: string;
23
- url?: string;
24
- }
25
-
26
- export class PresignedUrlRejectedError extends Error {
27
- public readonly code: PresignedUrlRejectionCode;
28
- /** The host that triggered the rejection (never the full URL). */
29
- public readonly url: string | undefined;
30
-
31
- constructor(options: PresignedUrlRejectionOptions) {
32
- super(options.message);
33
- this.name = "PresignedUrlRejectedError";
34
- this.code = options.code;
35
- this.url = options.url;
36
- }
37
- }
38
-
39
- /**
40
- * Strict default: accepts `https:` URLs to an AWS S3 endpoint with no
41
- * userinfo and no private/internal address. Rejects DNS hosts that
42
- * resolve into the private IP space and IPv4-mapped IPv6 literals.
43
- */
44
- export function defaultPresignedUrlValidator(url: URL): void {
45
- assertSafeAuthority(url);
46
- if (url.protocol !== "https:") {
47
- throw new PresignedUrlRejectedError({
48
- code: "scheme_not_allowed",
49
- message: `Refused presigned URL with non-https scheme: ${url.protocol}`,
50
- url: redactPresignedUrlString(url),
51
- });
52
- }
53
- const hostname = stripIPv6Brackets(url.hostname);
54
- if (isLocalOrInternalHost(hostname)) {
55
- throw new PresignedUrlRejectedError({
56
- code: "private_host_not_allowed",
57
- message: `Refused https presigned URL with private/internal host: ${hostname}`,
58
- url: redactPresignedUrlString(url),
59
- });
60
- }
61
- if (!isAwsS3Hostname(hostname)) {
62
- throw new PresignedUrlRejectedError({
63
- code: "host_not_allowed",
64
- message: `Refused presigned URL whose host is not an AWS S3 endpoint: ${hostname}`,
65
- url: redactPresignedUrlString(url),
66
- });
67
- }
68
- }
69
-
70
- /**
71
- * True when `hostname` is an AWS S3 object-storage endpoint:
72
- * `*.amazonaws.com` with at least one label in
73
- * {@link S3_OBJECT_LABELS}. Covers modern virtual-host, path-style,
74
- * dualstack, accelerate, and FIPS URLs. Rejects the S3-named control
75
- * planes (`s3-control`, `s3-outposts`, `s3-object-lambda`) and the
76
- * legacy regional virtual-host form AWS deprecated in 2019, both of
77
- * which would otherwise let a tampered presign issuer turn the sync
78
- * client into a signed-request gadget against non-storage APIs.
79
- */
80
- export function isAwsS3Hostname(hostname: string): boolean {
81
- const lowered = hostname.toLowerCase();
82
- if (lowered !== "amazonaws.com" && !lowered.endsWith(".amazonaws.com")) {
83
- return false;
84
- }
85
- const labels = lowered.split(".");
86
- return labels.some(label => S3_OBJECT_LABELS.has(label));
87
- }
88
-
89
- const S3_OBJECT_LABELS = new Set(["s3", "s3-accelerate", "s3-fips"]);
90
-
91
- /**
92
- * Like {@link defaultPresignedUrlValidator} plus a case-insensitive
93
- * exact-match check against `allowedHosts` (no subdomain wildcards).
94
- */
95
- export function createHostAllowlistValidator(
96
- allowedHosts: readonly string[]
97
- ): PresignedUrlValidator {
98
- const allowed = new Set(allowedHosts.map(host => host.toLowerCase()));
99
- return (url: URL): void => {
100
- defaultPresignedUrlValidator(url);
101
- if (!allowed.has(url.hostname.toLowerCase())) {
102
- throw new PresignedUrlRejectedError({
103
- code: "host_not_allowed",
104
- message: `Refused presigned URL whose host is not in the allowlist: ${url.hostname}`,
105
- url: redactPresignedUrlString(url),
106
- });
107
- }
108
- };
109
- }
110
-
111
- /**
112
- * Virtual-host S3 endpoints for the production `agent-stores` bucket
113
- * (matches backend `getAgentStoreBucket()` in
114
- * `backend/server/src/agent-stores/utils/agentStoreS3.ts`, which presigns
115
- * with a `us-east-1` `S3Client` and the SDK's default virtual-host
116
- * addressing). Both the regional form emitted by modern AWS SDK v3 and
117
- * the legacy global form are listed so a presign issuer upgrade or
118
- * downgrade can't trip the allowlist. Path-style (`s3.amazonaws.com/agent-stores/…`)
119
- * is intentionally not listed: the server never sets `forcePathStyle`,
120
- * and admitting bare `s3.amazonaws.com` would re-open the "any bucket"
121
- * gap this allowlist exists to close.
122
- */
123
- export const BCS_AGENT_STORE_BUCKET_HOSTS: readonly string[] = [
124
- "agent-stores.s3.us-east-1.amazonaws.com",
125
- "agent-stores.s3.amazonaws.com",
126
- ];
127
-
128
- const PLAYGROUND_AGENT_STORE_BUCKET_HOSTS: readonly string[] = [
129
- "agent-stores-928182716709-us-west-2-an.s3.us-west-2.amazonaws.com",
130
- "agent-stores-928182716709-us-west-2-an.s3.amazonaws.com",
131
- ];
132
-
133
- /**
134
- * Production validator for the BCS agent-store sync transport: strict
135
- * default (https, S3 endpoint, no private hosts, no userinfo) plus the
136
- * {@link BCS_AGENT_STORE_BUCKET_HOSTS} pin. Pre-built so callers don't
137
- * accidentally pass {@link defaultPresignedUrlValidator}, which would
138
- * accept any AWS S3 bucket on the internet and let a buggy or
139
- * compromised presign issuer redirect sync traffic away from the real
140
- * `agent-stores` bucket.
141
- */
142
- export function createBcsPresignedUrlValidator(): PresignedUrlValidator {
143
- return createHostAllowlistValidator(BCS_AGENT_STORE_BUCKET_HOSTS);
144
- }
145
-
146
- /**
147
- * True when `backendUrl` targets a local dev backend (localhost or
148
- * lclhst.build). Matches IDE checks in `cursor-always-local` /
149
- * `cursorCredsService`.
150
- */
151
- export function isLocalAgentStoreBackendUrl(backendUrl: string): boolean {
152
- const trimmed = backendUrl.trim();
153
- if (trimmed === "") {
154
- return false;
155
- }
156
- return trimmed.includes("localhost") || trimmed.includes("lclhst.build");
157
- }
158
-
159
- function isPlaygroundAgentStoreBackendUrl(backendUrl: string): boolean {
160
- try {
161
- const hostname = new URL(backendUrl).hostname.toLowerCase();
162
- return (
163
- hostname === "playground.cursor.sh" ||
164
- hostname.endsWith(".playground.cursor.sh")
165
- );
166
- } catch {
167
- return false;
168
- }
169
- }
170
-
171
- /**
172
- * Presign validator for the sync client. Production/staging and playground
173
- * backends are pinned to their respective agent-store buckets. For a local dev API (see
174
- * {@link isLocalAgentStoreBackendUrl}), allow `http:` to loopback only
175
- * (LocalStack) and still pin all other URLs to {@link BCS_AGENT_STORE_BUCKET_HOSTS}.
176
- */
177
- export function createAgentStorePresignedUrlValidatorForBackend(
178
- backendUrl: string
179
- ): PresignedUrlValidator {
180
- if (isLocalAgentStoreBackendUrl(backendUrl)) {
181
- return createLocalDevAgentStorePresignedUrlValidator();
182
- }
183
- if (isPlaygroundAgentStoreBackendUrl(backendUrl)) {
184
- return createHostAllowlistValidator(PLAYGROUND_AGENT_STORE_BUCKET_HOSTS);
185
- }
186
- return createBcsPresignedUrlValidator();
187
- }
188
-
189
- function createLocalDevAgentStorePresignedUrlValidator(): PresignedUrlValidator {
190
- const bcsValidator = createBcsPresignedUrlValidator();
191
- return (url: URL): void => {
192
- assertSafeAuthority(url);
193
- const hostname = stripIPv6Brackets(url.hostname);
194
- if (url.protocol === "http:" && isLoopbackHostName(hostname)) {
195
- return;
196
- }
197
- bcsValidator(url);
198
- };
199
- }
200
-
201
- /**
202
- * Like {@link defaultPresignedUrlValidator} plus `http://localhost` /
203
- * `127.0.0.1` / `[::1]` (any port) for LocalStack-backed unit tests.
204
- * Non-loopback `https:` URLs fall through to the strict default (any S3
205
- * bucket). Do not use for IDE wiring — use
206
- * {@link createAgentStorePresignedUrlValidatorForBackend} instead.
207
- */
208
- export function localhostPermissivePresignedUrlValidator(url: URL): void {
209
- assertSafeAuthority(url);
210
- const hostname = stripIPv6Brackets(url.hostname);
211
- if (url.protocol === "http:" && isLoopbackHostName(hostname)) {
212
- return;
213
- }
214
- defaultPresignedUrlValidator(url);
215
- }
216
-
217
- function assertSafeAuthority(url: URL): void {
218
- if (url.username !== "" || url.password !== "") {
219
- throw new PresignedUrlRejectedError({
220
- code: "userinfo_not_allowed",
221
- message: "Refused presigned URL with embedded userinfo",
222
- url: redactPresignedUrlString(url),
223
- });
224
- }
225
- }
226
-
227
- /**
228
- * Strip the query string and userinfo from a presigned URL so the AWS
229
- * SigV4 signature never reaches error messages or telemetry.
230
- */
231
- export function redactPresignedUrlString(url: URL | string): string {
232
- try {
233
- const parsed = typeof url === "string" ? new URL(url) : url;
234
- return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
235
- } catch {
236
- return "<unparseable-presigned-url>";
237
- }
238
- }
239
-
240
- /**
241
- * Parse and validate a server-returned presigned URL before fetch. Uses
242
- * {@link relPath} in error text (never the raw URL) so SigV4 credentials
243
- * cannot leak into messages.
244
- */
245
- export function assertPresignedUrlSafe(args: {
246
- readonly rawUrl: string;
247
- readonly relPath: string;
248
- readonly validatePresignedUrl: PresignedUrlValidator;
249
- }): void {
250
- let parsed: URL;
251
- try {
252
- parsed = new URL(args.rawUrl);
253
- } catch {
254
- throw new Error(`Refusing unparseable presigned URL for ${args.relPath}`);
255
- }
256
- try {
257
- args.validatePresignedUrl(parsed);
258
- } catch (error) {
259
- throw new Error(
260
- `Refusing presigned URL for ${args.relPath}: ${
261
- error instanceof Error ? error.message : String(error)
262
- }`,
263
- { cause: error instanceof Error ? error : undefined }
264
- );
265
- }
266
- }
267
-
268
- function isLoopbackHostName(hostname: string): boolean {
269
- const lowered = hostname.toLowerCase();
270
- return (
271
- lowered === "localhost" ||
272
- lowered === "ip6-localhost" ||
273
- lowered === "127.0.0.1" ||
274
- lowered === "::1" ||
275
- lowered === "0:0:0:0:0:0:0:1"
276
- );
277
- }
278
-
279
- function isLocalOrInternalHost(hostname: string): boolean {
280
- if (hostname.length === 0) {
281
- return true;
282
- }
283
- if (isLoopbackHostName(hostname)) {
284
- return true;
285
- }
286
- if (isIPv4Address(hostname)) {
287
- return isPrivateOrInternalIPv4(hostname);
288
- }
289
- if (isIPv6Address(hostname)) {
290
- return isPrivateOrInternalIPv6(hostname);
291
- }
292
- // DNS hostnames can't be classified without resolving; the caller
293
- // relies on transport-level allowlists for those.
294
- return false;
295
- }
296
-
297
- function stripIPv6Brackets(hostname: string): string {
298
- return hostname.startsWith("[") && hostname.endsWith("]")
299
- ? hostname.slice(1, -1)
300
- : hostname;
301
- }
302
-
303
- function isIPv4Address(hostname: string): boolean {
304
- const parts = hostname.split(".");
305
- if (parts.length !== 4) {
306
- return false;
307
- }
308
- return parts.every(part => {
309
- if (part.length === 0 || part.length > 3) {
310
- return false;
311
- }
312
- if (!/^\d+$/.test(part)) {
313
- return false;
314
- }
315
- const n = Number(part);
316
- return Number.isInteger(n) && n >= 0 && n <= 255;
317
- });
318
- }
319
-
320
- function isPrivateOrInternalIPv4(hostname: string): boolean {
321
- const [a, b] = hostname.split(".").map(Number);
322
- if (a === undefined || b === undefined) {
323
- return true;
324
- }
325
- // RFC1918, loopback, link-local, multicast, unspecified, broadcast,
326
- // carrier-grade NAT, and reserved blocks.
327
- if (a === 0) return true;
328
- if (a === 10) return true;
329
- if (a === 127) return true;
330
- if (a === 169 && b === 254) return true;
331
- if (a === 172 && b >= 16 && b <= 31) return true;
332
- if (a === 192 && b === 168) return true;
333
- if (a === 100 && b >= 64 && b <= 127) return true;
334
- if (a >= 224 && a <= 239) return true;
335
- if (a >= 240) return true;
336
- return false;
337
- }
338
-
339
- function isIPv6Address(hostname: string): boolean {
340
- // Loose check — just enough to route the host to the IPv6 classifier.
341
- return hostname.includes(":");
342
- }
343
-
344
- function isPrivateOrInternalIPv6(hostname: string): boolean {
345
- const lowered = hostname.toLowerCase();
346
- if (lowered === "::" || lowered === "::1") {
347
- return true;
348
- }
349
- // IPv4-mapped IPv6 (RFC4291) routes back through the IPv4 classifier
350
- // so `[::ffff:169.254.169.254]` can't slip past the private-IP check.
351
- const mapped = extractIPv4MappedAddress(lowered);
352
- if (mapped !== undefined) {
353
- return isPrivateOrInternalIPv4(mapped);
354
- }
355
- // ULA fc00::/7, link-local fe80::/10, multicast ff00::/8.
356
- if (lowered.startsWith("fc") || lowered.startsWith("fd")) {
357
- return true;
358
- }
359
- if (lowered.startsWith("fe8") || lowered.startsWith("fe9")) {
360
- return true;
361
- }
362
- if (lowered.startsWith("fea") || lowered.startsWith("feb")) {
363
- return true;
364
- }
365
- if (lowered.startsWith("ff")) {
366
- return true;
367
- }
368
- return false;
369
- }
370
-
371
- // Convert an IPv4-mapped IPv6 literal (`::ffff:a.b.c.d` or
372
- // `::ffff:hhhh:hhhh`) to its dotted-quad form. Returns undefined otherwise.
373
- function extractIPv4MappedAddress(hostname: string): string | undefined {
374
- const dotted = hostname.match(
375
- /^(?:0:0:0:0:0|::):?ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i
376
- );
377
- if (dotted) {
378
- return dotted[1];
379
- }
380
- if (hostname.startsWith("::ffff:")) {
381
- const tail = hostname.slice("::ffff:".length);
382
- if (isIPv4Address(tail)) {
383
- return tail;
384
- }
385
- const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
386
- if (hex) {
387
- const high = Number.parseInt(hex[1], 16);
388
- const low = Number.parseInt(hex[2], 16);
389
- if (
390
- Number.isFinite(high) &&
391
- Number.isFinite(low) &&
392
- high >= 0 &&
393
- high <= 0xffff &&
394
- low >= 0 &&
395
- low <= 0xffff
396
- ) {
397
- return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
398
- }
399
- }
400
- }
401
- return undefined;
402
- }
8
+ export { createAgentStorePresignedUrlValidatorForBackend } from "@anysphere/agent-store-sync";
@@ -437,11 +437,22 @@ export function cursorHostedFiles(
437
437
 
438
438
  async get(key): Promise<FileSinkBody | undefined> {
439
439
  return await withFiles(async (files) => {
440
- const presigned = await storePost<ApiPresignResponse>(
441
- options,
442
- files.path("/download"),
443
- { paths: [key] }
444
- );
440
+ // Presigning a read of a path the store has no live object for — never
441
+ // written, or deleted and now holding a tombstone — is a 404. That is
442
+ // absence, which `read()` reports as undefined, not a failure.
443
+ let presigned: ApiPresignResponse;
444
+ try {
445
+ presigned = await storePost<ApiPresignResponse>(
446
+ options,
447
+ files.path("/download"),
448
+ { paths: [key] }
449
+ );
450
+ } catch (error) {
451
+ if (error instanceof StoreApiError && error.status === 404) {
452
+ return undefined;
453
+ }
454
+ throw error;
455
+ }
445
456
  const url = presigned.files?.[0]?.url;
446
457
  if (url === undefined) {
447
458
  return undefined;
@@ -661,13 +672,28 @@ async function sendJson<T>(
661
672
  );
662
673
  }
663
674
  if (!response.ok) {
664
- throw new Error(
675
+ throw new StoreApiError(
676
+ response.status,
665
677
  `cursorHostedFiles ${url}: ${describeError(parsed, response.status)}`
666
678
  );
667
679
  }
668
680
  return parsed as T;
669
681
  }
670
682
 
683
+ /**
684
+ * A non-2xx answer from the files API, carrying the status so a caller can
685
+ * tell "absent" apart from a failure it must not swallow.
686
+ */
687
+ class StoreApiError extends Error {
688
+ constructor(
689
+ readonly status: number,
690
+ message: string
691
+ ) {
692
+ super(message);
693
+ this.name = "StoreApiError";
694
+ }
695
+ }
696
+
671
697
  function describeError(parsed: unknown, status: number): string {
672
698
  if (
673
699
  parsed !== null &&