@shardflux/sdk 0.14.0 → 0.16.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +85 -23
  2. package/README.md +226 -7
  3. package/dist/account.d.ts +2 -0
  4. package/dist/account.js +6 -0
  5. package/dist/cell.d.ts +27 -0
  6. package/dist/cell.js +71 -0
  7. package/dist/client.d.ts +54 -5
  8. package/dist/client.js +100 -7
  9. package/dist/computer.d.ts +153 -0
  10. package/dist/computer.js +229 -0
  11. package/dist/errors.d.ts +5 -1
  12. package/dist/errors.js +9 -0
  13. package/dist/executions.d.ts +2 -6
  14. package/dist/executions.js +9 -0
  15. package/dist/exit-code.d.ts +7 -0
  16. package/dist/exit-code.js +12 -0
  17. package/dist/generated/app-api.d.ts +1055 -119
  18. package/dist/generated/cell-api.d.ts +334 -0
  19. package/dist/http.d.ts +7 -1
  20. package/dist/http.js +34 -13
  21. package/dist/index.d.ts +13 -6
  22. package/dist/index.js +7 -2
  23. package/dist/ports.d.ts +7 -0
  24. package/dist/ports.js +1 -1
  25. package/dist/progress.d.ts +2 -2
  26. package/dist/progress.js +1 -1
  27. package/dist/templates.d.ts +12 -0
  28. package/dist/templates.js +9 -0
  29. package/dist/testing/index.d.ts +62 -0
  30. package/dist/testing/index.js +585 -0
  31. package/dist/testing/seed.d.ts +433 -0
  32. package/dist/testing/seed.js +449 -0
  33. package/dist/tools.d.ts +21 -3
  34. package/dist/tools.js +113 -22
  35. package/dist/tunnel-assets/linux-amd64.gz +0 -0
  36. package/dist/tunnel-assets/linux-arm64.gz +0 -0
  37. package/dist/tunnel-assets.d.ts +10 -0
  38. package/dist/tunnel-assets.js +11 -0
  39. package/dist/tunnel-packet.d.ts +3 -0
  40. package/dist/tunnel-packet.js +43 -0
  41. package/dist/tunnel-pty.d.ts +86 -0
  42. package/dist/tunnel-pty.js +243 -0
  43. package/dist/tunnels.d.ts +47 -0
  44. package/dist/tunnels.js +454 -0
  45. package/dist/workspace-ref.d.ts +87 -0
  46. package/dist/workspace-ref.js +173 -0
  47. package/dist/workspace.d.ts +40 -1
  48. package/dist/workspace.js +111 -2
  49. package/package.json +7 -2
package/dist/tools.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * application; toOpenAITools/toAnthropicTools export the definitions in those
11
11
  * providers' formats and executeToolCall dispatches a model's tool call.
12
12
  */
13
+ import { runComputerActions } from "./computer.js";
13
14
  import { CAPTURE_BARRIER, burstFailure } from "./cell.js";
14
15
  import { ExecStartError, ShardfluxApiError } from "./errors.js";
15
16
  import { newExecutionId } from "./executions.js";
@@ -82,6 +83,8 @@ export function validateArgs(schema, value, path = '$') {
82
83
  issues.push(`${path} must be one of ${schema.enum.join(', ')}`);
83
84
  return issues;
84
85
  }
86
+ // Without a token yet (no grantedTools) and no opts.tools, every tool but computer: computer is offered only when a
87
+ // token grants it (the workspace's computer use is on) or opts.tools names it.
85
88
  const ALL = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
86
89
  /** The tool permissions whose tools work on a file-first workspace: files and executions. */
87
90
  const FILE_FIRST_TOOLS = ['exec', 'files'];
@@ -97,6 +100,45 @@ const RESOURCE_HINTS = new Set(['auto', 'light', 'heavy']);
97
100
  /** Tools served from a sleeping workspace's disk without waking it: the runner sends no hint. */
98
101
  const DISK_READS = new Set(['read_file', 'list_files', 'search_files']);
99
102
  const obj = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
