@sandbank.dev/core 0.2.1 → 0.3.4
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 +8 -2
- package/dist/hooks.d.ts +78 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +218 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/observer.d.ts +32 -0
- package/dist/observer.d.ts.map +1 -0
- package/dist/observer.js +33 -0
- package/dist/provider.d.ts +5 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +58 -22
- package/dist/sandbox-user.d.ts +15 -0
- package/dist/sandbox-user.d.ts.map +1 -0
- package/dist/sandbox-user.js +47 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,11 +18,17 @@ const provider = createProvider(
|
|
|
18
18
|
new DaytonaAdapter({ apiKey: process.env.DAYTONA_API_KEY! })
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
// Create
|
|
22
|
-
const sandbox = await provider.create({
|
|
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
|
+
})
|
|
23
26
|
const { stdout } = await sandbox.exec('node --version')
|
|
24
27
|
await sandbox.writeFile('/app/index.js', 'console.log("hi")')
|
|
25
28
|
|
|
29
|
+
// Run privileged commands with asRoot
|
|
30
|
+
await sandbox.exec('apt-get update', { asRoot: true })
|
|
31
|
+
|
|
26
32
|
// Capability detection
|
|
27
33
|
const terminal = withTerminal(sandbox)
|
|
28
34
|
if (terminal) {
|
package/dist/hooks.d.ts
ADDED
|
@@ -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"}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// --- Constants ---
|
|
2
|
+
export const DEFAULT_EVENTS_FILE = '/tmp/sandbank-hook-events.jsonl';
|
|
3
|
+
const HANDLER_SCRIPT_PATH = '/tmp/sandbank-hook-handler.sh';
|
|
4
|
+
const SCREEN_SESSION = 'claude-login';
|
|
5
|
+
const SCREEN_OUTPUT = '/tmp/sandbank-screen-output';
|
|
6
|
+
// --- Public API ---
|
|
7
|
+
/**
|
|
8
|
+
* 将 Claude Code hooks 配置注入沙箱。
|
|
9
|
+
* hooks 会在 agent 的每次工具调用后自动触发,将事件发送到指定端点。
|
|
10
|
+
*/
|
|
11
|
+
export async function injectClaudeHooks(sandbox, config) {
|
|
12
|
+
const events = config.events ?? ['PostToolUse', 'Stop'];
|
|
13
|
+
const isAsync = config.async ?? true;
|
|
14
|
+
const hooksConfig = {};
|
|
15
|
+
for (const event of events) {
|
|
16
|
+
const hookDef = buildHookDef(event, config, isAsync);
|
|
17
|
+
hooksConfig[event] = [{
|
|
18
|
+
...(event !== 'Stop' ? { matcher: '.*' } : {}),
|
|
19
|
+
hooks: [hookDef],
|
|
20
|
+
}];
|
|
21
|
+
}
|
|
22
|
+
const settings = { hooks: hooksConfig };
|
|
23
|
+
// 确定 settings 目录:用户指定 > 自动检测 $HOME
|
|
24
|
+
let baseDir = config.settingsDir;
|
|
25
|
+
if (!baseDir) {
|
|
26
|
+
const homeResult = await sandbox.exec('echo $HOME');
|
|
27
|
+
baseDir = homeResult.stdout.trim() || '/root';
|
|
28
|
+
}
|
|
29
|
+
const claudeDir = `${baseDir}/.claude`;
|
|
30
|
+
const settingsPath = `${claudeDir}/settings.json`;
|
|
31
|
+
// 确保 .claude 目录存在
|
|
32
|
+
await sandbox.exec(`mkdir -p '${claudeDir}'`);
|
|
33
|
+
// 写入 settings.json
|
|
34
|
+
await sandbox.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
35
|
+
// file 模式: 写入 handler 脚本并创建事件文件
|
|
36
|
+
if (config.endpoint.type === 'file') {
|
|
37
|
+
const eventsPath = config.endpoint.path ?? DEFAULT_EVENTS_FILE;
|
|
38
|
+
await writeHandlerScript(sandbox, eventsPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 从沙箱内的 JSONL 文件读取 hook 事件。
|
|
43
|
+
* 用于 file 模式下拉取事件。
|
|
44
|
+
*/
|
|
45
|
+
export async function readHookEvents(sandbox, path) {
|
|
46
|
+
const filePath = path ?? DEFAULT_EVENTS_FILE;
|
|
47
|
+
const result = await sandbox.exec(`cat '${filePath}' 2>/dev/null || true`);
|
|
48
|
+
const output = result.stdout.trim();
|
|
49
|
+
if (!output)
|
|
50
|
+
return [];
|
|
51
|
+
return output
|
|
52
|
+
.split('\n')
|
|
53
|
+
.filter(line => line.trim())
|
|
54
|
+
.map(line => JSON.parse(line));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 在沙箱内自动化 `claude login`,捕获 OAuth 授权 URL。
|
|
58
|
+
*
|
|
59
|
+
* 使用 GNU screen 管理 PTY:
|
|
60
|
+
* - screen 提供真实 PTY,满足 claude login 的 TUI 需求
|
|
61
|
+
* - screen -X stuff 注入按键(导航 TUI)
|
|
62
|
+
* - screen -X hardcopy 截取屏幕纯文本(无 ANSI 转义码)
|
|
63
|
+
* - screen -X source 执行 stuff 命令文件(精确控制注入内容)
|
|
64
|
+
*
|
|
65
|
+
* 返回 URL 和 sendCode/waitForCredentials 回调。
|
|
66
|
+
*/
|
|
67
|
+
export async function startClaudeLogin(sandbox, config) {
|
|
68
|
+
const enterInterval = config?.enterInterval ?? 2;
|
|
69
|
+
const maxRetries = config?.maxRetries ?? 30;
|
|
70
|
+
const installCmd = config?.installCommand
|
|
71
|
+
?? 'apt-get update -qq && apt-get install -y -qq screen';
|
|
72
|
+
// 1. 确保 screen 已安装(需 root 权限安装包)
|
|
73
|
+
const checkScreen = await sandbox.exec('which screen 2>/dev/null');
|
|
74
|
+
if (checkScreen.exitCode !== 0) {
|
|
75
|
+
const installResult = await sandbox.exec(installCmd, { timeout: 60_000, asRoot: true });
|
|
76
|
+
if (installResult.exitCode !== 0) {
|
|
77
|
+
throw new Error(`Failed to install screen: ${installResult.stderr || installResult.stdout}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 2. 清理旧文件和 screen 会话
|
|
81
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X quit 2>/dev/null || true; `
|
|
82
|
+
+ `rm -f '${SCREEN_OUTPUT}' /tmp/sandbank-code-debug /tmp/sandbank-screen-cmd`);
|
|
83
|
+
// 3. 在 screen 会话中启动 claude login
|
|
84
|
+
// **不要** stty columns 1000 — screen 虚拟终端固定 80 列,
|
|
85
|
+
// stty 改宽度会让 TUI 不换行,但超出 80 列的部分直接不可见(hardcopy 不捕获)。
|
|
86
|
+
// 保持默认 80 列让 URL 自然换行,extractUrlFromText 会 trimEnd+join 重组。
|
|
87
|
+
await sandbox.exec(`screen -dmS ${SCREEN_SESSION} bash -c 'exec claude login'`);
|
|
88
|
+
// 等待 screen 会话启动
|
|
89
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
90
|
+
// 4. 反复检查屏幕并发送 Enter 直到出现 OAuth URL
|
|
91
|
+
// 关键: 先检查再发送 Enter,避免 URL 出现后多发一个 Enter 误提交空 code
|
|
92
|
+
let urlFound = false;
|
|
93
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
94
|
+
await new Promise(r => setTimeout(r, enterInterval * 1000));
|
|
95
|
+
// 先检查当前屏幕(纯文本,无 ANSI 转义码)
|
|
96
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X hardcopy ${SCREEN_OUTPUT}`);
|
|
97
|
+
const result = await sandbox.exec(`cat '${SCREEN_OUTPUT}' 2>/dev/null || true`);
|
|
98
|
+
if (result.stdout.includes('claude.ai/oauth')) {
|
|
99
|
+
urlFound = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
// URL 未找到,发送 Enter 导航 TUI
|
|
103
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X stuff $'\\r'`);
|
|
104
|
+
}
|
|
105
|
+
if (!urlFound) {
|
|
106
|
+
const output = await sandbox.exec(`cat '${SCREEN_OUTPUT}' 2>/dev/null || true`);
|
|
107
|
+
throw new Error(`Failed to detect OAuth URL after ${maxRetries} retries.\n`
|
|
108
|
+
+ `Screen output:\n${output.stdout.substring(0, 1000)}`);
|
|
109
|
+
}
|
|
110
|
+
// 5. 从 hardcopy 提取完整 URL(extractUrlFromText 处理 80 列断行)
|
|
111
|
+
const screenResult = await sandbox.exec(`cat '${SCREEN_OUTPUT}' 2>/dev/null || true`);
|
|
112
|
+
const url = extractUrlFromText(screenResult.stdout);
|
|
113
|
+
if (!url) {
|
|
114
|
+
throw new Error(`OAuth URL detected but failed to extract.\n`
|
|
115
|
+
+ `Screen output:\n${screenResult.stdout.substring(0, 1000)}`);
|
|
116
|
+
}
|
|
117
|
+
// 6. sendCode: 用 screen source + stuff 注入 code
|
|
118
|
+
const sendCode = async (code) => {
|
|
119
|
+
// 检查当前 screen 状态 — 如果之前的 Enter 导致了 "Invalid code",先恢复
|
|
120
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X hardcopy /tmp/sandbank-pre-send`);
|
|
121
|
+
const preState = await sandbox.exec('cat /tmp/sandbank-pre-send 2>/dev/null');
|
|
122
|
+
if (preState.stdout.match(/retry|try.again|Invalid/i)) {
|
|
123
|
+
// 发送 Enter 跳过 retry 提示,回到 code 输入
|
|
124
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X stuff $'\\r'`);
|
|
125
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
126
|
+
}
|
|
127
|
+
// 直接用 screen -X stuff 注入 code + CR
|
|
128
|
+
// 不能用 source 文件:auth code 含 # 字符,screen source 解析器会把 # 后内容当注释
|
|
129
|
+
// 用 $'...\r' 安全发送:$'...' 内 # 是字面量,\r 被 bash 解释为 CR
|
|
130
|
+
const escaped = code.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
131
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X stuff $'${escaped}\\r'`);
|
|
132
|
+
};
|
|
133
|
+
// 7. waitForCredentials
|
|
134
|
+
const waitForCredentials = async (timeoutMs = 300_000) => {
|
|
135
|
+
const homeResult = await sandbox.exec('echo $HOME');
|
|
136
|
+
const home = homeResult.stdout.trim() || '/root';
|
|
137
|
+
const credPath = `${home}/.claude/.credentials.json`;
|
|
138
|
+
const start = Date.now();
|
|
139
|
+
const interval = 2000;
|
|
140
|
+
while (Date.now() - start < timeoutMs) {
|
|
141
|
+
const check = await sandbox.exec(`test -s '${credPath}' && echo OK || echo MISSING`);
|
|
142
|
+
if (check.stdout.trim() === 'OK') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await new Promise(r => setTimeout(r, interval));
|
|
146
|
+
}
|
|
147
|
+
// 收集诊断信息
|
|
148
|
+
const diag = [`Timed out waiting for credentials at ${credPath} (${timeoutMs}ms)`];
|
|
149
|
+
await sandbox.exec(`screen -S ${SCREEN_SESSION} -X hardcopy /tmp/sandbank-screen-final 2>/dev/null || true`);
|
|
150
|
+
const screenFinal = await sandbox.exec('cat /tmp/sandbank-screen-final 2>/dev/null');
|
|
151
|
+
if (screenFinal.stdout)
|
|
152
|
+
diag.push(`Screen (final):\n${screenFinal.stdout}`);
|
|
153
|
+
const codeDebug = await sandbox.exec('cat /tmp/sandbank-code-debug 2>/dev/null');
|
|
154
|
+
if (codeDebug.stdout)
|
|
155
|
+
diag.push(`Code debug:\n${codeDebug.stdout}`);
|
|
156
|
+
const screenCmd = await sandbox.exec('cat /tmp/sandbank-screen-cmd 2>/dev/null');
|
|
157
|
+
if (screenCmd.stdout)
|
|
158
|
+
diag.push(`Screen cmd:\n${screenCmd.stdout}`);
|
|
159
|
+
const psResult = await sandbox.exec('ps aux | grep -E "screen|claude" | grep -v grep 2>/dev/null');
|
|
160
|
+
if (psResult.stdout)
|
|
161
|
+
diag.push(`Processes:\n${psResult.stdout}`);
|
|
162
|
+
throw new Error(diag.join('\n\n'));
|
|
163
|
+
};
|
|
164
|
+
return { url, sendCode, waitForCredentials };
|
|
165
|
+
}
|
|
166
|
+
// --- Internal ---
|
|
167
|
+
function buildHookDef(event, config, isAsync) {
|
|
168
|
+
if (config.endpoint.type === 'http') {
|
|
169
|
+
return {
|
|
170
|
+
type: 'http',
|
|
171
|
+
url: config.endpoint.url,
|
|
172
|
+
...(config.endpoint.headers ? { headers: config.endpoint.headers } : {}),
|
|
173
|
+
timeout: 10,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// file 模式: 使用 command hook 调用 handler 脚本
|
|
177
|
+
return {
|
|
178
|
+
type: 'command',
|
|
179
|
+
command: HANDLER_SCRIPT_PATH,
|
|
180
|
+
timeout: 5,
|
|
181
|
+
async: isAsync,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const URL_RE = /https?:\/\/[^\s'"><]+/g;
|
|
185
|
+
/**
|
|
186
|
+
* 从 screen hardcopy 纯文本中提取 OAuth URL。
|
|
187
|
+
*
|
|
188
|
+
* hardcopy 按 screen 虚拟终端宽度(默认 80 列)输出,
|
|
189
|
+
* 长 URL 会被断行,每行尾部填充空格到 80 列。
|
|
190
|
+
* 先 trimEnd 每行再拼接,消除断行产生的空格,重组完整 URL。
|
|
191
|
+
*/
|
|
192
|
+
function extractUrlFromText(text) {
|
|
193
|
+
if (!text)
|
|
194
|
+
return null;
|
|
195
|
+
// 重组被 hardcopy 断行的长 URL
|
|
196
|
+
const joined = text.split('\n').map(line => line.trimEnd()).join('');
|
|
197
|
+
const urls = joined.match(URL_RE);
|
|
198
|
+
if (!urls)
|
|
199
|
+
return null;
|
|
200
|
+
const oauthUrls = urls.filter(u => u.includes('claude.ai/oauth'));
|
|
201
|
+
if (oauthUrls.length === 0)
|
|
202
|
+
return null;
|
|
203
|
+
return oauthUrls.reduce((a, b) => a.length >= b.length ? a : b);
|
|
204
|
+
}
|
|
205
|
+
async function writeHandlerScript(sandbox, eventsPath) {
|
|
206
|
+
// 使用纯 POSIX shell 以保证最大兼容性
|
|
207
|
+
// tr -d '\n' 确保多行 JSON 输入被压成单行 JSONL
|
|
208
|
+
const script = `#!/bin/sh
|
|
209
|
+
TS=$(($(date +%s) * 1000))
|
|
210
|
+
INPUT=$(cat | tr -d '\\n')
|
|
211
|
+
printf '{"ts":%d,"data":%s}\\n' "$TS" "$INPUT" >> '${eventsPath}'
|
|
212
|
+
`;
|
|
213
|
+
await sandbox.writeFile(HANDLER_SCRIPT_PATH, script);
|
|
214
|
+
// writeFile 以 root 写入,chmod 需 root 权限
|
|
215
|
+
await sandbox.exec(`chmod +x '${HANDLER_SCRIPT_PATH}'`, { asRoot: true });
|
|
216
|
+
// events 文件由 hook handler(以沙箱用户身份)追加写入,需用户可写
|
|
217
|
+
await sandbox.exec(`touch '${eventsPath}'`);
|
|
218
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
export type { SandboxProvider, Sandbox, CreateConfig, ExecOptions, ExecResult, SandboxState, SandboxInfo, ListFilter, Capability, StreamableSandbox, TerminalSandbox, TerminalOptions, TerminalInfo, TerminalSession, Disposable, SleepableSandbox, PortExposeSandbox, SnapshotSandbox, VolumeProvider, VolumeConfig, VolumeInfo, ServiceType, ServiceConfig, ServiceCredentials, ServiceInfo, ServiceProvider, ServiceBinding, SandboxAdapter, AdapterSandbox, SkillDefinition, } from './types.js';
|
|
1
|
+
export type { SandboxProvider, Sandbox, CreateConfig, ExecOptions, ExecResult, SandboxState, SandboxInfo, ListFilter, Capability, StreamableSandbox, TerminalSandbox, TerminalOptions, TerminalInfo, TerminalSession, Disposable, SleepableSandbox, PortExposeSandbox, SnapshotSandbox, VolumeProvider, VolumeConfig, VolumeInfo, ServiceType, ServiceConfig, ServiceCredentials, ServiceInfo, ServiceProvider, ServiceBinding, SandboxAdapter, AdapterSandbox, SkillDefinition, SandboxUser, SandboxUserInfo, } from './types.js';
|
|
2
2
|
export { createProvider } from './provider.js';
|
|
3
3
|
export { hasCapability, withStreaming, withTerminal, withSleep, withPortExpose, withSnapshot, withVolumes, withServices, } from './capabilities.js';
|
|
4
4
|
export { SandboxError, SandboxNotFoundError, SandboxStateError, ExecTimeoutError, RateLimitError, ProviderError, CapabilityNotSupportedError, } from './errors.js';
|
|
5
5
|
export { connectTerminal } from './terminal.js';
|
|
6
6
|
export { injectSkills } from './skill-inject.js';
|
|
7
7
|
export { writeFileViaExec, readFileViaExec, uploadArchiveViaExec, downloadArchiveViaExec } from './file-helpers.js';
|
|
8
|
+
export { setupSandboxUser, wrapAsUser } from './sandbox-user.js';
|
|
9
|
+
export type { SandboxEvent, SandboxEventType, SandboxObserver, ProviderOptions } from './observer.js';
|
|
10
|
+
export { emitEvent, createNoopObserver, createWebhookObserver } from './observer.js';
|
|
11
|
+
export type { ClaudeHookEvent, InjectHooksConfig, HookEventData, ClaudeLoginConfig, ClaudeLoginResult } from './hooks.js';
|
|
12
|
+
export { injectClaudeHooks, readHookEvents, startClaudeLogin, DEFAULT_EVENTS_FILE } from './hooks.js';
|
|
8
13
|
export type { MessagePriority, SessionMessage, ContextStore, CompletionStatus, SandboxCompletion, SendOptions, Session, RelayConfig, CreateSessionConfig, JsonRpcRequest, JsonRpcResponse, JsonRpcNotification, JsonRpcError, Transport, } from './session-types.js';
|
|
9
14
|
export { createSession } from './session.js';
|
|
10
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAEV,eAAe,EACf,OAAO,EACP,YAAY,EACZ,WAAW,EACX,UAAU,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EAEV,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,UAAU,EAEV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,EAEd,cAAc,EACd,cAAc,EAEd,eAAe,GAChB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,SAAS,EACT,cAAc,EACd,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAG/C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAGnH,YAAY,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,OAAO,EACP,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,SAAS,GACV,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAEV,eAAe,EACf,OAAO,EACP,YAAY,EACZ,WAAW,EACX,UAAU,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EAEV,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,UAAU,EAEV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,EAEd,cAAc,EACd,cAAc,EAEd,eAAe,EAEf,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,SAAS,EACT,cAAc,EACd,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAG/C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAGnH,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAGhE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACrG,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAGpF,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACzH,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAGrG,YAAY,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,OAAO,EACP,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,SAAS,GACV,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,9 @@ export { connectTerminal } from './terminal.js';
|
|
|
10
10
|
export { injectSkills } from './skill-inject.js';
|
|
11
11
|
// File helpers (for adapter authors)
|
|
12
12
|
export { writeFileViaExec, readFileViaExec, uploadArchiveViaExec, downloadArchiveViaExec } from './file-helpers.js';
|
|
13
|
+
// Sandbox user
|
|
14
|
+
export { setupSandboxUser, wrapAsUser } from './sandbox-user.js';
|
|
15
|
+
export { emitEvent, createNoopObserver, createWebhookObserver } from './observer.js';
|
|
16
|
+
export { injectClaudeHooks, readHookEvents, startClaudeLogin, DEFAULT_EVENTS_FILE } from './hooks.js';
|
|
13
17
|
// Session factory
|
|
14
18
|
export { createSession } from './session.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SandboxEventType = 'sandbox:exec' | 'sandbox:writeFile' | 'sandbox:readFile' | 'sandbox:uploadArchive' | 'sandbox:downloadArchive';
|
|
2
|
+
export interface SandboxEvent {
|
|
3
|
+
/** 事件类型 */
|
|
4
|
+
type: SandboxEventType;
|
|
5
|
+
/** 沙箱 ID */
|
|
6
|
+
sandboxId: string;
|
|
7
|
+
/** 关联的任务 ID(可选) */
|
|
8
|
+
taskId?: string;
|
|
9
|
+
/** 事件时间戳(Unix ms) */
|
|
10
|
+
timestamp: number;
|
|
11
|
+
/** 事件数据(命令、路径、退出码等) */
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface SandboxObserver {
|
|
15
|
+
/** 接收一个沙箱事件。可以返回 Promise,但调用方不会等待。 */
|
|
16
|
+
onEvent(event: SandboxEvent): void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface ProviderOptions {
|
|
19
|
+
/** 可选的事件观察者。设置后,所有沙箱操作都会自动记录。 */
|
|
20
|
+
observer?: SandboxObserver;
|
|
21
|
+
/** 默认任务 ID,会附加到所有事件上 */
|
|
22
|
+
taskId?: string;
|
|
23
|
+
}
|
|
24
|
+
/** 安全地发射事件:不阻塞、不抛错 */
|
|
25
|
+
export declare function emitEvent(observer: SandboxObserver, event: SandboxEvent): void;
|
|
26
|
+
/** 空观察者,不做任何操作 */
|
|
27
|
+
export declare function createNoopObserver(): SandboxObserver;
|
|
28
|
+
/** Webhook 观察者,将事件 POST 到指定 URL */
|
|
29
|
+
export declare function createWebhookObserver(url: string, options?: {
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
}): SandboxObserver;
|
|
32
|
+
//# sourceMappingURL=observer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.d.ts","sourceRoot":"","sources":["../src/observer.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,mBAAmB,GACnB,kBAAkB,GAClB,uBAAuB,GACvB,yBAAyB,CAAA;AAE7B,MAAM,WAAW,YAAY;IAC3B,WAAW;IACX,IAAI,EAAE,gBAAgB,CAAA;IACtB,YAAY;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,mBAAmB;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnD;AAED,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,sBAAsB;AACtB,wBAAgB,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAS9E;AAED,kBAAkB;AAClB,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAED,mCAAmC;AACnC,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7C,eAAe,CAajB"}
|
package/dist/observer.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// --- Sandbox Observer ---
|
|
2
|
+
/** 安全地发射事件:不阻塞、不抛错 */
|
|
3
|
+
export function emitEvent(observer, event) {
|
|
4
|
+
try {
|
|
5
|
+
const result = observer.onEvent(event);
|
|
6
|
+
if (result && typeof result.catch === 'function') {
|
|
7
|
+
;
|
|
8
|
+
result.catch(() => { });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// observer 报错不影响沙箱操作
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** 空观察者,不做任何操作 */
|
|
16
|
+
export function createNoopObserver() {
|
|
17
|
+
return { onEvent() { } };
|
|
18
|
+
}
|
|
19
|
+
/** Webhook 观察者,将事件 POST 到指定 URL */
|
|
20
|
+
export function createWebhookObserver(url, options) {
|
|
21
|
+
return {
|
|
22
|
+
onEvent(event) {
|
|
23
|
+
return fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...options?.headers,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(event),
|
|
30
|
+
}).then(() => { });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { SandboxAdapter, SandboxProvider } from './types.js';
|
|
2
|
+
import type { SandboxObserver } from './observer.js';
|
|
2
3
|
/** 创建一个 SandboxProvider */
|
|
3
|
-
export declare function createProvider(adapter: SandboxAdapter
|
|
4
|
+
export declare function createProvider(adapter: SandboxAdapter, options?: {
|
|
5
|
+
observer?: SandboxObserver;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
}): SandboxProvider;
|
|
4
8
|
//# sourceMappingURL=provider.d.ts.map
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAQV,cAAc,EAEd,eAAe,EAShB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAQV,cAAc,EAEd,eAAe,EAShB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,eAAe,EAAoB,MAAM,eAAe,CAAA;AA8ItE,2BAA2B;AAC3B,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD,eAAe,CAmFjB"}
|
package/dist/provider.js
CHANGED
|
@@ -1,37 +1,66 @@
|
|
|
1
|
+
import { emitEvent } from './observer.js';
|
|
1
2
|
import { CapabilityNotSupportedError, ProviderError } from './errors.js';
|
|
2
3
|
import { injectSkills } from './skill-inject.js';
|
|
3
4
|
import { readFileViaExec, writeFileViaExec, uploadArchiveViaExec, downloadArchiveViaExec } from './file-helpers.js';
|
|
5
|
+
import { setupSandboxUser, wrapAsUser } from './sandbox-user.js';
|
|
4
6
|
/**
|
|
5
7
|
* 将 AdapterSandbox 包装为完整的 Sandbox 接口。
|
|
6
8
|
* 自动补充缺失的 writeFile/readFile 默认实现。
|
|
9
|
+
* 若传入 observer,自动对所有操作发射事件。
|
|
7
10
|
*/
|
|
8
|
-
function wrapSandbox(raw, providerName) {
|
|
11
|
+
function wrapSandbox(raw, providerName, observer, taskId, userInfo) {
|
|
12
|
+
function emit(type, data) {
|
|
13
|
+
if (!observer)
|
|
14
|
+
return;
|
|
15
|
+
emitEvent(observer, { type, sandboxId: raw.id, taskId, timestamp: Date.now(), data });
|
|
16
|
+
}
|
|
9
17
|
const sandbox = {
|
|
10
18
|
get id() { return raw.id; },
|
|
11
19
|
get state() { return raw.state; },
|
|
12
20
|
get createdAt() { return raw.createdAt; },
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
get user() { return userInfo; },
|
|
22
|
+
async exec(command, options) {
|
|
23
|
+
let cmd = command;
|
|
24
|
+
let opts = options;
|
|
25
|
+
// 非 root 用户包装: 默认以该用户执行,asRoot 跳过
|
|
26
|
+
if (userInfo && !options?.asRoot) {
|
|
27
|
+
cmd = wrapAsUser(command, userInfo.name, options?.cwd);
|
|
28
|
+
// cwd 已包含在 wrapped command 中,不再传给 adapter
|
|
29
|
+
opts = options ? { ...options, cwd: undefined, asRoot: undefined } : undefined;
|
|
30
|
+
}
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
try {
|
|
33
|
+
const result = await raw.exec(cmd, opts);
|
|
34
|
+
emit('sandbox:exec', { command, exitCode: result.exitCode, duration: Date.now() - start });
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
emit('sandbox:exec', { command, error: err.message, duration: Date.now() - start });
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
15
41
|
},
|
|
16
|
-
writeFile(path, content) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
42
|
+
async writeFile(path, content) {
|
|
43
|
+
const size = typeof content === 'string' ? content.length : content.byteLength;
|
|
44
|
+
const fn = raw.writeFile ? raw.writeFile.bind(raw) : (p, c) => writeFileViaExec(raw, p, c);
|
|
45
|
+
await fn(path, content);
|
|
46
|
+
emit('sandbox:writeFile', { path, size });
|
|
20
47
|
},
|
|
21
|
-
readFile(path) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
async readFile(path) {
|
|
49
|
+
const fn = raw.readFile ? raw.readFile.bind(raw) : (p) => readFileViaExec(raw, p);
|
|
50
|
+
const result = await fn(path);
|
|
51
|
+
emit('sandbox:readFile', { path, size: result.byteLength });
|
|
52
|
+
return result;
|
|
25
53
|
},
|
|
26
|
-
uploadArchive(archive, destDir) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
async uploadArchive(archive, destDir) {
|
|
55
|
+
const fn = raw.uploadArchive ? raw.uploadArchive.bind(raw) : (a, d) => uploadArchiveViaExec(raw, a, d);
|
|
56
|
+
await fn(archive, destDir);
|
|
57
|
+
emit('sandbox:uploadArchive', { destDir: destDir ?? '/' });
|
|
30
58
|
},
|
|
31
|
-
downloadArchive(srcDir) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
async downloadArchive(srcDir) {
|
|
60
|
+
const fn = raw.downloadArchive ? raw.downloadArchive.bind(raw) : (s) => downloadArchiveViaExec(raw, s);
|
|
61
|
+
const result = await fn(srcDir);
|
|
62
|
+
emit('sandbox:downloadArchive', { srcDir: srcDir ?? '/' });
|
|
63
|
+
return result;
|
|
35
64
|
},
|
|
36
65
|
};
|
|
37
66
|
// 转发可选能力方法(只有 adapter 真正实现的才转发)
|
|
@@ -89,8 +118,10 @@ function detectCapabilities(adapter) {
|
|
|
89
118
|
return validated;
|
|
90
119
|
}
|
|
91
120
|
/** 创建一个 SandboxProvider */
|
|
92
|
-
export function createProvider(adapter) {
|
|
121
|
+
export function createProvider(adapter, options) {
|
|
93
122
|
const capabilities = detectCapabilities(adapter);
|
|
123
|
+
const observer = options?.observer;
|
|
124
|
+
const taskId = options?.taskId;
|
|
94
125
|
const provider = {
|
|
95
126
|
get name() { return adapter.name; },
|
|
96
127
|
get capabilities() { return capabilities; },
|
|
@@ -115,7 +146,12 @@ export function createProvider(adapter) {
|
|
|
115
146
|
config = { ...config, env: mergedEnv };
|
|
116
147
|
}
|
|
117
148
|
const raw = await adapter.createSandbox(config);
|
|
118
|
-
|
|
149
|
+
// 创建非 root 用户(如果配置了)
|
|
150
|
+
let userInfo;
|
|
151
|
+
if (config.user) {
|
|
152
|
+
userInfo = await setupSandboxUser(raw, config.user);
|
|
153
|
+
}
|
|
154
|
+
const sandbox = wrapSandbox(raw, adapter.name, observer, taskId, userInfo);
|
|
119
155
|
if (config.skills?.length) {
|
|
120
156
|
try {
|
|
121
157
|
await injectSkills(sandbox, config.skills);
|
|
@@ -129,7 +165,7 @@ export function createProvider(adapter) {
|
|
|
129
165
|
},
|
|
130
166
|
async get(id) {
|
|
131
167
|
const raw = await adapter.getSandbox(id);
|
|
132
|
-
return wrapSandbox(raw, adapter.name);
|
|
168
|
+
return wrapSandbox(raw, adapter.name, observer, taskId);
|
|
133
169
|
},
|
|
134
170
|
async list(filter) {
|
|
135
171
|
return adapter.listSandboxes(filter);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AdapterSandbox, SandboxUser, SandboxUserInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 在沙箱中创建非 root 用户。
|
|
4
|
+
* 假设 Debian/Ubuntu 基础镜像(useradd)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function setupSandboxUser(sandbox: AdapterSandbox, config: string | SandboxUser): Promise<SandboxUserInfo>;
|
|
7
|
+
/**
|
|
8
|
+
* 将命令包装为指定用户执行。
|
|
9
|
+
* 使用 `su - <user> -c '...'` — 由 root 调用无需密码,`-` 设置完整环境。
|
|
10
|
+
*
|
|
11
|
+
* 构建 inner command(su 的 bash 将执行的内容),然后整体做一次
|
|
12
|
+
* 单引号转义放入外层 `'...'`。cwd 在 inner command 中用双引号包裹。
|
|
13
|
+
*/
|
|
14
|
+
export declare function wrapAsUser(command: string, user: string, cwd?: string): string;
|
|
15
|
+
//# sourceMappingURL=sandbox-user.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-user.d.ts","sourceRoot":"","sources":["../src/sandbox-user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE9E;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,MAAM,GAAG,WAAW,GAC3B,OAAO,CAAC,eAAe,CAAC,CA8B1B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ9E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在沙箱中创建非 root 用户。
|
|
3
|
+
* 假设 Debian/Ubuntu 基础镜像(useradd)。
|
|
4
|
+
*/
|
|
5
|
+
export async function setupSandboxUser(sandbox, config) {
|
|
6
|
+
const opts = typeof config === 'string' ? { name: config } : config;
|
|
7
|
+
const name = opts.name ?? 'sandbank';
|
|
8
|
+
const sudo = opts.sudo ?? true;
|
|
9
|
+
// 1. 创建用户(如果不存在)
|
|
10
|
+
const uidFlag = opts.uid != null ? `-u ${opts.uid}` : '';
|
|
11
|
+
const result = await sandbox.exec(`id ${name} >/dev/null 2>&1 || useradd -m -s /bin/bash ${uidFlag} ${name}`);
|
|
12
|
+
if (result.exitCode !== 0) {
|
|
13
|
+
throw new Error(`Failed to create user '${name}': ${result.stderr || result.stdout}`);
|
|
14
|
+
}
|
|
15
|
+
// 2. 获取 home 目录
|
|
16
|
+
const homeResult = await sandbox.exec(`eval echo ~${name}`);
|
|
17
|
+
const home = homeResult.stdout.trim();
|
|
18
|
+
if (!home || home === `~${name}`) {
|
|
19
|
+
throw new Error(`Failed to resolve home directory for user '${name}'`);
|
|
20
|
+
}
|
|
21
|
+
// 3. 配置 sudo(可选)
|
|
22
|
+
if (sudo) {
|
|
23
|
+
await sandbox.exec(`command -v sudo >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq sudo) 2>/dev/null; `
|
|
24
|
+
+ `echo '${name} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/${name} && chmod 440 /etc/sudoers.d/${name}`);
|
|
25
|
+
}
|
|
26
|
+
return { name, home };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 将命令包装为指定用户执行。
|
|
30
|
+
* 使用 `su - <user> -c '...'` — 由 root 调用无需密码,`-` 设置完整环境。
|
|
31
|
+
*
|
|
32
|
+
* 构建 inner command(su 的 bash 将执行的内容),然后整体做一次
|
|
33
|
+
* 单引号转义放入外层 `'...'`。cwd 在 inner command 中用双引号包裹。
|
|
34
|
+
*/
|
|
35
|
+
export function wrapAsUser(command, user, cwd) {
|
|
36
|
+
let innerCmd = command;
|
|
37
|
+
if (cwd) {
|
|
38
|
+
// 双引号包裹 cwd,转义双引号上下文中的特殊字符
|
|
39
|
+
const safeCwd = cwd.replace(/["$`\\]/g, '\\$&');
|
|
40
|
+
innerCmd = `cd "${safeCwd}" && ${command}`;
|
|
41
|
+
}
|
|
42
|
+
return `su - ${user} -c '${escapeSingleQuotes(innerCmd)}'`;
|
|
43
|
+
}
|
|
44
|
+
/** POSIX 单引号转义: ' → '\'' */
|
|
45
|
+
function escapeSingleQuotes(s) {
|
|
46
|
+
return s.replace(/'/g, "'\\''");
|
|
47
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ export interface SkillDefinition {
|
|
|
19
19
|
content: string;
|
|
20
20
|
}
|
|
21
21
|
export interface CreateConfig {
|
|
22
|
-
/** 容器镜像(如 'node:22-slim', 'ubuntu:24.04'
|
|
23
|
-
image
|
|
22
|
+
/** 容器镜像(如 'node:22-slim', 'ubuntu:24.04')。使用 snapshot 时可省略。 */
|
|
23
|
+
image?: string;
|
|
24
24
|
/** 环境变量注入 */
|
|
25
25
|
env?: Record<string, string>;
|
|
26
26
|
/** 资源配置(provider 会映射到最接近的规格) */
|
|
@@ -47,11 +47,34 @@ export interface CreateConfig {
|
|
|
47
47
|
skills?: SkillDefinition[];
|
|
48
48
|
/** 绑定的服务。凭证自动注入为环境变量(需 provider 支持 'services' 能力) */
|
|
49
49
|
services?: ServiceBinding[];
|
|
50
|
+
/** 端口映射 [hostPort, guestPort][]。本地模式使用,将容器端口转发到宿主机端口 */
|
|
51
|
+
ports?: [number, number][];
|
|
52
|
+
/**
|
|
53
|
+
* 创建非 root 用户并以该用户身份执行命令。
|
|
54
|
+
* - string: 用户名(等价于 { name: 'xxx' })
|
|
55
|
+
* - object: 完整配置
|
|
56
|
+
* - 未设置: 保持 root(向后兼容)
|
|
57
|
+
*/
|
|
58
|
+
user?: string | SandboxUser;
|
|
50
59
|
}
|
|
51
60
|
export interface ListFilter {
|
|
52
61
|
state?: SandboxState | SandboxState[];
|
|
53
62
|
limit?: number;
|
|
54
63
|
}
|
|
64
|
+
export interface SandboxUser {
|
|
65
|
+
/** 用户名。默认: 'sandbank' */
|
|
66
|
+
name?: string;
|
|
67
|
+
/** 指定 UID。默认: 自动分配 */
|
|
68
|
+
uid?: number;
|
|
69
|
+
/** 是否授予 sudo 权限。默认: true */
|
|
70
|
+
sudo?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface SandboxUserInfo {
|
|
73
|
+
/** 用户名 */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Home 目录路径 */
|
|
76
|
+
home: string;
|
|
77
|
+
}
|
|
55
78
|
export interface Sandbox {
|
|
56
79
|
/** 沙箱唯一 ID */
|
|
57
80
|
readonly id: string;
|
|
@@ -59,6 +82,8 @@ export interface Sandbox {
|
|
|
59
82
|
readonly state: SandboxState;
|
|
60
83
|
/** 创建时间 */
|
|
61
84
|
readonly createdAt: string;
|
|
85
|
+
/** 非 root 用户信息(如已配置) */
|
|
86
|
+
readonly user?: SandboxUserInfo;
|
|
62
87
|
/** 执行命令,等待完成 */
|
|
63
88
|
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
64
89
|
/** 写入单个文件 */
|
|
@@ -76,6 +101,8 @@ export interface ExecOptions {
|
|
|
76
101
|
timeout?: number;
|
|
77
102
|
/** 工作目录 */
|
|
78
103
|
cwd?: string;
|
|
104
|
+
/** 以 root 身份执行(仅在配置了 user 时有意义)。默认: false */
|
|
105
|
+
asRoot?: boolean;
|
|
79
106
|
}
|
|
80
107
|
export interface ExecResult {
|
|
81
108
|
stdout: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB,wBAAwB;IACxB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAAA;IAE9C,eAAe;IACf,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAE9C,2CAA2C;IAC3C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEjC,eAAe;IACf,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;IAEjD,uBAAuB;IACvB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|