Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"image-size": "^1.1.1",
"ioredis": "^5.4.1",
"isbinaryfile": "^5.0.2",
"isolated-vm": "^6.0.0",
"joi": "^17.13.1",
"js-yaml": "^4.1.0",
"jsonrepair": "^3.8.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/Core/ConnectorsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ManagedVaultConnector } from '@sre/Security/ManagedVault.service/Manage
import { LogConnector } from '@sre/IO/Log.service/LogConnector';
import { ComponentConnector } from '@sre/AgentManager/Component.service/ComponentConnector';
import { ModelsProviderConnector } from '@sre/LLMManager/ModelsProvider.service/ModelsProviderConnector';
import { CodeConnector } from '@sre/ComputeManager/Code.service/CodeConnector';
const console = Logger('ConnectorService');

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


static getCodeConnector(name?: string): RouterConnector {
return ConnectorService.getInstance<RouterConnector>(TConnectorService.Code, name);
static getCodeConnector(name?: string): CodeConnector {
return ConnectorService.getInstance<CodeConnector>(TConnectorService.Code, name);
}
}

Expand Down
126 changes: 126 additions & 0 deletions packages/core/src/helpers/ECMASandbox.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import ivm from 'isolated-vm';
import https from 'https';

function extractFetchUrls(str) {
const regex = /\/\/@fetch\((https?:\/\/[^\s]+)\)/g;
let match;
const urls = [];

while ((match = regex.exec(str)) !== null) {
urls.push(match[1]);
}

return urls;
}
function fetchCodeFromCDN(url) {
return new Promise((resolve, reject) => {
https
.get(url, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(data));
})
.on('error', reject);
});
}

async function setupIsolate() {
try {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
const jail = context.global;
await jail.set('global', jail.derefInto());
// Define a SafeBuffer object
const ___internal = {
b64decode: (str) => Buffer.from(str, 'base64').toString('utf8'),
b64encode: (str) => Buffer.from(str, 'utf8').toString('base64'),
};

//const closureStr =
const keys = Object.keys(___internal);
const functions = keys.map((key) => ___internal[key]);
const closure = `
globalThis.___internal = {
${keys.map((key, i) => `${key}: $${i}`).join(',\n')}
}`;

await context.evalClosure(closure, functions);

return { isolate, context, jail };
} catch (error) {
console.error(error);
throw error;
}
}

export async function runJs(code: string) {
try {
if (!code) {
throw new Error('No code provided');
}

if (!code.endsWith(';')) code += ';';

const { isolate, context, jail } = await setupIsolate();
const remoteUrls = await extractFetchUrls(code);
for (const url of remoteUrls) {
const remoteCode = await fetchCodeFromCDN(url);
await context.eval(`${remoteCode}`);
}

const executionCode = `
(async () => {
${code}
globalThis.__finalResult = result;
})();
`;

// Execute the original code
const executeScript = await isolate.compileScript(executionCode).catch((err) => {
console.error(err);
return { error: 'Compile Error - ' + err.message };
});
if ('error' in executeScript) {
throw new Error(executeScript.error);
}

await executeScript.run(context).catch((err) => {
console.error(err);
throw new Error('Run Error - ' + err.message);
});

// Try to get the result from the global variable first, then fallback to 'result'
let rawResult = await context.eval('globalThis.__finalResult').catch((err) => {
console.error('Failed to get __finalResult:', err);
return null;
});

if (rawResult?.error) {
throw new Error(rawResult.error);
}
return { Output: rawResult };
} catch (error) {
console.error(error);
throw new Error(error.message);
}
}

function getParametersString(parameters: string[], inputs: Record<string, any>) {
let params = [];
for (const parameter of parameters) {
if (typeof inputs[parameter] === 'string') {
params.push(`'${inputs[parameter]}'`);
} else {
params.push(`${inputs[parameter]};`);
}
}
return params.join(',');
}

export function generateExecutableCode(code: string, parameters: string[], inputs: Record<string, any>) {
const executableCode = `
${code}
const result = await main(${getParametersString(parameters, inputs)});
`
return executableCode;
}
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export * from './Core/SystemEvents';
export * from './helpers/AWSLambdaCode.helper';
export * from './helpers/BinaryInput.helper';
export * from './helpers/Conversation.helper';
export * from './helpers/ECMASandbox.helper';
export * from './helpers/JsonContent.helper';
export * from './helpers/LocalCache.helper';
export * from './helpers/Log.helper';
Expand Down Expand Up @@ -149,6 +150,7 @@ export * from './subsystems/AgentManager/AgentData.service/connectors/LocalAgent
export * from './subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class';
export * from './subsystems/AgentManager/Component.service/connectors/LocalComponentConnector.class';
export * from './subsystems/ComputeManager/Code.service/connectors/AWSLambdaCode.class';
export * from './subsystems/ComputeManager/Code.service/connectors/ECMASandbox.class';
export * from './subsystems/IO/Log.service/connectors/ConsoleLog.class';
export * from './subsystems/IO/NKV.service/connectors/NKVLocalStorage.class';
export * from './subsystems/IO/NKV.service/connectors/NKVRAM.class';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { IAccessCandidate, TAccessLevel } from '@sre/types/ACL.types';
import { ACL } from '@sre/Security/AccessControl/ACL.class';
import { CodeConfig, CodePreparationResult, CodeConnector, CodeInput, CodeDeployment, CodeExecutionResult } from '../CodeConnector';
import { AccessRequest } from '@sre/Security/AccessControl/AccessRequest.class';
import { Logger } from '@sre/helpers/Log.helper';
import axios from 'axios';
import { generateExecutableCode, runJs } from '@sre/helpers/ECMASandbox.helper';
import { validateAsyncMainFunction } from '@sre/helpers/AWSLambdaCode.helper';

