Skip to content

Commit 604aad0

Browse files
Merge pull request #36 from SmythOS/feat/core/ecma-sandbox-connector-implementation
Added ECMA Code Sandbox connector Implementation
2 parents 686ced8 + 0ceb2b2 commit 604aad0

File tree

8 files changed

+426
-3
lines changed

8 files changed

+426
-3
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"image-size": "^1.1.1",
8282
"ioredis": "^5.4.1",
8383
"isbinaryfile": "^5.0.2",
84+
"isolated-vm": "^6.0.0",
8485
"joi": "^17.13.1",
8586
"js-yaml": "^4.1.0",
8687
"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: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
// Define a SafeBuffer object
34+
const ___internal = {
35+
b64decode: (str) => Buffer.from(str, 'base64').toString('utf8'),
36+
b64encode: (str) => Buffer.from(str, 'utf8').toString('base64'),
37+
};
38+
39+
//const closureStr =
40+
const keys = Object.keys(___internal);
41+
const functions = keys.map((key) => ___internal[key]);
42+
const closure = `
43+
globalThis.___internal = {
44+
${keys.map((key, i) => `${key}: $${i}`).join(',\n')}
45+
}`;
46+
47+
await context.evalClosure(closure, functions);
48+
49+
return { isolate, context, jail };
50+
} catch (error) {
51+
console.error(error);
52+
throw error;
53+
}
54+
}
55+
56+
export async function runJs(code: string) {
57+
try {
58+
if (!code) {
59+
throw new Error('No code provided');
60+
}
61+
62+
if (!code.endsWith(';')) code += ';';
63+
64+
const { isolate, context, jail } = await setupIsolate();
65+
const remoteUrls = await extractFetchUrls(code);
66+
for (const url of remoteUrls) {
67+
const remoteCode = await fetchCodeFromCDN(url);
68+
await context.eval(`${remoteCode}`);
69+
}
70+
71+
const executionCode = `
72+
(async () => {
73+
${code}
74+
globalThis.__finalResult = result;
75+
})();
76+
`;
77+
78+
// Execute the original code
79+
const executeScript = await isolate.compileScript(executionCode).catch((err) => {
80+
console.error(err);
81+
return { error: 'Compile Error - ' + err.message };
82+
});
83+
if ('error' in executeScript) {
84+
throw new Error(executeScript.error);
85+
}
86+
87+
await executeScript.run(context).catch((err) => {
88+
console.error(err);
89+
throw new Error('Run Error - ' + err.message);
90+
});
91+
92+
// Try to get the result from the global variable first, then fallback to 'result'
93+
let rawResult = await context.eval('globalThis.__finalResult').catch((err) => {
94+
console.error('Failed to get __finalResult:', err);
95+
return null;
96+
});
97+
98+
if (rawResult?.error) {
99+
throw new Error(rawResult.error);
100+
}
101+
return { Output: rawResult };
102+
} catch (error) {
103+
console.error(error);
104+
throw new Error(error.message);
105+
}
106+
}
107+
108+
function getParametersString(parameters: string[], inputs: Record<string, any>) {
109+
let params = [];
110+
for (const parameter of parameters) {
111+
if (typeof inputs[parameter] === 'string') {
112+
params.push(`'${inputs[parameter]}'`);
113+
} else {
114+
params.push(`${inputs[parameter]};`);
115+
}
116+
}
117+
return params.join(',');
118+
}
119+
120+
export function generateExecutableCode(code: string, parameters: string[], inputs: Record<string, any>) {
121+
const executableCode = `
122+
${code}
123+
const result = await main(${getParametersString(parameters, inputs)});
124+
`
125+
return executableCode;
126+
}

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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 { generateExecutableCode, runJs } from '@sre/helpers/ECMASandbox.helper';
8+
import { validateAsyncMainFunction } from '@sre/helpers/AWSLambdaCode.helper';
9+
10+
const console = Logger('ECMASandbox');
11+
export class ECMASandbox extends CodeConnector {
12+
public name = 'ECMASandbox';
13+
private sandboxUrl: string;
14+
15+
constructor(config: { sandboxUrl: string }) {
16+
super(config);
17+
this.sandboxUrl = config.sandboxUrl;
18+
}
19+
public async prepare(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodePreparationResult> {
20+
return {
21+
prepared: true,
22+
errors: [],
23+
warnings: [],
24+
};
25+
}
26+
27+
public async deploy(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodeDeployment> {
28+
return {
29+
id: codeUID,
30+
runtime: config.runtime,
31+
createdAt: new Date(),
32+
status: 'Deployed',
33+
};
34+
}
35+
36+
public async execute(acRequest: AccessRequest, codeUID: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
37+
try {
38+
const { isValid, error, parameters } = validateAsyncMainFunction(inputs.code);
39+
if (!isValid) {
40+
return {
41+
output: undefined,
42+
executionTime: 0,
43+
success: false,
44+
errors: [error],
45+
}
46+
}
47+
const executableCode = generateExecutableCode(inputs.code, parameters, inputs.inputs);
48+
if (!this.sandboxUrl) {
49+
// run js code in isolated vm
50+
console.debug('Running code in isolated vm');
51+
const result = await runJs(executableCode);
52+
console.debug(`Code result: ${result}`);
53+
return {
54+
output: result?.Output,
55+
executionTime: 0,
56+
success: true,
57+
errors: [],
58+
};
59+
} else {
60+
console.debug('Running code in remote sandbox');
61+
const result: any = await axios.post(this.sandboxUrl, { code: executableCode }).catch((error) => ({ error }));
62+
if (result.error) {
63+
64+
const error = result.error?.response?.data || result.error?.message || result.error.toString() || 'Unknown error';
65+
console.error(`Error running code: ${JSON.stringify(error, null, 2)}`);
66+
return {
67+
output: undefined,
68+
executionTime: 0,
69+
success: false,
70+
errors: [error],
71+
};
72+
} else {
73+
console.debug(`Code result: ${result?.data?.Output}`);
74+
return {
75+
output: result.data?.Output,
76+
executionTime: 0,
77+
success: true,
78+
errors: [],
79+
};
80+
}
81+
}
82+
} catch (error) {
83+
console.error(`Error running code: ${error}`);
84+
return {
85+
output: undefined,
86+
executionTime: 0,
87+
success: false,
88+
errors: [error],
89+
};
90+
}
91+
}
92+
public async executeDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
93+
const result = await this.execute(acRequest, codeUID, inputs, config);
94+
return result;
95+
}
96+
97+
public async listDeployments(acRequest: AccessRequest, codeUID: string, config: CodeConfig): Promise<CodeDeployment[]> {
98+
return [];
99+
}
100+
101+
public async getDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string, config: CodeConfig): Promise<CodeDeployment | null> {
102+
return null;
103+
}
104+
105+
public async deleteDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string): Promise<void> {
106+
return;
107+
}
108+
109+
public async getResourceACL(resourceId: string, candidate: IAccessCandidate): Promise<ACL> {
110+
const acl = new ACL();
111+
112+
//give Read access everytime
113+
//FIXME: !!!!!! IMPORTANT !!!!!! this implementation have to be changed in order to reflect the security model of AWS Lambda
114+
acl.addAccess(candidate.role, candidate.id, TAccessLevel.Read);
115+
116+
return acl;
117+
}
118+
}

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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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/v2',
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: `async function main(prompt) { return prompt + ' ' + 'Hello World'; }`,
30+
inputs: {
31+
prompt: 'Say'
32+
}
33+
});
34+
35+
const output = result.output;
36+
expect(output).toBe('Say Hello World');
37+
},
38+
);
39+
it(
40+
'Try to run a simple code without main function',
41+
async () => {
42+
const mockCandidate: IAccessCandidate = {
43+
id: 'test-user',
44+
role: TAccessRole.User,
45+
};
46+
47+
const codeConnector = ConnectorService.getCodeConnector('ECMASandbox');
48+
const result = await codeConnector.agent(mockCandidate.id).execute(Date.now().toString(), {
49+
code: `async function testFunction(prompt) { return prompt + ' ' + 'Hello World'; }`,
50+
inputs: {
51+
prompt: 'Say'
52+
}
53+
});
54+
const error = result.errors;
55+
expect(error).toContain('No main function found at root level');
56+
},
57+
);
58+
});

0 commit comments

Comments
 (0)