nexus-ai-pro 2.10.0 → 2.11.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 (69) hide show
  1. package/API_STABILITY.md +30 -0
  2. package/CHANGELOG.md +72 -1
  3. package/README.md +9 -5
  4. package/dist/agent/files.d.ts +36 -0
  5. package/dist/agent/files.js +112 -0
  6. package/dist/agent/middleware/filesystem.d.ts +3 -3
  7. package/dist/agent/sandbox-container.js +6 -4
  8. package/dist/agent/sandbox.d.ts +17 -58
  9. package/dist/agent/sandbox.js +19 -229
  10. package/dist/agent/workspace.d.ts +220 -0
  11. package/dist/agent/workspace.js +673 -0
  12. package/dist/core/nexus.d.ts +7 -0
  13. package/dist/core/nexus.js +3 -0
  14. package/dist/deep-agent/index.d.ts +15 -6
  15. package/dist/deep-agent/index.js +20 -18
  16. package/dist/realtime/session.d.ts +6 -0
  17. package/dist/realtime/session.js +34 -3
  18. package/dist/telephony/index.d.ts +4 -2
  19. package/dist/telephony/index.js +2 -1
  20. package/dist/telephony/manager.d.ts +31 -1
  21. package/dist/telephony/manager.js +84 -0
  22. package/dist/telephony/providers/telnyx.d.ts +108 -0
  23. package/dist/telephony/providers/telnyx.js +646 -0
  24. package/dist/telephony/providers/twilio.d.ts +24 -3
  25. package/dist/telephony/providers/twilio.js +174 -0
  26. package/dist/telephony/realtime-bridge.d.ts +5 -0
  27. package/dist/telephony/realtime-bridge.js +1 -0
  28. package/dist/telephony/recordings.d.ts +91 -0
  29. package/dist/telephony/recordings.js +86 -0
  30. package/dist/telephony/twiml.d.ts +2 -1
  31. package/dist/telephony/twiml.js +20 -0
  32. package/dist/types/lifecycle.d.ts +1 -1
  33. package/dist/types/telephony.d.ts +247 -1
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/dist-cjs/agent/files.d.ts +36 -0
  37. package/dist-cjs/agent/files.js +148 -0
  38. package/dist-cjs/agent/middleware/filesystem.d.ts +3 -3
  39. package/dist-cjs/agent/sandbox-container.js +6 -4
  40. package/dist-cjs/agent/sandbox.d.ts +17 -58
  41. package/dist-cjs/agent/sandbox.js +19 -229
  42. package/dist-cjs/agent/workspace.d.ts +220 -0
  43. package/dist-cjs/agent/workspace.js +686 -0
  44. package/dist-cjs/core/nexus.d.ts +7 -0
  45. package/dist-cjs/core/nexus.js +3 -0
  46. package/dist-cjs/deep-agent/index.d.ts +15 -6
  47. package/dist-cjs/deep-agent/index.js +20 -18
  48. package/dist-cjs/realtime/session.d.ts +6 -0
  49. package/dist-cjs/realtime/session.js +34 -3
  50. package/dist-cjs/telephony/index.d.ts +4 -2
  51. package/dist-cjs/telephony/index.js +4 -1
  52. package/dist-cjs/telephony/manager.d.ts +31 -1
  53. package/dist-cjs/telephony/manager.js +84 -0
  54. package/dist-cjs/telephony/providers/telnyx.d.ts +108 -0
  55. package/dist-cjs/telephony/providers/telnyx.js +650 -0
  56. package/dist-cjs/telephony/providers/twilio.d.ts +24 -3
  57. package/dist-cjs/telephony/providers/twilio.js +174 -0
  58. package/dist-cjs/telephony/realtime-bridge.d.ts +5 -0
  59. package/dist-cjs/telephony/realtime-bridge.js +2 -0
  60. package/dist-cjs/telephony/recordings.d.ts +91 -0
  61. package/dist-cjs/telephony/recordings.js +89 -0
  62. package/dist-cjs/telephony/twiml.d.ts +2 -1
  63. package/dist-cjs/telephony/twiml.js +20 -0
  64. package/dist-cjs/types/lifecycle.d.ts +1 -1
  65. package/dist-cjs/types/telephony.d.ts +247 -1
  66. package/dist-cjs/version.d.ts +1 -1
  67. package/dist-cjs/version.js +1 -1
  68. package/llms.txt +1 -1
  69. package/package.json +27 -1
