Skip to content
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ npm i @digitalocean/dots

https://digitaloceandots.readthedocs.io/en/latest/

#### **Action Gateway**

Use `@digitalocean/dots/action_gateway` for session-bound tools with Chat
Completions, Messages, and Responses. Toolbelt CRUD is generated from the
public DigitalOcean OpenAPI specification, with a `createToolbelt` convenience
method on `ActionGatewayClient`.

See the [Action Gateway guide](docs/action-gateway.md) and
Comment thread
SSharma-10 marked this conversation as resolved.
Outdated
[TypeScript examples](examples/action-gateway/).

## **Basic Usage**
> A quick guide to getting started with client
#### Authenticating
Expand Down
13 changes: 13 additions & 0 deletions examples/action-gateway/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

const gateway = new ActionGatewayClient({
apiKey: process.env.DIGITALOCEAN_TOKEN!,
});
const session = await gateway.session.create({ actorId: "end-user-123" });

const [tools, catalog] = await Promise.all([
session.tools(),
session.toolsOperations.list({ includeAll: true }),
]);

console.log(`Loaded ${tools.length} model tools and ${catalog.length} session tools.`);
36 changes: 36 additions & 0 deletions examples/action-gateway/chat-completions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Client } from "../../src/inference-gen/inference.js";
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

const apiKey = process.env.DIGITALOCEAN_TOKEN!;
const inference = new Client({ apiKey });
const gateway = new ActionGatewayClient({ apiKey });
const session = await gateway.session.create({
actorId: "end-user-123",
permissions: {
defaultAction: "ask",
rules: [
{ tool: "exa_web_search", action: "allow" },
{ tool: "exa_web_fetch", action: "allow" },
],
},
});

const messages: Record<string, unknown>[] = [{
role: "user",
content: "Find the latest DigitalOcean news and summarize it.",
}];

while (true) {
const response = await inference.chat.completions.create({
model: "llama3.3-70b-instruct",
messages,
tools: await session.tools(),
});
const message = response.choices[0].message;
messages.push(message);
if (!message.tool_calls?.length) {
console.log(message.content);
break;
}
messages.push(...await session.handleToolCalls(response));
}
27 changes: 27 additions & 0 deletions examples/action-gateway/create-toolbelt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

const gateway = new ActionGatewayClient({
apiKey: process.env.DIGITALOCEAN_TOKEN!,
});

const toolbelt = await gateway.createToolbelt({
name: "search-toolbelt",
tools: ["exa_web_search", "exa_web_fetch"],
});

console.log(toolbelt.ref); // search-toolbelt@1

// The base CRUD surface is generated from the public OpenAPI specification.
await gateway.toolbelts.get({ queryParameters: { status: "active" } });
await gateway.toolbelts.byName("search-toolbelt").get({
queryParameters: { version: "1" },
});
await gateway.toolbelts.byName("search-toolbelt").tools.add.post({
tools: ["jira_create_issue"],
});
await gateway.toolbelts.byName("search-toolbelt").tools.remove.post({
tools: ["exa_web_fetch"],
});

// Delete the toolbelt when it is no longer needed.
// await gateway.toolbelts.byName("search-toolbelt").delete();
26 changes: 26 additions & 0 deletions examples/action-gateway/direct-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

const gateway = new ActionGatewayClient({
apiKey: process.env.DIGITALOCEAN_TOKEN!,
});
const session = await gateway.session.create({
actorId: "end-user-123",
tools: ["exa_web_search@v1", "execute_code@v1"],
config: { preloadTools: ["exa_web_search@v1"] },
permissions: {
defaultAction: "ask",
rules: [
{ tool: "exa_web_search", action: "allow" },
{ tool: "execute_code", action: "allow" },
],
},
});

const search = await session.toolsOperations.search("search the web for DigitalOcean news");
const result = await session.toolsOperations.invokeOne("exa_web_search", {
query: "DigitalOcean news",
max_results: 5,
});
const code = await session.code.execute("print(sum(range(10)))");

console.dir({ search, result, code }, { depth: null });
31 changes: 31 additions & 0 deletions examples/action-gateway/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Client } from "../../src/inference-gen/inference.js";
import {
ActionGatewayClient,
MessagesProvider,
} from "../../src/action-gateway/index.js";

const apiKey = process.env.DIGITALOCEAN_TOKEN!;
const inference = new Client({ apiKey });
const gateway = new ActionGatewayClient({
apiKey,
provider: new MessagesProvider(),
});
const session = await gateway.session.create({
actorId: "end-user-123",
permissions: {
defaultAction: "ask",
rules: [
{ tool: "exa_web_search", action: "allow" },
{ tool: "exa_web_fetch", action: "allow" },
],
},
});

const response = await inference.messages.create({
model: "anthropic-claude-sonnet-4",
max_tokens: 1024,
messages: [{ role: "user", content: "Find the latest DigitalOcean news." }],
tools: await session.tools(),
});

console.dir(await session.handleToolCalls(response), { depth: null });
78 changes: 78 additions & 0 deletions examples/action-gateway/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";
import type { Create_connection_request } from "../../src/dots/models/index.js";

