2708 lines
93 KiB
JavaScript
2708 lines
93 KiB
JavaScript
/** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* Host-checkout verification hints (keep loosely aligned with scripts/lib/agent-check-hints-data.mjs).
|
||
* @param {string} combined paths + topic
|
||
* @returns {string[]}
|
||
*/
|
||
function bareAgentVerificationHintsList(combined) {
|
||
const c = String(combined || '').toLowerCase()
|
||
/** @type {string[]} */
|
||
const hints = []
|
||
if (/(^|\/)kernel\/|kernel\\|\/boot\/init|lib\/init|lib\\init/.test(c)) {
|
||
hints.push('npm run bundle:kernel', 'node scripts/verify-kernel-seeder-parity.mjs')
|
||
}
|
||
if (/bare-os-coreutils|kernel\/bin|kernel\\bin/.test(c)) {
|
||
hints.push('npm run build -w bare-os-coreutils', 'node scripts/verify-man-coverage.mjs')
|
||
}
|
||
if (/bare-os-booter|bare-os-ctx-api/.test(c)) {
|
||
hints.push(
|
||
'npm run test -w bare-os-booter',
|
||
'node scripts/verify-ctx-api-feature-bits.mjs'
|
||
)
|
||
}
|
||
if (/bare-os-protocol|seed-rpc|channel\.js/.test(c)) {
|
||
hints.push('npm run test -w bare-os-protocol')
|
||
}
|
||
if (/bare-os-seeder/.test(c)) {
|
||
hints.push('node scripts/verify-kernel-seeder-parity.mjs')
|
||
}
|
||
if (/bare-os-bare-libs|kernel\/lib\/bare|kernel\\lib\\bare/.test(c)) {
|
||
hints.push('npm run build -w bare-os-bare-libs', 'node scripts/verify-bundle-health.mjs')
|
||
}
|
||
if (/shell|sh\.js|test\.js/.test(c) && /booter/.test(c)) {
|
||
hints.push('npm run test:shell-fast')
|
||
}
|
||
if (/docs\/|handbook\/|developer-guide\//.test(c)) {
|
||
hints.push('npm run pretest', 'node scripts/verify-doc-links.mjs')
|
||
}
|
||
if (!hints.length) hints.push('npm run pretest', 'npm test')
|
||
return [...new Set(hints)]
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentShellQuote(s) {
|
||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentJsonResult(v) {
|
||
try {
|
||
return JSON.stringify(v)
|
||
} catch {
|
||
return '{"error":"json_stringify_failed"}'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentToolDefinitions() {
|
||
return [
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_file',
|
||
description:
|
||
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute file path' },
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes to read (default 256000)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'write_file',
|
||
description:
|
||
'Create or overwrite a file. Parent directories are created as needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: { type: 'string', description: 'Full file contents' }
|
||
},
|
||
required: ['path', 'content']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_file',
|
||
description:
|
||
'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: {
|
||
type: 'string',
|
||
description: 'If set (non-empty), full file replacement'
|
||
},
|
||
old_string: {
|
||
type: 'string',
|
||
description: 'Search string (used with new_string)'
|
||
},
|
||
new_string: { type: 'string', description: 'Replacement text' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'create_directory',
|
||
description: 'Create a directory (recursive).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'search_files',
|
||
description:
|
||
'Run grep -R to list paths matching a pattern (bounded). Uses the shell.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: { type: 'string' },
|
||
root: {
|
||
type: 'string',
|
||
description: 'Directory to search (default /home)'
|
||
},
|
||
max_lines: { type: 'integer', description: 'Default 200' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_command',
|
||
description:
|
||
'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT:<code> line.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
command: {
|
||
type: 'string',
|
||
description: 'Full command string (e.g. ls -la /bin)'
|
||
},
|
||
timeout_ms: { type: 'integer' },
|
||
capture_exit: {
|
||
type: 'boolean',
|
||
description: 'If true, append last line EXIT:<code> to capture (default false)'
|
||
}
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script',
|
||
description:
|
||
'REQUIRED to run agent-authored JavaScript: Node is not installed. Writes code to ~/.agent/_tmp_agent_run.mjs and runs it by absolute path (Bare kernel — same as /bin scripts). Do not use run_command with node/npm/npx. Prefer async function run(ctx, argv). stdout/stderr captured.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
code: { type: 'string', description: 'Full ESM/CommonJS script body' }
|
||
},
|
||
required: ['code']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_system_info',
|
||
description:
|
||
'Lightweight context: API version, uname, optional resource hook. Questions about **which kernel features are on/off in this session** → read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json; not apropos_man. For other /proc JSON use read_proc_file; swarm → get_swarm_peers; resource table → get_resource_limits. want=capabilities|swarm still returns those blobs when needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
want: {
|
||
type: 'string',
|
||
enum: ['summary', 'capabilities', 'swarm'],
|
||
description: 'Optional focus (default summary)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_agent_config',
|
||
description:
|
||
'Merge keys into ~/.agent/config.json (shallow merge for known keys only).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
patch: {
|
||
type: 'object',
|
||
description:
|
||
'Partial config object (rest_base_url, model, temperature, owner_name, agent_label, …)'
|
||
}
|
||
},
|
||
required: ['patch']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_bin',
|
||
description: 'List Tier-1 utilities in /bin via VFS.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
limit: { type: 'integer', description: 'Max names (default 400)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_directory',
|
||
description:
|
||
'List directory entries via ctx.vfs.readdir. Optional one-line stat per entry (bounded).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute directory path' },
|
||
max_entries: { type: 'integer', description: 'Max names (default 500, cap 2000)' },
|
||
include_stat: {
|
||
type: 'boolean',
|
||
description: 'If true, call stat on each entry (slower; default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'file_stat',
|
||
description:
|
||
'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
follow_symlinks: {
|
||
type: 'boolean',
|
||
description: 'If true, use stat (follow); if false, lstat (default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'move_path',
|
||
description:
|
||
'Rename or move a file or directory via shell mv (same rules as mv). Paths must be under /home, /tmp, /mnt, or /root.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'delete_path',
|
||
description:
|
||
'Delete a file or directory (recursive optional). Requires ~/.agent/config.json allow_delete; optional confirm_token when require_confirm_token is set.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
recursive: {
|
||
type: 'boolean',
|
||
description: 'Remove directories recursively (default false)'
|
||
},
|
||
confirm_token: {
|
||
type: 'string',
|
||
description: 'Must match config require_confirm_token when that key is non-empty'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_man_page',
|
||
description:
|
||
'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: { type: 'string', description: 'Page name (e.g. grep, agent)' },
|
||
section: {
|
||
type: 'integer',
|
||
description: 'Manual section 1–8 if disambiguating (optional)'
|
||
},
|
||
max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' }
|
||
},
|
||
required: ['topic']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'apropos_man',
|
||
description:
|
||
'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines. This is documentation search only—never use it to answer what kernel features are currently enabled or disabled (use read_proc_file on /proc/bare_os/features).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
keyword: { type: 'string' },
|
||
max_results: { type: 'integer', description: 'Default 40, max 200' }
|
||
},
|
||
required: ['keyword']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_proc_file',
|
||
description:
|
||
'Read a small allowlisted /proc/bare_os pseudo file (bounded). Canonical live kernel feature state: /proc/bare_os/features or /proc/bare_os/features.json (same content). Use instead of shelling cat.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: {
|
||
type: 'string',
|
||
description:
|
||
'Exact allowlisted path (metrics_live.json, features or features.json, capabilities.json, swarm*.json, swarm_*_status.json); bounded read.'
|
||
},
|
||
max_bytes: { type: 'integer', description: 'Default 256000' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_swarm_peers',
|
||
description: 'Return parsed /proc/bare_os/swarm.json when readable (P2P / Hyperswarm snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_resource_limits',
|
||
description:
|
||
'Return ctx.bareOsGetResourceStatus() when available (pipeline / resource snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script_at_path',
|
||
description:
|
||
'Execute an existing .mjs script by absolute path (Bare kernel runner). Same as running that path with run_command but dedicated for clarity.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute path to .mjs file' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'web_fetch',
|
||
description:
|
||
'Fetch live HTTP(S) URLs and return structured content for the assistant. Uses ctx.httpFetch (same policy as wget/curl: BARE_OS_HTTP_ALLOWLIST / DENYLIST). For official docs index use read_man_page / apropos_man — they are not web pages. Supports GET/HEAD/POST and extract modes: auto (JSON vs HTML vs text), markdownish plain text from HTML, links (anchor hrefs), meta (title/og:), raw UTF-8 slice, or json parse.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
url: { type: 'string', description: 'Absolute http(s) URL' },
|
||
method: {
|
||
type: 'string',
|
||
description: 'HTTP method (default GET)',
|
||
enum: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
|
||
},
|
||
headers: {
|
||
type: 'object',
|
||
description: 'Optional header map (string values only)'
|
||
},
|
||
body: {
|
||
type: 'string',
|
||
description: 'Request body for non-GET (e.g. JSON string for APIs)'
|
||
},
|
||
content_type: {
|
||
type: 'string',
|
||
description: 'Content-Type when body is set (default application/octet-stream)'
|
||
},
|
||
format: {
|
||
type: 'string',
|
||
enum: ['auto', 'json', 'markdownish', 'text', 'links', 'meta', 'raw'],
|
||
description:
|
||
'auto: sniff Content-Type; json: parse JSON; markdownish/text: strip HTML to readable text; links: absolute http(s) links; meta: title/description/og tags; raw: bounded UTF-8 text'
|
||
},
|
||
max_response_bytes: {
|
||
type: 'integer',
|
||
description: 'Cap downloaded bytes (default 524288, max 2MiB)'
|
||
},
|
||
max_redirects: {
|
||
type: 'integer',
|
||
description: 'Max redirects to follow (default 5)'
|
||
},
|
||
timeout_ms: {
|
||
type: 'integer',
|
||
description: 'Per-request timeout ms (default 30000, max 120000)'
|
||
},
|
||
max_links: {
|
||
type: 'integer',
|
||
description: 'Max links when format=links (default 200)'
|
||
}
|
||
},
|
||
required: ['url']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_skill',
|
||
description:
|
||
'Load the full SKILL.md for a modular agent skill (folder id or frontmatter name, case-insensitive). Workspace ~/.agent/workspace/skills/ overrides ~/.agent/skills/. Use after checking the compact skills index in the system prompt.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
skill: {
|
||
type: 'string',
|
||
description: 'Skill folder name (e.g. p2p-os-status) or YAML frontmatter name'
|
||
},
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes of SKILL.md (default 256000)'
|
||
}
|
||
},
|
||
required: ['skill']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_services',
|
||
description:
|
||
'List initd service definitions and current runtime phases from /proc/bare_os/initd_readiness.json when available.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'service_status',
|
||
description:
|
||
'Read one initd unit status by name from /proc/bare_os/initd_readiness.json and include journal hint paths when present.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
name: { type: 'string', description: 'Unit name, e.g. bare-cron' }
|
||
},
|
||
required: ['name']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_timers',
|
||
description:
|
||
'List user timer drop-ins from ~/.config/bare-os/timers and optionally include short file previews.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
include_preview: {
|
||
type: 'boolean',
|
||
description: 'Include bounded timer file text previews (default false)'
|
||
},
|
||
max_entries: {
|
||
type: 'integer',
|
||
description: 'Maximum timer files to return (default 128, max 512)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_cron_log',
|
||
description:
|
||
'Read /var/log/bare-os/cron.log with bounded output and optional tail mode.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 12000)'
|
||
},
|
||
tail_only: {
|
||
type: 'boolean',
|
||
description: 'When true, return only the trailing max_chars slice'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_audit_log',
|
||
description:
|
||
'Read /var/log/bare-os/audit.log with bounded output and best-effort secret redaction.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 12000)'
|
||
},
|
||
tail_only: {
|
||
type: 'boolean',
|
||
description: 'When true, return only the trailing max_chars slice'
|
||
},
|
||
redact: {
|
||
type: 'boolean',
|
||
description: 'Apply lightweight token redaction (default true)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_boot_policy',
|
||
description:
|
||
'Read /etc/bare-os/boot.policy.json and return text plus parsed JSON when available.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 20000)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_kernel_extension_resolution',
|
||
description:
|
||
'Read /run/bare-os/kernel-ext-resolution.json for extension ordering, conflicts, and pin outcomes.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 20000)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_initd_graph',
|
||
description:
|
||
'Read initd dependency DAG/readiness graph from /proc/bare_os/initd_dag.json and /proc/bare_os/initd_readiness.json.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_unit_journal',
|
||
description:
|
||
'Read a bounded/redacted tail of /run/bare-os/unit-journal/<unit>.ndjson.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
unit: { type: 'string', description: 'Initd unit name, e.g. bare-cron' },
|
||
max_chars: { type: 'integer', description: 'Maximum output chars (default 12000)' },
|
||
tail_only: { type: 'boolean', description: 'Return trailing max_chars only' }
|
||
},
|
||
required: ['unit']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'inspect_ipc_backpressure',
|
||
description:
|
||
'Inspect IPC/backpressure operator snapshots from /proc/bare_os JSON surfaces.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_network_summary',
|
||
description:
|
||
'Read a typed network/swarm summary from /proc/bare_os surfaces with best-effort fallback paths.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'tail_telemetry_streams',
|
||
description:
|
||
'Read bounded/redacted tails from telemetry logs such as /var/log/bare-os/audit.log, logger.jsonl, and initd logs.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: { type: 'integer', description: 'Maximum chars per stream (default 8000)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'pkg_index_lookup',
|
||
description:
|
||
'Run pkg-swarm-index lookup and return parsed output for one package key.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
key: { type: 'string', description: 'Package key to lookup' }
|
||
},
|
||
required: ['key']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_verification_scripts',
|
||
description:
|
||
'List known verification scripts from /scripts and summarize likely check families.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_maintenance_gate',
|
||
description:
|
||
'Run one allowlisted maintenance command with bounded capture for automation workflows.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
command: { type: 'string', description: 'Allowlisted command id' },
|
||
cwd: { type: 'string', description: 'Optional working directory' },
|
||
timeout_ms: { type: 'integer', description: 'Timeout in milliseconds' }
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_contract_checks',
|
||
description:
|
||
'Run a grouped set of contract checks by profile id (allowlisted) with bounded output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
profile: { type: 'string', description: 'Check profile id, e.g. core, docs, parity' }
|
||
},
|
||
required: ['profile']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'summarize_build_drift',
|
||
description:
|
||
'Summarize build/generated drift by comparing git status and key generated artifacts.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_hrpc_bridge_health',
|
||
description:
|
||
'Read HRPC bridge/operator health from /proc/bare_os surfaces and include host capability hints when available.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_hrpc_allowlist_status',
|
||
description:
|
||
'Inspect effective HRPC allowlist/probe status using hrpc probe and operator snapshots.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'emit_host_notification',
|
||
description:
|
||
'Request an audited host notification via HRPC route (policy-gated; disabled by default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
title: { type: 'string' },
|
||
message: { type: 'string' },
|
||
level: { type: 'string', enum: ['info', 'warn', 'error'] }
|
||
},
|
||
required: ['title', 'message']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'request_host_action',
|
||
description:
|
||
'Request a schema-validated host action through HRPC (policy-gated; disabled by default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
action: { type: 'string', description: 'Host action id' },
|
||
payload: { type: 'object', description: 'Action payload object' }
|
||
},
|
||
required: ['action']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'autonomous_run',
|
||
description:
|
||
'Start an autonomous coding run with goal, optional scope path, and runtime cap. This enables autonomous mode in config.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
goal: { type: 'string', description: 'Task goal the agent should complete autonomously' },
|
||
scope_path: { type: 'string', description: 'Preferred working scope path (optional)' },
|
||
max_runtime_ms: { type: 'integer', description: 'Optional runtime cap override' },
|
||
required_checks: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
description: 'Optional quality gates (allowlisted check ids)'
|
||
}
|
||
},
|
||
required: ['goal']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'autonomous_run_status',
|
||
description:
|
||
'Return current autonomous run state, elapsed/runtime budget, configured checks, and latest status.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'autonomous_run_stop',
|
||
description:
|
||
'Request manual stop for an autonomous run; loop will stop safely on next control checkpoint.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
reason: { type: 'string' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'verification_hints',
|
||
description:
|
||
'Suggest npm/node verification commands for a developer working at the Bare OS git checkout on the host (paths or topic keywords). Does not run commands.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: {
|
||
type: 'string',
|
||
description: 'Free-text task or area (e.g. kernel init, seed RPC, shell)'
|
||
},
|
||
paths_touched: {
|
||
type: 'string',
|
||
description:
|
||
'Optional comma-separated path-like strings from the repo (forward slashes ok)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'runtime_diagnostic_bundle',
|
||
description:
|
||
'Non-secret snapshot: ctx API version, optional resource status, and allowlisted /proc/bare_os files that exist (features, swarm, metrics). Prefer over many separate read_proc_file calls.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'task_complete',
|
||
description:
|
||
'Call when the user task is fully done. Provide a concise summary.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
summary: { type: 'string' }
|
||
},
|
||
required: ['summary']
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} absPath
|
||
*/
|
||
function bareAgentPathAllowed(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/')) return false
|
||
const ok =
|
||
p.startsWith('/home/') ||
|
||
p.startsWith('/tmp/') ||
|
||
p === '/tmp' ||
|
||
p.startsWith('/root/') ||
|
||
p.startsWith('/mnt/') ||
|
||
p.startsWith('/bin/') ||
|
||
p === '/bin' ||
|
||
p.startsWith('/etc/') ||
|
||
p.startsWith('/share/') ||
|
||
p.startsWith('/usr/') ||
|
||
p.startsWith('/var/') ||
|
||
p.startsWith('/proc/') ||
|
||
p.startsWith('/boot/') ||
|
||
p.startsWith('/lib/') ||
|
||
p.startsWith('/dev/')
|
||
return ok
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* ctx: Record<string, unknown>,
|
||
* toolName: string,
|
||
* argsJson: string,
|
||
* paths: { dir: string, config: string, cmdOut: string, workspace?: string, workspaceSkills?: string, skillsGlobal?: string },
|
||
* signal?: AbortSignal,
|
||
* appendProgress: (line: string) => void,
|
||
* home: string,
|
||
* configRef: { current: Record<string, unknown> },
|
||
* manCacheRef?: { db: unknown | null },
|
||
* onTaskComplete: (summary: string) => void
|
||
* }} o
|
||
*/
|
||
async function bareAgentDispatchTool(o) {
|
||
const {
|
||
ctx,
|
||
toolName,
|
||
argsJson,
|
||
paths,
|
||
signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
} = o
|
||
const manDbCache = manCacheRef || { db: null }
|
||
/** @type {Record<string, unknown>} */
|
||
let args = {}
|
||
try {
|
||
args = /** @type {Record<string, unknown>} */ (JSON.parse(argsJson || '{}'))
|
||
} catch {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
|
||
}
|
||
const cfgNow = configRef.current || {}
|
||
if (
|
||
cfgNow.autonomous_active &&
|
||
Array.isArray(cfgNow.autonomous_deny_ops) &&
|
||
cfgNow.autonomous_deny_ops.map((x) => String(x)).includes(toolName)
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_op_denied', tool: toolName })
|
||
}
|
||
|
||
const vfs = ctx.vfs
|
||
const AUTONOMOUS_DENY_PATH_PREFIXES = [
|
||
'/.git',
|
||
'/proc',
|
||
'/dev',
|
||
'/sys',
|
||
'/run',
|
||
'/boot',
|
||
'/lib',
|
||
'/usr/lib'
|
||
]
|
||
const AUTONOMOUS_CHECK_ALLOW = {
|
||
'coreutils-test': 'npm test -w bare-os-coreutils',
|
||
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
||
'verify-man-coverage': 'node scripts/verify-man-coverage.mjs',
|
||
'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs'
|
||
}
|
||
const execLine =
|
||
typeof ctx.execLine === 'function'
|
||
? /** @type {(s: string, opts?: unknown) => Promise<unknown>} */ (
|
||
ctx.execLine.bind(ctx)
|
||
)
|
||
: null
|
||
|
||
/**
|
||
* @param {string} line
|
||
* @param {number | undefined} timeoutMs
|
||
* @param {{ captureExit?: boolean }} [captureOpts]
|
||
*/
|
||
async function captureExec(line, timeoutMs, captureOpts) {
|
||
const outPath = paths.cmdOut
|
||
const captureExit = Boolean(captureOpts && captureOpts.captureExit)
|
||
const trimmed = String(line || '').trim()
|
||
const compoundShell =
|
||
/\n/.test(trimmed) ||
|
||
/(^|[;\s])(for|if|while|until|case|function)\b/.test(trimmed) ||
|
||
/\b(do|done|then|else|fi|esac)\b/.test(trimmed) ||
|
||
/&&|\|\||\(\(|\{|\}/.test(trimmed)
|
||
/**
|
||
* Do not wrap with `{ cmd ; }` — Bare OS `splitTokensBySemicolon` splits on every `;`
|
||
* at depth 0 and does not treat `{ … }` as a compound, so `{` became argv[0]
|
||
* (`unknown command: {`). Redirect only; read exit from env after `execLine`.
|
||
*/
|
||
const wrapped =
|
||
(compoundShell
|
||
? 'sh -c ' + bareAgentShellQuote(trimmed)
|
||
: line) +
|
||
' > ' +
|
||
bareAgentShellQuote(outPath) +
|
||
' 2>&1'
|
||
const opts =
|
||
signal || timeoutMs
|
||
? {
|
||
signal,
|
||
timeoutMs: timeoutMs || undefined
|
||
}
|
||
: undefined
|
||
try {
|
||
if (opts) await execLine(wrapped, opts)
|
||
else await execLine(wrapped)
|
||
} catch (e) {
|
||
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, exitNote: msg }
|
||
}
|
||
let captured = ''
|
||
try {
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
const buf = await vfs.readFile(outPath)
|
||
if (buf && buf.length) {
|
||
captured =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (captureExit) {
|
||
const env = ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
|
||
const rawEc =
|
||
env && env.BARE_OS_EXIT_STATUS != null && env.BARE_OS_EXIT_STATUS !== ''
|
||
? env.BARE_OS_EXIT_STATUS
|
||
: ctx.exitCode
|
||
const n = Number(rawEc)
|
||
const codeStr = String(Number.isFinite(n) ? n : 0)
|
||
const exitLine = '\nEXIT:' + codeStr + '\n'
|
||
captured += exitLine
|
||
try {
|
||
if (vfs?.readFile && vfs?.writeFile && ctx.b4a && typeof ctx.b4a.concat === 'function') {
|
||
let prev = await vfs.readFile(outPath)
|
||
const prevBytes =
|
||
prev && prev.length
|
||
? prev instanceof Uint8Array
|
||
? prev
|
||
: ctx.b4a.from(prev)
|
||
: ctx.b4a.from('')
|
||
await vfs.writeFile(
|
||
outPath,
|
||
ctx.b4a.concat([prevBytes, ctx.b4a.from(exitLine)])
|
||
)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
const max = 120_000
|
||
if (captured.length > max) captured = captured.slice(0, max) + '\n… truncated'
|
||
return { ok: true, stdout_stderr: captured }
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
*/
|
||
function redactSensitiveText(text) {
|
||
return String(text || '')
|
||
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
||
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
||
.replace(
|
||
/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi,
|
||
'$1=<redacted>'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {string} path
|
||
* @param {number} maxChars
|
||
* @param {boolean} tailOnly
|
||
* @param {boolean} redact
|
||
*/
|
||
async function readBoundedText(path, maxChars, tailOnly, redact) {
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return { ok: false, error: 'vfs unavailable' }
|
||
}
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
if (!b || !b.length) return { ok: false, error: 'empty_or_missing' }
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
if (redact) t = redactSensitiveText(t)
|
||
const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars)
|
||
return { ok: true, path, text: out, truncated: t.length > out.length }
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, error: msg }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} p
|
||
*/
|
||
function autonomousPathAllowed(p) {
|
||
const s = String(p || '').trim()
|
||
if (!s) return true
|
||
if (!s.startsWith('/')) return false
|
||
for (const pref of AUTONOMOUS_DENY_PATH_PREFIXES) {
|
||
if (s === pref || s.startsWith(pref + '/')) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {string} p
|
||
*/
|
||
function enforceAutonomousPath(p) {
|
||
const cfg = configRef.current || {}
|
||
if (!cfg.autonomous_active) return true
|
||
const path = String(p || '').trim()
|
||
if (!path) return true
|
||
if (!autonomousPathAllowed(path)) return false
|
||
const allowList = Array.isArray(cfg.autonomous_allow_paths)
|
||
? cfg.autonomous_allow_paths.map((x) => String(x || '').trim()).filter(Boolean)
|
||
: []
|
||
if (!allowList.length || allowList.includes('*')) return true
|
||
for (const pref of allowList) {
|
||
if (path === pref || path.startsWith(pref.endsWith('/') ? pref : pref + '/')) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
try {
|
||
if (toolName === 'read_skill') {
|
||
const skill = typeof args.skill === 'string' ? args.skill.trim() : ''
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 512_000)
|
||
: 256_000
|
||
if (!skill) {
|
||
return bareAgentJsonResult({ ok: false, error: 'skill_required' })
|
||
}
|
||
const skillPaths = {
|
||
workspaceSkills:
|
||
typeof paths.workspaceSkills === 'string'
|
||
? paths.workspaceSkills
|
||
: paths.dir + '/workspace/skills',
|
||
skillsGlobal:
|
||
typeof paths.skillsGlobal === 'string' ? paths.skillsGlobal : paths.dir + '/skills'
|
||
}
|
||
appendProgress('read_skill ' + skill)
|
||
const loaded = await bareAgentLoadSkillMarkdown(ctx, skillPaths, skill)
|
||
if (!loaded.ok) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: loaded.error || 'load_failed',
|
||
skill
|
||
})
|
||
}
|
||
let content = loaded.content
|
||
if (content.length > maxB) content = content.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
id: loaded.id,
|
||
name: loaded.skill,
|
||
path: loaded.path,
|
||
source: loaded.source,
|
||
content
|
||
})
|
||
}
|
||
|
||
if (toolName === 'task_complete') {
|
||
const summary = typeof args.summary === 'string' ? args.summary : ''
|
||
appendProgress('task_complete: ' + summary.slice(0, 200))
|
||
onTaskComplete(summary || '(done)')
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
completed: true,
|
||
summary
|
||
})
|
||
}
|
||
|
||
if (toolName === 'autonomous_run') {
|
||
const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
|
||
const scopePath = typeof args.scope_path === 'string' ? args.scope_path.trim() : ''
|
||
const cfg = configRef.current || {}
|
||
if (!cfg.autonomous_mode_enabled) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_mode_disabled' })
|
||
}
|
||
if (!goal) return bareAgentJsonResult({ ok: false, error: 'goal_required' })
|
||
if (scopePath && !autonomousPathAllowed(scopePath)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'scope_path_denied' })
|
||
}
|
||
const maxRuntimeMsRaw =
|
||
typeof args.max_runtime_ms === 'number' && Number.isFinite(args.max_runtime_ms)
|
||
? args.max_runtime_ms
|
||
: cfg.autonomous_max_runtime_ms
|
||
const maxRuntimeMs = Math.min(Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000), 7_200_000)
|
||
const requiredChecks = Array.isArray(args.required_checks)
|
||
? args.required_checks.map((x) => String(x || '').trim()).filter(Boolean)
|
||
: []
|
||
const unknown = requiredChecks.filter((x) => !Object.prototype.hasOwnProperty.call(AUTONOMOUS_CHECK_ALLOW, x))
|
||
if (unknown.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_required_checks', unknown, allowlist: Object.keys(AUTONOMOUS_CHECK_ALLOW) })
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(cfg, {
|
||
autonomous_active: true,
|
||
autonomous_stop_requested: false,
|
||
autonomous_started_at_ms: Date.now(),
|
||
autonomous_goal: goal,
|
||
autonomous_status: 'running',
|
||
autonomous_last_error: '',
|
||
autonomous_max_runtime_ms: maxRuntimeMs,
|
||
autonomous_completion_required_checks: requiredChecks
|
||
})
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
configRef.current = merged
|
||
appendProgress('autonomous_run start goal=' + goal.slice(0, 160))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
active: true,
|
||
goal,
|
||
scope_path: scopePath || null,
|
||
max_runtime_ms: maxRuntimeMs,
|
||
required_checks: requiredChecks,
|
||
status: 'running'
|
||
})
|
||
}
|
||
|
||
if (toolName === 'autonomous_run_status') {
|
||
const cfg = configRef.current || {}
|
||
const started = Number(cfg.autonomous_started_at_ms) || 0
|
||
const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0
|
||
const maxRuntime = Number(cfg.autonomous_max_runtime_ms) || 0
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
autonomous_mode_enabled: Boolean(cfg.autonomous_mode_enabled),
|
||
active: Boolean(cfg.autonomous_active),
|
||
stop_requested: Boolean(cfg.autonomous_stop_requested),
|
||
goal: String(cfg.autonomous_goal || ''),
|
||
status: String(cfg.autonomous_status || 'idle'),
|
||
last_error: String(cfg.autonomous_last_error || ''),
|
||
started_at_ms: started,
|
||
elapsed_ms: elapsed,
|
||
max_runtime_ms: maxRuntime,
|
||
remaining_ms: maxRuntime > 0 ? Math.max(0, maxRuntime - elapsed) : 0,
|
||
required_checks: Array.isArray(cfg.autonomous_completion_required_checks)
|
||
? cfg.autonomous_completion_required_checks
|
||
: []
|
||
})
|
||
}
|
||
|
||
if (toolName === 'autonomous_run_stop') {
|
||
const cfg = configRef.current || {}
|
||
const reason = typeof args.reason === 'string' ? args.reason.trim() : ''
|
||
const merged = bareAgentMergeConfigPatch(cfg, {
|
||
autonomous_stop_requested: true,
|
||
autonomous_status: 'stopped',
|
||
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
|
||
})
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
configRef.current = merged
|
||
appendProgress('autonomous_run_stop ' + (reason || 'requested'))
|
||
return bareAgentJsonResult({ ok: true, stop_requested: true, reason: reason || null })
|
||
}
|
||
|
||
if (toolName === 'read_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 1_000_000)
|
||
: 256_000
|
||
if (!bareAgentPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('read_file ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({ ok: true, path, content: t })
|
||
}
|
||
|
||
if (toolName === 'write_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
const content = typeof args.content === 'string' ? args.content : ''
|
||
if (!path.startsWith('/') || path.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||
}
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('write_file ' + path)
|
||
let dir = path.replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(content)
|
||
: new TextEncoder().encode(content)
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
||
}
|
||
|
||
if (toolName === 'edit_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
if (!bareAgentPathAllowed(path) || !vfs?.readFile || !vfs?.writeFile) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
|
||
}
|
||
appendProgress('edit_file ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
let prev =
|
||
buf && buf.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
: ''
|
||
const full = typeof args.content === 'string' ? args.content : ''
|
||
const oldStr = typeof args.old_string === 'string' ? args.old_string : ''
|
||
const newStr = typeof args.new_string === 'string' ? args.new_string : ''
|
||
let next = prev
|
||
if (full.length > 0) next = full
|
||
else if (oldStr) {
|
||
if (!prev.includes(oldStr)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'old_string not found' })
|
||
}
|
||
next = prev.replace(oldStr, newStr)
|
||
} else {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'need content or old_string+new_string'
|
||
})
|
||
}
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(next)
|
||
: new TextEncoder().encode(next)
|
||
let dir = path.replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
||
}
|
||
|
||
if (toolName === 'create_directory') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
if (!path.startsWith('/')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||
}
|
||
if (!vfs || typeof vfs.mkdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('mkdir ' + path)
|
||
await vfs.mkdir(path, { recursive: true })
|
||
return bareAgentJsonResult({ ok: true })
|
||
}
|
||
|
||
if (toolName === 'search_files') {
|
||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||
const root = typeof args.root === 'string' ? args.root : '/home'
|
||
const maxLines =
|
||
typeof args.max_lines === 'number' && Number.isFinite(args.max_lines)
|
||
? Math.min(Math.floor(args.max_lines), 500)
|
||
: 200
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('search_files ' + pattern + ' @ ' + root)
|
||
const cmd =
|
||
'grep -Rnl -- ' +
|
||
bareAgentShellQuote(pattern) +
|
||
' ' +
|
||
bareAgentShellQuote(root) +
|
||
' 2>/dev/null | head -n ' +
|
||
maxLines
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_command') {
|
||
const command = typeof args.command === 'string' ? args.command : ''
|
||
if ((configRef.current || {}).autonomous_active) {
|
||
const cmd = command.trim().toLowerCase()
|
||
if (
|
||
cmd.includes('rm ') ||
|
||
cmd.includes(' git reset') ||
|
||
cmd.includes(' git clean') ||
|
||
cmd.startsWith('git ') ||
|
||
cmd.includes(' git ')
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_command_denied' })
|
||
}
|
||
}
|
||
const timeoutMs =
|
||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||
? Math.min(Math.floor(args.timeout_ms), 600000)
|
||
: 120000
|
||
const captureExit = Boolean(args.capture_exit)
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('run_command ' + command.slice(0, 160))
|
||
const r = await captureExec(command, timeoutMs, { captureExit })
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_js_script') {
|
||
const code = typeof args.code === 'string' ? args.code : ''
|
||
const scriptPath = paths.dir + '/_tmp_agent_run.mjs'
|
||
if (!vfs?.writeFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script (' + code.length + ' chars)')
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(code)
|
||
: new TextEncoder().encode(code)
|
||
await vfs.writeFile(scriptPath, body)
|
||
/** Absolute path → kernel-runner runs .mjs like `./script.mjs` (no host `node` binary). captureExec adds stdout redirect. */
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'get_system_info') {
|
||
/** @type {Record<string, unknown>} */
|
||
const info = {}
|
||
try {
|
||
info.ctxApiVersion =
|
||
typeof ctx.ctxApiVersion === 'string'
|
||
? ctx.ctxApiVersion
|
||
: typeof ctx.ctxApiVersion === 'number'
|
||
? String(ctx.ctxApiVersion)
|
||
: undefined
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const want =
|
||
typeof args.want === 'string' ? args.want : 'summary'
|
||
if (want === 'summary') {
|
||
info.discovery_hint =
|
||
'For live kernel feature flags use read_proc_file on /proc/bare_os/features (or features.json); apropos_man only searches man-page text, not runtime state. Otherwise prefer read_proc_file, get_swarm_peers, get_resource_limits, read_man_page / apropos_man instead of dumping large blobs here.'
|
||
}
|
||
if (want === 'capabilities' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/capabilities.json')
|
||
if (b && b.length) {
|
||
info.capabilities_json =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (want === 'swarm' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (b && b.length) {
|
||
info.swarm =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
ctx.b4a.toString
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (execLine) await execLine('uname -a > ' + bareAgentShellQuote(paths.cmdOut) + ' 2>&1')
|
||
if (vfs?.readFile) {
|
||
const buf = await vfs.readFile(paths.cmdOut)
|
||
if (buf && buf.length) {
|
||
info.uname =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf).trim()
|
||
: String(new TextDecoder().decode(buf)).trim()
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
appendProgress('get_system_info ' + want)
|
||
return bareAgentJsonResult({ ok: true, want, info })
|
||
}
|
||
|
||
if (toolName === 'edit_agent_config') {
|
||
const patch = args.patch
|
||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad patch' })
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
|
||
configRef.current = merged
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
if (
|
||
Object.prototype.hasOwnProperty.call(patch, 'owner_name') ||
|
||
Object.prototype.hasOwnProperty.call(patch, 'agent_label')
|
||
) {
|
||
const workspace =
|
||
typeof paths.workspace === 'string'
|
||
? paths.workspace
|
||
: paths.dir + '/workspace'
|
||
await bareAgentSyncWorkspaceFromConfig(ctx, { workspace }, merged)
|
||
}
|
||
appendProgress('edit_agent_config')
|
||
return bareAgentJsonResult({ ok: true, saved: true })
|
||
}
|
||
|
||
if (toolName === 'list_bin') {
|
||
const limit =
|
||
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
||
? Math.min(Math.floor(args.limit), 800)
|
||
: 400
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_bin')
|
||
try {
|
||
const names = await vfs.readdir('/bin')
|
||
const arr = Array.isArray(names) ? [...names].slice(0, limit) : []
|
||
arr.sort()
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: arr.length,
|
||
names: arr
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_directory') {
|
||
const dir = typeof args.path === 'string' ? args.path : ''
|
||
const maxEnt =
|
||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||
? Math.min(Math.floor(args.max_entries), 2000)
|
||
: 500
|
||
const includeStat = Boolean(args.include_stat)
|
||
if (!bareAgentPathAllowed(dir)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_directory ' + dir)
|
||
try {
|
||
const names = await vfs.readdir(dir)
|
||
const arr = Array.isArray(names) ? [...names] : []
|
||
arr.sort()
|
||
const slice = arr.slice(0, maxEnt)
|
||
const base = dir.replace(/\/+$/, '') || '/'
|
||
/** @type {{ name: string, stat?: Record<string, unknown> }[]} */
|
||
const entries = []
|
||
for (const n of slice) {
|
||
const entry = { name: n }
|
||
if (includeStat && (vfs.lstat || vfs.stat)) {
|
||
try {
|
||
const full = base + '/' + n
|
||
const st =
|
||
typeof vfs.lstat === 'function'
|
||
? await vfs.lstat(full)
|
||
: await vfs.stat(full)
|
||
entry.stat = bareAgentSerializeStat(st, full)
|
||
} catch {
|
||
/* ignore per-entry stat errors */
|
||
}
|
||
}
|
||
entries.push(entry)
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path: dir,
|
||
count: entries.length,
|
||
truncated: arr.length > maxEnt,
|
||
entries
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'file_stat') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const follow = Boolean(args.follow_symlinks)
|
||
if (!bareAgentPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('file_stat ' + path)
|
||
try {
|
||
/** @type {unknown} */
|
||
let st = null
|
||
if (follow && typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
else if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
if (!st) return bareAgentJsonResult({ ok: false, error: 'stat unavailable' })
|
||
const serialized = bareAgentSerializeStat(st, path)
|
||
if (
|
||
serialized.kind === 'symlink' &&
|
||
typeof vfs.readlink === 'function'
|
||
) {
|
||
try {
|
||
serialized.target = await vfs.readlink(path)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult({ ok: true, stat: serialized })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'move_path') {
|
||
const from = typeof args.from_path === 'string' ? args.from_path : ''
|
||
const to = typeof args.to_path === 'string' ? args.to_path : ''
|
||
if (
|
||
!bareAgentPathAllowedMutate(from) ||
|
||
!bareAgentPathAllowedMutate(to) ||
|
||
from.includes('..') ||
|
||
to.includes('..')
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('move_path')
|
||
const cmd =
|
||
'mv -- ' + bareAgentShellQuote(from) + ' ' + bareAgentShellQuote(to)
|
||
const r = await captureExec(cmd, 120000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'delete_path') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const recursive = Boolean(args.recursive)
|
||
const token = typeof args.confirm_token === 'string' ? args.confirm_token : ''
|
||
const cfg = configRef.current
|
||
const allowDel = Boolean(cfg && cfg.allow_delete)
|
||
const reqTok =
|
||
cfg && typeof cfg.require_confirm_token === 'string'
|
||
? String(cfg.require_confirm_token)
|
||
: ''
|
||
if (!allowDel) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'delete_disabled',
|
||
hint: 'set allow_delete true in ~/.agent/config.json'
|
||
})
|
||
}
|
||
if (reqTok && token !== reqTok) {
|
||
return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' })
|
||
}
|
||
if (!bareAgentPathAllowedMutate(path) || path.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('delete_path ' + path)
|
||
try {
|
||
/** @type {unknown} */
|
||
let st = null
|
||
if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
const isDir =
|
||
st &&
|
||
typeof st === 'object' &&
|
||
typeof /** @type {{ isDirectory?: () => boolean }} */ (st).isDirectory ===
|
||
'function' &&
|
||
st.isDirectory()
|
||
if (isDir && recursive && typeof vfs.rm === 'function') {
|
||
await vfs.rm(path, { recursive: true })
|
||
return bareAgentJsonResult({ ok: true, removed: 'directory', recursive: true })
|
||
}
|
||
if (isDir && !recursive) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'is_directory',
|
||
hint: 'pass recursive true to remove a directory tree'
|
||
})
|
||
}
|
||
if (typeof vfs.unlink !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'unlink unavailable' })
|
||
}
|
||
await vfs.unlink(path)
|
||
return bareAgentJsonResult({ ok: true, removed: isDir ? 'directory' : 'file' })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'read_man_page') {
|
||
const topic = typeof args.topic === 'string' ? args.topic : ''
|
||
const maxC =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.floor(args.max_chars), 64_000)
|
||
: 12_000
|
||
let secExplicit = null
|
||
if (
|
||
typeof args.section === 'number' &&
|
||
Number.isFinite(args.section) &&
|
||
args.section >= 1 &&
|
||
args.section <= 8
|
||
) {
|
||
secExplicit = Math.floor(args.section)
|
||
}
|
||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||
if (!db) {
|
||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||
}
|
||
appendProgress('read_man_page ' + topic)
|
||
const resolved = bareAgentManResolvePage(db, topic, secExplicit)
|
||
if ('error' in resolved && resolved.error === 'wrong_section') {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'wrong_section',
|
||
foundSection: resolved.foundSection
|
||
})
|
||
}
|
||
if (!resolved.page) {
|
||
return bareAgentJsonResult({ ok: false, error: 'not_found' })
|
||
}
|
||
const slice = bareAgentManExtractPageSlice(resolved.page, maxC)
|
||
return bareAgentJsonResult({ ok: true, ...slice })
|
||
}
|
||
|
||
if (toolName === 'apropos_man') {
|
||
const kw = typeof args.keyword === 'string' ? args.keyword : ''
|
||
const maxRes =
|
||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||
? Math.floor(args.max_results)
|
||
: 40
|
||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||
if (!db) {
|
||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||
}
|
||
appendProgress('apropos_man ' + kw)
|
||
const { lines, truncated } = bareAgentManAproposHits(db, kw, maxRes)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: lines.length,
|
||
truncated,
|
||
lines
|
||
})
|
||
}
|
||
|
||
if (toolName === 'read_proc_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 500_000)
|
||
: 256_000
|
||
if (!bareAgentProcReadPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', allowlist: [...BARE_AGENT_PROC_READ_ALLOWLIST] })
|
||
}
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('read_proc_file ' + path)
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
let parsed = null
|
||
try {
|
||
parsed = JSON.parse(t)
|
||
} catch {
|
||
parsed = null
|
||
}
|
||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: t,
|
||
json: parsed
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_services') {
|
||
appendProgress('list_services')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
let readinessText = ''
|
||
/** @type {Record<string, unknown> | null} */
|
||
let readinessJson = null
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/initd_readiness.json')
|
||
if (b && b.length) {
|
||
readinessText =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
try {
|
||
const parsed = JSON.parse(readinessText)
|
||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
||
readinessJson = /** @type {Record<string, unknown>} */ (parsed)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const units = Array.isArray(readinessJson?.units)
|
||
? /** @type {unknown[]} */ (readinessJson.units)
|
||
: []
|
||
const rows = units
|
||
.filter((u) => u && typeof u === 'object')
|
||
.map((u) => {
|
||
const o = /** @type {Record<string, unknown>} */ (u)
|
||
return {
|
||
name: String(o.name || ''),
|
||
phase: String(o.phase || ''),
|
||
startedAtMs:
|
||
typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined,
|
||
error: typeof o.error === 'string' ? o.error : undefined
|
||
}
|
||
})
|
||
.filter((r) => r.name)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
source: '/proc/bare_os/initd_readiness.json',
|
||
count: rows.length,
|
||
units: rows,
|
||
note:
|
||
rows.length > 0
|
||
? 'Runtime units from initd readiness snapshot.'
|
||
: 'No parsed readiness units available.'
|
||
})
|
||
}
|
||
|
||
if (toolName === 'service_status') {
|
||
const name = typeof args.name === 'string' ? args.name.trim() : ''
|
||
if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' })
|
||
appendProgress('service_status ' + name)
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/initd_readiness.json')
|
||
const t =
|
||
b && b.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
: ''
|
||
const parsed = JSON.parse(t)
|
||
const units = Array.isArray(parsed?.units) ? parsed.units : []
|
||
const hit =
|
||
units.find((u) => u && typeof u === 'object' && String(u.name || '') === name) ||
|
||
null
|
||
if (!hit) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'not_found',
|
||
source: '/proc/bare_os/initd_readiness.json'
|
||
})
|
||
}
|
||
const o = /** @type {Record<string, unknown>} */ (hit)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
status: {
|
||
name: String(o.name || ''),
|
||
phase: String(o.phase || ''),
|
||
startedAtMs:
|
||
typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined,
|
||
error: typeof o.error === 'string' ? o.error : undefined
|
||
},
|
||
journal_hint: '/run/bare-os/unit-journal/' + name + '.ndjson'
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_timers') {
|
||
const includePreview = Boolean(args.include_preview)
|
||
const maxEntries =
|
||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||
? Math.min(Math.max(Math.floor(args.max_entries), 1), 512)
|
||
: 128
|
||
appendProgress('list_timers')
|
||
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const dir = home + '/.config/bare-os/timers'
|
||
let names = []
|
||
try {
|
||
names = await vfs.readdir(dir)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg, path: dir })
|
||
}
|
||
const timerNames = names
|
||
.filter((n) => typeof n === 'string' && n.endsWith('.timer'))
|
||
.sort()
|
||
.slice(0, maxEntries)
|
||
/** @type {unknown[]} */
|
||
const timers = []
|
||
for (const name of timerNames) {
|
||
const path = dir + '/' + name
|
||
/** @type {Record<string, unknown>} */
|
||
const row = { name, path }
|
||
if (includePreview) {
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
const txt =
|
||
b && b.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
: ''
|
||
row.preview = bareAgentTruncateChars(txt, 1200)
|
||
} catch (e) {
|
||
row.preview_error =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
}
|
||
}
|
||
timers.push(row)
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path: dir,
|
||
count: timers.length,
|
||
timers
|
||
})
|
||
}
|
||
|
||
if (toolName === 'read_cron_log' || toolName === 'read_audit_log') {
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 80_000)
|
||
: 12_000
|
||
const tailOnly = Boolean(args.tail_only)
|
||
const redact = toolName === 'read_audit_log' ? args.redact !== false : false
|
||
const path =
|
||
toolName === 'read_audit_log'
|
||
? '/var/log/bare-os/audit.log'
|
||
: '/var/log/bare-os/cron.log'
|
||
appendProgress(toolName + ' ' + path)
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
if (redact) {
|
||
t = t
|
||
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
||
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
||
.replace(/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=<redacted>')
|
||
}
|
||
const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: out,
|
||
truncated: t.length > out.length
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'read_boot_policy' || toolName === 'read_kernel_extension_resolution') {
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000)
|
||
: 20_000
|
||
const path =
|
||
toolName === 'read_boot_policy'
|
||
? '/etc/bare-os/boot.policy.json'
|
||
: '/run/bare-os/kernel-ext-resolution.json'
|
||
appendProgress(toolName + ' ' + path)
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
/** @type {unknown} */
|
||
let json = null
|
||
try {
|
||
json = JSON.parse(t)
|
||
} catch {
|
||
json = null
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: bareAgentTruncateChars(t, maxChars),
|
||
json
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_initd_graph') {
|
||
appendProgress('get_initd_graph')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true }
|
||
for (const p of ['/proc/bare_os/initd_dag.json', '/proc/bare_os/initd_readiness.json']) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 8000)
|
||
}
|
||
} catch {
|
||
/* ignore missing */
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'read_unit_journal') {
|
||
const unit = typeof args.unit === 'string' ? args.unit.trim() : ''
|
||
if (!/^[a-zA-Z0-9._-]{1,96}$/.test(unit)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_unit' })
|
||
}
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000)
|
||
: 12_000
|
||
const tailOnly = Boolean(args.tail_only)
|
||
const p = '/run/bare-os/unit-journal/' + unit + '.ndjson'
|
||
appendProgress('read_unit_journal ' + unit)
|
||
return bareAgentJsonResult(await readBoundedText(p, maxChars, tailOnly, true))
|
||
}
|
||
|
||
if (toolName === 'inspect_ipc_backpressure') {
|
||
appendProgress('inspect_ipc_backpressure')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const candidates = [
|
||
'/proc/bare_os/ipc_backpressure.json',
|
||
'/proc/bare_os/replication_operator_sketch.json',
|
||
'/proc/bare_os/metrics_live.json'
|
||
]
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true, sources: [] }
|
||
for (const p of candidates) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
out.sources.push(p)
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 6000)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'get_network_summary') {
|
||
appendProgress('get_network_summary')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const candidates = [
|
||
'/proc/bare_os/net_summary.json',
|
||
'/proc/bare_os/swarm.json',
|
||
'/proc/bare_os/swarm_status.json',
|
||
'/proc/bare_os/swarm_connection_manager_status.json'
|
||
]
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true, sources: [] }
|
||
for (const p of candidates) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
out.sources.push(p)
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 6000)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'tail_telemetry_streams') {
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 60_000)
|
||
: 8000
|
||
appendProgress('tail_telemetry_streams')
|
||
const pathsToRead = [
|
||
'/var/log/bare-os/audit.log',
|
||
'/var/log/bare-os/logger.jsonl',
|
||
'/var/log/bare-os/initd.log',
|
||
'/var/log/bare-os/cron.log'
|
||
]
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true, streams: {} }
|
||
for (const p of pathsToRead) {
|
||
out.streams[p] = await readBoundedText(p, maxChars, true, true)
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'pkg_index_lookup') {
|
||
const key = typeof args.key === 'string' ? args.key.trim() : ''
|
||
if (!key) return bareAgentJsonResult({ ok: false, error: 'key_required' })
|
||
appendProgress('pkg_index_lookup ' + key.slice(0, 80))
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const cmd = 'pkg-swarm-index get --key ' + bareAgentShellQuote(key)
|
||
const r = await captureExec(cmd, 90000)
|
||
if (r.ok === false) return bareAgentJsonResult(r)
|
||
const txt = typeof r.stdout_stderr === 'string' ? r.stdout_stderr : ''
|
||
let json = null
|
||
try {
|
||
json = JSON.parse(txt)
|
||
} catch {
|
||
json = null
|
||
}
|
||
return bareAgentJsonResult({ ok: true, key, json, text: bareAgentTruncateChars(txt, 12000) })
|
||
}
|
||
|
||
if (toolName === 'list_verification_scripts') {
|
||
appendProgress('list_verification_scripts')
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const out = []
|
||
for (const dir of ['/scripts', '/home/guest/scripts']) {
|
||
try {
|
||
const names = await vfs.readdir(dir)
|
||
const rows = names
|
||
.filter((n) => typeof n === 'string' && (n.endsWith('.mjs') || n.endsWith('.js')))
|
||
.sort()
|
||
.slice(0, 400)
|
||
.map((n) => dir + '/' + n)
|
||
out.push(...rows)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult({ ok: true, scripts: out })
|
||
}
|
||
|
||
if (toolName === 'run_maintenance_gate') {
|
||
const command = typeof args.command === 'string' ? args.command.trim() : ''
|
||
const timeoutMs =
|
||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||
? Math.min(Math.max(Math.floor(args.timeout_ms), 1000), 900000)
|
||
: 180000
|
||
appendProgress('run_maintenance_gate ' + command)
|
||
const allow = {
|
||
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
||
'verify-man-coverage': 'node scripts/verify-man-coverage.mjs',
|
||
'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs',
|
||
'coreutils-test': 'npm test -w bare-os-coreutils'
|
||
}
|
||
if (!Object.prototype.hasOwnProperty.call(allow, command)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'command_not_allowlisted', allowlist: Object.keys(allow) })
|
||
}
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const cmd = /** @type {Record<string, string>} */ (allow)[command]
|
||
const r = await captureExec(cmd, timeoutMs, { captureExit: true })
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'run_contract_checks') {
|
||
const profile = typeof args.profile === 'string' ? args.profile.trim() : ''
|
||
appendProgress('run_contract_checks ' + profile)
|
||
const mapping = {
|
||
core: 'node scripts/verify-kernel-seeder-parity.mjs && node scripts/verify-man-coverage.mjs',
|
||
docs: 'node scripts/verify-doc-links.mjs && node scripts/verify-doc-contracts.mjs',
|
||
parity: 'node scripts/verify-kernel-seeder-parity.mjs'
|
||
}
|
||
if (!Object.prototype.hasOwnProperty.call(mapping, profile)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_profile', profiles: Object.keys(mapping) })
|
||
}
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const cmd = /** @type {Record<string, string>} */ (mapping)[profile]
|
||
const r = await captureExec(cmd, 300000, { captureExit: true })
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'summarize_build_drift') {
|
||
appendProgress('summarize_build_drift')
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const r = await captureExec('git status --short', 60000)
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'get_hrpc_bridge_health') {
|
||
appendProgress('get_hrpc_bridge_health')
|
||
/** @type {Record<string, unknown>} */
|
||
const out = {
|
||
ok: true,
|
||
hostCapabilities: {
|
||
hrpcBridge:
|
||
typeof ctx.bareOsHostCapability === 'function'
|
||
? Boolean(ctx.bareOsHostCapability('hrpcBridge'))
|
||
: false
|
||
}
|
||
}
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
for (const p of ['/proc/bare_os/hrpc_route_table.json', '/proc/bare_os/hrpc_health.json']) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 6000)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'get_hrpc_allowlist_status') {
|
||
appendProgress('get_hrpc_allowlist_status')
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const r = await captureExec('hrpc probe', 60000)
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'emit_host_notification' || toolName === 'request_host_action') {
|
||
const cfg = configRef.current || {}
|
||
if (cfg && cfg.emergency_stop_mutations) {
|
||
return bareAgentJsonResult({ ok: false, error: 'emergency_stop_mutations_enabled' })
|
||
}
|
||
if (toolName === 'emit_host_notification' && !cfg.allow_host_notifications) {
|
||
return bareAgentJsonResult({ ok: false, error: 'host_notifications_disabled' })
|
||
}
|
||
if (toolName === 'request_host_action' && !cfg.allow_host_actions) {
|
||
return bareAgentJsonResult({ ok: false, error: 'host_actions_disabled' })
|
||
}
|
||
if (!cfg.allow_bridge_mutations) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bridge_mutations_disabled' })
|
||
}
|
||
if (typeof ctx.bareOsHrpcRequest !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'bareOsHrpcRequest unavailable' })
|
||
}
|
||
try {
|
||
if (toolName === 'emit_host_notification') {
|
||
const payload = {
|
||
title: String(args.title || '').slice(0, 200),
|
||
message: String(args.message || '').slice(0, 2000),
|
||
level: typeof args.level === 'string' ? args.level : 'info'
|
||
}
|
||
appendProgress('emit_host_notification ' + payload.title)
|
||
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_notify', payload)
|
||
return bareAgentJsonResult({ ok: true, result: res })
|
||
}
|
||
const action = String(args.action || '').trim()
|
||
if (!/^[a-zA-Z0-9._-]{1,64}$/.test(action)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_action' })
|
||
}
|
||
const payload =
|
||
args.payload && typeof args.payload === 'object' && !Array.isArray(args.payload)
|
||
? args.payload
|
||
: {}
|
||
appendProgress('request_host_action ' + action)
|
||
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_action', {
|
||
action,
|
||
payload
|
||
})
|
||
return bareAgentJsonResult({ ok: true, result: res })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_swarm_peers') {
|
||
appendProgress('get_swarm_peers')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const buf = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (!buf || !buf.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
}
|
||
const txt =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
/** @type {unknown} */
|
||
let j = null
|
||
try {
|
||
j = JSON.parse(txt)
|
||
} catch {
|
||
return bareAgentJsonResult({ ok: true, raw: txt.slice(0, 120_000) })
|
||
}
|
||
return bareAgentJsonResult({ ok: true, swarm: j })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_resource_limits') {
|
||
appendProgress('get_resource_limits')
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus !== 'function') {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'bareOsGetResourceStatus unavailable'
|
||
})
|
||
}
|
||
const r = ctx.bareOsGetResourceStatus()
|
||
return bareAgentJsonResult({ ok: true, resources: r })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'run_js_script_at_path') {
|
||
const scriptPath = typeof args.path === 'string' ? args.path : ''
|
||
if (!bareAgentPathAllowed(scriptPath) || scriptPath.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs?.readFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script_at_path ' + scriptPath)
|
||
try {
|
||
await vfs.readFile(scriptPath)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: 'cannot_read_script', detail: msg })
|
||
}
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'verification_hints') {
|
||
const topic = typeof args.topic === 'string' ? args.topic : ''
|
||
const pathsTouch =
|
||
typeof args.paths_touched === 'string' ? args.paths_touched : ''
|
||
appendProgress('verification_hints')
|
||
const combined = topic + ' ' + pathsTouch.replace(/,/g, ' ')
|
||
const hints = bareAgentVerificationHintsList(combined)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
scope_note:
|
||
'Suggested commands apply to the Bare OS git checkout on the host (npm/node at repo root). They do not run automatically.',
|
||
suggested_commands: hints
|
||
})
|
||
}
|
||
|
||
if (toolName === 'runtime_diagnostic_bundle') {
|
||
appendProgress('runtime_diagnostic_bundle')
|
||
/** @type {Record<string, unknown>} */
|
||
const bundle = {}
|
||
bundle.bareOsCtxApiVersion =
|
||
typeof ctx.bareOsCtxApiVersion !== 'undefined' ? ctx.bareOsCtxApiVersion : null
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus === 'function')
|
||
bundle.resources = ctx.bareOsGetResourceStatus()
|
||
} catch {
|
||
bundle.resources_error = true
|
||
}
|
||
/** @type {Record<string, unknown>} */
|
||
const procParts = {}
|
||
const list = BARE_AGENT_PROC_READ_ALLOWLIST
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
for (let i = 0; i < list.length; i++) {
|
||
const procPath = list[i]
|
||
try {
|
||
const buf = await vfs.readFile(procPath)
|
||
if (!buf || !buf.length) continue
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
if (t.length > 80_000) t = t.slice(0, 80_000) + '\n… truncated'
|
||
try {
|
||
procParts[procPath] = JSON.parse(t)
|
||
} catch {
|
||
procParts[procPath] = t
|
||
}
|
||
} catch {
|
||
/* missing path */
|
||
}
|
||
}
|
||
}
|
||
bundle.proc = procParts
|
||
return bareAgentJsonResult({ ok: true, bundle })
|
||
}
|
||
|
||
if (toolName === 'web_fetch') {
|
||
const url = typeof args.url === 'string' ? args.url : ''
|
||
let hostHint = ''
|
||
try {
|
||
hostHint = new URL(url).hostname
|
||
} catch {
|
||
hostHint = ''
|
||
}
|
||
appendProgress('web_fetch ' + (hostHint || url.slice(0, 80)))
|
||
try {
|
||
const out = await bareWebRunTool({
|
||
ctx,
|
||
url,
|
||
method: typeof args.method === 'string' ? args.method : undefined,
|
||
headers:
|
||
args.headers &&
|
||
typeof args.headers === 'object' &&
|
||
!Array.isArray(args.headers)
|
||
? /** @type {Record<string, unknown>} */ (args.headers)
|
||
: undefined,
|
||
body: typeof args.body === 'string' ? args.body : undefined,
|
||
content_type:
|
||
typeof args.content_type === 'string' ? args.content_type : undefined,
|
||
max_response_bytes:
|
||
typeof args.max_response_bytes === 'number'
|
||
? args.max_response_bytes
|
||
: undefined,
|
||
max_redirects:
|
||
typeof args.max_redirects === 'number' ? args.max_redirects : undefined,
|
||
timeout_ms:
|
||
typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
||
format: typeof args.format === 'string' ? args.format : undefined,
|
||
max_links:
|
||
typeof args.max_links === 'number' ? args.max_links : undefined,
|
||
signal
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
} catch (e) {
|
||
const msg = bareWebFmtErr(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200))
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} base
|
||
* @param {Record<string, unknown>} patch
|
||
*/
|
||
function bareAgentMergeConfigPatch(base, patch) {
|
||
const out = { ...base }
|
||
const keys = [
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'owner_name',
|
||
'agent_label',
|
||
'show_reasoning',
|
||
'reasoning_mode',
|
||
'reasoning_max_chars',
|
||
'reasoning_include_tools',
|
||
'allow_bridge_mutations',
|
||
'allow_host_notifications',
|
||
'allow_host_actions',
|
||
'emergency_stop_mutations',
|
||
'autonomous_mode_enabled',
|
||
'autonomous_max_runtime_ms',
|
||
'autonomous_completion_required_checks',
|
||
'autonomous_allow_paths',
|
||
'autonomous_deny_ops',
|
||
'autonomous_active',
|
||
'autonomous_started_at_ms',
|
||
'autonomous_stop_requested',
|
||
'autonomous_goal',
|
||
'autonomous_status',
|
||
'autonomous_last_error'
|
||
]
|
||
const numKeys = new Set([
|
||
'max_tokens',
|
||
'temperature',
|
||
'max_iterations',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'reasoning_max_chars',
|
||
'autonomous_max_runtime_ms',
|
||
'autonomous_started_at_ms'
|
||
])
|
||
for (const k of keys) {
|
||
if (Object.prototype.hasOwnProperty.call(patch, k)) {
|
||
/** @type {unknown} */
|
||
const v = patch[k]
|
||
if (numKeys.has(k)) {
|
||
const n = Number(v)
|
||
if (Number.isFinite(n)) out[k] = n
|
||
} else if (
|
||
k === 'autonomous_completion_required_checks' ||
|
||
k === 'autonomous_allow_paths' ||
|
||
k === 'autonomous_deny_ops'
|
||
) {
|
||
out[k] = Array.isArray(v) ? v.map((x) => String(x ?? '')).filter(Boolean) : out[k]
|
||
} else if (
|
||
k === 'stream' ||
|
||
k === 'allow_delete' ||
|
||
k === 'show_reasoning' ||
|
||
k === 'reasoning_include_tools' ||
|
||
k === 'allow_bridge_mutations' ||
|
||
k === 'allow_host_notifications' ||
|
||
k === 'allow_host_actions' ||
|
||
k === 'emergency_stop_mutations' ||
|
||
k === 'autonomous_mode_enabled' ||
|
||
k === 'autonomous_active' ||
|
||
k === 'autonomous_stop_requested'
|
||
) {
|
||
out[k] = Boolean(v)
|
||
} else if (k === 'reasoning_mode') {
|
||
const mode = String(v ?? '').trim().toLowerCase()
|
||
out[k] = mode === 'summary' || mode === 'trace' ? mode : 'off'
|
||
} else if (k === 'require_confirm_token') {
|
||
out[k] = String(v ?? '')
|
||
} else {
|
||
out[k] = String(v ?? '')
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
patch.extra_headers &&
|
||
typeof patch.extra_headers === 'object' &&
|
||
!Array.isArray(patch.extra_headers)
|
||
) {
|
||
out.extra_headers = { .../** @type {Record<string, string>} */ (patch.extra_headers) }
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSaveConfigFromTools(ctx, paths, config) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs?.writeFile) return
|
||
const json = JSON.stringify(config, null, 2) + '\n'
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(json)
|
||
: new TextEncoder().encode(json)
|
||
await vfs.writeFile(paths.config, body)
|
||
}
|