package/API_STABILITY.md CHANGED
@@ -41,6 +41,36 @@ Both are covered by this policy:
41
41
  Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
42
42
  one of its explicit subpaths.
43
43
 
44
+ ## 2.11 stage (2.11.0)
45
+
46
+ Everything 2.11 adds is stable from the start and additive, unless this section says otherwise.
47
+
48
+ New and stable:
49
+ - `nexus-ai-pro/agent/workspace`: `Workspace`, `WorkspaceGlobOptions`, `WorkspaceGrepOptions`,
50
+ `WorkspaceMatch`, `WorkspacePathError`, `WorkspaceReadOnlyError`, `resolveWorkspacePath()`,
51
+ `memoryWorkspace()`, `fileWorkspace()`, `storeWorkspace()`, `readOnlyWorkspace()`,
52
+ `compositeWorkspace()` and their options, `WorkspaceMount`, `workspaceTools()` and
53
+ `WorkspaceToolsOptions`, and `runWorkspaceConformance()` with its report and check;
54
+ - `DeepAgentOptions.workspace`;
55
+ - `nexus-ai-pro/telephony/telnyx`, with `TelnyxTelephonyProvider` and its config;
56
+ - on `nexus-ai-pro/telephony`: `TelephonyManager.answer()`, `transferCall()`, `joinConference()`,
57
+ `startRecording()`, `stopRecording()`, `parseRecordingCallback()`, `downloadRecording()`,
58
+ `deleteRecording()`, `sendMessage()`, and `parseMessageWebhook()`, the matching optional
59
+ `TelephonyProvider` methods, their request and result types, `TelephonyDialConfig`,
60
+ `TelephonyConferenceConfig`, and `TelephonyResponseRequest.dial`; `recordingKeeper()` with
61
+ `RecordingKeeper`, `RecordingKeeperOptions`, `RecordingRetention`, and `KeptRecording`;
62
+ `telnyxRealtimeAudioOptions()`; the `TelephonyProviderInfo.supports` flags `transfer`,
63
+ `conference`, `recording`, and `messaging`; `TelephonyWebhookValidationRequest.publicKey`;
64
+ - `NexusAI.telephony`; the operation family `messaging`;
65
+ - `Sandbox` extends `Workspace`, so it gains `writeBytes()`, optional; `SandboxGlobOptions`,
66
+ `SandboxGrepOptions`, and `SandboxMatch` are the workspace's types under their old names, and
67
+ `SandboxPathError` is a `WorkspacePathError`.
68
+
69
+ Changed, each to keep a promise the surface already made:
70
+ - A sandbox's file methods take a leading `/` as the sandbox's root, as the workspace contract
71
+ says; a path that leaves the root is still refused. The container sandbox rejects reading a
72
+ missing file with `ENOENT`.
73
+
44
74
  ## 2.10 stage (2.10.0)
45
75
 
46
76
  Everything 2.10 adds is stable from the start and additive, unless this section says otherwise.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,76 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.11.0] - 2026-10-18