const console = Logger('ECMASandbox');
export class ECMASandbox extends CodeConnector {
public name = 'ECMASandbox';
private sandboxUrl: string;

constructor(config: { sandboxUrl: string }) {
super(config);
this.sandboxUrl = config.sandboxUrl;
}
public async prepare(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodePreparationResult> {
return {
prepared: true,
errors: [],
warnings: [],
};
}

public async deploy(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodeDeployment> {
return {
id: codeUID,
runtime: config.runtime,
createdAt: new Date(),
status: 'Deployed',
};
}

public async execute(acRequest: AccessRequest, codeUID: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
try {
const { isValid, error, parameters } = validateAsyncMainFunction(inputs.code);
if (!isValid) {
return {
output: undefined,
executionTime: 0,
success: false,
errors: [error],
}
}
const executableCode = generateExecutableCode(inputs.code, parameters, inputs.inputs);
if (!this.sandboxUrl) {
// run js code in isolated vm
console.debug('Running code in isolated vm');
const result = await runJs(executableCode);
console.debug(`Code result: ${result}`);
return {
output: result?.Output,
executionTime: 0,
success: true,
errors: [],
};
} else {
console.debug('Running code in remote sandbox');
const result: any = await axios.post(this.sandboxUrl, { code: executableCode }).catch((error) => ({ error }));
if (result.error) {

const error = result.error?.response?.data || result.error?.message || result.error.toString() || 'Unknown error';
console.error(`Error running code: ${JSON.stringify(error, null, 2)}`);
return {
output: undefined,
executionTime: 0,
success: false,
errors: [error],
};
} else {
console.debug(`Code result: ${result?.data?.Output}`);
return {
output: result.data?.Output,
executionTime: 0,
success: true,
errors: [],
};
}
}
} catch (error) {
console.error(`Error running code: ${error}`);
return {
output: undefined,
executionTime: 0,
success: false,
errors: [error],
};
}
}
public async executeDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
const result = await this.execute(acRequest, codeUID, inputs, config);
return result;
}

public async listDeployments(acRequest: AccessRequest, codeUID: string, config: CodeConfig): Promise<CodeDeployment[]> {
return [];
}

public async getDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string, config: CodeConfig): Promise<CodeDeployment | null> {
return null;
}

public async deleteDeployment(acRequest: AccessRequest, codeUID: string, deploymentId: string): Promise<void> {
return;
}

public async getResourceACL(resourceId: string, candidate: IAccessCandidate): Promise<ACL> {
const acl = new ACL();

//give Read access everytime
//FIXME: !!!!!! IMPORTANT !!!!!! this implementation have to be changed in order to reflect the security model of AWS Lambda
acl.addAccess(candidate.role, candidate.id, TAccessLevel.Read);

return acl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import { ConnectorService, ConnectorServiceProvider } from '@sre/Core/ConnectorsService';
import { TConnectorService } from '@sre/types/SRE.types';
import { AWSLambdaCode } from './connectors/AWSLambdaCode.class';
import { ECMASandbox } from './connectors/ECMASandbox.class';

export class CodeService extends ConnectorServiceProvider {
public register() {
ConnectorService.register(TConnectorService.Code, 'AWSLambda', AWSLambdaCode);
ConnectorService.register(TConnectorService.Code, 'ECMASandbox', ECMASandbox);
}
}
58 changes: 58 additions & 0 deletions packages/core/tests/unit/core/ecma-sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest';
import { setupSRE } from '../../utils/sre';
import { ConnectorService } from '@sre/Core/ConnectorsService';
import { IAccessCandidate, TAccessRole } from 'index';

setupSRE({
Code: {
Connector: 'ECMASandbox',
Settings: {
sandboxUrl: 'http://localhost:6100/run-js/v2',
},
},
Log: {
Connector: 'ConsoleLog',
},
});

describe('ECMASandbox Tests', () => {
it(
'Runs a simple code and returns the output',
async () => {
const mockCandidate: IAccessCandidate = {
id: 'test-user',
role: TAccessRole.User,
};

const codeConnector = ConnectorService.getCodeConnector('ECMASandbox');
const result = await codeConnector.agent(mockCandidate.id).execute(Date.now().toString(), {
code: `async function main(prompt) { return prompt + ' ' + 'Hello World'; }`,
inputs: {
prompt: 'Say'
}
});

const output = result.output;
expect(output).toBe('Say Hello World');
},
);
it(
'Try to run a simple code without main function',
async () => {
const mockCandidate: IAccessCandidate = {
id: 'test-user',
role: TAccessRole.User,
};

const codeConnector = ConnectorService.getCodeConnector('ECMASandbox');
const result = await codeConnector.agent(mockCandidate.id).execute(Date.now().toString(), {
code: `async function testFunction(prompt) { return prompt + ' ' + 'Hello World'; }`,
inputs: {
prompt: 'Say'
}
});
const error = result.errors;
expect(error).toContain('No main function found at root level');
},
);
});
Loading