Skip to content

Commit f071e40

Browse files
committed
added ECMA Code Sandbox connector Implementation
1 parent ec1bc83 commit f071e40

File tree

8 files changed

+375
-3
lines changed

8 files changed

+375
-3
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"image-size": "^1.1.1",
7979
"ioredis": "^5.4.1",
8080
"isbinaryfile": "^5.0.2",
81+
"isolated-vm": "^6.0.0",
8182
"joi": "^17.13.1",
8283
"js-yaml": "^4.1.0",
8384
"jsonrepair": "^3.8.0",

packages/core/src/Core/ConnectorsService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ManagedVaultConnector } from '@sre/Security/ManagedVault.service/Manage
1818
import { LogConnector } from '@sre/IO/Log.service/LogConnector';
1919
import { ComponentConnector } from '@sre/AgentManager/Component.service/ComponentConnector';
2020
import { ModelsProviderConnector } from '@sre/LLMManager/ModelsProvider.service/ModelsProviderConnector';
21+
import { CodeConnector } from '@sre/ComputeManager/Code.service/CodeConnector';
2122
const console = Logger('ConnectorService');
2223

2324
let ServiceRegistry: TServiceRegistry = {};
@@ -182,9 +183,8 @@ export class ConnectorService {
182183
return ConnectorService.getInstance<RouterConnector>(TConnectorService.Router, name);
183184
}
184185

