@cursor/july 0.1.78 → 0.1.80

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 (78) hide show
  1. package/dist/channels/github/api.d.ts.map +1 -1
  2. package/dist/channels/github/api.js +8 -1
  3. package/dist/channels/github/cursor-account.d.ts +17 -0
  4. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  5. package/dist/channels/github/cursor-account.js +47 -0
  6. package/dist/docs/404.html +1 -1
  7. package/dist/docs/ab.html +2 -2
  8. package/dist/docs/assets/{app.BsA7t7rS.js → app.e4_wtjUS.js} +1 -1
  9. package/dist/docs/assets/chunks/@localSearchIndexroot.BGiMJpnL.js +1 -0
  10. package/dist/docs/assets/chunks/{VPLocalSearchBox.B1hHhktM.js → VPLocalSearchBox.DbdzyEie.js} +1 -1
  11. package/dist/docs/assets/chunks/{theme.DLpLSsQh.js → theme.D8HpLrLK.js} +2 -2
  12. package/dist/docs/building-with-agents.html +2 -2
  13. package/dist/docs/concepts.html +2 -2
  14. package/dist/docs/deployment.html +2 -2
  15. package/dist/docs/evals.html +2 -2
  16. package/dist/docs/example-agents/approval-buddy.html +2 -2
  17. package/dist/docs/example-agents/benny.html +2 -2
  18. package/dist/docs/example-agents/bugbot.html +2 -2
  19. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  20. package/dist/docs/example-agents/codeowners-review.html +2 -2
  21. package/dist/docs/example-agents/concierge.html +2 -2
  22. package/dist/docs/example-agents/fsd.html +2 -2
  23. package/dist/docs/example-agents/index.html +2 -2
  24. package/dist/docs/example-agents/knowledge-base.html +2 -2
  25. package/dist/docs/example-agents/oncall.html +2 -2
  26. package/dist/docs/example-agents/security-reviewer.html +2 -2
  27. package/dist/docs/example-agents/slack-agent.html +2 -2
  28. package/dist/docs/example-agents/weather-agent.html +2 -2
  29. package/dist/docs/guides/agent-to-agent.html +2 -2
  30. package/dist/docs/guides/cloud-runtime.html +2 -2
  31. package/dist/docs/guides/convert-automation.html +2 -2
  32. package/dist/docs/guides/github.html +2 -2
  33. package/dist/docs/guides/human-in-the-loop.html +2 -2
  34. package/dist/docs/guides/mcp-oauth.html +2 -2
  35. package/dist/docs/guides/opentelemetry.html +2 -2
  36. package/dist/docs/guides/slack.html +2 -2
  37. package/dist/docs/guides/webhooks.html +2 -2
  38. package/dist/docs/hillclimbing.html +2 -2
  39. package/dist/docs/index.html +2 -2
  40. package/dist/docs/quickstart.html +2 -2
  41. package/dist/docs/reference/agent-config.html +2 -2
  42. package/dist/docs/reference/artifacts.html +2 -2
  43. package/dist/docs/reference/channels.html +2 -2
  44. package/dist/docs/reference/cli.html +2 -2
  45. package/dist/docs/reference/connections.html +2 -2
  46. package/dist/docs/reference/hooks.html +2 -2
  47. package/dist/docs/reference/http-api.html +2 -2
  48. package/dist/docs/reference/instructions.html +2 -2
  49. package/dist/docs/reference/playground.html +2 -2
  50. package/dist/docs/reference/project-layout.html +2 -2
  51. package/dist/docs/reference/prompt.html +2 -2
  52. package/dist/docs/reference/schedules.html +2 -2
  53. package/dist/docs/reference/sessions.html +2 -2
  54. package/dist/docs/reference/skills.html +2 -2
  55. package/dist/docs/reference/subagents.html +2 -2
  56. package/dist/docs/reference/tools.html +2 -2
  57. package/dist/docs/scaffolding-agents.html +2 -2
  58. package/dist/docs/storage.html +2 -2
  59. package/dist/docs/templates/agentic-owners.html +2 -2
  60. package/dist/docs/templates/demo.html +2 -2
  61. package/dist/docs/templates/pr-autofixer.html +2 -2
  62. package/dist/docs/templates/security-reviewer.html +2 -2
  63. package/dist/docs/templates/triage.html +2 -2
  64. package/dist/docs/troubleshooting.html +2 -2
  65. package/dist/internal/review-comments.d.ts +37 -7
  66. package/dist/internal/review-comments.d.ts.map +1 -1
  67. package/dist/internal/review-comments.js +209 -10
  68. package/dist/internal/server.d.ts.map +1 -1
  69. package/dist/internal/server.js +101 -49
  70. package/package.json +1 -1
  71. package/src/channels/github/api.ts +8 -0
  72. package/src/channels/github/cursor-account.ts +56 -0
  73. package/src/internal/review-comments.ts +257 -16
  74. package/src/internal/server.ts +124 -60
  75. package/templates/agentic-owners/agent/lib/owners/match.ts +64 -3
  76. package/templates/agentic-owners/agent/lib/owners/parse.ts +55 -1
  77. package/templates/agentic-owners/agent/lib/owners/types.ts +5 -0
  78. package/dist/docs/assets/chunks/@localSearchIndexroot.Dcs6boxD.js +0 -1
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import type { HostGitHubClient } from "../types.js";
3
3
 
