@ct-agents/worker 0.1.8 → 0.2.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.
- package/package.json +7 -6
- package/src/index.ts +388 -377
- 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 +25 -2
- package/src/resources/web-search/tavily.ts +1 -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 +1 -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,
|
|
@@ -115,6 +116,28 @@ function classifyProviderError(error: unknown): {
|
|
|
115
116
|
message: string;
|
|
116
117
|
retryable: boolean;
|
|
117
118
|
} {
|
|
119
|
+
if (error instanceof ModelOutputError) {
|
|
120
|
+
const isCompletedEmpty = error.diagnostic.stage === 'response_completion'
|
|
121
|
+
&& [
|
|
122
|
+
'completed_empty',
|
|
123
|
+
'end_turn',
|
|
124
|
+
'stop_sequence',
|
|
125
|
+
'tool_use',
|
|
126
|
+
'refusal',
|
|
127
|
+
'missing',
|
|
128
|
+
].includes(error.diagnostic.completionReason);
|
|
129
|
+
return isCompletedEmpty
|
|
130
|
+
? {
|
|
131
|
+
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
132
|
+
message: '模型服务返回了空文本',
|
|
133
|
+
retryable: false,
|
|
134
|
+
}
|
|
135
|
+
: {
|
|
136
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
137
|
+
message: '模型服务返回了非预期错误',
|
|
138
|
+
retryable: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
118
141
|
const status = readHttpStatus(error);
|
|
119
142
|
if (status === 400 || status === 404 || status === 422) {
|
|
120
143
|
return {
|
|
@@ -147,7 +170,7 @@ function classifyProviderError(error: unknown): {
|
|
|
147
170
|
return {
|
|
148
171
|
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
149
172
|
message: '模型服务返回了非预期错误',
|
|
150
|
-
retryable: status === undefined,
|
|
173
|
+
retryable: status === undefined && !(error instanceof ModelProviderError),
|
|
151
174
|
};
|
|
152
175
|
}
|
|
153
176
|
|
|
@@ -350,7 +373,7 @@ export function createTextGenerationResourceImplementation(
|
|
|
350
373
|
|
|
351
374
|
export function createTextGenerationResourceDefinition(
|
|
352
375
|
dependencies: TextGenerationResourceDependencies = {},
|
|
353
|
-
): ResourceDefinition<TextGenerationResource
|
|
376
|
+
): ResourceDefinition<TextGenerationResource> {
|
|
354
377
|
return {
|
|
355
378
|
id: 'textGeneration',
|
|
356
379
|
title: '模型文本生成',
|
|
@@ -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: '互联网搜索',
|
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',
|
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
|
+
};
|