const gateway = new ActionGatewayClient({
apiKey: process.env.DIGITALOCEAN_TOKEN!,
});
const actorId = process.env.ACTOR_ID ?? "example-user";

// Public Tool Registry APIs are generated from DigitalOcean's OpenAPI spec.
console.log("Tools:", await gateway.tools.get({
queryParameters: { toolkitId: "exa" },
}));
console.log("Toolkits:", await gateway.tools.toolkits.get());
console.log("Providers:", await gateway.tools.providers.get());
console.log("Definition:", await gateway.tools.byName("exa_web_search").definition.get({
queryParameters: { version: "v1" },
}));

// Toolbelts support create, list, get, membership changes, and delete.
console.log("Created toolbelt:", await gateway.toolbelts.post({
name: "search-toolbelt",
tools: ["exa_web_search"],
}));
console.log("Toolbelts:", await gateway.toolbelts.get({
queryParameters: { status: "active" },
}));
const toolbelt = gateway.toolbelts.byName("search-toolbelt");
console.log("Toolbelt:", await toolbelt.get());
await toolbelt.tools.add.post({ tools: ["exa_web_fetch"] });
await toolbelt.tools.remove.post({ tools: ["exa_web_fetch"] });

// Connections support create, list, get, parameter updates, and delete.
const connectionRequest: Create_connection_request = {
provider: "github",
userId: actorId,
scopes: ["repo"],
};
console.log("Created connection:", await gateway.connections.post(connectionRequest));
console.log("Connections:", await gateway.connections.get({
queryParameters: { userId: actorId },
}));

const connectionId = process.env.CONNECTION_ID;
if (connectionId) {
const connection = gateway.connections.byId(connectionId);
console.log("Connection:", await connection.get());
await connection.patch({
connectionParameters: {
additionalData: { site_url: "https://github.com" },
},
});
await connection.delete();
}

// Users are derived from their sessions and connections.
console.log("Users:", await gateway.users.get());
console.log("User:", await gateway.users.byUser_id(actorId).get());

// The convenience API delegates session creation to the generated resource
// and returns a session bound to response.mcpUrl.
console.log("Sessions:", await gateway.sessionsApi.get({
queryParameters: { endUserId: actorId },
}));
const session = await gateway.session.create({
actorId,
tools: ["exa_web_search@v1"],
config: { preloadTools: ["exa_web_search@v1"] },
permissions: { defaultAction: "ask" },
});
console.log("Session MCP URL:", session.url);

const sessionUrn = process.env.SESSION_URN;
if (sessionUrn) {
await gateway.sessionsApi.bySession_urn(sessionUrn).delete();
}

// Uncomment when the example toolbelt is no longer needed.
// await toolbelt.delete();
27 changes: 27 additions & 0 deletions examples/action-gateway/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Client } from "../../src/inference-gen/inference.js";
import {
ActionGatewayClient,
ResponsesProvider,
} from "../../src/action-gateway/index.js";

const apiKey = process.env.DIGITALOCEAN_TOKEN!;
const inference = new Client({ apiKey });
const gateway = new ActionGatewayClient({
apiKey,
provider: new ResponsesProvider(),
});
const session = await gateway.session.create({
actorId: "end-user-123",
permissions: {
defaultAction: "ask",
rules: [{ tool: "exa_web_search", action: "allow" }],
},
});

const response = await inference.responses.create({
model: "openai-gpt-4o",
input: "Find the latest DigitalOcean news and summarize it.",
tools: await session.tools(),
});

console.dir(await session.handleToolCalls(response), { depth: null });
28 changes: 28 additions & 0 deletions examples/action-gateway/session-controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

const gateway = new ActionGatewayClient({
apiKey: process.env.DIGITALOCEAN_TOKEN!,
});
const session = await gateway.session.create({
actorId: "end-user-123",

tools: ["exa_web_search@v1", "exa_web_fetch@v1"],
config: { preloadTools: ["exa_web_search@v1"] },
permissions: {
defaultAction: "deny",
rules: [
{ tool: "exa_web_search", action: "allow" },
{ tool: "exa_web_fetch", action: "ask" },
],
},
});

console.log("MCP URL:", session.url);
console.log("Selected for search/invoke:", session.selectedTools);
console.log(
"Exposed directly:",
(await session.toolsOperations.list({ includeAll: true })).map((tool) => tool.name),
);

const results = await session.toolsOperations.search("search or fetch a public web page");
console.dir(results, { depth: null });
19 changes: 19 additions & 0 deletions examples/action-gateway/toolbelt-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

const gateway = new ActionGatewayClient({
apiKey: process.env.DIGITALOCEAN_TOKEN!,
});
const toolbelt = await gateway.createToolbelt({
name: "search-toolbelt",
tools: ["exa_web_search", "exa_web_fetch"],
});

const session = await gateway.session.create({
actorId: "end-user-123",
permissions: {
defaultAction: "ask",
rules: [{ tool: `toolbelt:${toolbelt.ref}`, action: "allow" }],
},
});

console.log(session.url);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"main": "index.js",
"exports": {
".": "./index.js",
"./action_gateway": "./src/action-gateway/index.js",
"./inference": "./src/inference-gen/inference.js",
"./package.json": "./package.json",
"./*": "./*"
Expand Down
Loading
Loading