103
+ /** One computer toolset action's parameters (the computer tool's and each computer_batch item's). */
104
+ const COMPUTER_ACTION = {
105
+ action: {
106
+ type: 'string',
107
+ enum: ['screenshot', 'zoom', 'left_click', 'right_click', 'middle_click', 'double_click', 'triple_click', 'left_click_drag', 'mouse_move',
108
+ 'left_mouse_down', 'left_mouse_up', 'cursor_position', 'scroll', 'type', 'key', 'hold_key', 'wait'],
109
+ },
110
+ coordinate: { type: 'array', items: { type: 'integer', minimum: 0 }, minItems: 2, maxItems: 2, description: '[x, y]. Required for mouse_move and as the end of left_click_drag; optional for clicks and scroll (default: at the pointer).' },
111
+ start_coordinate: { type: 'array', items: { type: 'integer', minimum: 0 }, minItems: 2, maxItems: 2, description: 'left_click_drag: where the drag starts.' },
112
+ region: { type: 'array', items: { type: 'integer', minimum: 0 }, minItems: 4, maxItems: 4, description: 'zoom: [x0, y0, x1, y1], shown enlarged.' },
113
+ text: { type: 'string', maxLength: 20_000, description: 'type: the text. key, hold_key: keys such as "Return", "ctrl+s", "alt+Tab" (space-separated for a sequence). Clicks, drag, scroll: modifiers held, e.g. "shift".' },
114
+ scroll_direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
115
+ scroll_amount: { type: 'integer', minimum: 1, maximum: 100, description: 'scroll: wheel clicks.' },
116
+ duration: { type: 'number', minimum: 0, maximum: 300, description: 'Seconds: wait, hold_key.' },
117
+ repeat: { type: 'integer', minimum: 1, maximum: 100, description: 'key: presses (default 1).' },
118
+ };
119
+ /** The screenshot after input actions (0.15.0+). */
120
+ const COMPUTER_LOOK = {
121
+ screenshot: { type: 'boolean', description: 'Take a screenshot after the input actions (default true); false when you do not need to look.' },
122
+ settle_ms: { type: 'integer', minimum: 0, maximum: 10_000, description: 'Wait this long before that screenshot when the screen may change (default 250).' },
123
+ format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format (default png; jpeg is smaller).' },
124
+ quality: { type: 'integer', minimum: 1, maximum: 100, description: 'JPEG quality (default 80).' },
125
+ };
126
+ const COMPUTER_ACTION_KEYS = ['coordinate', 'start_coordinate', 'region', 'text', 'scroll_direction', 'scroll_amount', 'duration', 'repeat'];
127
+ function computerAction(a) {
128
+ const action = { action: a.action };
129
+ for (const k of COMPUTER_ACTION_KEYS)
130
+ if (a[k] !== undefined && a[k] !== null)
131
+ action[k] = a[k];
132
+ return action;
133
+ }
134
+ function computerImage(a) {
135
+ return {
136
+ ...(typeof a.settle_ms === 'number' ? { settle_ms: a.settle_ms } : {}),
137
+ ...(a.format === 'png' || a.format === 'jpeg' ? { format: a.format } : {}),
138
+ ...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
139
+ };
140
+ }
141
+ const imageView = (img) => ({ mime_type: img.format === 'jpeg' ? 'image/jpeg' : 'image/png', width: img.width, height: img.height, data_base64: img.data });
100
142
  const path = (description = 'Absolute path inside the workspace, e.g. /home/user/project/main.py') => ({ type: 'string', minLength: 1, maxLength: 4096, description });
101
143
  const signal = { type: 'string', description: 'Signal name such as SIGTERM, SIGINT or SIGKILL.', minLength: 2, maxLength: 12 };
