@ct-agents/worker 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/index.ts +399 -376
- package/src/leased-item-runner.ts +85 -0
- package/src/platform-api-path.ts +5 -0
- package/src/resources/database/postgres.ts +36 -37
- package/src/resources/platform-proxy.ts +6 -33
- package/src/resources/text-generation/index.ts +33 -2
- package/src/resources/web-search/tavily.ts +2 -1
- package/src/sandbox/docker.ts +7 -78
- package/src/sandbox/index.ts +1 -0
- package/src/sandbox/local-process.ts +1 -6
- package/src/sandbox/manager.ts +1 -7
- package/src/sandbox/resources-def.ts +2 -20
- package/src/sandbox/resources.ts +1 -13
- package/src/sandbox/types.ts +124 -0
- package/src/worker-http-transport.ts +79 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS = 60_000;
|
|
2
|
+
|
|
3
|
+
export type LeasedItemRenewal = {
|
|
4
|
+
intervalMs?: number;
|
|
5
|
+
renew(): Promise<boolean>;
|
|
6
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RunLeasedItemInput<TExecution> = {
|
|
10
|
+
renewal?: LeasedItemRenewal;
|
|
11
|
+
execute(): Promise<TExecution>;
|
|
12
|
+
complete(execution: TExecution): Promise<boolean>;
|
|
13
|
+
completeRejectedError(execution: TExecution): Error;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function normalizeRenewInterval(value: number | undefined) {
|
|
17
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0
|
|
18
|
+
? value
|
|
19
|
+
: DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function delay(ms: number, signal: AbortSignal): Promise<void> {
|
|
23
|
+
if (signal.aborted) {
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const finish = () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
signal.removeEventListener('abort', finish);
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
const timer = setTimeout(finish, ms);
|
|
33
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
34
|
+
if (signal.aborted) {
|
|
35
|
+
finish();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function reportRenewError(
|
|
41
|
+
onError: LeasedItemRenewal['onError'],
|
|
42
|
+
error: unknown,
|
|
43
|
+
) {
|
|
44
|
+
try {
|
|
45
|
+
await onError?.(error);
|
|
46
|
+
} catch {
|
|
47
|
+
// 观测回调不能中断仍持有 lease 的业务执行;下一周期继续尝试 renew。
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function renewUntilStopped(renewal: LeasedItemRenewal, signal: AbortSignal) {
|
|
52
|
+
const intervalMs = normalizeRenewInterval(renewal.intervalMs);
|
|
53
|
+
while (!signal.aborted) {
|
|
54
|
+
await delay(intervalMs, signal);
|
|
55
|
+
if (signal.aborted) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
if (!await renewal.renew()) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
await reportRenewError(renewal.onError, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 组合单个 leased item 的续租、执行、完成和续租协程收口。 */
|
|
69
|
+
export async function runLeasedItem<TExecution>(
|
|
70
|
+
input: RunLeasedItemInput<TExecution>,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const renewController = new AbortController();
|
|
73
|
+
const renewal = input.renewal
|
|
74
|
+
? renewUntilStopped(input.renewal, renewController.signal)
|
|
75
|
+
: Promise.resolve();
|
|
76
|
+
try {
|
|
77
|
+
const execution = await input.execute();
|
|
78
|
+
if (!await input.complete(execution)) {
|
|
79
|
+
throw input.completeRejectedError(execution);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
renewController.abort();
|
|
83
|
+
await renewal;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
ResourceDefinition,
|
|
5
|
-
} from '@ct-agents/protocol';
|
|
2
|
+
import type { Database } from '@ct-agents/protocol';
|
|
3
|
+
import type { EnvironmentWorkerResourceImplementation } from '../../index.js';
|
|
6
4
|
|
|
7
5
|
export type PostgresQueryResult = {
|
|
8
6
|
rows: unknown[];
|
|
@@ -38,46 +36,37 @@ export type PostgresDatabaseOptions = {
|
|
|
38
36
|
maxRows?: number;
|
|
39
37
|
};
|
|
40
38
|
|
|
41
|
-
export type
|
|
39
|
+
export type CreatePostgresDatabaseResourceImplementationInput = {
|
|
42
40
|
createConnection: PostgresConnectionFactory;
|
|
43
41
|
};
|
|
44
42
|
|
|
45
43
|
export const postgresDatabaseOptionsSchema = z.object({
|
|
46
|
-
connectionString: z.string().trim().min(1, 'connectionString 为必填字段'),
|
|
44
|
+
connectionString: z.string().trim().min(1, 'connectionString 为必填字段').meta({ secret: true }),
|
|
47
45
|
allowWrite: z.boolean().optional(),
|
|
48
46
|
maxRows: z.number().int().positive().optional(),
|
|
49
47
|
}).strict();
|
|
50
48
|
|
|
51
|
-
export function
|
|
52
|
-
input:
|
|
53
|
-
):
|
|
49
|
+
export function createPostgresDatabaseResourceImplementation(
|
|
50
|
+
input: CreatePostgresDatabaseResourceImplementationInput,
|
|
51
|
+
): EnvironmentWorkerResourceImplementation<Database, PostgresDatabaseOptions> {
|
|
54
52
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error('PostgreSQL database resource 需要提供 withConnection,不能使用无状态 pool.query');
|
|
73
|
-
}
|
|
74
|
-
return createPostgresDatabase({
|
|
75
|
-
...parsed,
|
|
76
|
-
connection,
|
|
77
|
-
});
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
],
|
|
53
|
+
title: 'PostgreSQL',
|
|
54
|
+
description: '通过 worker 本地注入的 PostgreSQL 查询函数访问数据库。',
|
|
55
|
+
options: postgresDatabaseOptionsSchema,
|
|
56
|
+
factory: (options) => {
|
|
57
|
+
const parsed = postgresDatabaseOptionsSchema.parse(options);
|
|
58
|
+
const connection = input.createConnection(parsed.connectionString, {
|
|
59
|
+
allowWrite: parsed.allowWrite ?? false,
|
|
60
|
+
maxRows: parsed.maxRows,
|
|
61
|
+
});
|
|
62
|
+
if (!connection.withConnection) {
|
|
63
|
+
throw new Error('PostgreSQL database resource 需要提供 withConnection,不能使用无状态 pool.query');
|
|
64
|
+
}
|
|
65
|
+
return createPostgresDatabase({
|
|
66
|
+
...parsed,
|
|
67
|
+
connection,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
81
70
|
};
|
|
82
71
|
}
|
|
83
72
|
|
|
@@ -95,6 +84,10 @@ export function createPostgresDatabase(input: PostgresDatabaseOptions & {
|
|
|
95
84
|
query: async (sql, params, options) => {
|
|
96
85
|
const query = input.connection?.query ?? input.query;
|
|
97
86
|
const withConnection = input.connection?.withConnection ?? input.withConnection;
|
|
87
|
+
const maxRows = resolveMaxRows(input.maxRows, options?.maxRows);
|
|
88
|
+
const effectiveOptions = options || maxRows !== undefined
|
|
89
|
+
? { ...options, ...(maxRows !== undefined ? { maxRows } : {}) }
|
|
90
|
+
: undefined;
|
|
98
91
|
if (!query) {
|
|
99
92
|
throw new Error('PostgreSQL database resource 缺少 query 实现');
|
|
100
93
|
}
|
|
@@ -102,17 +95,23 @@ export function createPostgresDatabase(input: PostgresDatabaseOptions & {
|
|
|
102
95
|
if (input.allowWrite !== true) {
|
|
103
96
|
throw new Error('当前 database resource 未允许写 SQL');
|
|
104
97
|
}
|
|
105
|
-
return query(sql, params,
|
|
98
|
+
return query(sql, params, effectiveOptions);
|
|
106
99
|
}
|
|
107
100
|
|
|
108
101
|
if (!withConnection) {
|
|
109
102
|
throw new Error('readOnly 查询需要同连接 withConnection 实现,不能使用无状态 pool.query');
|
|
110
103
|
}
|
|
111
|
-
return withConnection(async (connection) => executeReadOnlyQuery(connection, sql, params,
|
|
104
|
+
return withConnection(async (connection) => executeReadOnlyQuery(connection, sql, params, maxRows));
|
|
112
105
|
},
|
|
113
106
|
};
|
|
114
107
|
}
|
|
115
108
|
|
|
109
|
+
function resolveMaxRows(configured: number | undefined, requested: number | undefined) {
|
|
110
|
+
if (configured === undefined) return requested;
|
|
111
|
+
if (requested === undefined) return configured;
|
|
112
|
+
return Math.min(configured, requested);
|
|
113
|
+
}
|
|
114
|
+
|
|
116
115
|
async function executeReadOnlyQuery(
|
|
117
116
|
connection: PostgresConnectionExecutor,
|
|
118
117
|
sql: string,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { platformApiPath } from '../platform-api-path.js';
|
|
2
2
|
|
|
3
3
|
export type PlatformResourceProxyInput = {
|
|
4
4
|
baseUrl: string;
|
|
@@ -17,10 +17,6 @@ export type PlatformResourceProxyInput = {
|
|
|
17
17
|
fetchImpl?: typeof fetch;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export type PlatformDatabaseProxyResourceInput = Omit<PlatformResourceProxyInput, 'slot'> & {
|
|
21
|
-
slot?: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
20
|
export type InvokePlatformResourceProxyInput = PlatformResourceProxyInput & {
|
|
25
21
|
method: string;
|
|
26
22
|
args?: unknown[] | Record<string, unknown>;
|
|
@@ -92,7 +88,10 @@ export async function invokePlatformResourceProxy(input: InvokePlatformResourceP
|
|
|
92
88
|
const requestTimeoutMs = input.requestTimeoutMs ?? (input.timeoutMs ? input.timeoutMs + 5_000 : 35_000);
|
|
93
89
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
94
90
|
try {
|
|
95
|
-
const
|
|
91
|
+
const environmentId = encodeURIComponent(input.environmentId);
|
|
92
|
+
const slot = encodeURIComponent(input.slot);
|
|
93
|
+
const path = platformApiPath(`/environments/${environmentId}/resources/${slot}/invoke`);
|
|
94
|
+
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
96
95
|
method: 'POST',
|
|
97
96
|
headers,
|
|
98
97
|
body: JSON.stringify(body),
|
|
@@ -101,7 +100,7 @@ export async function invokePlatformResourceProxy(input: InvokePlatformResourceP
|
|
|
101
100
|
return await parseProxyEnvelope(response);
|
|
102
101
|
} catch (error) {
|
|
103
102
|
if (isAbortError(error)) {
|
|
104
|
-
throw new Error('平台 resource proxy 请求超时');
|
|
103
|
+
throw new Error('平台 resource proxy 请求超时', { cause: error });
|
|
105
104
|
}
|
|
106
105
|
throw error;
|
|
107
106
|
} finally {
|
|
@@ -138,32 +137,6 @@ export function createPlatformResourceProxy<TResource extends object>(
|
|
|
138
137
|
}) as TResource;
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
/**
|
|
142
|
-
* 创建通过平台 Resource proxy 访问的 database resource。
|
|
143
|
-
*
|
|
144
|
-
* worker 只持有 environment key,不持有平台内置 resource 的原始密钥。
|
|
145
|
-
*/
|
|
146
|
-
export function createPlatformDatabaseProxyResource(input: PlatformDatabaseProxyResourceInput): Database {
|
|
147
|
-
const slot = input.slot?.trim() || 'database';
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
async query(sql, params, options) {
|
|
151
|
-
const timeoutMs = options?.timeoutMs ?? input.timeoutMs;
|
|
152
|
-
return invokePlatformResourceProxy({
|
|
153
|
-
...input,
|
|
154
|
-
slot,
|
|
155
|
-
method: 'query',
|
|
156
|
-
args: {
|
|
157
|
-
sql,
|
|
158
|
-
...(params !== undefined ? { params: Array.from(params) } : {}),
|
|
159
|
-
...(options !== undefined ? { options } : {}),
|
|
160
|
-
},
|
|
161
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
162
|
-
});
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
140
|
function isAbortError(error: unknown) {
|
|
168
141
|
return error instanceof DOMException && error.name === 'AbortError';
|
|
169
142
|
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TextGenerationResource,
|
|
8
8
|
TextGenerationResult,
|
|
9
9
|
} from '@ct-agents/protocol';
|
|
10
|
+
import { ModelOutputError, ModelProviderError } from '@ct-agents/harness';
|
|
10
11
|
import { runAnthropic } from '@ct-agents/harness/llm/anthropic-provider.js';
|
|
11
12
|
import type {
|
|
12
13
|
NormalizedModelOutput,
|
|
@@ -18,6 +19,8 @@ import type { EnvironmentWorkerResourceImplementation } from '../../index.js';
|
|
|
18
19
|
|
|
19
20
|
const DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
|
|
20
21
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
22
|
+
const DEFAULT_PROVIDER = 'anthropic' as const;
|
|
23
|
+
const DEFAULT_MODEL = 'deepseek-v4-flash';
|
|
21
24
|
const MAX_PROMPT_BYTES = 512 * 1_024;
|
|
22
25
|
const MAX_SYSTEM_PROMPT_BYTES = 32 * 1_024;
|
|
23
26
|
const MAX_RESPONSE_BYTES = 1_024 * 1_024;
|
|
@@ -115,6 +118,28 @@ function classifyProviderError(error: unknown): {
|
|
|
115
118
|
message: string;
|
|
116
119
|
retryable: boolean;
|
|
117
120
|
} {
|
|
121
|
+
if (error instanceof ModelOutputError) {
|
|
122
|
+
const isCompletedEmpty = error.diagnostic.stage === 'response_completion'
|
|
123
|
+
&& [
|
|
124
|
+
'completed_empty',
|
|
125
|
+
'end_turn',
|
|
126
|
+
'stop_sequence',
|
|
127
|
+
'tool_use',
|
|
128
|
+
'refusal',
|
|
129
|
+
'missing',
|
|
130
|
+
].includes(error.diagnostic.completionReason);
|
|
131
|
+
return isCompletedEmpty
|
|
132
|
+
? {
|
|
133
|
+
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
134
|
+
message: '模型服务返回了空文本',
|
|
135
|
+
retryable: false,
|
|
136
|
+
}
|
|
137
|
+
: {
|
|
138
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
139
|
+
message: '模型服务返回了非预期错误',
|
|
140
|
+
retryable: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
118
143
|
const status = readHttpStatus(error);
|
|
119
144
|
if (status === 400 || status === 404 || status === 422) {
|
|
120
145
|
return {
|
|
@@ -147,7 +172,7 @@ function classifyProviderError(error: unknown): {
|
|
|
147
172
|
return {
|
|
148
173
|
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
149
174
|
message: '模型服务返回了非预期错误',
|
|
150
|
-
retryable: status === undefined,
|
|
175
|
+
retryable: status === undefined && !(error instanceof ModelProviderError),
|
|
151
176
|
};
|
|
152
177
|
}
|
|
153
178
|
|
|
@@ -350,7 +375,7 @@ export function createTextGenerationResourceImplementation(
|
|
|
350
375
|
|
|
351
376
|
export function createTextGenerationResourceDefinition(
|
|
352
377
|
dependencies: TextGenerationResourceDependencies = {},
|
|
353
|
-
): ResourceDefinition<TextGenerationResource
|
|
378
|
+
): ResourceDefinition<TextGenerationResource> {
|
|
354
379
|
return {
|
|
355
380
|
id: 'textGeneration',
|
|
356
381
|
title: '模型文本生成',
|
|
@@ -360,6 +385,12 @@ export function createTextGenerationResourceDefinition(
|
|
|
360
385
|
title: '模型 Provider API',
|
|
361
386
|
description: '通过 OpenAI 或 Anthropic 兼容 API 生成文本。',
|
|
362
387
|
supportedExecutionModes: ['hosted', 'self_hosted'],
|
|
388
|
+
defaultOptions: {
|
|
389
|
+
provider: DEFAULT_PROVIDER,
|
|
390
|
+
defaultModel: DEFAULT_MODEL,
|
|
391
|
+
maxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
392
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
393
|
+
},
|
|
363
394
|
optionsSchema: textGenerationOptionsSchema,
|
|
364
395
|
optionsJsonSchema: z.toJSONSchema(textGenerationOptionsSchema),
|
|
365
396
|
factory: (options) => createTextGenerationResource(
|
|
@@ -391,7 +391,7 @@ export function createTavilyWebSearchResourceImplementation(
|
|
|
391
391
|
|
|
392
392
|
export function createTavilyWebSearchResourceDefinition(
|
|
393
393
|
dependencies: TavilyWebSearchDependencies = {},
|
|
394
|
-
): ResourceDefinition<WebSearchResource
|
|
394
|
+
): ResourceDefinition<WebSearchResource> {
|
|
395
395
|
return {
|
|
396
396
|
id: 'webSearch',
|
|
397
397
|
title: '互联网搜索',
|
|
@@ -401,6 +401,7 @@ export function createTavilyWebSearchResourceDefinition(
|
|
|
401
401
|
title: 'Tavily',
|
|
402
402
|
description: '通过 Tavily 官方 Search API 搜索公开互联网内容。',
|
|
403
403
|
supportedExecutionModes: ['hosted', 'self_hosted'],
|
|
404
|
+
defaultOptions: { timeoutMs: 30_000 },
|
|
404
405
|
optionsSchema: tavilyWebSearchOptionsSchema,
|
|
405
406
|
optionsJsonSchema: z.toJSONSchema(tavilyWebSearchOptionsSchema),
|
|
406
407
|
factory: (options) => createTavilyWebSearchResource(tavilyWebSearchOptionsSchema.parse(options), dependencies),
|
package/src/sandbox/docker.ts
CHANGED
|
@@ -6,84 +6,13 @@ import type {
|
|
|
6
6
|
Sandbox,
|
|
7
7
|
SandboxHandle,
|
|
8
8
|
} from '@ct-agents/protocol';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type DockerExec = {
|
|
18
|
-
start(options?: { hijack?: boolean; stdin?: boolean }): Promise<DockerReadableStream>;
|
|
19
|
-
inspect(): Promise<{ ExitCode?: number | null; Running?: boolean }>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type DockerReadableStream = NodeJS.ReadableStream & {
|
|
23
|
-
destroy?: () => void;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type DockerClient = {
|
|
27
|
-
createContainer(options: DockerCreateContainerOptions): Promise<DockerContainer>;
|
|
28
|
-
listContainers?(options?: { all?: boolean; filters?: Record<string, string[]> }): Promise<Array<{ Id?: string; id?: string }>>;
|
|
29
|
-
getContainer?(id: string): Pick<DockerContainer, 'remove'>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
type DockerExecCreateOptions = {
|
|
33
|
-
Cmd: string[];
|
|
34
|
-
AttachStdout: boolean;
|
|
35
|
-
AttachStderr: boolean;
|
|
36
|
-
Tty: boolean;
|
|
37
|
-
User?: string;
|
|
38
|
-
WorkingDir?: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type DockerCreateContainerOptions = {
|
|
42
|
-
Image: string;
|
|
43
|
-
name?: string;
|
|
44
|
-
User: string;
|
|
45
|
-
WorkingDir: string;
|
|
46
|
-
Cmd: string[];
|
|
47
|
-
Tty: boolean;
|
|
48
|
-
OpenStdin: boolean;
|
|
49
|
-
NetworkDisabled: boolean;
|
|
50
|
-
Labels: Record<string, string>;
|
|
51
|
-
HostConfig: {
|
|
52
|
-
Runtime?: string;
|
|
53
|
-
CapDrop: string[];
|
|
54
|
-
SecurityOpt: string[];
|
|
55
|
-
ReadonlyRootfs: boolean;
|
|
56
|
-
NetworkMode: 'none';
|
|
57
|
-
AutoRemove: boolean;
|
|
58
|
-
CpuQuota?: number;
|
|
59
|
-
Memory?: number;
|
|
60
|
-
PidsLimit?: number;
|
|
61
|
-
Tmpfs: Record<string, string>;
|
|
62
|
-
};
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export type DockerSandboxRuntime = 'runsc' | 'runc';
|
|
66
|
-
|
|
67
|
-
export type DockerSandboxOptions = {
|
|
68
|
-
docker: DockerClient;
|
|
69
|
-
image: string;
|
|
70
|
-
runtime?: DockerSandboxRuntime;
|
|
71
|
-
ownerId?: string;
|
|
72
|
-
nodeEnv?: string;
|
|
73
|
-
user?: string;
|
|
74
|
-
workspaceDir?: string;
|
|
75
|
-
tmpSize?: string;
|
|
76
|
-
workspaceSize?: string;
|
|
77
|
-
cpuQuota?: number;
|
|
78
|
-
memoryBytes?: number;
|
|
79
|
-
pidsLimit?: number;
|
|
80
|
-
defaultMaxOutputBytes?: number;
|
|
81
|
-
defaultFileTimeoutMs?: number;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export type CreateDockerSandboxOptions = Omit<DockerSandboxOptions, 'runtime'> & {
|
|
85
|
-
useGvisor?: boolean;
|
|
86
|
-
};
|
|
9
|
+
import type {
|
|
10
|
+
CreateDockerSandboxOptions,
|
|
11
|
+
DockerClient,
|
|
12
|
+
DockerContainer,
|
|
13
|
+
DockerSandboxOptions,
|
|
14
|
+
DockerSandboxRuntime,
|
|
15
|
+
} from './types.js';
|
|
87
16
|
|
|
88
17
|
export class DockerSandbox implements Sandbox {
|
|
89
18
|
readonly kind = 'docker' as const;
|
package/src/sandbox/index.ts
CHANGED
|
@@ -8,12 +8,7 @@ import type {
|
|
|
8
8
|
Sandbox,
|
|
9
9
|
SandboxHandle,
|
|
10
10
|
} from '@ct-agents/protocol';
|
|
11
|
-
|
|
12
|
-
export type LocalProcessSandboxOptions = {
|
|
13
|
-
rootDir?: string;
|
|
14
|
-
nodeEnv?: string;
|
|
15
|
-
defaultMaxOutputBytes?: number;
|
|
16
|
-
};
|
|
11
|
+
import type { LocalProcessSandboxOptions } from './types.js';
|
|
17
12
|
|
|
18
13
|
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
19
14
|
|
package/src/sandbox/manager.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
Sandbox,
|
|
3
2
|
SandboxHandle,
|
|
4
3
|
} from '@ct-agents/protocol';
|
|
5
|
-
|
|
6
|
-
export type SandboxManagerOptions = {
|
|
7
|
-
sandbox: Sandbox & { reapOrphanedContainers?: () => Promise<number> };
|
|
8
|
-
now?: () => number;
|
|
9
|
-
releaseTimeoutMs?: number;
|
|
10
|
-
};
|
|
4
|
+
import type { SandboxManagerOptions } from './types.js';
|
|
11
5
|
|
|
12
6
|
type SandboxEntry = {
|
|
13
7
|
handlePromise: Promise<SandboxHandle>;
|
|
@@ -3,25 +3,6 @@ import type {
|
|
|
3
3
|
ResourceDefinition,
|
|
4
4
|
Sandbox,
|
|
5
5
|
} from '@ct-agents/protocol';
|
|
6
|
-
|
|
7
|
-
export type SandboxLifecycle = 'session' | 'work-item';
|
|
8
|
-
|
|
9
|
-
export type SandboxResourceOptions = {
|
|
10
|
-
allowedCommands?: string[];
|
|
11
|
-
deniedCommands?: string[];
|
|
12
|
-
requireApproval?: boolean;
|
|
13
|
-
timeoutMs?: number;
|
|
14
|
-
sandboxLifecycle?: SandboxLifecycle;
|
|
15
|
-
workspaceRoot?: string;
|
|
16
|
-
readPaths?: string[];
|
|
17
|
-
writePaths?: string[];
|
|
18
|
-
limits?: {
|
|
19
|
-
cpu?: number;
|
|
20
|
-
memoryMb?: number;
|
|
21
|
-
processes?: number;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
6
|
export const sandboxResourceOptionsSchema = z.object({
|
|
26
7
|
allowedCommands: z.array(z.string().trim().min(1)).optional(),
|
|
27
8
|
deniedCommands: z.array(z.string().trim().min(1)).optional(),
|
|
@@ -38,7 +19,7 @@ export const sandboxResourceOptionsSchema = z.object({
|
|
|
38
19
|
}).strict().optional(),
|
|
39
20
|
}).strict();
|
|
40
21
|
|
|
41
|
-
export function createSandboxResourceDefinition(): ResourceDefinition<Sandbox
|
|
22
|
+
export function createSandboxResourceDefinition(): ResourceDefinition<Sandbox> {
|
|
42
23
|
return {
|
|
43
24
|
id: 'sandbox',
|
|
44
25
|
title: 'Sandbox',
|
|
@@ -48,6 +29,7 @@ export function createSandboxResourceDefinition(): ResourceDefinition<Sandbox, S
|
|
|
48
29
|
id: 'local-process',
|
|
49
30
|
title: 'Local process',
|
|
50
31
|
description: '本地开发用 sandbox 声明;生产应替换为隔离实现。',
|
|
32
|
+
defaultOptions: {},
|
|
51
33
|
optionsSchema: sandboxResourceOptionsSchema,
|
|
52
34
|
optionsJsonSchema: z.toJSONSchema(sandboxResourceOptionsSchema),
|
|
53
35
|
factory: () => {
|
package/src/sandbox/resources.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
Database,
|
|
3
|
-
MemoryResource,
|
|
4
2
|
ResourceSlots,
|
|
5
|
-
SandboxHandle,
|
|
6
|
-
SkillResource,
|
|
7
3
|
ToolWorkItem,
|
|
8
4
|
} from '@ct-agents/protocol';
|
|
9
5
|
import type { ResolveResources } from '../index.js';
|
|
10
|
-
|
|
11
|
-
export type CreateSandboxResolveResourcesInput = {
|
|
12
|
-
manager: {
|
|
13
|
-
forSession(input: { sessionId: string }): Promise<SandboxHandle>;
|
|
14
|
-
};
|
|
15
|
-
database?: Database;
|
|
16
|
-
skills?: SkillResource;
|
|
17
|
-
memory?: MemoryResource;
|
|
18
|
-
};
|
|
6
|
+
import type { CreateSandboxResolveResourcesInput } from './types.js';
|
|
19
7
|
|
|
20
8
|
export function createSandboxResolveResources(input: CreateSandboxResolveResourcesInput): ResolveResources {
|
|
21
9
|
return async (item: ToolWorkItem): Promise<ResourceSlots> => {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Database,
|
|
3
|
+
MemoryResource,
|
|
4
|
+
Sandbox,
|
|
5
|
+
SandboxHandle,
|
|
6
|
+
SkillResource,
|
|
7
|
+
} from '@ct-agents/protocol';
|
|
8
|
+
|
|
9
|
+
export type SandboxManagerOptions = {
|
|
10
|
+
sandbox: Sandbox & { reapOrphanedContainers?: () => Promise<number> };
|
|
11
|
+
now?: () => number;
|
|
12
|
+
releaseTimeoutMs?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type LocalProcessSandboxOptions = {
|
|
16
|
+
rootDir?: string;
|
|
17
|
+
nodeEnv?: string;
|
|
18
|
+
defaultMaxOutputBytes?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type DockerContainer = {
|
|
22
|
+
id?: string;
|
|
23
|
+
start(): Promise<unknown>;
|
|
24
|
+
exec(options: DockerExecCreateOptions): Promise<DockerExec>;
|
|
25
|
+
remove(options?: { force?: boolean; v?: boolean }): Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type DockerExec = {
|
|
29
|
+
start(options?: { hijack?: boolean; stdin?: boolean }): Promise<DockerReadableStream>;
|
|
30
|
+
inspect(): Promise<{ ExitCode?: number | null; Running?: boolean }>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type DockerReadableStream = NodeJS.ReadableStream & {
|
|
34
|
+
destroy?: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type DockerClient = {
|
|
38
|
+
createContainer(options: DockerCreateContainerOptions): Promise<DockerContainer>;
|
|
39
|
+
listContainers?(options?: { all?: boolean; filters?: Record<string, string[]> }): Promise<Array<{ Id?: string; id?: string }>>;
|
|
40
|
+
getContainer?(id: string): Pick<DockerContainer, 'remove'>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type DockerExecCreateOptions = {
|
|
44
|
+
Cmd: string[];
|
|
45
|
+
AttachStdout: boolean;
|
|
46
|
+
AttachStderr: boolean;
|
|
47
|
+
Tty: boolean;
|
|
48
|
+
User?: string;
|
|
49
|
+
WorkingDir?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DockerCreateContainerOptions = {
|
|
53
|
+
Image: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
User: string;
|
|
56
|
+
WorkingDir: string;
|
|
57
|
+
Cmd: string[];
|
|
58
|
+
Tty: boolean;
|
|
59
|
+
OpenStdin: boolean;
|
|
60
|
+
NetworkDisabled: boolean;
|
|
61
|
+
Labels: Record<string, string>;
|
|
62
|
+
HostConfig: {
|
|
63
|
+
Runtime?: string;
|
|
64
|
+
CapDrop: string[];
|
|
65
|
+
SecurityOpt: string[];
|
|
66
|
+
ReadonlyRootfs: boolean;
|
|
67
|
+
NetworkMode: 'none';
|
|
68
|
+
AutoRemove: boolean;
|
|
69
|
+
CpuQuota?: number;
|
|
70
|
+
Memory?: number;
|
|
71
|
+
PidsLimit?: number;
|
|
72
|
+
Tmpfs: Record<string, string>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type DockerSandboxRuntime = 'runsc' | 'runc';
|
|
77
|
+
|
|
78
|
+
export type DockerSandboxOptions = {
|
|
79
|
+
docker: DockerClient;
|
|
80
|
+
image: string;
|
|
81
|
+
runtime?: DockerSandboxRuntime;
|
|
82
|
+
ownerId?: string;
|
|
83
|
+
nodeEnv?: string;
|
|
84
|
+
user?: string;
|
|
85
|
+
workspaceDir?: string;
|
|
86
|
+
tmpSize?: string;
|
|
87
|
+
workspaceSize?: string;
|
|
88
|
+
cpuQuota?: number;
|
|
89
|
+
memoryBytes?: number;
|
|
90
|
+
pidsLimit?: number;
|
|
91
|
+
defaultMaxOutputBytes?: number;
|
|
92
|
+
defaultFileTimeoutMs?: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type CreateDockerSandboxOptions = Omit<DockerSandboxOptions, 'runtime'> & {
|
|
96
|
+
useGvisor?: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type SandboxLifecycle = 'session' | 'work-item';
|
|
100
|
+
|
|
101
|
+
export type SandboxResourceOptions = {
|
|
102
|
+
allowedCommands?: string[];
|
|
103
|
+
deniedCommands?: string[];
|
|
104
|
+
requireApproval?: boolean;
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
sandboxLifecycle?: SandboxLifecycle;
|
|
107
|
+
workspaceRoot?: string;
|
|
108
|
+
readPaths?: string[];
|
|
109
|
+
writePaths?: string[];
|
|
110
|
+
limits?: {
|
|
111
|
+
cpu?: number;
|
|
112
|
+
memoryMb?: number;
|
|
113
|
+
processes?: number;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type CreateSandboxResolveResourcesInput = {
|
|
118
|
+
manager: {
|
|
119
|
+
forSession(input: { sessionId: string }): Promise<SandboxHandle>;
|
|
120
|
+
};
|
|
121
|
+
database?: Database;
|
|
122
|
+
skills?: SkillResource;
|
|
123
|
+
memory?: MemoryResource;
|
|
124
|
+
};
|