@sandboxrouter/sdk 0.2.0 → 0.3.1

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.
Files changed (91) hide show
  1. package/README.md +229 -50
  2. package/dist/bench.js +4 -4
  3. package/dist/catalog.d.ts +159 -0
  4. package/dist/catalog.js +450 -15
  5. package/dist/check.d.ts +28 -0
  6. package/dist/check.js +185 -0
  7. package/dist/cli.js +171 -12
  8. package/dist/computers.d.ts +55 -13
  9. package/dist/computers.js +132 -6
  10. package/dist/conformance.d.ts +46 -0
  11. package/dist/conformance.js +65 -0
  12. package/dist/cost.d.ts +67 -0
  13. package/dist/cost.js +153 -0
  14. package/dist/gc.d.ts +29 -0
  15. package/dist/gc.js +54 -0
  16. package/dist/index.d.ts +28 -2
  17. package/dist/index.js +28 -2
  18. package/dist/leaderboard.d.ts +6 -0
  19. package/dist/leaderboard.js +5 -4
  20. package/dist/ledger.d.ts +38 -0
  21. package/dist/ledger.js +89 -0
  22. package/dist/providers/agent37.d.ts +40 -0
  23. package/dist/providers/agent37.js +224 -0
  24. package/dist/providers/agentcore.d.ts +71 -0
  25. package/dist/providers/agentcore.js +446 -0
  26. package/dist/providers/alibaba.d.ts +18 -0
  27. package/dist/providers/alibaba.js +106 -0
  28. package/dist/providers/azure.d.ts +43 -0
  29. package/dist/providers/azure.js +330 -0
  30. package/dist/providers/beam.d.ts +11 -0
  31. package/dist/providers/beam.js +179 -0
  32. package/dist/providers/blaxel.d.ts +3 -1
  33. package/dist/providers/blaxel.js +32 -22
  34. package/dist/providers/boat.js +8 -3
  35. package/dist/providers/cloudflare.js +8 -3
  36. package/dist/providers/codesandbox.d.ts +59 -0
  37. package/dist/providers/codesandbox.js +287 -0
  38. package/dist/providers/daytona.d.ts +25 -4
  39. package/dist/providers/daytona.js +449 -28
  40. package/dist/providers/dedalus.d.ts +12 -0
  41. package/dist/providers/dedalus.js +244 -0
  42. package/dist/providers/deno.d.ts +50 -0
  43. package/dist/providers/deno.js +170 -0
  44. package/dist/providers/docker.js +11 -5
  45. package/dist/providers/e2b.d.ts +70 -3
  46. package/dist/providers/e2b.js +376 -8
  47. package/dist/providers/exedev.js +17 -5
  48. package/dist/providers/freestyle.d.ts +49 -0
  49. package/dist/providers/freestyle.js +261 -0
  50. package/dist/providers/hopx.js +27 -18
  51. package/dist/providers/instabox.d.ts +76 -0
  52. package/dist/providers/instabox.js +360 -0
  53. package/dist/providers/koyeb.d.ts +71 -0
  54. package/dist/providers/koyeb.js +379 -0
  55. package/dist/providers/lambda.d.ts +51 -0
  56. package/dist/providers/lambda.js +467 -0
  57. package/dist/providers/langsmith.d.ts +16 -0
  58. package/dist/providers/langsmith.js +149 -0
  59. package/dist/providers/limrun.d.ts +77 -0
  60. package/dist/providers/limrun.js +626 -0
  61. package/dist/providers/maritime.d.ts +5 -1
  62. package/dist/providers/maritime.js +39 -8
  63. package/dist/providers/mobilerun.d.ts +44 -0
  64. package/dist/providers/mobilerun.js +417 -0
  65. package/dist/providers/modal.js +12 -4
  66. package/dist/providers/morph.js +12 -4
  67. package/dist/providers/northflank.js +11 -13
  68. package/dist/providers/novita.d.ts +12 -0
  69. package/dist/providers/novita.js +81 -0
  70. package/dist/providers/openscale.d.ts +17 -0
  71. package/dist/providers/openscale.js +87 -0
  72. package/dist/providers/orgo.d.ts +45 -0
  73. package/dist/providers/orgo.js +414 -0
  74. package/dist/providers/sail.d.ts +32 -0
  75. package/dist/providers/sail.js +372 -0
  76. package/dist/providers/superserve.d.ts +25 -0
  77. package/dist/providers/superserve.js +337 -0
  78. package/dist/providers/tensorlake.js +17 -14
  79. package/dist/providers/upstash.d.ts +30 -0
  80. package/dist/providers/upstash.js +258 -0
  81. package/dist/providers/vercel.js +14 -3
  82. package/dist/providers/workspaces.d.ts +60 -0
  83. package/dist/providers/workspaces.js +662 -0
  84. package/dist/router.d.ts +41 -5
  85. package/dist/router.js +179 -23
  86. package/dist/server.d.ts +1 -1
  87. package/dist/server.js +22 -3
  88. package/dist/types.d.ts +13 -5
  89. package/dist/util.d.ts +28 -1
  90. package/dist/util.js +56 -7
  91. package/package.json +81 -5