8
+
9
+ Workspaces, and telephony in full.
10
+
11
+ ### Added
12
+
13
+ - **Workspaces: where an agent's files live, apart from where its code runs.** A `Workspace`, on
14
+ the new `nexus-ai-pro/agent/workspace`, reads, writes, and lists files, and optionally searches
15
+ them, removes them, and holds bytes, with paths that mean the same in every one. Five ship:
16
+ `memoryWorkspace()`, `fileWorkspace()`, `storeWorkspace()` on any long-term `Store` (each file its
17
+ contents and a small tree entry, so listing reads no contents), `readOnlyWorkspace()`, and
18
+ `compositeWorkspace()`, which mounts several at paths of its own, read-only or not, and searches
19
+ across them. `workspaceTools()` gives the file tools over any of them, and
20
+ `runWorkspaceConformance()` holds any workspace to the contract. A `Sandbox` is now a workspace
21
+ that can also run commands. A deep agent takes `workspace` apart from `sandbox`: its files, skills,
22
+ instruction files, and offloaded results live in the workspace, and its commands run in the sandbox.
23
+ - `Workspace.writeBytes()`, optional, which the process sandbox now has.
24
+ - **Telephony in full, on a second provider.** `TelnyxTelephonyProvider`, on the new
25
+ `nexus-ai-pro/telephony/telnyx`, on Telnyx's own call-control protocol: calls placed and answered
26
+ by command, Ed25519-signed webhooks verified with Web Crypto, media streams that read as Twilio's
27
+ do, phone numbers, and everything below. `TelephonyManager.answer()` answers a call webhook the same
28
+ way on either provider: TwiML on Twilio, commands on Telnyx.
29
+ - **Transfer and conferences** on both: `transferCall()` and `joinConference()`, and a `dial` in a
30
+ webhook answer, rendered as `<Dial>` on Twilio.
31
+ - **Call recordings kept on your terms.** `startRecording()`, `stopRecording()`,
32
+ `parseRecordingCallback()`, `downloadRecording()`, and `deleteRecording()` on both, and
33
+ `recordingKeeper()`, which keeps each finished recording in an `AssetStore` to expire under a
34
+ `RecordingRetention`, deletes the provider's copy, keeps a retried callback's recording once, and
35
+ sweeps what expired.
36
+ - **Messages, as their own operation family.** `sendMessage()` (SMS and MMS) and
37
+ `parseMessageWebhook()` on both, running through the client's lifecycle as `messaging` operations.
38
+ - `NexusAI.telephony`, the client's telephony manager; `telnyxRealtimeAudioOptions()`;
39
+ `TelephonyWebhookValidationRequest.publicKey`.
40
+ - The weekly live workflow runs a Gemini Live session that calls a tool, OpenAI's SIP call control,
41
+ and the phone numbers of Twilio and Telnyx, where the repository's secrets are set.
42
+
43
+ ### Fixed
44
+
45
+ - A realtime session's `whenIdle()` resolved while a tool it ran on its own was still running, so
46
+ the tool's result could be missing from the record, and from the model, after it. It now waits for
47
+ running tools and their results, though not for a call waiting on a person's confirmation.
48
+
49
+ ### Changed
50
+
51
+ - A sandbox's file methods take a leading `/` as its root, as every workspace does: the process
52
+ sandbox and the container sandbox refused such a path unless it was already under their root.
53
+ `/etc/passwd` is now the workspace's own file, never the container's or the host's.
54
+ - The container sandbox rejects reading a missing file with `ENOENT`, as every workspace does, and
55
+ passes the workspace conformance suite.
56
+ - The file tools describe their files as the workspace's, not the sandbox's.
57
+
58
+ ### Proof
59
+
60
+ - `tests/workspaces.test.ts`: every workspace the package ships passes the workspace conformance
61
+ suite: in memory, on disk, in the memory, SQLite, Postgres, and Redis stores, the process sandbox,
62
+ and a composite. A composite's shadowing, read-only mounts, and search across mounts.
63
+ - `tests/phone-agent.test.ts`: one phone agent on Twilio and on Telnyx: answered the same way,
64
+ recorded, streamed through the bridge to a realtime session that hears the caller, answers aloud,
65
+ texts a receipt, and transfers the call; its recording kept in memory, on disk, and in S3, deleted
66
+ at the provider, kept once on a retried callback, there the day before its retention ends and gone
67
+ after the day after's sweep. Without the claim, the expiry, the provider delete, the answer by
68
+ command, the messaging family, or `whenIdle()` waiting for tools, it fails.
69
+ - `tests/telephony-providers.test.ts`: both providers against stand-ins for their APIs, and Telnyx's
70
+ webhooks verified with a real Ed25519 key pair: signed, altered, stale, and unsigned.
71
+ - `tests/deep-agent-workspaces.test.ts`: one deep agent does the same work over every workspace and
72
+ each ends holding the same files; and an agent whose code is in a sandbox, notes in a store, and
73
+ docs read-only finds each where it belongs. Without the composite's shadowing, its read-only
74
+ mounts, the store's directory filter, or the agent taking its files from the workspace, a test
75
+ fails.
76
+
7
77
  ## [2.10.0] - 2026-10-16
8
78
 