185-
186-
static getCodeConnector(name?: string): RouterConnector {
187-
return ConnectorService.getInstance<RouterConnector>(TConnectorService.Code, name);
186+
static getCodeConnector(name?: string): CodeConnector {
187+
return ConnectorService.getInstance<CodeConnector>(TConnectorService.Code, name);
188188
}
189189
}
190190

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import ivm from 'isolated-vm';
2+
import https from 'https';
3+
4+
function extractFetchUrls(str) {
5+
const regex = /\/\/@fetch\((https?:\/\/[^\s]+)\)/g;
6+
let match;
7+
const urls = [];
8+
9+
while ((match = regex.exec(str)) !== null) {
10+
urls.push(match[1]);
11+
}
12+
13+
return urls;
14+
}
15+
function fetchCodeFromCDN(url) {
16+
return new Promise((resolve, reject) => {
17+
https
18+
.get(url, (res) => {
19+
let data = '';
20+
res.on('data', (chunk) => (data += chunk));
21+
res.on('end', () => resolve(data));
22+
})
23+
.on('error', reject);
24+
});
25+
}
26+
27+
async function setupIsolate() {
28+
try {
29+
const isolate = new ivm.Isolate({ memoryLimit: 128 });
30+
const context = await isolate.createContext();
31+
const jail = context.global;
32+
await jail.set('global', jail.derefInto());
33+
34+
// Define a SafeBuffer object
35+
const ___internal = {
36+
b64decode: (str) => Buffer.from(str, 'base64').toString('utf8'),
37+
b64encode: (str) => Buffer.from(str, 'utf8').toString('base64'),
38+
};
39+
40+
//const closureStr =
41+
const keys = Object.keys(___internal);
42+
const functions = keys.map((key) => ___internal[key]);
43+
const closure = `
44+
globalThis.___internal = {
45+
${keys.map((key, i) => `${key}: $${i}`).join(',\n')}
46+
}`;
47+
48+
await context.evalClosure(closure, functions);
49+
50+
return { isolate, context, jail };
51+
} catch (error) {
52+
console.error(error);
53+
throw error;
54+
}
55+
}
56+
57+
export async function runJs(code: string) {
58+
try {
59+
if (!code) {
60+
throw new Error('No code provided');
61+
}
62+
63+
if (!code.endsWith(';')) code += ';';
64+
65+
let scriptCode = '';
66+
const { isolate, context, jail } = await setupIsolate();
67+
const remoteUrls = await extractFetchUrls(code);
68+
for (const url of remoteUrls) {
69+
const remoteCode = await fetchCodeFromCDN(url);
70+
context.eval(`${remoteCode}`);
71+
}
72+
const randomId = Math.random().toString(36).substring(2, 15);
73+
const resId = `res${randomId}`;
74+
scriptCode = `
75+
var ${resId};
76+
${code};
77+
78+
${resId} = JSON.stringify(_output);
79+
${resId};
80+
`;
81+
const script: any = await isolate.compileScript(scriptCode).catch((err) => {
82+
console.error(err);
83+
return { error: 'Compile Error - ' + err.message };
84+
});
85+
if (script?.error) {
86+
throw new Error(script.error);
87+
}
88+
89+
const rawResult = await script.run(context).catch((err) => {
90+
console.error(err);
91+
return { error: 'Run Error - ' + err.message };
92+
});
93+
if (rawResult?.error) {
94+
throw new Error(rawResult.error);
95+
}
96+
97+
// Transfer the result out of the isolate and parse it
98+
//const serializedResult = rawResult.copySync();
99+
const Output = JSON.parse(rawResult);
100+
101+
return Output;
102+
} catch (error) {
103+
console.error(error);
104+
throw new Error(error.message);
105+
}
106+
}

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export * from './Core/SystemEvents';
5656
export * from './helpers/AWSLambdaCode.helper';
5757
export * from './helpers/BinaryInput.helper';
5858
export * from './helpers/Conversation.helper';
59+
export * from './helpers/ECMASandbox.helper';
5960
export * from './helpers/JsonContent.helper';
6061
export * from './helpers/LocalCache.helper';
6162
export * from './helpers/Log.helper';
@@ -149,6 +150,7 @@ export * from './subsystems/AgentManager/AgentData.service/connectors/LocalAgent
149150
export * from './subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class';
150151
export * from './subsystems/AgentManager/Component.service/connectors/LocalComponentConnector.class';
151152
export * from './subsystems/ComputeManager/Code.service/connectors/AWSLambdaCode.class';
153+
export * from './subsystems/ComputeManager/Code.service/connectors/ECMASandbox.class';
152154
export * from './subsystems/IO/Log.service/connectors/ConsoleLog.class';
153155
export * from './subsystems/IO/NKV.service/connectors/NKVLocalStorage.class';
154156
export * from './subsystems/IO/NKV.service/connectors/NKVRAM.class';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { IAccessCandidate, TAccessLevel } from '@sre/types/ACL.types';
2+
import { ACL } from '@sre/Security/AccessControl/ACL.class';
3+
import { CodeConfig, CodePreparationResult, CodeConnector, CodeInput, CodeDeployment, CodeExecutionResult } from '../CodeConnector';
4+
import { AccessRequest } from '@sre/Security/AccessControl/AccessRequest.class';
5+
import { Logger } from '@sre/helpers/Log.helper';
6+
import axios from 'axios';
7+
import { runJs } from '@sre/helpers/ECMASandbox.helper';
8+
9+
const console = Logger('ECMASandbox');
10+
export class ECMASandbox extends CodeConnector {
11+
public name = 'ECMASandbox';
12+
private sandboxUrl: string;
13+
14+
constructor(config: { sandboxUrl: string }) {
15+
super(config);
16+
this.sandboxUrl = config.sandboxUrl;
17+
}
18+
public async prepare(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodePreparationResult> {
19+
return {
20+
prepared: true,
21+
errors: [],
22+
warnings: [],
23+
};
24+
}
25+
26+
public async deploy(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodeDeployment> {
27+
return {
28+
id: codeUID,
29+
runtime: config.runtime,
30+
createdAt: new Date(),
31+
status: 'Deployed',
32+
};
33+
}
34+
35+
public async execute(acRequest: AccessRequest, codeUID: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
36+
try {
37+
if (!this.sandboxUrl) {
38+
// run js code in isolated vm
39+
console.debug('Running code in isolated vm');
40+
const result = await runJs(inputs.code);
41+
console.debug(`Code result: ${result}`);
42+
return {
43+
output: result?.Output,
44+
executionTime: 0,
45+
success: true,
46+
errors: [],
47+
};
48+
} else {
49+
console.debug('Running code in remote sandbox');
50+
const result: any = await axios.post(this.sandboxUrl, { code: inputs.code }).catch((error) => ({ error }));
51+
if (result.error) {
52+
53+
const error = result.error?.response?.data || result.error?.message || result.error.toString() || 'Unknown error';
54+
console.error(`Error running code: ${JSON.stringify(error, null, 2)}`);
55+
return {
56+
output: undefined,
57+
executionTime: 0,
58+
success: false,
59+
errors: [error],
60+
};
61+
} else {
62+
console.debug(`Code result: ${result?.data?.Output}`);
63+
return {
64+
output: result.data?.Output,
65+
executionTime: 0,
66+
success: true,
67+
errors: [],
68+
};
69+
}
70+
}
71+
} catch (error) {
72+
console.error(`Error running code: ${error}`);
73+
return {
74+
output: undefined,
75+
executionTime: 0,
76+
success: false,
77+
errors: [error],
78+
};
79+
}
80+
}
81+
public async executeDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
82+
const result = await this.execute(acRequest, codeUID, inputs, config);
83+
return result;
84+
}
85+
86+
public async listDeployments(acRequest: AccessRequest, codeUID: string, config: CodeConfig): Promise<CodeDeployment[]> {
87+
return [];
88+
}
89+
90+
public async getDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string, config: CodeConfig): Promise<CodeDeployment | null> {
91+
return null;
92+
}
93+
94+
public async deleteDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string): Promise<void> {
95+
return;
96+
}
97+
98+
public async getResourceACL(resourceId: string, candidate: IAccessCandidate): Promise<ACL> {
99+
const acl = new ACL();
100+
101+
//give Read access everytime
102+
//FIXME: !!!!!! IMPORTANT !!!!!! this implementation have to be changed in order to reflect the security model of AWS Lambda
103+
acl.addAccess(candidate.role, candidate.id, TAccessLevel.Read);
104+
105+
return acl;
106+
}
107+
}

