@agentuity/core 1.0.59 → 2.0.0-beta.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/dist/deprecation.d.ts +20 -0
- package/dist/deprecation.d.ts.map +1 -0
- package/dist/deprecation.js +102 -0
- package/dist/deprecation.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/services/oauth/flow.d.ts +0 -31
- package/dist/services/oauth/flow.d.ts.map +1 -1
- package/dist/services/oauth/flow.js +13 -138
- package/dist/services/oauth/flow.js.map +1 -1
- package/dist/services/oauth/index.d.ts +0 -1
- package/dist/services/oauth/index.d.ts.map +1 -1
- package/dist/services/oauth/index.js +0 -1
- package/dist/services/oauth/index.js.map +1 -1
- package/dist/services/oauth/types.d.ts +0 -11
- package/dist/services/oauth/types.d.ts.map +1 -1
- package/dist/services/oauth/types.js +0 -19
- package/dist/services/oauth/types.js.map +1 -1
- package/dist/services/sandbox/execute.d.ts.map +1 -1
- package/dist/services/sandbox/execute.js +11 -22
- package/dist/services/sandbox/execute.js.map +1 -1
- package/dist/services/sandbox/run.d.ts.map +1 -1
- package/dist/services/sandbox/run.js +30 -83
- package/dist/services/sandbox/run.js.map +1 -1
- package/dist/services/sandbox/types.d.ts +0 -8
- package/dist/services/sandbox/types.d.ts.map +1 -1
- package/dist/services/sandbox/types.js +0 -14
- package/dist/services/sandbox/types.js.map +1 -1
- package/package.json +2 -2
- package/src/deprecation.ts +120 -0
- package/src/index.ts +3 -0
- package/src/services/oauth/flow.ts +15 -156
- package/src/services/oauth/index.ts +0 -1
- package/src/services/oauth/types.ts +0 -26
- package/src/services/sandbox/execute.ts +12 -26
- package/src/services/sandbox/run.ts +34 -129
- package/src/services/sandbox/types.ts +0 -14
- package/dist/services/oauth/token-storage.d.ts +0 -109
- package/dist/services/oauth/token-storage.d.ts.map +0 -1
- package/dist/services/oauth/token-storage.js +0 -140
- package/dist/services/oauth/token-storage.js.map +0 -1
- package/src/services/oauth/token-storage.ts +0 -220
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecation warning for v1 SDK packages.
|
|
3
|
+
*
|
|
4
|
+
* This module logs a deprecation warning when v1 packages are used,
|
|
5
|
+
* recommending migration to v2.
|
|
6
|
+
*
|
|
7
|
+
* The warning is only shown once per process to avoid noise.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Logger } from './logger';
|
|
11
|
+
|
|
12
|
+
let deprecationWarningShown = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Package version info
|
|
16
|
+
*/
|
|
17
|
+
interface PackageVersion {
|
|
18
|
+
version: string;
|
|
19
|
+
major: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get version from package.json using various strategies.
|
|
24
|
+
*/
|
|
25
|
+
function getPackageVersion(): PackageVersion | null {
|
|
26
|
+
try {
|
|
27
|
+
// Try to read from the bundled package.json
|
|
28
|
+
// When bundled by Bun/Vite, import.meta.resolve can find the package.json
|
|
29
|
+
const pkgUrl = import.meta.resolve('@agentuity/core/package.json');
|
|
30
|
+
const pkg = require(pkgUrl);
|
|
31
|
+
if (pkg?.version) {
|
|
32
|
+
const match = pkg.version.match(/^(\d+)\.\d+\.\d+/);
|
|
33
|
+
return {
|
|
34
|
+
version: pkg.version,
|
|
35
|
+
major: match ? parseInt(match[1], 10) : 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Try require fallback
|
|
40
|
+
try {
|
|
41
|
+
const pkg = require('@agentuity/core/package.json');
|
|
42
|
+
if (pkg?.version) {
|
|
43
|
+
const match = pkg.version.match(/^(\d+)\.\d+\.\d+/);
|
|
44
|
+
return {
|
|
45
|
+
version: pkg.version,
|
|
46
|
+
major: match ? parseInt(match[1], 10) : 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Show deprecation warning for v1 packages.
|
|
58
|
+
*
|
|
59
|
+
* @param logger - Optional logger instance (falls back to console)
|
|
60
|
+
*/
|
|
61
|
+
export function showDeprecationWarning(logger?: Logger): void {
|
|
62
|
+
// Only show once per process
|
|
63
|
+
if (deprecationWarningShown) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Skip if explicitly disabled
|
|
68
|
+
if (process.env.AGENTUITY_NO_DEPRECATION_WARNING === 'true') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Skip in test environments
|
|
73
|
+
if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pkgVersion = getPackageVersion();
|
|
78
|
+
|
|
79
|
+
// Only warn if this is a v1 package
|
|
80
|
+
if (!pkgVersion || pkgVersion.major !== 1) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
deprecationWarningShown = true;
|
|
85
|
+
|
|
86
|
+
const message =
|
|
87
|
+
'\n' +
|
|
88
|
+
'┌──────────────────────────────────────────────────────────────────────┐\n' +
|
|
89
|
+
'│ │\n' +
|
|
90
|
+
'│ ⚠️ Agentuity SDK v1 is deprecated │\n' +
|
|
91
|
+
'│ │\n' +
|
|
92
|
+
'│ You are using @agentuity/core@' +
|
|
93
|
+
pkgVersion.version.padEnd(22) +
|
|
94
|
+
' │\n' +
|
|
95
|
+
'│ │\n' +
|
|
96
|
+
'│ v2 introduces major improvements: │\n' +
|
|
97
|
+
'│ • Hono RPC for end-to-end type safety │\n' +
|
|
98
|
+
'│ • Vite-native dev server with HMR │\n' +
|
|
99
|
+
'│ • Simplified configuration in createApp() │\n' +
|
|
100
|
+
'│ │\n' +
|
|
101
|
+
'│ → Run `npx @agentuity/migrate` to upgrade your project │\n' +
|
|
102
|
+
'│ │\n' +
|
|
103
|
+
'│ Docs: https://docs.agentuity.com/migration/v1-to-v2 │\n' +
|
|
104
|
+
'│ │\n' +
|
|
105
|
+
'└──────────────────────────────────────────────────────────────────────┘\n';
|
|
106
|
+
|
|
107
|
+
if (logger) {
|
|
108
|
+
logger.warn(message);
|
|
109
|
+
} else {
|
|
110
|
+
console.warn(message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if the current package is v1.
|
|
116
|
+
*/
|
|
117
|
+
export function isV1Package(): boolean {
|
|
118
|
+
const pkgVersion = getPackageVersion();
|
|
119
|
+
return pkgVersion?.major === 1;
|
|
120
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export type { EnvField, ResourceType } from './env-example.ts';
|
|
3
3
|
export { detectResourceFromKey, parseEnvExample } from './env-example.ts';
|
|
4
4
|
|
|
5
|
+
// deprecation.ts exports
|
|
6
|
+
export { isV1Package, showDeprecationWarning } from './deprecation';
|
|
7
|
+
|
|
5
8
|
// error.ts exports
|
|
6
9
|
export { isStructuredError, RichError, StructuredError } from './error.ts';
|
|
7
10
|
|
|
@@ -25,28 +25,11 @@ function resolveConfig(config?: OAuthFlowConfig) {
|
|
|
25
25
|
config?.userinfoUrl ??
|
|
26
26
|
getEnv('OAUTH_USERINFO_URL') ??
|
|
27
27
|
(issuer ? `${issuer}/userinfo` : undefined);
|
|
28
|
-
const revokeUrl =
|
|
29
|
-
config?.revokeUrl ?? getEnv('OAUTH_REVOKE_URL') ?? (issuer ? `${issuer}/revoke` : undefined);
|
|
30
|
-
const endSessionUrl =
|
|
31
|
-
config?.endSessionUrl ??
|
|
32
|
-
getEnv('OAUTH_END_SESSION_URL') ??
|
|
33
|
-
(issuer ? `${issuer}/end_session` : undefined);
|
|
34
28
|
const scopes = config?.scopes ?? getEnv('OAUTH_SCOPES') ?? 'openid profile email';
|
|
35
29
|
|
|
36
30
|
const prompt = config?.prompt;
|
|
37
31
|
|
|
38
|
-
return {
|
|
39
|
-
clientId,
|
|
40
|
-
clientSecret,
|
|
41
|
-
issuer,
|
|
42
|
-
authorizeUrl,
|
|
43
|
-
tokenUrl,
|
|
44
|
-
userinfoUrl,
|
|
45
|
-
revokeUrl,
|
|
46
|
-
endSessionUrl,
|
|
47
|
-
scopes,
|
|
48
|
-
prompt,
|
|
49
|
-
};
|
|
32
|
+
return { clientId, clientSecret, issuer, authorizeUrl, tokenUrl, userinfoUrl, scopes, prompt };
|
|
50
33
|
}
|
|
51
34
|
|
|
52
35
|
/**
|
|
@@ -135,6 +118,9 @@ export async function exchangeToken(
|
|
|
135
118
|
});
|
|
136
119
|
}
|
|
137
120
|
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
123
|
+
|
|
138
124
|
let response: Response;
|
|
139
125
|
try {
|
|
140
126
|
response = await fetch(resolved.tokenUrl, {
|
|
@@ -147,16 +133,18 @@ export async function exchangeToken(
|
|
|
147
133
|
client_id: resolved.clientId,
|
|
148
134
|
client_secret: resolved.clientSecret,
|
|
149
135
|
}),
|
|
150
|
-
signal:
|
|
136
|
+
signal: controller.signal,
|
|
151
137
|
});
|
|
152
138
|
} catch (err) {
|
|
153
|
-
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
154
141
|
throw new OAuthResponseError({
|
|
155
142
|
message: `Token exchange timed out after ${DEFAULT_TIMEOUT_MS}ms`,
|
|
156
143
|
});
|
|
157
144
|
}
|
|
158
145
|
throw err;
|
|
159
146
|
}
|
|
147
|
+
clearTimeout(timer);
|
|
160
148
|
|
|
161
149
|
if (!response.ok) {
|
|
162
150
|
const error = await response.text();
|
|
@@ -169,140 +157,6 @@ export async function exchangeToken(
|
|
|
169
157
|
return OAuthTokenResponseSchema.parse(data);
|
|
170
158
|
}
|
|
171
159
|
|
|
172
|
-
/**
|
|
173
|
-
* Refresh an access token using a refresh token.
|
|
174
|
-
*
|
|
175
|
-
* @param refreshTokenValue - The refresh token obtained from a previous token exchange
|
|
176
|
-
* @param config - Optional OAuth configuration. Falls back to environment variables.
|
|
177
|
-
* @returns The token response including a new access_token, and optionally a new refresh_token
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* ```typescript
|
|
181
|
-
* const newToken = await refreshToken(previousToken.refresh_token!);
|
|
182
|
-
* console.log(newToken.access_token);
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
export async function refreshToken(
|
|
186
|
-
refreshTokenValue: string,
|
|
187
|
-
config?: OAuthFlowConfig
|
|
188
|
-
): Promise<OAuthTokenResponse> {
|
|
189
|
-
const resolved = resolveConfig(config);
|
|
190
|
-
|
|
191
|
-
if (!resolved.tokenUrl) {
|
|
192
|
-
throw new OAuthResponseError({
|
|
193
|
-
message:
|
|
194
|
-
'No token URL configured. Set OAUTH_TOKEN_URL or OAUTH_ISSUER environment variable.',
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
if (!resolved.clientId) {
|
|
198
|
-
throw new OAuthResponseError({
|
|
199
|
-
message: 'No client ID configured. Set OAUTH_CLIENT_ID environment variable.',
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const params = new URLSearchParams({
|
|
204
|
-
grant_type: 'refresh_token',
|
|
205
|
-
refresh_token: refreshTokenValue,
|
|
206
|
-
client_id: resolved.clientId,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
if (resolved.clientSecret) {
|
|
210
|
-
params.set('client_secret', resolved.clientSecret);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
let response: Response;
|
|
214
|
-
try {
|
|
215
|
-
response = await fetch(resolved.tokenUrl, {
|
|
216
|
-
method: 'POST',
|
|
217
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
218
|
-
body: params,
|
|
219
|
-
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
220
|
-
});
|
|
221
|
-
} catch (err) {
|
|
222
|
-
if (err instanceof DOMException && err.name === 'TimeoutError') {
|
|
223
|
-
throw new OAuthResponseError({
|
|
224
|
-
message: `Token refresh timed out after ${DEFAULT_TIMEOUT_MS}ms`,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
throw err;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!response.ok) {
|
|
231
|
-
const error = await response.text();
|
|
232
|
-
throw new OAuthResponseError({
|
|
233
|
-
message: `Token refresh failed (${response.status}): ${error}`,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const data = await response.json();
|
|
238
|
-
return OAuthTokenResponseSchema.parse(data);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Revoke an OAuth token (access token or refresh token) to log the user out.
|
|
243
|
-
*
|
|
244
|
-
* Calls the token revocation endpoint (RFC 7009). The server will invalidate
|
|
245
|
-
* the token so it can no longer be used. Per the spec, the endpoint returns
|
|
246
|
-
* a success response even if the token was already invalid.
|
|
247
|
-
*
|
|
248
|
-
* @param token - The access token or refresh token to revoke
|
|
249
|
-
* @param config - Optional OAuth configuration. Falls back to environment variables.
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* ```typescript
|
|
253
|
-
* // Revoke the refresh token to fully log out
|
|
254
|
-
* await logout(token.refresh_token!);
|
|
255
|
-
* ```
|
|
256
|
-
*/
|
|
257
|
-
export async function logout(token: string, config?: OAuthFlowConfig): Promise<void> {
|
|
258
|
-
const resolved = resolveConfig(config);
|
|
259
|
-
|
|
260
|
-
if (!resolved.revokeUrl) {
|
|
261
|
-
throw new OAuthResponseError({
|
|
262
|
-
message:
|
|
263
|
-
'No revoke URL configured. Set OAUTH_REVOKE_URL or OAUTH_ISSUER environment variable.',
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (!resolved.clientId) {
|
|
267
|
-
throw new OAuthResponseError({
|
|
268
|
-
message: 'No client ID configured. Set OAUTH_CLIENT_ID environment variable.',
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const params = new URLSearchParams({
|
|
273
|
-
token,
|
|
274
|
-
client_id: resolved.clientId,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
if (resolved.clientSecret) {
|
|
278
|
-
params.set('client_secret', resolved.clientSecret);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let response: Response;
|
|
282
|
-
try {
|
|
283
|
-
response = await fetch(resolved.revokeUrl, {
|
|
284
|
-
method: 'POST',
|
|
285
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
286
|
-
body: params,
|
|
287
|
-
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
288
|
-
});
|
|
289
|
-
} catch (err) {
|
|
290
|
-
if (err instanceof DOMException && err.name === 'TimeoutError') {
|
|
291
|
-
throw new OAuthResponseError({
|
|
292
|
-
message: `Token revocation timed out after ${DEFAULT_TIMEOUT_MS}ms`,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
throw err;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!response.ok) {
|
|
299
|
-
const error = await response.text();
|
|
300
|
-
throw new OAuthResponseError({
|
|
301
|
-
message: `Token revocation failed (${response.status}): ${error}`,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
160
|
/**
|
|
307
161
|
* Fetch user information from the OIDC userinfo endpoint using an access token.
|
|
308
162
|
*
|
|
@@ -329,20 +183,25 @@ export async function fetchUserInfo(
|
|
|
329
183
|
});
|
|
330
184
|
}
|
|
331
185
|
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
188
|
+
|
|
332
189
|
let response: Response;
|
|
333
190
|
try {
|
|
334
191
|
response = await fetch(resolved.userinfoUrl, {
|
|
335
192
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
336
|
-
signal:
|
|
193
|
+
signal: controller.signal,
|
|
337
194
|
});
|
|
338
195
|
} catch (err) {
|
|
339
|
-
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
340
198
|
throw new OAuthResponseError({
|
|
341
199
|
message: `Userinfo request timed out after ${DEFAULT_TIMEOUT_MS}ms`,
|
|
342
200
|
});
|
|
343
201
|
}
|
|
344
202
|
throw err;
|
|
345
203
|
}
|
|
204
|
+
clearTimeout(timer);
|
|
346
205
|
|
|
347
206
|
if (!response.ok) {
|
|
348
207
|
const error = await response.text();
|
|
@@ -268,18 +268,6 @@ export const OAuthFlowConfigSchema = z.object({
|
|
|
268
268
|
.string()
|
|
269
269
|
.optional()
|
|
270
270
|
.describe('UserInfo endpoint. Defaults to OAUTH_USERINFO_URL or {issuer}/userinfo'),
|
|
271
|
-
revokeUrl: z
|
|
272
|
-
.string()
|
|
273
|
-
.optional()
|
|
274
|
-
.describe(
|
|
275
|
-
'Token revocation endpoint (RFC 7009). Defaults to OAUTH_REVOKE_URL or {issuer}/revoke'
|
|
276
|
-
),
|
|
277
|
-
endSessionUrl: z
|
|
278
|
-
.string()
|
|
279
|
-
.optional()
|
|
280
|
-
.describe(
|
|
281
|
-
'OIDC end session endpoint. Defaults to OAUTH_END_SESSION_URL or {issuer}/end_session'
|
|
282
|
-
),
|
|
283
271
|
scopes: z
|
|
284
272
|
.string()
|
|
285
273
|
.optional()
|
|
@@ -317,17 +305,3 @@ export const OAuthUserInfoSchema = z
|
|
|
317
305
|
.catchall(z.unknown());
|
|
318
306
|
|
|
319
307
|
export type OAuthUserInfo = z.infer<typeof OAuthUserInfoSchema>;
|
|
320
|
-
|
|
321
|
-
export const StoredTokenSchema = z.object({
|
|
322
|
-
access_token: z.string(),
|
|
323
|
-
token_type: z.string().optional(),
|
|
324
|
-
refresh_token: z.string().optional(),
|
|
325
|
-
scope: z.string().optional(),
|
|
326
|
-
id_token: z.string().optional(),
|
|
327
|
-
expires_at: z
|
|
328
|
-
.number()
|
|
329
|
-
.optional()
|
|
330
|
-
.describe('Unix timestamp (seconds) when the access token expires'),
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
export type StoredToken = z.infer<typeof StoredTokenSchema>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExecuteOptions, Execution, ExecutionStatus } from './types.ts';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { APIClient } from '../api.ts';
|
|
4
|
-
import { SandboxBusyError,
|
|
4
|
+
import { SandboxBusyError, throwSandboxError } from './util.ts';
|
|
5
5
|
|
|
6
6
|
export const ExecuteRequestSchema = z
|
|
7
7
|
.object({
|
|
@@ -114,37 +114,23 @@ export async function sandboxExecute(
|
|
|
114
114
|
signal ?? options.signal
|
|
115
115
|
);
|
|
116
116
|
} catch (error: unknown) {
|
|
117
|
+
// Detect 409 Conflict (sandbox busy) and throw a specific error.
|
|
118
|
+
// The sandbox API client is configured with maxRetries: 0 to fail fast
|
|
119
|
+
// when sandbox is busy (retrying wouldn't help - sandbox is still busy).
|
|
120
|
+
// Convert APIErrorResponse with status 409 to SandboxBusyError for clarity.
|
|
117
121
|
if (
|
|
118
122
|
error &&
|
|
119
123
|
typeof error === 'object' &&
|
|
120
124
|
'_tag' in error &&
|
|
121
125
|
(error as { _tag: string })._tag === 'APIErrorResponse' &&
|
|
122
|
-
'status' in error
|
|
126
|
+
'status' in error &&
|
|
127
|
+
(error as { status: number }).status === 409
|
|
123
128
|
) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (status === 404) {
|
|
130
|
-
throw new SandboxNotFoundError({
|
|
131
|
-
message:
|
|
132
|
-
'Sandbox not found. If you just created this sandbox, it may still be initializing. The server should handle this automatically — if this persists, the sandbox may have been destroyed.',
|
|
133
|
-
sandboxId,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Detect 409 Conflict (sandbox busy) and throw a specific error.
|
|
138
|
-
// The sandbox API client is configured with maxRetries: 0 to fail fast
|
|
139
|
-
// when sandbox is busy (retrying wouldn't help - sandbox is still busy).
|
|
140
|
-
// Convert APIErrorResponse with status 409 to SandboxBusyError for clarity.
|
|
141
|
-
if (status === 409) {
|
|
142
|
-
throw new SandboxBusyError({
|
|
143
|
-
message:
|
|
144
|
-
'Sandbox is currently busy executing another command. Please wait for the current execution to complete before submitting a new one.',
|
|
145
|
-
sandboxId,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
129
|
+
throw new SandboxBusyError({
|
|
130
|
+
message:
|
|
131
|
+
'Sandbox is currently busy executing another command. Please wait for the current execution to complete before submitting a new one.',
|
|
132
|
+
sandboxId,
|
|
133
|
+
});
|
|
148
134
|
}
|
|
149
135
|
throw error;
|
|
150
136
|
}
|
|
@@ -224,103 +224,20 @@ export async function sandboxRun(
|
|
|
224
224
|
logger?.debug('streams completed, fetching final status');
|
|
225
225
|
|
|
226
226
|
// Stream EOF means the sandbox is done — hadron only closes streams after the
|
|
227
|
-
// container exits.
|
|
228
|
-
//
|
|
229
|
-
// stream completes.
|
|
230
|
-
//
|
|
231
|
-
// Hadron drains container logs for up to 5s after exit, then closes the
|
|
232
|
-
// stream, then sends the lifecycle event in a goroutine. So the exit code
|
|
233
|
-
// typically arrives at Catalyst 5–7s after the container exits. We use a
|
|
234
|
-
// linear 1s polling interval (not exponential backoff) so we don't overshoot
|
|
235
|
-
// the window — 15 attempts × 1s = 15s total, which comfortably covers the
|
|
236
|
-
// drain + lifecycle propagation delay.
|
|
237
|
-
// Abort-aware sleep that rejects when the caller's signal fires.
|
|
238
|
-
const abortAwareSleep = (ms: number): Promise<void> =>
|
|
239
|
-
new Promise((resolve, reject) => {
|
|
240
|
-
if (signal?.aborted) {
|
|
241
|
-
reject(new DOMException('Aborted', 'AbortError'));
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const timer = setTimeout(resolve, ms);
|
|
245
|
-
signal?.addEventListener(
|
|
246
|
-
'abort',
|
|
247
|
-
() => {
|
|
248
|
-
clearTimeout(timer);
|
|
249
|
-
reject(new DOMException('Aborted', 'AbortError'));
|
|
250
|
-
},
|
|
251
|
-
{ once: true }
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
|
|
227
|
+
// container exits. Fetch status once for the exit code; if lifecycle events
|
|
228
|
+
// haven't propagated to Catalyst yet, default to exit code 0.
|
|
255
229
|
let exitCode = 0;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
try {
|
|
264
|
-
const sandboxStatus = await sandboxGetStatus(client, { sandboxId, orgId });
|
|
265
|
-
if (sandboxStatus.exitCode != null) {
|
|
266
|
-
exitCode = sandboxStatus.exitCode;
|
|
267
|
-
logger?.debug(
|
|
268
|
-
'[run] exit code %d found on attempt %d/%d (+%dms)',
|
|
269
|
-
exitCode,
|
|
270
|
-
attempt + 1,
|
|
271
|
-
maxStatusRetries,
|
|
272
|
-
Date.now() - statusPollStart
|
|
273
|
-
);
|
|
274
|
-
break;
|
|
275
|
-
} else if (sandboxStatus.status === 'failed') {
|
|
276
|
-
exitCode = 1;
|
|
277
|
-
logger?.debug(
|
|
278
|
-
'[run] sandbox failed on attempt %d/%d (+%dms)',
|
|
279
|
-
attempt + 1,
|
|
280
|
-
maxStatusRetries,
|
|
281
|
-
Date.now() - statusPollStart
|
|
282
|
-
);
|
|
283
|
-
break;
|
|
284
|
-
} else if (sandboxStatus.status === 'terminated') {
|
|
285
|
-
// Sandbox was destroyed. If exit code is missing, the
|
|
286
|
-
// terminated event may have overwritten it. Stop polling —
|
|
287
|
-
// no further updates will come.
|
|
288
|
-
logger?.debug(
|
|
289
|
-
'[run] sandbox terminated without exit code on attempt %d/%d (+%dms)',
|
|
290
|
-
attempt + 1,
|
|
291
|
-
maxStatusRetries,
|
|
292
|
-
Date.now() - statusPollStart
|
|
293
|
-
);
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
// Exit code not yet propagated — wait before next poll.
|
|
297
|
-
if (attempt < maxStatusRetries - 1) {
|
|
298
|
-
await abortAwareSleep(statusPollInterval);
|
|
299
|
-
}
|
|
300
|
-
} catch (err) {
|
|
301
|
-
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
// Transient failure (sandbox briefly unavailable, network error).
|
|
305
|
-
// Retry instead of giving up — the lifecycle event may still arrive.
|
|
306
|
-
logger?.debug(
|
|
307
|
-
'[run] sandboxGetStatus attempt %d/%d failed (+%dms): %s',
|
|
308
|
-
attempt + 1,
|
|
309
|
-
maxStatusRetries,
|
|
310
|
-
Date.now() - statusPollStart,
|
|
311
|
-
err
|
|
312
|
-
);
|
|
313
|
-
if (attempt < maxStatusRetries - 1) {
|
|
314
|
-
await abortAwareSleep(statusPollInterval);
|
|
315
|
-
}
|
|
230
|
+
try {
|
|
231
|
+
const sandboxStatus = await sandboxGetStatus(client, { sandboxId, orgId });
|
|
232
|
+
if (sandboxStatus.exitCode != null) {
|
|
233
|
+
exitCode = sandboxStatus.exitCode;
|
|
234
|
+
} else if (sandboxStatus.status === 'failed') {
|
|
235
|
+
exitCode = 1;
|
|
316
236
|
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
maxStatusRetries,
|
|
322
|
-
Date.now() - statusPollStart
|
|
323
|
-
);
|
|
237
|
+
} catch {
|
|
238
|
+
// Sandbox may already be destroyed (fire-and-forget teardown).
|
|
239
|
+
// Stream EOF already confirmed execution completed.
|
|
240
|
+
logger?.debug('sandboxGetStatus failed after stream EOF, using default exit code 0');
|
|
324
241
|
}
|
|
325
242
|
|
|
326
243
|
if (timingLogsEnabled)
|
|
@@ -470,56 +387,44 @@ async function streamUrlToWritable(
|
|
|
470
387
|
writable: Writable,
|
|
471
388
|
signal: AbortSignal,
|
|
472
389
|
logger?: Logger,
|
|
473
|
-
|
|
390
|
+
started?: number
|
|
474
391
|
): Promise<void> {
|
|
475
|
-
const streamStart = Date.now();
|
|
476
392
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
'[stream] response status=%d in %dms',
|
|
485
|
-
response.status,
|
|
486
|
-
Date.now() - streamStart
|
|
487
|
-
);
|
|
393
|
+
logger?.debug('fetching stream: %s', url);
|
|
394
|
+
const response = await fetch(url, { signal });
|
|
395
|
+
logger?.debug('stream response status: %d', response.status);
|
|
396
|
+
if (timingLogsEnabled && started)
|
|
397
|
+
console.error(
|
|
398
|
+
`[TIMING] +${Date.now() - started}ms: stream response received (status: ${response.status})`
|
|
399
|
+
);
|
|
488
400
|
|
|
489
401
|
if (!response.ok || !response.body) {
|
|
490
|
-
logger?.debug('
|
|
402
|
+
logger?.debug('stream response not ok or no body');
|
|
491
403
|
return;
|
|
492
404
|
}
|
|
493
405
|
|
|
494
406
|
const reader = response.body.getReader();
|
|
495
|
-
let
|
|
496
|
-
let totalBytes = 0;
|
|
407
|
+
let firstChunk = true;
|
|
497
408
|
|
|
498
409
|
// Read until EOF - Pulse will block until data is available
|
|
499
410
|
while (true) {
|
|
500
411
|
const { done, value } = await reader.read();
|
|
501
412
|
if (done) {
|
|
502
|
-
logger?.debug(
|
|
503
|
-
|
|
504
|
-
Date.now() -
|
|
505
|
-
chunks,
|
|
506
|
-
totalBytes
|
|
507
|
-
);
|
|
413
|
+
logger?.debug('stream EOF');
|
|
414
|
+
if (timingLogsEnabled && started)
|
|
415
|
+
console.error(`[TIMING] +${Date.now() - started}ms: stream EOF`);
|
|
508
416
|
break;
|
|
509
417
|
}
|
|
510
418
|
|
|
511
419
|
if (value) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
value.length,
|
|
519
|
-
totalBytes,
|
|
520
|
-
Date.now() - streamStart
|
|
521
|
-
);
|
|
420
|
+
if (firstChunk && started) {
|
|
421
|
+
if (timingLogsEnabled)
|
|
422
|
+
console.error(
|
|
423
|
+
`[TIMING] +${Date.now() - started}ms: first chunk (${value.length} bytes)`
|
|
424
|
+
);
|
|
425
|
+
firstChunk = false;
|
|
522
426
|
}
|
|
427
|
+
logger?.debug('stream chunk: %d bytes', value.length);
|
|
523
428
|
await writeAndDrain(writable, value);
|
|
524
429
|
}
|
|
525
430
|
}
|
|
@@ -528,9 +433,9 @@ async function streamUrlToWritable(
|
|
|
528
433
|
writable.end();
|
|
529
434
|
} catch (err) {
|
|
530
435
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
531
|
-
logger?.debug('
|
|
436
|
+
logger?.debug('stream aborted');
|
|
532
437
|
return;
|
|
533
438
|
}
|
|
534
|
-
logger?.debug('
|
|
439
|
+
logger?.debug('stream error: %s', err);
|
|
535
440
|
}
|
|
536
441
|
}
|
|
@@ -398,20 +398,6 @@ export const SandboxSchema = z.object({
|
|
|
398
398
|
.describe('Resume the sandbox from a paused or evacuated state.'),
|
|
399
399
|
/** Destroy the sandbox */
|
|
400
400
|
destroy: z.custom<() => Promise<void>>().describe('Destroy the sandbox'),
|
|
401
|
-
/** Create a new job in the sandbox */
|
|
402
|
-
createJob: z
|
|
403
|
-
.custom<(options: CreateJobOptions) => Promise<Job>>()
|
|
404
|
-
.describe('Create a new job in the sandbox'),
|
|
405
|
-
/** Get a job by ID */
|
|
406
|
-
getJob: z.custom<(jobId: string) => Promise<Job>>().describe('Get a job by ID'),
|
|
407
|
-
/** List jobs in the sandbox */
|
|
408
|
-
listJobs: z
|
|
409
|
-
.custom<(limit?: number) => Promise<{ jobs: Job[] }>>()
|
|
410
|
-
.describe('List jobs in the sandbox'),
|
|
411
|
-
/** Stop a running job */
|
|
412
|
-
stopJob: z
|
|
413
|
-
.custom<(jobId: string, force?: boolean) => Promise<Job>>()
|
|
414
|
-
.describe('Stop a running job'),
|
|
415
401
|
});
|
|
416
402
|
export type Sandbox = z.infer<typeof SandboxSchema>;
|
|
417
403
|
|