9
79
  Durable conversations, and a second realtime provider.
@@ -2694,7 +2764,8 @@ shared policy vocabulary is in place for a future release that revisits this.
2694
2764
  - URL fetching rejects unsafe private-network targets and limits response reads.
2695
2765
  - CLI and audit findings no longer disclose detected secret values.
2696
2766
 
2697
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.10.0...HEAD
2767
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.11.0...HEAD
2768
+ [2.11.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.10.0...v2.11.0
2698
2769
  [2.10.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.9.0...v2.10.0
2699
2770
  [2.9.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.8.0...v2.9.0
2700
2771
  [2.8.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.7.0...v2.8.0
package/README.md CHANGED
@@ -299,14 +299,18 @@ Shipped so far:
299
299
  sessions taken over by another worker when theirs dies, each proven killed at every write; realtime
300
300
  on the client's lifecycle; Gemini Live behind the same session, with video and images; phone calls
301
301
  answered over SIP; helpers on another server whose runs outlive their worker; and realtime over
302
- real WebRTC in a browser, with published budgets for turn latency and session load.
302
+ real WebRTC in a browser, with published budgets for turn latency and session load;
303
+ - 2.11 workspaces, and telephony in full: an agent's files apart from where its code runs, in memory,
304
+ on disk, in any long-term store, read-only, or a composite of several; and a second phone provider,
305
+ Telnyx, beside Twilio, both answering, transferring, conferencing, recording under a retention
306
+ policy, and sending and receiving messages.
303
307
 
304
308
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
305
309
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
306
- Next, 2.11 separates where an agent's files live from where its code runs, with workspaces, and
307
- completes telephony: a second provider, messaging, recording, conference, and transfer. Pinecone,
308
- images, the Gemini embedding batch tier, media understanding, and video graduate once the live
309
- workflow passes against each. The
310
+ Next, 2.12 makes observability lose nothing, with spans spooled when the collector is down and
311
+ deployment evaluation that reads the whole trace, and makes media something you can evaluate.
312
+ Pinecone, images, the Gemini embedding batch tier, media understanding, video, Gemini Live, SIP, and
313
+ both phone providers graduate once the live workflow passes against each. The
310
314
  [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
311
315
  applications ask for it.
312
316
 
@@ -0,0 +1,36 @@
1
+ /** The file methods of a directory on disk. */
2
+ export interface DirectoryFiles {
3
+ readFile(file: string): Promise<string>;
4
+ writeFile(file: string, content: string): Promise<void>;
5
+ listFiles(directory?: string): Promise<string[]>;
6
+ removeFile(file: string): Promise<void>;
7
+ glob(pattern: string, options?: {
8
+ cwd?: string;
9
+ limit?: number;
10
+ }): Promise<string[]>;
11
+ grep(pattern: string, options?: {
12
+ cwd?: string;
13
+ glob?: string;
14
+ ignoreCase?: boolean;
15
+ limit?: number;
16
+ }): Promise<Array<{
17
+ file: string;
18
+ line: number;
19
+ text: string;
20
+ }>>;
21
+ readBytes(file: string): Promise<Uint8Array>;
22
+ writeBytes(file: string, bytes: Uint8Array): Promise<void>;
23
+ }
24
+ /** The files under `root`, refusing a path outside it with the error `refuse` makes. */
25
+ export declare function directoryFiles(rootDirectory: string, refuse: (path: string) => Error): DirectoryFiles & {
26
+ ready: Promise<DirectoryHandle>;
27
+ };
28
+ /** What a directory's file methods hold once Node's modules have loaded. */
29
+ export interface DirectoryHandle {
30
+ fs: typeof import('node:fs/promises');
31
+ dirname: typeof import('node:path').dirname;
32
+ join: typeof import('node:path').join;
33
+ relative: typeof import('node:path').relative;
34
+ root: string;
35
+ inside: (file: string) => string;
36
+ }
@@ -0,0 +1,112 @@
1
+ import { GREP_FILE_BYTES, GREP_TOTAL_BYTES, globMatcher, matchingLines, SKIPPED_DIRECTORIES, WALK_LIMIT, } from './glob.js';
2
+ export function directoryFiles(rootDirectory, refuse) {
3
+ const ready = (async () => {
4
+ const [fs, { dirname, isAbsolute, join, relative, resolve }] = await Promise.all([
5
+ import('node:fs/promises'),
6
+ import('node:path'),
7
+ ]);
8
+ const root = resolve(rootDirectory);
9
+ await fs.mkdir(root, { recursive: true });
10
+ const outside = (fromRoot) => fromRoot.startsWith('..') || isAbsolute(fromRoot);
11
+ const inside = (file) => {
12
+ let resolved = resolve(root, file);
13
+ if (outside(relative(root, resolved)) && /^[\\/]/.test(file))
14
+ resolved = resolve(root, file.replace(/^[\\/]+/, ''));
15
+ if (outside(relative(root, resolved)))
16
+ throw refuse(file);
17
+ return resolved;
18
+ };
19
+ return { fs, dirname, join, relative, root, inside };
20
+ })();
21
+ ready.catch(() => undefined);
22
+ const walk = async (directory) => {
23
+ const { fs, join, relative, root, inside } = await ready;
24
+ const start = inside(directory);
25
+ const slash = (path) => path.replace(/\\/g, '/');
26
+ const files = [];
27
+ const queue = [start];
28
+ let walked = 0;
29
+ while (queue.length > 0 && walked < WALK_LIMIT) {
30
+ const current = queue.shift();
31
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
32
+ for (const entry of entries) {
33
+ walked += 1;
34
+ if (walked > WALK_LIMIT)
35
+ break;
36
+ const full = join(current, entry.name);
37
+ if (entry.isDirectory()) {
38
+ if (!SKIPPED_DIRECTORIES.has(entry.name))
39
+ queue.push(full);
40
+ }
41
+ else if (entry.isFile()) {
42
+ files.push({ full, fromRoot: slash(relative(root, full)), fromStart: slash(relative(start, full)) });
43
+ }
44
+ }
45
+ }
46
+ return files.sort((a, b) => (a.fromRoot < b.fromRoot ? -1 : a.fromRoot > b.fromRoot ? 1 : 0));
47
+ };
48
+ return {
49
+ ready,
50
+ async readFile(file) {
51
+ const { fs, inside } = await ready;
52
+ return fs.readFile(inside(file), 'utf8');
53
+ },
54
+ async writeFile(file, content) {
55
+ const { fs, dirname, inside } = await ready;
56
+ const target = inside(file);
57
+ await fs.mkdir(dirname(target), { recursive: true });
58
+ await fs.writeFile(target, content, 'utf8');
59
+ },
60
+ async listFiles(directory = '.') {
61
+ const { fs, inside } = await ready;
62
+ return (await fs.readdir(inside(directory))).sort();
63
+ },
64
+ async removeFile(file) {
65
+ const { fs, root, inside } = await ready;
66
+ const target = inside(file);
67
+ if (target === root)
68
+ throw refuse(file);
69
+ await fs.rm(target, { recursive: true, force: true });
70
+ },
71
+ async glob(pattern, options = {}) {
72
+ const matches = globMatcher(pattern);
73
+ return (await walk(options.cwd ?? '.'))
74
+ .filter((file) => matches(file.fromStart))
75
+ .slice(0, options.limit ?? 1_000)
76
+ .map((file) => file.fromRoot);
77
+ },
78
+ async grep(pattern, options = {}) {
79
+ const { fs } = await ready;
80
+ const expression = new RegExp(pattern, options.ignoreCase ? 'i' : '');
81
+ const only = options.glob ? globMatcher(options.glob) : undefined;
82
+ const limit = options.limit ?? 200;
83
+ const found = [];
84
+ let scanned = 0;
85
+ for (const file of await walk(options.cwd ?? '.')) {
86
+ if (only && !only(file.fromStart))
87
+ continue;
88
+ const size = (await fs.stat(file.full).catch(() => undefined))?.size;
89
+ if (size === undefined || size > GREP_FILE_BYTES)
90
+ continue;
91
+ scanned += size;
92
+ if (scanned > GREP_TOTAL_BYTES)
93
+ break;
94
+ const lines = matchingLines(await fs.readFile(file.full), expression, limit - found.length);
95
+ found.push(...lines.map((match) => ({ file: file.fromRoot, ...match })));
96
+ if (found.length >= limit)
97
+ break;
98
+ }
99
+ return found;
100
+ },
101
+ async readBytes(file) {
102
+ const { fs, inside } = await ready;
103
+ return new Uint8Array(await fs.readFile(inside(file)));
104
+ },
105
+ async writeBytes(file, bytes) {
106
+ const { fs, dirname, inside } = await ready;
107
+ const target = inside(file);
108
+ await fs.mkdir(dirname(target), { recursive: true });
109
+ await fs.writeFile(target, bytes);
110
+ },
111
+ };
112
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AgentMiddleware } from '../create-agent.js';
2
- import type { Sandbox } from '../sandbox.js';
3
- /** Where `filesystemContext()` reads and writes: a `Sandbox`, or anything with the same file methods. */
4
- export type FilesystemContextSource = Pick<Sandbox, 'readFile'> & Partial<Pick<Sandbox, 'writeFile' | 'listFiles'>>;
2
+ import type { Workspace } from '../workspace.js';
3
+ /** Where `filesystemContext()` reads and writes: a `Workspace` or a `Sandbox`, or anything with the same file methods. */
4
+ export type FilesystemContextSource = Pick<Workspace, 'readFile'> & Partial<Pick<Workspace, 'writeFile' | 'listFiles'>>;
5
5
  /** Where large tool results go instead of the transcript. */
6
6
  export interface ContextOffloadOptions {
7
7
  /** Results longer than this, in characters, are offloaded. Defaults to 20,000. */
@@ -12,7 +12,8 @@ export function containerSandbox(options) {
12
12
  if (unified.includes('\0'))
13
13
  throw new SandboxPathError(file);
14
14
  const parts = [];
15
- for (const segment of (unified.startsWith('/') ? unified : `${workdir}/${unified}`).split('/')) {
15
+ const rooted = unified === workdir || unified.startsWith(`${workdir}/`);
16
+ for (const segment of (rooted ? unified : `${workdir}/${unified.replace(/^\/+/, '')}`).split('/')) {
16
17
  if (segment === '' || segment === '.')
17
18
  continue;
18
19
  if (segment !== '..')
@@ -147,9 +148,10 @@ export function containerSandbox(options) {
147
148
  };
148
149
  const readBytes = async (file) => {
149
150
  const result = await shell('cat "$1"', [inside(file)]);
150
- if (result.code !== 0)
151
- throw failed(`Reading ${file}`, result);
152
- return result.stdout;
151
+ if (result.code === 0)
152
+ return result.stdout;
153
+ const error = failed(`Reading ${file}`, result);
154
+ throw /no such file/i.test(result.stderr) ? Object.assign(error, { code: 'ENOENT', path: file }) : error;
153
155
  };
154
156
  return {
155
157
  name: `container (${binary} ${options.image})`,
@@ -1,5 +1,6 @@
1
1
  /**
2
- * The sandbox contract: where an agent's commands and code run, and where its files live.
2
+ * The sandbox contract: where an agent's commands and code run. A sandbox is a `Workspace` that can
3
+ * also run commands, and says how it is isolated, so its files are the files its commands see.
3
4
  *
4
5
  * The package ships the contract and a conformance suite, not the isolation: a container, a VM, a
5
6
  * remote interpreter, or a hosted sandbox fills the `Sandbox` interface. `processSandbox()` is a
@@ -7,6 +8,7 @@
7
8
  * `isolation`, and is never a security boundary.
8
9
  */
9
10
  import type { ToolDefinition } from '../types/messages.js';
11
+ import { type Workspace, type WorkspaceGlobOptions, type WorkspaceGrepOptions, type WorkspaceMatch, WorkspacePathError } from './workspace.js';
10
12
  /** Options for one command. */
11
13
  export interface SandboxExecOptions {
12
14
  /** The working directory, inside the sandbox. Defaults to its root. */
@@ -33,33 +35,12 @@ export interface SandboxExecResult {
33
35
  /** How long it ran, in milliseconds. */
34
36
  durationMs: number;
35
37
  }
36
- /** Options for `Sandbox.glob()`. */
37
- export interface SandboxGlobOptions {
38
- /** The directory to search, inside the sandbox. Defaults to its root. */
39
- cwd?: string;
40
- /** The most paths returned. Defaults to 1,000. */
41
- limit?: number;
42
- }
43
- /** Options for `Sandbox.grep()`. */
44
- export interface SandboxGrepOptions {
45
- /** The directory to search, inside the sandbox. Defaults to its root. */
46
- cwd?: string;
47
- /** Searches only the files whose paths, relative to `cwd`, match this glob. */
48
- glob?: string;
49
- /** Matches without regard to case. */
50
- ignoreCase?: boolean;
51
- /** The most matching lines returned. Defaults to 200. */
52
- limit?: number;
53
- }
54
- /** One line `Sandbox.grep()` matched. */
55
- export interface SandboxMatch {
56
- /** The file, relative to the sandbox's root. */
57
- file: string;
58
- /** The line's number, counting from 1. */
59
- line: number;
60
- /** The line, cut to 300 characters. */
61
- text: string;
62
- }
38
+ /** Options for `Sandbox.glob()`: a workspace's. */
39
+ export type SandboxGlobOptions = WorkspaceGlobOptions;
40
+ /** Options for `Sandbox.grep()`: a workspace's. */
41
+ export type SandboxGrepOptions = WorkspaceGrepOptions;
42
+ /** One line `Sandbox.grep()` matched: a workspace's match. */
43
+ export type SandboxMatch = WorkspaceMatch;
63
44
  /** What a sandbox claims to isolate. The conformance suite checks the claims it can observe. */
64
45
  export interface SandboxIsolation {
65
46
  /** Commands cannot read or write the host's files. */
@@ -69,42 +50,20 @@ export interface SandboxIsolation {
69
50
  /** Commands cannot see or signal the host's processes. */
70
51
  processes: boolean;
71
52
  }
72
- /** Where an agent's commands, code, and files go. The application provides one. */
73
- export interface Sandbox {
74
- /** Names the sandbox in reports. */
75
- readonly name: string;
53
+ /**
54
+ * Where an agent's commands and code run: a workspace that can also run commands, and says what it
55
+ * isolates. The application provides one.
56
+ */
57
+ export interface Sandbox extends Workspace {
76
58
  /** What it isolates. */
77
59
  readonly isolation: SandboxIsolation;
78
- /** Runs a command line in the sandbox. */
60
+ /** Runs a command line in the sandbox, where its files are. */
79
61
  exec(command: string, options?: SandboxExecOptions): Promise<SandboxExecResult>;
80
- /** Reads a file, by a path inside the sandbox. */
81
- readFile(file: string): Promise<string>;
82
- /** Writes a file, creating its directory. */
83
- writeFile(file: string, content: string): Promise<void>;
84
- /** Lists a directory's entries. */
85
- listFiles?(directory?: string): Promise<string[]>;
86
- /** Removes a file or directory. */
87
- removeFile?(file: string): Promise<void>;
88
- /**
89
- * The files under a directory whose paths match a glob, such as `src/**\/*.ts`, sorted. The glob
90
- * matches paths relative to `cwd`; the paths returned are relative to the root, as `readFile`
91
- * takes them. Returns at most `limit`.
92
- */
93
- glob?(pattern: string, options?: SandboxGlobOptions): Promise<string[]>;
94
- /**
95
- * The lines matching a regular expression in the files under a directory, at most `limit`. Large
96
- * files and binary files are skipped.
97
- */
98
- grep?(pattern: string, options?: SandboxGrepOptions): Promise<SandboxMatch[]>;
99
- /** Reads a file's bytes, for an image, audio, or a document. */
100
- readBytes?(file: string): Promise<Uint8Array>;
101
62
  /** Releases the sandbox. */
102
63
  dispose?(): Promise<void>;
103
64
  }
104
- /** Raised when a path would leave the sandbox. */
105
- export declare class SandboxPathError extends Error {
106
- /** The path that was refused. */
107
- readonly path: string;
65
+ /** Raised when a path would leave the sandbox: a `WorkspacePathError` whose code is `SANDBOX_PATH`. */
66
+ export declare class SandboxPathError extends WorkspacePathError {
108
67
  /** Always `SANDBOX_PATH`. */
109
68
  readonly code = "SANDBOX_PATH";
110
69
  constructor(