packages/core/src/subsystems/ComputeManager/Code.service/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import { ConnectorService, ConnectorServiceProvider } from '@sre/Core/ConnectorsService';
44
import { TConnectorService } from '@sre/types/SRE.types';
55
import { AWSLambdaCode } from './connectors/AWSLambdaCode.class';
6+
import { ECMASandbox } from './connectors/ECMASandbox.class';
67

78
export class CodeService extends ConnectorServiceProvider {
89
public register() {
910
ConnectorService.register(TConnectorService.Code, 'AWSLambda', AWSLambdaCode);
11+
ConnectorService.register(TConnectorService.Code, 'ECMASandbox', ECMASandbox);
1012
}
1113
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { setupSRE } from '../../utils/sre';
3+
import { ConnectorService } from '@sre/Core/ConnectorsService';
4+
import { IAccessCandidate, TAccessRole } from 'index';
5+
6+
setupSRE({
7+
Code: {
8+
Connector: 'ECMASandbox',
9+
Settings: {
10+
sandboxUrl: 'http://localhost:6100/run-js',
11+
},
12+
},
13+
Log: {
14+
Connector: 'ConsoleLog',
15+
},
16+
});
17+
18+
describe('ECMASandbox Tests', () => {
19+
it(
20+
'Runs a simple code and returns the output',
21+
async () => {
22+
const mockCandidate: IAccessCandidate = {
23+
id: 'test-user',
24+
role: TAccessRole.User,
25+
};
26+
27+
const codeConnector = ConnectorService.getCodeConnector('ECMASandbox');
28+
const result = await codeConnector.agent(mockCandidate.id).execute(Date.now().toString(), {
29+
code: 'let _output=undefined;\nconsole.log("Hello, world!");\n_output=1;',
30+
}, {});
31+
32+
const output = result.output;
33+
34+
expect(output).toBe(1);
35+
},
36+
);
37+
38+
});

0 commit comments

Comments
 (0)