Skip to content

Commit bf0f56f

Browse files
noah-databricksIsaac
andcommitted
feat(appkit): add the omnigent plugin on managed Omnigent (1/3)
Gives the app's users Omnigent agent sessions (Claude Agent SDK, Pi, OpenAI Agents SDK) on the workspace's managed Omnigent server. Every call carries the signed-in user's token, so each session belongs to that user, runs on a Databricks Sandbox host and calls models through Unity Gateway. - Session routes under /api/omnigent relay to /api/2.0/omnigent and are limited to sessions this app created. - Harnesses are offered the UC system.ai models whose API types they speak. - A clear setup error when the app's user token lacks all-apis, which managed Omnigent requires and an account admin adds to the app's OAuth integration. Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: Noah Solomon <noah@databricks.com>
1 parent 4917a0c commit bf0f56f

12 files changed

Lines changed: 1201 additions & 0 deletions

File tree

‎docs/docs/plugins/omnigent.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Omnigent
2+
3+
<!-- AUTO-GENERATED: stability-banner-start -->
4+
:::warning Beta plugin
5+
This plugin is currently **beta**. APIs may change between minor releases. Import from `@databricks/appkit/beta`. See [Plugin Stability Tiers](./stability.md).
6+
:::
7+
<!-- AUTO-GENERATED: stability-banner-end -->
8+
9+
The `omnigent` plugin gives your app's users [Omnigent](https://docs.databricks.com/aws/en/omnigent/) agent sessions: Claude Agent SDK, Pi and OpenAI Agents SDK, on the workspace's managed Omnigent.
10+
11+
- **Sessions belong to the signed-in user.** Every call carries the user's token, so a session is theirs, appears in `<workspace>/omnigent`, and is private to them until they share it.
12+
- **Each session runs on a Databricks Sandbox**, so agent-written code runs in an isolated, Databricks-managed box with the user's own permissions.
13+
- **Model calls go through Unity Gateway**, with its governance, limits and usage tracking.
14+
15+
Omnigent owns the agent loop: sessions, history, approvals. Use the [`agents`](./agents.md) plugin when you want AppKit to own the loop against one serving endpoint. Use `omnigent` when you want the harnesses themselves.
16+
17+
## Install
18+
19+
```ts
20+
import { createApp, server } from "@databricks/appkit";
21+
import { omnigent } from "@databricks/appkit/beta";
22+
23+
await createApp({
24+
plugins: [
25+
server(),
26+
omnigent({ instructions: "You help the team explore order data." }),
27+
],
28+
});
29+
```
30+
31+
## Workspace setup
32+
33+
- **Previews.** Enable the **Omnigent** and **Databricks Sandbox** previews in the workspace. Omnigent needs a region that supports Unity Gateway; Sandbox hosts are in select AWS regions.
34+
- **The `all-apis` scope on the app.** Managed Omnigent only accepts tokens with the `all-apis` scope, which an app's user authorization cannot request itself. An account admin adds it to the app's OAuth integration once, keeping the app's existing scopes:
35+
36+
```bash
37+
databricks apps get <app-name> -o json # oauth2_app_integration_id, effective_user_api_scopes
38+
databricks account custom-app-integration update <oauth2_app_integration_id> \
39+
--json '{"scopes": ["<existing scopes>", "all-apis"]}'
40+
```
41+
42+
The update replaces the whole scope list. New scopes can take up to an hour to reach users' tokens. A bundle deploy, or a change to the app's user API scopes, sets the scopes back to the declared list, so repeat the update after either.
43+
44+
`GET /api/omnigent/status` reports whether the user can reach managed Omnigent, and why not.
45+
46+
## Configuration
47+
48+
| Option | Default | |
49+
|---|---|---|
50+
| `instructions` | a generic assistant prompt | System instructions for every session |
51+
| `harnesses` | `claude-sdk`, `pi`, `openai-agents` | Harnesses users may pick |
52+
| `defaultHarness`, `defaultModel` | first available | |
53+
54+
Each harness is offered the Unity Gateway `system.ai` models whose API it speaks: Claude Agent SDK the Anthropic Messages API, Pi the Codex Responses API, OpenAI Agents SDK the OpenAI Responses API.
55+
56+
## Routes
57+
58+
All routes are under `/api/omnigent` and act as the signed-in user. Sessions are limited to the ones this app created.
59+
60+
| Route | |
61+
|---|---|
62+
| `GET /status` | Whether managed Omnigent and Sandbox hosts are usable |
63+
| `GET /harnesses` | Harnesses available to the user, each with the models it can drive |
64+
| `GET /sessions`, `POST /sessions` | List, create (`harness`, `model`, `message`, `title`) |
65+
| `GET /sessions/:id`, `DELETE /sessions/:id` | Snapshot (items, status), delete |
66+
| `POST /sessions/:id/messages` | `{ "text": "..." }` |
67+
| `GET /sessions/:id/stream` | Server-sent events (Omnigent's session stream, which replays from `Last-Event-ID`) |
68+
| `POST /sessions/:id/interrupt` | Stop the current turn |
69+
70+
Creating a session returns once its Sandbox host is up, about 30 seconds on a cold start.
71+
72+
## Local development
73+
74+
With `NODE_ENV=development` and no forwarded token, the plugin calls Omnigent as the developer, with the token of the app's local Databricks CLI profile.
75+
76+
## Limits
77+
78+
- **Beta, in Omnigent's regions.** The plugin needs the workspace's managed Omnigent; it does not run Omnigent itself.
79+
- **Only gateway harnesses.** Harnesses that sign in with their vendor (Cursor, Copilot, Gemini and others) are not offered. Codex is not offered yet: on a Sandbox host, its brokered sign-in rejects the Codex CLI version the Sandbox ships.
80+
- **Broad app scope.** The `all-apis` scope lets the app call any API the user can, on the user's behalf.

‎packages/appkit/src/plugins/beta-exports.generated.ts‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { gzipSync } from "node:zlib";
2+
3+
interface BundleSpec {
4+
name: string;
5+
description?: string;
6+
harness: string;
7+
model: string;
8+
instructions: string;
9+
}
10+
11+
/**
12+
* An Omnigent agent bundle: `config.yaml` (the agent spec) and `AGENTS.md`
13+
* (the instructions), as a tar.gz. The YAML is written as JSON, which is
14+
* valid YAML.
15+
*/
16+
export function buildBundle(spec: BundleSpec): Buffer {
17+
const config = {
18+
spec_version: 1,
19+
name: spec.name,
20+
description: spec.description ?? spec.name,
21+
executor: {
22+
type: "omnigent",
23+
model: spec.model,
24+
config: { harness: spec.harness },
25+
},
26+
instructions: "AGENTS.md",
27+
};
28+
const files: Record<string, string> = {
29+
"config.yaml": `${JSON.stringify(config, null, 2)}\n`,
30+
"AGENTS.md": spec.instructions,
31+
};
32+
return gzipSync(tar(files));
33+
}
34+
35+
/** Minimal ustar writer: regular files only, names under 100 bytes. */
36+
function tar(files: Record<string, string | Buffer>): Buffer {
37+
const blocks: Buffer[] = [];
38+
const mtime = Math.floor(Date.now() / 1000);
39+
for (const [name, content] of Object.entries(files)) {
40+
if (Buffer.byteLength(name) >= 100)
41+
throw new Error(`tar: name too long: ${name}`);
42+
const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
43+
const header = Buffer.alloc(512);
44+
header.write(name, 0, 100);
45+
header.write("0000644\0", 100);
46+
header.write("0000000\0", 108);
47+
header.write("0000000\0", 116);
48+
header.write(`${data.length.toString(8).padStart(11, "0")}\0`, 124);
49+
header.write(`${mtime.toString(8).padStart(11, "0")}\0`, 136);
50+
header.write(" ", 148); // checksum placeholder
51+
header.write("0", 156);
52+
header.write("ustar\u000000", 257);
53+
let sum = 0;
54+
for (const b of header) sum += b;
55+
header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148);
56+
blocks.push(header, data, Buffer.alloc((512 - (data.length % 512)) % 512));
57+
}
58+
blocks.push(Buffer.alloc(1024));
59+
return Buffer.concat(blocks);
60+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Calls the workspace's managed Omnigent server (`/api/2.0/omnigent`) with
3+
* the signed-in user's token, so sessions, Sandbox hosts and model calls are
4+
* the user's own.
5+
*/
6+
export class ManagedOmnigent {
7+
constructor(private readonly host: () => Promise<string>) {}
8+
9+
async request(
10+
token: string,
11+
method: string,
12+
path: string,
13+
init: {
14+
json?: unknown;
15+
body?: RequestInit["body"];
16+
headers?: Record<string, string>;
17+
signal?: AbortSignal;
18+
} = {},
19+
): Promise<Response> {
20+
const r = await fetch(`${await this.host()}/api/2.0/omnigent${path}`, {
21+
method,
22+
headers: {
23+
Authorization: `Bearer ${token}`,
24+
...(init.json !== undefined
25+
? { "content-type": "application/json" }
26+
: {}),
27+
...init.headers,
28+
},
29+
body: init.json !== undefined ? JSON.stringify(init.json) : init.body,
30+
signal: init.signal,
31+
redirect: "manual",
32+
});
33+
if (r.status === 403) {
34+
const text = await r.clone().text();
35+
if (text.includes("required scopes: all-apis")) {
36+
throw new OmnigentScopeError();
37+
}
38+
}
39+
return r;
40+
}
41+
}
42+
43+
/**
44+
* Managed Omnigent only accepts tokens with the `all-apis` scope, which a
45+
* Databricks App's user authorization cannot request itself.
46+
*/
47+
export class OmnigentScopeError extends Error {
48+
readonly status = 403;
49+
constructor() {
50+
super(
51+
"Omnigent needs this app's user token to carry the all-apis scope. " +
52+
"An account admin adds it to the app's OAuth integration: " +
53+
"databricks account custom-app-integration update <app oauth2_app_integration_id> " +
54+
"with the app's current scopes plus all-apis. It can take up to an hour to reach users' tokens.",
55+
);
56+
}
57+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
interface HarnessInfo {
2+
label: string;
3+
/**
4+
* Unity Gateway API types it calls (a UC model service's
5+
* `supported_api_types`); a model must support one of them.
6+
*/
7+
apis: string[];
8+
/** Default models, best first, when the workspace has them. */
9+
preferred: string[];
10+
}
11+
12+
/**
13+
* Harnesses whose model calls a Databricks Sandbox routes through Unity
14+
* Gateway. The other harnesses Omnigent supports sign in with their vendor,
15+
* so they are not offered. Codex is left out for now: on a Sandbox host its
16+
* brokered sign-in rejects the Codex CLI version the Sandbox ships.
17+
*/
18+
const GATEWAY_HARNESSES: Readonly<Record<string, HarnessInfo>> = {
19+
"claude-sdk": {
20+
label: "Claude Agent SDK",
21+
apis: ["anthropic/v1/messages"],
22+
preferred: ["system.ai.claude-sonnet-5", "system.ai.claude-opus-5-5"],
23+
},
24+
pi: {
25+
label: "Pi",
26+
// Pi reaches Databricks models through the Codex responses API; on the
27+
// chat-completions path Gemini fails with an empty 400 (tested).
28+
apis: ["codex/v1/responses"],
29+
preferred: ["system.ai.gpt-5-5"],
30+
},
31+
"openai-agents": {
32+
label: "OpenAI Agents SDK",
33+
apis: ["openai/v1/responses"],
34+
preferred: ["system.ai.gpt-5-5"],
35+
},
36+
};
37+
38+
export const DEFAULT_HARNESSES = Object.keys(GATEWAY_HARNESSES);
39+
40+
/** A Unity Gateway model, e.g. `system.ai.claude-sonnet-5`. */
41+
export interface GatewayModel {
42+
name: string;
43+
/** The model service's `supported_api_types`. */
44+
apiTypes: string[];
45+
}
46+
47+
/** A harness the user can pick, with the models it can drive. */
48+
export interface HarnessOffer {
49+
id: string;
50+
label: string;
51+
models: string[];
52+
defaultModel?: string;
53+
}
54+
55+
/**
56+
* What a user can pick: the allowed gateway harnesses, each with the models
57+
* whose API it speaks.
58+
*/
59+
export function offerHarnesses(opts: {
60+
allowed: string[];
61+
models: GatewayModel[];
62+
}): HarnessOffer[] {
63+
const out: HarnessOffer[] = [];
64+
for (const id of opts.allowed) {
65+
const info = GATEWAY_HARNESSES[id];
66+
if (!info) continue;
67+
const models = opts.models
68+
.filter((m) => m.apiTypes.some((t) => info.apis.includes(t)))
69+
.map((m) => m.name);
70+
out.push({
71+
id,
72+
label: info.label,
73+
models,
74+
defaultModel: info.preferred.find((m) => models.includes(m)) ?? models[0],
75+
});
76+
}
77+
return out;
78+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./omnigent";
2+
export * from "./types";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"$schema": "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
3+
"name": "omnigent",
4+
"displayName": "Omnigent Plugin",
5+
"description": "Omnigent agent sessions for the app's users on the workspace's managed Omnigent: Claude Agent SDK, Pi and OpenAI Agents SDK on Databricks Sandboxes, with models through Unity Gateway",
6+
"stability": "beta",
7+
"resources": {
8+
"required": [],
9+
"optional": []
10+
},
11+
"scaffolding": {
12+
"rules": {
13+
"must": [
14+
"Enable the Omnigent and Databricks Sandbox previews in the workspace",
15+
"Account admin: add all-apis to the app's OAuth integration (databricks account custom-app-integration update)",
16+
"Re-run that update after a bundle deploy or a user_api_scopes change, which both drop all-apis"
17+
]
18+
}
19+
},
20+
"config": {
21+
"schema": {
22+
"type": "object",
23+
"properties": {
24+
"instructions": {
25+
"type": "string",
26+
"description": "System instructions for every session"
27+
},
28+
"harnesses": {
29+
"type": "array",
30+
"items": {
31+
"type": "string"
32+
},
33+
"description": "Harnesses users may pick"
34+
},
35+
"defaultHarness": {
36+
"type": "string"
37+
},
38+
"defaultModel": {
39+
"type": "string"
40+
}
41+
}
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)