Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ plugins/shared/*.js
plugins/plugin-schema-types.d.ts
plugins/plugin-schema-types.js
plugins/host-modules.d.ts
output-hyperagent**/**
output-hyperagent**/**scripts/bash-bundle/_tmp_bundle.js
output-hyperagent-*/
scripts/bash-bundle/_tmp_bundle.js
4 changes: 4 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ setup: ensure-tools install
build: install
@echo "✅ Build complete — run 'just start' to launch the agent"

# Rebuild the ha:bash bundle from just-bash (only needed when just-bash updates)
build-bash:
node scripts/bash-bundle/build.mjs

# Build everything in release mode (hyperlight-js, guest runtime, NAPI addon)
build-release: install-release
@echo "✅ Release build complete — run 'just start-release' to launch"
Expand Down
11 changes: 11 additions & 0 deletions builtin-modules/bash.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "bash",
"description": "Sandboxed bash interpreter powered by just-bash. Provides 40+ Unix commands (ls, grep, jq, curl, sed, awk, etc.) running entirely in JavaScript inside the Hyperlight micro-VM.",
"author": "system",
"mutable": false,
"type": "script",
"sourceHash": "sha256:c53f56d194f34b81",
"hints": {
"overview": "Pure-JS bash interpreter for the sandbox. Used internally by the execute_bash tool — not intended for direct import in handlers."
}
}
1,634 changes: 1,522 additions & 112 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"@types/node": "^25.3.3",
"@types/pngjs": "^6.0.5",
"@xarsh/ooxml-validator": "^0.1.10",
"esbuild": "^0.28.0",
"just-bash": "^2.14.4",
"pdf-parse": "^2.4.5",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
Expand Down
97 changes: 97 additions & 0 deletions scripts/bash-bundle/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// scripts/bash-bundle/build.mjs
//
// Builds the ha:bash module bundle from just-bash.
// Output: builtin-modules/bash.js (self-contained ESM module for QuickJS)
//
// Usage: node scripts/bash-bundle/build.mjs
//
// Prerequisites: npm install (just-bash and esbuild must be in node_modules)

import { execSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(__dirname, "..", "..");
const outFile = join(repoRoot, "builtin-modules", "bash.js");

// ── Step 1: esbuild bundle ──────────────────────────────────────────

console.log("Building ha:bash bundle from just-bash...");

const stubDir = __dirname;
const entryFile = join(stubDir, "entry.mjs");

// Check prerequisites
if (!existsSync(join(repoRoot, "node_modules", "just-bash"))) {
console.error(
"Error: just-bash not found in node_modules. Run npm install first.",
);
process.exit(1);
}

const aliasArgs = [
`--alias:node:zlib=${join(stubDir, "zlib-stub.mjs")}`,
`--alias:node:worker_threads=${join(stubDir, "worker-stub.mjs")}`,
`--alias:node:path=${join(stubDir, "node-path-stub.mjs")}`,
`--alias:node:dns=${join(stubDir, "dns-stub.mjs")}`,
`--alias:node:crypto=${join(stubDir, "crypto-stub.mjs")}`,
`--alias:node:url=${join(stubDir, "url-stub.mjs")}`,
`--alias:node:fs=${join(stubDir, "fs-stub.mjs")}`,
`--alias:node:fs/promises=${join(stubDir, "fs-stub.mjs")}`,
`--alias:node:child_process=${join(stubDir, "worker-stub.mjs")}`,
`--alias:node:os=${join(stubDir, "worker-stub.mjs")}`,
`--alias:node:async_hooks=${join(stubDir, "worker-stub.mjs")}`,
`--alias:turndown=${join(stubDir, "turndown-stub.mjs")}`,
`--alias:seek-bzip=${join(stubDir, "bzip-stub.mjs")}`,
`--alias:node-liblzma=${join(stubDir, "liblzma-stub.mjs")}`,
`--alias:@mongodb-js/zstd=${join(stubDir, "zstd-stub.mjs")}`,
`--alias:sql.js=${join(stubDir, "sqljs-stub.mjs")}`,
].join(" ");

const tmpBundle = join(stubDir, "_tmp_bundle.js");

execSync(
`npx esbuild ${entryFile} ` +
`--bundle --format=esm --platform=neutral --target=es2020 ` +
`--main-fields=module,main ` +
`${aliasArgs} ` +
`--outfile=${tmpBundle} ` +
`--minify --tree-shaking=true`,
{ stdio: "inherit", cwd: repoRoot },
);

// ── Step 2: Prepend polyfills ───────────────────────────────────────

const polyfills = `// @module bash
// @description Sandboxed bash interpreter (just-bash) with polyfills for QuickJS
// @author system
// @generated DO NOT EDIT — built by scripts/bash-bundle/build.mjs

// ── QuickJS Polyfills ────────────────────────────────────────────────
if(typeof globalThis.URL==='undefined'){globalThis.URL=class URL{constructor(input,base){let full=String(input);if(base&&!full.match(/^[a-z]+:\\/\\//i)){full=String(base).replace(/\\/[^\\/]*$/,'/')+full}const m=full.match(/^(https?:)\\/\\/([^\\/:]+)(:\\d+)?(\\/[^?#]*)?(\\?[^#]*)?(#.*)?$/i);if(m){this.protocol=m[1];this.hostname=m[2];this.port=m[3]?m[3].slice(1):'';this.pathname=m[4]||'/';this.search=m[5]||'';this.hash=m[6]||'';this.host=this.hostname+(this.port?':'+this.port:'');this.origin=this.protocol+'//'+this.host;this.href=this.origin+this.pathname+this.search+this.hash;this.searchParams=new URLSearchParams(this.search);this.username='';this.password=''}else{this.href=full;this.protocol='';this.hostname='';this.port='';this.pathname=full;this.search='';this.hash='';this.host='';this.origin='';this.searchParams=new URLSearchParams();this.username='';this.password=''}}toString(){return this.href}}}
if(typeof globalThis.URLSearchParams==='undefined'){globalThis.URLSearchParams=class URLSearchParams{constructor(init){this._p=[];if(typeof init==='string'){const s=init.startsWith('?')?init.slice(1):init;for(const pair of s.split('&')){const[k,v]=pair.split('=');if(k)this._p.push([decodeURIComponent(k),decodeURIComponent(v||'')])}}}get(k){const p=this._p.find(([a])=>a===k);return p?p[1]:null}has(k){return this._p.some(([a])=>a===k)}toString(){return this._p.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&')}entries(){return this._p[Symbol.iterator]()}[Symbol.iterator](){return this._p[Symbol.iterator]()}forEach(fn){this._p.forEach(([k,v])=>fn(v,k))}}}
if(typeof globalThis.Buffer==='undefined'){const _e=new TextEncoder();const _d=new TextDecoder();class HaBuffer extends Uint8Array{toString(encoding){if(!encoding||encoding==='utf-8'||encoding==='utf8')return _d.decode(this);if(encoding==='base64')return btoa(String.fromCharCode.apply(null,this));if(encoding==='latin1'||encoding==='binary'){let s='';for(let i=0;i<this.length;i++)s+=String.fromCharCode(this[i]);return s}if(encoding==='hex'){let s='';for(let i=0;i<this.length;i++)s+=this[i].toString(16).padStart(2,'0');return s}return _d.decode(this)}}globalThis.Buffer={from(d,e){if(typeof d==='string'){if(e==='base64'){const b=atob(d);const a=new HaBuffer(b.length);for(let i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return a}if(e==='hex'){const a=new HaBuffer(d.length/2);for(let i=0;i<d.length;i+=2)a[i/2]=parseInt(d.substr(i,2),16);return a}const enc=_e.encode(d);const r=new HaBuffer(enc.length);r.set(enc);return r}if(d instanceof Uint8Array){const r=new HaBuffer(d.length);r.set(d);return r}if(Array.isArray(d))return new HaBuffer(d);return new HaBuffer(0)},isBuffer(o){return o instanceof Uint8Array},concat(l){const t=l.reduce((s,b)=>s+b.length,0);const r=new HaBuffer(t);let o=0;for(const b of l){r.set(b,o);o+=b.length}return r},alloc(s){return new HaBuffer(s)},byteLength(s,e){if(typeof s==='string')return _e.encode(s).length;return s.length}}}
if(typeof globalThis.process==='undefined'){globalThis.process={env:{},nextTick(fn){queueMicrotask(fn)},execPath:'/usr/bin/node',mainModule:null,umask(){return 18},type:'renderer'}}
if(typeof globalThis.AbortController==='undefined'){globalThis.AbortController=class AbortController{constructor(){this.signal={aborted:false,addEventListener(){}}}abort(){this.signal.aborted=true}}}
// crypto.getRandomValues, crypto.randomUUID, and Math.random are provided
// natively by the Hyperlight runtime via RDRAND — no JS polyfill needed.
if(typeof globalThis.setTimeout==='undefined'){globalThis.setTimeout=(fn)=>{fn();return 0};globalThis.clearTimeout=()=>{};globalThis.setInterval=()=>0;globalThis.clearInterval=()=>{}}
if(typeof globalThis.performance==='undefined'){globalThis.performance={now(){return Date.now()}}}
// ── End Polyfills ────────────────────────────────────────────────────

`;

const bundleSource = readFileSync(tmpBundle, "utf-8");
const output = polyfills + bundleSource;
writeFileSync(outFile, output);

// Clean up temp file
try {
require("node:fs").unlinkSync(tmpBundle);
} catch {}
Comment on lines +91 to +94
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aplicado no commit c6c029a6. Helper privado removido, agora usa StringUtils.isBlank (já importado para extractFirstName).


const sizeKb = (output.length / 1024).toFixed(0);
console.log(`\\n✅ Built builtin-modules/bash.js (${sizeKb} KB)`);
1 change: 1 addition & 0 deletions scripts/bash-bundle/bzip-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { decode() { throw new Error("bzip2: not available in sandbox"); } };
19 changes: 19 additions & 0 deletions scripts/bash-bundle/crypto-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Stub for node:crypto
export function randomBytes(n) {
const buf = new Uint8Array(n);
for (let i = 0; i < n; i++) buf[i] = Math.floor(Math.random() * 256);
return buf;
}
export function randomUUID() {
const h = "0123456789abcdef";
let u = "";
for (let i = 0; i < 36; i++) {
if (i === 8 || i === 13 || i === 18 || i === 23) u += "-";
else u += h[Math.floor(Math.random() * 16)];
}
return u;
}
export function createHash() {
throw new Error("createHash: not available in sandbox");
}
export default { randomBytes, randomUUID, createHash };
8 changes: 8 additions & 0 deletions scripts/bash-bundle/dns-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Stub for node:dns — just-bash uses this for network allow-list validation
// We handle networking through host:fetch plugin, so DNS lookup is not needed
export function lookup(hostname, opts, cb) {
if (typeof opts === "function") { cb = opts; opts = {}; }
// Return a dummy address — our fetch plugin does its own SSRF checks
if (cb) cb(null, "0.0.0.0", 4);
}
export default { lookup };
4 changes: 4 additions & 0 deletions scripts/bash-bundle/entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Entry point for esbuild bundling of just-bash for QuickJS/Hyperlight
// Re-exports only what we need, stubs out node:zlib
export { Bash } from "just-bash";
export { InMemoryFs } from "just-bash";
22 changes: 22 additions & 0 deletions scripts/bash-bundle/fs-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Stub for node:fs — just-bash's ReadWriteFs uses this for real FS
// We don't use ReadWriteFs — we use our own IFileSystem adapter
// But the import exists in the bundle, so we stub it
export function readFileSync() { throw new Error("node:fs not available in sandbox"); }
export function writeFileSync() { throw new Error("node:fs not available in sandbox"); }
export function existsSync() { return false; }
export function statSync() { throw new Error("node:fs not available in sandbox"); }
export function readdirSync() { return []; }
export function mkdirSync() { throw new Error("node:fs not available in sandbox"); }
export function unlinkSync() { throw new Error("node:fs not available in sandbox"); }
export function rmdirSync() { throw new Error("node:fs not available in sandbox"); }
export function chmodSync() { throw new Error("node:fs not available in sandbox"); }
export function symlinkSync() { throw new Error("node:fs not available in sandbox"); }
export function linkSync() { throw new Error("node:fs not available in sandbox"); }
export function readlinkSync() { throw new Error("node:fs not available in sandbox"); }
export function realpathSync() { throw new Error("node:fs not available in sandbox"); }
export function lstatSync() { throw new Error("node:fs not available in sandbox"); }
export function utimesSync() { throw new Error("node:fs not available in sandbox"); }
export function copyFileSync() { throw new Error("node:fs not available in sandbox"); }
export function renameSync() { throw new Error("node:fs not available in sandbox"); }
export async function open() { throw new Error("node:fs/promises not available in sandbox"); }
export default {};
1 change: 1 addition & 0 deletions scripts/bash-bundle/liblzma-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default null;
53 changes: 53 additions & 0 deletions scripts/bash-bundle/node-path-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Minimal node:path polyfill for just-bash
export function join(...parts) {
return parts.filter(Boolean).join("/").replace(/\/+/g, "/");
}
export function resolve(...parts) {
let result = "";
for (const part of parts) {
if (part.startsWith("/")) result = part;
else result = result ? result + "/" + part : part;
}
return normalize(result || "/");
}
export function normalize(p) {
const absolute = p.startsWith("/");
const parts = p.split("/");
const out = [];
for (const part of parts) {
if (part === "..") {
if (out.length > 0) out.pop();
else if (!absolute) out.push("..");
}
else if (part !== "." && part !== "") out.push(part);
}
const normalized = (absolute ? "/" : "") + out.join("/");
return normalized || (absolute ? "/" : ".");
}
export function dirname(p) {
const i = p.lastIndexOf("/");
return i <= 0 ? (p.startsWith("/") ? "/" : ".") : p.slice(0, i);
}
export function basename(p, ext) {
let b = p.split("/").pop() || "";
if (ext && b.endsWith(ext)) b = b.slice(0, -ext.length);
return b;
}
export function extname(p) {
const b = basename(p);
const i = b.lastIndexOf(".");
return i > 0 ? b.slice(i) : "";
}
export function isAbsolute(p) { return p.startsWith("/"); }
export function relative(from, to) {
const f = resolve(from).split("/").filter(Boolean);
const t = resolve(to).split("/").filter(Boolean);
let i = 0;
while (i < f.length && i < t.length && f[i] === t[i]) i++;
const ups = f.length - i;
return [...Array(ups).fill(".."), ...t.slice(i)].join("/") || ".";
}
export const sep = "/";
export const delimiter = ":";
export const posix = { join, resolve, normalize, dirname, basename, extname, isAbsolute, relative, sep, delimiter };
export default { join, resolve, normalize, dirname, basename, extname, isAbsolute, relative, sep, delimiter, posix };
4 changes: 4 additions & 0 deletions scripts/bash-bundle/sqljs-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Stub for sql.js — sqlite3 not supported in sandbox
export default function () {
throw new Error("sqlite3: not available in sandbox");
}
5 changes: 5 additions & 0 deletions scripts/bash-bundle/turndown-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Stub for turndown (html-to-markdown) — not needed in sandbox
export default class TurndownService {
constructor() {}
turndown(html) { return html.replace(/<[^>]+>/g, ''); }
}
7 changes: 7 additions & 0 deletions scripts/bash-bundle/url-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Stub for node:url
export function fileURLToPath(url) {
if (typeof url === "string" && url.startsWith("file://")) return url.slice(7);
return String(url);
}
export function pathToFileURL(p) { return new URL("file://" + p); }
export default { fileURLToPath, pathToFileURL };
11 changes: 11 additions & 0 deletions scripts/bash-bundle/worker-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Stub for node:worker_threads and node:async_hooks
export class Worker {
constructor() { throw new Error("Workers not available in this environment"); }
}
export const parentPort = null;
export const workerData = null;
export const isMainThread = true;
export class AsyncLocalStorage {
getStore() { return undefined; }
run(store, fn) { return fn(); }
}
11 changes: 11 additions & 0 deletions scripts/bash-bundle/zlib-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Stub for node:zlib — just-bash's gzip/tar commands use this
export function gzipSync(data) {
throw new Error("gzip: not available in this environment. Use the ha:zip-format module in a JavaScript handler instead.");
}
export function gunzipSync(data) {
throw new Error("gunzip: not available in this environment. Use the ha:zip-format module in a JavaScript handler instead.");
}
export function deflateSync(data) { return gzipSync(data); }
export function inflateSync(data) { return gunzipSync(data); }
export const constants = { Z_SYNC_FLUSH: 0, Z_FINISH: 4, Z_DEFAULT_COMPRESSION: -1, Z_NO_COMPRESSION: 0, Z_BEST_COMPRESSION: 9 };
export default { gzipSync, gunzipSync, deflateSync, inflateSync, constants };
1 change: 1 addition & 0 deletions scripts/bash-bundle/zstd-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default null;
15 changes: 15 additions & 0 deletions scripts/build-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,19 @@ execSync("npx tsx scripts/generate-host-modules-dts.ts", {
stdio: "inherit",
});

// Step 9: Rebuild ha:bash bundle from just-bash (if just-bash is installed)
try {
const justBashPath = join(ROOT, "node_modules", "just-bash");
if (existsSync(justBashPath)) {
console.log("\nRebuilding ha:bash bundle from just-bash...");
execSync("node scripts/bash-bundle/build.mjs", {
cwd: ROOT,
stdio: "inherit",
});
}
} catch (e) {
console.error(" ⚠️ bash bundle build failed:", e.message);
// Non-fatal — bash is optional, core agent works without it
}
Comment on lines +143 to +156
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aplicado no commit c6c029a6. Description atualizada pra deixar explícito: somente dígitos (10-15), sem + ou caracteres especiais, com exemplo.


console.log("✓ Build complete");
1 change: 1 addition & 0 deletions skills/api-explorer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/data-processor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/mcp-services/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ allowed-tools:
- mcp_tool_info
- manage_mcp
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/pdf-expert/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/pptx-expert/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/report-builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/research-synthesiser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/web-scraper/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
1 change: 1 addition & 0 deletions skills/xlsx-expert/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ antiPatterns:
allowed-tools:
- register_handler
- execute_javascript
- execute_bash
- delete_handler
- get_handler_source
- edit_handler
Expand Down
Loading
Loading