From a9f1d2092275e9f8c5c88dc76d6788123e827083 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Wed, 29 Jul 2026 16:24:29 +0200 Subject: [PATCH 1/2] fix(ci): upgrade PostHog CLI for release races --- package.json | 2 +- pnpm-lock.yaml | 56 ++++++-------------------------------------------- 2 files changed, 7 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 7c42b04b08..47a7ec0465 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "license": "MIT", "devDependencies": { "@biomejs/biome": "2.2.4", - "@posthog/cli": "^0.7.3", + "@posthog/cli": "^0.8.2", "fflate": "^0.8.2", "husky": "^9.1.7", "knip": "^5.66.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4909cf2c9a..28baa3b7d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,8 +110,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@posthog/cli': - specifier: ^0.7.3 - version: 0.7.5 + specifier: ^0.8.2 + version: 0.8.4 fflate: specifier: ^0.8.2 version: 0.8.2 @@ -5656,9 +5656,9 @@ packages: engines: {node: '>=14.14', npm: '>=6'} hasBin: true - '@posthog/cli@0.7.5': - resolution: {integrity: sha512-yX0tU2z5zXJZqyzT+qko3r/kNvuejUlyoAU7Og5S9zeyuwPJcWuFpcF4D+NN0aswawRH6O7gdS9R6FZ01wvakw==} - engines: {node: '>=14', npm: '>=6'} + '@posthog/cli@0.8.4': + resolution: {integrity: sha512-NLoLF6j+dcnBrbTUOYPUaOR4hj+bniXEyPJ3loDu3/HAOZo0nLasVd6Y+YiM4BTynrygvHT+KZUNZpRVLHzxoQ==} + engines: {node: '>=14.14', npm: '>=6'} hasBin: true '@posthog/core@1.20.0': @@ -8680,9 +8680,6 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} - axios-proxy-builder@0.1.2: - resolution: {integrity: sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==} - axios@1.15.0: resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} @@ -9255,10 +9252,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - console.table@0.10.0: - resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==} - engines: {node: '> 0.10'} - content-disposition@1.0.1: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} @@ -9865,9 +9858,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - easy-table@1.1.0: - resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==} - ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -13910,11 +13900,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.1.3: - resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} - engines: {node: 20 || >=22} - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -14813,10 +14798,6 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.9.18: resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==} hasBin: true @@ -19814,15 +19795,9 @@ snapshots: dependencies: detect-libc: 2.1.2 - '@posthog/cli@0.7.5': + '@posthog/cli@0.8.4': dependencies: - axios: 1.15.0 - axios-proxy-builder: 0.1.2 - console.table: 0.10.0 detect-libc: 2.1.2 - rimraf: 6.1.3 - transitivePeerDependencies: - - debug '@posthog/core@1.20.0': dependencies: @@ -23056,10 +23031,6 @@ snapshots: axe-core@4.11.1: {} - axios-proxy-builder@0.1.2: - dependencies: - tunnel: 0.0.6 - axios@1.15.0: dependencies: follow-redirects: 1.15.11 @@ -23729,10 +23700,6 @@ snapshots: consola@3.4.2: {} - console.table@0.10.0: - dependencies: - easy-table: 1.1.0 - content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -24133,10 +24100,6 @@ snapshots: eastasianwidth@0.2.0: {} - easy-table@1.1.0: - optionalDependencies: - wcwidth: 1.0.1 - ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -29590,11 +29553,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.1.3: - dependencies: - glob: 13.0.6 - package-json-from-dist: 1.0.1 - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -30704,8 +30662,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - tunnel@0.0.6: {} - turbo@2.9.18: optionalDependencies: '@turbo/darwin-64': 2.9.18 From 797fac9231813e5a3d95731b887b974a328297f3 Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Wed, 29 Jul 2026 15:36:44 +0100 Subject: [PATCH 2/2] fix(agents): validate the agent slug before building the ingress URL The agent slug becomes a subdomain label in the ingress URL (`.agents..posthog.com`), but it arrived from an `?agent=` approval deep link and was interpolated without any validation, so a slug that isn't a plain label could change where the resulting request goes. Validate it as a single DNS label in two places: the deep-link handler drops a malformed slug so the link falls back to the fleet Approvals inbox, and `agentIngressBaseUrl` returns null rather than interpolating one, which keeps callers on their existing "no ingress" path. --- packages/core/src/links/approval-link.test.ts | 21 ++++++++---- packages/core/src/links/approval-link.ts | 15 ++++++--- packages/shared/src/agent-slug.test.ts | 32 +++++++++++++++++++ packages/shared/src/agent-slug.ts | 7 ++++ packages/shared/src/index.ts | 1 + .../agent-applications/utils/ingress.test.ts | 21 ++++++++++++ .../agent-applications/utils/ingress.ts | 4 +-- 7 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 packages/shared/src/agent-slug.test.ts create mode 100644 packages/shared/src/agent-slug.ts create mode 100644 packages/ui/src/features/agent-applications/utils/ingress.test.ts diff --git a/packages/core/src/links/approval-link.test.ts b/packages/core/src/links/approval-link.test.ts index 26b67393e7..3e40abb412 100644 --- a/packages/core/src/links/approval-link.test.ts +++ b/packages/core/src/links/approval-link.test.ts @@ -102,20 +102,27 @@ describe("ApprovalLinkService", () => { expect(listener).toHaveBeenCalledWith(expected); }); - it("carries the agent slug from the ?agent= query string", () => { + it.each<{ name: string; search: string; agent: string | null }>([ + { + name: "carries the agent slug from the ?agent= query string", + search: "agent=my-agent", + agent: "my-agent", + }, + { + name: "drops a slug that is not a single DNS label", + search: "agent=evil.com%2F", + agent: null, + }, + ])("$name", ({ search, agent }) => { const listener = vi.fn(); service.on(ApprovalLinkEvent.OpenApproval, listener); - const result = deepLinkService.trigger( - "approval", - "ar_abc123", - "agent=my-agent", - ); + const result = deepLinkService.trigger("approval", "ar_abc123", search); expect(result).toBe(true); expect(listener).toHaveBeenCalledWith({ requestId: "ar_abc123", - agent: "my-agent", + agent, }); }); diff --git a/packages/core/src/links/approval-link.ts b/packages/core/src/links/approval-link.ts index 74f7f7ffda..44f422d03e 100644 --- a/packages/core/src/links/approval-link.ts +++ b/packages/core/src/links/approval-link.ts @@ -7,7 +7,7 @@ import { type IMainWindow, MAIN_WINDOW_SERVICE, } from "@posthog/platform/main-window"; -import { TypedEventEmitter } from "@posthog/shared"; +import { isValidAgentSlug, TypedEventEmitter } from "@posthog/shared"; import { inject, injectable } from "inversify"; import type { LinkLogger } from "./identifiers"; @@ -72,10 +72,15 @@ export class ApprovalLinkService extends TypedEventEmitter { return false; } - const payload: ApprovalLinkPayload = { - requestId, - agent: searchParams.get("agent") || null, - }; + const rawAgent = searchParams.get("agent"); + const agent = isValidAgentSlug(rawAgent) ? rawAgent : null; + if (rawAgent && !agent) { + this.log.warn( + "Approval link carried a malformed agent slug; ignoring it", + ); + } + + const payload: ApprovalLinkPayload = { requestId, agent }; const hasListeners = this.listenerCount(ApprovalLinkEvent.OpenApproval) > 0; diff --git a/packages/shared/src/agent-slug.test.ts b/packages/shared/src/agent-slug.test.ts new file mode 100644 index 0000000000..0c81e36428 --- /dev/null +++ b/packages/shared/src/agent-slug.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { isValidAgentSlug } from "./agent-slug"; + +describe("isValidAgentSlug", () => { + it.each(["a", "my-agent", "agent123", "A1-b2", "x".repeat(63)])( + "accepts the DNS label %s", + (slug) => { + expect(isValidAgentSlug(slug)).toBe(true); + }, + ); + + it.each([ + "evil.com", + "evil.com/", + "evil.com#x", + "evil.com?x", + "evil.com:9999/", + "evil.com\\x", + "user@evil.com", + "foo/../bar", + "foo bar", + "-lead", + "trail-", + "under_score", + "x".repeat(64), + "", + null, + undefined, + ])("rejects %s", (slug) => { + expect(isValidAgentSlug(slug)).toBe(false); + }); +}); diff --git a/packages/shared/src/agent-slug.ts b/packages/shared/src/agent-slug.ts new file mode 100644 index 0000000000..91e02c0304 --- /dev/null +++ b/packages/shared/src/agent-slug.ts @@ -0,0 +1,7 @@ +export const AGENT_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; + +export function isValidAgentSlug( + slug: string | null | undefined, +): slug is string { + return !!slug && AGENT_SLUG_PATTERN.test(slug); +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 6d3b6b608a..f6d15bc1ae 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -19,6 +19,7 @@ export type { AgentToolKind, } from "./agent-conversation"; export * from "./agent-runtime"; +export { AGENT_SLUG_PATTERN, isValidAgentSlug } from "./agent-slug"; export * from "./analytics-events"; export { type ArchivedTask, archivedTaskSchema } from "./archive-domain"; export { withTimeout } from "./async"; diff --git a/packages/ui/src/features/agent-applications/utils/ingress.test.ts b/packages/ui/src/features/agent-applications/utils/ingress.test.ts new file mode 100644 index 0000000000..877881cccc --- /dev/null +++ b/packages/ui/src/features/agent-applications/utils/ingress.test.ts @@ -0,0 +1,21 @@ +import type { CloudRegion } from "@posthog/shared"; +import { describe, expect, it } from "vitest"; +import { agentIngressBaseUrl } from "./ingress"; + +describe("agentIngressBaseUrl", () => { + it.each<{ region: CloudRegion; expected: string }>([ + { region: "us", expected: "https://my-agent.agents.us.posthog.com" }, + { region: "eu", expected: "https://my-agent.agents.eu.posthog.com" }, + { region: "dev", expected: "http://localhost:3030/agents/my-agent" }, + ])("builds the $region URL for a valid slug", ({ region, expected }) => { + expect(agentIngressBaseUrl("my-agent", region)).toBe(expected); + }); + + it.each(["evil.com/", ""])("refuses to interpolate the slug %s", (slug) => { + expect(agentIngressBaseUrl(slug, "us")).toBeNull(); + }); + + it("returns null without a region", () => { + expect(agentIngressBaseUrl("my-agent", null)).toBeNull(); + }); +}); diff --git a/packages/ui/src/features/agent-applications/utils/ingress.ts b/packages/ui/src/features/agent-applications/utils/ingress.ts index cf5af5c19c..f4f1a11a68 100644 --- a/packages/ui/src/features/agent-applications/utils/ingress.ts +++ b/packages/ui/src/features/agent-applications/utils/ingress.ts @@ -1,4 +1,4 @@ -import type { CloudRegion } from "@posthog/shared"; +import { type CloudRegion, isValidAgentSlug } from "@posthog/shared"; /** * Resolve the agent-ingress base URL for live (streaming) calls, derived per @@ -41,7 +41,7 @@ export function agentIngressBaseUrl( slug: string, region: CloudRegion | null, ): string | null { - if (!slug || !region) return null; + if (!isValidAgentSlug(slug) || !region) return null; switch (region) { case "us": return `https://${slug}.agents.us.posthog.com`;