@cursor/july 0.1.55 → 0.1.61

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 (261) hide show
  1. package/dist/bin/agent-serve.js +30 -23
  2. package/dist/channels/github/github-channel.d.ts +12 -0
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +67 -15
  5. package/dist/channels/github/types.d.ts +27 -1
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/origin/cursor-account.d.ts +22 -0
  8. package/dist/channels/origin/cursor-account.d.ts.map +1 -0
  9. package/dist/channels/origin/cursor-account.js +35 -0
  10. package/dist/channels/origin/events.d.ts +10 -0
  11. package/dist/channels/origin/events.d.ts.map +1 -0
  12. package/dist/channels/origin/events.js +27 -0
  13. package/dist/channels/origin/index.d.ts +29 -0
  14. package/dist/channels/origin/index.d.ts.map +1 -0
  15. package/dist/channels/origin/index.js +28 -0
  16. package/dist/channels/origin/origin-channel.d.ts +68 -0
  17. package/dist/channels/origin/origin-channel.d.ts.map +1 -0
  18. package/dist/channels/origin/origin-channel.js +567 -0
  19. package/dist/channels/origin/origin-webhook.d.ts +38 -0
  20. package/dist/channels/origin/origin-webhook.d.ts.map +1 -0
  21. package/dist/channels/origin/origin-webhook.js +151 -0
  22. package/dist/channels/origin/types.d.ts +152 -0
  23. package/dist/channels/origin/types.d.ts.map +1 -0
  24. package/dist/channels/origin/types.js +1 -0
  25. package/dist/channels/slack/doctor.d.ts +17 -1
  26. package/dist/channels/slack/doctor.d.ts.map +1 -1
  27. package/dist/channels/slack/doctor.js +186 -10
  28. package/dist/docs/404.html +2 -2
  29. package/dist/docs/ab.html +4 -4
  30. package/dist/docs/assets/{app.CVQxhV85.js → app.CW3KJh72.js} +1 -1
  31. package/dist/docs/assets/chunks/@localSearchIndexroot.BuymfBAo.js +1 -0
  32. package/dist/docs/assets/chunks/{VPLocalSearchBox.QAPzJjql.js → VPLocalSearchBox.Bk6EWpPj.js} +1 -1
  33. package/dist/docs/assets/chunks/{theme.CSxTCOFu.js → theme.Bz9AodBA.js} +2 -2
  34. package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.js → example-agents_fsd.md.ZeGEpAw_.js} +2 -2
  35. package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.lean.js → example-agents_fsd.md.ZeGEpAw_.lean.js} +1 -1
  36. package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.js → example-agents_security-reviewer.md.DBL1TwtP.js} +2 -2
  37. package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.lean.js → example-agents_security-reviewer.md.DBL1TwtP.lean.js} +1 -1
  38. package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.js +12 -0
  39. package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.lean.js +1 -0
  40. package/dist/docs/assets/{guides_github.md.B8QKtIpw.js → guides_github.md.CLLQJKRB.js} +4 -4
  41. package/dist/docs/assets/{guides_github.md.B8QKtIpw.lean.js → guides_github.md.CLLQJKRB.lean.js} +1 -1
  42. package/dist/docs/assets/index.md.DhRHS_-L.js +5 -0
  43. package/dist/docs/assets/{index.md.COeoSkcF.lean.js → index.md.DhRHS_-L.lean.js} +1 -1
  44. package/dist/docs/assets/{reference_cli.md.D_CCfG8R.js → reference_cli.md.Cl14Jfo3.js} +8 -2
  45. package/dist/docs/assets/{reference_cli.md.D_CCfG8R.lean.js → reference_cli.md.Cl14Jfo3.lean.js} +1 -1
  46. package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +1 -0
  47. package/dist/docs/assets/{scaffolding-agents.md.mkc3B_ZW.lean.js → scaffolding-agents.md.CrWSA0Ko.lean.js} +1 -1
  48. package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.js +14 -0
  49. package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.lean.js +1 -0
  50. package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.js +4 -0
  51. package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.lean.js +1 -0
  52. package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.js +8 -0
  53. package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.lean.js +1 -0
  54. package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.js +2 -0
  55. package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.lean.js +1 -0
  56. package/dist/docs/building-with-agents.html +4 -4
  57. package/dist/docs/concepts.html +5 -5
  58. package/dist/docs/deployment.html +4 -4
  59. package/dist/docs/evals.html +4 -4
  60. package/dist/docs/example-agents/approval-buddy.html +4 -4
  61. package/dist/docs/example-agents/benny.html +4 -4
  62. package/dist/docs/example-agents/bugbot.html +4 -4
  63. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  64. package/dist/docs/example-agents/codeowners-review.html +4 -4
  65. package/dist/docs/example-agents/concierge.html +4 -4
  66. package/dist/docs/example-agents/fsd.html +5 -5
  67. package/dist/docs/example-agents/index.html +4 -4
  68. package/dist/docs/example-agents/knowledge-base.html +4 -4
  69. package/dist/docs/example-agents/oncall.html +4 -4
  70. package/dist/docs/example-agents/security-reviewer.html +5 -5
  71. package/dist/docs/example-agents/slack-agent.html +4 -4
  72. package/dist/docs/example-agents/weather-agent.html +4 -4
  73. package/dist/docs/guides/agent-to-agent.html +4 -4
  74. package/dist/docs/guides/cloud-runtime.html +4 -4
  75. package/dist/docs/guides/convert-automation.html +36 -0
  76. package/dist/docs/guides/github.html +7 -7
  77. package/dist/docs/guides/human-in-the-loop.html +4 -4
  78. package/dist/docs/guides/mcp-oauth.html +4 -4
  79. package/dist/docs/guides/opentelemetry.html +4 -4
  80. package/dist/docs/guides/slack.html +4 -4
  81. package/dist/docs/guides/webhooks.html +4 -4
  82. package/dist/docs/hashmap.json +1 -1
  83. package/dist/docs/hillclimbing.html +4 -4
  84. package/dist/docs/index.html +6 -6
  85. package/dist/docs/quickstart.html +4 -4
  86. package/dist/docs/reference/agent-config.html +4 -4
  87. package/dist/docs/reference/artifacts.html +4 -4
  88. package/dist/docs/reference/channels.html +4 -4
  89. package/dist/docs/reference/cli.html +12 -6
  90. package/dist/docs/reference/connections.html +4 -4
  91. package/dist/docs/reference/hooks.html +4 -4
  92. package/dist/docs/reference/http-api.html +4 -4
  93. package/dist/docs/reference/instructions.html +4 -4
  94. package/dist/docs/reference/playground.html +4 -4
  95. package/dist/docs/reference/project-layout.html +4 -4
  96. package/dist/docs/reference/prompt.html +4 -4
  97. package/dist/docs/reference/schedules.html +4 -4
  98. package/dist/docs/reference/sessions.html +4 -4
  99. package/dist/docs/reference/skills.html +4 -4
  100. package/dist/docs/reference/subagents.html +4 -4
  101. package/dist/docs/reference/tools.html +4 -4
  102. package/dist/docs/scaffolding-agents.html +5 -5
  103. package/dist/docs/storage.html +4 -4
  104. package/dist/docs/templates/agentic-owners.html +38 -0
  105. package/dist/docs/templates/demo.html +28 -0
  106. package/dist/docs/templates/pr-autofixer.html +32 -0
  107. package/dist/docs/templates/security-reviewer.html +26 -0
  108. package/dist/docs/templates/triage.html +5 -5
  109. package/dist/docs/troubleshooting.html +4 -4
  110. package/dist/index.d.ts +3 -3
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +2 -2
  113. package/dist/internal/cli-ax.d.ts +2 -0
  114. package/dist/internal/cli-ax.d.ts.map +1 -1
  115. package/dist/internal/cli-ax.js +20 -0
  116. package/dist/internal/cli-slack.d.ts +2 -0
  117. package/dist/internal/cli-slack.d.ts.map +1 -1
  118. package/dist/internal/cli-slack.js +11 -2
  119. package/dist/internal/cursor-event-relay.d.ts +59 -11
  120. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  121. package/dist/internal/cursor-event-relay.js +133 -26
  122. package/dist/internal/discovery.d.ts.map +1 -1
  123. package/dist/internal/discovery.js +11 -2
  124. package/dist/internal/distribution.d.ts.map +1 -1
  125. package/dist/internal/distribution.js +1 -0
  126. package/dist/internal/handleAgentServeTrigger.d.ts +33 -6
  127. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  128. package/dist/internal/handleAgentServeTrigger.js +97 -19
  129. package/dist/internal/init-interview.d.ts +67 -0
  130. package/dist/internal/init-interview.d.ts.map +1 -0
  131. package/dist/internal/init-interview.js +300 -0
  132. package/dist/internal/init-project.d.ts +4 -0
  133. package/dist/internal/init-project.d.ts.map +1 -1
  134. package/dist/internal/init-project.js +40 -16
  135. package/dist/internal/mcp-endpoint.d.ts +7 -0
  136. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  137. package/dist/internal/mcp-endpoint.js +7 -0
  138. package/dist/internal/server.d.ts.map +1 -1
  139. package/dist/internal/server.js +159 -57
  140. package/dist/internal/session-engine.d.ts.map +1 -1
  141. package/dist/internal/session-engine.js +20 -21
  142. package/dist/internal/session-links.d.ts +14 -0
  143. package/dist/internal/session-links.d.ts.map +1 -0
  144. package/dist/internal/session-links.js +69 -0
  145. package/dist/internal/sessions-client.d.ts.map +1 -1
  146. package/dist/internal/sessions-client.js +1 -0
  147. package/dist/playground/assets/index-ClbuaLpm.css +1 -0
  148. package/dist/playground/assets/{index-0oqXUkrR.js → index-SqkR13k_.js} +39 -39
  149. package/dist/playground/index.html +2 -2
  150. package/dist/types.d.ts +19 -12
  151. package/dist/types.d.ts.map +1 -1
  152. package/docs/README.md +15 -0
  153. package/docs/example-agents/fsd.md +3 -0
  154. package/docs/example-agents/security-reviewer.md +3 -0
  155. package/docs/guides/convert-automation.md +176 -0
  156. package/docs/guides/github.md +6 -2
  157. package/docs/reference/cli.md +34 -10
  158. package/docs/scaffolding-agents.md +3 -0
  159. package/docs/templates/agentic-owners.md +97 -0
  160. package/docs/templates/demo.md +84 -0
  161. package/docs/templates/pr-autofixer.md +136 -0
  162. package/docs/templates/security-reviewer.md +89 -0
  163. package/package.json +9 -1
  164. package/skills/github/SKILL.md +4 -5
  165. package/src/bin/agent-serve.ts +13 -6
  166. package/src/channels/github/github-channel.ts +95 -18
  167. package/src/channels/github/types.ts +31 -0
  168. package/src/channels/origin/cursor-account.ts +64 -0
  169. package/src/channels/origin/events.ts +41 -0
  170. package/src/channels/origin/index.ts +29 -0
  171. package/src/channels/origin/origin-channel.ts +784 -0
  172. package/src/channels/origin/origin-webhook.ts +202 -0
  173. package/src/channels/origin/types.ts +186 -0
  174. package/src/channels/slack/doctor.ts +218 -5
  175. package/src/index.ts +3 -1
  176. package/src/internal/cli-ax.ts +23 -0
  177. package/src/internal/cli-slack.ts +11 -1
  178. package/src/internal/cursor-event-relay.ts +193 -31
  179. package/src/internal/discovery.ts +15 -5
  180. package/src/internal/distribution.ts +1 -0
  181. package/src/internal/handleAgentServeTrigger.ts +129 -19
  182. package/src/internal/init-interview.ts +384 -0
  183. package/src/internal/init-project.ts +70 -15
  184. package/src/internal/mcp-endpoint.ts +7 -0
  185. package/src/internal/server.ts +188 -58
  186. package/src/internal/session-engine.ts +5 -21
  187. package/src/internal/session-links.ts +88 -0
  188. package/src/internal/sessions-client.ts +1 -0
  189. package/src/types.ts +19 -12
  190. package/templates/agentic-owners/README.md +73 -0
  191. package/templates/agentic-owners/agent/agent.ts +15 -0
  192. package/templates/agentic-owners/agent/channels/github.ts +85 -0
  193. package/templates/agentic-owners/agent/instructions.md +34 -0
  194. package/templates/agentic-owners/agent/lib/config.ts +13 -0
  195. package/templates/agentic-owners/agent/lib/github.ts +241 -0
  196. package/templates/agentic-owners/agent/lib/owners/README.md +50 -0
  197. package/templates/agentic-owners/agent/lib/owners/constants.ts +22 -0
  198. package/templates/agentic-owners/agent/lib/owners/decision.ts +485 -0
  199. package/templates/agentic-owners/agent/lib/owners/handles.ts +54 -0
  200. package/templates/agentic-owners/agent/lib/owners/match.ts +177 -0
  201. package/templates/agentic-owners/agent/lib/owners/matching.ts +95 -0
  202. package/templates/agentic-owners/agent/lib/owners/parse.ts +589 -0
  203. package/templates/agentic-owners/agent/lib/owners/policyMode.ts +65 -0
  204. package/templates/agentic-owners/agent/lib/owners/prompt.ts +66 -0
  205. package/templates/agentic-owners/agent/lib/owners/prompts/base-review.md +105 -0
  206. package/templates/agentic-owners/agent/lib/owners/types.ts +45 -0
  207. package/templates/agentic-owners/agent/lib/review.ts +777 -0
  208. package/templates/agentic-owners/agent/tools/record_review.ts +69 -0
  209. package/templates/agentic-owners/evals/evals.config.ts +5 -0
  210. package/templates/agentic-owners/evals/owners.eval.ts +116 -0
  211. package/templates/agentic-owners/package.json +20 -0
  212. package/templates/agentic-owners/tsconfig.json +12 -0
  213. package/templates/demo/README.md +33 -0
  214. package/templates/demo/agent/agent.ts +15 -0
  215. package/templates/demo/agent/channels/github.ts +50 -0
  216. package/templates/demo/agent/channels/queue.ts +30 -0
  217. package/templates/demo/agent/instructions.md +8 -0
  218. package/templates/demo/agent/lib/collect.ts +78 -0
  219. package/templates/demo/agent/tools/record_demo.ts +40 -0
  220. package/templates/demo/evals/demo.eval.ts +33 -0
  221. package/templates/demo/evals/evals.config.ts +5 -0
  222. package/templates/demo/package.json +18 -0
  223. package/templates/demo/tsconfig.json +12 -0
  224. package/templates/pr-autofixer/README.md +44 -0
  225. package/templates/pr-autofixer/agent/agent.ts +22 -0
  226. package/templates/pr-autofixer/agent/channels/drive.ts +40 -0
  227. package/templates/pr-autofixer/agent/channels/github.ts +167 -0
  228. package/templates/pr-autofixer/agent/channels/slack.ts +17 -0
  229. package/templates/pr-autofixer/agent/hooks/bind-agent.ts +19 -0
  230. package/templates/pr-autofixer/agent/instructions.md +83 -0
  231. package/templates/pr-autofixer/agent/lib/affinity.ts +33 -0
  232. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +15 -0
  233. package/templates/pr-autofixer/agent/lib/drive.ts +79 -0
  234. package/templates/pr-autofixer/agent/lib/merge-watch.ts +123 -0
  235. package/templates/pr-autofixer/agent/lib/pr-state.ts +54 -0
  236. package/templates/pr-autofixer/agent/lib/repos.ts +19 -0
  237. package/templates/pr-autofixer/agent/lib/start-drive.ts +122 -0
  238. package/templates/pr-autofixer/agent/lib/webhook-buffer.ts +225 -0
  239. package/templates/pr-autofixer/agent/lib/webhook-routing.ts +231 -0
  240. package/templates/pr-autofixer/agent/storage.ts +7 -0
  241. package/templates/pr-autofixer/agent/tools/drive_pr.ts +22 -0
  242. package/templates/pr-autofixer/evals/coordinator.eval.ts +16 -0
  243. package/templates/pr-autofixer/evals/evals.config.ts +5 -0
  244. package/templates/pr-autofixer/init.json +25 -0
  245. package/templates/pr-autofixer/package.json +18 -0
  246. package/templates/pr-autofixer/tsconfig.json +12 -0
  247. package/templates/security-reviewer/README.md +31 -0
  248. package/templates/security-reviewer/agent/agent.ts +15 -0
  249. package/templates/security-reviewer/agent/channels/github.ts +46 -0
  250. package/templates/security-reviewer/agent/instructions.md +22 -0
  251. package/templates/security-reviewer/agent/lib/repos.ts +5 -0
  252. package/templates/security-reviewer/agent/tools/post_findings.ts +72 -0
  253. package/templates/security-reviewer/evals/evals.config.ts +5 -0
  254. package/templates/security-reviewer/evals/review.eval.ts +53 -0
  255. package/templates/security-reviewer/init.json +25 -0
  256. package/templates/security-reviewer/package.json +18 -0
  257. package/templates/security-reviewer/tsconfig.json +12 -0
  258. package/dist/docs/assets/chunks/@localSearchIndexroot.BFpWSo6D.js +0 -1
  259. package/dist/docs/assets/index.md.COeoSkcF.js +0 -5
  260. package/dist/docs/assets/scaffolding-agents.md.mkc3B_ZW.js +0 -1
  261. package/dist/playground/assets/index-tHCjJxv4.css +0 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Origin webhook verification (Standard Webhooks `v1ed`).