@@ -0,0 +1,662 @@
1
+ import { createHash } from "node:crypto";
2
+ import { ProviderError } from "../types.js";
3
+ import { optionalImport } from "../util.js";
4
+ /**
5
+ * Amazon WorkSpaces Applications (formerly AppStream 2.0) with agent access: a Windows or Linux
6
+ * desktop streamed from a fleet you own, driven through AWS's managed MCP server.
7
+ *
8
+ * MCP endpoint: https://agentaccess-mcp.{region}.api.aws/mcp (Streamable HTTP, SigV4 service "agentaccess-mcp")
9
+ * Docs: https://docs.aws.amazon.com/appstream2/latest/developerguide/agent-access-mcp-server.html
10
+ * Sample: https://github.com/aws-samples/sample-code-for-workspaces-agent-access
11
+ *
12
+ * This adapter does not create fleets, images or stacks (that takes tens of minutes and a VPC).
13
+ * It attaches agents to a fleet + stack you provision once.
14
+ *
15
+ * ## Setup (once per region)
16
+ *
17
+ * 1. In the WorkSpaces Applications console choose Get started once, so the
18
+ * `AmazonAppStreamServiceAccess` service role exists.
19
+ * 2. Create a fleet: Always-On or On-Demand (Elastic and multi-session fleets are not supported by
20
+ * agent access), a current AWS base image (Windows Server, or a Linux base image such as Rocky
21
+ * Linux) running the latest WorkSpaces Applications agent, e.g. `stream.standard.medium`, in a VPC
22
+ * subnet with internet access. Set desired capacity to the number of agents you want to run at once.
23
+ * 3. Create a stack, associate it with the fleet, and turn on agent access with COMPUTER_VISION and
24
+ * COMPUTER_INPUT enabled, screen resolution 1280x720 (the only one supported), image type PNG, and
25
+ * UserControlMode VIEW_ONLY or VIEW_STOP if you want `viewer()` links to work. With the CLI:
26
+ * aws appstream create-stack --name my-agents --agent-access-config \
27
+ * 'Settings=[{AgentAction=COMPUTER_VISION,Permission=ENABLED},{AgentAction=COMPUTER_INPUT,Permission=ENABLED}],ScreenResolution=W_1280xH_720,ScreenImageFormat=PNG,UserControlMode=VIEW_ONLY'
28
+ * aws appstream associate-fleet --stack-name my-agents --fleet-name my-fleet
29
+ * aws appstream start-fleet --name my-fleet
30
+ * (or run ./scripts/setup.sh from the AWS sample repo, which builds VPC + fleet + stack for you.)
31
+ * 4. Give the identity that runs this code:
32
+ * { "Effect": "Allow", "Action": ["agentaccess-mcp:*"], "Resource": "*" }
33
+ * { "Effect": "Allow", "Action": ["appstream:CreateStreamingURL", "appstream:DescribeFleets"], "Resource": "*" }
34
+ * 5. Set the env: AWS credentials (standard chain: env vars, profile, SSO, instance role),
35
+ * AWS_REGION, WORKSPACES_AGENT_STACK, WORKSPACES_AGENT_FLEET, and WORKSPACES_AGENT_OS
36
+ * ("windows" or "linux", what the fleet runs; default windows). Optional:
37
+ * WORKSPACES_AGENT_USER_ID, the AppStream user id used when a spec has no externalUserId.
38
+ *
39
+ * Packages: `@smithy/signature-v4`, `@aws-crypto/sha256-js` and `@aws-sdk/credential-provider-node`
40
+ * for signing; `@aws-sdk/client-appstream` only for `viewer()` when the session doesn't hand out a URL.
41
+ *
42
+ * ## How a computer maps onto the service
43
+ *
44
+ * createComputer sends MCP `initialize` with the stack and user id headers, and the MCP server
45
+ * creates (or reattaches to) that user's streaming session. nativeId is the MCP session id, plus
46
+ * `~userId` when a user id was sent. delete() sends the HTTP DELETE that ends the MCP session with
47
+ * `x-amzn-agentaccess-expire-streaming-session-on-delete: true`, which also expires the desktop
48
+ * session (the instance is recycled). If the MCP server forgets a session (HTTP 404), the next call
49
+ * opens a new one for the same user and nativeId changes; the desktop survives until the fleet's
50
+ * disconnect timeout.
51
+ *
52
+ * Cost notes: every distinct user id is a WorkSpaces "AI agent user", $6.42/month on Windows fleets
53
+ * (no user fee on Linux), so reuse ids. A named user has one session per fleet and a session takes
54
+ * one agent, so concurrent computers need distinct user ids. Always-On instances bill while running
55
+ * whether or not an agent is attached; On-Demand fleets bill $0.025/h for stopped instances.
56
+ */
57
+ const ID = "aws-workspaces";
58
+ /** ProviderId is a closed union in types.ts; this adapter's id isn't in it yet. */
59
+ const PID = ID;
60
+ const PREFIX = "agentaccess___";
61
+ const PROTOCOL_VERSION = "2025-06-18";
62
+ const SCREEN = { width: 1280, height: 720 };
63
+ const TICKS_PER_NOTCH = 120;
64
+ const MAX_TYPE_CHARS = 10_000;
65
+ /** AppStream UserId: 2-128 of [\w+=,.@-]. */
66
+ const USER_ID_RE = /^[\w+=,.@-]{2,128}$/;
67
+ const MODIFIERS = {
68
+ ctrl: "ctrl", control: "ctrl", alt: "alt", option: "alt", shift: "shift",
69
+ super: "super", win: "super", windows: "super", meta: "super", cmd: "super", command: "super",
70
+ };
71
+ export class WorkspacesAgentProvider {
72
+ id = ID;
73
+ computerOs;
74
+ region;
75
+ stack;
76
+ fleet;
77
+ os;
78
+ /** @internal */ opts;
79
+ signer;
80
+ constructor(opts = {}) {
81
+ this.opts = opts;
82
+ this.region = opts.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
83
+ this.stack = opts.stack ?? process.env.WORKSPACES_AGENT_STACK;
84
+ this.fleet = opts.fleet ?? process.env.WORKSPACES_AGENT_FLEET;
85
+ this.os = (opts.os ?? process.env.WORKSPACES_AGENT_OS ?? "windows").toLowerCase() === "linux" ? "linux" : "windows";
86
+ this.computerOs = [this.os];
87
+ }
88
+ configured() {
89
+ return !!(this.region && this.stack && this.fleet);
90
+ }
91
+ /** @internal */
92
+ get endpoint() {
93
+ return this.opts.endpoint ?? `https://agentaccess-mcp.${this.region}.api.aws/mcp`;
94
+ }
95
+ /** @internal */
96
+ requireConfig() {
97
+ if (!this.configured())
98
+ throw new ProviderError(PID, "set AWS_REGION, WORKSPACES_AGENT_STACK and WORKSPACES_AGENT_FLEET (see src/providers/workspaces.ts for the one-time fleet + stack setup)");
99
+ return { region: this.region, stack: this.stack, fleet: this.fleet };
100
+ }
101
+ /** @internal SigV4 signer for service agentaccess-mcp, built on first use. */
102
+ getSigner() {
103
+ this.signer ??= (async () => {
104
+ const { region } = this.requireConfig();
105
+ const { SignatureV4 } = await optionalImport("@smithy/signature-v4", "aws-workspaces");
106
+ const { Sha256 } = await optionalImport("@aws-crypto/sha256-js", "aws-workspaces");
107
+ let credentials = this.opts.credentials;
108
+ if (!credentials) {
109
+ const { defaultProvider } = await optionalImport("@aws-sdk/credential-provider-node", "aws-workspaces");
110
+ credentials = defaultProvider();
111
+ }
112
+ return new SignatureV4({ service: "agentaccess-mcp", region, credentials, sha256: Sha256, applyChecksum: false });
113
+ })();
114
+ this.signer.catch(() => (this.signer = undefined));
115
+ return this.signer;
116
+ }
117
+ /** The user id for a spec: externalUserId as-is when AppStream accepts it, else a stable hash of it. */
118
+ userIdFor(spec) {
119
+ const raw = spec.externalUserId ?? this.opts.userId ?? process.env.WORKSPACES_AGENT_USER_ID;
120
+ if (raw === undefined || raw === "")
121
+ return undefined;
122
+ if (USER_ID_RE.test(raw))
123
+ return raw;
124
+ return `sbr-${createHash("sha256").update(raw).digest("hex").slice(0, 40)}`;
125
+ }
126
+ async createComputer(spec) {
127
+ this.requireConfig();
128
+ if (spec.os && spec.os !== this.os)
129
+ throw new ProviderError(PID, `this fleet runs ${this.os} (WORKSPACES_AGENT_OS), not ${spec.os}`);
130
+ const session = new McpSession(this, this.userIdFor(spec));
131
+ await session.open();
132
+ return new WorkspacesComputer(this, session, spec.externalUserId ?? null);
133
+ }
134
+ async connectComputer(nativeId) {
135
+ this.requireConfig();
136
+ const i = nativeId.lastIndexOf("~");
137
+ const sessionId = i === -1 ? nativeId : nativeId.slice(0, i);
138
+ const userId = i === -1 ? undefined : nativeId.slice(i + 1);
139
+ if (!sessionId)
140
+ throw new ProviderError(PID, `bad computer id ${JSON.stringify(nativeId)}`);
141
+ const session = new McpSession(this, userId, sessionId);
142
+ await session.tools(); // proves the session is live (reopens it on 404)
143
+ return new WorkspacesComputer(this, session, userId ?? null);
144
+ }
145
+ /** @internal Uses @aws-sdk/client-appstream CreateStreamingURL. */
146
+ async createStreamingUrl(userId, validitySeconds) {
147
+ const { region, stack, fleet } = this.requireConfig();
148
+ const { AppStreamClient, CreateStreamingURLCommand } = await optionalImport("@aws-sdk/client-appstream", "aws-workspaces viewer");
149
+ const client = new AppStreamClient({ region, ...(this.opts.credentials ? { credentials: this.opts.credentials } : {}) });
150
+ const r = await client.send(new CreateStreamingURLCommand({ StackName: stack, FleetName: fleet, UserId: userId, Validity: validitySeconds }));
151
+ if (!r.StreamingURL)
152
+ throw new ProviderError(PID, "CreateStreamingURL returned no URL");
153
+ return { url: r.StreamingURL, expiresAt: r.Expires ? new Date(r.Expires).toISOString() : null };
154
+ }
155
+ }
156
+ /** Minimal MCP Streamable HTTP client: POST JSON-RPC, read a JSON or SSE answer, DELETE to end. */
157
+ class McpSession {
158
+ p;
159
+ userId;
160
+ sessionId;
161
+ nextId = 1;
162
+ protocolVersion = PROTOCOL_VERSION;
163
+ toolNames;
164
+ ended = false;
165
+ constructor(p, userId, sessionId) {
166
+ this.p = p;
167
+ this.userId = userId;
168
+ this.sessionId = sessionId;
169
+ }
170
+ get nativeId() {
171
+ return this.userId ? `${this.sessionId}~${this.userId}` : this.sessionId;
172
+ }
173
+ headers() {
174
+ const { stack } = this.p.requireConfig();
175
+ const h = {
176
+ "x-amzn-agentaccess-workspaces-applications-stack": stack,
177
+ "x-amzn-agentaccess-expire-streaming-session-on-delete": "true",
178
+ };
179
+ if (this.userId)
180
+ h["x-amzn-agentaccess-workspaces-applications-user-id"] = this.userId;
181
+ if (this.sessionId) {
182
+ h["mcp-session-id"] = this.sessionId;
183
+ h["mcp-protocol-version"] = this.protocolVersion;
184
+ }
185
+ return h;
186
+ }
187
+ /** Signs and sends one HTTP request to the MCP endpoint. */
188
+ async send(method, body) {
189
+ const url = new URL(this.p.endpoint);
190
+ const headers = { host: url.host, ...this.headers() };
191
+ if (body !== undefined) {
192
+ headers["content-type"] = "application/json";
193
+ headers.accept = "application/json, text/event-stream";
194
+ }
195
+ const query = {};
196
+ url.searchParams.forEach((v, k) => (query[k] = v));
197
+ const signed = await (await this.p.getSigner()).sign({
198
+ method,
199
+ protocol: url.protocol,
200
+ hostname: url.hostname,
201
+ port: url.port ? Number(url.port) : undefined,
202
+ path: url.pathname,
203
+ query,
204
+ headers,
205
+ body,
206
+ });
207
+ const out = { ...signed.headers };
208
+ for (const k of Object.keys(out))
209
+ if (k.toLowerCase() === "host")
210
+ delete out[k]; // fetch sets it from the URL
211
+ try {
212
+ return await fetch(url, { method, headers: out, body, signal: AbortSignal.timeout(this.p.opts.httpTimeoutMs ?? 300_000) });
213
+ }
214
+ catch (err) {
215
+ throw new ProviderError(PID, `${method} ${url.host}${url.pathname}: ${err.message}`);
216
+ }
217
+ }
218
+ async fail(res, what) {
219
+ const text = (await res.text().catch(() => "")).slice(0, 400);
220
+ let detail = text;
221
+ try {
222
+ const j = JSON.parse(text);
223
+ detail = j?.error?.message ?? j?.message ?? j?.Message ?? text;
224
+ }
225
+ catch {
226
+ /* not JSON */
227
+ }
228
+ throw new ProviderError(PID, `${what} -> HTTP ${res.status} ${detail}`.trim(), res.status);
229
+ }
230
+ /** One JSON-RPC request; the answer may come as JSON or as an SSE stream. */
231
+ async rpc(method, params = {}, retried = false) {
232
+ if (this.ended)
233
+ throw new ProviderError(PID, "this computer was deleted");
234
+ const id = this.nextId++;
235
+ const res = await this.send("POST", JSON.stringify({ jsonrpc: "2.0", id, method, params }));
236
+ if (res.status === 404 && this.sessionId && method !== "initialize" && !retried) {
237
+ // Streamable HTTP: a 404 for a session id means the server ended it; start a new one.
238
+ await res.body?.cancel().catch(() => { });
239
+ this.sessionId = undefined;
240
+ this.toolNames = undefined;
241
+ await this.initialize();
242
+ return this.rpc(method, params, true);
243
+ }
244
+ if (!res.ok)
245
+ await this.fail(res, method);
246
+ const sid = res.headers.get("mcp-session-id");
247
+ if (sid && method === "initialize")
248
+ this.sessionId = sid;
249
+ const msg = await readAnswer(res, id);
250
+ if (msg.error)
251
+ throw new ProviderError(PID, `${method}: ${msg.error.message}${msg.error.code ? ` (${msg.error.code})` : ""}`);
252
+ return msg.result;
253
+ }
254
+ async notify(method) {
255
+ const res = await this.send("POST", JSON.stringify({ jsonrpc: "2.0", method }));
256
+ if (!res.ok)
257
+ await this.fail(res, method);
258
+ await res.body?.cancel().catch(() => { });
259
+ }
260
+ async initialize() {
261
+ const r = await this.rpc("initialize", {
262
+ protocolVersion: PROTOCOL_VERSION,
263
+ capabilities: {},
264
+ clientInfo: { name: "sandboxrouter", version: "0.2.0" },
265
+ });
266
+ if (!this.sessionId)
267
+ throw new ProviderError(PID, "initialize: the MCP server returned no Mcp-Session-Id");
268
+ if (typeof r?.protocolVersion === "string")
269
+ this.protocolVersion = r.protocolVersion;
270
+ await this.notify("notifications/initialized");
271
+ }
272
+ /** initialize, then wait until tools/list shows the desktop tools (the desktop may still be connecting). */
273
+ async open() {
274
+ await this.initialize();
275
+ const deadline = Date.now() + (this.p.opts.readyTimeoutMs ?? 600_000);
276
+ let lastError = "";
277
+ for (;;) {
278
+ try {
279
+ const names = await this.tools(true);
280
+ if (names.has(`${PREFIX}screenshot`))
281
+ return;
282
+ lastError = `tools/list has no ${PREFIX}screenshot (is COMPUTER_VISION enabled on the stack?): ${[...names].join(", ") || "no tools"}`;
283
+ }
284
+ catch (err) {
285
+ lastError = err.message.replace(`${ID}: `, "");
286
+ // Bad credentials or another agent holding the desktop won't fix themselves; a booting desktop will.
287
+ if ((err instanceof ProviderError && (err.status === 401 || err.status === 403)) || /CONNECTION_LIMIT_REACHED|already connected/i.test(lastError))
288
+ break;
289
+ }
290
+ if (Date.now() >= deadline)
291
+ break;
292
+ await sleep(this.p.opts.pollMs ?? 3000);
293
+ }
294
+ await this.end().catch(() => { });
295
+ throw new ProviderError(PID, `the desktop did not become ready: ${lastError}`);
296
+ }
297
+ async tools(refresh = false) {
298
+ if (this.toolNames && !refresh)
299
+ return this.toolNames;
300
+ const names = new Set();
301
+ let cursor;
302
+ do {
303
+ const r = await this.rpc("tools/list", cursor ? { cursor } : {});
304
+ if (r?.isError)
305
+ throw new ProviderError(PID, `tools/list refused: ${textOf(r)}`);
306
+ for (const t of r?.tools ?? [])
307
+ names.add(t.name);
308
+ cursor = r?.nextCursor;
309
+ } while (cursor);
310
+ this.toolNames = names;
311
+ return names;
312
+ }
313
+ /** tools/call with the service prefix added to short names. */
314
+ call(name, args = {}) {
315
+ const full = name.includes("___") ? name : PREFIX + name;
316
+ return this.rpc("tools/call", { name: full, arguments: args });
317
+ }
318
+ /** HTTP DELETE ends the MCP session (and, with our header, the streaming session). */
319
+ async end() {
320
+ if (!this.sessionId || this.ended)
321
+ return;
322
+ const res = await this.send("DELETE");
323
+ if (res.status !== 404 && !res.ok)
324
+ await this.fail(res, "DELETE session"); // 404: already gone
325
+ await res.body?.cancel().catch(() => { });
326
+ this.ended = true;
327
+ }
328
+ }
329
+ /** The JSON-RPC response with this id, from a JSON body or a text/event-stream body. */
330
+ async function readAnswer(res, id) {
331
+ const type = res.headers.get("content-type") ?? "";
332
+ if (!type.includes("text/event-stream")) {
333
+ const text = await res.text();
334
+ const body = text ? JSON.parse(text) : undefined;
335
+ const msgs = Array.isArray(body) ? body : body ? [body] : [];
336
+ const m = msgs.find((x) => x.id === id);
337
+ if (!m)
338
+ throw new ProviderError(PID, `no JSON-RPC response for request ${id}`);
339
+ return m;
340
+ }
341
+ const reader = res.body.getReader();
342
+ const decoder = new TextDecoder();
343
+ let buffer = "";
344
+ let data = [];
345
+ try {
346
+ for (;;) {
347
+ const { done, value } = await reader.read();
348
+ buffer += done ? decoder.decode() + "\n\n" : decoder.decode(value, { stream: true });
349
+ let nl;
350
+ while ((nl = buffer.indexOf("\n")) !== -1) {
351
+ const line = buffer.slice(0, nl).replace(/\r$/, "");
352
+ buffer = buffer.slice(nl + 1);
353
+ if (line.startsWith("data:"))
354
+ data.push(line.slice(5).replace(/^ /, ""));
355
+ else if (line === "" && data.length) {
356
+ const payload = data.join("\n");
357
+ data = [];
358
+ let msg;
359
+ try {
360
+ msg = JSON.parse(payload);
361
+ }
362
+ catch {
363
+ continue;
364
+ }
365
+ // Skip server notifications and requests; return our response.
366
+ if (msg && msg.id === id && (msg.result !== undefined || msg.error !== undefined))
367
+ return msg;
368
+ }
369
+ }
370
+ if (done)
371
+ throw new ProviderError(PID, `event stream ended without a response to request ${id}`);
372
+ }
373
+ }
374
+ finally {
375
+ reader.cancel().catch(() => { });
376
+ }
377
+ }
378
+ function textOf(r) {
379
+ return (r.content ?? []).filter((c) => c.type === "text" && c.text).map((c) => c.text).join(" ").trim();
380
+ }
381
+ function sleep(ms) {
382
+ return new Promise((r) => setTimeout(r, ms));
383
+ }
384
+ /** Width and height from PNG IHDR or a JPEG SOF marker. */
385
+ function imageSize(b) {
386
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
387
+ if (b.length >= 24 && b[0] === 0x89 && b[1] === 0x50)
388
+ return { width: v.getUint32(16), height: v.getUint32(20) };
389
+ if (b.length > 4 && b[0] === 0xff && b[1] === 0xd8) {
390
+ let i = 2;
391
+ while (i + 9 < b.length) {
392
+ if (b[i] !== 0xff)
393
+ return {};
394
+ const marker = b[i + 1];
395
+ const len = v.getUint16(i + 2);
396
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
397
+ return { height: v.getUint16(i + 5), width: v.getUint16(i + 7) };
398
+ }
399
+ i += 2 + len;
400
+ }
401
+ }
402
+ return {};
403
+ }
404
+ class ActionError extends Error {
405
+ }
406
+ /** A streaming session on your fleet, driven by the agent access MCP tools. */
407
+ class WorkspacesComputer {
408
+ p;
409
+ s;
410
+ externalUserId;
411
+ frame = 0;
412
+ pointer;
413
+ constructor(p, s, externalUserId) {
414
+ this.p = p;
415
+ this.s = s;
416
+ this.externalUserId = externalUserId;
417
+ }
418
+ get nativeId() {
419
+ return this.s.nativeId;
420
+ }
421
+ /** get_session_info: mcpSessionId, stack, userId, workSpacesSessionId, fleet. */
422
+ async sessionInfo() {
423
+ const r = await this.s.call("get_session_info");
424
+ if (r.isError)
425
+ throw new ProviderError(PID, `get_session_info: ${textOf(r)}`);
426
+ let info = {};
427
+ if (r.structuredContent && typeof r.structuredContent === "object")
428
+ info = r.structuredContent;
429
+ else {
430
+ try {
431
+ info = JSON.parse(textOf(r));
432
+ }
433
+ catch {
434
+ /* free text */
435
+ }
436
+ }
437
+ return { info, meta: r._meta ?? {} };
438
+ }
439
+ async info() {
440
+ const { info } = await this.sessionInfo();
441
+ return {
442
+ id: this.nativeId,
443
+ os: this.p.os,
444
+ status: "ready",
445
+ screen: { ...SCREEN },
446
+ externalUserId: this.externalUserId ?? (typeof info.userId === "string" ? info.userId : null),
447
+ };
448
+ }
449
+ /** Any tool the session lists, e.g. launch_application, toggle_app_switcher or forwarded___server___tool. */
450
+ callTool(name, args = {}) {
451
+ return this.s.call(name, args);
452
+ }
453
+ /** The service's tools, prefixed (agentaccess___screenshot, ...). */
454
+ async listTools() {
455
+ return [...(await this.s.tools(true))];
456
+ }
457
+ async shot() {
458
+ const r = await this.s.call("screenshot");
459
+ if (r.isError)
460
+ throw new ActionError(`screenshot: ${textOf(r) || "failed"}`);
461
+ const img = (r.content ?? []).find((c) => c.type === "image" && c.data);
462
+ if (!img)
463
+ throw new ActionError("screenshot returned no image");
464
+ return { bytes: new Uint8Array(Buffer.from(img.data, "base64")), mime: img.mimeType ?? "image/png" };
465
+ }
466
+ async act(action) {
467
+ try {
468
+ const extra = await this.perform(action);
469
+ if (action.action === "screenshot" || !action.noScreenshot) {
470
+ const { bytes, mime } = await this.shot();
471
+ return { action: action.action, ok: true, ...extra, frameId: ++this.frame, imageB64: Buffer.from(bytes).toString("base64"), mime, ...imageSize(bytes) };
472
+ }
473
+ return { action: action.action, ok: true, ...extra };
474
+ }
475
+ catch (err) {
476
+ return { action: action.action, ok: false, error: err?.message ?? String(err) };
477
+ }
478
+ }
479
+ /** In order; stops at the first failure (later coordinates may no longer apply). */
480
+ async actBatch(actions) {
481
+ const out = [];
482
+ for (const a of actions) {
483
+ const r = await this.act(a);
484
+ out.push(r);
485
+ if (!r.ok)
486
+ break;
487
+ }
488
+ return out;
489
+ }
490
+ async tool(name, args = {}) {
491
+ const r = await this.s.call(name, args);
492
+ if (r.isError)
493
+ throw new ActionError(`${name}: ${textOf(r) || "failed"}`);
494
+ }
495
+ point(a, what = "coordinate") {
496
+ const c = a.coordinate ?? this.pointer;
497
+ if (!c)
498
+ throw new ActionError(`${a.action} needs a ${what} [x, y]`);
499
+ return [Math.round(c[0]), Math.round(c[1])];
500
+ }
501
+ modifiers(a) {
502
+ if (!a.modifier?.trim())
503
+ return {};
504
+ const parts = a.modifier.split(/[+\s]+/).map((x) => x.trim().toLowerCase()).filter(Boolean);
505
+ const bad = parts.filter((x) => !MODIFIERS[x]);
506
+ if (bad.length)
507
+ throw new ActionError(`unsupported modifier ${JSON.stringify(bad.join("+"))}: use ctrl, alt, shift or super`);
508
+ return { modifiers: [...new Set(parts.map((x) => MODIFIERS[x]))].join("+") };
509
+ }
510
+ async perform(a) {
511
+ switch (a.action) {
512
+ case "screenshot":
513
+ return {};
514
+ case "left_click":
515
+ case "right_click":
516
+ case "middle_click":
517
+ case "double_click":
518
+ case "triple_click": {
519
+ const [x, y] = this.point(a);
520
+ const mods = this.modifiers(a);
521
+ for (let i = 0; i < Math.max(1, Math.round(a.repeat ?? 1)); i++)
522
+ await this.tool(a.action, { x, y, ...mods });
523
+ this.pointer = [x, y];
524
+ return {};
525
+ }
526
+ case "mouse_move": {
527
+ if (!a.coordinate)
528
+ throw new ActionError("mouse_move needs a coordinate [x, y]");
529
+ const [x, y] = this.point(a);
530
+ await this.tool("move_pointer", { x, y });
531
+ this.pointer = [x, y];
532
+ return {};
533
+ }
534
+ case "left_click_drag": {
535
+ if (!a.coordinate)
536
+ throw new ActionError("left_click_drag needs a coordinate [x, y] to drag to");
537
+ const from = a.startCoordinate ?? this.pointer;
538
+ if (!from)
539
+ throw new ActionError("left_click_drag needs a startCoordinate [x, y]");
540
+ const end = [Math.round(a.coordinate[0]), Math.round(a.coordinate[1])];
541
+ await this.tool("left_click_drag", { start_x: Math.round(from[0]), start_y: Math.round(from[1]), end_x: end[0], end_y: end[1] });
542
+ this.pointer = end;
543
+ return {};
544
+ }
545
+ case "left_mouse_down":
546
+ case "left_mouse_up": {
547
+ const [x, y] = this.point(a);
548
+ await this.tool(a.action, { x, y, ...this.modifiers(a) });
549
+ this.pointer = [x, y];
550
+ return {};
551
+ }
552
+ case "scroll": {
553
+ const [x, y] = this.point(a);
554
+ const dir = a.scrollDirection ?? "down";
555
+ const notches = Math.round(a.scrollAmount ?? 3);
556
+ if (notches < 1 || notches > 100)
557
+ throw new ActionError("scrollAmount must be 1 to 100 wheel notches");
558
+ await this.tool("scroll", {
559
+ x,
560
+ y,
561
+ scroll_direction: dir[0].toUpperCase() + dir.slice(1),
562
+ scroll_amount: notches * TICKS_PER_NOTCH,
563
+ ...this.modifiers(a),
564
+ });
565
+ this.pointer = [x, y];
566
+ return {};
567
+ }
568
+ case "type": {
569
+ if (!a.text)
570
+ throw new ActionError("type needs text");
571
+ for (let i = 0; i < a.text.length; i += MAX_TYPE_CHARS)
572
+ await this.tool("type_text", { text: a.text.slice(i, i + MAX_TYPE_CHARS) });
573
+ return {};
574
+ }
575
+ case "key": {
576
+ if (!a.text?.trim())
577
+ throw new ActionError('key needs text, e.g. "Return" or "ctrl+s"');
578
+ // xdotool-style "ctrl+a Delete" is a sequence of presses; the service takes one combo per call.
579
+ const combos = a.text.trim().split(/\s+/);
580
+ for (let i = 0; i < Math.max(1, Math.round(a.repeat ?? 1)); i++)
581
+ for (const keys of combos)
582
+ await this.tool("key", { keys });
583
+ return {};
584
+ }
585
+ case "hold_key": {
586
+ if (!a.text?.trim())
587
+ throw new ActionError("hold_key needs text");
588
+ const duration = a.duration ?? 1;
589
+ if (duration < 1 || duration > 30)
590
+ throw new ActionError("hold_key duration must be 1 to 30 seconds");
591
+ await this.tool("hold_key", { keys: a.text.trim(), duration });
592
+ return {};
593
+ }
594
+ case "wait": {
595
+ // The service has no wait tool; pause here.
596
+ const secs = a.duration ?? 1;
597
+ if (secs < 0 || secs > 100)
598
+ throw new ActionError("wait duration must be 0 to 100 seconds");
599
+ await sleep(secs * 1000);
600
+ return {};
601
+ }
602
+ case "cursor_position": {
603
+ // No tool reports the pointer; this is where our last action left it.
604
+ if (!this.pointer)
605
+ throw new ActionError("the pointer position is not known yet: move the mouse or click first");
606
+ return { x: this.pointer[0], y: this.pointer[1], coordinate: [...this.pointer] };
607
+ }
608
+ case "zoom":
609
+ throw new ActionError("aws-workspaces does not support zoom; take a screenshot instead");
610
+ default:
611
+ throw new ActionError(`aws-workspaces does not support the "${a.action}" action`);
612
+ }
613
+ }
614
+ /** The service picks the format (the stack's ScreenImageFormat); `format`/`quality` are ignored. */
615
+ async screenshot(_opts = {}) {
616
+ try {
617
+ return (await this.shot()).bytes;
618
+ }
619
+ catch (err) {
620
+ if (err instanceof ActionError)
621
+ throw new ProviderError(PID, err.message);
622
+ throw err;
623
+ }
624
+ }
625
+ noShell(what) {
626
+ throw new ProviderError(PID, `${what} is not available: WorkSpaces agent access exposes only screen, mouse, keyboard, app-launch and session tools, no shell or file tools. ` +
627
+ `Drive a terminal through act(), or bake an MCP server into the fleet image and enable FORWARD_MCP_TOOLS, then use callTool("forwarded___<server>___<tool>").`);
628
+ }
629
+ async exec(_command, _timeoutSeconds) {
630
+ this.noShell("exec");
631
+ }
632
+ async readFile(_path) {
633
+ this.noShell("readFile");
634
+ }
635
+ async writeFile(_path, _content) {
636
+ this.noShell("writeFile");
637
+ }
638
+ /**
639
+ * A browser link to this desktop session. `watch` only: what the person can do is enforced by the
640
+ * service through the stack's UserControlMode (VIEW_ONLY: watch; VIEW_STOP: watch plus a stop
641
+ * button that ends the agent's session; DISABLED: no view). There is no documented way to hand a
642
+ * person control of an agent session, so `control` throws.
643
+ * Without ttlSeconds the session's own `_meta.streaming_url` from get_session_info is used when the
644
+ * server provides one (expiry not reported); otherwise CreateStreamingURL (default 3600 s).
645
+ */
646
+ async viewer(mode = "watch", opts = {}) {
647
+ if (mode === "control") {
648
+ throw new ProviderError(PID, "WorkSpaces agent access has no takeover link: observers can watch (UserControlMode VIEW_ONLY) or watch and stop the agent (VIEW_STOP)");
649
+ }
650
+ const { info, meta } = await this.sessionInfo();
651
+ if (opts.ttlSeconds === undefined && typeof meta.streaming_url === "string")
652
+ return { url: meta.streaming_url, expiresAt: null };
653
+ const userId = typeof info.userId === "string" ? info.userId : this.s.userId;
654
+ if (!userId)
655
+ throw new ProviderError(PID, "get_session_info returned no userId, so no streaming URL can be made");
656
+ const ttl = Math.min(604_800, Math.max(1, Math.round(opts.ttlSeconds ?? 3600)));
657
+ return this.p.createStreamingUrl(userId, ttl);
658
+ }
659
+ async delete() {
660
+ await this.s.end();
661
+ }
662
+ }