Skip to content

Commit 3abe9ed

Browse files
authored
feat(backend,testing): Add agent tasks endpoints & helpers (#7783)
1 parent 24457b7 commit 3abe9ed

17 files changed

Lines changed: 335 additions & 0 deletions

File tree

.changeset/eighty-bobcats-unite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': minor
3+
---
4+
5+
Add support for Agent Tasks API endpoint which allows developers to create agent tasks that can be used to act on behalf of users through automated flows.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/testing': minor
3+
---
4+
5+
Export `createAgentTestingTask` helper for creating agent tasks via the Clerk Backend API from both `@clerk/testing/playwright` and `@clerk/testing/cypress` subpaths.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { server, validateHeaders } from '../../mock-server';
5+
import { createBackendApiClient } from '../factory';
6+
7+
describe('AgentTaskAPI', () => {
8+
const apiClient = createBackendApiClient({
9+
apiUrl: 'https://api.clerk.test',
10+
secretKey: 'deadbeef',
11+
});
12+
13+
const mockAgentTaskResponse = {
14+
object: 'agent_task',
15+
agent_id: 'agent_123',
16+
task_id: 'task_456',
17+
url: 'https://example.com/agent-task',
18+
};
19+
20+
describe('create', () => {
21+
it('converts nested onBehalfOf.userId to snake_case', async () => {
22+
server.use(
23+
http.post(
24+
'https://api.clerk.test/v1/agents/tasks',
25+
validateHeaders(async ({ request }) => {
26+
const body = await request.json();
27+
28+
expect(body).toEqual({
29+
on_behalf_of: {
30+
user_id: 'user_123',
31+
},
32+
permissions: 'read,write',
33+
agent_name: 'test-agent',
34+
task_description: 'Test task',
35+
redirect_url: 'https://example.com/callback',
36+
session_max_duration_in_seconds: 1800,
37+
});
38+
39+
return HttpResponse.json(mockAgentTaskResponse);
40+
}),
41+
),
42+
);
43+
44+
const response = await apiClient.agentTasks.create({
45+
onBehalfOf: {
46+
userId: 'user_123',
47+
},
48+
permissions: 'read,write',
49+
agentName: 'test-agent',
50+
taskDescription: 'Test task',
51+
redirectUrl: 'https://example.com/callback',
52+
sessionMaxDurationInSeconds: 1800,
53+
});
54+
55+
expect(response.agentId).toBe('agent_123');
56+
expect(response.taskId).toBe('task_456');
57+
expect(response.url).toBe('https://example.com/agent-task');
58+
});
59+
60+
it('converts nested onBehalfOf.identifier to snake_case', async () => {
61+
server.use(
62+
http.post(
63+
'https://api.clerk.test/v1/agents/tasks',
64+
validateHeaders(async ({ request }) => {
65+
const body = await request.json();
66+
67+
expect(body).toEqual({
68+
on_behalf_of: {
69+
identifier: 'user@example.com',
70+
},
71+
permissions: 'read',
72+
agent_name: 'test-agent',
73+
task_description: 'Test task',
74+
redirect_url: 'https://example.com/callback',
75+
});
76+
77+
return HttpResponse.json(mockAgentTaskResponse);
78+
}),
79+
),
80+
);
81+
82+
const response = await apiClient.agentTasks.create({
83+
onBehalfOf: {
84+
identifier: 'user@example.com',
85+
},
86+
permissions: 'read',
87+
agentName: 'test-agent',
88+
taskDescription: 'Test task',
89+
redirectUrl: 'https://example.com/callback',
90+
});
91+
92+
expect(response.agentId).toBe('agent_123');
93+
expect(response.taskId).toBe('task_456');
94+
});
95+
});
96+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { joinPaths } from '../../util/path';
2+
import type { AgentTask } from '../resources/AgentTask';
3+
import { AbstractAPI } from './AbstractApi';
4+
5+
type CreateAgentTaskParams = {
6+
/**
7+
* The user to create an agent task for.
8+
*/
9+
onBehalfOf:
10+
| {
11+
/**
12+
* The identifier of the user to create an agent task for.
13+
*/
14+
identifier: string;
15+
userId?: never;
16+
}
17+
| {
18+
/**
19+
* The ID of the user to create an agent task for.
20+
*/
21+
userId: string;
22+
identifier?: never;
23+
};
24+
/**
25+
* The permissions the agent task will have.
26+
*/
27+
permissions: string;
28+
/**
29+
* The name of the agent to create an agent task for.
30+
*/
31+
agentName: string;
32+
/**
33+
* The description of the agent task to create.
34+
*/
35+
taskDescription: string;
36+
/**
37+
* The URL to redirect to after the agent task is consumed.
38+
*/
39+
redirectUrl: string;
40+
41+
/**
42+
* The maximum duration that the session which will be created by the generated agent task should last.
43+
* By default, the duration is 30 minutes.
44+
*/
45+
sessionMaxDurationInSeconds?: number;
46+
};
47+
48+
const basePath = '/agents/tasks';
49+
50+
export class AgentTaskAPI extends AbstractAPI {
51+
/**
52+
* @experimental This is an experimental API for the Agent Tokens feature that is available under a private beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes.
53+
*/
54+
public async create(params: CreateAgentTaskParams) {
55+
return this.request<AgentTask>({
56+
method: 'POST',
57+
path: basePath,
58+
bodyParams: params,
59+
options: {
60+
deepSnakecaseBodyParamKeys: true,
61+
},
62+
});
63+
}
64+
65+
public async revoke(agentTaskId: string) {
66+
this.requireId(agentTaskId);
67+
return this.request<Omit<AgentTask, 'url'>>({
68+
method: 'POST',
69+
path: joinPaths(basePath, agentTaskId, 'revoke'),
70+
});
71+
}
72+
}

packages/backend/src/api/endpoints/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './ActorTokenApi';
2+
export * from './AgentTaskApi';
23
export * from './AccountlessApplicationsAPI';
34
export * from './AbstractApi';
45
export * from './AllowlistIdentifierApi';

packages/backend/src/api/factory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
AccountlessApplicationAPI,
33
ActorTokenAPI,
4+
AgentTaskAPI,
45
AllowlistIdentifierAPI,
56
APIKeysAPI,
67
BetaFeaturesAPI,
@@ -44,6 +45,10 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
4445
buildRequest({ ...options, requireSecretKey: false }),
4546
),
4647
actorTokens: new ActorTokenAPI(request),
48+
/**
49+
* @experimental This is an experimental API for the Agent Tasks feature that is available under a private beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes.
50+
*/
51+
agentTasks: new AgentTaskAPI(request),
4752
allowlistIdentifiers: new AllowlistIdentifierAPI(request),
4853
apiKeys: new APIKeysAPI(
4954
buildRequest({
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { AgentTaskJSON } from './JSON';
2+
3+
/**
4+
* Represents a agent token resource.
5+
*
6+
* Agent tokens are used for testing purposes and allow creating sessions
7+
* for users without requiring full authentication flows.
8+
*/
9+
export class AgentTask {
10+
constructor(
11+
/**
12+
* A stable identifier for the agent, unique per agent_name within an instance.
13+
*/
14+
readonly agentId: string,
15+
/**
16+
* A unique identifier for this agent task.
17+
*/
18+
readonly taskId: string,
19+
/**
20+
* The FAPI URL that, when visited, creates a session for the user.
21+
*/
22+
readonly url: string,
23+
) {}
24+
25+
/**
26+
* Creates a AgentTask instance from a JSON object.
27+
*
28+
* @param data - The JSON object containing agent task data
29+
* @returns A new AgentTask instance
30+
*/
31+
static fromJSON(data: AgentTaskJSON): AgentTask {
32+
return new AgentTask(data.agent_id, data.task_id, data.url);
33+
}
34+
}

packages/backend/src/api/resources/Deserializer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ActorToken,
3+
AgentTask,
34
AllowlistIdentifier,
45
APIKey,
56
BlocklistIdentifier,
@@ -169,6 +170,8 @@ function jsonToObject(item: any): any {
169170
return SamlConnection.fromJSON(item);
170171
case ObjectType.SignInToken:
171172
return SignInToken.fromJSON(item);
173+
case ObjectType.AgentTask:
174+
return AgentTask.fromJSON(item);
172175
case ObjectType.SignUpAttempt:
173176
return SignUpAttempt.fromJSON(item);
174177
case ObjectType.Session:

packages/backend/src/api/resources/JSON.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
export const ObjectType = {
2020
AccountlessApplication: 'accountless_application',
2121
ActorToken: 'actor_token',
22+
AgentTask: 'agent_task',
2223
AllowlistIdentifier: 'allowlist_identifier',
2324
ApiKey: 'api_key',
2425
BlocklistIdentifier: 'blocklist_identifier',
@@ -512,6 +513,13 @@ export interface SignInTokenJSON extends ClerkResourceJSON {
512513
updated_at: number;
513514
}
514515

516+
export interface AgentTaskJSON extends ClerkResourceJSON {
517+
object: typeof ObjectType.AgentTask;
518+
agent_id: string;
519+
task_id: string;
520+
url: string;
521+
}
522+
515523
export interface SignUpJSON extends ClerkResourceJSON {
516524
object: typeof ObjectType.SignUpAttempt;
517525
id: string;

packages/backend/src/api/resources/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './AccountlessApplication';
2+
export * from './AgentTask';
23
export * from './ActorToken';
34
export * from './AllowlistIdentifier';
45
export * from './APIKey';

0 commit comments

Comments
 (0)