Crates.io | hexput-runtime |
lib.rs | hexput-runtime |
version | |
source | src |
created_at | 2025-04-06 03:01:59.21734+00 |
updated_at | 2025-04-19 11:07:18.111118+00 |
description | WebSocket runtime server for Hexput AST processing |
homepage | |
repository | |
max_upload_size | |
id | 1622602 |
Cargo.toml error: | TOML parse error at line 17, column 1 | 17 | autolib = false | ^^^^^^^ unknown field `autolib`, expected one of `name`, `version`, `edition`, `authors`, `description`, `readme`, `license`, `repository`, `homepage`, `documentation`, `build`, `resolver`, `links`, `default-run`, `default_dash_run`, `rust-version`, `rust_dash_version`, `rust_version`, `license-file`, `license_dash_file`, `license_file`, `licenseFile`, `license_capital_file`, `forced-target`, `forced_dash_target`, `autobins`, `autotests`, `autoexamples`, `autobenches`, `publish`, `metadata`, `keywords`, `categories`, `exclude`, `include` |
size | 0 |
A WebSocket server for parsing and executing Hexput AST code with configurable security constraints.
Hexput Runtime is a Rust-based execution environment that allows clients to send code via WebSocket connections and receive execution results. The runtime provides:
# Clone the repository
git clone https://github.com/hexput/main hexput-main
cd hexput-main/hexput-runtime
# Build the project
cargo build -r
# Run the server
../target/release/hexput-runtime
# Run with default settings (127.0.0.1:9001)
./hexput-runtime
# Specify address and port
./hexput-runtime --address 0.0.0.0 --port 9001
# Enable debug logging
./hexput-runtime --debug
# Set specific log level
./hexput-runtime --log-level debug
To connect to the Hexput Runtime server, use a WebSocket client to connect to the server's address and port. When the connection is established, the server will send a welcome message:
{"type":"connection","status":"connected"}
For reliable WebSocket communication:
Connection Establishment:
Message Handling:
Connection Management:
Error Handling:
The server accepts the following request types:
{
"id": "unique-request-id",
"action": "parse",
"code": "vl x = 10;",
"options": {
"minify": true,
"include_source_mapping": false,
"no_object_constructions": false
}
}
{
"id": "unique-request-id",
"action": "execute",
"code": "vl x = 10; return x * 2;",
"options": {
"no_loops": true,
"no_callbacks": true
},
"context": {
"initialValue": 5
},
"secret_context": { // Optional: Data passed only to remote functions
"apiKey": "sensitive-key-123"
}
}
{
"id": "unique-request-id",
"success": true,
"result": { /* AST representation */ }
}
{
"id": "unique-request-id",
"success": true,
"result": { /* Execution result */ }
}
{
"id": "unique-request-id",
"success": false,
"error": "Error message with details"
}
Function Existence Check (Server -> Client): When the runtime needs to call a function not defined locally.
{
"id": "check-uuid",
"action": "is_function_exists",
"function_name": "calculateTotal"
}
Function Existence Response (Client -> Server): Client confirms if it handles the function.
{
"id": "check-uuid",
"exists": true
}
Function Call Request (Server -> Client): If the function exists, the server requests its execution.
{
"id": "call-uuid",
"function_name": "calculateTotal",
"arguments": [10, 20, {"tax": 0.05}],
"secret_context": { "apiKey": "sensitive-key-123" } // Included if provided in original execute request
}
Function Call Response (Client -> Server): Client returns the result of the function execution.
{
"id": "call-uuid",
"result": { /* Function result */ },
"error": null /* or error message */
}
The execute
request accepts an optional secret_context
field. This field allows the client initiating the execution to provide sensitive data (like API keys, user tokens, etc.) that should be made available only to remote functions called by the script, but not directly accessible within the script's execution environment itself.
is_function_exists
followed by the function call request), the secret_context
provided in the original execute
request is included in the FunctionCallRequest
sent to the client handling the remote function.secret_context
directly.Example usage in the client handling the remote call:
// In the client's message handler for function calls
handleMessage(data) {
const message = JSON.parse(data);
if (message.function_name && message.arguments) {
const handler = this.callHandlers[message.function_name];
if (handler) {
// Access secret context if needed by the handler
const secretContext = message.secret_context;
console.log("Secret context received:", secretContext);
// Execute handler, potentially using secretContext
// ... handler(...message.arguments, secretContext) ...
}
// ... rest of the handler ...
}
// ... other message handling ...
}
One of the most powerful features of Hexput Runtime is remote function calling. This capability allows code executing in the runtime to call functions that are implemented on the client side, enabling sandboxed code to safely interact with the host environment.
is_function_exists
) request to the client, including a unique ID.exists
field.exists: true
), the runtime sends a function call request. This includes a new unique ID, the function name, and the evaluated arguments.result
(or an error
if something went wrong).FunctionNotFoundError
.Check if Function Exists:
{"id": "check-uuid", "action": "is_function_exists", "function_name": "myFunction"}
{"id": "check-uuid", "exists": true}
or {"id": "check-uuid", "exists": false}
Call Function (only if exists
was true):
{"id": "call-uuid", "function_name": "myFunction", "arguments": [arg1, arg2, ...]}
{"id": "call-uuid", "result": functionResult}
or {"id": "call-uuid", "result": null, "error": "Error message"}
This example shows how to implement a client that handles remote function calls according to the protocol:
// ... (HexputClient class definition remains the same) ...
handleMessage(data) {
const message = JSON.parse(data);
// Handle function existence check from server
if (message.action === "is_function_exists") {
const functionName = message.function_name;
const exists = typeof this.callHandlers[functionName] === "function";
console.log(`Runtime checking existence of '${functionName}': ${exists}`);
this.ws.send(JSON.stringify({
id: message.id, // Use the ID from the server's request
exists: exists
}));
return;
}
// Handle function call request from server
if (message.function_name && message.arguments) {
const functionName = message.function_name;
const handler = this.callHandlers[functionName];
console.log(`Runtime calling function '${functionName}' with args:`, message.arguments);
if (handler) {
try {
// Handle both sync and async handlers
Promise.resolve(handler(...message.arguments))
.then(result => {
this.ws.send(JSON.stringify({
id: message.id, // Use the ID from the server's request
result: result === undefined ? null : result // Ensure result is not undefined
}));
})
.catch(error => {
console.error(`Error executing remote function '${functionName}':`, error);
this.ws.send(JSON.stringify({
id: message.id,
result: null,
error: error instanceof Error ? error.message : String(error)
}));
});
} catch (error) { // Catch synchronous errors
console.error(`Synchronous error executing remote function '${functionName}':`, error);
this.ws.send(JSON.stringify({
id: message.id,
result: null,
error: error instanceof Error ? error.message : String(error)
}));
}
} else {
// Should ideally not happen if existence check works, but handle defensively
console.warn(`Received call for unknown function '${functionName}'`);
this.ws.send(JSON.stringify({
id: message.id,
result: null,
error: `Function '${functionName}' not found on client.`
}));
}
return;
}
// Handle response to our own requests (e.g., execute)
if (message.id && this.responseHandlers[message.id]) {
console.log(`Received response for request ID '${message.id}'`);
this.responseHandlers[message.id](message);
delete this.responseHandlers[message.id];
return;
}
// Handle connection status messages or other types
if (message.type === 'connection' && message.status === 'connected') {
console.log("Successfully connected to Hexput Runtime.");
return;
}
console.warn("Received unhandled message:", message);
}
// ... (registerFunction, execute methods remain the same) ...
}
// ... (Usage example remains the same) ...
The example client implementation already supports asynchronous functions (returning Promises) in handlers. The client will wait for the Promise to resolve or reject before sending the result back to the runtime.
client.registerFunction("fetchUserData", async (userId) => {
console.log(`Fetching user data for ${userId}`);
// The client waits for this Promise to resolve
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user data: ${response.statusText}`);
}
const data = await response.json();
console.log(`User data fetched for ${userId}:`, data);
return data; // This data will be sent back to the runtime
});
// In the runtime code:
// vl userData = fetchUserData(1); // Calls the async client function
// return userData.name;
When implementing remote function calling:
By carefully implementing these patterns, you can safely bridge between sandboxed code and your application's functionality.
Hexput Runtime offers configurable security constraints via the options
field in parse
and execute
requests to restrict what code can do:
no_object_constructions
: Prevents creating new objects ({}
).no_array_constructions
: Prevents creating new arrays ([]
).no_object_navigation
: Prevents accessing object properties (obj.prop
, obj['prop']
).no_variable_declaration
: Prevents declaring new variables (vl x = ...
).no_loops
: Prevents using loop constructs (loop item in list { ... }
).no_object_keys
: Prevents getting object keys (keysOf obj
).no_callbacks
: Prevents defining (callback name() { ... }
) and using callbacks.no_conditionals
: Prevents using if/else statements (if condition { ... }
).no_return_statements
: Prevents using return statements (return value
).no_loop_control
: Prevents using break/continue (end
, continue
).no_operators
: Prevents using mathematical operators (+
, -
, *
, /
).no_equality
: Prevents using equality and comparison operators (==
, <
, >
, <=
, >=
).no_assignments
: Prevents assigning values to variables (x = value
, obj.prop = value
).Client code to execute a simple expression:
const ws = new WebSocket('ws://localhost:9001');
ws.onopen = () => {
console.log("WebSocket connected");
ws.send(JSON.stringify({
id: "req-1",
action: "execute",
code: "vl result = 5 + 10; return result;",
options: {} // Default options (all features enabled)
}));
};
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
// Ignore connection message
if (response.type === 'connection') return;
console.log('Execution result:', response);
// Example output: { id: 'req-1', success: true, result: 15, error: null }
ws.close();
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
ws.onclose = () => {
console.log("WebSocket closed");
};
// Assumes HexputClient class is defined as shown previously
const client = new HexputClient("ws://localhost:9001");
// Register a function the runtime can call
client.registerFunction("calculateTotal", (base, tax) => {
console.log(`Client executing calculateTotal(${base}, ${tax})`);
if (typeof base !== 'number' || typeof tax !== 'number') {
throw new Error("Invalid arguments for calculateTotal");
}
return base + (base * tax);
});
// Wait for connection before executing
setTimeout(() => {
if (client.ws.readyState === WebSocket.OPEN) {
client.execute(`
vl price = 100;
vl taxRate = 0.07;
// This will trigger the remote function call protocol
vl total = calculateTotal(price, taxRate);
return total;
`)
.then(result => {
console.log("Execution result from runtime:", result); // Should be 107
})
.catch(error => {
console.error("Execution error from runtime:", error);
});
} else {
console.error("WebSocket not open. Cannot execute code.");
}
}, 1000); // Simple delay to allow connection
The runtime includes built-in methods for common data types, callable using member call syntax (e.g., "hello".toUpperCase()
).
length()
, len()
: Returns string length (number).isEmpty()
: Checks if the string is empty (boolean).substring(start, end)
: Extracts a portion of the string (string). end
is optional. Indices are 0-based.toLowerCase()
: Converts to lowercase (string).toUpperCase()
: Converts to uppercase (string).trim()
: Removes whitespace from both ends (string).includes(substring)
, contains(substring)
: Checks if string contains a substring (boolean).startsWith(prefix)
: Checks if string starts with prefix (boolean).endsWith(suffix)
: Checks if string ends with suffix (boolean).indexOf(substring)
: Returns the position (0-based index) of the first occurrence, or -1 if not found (number).split(delimiter)
: Splits string into an array of strings based on the delimiter (array).replace(old, new)
: Replaces occurrences of old
string with new
string (string).length()
, len()
: Returns array length (number).isEmpty()
: Checks if the array is empty (boolean).join(separator)
: Joins array elements into a string using the separator (string). Elements are converted to strings.first()
: Returns the first element, or null
if empty.last()
: Returns the last element, or null
if empty.includes(item)
, contains(item)
: Checks if array contains an item (uses simple equality check) (boolean).slice(start, end)
: Extracts a portion of the array (array). end
is optional. Indices are 0-based.keys()
: Returns an array of the object's property names (strings) (array).values()
: Returns an array of the object's property values (array).isEmpty()
: Checks if the object has no properties (boolean).has(key)
: Checks if the object has a specific property key (string) (boolean).entries()
: Returns an array of [key, value]
pairs (array of arrays).toString()
: Converts the number to its string representation (string).toFixed(digits)
: Formats the number using fixed-point notation (string). Requires one number argument for digits.isInteger()
: Checks if the number is an integer (boolean).abs()
: Returns the absolute value of the number (number).toString()
: Converts the boolean to "true"
or "false"
(string).toString()
: Returns the string "null"
(string).