web-agent-bridge 2.8.0 → 3.0.0
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/package.json +1 -1
- package/sdk/package.json +1 -1
- package/server/index.js +4 -0
- package/server/routes/runtime.js +401 -3
- package/server/runtime/container-worker.js +111 -0
- package/server/runtime/container.js +448 -0
- package/server/runtime/distributed-worker.js +362 -0
- package/server/runtime/index.js +21 -1
- package/server/runtime/queue.js +599 -0
- package/server/runtime/replay.js +431 -29
- package/server/runtime/scheduler.js +194 -55
- package/server/services/cluster.js +894 -0
package/package.json
CHANGED
package/sdk/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { maybeBootstrapAdmin, db } = require('./models/db');
|
|
|
15
15
|
const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
|
|
16
16
|
const { processMessage: agentChat } = require('./services/agent-chat');
|
|
17
17
|
const agentTasks = require('./services/agent-tasks');
|
|
18
|
+
const { cluster } = require('./services/cluster');
|
|
18
19
|
|
|
19
20
|
const authRoutes = require('./routes/auth');
|
|
20
21
|
const apiRoutes = require('./routes/api');
|
|
@@ -359,6 +360,9 @@ if (process.env.NODE_ENV !== 'test') {
|
|
|
359
360
|
// Start Agent OS runtime
|
|
360
361
|
runtime.start();
|
|
361
362
|
|
|
363
|
+
// Start Cluster Orchestrator
|
|
364
|
+
cluster.start();
|
|
365
|
+
|
|
362
366
|
server.listen(PORT, () => {
|
|
363
367
|
console.log(`\n ╔══════════════════════════════════════════╗`);
|
|
364
368
|
console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
|
package/server/routes/runtime.js
CHANGED
|
@@ -36,6 +36,7 @@ const { hostedRuntime } = require('../services/hosted-runtime');
|
|
|
36
36
|
const { sessionEngine } = require('../runtime/session-engine');
|
|
37
37
|
const vision = require('../services/vision');
|
|
38
38
|
const { lfdEngine } = require('../services/lfd');
|
|
39
|
+
const { cluster, distributor } = require('../services/cluster');
|
|
39
40
|
|
|
40
41
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
42
|
// AUTH MIDDLEWARE
|
|
@@ -60,9 +61,7 @@ const PUBLIC_PATHS = [
|
|
|
60
61
|
'/recipes',
|
|
61
62
|
'/vision/models',
|
|
62
63
|
'/vision/extraction-script',
|
|
63
|
-
'/
|
|
64
|
-
'/vision/models',
|
|
65
|
-
'/vision/extraction-script',
|
|
64
|
+
'/cluster/status',
|
|
66
65
|
];
|
|
67
66
|
|
|
68
67
|
function authMiddleware(req, res, next) {
|
|
@@ -543,6 +542,7 @@ router.get('/observability/health', (req, res) => {
|
|
|
543
542
|
health.hostedRuntime = hostedRuntime.getStats();
|
|
544
543
|
health.metering = metering.getStats();
|
|
545
544
|
health.lfd = lfdEngine.getStats();
|
|
545
|
+
health.cluster = cluster.getClusterStatus();
|
|
546
546
|
res.json(health);
|
|
547
547
|
});
|
|
548
548
|
|
|
@@ -1746,4 +1746,402 @@ router.get('/lfd/stats', (req, res) => {
|
|
|
1746
1746
|
res.json(lfdEngine.getStats());
|
|
1747
1747
|
});
|
|
1748
1748
|
|
|
1749
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1750
|
+
// CLUSTER — DISTRIBUTED EXECUTION & WORKER NODES
|
|
1751
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Get cluster status (public)
|
|
1755
|
+
*/
|
|
1756
|
+
router.get('/cluster/status', (req, res) => {
|
|
1757
|
+
res.json(cluster.getClusterStatus());
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Register a worker node
|
|
1762
|
+
*/
|
|
1763
|
+
router.post('/cluster/nodes', (req, res) => {
|
|
1764
|
+
try {
|
|
1765
|
+
const result = cluster.registerNode({
|
|
1766
|
+
name: req.body.name,
|
|
1767
|
+
endpoint: req.body.endpoint,
|
|
1768
|
+
region: req.body.region,
|
|
1769
|
+
zone: req.body.zone,
|
|
1770
|
+
role: req.body.role,
|
|
1771
|
+
capacity: req.body.capacity,
|
|
1772
|
+
tags: req.body.tags,
|
|
1773
|
+
hardware: req.body.hardware,
|
|
1774
|
+
version: req.body.version,
|
|
1775
|
+
secret: req.body.secret,
|
|
1776
|
+
});
|
|
1777
|
+
res.json(result);
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
res.status(400).json({ error: err.message });
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* List cluster nodes
|
|
1785
|
+
*/
|
|
1786
|
+
router.get('/cluster/nodes', (req, res) => {
|
|
1787
|
+
const nodes = cluster.listNodes({
|
|
1788
|
+
region: req.query.region,
|
|
1789
|
+
active: req.query.active === 'true',
|
|
1790
|
+
});
|
|
1791
|
+
res.json({ nodes });
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Get a specific node
|
|
1796
|
+
*/
|
|
1797
|
+
router.get('/cluster/nodes/:nodeId', (req, res) => {
|
|
1798
|
+
const node = cluster.getNode(req.params.nodeId);
|
|
1799
|
+
if (!node) return res.status(404).json({ error: 'Node not found' });
|
|
1800
|
+
res.json(node);
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Remove a node
|
|
1805
|
+
*/
|
|
1806
|
+
router.delete('/cluster/nodes/:nodeId', (req, res) => {
|
|
1807
|
+
const result = cluster.deregisterNode(req.params.nodeId);
|
|
1808
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1809
|
+
res.json(result);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Worker heartbeat
|
|
1814
|
+
*/
|
|
1815
|
+
router.post('/cluster/nodes/:nodeId/heartbeat', (req, res) => {
|
|
1816
|
+
const result = cluster.heartbeat(req.params.nodeId, {
|
|
1817
|
+
capacityUsed: req.body.capacityUsed,
|
|
1818
|
+
capacityTotal: req.body.capacityTotal,
|
|
1819
|
+
hardware: req.body.hardware,
|
|
1820
|
+
tags: req.body.tags,
|
|
1821
|
+
version: req.body.version,
|
|
1822
|
+
});
|
|
1823
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1824
|
+
res.json(result);
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Drain a node (stop new tasks, wait for running)
|
|
1829
|
+
*/
|
|
1830
|
+
router.post('/cluster/nodes/:nodeId/drain', (req, res) => {
|
|
1831
|
+
const result = cluster.drainNode(req.params.nodeId);
|
|
1832
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1833
|
+
res.json(result);
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Cordon a node (prevent scheduling)
|
|
1838
|
+
*/
|
|
1839
|
+
router.post('/cluster/nodes/:nodeId/cordon', (req, res) => {
|
|
1840
|
+
const result = cluster.cordonNode(req.params.nodeId);
|
|
1841
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1842
|
+
res.json(result);
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Uncordon a node (allow scheduling again)
|
|
1847
|
+
*/
|
|
1848
|
+
router.post('/cluster/nodes/:nodeId/uncordon', (req, res) => {
|
|
1849
|
+
const result = cluster.uncordonNode(req.params.nodeId);
|
|
1850
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1851
|
+
res.json(result);
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Submit a task for distributed execution
|
|
1856
|
+
*/
|
|
1857
|
+
router.post('/cluster/tasks', (req, res) => {
|
|
1858
|
+
try {
|
|
1859
|
+
const result = distributor.submit({
|
|
1860
|
+
type: req.body.type,
|
|
1861
|
+
objective: req.body.objective,
|
|
1862
|
+
params: req.body.params,
|
|
1863
|
+
priority: req.body.priority,
|
|
1864
|
+
affinityTags: req.body.affinityTags,
|
|
1865
|
+
affinityRegion: req.body.affinityRegion,
|
|
1866
|
+
timeout: req.body.timeout,
|
|
1867
|
+
maxAttempts: req.body.maxAttempts,
|
|
1868
|
+
externalId: req.body.externalId,
|
|
1869
|
+
});
|
|
1870
|
+
res.json(result);
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
res.status(400).json({ error: err.message });
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Get task details
|
|
1878
|
+
*/
|
|
1879
|
+
router.get('/cluster/tasks/:taskId', (req, res) => {
|
|
1880
|
+
const task = cluster.getTask(req.params.taskId);
|
|
1881
|
+
if (!task) return res.status(404).json({ error: 'Task not found' });
|
|
1882
|
+
res.json(task);
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* List tasks
|
|
1887
|
+
*/
|
|
1888
|
+
router.get('/cluster/tasks', (req, res) => {
|
|
1889
|
+
const tasks = cluster.listTasks({
|
|
1890
|
+
status: req.query.status,
|
|
1891
|
+
nodeId: req.query.nodeId,
|
|
1892
|
+
limit: parseInt(req.query.limit) || 50,
|
|
1893
|
+
});
|
|
1894
|
+
res.json({ tasks });
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* Worker pulls tasks (poll-based)
|
|
1899
|
+
*/
|
|
1900
|
+
router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
|
|
1901
|
+
const tasks = distributor.pullTasks(req.params.nodeId, parseInt(req.body.limit) || 5);
|
|
1902
|
+
res.json({ tasks });
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Worker reports task started
|
|
1907
|
+
*/
|
|
1908
|
+
router.post('/cluster/tasks/:taskId/started', (req, res) => {
|
|
1909
|
+
const result = cluster.reportTaskStarted(req.params.taskId);
|
|
1910
|
+
if (!result) return res.status(404).json({ error: 'Task not found' });
|
|
1911
|
+
res.json(result);
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Worker reports task completed
|
|
1916
|
+
*/
|
|
1917
|
+
router.post('/cluster/tasks/:taskId/completed', (req, res) => {
|
|
1918
|
+
const result = cluster.reportTaskCompleted(req.params.taskId, req.body.result);
|
|
1919
|
+
if (!result) return res.status(404).json({ error: 'Task not found' });
|
|
1920
|
+
res.json(result);
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Worker reports task failed
|
|
1925
|
+
*/
|
|
1926
|
+
router.post('/cluster/tasks/:taskId/failed', (req, res) => {
|
|
1927
|
+
const result = cluster.reportTaskFailed(req.params.taskId, req.body.error);
|
|
1928
|
+
if (!result) return res.status(404).json({ error: 'Task not found' });
|
|
1929
|
+
res.json(result);
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Get cluster events log
|
|
1934
|
+
*/
|
|
1935
|
+
router.get('/cluster/events', (req, res) => {
|
|
1936
|
+
const events = cluster.getEvents(
|
|
1937
|
+
parseInt(req.query.limit) || 100,
|
|
1938
|
+
req.query.nodeId || null
|
|
1939
|
+
);
|
|
1940
|
+
res.json({ events });
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1944
|
+
// CONTAINER ISOLATION
|
|
1945
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1946
|
+
|
|
1947
|
+
let containerRunner;
|
|
1948
|
+
try { containerRunner = require('../runtime/container').containerRunner; } catch {}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* Run a task in an isolated container
|
|
1952
|
+
*/
|
|
1953
|
+
router.post('/containers/run', async (req, res) => {
|
|
1954
|
+
if (!containerRunner) return res.status(501).json({ error: 'Container isolation not available' });
|
|
1955
|
+
try {
|
|
1956
|
+
const result = await containerRunner.runInProcess(
|
|
1957
|
+
req.body.taskId || `ctr_task_${Date.now()}`,
|
|
1958
|
+
req.body.code || '',
|
|
1959
|
+
{
|
|
1960
|
+
params: req.body.params || {},
|
|
1961
|
+
timeout: req.body.timeout || 60000,
|
|
1962
|
+
maxMemory: req.body.maxMemory || 256 * 1024 * 1024,
|
|
1963
|
+
allowNetwork: req.body.allowNetwork !== false,
|
|
1964
|
+
}
|
|
1965
|
+
);
|
|
1966
|
+
res.json(result);
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
res.status(500).json({ error: err.message });
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* List active containers
|
|
1974
|
+
*/
|
|
1975
|
+
router.get('/containers', (req, res) => {
|
|
1976
|
+
if (!containerRunner) return res.json({ containers: [] });
|
|
1977
|
+
res.json({ containers: containerRunner.listContainers() });
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* Get container details
|
|
1982
|
+
*/
|
|
1983
|
+
router.get('/containers/:containerId', (req, res) => {
|
|
1984
|
+
if (!containerRunner) return res.status(404).json({ error: 'Not found' });
|
|
1985
|
+
const c = containerRunner.getContainer(req.params.containerId);
|
|
1986
|
+
if (!c) return res.status(404).json({ error: 'Container not found' });
|
|
1987
|
+
res.json(c);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
/**
|
|
1991
|
+
* Kill a container
|
|
1992
|
+
*/
|
|
1993
|
+
router.post('/containers/:containerId/kill', (req, res) => {
|
|
1994
|
+
if (!containerRunner) return res.status(404).json({ error: 'Not found' });
|
|
1995
|
+
const ok = containerRunner.kill(req.params.containerId);
|
|
1996
|
+
res.json({ success: ok });
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Container stats
|
|
2001
|
+
*/
|
|
2002
|
+
router.get('/containers/stats/summary', (req, res) => {
|
|
2003
|
+
if (!containerRunner) return res.json({ active: 0 });
|
|
2004
|
+
res.json(containerRunner.getStats());
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Check Docker availability
|
|
2009
|
+
*/
|
|
2010
|
+
router.get('/containers/docker/status', (req, res) => {
|
|
2011
|
+
if (!containerRunner) return res.json({ available: false });
|
|
2012
|
+
res.json({ available: containerRunner.isDockerAvailable() });
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2016
|
+
// EXTERNAL QUEUE MANAGEMENT
|
|
2017
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2018
|
+
|
|
2019
|
+
let queueModule;
|
|
2020
|
+
try { queueModule = require('../runtime/queue'); } catch {}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* Queue stats
|
|
2024
|
+
*/
|
|
2025
|
+
router.get('/queue/stats', (req, res) => {
|
|
2026
|
+
if (!queueModule) return res.json({ backend: 'memory' });
|
|
2027
|
+
const q = queueModule.createQueue('scheduler');
|
|
2028
|
+
res.json(q.getStats());
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Purge completed items from queue
|
|
2033
|
+
*/
|
|
2034
|
+
router.post('/queue/purge', (req, res) => {
|
|
2035
|
+
if (!queueModule) return res.json({ purged: 0 });
|
|
2036
|
+
const q = queueModule.createQueue('scheduler');
|
|
2037
|
+
const purged = q.purgeCompleted(parseInt(req.body.maxAge) || 3600_000);
|
|
2038
|
+
res.json({ purged });
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2042
|
+
// ENHANCED REPLAY
|
|
2043
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Export a recording (full data for download)
|
|
2047
|
+
*/
|
|
2048
|
+
router.get('/replay/recordings/:taskId/export', (req, res) => {
|
|
2049
|
+
const data = replayEngine.exportRecording(req.params.taskId);
|
|
2050
|
+
if (!data) return res.status(404).json({ error: 'Recording not found' });
|
|
2051
|
+
res.json(data);
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Import a recording
|
|
2056
|
+
*/
|
|
2057
|
+
router.post('/replay/recordings/import', (req, res) => {
|
|
2058
|
+
try {
|
|
2059
|
+
const id = replayEngine.importRecording(req.body);
|
|
2060
|
+
res.json({ success: true, recordingId: id });
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
res.status(400).json({ error: err.message });
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Delete a recording
|
|
2068
|
+
*/
|
|
2069
|
+
router.delete('/replay/recordings/:taskId', (req, res) => {
|
|
2070
|
+
replayEngine.deleteRecording(req.params.taskId);
|
|
2071
|
+
res.json({ success: true });
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Replay from a specific checkpoint
|
|
2076
|
+
*/
|
|
2077
|
+
router.post('/replay/:taskId/from-checkpoint', async (req, res) => {
|
|
2078
|
+
try {
|
|
2079
|
+
const result = await replayEngine.replay(req.params.taskId, {
|
|
2080
|
+
verify: req.body.verify !== false,
|
|
2081
|
+
continueOnMismatch: !!req.body.continueOnMismatch,
|
|
2082
|
+
fromCheckpoint: req.body.checkpoint,
|
|
2083
|
+
});
|
|
2084
|
+
res.json(result);
|
|
2085
|
+
} catch (err) {
|
|
2086
|
+
res.status(400).json({ error: err.message });
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Purge old recordings
|
|
2092
|
+
*/
|
|
2093
|
+
router.post('/replay/purge', (req, res) => {
|
|
2094
|
+
const maxAge = parseInt(req.body.maxAge) || 7 * 24 * 3600_000;
|
|
2095
|
+
replayEngine.purgeOld(maxAge);
|
|
2096
|
+
res.json({ success: true });
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2100
|
+
// WORKER PULL ENDPOINT (for distributed workers)
|
|
2101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2102
|
+
|
|
2103
|
+
/**
|
|
2104
|
+
* Workers pull tasks from here
|
|
2105
|
+
*/
|
|
2106
|
+
router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
|
|
2107
|
+
const limit = parseInt(req.body.limit) || 5;
|
|
2108
|
+
// Fetch pending tasks from the cluster task distributor
|
|
2109
|
+
const tasks = [];
|
|
2110
|
+
try {
|
|
2111
|
+
const pending = distributor.getPendingTasks ? distributor.getPendingTasks(req.params.nodeId, limit) : [];
|
|
2112
|
+
tasks.push(...pending);
|
|
2113
|
+
} catch {}
|
|
2114
|
+
res.json({ tasks });
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
/**
|
|
2118
|
+
* Worker reports task started
|
|
2119
|
+
*/
|
|
2120
|
+
router.post('/cluster/tasks/:taskId/started', (req, res) => {
|
|
2121
|
+
bus.emit('cluster.task.started', { taskId: req.params.taskId, nodeId: req.body.nodeId });
|
|
2122
|
+
res.json({ ok: true });
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* Worker reports task completed
|
|
2127
|
+
*/
|
|
2128
|
+
router.post('/cluster/tasks/:taskId/completed', (req, res) => {
|
|
2129
|
+
bus.emit('cluster.task.completed', {
|
|
2130
|
+
taskId: req.params.taskId,
|
|
2131
|
+
result: req.body.result,
|
|
2132
|
+
});
|
|
2133
|
+
res.json({ ok: true });
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
/**
|
|
2137
|
+
* Worker reports task failed
|
|
2138
|
+
*/
|
|
2139
|
+
router.post('/cluster/tasks/:taskId/failed', (req, res) => {
|
|
2140
|
+
bus.emit('cluster.task.failed', {
|
|
2141
|
+
taskId: req.params.taskId,
|
|
2142
|
+
error: req.body.error,
|
|
2143
|
+
});
|
|
2144
|
+
res.json({ ok: true });
|
|
2145
|
+
});
|
|
2146
|
+
|
|
1749
2147
|
module.exports = router;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Container Worker — Runs inside a forked child process
|
|
5
|
+
*
|
|
6
|
+
* This script is the entry point for process-isolated task execution.
|
|
7
|
+
* It reads the task definition from a JSON file, executes it,
|
|
8
|
+
* and sends results back via IPC.
|
|
9
|
+
*
|
|
10
|
+
* Security:
|
|
11
|
+
* - Runs in a separate process with memory limits (--max-old-space-size)
|
|
12
|
+
* - Limited filesystem access (only its tmp directory)
|
|
13
|
+
* - Can disable network via environment
|
|
14
|
+
* - Timeout enforced by parent
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const taskFile = process.argv[2];
|
|
21
|
+
if (!taskFile) {
|
|
22
|
+
process.stderr.write('No task file specified\n');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let taskData;
|
|
27
|
+
try {
|
|
28
|
+
taskData = JSON.parse(fs.readFileSync(taskFile, 'utf8'));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
process.stderr.write(`Failed to read task file: ${err.message}\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Sandbox Utilities (available to task code) ──────────────────────
|
|
35
|
+
|
|
36
|
+
const sandbox = {
|
|
37
|
+
taskId: taskData.taskId,
|
|
38
|
+
containerId: taskData.containerId,
|
|
39
|
+
params: taskData.params || {},
|
|
40
|
+
|
|
41
|
+
// Send progress updates
|
|
42
|
+
progress(pct) {
|
|
43
|
+
if (process.send) process.send({ type: 'progress', progress: pct });
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Send log messages
|
|
47
|
+
log(message) {
|
|
48
|
+
if (process.send) process.send({ type: 'log', message: String(message).slice(0, 1000) });
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Read a param
|
|
52
|
+
param(key, defaultValue) {
|
|
53
|
+
return taskData.params[key] !== undefined ? taskData.params[key] : defaultValue;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Filesystem is restricted to tmpDir
|
|
57
|
+
tmpDir: path.dirname(taskFile),
|
|
58
|
+
|
|
59
|
+
readFile(name) {
|
|
60
|
+
const p = path.join(sandbox.tmpDir, path.basename(name));
|
|
61
|
+
return fs.readFileSync(p, 'utf8');
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
writeFile(name, content) {
|
|
65
|
+
const p = path.join(sandbox.tmpDir, path.basename(name));
|
|
66
|
+
fs.writeFileSync(p, content);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ─── Execute Task ────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
async function execute() {
|
|
73
|
+
try {
|
|
74
|
+
let result;
|
|
75
|
+
|
|
76
|
+
if (taskData.code) {
|
|
77
|
+
// Execute provided code string in a restricted scope
|
|
78
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
79
|
+
const fn = new AsyncFunction('sandbox', 'params', taskData.code);
|
|
80
|
+
result = await fn(sandbox, taskData.params);
|
|
81
|
+
} else if (taskData.module) {
|
|
82
|
+
// Execute a module (for trusted internal tasks)
|
|
83
|
+
const mod = require(taskData.module);
|
|
84
|
+
if (typeof mod.execute === 'function') {
|
|
85
|
+
result = await mod.execute(taskData.params, sandbox);
|
|
86
|
+
} else {
|
|
87
|
+
result = { error: 'Module has no execute() function' };
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
result = { echo: taskData.params, message: 'No code or module specified' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Send result back via IPC
|
|
94
|
+
if (process.send) {
|
|
95
|
+
process.send({ type: 'result', data: result });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Give IPC time to flush
|
|
99
|
+
setTimeout(() => process.exit(0), 100);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
process.stderr.write(`Task error: ${err.message}\n${err.stack}\n`);
|
|
102
|
+
|
|
103
|
+
if (process.send) {
|
|
104
|
+
process.send({ type: 'result', data: null });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setTimeout(() => process.exit(1), 100);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
execute();
|