@cursor/july 0.1.84 → 0.1.86

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 (116) hide show
  1. package/dist/channels/deployments/deployments-channel.d.ts +2 -2
  2. package/dist/channels/deployments/deployments-channel.d.ts.map +1 -1
  3. package/dist/channels/deployments/deployments-channel.js +7 -6
  4. package/dist/channels/deployments/index.d.ts +2 -3
  5. package/dist/channels/deployments/index.d.ts.map +1 -1
  6. package/dist/channels/deployments/index.js +2 -3
  7. package/dist/channels/deployments/types.d.ts +14 -14
  8. package/dist/channels/deployments/types.d.ts.map +1 -1
  9. package/dist/channels/github/github-channel.d.ts +4 -0
  10. package/dist/channels/github/github-channel.d.ts.map +1 -1
  11. package/dist/channels/github/github-channel.js +18 -11
  12. package/dist/channels/origin/origin-channel.d.ts +6 -0
  13. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  14. package/dist/channels/origin/origin-channel.js +24 -11
  15. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  16. package/dist/channels/slack/dispatch.js +12 -2
  17. package/dist/docs/404.html +2 -2
  18. package/dist/docs/ab.html +3 -3
  19. package/dist/docs/assets/{app.kqdC1N7p.js → app.DUOPbN18.js} +1 -1
  20. package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +1 -0
  21. package/dist/docs/assets/chunks/{VPLocalSearchBox.DXDE2WgG.js → VPLocalSearchBox.CmWbGcGk.js} +1 -1
  22. package/dist/docs/assets/chunks/{theme.DFYqbyta.js → theme.S57OeOLA.js} +2 -2
  23. package/dist/docs/assets/{reference_channels.md.G5GysZ7e.js → reference_channels.md.CboFd5IH.js} +1 -1
  24. package/dist/docs/building-with-agents.html +3 -3
  25. package/dist/docs/concepts.html +3 -3
  26. package/dist/docs/deployment.html +3 -3
  27. package/dist/docs/evals.html +3 -3
  28. package/dist/docs/example-agents/approval-buddy.html +3 -3
  29. package/dist/docs/example-agents/benny.html +3 -3
  30. package/dist/docs/example-agents/bugbot.html +3 -3
  31. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  32. package/dist/docs/example-agents/codeowners-review.html +3 -3
  33. package/dist/docs/example-agents/concierge.html +3 -3
  34. package/dist/docs/example-agents/fsd.html +3 -3
  35. package/dist/docs/example-agents/index.html +3 -3
  36. package/dist/docs/example-agents/knowledge-base.html +3 -3
  37. package/dist/docs/example-agents/oncall.html +3 -3
  38. package/dist/docs/example-agents/security-reviewer.html +3 -3
  39. package/dist/docs/example-agents/slack-agent.html +3 -3
  40. package/dist/docs/example-agents/weather-agent.html +3 -3
  41. package/dist/docs/guides/agent-to-agent.html +3 -3
  42. package/dist/docs/guides/cloud-runtime.html +3 -3
  43. package/dist/docs/guides/convert-automation.html +3 -3
  44. package/dist/docs/guides/github.html +3 -3
  45. package/dist/docs/guides/human-in-the-loop.html +3 -3
  46. package/dist/docs/guides/mcp-oauth.html +3 -3
  47. package/dist/docs/guides/opentelemetry.html +3 -3
  48. package/dist/docs/guides/slack.html +3 -3
  49. package/dist/docs/guides/webhooks.html +3 -3
  50. package/dist/docs/hashmap.json +1 -1
  51. package/dist/docs/hillclimbing.html +3 -3
  52. package/dist/docs/index.html +3 -3
  53. package/dist/docs/quickstart.html +3 -3
  54. package/dist/docs/reference/agent-config.html +3 -3
  55. package/dist/docs/reference/artifacts.html +3 -3
  56. package/dist/docs/reference/channels.html +5 -5
  57. package/dist/docs/reference/cli.html +3 -3
  58. package/dist/docs/reference/connections.html +3 -3
  59. package/dist/docs/reference/hooks.html +3 -3
  60. package/dist/docs/reference/http-api.html +3 -3
  61. package/dist/docs/reference/instructions.html +3 -3
  62. package/dist/docs/reference/playground.html +3 -3
  63. package/dist/docs/reference/project-layout.html +3 -3
  64. package/dist/docs/reference/prompt.html +3 -3
  65. package/dist/docs/reference/schedules.html +3 -3
  66. package/dist/docs/reference/sessions.html +3 -3
  67. package/dist/docs/reference/skills.html +3 -3
  68. package/dist/docs/reference/subagents.html +3 -3
  69. package/dist/docs/reference/tools.html +3 -3
  70. package/dist/docs/scaffolding-agents.html +3 -3
  71. package/dist/docs/storage.html +3 -3
  72. package/dist/docs/templates/agentic-owners.html +3 -3
  73. package/dist/docs/templates/demo.html +3 -3
  74. package/dist/docs/templates/pr-autofixer.html +3 -3
  75. package/dist/docs/templates/security-reviewer.html +3 -3
  76. package/dist/docs/templates/triage.html +3 -3
  77. package/dist/docs/troubleshooting.html +3 -3
  78. package/dist/internal/continuation-identity.d.ts +10 -0
  79. package/dist/internal/continuation-identity.d.ts.map +1 -1
  80. package/dist/internal/continuation-identity.js +10 -0
  81. package/dist/internal/cursor-deployment-relay.d.ts +4 -4
  82. package/dist/internal/cursor-deployment-relay.d.ts.map +1 -1
  83. package/dist/internal/cursor-deployment-relay.js +33 -32
  84. package/dist/internal/handleAgentServeTrigger.d.ts +5 -0
  85. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  86. package/dist/internal/handleAgentServeTrigger.js +11 -3
  87. package/dist/internal/local-control-plane.d.ts +19 -0
  88. package/dist/internal/local-control-plane.d.ts.map +1 -1
  89. package/dist/internal/local-control-plane.js +17 -0
  90. package/dist/internal/local-ingress.d.ts +35 -0
  91. package/dist/internal/local-ingress.d.ts.map +1 -0
  92. package/dist/internal/local-ingress.js +44 -0
  93. package/dist/internal/server.d.ts.map +1 -1
  94. package/dist/internal/server.js +151 -75
  95. package/dist/playground/assets/{index-Cspgk9jC.js → index-Bqn91tW4.js} +45 -45
  96. package/dist/playground/assets/index-C0_5hOsf.css +1 -0
  97. package/dist/playground/index.html +2 -2
  98. package/dist/types.d.ts +2 -2
  99. package/docs/reference/channels.md +13 -11
  100. package/package.json +1 -1
  101. package/src/channels/deployments/deployments-channel.ts +7 -6
  102. package/src/channels/deployments/index.ts +2 -3
  103. package/src/channels/deployments/types.ts +14 -14
  104. package/src/channels/github/github-channel.ts +18 -21
  105. package/src/channels/origin/origin-channel.ts +26 -15
  106. package/src/channels/slack/dispatch.ts +11 -7
  107. package/src/internal/continuation-identity.ts +10 -0
  108. package/src/internal/cursor-deployment-relay.ts +38 -36
  109. package/src/internal/handleAgentServeTrigger.ts +31 -2
  110. package/src/internal/local-control-plane.ts +33 -0
  111. package/src/internal/local-ingress.ts +90 -0
  112. package/src/internal/server.ts +194 -40
  113. package/src/types.ts +2 -2
  114. package/dist/docs/assets/chunks/@localSearchIndexroot.BqhvUA76.js +0 -1
  115. package/dist/playground/assets/index-ChETdG-_.css +0 -1
  116. /package/dist/docs/assets/{reference_channels.md.G5GysZ7e.lean.js → reference_channels.md.CboFd5IH.lean.js} +0 -0
