diff --git a/cloud/apps/push/src/push-delivery-message.ts b/cloud/apps/push/src/push-delivery-message.ts index 3bd2dae6e7c..1d37f621573 100644 --- a/cloud/apps/push/src/push-delivery-message.ts +++ b/cloud/apps/push/src/push-delivery-message.ts @@ -6,6 +6,7 @@ export type PushOrcaData = { hostFingerprint: string worktreeId?: string paneKey?: string + executionHost?: string notificationId?: string notificationSeq: number notificationEpoch: string @@ -54,6 +55,9 @@ export function buildPushDelivery(input: { hostFingerprint, ...(notification.paneKey === undefined ? {} : { paneKey: notification.paneKey }), ...(notification.worktreeId === undefined ? {} : { worktreeId: notification.worktreeId }), + ...(notification.executionHost === undefined + ? {} + : { executionHost: notification.executionHost }), ...(notification.notificationId === undefined ? {} : { notificationId: notification.notificationId }), diff --git a/cloud/apps/push/src/push-execution-host-routing.test.ts b/cloud/apps/push/src/push-execution-host-routing.test.ts new file mode 100644 index 00000000000..fcc688e6bcc --- /dev/null +++ b/cloud/apps/push/src/push-execution-host-routing.test.ts @@ -0,0 +1,35 @@ +import { expect, it } from 'vitest' +import { PushNotificationSchema } from '@orca-cloud/push-contract' +import { buildPushDelivery, orcaDataStrings } from './push-delivery-message.js' + +const base = { + notificationSeq: 1, + notificationEpoch: 'epoch', + source: 'agent-task-complete', + agentState: 'finished', + title: 'Done', + body: '', + worktreeId: 'repo-1::/srv/work' +} + +it('carries the workspace server to APNs and FCM, and accepts older messages without one', () => { + const executionHost = 'runtime:env%201' + for (const extra of [{}, { executionHost }]) { + const delivery = buildPushDelivery({ + notification: PushNotificationSchema.parse({ ...base, ...extra }), + hostFingerprint: 'host', + registrationId: 'phone', + expiresAt: Date.now() + 300000 + }) + const expected = 'executionHost' in extra ? executionHost : undefined + expect(delivery.orca.executionHost).toBe(expected) + expect(orcaDataStrings(delivery.orca).executionHost).toBe(expected) + } +}) + +it('accepts only runtime execution hosts', () => { + const invalid = ['local', 'ssh:box', 'runtime:', 'runtime:a|b', `runtime:${'x'.repeat(2048)}`] + for (const executionHost of invalid) { + expect(PushNotificationSchema.safeParse({ ...base, executionHost }).success).toBe(false) + } +}) diff --git a/cloud/packages/push-contract/src/send-messages.ts b/cloud/packages/push-contract/src/send-messages.ts index a8959c18b05..61e4c1b9166 100644 --- a/cloud/packages/push-contract/src/send-messages.ts +++ b/cloud/packages/push-contract/src/send-messages.ts @@ -27,7 +27,13 @@ export const PushNotificationSchema = z title: z.string().min(1).max(PUSH_LIMITS.titleMaxChars), body: z.string().max(PUSH_LIMITS.bodyMaxChars), worktreeId: z.string().min(1).max(2048).optional(), - paneKey: z.string().min(1).max(2048).optional() + paneKey: z.string().min(1).max(2048).optional(), + // The workspace's server; mirrors the app's runtime host id (cannot import src/shared). + executionHost: z + .string() + .max(2048) + .regex(/^runtime:[^|\s]+$/) + .optional() }) .strict() .refine(