Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions packages/core/src/links/approval-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});

Expand Down
15 changes: 10 additions & 5 deletions packages/core/src/links/approval-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -72,10 +72,15 @@ export class ApprovalLinkService extends TypedEventEmitter<ApprovalLinkEvents> {
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;

Expand Down
32 changes: 32 additions & 0 deletions packages/shared/src/agent-slug.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
7 changes: 7 additions & 0 deletions packages/shared/src/agent-slug.ts
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/src/features/agent-applications/utils/ingress.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
4 changes: 2 additions & 2 deletions packages/ui/src/features/agent-applications/utils/ingress.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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`;
Expand Down
Loading