@better-agent/plugins 0.1.0-beta.1 → 0.1.0-beta.3
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/index.d.mts +354 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +705 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BetterAgentError } from "@better-agent/shared/errors";
|
|
2
|
+
import { defineTool } from "@better-agent/core";
|
|
2
3
|
|
|
3
4
|
//#region src/shared/json.ts
|
|
4
5
|
/** Creates a JSON response with a default content type. */
|
|
@@ -24,26 +25,26 @@ function createUnauthorizedResponse() {
|
|
|
24
25
|
//#endregion
|
|
25
26
|
//#region src/shared/validation.ts
|
|
26
27
|
/** Creates a plugin validation error. */
|
|
27
|
-
function createValidationError(message, at) {
|
|
28
|
+
function createValidationError$1(message, at) {
|
|
28
29
|
return BetterAgentError.fromCode("VALIDATION_FAILED", message, { trace: [{ at }] });
|
|
29
30
|
}
|
|
30
31
|
/** Requires a positive finite number. */
|
|
31
32
|
function requirePositiveNumber(value, name, at) {
|
|
32
|
-
if (!Number.isFinite(value) || value <= 0) throw createValidationError(`\`${name}\` must be a positive number.`, at);
|
|
33
|
+
if (!Number.isFinite(value) || value <= 0) throw createValidationError$1(`\`${name}\` must be a positive number.`, at);
|
|
33
34
|
}
|
|
34
35
|
/** Requires a non-empty array. */
|
|
35
36
|
function requireNonEmptyArray(value, name, at) {
|
|
36
|
-
if (!value || value.length === 0) throw createValidationError(`\`${name}\` must contain at least one value.`, at);
|
|
37
|
+
if (!value || value.length === 0) throw createValidationError$1(`\`${name}\` must contain at least one value.`, at);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
//#endregion
|
|
40
41
|
//#region src/auth/validate.ts
|
|
41
42
|
/** Validates `authPlugin` configuration. */
|
|
42
43
|
function validateAuthPluginConfig(config) {
|
|
43
|
-
if (!config.validate && (!config.apiKeys || config.apiKeys.length === 0)) throw createValidationError("`authPlugin` requires either `apiKeys` or `validate`.", "plugins.authPlugin");
|
|
44
|
-
if (config.header !== void 0 && config.header.trim().length === 0) throw createValidationError("`authPlugin` requires `header` to be a non-empty string when provided.", "plugins.authPlugin");
|
|
44
|
+
if (!config.validate && (!config.apiKeys || config.apiKeys.length === 0)) throw createValidationError$1("`authPlugin` requires either `apiKeys` or `validate`.", "plugins.authPlugin");
|
|
45
|
+
if (config.header !== void 0 && config.header.trim().length === 0) throw createValidationError$1("`authPlugin` requires `header` to be a non-empty string when provided.", "plugins.authPlugin");
|
|
45
46
|
if (config.apiKeys) {
|
|
46
|
-
if (config.apiKeys.filter((key) => typeof key === "string").map((key) => key.trim()).filter((key) => key.length > 0).length === 0 && !config.validate) throw createValidationError("`authPlugin` requires `apiKeys` to contain at least one non-empty key.", "plugins.authPlugin");
|
|
47
|
+
if (config.apiKeys.filter((key) => typeof key === "string").map((key) => key.trim()).filter((key) => key.length > 0).length === 0 && !config.validate) throw createValidationError$1("`authPlugin` requires `apiKeys` to contain at least one non-empty key.", "plugins.authPlugin");
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -224,7 +225,7 @@ function createIpDeniedResponse() {
|
|
|
224
225
|
/** Validates `ipAllowlistPlugin` configuration. */
|
|
225
226
|
function validateIpAllowlistPluginConfig(config) {
|
|
226
227
|
requireNonEmptyArray(config.allow, "allow", "plugins.ipAllowlistPlugin");
|
|
227
|
-
for (const entry of config.allow) if (typeof entry !== "string" || !parseAllowEntry(entry)) throw createValidationError(`\`ipAllowlistPlugin\` received an invalid allow entry: '${String(entry)}'.`, "plugins.ipAllowlistPlugin");
|
|
228
|
+
for (const entry of config.allow) if (typeof entry !== "string" || !parseAllowEntry(entry)) throw createValidationError$1(`\`ipAllowlistPlugin\` received an invalid allow entry: '${String(entry)}'.`, "plugins.ipAllowlistPlugin");
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
//#endregion
|
|
@@ -702,5 +703,701 @@ const rateLimitPlugin = (config) => {
|
|
|
702
703
|
};
|
|
703
704
|
|
|
704
705
|
//#endregion
|
|
705
|
-
|
|
706
|
+
//#region src/sandbox/daytona.ts
|
|
707
|
+
const loadDaytona = async () => {
|
|
708
|
+
try {
|
|
709
|
+
return await import("@daytonaio/sdk");
|
|
710
|
+
} catch (error) {
|
|
711
|
+
throw BetterAgentError.fromCode("INTERNAL", "The Daytona sandbox client requires the `@daytonaio/sdk` package to be installed in the host app.", {
|
|
712
|
+
cause: error,
|
|
713
|
+
trace: [{ at: "plugins.sandbox.createDaytonaSandboxClient.loadDaytona" }]
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const removeUndefined$1 = (value) => Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
718
|
+
const toSeconds = (value) => value === void 0 ? void 0 : Math.max(1, Math.ceil(value / 1e3));
|
|
719
|
+
const toUtf8 = (value) => typeof value === "string" ? value : new TextDecoder().decode(value);
|
|
720
|
+
const normalizeFileEntries = (files) => files.map((entry) => ({
|
|
721
|
+
name: entry.name ?? entry.path?.split("/").filter(Boolean).at(-1) ?? "",
|
|
722
|
+
path: entry.path ?? entry.name ?? "",
|
|
723
|
+
type: entry.type ?? (entry.isDir ? "directory" : "file")
|
|
724
|
+
}));
|
|
725
|
+
/** Creates a sandbox client backed by the Daytona SDK. */
|
|
726
|
+
function createDaytonaSandboxClient(config = {}) {
|
|
727
|
+
const clientConfig = removeUndefined$1({
|
|
728
|
+
apiKey: config.apiKey,
|
|
729
|
+
apiUrl: config.apiUrl,
|
|
730
|
+
target: config.target
|
|
731
|
+
});
|
|
732
|
+
const buildCreateParams = (overrides) => {
|
|
733
|
+
const template = overrides?.template ?? config.template;
|
|
734
|
+
const templateKind = config.templateKind ?? "snapshot";
|
|
735
|
+
return removeUndefined$1({
|
|
736
|
+
language: config.language,
|
|
737
|
+
envVars: overrides?.envs ?? config.envs,
|
|
738
|
+
labels: overrides?.metadata ?? config.metadata,
|
|
739
|
+
public: config.public,
|
|
740
|
+
autoStopInterval: config.autoStopInterval,
|
|
741
|
+
autoArchiveInterval: config.autoArchiveInterval,
|
|
742
|
+
autoDeleteInterval: config.autoDeleteInterval,
|
|
743
|
+
...template !== void 0 ? templateKind === "image" ? { image: template } : { snapshot: template } : {},
|
|
744
|
+
...config.snapshot !== void 0 ? { snapshot: config.snapshot } : {},
|
|
745
|
+
...config.image !== void 0 ? { image: config.image } : {}
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
const getClient = async () => {
|
|
749
|
+
const { Daytona } = await loadDaytona();
|
|
750
|
+
return new Daytona(clientConfig);
|
|
751
|
+
};
|
|
752
|
+
const getSandbox = async (sandboxId) => {
|
|
753
|
+
const client = await getClient();
|
|
754
|
+
return {
|
|
755
|
+
sandbox: await client.get(sandboxId),
|
|
756
|
+
client
|
|
757
|
+
};
|
|
758
|
+
};
|
|
759
|
+
const toCommandResult = (response) => ({
|
|
760
|
+
exitCode: response.exitCode,
|
|
761
|
+
stdout: response.result
|
|
762
|
+
});
|
|
763
|
+
return {
|
|
764
|
+
provider: "daytona",
|
|
765
|
+
capabilities: {
|
|
766
|
+
commands: true,
|
|
767
|
+
filesystem: true,
|
|
768
|
+
preview: true,
|
|
769
|
+
pty: true,
|
|
770
|
+
desktop: true,
|
|
771
|
+
git: true,
|
|
772
|
+
snapshots: true,
|
|
773
|
+
volumes: true,
|
|
774
|
+
lifecycle: {
|
|
775
|
+
start: true,
|
|
776
|
+
stop: true,
|
|
777
|
+
archive: true,
|
|
778
|
+
resume: true
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
async createSandbox(params) {
|
|
782
|
+
const client = await getClient();
|
|
783
|
+
const timeout = toSeconds(params?.timeoutMs ?? config.timeoutMs);
|
|
784
|
+
return { sandboxId: (await client.create(buildCreateParams(params), { ...timeout !== void 0 ? { timeout } : {} })).id };
|
|
785
|
+
},
|
|
786
|
+
async runCommand(params) {
|
|
787
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
788
|
+
return toCommandResult(await sandbox.process.executeCommand(params.cmd, params.cwd, params.envs, toSeconds(params.timeoutMs)));
|
|
789
|
+
},
|
|
790
|
+
async readFile(params) {
|
|
791
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
792
|
+
return toUtf8(await sandbox.fs.downloadFile(params.path));
|
|
793
|
+
},
|
|
794
|
+
async writeFile(params) {
|
|
795
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
796
|
+
await sandbox.fs.uploadFile(Buffer.from(params.content), params.path);
|
|
797
|
+
return { path: params.path };
|
|
798
|
+
},
|
|
799
|
+
async listFiles(params) {
|
|
800
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
801
|
+
return normalizeFileEntries(await sandbox.fs.listFiles(params.path));
|
|
802
|
+
},
|
|
803
|
+
async makeDir(params) {
|
|
804
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
805
|
+
await sandbox.fs.createFolder(params.path, "755");
|
|
806
|
+
return { created: true };
|
|
807
|
+
},
|
|
808
|
+
async removePath(params) {
|
|
809
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
810
|
+
await sandbox.fs.deleteFile(params.path, true);
|
|
811
|
+
},
|
|
812
|
+
async getHost(params) {
|
|
813
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
814
|
+
return await sandbox.getPreviewLink(params.port);
|
|
815
|
+
},
|
|
816
|
+
async killSandbox(params) {
|
|
817
|
+
const { client, sandbox } = await getSandbox(params.sandboxId);
|
|
818
|
+
if (typeof sandbox.delete === "function") {
|
|
819
|
+
await sandbox.delete(60);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (typeof client.delete === "function") {
|
|
823
|
+
await client.delete(sandbox, 60);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
throw BetterAgentError.fromCode("NOT_IMPLEMENTED", "The active Daytona SDK instance does not expose sandbox deletion.", {
|
|
827
|
+
context: { sandboxId: params.sandboxId },
|
|
828
|
+
trace: [{ at: "plugins.sandbox.createDaytonaSandboxClient.killSandbox" }]
|
|
829
|
+
});
|
|
830
|
+
},
|
|
831
|
+
lifecycle: {
|
|
832
|
+
async startSandbox(params) {
|
|
833
|
+
const { client, sandbox } = await getSandbox(params.sandboxId);
|
|
834
|
+
const timeout = toSeconds(params.timeoutMs);
|
|
835
|
+
if (typeof sandbox.start === "function") {
|
|
836
|
+
await sandbox.start(timeout);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (typeof client.start === "function") await client.start(sandbox, timeout);
|
|
840
|
+
},
|
|
841
|
+
async stopSandbox(params) {
|
|
842
|
+
const { client, sandbox } = await getSandbox(params.sandboxId);
|
|
843
|
+
if (typeof sandbox.stop === "function") {
|
|
844
|
+
await sandbox.stop(toSeconds(params.timeoutMs));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (typeof client.stop === "function") await client.stop(sandbox);
|
|
848
|
+
},
|
|
849
|
+
async archiveSandbox(params) {
|
|
850
|
+
const { sandbox } = await getSandbox(params.sandboxId);
|
|
851
|
+
if (typeof sandbox.archive === "function") {
|
|
852
|
+
await sandbox.archive();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
throw BetterAgentError.fromCode("NOT_IMPLEMENTED", "The active Daytona SDK instance does not expose sandbox archiving.", {
|
|
856
|
+
context: { sandboxId: params.sandboxId },
|
|
857
|
+
trace: [{ at: "plugins.sandbox.createDaytonaSandboxClient.archiveSandbox" }]
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/sandbox/e2b.ts
|
|
866
|
+
const removeUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
867
|
+
const loadE2B = async () => {
|
|
868
|
+
try {
|
|
869
|
+
return await import("e2b");
|
|
870
|
+
} catch (error) {
|
|
871
|
+
throw BetterAgentError.fromCode("INTERNAL", "The built-in E2B sandbox client requires the `e2b` package to be installed in the host app.", {
|
|
872
|
+
cause: error,
|
|
873
|
+
trace: [{ at: "plugins.sandbox.createE2BSandboxClient.loadE2B" }]
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
/** Creates a sandbox client backed by the E2B SDK. */
|
|
878
|
+
function createE2BSandboxClient(config = {}) {
|
|
879
|
+
const connectionOptions = removeUndefined({
|
|
880
|
+
apiKey: config.apiKey,
|
|
881
|
+
accessToken: config.accessToken,
|
|
882
|
+
domain: config.domain,
|
|
883
|
+
requestTimeoutMs: config.requestTimeoutMs
|
|
884
|
+
});
|
|
885
|
+
const toCreateOptions = (overrides) => removeUndefined({
|
|
886
|
+
...connectionOptions,
|
|
887
|
+
timeoutMs: overrides?.timeoutMs ?? config.timeoutMs,
|
|
888
|
+
envs: overrides?.envs ?? config.envs,
|
|
889
|
+
metadata: overrides?.metadata ?? config.metadata
|
|
890
|
+
});
|
|
891
|
+
const connectSandbox = async (sandboxId) => {
|
|
892
|
+
const { Sandbox } = await loadE2B();
|
|
893
|
+
return await Sandbox.connect(sandboxId, connectionOptions);
|
|
894
|
+
};
|
|
895
|
+
const mapCommandResult = (result) => ({
|
|
896
|
+
exitCode: result.exitCode,
|
|
897
|
+
stdout: result.stdout,
|
|
898
|
+
stderr: result.stderr,
|
|
899
|
+
pid: result.pid
|
|
900
|
+
});
|
|
901
|
+
return {
|
|
902
|
+
provider: "e2b",
|
|
903
|
+
capabilities: {
|
|
904
|
+
commands: true,
|
|
905
|
+
filesystem: true,
|
|
906
|
+
preview: true,
|
|
907
|
+
mcp: true,
|
|
908
|
+
pty: true,
|
|
909
|
+
desktop: true
|
|
910
|
+
},
|
|
911
|
+
async createSandbox(params) {
|
|
912
|
+
const { Sandbox } = await loadE2B();
|
|
913
|
+
const template = params?.template ?? config.template;
|
|
914
|
+
const options = toCreateOptions(params);
|
|
915
|
+
return { sandboxId: (template !== void 0 ? await Sandbox.create(template, options) : await Sandbox.create(options)).sandboxId };
|
|
916
|
+
},
|
|
917
|
+
async runCommand(params) {
|
|
918
|
+
return mapCommandResult(await (await connectSandbox(params.sandboxId)).commands.run(params.cmd, removeUndefined({
|
|
919
|
+
cwd: params.cwd,
|
|
920
|
+
envs: params.envs,
|
|
921
|
+
timeoutMs: params.timeoutMs
|
|
922
|
+
})));
|
|
923
|
+
},
|
|
924
|
+
async readFile(params) {
|
|
925
|
+
return await (await connectSandbox(params.sandboxId)).files.read(params.path);
|
|
926
|
+
},
|
|
927
|
+
async writeFile(params) {
|
|
928
|
+
return { path: (await (await connectSandbox(params.sandboxId)).files.write(params.path, params.content)).path ?? params.path };
|
|
929
|
+
},
|
|
930
|
+
async listFiles(params) {
|
|
931
|
+
return await (await connectSandbox(params.sandboxId)).files.list(params.path);
|
|
932
|
+
},
|
|
933
|
+
async makeDir(params) {
|
|
934
|
+
return { created: await (await connectSandbox(params.sandboxId)).files.makeDir(params.path) };
|
|
935
|
+
},
|
|
936
|
+
async removePath(params) {
|
|
937
|
+
await (await connectSandbox(params.sandboxId)).files.remove(params.path);
|
|
938
|
+
},
|
|
939
|
+
async getHost(params) {
|
|
940
|
+
return (await connectSandbox(params.sandboxId)).getHost(params.port);
|
|
941
|
+
},
|
|
942
|
+
async killSandbox(params) {
|
|
943
|
+
await (await connectSandbox(params.sandboxId)).kill();
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/sandbox/memory-store.ts
|
|
950
|
+
/** Creates an in-memory sandbox session store. */
|
|
951
|
+
function createMemorySandboxSessionStore() {
|
|
952
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
953
|
+
return {
|
|
954
|
+
async get(key) {
|
|
955
|
+
return sessions.get(key) ?? null;
|
|
956
|
+
},
|
|
957
|
+
async set(key, sandboxId) {
|
|
958
|
+
sessions.set(key, sandboxId);
|
|
959
|
+
},
|
|
960
|
+
async delete(key) {
|
|
961
|
+
sessions.delete(key);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/sandbox/validate.ts
|
|
968
|
+
/** Validates `sandboxPlugin` configuration. */
|
|
969
|
+
function validateSandboxPluginConfig(config) {
|
|
970
|
+
const client = config.client;
|
|
971
|
+
if (!client || typeof client !== "object") throw createValidationError$1("`sandboxPlugin` requires a `client`.", "plugins.sandboxPlugin");
|
|
972
|
+
if (config.prefix !== void 0 && config.prefix.trim().length === 0) throw createValidationError$1("`sandboxPlugin` requires `prefix` to be a non-empty string when provided.", "plugins.sandboxPlugin");
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/sandbox/plugin.ts
|
|
977
|
+
const trimToUndefined = (value) => {
|
|
978
|
+
const trimmed = value?.trim();
|
|
979
|
+
return trimmed ? trimmed : void 0;
|
|
980
|
+
};
|
|
981
|
+
const normalizeHostUrl = (value) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
|
|
982
|
+
const createValidationError = (message, at, context) => BetterAgentError.fromCode("VALIDATION_FAILED", message, {
|
|
983
|
+
...context !== void 0 ? { context } : {},
|
|
984
|
+
trace: [{ at }]
|
|
985
|
+
});
|
|
986
|
+
/**
|
|
987
|
+
* Adds sandbox tools.
|
|
988
|
+
*
|
|
989
|
+
* By default, the plugin reuses one sandbox per `conversationId`.
|
|
990
|
+
* Use `sessionKey` to customize reuse, or return `null`/`undefined`
|
|
991
|
+
* to disable reuse for a specific tool call.
|
|
992
|
+
*
|
|
993
|
+
* Uses an in-memory store by default.
|
|
994
|
+
*
|
|
995
|
+
* @example
|
|
996
|
+
* ```ts
|
|
997
|
+
* import { sandboxPlugin, createE2BSandboxClient } from "@better-agent/plugins";
|
|
998
|
+
*
|
|
999
|
+
* const plugin = sandboxPlugin({
|
|
1000
|
+
* client: createE2BSandboxClient({
|
|
1001
|
+
* apiKey: process.env.E2B_API_KEY,
|
|
1002
|
+
* }),
|
|
1003
|
+
* defaults: {
|
|
1004
|
+
* template: "base",
|
|
1005
|
+
* timeoutMs: 10 * 60_000,
|
|
1006
|
+
* },
|
|
1007
|
+
* });
|
|
1008
|
+
* ```
|
|
1009
|
+
*/
|
|
1010
|
+
const sandboxPlugin = (config) => {
|
|
1011
|
+
validateSandboxPluginConfig(config);
|
|
1012
|
+
const sandboxClient = config.client;
|
|
1013
|
+
const store = config.store ?? createMemorySandboxSessionStore();
|
|
1014
|
+
const prefix = trimToUndefined(config.prefix) ?? "sandbox";
|
|
1015
|
+
const nameFor = (suffix) => `${prefix}_${suffix}`;
|
|
1016
|
+
const getSessionPolicy = (ctx, toolName) => {
|
|
1017
|
+
if (config.sessionKey) {
|
|
1018
|
+
const custom = trimToUndefined(config.sessionKey({
|
|
1019
|
+
runId: ctx.runId,
|
|
1020
|
+
agentName: ctx.agentName,
|
|
1021
|
+
...ctx.conversationId !== void 0 ? { conversationId: ctx.conversationId } : {},
|
|
1022
|
+
toolName
|
|
1023
|
+
}) ?? void 0);
|
|
1024
|
+
return custom !== void 0 ? {
|
|
1025
|
+
kind: "managed",
|
|
1026
|
+
sessionKey: custom
|
|
1027
|
+
} : { kind: "disabled" };
|
|
1028
|
+
}
|
|
1029
|
+
return ctx.conversationId ? {
|
|
1030
|
+
kind: "managed",
|
|
1031
|
+
sessionKey: `conversation:${ctx.conversationId}`
|
|
1032
|
+
} : { kind: "disabled" };
|
|
1033
|
+
};
|
|
1034
|
+
const getExplicitSandboxId = (value) => {
|
|
1035
|
+
if (value === void 0) return;
|
|
1036
|
+
const trimmed = value.trim();
|
|
1037
|
+
if (!trimmed) throw createValidationError("`sandboxId` must be a non-empty string when provided.", "plugins.sandboxPlugin.sandboxId");
|
|
1038
|
+
return trimmed;
|
|
1039
|
+
};
|
|
1040
|
+
const createSandbox = async (params, ctx, toolName) => {
|
|
1041
|
+
const sessionPolicy = getSessionPolicy(ctx, toolName);
|
|
1042
|
+
const created = await sandboxClient.createSandbox({
|
|
1043
|
+
template: params?.template ?? config.defaults?.template,
|
|
1044
|
+
timeoutMs: params?.timeoutMs ?? config.defaults?.timeoutMs,
|
|
1045
|
+
envs: params?.envs ?? config.defaults?.envs,
|
|
1046
|
+
metadata: params?.metadata ?? config.defaults?.metadata
|
|
1047
|
+
});
|
|
1048
|
+
if (sessionPolicy.kind === "managed") await store.set(sessionPolicy.sessionKey, created.sandboxId);
|
|
1049
|
+
return {
|
|
1050
|
+
sandboxId: created.sandboxId,
|
|
1051
|
+
...sessionPolicy.kind === "managed" ? { sessionKey: sessionPolicy.sessionKey } : {},
|
|
1052
|
+
created: true
|
|
1053
|
+
};
|
|
1054
|
+
};
|
|
1055
|
+
const resolveSandbox = async (params) => {
|
|
1056
|
+
const explicitSandboxId = getExplicitSandboxId(params.sandboxId);
|
|
1057
|
+
if (explicitSandboxId) return {
|
|
1058
|
+
sandboxId: explicitSandboxId,
|
|
1059
|
+
created: false
|
|
1060
|
+
};
|
|
1061
|
+
const sessionPolicy = getSessionPolicy(params.ctx, params.toolName);
|
|
1062
|
+
if (sessionPolicy.kind === "managed") {
|
|
1063
|
+
const existing = trimToUndefined(await store.get(sessionPolicy.sessionKey) ?? void 0);
|
|
1064
|
+
if (existing) return {
|
|
1065
|
+
sandboxId: existing,
|
|
1066
|
+
sessionKey: sessionPolicy.sessionKey,
|
|
1067
|
+
created: false
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
if (!params.createIfMissing) throw createValidationError(`No sandbox is available for this ${params.ctx.conversationId ? "conversation" : "run"}. Create one first with '${nameFor("create")}' or pass a sandboxId explicitly.`, "plugins.sandboxPlugin.resolveSandbox.missing", {
|
|
1071
|
+
runId: params.ctx.runId,
|
|
1072
|
+
agentName: params.ctx.agentName,
|
|
1073
|
+
conversationId: params.ctx.conversationId,
|
|
1074
|
+
toolName: params.toolName
|
|
1075
|
+
});
|
|
1076
|
+
return await createSandbox(params.createParams, params.ctx, params.toolName);
|
|
1077
|
+
};
|
|
1078
|
+
const clearManagedSessionIfMatches = async (ctx, toolName, sandboxId) => {
|
|
1079
|
+
const sessionPolicy = getSessionPolicy(ctx, toolName);
|
|
1080
|
+
if (sessionPolicy.kind !== "managed") return;
|
|
1081
|
+
if (await store.get(sessionPolicy.sessionKey) === sandboxId) {
|
|
1082
|
+
await store.delete(sessionPolicy.sessionKey);
|
|
1083
|
+
return sessionPolicy.sessionKey;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
const createSchema = {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
properties: {
|
|
1089
|
+
forceNew: { type: "boolean" },
|
|
1090
|
+
template: { type: "string" },
|
|
1091
|
+
timeoutMs: {
|
|
1092
|
+
type: "number",
|
|
1093
|
+
exclusiveMinimum: 0
|
|
1094
|
+
},
|
|
1095
|
+
envs: {
|
|
1096
|
+
type: "object",
|
|
1097
|
+
additionalProperties: { type: "string" }
|
|
1098
|
+
},
|
|
1099
|
+
metadata: {
|
|
1100
|
+
type: "object",
|
|
1101
|
+
additionalProperties: { type: "string" }
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
additionalProperties: false
|
|
1105
|
+
};
|
|
1106
|
+
const sandboxIdSchema = { type: "string" };
|
|
1107
|
+
const pathSchema = {
|
|
1108
|
+
type: "string",
|
|
1109
|
+
minLength: 1
|
|
1110
|
+
};
|
|
1111
|
+
const tools = [
|
|
1112
|
+
defineTool({
|
|
1113
|
+
name: nameFor("create"),
|
|
1114
|
+
description: "Create a sandbox, or reuse the current conversation sandbox unless forceNew is true.",
|
|
1115
|
+
schema: createSchema
|
|
1116
|
+
}).server(async (input, ctx) => {
|
|
1117
|
+
if (!input.forceNew) {
|
|
1118
|
+
const sessionPolicy = getSessionPolicy(ctx, nameFor("create"));
|
|
1119
|
+
if (sessionPolicy.kind === "managed") {
|
|
1120
|
+
const existing = trimToUndefined(await store.get(sessionPolicy.sessionKey) ?? void 0);
|
|
1121
|
+
if (existing) return {
|
|
1122
|
+
sandboxId: existing,
|
|
1123
|
+
reused: true,
|
|
1124
|
+
created: false,
|
|
1125
|
+
sessionKey: sessionPolicy.sessionKey
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const resolved = await createSandbox({
|
|
1130
|
+
template: input.template,
|
|
1131
|
+
timeoutMs: input.timeoutMs,
|
|
1132
|
+
envs: input.envs,
|
|
1133
|
+
metadata: input.metadata
|
|
1134
|
+
}, ctx, nameFor("create"));
|
|
1135
|
+
return {
|
|
1136
|
+
sandboxId: resolved.sandboxId,
|
|
1137
|
+
reused: false,
|
|
1138
|
+
created: true,
|
|
1139
|
+
...resolved.sessionKey !== void 0 ? { sessionKey: resolved.sessionKey } : {}
|
|
1140
|
+
};
|
|
1141
|
+
}),
|
|
1142
|
+
defineTool({
|
|
1143
|
+
name: nameFor("exec"),
|
|
1144
|
+
description: "Run one shell command inside a sandbox. Creates a sandbox automatically when none exists for the conversation.",
|
|
1145
|
+
schema: {
|
|
1146
|
+
type: "object",
|
|
1147
|
+
properties: {
|
|
1148
|
+
sandboxId: sandboxIdSchema,
|
|
1149
|
+
cmd: {
|
|
1150
|
+
type: "string",
|
|
1151
|
+
minLength: 1
|
|
1152
|
+
},
|
|
1153
|
+
cwd: { type: "string" },
|
|
1154
|
+
timeoutMs: {
|
|
1155
|
+
type: "number",
|
|
1156
|
+
exclusiveMinimum: 0
|
|
1157
|
+
},
|
|
1158
|
+
envs: {
|
|
1159
|
+
type: "object",
|
|
1160
|
+
additionalProperties: { type: "string" }
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
required: ["cmd"],
|
|
1164
|
+
additionalProperties: false
|
|
1165
|
+
},
|
|
1166
|
+
approval: config.approvals?.exec
|
|
1167
|
+
}).server(async (input, ctx) => {
|
|
1168
|
+
const resolved = await resolveSandbox({
|
|
1169
|
+
sandboxId: input.sandboxId,
|
|
1170
|
+
createIfMissing: true,
|
|
1171
|
+
ctx,
|
|
1172
|
+
toolName: nameFor("exec")
|
|
1173
|
+
});
|
|
1174
|
+
const result = await sandboxClient.runCommand({
|
|
1175
|
+
sandboxId: resolved.sandboxId,
|
|
1176
|
+
cmd: input.cmd,
|
|
1177
|
+
cwd: input.cwd,
|
|
1178
|
+
timeoutMs: input.timeoutMs,
|
|
1179
|
+
envs: input.envs
|
|
1180
|
+
});
|
|
1181
|
+
return {
|
|
1182
|
+
sandboxId: resolved.sandboxId,
|
|
1183
|
+
createdSandbox: resolved.created,
|
|
1184
|
+
...result
|
|
1185
|
+
};
|
|
1186
|
+
}),
|
|
1187
|
+
defineTool({
|
|
1188
|
+
name: nameFor("read_file"),
|
|
1189
|
+
description: "Read one text file from a sandbox. Creates a sandbox automatically when none exists for the conversation.",
|
|
1190
|
+
schema: {
|
|
1191
|
+
type: "object",
|
|
1192
|
+
properties: {
|
|
1193
|
+
sandboxId: sandboxIdSchema,
|
|
1194
|
+
path: pathSchema
|
|
1195
|
+
},
|
|
1196
|
+
required: ["path"],
|
|
1197
|
+
additionalProperties: false
|
|
1198
|
+
}
|
|
1199
|
+
}).server(async (input, ctx) => {
|
|
1200
|
+
const resolved = await resolveSandbox({
|
|
1201
|
+
sandboxId: input.sandboxId,
|
|
1202
|
+
createIfMissing: true,
|
|
1203
|
+
ctx,
|
|
1204
|
+
toolName: nameFor("read_file")
|
|
1205
|
+
});
|
|
1206
|
+
return {
|
|
1207
|
+
sandboxId: resolved.sandboxId,
|
|
1208
|
+
path: input.path,
|
|
1209
|
+
content: await sandboxClient.readFile({
|
|
1210
|
+
sandboxId: resolved.sandboxId,
|
|
1211
|
+
path: input.path
|
|
1212
|
+
})
|
|
1213
|
+
};
|
|
1214
|
+
}),
|
|
1215
|
+
defineTool({
|
|
1216
|
+
name: nameFor("write_file"),
|
|
1217
|
+
description: "Write one text file into a sandbox. Creates a sandbox automatically when none exists for the conversation.",
|
|
1218
|
+
schema: {
|
|
1219
|
+
type: "object",
|
|
1220
|
+
properties: {
|
|
1221
|
+
sandboxId: sandboxIdSchema,
|
|
1222
|
+
path: pathSchema,
|
|
1223
|
+
content: { type: "string" }
|
|
1224
|
+
},
|
|
1225
|
+
required: ["path", "content"],
|
|
1226
|
+
additionalProperties: false
|
|
1227
|
+
},
|
|
1228
|
+
approval: config.approvals?.writeFile
|
|
1229
|
+
}).server(async (input, ctx) => {
|
|
1230
|
+
const resolved = await resolveSandbox({
|
|
1231
|
+
sandboxId: input.sandboxId,
|
|
1232
|
+
createIfMissing: true,
|
|
1233
|
+
ctx,
|
|
1234
|
+
toolName: nameFor("write_file")
|
|
1235
|
+
});
|
|
1236
|
+
const result = await sandboxClient.writeFile({
|
|
1237
|
+
sandboxId: resolved.sandboxId,
|
|
1238
|
+
path: input.path,
|
|
1239
|
+
content: input.content
|
|
1240
|
+
});
|
|
1241
|
+
return {
|
|
1242
|
+
sandboxId: resolved.sandboxId,
|
|
1243
|
+
createdSandbox: resolved.created,
|
|
1244
|
+
path: result.path
|
|
1245
|
+
};
|
|
1246
|
+
}),
|
|
1247
|
+
defineTool({
|
|
1248
|
+
name: nameFor("list_files"),
|
|
1249
|
+
description: "List directory entries inside a sandbox. Creates a sandbox automatically when none exists for the conversation.",
|
|
1250
|
+
schema: {
|
|
1251
|
+
type: "object",
|
|
1252
|
+
properties: {
|
|
1253
|
+
sandboxId: sandboxIdSchema,
|
|
1254
|
+
path: pathSchema
|
|
1255
|
+
},
|
|
1256
|
+
required: ["path"],
|
|
1257
|
+
additionalProperties: false
|
|
1258
|
+
}
|
|
1259
|
+
}).server(async (input, ctx) => {
|
|
1260
|
+
const resolved = await resolveSandbox({
|
|
1261
|
+
sandboxId: input.sandboxId,
|
|
1262
|
+
createIfMissing: true,
|
|
1263
|
+
ctx,
|
|
1264
|
+
toolName: nameFor("list_files")
|
|
1265
|
+
});
|
|
1266
|
+
return {
|
|
1267
|
+
sandboxId: resolved.sandboxId,
|
|
1268
|
+
path: input.path,
|
|
1269
|
+
entries: await sandboxClient.listFiles({
|
|
1270
|
+
sandboxId: resolved.sandboxId,
|
|
1271
|
+
path: input.path
|
|
1272
|
+
})
|
|
1273
|
+
};
|
|
1274
|
+
}),
|
|
1275
|
+
defineTool({
|
|
1276
|
+
name: nameFor("make_dir"),
|
|
1277
|
+
description: "Create a directory inside a sandbox. Creates a sandbox automatically when none exists for the conversation.",
|
|
1278
|
+
schema: {
|
|
1279
|
+
type: "object",
|
|
1280
|
+
properties: {
|
|
1281
|
+
sandboxId: sandboxIdSchema,
|
|
1282
|
+
path: pathSchema
|
|
1283
|
+
},
|
|
1284
|
+
required: ["path"],
|
|
1285
|
+
additionalProperties: false
|
|
1286
|
+
}
|
|
1287
|
+
}).server(async (input, ctx) => {
|
|
1288
|
+
const resolved = await resolveSandbox({
|
|
1289
|
+
sandboxId: input.sandboxId,
|
|
1290
|
+
createIfMissing: true,
|
|
1291
|
+
ctx,
|
|
1292
|
+
toolName: nameFor("make_dir")
|
|
1293
|
+
});
|
|
1294
|
+
const result = await sandboxClient.makeDir({
|
|
1295
|
+
sandboxId: resolved.sandboxId,
|
|
1296
|
+
path: input.path
|
|
1297
|
+
});
|
|
1298
|
+
return {
|
|
1299
|
+
sandboxId: resolved.sandboxId,
|
|
1300
|
+
path: input.path,
|
|
1301
|
+
created: result.created
|
|
1302
|
+
};
|
|
1303
|
+
}),
|
|
1304
|
+
defineTool({
|
|
1305
|
+
name: nameFor("remove_path"),
|
|
1306
|
+
description: "Remove a file or directory from a sandbox.",
|
|
1307
|
+
schema: {
|
|
1308
|
+
type: "object",
|
|
1309
|
+
properties: {
|
|
1310
|
+
sandboxId: sandboxIdSchema,
|
|
1311
|
+
path: pathSchema
|
|
1312
|
+
},
|
|
1313
|
+
required: ["path"],
|
|
1314
|
+
additionalProperties: false
|
|
1315
|
+
},
|
|
1316
|
+
approval: config.approvals?.removePath
|
|
1317
|
+
}).server(async (input, ctx) => {
|
|
1318
|
+
const resolved = await resolveSandbox({
|
|
1319
|
+
sandboxId: input.sandboxId,
|
|
1320
|
+
createIfMissing: false,
|
|
1321
|
+
ctx,
|
|
1322
|
+
toolName: nameFor("remove_path")
|
|
1323
|
+
});
|
|
1324
|
+
await sandboxClient.removePath({
|
|
1325
|
+
sandboxId: resolved.sandboxId,
|
|
1326
|
+
path: input.path
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
sandboxId: resolved.sandboxId,
|
|
1330
|
+
removed: true,
|
|
1331
|
+
path: input.path
|
|
1332
|
+
};
|
|
1333
|
+
}),
|
|
1334
|
+
defineTool({
|
|
1335
|
+
name: nameFor("get_host"),
|
|
1336
|
+
description: "Expose one sandbox port as a host URL so callers can reach an app running inside the sandbox.",
|
|
1337
|
+
schema: {
|
|
1338
|
+
type: "object",
|
|
1339
|
+
properties: {
|
|
1340
|
+
sandboxId: sandboxIdSchema,
|
|
1341
|
+
port: {
|
|
1342
|
+
type: "number",
|
|
1343
|
+
minimum: 1,
|
|
1344
|
+
maximum: 65535
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
required: ["port"],
|
|
1348
|
+
additionalProperties: false
|
|
1349
|
+
}
|
|
1350
|
+
}).server(async (input, ctx) => {
|
|
1351
|
+
const resolved = await resolveSandbox({
|
|
1352
|
+
sandboxId: input.sandboxId,
|
|
1353
|
+
createIfMissing: false,
|
|
1354
|
+
ctx,
|
|
1355
|
+
toolName: nameFor("get_host")
|
|
1356
|
+
});
|
|
1357
|
+
const hostResult = await sandboxClient.getHost({
|
|
1358
|
+
sandboxId: resolved.sandboxId,
|
|
1359
|
+
port: input.port
|
|
1360
|
+
});
|
|
1361
|
+
const preview = typeof hostResult === "string" ? void 0 : hostResult;
|
|
1362
|
+
const host = normalizeHostUrl(typeof hostResult === "string" ? hostResult : hostResult.url);
|
|
1363
|
+
return {
|
|
1364
|
+
sandboxId: resolved.sandboxId,
|
|
1365
|
+
port: input.port,
|
|
1366
|
+
host,
|
|
1367
|
+
...preview?.token !== void 0 ? { token: preview.token } : {}
|
|
1368
|
+
};
|
|
1369
|
+
}),
|
|
1370
|
+
defineTool({
|
|
1371
|
+
name: nameFor("kill"),
|
|
1372
|
+
description: "Terminate a sandbox and clear the current conversation sandbox when applicable.",
|
|
1373
|
+
schema: {
|
|
1374
|
+
type: "object",
|
|
1375
|
+
properties: { sandboxId: sandboxIdSchema },
|
|
1376
|
+
additionalProperties: false
|
|
1377
|
+
},
|
|
1378
|
+
approval: config.approvals?.killSandbox
|
|
1379
|
+
}).server(async (input, ctx) => {
|
|
1380
|
+
const resolved = await resolveSandbox({
|
|
1381
|
+
sandboxId: input.sandboxId,
|
|
1382
|
+
createIfMissing: false,
|
|
1383
|
+
ctx,
|
|
1384
|
+
toolName: nameFor("kill")
|
|
1385
|
+
});
|
|
1386
|
+
await sandboxClient.killSandbox({ sandboxId: resolved.sandboxId });
|
|
1387
|
+
const clearedSessionKey = await clearManagedSessionIfMatches(ctx, nameFor("kill"), resolved.sandboxId);
|
|
1388
|
+
return {
|
|
1389
|
+
sandboxId: resolved.sandboxId,
|
|
1390
|
+
killed: true,
|
|
1391
|
+
...clearedSessionKey !== void 0 ? { clearedSessionKey } : {}
|
|
1392
|
+
};
|
|
1393
|
+
})
|
|
1394
|
+
];
|
|
1395
|
+
return {
|
|
1396
|
+
id: config.id ?? "sandbox",
|
|
1397
|
+
tools
|
|
1398
|
+
};
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
//#endregion
|
|
1402
|
+
export { authPlugin, createDaytonaSandboxClient, createE2BSandboxClient, createMemorySandboxSessionStore, ipAllowlistPlugin, loggingPlugin, rateLimitPlugin, sandboxPlugin };
|
|
706
1403
|
//# sourceMappingURL=index.mjs.map
|