@douglas-agent/sandbank-core 0.3.6

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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @douglas-agent/sandbank-core
2
+
3
+ > Unified sandbox SDK for AI agents — provider abstraction, capability system, and error types.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @douglas-agent/sandbank-core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createProvider, withTerminal, connectTerminal } from '@douglas-agent/sandbank-core'
15
+ import { DaytonaAdapter } from '@douglas-agent/sandbank-daytona'
16
+
17
+ const provider = createProvider(
18
+ new DaytonaAdapter({ apiKey: process.env.DAYTONA_API_KEY! })
19
+ )
20
+
21
+ // Create a sandbox with a non-root user
22
+ const sandbox = await provider.create({
23
+ image: 'node:22',
24
+ user: 'sandbank', // creates non-root user with sudo
25
+ })
26
+ const { stdout } = await sandbox.exec('node --version')
27
+ await sandbox.writeFile('/app/index.js', 'console.log("hi")')
28
+
29
+ // Run privileged commands with asRoot
30
+ await sandbox.exec('apt-get update', { asRoot: true })
31
+
32
+ // Capability detection
33
+ const terminal = withTerminal(sandbox)
34
+ if (terminal) {
35
+ const info = await terminal.startTerminal()
36
+ const session = connectTerminal(info)
37
+ await session.ready
38
+ session.onData((data) => process.stdout.write(data))
39
+ session.write('ls\n')
40
+ }
41
+
42
+ await provider.destroy(sandbox.id)
43
+ ```
44
+
45
+ ## Capabilities
46
+
47
+ Use `hasCapability(provider, name)` to check provider support, and `withTerminal(sandbox)` / `withStreaming(sandbox)` / etc. for type-safe downcasting.
48
+
49
+ | Capability | Description |
50
+ |------------|-------------|
51
+ | `exec.stream` | Real-time stdout/stderr streaming |
52
+ | `terminal` | Interactive web terminal (ttyd) |
53
+ | `sleep` | Hibernate and wake sandboxes |
54
+ | `volumes` | Persistent volume management |
55
+ | `snapshot` | Snapshot and restore sandbox state |
56
+ | `port.expose` | Expose sandbox ports to the internet |
57
+
58
+ ## Multi-Agent Sessions
59
+
60
+ ```typescript
61
+ import { createSession } from '@douglas-agent/sandbank-core'
62
+
63
+ const session = await createSession({ provider, relay: { type: 'memory' } })
64
+ const agent = await session.spawn('worker', { image: 'node:22' })
65
+ await session.waitForAll()
66
+ await session.close()
67
+ ```
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,18 @@
1
+ import type { Capability, PortExposeSandbox, Sandbox, SandboxProvider, ServiceProvider, SleepableSandbox, SnapshotSandbox, StreamableSandbox, TerminalSandbox, VolumeProvider } from './types.js';
2
+ /** 检查 provider 是否支持某个能力 */
3
+ export declare function hasCapability(provider: SandboxProvider, capability: Capability): boolean;
4
+ /** 向下转型为支持流式执行的 Sandbox,不支持则返回 null */
5
+ export declare function withStreaming(sandbox: Sandbox): StreamableSandbox | null;
6
+ /** 向下转型为支持终端的 Sandbox,不支持则返回 null */
7
+ export declare function withTerminal(sandbox: Sandbox): TerminalSandbox | null;
8
+ /** 向下转型为支持休眠的 Sandbox,不支持则返回 null */
9
+ export declare function withSleep(sandbox: Sandbox): SleepableSandbox | null;
10
+ /** 向下转型为支持端口暴露的 Sandbox,不支持则返回 null */
11
+ export declare function withPortExpose(sandbox: Sandbox): PortExposeSandbox | null;
12
+ /** 向下转型为支持快照的 Sandbox,不支持则返回 null */
13
+ export declare function withSnapshot(sandbox: Sandbox): SnapshotSandbox | null;
14
+ /** 向下转型为支持卷管理的 Provider,不支持则返回 null */
15
+ export declare function withVolumes(provider: SandboxProvider): VolumeProvider | null;
16
+ /** 向下转型为支持服务管理的 Provider,不支持则返回 null */
17
+ export declare function withServices(provider: SandboxProvider): ServiceProvider | null;
18
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,cAAc,EACf,MAAM,YAAY,CAAA;AAEnB,2BAA2B;AAC3B,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,eAAe,EACzB,UAAU,EAAE,UAAU,GACrB,OAAO,CAET;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAKxE;AAED,qCAAqC;AACrC,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAKrE;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAKnE;AAED,uCAAuC;AACvC,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAKzE;AAED,qCAAqC;AACrC,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAKrE;AAED,uCAAuC;AACvC,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,cAAc,GAAG,IAAI,CAS5E;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,GAAG,IAAI,CAU9E"}
@@ -0,0 +1,58 @@
1
+ /** 检查 provider 是否支持某个能力 */
2
+ export function hasCapability(provider, capability) {
3
+ return provider.capabilities.has(capability);
4
+ }
5
+ /** 向下转型为支持流式执行的 Sandbox,不支持则返回 null */
6
+ export function withStreaming(sandbox) {
7
+ if ('execStream' in sandbox && typeof sandbox.execStream === 'function') {
8
+ return sandbox;
9
+ }
10
+ return null;
11
+ }
12
+ /** 向下转型为支持终端的 Sandbox,不支持则返回 null */
13
+ export function withTerminal(sandbox) {
14
+ if ('startTerminal' in sandbox && typeof sandbox.startTerminal === 'function') {
15
+ return sandbox;
16
+ }
17
+ return null;
18
+ }
19
+ /** 向下转型为支持休眠的 Sandbox,不支持则返回 null */
20
+ export function withSleep(sandbox) {
21
+ if ('sleep' in sandbox && typeof sandbox.sleep === 'function') {
22
+ return sandbox;
23
+ }
24
+ return null;
25
+ }
26
+ /** 向下转型为支持端口暴露的 Sandbox,不支持则返回 null */
27
+ export function withPortExpose(sandbox) {
28
+ if ('exposePort' in sandbox && typeof sandbox.exposePort === 'function') {
29
+ return sandbox;
30
+ }
31
+ return null;
32
+ }
33
+ /** 向下转型为支持快照的 Sandbox,不支持则返回 null */
34
+ export function withSnapshot(sandbox) {
35
+ if ('createSnapshot' in sandbox && typeof sandbox.createSnapshot === 'function') {
36
+ return sandbox;
37
+ }
38
+ return null;
39
+ }
40
+ /** 向下转型为支持卷管理的 Provider,不支持则返回 null */
41
+ export function withVolumes(provider) {
42
+ if ('createVolume' in provider && typeof provider.createVolume === 'function' &&
43
+ 'deleteVolume' in provider && typeof provider.deleteVolume === 'function' &&
44
+ 'listVolumes' in provider && typeof provider.listVolumes === 'function') {
45
+ return provider;
46
+ }
47
+ return null;
48
+ }
49
+ /** 向下转型为支持服务管理的 Provider,不支持则返回 null */
50
+ export function withServices(provider) {
51
+ if ('createService' in provider && typeof provider.createService === 'function' &&
52
+ 'getService' in provider && typeof provider.getService === 'function' &&
53
+ 'listServices' in provider && typeof provider.listServices === 'function' &&
54
+ 'destroyService' in provider && typeof provider.destroyService === 'function') {
55
+ return provider;
56
+ }
57
+ return null;
58
+ }
@@ -0,0 +1,38 @@
1
+ import type { Capability, SandboxState } from './types.js';
2
+ /** 所有 SDK 错误的基类 */
3
+ export declare class SandboxError extends Error {
4
+ readonly provider: string;
5
+ readonly sandboxId?: string;
6
+ constructor(message: string, provider: string, sandboxId?: string);
7
+ }
8
+ /** 沙箱不存在(已销毁或从未创建) */
9
+ export declare class SandboxNotFoundError extends SandboxError {
10
+ constructor(provider: string, sandboxId: string);
11
+ }
12
+ /** 沙箱状态不对(如在 stopped 状态下 exec) */
13
+ export declare class SandboxStateError extends SandboxError {
14
+ readonly currentState: SandboxState;
15
+ readonly requiredState: SandboxState;
16
+ constructor(provider: string, sandboxId: string, currentState: SandboxState, requiredState: SandboxState);
17
+ }
18
+ /** 命令执行超时 */
19
+ export declare class ExecTimeoutError extends SandboxError {
20
+ readonly timeout: number;
21
+ constructor(provider: string, sandboxId: string, timeout: number);
22
+ }
23
+ /** Provider 返回速率限制 */
24
+ export declare class RateLimitError extends SandboxError {
25
+ readonly retryAfter?: number;
26
+ constructor(provider: string, retryAfter?: number);
27
+ }
28
+ /** Provider 自身错误(500、网络问题等) */
29
+ export declare class ProviderError extends SandboxError {
30
+ readonly cause: unknown;
31
+ constructor(provider: string, cause: unknown, sandboxId?: string);
32
+ }
33
+ /** 不支持的能力 */
34
+ export declare class CapabilityNotSupportedError extends SandboxError {
35
+ readonly capability: Capability;
36
+ constructor(provider: string, capability: Capability);
37
+ }
38
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE1D,mBAAmB;AACnB,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;gBAEf,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;CAMlE;AAED,sBAAsB;AACtB,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAIhD;AAED,kCAAkC;AAClC,qBAAa,iBAAkB,SAAQ,YAAY;IACjD,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAA;gBAExB,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY;CAUzG;AAED,aAAa;AACb,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;gBAEZ,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAKjE;AAED,sBAAsB;AACtB,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;gBAEhB,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAUlD;AAED,+BAA+B;AAC/B,qBAAa,aAAc,SAAQ,YAAY;IAC7C,SAAkB,KAAK,EAAE,OAAO,CAAA;gBAEpB,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM;CAMjE;AAED,aAAa;AACb,qBAAa,2BAA4B,SAAQ,YAAY;IAC3D,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;gBAEnB,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU;CAKrD"}
package/dist/errors.js ADDED
@@ -0,0 +1,68 @@
1
+ /** 所有 SDK 错误的基类 */
2
+ export class SandboxError extends Error {
3
+ provider;
4
+ sandboxId;
5
+ constructor(message, provider, sandboxId) {
6
+ super(message);
7
+ this.name = 'SandboxError';
8
+ this.provider = provider;
9
+ this.sandboxId = sandboxId;
10
+ }
11
+ }
12
+ /** 沙箱不存在(已销毁或从未创建) */
13
+ export class SandboxNotFoundError extends SandboxError {
14
+ constructor(provider, sandboxId) {
15
+ super(`Sandbox '${sandboxId}' not found`, provider, sandboxId);
16
+ this.name = 'SandboxNotFoundError';
17
+ }
18
+ }
19
+ /** 沙箱状态不对(如在 stopped 状态下 exec) */
20
+ export class SandboxStateError extends SandboxError {
21
+ currentState;
22
+ requiredState;
23
+ constructor(provider, sandboxId, currentState, requiredState) {
24
+ super(`Sandbox '${sandboxId}' is '${currentState}', expected '${requiredState}'`, provider, sandboxId);
25
+ this.name = 'SandboxStateError';
26
+ this.currentState = currentState;
27
+ this.requiredState = requiredState;
28
+ }
29
+ }
30
+ /** 命令执行超时 */
31
+ export class ExecTimeoutError extends SandboxError {
32
+ timeout;
33
+ constructor(provider, sandboxId, timeout) {
34
+ super(`Command timed out after ${timeout}ms`, provider, sandboxId);
35
+ this.name = 'ExecTimeoutError';
36
+ this.timeout = timeout;
37
+ }
38
+ }
39
+ /** Provider 返回速率限制 */
40
+ export class RateLimitError extends SandboxError {
41
+ retryAfter;
42
+ constructor(provider, retryAfter) {
43
+ super(retryAfter
44
+ ? `Rate limited, retry after ${retryAfter}s`
45
+ : 'Rate limited', provider);
46
+ this.name = 'RateLimitError';
47
+ this.retryAfter = retryAfter;
48
+ }
49
+ }
50
+ /** Provider 自身错误(500、网络问题等) */
51
+ export class ProviderError extends SandboxError {
52
+ cause;
53
+ constructor(provider, cause, sandboxId) {
54
+ const message = cause instanceof Error ? cause.message : String(cause);
55
+ super(`Provider error: ${message}`, provider, sandboxId);
56
+ this.name = 'ProviderError';
57
+ this.cause = cause;
58
+ }
59
+ }
60
+ /** 不支持的能力 */
61
+ export class CapabilityNotSupportedError extends SandboxError {
62
+ capability;
63
+ constructor(provider, capability) {
64
+ super(`Capability '${capability}' is not supported by '${provider}'`, provider);
65
+ this.name = 'CapabilityNotSupportedError';
66
+ this.capability = capability;
67
+ }
68
+ }
@@ -0,0 +1,22 @@
1
+ import type { AdapterSandbox } from './types.js';
2
+ /**
3
+ * 基于 exec 的 writeFile 默认实现。
4
+ * 将内容 base64 编码后通过 printf | base64 -d 写入文件。
5
+ */
6
+ export declare function writeFileViaExec(sandbox: AdapterSandbox, path: string, content: string | Uint8Array): Promise<void>;
7
+ /**
8
+ * 基于 exec 的 readFile 默认实现。
9
+ * 通过 base64 编码读取文件内容。
10
+ */
11
+ export declare function readFileViaExec(sandbox: AdapterSandbox, path: string): Promise<Uint8Array>;
12
+ /**
13
+ * 基于 exec 的 uploadArchive 默认实现。
14
+ * 将 tar.gz 数据 base64 编码后传输并解压。
15
+ */
16
+ export declare function uploadArchiveViaExec(sandbox: AdapterSandbox, archive: Uint8Array | ReadableStream, destDir?: string): Promise<void>;
17
+ /**
18
+ * 基于 exec 的 downloadArchive 默认实现。
19
+ * 将指定目录打包为 tar.gz 并通过 base64 传输。
20
+ */
21
+ export declare function downloadArchiveViaExec(sandbox: AdapterSandbox, srcDir?: string): Promise<ReadableStream>;
22
+ //# sourceMappingURL=file-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-helpers.d.ts","sourceRoot":"","sources":["../src/file-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAMhD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,UAAU,GAC3B,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,CAAC,CAarB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,UAAU,GAAG,cAAc,EACpC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAmDf;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,cAAc,CAAC,CAsCzB"}
@@ -0,0 +1,136 @@
1
+ function shellEscape(s) {
2
+ return "'" + s.replace(/'/g, "'\\''") + "'";
3
+ }
4
+ /**
5
+ * 基于 exec 的 writeFile 默认实现。
6
+ * 将内容 base64 编码后通过 printf | base64 -d 写入文件。
7
+ */
8
+ export async function writeFileViaExec(sandbox, path, content) {
9
+ const bytes = typeof content === 'string'
10
+ ? new TextEncoder().encode(content)
11
+ : content;
12
+ let binary = '';
13
+ const chunkSize = 8192;
14
+ for (let i = 0; i < bytes.length; i += chunkSize) {
15
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
16
+ }
17
+ const base64 = btoa(binary);
18
+ // 确保目标目录存在
19
+ const dir = path.substring(0, path.lastIndexOf('/'));
20
+ if (dir) {
21
+ await sandbox.exec(`mkdir -p ${shellEscape(dir)}`);
22
+ }
23
+ const result = await sandbox.exec(`printf '%s' ${shellEscape(base64)} | base64 -d > ${shellEscape(path)}`);
24
+ if (result.exitCode !== 0) {
25
+ throw new Error(`writeFile failed: ${result.stderr}`);
26
+ }
27
+ }
28
+ /**
29
+ * 基于 exec 的 readFile 默认实现。
30
+ * 通过 base64 编码读取文件内容。
31
+ */
32
+ export async function readFileViaExec(sandbox, path) {
33
+ const result = await sandbox.exec(`base64 ${shellEscape(path)}`);
34
+ if (result.exitCode !== 0) {
35
+ throw new Error(`readFile failed: ${result.stderr}`);
36
+ }
37
+ const clean = result.stdout.replace(/\s/g, '');
38
+ const binary = atob(clean);
39
+ const bytes = new Uint8Array(binary.length);
40
+ for (let i = 0; i < binary.length; i++) {
41
+ bytes[i] = binary.charCodeAt(i);
42
+ }
43
+ return bytes;
44
+ }
45
+ /**
46
+ * 基于 exec 的 uploadArchive 默认实现。
47
+ * 将 tar.gz 数据 base64 编码后传输并解压。
48
+ */
49
+ export async function uploadArchiveViaExec(sandbox, archive, destDir) {
50
+ // ReadableStream → Uint8Array
51
+ let bytes;
52
+ if (archive instanceof Uint8Array) {
53
+ bytes = archive;
54
+ }
55
+ else {
56
+ const reader = archive.getReader();
57
+ const chunks = [];
58
+ while (true) {
59
+ const { done, value } = await reader.read();
60
+ if (done)
61
+ break;
62
+ chunks.push(value);
63
+ }
64
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
65
+ bytes = new Uint8Array(totalLength);
66
+ let offset = 0;
67
+ for (const chunk of chunks) {
68
+ bytes.set(chunk, offset);
69
+ offset += chunk.length;
70
+ }
71
+ }
72
+ // base64 编码
73
+ let binary = '';
74
+ const chunkSize = 8192;
75
+ for (let i = 0; i < bytes.length; i += chunkSize) {
76
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
77
+ }
78
+ const base64 = btoa(binary);
79
+ const target = destDir ?? '/';
80
+ const tmp = `/tmp/_sb_archive_${Date.now()}_${Math.random().toString(36).slice(2)}.tar.gz`;
81
+ // 写入临时文件
82
+ const writeResult = await sandbox.exec(`printf '%s' ${shellEscape(base64)} | base64 -d > ${tmp}`);
83
+ if (writeResult.exitCode !== 0) {
84
+ throw new Error(`uploadArchive: write failed: ${writeResult.stderr}`);
85
+ }
86
+ // 确保目标目录存在
87
+ await sandbox.exec(`mkdir -p ${shellEscape(target)}`);
88
+ // 解压
89
+ const extractResult = await sandbox.exec(`tar xzf ${tmp} -C ${shellEscape(target)}`);
90
+ if (extractResult.exitCode !== 0) {
91
+ await sandbox.exec(`rm -f ${tmp}`);
92
+ throw new Error(`uploadArchive: extract failed: ${extractResult.stderr}`);
93
+ }
94
+ // 清理
95
+ await sandbox.exec(`rm -f ${tmp}`);
96
+ }
97
+ /**
98
+ * 基于 exec 的 downloadArchive 默认实现。
99
+ * 将指定目录打包为 tar.gz 并通过 base64 传输。
100
+ */
101
+ export async function downloadArchiveViaExec(sandbox, srcDir) {
102
+ const source = srcDir ?? '/';
103
+ const tmp = `/tmp/_sb_archive_${Date.now()}_${Math.random().toString(36).slice(2)}.tar.gz`;
104
+ // 打包
105
+ const tarResult = await sandbox.exec(`tar czf ${tmp} -C ${shellEscape(source)} .`);
106
+ if (tarResult.exitCode !== 0) {
107
+ await sandbox.exec(`rm -f ${tmp}`).catch(() => { });
108
+ throw new Error(`downloadArchive: tar failed: ${tarResult.stderr}`);
109
+ }
110
+ let readResult;
111
+ try {
112
+ // 读取 base64
113
+ readResult = await sandbox.exec(`base64 ${tmp}`);
114
+ if (readResult.exitCode !== 0) {
115
+ throw new Error(`downloadArchive: read failed: ${readResult.stderr}`);
116
+ }
117
+ }
118
+ finally {
119
+ // 清理临时文件(无论成功或失败)
120
+ await sandbox.exec(`rm -f ${tmp}`).catch(() => { });
121
+ }
122
+ // 解码为 Uint8Array
123
+ const clean = readResult.stdout.replace(/\s/g, '');
124
+ const binaryStr = atob(clean);
125
+ const bytes = new Uint8Array(binaryStr.length);
126
+ for (let i = 0; i < binaryStr.length; i++) {
127
+ bytes[i] = binaryStr.charCodeAt(i);
128
+ }
129
+ // 包装为 ReadableStream
130
+ return new ReadableStream({
131
+ start(controller) {
132
+ controller.enqueue(bytes);
133
+ controller.close();
134
+ },
135
+ });
136
+ }
@@ -0,0 +1,78 @@
1
+ import type { Sandbox } from './types.js';
2
+ export type ClaudeHookEvent = 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure' | 'Stop';
3
+ export interface InjectHooksConfig {
4
+ /**
5
+ * Event destination.
6
+ * - http: 使用 Claude Code 内置 HTTP hook 类型(sandbox 需能访问该 URL)
7
+ * - file: 使用 command hook 将事件追加到 JSONL 文件
8
+ */
9
+ endpoint: {
10
+ type: 'http';
11
+ url: string;
12
+ headers?: Record<string, string>;
13
+ } | {
14
+ type: 'file';
15
+ path?: string;
16
+ };
17
+ /** 要捕获的事件。默认: ['PostToolUse', 'Stop'] */
18
+ events?: ClaudeHookEvent[];
19
+ /** 是否异步执行 hook(不阻塞 agent)。默认: true */
20
+ async?: boolean;
21
+ /**
22
+ * settings.json 写入路径。默认: 自动检测 $HOME/.claude/settings.json
23
+ * 传入目录时会追加 /.claude/settings.json
24
+ */
25
+ settingsDir?: string;
26
+ }
27
+ export interface HookEventData {
28
+ /** Unix timestamp (ms) */
29
+ ts: number;
30
+ /** 原始 hook 输入数据 */
31
+ data: Record<string, unknown>;
32
+ }
33
+ export interface ClaudeLoginConfig {
34
+ /** 每次按 Enter 的间隔秒数。默认: 2 */
35
+ enterInterval?: number;
36
+ /** 最大按 Enter 次数。默认: 30 */
37
+ maxRetries?: number;
38
+ /** 安装依赖的命令。默认: apt-get update -qq && apt-get install -y -qq screen */
39
+ installCommand?: string;
40
+ }
41
+ export interface ClaudeLoginResult {
42
+ /** OAuth 授权 URL,用户需要在浏览器中打开 */
43
+ url: string;
44
+ /**
45
+ * 将 OAuth 回调返回的 auth code 发送到沙箱内的 claude login 进程。
46
+ * 使用 screen -X source 注入字符到 PTY,避免 shell 转义问题。
47
+ */
48
+ sendCode: (code: string) => Promise<void>;
49
+ /**
50
+ * 等待登录完成。用户在浏览器中完成授权后,此函数 resolve。
51
+ * 超时则 reject。
52
+ */
53
+ waitForCredentials: (timeoutMs?: number) => Promise<void>;
54
+ }
55
+ export declare const DEFAULT_EVENTS_FILE = "/tmp/sandbank-hook-events.jsonl";
56
+ /**
57
+ * 将 Claude Code hooks 配置注入沙箱。
58
+ * hooks 会在 agent 的每次工具调用后自动触发,将事件发送到指定端点。
59
+ */
60
+ export declare function injectClaudeHooks(sandbox: Sandbox, config: InjectHooksConfig): Promise<void>;
61
+ /**
62
+ * 从沙箱内的 JSONL 文件读取 hook 事件。
63
+ * 用于 file 模式下拉取事件。
64
+ */
65
+ export declare function readHookEvents(sandbox: Sandbox, path?: string): Promise<HookEventData[]>;
66
+ /**
67
+ * 在沙箱内自动化 `claude login`,捕获 OAuth 授权 URL。
68
+ *
69
+ * 使用 GNU screen 管理 PTY:
70
+ * - screen 提供真实 PTY,满足 claude login 的 TUI 需求
71
+ * - screen -X stuff 注入按键(导航 TUI)
72
+ * - screen -X hardcopy 截取屏幕纯文本(无 ANSI 转义码)
73
+ * - screen -X source 执行 stuff 命令文件(精确控制注入内容)
74
+ *
75
+ * 返回 URL 和 sendCode/waitForCredentials 回调。
76
+ */
77
+ export declare function startClaudeLogin(sandbox: Sandbox, config?: ClaudeLoginConfig): Promise<ClaudeLoginResult>;
78
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAIzC,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,aAAa,GAAG,oBAAoB,GAAG,MAAM,CAAA;AAE1F,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,EACJ;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAC/D;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAEnC,yCAAyC;IACzC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAA;IAE1B,sCAAsC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,0BAA0B;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC;;;OAGG;IACH,kBAAkB,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1D;AAID,eAAO,MAAM,mBAAmB,oCAAoC,CAAA;AAOpE;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAoCf;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,EAAE,CAAC,CAU1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAwH5B"}