@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.
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +8 -1
- package/dist/channels/github/cursor-account.d.ts +17 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +47 -0
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.BsA7t7rS.js → app.e4_wtjUS.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BGiMJpnL.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.B1hHhktM.js → VPLocalSearchBox.DbdzyEie.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DLpLSsQh.js → theme.D8HpLrLK.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/convert-automation.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/opentelemetry.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/artifacts.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/templates/agentic-owners.html +2 -2
- package/dist/docs/templates/demo.html +2 -2
- package/dist/docs/templates/pr-autofixer.html +2 -2
- package/dist/docs/templates/security-reviewer.html +2 -2
- package/dist/docs/templates/triage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/review-comments.d.ts +37 -7
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +209 -10
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +101 -49
- package/package.json +1 -1
- package/src/channels/github/api.ts +8 -0
- package/src/channels/github/cursor-account.ts +56 -0
- package/src/internal/review-comments.ts +257 -16
- package/src/internal/server.ts +124 -60
- package/templates/agentic-owners/agent/lib/owners/match.ts +64 -3
- package/templates/agentic-owners/agent/lib/owners/parse.ts +55 -1
- package/templates/agentic-owners/agent/lib/owners/types.ts +5 -0
- 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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
):
|
package/src/internal/server.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 })
|
|
334
|
+
"githubChannel({ cursorAccount: true }) could not resolve Cursor event authentication."
|
|
328
335
|
);
|
|
329
336
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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) =>
|
|
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 (
|
|
96
|
+
if (patternMatchesFileFromPolicy({ rule, pattern, filepath })) {
|
|
36
97
|
best = Math.max(best, patternSpecificity(pattern));
|
|
37
98
|
}
|
|
38
99
|
}
|