@allternit/computer-driver 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +104 -2
- package/dist/anthropic.d.ts +43 -0
- package/dist/anthropic.js +157 -0
- package/dist/client.d.ts +157 -0
- package/dist/client.js +173 -0
- package/dist/gemini.d.ts +56 -0
- package/dist/gemini.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/openai.d.ts +98 -0
- package/dist/openai.js +113 -0
- package/dist/v2-generated.d.ts +1445 -0
- package/dist/v2-generated.js +1266 -0
- package/dist/v2.d.ts +279 -0
- package/dist/v2.js +154 -0
- package/package.json +58 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Contract v2 (`allternit.computer.v2`) structured members.** Typed client methods for all ten driver-backed members — `read_ui`, `act`, `run_batch`, `verify`, `request_human`, `use_credential`, `run_subtask`, `run_parallel`, `run_skill`, `skills` — on the new `ComputerV2Driver` (exported from the package root). Input and result types are generated from `contracts/computer-toolset/allternit-computer-v2.json` (`src/v2-generated.ts`, emitted by `contracts/computer-toolset/generate.mjs`), so they cannot drift from the server contract.
|
|
8
|
+
- **`computer_v2` function tool next to the pixel tool in every adapter**, with the same steering guidance gizzi sends: `computerV2AnthropicTool` + `runAnthropicComputerV2` (anthropic subpath), `openaiComputerV2Tool` + `runOpenAIV2Call`, `geminiComputerV2Declaration` + `runGeminiV2Call`, plus the provider-neutral `computerV2Tool()`.
|
|
9
|
+
- **Approval flow helper.** `client.toolsetWithApproval(id, call, onApproval)` answers a 409 `approval_required` hold (approve + resend with the single-use `approval_grant`) for any member, pixel or structured.
|
|
10
|
+
- **Typed errors.** `ComputerV2Error` (a structured member answered `is_error`), `ComputerBusyError` (423 `computer_busy` / `computer_controlled_elsewhere`), `SandboxRequiredError` (409 `sandbox_required`) and `ComputerConflictError` (409 `computer_conflict`).
|
|
11
|
+
|
|
12
|
+
### Notes
|
|
13
|
+
|
|
14
|
+
- Requires the Allternit Driver sidecar on the computer for the structured members (this-device today). The 17 pixel members are unchanged.
|
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @allternit/computer-driver
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Drive an Allternit hosted computer (`/v1/computers`) from Claude, OpenAI computer use, or Gemini computer use. Needs a project key with the `computers` scope and the project's hosted driver turned on.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { AllternitComputers } from "@allternit/computer-driver"
|
|
7
|
+
const client = new AllternitComputers({ apiKey: process.env.ALLTERNIT_API_KEY })
|
|
8
|
+
const computer = await client.create({ name: "agent-1" })
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Structured UI driving (allternit.computer.v2)
|
|
12
|
+
|
|
13
|
+
Contract v2 adds ten driver-backed structured members — `read_ui`, `act`, `run_batch`, `verify`, `request_human`, `use_credential`, `run_subtask`, `run_parallel`, `run_skill`, `skills` — typed end to end. `ComputerV2Driver` calls them directly; every adapter also exposes them to the model as the `computer_v2` function tool. The structured members need the Allternit Driver on the computer (this-device today).
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { ComputerV2Driver } from "@allternit/computer-driver"
|
|
17
|
+
const v2 = new ComputerV2Driver({ client, computerId: computer.id, onApproval: async (a) => askAPerson(a) })
|
|
18
|
+
|
|
19
|
+
// Read the UI as an element tree — no screenshots.
|
|
20
|
+
const ui = await v2.read_ui({ app: "Safari" })
|
|
21
|
+
const field = ui.elements?.find((e) => e.role === "textfield")
|
|
22
|
+
if (field) await v2.act({ id: field.id, op: "set_value", value: "hello@example.com", version: ui.version })
|
|
23
|
+
|
|
24
|
+
// Hand a bounded step sequence to the fast decision loop instead of clicking through it yourself.
|
|
25
|
+
const sub = await v2.run_subtask({
|
|
26
|
+
goal: "Fill the signup form and submit it",
|
|
27
|
+
inputs: [{ name: "email", value: "hello@example.com" }],
|
|
28
|
+
success: [{ role: "button", name: "Submit" }],
|
|
29
|
+
})
|
|
30
|
+
if (sub.status !== "done") console.log(sub.status, sub.next)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Subtask safety statuses
|
|
34
|
+
|
|
35
|
+
`run_subtask` / `run_skill` / `run_parallel` results end with a `status`:
|
|
36
|
+
|
|
37
|
+
- `done` — the goal is met.
|
|
38
|
+
- `escalated` — handed back with a `reason` and the current `screen`; continue from there yourself.
|
|
39
|
+
- `needs_confirmation` — the next step (`held_step`) needs the person's confirmation; run it yourself with `act`/`run_batch` and the server asks them.
|
|
40
|
+
- `paused` — the safety monitor paused the subtask; stop and call `request_human`.
|
|
41
|
+
- `denied` — the step isn't allowed on this computer (app/domain lists or a credential binding); find another way.
|
|
42
|
+
- `use_api` — make the named API/MCP call yourself (`api.tool`); the screen is untouched.
|
|
43
|
+
- `failed` — ran out of steps, budget or a hard error.
|
|
44
|
+
|
|
45
|
+
### The computer_v2 function tool in a model loop
|
|
46
|
+
|
|
47
|
+
Each adapter exposes the structured members with the same steering guidance gizzi sends:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { computerV2AnthropicTool, runAnthropicComputerV2 } from "@allternit/computer-driver/anthropic"
|
|
51
|
+
import { openaiComputerV2Tool, runOpenAIV2Call } from "@allternit/computer-driver"
|
|
52
|
+
import { geminiComputerV2Declaration, runGeminiV2Call } from "@allternit/computer-driver"
|
|
53
|
+
|
|
54
|
+
// Anthropic: put computerV2AnthropicTool() next to the toolset in tools, and answer
|
|
55
|
+
// tool_use blocks whose name is "computer_v2" with runAnthropicComputerV2({ client, computerId, onApproval }, name, input).
|
|
56
|
+
// OpenAI: put openaiComputerV2Tool() in tools and answer function_call items with
|
|
57
|
+
// runOpenAIV2Call({ client, computerId }, call.call_id, JSON.parse(call.arguments)).
|
|
58
|
+
// Gemini: put geminiComputerV2Declaration() in functionDeclarations and answer
|
|
59
|
+
// functionCall parts with runGeminiV2Call({ client, computerId }, part.functionCall.name, part.functionCall.args).
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`computerV2Tool()` returns the provider-neutral `{ name, description, schema }` if you wire tools yourself.
|
|
63
|
+
|
|
64
|
+
## Claude (`computer_toolset_20260801` / `browser_toolset_20260801`)
|
|
65
|
+
|
|
66
|
+
Requires `@anthropic-ai/sdk` >= 0.132.0 (optional peer).
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { AllternitComputerToolset } from "@allternit/computer-driver/anthropic"
|
|
70
|
+
const toolset = new AllternitComputerToolset({
|
|
71
|
+
client, computerId: computer.id,
|
|
72
|
+
onApproval: async (approval) => askAPerson(approval), // server-held risky calls
|
|
73
|
+
})
|
|
74
|
+
// pass `toolset` to client.beta.messages.toolRunner({ tools: [toolset, computerV2AnthropicTool()], ... })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every member runs on the server through `POST /v1/computers/{id}/toolset`. When the server holds a call (409 `approval_required`), `onApproval` decides: `true` approves it and resends with the grant, `false` returns the held result to the model as an error. The SDK's own `confirm` option still runs first. Approving with an API key needs the project's `approval_mode` set to `api_key`.
|
|
78
|
+
|
|
79
|
+
## OpenAI computer use
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { runOpenAIAction } from "@allternit/computer-driver"
|
|
83
|
+
const { output } = await runOpenAIAction({ client, computerId, display: { width: 1280, height: 800 } }, call.call_id, call.action)
|
|
84
|
+
// push `output` into the next responses.create input
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Gemini computer use
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { runGeminiCall } from "@allternit/computer-driver"
|
|
91
|
+
const { functionResponse, inlineData } = await runGeminiCall({ client, computerId }, part.functionCall)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Gemini's 0–999 coordinates are sent as `coordinate_space: "normalized_1000"`; the server scales them.
|
|
95
|
+
|
|
96
|
+
The OpenAI and Gemini pixel adapters let `ApprovalRequiredError` propagate; catch it, call `client.approve()`, and resend the action.
|
|
97
|
+
|
|
98
|
+
## Errors
|
|
99
|
+
|
|
100
|
+
- `ApprovalRequiredError` — 409 `approval_required`; carries `approval` (id, member, `approve_url`) and the held `result`. `client.toolsetWithApproval(id, call, onApproval)` approves and resends with the `approval_grant` for you.
|
|
101
|
+
- `ComputerV2Error` — a structured member answered `is_error: true`; carries the member name and raw `result`.
|
|
102
|
+
- `ComputerBusyError` — 423 `computer_busy` / `computer_controlled_elsewhere`.
|
|
103
|
+
- `SandboxRequiredError` — 409 `sandbox_required` (the call needs a sandbox computer).
|
|
104
|
+
- `ComputerConflictError` — 409 `computer_conflict` (another subtask or lease conflicts).
|
|
105
|
+
- `AllternitApiError` — everything else, with `status`, `code`, `type`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BetaAbstractBrowserToolset20260801, BetaAbstractComputerToolset20260801, type BetaBrowserMemberResult, type BetaBrowserToolsetOptions, type BetaComputerMemberResult, type BetaComputerToolsetOptions, type BetaToolsetCallContext } from "@anthropic-ai/sdk/helpers/beta/toolsets";
|
|
2
|
+
import { type AllternitComputers, type Approval } from "./client.ts";
|
|
3
|
+
export interface DriverOptions {
|
|
4
|
+
client: AllternitComputers;
|
|
5
|
+
computerId: string;
|
|
6
|
+
/** Called when the server holds a call for approval. true → approve() and resend with the grant. */
|
|
7
|
+
onApproval?: (approval: Approval) => Promise<boolean>;
|
|
8
|
+
/** run_id / turn_id / call_index for this call, if your loop tracks them. */
|
|
9
|
+
callIds?: (ctx: BetaToolsetCallContext, member: string) => {
|
|
10
|
+
run_id?: string;
|
|
11
|
+
turn_id?: string;
|
|
12
|
+
call_index?: number;
|
|
13
|
+
};
|
|
14
|
+
browserSessionId?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class AllternitComputerToolset extends BetaAbstractComputerToolset20260801 {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(o: DriverOptions & BetaComputerToolsetOptions);
|
|
19
|
+
protected execute(ctx: BetaToolsetCallContext, name: string, input: unknown): Promise<BetaComputerMemberResult>;
|
|
20
|
+
}
|
|
21
|
+
export declare class AllternitBrowserToolset extends BetaAbstractBrowserToolset20260801 {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(o: DriverOptions & Omit<BetaBrowserToolsetOptions, "browserState"> & Partial<Pick<BetaBrowserToolsetOptions, "browserState">>);
|
|
24
|
+
protected execute(ctx: BetaToolsetCallContext, name: string, input: unknown): Promise<BetaBrowserMemberResult>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The `computer_v2` function tool for Anthropic models: the ten structured
|
|
28
|
+
* members (read_ui … skills) next to the native pixel toolset. The Anthropic
|
|
29
|
+
* wire hook only rewrites tools carrying the v1 pixel marker, so this passes
|
|
30
|
+
* through untouched.
|
|
31
|
+
*/
|
|
32
|
+
export declare function computerV2AnthropicTool(): {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
input_schema: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Run one `computer_v2` function call (`{action, ...fields}`) and return the
|
|
39
|
+
* tool-result content blocks, like any toolset member. Throws ToolError on a
|
|
40
|
+
* failed action; without `onApproval`, a held call (409 approval_required)
|
|
41
|
+
* resolves its held result as an error, the same rule as the toolsets.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runAnthropicComputerV2(o: DriverOptions, name: string, input: unknown): Promise<any[]>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Anthropic toolset drivers: subclasses of the SDK's abstract computer and browser
|
|
2
|
+
// toolsets (computer_toolset_20260801 / browser_toolset_20260801) that run every
|
|
3
|
+
// member on an Allternit hosted computer via POST /v1/computers/{id}/toolset.
|
|
4
|
+
import { BetaAbstractBrowserToolset20260801, BetaAbstractComputerToolset20260801, ToolError, } from "@anthropic-ai/sdk/helpers/beta/toolsets";
|
|
5
|
+
import { ApprovalRequiredError, resultImage, resultText, } from "./client.js";
|
|
6
|
+
import { computerV2Tool, runComputerV2Member } from "./v2.js";
|
|
7
|
+
/**
|
|
8
|
+
* Default SDK `confirm`: let the call through. The Allternit server is the gate: it applies the
|
|
9
|
+
* project's member policy and holds risky calls with a 409, which `onApproval` answers. Pass your
|
|
10
|
+
* own `confirm` to also prompt locally before the call leaves the process.
|
|
11
|
+
*/
|
|
12
|
+
const serverDecides = () => true;
|
|
13
|
+
async function callMember(o, toolset, ctx, member, input) {
|
|
14
|
+
const call = {
|
|
15
|
+
toolset,
|
|
16
|
+
member,
|
|
17
|
+
input: (input ?? {}),
|
|
18
|
+
// The SDK only calls members its toolset config turned on, which is the
|
|
19
|
+
// opt-in the executor asks for on off-by-default members.
|
|
20
|
+
enable: [member],
|
|
21
|
+
...(o.callIds?.(ctx, member) ?? {}),
|
|
22
|
+
...(o.browserSessionId ? { browser_session_id: o.browserSessionId } : {}),
|
|
23
|
+
};
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await o.client.toolset(o.computerId, call);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (!(e instanceof ApprovalRequiredError))
|
|
30
|
+
throw e;
|
|
31
|
+
if (!o.onApproval || !(await o.onApproval(e.approval)))
|
|
32
|
+
res = e.result;
|
|
33
|
+
else {
|
|
34
|
+
await o.client.approve(o.computerId, e.approval.id);
|
|
35
|
+
res = await o.client.toolset(o.computerId, { ...call, approval_grant: e.approval.id });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (res.is_error)
|
|
39
|
+
throw new ToolError(toAnthropicBlocks(res));
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
function toAnthropicBlocks(r) {
|
|
43
|
+
const blocks = r.content.map((b) => b.type === "text"
|
|
44
|
+
? { type: "text", text: b.text }
|
|
45
|
+
: { type: "image", source: { type: "base64", media_type: b.media_type, data: b.data } });
|
|
46
|
+
return blocks.length ? blocks : [{ type: "text", text: "The action failed." }];
|
|
47
|
+
}
|
|
48
|
+
function shot(r) {
|
|
49
|
+
const img = resultImage(r);
|
|
50
|
+
if (!img)
|
|
51
|
+
throw new ToolError("The computer returned no screenshot.");
|
|
52
|
+
return { data: img.data, mediaType: img.media_type };
|
|
53
|
+
}
|
|
54
|
+
function textOrVoid(r) {
|
|
55
|
+
const t = resultText(r);
|
|
56
|
+
return t ? t : undefined;
|
|
57
|
+
}
|
|
58
|
+
function jsonOf(r) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(resultText(r));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class AllternitComputerToolset extends BetaAbstractComputerToolset20260801 {
|
|
67
|
+
#o;
|
|
68
|
+
constructor(o) {
|
|
69
|
+
const { client: _c, computerId: _i, onApproval: _a, callIds: _d, browserSessionId: _b, ...sdk } = o;
|
|
70
|
+
super({ ...sdk, confirm: sdk.confirm ?? serverDecides });
|
|
71
|
+
this.#o = o;
|
|
72
|
+
}
|
|
73
|
+
async execute(ctx, name, input) {
|
|
74
|
+
const r = await callMember(this.#o, "computer", ctx, name, input);
|
|
75
|
+
if (name === "screenshot" || name === "zoom")
|
|
76
|
+
return shot(r);
|
|
77
|
+
if (name === "cursor_position") {
|
|
78
|
+
const j = jsonOf(r);
|
|
79
|
+
if (j && typeof j.x === "number")
|
|
80
|
+
return { x: j.x, y: j.y };
|
|
81
|
+
const m = /(-?\d+)\D+(-?\d+)/.exec(resultText(r));
|
|
82
|
+
if (!m)
|
|
83
|
+
throw new ToolError("The computer returned no cursor position.");
|
|
84
|
+
return { x: Number(m[1]), y: Number(m[2]) };
|
|
85
|
+
}
|
|
86
|
+
return textOrVoid(r);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export class AllternitBrowserToolset extends BetaAbstractBrowserToolset20260801 {
|
|
90
|
+
#o;
|
|
91
|
+
#state;
|
|
92
|
+
constructor(o) {
|
|
93
|
+
const { client: _c, computerId: _i, onApproval: _a, callIds: _d, browserSessionId: _b, ...sdk } = o;
|
|
94
|
+
const self = {};
|
|
95
|
+
super({ ...sdk, confirm: sdk.confirm ?? serverDecides, browserState: sdk.browserState ?? ((ctx) => self.t.#currentState(ctx)) });
|
|
96
|
+
this.#o = o;
|
|
97
|
+
self.t = this;
|
|
98
|
+
}
|
|
99
|
+
async #currentState(ctx) {
|
|
100
|
+
if (this.#state)
|
|
101
|
+
return this.#state;
|
|
102
|
+
const r = await callMember(this.#o, "browser", ctx, "list_tabs", {});
|
|
103
|
+
return (this.#state = r.browser_state ?? { tabs: jsonOf(r) ?? [] });
|
|
104
|
+
}
|
|
105
|
+
async execute(ctx, name, input) {
|
|
106
|
+
const r = await callMember(this.#o, "browser", ctx, name, input);
|
|
107
|
+
if (r.browser_state)
|
|
108
|
+
this.#state = r.browser_state;
|
|
109
|
+
if (name === "screenshot" || name === "zoom")
|
|
110
|
+
return shot(r);
|
|
111
|
+
const j = jsonOf(r);
|
|
112
|
+
const tabs = this.#state?.tabs ?? [];
|
|
113
|
+
switch (name) {
|
|
114
|
+
case "navigate":
|
|
115
|
+
return j?.url ? j : { url: tabs[0]?.url ?? String(input?.url ?? "") };
|
|
116
|
+
case "list_tabs":
|
|
117
|
+
return Array.isArray(j) ? j : tabs;
|
|
118
|
+
case "new_tab":
|
|
119
|
+
case "switch_tab":
|
|
120
|
+
if (j?.tab_id)
|
|
121
|
+
return j;
|
|
122
|
+
if (tabs[0])
|
|
123
|
+
return tabs[0];
|
|
124
|
+
throw new ToolError("The browser returned no tab.");
|
|
125
|
+
case "close_tab":
|
|
126
|
+
return undefined;
|
|
127
|
+
default:
|
|
128
|
+
return textOrVoid(r);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ------------------------------------------------------------------ computer_v2
|
|
133
|
+
/**
|
|
134
|
+
* The `computer_v2` function tool for Anthropic models: the ten structured
|
|
135
|
+
* members (read_ui … skills) next to the native pixel toolset. The Anthropic
|
|
136
|
+
* wire hook only rewrites tools carrying the v1 pixel marker, so this passes
|
|
137
|
+
* through untouched.
|
|
138
|
+
*/
|
|
139
|
+
export function computerV2AnthropicTool() {
|
|
140
|
+
const t = computerV2Tool();
|
|
141
|
+
return { name: t.name, description: t.description, input_schema: t.schema };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Run one `computer_v2` function call (`{action, ...fields}`) and return the
|
|
145
|
+
* tool-result content blocks, like any toolset member. Throws ToolError on a
|
|
146
|
+
* failed action; without `onApproval`, a held call (409 approval_required)
|
|
147
|
+
* resolves its held result as an error, the same rule as the toolsets.
|
|
148
|
+
*/
|
|
149
|
+
export async function runAnthropicComputerV2(o, name, input) {
|
|
150
|
+
const { action, ...fields } = (input ?? {});
|
|
151
|
+
if (!action)
|
|
152
|
+
throw new ToolError("The computer_v2 call needs an action.");
|
|
153
|
+
const res = await runComputerV2Member({ client: o.client, computerId: o.computerId, onApproval: o.onApproval }, action, fields);
|
|
154
|
+
if (res.is_error)
|
|
155
|
+
throw new ToolError(toAnthropicBlocks(res));
|
|
156
|
+
return toAnthropicBlocks(res);
|
|
157
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export declare const DEFAULT_BASE_URL = "https://api.allternit.com";
|
|
2
|
+
export type ToolsetName = "computer" | "browser";
|
|
3
|
+
export type CoordinateSpace = "pixels" | "normalized_1000";
|
|
4
|
+
export type ComputerStatus = "provisioning" | "running" | "stopped" | "starting" | "stopping" | "error" | "deleted";
|
|
5
|
+
export interface Computer {
|
|
6
|
+
id: string;
|
|
7
|
+
object: "computer";
|
|
8
|
+
name: string | null;
|
|
9
|
+
status: ComputerStatus;
|
|
10
|
+
account_id: string | null;
|
|
11
|
+
key_id: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
started_at: string | null;
|
|
14
|
+
metadata: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface Page<T> {
|
|
17
|
+
data: T[];
|
|
18
|
+
has_more: boolean;
|
|
19
|
+
next_cursor: string | null;
|
|
20
|
+
}
|
|
21
|
+
export type ContentBlock = {
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: "image";
|
|
26
|
+
media_type: string;
|
|
27
|
+
data: string;
|
|
28
|
+
};
|
|
29
|
+
export interface Screen {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
scale: number;
|
|
33
|
+
frame_width: number;
|
|
34
|
+
frame_height: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ToolsetResult {
|
|
37
|
+
is_error: boolean;
|
|
38
|
+
content: ContentBlock[];
|
|
39
|
+
browser_state?: unknown;
|
|
40
|
+
screen?: Screen;
|
|
41
|
+
error?: unknown;
|
|
42
|
+
}
|
|
43
|
+
export interface ToolsetCall {
|
|
44
|
+
toolset: ToolsetName;
|
|
45
|
+
member: string;
|
|
46
|
+
input?: Record<string, unknown>;
|
|
47
|
+
run_id?: string;
|
|
48
|
+
turn_id?: string;
|
|
49
|
+
call_index?: number;
|
|
50
|
+
model_frame?: {
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
};
|
|
54
|
+
coordinate_space?: CoordinateSpace;
|
|
55
|
+
approval_grant?: string;
|
|
56
|
+
browser_session_id?: string;
|
|
57
|
+
/** Off-by-default members (file_upload, read_console, read_network, javascript_exec) to allow for this call. */
|
|
58
|
+
enable?: string[];
|
|
59
|
+
}
|
|
60
|
+
export interface Approval {
|
|
61
|
+
id: string;
|
|
62
|
+
action_hash: string;
|
|
63
|
+
member: string;
|
|
64
|
+
toolset: ToolsetName;
|
|
65
|
+
risk: string;
|
|
66
|
+
confirmation_class: string;
|
|
67
|
+
approve_url: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ComputerEvent {
|
|
70
|
+
id: string;
|
|
71
|
+
type: "computer.action";
|
|
72
|
+
ts: string;
|
|
73
|
+
data: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
export interface ApiErrorBody {
|
|
76
|
+
type: string;
|
|
77
|
+
code: string;
|
|
78
|
+
message: string;
|
|
79
|
+
param: string | null;
|
|
80
|
+
}
|
|
81
|
+
export declare class AllternitApiError extends Error {
|
|
82
|
+
readonly status: number;
|
|
83
|
+
readonly type: string;
|
|
84
|
+
readonly code: string;
|
|
85
|
+
readonly param: string | null;
|
|
86
|
+
readonly body: unknown;
|
|
87
|
+
constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
|
|
88
|
+
}
|
|
89
|
+
/** 409 approval_required: the call was held until someone approves it. */
|
|
90
|
+
export declare class ApprovalRequiredError extends AllternitApiError {
|
|
91
|
+
readonly approval: Approval;
|
|
92
|
+
readonly result: ToolsetResult;
|
|
93
|
+
constructor(err: Partial<ApiErrorBody>, approval: Approval, result: ToolsetResult, body: unknown);
|
|
94
|
+
}
|
|
95
|
+
/** 423 computer_busy / computer_controlled_elsewhere: someone else holds the computer right now; retry shortly or request_human. */
|
|
96
|
+
export declare class ComputerBusyError extends AllternitApiError {
|
|
97
|
+
constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
|
|
98
|
+
}
|
|
99
|
+
/** 409 sandbox_required: the call only runs on a sandbox (cloud/bot) computer. */
|
|
100
|
+
export declare class SandboxRequiredError extends AllternitApiError {
|
|
101
|
+
constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
|
|
102
|
+
}
|
|
103
|
+
/** 409 computer_conflict: the call conflicts with another subtask or lease on the computer. */
|
|
104
|
+
export declare class ComputerConflictError extends AllternitApiError {
|
|
105
|
+
constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
|
|
106
|
+
}
|
|
107
|
+
export interface ClientOptions {
|
|
108
|
+
/** Project key (alt_live_… / alt_test_…). Defaults to ALLTERNIT_API_KEY. */
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
baseUrl?: string;
|
|
111
|
+
fetch?: typeof fetch;
|
|
112
|
+
}
|
|
113
|
+
export declare class AllternitComputers {
|
|
114
|
+
#private;
|
|
115
|
+
readonly baseUrl: string;
|
|
116
|
+
constructor(opts?: ClientOptions);
|
|
117
|
+
create(body?: {
|
|
118
|
+
name?: string;
|
|
119
|
+
account_id?: string;
|
|
120
|
+
metadata?: Record<string, unknown>;
|
|
121
|
+
}): Promise<Computer>;
|
|
122
|
+
list(q?: {
|
|
123
|
+
limit?: number;
|
|
124
|
+
after?: string;
|
|
125
|
+
}): Promise<Page<Computer>>;
|
|
126
|
+
get(id: string): Promise<Computer>;
|
|
127
|
+
start(id: string): Promise<Computer>;
|
|
128
|
+
stop(id: string): Promise<Computer>;
|
|
129
|
+
delete(id: string): Promise<Computer>;
|
|
130
|
+
/** Run one toolset member. Action failures resolve with is_error:true; a held call throws ApprovalRequiredError. */
|
|
131
|
+
toolset(id: string, call: ToolsetCall): Promise<ToolsetResult>;
|
|
132
|
+
/**
|
|
133
|
+
* Run one toolset member, answering a 409 approval_required hold. When the
|
|
134
|
+
* server holds the call and `onApproval` returns true, the approval is
|
|
135
|
+
* granted (POST /approvals/{id}) and the same call resent with the
|
|
136
|
+
* single-use `approval_grant`. When `onApproval` is missing or returns
|
|
137
|
+
* false, the held result resolves (is_error:true) so the model sees it.
|
|
138
|
+
*/
|
|
139
|
+
toolsetWithApproval(id: string, call: ToolsetCall, onApproval?: (approval: Approval) => boolean | Promise<boolean>): Promise<ToolsetResult>;
|
|
140
|
+
schema(id: string, toolset?: ToolsetName): Promise<Record<string, unknown>>;
|
|
141
|
+
events(id: string, q?: {
|
|
142
|
+
after?: string;
|
|
143
|
+
limit?: number;
|
|
144
|
+
}): Promise<Page<ComputerEvent>>;
|
|
145
|
+
approve(id: string, approvalId: string): Promise<{
|
|
146
|
+
approval_id: string;
|
|
147
|
+
approved: true;
|
|
148
|
+
approval_grant: string;
|
|
149
|
+
}>;
|
|
150
|
+
}
|
|
151
|
+
/** Text of a result, joined. */
|
|
152
|
+
export declare function resultText(r: ToolsetResult): string;
|
|
153
|
+
/** First image of a result, if any. */
|
|
154
|
+
export declare function resultImage(r: ToolsetResult): {
|
|
155
|
+
media_type: string;
|
|
156
|
+
data: string;
|
|
157
|
+
} | undefined;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Plain client for the Allternit hosted computer API (/v1/computers).
|
|
2
|
+
// Standard fetch only; every adapter in this package goes through it.
|
|
3
|
+
export const DEFAULT_BASE_URL = "https://api.allternit.com";
|
|
4
|
+
export class AllternitApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
type;
|
|
7
|
+
code;
|
|
8
|
+
param;
|
|
9
|
+
body;
|
|
10
|
+
constructor(status, err, body) {
|
|
11
|
+
super(err.message ?? `Allternit API error ${status}`);
|
|
12
|
+
this.name = "AllternitApiError";
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.type = err.type ?? "api_error";
|
|
15
|
+
this.code = err.code ?? "unknown";
|
|
16
|
+
this.param = err.param ?? null;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** 409 approval_required: the call was held until someone approves it. */
|
|
21
|
+
export class ApprovalRequiredError extends AllternitApiError {
|
|
22
|
+
approval;
|
|
23
|
+
result;
|
|
24
|
+
constructor(err, approval, result, body) {
|
|
25
|
+
super(409, err, body);
|
|
26
|
+
this.name = "ApprovalRequiredError";
|
|
27
|
+
this.approval = approval;
|
|
28
|
+
this.result = result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** 423 computer_busy / computer_controlled_elsewhere: someone else holds the computer right now; retry shortly or request_human. */
|
|
32
|
+
export class ComputerBusyError extends AllternitApiError {
|
|
33
|
+
constructor(status, err, body) {
|
|
34
|
+
super(status, err, body);
|
|
35
|
+
this.name = "ComputerBusyError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** 409 sandbox_required: the call only runs on a sandbox (cloud/bot) computer. */
|
|
39
|
+
export class SandboxRequiredError extends AllternitApiError {
|
|
40
|
+
constructor(status, err, body) {
|
|
41
|
+
super(status, err, body);
|
|
42
|
+
this.name = "SandboxRequiredError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** 409 computer_conflict: the call conflicts with another subtask or lease on the computer. */
|
|
46
|
+
export class ComputerConflictError extends AllternitApiError {
|
|
47
|
+
constructor(status, err, body) {
|
|
48
|
+
super(status, err, body);
|
|
49
|
+
this.name = "ComputerConflictError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Maps a toolset error body onto the typed error for its code, if one exists. */
|
|
53
|
+
function typedApiError(status, err, body) {
|
|
54
|
+
switch (err.code) {
|
|
55
|
+
case "computer_busy":
|
|
56
|
+
case "computer_controlled_elsewhere":
|
|
57
|
+
return new ComputerBusyError(status, err, body);
|
|
58
|
+
case "sandbox_required":
|
|
59
|
+
return new SandboxRequiredError(status, err, body);
|
|
60
|
+
case "computer_conflict":
|
|
61
|
+
return new ComputerConflictError(status, err, body);
|
|
62
|
+
default:
|
|
63
|
+
return new AllternitApiError(status, err, body);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class AllternitComputers {
|
|
67
|
+
baseUrl;
|
|
68
|
+
#apiKey;
|
|
69
|
+
#fetch;
|
|
70
|
+
constructor(opts = {}) {
|
|
71
|
+
const env = globalThis.process?.env;
|
|
72
|
+
const key = opts.apiKey ?? env?.ALLTERNIT_API_KEY;
|
|
73
|
+
if (!key)
|
|
74
|
+
throw new Error("AllternitComputers: apiKey is required (or set ALLTERNIT_API_KEY).");
|
|
75
|
+
this.#apiKey = key;
|
|
76
|
+
this.baseUrl = (opts.baseUrl ?? env?.ALLTERNIT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
77
|
+
this.#fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
78
|
+
}
|
|
79
|
+
create(body = {}) {
|
|
80
|
+
return this.#req("POST", "/v1/computers", body);
|
|
81
|
+
}
|
|
82
|
+
list(q = {}) {
|
|
83
|
+
return this.#req("GET", `/v1/computers${qs(q)}`);
|
|
84
|
+
}
|
|
85
|
+
get(id) {
|
|
86
|
+
return this.#req("GET", `/v1/computers/${enc(id)}`);
|
|
87
|
+
}
|
|
88
|
+
start(id) {
|
|
89
|
+
return this.#req("POST", `/v1/computers/${enc(id)}/start`, {});
|
|
90
|
+
}
|
|
91
|
+
stop(id) {
|
|
92
|
+
return this.#req("POST", `/v1/computers/${enc(id)}/stop`, {});
|
|
93
|
+
}
|
|
94
|
+
delete(id) {
|
|
95
|
+
return this.#req("DELETE", `/v1/computers/${enc(id)}`);
|
|
96
|
+
}
|
|
97
|
+
/** Run one toolset member. Action failures resolve with is_error:true; a held call throws ApprovalRequiredError. */
|
|
98
|
+
toolset(id, call) {
|
|
99
|
+
return this.#req("POST", `/v1/computers/${enc(id)}/toolset`, call);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Run one toolset member, answering a 409 approval_required hold. When the
|
|
103
|
+
* server holds the call and `onApproval` returns true, the approval is
|
|
104
|
+
* granted (POST /approvals/{id}) and the same call resent with the
|
|
105
|
+
* single-use `approval_grant`. When `onApproval` is missing or returns
|
|
106
|
+
* false, the held result resolves (is_error:true) so the model sees it.
|
|
107
|
+
*/
|
|
108
|
+
async toolsetWithApproval(id, call, onApproval) {
|
|
109
|
+
try {
|
|
110
|
+
return await this.toolset(id, call);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
if (!(e instanceof ApprovalRequiredError))
|
|
114
|
+
throw e;
|
|
115
|
+
if (!onApproval || !(await onApproval(e.approval)))
|
|
116
|
+
return e.result;
|
|
117
|
+
await this.approve(id, e.approval.id);
|
|
118
|
+
return this.toolset(id, { ...call, approval_grant: e.approval.id });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
schema(id, toolset = "computer") {
|
|
122
|
+
return this.#req("GET", `/v1/computers/${enc(id)}/toolset/schema${qs({ toolset })}`);
|
|
123
|
+
}
|
|
124
|
+
events(id, q = {}) {
|
|
125
|
+
return this.#req("GET", `/v1/computers/${enc(id)}/events${qs(q)}`);
|
|
126
|
+
}
|
|
127
|
+
approve(id, approvalId) {
|
|
128
|
+
return this.#req("POST", `/v1/computers/${enc(id)}/approvals/${enc(approvalId)}`, {});
|
|
129
|
+
}
|
|
130
|
+
async #req(method, path, body) {
|
|
131
|
+
const headers = { Authorization: `Bearer ${this.#apiKey}`, Accept: "application/json" };
|
|
132
|
+
if (body !== undefined)
|
|
133
|
+
headers["Content-Type"] = "application/json";
|
|
134
|
+
if (method === "POST")
|
|
135
|
+
headers["Idempotency-Key"] = crypto.randomUUID();
|
|
136
|
+
const res = await this.#fetch(this.baseUrl + path, {
|
|
137
|
+
method,
|
|
138
|
+
headers,
|
|
139
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
140
|
+
});
|
|
141
|
+
const text = await res.text();
|
|
142
|
+
let json = undefined;
|
|
143
|
+
try {
|
|
144
|
+
json = text ? JSON.parse(text) : undefined;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
json = { error: { message: text } };
|
|
148
|
+
}
|
|
149
|
+
if (res.ok)
|
|
150
|
+
return json;
|
|
151
|
+
const err = json?.error ?? {};
|
|
152
|
+
if (res.status === 409 && err.code === "approval_required" && json?.approval) {
|
|
153
|
+
throw new ApprovalRequiredError(err, json.approval, json.result ?? { is_error: true, content: [] }, json);
|
|
154
|
+
}
|
|
155
|
+
throw typedApiError(res.status, err, json);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Text of a result, joined. */
|
|
159
|
+
export function resultText(r) {
|
|
160
|
+
return r.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("\n");
|
|
161
|
+
}
|
|
162
|
+
/** First image of a result, if any. */
|
|
163
|
+
export function resultImage(r) {
|
|
164
|
+
const img = r.content.find((b) => b.type === "image");
|
|
165
|
+
return img && img.type === "image" ? { media_type: img.media_type, data: img.data } : undefined;
|
|
166
|
+
}
|
|
167
|
+
function enc(s) {
|
|
168
|
+
return encodeURIComponent(s);
|
|
169
|
+
}
|
|
170
|
+
function qs(q) {
|
|
171
|
+
const p = Object.entries(q).filter(([, v]) => v !== undefined && v !== "");
|
|
172
|
+
return p.length ? "?" + new URLSearchParams(p.map(([k, v]) => [k, String(v)])).toString() : "";
|
|
173
|
+
}
|