102
144
  function clip(text, max) {
@@ -245,7 +287,7 @@ async function waitForExit(c, s, waitMs, signal) {
245
287
  }
246
288
  /** Builds the tool list for a workspace. Synchronous: tokens are fetched on first use. */
247
289
  export function workspaceTools(workspace, opts = {}) {
248
- const cell = () => workspace.cell({
290
+ const cell = async () => workspace.cell({
249
291
  ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}),
250
292
  ...(opts.wake !== undefined ? { wake: opts.wake } : {}),
251
293
  ...(opts.transitionTimeoutMs !== undefined ? { transitionTimeoutMs: opts.transitionTimeoutMs } : {}),
@@ -297,7 +339,7 @@ export function workspaceTools(workspace, opts = {}) {
297
339
  run: async (a, o) => {
298
340
  const executionId = newExecutionId();
299
341
  opts.onExecution?.(executionId);
300
- const r = await cell().executions.run(['bash', '-lc', String(a.command)], {
342
+ const r = await (await cell()).executions.run(['bash', '-lc', String(a.command)], {
301
343
  executionId,
302
344
  ...(typeof a.cwd === 'string' ? { cwd: a.cwd } : opts.defaultCwd ? { cwd: opts.defaultCwd } : {}),
303
345
  timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
@@ -343,7 +385,7 @@ export function workspaceTools(workspace, opts = {}) {
343
385
  if (a.background === true) {
344
386
  // A session of its own: the start answers at once, and exec_read / exec_cancel find it by session_id.
345
387
  // The command runs until it exits or its timeout_ms, if one is set. (Never a burst: refused above.)
346
- const s = await cell().exec.start({
388
+ const s = await (await cell()).exec.start({
347
389
  argv: ['bash', '-lc', String(a.command)],
348
390
  ...(cwd !== undefined ? { cwd } : {}),
349
391
  ...(typeof a.timeout_ms === 'number' ? { timeout_ms: a.timeout_ms } : {}),
@@ -359,7 +401,7 @@ export function workspaceTools(workspace, opts = {}) {
359
401
  }
360
402
  let r;
361
403
  try {
362
- r = await cell().exec.run(['bash', '-lc', String(a.command)], {
404
+ r = await (await cell()).exec.run(['bash', '-lc', String(a.command)], {
363
405
  ...(cwd !== undefined ? { cwd } : {}),
364
406
  timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
365
407
  ...(typeof a.stdin === 'string' ? { stdin: a.stdin } : {}),
@@ -411,7 +453,7 @@ export function workspaceTools(workspace, opts = {}) {
411
453
  description: 'Read a command started with exec background: true: its state, exit code and output. Without offsets it returns the end of each stream; pass next_stdout_offset and next_stderr_offset back to read only new output. wait_ms waits up to that long for the command to exit and returns as soon as it does.',
412
454
  parameters: obj({ session_id: sessionId, stdout_offset: { type: 'integer', minimum: 0 }, stderr_offset: { type: 'integer', minimum: 0 }, wait_ms: { type: 'integer', minimum: 0, maximum: 60_000 } }, ['session_id']),
413
455
  run: async (a, o) => {
414
- const c = cell();
456
+ const c = await cell();
415
457
  const id = String(a.session_id);
416
458
  let s = await c.exec.get(id);
417
459
  if (typeof a.wait_ms === 'number' && a.wait_ms > 0 && running(s))
@@ -461,7 +503,7 @@ export function workspaceTools(workspace, opts = {}) {
461
503
  // The workspace waits 5 s without a grace and at most 60 s.
462
504
  parameters: obj({ session_id: sessionId, grace_ms: { type: 'integer', minimum: 1, maximum: 60_000 } }, ['session_id']),
463
505
  run: async (a) => {
464
- const s = await cell().exec.cancel(String(a.session_id), typeof a.grace_ms === 'number' ? a.grace_ms : undefined);
506
+ const s = await (await cell()).exec.cancel(String(a.session_id), typeof a.grace_ms === 'number' ? a.grace_ms : undefined);
465
507
  return { session_id: s.session_id, state: s.state, exit_code: running(s) ? null : (s.exit_code ?? null), canceled: s.canceled ?? false };
466
508
  },
467
509
  },
@@ -472,7 +514,7 @@ export function workspaceTools(workspace, opts = {}) {
472
514
  description: 'Read a text file from the workspace (UTF-8). Use offset/length for large files.',
473
515
  parameters: obj({ path: path(), offset: { type: 'integer', minimum: 0 }, length: { type: 'integer', minimum: 1, maximum: 10_485_760 } }, ['path']),
474
516
  run: async (a) => {
475
- const bytes = await cell().files.read(String(a.path), {
517
+ const bytes = await (await cell()).files.read(String(a.path), {
476
518
  ...(typeof a.offset === 'number' ? { offset: a.offset } : {}),
477
519
  length: typeof a.length === 'number' ? Math.min(a.length, max) : max + 1,
478
520
  });
@@ -491,7 +533,7 @@ export function workspaceTools(workspace, opts = {}) {
491
533
  create_parents: { type: 'boolean', description: 'Create missing parent directories (default true).' },
492
534
  }, ['path', 'content']),
493
535
  run: async (a) => {
494
- const r = await cell().files.write(String(a.path), String(a.content), { append: a.append === true, createParents: a.create_parents !== false });
536
+ const r = await (await cell()).files.write(String(a.path), String(a.content), { append: a.append === true, createParents: a.create_parents !== false });
495
537
  return { path: r.path, bytes_written: r.bytes_written, sha256: r.sha256, durable: r.durable };
496
538
  },
497
539
  },
@@ -501,7 +543,7 @@ export function workspaceTools(workspace, opts = {}) {
501
543
  description: 'List a directory in the workspace.',
502
544
  parameters: obj({ path: path('Absolute directory path.'), limit: { type: 'integer', minimum: 1, maximum: 10_000 } }, ['path']),
503
545
  run: async (a) => {
504
- const r = await cell().files.list(String(a.path), typeof a.limit === 'number' ? { limit: a.limit } : { limit: 500 });
546
+ const r = await (await cell()).files.list(String(a.path), typeof a.limit === 'number' ? { limit: a.limit } : { limit: 500 });
505
547
  return { entries: r.entries.map((e) => ({ name: e.name, path: e.path, type: e.type, size: e.size, modified_at: e.modified_at })), truncated: r.truncated };
506
548
  },
507
549
  },
@@ -520,7 +562,7 @@ export function workspaceTools(workspace, opts = {}) {
520
562
  context_lines: { type: 'integer', minimum: 0, maximum: 5, description: 'Lines of context to return before and after each match.' },
521
563
  }, ['path', 'pattern']),
522
564
  run: async (a, o) => {
523
- const r = await cell().files.search(String(a.path), String(a.pattern), {
565
+ const r = await (await cell()).files.search(String(a.path), String(a.pattern), {
524
566
  ...(typeof a.regex === 'boolean' ? { regex: a.regex } : {}),
525
567
  ...(typeof a.case_insensitive === 'boolean' ? { caseInsensitive: a.case_insensitive } : {}),
526
568
  ...(Array.isArray(a.include) ? { include: a.include } : {}),
@@ -566,7 +608,7 @@ export function workspaceTools(workspace, opts = {}) {
566
608
  },
567
609
  }, ['path', 'edits']),
568
610
  run: async (a, o) => {
569
- const c = cell();
611
+ const c = await cell();
570
612
  const file = String(a.path);
571
613
  // Without a revision from the model, pin the edit to the content current now, so a concurrent change between
572
614
  // this read and the patch is refused (revision_mismatch) instead of edited blindly.
@@ -582,7 +624,7 @@ export function workspaceTools(workspace, opts = {}) {
582
624
  description: 'List processes running in the workspace.',
583
625
  parameters: obj({}),
584
626
  run: async () => {
585
- const r = await cell().processes.list();
627
+ const r = await (await cell()).processes.list();
586
628
  return { processes: r.data.map((p) => ({ pid: p.pid, ppid: p.ppid, comm: p.comm, cmdline: p.cmdline, state: p.state, rss_bytes: p.rss_bytes })) };
587
629
  },
588
630
  },
@@ -592,7 +634,7 @@ export function workspaceTools(workspace, opts = {}) {
592
634
  description: 'Send a signal to a process in the workspace (e.g. stop a server).',
593
635
  parameters: obj({ pid: { type: 'integer', minimum: 2 }, signal }, ['pid', 'signal']),
594
636
  run: async (a) => {
595
- await cell().processes.signal(Number(a.pid), String(a.signal));
637
+ await (await cell()).processes.signal(Number(a.pid), String(a.signal));
596
638
  return { signalled: true };
597
639
  },
598
640
  },
@@ -602,7 +644,7 @@ export function workspaceTools(workspace, opts = {}) {
602
644
  description: 'Open an interactive terminal (PTY) in the workspace; returns a session_id for terminal_send/terminal_read.',
603
645
  parameters: obj({ command: { type: 'string', maxLength: 10_000, description: 'Program to run (default: login shell).' }, rows: { type: 'integer', minimum: 1, maximum: 1000 }, cols: { type: 'integer', minimum: 1, maximum: 1000 } }),
604
646
  run: async (a) => {
605
- const s = await cell().pty.open({
647
+ const s = await (await cell()).pty.open({
606
648
  ...(typeof a.command === 'string' ? { argv: ['bash', '-lc', a.command] } : {}),
607
649
  ...(typeof a.rows === 'number' ? { rows: a.rows } : {}),
608
650
  ...(typeof a.cols === 'number' ? { cols: a.cols } : {}),
@@ -616,7 +658,7 @@ export function workspaceTools(workspace, opts = {}) {
616
658
  description: 'Type input into a terminal session (include "\\n" to press Enter).',
617
659
  parameters: obj({ session_id: { type: 'string', minLength: 1, maxLength: 64 }, input: { type: 'string', maxLength: 1_000_000 } }, ['session_id', 'input']),
618
660
  run: async (a) => {
619
- const s = await cell().pty.input(String(a.session_id), String(a.input));
661
+ const s = await (await cell()).pty.input(String(a.session_id), String(a.input));
620
662
  return { state: s.state, next_offset: s.output_size };
621
663
  },
622
664
  },
@@ -626,7 +668,7 @@ export function workspaceTools(workspace, opts = {}) {
626
668
  description: 'Read terminal output from an offset (returns next_offset to continue). Waits briefly for new output.',
627
669
  parameters: obj({ session_id: { type: 'string', minLength: 1, maxLength: 64 }, offset: { type: 'integer', minimum: 0 }, wait_ms: { type: 'integer', minimum: 0, maximum: 60_000 } }, ['session_id']),
628
670
  run: async (a) => {
629
- const r = await cell().pty.read(String(a.session_id), {
671
+ const r = await (await cell()).pty.read(String(a.session_id), {
630
672
  offset: typeof a.offset === 'number' ? a.offset : 0,
631
673
  timeoutMs: typeof a.wait_ms === 'number' ? Math.max(100, a.wait_ms) : 3_000,
632
674
  maxBytes: max,
@@ -640,7 +682,7 @@ export function workspaceTools(workspace, opts = {}) {
640
682
  description: 'Close a terminal session.',
641
683
  parameters: obj({ session_id: { type: 'string', minLength: 1, maxLength: 64 } }, ['session_id']),
642
684
  run: async (a) => {
643
- const s = await cell().pty.close(String(a.session_id));
685
+ const s = await (await cell()).pty.close(String(a.session_id));
644
686
  return { state: s.state };
645
687
  },
646
688
  },
@@ -650,7 +692,7 @@ export function workspaceTools(workspace, opts = {}) {
650
692
  description: 'Clone a git repository (HTTPS) into the workspace.',
651
693
  parameters: obj({ url: { type: 'string', minLength: 9, maxLength: 2048, description: 'https:// remote URL.' }, path: path('Destination directory.'), branch: { type: 'string', maxLength: 255 }, depth: { type: 'integer', minimum: 1 } }, ['url', 'path']),
652
694
  run: async (a) => {
653
- const r = await cell().git.clone({
695
+ const r = await (await cell()).git.clone({
654
696
  url: String(a.url),
655
697
  path: String(a.path),
656
698
  ...(typeof a.branch === 'string' ? { branch: a.branch } : {}),
@@ -664,7 +706,7 @@ export function workspaceTools(workspace, opts = {}) {
664
706
  permission: 'git',
665
707
  description: 'Show the git status of a repository in the workspace.',
666
708
  parameters: obj({ path: path('Repository directory.') }, ['path']),
667
- run: async (a) => cell().git.status(String(a.path)),
709
+ run: async (a) => (await cell()).git.status(String(a.path)),
668
710
  },
669
711
  {
670
712
  name: 'git_commit',
@@ -672,17 +714,66 @@ export function workspaceTools(workspace, opts = {}) {
672
714
  description: 'Stage all changes and commit in a repository in the workspace.',
673
715
  parameters: obj({ path: path('Repository directory.'), message: { type: 'string', minLength: 1, maxLength: 65_536 } }, ['path', 'message']),
674
716
  run: async (a) => {
675
- const r = await cell().git.commit({ path: String(a.path), message: String(a.message), all: true });
717
+ const r = await (await cell()).git.commit({ path: String(a.path), message: String(a.message), all: true });
676
718
  return { exit_code: r.exit_code, commit: r.commit ?? null, stdout: clip(r.stdout, max).text, stderr: clip(r.stderr, max).text };
677
719
  },
678
720
  },
721
+ {
722
+ name: 'computer',
723
+ permission: 'computer',
724
+ description: 'Use the workspace desktop: its screen, mouse and keyboard. One action per call (computer_batch runs several); input actions answer with a screenshot taken after them, screenshot and zoom answer with the image, cursor_position with the pointer. Coordinates are screen pixels from the top-left of the latest screenshot. Start with action "screenshot". Programs started with exec in the background (e.g. "chromium https://example.com &") open on this desktop.',
725
+ parameters: obj({ ...COMPUTER_ACTION, ...COMPUTER_LOOK }, ['action']),
726
+ run: async (a, o) => {
727
+ const look = a.action === 'screenshot' || a.action === 'zoom' || a.action === 'cursor_position';
728
+ const r = await runComputerActions(async (actions, options) => (await cell()).computer.act({ actions, screenshot: options.screenshot, ...computerImage(a) }, o.signal), [computerAction(a)], { screenshot: !look && a.screenshot !== false }, opts.computer?.beforeAction);
729
+ const res = r.results[0];
730
+ const img = res?.image ?? r.screenshot;
731
+ return {
732
+ ok: res?.ok ?? false,
733
+ ...(res?.output !== undefined ? { output: res.output } : {}),
734
+ ...(res?.error ? { error: res.error.message } : {}),
735
+ cursor: r.cursor,
736
+ display: r.display,
737
+ ...(img ? { screenshot: imageView(img) } : {}),
738
+ };
739
+ },
740
+ },
741
+ {
742
+ name: 'computer_batch',
743
+ permission: 'computer',
744
+ description: 'Run several actions of the computer tool on the workspace desktop in one call, in order (e.g. click a field, type, press Return). The first failure stops the batch and the rest come back skipped. Answers with each action\'s result (zoom with its image, cursor_position with the pointer) and one screenshot after the last action that ran (screenshot: false skips it).',
745
+ parameters: obj({
746
+ actions: { type: 'array', minItems: 1, maxItems: 50, items: obj(COMPUTER_ACTION, ['action']), description: 'Actions with the computer tool\'s parameters, run in order.' },
747
+ ...COMPUTER_LOOK,
748
+ }, ['actions']),
749
+ run: async (a, o) => {
750
+ const actions = a.actions.map(computerAction);
751
+ const lastLooks = ['screenshot', 'zoom'].includes(actions.at(-1)?.action ?? '');
752
+ const r = await runComputerActions(async (batch, options) => (await cell()).computer.act({ actions: batch, screenshot: options.screenshot, ...computerImage(a) }, o.signal), actions, { screenshot: !lastLooks && a.screenshot !== false }, opts.computer?.beforeAction);
753
+ return {
754
+ ok: r.results.every((x) => x.ok),
755
+ results: r.results.map((x) => ({
756
+ action: x.action,
757
+ ok: x.ok,
758
+ ...(x.skipped ? { skipped: true } : {}),
759
+ ...(x.output !== undefined ? { output: x.output } : {}),
760
+ ...(x.error ? { error: x.error.message } : {}),
761
+ took_ms: x.took_ms,
762
+ ...(x.image ? { image: imageView(x.image) } : {}),
763
+ })),
764
+ cursor: r.cursor,
765
+ display: r.display,
766
+ ...(r.screenshot ? { screenshot: imageView(r.screenshot) } : {}),
767
+ };
768
+ },
769
+ },
679
770
  {
680
771
  name: 'browser_screenshot',
681
772
  permission: 'browser',
682
773
  description: 'Open a URL in the workspace’s headless browser and return a PNG screenshot (base64).',
683
774
  parameters: obj({ url: { type: 'string', minLength: 8, maxLength: 8192 }, width: { type: 'integer', minimum: 100, maximum: 3840 }, height: { type: 'integer', minimum: 100, maximum: 2160 } }, ['url']),
684
775
  run: async (a) => {
685
- const png = await cell().browser.screenshot({
776
+ const png = await (await cell()).browser.screenshot({
686
777
  url: String(a.url),
687
778
  ...(typeof a.width === 'number' ? { width: a.width } : {}),
688
779
  ...(typeof a.height === 'number' ? { height: a.height } : {}),
@@ -696,7 +787,7 @@ export function workspaceTools(workspace, opts = {}) {
696
787
  description: 'Open a URL in the workspace’s headless browser and return the rendered page as text or HTML.',
697
788
  parameters: obj({ url: { type: 'string', minLength: 8, maxLength: 8192 }, format: { type: 'string', enum: ['text', 'html'] } }, ['url']),
698
789
  run: async (a) => {
699
- const r = await cell().browser.content({ url: String(a.url), format: a.format === 'html' ? 'html' : 'text' });
790
+ const r = await (await cell()).browser.content({ url: String(a.url), format: a.format === 'html' ? 'html' : 'text' });
700
791
  const c = clip(r.content, max);
701
792
  return { url: r.url, format: r.format, content: c.text, truncated: r.truncated || c.truncated };
702
793
  },
@@ -0,0 +1,10 @@
1
+ export declare const TUNNEL_ASSETS: {
2
+ readonly x86_64: {
3
+ readonly file: "linux-amd64.gz";
4
+ readonly sha256: "e7b8a88b27c36b2605ee9e8182dae7700f816d9058d92bd87d6ae66781303650";
5
+ };
6
+ readonly aarch64: {
7
+ readonly file: "linux-arm64.gz";
8
+ readonly sha256: "740ce49febb1ab0e614a520d8daff68fa7b4ac32adf18dc93d5ff16b2dc57735";
9
+ };
10
+ };
@@ -0,0 +1,11 @@
1
+ // Generated by scripts/build-tunnel-helper.py.
2
+ export const TUNNEL_ASSETS = {
3
+ "x86_64": {
4
+ "file": "linux-amd64.gz",
5
+ "sha256": "e7b8a88b27c36b2605ee9e8182dae7700f816d9058d92bd87d6ae66781303650"
6
+ },
7
+ "aarch64": {
8
+ "file": "linux-arm64.gz",
9
+ "sha256": "740ce49febb1ab0e614a520d8daff68fa7b4ac32adf18dc93d5ff16b2dc57735"
10
+ }
11
+ };
@@ -0,0 +1,3 @@
1
+ export declare function packetCrc(bytes: Uint8Array): number;
2
+ /** Delimit both ends so a replay discards any truncated preceding packet before decoding. */
3
+ export declare function tunnelPacket(offset: number, bytes: Uint8Array): Buffer;
@@ -0,0 +1,43 @@
1
+ const table = Uint32Array.from({ length: 256 }, (_, value) => {
2
+ let crc = value;
3
+ for (let n = 0; n < 8; n++)
4
+ crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
5
+ return crc >>> 0;
6
+ });
7
+ export function packetCrc(bytes) {
8
+ let crc = 0xffffffff;
9
+ for (const b of bytes)
10
+ crc = (crc >>> 8) ^ table[(crc ^ b) & 255];
11
+ return (crc ^ 0xffffffff) >>> 0;
12
+ }
13
+ /** Delimit both ends so a replay discards any truncated preceding packet before decoding. */
14
+ export function tunnelPacket(offset, bytes) {
15
+ if (!Number.isSafeInteger(offset) || offset < 0 || bytes.length > 65536)
16
+ throw new Error('invalid tunnel packet');
17
+ const data = Buffer.alloc(bytes.length + 16);
18
+ data.write('SFRT');
19
+ data.writeBigUInt64BE(BigInt(offset), 4);
20
+ data.set(bytes, 12);
21
+ data.writeUInt32BE(packetCrc(data.subarray(0, -4)), data.length - 4);
22
+ const result = Buffer.alloc(data.length + Math.ceil(data.length / 254) + 3);
23
+ let write = 2, codeAt = 1, code = 1;
24
+ for (const b of data) {
25
+ if (b === 0) {
26
+ result[codeAt] = code;
27
+ codeAt = write++;
28
+ code = 1;
29
+ }
30
+ else {
31
+ result[write++] = b;
32
+ code++;
33
+ if (code === 255) {
34
+ result[codeAt] = code;
35
+ codeAt = write++;
36
+ code = 1;
37
+ }
38
+ }
39
+ }
40
+ result[codeAt] = code;
41
+ result[write++] = 0;
42
+ return result.subarray(0, write);
43
+ }
@@ -0,0 +1,86 @@
1
+ import { WebSocket } from 'undici';
2
+ import type { CellClient, ExecSession, OutputEvent } from './cell.js';
3
+ import type { TunnelTransport } from './tunnels.js';
4
+ /** Native PTY owns the channel; helper acknowledgements own bytes, independently of an HTTP request outcome. */
5
+ export declare class PtyTunnelTransport implements TunnelTransport {
6
+ #private;
7
+ readonly maxInputConcurrency = 64;
8
+ readonly kind: "pty";
9
+ constructor(cell: CellClient, open?: (url: string, opts: NonNullable<ConstructorParameters<typeof WebSocket>[1]>) => WebSocket);
10
+ readonly files: {
11
+ remove: (path: string, opts?: {
12
+ recursive?: boolean;
13
+ }) => Promise<void>;
14
+ };
15
+ readonly exec: {
16
+ start: (req: {
17
+ session_id?: import("./index.js").CellComponents["schemas"]["SessionIdValue"];
18
+ argv: string[];
19
+ env?: {
20
+ [key: string]: string;
21
+ };
22
+ cwd?: string;
23
+ user?: string;
24
+ stdin_open?: boolean;
25
+ stdin?: string;
26
+ timeout_ms?: number;
27
+ kill_grace_ms?: number;
28
+ secret_refs?: import("./index.js").CellComponents["schemas"]["SecretRefs"];
29
+ execution_id?: import("./index.js").CellComponents["schemas"]["ExecutionIdValue"];
30
+ output_limit_bytes?: number;
31
+ burst?: "never" | "always";
32
+ burst_vcpus?: number;
33
+ burst_memory_mib?: number;
34
+ resource_hint?: "auto" | "light" | "heavy";
35
+ }, signal?: AbortSignal | undefined) => Promise<{
36
+ session_id: string;
37
+ state: import("./index.js").CellComponents["schemas"]["SessionState"];
38
+ created: boolean;
39
+ pid?: number;
40
+ exit_code?: number | null;
41
+ term_signal?: number | null;
42
+ timed_out?: boolean;
43
+ canceled?: boolean;
44
+ stdout_size: number;
45
+ stderr_size: number;
46
+ created_at?: string;
47
+ started_at?: string | null;
48
+ ended_at?: string | null;
49
+ error?: string;
50
+ memory_grow?: import("./index.js").CellComponents["schemas"]["MemoryGrow"];
51
+ burst?: import("./index.js").CellComponents["schemas"]["BurstSummary"];
52
+ }>;
53
+ input: (id: string, bytes: string | Uint8Array, opts: {
54
+ offset: number;
55
+ close?: boolean;
56
+ signal?: AbortSignal;
57
+ }) => Promise<{
58
+ offset: number;
59
+ closed: false;
60
+ }>;
61
+ output: (id: string, opts?: {
62
+ stdoutOffset?: number;
63
+ stderrOffset?: number;
64
+ follow?: boolean;
65
+ signal?: AbortSignal;
66
+ }) => Promise<AsyncGenerator<{
67
+ type: "output" | "exit" | "heartbeat" | "error";
68
+ stream?: "stdout" | "stderr";
69
+ offset?: number;
70
+ data?: string;
71
+ session?: import("./index.js").CellComponents["schemas"]["ExecSession"];
72
+ error?: import("./index.js").CellComponents["schemas"]["ErrorBody"];
73
+ }, any, any>>;
74
+ cancel: (id: string) => Promise<ExecSession>;
75
+ };
76
+ input(id: string, value: string | Uint8Array, opts: {
77
+ offset: number;
78
+ signal?: AbortSignal;
79
+ }): Promise<{
80
+ offset: number;
81
+ closed: false;
82
+ }>;
83
+ output(id: string, opts?: {
84
+ signal?: AbortSignal;
85
+ }): Promise<AsyncGenerator<OutputEvent>>;
86
+ }