Skip to content
Open
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
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
[TypeScript examples](examples/action-gateway/).

## **Basic Usage**
> A quick guide to getting started with client
#### Authenticating
Expand Down
94 changes: 94 additions & 0 deletions docs/action-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Action Gateway

The TypeScript SDK uses a session-first Action Gateway flow. Create a session
on the DigitalOcean public API, then discover or invoke tools through the
returned session MCP URL with authentication and actor headers managed by the
SDK.

```ts
import { ActionGatewayClient } from "@digitalocean/dots/action_gateway";

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

Session creation sends `actor_id`, `name`, and typed `policy` to
`POST /v2/action-gateway/sessions`. The default policy action is `ask`. Use the
optional `tools` field to select tools (omit it for all tools, or pass `[]` for
none) and `config.preloadTools` to expose concrete tools alongside the three
meta-tools on the returned MCP endpoint.

```ts
const session = await gateway.session.create({
actorId: "end-user-123",
tools: ["exa_web_search@v1"],
config: { preloadTools: ["exa_web_search@v1"] },
});

console.log(session.url); // API-returned mcpUrl
```

The controls are complementary: top-level `tools` selects the catalog visible
to `action_search` and callable through `action_invoke`, `config.preloadTools`
also exposes selected concrete tools directly, and `permissions` applies
`allow`, `ask`, or `deny` when any selected tool is invoked. See
`examples/action-gateway/session-controls.ts` for a complete configuration.

If a policy returns a pending approval, decide it and retry the invocation:

```ts
await session.approve(approvalId);
// or: await session.deny(approvalId);
```

## Inference API formats

Select the inference API when creating the client so `session.tools()` and
`session.handleToolCalls()` use the matching wire format. The default is
`chat.completions`; Responses uses top-level `name` and `parameters` fields:

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

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

const toolOutputs = await session.handleToolCalls(response);
```

Use `provider: "messages"` for the Messages API. Provider instances remain
supported for custom integrations.

## Toolbelts

Toolbelts are public DigitalOcean API resources, so CRUD operations are
generated from the public OpenAPI specification under `gateway.toolbelts`.
`createToolbelt` is the Action Gateway convenience wrapper:

```ts
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" }],
},
});
```

See `examples/action-gateway/` for Chat Completions, Messages, Responses,
direct tool and code execution, asynchronous usage, toolbelt creation, and
toolbelt policy examples.
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();
22 changes: 22 additions & 0 deletions examples/action-gateway/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ActionGatewayClient } from "../../src/action-gateway/index.js";

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

const response = await gateway.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 });
Loading
Loading