4
- export type ReviewCommentPublisher = "local_gh" | "host_github";
4
+ export type ReviewCommentPublisher = "local_gh" | "host_github" | "host_origin";
5
5
 
6
6
  export type ReviewCommentPostResult = {
7
7
  posted: boolean;
@@ -57,12 +57,25 @@ export type ReviewCommentPublisherImplementation = {
57
57
  };
58
58
 
59
59
  /**
60
- * SCMs this module can read review comments back from. Same growth seam as
61
- * `shallow-clone.ts`'s {@link import("./shallow-clone.js").ScmProvider}:
62
- * adding GitLab/Bitbucket/Origin means a resolver case plus a reader
63
- * implementation, and nothing above this line changes.
60
+ * SCMs this module can address. Same growth seam as `shallow-clone.ts`'s
61
+ * {@link import("./shallow-clone.js").ScmProvider}: adding a forge means a
62
+ * resolver case plus a publisher (and, when the public API supports it, a
63
+ * reader / thread resolver). Callers above this line stay provider-neutral.
64
64
  */
65
- export type ScmReviewProvider = "github";
65
+ export type ScmReviewProvider = "github" | "origin";
66
+
67
+ /**
68
+ * Origin public REST client used to post PR reviews (`ctx.origin` /
69
+ * {@link import("../channels/origin/api.js").createOriginClient}). Narrower
70
+ * than the full Origin handle so tests and CLI paths can inject `request()`.
71
+ */
72
+ export type HostOriginReviewClient = {
73
+ request(input: {
74
+ method: "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
75
+ path: string;
76
+ body?: unknown;
77
+ }): Promise<unknown>;
78
+ };
66
79
 
67
80
  /** Which SCM's PR to read review comments from. */
68
81
  export type ScmReviewTarget = {
@@ -147,18 +160,12 @@ export type ReviewThreadResolverImplementation = {
147
160
  ) => Promise<ResolveReviewThreadsResult>;
148
161
  };
149
162
 
150
- /** Parse a PR URL into the SCM target to read comments from. */
163
+ /** Parse a PR URL into the SCM target to read or post comments on. */
151
164
  export function resolveScmReviewTarget(
152
165
  prUrl: string
153
166
  ): ScmReviewTarget | undefined {
154
- const parsed = parseGithubPrUrl(prUrl);
155
- return parsed.kind === "valid"
156
- ? {
157
- provider: "github",
158
- repo: parsed.value.repo,
159
- prNumber: parsed.value.prNumber,
160
- }
161
- : undefined;
167
+ const parsed = parseReviewPrUrl(prUrl);
168
+ return parsed.kind === "valid" ? parsed.value : undefined;
162
169
  }
163
170
 
164
171
  /**
@@ -175,6 +182,10 @@ export function createReviewCommentReader(args: {
175
182
  return args.github === undefined
176
183
  ? undefined
177
184
  : createHostGithubReviewCommentReader(args.github);
185
+ case "origin":
186
+ // Public Origin REST lists discussion comments, not diff-anchored
187
+ // review threads. Stay undefined until a reader can fetch by id.
188
+ return undefined;
178
189
  }
179
190
  }
180
191
 
@@ -191,6 +202,8 @@ export function createReviewThreadResolver(args: {
191
202
  return args.github === undefined
192
203
  ? undefined
193
204
  : createHostGithubReviewThreadResolver(args.github);
205
+ case "origin":
206
+ return undefined;
194
207
  }
195
208
  }
196
209
 
@@ -222,7 +235,11 @@ export async function resolveReviewThreadsForComments(args: {
222
235
  github: args.github,
223
236
  });
224
237
  if (resolver === undefined) {
225
- return skippedResolve("no GitHub client bound to resolve threads");
238
+ return skippedResolve(
239
+ target.provider === "github"
240
+ ? "no GitHub client bound to resolve threads"
241
+ : `no thread resolver for ${target.provider}`
242
+ );
226
243
  }
227
244
  return resolver.resolveReviewThreadsForComments({
228
245
  target,
@@ -560,6 +577,49 @@ export function createHostGithubReviewPublisher(
560
577
  };
561
578
  }
562
579
 
580
+ /**
581
+ * Pick the publisher for `prUrl`'s SCM. GitHub falls back to local `gh`
582
+ * when no host client is bound (CLI). Origin has no local CLI publisher —
583
+ * returns undefined unless an Origin REST client is provided.
584
+ */
585
+ export function createReviewCommentPublisher(args: {
586
+ prUrl: string;
587
+ github?: HostGitHubClient;
588
+ origin?: HostOriginReviewClient;
589
+ }): ReviewCommentPublisherImplementation | undefined {
590
+ const target = resolveScmReviewTarget(args.prUrl);
591
+ if (target === undefined) {
592
+ return undefined;
593
+ }
594
+ switch (target.provider) {
595
+ case "github":
596
+ return args.github !== undefined
597
+ ? createHostGithubReviewPublisher(args.github)
598
+ : createLocalGhReviewPublisher();
599
+ case "origin":
600
+ return args.origin === undefined
601
+ ? undefined
602
+ : createHostOriginReviewPublisher(args.origin);
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Posts a COMMENT-verdict Origin review through the public REST API.
608
+ *
609
+ * Public Origin REST has no diff-anchored review comments (those are an
610
+ * internal CreateCommentThread RPC). Inline comments in the request are
611
+ * folded into the review body so findings still land on the PR.
612
+ */
613
+ export function createHostOriginReviewPublisher(
614
+ origin: HostOriginReviewClient
615
+ ): ReviewCommentPublisherImplementation {
616
+ return {
617
+ async postReview(args) {
618
+ return postReviewViaHostOrigin(origin, args);
619
+ },
620
+ };
621
+ }
622
+
563
623
  async function postReviewViaHostGithub(
564
624
  github: HostGitHubClient,
565
625
  args: ReviewCommentPostArgs
@@ -779,6 +839,76 @@ function skippedHost(detail: string): ReviewCommentPostResult {
779
839
  };
780
840
  }
781
841
 
842
+ function skippedOrigin(detail: string): ReviewCommentPostResult {
843
+ return {
844
+ posted: false,
845
+ publisher: "host_origin",
846
+ mode: "skipped",
847
+ inlineCommentCount: 0,
848
+ detail,
849
+ };
850
+ }
851
+
852
+ async function postReviewViaHostOrigin(
853
+ origin: HostOriginReviewClient,
854
+ args: ReviewCommentPostArgs
855
+ ): Promise<ReviewCommentPostResult> {
856
+ const target = resolveScmReviewTarget(args.prUrl);
857
+ if (target === undefined || target.provider !== "origin") {
858
+ return skippedOrigin("host Origin publisher only supports Origin PRs");
859
+ }
860
+ const [owner, repo] = target.repo.split("/", 2);
861
+ if (owner === undefined || repo === undefined || repo === "") {
862
+ return skippedOrigin("unparseable owner/repo from PR URL");
863
+ }
864
+
865
+ try {
866
+ args.signal?.throwIfAborted();
867
+ await origin.request({
868
+ method: "POST",
869
+ path: `/v1/origin/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${target.prNumber}/reviews`,
870
+ body: {
871
+ verdict: "comment",
872
+ body: originReviewBody(args),
873
+ },
874
+ });
875
+ return {
876
+ posted: true,
877
+ publisher: "host_origin",
878
+ mode: "review_body_only",
879
+ inlineCommentCount: 0,
880
+ detail: `posted Origin review on ${target.repo}#${target.prNumber}`,
881
+ };
882
+ } catch (error) {
883
+ if (args.signal?.aborted) {
884
+ throw error;
885
+ }
886
+ return skippedOrigin(
887
+ `host Origin review post failed: ${error instanceof Error ? error.message : String(error)}`
888
+ );
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Origin's public CreatePullRequestReview has a body, not inline comments.
894
+ * Fold attempted anchors into the summary so findings are not dropped.
895
+ */
896
+ function originReviewBody(args: ReviewCommentPostArgs): string {
897
+ if (args.comments.length === 0) {
898
+ return args.message;
899
+ }
900
+ const sections = args.comments.map((comment) => {
901
+ const location =
902
+ comment.start_line !== undefined &&
903
+ comment.start_line > 0 &&
904
+ comment.start_line !== comment.line
905
+ ? `${comment.file}:${comment.start_line}-${comment.line}`
906
+ : `${comment.file}:${comment.line}`;
907
+ return `### \`${location}\`\n\n${comment.body}`;
908
+ });
909
+ return [args.message, ...sections].join("\n\n");
910
+ }
911
+
782
912
  function skippedResolve(detail: string): ResolveReviewThreadsResult {
783
913
  return {
784
914
  requested: 0,
@@ -790,6 +920,117 @@ function skippedResolve(detail: string): ResolveReviewThreadsResult {
790
920
  };
791
921
  }
792
922
 
923
+ const ORIGIN_PR_PAGE_HOST_PATTERN = /^origin(-[a-z0-9]+)?\.cursor\.com$/i;
924
+
925
+ /**
926
+ * Provider-neutral PR URL parse. GitHub.com plus the Origin page shapes
927
+ * `parsePrTarget` in security-review-lib accepts. Duplicated here so
928
+ * agent-serve does not import that package.
929
+ */
930
+ function parseReviewPrUrl(
931
+ prUrl: string
932
+ ):
933
+ | { kind: "valid"; value: ScmReviewTarget }
934
+ | { kind: "invalid"; detail: string } {
935
+ let url: URL;
936
+ try {
937
+ url = new URL(prUrl);
938
+ } catch (error) {
939
+ return {
940
+ kind: "invalid",
941
+ detail: `unparseable PR URL: ${error instanceof Error ? error.message : String(error)}`,
942
+ };
943
+ }
944
+
945
+ const parts = url.pathname.split("/").filter(Boolean);
946
+ const hostname = url.hostname;
947
+
948
+ if (hostname === "github.com" && parts.length >= 4 && parts[2] === "pull") {
949
+ const prNumber = Number(parts[3]);
950
+ if (Number.isInteger(prNumber) && prNumber > 0) {
951
+ return {
952
+ kind: "valid",
953
+ value: {
954
+ provider: "github",
955
+ repo: `${parts[0]}/${parts[1]}`,
956
+ prNumber,
957
+ },
958
+ };
959
+ }
960
+ }
961
+
962
+ if (
963
+ ORIGIN_PR_PAGE_HOST_PATTERN.test(hostname) &&
964
+ parts.length >= 4 &&
965
+ parts[2] === "github"
966
+ ) {
967
+ const prNumber = Number(parts[3]);
968
+ if (Number.isInteger(prNumber) && prNumber > 0) {
969
+ return {
970
+ kind: "valid",
971
+ value: {
972
+ provider: "origin",
973
+ repo: `${parts[0]}/${parts[1]}`,
974
+ prNumber,
975
+ },
976
+ };
977
+ }
978
+ }
979
+
980
+ if (
981
+ /^(www\.)?cursor\.com$/i.test(hostname) &&
982
+ parts[0] === "codebase" &&
983
+ parts.length >= 5 &&
984
+ parts[3] === "pull"
985
+ ) {
986
+ const prNumber = Number(parts[4]);
987
+ if (Number.isInteger(prNumber) && prNumber > 0) {
988
+ return {
989
+ kind: "valid",
990
+ value: {
991
+ provider: "origin",
992
+ repo: `${parts[1]}/${parts[2]}`,
993
+ prNumber,
994
+ },
995
+ };
996
+ }
997
+ }
998
+
999
+ if (hostname === "review.cursor.com" && parts[0] === "pr") {
1000
+ if (parts.length >= 5 && parts[3] === "github") {
1001
+ const prNumber = Number(parts[4]);
1002
+ if (Number.isInteger(prNumber) && prNumber > 0) {
1003
+ return {
1004
+ kind: "valid",
1005
+ value: {
1006
+ provider: "origin",
1007
+ repo: `${parts[1]}/${parts[2]}`,
1008
+ prNumber,
1009
+ },
1010
+ };
1011
+ }
1012
+ }
1013
+ if (parts.length >= 4) {
1014
+ const prNumber = Number(parts[3]);
1015
+ if (Number.isInteger(prNumber) && prNumber > 0) {
1016
+ return {
1017
+ kind: "valid",
1018
+ value: {
1019
+ provider: "origin",
1020
+ repo: `${parts[1]}/${parts[2]}`,
1021
+ prNumber,
1022
+ },
1023
+ };
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ return {
1029
+ kind: "invalid",
1030
+ detail: "unparseable PR URL: not a GitHub or Origin pull request",
1031
+ };
1032
+ }
1033
+
793
1034
  function parseGithubPrUrl(
794
1035
  prUrl: string
795
1036
  ):
@@ -22,9 +22,10 @@ import type { DeploymentsChannelOptions } from "../channels/deployments/types.js
22
22
  import {
23
23
  type CursorGithubRepoScope,
24
24
  createCursorAccountGithubTokenResolver,
25
+ cursorGithubPermissionsForChannel,
25
26
  type GithubCursorAccountRuntime,
26
27
  getGithubCursorAccountRuntime,
27
- parseCursorGithubRepoScope,
28
+ resolveCursorGithubCredentialScope,
28
29
  resolveCursorGithubPermissions,
29
30
  resolveCursorGithubProxyUrl,
30
31
  } from "../channels/github/cursor-account.js";
@@ -315,65 +316,101 @@ export async function startServer(
315
316
  : requireCursorEventsAuth(effectiveCursorEvents, accountKey);
316
317
 
317
318
  let cursorGithubCredentials: CursorGithubCredentialProvider | undefined;
318
- if (cursorGithubConfig.scope !== undefined) {
319
- if (cursorEventsAuth === undefined) {
320
- throw new Error(
321
- "githubChannel({ cursorAccount: true }) could not resolve Cursor event authentication."
322
- );
323
- }
324
- if (options.allowAnonymous === true) {
325
- if (options.allowAnonymousCursorGithub !== true) {
319
+ if (cursorGithubTargets.length > 0) {
320
+ const githubProxyRequested = options.cursorGithubProxy === true;
321
+ const needsGithubAuth =
322
+ cursorGithubConfig.scope !== undefined || githubProxyRequested;
323
+ if (needsGithubAuth) {
324
+ const cursorGithubAuth =
325
+ cursorEventsAuth ??
326
+ (accountKey?.apiKey !== undefined && accountKey.apiKey.trim() !== ""
327
+ ? {
328
+ apiKey: accountKey.apiKey.trim(),
329
+ baseUrl: cursorExternalApiUrl(),
330
+ }
331
+ : undefined);
332
+ if (cursorGithubAuth === undefined) {
326
333
  throw new Error(
327
- "githubChannel({ cursorAccount: true }) cannot be served with --allow-anonymous because admitted callers could use the signed-in account's GitHub access. If every admitted caller is trusted (e.g. behind an SSO proxy), opt in with --allow-anonymous-cursor-github."
334
+ "githubChannel({ cursorAccount: true }) could not resolve Cursor event authentication."
328
335
  );
329
336
  }
330
- logger(
331
- "[agent-sdk] warning: --allow-anonymous-cursor-github — every admitted HTTP caller can drive sessions holding the signed-in account's repo-scoped GitHub credential; serve this only behind a trusted boundary (e.g. an SSO proxy)"
332
- );
333
- }
334
- if (cursorGithubConfig.scope.permissions === "contents-write") {
335
- logger(
336
- '[agent-sdk] warning: Cursor GitHub credentials include contents:write — the host holds a push-capable token shared with model-driven tools and untrusted webhook content; prefer permissions: "pr-write" unless this agent must push code'
337
- );
338
- }
339
- const provider = new CursorGithubCredentialProvider({
340
- client: new CursorBackendClient({
341
- backendUrl: cursorEventsAuth.baseUrl,
342
- apiKey: cursorEventsAuth.apiKey,
343
- logger,
344
- }),
345
- scope: cursorGithubConfig.scope,
346
- logger,
347
- });
348
- cursorGithubCredentials = provider;
349
- const githubProxyUrl =
350
- options.cursorGithubProxy === true
351
- ? resolveCursorGithubProxyUrl(cursorEventsAuth.baseUrl)
352
- : undefined;
353
- if (githubProxyUrl !== undefined) {
354
- // Proxy mode: channel Octokit calls carry the Cursor API key to the
355
- // backend forwarder, which swaps in the installation token server-side
356
- // and logs every call with per-deployment attribution. The provider
357
- // still runs so child `gh` processes get a `GH_TOKEN` lease (v1).
358
- const proxyTransport = {
359
- apiBaseUrl: githubProxyUrl,
360
- headers: {
361
- "x-cursor-github-repos": cursorGithubConfig.scope.repos.join(","),
362
- "x-cursor-github-permissions": cursorGithubConfig.scope.permissions,
363
- },
364
- };
365
- for (const target of cursorGithubTargets) {
366
- target.accountRuntime.credentials.bind(
367
- async () => cursorEventsAuth.apiKey
337
+ if (options.allowAnonymous === true) {
338
+ if (options.allowAnonymousCursorGithub !== true) {
339
+ throw new Error(
340
+ "githubChannel({ cursorAccount: true }) cannot be served with --allow-anonymous because admitted callers could use the signed-in account's GitHub access. If every admitted caller is trusted (e.g. behind an SSO proxy), opt in with --allow-anonymous-cursor-github."
341
+ );
342
+ }
343
+ logger(
344
+ "[agent-sdk] warning: --allow-anonymous-cursor-github — every admitted HTTP caller can drive sessions holding the signed-in account's repo-scoped GitHub credential; serve this only behind a trusted boundary (e.g. an SSO proxy)"
368
345
  );
369
- target.accountRuntime.credentials.bindProxyTransport(proxyTransport);
370
346
  }
371
- logger(
372
- `[agent-sdk] GitHub auth: channel API calls route through ${githubProxyUrl}`
373
- );
374
- } else {
375
- for (const target of cursorGithubTargets) {
376
- target.accountRuntime.credentials.bind(() => provider.getToken());
347
+ if (cursorGithubConfig.permissions === "contents-write") {
348
+ logger(
349
+ '[agent-sdk] warning: Cursor GitHub credentials include contents:write — the host holds a push-capable token shared with model-driven tools and untrusted webhook content; prefer permissions: "pr-write" unless this agent must push code'
350
+ );
351
+ }
352
+ const githubProxyUrl = githubProxyRequested
353
+ ? resolveCursorGithubProxyUrl(cursorGithubAuth.baseUrl)
354
+ : undefined;
355
+ if (cursorGithubConfig.scope !== undefined) {
356
+ const provider = new CursorGithubCredentialProvider({
357
+ client: new CursorBackendClient({
358
+ backendUrl: cursorGithubAuth.baseUrl,
359
+ apiKey: cursorGithubAuth.apiKey,
360
+ logger,
361
+ }),
362
+ scope: cursorGithubConfig.scope,
363
+ logger,
364
+ });
365
+ cursorGithubCredentials = provider;
366
+ if (githubProxyUrl !== undefined) {
367
+ // Proxy mode: channel Octokit calls carry the Cursor API key to the
368
+ // backend forwarder, which swaps in the installation token server-side
369
+ // and logs every call with per-deployment attribution. The provider
370
+ // still runs so child `gh` processes get a `GH_TOKEN` lease (v1).
371
+ const proxyTransport = {
372
+ apiBaseUrl: githubProxyUrl,
373
+ headers: {
374
+ "x-cursor-github-repos": cursorGithubConfig.scope.repos.join(","),
375
+ "x-cursor-github-permissions": cursorGithubConfig.permissions,
376
+ },
377
+ };
378
+ for (const target of cursorGithubTargets) {
379
+ target.accountRuntime.credentials.bind(
380
+ async () => cursorGithubAuth.apiKey
381
+ );
382
+ target.accountRuntime.credentials.bindProxyTransport(
383
+ proxyTransport
384
+ );
385
+ }
386
+ logger(
387
+ `[agent-sdk] GitHub auth: channel API calls route through ${githubProxyUrl}`
388
+ );
389
+ } else {
390
+ for (const target of cursorGithubTargets) {
391
+ target.accountRuntime.credentials.bind(() => provider.getToken());
392
+ }
393
+ }
394
+ } else if (githubProxyUrl !== undefined) {
395
+ // `cursorAccount: true` with no `--repo`: skip the startup token mint
396
+ // (it requires a repo list) and let github-proxy take the repo from
397
+ // each Octokit call. Hosted multi-tenant uses this with the
398
+ // deployment event stream.
399
+ const proxyTransport = {
400
+ apiBaseUrl: githubProxyUrl,
401
+ headers: {
402
+ "x-cursor-github-permissions": cursorGithubConfig.permissions,
403
+ },
404
+ };
405
+ for (const target of cursorGithubTargets) {
406
+ target.accountRuntime.credentials.bind(
407
+ async () => cursorGithubAuth.apiKey
408
+ );
409
+ target.accountRuntime.credentials.bindProxyTransport(proxyTransport);
410
+ }
411
+ logger(
412
+ `[agent-sdk] GitHub auth: channel API calls route through ${githubProxyUrl} (repo scope per request)`
413
+ );
377
414
  }
378
415
  }
379
416
  }
@@ -2003,6 +2040,20 @@ function collectCursorGithubTargets(
2003
2040
  interface CursorGithubConfigResolution {
2004
2041
  cursorEvents: ServeOptions["cursorEvents"];
2005
2042
  scope: CursorGithubRepoScope | undefined;
2043
+ permissions: CursorGithubRepoScope["permissions"];
2044
+ }
2045
+
2046
+ function cursorGithubPermissionsForTarget(
2047
+ target: CursorGithubTarget
2048
+ ): CursorGithubRepoScope["permissions"] {
2049
+ const authored =
2050
+ typeof target.accountRuntime.options.cursorAccount === "object"
2051
+ ? target.accountRuntime.options.cursorAccount.permissions
2052
+ : undefined;
2053
+ return (
2054
+ target.accountRuntime.scope?.permissions ??
2055
+ cursorGithubPermissionsForChannel(target.accountRuntime.options, authored)
2056
+ );
2006
2057
  }
2007
2058
 
2008
2059
  function resolveCursorGithubEventConfig(input: {
@@ -2010,7 +2061,11 @@ function resolveCursorGithubEventConfig(input: {
2010
2061
  targets: CursorGithubTarget[];
2011
2062
  }): CursorGithubConfigResolution {
2012
2063
  if (input.targets.length === 0) {
2013
- return { cursorEvents: input.configured, scope: undefined };
2064
+ return {
2065
+ cursorEvents: input.configured,
2066
+ scope: undefined,
2067
+ permissions: "pr-write",
2068
+ };
2014
2069
  }
2015
2070
  const repos = [
2016
2071
  ...(input.configured?.repos ?? []),
@@ -2019,17 +2074,23 @@ function resolveCursorGithubEventConfig(input: {
2019
2074
  ]),
2020
2075
  ];
2021
2076
  const permissions = resolveCursorGithubPermissions(
2022
- input.targets.map(
2023
- (target) => target.accountRuntime.scope?.permissions ?? "pr-write"
2024
- )
2077
+ input.targets.map(cursorGithubPermissionsForTarget)
2025
2078
  );
2026
- const scope = parseCursorGithubRepoScope(repos, permissions);
2079
+ const scope = resolveCursorGithubCredentialScope(repos, permissions);
2080
+ if (scope === undefined) {
2081
+ return {
2082
+ cursorEvents: input.configured,
2083
+ scope: undefined,
2084
+ permissions,
2085
+ };
2086
+ }
2027
2087
  return {
2028
2088
  cursorEvents: {
2029
2089
  ...input.configured,
2030
2090
  repos: [...scope.repos],
2031
2091
  },
2032
2092
  scope,
2093
+ permissions,
2033
2094
  };
2034
2095
  }
2035
2096
 
@@ -2078,6 +2139,9 @@ function resolveCursorOriginRepoScope(input: {
2078
2139
  (target) => target.accountRuntime.scope?.repos ?? []
2079
2140
  ),
2080
2141
  ];
2142
+ if (repos.length === 0) {
2143
+ return undefined;
2144
+ }
2081
2145
  return parseCursorOriginRepoScope(repos);
2082
2146
  }
2083
2147
 
@@ -1,6 +1,8 @@
1
1
  // Vendored from the Agentic Owners policy core; see README.md in this
2
2
  // directory. Precedence must stay identical to the source's.
3
3
 
4
+ import { posix } from "node:path";
5
+ import { AGENTIC_OWNERS_DIR } from "./constants.js";
4
6
  import { patternMatchesFile, patternSpecificity } from "./matching.js";
5
7
  import type {
6
8
  AgenticOwnersMatchedConfig,
@@ -12,10 +14,63 @@ type PathScopedRule = {
12
14
  readonly scopeDepth: number;
13
15
  readonly sourcePath: string;
14
16
  readonly paths: readonly string[];
15
- /** Root-only absolute exemption, produced from `global_exclusions`. */
17
+ /** Root-only repository-wide exemption, produced from `global_exclusions`. */
16
18
  readonly globalExclusion?: boolean;
17
19
  };
18
20
 
21
+ /**
22
+ * Convert a repository-relative changed path to the path seen by a policy.
23
+ *
24
+ * `foo/.agentic-owners/policy.yml` owns the `foo/` scope, so its `paths`
25
+ * entries match against filenames relative to `foo/`. Root policies continue
26
+ * to see repository-relative paths. Explicit leading `../` segments can
27
+ * address a sibling tree.
28
+ */
29
+ function filepathRelativeToPolicyScope({
30
+ sourcePath,
31
+ filepath,
32
+ }: {
33
+ sourcePath: string;
34
+ filepath: string;
35
+ }): string {
36
+ const normalizedSourcePath = sourcePath.replace(/^\/+/, "");
37
+ const normalizedFilepath = filepath.replace(/^\/+/, "");
38
+ const marker = `/${AGENTIC_OWNERS_DIR}/`;
39
+ const markerIndex = normalizedSourcePath.lastIndexOf(marker);
40
+ if (markerIndex < 0) {
41
+ return normalizedFilepath;
42
+ }
43
+
44
+ const scope = normalizedSourcePath.slice(0, markerIndex);
45
+ return posix.relative(scope, normalizedFilepath);
46
+ }
47
+
48
+ function patternMatchesFileFromPolicy({
49
+ rule,
50
+ pattern,
51
+ filepath,
52
+ }: {
53
+ rule: PathScopedRule;
54
+ pattern: string;
55
+ filepath: string;
56
+ }): boolean {
57
+ const relativeFilepath = filepathRelativeToPolicyScope({
58
+ sourcePath: rule.sourcePath,
59
+ filepath,
60
+ });
61
+ const filepathIsOutsideScope =
62
+ relativeFilepath === ".." || relativeFilepath.startsWith("../");
63
+ if (filepathIsOutsideScope && !pattern.startsWith("../")) {
64
+ return false;
65
+ }
66
+ const slashlessPatternMatchesOnlyPolicyRoot =
67
+ pattern !== "**" && !pattern.includes("/");
68
+ if (slashlessPatternMatchesOnlyPolicyRoot && relativeFilepath.includes("/")) {
69
+ return false;
70
+ }
71
+ return patternMatchesFile(pattern, relativeFilepath);
72
+ }
73
+
19
74
  function ruleMatchesFile({
20
75
  rule,
21
76
  filepath,
@@ -23,7 +78,13 @@ function ruleMatchesFile({
23
78
  rule: PathScopedRule;
24
79
  filepath: string;
25
80
  }): boolean {
26
- return rule.paths.some((pattern) => patternMatchesFile(pattern, filepath));
81
+ return rule.paths.some((pattern) =>
82
+ patternMatchesFileFromPolicy({
83
+ rule,
84
+ pattern,
85
+ filepath,
86
+ })
87
+ );
27
88
  }
28
89
 
29
90
  function bestMatchingPatternSpecificity(
@@ -32,7 +93,7 @@ function bestMatchingPatternSpecificity(
32
93
  ): number {
33
94
  let best = 0;
34
95
  for (const pattern of rule.paths) {
35
- if (patternMatchesFile(pattern, filepath)) {
96
+ if (patternMatchesFileFromPolicy({ rule, pattern, filepath })) {
36
97
  best = Math.max(best, patternSpecificity(pattern));
37
98
  }
38
99
  }