@@ -8,8 +8,8 @@
8
8
  />
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1" />
10
10
  <title>agent-serve playground</title>
11
- <script type="module" crossorigin src="./assets/index-Cspgk9jC.js"></script>
12
- <link rel="stylesheet" crossorigin href="./assets/index-ChETdG-_.css">
11
+ <script type="module" crossorigin src="./assets/index-Bqn91tW4.js"></script>
12
+ <link rel="stylesheet" crossorigin href="./assets/index-C0_5hOsf.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/dist/types.d.ts CHANGED
@@ -1463,8 +1463,8 @@ export interface SendMessageOptions {
1463
1463
  */
1464
1464
  continuationToken?: string;
1465
1465
  /**
1466
- * v2 mailbox stamp from the control plane (or the pack's builtin extract
1467
- * on local serve). Ignored on v1.
1466
+ * v2 mailbox stamp from the control plane (hosted verified ingress or
1467
+ * the local CP). Channel adapters do not extract. Ignored on v1.
1468
1468
  */
1469
1469
  mailboxContinuation?: MailboxContinuation;
1470
1470
  /**
@@ -218,19 +218,21 @@ converge a merge-box check and sticky PR comment from default stream
218
218
  events. Guide: [GitHub](../guides/github.md).
219
219
 
220
220
  **Deployments** (`@cursor/july/channels/deployments`): pull transport
221
- over `/v0/deployment-events`. Subscribe per repository with `repos`,
222
- narrow with `environments` / `events`, and handle each event in
223
- `onEvent`. `repos` must match `Deployment.repository` as your deployment
224
- writer records it; the field has no format, and matching is
225
- case-insensitive but otherwise literal.
221
+ over `/v0/deployment-events`. Subscribe per deploy source with
222
+ `deploySourceUris`, narrow with `environments` / `events`, and handle
223
+ each event in `onEvent`. `deploySourceUris` must match
224
+ `Deployment.deploy_source_uri` as your deployment writer records it; the
225
+ field has no format, and matching is case-insensitive but otherwise
226
+ literal. Each event carries `deploySourceUri` and `deployVersion`.
226
227
  Author `agent/channels/deployments.ts` with `deploymentsChannel()`. The
227
228
  serve host discovers every mounted deployments channel, runs one relay
228
- for the process against the union of their repositories, and routes each
229
- event to the channels that asked for it the same ownership the Slack
230
- and SCM relays have. It authenticates with the host credential and keeps
231
- a durable offset, so a restart resumes rather than dropping events. An
232
- empty `repos` list mounts the channel but starts no relay for it, so an
233
- env-configured agent stays inert until its repositories are set.
229
+ for the process against the union of their deploy sources, and routes
230
+ each event to the channels that asked for it; the Slack and SCM relays
231
+ use the same ownership. It authenticates with the host credential and
232
+ keeps a durable offset, so a restart resumes rather than dropping
233
+ events. An empty `deploySourceUris` list mounts the channel but starts
234
+ no relay for it, so an env-configured agent stays inert until its
235
+ deploy sources are set.
234
236
 
235
237
  For other platforms like Discord or Teams, use the authored
236
238
  `defineChannel` webhook form.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "repository": {
@@ -14,11 +14,11 @@ const deploymentEventSchema = z.object({
14
14
  eventType: z.enum(DEPLOYMENT_EVENT_TYPES),
15
15
  eventId: z.string().min(1),
16
16
  teamId: z.number(),
17
- repo: z.string().min(1),
17
+ deploySourceUri: z.string().min(1),
18
18
  deploymentId: z.string().min(1),
19
19
  environment: z.string().min(1),
20
20
  service: z.string().min(1).optional(),
21
- sha: z.string().min(1),
21
+ deployVersion: z.string().min(1),
22
22
  status: z.enum(["pending", "in_progress", "success", "failed"]),
23
23
  occurredAtMs: z.number(),
24
24
  });
@@ -60,10 +60,10 @@ export async function dispatchDeploymentEvent(input: {
60
60
  * import { deploymentsChannel } from "@cursor/july/channels/deployments";
61
61
  *
62
62
  * export default deploymentsChannel({
63
- * repos: ["repos/anysphere/everysphere"], // as your writer records it
63
+ * deploySourceUris: ["https://origin.cursor.com/repos/anysphere/everysphere"],
64
64
  * environments: ["prod"],
65
65
  * onEvent: async (event, { send }) => {
66
- * await send(`${event.repo}@${event.sha} shipped to ${event.environment}`);
66
+ * await send(`${event.deploySourceUri}@${event.deployVersion} shipped to ${event.environment}`);
67
67
  * },
68
68
  * });
69
69
  * ```
@@ -81,8 +81,9 @@ export async function dispatchDeploymentEvent(input: {
81
81
  export function deploymentsChannel(
82
82
  options: DeploymentsChannelOptions
83
83
  ): ChannelDefinition {
84
- // Throws here rather than at startup, so a malformed `repos` entry fails
85
- // when the channel module is authored instead of when the host mounts it.
84
+ // Throws here rather than at startup, so a malformed `deploySourceUris`
85
+ // entry fails when the channel module is authored instead of when the host
86
+ // mounts it.
86
87
  resolveDeploymentSubscriptions(options);
87
88
  const routePath = options.path ?? "/";
88
89
 
@@ -5,9 +5,8 @@
5
5
  * import { deploymentsChannel } from "@cursor/july/channels/deployments";
6
6
  *
7
7
  * export default deploymentsChannel({
8
- * repos: ["repos/anysphere/everysphere"], // as your writer records it
9
- * environments: ["prod"],
10
- * onEvent: (event, { send }) => send(`${event.sha} shipped`),
8
+ * deploySourceUris: ["https://origin.cursor.com/repos/anysphere/everysphere"],
9
+ * onEvent: (event, { send }) => send(`${event.deployVersion} shipped`),
11
10
  * });
12
11
  * ```
13
12
  *
@@ -18,25 +18,25 @@ export interface DeploymentEvent {
18
18
  eventId: string;
19
19
  /** Owning Cursor team. */
20
20
  teamId: number;
21
- /** The Deployment's repository string, case-folded. */
22
- repo: string;
21
+ /** The Deployment's deploy_source_uri string, case-folded. */
22
+ deploySourceUri: string;
23
23
  deploymentId: string;
24
24
  /** Environment slug, e.g. `prod`. */
25
25
  environment: string;
26
26
  /** Absent when the deployment records no service. */
27
27
  service?: string;
28
- sha: string;
28
+ deployVersion: string;
29
29
  status: DeploymentStatus;
30
30
  occurredAtMs: number;
31
31
  }