3
+ *
4
+ * Direct Origin HTTP deliveries must verify the Ed25519 signature
5
+ * (`webhook-id`, `webhook-timestamp`, `webhook-signature`) against Origin's
6
+ * JWKS. Cursor-events relay deliveries skip this — the platform already
7
+ * verified the webhook before enqueueing.
8
+ *
9
+ * The signed content matches the Standard Webhooks `v1` scheme
10
+ * (`${webhook-id}.${webhook-timestamp}.${body}`) but is hashed to a lowercase
11
+ * hex SHA-256 before signing. Lifted from
12
+ * `factory/agentic-owners/agent/lib/origin-webhook.ts` /
13
+ * `backend/server/src/origin/webhooks/asymmetricWebhookSignature.ts`.
14
+ */
15
+ import {
16
+ createHash,
17
+ createPublicKey,
18
+ type JsonWebKeyInput,
19
+ type KeyObject,
20
+ verify as verifyEd25519,
21
+ } from "node:crypto";
22
+
23
+ export const V1ED_SIGNATURE_VERSION = "v1ed";
24
+ export const DEFAULT_ORIGIN_API_BASE_URL = "https://origin.cursor.com";
25
+
26
+ /** ±5 minutes, the Standard Webhooks replay-window convention. */
27
+ const V1ED_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60;
28
+
29
+ /**
30
+ * The sender emits exactly one v1ed entry; key rotation is handled by trying
31
+ * multiple public keys, never multiple signatures.
32
+ */
33
+ const MAX_V1ED_CANDIDATE_SIGNATURES = 1;
34
+
35
+ const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
36
+
37
+ export type OriginJwk = Record<string, unknown>;
38
+
39
+ export type OriginWebhookHeaders = {
40
+ "webhook-id"?: string;
41
+ "webhook-timestamp"?: string;
42
+ "webhook-signature"?: string;
43
+ };
44
+
45
+ export function buildV1edSignedContentDigest(args: {
46
+ webhookId: string;
47
+ timestampSeconds: number;
48
+ body: string | Buffer;
49
+ }): string {
50
+ const prefix = Buffer.from(
51
+ `${args.webhookId}.${args.timestampSeconds}.`,
52
+ "utf8"
53
+ );
54
+ const body =
55
+ typeof args.body === "string" ? Buffer.from(args.body, "utf8") : args.body;
56
+ return createHash("sha256")
57
+ .update(Buffer.concat([prefix, body]))
58
+ .digest("hex");
59
+ }
60
+
61
+ /**
62
+ * Verify a delivery's Standard Webhooks headers against the Origin signing
63
+ * JWKS. Owns the whole check: header presence, timestamp parsing, the replay
64
+ * window, and Ed25519 verification against any provided key.
65
+ */
66
+ export function verifyV1edWebhookPayload(args: {
67
+ publicJwks: readonly OriginJwk[];
68
+ body: string | Buffer;
69
+ headers: OriginWebhookHeaders;
70
+ nowMs?: number;
71
+ }): boolean {
72
+ const webhookId = args.headers["webhook-id"];
73
+ const webhookTimestamp = args.headers["webhook-timestamp"];
74
+ const signatureHeader = args.headers["webhook-signature"];
75
+ if (
76
+ webhookId === undefined ||
77
+ webhookTimestamp === undefined ||
78
+ signatureHeader === undefined
79
+ ) {
80
+ return false;
81
+ }
82
+ const timestampSeconds = Number.parseInt(webhookTimestamp, 10);
83
+ if (Number.isNaN(timestampSeconds)) {
84
+ return false;
85
+ }
86
+ const nowSeconds = Math.floor((args.nowMs ?? Date.now()) / 1000);
87
+ if (
88
+ Math.abs(nowSeconds - timestampSeconds) > V1ED_TIMESTAMP_TOLERANCE_SECONDS
89
+ ) {
90
+ return false;
91
+ }
92
+
93
+ const digestBytes = Buffer.from(
94
+ buildV1edSignedContentDigest({
95
+ webhookId,
96
+ timestampSeconds,
97
+ body: args.body,
98
+ }),
99
+ "utf8"
100
+ );
101
+ const candidateSignatures = signatureHeader
102
+ .split(/\s+/)
103
+ .filter((entry) => entry.startsWith(`${V1ED_SIGNATURE_VERSION},`))
104
+ .map((entry) => entry.slice(V1ED_SIGNATURE_VERSION.length + 1))
105
+ .slice(0, MAX_V1ED_CANDIDATE_SIGNATURES);
106
+ if (candidateSignatures.length === 0) {
107
+ return false;
108
+ }
109
+
110
+ for (const jwk of args.publicJwks) {
111
+ let publicKey: KeyObject;
112
+ try {
113
+ publicKey = createPublicKey({
114
+ key: jwk,
115
+ format: "jwk",
116
+ } as JsonWebKeyInput);
117
+ } catch {
118
+ continue;
119
+ }
120
+ for (const candidate of candidateSignatures) {
121
+ const signature = Buffer.from(candidate, "base64");
122
+ try {
123
+ if (verifyEd25519(null, digestBytes, publicKey, signature)) {
124
+ return true;
125
+ }
126
+ } catch {
127
+ // Malformed signature bytes or a non-Ed25519 key: try the next pair.
128
+ }
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+
134
+ let jwksCache:
135
+ | { apiBaseUrl: string; keys: OriginJwk[]; fetchedAtMs: number }
136
+ | undefined;
137
+
138
+ /** Fetch (and cache for 5 minutes) the Origin signing JWKS. */
139
+ export async function fetchOriginJwks(
140
+ apiBaseUrl: string,
141
+ fetchImpl: typeof fetch = fetch
142
+ ): Promise<readonly OriginJwk[]> {
143
+ const now = Date.now();
144
+ if (
145
+ jwksCache !== undefined &&
146
+ jwksCache.apiBaseUrl === apiBaseUrl &&
147
+ now - jwksCache.fetchedAtMs < JWKS_CACHE_TTL_MS
148
+ ) {
149
+ return jwksCache.keys;
150
+ }
151
+ const response = await fetchImpl(
152
+ `${apiBaseUrl.replace(/\/+$/, "")}/v1/origin/keys`
153
+ );
154
+ if (!response.ok) {
155
+ throw new Error(`Origin JWKS fetch failed: status=${response.status}`);
156
+ }
157
+ const body: unknown = await response.json().catch(() => undefined);
158
+ const keys =
159
+ typeof body === "object" &&
160
+ body !== null &&
161
+ Array.isArray((body as Record<string, unknown>).keys)
162
+ ? ((body as Record<string, unknown>).keys as OriginJwk[])
163
+ : [];
164
+ if (keys.length === 0) {
165
+ throw new Error("Origin JWKS fetch returned no keys");
166
+ }
167
+ jwksCache = { apiBaseUrl, keys, fetchedAtMs: now };
168
+ return keys;
169
+ }
170
+
171
+ export interface VerifyOriginWebhookOptions {
172
+ apiBaseUrl?: string;
173
+ fetchImpl?: typeof fetch;
174
+ nowMs?: number;
175
+ }
176
+
177
+ /**
178
+ * Verify a Standard-Webhooks v1ed Origin payload. Throws on failure.
179
+ */
180
+ export async function verifyOriginWebhook(
181
+ body: string | Buffer,
182
+ headers: OriginWebhookHeaders,
183
+ options?: VerifyOriginWebhookOptions
184
+ ): Promise<void> {
185
+ const apiBaseUrl = options?.apiBaseUrl ?? DEFAULT_ORIGIN_API_BASE_URL;
186
+ const keys = await fetchOriginJwks(apiBaseUrl, options?.fetchImpl ?? fetch);
187
+ if (
188
+ !verifyV1edWebhookPayload({
189
+ publicJwks: keys,
190
+ body,
191
+ headers,
192
+ nowMs: options?.nowMs,
193
+ })
194
+ ) {
195
+ throw new Error("Origin webhook signature verification failed");
196
+ }
197
+ }
198
+
199
+ /** Test helper: drop the in-process JWKS cache. */
200
+ export function resetOriginWebhookKeysCache(): void {
201
+ jwksCache = undefined;
202
+ }
@@ -0,0 +1,186 @@
1
+ import type {
2
+ AgentCloudOptions,
3
+ ArtifactsApi,
4
+ AuthContext,
5
+ AuthPolicy,
6
+ ChannelStartArgs,
7
+ HostContext,
8
+ } from "../../types.js";
9
+
10
+ export interface OriginActor {
11
+ id: string;
12
+ kind?: string;
13
+ }
14
+
15
+ export interface OriginRepositoryRef {
16
+ owner: string;
17
+ name: string;
18
+ fullName: string;
19
+ }
20
+
21
+ export interface OriginPullRequestEvent {
22
+ type: string;
23
+ number: number;
24
+ url: string;
25
+ title?: string;
26
+ body?: string | null;
27
+ draft?: boolean;
28
+ merged?: boolean;
29
+ authorId?: string;
30
+ headRef?: string;
31
+ headSha?: string;
32
+ baseRef?: string;
33
+ baseSha?: string;
34
+ }
35
+
36
+ export interface OriginComment {
37
+ id: string;
38
+ body: string;
39
+ author?: OriginActor;
40
+ }
41
+
42
+ export interface OriginReview {
43
+ id?: string;
44
+ verdict?: string;
45
+ author?: OriginActor;
46
+ }
47
+
48
+ export interface OriginEventContext {
49
+ deliveryId?: string;
50
+ eventType: string;
51
+ repository: OriginRepositoryRef;
52
+ pullRequestNumber: number;
53
+ pullRequestUrl: string;
54
+ sender?: OriginActor;
55
+ host: HostContext;
56
+ artifacts: ArtifactsApi;
57
+ }
58
+
59
+ export type OriginTurnDispatch = {
60
+ auth: AuthContext;
61
+ message?: string;
62
+ title?: string;
63
+ context?: readonly string[];
64
+ };
65
+
66
+ export type OriginTaskDispatch = { task: () => Promise<void> };
67
+
68
+ export type OriginDispatch = OriginTurnDispatch | OriginTaskDispatch;
69
+
70
+ export type OriginDispatchResult =
71
+ | OriginDispatch
72
+ | null
73
+ | Promise<OriginDispatch | null>;
74
+
75
+ export type OriginPullRequestHook = (
76
+ ctx: OriginEventContext,
77
+ pullRequest: OriginPullRequestEvent
78
+ ) => OriginDispatchResult;
79
+
80
+ export type OriginCommentHook = (
81
+ ctx: OriginEventContext,
82
+ comment: OriginComment
83
+ ) => OriginDispatchResult;
84
+
85
+ export type OriginReviewHook = (
86
+ ctx: OriginEventContext,
87
+ review: OriginReview
88
+ ) => OriginDispatchResult;
89
+
90
+ export type OriginEventHook = (
91
+ ctx: OriginEventContext,
92
+ event: { eventType: string; payload: unknown }
93
+ ) => OriginDispatchResult;
94
+
95
+ export type OriginCloudOptionsResolver = (
96
+ ctx: OriginEventContext,
97
+ pullRequest: OriginPullRequestEvent
98
+ ) => AgentCloudOptions | undefined | Promise<AgentCloudOptions | undefined>;
99
+
100
+ /**
101
+ * Origin delivery envelope. Public payloads are proto3 camelCase
102
+ * (`pullRequest`, `repository.owner.slug`).
103
+ */
104
+ export interface OriginWebhookEnvelope {
105
+ deliveryId?: string;
106
+ appId?: string;
107
+ installationId?: string;
108
+ sender?: OriginActor;
109
+ event: {
110
+ id?: string;
111
+ type: string;
112
+ eventTime?: string;
113
+ payload?: unknown;
114
+ };
115
+ }
116
+
117
+ export interface OriginChannelOptions {
118
+ /**
119
+ * Use the signed-in Cursor account for inbound Origin events via
120
+ * `/v0/scm-events` (`provider: "origin"`). `true` takes repository scope
121
+ * from `serve --cursor-events --repo`; `{ repos }` makes the channel
122
+ * self-contained. Origin identity is the Cursor user — there is no
123
+ * separate OAuth principal, and this does not mint a GitHub token.
124
+ */
125
+ cursorAccount?: boolean | { repos: readonly string[] };
126
+ /**
127
+ * Origin app id this channel accepts (`envelope.appId`). v1ed only proves
128
+ * Origin signed the body with the platform JWKS; it does not bind the
129
+ * delivery to one app (any Origin app can aim signed POSTs at a public
130
+ * URL). Required for non-loopback HTTP, same role as GitHub's per-channel
131
+ * webhook secret. Falls back to `AGENT_SERVE_ORIGIN_APP_ID`.
132
+ */
133
+ appId?: string | readonly string[];
134
+ /**
135
+ * Optional Origin installation id bind (`envelope.installationId`). When
136
+ * set, HTTP deliveries for any other installation are rejected.
137
+ */
138
+ installationId?: string | readonly string[];
139
+ /**
140
+ * Optional HTTP/dispatch repo allowlist (`owner/name`). `{ cursorAccount:
141
+ * { repos } }` is also an allowlist. Events outside the set are acked
142
+ * without starting a turn. Omit to accept every repo the bound app sends.
143
+ */
144
+ repos?: readonly string[];
145
+ /**
146
+ * Route auth chain. Defaults to allow-all when {@link appId} (or
147
+ * `AGENT_SERVE_ORIGIN_APP_ID`) is set, because v1ed plus that app-id bind
148
+ * is the request auth. Without an app id, production stays loopback-strict
149
+ * (GitHub's no-secret posture); `--dev` / skip-verify admits loopback only.
150
+ */
151
+ auth?: AuthPolicy[];
152
+ /** Route path under `/v1/channels/<name>`. Defaults to `/`. */
153
+ path?: string;
154
+ /** Origin API base used to fetch `/v1/origin/keys` for v1ed verify. */
155
+ apiBaseUrl?: string | (() => string | undefined);
156
+ /**
157
+ * Pin the Origin event types this channel dispatches on (published as
158
+ * `meta.originWebhookEvents`). Defaults to the set derived from hooks.
159
+ */
160
+ webhookEvents?: readonly string[];
161
+ /**
162
+ * Decide whether an inbound pull-request event starts a turn. Return
163
+ * `{ auth: defaultOriginAuth(ctx) }` to dispatch, `null` to ignore.
164
+ * Without this hook the channel dispatches on created / published /
165
+ * head_ref.pushed / base_ref.updated.
166
+ */
167
+ onPullRequest?: OriginPullRequestHook;
168
+ /** Opt-in `pull_request.comment.*` handler. No default dispatch. */
169
+ onComment?: OriginCommentHook;
170
+ /** Opt-in `pull_request.review.*` handler. No default dispatch. */
171
+ onReview?: OriginReviewHook;
172
+ /**
173
+ * Catch-all for Origin event types without a typed hook. NOT a fallback:
174
+ * events with a typed hook never reach this one.
175
+ */
176
+ onEvent?: OriginEventHook;
177
+ /**
178
+ * Per-session cloud options. Pass `true` to attach the PR repo using the
179
+ * pull request URL, or a resolver for custom cloud options.
180
+ */
181
+ cloud?: boolean | AgentCloudOptions | OriginCloudOptionsResolver;
182
+ /** Called once when the server mounts this channel. */
183
+ onStart?: (args: ChannelStartArgs) => void | Promise<void>;
184
+ /** Called when the server shuts down. */
185
+ onStop?: () => void | Promise<void>;
186
+ }
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Slack channel doctor — verify env, Socket Mode app token, auth.test.
2
+ * Slack channel doctor — verify env, Socket Mode app token, auth.test, and
3
+ * (when watched channels are supplied) that the bot token's workspace can
4
+ * actually see, join, and read those channels.
3
5
  */
4
6
 
5
7
  import { callSlackApi, resolveAppToken, resolveBotToken } from "./api.js";
@@ -16,12 +18,163 @@ export interface SlackDoctorResult {
16
18
  ok: boolean;
17
19
  checks: SlackDoctorCheck[];
18
20
  envKeys: { bot: string; app: string };
21
+ /**
22
+ * Things a bot token cannot prove. Event subscriptions are app-level
23
+ * config, so a fully green doctor can still be a silent agent.
24
+ */
25
+ notes?: string[];
19
26
  }
20
27
 
21
28
  export interface SlackDoctorOptions {
22
29
  credentials?: SlackCredentials;
23
30
  /** Per-agent env prefix (e.g. `MY_AGENT`). */
24
31
  envPrefix?: string;
32
+ /**
33
+ * Watched channel ids (`engagement.channelPosts.allow`). Each is checked
34
+ * for visibility, bot membership, and history scope.
35
+ */
36
+ channels?: readonly string[];
37
+ /** Test seams. */
38
+ callImpl?: typeof callSlackApi;
39
+ fetchImpl?: typeof fetch;
40
+ }
41
+
42
+ /**
43
+ * Socket Mode only delivers bot events the app subscribes to. Missing
44
+ * subscriptions produce total silence with valid tokens, and cannot be read
45
+ * back from a bot token.
46
+ */
47
+ const REQUIRED_BOT_EVENTS: Readonly<Record<string, string>> = {
48
+ app_mention: "@mentions of the bot",
49
+ "message.im": "direct messages",
50
+ "message.channels": "public channel posts",
51
+ "message.groups": "private channel posts",
52
+ reaction_added: "reaction handlers (onReactionAdded)",
53
+ };
54
+
55
+ function eventSubscriptionNote(): string {
56
+ const lines = Object.entries(REQUIRED_BOT_EVENTS).map(
57
+ ([event, purpose]) => ` ${event} — ${purpose}`
58
+ );
59
+ return [
60
+ "A bot token cannot read Event Subscriptions. If every check above passes",
61
+ "but the bot is silent, verify these bot events are subscribed on the app",
62
+ "(Slack app config -> Event Subscriptions -> Subscribe to bot events):",
63
+ ...lines,
64
+ ].join("\n");
65
+ }
66
+
67
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
68
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
69
+ return undefined;
70
+ }
71
+ return value as Record<string, unknown>;
72
+ }
73
+
74
+ function errorOf(response: Record<string, unknown>): string {
75
+ return typeof response.error === "string" ? response.error : "unknown";
76
+ }
77
+
78
+ /**
79
+ * Visibility, membership, and history-scope checks for one watched channel.
80
+ * `channel_not_found` here is the classic wrong-workspace bot token: the
81
+ * token authenticates fine, it just belongs to a different installation.
82
+ */
83
+ async function checkChannel(args: {
84
+ call: typeof callSlackApi;
85
+ botToken: string;
86
+ channelId: string;
87
+ }): Promise<SlackDoctorCheck[]> {
88
+ const { call, botToken, channelId } = args;
89
+ const checks: SlackDoctorCheck[] = [];
90
+
91
+ let info: Record<string, unknown>;
92
+ try {
93
+ info = await call({
94
+ botToken,
95
+ operation: "conversations.info",
96
+ body: { channel: channelId },
97
+ });
98
+ } catch (error) {
99
+ checks.push({
100
+ id: `channel_visible:${channelId}`,
101
+ ok: false,
102
+ message: `conversations.info threw: ${
103
+ error instanceof Error ? error.message : String(error)
104
+ }`,
105
+ });
106
+ return checks;
107
+ }
108
+
109
+ if (info.ok !== true) {
110
+ const error = errorOf(info);
111
+ checks.push({
112
+ id: `channel_visible:${channelId}`,
113
+ ok: false,
114
+ message:
115
+ error === "channel_not_found"
116
+ ? `${channelId} not found by this bot token — the token is probably installed on a different workspace, or the channel id is wrong`
117
+ : `conversations.info failed: ${error}`,
118
+ });
119
+ return checks;
120
+ }
121
+
122
+ const channel = asRecord(info.channel);
123
+ const name = typeof channel?.name === "string" ? channel.name : "?";
124
+ checks.push({
125
+ id: `channel_visible:${channelId}`,
126
+ ok: true,
127
+ message: `${channelId} visible (#${name})`,
128
+ });
129
+
130
+ const isMember = channel?.is_member === true;
131
+ checks.push({
132
+ id: `channel_member:${channelId}`,
133
+ ok: isMember,
134
+ message: isMember
135
+ ? `bot is a member of ${channelId}`
136
+ : `bot is NOT a member of ${channelId} — invite it to the channel`,
137
+ });
138
+
139
+ let history: Record<string, unknown>;
140
+ try {
141
+ history = await call({
142
+ botToken,
143
+ operation: "conversations.history",
144
+ body: { channel: channelId, limit: 1 },
145
+ });
146
+ } catch (error) {
147
+ checks.push({
148
+ id: `channel_history:${channelId}`,
149
+ ok: false,
150
+ message: `conversations.history threw: ${
151
+ error instanceof Error ? error.message : String(error)
152
+ }`,
153
+ });
154
+ return checks;
155
+ }
156
+
157
+ if (history.ok === true) {
158
+ checks.push({
159
+ id: `channel_history:${channelId}`,
160
+ ok: true,
161
+ message: `conversations.history ok for ${channelId}`,
162
+ });
163
+ return checks;
164
+ }
165
+
166
+ const error = errorOf(history);
167
+ const needed =
168
+ typeof history.needed === "string" ? history.needed : undefined;
169
+ checks.push({
170
+ id: `channel_history:${channelId}`,
171
+ ok: false,
172
+ message:
173
+ error === "missing_scope"
174
+ ? `conversations.history missing scope${needed === undefined ? "" : ` ${needed}`} — add it and reinstall the app`
175
+ : `conversations.history failed: ${error}`,
176
+ });
177
+ return checks;
25
178
  }
26
179
 
27
180
  export async function runSlackDoctor(
@@ -31,6 +184,8 @@ export async function runSlackDoctor(
31
184
  options.credentials ?? credentialsFromEnv(options.envPrefix);
32
185
  const envKeys = slackEnvKeys(options.envPrefix);
33
186
  const checks: SlackDoctorCheck[] = [];
187
+ const call = options.callImpl ?? callSlackApi;
188
+ const fetchImpl = options.fetchImpl ?? fetch;
34
189
 
35
190
  let appTokenRaw: string | undefined;
36
191
  try {
@@ -48,7 +203,7 @@ export async function runSlackDoctor(
48
203
  });
49
204
  if (hasApp && appTokenRaw !== undefined) {
50
205
  try {
51
- const response = await fetch(
206
+ const response = await fetchImpl(
52
207
  "https://slack.com/api/apps.connections.open",
53
208
  {
54
209
  method: "POST",
@@ -100,18 +255,25 @@ export async function runSlackDoctor(
100
255
  message: hasToken ? `${envKeys.bot} present` : `${envKeys.bot} is missing`,
101
256
  });
102
257
 
258
+ let authOk = false;
259
+ let botUserId = "";
103
260
  if (hasToken) {
104
261
  try {
105
- const auth = await callSlackApi({
262
+ const auth = await call({
106
263
  botToken,
107
264
  operation: "auth.test",
108
265
  body: {},
109
266
  });
110
267
  if (auth.ok === true) {
268
+ authOk = true;
269
+ botUserId = typeof auth.user_id === "string" ? auth.user_id : "";
111
270
  checks.push({
112
271
  id: "auth_test",
113
272
  ok: true,
114
- message: `auth.test ok (team=${String(auth.team ?? "?")}, user=${String(auth.user ?? "?")})`,
273
+ message:
274
+ `auth.test ok (team=${String(auth.team ?? "?")}` +
275
+ `, team_id=${String(auth.team_id ?? "?")}` +
276
+ `, user=${String(auth.user ?? "?")})`,
115
277
  });
116
278
  } else {
117
279
  checks.push({
@@ -131,5 +293,56 @@ export async function runSlackDoctor(
131
293
  }
132
294
  }
133
295
 
134
- return { ok: checks.every((check) => check.ok), checks, envKeys };
296
+ // The external-sender policy (`blockExternals`, on by default) classifies a
297
+ // sender as external whenever `users.info` fails — so a token that cannot
298
+ // look up users silently drops EVERY inbound event while all other checks
299
+ // stay green. Probe with the bot's own user id from auth.test.
300
+ if (authOk && botToken !== undefined && botUserId !== "") {
301
+ let info: Record<string, unknown> | undefined;
302
+ let threw: string | undefined;
303
+ try {
304
+ info = await call({
305
+ botToken,
306
+ operation: "users.info",
307
+ body: { user: botUserId },
308
+ });
309
+ } catch (error) {
310
+ threw = error instanceof Error ? error.message : String(error);
311
+ }
312
+ if (info?.ok === true) {
313
+ checks.push({
314
+ id: "users_read",
315
+ ok: true,
316
+ message:
317
+ "users.info ok (users:read granted; external-sender policy can classify senders)",
318
+ });
319
+ } else {
320
+ const error = threw ?? (info === undefined ? "unknown" : errorOf(info));
321
+ const hint =
322
+ error === "missing_scope"
323
+ ? "add the users:read scope and reinstall the app"
324
+ : error === "user_not_found"
325
+ ? "the token cannot see its own workspace's users — probably a token from a different installation"
326
+ : "fix the token";
327
+ checks.push({
328
+ id: "users_read",
329
+ ok: false,
330
+ message: `users.info failed: ${error} — ${hint}. The external-sender policy fails open to "external" on users.info errors, so every inbound event is silently dropped.`,
331
+ });
332
+ }
333
+ }
334
+
335
+ const channels = options.channels ?? [];
336
+ if (authOk && botToken !== undefined && channels.length > 0) {
337
+ for (const channelId of channels) {
338
+ checks.push(...(await checkChannel({ call, botToken, channelId })));
339
+ }
340
+ }
341
+
342
+ return {
343
+ ok: checks.every((check) => check.ok),
344
+ checks,
345
+ envKeys,
346
+ notes: [eventSubscriptionNote()],
347
+ };
135
348
  }
package/src/index.ts CHANGED
@@ -65,12 +65,15 @@ export {
65
65
  export type {
66
66
  AgentServeGitHubTriggerEvent,
67
67
  AgentServeGitHubTriggerTarget,
68
+ AgentServeOriginTriggerEvent,
69
+ AgentServeOriginTriggerTarget,
68
70
  AgentServeTriggerEvent,
69
71
  AgentServeTriggerResult,
70
72
  AgentServeTriggerTargetResult,
71
73
  } from "./internal/handleAgentServeTrigger.js";
72
74
  export {
73
75
  collectGitHubTriggerTargets,
76
+ collectOriginTriggerTargets,
74
77
  handleAgentServeTrigger,
75
78
  } from "./internal/handleAgentServeTrigger.js";
76
79
  export {
@@ -94,7 +97,6 @@ export {
94
97
  } from "./internal/session-engine.js";
95
98
  export { createMemoryKv, memoryCasTable } from "./kv/memory-kv.js";
96
99
  export {
97
- createLedgerJanitor,
98
100
  LedgerCasExhaustedError,
99
101
  LedgerFencedError,
100
102
  SessionLedger,