@dbx-tools/appkit-mastra 0.6.216 → 0.6.217
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 +96 -4
- package/index.ts +9 -3
- package/lib/index.d.ts +9 -3
- package/lib/index.js +6 -2
- package/lib/src/agents.d.ts +4 -3
- package/lib/src/agents.js +10 -5
- package/lib/src/config.d.ts +8 -0
- package/lib/src/config.js +5 -1
- package/lib/src/model.d.ts +2 -0
- package/lib/src/model.js +21 -22
- package/lib/src/monty-sandbox.d.ts +51 -0
- package/lib/src/monty-sandbox.js +331 -0
- package/lib/src/plugin.d.ts +1 -1
- package/lib/src/sandbox.d.ts +100 -0
- package/lib/src/sandbox.js +418 -0
- package/lib/src/serving-sanitize.d.ts +18 -1
- package/lib/src/serving-sanitize.js +41 -3
- package/lib/src/workspaces.d.ts +19 -6
- package/lib/src/workspaces.js +70 -7
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -22
- package/src/agents.ts +13 -5
- package/src/config.ts +13 -0
- package/src/model.ts +21 -23
- package/src/monty-sandbox.ts +406 -0
- package/src/sandbox.ts +515 -0
- package/src/serving-sanitize.ts +50 -2
- package/src/workspaces.ts +105 -6
package/src/agents.ts
CHANGED
|
@@ -151,8 +151,9 @@ function deriveToolId(description: string): string {
|
|
|
151
151
|
* });
|
|
152
152
|
* ```
|
|
153
153
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
154
|
+
* Adds the package's default workspace when the definition omits one. That
|
|
155
|
+
* workspace carries Databricks skill mounts and Databricks Sandbox command
|
|
156
|
+
* execution. An explicit workspace remains the caller's complete override.
|
|
156
157
|
*/
|
|
157
158
|
export function createAgent<T extends MastraAgentDefinition>(def: T): T {
|
|
158
159
|
if (def.workspace) return { ...def };
|
|
@@ -250,7 +251,7 @@ export type MastraPlugins = Record<string, MastraPluginToolkitProvider>;
|
|
|
250
251
|
/** Function form of {@link MastraAgentDefinition.tools}. */
|
|
251
252
|
export type MastraToolsFn = (plugins: MastraPlugins) => MastraTools | Promise<MastraTools>;
|
|
252
253
|
|
|
253
|
-
/** Function form of {@link MastraAgentDefinition.workspace}. */
|
|
254
|
+
/** Function form of {@link MastraAgentDefinition.workspace}; `undefined` disables it for this agent. */
|
|
254
255
|
export type MastraAgentWorkspaceResolver = () => Workspace | undefined;
|
|
255
256
|
|
|
256
257
|
/**
|
|
@@ -512,8 +513,15 @@ export async function buildAgents(opts: {
|
|
|
512
513
|
for (const [id, def] of Object.entries(definitions)) {
|
|
513
514
|
const tools = await resolveTools(def.tools, plugins, ambientTools);
|
|
514
515
|
let workspace = resolveAgentWorkspace(def.workspace);
|
|
515
|
-
if (
|
|
516
|
-
workspace
|
|
516
|
+
if (
|
|
517
|
+
(def.workspace === undefined && !workspace) ||
|
|
518
|
+
((extraSkillPaths?.length || config.sandbox !== undefined) && isDefaultWorkspace(workspace))
|
|
519
|
+
) {
|
|
520
|
+
workspace = createWorkspace({
|
|
521
|
+
extraSkillPaths,
|
|
522
|
+
sandbox:
|
|
523
|
+
config.sandbox === undefined || config.sandbox === true ? "databricks" : config.sandbox,
|
|
524
|
+
});
|
|
517
525
|
markDefaultWorkspace(workspace);
|
|
518
526
|
}
|
|
519
527
|
const gated = approvalGatedToolIds(tools);
|
package/src/config.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { MastraAgentDefinition, MastraTools } from "./agents.ts";
|
|
|
26
26
|
import type { GenieSpacesConfig } from "./genie.ts";
|
|
27
27
|
import { IDENTITY_MODES, type MastraIdentityMode } from "./identity.ts";
|
|
28
28
|
import type { RemoteSkillsOption } from "./remote-skills.ts";
|
|
29
|
+
import type { DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* `RequestContext` key under which {@link MastraServer} stores the
|
|
@@ -198,6 +199,13 @@ export interface MastraPluginConfig extends BasePluginConfig {
|
|
|
198
199
|
* `lakebase` plugin's pool; an object opens a dedicated store.
|
|
199
200
|
*/
|
|
200
201
|
memory?: boolean | MastraMemoryConfig;
|
|
202
|
+
/**
|
|
203
|
+
* Sandbox for auto-created agent workspaces. Defaults to Databricks Sandbox.
|
|
204
|
+
* `true` selects Databricks with defaults, an object configures its lifecycle,
|
|
205
|
+
* and `false` disables workspace command execution. An agent with an explicit
|
|
206
|
+
* custom `workspace` keeps that workspace and its sandbox.
|
|
207
|
+
*/
|
|
208
|
+
sandbox?: boolean | "databricks" | "monty" | DatabricksWorkspaceSandboxOptions;
|
|
201
209
|
/**
|
|
202
210
|
* Code-defined agents. Accepts three shapes for convenience:
|
|
203
211
|
*
|
|
@@ -630,6 +638,11 @@ export const MASTRA_CONFIG_SCHEMA: ConfigSchema = {
|
|
|
630
638
|
description:
|
|
631
639
|
"PgVector store for Mastra semantic recall. `true` reuses the `lakebase` plugin's pool, an object opens a dedicated store. Auto-enabled when the `lakebase` plugin is registered.",
|
|
632
640
|
},
|
|
641
|
+
sandbox: {
|
|
642
|
+
type: ["boolean", "string", "object"],
|
|
643
|
+
description:
|
|
644
|
+
'Command sandbox for auto-created agent workspaces. Defaults to Databricks Sandbox with Monty fallback; false disables command execution, "monty" selects Monty directly, and an object configures Databricks lifecycle/fallback settings.',
|
|
645
|
+
},
|
|
633
646
|
defaultAgent: {
|
|
634
647
|
type: "string",
|
|
635
648
|
description:
|
package/src/model.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { getExecutionContext } from "@databricks/appkit";
|
|
27
|
-
import { classes, resolve } from "@dbx-tools/model";
|
|
27
|
+
import { classes, invoke, resolve } from "@dbx-tools/model";
|
|
28
28
|
import { functionModule, json, log, net } from "@dbx-tools/shared-core";
|
|
29
29
|
import { model } from "@dbx-tools/shared-model";
|
|
30
30
|
import type { MastraModelConfig } from "@mastra/core/llm";
|
|
@@ -32,7 +32,7 @@ import type { RequestContext } from "@mastra/core/request-context";
|
|
|
32
32
|
|
|
33
33
|
import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config.ts";
|
|
34
34
|
import {
|
|
35
|
-
|
|
35
|
+
rewriteServingRequest,
|
|
36
36
|
rewriteServingResponseBody,
|
|
37
37
|
rewriteServingResponseStream,
|
|
38
38
|
} from "./serving-sanitize.ts";
|
|
@@ -131,16 +131,16 @@ export async function buildModel(
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
const
|
|
134
|
+
/** Chat Completions route whose provider-specific wire shapes are normalized here. */
|
|
135
|
+
const CHAT_COMPLETIONS_PATH = `/${invoke.CHAT_COMPLETIONS_PATH}`;
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* Install a single shared `globalThis.fetch` wrapper for
|
|
139
|
-
*
|
|
138
|
+
* Install a single shared `globalThis.fetch` wrapper for Chat Completions
|
|
139
|
+
* requests. The wrapper does three things:
|
|
140
140
|
*
|
|
141
|
-
* 1. Rewrites
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* 1. Rewrites outgoing JSON from either `init.body` or a `Request` to repair
|
|
142
|
+
* provider-specific request constraints (see {@link rewriteServingRequest}
|
|
143
|
+
* in `./serving-sanitize.js`).
|
|
144
144
|
* 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
|
|
145
145
|
* 4xx debugging doesn't have to fight AI SDK's `[Array]`
|
|
146
146
|
* formatter.
|
|
@@ -154,32 +154,30 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
|
|
|
154
154
|
* step.
|
|
155
155
|
*/
|
|
156
156
|
const setupFetchInterceptor = functionModule.memoize((): void => {
|
|
157
|
+
globalThis.fetch = createServingFetchInterceptor(globalThis.fetch.bind(globalThis));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/** Build the serving fetch wrapper; exported for transport-level regression tests. */
|
|
161
|
+
export function createServingFetchInterceptor(original: typeof fetch): typeof fetch {
|
|
157
162
|
const logger = log.logger("mastra/llm");
|
|
158
|
-
|
|
159
|
-
globalThis.fetch = (async (input, init) => {
|
|
163
|
+
return (async (input, init) => {
|
|
160
164
|
const url = net.urlBuilder(input);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
!url.pathname.startsWith(SERVING_ENDPOINTS_PATH_PREFIX) ||
|
|
164
|
-
typeof init?.body !== "string"
|
|
165
|
-
) {
|
|
165
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
166
|
+
if (!url || url.pathname !== CHAT_COMPLETIONS_PATH || method.toUpperCase() !== "POST") {
|
|
166
167
|
return original(input, init);
|
|
167
168
|
}
|
|
168
|
-
const rewritten =
|
|
169
|
-
|
|
170
|
-
init = { ...init, body: rewritten };
|
|
171
|
-
}
|
|
172
|
-
const parsed = json.parse<unknown>(rewritten);
|
|
169
|
+
const rewritten = await rewriteServingRequest(input, init);
|
|
170
|
+
const parsed = json.parse<unknown>(rewritten.body);
|
|
173
171
|
logger.debug(
|
|
174
172
|
"POST",
|
|
175
173
|
parsed === undefined
|
|
176
174
|
? { url: url.toString(), bodyType: "non-JSON" }
|
|
177
175
|
: { url: url.toString(), body: parsed },
|
|
178
176
|
);
|
|
179
|
-
const response = await original(input, init);
|
|
177
|
+
const response = await original(rewritten.input, rewritten.init);
|
|
180
178
|
return repairServingResponse(response);
|
|
181
179
|
}) as typeof globalThis.fetch;
|
|
182
|
-
}
|
|
180
|
+
}
|
|
183
181
|
|
|
184
182
|
/**
|
|
185
183
|
* Rewrite a serving response whose body needs repair, leaving unsupported
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pydantic Monty fallback for Mastra command execution.
|
|
3
|
+
*
|
|
4
|
+
* Uses the Node subprocess-worker build of Monty. It executes Python source
|
|
5
|
+
* with no host filesystem, network, environment, shell, or third-party package
|
|
6
|
+
* access unless a future caller explicitly adds those capabilities.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { error, functionModule } from "@dbx-tools/shared-core";
|
|
12
|
+
import type {
|
|
13
|
+
CommandResult,
|
|
14
|
+
ExecuteCommandOptions,
|
|
15
|
+
SandboxInfo,
|
|
16
|
+
WorkspaceSandbox,
|
|
17
|
+
} from "@mastra/core/workspace";
|
|
18
|
+
import type { CheckoutOptions, Monty as MontyPool, MontySession } from "@pydantic/monty/node";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
|
|
21
|
+
const DEFAULT_MAX_MEMORY_BYTES = 10_000_000;
|
|
22
|
+
|
|
23
|
+
const montyModule = functionModule.memoize(() => import("@pydantic/monty/node"));
|
|
24
|
+
const montyPool = functionModule.memoize(async (): Promise<MontyPool> => {
|
|
25
|
+
const { Monty } = await montyModule();
|
|
26
|
+
return Monty.create({
|
|
27
|
+
minProcesses: 1,
|
|
28
|
+
maxProcesses: 4,
|
|
29
|
+
requestTimeout: 35,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Resource controls for the Monty Python fallback. */
|
|
34
|
+
export interface MontySandboxOptions {
|
|
35
|
+
/** Mastra provider id. */
|
|
36
|
+
id?: string;
|
|
37
|
+
/** Display name. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Default execution timeout. Per-call `options.timeout` wins. */
|
|
40
|
+
commandTimeoutMs?: number;
|
|
41
|
+
/** Maximum Monty heap bytes for one checkout. */
|
|
42
|
+
maxMemoryBytes?: number;
|
|
43
|
+
/** Type-check each Python snippet before running it. Defaults to false. */
|
|
44
|
+
typeCheck?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Python-only, deny-by-default sandbox backed by Monty's Node worker pool. */
|
|
48
|
+
export class MontySandbox implements WorkspaceSandbox {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly provider = "monty";
|
|
52
|
+
status: WorkspaceSandbox["status"] = "pending";
|
|
53
|
+
error?: string;
|
|
54
|
+
|
|
55
|
+
private readonly commandTimeoutMs: number;
|
|
56
|
+
private readonly maxMemoryBytes: number;
|
|
57
|
+
private readonly typeCheck: boolean;
|
|
58
|
+
private readonly createdAt = new Date();
|
|
59
|
+
private lastUsedAt?: Date;
|
|
60
|
+
|
|
61
|
+
constructor(options: MontySandboxOptions = {}) {
|
|
62
|
+
this.id = options.id ?? "monty";
|
|
63
|
+
this.name = options.name ?? "Pydantic Monty";
|
|
64
|
+
this.commandTimeoutMs = options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
65
|
+
this.maxMemoryBytes = options.maxMemoryBytes ?? DEFAULT_MAX_MEMORY_BYTES;
|
|
66
|
+
this.typeCheck = options.typeCheck ?? false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Warm the shared crash-isolated worker pool. */
|
|
70
|
+
async start(): Promise<void> {
|
|
71
|
+
if (this.status === "running") return;
|
|
72
|
+
this.status = "starting";
|
|
73
|
+
this.error = undefined;
|
|
74
|
+
try {
|
|
75
|
+
await montyPool();
|
|
76
|
+
this.status = "running";
|
|
77
|
+
} catch (caught) {
|
|
78
|
+
this.status = "error";
|
|
79
|
+
this.error = error.errorMessage(caught);
|
|
80
|
+
throw caught;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The process-wide pool stays warm; this instance carries no open session. */
|
|
85
|
+
async stop(): Promise<void> {
|
|
86
|
+
this.status = "stopped";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The process-wide pool stays warm; each command closes its own checkout. */
|
|
90
|
+
async destroy(): Promise<void> {
|
|
91
|
+
this.status = "destroyed";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Monty sessions are ephemeral and do not provide persistent checkpoints. */
|
|
95
|
+
async snapshot(): Promise<void> {}
|
|
96
|
+
|
|
97
|
+
readonly supportsCheckpoints = false;
|
|
98
|
+
|
|
99
|
+
/** Execute Python source and map Monty output to Mastra's command result. */
|
|
100
|
+
async executeCommand(
|
|
101
|
+
command: string,
|
|
102
|
+
args: string[] = [],
|
|
103
|
+
options: ExecuteCommandOptions = {},
|
|
104
|
+
): Promise<CommandResult> {
|
|
105
|
+
const started = performance.now();
|
|
106
|
+
const code = montyCode(command, args);
|
|
107
|
+
if (code === undefined) {
|
|
108
|
+
return failedResult(
|
|
109
|
+
command,
|
|
110
|
+
args,
|
|
111
|
+
started,
|
|
112
|
+
"Monty fallback accepts Python source directly, or python/python3 with a single -c script.",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const environment = Object.entries(options.env ?? {}).filter(
|
|
116
|
+
([, value]) => value !== undefined,
|
|
117
|
+
);
|
|
118
|
+
if (environment.length > 0) {
|
|
119
|
+
return failedResult(
|
|
120
|
+
command,
|
|
121
|
+
args,
|
|
122
|
+
started,
|
|
123
|
+
"Monty fallback does not expose host environment variables.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (options.cwd) {
|
|
127
|
+
return failedResult(
|
|
128
|
+
command,
|
|
129
|
+
args,
|
|
130
|
+
started,
|
|
131
|
+
"Monty fallback does not expose a host working directory.",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (options.abortSignal?.aborted) throw options.abortSignal.reason;
|
|
135
|
+
|
|
136
|
+
await this.start();
|
|
137
|
+
const timeout = options.timeout ?? this.commandTimeoutMs;
|
|
138
|
+
const pool = await montyPool();
|
|
139
|
+
const session = await checkoutSession(
|
|
140
|
+
pool,
|
|
141
|
+
{
|
|
142
|
+
limits: {
|
|
143
|
+
maxDurationSecs: Math.max(0.001, timeout / 1_000),
|
|
144
|
+
maxMemory: this.maxMemoryBytes,
|
|
145
|
+
},
|
|
146
|
+
typeCheck: this.typeCheck,
|
|
147
|
+
},
|
|
148
|
+
options.abortSignal,
|
|
149
|
+
);
|
|
150
|
+
const output = new RetainedOutput(options);
|
|
151
|
+
const workerPid = session.workerPid;
|
|
152
|
+
let aborted = false;
|
|
153
|
+
const onAbort = (): void => {
|
|
154
|
+
aborted = true;
|
|
155
|
+
terminateWorker(workerPid);
|
|
156
|
+
};
|
|
157
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
158
|
+
if (options.abortSignal?.aborted) onAbort();
|
|
159
|
+
try {
|
|
160
|
+
if (aborted) throw abortReason(options.abortSignal);
|
|
161
|
+
const value = await session.feedRun(stripPythonFence(code), {
|
|
162
|
+
printCallback: (stream, text) => output.emit(stream, text),
|
|
163
|
+
});
|
|
164
|
+
const returned = formatResult(value);
|
|
165
|
+
if (returned) {
|
|
166
|
+
const suffix = returned.endsWith("\n") ? returned : `${returned}\n`;
|
|
167
|
+
output.emit("stdout", suffix);
|
|
168
|
+
}
|
|
169
|
+
this.lastUsedAt = new Date();
|
|
170
|
+
return {
|
|
171
|
+
command,
|
|
172
|
+
args,
|
|
173
|
+
success: true,
|
|
174
|
+
exitCode: 0,
|
|
175
|
+
stdout: output.stdout.value,
|
|
176
|
+
stderr: output.stderr.value,
|
|
177
|
+
executionTimeMs: performance.now() - started,
|
|
178
|
+
...output.metadata(),
|
|
179
|
+
};
|
|
180
|
+
} catch (caught) {
|
|
181
|
+
if (aborted || options.abortSignal?.aborted) {
|
|
182
|
+
throw abortReason(options.abortSignal);
|
|
183
|
+
}
|
|
184
|
+
const message = error.errorMessage(caught);
|
|
185
|
+
output.emit("stderr", output.stderr.value ? `\n${message}` : message);
|
|
186
|
+
const { MontyCrashedError } = await montyModule();
|
|
187
|
+
const timedOut = caught instanceof MontyCrashedError && caught.timedOut;
|
|
188
|
+
return {
|
|
189
|
+
command,
|
|
190
|
+
args,
|
|
191
|
+
success: false,
|
|
192
|
+
exitCode: timedOut ? -1 : 1,
|
|
193
|
+
stdout: output.stdout.value,
|
|
194
|
+
stderr: output.stderr.value,
|
|
195
|
+
executionTimeMs: performance.now() - started,
|
|
196
|
+
...(timedOut ? { timedOut: true, killed: true } : {}),
|
|
197
|
+
...output.metadata(),
|
|
198
|
+
};
|
|
199
|
+
} finally {
|
|
200
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
201
|
+
await session.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async isReady(): Promise<boolean> {
|
|
206
|
+
return this.status === "running";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getInfo(): SandboxInfo {
|
|
210
|
+
return {
|
|
211
|
+
id: this.id,
|
|
212
|
+
name: this.name,
|
|
213
|
+
provider: this.provider,
|
|
214
|
+
status: this.status,
|
|
215
|
+
createdAt: this.createdAt,
|
|
216
|
+
...(this.lastUsedAt ? { lastUsedAt: this.lastUsedAt } : {}),
|
|
217
|
+
metadata: {
|
|
218
|
+
language: "python",
|
|
219
|
+
hostAccess: false,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getInstructions(): string {
|
|
225
|
+
return [
|
|
226
|
+
"Commands run as Python source in the Pydantic Monty fallback.",
|
|
227
|
+
"Pass Python code directly rather than shell syntax.",
|
|
228
|
+
"There is no host filesystem, network, environment, shell, or third-party package access.",
|
|
229
|
+
].join(" ");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function montyCode(command: string, args: readonly string[]): string | undefined {
|
|
234
|
+
if (args.length === 0) return command;
|
|
235
|
+
if ((command === "python" || command === "python3") && args.length === 2 && args[0] === "-c") {
|
|
236
|
+
return args[1];
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function stripPythonFence(code: string): string {
|
|
242
|
+
const trimmed = code.trim();
|
|
243
|
+
const match = /^```(?:python|py)?\s*\n([\s\S]*?)\n```$/i.exec(trimmed);
|
|
244
|
+
return match?.[1] ?? code;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatResult(value: unknown): string {
|
|
248
|
+
if (value === undefined || value === null) return "";
|
|
249
|
+
if (typeof value === "string") return value;
|
|
250
|
+
try {
|
|
251
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
252
|
+
} catch {
|
|
253
|
+
return String(value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
class RetainedOutput {
|
|
258
|
+
readonly stdout: RetainedText;
|
|
259
|
+
readonly stderr: RetainedText;
|
|
260
|
+
|
|
261
|
+
private readonly onStdout: ExecuteCommandOptions["onStdout"];
|
|
262
|
+
private readonly onStderr: ExecuteCommandOptions["onStderr"];
|
|
263
|
+
|
|
264
|
+
constructor(options: ExecuteCommandOptions) {
|
|
265
|
+
this.stdout = new RetainedText(options.maxRetainedBytes);
|
|
266
|
+
this.stderr = new RetainedText(options.maxRetainedBytes);
|
|
267
|
+
this.onStdout = options.onStdout;
|
|
268
|
+
this.onStderr = options.onStderr;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
emit(stream: "stdout" | "stderr", text: string): void {
|
|
272
|
+
if (stream === "stdout") {
|
|
273
|
+
this.onStdout?.(text);
|
|
274
|
+
this.stdout.append(text);
|
|
275
|
+
} else {
|
|
276
|
+
this.onStderr?.(text);
|
|
277
|
+
this.stderr.append(text);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
metadata(): Pick<
|
|
282
|
+
CommandResult,
|
|
283
|
+
"stdoutTruncated" | "stderrTruncated" | "stdoutDroppedBytes" | "stderrDroppedBytes"
|
|
284
|
+
> {
|
|
285
|
+
return {
|
|
286
|
+
...(this.stdout.droppedBytes > 0
|
|
287
|
+
? {
|
|
288
|
+
stdoutTruncated: true,
|
|
289
|
+
stdoutDroppedBytes: this.stdout.droppedBytes,
|
|
290
|
+
}
|
|
291
|
+
: {}),
|
|
292
|
+
...(this.stderr.droppedBytes > 0
|
|
293
|
+
? {
|
|
294
|
+
stderrTruncated: true,
|
|
295
|
+
stderrDroppedBytes: this.stderr.droppedBytes,
|
|
296
|
+
}
|
|
297
|
+
: {}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
class RetainedText {
|
|
303
|
+
value = "";
|
|
304
|
+
droppedBytes = 0;
|
|
305
|
+
|
|
306
|
+
private readonly maxBytes: number;
|
|
307
|
+
private readonly encoder = new TextEncoder();
|
|
308
|
+
private readonly decoder = new TextDecoder();
|
|
309
|
+
|
|
310
|
+
constructor(maxBytes: number | undefined) {
|
|
311
|
+
const resolved = maxBytes ?? Number.POSITIVE_INFINITY;
|
|
312
|
+
if (
|
|
313
|
+
resolved !== Number.POSITIVE_INFINITY &&
|
|
314
|
+
(!Number.isSafeInteger(resolved) || resolved < 0)
|
|
315
|
+
) {
|
|
316
|
+
throw new TypeError("maxRetainedBytes must be a non-negative safe integer or Infinity");
|
|
317
|
+
}
|
|
318
|
+
this.maxBytes = resolved;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
append(text: string): void {
|
|
322
|
+
if (!text) return;
|
|
323
|
+
if (this.maxBytes === Number.POSITIVE_INFINITY) {
|
|
324
|
+
this.value += text;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const combined = this.encoder.encode(this.value + text);
|
|
328
|
+
if (combined.length <= this.maxBytes) {
|
|
329
|
+
this.value += text;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let start = combined.length - this.maxBytes;
|
|
333
|
+
while (start < combined.length && (combined[start]! & 0xc0) === 0x80) start++;
|
|
334
|
+
const retained = combined.subarray(start);
|
|
335
|
+
this.droppedBytes += combined.length - retained.length;
|
|
336
|
+
this.value = this.decoder.decode(retained);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function terminateWorker(workerPid: number | undefined): void {
|
|
341
|
+
if (workerPid === undefined) return;
|
|
342
|
+
try {
|
|
343
|
+
process.kill(workerPid, "SIGKILL");
|
|
344
|
+
} catch {
|
|
345
|
+
// The command can finish between the abort event and the kill request.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function checkoutSession(
|
|
350
|
+
pool: MontyPool,
|
|
351
|
+
options: CheckoutOptions,
|
|
352
|
+
signal: AbortSignal | undefined,
|
|
353
|
+
): Promise<MontySession> {
|
|
354
|
+
const checkout = pool.checkout(options);
|
|
355
|
+
if (!signal) return checkout;
|
|
356
|
+
if (signal.aborted) {
|
|
357
|
+
void checkout.then((session) => session.close()).catch(() => undefined);
|
|
358
|
+
throw abortReason(signal);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return new Promise<MontySession>((resolve, reject) => {
|
|
362
|
+
let settled = false;
|
|
363
|
+
const onAbort = (): void => {
|
|
364
|
+
if (settled) return;
|
|
365
|
+
settled = true;
|
|
366
|
+
reject(abortReason(signal));
|
|
367
|
+
void checkout.then((session) => session.close()).catch(() => undefined);
|
|
368
|
+
};
|
|
369
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
370
|
+
checkout.then(
|
|
371
|
+
(session) => {
|
|
372
|
+
if (settled) return;
|
|
373
|
+
settled = true;
|
|
374
|
+
signal.removeEventListener("abort", onAbort);
|
|
375
|
+
resolve(session);
|
|
376
|
+
},
|
|
377
|
+
(caught: unknown) => {
|
|
378
|
+
if (settled) return;
|
|
379
|
+
settled = true;
|
|
380
|
+
signal.removeEventListener("abort", onAbort);
|
|
381
|
+
reject(caught);
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function abortReason(signal: AbortSignal | undefined): unknown {
|
|
388
|
+
return signal?.reason ?? new DOMException("The command was aborted", "AbortError");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function failedResult(
|
|
392
|
+
command: string,
|
|
393
|
+
args: string[],
|
|
394
|
+
started: number,
|
|
395
|
+
stderr: string,
|
|
396
|
+
): CommandResult {
|
|
397
|
+
return {
|
|
398
|
+
command,
|
|
399
|
+
args,
|
|
400
|
+
success: false,
|
|
401
|
+
exitCode: 1,
|
|
402
|
+
stdout: "",
|
|
403
|
+
stderr,
|
|
404
|
+
executionTimeMs: performance.now() - started,
|
|
405
|
+
};
|
|
406
|
+
}
|