32
32
 
33
- /** What a repository subscription asks for. */
33
+ /** What a deploy-source subscription asks for. */
34
34
  export interface DeploymentSubscription {
35
35
  /**
36
- * The repository string as the factory records it on a Deployment. Opaque:
36
+ * The deploy-source URI as the factory records it on a Deployment. Opaque:
37
37
  * whatever your deployment writer puts there, matched case-insensitively.
38
38
  */
39
- repo: string;
39
+ deploySourceUri: string;
40
40
  /** Omit or leave empty for every environment. */
41
41
  environments?: readonly string[];
42
42
  /** Defaults to `deployment.succeeded`. */
@@ -45,18 +45,18 @@ export interface DeploymentSubscription {
45
45
 
46
46
  export interface DeploymentsChannelOptions {
47
47
  /**
48
- * Repositories to watch, spelled exactly as your deployment writer records
49
- * `Deployment.repository`. The field has no format — whatever the writer
50
- * puts there is the key — and matching is case-insensitive but otherwise
51
- * literal, so a value that does not match subscribes successfully and then
52
- * receives nothing. A bare string takes the default environment and event
53
- * filters.
48
+ * Deploy sources to watch, spelled exactly as your deployment writer records
49
+ * `Deployment.deploy_source_uri`. The field has no format — whatever the
50
+ * writer puts there is the key — and matching is case-insensitive but
51
+ * otherwise literal, so a value that does not match subscribes successfully
52
+ * and then receives nothing. A bare string takes the default environment and
53
+ * event filters.
54
54
  *
55
- * There is no team-wide subscription: every repository is named. At least
55
+ * There is no team-wide subscription: every deploy source is named. At least
56
56
  * one is required — an empty list is how a consumer *deregisters*, so it
57
57
  * cannot also mean "everything".
58
58
  */
59
- repos: readonly (string | DeploymentSubscription)[];
59
+ deploySourceUris: readonly (string | DeploymentSubscription)[];
60
60
  /**
61
61
  * Applied to every subscription that does not set its own. Omit or leave
62
62
  * empty for every environment.
@@ -8,12 +8,11 @@ import {
8
8
  POST,
9
9
  } from "../../channels.js";
10
10
  import {
11
- bindMailboxContinuationSend,
12
11
  continuationSource,
13
12
  normalizeIdentity,
14
- stampMailboxContinuation,
15
13
  } from "../../internal/continuation-identity.js";
16
14
  import { isDevMode } from "../../internal/dev-mode.js";
15
+ import { bindLocalIngressSend } from "../../internal/local-ingress.js";
17
16
  import type {
18
17
  AgentCloudOptions,
19
18
  ArtifactsApi,
@@ -477,6 +476,10 @@ function verifyWebhookSha256Signature(args: {
477
476
  * {@link handleAgentServeTrigger} fan-out. Signature verification is the
478
477
  * caller's responsibility (HTTP route verifies; internal fan-out skips).
479
478
  */
479
+ /**
480
+ * Run authored GitHub hooks and `send()`. Does not extract a v2 mailbox —
481
+ * the local CP (or hosted verified ingress) stamps before this runs.
482
+ */
480
483
  export async function dispatchGitHubWebhook(input: {
481
484
  options: GitHubChannelOptions;
482
485
  eventName: string | null;
@@ -484,23 +487,7 @@ export async function dispatchGitHubWebhook(input: {
484
487
  deliveryId?: string;
485
488
  args: ChannelHandlerArgs;
486
489
  }): Promise<Response> {
487
- const identity = normalizeIdentity(input.options.continuation, "github");
488
- let args = input.args;
489
- if (
490
- continuationSource(identity, "github") === "control-plane" &&
491
- input.eventName !== null
492
- ) {
493
- const mailbox = stampMailboxContinuation(
494
- identity,
495
- input.eventName,
496
- input.payload
497
- );
498
- args = {
499
- ...input.args,
500
- send: bindMailboxContinuationSend(input.args.send, mailbox),
501
- };
502
- }
503
- return dispatchGitHubWebhookInner({ ...input, args });
490
+ return dispatchGitHubWebhookInner(input);
504
491
  }
505
492
 
506
493
  async function dispatchGitHubWebhookInner(input: {
@@ -644,12 +631,22 @@ export async function handleGitHubWebhookRequest(input: {
644
631
  return Response.json({ ok: false, error: "invalid_json" }, { status: 400 });
645
632
  }
646
633
 
634
+ const eventName = input.request.headers.get("x-github-event");
635
+ const args =
636
+ eventName === null
637
+ ? input.args
638
+ : bindLocalIngressSend(input.args, {
639
+ identity: input.options.continuation,
640
+ kind: "github",
641
+ eventType: eventName,
642
+ payload,
643
+ });
647
644
  return dispatchGitHubWebhook({
648
645
  options: input.options,
649
- eventName: input.request.headers.get("x-github-event"),
646
+ eventName,
650
647
  payload,
651
648
  deliveryId: input.request.headers.get("x-github-delivery") ?? undefined,
652
- args: input.args,
649
+ args,
653
650
  });
654
651
  }
655
652
 
@@ -7,12 +7,11 @@ import {
7
7
  POST,
8
8
  } from "../../channels.js";
9
9
  import {
10
- bindMailboxContinuationSend,
11
10
  continuationSource,
12
11
  normalizeIdentity,
13
- stampMailboxContinuation,
14
12
  } from "../../internal/continuation-identity.js";
15
13
  import { isDevMode } from "../../internal/dev-mode.js";
14
+ import { bindLocalIngressSend } from "../../internal/local-ingress.js";
16
15
  import type {
17
16
  AgentCloudOptions,
18
17
  AuthContext,
@@ -364,6 +363,20 @@ export function originContinuationKey(
364
363
  return `origin-pr:${ctx.repository.fullName}#${ctx.pullRequestNumber}`;
365
364
  }
366
365
 
366
+ /** Payload shape local/hosted CP extract expects (`fullName` + number). */
367
+ export function originIngressExtractPayload(
368
+ envelope: OriginWebhookEnvelope
369
+ ): unknown {
370
+ const parsed = parseOriginPullRequestEvent({ envelope });
371
+ if (parsed === null) {
372
+ return envelope.event.payload;
373
+ }
374
+ return {
375
+ pullRequest: { number: parsed.ctx.pullRequestNumber },
376
+ repository: { fullName: parsed.ctx.repository.fullName },
377
+ };
378
+ }
379
+
367
380
  export function defaultOriginTitle(
368
381
  ctx: Pick<OriginEventContext, "repository" | "pullRequestNumber">
369
382
  ): string {
@@ -532,6 +545,10 @@ async function applyDispatch(input: {
532
545
  * {@link handleAgentServeTrigger} fan-out. Signature verification is the
533
546
  * caller's responsibility (HTTP route verifies; internal fan-out skips).
534
547
  */
548
+ /**
549
+ * Run authored Origin hooks and `send()`. Does not extract a v2 mailbox —
550
+ * the local CP (or hosted verified ingress) stamps before this runs.
551
+ */
535
552
  export async function dispatchOriginWebhook(input: {
536
553
  options: OriginChannelOptions;
537
554
  envelope: OriginWebhookEnvelope;
@@ -545,18 +562,7 @@ export async function dispatchOriginWebhook(input: {
545
562
  { status: 400 }
546
563
  );
547
564
  }
548
- const identity = normalizeIdentity(input.options.continuation, "origin");
549
- let args = input.args;
550
- if (continuationSource(identity, "origin") === "control-plane") {
551
- const mailbox = stampMailboxContinuation(identity, eventType, {
552
- pullRequest: { number: parsed.ctx.pullRequestNumber },
553
- repository: { fullName: parsed.ctx.repository.fullName },
554
- });
555
- args = {
556
- ...input.args,
557
- send: bindMailboxContinuationSend(input.args.send, mailbox),
558
- };
559
- }
565
+ const args = input.args;
560
566
  const ctx = enrichEventContext({
561
567
  ctx: parsed.ctx,
562
568
  args,
@@ -711,7 +717,12 @@ export async function handleOriginWebhookRequest(input: {
711
717
  return dispatchOriginWebhook({
712
718
  options: input.options,
713
719
  envelope,
714
- args: input.args,
720
+ args: bindLocalIngressSend(input.args, {
721
+ identity: input.options.continuation,
722
+ kind: "origin",
723
+ eventType: envelope.event.type,
724
+ payload: originIngressExtractPayload(envelope),
725
+ }),
715
726
  });
716
727
  }
717
728
 
@@ -7,8 +7,8 @@ import {
7
7
  continuationSource,
8
8
  type MailboxContinuation,
9
9
  normalizeIdentity,
10
- stampMailboxContinuation,
11
10
  } from "../../internal/continuation-identity.js";
11
+ import { routeLocalIngressContinuation } from "../../internal/local-ingress.js";
12
12
  import type {
13
13
  ArtifactsApi,
14
14
  HostContext,
@@ -208,12 +208,16 @@ function slackSessionContinuation(
208
208
  const key = slackContinuationKey(channelId, threadTs);
209
209
  return { probeKey: key, sendPatch: { continuationToken: key } };
210
210
  }
211
- const mailbox = stampMailboxContinuation(
212
- normalizeIdentity(identity, "slack"),
213
- "message",
214
- { channelId, threadTs },
215
- "slack"
216
- );
211
+ const routed = routeLocalIngressContinuation({
212
+ identity: normalizeIdentity(identity, "slack"),
213
+ kind: "slack",
214
+ eventType: "message",
215
+ payload: { channelId, threadTs },
216
+ });
217
+ const mailbox = routed.mailboxContinuation;
218
+ if (mailbox === undefined) {
219
+ throw new Error("v2 slack ingress did not stamp a mailbox");
220
+ }
217
221
  return { probeKey: mailbox.key, sendPatch: { mailboxContinuation: mailbox } };
218
222
  }
219
223
 
@@ -361,6 +361,16 @@ export function extractContinuation(
361
361
  }
362
362
  }
363
363
 
364
+ /**
365
+ * Extract primitive used by the control plane (hosted
366
+ * `routeVerifiedIngressContinuation` and local
367
+ * `routeLocalIngressContinuation`). Channel adapters must not call this.
368
+ */
369
+ /**
370
+ * Extract primitive used by the control plane (hosted
371
+ * `routeVerifiedIngressContinuation` and local
372
+ * `routeLocalIngressContinuation`). Channel adapters must not call this.
373
+ */
364
374
  export function stampMailboxContinuation(
365
375
  identity: ContinuationIdentity,
366
376
  eventName: string,
@@ -22,13 +22,13 @@ const DEFAULT_EVENTS: readonly DeploymentEventType[] = ["deployment.succeeded"];
22
22
  export type DeploymentRelayEvent = DeploymentEvent & { offset: string };
23
23
 
24
24
  export interface ResolvedSubscription {
25
- repo: string;
25
+ deploySourceUri: string;
26
26
  environments: string[];
27
27
  events: Set<DeploymentEventType>;
28
28
  }
29
29
 
30
30
  interface SubscriptionStatus {
31
- repo: string;
31
+ deploySourceUri: string;
32
32
  isSubscribed: boolean;
33
33
  detail?: string;
34
34
  }
@@ -44,22 +44,22 @@ export interface DeploymentEventRelayOptions {
44
44
  pollWaitMs?: number;
45
45
  }
46
46
 
47
- /** Normalize the channel's `repos` option into concrete subscriptions. */
47
+ /** Normalize the channel's `deploySourceUris` option into concrete subscriptions. */
48
48
  export function resolveDeploymentSubscriptions(options: {
49
- repos: readonly (string | DeploymentSubscription)[];
49
+ deploySourceUris: readonly (string | DeploymentSubscription)[];
50
50
  environments?: readonly string[];
51
51
  events?: readonly DeploymentEventType[];
52
52
  }): ResolvedSubscription[] {
53
- const byRepo = new Map<string, ResolvedSubscription>();
54
- for (const entry of options.repos) {
53
+ const bySource = new Map<string, ResolvedSubscription>();
54
+ for (const entry of options.deploySourceUris) {
55
55
  const subscription: DeploymentSubscription =
56
- typeof entry === "string" ? { repo: entry } : entry;
57
- // Case-folded only: `Deployment.repository` has no documented format, and
58
- // the server keys subscriptions the same way.
59
- const repo = subscription.repo.trim().toLowerCase();
60
- if (repo === "") {
56
+ typeof entry === "string" ? { deploySourceUri: entry } : entry;
57
+ // Case-folded only: `Deployment.deploy_source_uri` has no documented
58
+ // format, and the server keys subscriptions the same way.
59
+ const deploySourceUri = subscription.deploySourceUri.trim().toLowerCase();
60
+ if (deploySourceUri === "") {
61
61
  throw new Error(
62
- `deploymentsChannel: invalid repo "${subscription.repo}".`
62
+ `deploymentsChannel: invalid deploy source "${subscription.deploySourceUri}".`
63
63
  );
64
64
  }
65
65
  const environments = [
@@ -74,14 +74,14 @@ export function resolveDeploymentSubscriptions(options: {
74
74
  );
75
75
  if (events.size === 0) {
76
76
  throw new Error(
77
- `deploymentsChannel: repo "${repo}" subscribes to no events.`
77
+ `deploymentsChannel: deploy source "${deploySourceUri}" subscribes to no events.`
78
78
  );
79
79
  }
80
- // Later entries for the same repo widen the earlier one rather than
81
- // silently replacing it.
82
- const existing = byRepo.get(repo);
80
+ // Later entries for the same deploy source widen the earlier one rather
81
+ // than silently replacing it.
82
+ const existing = bySource.get(deploySourceUri);
83
83
  if (existing === undefined) {
84
- byRepo.set(repo, { repo, environments, events });
84
+ bySource.set(deploySourceUri, { deploySourceUri, environments, events });
85
85
  continue;
86
86
  }
87
87
  for (const event of events) existing.events.add(event);
@@ -91,9 +91,10 @@ export function resolveDeploymentSubscriptions(options: {
91
91
  : [...new Set([...existing.environments, ...environments])];
92
92
  }
93
93
  // Empty is legal here and means "this channel wants nothing", which is how
94
- // an env-configured channel stays inert without a repo list. The wire-level
95
- // empty list means deregister, so the host simply never registers it.
96
- return [...byRepo.values()];
94
+ // an env-configured channel stays inert without a deploy-source list. The
95
+ // wire-level empty list means deregister, so the host simply never
96
+ // registers it.
97
+ return [...bySource.values()];
97
98
  }
98
99
 
99
100
  /** Whether one confirmed subscription wants this event. */
@@ -136,7 +137,7 @@ export async function fanOutDeploymentEvent(input: {
136
137
  const matched = input.targets.filter((target) =>
137
138
  target.subscriptions.some(
138
139
  (subscription) =>
139
- subscription.repo === event.repo &&
140
+ subscription.deploySourceUri === event.deploySourceUri &&
140
141
  deploymentEventMatches(subscription, event)
141
142
  )
142
143
  );
@@ -154,7 +155,7 @@ export async function fanOutDeploymentEvent(input: {
154
155
  }
155
156
  }
156
157
  logger(
157
- `[agent-sdk] deployment-events: ${event.eventType} ${event.repo}@${event.sha} (${event.environment}) → ${matched.length - failures.length}/${matched.length} channel(s)`
158
+ `[agent-sdk] deployment-events: ${event.eventType} ${event.deploySourceUri}@${event.deployVersion} (${event.environment}) → ${matched.length - failures.length}/${matched.length} channel(s)`
158
159
  );
159
160
  if (failures.length > 0 && failures.length === matched.length) {
160
161
  throw new Error(
@@ -166,7 +167,7 @@ export async function fanOutDeploymentEvent(input: {
166
167
  export class DeploymentEventRelay {
167
168
  private readonly core: CursorRelayCore<DeploymentRelayEvent>;
168
169
  private subscribed = new Map<string, ResolvedSubscription>();
169
- private readonly loggedSkippedRepos = new Set<string>();
170
+ private readonly loggedSkippedSources = new Set<string>();
170
171
 
171
172
  constructor(private readonly options: DeploymentEventRelayOptions) {
172
173
  const adapter: RelayAdapter<DeploymentRelayEvent> = {
@@ -176,7 +177,7 @@ export class DeploymentEventRelay {
176
177
  buildRegistrationBody: (consumerId) => ({
177
178
  consumerId,
178
179
  subscriptions: options.subscriptions.map((subscription) => ({
179
- repo: subscription.repo,
180
+ deploySourceUri: subscription.deploySourceUri,
180
181
  environments: subscription.environments,
181
182
  events: [...subscription.events],
182
183
  })),
@@ -188,23 +189,24 @@ export class DeploymentEventRelay {
188
189
  subscriptions: [],
189
190
  }),
190
191
  applyRegistrationResponse: (json) => {
191
- const result = json as { repos?: SubscriptionStatus[] };
192
+ const result = json as { deploySourceUris?: SubscriptionStatus[] };
192
193
  this.subscribed = new Map();
193
- this.loggedSkippedRepos.clear();
194
+ this.loggedSkippedSources.clear();
194
195
  const lines: string[] = [];
195
196
  const confirmed: string[] = [];
196
- for (const status of result.repos ?? []) {
197
+ for (const status of result.deploySourceUris ?? []) {
197
198
  const wanted = options.subscriptions.find(
198
- (subscription) => subscription.repo === status.repo
199
+ (subscription) =>
200
+ subscription.deploySourceUri === status.deploySourceUri
199
201
  );
200
202
  if (!status.isSubscribed || wanted === undefined) {
201
203
  lines.push(
202
- `not subscribed to ${status.repo}: ${status.detail ?? "unknown reason"}`
204
+ `not subscribed to ${status.deploySourceUri}: ${status.detail ?? "unknown reason"}`
203
205
  );
204
206
  continue;
205
207
  }
206
- this.subscribed.set(status.repo, wanted);
207
- confirmed.push(status.repo);
208
+ this.subscribed.set(status.deploySourceUri, wanted);
209
+ confirmed.push(status.deploySourceUri);
208
210
  }
209
211
  if (confirmed.length > 0) {
210
212
  const kinds = [
@@ -247,16 +249,16 @@ export class DeploymentEventRelay {
247
249
  if (
248
250
  envelope === null ||
249
251
  typeof envelope !== "object" ||
250
- typeof envelope.repo !== "string"
252
+ typeof envelope.deploySourceUri !== "string"
251
253
  ) {
252
254
  return false;
253
255
  }
254
- const subscription = this.subscribed.get(envelope.repo);
256
+ const subscription = this.subscribed.get(envelope.deploySourceUri);
255
257
  if (subscription === undefined) {
256
- if (!this.loggedSkippedRepos.has(envelope.repo)) {
257
- this.loggedSkippedRepos.add(envelope.repo);
258
+ if (!this.loggedSkippedSources.has(envelope.deploySourceUri)) {
259
+ this.loggedSkippedSources.add(envelope.deploySourceUri);
258
260
  this.options.logger(
259
- `[agent-sdk] deployment-events: skipping events for ${envelope.repo}: not in this host's subscription (another consumer of this API key likely subscribed it)`
261
+ `[agent-sdk] deployment-events: skipping events for ${envelope.deploySourceUri}: not in this host's subscription (another consumer of this API key likely subscribed it)`
260
262
  );
261
263
  }
262
264
  return false;
@@ -19,10 +19,15 @@ import type { GitHubChannelOptions } from "../channels/github/types.js";
19
19
  import {
20
20
  dispatchOriginWebhook,
21
21
  getOriginChannelOptions,
22
+ originIngressExtractPayload,
22
23
  parseOriginWebhookEnvelope,
23
24
  } from "../channels/origin/origin-channel.js";
24
25
  import type { OriginChannelOptions } from "../channels/origin/types.js";
25
26
  import type { ChannelHandlerArgs } from "../types.js";
27
+ import {
28
+ bindLocalIngressSend,
29
+ type RouteLocalIngressContinuation,
30
+ } from "./local-ingress.js";
26
31
 
27
32
  export interface AgentServeGitHubTriggerTarget {
28
33
  /** Agent mount slug (empty string in single-agent mode). */
@@ -40,6 +45,8 @@ export interface AgentServeGitHubTriggerEvent {
40
45
  eventName: string | null;
41
46
  deliveryId?: string;
42
47
  targets: AgentServeGitHubTriggerTarget[];
48
+ /** Local CP ingest. Defaults to {@link routeLocalIngressContinuation}. */
49
+ routeContinuation?: RouteLocalIngressContinuation;
43
50
  }
44
51
 
45
52
  export interface AgentServeOriginTriggerTarget {
@@ -53,6 +60,8 @@ export interface AgentServeOriginTriggerEvent {
53
60
  /** Origin envelope JSON (signature already verified by the caller). */
54
61
  bodyText: string;
55
62
  targets: AgentServeOriginTriggerTarget[];
63
+ /** Local CP ingest. Defaults to {@link routeLocalIngressContinuation}. */
64
+ routeContinuation?: RouteLocalIngressContinuation;
56
65
  }
57
66
 
58
67
  export type AgentServeTriggerEvent =
@@ -177,12 +186,24 @@ async function handleAgentServeGitHubTrigger(
177
186
 
178
187
  const targets: AgentServeTriggerTargetResult[] = [];
179
188
  for (const target of event.targets) {
189
+ const args =
190
+ event.eventName === null
191
+ ? target.args
192
+ : bindLocalIngressSend(target.args, {
193
+ identity: target.options.continuation,
194
+ kind: "github",
195
+ eventType: event.eventName,
196
+ payload,
197
+ ...(event.routeContinuation === undefined
198
+ ? {}
199
+ : { routeContinuation: event.routeContinuation }),
200
+ });
180
201
  const response = await dispatchGitHubWebhook({
181
202
  options: target.options,
182
203
  eventName: event.eventName,
183
204
  payload,
184
205
  deliveryId: event.deliveryId,
185
- args: target.args,
206
+ args,
186
207
  });
187
208
  targets.push(await triggerTargetResult(target.slug, response));
188
209
  }
@@ -234,7 +255,15 @@ async function handleAgentServeOriginTrigger(
234
255
  const response = await dispatchOriginWebhook({
235
256
  options: target.options,
236
257
  envelope,
237
- args: target.args,
258
+ args: bindLocalIngressSend(target.args, {
259
+ identity: target.options.continuation,
260
+ kind: "origin",
261
+ eventType: envelope.event.type,
262
+ payload: originIngressExtractPayload(envelope),
263
+ ...(event.routeContinuation === undefined
264
+ ? {}
265
+ : { routeContinuation: event.routeContinuation }),
266
+ }),
238
267
  });
239
268
  targets.push(await triggerTargetResult(target.slug, response));
240
269
  }